@ankhorage/infra 4.0.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 +6 -0
- package/README.md +6 -1
- package/dist/adapters/minikube/base/index.js +26 -10
- package/dist/adapters/minikube/base/index.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/package.json +1 -1
- 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/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
|
@@ -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;
|
|
@@ -5,6 +5,7 @@ import path from 'path';
|
|
|
5
5
|
|
|
6
6
|
import { createAppManifest } from '../testSupport.js';
|
|
7
7
|
import {
|
|
8
|
+
ensureProjectInfrastructureRuntime,
|
|
8
9
|
inspectProjectInfrastructure,
|
|
9
10
|
readProjectInfrastructureEnvironment,
|
|
10
11
|
resolveProjectInfrastructureDatabaseUrl,
|
|
@@ -78,6 +79,39 @@ describe('@ankhorage/infra/project', () => {
|
|
|
78
79
|
});
|
|
79
80
|
});
|
|
80
81
|
|
|
82
|
+
test('ensures the generated runtime port-forward group through the owner API', async () => {
|
|
83
|
+
const projectPath = await createProjectFixture();
|
|
84
|
+
const scriptPath = path.join(projectPath, 'infra', 'minikube', 'scripts', 'port-forward.sh');
|
|
85
|
+
await fs.writeFile(scriptPath, '#!/usr/bin/env bash\nprintf \'%s\' "$*"\n', 'utf8');
|
|
86
|
+
|
|
87
|
+
const result = await ensureProjectInfrastructureRuntime({
|
|
88
|
+
projectId: 'example',
|
|
89
|
+
projectPath,
|
|
90
|
+
target: 'minikube',
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
expect(result).toEqual({ stdout: 'start runtime', stderr: '' });
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('reports actionable runtime-forward failures without reconciling infrastructure', async () => {
|
|
97
|
+
const projectPath = await createProjectFixture();
|
|
98
|
+
const scriptPath = path.join(projectPath, 'infra', 'minikube', 'scripts', 'port-forward.sh');
|
|
99
|
+
await fs.writeFile(
|
|
100
|
+
scriptPath,
|
|
101
|
+
'#!/usr/bin/env bash\necho "app: target app/service/app-runtime not found"\nexit 1\n',
|
|
102
|
+
'utf8',
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
await expectRejectMessage(
|
|
106
|
+
ensureProjectInfrastructureRuntime({
|
|
107
|
+
projectId: 'example',
|
|
108
|
+
projectPath,
|
|
109
|
+
target: 'minikube',
|
|
110
|
+
}),
|
|
111
|
+
"Failed to ensure infrastructure runtime for project 'example': app: target app/service/app-runtime not found",
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
81
115
|
test('reads only requested runtime environment values from the generated fallback', async () => {
|
|
82
116
|
const projectPath = await createProjectFixture();
|
|
83
117
|
const result = await readProjectInfrastructureEnvironment({
|
|
@@ -147,3 +181,15 @@ describe('@ankhorage/infra/project', () => {
|
|
|
147
181
|
});
|
|
148
182
|
});
|
|
149
183
|
});
|
|
184
|
+
|
|
185
|
+
async function expectRejectMessage(
|
|
186
|
+
promise: Promise<unknown>,
|
|
187
|
+
expectedMessage: string,
|
|
188
|
+
): Promise<void> {
|
|
189
|
+
try {
|
|
190
|
+
await promise;
|
|
191
|
+
throw new Error('Expected operation to reject.');
|
|
192
|
+
} catch (error) {
|
|
193
|
+
expect(error instanceof Error ? error.message : String(error)).toContain(expectedMessage);
|
|
194
|
+
}
|
|
195
|
+
}
|
package/src/project/index.ts
CHANGED
package/src/readme-usage.ts
CHANGED
|
@@ -29,6 +29,11 @@ import { runCli } from './cli/bin.js';
|
|
|
29
29
|
* operations accept an explicit project path and do not require Studio or a
|
|
30
30
|
* particular workspace layout.
|
|
31
31
|
*
|
|
32
|
+
* Running applications can repair their provider-aware host endpoints through
|
|
33
|
+
* `ensureProjectInfrastructureRuntime()` from `@ankhorage/infra/project`. This
|
|
34
|
+
* operation only ensures the generated runtime port-forward group; it does not
|
|
35
|
+
* deploy, bootstrap, or reconcile infrastructure.
|
|
36
|
+
*
|
|
32
37
|
* @usage
|
|
33
38
|
*/
|
|
34
39
|
await runCli(['--help']);
|
package/src/runtime.ts
CHANGED
|
@@ -85,6 +85,37 @@ export async function resolveProjectInfrastructurePortForward(args: {
|
|
|
85
85
|
};
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
/** Ensures the generated host forwards required by a running application are active. */
|
|
89
|
+
export async function ensureProjectInfrastructureRuntime(args: {
|
|
90
|
+
readonly projectId: string;
|
|
91
|
+
readonly projectPath: string;
|
|
92
|
+
readonly target: string;
|
|
93
|
+
}): Promise<InfraScriptOutput> {
|
|
94
|
+
try {
|
|
95
|
+
return await runProjectInfrastructureLifecycle({
|
|
96
|
+
args: ['start', 'runtime'],
|
|
97
|
+
projectId: args.projectId,
|
|
98
|
+
projectPath: args.projectPath,
|
|
99
|
+
script: 'port-forward',
|
|
100
|
+
target: args.target,
|
|
101
|
+
});
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (!(error instanceof InfraScriptExecutionError)) {
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const detail = error.stderr.trim() || error.stdout.trim();
|
|
108
|
+
throw new InfraScriptExecutionError({
|
|
109
|
+
exitCode: error.exitCode,
|
|
110
|
+
message: `Failed to ensure infrastructure runtime for project '${args.projectId}'${
|
|
111
|
+
detail.length > 0 ? `: ${detail}` : '.'
|
|
112
|
+
}`,
|
|
113
|
+
stderr: error.stderr,
|
|
114
|
+
stdout: error.stdout,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
88
119
|
export async function runProjectInfraScript(args: {
|
|
89
120
|
readonly context: InfraCommandContext;
|
|
90
121
|
readonly projectId: string;
|