From e99d01a55f0ff85da1199d99ee22599c63270017 Mon Sep 17 00:00:00 2001 From: Roman Berezkin Date: Tue, 14 Jul 2026 15:54:30 +0300 Subject: [PATCH 1/4] Add output format helper to utilk8s GetOutputFormat pairs with AddOutputFlag: reads the -o/--output flag and validates the value against the accepted formats. Signed-off-by: Roman Berezkin --- internal/utilk8s/output.go | 16 +++++++ internal/utilk8s/output_test.go | 78 +++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 internal/utilk8s/output_test.go diff --git a/internal/utilk8s/output.go b/internal/utilk8s/output.go index 328024b0d..1654f82a8 100644 --- a/internal/utilk8s/output.go +++ b/internal/utilk8s/output.go @@ -20,6 +20,7 @@ import ( "encoding/json" "fmt" "io" + "slices" "strings" "github.com/spf13/cobra" @@ -35,6 +36,21 @@ func AddOutputFlag(cmd *cobra.Command, defaultFmt string, formats ...string) { _ = cmd.RegisterFlagCompletionFunc("output", CompleteOutputFormats(formats...)) } +// GetOutputFormat reads the "-o/--output" flag declared by AddOutputFlag +// and validates it against the accepted formats. +func GetOutputFormat(cmd *cobra.Command, formats ...string) (string, error) { + format, err := cmd.Flags().GetString("output") + if err != nil { + return "", fmt.Errorf("reading output flag: %w", err) + } + + if !slices.Contains(formats, format) { + return "", fmt.Errorf("unsupported output format %q; use %s", format, strings.Join(formats, "|")) + } + + return format, nil +} + // PrintObject writes an unstructured Kubernetes object to w in the given format. // Supported formats: "json", "yaml"; anything else prints Kind/Name. func PrintObject(w io.Writer, obj *unstructured.Unstructured, format string) error { diff --git a/internal/utilk8s/output_test.go b/internal/utilk8s/output_test.go new file mode 100644 index 000000000..7343b4cc6 --- /dev/null +++ b/internal/utilk8s/output_test.go @@ -0,0 +1,78 @@ +/* +Copyright 2025 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package utilk8s + +import ( + "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" +) + +func TestGetOutputFormat(t *testing.T) { + tests := []struct { + name string + formats []string + args []string + expected string + wantErr string + }{ + { + name: "default format when flag is not set", + formats: []string{"yaml", "json"}, + args: []string{}, + expected: "yaml", + }, + { + name: "explicit allowed format", + formats: []string{"yaml", "json"}, + args: []string{"-o", "json"}, + expected: "json", + }, + { + name: "unsupported format", + formats: []string{"yaml", "json"}, + args: []string{"-o", "xml"}, + wantErr: `unsupported output format "xml"; use yaml|json`, + }, + { + name: "flag not registered", + formats: nil, + args: []string{}, + wantErr: "reading output flag", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := &cobra.Command{Use: "test"} + if tt.formats != nil { + AddOutputFlag(cmd, tt.formats[0], tt.formats...) + } + assert.NoError(t, cmd.Flags().Parse(tt.args)) + + format, err := GetOutputFormat(cmd, tt.formats...) + if tt.wantErr != "" { + assert.ErrorContains(t, err, tt.wantErr) + return + } + + assert.NoError(t, err) + assert.Equal(t, tt.expected, format) + }) + } +} From 79fc0ae3718582f1b954be74efcda614056fd0c2 Mon Sep 17 00:00:00 2001 From: Roman Berezkin Date: Tue, 14 Jul 2026 17:19:49 +0300 Subject: [PATCH 2/4] Add output format flag to module values The addon-operator debug API selects the response format by the path extension, so the flag value maps to values.. Signed-off-by: Roman Berezkin --- internal/system/cmd/module/cmd/values/values.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/internal/system/cmd/module/cmd/values/values.go b/internal/system/cmd/module/cmd/values/values.go index 7a9df8225..f6e719985 100644 --- a/internal/system/cmd/module/cmd/values/values.go +++ b/internal/system/cmd/module/cmd/values/values.go @@ -31,6 +31,8 @@ Dump module hooks values. © Flant JSC 2025`) +var outputFormats = []string{"yaml", "json"} + func NewCommand() *cobra.Command { valuesCmd := &cobra.Command{ Use: "values", @@ -41,6 +43,7 @@ func NewCommand() *cobra.Command { SilenceUsage: true, RunE: valuesModule, } + utilk8s.AddOutputFlag(valuesCmd, "yaml", outputFormats...) return valuesCmd } @@ -52,6 +55,11 @@ func valuesModule(cmd *cobra.Command, args []string) error { moduleName := args[0] + format, err := utilk8s.GetOutputFormat(cmd, outputFormats...) + if err != nil { + return err + } + kubeconfigPath, err := cmd.Flags().GetString("kubeconfig") if err != nil { return fmt.Errorf("Failed to setup Kubernetes client: %w", err) @@ -67,7 +75,7 @@ func valuesModule(cmd *cobra.Command, args []string) error { return fmt.Errorf("Failed to setup Kubernetes client: %w", err) } - pathFromOption := fmt.Sprintf("%s/values.yaml", moduleName) + pathFromOption := fmt.Sprintf("%s/values.%s", moduleName, format) err = deckhouse.QueryAPI(config, kubeCl, pathFromOption) if err != nil { From 16c3f115d9bd461c57fa1d1b52333ad7073e90bc Mon Sep 17 00:00:00 2001 From: Roman Berezkin Date: Tue, 14 Jul 2026 17:23:59 +0300 Subject: [PATCH 3/4] Add output format flag to module list and snapshots Same path-extension mechanism as module values. Also drop the stray --editor flag from list and require the module name argument in snapshots instead of panicking. Signed-off-by: Roman Berezkin --- internal/system/cmd/module/cmd/list/list.go | 12 +++++++++--- .../system/cmd/module/cmd/snapshots/snapshots.go | 14 +++++++++++++- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/internal/system/cmd/module/cmd/list/list.go b/internal/system/cmd/module/cmd/list/list.go index 074ee804f..a515c9292 100644 --- a/internal/system/cmd/module/cmd/list/list.go +++ b/internal/system/cmd/module/cmd/list/list.go @@ -22,7 +22,6 @@ import ( "github.com/spf13/cobra" "k8s.io/kubectl/pkg/util/templates" - "github.com/deckhouse/deckhouse-cli/internal/system/cmd/edit/flags" "github.com/deckhouse/deckhouse-cli/internal/system/cmd/module/deckhouse" "github.com/deckhouse/deckhouse-cli/internal/utilk8s" ) @@ -32,6 +31,8 @@ List enabled Deckhouse Kubernetes Platform modules. © Flant JSC 2025`) +var outputFormats = []string{"yaml", "json", "text"} + func NewCommand() *cobra.Command { listCmd := &cobra.Command{ Use: "list", @@ -41,12 +42,17 @@ func NewCommand() *cobra.Command { SilenceUsage: true, RunE: listModule, } - flags.AddFlags(listCmd.Flags()) + utilk8s.AddOutputFlag(listCmd, "yaml", outputFormats...) return listCmd } func listModule(cmd *cobra.Command, _ []string) error { + format, err := utilk8s.GetOutputFormat(cmd, outputFormats...) + if err != nil { + return err + } + kubeconfigPath, err := cmd.Flags().GetString("kubeconfig") if err != nil { return fmt.Errorf("Failed to setup Kubernetes client: %w", err) @@ -62,7 +68,7 @@ func listModule(cmd *cobra.Command, _ []string) error { return fmt.Errorf("Failed to setup Kubernetes client: %w", err) } - err = deckhouse.QueryAPI(config, kubeCl, "list.yaml") + err = deckhouse.QueryAPI(config, kubeCl, "list."+format) if err != nil { return fmt.Errorf("Error list modules: %w", err) } diff --git a/internal/system/cmd/module/cmd/snapshots/snapshots.go b/internal/system/cmd/module/cmd/snapshots/snapshots.go index 5c112f874..47cff4c2c 100644 --- a/internal/system/cmd/module/cmd/snapshots/snapshots.go +++ b/internal/system/cmd/module/cmd/snapshots/snapshots.go @@ -31,6 +31,8 @@ Dump module hooks snapshots. © Flant JSC 2025`) +var outputFormats = []string{"yaml", "json"} + func NewCommand() *cobra.Command { snapshotsCmd := &cobra.Command{ Use: "snapshots", @@ -41,13 +43,23 @@ func NewCommand() *cobra.Command { SilenceUsage: true, RunE: snapshotsModule, } + utilk8s.AddOutputFlag(snapshotsCmd, "yaml", outputFormats...) return snapshotsCmd } func snapshotsModule(cmd *cobra.Command, args []string) error { + if len(args) != 1 { + return fmt.Errorf("this command requires exactly 1 argument: module name") + } + moduleName := args[0] + format, err := utilk8s.GetOutputFormat(cmd, outputFormats...) + if err != nil { + return err + } + kubeconfigPath, err := cmd.Flags().GetString("kubeconfig") if err != nil { return fmt.Errorf("Failed to setup Kubernetes client: %w", err) @@ -63,7 +75,7 @@ func snapshotsModule(cmd *cobra.Command, args []string) error { return fmt.Errorf("Failed to setup Kubernetes client: %w", err) } - pathFromOption := fmt.Sprintf("%s/snapshots.yaml", moduleName) + pathFromOption := fmt.Sprintf("%s/snapshots.%s", moduleName, format) err = deckhouse.QueryAPI(config, kubeCl, pathFromOption) if err != nil { From dcfd685c53de352e86d05d28c91024e874d0549e Mon Sep 17 00:00:00 2001 From: Roman Berezkin Date: Wed, 15 Jul 2026 11:14:03 +0300 Subject: [PATCH 4/4] Restrict module list output formats to yaml and json - The /module/list debug endpoint matches .text in its route but renders the structured payload as JSON anyway, so a text option would promise one format and deliver another. Signed-off-by: Roman Berezkin --- internal/system/cmd/module/cmd/list/list.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/system/cmd/module/cmd/list/list.go b/internal/system/cmd/module/cmd/list/list.go index a515c9292..394ddddfc 100644 --- a/internal/system/cmd/module/cmd/list/list.go +++ b/internal/system/cmd/module/cmd/list/list.go @@ -31,7 +31,9 @@ List enabled Deckhouse Kubernetes Platform modules. © Flant JSC 2025`) -var outputFormats = []string{"yaml", "json", "text"} +// The /module/list debug endpoint also matches ".text" in its route, +// but renders structured payloads as JSON anyway, so text is not offered. +var outputFormats = []string{"yaml", "json"} func NewCommand() *cobra.Command { listCmd := &cobra.Command{