Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
725085f
ROSAENG-62606: port codegen tools into hack/api-codegen/
cdoan1 Jul 26, 2026
6b123a7
ROSAENG-62606: harden codegen generators and update Go to 1.26.5
cdoan1 Jul 26, 2026
3de2745
ROSAENG-62606: fix passthrough loader FieldPrefix and typeToString
cdoan1 Jul 26, 2026
aff9b78
ROSAENG-61801: add passthrough types with markers and runtime libraries
cdoan1 Jul 28, 2026
86920bb
ROSAENG-61801: fix codegen-passthrough to resolve source via go.mod
cdoan1 Jul 28, 2026
fc70161
ROSAENG-61801: sort imports in passthrough-gen for deterministic output
cdoan1 Jul 28, 2026
ed73f2f
ROSAENG-61801: regenerate deepcopy for new passthrough and envelope t…
cdoan1 Jul 28, 2026
18f1b26
ROSAENG-61801: tidy hack/api-codegen go.mod dependencies
cdoan1 Jul 28, 2026
4e7544f
ROSAENG-61801: single source of truth for field_metadata.json
cdoan1 Jul 28, 2026
e505a6e
ROSAENG-61801: gitignore generated passthrough raw files
cdoan1 Jul 28, 2026
f6cf272
ROSAENG-61801: remove tracked passthrough raw file
cdoan1 Jul 28, 2026
d2defd4
ROSAENG-61801: scan passthrough types as root entries in marker-scanner
cdoan1 Jul 28, 2026
564560e
ROSAENG-61801: fix GOCACHE permission in operator Containerfile
cdoan1 Jul 28, 2026
219b378
ROSAENG-61802: add field validation using codegen registry
cdoan1 Jul 28, 2026
02d7429
ROSAENG-61803: replace hardcoded service-set injection with codegen c…
cdoan1 Jul 28, 2026
529c6b3
ROSAENG-61805: align OpenAPI spec with codegen-generated schemas
cdoan1 Jul 28, 2026
4b677fd
ROSAENG-61805: add swagger-ui-serve target and link hostedCluster to …
cdoan1 Jul 29, 2026
ab144ec
ROSAENG-61805: remove no-op +k8s:openapi-gen=true markers and update …
cdoan1 Jul 29, 2026
a67d6f3
add rabbit fixes
cdoan1 Jul 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,9 @@ hyperfleet-operator/bin/
# Compiled operator binaries (built outside bin/)
hyperfleet-operator/manager
hyperfleet-operator/compactor

# Generated passthrough raw output (regenerate with make codegen-passthrough)
*.passthrough.go.raw

# Generated OpenAPI schemas (regenerate with make codegen-openapi)
platform-api/openapi/generated-schemas.json
100 changes: 93 additions & 7 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
.PHONY: help build test test-unit test-integration lint clean \
build-hyperfleet-db build-operator build-api \
test-hyperfleet-db test-operator test-operator-int test-api \
build-hyperfleet-db build-operator build-api build-api-codegen \
test-hyperfleet-db test-operator test-operator-int test-api test-api-codegen \
coverage-api-codegen \
test-e2e test-e2e-api test-e2e-cli test-e2e-platform-monitoring test-e2e-zoa test-e2e-authz \
e2e-authz-infra-up e2e-authz-infra-down e2e-init-db \
fmt vet verify deps \
manifests generate setup-envtest \
image-api image-operator image-push-api image-push-operator
codegen-passthrough codegen-registry codegen-verify codegen-openapi verify-openapi \
image-api image-operator image-push-api image-push-operator \
swagger-ui-serve swagger-ui-open

# ── Configuration ────────────────────────────────────────────────────────

