Kubernetes operator for managing Forgejo webhooks
  • Go 94.9%
  • Makefile 3.2%
  • Shell 1%
  • Dockerfile 0.9%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
pipeline-release-prepare 229a34dc44
All checks were successful
pipeline/ci CI green v0.6.27
release: v0.6.27 (auto-release)
2026-07-24 09:38:52 +00:00
.devcontainer Initial Forgejo operator implementation 2026-04-01 22:21:17 +02:00
api/v1alpha1 fix(repository): enforce one-shot import contract 2026-07-24 09:24:56 +02:00
cmd Merge branch 'trunk' of https://code.sean.farm/sean/forgejo-operator into feat/reduce-forgejo-polling 2026-07-07 21:11:52 +02:00
config fix(repository): enforce one-shot import contract 2026-07-24 09:24:56 +02:00
hack Initial Forgejo operator implementation 2026-04-01 22:21:17 +02:00
internal/controller test(repository): exercise real cold-seed lifecycle 2026-07-24 11:12:40 +02:00
test Initial Forgejo operator implementation 2026-04-01 22:21:17 +02:00
.custom-gcl.yml Initial Forgejo operator implementation 2026-04-01 22:21:17 +02:00
.dockerignore Fix 10 more plot holes: API checks, pagination, passwords, resources 2026-04-18 11:19:41 +02:00
.gitattributes fix(cicd): auto-resolve CHANGELOG land conflicts via .gitattributes merge=union 2026-07-05 12:14:37 +00:00
.gitignore Fix deferred issues: workflow config, status logging, RBAC, MirrorID, tests 2026-04-18 11:01:40 +02:00
.golangci.yml Initial Forgejo operator implementation 2026-04-01 22:21:17 +02:00
AGENTS.md Initial Forgejo operator implementation 2026-04-01 22:21:17 +02:00
CHANGELOG.md release: v0.6.27 (auto-release) 2026-07-24 09:38:52 +00:00
ci-test.Dockerfile fix(ci): pin golang:1.26 builder base by digest 2026-07-05 13:19:43 +02:00
Dockerfile fix(ci): pin golang:1.26 builder base by digest 2026-07-05 13:19:43 +02:00
go.mod fix(security): upgrade reachable vulnerable dependencies 2026-07-24 09:24:57 +02:00
go.sum fix(security): upgrade reachable vulnerable dependencies 2026-07-24 09:24:57 +02:00
Makefile test(repository): exercise real cold-seed lifecycle 2026-07-24 11:12:40 +02:00
operator.mk chore(scaffolding): adopt operator.mk + httpkit/secret helpers (lib v0.4.12) 2026-07-17 12:27:52 +02:00
PROJECT feat(crd): ForgejoTagProtection + ForgejoCollaborator + merge-whitelist 2026-07-07 00:15:14 +02:00
README.md chore(release): v0.6.2 — README rewrite, drop scaffolded Actions CI 2026-06-11 14:01:23 +02:00
VERSION release: v0.6.27 (auto-release) 2026-07-24 09:38:52 +00:00

forgejo-operator

A Kubernetes operator that manages Forgejo (a self-hosted Gitea fork) declaratively. Each piece of Forgejo state — a repository, an organization, a webhook, a push-mirror — is expressed as a Kubernetes custom resource; the operator reconciles the live Forgejo instance (over its Gitea-compatible API) toward that desired state and keeps it there. It is one of four seanfarm operators that share a single reconcile harness, and the one the harness was originally extracted from.

Custom resources

All kinds live in API group forgejo.forgejo.io, version v1alpha1, and are namespaced. Each CR references a Forgejo instance (forgejoRef, defaulting to forgejo-http.forgejo:3000 in-cluster) and an admin-token Secret used to authenticate to Forgejo. (Most kinds name that Secret authSecretRef; ForgejoPushMirror names it forgejoAuthSecretRef and reserves authSecretRef for the remote push credential.)

Kind Purpose
ForgejoRepository Create or import (migrate) a repository under an owner; reconcile its settings.
ForgejoOrganization Create and reconcile an organization.
ForgejoUser Create and reconcile a user account.
ForgejoTeam Create and reconcile a team within an organization.
ForgejoWebhook Manage a repository webhook (URL, events, secret).
ForgejoPushMirror Configure a push-mirror that replicates a repo to a remote on an interval.
ForgejoSSHKey Register an SSH public key on a user.
ForgejoOAuthApp Manage an OAuth2 application registration.
ForgejoLabel Manage an issue/PR label on a repository.
ForgejoBranchProtection Manage branch-protection rules on a repository.
ForgejoRelease Manage a release on a repository.

Most kinds publish a Ready print column (kubectl get forgejorepository shows it). ForgejoPushMirror publishes no print columns — its readiness is not in kubectl get output; read .status.conditions (the Ready condition) to see whether the mirror is configured.

