A Kubernetes operator that provisions StackIT Object Storage buckets, workload credentials and isolation policies through Custom Resources. One operator deployment per cluster, bound to a single StackIT project via a service-account key.
The operator runs on any Kubernetes cluster, but it is designed and tuned for GitOps workflows — FluxCD in particular: see GitOps / FluxCD.
A Bucket custom resource maps to one isolated workload: a StackIT bucket, a
dedicated credentials group, an S3 access key, and a deny-based bucket policy that
isolates workloads from each other (Layer 2). Cross-project isolation (Layer 1) is
structurally guaranteed by StackIT itself. A bucket can optionally
share itself read-only with other Buckets in its
namespace. See CLAUDE.md and INIT-SETUP.md for
the architecture and security invariants.
apiVersion: stackit-bucket.gtrfc.com/v1
kind: Bucket
metadata:
name: my-bucket
namespace: team-a
spec:
bucketName: my-bucket
region: eu01
secretRef:
name: my-bucket-s3 # operator writes the credentials + connection info hereThe operator writes the provisioned access key and the S3 connection
parameters a workload needs into the referenced Secret. By default the data keys
are env-var style, so the Secret can be consumed directly via envFrom:
| Default key | Value |
|---|---|
AWS_ACCESS_KEY_ID |
S3 access key id |
AWS_SECRET_ACCESS_KEY |
S3 secret access key |
S3_BUCKET |
physical bucket name (see Bucket naming) |
S3_REGION |
region (e.g. eu01) |
S3_ENDPOINT |
endpoint host (e.g. object.storage.eu01.onstackit.cloud) |
S3_BUCKET_URL |
full path-style bucket URL |
Every data-key name is overridable per Bucket via spec.secretRef.keys — empty
fields fall back to the defaults above:
spec:
bucketName: my-bucket
secretRef:
name: my-bucket-s3
keys: # all optional
accessKeyID: ACCESS_KEY # default AWS_ACCESS_KEY_ID
secretAccessKey: SECRET_KEY # default AWS_SECRET_ACCESS_KEY
bucketName: BUCKET # default S3_BUCKET
region: REGION # default S3_REGION
endpoint: ENDPOINT # default S3_ENDPOINT
bucketURL: BUCKET_URL # default S3_BUCKET_URLNothing in the operator requires FluxCD — it works with plain kubectl, Argo CD
or any other tooling. But its behavior is deliberately shaped so that a Git
repository can stay the single source of truth and a continuously syncing
controller like Flux never fights the operator:
- The operator never mutates
spec, labels or annotations of aBucket. All operator state goes to the status subresource (plus one operator-owned bookkeeping annotation it only ever adds). Server-side apply and Flux drift detection stay clean; re-applying the same manifests is always a no-op. - Credentials rotation is level-based, not edge-based. The
rotate-credentials-atannotation value lives in Git; changing it in Git rotates exactly once, and every subsequent Flux sync of the same value does nothing (see Credentials rotation). - Bucket cloning is one-shot and terminal. Once
status.clone.phaseisCompleted, re-applied or even editedcloneFrommanifests never re-trigger a copy (see Cloning an existing bucket). - Config faults fail without a requeue hammer. An invalid CR (region
mismatch, key collision, foreign bucket, self-clone …) parks as
Ready=Failedwith a message instead of hot-looping; fixing the manifest in Git and letting Flux sync it reconciles the new generation. - Secret gating composes with GitOps app rollouts. With a clone requested,
the credentials Secret only appears after the data is complete — pods that
Flux deploys in parallel and that consume the Secret via
envFrom/secretKeyRefsimply stay pending until the bucket is actually ready. NodependsOnchoreography required. - Disaster recovery replays from Git. Physical bucket names are frozen in a
durable annotation, ownership tags use
namespace/name(not the CR UID), and cloud resources are found by deterministic names — restoring the same manifests into a fresh cluster re-adopts the existing buckets instead of duplicating them.
The operator reports progress on the Bucket status subresource, so kubectl get bucket (short name bkt) shows the live state:
NAME BUCKET PHASE READY STATUS REGION AGE
my-bucket my-bucket Ready True provisioned eu01 2m
status.phase—Pending→Provisioning→Ready, orFailed/Deleting.Readycondition — reasonsProvisioned,Provisioning,Failed, orNotImplemented(skeleton mode).status.messagecarries the current step or failure reason.- Config faults (a
secretRefpointing at the operator admin Secret, aspec.regionthat differs from the operator's region, a bucket-name/secret-key collision, or a bucket owned by someone else) setReady=Failedwithout requeue-hammering — fix the CR and the next generation reconciles. ProviderReachablecondition —Falsewhile a provisioned Bucket'sReadystate is being held through repeated provider failures, see Ready during provider outages. Absent on a healthy Bucket.- Other status fields:
resolvedBucketName,bucketURL,credentialsGroupID,credentialsGroupURN,accessKeyID(never the secret),observedGeneration,operatorVersion,degradedSince(see Ready during provider outages),grantedReadTo(see Sharing a bucket read-only),clone(see Cloning an existing bucket),lastRotationTrigger/lastRotationTime(see Credentials rotation).
Each Bucket is stamped with S3 ownership tags (managed-by + owner=<ns>/<name>)
so the operator adopts only buckets it owns and refuses to clobber a pre-existing
foreign or non-empty bucket. On bootstrap the operator creates a shared
operator-admin credentials group + S3 key (persisted in its own admin Secret,
default stackit-s3-provisioner-admin); that group's URN sits in every bucket
policy's exemption list as a lockout safeguard.
Ready on a provisioned Bucket describes the last verified state of the
bucket, not the outcome of the last attempt to verify it. When a reconcile of
an already-Ready Bucket fails for a reason that says nothing about the bucket —
the StackIT API unreachable, a gateway or WAF answering with an error page, a
Kubernetes API blip — the operator keeps Ready=True and records the
degradation instead:
NAME PHASE READY STATUS AGE
my-bucket Ready True ensure bucket: unexpected EOF 3h
status:
phase: Ready
degradedSince: "2026-08-25T08:13:04Z" # when the failures started
conditions:
- type: Ready
status: "True" # held: last VERIFIED state
reason: Provisioned
- type: ProviderReachable
status: "False"
reason: ProviderUnreachable
message: 'ensure bucket: unexpected EOF'Why: without this, one failed control-plane call flips a healthy Bucket to
Failed immediately. A short provider blip therefore marked every Bucket on the
cluster non-ready at once, which cascaded into everything health-checking them
(Flux Kustomization health checks in particular) and produced a cluster-wide
alert storm out of a two-minute outage.
The hold is bounded by providerDegradedGrace (default 30m). Once it
elapses the Bucket drops to Failed exactly as before, so a real outage still
becomes visible in the Bucket's own status — the window only decides how fast.
What is not held, and drops Ready immediately regardless of the grace:
| Case | Why |
|---|---|
A structured 400/401/403 from the provider |
The provider refusing the request, not failing to answer. 401/403 is the Object Storage API; 400 is how a revoked service-account key surfaces — the key flow never reaches the API, and the token endpoint answers 400 invalid_grant. A gateway error page carrying any of those codes has a non-JSON body and is held. |
| A workload credential the operator destroyed | The old access key was deleted and the replacement could not be published (a rotation or a re-created Secret hitting a provider failure). The operator knows the published credential is dead — that is local certainty, not an unverifiable provider state. |
Config faults (the failNoRequeue family above) |
Statements about this Bucket that the operator established locally. |
A Bucket that has never been Ready |
There is no verified state to defend; initial provisioning failures surface at once. |
A Bucket whose spec changed (observedGeneration != generation) |
The user asked for something new and it was not achieved. |
| A Bucket being deleted | Holding Ready would hide a teardown blocked by the non-empty data-loss guard. |
Independently of the hold, the reconcile still returns an error, so the retry
backoff, the Warning events and controller_runtime_reconcile_errors_total are
unchanged — the StackitS3ReconcileErrors alert fires immediately as before, and
StackitS3BucketProviderDegraded fires while a hold is in effect (see
Monitoring).
# values.yaml
providerDegradedGrace: "30m" # default; "0" disables the hold entirelySetting it to "0" restores the previous behavior without deploying a different
image.
Trade-off: while
Readyis held, a bucket that really did break stays green for up to the grace window. That is deliberate — a delayed signal is bounded and recoverable, whereas marking the whole fleet unhealthy on the first blip is neither.
A Bucket can be seeded from an existing S3 bucket — any S3-compatible endpoint
(another StackIT project, AWS, MinIO, …) — by declaring spec.cloneFrom. The
contents are copied once, right after the bucket is provisioned:
apiVersion: stackit-bucket.gtrfc.com/v1
kind: Bucket
metadata:
name: my-bucket
namespace: team-a
spec:
bucketName: my-bucket
secretRef:
name: my-bucket-s3
cloneFrom:
endpoint: object.storage.eu01.onstackit.cloud # host or URL of the source
bucket: seed-data # source bucket name
region: eu01 # optional (SigV4 signing)
addressingStyle: path # optional: path (default) or
# virtual-hosted (AWS style)
secretRef:
name: seed-data-creds # Secret with read access to the source bucket;
keys: # must live in the Bucket's own namespace
accessKeyID: AWS_ACCESS_KEY_ID # optional overrides,
secretAccessKey: AWS_SECRET_ACCESS_KEY # defaults shown
holdSecretUntilCloned: true # defaultThe source credentials Secret is read from the Bucket's own namespace only
(no namespace field — referencing foreign namespaces through the operator's
privileges is deliberately not possible). Its data-key names are configurable
via cloneFrom.secretRef.keys, and the defaults match what this operator writes
into its own credentials Secrets — so a Secret provisioned for another Bucket
works as a clone source as-is.
Addressing style. The source is addressed path-style by default
(endpoint/bucket — the norm for S3-compatible services like StackIT, MinIO or
Ceph). For sources that prefer or require virtual-hosted addressing
(bucket.endpoint — AWS's recommended style), set
cloneFrom.addressingStyle: virtual-hosted. The destination (StackIT) always
stays path-style.
Secret gating. By default (holdSecretUntilCloned: true) the workload
credentials Secret is only written once the copy finished successfully, so
consuming workloads never start against a half-filled bucket. Set it to false
to publish the credentials immediately; the Ready condition still waits for
the clone either way.
How it runs. The copy is executed by an rclone Job in
the operator's namespace (image and pod resources via the Helm values
clone.image / clone.resources). rclone's remote-control API — protected by
a generated 32-character password, and by a NetworkPolicy restricting it to the
operator (clone.networkPolicy.enabled, default true; disable on clusters
whose CNI does not enforce NetworkPolicies) — is polled while the job runs, and
the transfer progress lands in the CR status:
$ kubectl get bkt my-bucket -o wide
NAME BUCKET PHASE READY STATUS CLONE
my-bucket my-bucket Provisioning False cloning from …/seed-data: 2.0 GiB / 18.0 GiB (11%) 2.0 GiB / 18.0 GiB (11%)
status.clone carries the details (phase, bytesCopied, totalBytes,
progress, rate, eta, startedAt, completedAt), and the CloneCompleted
condition tracks the outcome. The total size is measured once up front, so the
percentage has a stable denominator.
Semantics.
- The clone is one-shot and terminal: once
status.clone.phaseisCompletedit never runs again for this Bucket, even ifcloneFromchanges. - A failed attempt is retried with backoff; rclone resumes and skips objects
that were already copied.
rclone copysemantics: the destination is merged into, never deleted from. - Cloning a bucket onto itself (same endpoint + bucket) is rejected as a config fault.
- Deleting the CR while a clone is running stops the job and cleans up its staging Secret before the normal teardown.
The workload access key can be rotated on demand via an annotation on the
Bucket CR — no spec change required:
metadata:
annotations:
stackit-bucket.gtrfc.com/rotate-credentials-at: "2026-07-16T10:00:00Z"The value is an opaque trigger (by convention an RFC3339 timestamp, mirroring
kubectl rollout restart's restartedAt). Whenever it differs from
status.lastRotationTrigger, the operator replaces the access key — all keys in
the bucket's credentials group are deleted first, then a single fresh key is
created and written to the credentials Secret — and records the handled value
and time in status.lastRotationTrigger / status.lastRotationTime, emitting
a CredentialsRotated event.
The trigger is level-based and GitOps-safe: the operator never mutates the annotation, an unchanged value is a no-op, and removing the annotation triggers nothing. Rotation is hard: the old key stops working immediately, so workloads must re-read the Secret (e.g. restart their pods) to pick up the new credentials.
Rotate a specific Bucket:
kubectl annotate bucket my-bucket \
stackit-bucket.gtrfc.com/rotate-credentials-at="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--overwriteRotate all Buckets matching a label selector (e.g. everything labelled
team=payments):
kubectl annotate buckets -l team=payments \
stackit-bucket.gtrfc.com/rotate-credentials-at="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--overwrite--overwrite is required on re-rotation (the annotation already exists then).
Both commands operate on the current namespace; add -n <namespace> or
--all-namespaces (with kubectl annotate buckets --all-namespaces -l …) as
needed.
By default a bucket is reachable by exactly one credential: its own. A bucket
can additionally grant read-only access to other Bucket CRs in the same
namespace via spec.grantReadAccess. The grant is declared on the bucket that
owns the data, so a bucket's full access list is visible in its own spec:
apiVersion: stackit-bucket.gtrfc.com/v1
kind: Bucket
metadata:
name: gitlab-artifacts
namespace: gitlab
spec:
bucketName: gitlab-artifacts
secretRef:
name: gitlab-artifacts-s3
grantReadAccess: # optional, default: no additional access
- name: gitlab-backups # metadata.name of a Bucket CR in namespace gitlabThe credentials in gitlab-backups-s3 can now list gitlab-artifacts and get
its objects. They still cannot write to it, delete from it, or touch its
configuration.
| Granted to a reader | Denied to a reader |
|---|---|
ListBucket, ListBucketVersions |
every Put*, Delete* and Create* action |
GetObject, GetObjectVersion |
multipart listing/abort (would expose or destroy the owner's in-flight uploads) |
GetObjectTagging, GetObjectVersionTagging |
bucket policy, replication, notifications, lifecycle, object lock |
GetBucketLocation, GetBucketVersioning, GetBucketObjectLockConfiguration |
anything at all on buckets that did not grant it |
Rules worth knowing:
-
Namespace-scoped. Entries name a
BucketCR and are resolved in the granting Bucket's own namespace, so a Bucket in another namespace cannot be named here and a same-named Bucket elsewhere resolves to a different credentials group. Note that the principal written into the policy is located by the operator's derived credentials-group name, which is what already decides which group a Bucket owns — a namespace allowed to createBucketresources is inside the trust boundary either way. -
Never blocking. A referenced Bucket that does not exist yet (or is not finished provisioning) is skipped with a
ReadGrantPendingwarning event; the granting bucket still becomesReady. The grant is applied automatically as soon as the reference resolves. -
Revocation is automatic. Deleting a referenced Bucket removes it from the policy on the granting bucket's next reconcile. Removing the entry from
spec.grantReadAccessdoes the same, immediately. -
Self-references are rejected by the CRD schema.
-
A bucket being filled by a clone shares nothing yet. While
spec.cloneFromis still copying, granted readers stay out of the policy and are added the moment the copy succeeds — the same reason the bucket's own credentials Secret is held back by default. -
An ambiguous reference grants nothing. If the credentials-group name a reference resolves to exists more than once in the StackIT project, the grant is refused with a
ReadGrantPendingevent rather than pointed at a guess. -
Whatever is currently in effect is listed in
status.grantedReadTo:$ kubectl get bucket gitlab-artifacts -o jsonpath='{.status.grantedReadTo}' ["gitlab-backups"]
Leaving grantReadAccess unset keeps the previous behavior exactly — the
bucket policy is then identical to that of a bucket that never used the feature.
Deleting a Bucket CR tears down the access key, credentials group, bucket and
credentials Secret — but only when the bucket is empty. A non-empty bucket
blocks deletion (data-loss guard) until its objects are removed.
A Bucket can opt into an automatic wipe instead: with spec.wipeOnDelete: true
the operator deletes all objects (including versions and delete markers)
before removing the bucket. The field is mutable, so it can be set right before
deleting the CR.
spec:
bucketName: my-bucket
wipeOnDelete: true # default false: deletion is blocked while data exists
secretRef:
name: my-bucket-s3The feature is gated operator-wide by the Helm value wipeOnDelete.enabled
(default false). While the gate is off, a requested wipe is ignored: deletion
degrades to the safe empty-only guard and a warning event
(WipeOnDeleteSkipped) is emitted. A wipe also never runs on a bucket whose
ownership tags do not prove this operator provisioned it.
The operator serves Prometheus metrics on :8080 (--metrics-bind-address) —
the standard controller-runtime and Go collectors plus its own metrics:
| Metric | Type | Meaning |
|---|---|---|
stackit_s3_provisioner_buckets{phase} |
gauge | Number of Bucket resources per status.phase (Pending, Provisioning, Ready, Failed, Deleting; Unknown for CRs without a status yet). All phases are always exported. |
stackit_s3_provisioner_buckets_clone{phase} |
gauge | Number of Bucket resources per clone phase (Running, Completed, Failed); only Buckets with a clone are counted |
stackit_s3_provisioner_buckets_wipe_on_delete |
gauge | Number of Bucket resources with spec.wipeOnDelete: true |
stackit_s3_provisioner_buckets_provider_degraded |
gauge | Number of Bucket resources whose Ready state is being held through provider failures |
stackit_s3_provisioner_bucket_degraded_since_timestamp_seconds{namespace,name} |
gauge | Unix time at which this Bucket started degrading; absent for Buckets that are not degraded — so time() - <series> is the age of the degradation wherever the series exists |
stackit_s3_provisioner_skeleton_mode |
gauge | 1 while the operator runs without a StackIT service-account key (provisions nothing) |
stackit_s3_provisioner_wipe_on_delete_gate_enabled |
gauge | 1 while the operator-wide --enable-wipe-on-delete feature gate is on |
stackit_s3_provisioner_credentials_last_rotation_timestamp_seconds{namespace,name} |
gauge | Unix time of the Bucket's last credentials rotation; absent for never-rotated Buckets |
All gauges are computed live from the cluster state on every scrape, so they
never drift. For clusters running the
prometheus-operator
stack (e.g. kube-prometheus-stack), the chart can ship the scrape config and
alerting rules — both disabled by default because they require the
monitoring.coreos.com CRDs, and installation would fail on clusters without
them:
# values.yaml
monitoring:
serviceMonitor:
enabled: true # renders a metrics Service + ServiceMonitor
interval: 30s
scrapeTimeout: "" # empty = Prometheus default
labels: {} # extra ServiceMonitor labels, e.g. release: kube-prometheus-stack
prometheusRule:
enabled: true
labels: {} # extra PrometheusRule labels
alerts: # every alert has its own toggle, all default to enabled
bucketsWipeOnDelete: { enabled: true }
bucketFailed: { enabled: true }
bucketStuckProvisioning: { enabled: true }
bucketStuckDeleting: { enabled: true }
cloneFailed: { enabled: true }
skeletonMode: { enabled: true }
wipeRequestedButGateDisabled: { enabled: true }
reconcileErrors: { enabled: true }
bucketProviderDegraded: { enabled: true }Some kube-prometheus-stack installs only discover ServiceMonitor/
PrometheusRule objects carrying a specific label (typically
release: <helm-release-name>); set it via the labels values above.
Shipped alerts — every toggle lives under monitoring.prometheusRule.alerts.<name>.enabled:
| Alert | Toggle | Severity | Fires when |
|---|---|---|---|
StackitS3BucketFailed |
bucketFailed |
warning |
a Bucket sits in phase Failed for 15m. Config faults deliberately park without requeueing — without this alert nobody notices them. |
StackitS3BucketStuckProvisioning |
bucketStuckProvisioning |
warning |
Buckets sit in Pending/Provisioning for 30m (StackIT API problems, quota, a long-running clone) |
StackitS3BucketStuckDeleting |
bucketStuckDeleting |
warning |
a finalizer teardown hangs for 30m — usually the non-empty data-loss guard blocking deletion |
StackitS3CloneFailed |
cloneFailed |
warning |
a bucket clone stays Failed for 30m despite backoff retries |
StackitS3SkeletonMode |
skeletonMode |
critical |
the operator runs without a service-account key for 15m: probes stay green, nothing is provisioned |
StackitS3BucketsWipeOnDelete |
bucketsWipeOnDelete |
warning |
at least one Bucket carries spec.wipeOnDelete: true for 5m — deleting such a CR irreversibly wipes all objects in its bucket |
StackitS3WipeRequestedButGateDisabled |
wipeRequestedButGateDisabled |
warning |
Buckets request spec.wipeOnDelete while the operator-wide gate (wipeOnDelete.enabled) is off — deletion would silently degrade to the empty-only guard |
StackitS3ReconcileErrors |
reconcileErrors |
warning |
more than 3 reconcile errors within 15m (controller-runtime's built-in controller_runtime_reconcile_errors_total) |
StackitS3BucketProviderDegraded |
bucketProviderDegraded |
warning |
a Bucket's Ready state has been held through provider failures for 10m. These Buckets still report Ready=True to health checks, so this alert is the only signal until the grace elapses. |
The chart is served from a plain Helm repository, so a HelmRepository +
HelmRelease pair is all Flux needs:
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: stackit-s3-provisioner
namespace: flux-system
spec:
interval: 1h
url: https://guided-traffic.github.io/stackit-s3-provisioner/
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: stackit-s3-provisioner
namespace: stackit-s3-provisioner-system
spec:
interval: 1h
chart:
spec:
chart: stackit-s3-provisioner
version: "1.x" # or pin an exact version
sourceRef:
kind: HelmRepository
name: stackit-s3-provisioner
namespace: flux-system
install:
createNamespace: true
values:
stackit:
region: eu01
serviceAccountKey:
secretName: stackit-sa-key # see belowThe StackIT service-account key must exist as a Secret (key sa-key.json) in
the release namespace. It contains a private key, so never commit it to Git in
plain text — ship it as a SOPS-encrypted
Secret manifest alongside the HelmRelease (or via SealedSecrets / ExternalSecrets):
apiVersion: v1
kind: Secret
metadata:
name: stackit-sa-key
namespace: stackit-s3-provisioner-system
stringData:
sa-key.json: |
{ … service-account key JSON, SOPS-encrypted in Git … }helm repo add stackit-s3-provisioner https://guided-traffic.github.io/stackit-s3-provisioner/
helm repo update
# Provide the StackIT service-account key (key flow) as a Secret:
kubectl create namespace stackit-s3-provisioner-system
kubectl -n stackit-s3-provisioner-system create secret generic stackit-sa-key \
--from-file=sa-key.json=./account.json
helm install stackit-s3-provisioner stackit-s3-provisioner/stackit-s3-provisioner \
--namespace stackit-s3-provisioner-system \
--set stackit.region=eu01 \
--set stackit.serviceAccountKey.secretName=stackit-sa-keyWithout stackit.serviceAccountKey.secretName the operator runs in skeleton
mode: it reconciles Bucket resources but does not touch the cloud.
By default the physical StackIT bucket name equals spec.bucketName. The operator
can prepend a fixed prefix (e.g. a cluster identifier) and optionally the
Bucket's namespace, so bucket names stay unique and traceable across clusters
or teams that share one StackIT project. It is an operator-wide policy configured
at install time:
# values.yaml
bucketNaming:
prefix: my-cluster # prepended to every bucket name (empty = disabled)
includeNamespace: true # append the Bucket's namespace after the prefixWith the above, a Bucket named my-bucket in namespace monitoring is
provisioned as the physical bucket my-cluster-monitoring-my-bucket. The name
is composed as <prefix>-<namespace>-<spec.bucketName>, dropping any disabled
part; the defaults (prefix: "", includeNamespace: false) reproduce the legacy
behaviour where the physical name equals spec.bucketName.
The composed name is what workloads connect to: it is written to the S3_BUCKET
and S3_BUCKET_URL keys of the credentials Secret and shown as the RESOLVED
column in kubectl get bucket.
Stable across policy changes. The physical name is frozen per Bucket the first
time it is provisioned — recorded in status.resolvedBucketName and a durable
annotation (stackit-bucket.gtrfc.com/resolved-bucket-name) that survives status
loss (e.g. a CR restored from backup). Changing prefix or includeNamespace
later therefore only affects newly created buckets; existing buckets keep their
original name and stay reachable. Buckets provisioned before this feature existed
keep their raw spec.bucketName.
Constraints. prefix must be a lowercase DNS-1123 label (letters, digits and
-, no leading/trailing -); an invalid prefix stops the operator at startup. The
composed name must be 3–63 characters and DNS-compliant — if the prefix and
namespace push it out of range the Bucket is rejected (Ready=Failed) rather than
silently truncated.
make help # list all targets
make build # build the manager binary
make test-unit-coverage # unit tests (offline)
make test-integration-coverage # envtest integration tests
make lint gosec vuln cyclo # linters and security scans
make generate-all # regenerate CRD + DeepCopy and sync the Helm chart
make e2e-local # spin up Kind, install via Helm, run e2e smoke testsRun make generate-all after any change to api/v1/ types and commit the result —
CI fails the release if the checked-in CRD/DeepCopy/Helm chart drift from the types.
Apache-2.0 — see LICENSE.