Skip to content
Open
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
213 changes: 213 additions & 0 deletions docs/ga-test-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
# a7 GA Test Plan — Local API7 EE 3.9.12

Execution plan for **Task #3 (real CLI smoke tests)** and **Task #4 (complete E2E coverage)**
from the GA Readiness Handoff. Run against a locally deployed API7 EE.

## Scope

- **In scope**: management-plane validation of the `a7` CLI — CRUD round-trips, declarative
config, output formats, error handling, and confirmation that unsupported resources are
removed/blocked.
- **Out of scope**: real gateway data-plane traffic forwarding. Per the handoff, traffic
tests stay in the gateway repository. The separately-running gateway container is only
needed to keep the EE deployment healthy; we do **not** assert on proxied traffic here.

## Local environment (assumed already running)

| Component | How it runs | Notes |
|---|---|---|
| API7 EE dashboard / control-plane + dp-manager + PostgreSQL | docker compose | the management API `a7` talks to |
| API7 gateway | standalone docker container | connected to the dp-manager; health only |

### 0. Pre-flight — capture connection facts

Fill these in before doing anything else:

```
A7_ADMIN_URL = https://localhost:7443 # control-plane HTTPS port — confirm
A7_TOKEN = a7ee-<your-access-token> # access token from the dashboard UI / API
A7_GATEWAY_GROUP = default # resolved to a UUID at runtime
```
Comment on lines +26 to +30
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a language tag to the fenced block to satisfy markdown linting.

The block starting at Line 26 is missing a fence language (MD040).

Suggested fix
-```
+```text
 A7_ADMIN_URL   = https://localhost:7443      # control-plane HTTPS port — confirm
 A7_TOKEN       = a7ee-<your-access-token>    # access token from the dashboard UI / API
 A7_GATEWAY_GROUP = default                   # resolved to a UUID at runtime
</details>

<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 26-26: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

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

