- Go 95.8%
- Makefile 2.6%
- Shell 0.9%
- Dockerfile 0.7%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| .devcontainer | ||
| api/v1alpha1 | ||
| cmd | ||
| config | ||
| hack | ||
| internal/controller | ||
| test | ||
| .custom-gcl.yml | ||
| .dockerignore | ||
| .gitattributes | ||
| .gitignore | ||
| .golangci.yml | ||
| AGENTS.md | ||
| CHANGELOG.md | ||
| ci-test.Dockerfile | ||
| Dockerfile | ||
| go.mod | ||
| go.sum | ||
| Makefile | ||
| operator.mk | ||
| PROJECT | ||
| README.md | ||
| VERSION | ||
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 returnsConvergedonly after the remote state is verified. It returnsProgressingin every other case. For example, a repository import returnsProgressingwhile 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 ownDeletemethod 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.Clientfor each reconcile. It builds this client from the CR's auth-token Secret and theforgejoRefendpoint. - 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
SanitizingRecorderand 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. AForgejoPushMirrorbacks up the repository tocodeberg.org/someara/forgejo-operatorroughly every 10 minutes. A clustermake rebuildre-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.