feat(infrastructure,app-studio): enforce retirement of removed platform templates#416
Merged
Conversation
…rm templates Seeding only upserts the CURRENT catalog, so a workspace initialized before a template was retired keeps serving it forever — visible in the portal, selectable by agents, backed by code paths that no longer exist. sandbox-runner (replaced by template-native development mode in #394) is exactly that ghost on any long-lived deployment. Add a retirement mechanism to the Template controller: templates whose names are on the retired list (retired.go — sandbox-runner, sandbox-preview-httproute) are deleted on sight, and the existing finalize chain (backend teardown → APIExport entry removal → per-template CRD deletion) dismantles everything they authored. Because it runs in the watch loop, a re-applied retired template is swept again — retirement is enforced, not a one-shot migration. The check sits after the finalizer add (teardown guaranteed) and before the backend lookup (a retired template with an unregistered backend still gets swept). Pinned by TestReconcileRetiredTemplateIsSwept. Also sweep the stale prose the retirement leaves behind: - Project CRD: spec.template's description claimed an empty value "keeps the legacy SandboxRunner development binding" — empty now means no development environment. Fixed in types_project.go and regenerated (config/crds, apiresourceschema, chart schema). - Factually wrong comments: the infra chart claimed the node dev-image default is "the kedge sandbox-runner image" (it is docker.io/library/node:22-bookworm); the kro backend documented a ${kedge.sandboxPreviewBaseDomain} token that no code mints; dataplane route examples and app-studio runtime comments referenced the SandboxRunner kind; template-conventions.md named the database template "postgres-database". All updated to current names — genuinely historical notes (e.g. "the proven sandbox-runner pattern") are kept. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR enforces retirement of removed infrastructure “platform templates” by having the infrastructure Template controller automatically delete any Template whose name is on a retired list, ensuring long-lived workspaces don’t keep serving ghost templates. It also sweeps stale references to the retired sandbox-runner era across infrastructure/app-studio docs, charts, and API/CRD prose.
Changes:
- Add an infrastructure Template “retirement” mechanism that deletes retired templates on sight and relies on the existing finalizer cleanup path.
- Add a unit test pinning the retirement behavior, including re-apply enforcement.
- Remove/replace stale sandbox-runner references in charts, comments, and Project CRD/API descriptions; regenerate app-studio schemas.
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| providers/infrastructure/docs/template-conventions.md | Updates examples to reflect current template names/conventions. |
| providers/infrastructure/deploy/chart/values.yaml | Fixes chart comments about dev image defaults and schema-field conventions. |
| providers/infrastructure/dataplane/handler.go | Updates dataplane URL examples/comments to non-retired resources. |
| providers/infrastructure/dataplane/contract.go | Updates contract doc comment example resource. |
| providers/infrastructure/controller/template/retired.go | Introduces the retired template name list and rationale. |
| providers/infrastructure/controller/template/controller.go | Enforces retirement early in reconcile by deleting retired templates on sight. |
| providers/infrastructure/controller/template/controller_test.go | Adds unit coverage for retirement + re-apply enforcement. |
| providers/infrastructure/backend/kro/e2e_test.go | Removes outdated sandbox-runner-specific naming commentary. |
| providers/infrastructure/backend/kro/backend.go | Updates token/env-var documentation comment to match current behavior. |
| providers/app-studio/deploy/chart/values.yaml | Updates chart comments to reflect template-instance-based development dataplane. |
| providers/app-studio/deploy/chart/files/schemas/projects.ai.kedge.faros.sh.yaml | Regenerated schema with updated Project.template description. |
| providers/app-studio/config/kcp/apiresourceschema-projects.ai.kedge.faros.sh.yaml | Regenerated APIResourceSchema with updated Project.template description. |
| providers/app-studio/config/kcp/apiexport-ai.kedge.faros.sh.yaml | Updates APIExport schema reference to the regenerated schema name. |
| providers/app-studio/config/crds/ai.kedge.faros.sh_projects.yaml | Regenerated CRD with updated Project.template description. |
| providers/app-studio/apis/ai/v1alpha1/types_project.go | Fixes Project.template field description to reflect current semantics. |
| providers/app-studio/api/development_sync.go | Updates comments to remove sandboxrunner-specific wording. |
| providers/app-studio/api/assistant_workflow.go | Updates comments to reflect generic “development runtime” terminology. |
| providers/app-studio/api/assistant_runtime_tools.go | Updates comments to reflect generic dev-agent/development dataplane terminology. |
Comment on lines
+18
to
+22
| // reconciler deletes any Template on this list on sight; the normal finalize | ||
| // chain (backend teardown → APIExport entry removal → per-template CRD | ||
| // deletion) then dismantles what it authored, exactly as if an operator had | ||
| // deleted it by hand. Because this runs in the watch loop, a re-applied | ||
| // retired template is removed again — retirement is enforced, not one-shot. |
Comment on lines
+121
to
+125
| // Retired platform templates are deleted on sight (see retired.go). | ||
| // AFTER the finalizer add so the deletion runs the full finalize chain, | ||
| // BEFORE the backend lookup so a retired template whose backend is no | ||
| // longer registered still gets swept instead of parking on a | ||
| // BackendNotFound condition. |
Comment on lines
+268
to
+272
| var post infrav1alpha1.Template | ||
| if err := r.Client.Get(context.Background(), types.NamespacedName{Name: "sandbox-runner"}, &post); err == nil { | ||
| t.Fatalf("retired template still present after reconcile (deletionTimestamp=%v, finalizers=%v)", | ||
| post.DeletionTimestamp, post.Finalizers) | ||
| } |
Comment on lines
+281
to
+283
| if _, err := dyn.Resource(crdGVR).Get(context.Background(), perTemplateCRDName(tmpl), metav1.GetOptions{}); err == nil { | ||
| t.Fatal("per-template CRD exists for a retired template") | ||
| } |
Comment on lines
+303
to
+305
| if err := r.Client.Get(context.Background(), types.NamespacedName{Name: "sandbox-runner"}, &post); err == nil { | ||
| t.Fatal("re-applied retired template survived reconciliation") | ||
| } |
mjudeikis
added a commit
that referenced
this pull request
Jul 11, 2026
…ider suites (#417) * test(e2e): infrastructure provider subprocess suite + CI job for provider suites The infrastructure provider had e2e coverage only for its kro side (the kind-based template e2e). Its kcp side — provisioning, init bootstrap, the Template controller, the tenant catalog — ran only in unit tests or against a manually-started tilt stack. And the existing quickstart subprocess suite (make e2e-provider) was wired into no CI workflow at all, so neither suite ever ran automatically. Add suites/infraprovider, modeled on suites/provider: kedge-hub with embedded kcp + the infrastructure provider (init, then serve) as host subprocesses. No kind/Helm/Docker/kro — with KRO_KUBECONFIG unset only the stub backend registers, which is exactly what lets the suite drive the Template controller end-to-end without a runtime cluster: - TestABootstrapSeedsCatalog — init seeded every current template; the platform APIExport exists. - TestBStubTemplateFullReconcile — a backend:stub Template reaches Ready=True with its per-template CRD established and the APIExport resources entry added, and deletion runs the finalize chain back down. - TestCRetiredTemplateIsSwept — a retired platform template applied into the live workspace (the pre-retirement deployment state) is deleted by the controller without operator action (#416's reconciler, now covered against real kcp). - TestDProvidersDTO — the hub lists the provider for a logged-in user. - TestETenantSeesTemplatesCatalog — APIBinding in the static user's workspace; the seeded templates are listable through the binding (the read App Studio's picker and MCP list_templates perform). Wire both subprocess suites into CI as one sequential job (they share the embedded kcp's fixed etcd port 2380) in e2e.yaml, and add the make e2e-infra-provider target with the same port guard the quickstart suite uses. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(e2e): serve with the init-minted SA kubeconfig, not admin Review feedback: running serve with the kcp admin kubeconfig bypasses the init/serve split's RBAC — the suite would mask permission regressions in the controller chain. init already mints the workspace-scoped ServiceAccount kubeconfig; serve now uses it, exactly like the chart's serve container. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(e2e): harden infraprovider suite per review - Handle os.Create errors for hub/init/provider logs (a failed create nil-panicked on .Name() and hid the root cause). - Surface SetNestedField errors when overriding the CatalogEntry URLs. - Per-attempt HTTP timeouts inside waitForCondition loops (an unbounded DefaultClient request could hang the whole suite). - Bounded contexts in t.Cleanup deletes so teardown can't hang the run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(e2e): retry manifest apply until catalog APIs are servable First CI run of the provider suites flushed out a startup race the suites' own login helper already documents: the hub reports /readyz before the catalog APIs in the providers workspaces are fully servable, so on a slow runner the first CatalogEntry/Provider Create fails with "the server could not find the requested resource" and cascades through the whole quickstart suite. Retry the applies (90s bound) in both the quickstart and infraprovider suites instead of failing on the first attempt. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(e2e): descope the stale quickstart suite from the CI job Its first-ever CI run revealed the quickstart suite predates the provider bootstrap refactor: it applies only the CatalogEntry, into root:kedge:providers, while the current flow applies Provider + CatalogEntry into root:kedge:system:providers and delegates the APIExport/schemas/bind-grant to `quickstart-provider init` — so every test from catalog provisioning to tenant enable fails against today's hub. Modernizing it onto the same bootstrap the infraprovider suite uses is queued as a follow-up; until then the CI job runs the infraprovider suite only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(infrastructure): grant the runtime SA delete on templates + CRDs The infraprovider e2e suite's first run (serving with the init-minted SA kubeconfig instead of the hub-minted cluster-admin one) exposed a real RBAC gap: the runtime ClusterRole granted no delete verb anywhere, so the retired-template sweep (controller deletes Templates on sight) and the finalize chain's per-template CRD deletion both 403 — the features work only under the cluster-admin hub-minted kubeconfig. Grant delete on templates and customresourcedefinitions; the role is idempotently overwritten on init, so existing deployments pick this up on their next init run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Seeding only upserts the current catalog, so a workspace initialized before a template was retired keeps serving it forever — visible in the portal, selectable by agents, backed by code paths that no longer exist.
sandbox-runner(replaced by template-native development mode in #394) is exactly that ghost on any long-lived deployment.What
Retirement reconciler (
controller/template/retired.go): Templates whose names are on the retired list (sandbox-runner,sandbox-preview-httproute) are deleted on sight by the Template controller, and the existing finalize chain (backend teardown → APIExport entry removal → per-template CRD deletion) dismantles everything they authored — exactly as if an operator had deleted them by hand. Because it runs in the watch loop, a re-applied retired template is swept again: retirement is enforced, not a one-shot migration.Placement details:
BackendNotFound.Pinned by
TestReconcileRetiredTemplateIsSwept, including the re-apply case.Stale-prose sweep (follow-through on the retirement):
spec.templatedescription claimed empty "keeps the legacy SandboxRunner development binding" — empty now means no development environment. Fixed intypes_project.go+ regenerated schemas (make codegen-app-studio-provider).docker.io/library/node:22-bookworm); the kro backend documented a${kedge.sandboxPreviewBaseDomain}token no code mints; dataplane route examples used the retired kind;template-conventions.mdcalled the database templatepostgres-database.Verification
go build,go vet(incl.-tags e2e), and fullgo test ./...pass for both providers; app-studio schema regen is included and clean.🤖 Generated with Claude Code