diff --git a/docs/plans/2026-08-11-2152-discovery-api-docs.md b/docs/plans/2026-08-11-2152-discovery-api-docs.md new file mode 100644 index 0000000..d19197f --- /dev/null +++ b/docs/plans/2026-08-11-2152-discovery-api-docs.md @@ -0,0 +1,58 @@ +# Plan: Discovery API documentation (docs/api.md) +**Created:** 2026-08-11 21:52 + + +## Context + +The operator serves an HTTP discovery/lease API on `:8090` ([internal/discovery/](internal/discovery/)) that crawler clients use to list proxies, acquire TTL leases, release them, and report rate-limiting. There is no dedicated API reference today: README has four quickstart curls (no auth header, no schemas), and `docs/architecture.md` §7 has an ASCII route map. The user wants full documentation with curl examples for every feature. + +**Decisions made with user:** doc lives in a new `docs/api.md`; commit straight to `main` (no MR). + +## Deliverable + +### 1. New file `docs/api.md` — full API reference + +Content (all facts verified against code during planning): + +- **Overview & base URL** — what the API is; in-cluster FQDN `http://egress-proxies-operator-controller-manager-discovery-service.egress-proxies-operator-system.svc.cluster.local:8090` (Service: `config/default/discovery_service.yaml`); local access via `kubectl port-forward svc/egress-proxies-operator-controller-manager-discovery-service 8090:8090`. +- **Authentication** — static bearer token from `DISCOVERY_TOKEN` env var (populated from the optional `discovery-token` Secret, key `token`; `config/manager/manager.yaml`). Empty token ⇒ auth disabled with startup warning. Curl: `-H "Authorization: Bearer $TOKEN"` on every example. `/healthz` always exempt. +- **Conventions** — JSON everywhere; error envelope `{"error":"","message":""}`; request bodies capped at 64 KiB; proxies with a deletion timestamp are excluded from all responses. +- **Configuration table** — `--discovery-addr` (default `:8090`), `--max-lease-ttl` (default 1h), `--lease-cooldown` (default 15m), `DISCOVERY_TOKEN`. Note the shipped Deployment passes none of these flags, so defaults apply. +- **Endpoints**, each with request/response schema, status codes, and a copy-pasteable curl example: + - `GET /healthz` — liveness, unauthenticated. + - `GET /v1/proxies` — filters `healthy=true|false` (else 400 `invalid_query`) and repeatable `attr.=` (verbatim equality on `spec.attributes`, all pairs must match). Response `{"proxies":[proxyView...],"count":N}` sorted by id. Full `proxyView` field table: `id` (ns/name), `ip`, `port`, `attributes`, `phase` (Pending/Provisioning/Ready/Unhealthy/Deleting/Failed), `healthy` (condition `Healthy` == True), `latencyMillis`, `activeLeases`, `maxLeases` (default 5; explicit 0 = unleasable). + - `POST /v1/leases` — body `{selector, ttlSeconds, target}` all optional; TTL defaults 5m, capped at max-lease-ttl (else 400 `invalid_ttl`). 201 `{leaseID, proxy, expiresAt, ttlSeconds}`; 409 `no_match` with `considered/atCapacity/inCooldown/unhealthy` counts (documented meanings). + - `DELETE /v1/leases/{id}` — early release; always 204, idempotent. + - `POST /v1/leases/{id}/report` — body `{result: ok|rate_limited|banned, target}`; 204, 404 `unknown_lease`, 400 `invalid_result`. `ok` is a pure ack; `rate_limited` and `banned` behave identically today (both start one cooldown window). +- **Selection & cooldown semantics** (short section — this is the non-obvious part clients need): + - Selection order: fewest active leases → lowest latency → lexicographic id; deterministic; only healthy, non-deleting proxies with free capacity are candidates. + - Cooldown: 15m default (`--lease-cooldown`), keyed `{proxy, target}`. Report with a target blocks only leases requesting that target; report without a target (and lease without one) creates a **global** cooldown blocking all acquisitions of that proxy — call this footgun out explicitly. + - Expired leases stay reportable for one cooldown window past TTL. +- **End-to-end workflow example** — numbered curl walkthrough: acquire → use `proxy.ip:port` as HTTP proxy (`curl -x`) → report `rate_limited` on 429 → release. Using a `jq`-extracted `leaseID`. +- **Caveats** — lease/cooldown state is in-memory and per-process: single replica only, operator restart drops all leases and cooldowns. + +### 2. Pointers to the new doc (small edits) + +- `README.md`: one-line link near the quickstart curl section ("full reference: docs/api.md"). +- `docs/architecture.md` §7: one-line link to `docs/api.md` as the detailed reference. + +### 3. Housekeeping per CLAUDE.md + +- First action post-approval: copy this plan to `docs/plans/-discovery-api-docs.md` (timestamp from `date "+%Y-%m-%d-%H%M"`), commit it alone. +- Then write the docs, commit to `main` with `Co-Authored-By: Claude ` trailer, push. +- Append execution summary + status checklist to `docs/plans-executions/-discovery-api-docs.md` in the docs commit. +- Add `CHANGELOG.md` entry (timestamp via `date "+%Y-%m-%d %H:%M %Z"`) once the user confirms. + +## Key source files (facts source of truth) + +- [internal/discovery/handlers.go](internal/discovery/handlers.go), [internal/discovery/server.go](internal/discovery/server.go) — routes, schemas, status codes, auth, limits. +- [internal/lease/store.go](internal/lease/store.go) — selection order, cooldown/retention, stats. +- [api/v1alpha1/proxy_types.go](api/v1alpha1/proxy_types.go), [api/v1alpha1/helpers.go](api/v1alpha1/helpers.go) — defaults (port 3128, maxLeases 5), phases, conditions. +- [cmd/main.go](cmd/main.go) — flags/env defaults. +- [config/default/discovery_service.yaml](config/default/discovery_service.yaml), [config/manager/manager.yaml](config/manager/manager.yaml) — service DNS, token secret. + +## Verification + +- Cross-check every documented status code / field name against `internal/discovery/server_test.go` expectations. +- Sanity-run `go build ./... && go test -short ./internal/discovery/ ./internal/lease/` (no code changes expected — confirms docs match current behavior, not a stale tree). +- Optionally lint the curl JSON bodies by piping each through `jq .` locally.