@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,378 +1,431 @@
|
|
|
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
|
-
|
|
6
1
|
import { describe, expect, test } from 'bun:test';
|
|
7
2
|
|
|
3
|
+
import { generateInfrastructure } from '../../../index';
|
|
4
|
+
import { createAppManifest } from '../../../testSupport';
|
|
8
5
|
import type { InfraManifestInput } from '../../../types';
|
|
9
|
-
import { generateMinikubeBaseArtifacts } from './index';
|
|
10
|
-
|
|
11
|
-
describe('generateMinikubeBaseArtifacts Supabase local ports', () => {
|
|
12
|
-
test('keeps legacy my-app defaults while allocating a different range for another app', () => {
|
|
13
|
-
const myAppEnv = getGeneratedFileContent('my-app', 'infra/minikube/.env.example');
|
|
14
|
-
const chessEnv = getGeneratedFileContent('chess', 'infra/minikube/.env.example');
|
|
15
|
-
|
|
16
|
-
expect(myAppEnv).toContain('SUPABASE_LOCAL_PORT_BASE=55320');
|
|
17
|
-
expect(myAppEnv).toContain('SUPABASE_LOCAL_SHADOW_PORT=55320');
|
|
18
|
-
expect(myAppEnv).toContain('SUPABASE_LOCAL_API_PORT=55321');
|
|
19
|
-
expect(myAppEnv).toContain('SUPABASE_LOCAL_DB_PORT=55322');
|
|
20
|
-
expect(myAppEnv).toContain('SUPABASE_LOCAL_STUDIO_PORT=55323');
|
|
21
|
-
expect(myAppEnv).toContain('SUPABASE_LOCAL_INBUCKET_PORT=55324');
|
|
22
|
-
expect(myAppEnv).toContain('SUPABASE_LOCAL_ANALYTICS_PORT=55325');
|
|
23
6
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
7
|
+
describe('generateMinikubeBaseArtifacts app-owned cluster model', () => {
|
|
8
|
+
test('uses the canonical app slug as the only Minikube profile identity', () => {
|
|
9
|
+
const result = generateInfrastructure(createSupabaseManifest(), {
|
|
10
|
+
namespaceHint: 'ignored',
|
|
11
|
+
appManifest: createAppManifest('chess'),
|
|
12
|
+
});
|
|
13
|
+
const envExample = getFile(result.files, 'infra/minikube/.env.example');
|
|
14
|
+
const upScript = getFile(result.files, 'infra/minikube/scripts/up.sh');
|
|
15
|
+
const downScript = getFile(result.files, 'infra/minikube/scripts/down.sh');
|
|
16
|
+
const destroyScript = getFile(result.files, 'infra/minikube/scripts/destroy.sh');
|
|
17
|
+
|
|
18
|
+
expect(envExample).toContain('ANKH_APP_SLUG=chess');
|
|
19
|
+
expect(envExample).not.toContain('MINIKUBE_PROFILE=');
|
|
20
|
+
expect(upScript).toContain('PROFILE="${ANKH_APP_SLUG:-chess}"');
|
|
21
|
+
expect(upScript).toContain('minikube start -p "${PROFILE}"');
|
|
22
|
+
expect(upScript).not.toContain('PROFILE="${MINIKUBE_PROFILE:-minikube}"');
|
|
23
|
+
expect(downScript).toContain('minikube stop -p "${PROFILE}"');
|
|
24
|
+
expect(destroyScript).toContain('minikube delete -p "${PROFILE}"');
|
|
32
25
|
});
|
|
33
26
|
|
|
34
|
-
test('generates
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
);
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
);
|
|
46
|
-
expect(
|
|
47
|
-
|
|
48
|
-
);
|
|
27
|
+
test('generates fixed app and supabase namespaces instead of slug namespaces', () => {
|
|
28
|
+
const result = generateInfrastructure(createSupabaseManifest(), {
|
|
29
|
+
appManifest: createAppManifest('uwm'),
|
|
30
|
+
});
|
|
31
|
+
const paths = result.files.map((file) => file.path);
|
|
32
|
+
const appNamespace = getFile(result.files, 'infra/minikube/k8s/namespaces/app.yaml');
|
|
33
|
+
const supabaseNamespace = getFile(result.files, 'infra/minikube/k8s/namespaces/supabase.yaml');
|
|
34
|
+
const appDeployment = getFile(result.files, 'infra/minikube/k8s/app/deployment.yaml');
|
|
35
|
+
const postgres = getFile(result.files, 'infra/minikube/k8s/supabase/postgres.yaml');
|
|
36
|
+
|
|
37
|
+
expect(paths).toContain('infra/minikube/k8s/namespaces/app.yaml');
|
|
38
|
+
expect(paths).toContain('infra/minikube/k8s/namespaces/supabase.yaml');
|
|
39
|
+
expect(paths).not.toContain('infra/minikube/k8s/namespace.yaml');
|
|
40
|
+
expect(appNamespace).toContain('name: app');
|
|
41
|
+
expect(supabaseNamespace).toContain('name: supabase');
|
|
42
|
+
expect(appDeployment).toContain('namespace: app');
|
|
43
|
+
expect(appDeployment).toContain('name: supabase-public-runtime');
|
|
44
|
+
expect(postgres).toContain('namespace: supabase');
|
|
49
45
|
});
|
|
50
46
|
|
|
51
|
-
test('
|
|
52
|
-
const
|
|
53
|
-
'
|
|
54
|
-
|
|
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
|
-
});
|
|
47
|
+
test('removes host-level Supabase runtime ownership from generated files', () => {
|
|
48
|
+
const result = generateInfrastructure(createSupabaseManifest(), {
|
|
49
|
+
appManifest: createAppManifest('scanner'),
|
|
50
|
+
});
|
|
51
|
+
const serialized = JSON.stringify(result.files);
|
|
76
52
|
|
|
77
|
-
|
|
78
|
-
const script = getGeneratedFileContent(
|
|
79
|
-
'scanner',
|
|
53
|
+
expect(result.files.map((file) => file.path)).not.toContain(
|
|
80
54
|
'infra/minikube/scripts/supabase-local-env.sh',
|
|
81
55
|
);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
expect(
|
|
85
|
-
expect(
|
|
86
|
-
expect(
|
|
87
|
-
expect(
|
|
88
|
-
expect(
|
|
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');
|
|
56
|
+
expect(serialized).not.toContain('supabase start');
|
|
57
|
+
expect(serialized).not.toContain('supabase stop');
|
|
58
|
+
expect(serialized).not.toContain('supabase status');
|
|
59
|
+
expect(serialized).not.toContain('SUPABASE_PROJECT_DIR');
|
|
60
|
+
expect(serialized).not.toContain('SUPABASE_LOCAL_PORT');
|
|
61
|
+
expect(serialized).not.toContain('supabase_*_');
|
|
62
|
+
expect(serialized).not.toContain('supabase-auth-secrets');
|
|
113
63
|
});
|
|
114
64
|
|
|
115
|
-
test('
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
existingConfig: null,
|
|
65
|
+
test('keeps Supabase migration history but executes against the Kubernetes DB URL', () => {
|
|
66
|
+
const result = generateInfrastructure(createSupabaseManifest(), {
|
|
67
|
+
appManifest: createAppManifest('scanner'),
|
|
119
68
|
});
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
expect(
|
|
124
|
-
|
|
125
|
-
);
|
|
69
|
+
const upScript = getFile(result.files, 'infra/minikube/scripts/up.sh');
|
|
70
|
+
const readme = getFile(result.files, 'infra/minikube/README.md');
|
|
71
|
+
|
|
72
|
+
expect(upScript).toContain('supabase migration up --db-url "${SUPABASE_DB_URL}"');
|
|
73
|
+
expect(upScript).toContain('export SUPABASE_DB_URL PGSSLMODE=disable');
|
|
74
|
+
expect(upScript).not.toContain('migration up --local');
|
|
75
|
+
expect(upScript).toContain('cd "${ROOT_DIR}"');
|
|
76
|
+
expect(upScript).toContain('set_required_env_default POSTGRES_PASSWORD "$(random_hex 32)"');
|
|
77
|
+
expect(upScript).not.toContain('set_env_default POSTGRES_PASSWORD "$(random_base64');
|
|
78
|
+
expect(upScript).toContain(
|
|
79
|
+
'set_required_env_default SUPABASE_DB_URL "postgres://postgres:${POSTGRES_PASSWORD}@127.0.0.1:${SUPABASE_DB_FORWARD_LOCAL_PORT}/postgres?sslmode=disable"',
|
|
80
|
+
);
|
|
81
|
+
expect(upScript).toContain('bootstrap_supabase_database');
|
|
82
|
+
expect(readme).toContain('Migration authoring/history remains Supabase');
|
|
126
83
|
});
|
|
127
84
|
|
|
128
|
-
test('
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
existingConfig: 'project_id = "minikube"\n[api]\nport = 54321\n',
|
|
85
|
+
test('lists and generates Supabase Kubernetes bootstrap and gateway contract files', () => {
|
|
86
|
+
const result = generateInfrastructure(createSupabaseManifest(), {
|
|
87
|
+
appManifest: createAppManifest('scanner'),
|
|
132
88
|
});
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
89
|
+
const paths = result.files.map((file) => file.path);
|
|
90
|
+
const readme = getFile(result.files, 'infra/minikube/README.md');
|
|
91
|
+
const kustomization = getFile(result.files, 'infra/minikube/k8s/kustomization.yaml');
|
|
92
|
+
const bootstrap = getFile(result.files, 'infra/minikube/k8s/supabase/bootstrap.sql');
|
|
93
|
+
const postgresInit = getFile(
|
|
94
|
+
result.files,
|
|
95
|
+
'infra/minikube/k8s/supabase/postgres.init.configmap.yaml',
|
|
96
|
+
);
|
|
97
|
+
const postgres = getFile(result.files, 'infra/minikube/k8s/supabase/postgres.yaml');
|
|
98
|
+
const auth = getFile(result.files, 'infra/minikube/k8s/supabase/auth.yaml');
|
|
99
|
+
const envExample = getFile(result.files, 'infra/minikube/.env.example');
|
|
100
|
+
const gatewayTemplate = getFile(
|
|
101
|
+
result.files,
|
|
102
|
+
'infra/minikube/k8s/supabase/gateway.template.yml',
|
|
103
|
+
);
|
|
104
|
+
const gateway = getFile(result.files, 'infra/minikube/k8s/supabase/gateway.yaml');
|
|
105
|
+
const storage = getFile(result.files, 'infra/minikube/k8s/supabase/storage.yaml');
|
|
106
|
+
const kongEntrypoint = getFile(result.files, 'infra/minikube/k8s/supabase/kong-entrypoint.sh');
|
|
107
|
+
|
|
108
|
+
expect(paths).toContain('infra/minikube/k8s/supabase/bootstrap.sql');
|
|
109
|
+
expect(paths).toContain('infra/minikube/k8s/supabase/postgres.init.configmap.yaml');
|
|
110
|
+
expect(paths).toContain('infra/minikube/k8s/supabase/gateway.template.yml');
|
|
111
|
+
expect(paths).toContain('infra/minikube/k8s/supabase/kong-entrypoint.sh');
|
|
112
|
+
expect(paths).not.toContain('infra/minikube/k8s/supabase/secrets.yaml');
|
|
113
|
+
expect(readme).toContain('supabase/bootstrap.sql');
|
|
114
|
+
expect(readme).toContain('supabase/postgres.init.configmap.yaml');
|
|
115
|
+
expect(readme).toContain('supabase/gateway.template.yml');
|
|
116
|
+
expect(readme).toContain('supabase/kong-entrypoint.sh');
|
|
117
|
+
expect(kustomization).not.toContain('supabase/secrets.yaml');
|
|
118
|
+
expect(kustomization).not.toContain('gateway.configmap.yaml');
|
|
119
|
+
expect(kustomization).toContain('supabase/postgres.init.configmap.yaml');
|
|
120
|
+
expect(bootstrap).toContain('CREATE SCHEMA IF NOT EXISTS auth');
|
|
121
|
+
expect(bootstrap).toContain(
|
|
122
|
+
'GRANT anon, authenticated, service_role TO supabase_storage_admin',
|
|
123
|
+
);
|
|
124
|
+
expect(postgresInit).toContain('alter role postgres with superuser');
|
|
125
|
+
expect(postgresInit).toContain('grant pg_read_server_files to supabase_admin');
|
|
126
|
+
expect(postgresInit).toContain('grant execute on function pg_read_file(text) to public');
|
|
127
|
+
expect(postgres).toContain('name: POSTGRES_USER');
|
|
128
|
+
expect(postgres).toContain('value: postgres');
|
|
129
|
+
expect(postgres).toContain('name: PGPORT');
|
|
130
|
+
expect(postgres).toContain(
|
|
131
|
+
'mountPath: /docker-entrypoint-initdb.d/99-ankhorage-local-dev-extensions.sql',
|
|
132
|
+
);
|
|
133
|
+
expect(postgres).toContain('config_file=/etc/postgresql/postgresql.conf');
|
|
134
|
+
expect(gatewayTemplate).toContain('keyauth_credentials');
|
|
135
|
+
expect(gatewayTemplate).toContain('request-transformer');
|
|
136
|
+
expect(kongEntrypoint).toContain('LUA_AUTH_EXPR');
|
|
137
|
+
expect(auth).toContain('name: GOTRUE_JWT_EXP');
|
|
138
|
+
expect(auth).toContain('key: JWT_EXPIRY');
|
|
139
|
+
expect(auth).toContain(
|
|
140
|
+
'postgres://supabase_auth_admin:$(POSTGRES_PASSWORD)@postgres.supabase.svc.cluster.local:5432/postgres?search_path=auth&sslmode=disable',
|
|
141
|
+
);
|
|
142
|
+
expect(storage).toContain(
|
|
143
|
+
'postgres://supabase_storage_admin:$(POSTGRES_PASSWORD)@postgres.supabase.svc.cluster.local:5432/postgres?search_path=storage&sslmode=disable',
|
|
144
|
+
);
|
|
145
|
+
expect(envExample).toContain('PGRST_DB_SCHEMAS=public,storage,graphql_public');
|
|
146
|
+
expect(auth).toContain('name: GOTRUE_EXTERNAL_EMAIL_ENABLED');
|
|
147
|
+
expect(auth).toContain('key: ENABLE_EMAIL_SIGNUP');
|
|
148
|
+
expect(auth).toContain('name: GOTRUE_SMTP_HOST');
|
|
149
|
+
expect(auth).toContain('name: GOTRUE_SMTP_PORT');
|
|
150
|
+
expect(auth).toContain('name: GOTRUE_SMTP_USER');
|
|
151
|
+
expect(auth).toContain('name: GOTRUE_SMTP_PASS');
|
|
152
|
+
expect(gatewayTemplate).toContain('name: realtime-v1-rest-openapi');
|
|
153
|
+
expect(gatewayTemplate).toContain('paths: [/realtime/v1/api/openapi]');
|
|
154
|
+
expect(gatewayTemplate).toContain('name: realtime-v1-rest-tenants');
|
|
155
|
+
expect(gatewayTemplate).toContain('paths: [/realtime/v1/api/tenants]');
|
|
156
|
+
expect(gatewayTemplate).toContain('name: request-termination');
|
|
157
|
+
expect(gatewayTemplate).toContain('status_code: 403');
|
|
158
|
+
expect(gateway).toContain('mountPath: /home/kong/temp.yml');
|
|
159
|
+
expect(gateway).toContain('subPath: temp.yml');
|
|
160
|
+
expect(gateway).toContain('mountPath: /home/kong/kong-entrypoint.sh');
|
|
161
|
+
expect(gateway).toContain('command: ["kong", "health"]');
|
|
142
162
|
});
|
|
143
163
|
|
|
144
|
-
test('
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
existingConfig: '[api]\nport = 54321\n',
|
|
164
|
+
test('defines reset separately from down and destroy', () => {
|
|
165
|
+
const result = generateInfrastructure(createSupabaseManifest(), {
|
|
166
|
+
appManifest: createAppManifest('uwm'),
|
|
148
167
|
});
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
expect(
|
|
153
|
-
expect(
|
|
168
|
+
const resetScript = getFile(result.files, 'infra/minikube/scripts/reset.sh');
|
|
169
|
+
const downScript = getFile(result.files, 'infra/minikube/scripts/down.sh');
|
|
170
|
+
|
|
171
|
+
expect(resetScript).toContain('ANKH_RESET_CONFIRM');
|
|
172
|
+
expect(resetScript).toContain('delete namespace app');
|
|
173
|
+
expect(resetScript).toContain('delete namespace supabase');
|
|
174
|
+
expect(resetScript).toContain('does not delete the Minikube profile');
|
|
175
|
+
expect(downScript).toContain('minikube stop -p "${PROFILE}"');
|
|
176
|
+
expect(downScript).not.toContain('delete namespace');
|
|
154
177
|
});
|
|
155
178
|
|
|
156
|
-
test('
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
179
|
+
test('keeps reset scoped to app namespace when Supabase is not generated', () => {
|
|
180
|
+
const result = generateInfrastructure(
|
|
181
|
+
{
|
|
182
|
+
deployment: {
|
|
183
|
+
target: 'minikube',
|
|
184
|
+
monitoring: false,
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
appManifest: createAppManifest('plain'),
|
|
162
189
|
},
|
|
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
190
|
);
|
|
169
|
-
|
|
170
|
-
|
|
191
|
+
const paths = result.files.map((file) => file.path);
|
|
192
|
+
const resetScript = getFile(result.files, 'infra/minikube/scripts/reset.sh');
|
|
193
|
+
const statusScript = getFile(result.files, 'infra/minikube/scripts/status.sh');
|
|
171
194
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
195
|
+
expect(paths).not.toContain('infra/minikube/k8s/namespaces/supabase.yaml');
|
|
196
|
+
expect(resetScript).toContain('delete namespace app');
|
|
197
|
+
expect(resetScript).not.toContain('delete namespace supabase');
|
|
198
|
+
expect(resetScript).not.toContain('namespaces/supabase.yaml');
|
|
199
|
+
expect(statusScript).toContain('for namespace in app; do');
|
|
200
|
+
});
|
|
178
201
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
202
|
+
test('places standalone provider workloads in provider-owned namespaces', () => {
|
|
203
|
+
const result = generateInfrastructure(
|
|
204
|
+
{
|
|
205
|
+
...createSupabaseManifest(),
|
|
206
|
+
auth: {
|
|
207
|
+
scope: 'global',
|
|
208
|
+
provider: 'supabase',
|
|
209
|
+
authorization: {
|
|
210
|
+
engine: 'cerbos',
|
|
211
|
+
kind: 'RBAC',
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
appManifest: createAppManifest('authz-app'),
|
|
217
|
+
},
|
|
182
218
|
);
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
219
|
+
const paths = result.files.map((file) => file.path);
|
|
220
|
+
const kustomization = getFile(result.files, 'infra/minikube/k8s/kustomization.yaml');
|
|
221
|
+
const readme = getFile(result.files, 'infra/minikube/README.md');
|
|
222
|
+
const cerbosDeployment = getFile(
|
|
223
|
+
result.files,
|
|
224
|
+
'infra/minikube/k8s/authz/cerbos/cerbos.deployment.yaml',
|
|
225
|
+
);
|
|
226
|
+
const envExample = getFile(result.files, 'infra/minikube/.env.example');
|
|
227
|
+
const resetScript = getFile(result.files, 'infra/minikube/scripts/reset.sh');
|
|
228
|
+
const statusScript = getFile(result.files, 'infra/minikube/scripts/status.sh');
|
|
229
|
+
const upScript = getFile(result.files, 'infra/minikube/scripts/up.sh');
|
|
230
|
+
|
|
231
|
+
expect(paths).toContain('infra/minikube/k8s/namespaces/cerbos.yaml');
|
|
232
|
+
expect(kustomization).toContain('namespaces/cerbos.yaml');
|
|
233
|
+
expect(readme).toContain('- Provider namespaces: `cerbos`');
|
|
234
|
+
expect(readme).toContain('## Provider Lifecycle Contributions');
|
|
235
|
+
expect(readme).toContain('`cerbos`: namespace `cerbos`');
|
|
236
|
+
expect(readme).toContain('`deployment/cerbos` in namespace `cerbos`');
|
|
237
|
+
expect(readme).toContain('`http` = `http://cerbos.cerbos.svc.cluster.local:3592`');
|
|
238
|
+
expect(cerbosDeployment).toContain('namespace: cerbos');
|
|
239
|
+
expect(cerbosDeployment).not.toContain('namespace: app');
|
|
240
|
+
expect(envExample).toContain('CERBOS_URL=http://cerbos.cerbos.svc.cluster.local:3592');
|
|
241
|
+
expect(resetScript).toContain('delete namespace cerbos');
|
|
242
|
+
expect(upScript).toContain('wait_for_provider_readiness');
|
|
243
|
+
expect(upScript).toContain('run_provider_migrations');
|
|
244
|
+
expect(upScript).toContain('run_provider_reconciliation');
|
|
245
|
+
expect(upScript).toContain('rollout status "deployment/cerbos" --timeout=180s');
|
|
246
|
+
expect(upScript.indexOf('kubectl --context "${PROFILE}" apply -k "${K8S_DIR}"')).toBeLessThan(
|
|
247
|
+
upScript.lastIndexOf('wait_for_provider_readiness'),
|
|
248
|
+
);
|
|
249
|
+
expect(statusScript).toContain('for namespace in app supabase cerbos; do');
|
|
250
|
+
expect(statusScript).toContain(
|
|
251
|
+
'- provider cerbos endpoint/http: http://cerbos.cerbos.svc.cluster.local:3592',
|
|
252
|
+
);
|
|
253
|
+
expect(statusScript).toContain('- provider cerbos/cerbos: ready');
|
|
186
254
|
});
|
|
187
255
|
|
|
188
|
-
test('
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
existingConfig: null,
|
|
192
|
-
withoutPython: true,
|
|
256
|
+
test('uses generated Supabase runtime ownership as an immutable script decision', () => {
|
|
257
|
+
const result = generateInfrastructure(createSupabaseManifest(), {
|
|
258
|
+
appManifest: createAppManifest('scanner'),
|
|
193
259
|
});
|
|
260
|
+
const envExample = getFile(result.files, 'infra/minikube/.env.example');
|
|
261
|
+
const upScript = getFile(result.files, 'infra/minikube/scripts/up.sh');
|
|
194
262
|
|
|
195
|
-
expect(
|
|
196
|
-
expect(
|
|
197
|
-
|
|
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();
|
|
263
|
+
expect(envExample).not.toContain('SUPABASE_RUNTIME_ENABLED=');
|
|
264
|
+
expect(upScript).toContain('SUPABASE_RUNTIME_ENABLED="true"');
|
|
265
|
+
expect(upScript).not.toContain('SUPABASE_RUNTIME_ENABLED="${SUPABASE_RUNTIME_ENABLED');
|
|
202
266
|
});
|
|
203
267
|
|
|
204
|
-
test('
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
268
|
+
test('wires configured OAuth providers into GoTrue runtime env without serializing secrets', () => {
|
|
269
|
+
const result = generateInfrastructure(
|
|
270
|
+
{
|
|
271
|
+
...createSupabaseManifest(),
|
|
272
|
+
secretStore: {
|
|
273
|
+
provider: 'supabase-vault',
|
|
274
|
+
},
|
|
275
|
+
auth: {
|
|
276
|
+
scope: 'global',
|
|
277
|
+
provider: 'supabase',
|
|
278
|
+
oauth: {
|
|
279
|
+
enabled: true,
|
|
280
|
+
callbackRoute: '/auth/callback',
|
|
281
|
+
providers: [
|
|
282
|
+
{
|
|
283
|
+
id: 'google',
|
|
284
|
+
enabled: true,
|
|
285
|
+
credentialsRef: 'auth/oauth/google',
|
|
286
|
+
},
|
|
287
|
+
],
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
appManifest: createAppManifest('oauth-app'),
|
|
293
|
+
},
|
|
213
294
|
);
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
).
|
|
219
|
-
expect(
|
|
220
|
-
|
|
221
|
-
).
|
|
295
|
+
const envExample = getFile(result.files, 'infra/minikube/.env.example');
|
|
296
|
+
const upScript = getFile(result.files, 'infra/minikube/scripts/up.sh');
|
|
297
|
+
|
|
298
|
+
expect(envExample).toContain('# google: auth/oauth/google');
|
|
299
|
+
expect(envExample).toContain('GOTRUE_EXTERNAL_GOOGLE_CLIENT_ID=');
|
|
300
|
+
expect(envExample).toContain('GOTRUE_EXTERNAL_GOOGLE_SECRET=');
|
|
301
|
+
expect(upScript).toContain('GOTRUE_EXTERNAL_GOOGLE_ENABLED=true');
|
|
302
|
+
expect(upScript).toContain(
|
|
303
|
+
'GOTRUE_EXTERNAL_GOOGLE_CLIENT_ID=${GOTRUE_EXTERNAL_GOOGLE_CLIENT_ID}',
|
|
304
|
+
);
|
|
305
|
+
expect(upScript).toContain('credentialsRef auth/oauth/google');
|
|
306
|
+
expect(upScript).toContain("resolve_vault_secret_field 'auth/oauth/google' 'clientSecret'");
|
|
307
|
+
expect(upScript).toContain(
|
|
308
|
+
'set_required_env_default ADDITIONAL_REDIRECT_URLS "$(derive_additional_redirect_urls)"',
|
|
309
|
+
);
|
|
310
|
+
expect(upScript).toContain('site_url_without_slash}/${callback_path}');
|
|
311
|
+
expect(upScript).toContain('OAUTH_NATIVE_REDIRECT_URLS');
|
|
312
|
+
expect(upScript).not.toContain('write_env_value GOTRUE_EXTERNAL_GOOGLE_CLIENT_ID');
|
|
313
|
+
expect(upScript).not.toContain('write_env_value GOTRUE_EXTERNAL_GOOGLE_SECRET');
|
|
314
|
+
expect(upScript).not.toContain('sentinel-client-secret-value');
|
|
222
315
|
});
|
|
223
316
|
|
|
224
|
-
test('generates
|
|
225
|
-
const
|
|
317
|
+
test('generates required secrets even when copied env example contains empty keys', () => {
|
|
318
|
+
const result = generateInfrastructure(createSupabaseManifest(), {
|
|
319
|
+
appManifest: createAppManifest('scanner'),
|
|
320
|
+
});
|
|
321
|
+
const upScript = getFile(result.files, 'infra/minikube/scripts/up.sh');
|
|
226
322
|
|
|
227
|
-
expect(
|
|
228
|
-
expect(
|
|
229
|
-
expect(
|
|
230
|
-
'
|
|
323
|
+
expect(upScript).toContain('load_env_file_preserving_process_env');
|
|
324
|
+
expect(upScript).toContain('APP_IMAGE="${APP_IMAGE:-}"');
|
|
325
|
+
expect(upScript).toContain(
|
|
326
|
+
'load_env_file_preserving_process_env\nSUPABASE_RUNTIME_ENABLED="true"',
|
|
231
327
|
);
|
|
232
|
-
expect(
|
|
233
|
-
|
|
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.',
|
|
328
|
+
expect(upScript.indexOf('load_env_file_preserving_process_env')).toBeLessThan(
|
|
329
|
+
upScript.lastIndexOf('APP_IMAGE="${APP_IMAGE:-ankh/scanner:dev}"'),
|
|
237
330
|
);
|
|
238
|
-
expect(
|
|
239
|
-
expect(
|
|
240
|
-
expect(
|
|
241
|
-
expect(script).toContain('psql "${SUPABASE_DB_URL}" -v ON_ERROR_STOP=1 -q -f "${sql_file}"');
|
|
331
|
+
expect(upScript).toContain('set_required_env_default POSTGRES_PASSWORD "$(random_hex 32)"');
|
|
332
|
+
expect(upScript).toContain('write_env_value "${key}" "${value}"');
|
|
333
|
+
expect(upScript).toContain('set_optional_env_default SMTP_USER ""');
|
|
242
334
|
});
|
|
243
335
|
|
|
244
|
-
test('
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
fields: ['email', 'displayName'],
|
|
250
|
-
},
|
|
251
|
-
},
|
|
252
|
-
} satisfies Partial<InfraManifestInput>;
|
|
253
|
-
const script = getGeneratedFileContent(
|
|
254
|
-
'chess',
|
|
255
|
-
'infra/minikube/scripts/supabase-local-env.sh',
|
|
256
|
-
overrides,
|
|
257
|
-
);
|
|
258
|
-
const status = getGeneratedFileContent('chess', 'infra/minikube/scripts/status.sh', overrides);
|
|
336
|
+
test('starts Supabase schema owners before app migrations and profile reconciliation', () => {
|
|
337
|
+
const result = generateInfrastructure(createSupabaseManifest(), {
|
|
338
|
+
appManifest: createAppManifest('scanner'),
|
|
339
|
+
});
|
|
340
|
+
const upScript = getFile(result.files, 'infra/minikube/scripts/up.sh');
|
|
259
341
|
|
|
260
|
-
expect(
|
|
261
|
-
expect(
|
|
262
|
-
|
|
263
|
-
);
|
|
264
|
-
expect(
|
|
265
|
-
|
|
266
|
-
expect(script).toContain('Supabase project workdir: ${SUPABASE_PROJECT_DIR}');
|
|
267
|
-
expect(script).toContain('Exit status: ${status}');
|
|
268
|
-
expect(script).toContain('ankhorage_internal.generated_schema_state');
|
|
269
|
-
expect(script).toContain('check_immutable_migrations_applied');
|
|
270
|
-
expect(status).toContain('- immutable migrations: applied');
|
|
271
|
-
expect(status).toContain('- immutable migrations: pending');
|
|
272
|
-
expect(status).toContain('- profile reconciliation: applied, checksum matches');
|
|
273
|
-
expect(status).toContain('- profile schema: verified');
|
|
274
|
-
expect(status).toContain('reserved conflicting identity table public.users exists');
|
|
275
|
-
expect(status).toContain('stale managed profile column');
|
|
276
|
-
expect(status).toContain('own-profile SELECT policy is missing or has unsafe definition');
|
|
277
|
-
expect(status).toContain('unexpected profile table RLS policy exists');
|
|
278
|
-
expect(status).toContain(
|
|
279
|
-
"has_any_column_privilege('anon', format('public.%I', profile_table), 'SELECT')",
|
|
342
|
+
expect(upScript).toContain('apply_supabase_runtime_workloads');
|
|
343
|
+
expect(upScript).toContain('exec deployment/postgres -- psql -U postgres');
|
|
344
|
+
expect(upScript).toContain('create schema if not exists vault');
|
|
345
|
+
expect(upScript).toContain('grant execute on function pg_read_file(text) to public');
|
|
346
|
+
expect(upScript.indexOf('postgres.init.configmap.yaml')).toBeLessThan(
|
|
347
|
+
upScript.indexOf('postgres.pvc.yaml'),
|
|
280
348
|
);
|
|
281
|
-
expect(
|
|
282
|
-
|
|
349
|
+
expect(upScript.lastIndexOf(' wait_for_supabase_database')).toBeLessThan(
|
|
350
|
+
upScript.lastIndexOf(' bootstrap_supabase_database'),
|
|
283
351
|
);
|
|
284
|
-
expect(
|
|
285
|
-
|
|
352
|
+
expect(upScript).toContain('rollout status deployment/postgres --timeout=900s');
|
|
353
|
+
expect(upScript).toContain('rollout status deployment/gateway --timeout=600s');
|
|
354
|
+
const runMigrationsCallIndex = upScript.lastIndexOf(' run_supabase_migrations');
|
|
355
|
+
const reconcileProfileCallIndex = upScript.lastIndexOf(' reconcile_supabase_profile');
|
|
356
|
+
const appRuntimeApplyIndex = upScript.lastIndexOf(
|
|
357
|
+
'kubectl --context "${PROFILE}" apply -k "${K8S_DIR}"',
|
|
286
358
|
);
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
expect(status).toContain(
|
|
291
|
-
"has_any_column_privilege('authenticated', format('public.%I', profile_table), 'INSERT')",
|
|
359
|
+
|
|
360
|
+
expect(upScript.lastIndexOf(' apply_supabase_runtime_workloads')).toBeLessThan(
|
|
361
|
+
upScript.indexOf('rollout status deployment/auth'),
|
|
292
362
|
);
|
|
293
|
-
expect(status).
|
|
294
|
-
|
|
363
|
+
expect(upScript.indexOf('rollout status deployment/auth')).toBeLessThan(runMigrationsCallIndex);
|
|
364
|
+
expect(upScript.indexOf('rollout status deployment/storage')).toBeLessThan(
|
|
365
|
+
runMigrationsCallIndex,
|
|
295
366
|
);
|
|
296
|
-
expect(
|
|
297
|
-
'
|
|
367
|
+
expect(upScript.indexOf(' reconcile_supabase_profile')).toBeLessThan(
|
|
368
|
+
upScript.indexOf(' reload_postgrest_schema'),
|
|
298
369
|
);
|
|
299
|
-
expect(status).
|
|
300
|
-
'
|
|
370
|
+
expect(upScript.indexOf('rollout status deployment/studio')).toBeLessThan(
|
|
371
|
+
upScript.indexOf('"${PORT_FORWARD_SCRIPT}" start studio'),
|
|
301
372
|
);
|
|
302
|
-
expect(
|
|
373
|
+
expect(runMigrationsCallIndex).toBeLessThan(reconcileProfileCallIndex);
|
|
374
|
+
expect(reconcileProfileCallIndex).toBeLessThan(appRuntimeApplyIndex);
|
|
303
375
|
});
|
|
304
376
|
|
|
305
|
-
test('
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
deployment: {
|
|
309
|
-
target: 'minikube',
|
|
310
|
-
monitoring: false,
|
|
311
|
-
},
|
|
312
|
-
plugins: [],
|
|
313
|
-
},
|
|
314
|
-
supabaseProjectId: null,
|
|
315
|
-
env: {
|
|
316
|
-
SUPABASE_PROJECT_ID: 'ambient-project',
|
|
317
|
-
},
|
|
377
|
+
test('treats port-forwards as slug-owned lifecycle resources', () => {
|
|
378
|
+
const result = generateInfrastructure(createSupabaseManifest(), {
|
|
379
|
+
appManifest: createAppManifest('uwm'),
|
|
318
380
|
});
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
expect(
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
},
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
expect(
|
|
343
|
-
expect(
|
|
344
|
-
|
|
345
|
-
)
|
|
346
|
-
|
|
381
|
+
const portForwardScript = getFile(result.files, 'infra/minikube/scripts/port-forward.sh');
|
|
382
|
+
const statusScript = getFile(result.files, 'infra/minikube/scripts/status.sh');
|
|
383
|
+
const upScript = getFile(result.files, 'infra/minikube/scripts/up.sh');
|
|
384
|
+
const downScript = getFile(result.files, 'infra/minikube/scripts/down.sh');
|
|
385
|
+
|
|
386
|
+
expect(portForwardScript).toContain('service/app-runtime');
|
|
387
|
+
expect(portForwardScript).toContain('service/gateway');
|
|
388
|
+
expect(portForwardScript).toContain('service/studio');
|
|
389
|
+
expect(portForwardScript).toContain('service/postgres');
|
|
390
|
+
expect(portForwardScript).toContain('${PROFILE}-${1}.pid');
|
|
391
|
+
expect(portForwardScript).toContain('crashed stale_pid');
|
|
392
|
+
expect(portForwardScript).toContain('is_port_accepting()');
|
|
393
|
+
expect(portForwardScript).toContain('recent kubectl port-forward log');
|
|
394
|
+
expect(portForwardScript).toContain('did not become ready on local port');
|
|
395
|
+
expect(portForwardScript).toContain('not ready pid=');
|
|
396
|
+
expect(portForwardScript).toContain('terminate_forward_process()');
|
|
397
|
+
expect(portForwardScript).toContain('ps -p "${pid}" -o command=');
|
|
398
|
+
expect(portForwardScript).toContain('ps -p "${pid}" -o comm=');
|
|
399
|
+
expect(portForwardScript).toContain('is_owned_forward_process()');
|
|
400
|
+
expect(portForwardScript).toContain(
|
|
401
|
+
'*"kubectl --context ${PROFILE} -n ${namespace} port-forward ${resource} ${local_port}:${remote_port}"*',
|
|
402
|
+
);
|
|
403
|
+
expect(portForwardScript).toContain('kill -KILL "${pid}"');
|
|
404
|
+
expect(portForwardScript).toContain('failed to stop owned port-forward pid');
|
|
405
|
+
expect(portForwardScript).toContain('local port ${local_port} is still accepting connections');
|
|
406
|
+
expect(portForwardScript).toContain('refusing to terminate non-owned live pid');
|
|
407
|
+
expect(portForwardScript).toContain(
|
|
408
|
+
'stale pid file pointed at non-owned live pid ${pid}; removing ownership record ${pid_file}',
|
|
409
|
+
);
|
|
410
|
+
expect(portForwardScript).toContain(
|
|
411
|
+
'port occupied by non-owned process on local port ${local_port}',
|
|
412
|
+
);
|
|
413
|
+
expect(portForwardScript).toContain('stale_pid_non_owned=${pid}');
|
|
414
|
+
expect(statusScript).toContain('port-forward.sh" status all');
|
|
415
|
+
expect(upScript).toContain('set -Eeuo pipefail');
|
|
416
|
+
expect(upScript).toContain('trap cleanup_failed_up ERR');
|
|
417
|
+
expect(upScript).toContain('start supabase-gateway');
|
|
418
|
+
expect(upScript).toContain('start db-migration');
|
|
419
|
+
expect(upScript).toContain('Failed to start db-migration port-forward for Supabase bootstrap.');
|
|
420
|
+
expect(upScript.indexOf('trap cleanup_failed_up ERR')).toBeLessThan(
|
|
421
|
+
upScript.indexOf('require_command minikube'),
|
|
422
|
+
);
|
|
423
|
+
expect(upScript).toContain('trap - ERR');
|
|
424
|
+
expect(downScript).toContain('port-forward.sh" stop all');
|
|
347
425
|
});
|
|
348
426
|
});
|
|
349
427
|
|
|
350
|
-
function
|
|
351
|
-
namespace: string,
|
|
352
|
-
filePath: string,
|
|
353
|
-
overrides: Partial<InfraManifestInput> = {},
|
|
354
|
-
supabaseProjectId = namespace,
|
|
355
|
-
): string {
|
|
356
|
-
const artifact = generateMinikubeBaseArtifacts({
|
|
357
|
-
manifest: createInfraManifest(overrides),
|
|
358
|
-
namespace,
|
|
359
|
-
supabaseProjectId,
|
|
360
|
-
extraResources: [],
|
|
361
|
-
extraEnvEntries: [],
|
|
362
|
-
}).find((file) => file.path === filePath);
|
|
363
|
-
|
|
364
|
-
if (!artifact) {
|
|
365
|
-
throw new Error(`Expected generated artifact at ${filePath}`);
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
return artifact.content;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
function countOccurrences(source: string, needle: string): number {
|
|
372
|
-
return source.split(needle).length - 1;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
function createInfraManifest(overrides: Partial<InfraManifestInput> = {}): InfraManifestInput {
|
|
428
|
+
function createSupabaseManifest(): InfraManifestInput {
|
|
376
429
|
return {
|
|
377
430
|
deployment: {
|
|
378
431
|
target: 'minikube',
|
|
@@ -381,273 +434,21 @@ function createInfraManifest(overrides: Partial<InfraManifestInput> = {}): Infra
|
|
|
381
434
|
auth: {
|
|
382
435
|
scope: 'global',
|
|
383
436
|
provider: 'supabase',
|
|
384
|
-
authorization: {
|
|
385
|
-
kind: 'RBAC',
|
|
386
|
-
engine: 'native',
|
|
387
|
-
},
|
|
388
437
|
},
|
|
389
438
|
database: {
|
|
390
439
|
provider: 'supabase',
|
|
391
440
|
tier: 'dev',
|
|
392
441
|
},
|
|
393
|
-
|
|
394
|
-
cdn: false,
|
|
395
|
-
},
|
|
396
|
-
plugins: [],
|
|
397
|
-
...overrides,
|
|
398
|
-
auth: {
|
|
399
|
-
scope: 'global',
|
|
442
|
+
storage: {
|
|
400
443
|
provider: 'supabase',
|
|
401
|
-
|
|
402
|
-
kind: 'RBAC',
|
|
403
|
-
engine: 'native',
|
|
404
|
-
},
|
|
405
|
-
...overrides.auth,
|
|
406
|
-
},
|
|
407
|
-
};
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
function runGeneratedSupabaseScript(args: {
|
|
411
|
-
projectId: string;
|
|
412
|
-
supabaseProjectId?: string | null;
|
|
413
|
-
existingConfig: string | null;
|
|
414
|
-
env?: Record<string, string>;
|
|
415
|
-
withoutPython?: boolean;
|
|
416
|
-
}): {
|
|
417
|
-
status: number | null;
|
|
418
|
-
combinedOutput: string;
|
|
419
|
-
root: string;
|
|
420
|
-
configPath: string;
|
|
421
|
-
supabaseLogPath: string;
|
|
422
|
-
} {
|
|
423
|
-
const tempRoot = mkdtempSync(join(tmpdir(), 'infra-supabase-identity-'));
|
|
424
|
-
const root = join(tempRoot, 'infra', 'minikube');
|
|
425
|
-
const scriptsDir = join(root, 'scripts');
|
|
426
|
-
const supabaseDir = join(root, 'supabase');
|
|
427
|
-
const binDir = join(tempRoot, 'bin');
|
|
428
|
-
const appSourceDir = join(tempRoot, 'app');
|
|
429
|
-
const scriptPath = join(scriptsDir, 'supabase-local-env.sh');
|
|
430
|
-
const configPath = join(supabaseDir, 'config.toml');
|
|
431
|
-
const supabaseLogPath = join(tempRoot, 'supabase.log');
|
|
432
|
-
|
|
433
|
-
mkdirSync(scriptsDir, { recursive: true });
|
|
434
|
-
mkdirSync(binDir, { recursive: true });
|
|
435
|
-
mkdirSync(appSourceDir, { recursive: true });
|
|
436
|
-
writeFileSync(join(root, '.env.example'), '');
|
|
437
|
-
writeFileSync(supabaseLogPath, '');
|
|
438
|
-
|
|
439
|
-
if (args.existingConfig !== null) {
|
|
440
|
-
mkdirSync(supabaseDir, { recursive: true });
|
|
441
|
-
writeFileSync(configPath, args.existingConfig);
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
const script = getGeneratedFileContent(
|
|
445
|
-
args.projectId,
|
|
446
|
-
'infra/minikube/scripts/supabase-local-env.sh',
|
|
447
|
-
{},
|
|
448
|
-
args.supabaseProjectId === undefined ? args.projectId : args.supabaseProjectId,
|
|
449
|
-
);
|
|
450
|
-
writeFileSync(scriptPath, script);
|
|
451
|
-
chmodSync(scriptPath, 0o755);
|
|
452
|
-
|
|
453
|
-
const supabaseStubPath = join(binDir, 'supabase');
|
|
454
|
-
writeFileSync(
|
|
455
|
-
supabaseStubPath,
|
|
456
|
-
`#!/bin/bash
|
|
457
|
-
set -euo pipefail
|
|
458
|
-
printf '%s\\n' "$*" >> "\${SUPABASE_STUB_LOG}"
|
|
459
|
-
|
|
460
|
-
if [[ "\${1:-}" == "--version" ]]; then
|
|
461
|
-
echo "supabase 2.106.0"
|
|
462
|
-
exit 0
|
|
463
|
-
fi
|
|
464
|
-
|
|
465
|
-
if [[ "\${1:-}" == "status" && "\${2:-}" == "--help" ]]; then
|
|
466
|
-
echo "--workdir"
|
|
467
|
-
exit 0
|
|
468
|
-
fi
|
|
469
|
-
|
|
470
|
-
if [[ "\${1:-}" == "migration" && "\${2:-}" == "up" && "\${3:-}" == "--help" ]]; then
|
|
471
|
-
echo "--local"
|
|
472
|
-
exit 0
|
|
473
|
-
fi
|
|
474
|
-
|
|
475
|
-
if [[ "\${1:-}" == "--workdir" && "\${3:-}" == "init" && "\${4:-}" == "--yes" ]]; then
|
|
476
|
-
mkdir -p "\${2}/supabase"
|
|
477
|
-
printf '%s\\n' 'project_id = "minikube"' '[api]' 'port = 54321' > "\${2}/supabase/config.toml"
|
|
478
|
-
exit 0
|
|
479
|
-
fi
|
|
480
|
-
|
|
481
|
-
if [[ "\${1:-}" == "--workdir" && "\${3:-}" == "status" && "\${4:-}" == "-o" && "\${5:-}" == "env" ]]; then
|
|
482
|
-
cat <<'ENV'
|
|
483
|
-
DB_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres
|
|
484
|
-
API_URL=http://127.0.0.1:54321
|
|
485
|
-
ANON_KEY=anon
|
|
486
|
-
SERVICE_ROLE_KEY=service
|
|
487
|
-
JWT_SECRET=secret
|
|
488
|
-
ENV
|
|
489
|
-
exit 0
|
|
490
|
-
fi
|
|
491
|
-
|
|
492
|
-
if [[ "\${1:-}" == "--workdir" && "\${3:-}" == "status" ]]; then
|
|
493
|
-
exit 0
|
|
494
|
-
fi
|
|
495
|
-
|
|
496
|
-
if [[ "\${1:-}" == "--workdir" && "\${3:-}" == "migration" && "\${4:-}" == "up" && "\${5:-}" == "--local" ]]; then
|
|
497
|
-
exit 0
|
|
498
|
-
fi
|
|
499
|
-
|
|
500
|
-
echo "unexpected supabase call: $*" >&2
|
|
501
|
-
exit 1
|
|
502
|
-
`,
|
|
503
|
-
);
|
|
504
|
-
chmodSync(supabaseStubPath, 0o755);
|
|
505
|
-
|
|
506
|
-
const psqlStubPath = join(binDir, 'psql');
|
|
507
|
-
writeFileSync(
|
|
508
|
-
psqlStubPath,
|
|
509
|
-
`#!/bin/bash
|
|
510
|
-
exit 0
|
|
511
|
-
`,
|
|
512
|
-
);
|
|
513
|
-
chmodSync(psqlStubPath, 0o755);
|
|
514
|
-
|
|
515
|
-
const result = spawnSync('/bin/bash', [scriptPath], {
|
|
516
|
-
cwd: tempRoot,
|
|
517
|
-
env: {
|
|
518
|
-
...(args.withoutPython ? {} : process.env),
|
|
519
|
-
PATH: args.withoutPython ? binDir : `${binDir}:${process.env.PATH ?? ''}`,
|
|
520
|
-
APP_SOURCE_DIR: appSourceDir,
|
|
521
|
-
SUPABASE_STUB_LOG: supabaseLogPath,
|
|
522
|
-
...args.env,
|
|
444
|
+
buckets: ['assets'],
|
|
523
445
|
},
|
|
524
|
-
|
|
525
|
-
});
|
|
526
|
-
|
|
527
|
-
return {
|
|
528
|
-
status: result.status,
|
|
529
|
-
combinedOutput: `${result.stdout}${result.stderr}`,
|
|
530
|
-
root,
|
|
531
|
-
configPath,
|
|
532
|
-
supabaseLogPath,
|
|
446
|
+
plugins: [],
|
|
533
447
|
};
|
|
534
448
|
}
|
|
535
449
|
|
|
536
|
-
function
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
}): {
|
|
541
|
-
status: number | null;
|
|
542
|
-
combinedOutput: string;
|
|
543
|
-
kubectlLog: string;
|
|
544
|
-
supabaseLog: string;
|
|
545
|
-
} {
|
|
546
|
-
const tempRoot = mkdtempSync(join(tmpdir(), 'infra-status-'));
|
|
547
|
-
const root = join(tempRoot, 'infra', 'minikube');
|
|
548
|
-
const scriptsDir = join(root, 'scripts');
|
|
549
|
-
const binDir = join(tempRoot, 'bin');
|
|
550
|
-
const scriptPath = join(scriptsDir, 'status.sh');
|
|
551
|
-
const kubectlLogPath = join(tempRoot, 'kubectl.log');
|
|
552
|
-
const supabaseLogPath = join(tempRoot, 'supabase.log');
|
|
553
|
-
|
|
554
|
-
mkdirSync(scriptsDir, { recursive: true });
|
|
555
|
-
mkdirSync(binDir, { recursive: true });
|
|
556
|
-
writeFileSync(join(root, '.env.example'), '');
|
|
557
|
-
writeFileSync(kubectlLogPath, '');
|
|
558
|
-
writeFileSync(supabaseLogPath, '');
|
|
559
|
-
|
|
560
|
-
const artifact = generateMinikubeBaseArtifacts({
|
|
561
|
-
manifest: args.manifest,
|
|
562
|
-
namespace: 'plain',
|
|
563
|
-
supabaseProjectId: args.supabaseProjectId,
|
|
564
|
-
extraResources: [],
|
|
565
|
-
extraEnvEntries: [],
|
|
566
|
-
}).find((file) => file.path === 'infra/minikube/scripts/status.sh');
|
|
567
|
-
|
|
568
|
-
if (!artifact) {
|
|
569
|
-
throw new Error('Expected generated status.sh artifact');
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
writeFileSync(scriptPath, artifact.content);
|
|
573
|
-
chmodSync(scriptPath, 0o755);
|
|
574
|
-
|
|
575
|
-
const kubectlStubPath = join(binDir, 'kubectl');
|
|
576
|
-
writeFileSync(
|
|
577
|
-
kubectlStubPath,
|
|
578
|
-
`#!/bin/bash
|
|
579
|
-
set -euo pipefail
|
|
580
|
-
printf '%s\\n' "$*" >> "\${KUBECTL_STUB_LOG}"
|
|
581
|
-
|
|
582
|
-
if [[ "\${1:-}" == "config" && "\${2:-}" == "current-context" ]]; then
|
|
583
|
-
echo "minikube"
|
|
584
|
-
exit 0
|
|
585
|
-
fi
|
|
586
|
-
|
|
587
|
-
if [[ "\${1:-}" == "cluster-info" ]]; then
|
|
588
|
-
echo "Kubernetes control plane is running"
|
|
589
|
-
exit 0
|
|
590
|
-
fi
|
|
591
|
-
|
|
592
|
-
if [[ "\${1:-}" == "get" && "\${2:-}" == "namespace" ]]; then
|
|
593
|
-
exit 0
|
|
594
|
-
fi
|
|
595
|
-
|
|
596
|
-
if [[ "\${1:-}" == "get" && "\${2:-}" == "all" ]]; then
|
|
597
|
-
echo "No resources found"
|
|
598
|
-
exit 0
|
|
599
|
-
fi
|
|
600
|
-
|
|
601
|
-
echo "unexpected kubectl call: $*" >&2
|
|
602
|
-
exit 1
|
|
603
|
-
`,
|
|
604
|
-
);
|
|
605
|
-
chmodSync(kubectlStubPath, 0o755);
|
|
606
|
-
|
|
607
|
-
const supabaseStubPath = join(binDir, 'supabase');
|
|
608
|
-
writeFileSync(
|
|
609
|
-
supabaseStubPath,
|
|
610
|
-
`#!/bin/bash
|
|
611
|
-
set -euo pipefail
|
|
612
|
-
printf '%s\\n' "$*" >> "\${SUPABASE_STUB_LOG}"
|
|
613
|
-
|
|
614
|
-
if [[ "\${1:-}" == "--version" ]]; then
|
|
615
|
-
echo "supabase 2.106.0"
|
|
616
|
-
exit 0
|
|
617
|
-
fi
|
|
618
|
-
|
|
619
|
-
if [[ "\${1:-}" == "status" && "\${2:-}" == "--help" ]]; then
|
|
620
|
-
echo "--workdir"
|
|
621
|
-
exit 0
|
|
622
|
-
fi
|
|
623
|
-
|
|
624
|
-
if [[ "\${1:-}" == "migration" && "\${2:-}" == "up" && "\${3:-}" == "--help" ]]; then
|
|
625
|
-
echo "--local"
|
|
626
|
-
exit 0
|
|
627
|
-
fi
|
|
628
|
-
|
|
629
|
-
echo "unexpected supabase call: $*" >&2
|
|
630
|
-
exit 1
|
|
631
|
-
`,
|
|
632
|
-
);
|
|
633
|
-
chmodSync(supabaseStubPath, 0o755);
|
|
634
|
-
|
|
635
|
-
const result = spawnSync('/bin/bash', [scriptPath], {
|
|
636
|
-
cwd: tempRoot,
|
|
637
|
-
env: {
|
|
638
|
-
...process.env,
|
|
639
|
-
PATH: `${binDir}:${process.env.PATH ?? ''}`,
|
|
640
|
-
KUBECTL_STUB_LOG: kubectlLogPath,
|
|
641
|
-
SUPABASE_STUB_LOG: supabaseLogPath,
|
|
642
|
-
...args.env,
|
|
643
|
-
},
|
|
644
|
-
encoding: 'utf-8',
|
|
645
|
-
});
|
|
646
|
-
|
|
647
|
-
return {
|
|
648
|
-
status: result.status,
|
|
649
|
-
combinedOutput: `${result.stdout}${result.stderr}`,
|
|
650
|
-
kubectlLog: readFileSync(kubectlLogPath, 'utf-8'),
|
|
651
|
-
supabaseLog: readFileSync(supabaseLogPath, 'utf-8'),
|
|
652
|
-
};
|
|
450
|
+
function getFile(files: readonly { path: string; content: string }[], path: string): string {
|
|
451
|
+
const file = files.find((candidate) => candidate.path === path);
|
|
452
|
+
if (!file) throw new Error(`Missing generated file: ${path}`);
|
|
453
|
+
return file.content;
|
|
653
454
|
}
|