From 93ec74ab5c2f84ae90e3568e17e3947c55d3d585 Mon Sep 17 00:00:00 2001 From: mliptak0 Date: Thu, 14 May 2026 13:48:01 +0200 Subject: [PATCH 1/2] HYPERFLEET-1033 - feat: consume OpenAPI schemas from hyperfleet-api-spec Go module Replace the committed openapi/openapi.yaml with a Go module dependency on github.com/openshift-hyperfleet/hyperfleet-api-spec. Code generation now extracts the core schema from the module cache via make generate; the file is no longer tracked in git. Runtime partner schema validation is preserved unchanged: operators supply --server-openapi-schema-path (or HYPERFLEET_SERVER_OPENAPI_SCHEMA_PATH) to point at a provider-specific openapi.yaml; validation is optional and non-fatal at startup. - go.mod: add hyperfleet-api-spec module dependency - Makefile: generate target extracts core/openapi.yaml from module cache - .gitignore: exclude openapi/openapi.yaml - routes.go: read schema from operator-supplied file path; warn and skip if missing/invalid (non-fatal) - config: add OpenAPISchemaPath field, --server-openapi-schema-path flag, HYPERFLEET_SERVER_OPENAPI_SCHEMA_PATH env binding (default: openapi/openapi.yaml) - logger: add FieldSchemaPath constant - validators: add NewSchemaValidatorFromData alongside NewSchemaValidator(path) - docs: add openapi/README.md as source of truth; deduplicate operator guide and architecture docs; update ADR, bill-of-artifacts, generated-code policy - tests: restore TestClusterSchemaValidationWithProviderSchema and integration_test.go schema path auto-setup Co-authored-by: Cursor --- .gitignore | 3 + AGENTS.md | 8 +- CHANGELOG.md | 1 + CLAUDE.md | 14 +- Dockerfile | 3 - Makefile | 8 +- README.md | 5 +- docs/api-operator-guide.md | 4 +- docs/deployment.md | 3 +- go.mod | 3 +- go.sum | 2 + openapi/CLAUDE.md | 39 +- openapi/README.md | 144 +++ openapi/openapi.yaml | 1795 ------------------------------------ pkg/logger/fields.go | 6 +- test/CLAUDE.md | 4 +- 16 files changed, 186 insertions(+), 1856 deletions(-) create mode 100644 openapi/README.md delete mode 100644 openapi/openapi.yaml diff --git a/.gitignore b/.gitignore index 2240c6ca..a571a42d 100755 --- a/.gitignore +++ b/.gitignore @@ -51,6 +51,9 @@ secrets /pkg/api/openapi/ /data/generated/ +# Ignore extracted OpenAPI spec (generated by make generate from hyperfleet-api-spec module) +/openapi/openapi.yaml + # Ignore generated mock files *_mock.go diff --git a/AGENTS.md b/AGENTS.md index 2909620c..a3efd77b 100755 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,7 +27,7 @@ make run-no-auth # Build, migrate, and run without auth ### Code Generation ``` -make generate # Regenerate Go models from openapi/openapi.yaml (oapi-codegen) +make generate # Extract schema from hyperfleet-api-spec module, then run oapi-codegen make generate-mocks # Regenerate mock implementations (go generate) make generate-all # Both of the above ``` @@ -91,7 +91,8 @@ plugins/ # Plugin registration (init-based) nodepools/plugin.go generic/plugin.go openapi/ - openapi.yaml # SOURCE spec (TypeSpec output, 32KB, uses $ref) + README.md # Schema import, code generation, and validation details + openapi.yaml # Not in git — generated by make generate oapi-codegen.yaml # Code generation config test/ integration/ # Integration tests (testcontainers) @@ -159,6 +160,5 @@ Create feature branches from `main`. PRs target `main`. - **Never set** `status.phase` manually — calculated from adapter conditions - **Never create** direct DB connections — use `SessionFactory.New(ctx)` for transaction participation - **FIPS required**: build with `CGO_ENABLED=1 GOEXPERIMENT=boringcrypto` -- **Spec source of truth**: `openapi/openapi.yaml` (TypeSpec output); generated spec at `pkg/api/openapi/api/openapi.yaml` is never edited -- **TypeSpec** definitions live in a separate `hyperfleet-api-spec` repository +- **Spec source of truth**: `hyperfleet-api-spec` Go module; update `go.mod` to change spec versions — see [openapi/README.md](openapi/README.md) for full details on schema import, code generation, and validation - **Tool versions** managed by Bingo (`.bingo/`) — don't manually install oapi-codegen or golangci-lint diff --git a/CHANGELOG.md b/CHANGELOG.md index efa48dc8..c4be0242 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- OpenAPI schema is now consumed from the `hyperfleet-api-spec` Go module (`v1.0.12`) for code generation; `openapi/openapi.yaml` is extracted during `make generate` and is no longer tracked in git ([#155](https://github.com/openshift-hyperfleet/hyperfleet-api/pull/155)) - Replaced OCM SDK authentication handler with standalone JWT middleware, removing `ocm-sdk-go` dependency and its transitive dependencies (`glog`, `bluemonday`, `json-iterator`) ([#120](https://github.com/openshift-hyperfleet/hyperfleet-api/pull/120)) - Upgraded JWT library from `golang-jwt/jwt/v4` to `golang-jwt/jwt/v5` ([#120](https://github.com/openshift-hyperfleet/hyperfleet-api/pull/120)) - Refactored `AdapterStatusDao.Upsert()` to accept a pre-fetched existing record, moving lookup and `LastTransitionTime` preservation logic to the service layer ([#119](https://github.com/openshift-hyperfleet/hyperfleet-api/pull/119)) diff --git a/CLAUDE.md b/CLAUDE.md index df9a1e4e..8079fa2a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,7 @@ HyperFleet API is a **stateless REST API** serving as the pure CRUD data layer f - **Language**: Go 1.24+ with FIPS crypto (`CGO_ENABLED=1 GOEXPERIMENT=boringcrypto`) - **Database**: PostgreSQL 14.2 with GORM ORM -- **API Spec**: TypeSpec → OpenAPI 3.0.3 → oapi-codegen → Go models +- **API Spec**: TypeSpec → `hyperfleet-api-spec` Go module → oapi-codegen → Go models - **Architecture**: Plugin-based route registration, transaction-per-request middleware ## Critical First Steps @@ -100,16 +100,10 @@ Each resource registers via `init()` function: - Transaction middleware creates GORM transactions for **write requests only** (POST/PUT/PATCH/DELETE): `pkg/db/transaction_middleware.go` - Read requests (GET) skip transaction creation for performance -- OpenAPI source spec: `openapi/openapi.yaml` (TypeSpec-generated, uses `$ref`) -- Generated code: `pkg/api/openapi/` (models + embedded spec) — **never edit** -- Codegen config: `openapi/oapi-codegen.yaml` — uses oapi-codegen (not openapi-generator-cli) +- OpenAPI spec and code generation: see [openapi/README.md](openapi/README.md) — run `make generate` before building; generated files in `pkg/api/openapi/` are **never edited** - Status aggregation: Service layer synthesizes `Available` and `Ready` conditions from adapter reports - Plugin-based: each resource type registers routes/services in `plugins/*/plugin.go` -Two `openapi.yaml` files exist: -- `openapi/openapi.yaml` — source (32KB, has `$ref`) -- `pkg/api/openapi/api/openapi.yaml` — generated (44KB, fully resolved, embedded in binary) - ## Boundaries - **Never edit** files in `pkg/api/openapi/` — they are generated by `make generate` @@ -117,7 +111,7 @@ Two `openapi.yaml` files exist: - **Never set** `status.phase` manually — it is calculated from adapter conditions - **Never create** direct DB connections — use `SessionFactory.New(ctx)` for transaction participation - **FIPS required**: always build with `CGO_ENABLED=1 GOEXPERIMENT=boringcrypto` -- **Spec source of truth**: `openapi/openapi.yaml` (TypeSpec output); don't modify generated spec +- **OpenAPI spec**: not tracked in git — see [openapi/README.md](openapi/README.md) for spec versioning and generation details ## Related CLAUDE.md Files @@ -131,4 +125,4 @@ Subdirectories contain context-specific guidance that loads when you work in tho - `plugins/CLAUDE.md` — Plugin registration (init-based) - `test/CLAUDE.md` — Test conventions, factories, and environment variables - `charts/CLAUDE.md` — Helm chart testing and configuration -- `openapi/CLAUDE.md` — OpenAPI spec, code generation, and oapi-codegen config +- `openapi/README.md` — OpenAPI schema import, code generation, schema validation, and oapi-codegen config diff --git a/Dockerfile b/Dockerfile index 4763f890..6a103e9f 100755 --- a/Dockerfile +++ b/Dockerfile @@ -43,9 +43,6 @@ WORKDIR /app # ubi9-micro doesn't include CA certificates; copy from builder for TLS (e.g. Google Pub/Sub) COPY --from=builder /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem COPY --from=builder /build/bin/hyperfleet-api /app/hyperfleet-api -COPY --from=builder /build/openapi/openapi.yaml /app/openapi/openapi.yaml - -ENV HYPERFLEET_SERVER_OPENAPI_SCHEMA_PATH=/app/openapi/openapi.yaml USER 65532:65532 diff --git a/Makefile b/Makefile index a340e369..0ceeab25 100755 --- a/Makefile +++ b/Makefile @@ -114,8 +114,11 @@ verify-migrations: ## Verify migration files follow project conventions .PHONY: generate generate: $(OAPI_CODEGEN) ## Generate OpenAPI types using oapi-codegen + $(GO) mod download rm -rf pkg/api/openapi - mkdir -p pkg/api/openapi + mkdir -p pkg/api/openapi openapi + @rm -f openapi/openapi.yaml + @cp "$$($(GO) list -m -f '{{.Dir}}' github.com/openshift-hyperfleet/hyperfleet-api-spec)/schemas/core/openapi.yaml" openapi/openapi.yaml $(OAPI_CODEGEN) --config openapi/oapi-codegen.yaml openapi/openapi.yaml .PHONY: generate-mocks @@ -159,7 +162,7 @@ run/docs: check-container-tool ## Run swagger and host the api spec @echo "Please open http://localhost:8081/" # Port 8081 instead of 80: ports <1024 are privileged and fail with rootless Podman. # Port 8080 is avoided since it's used by the health endpoint server. - $(CONTAINER_TOOL) run -d -p 8081:8080 -e SWAGGER_JSON=/hyperfleet.yaml -v $(PWD)/openapi/hyperfleet.yaml:/hyperfleet.yaml swaggerapi/swagger-ui + $(CONTAINER_TOOL) run -d -p 8081:8080 -e SWAGGER_JSON=/openapi.yaml -v $(PWD)/openapi/openapi.yaml:/openapi.yaml swaggerapi/swagger-ui .PHONY: cmds cmds: ## Build all binaries under cmd/ @@ -180,6 +183,7 @@ clean: ## Delete temporary generated files pkg/api/openapi \ data/generated/openapi/*.json \ secrets \ + openapi/openapi.yaml \ .PHONY: secrets secrets: ## Initialize secrets directory with default values diff --git a/README.md b/README.md index 341dee0f..2d371926 100755 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ HyperFleet API - Simple REST API for cluster lifecycle management. Provides CRUD - **Language**: Go 1.24+ - **API Definition**: OpenAPI 3.0 -- **Code Generation**: openapi-generator-cli +- **Code Generation**: oapi-codegen - **Database**: PostgreSQL with GORM ORM - **Container Runtime**: Podman - **Testing**: Gomega + Resty @@ -35,7 +35,7 @@ hyperfleet-api/ │ ├── db/ # Database setup and migrations │ ├── handlers/ # HTTP request handlers │ └── services/ # Business logic -├── openapi/ # API specification source +├── openapi/ # Generated artifacts from hyperfleet-api-spec module ├── test/ # Integration tests and factories ├── docs/ # Detailed documentation └── Makefile # Build automation @@ -171,6 +171,7 @@ This project uses [pre-commit](https://pre-commit.io/) for code quality checks. - **[Deployment](docs/deployment.md)** - Container images, Kubernetes deployment, and configuration - **[Authentication](docs/authentication.md)** - Development and production auth - **[Logging](docs/logging.md)** - Structured logging, OpenTelemetry integration, and data masking +- **[Partner Schema Validation](openapi/README.md#partner-schema-validation)** - How to supply a partner-specific OpenAPI schema for runtime `spec` field validation ### Additional Resources diff --git a/docs/api-operator-guide.md b/docs/api-operator-guide.md index 0be4f88b..55e714f6 100644 --- a/docs/api-operator-guide.md +++ b/docs/api-operator-guide.md @@ -738,6 +738,8 @@ The API uses a two-step process to validate specs: - **Schema missing or invalid**: API logs a warning and starts without validation. Specs are stored without schema validation. - Startup is **non-blocking** — missing or invalid schema files do not prevent API startup +For details on how schemas are imported for code generation and which schema components map to each resource type, see [openapi/README.md](../openapi/README.md) in this repository. + --- ## 4. Deployment Checklist @@ -1053,7 +1055,7 @@ This section provides a **quick reference** for common API-specific issues and t | **Pods stuck in init phase: `database does not exist`** | Database not created | Create database: `CREATE DATABASE hyperfleet;` | | **Pods stuck in init phase: `connection timeout`** | Database connection retry settings too low | Increase database connection retry settings using `--db-conn-retry-attempts` and `--db-conn-retry-interval` flags in the init container command | | **High API latency, slow responses** | Resource limits, database slow queries, or connection pool exhausted | Check metrics: `curl http://:9090/metrics \| grep hyperfleet_api_request_duration_seconds`. Check resources: `kubectl top pods -n hyperfleet-system`. Check slow queries: `kubectl logs -n hyperfleet-system deployment/hyperfleet-api \| grep "slow query"`. Resolution: Increase resource limits/replicas, add database indexes, or increase `--db-max-open-connections` (default: 50). | -| **400 Bad Request** | Resource spec doesn't match OpenAPI schema | Check loaded schema: `kubectl logs -n hyperfleet-system deployment/hyperfleet-api \| grep "OPENAPI_SCHEMA_PATH"`. Retrieve schema: `kubectl exec -n hyperfleet-system deployment/hyperfleet-api -- cat /etc/hyperfleet/schemas/openapi.yaml`. Validate and fix spec. | +| **400 Bad Request** | Resource spec doesn't match OpenAPI schema | Check the loaded schema path: `kubectl logs -n hyperfleet-system deployment/hyperfleet-api \| grep "schema_path"`. Retrieve and inspect the schema: `kubectl exec -n hyperfleet-system deployment/hyperfleet-api -- cat $HYPERFLEET_SERVER_OPENAPI_SCHEMA_PATH`. Validate and fix spec. | | **401 Unauthorized** | Missing or invalid JWT token | Verify authentication is enabled (`server.jwt.enabled=true`). If production, ensure valid JWT token is provided. Reference: [Authentication Guide](authentication.md). | | **404 Not Found** | Resource doesn't exist | Verify resource ID is correct. Check if resource was deleted: `curl http://:8000/api/hyperfleet/v1/clusters/$CLUSTER_ID`. | | **409 Conflict** | Concurrent update or generation mismatch | Retry with exponential backoff. Ensure only one controller updates the same resource. | diff --git a/docs/deployment.md b/docs/deployment.md index 76ce566c..1536e5be 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -145,8 +145,7 @@ HyperFleet API is configured via environment variables and configuration files. The API validates cluster and nodepool `spec` fields against an OpenAPI schema. This allows different providers (GCP, AWS, Azure) to have different spec structures. -- **Configuration:** `server.openapi_schema_path` (supports config file, env var, or CLI flag) -- **Default:** `openapi/openapi.yaml` (provider-agnostic base schema) +Schema validation is optional and file-path based. Set `--server-openapi-schema-path` (or `HYPERFLEET_SERVER_OPENAPI_SCHEMA_PATH`) to a provider-specific OpenAPI schema file to enable it. If the path is missing or the file is unreadable, the API logs a warning and starts without validation — startup is non-blocking. See [Configuration Guide](config.md) for all configuration options. diff --git a/go.mod b/go.mod index 5632cd5e..15c29307 100755 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( github.com/mendsley/gojwk v0.0.0-20141217222730-4d5ec6e58103 github.com/oapi-codegen/runtime v1.2.0 github.com/onsi/gomega v1.27.1 + github.com/openshift-hyperfleet/hyperfleet-api-spec v1.0.12 github.com/prometheus/client_golang v1.16.0 github.com/prometheus/client_model v0.3.0 github.com/spf13/cobra v1.8.1 @@ -44,6 +45,7 @@ require ( ) require ( + github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect go.opentelemetry.io/contrib/propagators/aws v1.43.0 // indirect go.opentelemetry.io/contrib/propagators/b3 v1.43.0 // indirect go.opentelemetry.io/contrib/propagators/jaeger v1.43.0 // indirect @@ -58,7 +60,6 @@ require ( github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/antlr/antlr4 v0.0.0-20190518164840-edae2a1c9b4b // indirect - github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect diff --git a/go.sum b/go.sum index 6db4c184..13669842 100755 --- a/go.sum +++ b/go.sum @@ -336,6 +336,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/openshift-hyperfleet/hyperfleet-api-spec v1.0.12 h1:Eo0LvsU2lz2fJiy9LNiGSIqiOhHMTINK7P70VR+hkMA= +github.com/openshift-hyperfleet/hyperfleet-api-spec v1.0.12/go.mod h1:KITzIAd8HcMpH5lXdHFjgk45dvL6XLpP3wwz8iK+KCI= github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= diff --git a/openapi/CLAUDE.md b/openapi/CLAUDE.md index 3fa4764e..922552ad 100644 --- a/openapi/CLAUDE.md +++ b/openapi/CLAUDE.md @@ -1,36 +1,17 @@ # Claude Code Guidelines for OpenAPI -## Two openapi.yaml Files +See [README.md](README.md) for the full reference on how schemas are imported, generated, and used for validation. -1. **Source**: `openapi.yaml` in this directory (32KB, uses `$ref`) — generated by TypeSpec from a separate `hyperfleet-api-spec` repo. This is the source of truth for this repository. -2. **Generated**: `pkg/api/openapi/api/openapi.yaml` (44KB, fully resolved) — produced by `make generate`, embedded in binary via `//go:embed` +## Key Rules -**Never edit either file directly.** The source comes from TypeSpec; the generated one is overwritten by `make generate`. +- **Never edit** `openapi.yaml` or `pkg/api/openapi/openapi.gen.go` — both are overwritten by `make generate`. +- **Never copy** the spec file manually — `make generate` extracts it from the Go module cache automatically. +- **To change the schema**, update TypeSpec in `hyperfleet-api-spec`, publish a new release, bump the version in `go.mod`, then run `make generate-all`. -## Code Generation +## Quick Commands -Tool: **oapi-codegen** (not openapi-generator-cli) -Config: `oapi-codegen.yaml` in this directory - -``` -make generate # Regenerates pkg/api/openapi/ from openapi/openapi.yaml -make generate-all # generate + generate-mocks +```shell +make generate # Extract schema from spec module, then run oapi-codegen +make generate-mocks # Regenerate mock implementations +make generate-all # Both of the above ``` - -Generation produces: -- `pkg/api/openapi/openapi.gen.go` — Go model structs + client + embedded spec - -## Config Details - -From `oapi-codegen.yaml`: -- Package: `openapi` -- Output: `pkg/api/openapi/openapi.gen.go` -- Generates: models (yes), chi-server (no), client (yes), embedded-spec (yes) -- Compatibility: `old-merge-schemas: true` (inlines allOf), `old-aliasing: true` (type defs not aliases) - -## Updating the API - -1. Update TypeSpec definitions in `hyperfleet-api-spec` repo -2. Copy generated `openapi.yaml` to this directory -3. Run `make generate-all` -4. Update handlers/services/DAOs for any new or changed fields diff --git a/openapi/README.md b/openapi/README.md new file mode 100644 index 00000000..cc94c5c3 --- /dev/null +++ b/openapi/README.md @@ -0,0 +1,144 @@ +# OpenAPI Schema — Source of Truth + +This directory contains the code-generation configuration for the HyperFleet API's OpenAPI layer. + +## Overview + +OpenAPI schemas are **not authored here**. They are defined in the [`hyperfleet-api-spec`](https://github.com/openshift-hyperfleet/hyperfleet-api-spec) repository (TypeSpec) and consumed by this repository as a Go module. The `openapi/openapi.yaml` file is extracted from the module cache at code-generation time and is **not tracked in git**. + +## Directory Contents + +| File | Purpose | +|------|---------| +| `oapi-codegen.yaml` | Code-generation config for `oapi-codegen` | +| `openapi.yaml` | **Not in git** — extracted from the Go module by `make generate` | + +## How Schemas Are Imported + +1. The `github.com/openshift-hyperfleet/hyperfleet-api-spec` module is declared in `go.mod`. +2. `make generate` locates the module's on-disk path via `go list -m -f '{{.Dir}}'` and copies `schemas/core/openapi.yaml` to `openapi/openapi.yaml`. Code generation always uses the `core` variant. +3. `oapi-codegen` reads `openapi/openapi.yaml` and produces `pkg/api/openapi/openapi.gen.go` — Go model structs, an HTTP client, and an embedded resolved spec. + +## Generated Artifacts + +| Artifact | Location | Description | +|----------|----------|-------------| +| Extracted spec | `openapi/openapi.yaml` | Copied from Go module; input to oapi-codegen | +| Go models + client | `pkg/api/openapi/openapi.gen.go` | Never edit — regenerate with `make generate` | +| Embedded resolved spec | Inside `openapi.gen.go` | Fully resolved; served at `/api/hyperfleet/v1/openapi` | + +**Never edit `openapi.yaml` or `openapi.gen.go` directly.** Both are overwritten by `make generate`. + +## Partner Schema Validation + +### Why this exists + +HyperFleet API is intentionally schema-agnostic at its core: it stores clusters and nodepools as long as the `spec` field is present and non-null, without caring what is inside it. This is by design — the API serves multiple partners with different provider-specific payloads. + +Partners, however, **do** care. A GCP partner might require a `region` field inside `spec`; an AWS partner might require an `instanceType`. Without validation, invalid or incomplete specs silently end up in the database and only fail later when a downstream component tries to use them. + +The `--server-openapi-schema-path` flag solves this: at deploy time, the operator points the API at a partner-specific OpenAPI schema file. The API then validates every `POST`/`PATCH` request's `spec` payload against that schema in HTTP middleware — before any service or database code runs. + +### What the schema file must contain + +The schema file must be a valid OpenAPI 3.0 document. The API looks up two specific component schemas by name: + +| Resource | Required component | +|----------|--------------------| +| `cluster` | `components.schemas.ClusterSpec` | +| `nodepool` | `components.schemas.NodePoolSpec` | + +A minimal example for a GCP partner: + +```yaml +openapi: 3.0.0 +info: + title: HyperFleet GCP Partner Schema + version: 1.0.0 +paths: {} +components: + schemas: + ClusterSpec: + type: object + required: + - region + properties: + region: + type: string + description: GCP region (e.g. us-central1) + zone: + type: string + NodePoolSpec: + type: object + required: + - machineType + - replicas + properties: + machineType: + type: string + replicas: + type: integer + minimum: 1 + maximum: 100 +``` + +If `ClusterSpec` or `NodePoolSpec` is absent from the file, the API will fail to load the validator and log a warning (startup remains non-blocking). + +### How to configure it + +Three equivalent ways to supply the path: + +| Method | Example | +|--------|---------| +| CLI flag | `--server-openapi-schema-path=/etc/hyperfleet/schemas/openapi.yaml` | +| Environment variable | `HYPERFLEET_SERVER_OPENAPI_SCHEMA_PATH=/etc/hyperfleet/schemas/openapi.yaml` | +| Config file | `server.openapi_schema_path: /etc/hyperfleet/schemas/openapi.yaml` | + +**Default:** `openapi/openapi.yaml` (the core schema extracted by `make generate` — provider-agnostic, accepts any non-null spec). + +### Runtime behaviour + +- Validation runs in HTTP middleware on every `POST` and `PATCH` request, before the service or database layer. +- Invalid specs return `400 Bad Request` with field-level error details. +- Startup is **non-blocking**: if the schema file is missing or malformed, the API logs a warning and starts without validation — specs are accepted without field-level checks. + +## Updating the API Schema + +1. Update TypeSpec definitions in the [`hyperfleet-api-spec`](https://github.com/openshift-hyperfleet/hyperfleet-api-spec) repository and publish a new release. + +2. Bump the module version in `go.mod`: + + ```shell + go get github.com/openshift-hyperfleet/hyperfleet-api-spec@vX.Y.Z + ``` + +3. Regenerate: + + ```shell + make generate-all + ``` + +4. Update handlers, services, and DAOs for any new or changed fields. + +For local development before a new spec version is published, add a `replace` directive in `go.mod`: + +```go +replace github.com/openshift-hyperfleet/hyperfleet-api-spec => /path/to/local/hyperfleet-api-spec +``` + +## Code Generation Commands + +```shell +make generate # Extract schema from spec module, then run oapi-codegen +make generate-mocks # Regenerate mock implementations (go generate) +make generate-all # Both of the above +``` + +## oapi-codegen Configuration + +From `oapi-codegen.yaml`: + +- **Package**: `openapi` +- **Output**: `pkg/api/openapi/openapi.gen.go` +- **Generates**: models, HTTP client, embedded spec (chi-server disabled) +- **Compatibility flags**: `old-merge-schemas: true` (inlines `allOf`), `old-aliasing: true` (type definitions, not aliases) diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml deleted file mode 100644 index 0a5a8488..00000000 --- a/openapi/openapi.yaml +++ /dev/null @@ -1,1795 +0,0 @@ -openapi: 3.0.0 -info: - title: HyperFleet API - version: 1.0.12 - contact: - name: HyperFleet Team - license: - name: Apache 2.0 - url: https://www.apache.org/licenses/LICENSE-2.0 - description: |- - HyperFleet API provides simple CRUD operations for managing cluster resources and their status history. - - **Architecture**: Simple CRUD only, no business logic, no event creation. - Sentinel operator handles all orchestration logic. - Adapters handle the specifics of managing spec -tags: - - name: Clusters - - name: Cluster statuses - - name: NodePool statuses - - name: NodePools -paths: - /api/hyperfleet/v1/clusters: - get: - operationId: getClusters - summary: List clusters - description: Returns a list of all clusters. - parameters: - - $ref: '#/components/parameters/SearchParams' - - $ref: '#/components/parameters/QueryParams.page' - - $ref: '#/components/parameters/QueryParams.pageSize' - - $ref: '#/components/parameters/QueryParams.orderBy' - - $ref: '#/components/parameters/QueryParams.order' - responses: - '200': - description: The request has succeeded. - content: - application/json: - schema: - $ref: '#/components/schemas/ClusterList' - '400': - description: The server could not understand the request due to invalid syntax. - default: - description: An unexpected error response. - content: - application/problem+json: - schema: - $ref: '#/components/schemas/Error' - tags: - - Clusters - security: - - BearerAuth: [] - post: - operationId: postCluster - summary: Create cluster - description: |- - Create a new cluster resource. - - **Note**: The `status` object in the response is read-only and computed by the service. - It is NOT part of the request body. Initially, - status.conditions will include mandatory "LastKnownReconciled", "Ready" and "Reconciled" conditions. - parameters: [] - responses: - '201': - description: The request has succeeded and a new resource has been created as a result. - content: - application/json: - schema: - $ref: '#/components/schemas/Cluster' - '400': - description: The server could not understand the request due to invalid syntax. - default: - description: An unexpected error response. - content: - application/problem+json: - schema: - $ref: '#/components/schemas/Error' - tags: - - Clusters - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/ClusterCreateRequest' - security: - - BearerAuth: [] - /api/hyperfleet/v1/clusters/{cluster_id}: - get: - operationId: getClusterById - summary: Get cluster by ID - description: Returns a single cluster by its ID. - parameters: - - $ref: '#/components/parameters/SearchParams' - - name: cluster_id - in: path - required: true - schema: - type: string - responses: - '200': - description: The request has succeeded. - content: - application/json: - schema: - $ref: '#/components/schemas/Cluster' - '400': - description: The server could not understand the request due to invalid syntax. - default: - description: An unexpected error response. - content: - application/problem+json: - schema: - $ref: '#/components/schemas/Error' - tags: - - Clusters - security: - - BearerAuth: [] - patch: - operationId: patchClusterById - summary: Patch cluster by ID - description: Patch a specific cluster by ID - parameters: - - name: cluster_id - in: path - required: true - description: Cluster ID - schema: - type: string - responses: - '200': - description: The request has succeeded. - content: - application/json: - schema: - $ref: '#/components/schemas/Cluster' - '400': - description: The server could not understand the request due to invalid syntax. - '404': - description: The server cannot find the requested resource. - '409': - description: The request conflicts with the current state of the server. - default: - description: An unexpected error response. - content: - application/problem+json: - schema: - $ref: '#/components/schemas/Error' - tags: - - Clusters - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/ClusterPatchRequest' - security: - - BearerAuth: [] - delete: - operationId: deleteClusterById - summary: Request cluster deletion - description: |- - Marks the cluster for deletion by setting deleted_time to the current time, cascades to its nodepools. - The cluster remains in the database until it is fully deleted. - Returns the updated cluster with generation incremented. - parameters: - - name: cluster_id - in: path - required: true - schema: - type: string - responses: - '202': - description: The request has been accepted for processing, but processing has not yet completed. - content: - application/json: - schema: - $ref: '#/components/schemas/Cluster' - example: - kind: Cluster - id: 019466a0-8f8e-7abc-9def-0123456789ab - href: https://api.hyperfleet.com/v1/clusters/019466a0-8f8e-7abc-9def-0123456789ab - name: cluster-123 - labels: - environment: production - team: platform - spec: {} - generation: 2 - status: - conditions: - - type: Ready - status: 'True' - reason: ReconciledAll - message: All required adapters reported Available=True or Finalized=True at the current generation - observed_generation: 2 - created_time: '2021-01-01T10:00:00Z' - last_updated_time: '2021-01-01T10:00:00Z' - last_transition_time: '2021-01-01T10:00:00Z' - - type: Reconciled - status: 'True' - reason: ReconciledAll - message: All required adapters reported Available=True or Finalized=True at the current generation - observed_generation: 2 - created_time: '2021-01-01T10:00:00Z' - last_updated_time: '2021-01-01T10:00:00Z' - last_transition_time: '2021-01-01T10:00:00Z' - - type: LastKnownReconciled - status: 'True' - reason: AllAdaptersReconciled - message: All required adapters report Available=True for the tracked generation - observed_generation: 2 - created_time: '2021-01-01T10:00:00Z' - last_updated_time: '2021-01-01T10:00:00Z' - last_transition_time: '2021-01-01T10:00:00Z' - - type: Adapter1Successful - status: 'True' - reason: This adapter1 is available - message: This adapter1 is available - observed_generation: 2 - created_time: '2021-01-01T10:00:00Z' - last_updated_time: '2021-01-01T10:00:00Z' - last_transition_time: '2021-01-01T10:00:00Z' - - type: Adapter2Successful - status: 'True' - reason: This adapter2 is available - message: This adapter2 is available - observed_generation: 2 - created_time: '2021-01-01T10:01:00Z' - last_updated_time: '2021-01-01T10:01:00Z' - last_transition_time: '2021-01-01T10:01:00Z' - created_time: '2021-01-01T00:00:00Z' - updated_time: '2021-01-01T10:02:00Z' - deleted_time: '2021-01-01T10:05:00Z' - created_by: user-123@example.com - updated_by: user-123@example.com - deleted_by: user-123@example.com - '400': - description: The server could not understand the request due to invalid syntax. - '404': - description: The server cannot find the requested resource. - default: - description: An unexpected error response. - content: - application/problem+json: - schema: - $ref: '#/components/schemas/Error' - tags: - - Clusters - security: - - BearerAuth: [] - /api/hyperfleet/v1/clusters/{cluster_id}/nodepools: - get: - operationId: getNodePoolsByClusterId - summary: List all nodepools for cluster - description: Returns the list of all nodepools for a cluster - parameters: - - name: cluster_id - in: path - required: true - description: Cluster ID - schema: - type: string - - $ref: '#/components/parameters/SearchParams' - - $ref: '#/components/parameters/QueryParams.page' - - $ref: '#/components/parameters/QueryParams.pageSize' - - $ref: '#/components/parameters/QueryParams.orderBy' - - $ref: '#/components/parameters/QueryParams.order' - responses: - '200': - description: The request has succeeded. - content: - application/json: - schema: - $ref: '#/components/schemas/NodePoolList' - '400': - description: The server could not understand the request due to invalid syntax. - default: - description: An unexpected error response. - content: - application/problem+json: - schema: - $ref: '#/components/schemas/Error' - tags: - - NodePools - security: - - BearerAuth: [] - post: - operationId: createNodePool - summary: Create nodepool - description: Create a NodePool for a cluster - parameters: - - name: cluster_id - in: path - required: true - description: Cluster ID - schema: - type: string - responses: - '201': - description: The request has succeeded and a new resource has been created as a result. - content: - application/json: - schema: - $ref: '#/components/schemas/NodePoolCreateResponse' - '400': - description: The server could not understand the request due to invalid syntax. - '409': - description: The request conflicts with the current state of the server. - default: - description: An unexpected error response. - content: - application/problem+json: - schema: - $ref: '#/components/schemas/Error' - tags: - - NodePools - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/NodePoolCreateRequest' - security: - - BearerAuth: [] - /api/hyperfleet/v1/clusters/{cluster_id}/nodepools/{nodepool_id}: - get: - operationId: getNodePoolById - summary: Get nodepool by ID - description: Returns specific nodepool - parameters: - - name: cluster_id - in: path - required: true - description: Cluster ID - schema: - type: string - - name: nodepool_id - in: path - required: true - description: NodePool ID - schema: - type: string - responses: - '200': - description: The request has succeeded. - content: - application/json: - schema: - $ref: '#/components/schemas/NodePool' - '400': - description: The server could not understand the request due to invalid syntax. - default: - description: An unexpected error response. - content: - application/problem+json: - schema: - $ref: '#/components/schemas/Error' - tags: - - NodePools - security: - - BearerAuth: [] - delete: - operationId: deleteNodePoolById - summary: Request nodepool deletion - description: |- - Marks the nodepool for deletion by setting deleted_time and deleted_by. Does not affect the parent cluster. - Returns the updated nodepool with generation incremented. - parameters: - - name: cluster_id - in: path - required: true - description: Cluster ID - schema: - type: string - - name: nodepool_id - in: path - required: true - description: NodePool ID - schema: - type: string - responses: - '202': - description: The request has been accepted for processing, but processing has not yet completed. - content: - application/json: - schema: - $ref: '#/components/schemas/NodePool' - example: - kind: NodePool - id: 019466a1-2b3c-7def-8abc-456789abcdef - href: https://api.hyperfleet.com/v1/nodepools/019466a1-2b3c-7def-8abc-456789abcdef - name: worker-pool-1 - labels: - environment: production - pooltype: worker - spec: {} - generation: 2 - owner_references: - id: 019466a0-8f8e-7abc-9def-0123456789ab - kind: Cluster - href: https://api.hyperfleet.com/v1/clusters/019466a0-8f8e-7abc-9def-0123456789ab - status: - conditions: - - type: Ready - status: 'True' - reason: ReconciledAll - message: All required adapters reported Available=True or Finalized=True at the current generation - observed_generation: 2 - created_time: '2021-01-01T10:00:00Z' - last_updated_time: '2021-01-01T10:00:00Z' - last_transition_time: '2021-01-01T10:00:00Z' - - type: Reconciled - status: 'True' - reason: ReconciledAll - message: All required adapters reported Available=True or Finalized=True at the current generation - observed_generation: 2 - created_time: '2021-01-01T10:00:00Z' - last_updated_time: '2021-01-01T10:00:00Z' - last_transition_time: '2021-01-01T10:00:00Z' - - type: LastKnownReconciled - status: 'True' - reason: AllAdaptersReconciled - message: All required adapters report Available=True for the tracked generation - observed_generation: 2 - created_time: '2021-01-01T10:00:00Z' - last_updated_time: '2021-01-01T10:00:00Z' - last_transition_time: '2021-01-01T10:00:00Z' - - type: Adapter1Successful - status: 'True' - reason: This adapter1 is available - message: This adapter1 is available - observed_generation: 2 - created_time: '2021-01-01T10:00:00Z' - last_updated_time: '2021-01-01T10:00:00Z' - last_transition_time: '2021-01-01T10:00:00Z' - - type: Adapter2Successful - status: 'True' - reason: This adapter2 is available - message: This adapter2 is available - observed_generation: 2 - created_time: '2021-01-01T10:01:00Z' - last_updated_time: '2021-01-01T10:01:00Z' - last_transition_time: '2021-01-01T10:01:00Z' - created_time: '2021-01-01T00:00:00Z' - updated_time: '2021-01-01T10:02:00Z' - deleted_time: '2021-01-01T10:05:00Z' - created_by: user-123@example.com - updated_by: user-123@example.com - deleted_by: user-123@example.com - '400': - description: The server could not understand the request due to invalid syntax. - '404': - description: The server cannot find the requested resource. - default: - description: An unexpected error response. - content: - application/problem+json: - schema: - $ref: '#/components/schemas/Error' - tags: - - NodePools - security: - - BearerAuth: [] - patch: - operationId: patchNodePoolById - summary: Patch nodepool by ID - description: Patch a specific nodepool within a cluster - parameters: - - name: cluster_id - in: path - required: true - description: Cluster ID - schema: - type: string - - name: nodepool_id - in: path - required: true - description: NodePool ID - schema: - type: string - responses: - '200': - description: The request has succeeded. - content: - application/json: - schema: - $ref: '#/components/schemas/NodePool' - '400': - description: The server could not understand the request due to invalid syntax. - '404': - description: The server cannot find the requested resource. - '409': - description: The request conflicts with the current state of the server. - default: - description: An unexpected error response. - content: - application/problem+json: - schema: - $ref: '#/components/schemas/Error' - tags: - - NodePools - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/NodePoolPatchRequest' - security: - - BearerAuth: [] - /api/hyperfleet/v1/clusters/{cluster_id}/nodepools/{nodepool_id}/statuses: - post: - operationId: postNodePoolStatuses - summary: Create or update adapter status - description: |- - Adapter creates or updates its status report for this nodepool. - If adapter already has a status, it will be updated (upsert by adapter name). - - Response includes the full adapter status with all conditions. - Adapter should call this endpoint every time it evaluates the nodepool. - parameters: - - name: cluster_id - in: path - required: true - description: Cluster ID - schema: - type: string - - name: nodepool_id - in: path - required: true - description: Nodepool ID - schema: - type: string - responses: - '201': - description: The request has succeeded and a new resource has been created as a result. - content: - application/json: - schema: - $ref: '#/components/schemas/AdapterStatus' - '400': - description: The server could not understand the request due to invalid syntax. - '404': - description: The server cannot find the requested resource. - '409': - description: The request conflicts with the current state of the server. - tags: - - NodePool statuses - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/AdapterStatusCreateRequest' - security: - - BearerAuth: [] - put: - operationId: putNodePoolStatuses - summary: Adapter creates or updates resource nodepool status - description: Idempotent upsert of an adapter status for this nodepool. Use instead of POST when the adapter name is known upfront. - parameters: - - name: cluster_id - in: path - required: true - description: Cluster ID - schema: - type: string - - name: nodepool_id - in: path - required: true - description: Nodepool ID - schema: - type: string - responses: - '201': - description: The request has succeeded and a new resource has been created as a result. - content: - application/json: - schema: - $ref: '#/components/schemas/AdapterStatus' - '400': - description: The server could not understand the request due to invalid syntax. - '404': - description: The server cannot find the requested resource. - '409': - description: The request conflicts with the current state of the server. - tags: - - NodePool statuses - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/AdapterStatusCreateRequest' - security: - - BearerAuth: [] - get: - operationId: getNodePoolsStatuses - summary: List all adapter statuses for nodepools - description: Returns adapter status reports for this nodepool - parameters: - - name: cluster_id - in: path - required: true - description: Cluster ID - schema: - type: string - - name: nodepool_id - in: path - required: true - schema: - type: string - - $ref: '#/components/parameters/SearchParams' - - $ref: '#/components/parameters/QueryParams.page' - - $ref: '#/components/parameters/QueryParams.pageSize' - - $ref: '#/components/parameters/QueryParams.orderBy' - - $ref: '#/components/parameters/QueryParams.order' - responses: - '200': - description: The request has succeeded. - content: - application/json: - schema: - $ref: '#/components/schemas/AdapterStatusList' - '400': - description: The server could not understand the request due to invalid syntax. - default: - description: An unexpected error response. - content: - application/problem+json: - schema: - $ref: '#/components/schemas/Error' - tags: - - NodePool statuses - /api/hyperfleet/v1/clusters/{cluster_id}/statuses: - post: - operationId: postClusterStatuses - summary: Create or update adapter status - description: |- - Adapter creates or updates its status report for this cluster. - If adapter already has a status, it will be updated (upsert by adapter name). - - Response includes the full adapter status with all conditions. - Adapter should call this endpoint every time it evaluates the cluster. - parameters: - - name: cluster_id - in: path - required: true - description: Cluster ID - schema: - type: string - responses: - '201': - description: The request has succeeded and a new resource has been created as a result. - content: - application/json: - schema: - $ref: '#/components/schemas/AdapterStatus' - '400': - description: The server could not understand the request due to invalid syntax. - '404': - description: The server cannot find the requested resource. - '409': - description: The request conflicts with the current state of the server. - tags: - - Cluster statuses - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/AdapterStatusCreateRequest' - security: - - BearerAuth: [] - put: - operationId: putClusterStatuses - summary: Adapter creates or updates resource cluster status - description: Idempotent upsert of an adapter status for this cluster. Use instead of POST when the adapter name is known upfront. - parameters: - - name: cluster_id - in: path - required: true - description: Cluster ID - schema: - type: string - responses: - '201': - description: The request has succeeded and a new resource has been created as a result. - content: - application/json: - schema: - $ref: '#/components/schemas/AdapterStatus' - '400': - description: The server could not understand the request due to invalid syntax. - '404': - description: The server cannot find the requested resource. - '409': - description: The request conflicts with the current state of the server. - tags: - - Cluster statuses - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/AdapterStatusCreateRequest' - security: - - BearerAuth: [] - get: - operationId: getClusterStatuses - summary: List all adapter statuses for cluster - description: Returns adapter status reports for this cluster - parameters: - - name: cluster_id - in: path - required: true - description: Cluster ID - schema: - type: string - - $ref: '#/components/parameters/SearchParams' - - $ref: '#/components/parameters/QueryParams.page' - - $ref: '#/components/parameters/QueryParams.pageSize' - - $ref: '#/components/parameters/QueryParams.orderBy' - - $ref: '#/components/parameters/QueryParams.order' - responses: - '200': - description: The request has succeeded. - content: - application/json: - schema: - $ref: '#/components/schemas/AdapterStatusList' - '400': - description: The server could not understand the request due to invalid syntax. - '404': - description: The server cannot find the requested resource. - tags: - - Cluster statuses - security: - - BearerAuth: [] - /api/hyperfleet/v1/nodepools: - get: - operationId: getNodePools - summary: List all nodepools for cluster - description: Returns the list of all nodepools - parameters: - - $ref: '#/components/parameters/SearchParams' - - $ref: '#/components/parameters/QueryParams.page' - - $ref: '#/components/parameters/QueryParams.pageSize' - - $ref: '#/components/parameters/QueryParams.orderBy' - - $ref: '#/components/parameters/QueryParams.order' - responses: - '200': - description: The request has succeeded. - content: - application/json: - schema: - $ref: '#/components/schemas/NodePoolList' - '400': - description: The server could not understand the request due to invalid syntax. - default: - description: An unexpected error response. - content: - application/problem+json: - schema: - $ref: '#/components/schemas/Error' - tags: - - NodePools - security: - - BearerAuth: [] -components: - parameters: - QueryParams.order: - name: order - in: query - required: false - schema: - $ref: '#/components/schemas/OrderDirection' - explode: false - QueryParams.orderBy: - name: orderBy - in: query - required: false - schema: - type: string - default: created_time - explode: false - QueryParams.page: - name: page - in: query - required: false - schema: - type: integer - format: int32 - default: 1 - explode: false - QueryParams.pageSize: - name: pageSize - in: query - required: false - schema: - type: integer - format: int32 - default: 20 - explode: false - SearchParams: - name: search - in: query - required: false - description: |- - Filter results using TSL (Tree Search Language) query syntax. - Examples: `status.conditions.Ready='True'`, `name in ('c1','c2')`, `labels.region='us-east'` - schema: - type: string - explode: false - schemas: - AdapterCondition: - type: object - required: - - type - - last_transition_time - - status - properties: - type: - type: string - description: Condition type - reason: - type: string - description: Machine-readable reason code - message: - type: string - description: Human-readable message - last_transition_time: - type: string - format: date-time - description: |- - When this condition last transitioned status (API-managed) - Only updated when status changes (True/False), not when reason/message changes - status: - $ref: '#/components/schemas/AdapterConditionStatus' - description: |- - Condition in AdapterStatus - Used for standard Kubernetes condition types: "Available", "Applied", "Health", "Finalized" - Note: observed_generation is at AdapterStatus level, not per-condition, - since all conditions in one AdapterStatus share the same observed generation - AdapterConditionStatus: - type: string - enum: - - 'True' - - 'False' - - Unknown - description: Status value for adapter conditions - AdapterStatus: - type: object - required: - - adapter - - observed_generation - - conditions - - created_time - - last_report_time - properties: - adapter: - type: string - description: Adapter name (e.g., "validator", "dns", "provisioner") - observed_generation: - type: integer - format: int32 - description: Which generation of the resource this status reflects - metadata: - type: object - properties: - job_name: - type: string - job_namespace: - type: string - attempt: - type: integer - format: int32 - started_time: - type: string - format: date-time - completed_time: - type: string - format: date-time - duration: - type: string - description: Job execution metadata - data: - type: object - additionalProperties: {} - description: Adapter-specific data (structure varies by adapter type) - conditions: - type: array - items: - $ref: '#/components/schemas/AdapterCondition' - description: |- - Kubernetes-style conditions tracking adapter state - Typically includes: Available, Applied, Health, Finalized - created_time: - type: string - format: date-time - description: When this adapter status was first created (API-managed) - last_report_time: - type: string - format: date-time - description: |- - When this adapter last reported its status (API-managed) - Updated every time the adapter POSTs, even if conditions haven't changed - Used by Sentinel to detect adapter liveness - description: |- - AdapterStatus represents the complete status report from an adapter - Contains multiple conditions, job metadata, and adapter-specific data - example: - adapter: adapter1 - observed_generation: 1 - conditions: - - type: Available - status: 'True' - reason: This adapter1 is available - message: This adapter1 is available - last_transition_time: '2021-01-01T10:00:00Z' - - type: Applied - status: 'True' - reason: Validation job applied - message: Adapter1 validation job applied successfully - last_transition_time: '2021-01-01T10:00:00Z' - - type: Health - status: 'True' - reason: All adapter1 operations completed successfully - message: All adapter1 runtime operations completed successfully - last_transition_time: '2021-01-01T10:00:00Z' - - type: Finalized - status: 'True' - reason: All resources deleted; cleanup confirmed - message: All resources deleted; cleanup confirmed - last_transition_time: '2021-01-01T10:00:00Z' - metadata: - job_name: validator-job-abc123 - job_namespace: hyperfleet-system - attempt: 1 - started_time: '2021-01-01T10:00:00Z' - completed_time: '2021-01-01T10:02:00Z' - duration: 2m - data: - validation_results: - total_tests: 30 - passed: 30 - failed: 0 - created_time: '2021-01-01T10:00:00Z' - last_report_time: '2021-01-01T10:02:00Z' - AdapterStatusCreateRequest: - type: object - required: - - adapter - - observed_generation - - observed_time - - conditions - properties: - adapter: - type: string - description: Adapter name (e.g., "validator", "dns", "provisioner") - observed_generation: - type: integer - format: int32 - description: Which generation of the resource this status reflects - metadata: - type: object - properties: - job_name: - type: string - job_namespace: - type: string - attempt: - type: integer - format: int32 - started_time: - type: string - format: date-time - completed_time: - type: string - format: date-time - duration: - type: string - description: Job execution metadata - data: - type: object - additionalProperties: {} - description: Adapter-specific data (structure varies by adapter type) - observed_time: - type: string - format: date-time - description: |- - When the adapter observed this resource state - API will use this to set AdapterStatus.last_report_time - conditions: - type: array - items: - $ref: '#/components/schemas/ConditionRequest' - description: Request payload for creating/updating adapter status - example: - adapter: validator - observed_generation: 1 - observed_time: '2021-01-01T10:00:00Z' - conditions: - - type: Available - status: 'True' - reason: This adapter1 is available - message: This adapter1 is available - - type: Applied - status: 'True' - reason: Validation job applied - message: Adapter1 validation job applied successfully - - type: Health - status: 'True' - reason: All adapter1 operations completed successfully - message: All adapter1 runtime operations completed successfully - - type: Finalized - status: 'True' - reason: All resources deleted; cleanup confirmed - message: All resources deleted; cleanup confirmed - metadata: - job_name: validator-job-abc123 - job_namespace: hyperfleet-system - attempt: 1 - started_time: '2021-01-01T10:00:00Z' - completed_time: '2021-01-01T10:02:00Z' - duration: 2m - data: - validation_results: - total_tests: 30 - passed: 30 - failed: 0 - AdapterStatusList: - type: object - required: - - kind - - page - - size - - total - - items - properties: - kind: - type: string - page: - type: integer - format: int32 - size: - type: integer - format: int32 - total: - type: integer - format: int32 - items: - type: array - items: - $ref: '#/components/schemas/AdapterStatus' - description: List of adapter statuses with pagination metadata - example: - kind: AdapterStatusList - page: 1 - size: 2 - total: 2 - items: - - adapter: adapter1 - observed_generation: 1 - conditions: - - type: Available - status: 'True' - reason: This adapter1 is available - message: This adapter1 is available - last_transition_time: '2021-01-01T10:00:00Z' - metadata: - job_name: validator-job-abc123 - duration: 2m - created_time: '2021-01-01T10:00:00Z' - last_report_time: '2021-01-01T10:02:00Z' - - adapter: adapter2 - observed_generation: 1 - conditions: - - type: Available - status: 'True' - reason: This adapter2 is available - message: This adapter2 is available - last_transition_time: '2021-01-01T10:01:00Z' - created_time: '2021-01-01T10:01:00Z' - last_report_time: '2021-01-01T10:01:30Z' - BearerAuth: - type: object - required: - - type - - scheme - properties: - type: - type: string - enum: - - http - scheme: - type: string - enum: - - bearer - Cluster: - type: object - required: - - name - - spec - - created_time - - updated_time - - created_by - - updated_by - - generation - - status - properties: - id: - type: string - description: Resource identifier - kind: - type: string - description: Resource kind - href: - type: string - description: Resource URI - labels: - type: object - additionalProperties: - type: string - description: labels for the API resource as pairs of name:value strings - name: - type: string - minLength: 3 - maxLength: 53 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ - description: Cluster name (unique) - spec: - $ref: '#/components/schemas/ClusterSpec' - created_time: - type: string - format: date-time - description: Timestamp when the resource was created - updated_time: - type: string - format: date-time - description: Timestamp when the resource was last updated - created_by: - type: string - format: email - description: Identity that created the resource - updated_by: - type: string - format: email - description: Identity that last updated the resource - deleted_time: - type: string - format: date-time - description: Timestamp when deletion was requested; omitted if not marked for deletion - deleted_by: - type: string - format: email - description: Identity that requested deletion; omitted if not marked for deletion - generation: - type: integer - format: int32 - minimum: 1 - description: Generation field is updated on customer updates, reflecting the version of the "intent" of the customer - status: - $ref: '#/components/schemas/ClusterStatus' - example: - kind: Cluster - id: 019466a0-8f8e-7abc-9def-0123456789ab - href: https://api.hyperfleet.com/v1/clusters/019466a0-8f8e-7abc-9def-0123456789ab - name: cluster-123 - labels: - environment: production - team: platform - spec: {} - generation: 1 - status: - conditions: - - type: Ready - status: 'True' - reason: ReconciledAll - message: All required adapters reported Available=True or Finalized=True at the current generation - observed_generation: 1 - created_time: '2021-01-01T10:00:00Z' - last_updated_time: '2021-01-01T10:00:00Z' - last_transition_time: '2021-01-01T10:00:00Z' - - type: Reconciled - status: 'True' - reason: ReconciledAll - message: All required adapters reported Available=True or Finalized=True at the current generation - observed_generation: 1 - created_time: '2021-01-01T10:00:00Z' - last_updated_time: '2021-01-01T10:00:00Z' - last_transition_time: '2021-01-01T10:00:00Z' - - type: LastKnownReconciled - status: 'True' - reason: AllAdaptersReconciled - message: All required adapters report Available=True for the tracked generation - observed_generation: 1 - created_time: '2021-01-01T10:00:00Z' - last_updated_time: '2021-01-01T10:00:00Z' - last_transition_time: '2021-01-01T10:00:00Z' - - type: Adapter1Successful - status: 'True' - reason: This adapter1 is available - message: This adapter1 is available - observed_generation: 1 - created_time: '2021-01-01T10:00:00Z' - last_updated_time: '2021-01-01T10:00:00Z' - last_transition_time: '2021-01-01T10:00:00Z' - - type: Adapter2Successful - status: 'True' - reason: This adapter2 is available - message: This adapter2 is available - observed_generation: 1 - created_time: '2021-01-01T10:01:00Z' - last_updated_time: '2021-01-01T10:01:00Z' - last_transition_time: '2021-01-01T10:01:00Z' - created_time: '2021-01-01T00:00:00Z' - updated_time: '2021-01-01T10:02:00Z' - created_by: user-123@example.com - updated_by: user-123@example.com - ClusterCreateRequest: - type: object - required: - - name - - spec - properties: - id: - type: string - description: Resource identifier - kind: - type: string - description: Resource kind - href: - type: string - description: Resource URI - labels: - type: object - additionalProperties: - type: string - description: labels for the API resource as pairs of name:value strings - name: - type: string - minLength: 3 - maxLength: 53 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ - description: Cluster name (unique) - spec: - $ref: '#/components/schemas/ClusterSpec' - example: - kind: Cluster - name: cluster-123 - labels: - environment: production - team: platform - spec: {} - ClusterList: - type: object - required: - - kind - - page - - size - - total - - items - properties: - kind: - type: string - page: - type: integer - format: int32 - size: - type: integer - format: int32 - total: - type: integer - format: int32 - items: - type: array - items: - $ref: '#/components/schemas/Cluster' - ClusterPatchRequest: - type: object - properties: - spec: - $ref: '#/components/schemas/ClusterSpec' - labels: - type: object - additionalProperties: - type: string - example: - spec: {} - labels: - env: staging - additionalProperties: false - minProperties: 1 - ClusterSpec: - type: object - description: |- - Core cluster specification. - Accepts any properties as the spec is provider-agnostic. - This is represented as a simple object to allow flexibility. - ClusterStatus: - type: object - required: - - conditions - properties: - conditions: - type: array - items: - $ref: '#/components/schemas/ResourceCondition' - minItems: 2 - description: |- - List of status conditions for the cluster. - - **Mandatory conditions**: - - `type: "Ready"` *(deprecated — use Reconciled)*: Whether all adapters report successfully at the current generation. - - `type: "Reconciled"`: Whether the resource's desired state has been fully reconciled by all adapters at the current generation. - - `type: "LastKnownReconciled"`: Sticky cross-generation condition — stays True as long as all required adapters were reconciled at a common observed generation, even if a newer generation is being processed. - - These conditions are present immediately upon resource creation. - description: |- - Cluster status computed from all status conditions. - - This object is computed by the service and CANNOT be modified directly. - It is aggregated from condition updates posted to `/clusters/{id}/statuses`. - - Provides quick overview of all reported conditions. - ConditionRequest: - type: object - required: - - type - - status - properties: - type: - type: string - status: - $ref: '#/components/schemas/AdapterConditionStatus' - reason: - type: string - message: - type: string - description: |- - Condition data for create/update requests (from adapters) - observed_generation and observed_time are now at AdapterStatusCreateRequest level - Error: - type: object - required: - - type - - title - - status - properties: - type: - type: string - format: uri - description: URI reference identifying the problem type - example: https://api.hyperfleet.io/errors/validation-error - title: - type: string - description: Short human-readable summary of the problem - example: Validation Failed - status: - type: integer - description: HTTP status code - example: 400 - detail: - type: string - description: Human-readable explanation specific to this occurrence - example: The cluster name field is required - instance: - type: string - format: uri-reference - description: URI reference for this specific occurrence - example: /api/hyperfleet/v1/clusters - code: - type: string - description: Machine-readable error code in HYPERFLEET-CAT-NUM format - example: HYPERFLEET-VAL-001 - timestamp: - type: string - format: date-time - description: RFC3339 timestamp of when the error occurred - example: '2024-01-15T10:30:00Z' - trace_id: - type: string - description: Distributed trace ID for correlation - example: abc123def456 - errors: - type: array - items: - $ref: '#/components/schemas/ValidationError' - description: Field-level validation errors (for validation failures) - description: RFC 9457 Problem Details error format with HyperFleet extensions - NodePool: - type: object - required: - - name - - spec - - created_time - - updated_time - - created_by - - updated_by - - generation - - owner_references - - status - properties: - id: - type: string - description: Resource identifier - kind: - type: string - description: Resource kind - href: - type: string - description: Resource URI - labels: - type: object - additionalProperties: - type: string - description: labels for the API resource as pairs of name:value strings - name: - type: string - minLength: 3 - maxLength: 15 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ - description: NodePool name (unique in a cluster) - spec: - $ref: '#/components/schemas/NodePoolSpec' - created_time: - type: string - format: date-time - description: Timestamp when the resource was created - updated_time: - type: string - format: date-time - description: Timestamp when the resource was last updated - created_by: - type: string - format: email - description: Identity that created the resource - updated_by: - type: string - format: email - description: Identity that last updated the resource - deleted_time: - type: string - format: date-time - description: Timestamp when deletion was requested; omitted if not marked for deletion - deleted_by: - type: string - format: email - description: Identity that requested deletion; omitted if not marked for deletion - generation: - type: integer - format: int32 - minimum: 1 - description: Generation field is updated on customer updates, reflecting the version of the "intent" of the customer - owner_references: - $ref: '#/components/schemas/ObjectReference' - status: - $ref: '#/components/schemas/NodePoolStatus' - example: - kind: NodePool - id: 019466a1-2b3c-7def-8abc-456789abcdef - href: https://api.hyperfleet.com/v1/nodepools/019466a1-2b3c-7def-8abc-456789abcdef - name: worker-pool-1 - labels: - environment: production - pooltype: worker - spec: {} - generation: 1 - owner_references: - id: 019466a0-8f8e-7abc-9def-0123456789ab - kind: Cluster - href: https://api.hyperfleet.com/v1/clusters/019466a0-8f8e-7abc-9def-0123456789ab - status: - conditions: - - type: Ready - status: 'True' - reason: ReconciledAll - message: All required adapters reported Available=True or Finalized=True at the current generation - observed_generation: 1 - created_time: '2021-01-01T10:00:00Z' - last_updated_time: '2021-01-01T10:00:00Z' - last_transition_time: '2021-01-01T10:00:00Z' - - type: Reconciled - status: 'True' - reason: ReconciledAll - message: All required adapters reported Available=True or Finalized=True at the current generation - observed_generation: 1 - created_time: '2021-01-01T10:00:00Z' - last_updated_time: '2021-01-01T10:00:00Z' - last_transition_time: '2021-01-01T10:00:00Z' - - type: LastKnownReconciled - status: 'True' - reason: AllAdaptersReconciled - message: All required adapters report Available=True for the tracked generation - observed_generation: 1 - created_time: '2021-01-01T10:00:00Z' - last_updated_time: '2021-01-01T10:00:00Z' - last_transition_time: '2021-01-01T10:00:00Z' - - type: Adapter1Successful - status: 'True' - reason: This adapter1 is available - message: This adapter1 is available - observed_generation: 1 - created_time: '2021-01-01T10:00:00Z' - last_updated_time: '2021-01-01T10:00:00Z' - last_transition_time: '2021-01-01T10:00:00Z' - - type: Adapter2Successful - status: 'True' - reason: This adapter2 is available - message: This adapter2 is available - observed_generation: 1 - created_time: '2021-01-01T10:01:00Z' - last_updated_time: '2021-01-01T10:01:00Z' - last_transition_time: '2021-01-01T10:01:00Z' - created_time: '2021-01-01T00:00:00Z' - updated_time: '2021-01-01T10:02:00Z' - created_by: user-123@example.com - updated_by: user-123@example.com - NodePoolCreateRequest: - type: object - required: - - name - - spec - properties: - id: - type: string - description: Resource identifier - kind: - type: string - description: Resource kind - href: - type: string - description: Resource URI - labels: - type: object - additionalProperties: - type: string - description: labels for the API resource as pairs of name:value strings - name: - type: string - minLength: 3 - maxLength: 15 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ - description: NodePool name (unique in a cluster) - spec: - $ref: '#/components/schemas/NodePoolSpec' - example: - name: worker-pool-1 - labels: - environment: production - pooltype: worker - spec: {} - NodePoolCreateResponse: - type: object - required: - - name - - spec - - created_time - - updated_time - - created_by - - updated_by - - generation - - owner_references - - status - properties: - id: - type: string - description: Resource identifier - kind: - type: string - description: Resource kind - href: - type: string - description: Resource URI - labels: - type: object - additionalProperties: - type: string - description: labels for the API resource as pairs of name:value strings - name: - type: string - minLength: 3 - maxLength: 15 - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ - description: NodePool name (unique in a cluster) - spec: - $ref: '#/components/schemas/NodePoolSpec' - created_time: - type: string - format: date-time - description: Timestamp when the resource was created - updated_time: - type: string - format: date-time - description: Timestamp when the resource was last updated - created_by: - type: string - format: email - description: Identity that created the resource - updated_by: - type: string - format: email - description: Identity that last updated the resource - deleted_time: - type: string - format: date-time - description: Timestamp when deletion was requested; omitted if not marked for deletion - deleted_by: - type: string - format: email - description: Identity that requested deletion; omitted if not marked for deletion - generation: - type: integer - format: int32 - minimum: 1 - description: Generation field is updated on customer updates, reflecting the version of the "intent" of the customer - owner_references: - $ref: '#/components/schemas/ObjectReference' - status: - $ref: '#/components/schemas/NodePoolStatus' - NodePoolList: - type: object - required: - - kind - - page - - size - - total - - items - properties: - kind: - type: string - page: - type: integer - format: int32 - size: - type: integer - format: int32 - total: - type: integer - format: int32 - items: - type: array - items: - $ref: '#/components/schemas/NodePool' - NodePoolPatchRequest: - type: object - properties: - spec: - $ref: '#/components/schemas/NodePoolSpec' - labels: - type: object - additionalProperties: - type: string - example: - spec: {} - labels: - env: staging - additionalProperties: false - minProperties: 1 - NodePoolSpec: - type: object - description: |- - Core nodepool specification. - Accepts any properties as the spec is provider-agnostic. - This is represented as a simple object to allow flexibility. - NodePoolStatus: - type: object - required: - - conditions - properties: - conditions: - type: array - items: - $ref: '#/components/schemas/ResourceCondition' - minItems: 2 - description: |- - List of status conditions for the nodepool. - - **Mandatory conditions**: - - `type: "Ready"` *(deprecated — use Reconciled)*: Whether all adapters report successfully at the current generation. - - `type: "Reconciled"`: Whether the resource's desired state has been fully reconciled by all adapters at the current generation. - - `type: "LastKnownReconciled"`: Sticky cross-generation condition — stays True as long as all required adapters were reconciled at a common observed generation, even if a newer generation is being processed. - - These conditions are present immediately upon resource creation. - description: |- - NodePool status computed from all status conditions. - - This object is computed by the service and CANNOT be modified directly. - ObjectReference: - type: object - properties: - id: - type: string - description: Resource identifier - kind: - type: string - description: Resource kind - href: - type: string - description: Resource URI - OrderDirection: - type: string - enum: - - asc - - desc - ResourceCondition: - type: object - required: - - type - - last_transition_time - - status - - observed_generation - - created_time - - last_updated_time - properties: - type: - type: string - description: Condition type - reason: - type: string - description: Machine-readable reason code - message: - type: string - description: Human-readable message - last_transition_time: - type: string - format: date-time - description: |- - When this condition last transitioned status (API-managed) - Only updated when status changes (True/False), not when reason/message changes - status: - $ref: '#/components/schemas/ResourceConditionStatus' - observed_generation: - type: integer - format: int32 - description: Generation of the spec that this condition reflects - created_time: - type: string - format: date-time - description: When this condition was first created (API-managed) - last_updated_time: - type: string - format: date-time - description: |- - When the corresponding adapter last reported (API-managed) - Updated every time the adapter POSTs, even if condition status hasn't changed - Copied from AdapterStatus.last_report_time - description: |- - Condition in Cluster/NodePool status - Used for semantic condition types: "ValidationSuccessful", "DNSSuccessful", "NodePoolSuccessful", etc. - Includes observed_generation and last_updated_time to track adapter-specific state - ResourceConditionStatus: - type: string - enum: - - 'True' - - 'False' - description: Status value for resource conditions - ValidationError: - type: object - required: - - field - - message - properties: - field: - type: string - description: JSON path to the field that failed validation - example: spec.name - value: - description: The invalid value that was provided (if safe to include) - constraint: - type: string - enum: - - required - - min - - max - - min_length - - max_length - - pattern - - enum - - format - - unique - description: The validation constraint that was violated - example: required - message: - type: string - description: Human-readable error message for this field - example: Cluster name is required - description: Field-level validation error detail - securitySchemes: - BearerAuth: - type: http - scheme: bearer -servers: - - url: https://hyperfleet.redhat.com - description: Production - variables: {} diff --git a/pkg/logger/fields.go b/pkg/logger/fields.go index ea75dec3..7012a9ac 100644 --- a/pkg/logger/fields.go +++ b/pkg/logger/fields.go @@ -15,6 +15,7 @@ const ( FieldLogLevel = "level" FieldLogFormat = "format" FieldLogOutput = "output" + FieldSchemaPath = "schema_path" ) // Resource related fields @@ -49,11 +50,6 @@ const ( FieldServiceVersion = "service_version" ) -// Schema related fields -const ( - FieldSchemaPath = "schema_path" -) - // Generic fields const ( FieldAdapter = "adapter" diff --git a/test/CLAUDE.md b/test/CLAUDE.md index c40fe287..fab8c522 100644 --- a/test/CLAUDE.md +++ b/test/CLAUDE.md @@ -30,7 +30,7 @@ Reference: `factories/` `integration/integration_test.go` — `TestMain(m *testing.M)`: - Sets default adapter env vars (`HYPERFLEET_ADAPTERS_REQUIRED_CLUSTER`, `HYPERFLEET_ADAPTERS_REQUIRED_NODEPOOL`) -- Sets `HYPERFLEET_SERVER_OPENAPI_SCHEMA_PATH` via `runtime.Caller` +- Schema validation: `TestMain` auto-sets `HYPERFLEET_SERVER_OPENAPI_SCHEMA_PATH` to `openapi/openapi.yaml` in the repo root if not already set; override with a provider schema path to run provider-specific tests - 45-second timeout safeguard for CI (prevents hung Prow jobs) ## Key Environment Variables @@ -38,4 +38,4 @@ Reference: `factories/` - `HYPERFLEET_ENV` — selects config environment: `unit_testing`, `integration_testing`, `development` - `TESTCONTAINERS_RYUK_DISABLED=true` — required for testcontainers in CI - `HYPERFLEET_ADAPTERS_REQUIRED_CLUSTER` / `HYPERFLEET_ADAPTERS_REQUIRED_NODEPOOL` — adapter lists for tests -- `HYPERFLEET_SERVER_OPENAPI_SCHEMA_PATH` — OpenAPI schema path for spec validation +- `HYPERFLEET_SERVER_OPENAPI_SCHEMA_PATH` — path to OpenAPI schema for spec validation (auto-set by `TestMain` to `openapi/openapi.yaml`; override with a provider schema to run `TestClusterSchemaValidationWithProviderSchema`) From 97a02f0781f3cd94ce890adb58946bd644744d97 Mon Sep 17 00:00:00 2001 From: Martin Liptak Date: Fri, 15 May 2026 11:00:16 +0200 Subject: [PATCH 2/2] HYPERFLEET-1033 - fix: revert fields.go --- pkg/logger/fields.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/logger/fields.go b/pkg/logger/fields.go index 7012a9ac..ea75dec3 100644 --- a/pkg/logger/fields.go +++ b/pkg/logger/fields.go @@ -15,7 +15,6 @@ const ( FieldLogLevel = "level" FieldLogFormat = "format" FieldLogOutput = "output" - FieldSchemaPath = "schema_path" ) // Resource related fields @@ -50,6 +49,11 @@ const ( FieldServiceVersion = "service_version" ) +// Schema related fields +const ( + FieldSchemaPath = "schema_path" +) + // Generic fields const ( FieldAdapter = "adapter"