unkinben 54d3e38223
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
Support adopting existing radosgw buckets and users
The operator previously assumed it created every user and bucket it managed:
reconciling an existing resource could overwrite its user attributes or wipe its
bucket policy, and deleting a CRD always deleted the underlying RGW object (only
Bucket had retainOnDelete). That made taking over pre-existing radosgw state
unsafe. Make adoption first-class.

- add retainOnDelete to ObjectStoreUser and BucketAccess (dedicated users), so
  deleting the CRD orphans the RGW user instead of deleting it (symmetric with
  Bucket)
- merge bucket policy instead of replacing it: the operator marks its own
  statements with a cephrgwop* Sid and preserves any statement it does not own,
  so adopting a bucket with a hand-written policy keeps it; add Bucket
  managePolicy (default true) to opt out of policy management entirely
- only reconcile user attributes the spec sets: DisplayName when non-empty and
  Suspended is now an optional *bool, so adopting a user does not reset them
- record adoption: ObjectStoreUser/Bucket status.adopted (+ printcolumn) is true
  when the RGW object already existed on first reconcile
- add GetBucketPolicy + MergeBucketPolicy; keyed adoption detection off the
  status identity field so a Pending owner wait does not mislabel it
- regenerate CRDs/deepcopy; add docs/adoption.md and
  config/samples/05-adoption.yaml; cover the merge in policy_test.go

Claude-Session: https://claude.ai/code/session_016CEncETbf8cvy1PhsHfFHM
2026-07-25 00:15:10 +10:00

cephrgw-operator

A Kubernetes operator that provisions Ceph RGW (S3) buckets and access keys declaratively, talking directly to radosgw the way the CLI does. You describe a bucket, its owner, and who may read or write it as custom resources; the operator creates the RGW users and bucket, delivers the access/secret keys into Kubernetes Secrets, and maintains the bucket's S3 policy.

