diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 116fdc51..602af65c 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -3,7 +3,7 @@ name: Bug Report about: Report a bug to help us improve title: "[BUG] " labels: bug -assignees: '' +assignees: "" --- ## Describe the Bug @@ -12,9 +12,9 @@ assignees: '' ## Steps to Reproduce -1. -2. -3. +1. +2. +3. ## Expected Behavior @@ -26,9 +26,9 @@ assignees: '' ## Environment -- OS: -- Version: -- Go version (if applicable): +- OS: +- Version: +- Go version (if applicable): ## Additional Context diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index af6d4395..b6ce24c9 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -3,7 +3,7 @@ name: Feature Request about: Suggest an idea for this project title: "[FEATURE] " labels: enhancement -assignees: '' +assignees: "" --- ## Problem Statement diff --git a/clientset/docs/architecture.md b/clientset/docs/architecture.md index 9ee710d3..d593c612 100644 --- a/clientset/docs/architecture.md +++ b/clientset/docs/architecture.md @@ -102,11 +102,11 @@ func Install(scheme *runtime.Scheme) { The platform API differs from a standard Kubernetes API in three ways that require hand-written code: -| Difference | Solution | -|---|---| -| Requests are signed with AWS SigV4 | `transport/sigv4.go` — custom RoundTripper | -| Resources are account-scoped, not namespace-scoped | SigV4 transport extracts the Kubernetes namespace from the URL, maps it to `X-Amz-Account-Id`, and strips the `/namespaces/{ns}/` segment | -| Wire format is flat JSON, not Kubernetes nested metadata | `transport/bridge.go` — request/response adapter | +| Difference | Solution | +| -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| Requests are signed with AWS SigV4 | `transport/sigv4.go` — custom RoundTripper | +| Resources are account-scoped, not namespace-scoped | SigV4 transport extracts the Kubernetes namespace from the URL, maps it to `X-Amz-Account-Id`, and strips the `/namespaces/{ns}/` segment | +| Wire format is flat JSON, not Kubernetes nested metadata | `transport/bridge.go` — request/response adapter | ### `rest/config.go` — SDK configuration @@ -137,18 +137,24 @@ The `Adapter` RoundTripper handles four transformations: **Response rewriting** — platform API returns flat JSON objects: ```json -{"id": "abc-123", "name": "my-cluster", "resource_version": "1", "spec": {}, "status": {}} +{ + "id": "abc-123", + "name": "my-cluster", + "resource_version": "1", + "spec": {}, + "status": {} +} ``` The Kubernetes decoder populates `v1alpha1.Cluster` from `metadata.*` fields. The adapter rewrites each response before the decoder sees it: -| Wire field | → | Kubernetes field | -|---|---|---| -| `name` | → | `metadata.name` | -| `id` | → | `metadata.uid` | -| `resource_version` | → | `metadata.resourceVersion` | -| `generation` | → | `metadata.generation` | -| `spec`, `status`, … | → | unchanged | +| Wire field | → | Kubernetes field | +| ------------------- | --- | -------------------------- | +| `name` | → | `metadata.name` | +| `id` | → | `metadata.uid` | +| `resource_version` | → | `metadata.resourceVersion` | +| `generation` | → | `metadata.generation` | +| `spec`, `status`, … | → | unchanged | Both single-object and list (`{"items": [...]}`) responses are handled. @@ -159,15 +165,24 @@ Both single-object and list (`{"items": [...]}`) responses are handled. **Error response translation** — platform API errors use a different envelope from `metav1.Status`: ```json -{"kind": "Error", "code": "CLUSTERS-MGMT-001", "reason": "account not authorized"} +{ + "kind": "Error", + "code": "CLUSTERS-MGMT-001", + "reason": "account not authorized" +} ``` client-go's `transformResponse` cannot parse this format and falls back to `StatusReasonUnknown`, silently discarding the server's error message. The adapter intercepts any non-2xx response that matches the platform envelope and rewrites it to a minimal `metav1.Status` JSON body before client-go sees it: ```json -{"apiVersion": "v1", "kind": "Status", "status": "Failure", - "message": "CLUSTERS-MGMT-001: account not authorized", - "reason": "Forbidden", "code": 403} +{ + "apiVersion": "v1", + "kind": "Status", + "status": "Failure", + "message": "CLUSTERS-MGMT-001: account not authorized", + "reason": "Forbidden", + "code": 403 +} ``` This ensures `k8s.io/apimachinery/pkg/api/errors` helpers (`IsNotFound`, `IsForbidden`, etc.) classify errors correctly and that callers receive the full server message rather than a generic unknown error. diff --git a/docs/api/api-management.md b/docs/api/api-management.md index 1e38c7e2..2f332fb5 100644 --- a/docs/api/api-management.md +++ b/docs/api/api-management.md @@ -115,11 +115,11 @@ type ClusterSpec struct { } ``` -| Write Mode | On Create (POST) | On Update (PUT/PATCH) | -| ---------- | ----------------- | --------------------- | -| **mutable** | Customer can set | Customer can change | -| **immutable** | Customer can set | Rejected if changed | -| **service-set** | Platform fills it in | Rejected if present | +| Write Mode | On Create (POST) | On Update (PUT/PATCH) | +| --------------- | -------------------- | --------------------- | +| **mutable** | Customer can set | Customer can change | +| **immutable** | Customer can set | Rejected if changed | +| **service-set** | Platform fills it in | Rejected if present | ### Marker 3: Feature Gate (`+openshift:enable:FeatureGate=X`) diff --git a/docs/api/passthrough-design.md b/docs/api/passthrough-design.md index 2591587a..64692e76 100644 --- a/docs/api/passthrough-design.md +++ b/docs/api/passthrough-design.md @@ -13,12 +13,12 @@ The passthrough codegen pipeline generates Go struct types that mirror upstream ## File roles -| File | Role | -|------|------| -| `api/v1alpha1/zz_generated.passthrough.go` | The committed passthrough types. Human-curated markers live here. This is the source of truth for field policy. Despite the `zz_generated` prefix, this file is intentionally hand-edited to curate markers, then regenerated to pick up upstream struct changes. | -| `api/v1alpha1/configuration.go` | Local mirror of `ClusterConfiguration` with granular markers on nested fields (kubelet, machineConfig). Used by the scanner for nested field marker extraction. | -| `hack/api-codegen/pkg/registry/field_metadata.json` | Generated field registry (JSON). Produced by `marker-scanner` from the passthrough file. Consumed by `passthrough-gen`, `conversion-gen`, and `openapi-gen`. | -| `hack/api-codegen/pkg/registry/field_metadata.go` | Generated field registry (Go). Same data as the JSON, importable by Go code. | +| File | Role | +| --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `api/v1alpha1/zz_generated.passthrough.go` | The committed passthrough types. Human-curated markers live here. This is the source of truth for field policy. Despite the `zz_generated` prefix, this file is intentionally hand-edited to curate markers, then regenerated to pick up upstream struct changes. | +| `api/v1alpha1/configuration.go` | Local mirror of `ClusterConfiguration` with granular markers on nested fields (kubelet, machineConfig). Used by the scanner for nested field marker extraction. | +| `hack/api-codegen/pkg/registry/field_metadata.json` | Generated field registry (JSON). Produced by `marker-scanner` from the passthrough file. Consumed by `passthrough-gen`, `conversion-gen`, and `openapi-gen`. | +| `hack/api-codegen/pkg/registry/field_metadata.go` | Generated field registry (Go). Same data as the JSON, importable by Go code. | ## Pipeline @@ -84,12 +84,12 @@ If HyperShift removes a field from `HostedClusterSpec` or `NodePoolSpec`, `make The registry captures the following marker categories from the passthrough file: -| Marker | Registry field | Purpose | -|--------|---------------|---------| -| `+k8s:openapi-gen=false` | `hidden: true` | Field excluded from public OpenAPI and REST types | -| `+hyperfleet:write-mode=mutable\|immutable\|service-set` | `writeMode` | Controls customer mutability | -| `+openshift:enable:FeatureGate=X` | `featureGate` | Field gated behind a feature flag | -| `+hyperfleet:validation:FeatureGateAwareWriteMode:...` | `featureGateAwareWriteModes` | Write-mode varies by active feature gates | +| Marker | Registry field | Purpose | +| -------------------------------------------------------- | ---------------------------- | ------------------------------------------------- | +| `+k8s:openapi-gen=false` | `hidden: true` | Field excluded from public OpenAPI and REST types | +| `+hyperfleet:write-mode=mutable\|immutable\|service-set` | `writeMode` | Controls customer mutability | +| `+openshift:enable:FeatureGate=X` | `featureGate` | Field gated behind a feature flag | +| `+hyperfleet:validation:FeatureGateAwareWriteMode:...` | `featureGateAwareWriteModes` | Write-mode varies by active feature gates | Upstream markers like `+optional` and `+required` are propagated directly from HyperShift source by `passthrough-gen` via `isForwardedMarker()` — they do not go through the registry. @@ -104,7 +104,7 @@ This ensures new upstream fields don't accidentally become visible or mutable. ## Resolved gaps -The following issues existed in main and were fixed on this branch: +The following issues existed in main and were fixed: 1. **Stale embedded registry** (fixed): `passthrough-gen` embedded a copy of `field_metadata.json` via `//go:embed`. Removed the embedded copy and made `-registry` mandatory. diff --git a/docs/api/v2-sdk-rosa-integration.md b/docs/api/v2-sdk-rosa-integration.md index 8aaf636b..f5da41e9 100644 --- a/docs/api/v2-sdk-rosa-integration.md +++ b/docs/api/v2-sdk-rosa-integration.md @@ -18,7 +18,7 @@ Two findings from the rosa codebase drive the design: SDK cannot simply be swapped underneath the CLI. 2. The HCP lifecycle commands call the OCM API far beyond CRUD — versions, regions, machine types, billing accounts, quota, subscriptions. The - Platform API's *initial* surface (Cluster + NodePool) backs almost none of + Platform API's _initial_ surface (Cluster + NodePool) backs almost none of these, and even as it reaches feature parity over time, those features return **re-based on AWS-account tenancy** instead of the OCM org. The v2 flow is therefore a genuinely different flow — initially smaller, and @@ -64,11 +64,11 @@ cluster model. Measured by distinct `OCMClient` method calls: -| Code path | Distinct OCM calls | Examples beyond CRUD | -|---|---|---| -| `cmd/create/cluster/cmd.go` | ~16 | `GetRegionList`, `ValidateVersion`, `GetAvailableMachineTypesInRegion`, `GetBillingAccounts`, `GetCredRequests`, `EnsureNoPendingClusters` (quota), `IsTechnologyPreview`, `GetOidcConfig` | -| `cmd/describe/cluster/cmd.go` | ~11 | `GetSubscriptionBySubscriptionID`, `GetLimitedSupportReasons`, `GetInflightChecks`, `FetchClusterMigrations`, `GetScheduledUpgrade` | -| `pkg/machinepool` | ~19 | `GetDefaultClusterFlavors`, `ListKubeletConfigNames`, `GetTuningConfigsName`, `GetClusterAutoscaler` | +| Code path | Distinct OCM calls | Examples beyond CRUD | +| ----------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `cmd/create/cluster/cmd.go` | ~16 | `GetRegionList`, `ValidateVersion`, `GetAvailableMachineTypesInRegion`, `GetBillingAccounts`, `GetCredRequests`, `EnsureNoPendingClusters` (quota), `IsTechnologyPreview`, `GetOidcConfig` | +| `cmd/describe/cluster/cmd.go` | ~11 | `GetSubscriptionBySubscriptionID`, `GetLimitedSupportReasons`, `GetInflightChecks`, `FetchClusterMigrations`, `GetScheduledUpgrade` | +| `pkg/machinepool` | ~19 | `GetDefaultClusterFlavors`, `ListKubeletConfigNames`, `GetTuningConfigsName`, `GetClusterAutoscaler` | The HyperFleet Platform API's initial surface is **Cluster + NodePool only** (see [v2-sdk-initiative.md](v2-sdk-initiative.md)). Today there is nothing to @@ -91,11 +91,11 @@ the SDK through `r.OCMClient.*` and `r.FetchCluster()` (`pkg/rosa/runner.go`, ### 4. Auth models diverge -| | v1 (OCM) | v2 (HyperFleet) | -|---|---|---| -| Credential | OCM SSO tokens (`rosa login`, `config.Load()`) | AWS IAM (SigV4) | -| Endpoint | Global `api.openshift.com` | Per-region Platform API endpoint | -| Runtime support today | `WithOCM()` | needs AWS creds (`WithAWS()` exists) + endpoint resolution | +| | v1 (OCM) | v2 (HyperFleet) | +| --------------------- | ---------------------------------------------- | ---------------------------------------------------------- | +| Credential | OCM SSO tokens (`rosa login`, `config.Load()`) | AWS IAM (SigV4) | +| Endpoint | Global `api.openshift.com` | Per-region Platform API endpoint | +| Runtime support today | `WithOCM()` | needs AWS creds (`WithAWS()` exists) + endpoint resolution | `--hyperfleet` mode cannot reuse the login config; it needs AWS credentials (the Runtime already obtains these via `WithAWS()`) plus regional endpoint @@ -150,20 +150,20 @@ rosa create cluster --hosted-cp [--hyperfleet] radius), the v2 path maps `v1alpha1.Cluster → cmv1.Cluster` **only for display**, populating only fields the printers read. `cmv1` types are constructible via builders (`cmv1.NewCluster().ID(...).Build()`), so this is - mechanical. Fields with no HyperFleet equivalent *yet* (subscription, + mechanical. Fields with no HyperFleet equivalent _yet_ (subscription, limited support, billing) render as absent; the mapper grows alongside the Platform API surface and the commands wired to it. ### Commands in scope (per acceptance criteria) -| Command | v2 path | -|---|---| -| `rosa create cluster --hosted-cp` | flags → `v1alpha1.Cluster` → `Clusters().Create()` | -| `rosa describe cluster` | `Clusters().Get()` → display mapper → existing printer | -| `rosa edit cluster` | `Clusters().Patch()` / `Update()` | -| `rosa delete cluster` | `Clusters().Delete()` | -| `rosa create/list/describe/edit/delete machinepool` (HCP) | `Clusters().NodePools(...)` verbs | -| `rosa logs`, log forwarders | TBD — depends on Platform API logs surface (initiative Open Question 4) | +| Command | v2 path | +| --------------------------------------------------------- | ----------------------------------------------------------------------- | +| `rosa create cluster --hosted-cp` | flags → `v1alpha1.Cluster` → `Clusters().Create()` | +| `rosa describe cluster` | `Clusters().Get()` → display mapper → existing printer | +| `rosa edit cluster` | `Clusters().Patch()` / `Update()` | +| `rosa delete cluster` | `Clusters().Delete()` | +| `rosa create/list/describe/edit/delete machinepool` (HCP) | `Clusters().NodePools(...)` verbs | +| `rosa logs`, log forwarders | TBD — depends on Platform API logs surface (initiative Open Question 4) | Everything else (`idp`, `ingress`, `addons`, roles, upgrades, …) has no v2 path and errors fast under `--hyperfleet`. @@ -192,7 +192,7 @@ implementation on the flag, and having the v2 implementation map - Even at feature parity, the flows fork wherever identity is consulted. Most v1 features (billing account selection, versions, IdPs, …) are expected to return in the Platform API — re-based on **AWS-account tenancy** instead of - the OCM org (some, like the OCM quota precheck, will not). The *step* + the OCM org (some, like the OCM quota precheck, will not). The _step_ "select a billing account" exists in both flows, but the data source, credentials, and failure modes differ. Shared command code would need identity-conditional branches at every such point — which is command-level @@ -207,17 +207,17 @@ Two other alternatives were rejected earlier and remain rejected: ## Risks and Mitigations -| Risk | Mitigation | -|---|---| -| Output drift between v1 and v2 paths | Reuse v1 printers via the display mapper; acceptance e2e tests (`hcp_cluster_test.go`, `hcp_machine_pool_test.go`) are the contract | -| Duplicated command-flow logic | The duplicated part is precisely what *must* differ (validation backed by endpoints that don't exist in v2); keep v2 run functions thin and share flag parsing/printing | -| Interactive mode in v2 (`--interactive` prompts source region/version/machine-type lists from OCM) | Phase 1: disable interactive under `--hyperfleet`; later source lists from AWS APIs or Platform API when available | -| Flag surface ambiguity (~300 create-cluster flags, many meaningless in v2) | Explicit allowlist per v2 command; anything else errors with "not supported with --hyperfleet" | -| Display mapper completeness | Populate only fields the printers read for the acceptance-test commands; grow test-by-test | +| Risk | Mitigation | +| -------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Output drift between v1 and v2 paths | Reuse v1 printers via the display mapper; acceptance e2e tests (`hcp_cluster_test.go`, `hcp_machine_pool_test.go`) are the contract | +| Duplicated command-flow logic | The duplicated part is precisely what _must_ differ (validation backed by endpoints that don't exist in v2); keep v2 run functions thin and share flag parsing/printing | +| Interactive mode in v2 (`--interactive` prompts source region/version/machine-type lists from OCM) | Phase 1: disable interactive under `--hyperfleet`; later source lists from AWS APIs or Platform API when available | +| Flag surface ambiguity (~300 create-cluster flags, many meaningless in v2) | Explicit allowlist per v2 command; anything else errors with "not supported with --hyperfleet" | +| Display mapper completeness | Populate only fields the printers read for the acceptance-test commands; grow test-by-test | ## Relationship to the "migrate directly to client-go" vision -This approach *is* the direct migration, scoped: the v2 run functions consume +This approach _is_ the direct migration, scoped: the v2 run functions consume the v2 SDK natively (`v1alpha1` struct literals + verb methods), exactly as the initiative doc intends. Only the presentation layer borrows v1's printers via the display mapper — a bounded concession to "identical output formats" that @@ -234,30 +234,30 @@ and mismatch validation that Story 3 implementations and tests must follow. ### Precedence (highest wins) -| Priority | Source | What it sets | -|----------|--------|-------------| -| 1 | `--hyperfleet-url ` flag | Explicit endpoint URL; skips all other endpoint resolution | -| 2 | Built-in region→endpoint map | Endpoint looked up by resolved region (see below) | -| 3 | Discovery (future) | Reserved for a future service-discovery mechanism; not implemented in the initial version — if precedence 1 and 2 both miss, resolution fails | +| Priority | Source | What it sets | +| -------- | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | +| 1 | `--hyperfleet-url ` flag | Explicit endpoint URL; skips all other endpoint resolution | +| 2 | Built-in region→endpoint map | Endpoint looked up by resolved region (see below) | +| 3 | Discovery (future) | Reserved for a future service-discovery mechanism; not implemented in the initial version — if precedence 1 and 2 both miss, resolution fails | ### Region derivation (highest wins) -| Priority | Source | -|----------|--------| -| 1 | `--region` flag (explicit) | -| 2 | `AWS_DEFAULT_REGION` environment variable | -| 3 | AWS SDK default credential chain region (e.g. `~/.aws/config` profile) | +| Priority | Source | +| -------- | ---------------------------------------------------------------------- | +| 1 | `--region` flag (explicit) | +| 2 | `AWS_DEFAULT_REGION` environment variable | +| 3 | AWS SDK default credential chain region (e.g. `~/.aws/config` profile) | If none of these sources yield a region, `WithHyperFleet()` must return a clear error: `"region is required for --hyperfleet mode: set --region, AWS_DEFAULT_REGION, or configure a region in your AWS profile"`. ### Error behavior -| Condition | Behavior | -|-----------|----------| -| No region resolvable | Fatal error before SDK client construction (see message above) | +| Condition | Behavior | +| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | +| No region resolvable | Fatal error before SDK client construction (see message above) | | Region resolved but not present in the built-in region→endpoint map and `--hyperfleet-url` not set | Fatal error: `"no Platform API endpoint known for region %s; use --hyperfleet-url to specify one explicitly"` | -| `--hyperfleet-url` set but no region resolvable | Fatal error: region is still required for SigV4 signing even when the endpoint is explicit | +| `--hyperfleet-url` set but no region resolvable | Fatal error: region is still required for SigV4 signing even when the endpoint is explicit | ### Region/endpoint mismatch validation diff --git a/docs/api/zoa-endpoints.md b/docs/api/zoa-endpoints.md index 97ff2535..eff7a123 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 via Manifest. **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 via Manifest. } ``` -| 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,10 @@ S3 content (output/logs) is only fetched for terminal executions (`succeeded`, ` "status": "succeeded", "output_status": "uploaded", "revision": "a1b2c3d", - "params": {"namespace": "hyperfleet", "name": "hyperfleet-operator-abc-123"}, + "params": { + "namespace": "hyperfleet", + "name": "hyperfleet-operator-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 +286,13 @@ S3 content (output/logs) is only fetched for terminal executions (`succeeded`, ` "duration_seconds": 29, "output": [ - {"name": "hyperfleet-operator-abc-123", "namespace": "hyperfleet", "status": "Running", "restarts": 0, "age": "3d"} + { + "name": "hyperfleet-operator-abc-123", + "namespace": "hyperfleet", + "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 +328,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 +377,7 @@ Filters are applied at DynamoDB level: "approval_state": "not_required", "status": "succeeded", "output_status": "uploaded", - "params": {"namespace": "hyperfleet"}, + "params": { "namespace": "hyperfleet" }, "created_at": "2026-06-10T12:00:00Z", "updated_at": "2026-06-10T12:00:29Z", "completed_at": "2026-06-10T12:00:29Z", @@ -405,15 +414,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 +566,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 +622,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 +654,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` | Manifest generation failed | -| 500 | `dry-run-error` | `dry_run_action` references unknown TA | -| 502 | `dispatch-error` | Manifest creation on hyperfleet-db 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` | Manifest generation failed | +| 500 | `dry-run-error` | `dry_run_action` references unknown TA | +| 502 | `dispatch-error` | Manifest creation on hyperfleet-db failed | +| 404 | `audit-disabled` | Audit logging not configured (GET /audit only) | --- @@ -688,20 +697,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 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 dispatch overhead) | **Derived metric** (not stored): `dispatch_overhead = duration_seconds - runner_seconds - upload_seconds` @@ -756,50 +766,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 | — | Manifest CR 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 | — | Manifest CR 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 +817,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 +840,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. diff --git a/docs/authz.md b/docs/authz.md index 08554b32..7b33e091 100644 --- a/docs/authz.md +++ b/docs/authz.md @@ -114,10 +114,10 @@ Principal linking grants administrative access when the linked Red Hat user hold Each AWS account maps to exactly one Red Hat organization (many-to-one: one RH org can have many AWS accounts). -| Scope | What | -| --- | --- | -| **Global** | AWS IAM identity, AWS account → RH org mapping, IAM principal → RH user mapping, RH Org Admin status, RBAC role assignments, ROSA policies, global attachments | -| **Regional (per AWS account, per region)** | Regional attachments, policy evaluation (AVP policy stores), ROSA resources (clusters, node pools, access entries) | +| Scope | What | +| ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Global** | AWS IAM identity, AWS account → RH org mapping, IAM principal → RH user mapping, RH Org Admin status, RBAC role assignments, ROSA policies, global attachments | +| **Regional (per AWS account, per region)** | Regional attachments, policy evaluation (AVP policy stores), ROSA resources (clusters, node pools, access entries) | ROSA policies are defined globally — a ROSA policy created from any region is available everywhere. Attachments can be global (replicated to all regions) or regional (stored only in the target region). To restrict a policy to specific regions, use `context.region` conditions in Cedar (see [Policy Examples](#policy-examples)), or use regional attachments to limit where a ROSA policy is applied. @@ -141,14 +141,14 @@ Organization Administrators can attach managed ROSA policies to any IAM principa ## Data Storage -| Entity | Storage | Scope | -| --- | --- | --- | -| AWS account → RH org mapping | DynamoDB Global Tables | Global | -| IAM principal → RH user mapping | DynamoDB Global Tables | Global | -| ROSA policy templates | DynamoDB Global Tables | Global | -| Global attachments | DynamoDB Global Tables | Global | -| Regional attachments | DynamoDB (regional, non-global) | Regional | -| Policy evaluation | AVP IsAuthorized API | Regional (per AWS account, per region) | +| Entity | Storage | Scope | +| ------------------------------- | ------------------------------- | -------------------------------------- | +| AWS account → RH org mapping | DynamoDB Global Tables | Global | +| IAM principal → RH user mapping | DynamoDB Global Tables | Global | +| ROSA policy templates | DynamoDB Global Tables | Global | +| Global attachments | DynamoDB Global Tables | Global | +| Regional attachments | DynamoDB (regional, non-global) | Regional | +| Policy evaluation | AVP IsAuthorized API | Regional (per AWS account, per region) | DynamoDB Global Tables are the source of truth for ROSA policies and global attachments. Regional attachments are stored in a standard (non-global) DynamoDB table in each region. AVP is used only for evaluation — it is not the source of truth. @@ -156,30 +156,30 @@ DynamoDB Global Tables are the source of truth for ROSA policies and global atta ### Account Management (Org Admin Only) -| Method | Path | Description | -| --- | --- | --- | -| POST | `/api/v0/accounts` | Link an AWS account (creates policy store) | -| GET | `/api/v0/accounts` | List linked accounts | -| GET | `/api/v0/accounts/{id}` | Get AWS account details | -| DELETE | `/api/v0/accounts/{id}` | Unlink AWS account (deletes policy store) | +| Method | Path | Description | +| ------ | ----------------------- | ------------------------------------------ | +| POST | `/api/v0/accounts` | Link an AWS account (creates policy store) | +| GET | `/api/v0/accounts` | List linked accounts | +| GET | `/api/v0/accounts/{id}` | Get AWS account details | +| DELETE | `/api/v0/accounts/{id}` | Unlink AWS account (deletes policy store) | ### Policy Management (Org Admin or Authorized Principal) -| Method | Path | Description | -| --- | --- | --- | -| POST | `/api/v0/authz/policies` | Create policy | -| GET | `/api/v0/authz/policies` | List policies | -| GET | `/api/v0/authz/policies/{id}` | Get policy | -| PUT | `/api/v0/authz/policies/{id}` | Update policy | +| Method | Path | Description | +| ------ | ----------------------------- | ------------- | +| POST | `/api/v0/authz/policies` | Create policy | +| GET | `/api/v0/authz/policies` | List policies | +| GET | `/api/v0/authz/policies/{id}` | Get policy | +| PUT | `/api/v0/authz/policies/{id}` | Update policy | | DELETE | `/api/v0/authz/policies/{id}` | Delete policy | ### Attachment Management (Org Admin or Authorized Principal) -| Method | Path | Description | -| --- | --- | --- | -| POST | `/api/v0/authz/attachments` | Attach policy to a principal (global or regional) | -| GET | `/api/v0/authz/attachments` | List attachments (global + current region's regional) | -| DELETE | `/api/v0/authz/attachments/{id}` | Detach policy | +| Method | Path | Description | +| ------ | -------------------------------- | ----------------------------------------------------- | +| POST | `/api/v0/authz/attachments` | Attach policy to a principal (global or regional) | +| GET | `/api/v0/authz/attachments` | List attachments (global + current region's regional) | +| DELETE | `/api/v0/authz/attachments/{id}` | Detach policy | Attachments bind a ROSA policy to an IAM principal ARN (user or role). Attachments are **global** by default. Pass `--regional` to create a regional attachment that applies only in the current region. @@ -189,9 +189,9 @@ Attachments bind a ROSA policy to an IAM principal ARN (user or role). Attachmen ### Authorization Check -| Method | Path | Description | -| --- | --- | --- | -| POST | `/api/v0/authz/check` | Test whether a principal is authorized for a given action/resource | +| Method | Path | Description | +| ------ | --------------------- | ------------------------------------------------------------------ | +| POST | `/api/v0/authz/check` | Test whether a principal is authorized for a given action/resource | > **Note:** Policy and attachment management endpoints are accessible to Organization Administrators (via RH token) and to any IAM principal that has been granted a Cedar policy authorizing policy management. The `/api/v0/authz/check` endpoint allows a principal to check their own permissions. Checking another principal's permissions requires administrative access or a Cedar policy granting the `CheckAuthorization` action. @@ -374,6 +374,7 @@ when { context.requestTime.hour >= 9 && context.requestTime.hour < 17 }; permit(?principal, action, resource) when { context.region in ["us-east-1", "us-west-2"] }; ``` + > **Note:** In order for this policy to take effect, the IAM principal must have a corresponding attachment in the specified regions, either globally or regionally. ```cedar @@ -430,15 +431,15 @@ This means a single policy scoped to a cluster covers all current and future chi Context attributes are passed alongside each AVP authorization request and can be referenced in Cedar policies via `context.`. The available attributes are derived from the SigV4 request as it flows through API Gateway (IAM auth mode): -| Attribute | Type | Description | -| --- | --- | --- | -| `region` | String | AWS region where the request is being evaluated (e.g., `us-east-1`) | -| `principalArn` | String | Full ARN of the calling IAM principal | -| `accountId` | String | AWS account ID of the caller | -| `sourceIp` | String | Source IP address of the request | -| `userAgent` | String | User-Agent header from the request | -| `requestTime` | Record | Request timestamp with `hour`, `dayOfWeek`, and `timezone` fields for time-based policies. The `timezone` field (IANA tz name, e.g., `America/New_York`) is mandatory in time-based conditions | -| `requestLabels` | Map\ | Labels provided in the request body (e.g., when creating a cluster) | +| Attribute | Type | Description | +| --------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `region` | String | AWS region where the request is being evaluated (e.g., `us-east-1`) | +| `principalArn` | String | Full ARN of the calling IAM principal | +| `accountId` | String | AWS account ID of the caller | +| `sourceIp` | String | Source IP address of the request | +| `userAgent` | String | User-Agent header from the request | +| `requestTime` | Record | Request timestamp with `hour`, `dayOfWeek`, and `timezone` fields for time-based policies. The `timezone` field (IANA tz name, e.g., `America/New_York`) is mandatory in time-based conditions | +| `requestLabels` | Map\ | Labels provided in the request body (e.g., when creating a cluster) | > **Note:** IAM-internal condition keys such as `aws:MultiFactorAuthPresent` and session tags (`aws:PrincipalTag/*`) are not available — API Gateway does not forward them to the backend. diff --git a/docs/e2e-lifecycle-testing.md b/docs/e2e-lifecycle-testing.md index 96d5242c..3c197a06 100644 --- a/docs/e2e-lifecycle-testing.md +++ b/docs/e2e-lifecycle-testing.md @@ -37,13 +37,14 @@ The `hcp:available` phase is distinct from `hcp:monitor` — it represents the w **State**: Infrastructure provisioning in progress. **Purpose**: Create the cloud infrastructure required for an HCP. **Current tests**: -| Test | Label | Description | -|------|-------|-------------| -| Login to BASE_URL | `login` | Authenticate rosactl against the platform API | -| Create cluster-vpc | `vpc-create` | Provision VPC via rosactl | -| List cluster-vpc | `vpc-list` | Verify VPC appears in listing | -| Create cluster-iam | `iam-create` | Provision IAM roles via rosactl | -| List cluster-iam | `iam-list` | Verify IAM roles appear in listing | + +| Test | Label | Description | +| -------------------- | ------------- | ----------------------------------------------- | +| Login to BASE_URL | `login` | Authenticate rosactl against the platform API | +| Create cluster-vpc | `vpc-create` | Provision VPC via rosactl | +| List cluster-vpc | `vpc-list` | Verify VPC appears in listing | +| Create cluster-iam | `iam-create` | Provision IAM roles via rosactl | +| List cluster-iam | `iam-list` | Verify IAM roles appear in listing | | Add customer account | `account-add` | Register customer account with the platform API | #### `hcp:post-setup` @@ -63,11 +64,12 @@ The `hcp:available` phase is distinct from `hcp:monitor` — it represents the w **State**: HCP creation in progress. **Purpose**: Create the HCP and supporting resources (OIDC). **Current tests**: -| Test | Label | Description | -|------|-------|-------------| -| Create HCP cluster | `hcp-create` | Create cluster via rosactl, capture cluster ID | -| Create cluster-oidc | `oidc-create` | Provision OIDC provider for the cluster | -| List cluster-oidc | `oidc-list` | Verify OIDC provider appears in listing | + +| Test | Label | Description | +| ------------------- | ------------- | ---------------------------------------------- | +| Create HCP cluster | `hcp-create` | Create cluster via rosactl, capture cluster ID | +| Create cluster-oidc | `oidc-create` | Provision OIDC provider for the cluster | +| List cluster-oidc | `oidc-list` | Verify OIDC provider appears in listing | #### `hcp:post-create` @@ -86,10 +88,11 @@ The `hcp:available` phase is distinct from `hcp:monitor` — it represents the w **State**: Cluster transitioning to ready. **Purpose**: Poll for readiness. **Current tests**: -| Test | Label | Description | -|------|-------|-------------| + +| Test | Label | Description | +| ---------------------- | ---------------- | --------------------------------------------------------------------------------------- | | Wait for cluster ready | `cluster-status` | Poll `/clusters/{id}/statuses` until all controller conditions are True (20min timeout) | -| Wait for nodepools | `nodepools-wait` | Wait 5min for nodepools to deploy | +| Wait for nodepools | `nodepools-wait` | Wait 5min for nodepools to deploy | #### `hcp:post-monitor` @@ -108,8 +111,9 @@ The `hcp:available` phase is distinct from `hcp:monitor` — it represents the w **State**: HCP is fully operational. API server reachable, nodepools ready, metrics flowing. **Purpose**: Run functional tests against a live HCP. This is the primary phase for feature verification. **Current tests**: -| Test | Label | Description | -|------|-------|-------------| + +| Test | Label | Description | +| ----------------------- | ------------- | -------------------------------------------------------------------- | | HCP availability metric | `hcp-metrics` | Query Thanos for `hcp:hostedcluster_available` recording rule metric | **Example uses**: Run workloads on the cluster, verify ingress, test HCP API server responsiveness, validate SLA recording rules are producing data, run customer-facing feature smoke tests. @@ -131,14 +135,15 @@ The `hcp:available` phase is distinct from `hcp:monitor` — it represents the w **State**: Teardown in progress. **Purpose**: Delete the HCP and all associated infrastructure. **Current tests**: -| Test | Label | Description | -|------|-------|-------------| -| Delete HCP cluster | `hcp-delete` | DELETE `/clusters/{id}`, expect 202 | -| Poll until deleted | `cluster-query` | Poll GET `/clusters/{id}` until 404/410 (10min timeout) | -| Delete resource bundles | `bundles-delete` | Delete all resource bundles matching cluster ID | -| Delete cluster-oidc | `oidc-delete` | Remove OIDC provider via rosactl | -| Delete cluster-vpc | `vpc-delete` | Remove VPC via rosactl (3 retries, 5min backoff) | -| Delete cluster-iam | `iam-delete` | Remove IAM roles via rosactl | + +| Test | Label | Description | +| ----------------------- | ---------------- | ------------------------------------------------------- | +| Delete HCP cluster | `hcp-delete` | DELETE `/clusters/{id}`, expect 202 | +| Poll until deleted | `cluster-query` | Poll GET `/clusters/{id}` until 404/410 (10min timeout) | +| Delete resource bundles | `bundles-delete` | Delete all resource bundles matching cluster ID | +| Delete cluster-oidc | `oidc-delete` | Remove OIDC provider via rosactl | +| Delete cluster-vpc | `vpc-delete` | Remove VPC via rosactl (3 retries, 5min backoff) | +| Delete cluster-iam | `iam-delete` | Remove IAM roles via rosactl | #### `hcp:post-cleanup` diff --git a/docs/konflux/quay-image-tags.md b/docs/konflux/quay-image-tags.md index d2d23e62..f69e1971 100644 --- a/docs/konflux/quay-image-tags.md +++ b/docs/konflux/quay-image-tags.md @@ -2,9 +2,9 @@ This repository builds container images on Konflux (`rosa-tenant` on `kflux-prd-rh02`). Builds are defined under [`.tekton/`](../../.tekton/). -| Component | Quay repository | PipelineRun names | -| --- | --- | --- | -| `platform-api` | `quay.io/redhat-user-workloads/rosa-tenant/platform-api` | `rosa-hyperfleet-api-on-pull-request`, `rosa-hyperfleet-api-on-push` | +| Component | Quay repository | PipelineRun names | +| --------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------ | +| `platform-api` | `quay.io/redhat-user-workloads/rosa-tenant/platform-api` | `rosa-hyperfleet-api-on-pull-request`, `rosa-hyperfleet-api-on-push` | | `hyperfleet-operator` | `quay.io/redhat-user-workloads/rosa-tenant/hyperfleet-operator` | `rosa-hyperfleet-operator-on-pull-request`, `rosa-hyperfleet-operator-on-push` | Pull-request and push builds **share the same Quay repository** (one ImageRepository per component). That is expected Konflux behavior, not a separate “PR” vs “release” repo. @@ -13,12 +13,12 @@ Konflux **component** names in this repo (`rosa-hyperfleet-api`, `rosa-hyperflee ## When pipelines run -| Pipeline | Trigger (summary) | -| --- | --- | -| `rosa-hyperfleet-api-on-push` | Every push to `main` | -| `rosa-hyperfleet-api-on-pull-request` | Pull requests targeting `main` | -| `rosa-hyperfleet-operator-on-push` | Push to `main` only when `hyperfleet-operator/`, `hyperfleet-db/`, or related Tekton/Containerfile paths change | -| `rosa-hyperfleet-operator-on-pull-request` | Same path filter on pull requests | +| Pipeline | Trigger (summary) | +| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------- | +| `rosa-hyperfleet-api-on-push` | Every push to `main` | +| `rosa-hyperfleet-api-on-pull-request` | Pull requests targeting `main` | +| `rosa-hyperfleet-operator-on-push` | Push to `main` only when `hyperfleet-operator/`, `hyperfleet-db/`, or related Tekton/Containerfile paths change | +| `rosa-hyperfleet-operator-on-pull-request` | Same path filter on pull requests | A commit SHA tag for `hyperfleet-operator` exists on Quay **only if** that commit triggered the operator on-push (or on-pull-request) pipeline. A `platform-api`-only merge still produces a new `platform-api:` image on every push to `main`, but does not necessarily rebuild the operator image at that SHA. @@ -42,10 +42,10 @@ Do **not** use Quay `:latest` as the source of truth. Konflux push pipelines tag A single successful on-push run pushes more than the runnable image. The default multi-platform OCI pipeline also publishes **trusted-build artifacts** into the same repository, using suffix tags on the same commit: -| Tag pattern | Meaning | -| --- | --- | -| `` | Runnable container image — **use this for pins** | -| `.git` | Git/source artifact for the trusted build chain | +| Tag pattern | Meaning | +| ---------------- | ------------------------------------------------ | +| `` | Runnable container image — **use this for pins** | +| `.git` | Git/source artifact for the trusted build chain | | `.prefetch` | Prefetched dependencies (hermetic / gomod cache) | Source images may be enabled (`build-source-image: true`). Quay’s tag list can look like several entries for one pipeline run; only the plain `` tag (no suffix) is the image to deploy or reference from `rosa-hyperfleet` Helm values. @@ -74,11 +74,11 @@ If e2e or regional tests fail after a change merges to `platform-api` on `main` ## Quick reference in Quay -| What you see | Interpretation | -| --- | --- | -| `on-pr-*` | Pull-request build (short-lived) | -| Plain 40-character hex SHA | `main` push (or the commit that built the image) — **deployable image** | -| `.git`, `.prefetch` | Pipeline artifacts — ignore for deploy pins | +| What you see | Interpretation | +| ----------------------------- | ----------------------------------------------------------------------- | +| `on-pr-*` | Pull-request build (short-lived) | +| Plain 40-character hex SHA | `main` push (or the commit that built the image) — **deployable image** | +| `.git`, `.prefetch` | Pipeline artifacts — ignore for deploy pins | ## Related configuration diff --git a/hack/api-codegen/README.md b/hack/api-codegen/README.md index 68d7a0c8..8deebf28 100644 --- a/hack/api-codegen/README.md +++ b/hack/api-codegen/README.md @@ -15,15 +15,15 @@ This tooling follows the same patterns as the GCP HCP **gecko/orlop** codegen: ## Generators -| Command | Purpose | -|---------|---------| -| `passthrough-gen` | Generate passthrough struct types from HyperShift API types | -| `marker-scanner` | Extract `+hyperfleet:` marker metadata from Go types | -| `openapi-gen` | Generate OpenAPI v3 schemas via controller-tools CRD extraction | -| `conversion-gen` | Generate JSON-roundtrip conversion functions (CRD ↔ REST) | -| `crd-variants` | Produce CRD variants filtered by feature gates | -| `featuregate-info` | Emit feature gate metadata for CRD fields | -| `verify-configuration` | Validate marker consistency across types | +| Command | Purpose | +| ---------------------- | --------------------------------------------------------------- | +| `passthrough-gen` | Generate passthrough struct types from HyperShift API types | +| `marker-scanner` | Extract `+hyperfleet:` marker metadata from Go types | +| `openapi-gen` | Generate OpenAPI v3 schemas via controller-tools CRD extraction | +| `conversion-gen` | Generate JSON-roundtrip conversion functions (CRD ↔ REST) | +| `crd-variants` | Produce CRD variants filtered by feature gates | +| `featuregate-info` | Emit feature gate metadata for CRD fields | +| `verify-configuration` | Validate marker consistency across types | ## Package layout @@ -58,15 +58,15 @@ make coverage-api-codegen # Generate coverage report The top-level Makefile exposes the following targets for running the codegen pipeline: -| Target | Description | -|--------|-------------| +| Target | Description | +| --------------------- | --------------------------------------------------------------- | | `codegen-passthrough` | Generate passthrough types from HyperShift into `api/v1alpha1/` | -| `codegen-registry` | Generate field metadata registry from `+hyperfleet:` markers | -| `codegen-conversion` | Generate REST types and Project/Unproject conversion functions | -| `codegen-verify` | Verify codegen outputs compile (`api` + `platform-api`) | -| `codegen` | Run full pipeline: passthrough + registry + verify | -| `verify-codegen` | Fail if codegen outputs are out of date (git diff check) | -| `verify-conversion` | Fail if conversion outputs are out of date | +| `codegen-registry` | Generate field metadata registry from `+hyperfleet:` markers | +| `codegen-conversion` | Generate REST types and Project/Unproject conversion functions | +| `codegen-verify` | Verify codegen outputs compile (`api` + `platform-api`) | +| `codegen` | Run full pipeline: passthrough + registry + verify | +| `verify-codegen` | Fail if codegen outputs are out of date (git diff check) | +| `verify-conversion` | Fail if conversion outputs are out of date | ### Dependency chain diff --git a/hyperfleet-operator/docs/quickstart.md b/hyperfleet-operator/docs/quickstart.md index b1e5b8d5..0e28fa81 100644 --- a/hyperfleet-operator/docs/quickstart.md +++ b/hyperfleet-operator/docs/quickstart.md @@ -73,12 +73,12 @@ helm install hyperfleet-operator charts/hyperfleet-operator \ ### Optional Values -| Value | Default | Description | -| ---------------------------- | ---------------------------------------------- | ---------------------------------------------------- | -| `image.repository` | `quay.io/cbusse_openshift/hyperfleet-operator` | Container image | -| `image.tag` | `latest` | Image tag | -| `serviceAccount.annotations` | `{}` | SA annotations (set IAM role ARN) | -| `replicaCount` | `1` | Number of replicas (= shard count) | +| Value | Default | Description | +| ---------------------------- | ---------------------------------------------- | ---------------------------------- | +| `image.repository` | `quay.io/cbusse_openshift/hyperfleet-operator` | Container image | +| `image.tag` | `latest` | Image tag | +| `serviceAccount.annotations` | `{}` | SA annotations (set IAM role ARN) | +| `replicaCount` | `1` | Number of replicas (= shard count) | ## 5. Create a ManagementCluster diff --git a/hyperfleet-operator/docs/sharding.md b/hyperfleet-operator/docs/sharding.md index c6973a76..c27c8717 100644 --- a/hyperfleet-operator/docs/sharding.md +++ b/hyperfleet-operator/docs/sharding.md @@ -53,12 +53,12 @@ my_shard = ordinal Example with 4 replicas: -| Pod | Shard | Reconciles namespaces where | -| ----- | ----- | --------------------------------- | -| Pod-0 | 0 | `abs(hashtext(namespace)::bigint) % 4 == 0` | -| Pod-1 | 1 | `abs(hashtext(namespace)::bigint) % 4 == 1` | -| Pod-2 | 2 | `abs(hashtext(namespace)::bigint) % 4 == 2` | -| Pod-3 | 3 | `abs(hashtext(namespace)::bigint) % 4 == 3` | +| Pod | Shard | Reconciles namespaces where | +| ----- | ----- | ------------------------------------------- | +| Pod-0 | 0 | `abs(hashtext(namespace)::bigint) % 4 == 0` | +| Pod-1 | 1 | `abs(hashtext(namespace)::bigint) % 4 == 1` | +| Pod-2 | 2 | `abs(hashtext(namespace)::bigint) % 4 == 2` | +| Pod-3 | 3 | `abs(hashtext(namespace)::bigint) % 4 == 3` | ### ManagementCluster Visibility