@ankhorage/infra 0.4.2 → 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 +8 -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 -1843
- 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.ts +1 -1
- package/src/adapters/minikube/authz/cerbos/index.ts +25 -3
- package/src/adapters/minikube/base/index.test.ts +380 -675
- package/src/adapters/minikube/base/index.ts +2935 -1878
- 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,429 +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')",
|
|
280
|
-
);
|
|
281
|
-
expect(status).toContain(
|
|
282
|
-
"has_any_column_privilege('anon', format('public.%I', profile_table), 'INSERT')",
|
|
283
|
-
);
|
|
284
|
-
expect(status).toContain(
|
|
285
|
-
"has_any_column_privilege('anon', format('public.%I', profile_table), 'UPDATE')",
|
|
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'),
|
|
286
348
|
);
|
|
287
|
-
expect(
|
|
288
|
-
|
|
349
|
+
expect(upScript.lastIndexOf(' wait_for_supabase_database')).toBeLessThan(
|
|
350
|
+
upScript.lastIndexOf(' bootstrap_supabase_database'),
|
|
289
351
|
);
|
|
290
|
-
expect(
|
|
291
|
-
|
|
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}"',
|
|
292
358
|
);
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
expect(status).toContain(
|
|
297
|
-
'authenticated role must not have profile table INSERT, DELETE, or REFERENCES privilege',
|
|
359
|
+
|
|
360
|
+
expect(upScript.lastIndexOf(' apply_supabase_runtime_workloads')).toBeLessThan(
|
|
361
|
+
upScript.indexOf('rollout status deployment/auth'),
|
|
298
362
|
);
|
|
299
|
-
expect(status).
|
|
300
|
-
|
|
363
|
+
expect(upScript.indexOf('rollout status deployment/auth')).toBeLessThan(runMigrationsCallIndex);
|
|
364
|
+
expect(upScript.indexOf('rollout status deployment/storage')).toBeLessThan(
|
|
365
|
+
runMigrationsCallIndex,
|
|
301
366
|
);
|
|
302
|
-
expect(
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
test('generates relation-safe disabled profile verification for local env and status', () => {
|
|
306
|
-
const script = getGeneratedFileContent('chess', 'infra/minikube/scripts/supabase-local-env.sh');
|
|
307
|
-
const status = getGeneratedFileContent('chess', 'infra/minikube/scripts/status.sh');
|
|
308
|
-
const expectedDynamicLookup = `execute
|
|
309
|
-
'select exists (
|
|
310
|
-
select 1
|
|
311
|
-
from ankhorage_internal.generated_schema_state
|
|
312
|
-
where artifact_key = $1
|
|
313
|
-
)'
|
|
314
|
-
into has_profile_state
|
|
315
|
-
using 'auth.profile';`;
|
|
316
|
-
|
|
317
|
-
for (const generated of [script, status]) {
|
|
318
|
-
expect(generated).toContain('has_profile_state boolean := false;');
|
|
319
|
-
expect(generated).toContain(expectedDynamicLookup);
|
|
320
|
-
expect(generated).toContain(
|
|
321
|
-
'manifest auth.profile.table was removed but local generated auth.profile state still exists; reset local Supabase or add an explicit cleanup migration',
|
|
322
|
-
);
|
|
323
|
-
expect(generated).not.toContain(
|
|
324
|
-
`if to_regclass('ankhorage_internal.generated_schema_state') is not null
|
|
325
|
-
and exists (`,
|
|
326
|
-
);
|
|
327
|
-
expect(generated.replace(expectedDynamicLookup, '')).not.toContain(
|
|
328
|
-
`from ankhorage_internal.generated_schema_state
|
|
329
|
-
where artifact_key = 'auth.profile'`,
|
|
330
|
-
);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
expect(script).toContain('Generated profile disabled-state verification');
|
|
334
|
-
expect(status).toContain('- profile reconciliation: skipped (no profile table configured)');
|
|
335
|
-
expect(status).toContain('- profile schema: skipped (no profile table configured)');
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
test('status succeeds for a clean disabled profile local Supabase stack', () => {
|
|
339
|
-
const run = runGeneratedStatusScript({
|
|
340
|
-
manifest: createInfraManifest(),
|
|
341
|
-
supabaseProjectId: 'plain',
|
|
342
|
-
env: {
|
|
343
|
-
AUTH_RUNTIME_MODE: 'local',
|
|
344
|
-
},
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
expect(run.status).toBe(0);
|
|
348
|
-
expect(run.combinedOutput).toContain('- immutable migrations: applied');
|
|
349
|
-
expect(run.combinedOutput).toContain(
|
|
350
|
-
'- profile reconciliation: skipped (no profile table configured)',
|
|
367
|
+
expect(upScript.indexOf(' reconcile_supabase_profile')).toBeLessThan(
|
|
368
|
+
upScript.indexOf(' reload_postgrest_schema'),
|
|
351
369
|
);
|
|
352
|
-
expect(
|
|
353
|
-
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
test('does not reject SUPABASE_PROJECT_ID for non-Supabase status checks', () => {
|
|
357
|
-
const run = runGeneratedStatusScript({
|
|
358
|
-
manifest: {
|
|
359
|
-
deployment: {
|
|
360
|
-
target: 'minikube',
|
|
361
|
-
monitoring: false,
|
|
362
|
-
},
|
|
363
|
-
plugins: [],
|
|
364
|
-
},
|
|
365
|
-
supabaseProjectId: null,
|
|
366
|
-
env: {
|
|
367
|
-
SUPABASE_PROJECT_ID: 'ambient-project',
|
|
368
|
-
},
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
expect(run.status).toBe(0);
|
|
372
|
-
expect(run.combinedOutput).not.toContain(
|
|
373
|
-
'SUPABASE_PROJECT_ID must not be set for generated local Infra scripts.',
|
|
370
|
+
expect(upScript.indexOf('rollout status deployment/studio')).toBeLessThan(
|
|
371
|
+
upScript.indexOf('"${PORT_FORWARD_SCRIPT}" start studio'),
|
|
374
372
|
);
|
|
375
|
-
expect(
|
|
373
|
+
expect(runMigrationsCallIndex).toBeLessThan(reconcileProfileCallIndex);
|
|
374
|
+
expect(reconcileProfileCallIndex).toBeLessThan(appRuntimeApplyIndex);
|
|
376
375
|
});
|
|
377
376
|
|
|
378
|
-
test('
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
deployment: {
|
|
382
|
-
target: 'minikube',
|
|
383
|
-
monitoring: false,
|
|
384
|
-
},
|
|
385
|
-
plugins: [],
|
|
386
|
-
},
|
|
387
|
-
supabaseProjectId: null,
|
|
388
|
-
env: {
|
|
389
|
-
SUPABASE_LOCAL_ENABLED: 'true',
|
|
390
|
-
},
|
|
377
|
+
test('treats port-forwards as slug-owned lifecycle resources', () => {
|
|
378
|
+
const result = generateInfrastructure(createSupabaseManifest(), {
|
|
379
|
+
appManifest: createAppManifest('uwm'),
|
|
391
380
|
});
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
expect(
|
|
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');
|
|
398
425
|
});
|
|
399
426
|
});
|
|
400
427
|
|
|
401
|
-
function
|
|
402
|
-
namespace: string,
|
|
403
|
-
filePath: string,
|
|
404
|
-
overrides: Partial<InfraManifestInput> = {},
|
|
405
|
-
supabaseProjectId = namespace,
|
|
406
|
-
): string {
|
|
407
|
-
const artifact = generateMinikubeBaseArtifacts({
|
|
408
|
-
manifest: createInfraManifest(overrides),
|
|
409
|
-
namespace,
|
|
410
|
-
supabaseProjectId,
|
|
411
|
-
extraResources: [],
|
|
412
|
-
extraEnvEntries: [],
|
|
413
|
-
}).find((file) => file.path === filePath);
|
|
414
|
-
|
|
415
|
-
if (!artifact) {
|
|
416
|
-
throw new Error(`Expected generated artifact at ${filePath}`);
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
return artifact.content;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
function countOccurrences(source: string, needle: string): number {
|
|
423
|
-
return source.split(needle).length - 1;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
function createInfraManifest(overrides: Partial<InfraManifestInput> = {}): InfraManifestInput {
|
|
428
|
+
function createSupabaseManifest(): InfraManifestInput {
|
|
427
429
|
return {
|
|
428
430
|
deployment: {
|
|
429
431
|
target: 'minikube',
|
|
@@ -432,318 +434,21 @@ function createInfraManifest(overrides: Partial<InfraManifestInput> = {}): Infra
|
|
|
432
434
|
auth: {
|
|
433
435
|
scope: 'global',
|
|
434
436
|
provider: 'supabase',
|
|
435
|
-
authorization: {
|
|
436
|
-
kind: 'RBAC',
|
|
437
|
-
engine: 'native',
|
|
438
|
-
},
|
|
439
437
|
},
|
|
440
438
|
database: {
|
|
441
439
|
provider: 'supabase',
|
|
442
440
|
tier: 'dev',
|
|
443
441
|
},
|
|
444
|
-
|
|
445
|
-
cdn: false,
|
|
446
|
-
},
|
|
447
|
-
plugins: [],
|
|
448
|
-
...overrides,
|
|
449
|
-
auth: {
|
|
450
|
-
scope: 'global',
|
|
442
|
+
storage: {
|
|
451
443
|
provider: 'supabase',
|
|
452
|
-
|
|
453
|
-
kind: 'RBAC',
|
|
454
|
-
engine: 'native',
|
|
455
|
-
},
|
|
456
|
-
...overrides.auth,
|
|
444
|
+
buckets: ['assets'],
|
|
457
445
|
},
|
|
446
|
+
plugins: [],
|
|
458
447
|
};
|
|
459
448
|
}
|
|
460
449
|
|
|
461
|
-
function
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
env?: Record<string, string>;
|
|
466
|
-
withoutPython?: boolean;
|
|
467
|
-
}): {
|
|
468
|
-
status: number | null;
|
|
469
|
-
combinedOutput: string;
|
|
470
|
-
root: string;
|
|
471
|
-
configPath: string;
|
|
472
|
-
supabaseLogPath: string;
|
|
473
|
-
} {
|
|
474
|
-
const tempRoot = mkdtempSync(join(tmpdir(), 'infra-supabase-identity-'));
|
|
475
|
-
const root = join(tempRoot, 'infra', 'minikube');
|
|
476
|
-
const scriptsDir = join(root, 'scripts');
|
|
477
|
-
const supabaseDir = join(root, 'supabase');
|
|
478
|
-
const binDir = join(tempRoot, 'bin');
|
|
479
|
-
const appSourceDir = join(tempRoot, 'app');
|
|
480
|
-
const scriptPath = join(scriptsDir, 'supabase-local-env.sh');
|
|
481
|
-
const configPath = join(supabaseDir, 'config.toml');
|
|
482
|
-
const supabaseLogPath = join(tempRoot, 'supabase.log');
|
|
483
|
-
|
|
484
|
-
mkdirSync(scriptsDir, { recursive: true });
|
|
485
|
-
mkdirSync(binDir, { recursive: true });
|
|
486
|
-
mkdirSync(appSourceDir, { recursive: true });
|
|
487
|
-
writeFileSync(join(root, '.env.example'), '');
|
|
488
|
-
writeFileSync(supabaseLogPath, '');
|
|
489
|
-
|
|
490
|
-
if (args.existingConfig !== null) {
|
|
491
|
-
mkdirSync(supabaseDir, { recursive: true });
|
|
492
|
-
writeFileSync(configPath, args.existingConfig);
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
const script = getGeneratedFileContent(
|
|
496
|
-
args.projectId,
|
|
497
|
-
'infra/minikube/scripts/supabase-local-env.sh',
|
|
498
|
-
{},
|
|
499
|
-
args.supabaseProjectId === undefined ? args.projectId : args.supabaseProjectId,
|
|
500
|
-
);
|
|
501
|
-
writeFileSync(scriptPath, script);
|
|
502
|
-
chmodSync(scriptPath, 0o755);
|
|
503
|
-
|
|
504
|
-
const supabaseStubPath = join(binDir, 'supabase');
|
|
505
|
-
writeFileSync(
|
|
506
|
-
supabaseStubPath,
|
|
507
|
-
`#!/bin/bash
|
|
508
|
-
set -euo pipefail
|
|
509
|
-
printf '%s\\n' "$*" >> "\${SUPABASE_STUB_LOG}"
|
|
510
|
-
|
|
511
|
-
if [[ "\${1:-}" == "--version" ]]; then
|
|
512
|
-
echo "supabase 2.106.0"
|
|
513
|
-
exit 0
|
|
514
|
-
fi
|
|
515
|
-
|
|
516
|
-
if [[ "\${1:-}" == "status" && "\${2:-}" == "--help" ]]; then
|
|
517
|
-
echo "--workdir"
|
|
518
|
-
exit 0
|
|
519
|
-
fi
|
|
520
|
-
|
|
521
|
-
if [[ "\${1:-}" == "migration" && "\${2:-}" == "up" && "\${3:-}" == "--help" ]]; then
|
|
522
|
-
echo "--local"
|
|
523
|
-
exit 0
|
|
524
|
-
fi
|
|
525
|
-
|
|
526
|
-
if [[ "\${1:-}" == "--workdir" && "\${3:-}" == "init" && "\${4:-}" == "--yes" ]]; then
|
|
527
|
-
mkdir -p "\${2}/supabase"
|
|
528
|
-
printf '%s\\n' 'project_id = "minikube"' '[api]' 'port = 54321' > "\${2}/supabase/config.toml"
|
|
529
|
-
exit 0
|
|
530
|
-
fi
|
|
531
|
-
|
|
532
|
-
if [[ "\${1:-}" == "--workdir" && "\${3:-}" == "status" && "\${4:-}" == "-o" && "\${5:-}" == "env" ]]; then
|
|
533
|
-
cat <<'ENV'
|
|
534
|
-
DB_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres
|
|
535
|
-
API_URL=http://127.0.0.1:54321
|
|
536
|
-
ANON_KEY=anon
|
|
537
|
-
SERVICE_ROLE_KEY=service
|
|
538
|
-
JWT_SECRET=secret
|
|
539
|
-
ENV
|
|
540
|
-
exit 0
|
|
541
|
-
fi
|
|
542
|
-
|
|
543
|
-
if [[ "\${1:-}" == "--workdir" && "\${3:-}" == "status" ]]; then
|
|
544
|
-
exit 0
|
|
545
|
-
fi
|
|
546
|
-
|
|
547
|
-
if [[ "\${1:-}" == "--workdir" && "\${3:-}" == "migration" && "\${4:-}" == "up" && "\${5:-}" == "--local" ]]; then
|
|
548
|
-
exit 0
|
|
549
|
-
fi
|
|
550
|
-
|
|
551
|
-
echo "unexpected supabase call: $*" >&2
|
|
552
|
-
exit 1
|
|
553
|
-
`,
|
|
554
|
-
);
|
|
555
|
-
chmodSync(supabaseStubPath, 0o755);
|
|
556
|
-
|
|
557
|
-
const psqlStubPath = join(binDir, 'psql');
|
|
558
|
-
writeFileSync(
|
|
559
|
-
psqlStubPath,
|
|
560
|
-
`#!/bin/bash
|
|
561
|
-
exit 0
|
|
562
|
-
`,
|
|
563
|
-
);
|
|
564
|
-
chmodSync(psqlStubPath, 0o755);
|
|
565
|
-
|
|
566
|
-
const result = spawnSync('/bin/bash', [scriptPath], {
|
|
567
|
-
cwd: tempRoot,
|
|
568
|
-
env: {
|
|
569
|
-
...(args.withoutPython ? {} : process.env),
|
|
570
|
-
PATH: args.withoutPython ? binDir : `${binDir}:${process.env.PATH ?? ''}`,
|
|
571
|
-
APP_SOURCE_DIR: appSourceDir,
|
|
572
|
-
SUPABASE_STUB_LOG: supabaseLogPath,
|
|
573
|
-
...args.env,
|
|
574
|
-
},
|
|
575
|
-
encoding: 'utf-8',
|
|
576
|
-
});
|
|
577
|
-
|
|
578
|
-
return {
|
|
579
|
-
status: result.status,
|
|
580
|
-
combinedOutput: `${result.stdout}${result.stderr}`,
|
|
581
|
-
root,
|
|
582
|
-
configPath,
|
|
583
|
-
supabaseLogPath,
|
|
584
|
-
};
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
function runGeneratedStatusScript(args: {
|
|
588
|
-
manifest: InfraManifestInput;
|
|
589
|
-
supabaseProjectId: string | null;
|
|
590
|
-
env?: Record<string, string>;
|
|
591
|
-
}): {
|
|
592
|
-
status: number | null;
|
|
593
|
-
combinedOutput: string;
|
|
594
|
-
kubectlLog: string;
|
|
595
|
-
supabaseLog: string;
|
|
596
|
-
} {
|
|
597
|
-
const tempRoot = mkdtempSync(join(tmpdir(), 'infra-status-'));
|
|
598
|
-
const root = join(tempRoot, 'infra', 'minikube');
|
|
599
|
-
const scriptsDir = join(root, 'scripts');
|
|
600
|
-
const supabaseDir = join(root, 'supabase');
|
|
601
|
-
const binDir = join(tempRoot, 'bin');
|
|
602
|
-
const scriptPath = join(scriptsDir, 'status.sh');
|
|
603
|
-
const kubectlLogPath = join(tempRoot, 'kubectl.log');
|
|
604
|
-
const supabaseLogPath = join(tempRoot, 'supabase.log');
|
|
605
|
-
const psqlLogPath = join(tempRoot, 'psql.log');
|
|
606
|
-
|
|
607
|
-
mkdirSync(scriptsDir, { recursive: true });
|
|
608
|
-
mkdirSync(binDir, { recursive: true });
|
|
609
|
-
writeFileSync(join(root, '.env.example'), '');
|
|
610
|
-
writeFileSync(kubectlLogPath, '');
|
|
611
|
-
writeFileSync(supabaseLogPath, '');
|
|
612
|
-
writeFileSync(psqlLogPath, '');
|
|
613
|
-
|
|
614
|
-
if (args.supabaseProjectId !== null) {
|
|
615
|
-
mkdirSync(supabaseDir, { recursive: true });
|
|
616
|
-
writeFileSync(join(supabaseDir, 'config.toml'), `project_id = "${args.supabaseProjectId}"\n`);
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
const artifact = generateMinikubeBaseArtifacts({
|
|
620
|
-
manifest: args.manifest,
|
|
621
|
-
namespace: 'plain',
|
|
622
|
-
supabaseProjectId: args.supabaseProjectId,
|
|
623
|
-
extraResources: [],
|
|
624
|
-
extraEnvEntries: [],
|
|
625
|
-
}).find((file) => file.path === 'infra/minikube/scripts/status.sh');
|
|
626
|
-
|
|
627
|
-
if (!artifact) {
|
|
628
|
-
throw new Error('Expected generated status.sh artifact');
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
writeFileSync(scriptPath, artifact.content);
|
|
632
|
-
chmodSync(scriptPath, 0o755);
|
|
633
|
-
|
|
634
|
-
const kubectlStubPath = join(binDir, 'kubectl');
|
|
635
|
-
writeFileSync(
|
|
636
|
-
kubectlStubPath,
|
|
637
|
-
`#!/bin/bash
|
|
638
|
-
set -euo pipefail
|
|
639
|
-
printf '%s\\n' "$*" >> "\${KUBECTL_STUB_LOG}"
|
|
640
|
-
|
|
641
|
-
if [[ "\${1:-}" == "config" && "\${2:-}" == "current-context" ]]; then
|
|
642
|
-
echo "minikube"
|
|
643
|
-
exit 0
|
|
644
|
-
fi
|
|
645
|
-
|
|
646
|
-
if [[ "\${1:-}" == "cluster-info" ]]; then
|
|
647
|
-
echo "Kubernetes control plane is running"
|
|
648
|
-
exit 0
|
|
649
|
-
fi
|
|
650
|
-
|
|
651
|
-
if [[ "\${1:-}" == "get" && "\${2:-}" == "namespace" ]]; then
|
|
652
|
-
exit 0
|
|
653
|
-
fi
|
|
654
|
-
|
|
655
|
-
if [[ "\${1:-}" == "get" && "\${2:-}" == "all" ]]; then
|
|
656
|
-
echo "No resources found"
|
|
657
|
-
exit 0
|
|
658
|
-
fi
|
|
659
|
-
|
|
660
|
-
echo "unexpected kubectl call: $*" >&2
|
|
661
|
-
exit 1
|
|
662
|
-
`,
|
|
663
|
-
);
|
|
664
|
-
chmodSync(kubectlStubPath, 0o755);
|
|
665
|
-
|
|
666
|
-
const supabaseStubPath = join(binDir, 'supabase');
|
|
667
|
-
writeFileSync(
|
|
668
|
-
supabaseStubPath,
|
|
669
|
-
`#!/bin/bash
|
|
670
|
-
set -euo pipefail
|
|
671
|
-
printf '%s\\n' "$*" >> "\${SUPABASE_STUB_LOG}"
|
|
672
|
-
|
|
673
|
-
if [[ "\${1:-}" == "--version" ]]; then
|
|
674
|
-
echo "supabase 2.106.0"
|
|
675
|
-
exit 0
|
|
676
|
-
fi
|
|
677
|
-
|
|
678
|
-
if [[ "\${1:-}" == "status" && "\${2:-}" == "--help" ]]; then
|
|
679
|
-
echo "--workdir"
|
|
680
|
-
exit 0
|
|
681
|
-
fi
|
|
682
|
-
|
|
683
|
-
if [[ "\${1:-}" == "migration" && "\${2:-}" == "up" && "\${3:-}" == "--help" ]]; then
|
|
684
|
-
echo "--local"
|
|
685
|
-
exit 0
|
|
686
|
-
fi
|
|
687
|
-
|
|
688
|
-
if [[ "\${1:-}" == "--workdir" && "\${3:-}" == "status" && "\${4:-}" == "-o" && "\${5:-}" == "env" ]]; then
|
|
689
|
-
cat <<'ENV'
|
|
690
|
-
DB_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres
|
|
691
|
-
API_URL=http://127.0.0.1:54321
|
|
692
|
-
ANON_KEY=anon
|
|
693
|
-
SERVICE_ROLE_KEY=service
|
|
694
|
-
JWT_SECRET=secret
|
|
695
|
-
ENV
|
|
696
|
-
exit 0
|
|
697
|
-
fi
|
|
698
|
-
|
|
699
|
-
if [[ "\${1:-}" == "--workdir" && "\${3:-}" == "status" ]]; then
|
|
700
|
-
exit 0
|
|
701
|
-
fi
|
|
702
|
-
|
|
703
|
-
echo "unexpected supabase call: $*" >&2
|
|
704
|
-
exit 1
|
|
705
|
-
`,
|
|
706
|
-
);
|
|
707
|
-
chmodSync(supabaseStubPath, 0o755);
|
|
708
|
-
|
|
709
|
-
const psqlStubPath = join(binDir, 'psql');
|
|
710
|
-
writeFileSync(
|
|
711
|
-
psqlStubPath,
|
|
712
|
-
`#!/bin/bash
|
|
713
|
-
set -euo pipefail
|
|
714
|
-
printf '%s\\n' "$*" >> "\${PSQL_STUB_LOG}"
|
|
715
|
-
|
|
716
|
-
if [[ "$*" == *"select version from supabase_migrations.schema_migrations"* ]]; then
|
|
717
|
-
exit 0
|
|
718
|
-
fi
|
|
719
|
-
|
|
720
|
-
if [[ "$*" == *"-f"* ]]; then
|
|
721
|
-
exit 0
|
|
722
|
-
fi
|
|
723
|
-
|
|
724
|
-
echo "unexpected psql call: $*" >&2
|
|
725
|
-
exit 1
|
|
726
|
-
`,
|
|
727
|
-
);
|
|
728
|
-
chmodSync(psqlStubPath, 0o755);
|
|
729
|
-
|
|
730
|
-
const result = spawnSync('/bin/bash', [scriptPath], {
|
|
731
|
-
cwd: tempRoot,
|
|
732
|
-
env: {
|
|
733
|
-
...process.env,
|
|
734
|
-
PATH: `${binDir}:${process.env.PATH ?? ''}`,
|
|
735
|
-
KUBECTL_STUB_LOG: kubectlLogPath,
|
|
736
|
-
SUPABASE_STUB_LOG: supabaseLogPath,
|
|
737
|
-
PSQL_STUB_LOG: psqlLogPath,
|
|
738
|
-
...args.env,
|
|
739
|
-
},
|
|
740
|
-
encoding: 'utf-8',
|
|
741
|
-
});
|
|
742
|
-
|
|
743
|
-
return {
|
|
744
|
-
status: result.status,
|
|
745
|
-
combinedOutput: `${result.stdout}${result.stderr}`,
|
|
746
|
-
kubectlLog: readFileSync(kubectlLogPath, 'utf-8'),
|
|
747
|
-
supabaseLog: readFileSync(supabaseLogPath, 'utf-8'),
|
|
748
|
-
};
|
|
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;
|
|
749
454
|
}
|