@ankhorage/infra 5.1.0 → 5.1.1
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
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ankhorage/infra",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "5.1.
|
|
4
|
+
"version": "5.1.1",
|
|
5
5
|
"description": "Project-aware infrastructure generation library and executable Ankh provider for Ankhorage.",
|
|
6
6
|
"homepage": "https://github.com/ankhorage/infra#readme",
|
|
7
7
|
"bugs": {
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"openapi"
|
|
36
36
|
],
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@ankhorage/contracts": "^19.
|
|
38
|
+
"@ankhorage/contracts": "^19.1.0"
|
|
39
39
|
},
|
|
40
40
|
"files": [
|
|
41
41
|
"dist",
|
|
@@ -100,6 +100,7 @@
|
|
|
100
100
|
"prepack": "bun run build",
|
|
101
101
|
"test": "bun test src",
|
|
102
102
|
"test:e2e": "ANKH_INFRA_COMPOSITION_E2E=1 bun test src/infraComposition.integration.test.ts",
|
|
103
|
+
"test:e2e:minikube-supabase": "ANKH_INFRA_MINIKUBE_SUPABASE_E2E=1 bun test src/localMinikubeSupabase.integration.test.ts",
|
|
103
104
|
"typecheck": "bun x tsc --noEmit -p tsconfig.json",
|
|
104
105
|
"check-types": "bun run typecheck",
|
|
105
106
|
"version-packages": "ankhorage-changeset version",
|
|
@@ -114,10 +115,10 @@
|
|
|
114
115
|
"@ankhorage/hetzner": "^0.3.0",
|
|
115
116
|
"@ankhorage/k3s": "^0.3.0",
|
|
116
117
|
"@ankhorage/local": "^0.3.0",
|
|
117
|
-
"@ankhorage/minikube": "^0.
|
|
118
|
+
"@ankhorage/minikube": "^0.4.1",
|
|
118
119
|
"@ankhorage/paradox": "^0.1.24",
|
|
119
120
|
"@ankhorage/r2": "^0.2.0",
|
|
120
|
-
"@ankhorage/supabase": "^0.
|
|
121
|
+
"@ankhorage/supabase": "^0.4.5",
|
|
121
122
|
"@ankhorage/supabase-vault": "^0.3.1",
|
|
122
123
|
"@types/bun": "^1.4.1",
|
|
123
124
|
"@types/node": "^25.6.0",
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { createHmac } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
InfraLedger,
|
|
5
|
+
InfraManifest,
|
|
6
|
+
InfraResourceIdentity,
|
|
7
|
+
InfraResult,
|
|
8
|
+
} from '@ankhorage/contracts/infra';
|
|
9
|
+
import { expect, test } from 'bun:test';
|
|
10
|
+
|
|
11
|
+
import { createNodeInfraAdapterPackageResolver } from './features/environment-lifecycle/adapters/outbound/createNodeInfraAdapterPackageResolver.js';
|
|
12
|
+
import { destroyInfraEnvironmentAsync } from './features/environment-lifecycle/application/use-cases/destroyInfraEnvironmentAsync.js';
|
|
13
|
+
import { downInfraEnvironmentAsync } from './features/environment-lifecycle/application/use-cases/downInfraEnvironmentAsync.js';
|
|
14
|
+
import { planInfraEnvironmentAsync } from './features/environment-lifecycle/application/use-cases/planInfraEnvironmentAsync.js';
|
|
15
|
+
import { statusInfraEnvironmentAsync } from './features/environment-lifecycle/application/use-cases/statusInfraEnvironmentAsync.js';
|
|
16
|
+
import { upInfraEnvironmentAsync } from './features/environment-lifecycle/application/use-cases/upInfraEnvironmentAsync.js';
|
|
17
|
+
import type { InfraOrchestrationDependencies } from './types/infraOrchestration.js';
|
|
18
|
+
|
|
19
|
+
const projectId = 'infra145-minikube-supabase';
|
|
20
|
+
const baseUrl = 'http://127.0.0.1:54321';
|
|
21
|
+
const bucket = 'phase8-acceptance';
|
|
22
|
+
const jwtSecret = 'phase8-jwt-secret-that-is-at-least-thirty-two-characters';
|
|
23
|
+
const credentials = {
|
|
24
|
+
postgresPassword: 'phase8-postgres-password',
|
|
25
|
+
jwtSecret,
|
|
26
|
+
anonKey: createJwt('anon'),
|
|
27
|
+
serviceRoleKey: createJwt('service_role'),
|
|
28
|
+
realtimeSecretKeyBase: 'r'.repeat(64),
|
|
29
|
+
realtimeDatabaseEncryptionKey: '0123456789abcdef',
|
|
30
|
+
pgMetaCryptoKey: 'phase8-meta-crypto-key-that-is-at-least-32-characters',
|
|
31
|
+
} as const;
|
|
32
|
+
const manifest = {
|
|
33
|
+
environments: {
|
|
34
|
+
local: {
|
|
35
|
+
deployment: {
|
|
36
|
+
compute: { provider: 'local' },
|
|
37
|
+
runtime: {
|
|
38
|
+
provider: 'minikube',
|
|
39
|
+
profile: projectId,
|
|
40
|
+
driver: 'docker',
|
|
41
|
+
cpus: 2,
|
|
42
|
+
memoryMiB: 5_120,
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
database: { provider: 'supabase', tier: 'dev' },
|
|
46
|
+
auth: { provider: 'supabase' },
|
|
47
|
+
objectStorage: { provider: 'supabase', buckets: [bucket] },
|
|
48
|
+
networking: { publicBaseUrl: baseUrl },
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
modules: [],
|
|
52
|
+
} as const satisfies InfraManifest;
|
|
53
|
+
|
|
54
|
+
test.skipIf(process.env.ANKH_INFRA_MINIKUBE_SUPABASE_E2E !== '1')(
|
|
55
|
+
'runs local Minikube and Supabase through the provider-neutral lifecycle',
|
|
56
|
+
async () => {
|
|
57
|
+
const dependencies = createDependencies();
|
|
58
|
+
let ledger: InfraLedger | undefined;
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const initialPlan = requireSuccess(
|
|
62
|
+
await planInfraEnvironmentAsync({ projectId, manifest }, dependencies),
|
|
63
|
+
);
|
|
64
|
+
expect(initialPlan.actions.some(({ operation }) => operation === 'create')).toBe(true);
|
|
65
|
+
|
|
66
|
+
const { ledger: firstLedger, outputs: firstOutputs } = requireSuccess(
|
|
67
|
+
await upInfraEnvironmentAsync({ projectId, manifest }, dependencies),
|
|
68
|
+
);
|
|
69
|
+
ledger = firstLedger;
|
|
70
|
+
assertSafePublicOutputs(firstOutputs);
|
|
71
|
+
expect(
|
|
72
|
+
firstOutputs.some(
|
|
73
|
+
({ owner, name, value }) =>
|
|
74
|
+
owner.adapter === 'minikube' && name === 'localUrl' && value === baseUrl,
|
|
75
|
+
),
|
|
76
|
+
).toBe(true);
|
|
77
|
+
expect(
|
|
78
|
+
firstOutputs.some(
|
|
79
|
+
({ environmentVariable, value }) =>
|
|
80
|
+
environmentVariable === 'EXPO_PUBLIC_SUPABASE_URL' && value === baseUrl,
|
|
81
|
+
),
|
|
82
|
+
).toBe(true);
|
|
83
|
+
expect(firstOutputs.some(({ name, value }) => name === 'bucket' && value === bucket)).toBe(
|
|
84
|
+
true,
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const status = requireSuccess(
|
|
88
|
+
await statusInfraEnvironmentAsync({ projectId, manifest, previous: ledger }, dependencies),
|
|
89
|
+
);
|
|
90
|
+
expect(status.state).toBe('ready');
|
|
91
|
+
|
|
92
|
+
const convergedPlan = requireSuccess(
|
|
93
|
+
await planInfraEnvironmentAsync({ projectId, manifest, previous: ledger }, dependencies),
|
|
94
|
+
);
|
|
95
|
+
expect(convergedPlan.actions.every(({ operation }) => operation === 'noop')).toBe(true);
|
|
96
|
+
|
|
97
|
+
const { ledger: downLedger } = requireSuccess(
|
|
98
|
+
await downInfraEnvironmentAsync({ projectId, manifest, previous: ledger }, dependencies),
|
|
99
|
+
);
|
|
100
|
+
ledger = downLedger;
|
|
101
|
+
|
|
102
|
+
const { ledger: resumedLedger, outputs: resumedOutputs } = requireSuccess(
|
|
103
|
+
await upInfraEnvironmentAsync({ projectId, manifest, previous: ledger }, dependencies),
|
|
104
|
+
);
|
|
105
|
+
ledger = resumedLedger;
|
|
106
|
+
assertSafePublicOutputs(resumedOutputs);
|
|
107
|
+
|
|
108
|
+
const health = await fetch(`${baseUrl}/auth/v1/health`);
|
|
109
|
+
expect(health.ok).toBe(true);
|
|
110
|
+
|
|
111
|
+
const destroyed = requireSuccess(
|
|
112
|
+
await destroyInfraEnvironmentAsync(
|
|
113
|
+
createDestroyRequest(ledger, persistentIdentities(ledger)),
|
|
114
|
+
dependencies,
|
|
115
|
+
),
|
|
116
|
+
);
|
|
117
|
+
ledger = destroyed.ledger ?? undefined;
|
|
118
|
+
expect(destroyed.ledger).toBeNull();
|
|
119
|
+
} finally {
|
|
120
|
+
if (ledger !== undefined) {
|
|
121
|
+
await destroyInfraEnvironmentAsync(
|
|
122
|
+
createDestroyRequest(ledger, persistentIdentities(ledger)),
|
|
123
|
+
dependencies,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
900_000,
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
function createDependencies(): InfraOrchestrationDependencies {
|
|
132
|
+
return {
|
|
133
|
+
adapterResolver: createNodeInfraAdapterPackageResolver(),
|
|
134
|
+
credentials: {
|
|
135
|
+
resolveAsync: (reference) =>
|
|
136
|
+
Promise.resolve(
|
|
137
|
+
reference.name === 'SUPABASE_BOOTSTRAP'
|
|
138
|
+
? success(credentials)
|
|
139
|
+
: failure(
|
|
140
|
+
'unexpected-control-plane-credential',
|
|
141
|
+
`Unexpected control-plane credential ${reference.name}.`,
|
|
142
|
+
),
|
|
143
|
+
),
|
|
144
|
+
},
|
|
145
|
+
secrets: {
|
|
146
|
+
resolveAsync: (reference) =>
|
|
147
|
+
Promise.resolve(
|
|
148
|
+
failure(
|
|
149
|
+
'unexpected-managed-secret',
|
|
150
|
+
`Unexpected managed secret ${reference.ref}/${reference.key}.`,
|
|
151
|
+
),
|
|
152
|
+
),
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function createDestroyRequest(
|
|
158
|
+
previous: InfraLedger,
|
|
159
|
+
confirmedResources: readonly InfraResourceIdentity[],
|
|
160
|
+
) {
|
|
161
|
+
return {
|
|
162
|
+
projectId,
|
|
163
|
+
manifest,
|
|
164
|
+
environment: 'local' as const,
|
|
165
|
+
previous,
|
|
166
|
+
confirmation: { projectId, environment: 'local' as const },
|
|
167
|
+
persistence: { policy: 'delete' as const, confirmedResources },
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function persistentIdentities(ledger: InfraLedger): readonly InfraResourceIdentity[] {
|
|
172
|
+
return ledger.resources.filter(({ persistent }) => persistent).map(({ identity }) => identity);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function assertSafePublicOutputs(outputs: InfraLedger['outputs']): void {
|
|
176
|
+
const serialized = JSON.stringify(outputs);
|
|
177
|
+
expect(serialized).not.toContain(credentials.serviceRoleKey);
|
|
178
|
+
expect(serialized).not.toContain(credentials.postgresPassword);
|
|
179
|
+
expect(serialized).not.toContain(credentials.jwtSecret);
|
|
180
|
+
expect(serialized).not.toContain(credentials.realtimeSecretKeyBase);
|
|
181
|
+
expect(serialized).not.toContain(credentials.realtimeDatabaseEncryptionKey);
|
|
182
|
+
expect(serialized).not.toContain(credentials.pgMetaCryptoKey);
|
|
183
|
+
expect(
|
|
184
|
+
outputs.some(
|
|
185
|
+
({ environmentVariable, value }) =>
|
|
186
|
+
environmentVariable === 'EXPO_PUBLIC_SUPABASE_ANON_KEY' && value === credentials.anonKey,
|
|
187
|
+
),
|
|
188
|
+
).toBe(true);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function createJwt(role: 'anon' | 'service_role'): string {
|
|
192
|
+
const encodedHeader = encodeJwtPart({ alg: 'HS256', typ: 'JWT' });
|
|
193
|
+
const encodedPayload = encodeJwtPart({
|
|
194
|
+
role,
|
|
195
|
+
iss: 'supabase',
|
|
196
|
+
iat: 1_700_000_000,
|
|
197
|
+
exp: 4_102_444_800,
|
|
198
|
+
});
|
|
199
|
+
const unsigned = `${encodedHeader}.${encodedPayload}`;
|
|
200
|
+
const signature = createHmac('sha256', jwtSecret).update(unsigned).digest('base64url');
|
|
201
|
+
return `${unsigned}.${signature}`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function encodeJwtPart(value: Readonly<Record<string, string | number>>): string {
|
|
205
|
+
return Buffer.from(JSON.stringify(value)).toString('base64url');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function requireSuccess<T>(result: InfraResult<T>): T {
|
|
209
|
+
if (!result.ok) throw new Error(result.diagnostics.map(({ message }) => message).join(' '));
|
|
210
|
+
return result.value;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function success<T>(value: T): InfraResult<T> {
|
|
214
|
+
return { ok: true, value, diagnostics: [] };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function failure(code: string, message: string): InfraResult<never> {
|
|
218
|
+
return { ok: false, diagnostics: [{ severity: 'error', code, message }] };
|
|
219
|
+
}
|