Expand Down Expand Up @@ -57,17 +60,20 @@ help:
@echo " build-api Platform API server"
@echo " build-operator Hyperfleet operator (manager + compactor)"
@echo " build-hyperfleet-db Hyperfleet DB library"
@echo " build-api-codegen API codegen tools (build-time generators)"
@echo ""
@echo "Test:"
@echo " test All tests (unit + integration)"
@echo " test-unit Unit tests: API + operator (no external services)"
@echo " test-unit Unit tests: API + operator + codegen (no external services)"
@echo " test-integration Integration tests: FleetDB + operator (podman)"
@echo " test-e2e-authz E2E authz (starts local infra)"
@echo " test-e2e-api E2E API"
@echo " test-e2e-cli E2E CLI"
@echo " test-e2e-zoa E2E ZOA"
@echo " test-e2e-platform-monitoring E2E monitoring"
@echo ""
@echo " coverage-api-codegen Coverage report for codegen (hack/api-codegen)"
@echo ""
@echo "Code Quality:"
@echo " lint golangci-lint on all modules"
@echo " fmt Format Go source"
Expand All @@ -77,6 +83,11 @@ help:
@echo "Code Generation:"
@echo " manifests Generate CRD manifests"
@echo " generate Generate deepcopy methods"
@echo " codegen-passthrough Run passthrough-gen (raw + curated types)"
@echo " codegen-registry Run marker-scanner (field_metadata.go/.json)"
@echo " codegen-verify Verify codegen packages compile"
@echo " codegen-openapi Generate and merge OpenAPI schemas from Go types"
@echo " verify-openapi Verify openapi.yaml matches generated schemas"
@echo " setup-envtest Install envtest binaries (etcd, kube-apiserver)"
@echo " deps Download and tidy all modules"
@echo ""
Expand All @@ -86,7 +97,7 @@ help:

# ── Build ────────────────────────────────────────────────────────────────

build: build-hyperfleet-db build-operator build-api
build: build-hyperfleet-db build-operator build-api build-api-codegen

build-hyperfleet-db:
cd hyperfleet-db && go build ./...
Expand All @@ -98,17 +109,37 @@ build-operator:
build-api:
cd platform-api && go build -o ../bin/rosa-hyperfleet-api ./cmd

build-api-codegen:
cd hack/api-codegen && go build -o ../../bin/passthrough-gen ./cmd/passthrough-gen
cd hack/api-codegen && go build -o ../../bin/marker-scanner ./cmd/marker-scanner
cd hack/api-codegen && go build -o ../../bin/openapi-gen ./cmd/openapi-gen
cd hack/api-codegen && go build -o ../../bin/conversion-gen ./cmd/conversion-gen
cd hack/api-codegen && go build -o ../../bin/crd-variants ./cmd/crd-variants
cd hack/api-codegen && go build -o ../../bin/featuregate-info ./cmd/featuregate-info
cd hack/api-codegen && go build -o ../../bin/verify-configuration ./cmd/verify-configuration
cd hack/api-codegen && go build -o ../../bin/openapi-merge ./cmd/openapi-merge

# ── Test ─────────────────────────────────────────────────────────────────

test: test-unit test-integration

test-unit: test-api test-operator
test-unit: test-api test-operator test-api-codegen

test-integration: test-hyperfleet-db test-operator-int

test-api:
cd platform-api && go test -v -race -count=1 $$(go list ./... | grep -v '/test/e2e')

test-api-codegen:
cd hack/api-codegen && go test -v -race -count=1 ./...

coverage-api-codegen:
cd hack/api-codegen && go test -race -coverprofile=coverage.out ./...
cd hack/api-codegen && go tool cover -func=coverage.out
@echo ""
@echo "HTML report: hack/api-codegen/coverage.html"
cd hack/api-codegen && go tool cover -html=coverage.out -o coverage.html

test-operator: $(SETUP_ENVTEST)
@ASSETS=$$($(SETUP_ENVTEST) use -p path --bin-dir $(ENVTEST_BIN_DIR)) && \
echo "envtest assets: $$ASSETS" && \
Expand Down Expand Up @@ -170,16 +201,19 @@ fmt:
cd hyperfleet-db && go fmt ./...
cd hyperfleet-operator && go fmt ./...
cd platform-api && go fmt ./...
cd hack/api-codegen && go fmt ./...

