From 6ad9688dcc0e18398f144fb24e5a04f67d02ea30 Mon Sep 17 00:00:00 2001 From: Ming Wen Date: Tue, 10 Mar 2026 12:31:58 +0800 Subject: [PATCH 1/3] fix(cmdutil): add NormalizeLabel to convert = to : for APISIX API APISIX Admin API expects label filters in key:value format (colon separator), but the CLI --label flag accepts key=value (equals). Add a shared NormalizeLabel function that converts between the two formats. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus --- pkg/cmdutil/errors.go | 13 +++++++++++++ pkg/cmdutil/errors_test.go | 27 +++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 pkg/cmdutil/errors_test.go diff --git a/pkg/cmdutil/errors.go b/pkg/cmdutil/errors.go index cb8e0c9..af09b02 100644 --- a/pkg/cmdutil/errors.go +++ b/pkg/cmdutil/errors.go @@ -3,6 +3,7 @@ package cmdutil import ( "errors" "fmt" + "strings" "github.com/api7/a6/pkg/api" ) @@ -79,3 +80,15 @@ func IsOptionalResourceError(err error) bool { } return apiErr.StatusCode == 400 || apiErr.StatusCode == 404 } + +// NormalizeLabel converts "key=value" to "key:value" for the APISIX Admin API. +func NormalizeLabel(label string) string { + if label == "" { + return "" + } + parts := strings.SplitN(label, "=", 2) + if len(parts) == 2 { + return parts[0] + ":" + parts[1] + } + return label +} diff --git a/pkg/cmdutil/errors_test.go b/pkg/cmdutil/errors_test.go new file mode 100644 index 0000000..4a88a23 --- /dev/null +++ b/pkg/cmdutil/errors_test.go @@ -0,0 +1,27 @@ +package cmdutil + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNormalizeLabel(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {"", ""}, + {"env=test", "env:test"}, + {"env=prod", "env:prod"}, + {"team=backend", "team:backend"}, + {"env:test", "env:test"}, + {"env", "env"}, + {"key=value=extra", "key:value=extra"}, + } + for _, tc := range tests { + t.Run(tc.input, func(t *testing.T) { + assert.Equal(t, tc.expected, NormalizeLabel(tc.input)) + }) + } +} From 70d900e829124d400dbb01b8617752d92eedb303 Mon Sep 17 00:00:00 2001 From: Ming Wen Date: Tue, 10 Mar 2026 12:32:10 +0800 Subject: [PATCH 2/3] fix(route): normalize label query parameter for APISIX API Route export and delete split "env=test" on "=" and sent only the key "env" to APISIX, which returned no results. Route list passed the label through without conversion. Use cmdutil.NormalizeLabel to send the correct "env:test" format. Remove broken client-side label filtering. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus --- pkg/cmd/route/delete/delete.go | 27 +++------------------------ pkg/cmd/route/export/export.go | 23 ++++------------------- pkg/cmd/route/export/export_test.go | 12 +++++------- pkg/cmd/route/list/list.go | 2 +- 4 files changed, 13 insertions(+), 51 deletions(-) diff --git a/pkg/cmd/route/delete/delete.go b/pkg/cmd/route/delete/delete.go index 81aedb1..4e8d8ce 100644 --- a/pkg/cmd/route/delete/delete.go +++ b/pkg/cmd/route/delete/delete.go @@ -164,35 +164,20 @@ func bulkDeleteRoutes(opts *Options) error { return nil } -// parseLabel splits a "key=value" label into key and value parts. -// APISIX Admin API only supports filtering by label key, so we send the key -// as the query parameter and perform client-side filtering by value. -func parseLabel(label string) (key, value string) { - parts := strings.SplitN(label, "=", 2) - key = parts[0] - if len(parts) == 2 { - value = parts[1] - } - return -} - func listAllRouteIDs(client *api.Client, label string) ([]string, error) { page := 1 pageSize := 500 ids := make([]string, 0) - var labelKey, labelValue string - if label != "" { - labelKey, labelValue = parseLabel(label) - } + apiLabel := cmdutil.NormalizeLabel(label) for { query := map[string]string{ "page": fmt.Sprintf("%d", page), "page_size": fmt.Sprintf("%d", pageSize), } - if labelKey != "" { - query["label"] = labelKey + if apiLabel != "" { + query["label"] = apiLabel } body, err := client.Get("/apisix/admin/routes", query) @@ -209,12 +194,6 @@ func listAllRouteIDs(client *api.Client, label string) ([]string, error) { if item.Value.ID == nil || *item.Value.ID == "" { continue } - // Client-side filter by label value if specified. - if labelValue != "" && item.Value.Labels != nil { - if v, ok := item.Value.Labels[labelKey]; !ok || v != labelValue { - continue - } - } ids = append(ids, *item.Value.ID) } diff --git a/pkg/cmd/route/export/export.go b/pkg/cmd/route/export/export.go index 3e6e9c0..1258c15 100644 --- a/pkg/cmd/route/export/export.go +++ b/pkg/cmd/route/export/export.go @@ -6,7 +6,6 @@ import ( "io" "net/http" "os" - "strings" "github.com/spf13/cobra" @@ -104,22 +103,15 @@ func fetchAll(client *api.Client, label string) ([]api.ListItem[api.Route], erro pageSize := 500 items := make([]api.ListItem[api.Route], 0) - var labelKey, labelValue string - if label != "" { - parts := strings.SplitN(label, "=", 2) - labelKey = parts[0] - if len(parts) == 2 { - labelValue = parts[1] - } - } + apiLabel := cmdutil.NormalizeLabel(label) for { query := map[string]string{ "page": fmt.Sprintf("%d", page), "page_size": fmt.Sprintf("%d", pageSize), } - if labelKey != "" { - query["label"] = labelKey + if apiLabel != "" { + query["label"] = apiLabel } body, err := client.Get("/apisix/admin/routes", query) @@ -132,14 +124,7 @@ func fetchAll(client *api.Client, label string) ([]api.ListItem[api.Route], erro return nil, fmt.Errorf("failed to parse response: %w", err) } - for _, item := range resp.List { - if labelValue != "" && item.Value.Labels != nil { - if v, ok := item.Value.Labels[labelKey]; !ok || v != labelValue { - continue - } - } - items = append(items, item) - } + items = append(items, resp.List...) if len(resp.List) == 0 || page*pageSize >= resp.Total { break diff --git a/pkg/cmd/route/export/export_test.go b/pkg/cmd/route/export/export_test.go index 953d909..b02af5f 100644 --- a/pkg/cmd/route/export/export_test.go +++ b/pkg/cmd/route/export/export_test.go @@ -70,17 +70,16 @@ func TestRouteExport_BasicYAML(t *testing.T) { } func TestRouteExport_WithLabelFilter(t *testing.T) { - calledWithLabelKey := false + calledWithNormalizedLabel := false transport := roundTripperFunc(func(req *http.Request) (*http.Response, error) { if req.Method == http.MethodGet && req.URL.Path == "/apisix/admin/routes" { - if req.URL.Query().Get("label") == "env" { - calledWithLabelKey = true + if req.URL.Query().Get("label") == "env:test" { + calledWithNormalizedLabel = true } - // Return routes with labels; client-side filtering will match env=test return &http.Response{ StatusCode: 200, Header: http.Header{"Content-Type": []string{"application/json"}}, - Body: io.NopCloser(strings.NewReader(`{"total":2,"list":[{"key":"/apisix/routes/r1","value":{"id":"r1","name":"match","uri":"/r1","labels":{"env":"test"}}},{"key":"/apisix/routes/r2","value":{"id":"r2","name":"no-match","uri":"/r2","labels":{"env":"prod"}}}]}`)), + Body: io.NopCloser(strings.NewReader(`{"total":1,"list":[{"key":"/apisix/routes/r1","value":{"id":"r1","name":"match","uri":"/r1","labels":{"env":"test"}}}]}`)), }, nil } return &http.Response{StatusCode: 404, Body: io.NopCloser(strings.NewReader(`{"error_msg":"not found"}`))}, nil @@ -102,8 +101,7 @@ func TestRouteExport_WithLabelFilter(t *testing.T) { err := c.Execute() require.NoError(t, err) - assert.True(t, calledWithLabelKey, "should send only label key to API") + assert.True(t, calledWithNormalizedLabel, "should send normalized label key:value to API") out := stdout.String() assert.Contains(t, out, "match") - assert.NotContains(t, out, "no-match") } diff --git a/pkg/cmd/route/list/list.go b/pkg/cmd/route/list/list.go index e59715b..4aed19d 100644 --- a/pkg/cmd/route/list/list.go +++ b/pkg/cmd/route/list/list.go @@ -78,7 +78,7 @@ func listRun(opts *Options) error { query["name"] = opts.Name } if opts.Label != "" { - query["label"] = opts.Label + query["label"] = cmdutil.NormalizeLabel(opts.Label) } if opts.URI != "" { query["uri"] = opts.URI From dbdf629384affa893da59612eca71a23e7000914 Mon Sep 17 00:00:00 2001 From: Ming Wen Date: Tue, 10 Mar 2026 12:32:22 +0800 Subject: [PATCH 3/3] fix(*): apply NormalizeLabel to all resource export, delete, and list commands Same label normalization bug existed across all resource types. Apply cmdutil.NormalizeLabel consistently so --label env=test is correctly sent as label=env:test to the APISIX Admin API. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus --- pkg/cmd/consumer/delete/delete.go | 2 +- pkg/cmd/consumer/export/export.go | 2 +- pkg/cmd/consumergroup/delete/delete.go | 2 +- pkg/cmd/consumergroup/export/export.go | 2 +- pkg/cmd/globalrule/delete/delete.go | 2 +- pkg/cmd/globalrule/export/export.go | 2 +- pkg/cmd/pluginconfig/delete/delete.go | 2 +- pkg/cmd/pluginconfig/export/export.go | 2 +- pkg/cmd/proto/delete/delete.go | 2 +- pkg/cmd/proto/export/export.go | 2 +- pkg/cmd/service/delete/delete.go | 2 +- pkg/cmd/service/export/export.go | 2 +- pkg/cmd/service/list/list.go | 2 +- pkg/cmd/ssl/delete/delete.go | 2 +- pkg/cmd/ssl/export/export.go | 2 +- pkg/cmd/streamroute/delete/delete.go | 2 +- pkg/cmd/streamroute/export/export.go | 2 +- pkg/cmd/upstream/delete/delete.go | 2 +- pkg/cmd/upstream/export/export.go | 2 +- pkg/cmd/upstream/list/list.go | 2 +- 20 files changed, 20 insertions(+), 20 deletions(-) diff --git a/pkg/cmd/consumer/delete/delete.go b/pkg/cmd/consumer/delete/delete.go index 1f786bb..c7857cf 100644 --- a/pkg/cmd/consumer/delete/delete.go +++ b/pkg/cmd/consumer/delete/delete.go @@ -174,7 +174,7 @@ func listAllConsumerUsernames(client *api.Client, label string) ([]string, error "page_size": fmt.Sprintf("%d", pageSize), } if label != "" { - query["label"] = label + query["label"] = cmdutil.NormalizeLabel(label) } body, err := client.Get("/apisix/admin/consumers", query) diff --git a/pkg/cmd/consumer/export/export.go b/pkg/cmd/consumer/export/export.go index fb20a99..e274c72 100644 --- a/pkg/cmd/consumer/export/export.go +++ b/pkg/cmd/consumer/export/export.go @@ -109,7 +109,7 @@ func fetchAll(client *api.Client, label string) ([]api.ListItem[api.Consumer], e "page_size": fmt.Sprintf("%d", pageSize), } if label != "" { - query["label"] = label + query["label"] = cmdutil.NormalizeLabel(label) } body, err := client.Get("/apisix/admin/consumers", query) diff --git a/pkg/cmd/consumergroup/delete/delete.go b/pkg/cmd/consumergroup/delete/delete.go index 1264a18..d0cba2b 100644 --- a/pkg/cmd/consumergroup/delete/delete.go +++ b/pkg/cmd/consumergroup/delete/delete.go @@ -174,7 +174,7 @@ func listAllConsumerGroupIDs(client *api.Client, label string) ([]string, error) "page_size": fmt.Sprintf("%d", pageSize), } if label != "" { - query["label"] = label + query["label"] = cmdutil.NormalizeLabel(label) } body, err := client.Get("/apisix/admin/consumer_groups", query) diff --git a/pkg/cmd/consumergroup/export/export.go b/pkg/cmd/consumergroup/export/export.go index 6ebe12f..1898478 100644 --- a/pkg/cmd/consumergroup/export/export.go +++ b/pkg/cmd/consumergroup/export/export.go @@ -109,7 +109,7 @@ func fetchAll(client *api.Client, label string) ([]api.ListItem[api.ConsumerGrou "page_size": fmt.Sprintf("%d", pageSize), } if label != "" { - query["label"] = label + query["label"] = cmdutil.NormalizeLabel(label) } body, err := client.Get("/apisix/admin/consumer_groups", query) diff --git a/pkg/cmd/globalrule/delete/delete.go b/pkg/cmd/globalrule/delete/delete.go index 233ccdd..42a2d20 100644 --- a/pkg/cmd/globalrule/delete/delete.go +++ b/pkg/cmd/globalrule/delete/delete.go @@ -174,7 +174,7 @@ func listAllGlobalRuleIDs(client *api.Client, label string) ([]string, error) { "page_size": fmt.Sprintf("%d", pageSize), } if label != "" { - query["label"] = label + query["label"] = cmdutil.NormalizeLabel(label) } body, err := client.Get("/apisix/admin/global_rules", query) diff --git a/pkg/cmd/globalrule/export/export.go b/pkg/cmd/globalrule/export/export.go index 8d91bc6..1167518 100644 --- a/pkg/cmd/globalrule/export/export.go +++ b/pkg/cmd/globalrule/export/export.go @@ -109,7 +109,7 @@ func fetchAll(client *api.Client, label string) ([]api.ListItem[api.GlobalRule], "page_size": fmt.Sprintf("%d", pageSize), } if label != "" { - query["label"] = label + query["label"] = cmdutil.NormalizeLabel(label) } body, err := client.Get("/apisix/admin/global_rules", query) diff --git a/pkg/cmd/pluginconfig/delete/delete.go b/pkg/cmd/pluginconfig/delete/delete.go index 586c296..6f9b7be 100644 --- a/pkg/cmd/pluginconfig/delete/delete.go +++ b/pkg/cmd/pluginconfig/delete/delete.go @@ -173,7 +173,7 @@ func listAllPluginConfigIDs(client *api.Client, label string) ([]string, error) "page_size": fmt.Sprintf("%d", pageSize), } if label != "" { - query["label"] = label + query["label"] = cmdutil.NormalizeLabel(label) } body, err := client.Get("/apisix/admin/plugin_configs", query) diff --git a/pkg/cmd/pluginconfig/export/export.go b/pkg/cmd/pluginconfig/export/export.go index de27b14..676fc59 100644 --- a/pkg/cmd/pluginconfig/export/export.go +++ b/pkg/cmd/pluginconfig/export/export.go @@ -109,7 +109,7 @@ func fetchAll(client *api.Client, label string) ([]api.ListItem[api.PluginConfig "page_size": fmt.Sprintf("%d", pageSize), } if label != "" { - query["label"] = label + query["label"] = cmdutil.NormalizeLabel(label) } body, err := client.Get("/apisix/admin/plugin_configs", query) diff --git a/pkg/cmd/proto/delete/delete.go b/pkg/cmd/proto/delete/delete.go index 046175a..19b9282 100644 --- a/pkg/cmd/proto/delete/delete.go +++ b/pkg/cmd/proto/delete/delete.go @@ -174,7 +174,7 @@ func listAllProtoIDs(client *api.Client, label string) ([]string, error) { "page_size": fmt.Sprintf("%d", pageSize), } if label != "" { - query["label"] = label + query["label"] = cmdutil.NormalizeLabel(label) } body, err := client.Get("/apisix/admin/protos", query) diff --git a/pkg/cmd/proto/export/export.go b/pkg/cmd/proto/export/export.go index c48ecaf..23b2648 100644 --- a/pkg/cmd/proto/export/export.go +++ b/pkg/cmd/proto/export/export.go @@ -109,7 +109,7 @@ func fetchAll(client *api.Client, label string) ([]api.ListItem[api.Proto], erro "page_size": fmt.Sprintf("%d", pageSize), } if label != "" { - query["label"] = label + query["label"] = cmdutil.NormalizeLabel(label) } body, err := client.Get("/apisix/admin/protos", query) diff --git a/pkg/cmd/service/delete/delete.go b/pkg/cmd/service/delete/delete.go index 650dec9..f927c35 100644 --- a/pkg/cmd/service/delete/delete.go +++ b/pkg/cmd/service/delete/delete.go @@ -174,7 +174,7 @@ func listAllServiceIDs(client *api.Client, label string) ([]string, error) { "page_size": fmt.Sprintf("%d", pageSize), } if label != "" { - query["label"] = label + query["label"] = cmdutil.NormalizeLabel(label) } body, err := client.Get("/apisix/admin/services", query) diff --git a/pkg/cmd/service/export/export.go b/pkg/cmd/service/export/export.go index b03494f..f1c8846 100644 --- a/pkg/cmd/service/export/export.go +++ b/pkg/cmd/service/export/export.go @@ -109,7 +109,7 @@ func fetchAll(client *api.Client, label string) ([]api.ListItem[api.Service], er "page_size": fmt.Sprintf("%d", pageSize), } if label != "" { - query["label"] = label + query["label"] = cmdutil.NormalizeLabel(label) } body, err := client.Get("/apisix/admin/services", query) diff --git a/pkg/cmd/service/list/list.go b/pkg/cmd/service/list/list.go index ea32919..651dc8a 100644 --- a/pkg/cmd/service/list/list.go +++ b/pkg/cmd/service/list/list.go @@ -76,7 +76,7 @@ func listRun(opts *Options) error { query["name"] = opts.Name } if opts.Label != "" { - query["label"] = opts.Label + query["label"] = cmdutil.NormalizeLabel(opts.Label) } body, err := client.Get("/apisix/admin/services", query) diff --git a/pkg/cmd/ssl/delete/delete.go b/pkg/cmd/ssl/delete/delete.go index c18c5b8..a01b816 100644 --- a/pkg/cmd/ssl/delete/delete.go +++ b/pkg/cmd/ssl/delete/delete.go @@ -174,7 +174,7 @@ func listAllSSLIDs(client *api.Client, label string) ([]string, error) { "page_size": fmt.Sprintf("%d", pageSize), } if label != "" { - query["label"] = label + query["label"] = cmdutil.NormalizeLabel(label) } body, err := client.Get("/apisix/admin/ssls", query) diff --git a/pkg/cmd/ssl/export/export.go b/pkg/cmd/ssl/export/export.go index 1893313..7766ad7 100644 --- a/pkg/cmd/ssl/export/export.go +++ b/pkg/cmd/ssl/export/export.go @@ -109,7 +109,7 @@ func fetchAll(client *api.Client, label string) ([]api.ListItem[api.SSL], error) "page_size": fmt.Sprintf("%d", pageSize), } if label != "" { - query["label"] = label + query["label"] = cmdutil.NormalizeLabel(label) } body, err := client.Get("/apisix/admin/ssls", query) diff --git a/pkg/cmd/streamroute/delete/delete.go b/pkg/cmd/streamroute/delete/delete.go index 5230cb3..e8359c1 100644 --- a/pkg/cmd/streamroute/delete/delete.go +++ b/pkg/cmd/streamroute/delete/delete.go @@ -174,7 +174,7 @@ func listAllStreamRouteIDs(client *api.Client, label string) ([]string, error) { "page_size": fmt.Sprintf("%d", pageSize), } if label != "" { - query["label"] = label + query["label"] = cmdutil.NormalizeLabel(label) } body, err := client.Get("/apisix/admin/stream_routes", query) diff --git a/pkg/cmd/streamroute/export/export.go b/pkg/cmd/streamroute/export/export.go index fcbf59d..b08150a 100644 --- a/pkg/cmd/streamroute/export/export.go +++ b/pkg/cmd/streamroute/export/export.go @@ -109,7 +109,7 @@ func fetchAll(client *api.Client, label string) ([]api.ListItem[api.StreamRoute] "page_size": fmt.Sprintf("%d", pageSize), } if label != "" { - query["label"] = label + query["label"] = cmdutil.NormalizeLabel(label) } body, err := client.Get("/apisix/admin/stream_routes", query) diff --git a/pkg/cmd/upstream/delete/delete.go b/pkg/cmd/upstream/delete/delete.go index 50b5167..b033ffc 100644 --- a/pkg/cmd/upstream/delete/delete.go +++ b/pkg/cmd/upstream/delete/delete.go @@ -174,7 +174,7 @@ func listAllUpstreamIDs(client *api.Client, label string) ([]string, error) { "page_size": fmt.Sprintf("%d", pageSize), } if label != "" { - query["label"] = label + query["label"] = cmdutil.NormalizeLabel(label) } body, err := client.Get("/apisix/admin/upstreams", query) diff --git a/pkg/cmd/upstream/export/export.go b/pkg/cmd/upstream/export/export.go index 835b654..b490513 100644 --- a/pkg/cmd/upstream/export/export.go +++ b/pkg/cmd/upstream/export/export.go @@ -109,7 +109,7 @@ func fetchAll(client *api.Client, label string) ([]api.ListItem[api.Upstream], e "page_size": fmt.Sprintf("%d", pageSize), } if label != "" { - query["label"] = label + query["label"] = cmdutil.NormalizeLabel(label) } body, err := client.Get("/apisix/admin/upstreams", query) diff --git a/pkg/cmd/upstream/list/list.go b/pkg/cmd/upstream/list/list.go index 6e5e999..10a24a2 100644 --- a/pkg/cmd/upstream/list/list.go +++ b/pkg/cmd/upstream/list/list.go @@ -76,7 +76,7 @@ func listRun(opts *Options) error { query["name"] = opts.Name } if opts.Label != "" { - query["label"] = opts.Label + query["label"] = cmdutil.NormalizeLabel(opts.Label) } body, err := client.Get("/apisix/admin/upstreams", query)