- Go 97.3%
- Shell 1.5%
- Makefile 0.9%
- Dockerfile 0.3%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| .devcontainer | ||
| api | ||
| cmd | ||
| config | ||
| dist | ||
| docs | ||
| hack | ||
| internal | ||
| 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 | ||
| SECURITY.md | ||
| staticcheck.conf | ||
| VERSION | ||
minio-resource-operator
Formerly
minio-operator. Renamed to distinguish it from the upstream MinIO operator that manages MinIO tenants/clusters. This project manages MinIO resources — IAM and buckets — inside an already-running MinIO deployment. The CRD API group (minio.sean.farm) is unchanged.
A Kubernetes operator that declaratively manages MinIO
resources from custom resources. Each CR names a desired piece of MinIO state —
an IAM user/group/policy/service-account, an OpenID identity provider, a bucket,
or one of a bucket's sub-resources (versioning, object-lock, retention, quota,
encryption, tags, CORS, policy, lifecycle) — and the operator reconciles the
external MinIO system to match, through MinIO's admin API (madmin) and S3 API
(minio-go). It is one of the seanfarm operator fleet
(forgejo-operator, kratos-identity-operator, openbao-operator,
minio-resource-operator), all built on the shared
libseanfarm-operator
reconcile harness.
Built with kubebuilder.
Custom resources
All kinds live in API group minio.sean.farm. The API is served at two
versions — v1beta1 (the stored hub) and v1alpha1 (a served spoke that
round-trips through generated conversions to the hub).
IAM
| Kind | Purpose |
|---|---|
MinioUser |
IAM user; writes generated credentials into a managed Secret |
MinioGroup |
IAM group |
MinioGroupMembership |
Membership edge binding a user to a group |
MinioPolicy |
Canned IAM policy document |
MinioServiceAccount |
Service-account credentials derived from a user |
MinioIDPOpenID |
OpenID Connect identity-provider configuration |
Buckets
| Kind | Purpose |
|---|---|
MinioBucket |
A bucket (parent of every sub-resource below) |
MinioBucketVersioning |
Bucket versioning configuration |
MinioBucketObjectLock |
Default object-lock retention (requires versioning) |
MinioBucketRetention |
Bucket-level retention (mutually exclusive with object-lock) |
MinioBucketQuota |
Bucket quota |
MinioBucketEncryption |
Bucket server-side-encryption configuration |
MinioBucketTags |
Bucket tags |
MinioBucketCORS |
Bucket CORS configuration |
MinioBucketPolicy |
Bucket access policy (raw JSON) |
MinioILMPolicy |
Bucket lifecycle (ILM) rules |
Each bucket sub-resource targets its parent MinioBucket via
spec.bucketRef.name in the same namespace, rather than addressing the bucket
directly.
Architecture
Shared reconcile harness
The top-level controllers run on libseanfarm-operator's generic reconcile
harness — the lifecycle extracted from four near-identical, drifting
hand-written reconcile loops across the seanfarm operators into one place. The
harness owns the entire reconcile lifecycle: finalizer mechanics,
external-client resolution, mapping a handler's convergence result onto the
Ready condition, the reason→requeue taxonomy, deletion-policy semantics,
conflict-safe status writes, secret-watch mapping, sanitized Events, and uniform
concurrency with exponential backoff.
An operator supplies only a thin policy shim:
Handler(Converge+Delete).Convergemoves MinIO-side state toward the spec and reports eitherConvergedorProgressing— it never reportsConvergeduntil it has verified the remote state.Deleteremoves the MinIO-side object.- A
Taxonomytable classifying each failure reason asTerminal,FixedRequeue, orBackoff.
Ready is structural. The harness — never the handler — sets Ready=true,
and only after Converge reports it actually verified the external state. A
handler that is unsure returns Progressing, which keeps Ready=false and
requeues; it cannot accidentally assert readiness.
MinIO specifics
-
External client. Reconcilers act through a
Clientstruct that wraps the admin API (madmin) and the S3 API (minio-go); both connections target the same endpoint with the same root credentials, multiplexed over MinIO's HTTP API. A factory resolves the client from a CR'sminioRef. While MinIO is bootstrapping or unreachable, client resolution surfacesDependencyNotReadyand requeues on a short transient cadence rather than latching a terminal error — so freshly-created CRs don't false-fail Flux health checks during tenant bring-up. -
Cleanup-forwarding adapter. The top-level controllers reach the harness through an adapter that forwards an optional
CleanupHandler(a plain adapter would hide it).Cleanupremoves Kubernetes-side state a CR owns — e.g. a managed credentials Secret — and the harness runs it on every delete path, including under aRetaindeletion policy. Deletion policy governs only MinIO-side state; the invariant "CR gone ⇒ managed Secret gone" holds regardless. -
Delete taxonomy holds, it doesn't fail. On the delete path, safety refusals — a
MinioBucketthat isBucketNotEmpty, or a sub-resource stillReferenced— hold the finalizer and re-check on the resync cadence without returning an error, so a state only a human can resolve isn't exponentially backed off and the object stays visible inkubectlas a clear signal. -
An auditable requeue knob. This operator deliberately maps
TransientErrortoBackoff(controller-runtime's workqueue), the opposite ofopenbao-operator'sFixedRequeuechoice for the same reason. Because dispositions are data in theTaxonomytable rather than buried in a code ladder, that divergence is an explicit, reviewable decision. -
Bucket sub-resources are a second lifecycle. The nine per-bucket sub-resource controllers resolve a parent
MinioBucketviabucketRefand wait for it to beReadybefore acting; they are deliberately kept off the generic harness and re-stamp their own lifecycle (still using the shared status, event, and finalizer helpers). -
Secret + reference watches. Controllers watch the resources they depend on — a
MinioUserre-reconciles when a referencedMinioPolicychanges, a bucket sub-resource when its parentMinioBucketchanges,MinioIDPOpenIDwhen its client-secretSecretchanges — so dependents converge promptly instead of waiting for the next resync. -
Validating webhook. A
ValidatingWebhookConfigurationenforces cross-resource invariants the CRD OpenAPI schema can't express — notably the XOR betweenMinioBucketRetentionandMinioBucketObjectLockfor a given bucket. -
Metrics. The manager exports per-resource Prometheus series (reconcile duration, reconcile errors, resource status).
Security
The operator was swept against a real security audit. Condition reasons/messages and Kubernetes Events land in world-readable status surfaces, so:
- Every condition and Event message is bounded (the library caps messages at
512 bytes, cutting on a UTF-8 boundary). A splatted MinIO error body can't
land unbounded in a world-readable Event. Events route through the library's
truncating emitters, and the library additionally offers a
SanitizingRecorderfor the same guarantee at the recorder boundary. - Credential paths emit generic messages. No plaintext credential material —
access keys, secret keys, service-account credentials — ever reaches status,
Events, or logs. The rule is: name the Secret, never quote it. Managed-Secret
errors identify the Secret by
namespace/nameand describe the conflict, not its contents.
Build & distribution
- Container images are built in-cluster. The seanfarm cluster builds the operator image with Shipwright (buildkit strategy) and cosign-signs it; that in-cluster pipeline is the canonical image path. Images are not built on a laptop for the running cluster.
- Forgejo is canonical; codeberg is the backup. Day-to-day pushes go to
Forgejo (
code.sean.farm). AForgejoPushMirrorbacks the repo up tocodeberg.org/someara/minio-resource-operatorroughly every ten minutes, andmake rebuildre-seeds Forgejo from that codeberg backup. Go module fetches read codeberg through the defaultGOPROXY. - CI runs in-cluster via Argo Workflows / Tekton. There are no GitHub or
Forgejo Actions, and there must never be a
.githubor.forgejo/workflowsdirectory in this repo.
Release tags are immutable; the project follows pre-1.0 semantic versioning.
Development
-
Red/green TDD. Tests live alongside each reconciler (
internal/controller/*_controller_test.go) and run on controller-runtime envtest; the MinIO client has a fake backend (internal/minioclient/fake.go) so reconcile behavior is exercised without a live server. -
Standard targets (see the
Makefile):Target Purpose make testGenerate, format, vet, and run the unit + envtest suites make buildBuild the manager binary make manifests/make generateRegenerate CRDs/RBAC/webhook config and deepcopy code make lintRun golangci-lint make build-installerRender a consolidated dist/install.yamlfromconfig/make verify-installerFail if dist/install.yamlis out of sync withconfig/make smoke-installStand up a kind cluster, install cert-manager + the operator, and verify webhook readiness -
How a change ships. Edit → red/green test → push to Forgejo. The in-cluster Shipwright Build/BuildRun produces and signs the image; the deployed tag/digest is pinned from the seanfarm GitOps repo and platform compositions, and Flux reconciles it onto the cluster.
License
Apache License 2.0 (per the source file headers).