From 9d5be803bc0d408944f6b30c98a05c4026abc6e2 Mon Sep 17 00:00:00 2001 From: Soujanya Mangipudi Date: Thu, 28 Apr 2022 13:59:38 -0700 Subject: [PATCH] feat(helm): Supporting helm3 to show up resource names that were deployed as part of release in helm status command Creating a new PR based on this existing stale PR https://github.com/helm/helm/pull/7728 Signed-off-by: Soujanya Mangipudi # Conflicts: # go.sum --- cmd/helm/status.go | 4 + cmd/helm/status_test.go | 20 +++ .../output/status-with-resources.json | 1 + .../testdata/output/status-with-resources.txt | 8 + go.mod | 1 + go.sum | 1 + pkg/action/status.go | 16 +- pkg/kube/client.go | 154 +++++++++++++++++- pkg/kube/fake/printer.go | 8 + pkg/kube/interface.go | 2 + pkg/release/info.go | 2 + 11 files changed, 211 insertions(+), 6 deletions(-) create mode 100644 cmd/helm/testdata/output/status-with-resources.json create mode 100644 cmd/helm/testdata/output/status-with-resources.txt diff --git a/cmd/helm/status.go b/cmd/helm/status.go index 6085251d5..9ea7ea663 100644 --- a/cmd/helm/status.go +++ b/cmd/helm/status.go @@ -124,6 +124,10 @@ func (s statusPrinter) WriteTable(out io.Writer) error { fmt.Fprintf(out, "DESCRIPTION: %s\n", s.release.Info.Description) } + if len(s.release.Info.Resources) > 0 { + fmt.Fprintf(out, "RESOURCES:\n%s\n", s.release.Info.Resources) + } + executions := executionsByHookEvent(s.release) if tests, ok := executions[release.HookTest]; !ok || len(tests) == 0 { fmt.Fprintln(out, "TEST SUITE: None") diff --git a/cmd/helm/status_test.go b/cmd/helm/status_test.go index 7f305d56b..3cf694da9 100644 --- a/cmd/helm/status_test.go +++ b/cmd/helm/status_test.go @@ -68,6 +68,26 @@ func TestStatusCmd(t *testing.T) { Status: release.StatusDeployed, Notes: "release notes", }), + }, { + name: "get status of a deployed release with resources", + cmd: "status flummoxed-chickadee", + golden: "output/status-with-resources.txt", + rels: releasesMockWithStatus( + &release.Info{ + Resources: "hello resource", + Status: release.StatusDeployed, + }, + ), + }, { + name: "get status of a deployed release with resources in json", + cmd: "status flummoxed-chickadee -o json", + golden: "output/status-with-resources.json", + rels: releasesMockWithStatus( + &release.Info{ + Resources: "hello resource", + Status: release.StatusDeployed, + }, + ), }, { name: "get status of a deployed release with test suite", cmd: "status flummoxed-chickadee", diff --git a/cmd/helm/testdata/output/status-with-resources.json b/cmd/helm/testdata/output/status-with-resources.json new file mode 100644 index 000000000..3a4a2d99e --- /dev/null +++ b/cmd/helm/testdata/output/status-with-resources.json @@ -0,0 +1 @@ +{"name":"flummoxed-chickadee","info":{"first_deployed":"","last_deployed":"2016-01-16T00:00:00Z","deleted":"","status":"deployed","resources":"hello resource"},"namespace":"default"} diff --git a/cmd/helm/testdata/output/status-with-resources.txt b/cmd/helm/testdata/output/status-with-resources.txt new file mode 100644 index 000000000..a432afa48 --- /dev/null +++ b/cmd/helm/testdata/output/status-with-resources.txt @@ -0,0 +1,8 @@ +NAME: flummoxed-chickadee +LAST DEPLOYED: Sat Jan 16 00:00:00 2016 +NAMESPACE: default +STATUS: deployed +REVISION: 0 +RESOURCES: +hello resource +TEST SUITE: None diff --git a/go.mod b/go.mod index 9ced3597c..c0b5caf5c 100644 --- a/go.mod +++ b/go.mod @@ -82,6 +82,7 @@ require ( github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect github.com/fatih/color v1.13.0 // indirect github.com/felixge/httpsnoop v1.0.1 // indirect + github.com/fvbommel/sortorder v1.0.1 // indirect github.com/go-errors/errors v1.0.1 // indirect github.com/go-gorp/gorp/v3 v3.0.2 // indirect github.com/go-logr/logr v1.2.3 // indirect diff --git a/go.sum b/go.sum index 93d35468f..9a7deec2f 100644 --- a/go.sum +++ b/go.sum @@ -201,6 +201,7 @@ github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYF github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fvbommel/sortorder v1.0.1 h1:dSnXLt4mJYH25uDDGa3biZNQsozaUWDSWeKJ0qqFfzE= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= diff --git a/pkg/action/status.go b/pkg/action/status.go index 1c556e28d..86d2f0f57 100644 --- a/pkg/action/status.go +++ b/pkg/action/status.go @@ -17,6 +17,8 @@ limitations under the License. package action import ( + "bytes" + "helm.sh/helm/v3/pkg/release" ) @@ -47,5 +49,17 @@ func (s *Status) Run(name string) (*release.Release, error) { return nil, err } - return s.cfg.releaseContent(name, s.Version) + rel, err := s.cfg.releaseContent(name, s.Version) + if err != nil { + return nil, err + } + resources, _ := s.cfg.KubeClient.Build(bytes.NewBufferString(rel.Manifest), false) + resp, err := s.cfg.KubeClient.Get(resources, bytes.NewBufferString(rel.Manifest)) + if err != nil { + return nil, err + } + if resp != "" { + rel.Info.Resources = resp + } + return rel, nil } diff --git a/pkg/kube/client.go b/pkg/kube/client.go index 38c8b93f2..401756ebf 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -17,12 +17,14 @@ limitations under the License. package kube // import "helm.sh/helm/v3/pkg/kube" import ( + "bytes" "context" "encoding/json" "fmt" "io" "os" "path/filepath" + "reflect" "strings" "sync" "time" @@ -38,7 +40,9 @@ import ( "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/strategicpatch" @@ -47,8 +51,10 @@ import ( "k8s.io/cli-runtime/pkg/resource" "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" cachetools "k8s.io/client-go/tools/cache" watchtools "k8s.io/client-go/tools/watch" + "k8s.io/kubectl/pkg/cmd/get" cmdutil "k8s.io/kubectl/pkg/cmd/util" ) @@ -132,6 +138,134 @@ func (c *Client) Create(resources ResourceList) (*Result, error) { return &Result{Created: resources}, nil } +func transformRequests(req *rest.Request) { + tableParam := strings.Join([]string{ + fmt.Sprintf("application/json;as=Table;v=%s;g=%s", metav1.SchemeGroupVersion.Version, metav1.GroupName), + fmt.Sprintf("application/json;as=Table;v=%s;g=%s", metav1beta1.SchemeGroupVersion.Version, metav1beta1.GroupName), + "application/json", + }, ",") + req.SetHeader("Accept", tableParam) + + // if sorting, ensure we receive the full object in order to introspect its fields via jsonpath + req.Param("includeObject", "Object") +} + +func (c *Client) Get(resources ResourceList, reader io.Reader) (string, error) { + buf := new(bytes.Buffer) + printFlags := get.NewHumanPrintFlags() + typePrinter, _ := printFlags.ToPrinter("") + printer := &get.TablePrinter{Delegate: typePrinter} + objs := make(map[string][]runtime.Object) + + podSelectors := []map[string]string{} + err := resources.Visit(func(info *resource.Info, err error) error { + if err != nil { + return err + } + + gvk := info.ResourceMapping().GroupVersionKind + vk := gvk.Version + "/" + gvk.Kind + obj, err := getResource(info) + if err != nil { + fmt.Fprintf(buf, "Get resource %s failed, err:%v\n", info.Name, err) + } else { + objs[vk] = append(objs[vk], obj) + + objs, err = c.getSelectRelationPod(info, objs, &podSelectors) + if err != nil { + c.Log("Warning: get the relation pod is failed, err:%s", err.Error()) + } + } + + return nil + }) + if err != nil { + return "", err + } + + var keys []string + for key := range objs { + keys = append(keys, key) + } + + for _, t := range keys { + if _, err = fmt.Fprintf(buf, "==> %s\n", t); err != nil { + return "", err + } + vk := objs[t] + for _, resource := range vk { + if err := printer.PrintObj(resource, buf); err != nil { + c.Log("failed to print object type %s: %v", t, err) + return "", err + } + } + if _, err := buf.WriteString("\n"); err != nil { + return "", err + } + } + return buf.String(), nil +} + +func (c *Client) getSelectRelationPod(info *resource.Info, objs map[string][]runtime.Object, podSelectors *[]map[string]string) (map[string][]runtime.Object, error) { + if info == nil { + return objs, nil + } + c.Log("get relation pod of object: %s/%s/%s", info.Namespace, info.Mapping.GroupVersionKind.Kind, info.Name) + selector, ok, _ := getSelectorFromObject(info.Object) + if !ok { + return objs, nil + } + + for index := range *podSelectors { + if reflect.DeepEqual((*podSelectors)[index], selector) { + // check if pods for selectors are already added. This avoids duplicate printing of pods + return objs, nil + } + } + + *podSelectors = append(*podSelectors, selector) + + infos, err := c.Factory.NewBuilder(). + Unstructured(). + ContinueOnError(). + NamespaceParam(info.Namespace). + DefaultNamespace(). + ResourceTypes("pods"). + LabelSelector(labels.Set(selector).AsSelector().String()). + TransformRequests(transformRequests). + Do().Infos() + if err != nil { + return objs, err + } + vk := "v1/Pod(related)" + + for _, info := range infos { + objs[vk] = append(objs[vk], info.Object) + } + return objs, nil +} + +func getSelectorFromObject(obj runtime.Object) (map[string]string, bool, error) { + typed := obj.(*unstructured.Unstructured) + kind := typed.Object["kind"] + switch kind { + case "ReplicaSet", "Deployment", "StatefulSet", "DaemonSet", "Job": + return unstructured.NestedStringMap(typed.Object, "spec", "selector", "matchLabels") + case "ReplicationController": + return unstructured.NestedStringMap(typed.Object, "spec", "selector") + default: + return nil, false, nil + } +} + +func getResource(info *resource.Info) (runtime.Object, error) { + obj, err := resource.NewHelper(info.Client, info.Mapping).Get(info.Namespace, info.Name) + if err != nil { + return nil, err + } + return obj, nil +} + // Wait waits up to the given timeout for the specified resources to be ready. func (c *Client) Wait(resources ResourceList, timeout time.Duration) error { cs, err := c.getKubeClient() @@ -207,11 +341,21 @@ func (c *Client) Build(reader io.Reader, validate bool) (ResourceList, error) { if err != nil { return nil, err } - result, err := c.newBuilder(). - Unstructured(). - Schema(schema). - Stream(reader, ""). - Do().Infos() + var result ResourceList + if validate { + result, err = c.newBuilder(). + Unstructured(). + Schema(schema). + Stream(reader, ""). + Do().Infos() + } else { + result, err = c.newBuilder(). + Unstructured(). + Schema(schema). + Stream(reader, ""). + TransformRequests(transformRequests). + Do().Infos() + } return result, scrubValidationError(err) } diff --git a/pkg/kube/fake/printer.go b/pkg/kube/fake/printer.go index 1e8cf0066..d49ff15bb 100644 --- a/pkg/kube/fake/printer.go +++ b/pkg/kube/fake/printer.go @@ -47,6 +47,14 @@ func (p *PrintingKubeClient) Create(resources kube.ResourceList) (*kube.Result, return &kube.Result{Created: resources}, nil } +func (p *PrintingKubeClient) Get(resources kube.ResourceList, reader io.Reader) (string, error) { + _, err := io.Copy(p.Out, bufferize(resources)) + if err != nil { + return "", err + } + return "", nil +} + func (p *PrintingKubeClient) Wait(resources kube.ResourceList, _ time.Duration) error { _, err := io.Copy(p.Out, bufferize(resources)) return err diff --git a/pkg/kube/interface.go b/pkg/kube/interface.go index 299e34e95..3c1afbd6c 100644 --- a/pkg/kube/interface.go +++ b/pkg/kube/interface.go @@ -68,6 +68,8 @@ type Interface interface { // IsReachable checks whether the client is able to connect to the cluster. IsReachable() error + + Get(resources ResourceList, reader io.Reader) (string, error) } // InterfaceExt is introduced to avoid breaking backwards compatibility for Interface implementers. diff --git a/pkg/release/info.go b/pkg/release/info.go index 0cb2bab64..c8bd64834 100644 --- a/pkg/release/info.go +++ b/pkg/release/info.go @@ -33,4 +33,6 @@ type Info struct { Status Status `json:"status,omitempty"` // Contains the rendered templates/NOTES.txt if available Notes string `json:"notes,omitempty"` + // Contains the deployed resources information + Resources string `json:"resources,omitempty"` }