ReviewOS

also looking at this

stacks/ts-cloud

fix(fleet): carry a site's TLS material when it moves

#179
Merged chrisbbreuer wants to merge feat/site-move-tls into main
4 files +328 -2
packages/ts-cloud/bin/commands/site.tsmodified+45-2
Changes to packages/ts-cloud/bin/commands/site.ts
@@ -14,10 +14,19 @@ import { siteInstallBase } from '../../src/deploy/site-target'
1414import { buildBackupScript } from '../../src/deploy/dashboard-database'
1515import { buildDatabaseSetupScript, isLocalDatabase } from '../../src/drivers/shared/db-provision'
1616import { buildBackupRestoreScript } from '../../src/drivers/shared/backups'
17import { buildRpxConfig, buildRpxFragmentRefreshScript } from '../../src/drivers/shared/rpx-gateway'
17import { buildRpxConfig, buildRpxFragmentRefreshScript, DEFAULT_RPX_CERTS_DIR } from '../../src/drivers/shared/rpx-gateway'
1818import { FleetStore, SystemFleetSshTransport } from '../../src/fleet'
1919import { applyPlan, formatPlan, resolvePlan } from '../../src/operations/plan'
20import { planSiteMove, siteMoveArchivePath } from '../../src/operations/site-move'
20import {
21 buildCertificatePackScript,
22 buildCertificateStateScript,
23 buildCertificateUnpackScript,
24 certificatesMatch,
25 parseCertificateState,
26 planSiteMove,
27 siteMoveArchivePath,
28 siteMoveCertArchivePath,
29} from '../../src/operations/site-move'
2130import { loadValidatedConfig, resolveDnsProviderConfig } from './shared'
2231
2332interface SiteAddOptions {
@@ -126,6 +135,10 @@ async function runSiteMove(siteName: string, options: SiteMoveCommandOptions): P
126135 const onBoxDatabase =
127136 appDatabase?.name && isLocalDatabase(appDatabase) ? { name: appDatabase.name } : undefined
128137 const dumpPath = `/tmp/ts-cloud-move-${slug}-${siteName}.sql.gz`
138 const certArchive = siteMoveCertArchivePath(slug, siteName)
139 const certsDir = proxy?.certsDir ?? DEFAULT_RPX_CERTS_DIR
140 // Every hostname this site is served on needs its own certificate.
141 const certDomains = [domain, ...(site.aliases ?? [])].filter((value): value is string => !!value)
129142 const engine = (appDatabase?.engine ?? 'mysql') as 'mysql' | 'mariadb' | 'postgres'
130143
131144 const effects: SiteMoveEffects = {
@@ -233,6 +246,36 @@ async function runSiteMove(siteName: string, options: SiteMoveCommandOptions): P
233246 },
234247 }
235248 : {}),
249 ...(proxy
250 ? {
251 certificates: {
252 inPlace: async () => {
253 const state = buildCertificateStateScript(certsDir, certDomains)
254 const [onSource, onTarget] = await Promise.all([
255 transport.exec(source, state),
256 transport.exec(target, state),
257 ])
258 return certificatesMatch(
259 parseCertificateState(onSource.stdout),
260 parseCertificateState(onTarget.stdout),
261 )
262 },
263 carry: async () => {
264 await execOn(transport, source, buildCertificatePackScript(certsDir, certDomains, certArchive))
265 const local = `${process.cwd()}/.ts-cloud-move-${slug}-${siteName}-certs.tar.gz`
266 // A site behind on-demand TLS may have no certificate yet; the
267 // pack script says so and exits clean rather than failing.
268 const staged = await transport.exec(source, `test -s ${certArchive} && echo staged || true`)
269 if (!staged.stdout.includes('staged')) return
270 await copyFile(source, `${source.sshUser}@${source.endpoint}:${certArchive}`, local)
271 await copyFile(target, local, `${target.sshUser}@${target.endpoint}:${certArchive}`)
272 await Bun.file(local).delete().catch(() => {})
273 await execOn(transport, target, buildCertificateUnpackScript(certsDir, certArchive))
274 await transport.exec(source, `rm -f ${certArchive}`)
275 },
276 },
277 }
278 : {}),
236279 cutoverDns: async () => {
237280 if (!domain) return []
238281 if (!dnsName) return [`No DNS provider configured — point ${domain} at ${target.endpoint} manually.`]