diff --git a/AGENTS.md b/AGENTS.md index 23673944..35020395 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ This file provides guidance to AI coding assistants when working with this repos ## Project Overview -ROSA Regional Platform API — a stateless gateway API for ROSA HCP regional cluster management. Provides REST and gRPC interfaces for managing clusters within a specific cloud region. +ROSA HyperFleet API — a stateless gateway API for ROSA HCP regional cluster management. Provides REST and gRPC interfaces for managing clusters within a specific cloud region. ## Build & Test Commands @@ -19,6 +19,7 @@ make clean # Remove build artifacts ``` ### Integration & E2E Tests + ```bash make e2e-init-db # Initialize test database make e2e-authz-infra-up # Start authorization test infrastructure diff --git a/CLAUDE.md b/CLAUDE.md index 4c16257f..9ff62dd5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,7 @@ This file provides guidance to AI coding assistants when working with this repos ## Project Overview -ROSA Regional Platform API — a stateless gateway API for ROSA HCP regional cluster management. Provides REST and gRPC interfaces for managing clusters within a specific cloud region. +ROSA HyperFleet API — a stateless gateway API for ROSA HCP regional cluster management. Provides REST and gRPC interfaces for managing clusters within a specific cloud region. ## Build & Test Commands @@ -21,6 +21,7 @@ make clean # Remove build artifacts ``` ### Integration & E2E Tests + ```bash make e2e-init-db # Initialize test database make e2e-authz-infra-up # Start authorization test infrastructure diff --git a/README.md b/README.md index 744b0773..27a607ab 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ROSA Regional Platform API +# ROSA HyperFleet API Stateless gateway API for ROSA HCP regional cluster management. @@ -35,26 +35,26 @@ flowchart LR ## API Documentation -- [View the full API spec (Swagger UI)](https://petstore.swagger.io/?url=https://raw.githubusercontent.com/openshift-online/rosa-regional-platform-api/main/openapi/openapi.yaml) +- [View the full API spec (Swagger UI)](https://petstore.swagger.io/?url=https://raw.githubusercontent.com/openshift-online/rosa-hyperfleet-api/main/openapi/openapi.yaml) - [ZOA Trusted Actions API Reference](docs/api/zoa-endpoints.md) ## Configuration -| Flag | Default | Description | -| ------------------- | ------------------------------------------------ | ------------------------ | -| `--api-port` | `8000` | API server port | -| `--maestro-url` | `http://maestro:8000` | Maestro API URL | -| `--hyperfleet-url` | `http://hyperfleet-api.hyperfleet-system:8000` | Hyperfleet API base URL | -| `--dynamodb-table` | `rosa-customer-accounts` | DynamoDB table | -| `--dynamodb-region` | `us-east-1` | AWS region | -| `--zoa.enabled` | `false` | Enable ZOA Trusted Actions | -| `--zoa.table-name` | `rosa-zoa-actions` | ZOA DynamoDB table | -| `--zoa.audit-table-name` | `rosa-zoa-audit` | ZOA audit log table | -| `--zoa.bucket-name` | `rosa-zoa-artifacts` | ZOA S3 artifacts bucket | -| `--zoa.aws-region` | `us-east-1` | ZOA AWS region | -| `--zoa.templates-dir` | `/etc/zoa/templates` | ZOA action templates dir | -| `--zoa.job-config-dir` | `/etc/zoa/jobs` | ZOA job configuration dir | -| `--zoa.poll-interval` | `30s` | ZOA job poll interval | +| Flag | Default | Description | +| ------------------------ | ---------------------------------------------- | -------------------------- | +| `--api-port` | `8000` | API server port | +| `--maestro-url` | `http://maestro:8000` | Maestro API URL | +| `--hyperfleet-url` | `http://hyperfleet-api.hyperfleet-system:8000` | Hyperfleet API base URL | +| `--dynamodb-table` | `rosa-customer-accounts` | DynamoDB table | +| `--dynamodb-region` | `us-east-1` | AWS region | +| `--zoa.enabled` | `false` | Enable ZOA Trusted Actions | +| `--zoa.table-name` | `rosa-zoa-actions` | ZOA DynamoDB table | +| `--zoa.audit-table-name` | `rosa-zoa-audit` | ZOA audit log table | +| `--zoa.bucket-name` | `rosa-zoa-artifacts` | ZOA S3 artifacts bucket | +| `--zoa.aws-region` | `us-east-1` | ZOA AWS region | +| `--zoa.templates-dir` | `/etc/zoa/templates` | ZOA action templates dir | +| `--zoa.job-config-dir` | `/etc/zoa/jobs` | ZOA job configuration dir | +| `--zoa.poll-interval` | `30s` | ZOA job poll interval | ## Build @@ -69,31 +69,38 @@ make image ### Unit Tests Run all unit tests (excludes e2e tests): + ```bash make test ``` Run tests for a specific package: + ```bash make test-unit PKG=./pkg/authz/... ``` Run authorization package tests only: + ```bash make test-authz ``` Generate coverage report: + ```bash make test-coverage # Opens coverage.html in your browser ``` ### E2E Tests + E2E tests use [Ginkgo](https://onsi.github.io/ginkgo/) and generate JUnit XML reports in `./test-results/junit.xml`. #### Prerequisites + Set the following environment variables: + - `BASE_URL` - API Gateway URL (e.g., `https://xxxxx.execute-api.us-east-2.amazonaws.com/prod`) - `E2E_ACCOUNT_ID` - AWS account ID for testing (optional, defaults to current AWS credentials) @@ -102,11 +109,13 @@ Set the following environment variables: The tests run natively on your platform (Linux, macOS, Windows). **Prerequisites**: Install Ginkgo CLI + ```bash go install github.com/onsi/ginkgo/v2/ginkgo@latest ``` **Run tests:** + ```bash export BASE_URL="https://xxxxx.execute-api.us-east-2.amazonaws.com/prod" export E2E_ACCOUNT_ID="123456789012" # Optional @@ -122,6 +131,7 @@ This is useful for CI/CD pipelines or isolated test environments. **Note**: Containers run Linux. You can **build** the container on macOS/Windows, but it runs Linux inside. 1. Build the e2e container: + ```bash # Single platform build (default: linux/amd64) # Works on macOS (including M1/M2), Linux, Windows @@ -136,11 +146,13 @@ make image-e2e-push-multiarch ``` You can customize the target platforms: + ```bash make image-e2e-multiarch PLATFORMS=linux/amd64,linux/arm64,linux/ppc64le ``` **Building on macOS (including Apple Silicon)**: + ```bash # On macOS M1/M2, build for linux/arm64 (faster) make image-e2e GOOS=linux GOARCH=arm64 @@ -150,6 +162,7 @@ make image-e2e GOOS=linux GOARCH=amd64 ``` 2. Run tests in the container: + ```bash make test-e2e-container \ BASE_URL="https://xxxxx.execute-api.us-east-2.amazonaws.com/prod" \ @@ -157,6 +170,7 @@ make test-e2e-container \ ``` **With a specific AWS profile** (shares your `~/.aws` credentials): + ```bash make test-e2e-container \ BASE_URL="https://xxxxx.execute-api.us-east-2.amazonaws.com/prod" \ @@ -165,6 +179,7 @@ make test-e2e-container \ ``` **Run specific tests with --focus**: + ```bash # Run only AWS Credentials Check test make test-e2e-container \ @@ -178,6 +193,7 @@ make test-e2e-container \ ``` **Skip specific tests**: + ```bash # Skip authorization tests (default behavior) make test-e2e-container \ @@ -191,11 +207,13 @@ make test-e2e-container \ ``` The container automatically: + - Mounts your `~/.aws` directory (read-only) - includes both `credentials` and `config` files - Passes the `AWS_PROFILE` environment variable - Configures AWS SDK to load the profile **Using credentials from a custom location**: + ```bash make test-e2e-container \ BASE_URL="https://xxxxx.execute-api.us-east-2.amazonaws.com/prod" \ @@ -203,12 +221,14 @@ make test-e2e-container \ ``` **Note**: The AWS SDK requires both `credentials` and `config` files for full profile support. If you only have a `credentials` file, make sure to: + - Set `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` directly, OR - Ensure your credentials file contains all necessary settings **Or use Docker/Podman directly**: Standard approach (mounts entire `~/.aws` directory): + ```bash docker run --rm \ -e E2E_BASE_URL="https://xxxxx.execute-api.us-east-2.amazonaws.com/prod" \ @@ -222,6 +242,7 @@ docker run --rm \ ``` With a custom credentials directory: + ```bash docker run --rm \ -e E2E_BASE_URL="https://xxxxx.execute-api.us-east-2.amazonaws.com/prod" \ @@ -238,6 +259,7 @@ The JUnit XML results will be available in `./test-results/junit.xml` after the #### Common E2E Test Scenarios **Local development on macOS/Linux** (fastest): + ```bash export BASE_URL="https://xxxxx.execute-api.us-east-2.amazonaws.com/prod" export AWS_PROFILE="my-profile" @@ -245,6 +267,7 @@ make test-e2e ``` **Testing with a specific AWS profile in a container**: + ```bash make test-e2e-container \ BASE_URL="https://xxxxx.execute-api.us-east-2.amazonaws.com/prod" \ @@ -252,6 +275,7 @@ make test-e2e-container \ ``` **Debug AWS credentials in container**: + ```bash make test-e2e-container \ BASE_URL="https://xxxxx.execute-api.us-east-2.amazonaws.com/prod" \ @@ -260,6 +284,7 @@ make test-e2e-container \ ``` **Run specific test in container**: + ```bash make test-e2e-container \ BASE_URL="https://xxxxx.execute-api.us-east-2.amazonaws.com/prod" \ @@ -267,12 +292,14 @@ make test-e2e-container \ ``` **CI/CD pipeline** (uses IAM role or instance profile): + ```bash # AWS credentials provided by CI environment make test-e2e-container BASE_URL="${API_URL}" ``` **Using Podman instead of Docker**: + ```bash # Replace 'docker' with 'podman' in any command podman run --rm \ @@ -299,10 +326,12 @@ make test-e2e-container \ ``` The test will show: + - ✓ Successfully validated credentials with your AWS account, ARN, and UserId - ✗ Error message if credentials are missing or invalid **Example output:** + ```bash ✓ AWS Credentials verified successfully Account: 123456789012 @@ -313,6 +342,7 @@ The test will show: ``` Common issues: + - **"no such file or directory"**: AWS credentials file not mounted correctly - **"Unable to locate credentials"**: `AWS_PROFILE` doesn't exist or credentials file is malformed - **"ExpiredToken"**: Your AWS session token has expired (common with SSO) @@ -331,11 +361,13 @@ make test-e2e-container-static-creds \ ``` This target: + 1. Exports credentials from your profile using `aws configure export-credentials` 2. Passes them as environment variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN) 3. Avoids mounting credential files and running credential_process in the container Or manually export credentials: + ```bash # Export your credentials directly (avoids credential_process) export AWS_ACCESS_KEY_ID="your-access-key" @@ -347,6 +379,7 @@ make test-e2e-container BASE_URL="https://xxxxx.execute-api.us-east-2.amazonaws. ``` Or, get temporary credentials from your credential process locally and pass them: + ```bash # Get credentials from your local credential process aws configure export-credentials --profile rrp-chris-regional_cluster --format env @@ -363,6 +396,7 @@ docker run --rm \ ``` **Debug credentials inside the container:** + ```bash # Drop into an interactive shell to troubleshoot make debug-e2e-container-creds AWS_PROFILE="rrp-chris-regional_cluster" @@ -379,32 +413,36 @@ make debug-e2e-container-creds AWS_PROFILE="rrp-chris-regional_cluster" Authorization tests require local DynamoDB and cedar-agent infrastructure. Start the infrastructure: + ```bash make e2e-authz-infra-up ``` Run authz e2e tests: + ```bash make test-e2e-authz ``` Stop the infrastructure: + ```bash make e2e-authz-infra-down ``` Or run everything with automatic cleanup: + ```bash make test-e2e-authz-clean ``` ### Prow CI E2E Tests (`ci/prow/rosa-regionality-compatibility-e2e`) -Tests compatibility by spinning up an ephemeral [rosa-regional-platform](https://github.com/openshift-online/rosa-regional-platform) environment with the platform-api image from the PR, then running the rosa-regional-platform test suite against it using the commit hash of the PR. +Tests compatibility by spinning up an ephemeral [rosa-hyperfleet](https://github.com/openshift-online/rosa-hyperfleet) environment with the platform-api image from the PR, then running the rosa-hyperfleet test suite against it using the commit hash of the PR. **Trigger:** `/test rosa-regionality-compatibility-e2e` (does not run automatically). -**Configuration:** Defined in [openshift/release](https://github.com/openshift/release) under `ci-operator/config/openshift-online/rosa-regional-platform-api/`. +**Configuration:** Defined in [openshift/release](https://github.com/openshift/release) under `ci-operator/config/openshift-online/rosa-hyperfleet-api/`. ### Test Structure @@ -450,6 +488,7 @@ It("should successfully call an endpoint", func() { ``` For POST requests: + ```go It("should create a resource", func() { payload := map[string]interface{}{ @@ -467,6 +506,7 @@ It("should create a resource", func() { ## API Examples ### Register a new management cluster + ```bash awscurl -X POST https://z11111111.execute-api.us-east-2.amazonaws.com/prod/api/v0/management_clusters \ --service execute-api \ @@ -476,6 +516,7 @@ awscurl -X POST https://z11111111.execute-api.us-east-2.amazonaws.com/prod/api/v ``` ### Get the current resource bundles + ```bash awscurl https://z11111111.execute-api.us-east-2.amazonaws.com/prod/api/v0/resource_bundles \ --service execute-api \ @@ -483,6 +524,7 @@ awscurl https://z11111111.execute-api.us-east-2.amazonaws.com/prod/api/v0/resour ``` ### Create a manifestwork for management-01 + ```bash # see swagger for reference for the payload struct awscurl -X POST https://z11111111.execute-api.us-east-2.amazonaws.com/prod/api/v0/work \ @@ -490,6 +532,3 @@ awscurl -X POST https://z11111111.execute-api.us-east-2.amazonaws.com/prod/api/v --region us-east-2 \ -d @payload.json ``` - - - diff --git a/SECURITY-AUDIT.md b/SECURITY-AUDIT.md index ded71a47..ea7ff209 100644 --- a/SECURITY-AUDIT.md +++ b/SECURITY-AUDIT.md @@ -1,4 +1,4 @@ -# Security Audit — rosa-regional-platform-api +# Security Audit — rosa-hyperfleet-api **Audit Date:** 2026-05-15 **Auditor:** security-audit-agent @@ -35,12 +35,14 @@ The entire authentication model trusts HTTP headers (`X-Amz-Account-Id`, `X-Amz- If the API server is reachable by any path that bypasses API Gateway (e.g., direct pod IP, internal service name, misconfigured Kubernetes NetworkPolicy, VPC peering, or load balancer), any caller can forge any identity, including privileged accounts. **Attack Vectors:** + 1. **Direct pod access:** From within the EKS cluster (another pod, compromised node), call the API directly at `http://rosa-regional-platform-api:8000` with `X-Amz-Account-Id: 000000000000` (a hardcoded privileged account in test data) and `X-Amz-Caller-Arn: arn:aws:iam::000000000000:root` to gain full privileged access. 2. **Network policy gap:** If Kubernetes NetworkPolicy is not configured (or uses default-allow), any pod in the cluster can forge headers and call the API. 3. **API Gateway bypass:** If the ALB target group binding (`targetgroupbinding.yaml`) is misconfigured to also expose the pod directly, the API Gateway IAM auth layer is bypassed. 4. **Internal AWS service access:** VPC peering, Transit Gateway, or AWS PrivateLink configurations that allow other accounts/services to reach the pod's VPC could be used to forge headers. **What to Mitigate:** + - Implement a **request signing validation** layer. API Gateway signs forwarded requests with a known secret. Validate that all requests carry this signature before trusting the `X-Amz-*` headers. - Alternatively, configure **Kubernetes NetworkPolicy** to restrict ingress to the API pod to only the API Gateway VPC endpoint's source IP range. - Add a shared secret or HMAC signature in a custom header that API Gateway injects (via a request transformer), which the Go service validates before processing any `X-Amz-*` header. @@ -64,6 +66,7 @@ apiHandler := handlers.CORS( `AllowedOrigins([]string{"*"})` permits cross-origin requests from any domain. Because authentication relies on AWS SigV4-signed requests (injected by API Gateway), this CORS policy cannot be directly exploited for CSRF in typical browser flows. However, it creates several risks: **Attack Vectors:** + 1. **Information leakage:** Error responses, API versions (`/api/v0/info`), and health endpoints are accessible from any origin in a browser context without preflight restrictions, potentially leaking environment information. 2. **If custom auth headers are added:** If the API is ever extended with cookie-based auth or `Authorization` header-based auth (which is already listed in `AllowedHeaders`), the wildcard CORS policy immediately enables CSRF attacks from any origin. 3. **Reflected XSS pivot:** If any endpoint returns user-controlled content without escaping, the CORS policy enables cross-origin exploitation. @@ -81,12 +84,13 @@ Restrict `AllowedOrigins` to the specific frontend domains that need access (e.g ```go if cfg.Authz != nil && cfg.Authz.Enabled { // Full Cedar/AVP authz setup -} +} // else: only RequireAllowedAccount is applied (account allowlist only) ``` **Risk:** When `cfg.Authz` is `nil` or `cfg.Authz.Enabled` is `false`, the server falls back to the legacy `RequireAllowedAccount` middleware, which only checks if the `X-Amz-Account-Id` is in a static allowlist. This means: + - All accounts in the allowlist have full access to all operations, regardless of which IAM principal they use. - There is no per-resource authorization, no admin requirement, and no Cedar policy evaluation. - Any misconfiguration in the deployment (missing `AUTHZ_ENABLED=true` env var, missing DynamoDB config) silently degrades security. @@ -95,6 +99,7 @@ When `cfg.Authz` is `nil` or `cfg.Authz.Enabled` is `false`, the server falls ba A deployment error (e.g., wrong ConfigMap, missing environment variable, failed DynamoDB connection that returns nil config) causes the server to start in the degraded `allowlist-only` mode. Any allowed account can perform all operations (create, delete, modify clusters, nodepools, etc.) without admin privileges. **What to Mitigate:** + - **Fail-closed:** If authz configuration is expected but missing or invalid, the server should refuse to start rather than fall back to weaker controls. - Add a startup assertion: if `cfg.Authz` is nil and the environment requires authz, log a fatal error and exit. - Add a runtime health check that exposes the current auth mode, so monitoring can alert on degraded auth state. @@ -152,6 +157,7 @@ With `MetricsBindAddress: "0.0.0.0"` and `MetricsPort: 9090`. **Risk:** The Prometheus metrics endpoint is bound to `0.0.0.0:9090` with no authentication. Prometheus metrics can expose: + - Request rates and patterns that reveal which API endpoints are being called (information useful for attackers) - Error rates and types (useful for understanding what attacks are partially succeeding) - Internal runtime metrics (goroutine counts, GC pauses, memory usage) @@ -161,6 +167,7 @@ The Prometheus metrics endpoint is bound to `0.0.0.0:9090` with no authenticatio Any pod in the cluster can scrape `http://rosa-regional-platform-api:9090/metrics` and gain operational visibility into the API service. From within a compromised pod, this data can help an attacker understand traffic patterns and time attacks. **What to Mitigate:** + - Bind the metrics server to `127.0.0.1` (localhost) only, and use Prometheus' in-cluster scraping (which uses the pod's localhost address, not a network service). - Or restrict metrics access via Kubernetes NetworkPolicy to only the Prometheus scraper pod(s). - Or add bearer token authentication to the metrics endpoint. diff --git a/docs/api/zoa-endpoints.md b/docs/api/zoa-endpoints.md index ff57fbf8..250b91b6 100644 --- a/docs/api/zoa-endpoints.md +++ b/docs/api/zoa-endpoints.md @@ -22,24 +22,24 @@ Every mutating and read API call (except catalog and describe) is recorded in th ## Key Types -| Type | Values | Description | -|------|--------|-------------| -| `ExecutionStatus` | `pending`, `running`, `succeeded`, `failed`, `timed_out` | Lifecycle state of a TA execution | -| `OutputStatus` | `pending`, `uploaded`, `failed` | State of S3 artifact upload | -| `ApprovalState` | `not_required`, `pending`, `approved`, `rejected` | Approval lifecycle for an execution | -| `Scope` | `kube-api`, `aws-api` | Where the TA executes (Kubernetes API or AWS API) | -| `Type` | `read`, `write` | Whether the TA is read-only or mutating | +| Type | Values | Description | +| ----------------- | -------------------------------------------------------- | ------------------------------------------------- | +| `ExecutionStatus` | `pending`, `running`, `succeeded`, `failed`, `timed_out` | Lifecycle state of a TA execution | +| `OutputStatus` | `pending`, `uploaded`, `failed` | State of S3 artifact upload | +| `ApprovalState` | `not_required`, `pending`, `approved`, `rejected` | Approval lifecycle for an execution | +| `Scope` | `kube-api`, `aws-api` | Where the TA executes (Kubernetes API or AWS API) | +| `Type` | `read`, `write` | Whether the TA is read-only or mutating | ## Endpoints Overview -| Method | Path | Handler | Description | -|--------|------|---------|-------------| -| `POST` | `/{action}/run` | `Create` | Execute a Trusted Action | -| `GET` | `/runs/{id}` | `Get` | Retrieve execution details | -| `GET` | `/runs` | `List` | List executions (filtered, paginated) | -| `GET` | `/audit` | `AuditList` | List API call audit log entries | -| `GET` | `/` | `Catalog` | List all available Trusted Actions | -| `GET` | `/{action}` | `Describe` | Describe a specific Trusted Action | +| Method | Path | Handler | Description | +| ------ | --------------- | ----------- | ------------------------------------- | +| `POST` | `/{action}/run` | `Create` | Execute a Trusted Action | +| `GET` | `/runs/{id}` | `Get` | Retrieve execution details | +| `GET` | `/runs` | `List` | List executions (filtered, paginated) | +| `GET` | `/audit` | `AuditList` | List API call audit log entries | +| `GET` | `/` | `Catalog` | List all available Trusted Actions | +| `GET` | `/{action}` | `Describe` | Describe a specific Trusted Action | --- @@ -49,9 +49,9 @@ Execute a Trusted Action on a target cluster. ### Path Parameters -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `action` | string | Yes | TA name (e.g., `get_pods`, `rollout_restart`) | +| Parameter | Type | Required | Description | +| --------- | ------ | -------- | --------------------------------------------- | +| `action` | string | Yes | TA name (e.g., `get_pods`, `rollout_restart`) | ### Request Body @@ -70,13 +70,13 @@ Execute a Trusted Action on a target cluster. } ``` -| Field | Type | Required | Description | -|-------|------|----------|-------------| -| `target_cluster` | string | Yes | Target management cluster identifier | -| `jira` | string | Yes | Jira ticket reference; must match `PROJECT-NUMBER` format (e.g. `ROSAENG-1234`) | -| `params` | object | No | Key-value pairs of TA parameters (all values are strings) | -| `force` | boolean | No | Bypass write cooldown for write TAs (default: `false`) | -| `dry_run` | boolean | No | Execute the TA's `dry_run_action` instead (preview; default: `false`) | +| Field | Type | Required | Description | +| ---------------- | ------- | -------- | ------------------------------------------------------------------------------- | +| `target_cluster` | string | Yes | Target management cluster identifier | +| `jira` | string | Yes | Jira ticket reference; must match `PROJECT-NUMBER` format (e.g. `ROSAENG-1234`) | +| `params` | object | No | Key-value pairs of TA parameters (all values are strings) | +| `force` | boolean | No | Bypass write cooldown for write TAs (default: `false`) | +| `dry_run` | boolean | No | Execute the TA's `dry_run_action` instead (preview; default: `false`) | ### Parameter Validation @@ -90,10 +90,10 @@ Parameters are validated against the TA template definition: **Unknown parameter error messages:** -| Condition | Message | -|-----------|---------| -| TA accepts no parameters | `unknown parameter 'foo'; this action accepts no parameters` | -| TA has defined parameters | `unknown parameter 'foo'; allowed parameters: namespace, name, ...` | +| Condition | Message | +| ------------------------------ | ------------------------------------------------------------------------------ | +| TA accepts no parameters | `unknown parameter 'foo'; this action accepts no parameters` | +| TA has defined parameters | `unknown parameter 'foo'; allowed parameters: namespace, name, ...` | | Name matches a top-level field | Same as above, plus hint: `('jira' is a top-level request field, not a param)` | Top-level request fields (`target_cluster`, `jira`, `force`, `dry_run`) must not be placed inside `params`. @@ -128,12 +128,12 @@ Execution created and dispatched to Maestro. **Approval state values:** -| Value | Meaning | -|-------|---------| +| Value | Meaning | +| -------------- | ---------------------------------------------------------------- | | `not_required` | TA policy is `authorization.approval: none` — no approval needed | -| `pending` | Approval required but not yet obtained (execution blocked) | -| `approved` | Required approvals received — execution authorized | -| `rejected` | Approval explicitly denied — execution will not proceed | +| `pending` | Approval required but not yet obtained (execution blocked) | +| `approved` | Required approvals received — execution authorized | +| `rejected` | Approval explicitly denied — execution will not proceed | #### 400 Bad Request @@ -161,13 +161,13 @@ Execution created and dispatched to Maestro. } ``` -| Error Code | Condition | -|-----------|-----------| -| `invalid-request` | Request body is not valid JSON | -| `missing-target-cluster` | `target_cluster` field is empty | -| `missing-jira` | `jira` field is empty | -| `invalid-params` | Required parameter missing, unknown parameter, or namespace scoping violated | -| `invalid-jira` | `jira` does not match `PROJECT-NUMBER` format | +| Error Code | Condition | +| ------------------------ | ---------------------------------------------------------------------------- | +| `invalid-request` | Request body is not valid JSON | +| `missing-target-cluster` | `target_cluster` field is empty | +| `missing-jira` | `jira` field is empty | +| `invalid-params` | Required parameter missing, unknown parameter, or namespace scoping violated | +| `invalid-jira` | `jira` does not match `PROJECT-NUMBER` format | #### 404 Not Found @@ -221,9 +221,9 @@ Write cooldown active or max concurrent limit reached. } ``` -| Error Code | Condition | -|-----------|-----------| -| `write-cooldown` | Write TA executed on same target within cooldown window; use `force: true` to bypass | +| Error Code | Condition | +| ---------------- | ----------------------------------------------------------------------------------------------------- | +| `write-cooldown` | Write TA executed on same target within cooldown window; use `force: true` to bypass | | `max-concurrent` | Target cluster has reached max concurrent executions (running + pending); use `force: true` to bypass | --- @@ -234,24 +234,24 @@ Retrieve an execution's metadata and optionally its output/logs. ### Path Parameters -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `id` | string (UUID) | Yes | Execution ID | +| Parameter | Type | Required | Description | +| --------- | ------------- | -------- | ------------ | +| `id` | string (UUID) | Yes | Execution ID | ### Query Parameters -| Parameter | Values | Default | Description | -|-----------|--------|---------|-------------| -| `include` | `output`, `logs`, or comma-separated combination | (none) | Opt-in: which S3 content to include alongside metadata | +| Parameter | Values | Default | Description | +| --------- | ------------------------------------------------ | ------- | ------------------------------------------------------ | +| `include` | `output`, `logs`, or comma-separated combination | (none) | Opt-in: which S3 content to include alongside metadata | **Content selection behavior:** | `include` Value | Metadata | Output | Logs | -|-----------------|----------|--------|------| -| (empty/omitted) | Yes | No | No | -| `output` | Yes | Yes | No | -| `logs` | Yes | No | Yes | -| `output,logs` | Yes | Yes | Yes | +| --------------- | -------- | ------ | ---- | +| (empty/omitted) | Yes | No | No | +| `output` | Yes | Yes | No | +| `logs` | Yes | No | Yes | +| `output,logs` | Yes | Yes | Yes | S3 content (output/logs) is only fetched for terminal executions (`succeeded`, `failed`, `timed_out`). For `pending` or `running` executions, only metadata is returned regardless of `include`. @@ -274,7 +274,7 @@ S3 content (output/logs) is only fetched for terminal executions (`succeeded`, ` "status": "succeeded", "output_status": "uploaded", "revision": "a1b2c3d", - "params": {"namespace": "maestro", "name": "maestro-abc-123"}, + "params": { "namespace": "maestro", "name": "maestro-abc-123" }, "created_at": "2026-06-10T12:00:00Z", "updated_at": "2026-06-10T12:00:29Z", "completed_at": "2026-06-10T12:00:29Z", @@ -283,7 +283,13 @@ S3 content (output/logs) is only fetched for terminal executions (`succeeded`, ` "duration_seconds": 29, "output": [ - {"name": "maestro-abc-123", "namespace": "maestro", "status": "Running", "restarts": 0, "age": "3d"} + { + "name": "maestro-abc-123", + "namespace": "maestro", + "status": "Running", + "restarts": 0, + "age": "3d" + } ], "logs": "[11:00:01] runner starting\n[zoa] execution_id=fa65418c-... action=get_pods target=mc-useast1-1\n...\n--- upload ---\n[11:00:06] upload starting\n[11:00:09] runner waited (3s)\n[11:00:10] configmap read (1s)\n[11:00:10] decoded (0s), uploading to s3\n" @@ -319,20 +325,20 @@ List executions for the authenticated account, with filtering and pagination. ### Query Parameters -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| `limit` | integer (1-100) | 20 | Max results per page | -| `status` | string | — | Filter: `pending`, `running`, `succeeded`, `failed`, `timed_out` | -| `action` | string | — | Filter by TA name (exact match) | -| `target` | string | — | Filter by target cluster (exact match) | -| `operator` | string | — | Filter by operator name (exact match) | -| `scope` | string | — | Filter by scope: `kube-api`, `aws-api` | -| `type` | string | — | Filter by type: `read`, `write` | -| `output_status` | string | — | Filter by output status: `pending`, `uploaded`, `failed` | -| `approval_state` | string | — | Filter by approval state: `not_required`, `pending`, `approved`, `rejected` | -| `dry_run` | string | — | Filter by dry-run flag: `true` or `false` | -| `force` | string | — | Filter by force flag: `true` or `false` | -| `since` | string | — | Time filter (see below) | +| Parameter | Type | Default | Description | +| ---------------- | --------------- | ------- | --------------------------------------------------------------------------- | +| `limit` | integer (1-100) | 20 | Max results per page | +| `status` | string | — | Filter: `pending`, `running`, `succeeded`, `failed`, `timed_out` | +| `action` | string | — | Filter by TA name (exact match) | +| `target` | string | — | Filter by target cluster (exact match) | +| `operator` | string | — | Filter by operator name (exact match) | +| `scope` | string | — | Filter by scope: `kube-api`, `aws-api` | +| `type` | string | — | Filter by type: `read`, `write` | +| `output_status` | string | — | Filter by output status: `pending`, `uploaded`, `failed` | +| `approval_state` | string | — | Filter by approval state: `not_required`, `pending`, `approved`, `rejected` | +| `dry_run` | string | — | Filter by dry-run flag: `true` or `false` | +| `force` | string | — | Filter by force flag: `true` or `false` | +| `since` | string | — | Time filter (see below) | **`since` format:** @@ -368,7 +374,7 @@ Filters are applied at DynamoDB level: "approval_state": "not_required", "status": "succeeded", "output_status": "uploaded", - "params": {"namespace": "maestro"}, + "params": { "namespace": "maestro" }, "created_at": "2026-06-10T12:00:00Z", "updated_at": "2026-06-10T12:00:29Z", "completed_at": "2026-06-10T12:00:29Z", @@ -405,15 +411,15 @@ Audit logging requires `ZOA_AUDIT_TABLE_NAME` to be configured. If not enabled, ### Query Parameters -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| `limit` | integer (1-200) | 50 | Max results per page | -| `action` | string | — | Filter by TA name | -| `target` | string | — | Filter by target cluster | -| `operator` | string | — | Filter by operator name | -| `method` | string | — | Filter by HTTP method: `GET`, `POST` | -| `approval_state` | string | — | Filter by approval state: `not_required`, `pending`, `approved`, `rejected` | -| `since` | string | — | Time filter (duration shorthand or RFC3339) | +| Parameter | Type | Default | Description | +| ---------------- | --------------- | ------- | --------------------------------------------------------------------------- | +| `limit` | integer (1-200) | 50 | Max results per page | +| `action` | string | — | Filter by TA name | +| `target` | string | — | Filter by target cluster | +| `operator` | string | — | Filter by operator name | +| `method` | string | — | Filter by HTTP method: `GET`, `POST` | +| `approval_state` | string | — | Filter by approval state: `not_required`, `pending`, `approved`, `rejected` | +| `since` | string | — | Time filter (duration shorthand or RFC3339) | ### Responses @@ -557,9 +563,9 @@ Describe a specific Trusted Action — includes full parameter definitions. ### Path Parameters -| Parameter | Type | Required | Description | -|-----------|------|----------|-------------| -| `action` | string | Yes | TA name | +| Parameter | Type | Required | Description | +| --------- | ------ | -------- | ----------- | +| `action` | string | Yes | TA name | ### Responses @@ -613,11 +619,11 @@ Describe a specific Trusted Action — includes full parameter definitions. **Template metadata fields:** -| Field | Type | Default | Description | -|-------|------|---------|-------------| -| `authorization` | object | `{"approval": "none"}` | Authorization policy for this TA. `approval` is `"none"` (no approval needed) or a structured object defining approval requirements (min_count, ttl, approver rules). See authorization design. | -| `write_cooldown_seconds` | integer | `0` (uses global default) | Per-TA write cooldown override in seconds | -| `dry_run_action` | string | `""` | Name of a read TA to execute when `dry_run: true` is set in the request | +| Field | Type | Default | Description | +| ------------------------ | ------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `authorization` | object | `{"approval": "none"}` | Authorization policy for this TA. `approval` is `"none"` (no approval needed) or a structured object defining approval requirements (min_count, ttl, approver rules). See authorization design. | +| `write_cooldown_seconds` | integer | `0` (uses global default) | Per-TA write cooldown override in seconds | +| `dry_run_action` | string | `""` | Name of a read TA to execute when `dry_run: true` is set in the request | #### 404 Not Found @@ -645,22 +651,22 @@ All errors follow a consistent structure: ### Error Codes Reference -| HTTP Status | Code | When | -|-------------|------|------| -| 400 | `invalid-request` | Request body is not valid JSON | -| 400 | `missing-target-cluster` | `target_cluster` not provided | -| 400 | `missing-jira` | `jira` not provided | -| 400 | `invalid-jira` | `jira` format invalid (expected `PROJECT-NUMBER`, e.g. `ROSAENG-1234`) | -| 400 | `invalid-params` | Parameter validation failed | -| 404 | `unknown-action` | TA name not found in registry | -| 404 | `not-found` | Execution ID not found in DynamoDB | -| 429 | `write-cooldown` | Write TA cooldown active on target (use `force: true` to bypass) | -| 429 | `max-concurrent` | Target cluster at max concurrent executions (use `force: true` to bypass) | -| 500 | `store-error` | DynamoDB operation failed | -| 500 | `render-error` | ManifestWork generation failed | -| 500 | `dry-run-error` | `dry_run_action` references unknown TA | -| 502 | `maestro-error` | Maestro gRPC call failed | -| 404 | `audit-disabled` | Audit logging not configured (GET /audit only) | +| HTTP Status | Code | When | +| ----------- | ------------------------ | ------------------------------------------------------------------------- | +| 400 | `invalid-request` | Request body is not valid JSON | +| 400 | `missing-target-cluster` | `target_cluster` not provided | +| 400 | `missing-jira` | `jira` not provided | +| 400 | `invalid-jira` | `jira` format invalid (expected `PROJECT-NUMBER`, e.g. `ROSAENG-1234`) | +| 400 | `invalid-params` | Parameter validation failed | +| 404 | `unknown-action` | TA name not found in registry | +| 404 | `not-found` | Execution ID not found in DynamoDB | +| 429 | `write-cooldown` | Write TA cooldown active on target (use `force: true` to bypass) | +| 429 | `max-concurrent` | Target cluster at max concurrent executions (use `force: true` to bypass) | +| 500 | `store-error` | DynamoDB operation failed | +| 500 | `render-error` | ManifestWork generation failed | +| 500 | `dry-run-error` | `dry_run_action` references unknown TA | +| 502 | `maestro-error` | Maestro gRPC call failed | +| 404 | `audit-disabled` | Audit logging not configured (GET /audit only) | --- @@ -688,20 +694,21 @@ pending → uploaded (uploader Job succeeded) - `timed_out`: Execution exceeded timeout, cleaned up by reconciler **Output status:** + - `pending`: Uploader Job not yet completed - `uploaded`: Uploader Job succeeded, artifacts available in S3 - `failed`: Uploader Job failed (logs still available via execution metadata) ### Timing Fields -| Field | Set When | Meaning | -|-------|----------|---------| -| `created_at` | On POST (submission) | When the execution was requested | -| `updated_at` | On every status transition | Last time the execution record changed (create, pending→running, completion) | -| `completed_at` | On overall completion | When the reconciler detected both Jobs done | -| `runner_seconds` | On overall completion | Runner Job wall-clock time (from K8s `.status.startTime` to `.status.completionTime`) | -| `upload_seconds` | On overall completion | Time from runner completion to uploader completion (wait + configmap + decode + S3 upload) | -| `duration_seconds` | On overall completion | Total wall-clock: `completed_at - created_at` (includes Maestro dispatch overhead) | +| Field | Set When | Meaning | +| ------------------ | -------------------------- | ------------------------------------------------------------------------------------------ | +| `created_at` | On POST (submission) | When the execution was requested | +| `updated_at` | On every status transition | Last time the execution record changed (create, pending→running, completion) | +| `completed_at` | On overall completion | When the reconciler detected both Jobs done | +| `runner_seconds` | On overall completion | Runner Job wall-clock time (from K8s `.status.startTime` to `.status.completionTime`) | +| `upload_seconds` | On overall completion | Time from runner completion to uploader completion (wait + configmap + decode + S3 upload) | +| `duration_seconds` | On overall completion | Total wall-clock: `completed_at - created_at` (includes Maestro dispatch overhead) | **Derived metric** (not stored): `dispatch_overhead = duration_seconds - runner_seconds - upload_seconds` @@ -756,50 +763,50 @@ The `force: true` flag bypasses both safety controls: ### Table: `-regional-zoa-executions` -| Attribute | Type | Key | Description | -|-----------|------|-----|-------------| -| `executionId` | String | PK | UUID v4 | -| `accountId` | String | — | AWS account ID of caller | -| `callerArn` | String | — | Full ARN of STS caller | -| `operator` | String | — | Extracted operator name | -| `action` | String | — | TA name | -| `targetCluster` | String | — | Target MC identifier | -| `scope` | String | — | `kube-api` or `aws-api` | -| `type` | String | — | `read` or `write` | -| `params` | Map | — | Execution parameters (audit trail) | -| `jira` | String | — | Associated Jira ticket | -| `approvalState` | String | — | Approval lifecycle state | -| `status` | String | — | Current status | -| `outputStatus` | String | — | `pending`, `uploaded`, or `failed` | -| `revision` | String | — | Git SHA of TA definition | -| `outputPath` | String | — | S3 URI for output.json | -| `executedAction` | String | — | Substituted action name (dry-run only) | -| `dryRun` | Boolean | — | Whether this was a dry-run execution | -| `force` | Boolean | — | Whether safety checks were bypassed | -| `manifestWorkName` | String | — | Maestro RB name | -| `createdAt` | String (RFC3339) | — | Submission timestamp | -| `updatedAt` | String (RFC3339) | — | Last status transition timestamp | -| `completedAt` | String (RFC3339) | — | Overall completion timestamp | -| `runnerSeconds` | Number | — | Runner Job duration (startTime → completionTime) | -| `uploadSeconds` | Number | — | Upload duration (runner completion → uploader completion) | -| `durationSeconds` | Number | — | Total wall-clock (created → reconciler detected completion) | -| `ttl` | Number (epoch seconds) | — | DynamoDB TTL for auto-expiry (configurable via `dynamodb_ttl_days`, default 365 days; not exposed in API responses) | +| Attribute | Type | Key | Description | +| ------------------ | ---------------------- | --- | ------------------------------------------------------------------------------------------------------------------- | +| `executionId` | String | PK | UUID v4 | +| `accountId` | String | — | AWS account ID of caller | +| `callerArn` | String | — | Full ARN of STS caller | +| `operator` | String | — | Extracted operator name | +| `action` | String | — | TA name | +| `targetCluster` | String | — | Target MC identifier | +| `scope` | String | — | `kube-api` or `aws-api` | +| `type` | String | — | `read` or `write` | +| `params` | Map | — | Execution parameters (audit trail) | +| `jira` | String | — | Associated Jira ticket | +| `approvalState` | String | — | Approval lifecycle state | +| `status` | String | — | Current status | +| `outputStatus` | String | — | `pending`, `uploaded`, or `failed` | +| `revision` | String | — | Git SHA of TA definition | +| `outputPath` | String | — | S3 URI for output.json | +| `executedAction` | String | — | Substituted action name (dry-run only) | +| `dryRun` | Boolean | — | Whether this was a dry-run execution | +| `force` | Boolean | — | Whether safety checks were bypassed | +| `manifestWorkName` | String | — | Maestro RB name | +| `createdAt` | String (RFC3339) | — | Submission timestamp | +| `updatedAt` | String (RFC3339) | — | Last status transition timestamp | +| `completedAt` | String (RFC3339) | — | Overall completion timestamp | +| `runnerSeconds` | Number | — | Runner Job duration (startTime → completionTime) | +| `uploadSeconds` | Number | — | Upload duration (runner completion → uploader completion) | +| `durationSeconds` | Number | — | Total wall-clock (created → reconciler detected completion) | +| `ttl` | Number (epoch seconds) | — | DynamoDB TTL for auto-expiry (configurable via `dynamodb_ttl_days`, default 365 days; not exposed in API responses) | ### GSI: `account-index` -| Key | Attribute | Purpose | -|-----|-----------|---------| -| PK | `accountId` | Scope queries to caller's account | -| SK | `createdAt` | Enable time-range queries (`since` filter) | +| Key | Attribute | Purpose | +| --- | ----------- | ------------------------------------------ | +| PK | `accountId` | Scope queries to caller's account | +| SK | `createdAt` | Enable time-range queries (`since` filter) | Projection: ALL ### GSI: `status-index` -| Key | Attribute | Purpose | -|-----|-----------|---------| -| PK | `status` | Reconciler queries pending/running executions | -| SK | `createdAt` | Order by time | +| Key | Attribute | Purpose | +| --- | ----------- | --------------------------------------------- | +| PK | `status` | Reconciler queries pending/running executions | +| SK | `createdAt` | Order by time | Projection: ALL @@ -807,22 +814,22 @@ Projection: ALL ### Table: `-regional-zoa-audit-log` -| Attribute | Type | Key | Description | -|-----------|------|-----|-------------| -| `accountId` | String | PK | AWS account ID of caller | -| `timestamp` | String (nanosecond RFC3339) | SK | When the API call was made (`2006-01-02T15:04:05.000000000Z`) | -| `approvalState` | String | — | Approval state at time of POST (populated for POST /run) | -| `id` | String (UUID) | — | Unique audit entry ID | -| `callerArn` | String | — | Full ARN of STS caller | -| `operator` | String | — | Extracted operator name | -| `method` | String | — | HTTP method (`GET`, `POST`) | -| `path` | String | — | Full request URI (path + query string) | -| `action` | String | — | TA name (populated for POST /run) | -| `targetCluster` | String | — | Target cluster (populated for POST /run) | -| `executionId` | String | — | Execution ID (POST /run: created ID; GET /runs/{id}: accessed ID) | -| `jira` | String | — | Jira ticket (populated for POST /run) | -| `statusCode` | Number | — | HTTP response status code | -| `ttl` | Number (epoch seconds) | — | DynamoDB TTL for auto-expiry (configurable, default 365 days) | +| Attribute | Type | Key | Description | +| --------------- | --------------------------- | --- | ----------------------------------------------------------------- | +| `accountId` | String | PK | AWS account ID of caller | +| `timestamp` | String (nanosecond RFC3339) | SK | When the API call was made (`2006-01-02T15:04:05.000000000Z`) | +| `approvalState` | String | — | Approval state at time of POST (populated for POST /run) | +| `id` | String (UUID) | — | Unique audit entry ID | +| `callerArn` | String | — | Full ARN of STS caller | +| `operator` | String | — | Extracted operator name | +| `method` | String | — | HTTP method (`GET`, `POST`) | +| `path` | String | — | Full request URI (path + query string) | +| `action` | String | — | TA name (populated for POST /run) | +| `targetCluster` | String | — | Target cluster (populated for POST /run) | +| `executionId` | String | — | Execution ID (POST /run: created ID; GET /runs/{id}: accessed ID) | +| `jira` | String | — | Jira ticket (populated for POST /run) | +| `statusCode` | Number | — | HTTP response status code | +| `ttl` | Number (epoch seconds) | — | DynamoDB TTL for auto-expiry (configurable, default 365 days) | **Key design**: Uses `accountId` as PK and nanosecond-precision `timestamp` as SK, enabling efficient time-range queries per account without a GSI. The `since` filter applies as a key condition on the sort key. @@ -830,12 +837,12 @@ Projection: ALL **Audited endpoints and field population**: -| Endpoint | `action` | `target_cluster` | `execution_id` | `jira` | `approval_state` | -|----------|----------|-------------------|-----------------|--------|------------------| -| `POST /{action}/run` | TA name | target cluster | created exec ID | ticket | approval state | -| `GET /runs/{id}` | — | — | accessed exec ID | — | — | -| `GET /runs` | — | — | — | — | — | -| `GET /audit` | — | — | — | — | — | +| Endpoint | `action` | `target_cluster` | `execution_id` | `jira` | `approval_state` | +| -------------------- | -------- | ---------------- | ---------------- | ------ | ---------------- | +| `POST /{action}/run` | TA name | target cluster | created exec ID | ticket | approval state | +| `GET /runs/{id}` | — | — | accessed exec ID | — | — | +| `GET /runs` | — | — | — | — | — | +| `GET /audit` | — | — | — | — | — | **Rejected requests**: POST requests rejected by validation (400) or rate limits (429) are also audited with whatever context is available at the point of rejection. @@ -932,6 +939,6 @@ curl "$ZOA_API/api/v0/trusted-actions/runs?dry_run=true&since=24h" \ ## Related Documentation -- [ZOA Architecture](https://github.com/openshift-online/rosa-regional-platform/blob/main/docs/design/zoa-architecture.md) — System architecture and network flows -- [ZOA Security Model](https://github.com/openshift-online/rosa-regional-platform/blob/main/docs/design/zoa-security-model.md) — SA isolation and RBAC -- [ZOA Trusted Actions](https://github.com/openshift-online/rosa-regional-platform/blob/main/docs/design/zoa-trusted-actions.md) — TA template format and CLI design +- [ZOA Architecture](https://github.com/openshift-online/rosa-hyperfleet/blob/main/docs/design/zoa-architecture.md) — System architecture and network flows +- [ZOA Security Model](https://github.com/openshift-online/rosa-hyperfleet/blob/main/docs/design/zoa-security-model.md) — SA isolation and RBAC +- [ZOA Trusted Actions](https://github.com/openshift-online/rosa-hyperfleet/blob/main/docs/design/zoa-trusted-actions.md) — TA template format and CLI design diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index b5d2a803..eea8d871 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -1,6 +1,6 @@ openapi: 3.0.3 info: - title: ROSA Regional Platform API + title: ROSA HyperFleet API description: Platform API for ROSA HCP regional cluster management version: 0.0.1 license: diff --git a/test/e2e-api/README.md b/test/e2e-api/README.md index 26cb8252..6c20b794 100644 --- a/test/e2e-api/README.md +++ b/test/e2e-api/README.md @@ -1,6 +1,6 @@ # E2E Tests -End-to-end integration and functional tests for the ROSA Regional Platform API. +End-to-end integration and functional tests for the ROSA HyperFleet API. ## Prerequisites