diff --git a/app/api_topologies.go b/app/api_topologies.go index 0a0d36b61..5233ff26c 100644 --- a/app/api_topologies.go +++ b/app/api_topologies.go @@ -48,6 +48,14 @@ var ( {Value: "hide", Label: "Hide Unmanaged", filter: render.IsNotPseudo, filterPseudo: true}, }, } + storageFilter = APITopologyOptionGroup{ + ID: "storage", + Default: "hide", + Options: []APITopologyOption{ + {Value: "show", Label: "Show Storage", filter: render.IsStorageComponent, filterPseudo: false}, + {Value: "hide", Label: "Hide Storage", filter: render.IsNotStorageComponent, filterPseudo: false}, + }, + } ) // namespaceFilters generates a namespace selector option group based on the given namespaces @@ -230,7 +238,7 @@ func MakeRegistry() *Registry { renderer: render.PodRenderer, Name: "Pods", Rank: 3, - Options: []APITopologyOptionGroup{unmanagedFilter}, + Options: []APITopologyOptionGroup{storageFilter, unmanagedFilter}, HideIfEmpty: true, }, APITopologyDesc{ @@ -249,14 +257,6 @@ func MakeRegistry() *Registry { Options: []APITopologyOptionGroup{unmanagedFilter}, HideIfEmpty: true, }, - APITopologyDesc{ - id: persistentVolumesID, - parent: podsID, - renderer: render.PersistentVolumeRenderer, - Name: "persistent volumes", - Options: []APITopologyOptionGroup{}, - HideIfEmpty: true, - }, APITopologyDesc{ id: ecsTasksID, renderer: render.ECSTaskRenderer, diff --git a/client/app/scripts/charts/edge.js b/client/app/scripts/charts/edge.js index dc8d27d9d..29b812f4b 100644 --- a/client/app/scripts/charts/edge.js +++ b/client/app/scripts/charts/edge.js @@ -5,6 +5,20 @@ import classNames from 'classnames'; import { enterEdge, leaveEdge } from '../actions/app-actions'; import { encodeIdAttribute, decodeIdAttribute } from '../utils/dom-utils'; + +function getAdjacencyClass(id) { + const topologyId = id.split('---'); + const from = topologyId[0].split(';'); + const to = topologyId[1].split(';'); + if (from[1] !== undefined && to[1] !== undefined) { + from[1] = from[1].slice(1, -1); + to[1] = to[1].slice(1, -1); + if ((from[1] === 'persistent_volume' || from[1] === 'storage_class' || from[1] === 'persistent_volume_claim') || (to[1] === 'persistent_volume' || to[1] === 'storage_class' || to[1] === 'persistent_volume_claim')) { + return 'link-storage'; + } + } + return 'link-none'; +} class Edge extends React.Component { constructor(props, context) { super(props, context); @@ -18,7 +32,6 @@ class Edge extends React.Component { } = this.props; const shouldRenderMarker = (focused || highlighted) && (source !== target); const className = classNames('edge', { highlighted }); - return ( + NodeShape('octagon', pathElement, octag export const NodeShapeCloud = props => NodeShape('cloud', pathElement, cloudShapeProps, props); export const NodeShapeCylinder = props => NodeShape('cylinder', pathElement, cylinderShapeProps, props); export const NodeShapeDottedCylinder = props => NodeShape('dottedcylinder', pathElement, dottedCylinderShapeProps, props); +export const NodeShapeSheet = props => NodeShape('sheet', pathElement, sheetShapeProps, props); diff --git a/client/app/scripts/charts/node.js b/client/app/scripts/charts/node.js index 6f0dc039d..8f0cb2c07 100644 --- a/client/app/scripts/charts/node.js +++ b/client/app/scripts/charts/node.js @@ -23,7 +23,8 @@ import { NodeShapeOctagon, NodeShapeCloud, NodeShapeCylinder, - NodeShapeDottedCylinder + NodeShapeDottedCylinder, + NodeShapeSheet } from './node-shapes'; @@ -38,7 +39,8 @@ const nodeShapes = { octagon: NodeShapeOctagon, cloud: NodeShapeCloud, cylinder: NodeShapeCylinder, - dottedcylinder: NodeShapeDottedCylinder + dottedcylinder: NodeShapeDottedCylinder, + storagesheet: NodeShapeSheet }; function stackedShape(Shape) { diff --git a/client/app/scripts/constants/styles.js b/client/app/scripts/constants/styles.js index 049155bd2..545da60c1 100644 --- a/client/app/scripts/constants/styles.js +++ b/client/app/scripts/constants/styles.js @@ -32,6 +32,9 @@ export const UNIT_CYLINDER_PATH = 'm -1 -1.25' // this line is responsible for a + 'v -1.8' + 'a 1 0.4 0 0 0 -2 0'; +// Node Storage Sheet Shape +export const SHEET = 'm -1.2 -1.6 m 0.4 0 v 2.4 m -0.4 -2.4 v 2.4 h 2 v -2.4 z m 0 0.4 h 2'; + // NOTE: This value represents the node unit radius (in pixels). Since zooming is // controlled at the top level now, this renormalization would be obsolete (i.e. // value 1 could be used instead), if it wasn't for the following factors: diff --git a/client/app/scripts/utils/node-shape-utils.js b/client/app/scripts/utils/node-shape-utils.js index 07c05583e..c37f2fa1a 100644 --- a/client/app/scripts/utils/node-shape-utils.js +++ b/client/app/scripts/utils/node-shape-utils.js @@ -2,7 +2,7 @@ import React from 'react'; import range from 'lodash/range'; import { line, curveCardinalClosed } from 'd3-shape'; -import { UNIT_CLOUD_PATH, UNIT_CYLINDER_PATH } from '../constants/styles'; +import { UNIT_CLOUD_PATH, UNIT_CYLINDER_PATH, SHEET } from '../constants/styles'; export const pathElement = React.createFactory('path'); @@ -31,3 +31,4 @@ export const octagonShapeProps = { d: curvedUnitPolygonPath(8) }; export const cloudShapeProps = { d: UNIT_CLOUD_PATH }; export const cylinderShapeProps = { d: UNIT_CYLINDER_PATH }; export const dottedCylinderShapeProps = { d: UNIT_CYLINDER_PATH }; +export const sheetShapeProps = { d: SHEET }; diff --git a/client/app/styles/_base.scss b/client/app/styles/_base.scss index 35d234b13..05217bdd7 100644 --- a/client/app/styles/_base.scss +++ b/client/app/styles/_base.scss @@ -533,9 +533,19 @@ a { } .edge { - .link { + .link-none { + fill: none; + display: none; + } + .link-storage { fill: none; stroke: $edge-color; + stroke-dasharray: 1, 30; + stroke-linecap: round; + } + .link { + fill: none; + stroke: $edge-color } .shadow { fill: none; diff --git a/probe/kubernetes/client.go b/probe/kubernetes/client.go index dcf3b9d79..cbf91834b 100644 --- a/probe/kubernetes/client.go +++ b/probe/kubernetes/client.go @@ -15,6 +15,7 @@ import ( apibatchv2alpha1 "k8s.io/api/batch/v2alpha1" apiv1 "k8s.io/api/core/v1" apiextensionsv1beta1 "k8s.io/api/extensions/v1beta1" + storagev1 "k8s.io/api/storage/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" @@ -39,6 +40,7 @@ type Client interface { WalkNamespaces(f func(NamespaceResource) error) error WalkPersistentVolumes(f func(PersistentVolume) error) error WalkPersistentVolumeClaims(f func(PersistentVolumeClaim) error) error + WalkStorageClasses(f func(StorageClass) error) error WatchPods(f func(Event, Pod)) @@ -62,6 +64,7 @@ type client struct { namespaceStore cache.Store persistentVolumeStore cache.Store persistentVolumeClaimStore cache.Store + storageClassStore cache.Store podWatchesMutex sync.Mutex podWatches []func(Event, Pod) @@ -148,6 +151,7 @@ func NewClient(config ClientConfig) (Client, error) { result.cronJobStore = result.setupStore("cronjobs") result.persistentVolumeStore = result.setupStore("persistentvolumes") result.persistentVolumeClaimStore = result.setupStore("persistentvolumeclaims") + result.storageClassStore = result.setupStore("storageclasses") return result, nil } @@ -190,6 +194,8 @@ func (c *client) clientAndType(resource string) (rest.Interface, interface{}, er return c.client.CoreV1().RESTClient(), &apiv1.PersistentVolume{}, nil case "persistentvolumeclaims": return c.client.CoreV1().RESTClient(), &apiv1.PersistentVolumeClaim{}, nil + case "storageclasses": + return c.client.StorageV1().RESTClient(), &storagev1.StorageClass{}, nil case "deployments": return c.client.ExtensionsV1beta1().RESTClient(), &apiextensionsv1beta1.Deployment{}, nil case "daemonsets": @@ -292,6 +298,16 @@ func (c *client) WalkPersistentVolumeClaims(f func(PersistentVolumeClaim) error) return nil } +func (c *client) WalkStorageClasses(f func(StorageClass) error) error { + for _, m := range c.storageClassStore.List() { + sc := m.(*storagev1.StorageClass) + if err := f(NewStorageClass(sc)); err != nil { + return err + } + } + return nil +} + func (c *client) WalkServices(f func(Service) error) error { for _, m := range c.serviceStore.List() { s := m.(*apiv1.Service) diff --git a/probe/kubernetes/reporter.go b/probe/kubernetes/reporter.go index 37cbd914d..2b7e342c7 100644 --- a/probe/kubernetes/reporter.go +++ b/probe/kubernetes/reporter.go @@ -32,6 +32,7 @@ const ( Status = report.KubernetesStatus Message = report.KubernetesMessage VolumeName = report.KubernetesVolumeName + Provisioner = report.KubernetesProvisioner ) // Exposed for testing @@ -123,6 +124,12 @@ var ( AccessModes: {ID: AccessModes, Label: "Access Modes", From: report.FromLatest, Priority: 5}, } + StorageClassMetadataTemplates = report.MetadataTemplates{ + NodeType: {ID: NodeType, Label: "Type", From: report.FromLatest, Priority: 1}, + Name: {ID: Name, Label: "Name", From: report.FromLatest, Priority: 2}, + Provisioner: {ID: Provisioner, Label: "Provisioner", From: report.FromLatest, Priority: 3}, + } + TableTemplates = report.TableTemplates{ LabelPrefix: { ID: LabelPrefix, @@ -292,6 +299,10 @@ func (r *Reporter) Report() (report.Report, error) { if err != nil { return result, err } + storageClassTopology, _, err := r.storageClassTopology() + if err != nil { + return result, err + } result.Pod = result.Pod.Merge(podTopology) result.Service = result.Service.Merge(serviceTopology) result.Host = result.Host.Merge(hostTopology) @@ -302,6 +313,7 @@ func (r *Reporter) Report() (report.Report, error) { result.Namespace = result.Namespace.Merge(namespaceTopology) result.PersistentVolume = result.PersistentVolume.Merge(persistentVolumeTopology) result.PersistentVolumeClaim = result.PersistentVolumeClaim.Merge(persistentVolumeClaimTopology) + result.StorageClass = result.StorageClass.Merge(storageClassTopology) return result, nil } @@ -437,6 +449,19 @@ func (r *Reporter) persistentVolumeClaimTopology() (report.Topology, []Persisten return result, persistentVolumeClaims, err } +func (r *Reporter) storageClassTopology() (report.Topology, []StorageClass, error) { + storageClasses := []StorageClass{} + result := report.MakeTopology(). + WithMetadataTemplates(StorageClassMetadataTemplates). + WithTableTemplates(TableTemplates) + err := r.client.WalkStorageClasses(func(p StorageClass) error { + result.AddNode(p.GetNode(r.probeID)) + storageClasses = append(storageClasses, p) + return nil + }) + return result, storageClasses, err +} + type labelledChild interface { Labels() map[string]string AddParent(string, string) diff --git a/probe/kubernetes/reporter_test.go b/probe/kubernetes/reporter_test.go index e4aa304e8..54cda4204 100644 --- a/probe/kubernetes/reporter_test.go +++ b/probe/kubernetes/reporter_test.go @@ -154,6 +154,9 @@ func (c *mockClient) WalkPersistentVolumes(f func(kubernetes.PersistentVolume) e func (c *mockClient) WalkPersistentVolumeClaims(f func(kubernetes.PersistentVolumeClaim) error) error { return nil } +func (c *mockClient) WalkStorageClasses(f func(kubernetes.StorageClass) error) error { + return nil +} func (*mockClient) WatchPods(func(kubernetes.Event, kubernetes.Pod)) {} func (c *mockClient) GetLogs(namespaceID, podName string, _ []string) (io.ReadCloser, error) { r, ok := c.logs[namespaceID+";"+podName] diff --git a/probe/kubernetes/storageclass.go b/probe/kubernetes/storageclass.go new file mode 100644 index 000000000..6b758ceff --- /dev/null +++ b/probe/kubernetes/storageclass.go @@ -0,0 +1,33 @@ +package kubernetes + +import ( + "github.com/weaveworks/scope/report" + storagev1 "k8s.io/api/storage/v1" +) + +// StorageClass represent kubernetes StorageClass interface +type StorageClass interface { + Meta + GetNode(probeID string) report.Node +} + +// storageClass represents kubernetes storage classes +type storageClass struct { + *storagev1.StorageClass + Meta +} + +// NewStorageClass returns new Storage Class type +func NewStorageClass(p *storagev1.StorageClass) StorageClass { + return &storageClass{StorageClass: p, Meta: meta{p.ObjectMeta}} +} + +// GetNode returns StorageClass as Node +func (p *storageClass) GetNode(probeID string) report.Node { + return p.MetaNode(report.MakeStorageClassNodeID(p.UID())).WithLatests(map[string]string{ + report.ControlProbeID: probeID, + NodeType: "Storage Class", + Name: p.GetName(), + Provisioner: p.Provisioner, + }) +} diff --git a/render/detailed/node.go b/render/detailed/node.go index 5e7c5fc7d..329f20750 100644 --- a/render/detailed/node.go +++ b/render/detailed/node.go @@ -199,6 +199,13 @@ var nodeSummaryGroupSpecs = []struct { Columns: []Column{}, }, }, + { + topologyID: report.StorageClass, + NodeSummaryGroup: NodeSummaryGroup{ + Label: "Storage Classes", + Columns: []Column{}, + }, + }, } func children(rc RenderContext, n report.Node) []NodeSummaryGroup { diff --git a/render/detailed/summary.go b/render/detailed/summary.go index 3613f02b8..557717fb5 100644 --- a/render/detailed/summary.go +++ b/render/detailed/summary.go @@ -82,6 +82,7 @@ var renderers = map[string]func(BasicNodeSummary, report.Node) BasicNodeSummary{ report.Endpoint: nil, // Do not render report.PersistentVolume: persistentVolumeNodeSummary, report.PersistentVolumeClaim: persistentVolumeClaimNodeSummary, + report.StorageClass: storageClassNodeSummary, } // For each report.Topology, map to a 'primary' API topology. This can then be used in a variety of places. @@ -99,8 +100,9 @@ var primaryAPITopology = map[string]string{ report.ECSService: "ecs-services", report.SwarmService: "swarm-services", report.Host: "hosts", - report.PersistentVolume: "persistent-volumes", - report.PersistentVolumeClaim: "persistent-volumes", + report.PersistentVolume: "pods", + report.PersistentVolumeClaim: "pods", + report.StorageClass: "pods", } // MakeBasicNodeSummary returns a basic summary of a node, if @@ -385,6 +387,11 @@ func persistentVolumeClaimNodeSummary(base BasicNodeSummary, n report.Node) Basi return base } +func storageClassNodeSummary(base BasicNodeSummary, n report.Node) BasicNodeSummary { + base = addKubernetesLabelAndRank(base, n) + return base +} + // groupNodeSummary renders the summary for a group node. n.Topology is // expected to be of the form: group:container:hostname func groupNodeSummary(base BasicNodeSummary, r report.Report, n report.Node) BasicNodeSummary { diff --git a/render/expected/expected.go b/render/expected/expected.go index 4b10f1cf4..dcca130ff 100644 --- a/render/expected/expected.go +++ b/render/expected/expected.go @@ -329,6 +329,14 @@ var ( } RenderedPersistentVolume = report.Nodes{ + fixture.ClientPodNodeID: pod(fixture.ClientPodNodeID, fixture.PersistentVolumeClaimNodeID, fixture.ServerPodNodeID). + WithLatests(map[string]string{ + kubernetes.Name: "pong-a", + kubernetes.Namespace: "ping", + kubernetes.State: "running", + kubernetes.VolumeClaim: "pvc-6124", + }).WithChild(report.MakeNode(fixture.PersistentVolumeClaimNodeID).WithTopology(report.Pod)), + fixture.PersistentVolumeClaimNodeID: persistentVolumeClaim(fixture.PersistentVolumeClaimNodeID, fixture.PersistentVolumeNodeID). WithLatests(map[string]string{ kubernetes.Name: "pvc-6124", diff --git a/render/filters.go b/render/filters.go index cb2e60651..f5c6aa409 100644 --- a/render/filters.go +++ b/render/filters.go @@ -131,6 +131,35 @@ func IsConnected(node report.Node) bool { return ok } +// IsStorageComponent check whether given node is PV, PVC, SC or not +func IsStorageComponent(node report.Node) bool { + var storageComponent bool + if node.Topology == "persistent_volume" || node.Topology == "persistent_volume_claim" || node.Topology == "storage_class" { + storageComponent = true + } + if node.Topology == "pod" { + volumeClaim, ok := node.Latest.Lookup(kubernetes.VolumeClaim) + if !ok { + storageComponent = false + } else if volumeClaim == "" { + storageComponent = false + } else { + storageComponent = true + } + } + return storageComponent +} + +// IsNotStorageComponent check whether given node is PV, PVC, SC or not +func IsNotStorageComponent(node report.Node) bool { + var ok bool + ok = true + if node.Topology == "persistent_volume" || node.Topology == "persistent_volume_claim" || node.Topology == "storage_class" { + ok = false + } + return ok +} + // connected returns the node ids of nodes which have edges to/from // them, excluding edges to/from themselves. func connected(nodes report.Nodes) map[string]struct{} { diff --git a/render/persistentvolume.go b/render/persistentvolume.go index b2d201535..a926f2063 100644 --- a/render/persistentvolume.go +++ b/render/persistentvolume.go @@ -5,93 +5,34 @@ import ( "github.com/weaveworks/scope/report" ) -// PersistentVolumeRenderer is the common renderer for all the storage components. -var PersistentVolumeRenderer = Memoise( - MakeReduce( - ConnectionStorageJoin( - Map2PVName, - report.PersistentVolumeClaim, - ), - MakeFilter( - func(n report.Node) bool { - claimName, ok := n.Latest.Lookup(kubernetes.VolumeClaim) - if claimName == "" { - return !ok - } - return ok - }, - MakeReduce( - PropagateSingleMetrics(report.Container, - MakeMap( - Map3Parent([]string{report.Pod}), - MakeFilter( - ComposeFilterFuncs( - IsRunning, - Complement(isPauseContainer), - ), - ContainerWithImageNameRenderer, - ), - ), - ), - ConnectionStorageJoin( - Map2PVCName, - report.Pod, - ), - ), - ), - MapStorageEndpoints( - Map2PVNode, - report.PersistentVolume, - ), - ), -) - -// Map3Parent returns a MapFunc which maps Nodes to some parent grouping. -func Map3Parent( - // The topology IDs to look for parents in - topologies []string, -) MapFunc { - return func(n report.Node) report.Nodes { - 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) - result[id] = node - } - } - } - return result - } -} - // ConnectionStorageJoin returns connectionStorageJoin object -func ConnectionStorageJoin(toPV func(report.Node) string, topology string) Renderer { +func ConnectionStorageJoin(toPV func(report.Node) []string, topology string) Renderer { return connectionStorageJoin{toPV: toPV, topology: topology} } // connectionStorageJoin holds the information about mapping of storage components // along with TopologySelector type connectionStorageJoin struct { - toPV func(report.Node) string + toPV func(report.Node) []string topology string } func (c connectionStorageJoin) Render(rpt report.Report) Nodes { inputNodes := TopologySelector(c.topology).Render(rpt).Nodes - var pvcNodes = map[string]string{} + var pvcNodes = map[string][]string{} for _, n := range inputNodes { pvName := c.toPV(n) - pvcNodes[pvName] = n.ID + for _, name := range pvName { + pvcNodes[name] = append(pvcNodes[name], n.ID) + } } return MapStorageEndpoints( - func(m report.Node) string { + func(m report.Node) []string { pvName, ok := m.Latest.Lookup(kubernetes.Name) if !ok { - return "" + return []string{""} } id := pvcNodes[pvName] return id @@ -99,57 +40,81 @@ func (c connectionStorageJoin) Render(rpt report.Report) Nodes { } // Map2PVName accepts PV Node and returns Volume name associated with PV Node. -func Map2PVName(m report.Node) string { +func Map2PVName(m report.Node) []string { pvName, ok := m.Latest.Lookup(kubernetes.VolumeName) + scName, ok1 := m.Latest.Lookup(kubernetes.StorageClassName) if !ok { pvName = "" } - return pvName + if !ok1 { + scName = "" + } + return []string{pvName, scName} } // Map2PVCName returns pvc name -func Map2PVCName(m report.Node) string { +func Map2PVCName(m report.Node) []string { pvcName, ok := m.Latest.Lookup(kubernetes.VolumeClaim) if !ok { pvcName = "" } - return pvcName + return []string{pvcName} } // Map2PVNode returns pv node ID -func Map2PVNode(n report.Node) string { +func Map2PVNode(n report.Node) []string { if pvNodeID, ok := n.Latest.Lookup(report.MakePersistentVolumeNodeID(n.ID)); ok { - return pvNodeID + return []string{pvNodeID} } - return "" + return []string{""} } +type storageEndpointMapFunc func(report.Node) []string + // mapStorageEndpoints is the Renderer for rendering storage components together. type mapStorageEndpoints struct { - f endpointMapFunc + f storageEndpointMapFunc topology string } // MapStorageEndpoints instantiates mapStorageEndpoints and returns same -func MapStorageEndpoints(f endpointMapFunc, topology string) Renderer { +func MapStorageEndpoints(f storageEndpointMapFunc, topology string) Renderer { return mapStorageEndpoints{f: f, topology: topology} } func (e mapStorageEndpoints) Render(rpt report.Report) Nodes { - var endpoints Nodes - if e.topology == "persistent_volume_claim" { + if e.topology == report.PersistentVolumeClaim { endpoints = SelectPersistentVolume.Render(rpt) + endpoints.Merge(SelectStorageClass.Render(rpt)) } - if e.topology == "pod" { + if e.topology == report.Pod { endpoints = SelectPersistentVolumeClaim.Render(rpt) } ret := newJoinResults(TopologySelector(e.topology).Render(rpt).Nodes) for _, n := range endpoints.Nodes { - if id := e.f(n); id != "" { - ret.addChild(n, id, e.topology) + if id := e.f(n); len(id) > 0 { + for _, nodeID := range id { + if nodeID != "" { + ret.addChild(n, nodeID, e.topology) + } + } } } + if e.topology == report.PersistentVolumeClaim { + ret.storageResult(endpoints) + endpoints = SelectStorageClass.Render(rpt) + for _, n := range endpoints.Nodes { + if id := e.f(n); len(id) > 0 { + for _, nodeID := range id { + if nodeID != "" { + ret.addChild(n, nodeID, e.topology) + } + } + } + } + return ret.storageResult(endpoints) + } return ret.storageResult(endpoints) } diff --git a/render/pod.go b/render/pod.go index ee9c68b92..0b5b79401 100644 --- a/render/pod.go +++ b/render/pod.go @@ -26,6 +26,9 @@ func renderKubernetesTopologies(rpt report.Report) bool { &rpt.DaemonSet, &rpt.StatefulSet, &rpt.CronJob, + &rpt.PersistentVolume, + &rpt.PersistentVolumeClaim, + &rpt.StorageClass, } for _, t := range topologies { if len(t.Nodes) > 0 { @@ -62,6 +65,22 @@ var PodRenderer = Memoise(ConditionalRenderer(renderKubernetesTopologies, ), ), ConnectionJoin(MapPod2IP, report.Pod), + ConnectionStorageJoin( + Map2PVName, + report.PersistentVolumeClaim, + ), + ConnectionStorageJoin( + Map2PVCName, + report.Pod, + ), + MapStorageEndpoints( + Map2PVNode, + report.PersistentVolume, + ), + MapStorageEndpoints( + Map2PVNode, + report.StorageClass, + ), ), ), )) diff --git a/render/render.go b/render/render.go index 7151d962d..8c05eb0aa 100644 --- a/render/render.go +++ b/render/render.go @@ -267,6 +267,9 @@ func (ret *joinResults) storageResult(input Nodes) Nodes { } // Since PV and PVC will have only single adjacency ret.storageAdjacency(outID, n.ID) + for _, outID := range ret.multi[n.ID] { + ret.storageAdjacency(outID, n.ID) + } } return Nodes{Nodes: ret.nodes} } diff --git a/render/selectors.go b/render/selectors.go index 18fcfc20f..036f10720 100644 --- a/render/selectors.go +++ b/render/selectors.go @@ -34,4 +34,5 @@ var ( SelectOverlay = TopologySelector(report.Overlay) SelectPersistentVolume = TopologySelector(report.PersistentVolume) SelectPersistentVolumeClaim = TopologySelector(report.PersistentVolumeClaim) + SelectStorageClass = TopologySelector(report.StorageClass) ) diff --git a/report/id.go b/report/id.go index 44a265022..d6731f35b 100644 --- a/report/id.go +++ b/report/id.go @@ -169,6 +169,12 @@ var ( // ParsePersistentVolumeClaimNodeID parses a Persistent Volume Claim node ID ParsePersistentVolumeClaimNodeID = parseSingleComponentID("persistent_volume_claim") + + // MakeStorageClassNodeID produces a storage class node ID from its composite parts. + MakeStorageClassNodeID = makeSingleComponentID("storage_class") + + // ParseStorageClassNodeID parses a storage class node ID + ParseStorageClassNodeID = parseSingleComponentID("storage_class") ) // makeSingleComponentID makes a single-component node id encoder diff --git a/report/map_keys.go b/report/map_keys.go index 783386dd5..4f3f77393 100644 --- a/report/map_keys.go +++ b/report/map_keys.go @@ -78,6 +78,7 @@ const ( KubernetesStatus = "kubernetes_status" KubernetesMessage = "kubernetes_message" KubernetesVolumeName = "kubernetes_volume_name" + KubernetesProvisioner = "kubernetes_provisioner" // probe/awsecs ECSCluster = "ecs_cluster" ECSCreatedAt = "ecs_created_at" @@ -112,6 +113,7 @@ var commonKeys = map[string]string{ SwarmService: SwarmService, PersistentVolume: PersistentVolume, PersistentVolumeClaim: PersistentVolumeClaim, + StorageClass: StorageClass, HostNodeID: HostNodeID, ControlProbeID: ControlProbeID, diff --git a/report/report.go b/report/report.go index a6eca7add..415e8dc1b 100644 --- a/report/report.go +++ b/report/report.go @@ -31,6 +31,7 @@ const ( SwarmService = "swarm_service" PersistentVolume = "persistent_volume" PersistentVolumeClaim = "persistent_volume_claim" + StorageClass = "storage_class" // Shapes used for different nodes Circle = "circle" @@ -43,6 +44,7 @@ const ( Cloud = "cloud" Cylinder = "cylinder" DottedCylinder = "dottedcylinder" + StorageSheet = "storagesheet" // Used when counting the number of containers ContainersKey = "containers" @@ -69,6 +71,7 @@ var topologyNames = []string{ SwarmService, PersistentVolume, PersistentVolumeClaim, + StorageClass, } // Report is the core data type. It's produced by probes, and consumed and @@ -165,6 +168,10 @@ type Report struct { // Metadata is limited for now, more to come later. PersistentVolumeClaim Topology + // Storage Class represent all kubernetes Storage Classes on hosts running probes. + // Metadata is limited for now, more to come later. + StorageClass Topology + DNS DNSRecords // Sampling data for this report. @@ -266,6 +273,10 @@ func MakeReport() Report { WithShape(DottedCylinder). WithLabel("persistent volume claim", "persistent volume claims"), + StorageClass: MakeTopology(). + WithShape(StorageSheet). + WithLabel("storage class", "storage classes"), + DNS: DNSRecords{}, Sampling: Sampling{}, @@ -371,6 +382,8 @@ func (r *Report) topology(name string) *Topology { return &r.PersistentVolume case PersistentVolumeClaim: return &r.PersistentVolumeClaim + case StorageClass: + return &r.StorageClass } return nil }