diff --git a/CLAUDE.md b/CLAUDE.md index 28d7055e..fef70863 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,6 +5,7 @@ ROSA Hyperfleet API — ROSA HCP regional cluster management. Three components: + - **platform-api/** — Stateless REST gateway (SigV4 auth, Cedar/AVP authz, ZOA) - **hyperfleet-operator/** — Kubernetes operator (Cluster, NodePool, Placement, ManagementCluster, Manifest CRDs) - **hyperfleet-db/** — PostgreSQL-backed controller-runtime library @@ -29,15 +30,32 @@ make test-operator-int # Operator integration tests (Postgres + DynamoDB) make manifests # Generate CRDs (controller-gen) make generate # Generate deepcopy + +make codegen # Full codegen pipeline (openapi, passthrough, conversion) +make generate-clientset # Regenerate typed clientset from CRD types +make generate-openapi # Regenerate OpenAPI spec from CRD types + +make verify-codegen # Verify codegen output is up to date +make verify-clientset # Verify clientset matches committed files +make verify-openapi # Verify OpenAPI spec is up to date + +make test-unit # All unit tests (api, operator, codegen, clientset) +make test-integration # Integration tests (fleetdb, operator) +make test-api-codegen # Codegen tool tests +make test-clientset # Clientset tests ``` ## Module Layout ``` -hyperfleet-db/go.mod ← standalone -api/go.mod ← standalone (CRD types sub-module) -hyperfleet-operator/go.mod ← requires: fleetdb, api -platform-api/go.mod ← requires: fleetdb, api +hyperfleet-db/go.mod ← standalone +api/go.mod ← standalone (CRD types, v1alpha1) +clientset/go.mod ← generated typed K8s client for HyperFleet CRDs +hyperfleet-operator/go.mod ← requires: fleetdb, api +platform-api/go.mod ← requires: fleetdb, api +hack/api-codegen/go.mod ← codegen tools (openapi-gen, crd-variants, conversion-gen) +hack/clientset/cmd/wire-gen/go.mod ← wire generation for clientset +hack/tools/go.mod ← dev tooling dependencies ``` Cross-module refs use permanent `replace` directives to sibling dirs. @@ -47,5 +65,5 @@ Cross-module refs use permanent `replace` directives to sibling dirs. - Multi-module monorepo: separate go.mod per component - Ginkgo/Gomega for testing - OpenAPI-first API design -- CRD types owned by hyperfleet-operator, imported by platform-api +- CRD types in standalone `api/` module, imported by hyperfleet-operator and platform-api - golangci-lint v2 with custom logcheck plugin diff --git a/README.md b/README.md index 3d1cfda2..0ae8252b 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,15 @@ ROSA HCP regional cluster management — platform API, operator, and backing database library. -| Directory | Description | -| --- | --- | -| `platform-api/` | REST gateway (SigV4 auth, Cedar/AVP authz, ZOA) | +| Directory | Description | +| ---------------------- | ------------------------------------------------------- | +| `api/` | CRD types and API definitions (v1alpha1) | +| `platform-api/` | REST gateway (SigV4 auth, Cedar/AVP authz, ZOA) | | `hyperfleet-operator/` | Kubernetes operator (Cluster, NodePool, Placement CRDs) | -| `hyperfleet-db/` | PostgreSQL-backed controller-runtime library | -| `test/` | E2E tests (API, CLI, monitoring, ZOA) | +| `hyperfleet-db/` | PostgreSQL-backed controller-runtime library | +| `clientset/` | Generated typed Kubernetes client for HyperFleet CRDs | +| `hack/` | Code generation tools and dev tooling | +| `test/` | E2E tests (API, CLI, monitoring, ZOA) | ## Quick Start diff --git a/docs/api/rate-limit.md b/docs/api/rate-limit.md index 93f4d175..00b182ec 100644 --- a/docs/api/rate-limit.md +++ b/docs/api/rate-limit.md @@ -23,16 +23,16 @@ Rate limits are configured via a YAML file mounted as a ConfigMap at `/etc/ratel ```yaml enabled: true -redisTimeout: 20 # ms before fail-open on backend error +redisTimeout: 20 # ms before fail-open on backend error exemptAccounts: - "111111111111" - "222222222222" default: - rate: 100 # requests per window - burst: 200 # max burst (spike allowance) - window: 1 # window duration in seconds + rate: 100 # requests per window + burst: 200 # max burst (spike allowance) + window: 1 # window duration in seconds routes: - path: "/api/v0/clusters" @@ -49,27 +49,27 @@ routes: ### Defaults (when omitted) -| Field | Default | -|----------------|----------------| -| `rate` | 100 | -| `burst` | `rate * 2` | -| `window` | 1 (second) | -| `redisTimeout` | 20 (ms) | +| Field | Default | +| -------------- | ---------- | +| `rate` | 100 | +| `burst` | `rate * 2` | +| `window` | 1 (second) | +| `redisTimeout` | 20 (ms) | Route-level `burst` defaults to `rate * 2` and `window` inherits from `default.window` if not set. ### Environment variables -| Variable | Description | -|----------------------------|--------------------------------------------------| -| `RATE_LIMIT_ENABLED` | Set to `true` to enable rate limiting | -| `RATE_LIMIT_TEST_MODE` | Set to `true` for test mode (rate=3, burst=6, window=1s, in-memory) | -| `RATE_LIMIT_CONFIG_FILE` | Path to limits YAML (default `/etc/ratelimit/limits.yaml`) | -| `RATE_LIMIT_IN_MEMORY` | Set to `true` to use in-memory GCRA instead of Redis | -| `REDIS_ENDPOINT` | Valkey/Redis address (required when not in-memory) | -| `RATE_LIMIT_DEFAULT_RATE` | Override default rate | -| `RATE_LIMIT_DEFAULT_BURST` | Override default burst | -| `RATE_LIMIT_DEFAULT_WINDOW`| Override default window | +| Variable | Description | +| --------------------------- | ------------------------------------------------------------------- | +| `RATE_LIMIT_ENABLED` | Set to `true` to enable rate limiting | +| `RATE_LIMIT_TEST_MODE` | Set to `true` for test mode (rate=3, burst=6, window=1s, in-memory) | +| `RATE_LIMIT_CONFIG_FILE` | Path to limits YAML (default `/etc/ratelimit/limits.yaml`) | +| `RATE_LIMIT_IN_MEMORY` | Set to `true` to use in-memory GCRA instead of Redis | +| `REDIS_ENDPOINT` | Valkey/Redis address (required when not in-memory) | +| `RATE_LIMIT_DEFAULT_RATE` | Override default rate | +| `RATE_LIMIT_DEFAULT_BURST` | Override default burst | +| `RATE_LIMIT_DEFAULT_WINDOW` | Override default window | When `RATE_LIMIT_CONFIG_FILE` is set, the YAML file is loaded and `RATE_LIMIT_DEFAULT_*` env vars are ignored. When no config file is set, `NewDefaultConfig()` is used with `RATE_LIMIT_DEFAULT_*` overrides applied. @@ -78,7 +78,8 @@ When `RATE_LIMIT_CONFIG_FILE` is set, the YAML file is loaded and `RATE_LIMIT_DE Run rate limiting locally without Redis/Valkey: ```bash -RATE_LIMIT_TEST_MODE=true go run ./cmd/rosa-regional-platform-api serve +cd platform-api +RATE_LIMIT_TEST_MODE=true go run ./cmd/... serve ``` Test mode uses an in-memory GCRA implementation. It explicitly sets `rate=3`, `burst=6`, `window=1s` — all three values are hardcoded, not derived from production defaults. @@ -88,15 +89,15 @@ Test mode uses an in-memory GCRA implementation. It explicitly sets `rate=3`, `b All rate-limited requests include: | Header | Description | -|-------------------------|------------------------------------------| +| ----------------------- | ---------------------------------------- | | `X-RateLimit-Limit` | Configured rate for the matched route | | `X-RateLimit-Remaining` | Remaining requests in the current window | | `X-RateLimit-Reset` | Seconds until the limit resets | Denied requests (429) additionally include: -| Header | Description | -|---------------|--------------------------------------| +| Header | Description | +| ------------- | ---------------------------------------------- | | `Retry-After` | Seconds until the next request will be allowed | ### 429 Response Body @@ -117,11 +118,11 @@ Denied requests (429) additionally include: ratelimit_requests_total{method, path, result} ``` -| `result` label | Meaning | -|------------------------|-------------------------------------------------| -| `ok` | Request allowed | -| `over_limit` | Request denied (429) | -| `failure_mode_allowed` | Backend error, request allowed (fail-open) | +| `result` label | Meaning | +| ---------------------- | ------------------------------------------ | +| `ok` | Request allowed | +| `over_limit` | Request denied (429) | +| `failure_mode_allowed` | Backend error, request allowed (fail-open) | The `path` label is the matched route pattern (e.g. `/api/v0/clusters`) or `"default"` for unmatched routes. @@ -143,7 +144,7 @@ Every denied request logs at WARN level: ## Architecture ``` -pkg/ratelimit/ +platform-api/pkg/ratelimit/ config.go # Config struct, YAML loader, defaults middleware.go # HTTP middleware, metrics, response writer local.go # RateLimiter interface, Redis adapter, in-memory GCRA @@ -158,6 +159,7 @@ The `RateLimiter` interface abstracts the backend: ## Running Tests ```bash +cd platform-api go test -race -count=1 ./pkg/ratelimit/... ``` diff --git a/docs/api/v2-sdk-initiative.md b/docs/api/v2-sdk-initiative.md index 5eff13fc..993e724c 100644 --- a/docs/api/v2-sdk-initiative.md +++ b/docs/api/v2-sdk-initiative.md @@ -10,8 +10,8 @@ The v2 SDK will be validated by running existing FVT/e2e tests against both the Requires: -* Clone/duplication of API tests pointing both to v1 and v2, and covering the same functionality (the test is AWARE of the SDK v1 vs v2 differences). -* Clone/duplication of Client tests, pointing both to ROSA v1 and v2, and covering the same functionality (the test is NOT AWARE of the SDK v1 vs v2 differences). +- Clone/duplication of API tests pointing both to v1 and v2, and covering the same functionality (the test is AWARE of the SDK v1 vs v2 differences). +- Clone/duplication of Client tests, pointing both to ROSA v1 and v2, and covering the same functionality (the test is NOT AWARE of the SDK v1 vs v2 differences). **Out of scope for this initiative**: CI/prow integration. Manual test execution is sufficient, but all identified tests must pass. @@ -64,6 +64,7 @@ Generated output: clients, builders, types, JSON serialization, OpenAPI specs ``` Key facts: + - **Model source**: `github.com/openshift-online/ocm-api-model/model` (proprietary DSL, not OpenAPI) - **Generator**: `github.com/openshift-online/ocm-api-metamodel/cmd/metamodel` - **Output**: ~1600 generated files in `clustersmgmt/v1/` alone @@ -99,35 +100,35 @@ created := response.Body() // typed *Cluster The minimum surface the v2 SDK must cover: -| Operation | V1 SDK call | HTTP | -|-----------|-------------|------| -| Create cluster | `Clusters().Add().Body(c)` | `POST /api/clusters_mgmt/v1/clusters` | -| Get cluster | `Clusters().Cluster(id).Get()` | `GET /api/clusters_mgmt/v1/clusters/{id}` | -| List clusters | `Clusters().List()` | `GET /api/clusters_mgmt/v1/clusters` | -| Update cluster | `Clusters().Cluster(id).Update().Body(c)` | `PATCH /api/clusters_mgmt/v1/clusters/{id}` | -| Delete cluster | `Clusters().Cluster(id).Delete()` | `DELETE /api/clusters_mgmt/v1/clusters/{id}` | -| Create node pool | `Cluster(id).NodePools().Add().Body(np)` | `POST .../clusters/{id}/node_pools` | -| Get node pool | `Cluster(id).NodePools().NodePool(npId).Get()` | `GET .../node_pools/{id}` | -| List node pools | `Cluster(id).NodePools().List()` | `GET .../clusters/{id}/node_pools` | -| Update node pool | `Cluster(id).NodePools().NodePool(npId).Update().Body(np)` | `PATCH .../node_pools/{id}` | -| Delete node pool | `Cluster(id).NodePools().NodePool(npId).Delete()` | `DELETE .../node_pools/{id}` | -| Delete protection | `Cluster(id).DeleteProtection()` | `POST .../delete_protection` | -| Hibernate | `Cluster(id).Hibernate()` | `POST .../hibernate` | -| Resume | `Cluster(id).Resume()` | `POST .../resume` | +| Operation | V1 SDK call | HTTP | +| ----------------- | ---------------------------------------------------------- | -------------------------------------------- | +| Create cluster | `Clusters().Add().Body(c)` | `POST /api/clusters_mgmt/v1/clusters` | +| Get cluster | `Clusters().Cluster(id).Get()` | `GET /api/clusters_mgmt/v1/clusters/{id}` | +| List clusters | `Clusters().List()` | `GET /api/clusters_mgmt/v1/clusters` | +| Update cluster | `Clusters().Cluster(id).Update().Body(c)` | `PATCH /api/clusters_mgmt/v1/clusters/{id}` | +| Delete cluster | `Clusters().Cluster(id).Delete()` | `DELETE /api/clusters_mgmt/v1/clusters/{id}` | +| Create node pool | `Cluster(id).NodePools().Add().Body(np)` | `POST .../clusters/{id}/node_pools` | +| Get node pool | `Cluster(id).NodePools().NodePool(npId).Get()` | `GET .../node_pools/{id}` | +| List node pools | `Cluster(id).NodePools().List()` | `GET .../clusters/{id}/node_pools` | +| Update node pool | `Cluster(id).NodePools().NodePool(npId).Update().Body(np)` | `PATCH .../node_pools/{id}` | +| Delete node pool | `Cluster(id).NodePools().NodePool(npId).Delete()` | `DELETE .../node_pools/{id}` | +| Delete protection | `Cluster(id).DeleteProtection()` | `POST .../delete_protection` | +| Hibernate | `Cluster(id).Hibernate()` | `POST .../hibernate` | +| Resume | `Cluster(id).Resume()` | `POST .../resume` | ## HyperFleet Platform API Surface -The v1 SDK exposes 12 service areas through the OCM API. The HyperFleet Platform API replaces a subset of these with a smaller, focused surface. +The v1 SDK exposes 12 service areas through the OCM API. The HyperFleet Platform API replaces a subset of these with a smaller, focused surface. The v2 SDK only needs to cover the HyperFleet equivalents. -| V1 OCM Service | V1 Path | V2 HyperFleet Equivalent | Phase | -|---|---|---|---| -| ClustersMgmt (clusters) | `/api/clusters_mgmt` | **Cluster** CRD | Immediate | -| ClustersMgmt (node pools) | `/api/clusters_mgmt` | **NodePool** CRD | Immediate | -| AccountsMgmt | `/api/accounts_mgmt` | Authz: accounts, policies, attachments (see [authz.md](../authz.md)) | Future | -| Authorizations | `/api/authorizations` | Authz: check endpoint (see [authz.md](../authz.md)) | Future | -| AccessTransparency | `/api/access_transparency` | TBD — likely needed - SRE flows | Future | -| ServiceLogs | `/api/service_logs` | Per-cluster logs — mechanism TBD | Future | +| V1 OCM Service | V1 Path | V2 HyperFleet Equivalent | Phase | +| ------------------------- | -------------------------- | -------------------------------------------------------------------- | --------- | +| ClustersMgmt (clusters) | `/api/clusters_mgmt` | **Cluster** CRD | Immediate | +| ClustersMgmt (node pools) | `/api/clusters_mgmt` | **NodePool** CRD | Immediate | +| AccountsMgmt | `/api/accounts_mgmt` | Authz: accounts, policies, attachments (see [authz.md](../authz.md)) | Future | +| Authorizations | `/api/authorizations` | Authz: check endpoint (see [authz.md](../authz.md)) | Future | +| AccessTransparency | `/api/access_transparency` | TBD — likely needed - SRE flows | Future | +| ServiceLogs | `/api/service_logs` | Per-cluster logs — mechanism TBD | Future | The v2 SDK architecture must accommodate adding new resource types (authz, logs, etc.) as the Platform API grows, but the initial implementation covers only Cluster and NodePool. @@ -139,7 +140,7 @@ The v1 SDK is generated from a proprietary metamodel DSL (`ocm-api-model`). The The v2 SDK exposes its generated interface directly — there is no v1-compatibility adapter. Consumers migrate to the new interface (see [Interface Decision](#interface-decision)). -**Generated core**: Auto-generated from the HyperFleet CRD types (`hyperfleet-operator/api/v1alpha1/`) using `client-gen` (from `k8s.io/code-generator`), the same tool that generates typed clientsets for any Kubernetes operator. This produces typed verb clients (`Create`, `Get`, `List`, `Update`, `Delete`) that match the CRD types exactly. A custom `wire-gen` tool extends the generated clients with platform-specific behavior (watch suppression, `WaitUntil` polling). +**Generated core**: Auto-generated from the HyperFleet CRD types (`api/v1alpha1/`) using `client-gen` (from `k8s.io/code-generator`), the same tool that generates typed clientsets for any Kubernetes operator. This produces typed verb clients (`Create`, `Get`, `List`, `Update`, `Delete`) that match the CRD types exactly. A custom `wire-gen` tool extends the generated clients with platform-specific behavior (watch suppression, `WaitUntil` polling). ### SDK Release Cadence and Strategy @@ -173,6 +174,7 @@ Go types (api/v1alpha1/*.go) ``` Markers in the CRD type comments drive `wire-gen` output: + - `+wire:field=,meta=` — field name mapping (transport layer) - `+wire:watch=disabled` — suppress Watch; generate an override returning `ErrWatchNotSupported` - `+wire:wait` — generate `WaitUntil(ctx, id, condition func(*T) bool, interval, timeout)` @@ -226,11 +228,11 @@ Because the generated core already produces the typed `Spec`/`Status` models fro ### Target Clients -| Client | Priority | Rationale | -|--------|----------|-----------| -| **rosa CLI** (`rosa-hyperfleet-cli` / `rosactl`) | P0 | Primary user-facing tool, exercises full lifecycle | -| **terraform-provider-rhcs** | P1 | Key IaC consumer, many enterprise customers depend on it | -| **CAPA** (Cluster API Provider AWS) | P2 | Lower priority for initial initiative; evaluate after rosa + terraform | +| Client | Priority | Rationale | +| ------------------------------------------------ | -------- | ---------------------------------------------------------------------- | +| **rosa CLI** (`rosa-hyperfleet-cli` / `rosactl`) | P0 | Primary user-facing tool, exercises full lifecycle | +| **terraform-provider-rhcs** | P1 | Key IaC consumer, many enterprise customers depend on it | +| **CAPA** (Cluster API Provider AWS) | P2 | Lower priority for initial initiative; evaluate after rosa + terraform | ### In Scope @@ -256,47 +258,47 @@ Because the generated core already produces the typed `Spec`/`Status` models fro These tests in `rosa/tests/e2e/` exercise HCP cluster lifecycle through the CLI, which uses `ocm-sdk-go` under the hood: -| Test file | What it exercises | Labels | -|-----------|-------------------|--------| -| `hcp_cluster_test.go` | HCP cluster create/describe/edit/delete, log forwarders | `Feature.Cluster`, `Runtime.Day2` | -| `hcp_machine_pool_test.go` | HCP node pool (machine pool) CRUD | `Feature.MachinePool` | -| `test_rosacli_idp.go` | External auth configuration | | -| `hcp_tuning_config_test.go` | Tuning configs on HCP clusters | `Feature.TuningConfig` | -| `e2e_setup_test.go` | Cluster provisioning (precondition) | setup | -| `e2e_tear_down_test.go` | Cluster deletion (cleanup) | cleanup | +| Test file | What it exercises | Labels | +| --------------------------- | ------------------------------------------------------- | --------------------------------- | +| `hcp_cluster_test.go` | HCP cluster create/describe/edit/delete, log forwarders | `Feature.Cluster`, `Runtime.Day2` | +| `hcp_machine_pool_test.go` | HCP node pool (machine pool) CRUD | `Feature.MachinePool` | +| `test_rosacli_idp.go` | External auth configuration | | +| `hcp_tuning_config_test.go` | Tuning configs on HCP clusters | `Feature.TuningConfig` | +| `e2e_setup_test.go` | Cluster provisioning (precondition) | setup | +| `e2e_tear_down_test.go` | Cluster deletion (cleanup) | cleanup | ### HyperFleet CLI E2E Tests (this repo) These tests in `test/e2e-cli/` exercise the full lifecycle through `rosactl`: -| Test area | Labels | What it exercises | -|-----------|--------|-------------------| -| VPC setup | `vpc-create`, `vpc-list` | AWS VPC creation for cluster | -| IAM setup | `iam-create`, `iam-list` | IAM roles and OIDC | -| Cluster create | `hcp-create` | `rosactl cluster create` | -| Cluster status | `cluster-status` | Poll until cluster is ready | -| Node pools | `nodepools-wait` | Node pool readiness | -| Cluster update | `hcp-patch` | `rosactl cluster patch` | -| Cleanup | `bundles-delete`, `oidc-delete`, `iam-delete`, `vpc-delete` | Full teardown | +| Test area | Labels | What it exercises | +| -------------- | ----------------------------------------------------------- | ---------------------------- | +| VPC setup | `vpc-create`, `vpc-list` | AWS VPC creation for cluster | +| IAM setup | `iam-create`, `iam-list` | IAM roles and OIDC | +| Cluster create | `hcp-create` | `rosactl cluster create` | +| Cluster status | `cluster-status` | Poll until cluster is ready | +| Node pools | `nodepools-wait` | Node pool readiness | +| Cluster update | `hcp-patch` | `rosactl cluster patch` | +| Cleanup | `bundles-delete`, `oidc-delete`, `iam-delete`, `vpc-delete` | Full teardown | ### HyperFleet Platform API E2E Tests (this repo) The `test/e2e-api/` tests exercise the Platform API directly: -| Test file | What it exercises | -|-----------|-------------------| -| `e2e_test.go` | Basic API connectivity | +| Test file | What it exercises | +| ------------------- | ---------------------- | +| `e2e_test.go` | Basic API connectivity | | `authz_e2e_test.go` | Authorization policies | ### Acceptance Criteria for V2 SDK Each test suite below must pass against **both** the v1 SDK (ocm-sdk-go, baseline) and the v2 SDK (`clientset/`). The v1 run establishes the expected baseline; the v2 run proves parity. The initiative is not complete until both columns are green and behavioral parity is validated. -| # | Test suite | v1 SDK (baseline) | v2 SDK | -|---|-----------|-------------------|--------| -| 1 | **Rosa CLI HCP tests** (`hcp_cluster_test.go`, `hcp_machine_pool_test.go`): create HCP cluster, CRUD node pools, delete cluster | Must pass against ocm-sdk-go to establish baseline behavior | Must pass against `clientset/` with identical observable outcomes | -| 2 | **HyperFleet CLI lifecycle** (`test/e2e-cli/cluster_test.go` labels: `setup`, `create`, `monitor`, `cleanup`): full VPC → IAM → cluster → node pool → teardown cycle through rosactl | Must pass against ocm-sdk-go to establish baseline behavior | Must pass against `clientset/` with identical observable outcomes | -| 3 | **Terraform basic lifecycle**: `terraform apply` + `terraform destroy` of an HCP cluster with node pools | Must pass with provider backed by ocm-sdk-go to establish baseline behavior | Must pass with provider backed by `clientset/` with identical observable outcomes | +| # | Test suite | v1 SDK (baseline) | v2 SDK | +| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | +| 1 | **Rosa CLI HCP tests** (`hcp_cluster_test.go`, `hcp_machine_pool_test.go`): create HCP cluster, CRUD node pools, delete cluster | Must pass against ocm-sdk-go to establish baseline behavior | Must pass against `clientset/` with identical observable outcomes | +| 2 | **HyperFleet CLI lifecycle** (`test/e2e-cli/cluster_test.go` labels: `setup`, `create`, `monitor`, `cleanup`): full VPC → IAM → cluster → node pool → teardown cycle through rosactl | Must pass against ocm-sdk-go to establish baseline behavior | Must pass against `clientset/` with identical observable outcomes | +| 3 | **Terraform basic lifecycle**: `terraform apply` + `terraform destroy` of an HCP cluster with node pools | Must pass with provider backed by ocm-sdk-go to establish baseline behavior | Must pass with provider backed by `clientset/` with identical observable outcomes | **Behavioral-parity validation**: For each test suite, the v1 and v2 runs must produce the same observable outcomes — identical resource states, API response codes, and CLI/Terraform output (excluding endpoint URLs and auth-mechanism differences). Any behavioral divergence must be documented and justified before the initiative is declared complete. @@ -307,6 +309,7 @@ Each test suite below must pass against **both** the v1 SDK (ocm-sdk-go, baselin #### Story 1: V2 SDK Skeleton and Authentication Set up the `clientset/` module with: + - Connection builder with AWS SigV4 authentication (the HyperFleet API uses IAM auth, not OCM SSO tokens) - Regional Platform API endpoint (required; each region has its own endpoint, e.g. `https://hyperfleet.us-east-1.api.example.com`) - AWS signing region (required; must match the region of the Platform API endpoint, e.g. `us-east-1`) @@ -317,6 +320,7 @@ Set up the `clientset/` module with: - Basic HTTP round-tripper that talks to the HyperFleet Platform API **Acceptance**: + 1. SDK requires a regional Platform API endpoint and rejects initialization when it is missing 2. SDK requires a signing region and rejects initialization when it is missing 3. SigV4 signatures use `execute-api` as the signing service name @@ -327,7 +331,8 @@ Set up the `clientset/` module with: #### Story 2: Generated Core from CRD Types Set up the generation pipeline: -- Use `client-gen` to generate typed clientsets from `hyperfleet-operator/api/v1alpha1/` CRD types + +- Use `client-gen` to generate typed clientsets from `api/v1alpha1/` CRD types - Use `wire-gen --mode=mappings` to generate wire↔metadata field name mappings from `+wire:field` markers - Use `wire-gen --mode=wrappers` to generate Watch overrides and `WaitUntil` polling helpers from `+wire:watch=disabled` / `+wire:wait` markers - Wire the generated client into the SDK's transport layer (`clientset/transport`) @@ -385,8 +390,8 @@ methods — see [Interface Decision](#interface-decision)), not a **dynamic** on (a generic client that fetches the schema at runtime and operates on `unstructured` / `map[string]any`, the way `kubectl` does). -The rationale: a typed client suits consumers whose *own code names individual -fields*. `rosa` CLI (each flag maps to a field), terraform-provider-rhcs (each +The rationale: a typed client suits consumers whose _own code names individual +fields_. `rosa` CLI (each flag maps to a field), terraform-provider-rhcs (each HCL attribute maps to a field), and CAPA (reconcile logic maps field to field) are all in this category — they hand-write field names in source, so compile-time type safety is a direct benefit and the "new field ⇒ regenerate" cost is diff --git a/docs/testing/rate-limit.md b/docs/testing/rate-limit.md index b77a8035..c0cb3e32 100644 --- a/docs/testing/rate-limit.md +++ b/docs/testing/rate-limit.md @@ -2,11 +2,11 @@ ## Testing Layers -| Layer | Backend | Files | Validates | -|---|---|---|---| -| **Unit — middleware** | miniredis | `pkg/ratelimit/middleware_test.go` | HTTP middleware behavior with Redis adapter | -| **Unit — GCRA algorithm** | in-memory GCRA | `pkg/ratelimit/local_test.go` | GCRA math used by CLI test mode | -| **E2E** | real Valkey (ElastiCache) | `test/e2e-api/ratelimit_e2e_test.go` | Full path: API Gateway -> middleware -> Valkey -> response | +| Layer | Backend | Files | Validates | +| ------------------------- | ------------------------- | ----------------------------------------------- | ---------------------------------------------------------- | +| **Unit — middleware** | miniredis | `platform-api/pkg/ratelimit/middleware_test.go` | HTTP middleware behavior with Redis adapter | +| **Unit — GCRA algorithm** | in-memory GCRA | `platform-api/pkg/ratelimit/local_test.go` | GCRA math used by CLI test mode | +| **E2E** | real Valkey (ElastiCache) | `test/e2e-api/ratelimit_e2e_test.go` | Full path: API Gateway -> middleware -> Valkey -> response | ## Unit Tests — Middleware (`middleware_test.go`) @@ -14,27 +14,28 @@ Uses [miniredis](https://github.com/alicebob/miniredis) as an in-process Redis m ### Scenarios covered -| Test | What it validates | -|---|---| -| `SetsRateLimitHeadersOnAllowedRequests` | `X-RateLimit-Limit`, `Remaining`, `Reset` headers present | -| `AllowsRequestsUnderLimit` | Requests within burst all return 200 | -| `DeniesRequestsOverLimit` | Request over burst returns 429 + `Retry-After` | -| `IsolatesRateLimitsByAccount` | Different accounts have independent GCRA buckets | -| `SkipsWhenNoAccountID` | No account ID -> no rate limiting, no headers | -| `SkipsExemptAccounts` | Exempt accounts bypass rate limiting; non-exempt accounts are limited | -| `AppliesRouteOverrides` | Route-specific rate/burst override default; other methods unaffected | -| `DifferentiatesHTTPMethods` | GET and POST have separate limit buckets | -| `FailOpenWhenRedisDown` | Returns 200 when Redis is unreachable (fail-open) | -| `RecoverAfterWindow` | Requests allowed again after the GCRA window resets | -| `DisabledConfig` | `enabled: false` disables all rate limiting | -| `429ResponseFormat` | 429 body: `kind=Error`, `code=429`, reason with method/path | -| `KeyStructure` | Redis key format is `rate:rl:{account}:{method}:{path}` | -| `ConcurrentRequests` | Thread-safe under concurrent access | -| `PrometheusMetrics_*` | `ratelimit_requests_total` counter with `ok`/`over_limit`/`failure_mode_allowed` labels | +| Test | What it validates | +| --------------------------------------- | --------------------------------------------------------------------------------------- | +| `SetsRateLimitHeadersOnAllowedRequests` | `X-RateLimit-Limit`, `Remaining`, `Reset` headers present | +| `AllowsRequestsUnderLimit` | Requests within burst all return 200 | +| `DeniesRequestsOverLimit` | Request over burst returns 429 + `Retry-After` | +| `IsolatesRateLimitsByAccount` | Different accounts have independent GCRA buckets | +| `SkipsWhenNoAccountID` | No account ID -> no rate limiting, no headers | +| `SkipsExemptAccounts` | Exempt accounts bypass rate limiting; non-exempt accounts are limited | +| `AppliesRouteOverrides` | Route-specific rate/burst override default; other methods unaffected | +| `DifferentiatesHTTPMethods` | GET and POST have separate limit buckets | +| `FailOpenWhenRedisDown` | Returns 200 when Redis is unreachable (fail-open) | +| `RecoverAfterWindow` | Requests allowed again after the GCRA window resets | +| `DisabledConfig` | `enabled: false` disables all rate limiting | +| `429ResponseFormat` | 429 body: `kind=Error`, `code=429`, reason with method/path | +| `KeyStructure` | Redis key format is `rate:rl:{account}:{method}:{path}` | +| `ConcurrentRequests` | Thread-safe under concurrent access | +| `PrometheusMetrics_*` | `ratelimit_requests_total` counter with `ok`/`over_limit`/`failure_mode_allowed` labels | ### Running ```bash +cd platform-api go test -race -count=1 -v ./pkg/ratelimit/... ``` @@ -44,20 +45,21 @@ Tests the in-memory GCRA implementation directly via the `RateLimiter.Allow()` i ### Scenarios covered -| Test | What it validates | -|---|---| -| `AllowsWithinBurst` | Requests within burst all return `Allowed=1` | -| `DeniesOverBurst` | Request over burst returns `Allowed=0` with positive `RetryAfter` | -| `RemainingDecreases` | `Remaining` count decreases with each allowed request | -| `IsolatesKeys` | Different keys have independent GCRA state | -| `ResetAfterIsPositive` | `ResetAfter` is positive on allowed requests | -| `DeniedResultHasZeroRemaining` | Denied requests have `Remaining=0` | -| `ConcurrentAccess` | Thread-safe under goroutine contention | -| `NeverReturnsError` | In-memory limiter never returns an error (no fail-open needed) | +| Test | What it validates | +| ------------------------------ | ----------------------------------------------------------------- | +| `AllowsWithinBurst` | Requests within burst all return `Allowed=1` | +| `DeniesOverBurst` | Request over burst returns `Allowed=0` with positive `RetryAfter` | +| `RemainingDecreases` | `Remaining` count decreases with each allowed request | +| `IsolatesKeys` | Different keys have independent GCRA state | +| `ResetAfterIsPositive` | `ResetAfter` is positive on allowed requests | +| `DeniedResultHasZeroRemaining` | Denied requests have `Remaining=0` | +| `ConcurrentAccess` | Thread-safe under goroutine contention | +| `NeverReturnsError` | In-memory limiter never returns an error (no fail-open needed) | ### Running ```bash +cd platform-api go test -race -count=1 -v -run TestLocalLimiter ./pkg/ratelimit/... ``` @@ -66,10 +68,12 @@ go test -race -count=1 -v -run TestLocalLimiter ./pkg/ratelimit/... Start the API locally with rate limiting in test mode — no Redis or Valkey required: ```bash -RATE_LIMIT_TEST_MODE=true go run ./cmd/rosa-regional-platform-api serve +cd platform-api +RATE_LIMIT_TEST_MODE=true go run ./cmd/... serve ``` This uses the in-memory GCRA with low defaults: + - **rate**: 3 requests per second - **burst**: 6 (auto-derived: `rate * 2`) - **window**: 1 second @@ -94,23 +98,23 @@ Tests use concurrent goroutines with a start-gate channel pattern to fire reques ### Environment variables -| Variable | Required | Default | Description | -|---|---|---|---| -| `E2E_BASE_URL` | Yes | — | Deployed API URL | -| `E2E_ACCOUNT_ID` | No | AWS STS caller identity | Account ID for rate-limited requests | -| `RATE_LIMIT_TEST_MODE` | No | — | If `true`, assumes rate=3; otherwise auto-discovers from `X-RateLimit-Limit` header | -| `E2E_EXEMPT_ACCOUNT_ID` | No | — | Exempt account to test; test skipped if unset | +| Variable | Required | Default | Description | +| ----------------------- | -------- | ----------------------- | ----------------------------------------------------------------------------------- | +| `E2E_BASE_URL` | Yes | — | Deployed API URL | +| `E2E_ACCOUNT_ID` | No | AWS STS caller identity | Account ID for rate-limited requests | +| `RATE_LIMIT_TEST_MODE` | No | — | If `true`, assumes rate=3; otherwise auto-discovers from `X-RateLimit-Limit` header | +| `E2E_EXEMPT_ACCOUNT_ID` | No | — | Exempt account to test; test skipped if unset | ### Scenarios covered -| Test | What it validates | -|---|---| -| Headers on allowed requests | `X-RateLimit-Limit`, `Remaining`, `Reset` present with valid values | -| 429 when over limit | Concurrent requests exceed burst, at least one gets 429 | -| 429 response body + Retry-After | `kind=Error`, `code=429`, `reason` contains "Too Many Requests" | +| Test | What it validates | +| ------------------------------------ | ------------------------------------------------------------------------ | +| Headers on allowed requests | `X-RateLimit-Limit`, `Remaining`, `Reset` present with valid values | +| 429 when over limit | Concurrent requests exceed burst, at least one gets 429 | +| 429 response body + Retry-After | `kind=Error`, `code=429`, `reason` contains "Too Many Requests" | | Exempt accounts bypass rate limiting | Exempt account never gets 429 (skipped if `E2E_EXEMPT_ACCOUNT_ID` unset) | -| Per-account isolation | Exhausting one account's limit doesn't affect a different account | -| Rate limit resets after window | After 429, waiting for reset window allows requests again | +| Per-account isolation | Exhausting one account's limit doesn't affect a different account | +| Rate limit resets after window | After 429, waiting for reset window allows requests again | ### Running @@ -129,33 +133,33 @@ RATE_LIMIT_TEST_MODE=true make test-e2e-api E2E_LABEL_FILTER=ratelimit `RATE_LIMIT_TEST_MODE=true` sets low rate limit defaults for testability. The backend depends on whether `REDIS_ENDPOINT` is also set: -| `RATE_LIMIT_TEST_MODE` | `REDIS_ENDPOINT` | Backend | Use case | -|---|---|---|---| -| `true` | unset | In-memory GCRA | CLI / local development | -| `true` | set | In-memory GCRA | Test mode always forces in-memory regardless of `REDIS_ENDPOINT` | -| `false` | set | Real Valkey | Production | +| `RATE_LIMIT_TEST_MODE` | `REDIS_ENDPOINT` | Backend | Use case | +| ---------------------- | ---------------- | -------------- | ---------------------------------------------------------------- | +| `true` | unset | In-memory GCRA | CLI / local development | +| `true` | set | In-memory GCRA | Test mode always forces in-memory regardless of `REDIS_ENDPOINT` | +| `false` | set | Real Valkey | Production | Test mode defaults: `rate=3`, `burst=6`, `window=1s`. ## Scenario Coverage Matrix -| Scenario | Unit (middleware) | Unit (GCRA) | E2E | -|---|---|---|---| -| Requests under limit allowed | x | x | x | -| Requests over limit denied (429) | x | x | x | -| Rate limit headers present | x | | x | -| 429 response body format | x | | x | -| Retry-After header | x | x | x | -| No account ID skips rate limiting | x | | | -| Exempt accounts bypass | x | | x | -| Per-account isolation | x | x | x | -| Rate limit resets after window | x | | x | -| Fail-open on backend error | x | | | -| Concurrent request safety | x | x | x | -| Route-specific overrides | x | | | -| HTTP method differentiation | x | | | -| Prometheus metrics | x | | | -| Redis key structure | x | | | -| Disabled config | x | | | -| Remaining decreases | | x | | -| Never returns error | | x | | +| Scenario | Unit (middleware) | Unit (GCRA) | E2E | +| --------------------------------- | ----------------- | ----------- | --- | +| Requests under limit allowed | x | x | x | +| Requests over limit denied (429) | x | x | x | +| Rate limit headers present | x | | x | +| 429 response body format | x | | x | +| Retry-After header | x | x | x | +| No account ID skips rate limiting | x | | | +| Exempt accounts bypass | x | | x | +| Per-account isolation | x | x | x | +| Rate limit resets after window | x | | x | +| Fail-open on backend error | x | | | +| Concurrent request safety | x | x | x | +| Route-specific overrides | x | | | +| HTTP method differentiation | x | | | +| Prometheus metrics | x | | | +| Redis key structure | x | | | +| Disabled config | x | | | +| Remaining decreases | | x | | +| Never returns error | | x | |