The operator's reconciliation model
The Stowage operator is a controller-runtime-based reconciler with
two controllers: one for S3Backend, one for BucketClaim. This
page walks the lifecycle of each.
#S3Backend reconciliation
The S3Backend controller is mostly a credentials and connectivity
checker. It doesn't create or destroy resources beyond updating the
status subresource.
#On create
- Read the admin credentials from
spec.adminCredentialsSecretRef.namein the named namespace. - Render the
bucketNameTemplatewith dummy variables to validate it parses. - Probe the endpoint: a
ListBuckets(or equivalent service-level call) with the admin credentials. - Update the status:
Ready=True, Reason=EndpointReachableon success.Ready=Falsewith a specific reason (EndpointUnreachable,CredentialsInvalid,TemplateInvalid) on failure.
#On update
The reconciler picks up changes via the standard
controller-runtime watch loop. Edits to spec.endpoint,
spec.adminCredentialsSecretRef, or spec.bucketNameTemplate
trigger a re-probe.
#On delete
Cluster-scoped, no finalizer. Kubernetes deletes the object and the
status disappears. BucketClaims that referenced it now point at a
non-existent backend; their reconciliation will set their status
condition to BackendNotReady.
#BucketClaim reconciliation
This is where the real work lives.
#Phases
Pending → Creating → Bound (steady state)
Pending → Creating → Failed (terminal until you fix the spec)
Bound → Deleting → (deleted) (after kubectl delete)#On create
- Attach finalizer
broker.stowage.io/bucketclaim-protection. Subsequent deletes become "marked for deletion" until the finalizer is removed. - Resolve the
S3Backend. If not found or notReady=True, set statusPhase=Pending, BackendNotReadyand requeue. - Compute the bucket name. From
spec.bucketNameif set, otherwise the template. - Create the bucket on the upstream via the
internal/operator/backend/shim (this is a separate package from the dashboard'sinternal/backend/because it talks the admin API for creation, not the user-facing operations). - Mint a virtual credential. Generate
(access_key_id, secret_access_key), persist into the internal Secret in the operator namespace. - Write the consumer Secret in the claim's namespace, with the AWS_* env vars and the connection metadata.
- Apply the quota by writing
quota_soft_bytes/quota_hard_bytesinto the internal Secret. The proxy's informer picks this up. - Update status:
Phase=Bound,Ready=True,BoundSecretName=...,AccessKeyId=....
#On update
- Name change → rejected by the webhook (immutable field).
- Backend change → currently triggers a re-resolve of the backend; the bucket itself is not migrated. Don't repoint a claim at a different backend casually.
- Anonymous mode change → updates the corresponding Secret's
anonymous_modedata field. The proxy informer picks it up. - Quota change → updates the consumer Secret's
quota_soft_bytes/quota_hard_bytes. Effective on the next proxy request after the informer notices. - Rotation policy change → the next reconciliation either schedules a rotation (TimeBased mode) or doesn't (Manual).
#On delete
Reconciler runs the deletionPolicy:
Retain(default) — leave the bucket on the upstream alone. Just delete the Secrets and remove the finalizer.Delete— empty the bucket on the upstream, delete it, delete the Secrets, remove the finalizer.
If forceDelete: false and the bucket isn't empty, the reconciler
sets status Reason=BucketNotEmpty and refuses to proceed. Edit the
claim with forceDelete: true to override.
#Rotation flow
When a rotation triggers (manual annotation or scheduled):
- Mint a new
(access_key_id, secret_access_key)pair. - Update the internal Secret with the new credential.
- Update the consumer Secret with the new
AWS_*env vars. - Mark the old credential as "in overlap" — the proxy still accepts it.
- Wait
overlapSeconds. - Revoke the old credential (delete its row in the internal Secret).
- Update status
RotatedAt, bumpmetadata.labels.broker.stowage.io/rotation-generation.
The overlap window is what gives tenants time to roll Pods that mount the consumer Secret.
#Concurrency model
- Single replica. The chart deploys 1 operator Pod. Leader election is supported by controller-runtime but defaults to off because a single replica is the supported topology today.
- Per-claim ordering. The work-queue serialises reconciles per object. Two changes to the same claim are processed in order.
- Inter-claim parallelism. Different claims reconcile in parallel up to the configured worker count.
#Idempotency
Every step in BucketClaim reconciliation is idempotent:
CreateBucketis OK if the bucket already exists.MintCredentialchecks for an existing internal Secret with the correct labels and reuses it.WriteConsumerSecretis server-side-apply where supported, so field ownership is clear.
If a reconcile is interrupted mid-way and re-runs, it does the right
thing. The CreationInconsistent reason exists for the rare case
where the upstream has a bucket but the operator's record of the
matching credential is missing or corrupt — that one needs operator
attention.
#Source
- Reconcilers:
internal/operator/controller/. - Credential generation:
internal/operator/credentials/. - Secret writing:
internal/operator/vcstore/. - Upstream bucket lifecycle:
internal/operator/backend/. - Webhook:
internal/operator/webhook/.