From 029bbbcec8e3faa4dfe12051454330201f3331cb Mon Sep 17 00:00:00 2001 From: Akash Srivastava Date: Mon, 1 Apr 2019 10:00:52 +0530 Subject: [PATCH] Add describe control to all k8s resources This commit will add a new control i.e. Describe which will describe a k8s resource similar to kubectl describe. Signed-off-by: Akash Srivastava --- probe/kubernetes/client.go | 48 ++++ probe/kubernetes/controls.go | 282 ++++++++++++++++++++++ probe/kubernetes/cronjob.go | 4 +- probe/kubernetes/daemonset.go | 2 +- probe/kubernetes/deployment.go | 2 +- probe/kubernetes/persistentvolume.go | 19 +- probe/kubernetes/persistentvolumeclaim.go | 2 +- probe/kubernetes/pod.go | 2 +- probe/kubernetes/reporter.go | 22 +- probe/kubernetes/reporter_test.go | 5 + probe/kubernetes/service.go | 4 +- probe/kubernetes/statefulset.go | 4 +- probe/kubernetes/storageclass.go | 13 +- probe/kubernetes/volumesnapshot.go | 2 +- probe/kubernetes/volumesnapshotdata.go | 2 +- report/map_keys.go | 1 + 16 files changed, 389 insertions(+), 25 deletions(-) diff --git a/probe/kubernetes/client.go b/probe/kubernetes/client.go index 770dcbfdd..0cba08dfa 100644 --- a/probe/kubernetes/client.go +++ b/probe/kubernetes/client.go @@ -1,9 +1,11 @@ package kubernetes import ( + "bytes" "errors" "fmt" "io" + "io/ioutil" "strings" "sync" "time" @@ -14,6 +16,7 @@ import ( snapshot "github.com/openebs/k8s-snapshot-client/snapshot/pkg/client/clientset/versioned" "github.com/pborman/uuid" log "github.com/sirupsen/logrus" + apiappsv1 "k8s.io/api/apps/v1" apiappsv1beta1 "k8s.io/api/apps/v1beta1" apibatchv1 "k8s.io/api/batch/v1" apibatchv1beta1 "k8s.io/api/batch/v1beta1" @@ -22,6 +25,7 @@ import ( apiextensionsv1beta1 "k8s.io/api/extensions/v1beta1" storagev1 "k8s.io/api/storage/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" + apimeta "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" @@ -32,6 +36,8 @@ import ( "k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + kubectldescribe "k8s.io/kubernetes/pkg/kubectl/describe" + kubectl "k8s.io/kubernetes/pkg/kubectl/describe/versioned" ) // Client keeps track of running kubernetes pods and services @@ -55,12 +61,28 @@ type Client interface { CloneVolumeSnapshot(namespaceID, volumeSnapshotID, persistentVolumeClaimID, capacity string) error CreateVolumeSnapshot(namespaceID, persistentVolumeClaimID, capacity string) error GetLogs(namespaceID, podID string, containerNames []string) (io.ReadCloser, error) + Describe(namespaceID, resourceID string, groupKind schema.GroupKind, restMapping apimeta.RESTMapping) (io.ReadCloser, error) DeletePod(namespaceID, podID string) error DeleteVolumeSnapshot(namespaceID, volumeSnapshotID string) error ScaleUp(namespaceID, id string) error ScaleDown(namespaceID, id string) error } +// ResourceMap is the mapping of resource and their GroupKind +var ResourceMap = map[string]schema.GroupKind{ + "Pod": {Group: apiv1.GroupName, Kind: "Pod"}, + "Service": {Group: apiv1.GroupName, Kind: "Service"}, + "Deployment": {Group: apiappsv1.GroupName, Kind: "Deployment"}, + "DaemonSet": {Group: apiappsv1.GroupName, Kind: "DaemonSet"}, + "StatefulSet": {Group: apiappsv1.GroupName, Kind: "StatefulSet"}, + "Job": {Group: apibatchv1.GroupName, Kind: "Job"}, + "CronJob": {Group: apibatchv1.GroupName, Kind: "CronJob"}, + "Node": {Group: apiv1.GroupName, Kind: "Node"}, + "PersistentVolume": {Group: apiv1.GroupName, Kind: "PersistentVolume"}, + "PersistentVolumeClaim": {Group: apiv1.GroupName, Kind: "PersistentVolumeClaim"}, + "StorageClass": {Group: storagev1.GroupName, Kind: "StorageClass"}, +} + type client struct { quit chan struct{} client *kubernetes.Clientset @@ -541,6 +563,32 @@ func (c *client) GetLogs(namespaceID, podID string, containerNames []string) (io return NewLogReadCloser(readClosersWithLabel), nil } +func (c *client) Describe(namespaceID, resourceID string, groupKind schema.GroupKind, restMapping apimeta.RESTMapping) (io.ReadCloser, error) { + readClosersWithLabel := map[io.ReadCloser]string{} + restConfig, err := rest.InClusterConfig() + if err != nil { + return nil, err + } + describer, ok := kubectl.DescriberFor(groupKind, restConfig) + if !ok { + describer, ok = kubectl.GenericDescriberFor(&restMapping, restConfig) + if !ok { + return nil, errors.New("Resource not found") + } + } + describerSetting := kubectldescribe.DescriberSettings{ + ShowEvents: true, + } + obj, err := describer.Describe(namespaceID, resourceID, describerSetting) + if err != nil { + return nil, err + } + formattedObj := ioutil.NopCloser(bytes.NewReader([]byte(obj))) + readClosersWithLabel[formattedObj] = "describe" + + return NewLogReadCloser(readClosersWithLabel), nil +} + func (c *client) DeletePod(namespaceID, podID string) error { return c.client.CoreV1().Pods(namespaceID).Delete(podID, &metav1.DeleteOptions{}) } diff --git a/probe/kubernetes/controls.go b/probe/kubernetes/controls.go index f38174890..cf39dfed8 100644 --- a/probe/kubernetes/controls.go +++ b/probe/kubernetes/controls.go @@ -7,6 +7,8 @@ import ( "github.com/weaveworks/scope/common/xfer" "github.com/weaveworks/scope/probe/controls" "github.com/weaveworks/scope/report" + apimeta "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime/schema" ) // Control IDs used by the kubernetes integration. @@ -14,12 +16,19 @@ const ( CloneVolumeSnapshot = report.KubernetesCloneVolumeSnapshot CreateVolumeSnapshot = report.KubernetesCreateVolumeSnapshot GetLogs = report.KubernetesGetLogs + Describe = report.KubernetesDescribe DeletePod = report.KubernetesDeletePod DeleteVolumeSnapshot = report.KubernetesDeleteVolumeSnapshot ScaleUp = report.KubernetesScaleUp ScaleDown = report.KubernetesScaleDown ) +// GroupName and version used by CRDs +const ( + SnapshotGroupName = "volumesnapshot.external-storage.k8s.io" + SnapshotVersion = "v1" +) + // GetLogs is the control to get the logs for a kubernetes pod func (r *Reporter) GetLogs(req xfer.Request, namespaceID, podID string, containerNames []string) xfer.Response { readCloser, err := r.client.GetLogs(namespaceID, podID, containerNames) @@ -46,6 +55,90 @@ func (r *Reporter) GetLogs(req xfer.Request, namespaceID, podID string, containe } } +func (r *Reporter) describePod(req xfer.Request, namespaceID, podID string, _ []string) xfer.Response { + return r.describe(req, namespaceID, podID, ResourceMap["Pod"], apimeta.RESTMapping{}) +} + +func (r *Reporter) describePVC(req xfer.Request, namespaceID, pvcID, _ string) xfer.Response { + return r.describe(req, namespaceID, pvcID, ResourceMap["PersistentVolumeClaim"], apimeta.RESTMapping{}) +} + +func (r *Reporter) describeDeployment(req xfer.Request, namespaceID, deploymentID string) xfer.Response { + return r.describe(req, namespaceID, deploymentID, ResourceMap["Deployment"], apimeta.RESTMapping{}) +} + +func (r *Reporter) describeService(req xfer.Request, namespaceID, serviceID string) xfer.Response { + return r.describe(req, namespaceID, serviceID, ResourceMap["Service"], apimeta.RESTMapping{}) +} + +func (r *Reporter) describeCronJob(req xfer.Request, namespaceID, cronJobID string) xfer.Response { + return r.describe(req, namespaceID, cronJobID, ResourceMap["CronJob"], apimeta.RESTMapping{}) +} + +func (r *Reporter) describePV(req xfer.Request, PVID string) xfer.Response { + return r.describe(req, "", PVID, ResourceMap["PersistentVolume"], apimeta.RESTMapping{}) +} + +func (r *Reporter) describeDaemonSet(req xfer.Request, namespaceID, daemonSetID string) xfer.Response { + return r.describe(req, namespaceID, daemonSetID, ResourceMap["DaemonSet"], apimeta.RESTMapping{}) +} + +func (r *Reporter) describeStatefulSet(req xfer.Request, namespaceID, statefulSetID string) xfer.Response { + return r.describe(req, namespaceID, statefulSetID, ResourceMap["StatefulSet"], apimeta.RESTMapping{}) +} + +func (r *Reporter) describeStoragelass(req xfer.Request, storageClassID string) xfer.Response { + return r.describe(req, "", storageClassID, ResourceMap["StorageClass"], apimeta.RESTMapping{}) +} + +func (r *Reporter) describeVolumeSnapshot(req xfer.Request, namespaceID, volumeSnapshotID, _, _ string) xfer.Response { + restMapping := apimeta.RESTMapping{ + Resource: schema.GroupVersionResource{ + Group: SnapshotGroupName, + Version: SnapshotVersion, + Resource: "volumesnapshots", + }, + } + return r.describe(req, namespaceID, volumeSnapshotID, schema.GroupKind{}, restMapping) +} + +func (r *Reporter) describeVolumeSnapshotData(req xfer.Request, volumeSnapshotID string) xfer.Response { + restMapping := apimeta.RESTMapping{ + Resource: schema.GroupVersionResource{ + Group: SnapshotGroupName, + Version: SnapshotVersion, + Resource: "volumesnapshotdatas", + }, + } + return r.describe(req, "", volumeSnapshotID, schema.GroupKind{}, restMapping) +} + +// GetLogs is the control to get the logs for a kubernetes pod +func (r *Reporter) describe(req xfer.Request, namespaceID, resourceID string, groupKind schema.GroupKind, restMapping apimeta.RESTMapping) xfer.Response { + readCloser, err := r.client.Describe(namespaceID, resourceID, groupKind, restMapping) + if err != nil { + return xfer.ResponseError(err) + } + + readWriter := struct { + io.Reader + io.Writer + }{ + readCloser, + ioutil.Discard, + } + id, pipe, err := controls.NewPipeFromEnds(nil, readWriter, r.pipes, req.AppID) + if err != nil { + return xfer.ResponseError(err) + } + pipe.OnClose(func() { + readCloser.Close() + }) + return xfer.Response{ + Pipe: id, + } +} + func (r *Reporter) cloneVolumeSnapshot(req xfer.Request, namespaceID, volumeSnapshotID, persistentVolumeClaimID, capacity string) xfer.Response { err := r.client.CloneVolumeSnapshot(namespaceID, volumeSnapshotID, persistentVolumeClaimID, capacity) if err != nil { @@ -80,6 +173,44 @@ func (r *Reporter) deleteVolumeSnapshot(req xfer.Request, namespaceID, volumeSna } } +// Describe will parse the nodeID and return response according to the node (resource) type. +func (r *Reporter) Describe() func(xfer.Request) xfer.Response { + return func(req xfer.Request) xfer.Response { + var f func(req xfer.Request) xfer.Response + _, tag, ok := report.ParseNodeID(req.NodeID) + if !ok { + return xfer.ResponseErrorf("Invalid ID: %s", req.NodeID) + } + switch tag { + case "": + f = r.CapturePod(r.describePod) + case "": + f = r.CaptureService(r.describeService) + case "": + f = r.CaptureCronJob(r.describeCronJob) + case "": + f = r.CaptureDeployment(r.describeDeployment) + case "": + f = r.CaptureDaemonSet(r.describeDaemonSet) + case "": + f = r.CapturePersistentVolume(r.describePV) + case "": + f = r.CapturePersistentVolumeClaim(r.describePVC) + case "": + f = r.CaptureStorageClass(r.describeStoragelass) + case "": + f = r.CaptureStatefulSet(r.describeStatefulSet) + case "": + f = r.CaptureVolumeSnapshot(r.describeVolumeSnapshot) + case "": + f = r.CaptureVolumeSnapshotData(r.describeVolumeSnapshotData) + default: + return xfer.ResponseErrorf("Node not found: %s", req.NodeID) + } + return f(req) + } +} + // CapturePod is exported for testing func (r *Reporter) CapturePod(f func(xfer.Request, string, string, []string) xfer.Response) func(xfer.Request) xfer.Response { return func(req xfer.Request) xfer.Response { @@ -167,6 +298,155 @@ func (r *Reporter) CaptureVolumeSnapshot(f func(xfer.Request, string, string, st } } +// CaptureService is exported for testing +func (r *Reporter) CaptureService(f func(xfer.Request, string, string) xfer.Response) func(xfer.Request) xfer.Response { + return func(req xfer.Request) xfer.Response { + uid, ok := report.ParseServiceNodeID(req.NodeID) + if !ok { + return xfer.ResponseErrorf("Invalid ID: %s", req.NodeID) + } + var service Service + r.client.WalkServices(func(s Service) error { + if s.UID() == uid { + service = s + } + return nil + }) + if service == nil { + return xfer.ResponseErrorf("Service not found: %s", uid) + } + return f(req, service.Namespace(), service.Name()) + } +} + +// CaptureDaemonSet is exported for testing +func (r *Reporter) CaptureDaemonSet(f func(xfer.Request, string, string) xfer.Response) func(xfer.Request) xfer.Response { + return func(req xfer.Request) xfer.Response { + uid, ok := report.ParseDaemonSetNodeID(req.NodeID) + if !ok { + return xfer.ResponseErrorf("Invalid ID: %s", req.NodeID) + } + var daemonSet DaemonSet + r.client.WalkDaemonSets(func(d DaemonSet) error { + if d.UID() == uid { + daemonSet = d + } + return nil + }) + if daemonSet == nil { + return xfer.ResponseErrorf("Daemon Set not found: %s", uid) + } + return f(req, daemonSet.Namespace(), daemonSet.Name()) + } +} + +// CaptureCronJob is exported for testing +func (r *Reporter) CaptureCronJob(f func(xfer.Request, string, string) xfer.Response) func(xfer.Request) xfer.Response { + return func(req xfer.Request) xfer.Response { + uid, ok := report.ParseCronJobNodeID(req.NodeID) + if !ok { + return xfer.ResponseErrorf("Invalid ID: %s", req.NodeID) + } + var cronJob CronJob + r.client.WalkCronJobs(func(c CronJob) error { + if c.UID() == uid { + cronJob = c + } + return nil + }) + if cronJob == nil { + return xfer.ResponseErrorf("Cron Job not found: %s", uid) + } + return f(req, cronJob.Namespace(), cronJob.Name()) + } +} + +// CaptureStatefulSet is exported for testing +func (r *Reporter) CaptureStatefulSet(f func(xfer.Request, string, string) xfer.Response) func(xfer.Request) xfer.Response { + return func(req xfer.Request) xfer.Response { + uid, ok := report.ParseStatefulSetNodeID(req.NodeID) + if !ok { + return xfer.ResponseErrorf("Invalid ID: %s", req.NodeID) + } + var statefulSet StatefulSet + r.client.WalkStatefulSets(func(s StatefulSet) error { + if s.UID() == uid { + statefulSet = s + } + return nil + }) + if statefulSet == nil { + return xfer.ResponseErrorf("Stateful Set not found: %s", uid) + } + return f(req, statefulSet.Namespace(), statefulSet.Name()) + } +} + +// CaptureStorageClass is exported for testing +func (r *Reporter) CaptureStorageClass(f func(xfer.Request, string) xfer.Response) func(xfer.Request) xfer.Response { + return func(req xfer.Request) xfer.Response { + uid, ok := report.ParseStorageClassNodeID(req.NodeID) + if !ok { + return xfer.ResponseErrorf("Invalid ID: %s", req.NodeID) + } + var storageClass StorageClass + r.client.WalkStorageClasses(func(s StorageClass) error { + if s.UID() == uid { + storageClass = s + } + return nil + }) + if storageClass == nil { + return xfer.ResponseErrorf("StorageClass not found: %s", uid) + } + return f(req, storageClass.Name()) + } +} + +// CapturePersistentVolume will return name of PV +func (r *Reporter) CapturePersistentVolume(f func(xfer.Request, string) xfer.Response) func(xfer.Request) xfer.Response { + return func(req xfer.Request) xfer.Response { + uid, ok := report.ParsePersistentVolumeNodeID(req.NodeID) + if !ok { + return xfer.ResponseErrorf("Invalid ID: %s", req.NodeID) + } + // find persistentVolume by UID + var persistentVolume PersistentVolume + r.client.WalkPersistentVolumes(func(p PersistentVolume) error { + if p.UID() == uid { + persistentVolume = p + } + return nil + }) + if persistentVolume == nil { + return xfer.ResponseErrorf("Persistent volume not found: %s", uid) + } + return f(req, persistentVolume.Name()) + } +} + +// CaptureVolumeSnapshotData will return name of volume snapshot data +func (r *Reporter) CaptureVolumeSnapshotData(f func(xfer.Request, string) xfer.Response) func(xfer.Request) xfer.Response { + return func(req xfer.Request) xfer.Response { + uid, ok := report.ParseVolumeSnapshotDataNodeID(req.NodeID) + if !ok { + return xfer.ResponseErrorf("Invalid ID: %s", req.NodeID) + } + // find volume snapshotData by UID + var volumeSnapshotData VolumeSnapshotData + r.client.WalkVolumeSnapshotData(func(v VolumeSnapshotData) error { + if v.UID() == uid { + volumeSnapshotData = v + } + return nil + }) + if volumeSnapshotData == nil { + return xfer.ResponseErrorf("Volume snapshot data not found: %s", uid) + } + return f(req, volumeSnapshotData.Name()) + } +} + // ScaleUp is the control to scale up a deployment func (r *Reporter) ScaleUp(req xfer.Request, namespace, id string) xfer.Response { return xfer.ResponseError(r.client.ScaleUp(namespace, id)) @@ -182,6 +462,7 @@ func (r *Reporter) registerControls() { CloneVolumeSnapshot: r.CaptureVolumeSnapshot(r.cloneVolumeSnapshot), CreateVolumeSnapshot: r.CapturePersistentVolumeClaim(r.createVolumeSnapshot), GetLogs: r.CapturePod(r.GetLogs), + Describe: r.Describe(), DeletePod: r.CapturePod(r.deletePod), DeleteVolumeSnapshot: r.CaptureVolumeSnapshot(r.deleteVolumeSnapshot), ScaleUp: r.CaptureDeployment(r.ScaleUp), @@ -195,6 +476,7 @@ func (r *Reporter) deregisterControls() { CloneVolumeSnapshot, CreateVolumeSnapshot, GetLogs, + Describe, DeletePod, DeleteVolumeSnapshot, ScaleUp, diff --git a/probe/kubernetes/cronjob.go b/probe/kubernetes/cronjob.go index abdeb421c..e6543efb9 100644 --- a/probe/kubernetes/cronjob.go +++ b/probe/kubernetes/cronjob.go @@ -85,7 +85,9 @@ func (cj *cronJob) GetNode(probeID string) report.Node { if cj.Status.LastScheduleTime != nil { latest[LastScheduled] = cj.Status.LastScheduleTime.Format(time.RFC3339Nano) } - return cj.MetaNode(report.MakeCronJobNodeID(cj.UID())).WithLatests(latest) + return cj.MetaNode(report.MakeCronJobNodeID(cj.UID())). + WithLatests(latest). + WithLatestActiveControls(Describe) } func upgradeCronJob(legacy *batchv2alpha1.CronJob) *batchv1beta1.CronJob { diff --git a/probe/kubernetes/daemonset.go b/probe/kubernetes/daemonset.go index 285f155c3..d7dd638b3 100644 --- a/probe/kubernetes/daemonset.go +++ b/probe/kubernetes/daemonset.go @@ -50,5 +50,5 @@ func (d *daemonSet) GetNode(probeID string) report.Node { MisscheduledReplicas: fmt.Sprint(d.Status.NumberMisscheduled), NodeType: "DaemonSet", report.ControlProbeID: probeID, - }) + }).WithLatestActiveControls(Describe) } diff --git a/probe/kubernetes/deployment.go b/probe/kubernetes/deployment.go index fad882dfb..78777e27c 100644 --- a/probe/kubernetes/deployment.go +++ b/probe/kubernetes/deployment.go @@ -61,5 +61,5 @@ func (d *deployment) GetNode(probeID string) report.Node { Strategy: string(d.Spec.Strategy.Type), report.ControlProbeID: probeID, NodeType: "Deployment", - }).WithLatestActiveControls(ScaleUp, ScaleDown) + }).WithLatestActiveControls(ScaleUp, ScaleDown, Describe) } diff --git a/probe/kubernetes/persistentvolume.go b/probe/kubernetes/persistentvolume.go index 2a920c0cb..1afd5c166 100644 --- a/probe/kubernetes/persistentvolume.go +++ b/probe/kubernetes/persistentvolume.go @@ -10,7 +10,7 @@ import ( // PersistentVolume represent kubernetes PersistentVolume interface type PersistentVolume interface { Meta - GetNode() report.Node + GetNode(probeID string) report.Node GetAccessMode() string GetVolume() string GetStorageDriver() string @@ -64,18 +64,21 @@ func (p *persistentVolume) GetStorageDriver() string { } // GetNode returns Persistent Volume as Node -func (p *persistentVolume) GetNode() report.Node { +func (p *persistentVolume) GetNode(probeID string) report.Node { latests := map[string]string{ - NodeType: "Persistent Volume", - VolumeClaim: p.GetVolume(), - StorageClassName: p.Spec.StorageClassName, - Status: string(p.Status.Phase), - AccessModes: p.GetAccessMode(), + NodeType: "Persistent Volume", + VolumeClaim: p.GetVolume(), + StorageClassName: p.Spec.StorageClassName, + Status: string(p.Status.Phase), + AccessModes: p.GetAccessMode(), + report.ControlProbeID: probeID, } if p.GetStorageDriver() != "" { latests[StorageDriver] = p.GetStorageDriver() } - return p.MetaNode(report.MakePersistentVolumeNodeID(p.UID())).WithLatests(latests) + return p.MetaNode(report.MakePersistentVolumeNodeID(p.UID())). + WithLatests(latests). + WithLatestActiveControls(Describe) } diff --git a/probe/kubernetes/persistentvolumeclaim.go b/probe/kubernetes/persistentvolumeclaim.go index 6237acc57..000820e53 100644 --- a/probe/kubernetes/persistentvolumeclaim.go +++ b/probe/kubernetes/persistentvolumeclaim.go @@ -73,7 +73,7 @@ func (p *persistentVolumeClaim) GetNode(probeID string) report.Node { return p.MetaNode(report.MakePersistentVolumeClaimNodeID(p.UID())). WithLatests(latests). - WithLatestActiveControls(CreateVolumeSnapshot) + WithLatestActiveControls(CreateVolumeSnapshot, Describe) } // Selector returns all Persistent Volume Claim selector diff --git a/probe/kubernetes/pod.go b/probe/kubernetes/pod.go index a703f5f6e..cc91548e2 100644 --- a/probe/kubernetes/pod.go +++ b/probe/kubernetes/pod.go @@ -107,7 +107,7 @@ func (p *pod) GetNode(probeID string) report.Node { return p.MetaNode(report.MakePodNodeID(p.UID())).WithLatests(latests). WithParents(p.parents). - WithLatestActiveControls(GetLogs, DeletePod) + WithLatestActiveControls(GetLogs, DeletePod, Describe) } func (p *pod) ContainerNames() []string { diff --git a/probe/kubernetes/reporter.go b/probe/kubernetes/reporter.go index e56b3ad0c..3535dc6ec 100644 --- a/probe/kubernetes/reporter.go +++ b/probe/kubernetes/reporter.go @@ -168,6 +168,13 @@ var ( Rank: 1, }, } + + DescribeControl = report.Control{ + ID: Describe, + Human: "Describe", + Icon: "fa fa-file-text", + Rank: 2, + } ) // Reporter generate Reports containing Container and ContainerImage topologies @@ -357,6 +364,7 @@ func (r *Reporter) serviceTopology() (report.Topology, []Service, error) { WithTableTemplates(TableTemplates) services = []Service{} ) + result.Controls.AddControl(DescribeControl) err := r.client.WalkServices(func(s Service) error { result.AddNode(s.GetNode(r.probeID)) services = append(services, s) @@ -374,6 +382,7 @@ func (r *Reporter) deploymentTopology() (report.Topology, []Deployment, error) { deployments = []Deployment{} ) result.Controls.AddControls(ScalingControls) + result.Controls.AddControl(DescribeControl) err := r.client.WalkDeployments(func(d Deployment) error { result.AddNode(d.GetNode(r.probeID)) @@ -389,6 +398,7 @@ func (r *Reporter) daemonSetTopology() (report.Topology, []DaemonSet, error) { WithMetadataTemplates(DaemonSetMetadataTemplates). WithMetricTemplates(DaemonSetMetricTemplates). WithTableTemplates(TableTemplates) + result.Controls.AddControl(DescribeControl) err := r.client.WalkDaemonSets(func(d DaemonSet) error { result.AddNode(d.GetNode(r.probeID)) daemonSets = append(daemonSets, d) @@ -403,6 +413,7 @@ func (r *Reporter) statefulSetTopology() (report.Topology, []StatefulSet, error) WithMetadataTemplates(StatefulSetMetadataTemplates). WithMetricTemplates(StatefulSetMetricTemplates). WithTableTemplates(TableTemplates) + result.Controls.AddControl(DescribeControl) err := r.client.WalkStatefulSets(func(s StatefulSet) error { result.AddNode(s.GetNode(r.probeID)) statefulSets = append(statefulSets, s) @@ -417,6 +428,7 @@ func (r *Reporter) cronJobTopology() (report.Topology, []CronJob, error) { WithMetadataTemplates(CronJobMetadataTemplates). WithMetricTemplates(CronJobMetricTemplates). WithTableTemplates(TableTemplates) + result.Controls.AddControl(DescribeControl) err := r.client.WalkCronJobs(func(c CronJob) error { result.AddNode(c.GetNode(r.probeID)) cronJobs = append(cronJobs, c) @@ -430,8 +442,9 @@ func (r *Reporter) persistentVolumeTopology() (report.Topology, []PersistentVolu result := report.MakeTopology(). WithMetadataTemplates(PersistentVolumeMetadataTemplates). WithTableTemplates(TableTemplates) + result.Controls.AddControl(DescribeControl) err := r.client.WalkPersistentVolumes(func(p PersistentVolume) error { - result.AddNode(p.GetNode()) + result.AddNode(p.GetNode(r.probeID)) persistentVolumes = append(persistentVolumes, p) return nil }) @@ -449,6 +462,7 @@ func (r *Reporter) persistentVolumeClaimTopology() (report.Topology, []Persisten Icon: "fa fa-camera", Rank: 0, }) + result.Controls.AddControl(DescribeControl) err := r.client.WalkPersistentVolumeClaims(func(p PersistentVolumeClaim) error { result.AddNode(p.GetNode(r.probeID)) persistentVolumeClaims = append(persistentVolumeClaims, p) @@ -462,8 +476,9 @@ func (r *Reporter) storageClassTopology() (report.Topology, []StorageClass, erro result := report.MakeTopology(). WithMetadataTemplates(StorageClassMetadataTemplates). WithTableTemplates(TableTemplates) + result.Controls.AddControl(DescribeControl) err := r.client.WalkStorageClasses(func(p StorageClass) error { - result.AddNode(p.GetNode()) + result.AddNode(p.GetNode(r.probeID)) storageClasses = append(storageClasses, p) return nil }) @@ -487,6 +502,7 @@ func (r *Reporter) volumeSnapshotTopology() (report.Topology, []VolumeSnapshot, Icon: "far fa-trash-alt", Rank: 1, }) + result.Controls.AddControl(DescribeControl) err := r.client.WalkVolumeSnapshots(func(p VolumeSnapshot) error { result.AddNode(p.GetNode(r.probeID)) volumeSnapshots = append(volumeSnapshots, p) @@ -500,6 +516,7 @@ func (r *Reporter) volumeSnapshotDataTopology() (report.Topology, []VolumeSnapsh result := report.MakeTopology(). WithMetadataTemplates(VolumeSnapshotDataMetadataTemplates). WithTableTemplates(TableTemplates) + result.Controls.AddControl(DescribeControl) err := r.client.WalkVolumeSnapshotData(func(p VolumeSnapshotData) error { result.AddNode(p.GetNode(r.probeID)) volumeSnapshotData = append(volumeSnapshotData, p) @@ -544,6 +561,7 @@ func (r *Reporter) podTopology(services []Service, deployments []Deployment, dae Confirmation: "Are you sure you want to delete this pod?", Rank: 1, }) + pods.Controls.AddControl(DescribeControl) for _, service := range services { selectors = append(selectors, match( service.Namespace(), diff --git a/probe/kubernetes/reporter_test.go b/probe/kubernetes/reporter_test.go index 22befa249..0487244ce 100644 --- a/probe/kubernetes/reporter_test.go +++ b/probe/kubernetes/reporter_test.go @@ -9,7 +9,9 @@ import ( apiv1 "k8s.io/api/core/v1" apiv1beta1 "k8s.io/api/extensions/v1beta1" + k8smeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "github.com/weaveworks/scope/common/xfer" @@ -196,6 +198,9 @@ func (c *mockClient) CreateVolumeSnapshot(namespaceID, persistentVolumeClaimID, func (c *mockClient) DeleteVolumeSnapshot(namespaceID, VolumeSnapshotID string) error { return nil } +func (c *mockClient) Describe(namespaceID, resourceID string, groupKind schema.GroupKind, restMapping k8smeta.RESTMapping) (io.ReadCloser, error) { + return nil, nil +} type mockPipeClient map[string]xfer.Pipe diff --git a/probe/kubernetes/service.go b/probe/kubernetes/service.go index 8251db4e0..2763b7d20 100644 --- a/probe/kubernetes/service.go +++ b/probe/kubernetes/service.go @@ -63,7 +63,9 @@ func (s *service) GetNode(probeID string) report.Node { } latest[Ports] = portStr[:len(portStr)-1] } - return s.MetaNode(report.MakeServiceNodeID(s.UID())).WithLatests(latest) + return s.MetaNode(report.MakeServiceNodeID(s.UID())). + WithLatests(latest). + WithLatestActiveControls(Describe) } func (s *service) ClusterIP() string { diff --git a/probe/kubernetes/statefulset.go b/probe/kubernetes/statefulset.go index 7cd5ae01e..db5539372 100644 --- a/probe/kubernetes/statefulset.go +++ b/probe/kubernetes/statefulset.go @@ -52,5 +52,7 @@ func (s *statefulSet) GetNode(probeID string) report.Node { if s.Status.ObservedGeneration != nil { latests[ObservedGeneration] = fmt.Sprint(*s.Status.ObservedGeneration) } - return s.MetaNode(report.MakeStatefulSetNodeID(s.UID())).WithLatests(latests) + return s.MetaNode(report.MakeStatefulSetNodeID(s.UID())). + WithLatests(latests). + WithLatestActiveControls(Describe) } diff --git a/probe/kubernetes/storageclass.go b/probe/kubernetes/storageclass.go index 832248528..e70f418ef 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() report.Node + GetNode(probeID string) report.Node } // storageClass represents kubernetes storage classes @@ -23,10 +23,11 @@ func NewStorageClass(p *storagev1.StorageClass) StorageClass { } // GetNode returns StorageClass as Node -func (p *storageClass) GetNode() report.Node { +func (p *storageClass) GetNode(probeID string) report.Node { return p.MetaNode(report.MakeStorageClassNodeID(p.UID())).WithLatests(map[string]string{ - NodeType: "Storage Class", - Name: p.GetName(), - Provisioner: p.Provisioner, - }) + NodeType: "Storage Class", + Name: p.GetName(), + Provisioner: p.Provisioner, + report.ControlProbeID: probeID, + }).WithLatestActiveControls(Describe) } diff --git a/probe/kubernetes/volumesnapshot.go b/probe/kubernetes/volumesnapshot.go index 96f1d1b40..5a4f97df7 100644 --- a/probe/kubernetes/volumesnapshot.go +++ b/probe/kubernetes/volumesnapshot.go @@ -56,5 +56,5 @@ func (p *volumeSnapshot) GetNode(probeID string) report.Node { VolumeClaim: p.GetVolumeName(), SnapshotData: p.Spec.SnapshotDataName, VolumeName: p.GetLabels()[SnapshotPVName], - }).WithLatestActiveControls(CloneVolumeSnapshot, DeleteVolumeSnapshot) + }).WithLatestActiveControls(CloneVolumeSnapshot, DeleteVolumeSnapshot, Describe) } diff --git a/probe/kubernetes/volumesnapshotdata.go b/probe/kubernetes/volumesnapshotdata.go index 0516bf6ec..858da25be 100644 --- a/probe/kubernetes/volumesnapshotdata.go +++ b/probe/kubernetes/volumesnapshotdata.go @@ -29,5 +29,5 @@ func (p *volumeSnapshotData) GetNode(probeID string) report.Node { NodeType: "Volume Snapshot Data", VolumeName: p.Spec.PersistentVolumeRef.Name, VolumeSnapshotName: p.Spec.VolumeSnapshotRef.Name, - }) + }).WithLatestActiveControls(Describe) } diff --git a/report/map_keys.go b/report/map_keys.go index a6094c6ee..9e4eb3d64 100644 --- a/report/map_keys.go +++ b/report/map_keys.go @@ -88,6 +88,7 @@ const ( KubernetesVolumeCapacity = "kubernetes_volume_capacity" KubernetesCloneVolumeSnapshot = "kubernetes_clone_volume_snapshot" KubernetesDeleteVolumeSnapshot = "kubernetes_delete_volume_snapshot" + KubernetesDescribe = "kubernetes_describe" // probe/awsecs ECSCluster = "ecs_cluster" ECSCreatedAt = "ecs_created_at"