@ankhorage/infra 0.4.2 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +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 +14 -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 +2764 -1841
- 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 +46 -38
- package/src/adapters/minikube/auth/supabase/index.ts +14 -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 +399 -673
- package/src/adapters/minikube/base/index.ts +2969 -1874
- 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 +64 -577
- package/src/minikubeImageLifecycle.test.ts +4 -3
|
@@ -1,429 +1,452 @@
|
|
|
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
|
-
|
|
24
|
-
expect(chessEnv).toContain('SUPABASE_LOCAL_PORT_BASE=62200');
|
|
25
|
-
expect(chessEnv).toContain('SUPABASE_LOCAL_SHADOW_PORT=62200');
|
|
26
|
-
expect(chessEnv).toContain('SUPABASE_LOCAL_API_PORT=62201');
|
|
27
|
-
expect(chessEnv).toContain('SUPABASE_LOCAL_DB_PORT=62202');
|
|
28
|
-
expect(chessEnv).toContain('SUPABASE_LOCAL_STUDIO_PORT=62203');
|
|
29
|
-
expect(chessEnv).toContain('SUPABASE_LOCAL_INBUCKET_PORT=62204');
|
|
30
|
-
expect(chessEnv).toContain('SUPABASE_LOCAL_ANALYTICS_PORT=62205');
|
|
31
|
-
expect(chessEnv).not.toContain('SUPABASE_LOCAL_DB_PORT=55322');
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test('generates bootstrap logic that derives ports from the app port base and preflights collisions', () => {
|
|
35
|
-
const script = getGeneratedFileContent('chess', 'infra/minikube/scripts/supabase-local-env.sh');
|
|
36
6
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
);
|
|
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}"');
|
|
49
25
|
});
|
|
50
26
|
|
|
51
|
-
test('
|
|
52
|
-
const
|
|
53
|
-
'
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
);
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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');
|
|
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');
|
|
75
45
|
});
|
|
76
46
|
|
|
77
|
-
test('
|
|
78
|
-
const
|
|
79
|
-
'scanner',
|
|
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);
|
|
52
|
+
|
|
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('syncs browser-safe Supabase env into the generated app env.local before build', () => {
|
|
337
|
+
const result = generateInfrastructure(createSupabaseManifest(), {
|
|
338
|
+
appManifest: createAppManifest('books'),
|
|
339
|
+
});
|
|
340
|
+
const upScript = getFile(result.files, 'infra/minikube/scripts/up.sh');
|
|
259
341
|
|
|
260
|
-
expect(
|
|
261
|
-
expect(
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
expect(script).toContain('Generated profile reconciliation');
|
|
265
|
-
expect(script).toContain('SQL file: ${sql_file}');
|
|
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('sync_app_public_supabase_env()');
|
|
343
|
+
expect(upScript).toContain('local app_env_file="${APP_SOURCE_DIR}/.env.local"');
|
|
344
|
+
expect(upScript).toContain(
|
|
345
|
+
'write_key_value_file_value "${app_env_file}" "EXPO_PUBLIC_SUPABASE_URL" "${EXPO_PUBLIC_SUPABASE_URL}"',
|
|
280
346
|
);
|
|
281
|
-
expect(
|
|
282
|
-
|
|
347
|
+
expect(upScript).toContain(
|
|
348
|
+
'write_key_value_file_value "${app_env_file}" "EXPO_PUBLIC_SUPABASE_ANON_KEY" "${EXPO_PUBLIC_SUPABASE_ANON_KEY}"',
|
|
283
349
|
);
|
|
284
|
-
expect(
|
|
285
|
-
|
|
350
|
+
expect(upScript).toContain('ensure_supabase_runtime_env\nsync_app_public_supabase_env');
|
|
351
|
+
expect(upScript.lastIndexOf('sync_app_public_supabase_env')).toBeLessThan(
|
|
352
|
+
upScript.indexOf('if [[ "${APP_BUILD_ENABLED}" == "true" ]]'),
|
|
286
353
|
);
|
|
287
|
-
expect(
|
|
288
|
-
"has_any_column_privilege('anon', format('public.%I', profile_table), 'REFERENCES')",
|
|
289
|
-
);
|
|
290
|
-
expect(status).toContain(
|
|
291
|
-
"has_any_column_privilege('authenticated', format('public.%I', profile_table), 'INSERT')",
|
|
292
|
-
);
|
|
293
|
-
expect(status).toContain(
|
|
294
|
-
"has_any_column_privilege('authenticated', format('public.%I', profile_table), 'REFERENCES')",
|
|
295
|
-
);
|
|
296
|
-
expect(status).toContain(
|
|
297
|
-
'authenticated role must not have profile table INSERT, DELETE, or REFERENCES privilege',
|
|
298
|
-
);
|
|
299
|
-
expect(status).toContain(
|
|
300
|
-
'authenticated role has unexpected UPDATE privilege on profile column',
|
|
301
|
-
);
|
|
302
|
-
expect(status).toContain('generated trigger function execute privilege must be revoked');
|
|
354
|
+
expect(upScript).not.toContain('SUPABASE_SERVICE_ROLE_KEY" "${SUPABASE_SERVICE_ROLE_KEY}"');
|
|
303
355
|
});
|
|
304
356
|
|
|
305
|
-
test('
|
|
306
|
-
const
|
|
307
|
-
|
|
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
|
-
},
|
|
357
|
+
test('starts Supabase schema owners before app migrations and profile reconciliation', () => {
|
|
358
|
+
const result = generateInfrastructure(createSupabaseManifest(), {
|
|
359
|
+
appManifest: createAppManifest('scanner'),
|
|
345
360
|
});
|
|
361
|
+
const upScript = getFile(result.files, 'infra/minikube/scripts/up.sh');
|
|
346
362
|
|
|
347
|
-
expect(
|
|
348
|
-
expect(
|
|
349
|
-
expect(
|
|
350
|
-
|
|
363
|
+
expect(upScript).toContain('apply_supabase_runtime_workloads');
|
|
364
|
+
expect(upScript).toContain('exec deployment/postgres -- psql -U postgres');
|
|
365
|
+
expect(upScript).toContain('create schema if not exists vault');
|
|
366
|
+
expect(upScript).toContain('grant execute on function pg_read_file(text) to public');
|
|
367
|
+
expect(upScript.indexOf('postgres.init.configmap.yaml')).toBeLessThan(
|
|
368
|
+
upScript.indexOf('postgres.pvc.yaml'),
|
|
369
|
+
);
|
|
370
|
+
expect(upScript.lastIndexOf(' wait_for_supabase_database')).toBeLessThan(
|
|
371
|
+
upScript.lastIndexOf(' bootstrap_supabase_database'),
|
|
372
|
+
);
|
|
373
|
+
expect(upScript).toContain('rollout status deployment/postgres --timeout=900s');
|
|
374
|
+
expect(upScript).toContain('rollout status deployment/gateway --timeout=600s');
|
|
375
|
+
const runMigrationsCallIndex = upScript.lastIndexOf(' run_supabase_migrations');
|
|
376
|
+
const reconcileProfileCallIndex = upScript.lastIndexOf(' reconcile_supabase_profile');
|
|
377
|
+
const appRuntimeApplyIndex = upScript.lastIndexOf(
|
|
378
|
+
'kubectl --context "${PROFILE}" apply -k "${K8S_DIR}"',
|
|
351
379
|
);
|
|
352
|
-
expect(run.combinedOutput).toContain('- profile schema: skipped (no profile table configured)');
|
|
353
|
-
expect(run.supabaseLog).toContain('--workdir');
|
|
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
380
|
|
|
371
|
-
expect(
|
|
372
|
-
|
|
373
|
-
|
|
381
|
+
expect(upScript.lastIndexOf(' apply_supabase_runtime_workloads')).toBeLessThan(
|
|
382
|
+
upScript.indexOf('rollout status deployment/auth'),
|
|
383
|
+
);
|
|
384
|
+
expect(upScript.indexOf('rollout status deployment/auth')).toBeLessThan(runMigrationsCallIndex);
|
|
385
|
+
expect(upScript.indexOf('rollout status deployment/storage')).toBeLessThan(
|
|
386
|
+
runMigrationsCallIndex,
|
|
387
|
+
);
|
|
388
|
+
expect(upScript.indexOf(' reconcile_supabase_profile')).toBeLessThan(
|
|
389
|
+
upScript.indexOf(' reload_postgrest_schema'),
|
|
374
390
|
);
|
|
375
|
-
expect(
|
|
391
|
+
expect(upScript.indexOf('rollout status deployment/studio')).toBeLessThan(
|
|
392
|
+
upScript.indexOf('"${PORT_FORWARD_SCRIPT}" start studio'),
|
|
393
|
+
);
|
|
394
|
+
expect(runMigrationsCallIndex).toBeLessThan(reconcileProfileCallIndex);
|
|
395
|
+
expect(reconcileProfileCallIndex).toBeLessThan(appRuntimeApplyIndex);
|
|
376
396
|
});
|
|
377
397
|
|
|
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
|
-
},
|
|
398
|
+
test('treats port-forwards as slug-owned lifecycle resources', () => {
|
|
399
|
+
const result = generateInfrastructure(createSupabaseManifest(), {
|
|
400
|
+
appManifest: createAppManifest('uwm'),
|
|
391
401
|
});
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
expect(
|
|
402
|
+
const portForwardScript = getFile(result.files, 'infra/minikube/scripts/port-forward.sh');
|
|
403
|
+
const statusScript = getFile(result.files, 'infra/minikube/scripts/status.sh');
|
|
404
|
+
const upScript = getFile(result.files, 'infra/minikube/scripts/up.sh');
|
|
405
|
+
const downScript = getFile(result.files, 'infra/minikube/scripts/down.sh');
|
|
406
|
+
|
|
407
|
+
expect(portForwardScript).toContain('service/app-runtime');
|
|
408
|
+
expect(portForwardScript).toContain('service/gateway');
|
|
409
|
+
expect(portForwardScript).toContain('service/studio');
|
|
410
|
+
expect(portForwardScript).toContain('service/postgres');
|
|
411
|
+
expect(portForwardScript).toContain('${PROFILE}-${1}.pid');
|
|
412
|
+
expect(portForwardScript).toContain('crashed stale_pid');
|
|
413
|
+
expect(portForwardScript).toContain('is_port_accepting()');
|
|
414
|
+
expect(portForwardScript).toContain('recent kubectl port-forward log');
|
|
415
|
+
expect(portForwardScript).toContain('did not become ready on local port');
|
|
416
|
+
expect(portForwardScript).toContain('not ready pid=');
|
|
417
|
+
expect(portForwardScript).toContain('terminate_forward_process()');
|
|
418
|
+
expect(portForwardScript).toContain('ps -p "${pid}" -o command=');
|
|
419
|
+
expect(portForwardScript).toContain('ps -p "${pid}" -o comm=');
|
|
420
|
+
expect(portForwardScript).toContain('is_owned_forward_process()');
|
|
421
|
+
expect(portForwardScript).toContain(
|
|
422
|
+
'*"kubectl --context ${PROFILE} -n ${namespace} port-forward ${resource} ${local_port}:${remote_port}"*',
|
|
423
|
+
);
|
|
424
|
+
expect(portForwardScript).toContain('kill -KILL "${pid}"');
|
|
425
|
+
expect(portForwardScript).toContain('failed to stop owned port-forward pid');
|
|
426
|
+
expect(portForwardScript).toContain('local port ${local_port} is still accepting connections');
|
|
427
|
+
expect(portForwardScript).toContain('refusing to terminate non-owned live pid');
|
|
428
|
+
expect(portForwardScript).toContain(
|
|
429
|
+
'stale pid file pointed at non-owned live pid ${pid}; removing ownership record ${pid_file}',
|
|
430
|
+
);
|
|
431
|
+
expect(portForwardScript).toContain(
|
|
432
|
+
'port occupied by non-owned process on local port ${local_port}',
|
|
433
|
+
);
|
|
434
|
+
expect(portForwardScript).toContain('stale_pid_non_owned=${pid}');
|
|
435
|
+
expect(statusScript).toContain('port-forward.sh" status all');
|
|
436
|
+
expect(upScript).toContain('set -Eeuo pipefail');
|
|
437
|
+
expect(upScript).toContain('trap cleanup_failed_up ERR');
|
|
438
|
+
expect(upScript).toContain('start supabase-gateway');
|
|
439
|
+
expect(upScript).toContain('start db-migration');
|
|
440
|
+
expect(upScript).toContain('Failed to start db-migration port-forward for Supabase bootstrap.');
|
|
441
|
+
expect(upScript.indexOf('trap cleanup_failed_up ERR')).toBeLessThan(
|
|
442
|
+
upScript.indexOf('require_command minikube'),
|
|
443
|
+
);
|
|
444
|
+
expect(upScript).toContain('trap - ERR');
|
|
445
|
+
expect(downScript).toContain('port-forward.sh" stop all');
|
|
398
446
|
});
|
|
399
447
|
});
|
|
400
448
|
|
|
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 {
|
|
449
|
+
function createSupabaseManifest(): InfraManifestInput {
|
|
427
450
|
return {
|
|
428
451
|
deployment: {
|
|
429
452
|
target: 'minikube',
|
|
@@ -432,318 +455,21 @@ function createInfraManifest(overrides: Partial<InfraManifestInput> = {}): Infra
|
|
|
432
455
|
auth: {
|
|
433
456
|
scope: 'global',
|
|
434
457
|
provider: 'supabase',
|
|
435
|
-
authorization: {
|
|
436
|
-
kind: 'RBAC',
|
|
437
|
-
engine: 'native',
|
|
438
|
-
},
|
|
439
458
|
},
|
|
440
459
|
database: {
|
|
441
460
|
provider: 'supabase',
|
|
442
461
|
tier: 'dev',
|
|
443
462
|
},
|
|
444
|
-
|
|
445
|
-
cdn: false,
|
|
446
|
-
},
|
|
447
|
-
plugins: [],
|
|
448
|
-
...overrides,
|
|
449
|
-
auth: {
|
|
450
|
-
scope: 'global',
|
|
463
|
+
storage: {
|
|
451
464
|
provider: 'supabase',
|
|
452
|
-
|
|
453
|
-
kind: 'RBAC',
|
|
454
|
-
engine: 'native',
|
|
455
|
-
},
|
|
456
|
-
...overrides.auth,
|
|
465
|
+
buckets: ['assets'],
|
|
457
466
|
},
|
|
467
|
+
plugins: [],
|
|
458
468
|
};
|
|
459
469
|
}
|
|
460
470
|
|
|
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
|
-
};
|
|
471
|
+
function getFile(files: readonly { path: string; content: string }[], path: string): string {
|
|
472
|
+
const file = files.find((candidate) => candidate.path === path);
|
|
473
|
+
if (!file) throw new Error(`Missing generated file: ${path}`);
|
|
474
|
+
return file.content;
|
|
749
475
|
}
|