From 16a5c738d9a2118eb720b6948eddcf847b828097 Mon Sep 17 00:00:00 2001 From: Paul Bellamy Date: Wed, 4 May 2016 17:03:05 +0100 Subject: [PATCH] Deployment and ReplicaSet views for k8s --- app/api_topologies.go | 37 +++++--- probe/appclient/app_client_internal_test.go | 4 + probe/kubernetes/client.go | 66 ++++++++++++-- probe/kubernetes/deployment.go | 71 +++++++++++++++ probe/kubernetes/meta.go | 46 ++++++++++ probe/kubernetes/pod.go | 20 ++--- probe/kubernetes/replica_set.go | 65 ++++++++++++++ probe/kubernetes/reporter.go | 97 +++++++++++++++++++-- probe/kubernetes/reporter_test.go | 17 ++-- probe/probe_internal_test.go | 2 + render/detailed/node_test.go | 5 ++ render/detailed/summary.go | 34 ++++++++ render/pod.go | 41 +++++++-- render/selectors.go | 2 + report/id.go | 6 ++ report/report.go | 28 ++++++ test/fixture/report_fixture.go | 9 +- 17 files changed, 488 insertions(+), 62 deletions(-) create mode 100644 probe/kubernetes/deployment.go create mode 100644 probe/kubernetes/meta.go create mode 100644 probe/kubernetes/replica_set.go diff --git a/app/api_topologies.go b/app/api_topologies.go index 625cb1335..3fb0be82b 100644 --- a/app/api_topologies.go +++ b/app/api_topologies.go @@ -81,13 +81,6 @@ func init() { Rank: 2, Options: containerFilters, }, - APITopologyDesc{ - id: "containers-by-image", - parent: "containers", - renderer: render.ContainerImageRenderer, - Name: "by image", - Options: containerFilters, - }, APITopologyDesc{ id: "containers-by-hostname", parent: "containers", @@ -95,6 +88,13 @@ func init() { Name: "by DNS name", Options: containerFilters, }, + APITopologyDesc{ + id: "containers-by-image", + parent: "containers", + renderer: render.ContainerImageRenderer, + Name: "by image", + Options: containerFilters, + }, APITopologyDesc{ id: "pods", renderer: render.PodRenderer, @@ -103,10 +103,24 @@ func init() { HideIfEmpty: true, }, APITopologyDesc{ - id: "pods-by-service", + id: "replica-sets", + parent: "pods", + renderer: render.ReplicaSetRenderer, + Name: "replica sets", + HideIfEmpty: true, + }, + APITopologyDesc{ + id: "deployments", + parent: "pods", + renderer: render.DeploymentRenderer, + Name: "deployments", + HideIfEmpty: true, + }, + APITopologyDesc{ + id: "services", parent: "pods", renderer: render.PodServiceRenderer, - Name: "by service", + Name: "services", HideIfEmpty: true, }, APITopologyDesc{ @@ -136,7 +150,7 @@ func kubernetesFilters(namespaces ...string) APITopologyOptionGroup { // Currently only kubernetes changes. func updateFilters(rpt report.Report, topologies []APITopologyDesc) []APITopologyDesc { namespaces := map[string]struct{}{} - for _, t := range []report.Topology{rpt.Pod, rpt.Service} { + for _, t := range []report.Topology{rpt.Pod, rpt.Service, rpt.Deployment, rpt.ReplicaSet} { for _, n := range t.Nodes { if state, ok := n.Latest.Lookup(kubernetes.PodState); ok && state == kubernetes.StateDeleted { continue @@ -152,7 +166,7 @@ func updateFilters(rpt report.Report, topologies []APITopologyDesc) []APITopolog } sort.Strings(ns) for i, t := range topologies { - if t.id == "pods" || t.id == "pods-by-service" { + if t.id == "pods" || t.id == "services" || t.id == "deployments" || t.id == "replica-sets" { topologies[i] = updateTopologyFilters(t, []APITopologyOptionGroup{kubernetesFilters(ns...)}) } } @@ -227,7 +241,6 @@ func (r *registry) add(ts ...APITopologyDesc) { if t.parent != "" { parent := r.items[t.parent] parent.SubTopologies = append(parent.SubTopologies, t) - sort.Sort(byName(parent.SubTopologies)) r.items[t.parent] = parent } diff --git a/probe/appclient/app_client_internal_test.go b/probe/appclient/app_client_internal_test.go index 575f0e670..21f96e568 100644 --- a/probe/appclient/app_client_internal_test.go +++ b/probe/appclient/app_client_internal_test.go @@ -74,6 +74,8 @@ func TestAppClientPublish(t *testing.T) { rpt.ContainerImage = report.MakeTopology() rpt.Pod = report.MakeTopology() rpt.Service = report.MakeTopology() + rpt.Deployment = report.MakeTopology() + rpt.ReplicaSet = report.MakeTopology() rpt.Host = report.MakeTopology() rpt.Overlay = report.MakeTopology() rpt.Endpoint.Controls = nil @@ -82,6 +84,8 @@ func TestAppClientPublish(t *testing.T) { rpt.ContainerImage.Controls = nil rpt.Pod.Controls = nil rpt.Service.Controls = nil + rpt.Deployment.Controls = nil + rpt.ReplicaSet.Controls = nil rpt.Host.Controls = nil rpt.Overlay.Controls = nil diff --git a/probe/kubernetes/client.go b/probe/kubernetes/client.go index 2b6129f0c..343b46f77 100644 --- a/probe/kubernetes/client.go +++ b/probe/kubernetes/client.go @@ -8,6 +8,7 @@ import ( log "github.com/Sirupsen/logrus" "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/extensions" "k8s.io/kubernetes/pkg/client/cache" "k8s.io/kubernetes/pkg/client/restclient" "k8s.io/kubernetes/pkg/client/unversioned" @@ -26,6 +27,8 @@ type Client interface { Stop() WalkPods(f func(Pod) error) error WalkServices(f func(Service) error) error + WalkDeployments(f func(Deployment) error) error + WalkReplicaSets(f func(ReplicaSet) error) error WalkNodes(f func(*api.Node) error) error WatchPods(f func(Event, Pod)) @@ -35,14 +38,18 @@ type Client interface { } type client struct { - quit chan struct{} - client *unversioned.Client - podReflector *cache.Reflector - serviceReflector *cache.Reflector - nodeReflector *cache.Reflector - podStore *cache.StoreToPodLister - serviceStore *cache.StoreToServiceLister - nodeStore *cache.StoreToNodeLister + quit chan struct{} + client *unversioned.Client + podReflector *cache.Reflector + serviceReflector *cache.Reflector + deploymentReflector *cache.Reflector + replicaSetReflector *cache.Reflector + nodeReflector *cache.Reflector + podStore *cache.StoreToPodLister + serviceStore *cache.StoreToServiceLister + deploymentStore *cache.StoreToDeploymentLister + replicaSetStore *cache.StoreToReplicaSetLister + nodeStore *cache.StoreToNodeLister podWatchesMutex sync.Mutex podWatches []func(Event, Pod) @@ -80,6 +87,11 @@ func NewClient(addr string, resyncPeriod time.Duration) (Client, error) { return nil, err } + ec, err := unversioned.NewExtensions(config) + if err != nil { + return nil, err + } + result := &client{ quit: make(chan struct{}), client: c, @@ -95,6 +107,16 @@ func NewClient(addr string, resyncPeriod time.Duration) (Client, error) { result.serviceStore = &cache.StoreToServiceLister{Store: serviceStore} result.serviceReflector = cache.NewReflector(serviceListWatch, &api.Service{}, serviceStore, resyncPeriod) + deploymentListWatch := cache.NewListWatchFromClient(ec, "deployments", api.NamespaceAll, fields.Everything()) + deploymentStore := cache.NewStore(cache.MetaNamespaceKeyFunc) + result.deploymentStore = &cache.StoreToDeploymentLister{Store: deploymentStore} + result.deploymentReflector = cache.NewReflector(deploymentListWatch, &extensions.Deployment{}, deploymentStore, resyncPeriod) + + replicaSetListWatch := cache.NewListWatchFromClient(ec, "replicasets", api.NamespaceAll, fields.Everything()) + replicaSetStore := cache.NewStore(cache.MetaNamespaceKeyFunc) + result.replicaSetStore = &cache.StoreToReplicaSetLister{Store: replicaSetStore} + result.replicaSetReflector = cache.NewReflector(replicaSetListWatch, &extensions.ReplicaSet{}, replicaSetStore, resyncPeriod) + nodeListWatch := cache.NewListWatchFromClient(c, "nodes", api.NamespaceAll, fields.Everything()) nodeStore := cache.NewStore(cache.MetaNamespaceKeyFunc) result.nodeStore = &cache.StoreToNodeLister{Store: nodeStore} @@ -102,6 +124,8 @@ func NewClient(addr string, resyncPeriod time.Duration) (Client, error) { runReflectorUntil(result.podReflector, resyncPeriod, result.quit) runReflectorUntil(result.serviceReflector, resyncPeriod, result.quit) + runReflectorUntil(result.deploymentReflector, resyncPeriod, result.quit) + runReflectorUntil(result.replicaSetReflector, resyncPeriod, result.quit) runReflectorUntil(result.nodeReflector, resyncPeriod, result.quit) return result, nil } @@ -146,6 +170,32 @@ func (c *client) WalkServices(f func(Service) error) error { return nil } +func (c *client) WalkDeployments(f func(Deployment) error) error { + list, err := c.deploymentStore.List() + if err != nil { + return err + } + for i := range list { + if err := f(NewDeployment(&(list[i]))); err != nil { + return err + } + } + return nil +} + +func (c *client) WalkReplicaSets(f func(ReplicaSet) error) error { + list, err := c.replicaSetStore.List() + if err != nil { + return err + } + for i := range list { + if err := f(NewReplicaSet(&(list[i]))); err != nil { + return err + } + } + return nil +} + func (c *client) WalkNodes(f func(*api.Node) error) error { list, err := c.nodeStore.List() if err != nil { diff --git a/probe/kubernetes/deployment.go b/probe/kubernetes/deployment.go new file mode 100644 index 000000000..ec2d48b24 --- /dev/null +++ b/probe/kubernetes/deployment.go @@ -0,0 +1,71 @@ +package kubernetes + +import ( + "fmt" + + "github.com/weaveworks/scope/report" + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/unversioned" + "k8s.io/kubernetes/pkg/apis/extensions" + "k8s.io/kubernetes/pkg/labels" +) + +// These constants are keys used in node metadata +const ( + DeploymentID = "kubernetes_deployment_id" + DeploymentName = "kubernetes_deployment_name" + DeploymentCreated = "kubernetes_deployment_created" + DeploymentObservedGeneration = "kubernetes_deployment_observed_generation" + DeploymentDesiredReplicas = "kubernetes_deployment_desired_replicas" + DeploymentReplicas = "kubernetes_deployment_replicas" + DeploymentUpdatedReplicas = "kubernetes_deployment_updated_replicas" + DeploymentAvailableReplicas = "kubernetes_deployment_available_replicas" + DeploymentUnavailableReplicas = "kubernetes_deployment_unavailable_replicas" + DeploymentLabelPrefix = "kubernetes_deployment_labels_" + DeploymentStrategy = "kubernetes_deployment_strategy" +) + +// Deployment represents a Kubernetes deployment +type Deployment interface { + Meta + Selector() labels.Selector + GetNode(probeID string) report.Node +} + +type deployment struct { + *extensions.Deployment + Meta + Node *api.Node +} + +// NewDeployment creates a new Deployment +func NewDeployment(d *extensions.Deployment) Deployment { + return &deployment{Deployment: d, Meta: meta{d.ObjectMeta}} +} + +func (d *deployment) Selector() labels.Selector { + selector, err := unversioned.LabelSelectorAsSelector(d.Spec.Selector) + if err != nil { + panic(err) + } + return selector +} + +func (d *deployment) GetNode(probeID string) report.Node { + n := report.MakeNodeWith(report.MakeDeploymentNodeID(d.UID()), map[string]string{ + DeploymentID: d.ID(), + DeploymentName: d.Name(), + Namespace: d.Namespace(), + DeploymentCreated: d.Created(), + DeploymentObservedGeneration: fmt.Sprint(d.Status.ObservedGeneration), + DeploymentDesiredReplicas: fmt.Sprint(d.Spec.Replicas), + DeploymentReplicas: fmt.Sprint(d.Status.Replicas), + DeploymentUpdatedReplicas: fmt.Sprint(d.Status.UpdatedReplicas), + DeploymentAvailableReplicas: fmt.Sprint(d.Status.AvailableReplicas), + DeploymentUnavailableReplicas: fmt.Sprint(d.Status.UnavailableReplicas), + DeploymentStrategy: string(d.Spec.Strategy.Type), + report.ControlProbeID: probeID, + }) + n = n.AddTable(DeploymentLabelPrefix, d.ObjectMeta.Labels) + return n +} diff --git a/probe/kubernetes/meta.go b/probe/kubernetes/meta.go new file mode 100644 index 000000000..c50d574e4 --- /dev/null +++ b/probe/kubernetes/meta.go @@ -0,0 +1,46 @@ +package kubernetes + +import ( + "time" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/labels" +) + +// Meta represents a metadata information about a Kubernetes object +type Meta interface { + UID() string + ID() string + Name() string + Namespace() string + Created() string + Labels() labels.Labels +} + +type meta struct { + ObjectMeta api.ObjectMeta +} + +func (m meta) UID() string { + return string(m.ObjectMeta.UID) +} + +func (m meta) ID() string { + return m.ObjectMeta.Namespace + "/" + m.ObjectMeta.Name +} + +func (m meta) Name() string { + return m.ObjectMeta.Name +} + +func (m meta) Namespace() string { + return m.ObjectMeta.Namespace +} + +func (m meta) Created() string { + return m.ObjectMeta.CreationTimestamp.Format(time.RFC822) +} + +func (m meta) Labels() labels.Labels { + return labels.Set(m.ObjectMeta.Labels) +} diff --git a/probe/kubernetes/pod.go b/probe/kubernetes/pod.go index 42a9c3df1..9182899da 100644 --- a/probe/kubernetes/pod.go +++ b/probe/kubernetes/pod.go @@ -13,7 +13,6 @@ const ( PodState = "kubernetes_pod_state" PodLabelPrefix = "kubernetes_pod_labels_" PodIP = "kubernetes_pod_ip" - ServiceIDs = "kubernetes_service_ids" StateDeleted = "deleted" ) @@ -21,7 +20,7 @@ const ( // Pod represents a Kubernetes pod type Pod interface { Meta - AddServiceID(id string) + AddParent(topology, id string) NodeName() string GetNode(probeID string) report.Node } @@ -29,13 +28,13 @@ type Pod interface { type pod struct { *api.Pod Meta - serviceIDs report.StringSet - Node *api.Node + parents report.Sets + Node *api.Node } // NewPod creates a new Pod func NewPod(p *api.Pod) Pod { - return &pod{Pod: p, Meta: meta{p.ObjectMeta}, serviceIDs: report.MakeStringSet()} + return &pod{Pod: p, Meta: meta{p.ObjectMeta}, parents: report.MakeSets()} } func (p *pod) UID() string { @@ -46,8 +45,8 @@ func (p *pod) UID() string { return p.Meta.UID() } -func (p *pod) AddServiceID(id string) { - p.serviceIDs = p.serviceIDs.Add(id) +func (p *pod) AddParent(topology, id string) { + p.parents = p.parents.Add(topology, report.MakeStringSet(id)) } func (p *pod) State() string { @@ -67,12 +66,7 @@ func (p *pod) GetNode(probeID string) report.Node { PodState: p.State(), PodIP: p.Status.PodIP, report.ControlProbeID: probeID, - }).WithSets(report.EmptySets.Add(ServiceIDs, p.serviceIDs)) - for _, serviceID := range p.serviceIDs { - n = n.WithParents(report.EmptySets. - Add(report.Service, report.MakeStringSet(report.MakeServiceNodeID(serviceID))), - ) - } + }).WithParents(p.parents) n = n.AddTable(PodLabelPrefix, p.ObjectMeta.Labels) n = n.WithControls(GetLogs, DeletePod) return n diff --git a/probe/kubernetes/replica_set.go b/probe/kubernetes/replica_set.go new file mode 100644 index 000000000..3b5696b2e --- /dev/null +++ b/probe/kubernetes/replica_set.go @@ -0,0 +1,65 @@ +package kubernetes + +import ( + "fmt" + + "github.com/weaveworks/scope/report" + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/unversioned" + "k8s.io/kubernetes/pkg/apis/extensions" + "k8s.io/kubernetes/pkg/labels" +) + +// These constants are keys used in node metadata +const ( + ReplicaSetID = "kubernetes_replica_set_id" + ReplicaSetName = "kubernetes_replica_set_name" + ReplicaSetCreated = "kubernetes_replica_set_created" + ReplicaSetObservedGeneration = "kubernetes_replica_set_observed_generation" + ReplicaSetReplicas = "kubernetes_replica_set_replicas" + ReplicaSetDesiredReplicas = "kubernetes_replica_set_desired_replicas" + ReplicaSetFullyLabeledReplicas = "kubernetes_replica_set_fully_labeled_replicas" + ReplicaSetLabelPrefix = "kubernetes_replica_set_labels_" +) + +// ReplicaSet represents a Kubernetes replica_set +type ReplicaSet interface { + Meta + Selector() labels.Selector + GetNode(probeID string) report.Node +} + +type replicaSet struct { + *extensions.ReplicaSet + Meta + Node *api.Node +} + +// NewReplicaSet creates a new ReplicaSet +func NewReplicaSet(d *extensions.ReplicaSet) ReplicaSet { + return &replicaSet{ReplicaSet: d, Meta: meta{d.ObjectMeta}} +} + +func (d *replicaSet) Selector() labels.Selector { + selector, err := unversioned.LabelSelectorAsSelector(d.Spec.Selector) + if err != nil { + panic(err) + } + return selector +} + +func (d *replicaSet) GetNode(probeID string) report.Node { + n := report.MakeNodeWith(report.MakeReplicaSetNodeID(d.UID()), map[string]string{ + ReplicaSetID: d.ID(), + ReplicaSetName: d.Name(), + Namespace: d.Namespace(), + ReplicaSetCreated: d.Created(), + ReplicaSetObservedGeneration: fmt.Sprint(d.Status.ObservedGeneration), + ReplicaSetReplicas: fmt.Sprint(d.Status.Replicas), + ReplicaSetDesiredReplicas: fmt.Sprint(d.Spec.Replicas), + ReplicaSetFullyLabeledReplicas: fmt.Sprint(d.Status.FullyLabeledReplicas), + report.ControlProbeID: probeID, + }) + n = n.AddTable(ReplicaSetLabelPrefix, d.ObjectMeta.Labels) + return n +} diff --git a/probe/kubernetes/reporter.go b/probe/kubernetes/reporter.go index 2fabdfdcb..df252a7b5 100644 --- a/probe/kubernetes/reporter.go +++ b/probe/kubernetes/reporter.go @@ -35,6 +35,25 @@ var ( report.Pod: {ID: report.Pod, Label: "# Pods", From: report.FromCounters, Datatype: "number", Priority: 6}, } + DeploymentMetadataTemplates = report.MetadataTemplates{ + DeploymentID: {ID: DeploymentID, Label: "ID", From: report.FromLatest, Priority: 1}, + Namespace: {ID: Namespace, Label: "Namespace", From: report.FromLatest, Priority: 2}, + DeploymentCreated: {ID: DeploymentCreated, Label: "Created", From: report.FromLatest, Priority: 3}, + DeploymentObservedGeneration: {ID: DeploymentObservedGeneration, Label: "Observed Gen.", From: report.FromLatest, Priority: 4}, + DeploymentDesiredReplicas: {ID: DeploymentDesiredReplicas, Label: "Desired Replicas", From: report.FromLatest, Datatype: "number", Priority: 5}, + report.Pod: {ID: report.Pod, Label: "# Pods", From: report.FromCounters, Datatype: "number", Priority: 6}, + DeploymentStrategy: {ID: DeploymentStrategy, Label: "Strategy", From: report.FromLatest, Priority: 7}, + } + + ReplicaSetMetadataTemplates = report.MetadataTemplates{ + ReplicaSetID: {ID: ReplicaSetID, Label: "ID", From: report.FromLatest, Priority: 1}, + Namespace: {ID: Namespace, Label: "Namespace", From: report.FromLatest, Priority: 2}, + ReplicaSetCreated: {ID: ReplicaSetCreated, Label: "Created", From: report.FromLatest, Priority: 3}, + ReplicaSetObservedGeneration: {ID: ReplicaSetObservedGeneration, Label: "Observed Gen.", From: report.FromLatest, Priority: 4}, + ReplicaSetDesiredReplicas: {ID: ReplicaSetDesiredReplicas, Label: "Desired Replicas", From: report.FromLatest, Datatype: "number", Priority: 5}, + report.Pod: {ID: report.Pod, Label: "# Pods", From: report.FromCounters, Datatype: "number", Priority: 6}, + } + PodTableTemplates = report.TableTemplates{ PodLabelPrefix: {ID: PodLabelPrefix, Label: "Kubernetes Labels", Prefix: PodLabelPrefix}, } @@ -42,6 +61,14 @@ var ( ServiceTableTemplates = report.TableTemplates{ ServiceLabelPrefix: {ID: ServiceLabelPrefix, Label: "Kubernetes Labels", Prefix: ServiceLabelPrefix}, } + + DeploymentTableTemplates = report.TableTemplates{ + DeploymentLabelPrefix: {ID: DeploymentLabelPrefix, Label: "Kubernetes Labels", Prefix: DeploymentLabelPrefix}, + } + + ReplicaSetTableTemplates = report.TableTemplates{ + ReplicaSetLabelPrefix: {ID: ReplicaSetLabelPrefix, Label: "Kubernetes Labels", Prefix: ReplicaSetLabelPrefix}, + } ) // Reporter generate Reports containing Container and ContainerImage topologies @@ -142,12 +169,22 @@ func (r *Reporter) Report() (report.Report, error) { if err != nil { return result, err } - podTopology, err := r.podTopology(services) + deploymentTopology, deployments, err := r.deploymentTopology(r.probeID) + if err != nil { + return result, err + } + replicaSetTopology, replicaSets, err := r.replicaSetTopology(r.probeID) + if err != nil { + return result, err + } + podTopology, err := r.podTopology(services, deployments, replicaSets) if err != nil { return result, err } - result.Service = result.Service.Merge(serviceTopology) result.Pod = result.Pod.Merge(podTopology) + result.Service = result.Service.Merge(serviceTopology) + result.Deployment = result.Deployment.Merge(deploymentTopology) + result.ReplicaSet = result.ReplicaSet.Merge(replicaSetTopology) return result, nil } @@ -166,6 +203,36 @@ func (r *Reporter) serviceTopology() (report.Topology, []Service, error) { return result, services, err } +func (r *Reporter) deploymentTopology(probeID string) (report.Topology, []Deployment, error) { + var ( + result = report.MakeTopology(). + WithMetadataTemplates(DeploymentMetadataTemplates). + WithTableTemplates(DeploymentTableTemplates) + deployments = []Deployment{} + ) + err := r.client.WalkDeployments(func(d Deployment) error { + result = result.AddNode(d.GetNode(probeID)) + deployments = append(deployments, d) + return nil + }) + return result, deployments, err +} + +func (r *Reporter) replicaSetTopology(probeID string) (report.Topology, []ReplicaSet, error) { + var ( + result = report.MakeTopology(). + WithMetadataTemplates(ReplicaSetMetadataTemplates). + WithTableTemplates(ReplicaSetTableTemplates) + replicaSets = []ReplicaSet{} + ) + err := r.client.WalkReplicaSets(func(r ReplicaSet) error { + result = result.AddNode(r.GetNode(probeID)) + replicaSets = append(replicaSets, r) + return nil + }) + return result, replicaSets, err +} + // GetNodeName return the k8s node name for the current machine. // It is exported for testing. var GetNodeName = func(r *Reporter) (string, error) { @@ -187,16 +254,16 @@ var GetNodeName = func(r *Reporter) (string, error) { return nodeName, err } -func (r *Reporter) podTopology(services []Service) (report.Topology, error) { +func (r *Reporter) podTopology(services []Service, deployments []Deployment, replicaSets []ReplicaSet) (report.Topology, error) { var ( pods = report.MakeTopology(). WithMetadataTemplates(PodMetadataTemplates). WithTableTemplates(PodTableTemplates) selectors = []func(Pod){} - match = func(selector labels.Selector, f func(Pod, string), id string) func(Pod) { + match = func(selector labels.Selector, topology, id string) func(Pod) { return func(p Pod) { if selector.Matches(p.Labels()) { - f(p, id) + p.AddParent(topology, id) } } } @@ -214,7 +281,25 @@ func (r *Reporter) podTopology(services []Service) (report.Topology, error) { Rank: 1, }) for _, service := range services { - selectors = append(selectors, match(service.Selector(), Pod.AddServiceID, service.UID())) + selectors = append(selectors, match( + service.Selector(), + report.Service, + report.MakeServiceNodeID(service.UID()), + )) + } + for _, deployment := range deployments { + selectors = append(selectors, match( + deployment.Selector(), + report.Deployment, + report.MakeDeploymentNodeID(deployment.UID()), + )) + } + for _, replicaSet := range replicaSets { + selectors = append(selectors, match( + replicaSet.Selector(), + report.ReplicaSet, + report.MakeReplicaSetNodeID(replicaSet.UID()), + )) } thisNodeName, err := GetNodeName(r) diff --git a/probe/kubernetes/reporter_test.go b/probe/kubernetes/reporter_test.go index 985b9304a..c8c19d7c0 100644 --- a/probe/kubernetes/reporter_test.go +++ b/probe/kubernetes/reporter_test.go @@ -131,6 +131,12 @@ func (c *mockClient) WalkServices(f func(kubernetes.Service) error) error { } return nil } +func (c *mockClient) WalkDeployments(f func(kubernetes.Deployment) error) error { + return nil +} +func (c *mockClient) WalkReplicaSets(f func(kubernetes.ReplicaSet) error) error { + return nil +} func (*mockClient) WalkNodes(f func(*api.Node) error) error { return nil } @@ -176,23 +182,18 @@ func TestReporter(t *testing.T) { id string parentService string latest map[string]string - sets map[string]report.StringSet }{ {pod1ID, serviceID, map[string]string{ kubernetes.PodID: "ping/pong-a", kubernetes.PodName: "pong-a", kubernetes.Namespace: "ping", kubernetes.PodCreated: pod1.Created(), - }, map[string]report.StringSet{ - kubernetes.ServiceIDs: report.MakeStringSet(serviceUID), }}, {pod2ID, serviceID, map[string]string{ kubernetes.PodID: "ping/pong-b", kubernetes.PodName: "pong-b", kubernetes.Namespace: "ping", kubernetes.PodCreated: pod1.Created(), - }, map[string]report.StringSet{ - kubernetes.ServiceIDs: report.MakeStringSet(serviceUID), }}, } { node, ok := rpt.Pod.Nodes[pod.id] @@ -209,12 +210,6 @@ func TestReporter(t *testing.T) { t.Errorf("Expected pod %s latest %q: %q, got %q", pod.id, k, want, have) } } - - for k, want := range pod.sets { - if have, ok := node.Sets.Lookup(k); !ok || !reflect.DeepEqual(want, have) { - t.Errorf("Expected pod %s sets %q: %q, got %q", pod.id, k, want, have) - } - } } // Reporter should have added a service diff --git a/probe/probe_internal_test.go b/probe/probe_internal_test.go index 81ab7e048..5b4aac0f4 100644 --- a/probe/probe_internal_test.go +++ b/probe/probe_internal_test.go @@ -87,6 +87,8 @@ func TestProbe(t *testing.T) { want.ContainerImage.Controls = nil want.Pod.Controls = nil want.Service.Controls = nil + want.Deployment.Controls = nil + want.ReplicaSet.Controls = nil want.Host.Controls = nil want.Overlay.Controls = nil want.Endpoint.AddNode(node) diff --git a/render/detailed/node_test.go b/render/detailed/node_test.go index c96fd671d..62acaec75 100644 --- a/render/detailed/node_test.go +++ b/render/detailed/node_test.go @@ -361,6 +361,11 @@ func TestMakeDetailedPodNode(t *testing.T) { Label: fixture.ServerHostName, TopologyID: "hosts", }, + { + ID: fixture.ServiceNodeID, + Label: fixture.ServiceName, + TopologyID: "pods-by-service", + }, }, Connections: []detailed.ConnectionsSummary{ { diff --git a/render/detailed/summary.go b/render/detailed/summary.go index c00962de2..099e2a177 100644 --- a/render/detailed/summary.go +++ b/render/detailed/summary.go @@ -78,6 +78,8 @@ func MakeNodeSummary(r report.Report, n report.Node) (NodeSummary, bool) { report.ContainerImage: containerImageNodeSummary, report.Pod: podNodeSummary, report.Service: serviceNodeSummary, + report.Deployment: deploymentNodeSummary, + report.ReplicaSet: replicaSetNodeSummary, report.Host: hostNodeSummary, } if renderer, ok := renderers[n.Topology]; ok { @@ -270,6 +272,38 @@ func serviceNodeSummary(base NodeSummary, n report.Node) (NodeSummary, bool) { return base, true } +func deploymentNodeSummary(base NodeSummary, n report.Node) (NodeSummary, bool) { + base.Label, _ = n.Latest.Lookup(kubernetes.DeploymentName) + base.Rank, _ = n.Latest.Lookup(kubernetes.DeploymentID) + base.Stack = true + + if p, ok := n.Counters.Lookup(report.Pod); ok { + if p == 1 { + base.LabelMinor = fmt.Sprintf("%d pod", p) + } else { + base.LabelMinor = fmt.Sprintf("%d pods", p) + } + } + + return base, true +} + +func replicaSetNodeSummary(base NodeSummary, n report.Node) (NodeSummary, bool) { + base.Label, _ = n.Latest.Lookup(kubernetes.ReplicaSetName) + base.Rank, _ = n.Latest.Lookup(kubernetes.ReplicaSetID) + base.Stack = true + + if p, ok := n.Counters.Lookup(report.Pod); ok { + if p == 1 { + base.LabelMinor = fmt.Sprintf("%d pod", p) + } else { + base.LabelMinor = fmt.Sprintf("%d pods", p) + } + } + + return base, true +} + func hostNodeSummary(base NodeSummary, n report.Node) (NodeSummary, bool) { var ( hostname, _ = n.Latest.Lookup(host.HostName) diff --git a/render/pod.go b/render/pod.go index f8b048961..a057ff3d7 100644 --- a/render/pod.go +++ b/render/pod.go @@ -50,6 +50,34 @@ var PodServiceRenderer = ConditionalRenderer(renderKubernetesTopologies, )), ) +// DeploymentRenderer is a Renderer which produces a renderable kubernetes deployments +// graph by merging the pods graph and the deployments topology. +var DeploymentRenderer = ApplyDecorators( + FilterEmpty(report.Pod, + MakeReduce( + MakeMap( + MapPod2Deployment, + PodRenderer, + ), + SelectDeployment, + ), + ), +) + +// ReplicaSetRenderer is a Renderer which produces a renderable kubernetes replica sets +// graph by merging the pods graph and the replica sets topology. +var ReplicaSetRenderer = ApplyDecorators( + FilterEmpty(report.Pod, + MakeReduce( + MakeMap( + MapPod2ReplicaSet, + PodRenderer, + ), + SelectReplicaSet, + ), + ), +) + // MapContainer2Pod maps container Nodes to pod // Nodes. // @@ -97,11 +125,13 @@ func MapContainer2Pod(n report.Node, _ report.Networks) report.Nodes { // The various ways of grouping pods var ( - MapPod2Service = MapPod2Grouping(report.Service, kubernetes.ServiceIDs, report.MakeServiceNodeID) + MapPod2Service = MapPod2Parent(report.Service) + MapPod2Deployment = MapPod2Parent(report.Deployment) + MapPod2ReplicaSet = MapPod2Parent(report.ReplicaSet) ) -// MapPod2Grouping maps pod Nodes to some kubernetes grouping. -func MapPod2Grouping(topology, setKey string, idMaker func(uid string) string) func(pod report.Node, _ report.Networks) report.Nodes { +// MapPod2Parent maps pod Nodes to some kubernetes grouping. +func MapPod2Parent(topology string) func(pod report.Node, _ report.Networks) report.Nodes { return func(pod report.Node, _ report.Networks) report.Nodes { // Propagate all pseudo nodes if pod.Topology == Pseudo { @@ -111,14 +141,13 @@ func MapPod2Grouping(topology, setKey string, idMaker func(uid string) string) f // Otherwise, if some some reason the pod doesn't have any of these ids // (maybe slightly out of sync reports, or its not in this group), just // drop it - groupIDs, ok := pod.Sets.Lookup(setKey) + groupIDs, ok := pod.Parents.Lookup(topology) if !ok { return report.Nodes{} } result := report.Nodes{} - for _, groupID := range groupIDs { - id := idMaker(groupID) + for _, id := range groupIDs { node := NewDerivedNode(id, pod).WithTopology(topology) node.Counters = node.Counters.Add(pod.Topology, 1) result[id] = node diff --git a/render/selectors.go b/render/selectors.go index b1bba8213..fa5344749 100644 --- a/render/selectors.go +++ b/render/selectors.go @@ -29,4 +29,6 @@ var ( SelectHost = TopologySelector(report.Host) SelectPod = TopologySelector(report.Pod) SelectService = TopologySelector(report.Service) + SelectDeployment = TopologySelector(report.Deployment) + SelectReplicaSet = TopologySelector(report.ReplicaSet) ) diff --git a/report/id.go b/report/id.go index ae4ebfc13..8a364e683 100644 --- a/report/id.go +++ b/report/id.go @@ -111,6 +111,12 @@ var ( // MakeServiceNodeID produces a service node ID from its composite parts. MakeServiceNodeID = singleComponentID("service") + + // MakeDeploymentNodeID produces a deployment node ID from its composite parts. + MakeDeploymentNodeID = singleComponentID("deployment") + + // MakeReplicaSetNodeID produces a replica set node ID from its composite parts. + MakeReplicaSetNodeID = singleComponentID("replica_set") ) // singleComponentID makes a diff --git a/report/report.go b/report/report.go index 493dcb55e..610d9d13b 100644 --- a/report/report.go +++ b/report/report.go @@ -16,6 +16,8 @@ const ( Container = "container" Pod = "pod" Service = "service" + Deployment = "deployment" + ReplicaSet = "replica_set" ContainerImage = "container_image" Host = "host" Overlay = "overlay" @@ -58,6 +60,16 @@ type Report struct { // present. Service Topology + // Deployment nodes represent all Kubernetes deployments running on hosts running probes. + // Metadata includes things like deployment id, name etc. Edges are not + // present. + Deployment Topology + + // ReplicaSet nodes represent all Kubernetes replicasets running on hosts running probes. + // Metadata includes things like replicaset id, name etc. Edges are not + // present. + ReplicaSet Topology + // ContainerImages nodes represent all Docker containers images on // hosts running probes. Metadata includes things like image id, name etc. // Edges are not present. @@ -126,6 +138,14 @@ func MakeReport() Report { WithShape(Heptagon). WithLabel("service", "services"), + Deployment: MakeTopology(). + WithShape(Heptagon). + WithLabel("deployment", "deployments"), + + ReplicaSet: MakeTopology(). + WithShape(Heptagon). + WithLabel("replica set", "replica sets"), + Overlay: MakeTopology(), Sampling: Sampling{}, @@ -145,6 +165,8 @@ func (r Report) Copy() Report { Host: r.Host.Copy(), Pod: r.Pod.Copy(), Service: r.Service.Copy(), + Deployment: r.Deployment.Copy(), + ReplicaSet: r.ReplicaSet.Copy(), Overlay: r.Overlay.Copy(), Sampling: r.Sampling, Window: r.Window, @@ -164,6 +186,8 @@ func (r Report) Merge(other Report) Report { cp.Host = r.Host.Merge(other.Host) cp.Pod = r.Pod.Merge(other.Pod) cp.Service = r.Service.Merge(other.Service) + cp.Deployment = r.Deployment.Merge(other.Deployment) + cp.ReplicaSet = r.ReplicaSet.Merge(other.ReplicaSet) cp.Overlay = r.Overlay.Merge(other.Overlay) cp.Sampling = r.Sampling.Merge(other.Sampling) cp.Window += other.Window @@ -180,6 +204,8 @@ func (r Report) Topologies() []Topology { r.ContainerImage, r.Pod, r.Service, + r.Deployment, + r.ReplicaSet, r.Host, r.Overlay, } @@ -194,6 +220,8 @@ func (r Report) Topology(name string) (Topology, bool) { ContainerImage: r.ContainerImage, Pod: r.Pod, Service: r.Service, + Deployment: r.Deployment, + ReplicaSet: r.ReplicaSet, Host: r.Host, Overlay: r.Overlay, }[name] diff --git a/test/fixture/report_fixture.go b/test/fixture/report_fixture.go index 91ef83886..4c54d7033 100644 --- a/test/fixture/report_fixture.go +++ b/test/fixture/report_fixture.go @@ -96,6 +96,7 @@ var ( ServerPodUID = "i9h8g7f6e" ClientPodNodeID = report.MakePodNodeID(ClientPodUID) ServerPodNodeID = report.MakePodNodeID(ServerPodUID) + ServiceName = "pongservice" ServiceID = "ping/pongservice" ServiceUID = "service1234" ServiceNodeID = report.MakeServiceNodeID(ServiceUID) @@ -373,11 +374,9 @@ var ( kubernetes.Namespace: KubernetesNamespace, report.HostNodeID: ClientHostNodeID, }). - WithSets(report.EmptySets. - Add(kubernetes.ServiceIDs, report.MakeStringSet(ServiceUID))). WithTopology(report.Pod).WithParents(report.EmptySets. Add("host", report.MakeStringSet(ClientHostNodeID)). - Add("service", report.MakeStringSet(ServiceID)), + Add("service", report.MakeStringSet(ServiceNodeID)), ), ServerPodNodeID: report.MakeNodeWith( ServerPodNodeID, map[string]string{ @@ -387,11 +386,9 @@ var ( kubernetes.PodState: "running", report.HostNodeID: ServerHostNodeID, }). - WithSets(report.EmptySets. - Add(kubernetes.ServiceIDs, report.MakeStringSet(ServiceUID))). WithTopology(report.Pod).WithParents(report.EmptySets. Add("host", report.MakeStringSet(ServerHostNodeID)). - Add("service", report.MakeStringSet(ServiceID)), + Add("service", report.MakeStringSet(ServiceNodeID)), ), }, MetadataTemplates: kubernetes.PodMetadataTemplates,