feat(*): Ports all output functionality from v2

As part of this port, I removed some now superfluous code from the `action` package.
This is technically a breaking change, but since the package was introduced in v3, it
is highly unlikely anyone is using it and we are still within the beta window.

Also closes #6437

Signed-off-by: Taylor Thomas <taylor.thomas@microsoft.com>
pull/6504/head
Taylor Thomas 5 years ago
parent 85572df378
commit eac6a60001

@ -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))
}

@ -56,12 +56,19 @@ func newHistoryCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
Aliases: []string{"hist"}, Aliases: []string{"hist"},
Args: require.ExactArgs(1), Args: require.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { 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]) history, err := getHistory(client, args[0])
if err != nil { if err != nil {
return err return err
} }
fmt.Fprintln(out, history)
return nil return output.Write(out, history)
}, },
} }
@ -83,28 +90,27 @@ type releaseInfo struct {
type releaseHistory []releaseInfo type releaseHistory []releaseInfo
func marshalHistory(format action.OutputFormat, hist releaseHistory) (byt []byte, err error) { func (r releaseHistory) WriteJSON(out io.Writer) error {
switch format { return action.EncodeJSON(out, r)
case action.YAML, action.JSON: }
byt, err = format.Marshal(hist)
case action.Table: func (r releaseHistory) WriteYAML(out io.Writer) error {
byt, err = format.MarshalTable(func(tbl *uitable.Table) { return action.EncodeYAML(out, r)
tbl.AddRow("REVISION", "UPDATED", "STATUS", "CHART", "APP VERSION", "DESCRIPTION") }
for i := 0; i <= len(hist)-1; i++ {
r := hist[i] func (r releaseHistory) WriteTable(out io.Writer) error {
tbl.AddRow(r.Revision, r.Updated, r.Status, r.Chart, r.AppVersion, r.Description) tbl := uitable.New()
} tbl.AddRow("REVISION", "UPDATED", "STATUS", "CHART", "APP VERSION", "DESCRIPTION")
}) for _, item := range r {
default: tbl.AddRow(item.Revision, item.Updated, item.Status, item.Chart, item.AppVersion, item.Description)
err = action.ErrInvalidFormatType
} }
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) hist, err := client.Run(name)
if err != nil { if err != nil {
return "", err return nil, err
} }
releaseutil.Reverse(hist, releaseutil.SortByRevision) releaseutil.Reverse(hist, releaseutil.SortByRevision)
@ -115,21 +121,12 @@ func getHistory(client *action.History, name string) (string, error) {
} }
if len(rels) == 0 { if len(rels) == 0 {
return "", nil return releaseHistory{}, nil
} }
releaseHistory := getReleaseHistory(rels) releaseHistory := getReleaseHistory(rels)
outputFormat, err := action.ParseOutputFormat(client.OutputFormat) return releaseHistory, nil
if err != nil {
return "", err
}
history, formattingError := marshalHistory(outputFormat, releaseHistory)
if formattingError != nil {
return "", formattingError
}
return string(history), nil
} }
func getReleaseHistory(rls []*release.Release) (history releaseHistory) { func getReleaseHistory(rls []*release.Release) (history releaseHistory) {

@ -111,16 +111,24 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
Long: installDesc, Long: installDesc,
Args: require.MinimumNArgs(1), Args: require.MinimumNArgs(1),
RunE: func(_ *cobra.Command, args []string) error { 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) rel, err := runInstall(args, client, valueOpts, out)
if err != nil { if err != nil {
return err return err
} }
action.PrintRelease(out, rel)
return nil return output.Write(out, &statusPrinter{rel})
}, },
} }
addInstallFlags(cmd.Flags(), client, valueOpts) addInstallFlags(cmd.Flags(), client, valueOpts)
bindOutputFlag(cmd, &client.OutputFormat)
return cmd return cmd
} }
@ -141,25 +149,6 @@ func addInstallFlags(f *pflag.FlagSet, client *action.Install, valueOpts *values
addChartPathOptionsFlags(f, &client.ChartPathOptions) 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) { func runInstall(args []string, client *action.Install, valueOpts *values.Options, out io.Writer) (*release.Release, error) {
debug("Original chart version: %q", client.Version) debug("Original chart version: %q", client.Version)
if client.Version == "" && client.Devel { if client.Version == "" && client.Devel {

@ -19,11 +19,14 @@ package main
import ( import (
"fmt" "fmt"
"io" "io"
"strconv"
"github.com/gosuri/uitable"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/cmd/helm/require" "helm.sh/helm/cmd/helm/require"
"helm.sh/helm/pkg/action" "helm.sh/helm/pkg/action"
"helm.sh/helm/pkg/release"
) )
var listHelp = ` var listHelp = `
@ -63,6 +66,13 @@ func newListCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
Aliases: []string{"ls"}, Aliases: []string{"ls"},
Args: require.NoArgs, Args: require.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error { 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 { if client.AllNamespaces {
initActionConfig(cfg, true) initActionConfig(cfg, true)
} }
@ -77,8 +87,7 @@ func newListCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
return err return err
} }
fmt.Fprintln(out, action.FormatList(results)) return output.Write(out, newReleaseListWriter(results))
return err
}, },
} }
@ -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.Pending, "pending", false, "show pending releases")
f.BoolVar(&client.AllNamespaces, "all-namespaces", false, "list releases across all namespaces") 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.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") 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 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)
}

