From 9c21d4635113ee30dd6063e6f7c2b4814aa53577 Mon Sep 17 00:00:00 2001 From: Gerard Nguyen Date: Thu, 21 May 2026 16:36:17 +1000 Subject: [PATCH 1/9] feat(cli): add global --output persistent flag Introduce a global --output (-o) persistent flag on the root command that all subcommands inherit, replacing ~78 local --output flag registrations. Also adds support for a new REPLICATED_OUTPUT environment variable for setting a default output format. Key changes: - Added resolveOutputFormat() with resolution order: explicit flag, REPLICATED_OUTPUT env var, then 'table' default - Migrated version command from --json bool to global --output - Refactored 17 commands that used local outputFormat variables - Removed redundant local --output flag registrations from ~50 commands - Added unit tests for output format resolution - Cleaned up runnerArgs by removing clusterAddonCreateObjectStoreOutput --- cli/cmd/app_create.go | 10 +- cli/cmd/app_hostname_ls.go | 16 +- cli/cmd/app_ls.go | 12 +- cli/cmd/app_rm.go | 11 +- cli/cmd/channel_create.go | 2 +- cli/cmd/channel_inspect.go | 2 +- cli/cmd/channel_ls.go | 2 +- cli/cmd/channel_releases.go | 2 +- cli/cmd/cluster_addon_create_objectstore.go | 3 +- cli/cmd/cluster_addon_ls.go | 21 +- cli/cmd/cluster_create.go | 2 - cli/cmd/cluster_ls.go | 1 - cli/cmd/cluster_nodegroup_ls.go | 1 - cli/cmd/cluster_port_expose.go | 1 - cli/cmd/cluster_port_ls.go | 1 - cli/cmd/cluster_port_rm.go | 1 - cli/cmd/cluster_update_nodegroup.go | 1 - cli/cmd/cluster_update_ttl.go | 1 - cli/cmd/cluster_upgrade.go | 1 - cli/cmd/cluster_versions.go | 1 - cli/cmd/collection_addmodel.go | 1 - cli/cmd/collection_create.go | 1 - cli/cmd/collection_ls.go | 1 - cli/cmd/collection_removemodel.go | 1 - cli/cmd/collection_rm.go | 1 - cli/cmd/customer_create.go | 9 +- cli/cmd/customer_inspect.go | 10 +- cli/cmd/customer_ls.go | 18 +- cli/cmd/customer_update.go | 1 - cli/cmd/default_set.go | 9 +- cli/cmd/default_show.go | 11 +- cli/cmd/enterprise_portal_invite.go | 6 +- cli/cmd/enterprise_portal_user_ls.go | 6 +- cli/cmd/enterpriseportal_status_get.go | 7 +- cli/cmd/enterpriseportal_status_update.go | 6 +- cli/cmd/installer_ls.go | 2 +- cli/cmd/instance_inspect.go | 2 +- cli/cmd/instance_ls.go | 2 +- cli/cmd/instance_tag.go | 2 +- cli/cmd/model_ls.go | 1 - cli/cmd/model_rm.go | 1 - cli/cmd/network_create.go | 1 - cli/cmd/network_ls.go | 1 - cli/cmd/notification.go | 60 ++-- cli/cmd/output_test.go | 41 +++ cli/cmd/policy_create.go | 8 +- cli/cmd/policy_get.go | 12 +- cli/cmd/policy_ls.go | 9 +- cli/cmd/policy_update.go | 8 +- cli/cmd/registry_add_dockerhub.go | 1 - cli/cmd/registry_add_ecr.go | 1 - cli/cmd/registry_add_gar.go | 1 - cli/cmd/registry_add_gcr.go | 1 - cli/cmd/registry_add_ghcr.go | 1 - cli/cmd/registry_add_other.go | 1 - cli/cmd/registry_add_quay.go | 1 - cli/cmd/registry_ls.go | 1 - cli/cmd/release_create.go | 3 +- cli/cmd/release_inspect.go | 1 - cli/cmd/release_lint.go | 3 +- cli/cmd/release_ls.go | 2 +- cli/cmd/root.go | 10 +- cli/cmd/runner.go | 11 +- cli/cmd/version.go | 8 +- cli/cmd/vm_create.go | 1 - cli/cmd/vm_ls.go | 1 - cli/cmd/vm_port_expose.go | 1 - cli/cmd/vm_port_ls.go | 1 - cli/cmd/vm_port_rm.go | 1 - cli/cmd/vm_update_ttl.go | 1 - cli/cmd/vm_versions.go | 1 - docs/plans/global-output-flag.md | 348 ++++++++++++++++++++ 72 files changed, 508 insertions(+), 223 deletions(-) create mode 100644 cli/cmd/output_test.go create mode 100644 docs/plans/global-output-flag.md diff --git a/cli/cmd/app_create.go b/cli/cmd/app_create.go index 9e1f25e3c..a35ad8a6f 100644 --- a/cli/cmd/app_create.go +++ b/cli/cmd/app_create.go @@ -16,8 +16,6 @@ type createAppOpts struct { func (r *runners) InitAppCreate(parent *cobra.Command) *cobra.Command { opts := createAppOpts{} - var outputFormat string - cmd := &cobra.Command{ Use: "create NAME", Short: "Create a new application", @@ -44,17 +42,15 @@ replicated app create "Custom App" --output table`, return errors.New("missing app name") } opts.name = args[0] - return r.createApp(ctx, cmd, opts, outputFormat) + return r.createApp(ctx, cmd, opts) }, SilenceUsage: true, } parent.AddCommand(cmd) - cmd.Flags().StringVarP(&outputFormat, "output", "o", "table", "The output format to use. One of: json|table") - return cmd } -func (r *runners) createApp(ctx context.Context, cmd *cobra.Command, opts createAppOpts, outputFormat string) error { +func (r *runners) createApp(ctx context.Context, cmd *cobra.Command, opts createAppOpts) error { kotsRestClient := kotsclient.VendorV3Client{ HTTPClient: *r.platformAPI, } @@ -75,5 +71,5 @@ func (r *runners) createApp(ctx context.Context, cmd *cobra.Command, opts create }, } - return print.Apps(outputFormat, r.w, apps) + return print.Apps(r.outputFormat, r.w, apps) } diff --git a/cli/cmd/app_hostname_ls.go b/cli/cmd/app_hostname_ls.go index bd881d0c2..53b1a76d9 100644 --- a/cli/cmd/app_hostname_ls.go +++ b/cli/cmd/app_hostname_ls.go @@ -14,8 +14,6 @@ import ( ) func (r *runners) InitAppHostnameListCommand(parent *cobra.Command) *cobra.Command { - var outputFormat string - cmd := &cobra.Command{ Use: "ls", Aliases: []string{"list"}, @@ -65,20 +63,18 @@ replicated app hostname ls --app myapp --output json`, }, RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - return r.listAppHostnames(ctx, outputFormat) + return r.listAppHostnames(ctx) }, SilenceUsage: true, } parent.AddCommand(cmd) - cmd.Flags().StringVarP(&outputFormat, "output", "o", "table", "The output format to use. One of: json|table") - return cmd } -func (r *runners) listAppHostnames(ctx context.Context, outputFormat string) error { +func (r *runners) listAppHostnames(ctx context.Context) error { // Only show spinners for table output - showSpinners := outputFormat == "table" + showSpinners := r.outputFormat == "table" log := logger.NewLogger(r.w).SetIsTerminal(r.stdoutIsTTY) // Resolve app ID from slug or ID @@ -141,7 +137,7 @@ func (r *runners) listAppHostnames(ctx context.Context, outputFormat string) err hostnameStrings := extractHostnameStrings(mergedHostnames) // Output based on format - if outputFormat == "json" { + if r.outputFormat == "json" { jsonBytes, err := json.MarshalIndent(hostnameStrings, "", " ") if err != nil { return errors.Wrap(err, "marshal json") @@ -153,11 +149,11 @@ func (r *runners) listAppHostnames(ctx context.Context, outputFormat string) err return nil } - if outputFormat == "table" { + if r.outputFormat == "table" { return printHostnamesTable(r.w, hostnameStrings) } - return errors.Errorf("unsupported output format: %s", outputFormat) + return errors.Errorf("unsupported output format: %s", r.outputFormat) } // extractHostnameStrings extracts just the hostname strings from the merged hostnames diff --git a/cli/cmd/app_ls.go b/cli/cmd/app_ls.go index 36726a4b5..ac7fc40e1 100644 --- a/cli/cmd/app_ls.go +++ b/cli/cmd/app_ls.go @@ -11,8 +11,6 @@ import ( ) func (r *runners) InitAppList(parent *cobra.Command) *cobra.Command { - var outputFormat string - cmd := &cobra.Command{ Use: "ls [NAME]", Aliases: []string{"list"}, @@ -39,24 +37,22 @@ replicated app ls --output json replicated app ls "App Name" --output table`, RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - return r.listApps(ctx, cmd, args, outputFormat) + return r.listApps(ctx, cmd, args) }, SilenceUsage: true, } parent.AddCommand(cmd) - cmd.Flags().StringVarP(&outputFormat, "output", "o", "table", "The output format to use. One of: json|table") - return cmd } -func (r *runners) listApps(ctx context.Context, cmd *cobra.Command, args []string, outputFormat string) error { +func (r *runners) listApps(ctx context.Context, cmd *cobra.Command, args []string) error { kotsApps, err := r.kotsAPI.ListApps(ctx, false) if err != nil { return errors.Wrap(err, "list apps") } if len(args) == 0 { - return print.Apps(outputFormat, r.w, kotsApps) + return print.Apps(r.outputFormat, r.w, kotsApps) } appSearch := args[0] @@ -66,5 +62,5 @@ func (r *runners) listApps(ctx context.Context, cmd *cobra.Command, args []strin resultApps = append(resultApps, app) } } - return print.Apps(outputFormat, r.w, resultApps) + return print.Apps(r.outputFormat, r.w, resultApps) } diff --git a/cli/cmd/app_rm.go b/cli/cmd/app_rm.go index 219b58be8..bc90808bc 100644 --- a/cli/cmd/app_rm.go +++ b/cli/cmd/app_rm.go @@ -17,8 +17,6 @@ type deleteAppOpts struct { func (r *runners) InitAppRm(parent *cobra.Command) *cobra.Command { opts := deleteAppOpts{} - var outputFormat string - cmd := &cobra.Command{ Use: "rm NAME", Aliases: []string{"delete"}, @@ -42,20 +40,19 @@ replicated app delete "Custom App" --output json`, if len(args) != 1 { return errors.New("missing app slug or id") } - return r.deleteApp(ctx, cmd, args[0], opts, outputFormat) + return r.deleteApp(ctx, cmd, args[0], opts) }, SilenceUsage: true, } parent.AddCommand(cmd) cmd.Flags().BoolVarP(&opts.force, "force", "f", false, "Skip confirmation prompt. There is no undo for this action.") - cmd.Flags().StringVarP(&outputFormat, "output", "o", "table", "The output format to use. One of: json|table") return cmd } -func (r *runners) deleteApp(ctx context.Context, cmd *cobra.Command, appName string, opts deleteAppOpts, outputFormat string) error { +func (r *runners) deleteApp(ctx context.Context, cmd *cobra.Command, appName string, opts deleteAppOpts) error { log := logger.NewLogger(r.w).SetIsTerminal(r.stdoutIsTTY) - showSpinners := outputFormat == "table" + showSpinners := r.outputFormat == "table" if showSpinners { log.ActionWithSpinner("Fetching App") @@ -77,7 +74,7 @@ func (r *runners) deleteApp(ctx context.Context, cmd *cobra.Command, appName str }, } - err = print.Apps(outputFormat, r.w, apps) + err = print.Apps(r.outputFormat, r.w, apps) if err != nil { return errors.Wrap(err, "print app") } diff --git a/cli/cmd/channel_create.go b/cli/cmd/channel_create.go index da65f7611..ee3beeead 100644 --- a/cli/cmd/channel_create.go +++ b/cli/cmd/channel_create.go @@ -17,7 +17,7 @@ func (r *runners) InitChannelCreate(parent *cobra.Command) { cmd.Flags().StringVar(&r.args.channelCreateName, "name", "", "The name of this channel") cmd.Flags().StringVar(&r.args.channelCreateDescription, "description", "", "A longer description of this channel") - cmd.Flags().StringVarP(&r.outputFormat, "output", "o", "table", "The output format to use. One of: json|table") + cmd.RunE = r.channelCreate } diff --git a/cli/cmd/channel_inspect.go b/cli/cmd/channel_inspect.go index 24348c4f1..ea9968b49 100644 --- a/cli/cmd/channel_inspect.go +++ b/cli/cmd/channel_inspect.go @@ -15,7 +15,7 @@ func (r *runners) InitChannelInspect(parent *cobra.Command) { Long: "Show full details for a channel", } parent.AddCommand(cmd) - cmd.Flags().StringVarP(&r.outputFormat, "output", "o", "table", "The output format to use. One of: json|table") + cmd.RunE = r.channelInspect } diff --git a/cli/cmd/channel_ls.go b/cli/cmd/channel_ls.go index 2144c6a7a..c3e811227 100644 --- a/cli/cmd/channel_ls.go +++ b/cli/cmd/channel_ls.go @@ -15,7 +15,7 @@ func (r *runners) InitChannelList(parent *cobra.Command) { } parent.AddCommand(cmd) - cmd.Flags().StringVarP(&r.outputFormat, "output", "o", "table", "The output format to use. One of: json|table") + cmd.RunE = r.channelList } diff --git a/cli/cmd/channel_releases.go b/cli/cmd/channel_releases.go index 5acb51bb0..0064ef4a2 100644 --- a/cli/cmd/channel_releases.go +++ b/cli/cmd/channel_releases.go @@ -25,7 +25,7 @@ replicated channel releases Stable --output json replicated channel releases Stable --page 1 --page-size 50`, } parent.AddCommand(cmd) - cmd.Flags().StringVarP(&r.outputFormat, "output", "o", "table", "The output format to use. One of: json|table") + cmd.Flags().IntVar(&r.args.channelReleasesPage, "page", 0, "The page to fetch (KOTS apps only).") cmd.Flags().IntVar(&r.args.channelReleasesPageSize, "page-size", 0, "The number of releases per page (KOTS apps only).") diff --git a/cli/cmd/cluster_addon_create_objectstore.go b/cli/cmd/cluster_addon_create_objectstore.go index b11e4aaeb..03c134e4d 100644 --- a/cli/cmd/cluster_addon_create_objectstore.go +++ b/cli/cmd/cluster_addon_create_objectstore.go @@ -61,7 +61,6 @@ func (r *runners) clusterAddonCreateObjectStoreFlags(cmd *cobra.Command) error { } cmd.Flags().DurationVar(&r.args.clusterAddonCreateObjectStoreDuration, "wait", 0, "Wait duration for add-on to be ready before exiting (leave empty to not wait)") cmd.Flags().BoolVar(&r.args.clusterAddonCreateObjectStoreDryRun, "dry-run", false, "Simulate creation to verify that your inputs are valid without actually creating an add-on") - cmd.Flags().StringVarP(&r.args.clusterAddonCreateObjectStoreOutput, "output", "o", "table", "The output format to use. One of: json|table|wide") return nil } @@ -86,7 +85,7 @@ func (r *runners) clusterAddonCreateObjectStoreCreateRun() error { return err } - return print.Addon(r.args.clusterAddonCreateObjectStoreOutput, r.w, addon) + return print.Addon(r.outputFormat, r.w, addon) } func (r *runners) createAndWaitForClusterAddonCreateObjectStore(opts kotsclient.CreateClusterAddonObjectStoreOpts, waitDuration time.Duration) (*types.ClusterAddon, error) { diff --git a/cli/cmd/cluster_addon_ls.go b/cli/cmd/cluster_addon_ls.go index 0c76b65cd..579fe6bc4 100644 --- a/cli/cmd/cluster_addon_ls.go +++ b/cli/cmd/cluster_addon_ls.go @@ -6,8 +6,7 @@ import ( ) type clusterAddonLsArgs struct { - clusterID string - outputFormat string + clusterID string } func (r *runners) InitClusterAddonLs(parent *cobra.Command) *cobra.Command { @@ -31,30 +30,20 @@ replicated cluster addon ls CLUSTER_ID_OR_NAME --output wide`, Args: cobra.ExactArgs(1), RunE: func(_ *cobra.Command, cmdArgs []string) error { args.clusterID = cmdArgs[0] - return r.addonClusterLsRun(args) + return r.addonClusterLsRun(args.clusterID) }, ValidArgsFunction: r.completeClusterIDs, } parent.AddCommand(cmd) - err := clusterAddonLsFlags(cmd, &args) - if err != nil { - panic(err) - } - return cmd } -func clusterAddonLsFlags(cmd *cobra.Command, args *clusterAddonLsArgs) error { - cmd.Flags().StringVarP(&args.outputFormat, "output", "o", "table", "The output format to use. One of: json|table|wide") - return nil -} - -func (r *runners) addonClusterLsRun(args clusterAddonLsArgs) error { - addons, err := r.kotsAPI.ListClusterAddons(args.clusterID) +func (r *runners) addonClusterLsRun(clusterID string) error { + addons, err := r.kotsAPI.ListClusterAddons(clusterID) if err != nil { return err } - return print.Addons(args.outputFormat, r.w, addons, true) + return print.Addons(r.outputFormat, r.w, addons, true) } diff --git a/cli/cmd/cluster_create.go b/cli/cmd/cluster_create.go index a32269afe..dd345a011 100644 --- a/cli/cmd/cluster_create.go +++ b/cli/cmd/cluster_create.go @@ -89,7 +89,6 @@ replicated cluster create --distribution eks --version 1.21 --nodes 3 --addon ob cmd.Flags().BoolVar(&r.args.createClusterDryRun, "dry-run", false, "Dry run") - cmd.Flags().StringVarP(&r.outputFormat, "output", "o", "table", "The output format to use. One of: json|table|wide") _ = cmd.MarkFlagRequired("distribution") @@ -205,7 +204,6 @@ func (r *runners) createAndWaitForAddons(clusterID string) error { r.args.clusterAddonCreateObjectStoreClusterID = clusterID r.args.clusterAddonCreateObjectStoreDryRun = r.args.createClusterDryRun r.args.clusterAddonCreateObjectStoreDuration = r.args.createClusterWaitDuration - r.args.clusterAddonCreateObjectStoreOutput = r.outputFormat err := r.clusterAddonCreateObjectStoreCreateRun() if err != nil { diff --git a/cli/cmd/cluster_ls.go b/cli/cmd/cluster_ls.go index 1ebc131c4..f2c463743 100644 --- a/cli/cmd/cluster_ls.go +++ b/cli/cmd/cluster_ls.go @@ -46,7 +46,6 @@ replicated cluster ls --output wide`, cmd.Flags().BoolVar(&r.args.lsClusterShowTerminated, "show-terminated", false, "when set, only show terminated clusters") cmd.Flags().StringVar(&r.args.lsClusterStartTime, "start-time", "", "start time for the query (Format: 2006-01-02T15:04:05Z)") cmd.Flags().StringVar(&r.args.lsClusterEndTime, "end-time", "", "end time for the query (Format: 2006-01-02T15:04:05Z)") - cmd.Flags().StringVarP(&r.outputFormat, "output", "o", "table", "The output format to use. One of: json|table|wide") cmd.Flags().BoolVarP(&r.args.lsClusterWatch, "watch", "w", false, "watch clusters") return cmd diff --git a/cli/cmd/cluster_nodegroup_ls.go b/cli/cmd/cluster_nodegroup_ls.go index 2864dad80..05bbfbed2 100644 --- a/cli/cmd/cluster_nodegroup_ls.go +++ b/cli/cmd/cluster_nodegroup_ls.go @@ -31,7 +31,6 @@ replicated cluster nodegroup ls CLUSTER_ID_OR_NAME --output wide`, } parent.AddCommand(cmd) - cmd.Flags().StringVarP(&r.outputFormat, "output", "o", "table", "The output format to use. One of: json|table|wide") return cmd } diff --git a/cli/cmd/cluster_port_expose.go b/cli/cmd/cluster_port_expose.go index 199b9b882..9d0edc089 100644 --- a/cli/cmd/cluster_port_expose.go +++ b/cli/cmd/cluster_port_expose.go @@ -41,7 +41,6 @@ replicated cluster port expose CLUSTER_ID_OR_NAME --port 8080 --protocol https - } cmd.Flags().StringSliceVar(&r.args.clusterExposePortProtocols, "protocol", []string{"http", "https"}, `Protocol to expose (valid values are "http", "https", "ws" and "wss")`) cmd.Flags().BoolVar(&r.args.clusterExposePortIsWildcard, "wildcard", false, "Create a wildcard DNS entry and TLS certificate for this port") - cmd.Flags().StringVarP(&r.outputFormat, "output", "o", "table", "The output format to use. One of: json|table|wide") return cmd } diff --git a/cli/cmd/cluster_port_ls.go b/cli/cmd/cluster_port_ls.go index 80c7b2026..1d9d70ead 100644 --- a/cli/cmd/cluster_port_ls.go +++ b/cli/cmd/cluster_port_ls.go @@ -27,7 +27,6 @@ replicated cluster port ls CLUSTER_ID_OR_NAME --output wide`, } parent.AddCommand(cmd) - cmd.Flags().StringVarP(&r.outputFormat, "output", "o", "table", "The output format to use. One of: json|table|wide") return cmd } diff --git a/cli/cmd/cluster_port_rm.go b/cli/cmd/cluster_port_rm.go index e419e7813..e8456819e 100644 --- a/cli/cmd/cluster_port_rm.go +++ b/cli/cmd/cluster_port_rm.go @@ -31,7 +31,6 @@ replicated cluster port rm CLUSTER_ID_OR_NAME --id PORT_ID --output json`, parent.AddCommand(cmd) cmd.Flags().StringVar(&r.args.clusterPortRemoveAddonID, "id", "", "ID of the port to remove (required)") - cmd.Flags().StringVarP(&r.outputFormat, "output", "o", "table", "The output format to use. One of: json|table|wide") // Deprecated flags cmd.Flags().IntVar(&r.args.clusterPortRemovePort, "port", 0, "Port to remove") diff --git a/cli/cmd/cluster_update_nodegroup.go b/cli/cmd/cluster_update_nodegroup.go index 4db002104..70060962f 100644 --- a/cli/cmd/cluster_update_nodegroup.go +++ b/cli/cmd/cluster_update_nodegroup.go @@ -34,7 +34,6 @@ replicated cluster update nodegroup CLUSTER_ID_OR_NAME --nodegroup-id NODEGROUP_ cmd.Flags().StringVar(&r.args.updateClusterNodeGroupMinCount, "min-nodes", "", "The minimum number of nodes in the nodegroup") cmd.Flags().StringVar(&r.args.updateClusterNodeGroupMaxCount, "max-nodes", "", "The maximum number of nodes in the nodegroup") - cmd.Flags().StringVarP(&r.outputFormat, "output", "o", "table", "The output format to use. One of: json|table|wide") return cmd } diff --git a/cli/cmd/cluster_update_ttl.go b/cli/cmd/cluster_update_ttl.go index d16c6aa9f..8479e51d4 100644 --- a/cli/cmd/cluster_update_ttl.go +++ b/cli/cmd/cluster_update_ttl.go @@ -22,7 +22,6 @@ replicated cluster update ttl CLUSTER_ID_OR_NAME --ttl 24h`, parent.AddCommand(cmd) cmd.Flags().StringVar(&r.args.updateClusterTTL, "ttl", "", "Update TTL which starts from the moment the cluster is running (duration, max 48h).") - cmd.Flags().StringVarP(&r.outputFormat, "output", "o", "table", "The output format to use. One of: json|table") cmd.MarkFlagRequired("ttl") diff --git a/cli/cmd/cluster_upgrade.go b/cli/cmd/cluster_upgrade.go index 2ab8bb826..ddb34484c 100644 --- a/cli/cmd/cluster_upgrade.go +++ b/cli/cmd/cluster_upgrade.go @@ -36,7 +36,6 @@ replicated cluster upgrade CLUSTER_ID_OR_NAME --version 1.31 --wait 30m`, cmd.Flags().BoolVar(&r.args.upgradeClusterDryRun, "dry-run", false, "Dry run") cmd.Flags().DurationVar(&r.args.upgradeClusterWaitDuration, "wait", 0, "Wait duration for cluster to be ready (leave empty to not wait)") - cmd.Flags().StringVarP(&r.outputFormat, "output", "o", "table", "The output format to use. One of: json|table|wide") _ = cmd.MarkFlagRequired("version") diff --git a/cli/cmd/cluster_versions.go b/cli/cmd/cluster_versions.go index d19a44ad1..06122fe3e 100644 --- a/cli/cmd/cluster_versions.go +++ b/cli/cmd/cluster_versions.go @@ -26,7 +26,6 @@ replicated cluster versions --output json`, parent.AddCommand(cmd) cmd.Flags().StringVar(&r.args.lsVersionsDistribution, "distribution", "", "Kubernetes distribution to filter by.") - cmd.Flags().StringVarP(&r.outputFormat, "output", "o", "table", "The output format to use. One of: json|table") return cmd } diff --git a/cli/cmd/collection_addmodel.go b/cli/cmd/collection_addmodel.go index e2fdda651..280093dbd 100644 --- a/cli/cmd/collection_addmodel.go +++ b/cli/cmd/collection_addmodel.go @@ -22,7 +22,6 @@ func (r *runners) InitCollectionAddModel(parent *cobra.Command) *cobra.Command { cmd.Flags().StringVar(&r.args.modelCollectionAddModelCollectionID, "collection-id", "", "The ID of the collection") cmd.MarkFlagRequired("collection-id") - cmd.Flags().StringVarP(&r.outputFormat, "output", "o", "table", "The output format to use. One of: json|table") return cmd } diff --git a/cli/cmd/collection_create.go b/cli/cmd/collection_create.go index 143b7aa70..df6f889fe 100644 --- a/cli/cmd/collection_create.go +++ b/cli/cmd/collection_create.go @@ -21,7 +21,6 @@ func (r *runners) InitCollectionCreate(parent *cobra.Command) *cobra.Command { cmd.Flags().StringVar(&r.args.modelCollectionCreateName, "name", "", "The name of the collection") cmd.MarkFlagRequired("name") - cmd.Flags().StringVarP(&r.outputFormat, "output", "o", "table", "The output format to use. One of: json|table") return cmd } diff --git a/cli/cmd/collection_ls.go b/cli/cmd/collection_ls.go index 2513ca629..ae0839e7c 100644 --- a/cli/cmd/collection_ls.go +++ b/cli/cmd/collection_ls.go @@ -16,7 +16,6 @@ func (r *runners) InitCollectionList(parent *cobra.Command) *cobra.Command { SilenceUsage: true, } parent.AddCommand(cmd) - cmd.Flags().StringVarP(&r.outputFormat, "output", "o", "table", "The output format to use. One of: json|table") return cmd } diff --git a/cli/cmd/collection_removemodel.go b/cli/cmd/collection_removemodel.go index f926a2895..75be42e14 100644 --- a/cli/cmd/collection_removemodel.go +++ b/cli/cmd/collection_removemodel.go @@ -22,7 +22,6 @@ func (r *runners) InitCollectionRemoveModel(parent *cobra.Command) *cobra.Comman cmd.Flags().StringVar(&r.args.modelCollectionRmModelCollectionID, "collection-id", "", "The ID of the collection") cmd.MarkFlagRequired("collection-id") - cmd.Flags().StringVarP(&r.outputFormat, "output", "o", "table", "The output format to use. One of: json|table") return cmd } diff --git a/cli/cmd/collection_rm.go b/cli/cmd/collection_rm.go index 8aab3eb1a..0952cdfb9 100644 --- a/cli/cmd/collection_rm.go +++ b/cli/cmd/collection_rm.go @@ -18,7 +18,6 @@ func (r *runners) InitCollectionRemove(parent *cobra.Command) *cobra.Command { } parent.AddCommand(cmd) - cmd.Flags().StringVarP(&r.outputFormat, "output", "o", "table", "The output format to use. One of: json|table") return cmd } diff --git a/cli/cmd/customer_create.go b/cli/cmd/customer_create.go index 29ecb58a6..1f84c8381 100644 --- a/cli/cmd/customer_create.go +++ b/cli/cmd/customer_create.go @@ -36,8 +36,6 @@ type createCustomerOpts struct { func (r *runners) InitCustomersCreateCommand(parent *cobra.Command) *cobra.Command { opts := createCustomerOpts{} - var outputFormat string - cmd := &cobra.Command{ Use: "create", Short: "Create a new customer for the current application", @@ -63,7 +61,7 @@ replicated customer create --app myapp --name "Full Options Inc" --custom-id "FU --airgap --snapshot --kots-install --embedded-cluster-download \ --support-bundle-upload --ensure-channel`, RunE: func(cmd *cobra.Command, args []string) error { - return r.createCustomer(cmd, opts, outputFormat) + return r.createCustomer(cmd, opts) }, SilenceUsage: false, SilenceErrors: false, @@ -91,14 +89,13 @@ replicated customer create --app myapp --name "Full Options Inc" --custom-id "FU cmd.Flags().BoolVar(&opts.IsDeveloperModeEnabled, "developer-mode", false, "If set, Replicated SDK installed in dev mode will use mock data.") cmd.Flags().StringVar(&opts.Email, "email", "", "Email address of the customer that is to be created.") cmd.Flags().StringVar(&opts.CustomerType, "type", "dev", "The license type to create. One of: dev|trial|paid|community|test (default: dev)") - cmd.Flags().StringVarP(&outputFormat, "output", "o", "table", "The output format to use. One of: json|table") cmd.MarkFlagRequired("channel") return cmd } -func (r *runners) createCustomer(cmd *cobra.Command, opts createCustomerOpts, outputFormat string) (err error) { +func (r *runners) createCustomer(cmd *cobra.Command, opts createCustomerOpts) (err error) { defer func() { printIfError(cmd, err) }() @@ -176,7 +173,7 @@ func (r *runners) createCustomer(cmd *cobra.Command, opts createCustomerOpts, ou return errors.Wrap(err, "create customer") } - err = print.Customer(outputFormat, r.w, customer) + err = print.Customer(r.outputFormat, r.w, customer) if err != nil { return errors.Wrap(err, "print customer") } diff --git a/cli/cmd/customer_inspect.go b/cli/cmd/customer_inspect.go index 03693e866..e15c4a9b8 100644 --- a/cli/cmd/customer_inspect.go +++ b/cli/cmd/customer_inspect.go @@ -13,8 +13,7 @@ import ( func (r *runners) InitCustomersInspectCommand(parent *cobra.Command) *cobra.Command { var ( - customer string - outputFormat string + customer string ) cmd := &cobra.Command{ Use: "inspect [flags]", @@ -38,20 +37,19 @@ replicated customer inspect --customer cus_abcdef123456 --output json # Inspect a customer for a specific app (if you have multiple apps) replicated customer inspect --app myapp --customer "Acme Inc"`, RunE: func(cmd *cobra.Command, args []string) error { - return r.inspectCustomer(cmd, customer, outputFormat) + return r.inspectCustomer(cmd, customer) }, SilenceUsage: true, } parent.AddCommand(cmd) cmd.Flags().StringVar(&customer, "customer", "", "The Customer Name or ID") - cmd.Flags().StringVarP(&outputFormat, "output", "o", "table", "The output format to use. One of: json|table") cmd.MarkFlagRequired("customer") return cmd } -func (r *runners) inspectCustomer(cmd *cobra.Command, customer string, outputFormat string) error { +func (r *runners) inspectCustomer(cmd *cobra.Command, customer string) error { if !r.hasApp() { return errors.New("no app specified") } @@ -75,7 +73,7 @@ func (r *runners) inspectCustomer(cmd *cobra.Command, customer string, outputFor return errors.Wrapf(err, "get registry hostname for customer %q", customer) } - if err = print.CustomerAttrs(outputFormat, r.w, r.appType, r.appSlug, ch, regHost, c); err != nil { + if err = print.CustomerAttrs(r.outputFormat, r.w, r.appType, r.appSlug, ch, regHost, c); err != nil { return errors.Wrap(err, "print customer attrs") } diff --git a/cli/cmd/customer_ls.go b/cli/cmd/customer_ls.go index 70e5464a5..2ce509ec4 100644 --- a/cli/cmd/customer_ls.go +++ b/cli/cmd/customer_ls.go @@ -8,9 +8,8 @@ import ( func (r *runners) InitCustomersLSCommand(parent *cobra.Command) *cobra.Command { var ( - appVersion string - includeTest bool - outputFormat string + appVersion string + includeTest bool ) customersLsCmd := &cobra.Command{ @@ -34,19 +33,18 @@ replicated customer ls --app myapp --output json # Combine multiple flags replicated customer ls --app myapp --output json`, RunE: func(cmd *cobra.Command, args []string) error { - return r.listCustomers(appVersion, includeTest, outputFormat) + return r.listCustomers(appVersion, includeTest) }, } parent.AddCommand(customersLsCmd) customersLsCmd.Flags().StringVar(&appVersion, "app-version", "", "Filter customers by a specific app version") customersLsCmd.Flags().BoolVar(&includeTest, "include-test", false, "Include test customers in the results") - customersLsCmd.Flags().StringVarP(&outputFormat, "output", "o", "table", "The output format to use. One of: json|table") return customersLsCmd } -func (r *runners) listCustomers(appVersion string, includeTest bool, outputFormat string) error { +func (r *runners) listCustomers(appVersion string, includeTest bool) error { if !r.hasApp() { return errors.New("no app specified") } @@ -56,14 +54,14 @@ func (r *runners) listCustomers(appVersion string, includeTest bool, outputForma if err != nil { return errors.Wrap(err, "list customers") } - return print.Customers(outputFormat, r.w, customers) + return print.Customers(r.outputFormat, r.w, customers) } else { customers, err := r.api.ListCustomersByAppAndVersion(r.appID, appVersion, r.appType) - if err != nil && outputFormat == "json" { - return print.CustomersWithInstances(outputFormat, r.w, customers) + if err != nil && r.outputFormat == "json" { + return print.CustomersWithInstances(r.outputFormat, r.w, customers) } else if err != nil { return errors.Wrap(err, "list customers by app and app version") } - return print.CustomersWithInstances(outputFormat, r.w, customers) + return print.CustomersWithInstances(r.outputFormat, r.w, customers) } } diff --git a/cli/cmd/customer_update.go b/cli/cmd/customer_update.go index bdf9afb1e..b3cec5825 100644 --- a/cli/cmd/customer_update.go +++ b/cli/cmd/customer_update.go @@ -94,7 +94,6 @@ replicated customer update --customer cus_abcdef123456 --name "JSON Corp" --outp cmd.Flags().BoolVar(&opts.IsDeveloperModeEnabled, "developer-mode", false, "If set, Replicated SDK installed in dev mode will use mock data.") cmd.Flags().StringVar(&opts.Email, "email", "", "Email address of the customer that is to be updated.") cmd.Flags().StringVar(&opts.Type, "type", "dev", "The license type to update. One of: dev|trial|paid|community|test (default: dev)") - cmd.Flags().StringVarP(&r.outputFormat, "output", "o", "table", "The output format to use. One of: json|table") cmd.MarkFlagRequired("customer") cmd.MarkFlagRequired("channel") diff --git a/cli/cmd/default_set.go b/cli/cmd/default_set.go index db9ffee6c..78f35385d 100644 --- a/cli/cmd/default_set.go +++ b/cli/cmd/default_set.go @@ -8,8 +8,6 @@ import ( ) func (r *runners) InitDefaultSetCommand(parent *cobra.Command) *cobra.Command { - var outputFormat string - cmd := &cobra.Command{ Use: "set KEY VALUE", Short: "Set default value for a key", @@ -26,17 +24,16 @@ either table or JSON format.`, replicated default set app my-app-slug`, Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { - return r.setDefault(cmd, args[0], args[1], outputFormat) + return r.setDefault(cmd, args[0], args[1]) }, } - cmd.Flags().StringVarP(&outputFormat, "output", "o", "table", "The output format to use. One of: json|table") parent.AddCommand(cmd) return cmd } -func (r *runners) setDefault(cmd *cobra.Command, defaultType string, defaultValue string, outputFormat string) error { +func (r *runners) setDefault(cmd *cobra.Command, defaultType string, defaultValue string) error { switch defaultType { case "app": app, err := getApp(defaultValue, r.api.KotsClient) @@ -48,7 +45,7 @@ func (r *runners) setDefault(cmd *cobra.Command, defaultType string, defaultValu return errors.Wrap(err, "set default in cache") } - if err := print.Apps(outputFormat, r.w, []types.AppAndChannels{{App: app}}); err != nil { + if err := print.Apps(r.outputFormat, r.w, []types.AppAndChannels{{App: app}}); err != nil { return errors.Wrap(err, "print app") } diff --git a/cli/cmd/default_show.go b/cli/cmd/default_show.go index 5bcc9a110..047ce2fbc 100644 --- a/cli/cmd/default_show.go +++ b/cli/cmd/default_show.go @@ -10,8 +10,6 @@ import ( ) func (r *runners) InitDefaultShowCommand(parent *cobra.Command) *cobra.Command { - var outputFormat string - cmd := &cobra.Command{ Use: "show KEY", Short: "Show default value for a key", @@ -29,24 +27,23 @@ replicated default show app `, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - return r.showDefault(cmd, args[0], outputFormat) + return r.showDefault(cmd, args[0]) }, } - cmd.Flags().StringVarP(&outputFormat, "output", "o", "table", "The output format to use. One of: json|table") parent.AddCommand(cmd) return cmd } -func (r *runners) showDefault(cmd *cobra.Command, defaultType string, outputFormat string) error { +func (r *runners) showDefault(cmd *cobra.Command, defaultType string) error { defaultValue, err := cache.GetDefault(defaultType) if err != nil { return errors.Wrap(err, "get default value") } if defaultValue == "" { - if outputFormat == "json" { + if r.outputFormat == "json" { fmt.Println("{}") } else { fmt.Printf("No default set for %s\n", defaultType) @@ -61,7 +58,7 @@ func (r *runners) showDefault(cmd *cobra.Command, defaultType string, outputForm return errors.Wrap(err, "get app") } - if err := print.Apps(outputFormat, r.w, []types.AppAndChannels{{App: app}}); err != nil { + if err := print.Apps(r.outputFormat, r.w, []types.AppAndChannels{{App: app}}); err != nil { return errors.Wrap(err, "print app") } } diff --git a/cli/cmd/enterprise_portal_invite.go b/cli/cmd/enterprise_portal_invite.go index 7edd9ebc4..f9a732c8a 100644 --- a/cli/cmd/enterprise_portal_invite.go +++ b/cli/cmd/enterprise_portal_invite.go @@ -11,7 +11,6 @@ import ( func (r *runners) InitEnterprisePortalInviteCmd(parent *cobra.Command) *cobra.Command { var customer string - var outputFormat string cmd := &cobra.Command{ Use: "invite [EMAIL_ADDRESSES...]", @@ -36,21 +35,20 @@ replicated enterprise-portal invite --customer "ACME Inc" --output json user@exa # Invite users to the Enterprise Portal for a specific app (if you have multiple apps) replicated enterprise-portal invite --app myapp --customer "ACME Inc" user1@example.com user2@example.com`, RunE: func(cmd *cobra.Command, args []string) error { - return r.enterprisePortalInvite(cmd, r.appID, customer, args, outputFormat) + return r.enterprisePortalInvite(cmd, r.appID, customer, args) }, SilenceUsage: true, } parent.AddCommand(cmd) cmd.Flags().StringVar(&customer, "customer", "", "The customer name or ID to invite") - cmd.Flags().StringVarP(&outputFormat, "output", "o", "table", "The output format to use. One of: json|table") cmd.MarkFlagRequired("customer") return cmd } -func (r *runners) enterprisePortalInvite(cmd *cobra.Command, appID string, customer string, emailAddresses []string, outputFormat string) error { +func (r *runners) enterprisePortalInvite(cmd *cobra.Command, appID string, customer string, emailAddresses []string) error { var c *types.Customer // try to get the customer as if we have an id first diff --git a/cli/cmd/enterprise_portal_user_ls.go b/cli/cmd/enterprise_portal_user_ls.go index ed98b4cbc..309c667ee 100644 --- a/cli/cmd/enterprise_portal_user_ls.go +++ b/cli/cmd/enterprise_portal_user_ls.go @@ -12,7 +12,6 @@ type listEnterprisePortalUsersOpts struct { func (r *runners) InitEnterprisePortalUserLsCmd(parent *cobra.Command) *cobra.Command { opts := listEnterprisePortalUsersOpts{} - var outputFormat string cmd := &cobra.Command{ Use: "ls", @@ -41,19 +40,18 @@ replicated enterprise-portal user ls --output json # List all users, including invites, for a specific app in table format replicated enterprise-portal user ls --app myapp --include-invites --output table`, RunE: func(cmd *cobra.Command, args []string) error { - return r.enterprisePortalUserLs(cmd, r.appID, opts, outputFormat) + return r.enterprisePortalUserLs(cmd, r.appID, opts) }, SilenceUsage: true, } parent.AddCommand(cmd) cmd.Flags().BoolVar(&opts.includeInvites, "include-invites", false, "Include pending invitations in the list") - cmd.Flags().StringVarP(&outputFormat, "output", "o", "table", "The output format to use. One of: json|table") return cmd } -func (r *runners) enterprisePortalUserLs(cmd *cobra.Command, appID string, opts listEnterprisePortalUsersOpts, outputFormat string) error { +func (r *runners) enterprisePortalUserLs(cmd *cobra.Command, appID string, opts listEnterprisePortalUsersOpts) error { users, err := r.kotsAPI.ListEnterprisePortalUsers(appID, opts.includeInvites) if err != nil { return err diff --git a/cli/cmd/enterpriseportal_status_get.go b/cli/cmd/enterpriseportal_status_get.go index cb53c54ba..5120977ea 100644 --- a/cli/cmd/enterpriseportal_status_get.go +++ b/cli/cmd/enterpriseportal_status_get.go @@ -8,8 +8,6 @@ import ( ) func (r *runners) InitEnterprisePortalStatusGetCmd(parent *cobra.Command) *cobra.Command { - var outputFormat string - cmd := &cobra.Command{ Use: "get", Short: "Get the status of the Enterprise Portal", @@ -33,17 +31,16 @@ replicated enterprise-portal status get --output json # Get the Enterprise Portal status and output in table format (default) replicated enterprise-portal status get --output table`, RunE: func(cmd *cobra.Command, args []string) error { - return r.enterprisePortalStatusGet(cmd, r.appID, outputFormat) + return r.enterprisePortalStatusGet(cmd, r.appID) }, SilenceUsage: true, } parent.AddCommand(cmd) - cmd.Flags().StringVarP(&outputFormat, "output", "o", "table", "The output format to use. One of: json|table") return cmd } -func (r *runners) enterprisePortalStatusGet(cmd *cobra.Command, appID string, outputFormat string) error { +func (r *runners) enterprisePortalStatusGet(cmd *cobra.Command, appID string) error { status, err := r.kotsAPI.GetEnterprisePortalStatus(appID) if err != nil { return errors.Wrap(err, "get enterprise portal status") diff --git a/cli/cmd/enterpriseportal_status_update.go b/cli/cmd/enterpriseportal_status_update.go index b70b253b7..d72d3e4e5 100644 --- a/cli/cmd/enterpriseportal_status_update.go +++ b/cli/cmd/enterpriseportal_status_update.go @@ -12,7 +12,6 @@ type updateEnterprisePortalStatusOpts struct { func (r *runners) InitEnterprisePortalStatusUpdateCmd(parent *cobra.Command) *cobra.Command { opts := updateEnterprisePortalStatusOpts{} - var outputFormat string cmd := &cobra.Command{ Use: "update", @@ -38,20 +37,19 @@ replicated enterprise-portal status update --status pending --output json # Update the Enterprise Portal status and output in table format (default) replicated enterprise-portal status update --status active --output table`, RunE: func(cmd *cobra.Command, args []string) error { - return r.enterprisePortalStatusUpdate(cmd, r.appID, opts, outputFormat) + return r.enterprisePortalStatusUpdate(cmd, r.appID, opts) }, SilenceUsage: true, } parent.AddCommand(cmd) cmd.Flags().StringVar(&opts.status, "status", "", "The status to set for the enterprise portal (active|inactive|pending)") - cmd.Flags().StringVarP(&outputFormat, "output", "o", "table", "The output format to use. One of: json|table") cmd.MarkFlagRequired("status") return cmd } -func (r *runners) enterprisePortalStatusUpdate(cmd *cobra.Command, appID string, opts updateEnterprisePortalStatusOpts, outputFormat string) error { +func (r *runners) enterprisePortalStatusUpdate(cmd *cobra.Command, appID string, opts updateEnterprisePortalStatusOpts) error { status, err := r.kotsAPI.UpdateEnterprisePortalStatus(appID, opts.status) if err != nil { return err diff --git a/cli/cmd/installer_ls.go b/cli/cmd/installer_ls.go index 0ab8ff411..870c0eedc 100644 --- a/cli/cmd/installer_ls.go +++ b/cli/cmd/installer_ls.go @@ -15,7 +15,7 @@ func (r *runners) InitInstallerList(parent *cobra.Command) { } parent.AddCommand(cmd) - cmd.Flags().StringVarP(&r.outputFormat, "output", "o", "table", "The output format to use. One of: json|table") + cmd.RunE = r.installerList } diff --git a/cli/cmd/instance_inspect.go b/cli/cmd/instance_inspect.go index 27739861f..0eeea2a21 100644 --- a/cli/cmd/instance_inspect.go +++ b/cli/cmd/instance_inspect.go @@ -19,7 +19,7 @@ func (r *runners) InitInstanceInspectCommand(parent *cobra.Command) *cobra.Comma parent.AddCommand(cmd) cmd.Flags().StringVar(&r.args.instanceInspectCustomer, "customer", "", "Customer Name or ID") cmd.Flags().StringVar(&r.args.instanceInspectInstance, "instance", "", "Instance Name or ID") - cmd.Flags().StringVarP(&r.outputFormat, "output", "o", "table", "The output format to use. One of: json|table") + return cmd } diff --git a/cli/cmd/instance_ls.go b/cli/cmd/instance_ls.go index fe12214d6..82b6536d9 100644 --- a/cli/cmd/instance_ls.go +++ b/cli/cmd/instance_ls.go @@ -20,7 +20,7 @@ func (r *runners) InitInstanceLSCommand(parent *cobra.Command) *cobra.Command { parent.AddCommand(cmd) cmd.Flags().StringVar(&r.args.instanceListCustomer, "customer", "", "Customer Name or ID") cmd.Flags().StringArrayVar(&r.args.instanceListTags, "tag", []string{}, "Tags to use to filter instances (key=value format, can be specified multiple times). Only one tag needs to match (an OR operation)") - cmd.Flags().StringVarP(&r.outputFormat, "output", "o", "table", "The output format to use. One of: json|table") + return cmd } diff --git a/cli/cmd/instance_tag.go b/cli/cmd/instance_tag.go index 62b970682..98df9ff68 100644 --- a/cli/cmd/instance_tag.go +++ b/cli/cmd/instance_tag.go @@ -19,7 +19,7 @@ func (r *runners) InitInstanceTagCommand(parent *cobra.Command) *cobra.Command { cmd.Flags().StringVar(&r.args.instanceTagCustomer, "customer", "", "Customer Name or ID") cmd.Flags().StringVar(&r.args.instanceTagInstacne, "instance", "", "Instance Name or ID") cmd.Flags().StringArrayVar(&r.args.instanceTagTags, "tag", []string{}, "Tags to apply to instance. Leave value empty to remove tag. Tags not specified will not be removed.") - cmd.Flags().StringVarP(&r.outputFormat, "output", "o", "table", "The output format to use. One of: json|table") + return cmd } diff --git a/cli/cmd/model_ls.go b/cli/cmd/model_ls.go index 11dbce428..b3841eb56 100644 --- a/cli/cmd/model_ls.go +++ b/cli/cmd/model_ls.go @@ -15,7 +15,6 @@ func (r *runners) InitModelList(parent *cobra.Command) *cobra.Command { SilenceUsage: true, } parent.AddCommand(cmd) - cmd.Flags().StringVarP(&r.outputFormat, "output", "o", "table", "The output format to use. One of: json|table") return cmd } diff --git a/cli/cmd/model_rm.go b/cli/cmd/model_rm.go index d2039ebc3..a55ad9b5f 100644 --- a/cli/cmd/model_rm.go +++ b/cli/cmd/model_rm.go @@ -16,7 +16,6 @@ func (r *runners) InitModelRemove(parent *cobra.Command) *cobra.Command { Args: cobra.ExactArgs(1), } parent.AddCommand(cmd) - cmd.Flags().StringVarP(&r.outputFormat, "output", "o", "table", "The output format to use. One of: json|table") return cmd } diff --git a/cli/cmd/network_create.go b/cli/cmd/network_create.go index 11013129b..1f1707c91 100644 --- a/cli/cmd/network_create.go +++ b/cli/cmd/network_create.go @@ -32,7 +32,6 @@ func (r *runners) InitNetworkCreate(parent *cobra.Command) *cobra.Command { cmd.Flags().BoolVar(&r.args.createNetworkDryRun, "dry-run", false, "Dry run") - cmd.Flags().StringVarP(&r.outputFormat, "output", "o", "table", "The output format to use. One of: json|table|wide") return cmd } diff --git a/cli/cmd/network_ls.go b/cli/cmd/network_ls.go index 82b0c4830..244e791b9 100644 --- a/cli/cmd/network_ls.go +++ b/cli/cmd/network_ls.go @@ -26,7 +26,6 @@ func (r *runners) InitNetworkList(parent *cobra.Command) *cobra.Command { cmd.Flags().BoolVar(&r.args.lsNetworkShowReports, "show-reports", false, "when set, only show networks that have reports") cmd.Flags().StringVar(&r.args.lsNetworkStartTime, "start-time", "", "start time for the query (Format: 2006-01-02T15:04:05Z)") cmd.Flags().StringVar(&r.args.lsNetworkEndTime, "end-time", "", "end time for the query (Format: 2006-01-02T15:04:05Z)") - cmd.Flags().StringVarP(&r.outputFormat, "output", "o", "table", "The output format to use. One of: json|table|wide") cmd.Flags().BoolVarP(&r.args.lsNetworkWatch, "watch", "w", false, "watch networks") return cmd diff --git a/cli/cmd/notification.go b/cli/cmd/notification.go index 8f052696e..2385a8b41 100644 --- a/cli/cmd/notification.go +++ b/cli/cmd/notification.go @@ -69,7 +69,7 @@ func (r *runners) InitNotificationWebhookCommand(parent *cobra.Command) *cobra.C } func (r *runners) InitNotificationSubscriptionList(parent *cobra.Command) *cobra.Command { - var outputFormat, search, subscriptionType string + var search, subscriptionType string cmd := &cobra.Command{ Use: "ls", @@ -81,22 +81,19 @@ func (r *runners) InitNotificationSubscriptionList(parent *cobra.Command) *cobra return errors.Wrap(err, "list notification subscriptions") } if len(resp.Subscriptions) == 0 { - return print.NoNotifications(outputFormat, r.w) + return print.NoNotifications(r.outputFormat, r.w) } - return print.Notifications(outputFormat, r.w, resp.Subscriptions) + return print.Notifications(r.outputFormat, r.w, resp.Subscriptions) }, SilenceUsage: true, } parent.AddCommand(cmd) cmd.Flags().StringVar(&search, "search", "", "Text search filter") cmd.Flags().StringVar(&subscriptionType, "type", "", "Filter by subscription type: personal|team") - cmd.Flags().StringVarP(&outputFormat, "output", "o", "table", "The output format to use. One of: json|table") return cmd } func (r *runners) InitNotificationSubscriptionGet(parent *cobra.Command) *cobra.Command { - var outputFormat string - cmd := &cobra.Command{ Use: "get ID", Short: "Get a notification subscription", @@ -106,17 +103,16 @@ func (r *runners) InitNotificationSubscriptionGet(parent *cobra.Command) *cobra. if err != nil { return errors.Wrap(err, "get notification subscription") } - return print.Notification(outputFormat, r.w, subscription) + return print.Notification(r.outputFormat, r.w, subscription) }, SilenceUsage: true, } parent.AddCommand(cmd) - cmd.Flags().StringVarP(&outputFormat, "output", "o", "table", "The output format to use. One of: json|table") return cmd } func (r *runners) InitNotificationSubscriptionCreate(parent *cobra.Command) *cobra.Command { - var outputFormat, file string + var file string cmd := &cobra.Command{ Use: "create", @@ -130,19 +126,18 @@ func (r *runners) InitNotificationSubscriptionCreate(parent *cobra.Command) *cob if err != nil { return errors.Wrap(err, "create notification subscription") } - return print.Notification(outputFormat, r.w, subscription) + return print.Notification(r.outputFormat, r.w, subscription) }, SilenceUsage: true, } parent.AddCommand(cmd) cmd.Flags().StringVar(&file, "file", "", "Path to a JSON file containing the subscription definition") - cmd.Flags().StringVarP(&outputFormat, "output", "o", "table", "The output format to use. One of: json|table") _ = cmd.MarkFlagRequired("file") return cmd } func (r *runners) InitNotificationSubscriptionUpdate(parent *cobra.Command) *cobra.Command { - var outputFormat, file string + var file string cmd := &cobra.Command{ Use: "update ID", @@ -157,13 +152,12 @@ func (r *runners) InitNotificationSubscriptionUpdate(parent *cobra.Command) *cob if err != nil { return errors.Wrap(err, "update notification subscription") } - return print.Notification(outputFormat, r.w, subscription) + return print.Notification(r.outputFormat, r.w, subscription) }, SilenceUsage: true, } parent.AddCommand(cmd) cmd.Flags().StringVar(&file, "file", "", "Path to a JSON file containing the subscription patch") - cmd.Flags().StringVarP(&outputFormat, "output", "o", "table", "The output format to use. One of: json|table") _ = cmd.MarkFlagRequired("file") return cmd } @@ -191,8 +185,6 @@ func (r *runners) InitNotificationSubscriptionRemove(parent *cobra.Command) *cob } func (r *runners) InitNotificationSubscriptionEvents(parent *cobra.Command) *cobra.Command { - var outputFormat string - cmd := &cobra.Command{ Use: "events ID", Short: "List delivery events for a notification subscription", @@ -203,19 +195,17 @@ func (r *runners) InitNotificationSubscriptionEvents(parent *cobra.Command) *cob return errors.Wrap(err, "list notification subscription events") } if len(resp.Events) == 0 { - return print.NoNotificationEvents(outputFormat, r.w) + return print.NoNotificationEvents(r.outputFormat, r.w) } - return print.NotificationEvents(outputFormat, r.w, resp.Events) + return print.NotificationEvents(r.outputFormat, r.w, resp.Events) }, SilenceUsage: true, } parent.AddCommand(cmd) - cmd.Flags().StringVarP(&outputFormat, "output", "o", "table", "The output format to use. One of: json|table") return cmd } func (r *runners) InitNotificationEventList(parent *cobra.Command) *cobra.Command { - var outputFormat string opts := kotsclient.ListNotificationEventsOpts{} cmd := &cobra.Command{ @@ -238,9 +228,9 @@ func (r *runners) InitNotificationEventList(parent *cobra.Command) *cobra.Comman return errors.Wrap(err, "list notification events") } if len(resp.Events) == 0 { - return print.NoNotificationEvents(outputFormat, r.w) + return print.NoNotificationEvents(r.outputFormat, r.w) } - return print.NotificationEvents(outputFormat, r.w, resp.Events) + return print.NotificationEvents(r.outputFormat, r.w, resp.Events) }, SilenceUsage: true, } @@ -255,13 +245,10 @@ func (r *runners) InitNotificationEventList(parent *cobra.Command) *cobra.Comman cmd.Flags().StringVar(&opts.Status, "status", "", "Filter by status: success|pending|failed") cmd.Flags().IntVar(&opts.CurrentPage, "current-page", 0, "Pagination page index") cmd.Flags().IntVar(&opts.PageSize, "page-size", 20, "Pagination page size") - cmd.Flags().StringVarP(&outputFormat, "output", "o", "table", "The output format to use. One of: json|table") return cmd } func (r *runners) InitNotificationEventRetry(parent *cobra.Command) *cobra.Command { - var outputFormat string - cmd := &cobra.Command{ Use: "retry EVENT_ID", Short: "Retry a notification event", @@ -271,17 +258,16 @@ func (r *runners) InitNotificationEventRetry(parent *cobra.Command) *cobra.Comma if err != nil { return errors.Wrap(err, "retry notification event") } - return print.NotificationRetry(outputFormat, r.w, resp) + return print.NotificationRetry(r.outputFormat, r.w, resp) }, SilenceUsage: true, } parent.AddCommand(cmd) - cmd.Flags().StringVarP(&outputFormat, "output", "o", "table", "The output format to use. One of: json|table") return cmd } func (r *runners) InitNotificationEventTypeList(parent *cobra.Command) *cobra.Command { - var outputFormat, query string + var query string var limit int cmd := &cobra.Command{ @@ -293,19 +279,18 @@ func (r *runners) InitNotificationEventTypeList(parent *cobra.Command) *cobra.Co if err != nil { return errors.Wrap(err, "list notification event types") } - return print.NotificationEventTypes(outputFormat, r.w, resp) + return print.NotificationEventTypes(r.outputFormat, r.w, resp) }, SilenceUsage: true, } parent.AddCommand(cmd) cmd.Flags().StringVar(&query, "q", "", "Search query") cmd.Flags().IntVar(&limit, "limit", 0, "Maximum results to request from the API (0 means no limit)") - cmd.Flags().StringVarP(&outputFormat, "output", "o", "table", "The output format to use. One of: json|table") return cmd } func (r *runners) InitNotificationEmailResendVerification(parent *cobra.Command) *cobra.Command { - var outputFormat, email string + var email string cmd := &cobra.Command{ Use: "resend-verification", @@ -320,19 +305,18 @@ func (r *runners) InitNotificationEmailResendVerification(parent *cobra.Command) if err != nil { return errors.Wrap(err, "resend verification email") } - return print.NotificationEmailAction(outputFormat, r.w, resp) + return print.NotificationEmailAction(r.outputFormat, r.w, resp) }, SilenceUsage: true, } parent.AddCommand(cmd) cmd.Flags().StringVar(&email, "email", "", "Email address to verify") - cmd.Flags().StringVarP(&outputFormat, "output", "o", "table", "The output format to use. One of: json|table") _ = cmd.MarkFlagRequired("email") return cmd } func (r *runners) InitNotificationEmailVerify(parent *cobra.Command) *cobra.Command { - var outputFormat, email, code string + var email, code string cmd := &cobra.Command{ Use: "verify", @@ -351,21 +335,20 @@ func (r *runners) InitNotificationEmailVerify(parent *cobra.Command) *cobra.Comm if err != nil { return errors.Wrap(err, "verify notification email") } - return print.NotificationEmailAction(outputFormat, r.w, resp) + return print.NotificationEmailAction(r.outputFormat, r.w, resp) }, SilenceUsage: true, } parent.AddCommand(cmd) cmd.Flags().StringVar(&email, "email", "", "Email address to verify") cmd.Flags().StringVar(&code, "code", "", "Verification code") - cmd.Flags().StringVarP(&outputFormat, "output", "o", "table", "The output format to use. One of: json|table") _ = cmd.MarkFlagRequired("email") _ = cmd.MarkFlagRequired("code") return cmd } func (r *runners) InitNotificationWebhookTest(parent *cobra.Command) *cobra.Command { - var outputFormat, file string + var file string cmd := &cobra.Command{ Use: "test", @@ -379,13 +362,12 @@ func (r *runners) InitNotificationWebhookTest(parent *cobra.Command) *cobra.Comm if err != nil { return errors.Wrap(err, "test notification webhook") } - return print.NotificationWebhookTest(outputFormat, r.w, resp) + return print.NotificationWebhookTest(r.outputFormat, r.w, resp) }, SilenceUsage: true, } parent.AddCommand(cmd) cmd.Flags().StringVar(&file, "file", "", "Path to a JSON file containing the webhook test payload") - cmd.Flags().StringVarP(&outputFormat, "output", "o", "table", "The output format to use. One of: json|table") _ = cmd.MarkFlagRequired("file") return cmd } diff --git a/cli/cmd/output_test.go b/cli/cmd/output_test.go new file mode 100644 index 000000000..4a5ee3207 --- /dev/null +++ b/cli/cmd/output_test.go @@ -0,0 +1,41 @@ +package cmd + +import ( + "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/require" +) + +func TestResolveOutputFormat_ExplicitFlagWins(t *testing.T) { + // Set env var + t.Setenv("REPLICATED_OUTPUT", "json") + + r := &runners{outputFormat: "table"} + cmd := &cobra.Command{} + cmd.Flags().StringVarP(&r.outputFormat, "output", "o", "table", "") + cmd.Flags().Set("output", "wide") + + r.resolveOutputFormat(cmd) + require.Equal(t, "wide", r.outputFormat) +} + +func TestResolveOutputFormat_EnvVarWinsOverDefault(t *testing.T) { + t.Setenv("REPLICATED_OUTPUT", "json") + + r := &runners{outputFormat: "table"} + cmd := &cobra.Command{} + cmd.Flags().StringVarP(&r.outputFormat, "output", "o", "table", "") + + r.resolveOutputFormat(cmd) + require.Equal(t, "json", r.outputFormat) +} + +func TestResolveOutputFormat_DefaultTable(t *testing.T) { + r := &runners{outputFormat: "table"} + cmd := &cobra.Command{} + cmd.Flags().StringVarP(&r.outputFormat, "output", "o", "table", "") + + r.resolveOutputFormat(cmd) + require.Equal(t, "table", r.outputFormat) +} diff --git a/cli/cmd/policy_create.go b/cli/cmd/policy_create.go index e3682c0ec..6fe085301 100644 --- a/cli/cmd/policy_create.go +++ b/cli/cmd/policy_create.go @@ -15,7 +15,6 @@ func (r *runners) InitPolicyCreate(parent *cobra.Command) *cobra.Command { name string description string definitionFile string - outputFormat string ) cmd := &cobra.Command{ @@ -43,7 +42,7 @@ Vendors not on an enterprise plan cannot create policies.`, # Create a policy with a description replicated policy create --name "My Policy" --description "Custom access policy" --definition policy.json`, RunE: func(cmd *cobra.Command, args []string) error { - return r.policyCreate(name, description, definitionFile, outputFormat) + return r.policyCreate(name, description, definitionFile) }, SilenceUsage: true, } @@ -51,14 +50,13 @@ Vendors not on an enterprise plan cannot create policies.`, cmd.Flags().StringVar(&name, "name", "", "Name of the policy") cmd.Flags().StringVar(&description, "description", "", "Description of the policy") cmd.Flags().StringVar(&definitionFile, "definition", "", "Path to the JSON file containing the policy definition") - cmd.Flags().StringVarP(&outputFormat, "output", "o", "table", "The output format to use. One of: json|table") cmd.MarkFlagRequired("name") cmd.MarkFlagRequired("definition") return cmd } -func (r *runners) policyCreate(name, description, definitionFile, outputFormat string) error { +func (r *runners) policyCreate(name, description, definitionFile string) error { definition, err := readPolicyDefinition(definitionFile) if err != nil { return errors.Wrap(err, "read policy definition") @@ -72,7 +70,7 @@ func (r *runners) policyCreate(name, description, definitionFile, outputFormat s return errors.Wrap(err, "create policy") } - return print.Policy(outputFormat, r.w, policy) + return print.Policy(r.outputFormat, r.w, policy) } func readPolicyDefinition(path string) (string, error) { diff --git a/cli/cmd/policy_get.go b/cli/cmd/policy_get.go index 9beac8328..1d44841a9 100644 --- a/cli/cmd/policy_get.go +++ b/cli/cmd/policy_get.go @@ -11,10 +11,7 @@ import ( ) func (r *runners) InitPolicyGet(parent *cobra.Command) *cobra.Command { - var ( - outputFormat string - outputFile string - ) + var outputFile string cmd := &cobra.Command{ Use: "get NAME_OR_ID", @@ -30,18 +27,17 @@ func (r *runners) InitPolicyGet(parent *cobra.Command) *cobra.Command { replicated policy get "My Policy" --output json`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - return r.policyGet(args[0], outputFormat, outputFile) + return r.policyGet(args[0], outputFile) }, SilenceUsage: true, } parent.AddCommand(cmd) - cmd.Flags().StringVarP(&outputFormat, "output", "o", "table", "The output format to use. One of: json|table") cmd.Flags().StringVar(&outputFile, "output-file", "", "If set, saves the policy definition to the specified file") return cmd } -func (r *runners) policyGet(nameOrID, outputFormat, outputFile string) error { +func (r *runners) policyGet(nameOrID, outputFile string) error { policy, err := r.kotsAPI.GetPolicyByNameOrID(nameOrID) if err != nil { return errors.Wrap(err, "get policy") @@ -59,5 +55,5 @@ func (r *runners) policyGet(nameOrID, outputFormat, outputFile string) error { return r.w.Flush() } - return print.Policy(outputFormat, r.w, policy) + return print.Policy(r.outputFormat, r.w, policy) } diff --git a/cli/cmd/policy_ls.go b/cli/cmd/policy_ls.go index 700b714f1..7fbaf9dd3 100644 --- a/cli/cmd/policy_ls.go +++ b/cli/cmd/policy_ls.go @@ -7,8 +7,6 @@ import ( ) func (r *runners) InitPolicyList(parent *cobra.Command) *cobra.Command { - var outputFormat string - cmd := &cobra.Command{ Use: "ls", Aliases: []string{"list"}, @@ -20,21 +18,20 @@ func (r *runners) InitPolicyList(parent *cobra.Command) *cobra.Command { # List policies in JSON format replicated policy ls --output json`, RunE: func(cmd *cobra.Command, args []string) error { - return r.policyList(outputFormat) + return r.policyList() }, SilenceUsage: true, } parent.AddCommand(cmd) - cmd.Flags().StringVarP(&outputFormat, "output", "o", "table", "The output format to use. One of: json|table") return cmd } -func (r *runners) policyList(outputFormat string) error { +func (r *runners) policyList() error { policies, err := r.kotsAPI.ListPolicies() if err != nil { return errors.Wrap(err, "list policies") } - return print.Policies(outputFormat, r.w, policies) + return print.Policies(r.outputFormat, r.w, policies) } diff --git a/cli/cmd/policy_update.go b/cli/cmd/policy_update.go index 1acba134f..f4c032d8f 100644 --- a/cli/cmd/policy_update.go +++ b/cli/cmd/policy_update.go @@ -14,7 +14,6 @@ func (r *runners) InitPolicyUpdate(parent *cobra.Command) *cobra.Command { newName string description string definitionFile string - outputFormat string ) cmd := &cobra.Command{ @@ -35,7 +34,7 @@ Vendors not on an enterprise plan cannot update policies.`, replicated policy update "My Policy" --description "Updated description" --definition policy.json`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - return r.policyUpdate(cmd, args[0], newName, description, definitionFile, outputFormat) + return r.policyUpdate(cmd, args[0], newName, description, definitionFile) }, SilenceUsage: true, } @@ -43,12 +42,11 @@ Vendors not on an enterprise plan cannot update policies.`, cmd.Flags().StringVar(&newName, "name", "", "New name for the policy") cmd.Flags().StringVar(&description, "description", "", "New description for the policy") cmd.Flags().StringVar(&definitionFile, "definition", "", "Path to the JSON file containing the updated policy definition") - cmd.Flags().StringVarP(&outputFormat, "output", "o", "table", "The output format to use. One of: json|table") return cmd } -func (r *runners) policyUpdate(cmd *cobra.Command, nameOrID, newName, description, definitionFile, outputFormat string) error { +func (r *runners) policyUpdate(cmd *cobra.Command, nameOrID, newName, description, definitionFile string) error { if !cmd.Flags().Changed("name") && !cmd.Flags().Changed("description") && !cmd.Flags().Changed("definition") { return errors.New("at least one of --name, --description, or --definition must be specified") } @@ -88,5 +86,5 @@ func (r *runners) policyUpdate(cmd *cobra.Command, nameOrID, newName, descriptio return errors.Wrap(err, "update policy") } - return print.Policy(outputFormat, r.w, policy) + return print.Policy(r.outputFormat, r.w, policy) } diff --git a/cli/cmd/registry_add_dockerhub.go b/cli/cmd/registry_add_dockerhub.go index 0962a2654..57e2b46a8 100644 --- a/cli/cmd/registry_add_dockerhub.go +++ b/cli/cmd/registry_add_dockerhub.go @@ -27,7 +27,6 @@ func (r *runners) InitRegistryAddDockerHub(parent *cobra.Command) *cobra.Command cmd.Flags().BoolVar(&r.args.addRegistryTokenFromStdIn, "token-stdin", false, "Take the token from stdin") cmd.Flags().StringVar(&r.args.addRegistryName, "name", "", "Name for the registry") cmd.Flags().StringVar(&r.args.addRegistryAppIds, "app-ids", "", "Comma-separated list of app IDs to scope this registry to") - cmd.Flags().StringVarP(&r.outputFormat, "output", "o", "table", "The output format to use. One of: json|table") cmd.RunE = r.registryAddDockerHub diff --git a/cli/cmd/registry_add_ecr.go b/cli/cmd/registry_add_ecr.go index 91adc69b4..bcb2fd61e 100644 --- a/cli/cmd/registry_add_ecr.go +++ b/cli/cmd/registry_add_ecr.go @@ -25,7 +25,6 @@ func (r *runners) InitRegistryAddECR(parent *cobra.Command) { cmd.Flags().BoolVar(&r.args.addRegistrySecretAccessKeyFromStdIn, "secretaccesskey-stdin", false, "Take the secret access key from stdin") cmd.Flags().StringVar(&r.args.addRegistryName, "name", "", "Name for the registry") cmd.Flags().StringVar(&r.args.addRegistryAppIds, "app-ids", "", "Comma-separated list of app IDs to scope this registry to") - cmd.Flags().StringVarP(&r.outputFormat, "output", "o", "table", "The output format to use. One of: json|table") cmd.RunE = r.registryAddECR } diff --git a/cli/cmd/registry_add_gar.go b/cli/cmd/registry_add_gar.go index 61fd5999a..4bde0434e 100644 --- a/cli/cmd/registry_add_gar.go +++ b/cli/cmd/registry_add_gar.go @@ -28,7 +28,6 @@ func (r *runners) InitRegistryAddGAR(parent *cobra.Command) { cmd.Flags().BoolVar(&r.args.addRegistryTokenFromStdIn, "token-stdin", false, "Take the token from stdin") cmd.Flags().StringVar(&r.args.addRegistryName, "name", "", "Name for the registry") cmd.Flags().StringVar(&r.args.addRegistryAppIds, "app-ids", "", "Comma-separated list of app IDs to scope this registry to") - cmd.Flags().StringVarP(&r.outputFormat, "output", "o", "table", "The output format to use. One of: json|table") cmd.RunE = r.registryAddGAR } diff --git a/cli/cmd/registry_add_gcr.go b/cli/cmd/registry_add_gcr.go index a391f8286..7eff8a585 100644 --- a/cli/cmd/registry_add_gcr.go +++ b/cli/cmd/registry_add_gcr.go @@ -24,7 +24,6 @@ func (r *runners) InitRegistryAddGCR(parent *cobra.Command) { cmd.Flags().BoolVar(&r.args.addRegistryServiceAccountKeyFromStdIn, "serviceaccountkey-stdin", false, "Take the service account key from stdin") cmd.Flags().StringVar(&r.args.addRegistryName, "name", "", "Name for the registry") cmd.Flags().StringVar(&r.args.addRegistryAppIds, "app-ids", "", "Comma-separated list of app IDs to scope this registry to") - cmd.Flags().StringVarP(&r.outputFormat, "output", "o", "table", "The output format to use. One of: json|table") cmd.RunE = r.registryAddGCR } diff --git a/cli/cmd/registry_add_ghcr.go b/cli/cmd/registry_add_ghcr.go index a2562140d..381b2893e 100644 --- a/cli/cmd/registry_add_ghcr.go +++ b/cli/cmd/registry_add_ghcr.go @@ -23,7 +23,6 @@ func (r *runners) InitRegistryAddGHCR(parent *cobra.Command) { cmd.Flags().BoolVar(&r.args.addRegistryTokenFromStdIn, "token-stdin", false, "Take the token from stdin") cmd.Flags().StringVar(&r.args.addRegistryName, "name", "", "Name for the registry") cmd.Flags().StringVar(&r.args.addRegistryAppIds, "app-ids", "", "Comma-separated list of app IDs to scope this registry to") - cmd.Flags().StringVarP(&r.outputFormat, "output", "o", "table", "The output format to use. One of: json|table") cmd.RunE = r.registryAddGHCR } diff --git a/cli/cmd/registry_add_other.go b/cli/cmd/registry_add_other.go index b1581c09d..8dcdb98d6 100644 --- a/cli/cmd/registry_add_other.go +++ b/cli/cmd/registry_add_other.go @@ -25,7 +25,6 @@ func (r *runners) InitRegistryAddOther(parent *cobra.Command) *cobra.Command { cmd.Flags().BoolVar(&r.args.addRegistryPasswordFromStdIn, "password-stdin", false, "Take the password from stdin") cmd.Flags().StringVar(&r.args.addRegistryName, "name", "", "Name for the registry") cmd.Flags().StringVar(&r.args.addRegistryAppIds, "app-ids", "", "Comma-separated list of app IDs to scope this registry to") - cmd.Flags().StringVarP(&r.outputFormat, "output", "o", "table", "The output format to use. One of: json|table") cmd.RunE = r.registryAddOther diff --git a/cli/cmd/registry_add_quay.go b/cli/cmd/registry_add_quay.go index 538215082..53a00028b 100644 --- a/cli/cmd/registry_add_quay.go +++ b/cli/cmd/registry_add_quay.go @@ -24,7 +24,6 @@ func (r *runners) InitRegistryAddQuay(parent *cobra.Command) *cobra.Command { cmd.Flags().BoolVar(&r.args.addRegistryPasswordFromStdIn, "password-stdin", false, "Take the password from stdin") cmd.Flags().StringVar(&r.args.addRegistryName, "name", "", "Name for the registry") cmd.Flags().StringVar(&r.args.addRegistryAppIds, "app-ids", "", "Comma-separated list of app IDs to scope this registry to") - cmd.Flags().StringVarP(&r.outputFormat, "output", "o", "table", "The output format to use. One of: json|table") cmd.RunE = r.registryAddQuay diff --git a/cli/cmd/registry_ls.go b/cli/cmd/registry_ls.go index f69342c54..903d5f98f 100644 --- a/cli/cmd/registry_ls.go +++ b/cli/cmd/registry_ls.go @@ -19,7 +19,6 @@ func (r *runners) InitRegistryList(parent *cobra.Command) *cobra.Command { SilenceUsage: true, } parent.AddCommand(cmd) - cmd.Flags().StringVarP(&r.outputFormat, "output", "o", "table", "The output format to use. One of: json|table") return cmd } diff --git a/cli/cmd/release_create.go b/cli/cmd/release_create.go index 5a5e8a3e3..33816f21a 100644 --- a/cli/cmd/release_create.go +++ b/cli/cmd/release_create.go @@ -80,8 +80,7 @@ replicated release create --version 1.0.0 --promote Unstable --required`, cmd.Flags().StringVar(&r.args.createReleaseOutputDir, "output-dir", "", "Stage the release artifacts (packaged charts and manifests) to this directory. Existing contents of the directory are removed before each run. The directory is preserved after the command completes.") cmd.Flags().BoolVar(&r.args.createReleaseNoUpload, "no-upload", false, "Build the release locally but do not upload it. Use with --output-dir to inspect or reuse the staged artifacts. Cannot be used with --promote.") - // output format - cmd.Flags().StringVarP(&r.outputFormat, "output", "o", "table", "The output format to use. One of: json|table") + // not supported for KOTS cmd.Flags().MarkHidden("yaml-file") diff --git a/cli/cmd/release_inspect.go b/cli/cmd/release_inspect.go index 1aad2bba8..cd73ad298 100644 --- a/cli/cmd/release_inspect.go +++ b/cli/cmd/release_inspect.go @@ -28,7 +28,6 @@ replicated release inspect 123 replicated release inspect 123 --output json`, Args: cobra.ExactArgs(1), } - cmd.Flags().StringVarP(&r.outputFormat, "output", "o", "table", "The output format to use. One of: json|table") parent.AddCommand(cmd) cmd.RunE = r.releaseInspect diff --git a/cli/cmd/release_lint.go b/cli/cmd/release_lint.go index f0ff97f8e..95b000361 100644 --- a/cli/cmd/release_lint.go +++ b/cli/cmd/release_lint.go @@ -42,8 +42,7 @@ func (r *runners) InitReleaseLint(parent *cobra.Command) { // New flags (for local lint - when flag=1) cmd.Flags().BoolVarP(&r.args.lintVerbose, "verbose", "v", false, "Show detailed output including extracted container images (local lint only)") - // Output format flag works for both old and new lint - cmd.Flags().StringVarP(&r.outputFormat, "output", "o", "table", "The output format to use. One of: json|table") + cmd.Flags().MarkHidden("chart") diff --git a/cli/cmd/release_ls.go b/cli/cmd/release_ls.go index de4efca42..36adbe41d 100644 --- a/cli/cmd/release_ls.go +++ b/cli/cmd/release_ls.go @@ -15,7 +15,7 @@ func (r *runners) IniReleaseList(parent *cobra.Command) { } parent.AddCommand(cmd) - cmd.Flags().StringVarP(&r.outputFormat, "output", "o", "table", "The output format to use. One of: json|table") + cmd.RunE = r.releaseList } diff --git a/cli/cmd/root.go b/cli/cmd/root.go index 2c1af2499..fadfc7f88 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -124,6 +124,12 @@ func Execute(rootCmd *cobra.Command, stdin io.Reader, stdout io.Writer, stderr i runCmds.rootCmd.SetErr(stderr) runCmds.rootCmd.SetOut(stderr) } + + runCmds.rootCmd.PersistentFlags().StringVarP( + &runCmds.outputFormat, "output", "o", "table", + "The output format to use. Supported formats vary by command (json, table, wide). (default 'table', override with REPLICATED_OUTPUT env var)", + ) + if stdout != nil { defaultHelpFunc := runCmds.rootCmd.HelpFunc() runCmds.rootCmd.SetHelpFunc(func(cmd *cobra.Command, args []string) { @@ -329,6 +335,7 @@ func Execute(rootCmd *cobra.Command, stdin io.Reader, stdout io.Writer, stderr i runCmds.rootCmd.SetUsageTemplate(rootCmdUsageTmpl) preRunSetupAPIs := func(cmd *cobra.Command, args []string) error { + runCmds.resolveOutputFormat(cmd) if apiToken == "" { // Try to load profile from --profile flag, then default profile var profileName string @@ -436,6 +443,7 @@ func Execute(rootCmd *cobra.Command, stdin io.Reader, stdout io.Writer, stderr i } prerunCommand := func(cmd *cobra.Command, args []string) (err error) { + runCmds.resolveOutputFormat(cmd) if cmd.SilenceErrors { // when SilenceErrors is set, command wants to use custom error printer defer func() { printIfError(cmd, err) @@ -547,7 +555,7 @@ func Execute(rootCmd *cobra.Command, stdin io.Reader, stdout io.Writer, stderr i runCmds.InitInitCommand(configCmd) configCmd.PersistentPreRunE = preRunSetupAPIs - runCmds.rootCmd.AddCommand(Version()) + runCmds.rootCmd.AddCommand(runCmds.Version()) return runCmds.rootCmd.Execute() } diff --git a/cli/cmd/runner.go b/cli/cmd/runner.go index 00dac30b2..536efe653 100644 --- a/cli/cmd/runner.go +++ b/cli/cmd/runner.go @@ -2,6 +2,7 @@ package cmd import ( "io" + "os" "text/tabwriter" "time" @@ -37,6 +38,15 @@ func (r *runners) hasApp() bool { return true } +func (r *runners) resolveOutputFormat(cmd *cobra.Command) { + if cmd.Flags().Changed("output") { + return // explicit flag wins + } + if env := os.Getenv("REPLICATED_OUTPUT"); env != "" { + r.outputFormat = env + } +} + type runnerArgs struct { channelCreateName string channelCreateDescription string @@ -289,7 +299,6 @@ type runnerArgs struct { clusterAddonCreateObjectStoreClusterID string clusterAddonCreateObjectStoreDuration time.Duration clusterAddonCreateObjectStoreDryRun bool - clusterAddonCreateObjectStoreOutput string demoteReleaseSequence int64 demoteChannelSequence int64 diff --git a/cli/cmd/version.go b/cli/cmd/version.go index 9f68ea5da..c439eab5e 100644 --- a/cli/cmd/version.go +++ b/cli/cmd/version.go @@ -9,9 +9,7 @@ import ( "github.com/replicatedhq/replicated/pkg/version" ) -func Version() *cobra.Command { - var versionJson bool - +func (r *runners) Version() *cobra.Command { cmd := &cobra.Command{ Use: "version", Short: "Print the current version and exit", @@ -36,7 +34,7 @@ func Version() *cobra.Command { // Now get the (potentially updated) build info build := version.GetBuild() - if !versionJson { + if r.outputFormat != "json" { // Special handling for development/unknown version when printing if currentVersion == "unknown" || currentVersion == "development" { fmt.Printf("replicated version %s (development build)\n", currentVersion) @@ -63,8 +61,6 @@ func Version() *cobra.Command { }, } - cmd.Flags().BoolVar(&versionJson, "json", false, "output version info in json") - cmd.AddCommand(versionUpgradeCmd()) return cmd diff --git a/cli/cmd/vm_create.go b/cli/cmd/vm_create.go index 714e095f9..9f9b302cb 100644 --- a/cli/cmd/vm_create.go +++ b/cli/cmd/vm_create.go @@ -90,7 +90,6 @@ ssh -i /tmp/ci_key $(replicated vm ssh-endpoint my-vm --username ci)`, cmd.Flags().BoolVar(&r.args.createVMDryRun, "dry-run", false, "Dry run") - cmd.Flags().StringVarP(&r.outputFormat, "output", "o", "table", "The output format to use. One of: json|table|wide") _ = cmd.MarkFlagRequired("distribution") diff --git a/cli/cmd/vm_ls.go b/cli/cmd/vm_ls.go index 9e6c68978..cfff4c2ef 100644 --- a/cli/cmd/vm_ls.go +++ b/cli/cmd/vm_ls.go @@ -44,7 +44,6 @@ replicated vm ls --watch`, cmd.Flags().BoolVar(&r.args.lsVMShowTerminated, "show-terminated", false, "when set, only show terminated vms") cmd.Flags().StringVar(&r.args.lsVMStartTime, "start-time", "", "start time for the query (Format: 2006-01-02T15:04:05Z)") cmd.Flags().StringVar(&r.args.lsVMEndTime, "end-time", "", "end time for the query (Format: 2006-01-02T15:04:05Z)") - cmd.Flags().StringVarP(&r.outputFormat, "output", "o", "table", "The output format to use. One of: json|table|wide") cmd.Flags().BoolVarP(&r.args.lsVMWatch, "watch", "w", false, "watch vms") return cmd diff --git a/cli/cmd/vm_port_expose.go b/cli/cmd/vm_port_expose.go index 1c36de95b..8295b0715 100644 --- a/cli/cmd/vm_port_expose.go +++ b/cli/cmd/vm_port_expose.go @@ -38,7 +38,6 @@ replicated vm port expose VM_ID_OR_NAME --port 8080 --protocol https --output js panic(err) } cmd.Flags().StringSliceVar(&r.args.vmExposePortProtocols, "protocol", []string{"http", "https"}, `Protocol to expose (valid values are "http", "https", "ws" and "wss")`) - cmd.Flags().StringVarP(&r.outputFormat, "output", "o", "table", "The output format to use. One of: json|table|wide") return cmd } diff --git a/cli/cmd/vm_port_ls.go b/cli/cmd/vm_port_ls.go index a29eede5b..6ee590537 100644 --- a/cli/cmd/vm_port_ls.go +++ b/cli/cmd/vm_port_ls.go @@ -28,7 +28,6 @@ replicated vm port ls VM_ID_OR_NAME --output wide`, } parent.AddCommand(cmd) - cmd.Flags().StringVarP(&r.outputFormat, "output", "o", "table", "The output format to use. One of: json|table|wide") return cmd } diff --git a/cli/cmd/vm_port_rm.go b/cli/cmd/vm_port_rm.go index b51ba49e0..4d4069935 100644 --- a/cli/cmd/vm_port_rm.go +++ b/cli/cmd/vm_port_rm.go @@ -30,7 +30,6 @@ replicated vm port rm VM_ID_OR_NAME --id PORT_ID --output json`, if err != nil { panic(err) } - cmd.Flags().StringVarP(&r.outputFormat, "output", "o", "table", "The output format to use. One of: json|table|wide") return cmd } diff --git a/cli/cmd/vm_update_ttl.go b/cli/cmd/vm_update_ttl.go index a0eedd624..4c5a60ab8 100644 --- a/cli/cmd/vm_update_ttl.go +++ b/cli/cmd/vm_update_ttl.go @@ -33,7 +33,6 @@ replicated vm update ttl my-test-vm --ttl 30m`, parent.AddCommand(cmd) cmd.Flags().StringVar(&r.args.updateVMTTL, "ttl", "", "Update TTL which starts from the moment the vm is running (duration, max 48h).") - cmd.Flags().StringVarP(&r.outputFormat, "output", "o", "table", "The output format to use. One of: json|table|wide") cmd.MarkFlagRequired("ttl") diff --git a/cli/cmd/vm_versions.go b/cli/cmd/vm_versions.go index d87d33685..242c86939 100644 --- a/cli/cmd/vm_versions.go +++ b/cli/cmd/vm_versions.go @@ -31,7 +31,6 @@ replicated vm versions --output json`, parent.AddCommand(cmd) cmd.Flags().StringVar(&r.args.lsVersionsDistribution, "distribution", "", "Kubernetes distribution to filter by.") - cmd.Flags().StringVarP(&r.outputFormat, "output", "o", "table", "The output format to use. One of: json|table") return cmd } diff --git a/docs/plans/global-output-flag.md b/docs/plans/global-output-flag.md new file mode 100644 index 000000000..be97921dc --- /dev/null +++ b/docs/plans/global-output-flag.md @@ -0,0 +1,348 @@ +# Implementation Plan: Global `--output` Persistent Flag + +## Goal + +Introduce a global `--output` (`-o`) persistent flag on the root command that all subcommands inherit, replacing ~78 local `--output` flag registrations. Support a new `REPLICATED_OUTPUT` environment variable for setting a default output format without providing the flag on every command. + +## Resolution Order + +1. Explicit `--output` flag on the command line +2. `REPLICATED_OUTPUT` environment variable +3. Default: `"table"` + +## Files to Edit + +- `cli/cmd/runner.go` — add resolution method, clean up `runnerArgs` +- `cli/cmd/root.go` — register global persistent flag, wire resolution into pre-run hooks +- `cli/cmd/version.go` — migrate from `--json` bool flag to global `--output` flag +- ~70 command files — delete local `--output` flags, refactor local `outputFormat` variables to `r.outputFormat` + +--- + +## Phase 1: Core Infrastructure + +### 1.1 Add Output Resolution Method + +**File:** `cli/cmd/runner.go` + +Add the following method to the `runners` struct: + +```go +func (r *runners) resolveOutputFormat(cmd *cobra.Command) { + if cmd.Flags().Changed("output") { + return // explicit flag wins + } + if env := os.Getenv("REPLICATED_OUTPUT"); env != "" { + r.outputFormat = env + } +} +``` + +Also ensure `"os"` is imported if not already present. + +### 1.2 Register Global Persistent Flag + +**File:** `cli/cmd/root.go` + +Inside `Execute()`, after creating `runCmds`, add the persistent flag: + +```go +runCmds.rootCmd.PersistentFlags().StringVarP( + &runCmds.outputFormat, "output", "o", "table", + "The output format to use. Supported formats vary by command (json, table, wide). (default 'table', override with REPLICATED_OUTPUT env var)", +) +``` + +### 1.3 Wire Resolution into Pre-Run Hooks + +**File:** `cli/cmd/root.go` + +Add `runCmds.resolveOutputFormat(cmd)` at the beginning of both: + +- `preRunSetupAPIs` — used by app, registry, cluster, vm, network, api, model, policy, notification, config, default +- `prerunCommand` — used by channel, release, collector, installer, customer, instance, enterprise-portal, cluster-prepare + +Example addition in `preRunSetupAPIs`: + +```go +preRunSetupAPIs := func(cmd *cobra.Command, args []string) error { + runCmds.resolveOutputFormat(cmd) + // ... rest of existing logic +} +``` + +Same for `prerunCommand`. + +--- + +## Phase 2: Refactor Commands Using Local `outputFormat` Variables + +These commands declare a local `var outputFormat string` and pass it to helper methods. Remove the local variable, update the helper method signature to drop the `outputFormat` parameter, and use `r.outputFormat` directly inside the helper. + +| File | Local Var | Helper to Update | +|---|---|---| +| `app_ls.go` | `outputFormat` | `listApps(...)` → drop `outputFormat` param | +| `app_create.go` | `outputFormat` | `createApp(...)` → drop `outputFormat` param | +| `app_rm.go` | `outputFormat` | `deleteApp(...)` → drop `outputFormat` param | +| `app_hostname_ls.go` | `outputFormat` | `listAppHostnames(...)` → drop `outputFormat` param | +| `customer_ls.go` | `outputFormat` | `listCustomers(...)` → drop `outputFormat` param | +| `customer_create.go` | `outputFormat` | `createCustomer(...)` → drop `outputFormat` param | +| `customer_inspect.go` | `outputFormat` | `inspectCustomer(...)` → drop `outputFormat` param | +| `policy_ls.go` | `outputFormat` | `policyList(...)` → drop `outputFormat` param | +| `policy_get.go` | `outputFormat` | `policyGet(...)` → drop `outputFormat` param | +| `policy_create.go` | `outputFormat` | `policyCreate(...)` → drop `outputFormat` param | +| `policy_update.go` | `outputFormat` | `policyUpdate(...)` → drop `outputFormat` param | +| `default_show.go` | `outputFormat` | `defaultShow(...)` → drop `outputFormat` param | +| `default_set.go` | `outputFormat` | `defaultSet(...)` → drop `outputFormat` param | +| `enterpriseportal_status_get.go` | `outputFormat` | `enterprisePortalStatusGet(...)` → drop `outputFormat` param | +| `enterpriseportal_status_update.go` | `outputFormat` | `enterprisePortalStatusUpdate(...)` → drop `outputFormat` param | +| `enterprise_portal_invite.go` | `outputFormat` | `enterprisePortalInvite(...)` → drop `outputFormat` param | +| `enterprise_portal_user_ls.go` | `outputFormat` | `enterprisePortalUserLs(...)` → drop `outputFormat` param | + +### 2.1 Refactor `notification.go` + +All 10 notification subcommands use local `var outputFormat string` inside closures that already capture `r *runners`. For each subcommand: + +1. Remove `var outputFormat string` declaration +2. Remove `cmd.Flags().StringVarP(&outputFormat, "output", ...)` line +3. Inside the closure, replace `outputFormat` with `r.outputFormat` + +Affected functions in `notification.go`: +- `InitNotificationSubscriptionList` +- `InitNotificationSubscriptionGet` +- `InitNotificationSubscriptionCreate` +- `InitNotificationSubscriptionUpdate` +- `InitNotificationSubscriptionEvents` +- `InitNotificationEventList` +- `InitNotificationEventTypeList` +- `InitNotificationEmailResendVerification` +- `InitNotificationEmailVerify` +- `InitNotificationWebhookTest` + +### 2.2 Refactor `cluster_addon_ls.go` + +1. Remove `outputFormat string` from `clusterAddonLsArgs` struct +2. Remove `cmd.Flags().StringVarP(&args.outputFormat, ...)` line from `clusterAddonLsFlags` +3. Update `addonClusterLsRun` signature: `func (r *runners) addonClusterLsRun(clusterID string) error` +4. In `InitClusterAddonLs`, capture `args.clusterID` directly instead of passing the whole struct +5. Use `r.outputFormat` inside `addonClusterLsRun` instead of `args.outputFormat` + +### 2.3 Refactor `cluster_addon_create_objectstore.go` + +1. Remove `clusterAddonCreateObjectStoreOutput string` from `runnerArgs` struct in `runner.go` +2. Remove `cmd.Flags().StringVarP(&r.args.clusterAddonCreateObjectStoreOutput, ...)` from `clusterAddonCreateObjectStoreFlags` +3. In `clusterAddonCreateObjectStoreCreateRun`, replace `r.args.clusterAddonCreateObjectStoreOutput` with `r.outputFormat` + +--- + +## Phase 3: Delete Local `--output` Flags (Already Using `r.outputFormat`) + +These commands already bind their `--output` flag directly to `r.outputFormat`. Simply delete the `cmd.Flags().StringVarP(&r.outputFormat, "output", ...)` line in each file. + +**Files to edit:** + +- `release_ls.go` +- `release_create.go` +- `release_inspect.go` +- `release_lint.go` +- `installer_ls.go` +- `channel_ls.go` +- `channel_create.go` +- `channel_inspect.go` +- `channel_releases.go` +- `instance_ls.go` +- `instance_inspect.go` +- `instance_tag.go` +- `registry_ls.go` +- `registry_add_dockerhub.go` +- `registry_add_ecr.go` +- `registry_add_gar.go` +- `registry_add_gcr.go` +- `registry_add_ghcr.go` +- `registry_add_quay.go` +- `registry_add_other.go` +- `model_ls.go` +- `model_rm.go` +- `collection_ls.go` +- `collection_create.go` +- `collection_rm.go` +- `collection_addmodel.go` +- `collection_removemodel.go` +- `cluster_ls.go` +- `cluster_create.go` +- `cluster_upgrade.go` +- `cluster_versions.go` +- `cluster_update_ttl.go` +- `cluster_update_nodegroup.go` +- `cluster_nodegroup_ls.go` +- `cluster_port_ls.go` +- `cluster_port_expose.go` +- `cluster_port_rm.go` +- `vm_ls.go` +- `vm_create.go` +- `vm_versions.go` +- `vm_update_ttl.go` +- `vm_port_ls.go` +- `vm_port_expose.go` +- `vm_port_rm.go` +- `network_ls.go` +- `network_create.go` +- `customer_update.go` + +--- + +## Phase 4: Clean Up `runnerArgs` + +**File:** `cli/cmd/runner.go` + +Remove from `runnerArgs` struct: + +```go +clusterAddonCreateObjectStoreOutput string +``` + +--- + +## Phase 5: Migrate `version` Command to Global `--output` + +**File:** `cli/cmd/version.go` + +1. Remove `var versionJson bool` +2. Remove `cmd.Flags().BoolVar(&versionJson, "json", false, "output version info in json")` +3. Change function signature from `func Version() *cobra.Command` to `func (r *runners) Version() *cobra.Command` +4. Replace `if !versionJson` with `if r.outputFormat != "json"` +5. Replace `else` branch with JSON output + +**File:** `cli/cmd/root.go` + +Update the call from: +```go +runCmds.rootCmd.AddCommand(Version()) +``` +to: +```go +runCmds.rootCmd.AddCommand(runCmds.Version()) +``` + +**Note:** `version_upgrade.go` does not need changes — it has no output formatting logic. + +--- + +## Phase 6: Test Update Pass + +### 6.1 Existing Tests That Directly Set `runners.outputFormat` + +Tests in these files directly instantiate `&runners{outputFormat: "json"}` — they continue working unchanged: + +- `lint_test.go` +- `release_create_test.go` +- `vm_create_test.go` +- `cluster_ls_test.go` +- `notification_test.go` +- `release_lint_test.go` +- `release_image_ls_test.go` +- `image_extraction_test.go` +- `completion_test.go` +- `cluster_prepare_test.go` +- `cluster_create_test.go` + +No changes needed for these. + +### 6.2 New Tests to Add + +Create a new test file (e.g., `cli/cmd/output_test.go`) with tests for `resolveOutputFormat`: + +```go +func TestResolveOutputFormat_ExplicitFlagWins(t *testing.T) { + // Set env var + t.Setenv("REPLICATED_OUTPUT", "json") + + r := &runners{outputFormat: "table"} + cmd := &cobra.Command{} + cmd.Flags().StringP("output", "o", "table", "") + cmd.Flags().Set("output", "wide") + + r.resolveOutputFormat(cmd) + require.Equal(t, "wide", r.outputFormat) +} + +func TestResolveOutputFormat_EnvVarWinsOverDefault(t *testing.T) { + t.Setenv("REPLICATED_OUTPUT", "json") + + r := &runners{outputFormat: "table"} + cmd := &cobra.Command{} + cmd.Flags().StringP("output", "o", "table", "") + + r.resolveOutputFormat(cmd) + require.Equal(t, "json", r.outputFormat) +} + +func TestResolveOutputFormat_DefaultTable(t *testing.T) { + r := &runners{outputFormat: "table"} + cmd := &cobra.Command{} + cmd.Flags().StringP("output", "o", "table", "") + + r.resolveOutputFormat(cmd) + require.Equal(t, "table", r.outputFormat) +} +``` + +### 6.3 Compile Check + +After all edits, run: + +```bash +go build ./... +go test ./cli/cmd/... +``` + +--- + +## Phase 7: Verification & Exclusions + +### 7.1 Commands Intentionally Excluded + +These commands have an `--output` flag but it is **NOT** a format flag — do **NOT** modify them: + +| File | Flag Purpose | +|---|---| +| `customer_download_license.go` | `--output` is a **file path** (e.g., `--output license.yaml`) | + +These commands have no output formatting and need no changes: + +| File | Reason | +|---|---| +| `login.go` | No output formatting | +| `logout.go` | No output formatting | +| `completion.go` | Shell script generation, not tabular/json data | +| `config.go` | No output formatting | + +### 7.2 Per-Command Validation Stays + +Different commands support different format sets (`json|table` vs `json|table|wide`). Existing validation blocks like: + +```go +if r.outputFormat != "table" && r.outputFormat != "json" { + return errors.Errorf("invalid output: %s", r.outputFormat) +} +``` + +continue to work unchanged because they validate `r.outputFormat`, which is now set globally. + +--- + +## Checklist + +- [ ] `runner.go`: Add `resolveOutputFormat` method, remove `clusterAddonCreateObjectStoreOutput` from `runnerArgs` +- [ ] `root.go`: Add global `--output` persistent flag, wire `resolveOutputFormat` into `preRunSetupAPIs` and `prerunCommand` +- [ ] `version.go`: Migrate from `--json` bool to global `--output`, change to `(r *runners) Version()` +- [ ] `root.go`: Update `runCmds.Version()` call +- [ ] Refactor all commands with local `outputFormat` vars (Phase 2) +- [ ] Delete local `--output` flags from ~50 commands already using `r.outputFormat` (Phase 3) +- [ ] Add `output_test.go` with resolution tests (Phase 6) +- [ ] Run `go build ./...` — zero errors +- [ ] Run `go test ./cli/cmd/...` — all pass +- [ ] Verify `REPLICATED_OUTPUT=json` works without explicit `--output json` +- [ ] Verify explicit `--output wide` still overrides env var +- [ ] Verify `--output` appears in `replicated --help` Global Flags section +- [ ] Verify `customer download-license --output` still works as a file path flag From 5b4ccf0bd47f5f28762940f3fb7fa6b45b26f6f3 Mon Sep 17 00:00:00 2001 From: Gerard Nguyen Date: Thu, 21 May 2026 16:48:58 +1000 Subject: [PATCH 2/9] run lint --- cli/cmd/channel_create.go | 1 - cli/cmd/channel_inspect.go | 1 - cli/cmd/channel_ls.go | 1 - cli/cmd/cluster_create.go | 1 - cli/cmd/cluster_nodegroup_ls.go | 1 - cli/cmd/cluster_port_ls.go | 1 - cli/cmd/cluster_update_nodegroup.go | 1 - cli/cmd/cluster_upgrade.go | 1 - cli/cmd/collection_addmodel.go | 1 - cli/cmd/collection_create.go | 1 - cli/cmd/collection_removemodel.go | 1 - cli/cmd/collection_rm.go | 1 - cli/cmd/installer_ls.go | 1 - cli/cmd/instance_inspect.go | 1 - cli/cmd/instance_ls.go | 1 - cli/cmd/instance_tag.go | 1 - cli/cmd/network_create.go | 1 - cli/cmd/release_create.go | 2 -- cli/cmd/release_lint.go | 2 -- cli/cmd/release_ls.go | 1 - cli/cmd/vm_create.go | 1 - cli/cmd/vm_port_ls.go | 1 - 22 files changed, 24 deletions(-) diff --git a/cli/cmd/channel_create.go b/cli/cmd/channel_create.go index ee3beeead..2a58f93a7 100644 --- a/cli/cmd/channel_create.go +++ b/cli/cmd/channel_create.go @@ -18,7 +18,6 @@ func (r *runners) InitChannelCreate(parent *cobra.Command) { cmd.Flags().StringVar(&r.args.channelCreateName, "name", "", "The name of this channel") cmd.Flags().StringVar(&r.args.channelCreateDescription, "description", "", "A longer description of this channel") - cmd.RunE = r.channelCreate } diff --git a/cli/cmd/channel_inspect.go b/cli/cmd/channel_inspect.go index ea9968b49..f6d5b27d8 100644 --- a/cli/cmd/channel_inspect.go +++ b/cli/cmd/channel_inspect.go @@ -16,7 +16,6 @@ func (r *runners) InitChannelInspect(parent *cobra.Command) { } parent.AddCommand(cmd) - cmd.RunE = r.channelInspect } diff --git a/cli/cmd/channel_ls.go b/cli/cmd/channel_ls.go index c3e811227..baddd3c63 100644 --- a/cli/cmd/channel_ls.go +++ b/cli/cmd/channel_ls.go @@ -16,7 +16,6 @@ func (r *runners) InitChannelList(parent *cobra.Command) { parent.AddCommand(cmd) - cmd.RunE = r.channelList } diff --git a/cli/cmd/cluster_create.go b/cli/cmd/cluster_create.go index dd345a011..78301c0dd 100644 --- a/cli/cmd/cluster_create.go +++ b/cli/cmd/cluster_create.go @@ -89,7 +89,6 @@ replicated cluster create --distribution eks --version 1.21 --nodes 3 --addon ob cmd.Flags().BoolVar(&r.args.createClusterDryRun, "dry-run", false, "Dry run") - _ = cmd.MarkFlagRequired("distribution") return cmd diff --git a/cli/cmd/cluster_nodegroup_ls.go b/cli/cmd/cluster_nodegroup_ls.go index 05bbfbed2..ad1a4590c 100644 --- a/cli/cmd/cluster_nodegroup_ls.go +++ b/cli/cmd/cluster_nodegroup_ls.go @@ -31,7 +31,6 @@ replicated cluster nodegroup ls CLUSTER_ID_OR_NAME --output wide`, } parent.AddCommand(cmd) - return cmd } diff --git a/cli/cmd/cluster_port_ls.go b/cli/cmd/cluster_port_ls.go index 1d9d70ead..511359a11 100644 --- a/cli/cmd/cluster_port_ls.go +++ b/cli/cmd/cluster_port_ls.go @@ -27,7 +27,6 @@ replicated cluster port ls CLUSTER_ID_OR_NAME --output wide`, } parent.AddCommand(cmd) - return cmd } diff --git a/cli/cmd/cluster_update_nodegroup.go b/cli/cmd/cluster_update_nodegroup.go index 70060962f..9dda94650 100644 --- a/cli/cmd/cluster_update_nodegroup.go +++ b/cli/cmd/cluster_update_nodegroup.go @@ -34,7 +34,6 @@ replicated cluster update nodegroup CLUSTER_ID_OR_NAME --nodegroup-id NODEGROUP_ cmd.Flags().StringVar(&r.args.updateClusterNodeGroupMinCount, "min-nodes", "", "The minimum number of nodes in the nodegroup") cmd.Flags().StringVar(&r.args.updateClusterNodeGroupMaxCount, "max-nodes", "", "The maximum number of nodes in the nodegroup") - return cmd } diff --git a/cli/cmd/cluster_upgrade.go b/cli/cmd/cluster_upgrade.go index ddb34484c..e1eca343d 100644 --- a/cli/cmd/cluster_upgrade.go +++ b/cli/cmd/cluster_upgrade.go @@ -36,7 +36,6 @@ replicated cluster upgrade CLUSTER_ID_OR_NAME --version 1.31 --wait 30m`, cmd.Flags().BoolVar(&r.args.upgradeClusterDryRun, "dry-run", false, "Dry run") cmd.Flags().DurationVar(&r.args.upgradeClusterWaitDuration, "wait", 0, "Wait duration for cluster to be ready (leave empty to not wait)") - _ = cmd.MarkFlagRequired("version") return cmd diff --git a/cli/cmd/collection_addmodel.go b/cli/cmd/collection_addmodel.go index 280093dbd..966a1d035 100644 --- a/cli/cmd/collection_addmodel.go +++ b/cli/cmd/collection_addmodel.go @@ -22,7 +22,6 @@ func (r *runners) InitCollectionAddModel(parent *cobra.Command) *cobra.Command { cmd.Flags().StringVar(&r.args.modelCollectionAddModelCollectionID, "collection-id", "", "The ID of the collection") cmd.MarkFlagRequired("collection-id") - return cmd } diff --git a/cli/cmd/collection_create.go b/cli/cmd/collection_create.go index df6f889fe..876f7f75e 100644 --- a/cli/cmd/collection_create.go +++ b/cli/cmd/collection_create.go @@ -21,7 +21,6 @@ func (r *runners) InitCollectionCreate(parent *cobra.Command) *cobra.Command { cmd.Flags().StringVar(&r.args.modelCollectionCreateName, "name", "", "The name of the collection") cmd.MarkFlagRequired("name") - return cmd } diff --git a/cli/cmd/collection_removemodel.go b/cli/cmd/collection_removemodel.go index 75be42e14..854b84370 100644 --- a/cli/cmd/collection_removemodel.go +++ b/cli/cmd/collection_removemodel.go @@ -22,7 +22,6 @@ func (r *runners) InitCollectionRemoveModel(parent *cobra.Command) *cobra.Comman cmd.Flags().StringVar(&r.args.modelCollectionRmModelCollectionID, "collection-id", "", "The ID of the collection") cmd.MarkFlagRequired("collection-id") - return cmd } diff --git a/cli/cmd/collection_rm.go b/cli/cmd/collection_rm.go index 0952cdfb9..c78499706 100644 --- a/cli/cmd/collection_rm.go +++ b/cli/cmd/collection_rm.go @@ -18,7 +18,6 @@ func (r *runners) InitCollectionRemove(parent *cobra.Command) *cobra.Command { } parent.AddCommand(cmd) - return cmd } diff --git a/cli/cmd/installer_ls.go b/cli/cmd/installer_ls.go index 870c0eedc..7b91105ae 100644 --- a/cli/cmd/installer_ls.go +++ b/cli/cmd/installer_ls.go @@ -16,7 +16,6 @@ func (r *runners) InitInstallerList(parent *cobra.Command) { parent.AddCommand(cmd) - cmd.RunE = r.installerList } diff --git a/cli/cmd/instance_inspect.go b/cli/cmd/instance_inspect.go index 0eeea2a21..745092e69 100644 --- a/cli/cmd/instance_inspect.go +++ b/cli/cmd/instance_inspect.go @@ -20,7 +20,6 @@ func (r *runners) InitInstanceInspectCommand(parent *cobra.Command) *cobra.Comma cmd.Flags().StringVar(&r.args.instanceInspectCustomer, "customer", "", "Customer Name or ID") cmd.Flags().StringVar(&r.args.instanceInspectInstance, "instance", "", "Instance Name or ID") - return cmd } diff --git a/cli/cmd/instance_ls.go b/cli/cmd/instance_ls.go index 82b6536d9..b8aa85483 100644 --- a/cli/cmd/instance_ls.go +++ b/cli/cmd/instance_ls.go @@ -21,7 +21,6 @@ func (r *runners) InitInstanceLSCommand(parent *cobra.Command) *cobra.Command { cmd.Flags().StringVar(&r.args.instanceListCustomer, "customer", "", "Customer Name or ID") cmd.Flags().StringArrayVar(&r.args.instanceListTags, "tag", []string{}, "Tags to use to filter instances (key=value format, can be specified multiple times). Only one tag needs to match (an OR operation)") - return cmd } diff --git a/cli/cmd/instance_tag.go b/cli/cmd/instance_tag.go index 98df9ff68..33d6ecb68 100644 --- a/cli/cmd/instance_tag.go +++ b/cli/cmd/instance_tag.go @@ -20,7 +20,6 @@ func (r *runners) InitInstanceTagCommand(parent *cobra.Command) *cobra.Command { cmd.Flags().StringVar(&r.args.instanceTagInstacne, "instance", "", "Instance Name or ID") cmd.Flags().StringArrayVar(&r.args.instanceTagTags, "tag", []string{}, "Tags to apply to instance. Leave value empty to remove tag. Tags not specified will not be removed.") - return cmd } diff --git a/cli/cmd/network_create.go b/cli/cmd/network_create.go index 1f1707c91..84994c054 100644 --- a/cli/cmd/network_create.go +++ b/cli/cmd/network_create.go @@ -32,7 +32,6 @@ func (r *runners) InitNetworkCreate(parent *cobra.Command) *cobra.Command { cmd.Flags().BoolVar(&r.args.createNetworkDryRun, "dry-run", false, "Dry run") - return cmd } diff --git a/cli/cmd/release_create.go b/cli/cmd/release_create.go index 33816f21a..0f5d0713e 100644 --- a/cli/cmd/release_create.go +++ b/cli/cmd/release_create.go @@ -80,8 +80,6 @@ replicated release create --version 1.0.0 --promote Unstable --required`, cmd.Flags().StringVar(&r.args.createReleaseOutputDir, "output-dir", "", "Stage the release artifacts (packaged charts and manifests) to this directory. Existing contents of the directory are removed before each run. The directory is preserved after the command completes.") cmd.Flags().BoolVar(&r.args.createReleaseNoUpload, "no-upload", false, "Build the release locally but do not upload it. Use with --output-dir to inspect or reuse the staged artifacts. Cannot be used with --promote.") - - // not supported for KOTS cmd.Flags().MarkHidden("yaml-file") cmd.Flags().MarkHidden("yaml") diff --git a/cli/cmd/release_lint.go b/cli/cmd/release_lint.go index 95b000361..2132eab7a 100644 --- a/cli/cmd/release_lint.go +++ b/cli/cmd/release_lint.go @@ -42,8 +42,6 @@ func (r *runners) InitReleaseLint(parent *cobra.Command) { // New flags (for local lint - when flag=1) cmd.Flags().BoolVarP(&r.args.lintVerbose, "verbose", "v", false, "Show detailed output including extracted container images (local lint only)") - - cmd.Flags().MarkHidden("chart") cmd.RunE = r.releaseLint diff --git a/cli/cmd/release_ls.go b/cli/cmd/release_ls.go index 36adbe41d..7c9b13fa3 100644 --- a/cli/cmd/release_ls.go +++ b/cli/cmd/release_ls.go @@ -16,7 +16,6 @@ func (r *runners) IniReleaseList(parent *cobra.Command) { parent.AddCommand(cmd) - cmd.RunE = r.releaseList } diff --git a/cli/cmd/vm_create.go b/cli/cmd/vm_create.go index 9f9b302cb..199e79776 100644 --- a/cli/cmd/vm_create.go +++ b/cli/cmd/vm_create.go @@ -90,7 +90,6 @@ ssh -i /tmp/ci_key $(replicated vm ssh-endpoint my-vm --username ci)`, cmd.Flags().BoolVar(&r.args.createVMDryRun, "dry-run", false, "Dry run") - _ = cmd.MarkFlagRequired("distribution") return cmd diff --git a/cli/cmd/vm_port_ls.go b/cli/cmd/vm_port_ls.go index 6ee590537..07b948663 100644 --- a/cli/cmd/vm_port_ls.go +++ b/cli/cmd/vm_port_ls.go @@ -28,7 +28,6 @@ replicated vm port ls VM_ID_OR_NAME --output wide`, } parent.AddCommand(cmd) - return cmd } From f06cab0faedade4e7f8e182033d558ee28c181b0 Mon Sep 17 00:00:00 2001 From: Gerard Nguyen Date: Fri, 22 May 2026 08:11:42 +1000 Subject: [PATCH 3/9] bug bot fix --- cli/cmd/version.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cli/cmd/version.go b/cli/cmd/version.go index c439eab5e..4b2337c59 100644 --- a/cli/cmd/version.go +++ b/cli/cmd/version.go @@ -15,6 +15,8 @@ func (r *runners) Version() *cobra.Command { Short: "Print the current version and exit", Long: `Print the current version and exit`, RunE: func(cmd *cobra.Command, args []string) error { + r.resolveOutputFormat(cmd) + currentVersion := version.Version() // For version command, do a synchronous update check From 413c383ab9da5b713ae676b6fdf4f365d3665e0a Mon Sep 17 00:00:00 2001 From: Gerard Nguyen Date: Fri, 22 May 2026 11:16:34 +1000 Subject: [PATCH 4/9] - respect proxy settings - add JSON output for release promote - hide usage text for channel adoption and counts --- cli/cmd/channel_adoption.go | 7 ++++--- cli/cmd/channel_counts.go | 7 ++++--- cli/cmd/release_promote.go | 26 +++++++++++++++++++++++++- pkg/platformclient/client.go | 1 + 4 files changed, 34 insertions(+), 7 deletions(-) diff --git a/cli/cmd/channel_adoption.go b/cli/cmd/channel_adoption.go index 415c5f6fe..5925bc1f6 100644 --- a/cli/cmd/channel_adoption.go +++ b/cli/cmd/channel_adoption.go @@ -9,9 +9,10 @@ import ( func (r *runners) InitChannelAdoption(parent *cobra.Command) { cmd := &cobra.Command{ - Use: "adoption CHANNEL_ID", - Short: "Print channel adoption statistics by license type", - Long: "Print channel adoption statistics by license type", + Use: "adoption CHANNEL_ID", + Short: "Print channel adoption statistics by license type", + Long: "Print channel adoption statistics by license type", + SilenceUsage: true, } cmd.Hidden = true // Not supported in KOTS parent.AddCommand(cmd) diff --git a/cli/cmd/channel_counts.go b/cli/cmd/channel_counts.go index bcf7f7a50..9cdd0f5be 100644 --- a/cli/cmd/channel_counts.go +++ b/cli/cmd/channel_counts.go @@ -9,9 +9,10 @@ import ( func (r *runners) InitChannelCounts(parent *cobra.Command) { cmd := &cobra.Command{ - Use: "counts CHANNEL_ID", - Short: "Print channel license counts", - Long: "Print channel license counts", + Use: "counts CHANNEL_ID", + Short: "Print channel license counts", + Long: "Print channel license counts", + SilenceUsage: true, } cmd.Hidden = true // Not supported in KOTS parent.AddCommand(cmd) diff --git a/cli/cmd/release_promote.go b/cli/cmd/release_promote.go index 37798c9a0..2eedb9128 100644 --- a/cli/cmd/release_promote.go +++ b/cli/cmd/release_promote.go @@ -1,6 +1,7 @@ package cmd import ( + "encoding/json" "fmt" "strconv" "time" @@ -102,10 +103,33 @@ func (r *runners) releasePromote(cmd *cobra.Command, args []string) (err error) return errors.Wrapf(err, "failed to promote release") } - fmt.Fprintf(r.w, "Channel %s successfully set to release %d\n", channelName, seq) + if r.outputFormat == "json" { + log := logger.NewLogger(r.w).SetIsTerminal(r.stdoutIsTTY) + log.Silence() + + out := struct { + Channel string `json:"channel"` + ReleaseSequence int64 `json:"release_sequence"` + VersionLabel string `json:"version_label"` + }{ + Channel: newID, + ReleaseSequence: seq, + VersionLabel: r.args.releaseVersion, + } + enc := json.NewEncoder(r.w) + enc.SetIndent("", " ") + if err := enc.Encode(out); err != nil { + return errors.Wrap(err, "encode json output") + } + } else { + fmt.Fprintf(r.w, "Channel %s successfully set to release %d\n", channelName, seq) + } if r.appType == "kots" && r.args.releasePromoteWaitForAirgap { log := logger.NewLogger(r.w).SetIsTerminal(r.stdoutIsTTY) + if r.outputFormat == "json" { + log.Silence() + } if err := r.waitForAirgapBuilds(promoteResp, r.args.releasePromoteWaitForAirgapTimeout, log); err != nil { return err } diff --git a/pkg/platformclient/client.go b/pkg/platformclient/client.go index dbbc9f20a..75f77cc5a 100644 --- a/pkg/platformclient/client.go +++ b/pkg/platformclient/client.go @@ -29,6 +29,7 @@ var ( // This is a singleton that's reused for all requests to avoid leaking connections. var httpClient = &http.Client{ Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { // Check if the address is a .localhost domain host, port, err := net.SplitHostPort(addr) From 67454cd5d216a0e2e1be2c24b5dd10ee6ad404cc Mon Sep 17 00:00:00 2001 From: Gerard Nguyen Date: Fri, 22 May 2026 11:23:02 +1000 Subject: [PATCH 5/9] mark verison --json as deprecated --- cli/cmd/version.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/cli/cmd/version.go b/cli/cmd/version.go index 4b2337c59..f2858aa3a 100644 --- a/cli/cmd/version.go +++ b/cli/cmd/version.go @@ -10,6 +10,7 @@ import ( ) func (r *runners) Version() *cobra.Command { + var versionJson bool cmd := &cobra.Command{ Use: "version", Short: "Print the current version and exit", @@ -17,6 +18,10 @@ func (r *runners) Version() *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { r.resolveOutputFormat(cmd) + if cmd.Flags().Changed("json") { + r.outputFormat = "json" + } + currentVersion := version.Version() // For version command, do a synchronous update check @@ -63,6 +68,9 @@ func (r *runners) Version() *cobra.Command { }, } + cmd.Flags().BoolVar(&versionJson, "json", false, "output version info in json") + _ = cmd.Flags().MarkDeprecated("json", "use --output json instead") + cmd.AddCommand(versionUpgradeCmd()) return cmd From 8feb2686ea2298c93166f400a3c85dbee0151851 Mon Sep 17 00:00:00 2001 From: Gerard Nguyen Date: Fri, 22 May 2026 12:13:05 +1000 Subject: [PATCH 6/9] release image ls output --- cli/cmd/release_image_ls.go | 2 +- cli/print/channel_images.go | 31 ++++++++++++----- cli/print/channel_images_test.go | 60 ++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 9 deletions(-) create mode 100644 cli/print/channel_images_test.go diff --git a/cli/cmd/release_image_ls.go b/cli/cmd/release_image_ls.go index c5b5b968c..6513a1e1a 100644 --- a/cli/cmd/release_image_ls.go +++ b/cli/cmd/release_image_ls.go @@ -126,7 +126,7 @@ func (r *runners) releaseImageLS(cmd *cobra.Command, args []string) error { } // Print images - return print.ChannelImages(r.w, images) + return print.ChannelImages(r.outputFormat, r.w, images) } func cleanImageName(image string, proxyRegistryDomain string) string { diff --git a/cli/print/channel_images.go b/cli/print/channel_images.go index 4116b76c4..56f08fa95 100644 --- a/cli/print/channel_images.go +++ b/cli/print/channel_images.go @@ -1,22 +1,37 @@ package print import ( + "encoding/json" "fmt" "sort" "text/tabwriter" ) -func ChannelImages(w *tabwriter.Writer, images []string) error { +func ChannelImages(format string, w *tabwriter.Writer, images []string) error { // Sort images for consistent output sort.Strings(images) - // Print header - fmt.Fprintln(w, "IMAGE") + switch format { + case "json": + out, err := json.MarshalIndent(images, "", " ") + if err != nil { + return err + } + if _, err := fmt.Fprintln(w, string(out)); err != nil { + return err + } + return w.Flush() + case "table": + // Print header + fmt.Fprintln(w, "IMAGE") - // Print each image - for _, image := range images { - fmt.Fprintln(w, image) - } + // Print each image + for _, image := range images { + fmt.Fprintln(w, image) + } - return w.Flush() + return w.Flush() + default: + return fmt.Errorf("unknown format: %s", format) + } } diff --git a/cli/print/channel_images_test.go b/cli/print/channel_images_test.go new file mode 100644 index 000000000..b4ce87144 --- /dev/null +++ b/cli/print/channel_images_test.go @@ -0,0 +1,60 @@ +package print + +import ( + "bytes" + "encoding/json" + "testing" + "text/tabwriter" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestChannelImages_Table(t *testing.T) { + images := []string{ + "nginx:1.27", + "postgres:14", + } + + var out bytes.Buffer + w := tabwriter.NewWriter(&out, 0, 8, 4, ' ', tabwriter.TabIndent) + require.NoError(t, ChannelImages("table", w, images)) + + got := out.String() + assert.Contains(t, got, "IMAGE") + assert.Contains(t, got, "nginx:1.27") + assert.Contains(t, got, "postgres:14") +} + +func TestChannelImages_JSON(t *testing.T) { + images := []string{ + "nginx:1.27", + "postgres:14", + } + + var out bytes.Buffer + w := tabwriter.NewWriter(&out, 0, 8, 4, ' ', tabwriter.TabIndent) + require.NoError(t, ChannelImages("json", w, images)) + + var decoded []string + require.NoError(t, json.Unmarshal(out.Bytes(), &decoded)) + assert.Equal(t, images, decoded) +} + +func TestChannelImages_Empty_JSON(t *testing.T) { + var out bytes.Buffer + w := tabwriter.NewWriter(&out, 0, 8, 4, ' ', tabwriter.TabIndent) + require.NoError(t, ChannelImages("json", w, []string{})) + + var decoded []string + require.NoError(t, json.Unmarshal(out.Bytes(), &decoded)) + assert.Empty(t, decoded) +} + +func TestChannelImages_UnknownFormat(t *testing.T) { + var out bytes.Buffer + w := tabwriter.NewWriter(&out, 0, 8, 4, ' ', tabwriter.TabIndent) + err := ChannelImages("yaml", w, []string{"nginx:1.27"}) + require.Error(t, err) + assert.Contains(t, err.Error(), "unknown format: yaml") +} From bdb47a93346cbcd6673b4cdfa7ff1329bc0d4667 Mon Sep 17 00:00:00 2001 From: Gerard Nguyen Date: Fri, 22 May 2026 12:18:49 +1000 Subject: [PATCH 7/9] channel adoption and channel counts --- cli/cmd/channel_adoption.go | 2 +- cli/cmd/channel_counts.go | 2 +- cli/print/channel_adoption.go | 91 ++++++++++++++---------- cli/print/channel_adoption_test.go | 83 +++++++++++++++++++++ cli/print/channel_license_counts.go | 73 +++++++++++-------- cli/print/channel_license_counts_test.go | 81 +++++++++++++++++++++ 6 files changed, 263 insertions(+), 69 deletions(-) create mode 100644 cli/print/channel_adoption_test.go create mode 100644 cli/print/channel_license_counts_test.go diff --git a/cli/cmd/channel_adoption.go b/cli/cmd/channel_adoption.go index 5925bc1f6..004756b5a 100644 --- a/cli/cmd/channel_adoption.go +++ b/cli/cmd/channel_adoption.go @@ -35,7 +35,7 @@ func (r *runners) channelAdoption(cmd *cobra.Command, args []string) error { return err } - if err = print.ChannelAdoption(r.w, appChan.Adoption); err != nil { + if err = print.ChannelAdoption(r.outputFormat, r.w, appChan.Adoption); err != nil { return err } diff --git a/cli/cmd/channel_counts.go b/cli/cmd/channel_counts.go index 9cdd0f5be..138f64962 100644 --- a/cli/cmd/channel_counts.go +++ b/cli/cmd/channel_counts.go @@ -35,7 +35,7 @@ func (r *runners) channelCounts(cmd *cobra.Command, args []string) error { return err } - if err = print.LicenseCounts(r.w, appChan.LicenseCounts); err != nil { + if err = print.LicenseCounts(r.outputFormat, r.w, appChan.LicenseCounts); err != nil { return err } } else if r.appType == "kots" { diff --git a/cli/print/channel_adoption.go b/cli/print/channel_adoption.go index fd286cc13..3e52f2806 100644 --- a/cli/print/channel_adoption.go +++ b/cli/print/channel_adoption.go @@ -1,6 +1,7 @@ package print import ( + "encoding/json" "fmt" "text/tabwriter" "text/template" @@ -28,52 +29,66 @@ type licenseAdoption struct { Other allActiveCounts } -func ChannelAdoption(w *tabwriter.Writer, adoption *channels.ChannelAdoption) error { - countsByLicense := make(map[string]*licenseAdoption) +func ChannelAdoption(format string, w *tabwriter.Writer, adoption *channels.ChannelAdoption) error { + switch format { + case "json": + out, err := json.MarshalIndent(adoption, "", " ") + if err != nil { + return err + } + if _, err := fmt.Fprintln(w, string(out)); err != nil { + return err + } + return w.Flush() + case "table": + countsByLicense := make(map[string]*licenseAdoption) - var getOrSetLicenseAdoption = func(licenseType string) *licenseAdoption { - la, ok := countsByLicense[licenseType] - if !ok { - la = &licenseAdoption{} - countsByLicense[licenseType] = la + var getOrSetLicenseAdoption = func(licenseType string) *licenseAdoption { + la, ok := countsByLicense[licenseType] + if !ok { + la = &licenseAdoption{} + countsByLicense[licenseType] = la + } + return la } - return la - } - // current - for licenseType, count := range adoption.CurrentVersionCountActive { - getOrSetLicenseAdoption(licenseType).Current.Active = count - } - for licenseType, count := range adoption.CurrentVersionCountAll { - getOrSetLicenseAdoption(licenseType).Current.All = count - } + // current + for licenseType, count := range adoption.CurrentVersionCountActive { + getOrSetLicenseAdoption(licenseType).Current.Active = count + } + for licenseType, count := range adoption.CurrentVersionCountAll { + getOrSetLicenseAdoption(licenseType).Current.All = count + } - // previous - for licenseType, count := range adoption.PreviousVersionCountActive { - getOrSetLicenseAdoption(licenseType).Previous.Active = count - } - for licenseType, count := range adoption.PreviousVersionCountAll { - getOrSetLicenseAdoption(licenseType).Previous.All = count - } + // previous + for licenseType, count := range adoption.PreviousVersionCountActive { + getOrSetLicenseAdoption(licenseType).Previous.Active = count + } + for licenseType, count := range adoption.PreviousVersionCountAll { + getOrSetLicenseAdoption(licenseType).Previous.All = count + } - // other - for licenseType, count := range adoption.OtherVersionCountActive { - getOrSetLicenseAdoption(licenseType).Other.Active = count - } - for licenseType, count := range adoption.OtherVersionCountAll { - getOrSetLicenseAdoption(licenseType).Other.All = count - } + // other + for licenseType, count := range adoption.OtherVersionCountActive { + getOrSetLicenseAdoption(licenseType).Other.Active = count + } + for licenseType, count := range adoption.OtherVersionCountAll { + getOrSetLicenseAdoption(licenseType).Other.All = count + } - if len(countsByLicense) == 0 { - if _, err := fmt.Fprintln(w, "No active licenses in channel"); err != nil { + if len(countsByLicense) == 0 { + if _, err := fmt.Fprintln(w, "No active licenses in channel"); err != nil { + return err + } + return w.Flush() + } + + if err := channelAdoptionTmpl.Execute(w, countsByLicense); err != nil { return err } - return w.Flush() - } - if err := channelAdoptionTmpl.Execute(w, countsByLicense); err != nil { - return err + return w.Flush() + default: + return fmt.Errorf("unknown format: %s", format) } - - return w.Flush() } diff --git a/cli/print/channel_adoption_test.go b/cli/print/channel_adoption_test.go new file mode 100644 index 000000000..f85610bd4 --- /dev/null +++ b/cli/print/channel_adoption_test.go @@ -0,0 +1,83 @@ +package print + +import ( + "bytes" + "encoding/json" + "testing" + "text/tabwriter" + + channels "github.com/replicatedhq/replicated/gen/go/v1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestChannelAdoption_Table(t *testing.T) { + adoption := &channels.ChannelAdoption{ + CurrentVersionCountActive: map[string]int64{"paid": 5, "trial": 3}, + CurrentVersionCountAll: map[string]int64{"paid": 10, "trial": 6}, + PreviousVersionCountActive: map[string]int64{"paid": 2}, + PreviousVersionCountAll: map[string]int64{"paid": 4}, + OtherVersionCountActive: map[string]int64{"trial": 1}, + OtherVersionCountAll: map[string]int64{"trial": 2}, + } + + var out bytes.Buffer + w := tabwriter.NewWriter(&out, 0, 8, 4, ' ', tabwriter.TabIndent) + require.NoError(t, ChannelAdoption("table", w, adoption)) + + got := out.String() + assert.Contains(t, got, "LICENSE_TYPE") + assert.Contains(t, got, "CURRENT") + assert.Contains(t, got, "PREVIOUS") + assert.Contains(t, got, "OTHER") + assert.Contains(t, got, "paid") + assert.Contains(t, got, "trial") +} + +func TestChannelAdoption_JSON(t *testing.T) { + adoption := &channels.ChannelAdoption{ + CurrentVersionCountActive: map[string]int64{"paid": 5}, + CurrentVersionCountAll: map[string]int64{"paid": 10}, + PreviousVersionCountActive: map[string]int64{}, + PreviousVersionCountAll: map[string]int64{}, + OtherVersionCountActive: map[string]int64{}, + OtherVersionCountAll: map[string]int64{}, + } + + var out bytes.Buffer + w := tabwriter.NewWriter(&out, 0, 8, 4, ' ', tabwriter.TabIndent) + require.NoError(t, ChannelAdoption("json", w, adoption)) + + var decoded channels.ChannelAdoption + require.NoError(t, json.Unmarshal(out.Bytes(), &decoded)) + assert.Equal(t, int64(5), decoded.CurrentVersionCountActive["paid"]) + assert.Equal(t, int64(10), decoded.CurrentVersionCountAll["paid"]) +} + +func TestChannelAdoption_Empty_Table(t *testing.T) { + adoption := &channels.ChannelAdoption{} + + var out bytes.Buffer + w := tabwriter.NewWriter(&out, 0, 8, 4, ' ', tabwriter.TabIndent) + require.NoError(t, ChannelAdoption("table", w, adoption)) + + assert.Contains(t, out.String(), "No active licenses in channel") +} + +func TestChannelAdoption_Empty_JSON(t *testing.T) { + adoption := &channels.ChannelAdoption{} + + var out bytes.Buffer + w := tabwriter.NewWriter(&out, 0, 8, 4, ' ', tabwriter.TabIndent) + require.NoError(t, ChannelAdoption("json", w, adoption)) + + assert.Equal(t, "{}\n", out.String()) +} + +func TestChannelAdoption_UnknownFormat(t *testing.T) { + var out bytes.Buffer + w := tabwriter.NewWriter(&out, 0, 8, 4, ' ', tabwriter.TabIndent) + err := ChannelAdoption("yaml", w, &channels.ChannelAdoption{}) + require.Error(t, err) + assert.Contains(t, err.Error(), "unknown format: yaml") +} diff --git a/cli/print/channel_license_counts.go b/cli/print/channel_license_counts.go index c2413e75b..a252d169b 100644 --- a/cli/print/channel_license_counts.go +++ b/cli/print/channel_license_counts.go @@ -1,6 +1,7 @@ package print import ( + "encoding/json" "fmt" "text/tabwriter" "text/template" @@ -20,41 +21,55 @@ type licenseTypeCounts struct { Active, Airgap, Inactive, Total int64 } -func LicenseCounts(w *tabwriter.Writer, counts *channels.LicenseCounts) error { - countsByLicenseType := make(map[string]*licenseTypeCounts) +func LicenseCounts(format string, w *tabwriter.Writer, counts *channels.LicenseCounts) error { + switch format { + case "json": + out, err := json.MarshalIndent(counts, "", " ") + if err != nil { + return err + } + if _, err := fmt.Fprintln(w, string(out)); err != nil { + return err + } + return w.Flush() + case "table": + countsByLicenseType := make(map[string]*licenseTypeCounts) - var getOrSetLicenseCounts = func(licenseType string) *licenseTypeCounts { - licenseCounts, ok := countsByLicenseType[licenseType] - if !ok { - licenseCounts = &licenseTypeCounts{} - countsByLicenseType[licenseType] = licenseCounts + var getOrSetLicenseCounts = func(licenseType string) *licenseTypeCounts { + licenseCounts, ok := countsByLicenseType[licenseType] + if !ok { + licenseCounts = &licenseTypeCounts{} + countsByLicenseType[licenseType] = licenseCounts + } + return licenseCounts } - return licenseCounts - } - for licenseType, count := range counts.Active { - getOrSetLicenseCounts(licenseType).Active = count - } - for licenseType, count := range counts.Airgap { - getOrSetLicenseCounts(licenseType).Airgap = count - } - for licenseType, count := range counts.Inactive { - getOrSetLicenseCounts(licenseType).Inactive = count - } - for licenseType, count := range counts.Total { - getOrSetLicenseCounts(licenseType).Total = count - } + for licenseType, count := range counts.Active { + getOrSetLicenseCounts(licenseType).Active = count + } + for licenseType, count := range counts.Airgap { + getOrSetLicenseCounts(licenseType).Airgap = count + } + for licenseType, count := range counts.Inactive { + getOrSetLicenseCounts(licenseType).Inactive = count + } + for licenseType, count := range counts.Total { + getOrSetLicenseCounts(licenseType).Total = count + } + + if len(countsByLicenseType) == 0 { + if _, err := fmt.Fprintln(w, "No active licenses in channel"); err != nil { + return err + } + return w.Flush() + } - if len(countsByLicenseType) == 0 { - if _, err := fmt.Fprintln(w, "No active licenses in channel"); err != nil { + if err := channelLicenseCountsTmpl.Execute(w, countsByLicenseType); err != nil { return err } - return w.Flush() - } - if err := channelLicenseCountsTmpl.Execute(w, countsByLicenseType); err != nil { - return err + return w.Flush() + default: + return fmt.Errorf("unknown format: %s", format) } - - return w.Flush() } diff --git a/cli/print/channel_license_counts_test.go b/cli/print/channel_license_counts_test.go new file mode 100644 index 000000000..20748b4fd --- /dev/null +++ b/cli/print/channel_license_counts_test.go @@ -0,0 +1,81 @@ +package print + +import ( + "bytes" + "encoding/json" + "testing" + "text/tabwriter" + + channels "github.com/replicatedhq/replicated/gen/go/v1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestLicenseCounts_Table(t *testing.T) { + counts := &channels.LicenseCounts{ + Active: map[string]int64{"paid": 5, "trial": 3}, + Airgap: map[string]int64{"paid": 2}, + Inactive: map[string]int64{"trial": 1}, + Total: map[string]int64{"paid": 7, "trial": 4}, + } + + var out bytes.Buffer + w := tabwriter.NewWriter(&out, 0, 8, 4, ' ', tabwriter.TabIndent) + require.NoError(t, LicenseCounts("table", w, counts)) + + got := out.String() + assert.Contains(t, got, "LICENSE_TYPE") + assert.Contains(t, got, "ACTIVE") + assert.Contains(t, got, "AIRGAP") + assert.Contains(t, got, "INACTIVE") + assert.Contains(t, got, "TOTAL") + assert.Contains(t, got, "paid") + assert.Contains(t, got, "trial") +} + +func TestLicenseCounts_JSON(t *testing.T) { + counts := &channels.LicenseCounts{ + Active: map[string]int64{"paid": 5}, + Airgap: map[string]int64{"paid": 2}, + Inactive: map[string]int64{}, + Total: map[string]int64{"paid": 7}, + } + + var out bytes.Buffer + w := tabwriter.NewWriter(&out, 0, 8, 4, ' ', tabwriter.TabIndent) + require.NoError(t, LicenseCounts("json", w, counts)) + + var decoded channels.LicenseCounts + require.NoError(t, json.Unmarshal(out.Bytes(), &decoded)) + assert.Equal(t, int64(5), decoded.Active["paid"]) + assert.Equal(t, int64(2), decoded.Airgap["paid"]) + assert.Equal(t, int64(7), decoded.Total["paid"]) +} + +func TestLicenseCounts_Empty_Table(t *testing.T) { + counts := &channels.LicenseCounts{} + + var out bytes.Buffer + w := tabwriter.NewWriter(&out, 0, 8, 4, ' ', tabwriter.TabIndent) + require.NoError(t, LicenseCounts("table", w, counts)) + + assert.Contains(t, out.String(), "No active licenses in channel") +} + +func TestLicenseCounts_Empty_JSON(t *testing.T) { + counts := &channels.LicenseCounts{} + + var out bytes.Buffer + w := tabwriter.NewWriter(&out, 0, 8, 4, ' ', tabwriter.TabIndent) + require.NoError(t, LicenseCounts("json", w, counts)) + + assert.Equal(t, "{}\n", out.String()) +} + +func TestLicenseCounts_UnknownFormat(t *testing.T) { + var out bytes.Buffer + w := tabwriter.NewWriter(&out, 0, 8, 4, ' ', tabwriter.TabIndent) + err := LicenseCounts("yaml", w, &channels.LicenseCounts{}) + require.Error(t, err) + assert.Contains(t, err.Error(), "unknown format: yaml") +} From 19169ad25717d141e235fe268337e9485c4a043a Mon Sep 17 00:00:00 2001 From: Gerard Nguyen Date: Fri, 22 May 2026 13:57:56 +1000 Subject: [PATCH 8/9] docs: remove global-output-flag.md plan doc --- .gitignore | 1 + docs/plans/global-output-flag.md | 348 ------------------------------- 2 files changed, 1 insertion(+), 348 deletions(-) delete mode 100644 docs/plans/global-output-flag.md diff --git a/.gitignore b/.gitignore index 9804be6bd..4b131eb8b 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ vendor/ .claude do-not-commit/ pkg/tools/embedded/ +docs/plans/ diff --git a/docs/plans/global-output-flag.md b/docs/plans/global-output-flag.md deleted file mode 100644 index be97921dc..000000000 --- a/docs/plans/global-output-flag.md +++ /dev/null @@ -1,348 +0,0 @@ -# Implementation Plan: Global `--output` Persistent Flag - -## Goal - -Introduce a global `--output` (`-o`) persistent flag on the root command that all subcommands inherit, replacing ~78 local `--output` flag registrations. Support a new `REPLICATED_OUTPUT` environment variable for setting a default output format without providing the flag on every command. - -## Resolution Order - -1. Explicit `--output` flag on the command line -2. `REPLICATED_OUTPUT` environment variable -3. Default: `"table"` - -## Files to Edit - -- `cli/cmd/runner.go` — add resolution method, clean up `runnerArgs` -- `cli/cmd/root.go` — register global persistent flag, wire resolution into pre-run hooks -- `cli/cmd/version.go` — migrate from `--json` bool flag to global `--output` flag -- ~70 command files — delete local `--output` flags, refactor local `outputFormat` variables to `r.outputFormat` - ---- - -## Phase 1: Core Infrastructure - -### 1.1 Add Output Resolution Method - -**File:** `cli/cmd/runner.go` - -Add the following method to the `runners` struct: - -```go -func (r *runners) resolveOutputFormat(cmd *cobra.Command) { - if cmd.Flags().Changed("output") { - return // explicit flag wins - } - if env := os.Getenv("REPLICATED_OUTPUT"); env != "" { - r.outputFormat = env - } -} -``` - -Also ensure `"os"` is imported if not already present. - -### 1.2 Register Global Persistent Flag - -**File:** `cli/cmd/root.go` - -Inside `Execute()`, after creating `runCmds`, add the persistent flag: - -```go -runCmds.rootCmd.PersistentFlags().StringVarP( - &runCmds.outputFormat, "output", "o", "table", - "The output format to use. Supported formats vary by command (json, table, wide). (default 'table', override with REPLICATED_OUTPUT env var)", -) -``` - -### 1.3 Wire Resolution into Pre-Run Hooks - -**File:** `cli/cmd/root.go` - -Add `runCmds.resolveOutputFormat(cmd)` at the beginning of both: - -- `preRunSetupAPIs` — used by app, registry, cluster, vm, network, api, model, policy, notification, config, default -- `prerunCommand` — used by channel, release, collector, installer, customer, instance, enterprise-portal, cluster-prepare - -Example addition in `preRunSetupAPIs`: - -```go -preRunSetupAPIs := func(cmd *cobra.Command, args []string) error { - runCmds.resolveOutputFormat(cmd) - // ... rest of existing logic -} -``` - -Same for `prerunCommand`. - ---- - -## Phase 2: Refactor Commands Using Local `outputFormat` Variables - -These commands declare a local `var outputFormat string` and pass it to helper methods. Remove the local variable, update the helper method signature to drop the `outputFormat` parameter, and use `r.outputFormat` directly inside the helper. - -| File | Local Var | Helper to Update | -|---|---|---| -| `app_ls.go` | `outputFormat` | `listApps(...)` → drop `outputFormat` param | -| `app_create.go` | `outputFormat` | `createApp(...)` → drop `outputFormat` param | -| `app_rm.go` | `outputFormat` | `deleteApp(...)` → drop `outputFormat` param | -| `app_hostname_ls.go` | `outputFormat` | `listAppHostnames(...)` → drop `outputFormat` param | -| `customer_ls.go` | `outputFormat` | `listCustomers(...)` → drop `outputFormat` param | -| `customer_create.go` | `outputFormat` | `createCustomer(...)` → drop `outputFormat` param | -| `customer_inspect.go` | `outputFormat` | `inspectCustomer(...)` → drop `outputFormat` param | -| `policy_ls.go` | `outputFormat` | `policyList(...)` → drop `outputFormat` param | -| `policy_get.go` | `outputFormat` | `policyGet(...)` → drop `outputFormat` param | -| `policy_create.go` | `outputFormat` | `policyCreate(...)` → drop `outputFormat` param | -| `policy_update.go` | `outputFormat` | `policyUpdate(...)` → drop `outputFormat` param | -| `default_show.go` | `outputFormat` | `defaultShow(...)` → drop `outputFormat` param | -| `default_set.go` | `outputFormat` | `defaultSet(...)` → drop `outputFormat` param | -| `enterpriseportal_status_get.go` | `outputFormat` | `enterprisePortalStatusGet(...)` → drop `outputFormat` param | -| `enterpriseportal_status_update.go` | `outputFormat` | `enterprisePortalStatusUpdate(...)` → drop `outputFormat` param | -| `enterprise_portal_invite.go` | `outputFormat` | `enterprisePortalInvite(...)` → drop `outputFormat` param | -| `enterprise_portal_user_ls.go` | `outputFormat` | `enterprisePortalUserLs(...)` → drop `outputFormat` param | - -### 2.1 Refactor `notification.go` - -All 10 notification subcommands use local `var outputFormat string` inside closures that already capture `r *runners`. For each subcommand: - -1. Remove `var outputFormat string` declaration -2. Remove `cmd.Flags().StringVarP(&outputFormat, "output", ...)` line -3. Inside the closure, replace `outputFormat` with `r.outputFormat` - -Affected functions in `notification.go`: -- `InitNotificationSubscriptionList` -- `InitNotificationSubscriptionGet` -- `InitNotificationSubscriptionCreate` -- `InitNotificationSubscriptionUpdate` -- `InitNotificationSubscriptionEvents` -- `InitNotificationEventList` -- `InitNotificationEventTypeList` -- `InitNotificationEmailResendVerification` -- `InitNotificationEmailVerify` -- `InitNotificationWebhookTest` - -### 2.2 Refactor `cluster_addon_ls.go` - -1. Remove `outputFormat string` from `clusterAddonLsArgs` struct -2. Remove `cmd.Flags().StringVarP(&args.outputFormat, ...)` line from `clusterAddonLsFlags` -3. Update `addonClusterLsRun` signature: `func (r *runners) addonClusterLsRun(clusterID string) error` -4. In `InitClusterAddonLs`, capture `args.clusterID` directly instead of passing the whole struct -5. Use `r.outputFormat` inside `addonClusterLsRun` instead of `args.outputFormat` - -### 2.3 Refactor `cluster_addon_create_objectstore.go` - -1. Remove `clusterAddonCreateObjectStoreOutput string` from `runnerArgs` struct in `runner.go` -2. Remove `cmd.Flags().StringVarP(&r.args.clusterAddonCreateObjectStoreOutput, ...)` from `clusterAddonCreateObjectStoreFlags` -3. In `clusterAddonCreateObjectStoreCreateRun`, replace `r.args.clusterAddonCreateObjectStoreOutput` with `r.outputFormat` - ---- - -## Phase 3: Delete Local `--output` Flags (Already Using `r.outputFormat`) - -These commands already bind their `--output` flag directly to `r.outputFormat`. Simply delete the `cmd.Flags().StringVarP(&r.outputFormat, "output", ...)` line in each file. - -**Files to edit:** - -- `release_ls.go` -- `release_create.go` -- `release_inspect.go` -- `release_lint.go` -- `installer_ls.go` -- `channel_ls.go` -- `channel_create.go` -- `channel_inspect.go` -- `channel_releases.go` -- `instance_ls.go` -- `instance_inspect.go` -- `instance_tag.go` -- `registry_ls.go` -- `registry_add_dockerhub.go` -- `registry_add_ecr.go` -- `registry_add_gar.go` -- `registry_add_gcr.go` -- `registry_add_ghcr.go` -- `registry_add_quay.go` -- `registry_add_other.go` -- `model_ls.go` -- `model_rm.go` -- `collection_ls.go` -- `collection_create.go` -- `collection_rm.go` -- `collection_addmodel.go` -- `collection_removemodel.go` -- `cluster_ls.go` -- `cluster_create.go` -- `cluster_upgrade.go` -- `cluster_versions.go` -- `cluster_update_ttl.go` -- `cluster_update_nodegroup.go` -- `cluster_nodegroup_ls.go` -- `cluster_port_ls.go` -- `cluster_port_expose.go` -- `cluster_port_rm.go` -- `vm_ls.go` -- `vm_create.go` -- `vm_versions.go` -- `vm_update_ttl.go` -- `vm_port_ls.go` -- `vm_port_expose.go` -- `vm_port_rm.go` -- `network_ls.go` -- `network_create.go` -- `customer_update.go` - ---- - -## Phase 4: Clean Up `runnerArgs` - -**File:** `cli/cmd/runner.go` - -Remove from `runnerArgs` struct: - -```go -clusterAddonCreateObjectStoreOutput string -``` - ---- - -## Phase 5: Migrate `version` Command to Global `--output` - -**File:** `cli/cmd/version.go` - -1. Remove `var versionJson bool` -2. Remove `cmd.Flags().BoolVar(&versionJson, "json", false, "output version info in json")` -3. Change function signature from `func Version() *cobra.Command` to `func (r *runners) Version() *cobra.Command` -4. Replace `if !versionJson` with `if r.outputFormat != "json"` -5. Replace `else` branch with JSON output - -**File:** `cli/cmd/root.go` - -Update the call from: -```go -runCmds.rootCmd.AddCommand(Version()) -``` -to: -```go -runCmds.rootCmd.AddCommand(runCmds.Version()) -``` - -**Note:** `version_upgrade.go` does not need changes — it has no output formatting logic. - ---- - -## Phase 6: Test Update Pass - -### 6.1 Existing Tests That Directly Set `runners.outputFormat` - -Tests in these files directly instantiate `&runners{outputFormat: "json"}` — they continue working unchanged: - -- `lint_test.go` -- `release_create_test.go` -- `vm_create_test.go` -- `cluster_ls_test.go` -- `notification_test.go` -- `release_lint_test.go` -- `release_image_ls_test.go` -- `image_extraction_test.go` -- `completion_test.go` -- `cluster_prepare_test.go` -- `cluster_create_test.go` - -No changes needed for these. - -### 6.2 New Tests to Add - -Create a new test file (e.g., `cli/cmd/output_test.go`) with tests for `resolveOutputFormat`: - -```go -func TestResolveOutputFormat_ExplicitFlagWins(t *testing.T) { - // Set env var - t.Setenv("REPLICATED_OUTPUT", "json") - - r := &runners{outputFormat: "table"} - cmd := &cobra.Command{} - cmd.Flags().StringP("output", "o", "table", "") - cmd.Flags().Set("output", "wide") - - r.resolveOutputFormat(cmd) - require.Equal(t, "wide", r.outputFormat) -} - -func TestResolveOutputFormat_EnvVarWinsOverDefault(t *testing.T) { - t.Setenv("REPLICATED_OUTPUT", "json") - - r := &runners{outputFormat: "table"} - cmd := &cobra.Command{} - cmd.Flags().StringP("output", "o", "table", "") - - r.resolveOutputFormat(cmd) - require.Equal(t, "json", r.outputFormat) -} - -func TestResolveOutputFormat_DefaultTable(t *testing.T) { - r := &runners{outputFormat: "table"} - cmd := &cobra.Command{} - cmd.Flags().StringP("output", "o", "table", "") - - r.resolveOutputFormat(cmd) - require.Equal(t, "table", r.outputFormat) -} -``` - -### 6.3 Compile Check - -After all edits, run: - -```bash -go build ./... -go test ./cli/cmd/... -``` - ---- - -## Phase 7: Verification & Exclusions - -### 7.1 Commands Intentionally Excluded - -These commands have an `--output` flag but it is **NOT** a format flag — do **NOT** modify them: - -| File | Flag Purpose | -|---|---| -| `customer_download_license.go` | `--output` is a **file path** (e.g., `--output license.yaml`) | - -These commands have no output formatting and need no changes: - -| File | Reason | -|---|---| -| `login.go` | No output formatting | -| `logout.go` | No output formatting | -| `completion.go` | Shell script generation, not tabular/json data | -| `config.go` | No output formatting | - -### 7.2 Per-Command Validation Stays - -Different commands support different format sets (`json|table` vs `json|table|wide`). Existing validation blocks like: - -```go -if r.outputFormat != "table" && r.outputFormat != "json" { - return errors.Errorf("invalid output: %s", r.outputFormat) -} -``` - -continue to work unchanged because they validate `r.outputFormat`, which is now set globally. - ---- - -## Checklist - -- [ ] `runner.go`: Add `resolveOutputFormat` method, remove `clusterAddonCreateObjectStoreOutput` from `runnerArgs` -- [ ] `root.go`: Add global `--output` persistent flag, wire `resolveOutputFormat` into `preRunSetupAPIs` and `prerunCommand` -- [ ] `version.go`: Migrate from `--json` bool to global `--output`, change to `(r *runners) Version()` -- [ ] `root.go`: Update `runCmds.Version()` call -- [ ] Refactor all commands with local `outputFormat` vars (Phase 2) -- [ ] Delete local `--output` flags from ~50 commands already using `r.outputFormat` (Phase 3) -- [ ] Add `output_test.go` with resolution tests (Phase 6) -- [ ] Run `go build ./...` — zero errors -- [ ] Run `go test ./cli/cmd/...` — all pass -- [ ] Verify `REPLICATED_OUTPUT=json` works without explicit `--output json` -- [ ] Verify explicit `--output wide` still overrides env var -- [ ] Verify `--output` appears in `replicated --help` Global Flags section -- [ ] Verify `customer download-license --output` still works as a file path flag From 029b29e4d3dc9d7c26b4b2c7dd1e058f6984f1c8 Mon Sep 17 00:00:00 2001 From: Gerard Nguyen Date: Mon, 25 May 2026 10:58:01 +1000 Subject: [PATCH 9/9] update output flag for network_update --- cli/cmd/network_update.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cli/cmd/network_update.go b/cli/cmd/network_update.go index 4e149d3a8..4ad3c000d 100644 --- a/cli/cmd/network_update.go +++ b/cli/cmd/network_update.go @@ -48,7 +48,6 @@ replicated network update NETWORK_ID --policy airgap --collect-report cmd.Flags().StringVarP(&r.args.updateNetworkPolicy, "policy", "p", "", "Update network policy setting") cmd.Flags().BoolVarP(&r.args.updateNetworkCollectReport, "collect-report", "r", false, "Enable report collection on this network (use --collect-report=false to disable)") - cmd.Flags().StringVar(&r.outputFormat, "output", "table", "The output format to use. One of: json|table|wide") return cmd }