Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pkg/cmd/consumer/delete/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/consumer/export/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/consumergroup/delete/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/consumergroup/export/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/globalrule/delete/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/globalrule/export/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/pluginconfig/delete/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/pluginconfig/export/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/proto/delete/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/proto/export/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
27 changes: 3 additions & 24 deletions pkg/cmd/route/delete/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
}

Expand Down
23 changes: 4 additions & 19 deletions pkg/cmd/route/export/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"io"
"net/http"
"os"
"strings"

"github.com/spf13/cobra"

Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down
12 changes: 5 additions & 7 deletions pkg/cmd/route/export/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines 72 to 83
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

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

The label-filter test doesn't actually validate filtering behavior: the mock server always returns only the matching route, regardless of what label query was sent. That means the test would still pass even if the command ignored --label entirely (as long as the query string check flips the boolean). Consider having the round tripper return different bodies depending on the label query (filtered vs unfiltered) and re-adding an assertion that non-matching routes are not present in the output.

Copilot uses AI. Check for mistakes.
}
return &http.Response{StatusCode: 404, Body: io.NopCloser(strings.NewReader(`{"error_msg":"not found"}`))}, nil
Expand All @@ -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")
}
2 changes: 1 addition & 1 deletion pkg/cmd/route/list/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/service/delete/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/service/export/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/service/list/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/ssl/delete/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/ssl/export/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/streamroute/delete/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/streamroute/export/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/upstream/delete/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/upstream/export/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion pkg/cmd/upstream/list/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
13 changes: 13 additions & 0 deletions pkg/cmdutil/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cmdutil
import (
"errors"
"fmt"
"strings"

"github.com/api7/a6/pkg/api"
)
Expand Down Expand Up @@ -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
Comment on lines +89 to +93
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

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

NormalizeLabel currently rewrites any string containing "=" into "key:value" even when either side is empty (e.g., "env=" -> "env:", "=test" -> ":test"). That produces an invalid/ambiguous label query and makes it hard to surface a clear user error. Consider only normalizing when both key and value are non-empty (or trimming whitespace), and otherwise returning the original string (or changing the helper to return an error so callers can raise a FlagError).

Suggested change
parts := strings.SplitN(label, "=", 2)
if len(parts) == 2 {
return parts[0] + ":" + parts[1]
}
return label
parts := strings.SplitN(label, "=", 2)
if len(parts) != 2 {
return label
}
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
if key == "" || value == "" {
// Leave invalid/ambiguous labels unchanged so callers can surface a clear error.
return label
}
return key + ":" + value

Copilot uses AI. Check for mistakes.
}
27 changes: 27 additions & 0 deletions pkg/cmdutil/errors_test.go
Original file line number Diff line number Diff line change
@@ -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))
})
Comment on lines +22 to +25
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

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

This table-driven test uses tc.input as the subtest name; the empty-string case creates an unnamed subtest ("TestNormalizeLabel/") which is awkward to read and filter with -run. Consider using the test index or a formatted/quoted name (e.g., fmt.Sprintf(%q, tc.input)) so every subtest has a stable, non-empty name.

Copilot uses AI. Check for mistakes.
}
}
Loading