@ -17,7 +17,6 @@ limitations under the License.
package main package main
import ( import (
"fmt"
"io" "io"
"github.com/gosuri/uitable" "github.com/gosuri/uitable"
@ -25,28 +24,79 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/cmd/helm/require" "helm.sh/helm/cmd/helm/require"
"helm.sh/helm/pkg/action"
"helm.sh/helm/pkg/repo" "helm.sh/helm/pkg/repo"
) )
func newRepoListCmd(out io.Writer) *cobra.Command { func newRepoListCmd(out io.Writer) *cobra.Command {
var output string
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "list", Use: "list",
Short: "list chart repositories", Short: "list chart repositories",
Args: require.NoArgs, Args: require.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error { 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) f, err := repo.LoadFile(settings.RepositoryConfig)
if isNotExist(err) || len(f.Repositories) == 0 { if isNotExist(err) || len(f.Repositories) == 0 {
return errors.New("no repositories to show") return errors.New("no repositories to show")
} }
table := uitable.New()
table.AddRow("NAME", "URL") return outfmt.Write(out, &repoListWriter{f.Repositories})
for _, re := range f.Repositories {
table.AddRow(re.Name, re.URL)
}
fmt.Fprintln(out, table)
return nil
}, },
} }
bindOutputFlag(cmd, &output)
return cmd 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
}

