@ankhorage/infra 3.3.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +6 -7
- package/dist/adapters/minikube/base/index.js +26 -10
- package/dist/adapters/minikube/base/index.js.map +1 -1
- package/dist/adapters/minikube/index.d.ts.map +1 -1
- package/dist/adapters/minikube/index.js +2 -16
- package/dist/adapters/minikube/index.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/infraValidation.d.ts +1 -0
- package/dist/infraValidation.d.ts.map +1 -1
- package/dist/infraValidation.js +6 -1
- package/dist/infraValidation.js.map +1 -1
- package/dist/project/index.d.ts +1 -1
- package/dist/project/index.d.ts.map +1 -1
- package/dist/project/index.js +1 -1
- package/dist/project/index.js.map +1 -1
- package/dist/readme-usage.js +5 -0
- package/dist/readme-usage.js.map +1 -1
- package/dist/runtime.d.ts +6 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +24 -0
- package/dist/runtime.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +7 -7
- package/src/adapters/minikube/README.md +9 -1
- package/src/adapters/minikube/base/index.test.ts +33 -0
- package/src/adapters/minikube/base/index.ts +28 -10
- package/src/adapters/minikube/base/portForwardRuntime.test.ts +288 -0
- package/src/adapters/minikube/base/twoAppIsolation.integration.test.ts +272 -9
- package/src/adapters/minikube/index.ts +2 -21
- package/src/apiInfrastructure.test.ts +69 -0
- package/src/generatedApiLedgerCleanup.test.ts +72 -0
- package/src/index.ts +0 -6
- package/src/infraApiValidation.test.ts +42 -0
- package/src/infraValidation.ts +10 -1
- package/src/project/index.test.ts +46 -0
- package/src/project/index.ts +1 -0
- package/src/readme-usage.ts +5 -0
- package/src/runtime.ts +31 -0
- package/src/types.ts +1 -1
- package/dist/generatedApiDatabase.d.ts +0 -20
- package/dist/generatedApiDatabase.d.ts.map +0 -1
- package/dist/generatedApiDatabase.js +0 -343
- package/dist/generatedApiDatabase.js.map +0 -1
- package/src/generatedApiDatabase.test.ts +0 -355
- package/src/generatedApiDatabase.ts +0 -558
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from 'bun:test';
|
|
2
|
+
import { promises as fs } from 'fs';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
import { generateInfrastructure } from '../../../index.js';
|
|
7
|
+
import { createAppManifest } from '../../../testSupport.js';
|
|
8
|
+
import type { InfraManifestInput } from '../../../types.js';
|
|
9
|
+
|
|
10
|
+
const temporaryPaths = new Set<string>();
|
|
11
|
+
|
|
12
|
+
afterEach(async () => {
|
|
13
|
+
for (const rootPath of temporaryPaths) {
|
|
14
|
+
await stopFixtureProcesses(rootPath);
|
|
15
|
+
await fs.rm(rootPath, { force: true, recursive: true });
|
|
16
|
+
}
|
|
17
|
+
temporaryPaths.clear();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('generated Minikube runtime port-forward lifecycle', () => {
|
|
21
|
+
test(
|
|
22
|
+
'keeps healthy app forwarding, repairs a stopped Supabase gateway, and isolates operational forwards',
|
|
23
|
+
async () => {
|
|
24
|
+
const fixture = await createPortForwardFixture(createSupabaseManifest(), 'runtime-recovery');
|
|
25
|
+
|
|
26
|
+
const firstStart = await runPortForward(fixture, 'start', 'runtime');
|
|
27
|
+
expect(firstStart.stdout).toContain('app: started');
|
|
28
|
+
expect(firstStart.stdout).toContain('supabase-gateway: started');
|
|
29
|
+
|
|
30
|
+
const appPid = await readForwardPid(fixture, 'app');
|
|
31
|
+
const firstGatewayPid = await readForwardPid(fixture, 'supabase-gateway');
|
|
32
|
+
await expectForwardPid(fixture, 'studio', false);
|
|
33
|
+
await expectForwardPid(fixture, 'db-migration', false);
|
|
34
|
+
|
|
35
|
+
const repeatedStart = await runPortForward(fixture, 'start', 'runtime');
|
|
36
|
+
expect(repeatedStart.stdout).toContain(`app: running (pid ${appPid})`);
|
|
37
|
+
expect(repeatedStart.stdout).toContain(`supabase-gateway: running (pid ${firstGatewayPid})`);
|
|
38
|
+
expect(await readForwardPid(fixture, 'app')).toBe(appPid);
|
|
39
|
+
expect(await readForwardPid(fixture, 'supabase-gateway')).toBe(firstGatewayPid);
|
|
40
|
+
|
|
41
|
+
await runPortForward(fixture, 'stop', 'supabase-gateway');
|
|
42
|
+
expect(await readForwardPid(fixture, 'app')).toBe(appPid);
|
|
43
|
+
await expectForwardPid(fixture, 'supabase-gateway', false);
|
|
44
|
+
|
|
45
|
+
const recovery = await runPortForward(fixture, 'start', 'runtime');
|
|
46
|
+
expect(recovery.stdout).toContain(`app: running (pid ${appPid})`);
|
|
47
|
+
expect(recovery.stdout).toContain('supabase-gateway: started');
|
|
48
|
+
expect(await readForwardPid(fixture, 'app')).toBe(appPid);
|
|
49
|
+
expect(await readForwardPid(fixture, 'supabase-gateway')).not.toBe(firstGatewayPid);
|
|
50
|
+
await expectForwardPid(fixture, 'studio', false);
|
|
51
|
+
await expectForwardPid(fixture, 'db-migration', false);
|
|
52
|
+
|
|
53
|
+
await runPortForward(fixture, 'stop', 'runtime');
|
|
54
|
+
},
|
|
55
|
+
{ timeout: 30_000 },
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
test(
|
|
59
|
+
'starts only the app forward when no provider owns another runtime endpoint',
|
|
60
|
+
async () => {
|
|
61
|
+
const fixture = await createPortForwardFixture(
|
|
62
|
+
{ deployment: { target: 'minikube', monitoring: false }, modules: [] },
|
|
63
|
+
'runtime-app-only',
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const result = await runPortForward(fixture, 'start', 'runtime');
|
|
67
|
+
|
|
68
|
+
expect(result.stdout).toContain('app: started');
|
|
69
|
+
expect(result.stdout).not.toContain('supabase-gateway');
|
|
70
|
+
await expectForwardPid(fixture, 'app', true);
|
|
71
|
+
await expectForwardPid(fixture, 'supabase-gateway', false);
|
|
72
|
+
await expectForwardPid(fixture, 'studio', false);
|
|
73
|
+
await expectForwardPid(fixture, 'db-migration', false);
|
|
74
|
+
|
|
75
|
+
await runPortForward(fixture, 'stop', 'runtime');
|
|
76
|
+
},
|
|
77
|
+
{ timeout: 15_000 },
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
interface PortForwardFixture {
|
|
82
|
+
readonly env: Record<string, string>;
|
|
83
|
+
readonly processDirectory: string;
|
|
84
|
+
readonly profile: string;
|
|
85
|
+
readonly rootPath: string;
|
|
86
|
+
readonly scriptPath: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function createPortForwardFixture(
|
|
90
|
+
manifest: InfraManifestInput,
|
|
91
|
+
profile: string,
|
|
92
|
+
): Promise<PortForwardFixture> {
|
|
93
|
+
const rootPath = await fs.mkdtemp(path.join(os.tmpdir(), 'infra-port-forward-runtime-'));
|
|
94
|
+
temporaryPaths.add(rootPath);
|
|
95
|
+
const generated = generateInfrastructure(manifest, {
|
|
96
|
+
appManifest: createAppManifest(profile),
|
|
97
|
+
});
|
|
98
|
+
const generatedScript = generated.files.find(
|
|
99
|
+
(file) => file.path === 'infra/minikube/scripts/port-forward.sh',
|
|
100
|
+
);
|
|
101
|
+
if (generatedScript === undefined) {
|
|
102
|
+
throw new Error('Generated port-forward script was missing.');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const scriptPath = path.join(rootPath, generatedScript.path);
|
|
106
|
+
const fakeBin = path.join(rootPath, 'fake-bin');
|
|
107
|
+
const processDirectory = path.join(rootPath, 'fake-processes');
|
|
108
|
+
const serverScript = path.join(rootPath, 'forward-server.ts');
|
|
109
|
+
await fs.mkdir(path.dirname(scriptPath), { recursive: true });
|
|
110
|
+
await fs.mkdir(fakeBin, { recursive: true });
|
|
111
|
+
await fs.mkdir(processDirectory, { recursive: true });
|
|
112
|
+
await fs.writeFile(scriptPath, generatedScript.content, 'utf8');
|
|
113
|
+
await fs.chmod(scriptPath, 0o755);
|
|
114
|
+
await fs.writeFile(
|
|
115
|
+
serverScript,
|
|
116
|
+
[
|
|
117
|
+
"const port = Number.parseInt(Bun.argv[2] ?? '', 10);",
|
|
118
|
+
"Bun.serve({ hostname: '127.0.0.1', port, fetch: () => new Response('ok') });",
|
|
119
|
+
'await new Promise(() => undefined);',
|
|
120
|
+
'',
|
|
121
|
+
].join('\n'),
|
|
122
|
+
'utf8',
|
|
123
|
+
);
|
|
124
|
+
await writeExecutable(
|
|
125
|
+
path.join(fakeBin, 'kubectl'),
|
|
126
|
+
`#!/usr/bin/env bash
|
|
127
|
+
set -euo pipefail
|
|
128
|
+
if [[ " $* " == *" get "* ]]; then
|
|
129
|
+
exit 0
|
|
130
|
+
fi
|
|
131
|
+
if [[ " $* " != *" port-forward "* ]]; then
|
|
132
|
+
exit 1
|
|
133
|
+
fi
|
|
134
|
+
endpoint="\${!#}"
|
|
135
|
+
local_port="\${endpoint%%:*}"
|
|
136
|
+
printf 'kubectl %s\n' "$*" > "\${FAKE_PROCESS_DIRECTORY}/$$.command"
|
|
137
|
+
exec bun "\${FAKE_FORWARD_SERVER_SCRIPT}" "\${local_port}"
|
|
138
|
+
`,
|
|
139
|
+
);
|
|
140
|
+
await writeExecutable(
|
|
141
|
+
path.join(fakeBin, 'ps'),
|
|
142
|
+
`#!/usr/bin/env bash
|
|
143
|
+
set -euo pipefail
|
|
144
|
+
pid=""
|
|
145
|
+
format=""
|
|
146
|
+
while [[ $# -gt 0 ]]; do
|
|
147
|
+
case "$1" in
|
|
148
|
+
-p) pid="$2"; shift 2 ;;
|
|
149
|
+
-o) format="$2"; shift 2 ;;
|
|
150
|
+
*) shift ;;
|
|
151
|
+
esac
|
|
152
|
+
done
|
|
153
|
+
command_file="\${FAKE_PROCESS_DIRECTORY}/\${pid}.command"
|
|
154
|
+
if [[ -f "\${command_file}" && "\${format}" == "comm=" ]]; then
|
|
155
|
+
echo kubectl
|
|
156
|
+
elif [[ -f "\${command_file}" && "\${format}" == "command=" ]]; then
|
|
157
|
+
cat "\${command_file}"
|
|
158
|
+
else
|
|
159
|
+
/bin/ps -p "\${pid}" -o "\${format}"
|
|
160
|
+
fi
|
|
161
|
+
`,
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const ports = reserveAvailablePorts(4);
|
|
165
|
+
return {
|
|
166
|
+
env: {
|
|
167
|
+
...process.env,
|
|
168
|
+
ANKH_APP_SLUG: profile,
|
|
169
|
+
APP_PORT_FORWARD_LOCAL_PORT: String(ports[0]),
|
|
170
|
+
SUPABASE_GATEWAY_FORWARD_LOCAL_PORT: String(ports[1]),
|
|
171
|
+
SUPABASE_STUDIO_FORWARD_LOCAL_PORT: String(ports[2]),
|
|
172
|
+
SUPABASE_DB_FORWARD_LOCAL_PORT: String(ports[3]),
|
|
173
|
+
FAKE_FORWARD_SERVER_SCRIPT: serverScript,
|
|
174
|
+
FAKE_PROCESS_DIRECTORY: processDirectory,
|
|
175
|
+
PATH: `${fakeBin}:${process.env.PATH ?? ''}`,
|
|
176
|
+
},
|
|
177
|
+
processDirectory,
|
|
178
|
+
profile,
|
|
179
|
+
rootPath,
|
|
180
|
+
scriptPath,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function runPortForward(
|
|
185
|
+
fixture: PortForwardFixture,
|
|
186
|
+
action: string,
|
|
187
|
+
name: string,
|
|
188
|
+
): Promise<{ readonly exitCode: number; readonly stderr: string; readonly stdout: string }> {
|
|
189
|
+
const process = Bun.spawn(['bash', fixture.scriptPath, action, name], {
|
|
190
|
+
env: fixture.env,
|
|
191
|
+
stderr: 'pipe',
|
|
192
|
+
stdout: 'pipe',
|
|
193
|
+
});
|
|
194
|
+
const [exitCode, stdout, stderr] = await Promise.all([
|
|
195
|
+
process.exited,
|
|
196
|
+
new Response(process.stdout).text(),
|
|
197
|
+
new Response(process.stderr).text(),
|
|
198
|
+
]);
|
|
199
|
+
if (exitCode !== 0) {
|
|
200
|
+
throw new Error(`port-forward ${action} ${name} failed (${exitCode}): ${stdout}${stderr}`);
|
|
201
|
+
}
|
|
202
|
+
return { exitCode, stderr, stdout };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function readForwardPid(fixture: PortForwardFixture, name: string): Promise<number> {
|
|
206
|
+
const value = await fs.readFile(forwardPidPath(fixture, name), 'utf8');
|
|
207
|
+
return Number.parseInt(value.trim(), 10);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function expectForwardPid(
|
|
211
|
+
fixture: PortForwardFixture,
|
|
212
|
+
name: string,
|
|
213
|
+
exists: boolean,
|
|
214
|
+
): Promise<void> {
|
|
215
|
+
expect(await pathExists(forwardPidPath(fixture, name))).toBe(exists);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function forwardPidPath(fixture: PortForwardFixture, name: string): string {
|
|
219
|
+
return path.join(
|
|
220
|
+
fixture.rootPath,
|
|
221
|
+
'infra/minikube/.state/forwards',
|
|
222
|
+
`${fixture.profile}-${name}.pid`,
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function reserveAvailablePorts(count: number): number[] {
|
|
227
|
+
const listeners = Array.from({ length: count }, () =>
|
|
228
|
+
Bun.listen({
|
|
229
|
+
hostname: '127.0.0.1',
|
|
230
|
+
port: 0,
|
|
231
|
+
socket: { data: () => undefined },
|
|
232
|
+
}),
|
|
233
|
+
);
|
|
234
|
+
const ports = listeners.map((listener) => listener.port);
|
|
235
|
+
listeners.forEach((listener) => listener.stop(true));
|
|
236
|
+
return ports;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function writeExecutable(filePath: string, content: string): Promise<void> {
|
|
240
|
+
await fs.writeFile(filePath, content, 'utf8');
|
|
241
|
+
await fs.chmod(filePath, 0o755);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function pathExists(filePath: string): Promise<boolean> {
|
|
245
|
+
try {
|
|
246
|
+
await fs.access(filePath);
|
|
247
|
+
return true;
|
|
248
|
+
} catch {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function stopFixtureProcesses(rootPath: string): Promise<void> {
|
|
254
|
+
const stateDirectory = path.join(rootPath, 'infra/minikube/.state/forwards');
|
|
255
|
+
let entries: string[];
|
|
256
|
+
try {
|
|
257
|
+
entries = await fs.readdir(stateDirectory);
|
|
258
|
+
} catch {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
await Promise.all(
|
|
263
|
+
entries
|
|
264
|
+
.filter((entry) => entry.endsWith('.pid'))
|
|
265
|
+
.map(async (entry) => {
|
|
266
|
+
const pid = Number.parseInt(
|
|
267
|
+
await fs.readFile(path.join(stateDirectory, entry), 'utf8'),
|
|
268
|
+
10,
|
|
269
|
+
);
|
|
270
|
+
if (Number.isInteger(pid)) {
|
|
271
|
+
try {
|
|
272
|
+
process.kill(pid, 'SIGKILL');
|
|
273
|
+
} catch {
|
|
274
|
+
// The generated lifecycle may already have stopped the process.
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}),
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function createSupabaseManifest(): InfraManifestInput {
|
|
282
|
+
return {
|
|
283
|
+
deployment: { target: 'minikube', monitoring: false },
|
|
284
|
+
auth: { scope: 'global', provider: 'supabase' },
|
|
285
|
+
database: { provider: 'supabase', tier: 'dev' },
|
|
286
|
+
modules: [],
|
|
287
|
+
};
|
|
288
|
+
}
|
|
@@ -7,6 +7,7 @@ import { promisify } from 'node:util';
|
|
|
7
7
|
import { describe, expect, test } from 'bun:test';
|
|
8
8
|
|
|
9
9
|
import { generateInfrastructure } from '../../../index';
|
|
10
|
+
import { ensureProjectInfrastructureRuntime } from '../../../project/index';
|
|
10
11
|
import { createAppManifest } from '../../../testSupport';
|
|
11
12
|
import type { GeneratedInfrastructureFile, InfraManifestInput } from '../../../types';
|
|
12
13
|
|
|
@@ -16,14 +17,22 @@ const supabaseIsolationTest =
|
|
|
16
17
|
process.env.ANKH_MINIKUBE_SUPABASE_ISOLATION === '1' ? test : test.skip;
|
|
17
18
|
const TEST_TIMEOUT_MS = 1_800_000;
|
|
18
19
|
const HTTP_TIMEOUT_MS = 60_000;
|
|
20
|
+
const DIAGNOSTIC_TIMEOUT_MS = 60_000;
|
|
21
|
+
const DIAGNOSTIC_OUTPUT_LIMIT = 20_000;
|
|
22
|
+
const APP_ONLY_TEST_PROFILES = ['ankh-isolation-a', 'ankh-isolation-b'] as const;
|
|
23
|
+
const SUPABASE_TEST_PROFILES = ['ankh-isolation-supa-a', 'ankh-isolation-supa-b'] as const;
|
|
24
|
+
type TestOwnedMinikubeProfile =
|
|
25
|
+
| (typeof APP_ONLY_TEST_PROFILES)[number]
|
|
26
|
+
| (typeof SUPABASE_TEST_PROFILES)[number];
|
|
19
27
|
|
|
20
28
|
describe('generated Minikube two-app isolation', () => {
|
|
21
29
|
isolationTest(
|
|
22
30
|
'runs two generated app profiles without sharing cluster resources',
|
|
23
31
|
async () => {
|
|
32
|
+
await deleteTestOwnedMinikubeProfiles(APP_ONLY_TEST_PROFILES);
|
|
24
33
|
const root = await mkdtemp(path.join(process.cwd(), '.tmp-minikube-isolation-'));
|
|
25
|
-
const first = await createGeneratedApp(root,
|
|
26
|
-
const second = await createGeneratedApp(root,
|
|
34
|
+
const first = await createGeneratedApp(root, APP_ONLY_TEST_PROFILES[0], 18181);
|
|
35
|
+
const second = await createGeneratedApp(root, APP_ONLY_TEST_PROFILES[1], 18182);
|
|
27
36
|
|
|
28
37
|
try {
|
|
29
38
|
await runScript(first.minikubeRoot, 'up.sh');
|
|
@@ -56,16 +65,18 @@ describe('generated Minikube two-app isolation', () => {
|
|
|
56
65
|
supabaseIsolationTest(
|
|
57
66
|
'runs two generated Supabase-backed profiles without shared runtime state',
|
|
58
67
|
async () => {
|
|
68
|
+
await deleteTestOwnedMinikubeProfiles(SUPABASE_TEST_PROFILES);
|
|
59
69
|
const root = await mkdtemp(path.join(process.cwd(), '.tmp-minikube-supabase-isolation-'));
|
|
60
70
|
const [firstPorts, secondPorts] = await reserveSupabaseHostPortSets(2);
|
|
61
71
|
if (!firstPorts || !secondPorts) {
|
|
62
72
|
throw new Error('Expected two reserved Supabase host port sets.');
|
|
63
73
|
}
|
|
64
|
-
const first = await createGeneratedSupabaseApp(root,
|
|
65
|
-
const second = await createGeneratedSupabaseApp(root,
|
|
74
|
+
const first = await createGeneratedSupabaseApp(root, SUPABASE_TEST_PROFILES[0], firstPorts);
|
|
75
|
+
const second = await createGeneratedSupabaseApp(root, SUPABASE_TEST_PROFILES[1], secondPorts);
|
|
66
76
|
|
|
67
77
|
try {
|
|
68
78
|
await runScript(first.minikubeRoot, 'up.sh');
|
|
79
|
+
await prepareSecondSupabaseProfile(first.slug, second.slug, root);
|
|
69
80
|
await runScript(second.minikubeRoot, 'up.sh');
|
|
70
81
|
|
|
71
82
|
await expectProfileOwnsAppNamespace(first.slug);
|
|
@@ -75,6 +86,7 @@ describe('generated Minikube two-app isolation', () => {
|
|
|
75
86
|
await expectNoHostSupabaseComposeContainersFor(first.slug, second.slug);
|
|
76
87
|
|
|
77
88
|
await expectSupabaseEndToEnd(first);
|
|
89
|
+
await expectRuntimePortForwardRecovery(first);
|
|
78
90
|
const secondSession = await expectSupabaseEndToEnd(second);
|
|
79
91
|
|
|
80
92
|
await runScript(second.minikubeRoot, 'down.sh');
|
|
@@ -97,6 +109,14 @@ describe('generated Minikube two-app isolation', () => {
|
|
|
97
109
|
);
|
|
98
110
|
});
|
|
99
111
|
|
|
112
|
+
async function deleteTestOwnedMinikubeProfiles(
|
|
113
|
+
profiles: readonly TestOwnedMinikubeProfile[],
|
|
114
|
+
): Promise<void> {
|
|
115
|
+
for (const profile of profiles) {
|
|
116
|
+
await execFile('minikube', ['delete', '-p', profile], { timeout: TEST_TIMEOUT_MS });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
100
120
|
async function createGeneratedApp(root: string, slug: string, appPort: number) {
|
|
101
121
|
const appRoot = path.join(root, slug);
|
|
102
122
|
const minikubeRoot = path.join(appRoot, 'infra', 'minikube');
|
|
@@ -305,11 +325,182 @@ async function writeGeneratedFiles(
|
|
|
305
325
|
}
|
|
306
326
|
|
|
307
327
|
async function runScript(minikubeRoot: string, scriptName: string) {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
328
|
+
try {
|
|
329
|
+
return await execFile(path.join(minikubeRoot, 'scripts', scriptName), {
|
|
330
|
+
cwd: minikubeRoot,
|
|
331
|
+
env: process.env,
|
|
332
|
+
timeout: TEST_TIMEOUT_MS,
|
|
333
|
+
});
|
|
334
|
+
} catch (error) {
|
|
335
|
+
if (scriptName !== 'up.sh') throw error;
|
|
336
|
+
const diagnostics = await collectFailedUpDiagnostics(minikubeRoot);
|
|
337
|
+
throw new Error(
|
|
338
|
+
`Generated ${scriptName} failed:\n${formatUnknownError(error)}\n\n${diagnostics}`,
|
|
339
|
+
{ cause: error },
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async function prepareSecondSupabaseProfile(
|
|
345
|
+
sourceProfile: string,
|
|
346
|
+
targetProfile: string,
|
|
347
|
+
scratchRoot: string,
|
|
348
|
+
): Promise<void> {
|
|
349
|
+
const imagesResult = await execFile(
|
|
350
|
+
'kubectl',
|
|
351
|
+
[
|
|
352
|
+
'--context',
|
|
353
|
+
sourceProfile,
|
|
354
|
+
'-n',
|
|
355
|
+
'supabase',
|
|
356
|
+
'get',
|
|
357
|
+
'deployments',
|
|
358
|
+
'-o',
|
|
359
|
+
'jsonpath={range .items[*].spec.template.spec.containers[*]}{.image}{"\\n"}{end}',
|
|
360
|
+
],
|
|
361
|
+
{ timeout: DIAGNOSTIC_TIMEOUT_MS },
|
|
362
|
+
);
|
|
363
|
+
const images = [
|
|
364
|
+
...new Set(imagesResult.stdout.split(/\r?\n/u).map((image) => image.trim())),
|
|
365
|
+
].filter(Boolean);
|
|
366
|
+
if (images.length === 0) {
|
|
367
|
+
throw new Error(`Expected Supabase runtime images in profile ${sourceProfile}.`);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
await execFile(
|
|
371
|
+
'minikube',
|
|
372
|
+
['start', '-p', targetProfile, `--driver=${process.env.MINIKUBE_DRIVER ?? 'docker'}`],
|
|
373
|
+
{ timeout: TEST_TIMEOUT_MS },
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
const imageArchive = path.join(scratchRoot, 'supabase-image-transfer.tar');
|
|
377
|
+
try {
|
|
378
|
+
for (const image of images) {
|
|
379
|
+
await execFile('minikube', ['-p', sourceProfile, 'image', 'save', image, imageArchive], {
|
|
380
|
+
timeout: TEST_TIMEOUT_MS,
|
|
381
|
+
});
|
|
382
|
+
await execFile('minikube', ['-p', targetProfile, 'image', 'load', imageArchive], {
|
|
383
|
+
timeout: TEST_TIMEOUT_MS,
|
|
384
|
+
});
|
|
385
|
+
await rm(imageArchive, { force: true });
|
|
386
|
+
}
|
|
387
|
+
} finally {
|
|
388
|
+
await rm(imageArchive, { force: true });
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async function collectFailedUpDiagnostics(minikubeRoot: string): Promise<string> {
|
|
393
|
+
let profile: string;
|
|
394
|
+
try {
|
|
395
|
+
profile = readRequiredEnv(await readGeneratedEnv(minikubeRoot), 'ANKH_APP_SLUG');
|
|
396
|
+
} catch (error) {
|
|
397
|
+
return `Unable to determine the failed Minikube profile: ${formatUnknownError(error)}`;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const commands: readonly DiagnosticCommand[] = [
|
|
401
|
+
{
|
|
402
|
+
label: 'Minikube status',
|
|
403
|
+
command: 'minikube',
|
|
404
|
+
args: ['-p', profile, 'status'],
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
label: 'Supabase pods',
|
|
408
|
+
command: 'kubectl',
|
|
409
|
+
args: ['--context', profile, '-n', 'supabase', 'get', 'pods', '-o', 'wide'],
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
label: 'Postgres deployment',
|
|
413
|
+
command: 'kubectl',
|
|
414
|
+
args: ['--context', profile, '-n', 'supabase', 'describe', 'deployment', 'postgres'],
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
label: 'Postgres pod',
|
|
418
|
+
command: 'kubectl',
|
|
419
|
+
args: [
|
|
420
|
+
'--context',
|
|
421
|
+
profile,
|
|
422
|
+
'-n',
|
|
423
|
+
'supabase',
|
|
424
|
+
'describe',
|
|
425
|
+
'pod',
|
|
426
|
+
'-l',
|
|
427
|
+
'app.kubernetes.io/name=supabase-postgres',
|
|
428
|
+
],
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
label: 'Postgres logs',
|
|
432
|
+
command: 'kubectl',
|
|
433
|
+
args: [
|
|
434
|
+
'--context',
|
|
435
|
+
profile,
|
|
436
|
+
'-n',
|
|
437
|
+
'supabase',
|
|
438
|
+
'logs',
|
|
439
|
+
'-l',
|
|
440
|
+
'app.kubernetes.io/name=supabase-postgres',
|
|
441
|
+
'--all-containers=true',
|
|
442
|
+
'--tail=200',
|
|
443
|
+
],
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
label: 'Supabase events',
|
|
447
|
+
command: 'kubectl',
|
|
448
|
+
args: ['--context', profile, '-n', 'supabase', 'get', 'events', '--sort-by=.lastTimestamp'],
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
label: 'Supabase volumes',
|
|
452
|
+
command: 'kubectl',
|
|
453
|
+
args: ['--context', profile, '-n', 'supabase', 'get', 'pvc,pv'],
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
label: 'Docker containers',
|
|
457
|
+
command: 'docker',
|
|
458
|
+
args: ['ps', '--format', '{{.Names}}\\t{{.Status}}\\t{{.Image}}'],
|
|
459
|
+
},
|
|
460
|
+
];
|
|
461
|
+
const output: string[] = [];
|
|
462
|
+
for (const command of commands) {
|
|
463
|
+
output.push(await captureDiagnostic(command));
|
|
464
|
+
}
|
|
465
|
+
return `Bounded diagnostics for failed Minikube profile ${profile}:\n\n${output.join('\n\n')}`;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
interface DiagnosticCommand {
|
|
469
|
+
readonly label: string;
|
|
470
|
+
readonly command: string;
|
|
471
|
+
readonly args: readonly string[];
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
async function captureDiagnostic(diagnostic: DiagnosticCommand): Promise<string> {
|
|
475
|
+
const renderedCommand = [diagnostic.command, ...diagnostic.args].join(' ');
|
|
476
|
+
try {
|
|
477
|
+
const result = await execFile(diagnostic.command, [...diagnostic.args], {
|
|
478
|
+
timeout: DIAGNOSTIC_TIMEOUT_MS,
|
|
479
|
+
});
|
|
480
|
+
const output = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join('\n');
|
|
481
|
+
return `${diagnostic.label}\n$ ${renderedCommand}\n${truncateDiagnostic(output || '(no output)')}`;
|
|
482
|
+
} catch (error) {
|
|
483
|
+
return `${diagnostic.label}\n$ ${renderedCommand}\n${truncateDiagnostic(
|
|
484
|
+
formatUnknownError(error),
|
|
485
|
+
)}`;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function formatUnknownError(error: unknown): string {
|
|
490
|
+
if (!(error instanceof Error)) return String(error);
|
|
491
|
+
const childProcessError = error as Error & { stderr?: unknown; stdout?: unknown };
|
|
492
|
+
return [
|
|
493
|
+
error.message,
|
|
494
|
+
typeof childProcessError.stdout === 'string' ? childProcessError.stdout.trim() : '',
|
|
495
|
+
typeof childProcessError.stderr === 'string' ? childProcessError.stderr.trim() : '',
|
|
496
|
+
]
|
|
497
|
+
.filter(Boolean)
|
|
498
|
+
.join('\n');
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function truncateDiagnostic(output: string): string {
|
|
502
|
+
if (output.length <= DIAGNOSTIC_OUTPUT_LIMIT) return output;
|
|
503
|
+
return `${output.slice(0, DIAGNOSTIC_OUTPUT_LIMIT)}\n... diagnostic output truncated ...`;
|
|
313
504
|
}
|
|
314
505
|
|
|
315
506
|
async function removeDockerImage(image: string): Promise<void> {
|
|
@@ -369,6 +560,78 @@ async function expectNoHostSupabaseComposeContainersFor(...slugs: string[]) {
|
|
|
369
560
|
}
|
|
370
561
|
}
|
|
371
562
|
|
|
563
|
+
async function expectRuntimePortForwardRecovery(app: {
|
|
564
|
+
appRoot: string;
|
|
565
|
+
minikubeRoot: string;
|
|
566
|
+
slug: string;
|
|
567
|
+
}): Promise<void> {
|
|
568
|
+
const appPidBefore = await readForwardPid(app.minikubeRoot, app.slug, 'app');
|
|
569
|
+
const gatewayPidBefore = await readForwardPid(app.minikubeRoot, app.slug, 'supabase-gateway');
|
|
570
|
+
|
|
571
|
+
await runPortForward(app.minikubeRoot, 'stop', 'supabase-gateway');
|
|
572
|
+
await runPortForward(app.minikubeRoot, 'stop', 'studio');
|
|
573
|
+
await runPortForward(app.minikubeRoot, 'stop', 'db-migration');
|
|
574
|
+
|
|
575
|
+
await ensureProjectInfrastructureRuntime({
|
|
576
|
+
projectId: app.slug,
|
|
577
|
+
projectPath: app.appRoot,
|
|
578
|
+
target: 'minikube',
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
expect(await readForwardPid(app.minikubeRoot, app.slug, 'app')).toBe(appPidBefore);
|
|
582
|
+
expect(await readForwardPid(app.minikubeRoot, app.slug, 'supabase-gateway')).not.toBe(
|
|
583
|
+
gatewayPidBefore,
|
|
584
|
+
);
|
|
585
|
+
await expectForwardPidMissing(app.minikubeRoot, app.slug, 'studio');
|
|
586
|
+
await expectForwardPidMissing(app.minikubeRoot, app.slug, 'db-migration');
|
|
587
|
+
|
|
588
|
+
const env = await readGeneratedEnv(app.minikubeRoot);
|
|
589
|
+
const gatewayUrl = readRequiredEnv(env, 'SUPABASE_PUBLIC_URL');
|
|
590
|
+
const anonKey = readRequiredEnv(env, 'SUPABASE_ANON_KEY');
|
|
591
|
+
const response = await fetch(`${gatewayUrl}/auth/v1/health`, {
|
|
592
|
+
signal: AbortSignal.timeout(HTTP_TIMEOUT_MS),
|
|
593
|
+
headers: {
|
|
594
|
+
apikey: anonKey,
|
|
595
|
+
},
|
|
596
|
+
});
|
|
597
|
+
expect(response.ok).toBe(true);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
async function runPortForward(
|
|
601
|
+
minikubeRoot: string,
|
|
602
|
+
action: 'start' | 'status' | 'stop',
|
|
603
|
+
name: string,
|
|
604
|
+
): Promise<void> {
|
|
605
|
+
await execFile(path.join(minikubeRoot, 'scripts', 'port-forward.sh'), [action, name], {
|
|
606
|
+
cwd: minikubeRoot,
|
|
607
|
+
env: process.env,
|
|
608
|
+
timeout: 60_000,
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
async function readForwardPid(minikubeRoot: string, slug: string, name: string): Promise<number> {
|
|
613
|
+
const value = await readFile(forwardPidPath(minikubeRoot, slug, name), 'utf8');
|
|
614
|
+
return Number.parseInt(value.trim(), 10);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
async function expectForwardPidMissing(
|
|
618
|
+
minikubeRoot: string,
|
|
619
|
+
slug: string,
|
|
620
|
+
name: string,
|
|
621
|
+
): Promise<void> {
|
|
622
|
+
let exists = true;
|
|
623
|
+
try {
|
|
624
|
+
await readFile(forwardPidPath(minikubeRoot, slug, name), 'utf8');
|
|
625
|
+
} catch {
|
|
626
|
+
exists = false;
|
|
627
|
+
}
|
|
628
|
+
expect(exists).toBe(false);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function forwardPidPath(minikubeRoot: string, slug: string, name: string): string {
|
|
632
|
+
return path.join(minikubeRoot, '.state', 'forwards', `${slug}-${name}.pid`);
|
|
633
|
+
}
|
|
634
|
+
|
|
372
635
|
interface SupabaseSessionFixture {
|
|
373
636
|
readonly accessToken: string;
|
|
374
637
|
readonly anonKey: string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { validateInfraApiSupport } from '../../infraValidation';
|
|
2
2
|
import type {
|
|
3
3
|
GeneratedPackageDependency,
|
|
4
4
|
InfraManifestInput,
|
|
@@ -45,21 +45,6 @@ export function generateMinikubeInfra(
|
|
|
45
45
|
});
|
|
46
46
|
const storageArtifacts = generateStorageArtifacts({ manifest, namespace });
|
|
47
47
|
const secretStoreArtifacts = generateSecretStoreArtifacts({ manifest, namespace });
|
|
48
|
-
const generatedApiDatabaseArtifacts = generateGeneratedApiDatabaseArtifacts({
|
|
49
|
-
generatedApis: options.appManifest?.generatedApis,
|
|
50
|
-
databaseProvider: manifest.database?.provider,
|
|
51
|
-
});
|
|
52
|
-
const generatedApiErrors = generatedApiDatabaseArtifacts.diagnostics.filter(
|
|
53
|
-
(diagnostic) => diagnostic.severity === 'error',
|
|
54
|
-
);
|
|
55
|
-
|
|
56
|
-
if (generatedApiErrors.length > 0) {
|
|
57
|
-
throw new Error(
|
|
58
|
-
`Generated API database definition is invalid:\n${generatedApiErrors
|
|
59
|
-
.map((diagnostic) => `- ${diagnostic.path ?? diagnostic.apiId}: ${diagnostic.message}`)
|
|
60
|
-
.join('\n')}`,
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
48
|
|
|
64
49
|
const extraResources = unique([
|
|
65
50
|
...authArtifacts.resources,
|
|
@@ -98,9 +83,6 @@ export function generateMinikubeInfra(
|
|
|
98
83
|
...authzArtifacts.warnings,
|
|
99
84
|
...storageArtifacts.warnings,
|
|
100
85
|
...secretStoreArtifacts.warnings,
|
|
101
|
-
...generatedApiDatabaseArtifacts.diagnostics
|
|
102
|
-
.filter((diagnostic) => diagnostic.severity === 'warning')
|
|
103
|
-
.map((diagnostic) => diagnostic.message),
|
|
104
86
|
]);
|
|
105
87
|
|
|
106
88
|
return {
|
|
@@ -110,7 +92,6 @@ export function generateMinikubeInfra(
|
|
|
110
92
|
...authzArtifacts.files,
|
|
111
93
|
...storageArtifacts.files,
|
|
112
94
|
...secretStoreArtifacts.files,
|
|
113
|
-
...generatedApiDatabaseArtifacts.files,
|
|
114
95
|
],
|
|
115
96
|
warnings,
|
|
116
97
|
meta: {
|
|
@@ -151,7 +132,7 @@ function collectDependencies(manifest: InfraManifestInput): readonly GeneratedPa
|
|
|
151
132
|
}
|
|
152
133
|
|
|
153
134
|
function collectWarnings(manifest: InfraManifestInput): string[] {
|
|
154
|
-
const warnings
|
|
135
|
+
const warnings = [...validateInfraApiSupport(manifest)];
|
|
155
136
|
|
|
156
137
|
if (manifest.deployment?.monitoring) {
|
|
157
138
|
warnings.push(
|