Repository navigation
ui: rebuild the system templates from the Templates page - #1278
Conversation
|
Thanks for opening this pull request! A SPACE maintainer will take a look. CONTRIBUTING.md explains how we review outside contributions. |
Rebuilding seed and code-runtime needed the admin CLI, and the console did not show which spaced guest versions sandboxes run. "Rebuild system templates", above Template Releases and for administrators of the managing organization, walks the seed README's release procedure one explicit step at a time. Nothing is built, created or promoted unless the operator clicks it in this tab: - Build seed through the system-template build API, then poll it. - When it succeeds, build code-runtime on seed@<digest>, then poll that. Neither build carries publish tags, so each registers a release named only by its digest. - For each new release, create a test sandbox from the pinned reference (seed@<digest>, code-runtime@<digest>), through the same public create route the Sandboxes page uses. The link to it opens its page, which shows the spaced version its guest reports. - Promote to latest, after a confirmation that reads latest fresh, shows what moves from which digest to which, and needs an explicit "promote anyway" when latest changed since the run started. The previous digests are recorded when the operator confirms, before the first tag request, and never again; afterwards the tags are read back and the result shown, since the tag route can commit and still answer an error. A failed promotion offers a retry, which re-reads the tags first. The run is kept in the browser's storage, keyed by cluster and user, for at most a day (the builder keeps job status for four), so a reload or another tab shows it and keeps polling. Status answers apply only to the run and job that asked for them, polling backs off while the status route fails, Dismiss is always available, and a step's button disappears once the run has a job for it. A build request that fails with a 5xx or a network error is reported as an unknown outcome, not as a build that did not start. "spaced in running sandboxes" counts the running sandboxes in the admin list's newest rows by template and reported spaced version, and says what it covers: the newest rows listed and the oldest one's creation time. The seed README's rollout section said sandbox-manager refuses templates whose compatibility family does not match its own. It admits guests whose version is in SUPPORTED_SPACED_GUEST_VERSIONS and whose family agrees with that version; host and guest families need not match. The section now says so and points to the versioning policy.
…nto empty steps
- A promotion record keeps only what it was asked to move (the targets)
and the rollback target (latest when the operator confirmed). Whether
it is complete is read from the tags on load, after Refresh and after
each attempt: complete when every latest is its target, otherwise
what still has to move, with Retry, and any latest someone moved to a
third release, which a retry asks before overwriting. Before, a
reload between saving the record and reading the tags back left a
Retry that did nothing, and an interrupted retry showed success.
- Confirming re-reads the tags; if anything changed since the dialog
showed its plan, it shows the new plan and asks again. The dialog
closes when its run, or the digests it would promote, change in
another tab, and a confirm for another run is refused. latest when a
run starts comes from a fresh read, not the page's cached list.
- No tag request is sent unless the promotion record was saved.
- Every change to the stored run is a read-modify-write under a lock the
browser's tabs share (the Web Locks API), so two tabs' writes do not
interleave. A build or test-sandbox answer is recorded only into the
still-empty step it was sent for. When another tab's click got there first, the
extra job or sandbox is shown ("another click also started job X",
"also created sandbox Y; delete it") instead of replacing it.
- Every request that changes something, and every write of an answer,
first checks that the run still exists and has not expired, so a run
dismissed in another tab stays dismissed, and the panel clears when
the run expires.
- The spaced coverage line counts database rows only for its N and its
"created since" time, and mentions the node-reported rows (such as
warm-pool slots) the list appends separately; the cap warning shows
even when no counted row is left.
- A failed test-sandbox create with an unknown outcome says to look for
its name in the Sandboxes list; an unknown build outcome says an
accepted build appears only when it finishes, and that building again
starts a second job.
1f738b4 to
0073ada
Compare
…de the run - A build the run recorded no longer shows "The rebuild was dismissed or expired meanwhile": that text is for an answer that found no run. The notice for each outcome comes from one function per kind, tested. - A promotion record names both templates: both targets, and both templates' latest as read just before the first tag request. Only the tag requests skip a template already at its target. A later change to that template reads as moved by someone, and a retry asks before moving it. - When every latest that can move back is on the release the record names as before, the panel says latest is back on it and offers "Promote again", with the same confirmation, instead of "incomplete" and Retry. The tags cannot tell a rollback from a tag request that never committed, and the panel says so. - Without the Web Locks API (plain http off localhost), writes of the stored run take the short-lived localStorage lock from view-refresh. Dismiss, and dropping an expired or unreadable record, go through the same lock and re-read inside it. - Late outcomes (an extra job or test sandbox, one that arrived after the run was dismissed, a test sandbox whose outcome is unknown) show above the run panel and outlive Dismiss until closed. Notes about the run, its poll errors and the last attempt's note clear when the run changes. - The release list applies only the newest read's answer, so a slower, older one cannot bring back a state that has changed. - A new run that this browser cannot save is not started; the page says so instead of offering steps the first click would drop.
zibi-pplx
left a comment
There was a problem hiding this comment.
The flow is careful about interruption: completion is read back from fresh tags, an incomplete promotion is detected, and Retry asks first when a tag has diverged. That leaves two questions about what promotion has to guarantee.
-
[medium] Does promoting seed and code-runtime need to be atomic?
ui/src/app/dashboard/templates/system-template-rebuild.tsx:461moveslatestwith onePUT /v1/template-releases/{id}/tagper template, in sequence. Between the two writes, or after a failure the operator never retries, seed'slatestnames the new release and code-runtime's names the old one. The re-read before writing narrows the race, but it can't stop a concurrent writer, because the tag route has no compare-and-set. If a mixed pair is acceptable (each template stands alone), a sentence in the seed README's release section saying so would settle it. If not, the fix belongs on the server: a route that moves both tags in one transaction, or an expected-current-release precondition on the tag route. Either way, a tracking issue would help. -
[low] The rollback target lives only in this browser.
ui/src/lib/system-template-rebuild.ts:15,35:Promotion.previousis stored in localStorage for this cluster and user, and is dropped afterRUN_MAX_AGE_MS(one day). Another operator, another browser, or the same operator a day later can't see whatlatestnamed before. The admin audit log records the release ID each tag request moved to, but not the one it replaced. Template Releases still allows recovery by hand. Suggestion: show the previous digests prominently and make them copyable in the success panel, so the operator can record them. Should promotions eventually be recorded server-side? -
[low] Would an idempotency key on the build route simplify this?
system-template-rebuild.tsx:351:POST /v1/system-templates/{template}/buildstakes noIdempotency-Key, so two tabs that both click Build seed start two builder jobs, and the page has to name the extra one. If that route accepted the run ID as an idempotency key, the cross-tab "extra job" handling could be removed. This is a server change and fits a tracking issue rather than this PR.
Withdrawing this approval for now. Any feedback above still stands.
After a promotion the panel named the releases latest pointed at before it only as short hashes inside a sentence. Those digests are the rollback target, and the run that holds them expires a day after it started and lives only in this browser. The success panel, the incomplete-promotion panel and the promote-again dialog now list each template's previous latest as a full digest with a copy button, and say to record them before the record expires.
The seed README's release section said promoting the two system templates is not atomic. It now also says what that leaves: between the two tag moves, or after one fails, seed's latest can name the new release while code-runtime's still names the old one, and that pair is consistent because each code-runtime release was built on a pinned seed digest.
Review — 2 findingsLow
Verified against What I ran: What I traced in the API, not just the UI:
What I didn't check: anything in a browser. I didn't run the Playwright loops, the two-tab races, or a real code-runtime build. |
the-gigi-pplx
left a comment
There was a problem hiding this comment.
Approved. 2 non-blocking findings in the review comment above.
Summary
Operators can now rebuild the system templates,
seedandcode-runtime, from the Templates page, check what a new release carries before promoting it, and see whichspacedguest versions the running sandboxes report, per template. Before, a rebuild needed the admin CLI, and nothing in the console showedspacedversions outside a single sandbox's page.POST /admin/api/v1/system-templates/seed/builds), then the page polls it.seed@<digest>(base: "seed@<digest>"), on that exact seed, then the page polls that. Neither build carries publish tags, so each registers a release named only by its digest.seed@<digest>,code-runtime@<digest>) through the public create route the Sandboxes page's form uses. The panel links to it: its page shows thespacedversion its guest reports, and whether it boots on this cluster. The panel reminds the operator to delete it.latestof both templates fresh, and shows what moves from which digest to which. Iflatestchanged since the run started, it says so, and Promote waits for an explicit "Promote anyway". On Promote, the page reads the tags again; if what would move is no longer what the dialog showed, it shows the new moves and asks again. Before the first tag request it saves the promotion record, for both templates: the digestslatestnamed (the rollback target the panel names) and the digests it moves to. That includes a template whoselatestalready names its new release; only the tag requests leave it out. The first confirmation writes the record and a retry keeps it. If this browser cannot save it, no tag request is sent and the dialog says why.latestnames the new releases, says which template still has to move, and says when someone movedlatestto a third release. A retry plans only what still has to move, reads the tags again, and asks before overwriting alatestsomeone else moved. Nothing retries by itself. The tag route has no compare-and-set, so a change made between the page's final read and a tag request is not detected; the dialog says so.latestthat can move back is on the release the record names as before, as after a rollback with the tag dialog, the panel sayslatestis back on it and offers Promote again…, with the same confirmation. The tags do not tell a rollback from a tag request that never committed, and the panel says so.latestto the releases it named before, with the existing tag dialog. The success panel lists those releases under "Rollback target", each template's as a full digest with a copy button, and says to record them: the run that holds them expires a day after it started and lives only in this browser. The incomplete-promotion panel and the promote-again dialog show the same list. A template that had nolatestbefore is listed as keeping the new release, because the API does not deletelatest.rebuild-check-<template>-<run>) in the Sandboxes list before creating another.spacedversion their guest reports, sampled from the admin sandbox list (its newest rows, at most 5,000, with deleted ones left out). A line says what that covers: "running sandboxes among the newest N listed, created since <the oldest listed row's creation time>", where N and that time come from the list's database rows only. Rows that nodes report without a database row, such as warm-pool slots, are named in a separate sentence. When the list reached its limit, the line says that long-lived sandboxes created earlier are not counted, even when none of the listed rows is a sandbox that is not deleted. A sandbox that reports no version counts as "unknown", and a template with no running sandbox among them does not appear. A failed refresh is shown even when the previous counts remain. A server-side count by template and version would need a backend change.SUPPORTED_SPACED_GUEST_VERSIONS(crates/libs/spaced/common/src/compatibility.rs) and whose family agrees with that version; host and guest families do not have to match. The section now says so, without copying the range, and links to the guest-compatibility section ofdocs/versioning.md. It also no longer says the stamp comes from the workspaceCargo.toml: it is the version of the guest binaries the build packages. Its "Build and publish system templates" section now also says that between the two tag moves, or after one fails,seed'slatestcan name the new release whilecode-runtime's still names the old one, and that this pair is consistent because eachcode-runtimerelease was built on a pinned seed digest.No service, API or schema changes.
Stack
Stacked on #1255 (templates first on the Templates page), which also rewrites the Template Releases header. Merge #1255 first.
Validation
npm ci,npm test(68 pass),tsc --noEmitandnpm run buildinui/. The new tests are in two files:src/lib/system-template-rebuild.test.mjs: which step is offered; status answers applied only to their run and job; answers accepted only into an empty step, and the extra one reported; no notice for an answer the run recorded, and the notice for an extra or late one; polling backoff; unknown outcomes for a build and for a test sandbox; the promotion plan, its "changed since the run started" and "moved by someone" flags, and the comparison that makes a changed plan ask again; the rollback target captured once and kept by a retry; a record that names both templates, including one already at its target, whose later change reads as moved by someone; complete, incomplete, moved-by-someone and back-on-the-previous-release promotions read from the tags; stored runs ignored for another cluster or user, after expiry, or in any shape the page does not write, including older shapes of the promotion record.src/lib/spaced-counts.test.mjs: the per-template counts, the coverage line, and the 5,000-row limit. That case fails if node-reported rows count toward the limit, or if 5,000 rows do not count as capped. Node-reported rows do not move the "created since" time, and the limit warning stays when no listed row counts.space-uiimage, at an earlier head of this branch:vmlinux-6.1.155-bpf-modsig) that the Kind builder and nodes do not have installed. A real code-runtime build fails when its build sandbox starts, and a sandbox from a rebuilt seed does not boot there. So code-runtime's build, its release and that release's tag request were answered by the loop. The page asked for code-runtime withbase: "seed@<the new seed digest>"and no publish tags.navigator.locksremoved, a build was recorded and polled to success with no notice, the localStorage lock was released after each write, and Dismiss forgot the run.seed@<new digest>. That sandbox could not boot on Kind, as above, and was deleted. To show the page a test sandbox links to, a second run's seed "build" reported the current seed release. Its test sandbox, created for real fromseed@sha256:…, started, and its page showedspaced0.8.0. The button disappeared once the run had a test sandbox.latestat the start of the run. With the dialog open, seed'slatestwas moved, through the tag route, to another seed release built by a first real run. Promote then sent nothing. The dialog showed the new moves, warned that seed'slatesthad changed since the run started, and kept Promote disabled until "Promote anyway" was checked.latestwas back on the release it named before, and that code-runtime, which had nolatestbefore, keeps the new release. It offered Promote again, not Retry. Promote again opened the same confirmation, with seed moving forward and nothing extra to acknowledge, and sent nothing (it was cancelled). Moving seed on to the release it had before the run made the panel say someone had movedlatest.latestalready named planned only code-runtime's tag request. The record named both templates, seed with itslatestas read. After seed'slatestwas moved to another release, the panel read the promotion as incomplete, with seed moved by someone, not as success. A retry planned seed and asked before overwriting it. seed'slatestwas then put back.main's baseline lacks. "owner row listed" is the user-page fixture this stack no longer has. "callback is not a function", in three Templates steps, came from the loop itself: an earlier step wrapsnavigator.locks.requestand handled only its three-argument form, while the panel uses the two-argument form and now also takes the lock on load. With the wrapper fixed, those steps and the one that installs it reran with no new error type. A refused connection in one user-page step did not recur when it reran.localStoragekey and removed afterwards. The release list was answered synthetically withlateston the promoted releases, and other methods on that route were refused. The earlier head's success panel named the rollback target as short hashes in a sentence. This head lists each template's full digest with a copy button and says to record them, because this browser keeps them only until the run expires and no other browser has them.Visual confirmation
On a local Kind stack with synthetic data. The steps, after a real seed build (the next step is an explicit button):
The promotion confirmation after seed's
latestmoved while it was open: Promote sent nothing and asks again:After a reload that interrupted the promotion between its two tag requests, the panel reads the tags and says what still has to move:
In the two pairs below, before is an earlier head of this branch and after is this head, from a synthetic rebuild record and release list with placeholder digests.
The success panel after a promotion: the rollback target is now each template's full digest, with a copy button and a note to record it:
The same panel on the Templates page:
After the tag dialog moved seed back to the rollback target, the panel says so and offers Promote again:
A promotion whose seed was already at its target records seed too; after someone moved seed's
latest, the panel does not read it as complete:Two tabs that clicked Build seed, and then Create a test sandbox, at the same time (stubbed answers): the run keeps one of each, and the tab whose clicks lost names the extra ones above the run panel:
The same notices after Dismiss in that tab:
A test sandbox created from a pinned seed digest; its page shows the
spacedversion:The per-template
spacedtable, with its coverage line:Severity and risk
Severity: medium. Rebuilding and promoting the system templates is how a guest or
spacedchange reaches new sandboxes, and the console could neither do it nor show whichspacedversions sandboxes run.Risk: low. The page uses existing admin and public routes with their own permission checks (the system build and status routes, the tag route, the admin sandbox list, and the public sandbox create route). It changes nothing unless the operator clicks a step, and moves
latestonly through the same tag route the dialog uses, after a confirmation. Each build costs builder time, and each test sandbox runs until the operator deletes it. Roll back by reverting the commit.Not covered
spacedversion it reports, and deletes it afterwards. A release can build and publish yet fail to boot on the cluster's nodes, as on the Kind stack used here (see Validation).AI assistance: Claude Code wrote the code, the tests and this description, and ran the validation; a maintainer reviews it.