It uses native Go libraries against radosgw (e.g. https://radosgw.service.consul:443) — no manager dashboard, no RADOS access, no admin socket, no in-cluster Ceph required:

  • go-ceph rgw/admin drives the RGW Admin Ops API (/admin/...) for users, keys, quotas and bucket info/removal — pure Go, no cgo.
  • aws-sdk-go-v2 drives the S3 API for bucket creation, versioning, policy, tagging and object lock — the operations the Admin Ops API does not expose. These are signed as the bucket owner, so the owner owns the bucket directly.

Custom resources

Kind Short Purpose
ObjectStoreUser osu An RGW S3 user. The operator creates it and writes its key pair into a Secret.
Bucket bkt An S3 bucket owned by an ObjectStoreUser. Owns the bucket's aggregate S3 policy.
BucketAccess ba Grants a user read-only, read-write or full access to a Bucket, delivering RW/RO keys.

How access levels work

The bucket owner (Bucket.spec.ownerRef) always has full control. Each BucketAccess adds a principal to the bucket's S3 policy:

  • read-onlys3:GetObject, s3:ListBucket and friends.
  • read-write → read plus s3:PutObject / s3:DeleteObject / multipart.
  • fulls3:* on the bucket and its objects.

If a BucketAccess omits userRef, the operator provisions a dedicated RGW user for that grant and writes its keys into spec.secretName (default <name>-rgw). If userRef names an existing ObjectStoreUser, that user's own credential Secret is reused and only the policy is extended.

Fine-grained grants

The level is the ergonomic default; four optional fields on BucketAccess refine it (see config/samples/04-access-fine-grained.yaml):

  • spec.paths — scope object access to key prefixes; each becomes the resource <bucket>/<prefix>*. The bucket-level ListBucket still spans the whole bucket.
  • spec.actions — grant exactly these S3 actions instead of the level's set (on the bucket and its, optionally prefixed, objects).
  • spec.conditionssourceIPs (an aws:SourceIp CIDR allowlist) and secureTransportOnly (require TLS).
  • spec.rawStatements — an escape hatch of raw S3 policy statements (effect/actions/resources/conditions) merged for this grant's principal. When set, level, actions, paths and conditions are ignored; resources without an arn: prefix are treated as bucket-relative key prefixes.

RGW honours S3 bucket policy on Reef 18.2+ / Squid; condition-key support is a subset of AWS, so validate exotic conditions against your cluster.

Adopting existing buckets and users

The operator can take over buckets/users that already exist in radosgw and hand them back without deleting them. In short: matching CRDs manage the resource in place (no recreation, keys reused, status.adopted: true), the bucket policy is merged so an existing hand-written policy is preserved (spec.managePolicy: false opts out entirely), and spec.retainOnDelete on ObjectStoreUser / Bucket / BucketAccess orphans the RGW object instead of deleting it. See docs/adoption.md and config/samples/05-adoption.yaml.

The Bucket controller renders the policy as the union of every ready BucketAccess that targets it, so the result is convergent regardless of the order objects are created or deleted. It watches BucketAccess and ObjectStoreUser, re-reconciling the bucket whenever a grant or user changes.

ObjectStoreUser ──admin PUT /admin/user──────▶ Secret (AK/SK)
Bucket          ──S3 CreateBucket (as owner)─▶ owns S3 policy
BucketAccess    ──admin PUT /admin/user──────▶ Secret (AK/SK, RW or RO)
                        └────── enqueues Bucket ──▶ S3 PutBucketPolicy (aggregate)

Credential Secrets

Every credential Secret carries the conventional keys, ready to mount straight into a workload:

  • AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY
  • RGW_UID
  • S3_ENDPOINT, BUCKET_HOST (when CEPH_RGW_ENDPOINT is set)
  • BUCKET_NAME (on BucketAccess Secrets)

Secrets are owner-referenced by the resource that produced them, so they are garbage-collected when the resource is deleted.

Prerequisites

The operator needs an RGW user with admin caps (users=*;buckets=*) and its access/secret key, the radosgw endpoint, and (for read-only/non-owner read-write grants) Ceph Reef 18.2+ / Squid. See docs/ceph-setup.md for the exact commands and the cephrgw-credentials Secret schema.

Quickstart

kubectl apply -f config/samples/00-owner-user.yaml
kubectl apply -f config/samples/01-bucket.yaml
kubectl apply -f config/samples/02-access-readonly.yaml
kubectl apply -f config/samples/03-access-readwrite.yaml

kubectl get osu,bkt,ba
kubectl get secret app-data-ro-rgw -o jsonpath='{.data.AWS_ACCESS_KEY_ID}' | base64 -d

Development

make generate   # regenerate deepcopy, CRDs and RBAC from kubebuilder markers
make build      # build the operator binary
make test       # go test -race
make lint fmt   # go vet / gofmt

Local (kind)

kind create cluster --name cephrgw
docker build -t cephrgw-operator:dev -f Dockerfile.operator .
kind load docker-image cephrgw-operator:dev --name cephrgw

kubectl apply -f config/crd/bases/
kubectl apply -f hack/kind/manifests/     # edit the Secret first

CI

Woodpecker runs pre-commit (gofmt + vet), test, and a dry-run image build on pull requests; pushing a v* tag builds and pushes git.unkin.net/unkin/cephrgw-operator to the Gitea registry. Bump a release with make patch|minor|major.

Notes & caveats

  • Policy clearing. Removing the last BucketAccess issues an S3 DeleteBucketPolicy. A NoSuchBucketPolicy response is treated as already clear. Adding/replacing grants always works.
  • Per-bucket quota. Bucket.spec.quota is applied as the owner's default bucket quota via the Admin Ops API, which is per-owner rather than strictly per-bucket. Use distinct owners if you need independent bucket quotas.
  • Immutability. bucketName, an ObjectStoreUser's uid, and object lock are fixed at creation; changing them on an existing object has no effect.
S
Description
Kubernetes operator that provisions Ceph RGW (S3) buckets and access keys (RW/RO) from CRDs via the Ceph manager dashboard API
Readme 298 KiB
Languages
Go 98.3%
Makefile 1.7%