Skip to content

fix(export): reject unsupported -o format on empty resource lists#53

Closed
shreemaan-abhishek wants to merge 1 commit into
mainfrom
cli-permutation-suite
Closed

fix(export): reject unsupported -o format on empty resource lists#53
shreemaan-abhishek wants to merge 1 commit into
mainfrom
cli-permutation-suite

Conversation

@shreemaan-abhishek
Copy link
Copy Markdown
Contributor

@shreemaan-abhishek shreemaan-abhishek commented May 27, 2026

Summary

Every <resource> 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 lived inside Exporter.Write, which the empty path never reached. Result: a6 route export -o table errored loudly when routes existed but silently succeeded otherwise. Same pattern across all 10 export commands.

The fix adds cmdutil.ValidateExportFormat and calls it at the top of every export command, so an invalid -o is rejected before any I/O — independent of result-set size.

Test plan

  • go test ./pkg/cmdutil/... — new unit test for ValidateExportFormat (accepts ""/json/yaml, rejects table/garbage).
  • Manual: a6 route export -o garbage on an empty server now exits non-zero with unsupported output format: garbage. Was exit 0 + No routes found. before.
  • CI passes on PR.

How it was found

Surfaced by a tiered combinatorial CLI stability suite I'm prepping in a follow-up PR. The output-format tier ran every export command against an empty server, and 7 of them silently passed with -o garbage-format. Same root cause across all of them, so fixing them together as one PR keeps the contract consistent.

The follow-up that adds the suite itself depends on this fix landing so its CI is green.

Summary by CodeRabbit

  • Bug Fixes

    • Export commands now validate the output format parameter at the start of execution, immediately rejecting invalid formats and preventing unnecessary processing.
  • Tests

    • Added validation tests to ensure supported output formats are accepted and unsupported formats are properly rejected.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 27, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7918d085-1098-44b9-84d3-cb9b9a5c290c

📥 Commits

Reviewing files that changed from the base of the PR and between 6279ee6 and 380edfe.

📒 Files selected for processing (12)
  • pkg/cmd/consumer/export/export.go
  • pkg/cmd/consumergroup/export/export.go
  • pkg/cmd/globalrule/export/export.go
  • pkg/cmd/pluginconfig/export/export.go
  • pkg/cmd/proto/export/export.go
  • pkg/cmd/route/export/export.go
  • pkg/cmd/service/export/export.go
  • pkg/cmd/ssl/export/export.go
  • pkg/cmd/streamroute/export/export.go
  • pkg/cmd/upstream/export/export.go
  • pkg/cmdutil/exporter.go
  • pkg/cmdutil/exporter_test.go
🚧 Files skipped from review as they are similar to previous changes (6)
  • pkg/cmd/service/export/export.go
  • pkg/cmd/consumergroup/export/export.go
  • pkg/cmd/ssl/export/export.go
  • pkg/cmd/pluginconfig/export/export.go
  • pkg/cmd/consumer/export/export.go
  • pkg/cmd/proto/export/export.go

📝 Walkthrough

Walkthrough

This PR introduces early export format validation by centralizing format checking in a ValidateExportFormat utility function and applying it consistently across all export command entry points, rejecting invalid formats before any configuration or API processing occurs.

Changes

Export Format Validation

Layer / File(s) Summary
Validation function and test coverage
pkg/cmdutil/exporter.go, pkg/cmdutil/exporter_test.go
ValidateExportFormat function accepts empty string, json, and yaml formats; rejects others with unsupported output format error. Tests verify accepted and rejected format handling with error message assertions.
Export command format validation
pkg/cmd/consumer/export/export.go, pkg/cmd/consumergroup/export/export.go, pkg/cmd/globalrule/export/export.go, pkg/cmd/pluginconfig/export/export.go, pkg/cmd/proto/export/export.go, pkg/cmd/route/export/export.go, pkg/cmd/service/export/export.go, pkg/cmd/ssl/export/export.go, pkg/cmd/streamroute/export/export.go, pkg/cmd/upstream/export/export.go
Ten export command files add early calls to ValidateExportFormat(opts.Output) at the start of exportRun, returning validation errors before configuration loading, client initialization, or API calls.

