|
| 1 | +# kube-bind v2 Extended: Backend API, CLI, UI |
| 2 | + |
| 3 | +* Status: **DRAFT — for iteration** |
| 4 | +* Authors: @mjudeikis |
| 5 | +* Date: 2026-06-10 |
| 6 | +* Builds on: [v2-slim-core.md](v2-slim-core.md) (Proposed)) |
| 7 | + |
| 8 | +## Summary |
| 9 | + |
| 10 | +The v2 core contract is up for discussion: a binding is one `kubectl apply` of a Secret + |
| 11 | +`Connection` + bindings, consumed by the konnector, with zero kube-bind components on |
| 12 | +the provider. This proposal designs everything *around* that contract — the optional |
| 13 | +service layer that answers the questions the core deliberately doesn't: |
| 14 | + |
| 15 | +* **Who are you?** (auth: OIDC, sessions) |
| 16 | +* **What may you have?** (catalog: curated offerings on top of raw exported APIs) |
| 17 | +* **Here are your credentials.** (issuer: per-consumer SA/RBAC/kubeconfig, tenancy) |
| 18 | +* **Here is your bundle.** (gateway: HTTP API whose terminal output is the one-apply file) |
| 19 | +* **You stopped coming.** (reaper: GC keyed off the core's Lease) |
| 20 | +* **Make it pleasant.** (CLI, UI) |
| 21 | + |
| 22 | +The defining rule, inherited from the core: **every path through this layer terminates |
| 23 | +in the same one-apply bundle.** The service layer negotiates; it never syncs. If the |
| 24 | +backend is deleted the day after binding, sync is unaffected. |
| 25 | + |
| 26 | +``` |
| 27 | + provider cluster consumer cluster |
| 28 | + ┌────────────────────────────────────┐ |
| 29 | + │ extended layer (this proposal) │ |
| 30 | + │ ┌──────────┐ ┌────────┐ ┌──────┐ │ bundle |
| 31 | + │ │ gateway │ │ issuer │ │reaper│ │ (one-apply file) ┌───────────┐ |
| 32 | + │ │ auth, UI │ │ creds, │ │ Lease│ │ ──────────────────────▶│ konnector │ |
| 33 | + │ │ catalog │ │ tenancy│ │ GC │ │ via CLI / UI / GitOps│ (core) │ |
| 34 | + │ └──────────┘ └────────┘ └──────┘ │ └─────┬─────┘ |
| 35 | + └────────────────────────────────────┘ │ |
| 36 | + ▲ sync (core contract: CRDs, spec ⇧, status ⇩) │ |
| 37 | + └────────────────────────────────────────────────────────┘ |
| 38 | +``` |
| 39 | + |
| 40 | +## Goals |
| 41 | + |
| 42 | +* Every component independently deployable and optional; any subset works. The core |
| 43 | + (GitOps-only, no extended layer at all) remains a first-class path forever. |
| 44 | +* The backend's terminal output is **exactly** the core's one-apply bundle — no |
| 45 | + intermediate request/response CRDs on the consumer, no phase-gated handshake. |
| 46 | +* Tenancy lives here: per-consumer provider namespaces / kcp workspaces are an issuer |
| 47 | + concern, invisible to the core (which just sees a kubeconfig whose RBAC fences it in). |
| 48 | +* Pluggable auth from day one — OIDC is the reference implementation, not the contract. |
| 49 | +* HA-capable by construction (the v1 in-memory-session/single-replica limitation, |
| 50 | + roadmap #424/#488, must not survive into v2). |
| 51 | + |
| 52 | +## Non-Goals |
| 53 | + |
| 54 | +* Anything that changes core sync semantics — the core contract is immutable, this layer must adapt around it. |
| 55 | +* Marketplace/billing/quotas (a future layer above this one; the catalog leaves room). |
| 56 | +* Re-implementing v1's wire protocol (`BindingProvider`, `BindingResourceResponse`, |
| 57 | + `APIServiceExportRequest` flow). v2 extended is a clean protocol. |
| 58 | + |
| 59 | +## Components |
| 60 | + |
| 61 | +### 1. Catalog (provider-side CRDs) |
| 62 | + |
| 63 | +Raw discovery already exists in core (`Connection.status.exportedAPIs` from labeled |
| 64 | +CRDs / the workspace boundary). The catalog adds **curation**: human-facing metadata |
| 65 | +and sensible defaults that turn "a list of CRD names" into "a service you'd choose". |
| 66 | + |
| 67 | +Group: `catalog.kube-bind.io`. Two kinds, successors of v1's |
| 68 | +`APIServiceExportTemplate` and `Collection`: |
| 69 | + |
| 70 | +```yaml |
| 71 | +apiVersion: catalog.kube-bind.io/v1alpha1 |
| 72 | +kind: Export # one offering |
| 73 | +metadata: |
| 74 | + name: mangodb |
| 75 | +spec: |
| 76 | + title: MangoDB |
| 77 | + description: Managed MangoDB instances with automated backups. |
| 78 | + icon: { url: … } # optional |
| 79 | + docs: https://… # optional |
| 80 | + apis: # what a binding to this offering syncs |
| 81 | + - name: mangodbs.mangodb.io |
| 82 | + - name: mangodbbackups.mangodb.io |
| 83 | + defaults: # copied into the generated ClusterBinding |
| 84 | + conflictPolicy: Fail |
| 85 | + relatedResources: |
| 86 | + - group: "" |
| 87 | + resource: secrets |
| 88 | + direction: FromProvider |
| 89 | + selector: |
| 90 | + labelSelector: |
| 91 | + matchLabels: |
| 92 | + mangodb.io/managed: "true" |
| 93 | +``` |
| 94 | +
|
| 95 | +```yaml |
| 96 | +apiVersion: catalog.kube-bind.io/v1alpha1 |
| 97 | +kind: Collection # grouping for UI/CLI browsing |
| 98 | +metadata: |
| 99 | + name: databases |
| 100 | +spec: |
| 101 | + title: Databases |
| 102 | + exports: |
| 103 | + - name: mangodb |
| 104 | +``` |
| 105 | +
|
| 106 | +Notes: |
| 107 | +
|
| 108 | +* The catalog is **derived-from-core-truth**: an `Export` listing an API that isn't |
| 109 | + actually exported (label/boundary) gets a condition; the gateway hides it. The label |
| 110 | + remains the source of truth, the catalog is presentation + defaults. |
| 111 | +* These CRDs live on the provider and are read only by the gateway/UI/CLI. The |
| 112 | + konnector never sees them. |
| 113 | + |
| 114 | +### 2. Issuer (provider-side controller) |
| 115 | + |
| 116 | +Everything v1's `backend/kubernetes` did, made a named component. The issuer is a Go |
| 117 | +interface (provision boundary, mint credentials, revoke); **the in-tree implementation |
| 118 | +is plain Kubernetes only** — the kcp issuer lives in the separate `contrib/kcp` |
| 119 | +distribution, which wires its own implementation against the same interface. |
| 120 | + |
| 121 | +* Per consumer identity: provision the tenancy boundary — a namespace set on plain |
| 122 | + Kubernetes (workspaces, in the contrib/kcp issuer). |
| 123 | +* Mint credentials: ServiceAccount + RBAC scoped to exactly the exported APIs (+ |
| 124 | + declared related resources) within that boundary + kubeconfig. This fixes v1's |
| 125 | + cluster-admin-ish `kube-binder` ClusterRole (roadmap #303: reduced footprint). |
| 126 | +* **Credential mechanism: long-lived SA token** (v1 behavior, secret-based |
| 127 | + ServiceAccount token). Trade-off accepted deliberately: zero rotation friction and no |
| 128 | + konnector-side refresh machinery, at the cost of security posture — and noting |
| 129 | + upstream Kubernetes is steering away from secret-based SA tokens, so this is |
| 130 | + revisitable without API change (the bundle's Secret is replaceable; a bounded-token + |
| 131 | + reissue mode can be added later behind the same interface). Revocation = delete the |
| 132 | + `Grant` → issuer deletes the SA/token. |
| 133 | +* Records issuance in **`Grant`** (`catalog.kube-bind.io`): "identity X was issued |
| 134 | + credentials Y for export Z". The anchor for revocation, audit, and the reaper. |
| 135 | + |
| 136 | +### 3. Gateway (HTTP API) |
| 137 | + |
| 138 | +Stateless HTTP server (sessions externalized), serving: |
| 139 | + |
| 140 | +One gateway fronts exactly **one provider** — catalog aggregation across providers is a |
| 141 | +future layer above this one, already possible externally because the protocol's output |
| 142 | +is just a bundle. |
| 143 | + |
| 144 | +| Endpoint | Purpose | |
| 145 | +|---|---| |
| 146 | +| `GET /api/provider` | provider metadata + supported auth methods (successor of v1 `/api/exports`) | |
| 147 | +| `GET /api/catalog` | `Export`s + `Collection`s visible to the caller | |
| 148 | +| `POST /api/bind` | input: export name + consumer identity → drives issuer → returns a **one-time pickup URL** for the bundle | |
| 149 | +| `GET /api/bundle/<token>` | single-use, short-TTL (5 min) bundle pickup — **returns the bundle**, then the token is dead | |
| 150 | +| `POST /api/apply` | optional, flag-gated: browser-apply path — gateway applies the bundle (+ konnector install) into a consumer cluster using a caller-supplied consumer kubeconfig | |
| 151 | +| `GET /api/authorize`, `/api/callback` | auth flow (delegated to authenticator plugin) | |
| 152 | +| `GET /api/healthz` | health | |
| 153 | + |
| 154 | +**The bundle is the one-apply file**, delivered through a one-time pickup URL so live |
| 155 | +credentials are fetched exactly once and never stored at rest in the gateway. Content |
| 156 | +negotiation on pickup: `application/yaml` returns the literal multi-doc bundle (Secret + |
| 157 | +`Connection` + `ClusterBinding`); `application/json` wraps the same objects in a thin |
| 158 | +envelope (`{ bundle: [...] }`). There is no other handshake state: no request objects to |
| 159 | +poll, no phases to wait on. `curl` bind + pickup piped to `kubectl apply -f -` is a |
| 160 | +complete client. |
| 161 | + |
| 162 | +### 4. Auth (pluggable) |
| 163 | + |
| 164 | +* `Authenticator` interface: `Routes()` (mounted under `/api/auth/…`) + |
| 165 | + `Authenticate(r) (Identity, error)`. Reference implementations: OIDC (code grant + |
| 166 | + PKCE, as v1) and `kubernetes` (SubjectAccessReview against the provider — for |
| 167 | + in-platform UIs that already hold a cluster identity). |
| 168 | +* The embedded mock-OIDC server survives **only** as a dev-mode flag. |
| 169 | +* Session store stays an interface (memory, Redis as today) but the gateway must be |
| 170 | + fully functional with ≥2 replicas out of the box — Redis (or any external store) is |
| 171 | + the documented production default, memory is dev-only. |
| 172 | +* Identity → tenancy key: `issuer + "/" + subject` hash, as v1, so the same human gets |
| 173 | + the same boundary on re-bind. |
| 174 | + |
| 175 | +### 5. Reaper (provider-side, optional) |
| 176 | + |
| 177 | +The core leaves dead-consumer GC explicitly to this layer, keyed off the per-Connection |
| 178 | +`Lease` the konnector maintains: |
| 179 | + |
| 180 | +* Lease expired beyond TTL → mark the issuance stale → (configurably) revoke |
| 181 | + credentials, then delete kube-bind-created namespaces and synced objects. |
| 182 | +* TTLs and the destructive step are opt-in and conservative by default (revoke ≠ |
| 183 | + delete; deletion requires explicit enablement). |
| 184 | + |
| 185 | +### 6. CLI (`kubectl bind`) |
| 186 | + |
| 187 | +Thin client over the gateway; everything it does is reproducible by hand: |
| 188 | + |
| 189 | +```sh |
| 190 | +kubectl bind login https://mangodb.example.com # auth, cache token |
| 191 | +kubectl bind catalog # list Exports/Collections |
| 192 | +kubectl bind mangodb # bind an Export: |
| 193 | + # POST /api/bind → bundle |
| 194 | + # → kubectl apply (or -o yaml) |
| 195 | +kubectl bind mangodb -o yaml > binding.yaml # GitOps mode: print, don't apply |
| 196 | +``` |
| 197 | + |
| 198 | +* `--install-konnector` (default on for interactive use) installs/upgrades the v2 |
| 199 | + konnector, as v1 did. |
| 200 | +* The CLI never creates bespoke objects — it applies the gateway's bundle verbatim. |
| 201 | + `-o yaml` output committed to git is byte-for-byte the GitOps path. |
| 202 | +* v1 subcommands that existed to ferry the old handshake (`apiservice`, `deploy`, |
| 203 | + per-resource polling) disappear. |
| 204 | + |
| 205 | +### 7. UI (SPA) |
| 206 | + |
| 207 | +Browse catalog → authenticate → bind → then either: |
| 208 | + |
| 209 | +* **download the bundle** (via the one-time pickup URL) / copy a `kubectl bind` |
| 210 | + one-liner, or |
| 211 | +* **browser-apply** (v1's UI-only flow, roadmap #406, kept): the user supplies a |
| 212 | + consumer-cluster kubeconfig (or the UI runs in-platform where one is already held), |
| 213 | + and the gateway's `/api/apply` applies the bundle and installs the konnector into the |
| 214 | + consumer cluster. |
| 215 | + |
| 216 | +The UI is a pure gateway client; it holds no flow state the gateway doesn't have. |
| 217 | +Browser-apply is flag-gated on the gateway and off by default — it means consumer |
| 218 | +credentials transit the gateway, which deployments must consciously accept. |
| 219 | + |
| 220 | +## Packaging & repo |
| 221 | + |
| 222 | +Per the frozen core layout: `v2/backend` (gateway + issuer + reaper + auth), `v2/cli`, |
| 223 | +`v2/web`. The backend ships as **one binary with module flags** (`--enable-gateway`, |
| 224 | +`--enable-issuer`, `--enable-reaper`, `--enable-apply`) — operational simplicity over |
| 225 | +purity; the boundaries stay as Go packages so a future split costs a `main.go`, not a |
| 226 | +refactor. All of it depends on `v2/sdk` only. The kcp distribution (`contrib/kcp`) |
| 227 | +remains separate, providing its own issuer implementation behind the same interface. |
| 228 | + |
| 229 | +## Migration notes |
| 230 | + |
| 231 | +* The v1 wire protocol is not bridged: v1 CLI cannot talk to a v2 gateway. Both stacks |
| 232 | + can run side by side on one provider during transition (different endpoints, disjoint |
| 233 | + CRD groups). |
| 234 | +* v1 catalog objects (`APIServiceExportTemplate`/`Collection`) translate mechanically |
| 235 | + to `Export`/`Collection`; a converter script ships with the backend. |
| 236 | + |
| 237 | +## Decided |
| 238 | + |
| 239 | +* **Packaging**: one `kube-bind-backend` binary; gateway/issuer/reaper/apply are module |
| 240 | + flags, boundaries kept as Go packages. |
| 241 | +* **Issuance anchor**: `Grant` in `catalog.kube-bind.io` — the typed record of |
| 242 | + "identity X was issued credentials Y for export Z"; anchor for revocation, audit, |
| 243 | + reaper. |
| 244 | +* **Credentials**: long-lived secret-based SA token (v1 behavior) — zero rotation |
| 245 | + friction accepted over security posture; revocation via `Grant` deletion; bounded |
| 246 | + tokens addable later behind the same issuer interface without API change. |
| 247 | +* **Catalog vocabulary**: `Export` + `Collection`. |
| 248 | +* **Bundle delivery**: one-time pickup URL, 5-minute TTL, single use; bundle never |
| 249 | + stored at rest in the gateway. |
| 250 | +* **kcp**: stays a separate distribution (`contrib/kcp`) providing its own issuer |
| 251 | + implementation; the in-tree backend issuer is plain Kubernetes only. |
| 252 | +* **UI reach**: browser-apply path **kept** (roadmap #406) — gateway `/api/apply` |
| 253 | + applies bundle + installs konnector into the consumer cluster with a caller-supplied |
| 254 | + kubeconfig; flag-gated, off by default, consumer credentials transiting the gateway |
| 255 | + is an explicitly accepted trade-off when enabled. |
| 256 | +* **Federation**: one gateway = one provider; cross-provider aggregation is a future |
| 257 | + layer above the bundle protocol. |
| 258 | + |
| 259 | +## Open questions |
| 260 | + |
| 261 | +None — initial design questions resolved (see **Decided**). New questions raised during |
| 262 | +review go here. |
0 commit comments