Skip to content

Commit eb2c262

Browse files
committed
Add v2 proposals for review
1 parent 73c7af1 commit eb2c262

2 files changed

Lines changed: 863 additions & 0 deletions

File tree

docs/proposals/v2-extended.md

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
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

Comments
 (0)