A small in-cluster operator that provisions named resources on
backing services (Kafka topics, S3 buckets) and mints
secretKeyRef-friendly credentials Secrets for consumers.
Status: v1alpha1 contract; controller implementation pending. The API and driver semantics in this README and
SPEC.mdare the surface currently under review. No release artefacts exist yet.
This README is for cluster maintainers — the person deploying
and configuring the controller. End-user examples for consumers
(workload teams writing Buckety resources) live under
examples/. The internal design contract is in
SPEC.md.
Buckety was designed informed by an exploration of the Container Object Storage Interface (COSI). COSI's two-resource split (compartment + credential binding) and the property that the controller is not on the data path are both inherited here. The deviations are operational, not architectural:
- Diagnosability. Backend identities are operator-chosen
(e.g.
tenant1.orders.v1for a Kafka topic,tenant1-ordersfor an S3 bucket), not opaque controller-generated UIDs. The name you wrote shows up in dashboards, inrpk topic list, inaws s3 ls, and in the operator's logs. - Blast radius of config changes. Each
Bucketycarries its own mutable parameters. A retention or partition-count change touches one resource. COSI's immutableBucketClassforces "rebuild everything in this class" cycles when a class-level setting needs to change. - Failure surface. One controller binary with all drivers
compiled in. No sidecar / Unix-socket dance, no per-driver
Deployment to monitor. Scaling the controller to zero is
safe by design — running workloads hold their credentials
Secret directly, with stock
secretKeyRefkeys, so they don't need a JSON-blob parser to keep working while the controller is down. - Recovery. Standard Kubernetes status conditions
(
Ready,Reconciling,BackendUnavailable,ParameterDrift,BlockedByAccesses) cover the diagnostic surface. Out-of-band drift on the backend is surfaced explicitly rather than silently re-reconciled. - Portability. Backend choice (VersityGW vs MinIO vs AWS S3) is a deploy-time cluster-maintainer decision, not part of the API. Consumer YAML moves between clusters with different backing services unchanged, as long as a backend by the same name exists.
v1alpha1. Two drivers shipped:
| Driver | Backing services | Notes |
|---|---|---|
kadm |
Kafka-protocol brokers (Redpanda, Apache Kafka, Confluent) | Topic create/alter/delete. v1alpha1: no per-consumer SASL/SCRAM scoping. |
s3 |
S3-compatible (VersityGW, MinIO, AWS S3, Cloudflare R2, Hetzner, GCS interop) | Bucket create/delete. v1alpha1: all consumers receive the backend's root keys. |
e2e coverage in CI runs against Redpanda (kadm) and VersityGW +
MinIO (s3). The other listed S3 backends share the same client
library and the same e2e shape; if you hit a compatibility issue
with one of them, please file an issue.
- You (the cluster maintainer) deploy the controller and write a config file listing named backends. Each backend wires one driver up to one backing-service instance.
- A consumer team writes a
Bucketyin their namespace selecting one of your named backends. The controller provisions the topic / bucket on the backing service. - The same consumer (or another) writes a
BucketyAccessto mint aSecretwith the bootstrap/endpoint/credentials. The Secret has flat keys;valueFrom.secretKeyRefandenvFrom.secretRefwork without any client-side parsing. - The controller is only required at provision/reconfigure/revoke. Once a Secret exists, workloads talk to the backend directly. Scaling the controller to zero does not affect running consumers.
The operator publishes a kustomize "release" base alongside its
container image. Every tagged release of Yolean/buckety-controller
generates a deploy/kustomize/release/ directory in the repo
with the image already pinned by digest — the same digest the
GHA workflow pushed to ghcr.io/yolean/buckety-controller. The
build is reproducible: image and base ship together.
Pre-v0.1.0 installs. Until the first tagged release,
deploy/kustomize/release/does not exist. Vendor fromdeploy/kustomize/base/instead and pin the image yourself via your overlay'simages:field. Migrate to the release base when v0.1.0 ships.
Vendor the release base into your platform repo:
# your-platform/buckety-controller/upstream/
# Copy from Yolean/buckety-controller@<tag>:deploy/kustomize/release/.
# Refresh by re-copying when you bump to a newer tag.
Then overlay it with namespace and your config:
# your-platform/buckety-controller/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: buckety
resources:
- upstream
secretGenerator:
- name: buckety-controller-config
files:
- buckety-controller.yaml # next file in this folder
patches:
- path: deployment-patch.yaml # env vars for ${VAR} interpolationsecretGenerator is preferred over a hand-rolled Secret
manifest: it hashes the file content into the Secret name, so a
config change triggers a controller-Pod rollout on the next
kubectl apply automatically.
The controller loads buckety-controller.yaml from a directory
passed via -c <dir> (same convention as y-cluster -c <dir>).
Strict YAML decode — typos in keys are a startup error.
# buckety-controller.yaml
backends:
- name: cluster-kafka
driver: kadm
config:
seedBrokers:
- y-bootstrap.kafka.svc.cluster.local:9092
- name: cluster-objects
driver: s3
config:
endpoint: http://y-s3-api.blobs.svc.cluster.local:9000
region: us-east-1
forcePathStyle: true
accessKeyID: ${VERSITYGW_ROOT_ACCESSKEY}
secretAccessKey: ${VERSITYGW_ROOT_SECRETKEY}Fields tagged envsubst:"true" in each driver's config struct
support shell-style interpolation:
${VAR} required; controller exits non-zero if VAR is unset
${VAR:-default} optional with default
$$ literal $
Fields not tagged that contain ${...} are also rejected at load.
This is the y-cluster pkg/envsubst forward-compatibility guard —
tagging a field is a commitment, anything else getting expansion
"for free" would surprise a later version.
For each driver, the documented config schema marks which fields accept env substitution. Typically: credential fields only. Wire them up via the controller Deployment's env:
# deployment-patch.yaml
spec:
template:
spec:
containers:
- name: controller
env:
- name: VERSITYGW_ROOT_ACCESSKEY
valueFrom:
secretKeyRef: { name: versitygw-server, key: root-accesskey }
- name: VERSITYGW_ROOT_SECRETKEY
valueFrom:
secretKeyRef: { name: versitygw-server, key: root-secretkey }Rotating a credential means rotating the Secret AND re-rolling the controller Pod. Hot-reload of the config file is not supported in v1alpha1.
You pick backend names; they're the consumer-facing surface. Conventions that have worked:
- Purpose-based:
cluster-kafka,cluster-objects,tenant1-objects. - The same name across clusters that play the same role — consumer YAML is portable as long as a backend by that name exists on the target cluster. The driver behind the name can differ (a dev cluster might run MinIO; prod might run VersityGW).
Every driver publishes two JSON Schemas, generated from its Go types using the y-cluster schema toolchain:
- The
config:block (used in this file). - The
spec.parametersshape forBucketyresources (used by the controller's admission webhook).
Schemas are published at stable GitHub raw URLs under
pkg/drivers/<driver>/schema/. Add a header to your config file
so your editor validates as you type:
# yaml-language-server: $schema=https://raw.githubusercontent.com/Yolean/buckety-controller/v0.1.0/pkg/drivers/kadm/schema/v0.1/config.schema.jsonA workload team writes:
apiVersion: buckety.yolean.se/v1alpha1
kind: Buckety
metadata:
name: orders
namespace: tenant1
spec:
backend: cluster-kafka
parameters:
partitions: "12"
config.retention.ms: "604800000"
retentionPolicy: Retain
# Optional: a single-consumer shortcut. Omit to author the
# BucketyAccess resource(s) separately.
defaultAccess:
role: ReadWrite
credentialsSecretName: orders-topicThe controller creates the Kafka topic and mints Secret
orders-topic in namespace tenant1:
apiVersion: v1
kind: Secret
metadata:
name: orders-topic
namespace: tenant1
type: Opaque
data:
bootstrap: <base64>
topic: <base64> # the actual topic name on the brokerThe workload references it directly:
env:
- name: KAFKA_BOOTSTRAP
valueFrom:
secretKeyRef: { name: orders-topic, key: bootstrap }
- name: KAFKA_TOPIC
valueFrom:
secretKeyRef: { name: orders-topic, key: topic }For multi-consumer / different-role setups, drop defaultAccess
and author one BucketyAccess per consumer — see
examples/kadm/multi-consumer/.
When you remove defaultAccess, the controller deletes the
implicit BucketyAccess it materialised and its Secret is
garbage-collected via owner-ref. If your explicit
BucketyAccess reuses the same credentialsSecretName,
consumers see a brief gap during the swap as the implicit Secret
is removed before the explicit one materialises. Pick fresh
Secret names, or remove defaultAccess in one apply and add the
explicit BucketyAccess in a second apply, if zero-gap matters.
See SPEC.md for the
full lifecycle.
roleis advisory in v1alpha1.BucketyAccess.spec.roleacceptsReader,Writer, orReadWrite, but the v1alpha1 kadm and s3 drivers do not yet scope credentials per role. Every Secret minted for the sameBucketycarries identical root credentials regardless ofrole. The controller surfaces aScopingNotImplemented=Truecondition on each affectedBucketyAccess(visible viakubectl describe bucketyaccess) so the gap is honest, not silent. Scoped credentials (SASL/SCRAM, IAM users) are v1alpha2 work. Until then, treatroleas documentation of intent, not enforcement.
S3 is the same shape; see examples/s3/.
For platform-conformant naming (region prefix, tenant
namespace, zero-padded generation), use a template in
spec.name instead of letting it default to metadata.name:
spec:
name: "${backend.zone}.${namespace}.${name}.v${label['yolean.se/generation']}"
# resolves at first reconcile to e.g. eu.tenant1.orders.v003The full set of substitution variables and the resolution rules
are in SPEC.md.
Buckety— a topic / bucket / future-MySQL-database. Selects a backend by name. Carries mutable parameters; the controller reconciles drift to the backing service.BucketyAccess— a Secret request. Each one mints exactly one Secret in the same namespace as theBucketyAccess. Multiple accesses can target the same Buckety (in v1alpha1 they all receive identical credentials).
Both kinds are namespaced. Cross-namespace bucketyRef is not
supported in v1alpha1.
Standard conditions you'll see on resources:
Ready=True— backend resource is in sync with spec, all Secrets minted.Reconciling=True— work in progress; check the message.BackendUnavailable=True— thestatus.backendrecorded on this resource no longer exists inbuckety-controller.yaml, or its driver changed. Restore the backend in config or migrate the resource (see Migration below).ParameterDrift=True— out-of-band change on the backend the driver can't reconcile in place (e.g. someone shrunk a Kafka partition count). Inspect, decide, and either recreate the resource or adjust the spec to match reality.BlockedByAccesses=True— aBucketydeletion is waiting for itsBucketyAccesschildren to be removed first. Delete them explicitly; the controller does not cascade.ScopingNotImplemented=True— aBucketyAccessrequested a per-consumer role/scope the v1alpha1 driver does not yet honour. The Secret still mints with the backend's root creds; this condition warns you that the scope you asked for is not enforced.
Strict YAML decode catches typos:
parse /etc/buckety/buckety-controller.yaml: error unmarshalling JSON:
while decoding JSON: json: unknown field "seedBroker"
Means you wrote seedBroker instead of seedBrokers. Fix it.
Undefined ${VAR} without a default also fails fast:
/etc/buckety/buckety-controller.yaml: backends[1].config.accessKeyID:
undefined variable "VERSITYGW_ROOT_ACCESSKEY"
The Pod CrashLoopBackOff is the user-visible symptom; the log message is the diagnostic. This path is covered by an e2e scenario, so if the message ever stops being useful, file an issue.
If you rename or replace a backend after consumers exist:
- Existing resources keep reconciling against the old name as
long as it's still in
buckety-controller.yaml. Keep both names for a deprecation window. - Consumer teams update their
Bucketyresources to reference the new name. Sincespec.backendis immutable, this means delete and recreate. Coordinate retention policy: withretentionPolicy: Retainthe backing resource survives the recreate. - Once no resources reference the old name, remove it from config.
Auto-migration is not in v1alpha1.
The full list is in SPEC.md. Highlights:
- No per-consumer credential scoping. SASL/SCRAM and IAM-user minting are v1alpha2 work.
- No MySQL driver.
- No cross-namespace
bucketyRef. - No hot-reload of
buckety-controller.yaml. - No adoption of pre-existing backing resources.
Issues that include the full controller log on startup, the
config file (credentials redacted), the affected resource's
status.conditions, and the backing-service version are
easiest to act on. The e2e suite in examples/ is the
reproducer template — if your scenario doesn't match any of
those, that's useful information too.