Skip to content

Commit 4412b19

Browse files
authored
Merge pull request #560 from mjudeikis/v2.arch
Add v2 proposals
2 parents b8ef02d + 260084e commit 4412b19

2 files changed

Lines changed: 963 additions & 0 deletions

File tree

‎docs/proposals/v2-extended.md‎

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
# kbind 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 kbind 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.kbind.dev`. Two kinds, successors of v1's
68+
`APIServiceExportTemplate` and `Collection`:
69+
70+
```yaml
71+
apiVersion: catalog.kbind.dev/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.kbind.dev/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). On a
126+
plain-Kubernetes provider "scoped to the exported APIs" is an explicit Role enumerating
127+
those resources; on the kcp/CRD-less flavor the tenancy boundary *is* the workspace, so
128+
scoping is the workspace grant itself (everything in it is exported by construction)
129+
rather than a per-resource enumeration — same `Issuer` interface, two scoping mechanisms
130+
matching the core's two schema sources.
131+
* **Credential mechanism: long-lived SA token** (v1 behavior, secret-based
132+
ServiceAccount token). Trade-off accepted deliberately: zero rotation friction and no
133+
konnector-side refresh machinery, at the cost of security posture — and noting
134+
upstream Kubernetes is steering away from secret-based SA tokens, so this is
135+
revisitable without API change (the bundle's Secret is replaceable; a bounded-token +
136+
reissue mode can be added later behind the same interface). Revocation = delete the
137+
`Grant` → issuer deletes the SA/token.
138+
* Records issuance in **`Grant`** (`iam.kbind.dev` — an issuance/identity record, not
139+
catalog presentation): "identity X was issued credentials Y for export Z". The anchor
140+
for revocation, audit, and the reaper.
141+
142+
### 3. Gateway (HTTP API)
143+
144+
Stateless HTTP server (sessions externalized), serving:
145+
146+
One gateway fronts exactly **one provider** — catalog aggregation across providers is a
147+
future layer above this one, already possible externally because the protocol's output
148+
is just a bundle.
149+
150+
| Endpoint | Purpose |
151+
|---|---|
152+
| `GET /api/provider` | provider metadata + supported auth methods (successor of v1 `/api/exports`) |
153+
| `GET /api/catalog` | `Export`s + `Collection`s visible to the caller |
154+
| `POST /api/bind` | input: export name + consumer identity → drives issuer → returns a **one-time pickup URL** for the bundle |
155+
| `GET /api/bundle/<token>` | single-use, short-TTL (5 min) bundle pickup — **returns the bundle**, then the token is dead |
156+
| `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 |
157+
| `GET /api/authorize`, `/api/callback` | auth flow (delegated to authenticator plugin) |
158+
| `GET /api/healthz` | health |
159+
160+
**The bundle is the one-apply file**, delivered through a one-time pickup URL so live
161+
credentials are fetched exactly once and never stored at rest in the gateway. Content
162+
negotiation on pickup: `application/yaml` returns the literal multi-doc bundle (Secret +
163+
`Connection` + `ClusterBinding`); `application/json` wraps the same objects in a thin
164+
envelope (`{ bundle: [...] }`). There is no other handshake state: no request objects to
165+
poll, no phases to wait on. `curl` bind + pickup piped to `kubectl apply -f -` is a
166+
complete client.
167+
168+
The single-use, short-TTL property applies to the **pickup URL**, not to the credential
169+
inside it: the bundle's Secret carries the long-lived SA token minted by the issuer
170+
(§2), so a bundle that has been picked up once stays valid and re-appliable. That is what
171+
makes `-o yaml > binding.yaml` committed to git a real GitOps artifact — the pickup is
172+
consumed once, the token it delivered keeps working until its `Grant` is revoked.
173+
Re-running `bind` mints a fresh `Grant`/token and a new pickup; it does not invalidate a
174+
previously committed bundle unless that `Grant` is explicitly revoked.
175+
176+
### 4. Auth (pluggable)
177+
178+
* `Authenticator` interface: `Routes()` (mounted under `/api/auth/…`) +
179+
`Authenticate(r) (Identity, error)`. Reference implementations: OIDC (code grant +
180+
PKCE, as v1) and `kubernetes` (SubjectAccessReview against the provider — for
181+
in-platform UIs that already hold a cluster identity).
182+
* The embedded mock-OIDC server survives **only** as a dev-mode flag.
183+
* Session store stays an interface (memory, Redis as today) but the gateway must be
184+
fully functional with ≥2 replicas out of the box — Redis (or any external store) is
185+
the documented production default, memory is dev-only.
186+
* Identity → tenancy key: `issuer + "/" + subject` hash, as v1, so the same human gets
187+
the same boundary on re-bind.
188+
189+
### 5. Reaper (provider-side, optional)
190+
191+
The core leaves dead-consumer GC explicitly to this layer, keyed off the per-Connection
192+
`Lease` the konnector maintains. **This component is blocked on that core primitive:** the
193+
konnector's provider-side `Lease` is specified in the core proposal but is new (no v1
194+
equivalent), so the reaper ships only once the konnector actually maintains the Lease;
195+
until then dead-consumer GC is manual.
196+
197+
* Lease expired beyond TTL → mark the issuance stale → (configurably) revoke
198+
credentials, then delete kbind-created namespaces and synced objects.
199+
* TTLs and the destructive step are opt-in and conservative by default (revoke ≠
200+
delete; deletion requires explicit enablement).
201+
202+
### 6. CLI (`kubectl bind`)
203+
204+
Thin client over the gateway; everything it does is reproducible by hand:
205+
206+
```sh
207+
kubectl bind login https://mangodb.example.com # auth, cache token
208+
kubectl bind catalog # list Exports/Collections
209+
kubectl bind export mangodb # bind an Export:
210+
# POST /api/bind → bundle
211+
# → kubectl apply (or -o yaml)
212+
kubectl bind export mangodb -o yaml > binding.yaml # GitOps mode: print, don't apply
213+
```
214+
215+
* Every CLI action is an explicit subcommand (`login`, `catalog`, `export`); export
216+
names never occupy the subcommand slot — `kubectl bind export mangodb`, not
217+
`kubectl bind mangodb` — so command names and export names cannot collide.
218+
* `--install-konnector` (default on for interactive use) installs/upgrades the v2
219+
konnector, as v1 did.
220+
* The CLI never creates bespoke objects — it applies the gateway's bundle verbatim.
221+
`-o yaml` output committed to git is byte-for-byte the GitOps path.
222+
* v1 subcommands that existed to ferry the old handshake (`apiservice`, `deploy`,
223+
per-resource polling) disappear.
224+
225+
### 7. UI (SPA)
226+
227+
Browse catalog → authenticate → bind → then either:
228+
229+
* **download the bundle** (via the one-time pickup URL) / copy a
230+
`kubectl bind export` one-liner, or
231+
* **browser-apply** (v1's UI-only flow, roadmap #406, kept): the user supplies a
232+
consumer-cluster kubeconfig (or the UI runs in-platform where one is already held),
233+
and the gateway's `/api/apply` applies the bundle and installs the konnector into the
234+
consumer cluster.
235+
236+
The UI is a pure gateway client; it holds no flow state the gateway doesn't have.
237+
Browser-apply is flag-gated on the gateway and off by default — it means consumer
238+
credentials transit the gateway, which deployments must consciously accept.
239+
240+
## Packaging & repo
241+
242+
Per the frozen core layout: `v2/backend` (gateway + issuer + reaper + auth), `v2/cli`,
243+
`v2/web`. The backend ships as **one binary with module flags** (`--enable-gateway`,
244+
`--enable-issuer`, `--enable-reaper`, `--enable-apply`) — operational simplicity over
245+
purity; the boundaries stay as Go packages so a future split costs a `main.go`, not a
246+
refactor. All of it depends on `v2/sdk` only. The kcp distribution (`contrib/kcp`)
247+
remains separate, providing its own issuer implementation behind the same interface.
248+
249+
## Migration notes
250+
251+
* The v1 wire protocol is not bridged: v1 CLI cannot talk to a v2 gateway. Both stacks
252+
can run side by side on one provider during transition (different endpoints, disjoint
253+
CRD groups).
254+
* v1 catalog objects (`APIServiceExportTemplate`/`Collection`) translate mechanically
255+
to `Export`/`Collection`; a converter script ships with the backend.
256+
257+
## Decided
258+
259+
* **Packaging**: one `kbind-backend` binary; gateway/issuer/reaper/apply are module
260+
flags, boundaries kept as Go packages.
261+
* **Issuance anchor**: `Grant` in `iam.kbind.dev` — the typed record of
262+
"identity X was issued credentials Y for export Z"; anchor for revocation, audit,
263+
reaper. Kept out of `catalog.kbind.dev` so that group stays purely presentation+defaults.
264+
* **Credentials**: long-lived secret-based SA token (v1 behavior) — zero rotation
265+
friction accepted over security posture; revocation via `Grant` deletion; bounded
266+
tokens addable later behind the same issuer interface without API change.
267+
* **Catalog vocabulary**: `Export` + `Collection`.
268+
* **Bundle delivery**: one-time pickup URL, 5-minute TTL, single use — the TTL/single-use
269+
applies to the *pickup URL*, not the long-lived SA token inside, so a picked-up bundle
270+
stays re-appliable (GitOps-safe). The bundle is never stored at rest in the gateway.
271+
* **kcp**: stays a separate distribution (`contrib/kcp`) providing its own issuer
272+
implementation; the in-tree backend issuer is plain Kubernetes only.
273+
* **UI reach**: browser-apply path **kept** (roadmap #406) — gateway `/api/apply`
274+
applies bundle + installs konnector into the consumer cluster with a caller-supplied
275+
kubeconfig; flag-gated, off by default, consumer credentials transiting the gateway
276+
is an explicitly accepted trade-off when enabled.
277+
* **Federation**: one gateway = one provider; cross-provider aggregation is a future
278+
layer above the bundle protocol.
279+
* **CLI verb structure**: every action is an explicit subcommand; binding an export is
280+
`kubectl bind export <name>`, never a bare `kubectl bind <name>` — export names and
281+
command names must not share a namespace (review feedback, @sttts).
282+
283+
## Open questions
284+
285+
None — initial design questions resolved (see **Decided**). New questions raised during
286+
review go here.

0 commit comments

Comments
 (0)