From 698795d843bb72f79a00b8cddc22169b5e824fd4 Mon Sep 17 00:00:00 2001 From: Abhishek Choudhary Date: Mon, 18 May 2026 16:07:39 +0800 Subject: [PATCH] chore(plugin-config): remove command not exposed by API7 EE The API7 EE control-plane does not expose /apisix/admin/plugin_configs as a management resource (verified against api7ee-3-control-plane: no routes, no handlers, not in the generated OpenAPI spec). Mirrors PR #21 (upstream/consumer-group/service-template): - delete pkg/cmd/plugin-config/ and pkg/api/types_plugin_config.go - unregister from pkg/cmd/root/root.go - drop plugin_configs from config dump/diff/sync paths - add plugin_configs to declarative-config validation rejection alongside upstreams/consumer_groups/service_templates - replace the old 'compatibility error' e2e file with the standard TestUnsupportedResourceCommandsAreRemoved + TestHelp assertions - extend TestConfigValidate_EmptyUnsupportedSections with plugin_configs - PRD: align the Plugin Config note with the consumer-groups treatment The CP still accepts plugin_configs *inside* batch config-validation payloads, so the rejection only applies to a7's standalone command and the top-level declarative section. Closes #24. --- PRD.md | 8 +- docs/ga-test-plan.md | 4 +- pkg/api/types_config.go | 7 +- pkg/api/types_plugin_config.go | 10 -- pkg/cmd/config/configutil/configutil.go | 20 +-- pkg/cmd/config/diff/diff_test.go | 1 - pkg/cmd/config/dump/dump_test.go | 1 - pkg/cmd/config/sync/sync.go | 4 - pkg/cmd/config/sync/sync_test.go | 1 - pkg/cmd/config/validate/validate.go | 4 +- pkg/cmd/config/validate/validate_test.go | 5 + pkg/cmd/plugin-config/create/create.go | 141 ------------------ pkg/cmd/plugin-config/create/create_test.go | 134 ------------------ pkg/cmd/plugin-config/delete/delete.go | 84 ----------- pkg/cmd/plugin-config/delete/delete_test.go | 104 -------------- pkg/cmd/plugin-config/export/export.go | 147 ------------------- pkg/cmd/plugin-config/export/export_test.go | 78 ---------- pkg/cmd/plugin-config/get/get.go | 87 ------------ pkg/cmd/plugin-config/get/get_test.go | 126 ----------------- pkg/cmd/plugin-config/list/list.go | 103 -------------- pkg/cmd/plugin-config/list/list_test.go | 148 ------------------- pkg/cmd/plugin-config/plugin_config.go | 30 ---- pkg/cmd/plugin-config/update/update.go | 132 ----------------- pkg/cmd/plugin-config/update/update_test.go | 149 -------------------- pkg/cmd/root/root.go | 2 - test/e2e/completion_version_test.go | 3 +- test/e2e/plugin_config_test.go | 35 ----- 27 files changed, 27 insertions(+), 1541 deletions(-) delete mode 100644 pkg/api/types_plugin_config.go delete mode 100644 pkg/cmd/plugin-config/create/create.go delete mode 100644 pkg/cmd/plugin-config/create/create_test.go delete mode 100644 pkg/cmd/plugin-config/delete/delete.go delete mode 100644 pkg/cmd/plugin-config/delete/delete_test.go delete mode 100644 pkg/cmd/plugin-config/export/export.go delete mode 100644 pkg/cmd/plugin-config/export/export_test.go delete mode 100644 pkg/cmd/plugin-config/get/get.go delete mode 100644 pkg/cmd/plugin-config/get/get_test.go delete mode 100644 pkg/cmd/plugin-config/list/list.go delete mode 100644 pkg/cmd/plugin-config/list/list_test.go delete mode 100644 pkg/cmd/plugin-config/plugin_config.go delete mode 100644 pkg/cmd/plugin-config/update/update.go delete mode 100644 pkg/cmd/plugin-config/update/update_test.go delete mode 100644 test/e2e/plugin_config_test.go diff --git a/PRD.md b/PRD.md index c050eae..a99b594 100644 --- a/PRD.md +++ b/PRD.md @@ -152,7 +152,7 @@ All runtime commands require `--gateway-group ` (or default from context). - `a7 stream-route list|get|create|update|delete|export --gateway-group ` #### Plugin Config -> **⚠️ NOT SUPPORTED**: Plugin configs are not exposed via the API7 EE Admin API. The `a7 plugin-config` commands exist for APISIX compatibility but will not work against API7 EE. +> **⚠️ NOT SUPPORTED**: Plugin configs are not exposed via the API7 EE Admin API, so a7 does not expose plugin-config commands. #### Plugin Metadata - `a7 plugin-metadata get|create|update|delete --gateway-group ` (no list — keyed by plugin name) @@ -207,7 +207,7 @@ All runtime commands require `--gateway-group ` (or default from context). ### Phase 3 — CLI Usability ✅ COMPLETE 1. ✅ `-f/--file` flag: file-based create/update for all resource commands. -2. ✅ `export` subcommand for all applicable resources (route, service, consumer, ssl, global-rule, stream-route, plugin-config, proto). +2. ✅ `export` subcommand for all applicable resources (route, service, consumer, ssl, global-rule, stream-route, proto). 3. ✅ `--force` flag for delete commands (skip confirmation). 4. ✅ `--label` flag for list/export commands (label-based filtering). 5. 🔲 `--dry-run` flag for create/update commands. @@ -229,7 +229,7 @@ All runtime commands require `--gateway-group ` (or default from context). 6. ✅ `docs/documentation-maintenance.md` — Doc update rules and templates. 7. ✅ `docs/roadmap.md` — Per-PR development plan for Phases 5-9. 8. ✅ `docs/api7ee-api-spec.md` — API7 EE Admin API reference (16 resources, dual-API). -9. ✅ `docs/user-guide/` — per-resource user guides (getting-started, configuration, route, service, upstream, consumer, ssl, plugin, global-rule, stream-route, plugin-config, plugin-metadata, credential, secret, proto, declarative-config, gateway-group, debug, bulk-operations). +9. ✅ `docs/user-guide/` — per-resource user guides (getting-started, configuration, route, service, upstream, consumer, ssl, plugin, global-rule, stream-route, plugin-metadata, credential, secret, proto, declarative-config, gateway-group, debug, bulk-operations). ### Phase 6 — AI Agent Skills ✅ COMPLETE Port and adapt 40 SKILL.md files from a6, organized by category: @@ -255,7 +255,7 @@ Port and adapt 40 SKILL.md files from a6, organized by category: ### Phase 8 — End-to-End Tests ✅ COMPLETE 1. ✅ `test/e2e/docker-compose.yml` — Docker Compose for API7 EE (Dashboard + DP Manager + Gateway + PostgreSQL). 2. ✅ `test/e2e/setup_test.go` — TestMain, binary build, admin/control API helpers, shared test utilities. -3. ✅ Per-resource E2E tests: route, service, consumer, ssl, plugin, global-rule, stream-route, plugin-config, plugin-metadata, credential, secret, proto, context, gateway-group. +3. ✅ Per-resource E2E tests: route, service, consumer, ssl, plugin, global-rule, stream-route, plugin-metadata, credential, secret, proto, context, gateway-group. 4. ✅ Declarative config E2E tests: dump, diff, sync, validate (config_test.go + config_sync_test.go). 5. ✅ Export and label E2E tests (integrated into resource test files). 6. ✅ Debug E2E tests: trace (JSON/method/headers/host/path) + logs (file mode). diff --git a/docs/ga-test-plan.md b/docs/ga-test-plan.md index 247d417..7d45e67 100644 --- a/docs/ga-test-plan.md +++ b/docs/ga-test-plan.md @@ -141,7 +141,7 @@ rejected — not present as broken commands. | `a7 upstream ...` | command does not exist (unknown command error) | | `a7 consumer-group ...` | command does not exist | | `a7 service-template ...` | command does not exist | -| `a7 plugin-config ...` | **after Task #2**: command does not exist | +| `a7 plugin-config ...` | command does not exist | | `a7 stream-route ...` | **works** — full CRUD (verified exposed by the control plane) | Declarative config — these top-level sections must be **rejected with a clear error**: @@ -150,7 +150,7 @@ Declarative config — these top-level sections must be **rejected with a clear upstreams: [...] # -> validation error consumer_groups: [...] # -> validation error service_templates: [...] # -> validation error -plugin_configs: [...] # -> validation error (after Task #2) +plugin_configs: [...] # -> validation error ``` > Caveat: the control plane still accepts `plugin_configs` *inside batch config-validation diff --git a/pkg/api/types_config.go b/pkg/api/types_config.go index 58feb71..9b4c98f 100644 --- a/pkg/api/types_config.go +++ b/pkg/api/types_config.go @@ -10,10 +10,11 @@ type ConfigFile struct { Consumers []Consumer `json:"consumers,omitempty" yaml:"consumers,omitempty"` SSL []SSL `json:"ssl,omitempty" yaml:"ssl,omitempty"` GlobalRules []GlobalRule `json:"global_rules,omitempty" yaml:"global_rules,omitempty"` - PluginConfigs []PluginConfig `json:"plugin_configs,omitempty" yaml:"plugin_configs,omitempty"` ConsumerGroups []ConsumerGroup `json:"consumer_groups,omitempty" yaml:"consumer_groups,omitempty"` - // ServiceTemplates is captured only so the section can be explicitly - // rejected; API7 EE does not support it as a top-level resource. + // PluginConfigs and ServiceTemplates are captured only so the sections + // can be explicitly rejected; API7 EE does not expose them as top-level + // resources. + PluginConfigs []interface{} `json:"plugin_configs,omitempty" yaml:"plugin_configs,omitempty"` ServiceTemplates []interface{} `json:"service_templates,omitempty" yaml:"service_templates,omitempty"` StreamRoutes []StreamRoute `json:"stream_routes,omitempty" yaml:"stream_routes,omitempty"` Protos []Proto `json:"protos,omitempty" yaml:"protos,omitempty"` diff --git a/pkg/api/types_plugin_config.go b/pkg/api/types_plugin_config.go deleted file mode 100644 index 06d1da2..0000000 --- a/pkg/api/types_plugin_config.go +++ /dev/null @@ -1,10 +0,0 @@ -package api - -// PluginConfig represents a reusable plugin configuration in API7 EE (runtime). -// Plugin configs can be referenced by routes to share plugin settings. -type PluginConfig struct { - ID string `json:"id,omitempty" yaml:"id,omitempty"` - Desc string `json:"desc,omitempty" yaml:"desc,omitempty"` - Plugins map[string]interface{} `json:"plugins,omitempty" yaml:"plugins,omitempty"` - Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"` -} diff --git a/pkg/cmd/config/configutil/configutil.go b/pkg/cmd/config/configutil/configutil.go index f485915..fcb3a5b 100644 --- a/pkg/cmd/config/configutil/configutil.go +++ b/pkg/cmd/config/configutil/configutil.go @@ -40,7 +40,6 @@ type DiffResult struct { Consumers ResourceDiff `json:"consumers"` SSL ResourceDiff `json:"ssl"` GlobalRules ResourceDiff `json:"global_rules"` - PluginConfigs ResourceDiff `json:"plugin_configs"` StreamRoutes ResourceDiff `json:"stream_routes"` Protos ResourceDiff `json:"protos"` Secrets ResourceDiff `json:"secrets"` @@ -73,7 +72,6 @@ func (r *DiffResult) Sections() []DiffSection { return []DiffSection{ {Name: "services", Diff: r.Services}, {Name: "consumers", Diff: r.Consumers}, - {Name: "plugin_configs", Diff: r.PluginConfigs}, {Name: "ssl", Diff: r.SSL}, {Name: "global_rules", Diff: r.GlobalRules}, {Name: "protos", Diff: r.Protos}, @@ -136,10 +134,6 @@ func FetchRemoteConfig(client *api.Client, gatewayGroup string) (*api.ConfigFile if err != nil { return nil, err } - pluginConfigs, err := fetchPaginated[api.PluginConfig](client, "/apisix/admin/plugin_configs", query) - if err != nil { - return nil, err - } streamRoutes, err := fetchPaginated[api.StreamRoute](client, "/apisix/admin/stream_routes", query) if err != nil { return nil, err @@ -165,7 +159,6 @@ func FetchRemoteConfig(client *api.Client, gatewayGroup string) (*api.ConfigFile Consumers: stripTimestampsFromSlice(consumers), SSL: stripTimestampsFromSlice(ssl), GlobalRules: stripTimestampsFromSlice(globalRules), - PluginConfigs: stripTimestampsFromSlice(pluginConfigs), StreamRoutes: stripTimestampsFromSlice(streamRoutes), Protos: stripTimestampsFromSlice(protos), Secrets: stripTimestampsFromSlice(secrets), @@ -196,7 +189,6 @@ func ComputeDiff(local, remote api.ConfigFile) (*DiffResult, error) { {local.Consumers, remote.Consumers, "username", "consumers"}, {local.SSL, remote.SSL, "id", "ssl"}, {local.GlobalRules, remote.GlobalRules, "id", "global_rules"}, - {local.PluginConfigs, remote.PluginConfigs, "id", "plugin_configs"}, {local.StreamRoutes, remote.StreamRoutes, "id", "stream_routes"}, {local.Protos, remote.Protos, "id", "protos"}, {local.Secrets, remote.Secrets, "id", "secrets"}, @@ -226,11 +218,10 @@ func ComputeDiff(local, remote api.ConfigFile) (*DiffResult, error) { Consumers: diffs[2], SSL: diffs[3], GlobalRules: diffs[4], - PluginConfigs: diffs[5], - StreamRoutes: diffs[6], - Protos: diffs[7], - Secrets: diffs[8], - PluginMetadata: diffs[9], + StreamRoutes: diffs[5], + Protos: diffs[6], + Secrets: diffs[7], + PluginMetadata: diffs[8], }, nil } @@ -246,6 +237,9 @@ func ValidateSupportedSections(cfg api.ConfigFile) error { if cfg.ConsumerGroups != nil { unsupported = append(unsupported, "consumer_groups") } + if cfg.PluginConfigs != nil { + unsupported = append(unsupported, "plugin_configs") + } if cfg.ServiceTemplates != nil { unsupported = append(unsupported, "service_templates") } diff --git a/pkg/cmd/config/diff/diff_test.go b/pkg/cmd/config/diff/diff_test.go index ab707f2..07c293f 100644 --- a/pkg/cmd/config/diff/diff_test.go +++ b/pkg/cmd/config/diff/diff_test.go @@ -40,7 +40,6 @@ func registerEmptyResources(reg *httpmock.Registry, skip map[string]bool) { "/apisix/admin/consumers", "/apisix/admin/ssls", "/apisix/admin/global_rules", - "/apisix/admin/plugin_configs", "/apisix/admin/stream_routes", "/apisix/admin/protos", "/apisix/admin/secret_providers", diff --git a/pkg/cmd/config/dump/dump_test.go b/pkg/cmd/config/dump/dump_test.go index f3af736..ce57c10 100644 --- a/pkg/cmd/config/dump/dump_test.go +++ b/pkg/cmd/config/dump/dump_test.go @@ -45,7 +45,6 @@ func registerEmptyResources(reg *httpmock.Registry, skip map[string]bool) { "/apisix/admin/consumers", "/apisix/admin/ssls", "/apisix/admin/global_rules", - "/apisix/admin/plugin_configs", "/apisix/admin/stream_routes", "/apisix/admin/protos", "/apisix/admin/secret_providers", diff --git a/pkg/cmd/config/sync/sync.go b/pkg/cmd/config/sync/sync.go index 71145df..3485446 100644 --- a/pkg/cmd/config/sync/sync.go +++ b/pkg/cmd/config/sync/sync.go @@ -212,8 +212,6 @@ func putPathAndBody(resourceType, key string, payload map[string]interface{}) (s return fmt.Sprintf("/apisix/admin/ssls/%s", key), payload, nil case "global_rules": return fmt.Sprintf("/apisix/admin/global_rules/%s", key), payload, nil - case "plugin_configs": - return fmt.Sprintf("/apisix/admin/plugin_configs/%s", key), payload, nil case "stream_routes": return fmt.Sprintf("/apisix/admin/stream_routes/%s", key), payload, nil case "protos": @@ -240,8 +238,6 @@ func deletePath(resourceType, key string) (string, error) { return fmt.Sprintf("/apisix/admin/ssls/%s", key), nil case "global_rules": return fmt.Sprintf("/apisix/admin/global_rules/%s", key), nil - case "plugin_configs": - return fmt.Sprintf("/apisix/admin/plugin_configs/%s", key), nil case "stream_routes": return fmt.Sprintf("/apisix/admin/stream_routes/%s", key), nil case "protos": diff --git a/pkg/cmd/config/sync/sync_test.go b/pkg/cmd/config/sync/sync_test.go index 44308f8..a28a104 100644 --- a/pkg/cmd/config/sync/sync_test.go +++ b/pkg/cmd/config/sync/sync_test.go @@ -41,7 +41,6 @@ func registerEmptyResources(reg *httpmock.Registry, skip map[string]bool) { "/apisix/admin/consumers", "/apisix/admin/ssls", "/apisix/admin/global_rules", - "/apisix/admin/plugin_configs", "/apisix/admin/stream_routes", "/apisix/admin/protos", "/apisix/admin/secret_providers", diff --git a/pkg/cmd/config/validate/validate.go b/pkg/cmd/config/validate/validate.go index 8f93100..421107c 100644 --- a/pkg/cmd/config/validate/validate.go +++ b/pkg/cmd/config/validate/validate.go @@ -107,6 +107,9 @@ func ValidateConfigFile(cfg api.ConfigFile) []string { if cfg.ConsumerGroups != nil { errs = append(errs, "consumer_groups are not supported by current API7 EE") } + if cfg.PluginConfigs != nil { + errs = append(errs, "plugin_configs are not supported by current API7 EE") + } if cfg.ServiceTemplates != nil { errs = append(errs, "service_templates are not supported by current API7 EE") } @@ -161,7 +164,6 @@ func ValidateConfigFile(cfg api.ConfigFile) []string { errs = append(errs, checkDuplicateIDs(cfg.SSL, func(s api.SSL) string { return s.ID }, "ssl")...) errs = append(errs, checkDuplicateIDs(cfg.GlobalRules, func(g api.GlobalRule) string { return g.ID }, "global_rules")...) - errs = append(errs, checkDuplicateIDs(cfg.PluginConfigs, func(p api.PluginConfig) string { return p.ID }, "plugin_configs")...) for i, item := range cfg.StreamRoutes { if strings.TrimSpace(item.ServiceID) == "" { errs = append(errs, fmt.Sprintf("stream_routes[%d]: service_id is required by API7 EE", i)) diff --git a/pkg/cmd/config/validate/validate_test.go b/pkg/cmd/config/validate/validate_test.go index ac9244d..5ae55e7 100644 --- a/pkg/cmd/config/validate/validate_test.go +++ b/pkg/cmd/config/validate/validate_test.go @@ -307,6 +307,11 @@ func TestConfigValidate_EmptyUnsupportedSections(t *testing.T) { body: "version: \"1\"\nconsumer_groups: []\n", wantErr: "consumer_groups are not supported", }, + { + name: "plugin_configs", + body: "version: \"1\"\nplugin_configs: []\n", + wantErr: "plugin_configs are not supported", + }, { name: "service_templates", body: "version: \"1\"\nservice_templates: []\n", diff --git a/pkg/cmd/plugin-config/create/create.go b/pkg/cmd/plugin-config/create/create.go deleted file mode 100644 index 97df693..0000000 --- a/pkg/cmd/plugin-config/create/create.go +++ /dev/null @@ -1,141 +0,0 @@ -package create - -import ( - "encoding/json" - "fmt" - "net/http" - "strings" - - "github.com/spf13/cobra" - - "github.com/api7/a7/internal/config" - "github.com/api7/a7/pkg/api" - cmd "github.com/api7/a7/pkg/cmd" - "github.com/api7/a7/pkg/cmdutil" - "github.com/api7/a7/pkg/iostreams" -) - -type Options struct { - IO *iostreams.IOStreams - Client func() (*http.Client, error) - Config func() (config.Config, error) - Output string - GatewayGroup string - File string - - Desc string - PluginsJSON string - Labels []string -} - -func NewCmd(f *cmd.Factory) *cobra.Command { - opts := &Options{IO: f.IOStreams, Client: f.HttpClient, Config: f.Config} - c := &cobra.Command{ - Use: "create", - Short: "Create a reusable plugin configuration", - Args: cobra.NoArgs, - RunE: func(c *cobra.Command, args []string) error { - opts.Output, _ = c.Flags().GetString("output") - opts.GatewayGroup, _ = c.Flags().GetString("gateway-group") - return actionRun(opts) - }, - } - - c.Flags().StringVar(&opts.Desc, "desc", "", "Plugin config description") - c.Flags().StringVarP(&opts.File, "file", "f", "", "Path to JSON/YAML file with resource definition") - c.Flags().StringVar(&opts.PluginsJSON, "plugins-json", "", "Plugins JSON string") - c.Flags().StringSliceVar(&opts.Labels, "labels", nil, "Labels in key=value format") - - return c -} - -func actionRun(opts *Options) error { - cfg, err := opts.Config() - if err != nil { - return err - } - - ggID := opts.GatewayGroup - if ggID == "" { - ggID = cfg.GatewayGroup() - } - if ggID == "" { - return fmt.Errorf("gateway group is required; use --gateway-group flag or set a default in context config") - } - if opts.File != "" { - payload, err := cmdutil.ReadResourceFile(opts.File, opts.IO.In) - if err != nil { - return err - } - - httpClient, err := opts.Client() - if err != nil { - return err - } - - client := api.NewClient(httpClient, cfg.BaseURL()) - var body []byte - if id, ok := payload["id"]; ok { - body, err = client.Put(fmt.Sprintf("/apisix/admin/plugin_configs/%v?gateway_group_id=%s", id, ggID), payload) - } else { - body, err = client.Post("/apisix/admin/plugin_configs?gateway_group_id="+ggID, payload) - } - if err != nil { - return fmt.Errorf("%s", cmdutil.FormatAPISIXCompatibilityResourceError(err, "plugin-config")) - } - - format := opts.Output - if format == "" { - format = "json" - } - return cmdutil.NewExporter(format, opts.IO.Out).Write(json.RawMessage(body)) - } - if opts.PluginsJSON == "" { - return fmt.Errorf("--plugins-json is required") - } - - httpClient, err := opts.Client() - if err != nil { - return err - } - - plugins := map[string]interface{}{} - if err := json.Unmarshal([]byte(opts.PluginsJSON), &plugins); err != nil { - return fmt.Errorf("invalid --plugins-json: %w", err) - } - - labels := make(map[string]string) - for _, label := range opts.Labels { - parts := strings.SplitN(label, "=", 2) - if len(parts) != 2 || parts[0] == "" { - return fmt.Errorf("invalid label %q, expected key=value", label) - } - labels[parts[0]] = parts[1] - } - - bodyReq := api.PluginConfig{ - Desc: opts.Desc, - Plugins: plugins, - } - if len(labels) > 0 { - bodyReq.Labels = labels - } - - client := api.NewClient(httpClient, cfg.BaseURL()) - body, err := client.Post("/apisix/admin/plugin_configs?gateway_group_id="+ggID, bodyReq) - if err != nil { - return fmt.Errorf("%s", cmdutil.FormatAPISIXCompatibilityResourceError(err, "plugin-config")) - } - - var created api.PluginConfig - if err := json.Unmarshal(body, &created); err != nil { - return fmt.Errorf("failed to decode response: %w", err) - } - - format := opts.Output - if format == "" { - format = "json" - } - exporter := cmdutil.NewExporter(format, opts.IO.Out) - return exporter.Write(created) -} diff --git a/pkg/cmd/plugin-config/create/create_test.go b/pkg/cmd/plugin-config/create/create_test.go deleted file mode 100644 index 168a8ec..0000000 --- a/pkg/cmd/plugin-config/create/create_test.go +++ /dev/null @@ -1,134 +0,0 @@ -package create - -import ( - "encoding/json" - "net/http" - "strings" - "testing" - - "github.com/api7/a7/internal/config" - "github.com/api7/a7/pkg/api" - "github.com/api7/a7/pkg/httpmock" - "github.com/api7/a7/pkg/iostreams" -) - -type mockConfig struct { - baseURL string - token string - gatewayGroup string -} - -func (m *mockConfig) BaseURL() string { return m.baseURL } -func (m *mockConfig) Token() string { return m.token } -func (m *mockConfig) GatewayGroup() string { return m.gatewayGroup } -func (m *mockConfig) TLSSkipVerify() bool { return false } -func (m *mockConfig) CACert() string { return "" } -func (m *mockConfig) CurrentContext() string { return "test" } -func (m *mockConfig) Contexts() []config.Context { return nil } -func (m *mockConfig) GetContext(name string) (*config.Context, error) { return nil, nil } -func (m *mockConfig) AddContext(ctx config.Context) error { return nil } -func (m *mockConfig) RemoveContext(name string) error { return nil } -func (m *mockConfig) SetCurrentContext(name string) error { return nil } -func (m *mockConfig) Save() error { return nil } - -func TestCreatePluginConfig_Success(t *testing.T) { - ios, _, out, _ := iostreams.Test() - registry := &httpmock.Registry{} - registry.Register(http.MethodPost, "/apisix/admin/plugin_configs", httpmock.JSONResponse(`{"id":"pc1","desc":"auth","plugins":{"key-auth":{}}}`)) - - err := actionRun(&Options{ - IO: ios, - Client: func() (*http.Client, error) { return registry.GetClient(), nil }, - GatewayGroup: "gg1", - Desc: "auth", - PluginsJSON: `{"key-auth":{}}`, - Config: func() (config.Config, error) { - return &mockConfig{baseURL: "http://api.local", token: "test", gatewayGroup: "gg1"}, nil - }, - }) - if err != nil { - t.Fatalf("actionRun failed: %v", err) - } - - var item api.PluginConfig - if err := json.Unmarshal(out.Bytes(), &item); err != nil { - t.Fatalf("failed to parse JSON output: %v", err) - } - if item.ID != "pc1" { - t.Fatalf("unexpected response: %+v", item) - } - - registry.Verify(t) -} - -func TestCreatePluginConfig_ValidationError(t *testing.T) { - ios, _, _, _ := iostreams.Test() - err := actionRun(&Options{ - IO: ios, - Client: func() (*http.Client, error) { return (&httpmock.Registry{}).GetClient(), nil }, - GatewayGroup: "gg1", - Config: func() (config.Config, error) { - return &mockConfig{baseURL: "http://api.local", token: "test", gatewayGroup: "gg1"}, nil - }, - }) - if err == nil || !strings.Contains(err.Error(), "--plugins-json is required") { - t.Fatalf("expected missing plugins-json error, got: %v", err) - } -} - -func TestCreatePluginConfig_MissingGatewayGroup(t *testing.T) { - ios, _, _, _ := iostreams.Test() - err := actionRun(&Options{ - IO: ios, - Client: func() (*http.Client, error) { return (&httpmock.Registry{}).GetClient(), nil }, - PluginsJSON: `{"key-auth":{}}`, - Config: func() (config.Config, error) { - return &mockConfig{baseURL: "http://api.local", token: "test", gatewayGroup: ""}, nil - }, - }) - if err == nil || !strings.Contains(err.Error(), "gateway group is required") { - t.Fatalf("expected gateway group error, got: %v", err) - } -} - -func TestCreatePluginConfig_APIError(t *testing.T) { - ios, _, _, _ := iostreams.Test() - registry := &httpmock.Registry{} - registry.Register(http.MethodPost, "/apisix/admin/plugin_configs", httpmock.StringResponse(http.StatusInternalServerError, `{"message":"boom"}`)) - - err := actionRun(&Options{ - IO: ios, - Client: func() (*http.Client, error) { return registry.GetClient(), nil }, - GatewayGroup: "gg1", - PluginsJSON: `{"key-auth":{}}`, - Config: func() (config.Config, error) { - return &mockConfig{baseURL: "http://api.local", token: "test", gatewayGroup: "gg1"}, nil - }, - }) - if err == nil || !strings.Contains(err.Error(), "status 500") { - t.Fatalf("expected API error with status 500, got: %v", err) - } - - registry.Verify(t) -} - -func TestCreatePluginConfig_NotFoundExplainsAPI7EECompatibility(t *testing.T) { - ios, _, _, _ := iostreams.Test() - registry := &httpmock.Registry{} - registry.Register(http.MethodPost, "/apisix/admin/plugin_configs", httpmock.StringResponse(http.StatusNotFound, `{"error_msg":"not found"}`)) - - err := actionRun(&Options{ - IO: ios, - Client: func() (*http.Client, error) { return registry.GetClient(), nil }, - GatewayGroup: "gg1", - PluginsJSON: `{"key-auth":{}}`, - Config: func() (config.Config, error) { - return &mockConfig{baseURL: "http://api.local", token: "test", gatewayGroup: "gg1"}, nil - }, - }) - if err == nil || !strings.Contains(err.Error(), "APISIX compatibility") { - t.Fatalf("expected compatibility error, got: %v", err) - } - - registry.Verify(t) -} diff --git a/pkg/cmd/plugin-config/delete/delete.go b/pkg/cmd/plugin-config/delete/delete.go deleted file mode 100644 index 596dfe4..0000000 --- a/pkg/cmd/plugin-config/delete/delete.go +++ /dev/null @@ -1,84 +0,0 @@ -package delete - -import ( - "bufio" - "fmt" - "net/http" - "strings" - - "github.com/spf13/cobra" - - "github.com/api7/a7/internal/config" - "github.com/api7/a7/pkg/api" - cmd "github.com/api7/a7/pkg/cmd" - "github.com/api7/a7/pkg/cmdutil" - "github.com/api7/a7/pkg/iostreams" -) - -type Options struct { - IO *iostreams.IOStreams - Client func() (*http.Client, error) - Config func() (config.Config, error) - GatewayGroup string - ID string - Force bool -} - -func NewCmd(f *cmd.Factory) *cobra.Command { - opts := &Options{IO: f.IOStreams, Client: f.HttpClient, Config: f.Config} - c := &cobra.Command{ - Use: "delete ", - Short: "Delete a reusable plugin configuration", - Args: cobra.ExactArgs(1), - RunE: func(c *cobra.Command, args []string) error { - opts.ID = args[0] - opts.GatewayGroup, _ = c.Flags().GetString("gateway-group") - return actionRun(opts) - }, - } - c.Flags().BoolVar(&opts.Force, "force", false, "Skip confirmation prompt") - - return c -} - -func actionRun(opts *Options) error { - cfg, err := opts.Config() - if err != nil { - return err - } - - ggID := opts.GatewayGroup - if ggID == "" { - ggID = cfg.GatewayGroup() - } - if ggID == "" { - return fmt.Errorf("gateway group is required; use --gateway-group flag or set a default in context config") - } - - httpClient, err := opts.Client() - if err != nil { - return err - } - - client := api.NewClient(httpClient, cfg.BaseURL()) - query := map[string]string{"gateway_group_id": ggID} - if !opts.Force && opts.IO.IsStdinTTY() { - fmt.Fprintf(opts.IO.ErrOut, "Delete plugin config %q? (y/N): ", opts.ID) - reader := bufio.NewReader(opts.IO.In) - answer, err := reader.ReadString('\n') - if err != nil { - return fmt.Errorf("failed to read confirmation: %w", err) - } - answer = strings.TrimSpace(strings.ToLower(answer)) - if answer != "y" && answer != "yes" { - fmt.Fprintln(opts.IO.ErrOut, "Aborted.") - return nil - } - } - if _, err := client.Delete("/apisix/admin/plugin_configs/"+opts.ID, query); err != nil { - return fmt.Errorf("%s", cmdutil.FormatAPISIXCompatibilityResourceError(err, "plugin-config")) - } - - _, err = fmt.Fprintf(opts.IO.Out, "Plugin config %q deleted.\n", opts.ID) - return err -} diff --git a/pkg/cmd/plugin-config/delete/delete_test.go b/pkg/cmd/plugin-config/delete/delete_test.go deleted file mode 100644 index e6524f4..0000000 --- a/pkg/cmd/plugin-config/delete/delete_test.go +++ /dev/null @@ -1,104 +0,0 @@ -package delete - -import ( - "net/http" - "strings" - "testing" - - "github.com/api7/a7/internal/config" - "github.com/api7/a7/pkg/httpmock" - "github.com/api7/a7/pkg/iostreams" -) - -type mockConfig struct { - baseURL string - token string - gatewayGroup string -} - -func (m *mockConfig) BaseURL() string { return m.baseURL } -func (m *mockConfig) Token() string { return m.token } -func (m *mockConfig) GatewayGroup() string { return m.gatewayGroup } -func (m *mockConfig) TLSSkipVerify() bool { return false } -func (m *mockConfig) CACert() string { return "" } -func (m *mockConfig) CurrentContext() string { return "test" } -func (m *mockConfig) Contexts() []config.Context { return nil } -func (m *mockConfig) GetContext(name string) (*config.Context, error) { return nil, nil } -func (m *mockConfig) AddContext(ctx config.Context) error { return nil } -func (m *mockConfig) RemoveContext(name string) error { return nil } -func (m *mockConfig) SetCurrentContext(name string) error { return nil } -func (m *mockConfig) Save() error { return nil } - -func TestDeletePluginConfig_Success(t *testing.T) { - ios, _, out, _ := iostreams.Test() - registry := &httpmock.Registry{} - registry.Register(http.MethodDelete, "/apisix/admin/plugin_configs/pc1", httpmock.StringResponse(http.StatusOK, `{}`)) - - err := actionRun(&Options{ - IO: ios, - Client: func() (*http.Client, error) { return registry.GetClient(), nil }, - GatewayGroup: "gg1", - ID: "pc1", - Config: func() (config.Config, error) { - return &mockConfig{baseURL: "http://api.local", token: "test", gatewayGroup: "gg1"}, nil - }, - }) - if err != nil { - t.Fatalf("actionRun failed: %v", err) - } - if !strings.Contains(out.String(), `Plugin config "pc1" deleted.`) { - t.Fatalf("unexpected output: %q", out.String()) - } - - registry.Verify(t) -} - -func TestDeletePluginConfig_ValidationError(t *testing.T) { - ios, _, _, _ := iostreams.Test() - err := actionRun(&Options{ - IO: ios, - Client: func() (*http.Client, error) { return (&httpmock.Registry{}).GetClient(), nil }, - Config: func() (config.Config, error) { - return &mockConfig{baseURL: "http://api.local", token: "test", gatewayGroup: "gg1"}, nil - }, - }) - if err == nil || !strings.Contains(err.Error(), "no mock registered") { - t.Fatalf("expected validation-style error for missing ID in direct actionRun call, got: %v", err) - } -} - -func TestDeletePluginConfig_MissingGatewayGroup(t *testing.T) { - ios, _, _, _ := iostreams.Test() - err := actionRun(&Options{ - IO: ios, - Client: func() (*http.Client, error) { return (&httpmock.Registry{}).GetClient(), nil }, - ID: "pc1", - Config: func() (config.Config, error) { - return &mockConfig{baseURL: "http://api.local", token: "test", gatewayGroup: ""}, nil - }, - }) - if err == nil || !strings.Contains(err.Error(), "gateway group is required") { - t.Fatalf("expected gateway group error, got: %v", err) - } -} - -func TestDeletePluginConfig_APIError(t *testing.T) { - ios, _, _, _ := iostreams.Test() - registry := &httpmock.Registry{} - registry.Register(http.MethodDelete, "/apisix/admin/plugin_configs/pc1", httpmock.StringResponse(http.StatusInternalServerError, `{"message":"boom"}`)) - - err := actionRun(&Options{ - IO: ios, - Client: func() (*http.Client, error) { return registry.GetClient(), nil }, - GatewayGroup: "gg1", - ID: "pc1", - Config: func() (config.Config, error) { - return &mockConfig{baseURL: "http://api.local", token: "test", gatewayGroup: "gg1"}, nil - }, - }) - if err == nil || !strings.Contains(err.Error(), "status 500") { - t.Fatalf("expected API error with status 500, got: %v", err) - } - - registry.Verify(t) -} diff --git a/pkg/cmd/plugin-config/export/export.go b/pkg/cmd/plugin-config/export/export.go deleted file mode 100644 index e1177d1..0000000 --- a/pkg/cmd/plugin-config/export/export.go +++ /dev/null @@ -1,147 +0,0 @@ -package export - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "os" - - "github.com/spf13/cobra" - - "github.com/api7/a7/internal/config" - "github.com/api7/a7/pkg/api" - cmd "github.com/api7/a7/pkg/cmd" - "github.com/api7/a7/pkg/cmdutil" - "github.com/api7/a7/pkg/iostreams" -) - -type Options struct { - IO *iostreams.IOStreams - Client func() (*http.Client, error) - Config func() (config.Config, error) - GatewayGroup string - Label string - Output string - File string -} - -func NewCmd(f *cmd.Factory) *cobra.Command { - opts := &Options{IO: f.IOStreams, Client: f.HttpClient, Config: f.Config} - c := &cobra.Command{ - Use: "export", - Short: "Export plugin configs as JSON or YAML", - Args: cobra.NoArgs, - RunE: func(c *cobra.Command, args []string) error { - opts.GatewayGroup, _ = c.Flags().GetString("gateway-group") - return actionRun(opts) - }, - } - c.Flags().StringVar(&opts.Label, "label", "", "Filter by label (key=value)") - c.Flags().StringVarP(&opts.Output, "output", "o", "yaml", "Output format: json, yaml") - c.Flags().StringVarP(&opts.File, "file", "f", "", "Write output to file") - return c -} - -func actionRun(opts *Options) error { - cfg, err := opts.Config() - if err != nil { - return err - } - - ggID := opts.GatewayGroup - if ggID == "" { - ggID = cfg.GatewayGroup() - } - if ggID == "" { - return fmt.Errorf("gateway group is required; use --gateway-group flag or set a default in context config") - } - - httpClient, err := opts.Client() - if err != nil { - return err - } - - client := api.NewClient(httpClient, cfg.BaseURL()) - items, err := fetchAll(client, ggID, opts.Label) - if err != nil { - return fmt.Errorf("%s", cmdutil.FormatAPISIXCompatibilityResourceError(err, "plugin-config")) - } - - if len(items) == 0 { - fmt.Fprintln(opts.IO.ErrOut, "No plugin configs found.") - return nil - } - - format := opts.Output - if format == "" { - format = "yaml" - } - - var out io.Writer = opts.IO.Out - if opts.File != "" { - f, err := os.Create(opts.File) - if err != nil { - return fmt.Errorf("failed to create file: %w", err) - } - defer f.Close() - out = f - } - - return cmdutil.NewExporter(format, out).Write(stripTimestamps(items)) -} - -func fetchAll(client *api.Client, ggID, label string) ([]api.PluginConfig, error) { - page := 1 - pageSize := 100 - var all []api.PluginConfig - labelKey, labelValue := cmdutil.ParseLabel(label) - - for { - query := map[string]string{ - "gateway_group_id": ggID, - "page": fmt.Sprintf("%d", page), - "page_size": fmt.Sprintf("%d", pageSize), - } - if labelKey != "" { - query["label"] = labelKey - } - - body, err := client.Get("/apisix/admin/plugin_configs", query) - if err != nil { - return nil, err - } - - var resp api.ListResponse[api.PluginConfig] - if err := json.Unmarshal(body, &resp); err != nil { - return nil, fmt.Errorf("failed to parse response: %w", err) - } - - for _, item := range resp.List { - if labelValue != "" && (item.Labels == nil || item.Labels[labelKey] != labelValue) { - continue - } - all = append(all, item) - } - - if len(resp.List) == 0 || page*pageSize >= resp.Total { - break - } - page++ - } - - return all, nil -} - -func stripTimestamps(items []api.PluginConfig) []map[string]interface{} { - out := make([]map[string]interface{}, 0, len(items)) - for _, item := range items { - var m map[string]interface{} - b, _ := json.Marshal(item) - _ = json.Unmarshal(b, &m) - delete(m, "create_time") - delete(m, "update_time") - out = append(out, m) - } - return out -} diff --git a/pkg/cmd/plugin-config/export/export_test.go b/pkg/cmd/plugin-config/export/export_test.go deleted file mode 100644 index 73ff4e4..0000000 --- a/pkg/cmd/plugin-config/export/export_test.go +++ /dev/null @@ -1,78 +0,0 @@ -package export - -import ( - "net/http" - "strings" - "testing" - - "github.com/api7/a7/internal/config" - "github.com/api7/a7/pkg/httpmock" - "github.com/api7/a7/pkg/iostreams" -) - -type mockConfig struct { - baseURL string - token string - gatewayGroup string -} - -func (m *mockConfig) BaseURL() string { return m.baseURL } -func (m *mockConfig) Token() string { return m.token } -func (m *mockConfig) GatewayGroup() string { return m.gatewayGroup } -func (m *mockConfig) TLSSkipVerify() bool { return false } -func (m *mockConfig) CACert() string { return "" } -func (m *mockConfig) CurrentContext() string { return "test" } -func (m *mockConfig) Contexts() []config.Context { return nil } -func (m *mockConfig) GetContext(name string) (*config.Context, error) { return nil, nil } -func (m *mockConfig) AddContext(ctx config.Context) error { return nil } -func (m *mockConfig) RemoveContext(name string) error { return nil } -func (m *mockConfig) SetCurrentContext(name string) error { return nil } -func (m *mockConfig) Save() error { return nil } - -func TestExport_Success(t *testing.T) { - ios, _, out, _ := iostreams.Test() - registry := &httpmock.Registry{} - registry.Register(http.MethodGet, "/apisix/admin/plugin_configs", httpmock.JSONResponse(`{"total":1,"list":[{"id":"pc1","desc":"demo"}]}`)) - - opts := &Options{ - IO: ios, - Client: func() (*http.Client, error) { return registry.GetClient(), nil }, - Config: func() (config.Config, error) { - return &mockConfig{baseURL: "http://api.local", gatewayGroup: "gg1"}, nil - }, - GatewayGroup: "gg1", - Output: "json", - } - - if err := actionRun(opts); err != nil { - t.Fatalf("actionRun failed: %v", err) - } - - if !strings.Contains(out.String(), "pc1") { - t.Fatalf("expected plugin config in output, got: %s", out.String()) - } - registry.Verify(t) -} - -func TestExport_Empty(t *testing.T) { - ios, _, _, errBuf := iostreams.Test() - registry := &httpmock.Registry{} - registry.Register(http.MethodGet, "/apisix/admin/plugin_configs", httpmock.JSONResponse(`{"total":0,"list":[]}`)) - - opts := &Options{ - IO: ios, - Client: func() (*http.Client, error) { return registry.GetClient(), nil }, - Config: func() (config.Config, error) { - return &mockConfig{baseURL: "http://api.local", gatewayGroup: "gg1"}, nil - }, - GatewayGroup: "gg1", - Output: "json", - } - - if err := actionRun(opts); err != nil { - t.Fatalf("actionRun failed: %v", err) - } - if !strings.Contains(errBuf.String(), "No plugin configs found") { - t.Fatalf("expected no plugin configs message, got: %s", errBuf.String()) - } -} diff --git a/pkg/cmd/plugin-config/get/get.go b/pkg/cmd/plugin-config/get/get.go deleted file mode 100644 index c5ac55e..0000000 --- a/pkg/cmd/plugin-config/get/get.go +++ /dev/null @@ -1,87 +0,0 @@ -package get - -import ( - "encoding/json" - "fmt" - "net/http" - - "github.com/spf13/cobra" - - "github.com/api7/a7/internal/config" - "github.com/api7/a7/pkg/api" - cmd "github.com/api7/a7/pkg/cmd" - "github.com/api7/a7/pkg/cmdutil" - "github.com/api7/a7/pkg/iostreams" - "github.com/api7/a7/pkg/tableprinter" -) - -type Options struct { - IO *iostreams.IOStreams - Client func() (*http.Client, error) - Config func() (config.Config, error) - Output string - GatewayGroup string - ID string -} - -func NewCmd(f *cmd.Factory) *cobra.Command { - opts := &Options{IO: f.IOStreams, Client: f.HttpClient, Config: f.Config} - c := &cobra.Command{ - Use: "get ", - Short: "Get a reusable plugin configuration", - Args: cobra.ExactArgs(1), - RunE: func(c *cobra.Command, args []string) error { - opts.ID = args[0] - opts.Output, _ = c.Flags().GetString("output") - opts.GatewayGroup, _ = c.Flags().GetString("gateway-group") - return actionRun(opts) - }, - } - - return c -} - -func actionRun(opts *Options) error { - cfg, err := opts.Config() - if err != nil { - return err - } - - ggID := opts.GatewayGroup - if ggID == "" { - ggID = cfg.GatewayGroup() - } - if ggID == "" { - return fmt.Errorf("gateway group is required; use --gateway-group flag or set a default in context config") - } - - httpClient, err := opts.Client() - if err != nil { - return err - } - - client := api.NewClient(httpClient, cfg.BaseURL()) - query := map[string]string{"gateway_group_id": ggID} - body, err := client.Get("/apisix/admin/plugin_configs/"+opts.ID, query) - if err != nil { - return fmt.Errorf("%s", cmdutil.FormatAPISIXCompatibilityResourceError(err, "plugin-config")) - } - - var item api.PluginConfig - if err := json.Unmarshal(body, &item); err != nil { - return fmt.Errorf("failed to decode response: %w", err) - } - - if opts.Output != "" { - exporter := cmdutil.NewExporter(opts.Output, opts.IO.Out) - return exporter.Write(item) - } - - tp := tableprinter.New(opts.IO.Out) - tp.SetHeaders("FIELD", "VALUE") - tp.AddRow("id", item.ID) - tp.AddRow("desc", item.Desc) - tp.AddRow("plugins", fmt.Sprintf("%d", len(item.Plugins))) - - return tp.Render() -} diff --git a/pkg/cmd/plugin-config/get/get_test.go b/pkg/cmd/plugin-config/get/get_test.go deleted file mode 100644 index 1aac5e2..0000000 --- a/pkg/cmd/plugin-config/get/get_test.go +++ /dev/null @@ -1,126 +0,0 @@ -package get - -import ( - "encoding/json" - "net/http" - "strings" - "testing" - - "github.com/api7/a7/internal/config" - "github.com/api7/a7/pkg/api" - "github.com/api7/a7/pkg/httpmock" - "github.com/api7/a7/pkg/iostreams" -) - -type mockConfig struct { - baseURL string - token string - gatewayGroup string -} - -func (m *mockConfig) BaseURL() string { return m.baseURL } -func (m *mockConfig) Token() string { return m.token } -func (m *mockConfig) GatewayGroup() string { return m.gatewayGroup } -func (m *mockConfig) TLSSkipVerify() bool { return false } -func (m *mockConfig) CACert() string { return "" } -func (m *mockConfig) CurrentContext() string { return "test" } -func (m *mockConfig) Contexts() []config.Context { return nil } -func (m *mockConfig) GetContext(name string) (*config.Context, error) { return nil, nil } -func (m *mockConfig) AddContext(ctx config.Context) error { return nil } -func (m *mockConfig) RemoveContext(name string) error { return nil } -func (m *mockConfig) SetCurrentContext(name string) error { return nil } -func (m *mockConfig) Save() error { return nil } - -func TestGetPluginConfig_Table(t *testing.T) { - ios, _, out, _ := iostreams.Test() - registry := &httpmock.Registry{} - registry.Register(http.MethodGet, "/apisix/admin/plugin_configs/pc1", httpmock.JSONResponse(`{"id":"pc1","desc":"auth","plugins":{"key-auth":{},"cors":{}}}`)) - - err := actionRun(&Options{ - IO: ios, - Client: func() (*http.Client, error) { return registry.GetClient(), nil }, - GatewayGroup: "gg1", - ID: "pc1", - Config: func() (config.Config, error) { - return &mockConfig{baseURL: "http://api.local", token: "test", gatewayGroup: "gg1"}, nil - }, - }) - if err != nil { - t.Fatalf("actionRun failed: %v", err) - } - - output := out.String() - for _, expect := range []string{"FIELD", "VALUE", "id", "pc1", "desc", "auth", "plugins", "2"} { - if !strings.Contains(output, expect) { - t.Fatalf("expected %q in output: %q", expect, output) - } - } - - registry.Verify(t) -} - -func TestGetPluginConfig_JSON(t *testing.T) { - ios, _, out, _ := iostreams.Test() - registry := &httpmock.Registry{} - registry.Register(http.MethodGet, "/apisix/admin/plugin_configs/pc1", httpmock.JSONResponse(`{"id":"pc1","desc":"auth"}`)) - - err := actionRun(&Options{ - IO: ios, - Client: func() (*http.Client, error) { return registry.GetClient(), nil }, - Output: "json", - GatewayGroup: "gg1", - ID: "pc1", - Config: func() (config.Config, error) { - return &mockConfig{baseURL: "http://api.local", token: "test", gatewayGroup: "gg1"}, nil - }, - }) - if err != nil { - t.Fatalf("actionRun failed: %v", err) - } - - var item api.PluginConfig - if err := json.Unmarshal(out.Bytes(), &item); err != nil { - t.Fatalf("failed to parse JSON output: %v", err) - } - if item.ID != "pc1" { - t.Fatalf("unexpected item: %+v", item) - } - - registry.Verify(t) -} - -func TestGetPluginConfig_MissingGatewayGroup(t *testing.T) { - ios, _, _, _ := iostreams.Test() - err := actionRun(&Options{ - IO: ios, - Client: func() (*http.Client, error) { return (&httpmock.Registry{}).GetClient(), nil }, - ID: "pc1", - Config: func() (config.Config, error) { - return &mockConfig{baseURL: "http://api.local", token: "test", gatewayGroup: ""}, nil - }, - }) - if err == nil || !strings.Contains(err.Error(), "gateway group is required") { - t.Fatalf("expected gateway group error, got: %v", err) - } -} - -func TestGetPluginConfig_APIError(t *testing.T) { - ios, _, _, _ := iostreams.Test() - registry := &httpmock.Registry{} - registry.Register(http.MethodGet, "/apisix/admin/plugin_configs/pc1", httpmock.StringResponse(http.StatusInternalServerError, `{"message":"boom"}`)) - - err := actionRun(&Options{ - IO: ios, - Client: func() (*http.Client, error) { return registry.GetClient(), nil }, - GatewayGroup: "gg1", - ID: "pc1", - Config: func() (config.Config, error) { - return &mockConfig{baseURL: "http://api.local", token: "test", gatewayGroup: "gg1"}, nil - }, - }) - if err == nil || !strings.Contains(err.Error(), "status 500") { - t.Fatalf("expected API error with status 500, got: %v", err) - } - - registry.Verify(t) -} diff --git a/pkg/cmd/plugin-config/list/list.go b/pkg/cmd/plugin-config/list/list.go deleted file mode 100644 index 20c6802..0000000 --- a/pkg/cmd/plugin-config/list/list.go +++ /dev/null @@ -1,103 +0,0 @@ -package list - -import ( - "encoding/json" - "fmt" - "net/http" - - "github.com/spf13/cobra" - - "github.com/api7/a7/internal/config" - "github.com/api7/a7/pkg/api" - cmd "github.com/api7/a7/pkg/cmd" - "github.com/api7/a7/pkg/cmdutil" - "github.com/api7/a7/pkg/iostreams" - "github.com/api7/a7/pkg/tableprinter" -) - -type Options struct { - IO *iostreams.IOStreams - Client func() (*http.Client, error) - Config func() (config.Config, error) - Output string - GatewayGroup string - Label string -} - -func NewCmd(f *cmd.Factory) *cobra.Command { - opts := &Options{IO: f.IOStreams, Client: f.HttpClient, Config: f.Config} - c := &cobra.Command{ - Use: "list", - Short: "List reusable plugin configurations", - Aliases: []string{"ls"}, - Args: cobra.NoArgs, - RunE: func(c *cobra.Command, args []string) error { - opts.Output, _ = c.Flags().GetString("output") - opts.GatewayGroup, _ = c.Flags().GetString("gateway-group") - opts.Label, _ = c.Flags().GetString("label") - return actionRun(opts) - }, - } - c.Flags().StringVar(&opts.Label, "label", "", "Filter by label (key=value)") - - return c -} - -func actionRun(opts *Options) error { - cfg, err := opts.Config() - if err != nil { - return err - } - - ggID := opts.GatewayGroup - if ggID == "" { - ggID = cfg.GatewayGroup() - } - if ggID == "" { - return fmt.Errorf("gateway group is required; use --gateway-group flag or set a default in context config") - } - - httpClient, err := opts.Client() - if err != nil { - return err - } - - client := api.NewClient(httpClient, cfg.BaseURL()) - query := map[string]string{"gateway_group_id": ggID} - labelKey, labelValue := cmdutil.ParseLabel(opts.Label) - if labelKey != "" { - query["label"] = labelKey - } - body, err := client.Get("/apisix/admin/plugin_configs", query) - if err != nil { - return fmt.Errorf("%s", cmdutil.FormatAPISIXCompatibilityResourceError(err, "plugin-config")) - } - - var resp api.ListResponse[api.PluginConfig] - if err := json.Unmarshal(body, &resp); err != nil { - return fmt.Errorf("failed to decode response: %w", err) - } - - if labelValue != "" { - filtered := make([]api.PluginConfig, 0) - for _, item := range resp.List { - if item.Labels != nil && item.Labels[labelKey] == labelValue { - filtered = append(filtered, item) - } - } - resp.List = filtered - } - - if opts.Output != "" { - exporter := cmdutil.NewExporter(opts.Output, opts.IO.Out) - return exporter.Write(resp.List) - } - - tp := tableprinter.New(opts.IO.Out) - tp.SetHeaders("ID", "DESCRIPTION", "PLUGINS") - for _, item := range resp.List { - tp.AddRow(item.ID, item.Desc, fmt.Sprintf("%d", len(item.Plugins))) - } - - return tp.Render() -} diff --git a/pkg/cmd/plugin-config/list/list_test.go b/pkg/cmd/plugin-config/list/list_test.go deleted file mode 100644 index a028593..0000000 --- a/pkg/cmd/plugin-config/list/list_test.go +++ /dev/null @@ -1,148 +0,0 @@ -package list - -import ( - "encoding/json" - "net/http" - "strings" - "testing" - - "github.com/api7/a7/internal/config" - "github.com/api7/a7/pkg/api" - "github.com/api7/a7/pkg/httpmock" - "github.com/api7/a7/pkg/iostreams" -) - -type mockConfig struct { - baseURL string - token string - gatewayGroup string -} - -func (m *mockConfig) BaseURL() string { return m.baseURL } -func (m *mockConfig) Token() string { return m.token } -func (m *mockConfig) GatewayGroup() string { return m.gatewayGroup } -func (m *mockConfig) TLSSkipVerify() bool { return false } -func (m *mockConfig) CACert() string { return "" } -func (m *mockConfig) CurrentContext() string { return "test" } -func (m *mockConfig) Contexts() []config.Context { return nil } -func (m *mockConfig) GetContext(name string) (*config.Context, error) { return nil, nil } -func (m *mockConfig) AddContext(ctx config.Context) error { return nil } -func (m *mockConfig) RemoveContext(name string) error { return nil } -func (m *mockConfig) SetCurrentContext(name string) error { return nil } -func (m *mockConfig) Save() error { return nil } - -func TestListPluginConfigs_Table(t *testing.T) { - ios, _, out, _ := iostreams.Test() - registry := &httpmock.Registry{} - registry.Register(http.MethodGet, "/apisix/admin/plugin_configs", httpmock.JSONResponse(`{ - "total": 2, - "list": [ - {"id":"pc1","desc":"auth", "plugins":{"key-auth":{},"limit-count":{}}}, - {"id":"pc2","desc":"cors", "plugins":{"cors":{}}} - ] - }`)) - - err := actionRun(&Options{ - IO: ios, - Client: func() (*http.Client, error) { return registry.GetClient(), nil }, - GatewayGroup: "gg1", - Config: func() (config.Config, error) { - return &mockConfig{baseURL: "http://api.local", token: "test", gatewayGroup: "gg1"}, nil - }, - }) - if err != nil { - t.Fatalf("actionRun failed: %v", err) - } - - output := out.String() - for _, expect := range []string{"ID", "DESCRIPTION", "PLUGINS", "pc1", "auth", "2", "pc2", "cors", "1"} { - if !strings.Contains(output, expect) { - t.Fatalf("expected %q in output: %q", expect, output) - } - } - - registry.Verify(t) -} - -func TestListPluginConfigs_JSON(t *testing.T) { - ios, _, out, _ := iostreams.Test() - registry := &httpmock.Registry{} - registry.Register(http.MethodGet, "/apisix/admin/plugin_configs", httpmock.JSONResponse(`{"total":1,"list":[{"id":"pc1","desc":"auth","plugins":{"key-auth":{}}}]}`)) - - err := actionRun(&Options{ - IO: ios, - Client: func() (*http.Client, error) { return registry.GetClient(), nil }, - Output: "json", - GatewayGroup: "gg1", - Config: func() (config.Config, error) { - return &mockConfig{baseURL: "http://api.local", token: "test", gatewayGroup: "gg1"}, nil - }, - }) - if err != nil { - t.Fatalf("actionRun failed: %v", err) - } - - var items []api.PluginConfig - if err := json.Unmarshal(out.Bytes(), &items); err != nil { - t.Fatalf("failed to parse JSON output: %v", err) - } - if len(items) != 1 || items[0].ID != "pc1" { - t.Fatalf("unexpected items: %+v", items) - } - - registry.Verify(t) -} - -func TestListPluginConfigs_MissingGatewayGroup(t *testing.T) { - ios, _, _, _ := iostreams.Test() - err := actionRun(&Options{ - IO: ios, - Client: func() (*http.Client, error) { return (&httpmock.Registry{}).GetClient(), nil }, - Config: func() (config.Config, error) { - return &mockConfig{baseURL: "http://api.local", token: "test", gatewayGroup: ""}, nil - }, - }) - if err == nil || !strings.Contains(err.Error(), "gateway group is required") { - t.Fatalf("expected gateway group error, got: %v", err) - } -} - -func TestListPluginConfigs_APIError(t *testing.T) { - ios, _, _, _ := iostreams.Test() - registry := &httpmock.Registry{} - registry.Register(http.MethodGet, "/apisix/admin/plugin_configs", httpmock.StringResponse(http.StatusInternalServerError, `{"message":"boom"}`)) - - err := actionRun(&Options{ - IO: ios, - Client: func() (*http.Client, error) { return registry.GetClient(), nil }, - GatewayGroup: "gg1", - Config: func() (config.Config, error) { - return &mockConfig{baseURL: "http://api.local", token: "test", gatewayGroup: "gg1"}, nil - }, - }) - if err == nil || !strings.Contains(err.Error(), "status 500") { - t.Fatalf("expected API error with status 500, got: %v", err) - } - - registry.Verify(t) -} - -func TestListPluginConfigs_NotFoundExplainsAPI7EECompatibility(t *testing.T) { - ios, _, _, _ := iostreams.Test() - registry := &httpmock.Registry{} - registry.Register(http.MethodGet, "/apisix/admin/plugin_configs", httpmock.StringResponse(http.StatusNotFound, `{"error_msg":"not found"}`)) - - err := actionRun(&Options{ - IO: ios, - Client: func() (*http.Client, error) { return registry.GetClient(), nil }, - GatewayGroup: "gg1", - Config: func() (config.Config, error) { - return &mockConfig{baseURL: "http://api.local", token: "test", gatewayGroup: "gg1"}, nil - }, - }) - if err == nil || !strings.Contains(err.Error(), "APISIX compatibility") { - t.Fatalf("expected compatibility error, got: %v", err) - } - - registry.Verify(t) -} diff --git a/pkg/cmd/plugin-config/plugin_config.go b/pkg/cmd/plugin-config/plugin_config.go deleted file mode 100644 index 235f30f..0000000 --- a/pkg/cmd/plugin-config/plugin_config.go +++ /dev/null @@ -1,30 +0,0 @@ -package pluginconfig - -import ( - "github.com/spf13/cobra" - - cmd "github.com/api7/a7/pkg/cmd" - "github.com/api7/a7/pkg/cmd/plugin-config/create" - del "github.com/api7/a7/pkg/cmd/plugin-config/delete" - "github.com/api7/a7/pkg/cmd/plugin-config/export" - "github.com/api7/a7/pkg/cmd/plugin-config/get" - "github.com/api7/a7/pkg/cmd/plugin-config/list" - "github.com/api7/a7/pkg/cmd/plugin-config/update" -) - -func NewCmd(f *cmd.Factory) *cobra.Command { - c := &cobra.Command{ - Use: "plugin-config", - Short: "Manage reusable plugin configurations", - Aliases: []string{"pc"}, - } - - c.AddCommand(list.NewCmd(f)) - c.AddCommand(get.NewCmd(f)) - c.AddCommand(create.NewCmd(f)) - c.AddCommand(update.NewCmd(f)) - c.AddCommand(del.NewCmd(f)) - c.AddCommand(export.NewCmd(f)) - - return c -} diff --git a/pkg/cmd/plugin-config/update/update.go b/pkg/cmd/plugin-config/update/update.go deleted file mode 100644 index 89997cc..0000000 --- a/pkg/cmd/plugin-config/update/update.go +++ /dev/null @@ -1,132 +0,0 @@ -package update - -import ( - "encoding/json" - "fmt" - "net/http" - "strings" - - "github.com/spf13/cobra" - - "github.com/api7/a7/internal/config" - "github.com/api7/a7/pkg/api" - cmd "github.com/api7/a7/pkg/cmd" - "github.com/api7/a7/pkg/cmdutil" - "github.com/api7/a7/pkg/iostreams" -) - -type Options struct { - IO *iostreams.IOStreams - Client func() (*http.Client, error) - Config func() (config.Config, error) - Output string - File string - GatewayGroup string - ID string - - Desc string - PluginsJSON string - Labels []string -} - -func NewCmd(f *cmd.Factory) *cobra.Command { - opts := &Options{IO: f.IOStreams, Client: f.HttpClient, Config: f.Config} - c := &cobra.Command{ - Use: "update ", - Short: "Update a reusable plugin configuration", - Args: cobra.ExactArgs(1), - RunE: func(c *cobra.Command, args []string) error { - opts.ID = args[0] - opts.Output, _ = c.Flags().GetString("output") - opts.GatewayGroup, _ = c.Flags().GetString("gateway-group") - return actionRun(opts) - }, - } - - c.Flags().StringVar(&opts.Desc, "desc", "", "Plugin config description") - c.Flags().StringVar(&opts.PluginsJSON, "plugins-json", "", "Plugins JSON string") - c.Flags().StringSliceVar(&opts.Labels, "labels", nil, "Labels in key=value format") - c.Flags().StringVarP(&opts.File, "file", "f", "", "Path to JSON/YAML file with resource definition") - - return c -} - -func actionRun(opts *Options) error { - cfg, err := opts.Config() - if err != nil { - return err - } - - ggID := opts.GatewayGroup - if ggID == "" { - ggID = cfg.GatewayGroup() - } - if ggID == "" { - return fmt.Errorf("gateway group is required; use --gateway-group flag or set a default in context config") - } - - httpClient, err := opts.Client() - if err != nil { - return err - } - - if opts.File != "" { - payload, err := cmdutil.ReadResourceFile(opts.File, opts.IO.In) - if err != nil { - return err - } - client := api.NewClient(httpClient, cfg.BaseURL()) - body, err := client.Put("/apisix/admin/plugin_configs/"+opts.ID+"?gateway_group_id="+ggID, payload) - if err != nil { - return fmt.Errorf("%s", cmdutil.FormatAPISIXCompatibilityResourceError(err, "plugin-config")) - } - format := opts.Output - if format == "" { - format = "json" - } - return cmdutil.NewExporter(format, opts.IO.Out).Write(json.RawMessage(body)) - } - if opts.PluginsJSON == "" { - return fmt.Errorf("--plugins-json is required") - } - - plugins := map[string]interface{}{} - if err := json.Unmarshal([]byte(opts.PluginsJSON), &plugins); err != nil { - return fmt.Errorf("invalid --plugins-json: %w", err) - } - - labels := make(map[string]string) - for _, label := range opts.Labels { - parts := strings.SplitN(label, "=", 2) - if len(parts) != 2 || parts[0] == "" { - return fmt.Errorf("invalid label %q, expected key=value", label) - } - labels[parts[0]] = parts[1] - } - - bodyReq := api.PluginConfig{ - Desc: opts.Desc, - Plugins: plugins, - } - if len(labels) > 0 { - bodyReq.Labels = labels - } - - client := api.NewClient(httpClient, cfg.BaseURL()) - body, err := client.Put("/apisix/admin/plugin_configs/"+opts.ID+"?gateway_group_id="+ggID, bodyReq) - if err != nil { - return fmt.Errorf("%s", cmdutil.FormatAPISIXCompatibilityResourceError(err, "plugin-config")) - } - - var updated api.PluginConfig - if err := json.Unmarshal(body, &updated); err != nil { - return fmt.Errorf("failed to decode response: %w", err) - } - - format := opts.Output - if format == "" { - format = "json" - } - exporter := cmdutil.NewExporter(format, opts.IO.Out) - return exporter.Write(updated) -} diff --git a/pkg/cmd/plugin-config/update/update_test.go b/pkg/cmd/plugin-config/update/update_test.go deleted file mode 100644 index 9982301..0000000 --- a/pkg/cmd/plugin-config/update/update_test.go +++ /dev/null @@ -1,149 +0,0 @@ -package update - -import ( - "encoding/json" - "net/http" - "os" - "path/filepath" - "strings" - "testing" - - "github.com/api7/a7/internal/config" - "github.com/api7/a7/pkg/api" - "github.com/api7/a7/pkg/httpmock" - "github.com/api7/a7/pkg/iostreams" -) - -type mockConfig struct { - baseURL string - token string - gatewayGroup string -} - -func (m *mockConfig) BaseURL() string { return m.baseURL } -func (m *mockConfig) Token() string { return m.token } -func (m *mockConfig) GatewayGroup() string { return m.gatewayGroup } -func (m *mockConfig) TLSSkipVerify() bool { return false } -func (m *mockConfig) CACert() string { return "" } -func (m *mockConfig) CurrentContext() string { return "test" } -func (m *mockConfig) Contexts() []config.Context { return nil } -func (m *mockConfig) GetContext(name string) (*config.Context, error) { return nil, nil } -func (m *mockConfig) AddContext(ctx config.Context) error { return nil } -func (m *mockConfig) RemoveContext(name string) error { return nil } -func (m *mockConfig) SetCurrentContext(name string) error { return nil } -func (m *mockConfig) Save() error { return nil } - -func TestUpdatePluginConfig_Success(t *testing.T) { - ios, _, out, _ := iostreams.Test() - registry := &httpmock.Registry{} - registry.Register(http.MethodPut, "/apisix/admin/plugin_configs/pc1", httpmock.JSONResponse(`{"id":"pc1","desc":"auth2","plugins":{"cors":{}}}`)) - - err := actionRun(&Options{ - IO: ios, - Client: func() (*http.Client, error) { return registry.GetClient(), nil }, - GatewayGroup: "gg1", - ID: "pc1", - Desc: "auth2", - PluginsJSON: `{"cors":{}}`, - Config: func() (config.Config, error) { - return &mockConfig{baseURL: "http://api.local", token: "test", gatewayGroup: "gg1"}, nil - }, - }) - if err != nil { - t.Fatalf("actionRun failed: %v", err) - } - - var item api.PluginConfig - if err := json.Unmarshal(out.Bytes(), &item); err != nil { - t.Fatalf("failed to parse JSON output: %v", err) - } - if item.ID != "pc1" { - t.Fatalf("unexpected response: %+v", item) - } - - registry.Verify(t) -} - -func TestUpdatePluginConfig_FileModeDoesNotRequirePluginsJSON(t *testing.T) { - ios, _, out, _ := iostreams.Test() - registry := &httpmock.Registry{} - registry.Register(http.MethodPut, "/apisix/admin/plugin_configs/pc1", httpmock.JSONResponse(`{"id":"pc1","desc":"from-file","plugins":{"cors":{}}}`)) - - tmpFile := filepath.Join(t.TempDir(), "plugin-config.json") - if err := os.WriteFile(tmpFile, []byte(`{"desc":"from-file","plugins":{"cors":{}}}`), 0o644); err != nil { - t.Fatalf("write temp file: %v", err) - } - - err := actionRun(&Options{ - IO: ios, - Client: func() (*http.Client, error) { return registry.GetClient(), nil }, - GatewayGroup: "gg1", - ID: "pc1", - File: tmpFile, - Config: func() (config.Config, error) { - return &mockConfig{baseURL: "http://api.local", token: "test", gatewayGroup: "gg1"}, nil - }, - }) - if err != nil { - t.Fatalf("actionRun failed: %v", err) - } - if !strings.Contains(out.String(), "from-file") { - t.Fatalf("expected file-mode output, got: %s", out.String()) - } - - registry.Verify(t) -} - -func TestUpdatePluginConfig_ValidationError(t *testing.T) { - ios, _, _, _ := iostreams.Test() - err := actionRun(&Options{ - IO: ios, - Client: func() (*http.Client, error) { return (&httpmock.Registry{}).GetClient(), nil }, - GatewayGroup: "gg1", - ID: "pc1", - Config: func() (config.Config, error) { - return &mockConfig{baseURL: "http://api.local", token: "test", gatewayGroup: "gg1"}, nil - }, - }) - if err == nil || !strings.Contains(err.Error(), "--plugins-json is required") { - t.Fatalf("expected missing plugins-json error, got: %v", err) - } -} - -func TestUpdatePluginConfig_MissingGatewayGroup(t *testing.T) { - ios, _, _, _ := iostreams.Test() - err := actionRun(&Options{ - IO: ios, - Client: func() (*http.Client, error) { return (&httpmock.Registry{}).GetClient(), nil }, - ID: "pc1", - PluginsJSON: `{"cors":{}}`, - Config: func() (config.Config, error) { - return &mockConfig{baseURL: "http://api.local", token: "test", gatewayGroup: ""}, nil - }, - }) - if err == nil || !strings.Contains(err.Error(), "gateway group is required") { - t.Fatalf("expected gateway group error, got: %v", err) - } -} - -func TestUpdatePluginConfig_APIError(t *testing.T) { - ios, _, _, _ := iostreams.Test() - registry := &httpmock.Registry{} - registry.Register(http.MethodPut, "/apisix/admin/plugin_configs/pc1", httpmock.StringResponse(http.StatusInternalServerError, `{"message":"boom"}`)) - - err := actionRun(&Options{ - IO: ios, - Client: func() (*http.Client, error) { return registry.GetClient(), nil }, - GatewayGroup: "gg1", - ID: "pc1", - PluginsJSON: `{"cors":{}}`, - Config: func() (config.Config, error) { - return &mockConfig{baseURL: "http://api.local", token: "test", gatewayGroup: "gg1"}, nil - }, - }) - if err == nil || !strings.Contains(err.Error(), "status 500") { - t.Fatalf("expected API error with status 500, got: %v", err) - } - - registry.Verify(t) -} diff --git a/pkg/cmd/root/root.go b/pkg/cmd/root/root.go index 3677775..1c89449 100644 --- a/pkg/cmd/root/root.go +++ b/pkg/cmd/root/root.go @@ -16,7 +16,6 @@ import ( gatewaygroup "github.com/api7/a7/pkg/cmd/gateway-group" globalrule "github.com/api7/a7/pkg/cmd/global-rule" "github.com/api7/a7/pkg/cmd/plugin" - pluginconfig "github.com/api7/a7/pkg/cmd/plugin-config" pluginmetadata "github.com/api7/a7/pkg/cmd/plugin-metadata" "github.com/api7/a7/pkg/cmd/proto" "github.com/api7/a7/pkg/cmd/route" @@ -88,7 +87,6 @@ func NewCmd(f *cmd.Factory, cfg *config.FileConfig) *cobra.Command { c.AddCommand(service.NewCmd(f)) c.AddCommand(globalrule.NewCmd(f)) c.AddCommand(streamroute.NewCmd(f)) - c.AddCommand(pluginconfig.NewCmd(f)) c.AddCommand(pluginmetadata.NewCmd(f)) c.AddCommand(credential.NewCmd(f)) c.AddCommand(secret.NewCmd(f)) diff --git a/test/e2e/completion_version_test.go b/test/e2e/completion_version_test.go index 333f8c9..f846eeb 100644 --- a/test/e2e/completion_version_test.go +++ b/test/e2e/completion_version_test.go @@ -85,10 +85,11 @@ func TestHelp(t *testing.T) { assert.NotContains(t, stdout, "upstream") assert.NotContains(t, stdout, "consumer-group") assert.NotContains(t, stdout, "service-template") + assert.NotContains(t, stdout, "plugin-config") } func TestUnsupportedResourceCommandsAreRemoved(t *testing.T) { - for _, command := range []string{"upstream", "consumer-group", "service-template"} { + for _, command := range []string{"upstream", "consumer-group", "service-template", "plugin-config"} { t.Run(command, func(t *testing.T) { _, _, err := runA7(command, "--help") assert.Error(t, err, "command should not be registered") diff --git a/test/e2e/plugin_config_test.go b/test/e2e/plugin_config_test.go deleted file mode 100644 index 570cf2a..0000000 --- a/test/e2e/plugin_config_test.go +++ /dev/null @@ -1,35 +0,0 @@ -//go:build e2e - -package e2e - -import ( - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func requirePluginConfigCompatibilityError(t *testing.T, stdout, stderr string, err error) { - t.Helper() - require.Error(t, err) - combined := strings.ToLower(stdout + "\n" + stderr) - assert.Contains(t, combined, "apisix compatibility") - assert.Contains(t, combined, "api7 ee admin api") -} - -func TestPluginConfig_ListUnsupportedInAPI7EE(t *testing.T) { - env := setupEnv(t) - - stdout, stderr, err := runA7WithEnv(env, "plugin-config", "list", "-g", gatewayGroup) - requirePluginConfigCompatibilityError(t, stdout, stderr, err) -} - -func TestPluginConfig_CreateUnsupportedInAPI7EE(t *testing.T) { - env := setupEnv(t) - - stdout, stderr, err := runA7WithEnv(env, "plugin-config", "create", - "--plugins-json", `{"key-auth":{}}`, - "-g", gatewayGroup) - requirePluginConfigCompatibilityError(t, stdout, stderr, err) -}