🎯 2 (Simple) | ⏱️ ~8 minutes

🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
E2e Test Quality Review ⚠️ Warning Unit test covers ValidateExportFormat; however, there is no E2E test verifying the core bug fix: that export -o invalid errors on empty resource collections as intended. Add E2E test for invalid export format rejection on empty resource collections (e.g., a6 route export -o table should error).
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(export): reject unsupported -o format on empty resource lists' directly and specifically describes the main fix implemented: validating export format flags early to catch invalid -o values before processing. This matches the primary bug fix in the changeset across all 10 export command files and the new ValidateExportFormat function.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Security Check ✅ Passed Format validation added upfront in all 10 export commands before config/client setup. No sensitive data logging, only read operations, no credential/secret exports, comprehensive tests.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cli-permutation-suite

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
test/e2e/permutation_report_test.go (1)

58-64: 💤 Low value

Byte-based truncation may split multi-byte UTF-8 characters.

len(s) returns byte count and s[:500] slices by bytes. If stderr contains multi-byte UTF-8 at the boundary, this could produce invalid UTF-8 in reports. For CLI output this is typically ASCII, so impact is minimal.

If you want UTF-8-safe truncation:

♻️ Optional: rune-safe truncation
 func first500(s string) string {
 	s = strings.TrimSpace(s)
-	if len(s) > 500 {
-		return s[:500]
+	runes := []rune(s)
+	if len(runes) > 500 {
+		return string(runes[:500])
 	}
 	return s
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/e2e/permutation_report_test.go` around lines 58 - 64, The function
first500 uses byte-based len and slicing which can split multi-byte UTF-8
characters; change it to perform rune-safe truncation: TrimSpace the input,
convert to a rune slice (e.g., runes := []rune(s)), and if len(runes) > 500
return string(runes[:500]) else return s (or string(runes)) so the returned
string is valid UTF-8; update the function first500 accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/e2e/docker-compose.yml`:
- Around line 15-16: The port mappings "9090:9090" and "9100:9100" expose
services on all interfaces; update those Docker Compose port entries to bind to
loopback by prefixing the host port with 127.0.0.1 (e.g., change "9090:9090" to
"127.0.0.1:9090:9090" and "9100:9100" to "127.0.0.1:9100:9100") so the
control/stream endpoints are only accessible locally.

In `@test/e2e/permutation_test.go`:
- Around line 154-159: The TCP dial block currently discards the error returned
by conn.Close(); update the logic around net.DialTimeout and conn.Close so you
capture and handle Close's error instead of ignoring it—for example, check the
error returned by conn.Close() and treat a non-nil close error as a failure
(return false or propagate an error) so resource/teardown failures aren't
suppressed; apply the same change pattern to the other occurrences that discard
Close errors (the other conn.Close usages at the noted locations).

---

Nitpick comments:
In `@test/e2e/permutation_report_test.go`:
- Around line 58-64: The function first500 uses byte-based len and slicing which
can split multi-byte UTF-8 characters; change it to perform rune-safe
truncation: TrimSpace the input, convert to a rune slice (e.g., runes :=
[]rune(s)), and if len(runes) > 500 return string(runes[:500]) else return s (or
string(runes)) so the returned string is valid UTF-8; update the function
first500 accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 29f78c5c-4d5c-4a99-a1be-e44121ad8001

📥 Commits

Reviewing files that changed from the base of the PR and between aa66ab8 and 6279ee6.

📒 Files selected for processing (21)
  • .gitignore
  • Makefile
  • pkg/cmd/consumer/export/export.go
  • pkg/cmd/consumergroup/export/export.go
  • pkg/cmd/globalrule/export/export.go
  • pkg/cmd/pluginconfig/export/export.go
  • pkg/cmd/proto/export/export.go
  • pkg/cmd/route/export/export.go
  • pkg/cmd/service/export/export.go
  • pkg/cmd/ssl/export/export.go
  • pkg/cmd/streamroute/export/export.go
  • pkg/cmd/upstream/export/export.go
  • pkg/cmdutil/exporter.go
  • pkg/cmdutil/exporter_test.go
  • test/e2e/apisix_conf/config-docker.yaml
  • test/e2e/docker-compose.permutation.yml
  • test/e2e/docker-compose.yml
  • test/e2e/permutation_cleanup_test.go
  • test/e2e/permutation_matrix_test.go
  • test/e2e/permutation_report_test.go
  • test/e2e/permutation_test.go

Comment thread test/e2e/docker-compose.yml Outdated
Comment thread test/e2e/permutation_test.go Outdated
Comment on lines +154 to +159
conn, err := net.DialTimeout("tcp", u.Host, 500*time.Millisecond)
if err != nil {
return false
}
_ = conn.Close()
return true
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle cleanup/close errors instead of discarding them.

Several calls currently suppress errors, which can hide resource-leak and teardown failures in permutation runs.

🔧 Suggested fix pattern
 func canReach(rawURL string) bool {
@@
-	_ = conn.Close()
-	return true
+	if err := conn.Close(); err != nil {
+		return false
+	}
+	return true
 }
@@
-					DeferCleanup(func() {
-						_, _ = adminDeleteByID(spec.AdminPath, id)
-					})
+					DeferCleanup(func() {
+						_, err := adminDeleteByID(spec.AdminPath, id)
+						Expect(err).NotTo(HaveOccurred())
+					})
@@
-			DeferCleanup(func() {
-				_, _ = adminDeleteByID("/apisix/admin/plugin_metadata", plugin)
-			})
+			DeferCleanup(func() {
+				_, err := adminDeleteByID("/apisix/admin/plugin_metadata", plugin)
+				Expect(err).NotTo(HaveOccurred())
+			})
@@
-			DeferCleanup(func() {
-				_, _ = adminDeleteByID("/apisix/admin/routes", id)
-			})
+			DeferCleanup(func() {
+				_, err := adminDeleteByID("/apisix/admin/routes", id)
+				Expect(err).NotTo(HaveOccurred())
+			})

As per coding guidelines, Never suppress errors; always handle and propagate them.

Also applies to: 304-306, 397-399, 463-465

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/e2e/permutation_test.go` around lines 154 - 159, The TCP dial block
currently discards the error returned by conn.Close(); update the logic around
net.DialTimeout and conn.Close so you capture and handle Close's error instead
of ignoring it—for example, check the error returned by conn.Close() and treat a
non-nil close error as a failure (return false or propagate an error) so
resource/teardown failures aren't suppressed; apply the same change pattern to
the other occurrences that discard Close errors (the other conn.Close usages at
the noted locations).

Every `<resource> 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).
@shreemaan-abhishek shreemaan-abhishek changed the title test+fix: CLI permutation stability suite (+ export -o validation) fix(export): reject unsupported -o format on empty resource lists May 27, 2026
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes inconsistent behavior across resource export commands where an unsupported -o/--output format could silently succeed (exit 0) when the exported collection was empty, by validating the output format before any early-return paths.

Changes:

  • Added cmdutil.ValidateExportFormat to validate supported export formats (json, yaml, and "" as a special case).
  • Invoked ValidateExportFormat at the start of every exportRun implementation across all export commands.
  • Added unit tests for ValidateExportFormat.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
pkg/cmdutil/exporter.go Adds ValidateExportFormat helper for early format validation.
pkg/cmdutil/exporter_test.go Adds unit tests covering accepted/rejected formats for ValidateExportFormat.
pkg/cmd/upstream/export/export.go Validates output format at start of exportRun.
pkg/cmd/streamroute/export/export.go Validates output format at start of exportRun.
pkg/cmd/ssl/export/export.go Validates output format at start of exportRun.
pkg/cmd/service/export/export.go Validates output format at start of exportRun.
pkg/cmd/route/export/export.go Validates output format at start of exportRun.
pkg/cmd/proto/export/export.go Validates output format at start of exportRun.
pkg/cmd/pluginconfig/export/export.go Validates output format at start of exportRun.
pkg/cmd/globalrule/export/export.go Validates output format at start of exportRun.
pkg/cmd/consumergroup/export/export.go Validates output format at start of exportRun.
pkg/cmd/consumer/export/export.go Validates output format at start of exportRun.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pkg/cmdutil/exporter.go
Comment on lines +33 to +40
func ValidateExportFormat(format string) error {
switch format {
case "", "json", "yaml":
return nil
default:
return fmt.Errorf("unsupported output format: %s", format)
}
}
Comment on lines 52 to +56
func exportRun(opts *Options) error {
if err := cmdutil.ValidateExportFormat(opts.Output); err != nil {
return err
}

Comment on lines 52 to +56
func exportRun(opts *Options) error {
if err := cmdutil.ValidateExportFormat(opts.Output); err != nil {
return err
}

Comment on lines 52 to +56
func exportRun(opts *Options) error {
if err := cmdutil.ValidateExportFormat(opts.Output); err != nil {
return err
}

Comment on lines 52 to +56
func exportRun(opts *Options) error {
if err := cmdutil.ValidateExportFormat(opts.Output); err != nil {
return err
}

Comment on lines 52 to +56
func exportRun(opts *Options) error {
if err := cmdutil.ValidateExportFormat(opts.Output); err != nil {
return err
}

Comment on lines 52 to +56
func exportRun(opts *Options) error {
if err := cmdutil.ValidateExportFormat(opts.Output); err != nil {
return err
}

Comment on lines 52 to +56
func exportRun(opts *Options) error {
if err := cmdutil.ValidateExportFormat(opts.Output); err != nil {
return err
}

Comment on lines 52 to +56
func exportRun(opts *Options) error {
if err := cmdutil.ValidateExportFormat(opts.Output); err != nil {
return err
}

@shreemaan-abhishek
Copy link
Copy Markdown
Contributor Author

Closing without merging.

After discussion, the empty-collection + invalid-format scenario isn't a real bug: "No X found." is higher-priority information for an interactive user than the format error, and the probability is low (typo on -o + empty resource type + scripted consumer that doesn't notice exit=0). Existing behaviour stays.

For reference, the inconsistency this PR addressed is:

  • Empty server: a6 route export -o garbage → exit 0, No routes found.
  • Seeded server: a6 route export -o garbage → exit 1, unsupported output format: garbage

Accepting that inconsistency as documented behaviour. The follow-up permutation suite will treat the empty-collection case as ExpectExit=0 to reflect the actual contract.

Thanks @coderabbitai for the duplication / regression-test feedback — both fair points, but they're moot now that we're not changing the behaviour.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 27, 2026

Tip

For best results, initiate chat on the files or code changes.

@shreemaan-abhishek Thanks for the clear rationale — that's a reasonable UX call. The "No X found." message being higher-priority than a format error for interactive users makes sense, and documenting it as the explicit contract is the right approach.

Appreciated the context, and good luck with the follow-up permutation suite! 🐇


✏️ Learnings added
Learnt from: shreemaan-abhishek
URL: https://github.com/api7/a6/pull/53

Timestamp: 2026-05-27T09:07:19.328Z
Learning: In the `api7/a6` CLI, the documented contract for all `export` commands is: when a resource collection is empty, the command prints "No X found." and exits with code 0, regardless of whether the `-o` flag value is a valid format or not. This is an accepted inconsistency — empty-collection takes priority over format validation. The permutation suite should treat the empty-collection case as `ExpectExit=0`.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants