From 380edfe7abef8e3c1388dace6129df28717ed925 Mon Sep 17 00:00:00 2001 From: Abhishek Choudhary Date: Wed, 27 May 2026 15:57:50 +0800 Subject: [PATCH] fix(export): reject unsupported -o format on empty resource lists Every ` export` command short-circuited with "No X found." and returned exit 0 when the collection was empty, even if -o named an unsupported format. Format validation happened lazily inside Exporter.Write, which was never reached on the empty path. The result was an inconsistent contract: `a6 route export -o table` errored loudly when routes existed but silently succeeded otherwise. Move the validation up. Add cmdutil.ValidateExportFormat and call it at the top of every export command so an invalid -o is rejected before any I/O, regardless of result-set size. Surfaced by the new permutation suite (tier 4). --- pkg/cmd/consumer/export/export.go | 4 ++++ pkg/cmd/consumergroup/export/export.go | 4 ++++ pkg/cmd/globalrule/export/export.go | 4 ++++ pkg/cmd/pluginconfig/export/export.go | 4 ++++ pkg/cmd/proto/export/export.go | 4 ++++ pkg/cmd/route/export/export.go | 4 ++++ pkg/cmd/service/export/export.go | 4 ++++ pkg/cmd/ssl/export/export.go | 4 ++++ pkg/cmd/streamroute/export/export.go | 4 ++++ pkg/cmd/upstream/export/export.go | 4 ++++ pkg/cmdutil/exporter.go | 16 ++++++++++++++++ pkg/cmdutil/exporter_test.go | 22 ++++++++++++++++++++++ 12 files changed, 78 insertions(+) diff --git a/pkg/cmd/consumer/export/export.go b/pkg/cmd/consumer/export/export.go index 7f44d98..cfa67b7 100644 --- a/pkg/cmd/consumer/export/export.go +++ b/pkg/cmd/consumer/export/export.go @@ -50,6 +50,10 @@ func NewCmdExport(f *cmd.Factory) *cobra.Command { } func exportRun(opts *Options) error { + if err := cmdutil.ValidateExportFormat(opts.Output); err != nil { + return err + } + cfg, err := opts.Config() if err != nil { return err diff --git a/pkg/cmd/consumergroup/export/export.go b/pkg/cmd/consumergroup/export/export.go index 23d256a..46f27d5 100644 --- a/pkg/cmd/consumergroup/export/export.go +++ b/pkg/cmd/consumergroup/export/export.go @@ -50,6 +50,10 @@ func NewCmdExport(f *cmd.Factory) *cobra.Command { } func exportRun(opts *Options) error { + if err := cmdutil.ValidateExportFormat(opts.Output); err != nil { + return err + } + cfg, err := opts.Config() if err != nil { return err diff --git a/pkg/cmd/globalrule/export/export.go b/pkg/cmd/globalrule/export/export.go index 38b3f01..99cc25e 100644 --- a/pkg/cmd/globalrule/export/export.go +++ b/pkg/cmd/globalrule/export/export.go @@ -50,6 +50,10 @@ func NewCmdExport(f *cmd.Factory) *cobra.Command { } func exportRun(opts *Options) error { + if err := cmdutil.ValidateExportFormat(opts.Output); err != nil { + return err + } + cfg, err := opts.Config() if err != nil { return err diff --git a/pkg/cmd/pluginconfig/export/export.go b/pkg/cmd/pluginconfig/export/export.go index b66e3c5..7ee61a0 100644 --- a/pkg/cmd/pluginconfig/export/export.go +++ b/pkg/cmd/pluginconfig/export/export.go @@ -50,6 +50,10 @@ func NewCmdExport(f *cmd.Factory) *cobra.Command { } func exportRun(opts *Options) error { + if err := cmdutil.ValidateExportFormat(opts.Output); err != nil { + return err + } + cfg, err := opts.Config() if err != nil { return err diff --git a/pkg/cmd/proto/export/export.go b/pkg/cmd/proto/export/export.go index a7cfb49..802ce08 100644 --- a/pkg/cmd/proto/export/export.go +++ b/pkg/cmd/proto/export/export.go @@ -50,6 +50,10 @@ func NewCmdExport(f *cmd.Factory) *cobra.Command { } func exportRun(opts *Options) error { + if err := cmdutil.ValidateExportFormat(opts.Output); err != nil { + return err + } + cfg, err := opts.Config() if err != nil { return err diff --git a/pkg/cmd/route/export/export.go b/pkg/cmd/route/export/export.go index fcfeac7..8982fa3 100644 --- a/pkg/cmd/route/export/export.go +++ b/pkg/cmd/route/export/export.go @@ -50,6 +50,10 @@ func NewCmdExport(f *cmd.Factory) *cobra.Command { } func exportRun(opts *Options) error { + if err := cmdutil.ValidateExportFormat(opts.Output); err != nil { + return err + } + cfg, err := opts.Config() if err != nil { return err diff --git a/pkg/cmd/service/export/export.go b/pkg/cmd/service/export/export.go index a44ea3a..38ff702 100644 --- a/pkg/cmd/service/export/export.go +++ b/pkg/cmd/service/export/export.go @@ -50,6 +50,10 @@ func NewCmdExport(f *cmd.Factory) *cobra.Command { } func exportRun(opts *Options) error { + if err := cmdutil.ValidateExportFormat(opts.Output); err != nil { + return err + } + cfg, err := opts.Config() if err != nil { return err diff --git a/pkg/cmd/ssl/export/export.go b/pkg/cmd/ssl/export/export.go index 60a6c3a..0555d9f 100644 --- a/pkg/cmd/ssl/export/export.go +++ b/pkg/cmd/ssl/export/export.go @@ -50,6 +50,10 @@ func NewCmdExport(f *cmd.Factory) *cobra.Command { } func exportRun(opts *Options) error { + if err := cmdutil.ValidateExportFormat(opts.Output); err != nil { + return err + } + cfg, err := opts.Config() if err != nil { return err diff --git a/pkg/cmd/streamroute/export/export.go b/pkg/cmd/streamroute/export/export.go index 5516300..45783de 100644 --- a/pkg/cmd/streamroute/export/export.go +++ b/pkg/cmd/streamroute/export/export.go @@ -50,6 +50,10 @@ func NewCmdExport(f *cmd.Factory) *cobra.Command { } func exportRun(opts *Options) error { + if err := cmdutil.ValidateExportFormat(opts.Output); err != nil { + return err + } + cfg, err := opts.Config() if err != nil { return err diff --git a/pkg/cmd/upstream/export/export.go b/pkg/cmd/upstream/export/export.go index 4c46a41..ba9176f 100644 --- a/pkg/cmd/upstream/export/export.go +++ b/pkg/cmd/upstream/export/export.go @@ -50,6 +50,10 @@ func NewCmdExport(f *cmd.Factory) *cobra.Command { } func exportRun(opts *Options) error { + if err := cmdutil.ValidateExportFormat(opts.Output); err != nil { + return err + } + cfg, err := opts.Config() if err != nil { return err diff --git a/pkg/cmdutil/exporter.go b/pkg/cmdutil/exporter.go index ef05388..e7912b7 100644 --- a/pkg/cmdutil/exporter.go +++ b/pkg/cmdutil/exporter.go @@ -23,6 +23,22 @@ func NewExporter(format string, writer io.Writer) *Exporter { } } +// ValidateExportFormat returns an error if format is not one of the formats +// that Exporter.Write accepts. The empty string is treated as valid because +// every export command defaults it to "yaml" later in its run. +// +// Callers should invoke this before any work that could short-circuit the +// run (e.g. an "empty collection" early return), so an invalid -o flag is +// rejected consistently regardless of the result set size. +func ValidateExportFormat(format string) error { + switch format { + case "", "json", "yaml": + return nil + default: + return fmt.Errorf("unsupported output format: %s", format) + } +} + // Write formats and writes the given data. func (e *Exporter) Write(data interface{}) error { switch e.format { diff --git a/pkg/cmdutil/exporter_test.go b/pkg/cmdutil/exporter_test.go index 026b1bc..06633dc 100644 --- a/pkg/cmdutil/exporter_test.go +++ b/pkg/cmdutil/exporter_test.go @@ -70,3 +70,25 @@ func TestExporter_JSONPrettyPrinted(t *testing.T) { // Verify pretty printing (indented) assert.Contains(t, buf.String(), " \"key\"") } + +func TestValidateExportFormat(t *testing.T) { + t.Run("accepts json", func(t *testing.T) { + require.NoError(t, ValidateExportFormat("json")) + }) + t.Run("accepts yaml", func(t *testing.T) { + require.NoError(t, ValidateExportFormat("yaml")) + }) + t.Run("accepts empty (callers default later)", func(t *testing.T) { + require.NoError(t, ValidateExportFormat("")) + }) + t.Run("rejects table", func(t *testing.T) { + err := ValidateExportFormat("table") + require.Error(t, err) + assert.Contains(t, err.Error(), "unsupported output format: table") + }) + t.Run("rejects garbage", func(t *testing.T) { + err := ValidateExportFormat("garbage-format") + require.Error(t, err) + assert.Contains(t, err.Error(), "unsupported output format: garbage-format") + }) +}