Kubernetes operator for managing Forgejo webhooks
  • Go 95.8%
  • Makefile 2.6%
  • Shell 0.9%
  • Dockerfile 0.7%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Sean OMeara b6bd840a4d
All checks were successful
binjovi/ci Binjovi completed the frozen plan
docs: explain native delivery and task telemetry
2026-09-09 18:10:36 +02:00
.devcontainer Initial Forgejo operator implementation 2026-04-01 22:21:17 +02:00
api/v1alpha1 docs(webhook): describe secret replacement 2026-08-20 01:26:46 +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 docs(webhook): describe secret replacement 2026-08-20 01:26:46 +02:00
hack Initial Forgejo operator implementation 2026-04-01 22:21:17 +02:00
internal/controller fix(webhook): replace hooks when signing secret changes 2026-08-20 01:06:08 +02:00
test docs: rewrite documentation to ASD-STE100 2026-08-07 19:21:30 +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 docs: rewrite documentation to ASD-STE100 2026-08-07 19:21:30 +02:00
CHANGELOG.md docs: rewrite documentation to ASD-STE100 2026-08-07 19:21:30 +02:00
ci-test.Dockerfile fix(ci): pin golang:1.26 builder base by digest 2026-07-05 13:19:43 +02:00
Dockerfile perf(forgejo): namespace BuildKit Go caches 2026-09-06 00:18:29 +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 docs: explain native delivery and task telemetry 2026-09-09 18:10:36 +02:00
VERSION chore: devbump 0.6.27 -> 0.6.28-dev 2026-07-24 09:50:46 +00:00

forgejo-operator

forgejo-operator is a Kubernetes operator. It manages Forgejo (a self-hosted Gitea fork) in a declarative way. Each piece of Forgejo state is a Kubernetes custom resource (CR). This includes a repository, an organization, a webhook, and a push-mirror. The operator reconciles the live Forgejo instance toward the desired state. It talks to Forgejo over its Gitea-compatible API, and keeps the live state matched to the desired state. This operator is one of four seanfarm operators. All four share one reconcile harness. The harness was first built in this operator, then extracted so the other three could share it.

Custom resources

All kinds live in the API group forgejo.forgejo.io, version v1alpha1. All kinds are namespaced. Each CR references a Forgejo instance through forgejoRef. This defaults to forgejo-http.forgejo:3000 in-cluster. Each CR also references an admin-token Secret. The operator uses this Secret to authenticate to Forgejo. Most kinds name this Secret authSecretRef. ForgejoPushMirror is different: it names the admin-token Secret forgejoAuthSecretRef, and reserves the name authSecretRef for the remote push credential instead.

Kind Purpose
ForgejoRepository Create a repository under an owner, or import one by migration. Reconcile its settings.
ForgejoOrganization Create an organization and reconcile it.
ForgejoUser Create a user account and reconcile it.
ForgejoTeam Create a team inside an organization and reconcile it.
ForgejoWebhook Manage a repository webhook: its URL, events, and secret.
ForgejoPushMirror Configure a push-mirror. It replicates a repository to a remote on a set interval.
ForgejoSSHKey Register an SSH public key on a user.
ForgejoOAuthApp Manage an OAuth2 application registration.
ForgejoLabel Manage an issue or pull-request 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. Running kubectl get forgejorepository shows it. ForgejoPushMirror publishes no print columns. Its readiness does not show in kubectl get output. To check whether the mirror is configured, read .status.conditions and look for the Ready condition.

Architecture

Every controller is a thin shim over the shared reconcile harness. The harness lives in libseanfarm-operator, imported as .../reconcile. The harness owns the whole reconcile lifecycle. This includes: adding and removing the finalizer, resolving the external client, mapping a handler's result to the Ready condition, applying deletion-policy semantics, writing status safely under conflicts, emitting conditions and Events, and setting a uniform requeue cadence. This repository is where that loop was first written. The other three seanfarm operators were built to mirror it. Later, the common pattern was pulled out into the shared library. forgejo-operator was then changed to use that shared library instead of its own copy.

A controller's Reconcile method does only two things. First, it fetches the typed object, and returns early if the object is not found. Second, it hands the object to runReconcile, which calls the library's generic reconcile.Run. Each resource supplies its own behavior through 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 after the remote state is verified. It returns Progressing in every other case. For example, a repository import returns Progressing while the server-side clone has not yet written its refs.
  • Delete(ctx, obj, *gitea.Client) error — removes the remote object.

Ready is structural. Only the harness sets Ready. A handler never sets it directly. The harness sets Ready only when Converge returns Converged. This means a handler cannot mark a resource Ready while it still reports the remote as unverified. The rule "Ready means verified" is enforced by the code structure, not by convention. For example, the ForgejoRepository handler holds Ready=False (state Progressing) for the whole length of an asynchronous import.