@ -26,6 +26,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/internal/monocular" "helm.sh/helm/internal/monocular"
"helm.sh/helm/pkg/action"
) )
const searchHubDesc = ` const searchHubDesc = `
@ -43,6 +44,7 @@ Helm Hub. You can find it at https://github.com/helm/monocular
type searchHubOptions struct { type searchHubOptions struct {
searchEndpoint string searchEndpoint string
maxColWidth uint maxColWidth uint
outputFormat string
} }
func newSearchHubCmd(out io.Writer) *cobra.Command { func newSearchHubCmd(out io.Writer) *cobra.Command {
@ -60,11 +62,18 @@ func newSearchHubCmd(out io.Writer) *cobra.Command {
f := cmd.Flags() f := cmd.Flags()
f.StringVar(&o.searchEndpoint, "endpoint", "https://hub.helm.sh", "monocular instance to query for charts") 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") f.UintVar(&o.maxColWidth, "max-col-width", 50, "maximum column width for output table")
bindOutputFlag(cmd, &o.outputFormat)
return cmd return cmd
} }
func (o *searchHubOptions) run(out io.Writer, args []string) error { 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) c, err := monocular.New(o.searchEndpoint)
if err != nil { 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) 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 { func newHubSearchWriter(results []monocular.SearchResult, endpoint string, columnWidth uint) *hubSearchWriter {
if len(res) == 0 { var elements []hubChartElement
return "No results found" 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 func (h *hubSearchWriter) WriteTable(out io.Writer) error {
// max value and we want the user to have the ability to display the whole url if len(h.elements) == 0 {
table.MaxColWidth = o.maxColWidth _, 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") table.AddRow("URL", "CHART VERSION", "APP VERSION", "DESCRIPTION")
var url string for _, r := range h.elements {
for _, r := range res { table.AddRow(r.URL, r.Version, r.AppVersion, r.Description)
url = endpoint + "/charts/" + r.ID }
table.AddRow(url, r.Relationships.LatestChartVersion.Data.Version, r.Relationships.LatestChartVersion.Data.AppVersion, r.Attributes.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
} }

@ -28,6 +28,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/cmd/helm/search" "helm.sh/helm/cmd/helm/search"
"helm.sh/helm/pkg/action"
"helm.sh/helm/pkg/helmpath" "helm.sh/helm/pkg/helmpath"
"helm.sh/helm/pkg/repo" "helm.sh/helm/pkg/repo"
) )
@ -50,6 +51,7 @@ type searchRepoOptions struct {
maxColWidth uint maxColWidth uint
repoFile string repoFile string
repoCacheDir string repoCacheDir string
outputFormat string
} }
func newSearchRepoCmd(out io.Writer) *cobra.Command { 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.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.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") f.UintVar(&o.maxColWidth, "max-col-width", 50, "maximum column width for output table")
bindOutputFlag(cmd, &o.outputFormat)
return cmd return cmd
} }
func (o *searchRepoOptions) run(out io.Writer, args []string) error { 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) index, err := o.buildIndex(out)
if err != nil { if err != nil {
return err return err
@ -98,9 +108,7 @@ func (o *searchRepoOptions) run(out io.Writer, args []string) error {
return err return err
} }
fmt.Fprintln(out, o.formatSearchResults(data)) return outfmt.Write(out, &repoSearchWriter{data, o.maxColWidth})
return nil
} }
func (o *searchRepoOptions) applyConstraint(res []*search.Result) ([]*search.Result, error) { 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 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) { func (o *searchRepoOptions) buildIndex(out io.Writer) (*search.Index, error) {
// Load the repositories.yaml // Load the repositories.yaml
rf, err := repo.LoadFile(o.repoFile) rf, err := repo.LoadFile(o.repoFile)
@ -166,3 +161,60 @@ func (o *searchRepoOptions) buildIndex(out io.Writer) (*search.Index, error) {
} }
return i, nil 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
}

@ -64,6 +64,14 @@ func TestSearchRepositoriesCmd(t *testing.T) {
name: "search for 'alp[', expect failure to compile regexp", name: "search for 'alp[', expect failure to compile regexp",
cmd: "search repo alp[ --regexp", cmd: "search repo alp[ --regexp",
wantError: true, 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 settings.Debug = true

@ -19,11 +19,11 @@ package main
import ( import (
"io" "io"
"github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/cmd/helm/require" "helm.sh/helm/cmd/helm/require"
"helm.sh/helm/pkg/action" "helm.sh/helm/pkg/action"
"helm.sh/helm/pkg/release"
) )
var statusHelp = ` var statusHelp = `
@ -46,6 +46,13 @@ func newStatusCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
Long: statusHelp, Long: statusHelp,
Args: require.ExactArgs(1), Args: require.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { 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]) rel, err := client.Run(args[0])
if err != nil { if err != nil {
return err return err
@ -54,32 +61,30 @@ func newStatusCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
// strip chart metadata from the output // strip chart metadata from the output
rel.Chart = nil rel.Chart = nil
outfmt, err := action.ParseOutputFormat(client.OutputFormat) return outfmt.Write(out, &statusPrinter{rel})
// 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)
}
}, },
} }
f := cmd.PersistentFlags() f := cmd.PersistentFlags()
f.IntVar(&client.Version, "revision", 0, "if set, display the status of the named release with revision") 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 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
}

@ -10,4 +10,3 @@
revision: 4 revision: 4
status: deployed status: deployed
updated: 1977-09-02 22:04:05 +0000 UTC updated: 1977-09-02 22:04:05 +0000 UTC

@ -0,0 +1 @@
[{"Name":"testing/mariadb","Version":"0.3.0","AppVersion":"","Description":"Chart for MariaDB"}]

@ -0,0 +1,4 @@
- AppVersion: 2.3.4
Description: Deploy a basic Alpine Linux pod
Name: testing/alpine
Version: 0.2.0

@ -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"} {"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"}

