@ankhorage/infra 0.4.0 → 0.4.2
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 +13 -0
- package/README.md +1 -1
- package/dist/adapters/minikube/base/index.d.ts +1 -0
- package/dist/adapters/minikube/base/index.d.ts.map +1 -1
- package/dist/adapters/minikube/base/index.js +245 -26
- 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 +26 -0
- package/dist/adapters/minikube/index.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters/minikube/auth/supabase/profile.integration.test.ts +170 -2
- package/src/adapters/minikube/base/index.test.ts +552 -0
- package/src/adapters/minikube/base/index.ts +249 -25
- package/src/adapters/minikube/index.ts +37 -0
- package/src/adapters/minikube/secrets/index.test.ts +37 -1
- package/src/index.test.ts +151 -12
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { chmodSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
|
|
1
6
|
import { describe, expect, test } from 'bun:test';
|
|
2
7
|
|
|
3
8
|
import type { InfraManifestInput } from '../../../types';
|
|
@@ -43,6 +48,159 @@ describe('generateMinikubeBaseArtifacts Supabase local ports', () => {
|
|
|
43
48
|
);
|
|
44
49
|
});
|
|
45
50
|
|
|
51
|
+
test('uses app project identity for Supabase ports independently from namespace', () => {
|
|
52
|
+
const scannerEnv = getGeneratedFileContent(
|
|
53
|
+
'local-example-test',
|
|
54
|
+
'infra/minikube/.env.example',
|
|
55
|
+
{},
|
|
56
|
+
'scanner',
|
|
57
|
+
);
|
|
58
|
+
const scannerNamespace = getGeneratedFileContent(
|
|
59
|
+
'local-example-test',
|
|
60
|
+
'infra/minikube/k8s/namespace.yaml',
|
|
61
|
+
{},
|
|
62
|
+
'scanner',
|
|
63
|
+
);
|
|
64
|
+
const chessEnv = getGeneratedFileContent(
|
|
65
|
+
'local-example-test',
|
|
66
|
+
'infra/minikube/.env.example',
|
|
67
|
+
{},
|
|
68
|
+
'chess',
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
expect(scannerNamespace).toContain('name: local-example-test');
|
|
72
|
+
expect(scannerEnv).toContain('SUPABASE_LOCAL_PORT_BASE=64020');
|
|
73
|
+
expect(chessEnv).toContain('SUPABASE_LOCAL_PORT_BASE=62200');
|
|
74
|
+
expect(scannerEnv).not.toContain('SUPABASE_LOCAL_PORT_BASE=62200');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('generates canonical Supabase project identity checks', () => {
|
|
78
|
+
const script = getGeneratedFileContent(
|
|
79
|
+
'scanner',
|
|
80
|
+
'infra/minikube/scripts/supabase-local-env.sh',
|
|
81
|
+
);
|
|
82
|
+
const status = getGeneratedFileContent('scanner', 'infra/minikube/scripts/status.sh');
|
|
83
|
+
|
|
84
|
+
expect(script).toContain('EXPECTED_SUPABASE_PROJECT_ID="scanner"');
|
|
85
|
+
expect(script).toContain('write_supabase_project_identity_for_new_config');
|
|
86
|
+
expect(script).toContain('validate_supabase_project_identity');
|
|
87
|
+
expect(script).toContain('project_id = "{expected}"');
|
|
88
|
+
expect(script).toContain(
|
|
89
|
+
'The existing local Supabase project belongs to a different identity.',
|
|
90
|
+
);
|
|
91
|
+
expect(script).toContain(
|
|
92
|
+
'Destroy the invalid local stack and its project-owned resources, then run Infra Up again.',
|
|
93
|
+
);
|
|
94
|
+
expect(script).toContain(
|
|
95
|
+
'supabase --workdir "${SUPABASE_PROJECT_DIR}" init --yes >/dev/null\n write_supabase_project_identity_for_new_config',
|
|
96
|
+
);
|
|
97
|
+
expect(script).toContain('validate_supabase_project_identity\nconfigure_supabase_local_ports');
|
|
98
|
+
expect(script).toContain(
|
|
99
|
+
'validate_supabase_project_identity\n assert_supabase_local_ports_available',
|
|
100
|
+
);
|
|
101
|
+
expect(script).toContain('validate_supabase_project_identity\nrun_checked_command');
|
|
102
|
+
expect(script).not.toContain('basename "${APP_SOURCE_DIR}"');
|
|
103
|
+
expect(script).not.toContain('_minikube');
|
|
104
|
+
|
|
105
|
+
expect(status).toContain('EXPECTED_SUPABASE_PROJECT_ID="scanner"');
|
|
106
|
+
expect(status).toContain('reject_supabase_project_id_override');
|
|
107
|
+
expect(status).toContain('validate_supabase_project_identity');
|
|
108
|
+
expect(status).toContain('Supabase project identity mismatch.');
|
|
109
|
+
expect(status).toContain('Destroy the invalid local stack and run Infra Up again.');
|
|
110
|
+
expect(status).not.toContain('write_supabase_project_identity_for_new_config');
|
|
111
|
+
expect(status).not.toContain('configure_supabase_local_ports');
|
|
112
|
+
expect(status).not.toContain('_minikube');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('patches only a newly initialized Supabase config to the canonical project identity', () => {
|
|
116
|
+
const run = runGeneratedSupabaseScript({
|
|
117
|
+
projectId: 'scanner',
|
|
118
|
+
existingConfig: null,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
expect(run.status).toBe(0);
|
|
122
|
+
expect(readFileSync(run.configPath, 'utf-8')).toContain('project_id = "scanner"');
|
|
123
|
+
expect(readFileSync(run.supabaseLogPath, 'utf-8')).toContain(
|
|
124
|
+
'--workdir ' + run.root + ' init --yes',
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('rejects an existing Supabase config with a different project identity before lifecycle work', () => {
|
|
129
|
+
const run = runGeneratedSupabaseScript({
|
|
130
|
+
projectId: 'scanner',
|
|
131
|
+
existingConfig: 'project_id = "minikube"\n[api]\nport = 54321\n',
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
expect(run.status).not.toBe(0);
|
|
135
|
+
expect(run.combinedOutput).toContain('Supabase project identity mismatch.');
|
|
136
|
+
expect(run.combinedOutput).toContain('Expected "scanner", found "minikube".');
|
|
137
|
+
expect(run.combinedOutput).toContain(
|
|
138
|
+
'Destroy the invalid local stack and its project-owned resources, then run Infra Up again.',
|
|
139
|
+
);
|
|
140
|
+
expect(readFileSync(run.configPath, 'utf-8')).toContain('project_id = "minikube"');
|
|
141
|
+
expect(readFileSync(run.supabaseLogPath, 'utf-8')).not.toContain('--workdir');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('rejects an existing Supabase config with missing top-level project identity', () => {
|
|
145
|
+
const run = runGeneratedSupabaseScript({
|
|
146
|
+
projectId: 'scanner',
|
|
147
|
+
existingConfig: '[api]\nport = 54321\n',
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
expect(run.status).not.toBe(0);
|
|
151
|
+
expect(run.combinedOutput).toContain('Expected "scanner", found missing.');
|
|
152
|
+
expect(readFileSync(run.configPath, 'utf-8')).not.toContain('project_id = "scanner"');
|
|
153
|
+
expect(readFileSync(run.supabaseLogPath, 'utf-8')).not.toContain('--workdir');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('rejects ambient SUPABASE_PROJECT_ID before Supabase CLI operations', () => {
|
|
157
|
+
const run = runGeneratedSupabaseScript({
|
|
158
|
+
projectId: 'scanner',
|
|
159
|
+
existingConfig: 'project_id = "scanner"\n[api]\nport = 54321\n',
|
|
160
|
+
env: {
|
|
161
|
+
SUPABASE_PROJECT_ID: 'minikube',
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
expect(run.status).not.toBe(0);
|
|
166
|
+
expect(run.combinedOutput).toContain(
|
|
167
|
+
'SUPABASE_PROJECT_ID must not be set for generated local Infra scripts.',
|
|
168
|
+
);
|
|
169
|
+
expect(readFileSync(run.supabaseLogPath, 'utf-8')).not.toContain('--workdir');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('rejects empty Supabase project identity before Supabase CLI operations', () => {
|
|
173
|
+
const run = runGeneratedSupabaseScript({
|
|
174
|
+
projectId: 'plain',
|
|
175
|
+
supabaseProjectId: null,
|
|
176
|
+
existingConfig: null,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
expect(run.status).not.toBe(0);
|
|
180
|
+
expect(run.combinedOutput).toContain(
|
|
181
|
+
'Cannot run local Supabase infrastructure: expected Supabase project identity is empty.',
|
|
182
|
+
);
|
|
183
|
+
expect(readFileSync(run.supabaseLogPath, 'utf-8')).not.toContain('--workdir');
|
|
184
|
+
expect(readFileSync(run.supabaseLogPath, 'utf-8')).not.toContain('init --yes');
|
|
185
|
+
expect(() => readFileSync(run.configPath, 'utf-8')).toThrow();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('requires python before Supabase init mutates the workdir', () => {
|
|
189
|
+
const run = runGeneratedSupabaseScript({
|
|
190
|
+
projectId: 'scanner',
|
|
191
|
+
existingConfig: null,
|
|
192
|
+
withoutPython: true,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
expect(run.status).not.toBe(0);
|
|
196
|
+
expect(run.combinedOutput).toContain(
|
|
197
|
+
'python3 is required to reconcile supabase/config.toml before local Supabase startup.',
|
|
198
|
+
);
|
|
199
|
+
expect(readFileSync(run.supabaseLogPath, 'utf-8')).not.toContain('--workdir');
|
|
200
|
+
expect(readFileSync(run.supabaseLogPath, 'utf-8')).not.toContain('init --yes');
|
|
201
|
+
expect(() => readFileSync(run.configPath, 'utf-8')).toThrow();
|
|
202
|
+
});
|
|
203
|
+
|
|
46
204
|
test('generates bootstrap logic that retries unhealthy local Supabase startup once', () => {
|
|
47
205
|
const script = getGeneratedFileContent('chess', 'infra/minikube/scripts/supabase-local-env.sh');
|
|
48
206
|
|
|
@@ -67,9 +225,16 @@ describe('generateMinikubeBaseArtifacts Supabase local ports', () => {
|
|
|
67
225
|
const script = getGeneratedFileContent('chess', 'infra/minikube/scripts/supabase-local-env.sh');
|
|
68
226
|
|
|
69
227
|
expect(script).toContain('SUPABASE_PROJECT_DIR="${SUPABASE_PROJECT_DIR:-${ROOT_DIR}}"');
|
|
228
|
+
expect(script).toContain('require_expected_supabase_project_identity');
|
|
229
|
+
expect(script).toContain(
|
|
230
|
+
'require_expected_supabase_project_identity\nreject_supabase_project_id_override\nrequire_supabase_cli_capabilities',
|
|
231
|
+
);
|
|
70
232
|
expect(script).toContain('supabase CLI >= 2.106.0 is required');
|
|
71
233
|
expect(script).toContain('supabase CLI does not support required global --workdir flag');
|
|
72
234
|
expect(script).toContain('supabase CLI does not support required migration up --local command');
|
|
235
|
+
expect(script).toContain(
|
|
236
|
+
'python3 is required to reconcile supabase/config.toml before local Supabase startup.',
|
|
237
|
+
);
|
|
73
238
|
expect(script).toContain('psql is required but not installed');
|
|
74
239
|
expect(script).toContain('supabase --workdir "${SUPABASE_PROJECT_DIR}" init --yes');
|
|
75
240
|
expect(script).toContain('supabase --workdir "${SUPABASE_PROJECT_DIR}" migration up --local');
|
|
@@ -136,16 +301,113 @@ describe('generateMinikubeBaseArtifacts Supabase local ports', () => {
|
|
|
136
301
|
);
|
|
137
302
|
expect(status).toContain('generated trigger function execute privilege must be revoked');
|
|
138
303
|
});
|
|
304
|
+
|
|
305
|
+
test('generates relation-safe disabled profile verification for local env and status', () => {
|
|
306
|
+
const script = getGeneratedFileContent('chess', 'infra/minikube/scripts/supabase-local-env.sh');
|
|
307
|
+
const status = getGeneratedFileContent('chess', 'infra/minikube/scripts/status.sh');
|
|
308
|
+
const expectedDynamicLookup = `execute
|
|
309
|
+
'select exists (
|
|
310
|
+
select 1
|
|
311
|
+
from ankhorage_internal.generated_schema_state
|
|
312
|
+
where artifact_key = $1
|
|
313
|
+
)'
|
|
314
|
+
into has_profile_state
|
|
315
|
+
using 'auth.profile';`;
|
|
316
|
+
|
|
317
|
+
for (const generated of [script, status]) {
|
|
318
|
+
expect(generated).toContain('has_profile_state boolean := false;');
|
|
319
|
+
expect(generated).toContain(expectedDynamicLookup);
|
|
320
|
+
expect(generated).toContain(
|
|
321
|
+
'manifest auth.profile.table was removed but local generated auth.profile state still exists; reset local Supabase or add an explicit cleanup migration',
|
|
322
|
+
);
|
|
323
|
+
expect(generated).not.toContain(
|
|
324
|
+
`if to_regclass('ankhorage_internal.generated_schema_state') is not null
|
|
325
|
+
and exists (`,
|
|
326
|
+
);
|
|
327
|
+
expect(generated.replace(expectedDynamicLookup, '')).not.toContain(
|
|
328
|
+
`from ankhorage_internal.generated_schema_state
|
|
329
|
+
where artifact_key = 'auth.profile'`,
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
expect(script).toContain('Generated profile disabled-state verification');
|
|
334
|
+
expect(status).toContain('- profile reconciliation: skipped (no profile table configured)');
|
|
335
|
+
expect(status).toContain('- profile schema: skipped (no profile table configured)');
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test('status succeeds for a clean disabled profile local Supabase stack', () => {
|
|
339
|
+
const run = runGeneratedStatusScript({
|
|
340
|
+
manifest: createInfraManifest(),
|
|
341
|
+
supabaseProjectId: 'plain',
|
|
342
|
+
env: {
|
|
343
|
+
AUTH_RUNTIME_MODE: 'local',
|
|
344
|
+
},
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
expect(run.status).toBe(0);
|
|
348
|
+
expect(run.combinedOutput).toContain('- immutable migrations: applied');
|
|
349
|
+
expect(run.combinedOutput).toContain(
|
|
350
|
+
'- profile reconciliation: skipped (no profile table configured)',
|
|
351
|
+
);
|
|
352
|
+
expect(run.combinedOutput).toContain('- profile schema: skipped (no profile table configured)');
|
|
353
|
+
expect(run.supabaseLog).toContain('--workdir');
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test('does not reject SUPABASE_PROJECT_ID for non-Supabase status checks', () => {
|
|
357
|
+
const run = runGeneratedStatusScript({
|
|
358
|
+
manifest: {
|
|
359
|
+
deployment: {
|
|
360
|
+
target: 'minikube',
|
|
361
|
+
monitoring: false,
|
|
362
|
+
},
|
|
363
|
+
plugins: [],
|
|
364
|
+
},
|
|
365
|
+
supabaseProjectId: null,
|
|
366
|
+
env: {
|
|
367
|
+
SUPABASE_PROJECT_ID: 'ambient-project',
|
|
368
|
+
},
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
expect(run.status).toBe(0);
|
|
372
|
+
expect(run.combinedOutput).not.toContain(
|
|
373
|
+
'SUPABASE_PROJECT_ID must not be set for generated local Infra scripts.',
|
|
374
|
+
);
|
|
375
|
+
expect(run.kubectlLog).toContain('get all -n plain');
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
test('rejects empty Supabase project identity before local Supabase status checks', () => {
|
|
379
|
+
const run = runGeneratedStatusScript({
|
|
380
|
+
manifest: {
|
|
381
|
+
deployment: {
|
|
382
|
+
target: 'minikube',
|
|
383
|
+
monitoring: false,
|
|
384
|
+
},
|
|
385
|
+
plugins: [],
|
|
386
|
+
},
|
|
387
|
+
supabaseProjectId: null,
|
|
388
|
+
env: {
|
|
389
|
+
SUPABASE_LOCAL_ENABLED: 'true',
|
|
390
|
+
},
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
expect(run.status).not.toBe(0);
|
|
394
|
+
expect(run.combinedOutput).toContain(
|
|
395
|
+
'Cannot run local Supabase infrastructure: expected Supabase project identity is empty.',
|
|
396
|
+
);
|
|
397
|
+
expect(run.supabaseLog).toBe('');
|
|
398
|
+
});
|
|
139
399
|
});
|
|
140
400
|
|
|
141
401
|
function getGeneratedFileContent(
|
|
142
402
|
namespace: string,
|
|
143
403
|
filePath: string,
|
|
144
404
|
overrides: Partial<InfraManifestInput> = {},
|
|
405
|
+
supabaseProjectId = namespace,
|
|
145
406
|
): string {
|
|
146
407
|
const artifact = generateMinikubeBaseArtifacts({
|
|
147
408
|
manifest: createInfraManifest(overrides),
|
|
148
409
|
namespace,
|
|
410
|
+
supabaseProjectId,
|
|
149
411
|
extraResources: [],
|
|
150
412
|
extraEnvEntries: [],
|
|
151
413
|
}).find((file) => file.path === filePath);
|
|
@@ -195,3 +457,293 @@ function createInfraManifest(overrides: Partial<InfraManifestInput> = {}): Infra
|
|
|
195
457
|
},
|
|
196
458
|
};
|
|
197
459
|
}
|
|
460
|
+
|
|
461
|
+
function runGeneratedSupabaseScript(args: {
|
|
462
|
+
projectId: string;
|
|
463
|
+
supabaseProjectId?: string | null;
|
|
464
|
+
existingConfig: string | null;
|
|
465
|
+
env?: Record<string, string>;
|
|
466
|
+
withoutPython?: boolean;
|
|
467
|
+
}): {
|
|
468
|
+
status: number | null;
|
|
469
|
+
combinedOutput: string;
|
|
470
|
+
root: string;
|
|
471
|
+
configPath: string;
|
|
472
|
+
supabaseLogPath: string;
|
|
473
|
+
} {
|
|
474
|
+
const tempRoot = mkdtempSync(join(tmpdir(), 'infra-supabase-identity-'));
|
|
475
|
+
const root = join(tempRoot, 'infra', 'minikube');
|
|
476
|
+
const scriptsDir = join(root, 'scripts');
|
|
477
|
+
const supabaseDir = join(root, 'supabase');
|
|
478
|
+
const binDir = join(tempRoot, 'bin');
|
|
479
|
+
const appSourceDir = join(tempRoot, 'app');
|
|
480
|
+
const scriptPath = join(scriptsDir, 'supabase-local-env.sh');
|
|
481
|
+
const configPath = join(supabaseDir, 'config.toml');
|
|
482
|
+
const supabaseLogPath = join(tempRoot, 'supabase.log');
|
|
483
|
+
|
|
484
|
+
mkdirSync(scriptsDir, { recursive: true });
|
|
485
|
+
mkdirSync(binDir, { recursive: true });
|
|
486
|
+
mkdirSync(appSourceDir, { recursive: true });
|
|
487
|
+
writeFileSync(join(root, '.env.example'), '');
|
|
488
|
+
writeFileSync(supabaseLogPath, '');
|
|
489
|
+
|
|
490
|
+
if (args.existingConfig !== null) {
|
|
491
|
+
mkdirSync(supabaseDir, { recursive: true });
|
|
492
|
+
writeFileSync(configPath, args.existingConfig);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const script = getGeneratedFileContent(
|
|
496
|
+
args.projectId,
|
|
497
|
+
'infra/minikube/scripts/supabase-local-env.sh',
|
|
498
|
+
{},
|
|
499
|
+
args.supabaseProjectId === undefined ? args.projectId : args.supabaseProjectId,
|
|
500
|
+
);
|
|
501
|
+
writeFileSync(scriptPath, script);
|
|
502
|
+
chmodSync(scriptPath, 0o755);
|
|
503
|
+
|
|
504
|
+
const supabaseStubPath = join(binDir, 'supabase');
|
|
505
|
+
writeFileSync(
|
|
506
|
+
supabaseStubPath,
|
|
507
|
+
`#!/bin/bash
|
|
508
|
+
set -euo pipefail
|
|
509
|
+
printf '%s\\n' "$*" >> "\${SUPABASE_STUB_LOG}"
|
|
510
|
+
|
|
511
|
+
if [[ "\${1:-}" == "--version" ]]; then
|
|
512
|
+
echo "supabase 2.106.0"
|
|
513
|
+
exit 0
|
|
514
|
+
fi
|
|
515
|
+
|
|
516
|
+
if [[ "\${1:-}" == "status" && "\${2:-}" == "--help" ]]; then
|
|
517
|
+
echo "--workdir"
|
|
518
|
+
exit 0
|
|
519
|
+
fi
|
|
520
|
+
|
|
521
|
+
if [[ "\${1:-}" == "migration" && "\${2:-}" == "up" && "\${3:-}" == "--help" ]]; then
|
|
522
|
+
echo "--local"
|
|
523
|
+
exit 0
|
|
524
|
+
fi
|
|
525
|
+
|
|
526
|
+
if [[ "\${1:-}" == "--workdir" && "\${3:-}" == "init" && "\${4:-}" == "--yes" ]]; then
|
|
527
|
+
mkdir -p "\${2}/supabase"
|
|
528
|
+
printf '%s\\n' 'project_id = "minikube"' '[api]' 'port = 54321' > "\${2}/supabase/config.toml"
|
|
529
|
+
exit 0
|
|
530
|
+
fi
|
|
531
|
+
|
|
532
|
+
if [[ "\${1:-}" == "--workdir" && "\${3:-}" == "status" && "\${4:-}" == "-o" && "\${5:-}" == "env" ]]; then
|
|
533
|
+
cat <<'ENV'
|
|
534
|
+
DB_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres
|
|
535
|
+
API_URL=http://127.0.0.1:54321
|
|
536
|
+
ANON_KEY=anon
|
|
537
|
+
SERVICE_ROLE_KEY=service
|
|
538
|
+
JWT_SECRET=secret
|
|
539
|
+
ENV
|
|
540
|
+
exit 0
|
|
541
|
+
fi
|
|
542
|
+
|
|
543
|
+
if [[ "\${1:-}" == "--workdir" && "\${3:-}" == "status" ]]; then
|
|
544
|
+
exit 0
|
|
545
|
+
fi
|
|
546
|
+
|
|
547
|
+
if [[ "\${1:-}" == "--workdir" && "\${3:-}" == "migration" && "\${4:-}" == "up" && "\${5:-}" == "--local" ]]; then
|
|
548
|
+
exit 0
|
|
549
|
+
fi
|
|
550
|
+
|
|
551
|
+
echo "unexpected supabase call: $*" >&2
|
|
552
|
+
exit 1
|
|
553
|
+
`,
|
|
554
|
+
);
|
|
555
|
+
chmodSync(supabaseStubPath, 0o755);
|
|
556
|
+
|
|
557
|
+
const psqlStubPath = join(binDir, 'psql');
|
|
558
|
+
writeFileSync(
|
|
559
|
+
psqlStubPath,
|
|
560
|
+
`#!/bin/bash
|
|
561
|
+
exit 0
|
|
562
|
+
`,
|
|
563
|
+
);
|
|
564
|
+
chmodSync(psqlStubPath, 0o755);
|
|
565
|
+
|
|
566
|
+
const result = spawnSync('/bin/bash', [scriptPath], {
|
|
567
|
+
cwd: tempRoot,
|
|
568
|
+
env: {
|
|
569
|
+
...(args.withoutPython ? {} : process.env),
|
|
570
|
+
PATH: args.withoutPython ? binDir : `${binDir}:${process.env.PATH ?? ''}`,
|
|
571
|
+
APP_SOURCE_DIR: appSourceDir,
|
|
572
|
+
SUPABASE_STUB_LOG: supabaseLogPath,
|
|
573
|
+
...args.env,
|
|
574
|
+
},
|
|
575
|
+
encoding: 'utf-8',
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
return {
|
|
579
|
+
status: result.status,
|
|
580
|
+
combinedOutput: `${result.stdout}${result.stderr}`,
|
|
581
|
+
root,
|
|
582
|
+
configPath,
|
|
583
|
+
supabaseLogPath,
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function runGeneratedStatusScript(args: {
|
|
588
|
+
manifest: InfraManifestInput;
|
|
589
|
+
supabaseProjectId: string | null;
|
|
590
|
+
env?: Record<string, string>;
|
|
591
|
+
}): {
|
|
592
|
+
status: number | null;
|
|
593
|
+
combinedOutput: string;
|
|
594
|
+
kubectlLog: string;
|
|
595
|
+
supabaseLog: string;
|
|
596
|
+
} {
|
|
597
|
+
const tempRoot = mkdtempSync(join(tmpdir(), 'infra-status-'));
|
|
598
|
+
const root = join(tempRoot, 'infra', 'minikube');
|
|
599
|
+
const scriptsDir = join(root, 'scripts');
|
|
600
|
+
const supabaseDir = join(root, 'supabase');
|
|
601
|
+
const binDir = join(tempRoot, 'bin');
|
|
602
|
+
const scriptPath = join(scriptsDir, 'status.sh');
|
|
603
|
+
const kubectlLogPath = join(tempRoot, 'kubectl.log');
|
|
604
|
+
const supabaseLogPath = join(tempRoot, 'supabase.log');
|
|
605
|
+
const psqlLogPath = join(tempRoot, 'psql.log');
|
|
606
|
+
|
|
607
|
+
mkdirSync(scriptsDir, { recursive: true });
|
|
608
|
+
mkdirSync(binDir, { recursive: true });
|
|
609
|
+
writeFileSync(join(root, '.env.example'), '');
|
|
610
|
+
writeFileSync(kubectlLogPath, '');
|
|
611
|
+
writeFileSync(supabaseLogPath, '');
|
|
612
|
+
writeFileSync(psqlLogPath, '');
|
|
613
|
+
|
|
614
|
+
if (args.supabaseProjectId !== null) {
|
|
615
|
+
mkdirSync(supabaseDir, { recursive: true });
|
|
616
|
+
writeFileSync(join(supabaseDir, 'config.toml'), `project_id = "${args.supabaseProjectId}"\n`);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const artifact = generateMinikubeBaseArtifacts({
|
|
620
|
+
manifest: args.manifest,
|
|
621
|
+
namespace: 'plain',
|
|
622
|
+
supabaseProjectId: args.supabaseProjectId,
|
|
623
|
+
extraResources: [],
|
|
624
|
+
extraEnvEntries: [],
|
|
625
|
+
}).find((file) => file.path === 'infra/minikube/scripts/status.sh');
|
|
626
|
+
|
|
627
|
+
if (!artifact) {
|
|
628
|
+
throw new Error('Expected generated status.sh artifact');
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
writeFileSync(scriptPath, artifact.content);
|
|
632
|
+
chmodSync(scriptPath, 0o755);
|
|
633
|
+
|
|
634
|
+
const kubectlStubPath = join(binDir, 'kubectl');
|
|
635
|
+
writeFileSync(
|
|
636
|
+
kubectlStubPath,
|
|
637
|
+
`#!/bin/bash
|
|
638
|
+
set -euo pipefail
|
|
639
|
+
printf '%s\\n' "$*" >> "\${KUBECTL_STUB_LOG}"
|
|
640
|
+
|
|
641
|
+
if [[ "\${1:-}" == "config" && "\${2:-}" == "current-context" ]]; then
|
|
642
|
+
echo "minikube"
|
|
643
|
+
exit 0
|
|
644
|
+
fi
|
|
645
|
+
|
|
646
|
+
if [[ "\${1:-}" == "cluster-info" ]]; then
|
|
647
|
+
echo "Kubernetes control plane is running"
|
|
648
|
+
exit 0
|
|
649
|
+
fi
|
|
650
|
+
|
|
651
|
+
if [[ "\${1:-}" == "get" && "\${2:-}" == "namespace" ]]; then
|
|
652
|
+
exit 0
|
|
653
|
+
fi
|
|
654
|
+
|
|
655
|
+
if [[ "\${1:-}" == "get" && "\${2:-}" == "all" ]]; then
|
|
656
|
+
echo "No resources found"
|
|
657
|
+
exit 0
|
|
658
|
+
fi
|
|
659
|
+
|
|
660
|
+
echo "unexpected kubectl call: $*" >&2
|
|
661
|
+
exit 1
|
|
662
|
+
`,
|
|
663
|
+
);
|
|
664
|
+
chmodSync(kubectlStubPath, 0o755);
|
|
665
|
+
|
|
666
|
+
const supabaseStubPath = join(binDir, 'supabase');
|
|
667
|
+
writeFileSync(
|
|
668
|
+
supabaseStubPath,
|
|
669
|
+
`#!/bin/bash
|
|
670
|
+
set -euo pipefail
|
|
671
|
+
printf '%s\\n' "$*" >> "\${SUPABASE_STUB_LOG}"
|
|
672
|
+
|
|
673
|
+
if [[ "\${1:-}" == "--version" ]]; then
|
|
674
|
+
echo "supabase 2.106.0"
|
|
675
|
+
exit 0
|
|
676
|
+
fi
|
|
677
|
+
|
|
678
|
+
if [[ "\${1:-}" == "status" && "\${2:-}" == "--help" ]]; then
|
|
679
|
+
echo "--workdir"
|
|
680
|
+
exit 0
|
|
681
|
+
fi
|
|
682
|
+
|
|
683
|
+
if [[ "\${1:-}" == "migration" && "\${2:-}" == "up" && "\${3:-}" == "--help" ]]; then
|
|
684
|
+
echo "--local"
|
|
685
|
+
exit 0
|
|
686
|
+
fi
|
|
687
|
+
|
|
688
|
+
if [[ "\${1:-}" == "--workdir" && "\${3:-}" == "status" && "\${4:-}" == "-o" && "\${5:-}" == "env" ]]; then
|
|
689
|
+
cat <<'ENV'
|
|
690
|
+
DB_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres
|
|
691
|
+
API_URL=http://127.0.0.1:54321
|
|
692
|
+
ANON_KEY=anon
|
|
693
|
+
SERVICE_ROLE_KEY=service
|
|
694
|
+
JWT_SECRET=secret
|
|
695
|
+
ENV
|
|
696
|
+
exit 0
|
|
697
|
+
fi
|
|
698
|
+
|
|
699
|
+
if [[ "\${1:-}" == "--workdir" && "\${3:-}" == "status" ]]; then
|
|
700
|
+
exit 0
|
|
701
|
+
fi
|
|
702
|
+
|
|
703
|
+
echo "unexpected supabase call: $*" >&2
|
|
704
|
+
exit 1
|
|
705
|
+
`,
|
|
706
|
+
);
|
|
707
|
+
chmodSync(supabaseStubPath, 0o755);
|
|
708
|
+
|
|
709
|
+
const psqlStubPath = join(binDir, 'psql');
|
|
710
|
+
writeFileSync(
|
|
711
|
+
psqlStubPath,
|
|
712
|
+
`#!/bin/bash
|
|
713
|
+
set -euo pipefail
|
|
714
|
+
printf '%s\\n' "$*" >> "\${PSQL_STUB_LOG}"
|
|
715
|
+
|
|
716
|
+
if [[ "$*" == *"select version from supabase_migrations.schema_migrations"* ]]; then
|
|
717
|
+
exit 0
|
|
718
|
+
fi
|
|
719
|
+
|
|
720
|
+
if [[ "$*" == *"-f"* ]]; then
|
|
721
|
+
exit 0
|
|
722
|
+
fi
|
|
723
|
+
|
|
724
|
+
echo "unexpected psql call: $*" >&2
|
|
725
|
+
exit 1
|
|
726
|
+
`,
|
|
727
|
+
);
|
|
728
|
+
chmodSync(psqlStubPath, 0o755);
|
|
729
|
+
|
|
730
|
+
const result = spawnSync('/bin/bash', [scriptPath], {
|
|
731
|
+
cwd: tempRoot,
|
|
732
|
+
env: {
|
|
733
|
+
...process.env,
|
|
734
|
+
PATH: `${binDir}:${process.env.PATH ?? ''}`,
|
|
735
|
+
KUBECTL_STUB_LOG: kubectlLogPath,
|
|
736
|
+
SUPABASE_STUB_LOG: supabaseLogPath,
|
|
737
|
+
PSQL_STUB_LOG: psqlLogPath,
|
|
738
|
+
...args.env,
|
|
739
|
+
},
|
|
740
|
+
encoding: 'utf-8',
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
return {
|
|
744
|
+
status: result.status,
|
|
745
|
+
combinedOutput: `${result.stdout}${result.stderr}`,
|
|
746
|
+
kubectlLog: readFileSync(kubectlLogPath, 'utf-8'),
|
|
747
|
+
supabaseLog: readFileSync(supabaseLogPath, 'utf-8'),
|
|
748
|
+
};
|
|
749
|
+
}
|