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
15 changes: 10 additions & 5 deletions docs/user-guide/route.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,25 @@ The `a7 route` command allows you to manage API7 Enterprise Edition (API7 EE) ro

### `a7 route list`

Lists routes in the specified gateway group. API7 EE may require
`--service-id` to list routes; omit it only if your environment supports
unscoped route listing.
Lists routes in the specified gateway group. By default, every route across
every service in the gateway group is returned; pass `--service-id` to
narrow the result to a single service.

| Flag | Short | Default | Description |
|------|-------|---------|-------------|
| `--gateway-group` | `-g` | | Target gateway group name (required) |
| `--label` | | | Filter routes by label |
| `--service-id` | | | Filter routes by service ID |
| `--service-id` | | | Filter routes to a single service |
| `--output` | `-o` | `table` | Output format (table, json, yaml) |

**Examples:**

List routes for a service in the "default" gateway group:
List every route in the "default" gateway group:
```bash
a7 route list -g default
```

List routes for a single service:
```bash
a7 route list -g default --service-id example-service
```
Expand Down
90 changes: 9 additions & 81 deletions pkg/cmd/config/configutil/configutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"github.com/api7/a7/pkg/api"
"github.com/api7/a7/pkg/cmdutil"
"github.com/api7/a7/pkg/listutil"
)

