package detailed import ( "context" "fmt" "strings" opentracing "github.com/opentracing/opentracing-go" "github.com/weaveworks/scope/probe/awsecs" "github.com/weaveworks/scope/probe/docker" "github.com/weaveworks/scope/probe/kubernetes" "github.com/weaveworks/scope/probe/overlay" "github.com/weaveworks/scope/probe/process" "github.com/weaveworks/scope/render" "github.com/weaveworks/scope/report" ) // Shapes that are allowed const ( ImageNameNone = "" // Keys we use to render container names AmazonECSContainerNameLabel = "com.amazonaws.ecs.container-name" KubernetesContainerNameLabel = "io.kubernetes.container.name" MarathonAppIDEnv = "MARATHON_APP_ID" ) // NodeSummaryGroup is a topology-typed group of children for a Node. type NodeSummaryGroup struct { ID string `json:"id"` Label string `json:"label"` Nodes []NodeSummary `json:"nodes"` TopologyID string `json:"topologyId"` Columns []Column `json:"columns"` } // Column provides special json serialization for column ids, so they include // their label for the frontend. type Column struct { ID string `json:"id"` Label string `json:"label"` DefaultSort bool `json:"defaultSort"` Datatype string `json:"dataType"` } // BasicNodeSummary is basic summary information about a Node, // sufficient for rendering links to the node. type BasicNodeSummary struct { ID string `json:"id"` Label string `json:"label"` LabelMinor string `json:"labelMinor"` Rank string `json:"rank"` Shape string `json:"shape,omitempty"` Tag string `json:"tag,omitempty"` Stack bool `json:"stack,omitempty"` Pseudo bool `json:"pseudo,omitempty"` } // NodeSummary is summary information about a Node. type NodeSummary struct { BasicNodeSummary Metadata []report.MetadataRow `json:"metadata,omitempty"` Parents []Parent `json:"parents,omitempty"` Metrics []report.MetricRow `json:"metrics,omitempty"` Tables []report.Table `json:"tables,omitempty"` Adjacency report.IDList `json:"adjacency,omitempty"` } var renderers = map[string]func(BasicNodeSummary, report.Node) BasicNodeSummary{ render.Pseudo: pseudoNodeSummary, report.Process: processNodeSummary, report.Container: containerNodeSummary, report.ContainerImage: containerImageNodeSummary, report.Pod: podNodeSummary, report.Service: podGroupNodeSummary, report.Deployment: podGroupNodeSummary, report.DaemonSet: podGroupNodeSummary, report.StatefulSet: podGroupNodeSummary, report.CronJob: podGroupNodeSummary, report.Job: podGroupNodeSummary, report.ECSTask: ecsTaskNodeSummary, report.ECSService: ecsServiceNodeSummary, report.SwarmService: swarmServiceNodeSummary, report.Host: hostNodeSummary, report.Overlay: weaveNodeSummary, report.Endpoint: nil, // Do not render report.PersistentVolume: persistentVolumeNodeSummary, report.PersistentVolumeClaim: persistentVolumeClaimNodeSummary, report.StorageClass: storageClassNodeSummary, report.VolumeSnapshot: volumeSnapshotNodeSummary, report.VolumeSnapshotData: volumeSnapshotDataNodeSummary, } // For each report.Topology, map to a 'primary' API topology. This can then be used in a variety of places. var primaryAPITopology = map[string]string{ report.Process: "processes", report.Container: "containers", report.ContainerImage: "containers-by-image", report.Pod: "pods", report.Deployment: "kube-controllers", report.DaemonSet: "kube-controllers", report.StatefulSet: "kube-controllers", report.CronJob: "kube-controllers", report.Job: "kube-controllers", report.Service: "services", report.ECSTask: "ecs-tasks", report.ECSService: "ecs-services", report.SwarmService: "swarm-services", report.Host: "hosts", report.PersistentVolume: "pods", report.PersistentVolumeClaim: "pods", report.StorageClass: "pods", report.VolumeSnapshot: "pods", report.VolumeSnapshotData: "pods", } // MakeBasicNodeSummary returns a basic summary of a node, if // possible. This summary is sufficient for rendering links to the node. func MakeBasicNodeSummary(r report.Report, n report.Node) (BasicNodeSummary, bool) { summary := BasicNodeSummary{ // This is unlikely to look very good, but is a reasonable fallback ID: n.ID, Label: n.ID, Shape: report.Triangle, } if t, ok := r.Topology(n.Topology); ok { summary.Shape = t.GetShape() summary.Tag = t.Tag } // Do we have a renderer for the topology? if renderer, ok := renderers[n.Topology]; ok { if renderer == nil { // we don't want to render this return summary, false } return renderer(summary, n), true } // Is it a group topology? if strings.HasPrefix(n.Topology, "group:") { return groupNodeSummary(summary, r, n), true } // Is it any known topology? if _, ok := r.Topology(n.Topology); ok { // We should never get here, since all known topologies are in // 'renderers'. return summary, true } // We have no idea how to render this. return summary, false } // MakeNodeSummary summarizes a node, if possible. func MakeNodeSummary(rc RenderContext, n report.Node) (NodeSummary, bool) { base, ok := MakeBasicNodeSummary(rc.Report, n) if !ok { return NodeSummary{}, false } summary := NodeSummary{ BasicNodeSummary: base, Parents: Parents(rc.Report, n), Adjacency: n.Adjacency, } // Only include metadata, metrics, tables when it's not a group node if _, ok := n.LookupCounter(n.Topology); !ok { if topology, ok := rc.Topology(n.Topology); ok { summary.Metadata = topology.MetadataTemplates.MetadataRows(n) summary.Metrics = topology.MetricTemplates.MetricRows(n) summary.Tables = topology.TableTemplates.Tables(n) } } return RenderMetricURLs(summary, n, rc.Report, rc.MetricsGraphURL), true } // SummarizeMetrics returns a copy of the NodeSummary where the metrics are // replaced with their summaries func (n NodeSummary) SummarizeMetrics() NodeSummary { summarizedMetrics := make([]report.MetricRow, len(n.Metrics)) for i, m := range n.Metrics { summarizedMetrics[i] = m.Summary() } n.Metrics = summarizedMetrics return n } func pseudoNodeSummary(base BasicNodeSummary, n report.Node) BasicNodeSummary { pseudoID, _ := render.ParsePseudoNodeID(n.ID) base.Pseudo = true base.Rank = pseudoID switch { case n.ID == render.IncomingInternetID: // render as an internet node base.Label = render.InboundMajor base.LabelMinor = render.InboundMinor base.Shape = report.Cloud case n.ID == render.OutgoingInternetID: // render as an internet node base.Label = render.OutboundMajor base.LabelMinor = render.OutboundMinor base.Shape = report.Cloud case strings.HasPrefix(n.ID, render.ServiceNodeIDPrefix): // render as a known service node base.Label = n.ID[len(render.ServiceNodeIDPrefix):] base.LabelMinor = "" base.Shape = report.Cloud case strings.HasPrefix(n.ID, render.UncontainedIDPrefix): // render as an uncontained node base.Label = render.UncontainedMajor base.LabelMinor = n.ID[len(render.UncontainedIDPrefix):] base.Shape = report.Square base.Stack = true case strings.HasPrefix(n.ID, render.UnmanagedIDPrefix): // render as an unmanaged node base.Label = render.UnmanagedMajor base.LabelMinor = n.ID[len(render.UnmanagedIDPrefix):] base.Shape = report.Square base.Stack = true default: // try rendering it as an endpoint if _, addr, _, ok := report.ParseEndpointNodeID(n.ID); ok { base.Label = addr base.Shape = report.Circle } else { // last resort base.Label = pseudoID } } return base } func processNodeSummary(base BasicNodeSummary, n report.Node) BasicNodeSummary { var ( hostID, pid, _ = report.ParseProcessNodeID(n.ID) processName, _ = n.Latest.Lookup(process.Name) containerName, _ = n.Latest.Lookup(docker.ContainerName) ) switch { case processName != "" && containerName != "": base.Label = processName base.LabelMinor = fmt.Sprintf("%s (%s:%s)", hostID, containerName, pid) base.Rank = processName case processName != "": base.Label = processName base.LabelMinor = fmt.Sprintf("%s (%s)", hostID, pid) base.Rank = processName case containerName != "": base.Label = pid base.LabelMinor = fmt.Sprintf("%s (%s)", hostID, containerName) base.Rank = hostID default: base.Label = pid base.LabelMinor = hostID base.Rank = hostID } return base } func containerNodeSummary(base BasicNodeSummary, n report.Node) BasicNodeSummary { var ( containerName = getRenderableContainerName(n) hostName = report.ExtractHostID(n) imageName, _ = n.Latest.Lookup(docker.ImageName) ) base.Label = containerName base.LabelMinor = hostName if imageName != "" { base.Rank = docker.ImageNameWithoutTag(imageName) } else if hostName != "" { base.Rank = hostName } else { base.Rank = base.Label } return base } func containerImageNodeSummary(base BasicNodeSummary, n report.Node) BasicNodeSummary { var ( imageName, _ = n.Latest.Lookup(docker.ImageName) imageNameWithoutTag = docker.ImageNameWithoutTag(imageName) ) switch { case imageNameWithoutTag != "" && imageNameWithoutTag != ImageNameNone: base.Label = imageNameWithoutTag case imageName != "" && imageName != ImageNameNone: base.Label = imageName default: // The id can be an image id or an image name. Ideally we'd // truncate the former but not the latter, but short of // heuristic regexp match we cannot tell the difference. base.Label, _ = report.ParseContainerImageNodeID(n.ID) } base.LabelMinor = pluralize(n, report.Container, "container", "containers") base.Rank = base.Label base.Stack = true return base } func addKubernetesLabelAndRank(base BasicNodeSummary, n report.Node) BasicNodeSummary { var ( name, _ = n.Latest.Lookup(kubernetes.Name) namespace, _ = n.Latest.Lookup(kubernetes.Namespace) ) if name != "" { base.Label = name } else { base.Label, _, _ = report.ParseNodeID(n.ID) } base.Rank = namespace + "/" + base.Label return base } func podNodeSummary(base BasicNodeSummary, n report.Node) BasicNodeSummary { base = addKubernetesLabelAndRank(base, n) base.LabelMinor = pluralize(n, report.Container, "container", "containers") return base } var podGroupNodeTypeName = map[string]string{ report.Deployment: "Deployment", report.DaemonSet: "DaemonSet", report.StatefulSet: "StatefulSet", report.CronJob: "CronJob", report.Job: "Job", } func podGroupNodeSummary(base BasicNodeSummary, n report.Node) BasicNodeSummary { base = addKubernetesLabelAndRank(base, n) base.Stack = true // NB: pods are the highest aggregation level for which we display // counts. count := pluralize(n, report.Pod, "pod", "pods") if typeName, ok := podGroupNodeTypeName[n.Topology]; ok { base.LabelMinor = fmt.Sprintf("%s of %s", typeName, count) } else { base.LabelMinor = count } return base } func ecsTaskNodeSummary(base BasicNodeSummary, n report.Node) BasicNodeSummary { base.Label, _ = n.Latest.Lookup(awsecs.TaskFamily) if base.Label == "" { base.Label, _ = report.ParseECSTaskNodeID(n.ID) } return base } func ecsServiceNodeSummary(base BasicNodeSummary, n report.Node) BasicNodeSummary { _, base.Label, _ = report.ParseECSServiceNodeID(n.ID) base.Stack = true return base } func swarmServiceNodeSummary(base BasicNodeSummary, n report.Node) BasicNodeSummary { base.Label, _ = n.Latest.Lookup(docker.ServiceName) if base.Label == "" { base.Label, _ = report.ParseSwarmServiceNodeID(n.ID) } return base } func hostNodeSummary(base BasicNodeSummary, n report.Node) BasicNodeSummary { var ( hostname, _ = report.ParseHostNodeID(n.ID) parts = strings.SplitN(hostname, ".", 2) ) if len(parts) == 2 { base.Label, base.LabelMinor, base.Rank = parts[0], parts[1], parts[1] } else { base.Label = hostname } return base } func weaveNodeSummary(base BasicNodeSummary, n report.Node) BasicNodeSummary { var ( nickname, _ = n.Latest.Lookup(overlay.WeavePeerNickName) _, peerName = report.ParseOverlayNodeID(n.ID) ) if nickname != "" { base.Label = nickname } else { base.Label = peerName } base.LabelMinor = peerName return base } func persistentVolumeNodeSummary(base BasicNodeSummary, n report.Node) BasicNodeSummary { base = addKubernetesLabelAndRank(base, n) return base } func persistentVolumeClaimNodeSummary(base BasicNodeSummary, n report.Node) BasicNodeSummary { base = addKubernetesLabelAndRank(base, n) return base } func storageClassNodeSummary(base BasicNodeSummary, n report.Node) BasicNodeSummary { base = addKubernetesLabelAndRank(base, n) return base } func volumeSnapshotNodeSummary(base BasicNodeSummary, n report.Node) BasicNodeSummary { base = addKubernetesLabelAndRank(base, n) return base } func volumeSnapshotDataNodeSummary(base BasicNodeSummary, n report.Node) BasicNodeSummary { base = addKubernetesLabelAndRank(base, n) return base } // groupNodeSummary renders the summary for a group node. n.Topology is // expected to be of the form: group:container:hostname func groupNodeSummary(base BasicNodeSummary, r report.Report, n report.Node) BasicNodeSummary { base.Label, base.Rank = n.ID, n.ID if topology, _, ok := render.ParseGroupNodeTopology(n.Topology); ok { if t, ok := r.Topology(topology); ok { base.Shape = t.GetShape() base.Tag = t.Tag if t.Label != "" { base.LabelMinor = pluralize(n, topology, t.Label, t.LabelPlural) } } } base.Stack = true return base } func pluralize(n report.Node, key, singular, plural string) string { c, ok := n.LookupCounter(key) if !ok { c = 0 } if c == 1 { return fmt.Sprintf("%d %s", c, singular) } return fmt.Sprintf("%d %s", c, plural) } type nodeSummariesByID []NodeSummary func (s nodeSummariesByID) Len() int { return len(s) } func (s nodeSummariesByID) Swap(i, j int) { s[i], s[j] = s[j], s[i] } func (s nodeSummariesByID) Less(i, j int) bool { return s[i].ID < s[j].ID } // NodeSummaries is a set of NodeSummaries indexed by ID. type NodeSummaries map[string]NodeSummary // Summaries converts RenderableNodes into a set of NodeSummaries func Summaries(ctx context.Context, rc RenderContext, rns report.Nodes) NodeSummaries { span, ctx := opentracing.StartSpanFromContext(ctx, "detailed.Summaries") defer span.Finish() result := NodeSummaries{} for id, node := range rns { if summary, ok := MakeNodeSummary(rc, node); ok { for i, m := range summary.Metrics { summary.Metrics[i] = m.Summary() } result[id] = summary } } return result } // getRenderableContainerName obtains a user-friendly container name, to render in the UI func getRenderableContainerName(nmd report.Node) string { for _, key := range []string{ // Amazon's ecs-agent produces huge Docker container names, destructively // derived from mangling Container Definition names in Task // Definitions. // // However, the ecs-agent provides a label containing the original Container // Definition name. docker.LabelPrefix + AmazonECSContainerNameLabel, // Kubernetes also mangles its Docker container names and provides a // label with the original container name. However, note that this label // is only provided by Kubernetes versions >= 1.2 (see // https://github.com/kubernetes/kubernetes/pull/17234/ ) docker.LabelPrefix + KubernetesContainerNameLabel, // Marathon doesn't set any Docker labels and this is the only meaningful // attribute we can find to make Scope useful without Mesos plugin docker.EnvPrefix + MarathonAppIDEnv, docker.ContainerName, docker.ContainerHostname, } { if label, ok := nmd.Latest.Lookup(key); ok { return label } } containerID, _ := report.ParseContainerNodeID(nmd.ID) if len(containerID) > 12 { containerID = containerID[:12] } return containerID }