ReviewOS

also looking at this

stacks/ts-cloud

feat(fleet): refuse to destroy a server still holding a moved site's rollback

#182
Closed chrisbbreuer wants to merge feat/destroy-drained-guard into main
4 files +284 -2
packages/ts-cloud/test/integration/site-move-e2e.test.tsmodified+23-0
Changes to packages/ts-cloud/test/integration/site-move-e2e.test.ts
@@ -24,6 +24,7 @@ import { afterAll, beforeAll, describe, expect, it } from 'bun:test'
2424import { mkdtemp, rm, writeFile } from 'node:fs/promises'
2525import { tmpdir } from 'node:os'
2626import { join } from 'node:path'
27import { buildDrainedSiteScanScript, parseDrainedSites } from '../../src/operations/drained-sites'
2728import {
2829 buildCertificatePackScript,
2930 buildCertificateStateScript,
@@ -318,10 +319,32 @@ describe.skipIf(!canRun)('site:move between two boxes (docker)', () => {
318319 expect((await exec(SOURCE, `test -f /etc/systemd/system/${SLUG}-${SITE}.service`)).code).toBe(0)
319320 })
320321
322 /**
323 * The drained tree is the rollback, and a teardown would take it. The scan has
324 * to see that on a real box: the site's files present, nothing running for it.
325 */
326 it('is reported as a drained site, so a teardown can refuse', async () => {
327 const scan = await run(SOURCE, buildDrainedSiteScanScript(SLUG).join('\n'))
328 const drained = parseDrainedSites(scan)
329 expect(drained.map(site => site.name)).toContain(SITE)
330 expect(drained[0].size).not.toBe('?')
331 })
332
333 it('is not reported as drained on the target, which is serving it', async () => {
334 const scan = await run(TARGET, buildDrainedSiteScanScript(SLUG).join('\n'))
335 expect(parseDrainedSites(scan)).toEqual([])
336 })
337
321338 it('comes back on the source by restarting it, with its data intact', async () => {
322339 await run(SOURCE, `systemctl start ${SLUG}-${SITE}.service`)
323340 const health = await exec(SOURCE, buildHealthGateScript(PORT, '/'))
324341 expect(health.code).toBe(0)
325342 expect((await run(SOURCE, `cat ${APP_BASE}/shared/database/app.sqlite`)).trim()).toBe(SHARED_DB_CONTENT)
326343 })
344
345 /** Once the source is serving again, it is no longer holding anyone's rollback. */
346 it('stops being reported as drained once it serves again', async () => {
347 const scan = await run(SOURCE, buildDrainedSiteScanScript(SLUG).join('\n'))
348 expect(parseDrainedSites(scan)).toEqual([])
349 })
327350})