connectTo-only management handle (Stage 1 of issue #269)¶
Status: Implemented in #270.
Problem¶
A user runs Garage deployed by the upstream Helm chart and wants the operator to manage cluster config declaratively — buckets, keys, permissions, layout — without the operator owning the workload. Federation is operator-to-operator, so it does not fit a cluster that has no operator on it yet.
Today there is no CR shape for "manage an external Garage's Admin-API state only." Three blockers exist in the code:
GetGarageClient(internal/controller/helpers.go) always builds the admin endpoint from the managed Service (svcFQDN(cluster.Name, …)) and reads the token fromcluster.Spec.Admin.connectTois consulted only on the gateway-layout path (gatewayLayoutClient→getExternalStorageClient).- The validating webhook rejects the natural shape:
validateTierserrors with "spec.connectTo is only valid alongside spec.gateway" wheneverconnectTois set without a gateway tier. GarageBucket/GarageKeygate oncluster.Status.Phase == Running, and the phase machinery derives Running only from storage/gateway pod readiness — a tier-less handle never reaches it.
Chosen shape¶
A GarageCluster with only spec.connectTo set (no storage, no gateway) —
a pure management handle. Inferred, no new API field.
apiVersion: garage.rajsingh.info/v1beta2
kind: GarageCluster
metadata:
name: existing-garage
spec:
connectTo:
adminApiEndpoint: http://garage.garage.svc:3903
adminTokenSecretRef:
name: garage-admin
key: admin-token
GarageBucket/GarageKey reference this cluster exactly as they would any
other. Helm keeps owning the pods; the operator touches only Admin-API state.
Changes¶
1. IsManagementHandle() — api/v1beta2/garagecluster_helpers.go¶
func (g *GarageCluster) IsManagementHandle() bool {
return g != nil &&
g.Spec.Storage == nil &&
g.Spec.Gateway == nil &&
g.Spec.ConnectTo != nil
}
2. Webhook — api/v1beta2/garagecluster_webhook.go¶
- In
validateTiers, replace the blankethasConnect && !hasGatewayrejection. AllowconnectTostandalone (management handle). Keep the edge-gateway rule (hasGateway && !hasStorage && !hasConnectstill errors). - In
validateConnectTo, when the CR is a management handle, require an admin-API path:adminApiEndpoint+adminTokenSecretRef, orclusterRef.rpcSecretRef/bootstrapPeersalone give no Admin API and are rejected as the only fields on a handle.
3. Client resolution — internal/controller/helpers.go¶
Extract the connectTo→client logic (currently duplicated in the reconciler's
getExternalStorageClient/getStorageClusterClient) into a shared free
function resolveConnectToClient(ctx, c, cluster, clusterDomain):
adminApiEndpoint+adminTokenSecretRef→ build directly.clusterRef→ resolve the referencedGarageCluster, use its managed endpoint + admin token.
GetGarageClient gains a leading branch: when cluster.IsManagementHandle(),
return resolveConnectToClient(...) instead of the svcFQDN path. Every
controller that already calls GetGarageClient (bucket, key, cluster) then
transparently talks to the external cluster.
4. Management-handle reconcile — internal/controller/garagecluster_controller.go¶
Early branch in Reconcile, after finalizer + maintenance, before
ensureRPCSecret:
reconcileManagementHandle:
- Resolves the admin client and probes reachability (
GetClusterStatus). - Reachable →
Status.Phase = Running, conditionManagementHandleReady=True, requeueRequeueAfterLong(5m, matching healthy edge gateways). - Unreachable →
Status.Phase = Pending, conditionFalsewith the error, requeueRequeueAfterUnhealthy. - Creates/reconciles no workload: no ConfigMap, Services, PDB, layout, migration, or pod-derived health. An Admin-only handle creates no RPC Secret; when an explicit or inherited RPC source exists, it pins only an immutable local credential snapshot for deterministic GarageKey derivation.
finalize short-circuits for a management handle (no owned Garage state or K8s
workload to tear down; bucket/key finalizers own their own remote cleanup).
5. Bucket/Key gating¶
No logic change — both already gate on Status.Phase == Running, satisfied by
the handle. Confirm GarageKey uses the same gate and nothing assumes a local
Service.
6. GarageKey RPC-secret consideration¶
deriveKeyMaterial needs GetRPCSecret. A handle may supply that identity via
spec.network.rpcSecretRef, spec.connectTo.rpcSecretRef, or inheritance from
spec.connectTo.clusterRef; reconciliation copies it into the same immutable
snapshot contract used by managed workloads before the handle becomes Ready.
An Admin-only handle does not invent a meaningless RPC identity. Such a handle
can still use importKey; requesting operator-derived key material surfaces an
actionable error naming the supported references. The first source may be
attached to an existing Admin-only handle, but its source and bytes are
immutable after pinning.
Testing¶
- Webhook: handle accepted;
connectTowith only rpcSecretRef/bootstrapPeers rejected; edge-gateway rule preserved; full cluster unaffected. GetGarageClient: handle → connectTo endpoint/token, notsvcFQDN.- Controller: handle reconcile sets
Runningon reachable admin API (mock),Pendingwhen unreachable; asserts no STS/ConfigMap/Service or generated RPC Secret is created, and explicit RPC sources produce an immutable owned snapshot.
Docs¶
CLAUDE.md: document the management-handle shape under the cluster-shapes / connectTo sections.config/samples/: add a management-handle example CR.
Out of scope (Stage 2)¶
In-place adoption of an existing StatefulSet + PVCs and retiring the Helm
release. Deferred — the modern operator manages per-node GarageNodes with
metadata/data PVC names that do not match the chart's meta-*/data-*,
so adoption needs a dedicated design.