Architecture

Every controller is a thin shim over the shared reconcile harness in libseanfarm-operator (imported as .../reconcile). The harness owns the entire reconcile lifecycle — finalizer add/remove, resolving the external client, mapping a handler's result to the Ready condition, deletion-policy semantics, conflict-safe status writes, condition/Event emission, and uniform requeue cadence. This repo is where that loop was first written; the other three seanfarm operators were built mirroring it, the common pattern was generalized into the library, and forgejo-operator was then ported back onto the shared implementation.

A controller's Reconcile does only two things: fetch the typed object (returning early on NotFound) and hand it to runReconcile, which calls the library's generic reconcile.Run. Per-resource behavior is supplied as a small Handler with two methods:

  • Converge(ctx, obj, *gitea.Client) (Outcome, error) — drives the Forgejo side toward the spec and reports whether it is fully reconciled. It returns Converged only once the remote state is verified, and Progressing otherwise (e.g. a repository import whose server-side clone hasn't written its refs yet).
  • Delete(ctx, obj, *gitea.Client) error — removes the remote object.

Ready is structural. The harness — never a handler — sets Ready, and only when Converge returns Converged. A handler cannot mark a resource Ready while it still reports the remote as unverified. This is "Ready means verified" enforced by construction: the ForgejoRepository handler, for instance, holds Ready=False (Progressing) for the whole duration of an async import.

Other wiring:

  • Taxonomy. The library classifies each failure reason as Terminal, FixedRequeue, or Backoff to decide requeue behavior. This operator's policy table is deliberately minimal: it sets a single default of Backoff, so every error falls through to controller-runtime's exponential workqueue backoff. (Auth/client failures carry distinct reasons — AuthTokenError, ClientError — for diagnostics, but all back off the same way.)
  • Deletion policy: BestEffortRelease. On delete the harness attempts the remote delete, then removes the finalizer regardless of the outcome — so a flaky or unreachable Forgejo can never wedge a CR's deletion. Each handler's Delete is itself best-effort (e.g. a repository with no recorded ID is a no-op).
  • External client. The harness resolves a *gitea.Client per reconcile from the CR's auth-token Secret and forgejoRef endpoint before calling the handler.
  • Secret-watch. Controllers watch Secrets via the library's MapSecretToRequests: when a referenced auth Secret appears or changes, every CR in that namespace referencing it is re-enqueued immediately, instead of waiting out the full requeue interval. This matters on a fresh cluster where a CR can reconcile before the admin-token Secret has been populated.

Security

Condition reasons/messages and Events land on world-readable status surfaces (etcd objects, kubectl describe), so they must never carry credential material.

  • Bounded messages. Every condition and Event message the harness writes is truncated to a fixed 512-byte cap (on a UTF-8 rune boundary). This caps how much a splatted remote-error body can disclose and prevents an unbounded message from blowing etcd's per-object size limit. Because the harness owns condition writes, this operator's conditions inherit the bound for free. The library also provides a SanitizingRecorder and bounded Event emission for operators that wire an event recorder.
  • Name the Secret, never quote it. Truncation bounds disclosure but does not sanitize; the real protection for credential-bearing paths is a generic message at the source. The auth-resolution path names the referenced Secret and key (secretRef.Name, secretRef.Key) in any error — it never interpolates the token value, DSN, or any other credential into status, Events, or logs.

Build & distribution

  • Images are built in-cluster. The operator container image is built by Shipwright (buildkit strategy) inside the seanfarm cluster and cosign-signed — never built or pushed from a laptop.
  • Forgejo is canonical; codeberg is the backup. Day-to-day pushes go to the in-cluster Forgejo at code.sean.farm. A ForgejoPushMirror backs the repo up to codeberg.org/someara/forgejo-operator roughly every 10 minutes, and a cluster make rebuild re-seeds Forgejo from that codeberg copy. Go module consumers fetch the module from codeberg through the default GOPROXY.

Development

Standard Kubebuilder layout. Work red/green: write or extend an envtest-backed controller test first, watch it fail, then make it pass.

The make targets that exist here:

Target Purpose
make test Generate manifests/deepcopy, fmt, vet, then run unit + envtest suites.
make test-e2e Run the e2e suite against a Kind cluster.
make lint / make lint-fix Run golangci-lint (optionally autofixing).
make manifests / make generate Regenerate CRDs/RBAC and DeepCopy code from the +kubebuilder markers.
make build / make run Build the manager binary / run it against your current kubeconfig.
make install / make deploy Install CRDs / deploy the manager to the current cluster.

CRD or API changes flow through make manifests generate, then the regenerated CRDs and the new image are rolled out via the in-cluster Shipwright Build/BuildRun and the Flux/Crossplane pins that reference the operator image — not by building locally.