In `@docs/ga-test-plan.md` around lines 26 - 30, The fenced code block containing
the environment variables (A7_ADMIN_URL, A7_TOKEN, A7_GATEWAY_GROUP) is missing
a language tag which triggers MD040; update the opening fence to include a
language (e.g., "text") so it becomes ```text to satisfy markdown linting while
keeping the block content unchanged.


Confirm before proceeding:

1. **Reachability** — `curl -k -H "X-API-KEY: $A7_TOKEN" $A7_ADMIN_URL/api/gateway_groups`
returns `200` with a non-empty `list`.
2. **Version is 3.9.12** — check the dashboard image tag in `docker compose ps` /
`docker inspect`, or the dashboard `/api/version` endpoint. Record the exact version.
If it is **not** 3.9.12, note the deviation in the report — the handoff specifically
wants 3.9.12 validated.
3. **Gateway is registered & healthy** — the standalone gateway container shows up under
the gateway group (dashboard UI or `GET /api/gateway_groups/{id}/instances`). This only
needs to be healthy; we are not testing traffic through it.

## Build

```bash
cd ~/Desktop/repos/xisipa/a7
make build # -> ./bin/a7
./bin/a7 version # record the version string in the report
```

All manual steps below use `./bin/a7`. The automated suite (Phase A) builds its own binary.

---

## Phase A — Run the existing automated E2E suite

Establishes a known-good baseline against the local 3.9.12 instance before manual work.

```bash
cd ~/Desktop/repos/xisipa/a7
export A7_ADMIN_URL="https://localhost:7443"
export A7_TOKEN="a7ee.xxxxxxxx"
export A7_GATEWAY_GROUP="default"
# A7_GATEWAY_URL and HTTPBIN_URL intentionally left UNSET — data-plane tests are skipped.
make test-e2e
```

**Expected**: suite passes, or fails only in ways explained by known gaps. For every
failure, decide: real bug, missing/incorrect test, or environment issue. Record each one.

> The standard `test/e2e/docker-compose.yml` is **not** used here — you already have a
> local deployment. Only the env vars above are needed.

---

## Phase B — Manual CRUD smoke tests (per resource)

For **each** resource below, run the handoff's round-trip pattern with `./bin/a7`:

1. `create` (both flag-based and `-f file` where supported)
2. `get` — assert the returned config matches what was sent
3. `list` — assert the new resource appears
4. `update` — change a field
5. `get` again — assert the update took effect
6. `export` (where supported) — assert valid YAML/JSON
7. `delete` (try with and without `--force`)
8. `get` / `list` — assert it is gone

Also exercise, at least once per resource: `-o json`, `-o yaml`, and default table output;
plus one deliberate error case (bad ID, missing `--gateway-group`, malformed `-f` file) and
confirm the error message is clear and actionable.

**Use unique resource IDs** (`a7-ga-<resource>-<timestamp>`) and clean up everything, even
on a local instance — it keeps reruns deterministic.

### Resource checklist

| Resource | Command | create/get/list/update/delete | export | Notes |
|---|---|:--:|:--:|---|
| context | `a7 context` | ☐ | — | create/use/list/current/delete; no gateway group needed |
| gateway-group | `a7 gateway-group` | ☐ | — | control-plane `/api/*` |
| service | `a7 service` | ☐ | ☐ | inline upstream — **node array format only** |
| route | `a7 route` | ☐ | ☐ | requires `--service-id`; route is service-centered |
| consumer | `a7 consumer` | ☐ | ☐ | |
| credential | `a7 credential` | ☐ | — | nested under a consumer |
| ssl | `a7 ssl` | ☐ | ☐ | **shared/global** — unique IDs, careful cleanup |
| secret | `a7 secret` | ☐ | — | **shared/global** — unique IDs, careful cleanup |
| global-rule | `a7 global-rule` | ☐ | ☐ | **shared/global** — affects whole gateway group |
| plugin | `a7 plugin` | ☐ (list/get only) | — | read-only: list plugins, get schema |
| plugin-metadata | `a7 plugin-metadata` | ☐ (no list) | — | **shared/global**; keyed by plugin name |
| proto | `a7 proto` | ☐ | ☐ | protobuf definitions |
| stream-route | `a7 stream-route` | ☐ | ☐ | **confirmed exposed** by the CP — must work |

> **Shared/global resources** (ssl, secret, global-rule, plugin-metadata) are visible
> gateway-group-wide. On a shared environment they collide between CI runs; even locally,
> assert you only touch IDs you created and never delete pre-existing resources.

### Inline upstream format (API7 EE)

`nodes` must be an **array of objects**. The APISIX map form is rejected.

```yaml
upstream:
type: roundrobin
nodes:
- host: 127.0.0.1
port: 8080
weight: 1
```

---

## Phase C — Verify unsupported resources are blocked

Confirm the resources removed in PR #21 (and the one pending in Task #2) are gone or clearly
rejected — not present as broken commands.

| Resource | Expected behavior |
|---|---|
| `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 stream-route ...` | **works** — full CRUD (verified exposed by the control plane) |

Declarative config — these top-level sections must be **rejected with a clear error**:

```yaml
upstreams: [...] # -> validation error
consumer_groups: [...] # -> validation error
service_templates: [...] # -> validation error
plugin_configs: [...] # -> validation error (after Task #2)
```

> Caveat: the control plane still accepts `plugin_configs` *inside batch config-validation
> payloads*. The rejection applies to `a7`'s standalone command and top-level declarative
> section, not to that internal validation path.

---

## Phase D — Declarative config

Run the full declarative workflow end to end:

1. `a7 config dump` — export the live gateway-group config to a YAML file. Assert it is
valid YAML and contains only supported resource sections.
2. `a7 config validate -f dump.yaml` — assert it passes for a known-good file, and fails
with a clear message for a file containing an unsupported section (see Phase C).
3. `a7 config diff -f dump.yaml` — assert "no diff" against the just-dumped state; then
edit a field and assert the diff is reported correctly.
4. `a7 config sync -f dump.yaml` — apply changes. Verify `--dry-run` first if available,
then a real sync, then `diff` again to confirm convergence.

Confirm all four work with **service-centered** API7 EE resources (no standalone upstreams).

---

## Bug-handling protocol

Per the handoff: **when a bug is found, add or update E2E coverage before fixing it.**

1. Reproduce with the real binary; capture exact command, output, and HTTP status.
2. Write or update a `test/e2e/<resource>_test.go` case that fails for the same reason.
3. Fix the code.
4. Re-run that test plus `make test-e2e` to confirm green.
5. Do **not** change code purely to satisfy a test if it contradicts real API7 EE behavior.

---

## Reporting

Record results in a table and attach it to the GA tracking issue / PR:

| Phase | Resource / area | Result | Bug? | E2E test added | Notes |
|---|---|---|---|---|---|
| A | automated suite | | | | |
| B | service | | | | |
| B | route | | | | |
| ... | ... | | | | |
| C | unsupported resources | | | | |
| D | config dump/validate/diff/sync | | | | |

Also capture: exact API7 EE version tested, `a7 version` string, date, and any environment
deviations from "API7 EE 3.9.12".

## Exit criteria

- Phase A suite green against the local instance.
- Phase B round-trip passes for every resource in the checklist.
- Phase C confirms all unsupported resources are absent/blocked and `stream-route` works.
- Phase D confirms declarative config works with service-centered resources.
- Every bug found has a corresponding E2E test and a fix (or a tracked follow-up).
89 changes: 89 additions & 0 deletions docs/ga-test-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# a7 GA Test Report

Execution of [`docs/ga-test-plan.md`](./ga-test-plan.md) against a locally deployed API7 EE.

## Environment

| Item | Value |
|---|---|
| Date | 2026-05-14 |
| API7 EE version | **3.9.12** (image `api7/api7-ee-3-integrated:v3.9.12`; `/api/version` → `v3.9.12`) |
| `a7 version` | `7b67da5` (then `7b67da5-dirty` after the fixes in this report) |
| Admin URL | `https://localhost:7443` |
| Gateway group | `default` |
| Gateway instance | `api7-ee-gateway-1` — `status: Healthy`, `compatibility: Compatible` |
| Deviations | (1) `a7 plugin-config` command still present — Task #2 not yet done. (2) Data-plane traffic tests not run (out of scope per plan; `A7_GATEWAY_URL`/`HTTPBIN_URL` unset). |

## Summary

All four phases executed. **5 real bugs found, all fixed with E2E + unit coverage.** Final automated suite: **128 passed / 10 skipped / 0 failed** (skips are the intentionally-unset data-plane and gateway-group-CRUD tests).

## Results

| Phase | Resource / area | Result | Bug? | E2E test added | Notes |
|---|---|---|---|---|---|
| A | automated suite | PASS | — | — | 122→128 pass, 10 skip, 0 fail. Skips = data-plane traffic + gateway-group CRUD (intentionally disabled). |
| B | context | PASS | — | — | create/list/use/current/delete, json output, error case. |
| B | gateway-group | PASS | — | — | full CRUD; non-interactive `delete` needs `--force`. |
| B | service | PASS | — | — | create (file+flags), get, list, update, export. Map-form upstream nodes correctly rejected (400). |
| B | route | PASS | — | — | create (file+flags), get, list, update, export, delete. `get`/`delete` take id only; `update` uses `--uri`. |
| B | consumer | PASS | — | — | CRUD + export. MINOR: file `description:` key silently dropped (accepted key is `desc`). |
| B | credential | PASS | — | — | CRUD via server-returned id. MINOR: `create [id]` positional is treated as `name` (id is server-generated). |
| B | ssl | **FIXED** | **BUG-1** | `TestSSL_UpdateFlagsRequireCertAndKey` | flag-based `ssl update` silently lost partial updates. |
| B | secret | PASS | — | — | CRUD (file+flags); id format is `provider/id`. |
| B | global-rule | PASS | — | — | CRUD (file+flags), export; id must equal the plugin name. MINOR: flag `--id` is required but its value is ignored by EE. |
| B | plugin | **FIXED** | **BUG-3** | `TestPlugin_GetYAML` | `plugin get -o yaml` emitted JSON. list/get otherwise fine. |
| B | plugin-metadata | **FIXED** | **BUG-2** | `TestPluginMetadata_GetYAML` | `plugin-metadata get -o yaml` emitted a byte-array. CRUD otherwise fine. |
| B | proto | PASS | — | — | CRUD (file+flags), export. MINOR: `--desc` / `desc:` silently dropped. |
| B | stream-route | **FIXED** | **BUG-4** | `TestStreamRoute_CreateWithFlags`, `TestStreamRoute_CreateRequiresName` | `create` had no `--name` flag; EE requires `name`. CRUD via `-f` worked. |
| C | unsupported commands | PASS | — | — | `upstream` / `consumer-group` / `service-template` removed. `plugin-config` still present (Task #2 pending) — gives a clear runtime error. `stream-route` works. |
| C | declarative unsupported sections | **FIXED** | **BUG-5** | `TestConfigValidate_RejectsUnsupportedServiceTemplates` | `upstreams`/`consumer_groups` rejected; `service_templates` was silently accepted. `plugin_configs` rejection is expected-pending per plan. |
| D | config dump/validate/diff/sync | PASS | — | — | dump → valid YAML, supported sections only. validate/diff/sync round-trip converges; `--dry-run` applies nothing; post-sync diff clean; route update verified persisted. |

## Bugs found & fixed

Per the bug-handling protocol, each bug was reproduced with the real binary, given failing E2E coverage, fixed, and re-verified.

### BUG-1 — `ssl update` silently lost partial flag-based updates
`a7 ssl update <id> --sni new` (without `--cert`/`--key`) exited 0 and echoed the new SNI, but the server was never updated. Root cause: the command does a GET-merge-PUT, but API7 EE never returns `cert`/`key` on GET, so the merged PUT dropped the certificate material and the EE ignored it.
**Fix:** `pkg/cmd/ssl/update/update.go` now requires `--cert` and `--key` for flag-based updates (consistent with `ssl create`), with a clear error pointing to the `-f` path. `-f` updates with a full definition are unaffected.

### BUG-2 — `plugin-metadata get -o yaml` emitted a byte-array
The YAML output was a list of integers. Root cause: `pkg/cmd/plugin-metadata/get/get.go` passed `json.RawMessage` (a `[]byte`) to the YAML encoder.
**Fix:** pass the decoded `map` to the exporter.

### BUG-3 — `plugin get -o yaml` emitted JSON
Root cause: `pkg/cmd/plugin/get/get.go` only special-cased `-o json`; every other value fell through to a hardcoded JSON encoder.
**Fix:** honor any explicit `--output` value via the exporter.

### BUG-4 — `stream-route create` had no `--name` flag
API7 EE 3.9.12 requires `name` on stream routes, but the create command exposed no `--name` flag, making flag-based creation impossible.
**Fix:** added a `Name` field to `api.StreamRoute`, a `--name` flag, and required-field validation on both the flag and `-f` paths.

### BUG-5 — `config validate` silently accepted `service_templates`
A declarative file with a top-level `service_templates:` section validated as "Config is valid". Root cause: `api.ConfigFile` had no field for it, so the section was dropped on unmarshal.
**Fix:** added a `ServiceTemplates` field and an explicit rejection in both `config validate` and `configutil.ValidateSupportedSections` (used by `diff`/`sync`), mirroring `upstreams`/`consumer_groups`.

## Minor observations (not fixed — low severity / by-design)

- **consumer**: `-f` file with a `description:` key is silently ignored; the accepted key is `desc`.
- **credential**: `create [id]` help text is misleading — the positional arg becomes the `name`; the `id` is server-generated. This is codified by `TestCredential_CreateWithPositionalID`, so it is current intended behavior.
- **global-rule**: flag-based `create` requires `--id`, but API7 EE forces the id to equal the single plugin key, so the `--id` value is effectively ignored. File-based create errors clearly on a mismatch.
- **proto**: `--desc` and a `desc:` file field are silently dropped.
- **stream-route / EE behavior**: API7 EE rejects stream routes bound to a service it has classified as HTTP. Binding to a `type: stream` service works reliably. `a7` surfaces the EE error cleanly; this is EE-side behavior, not an `a7` bug.
- **service**: the EE's schema-mismatch error (e.g. map-form upstream nodes) is correct and actionable but very verbose (dumps the full JSON schema).
- **tooling**: `golangci-lint run ./...` reports spurious `undefined: yaml (typecheck)` on files that correctly import `gopkg.in/yaml.v3` (including untouched files). `go build`, `go vet`, and `go test` are all clean — this is a pre-existing lint-environment issue, unrelated to these changes.

## Exit criteria

| Criterion | Status |
|---|---|
| Phase A suite green against the local instance | ✅ 128 pass / 10 skip / 0 fail |
| Phase B round-trip passes for every resource | ✅ all 13 resources pass (ssl/plugin/plugin-metadata/stream-route after fixes) |
| Phase C confirms unsupported resources absent/blocked and `stream-route` works | ✅ — except `plugin-config` command still present (Task #2 pending, tracked) |
| Phase D declarative config works with service-centered resources | ✅ dump/validate/diff/sync all verified |
| Every bug has a corresponding E2E test and a fix | ✅ 5/5 fixed with E2E + unit coverage |

## Follow-ups

- **Task #2** (remove the `plugin-config` standalone command) is still outstanding. Once done, Phase C also expects the declarative `plugin_configs` top-level section to be rejected — the `service_templates` rejection added here is the template for that change.
29 changes: 16 additions & 13 deletions pkg/api/types_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,22 @@ package api
// ConfigFile is the declarative configuration file format for a7.
// It holds all runtime resources that can be dumped/synced.
type ConfigFile struct {
Version string `json:"version" yaml:"version"`
Routes []Route `json:"routes,omitempty" yaml:"routes,omitempty"`
Services []Service `json:"services,omitempty" yaml:"services,omitempty"`
Upstreams []Upstream `json:"upstreams,omitempty" yaml:"upstreams,omitempty"`
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"`
StreamRoutes []StreamRoute `json:"stream_routes,omitempty" yaml:"stream_routes,omitempty"`
Protos []Proto `json:"protos,omitempty" yaml:"protos,omitempty"`
Secrets []Secret `json:"secrets,omitempty" yaml:"secrets,omitempty"`
PluginMetadata []PluginMetadataEntry `json:"plugin_metadata,omitempty" yaml:"plugin_metadata,omitempty"`
Version string `json:"version" yaml:"version"`
Routes []Route `json:"routes,omitempty" yaml:"routes,omitempty"`
Services []Service `json:"services,omitempty" yaml:"services,omitempty"`
Upstreams []Upstream `json:"upstreams,omitempty" yaml:"upstreams,omitempty"`
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.
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"`
Secrets []Secret `json:"secrets,omitempty" yaml:"secrets,omitempty"`
PluginMetadata []PluginMetadataEntry `json:"plugin_metadata,omitempty" yaml:"plugin_metadata,omitempty"`
}

// PluginMetadataEntry is a freeform map representing a plugin's metadata.
Expand Down
Loading
Loading