Other wiring:

  • Taxonomy. The library classifies each failure reason as Terminal, FixedRequeue, or Backoff. This classification decides the requeue behavior. This operator's policy table is deliberately minimal. It sets one default: Backoff. Every error therefore falls through to controller-runtime's exponential workqueue backoff. Authentication and client failures carry distinct reasons (AuthTokenError, ClientError) for diagnostics, but all of them back off the same way.
  • Deletion policy: BestEffortRelease. On delete, the harness attempts the remote delete first. It then removes the finalizer regardless of the outcome. A flaky or unreachable Forgejo instance can therefore never block a CR's deletion. Each handler's own Delete method is also best-effort. For example, deleting a repository with no recorded ID is a no-op.
  • External client. Before calling the handler, the harness resolves a *gitea.Client for each reconcile. It builds this client from the CR's auth-token Secret and the forgejoRef endpoint.
  • Secret-watch. Controllers watch Secrets through the library's MapSecretToRequests. When a referenced auth Secret appears or changes, every CR in that namespace that references it is re-enqueued right away. The controller does not wait out the full requeue interval. This matters on a fresh cluster, where a CR can reconcile before the admin-token Secret exists.

Security

Condition reasons, condition messages, and Events land on world-readable status surfaces. This includes etcd objects and kubectl describe output. These surfaces must never carry credential material.

  • Bounded messages. The harness truncates every condition and Event message it writes to a fixed 512-byte cap. It truncates on a UTF-8 rune boundary. This cap limits how much a raw remote-error body can disclose. It also stops an unbounded message from exceeding etcd's per-object size limit. Because the harness owns every condition write, this operator's conditions get this bound for free. The library also provides a SanitizingRecorder and bounded Event emission for operators that wire up an event recorder.
  • Name the Secret, never quote it. Truncation limits disclosure, but it does not sanitize a message. The real protection on credential-bearing paths is a generic message written at the source. When the auth-resolution path reports an error, it names the referenced Secret and key (secretRef.Name, secretRef.Key). It never inserts the token value, the DSN, or any other credential into status, Events, or logs.

Build & distribution

  • Images are built in-cluster. Native Binjovi tasks run the tests and build AMD64 and ARM64 candidates with BuildKit. Release publishes the exact candidate digests and verifies their cosign signatures. Local image builds are not the cluster delivery path.
  • Forgejo is canonical. Codeberg is the backup. Day-to-day pushes go to the in-cluster Forgejo instance at code.sean.farm. A ForgejoPushMirror backs up the repository to codeberg.org/someara/forgejo-operator roughly every 10 minutes. A cluster make rebuild re-seeds Forgejo from that codeberg copy. Go module consumers fetch the module from codeberg through the default GOPROXY.

Development

This repository uses the standard Kubebuilder layout. Work in red/green style: 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 and deepcopy code, run fmt and vet, then run the unit and envtest suites.
make test-e2e Run the e2e suite against a Kind cluster.
make lint / make lint-fix Run golangci-lint, with an option to fix issues automatically.
make manifests / make generate Regenerate CRDs, RBAC, and DeepCopy code from the +kubebuilder markers.
make build / make run Build the manager binary, or run it against your current kubeconfig.
make install / make deploy Install the CRDs, or deploy the manager to the current cluster.

CRD and API changes always go through make manifests generate first. The regenerated CRDs and the new image then roll out through the in-cluster native Binjovi Build and Release, and through the Flux and Crossplane pins that reference the operator image. Do not build or roll out these changes locally.

Binjovi Build and Release proof

Binjovi tests each pull request in an isolated envtest task. It then builds one AMD64 and ARM64 OCI candidate from the exact revision. Binjovi is now the guarded Build and Release authority. Pipelines no longer declares this project. This revision also proves immediate signed webhook delivery and schedulable image execution under shared cluster load.

Native CI

Binjovi builds pull requests with the recipe in sean/binjovi-plans:recipes/forgejo-operator/. The active plan fixes the recipe revision for each build. Recipe changes use a plans pull request and release; they do not require a Binjovi runtime deployment.

The native Binjovi result is the only admission result for this project.

Task timing and release verification

Open the project dashboard and expand an execution attempt for the native task timeline and available CPU, off-CPU, and syscall profiles. Release is this project's pipeline endpoint; there is no configured Deploy target. Consumer image pins are separate changes.

Use binjovictl release forgejo-operator --build BUILD_ID --wait for an exact successful Build. Confirm the published image digest and signature in Release evidence. A task profile explains execution; it does not replace artifact verification.