@ankhorage/infra 0.4.1 → 1.0.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 +15 -0
- package/README.md +1 -1
- package/dist/adapters/minikube/auth/supabase/index.d.ts.map +1 -1
- package/dist/adapters/minikube/auth/supabase/index.js +9 -39
- package/dist/adapters/minikube/auth/supabase/index.js.map +1 -1
- package/dist/adapters/minikube/auth/supabase/profile.d.ts +0 -1
- package/dist/adapters/minikube/auth/supabase/profile.d.ts.map +1 -1
- package/dist/adapters/minikube/auth/supabase/profile.js +1 -1
- package/dist/adapters/minikube/auth/supabase/profile.js.map +1 -1
- package/dist/adapters/minikube/authz/cerbos/index.d.ts.map +1 -1
- package/dist/adapters/minikube/authz/cerbos/index.js +22 -2
- package/dist/adapters/minikube/authz/cerbos/index.js.map +1 -1
- package/dist/adapters/minikube/base/index.d.ts +5 -2
- package/dist/adapters/minikube/base/index.d.ts.map +1 -1
- package/dist/adapters/minikube/base/index.js +2728 -1835
- package/dist/adapters/minikube/base/index.js.map +1 -1
- package/dist/adapters/minikube/contracts.d.ts +25 -0
- package/dist/adapters/minikube/contracts.d.ts.map +1 -1
- package/dist/adapters/minikube/contracts.js +1 -0
- package/dist/adapters/minikube/contracts.js.map +1 -1
- package/dist/adapters/minikube/index.d.ts.map +1 -1
- package/dist/adapters/minikube/index.js +18 -33
- package/dist/adapters/minikube/index.js.map +1 -1
- package/dist/adapters/minikube/secrets/supabase-vault/index.d.ts.map +1 -1
- package/dist/adapters/minikube/secrets/supabase-vault/index.js +49 -6
- package/dist/adapters/minikube/secrets/supabase-vault/index.js.map +1 -1
- package/dist/adapters/minikube/storage/supabase/index.d.ts.map +1 -1
- package/dist/adapters/minikube/storage/supabase/index.js +2 -1
- package/dist/adapters/minikube/storage/supabase/index.js.map +1 -1
- package/package.json +4 -1
- package/src/adapters/minikube/README.md +41 -38
- package/src/adapters/minikube/auth/supabase/index.ts +9 -40
- package/src/adapters/minikube/auth/supabase/profile.integration.test.ts +170 -2
- package/src/adapters/minikube/auth/supabase/profile.ts +1 -1
- package/src/adapters/minikube/authz/cerbos/index.ts +25 -3
- package/src/adapters/minikube/base/index.test.ts +380 -579
- package/src/adapters/minikube/base/index.ts +2935 -1870
- package/src/adapters/minikube/base/twoAppIsolation.integration.test.ts +636 -0
- package/src/adapters/minikube/contracts.ts +29 -0
- package/src/adapters/minikube/index.ts +18 -44
- package/src/adapters/minikube/secrets/index.test.ts +15 -7
- package/src/adapters/minikube/secrets/supabase-vault/index.ts +53 -6
- package/src/adapters/minikube/storage/supabase/index.ts +1 -5
- package/src/apis.test.ts +2 -1
- package/src/index.test.ts +63 -577
- package/src/minikubeImageLifecycle.test.ts +4 -3
|
@@ -1,18 +1,30 @@
|
|
|
1
1
|
import { execFile as execFileCallback } from 'node:child_process';
|
|
2
|
-
import { chmod, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
6
|
|
|
7
7
|
import { describe, expect, test } from 'bun:test';
|
|
8
8
|
|
|
9
|
-
import { generateInfrastructure } from '../../../../index';
|
|
9
|
+
import { generateInfrastructure as generateRawInfrastructure } from '../../../../index';
|
|
10
|
+
import { createAppManifest } from '../../../../testSupport';
|
|
10
11
|
import type { GeneratedInfrastructureFile, InfraManifestInput } from '../../../../types';
|
|
11
12
|
|
|
12
13
|
const execFile = promisify(execFileCallback);
|
|
13
14
|
const INTEGRATION_TIMEOUT_MS = 300_000;
|
|
14
15
|
const integrationTest = process.env.ANKH_SUPABASE_INTEGRATION === '1' ? test : test.skip;
|
|
15
16
|
|
|
17
|
+
function generateInfrastructure(
|
|
18
|
+
manifest: InfraManifestInput,
|
|
19
|
+
options: { namespaceHint?: string } = {},
|
|
20
|
+
): ReturnType<typeof generateRawInfrastructure> {
|
|
21
|
+
const slug = options.namespaceHint ?? 'profile-integration';
|
|
22
|
+
return generateRawInfrastructure(manifest, {
|
|
23
|
+
...options,
|
|
24
|
+
appManifest: createAppManifest(slug),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
16
28
|
describe('generated Supabase profile reconciliation integration', () => {
|
|
17
29
|
integrationTest(
|
|
18
30
|
'drops stale managed columns while preserving custom columns',
|
|
@@ -132,6 +144,109 @@ $$;`,
|
|
|
132
144
|
INTEGRATION_TIMEOUT_MS,
|
|
133
145
|
);
|
|
134
146
|
|
|
147
|
+
integrationTest(
|
|
148
|
+
'boots clean disabled profile and writes client env idempotently',
|
|
149
|
+
async () => {
|
|
150
|
+
const { appRoot, minikubeRoot, namespaceHint } = await createIntegrationProject();
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
await writeGeneratedFiles(
|
|
154
|
+
appRoot,
|
|
155
|
+
generateInfrastructure(createManifest({ table: undefined, fields: ['email'] }), {
|
|
156
|
+
namespaceHint,
|
|
157
|
+
}).files,
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
await runLocalSupabaseEnv(minikubeRoot);
|
|
161
|
+
await runLocalSupabaseEnv(minikubeRoot);
|
|
162
|
+
|
|
163
|
+
const { stdout: statusEnv } = await execFile('supabase', [
|
|
164
|
+
'--workdir',
|
|
165
|
+
minikubeRoot,
|
|
166
|
+
'status',
|
|
167
|
+
'-o',
|
|
168
|
+
'env',
|
|
169
|
+
]);
|
|
170
|
+
const expectedUrl = readStatusEnv(statusEnv, 'API_URL');
|
|
171
|
+
const expectedAnonKey = readStatusEnv(statusEnv, 'ANON_KEY');
|
|
172
|
+
const appEnv = await readFile(path.join(appRoot, '.env.local'), 'utf8');
|
|
173
|
+
|
|
174
|
+
expect(readStatusEnv(appEnv, 'EXPO_PUBLIC_SUPABASE_URL')).toBe(expectedUrl);
|
|
175
|
+
expect(readStatusEnv(appEnv, 'EXPO_PUBLIC_SUPABASE_ANON_KEY')).toBe(expectedAnonKey);
|
|
176
|
+
} finally {
|
|
177
|
+
await cleanupIntegrationProject(appRoot, minikubeRoot);
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
INTEGRATION_TIMEOUT_MS,
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
integrationTest(
|
|
184
|
+
'accepts disabled profile when state table exists without auth profile state',
|
|
185
|
+
async () => {
|
|
186
|
+
const { appRoot, minikubeRoot, namespaceHint } = await createIntegrationProject();
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
await writeGeneratedFiles(
|
|
190
|
+
appRoot,
|
|
191
|
+
generateInfrastructure(createManifest({ table: undefined, fields: ['email'] }), {
|
|
192
|
+
namespaceHint,
|
|
193
|
+
}).files,
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
await runLocalSupabaseEnv(minikubeRoot);
|
|
197
|
+
await runSupabaseSql(
|
|
198
|
+
minikubeRoot,
|
|
199
|
+
`create schema if not exists ankhorage_internal;
|
|
200
|
+
create table if not exists ankhorage_internal.generated_schema_state (
|
|
201
|
+
artifact_key text primary key,
|
|
202
|
+
table_name text,
|
|
203
|
+
content_hash text,
|
|
204
|
+
updated_at timestamptz not null default now()
|
|
205
|
+
);
|
|
206
|
+
insert into ankhorage_internal.generated_schema_state (
|
|
207
|
+
artifact_key,
|
|
208
|
+
table_name,
|
|
209
|
+
content_hash
|
|
210
|
+
)
|
|
211
|
+
values ('storage.bucket', 'objects', repeat('1', 64))
|
|
212
|
+
on conflict (artifact_key) do update
|
|
213
|
+
set table_name = excluded.table_name,
|
|
214
|
+
content_hash = excluded.content_hash,
|
|
215
|
+
updated_at = now();`,
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
await runLocalSupabaseEnv(minikubeRoot);
|
|
219
|
+
} finally {
|
|
220
|
+
await cleanupIntegrationProject(appRoot, minikubeRoot);
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
INTEGRATION_TIMEOUT_MS,
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
integrationTest(
|
|
227
|
+
'status reports clean disabled profile as skipped',
|
|
228
|
+
async () => {
|
|
229
|
+
const { appRoot, minikubeRoot, namespaceHint } = await createIntegrationProject();
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
await writeGeneratedFiles(
|
|
233
|
+
appRoot,
|
|
234
|
+
generateInfrastructure(createManifest({ table: undefined, fields: ['email'] }), {
|
|
235
|
+
namespaceHint,
|
|
236
|
+
}).files,
|
|
237
|
+
);
|
|
238
|
+
await runLocalSupabaseEnv(minikubeRoot);
|
|
239
|
+
|
|
240
|
+
const output = await runGeneratedStatus(minikubeRoot);
|
|
241
|
+
expect(output).toContain('- profile reconciliation: skipped (no profile table configured)');
|
|
242
|
+
expect(output).toContain('- profile schema: skipped (no profile table configured)');
|
|
243
|
+
} finally {
|
|
244
|
+
await cleanupIntegrationProject(appRoot, minikubeRoot);
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
INTEGRATION_TIMEOUT_MS,
|
|
248
|
+
);
|
|
249
|
+
|
|
135
250
|
integrationTest(
|
|
136
251
|
'rejects profile table rename once generated state exists',
|
|
137
252
|
async () => {
|
|
@@ -789,6 +904,59 @@ exit 0
|
|
|
789
904
|
throw new Error('Expected generated status script to fail');
|
|
790
905
|
}
|
|
791
906
|
|
|
907
|
+
async function runGeneratedStatus(minikubeRoot: string): Promise<string> {
|
|
908
|
+
const fakeBin = path.join(minikubeRoot, 'fake-status-bin');
|
|
909
|
+
const fakeKubectl = path.join(fakeBin, 'kubectl');
|
|
910
|
+
await mkdir(fakeBin, { recursive: true });
|
|
911
|
+
await writeFile(
|
|
912
|
+
fakeKubectl,
|
|
913
|
+
`#!/usr/bin/env bash
|
|
914
|
+
set -euo pipefail
|
|
915
|
+
|
|
916
|
+
if [[ "\${1:-}" == "config" && "\${2:-}" == "current-context" ]]; then
|
|
917
|
+
echo fake-context
|
|
918
|
+
exit 0
|
|
919
|
+
fi
|
|
920
|
+
|
|
921
|
+
if [[ "\${1:-}" == "cluster-info" ]]; then
|
|
922
|
+
echo "Kubernetes control plane is running"
|
|
923
|
+
exit 0
|
|
924
|
+
fi
|
|
925
|
+
|
|
926
|
+
if [[ "\${1:-}" == "get" && "\${2:-}" == "namespace" ]]; then
|
|
927
|
+
exit 0
|
|
928
|
+
fi
|
|
929
|
+
|
|
930
|
+
if [[ "\${1:-}" == "get" && "\${2:-}" == "all" ]]; then
|
|
931
|
+
exit 0
|
|
932
|
+
fi
|
|
933
|
+
|
|
934
|
+
if [[ "\${1:-}" == "-n" && "\${3:-}" == "get" && "\${4:-}" == "configmap" ]]; then
|
|
935
|
+
exit 0
|
|
936
|
+
fi
|
|
937
|
+
|
|
938
|
+
if [[ "\${1:-}" == "-n" && "\${3:-}" == "get" && "\${4:-}" == "secret" ]]; then
|
|
939
|
+
exit 1
|
|
940
|
+
fi
|
|
941
|
+
|
|
942
|
+
exit 0
|
|
943
|
+
`,
|
|
944
|
+
'utf8',
|
|
945
|
+
);
|
|
946
|
+
await chmod(fakeKubectl, 0o755);
|
|
947
|
+
|
|
948
|
+
const result = await execFile(path.join(minikubeRoot, 'scripts/status.sh'), {
|
|
949
|
+
cwd: minikubeRoot,
|
|
950
|
+
env: {
|
|
951
|
+
...process.env,
|
|
952
|
+
PATH: `${fakeBin}:${process.env.PATH ?? ''}`,
|
|
953
|
+
SUPABASE_PROJECT_DIR: minikubeRoot,
|
|
954
|
+
},
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
return `${result.stdout}\n${result.stderr}`;
|
|
958
|
+
}
|
|
959
|
+
|
|
792
960
|
async function runSupabaseSql(minikubeRoot: string, sql: string): Promise<void> {
|
|
793
961
|
const { stdout } = await execFile('supabase', ['--workdir', minikubeRoot, 'status', '-o', 'env']);
|
|
794
962
|
const dbUrl = readStatusEnv(stdout, 'DB_URL');
|
|
@@ -2,19 +2,40 @@ import type { AppManifest, NavigatorSpec, RouteDefinition } from '@ankhorage/con
|
|
|
2
2
|
import { resolveAuthFlow } from '@ankhorage/contracts/auth';
|
|
3
3
|
|
|
4
4
|
import type { InfraManifestInput } from '../../../../types';
|
|
5
|
-
import type { MinikubeAdapterArtifacts } from '../../contracts';
|
|
5
|
+
import type { MinikubeAdapterArtifacts, MinikubeProviderLifecycle } from '../../contracts';
|
|
6
|
+
|
|
7
|
+
const CERBOS_NAMESPACE = 'cerbos';
|
|
6
8
|
|
|
7
9
|
export function generateCerbosAuthzArtifacts(args: {
|
|
8
10
|
manifest: InfraManifestInput;
|
|
9
11
|
namespace: string;
|
|
10
12
|
appManifest?: CerbosAppManifest;
|
|
11
13
|
}): MinikubeAdapterArtifacts {
|
|
12
|
-
const { manifest,
|
|
14
|
+
const { manifest, appManifest } = args;
|
|
15
|
+
const namespace = CERBOS_NAMESPACE;
|
|
13
16
|
|
|
14
17
|
const root = 'infra/minikube/k8s/authz/cerbos';
|
|
15
18
|
const resourceRoot = 'authz/cerbos';
|
|
16
19
|
const intent = buildCerbosPolicyIntent({ manifest, appManifest });
|
|
17
20
|
|
|
21
|
+
const endpointUrl = `http://cerbos.${namespace}.svc.cluster.local:3592`;
|
|
22
|
+
const lifecycle: MinikubeProviderLifecycle = {
|
|
23
|
+
id: 'cerbos',
|
|
24
|
+
namespace,
|
|
25
|
+
endpoints: [{ name: 'http', url: endpointUrl }],
|
|
26
|
+
readinessChecks: [
|
|
27
|
+
{
|
|
28
|
+
label: 'cerbos',
|
|
29
|
+
namespace,
|
|
30
|
+
resource: 'deployment/cerbos',
|
|
31
|
+
timeoutSeconds: 180,
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
migrationCommands: [],
|
|
35
|
+
reconciliationCommands: [],
|
|
36
|
+
statusChecks: [],
|
|
37
|
+
};
|
|
38
|
+
|
|
18
39
|
return {
|
|
19
40
|
files: [
|
|
20
41
|
{
|
|
@@ -40,7 +61,8 @@ export function generateCerbosAuthzArtifacts(args: {
|
|
|
40
61
|
`${resourceRoot}/cerbos.deployment.yaml`,
|
|
41
62
|
`${resourceRoot}/cerbos.service.yaml`,
|
|
42
63
|
],
|
|
43
|
-
|
|
64
|
+
providerLifecycle: [lifecycle],
|
|
65
|
+
envEntries: [`CERBOS_URL=${endpointUrl}`],
|
|
44
66
|
warnings: [],
|
|
45
67
|
};
|
|
46
68
|
}
|