diff --git a/client/app/scripts/charts/node-shapes.js b/client/app/scripts/charts/node-shapes.js index fb2dfb700..9babd0115 100644 --- a/client/app/scripts/charts/node-shapes.js +++ b/client/app/scripts/charts/node-shapes.js @@ -86,4 +86,4 @@ export const NodeShapeHeptagon = props => NodeShape('heptagon', pathElement, hep export const NodeShapeOctagon = props => NodeShape('octagon', pathElement, octagonShapeProps, props); 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); \ No newline at end of file +export const NodeShapeDottedCylinder = props => NodeShape('dottedcylinder', pathElement, dottedCylinderShapeProps, props); diff --git a/client/app/scripts/charts/node.js b/client/app/scripts/charts/node.js index 4c18f38e6..6f0dc039d 100644 --- a/client/app/scripts/charts/node.js +++ b/client/app/scripts/charts/node.js @@ -186,4 +186,4 @@ function mapStateToProps(state) { export default connect( mapStateToProps, { clickNode, enterNode, leaveNode } -)(Node); \ No newline at end of file +)(Node); diff --git a/client/app/scripts/constants/styles.js b/client/app/scripts/constants/styles.js index b45841f51..049155bd2 100644 --- a/client/app/scripts/constants/styles.js +++ b/client/app/scripts/constants/styles.js @@ -99,4 +99,4 @@ export const NODE_DETAILS_TABLE_XS_LABEL = { count: '#', // TODO: consider changing the name of this field on the BE container: '#', -}; \ No newline at end of file +}; diff --git a/client/app/scripts/utils/node-shape-utils.js b/client/app/scripts/utils/node-shape-utils.js index ee0f0dd75..07c05583e 100644 --- a/client/app/scripts/utils/node-shape-utils.js +++ b/client/app/scripts/utils/node-shape-utils.js @@ -30,4 +30,4 @@ export const heptagonShapeProps = { d: curvedUnitPolygonPath(7) }; 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 }; \ No newline at end of file +export const dottedCylinderShapeProps = { d: UNIT_CYLINDER_PATH }; diff --git a/probe/kubernetes/meta.go b/probe/kubernetes/meta.go index 2b6a15bdd..d660d2535 100644 --- a/probe/kubernetes/meta.go +++ b/probe/kubernetes/meta.go @@ -10,10 +10,11 @@ import ( // These constants are keys used in node metadata const ( - Name = report.KubernetesName - Namespace = report.KubernetesNamespace - Created = report.KubernetesCreated - LabelPrefix = "kubernetes_labels_" + Name = report.KubernetesName + Namespace = report.KubernetesNamespace + Created = report.KubernetesCreated + LabelPrefix = "kubernetes_labels_" + VolumeClaimName = report.KubernetesVolumeClaim ) // Meta represents a metadata information about a Kubernetes object diff --git a/probe/kubernetes/persistentvolumeclaim.go b/probe/kubernetes/persistentvolumeclaim.go index 1f0c0121d..0f3a04a12 100644 --- a/probe/kubernetes/persistentvolumeclaim.go +++ b/probe/kubernetes/persistentvolumeclaim.go @@ -8,11 +8,17 @@ import ( "k8s.io/apimachinery/pkg/labels" ) +const ( + // BetaStorageClassAnnotation is the annotation for default storage class + BetaStorageClassAnnotation = "volume.beta.kubernetes.io/storage-class" +) + // PersistentVolumeClaim represents kubernetes PVC interface type PersistentVolumeClaim interface { Meta Selector() (labels.Selector, error) GetNode(probeID string) report.Node + GetStorageClass() string } // persistentVolumeClaim represents kubernetes Persistent Volume Claims @@ -26,15 +32,30 @@ func NewPersistentVolumeClaim(p *apiv1.PersistentVolumeClaim) PersistentVolumeCl return &persistentVolumeClaim{PersistentVolumeClaim: p, Meta: meta{p.ObjectMeta}} } +// GetStorageClass will fetch storage class name from given PVC +func (p *persistentVolumeClaim) GetStorageClass() string { + + // Use Beta storage class annotation first + storageClassName := p.Annotations[BetaStorageClassAnnotation] + if storageClassName != "" { + return storageClassName + } + if p.Spec.StorageClassName != nil { + storageClassName = *p.Spec.StorageClassName + } + + return storageClassName +} + // GetNode returns Persistent Volume Claim as Node func (p *persistentVolumeClaim) GetNode(probeID string) report.Node { return p.MetaNode(report.MakePersistentVolumeClaimNodeID(p.UID())).WithLatests(map[string]string{ report.ControlProbeID: probeID, NodeType: "Persistent Volume Claim", - Namespace: p.GetNamespace(), Status: string(p.Status.Phase), VolumeName: p.Spec.VolumeName, AccessModes: string(p.Spec.AccessModes[0]), + StorageClassName: p.GetStorageClass(), }) } diff --git a/probe/kubernetes/pod.go b/probe/kubernetes/pod.go index 9c8283dbd..e4b723f9b 100644 --- a/probe/kubernetes/pod.go +++ b/probe/kubernetes/pod.go @@ -75,12 +75,24 @@ func (p *pod) RestartCount() uint { return count } +func (p *pod) VolumeClaimName() string { + claimName := "" + for _, volume := range p.Spec.Volumes { + if volume.VolumeSource.PersistentVolumeClaim != nil { + claimName = volume.VolumeSource.PersistentVolumeClaim.ClaimName + break + } + } + return claimName +} + func (p *pod) GetNode(probeID string) report.Node { latests := map[string]string{ State: p.State(), IP: p.Status.PodIP, report.ControlProbeID: probeID, RestartCount: strconv.FormatUint(uint64(p.RestartCount()), 10), + VolumeClaim: p.VolumeClaimName(), } if p.Pod.Spec.HostNetwork { diff --git a/render/expected/expected.go b/render/expected/expected.go index f687f501d..4b10f1cf4 100644 --- a/render/expected/expected.go +++ b/render/expected/expected.go @@ -3,6 +3,7 @@ package expected import ( "github.com/weaveworks/scope/probe/docker" "github.com/weaveworks/scope/probe/host" + "github.com/weaveworks/scope/probe/kubernetes" "github.com/weaveworks/scope/probe/process" "github.com/weaveworks/scope/render" "github.com/weaveworks/scope/report" @@ -16,6 +17,7 @@ var ( heptagon = "heptagon" hexagon = "hexagon" cloud = "cloud" + cylinder = "cylinder" // Helper to make a report.node with some common options node = func(topology string) func(id string, adjacent ...string) report.Node { @@ -37,6 +39,8 @@ var ( pod = node(report.Pod) service = node(report.Service) hostNode = node(report.Host) + persistentVolume = node(report.PersistentVolume) + persistentVolumeClaim = node(report.PersistentVolumeClaim) UnknownPseudoNode1ID = render.MakePseudoNodeID(fixture.UnknownClient1IP) UnknownPseudoNode2ID = render.MakePseudoNodeID(fixture.UnknownClient3IP) @@ -323,6 +327,28 @@ var ( render.IncomingInternetID: theIncomingInternetNode(fixture.ServerHostNodeID), render.OutgoingInternetID: theOutgoingInternetNode, } + + RenderedPersistentVolume = report.Nodes{ + 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 7cd77c9b8..cb2e60651 100644 --- a/render/filters.go +++ b/render/filters.go @@ -80,7 +80,10 @@ func (f FilterFunc) Transform(nodes Nodes) Nodes { newAdjacency = newAdjacency.Add(dstID) } } - node.Adjacency = newAdjacency + claimName, ok := node.Latest.Lookup(kubernetes.VolumeClaim) + if claimName == "" || !ok { + node.Adjacency = newAdjacency + } output[id] = node } diff --git a/render/persistentvolume.go b/render/persistentvolume.go new file mode 100644 index 000000000..b2d201535 --- /dev/null +++ b/render/persistentvolume.go @@ -0,0 +1,155 @@ +package render + +import ( + "github.com/weaveworks/scope/probe/kubernetes" + "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 { + 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 + topology string +} + +func (c connectionStorageJoin) Render(rpt report.Report) Nodes { + inputNodes := TopologySelector(c.topology).Render(rpt).Nodes + + var pvcNodes = map[string]string{} + for _, n := range inputNodes { + pvName := c.toPV(n) + pvcNodes[pvName] = n.ID + } + + return MapStorageEndpoints( + func(m report.Node) string { + pvName, ok := m.Latest.Lookup(kubernetes.Name) + if !ok { + return "" + } + id := pvcNodes[pvName] + return id + }, c.topology).Render(rpt) +} + +// 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) + if !ok { + pvName = "" + } + return pvName +} + +// Map2PVCName returns pvc name +func Map2PVCName(m report.Node) string { + pvcName, ok := m.Latest.Lookup(kubernetes.VolumeClaim) + if !ok { + pvcName = "" + } + return pvcName +} + +// Map2PVNode returns pv node ID +func Map2PVNode(n report.Node) string { + if pvNodeID, ok := n.Latest.Lookup(report.MakePersistentVolumeNodeID(n.ID)); ok { + return pvNodeID + } + return "" +} + +// mapStorageEndpoints is the Renderer for rendering storage components together. +type mapStorageEndpoints struct { + f endpointMapFunc + topology string +} + +// MapStorageEndpoints instantiates mapStorageEndpoints and returns same +func MapStorageEndpoints(f endpointMapFunc, 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" { + endpoints = SelectPersistentVolume.Render(rpt) + } + if e.topology == "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) + } + } + return ret.storageResult(endpoints) +} diff --git a/render/persistentvolume_test.go b/render/persistentvolume_test.go new file mode 100644 index 000000000..b0ce305f4 --- /dev/null +++ b/render/persistentvolume_test.go @@ -0,0 +1,20 @@ +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/render.go b/render/render.go index ff4e57924..7151d962d 100644 --- a/render/render.go +++ b/render/render.go @@ -251,6 +251,26 @@ 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) + } + 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 dea5b40af..60fe9b50f 100644 --- a/test/fixture/report_fixture.go +++ b/test/fixture/report_fixture.go @@ -93,14 +93,18 @@ var ( ClientContainerImageName = "image/client" ServerContainerImageName = "image/server" - KubernetesNamespace = "ping" - ClientPodUID = "5d4c3b2a1" - ServerPodUID = "i9h8g7f6e" - ClientPodNodeID = report.MakePodNodeID(ClientPodUID) - ServerPodNodeID = report.MakePodNodeID(ServerPodUID) - ServiceName = "pongservice" - ServiceUID = "service1234" - ServiceNodeID = report.MakeServiceNodeID(ServiceUID) + KubernetesNamespace = "ping" + ClientPodUID = "5d4c3b2a1" + ServerPodUID = "i9h8g7f6e" + ClientPodNodeID = report.MakePodNodeID(ClientPodUID) + ServerPodNodeID = report.MakePodNodeID(ServerPodUID) + ServiceName = "pongservice" + ServiceUID = "service1234" + ServiceNodeID = report.MakeServiceNodeID(ServiceUID) + PersistentVolumeUID = "pv1234" + PersistentVolumeNodeID = report.MakePersistentVolumeNodeID(PersistentVolumeUID) + PersistentVolumeClaimUID = "pvc1234" + PersistentVolumeClaimNodeID = report.MakePersistentVolumeClaimNodeID(PersistentVolumeClaimUID) ClientProcess1CPUMetric = report.MakeSingletonMetric(Now.Add(-1*time.Second), 0.01) ClientProcess1MemoryMetric = report.MakeSingletonMetric(Now.Add(-2*time.Second), 0.02) @@ -348,6 +352,36 @@ var ( WithTopology(report.Service), }, }.WithShape(report.Heptagon).WithLabel("service", "services"), + PersistentVolumeClaim: report.Topology{ + Nodes: report.Nodes{ + PersistentVolumeClaimNodeID: report.MakeNodeWith( + + PersistentVolumeClaimNodeID, map[string]string{ + kubernetes.Name: "pvc-6124", + kubernetes.Namespace: "ping", + kubernetes.Status: "bound", + kubernetes.VolumeName: "pongvolume", + kubernetes.AccessModes: "ReadWriteOnce", + kubernetes.StorageClassName: "standard", + }). + WithTopology(report.PersistentVolumeClaim), + }, + }.WithShape(report.Cylinder).WithLabel("persistent volume claim", "persistent volume claims"), + PersistentVolume: report.Topology{ + Nodes: report.Nodes{ + PersistentVolumeNodeID: report.MakeNodeWith( + + PersistentVolumeNodeID, map[string]string{ + kubernetes.Name: "pongvolume", + kubernetes.Namespace: "ping", + kubernetes.Status: "bound", + kubernetes.VolumeClaim: "pvc-6124", + kubernetes.AccessModes: "ReadWriteOnce", + kubernetes.StorageClassName: "standard", + }). + WithTopology(report.PersistentVolume), + }, + }.WithShape(report.Cylinder).WithLabel("persistent volume", "persistent volumes"), Sampling: report.Sampling{ Count: 1024, Total: 4096,