Repository: helm/helm (go)

Issue:
Introduce `helm get metadata` command
### Problem
There's a command `helm get` with a number of subcommands:
* `values`
* `notes`
* `manifest`
* `hooks`
* `all`

None of them return the chart version that was used. The chart version is returned in `helm list` though.
None of them return the namespace or revision or helm chart app version as well.
Deploy timestamp is returned, but only by `all` command, which does not have `--output` flag, so it can't output to machine-readable JSON or YAML formats.

### Proposal
Introduce `helm get metadata RELEASE_NAME [flags]` command:
* to return the following release attributes:
  * `name` -- the release name
  * `chart` -- the chart name (note: just a chart name; chart version goes in a separate attribute)
  * `version` -- the helm chart `version` field
  * `appVersion` -- the helm chart `appVersion` field
  * `namespace` -- the namespace where the chart is installed into
  * `revision` -- the revision of the release
  * `status` -- the release status
* to have `--output` flag with options:
  * `table`  -- default
  * `json`
  * `yaml`

Pull request description:
Restore `helm get metadata` command
<!--  Thanks for sending a pull request!  Here are some tips for you:
1. Make sure to read the Contributing Guide before submitting your PR: https://github.com/helm/helm/blob/main/CONTRIBUTING.md
2. If this PR closes another issue, add 'closes #<issue number>' somewhere in the PR summary. GitHub will automatically close that issue when this PR gets merged. Alternatively, adding 'refs #<issue number>' will not close the issue, but help provide the reviewer more context.-->

Closes #11625

**What this PR does / why we need it**:
The PR introduces a new command `helm get metadata`

**Special notes for your reviewer**:

**If applicable**:
- [x] this PR contains unit tests
- [x] this PR has been tested for backwards compatibility



This PR restores #11631 that was merged first but rolled back then because `main` branch went forward while it was waiting, and it failed build after being merged to `main`

Patch:
diff --git a/cmd/helm/get.go b/cmd/helm/get.go
index 7c4854b59..727cdaf88 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 2dbef97cf..e51d50536 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 000000000..adab891bd
--- /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/install.go b/cmd/helm/install.go
index 7d1a761f8..771ee7fa6 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/status.go b/cmd/helm/status.go
index aa22aa02a..850862cd5 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/upgrade.go b/cmd/helm/upgrade.go
index e6da2d129..e7e232d39 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 000000000..ec096ae16
--- /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
+}


Test patch:
diff --git a/cmd/helm/get_metadata_test.go b/cmd/helm/get_metadata_test.go
new file mode 100644
index 000000000..b6f0ab9f2
--- /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/release_testing.go b/cmd/helm/release_testing.go
index d9b8fa8c9..f77c4b483 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_test.go b/cmd/helm/status_test.go
index 6d34d6db7..6722bf949 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 000000000..acd3f4c15
--- /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 000000000..1d5152b24
--- /dev/null
+++ b/cmd/helm/testdata/output/get-metadata.json
@@ -0,0 +1,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 000000000..b91f1b86a
--- /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 000000000..b6d49b038
--- /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 f6c3b57eb..12b4a407a 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