Kubernetes operator for managing MinIO resources via CRDs
  • Go 97.3%
  • Shell 1.5%
  • Makefile 0.9%
  • Dockerfile 0.3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
pipeline-release-prepare ed0462a2e9
All checks were successful
pipeline/ci CI green v0.3.11
release: v0.3.11 (auto-release)
2026-07-24 12:17:20 +00:00
.devcontainer Initial commit: minio-operator v0.0.1 2026-04-18 09:02:59 +02:00
api test(e2e): exercise metrics and API conversion lifecycle 2026-07-24 12:20:48 +02:00
cmd fix(operator): harden reconciliation and storage contracts 2026-07-24 02:51:54 +02:00
config test(e2e): exercise metrics and API conversion lifecycle 2026-07-24 12:20:48 +02:00
dist fix(webhook): wire conversion CRD trust bundles 2026-07-24 04:07:20 +02:00
docs test(e2e): exercise metrics and API conversion lifecycle 2026-07-24 12:20:48 +02:00
hack test(build): lock reviewed fleet builder digest 2026-07-24 14:02:54 +02:00
internal test(e2e): exercise metrics and API conversion lifecycle 2026-07-24 12:20:48 +02:00
test test(e2e): exercise metrics and API conversion lifecycle 2026-07-24 12:20:48 +02:00
.custom-gcl.yml Initial commit: minio-operator v0.0.1 2026-04-18 09:02:59 +02:00
.dockerignore Initial commit: minio-operator v0.0.1 2026-04-18 09:02:59 +02:00
.gitattributes feat(cicd): pipeline onboarding contract + libseanfarm-operator v0.4.1 -> v0.4.10 2026-07-15 20:46:30 +02:00
.gitignore fix+chore: remove dead drift.go (latent nil-panic), dead F156, 76 .bak files 2026-06-11 09:43:20 +02:00
.golangci.yml Initial commit: minio-operator v0.0.1 2026-04-18 09:02:59 +02:00
AGENTS.md chore: remove confirmed-dead code (dead-code hunt) 2026-07-17 17:58:53 +02:00
CHANGELOG.md release: v0.3.11 (auto-release) 2026-07-24 12:17:20 +00:00
ci-test.Dockerfile build: pull fleet Go builder from Codeberg 2026-07-24 12:35:58 +02:00
Dockerfile build: pull fleet Go builder from Codeberg 2026-07-24 12:35:58 +02:00
go.mod fix(operator): harden reconciliation and storage contracts 2026-07-24 02:51:54 +02:00
go.sum fix(operator): harden reconciliation and storage contracts 2026-07-24 02:51:54 +02:00
Makefile test(build): pin fleet builder contract 2026-07-24 13:44:40 +02:00
operator.mk chore(scaffolding): adopt operator.mk (lib v0.4.12) 2026-07-17 12:33:37 +02:00
PROJECT Rename project to minio-resource-operator 2026-04-20 10:39:01 +02:00
README.md chore(release): v0.3.3 — README rewrite + bump dist 2026-06-11 14:01:23 +02:00
SECURITY.md feat: implement v1beta1 API with conversion webhooks 2026-04-21 01:41:52 +02:00
staticcheck.conf Add staticcheck.conf suppressing ST1001 for idiomatic Ginkgo/Gomega dot imports 2026-04-18 12:18:45 +02:00
VERSION release: v0.3.11 (auto-release) 2026-07-24 12:17:20 +00:00

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). Converge moves MinIO-side state toward the spec and reports either Converged or Progressing — it never reports Converged until it has verified the remote state. Delete removes the MinIO-side object.
  • A Taxonomy table classifying each failure reason as Terminal, FixedRequeue, or Backoff.

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 Client struct 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's minioRef. While MinIO is bootstrapping or unreachable, client resolution surfaces DependencyNotReady and 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). Cleanup removes Kubernetes-side state a CR owns — e.g. a managed credentials Secret — and the harness runs it on every delete path, including under a Retain deletion 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 MinioBucket that is BucketNotEmpty, or a sub-resource still Referenced — 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 in kubectl as a clear signal.

  • An auditable requeue knob. This operator deliberately maps TransientError to Backoff (controller-runtime's workqueue), the opposite of openbao-operator's FixedRequeue choice for the same reason. Because dispositions are data in the Taxonomy table 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 MinioBucket via bucketRef and wait for it to be Ready before 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 MinioUser re-reconciles when a referenced MinioPolicy changes, a bucket sub-resource when its parent MinioBucket changes, MinioIDPOpenID when its client-secret Secret changes — so dependents converge promptly instead of waiting for the next resync.

  • Validating webhook. A ValidatingWebhookConfiguration enforces cross-resource invariants the CRD OpenAPI schema can't express — notably the XOR between MinioBucketRetention and MinioBucketObjectLock for 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 SanitizingRecorder for 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/name and 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). A ForgejoPushMirror backs the repo up to codeberg.org/someara/minio-resource-operator roughly every ten minutes, and make rebuild re-seeds Forgejo from that codeberg backup. Go module fetches read codeberg through the default GOPROXY.
  • CI runs in-cluster via Argo Workflows / Tekton. There are no GitHub or Forgejo Actions, and there must never be a .github or .forgejo/workflows directory 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 test Generate, format, vet, and run the unit + envtest suites
    make build Build the manager binary
    make manifests / make generate Regenerate CRDs/RBAC/webhook config and deepcopy code
    make lint Run golangci-lint
    make build-installer Render a consolidated dist/install.yaml from config/
    make verify-installer Fail if dist/install.yaml is out of sync with config/
    make smoke-install Stand 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).