Merge pull request #2552 from weaveworks/mike/k8s/combined-view

add k8s combined view
This commit is contained in:
Mike Lang
2017-06-27 11:09:13 -07:00
committed by GitHub
14 changed files with 187 additions and 146 deletions

View File

@@ -29,8 +29,7 @@ const (
containersByImageID = "containers-by-image"
podsID = "pods"
replicaSetsID = "replica-sets"
deploymentsID = "deployments"
daemonsetsID = "daemonsets"
kubeControllersID = "kube-controllers"
servicesID = "services"
hostsID = "hosts"
weaveID = "weave"
@@ -120,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 {
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"),
})
@@ -196,6 +195,17 @@ func MakeRegistry() *Registry {
},
}
k8sControllersTypeFilter := 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},
},
}
// Topology option labels should tell the current state. The first item must
// be the verb to get to that state
registry.Add(
@@ -253,19 +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},
renderer: render.KubeControllerRenderer,
Name: "controllers",
Options: []APITopologyOptionGroup{unmanagedFilter, k8sControllersTypeFilter},
HideIfEmpty: true,
},
APITopologyDesc{

View File

@@ -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);

View File

@@ -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,
};

View File

@@ -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 };

View File

@@ -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',

View File

@@ -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",
})
}

View File

@@ -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)
}

View File

@@ -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},

View File

@@ -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",
@@ -257,13 +257,23 @@ func podNodeSummary(base NodeSummary, n report.Node) (NodeSummary, bool) {
return base, true
}
var podGroupNodeTypeName = map[string]string{
report.Deployment: "Deployment",
report.DaemonSet: "Daemon 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
}

View File

@@ -6,32 +6,20 @@ import (
// ECSTaskRenderer is a Renderer for Amazon ECS tasks.
var ECSTaskRenderer = ConditionalRenderer(renderECSTopologies,
MakeMap(
PropagateSingleMetrics(report.Container),
MakeReduce(
MakeMap(
Map2Parent(report.ECSTask, 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(report.ECSService, "", nil),
ECSTaskRenderer,
),
SelectECSService,
),
renderParents(
report.ECSTask, []string{report.ECSService}, NoParentsDrop, "", nil,
ECSTaskRenderer,
),
)

View File

@@ -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": {},

View File

@@ -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,
@@ -34,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(report.Pod, 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),
),
),
)
@@ -57,62 +62,63 @@ 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(report.Service, "", nil),
PodRenderer,
),
SelectService,
),
),
)
// 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(report.Deployment, "", mapPodCounts),
ReplicaSetRenderer,
),
SelectDeployment,
),
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(report.ReplicaSet, "", nil),
renderParents(
report.Pod, []string{report.ReplicaSet}, NoParentsDrop, "", nil,
PodRenderer,
),
)
// 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 KubeControllerRenderer = ConditionalRenderer(renderKubernetesTopologies,
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,
),
SelectReplicaSet,
),
),
)
// 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(
// 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(report.DaemonSet, "", nil),
PodRenderer,
Map2Parent(parentTopologies, noParentsAction, noParentsPseudoID, modifyMappedNode),
childRenderer,
),
SelectDaemonSet,
),
),
)
)...)
}
func mapPodCounts(parent, original report.Node) report.Node {
// When mapping ReplicaSets to Deployments, we want to propagate the Pods counter
@@ -141,18 +147,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}
@@ -163,28 +174,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
}
}

View File

@@ -6,17 +6,11 @@ import (
// SwarmServiceRenderer is a Renderer for Docker Swarm services
var SwarmServiceRenderer = ConditionalRenderer(renderSwarmTopologies,
MakeMap(
PropagateSingleMetrics(report.Container),
MakeReduce(
MakeMap(
Map2Parent(report.SwarmService, UnmanagedID, nil),
MakeFilter(
IsRunning,
ContainerWithImageNameRenderer,
),
),
SelectSwarmService,
renderParents(
report.Container, []string{report.SwarmService}, NoParentsPseudo, UnmanagedID, nil,
MakeFilter(
IsRunning,
ContainerWithImageNameRenderer,
),
),
)

View File

@@ -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().