diff --git a/cmd/helm/flags.go b/cmd/helm/flags.go new file mode 100644 index 000000000..042484a29 --- /dev/null +++ b/cmd/helm/flags.go @@ -0,0 +1,57 @@ +/* +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" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "helm.sh/helm/pkg/action" + "helm.sh/helm/pkg/cli/values" +) + +const outputFlag = "output" + +func addValueOptionsFlags(f *pflag.FlagSet, v *values.Options) { + f.StringSliceVarP(&v.ValueFiles, "values", "f", []string{}, "specify values in a YAML file or a URL(can specify multiple)") + f.StringArrayVar(&v.Values, "set", []string{}, "set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") + f.StringArrayVar(&v.StringValues, "set-string", []string{}, "set STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") + f.StringArrayVar(&v.FileValues, "set-file", []string{}, "set values from respective files specified via the command line (can specify multiple or separate values with commas: key1=path1,key2=path2)") +} + +func addChartPathOptionsFlags(f *pflag.FlagSet, c *action.ChartPathOptions) { + f.StringVar(&c.Version, "version", "", "specify the exact chart version to install. If this is not specified, the latest version is installed") + f.BoolVar(&c.Verify, "verify", false, "verify the package before installing it") + f.StringVar(&c.Keyring, "keyring", defaultKeyring(), "location of public keys used for verification") + f.StringVar(&c.RepoURL, "repo", "", "chart repository url where to locate the requested chart") + f.StringVar(&c.Username, "username", "", "chart repository username where to locate the requested chart") + f.StringVar(&c.Password, "password", "", "chart repository password where to locate the requested chart") + f.StringVar(&c.CertFile, "cert-file", "", "identify HTTPS client using this SSL certificate file") + f.StringVar(&c.KeyFile, "key-file", "", "identify HTTPS client using this SSL key file") + f.StringVar(&c.CaFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle") +} + +// bindOutputFlag will add the output flag to the given command and bind the +// value to the given string pointer +func bindOutputFlag(cmd *cobra.Command, varRef *string) { + // NOTE(taylor): A possible refactor here is that we can implement all the + // validation for the OutputFormat type here so we don't have to do the + // parsing and checking in the command + cmd.Flags().StringVarP(varRef, outputFlag, "o", string(action.Table), fmt.Sprintf("Prints the output in the specified format. Allowed values: %s, %s, %s", action.Table, action.JSON, action.YAML)) +} diff --git a/cmd/helm/history.go b/cmd/helm/history.go index 3b75c644f..ce7ceda69 100644 --- a/cmd/helm/history.go +++ b/cmd/helm/history.go @@ -56,12 +56,19 @@ func newHistoryCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { Aliases: []string{"hist"}, Args: require.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + // validate the output format first so we don't waste time running a + // request that we'll throw away + output, err := action.ParseOutputFormat(client.OutputFormat) + if err != nil { + return err + } + history, err := getHistory(client, args[0]) if err != nil { return err } - fmt.Fprintln(out, history) - return nil + + return output.Write(out, history) }, } @@ -83,28 +90,27 @@ type releaseInfo struct { type releaseHistory []releaseInfo -func marshalHistory(format action.OutputFormat, hist releaseHistory) (byt []byte, err error) { - switch format { - case action.YAML, action.JSON: - byt, err = format.Marshal(hist) - case action.Table: - byt, err = format.MarshalTable(func(tbl *uitable.Table) { - tbl.AddRow("REVISION", "UPDATED", "STATUS", "CHART", "APP VERSION", "DESCRIPTION") - for i := 0; i <= len(hist)-1; i++ { - r := hist[i] - tbl.AddRow(r.Revision, r.Updated, r.Status, r.Chart, r.AppVersion, r.Description) - } - }) - default: - err = action.ErrInvalidFormatType +func (r releaseHistory) WriteJSON(out io.Writer) error { + return action.EncodeJSON(out, r) +} + +func (r releaseHistory) WriteYAML(out io.Writer) error { + return action.EncodeYAML(out, r) +} + +func (r releaseHistory) WriteTable(out io.Writer) error { + tbl := uitable.New() + tbl.AddRow("REVISION", "UPDATED", "STATUS", "CHART", "APP VERSION", "DESCRIPTION") + for _, item := range r { + tbl.AddRow(item.Revision, item.Updated, item.Status, item.Chart, item.AppVersion, item.Description) } - return + return action.EncodeTable(out, tbl) } -func getHistory(client *action.History, name string) (string, error) { +func getHistory(client *action.History, name string) (releaseHistory, error) { hist, err := client.Run(name) if err != nil { - return "", err + return nil, err } releaseutil.Reverse(hist, releaseutil.SortByRevision) @@ -115,21 +121,12 @@ func getHistory(client *action.History, name string) (string, error) { } if len(rels) == 0 { - return "", nil + return releaseHistory{}, nil } releaseHistory := getReleaseHistory(rels) - outputFormat, err := action.ParseOutputFormat(client.OutputFormat) - if err != nil { - return "", err - } - history, formattingError := marshalHistory(outputFormat, releaseHistory) - if formattingError != nil { - return "", formattingError - } - - return string(history), nil + return releaseHistory, nil } func getReleaseHistory(rls []*release.Release) (history releaseHistory) { diff --git a/cmd/helm/install.go b/cmd/helm/install.go index 898231a3a..4c36766ea 100644 --- a/cmd/helm/install.go +++ b/cmd/helm/install.go @@ -111,16 +111,24 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { Long: installDesc, Args: require.MinimumNArgs(1), RunE: func(_ *cobra.Command, args []string) error { + // validate the output format first so we don't waste time running a + // request that we'll throw away + output, err := action.ParseOutputFormat(client.OutputFormat) + if err != nil { + return err + } + rel, err := runInstall(args, client, valueOpts, out) if err != nil { return err } - action.PrintRelease(out, rel) - return nil + + return output.Write(out, &statusPrinter{rel}) }, } addInstallFlags(cmd.Flags(), client, valueOpts) + bindOutputFlag(cmd, &client.OutputFormat) return cmd } @@ -141,25 +149,6 @@ func addInstallFlags(f *pflag.FlagSet, client *action.Install, valueOpts *values addChartPathOptionsFlags(f, &client.ChartPathOptions) } -func addValueOptionsFlags(f *pflag.FlagSet, v *values.Options) { - f.StringSliceVarP(&v.ValueFiles, "values", "f", []string{}, "specify values in a YAML file or a URL(can specify multiple)") - f.StringArrayVar(&v.Values, "set", []string{}, "set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") - f.StringArrayVar(&v.StringValues, "set-string", []string{}, "set STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") - f.StringArrayVar(&v.FileValues, "set-file", []string{}, "set values from respective files specified via the command line (can specify multiple or separate values with commas: key1=path1,key2=path2)") -} - -func addChartPathOptionsFlags(f *pflag.FlagSet, c *action.ChartPathOptions) { - f.StringVar(&c.Version, "version", "", "specify the exact chart version to install. If this is not specified, the latest version is installed") - f.BoolVar(&c.Verify, "verify", false, "verify the package before installing it") - f.StringVar(&c.Keyring, "keyring", defaultKeyring(), "location of public keys used for verification") - f.StringVar(&c.RepoURL, "repo", "", "chart repository url where to locate the requested chart") - f.StringVar(&c.Username, "username", "", "chart repository username where to locate the requested chart") - f.StringVar(&c.Password, "password", "", "chart repository password where to locate the requested chart") - f.StringVar(&c.CertFile, "cert-file", "", "identify HTTPS client using this SSL certificate file") - f.StringVar(&c.KeyFile, "key-file", "", "identify HTTPS client using this SSL key file") - f.StringVar(&c.CaFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle") -} - func runInstall(args []string, client *action.Install, valueOpts *values.Options, out io.Writer) (*release.Release, error) { debug("Original chart version: %q", client.Version) if client.Version == "" && client.Devel { diff --git a/cmd/helm/list.go b/cmd/helm/list.go index 119fcac01..7ee7565f2 100644 --- a/cmd/helm/list.go +++ b/cmd/helm/list.go @@ -19,11 +19,14 @@ package main import ( "fmt" "io" + "strconv" + "github.com/gosuri/uitable" "github.com/spf13/cobra" "helm.sh/helm/cmd/helm/require" "helm.sh/helm/pkg/action" + "helm.sh/helm/pkg/release" ) var listHelp = ` @@ -63,6 +66,13 @@ func newListCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { Aliases: []string{"ls"}, Args: require.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { + // validate the output format first so we don't waste time running a + // request that we'll throw away + output, err := action.ParseOutputFormat(client.OutputFormat) + if err != nil { + return err + } + if client.AllNamespaces { initActionConfig(cfg, true) } @@ -77,8 +87,7 @@ func newListCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { return err } - fmt.Fprintln(out, action.FormatList(results)) - return err + return output.Write(out, newReleaseListWriter(results)) }, } @@ -95,8 +104,60 @@ func newListCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { f.BoolVar(&client.Pending, "pending", false, "show pending releases") f.BoolVar(&client.AllNamespaces, "all-namespaces", false, "list releases across all namespaces") f.IntVarP(&client.Limit, "max", "m", 256, "maximum number of releases to fetch") - f.IntVarP(&client.Offset, "offset", "o", 0, "next release name in the list, used to offset from start value") + f.IntVar(&client.Offset, "offset", 0, "next release name in the list, used to offset from start value") f.StringVarP(&client.Filter, "filter", "f", "", "a regular expression (Perl compatible). Any releases that match the expression will be included in the results") + bindOutputFlag(cmd, &client.OutputFormat) return cmd } + +type releaseElement struct { + Name string + Namespace string + Revision string + Updated string + Status string + Chart string +} + +type releaseListWriter struct { + releases []releaseElement +} + +func newReleaseListWriter(releases []*release.Release) *releaseListWriter { + // Initialize the array so no results returns an empty array instead of null + elements := make([]releaseElement, 0, len(releases)) + for _, r := range releases { + element := releaseElement{ + Name: r.Name, + Namespace: r.Namespace, + Revision: strconv.Itoa(r.Version), + Status: r.Info.Status.String(), + Chart: fmt.Sprintf("%s-%s", r.Chart.Metadata.Name, r.Chart.Metadata.Version), + } + t := "-" + if tspb := r.Info.LastDeployed; !tspb.IsZero() { + t = tspb.String() + } + element.Updated = t + elements = append(elements, element) + } + return &releaseListWriter{elements} +} + +func (r *releaseListWriter) WriteTable(out io.Writer) error { + table := uitable.New() + table.AddRow("NAME", "NAMESPACE", "REVISION", "UPDATED", "STATUS", "CHART") + for _, r := range r.releases { + table.AddRow(r.Name, r.Namespace, r.Revision, r.Updated, r.Status, r.Chart) + } + return action.EncodeTable(out, table) +} + +func (r *releaseListWriter) WriteJSON(out io.Writer) error { + return action.EncodeJSON(out, r.releases) +} + +func (r *releaseListWriter) WriteYAML(out io.Writer) error { + return action.EncodeYAML(out, r.releases) +} diff --git a/cmd/helm/repo_list.go b/cmd/helm/repo_list.go index 7825faebd..6136bc022 100644 --- a/cmd/helm/repo_list.go +++ b/cmd/helm/repo_list.go @@ -17,7 +17,6 @@ limitations under the License. package main import ( - "fmt" "io" "github.com/gosuri/uitable" @@ -25,28 +24,79 @@ import ( "github.com/spf13/cobra" "helm.sh/helm/cmd/helm/require" + "helm.sh/helm/pkg/action" "helm.sh/helm/pkg/repo" ) func newRepoListCmd(out io.Writer) *cobra.Command { + var output string cmd := &cobra.Command{ Use: "list", Short: "list chart repositories", Args: require.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { + // validate the output format first so we don't waste time running a + // request that we'll throw away + outfmt, err := action.ParseOutputFormat(output) + if err != nil { + return err + } f, err := repo.LoadFile(settings.RepositoryConfig) if isNotExist(err) || len(f.Repositories) == 0 { return errors.New("no repositories to show") } - table := uitable.New() - table.AddRow("NAME", "URL") - for _, re := range f.Repositories { - table.AddRow(re.Name, re.URL) - } - fmt.Fprintln(out, table) - return nil + + return outfmt.Write(out, &repoListWriter{f.Repositories}) }, } + bindOutputFlag(cmd, &output) + return cmd } + +type repositoryElement struct { + Name string + URL string +} + +type repoListWriter struct { + repos []*repo.Entry +} + +func (r *repoListWriter) WriteTable(out io.Writer) error { + table := uitable.New() + table.AddRow("NAME", "URL") + for _, re := range r.repos { + table.AddRow(re.Name, re.URL) + } + return action.EncodeTable(out, table) +} + +func (r *repoListWriter) WriteJSON(out io.Writer) error { + return r.encodeByFormat(out, action.JSON) +} + +func (r *repoListWriter) WriteYAML(out io.Writer) error { + return r.encodeByFormat(out, action.YAML) +} + +func (r *repoListWriter) encodeByFormat(out io.Writer, format action.OutputFormat) error { + // Initialize the array so no results returns an empty array instead of null + repolist := make([]repositoryElement, 0, len(r.repos)) + + for _, re := range r.repos { + repolist = append(repolist, repositoryElement{Name: re.Name, URL: re.URL}) + } + + switch format { + case action.JSON: + return action.EncodeJSON(out, repolist) + case action.YAML: + return action.EncodeYAML(out, repolist) + } + + // Because this is a non-exported function and only called internally by + // WriteJSON and WriteYAML, we shouldn't get invalid types + return nil +} diff --git a/cmd/helm/search_hub.go b/cmd/helm/search_hub.go index acc048aab..3775c79bd 100644 --- a/cmd/helm/search_hub.go +++ b/cmd/helm/search_hub.go @@ -26,6 +26,7 @@ import ( "github.com/spf13/cobra" "helm.sh/helm/internal/monocular" + "helm.sh/helm/pkg/action" ) const searchHubDesc = ` @@ -43,6 +44,7 @@ Helm Hub. You can find it at https://github.com/helm/monocular type searchHubOptions struct { searchEndpoint string maxColWidth uint + outputFormat string } func newSearchHubCmd(out io.Writer) *cobra.Command { @@ -60,11 +62,18 @@ func newSearchHubCmd(out io.Writer) *cobra.Command { f := cmd.Flags() f.StringVar(&o.searchEndpoint, "endpoint", "https://hub.helm.sh", "monocular instance to query for charts") f.UintVar(&o.maxColWidth, "max-col-width", 50, "maximum column width for output table") + bindOutputFlag(cmd, &o.outputFormat) return cmd } func (o *searchHubOptions) run(out io.Writer, args []string) error { + // validate the output format first so we don't waste time running a + // request that we'll throw away + outfmt, err := action.ParseOutputFormat(o.outputFormat) + if err != nil { + return err + } c, err := monocular.New(o.searchEndpoint) if err != nil { @@ -78,25 +87,71 @@ func (o *searchHubOptions) run(out io.Writer, args []string) error { return fmt.Errorf("unable to perform search against %q", o.searchEndpoint) } - fmt.Fprintln(out, o.formatSearchResults(o.searchEndpoint, results)) + return outfmt.Write(out, newHubSearchWriter(results, o.searchEndpoint, o.maxColWidth)) +} + +type hubChartElement struct { + URL string + Version string + AppVersion string + Description string +} - return nil +type hubSearchWriter struct { + elements []hubChartElement + columnWidth uint } -func (o *searchHubOptions) formatSearchResults(endpoint string, res []monocular.SearchResult) string { - if len(res) == 0 { - return "No results found" +func newHubSearchWriter(results []monocular.SearchResult, endpoint string, columnWidth uint) *hubSearchWriter { + var elements []hubChartElement + for _, r := range results { + url := endpoint + "/charts/" + r.ID + elements = append(elements, hubChartElement{url, r.Relationships.LatestChartVersion.Data.Version, r.Relationships.LatestChartVersion.Data.AppVersion, r.Attributes.Description}) } - table := uitable.New() + return &hubSearchWriter{elements, columnWidth} +} - // The max column width is configurable because a URL could be longer than the - // max value and we want the user to have the ability to display the whole url - table.MaxColWidth = o.maxColWidth +func (h *hubSearchWriter) WriteTable(out io.Writer) error { + if len(h.elements) == 0 { + _, err := out.Write([]byte("No results found\n")) + if err != nil { + return fmt.Errorf("unable to write results: %s", err) + } + return nil + } + table := uitable.New() + table.MaxColWidth = h.columnWidth table.AddRow("URL", "CHART VERSION", "APP VERSION", "DESCRIPTION") - var url string - for _, r := range res { - url = endpoint + "/charts/" + r.ID - table.AddRow(url, r.Relationships.LatestChartVersion.Data.Version, r.Relationships.LatestChartVersion.Data.AppVersion, r.Attributes.Description) + for _, r := range h.elements { + table.AddRow(r.URL, r.Version, r.AppVersion, r.Description) + } + return action.EncodeTable(out, table) +} + +func (h *hubSearchWriter) WriteJSON(out io.Writer) error { + return h.encodeByFormat(out, action.JSON) +} + +func (h *hubSearchWriter) WriteYAML(out io.Writer) error { + return h.encodeByFormat(out, action.YAML) +} + +func (h *hubSearchWriter) encodeByFormat(out io.Writer, format action.OutputFormat) error { + // Initialize the array so no results returns an empty array instead of null + chartList := make([]hubChartElement, 0, len(h.elements)) + + for _, r := range h.elements { + chartList = append(chartList, hubChartElement{r.URL, r.Version, r.AppVersion, r.Description}) + } + + switch format { + case action.JSON: + return action.EncodeJSON(out, chartList) + case action.YAML: + return action.EncodeYAML(out, chartList) } - return table.String() + + // Because this is a non-exported function and only called internally by + // WriteJSON and WriteYAML, we shouldn't get invalid types + return nil } diff --git a/cmd/helm/search_repo.go b/cmd/helm/search_repo.go index 21781d6da..b7c34ccc9 100644 --- a/cmd/helm/search_repo.go +++ b/cmd/helm/search_repo.go @@ -28,6 +28,7 @@ import ( "github.com/spf13/cobra" "helm.sh/helm/cmd/helm/search" + "helm.sh/helm/pkg/action" "helm.sh/helm/pkg/helmpath" "helm.sh/helm/pkg/repo" ) @@ -50,6 +51,7 @@ type searchRepoOptions struct { maxColWidth uint repoFile string repoCacheDir string + outputFormat string } func newSearchRepoCmd(out io.Writer) *cobra.Command { @@ -71,11 +73,19 @@ func newSearchRepoCmd(out io.Writer) *cobra.Command { f.BoolVarP(&o.versions, "versions", "l", false, "show the long listing, with each version of each chart on its own line, for repositories you have added") f.StringVar(&o.version, "version", "", "search using semantic versioning constraints on repositories you have added") f.UintVar(&o.maxColWidth, "max-col-width", 50, "maximum column width for output table") + bindOutputFlag(cmd, &o.outputFormat) return cmd } func (o *searchRepoOptions) run(out io.Writer, args []string) error { + // validate the output format first so we don't waste time running a + // request that we'll throw away + outfmt, err := action.ParseOutputFormat(o.outputFormat) + if err != nil { + return err + } + index, err := o.buildIndex(out) if err != nil { return err @@ -98,9 +108,7 @@ func (o *searchRepoOptions) run(out io.Writer, args []string) error { return err } - fmt.Fprintln(out, o.formatSearchResults(data)) - - return nil + return outfmt.Write(out, &repoSearchWriter{data, o.maxColWidth}) } func (o *searchRepoOptions) applyConstraint(res []*search.Result) ([]*search.Result, error) { @@ -131,19 +139,6 @@ func (o *searchRepoOptions) applyConstraint(res []*search.Result) ([]*search.Res return data, nil } -func (o *searchRepoOptions) formatSearchResults(res []*search.Result) string { - if len(res) == 0 { - return "No results found" - } - table := uitable.New() - table.MaxColWidth = o.maxColWidth - table.AddRow("NAME", "CHART VERSION", "APP VERSION", "DESCRIPTION") - for _, r := range res { - table.AddRow(r.Name, r.Chart.Version, r.Chart.AppVersion, r.Chart.Description) - } - return table.String() -} - func (o *searchRepoOptions) buildIndex(out io.Writer) (*search.Index, error) { // Load the repositories.yaml rf, err := repo.LoadFile(o.repoFile) @@ -166,3 +161,60 @@ func (o *searchRepoOptions) buildIndex(out io.Writer) (*search.Index, error) { } return i, nil } + +type repoChartElement struct { + Name string + Version string + AppVersion string + Description string +} + +type repoSearchWriter struct { + results []*search.Result + columnWidth uint +} + +func (r *repoSearchWriter) WriteTable(out io.Writer) error { + if len(r.results) == 0 { + _, err := out.Write([]byte("No results found\n")) + if err != nil { + return fmt.Errorf("unable to write results: %s", err) + } + return nil + } + table := uitable.New() + table.MaxColWidth = r.columnWidth + table.AddRow("NAME", "CHART VERSION", "APP VERSION", "DESCRIPTION") + for _, r := range r.results { + table.AddRow(r.Name, r.Chart.Version, r.Chart.AppVersion, r.Chart.Description) + } + return action.EncodeTable(out, table) +} + +func (r *repoSearchWriter) WriteJSON(out io.Writer) error { + return r.encodeByFormat(out, action.JSON) +} + +func (r *repoSearchWriter) WriteYAML(out io.Writer) error { + return r.encodeByFormat(out, action.YAML) +} + +func (r *repoSearchWriter) encodeByFormat(out io.Writer, format action.OutputFormat) error { + // Initialize the array so no results returns an empty array instead of null + chartList := make([]repoChartElement, 0, len(r.results)) + + for _, r := range r.results { + chartList = append(chartList, repoChartElement{r.Name, r.Chart.Version, r.Chart.AppVersion, r.Chart.Description}) + } + + switch format { + case action.JSON: + return action.EncodeJSON(out, chartList) + case action.YAML: + return action.EncodeYAML(out, chartList) + } + + // Because this is a non-exported function and only called internally by + // WriteJSON and WriteYAML, we shouldn't get invalid types + return nil +} diff --git a/cmd/helm/search_repo_test.go b/cmd/helm/search_repo_test.go index babab73e6..5e945e3b7 100644 --- a/cmd/helm/search_repo_test.go +++ b/cmd/helm/search_repo_test.go @@ -64,6 +64,14 @@ func TestSearchRepositoriesCmd(t *testing.T) { name: "search for 'alp[', expect failure to compile regexp", cmd: "search repo alp[ --regexp", wantError: true, + }, { + name: "search for 'maria', expect valid json output", + cmd: "search repo maria --output json", + golden: "output/search-output-json.txt", + }, { + name: "search for 'alpine', expect valid yaml output", + cmd: "search repo alpine --output yaml", + golden: "output/search-output-yaml.txt", }} settings.Debug = true diff --git a/cmd/helm/status.go b/cmd/helm/status.go index 5b7cecd68..f97e0d475 100644 --- a/cmd/helm/status.go +++ b/cmd/helm/status.go @@ -19,11 +19,11 @@ package main import ( "io" - "github.com/pkg/errors" "github.com/spf13/cobra" "helm.sh/helm/cmd/helm/require" "helm.sh/helm/pkg/action" + "helm.sh/helm/pkg/release" ) var statusHelp = ` @@ -46,6 +46,13 @@ func newStatusCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { Long: statusHelp, Args: require.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + // validate the output format first so we don't waste time running a + // request that we'll throw away + outfmt, err := action.ParseOutputFormat(client.OutputFormat) + if err != nil { + return err + } + rel, err := client.Run(args[0]) if err != nil { return err @@ -54,32 +61,30 @@ func newStatusCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { // strip chart metadata from the output rel.Chart = nil - outfmt, err := action.ParseOutputFormat(client.OutputFormat) - // We treat an invalid format type as the default - if err != nil && err != action.ErrInvalidFormatType { - return err - } - - switch outfmt { - case "": - action.PrintRelease(out, rel) - return nil - case action.JSON, action.YAML: - data, err := outfmt.Marshal(rel) - if err != nil { - return errors.Wrap(err, "failed to Marshal output") - } - out.Write(data) - return nil - default: - return errors.Errorf("unknown output format %q", outfmt) - } + return outfmt.Write(out, &statusPrinter{rel}) }, } f := cmd.PersistentFlags() f.IntVar(&client.Version, "revision", 0, "if set, display the status of the named release with revision") - f.StringVarP(&client.OutputFormat, "output", "o", "", "output the status in the specified format (json or yaml)") + bindOutputFlag(cmd, &client.OutputFormat) return cmd } + +type statusPrinter struct { + release *release.Release +} + +func (s statusPrinter) WriteJSON(out io.Writer) error { + return action.EncodeJSON(out, s.release) +} + +func (s statusPrinter) WriteYAML(out io.Writer) error { + return action.EncodeYAML(out, s.release) +} + +func (s statusPrinter) WriteTable(out io.Writer) error { + action.PrintRelease(out, s.release) + return nil +} diff --git a/cmd/helm/testdata/output/history.yaml b/cmd/helm/testdata/output/history.yaml index fc2887068..d315b6fc9 100644 --- a/cmd/helm/testdata/output/history.yaml +++ b/cmd/helm/testdata/output/history.yaml @@ -10,4 +10,3 @@ revision: 4 status: deployed updated: 1977-09-02 22:04:05 +0000 UTC - diff --git a/cmd/helm/testdata/output/search-output-json.txt b/cmd/helm/testdata/output/search-output-json.txt new file mode 100644 index 000000000..d462a12c1 --- /dev/null +++ b/cmd/helm/testdata/output/search-output-json.txt @@ -0,0 +1 @@ +[{"Name":"testing/mariadb","Version":"0.3.0","AppVersion":"","Description":"Chart for MariaDB"}] diff --git a/cmd/helm/testdata/output/search-output-yaml.txt b/cmd/helm/testdata/output/search-output-yaml.txt new file mode 100644 index 000000000..5034d8ce0 --- /dev/null +++ b/cmd/helm/testdata/output/search-output-yaml.txt @@ -0,0 +1,4 @@ +- AppVersion: 2.3.4 + Description: Deploy a basic Alpine Linux pod + Name: testing/alpine + Version: 0.2.0 diff --git a/cmd/helm/testdata/output/status.json b/cmd/helm/testdata/output/status.json index b57687c5c..4be4c7210 100644 --- a/cmd/helm/testdata/output/status.json +++ b/cmd/helm/testdata/output/status.json @@ -1 +1 @@ -{"name":"flummoxed-chickadee","info":{"first_deployed":"0001-01-01T00:00:00Z","last_deployed":"2016-01-16T00:00:00Z","deleted":"0001-01-01T00:00:00Z","status":"deployed","notes":"release notes"},"namespace":"default"} \ No newline at end of file +{"name":"flummoxed-chickadee","info":{"first_deployed":"0001-01-01T00:00:00Z","last_deployed":"2016-01-16T00:00:00Z","deleted":"0001-01-01T00:00:00Z","status":"deployed","notes":"release notes"},"namespace":"default"} diff --git a/cmd/helm/upgrade.go b/cmd/helm/upgrade.go index f04641dd5..d9cd997de 100644 --- a/cmd/helm/upgrade.go +++ b/cmd/helm/upgrade.go @@ -69,6 +69,13 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { Long: upgradeDesc, Args: require.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { + // validate the output format first so we don't waste time running a + // request that we'll throw away + output, err := action.ParseOutputFormat(client.OutputFormat) + if err != nil { + return err + } + client.Namespace = getNamespace() if client.Version == "" && client.Devel { @@ -104,8 +111,10 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { instClient.Atomic = client.Atomic rel, err := runInstall(args, instClient, valueOpts, out) - action.PrintRelease(out, rel) - return err + if err != nil { + return err + } + return output.Write(out, &statusPrinter{rel}) } } @@ -129,7 +138,9 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { action.PrintRelease(out, resp) } - fmt.Fprintf(out, "Release %q has been upgraded. Happy Helming!\n", args[0]) + if output == action.Table { + fmt.Fprintf(out, "Release %q has been upgraded. Happy Helming!\n", args[0]) + } // Print the status like status command does statusClient := action.NewStatus(cfg) @@ -137,9 +148,8 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { if err != nil { return err } - action.PrintRelease(out, rel) - return nil + return output.Write(out, &statusPrinter{rel}) }, } @@ -160,6 +170,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { f.BoolVar(&client.CleanupOnFail, "cleanup-on-fail", false, "allow deletion of new resources created in this upgrade when upgrade fails") addChartPathOptionsFlags(f, &client.ChartPathOptions) addValueOptionsFlags(f, valueOpts) + bindOutputFlag(cmd, &client.OutputFormat) return cmd } diff --git a/pkg/action/install.go b/pkg/action/install.go index 173a8b08b..8d2ea939d 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -82,6 +82,7 @@ type Install struct { OutputDir string Atomic bool SkipCRDs bool + OutputFormat string } // ChartPathOptions captures common options used for controlling chart paths diff --git a/pkg/action/list.go b/pkg/action/list.go index 201a0db3e..6022a24cd 100644 --- a/pkg/action/list.go +++ b/pkg/action/list.go @@ -17,11 +17,8 @@ limitations under the License. package action import ( - "fmt" "regexp" - "github.com/gosuri/uitable" - "helm.sh/helm/pkg/release" "helm.sh/helm/pkg/releaseutil" ) @@ -128,6 +125,7 @@ type List struct { Deployed bool Failed bool Pending bool + OutputFormat string } // NewList constructs a new *List @@ -278,21 +276,3 @@ func (l *List) SetStateMask() { l.StateMask = state } - -func FormatList(rels []*release.Release) string { - table := uitable.New() - table.AddRow("NAME", "NAMESPACE", "REVISION", "UPDATED", "STATUS", "CHART") - for _, r := range rels { - md := r.Chart.Metadata - c := fmt.Sprintf("%s-%s", md.Name, md.Version) - t := "-" - if tspb := r.Info.LastDeployed; !tspb.IsZero() { - t = tspb.String() - } - s := r.Info.Status.String() - v := r.Version - n := r.Namespace - table.AddRow(r.Name, n, v, t, s, c) - } - return table.String() -} diff --git a/pkg/action/output.go b/pkg/action/output.go index 4948464b5..0b3ee029a 100644 --- a/pkg/action/output.go +++ b/pkg/action/output.go @@ -19,17 +19,16 @@ package action import ( "encoding/json" "fmt" + "io" "github.com/gosuri/uitable" + "github.com/pkg/errors" "sigs.k8s.io/yaml" ) // OutputFormat is a type for capturing supported output formats type OutputFormat string -// TableFunc is a function that can be used to add rows to a table -type TableFunc func(tbl *uitable.Table) - const ( Table OutputFormat = "table" JSON OutputFormat = "json" @@ -44,32 +43,18 @@ func (o OutputFormat) String() string { return string(o) } -// Marshal uses the specified output format to marshal out the given data. It -// does not support tabular output. For tabular output, use MarshalTable -func (o OutputFormat) Marshal(data interface{}) (byt []byte, err error) { +// Write the output in the given format to the io.Writer. Unsupported formats +// will return an error +func (o OutputFormat) Write(out io.Writer, w Writer) error { switch o { - case YAML: - byt, err = yaml.Marshal(data) + case Table: + return w.WriteTable(out) case JSON: - byt, err = json.Marshal(data) - default: - err = ErrInvalidFormatType - } - return -} - -// MarshalTable returns a formatted table using the given headers. Rows can be -// added to the table using the given TableFunc -func (o OutputFormat) MarshalTable(f TableFunc) ([]byte, error) { - if o != Table { - return nil, ErrInvalidFormatType - } - tbl := uitable.New() - if f == nil { - return []byte{}, nil + return w.WriteJSON(out) + case YAML: + return w.WriteYAML(out) } - f(tbl) - return tbl.Bytes(), nil + return ErrInvalidFormatType } // ParseOutputFormat takes a raw string and returns the matching OutputFormat. @@ -87,3 +72,54 @@ func ParseOutputFormat(s string) (out OutputFormat, err error) { } return } + +// Writer is an interface that any type can implement to write supported formats +type Writer interface { + // WriteTable will write tabular output into the given io.Writer, returning + // an error if any occur + WriteTable(out io.Writer) error + // WriteJSON will write JSON formatted output into the given io.Writer, + // returning an error if any occur + WriteJSON(out io.Writer) error + // WriteYAML will write YAML formatted output into the given io.Writer, + // returning an error if any occur + WriteYAML(out io.Writer) error +} + +// EncodeJSON is a helper function to decorate any error message with a bit more +// context and avoid writing the same code over and over for printers. +func EncodeJSON(out io.Writer, obj interface{}) error { + enc := json.NewEncoder(out) + err := enc.Encode(obj) + if err != nil { + return errors.Wrap(err, "unable to write JSON output") + } + return nil +} + +// EncodeYAML is a helper function to decorate any error message with a bit more +// context and avoid writing the same code over and over for printers +func EncodeYAML(out io.Writer, obj interface{}) error { + raw, err := yaml.Marshal(obj) + if err != nil { + return errors.Wrap(err, "unable to write YAML output") + } + + _, err = out.Write(raw) + if err != nil { + return errors.Wrap(err, "unable to write YAML output") + } + return nil +} + +// EncodeTable is a helper function to decorate any error message with a bit +// more context and avoid writing the same code over and over for printers +func EncodeTable(out io.Writer, table *uitable.Table) error { + raw := table.Bytes() + raw = append(raw, []byte("\n")...) + _, err := out.Write(raw) + if err != nil { + return errors.Wrap(err, "unable to write table output") + } + return nil +} diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index 3d6220d7f..265e8df86 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -56,6 +56,7 @@ type Upgrade struct { MaxHistory int Atomic bool CleanupOnFail bool + OutputFormat string } // NewUpgrade creates a new Upgrade object with the given configuration.