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_metadata.go b/cmd/helm/get_metadata.go new file mode 100644 index 000000000..0c8302365 --- /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.Fprint(out, fmt.Sprintf("NAME: %v\n", w.metadata.Name)) + _, _ = fmt.Fprint(out, fmt.Sprintf("CHART: %v\n", w.metadata.Chart)) + _, _ = fmt.Fprint(out, fmt.Sprintf("VERSION: %v\n", w.metadata.Version)) + _, _ = fmt.Fprint(out, fmt.Sprintf("APP_VERSION: %v\n", w.metadata.AppVersion)) + _, _ = fmt.Fprint(out, fmt.Sprintf("NAMESPACE: %v\n", w.metadata.Namespace)) + _, _ = fmt.Fprint(out, fmt.Sprintf("REVISION: %v\n", w.metadata.Revision)) + _, _ = fmt.Fprint(out, fmt.Sprintf("STATUS: %v\n", w.metadata.Status)) + _, _ = fmt.Fprint(out, fmt.Sprintf("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 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/status.go b/cmd/helm/status.go index aa22aa02a..bfe058494 100644 --- a/cmd/helm/status.go +++ b/cmd/helm/status.go @@ -126,15 +126,18 @@ 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) + _, _ = fmt.Fprintf(out, "CHART: %s\n", s.release.Chart.Metadata.Name) + _, _ = 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) + _, _ = fmt.Fprintf(out, "VERSION: %s\n", s.release.Chart.Metadata.Version) + _, _ = fmt.Fprintf(out, "APP_VERSION: %s\n", s.release.Chart.Metadata.AppVersion) 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 +152,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 +186,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/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 @@ +{"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..3e83ca946 100644 --- a/cmd/helm/testdata/output/get-release.txt +++ b/cmd/helm/testdata/output/get-release.txt @@ -1,8 +1,11 @@ NAME: thomas-guide LAST DEPLOYED: Fri Sep 2 22:04:05 1977 +CHART: foo NAMESPACE: default STATUS: deployed REVISION: 1 +VERSION: 0.1.0-beta.1 +APP_VERSION: 1.0 TEST SUITE: None USER-SUPPLIED VALUES: name: value 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 +}