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/src/operations/drained-sites.test.tsadded+83-0
Changes to packages/ts-cloud/src/operations/drained-sites.test.ts
@@ -0,0 +1,83 @@
1import { describe, expect, it } from 'bun:test'
2import { buildDrainedSiteScanScript, formatDrainedSiteRefusal, parseDrainedSites } from './drained-sites'
3
4describe('buildDrainedSiteScanScript', () => {
5 it('scans this project\'s trees only', () => {
6 const script = buildDrainedSiteScanScript('hq').join('\n')
7 expect(script).toContain('/var/www/hq-*')
8 expect(script).toContain('^hq-$TS_CLOUD_SITE[-@.]')
9 })
10
11 /** A stray folder under /var/www must not block a teardown. */
12 it('counts only directories that hold a release tree', () => {
13 expect(buildDrainedSiteScanScript('hq').join('\n')).toContain('[ -d "$TS_CLOUD_DIR/releases" ] || continue')
14 })
15
16 /**
17 * A scan that could change the box would be a poor thing to run immediately
18 * before deciding whether to keep it.
19 */
20 it('only reads', () => {
21 const script = buildDrainedSiteScanScript('hq').join('\n')
22 for (const mutation of ['rm ', 'systemctl stop', 'systemctl start', 'systemctl disable', 'systemctl enable', 'mv ', 'tar ']) {
23 expect(script.includes(mutation)).toBe(false)
24 }
25 // Every redirect to a FILE goes to /dev/null (`>&1` duplicates a descriptor
26 // rather than writing anywhere), so nothing lands on disk.
27 for (const redirect of script.match(/>(?!&)\s*\S+/g) ?? []) {
28 expect(redirect.replace(/^>\s*/, '')).toBe('/dev/null')
29 }
30 })
31
32 it('escapes a slug with regex metacharacters', () => {
33 expect(buildDrainedSiteScanScript('my.app').join('\n')).toContain('^my\\.app-')
34 })
35
36 it('always exits 0, so a scan is never mistaken for a failure', () => {
37 expect(buildDrainedSiteScanScript('hq').at(-1)).toBe('exit 0')
38 })
39})
40
41describe('parseDrainedSites', () => {
42 it('reports the trees with nothing running for them', () => {
43 const drained = parseDrainedSites('site:bughq:no:1.2G\nsite:loghq:yes:800M')
44 expect(drained).toEqual([{ name: 'bughq', size: '1.2G' }])
45 })
46
47 /**
48 * A site with something active is a site on a box being torn down — ordinary,
49 * and already covered by the teardown's own confirmation.
50 */
51 it('ignores a site that is still running', () => {
52 expect(parseDrainedSites('site:bughq:yes:1.2G')).toEqual([])
53 })
54
55 it('is empty for a box with no trees, or no output at all', () => {
56 expect(parseDrainedSites('')).toEqual([])
57 expect(parseDrainedSites(undefined)).toEqual([])
58 expect(parseDrainedSites('some unrelated chatter')).toEqual([])
59 })
60
61 it('survives a missing size', () => {
62 expect(parseDrainedSites('site:bughq:no:')).toEqual([{ name: 'bughq', size: '?' }])
63 })
64})
65
66describe('formatDrainedSiteRefusal', () => {
67 it('names the sites, what the files are for, and the way forward', () => {
68 const message = formatDrainedSiteRefusal([{ name: 'bughq', size: '1.2G' }], 'hq', '--discard-drained-sites')
69 expect(message).toContain('hq-bughq (1.2G)')
70 expect(message).toContain('they are the rollback')
71 expect(message).toContain('--discard-drained-sites')
72 })
73
74 it('reads correctly for several', () => {
75 const message = formatDrainedSiteRefusal(
76 [{ name: 'a', size: '1G' }, { name: 'b', size: '2G' }],
77 'hq',
78 '--discard-drained-sites',
79 )
80 expect(message).toContain('2 site trees')
81 expect(message).toContain('for them')
82 })
83})