From 0b5e9d37c849df546580f78a387d7d4a3a2a1c8d Mon Sep 17 00:00:00 2001 From: Mikhail Kopylov Date: Fri, 4 Aug 2023 07:49:00 +0300 Subject: [PATCH] Restore `helm get metadata` command Signed-off-by: Mikhail Kopylov --- cmd/helm/get.go | 2 + cmd/helm/get_all.go | 2 +- cmd/helm/get_metadata.go | 94 +++++++++++++++++++ cmd/helm/get_metadata_test.go | 66 +++++++++++++ cmd/helm/install.go | 2 +- cmd/helm/release_testing.go | 2 +- cmd/helm/status.go | 46 +++++---- cmd/helm/status_test.go | 2 +- .../testdata/output/get-metadata-args.txt | 3 + cmd/helm/testdata/output/get-metadata.json | 1 + cmd/helm/testdata/output/get-metadata.txt | 8 ++ cmd/helm/testdata/output/get-metadata.yaml | 8 ++ cmd/helm/testdata/output/get-release.txt | 3 + cmd/helm/upgrade.go | 4 +- pkg/action/get_metadata.go | 69 ++++++++++++++ 15 files changed, 286 insertions(+), 26 deletions(-) create mode 100644 cmd/helm/get_metadata.go create mode 100644 cmd/helm/get_metadata_test.go create mode 100644 cmd/helm/testdata/output/get-metadata-args.txt create mode 100644 cmd/helm/testdata/output/get-metadata.json create mode 100644 cmd/helm/testdata/output/get-metadata.txt create mode 100644 cmd/helm/testdata/output/get-metadata.yaml create mode 100644 pkg/action/get_metadata.go diff --git a/cmd/helm/get.go b/cmd/helm/get.go index 7c4854b59e8..727cdaf88e5 100644 --- a/cmd/helm/get.go +++ b/cmd/helm/get.go @@ -33,6 +33,7 @@ get extended information about the release, including: - The generated manifest file - The notes provided by the chart of the release - The hooks associated with the release +- The metadata of the release ` func newGetCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { @@ -48,6 +49,7 @@ func newGetCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { cmd.AddCommand(newGetManifestCmd(cfg, out)) cmd.AddCommand(newGetHooksCmd(cfg, out)) cmd.AddCommand(newGetNotesCmd(cfg, out)) + cmd.AddCommand(newGetMetadataCmd(cfg, out)) return cmd } diff --git a/cmd/helm/get_all.go b/cmd/helm/get_all.go index 2dbef97cf48..e51d50536e3 100644 --- a/cmd/helm/get_all.go +++ b/cmd/helm/get_all.go @@ -59,7 +59,7 @@ func newGetAllCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { return tpl(template, data, out) } - return output.Table.Write(out, &statusPrinter{res, true, false, false}) + return output.Table.Write(out, &statusPrinter{res, true, false, false, true}) }, } diff --git a/cmd/helm/get_metadata.go b/cmd/helm/get_metadata.go new file mode 100644 index 00000000000..adab891bd64 --- /dev/null +++ b/cmd/helm/get_metadata.go @@ -0,0 +1,94 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "fmt" + "io" + "log" + + "github.com/spf13/cobra" + + "helm.sh/helm/v3/cmd/helm/require" + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/cli/output" +) + +type metadataWriter struct { + metadata *action.Metadata +} + +func newGetMetadataCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + var outfmt output.Format + client := action.NewGetMetadata(cfg) + + cmd := &cobra.Command{ + Use: "metadata RELEASE_NAME", + Short: "This command fetches metadata for a given release", + Args: require.ExactArgs(1), + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) != 0 { + return nil, cobra.ShellCompDirectiveNoFileComp + } + return compListReleases(toComplete, args, cfg) + }, + RunE: func(cmd *cobra.Command, args []string) error { + releaseMetadata, err := client.Run(args[0]) + if err != nil { + return err + } + return outfmt.Write(out, &metadataWriter{releaseMetadata}) + }, + } + + f := cmd.Flags() + f.IntVar(&client.Version, "revision", 0, "specify release revision") + err := cmd.RegisterFlagCompletionFunc("revision", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) == 1 { + return compListRevisions(toComplete, cfg, args[0]) + } + return nil, cobra.ShellCompDirectiveNoFileComp + }) + + if err != nil { + log.Fatal(err) + } + + bindOutputFlag(cmd, &outfmt) + + return cmd +} + +func (w metadataWriter) WriteTable(out io.Writer) error { + _, _ = fmt.Fprintf(out, "NAME: %v\n", w.metadata.Name) + _, _ = fmt.Fprintf(out, "CHART: %v\n", w.metadata.Chart) + _, _ = fmt.Fprintf(out, "VERSION: %v\n", w.metadata.Version) + _, _ = fmt.Fprintf(out, "APP_VERSION: %v\n", w.metadata.AppVersion) + _, _ = fmt.Fprintf(out, "NAMESPACE: %v\n", w.metadata.Namespace) + _, _ = fmt.Fprintf(out, "REVISION: %v\n", w.metadata.Revision) + _, _ = fmt.Fprintf(out, "STATUS: %v\n", w.metadata.Status) + _, _ = fmt.Fprintf(out, "DEPLOYED_AT: %v\n", w.metadata.DeployedAt) + return nil +} + +func (w metadataWriter) WriteJSON(out io.Writer) error { + return output.EncodeJSON(out, w.metadata) +} + +func (w metadataWriter) WriteYAML(out io.Writer) error { + return output.EncodeYAML(out, w.metadata) +} diff --git a/cmd/helm/get_metadata_test.go b/cmd/helm/get_metadata_test.go new file mode 100644 index 00000000000..b6f0ab9f2c9 --- /dev/null +++ b/cmd/helm/get_metadata_test.go @@ -0,0 +1,66 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "testing" + + "helm.sh/helm/v3/pkg/release" +) + +func TestGetMetadataCmd(t *testing.T) { + tests := []cmdTestCase{{ + name: "get metadata with a release", + cmd: "get metadata thomas-guide", + golden: "output/get-metadata.txt", + rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "thomas-guide"})}, + }, { + name: "get metadata requires release name arg", + cmd: "get metadata", + golden: "output/get-metadata-args.txt", + rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "thomas-guide"})}, + wantError: true, + }, { + name: "get metadata to json", + cmd: "get metadata thomas-guide --output json", + golden: "output/get-metadata.json", + rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "thomas-guide"})}, + }, { + name: "get metadata to yaml", + cmd: "get metadata thomas-guide --output yaml", + golden: "output/get-metadata.yaml", + rels: []*release.Release{release.Mock(&release.MockReleaseOptions{Name: "thomas-guide"})}, + }} + runTestCmd(t, tests) +} + +func TestGetMetadataCompletion(t *testing.T) { + checkReleaseCompletion(t, "get metadata", false) +} + +func TestGetMetadataRevisionCompletion(t *testing.T) { + revisionFlagCompletionTest(t, "get metadata") +} + +func TestGetMetadataOutputCompletion(t *testing.T) { + outputFlagCompletionTest(t, "get metadata") +} + +func TestGetMetadataFileCompletion(t *testing.T) { + checkFileCompletion(t, "get metadata", false) + checkFileCompletion(t, "get metadata myrelease", false) +} diff --git a/cmd/helm/install.go b/cmd/helm/install.go index 7d1a761f8b6..771ee7fa60b 100644 --- a/cmd/helm/install.go +++ b/cmd/helm/install.go @@ -154,7 +154,7 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { return errors.Wrap(err, "INSTALLATION FAILED") } - return outfmt.Write(out, &statusPrinter{rel, settings.Debug, false, false}) + return outfmt.Write(out, &statusPrinter{rel, settings.Debug, false, false, false}) }, } diff --git a/cmd/helm/release_testing.go b/cmd/helm/release_testing.go index d9b8fa8c92e..f77c4b48316 100644 --- a/cmd/helm/release_testing.go +++ b/cmd/helm/release_testing.go @@ -72,7 +72,7 @@ func newReleaseTestCmd(cfg *action.Configuration, out io.Writer) *cobra.Command return runErr } - if err := outfmt.Write(out, &statusPrinter{rel, settings.Debug, false, false}); err != nil { + if err := outfmt.Write(out, &statusPrinter{rel, settings.Debug, false, false, false}); err != nil { return err } diff --git a/cmd/helm/status.go b/cmd/helm/status.go index aa22aa02a49..850862cd549 100644 --- a/cmd/helm/status.go +++ b/cmd/helm/status.go @@ -80,7 +80,7 @@ func newStatusCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { // strip chart metadata from the output rel.Chart = nil - return outfmt.Write(out, &statusPrinter{rel, false, client.ShowDescription, client.ShowResources}) + return outfmt.Write(out, &statusPrinter{rel, false, client.ShowDescription, client.ShowResources, false}) }, } @@ -112,6 +112,7 @@ type statusPrinter struct { debug bool showDescription bool showResources bool + showMetadata bool } func (s statusPrinter) WriteJSON(out io.Writer) error { @@ -126,15 +127,20 @@ func (s statusPrinter) WriteTable(out io.Writer) error { if s.release == nil { return nil } - fmt.Fprintf(out, "NAME: %s\n", s.release.Name) + _, _ = fmt.Fprintf(out, "NAME: %s\n", s.release.Name) if !s.release.Info.LastDeployed.IsZero() { - fmt.Fprintf(out, "LAST DEPLOYED: %s\n", s.release.Info.LastDeployed.Format(time.ANSIC)) + _, _ = fmt.Fprintf(out, "LAST DEPLOYED: %s\n", s.release.Info.LastDeployed.Format(time.ANSIC)) + } + _, _ = fmt.Fprintf(out, "NAMESPACE: %s\n", s.release.Namespace) + _, _ = fmt.Fprintf(out, "STATUS: %s\n", s.release.Info.Status.String()) + _, _ = fmt.Fprintf(out, "REVISION: %d\n", s.release.Version) + if s.showMetadata { + _, _ = fmt.Fprintf(out, "CHART: %s\n", s.release.Chart.Metadata.Name) + _, _ = fmt.Fprintf(out, "VERSION: %s\n", s.release.Chart.Metadata.Version) + _, _ = fmt.Fprintf(out, "APP_VERSION: %s\n", s.release.Chart.Metadata.AppVersion) } - fmt.Fprintf(out, "NAMESPACE: %s\n", s.release.Namespace) - fmt.Fprintf(out, "STATUS: %s\n", s.release.Info.Status.String()) - fmt.Fprintf(out, "REVISION: %d\n", s.release.Version) if s.showDescription { - fmt.Fprintf(out, "DESCRIPTION: %s\n", s.release.Info.Description) + _, _ = fmt.Fprintf(out, "DESCRIPTION: %s\n", s.release.Info.Description) } if s.showResources && s.release.Info.Resources != nil && len(s.release.Info.Resources) > 0 { @@ -149,31 +155,31 @@ func (s statusPrinter) WriteTable(out io.Writer) error { } for _, t := range keys { - fmt.Fprintf(buf, "==> %s\n", t) + _, _ = fmt.Fprintf(buf, "==> %s\n", t) vk := s.release.Info.Resources[t] for _, resource := range vk { if err := printer.PrintObj(resource, buf); err != nil { - fmt.Fprintf(buf, "failed to print object type %s: %v\n", t, err) + _, _ = fmt.Fprintf(buf, "failed to print object type %s: %v\n", t, err) } } buf.WriteString("\n") } - fmt.Fprintf(out, "RESOURCES:\n%s\n", buf.String()) + _, _ = fmt.Fprintf(out, "RESOURCES:\n%s\n", buf.String()) } executions := executionsByHookEvent(s.release) if tests, ok := executions[release.HookTest]; !ok || len(tests) == 0 { - fmt.Fprintln(out, "TEST SUITE: None") + _, _ = fmt.Fprintln(out, "TEST SUITE: None") } else { for _, h := range tests { // Don't print anything if hook has not been initiated if h.LastRun.StartedAt.IsZero() { continue } - fmt.Fprintf(out, "TEST SUITE: %s\n%s\n%s\n%s\n", + _, _ = fmt.Fprintf(out, "TEST SUITE: %s\n%s\n%s\n%s\n", h.Name, fmt.Sprintf("Last Started: %s", h.LastRun.StartedAt.Format(time.ANSIC)), fmt.Sprintf("Last Completed: %s", h.LastRun.CompletedAt.Format(time.ANSIC)), @@ -183,38 +189,38 @@ func (s statusPrinter) WriteTable(out io.Writer) error { } if s.debug { - fmt.Fprintln(out, "USER-SUPPLIED VALUES:") + _, _ = fmt.Fprintln(out, "USER-SUPPLIED VALUES:") err := output.EncodeYAML(out, s.release.Config) if err != nil { return err } // Print an extra newline - fmt.Fprintln(out) + _, _ = fmt.Fprintln(out) cfg, err := chartutil.CoalesceValues(s.release.Chart, s.release.Config) if err != nil { return err } - fmt.Fprintln(out, "COMPUTED VALUES:") + _, _ = fmt.Fprintln(out, "COMPUTED VALUES:") err = output.EncodeYAML(out, cfg.AsMap()) if err != nil { return err } // Print an extra newline - fmt.Fprintln(out) + _, _ = fmt.Fprintln(out) } if strings.EqualFold(s.release.Info.Description, "Dry run complete") || s.debug { - fmt.Fprintln(out, "HOOKS:") + _, _ = fmt.Fprintln(out, "HOOKS:") for _, h := range s.release.Hooks { - fmt.Fprintf(out, "---\n# Source: %s\n%s\n", h.Path, h.Manifest) + _, _ = fmt.Fprintf(out, "---\n# Source: %s\n%s\n", h.Path, h.Manifest) } - fmt.Fprintf(out, "MANIFEST:\n%s\n", s.release.Manifest) + _, _ = fmt.Fprintf(out, "MANIFEST:\n%s\n", s.release.Manifest) } if len(s.release.Info.Notes) > 0 { - fmt.Fprintf(out, "NOTES:\n%s\n", strings.TrimSpace(s.release.Info.Notes)) + _, _ = fmt.Fprintf(out, "NOTES:\n%s\n", strings.TrimSpace(s.release.Info.Notes)) } return nil } diff --git a/cmd/helm/status_test.go b/cmd/helm/status_test.go index 6d34d6db713..6722bf9498a 100644 --- a/cmd/helm/status_test.go +++ b/cmd/helm/status_test.go @@ -32,7 +32,7 @@ func TestStatusCmd(t *testing.T) { Name: "flummoxed-chickadee", Namespace: "default", Info: info, - Chart: &chart.Chart{}, + Chart: &chart.Chart{Metadata: &chart.Metadata{Name: "name", Version: "1.2.3", AppVersion: "3.2.1"}}, Hooks: hooks, }} } diff --git a/cmd/helm/testdata/output/get-metadata-args.txt b/cmd/helm/testdata/output/get-metadata-args.txt new file mode 100644 index 00000000000..acd3f4c15ca --- /dev/null +++ b/cmd/helm/testdata/output/get-metadata-args.txt @@ -0,0 +1,3 @@ +Error: "helm get metadata" requires 1 argument + +Usage: helm get metadata RELEASE_NAME [flags] diff --git a/cmd/helm/testdata/output/get-metadata.json b/cmd/helm/testdata/output/get-metadata.json new file mode 100644 index 00000000000..1d5152b2403 --- /dev/null +++ b/cmd/helm/testdata/output/get-metadata.json @@ -0,0 +1 @@ +{"name":"thomas-guide","chart":"foo","version":"0.1.0-beta.1","appVersion":"1.0","namespace":"default","revision":1,"status":"deployed","deployedAt":"1977-09-02T22:04:05Z"} diff --git a/cmd/helm/testdata/output/get-metadata.txt b/cmd/helm/testdata/output/get-metadata.txt new file mode 100644 index 00000000000..b91f1b86a02 --- /dev/null +++ b/cmd/helm/testdata/output/get-metadata.txt @@ -0,0 +1,8 @@ +NAME: thomas-guide +CHART: foo +VERSION: 0.1.0-beta.1 +APP_VERSION: 1.0 +NAMESPACE: default +REVISION: 1 +STATUS: deployed +DEPLOYED_AT: 1977-09-02T22:04:05Z diff --git a/cmd/helm/testdata/output/get-metadata.yaml b/cmd/helm/testdata/output/get-metadata.yaml new file mode 100644 index 00000000000..b6d49b038a3 --- /dev/null +++ b/cmd/helm/testdata/output/get-metadata.yaml @@ -0,0 +1,8 @@ +appVersion: "1.0" +chart: foo +deployedAt: "1977-09-02T22:04:05Z" +name: thomas-guide +namespace: default +revision: 1 +status: deployed +version: 0.1.0-beta.1 diff --git a/cmd/helm/testdata/output/get-release.txt b/cmd/helm/testdata/output/get-release.txt index f6c3b57eb7e..12b4a407a55 100644 --- a/cmd/helm/testdata/output/get-release.txt +++ b/cmd/helm/testdata/output/get-release.txt @@ -3,6 +3,9 @@ LAST DEPLOYED: Fri Sep 2 22:04:05 1977 NAMESPACE: default STATUS: deployed REVISION: 1 +CHART: foo +VERSION: 0.1.0-beta.1 +APP_VERSION: 1.0 TEST SUITE: None USER-SUPPLIED VALUES: name: value diff --git a/cmd/helm/upgrade.go b/cmd/helm/upgrade.go index e6da2d129d8..e7e232d3937 100644 --- a/cmd/helm/upgrade.go +++ b/cmd/helm/upgrade.go @@ -139,7 +139,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { if err != nil { return err } - return outfmt.Write(out, &statusPrinter{rel, settings.Debug, false, false}) + return outfmt.Write(out, &statusPrinter{rel, settings.Debug, false, false, false}) } else if err != nil { return err } @@ -225,7 +225,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { fmt.Fprintf(out, "Release %q has been upgraded. Happy Helming!\n", args[0]) } - return outfmt.Write(out, &statusPrinter{rel, settings.Debug, false, false}) + return outfmt.Write(out, &statusPrinter{rel, settings.Debug, false, false, false}) }, } diff --git a/pkg/action/get_metadata.go b/pkg/action/get_metadata.go new file mode 100644 index 00000000000..ec096ae1654 --- /dev/null +++ b/pkg/action/get_metadata.go @@ -0,0 +1,69 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package action + +import "time" + +// GetMetadata is the action for checking a given release's metadata. +// +// It provides the implementation of 'helm get metadata'. +type GetMetadata struct { + cfg *Configuration + + Version int +} + +type Metadata struct { + Name string `json:"name" yaml:"name"` + Chart string `json:"chart" yaml:"chart"` + Version string `json:"version" yaml:"version"` + AppVersion string `json:"appVersion" yaml:"appVersion"` + Namespace string `json:"namespace" yaml:"namespace"` + Revision int `json:"revision" yaml:"revision"` + Status string `json:"status" yaml:"status"` + DeployedAt string `json:"deployedAt" yaml:"deployedAt"` +} + +// NewGetMetadata creates a new GetMetadata object with the given configuration. +func NewGetMetadata(cfg *Configuration) *GetMetadata { + return &GetMetadata{ + cfg: cfg, + } +} + +// Run executes 'helm get metadata' against the given release. +func (g *GetMetadata) Run(name string) (*Metadata, error) { + if err := g.cfg.KubeClient.IsReachable(); err != nil { + return nil, err + } + + rel, err := g.cfg.releaseContent(name, g.Version) + if err != nil { + return nil, err + } + + return &Metadata{ + Name: rel.Name, + Chart: rel.Chart.Metadata.Name, + Version: rel.Chart.Metadata.Version, + AppVersion: rel.Chart.Metadata.AppVersion, + Namespace: rel.Namespace, + Revision: rel.Version, + Status: rel.Info.Status.String(), + DeployedAt: rel.Info.LastDeployed.Format(time.RFC3339), + }, nil +}