vet:
cd hyperfleet-db && go vet ./...
cd hyperfleet-operator && go vet ./...
cd platform-api && go vet ./...
cd hack/api-codegen && go vet ./...

lint: $(GOLANGCI_LINT)
cd hyperfleet-db && $(GOLANGCI_LINT) run --config ../.golangci.yml --timeout 5m ./...
cd hyperfleet-operator && $(GOLANGCI_LINT) run --config ../.golangci.yml --timeout 5m ./...
cd platform-api && $(GOLANGCI_LINT) run --config ../.golangci.yml --timeout 5m ./...
cd hack/api-codegen && $(GOLANGCI_LINT) run --config ../../.golangci.yml --timeout 5m ./...

verify:
cd hyperfleet-db && go mod tidy
Expand All @@ -188,20 +222,23 @@ verify:
cd platform-api && go mod tidy
cd test && go mod tidy
cd hack/tools && go mod tidy
cd hack/api-codegen && go mod tidy
git diff --exit-code \
hyperfleet-db/go.mod hyperfleet-db/go.sum \
hyperfleet-operator/api/go.mod hyperfleet-operator/api/go.sum \
hyperfleet-operator/go.mod hyperfleet-operator/go.sum \
platform-api/go.mod platform-api/go.sum \
test/go.mod test/go.sum \
hack/tools/go.mod hack/tools/go.sum
hack/tools/go.mod hack/tools/go.sum \
hack/api-codegen/go.mod hack/api-codegen/go.sum

deps:
cd hyperfleet-db && go mod download && go mod tidy
cd hyperfleet-operator/api && go mod download && go mod tidy
cd hyperfleet-operator && go mod download && go mod tidy
cd platform-api && go mod download && go mod tidy
cd test && go mod download && go mod tidy
cd hack/api-codegen && go mod download && go mod tidy

# ── Code Generation ──────────────────────────────────────────────────────

Expand All @@ -216,6 +253,55 @@ ENVTEST_BIN_DIR ?= $(shell pwd)/.envtest
setup-envtest: $(SETUP_ENVTEST)
$(SETUP_ENVTEST) use --bin-dir $(ENVTEST_BIN_DIR)

# ── Codegen Pipeline ────────────────────────────────────────────────────

HYPERSHIFT_IMPORT_PATH ?= github.com/openshift/hypershift/api/hypershift/v1beta1
HYPERSHIFT_TYPES ?= HostedClusterSpec,NodePoolSpec
V1ALPHA1_DIR := hyperfleet-operator/api/v1alpha1
REGISTRY_DIR := platform-api/internal/codegen/registry

codegen-passthrough: build-api-codegen
./bin/passthrough-gen \
--source-dir=$$(cd hyperfleet-operator/api && go list -f '{{.Dir}}' $(HYPERSHIFT_IMPORT_PATH)) \
--types=$(HYPERSHIFT_TYPES) \
--output-dir=$(V1ALPHA1_DIR) \
--package=v1alpha1 \
--registry=$(REGISTRY_DIR)/field_metadata.json
rm -f $(V1ALPHA1_DIR)/zz_generated.passthrough.go

codegen-registry: build-api-codegen
./bin/marker-scanner \
--input-dirs=$(V1ALPHA1_DIR) \
--output-file=$(REGISTRY_DIR)/field_metadata.go

codegen-verify: build-api-codegen
cd hyperfleet-operator/api && go build ./...
cd platform-api && go build ./internal/codegen/...

codegen-openapi: build-api-codegen
./bin/openapi-gen \
--input-dirs=$(V1ALPHA1_DIR) \
--output-file=platform-api/openapi/generated-schemas.json
./bin/openapi-merge \
--generated=platform-api/openapi/generated-schemas.json \
--spec=platform-api/openapi/openapi.yaml

verify-openapi: codegen-openapi
@git diff --exit-code platform-api/openapi/openapi.yaml || \
(echo "openapi.yaml is out of date; run 'make codegen-openapi'" && exit 1)

