Add the initial repospawner service #1
Reference in New Issue
Block a user
Delete Branch "benvin/initial-service"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Why
New repositories in the estate come from
terraform-git'sconfig/git.unkin.net/unkin/repository/<name>.yamltree, so every new repomeans hand-writing that YAML, opening a PR, waiting for the merge, then
remembering to activate the repo in Woodpecker. repospawner does that from a
JSON request (or a small web form) without moving the review gate: the merge
approval stays exactly where it is today.
What this adds
secureHeadersCSP, oauth2-proxy group gate re-checked server-side).POST /api/requestsvalidates the request (DNS-ish name, description,non-empty status checks), checks the name against terraform-git's contents
API, and launches a Kubernetes Job.
GET /api/requests[/{id}]feeds the UItable;
/livezand/readyz(k8s API reachable) for the probes.mediamoverstyle:job pr(branch, write the YAML, open the PR),job watch(poll the PR tomerge or close, 7d deadline),
job woodpecker-enable(forge repo id ->POST /api/repos?forge_remote_id=, then verify). Each reports a JSON resulton the pod termination message.
creates the next Job. It doubles as startup recovery: state is rebuilt from
Job labels and annotations, so the deployment is one replica,
Recreate.internal/vaultauth: native Vault kubernetes login with the projectedaudience: vaultSA token, then a short-lived Gitea credential read per use.test,pre-commit(gofmt/vet + golangci-lint +uvx pre-commit),build(dry-run image),docker(push onv*), all onserviceAccountName: repospawner-ci.prerequisites.
Notes on two spec points
"Return a pull request URL on success." Opening the PR needs a Vault-minted
token and several forge round-trips, so it happens in the Job, not in the
request handler.
POSTtherefore answers202with{id, status_url}, and thePR URL appears on
GET /api/requests/{id}(and in the UI table) as soon as thePR Job reports it. Same information, without holding the HTTP request open on a
forge call that can take seconds.
No
agentprin-cluster.agentprauthenticates to Vault with an AppRolewhose CIDR binding excludes pod addresses, so the PR Job would fail there.
repospawner logs in through the kubernetes auth mount with its projected SA
token instead, mints per operation, never logs a token, and re-mints on a
401(the watch Job outlives the ~1h credential).
Deviation
The PR Job writes the config through Gitea's branch + contents API rather than
cloning and pushing with git. The runtime image is distroless static, which has
no git binary or shell; the API path is a single atomic commit with no working
copy to manage, and an existing branch is tolerated so a retried Job can finish
what its predecessor started.
Checks
gofmt,go vet,golangci-lint run(0 issues),uvx pre-commit run --all-filesandgo test -raceall clean locally; 11 packages with testscovering validation, YAML golden output, the Gitea/Vault/Woodpecker clients over
httptest(including the 401 re-mint), Job spec construction, terminationmessage parsing, the state machine, state reconstruction from Job lists, the
group middleware and the handler contracts.
CI needs the
repospawner-ciServiceAccount, which is in flight in argocd-apps.Review findings addressed in
8786636:--checkflag per context instead of a comma-joined--checks, so nothing can split a context in two;Validate()also rejects commas outright. Round-trip test (jobs.PRargv ->parsePRArgs) covers separator-bearing contexts.woodpecker token unavailableafter 30 reconcile passes (~5min) and the reason is served on the request;Advanceleaves a terminal request alone so it sticks. Old skip test split into wait / fail / recover-when-token-returns.HasActiveNamethroughstore.Put; 8 concurrent POSTs for one name now yield exactly one 202 (verified failing without the lock).maxlength.gofmt/vet/golangci-lint clean,
go test -race ./...green, pre-commit passes.