From 6dae01435266a6c344d48286620fd58efd2519de Mon Sep 17 00:00:00 2001 From: Mike Lang Date: Mon, 15 May 2017 16:15:10 -0700 Subject: [PATCH 1/9] Add new view for 'combined' daemonset/deployment view The idea is that this view shows all 'pods or groups of pods' at 'the highest level of abstraction'. For now, we just show daemonsets and deployments. --- app/api_topologies.go | 11 ++++++++++- render/pod.go | 7 +++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/app/api_topologies.go b/app/api_topologies.go index 29e6b64ca..bb80b147b 100644 --- a/app/api_topologies.go +++ b/app/api_topologies.go @@ -31,6 +31,7 @@ const ( replicaSetsID = "replica-sets" deploymentsID = "deployments" daemonsetsID = "daemonsets" + kubeCombinedID = "kube-combined" servicesID = "services" hostsID = "hosts" weaveID = "weave" @@ -120,7 +121,7 @@ func updateKubeFilters(rpt report.Report, topologies []APITopologyDesc) []APITop sort.Strings(ns) topologies = append([]APITopologyDesc{}, topologies...) // Make a copy so we can make changes safely for i, t := range topologies { - if t.id == containersID || t.id == podsID || t.id == servicesID || t.id == deploymentsID || t.id == replicaSetsID || t.id == daemonsetsID { + if t.id == containersID || t.id == podsID || t.id == servicesID || t.id == deploymentsID || t.id == replicaSetsID || t.id == daemonsetsID || t.id == kubeCombinedID { topologies[i] = mergeTopologyFilters(t, []APITopologyOptionGroup{ namespaceFilters(ns, "All Namespaces"), }) @@ -268,6 +269,14 @@ func MakeRegistry() *Registry { Options: []APITopologyOptionGroup{unmanagedFilter}, HideIfEmpty: true, }, + APITopologyDesc{ + id: kubeCombinedID, + parent: podsID, + renderer: render.KubeCombinedRenderer, + Name: "combined", + Options: []APITopologyOptionGroup{unmanagedFilter}, + HideIfEmpty: true, + }, APITopologyDesc{ id: servicesID, parent: podsID, diff --git a/render/pod.go b/render/pod.go index fcd553c47..585fb86f3 100644 --- a/render/pod.go +++ b/render/pod.go @@ -114,6 +114,13 @@ var DaemonSetRenderer = ConditionalRenderer(renderKubernetesTopologies, ), ) +// KubeCombinedRenderer is a Renderer which combines the daemonset and deployment views +// in (for now) a very naive way. +var KubeCombinedRenderer = MakeReduce( + DeploymentRenderer, + DaemonSetRenderer, +) + func mapPodCounts(parent, original report.Node) report.Node { // When mapping ReplicaSets to Deployments, we want to propagate the Pods counter if count, ok := original.Counters.Lookup(report.Pod); ok { From d0cbf47c1c2c8d4d184826a7c05eedcef636e30a Mon Sep 17 00:00:00 2001 From: Mike Lang Date: Tue, 16 May 2017 17:48:27 -0700 Subject: [PATCH 2/9] Add an option group to Combined view to select group type (daemonset or deployment) This is a union set, so it will be suitable even as we continue to add more node types to this view. --- app/api_topologies.go | 15 ++++++++++++++- render/filters.go | 17 +++++++++++------ 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/app/api_topologies.go b/app/api_topologies.go index bb80b147b..f7d4fb40c 100644 --- a/app/api_topologies.go +++ b/app/api_topologies.go @@ -197,6 +197,19 @@ func MakeRegistry() *Registry { }, } + k8sCombinedTypeFilter := APITopologyOptionGroup{ + ID: "grouptype", + Default: "", + SelectType: "union", + NoneLabel: "All Types", + Options: []APITopologyOption{ + {Value: report.Deployment, Label: "Deployments", filter: render.IsTopology(report.Deployment), filterPseudo: false}, + {Value: report.DaemonSet, Label: "Daemonsets", filter: render.IsTopology(report.DaemonSet), filterPseudo: false}, + {Value: report.ReplicaSet, Label: "Replica sets", filter: render.IsTopology(report.ReplicaSet), filterPseudo: false}, + {Value: report.Pod, Label: "Pods", filter: render.IsTopology(report.Pod), filterPseudo: false}, + }, + } + // Topology option labels should tell the current state. The first item must // be the verb to get to that state registry.Add( @@ -274,7 +287,7 @@ func MakeRegistry() *Registry { parent: podsID, renderer: render.KubeCombinedRenderer, Name: "combined", - Options: []APITopologyOptionGroup{unmanagedFilter}, + Options: []APITopologyOptionGroup{unmanagedFilter, k8sCombinedTypeFilter}, HideIfEmpty: true, }, APITopologyDesc{ diff --git a/render/filters.go b/render/filters.go index 7199e23a0..8cf384618 100644 --- a/render/filters.go +++ b/render/filters.go @@ -311,12 +311,6 @@ func IsNotPseudo(n report.Node) bool { return n.Topology != Pseudo || strings.HasSuffix(n.ID, TheInternetID) || strings.HasPrefix(n.ID, ServiceNodeIDPrefix) } -// IsPseudoTopology returns true if the node is in a pseudo topology, -// mimicing the check performed by MakeFilter() instead of the more complex check in IsNotPseudo() -func IsPseudoTopology(n report.Node) bool { - return n.Topology == Pseudo -} - // IsNamespace checks if the node is a pod/service in the specified namespace func IsNamespace(namespace string) FilterFunc { return func(n report.Node) bool { @@ -336,6 +330,17 @@ func IsNamespace(namespace string) FilterFunc { } } +// IsTopology checks if the node is from a particular report topology +func IsTopology(topology string) FilterFunc { + return func(n report.Node) bool { + return n.Topology == topology + } +} + +// IsPseudoTopology returns true if the node is in a pseudo topology, +// mimicing the check performed by MakeFilter() instead of the more complex check in IsNotPseudo() +var IsPseudoTopology = IsTopology(Pseudo) + var systemContainerNames = map[string]struct{}{ "weavescope": {}, "weavedns": {}, From 13b2ed69bdcd65deec5ae951b9a69e7c30fc5eed Mon Sep 17 00:00:00 2001 From: Mike Lang Date: Tue, 23 May 2017 01:56:31 -0700 Subject: [PATCH 3/9] Improve renderer for combined view by refactoring Map2Parent The existing technique of "reducing" the two rendered graphs for daemonsets and deployments had a glaring issue that no connections would ever be made between nodes of different types, since that information would've been discarded earlier in the process. It also makes it hard to identify "parentless" pods. This commit extends the Map2Parent function, teaching it: * To check multiple topologies for parents * To pass through nodes with no parents found without modification Since we already had two 'modes' for what to do with nodes without parents, and it would've been clunky to try to encode the third option into the existing PseudoNodeID arg in some way, we instead split it into two args, with the first being an enum specifying either the old pseudo node behaviour, the old drop behaviour, or the new keep behaviour. We then use the new Map2Parent to map pods to: * A replica set, if it has one * A daemonset, if it has one * Itself, if neither of the above and then map again from the results to any deployment, leaving as-is any nodes that don't map to a deployment. Hence we are left with: * Deployments * Daemonsets * Replica sets, but only if they map to no deployment * Pods, but only if they map to none of the above and connections between all these will be calculated correctly. --- render/ecs.go | 4 +- render/pod.go | 107 +++++++++++++++++++++++++++++++++--------------- render/swarm.go | 2 +- 3 files changed, 77 insertions(+), 36 deletions(-) diff --git a/render/ecs.go b/render/ecs.go index 50eef7226..6ec86df99 100644 --- a/render/ecs.go +++ b/render/ecs.go @@ -10,7 +10,7 @@ var ECSTaskRenderer = ConditionalRenderer(renderECSTopologies, PropagateSingleMetrics(report.Container), MakeReduce( MakeMap( - Map2Parent(report.ECSTask, UnmanagedID, nil), + Map2Parent([]string{report.ECSTask}, NoParentsPseudo, UnmanagedID, nil), MakeFilter( IsRunning, ContainerWithImageNameRenderer, @@ -27,7 +27,7 @@ var ECSServiceRenderer = ConditionalRenderer(renderECSTopologies, PropagateSingleMetrics(report.ECSTask), MakeReduce( MakeMap( - Map2Parent(report.ECSService, "", nil), + Map2Parent([]string{report.ECSService}, NoParentsDrop, "", nil), ECSTaskRenderer, ), SelectECSService, diff --git a/render/pod.go b/render/pod.go index 585fb86f3..3ebd143de 100644 --- a/render/pod.go +++ b/render/pod.go @@ -26,6 +26,15 @@ func isPauseContainer(n report.Node) bool { return ok && kubernetes.IsPauseImageName(image) } +type noParentsActionEnum int + +// Constants for specifying noParentsAction in Map2Parent +const ( + NoParentsPseudo noParentsActionEnum = iota + NoParentsDrop + NoParentsKeep +) + // PodRenderer is a Renderer which produces a renderable kubernetes // graph by merging the container graph and the pods topology. var PodRenderer = ConditionalRenderer(renderKubernetesTopologies, @@ -38,7 +47,7 @@ var PodRenderer = ConditionalRenderer(renderKubernetesTopologies, PropagateSingleMetrics(report.Container), MakeReduce( MakeMap( - Map2Parent(report.Pod, UnmanagedID, nil), + Map2Parent([]string{report.Pod}, NoParentsPseudo, UnmanagedID, nil), MakeFilter( ComposeFilterFuncs( IsRunning, @@ -61,7 +70,7 @@ var PodServiceRenderer = ConditionalRenderer(renderKubernetesTopologies, PropagateSingleMetrics(report.Pod), MakeReduce( MakeMap( - Map2Parent(report.Service, "", nil), + Map2Parent([]string{report.Service}, NoParentsDrop, "", nil), PodRenderer, ), SelectService, @@ -76,7 +85,7 @@ var DeploymentRenderer = ConditionalRenderer(renderKubernetesTopologies, PropagateSingleMetrics(report.ReplicaSet), MakeReduce( MakeMap( - Map2Parent(report.Deployment, "", mapPodCounts), + Map2Parent([]string{report.Deployment}, NoParentsDrop, "", mapPodCounts), ReplicaSetRenderer, ), SelectDeployment, @@ -91,7 +100,7 @@ var ReplicaSetRenderer = ConditionalRenderer(renderKubernetesTopologies, PropagateSingleMetrics(report.Pod), MakeReduce( MakeMap( - Map2Parent(report.ReplicaSet, "", nil), + Map2Parent([]string{report.ReplicaSet}, NoParentsDrop, "", nil), PodRenderer, ), SelectReplicaSet, @@ -106,7 +115,7 @@ var DaemonSetRenderer = ConditionalRenderer(renderKubernetesTopologies, PropagateSingleMetrics(report.Pod), MakeReduce( MakeMap( - Map2Parent(report.DaemonSet, "", nil), + Map2Parent([]string{report.DaemonSet}, NoParentsDrop, "", nil), PodRenderer, ), SelectDaemonSet, @@ -114,11 +123,30 @@ var DaemonSetRenderer = ConditionalRenderer(renderKubernetesTopologies, ), ) -// KubeCombinedRenderer is a Renderer which combines the daemonset and deployment views -// in (for now) a very naive way. -var KubeCombinedRenderer = MakeReduce( - DeploymentRenderer, - DaemonSetRenderer, +// KubeCombinedRenderer is a Renderer which combines the 'top abstraction' of all pods. +// We first map pods to all possible things they can map to, then we map again to +// deployments since some things (replica sets) can map to those (the rest are passed through +// unchanged). +// We can't simply combine the rendered graphs of the high level objects as they would never +// have connections to each other. +var KubeCombinedRenderer = ConditionalRenderer(renderKubernetesTopologies, + MakeReduce( + MakeMap( + Map2Parent([]string{report.Deployment}, NoParentsKeep, "", mapPodCounts), + MakeReduce( + MakeMap( + Map2Parent([]string{ + report.ReplicaSet, + report.DaemonSet, + }, NoParentsKeep, "", nil), + PodRenderer, + ), + SelectReplicaSet, + SelectDaemonSet, + ), + ), + SelectDeployment, + ), ) func mapPodCounts(parent, original report.Node) report.Node { @@ -148,18 +176,23 @@ func MapPod2IP(m report.Node) []string { // Map2Parent returns a MapFunc which maps Nodes to some parent grouping. func Map2Parent( - // The topology ID of the parents - topology string, - // Either the ID prefix of the pseudo node to use for nodes without - // any parents in the group, eg. UnmanagedID, or "" to drop nodes without any parents. + // The topology IDs to look for parents in + topologies []string, + // Choose what to do in the case of nodes with no parents. One of: + // NoParentsPseudo: Map them to a common pseudo node id with prefix noParentsPseudoID + // NoParentsDrop: Map them to no node. + // NoParentsKeep: Map them to themselves, preserving them in the new graph. + noParentsAction noParentsActionEnum, + // The ID prefix of the pseudo node to use for nodes without any parents in the group + // if noParentsAction == Pseudo, eg. UnmanagedID noParentsPseudoID string, // Optional (can be nil) function to modify any parent nodes, // eg. to copy over details from the original node. modifyMappedNode func(parent, original report.Node) report.Node, ) MapFunc { return func(n report.Node, _ report.Networks) report.Nodes { - // Uncontained becomes Unmanaged/whatever if noParentsPseudoID is set - if noParentsPseudoID != "" && strings.HasPrefix(n.ID, UncontainedIDPrefix) { + // Uncontained becomes Unmanaged/whatever if noParentsAction == Pseudo + if noParentsAction == NoParentsPseudo && strings.HasPrefix(n.ID, UncontainedIDPrefix) { id := MakePseudoNodeID(noParentsPseudoID, report.ExtractHostID(n)) node := NewDerivedPseudoNode(id, n) return report.Nodes{id: node} @@ -170,28 +203,36 @@ func Map2Parent( return report.Nodes{n.ID: n} } - // If some some reason the node doesn't have any of these ids - // (maybe slightly out of sync reports, or its not in this group), - // either drop it or put it in Uncontained/Unmanaged/whatever if one was given - groupIDs, ok := n.Parents.Lookup(topology) - if !ok || len(groupIDs) == 0 { - if noParentsPseudoID == "" { - return report.Nodes{} + // For each topology, map to any parents we can find + result := report.Nodes{} + for _, topology := range topologies { + if groupIDs, ok := n.Parents.Lookup(topology); ok { + for _, id := range groupIDs { + node := NewDerivedNode(id, n).WithTopology(topology) + node.Counters = node.Counters.Add(n.Topology, 1) + if modifyMappedNode != nil { + node = modifyMappedNode(node, n) + } + result[id] = node + } } - id := MakePseudoNodeID(UnmanagedID, report.ExtractHostID(n)) - node := NewDerivedPseudoNode(id, n) - return report.Nodes{id: node} } - result := report.Nodes{} - for _, id := range groupIDs { - node := NewDerivedNode(id, n).WithTopology(topology) - node.Counters = node.Counters.Add(n.Topology, 1) - if modifyMappedNode != nil { - node = modifyMappedNode(node, n) + if len(result) == 0 { + switch noParentsAction { + case NoParentsPseudo: + // Map to pseudo node + id := MakePseudoNodeID(UnmanagedID, report.ExtractHostID(n)) + node := NewDerivedPseudoNode(id, n) + result[id] = node + case NoParentsKeep: + // Pass n to output unmodified + result[n.ID] = n + case NoParentsDrop: + // Do nothing, we will return an empty result } - result[id] = node } + return result } } diff --git a/render/swarm.go b/render/swarm.go index b77177a5e..8385ac0d3 100644 --- a/render/swarm.go +++ b/render/swarm.go @@ -10,7 +10,7 @@ var SwarmServiceRenderer = ConditionalRenderer(renderSwarmTopologies, PropagateSingleMetrics(report.Container), MakeReduce( MakeMap( - Map2Parent(report.SwarmService, UnmanagedID, nil), + Map2Parent([]string{report.SwarmService}, NoParentsPseudo, UnmanagedID, nil), MakeFilter( IsRunning, ContainerWithImageNameRenderer, From 4f341cb1d2885ded0c13f806224b744867e9bdab Mon Sep 17 00:00:00 2001 From: jpellizzari Date: Wed, 7 Jun 2017 10:13:53 -0700 Subject: [PATCH 4/9] Add new node shapes for k8s combined view While we're there, adopt a consistent ordering for all places that shapes are listed Order is least sides to most sides, with circle before polygons, and complex shapes (currently just Cloud) after. On shape choices for topologies: * Since the k8s logo is a heptagon, we want pods to be heptagons. * Since triangle is 'a bit weird', we put it on the least-important type, replica sets. * Pentagons look a little weirder than octogons (it's the lack of symmetry) so we put octogons on the most common (deployments) --- client/app/scripts/charts/node-shapes.js | 12 +++++++++--- client/app/scripts/charts/node.js | 10 ++++++++-- client/app/scripts/utils/node-shape-utils.js | 11 +++++++---- report/report.go | 11 +++++++---- 4 files changed, 31 insertions(+), 13 deletions(-) diff --git a/client/app/scripts/charts/node-shapes.js b/client/app/scripts/charts/node-shapes.js index 3c421ab7c..f73545d86 100644 --- a/client/app/scripts/charts/node-shapes.js +++ b/client/app/scripts/charts/node-shapes.js @@ -11,11 +11,14 @@ import { pathElement, circleElement, rectangleElement, - cloudShapeProps, circleShapeProps, + triangleShapeProps, squareShapeProps, + pentagonShapeProps, hexagonShapeProps, heptagonShapeProps, + octagonShapeProps, + cloudShapeProps, } from '../utils/node-shape-utils'; @@ -69,8 +72,11 @@ function NodeShape(shapeType, shapeElement, shapeProps, { id, highlighted, color ); } -export const NodeShapeCloud = props => NodeShape('cloud', pathElement, cloudShapeProps, props); export const NodeShapeCircle = props => NodeShape('circle', circleElement, circleShapeProps, props); +export const NodeShapeTriangle = props => NodeShape('triangle', pathElement, triangleShapeProps, props); +export const NodeShapeSquare = props => NodeShape('square', rectangleElement, squareShapeProps, props); +export const NodeShapePentagon = props => NodeShape('pentagon', pathElement, pentagonShapeProps, props); export const NodeShapeHexagon = props => NodeShape('hexagon', pathElement, hexagonShapeProps, props); export const NodeShapeHeptagon = props => NodeShape('heptagon', pathElement, heptagonShapeProps, props); -export const NodeShapeSquare = props => NodeShape('square', rectangleElement, squareShapeProps, props); +export const NodeShapeOctagon = props => NodeShape('octagon', pathElement, octagonShapeProps, props); +export const NodeShapeCloud = props => NodeShape('cloud', pathElement, cloudShapeProps, props); diff --git a/client/app/scripts/charts/node.js b/client/app/scripts/charts/node.js index f5882cb5e..0b5cfb868 100644 --- a/client/app/scripts/charts/node.js +++ b/client/app/scripts/charts/node.js @@ -14,20 +14,26 @@ import { NODE_BASE_SIZE } from '../constants/styles'; import NodeShapeStack from './node-shape-stack'; import NodeNetworksOverlay from './node-networks-overlay'; import { - NodeShapeCloud, NodeShapeCircle, + NodeShapeTriangle, NodeShapeSquare, + NodeShapePentagon, NodeShapeHexagon, NodeShapeHeptagon, + NodeShapeOctagon, + NodeShapeCloud, } from './node-shapes'; const labelWidth = 1.2 * NODE_BASE_SIZE; const nodeShapes = { circle: NodeShapeCircle, + triangle: NodeShapeTriangle, + square: NodeShapeSquare, + pentagon: NodeShapePentagon, hexagon: NodeShapeHexagon, heptagon: NodeShapeHeptagon, - square: NodeShapeSquare, + octagon: NodeShapeOctagon, cloud: NodeShapeCloud, }; diff --git a/client/app/scripts/utils/node-shape-utils.js b/client/app/scripts/utils/node-shape-utils.js index 20b59f755..1f3f4c255 100644 --- a/client/app/scripts/utils/node-shape-utils.js +++ b/client/app/scripts/utils/node-shape-utils.js @@ -20,8 +20,11 @@ function curvedUnitPolygonPath(n) { ])); } -export const squareShapeProps = { width: 1.8, height: 1.8, rx: 0.4, ry: 0.4, x: -0.9, y: -0.9 }; -export const heptagonShapeProps = { d: curvedUnitPolygonPath(7) }; -export const hexagonShapeProps = { d: curvedUnitPolygonPath(6) }; -export const cloudShapeProps = { d: UNIT_CLOUD_PATH }; export const circleShapeProps = { r: 1 }; +export const triangleShapeProps = { d: curvedUnitPolygonPath(3) }; +export const squareShapeProps = { width: 1.8, height: 1.8, rx: 0.4, ry: 0.4, x: -0.9, y: -0.9 }; +export const pentagonShapeProps = { d: curvedUnitPolygonPath(5) }; +export const hexagonShapeProps = { d: curvedUnitPolygonPath(6) }; +export const heptagonShapeProps = { d: curvedUnitPolygonPath(7) }; +export const octagonShapeProps = { d: curvedUnitPolygonPath(8) }; +export const cloudShapeProps = { d: UNIT_CLOUD_PATH }; diff --git a/report/report.go b/report/report.go index f1f790a42..7fb95ec95 100644 --- a/report/report.go +++ b/report/report.go @@ -29,9 +29,12 @@ const ( // Shapes used for different nodes Circle = "circle" + Triangle = "triangle" Square = "square" - Heptagon = "heptagon" + Pentagon = "pentagon" Hexagon = "hexagon" + Heptagon = "heptagon" + Octagon = "octagon" Cloud = "cloud" // Used when counting the number of containers @@ -163,15 +166,15 @@ func MakeReport() Report { WithLabel("service", "services"), Deployment: MakeTopology(). - WithShape(Heptagon). + WithShape(Octagon). WithLabel("deployment", "deployments"), ReplicaSet: MakeTopology(). - WithShape(Heptagon). + WithShape(Triangle). WithLabel("replica set", "replica sets"), DaemonSet: MakeTopology(). - WithShape(Heptagon). + WithShape(Pentagon). WithLabel("daemonset", "daemonsets"), Overlay: MakeTopology(). From 889972c48a1de0d53551a3f9050248a03d903dcc Mon Sep 17 00:00:00 2001 From: Mike Lang Date: Wed, 7 Jun 2017 15:28:34 -0700 Subject: [PATCH 5/9] Display node type on k8s controller nodes Since there are multiple types in the same topology, displaying the type is important. We do this in multiple places: * Add node type to minor label * Add node type as metadata and include in metadata template. Even though this will always be the same for every node of that topology, this was the easiest way to add it so it displays in the table view. Note we can't control ordering of columns in table view, it's always alphabetical. --- probe/kubernetes/daemonset.go | 1 + probe/kubernetes/deployment.go | 1 + probe/kubernetes/reporter.go | 3 +++ render/detailed/summary.go | 13 ++++++++++++- 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/probe/kubernetes/daemonset.go b/probe/kubernetes/daemonset.go index 4e93e7a8b..3460895b3 100644 --- a/probe/kubernetes/daemonset.go +++ b/probe/kubernetes/daemonset.go @@ -48,5 +48,6 @@ func (d *daemonSet) GetNode() report.Node { DesiredReplicas: fmt.Sprint(d.Status.DesiredNumberScheduled), Replicas: fmt.Sprint(d.Status.CurrentNumberScheduled), MisscheduledReplicas: fmt.Sprint(d.Status.NumberMisscheduled), + NodeType: "Daemon Set", }) } diff --git a/probe/kubernetes/deployment.go b/probe/kubernetes/deployment.go index f67553b70..16211f7cd 100644 --- a/probe/kubernetes/deployment.go +++ b/probe/kubernetes/deployment.go @@ -54,5 +54,6 @@ func (d *deployment) GetNode(probeID string) report.Node { UnavailableReplicas: fmt.Sprint(d.Status.UnavailableReplicas), Strategy: string(d.Spec.Strategy.Type), report.ControlProbeID: probeID, + NodeType: "Deployment", }).WithLatestActiveControls(ScaleUp, ScaleDown) } diff --git a/probe/kubernetes/reporter.go b/probe/kubernetes/reporter.go index 4016e334b..a0db79273 100644 --- a/probe/kubernetes/reporter.go +++ b/probe/kubernetes/reporter.go @@ -21,6 +21,7 @@ const ( ObservedGeneration = "kubernetes_observed_generation" Replicas = "kubernetes_replicas" DesiredReplicas = "kubernetes_desired_replicas" + NodeType = "kubernetes_node_type" ) // Exposed for testing @@ -46,6 +47,7 @@ var ( ServiceMetricTemplates = PodMetricTemplates DeploymentMetadataTemplates = report.MetadataTemplates{ + NodeType: {ID: NodeType, Label: "Type", From: report.FromLatest, Priority: 1}, Namespace: {ID: Namespace, Label: "Namespace", From: report.FromLatest, Priority: 2}, Created: {ID: Created, Label: "Created", From: report.FromLatest, Datatype: "datetime", Priority: 3}, ObservedGeneration: {ID: ObservedGeneration, Label: "Observed Gen.", From: report.FromLatest, Datatype: "number", Priority: 4}, @@ -67,6 +69,7 @@ var ( ReplicaSetMetricTemplates = PodMetricTemplates DaemonSetMetadataTemplates = report.MetadataTemplates{ + NodeType: {ID: NodeType, Label: "Type", From: report.FromLatest, Priority: 1}, Namespace: {ID: Namespace, Label: "Namespace", From: report.FromLatest, Priority: 2}, Created: {ID: Created, Label: "Created", From: report.FromLatest, Datatype: "datetime", Priority: 3}, DesiredReplicas: {ID: DesiredReplicas, Label: "Desired Replicas", From: report.FromLatest, Datatype: "number", Priority: 4}, diff --git a/render/detailed/summary.go b/render/detailed/summary.go index 773039a26..5ba4061e3 100644 --- a/render/detailed/summary.go +++ b/render/detailed/summary.go @@ -257,13 +257,24 @@ func podNodeSummary(base NodeSummary, n report.Node) (NodeSummary, bool) { return base, true } +var podGroupNodeTypeName = map[string]string{ + report.Deployment: "Deployment", + report.DaemonSet: "Daemon Set", + report.ReplicaSet: "Replica Set", +} + func podGroupNodeSummary(base NodeSummary, n report.Node) (NodeSummary, bool) { base = addKubernetesLabelAndRank(base, n) base.Stack = true // NB: pods are the highest aggregation level for which we display // counts. - base.LabelMinor = pluralize(n.Counters, report.Pod, "pod", "pods") + count := pluralize(n.Counters, 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, true } From 8eb72cf276d35755b99d3b59adb72454affcd65f Mon Sep 17 00:00:00 2001 From: Mike Lang Date: Wed, 7 Jun 2017 16:54:53 -0700 Subject: [PATCH 6/9] Remove deployment and daemonset views, change combined to controllers and remove bare pods and replica sets Since we still need to map through replica sets to find matching deployments, we simply filter them out as a post-step. --- app/api_topologies.go | 34 ++++---------------- render/detailed/summary.go | 5 ++- render/pod.go | 65 +++++++++++--------------------------- 3 files changed, 28 insertions(+), 76 deletions(-) diff --git a/app/api_topologies.go b/app/api_topologies.go index f7d4fb40c..e4c6eafaf 100644 --- a/app/api_topologies.go +++ b/app/api_topologies.go @@ -29,9 +29,7 @@ const ( containersByImageID = "containers-by-image" podsID = "pods" replicaSetsID = "replica-sets" - deploymentsID = "deployments" - daemonsetsID = "daemonsets" - kubeCombinedID = "kube-combined" + kubeControllersID = "kube-controllers" servicesID = "services" hostsID = "hosts" weaveID = "weave" @@ -121,7 +119,7 @@ func updateKubeFilters(rpt report.Report, topologies []APITopologyDesc) []APITop sort.Strings(ns) topologies = append([]APITopologyDesc{}, topologies...) // Make a copy so we can make changes safely for i, t := range topologies { - if t.id == containersID || t.id == podsID || t.id == servicesID || t.id == deploymentsID || t.id == replicaSetsID || t.id == daemonsetsID || t.id == kubeCombinedID { + if t.id == containersID || t.id == podsID || t.id == servicesID || t.id == replicaSetsID || t.id == kubeControllersID { topologies[i] = mergeTopologyFilters(t, []APITopologyOptionGroup{ namespaceFilters(ns, "All Namespaces"), }) @@ -197,7 +195,7 @@ func MakeRegistry() *Registry { }, } - k8sCombinedTypeFilter := APITopologyOptionGroup{ + k8sControllersTypeFilter := APITopologyOptionGroup{ ID: "grouptype", Default: "", SelectType: "union", @@ -205,8 +203,6 @@ func MakeRegistry() *Registry { Options: []APITopologyOption{ {Value: report.Deployment, Label: "Deployments", filter: render.IsTopology(report.Deployment), filterPseudo: false}, {Value: report.DaemonSet, Label: "Daemonsets", filter: render.IsTopology(report.DaemonSet), filterPseudo: false}, - {Value: report.ReplicaSet, Label: "Replica sets", filter: render.IsTopology(report.ReplicaSet), filterPseudo: false}, - {Value: report.Pod, Label: "Pods", filter: render.IsTopology(report.Pod), filterPseudo: false}, }, } @@ -267,27 +263,11 @@ func MakeRegistry() *Registry { HideIfEmpty: true, }, APITopologyDesc{ - id: deploymentsID, + id: kubeControllersID, parent: podsID, - renderer: render.DeploymentRenderer, - Name: "deployments", - Options: []APITopologyOptionGroup{unmanagedFilter}, - HideIfEmpty: true, - }, - APITopologyDesc{ - id: daemonsetsID, - parent: podsID, - renderer: render.DaemonSetRenderer, - Name: "daemonsets", - Options: []APITopologyOptionGroup{unmanagedFilter}, - HideIfEmpty: true, - }, - APITopologyDesc{ - id: kubeCombinedID, - parent: podsID, - renderer: render.KubeCombinedRenderer, - Name: "combined", - Options: []APITopologyOptionGroup{unmanagedFilter, k8sCombinedTypeFilter}, + renderer: render.KubeControllerRenderer, + Name: "controllers", + Options: []APITopologyOptionGroup{unmanagedFilter, k8sControllersTypeFilter}, HideIfEmpty: true, }, APITopologyDesc{ diff --git a/render/detailed/summary.go b/render/detailed/summary.go index 5ba4061e3..b3d058b15 100644 --- a/render/detailed/summary.go +++ b/render/detailed/summary.go @@ -90,8 +90,8 @@ var primaryAPITopology = map[string]string{ report.ContainerImage: "containers-by-image", report.Pod: "pods", report.ReplicaSet: "replica-sets", - report.Deployment: "deployments", - report.DaemonSet: "daemonsets", + report.Deployment: "kube-controllers", + report.DaemonSet: "kube-controllers", report.Service: "services", report.ECSTask: "ecs-tasks", report.ECSService: "ecs-services", @@ -260,7 +260,6 @@ func podNodeSummary(base NodeSummary, n report.Node) (NodeSummary, bool) { var podGroupNodeTypeName = map[string]string{ report.Deployment: "Deployment", report.DaemonSet: "Daemon Set", - report.ReplicaSet: "Replica Set", } func podGroupNodeSummary(base NodeSummary, n report.Node) (NodeSummary, bool) { diff --git a/render/pod.go b/render/pod.go index 3ebd143de..4652d40eb 100644 --- a/render/pod.go +++ b/render/pod.go @@ -78,21 +78,6 @@ 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 = ConditionalRenderer(renderKubernetesTopologies, - MakeMap( - PropagateSingleMetrics(report.ReplicaSet), - MakeReduce( - MakeMap( - Map2Parent([]string{report.Deployment}, NoParentsDrop, "", mapPodCounts), - ReplicaSetRenderer, - ), - 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 = ConditionalRenderer(renderKubernetesTopologies, @@ -108,41 +93,29 @@ var ReplicaSetRenderer = ConditionalRenderer(renderKubernetesTopologies, ), ) -// DaemonSetRenderer is a Renderer which produces a renderable kubernetes daemonsets -// graph by merging the pods graph and the daemonsets topology. -var DaemonSetRenderer = ConditionalRenderer(renderKubernetesTopologies, - MakeMap( - PropagateSingleMetrics(report.Pod), - MakeReduce( - MakeMap( - Map2Parent([]string{report.DaemonSet}, NoParentsDrop, "", nil), - PodRenderer, - ), - SelectDaemonSet, - ), - ), -) - -// KubeCombinedRenderer is a Renderer which combines the 'top abstraction' of all pods. -// We first map pods to all possible things they can map to, then we map again to -// deployments since some things (replica sets) can map to those (the rest are passed through -// unchanged). +// KubeControllerRenderer is a Renderer which combines all the 'controller' topologies. +// We first map pods to daemonsets and replica sets, then to deployments since replica sets +// can map to those (the rest are passed through unchanged). +// Pods with no controller are mapped to 'Unmanaged' // We can't simply combine the rendered graphs of the high level objects as they would never // have connections to each other. -var KubeCombinedRenderer = ConditionalRenderer(renderKubernetesTopologies, +var KubeControllerRenderer = ConditionalRenderer(renderKubernetesTopologies, MakeReduce( - MakeMap( - Map2Parent([]string{report.Deployment}, NoParentsKeep, "", mapPodCounts), - MakeReduce( - MakeMap( - Map2Parent([]string{ - report.ReplicaSet, - report.DaemonSet, - }, NoParentsKeep, "", nil), - PodRenderer, + MakeFilter( + Complement(IsTopology(report.ReplicaSet)), + MakeMap( + Map2Parent([]string{report.Deployment}, NoParentsKeep, "", mapPodCounts), + MakeReduce( + MakeMap( + Map2Parent([]string{ + report.ReplicaSet, + report.DaemonSet, + }, NoParentsPseudo, UnmanagedID, nil), + PodRenderer, + ), + SelectReplicaSet, + SelectDaemonSet, ), - SelectReplicaSet, - SelectDaemonSet, ), ), SelectDeployment, From ec2a7fc753cd7bd4a2aa7d403a870036aaa6365c Mon Sep 17 00:00:00 2001 From: Mike Lang Date: Mon, 12 Jun 2017 09:36:37 -0700 Subject: [PATCH 7/9] Prefer controllers view for default view --- client/app/scripts/utils/topology-utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/app/scripts/utils/topology-utils.js b/client/app/scripts/utils/topology-utils.js index f92c15088..e67287de6 100644 --- a/client/app/scripts/utils/topology-utils.js +++ b/client/app/scripts/utils/topology-utils.js @@ -12,8 +12,8 @@ import { shownNodesSelector, shownResourceTopologyIdsSelector } from '../selecto const TOPOLOGY_DISPLAY_PRIORITY = [ 'ecs-services', 'ecs-tasks', + 'kube-controllers', 'services', - 'deployments', 'replica-sets', 'pods', 'containers', From f101f83c3d29e78135059bb6748b17f04db33c0c Mon Sep 17 00:00:00 2001 From: Mike Lang Date: Mon, 26 Jun 2017 11:31:27 -0700 Subject: [PATCH 8/9] controllers topology: Map metrics for single nodes * Maps metrics if there is a single pod in the controller, as per all other views. * Also added heavy commenting on the increasingly-complex render chain --- render/pod.go | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/render/pod.go b/render/pod.go index 4652d40eb..4028ca707 100644 --- a/render/pod.go +++ b/render/pod.go @@ -101,20 +101,33 @@ var ReplicaSetRenderer = ConditionalRenderer(renderKubernetesTopologies, // have connections to each other. var KubeControllerRenderer = ConditionalRenderer(renderKubernetesTopologies, MakeReduce( + // Include full deployment topology MakeFilter( + // Filter out any remaining unmatched replica sets Complement(IsTopology(report.ReplicaSet)), MakeMap( - Map2Parent([]string{report.Deployment}, NoParentsKeep, "", mapPodCounts), - MakeReduce( - MakeMap( - Map2Parent([]string{ - report.ReplicaSet, - report.DaemonSet, - }, NoParentsPseudo, UnmanagedID, nil), - PodRenderer, + // Include pod metrics previously mapped to replica sets, in deployments + PropagateSingleMetrics(report.ReplicaSet), + MakeMap( + // Map replica sets to deployments, leaving unmatched replica sets and anything else unchanged + Map2Parent([]string{report.Deployment}, NoParentsKeep, "", mapPodCounts), + MakeReduce( + // Include full replica set and daemonset topologies + MakeMap( + // Include pod metrics in mapped nodes + PropagateSingleMetrics(report.Pod), + MakeMap( + // Transform pods to replica sets, daemonsets and 'unmanaged' + Map2Parent([]string{ + report.ReplicaSet, + report.DaemonSet, + }, NoParentsPseudo, UnmanagedID, nil), + PodRenderer, + ), + ), + SelectReplicaSet, + SelectDaemonSet, ), - SelectReplicaSet, - SelectDaemonSet, ), ), ), From f7913ab550c51ea1541865a53bbe51edcc4063c0 Mon Sep 17 00:00:00 2001 From: Mike Lang Date: Thu, 15 Jun 2017 17:32:01 -0700 Subject: [PATCH 9/9] Rewrite renderers for k8s/ecs/swarm to abstract common parts Since all the renderers were doing almost-exactly the same thing, we abstract that out into a common function. --- render/ecs.go | 28 ++++--------- render/pod.go | 107 +++++++++++++++++++++--------------------------- render/swarm.go | 16 +++----- 3 files changed, 59 insertions(+), 92 deletions(-) diff --git a/render/ecs.go b/render/ecs.go index 6ec86df99..81b11a700 100644 --- a/render/ecs.go +++ b/render/ecs.go @@ -6,32 +6,20 @@ import ( // ECSTaskRenderer is a Renderer for Amazon ECS tasks. var ECSTaskRenderer = ConditionalRenderer(renderECSTopologies, - MakeMap( - PropagateSingleMetrics(report.Container), - MakeReduce( - MakeMap( - Map2Parent([]string{report.ECSTask}, NoParentsPseudo, UnmanagedID, nil), - MakeFilter( - IsRunning, - ContainerWithImageNameRenderer, - ), - ), - SelectECSTask, + renderParents( + report.Container, []string{report.ECSTask}, NoParentsPseudo, UnmanagedID, nil, + MakeFilter( + IsRunning, + ContainerWithImageNameRenderer, ), ), ) // ECSServiceRenderer is a Renderer for Amazon ECS services. var ECSServiceRenderer = ConditionalRenderer(renderECSTopologies, - MakeMap( - PropagateSingleMetrics(report.ECSTask), - MakeReduce( - MakeMap( - Map2Parent([]string{report.ECSService}, NoParentsDrop, "", nil), - ECSTaskRenderer, - ), - SelectECSService, - ), + renderParents( + report.ECSTask, []string{report.ECSService}, NoParentsDrop, "", nil, + ECSTaskRenderer, ), ) diff --git a/render/pod.go b/render/pod.go index 4028ca707..ccdb0dc1a 100644 --- a/render/pod.go +++ b/render/pod.go @@ -43,22 +43,18 @@ var PodRenderer = ConditionalRenderer(renderKubernetesTopologies, state, ok := n.Latest.Lookup(kubernetes.State) return (!ok || state != kubernetes.StateDeleted) }, - MakeMap( - PropagateSingleMetrics(report.Container), - MakeReduce( - MakeMap( - Map2Parent([]string{report.Pod}, NoParentsPseudo, UnmanagedID, nil), - MakeFilter( - ComposeFilterFuncs( - IsRunning, - Complement(isPauseContainer), - ), - ContainerWithImageNameRenderer, + MakeReduce( + renderParents( + report.Container, []string{report.Pod}, NoParentsPseudo, UnmanagedID, nil, + MakeFilter( + ComposeFilterFuncs( + IsRunning, + Complement(isPauseContainer), ), + ContainerWithImageNameRenderer, ), - ConnectionJoin(SelectPod, MapPod2IP), - SelectPod, ), + ConnectionJoin(SelectPod, MapPod2IP), ), ), ) @@ -66,30 +62,18 @@ var PodRenderer = ConditionalRenderer(renderKubernetesTopologies, // PodServiceRenderer is a Renderer which produces a renderable kubernetes services // graph by merging the pods graph and the services topology. var PodServiceRenderer = ConditionalRenderer(renderKubernetesTopologies, - MakeMap( - PropagateSingleMetrics(report.Pod), - MakeReduce( - MakeMap( - Map2Parent([]string{report.Service}, NoParentsDrop, "", nil), - PodRenderer, - ), - SelectService, - ), + renderParents( + report.Pod, []string{report.Service}, NoParentsDrop, "", nil, + PodRenderer, ), ) // ReplicaSetRenderer is a Renderer which produces a renderable kubernetes replica sets // graph by merging the pods graph and the replica sets topology. var ReplicaSetRenderer = ConditionalRenderer(renderKubernetesTopologies, - MakeMap( - PropagateSingleMetrics(report.Pod), - MakeReduce( - MakeMap( - Map2Parent([]string{report.ReplicaSet}, NoParentsDrop, "", nil), - PodRenderer, - ), - SelectReplicaSet, - ), + renderParents( + report.Pod, []string{report.ReplicaSet}, NoParentsDrop, "", nil, + PodRenderer, ), ) @@ -100,41 +84,42 @@ var ReplicaSetRenderer = ConditionalRenderer(renderKubernetesTopologies, // We can't simply combine the rendered graphs of the high level objects as they would never // have connections to each other. var KubeControllerRenderer = ConditionalRenderer(renderKubernetesTopologies, - MakeReduce( - // Include full deployment topology - MakeFilter( - // Filter out any remaining unmatched replica sets - Complement(IsTopology(report.ReplicaSet)), - MakeMap( - // Include pod metrics previously mapped to replica sets, in deployments - PropagateSingleMetrics(report.ReplicaSet), - MakeMap( - // Map replica sets to deployments, leaving unmatched replica sets and anything else unchanged - Map2Parent([]string{report.Deployment}, NoParentsKeep, "", mapPodCounts), - MakeReduce( - // Include full replica set and daemonset topologies - MakeMap( - // Include pod metrics in mapped nodes - PropagateSingleMetrics(report.Pod), - MakeMap( - // Transform pods to replica sets, daemonsets and 'unmanaged' - Map2Parent([]string{ - report.ReplicaSet, - report.DaemonSet, - }, NoParentsPseudo, UnmanagedID, nil), - PodRenderer, - ), - ), - SelectReplicaSet, - SelectDaemonSet, - ), - ), + MakeFilter( + // Filter out any remaining unmatched replica sets + Complement(IsTopology(report.ReplicaSet)), + renderParents( + report.ReplicaSet, []string{report.Deployment}, NoParentsKeep, "", mapPodCounts, + renderParents( + report.Pod, []string{report.ReplicaSet, report.DaemonSet}, + NoParentsPseudo, UnmanagedID, nil, + PodRenderer, ), ), - SelectDeployment, ), ) +// renderParents produces a 'standard' renderer for mapping from some child topology to some parent topologies, +// by taking a child renderer, mapping to parents, propagating single metrics, and joining with full parent topology. +// Most options are as per Map2Parent. +func renderParents(childTopology string, parentTopologies []string, noParentsAction noParentsActionEnum, + noParentsPseudoID string, modifyMappedNode func(parent, original report.Node) report.Node, + childRenderer Renderer) Renderer { + selectors := make([]Renderer, len(parentTopologies)) + for i, topology := range parentTopologies { + selectors[i] = TopologySelector(topology) + } + return MakeReduce(append( + selectors, + MakeMap( + PropagateSingleMetrics(childTopology), + MakeMap( + Map2Parent(parentTopologies, noParentsAction, noParentsPseudoID, modifyMappedNode), + childRenderer, + ), + ), + )...) +} + func mapPodCounts(parent, original report.Node) report.Node { // When mapping ReplicaSets to Deployments, we want to propagate the Pods counter if count, ok := original.Counters.Lookup(report.Pod); ok { diff --git a/render/swarm.go b/render/swarm.go index 8385ac0d3..4aecced10 100644 --- a/render/swarm.go +++ b/render/swarm.go @@ -6,17 +6,11 @@ import ( // SwarmServiceRenderer is a Renderer for Docker Swarm services var SwarmServiceRenderer = ConditionalRenderer(renderSwarmTopologies, - MakeMap( - PropagateSingleMetrics(report.Container), - MakeReduce( - MakeMap( - Map2Parent([]string{report.SwarmService}, NoParentsPseudo, UnmanagedID, nil), - MakeFilter( - IsRunning, - ContainerWithImageNameRenderer, - ), - ), - SelectSwarmService, + renderParents( + report.Container, []string{report.SwarmService}, NoParentsPseudo, UnmanagedID, nil, + MakeFilter( + IsRunning, + ContainerWithImageNameRenderer, ), ), )