swagger-ui-serve:
@command -v python3 >/dev/null 2>&1 || { echo "Error: python3 is required"; exit 1; }
@echo "Swagger UI: http://localhost:8080/platform-api/openapi/swagger-ui/"
@echo "OpenAPI spec: http://localhost:8080/platform-api/openapi/openapi.yaml"
@echo "Press Ctrl+C to stop"
@python3 -m http.server 8080 --directory .

swagger-ui-open:
@command -v open >/dev/null 2>&1 && open http://localhost:8080/platform-api/openapi/swagger-ui/ || \
command -v xdg-open >/dev/null 2>&1 && xdg-open http://localhost:8080/platform-api/openapi/swagger-ui/ || \
echo "Open http://localhost:8080/platform-api/openapi/swagger-ui/ in your browser"

# ── Images ───────────────────────────────────────────────────────────────

image-api:
Expand Down
90 changes: 90 additions & 0 deletions hack/api-codegen/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# api-codegen

![Coverage](https://img.shields.io/badge/coverage-31.3%25-yellow)

Build-time code generators for the ROSA Hyperfleet API. These tools scan Go types with markers and generate passthrough types, OpenAPI schemas, CRD variants, conversion functions, and field validation metadata.

## Generators

| Command | Purpose |
|---------|---------|
| `passthrough-gen` | Generate passthrough struct types from HyperShift API types with `+hyperfleet:` markers |
| `marker-scanner` | Extract marker metadata into `field_metadata.go` / `field_metadata.json` (field registry) |
| `openapi-gen` | Generate Swagger 2.0 JSON schemas from annotated Go types (respects `+k8s:openapi-gen=false`) |
| `openapi-merge` | Merge generated schemas into the hand-written `openapi.yaml` (Swagger 2.0 → OAS 3.0 conversion) |
| `conversion-gen` | Generate conversion functions between API versions |
| `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 |

## Codegen pipeline

The generators are chained in a dependency order. Each phase builds on the output of the previous one.

```text
Phase 0: Port codegen tools into monorepo (ROSAENG-62606)
Phase 1: passthrough-gen → marker-scanner (ROSAENG-61801)
Generates typed passthrough structs with markers,
then extracts field_metadata.json registry.
Phase 2: Field validation middleware (ROSAENG-61802)
Uses field_metadata.json to enforce write-mode
(mutable/immutable/service-set) and feature gates
on create/update requests.
Phase 3: Conversion functions (ROSAENG-61803)
Typed service-set injection and preservation functions
replace hardcoded field assignment in handlers.
Phase 5: OpenAPI alignment (ROSAENG-61805)
openapi-gen → openapi-merge pipeline generates typed
schemas for CRD types and merges them into openapi.yaml.
Phase 6: CI verification (ROSAENG-61806)
Wire codegen checks into CI.
```

## Makefile targets

```bash
# Build
make build-api-codegen # Build all 8 generator binaries
make test-api-codegen # Run tests
make coverage-api-codegen # Generate coverage report

# Codegen
make codegen-passthrough # Run passthrough-gen (typed passthrough structs)
make codegen-registry # Run marker-scanner (field_metadata.go/.json)
make codegen-openapi # Run openapi-gen + openapi-merge (update openapi.yaml)
make codegen-verify # Verify codegen packages compile

# Verification
make verify-openapi # Verify openapi.yaml matches generated schemas

# API docs
make swagger-ui-serve # Serve Swagger UI locally on port 8080 (requires Python 3)
make swagger-ui-open # Open Swagger UI in browser
```

## Key markers

| Marker | Purpose |
|--------|---------|
| `+hyperfleet:write-mode=mutable` | Field can be set on create and updated |
| `+hyperfleet:write-mode=immutable` | Field can be set on create but not changed |
| `+hyperfleet:write-mode=service-set` | Platform-managed field, rejected if customer sets it |
| `+k8s:openapi-gen=false` | Exclude field from generated OpenAPI schemas |
| `+openshift:enable:FeatureGate=X` | Field requires feature gate X to be enabled |

## How openapi-merge works

1. `openapi-gen` scans `hyperfleet-operator/api/v1alpha1` and emits Swagger 2.0 JSON with definitions for all visible types (fields marked `+k8s:openapi-gen=false` are excluded).
2. `openapi-merge` reads that JSON and:
- Converts `$ref` paths from `#/definitions/X` to `#/components/schemas/X`
- Strips `+hyperfleet:*` and `+kubebuilder:*` marker lines from descriptions
- Links `ClusterSpec.hostedCluster` to `HostedClusterSpecPassthrough`
- Inlines self-referential `$ref`s (e.g. `NodePoolSpec.nodePool`)
- Replaces 6 schema entries in `platform-api/openapi/openapi.yaml`: `ClusterSpec`, `NodePoolSpec`, `ClusterConfiguration`, `KubeletConfig`, `MachineConfigSpec`, `HostedClusterSpecPassthrough`
3. The pipeline is idempotent — running `make codegen-openapi` twice produces no diff.

## OpenAPI hybrid model

The `platform-api/openapi/openapi.yaml` uses a hybrid approach:
- **Codegen-owned**: CRD spec schemas (ClusterSpec, NodePoolSpec, sub-types) — generated from Go types with markers
- **Human-owned**: Routes, request/response envelopes, non-CRD schemas (Error, Cluster, NodePool wrapper types, etc.)
70 changes: 70 additions & 0 deletions hack/api-codegen/cmd/conversion-gen/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package main

import (
"flag"
"fmt"
"log"
"os"
"path/filepath"
"strings"

"github.com/openshift-online/rosa-hyperfleet-api/hack/api-codegen/pkg/conversion"
)

func main() {
var (
apiVersion string
crdPackage string
inputDirs string
outputDir string
)

flag.StringVar(&apiVersion, "api-version", "v1alpha1", "API version to generate for")
flag.StringVar(&crdPackage, "crd-package", "", "Import path to CRD types (required)")
flag.StringVar(&inputDirs, "input-dirs", "", "Comma-separated list of directories containing CRD source files (required)")
flag.StringVar(&outputDir, "output-dir", "", "Output directory for generated code (required)")
flag.Parse()

if crdPackage == "" || inputDirs == "" || outputDir == "" {
flag.Usage()
fmt.Fprintf(os.Stderr, "\nError: Missing required flags\n\n")
fmt.Fprintf(os.Stderr, "Example usage:\n")
fmt.Fprintf(os.Stderr, " %s \\\n", os.Args[0])
fmt.Fprintf(os.Stderr, " --api-version=v1alpha1 \\\n")
fmt.Fprintf(os.Stderr, " --crd-package=github.com/openshift-online/rosa-hyperfleet-api/hack/api-codegen/api/v1alpha1 \\\n")
fmt.Fprintf(os.Stderr, " --input-dirs=./api/v1alpha1 \\\n")
fmt.Fprintf(os.Stderr, " --output-dir=./pkg/conversion/v1alpha1\n")
os.Exit(1)
}

// Split input directories
dirs := strings.Split(inputDirs, ",")
for i, dir := range dirs {
// Convert to absolute path
absDir, err := filepath.Abs(dir)
if err != nil {
log.Fatalf("Failed to resolve directory %s: %v", dir, err)
}
dirs[i] = absDir
}

// Create generator
gen := conversion.NewGenerator(apiVersion, crdPackage, dirs, outputDir)

log.Printf("Conversion code generator")
log.Printf(" API Version: %s", apiVersion)
log.Printf(" CRD Package: %s", crdPackage)
log.Printf(" Input Dirs: %s", strings.Join(dirs, ", "))
log.Printf(" Output Dir: %s", outputDir)
log.Println()

// Generate
if err := gen.Generate(); err != nil {
log.Fatalf("Generation failed: %v", err)
}

log.Println("✓ Successfully generated:")
log.Println(" - REST types (rest/)")
log.Println(" - ServiceSetFields (../types.go)")
log.Println(" - Conversion functions (cluster.go, nodepool.go)")
}
Loading