@ -69,6 +69,13 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
Long: upgradeDesc, Long: upgradeDesc,
Args: require.ExactArgs(2), Args: require.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error { 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() client.Namespace = getNamespace()
if client.Version == "" && client.Devel { if client.Version == "" && client.Devel {
@ -104,8 +111,10 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
instClient.Atomic = client.Atomic instClient.Atomic = client.Atomic
rel, err := runInstall(args, instClient, valueOpts, out) rel, err := runInstall(args, instClient, valueOpts, out)
action.PrintRelease(out, rel) if err != nil {
return err 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) 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 // Print the status like status command does
statusClient := action.NewStatus(cfg) statusClient := action.NewStatus(cfg)
@ -137,9 +148,8 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
if err != nil { if err != nil {
return err 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") f.BoolVar(&client.CleanupOnFail, "cleanup-on-fail", false, "allow deletion of new resources created in this upgrade when upgrade fails")
addChartPathOptionsFlags(f, &client.ChartPathOptions) addChartPathOptionsFlags(f, &client.ChartPathOptions)
addValueOptionsFlags(f, valueOpts) addValueOptionsFlags(f, valueOpts)
bindOutputFlag(cmd, &client.OutputFormat)
return cmd return cmd
} }

@ -82,6 +82,7 @@ type Install struct {
OutputDir string OutputDir string
Atomic bool Atomic bool
SkipCRDs bool SkipCRDs bool
OutputFormat string
} }
// ChartPathOptions captures common options used for controlling chart paths // ChartPathOptions captures common options used for controlling chart paths

@ -17,11 +17,8 @@ limitations under the License.
package action package action
import ( import (
"fmt"
"regexp" "regexp"
"github.com/gosuri/uitable"
"helm.sh/helm/pkg/release" "helm.sh/helm/pkg/release"
"helm.sh/helm/pkg/releaseutil" "helm.sh/helm/pkg/releaseutil"
) )
@ -128,6 +125,7 @@ type List struct {
Deployed bool Deployed bool
Failed bool Failed bool
Pending bool Pending bool
OutputFormat string
} }
// NewList constructs a new *List // NewList constructs a new *List
@ -278,21 +276,3 @@ func (l *List) SetStateMask() {
l.StateMask = state 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()
}

@ -19,17 +19,16 @@ package action
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"github.com/gosuri/uitable" "github.com/gosuri/uitable"
"github.com/pkg/errors"
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
) )
// OutputFormat is a type for capturing supported output formats // OutputFormat is a type for capturing supported output formats
type OutputFormat string type OutputFormat string
// TableFunc is a function that can be used to add rows to a table
type TableFunc func(tbl *uitable.Table)
const ( const (
Table OutputFormat = "table" Table OutputFormat = "table"
JSON OutputFormat = "json" JSON OutputFormat = "json"
@ -44,32 +43,18 @@ func (o OutputFormat) String() string {
return string(o) return string(o)
} }
// Marshal uses the specified output format to marshal out the given data. It // Write the output in the given format to the io.Writer. Unsupported formats
// does not support tabular output. For tabular output, use MarshalTable // will return an error
func (o OutputFormat) Marshal(data interface{}) (byt []byte, err error) { func (o OutputFormat) Write(out io.Writer, w Writer) error {
switch o { switch o {
case YAML: case Table:
byt, err = yaml.Marshal(data) return w.WriteTable(out)
case JSON: case JSON:
byt, err = json.Marshal(data) return w.WriteJSON(out)
default: case YAML:
err = ErrInvalidFormatType return w.WriteYAML(out)
}
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
} }
f(tbl) return ErrInvalidFormatType
return tbl.Bytes(), nil
} }
// ParseOutputFormat takes a raw string and returns the matching OutputFormat. // ParseOutputFormat takes a raw string and returns the matching OutputFormat.
@ -87,3 +72,54 @@ func ParseOutputFormat(s string) (out OutputFormat, err error) {
} }
return 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
}

@ -56,6 +56,7 @@ type Upgrade struct {
MaxHistory int MaxHistory int
Atomic bool Atomic bool
CleanupOnFail bool CleanupOnFail bool
OutputFormat string
} }
// NewUpgrade creates a new Upgrade object with the given configuration. // NewUpgrade creates a new Upgrade object with the given configuration.

Loading…
Cancel
Save