diff --git a/cmd/helm/install.go b/cmd/helm/install.go index 23f307564..a0fa6edfb 100644 --- a/cmd/helm/install.go +++ b/cmd/helm/install.go @@ -143,6 +143,7 @@ type installCmd struct { certFile string keyFile string caFile string + output string } type valueFiles []string @@ -226,6 +227,7 @@ func newInstallCmd(c helm.Interface, out io.Writer) *cobra.Command { f.BoolVar(&inst.depUp, "dep-up", false, "Run helm dependency update before installing the chart") f.BoolVar(&inst.subNotes, "render-subchart-notes", false, "Render subchart notes along with the parent") f.StringVar(&inst.description, "description", "", "Specify a description for the release") + f.StringVarP(&inst.output, "output", "o", "table", "Prints the output in the specified format (json|table|yaml)") // set defaults from environment settings.InitTLS(f) @@ -335,7 +337,10 @@ func (i *installCmd) run() error { if rel == nil { return nil } - i.printRelease(rel) + + if i.output == "table" { + i.printRelease(rel) + } // If this is a dry run, we can't display status. if i.dryRun { @@ -351,7 +356,15 @@ func (i *installCmd) run() error { if err != nil { return prettyError(err) } - PrintStatus(i.out, status) + + output, err := PrintStatusFormated(i.output, status) + + if err != nil { + return err + } + + fmt.Fprintf(i.out, output) + return nil } diff --git a/cmd/helm/install_test.go b/cmd/helm/install_test.go index 24a5abe68..e00c33a81 100644 --- a/cmd/helm/install_test.go +++ b/cmd/helm/install_test.go @@ -191,6 +191,22 @@ func TestInstall(t *testing.T) { flags: []string{"--name-template", "{{UPPER \"foobar\"}}"}, err: true, }, + // Install, using --output json + { + name: "install using output json", + args: []string{"testdata/testcharts/alpine"}, + flags: strings.Split("--name virgil --output json", " "), + resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "virgil"}), + expected: regexp.QuoteMeta(`{"name":"virgil","info":{"status":{"code":1},"first_deployed":{"seconds":242085845},"last_deployed":{"seconds":242085845},"Description":"Release mock"},"namespace":"default"}`), + }, + // Install, using --output yaml + { + name: "install using output yaml", + args: []string{"testdata/testcharts/alpine"}, + flags: strings.Split("--name virgil --output yaml", " "), + resp: helm.ReleaseMock(&helm.MockReleaseOptions{Name: "virgil"}), + expected: "info:\n Description: Release mock\n first_deployed:\n seconds: 242085845\n last_deployed:\n seconds: 242085845\n status:\n code: 1\nname: virgil\nnamespace: default\n", + }, } runReleaseCases(t, tests, func(c *helm.FakeClient, out io.Writer) *cobra.Command { diff --git a/cmd/helm/repo_list.go b/cmd/helm/repo_list.go index 5983bca97..633bb7a75 100644 --- a/cmd/helm/repo_list.go +++ b/cmd/helm/repo_list.go @@ -17,10 +17,12 @@ limitations under the License. package main import ( - "errors" + "encoding/json" "fmt" "io" + "strings" + "github.com/ghodss/yaml" "github.com/gosuri/uitable" "github.com/spf13/cobra" @@ -29,8 +31,18 @@ import ( ) type repoListCmd struct { - out io.Writer - home helmpath.Home + out io.Writer + home helmpath.Home + output string +} + +type repositoryElement struct { + Name string + URL string +} + +type repositories struct { + Repositories []*repositoryElement } func newRepoListCmd(out io.Writer) *cobra.Command { @@ -45,22 +57,70 @@ func newRepoListCmd(out io.Writer) *cobra.Command { }, } + f := cmd.Flags() + f.StringVarP(&list.output, "output", "o", "table", "Prints the output in the specified format (json|table|yaml)") return cmd } func (a *repoListCmd) run() error { - f, err := repo.LoadRepositoriesFile(a.home.RepositoryFile()) + repoFile, err := repo.LoadRepositoriesFile(a.home.RepositoryFile()) if err != nil { return err } - if 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) + + output, err := formatRepoListResult(a.output, repoFile) + + if err != nil { + return err } - fmt.Fprintln(a.out, table) + fmt.Fprintln(a.out, output) + return nil } + +func formatRepoListResult(format string, repoFile *repo.RepoFile) (string, error) { + var output string + var err error + + if len(repoFile.Repositories) == 0 { + err = fmt.Errorf("no repositories to show") + return output, err + } + + switch format { + case "table": + table := uitable.New() + table.AddRow("NAME", "URL") + for _, re := range repoFile.Repositories { + table.AddRow(re.Name, re.URL) + } + output = table.String() + + case "json": + output, err = printFormatedRepoFile(format, repoFile, json.Marshal) + + case "yaml": + output, err = printFormatedRepoFile(format, repoFile, yaml.Marshal) + } + + return output, err +} + +func printFormatedRepoFile(format string, repoFile *repo.RepoFile, obj func(v interface{}) ([]byte, error)) (string, error) { + var output string + var err error + var repolist repositories + + for _, re := range repoFile.Repositories { + repolist.Repositories = append(repolist.Repositories, &repositoryElement{Name: re.Name, URL: re.URL}) + } + + o, e := obj(repolist) + if e != nil { + err = fmt.Errorf("Failed to Marshal %s output: %s", strings.ToUpper(format), e) + } else { + output = string(o) + } + + return output, err +} diff --git a/cmd/helm/search.go b/cmd/helm/search.go index 99ffafbd3..92e842823 100644 --- a/cmd/helm/search.go +++ b/cmd/helm/search.go @@ -17,11 +17,13 @@ limitations under the License. package main import ( + "encoding/json" "fmt" "io" "strings" "github.com/Masterminds/semver" + "github.com/ghodss/yaml" "github.com/gosuri/uitable" "github.com/spf13/cobra" @@ -48,6 +50,18 @@ type searchCmd struct { regexp bool version string colWidth uint + output string +} + +type chartElement struct { + Name string + Version string + AppVersion string + Description string +} + +type searchResult struct { + Charts []*chartElement } func newSearchCmd(out io.Writer) *cobra.Command { @@ -68,6 +82,7 @@ func newSearchCmd(out io.Writer) *cobra.Command { f.BoolVarP(&sc.versions, "versions", "l", false, "Show the long listing, with each version of each chart on its own line") f.StringVarP(&sc.version, "version", "v", "", "Search using semantic versioning constraints") f.UintVar(&sc.colWidth, "col-width", 60, "Specifies the max column width of output") + f.StringVarP(&sc.output, "output", "o", "table", "Prints the output in the specified format (json|table|yaml)") return cmd } @@ -95,7 +110,12 @@ func (s *searchCmd) run(args []string) error { return err } - fmt.Fprintln(s.out, s.formatSearchResults(data, s.colWidth)) + o, err := s.formatSearchResults(s.output, data, s.colWidth) + if err != nil { + return err + } + + fmt.Fprintln(s.out, o) return nil } @@ -128,17 +148,53 @@ func (s *searchCmd) applyConstraint(res []*search.Result) ([]*search.Result, err return data, nil } -func (s *searchCmd) formatSearchResults(res []*search.Result, colWidth uint) string { +func (s *searchCmd) formatSearchResults(format string, res []*search.Result, colWidth uint) (string, error) { + var output string + var err error + + switch format { + case "table": + if len(res) == 0 { + return "No results found", nil + } + table := uitable.New() + table.MaxColWidth = colWidth + 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) + } + output = table.String() + + case "json": + output, err = s.printFormated(format, res, json.Marshal) + + case "yaml": + output, err = s.printFormated(format, res, yaml.Marshal) + } + + return output, err +} + +func (s *searchCmd) printFormated(format string, res []*search.Result, obj func(v interface{}) ([]byte, error)) (string, error) { + var sResult searchResult + var output string + var err error + if len(res) == 0 { - return "No results found" + return "[]", nil } - table := uitable.New() - table.MaxColWidth = colWidth - 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) + sResult.Charts = append(sResult.Charts, &chartElement{r.Name, r.Chart.Version, r.Chart.AppVersion, r.Chart.Description}) + } + + o, e := obj(sResult) + if e != nil { + err = fmt.Errorf("Failed to Marshal %s output: %s", strings.ToUpper(format), e) + } else { + output = string(o) } - return table.String() + return output, err } func (s *searchCmd) buildIndex() (*search.Index, error) { diff --git a/cmd/helm/search_test.go b/cmd/helm/search_test.go index 233f94572..35a72260e 100644 --- a/cmd/helm/search_test.go +++ b/cmd/helm/search_test.go @@ -18,6 +18,8 @@ package main import ( "io" + "regexp" + "strings" "testing" "github.com/spf13/cobra" @@ -84,6 +86,30 @@ func TestSearchCmd(t *testing.T) { flags: []string{"--regexp"}, err: true, }, + { + name: "search for 'maria', expect one match output json", + args: []string{"maria"}, + flags: strings.Split("--output json", " "), + expected: regexp.QuoteMeta(`{"Charts":[{"Name":"testing/mariadb","Version":"0.3.0","AppVersion":"","Description":"Chart for MariaDB"}]}`), + }, + { + name: "search for 'alpine', expect two matches output json", + args: []string{"alpine"}, + flags: strings.Split("--output json", " "), + expected: regexp.QuoteMeta(`{"Charts":[{"Name":"testing/alpine","Version":"0.2.0","AppVersion":"2.3.4","Description":"Deploy a basic Alpine Linux pod"}]}`), + }, + { + name: "search for 'maria', expect one match output yaml", + args: []string{"maria"}, + flags: strings.Split("--output yaml", " "), + expected: "Charts:\n- AppVersion: \"\"\n Description: Chart for MariaDB\n Name: testing/mariadb\n Version: 0.3.0\n\n", + }, + { + name: "search for 'alpine', expect two matches output yaml", + args: []string{"alpine"}, + flags: strings.Split("--output yaml", " "), + expected: "Charts:\n- AppVersion: 2.3.4\n Description: Deploy a basic Alpine Linux pod\n Name: testing/alpine\n Version: 0.2.0\n\n", + }, } cleanup := resetEnv() diff --git a/cmd/helm/status.go b/cmd/helm/status.go index dac91916b..65025c8cc 100644 --- a/cmd/helm/status.go +++ b/cmd/helm/status.go @@ -21,6 +21,7 @@ import ( "fmt" "io" "regexp" + "strings" "text/tabwriter" "github.com/ghodss/yaml" @@ -79,7 +80,7 @@ func newStatusCmd(client helm.Interface, out io.Writer) *cobra.Command { f := cmd.Flags() settings.AddFlagsTLS(f) f.Int32Var(&status.version, "revision", 0, "If set, display the status of the named release with revision") - f.StringVarP(&status.outfmt, "output", "o", "", "Output the status in the specified format (json or yaml)") + f.StringVarP(&status.outfmt, "output", "o", "table", "Prints the output in the specified format (json|table|yaml)") // set defaults from environment settings.InitTLS(f) @@ -93,56 +94,82 @@ func (s *statusCmd) run() error { return prettyError(err) } - switch s.outfmt { - case "": - PrintStatus(s.out, res) - return nil + output, err := PrintStatusFormated(s.outfmt, res) + + if err != nil { + return err + } + + fmt.Fprintf(s.out, output) + + return nil +} + +// PrintStatusFormated prints out the status of a release. Shared because also used by +// install / upgrade +func PrintStatusFormated(format string, res *services.GetReleaseStatusResponse) (string, error) { + var output string + var err error + + switch format { + case "table": + output = printStatus(res) + case "json": - data, err := json.Marshal(res) - if err != nil { - return fmt.Errorf("Failed to Marshal JSON output: %s", err) - } - s.out.Write(data) - return nil + output, err = printFormatedReleaseStatus(format, res, json.Marshal) + case "yaml": - data, err := yaml.Marshal(res) - if err != nil { - return fmt.Errorf("Failed to Marshal YAML output: %s", err) - } - s.out.Write(data) - return nil + output, err = printFormatedReleaseStatus(format, res, yaml.Marshal) + + default: + err = fmt.Errorf("Unknown output format %q", err) } - return fmt.Errorf("Unknown output format %q", s.outfmt) + return output, err } -// PrintStatus prints out the status of a release. Shared because also used by -// install / upgrade -func PrintStatus(out io.Writer, res *services.GetReleaseStatusResponse) { +func printFormatedReleaseStatus(format string, res *services.GetReleaseStatusResponse, obj func(v interface{}) ([]byte, error)) (string, error) { + var output string + var err error + + o, err := obj(res) + if err != nil { + return "", fmt.Errorf("Failed to Marshal %s output: %s", strings.ToUpper(format), err) + } + output = string(o) + + return output, err +} + +func printStatus(res *services.GetReleaseStatusResponse) string { + var out strings.Builder + if res.Info.LastDeployed != nil { - fmt.Fprintf(out, "LAST DEPLOYED: %s\n", timeconv.String(res.Info.LastDeployed)) + fmt.Fprintf(&out, "LAST DEPLOYED: %s\n", timeconv.String(res.Info.LastDeployed)) } - fmt.Fprintf(out, "NAMESPACE: %s\n", res.Namespace) - fmt.Fprintf(out, "STATUS: %s\n", res.Info.Status.Code) - fmt.Fprintf(out, "\n") + fmt.Fprintf(&out, "NAMESPACE: %s\n", res.Namespace) + fmt.Fprintf(&out, "STATUS: %s\n", res.Info.Status.Code) + fmt.Fprintf(&out, "\n") if len(res.Info.Status.Resources) > 0 { re := regexp.MustCompile(" +") - w := tabwriter.NewWriter(out, 0, 0, 2, ' ', tabwriter.TabIndent) + w := tabwriter.NewWriter(&out, 0, 0, 2, ' ', tabwriter.TabIndent) fmt.Fprintf(w, "RESOURCES:\n%s\n", re.ReplaceAllString(res.Info.Status.Resources, "\t")) w.Flush() } if res.Info.Status.LastTestSuiteRun != nil { lastRun := res.Info.Status.LastTestSuiteRun - fmt.Fprintf(out, "TEST SUITE:\n%s\n%s\n\n%s\n", + fmt.Fprintf(&out, "TEST SUITE:\n%s\n%s\n\n%s\n", fmt.Sprintf("Last Started: %s", timeconv.String(lastRun.StartedAt)), fmt.Sprintf("Last Completed: %s", timeconv.String(lastRun.CompletedAt)), formatTestResults(lastRun.Results)) } if len(res.Info.Status.Notes) > 0 { - fmt.Fprintf(out, "NOTES:\n%s\n", res.Info.Status.Notes) + fmt.Fprintf(&out, "NOTES:\n%s\n", res.Info.Status.Notes) } + + return out.String() } func formatTestResults(results []*release.TestRun) string { diff --git a/cmd/helm/upgrade.go b/cmd/helm/upgrade.go index aa4bebeef..5c5af66cc 100644 --- a/cmd/helm/upgrade.go +++ b/cmd/helm/upgrade.go @@ -117,6 +117,7 @@ type upgradeCmd struct { certFile string keyFile string caFile string + output string } func newUpgradeCmd(client helm.Interface, out io.Writer) *cobra.Command { @@ -181,6 +182,7 @@ func newUpgradeCmd(client helm.Interface, out io.Writer) *cobra.Command { f.BoolVar(&upgrade.subNotes, "render-subchart-notes", false, "Render subchart notes along with parent") f.StringVar(&upgrade.description, "description", "", "Specify the description to use for the upgrade, rather than the default") f.BoolVar(&upgrade.cleanupOnFail, "cleanup-on-fail", false, "Allow deletion of new resources created in this upgrade when upgrade failed") + f.StringVarP(&upgrade.output, "output", "o", "table", "Prints the output in the specified format (json|table|yaml)") f.MarkDeprecated("disable-hooks", "Use --no-hooks instead") @@ -307,14 +309,22 @@ func (u *upgradeCmd) run() error { printRelease(u.out, resp.Release) } - fmt.Fprintf(u.out, "Release %q has been upgraded.\n", u.release) - + if u.output == "table" { + fmt.Fprintf(u.out, "Release %q has been upgraded.\n", u.release) + } // Print the status like status command does status, err := u.client.ReleaseStatus(u.release) if err != nil { return prettyError(err) } - PrintStatus(u.out, status) + + output, err := PrintStatusFormated(u.output, status) + + if err != nil { + return err + } + + fmt.Fprintf(u.out, output) return nil } diff --git a/docs/helm/helm_install.md b/docs/helm/helm_install.md index d988dfcdd..dc0591f6a 100644 --- a/docs/helm/helm_install.md +++ b/docs/helm/helm_install.md @@ -93,6 +93,7 @@ helm install [CHART] [flags] --namespace string Namespace to install the release into. Defaults to the current kube config namespace. --no-crd-hook Prevent CRD hooks from running, but run other hooks --no-hooks Prevent hooks from running during install + -o, --output string Prints the output in the specified format (json|table|yaml) (default "table") --password string Chart repository password where to locate the requested chart --render-subchart-notes Render subchart notes along with the parent --replace Re-use the given name, even if that name is already used. This is unsafe in production @@ -130,4 +131,4 @@ helm install [CHART] [flags] * [helm](helm.md) - The Helm package manager for Kubernetes. -###### Auto generated by spf13/cobra on 16-May-2019 +###### Auto generated by spf13/cobra on 24-Sep-2019 diff --git a/docs/helm/helm_repo_list.md b/docs/helm/helm_repo_list.md index 9a544a6ba..5f13c8c2d 100644 --- a/docs/helm/helm_repo_list.md +++ b/docs/helm/helm_repo_list.md @@ -13,7 +13,8 @@ helm repo list [flags] ### Options ``` - -h, --help help for list + -h, --help help for list + -o, --output string Prints the output in the specified format (json|table|yaml) (default "table") ``` ### Options inherited from parent commands @@ -32,4 +33,4 @@ helm repo list [flags] * [helm repo](helm_repo.md) - Add, list, remove, update, and index chart repositories -###### Auto generated by spf13/cobra on 16-May-2019 +###### Auto generated by spf13/cobra on 24-Sep-2019 diff --git a/docs/helm/helm_search.md b/docs/helm/helm_search.md index b1a89c4f9..e040b738f 100644 --- a/docs/helm/helm_search.md +++ b/docs/helm/helm_search.md @@ -20,6 +20,7 @@ helm search [keyword] [flags] ``` --col-width uint Specifies the max column width of output (default 60) -h, --help help for search + -o, --output string Prints the output in the specified format (json|table|yaml) (default "table") -r, --regexp Use regular expressions for searching -v, --version string Search using semantic versioning constraints -l, --versions Show the long listing, with each version of each chart on its own line @@ -41,4 +42,4 @@ helm search [keyword] [flags] * [helm](helm.md) - The Helm package manager for Kubernetes. -###### Auto generated by spf13/cobra on 16-May-2019 +###### Auto generated by spf13/cobra on 24-Sep-2019 diff --git a/docs/helm/helm_status.md b/docs/helm/helm_status.md index 38e774b8f..2c75ce21a 100644 --- a/docs/helm/helm_status.md +++ b/docs/helm/helm_status.md @@ -23,7 +23,7 @@ helm status [flags] RELEASE_NAME ``` -h, --help help for status - -o, --output string Output the status in the specified format (json or yaml) + -o, --output string Prints the output in the specified format (json|table|yaml) (default "table") --revision int32 If set, display the status of the named release with revision --tls Enable TLS for request --tls-ca-cert string Path to TLS CA certificate file (default "$HELM_HOME/ca.pem") @@ -49,4 +49,4 @@ helm status [flags] RELEASE_NAME * [helm](helm.md) - The Helm package manager for Kubernetes. -###### Auto generated by spf13/cobra on 16-May-2019 +###### Auto generated by spf13/cobra on 6-Sep-2019 diff --git a/docs/helm/helm_upgrade.md b/docs/helm/helm_upgrade.md index 0aa52565b..effb505f9 100644 --- a/docs/helm/helm_upgrade.md +++ b/docs/helm/helm_upgrade.md @@ -79,6 +79,7 @@ helm upgrade [RELEASE] [CHART] [flags] --keyring string Path to the keyring that contains public signing keys (default "~/.gnupg/pubring.gpg") --namespace string Namespace to install the release into (only used if --install is set). Defaults to the current kube config namespace --no-hooks Disable pre/post upgrade hooks + -o, --output string Prints the output in the specified format (json|table|yaml) (default "table") --password string Chart repository password where to locate the requested chart --recreate-pods Performs pods restart for the resource if applicable --render-subchart-notes Render subchart notes along with parent @@ -118,4 +119,4 @@ helm upgrade [RELEASE] [CHART] [flags] * [helm](helm.md) - The Helm package manager for Kubernetes. -###### Auto generated by spf13/cobra on 16-May-2019 +###### Auto generated by spf13/cobra on 24-Sep-2019