diff --git a/app/api_topologies.go b/app/api_topologies.go index d63edd77d4..fd68a2ac91 100644 --- a/app/api_topologies.go +++ b/app/api_topologies.go @@ -33,6 +33,7 @@ const ( weaveID = "weave" ecsTasksID = "ecs-tasks" ecsServicesID = "ecs-services" + swarmServicesID = "swarm-services" ) var ( @@ -252,6 +253,14 @@ func MakeRegistry() *Registry { Options: []APITopologyOptionGroup{unmanagedFilter}, HideIfEmpty: true, }, + APITopologyDesc{ + id: swarmServicesID, + renderer: render.SwarmServiceRenderer, + Name: "services", + Rank: 3, + Options: []APITopologyOptionGroup{unmanagedFilter}, + HideIfEmpty: true, + }, APITopologyDesc{ id: hostsID, renderer: render.HostRenderer, diff --git a/app/api_topologies_test.go b/app/api_topologies_test.go index 6ce9b49f15..c5dc75fc91 100644 --- a/app/api_topologies_test.go +++ b/app/api_topologies_test.go @@ -40,7 +40,7 @@ func TestAPITopology(t *testing.T) { if err := decoder.Decode(&topologies); err != nil { t.Fatalf("JSON parse error: %s", err) } - equals(t, 5, len(topologies)) + equals(t, 6, len(topologies)) for _, topology := range topologies { is200(t, ts, topology.URL) @@ -50,7 +50,7 @@ func TestAPITopology(t *testing.T) { } // TODO: add ECS nodes in report fixture - if topology.Name == "Tasks" { + if topology.Name == "Tasks" || topology.Name == "services" { continue } @@ -200,7 +200,7 @@ func TestAPITopologyAddsKubernetes(t *testing.T) { if err := decoder.Decode(&topologies); err != nil { t.Fatalf("JSON parse error: %s", err) } - equals(t, 5, len(topologies)) + equals(t, 6, len(topologies)) // Enable the kubernetes topologies rpt := report.MakeReport() @@ -234,7 +234,7 @@ func TestAPITopologyAddsKubernetes(t *testing.T) { if err := decoder.Decode(&topologies); err != nil { t.Fatalf("JSON parse error: %s", err) } - equals(t, 5, len(topologies)) + equals(t, 6, len(topologies)) found := false for _, topology := range topologies { diff --git a/probe/docker/reporter.go b/probe/docker/reporter.go index 44efb69e41..7b68e5eda5 100644 --- a/probe/docker/reporter.go +++ b/probe/docker/reporter.go @@ -21,6 +21,7 @@ const ( ImageLabelPrefix = "docker_image_label_" IsInHostNetwork = "docker_is_in_host_network" ImageTableID = "image_table" + ServiceName = "service_name" ) // Exposed for testing @@ -132,6 +133,10 @@ var ( Rank: 8, }, } + + SwarmServiceMetadataTemplates = report.MetadataTemplates{ + ServiceName: {ID: ServiceName, Label: "Service Name", From: report.FromLatest, Priority: 0}, + } ) // Reporter generate Reports containing Container and ContainerImage topologies @@ -177,6 +182,7 @@ func (r *Reporter) Report() (report.Report, error) { result.Container = result.Container.Merge(r.containerTopology(localAddrs)) result.ContainerImage = result.ContainerImage.Merge(r.containerImageTopology()) result.Overlay = result.Overlay.Merge(r.overlayTopology()) + result.SwarmService = result.SwarmService.Merge(r.swarmServiceTopology()) return result, nil } @@ -298,6 +304,10 @@ func (r *Reporter) overlayTopology() report.Topology { return report.MakeTopology().AddNode(node) } +func (r *Reporter) swarmServiceTopology() report.Topology { + return report.MakeTopology().WithMetadataTemplates(SwarmServiceMetadataTemplates) +} + // Docker sometimes prefixes ids with a "type" annotation, but it renders a bit // ugly and isn't necessary, so we should strip it off func trimImageID(id string) string { diff --git a/probe/docker/tagger.go b/probe/docker/tagger.go index 79f515ca4a..df4e009309 100644 --- a/probe/docker/tagger.go +++ b/probe/docker/tagger.go @@ -2,6 +2,7 @@ package docker import ( "strconv" + "strings" "github.com/weaveworks/scope/probe/process" "github.com/weaveworks/scope/report" @@ -21,6 +22,7 @@ var ( // Tagger is a tagger that tags Docker container information to process // nodes that have a PID. +// It also populates the SwarmService topology if any of the associated docker labels are present. type Tagger struct { registry Registry procWalker process.Walker @@ -44,6 +46,31 @@ func (t *Tagger) Tag(r report.Report) (report.Report, error) { return report.MakeReport(), err } t.tag(tree, &r.Process) + + // Scan for Swarm service info + for containerID, container := range r.Container.Nodes { + serviceID, ok := container.Latest.Lookup(LabelPrefix + "com.docker.swarm.service.id") + if !ok { + continue + } + serviceName, ok := container.Latest.Lookup(LabelPrefix + "com.docker.swarm.service.name") + if !ok { + continue + } + + if strings.HasPrefix(serviceName, "dockerswarm_") { + serviceName = serviceName[len("dockerswarm_"):] + } + + nodeID := report.MakeSwarmServiceNodeID(serviceID) + node := report.MakeNodeWith(nodeID, map[string]string{ + ServiceName: serviceName, + }) + r.SwarmService = r.SwarmService.AddNode(node) + + r.Container.Nodes[containerID] = container.WithParents(container.Parents.Add(report.SwarmService, report.MakeStringSet(nodeID))) + } + return r, nil } diff --git a/render/detailed/parents.go b/render/detailed/parents.go index b749e13265..5fce99de52 100644 --- a/render/detailed/parents.go +++ b/render/detailed/parents.go @@ -4,6 +4,7 @@ import ( "sort" "github.com/weaveworks/scope/probe/awsecs" + "github.com/weaveworks/scope/probe/docker" "github.com/weaveworks/scope/probe/host" "github.com/weaveworks/scope/probe/kubernetes" "github.com/weaveworks/scope/report" @@ -27,6 +28,7 @@ var ( report.Service: kubernetesParentLabel, report.ECSTask: latestLookup(awsecs.TaskFamily), report.ECSService: ecsServiceParentLabel, + report.SwarmService: latestLookup(docker.ServiceName), report.ContainerImage: containerImageParentLabel, report.Host: latestLookup(host.HostName), } diff --git a/render/detailed/summary.go b/render/detailed/summary.go index cd7f457b19..63d4620da3 100644 --- a/render/detailed/summary.go +++ b/render/detailed/summary.go @@ -71,6 +71,7 @@ var renderers = map[string]func(NodeSummary, report.Node) (NodeSummary, bool){ report.ReplicaSet: podGroupNodeSummary, report.ECSTask: ecsTaskNodeSummary, report.ECSService: ecsServiceNodeSummary, + report.SwarmService: swarmServiceNodeSummary, report.Host: hostNodeSummary, report.Overlay: weaveNodeSummary, report.Endpoint: nil, // Do not render @@ -93,6 +94,7 @@ var primaryAPITopology = map[string]string{ report.Service: "services", report.ECSTask: "ecs-tasks", report.ECSService: "ecs-services", + report.SwarmService: "swarm-services", report.Host: "hosts", } @@ -276,6 +278,11 @@ func ecsServiceNodeSummary(base NodeSummary, n report.Node) (NodeSummary, bool) return base, true } +func swarmServiceNodeSummary(base NodeSummary, n report.Node) (NodeSummary, bool) { + base.Label, _ = n.Latest.Lookup(docker.ServiceName) + return base, true +} + func hostNodeSummary(base NodeSummary, n report.Node) (NodeSummary, bool) { var ( hostname, _ = n.Latest.Lookup(host.HostName) diff --git a/render/selectors.go b/render/selectors.go index b4894b9c72..eb369f9322 100644 --- a/render/selectors.go +++ b/render/selectors.go @@ -33,5 +33,6 @@ var ( SelectReplicaSet = TopologySelector(report.ReplicaSet) SelectECSTask = TopologySelector(report.ECSTask) SelectECSService = TopologySelector(report.ECSService) + SelectSwarmService = TopologySelector(report.SwarmService) SelectOverlay = TopologySelector(report.Overlay) ) diff --git a/render/swarm.go b/render/swarm.go new file mode 100644 index 0000000000..b77177a5ea --- /dev/null +++ b/render/swarm.go @@ -0,0 +1,26 @@ +package render + +import ( + "github.com/weaveworks/scope/report" +) + +// SwarmServiceRenderer is a Renderer for Docker Swarm services +var SwarmServiceRenderer = ConditionalRenderer(renderSwarmTopologies, + MakeMap( + PropagateSingleMetrics(report.Container), + MakeReduce( + MakeMap( + Map2Parent(report.SwarmService, UnmanagedID, nil), + MakeFilter( + IsRunning, + ContainerWithImageNameRenderer, + ), + ), + SelectSwarmService, + ), + ), +) + +func renderSwarmTopologies(rpt report.Report) bool { + return len(rpt.SwarmService.Nodes) >= 1 +} diff --git a/report/id.go b/report/id.go index f243dafe82..6e22a23b8b 100644 --- a/report/id.go +++ b/report/id.go @@ -130,6 +130,12 @@ var ( // ParseECSTaskNodeID parses a replica set node ID ParseECSTaskNodeID = parseSingleComponentID("ecs_task") + + // MakeSwarmServiceNodeID produces a replica set node ID from its composite parts. + MakeSwarmServiceNodeID = makeSingleComponentID("swarm_service") + + // ParseSwarmServiceNodeID parses a replica set node ID + ParseSwarmServiceNodeID = parseSingleComponentID("swarm_service") ) // makeSingleComponentID makes a single-component node id encoder diff --git a/report/report.go b/report/report.go index a95f0a9b1e..aac6b76204 100644 --- a/report/report.go +++ b/report/report.go @@ -24,6 +24,7 @@ const ( Overlay = "overlay" ECSService = "ecs_service" ECSTask = "ecs_task" + SwarmService = "swarm_service" // Shapes used for different nodes Circle = "circle" @@ -92,6 +93,11 @@ type Report struct { // Metadata is limited for now, more to come later. Edges are not present. ECSService Topology + // Swarm Service nodes are Docker Swarm services, which represent a specification for a + // group of tasks (either one per host, or a desired count). + // Edges are not present. + SwarmService Topology + // Overlay nodes are active peers in any software-defined network that's // overlaid on the infrastructure. The information is scraped by polling // their status endpoints. Edges could be present, but aren't currently. @@ -170,6 +176,10 @@ func MakeReport() Report { WithShape(Heptagon). WithLabel("service", "services"), + SwarmService: MakeTopology(). + WithShape(Heptagon). + WithLabel("service", "services"), + Sampling: Sampling{}, Window: 0, Plugins: xfer.MakePluginSpecs(), @@ -192,6 +202,7 @@ func (r *Report) TopologyMap() map[string]*Topology { Overlay: &r.Overlay, ECSTask: &r.ECSTask, ECSService: &r.ECSService, + SwarmService: &r.SwarmService, } } @@ -253,6 +264,7 @@ func (r *Report) WalkPairedTopologies(o *Report, f func(*Topology, *Topology)) { f(&r.Overlay, &o.Overlay) f(&r.ECSTask, &o.ECSTask) f(&r.ECSService, &o.ECSService) + f(&r.SwarmService, &o.SwarmService) } // Topology gets a topology by name