From be1d1d52d0aa2be17a483e917e88a56bd362ef7e Mon Sep 17 00:00:00 2001 From: Paul Bellamy Date: Thu, 27 Aug 2015 11:17:08 +0100 Subject: [PATCH] Basic Kubernetes integration * Added helper for installing scope on gcloud * Added topologies Pods and Pods-by-Service * Uses k8s.io/kubernetes/pkg/client/cache for the client * Filter kube-system nodes by default * Only show the k8s topologies if we've received a non-empty k8s report --- Makefile | 2 +- app/api_topologies.go | 35 ++--- app/api_topologies_test.go | 61 ++++++++ app/router.go | 163 +++++++++++++++------ app/scope_test.go | 6 +- experimental/kubernetes/install-scope.bash | 20 +++ probe/kubernetes/client.go | 91 ++++++++++++ probe/kubernetes/pod.go | 88 +++++++++++ probe/kubernetes/reporter.go | 62 ++++++++ probe/kubernetes/reporter_test.go | 150 +++++++++++++++++++ probe/kubernetes/service.go | 59 ++++++++ probe/main.go | 13 ++ render/detailed_node_test.go | 2 + render/expected/expected.go | 135 +++++++++++++++++ render/mapping.go | 124 ++++++++++++++++ render/mapping_test.go | 19 +++ render/render.go | 7 + render/selectors.go | 10 ++ render/topologies.go | 32 ++++ render/topologies_test.go | 34 +++++ report/id.go | 10 ++ report/report.go | 18 +++ test/report_fixture.go | 45 +++++- 23 files changed, 1112 insertions(+), 74 deletions(-) create mode 100755 experimental/kubernetes/install-scope.bash create mode 100644 probe/kubernetes/client.go create mode 100644 probe/kubernetes/pod.go create mode 100644 probe/kubernetes/reporter.go create mode 100644 probe/kubernetes/reporter_test.go create mode 100644 probe/kubernetes/service.go diff --git a/Makefile b/Makefile index 07322f730..e196b4cd9 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,7 @@ $(RUNSVINIT): vendor/runsvinit/*.go $(APP_EXE): app/*.go render/*.go report/*.go xfer/*.go -$(PROBE_EXE): probe/*.go probe/docker/*.go probe/endpoint/*.go probe/host/*.go probe/process/*.go probe/overlay/*.go report/*.go xfer/*.go +$(PROBE_EXE): probe/*.go probe/docker/*.go probe/kubernetes/*.go probe/endpoint/*.go probe/host/*.go probe/process/*.go probe/overlay/*.go report/*.go xfer/*.go $(APP_EXE) $(PROBE_EXE): go get -d -tags netgo ./$(@D) diff --git a/app/api_topologies.go b/app/api_topologies.go index 6a3af6f1d..e5495a772 100644 --- a/app/api_topologies.go +++ b/app/api_topologies.go @@ -38,36 +38,25 @@ func makeTopologyList(rep xfer.Reporter) func(w http.ResponseWriter, r *http.Req rpt = rep.Report() topologies = []APITopologyDesc{} ) - for name, def := range topologyRegistry { - // Don't show sub-topologies at the top level. - if def.parent != "" { - continue + topologyRegistry.walk(func(name string, def topologyView, subDefs map[string]topologyView) { + describedSubDefs := []APITopologyDesc{} + for subName, subDef := range subDefs { + describedSubDefs = append(describedSubDefs, APITopologyDesc{ + Name: subDef.human, + URL: "/api/topology/" + subName, + Options: makeTopologyOptions(subDef), + Stats: stats(subDef.renderer, rpt), + }) } - decorateTopologyForRequest(r, &def) - - // Collect all sub-topologies of this one, depth=1 only. - subTopologies := []APITopologyDesc{} - for subName, subDef := range topologyRegistry { - if subDef.parent == name { - decorateTopologyForRequest(r, &subDef) - subTopologies = append(subTopologies, APITopologyDesc{ - Name: subDef.human, - URL: "/api/topology/" + subName, - Options: makeTopologyOptions(subDef), - Stats: stats(subDef.renderer, rpt), - }) - } - } - - // Append. topologies = append(topologies, APITopologyDesc{ Name: def.human, URL: "/api/topology/" + name, - SubTopologies: subTopologies, + SubTopologies: describedSubDefs, Options: makeTopologyOptions(def), Stats: stats(def.renderer, rpt), }) - } + }) + respondWith(w, http.StatusOK, topologies) } } diff --git a/app/api_topologies_test.go b/app/api_topologies_test.go index 3d8494a1c..3587232f0 100644 --- a/app/api_topologies_test.go +++ b/app/api_topologies_test.go @@ -1,9 +1,17 @@ package main import ( + "bytes" + "encoding/gob" "encoding/json" "net/http/httptest" "testing" + + "k8s.io/kubernetes/pkg/api" + + "github.com/weaveworks/scope/probe/kubernetes" + "github.com/weaveworks/scope/report" + "github.com/weaveworks/scope/test" ) func TestAPITopology(t *testing.T) { @@ -38,3 +46,56 @@ func TestAPITopology(t *testing.T) { } } } + +func TestAPITopologyAddsKubernetes(t *testing.T) { + ts := httptest.NewServer(Router(StaticReport{})) + defer ts.Close() + + body := getRawJSON(t, ts, "/api/topology") + + var topologies []APITopologyDesc + if err := json.Unmarshal(body, &topologies); err != nil { + t.Fatalf("JSON parse error: %s", err) + } + equals(t, 3, len(topologies)) + + // Enable the kubernetes topologies + rpt := report.MakeReport() + rpt.Pod = report.MakeTopology() + rpt.Pod.Nodes[test.ClientPodNodeID] = kubernetes.NewPod(&api.Pod{ + ObjectMeta: api.ObjectMeta{ + Name: "pong-a", + Namespace: "ping", + Labels: map[string]string{"ponger": "true"}, + }, + Status: api.PodStatus{ + HostIP: "1.2.3.4", + ContainerStatuses: []api.ContainerStatus{ + {ContainerID: "container1"}, + {ContainerID: "container2"}, + }, + }, + }).GetNode() + buf := &bytes.Buffer{} + if err := gob.NewEncoder(buf).Encode(rpt); err != nil { + t.Fatalf("GOB encoding error: %s", err) + } + checkRequest(t, ts, "POST", "/api/report", buf.Bytes()) + + body = getRawJSON(t, ts, "/api/topology") + if err := json.Unmarshal(body, &topologies); err != nil { + t.Fatalf("JSON parse error: %s", err) + } + equals(t, 4, len(topologies)) + + found := false + for _, topology := range topologies { + if topology.Name == "Pods" { + found = true + break + } + } + if !found { + t.Error("Could not find pods topology") + } +} diff --git a/app/router.go b/app/router.go index 7e3a0323f..a8a5c72b0 100644 --- a/app/router.go +++ b/app/router.go @@ -6,6 +6,7 @@ import ( "net/http" "net/url" "strings" + "sync" "github.com/PuerkitoBio/ghost/handlers" "github.com/gorilla/mux" @@ -96,6 +97,9 @@ func makeReportPostHandler(a xfer.Adder) http.HandlerFunc { return } a.Add(rpt) + if len(rpt.Pod.Nodes) > 0 { + enableKubernetesTopologies() + } w.WriteHeader(http.StatusOK) } } @@ -113,7 +117,7 @@ func decorateTopologyForRequest(r *http.Request, topology *topologyView) { func captureTopology(rep xfer.Reporter, f func(xfer.Reporter, topologyView, http.ResponseWriter, *http.Request)) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - topology, ok := topologyRegistry[mux.Vars(r)["topology"]] + topology, ok := topologyRegistry.get(mux.Vars(r)["topology"]) if !ok { http.NotFound(w, r) return @@ -133,50 +137,98 @@ func apiHandler(w http.ResponseWriter, r *http.Request) { respondWith(w, http.StatusOK, APIDetails{ID: uniqueID, Version: version}) } +// registry is a threadsafe store of the available topologies +type registry struct { + sync.RWMutex + items map[string]topologyView +} + +func (r *registry) add(ts map[string]topologyView) { + r.Lock() + defer r.Unlock() + result := map[string]topologyView{} + for name, t := range r.items { + result[name] = t + } + for name, t := range ts { + result[name] = t + } + r.items = result +} + +func (r *registry) get(name string) (topologyView, bool) { + r.RLock() + defer r.RUnlock() + t, ok := r.items[name] + return t, ok +} + +func (r *registry) walk(f func(string, topologyView, map[string]topologyView)) { + r.RLock() + defer r.RUnlock() + for name, def := range r.items { + if def.parent != "" { + continue + } + + subDefs := map[string]topologyView{} + for subName, subDef := range r.items { + if subDef.parent != name { + continue + } + subDefs[subName] = subDef + } + + f(name, def, subDefs) + } +} + // Topology option labels should tell the current state. The first item must // be the verb to get to that state -var topologyRegistry = map[string]topologyView{ - "applications": { - human: "Applications", - parent: "", - renderer: render.FilterUnconnected(render.ProcessWithContainerNameRenderer), - options: optionParams{"unconnected": { - // Show the user why there are filtered nodes in this view. - // Don't give them the option to show those nodes. - {"hide", "Unconnected nodes hidden", true, nop}, - }}, - }, - "applications-by-name": { - human: "by name", - parent: "applications", - renderer: render.FilterUnconnected(render.ProcessNameRenderer), - options: optionParams{"unconnected": { - // Ditto above. - {"hide", "Unconnected nodes hidden", true, nop}, - }}, - }, - "containers": { - human: "Containers", - parent: "", - renderer: render.ContainerWithImageNameRenderer, - options: optionParams{"system": { - {"show", "System containers shown", false, nop}, - {"hide", "System containers hidden", true, render.FilterSystem}, - }}, - }, - "containers-by-image": { - human: "by image", - parent: "containers", - renderer: render.ContainerImageRenderer, - options: optionParams{"system": { - {"show", "System containers shown", false, nop}, - {"hide", "System containers hidden", true, render.FilterSystem}, - }}, - }, - "hosts": { - human: "Hosts", - parent: "", - renderer: render.HostRenderer, +var topologyRegistry = ®istry{ + items: map[string]topologyView{ + "applications": { + human: "Applications", + parent: "", + renderer: render.FilterUnconnected(render.ProcessWithContainerNameRenderer), + options: optionParams{"unconnected": { + // Show the user why there are filtered nodes in this view. + // Don't give them the option to show those nodes. + {"hide", "Unconnected nodes hidden", true, nop}, + }}, + }, + "applications-by-name": { + human: "by name", + parent: "applications", + renderer: render.FilterUnconnected(render.ProcessNameRenderer), + options: optionParams{"unconnected": { + // Ditto above. + {"hide", "Unconnected nodes hidden", true, nop}, + }}, + }, + "containers": { + human: "Containers", + parent: "", + renderer: render.ContainerWithImageNameRenderer, + options: optionParams{"system": { + {"show", "System containers shown", false, nop}, + {"hide", "System containers hidden", true, render.FilterSystem}, + }}, + }, + "containers-by-image": { + human: "by image", + parent: "containers", + renderer: render.ContainerImageRenderer, + options: optionParams{"system": { + {"show", "System containers shown", false, nop}, + {"hide", "System containers hidden", true, render.FilterSystem}, + }}, + }, + "hosts": { + human: "Hosts", + parent: "", + renderer: render.HostRenderer, + }, }, } @@ -197,3 +249,28 @@ type optionValue struct { } func nop(r render.Renderer) render.Renderer { return r } + +func enableKubernetesTopologies() { + topologyRegistry.add(kubernetesTopologies) +} + +var kubernetesTopologies = map[string]topologyView{ + "pods": { + human: "Pods", + parent: "", + renderer: render.PodRenderer, + options: optionParams{"system": { + {"show", "System containers shown", false, nop}, + {"hide", "System containers hidden", true, render.FilterSystem}, + }}, + }, + "pods-by-service": { + human: "by service", + parent: "pods", + renderer: render.PodServiceRenderer, + options: optionParams{"system": { + {"show", "System containers shown", false, nop}, + {"hide", "System containers hidden", true, render.FilterSystem}, + }}, + }, +} diff --git a/app/scope_test.go b/app/scope_test.go index 9cfb35212..8c166f941 100644 --- a/app/scope_test.go +++ b/app/scope_test.go @@ -50,19 +50,19 @@ func checkRequest(t *testing.T, ts *httptest.Server, method, path string, body [ } req, err := http.NewRequest(method, fullPath, bodyReader) if err != nil { - t.Fatalf("Error getting %s: %s", path, err) + t.Fatalf("Error getting %s: %s %s", method, path, err) } client := &http.Client{} res, err := client.Do(req) if err != nil { - t.Fatalf("Error getting %s: %s", path, err) + t.Fatalf("Error getting %s %s: %s", method, path, err) } body, err = ioutil.ReadAll(res.Body) res.Body.Close() if err != nil { - t.Fatalf("%s body read error: %s", path, err) + t.Fatalf("%s %s body read error: %s", method, path, err) } return res, body } diff --git a/experimental/kubernetes/install-scope.bash b/experimental/kubernetes/install-scope.bash new file mode 100755 index 000000000..d791db252 --- /dev/null +++ b/experimental/kubernetes/install-scope.bash @@ -0,0 +1,20 @@ +#!/bin/bash + +export SSH_AUTH_SOCK= + +remote() { + host=$1 + shift + echo "[$host] Running: $@" >&2 + gcloud compute ssh $host --command "$@" +} + +for h in $(gcloud compute instances list | grep -v NAME | awk '{print $1}') ; do + cat ~/work/weave/repos/scope/scope.tar | remote $h "sudo docker load" + cat ~/work/weave/repos/scope/scope | remote $h "sudo tee /usr/local/bin/scope >/dev/null; sudo chmod a+x /usr/local/bin/scope" + if $(echo $h | grep -q "master") ; then + remote $h "sudo scope stop ; sudo DOCKER_BRIDGE=cbr0 scope launch --probe.docker.bridge cbr0 --probe.kubernetes true" + else + remote $h "sudo scope stop ; sudo DOCKER_BRIDGE=cbr0 scope launch --no-app --probe.docker.bridge cbr0 kubernetes-master" + fi +done diff --git a/probe/kubernetes/client.go b/probe/kubernetes/client.go new file mode 100644 index 000000000..5a4f4e1d9 --- /dev/null +++ b/probe/kubernetes/client.go @@ -0,0 +1,91 @@ +package kubernetes + +import ( + "time" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/client/cache" + "k8s.io/kubernetes/pkg/client/unversioned" + "k8s.io/kubernetes/pkg/fields" + "k8s.io/kubernetes/pkg/labels" +) + +// These constants are keys used in node metadata +const ( + Namespace = "kubernetes_namespace" +) + +// Client keeps track of running kubernetes pods and services +type Client interface { + Stop() + WalkPods(f func(Pod) error) error + WalkServices(f func(Service) error) error +} + +type client struct { + quit chan struct{} + client cache.Getter + podReflector *cache.Reflector + serviceReflector *cache.Reflector + podStore *cache.StoreToPodLister + serviceStore *cache.StoreToServiceLister +} + +// NewClient returns a usable Client. Don't forget to Stop it. +func NewClient(addr string, resyncPeriod time.Duration) (Client, error) { + c, err := unversioned.New(&unversioned.Config{Host: addr}) + if err != nil { + return nil, err + } + + podListWatch := cache.NewListWatchFromClient(c, "pods", api.NamespaceAll, fields.Everything()) + podStore := cache.NewStore(cache.MetaNamespaceKeyFunc) + podReflector := cache.NewReflector(podListWatch, &api.Pod{}, podStore, resyncPeriod) + + serviceListWatch := cache.NewListWatchFromClient(c, "services", api.NamespaceAll, fields.Everything()) + serviceStore := cache.NewStore(cache.MetaNamespaceKeyFunc) + serviceReflector := cache.NewReflector(serviceListWatch, &api.Service{}, serviceStore, resyncPeriod) + + quit := make(chan struct{}) + podReflector.RunUntil(quit) + serviceReflector.RunUntil(quit) + + return &client{ + quit: quit, + client: c, + podReflector: podReflector, + podStore: &cache.StoreToPodLister{Store: podStore}, + serviceReflector: serviceReflector, + serviceStore: &cache.StoreToServiceLister{Store: serviceStore}, + }, nil +} + +func (c *client) WalkPods(f func(Pod) error) error { + pods, err := c.podStore.List(labels.Everything()) + if err != nil { + return err + } + for _, pod := range pods { + if err := f(NewPod(pod)); err != nil { + return err + } + } + return nil +} + +func (c *client) WalkServices(f func(Service) error) error { + list, err := c.serviceStore.List() + if err != nil { + return err + } + for i := range list.Items { + if err := f(NewService(&(list.Items[i]))); err != nil { + return err + } + } + return nil +} + +func (c *client) Stop() { + close(c.quit) +} diff --git a/probe/kubernetes/pod.go b/probe/kubernetes/pod.go new file mode 100644 index 000000000..6cf86a205 --- /dev/null +++ b/probe/kubernetes/pod.go @@ -0,0 +1,88 @@ +package kubernetes + +import ( + "strings" + "time" + + "github.com/weaveworks/scope/report" + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/labels" +) + +// These constants are keys used in node metadata +const ( + PodID = "kubernetes_pod_id" + PodName = "kubernetes_pod_name" + PodCreated = "kubernetes_pod_created" + PodContainerIDs = "kubernetes_pod_container_ids" + ServiceIDs = "kubernetes_service_ids" +) + +// Pod represents a Kubernetes pod +type Pod interface { + ID() string + Name() string + Namespace() string + ContainerIDs() []string + Created() string + AddServiceID(id string) + Labels() labels.Labels + GetNode() report.Node +} + +type pod struct { + *api.Pod + serviceIDs []string + Node *api.Node +} + +// NewPod creates a new Pod +func NewPod(p *api.Pod) Pod { + return &pod{Pod: p} +} + +func (p *pod) ID() string { + return p.ObjectMeta.Namespace + "/" + p.ObjectMeta.Name +} + +func (p *pod) Name() string { + return p.ObjectMeta.Name +} + +func (p *pod) Namespace() string { + return p.ObjectMeta.Namespace +} + +func (p *pod) Created() string { + return p.ObjectMeta.CreationTimestamp.Format(time.RFC822) +} + +func (p *pod) ContainerIDs() []string { + ids := []string{} + for _, container := range p.Status.ContainerStatuses { + ids = append(ids, strings.TrimPrefix(container.ContainerID, "docker://")) + } + return ids +} + +func (p *pod) Labels() labels.Labels { + return labels.Set(p.ObjectMeta.Labels) +} + +func (p *pod) AddServiceID(id string) { + p.serviceIDs = append(p.serviceIDs, id) +} + +func (p *pod) GetNode() report.Node { + n := report.MakeNodeWith(map[string]string{ + PodID: p.ID(), + PodName: p.Name(), + Namespace: p.Namespace(), + PodCreated: p.Created(), + PodContainerIDs: strings.Join(p.ContainerIDs(), " "), + }) + if len(p.serviceIDs) > 0 { + n.Metadata[ServiceIDs] = strings.Join(p.serviceIDs, " ") + } + return n +} diff --git a/probe/kubernetes/reporter.go b/probe/kubernetes/reporter.go new file mode 100644 index 000000000..766083c9c --- /dev/null +++ b/probe/kubernetes/reporter.go @@ -0,0 +1,62 @@ +package kubernetes + +import ( + "github.com/weaveworks/scope/report" +) + +// Reporter generate Reports containing Container and ContainerImage topologies +type Reporter struct { + client Client +} + +// NewReporter makes a new Reporter +func NewReporter(client Client) *Reporter { + return &Reporter{ + client: client, + } +} + +// Report generates a Report containing Container and ContainerImage topologies +func (r *Reporter) Report() (report.Report, error) { + result := report.MakeReport() + serviceTopology, services, err := r.serviceTopology() + if err != nil { + return result, err + } + podTopology, err := r.podTopology(services) + if err != nil { + return result, err + } + result.Service = result.Service.Merge(serviceTopology) + result.Pod = result.Pod.Merge(podTopology) + return result, nil +} + +func (r *Reporter) serviceTopology() (report.Topology, []Service, error) { + var ( + result = report.MakeTopology() + services = []Service{} + ) + err := r.client.WalkServices(func(s Service) error { + nodeID := report.MakeServiceNodeID(s.Namespace(), s.Name()) + result = result.AddNode(nodeID, s.GetNode()) + services = append(services, s) + return nil + }) + return result, services, err +} + +func (r *Reporter) podTopology(services []Service) (report.Topology, error) { + result := report.MakeTopology() + err := r.client.WalkPods(func(p Pod) error { + for _, service := range services { + if service.Selector().Matches(p.Labels()) { + p.AddServiceID(service.ID()) + } + } + nodeID := report.MakePodNodeID(p.Namespace(), p.Name()) + result = result.AddNode(nodeID, p.GetNode()) + return nil + }) + return result, err +} diff --git a/probe/kubernetes/reporter_test.go b/probe/kubernetes/reporter_test.go new file mode 100644 index 000000000..5f319bbb9 --- /dev/null +++ b/probe/kubernetes/reporter_test.go @@ -0,0 +1,150 @@ +package kubernetes_test + +import ( + "reflect" + "testing" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/unversioned" + + "github.com/weaveworks/scope/probe/kubernetes" + "github.com/weaveworks/scope/report" + "github.com/weaveworks/scope/test" +) + +var ( + podTypeMeta = unversioned.TypeMeta{ + Kind: "Pod", + APIVersion: "v1", + } + apiPod1 = api.Pod{ + TypeMeta: podTypeMeta, + ObjectMeta: api.ObjectMeta{ + Name: "pong-a", + Namespace: "ping", + CreationTimestamp: unversioned.Now(), + Labels: map[string]string{"ponger": "true"}, + }, + Status: api.PodStatus{ + HostIP: "1.2.3.4", + ContainerStatuses: []api.ContainerStatus{ + {ContainerID: "container1"}, + {ContainerID: "container2"}, + }, + }, + } + apiPod2 = api.Pod{ + TypeMeta: podTypeMeta, + ObjectMeta: api.ObjectMeta{ + Name: "pong-b", + Namespace: "ping", + CreationTimestamp: unversioned.Now(), + Labels: map[string]string{"ponger": "true"}, + }, + Status: api.PodStatus{ + HostIP: "1.2.3.4", + ContainerStatuses: []api.ContainerStatus{ + {ContainerID: "container3"}, + {ContainerID: "container4"}, + }, + }, + } + apiService1 = api.Service{ + TypeMeta: unversioned.TypeMeta{ + Kind: "Service", + APIVersion: "v1", + }, + ObjectMeta: api.ObjectMeta{ + Name: "pongservice", + Namespace: "ping", + CreationTimestamp: unversioned.Now(), + }, + Spec: api.ServiceSpec{ + Type: api.ServiceTypeLoadBalancer, + ClusterIP: "10.0.1.1", + Ports: []api.ServicePort{ + {Protocol: "TCP", Port: 6379}, + }, + Selector: map[string]string{"ponger": "true"}, + }, + Status: api.ServiceStatus{ + LoadBalancer: api.LoadBalancerStatus{ + Ingress: []api.LoadBalancerIngress{ + {IP: "10.0.1.2"}, + }, + }, + }, + } + pod1 = kubernetes.NewPod(&apiPod1) + pod2 = kubernetes.NewPod(&apiPod2) + service1 = kubernetes.NewService(&apiService1) + mockClientInstance = &mockClient{ + pods: []kubernetes.Pod{pod1, pod2}, + services: []kubernetes.Service{service1}, + } +) + +type mockClient struct { + pods []kubernetes.Pod + services []kubernetes.Service +} + +func (c *mockClient) Stop() {} +func (c *mockClient) WalkPods(f func(kubernetes.Pod) error) error { + for _, pod := range c.pods { + if err := f(pod); err != nil { + return err + } + } + return nil +} +func (c *mockClient) WalkServices(f func(kubernetes.Service) error) error { + for _, service := range c.services { + if err := f(service); err != nil { + return err + } + } + return nil +} + +func TestReporter(t *testing.T) { + want := report.MakeReport() + pod1ID := report.MakePodNodeID("ping", "pong-a") + pod2ID := report.MakePodNodeID("ping", "pong-b") + want.Pod = report.Topology{ + Nodes: report.Nodes{ + pod1ID: report.MakeNodeWith(map[string]string{ + kubernetes.PodID: "ping/pong-a", + kubernetes.PodName: "pong-a", + kubernetes.Namespace: "ping", + kubernetes.PodCreated: pod1.Created(), + kubernetes.PodContainerIDs: "container1 container2", + kubernetes.ServiceIDs: "ping/pongservice", + }), + pod2ID: report.MakeNodeWith(map[string]string{ + kubernetes.PodID: "ping/pong-b", + kubernetes.PodName: "pong-b", + kubernetes.Namespace: "ping", + kubernetes.PodCreated: pod1.Created(), + kubernetes.PodContainerIDs: "container3 container4", + kubernetes.ServiceIDs: "ping/pongservice", + }), + }, + } + want.Service = report.Topology{ + Nodes: report.Nodes{ + report.MakeServiceNodeID("ping", "pongservice"): report.MakeNodeWith(map[string]string{ + kubernetes.ServiceID: "ping/pongservice", + kubernetes.ServiceName: "pongservice", + kubernetes.Namespace: "ping", + kubernetes.ServiceCreated: pod1.Created(), + }), + }, + } + + reporter := kubernetes.NewReporter(mockClientInstance) + have, _ := reporter.Report() + if !reflect.DeepEqual(want, have) { + t.Errorf("%s", test.Diff(want, have)) + } +} diff --git a/probe/kubernetes/service.go b/probe/kubernetes/service.go new file mode 100644 index 000000000..0717f27f5 --- /dev/null +++ b/probe/kubernetes/service.go @@ -0,0 +1,59 @@ +package kubernetes + +import ( + "time" + + "github.com/weaveworks/scope/report" + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/labels" +) + +// These constants are keys used in node metadata +const ( + ServiceID = "kubernetes_service_id" + ServiceName = "kubernetes_service_name" + ServiceCreated = "kubernetes_service_created" +) + +// Service represents a Kubernetes service +type Service interface { + ID() string + Name() string + Namespace() string + GetNode() report.Node + Selector() labels.Selector +} + +type service struct { + *api.Service +} + +// NewService creates a new Service +func NewService(s *api.Service) Service { + return &service{Service: s} +} + +func (s *service) ID() string { + return s.ObjectMeta.Namespace + "/" + s.ObjectMeta.Name +} + +func (s *service) Name() string { + return s.ObjectMeta.Name +} + +func (s *service) Namespace() string { + return s.ObjectMeta.Namespace +} + +func (s *service) Selector() labels.Selector { + return labels.SelectorFromSet(labels.Set(s.Spec.Selector)) +} + +func (s *service) GetNode() report.Node { + return report.MakeNodeWith(map[string]string{ + ServiceID: s.ID(), + ServiceName: s.Name(), + ServiceCreated: s.ObjectMeta.CreationTimestamp.Format(time.RFC822), + Namespace: s.Namespace(), + }) +} diff --git a/probe/main.go b/probe/main.go index 095e5c90d..cf7778551 100644 --- a/probe/main.go +++ b/probe/main.go @@ -17,6 +17,7 @@ import ( "github.com/weaveworks/scope/probe/docker" "github.com/weaveworks/scope/probe/endpoint" "github.com/weaveworks/scope/probe/host" + "github.com/weaveworks/scope/probe/kubernetes" "github.com/weaveworks/scope/probe/overlay" "github.com/weaveworks/scope/probe/process" "github.com/weaveworks/scope/report" @@ -37,6 +38,9 @@ func main() { dockerEnabled = flag.Bool("docker", false, "collect Docker-related attributes for processes") dockerInterval = flag.Duration("docker.interval", 10*time.Second, "how often to update Docker attributes") dockerBridge = flag.String("docker.bridge", "docker0", "the docker bridge name") + kubernetesEnabled = flag.Bool("kubernetes", false, "collect kubernetes-related attributes for containers, should only be enabled on the master node") + kubernetesAPI = flag.String("kubernetes.api", "http://localhost:8080", "Address of kubernetes master api") + kubernetesInterval = flag.Duration("kubernetes.interval", 10*time.Second, "how often to do a full resync of the kubernetes data") weaveRouterAddr = flag.String("weave.router.addr", "", "IP address or FQDN of the Weave router") procRoot = flag.String("proc.root", "/proc", "location of the proc filesystem") printVersion = flag.Bool("version", false, "print version number and exit") @@ -135,6 +139,15 @@ func main() { defer dockerRegistry.Stop() } + if *kubernetesEnabled { + if client, err := kubernetes.NewClient(*kubernetesAPI, *kubernetesInterval); err == nil { + defer client.Stop() + reporters = append(reporters, kubernetes.NewReporter(client)) + } else { + log.Printf("Kubernetes: failed to start client: %v", err) + } + } + if *weaveRouterAddr != "" { weave := overlay.NewWeave(hostID, *weaveRouterAddr) tickers = append(tickers, weave) diff --git a/render/detailed_node_test.go b/render/detailed_node_test.go index e3d005b78..5c5c5306a 100644 --- a/render/detailed_node_test.go +++ b/render/detailed_node_test.go @@ -61,6 +61,7 @@ func TestOriginTable(t *testing.T) { {fmt.Sprintf(`Label %q`, render.AmazonECSContainerNameLabel), `server`, "", false}, {`Label "foo1"`, `bar1`, "", false}, {`Label "foo2"`, `bar2`, "", false}, + {`Label "io.kubernetes.pod.name"`, "ping/pong-b", "", false}, }, }, } { @@ -162,6 +163,7 @@ func TestMakeDetailedContainerNode(t *testing.T) { {fmt.Sprintf(`Label %q`, render.AmazonECSContainerNameLabel), `server`, "", false}, {`Label "foo1"`, `bar1`, "", false}, {`Label "foo2"`, `bar2`, "", false}, + {`Label "io.kubernetes.pod.name"`, "ping/pong-b", "", false}, }, }, { diff --git a/render/expected/expected.go b/render/expected/expected.go index 77db5d920..3616e6070 100644 --- a/render/expected/expected.go +++ b/render/expected/expected.go @@ -368,6 +368,141 @@ var ( Origins: report.MakeIDList(test.RandomAddressNodeID), }, }).Prune() + + RenderedPods = (render.RenderableNodes{ + "ping/pong-a": { + ID: "ping/pong-a", + LabelMajor: "pong-a", + LabelMinor: "1 container", + Rank: "ping/pong-a", + Pseudo: false, + Origins: report.MakeIDList( + test.Client54001NodeID, + test.Client54002NodeID, + test.ClientProcess1NodeID, + test.ClientProcess2NodeID, + test.ClientHostNodeID, + test.ClientContainerNodeID, + test.ClientContainerImageNodeID, + test.ClientPodNodeID, + ), + Node: report.MakeNode().WithAdjacent("ping/pong-b"), + EdgeMetadata: report.EdgeMetadata{ + EgressPacketCount: newu64(30), + EgressByteCount: newu64(300), + }, + }, + "ping/pong-b": { + ID: "ping/pong-b", + LabelMajor: "pong-b", + LabelMinor: "1 container", + Rank: "ping/pong-b", + Pseudo: false, + Origins: report.MakeIDList( + test.Server80NodeID, + test.ServerPodNodeID, + test.ServerProcessNodeID, + test.ServerContainerNodeID, + test.ServerHostNodeID, + test.ServerContainerImageNodeID, + ), + Node: report.MakeNode(), + EdgeMetadata: report.EdgeMetadata{ + IngressPacketCount: newu64(210), + IngressByteCount: newu64(2100), + }, + }, + uncontainedServerID: { + ID: uncontainedServerID, + LabelMajor: render.UncontainedMajor, + LabelMinor: test.ServerHostName, + Rank: "", + Pseudo: true, + Origins: report.MakeIDList( + test.ServerHostNodeID, + test.NonContainerProcessNodeID, + test.NonContainerNodeID, + ), + Node: report.MakeNode().WithAdjacent(render.TheInternetID), + EdgeMetadata: report.EdgeMetadata{}, + }, + render.TheInternetID: { + ID: render.TheInternetID, + LabelMajor: render.TheInternetMajor, + Pseudo: true, + Node: report.MakeNode().WithAdjacent("ping/pong-b"), + EdgeMetadata: report.EdgeMetadata{ + EgressPacketCount: newu64(60), + EgressByteCount: newu64(600), + }, + Origins: report.MakeIDList( + test.RandomClientNodeID, + test.GoogleEndpointNodeID, + ), + }, + }).Prune() + + RenderedPodServices = (render.RenderableNodes{ + "ping/pongservice": { + ID: test.ServiceID, + LabelMajor: "pongservice", + LabelMinor: "2 pods", + Rank: test.ServiceID, + Pseudo: false, + Origins: report.MakeIDList( + test.Client54001NodeID, + test.Client54002NodeID, + test.ClientProcess1NodeID, + test.ClientProcess2NodeID, + test.ClientHostNodeID, + test.ClientContainerNodeID, + test.ClientContainerImageNodeID, + test.ClientPodNodeID, + test.Server80NodeID, + test.ServerPodNodeID, + test.ServiceNodeID, + test.ServerProcessNodeID, + test.ServerContainerNodeID, + test.ServerHostNodeID, + test.ServerContainerImageNodeID, + ), + Node: report.MakeNode().WithAdjacent(test.ServiceID), // ?? Shouldn't be adjacent to itself? + EdgeMetadata: report.EdgeMetadata{ + EgressPacketCount: newu64(30), + EgressByteCount: newu64(300), + IngressPacketCount: newu64(210), + IngressByteCount: newu64(2100), + }, + }, + uncontainedServerID: { + ID: uncontainedServerID, + LabelMajor: render.UncontainedMajor, + LabelMinor: test.ServerHostName, + Rank: "", + Pseudo: true, + Origins: report.MakeIDList( + test.ServerHostNodeID, + test.NonContainerProcessNodeID, + test.NonContainerNodeID, + ), + Node: report.MakeNode().WithAdjacent(render.TheInternetID), + EdgeMetadata: report.EdgeMetadata{}, + }, + render.TheInternetID: { + ID: render.TheInternetID, + LabelMajor: render.TheInternetMajor, + Pseudo: true, + Node: report.MakeNode().WithAdjacent(test.ServiceID), + EdgeMetadata: report.EdgeMetadata{ + EgressPacketCount: newu64(60), + EgressByteCount: newu64(600), + }, + Origins: report.MakeIDList( + test.RandomClientNodeID, + test.GoogleEndpointNodeID, + ), + }, + }).Prune() ) func newu64(value uint64) *uint64 { return &value } diff --git a/render/mapping.go b/render/mapping.go index fd8d33740..7f7353a91 100644 --- a/render/mapping.go +++ b/render/mapping.go @@ -9,6 +9,7 @@ import ( "github.com/weaveworks/scope/probe/docker" "github.com/weaveworks/scope/probe/endpoint" "github.com/weaveworks/scope/probe/host" + "github.com/weaveworks/scope/probe/kubernetes" "github.com/weaveworks/scope/probe/process" "github.com/weaveworks/scope/report" ) @@ -22,7 +23,9 @@ const ( TheInternetMajor = "The Internet" containersKey = "containers" + podsKey = "pods" processesKey = "processes" + servicesKey = "services" AmazonECSContainerNameLabel = "com.amazonaws.ecs.container-name" ) @@ -170,6 +173,40 @@ func MapContainerImageIdentity(m RenderableNode, _ report.Networks) RenderableNo return RenderableNodes{id: NewRenderableNodeWith(id, major, "", rank, m)} } +// MapPodIdentity maps a pod topology node to pod renderable node. As it is +// only ever run on pod topology nodes, we expect that certain keys +// are present. +func MapPodIdentity(m RenderableNode, _ report.Networks) RenderableNodes { + id, ok := m.Metadata[kubernetes.PodID] + if !ok { + return RenderableNodes{} + } + + var ( + major = m.Metadata[kubernetes.PodName] + rank = m.Metadata[kubernetes.PodID] + ) + + return RenderableNodes{id: NewRenderableNodeWith(id, major, "", rank, m)} +} + +// MapServiceIdentity maps a service topology node to service renderable node. As it is +// only ever run on service topology nodes, we expect that certain keys +// are present. +func MapServiceIdentity(m RenderableNode, _ report.Networks) RenderableNodes { + id, ok := m.Metadata[kubernetes.ServiceID] + if !ok { + return RenderableNodes{} + } + + var ( + major = m.Metadata[kubernetes.ServiceName] + rank = m.Metadata[kubernetes.ServiceID] + ) + + return RenderableNodes{id: NewRenderableNodeWith(id, major, "", rank, m)} +} + // MapAddressIdentity maps an address topology node to an address renderable // node. As it is only ever run on address topology nodes, we expect that // certain keys are present. @@ -430,6 +467,38 @@ func MapContainer2ContainerImage(n RenderableNode, _ report.Networks) Renderable return RenderableNodes{id: result} } +// MapPod2Service maps pod RenderableNodes to service RenderableNodes. +// +// If this function is given a node without a kubernetes_pod_id +// (including other pseudo nodes), it will produce an "Uncontained" +// pseudo node. +// +// Otherwise, this function will produce a node with the correct ID +// format for a container, but without any Major or Minor labels. +// It does not have enough info to do that, and the resulting graph +// must be merged with a pod graph to get that info. +func MapPod2Service(n RenderableNode, _ report.Networks) RenderableNodes { + // Propogate all pseudo nodes + if n.Pseudo { + return RenderableNodes{n.ID: n} + } + + // Otherwise, if some some reason the pod doesn't have a service_ids (maybe + // slightly out of sync reports, or its not in a service), just drop it + ids, ok := n.Node.Metadata[kubernetes.ServiceIDs] + if !ok { + return RenderableNodes{} + } + + result := RenderableNodes{} + for _, id := range strings.Fields(ids) { + n := NewDerivedNode(id, n) + n.Node.Counters[podsKey] = 1 + result[id] = n + } + return result +} + func imageNameWithoutVersion(name string) string { parts := strings.SplitN(name, ":", 2) if len(parts) == 2 { @@ -463,6 +532,45 @@ func MapContainerImage2Name(n RenderableNode, _ report.Networks) RenderableNodes return RenderableNodes{name: node} } +// MapContainer2Pod maps container RenderableNodes to pod +// RenderableNodes. +// +// If this function is given a node without a kubernetes_pod_id +// (including other pseudo nodes), it will produce an "Unmanaged" +// pseudo node. +// +// Otherwise, this function will produce a node with the correct ID +// format for a container, but without any Major or Minor labels. +// It does not have enough info to do that, and the resulting graph +// must be merged with a container graph to get that info. +func MapContainer2Pod(n RenderableNode, _ report.Networks) RenderableNodes { + // Propogate all pseudo nodes + if n.Pseudo { + return RenderableNodes{n.ID: n} + } + + // Otherwise, if some some reason the container doesn't have a pod_id (maybe + // slightly out of sync reports, or its not in a pod), just drop it + id, ok := n.Node.Metadata["docker_label_io.kubernetes.pod.name"] + if !ok { + return RenderableNodes{} + } + + // Add container- key to NMD, which will later be counted to produce the + // minor label + result := NewRenderableNodeWith(id, "", "", id, n) + result.Node.Counters[containersKey] = 1 + // Due to a bug in kubernetes, addon pods on the master node are not returned + // from the API. This is a workaround until + // https://github.com/kubernetes/kubernetes/issues/14738 is fixed. + if s := strings.SplitN(id, "/", 2); len(s) == 2 { + result.LabelMajor = s[1] + result.Node.Metadata[kubernetes.Namespace] = s[0] + result.Node.Metadata[kubernetes.PodName] = s[1] + } + return RenderableNodes{id: result} +} + // MapCountContainers maps 1:1 container image nodes, counting // the number of containers grouped together and putting // that info in the minor label. @@ -480,6 +588,22 @@ func MapCountContainers(n RenderableNode, _ report.Networks) RenderableNodes { return RenderableNodes{n.ID: n} } +// MapCountPods maps 1:1 service nodes, counting the number of pods grouped +// together and putting that info in the minor label. +func MapCountPods(n RenderableNode, _ report.Networks) RenderableNodes { + if n.Pseudo { + return RenderableNodes{n.ID: n} + } + + pods := n.Node.Counters[podsKey] + if pods == 1 { + n.LabelMinor = "1 pod" + } else { + n.LabelMinor = fmt.Sprintf("%d pods", pods) + } + return RenderableNodes{n.ID: n} +} + // MapAddress2Host maps address RenderableNodes to host RenderableNodes. // // Otherthan pseudo nodes, we can assume all nodes have a HostID diff --git a/render/mapping_test.go b/render/mapping_test.go index eaf1a299d..751f8bc64 100644 --- a/render/mapping_test.go +++ b/render/mapping_test.go @@ -6,6 +6,7 @@ import ( "github.com/weaveworks/scope/probe/docker" "github.com/weaveworks/scope/probe/endpoint" + "github.com/weaveworks/scope/probe/kubernetes" "github.com/weaveworks/scope/probe/process" "github.com/weaveworks/scope/render" "github.com/weaveworks/scope/report" @@ -72,6 +73,24 @@ func TestMapHostIdentity(t *testing.T) { } } +func TestMapPodIdentity(t *testing.T) { + for _, input := range []testcase{ + {nrn(report.MakeNode()), false}, + {nrn(report.MakeNodeWith(map[string]string{kubernetes.PodID: "ping/pong", kubernetes.PodName: "pong"})), true}, + } { + testMap(t, render.MapPodIdentity, input) + } +} + +func TestMapServiceIdentity(t *testing.T) { + for _, input := range []testcase{ + {nrn(report.MakeNode()), false}, + {nrn(report.MakeNodeWith(map[string]string{kubernetes.ServiceID: "ping/pong", kubernetes.ServiceName: "pong"})), true}, + } { + testMap(t, render.MapServiceIdentity, input) + } +} + type testcase struct { md render.RenderableNode ok bool diff --git a/render/render.go b/render/render.go index d964168e6..79db7a73a 100644 --- a/render/render.go +++ b/render/render.go @@ -4,6 +4,7 @@ import ( "strings" "github.com/weaveworks/scope/probe/docker" + "github.com/weaveworks/scope/probe/kubernetes" "github.com/weaveworks/scope/report" ) @@ -291,6 +292,12 @@ func FilterSystem(r Renderer) Renderer { if node.Metadata[docker.LabelPrefix+"works.weave.role"] == "system" { return false } + if node.Metadata[kubernetes.Namespace] == "kube-system" { + return false + } + if strings.HasPrefix(node.Metadata[docker.LabelPrefix+"io.kubernetes.pod.name"], "kube-system/") { + return false + } return true }, } diff --git a/render/selectors.go b/render/selectors.go index aafab995f..8733a5fe2 100644 --- a/render/selectors.go +++ b/render/selectors.go @@ -91,4 +91,14 @@ var ( SelectHost = TopologySelector(func(r report.Report) RenderableNodes { return MakeRenderableNodes(r.Host) }) + + // SelectPod selects the pod topology. + SelectPod = TopologySelector(func(r report.Report) RenderableNodes { + return MakeRenderableNodes(r.Pod) + }) + + // SelectService selects the service topology. + SelectService = TopologySelector(func(r report.Report) RenderableNodes { + return MakeRenderableNodes(r.Service) + }) ) diff --git a/render/topologies.go b/render/topologies.go index 13c10e700..2e3734c73 100644 --- a/render/topologies.go +++ b/render/topologies.go @@ -190,3 +190,35 @@ var HostRenderer = MakeReduce( Renderer: SelectHost, }, ) + +// PodRenderer is a Renderer which produces a renderable kubernetes +// graph by merging the container graph and the pods topology. +var PodRenderer = Map{ + MapFunc: MapCountContainers, + Renderer: MakeReduce( + Map{ + MapFunc: MapPodIdentity, + Renderer: SelectPod, + }, + Map{ + MapFunc: MapContainer2Pod, + Renderer: ContainerRenderer, + }, + ), +} + +// PodsServiceRenderer is a Renderer which produces a renderable kubernetes services +// graph by merging the pods graph and the services topology. +var PodServiceRenderer = Map{ + MapFunc: MapCountPods, + Renderer: MakeReduce( + Map{ + MapFunc: MapPod2Service, + Renderer: PodRenderer, + }, + Map{ + MapFunc: MapServiceIdentity, + Renderer: SelectService, + }, + ), +} diff --git a/render/topologies_test.go b/render/topologies_test.go index 07c5ab865..dfdd0cb04 100644 --- a/render/topologies_test.go +++ b/render/topologies_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/weaveworks/scope/probe/docker" + "github.com/weaveworks/scope/probe/kubernetes" "github.com/weaveworks/scope/render" "github.com/weaveworks/scope/render/expected" "github.com/weaveworks/scope/test" @@ -62,3 +63,36 @@ func TestHostRenderer(t *testing.T) { t.Error(test.Diff(want, have)) } } + +func TestPodRenderer(t *testing.T) { + have := render.PodRenderer.Render(test.Report).Prune() + want := expected.RenderedPods + if !reflect.DeepEqual(want, have) { + t.Error(test.Diff(want, have)) + } +} + +func TestPodFilterRenderer(t *testing.T) { + // tag on containers or pod namespace in the topology and ensure + // it is filtered out correctly. + input := test.Report.Copy() + input.Pod.Nodes[test.ClientPodNodeID].Metadata[kubernetes.PodID] = "kube-system/foo" + input.Pod.Nodes[test.ClientPodNodeID].Metadata[kubernetes.Namespace] = "kube-system" + input.Pod.Nodes[test.ClientPodNodeID].Metadata[kubernetes.PodName] = "foo" + input.Container.Nodes[test.ClientContainerNodeID].Metadata[docker.LabelPrefix+"io.kubernetes.pod.name"] = "kube-system/foo" + have := render.FilterSystem(render.PodRenderer).Render(input).Prune() + want := expected.RenderedPods.Copy() + delete(want, test.ClientPodID) + delete(want, test.ClientContainerID) + if !reflect.DeepEqual(want, have) { + t.Error(test.Diff(want, have)) + } +} + +func TestPodServiceRenderer(t *testing.T) { + have := render.PodServiceRenderer.Render(test.Report).Prune() + want := expected.RenderedPodServices + if !reflect.DeepEqual(want, have) { + t.Error(test.Diff(want, have)) + } +} diff --git a/report/id.go b/report/id.go index 71c69574c..daa816ce7 100644 --- a/report/id.go +++ b/report/id.go @@ -94,6 +94,16 @@ func MakeContainerNodeID(hostID, containerID string) string { return hostID + ScopeDelim + containerID } +// MakePodNodeID produces a pod node ID from its composite parts. +func MakePodNodeID(hostID, podID string) string { + return hostID + ScopeDelim + podID +} + +// MakeServiceNodeID produces a service node ID from its composite parts. +func MakeServiceNodeID(namespaceID, serviceID string) string { + return namespaceID + ScopeDelim + serviceID +} + // MakeOverlayNodeID produces an overlay topology node ID from a router peer's // name, which is assumed to be globally unique. func MakeOverlayNodeID(peerName string) string { diff --git a/report/report.go b/report/report.go index 8368b5bac..3d9b9a0e3 100644 --- a/report/report.go +++ b/report/report.go @@ -28,6 +28,16 @@ type Report struct { // Edges are not present. Container Topology + // Pod nodes represent all Kubernetes pods running on hosts running probes. + // Metadata includes things like pod id, name etc. Edges are not + // present. + Pod Topology + + // Service nodes represent all Kubernetes services running on hosts running probes. + // Metadata includes things like service id, name etc. Edges are not + // present. + Service Topology + // ContainerImages nodes represent all Docker containers images on // hosts running probes. Metadata includes things like image id, name etc. // Edges are not present. @@ -64,6 +74,8 @@ func MakeReport() Report { Container: MakeTopology(), ContainerImage: MakeTopology(), Host: MakeTopology(), + Pod: MakeTopology(), + Service: MakeTopology(), Overlay: MakeTopology(), Sampling: Sampling{}, Window: 0, @@ -79,6 +91,8 @@ func (r Report) Copy() Report { Container: r.Container.Copy(), ContainerImage: r.ContainerImage.Copy(), Host: r.Host.Copy(), + Pod: r.Pod.Copy(), + Service: r.Service.Copy(), Overlay: r.Overlay.Copy(), Sampling: r.Sampling, Window: r.Window, @@ -95,6 +109,8 @@ func (r Report) Merge(other Report) Report { cp.Container = r.Container.Merge(other.Container) cp.ContainerImage = r.ContainerImage.Merge(other.ContainerImage) cp.Host = r.Host.Merge(other.Host) + cp.Pod = r.Pod.Merge(other.Pod) + cp.Service = r.Service.Merge(other.Service) cp.Overlay = r.Overlay.Merge(other.Overlay) cp.Sampling = r.Sampling.Merge(other.Sampling) cp.Window += other.Window @@ -109,6 +125,8 @@ func (r Report) Topologies() []Topology { r.Process, r.Container, r.ContainerImage, + r.Pod, + r.Service, r.Host, r.Overlay, } diff --git a/test/report_fixture.go b/test/report_fixture.go index 5a9de4b63..06da33cf9 100644 --- a/test/report_fixture.go +++ b/test/report_fixture.go @@ -5,6 +5,7 @@ import ( "github.com/weaveworks/scope/probe/docker" "github.com/weaveworks/scope/probe/endpoint" + "github.com/weaveworks/scope/probe/kubernetes" "github.com/weaveworks/scope/probe/process" "github.com/weaveworks/scope/render" "github.com/weaveworks/scope/report" @@ -87,6 +88,13 @@ var ( UnknownAddress3NodeID = report.MakeAddressNodeID(ServerHostID, UnknownClient3IP) RandomAddressNodeID = report.MakeAddressNodeID(ServerHostID, RandomClientIP) // this should become an internet node + ClientPodID = "ping/pong-a" + ServerPodID = "ping/pong-b" + ClientPodNodeID = report.MakePodNodeID("ping", "pong-a") + ServerPodNodeID = report.MakePodNodeID("ping", "pong-b") + ServiceID = "ping/pongservice" + ServiceNodeID = report.MakeServiceNodeID("ping", "pongservice") + Report = report.Report{ Endpoint: report.Topology{ Nodes: report.Nodes{ @@ -205,10 +213,11 @@ var ( Container: report.Topology{ Nodes: report.Nodes{ ClientContainerNodeID: report.MakeNodeWith(map[string]string{ - docker.ContainerID: ClientContainerID, - docker.ContainerName: "client", - docker.ImageID: ClientContainerImageID, - report.HostNodeID: ClientHostNodeID, + docker.ContainerID: ClientContainerID, + docker.ContainerName: "client", + docker.ImageID: ClientContainerImageID, + report.HostNodeID: ClientHostNodeID, + docker.LabelPrefix + "io.kubernetes.pod.name": ClientPodID, }), ServerContainerNodeID: report.MakeNodeWith(map[string]string{ docker.ContainerID: ServerContainerID, @@ -218,6 +227,7 @@ var ( docker.LabelPrefix + render.AmazonECSContainerNameLabel: "server", docker.LabelPrefix + "foo1": "bar1", docker.LabelPrefix + "foo2": "bar2", + docker.LabelPrefix + "io.kubernetes.pod.name": ServerPodID, }), }, }, @@ -286,6 +296,33 @@ var ( }), }, }, + Pod: report.Topology{ + Nodes: report.Nodes{ + ClientPodNodeID: report.MakeNodeWith(map[string]string{ + kubernetes.PodID: ClientPodID, + kubernetes.PodName: "pong-a", + kubernetes.Namespace: "ping", + kubernetes.PodContainerIDs: ClientContainerID, + kubernetes.ServiceIDs: ServiceID, + }), + ServerPodNodeID: report.MakeNodeWith(map[string]string{ + kubernetes.PodID: ServerPodID, + kubernetes.PodName: "pong-b", + kubernetes.Namespace: "ping", + kubernetes.PodContainerIDs: ServerContainerID, + kubernetes.ServiceIDs: ServiceID, + }), + }, + }, + Service: report.Topology{ + Nodes: report.Nodes{ + ServiceNodeID: report.MakeNodeWith(map[string]string{ + kubernetes.ServiceID: ServiceID, + kubernetes.ServiceName: "pongservice", + kubernetes.Namespace: "ping", + }), + }, + }, Sampling: report.Sampling{ Count: 1024, Total: 4096,