You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(infra): template images as schema defaults + kro-acceptance e2e/CI (#369)
* feat(infra): sandbox-runner images as schema defaults (drop ${kedge.*} tokens)
The sandbox-runner template was the lone outlier using ${kedge.*}
env-substitution tokens for its container images (Phase 2a) — every
other template declares configurable images as spec.schema fields with
sane defaults and hardcodes fixed sidecar images. That outlier broke in
practice: an unset KEDGE_SANDBOX_RUNNER_IMAGE substituted to "" and kro
authored a Deployment/Job with `image: ""` (Required value).
Make sandbox-runner follow the same convention as simple-webapp / database
/ redis / application:
- spec.runnerImage is a schema field with a sane default
(ghcr.io/faroshq/kedge-sandbox-runner:dev); the runner Deployment uses
${schema.spec.runnerImage}. Overridable per instance.
- the control-token Job image is hardcoded bitnami/kubectl, like every
other template's control job (tokenGeneratorImage field dropped).
Remove all the now-dead image-token machinery:
- kro backend: drop the sandbox image tokens (only the exposure Gateway
tokens remain).
- infra chart: drop KEDGE_SANDBOX_*_IMAGE env, the install guard, and the
sandboxRunner values block; operator passthrough.
- App Studio: sandboxRunnerValues supplies only {projectRef}; drop the
image env helpers, chart guard/env/values, README rows.
Document the convention so this doesn't recur:
- new providers/infrastructure/docs/template-conventions.md (when to use a
schema default vs a hardcoded literal vs a ${kedge.*} token, with the
failure modes that motivated it).
- AGENTS.md §9 guardrail pointing at it.
Also: dev postgres images default to Google's Docker Hub mirror
(mirror.gcr.io/library/postgres:16-alpine) — Docker Hub has been dropping
pulls with "unexpected EOF".
Preview routing is unchanged (it was already schema-based on main).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(infra): e2e that every template's RGD is accepted by kro + CI gate
Unit tests only check buildRGD's output; they miss whether kro actually
ACCEPTS the authored graph — which is exactly the class of bug the
sandbox-runner template hit (an integer field fed a string, an
includeWhen that wasn't a standalone CEL expression). Add an e2e that
closes that gap for every seeded template.
- providers/infrastructure/backend/kro/e2e_test.go (build tag `e2e`):
for each install/templates/*.yaml, build the real RGD via buildRGD
and assert kro reports GraphAccepted=True. Reuses the package's
decodeTemplate/buildRGD/testTokens. Skips when KUBECONFIG is unset,
so it never runs in the unit pass.
- make e2e-infrastructure: stands up a throwaway kind cluster with the
Gateway API CRDs (templates declare HTTPRoute/ReferenceGrant) and
STANDALONE kro (multicluster.enabled=false — no kcp needed; graph
validation is independent of the watch source), then runs the test.
Split into -up / -run / -down for reuse.
- .github/workflows/infrastructure-templates-e2e.yaml: runs it on PRs
touching the templates, the kro backend, or the provider apis.
Validated end to end: a fresh kind + standalone kro accepts all five
templates (application, database, redis-cache, sandbox-runner,
simple-webapp).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(infra): e2e also creates an instance of each template
The template e2e proved only that kro ACCEPTS each authored RGD
(GraphAccepted=True). Extend it to also CREATE a sample instance per
template and assert kro reconciles it WITHOUT an apply error.
This is the exact failure that motivated the schema-default image
convention ("apply results contain errors: ... image: Required value"):
RGD acceptance alone never exercised it, because the empty-image fault
only surfaces when kro applies a child Deployment. The test now catches
it.
Per template: build an instance from sampleValues when present, else a
minimal valid spec (e2eMinimalSpecs); retry Create until the fresh CRD
is servable; poll status conditions and fail on any apply-error marker
(apply results contain errors / is invalid / Required value / failed to
apply / admission webhook). Readiness is out of scope — images need not
pull in CI; apply validates the spec.
Validated end-to-end on a fresh standalone kro kind cluster (ok 20.7s,
all five templates).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* ci(infra): reflect instance-creation coverage in template e2e workflow
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(infra): verify kro creates each template's child objects
The e2e proved kro accepted the RGD and the instance reported no apply
error, but never checked the runtime cluster for the objects kro is
supposed to create. Add that: per template, after the instance
reconciles, List every child the RGD declares (matched by kro's
kro.run/instance-id + node-id labels) and assert the always-on ones
exist; resources gated by includeWhen (e.g. the preview HTTPRoute) are
verified only when present.
This caught a real gap: application's HTTPRoute references
spec.expose.fqdn, a field the platform stamps onto the instance ("do
NOT set" in the sample) — with no controller in the standalone-kro e2e,
the route could never render. The test now stands in for the platform
via e2ePlatformStamped (supplies expose.fqdn + credentialsSecretName),
so the HTTPRoute is created and verified.
Instance names are made per-run-unique (a runID nonce; the 16-hex form
satisfies sandbox-runner's name pattern) so re-running against a reused
cluster can't collide with a prior run's not-yet-GC'd child objects.
Validated on a fresh standalone kro cluster: all five templates create
their declared objects (ok 27.6s) — application now incl. HTTPRoute×2.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs: name the provider-isolation rule (no cross-provider backend access)
Providers must not reach into another provider's backend layer (its
runtime/target clusters and credentials, databases, internal Services,
controllers, kro RGDs). All cross-provider interaction goes through the
owning provider's published API: its kcp APIExport resources (control
plane) and virtual-workspace subresources (data plane), invoked as the
tenant user and routed by binding — never by a hardcoded backend URL.
The rule was already practiced everywhere (the App Studio -> infrastructure
runtime-decoupling is the worked example) but never named. Make it explicit
and link the docs that embody it to one canonical statement:
- docs/providers.md: new "Provider isolation (the cross-provider boundary)"
section + a Security bullet.
- docs/provider-connectivity-contract.md: new Contract 3 (provider-to-
provider) alongside the existing platform-facing contracts 1 and 2, with
a conformance table and checklist item.
- AGENTS.md: a guardrail bullet so new work follows it by default.
- Reinforcement notes (linked, not duplicated) in infrastructure-,
kuery-, mcp-architecture, app-studio-runtime-decoupling,
app-studio-sandbox-runtime, and the infrastructure/app-studio READMEs.
Docs only; no code or behavior change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
> (contract 3 in [`provider-connectivity-contract.md`](./provider-connectivity-contract.md)):
66
+
> App Studio's `APP_STUDIO_RUNTIME_KUBECONFIG` is a direct credential into
67
+
> another provider's backend — exactly what the rule forbids — and this
68
+
> design replaces it with calls to the infrastructure provider's published
69
+
> subresources.
70
+
63
71
1.**The runtime cluster has exactly one owner: the infrastructure provider.** Whoever materializes a workload also serves its data plane. No other component holds a runtime credential.
64
72
2.**Data-plane verbs are subresources on the workload instance.**`sandboxrunners/{name}/log` is to a `SandboxRunner` what `pods/log` is to a Pod. This keeps the model k8s-native and routable by binding.
65
73
3.**Routing is binding-driven, never URL-driven.** App Studio never resolves a provider backend URL. It calls a subresource on a resource it is already bound to; kcp routes to the provider that exports it. This is the BYO mechanism.
Copy file name to clipboardExpand all lines: docs/infrastructure-architecture.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -42,6 +42,7 @@ Same user, same workspace, two different namespaces. The instance is real, the p
42
42
3.**Backends implement a narrow interface, not a data model.** Each backend is responsible for taking a `Template` and serving instances of the corresponding per-template CRD. *How* it does that — RGDs, terraform modules, cloud SDK calls — is its own business.
43
43
4.**kcp is the source of truth for identity.** Tenant = workspace logical-cluster name, embedded in the URL. No headers. No hashing.
44
44
5.**The provider binary owns the platform layer + the backends it ships with.** Today that's one backend (kro). The binary stays small; complexity lives in backends behind the interface.
45
+
6.**The backend layer is private to this provider; other providers reach it only through published APIs.** The runtime/target clusters a backend drives, the credentials to them, the kro RGDs, the internal Services — none of it is reachable by another provider. A consumer (e.g. App Studio) interacts with infrastructure-owned workloads **only** through the `Template`/instance CRs (control plane) and their VW subresources (data plane: `sandboxrunners/{name}/log`, `…/proxy/…`), as the tenant user. No other component holds a credential into the runtime cluster. This is the platform-wide [provider-isolation rule](./providers.md#provider-isolation-the-cross-provider-boundary); the worked example is [`app-studio-runtime-decoupling.md`](./app-studio-runtime-decoupling.md).
Copy file name to clipboardExpand all lines: docs/mcp-architecture.md
+5Lines changed: 5 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -172,6 +172,11 @@ Two consequences:
172
172
[`providers/infrastructure/tenant/client.go`](https://github.com/faroshq/kedge/blob/main/providers/infrastructure/tenant/client.go): the tenant client is
173
173
built per-(tenant, caller) from the request token; the provider's own
174
174
credentials are never used for tenant work.
175
+
-**Federation routes to published endpoints, not backends.** The aggregator
176
+
reaches each provider through its registered `BackendURL`/`/mcp` surface
177
+
with the caller's identity forwarded — never into the provider's runtime
178
+
cluster, DB, or internal Services. This is the cross-provider half of the
## The hub plumbing (and what it does with the token)
@@ -215,6 +246,22 @@ keeps only the CA, and authenticates with the **caller's** bearer token against
215
246
216
247
---
217
248
249
+
## Contract 3 conformance — provider-to-provider via published API only
250
+
251
+
| Interaction | Mechanism | Verdict |
252
+
|----------|-----------|---------|
253
+
|`app-studio` → `infrastructure` (sandbox data plane) |**Target:**`SandboxRunner` CR (control plane) + `sandboxrunners/{name}/{log,proxy,sync,restart}` VW subresources (data plane), called as the tenant user | ✅ Conforms (after runtime-decoupling) |
254
+
|`app-studio` → runtime cluster (legacy) | held `APP_STUDIO_RUNTIME_KUBECONFIG`, a direct second credential into infrastructure's runtime cluster | ❌ The violation being removed — see [`app-studio-runtime-decoupling.md`](./app-studio-runtime-decoupling.md)|
255
+
|`kuery` → `Edge`s | reads bound `Edge` CRs / engages clusters through the hub's edges-proxy as the caller, never the edge provider's backend | ✅ Conforms |
256
+
257
+
The shape that conforms: address a **bound resource** (or a subresource on
258
+
it), authenticate as the **caller**, and let the binding decide which
259
+
provider's backend answers. The shape that doesn't: a kubeconfig / DSN / URL
260
+
in provider A's config that points straight at provider B's cluster, DB, or
261
+
internal Service.
262
+
263
+
---
264
+
218
265
## Known divergences (and why)
219
266
220
267
1.**Built-ins are privileged by construction.**`mcp`, `kubernetesedges`, and
@@ -249,6 +296,10 @@ keeps only the CA, and authenticates with the **caller's** bearer token against
249
296
drop the provider's own credential.
250
297
-[ ] Never trust inbound `X-Kedge-*` headers in the provider — the backend
251
298
proxy strips and re-injects them; treat them as hub-asserted only.
299
+
-[ ] To reach **another provider**, bind its `APIExport` and call its CRs /
300
+
VW subresources as the caller (contract 3). Never hold a credential
301
+
into another provider's backend (runtime cluster, DB, internal
0 commit comments