type ResourceItem struct {
Expand Down Expand Up @@ -111,38 +112,38 @@ func FetchRemoteConfig(client *api.Client, gatewayGroup string) (*api.ConfigFile
query["gateway_group_id"] = gatewayGroup
}

services, err := fetchPaginated[api.Service](client, "/apisix/admin/services", query)
services, err := listutil.FetchPaginated[api.Service](client, "/apisix/admin/services", query)
if err != nil {
return nil, err
}

// API7 EE requires service_id when listing routes with access tokens.
// Fetch routes per service and aggregate.
routes, err := fetchRoutesForServices(client, services, query)
routes, err := listutil.FetchRoutesForServices(client, services, query)
if err != nil {
return nil, err
}
consumers, err := fetchPaginated[api.Consumer](client, "/apisix/admin/consumers", query)
consumers, err := listutil.FetchPaginated[api.Consumer](client, "/apisix/admin/consumers", query)
if err != nil {
return nil, err
}
ssl, err := fetchPaginated[api.SSL](client, "/apisix/admin/ssls", query)
ssl, err := listutil.FetchPaginated[api.SSL](client, "/apisix/admin/ssls", query)
if err != nil {
return nil, err
}
globalRules, err := fetchPaginated[api.GlobalRule](client, "/apisix/admin/global_rules", query)
globalRules, err := listutil.FetchPaginated[api.GlobalRule](client, "/apisix/admin/global_rules", query)
if err != nil {
return nil, err
}
streamRoutes, err := fetchPaginated[api.StreamRoute](client, "/apisix/admin/stream_routes", query)
streamRoutes, err := listutil.FetchPaginated[api.StreamRoute](client, "/apisix/admin/stream_routes", query)
if err != nil {
return nil, err
}
protos, err := fetchPaginated[api.Proto](client, "/apisix/admin/protos", query)
protos, err := listutil.FetchPaginated[api.Proto](client, "/apisix/admin/protos", query)
if err != nil {
return nil, err
}
secrets, err := fetchPaginated[api.Secret](client, "/apisix/admin/secret_providers", query)
secrets, err := listutil.FetchPaginated[api.Secret](client, "/apisix/admin/secret_providers", query)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -405,79 +406,6 @@ func toMapSlice(items interface{}) ([]map[string]interface{}, error) {
return out, nil
}

// fetchPaginated fetches all items from a paginated API7 EE list endpoint.
// API7 EE returns ListResponse[T] with .List []T directly (no ListItem wrapper).
func fetchPaginated[T any](client *api.Client, path string, extraQuery map[string]string) ([]T, error) {
page := 1
pageSize := 500
var items []T

for {
query := map[string]string{
"page": fmt.Sprintf("%d", page),
"page_size": fmt.Sprintf("%d", pageSize),
}
for k, v := range extraQuery {
query[k] = v
}

body, err := client.Get(path, query)
if err != nil {
if cmdutil.IsOptionalResourceError(err) {
return nil, nil
}
return nil, err
}

var resp api.ListResponse[T]
if err := json.Unmarshal(body, &resp); err != nil {
return nil, fmt.Errorf("failed to parse response: %w", err)
}

items = append(items, resp.List...)
if len(resp.List) == 0 || len(items) >= resp.Total {
break
}
page++
}

return items, nil
}

func fetchRoutesForServices(client *api.Client, services []api.Service, baseQuery map[string]string) ([]api.Route, error) {
seen := make(map[string]bool)
var allRoutes []api.Route
for _, svc := range services {
if svc.ID == "" {
continue
}
q := make(map[string]string, len(baseQuery)+1)
for k, v := range baseQuery {
q[k] = v
}
q["service_id"] = svc.ID
routes, err := fetchPaginated[api.Route](client, "/apisix/admin/routes", q)
if err != nil {
if cmdutil.IsOptionalResourceError(err) {
continue
}
return nil, err
}
for _, r := range routes {
key := r.ID
if key == "" {
allRoutes = append(allRoutes, r)
continue
}
if !seen[key] {
seen[key] = true
allRoutes = append(allRoutes, r)
}
}
}
return allRoutes, nil
}

func fetchPluginMetadata(client *api.Client, query map[string]string) ([]api.PluginMetadataEntry, error) {
body, err := client.Get("/apisix/admin/plugins/list", query)
if err != nil {
Expand Down
65 changes: 42 additions & 23 deletions pkg/cmd/route/list/list.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package list

import (
"encoding/json"
"fmt"
"net/http"
"strings"
Expand All @@ -13,6 +12,7 @@ import (
cmd "github.com/api7/a7/pkg/cmd"
"github.com/api7/a7/pkg/cmdutil"
"github.com/api7/a7/pkg/iostreams"
"github.com/api7/a7/pkg/listutil"
"github.com/api7/a7/pkg/tableprinter"
)

Expand Down Expand Up @@ -42,7 +42,7 @@ func NewCmd(f *cmd.Factory) *cobra.Command {
},
}
c.Flags().StringVar(&opts.Label, "label", "", "Filter by label (key=value)")
c.Flags().StringVar(&opts.ServiceID, "service-id", "", "Filter by service ID (required by API7 EE)")
c.Flags().StringVar(&opts.ServiceID, "service-id", "", "Filter to routes belonging to a single service; omit to list all routes in the gateway group")
return c
}

Expand All @@ -66,52 +66,71 @@ func actionRun(opts *Options) error {
}

client := api.NewClient(httpClient, cfg.BaseURL())
if opts.ServiceID == "" {
return fmt.Errorf("--service-id is required by API7 EE")
}
query := map[string]string{"gateway_group_id": ggID}
query["service_id"] = opts.ServiceID

labelKey, labelValue := cmdutil.ParseLabel(opts.Label)
if labelKey != "" {
query["label"] = labelKey
}
body, err := client.Get("/apisix/admin/routes", query)
baseQuery := map[string]string{"gateway_group_id": ggID}

routes, err := fetchRoutes(client, baseQuery, opts.ServiceID, labelKey)
if err != nil {
return fmt.Errorf("%s", cmdutil.FormatAPIError(err))
}

var resp api.ListResponse[api.Route]
if err := json.Unmarshal(body, &resp); err != nil {
return fmt.Errorf("failed to decode response: %w", err)
}

if labelValue != "" {
filtered := make([]api.Route, 0)
for _, item := range resp.List {
filtered := make([]api.Route, 0, len(routes))
for _, item := range routes {
if item.Labels != nil && item.Labels[labelKey] == labelValue {
filtered = append(filtered, item)
}
}
resp.List = filtered
routes = filtered
}

if opts.Output != "" {
exporter := cmdutil.NewExporter(opts.Output, opts.IO.Out)
return exporter.Write(resp.List)
return exporter.Write(routes)
}

tp := tableprinter.New(opts.IO.Out)
tp.SetHeaders("ID", "NAME", "PATHS", "METHODS", "STATUS")
for _, item := range resp.List {
tp.SetHeaders("ID", "NAME", "SERVICE_ID", "PATHS", "METHODS", "STATUS")
for _, item := range routes {
paths := strings.Join(item.Paths, ",")
if paths == "" {
paths = item.URI
if paths == "" && len(item.URIs) > 0 {
paths = strings.Join(item.URIs, ",")
}
}
tp.AddRow(item.ID, item.Name, paths, strings.Join(item.Methods, ","), fmt.Sprintf("%d", item.Status))
tp.AddRow(item.ID, item.Name, item.ServiceID, paths, strings.Join(item.Methods, ","), fmt.Sprintf("%d", item.Status))
}

return tp.Render()
}

// fetchRoutes returns the paginated route slice for the request. With a
// service ID, it pages through a single filtered query; without one, it lists
// every service in the gateway group and aggregates their routes (API7 EE
// requires `service_id` on this endpoint under access-token auth).
//
// labelKey is applied only to the /routes calls. /services discovery stays
// label-free so that services without the label are still enumerated and
// their matching routes returned.
func fetchRoutes(client *api.Client, baseQuery map[string]string, serviceID, labelKey string) ([]api.Route, error) {
routeQuery := make(map[string]string, len(baseQuery)+2)
for k, v := range baseQuery {
routeQuery[k] = v
}
if labelKey != "" {
routeQuery["label"] = labelKey
}

if serviceID != "" {
routeQuery["service_id"] = serviceID
return listutil.FetchPaginated[api.Route](client, "/apisix/admin/routes", routeQuery)
}

services, err := listutil.FetchPaginated[api.Service](client, "/apisix/admin/services", baseQuery)
if err != nil {
return nil, err
}
Comment thread
shreemaan-abhishek marked this conversation as resolved.
return listutil.FetchRoutesForServices(client, services, routeQuery)
}
Loading
Loading