diff --git a/app/api_topologies.go b/app/api_topologies.go index 5233ff26c..5d442f6a5 100644 --- a/app/api_topologies.go +++ b/app/api_topologies.go @@ -35,7 +35,6 @@ const ( ecsTasksID = "ecs-tasks" ecsServicesID = "ecs-services" swarmServicesID = "swarm-services" - persistentVolumesID = "persistent-volumes" ) var ( @@ -52,8 +51,8 @@ var ( 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}, + {Value: "show", Label: "Show Storage", filter: render.IsVolumesComponent, filterPseudo: false}, + {Value: "hide", Label: "Hide Storage", filter: render.IsPodComponent, filterPseudo: false}, }, } ) diff --git a/probe/kubernetes/persistentvolume.go b/probe/kubernetes/persistentvolume.go index 2d870db92..66a413b27 100644 --- a/probe/kubernetes/persistentvolume.go +++ b/probe/kubernetes/persistentvolume.go @@ -8,7 +8,9 @@ import ( // PersistentVolume represent kubernetes PersistentVolume interface type PersistentVolume interface { Meta - GetNode(probeID string) report.Node + GetNode() report.Node + GetAccessMode() string + GetVolume() string } // persistentVolume represents kubernetes persistent volume @@ -22,16 +24,34 @@ func NewPersistentVolume(p *apiv1.PersistentVolume) PersistentVolume { return &persistentVolume{PersistentVolume: p, Meta: meta{p.ObjectMeta}} } +// GetAccessMode returns access mode associated with PV +func (p *persistentVolume) GetAccessMode() string { + var accessMode string + + if len(p.Spec.AccessModes) > 0 { + // A volume can only be mounted using one access mode at a time, + //even if it supports many. + accessMode = string(p.Spec.AccessModes[0]) + } + return accessMode +} + +// GetVolume returns volume name +func (p *persistentVolume) GetVolume() string { + var volume string + if p.Spec.ClaimRef != nil { + volume = p.Spec.ClaimRef.Name + } + return volume +} + // GetNode returns Persistent Volume as Node -func (p *persistentVolume) GetNode(probeID string) report.Node { +func (p *persistentVolume) GetNode() report.Node { return p.MetaNode(report.MakePersistentVolumeNodeID(p.UID())).WithLatests(map[string]string{ - report.ControlProbeID: probeID, - NodeType: "Persistent Volume", - VolumeClaim: p.Spec.ClaimRef.Name, - StorageClassName: p.Spec.StorageClassName, - Status: string(p.Status.Phase), - AccessModes: string(p.Spec.AccessModes[0]), - ReclaimPolicy: string(p.Spec.PersistentVolumeReclaimPolicy), - Message: p.Status.Message, + NodeType: "Persistent Volume", + VolumeClaim: p.GetVolume(), + StorageClassName: p.Spec.StorageClassName, + Status: string(p.Status.Phase), + AccessModes: p.GetAccessMode(), }) } diff --git a/probe/kubernetes/persistentvolumeclaim.go b/probe/kubernetes/persistentvolumeclaim.go index 0f3a04a12..665ef4404 100644 --- a/probe/kubernetes/persistentvolumeclaim.go +++ b/probe/kubernetes/persistentvolumeclaim.go @@ -17,7 +17,7 @@ const ( type PersistentVolumeClaim interface { Meta Selector() (labels.Selector, error) - GetNode(probeID string) report.Node + GetNode() report.Node GetStorageClass() string } @@ -48,14 +48,12 @@ func (p *persistentVolumeClaim) GetStorageClass() string { } // GetNode returns Persistent Volume Claim as Node -func (p *persistentVolumeClaim) GetNode(probeID string) report.Node { +func (p *persistentVolumeClaim) GetNode() report.Node { return p.MetaNode(report.MakePersistentVolumeClaimNodeID(p.UID())).WithLatests(map[string]string{ - report.ControlProbeID: probeID, - NodeType: "Persistent Volume Claim", - Status: string(p.Status.Phase), - VolumeName: p.Spec.VolumeName, - AccessModes: string(p.Spec.AccessModes[0]), - StorageClassName: p.GetStorageClass(), + NodeType: "Persistent Volume Claim", + Status: string(p.Status.Phase), + VolumeName: p.Spec.VolumeName, + StorageClassName: p.GetStorageClass(), }) } diff --git a/probe/kubernetes/pod.go b/probe/kubernetes/pod.go index e4b723f9b..a681ac961 100644 --- a/probe/kubernetes/pod.go +++ b/probe/kubernetes/pod.go @@ -76,7 +76,7 @@ func (p *pod) RestartCount() uint { } func (p *pod) VolumeClaimName() string { - claimName := "" + var claimName string for _, volume := range p.Spec.Volumes { if volume.VolumeSource.PersistentVolumeClaim != nil { claimName = volume.VolumeSource.PersistentVolumeClaim.ClaimName @@ -92,7 +92,10 @@ func (p *pod) GetNode(probeID string) report.Node { IP: p.Status.PodIP, report.ControlProbeID: probeID, RestartCount: strconv.FormatUint(uint64(p.RestartCount()), 10), - VolumeClaim: p.VolumeClaimName(), + } + + if p.VolumeClaimName() != "" { + latests[VolumeClaim] = p.VolumeClaimName() } if p.Pod.Spec.HostNetwork { diff --git a/probe/kubernetes/reporter.go b/probe/kubernetes/reporter.go index 2b7e342c7..8e8f9e6f9 100644 --- a/probe/kubernetes/reporter.go +++ b/probe/kubernetes/reporter.go @@ -429,7 +429,7 @@ func (r *Reporter) persistentVolumeTopology() (report.Topology, []PersistentVolu WithMetadataTemplates(PersistentVolumeMetadataTemplates). WithTableTemplates(TableTemplates) err := r.client.WalkPersistentVolumes(func(p PersistentVolume) error { - result.AddNode(p.GetNode(r.probeID)) + result.AddNode(p.GetNode()) persistentVolumes = append(persistentVolumes, p) return nil }) @@ -442,7 +442,7 @@ func (r *Reporter) persistentVolumeClaimTopology() (report.Topology, []Persisten WithMetadataTemplates(PersistentVolumeClaimMetadataTemplates). WithTableTemplates(TableTemplates) err := r.client.WalkPersistentVolumeClaims(func(p PersistentVolumeClaim) error { - result.AddNode(p.GetNode(r.probeID)) + result.AddNode(p.GetNode()) persistentVolumeClaims = append(persistentVolumeClaims, p) return nil }) @@ -455,7 +455,7 @@ func (r *Reporter) storageClassTopology() (report.Topology, []StorageClass, erro WithMetadataTemplates(StorageClassMetadataTemplates). WithTableTemplates(TableTemplates) err := r.client.WalkStorageClasses(func(p StorageClass) error { - result.AddNode(p.GetNode(r.probeID)) + result.AddNode(p.GetNode()) storageClasses = append(storageClasses, p) return nil }) diff --git a/probe/kubernetes/storageclass.go b/probe/kubernetes/storageclass.go index 6b758ceff..832248528 100644 --- a/probe/kubernetes/storageclass.go +++ b/probe/kubernetes/storageclass.go @@ -8,7 +8,7 @@ import ( // StorageClass represent kubernetes StorageClass interface type StorageClass interface { Meta - GetNode(probeID string) report.Node + GetNode() report.Node } // storageClass represents kubernetes storage classes @@ -23,11 +23,10 @@ func NewStorageClass(p *storagev1.StorageClass) StorageClass { } // GetNode returns StorageClass as Node -func (p *storageClass) GetNode(probeID string) report.Node { +func (p *storageClass) GetNode() 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, + NodeType: "Storage Class", + Name: p.GetName(), + Provisioner: p.Provisioner, }) } diff --git a/render/expected/expected.go b/render/expected/expected.go index dcca130ff..290ceb128 100644 --- a/render/expected/expected.go +++ b/render/expected/expected.go @@ -12,12 +12,14 @@ import ( // Exported for testing. var ( - circle = "circle" - square = "square" - heptagon = "heptagon" - hexagon = "hexagon" - cloud = "cloud" - cylinder = "cylinder" + circle = "circle" + square = "square" + heptagon = "heptagon" + hexagon = "hexagon" + cloud = "cloud" + cylinder = "cylinder" + dottedcylinder = "dottedcylinder" + storagesheet = "storagesheet" // Helper to make a report.node with some common options node = func(topology string) func(id string, adjacent ...string) report.Node { @@ -41,6 +43,7 @@ var ( hostNode = node(report.Host) persistentVolume = node(report.PersistentVolume) persistentVolumeClaim = node(report.PersistentVolumeClaim) + StorageClass = node(report.StorageClass) UnknownPseudoNode1ID = render.MakePseudoNodeID(fixture.UnknownClient1IP) UnknownPseudoNode2ID = render.MakePseudoNodeID(fixture.UnknownClient3IP) @@ -269,6 +272,32 @@ var ( RenderedContainers[fixture.ServerContainerNodeID], )), + fixture.PersistentVolumeClaimNodeID: persistentVolumeClaim(fixture.PersistentVolumeClaimNodeID, fixture.PersistentVolumeNodeID). + WithLatests(map[string]string{ + kubernetes.Name: "pvc-6124", + kubernetes.Namespace: "ping", + kubernetes.Status: "bound", + kubernetes.VolumeName: "pongvolume", + kubernetes.AccessModes: "ReadWriteOnce", + kubernetes.StorageClassName: "standard", + }).WithChild(report.MakeNode(fixture.PersistentVolumeNodeID).WithTopology(report.PersistentVolume)), + + fixture.PersistentVolumeNodeID: persistentVolume(fixture.PersistentVolumeNodeID). + WithLatests(map[string]string{ + kubernetes.Name: "pongvolume", + kubernetes.Namespace: "ping", + kubernetes.Status: "bound", + kubernetes.VolumeClaim: "pvc-6124", + kubernetes.AccessModes: "ReadWriteOnce", + kubernetes.StorageClassName: "standard", + }), + + fixture.StorageClassNodeID: StorageClass(fixture.StorageClassNodeID, fixture.PersistentVolumeClaimNodeID). + WithLatests(map[string]string{ + kubernetes.Name: "standard", + kubernetes.Provisioner: "pong", + }).WithChild(report.MakeNode(fixture.PersistentVolumeClaimNodeID).WithTopology(report.PersistentVolumeClaim)), + UnmanagedServerID: unmanagedServerNode, render.IncomingInternetID: theIncomingInternetNode(fixture.ServerPodNodeID), render.OutgoingInternetID: theOutgoingInternetNode, @@ -327,36 +356,6 @@ var ( render.IncomingInternetID: theIncomingInternetNode(fixture.ServerHostNodeID), render.OutgoingInternetID: theOutgoingInternetNode, } - - 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", - kubernetes.Namespace: "ping", - kubernetes.Status: "bound", - kubernetes.VolumeName: "pongvolume", - kubernetes.AccessModes: "ReadWriteOnce", - kubernetes.StorageClassName: "standard", - }).WithChild(report.MakeNode(fixture.PersistentVolumeNodeID).WithTopology(report.PersistentVolume)), - - fixture.PersistentVolumeNodeID: persistentVolume(fixture.PersistentVolumeNodeID). - WithLatests(map[string]string{ - kubernetes.Name: "pongvolume", - kubernetes.Namespace: "ping", - kubernetes.Status: "bound", - kubernetes.VolumeClaim: "pvc-6124", - kubernetes.AccessModes: "ReadWriteOnce", - kubernetes.StorageClassName: "standard", - }), - } ) func newu64(value uint64) *uint64 { return &value } diff --git a/render/filters.go b/render/filters.go index f5c6aa409..385f8f21c 100644 --- a/render/filters.go +++ b/render/filters.go @@ -80,10 +80,7 @@ func (f FilterFunc) Transform(nodes Nodes) Nodes { newAdjacency = newAdjacency.Add(dstID) } } - claimName, ok := node.Latest.Lookup(kubernetes.VolumeClaim) - if claimName == "" || !ok { - node.Adjacency = newAdjacency - } + node.Adjacency = newAdjacency output[id] = node } @@ -131,8 +128,8 @@ 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 { +// IsVolumesComponent check whether given node is PV, PVC, SC or not +func IsVolumesComponent(node report.Node) bool { var storageComponent bool if node.Topology == "persistent_volume" || node.Topology == "persistent_volume_claim" || node.Topology == "storage_class" { storageComponent = true @@ -150,8 +147,8 @@ func IsStorageComponent(node report.Node) bool { return storageComponent } -// IsNotStorageComponent check whether given node is PV, PVC, SC or not -func IsNotStorageComponent(node report.Node) bool { +// IsPodComponent check whether given node is everything but PV, PVC, SC +func IsPodComponent(node report.Node) bool { var ok bool ok = true if node.Topology == "persistent_volume" || node.Topology == "persistent_volume_claim" || node.Topology == "storage_class" { diff --git a/render/persistentvolume.go b/render/persistentvolume.go index a926f2063..98197473f 100644 --- a/render/persistentvolume.go +++ b/render/persistentvolume.go @@ -5,116 +5,84 @@ import ( "github.com/weaveworks/scope/report" ) -// ConnectionStorageJoin returns connectionStorageJoin object -func ConnectionStorageJoin(toPV func(report.Node) []string, topology string) Renderer { - return connectionStorageJoin{toPV: toPV, topology: topology} -} +// KubernetesVolumesRenderer is a Renderer which combines all Kubernetes +// volumes components such as stateful Pods, Persistent Volume, Persistent Volume Claim, Storage Class. +var KubernetesVolumesRenderer = MakeReduce( + VolumesRenderer, + PodToVolumeRenderer, + PVCToStorageClassRenderer, +) -// connectionStorageJoin holds the information about mapping of storage components -// along with TopologySelector -type connectionStorageJoin struct { - toPV func(report.Node) []string - topology string -} +// VolumesRenderer is a Renderer which produces a renderable kubernetes PV & PVC +// graph by merging the pods graph and the Persistent Volume topology. +var VolumesRenderer = volumesRenderer{} -func (c connectionStorageJoin) Render(rpt report.Report) Nodes { - inputNodes := TopologySelector(c.topology).Render(rpt).Nodes +// volumesRenderer is a Renderer to render PV & PVC nodes. +type volumesRenderer struct{} - var pvcNodes = map[string][]string{} - for _, n := range inputNodes { - pvName := c.toPV(n) - for _, name := range pvName { - pvcNodes[name] = append(pvcNodes[name], n.ID) - } - } - - return MapStorageEndpoints( - func(m report.Node) []string { - pvName, ok := m.Latest.Lookup(kubernetes.Name) - if !ok { - return []string{""} +// Render renders PV & PVC nodes along with adjacency +func (v volumesRenderer) Render(rpt report.Report) Nodes { + nodes := make(report.Nodes) + for id, n := range rpt.PersistentVolumeClaim.Nodes { + volume, _ := n.Latest.Lookup(kubernetes.VolumeName) + for pvNodeID, p := range rpt.PersistentVolume.Nodes { + volumeName, _ := p.Latest.Lookup(kubernetes.Name) + if volume == volumeName { + n.Adjacency = n.Adjacency.Add(p.ID) + n.Children = n.Children.Add(p) } - id := pvcNodes[pvName] - return id - }, c.topology).Render(rpt) + nodes[pvNodeID] = p + } + nodes[id] = n + } + return Nodes{Nodes: nodes} } -// Map2PVName accepts PV Node and returns Volume name associated with PV Node. -func Map2PVName(m report.Node) []string { - pvName, ok := m.Latest.Lookup(kubernetes.VolumeName) - scName, ok1 := m.Latest.Lookup(kubernetes.StorageClassName) - if !ok { - pvName = "" - } - if !ok1 { - scName = "" - } - return []string{pvName, scName} -} +// PodToVolumeRenderer is a Renderer which produces a renderable kubernetes Pod +// graph by merging the pods graph and the Persistent Volume Claim topology. +// Pods having persistent volumes are rendered. +var PodToVolumeRenderer = podToVolumesRenderer{} -// Map2PVCName returns pvc name -func Map2PVCName(m report.Node) []string { - pvcName, ok := m.Latest.Lookup(kubernetes.VolumeClaim) - if !ok { - pvcName = "" - } - return []string{pvcName} -} +// VolumesRenderer is a Renderer to render Pods & PVCs. +type podToVolumesRenderer struct{} -// Map2PVNode returns pv node ID -func Map2PVNode(n report.Node) []string { - if pvNodeID, ok := n.Latest.Lookup(report.MakePersistentVolumeNodeID(n.ID)); ok { - return []string{pvNodeID} - } - return []string{""} -} - -type storageEndpointMapFunc func(report.Node) []string - -// mapStorageEndpoints is the Renderer for rendering storage components together. -type mapStorageEndpoints struct { - f storageEndpointMapFunc - topology string -} - -// MapStorageEndpoints instantiates mapStorageEndpoints and returns same -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 == report.PersistentVolumeClaim { - endpoints = SelectPersistentVolume.Render(rpt) - endpoints.Merge(SelectStorageClass.Render(rpt)) - } - 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); len(id) > 0 { - for _, nodeID := range id { - if nodeID != "" { - ret.addChild(n, nodeID, e.topology) - } +// Render renders the Pod nodes having volumes adjacency. +func (v podToVolumesRenderer) Render(rpt report.Report) Nodes { + nodes := make(report.Nodes) + for podID, podNode := range rpt.Pod.Nodes { + ClaimName, _ := podNode.Latest.Lookup(kubernetes.VolumeClaim) + for _, pvcNode := range rpt.PersistentVolumeClaim.Nodes { + pvcName, _ := pvcNode.Latest.Lookup(kubernetes.Name) + if pvcName == ClaimName { + podNode.Adjacency = podNode.Adjacency.Add(pvcNode.ID) + podNode.Children = podNode.Children.Add(pvcNode) } } + nodes[podID] = podNode } - 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 Nodes{Nodes: nodes} +} + +// PVCToStorageClassRenderer is a Renderer which produces a renderable kubernetes PVC +// & Storage class graph. +var PVCToStorageClassRenderer = pvcToStorageClassRenderer{} + +// pvcToStorageClassRenderer is a Renderer to render PVC & StorageClass. +type pvcToStorageClassRenderer struct{} + +// Render renders the PVC & Storage Class nodes with adjacency. +func (v pvcToStorageClassRenderer) Render(rpt report.Report) Nodes { + nodes := make(report.Nodes) + for scID, scNode := range rpt.StorageClass.Nodes { + storageClass, _ := scNode.Latest.Lookup(kubernetes.Name) + for _, pvcNode := range rpt.PersistentVolumeClaim.Nodes { + storageClassName, _ := pvcNode.Latest.Lookup(kubernetes.StorageClassName) + if storageClassName == storageClass { + scNode.Adjacency = scNode.Adjacency.Add(pvcNode.ID) + scNode.Children = scNode.Children.Add(pvcNode) } } - return ret.storageResult(endpoints) + nodes[scID] = scNode } - return ret.storageResult(endpoints) + return Nodes{Nodes: nodes} } diff --git a/render/persistentvolume_test.go b/render/persistentvolume_test.go deleted file mode 100644 index b0ce305f4..000000000 --- a/render/persistentvolume_test.go +++ /dev/null @@ -1,20 +0,0 @@ -package render_test - -import ( - "testing" - - "github.com/weaveworks/common/test" - "github.com/weaveworks/scope/render" - "github.com/weaveworks/scope/render/expected" - "github.com/weaveworks/scope/test/fixture" - "github.com/weaveworks/scope/test/reflect" - "github.com/weaveworks/scope/test/utils" -) - -func TestPersistentVolumeRenderer(t *testing.T) { - have := utils.Prune(render.PersistentVolumeRenderer.Render(fixture.Report).Nodes) - want := utils.Prune(expected.RenderedPersistentVolume) - if !reflect.DeepEqual(want, have) { - t.Error(test.Diff(want, have)) - } -} diff --git a/render/pod.go b/render/pod.go index 0b5b79401..e5a8d3009 100644 --- a/render/pod.go +++ b/render/pod.go @@ -65,22 +65,7 @@ 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, - ), + KubernetesVolumesRenderer, ), ), )) diff --git a/render/render.go b/render/render.go index 8c05eb0aa..ff4e57924 100644 --- a/render/render.go +++ b/render/render.go @@ -251,29 +251,6 @@ func (ret *joinResults) rewriteAdjacency(outID string, adjacency report.IDList) ret.nodes[outID] = out } -// storageAdjacency sets adjacency for the given node ID -func (ret *joinResults) storageAdjacency(outID string, adjacency string) { - out := ret.nodes[outID] - out.Adjacency = out.Adjacency.Add(adjacency) - ret.nodes[outID] = out -} - -// storageResult returns Nodes for after adding adjacencies -func (ret *joinResults) storageResult(input Nodes) Nodes { - for _, n := range input.Nodes { - outID, ok := ret.mapped[n.ID] - if !ok { - continue - } - // 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} -} - // ResetCache blows away the rendered node cache, and known service // cache. func ResetCache() { diff --git a/test/fixture/report_fixture.go b/test/fixture/report_fixture.go index 60fe9b50f..00f883064 100644 --- a/test/fixture/report_fixture.go +++ b/test/fixture/report_fixture.go @@ -105,6 +105,8 @@ var ( PersistentVolumeNodeID = report.MakePersistentVolumeNodeID(PersistentVolumeUID) PersistentVolumeClaimUID = "pvc1234" PersistentVolumeClaimNodeID = report.MakePersistentVolumeClaimNodeID(PersistentVolumeClaimUID) + StorageClassUID = "sc1234" + StorageClassNodeID = report.MakeStorageClassNodeID(StorageClassUID) ClientProcess1CPUMetric = report.MakeSingletonMetric(Now.Add(-1*time.Second), 0.01) ClientProcess1MemoryMetric = report.MakeSingletonMetric(Now.Add(-2*time.Second), 0.02) @@ -366,7 +368,7 @@ var ( }). WithTopology(report.PersistentVolumeClaim), }, - }.WithShape(report.Cylinder).WithLabel("persistent volume claim", "persistent volume claims"), + }.WithShape(report.DottedCylinder).WithLabel("persistent volume claim", "persistent volume claims"), PersistentVolume: report.Topology{ Nodes: report.Nodes{ PersistentVolumeNodeID: report.MakeNodeWith( @@ -382,6 +384,17 @@ var ( WithTopology(report.PersistentVolume), }, }.WithShape(report.Cylinder).WithLabel("persistent volume", "persistent volumes"), + StorageClass: report.Topology{ + Nodes: report.Nodes{ + StorageClassNodeID: report.MakeNodeWith( + + StorageClassNodeID, map[string]string{ + kubernetes.Name: "standard", + kubernetes.Provisioner: "pong", + }). + WithTopology(report.StorageClass), + }, + }.WithShape(report.StorageSheet).WithLabel("storage class", "storage classes"), Sampling: report.Sampling{ Count: 1024, Total: 4096,