ReviewOS

stacks/ts-cloud

No primitive for moving a deployed site between servers (#167 operation 2 is still absent in 0.12.7)

#190
Closed glennmichael123 opened this 2 months ago ·
2 months ago

#167 was closed as completed, but operation 2 of the three it described - move a deployed app between servers - has no primitive in the published package. This splits it out so it can be tracked on its own, with the evidence.

What is actually in 0.12.7

Grepped with word boundaries over the whole published dist (the word boundaries matter: my earlier claim that moveSite existed came from a substring match on removeSiteFromCloudConfig and moveSiteInput inside RemoveSiteInput):

$ grep -rohE '\b(moveSite|migrateSite|transferSite|relocateSite|moveApp|consolidateServer|attachSite|detachSite|renameServer|deleteServer|listServers)\b' package/dist/ | sort | uniq -c
  23 listServers
  10 deleteServer
   4 renameServer

Against #167's three operations:

#167 operation0.12.7Status
1. Attach an app to an existing server as a siteattachTo, attachToComputeInfrastructure, ensureAttachModeDatabase, allowStateOnlyworks, guardrails landed in #168/#169
2. Move a deployed app between serversnothingabsent
3. Rename a server in placeHetznerClient.renameServer(id, name)provider call only, no orchestration

renameServer is reachable (via the exported HetznerClient), but it is the raw Hetzner API call. Nothing updates the ts-cloud/* labels, the state file, or the inventory record alongside it, so calling it leaves the box named one thing and labelled another. Worth noting rename is the cheap one: install paths key off siteInstallBase(slug, site) and DNS points at the IP, so the server name is only load-bearing for box lookup.

Why a move cannot be composed from what exists

An attach deploy already stands a site up on a target box, so most of "arrive" is solved. What has no primitive is carrying the state and draining the source. From the layout ts-cloud itself defines, a move has to carry:

  • the release tree at siteInstallBase(slug, site) (/var/www/<slug>-<site>), or re-deploy into it
  • persistent state outside the release tree (/var/www/<slug>-shared/…, e.g. the SQLite file the deploy symlinks there)
  • the database role and schema that ensureAttachModeDatabase provisions inside the host's Postgres
  • the systemd units: <slug>-<site>@<sha>.service, plus -queue-<i> and -daemon-<name>
  • the rpx registry fragment /etc/rpx/sites.d/<slug>.json, and the certificates in /etc/rpx/certs
  • the owner stamp <base>/.ts-cloud/owner and the deploy history log beside it
  • DNS, last, and reversibly

Every one of those is a concept ts-cloud owns. What is missing is the operation that sequences them, and specifically the data-carrying step, which no redeploy can substitute for.

Where the plan should live

stacksjs/stacks#2342 asks buddy's --dry-run to print "the plan ts-cloud produced". For move that seems right: it is the operation whose plan is genuinely non-obvious (what is copied, what is recreated, what DNS changes, what is deleted and when), and the plan is a property of the operation, not of the CLI that calls it. For attach / destroy / rename, buddy can compose a plan from existing primitives without one upstream.

If that is the wrong split, it is worth saying so on #2342 before the CLI side commits to a shape.

9 days ago

Correcting this issue: the premise is wrong. site:move is implemented. I am sorry for the noise.

I searched for a vocabulary I invented (moveSite, migrateSite, transferSite, ...) instead of reading what is there. The operation is called planSiteMove and it lives in dist/operations/site-move.d.ts, with 20 exports, alongside three siblings I also missed:

ModuleExportsWhat it is
operations/site-move20planSiteMove - pause workers, snapshot, transfer, restore, health gate, DNS cutover, drain source
operations/server-rename3planServerRename - provider record, state pin, hostname, inventory, ordered for the crash in between
operations/drained-sites3scans a box for site trees it holds but no longer runs, so a teardown cannot silently eat somebody's rollback
operations/plan5resolvePlan / formatPlan / applyPlan - plan-then-apply with satisfied()-based resumability, typed confirmation for destructive steps, and an audit hook

That is #167's operations 2 and 3, and it is more thorough than the issue asked for. operations/plan is also, exactly, the "print the plan ts-cloud produced" that stacksjs/stacks#2342 wants from --dry-run.

What is actually wrong is that none of it is reachable from the published package.

$ ls package/dist/operations/          # 0.12.7
drained-sites.d.ts  plan.d.ts  server-rename.d.ts  site-move.d.ts  (+ .test.d.ts)
$ ls package/dist/operations/*.js
(none)

$ await import('@stacksjs/ts-cloud/operations/site-move')
Cannot find module '@stacksjs/ts-cloud/operations/site-move'

And across all three public entry points - 1067 exported names between root (996), /deploy (71) and /drivers (71) - not one of them is exported:

unreachable  planSiteMove          unreachable  resolvePlan
unreachable  planServerRename      unreachable  applyPlan
unreachable  planDrainedSites      unreachable  formatPlan

So this is the same defect as #191, with a far larger blast radius: the whole of #167's implementation is behind it. I have moved the detail there and am closing this as superseded rather than leaving a wrong description in the tracker.

One correction to #191 as well, which I will repeat there: I described those modules as shipping "types with no runtime". The dist is minified, so an identifier missing from the JS proves it is not publicly exported, not that the code is absent. The consumer-visible consequence is the same - it type-checks and then throws - but the diagnosis is "not exported", and the fix is correspondingly small.

9 days ago

Superseded by #191: the operations are implemented, they are just not exported. Detail moved there.

Sign in to comment on this issue.