Skip to main content
Version: 1.0

GitOps Scaffolder

GitOps Scaffolder turns a fresh-cluster request from Host Cluster Controller's GeneratingGitOpsTree phase into a real, customer-specific Flux tree — cert-manager, ingress-nginx, storage classes, PostgreSQL, and every platform service, all pointed at the customer's own domain, none of it copied from clusters/development/'s own real credentials. It's a plain chi + bun + goose HTTP service, structured identically to Cluster Provisioner — deliberately not a controller-runtime operator, for the same reason: a real generate-and-push run can take several minutes, the wrong shape for a reconcile loop's cheap-per-pass assumptions.

Architecture: one Job per generation run​

One Kubernetes Job per request, in kubeopera-provisioning (reused from Cluster Provisioner — both are "ops-tooling Jobs producing infrastructure for a HostCluster" in the same sense). The Job runs scaffold-runner, a separate binary built into the same container image as tools/generate-gitops-env (a two-binary image, the same pattern Host Cluster Controller uses for its own controller + eks-token-helper).

POST /api/v1/scaffolds
│ persist a scaffold_jobs row (status=queued)
▼
StartJob() creates batchv1.Job "scaffold-<id>" in
│ kubeopera-provisioning, running scaffold-runner
▼
scaffold-runner (inside the Job):
1. git clone the target repo (platform default or customer-supplied)
2. generate-gitops-env -infra-only -- no secrets sealed yet, see below
3. commit + push
4. poll the target's own sealed-secrets Deployment until Available
5. fetch its public cert via the Kubernetes API's service-proxy
subresource (no port-forward)
6. generate-gitops-env (full pass) -- Postgres/RabbitMQ/registry
secrets sealed against that real cert
7. commit + push
▲
│ polled by GetStatus
│
host-cluster-controller

Why two passes, not one​

clusters/development/'s own sealed-secrets HelmRelease lives inside infrastructure/ — the same layer the generated tree's secrets need a running sealed-secrets controller to encrypt against. A single-pass generate-and-push would need to seal credentials before the very release that decrypts them has ever been applied. Splitting into two passes — infrastructure-only first, then everything else once sealed-secrets is confirmed Available on the real target — is what actually makes this work; tools/generate-gitops-env's own -infra-only flag exists specifically to support this split, and genuinely seals nothing when set (confirmed: registering pull secrets, Postgres, and RabbitMQ credentials are all deferred to the second pass).

The generated tree reflects this same split structurally: flux-system/gotk-sync.yaml's infrastructure Kustomization has no dependsOn; apps, database, and cert-manager-config each dependsOn: [infrastructure] — Flux itself enforces the same ordering after generation that scaffold-runner enforced while generating.

What gets generated​

See Setup Guide § Creating a new environment for the full picture. In short: clusters/development/'s infrastructure layer copied verbatim (it's byte-identical across every existing environment already), external-dns repointed at the new domain/region, one ingress-hostname patch per service (a JSON6902 patch over ../../base, never an edit to the shared base itself), and fresh, sealed Postgres/RabbitMQ/registry-pull credentials — never the real plaintext values clusters/development/ itself still carries.

API​

All routes below /api/v1 are behind a shared X-Api-Key — trusted-caller-only, the same convention as cluster-provisioner/build-service.

MethodPathPurpose
POST/api/v1/scaffoldsStart a generation run. Returns 202 with the new job immediately.
GET/api/v1/scaffolds/{id}Poll status/message/gitops_path. Sync-on-read, the same pattern ProvisionService.GetStatus already established.
GET/api/v1/scaffolds/{id}/logsReal Job pod logs — a plain synchronous full-text fetch, matching cluster-provisioner's identical choice.

Deployment​

Single-replica Deployment in kubeopera-core. scaffold-runner's image is a separate two-binary build (gitops-iac/tools/scaffold-runner + tools/generate-gitops-env), pinned to an exact tag by hand in scaffold_job.go, the same hand-pinned-constant convention cluster-provisioner uses for its own terraform-runner image.

kubectl logs -n kubeopera-core deploy/gitops-scaffolder -f
kubectl get jobs -n kubeopera-provisioning
kubectl logs -n kubeopera-provisioning job/scaffold-<id> -f

Health check at GET /health.