@ankhorage/infra 0.3.7 → 0.4.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 +12 -0
- package/README.md +2 -1
- package/dist/adapters/minikube/base/index.d.ts +1 -0
- package/dist/adapters/minikube/base/index.d.ts.map +1 -1
- package/dist/adapters/minikube/base/index.js +239 -22
- package/dist/adapters/minikube/base/index.js.map +1 -1
- package/dist/adapters/minikube/index.d.ts.map +1 -1
- package/dist/adapters/minikube/index.js +34 -0
- package/dist/adapters/minikube/index.js.map +1 -1
- package/dist/adapters/minikube/secrets/index.d.ts +7 -0
- package/dist/adapters/minikube/secrets/index.d.ts.map +1 -0
- package/dist/adapters/minikube/secrets/index.js +13 -0
- package/dist/adapters/minikube/secrets/index.js.map +1 -0
- package/dist/adapters/minikube/secrets/supabase-vault/index.d.ts +8 -0
- package/dist/adapters/minikube/secrets/supabase-vault/index.d.ts.map +1 -0
- package/dist/adapters/minikube/secrets/supabase-vault/index.js +74 -0
- package/dist/adapters/minikube/secrets/supabase-vault/index.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/secretStore.d.ts +19 -0
- package/dist/secretStore.d.ts.map +1 -0
- package/dist/secretStore.js +22 -0
- package/dist/secretStore.js.map +1 -0
- package/package.json +7 -3
- package/src/adapters/minikube/base/index.test.ts +456 -0
- package/src/adapters/minikube/base/index.ts +244 -20
- package/src/adapters/minikube/index.ts +44 -0
- package/src/adapters/minikube/secrets/index.test.ts +139 -0
- package/src/adapters/minikube/secrets/index.ts +19 -0
- package/src/adapters/minikube/secrets/supabase-vault/index.ts +91 -0
- package/src/index.test.ts +151 -12
- package/src/index.ts +5 -0
- package/src/package.test.ts +8 -1
- package/src/secretStore.test.ts +76 -0
- package/src/secretStore.ts +44 -0
|
@@ -30,28 +30,33 @@ const SUPABASE_LOCAL_PORT_REFERENCE_PROJECT = 'my-app';
|
|
|
30
30
|
export function generateMinikubeBaseArtifacts(args: {
|
|
31
31
|
manifest: InfraManifestInput;
|
|
32
32
|
namespace: string;
|
|
33
|
+
supabaseProjectId: string | null;
|
|
33
34
|
extraResources: string[];
|
|
34
35
|
extraEnvEntries: string[];
|
|
35
36
|
}): GeneratedInfrastructureFile[] {
|
|
36
|
-
const { manifest, namespace, extraResources, extraEnvEntries } = args;
|
|
37
|
+
const { manifest, namespace, supabaseProjectId, extraResources, extraEnvEntries } = args;
|
|
37
38
|
|
|
38
39
|
const root = 'infra/minikube';
|
|
39
40
|
const k8sRoot = `${root}/k8s`;
|
|
40
41
|
const scriptsRoot = `${root}/scripts`;
|
|
41
42
|
const appImageRoot = `${root}/app-image`;
|
|
42
43
|
const defaultAppImage = getDefaultAppImage(namespace);
|
|
43
|
-
const supabaseLocalPorts = resolveSupabaseLocalPorts(namespace);
|
|
44
|
+
const supabaseLocalPorts = resolveSupabaseLocalPorts(supabaseProjectId ?? namespace);
|
|
44
45
|
|
|
45
46
|
const authScope = manifest.auth?.scope ?? 'none';
|
|
46
47
|
const authProvider = manifest.auth?.provider ?? 'none';
|
|
47
48
|
const authzEngine = manifest.auth?.authorization?.engine ?? 'none';
|
|
48
49
|
const databaseProvider = manifest.database?.provider ?? 'none';
|
|
50
|
+
const secretStoreProvider = manifest.secretStore?.provider ?? 'none';
|
|
49
51
|
const storageMetadata = resolveAppInfraStorageMetadata(manifest);
|
|
50
52
|
const profileModel = resolveSupabaseProfileModel(manifest);
|
|
51
53
|
const monitoringEnabled = manifest.deployment?.monitoring === true;
|
|
52
54
|
const domain = manifest.networking?.domain ?? '';
|
|
53
55
|
const supabaseLocalEnabled =
|
|
54
|
-
authProvider === 'supabase' ||
|
|
56
|
+
authProvider === 'supabase' ||
|
|
57
|
+
databaseProvider === 'supabase' ||
|
|
58
|
+
storageMetadata !== null ||
|
|
59
|
+
secretStoreProvider === 'supabase-vault';
|
|
55
60
|
|
|
56
61
|
return [
|
|
57
62
|
{
|
|
@@ -64,6 +69,7 @@ export function generateMinikubeBaseArtifacts(args: {
|
|
|
64
69
|
authzEngine,
|
|
65
70
|
extraResources,
|
|
66
71
|
supabaseLocalPorts,
|
|
72
|
+
supabaseProjectId,
|
|
67
73
|
supabaseLocalEnabled,
|
|
68
74
|
profileModel,
|
|
69
75
|
}),
|
|
@@ -90,6 +96,7 @@ export function generateMinikubeBaseArtifacts(args: {
|
|
|
90
96
|
authScope,
|
|
91
97
|
authProvider,
|
|
92
98
|
databaseProvider,
|
|
99
|
+
secretStoreProvider,
|
|
93
100
|
storageMetadata,
|
|
94
101
|
monitoringEnabled,
|
|
95
102
|
domain,
|
|
@@ -135,6 +142,7 @@ export function generateMinikubeBaseArtifacts(args: {
|
|
|
135
142
|
path: `${scriptsRoot}/status.sh`,
|
|
136
143
|
content: getStatusScript({
|
|
137
144
|
defaultNamespace: namespace,
|
|
145
|
+
supabaseProjectId,
|
|
138
146
|
supabaseLocalEnabled,
|
|
139
147
|
profileModel,
|
|
140
148
|
}),
|
|
@@ -143,6 +151,7 @@ export function generateMinikubeBaseArtifacts(args: {
|
|
|
143
151
|
{
|
|
144
152
|
path: `${scriptsRoot}/supabase-local-env.sh`,
|
|
145
153
|
content: getSupabaseLocalEnvScript({
|
|
154
|
+
supabaseProjectId,
|
|
146
155
|
supabaseLocalPorts,
|
|
147
156
|
profileModel,
|
|
148
157
|
}),
|
|
@@ -163,6 +172,7 @@ function getReadmeMarkdown(args: {
|
|
|
163
172
|
authzEngine: string;
|
|
164
173
|
extraResources: string[];
|
|
165
174
|
supabaseLocalPorts: SupabaseLocalPorts;
|
|
175
|
+
supabaseProjectId: string | null;
|
|
166
176
|
supabaseLocalEnabled: boolean;
|
|
167
177
|
profileModel: ResolvedProfileModel;
|
|
168
178
|
}): string {
|
|
@@ -174,6 +184,7 @@ function getReadmeMarkdown(args: {
|
|
|
174
184
|
authzEngine,
|
|
175
185
|
extraResources,
|
|
176
186
|
supabaseLocalPorts,
|
|
187
|
+
supabaseProjectId,
|
|
177
188
|
supabaseLocalEnabled,
|
|
178
189
|
profileModel,
|
|
179
190
|
} = args;
|
|
@@ -240,6 +251,7 @@ ${resourceLines}
|
|
|
240
251
|
## Runtime Conventions
|
|
241
252
|
|
|
242
253
|
- Namespace: \`${namespace}\`
|
|
254
|
+
- Supabase local project identity: \`${supabaseProjectId ?? 'unused'}\`
|
|
243
255
|
- Monitoring requested: \`${monitoringEnabled ? 'true' : 'false'}\`
|
|
244
256
|
- Auth provider: \`${authProvider}\`
|
|
245
257
|
- Authorization engine: \`${authzEngine}\`
|
|
@@ -327,6 +339,7 @@ Build flow:
|
|
|
327
339
|
|
|
328
340
|
- \`./scripts/up.sh\` calls \`./scripts/build-app-image.sh\` when \`APP_BUILD_ENABLED=true\`.
|
|
329
341
|
- Supabase local project root: \`infra/minikube\`${supabaseLocalEnabled ? '' : ' (unused unless Supabase-backed auth/database/storage is enabled)'}.
|
|
342
|
+
- Supabase local project identity: \`${supabaseProjectId ?? 'unused'}\`; existing local configs with another \`project_id\` must be destroyed and recreated manually.
|
|
330
343
|
- Immutable migrations live in \`supabase/migrations/\` and are applied with \`supabase migration up --local\`.
|
|
331
344
|
${profileModel.enabled ? '- Generated profile desired-state reconciliation lives in `supabase/generated/auth_profiles.sql` and records checksum state in `ankhorage_internal.generated_schema_state`.\n' : ''}- \`./scripts/supabase-local-env.sh\` can populate Supabase keys in \`.env\` from local \`supabase status -o env\`.
|
|
332
345
|
- \`./scripts/supabase-local-env.sh\` writes \`EXPO_PUBLIC_SUPABASE_URL\` + \`EXPO_PUBLIC_SUPABASE_ANON_KEY\` into \`$APP_SOURCE_DIR/.env.local\` (e.g. \`apps/card/.env.local\`) for local app runs.
|
|
@@ -427,6 +440,7 @@ function getAppConfigMap(args: {
|
|
|
427
440
|
authScope: string;
|
|
428
441
|
authProvider: string;
|
|
429
442
|
databaseProvider: string;
|
|
443
|
+
secretStoreProvider: string;
|
|
430
444
|
storageMetadata: AppInfraStorageMetadata | null;
|
|
431
445
|
monitoringEnabled: boolean;
|
|
432
446
|
domain: string;
|
|
@@ -436,6 +450,7 @@ function getAppConfigMap(args: {
|
|
|
436
450
|
authScope,
|
|
437
451
|
authProvider,
|
|
438
452
|
databaseProvider,
|
|
453
|
+
secretStoreProvider,
|
|
439
454
|
storageMetadata,
|
|
440
455
|
monitoringEnabled,
|
|
441
456
|
domain,
|
|
@@ -459,6 +474,7 @@ data:
|
|
|
459
474
|
AUTH_SCOPE: "${authScope}"
|
|
460
475
|
AUTH_PROVIDER: "${authProvider}"
|
|
461
476
|
DATABASE_PROVIDER: "${databaseProvider}"
|
|
477
|
+
SECRET_STORE_PROVIDER: "${secretStoreProvider}"
|
|
462
478
|
${storageLines} NETWORK_DOMAIN: "${domain}"
|
|
463
479
|
`;
|
|
464
480
|
}
|
|
@@ -1406,10 +1422,11 @@ $$;`;
|
|
|
1406
1422
|
|
|
1407
1423
|
function getStatusScript(args: {
|
|
1408
1424
|
defaultNamespace: string;
|
|
1425
|
+
supabaseProjectId: string | null;
|
|
1409
1426
|
supabaseLocalEnabled: boolean;
|
|
1410
1427
|
profileModel: ResolvedProfileModel;
|
|
1411
1428
|
}): string {
|
|
1412
|
-
const { defaultNamespace, supabaseLocalEnabled, profileModel } = args;
|
|
1429
|
+
const { defaultNamespace, supabaseProjectId, supabaseLocalEnabled, profileModel } = args;
|
|
1413
1430
|
const profileStatusSql = profileModel.enabled ? getSupabaseProfileChecksumSql(profileModel) : '';
|
|
1414
1431
|
const profileVerificationSql = profileModel.enabled
|
|
1415
1432
|
? getSupabaseProfileVerificationSql(profileModel)
|
|
@@ -1433,8 +1450,28 @@ AUTH_RUNTIME_MODE="\${AUTH_RUNTIME_MODE:-local}"
|
|
|
1433
1450
|
SUPABASE_LOCAL_ENABLED="\${SUPABASE_LOCAL_ENABLED:-${supabaseLocalEnabled ? 'true' : 'false'}}"
|
|
1434
1451
|
SUPABASE_SECRET_SYNC_ENABLED="\${SUPABASE_SECRET_SYNC_ENABLED:-true}"
|
|
1435
1452
|
SUPABASE_PROJECT_DIR="\${SUPABASE_PROJECT_DIR:-\${ROOT_DIR}}"
|
|
1453
|
+
EXPECTED_SUPABASE_PROJECT_ID="${supabaseProjectId ?? ''}"
|
|
1436
1454
|
SUPABASE_PROFILE_ENABLED="${profileModel.enabled ? 'true' : 'false'}"
|
|
1437
1455
|
SUPABASE_PROFILE_RECONCILE_FILE="\${SUPABASE_PROJECT_DIR}/supabase/generated/auth_profiles.sql"
|
|
1456
|
+
export EXPECTED_SUPABASE_PROJECT_ID
|
|
1457
|
+
|
|
1458
|
+
reject_supabase_project_id_override() {
|
|
1459
|
+
if [[ -n "\${SUPABASE_PROJECT_ID:-}" ]]; then
|
|
1460
|
+
echo "SUPABASE_PROJECT_ID must not be set for generated local Infra scripts."
|
|
1461
|
+
echo "Unset SUPABASE_PROJECT_ID and use supabase/config.toml project_id instead."
|
|
1462
|
+
return 1
|
|
1463
|
+
fi
|
|
1464
|
+
|
|
1465
|
+
unset SUPABASE_PROJECT_ID
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
require_expected_supabase_project_identity() {
|
|
1469
|
+
if [[ -z "\${EXPECTED_SUPABASE_PROJECT_ID}" ]]; then
|
|
1470
|
+
echo "Cannot run local Supabase infrastructure: expected Supabase project identity is empty."
|
|
1471
|
+
echo "Regenerate Infra with appManifest.metadata.slug for Supabase-backed local services."
|
|
1472
|
+
return 1
|
|
1473
|
+
fi
|
|
1474
|
+
}
|
|
1438
1475
|
|
|
1439
1476
|
if ! command -v kubectl >/dev/null 2>&1; then
|
|
1440
1477
|
echo "kubectl is required but not installed."
|
|
@@ -1598,6 +1635,61 @@ run_supabase_sql() {
|
|
|
1598
1635
|
return 1
|
|
1599
1636
|
}
|
|
1600
1637
|
|
|
1638
|
+
validate_supabase_project_identity() {
|
|
1639
|
+
local config_file="\${SUPABASE_PROJECT_DIR}/supabase/config.toml"
|
|
1640
|
+
|
|
1641
|
+
if [[ -z "\${EXPECTED_SUPABASE_PROJECT_ID}" ]]; then
|
|
1642
|
+
return 0
|
|
1643
|
+
fi
|
|
1644
|
+
|
|
1645
|
+
if [[ ! -f "\${config_file}" ]]; then
|
|
1646
|
+
echo "- local Supabase project identity: config missing at \${config_file}"
|
|
1647
|
+
return 1
|
|
1648
|
+
fi
|
|
1649
|
+
|
|
1650
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
1651
|
+
echo "- local Supabase project identity: python3 is required to read \${config_file}"
|
|
1652
|
+
return 1
|
|
1653
|
+
fi
|
|
1654
|
+
|
|
1655
|
+
python3 - "\${config_file}" <<'PY'
|
|
1656
|
+
import os
|
|
1657
|
+
import re
|
|
1658
|
+
import sys
|
|
1659
|
+
|
|
1660
|
+
|
|
1661
|
+
def read_top_level_project_id(config_path):
|
|
1662
|
+
key_re = re.compile(r'^\\s*project_id\\s*=\\s*(.*?)\\s*(?:#.*)?$')
|
|
1663
|
+
section_re = re.compile(r'^\\s*\\[[^\\]]+\\]\\s*$')
|
|
1664
|
+
|
|
1665
|
+
with open(config_path, 'r', encoding='utf-8') as f:
|
|
1666
|
+
for line in f:
|
|
1667
|
+
if section_re.match(line):
|
|
1668
|
+
return None
|
|
1669
|
+
match = key_re.match(line)
|
|
1670
|
+
if not match:
|
|
1671
|
+
continue
|
|
1672
|
+
raw = match.group(1).strip()
|
|
1673
|
+
quoted = re.match(r'^["\\']([^"\\']*)["\\']$', raw)
|
|
1674
|
+
return quoted.group(1) if quoted else raw
|
|
1675
|
+
|
|
1676
|
+
return None
|
|
1677
|
+
|
|
1678
|
+
|
|
1679
|
+
expected = os.environ['EXPECTED_SUPABASE_PROJECT_ID']
|
|
1680
|
+
actual = read_top_level_project_id(sys.argv[1])
|
|
1681
|
+
|
|
1682
|
+
if actual == expected:
|
|
1683
|
+
raise SystemExit(0)
|
|
1684
|
+
|
|
1685
|
+
found = 'missing' if actual is None else f'"{actual}"'
|
|
1686
|
+
print('Supabase project identity mismatch.', file=sys.stderr)
|
|
1687
|
+
print(f'Expected "{expected}", found {found}.', file=sys.stderr)
|
|
1688
|
+
print('Destroy the invalid local stack and run Infra Up again.', file=sys.stderr)
|
|
1689
|
+
raise SystemExit(1)
|
|
1690
|
+
PY
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1601
1693
|
check_immutable_migrations_applied() {
|
|
1602
1694
|
local migrations_dir="\${SUPABASE_PROJECT_DIR}/supabase/migrations"
|
|
1603
1695
|
local applied_versions
|
|
@@ -1632,8 +1724,14 @@ if [[ "\${AUTH_RUNTIME_MODE}" == "local" && "\${SUPABASE_LOCAL_ENABLED}" == "tru
|
|
|
1632
1724
|
echo "Local Supabase database checks:"
|
|
1633
1725
|
status_failed=0
|
|
1634
1726
|
|
|
1635
|
-
if
|
|
1636
|
-
|
|
1727
|
+
if ! require_expected_supabase_project_identity; then
|
|
1728
|
+
status_failed=1
|
|
1729
|
+
elif ! reject_supabase_project_id_override; then
|
|
1730
|
+
status_failed=1
|
|
1731
|
+
elif require_supabase_cli_capabilities; then
|
|
1732
|
+
if ! validate_supabase_project_identity; then
|
|
1733
|
+
status_failed=1
|
|
1734
|
+
elif supabase --workdir "\${SUPABASE_PROJECT_DIR}" status >/dev/null 2>&1; then
|
|
1637
1735
|
STATUS_ENV="$(supabase --workdir "\${SUPABASE_PROJECT_DIR}" status -o env)"
|
|
1638
1736
|
SUPABASE_DB_URL="$(read_env_value "\${STATUS_ENV}" DB_URL)"
|
|
1639
1737
|
if [[ -z "\${SUPABASE_DB_URL}" ]]; then
|
|
@@ -1705,10 +1803,11 @@ fi
|
|
|
1705
1803
|
}
|
|
1706
1804
|
|
|
1707
1805
|
function getSupabaseLocalEnvScript(args: {
|
|
1806
|
+
supabaseProjectId: string | null;
|
|
1708
1807
|
supabaseLocalPorts: SupabaseLocalPorts;
|
|
1709
1808
|
profileModel: ResolvedProfileModel;
|
|
1710
1809
|
}): string {
|
|
1711
|
-
const { supabaseLocalPorts, profileModel } = args;
|
|
1810
|
+
const { supabaseProjectId, supabaseLocalPorts, profileModel } = args;
|
|
1712
1811
|
const profileStatusSql = profileModel.enabled ? getSupabaseProfileChecksumSql(profileModel) : '';
|
|
1713
1812
|
const profileVerificationSql = profileModel.enabled
|
|
1714
1813
|
? getSupabaseProfileVerificationSql(profileModel)
|
|
@@ -1731,9 +1830,29 @@ fi
|
|
|
1731
1830
|
|
|
1732
1831
|
APP_SOURCE_DIR="\${APP_SOURCE_DIR:-$(cd "\${ROOT_DIR}/../.." && pwd)}"
|
|
1733
1832
|
SUPABASE_PROJECT_DIR="\${SUPABASE_PROJECT_DIR:-\${ROOT_DIR}}"
|
|
1833
|
+
EXPECTED_SUPABASE_PROJECT_ID="${supabaseProjectId ?? ''}"
|
|
1734
1834
|
APP_SUPABASE_ENV_FILE="\${APP_SUPABASE_ENV_FILE:-\${APP_SOURCE_DIR}/.env.local}"
|
|
1735
1835
|
SUPABASE_PROFILE_ENABLED="${profileModel.enabled ? 'true' : 'false'}"
|
|
1736
1836
|
SUPABASE_PROFILE_RECONCILE_FILE="\${SUPABASE_PROJECT_DIR}/supabase/generated/auth_profiles.sql"
|
|
1837
|
+
export EXPECTED_SUPABASE_PROJECT_ID
|
|
1838
|
+
|
|
1839
|
+
reject_supabase_project_id_override() {
|
|
1840
|
+
if [[ -n "\${SUPABASE_PROJECT_ID:-}" ]]; then
|
|
1841
|
+
echo "SUPABASE_PROJECT_ID must not be set for generated local Infra scripts."
|
|
1842
|
+
echo "Unset SUPABASE_PROJECT_ID and use supabase/config.toml project_id instead."
|
|
1843
|
+
exit 1
|
|
1844
|
+
fi
|
|
1845
|
+
|
|
1846
|
+
unset SUPABASE_PROJECT_ID
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
require_expected_supabase_project_identity() {
|
|
1850
|
+
if [[ -z "\${EXPECTED_SUPABASE_PROJECT_ID}" ]]; then
|
|
1851
|
+
echo "Cannot run local Supabase infrastructure: expected Supabase project identity is empty."
|
|
1852
|
+
echo "Regenerate Infra with appManifest.metadata.slug for Supabase-backed local services."
|
|
1853
|
+
exit 1
|
|
1854
|
+
fi
|
|
1855
|
+
}
|
|
1737
1856
|
|
|
1738
1857
|
SUPABASE_LOCAL_PORT_BASE="\${SUPABASE_LOCAL_PORT_BASE:-${supabaseLocalPorts.base}}"
|
|
1739
1858
|
SUPABASE_LOCAL_SHADOW_PORT="\${SUPABASE_LOCAL_SHADOW_PORT:-\${SUPABASE_LOCAL_PORT_BASE}}"
|
|
@@ -1779,6 +1898,11 @@ require_supabase_cli_capabilities() {
|
|
|
1779
1898
|
exit 1
|
|
1780
1899
|
fi
|
|
1781
1900
|
|
|
1901
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
1902
|
+
echo "python3 is required to reconcile supabase/config.toml before local Supabase startup."
|
|
1903
|
+
exit 1
|
|
1904
|
+
fi
|
|
1905
|
+
|
|
1782
1906
|
local version
|
|
1783
1907
|
version="$(supabase --version | awk '{print $NF}')"
|
|
1784
1908
|
if ! version_ge "\${version}" "2.106.0"; then
|
|
@@ -1840,6 +1964,8 @@ run_checked_sql_file() {
|
|
|
1840
1964
|
psql "\${SUPABASE_DB_URL}" -v ON_ERROR_STOP=1 -q -f "\${sql_file}"
|
|
1841
1965
|
}
|
|
1842
1966
|
|
|
1967
|
+
require_expected_supabase_project_identity
|
|
1968
|
+
reject_supabase_project_id_override
|
|
1843
1969
|
require_supabase_cli_capabilities
|
|
1844
1970
|
|
|
1845
1971
|
if [[ ! -d "\${SUPABASE_PROJECT_DIR}" ]]; then
|
|
@@ -1850,18 +1976,6 @@ fi
|
|
|
1850
1976
|
configure_supabase_local_ports() {
|
|
1851
1977
|
local config_file="\${SUPABASE_PROJECT_DIR}/supabase/config.toml"
|
|
1852
1978
|
|
|
1853
|
-
if ! command -v python3 >/dev/null 2>&1; then
|
|
1854
|
-
echo "python3 is required to patch supabase/config.toml with generated local port defaults."
|
|
1855
|
-
echo "Install python3 (recommended) or manually edit \${config_file} ports:"
|
|
1856
|
-
echo "- [api] port = \${SUPABASE_LOCAL_API_PORT}"
|
|
1857
|
-
echo "- [db] port = \${SUPABASE_LOCAL_DB_PORT}"
|
|
1858
|
-
echo "- [db] shadow_port = \${SUPABASE_LOCAL_SHADOW_PORT}"
|
|
1859
|
-
echo "- [studio] port = \${SUPABASE_LOCAL_STUDIO_PORT}"
|
|
1860
|
-
echo "- [inbucket] port = \${SUPABASE_LOCAL_INBUCKET_PORT}"
|
|
1861
|
-
echo "- [analytics] port = \${SUPABASE_LOCAL_ANALYTICS_PORT}"
|
|
1862
|
-
exit 1
|
|
1863
|
-
fi
|
|
1864
|
-
|
|
1865
1979
|
python3 - "\${config_file}" <<'PY'
|
|
1866
1980
|
import os
|
|
1867
1981
|
import re
|
|
@@ -1965,8 +2079,114 @@ if __name__ == "__main__":
|
|
|
1965
2079
|
PY
|
|
1966
2080
|
}
|
|
1967
2081
|
|
|
2082
|
+
write_supabase_project_identity_for_new_config() {
|
|
2083
|
+
local config_file="\${SUPABASE_PROJECT_DIR}/supabase/config.toml"
|
|
2084
|
+
|
|
2085
|
+
require_expected_supabase_project_identity
|
|
2086
|
+
|
|
2087
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
2088
|
+
echo "python3 is required to write Supabase project_id in \${config_file}."
|
|
2089
|
+
exit 1
|
|
2090
|
+
fi
|
|
2091
|
+
|
|
2092
|
+
python3 - "\${config_file}" <<'PY'
|
|
2093
|
+
import os
|
|
2094
|
+
import re
|
|
2095
|
+
import sys
|
|
2096
|
+
|
|
2097
|
+
|
|
2098
|
+
def write_top_level_project_id(config_path, expected):
|
|
2099
|
+
key_re = re.compile(r'^(\\s*)project_id\\s*=\\s*(.*?)(\\s*(#.*)?)?$')
|
|
2100
|
+
section_re = re.compile(r'^\\s*\\[[^\\]]+\\]\\s*$')
|
|
2101
|
+
|
|
2102
|
+
with open(config_path, 'r', encoding='utf-8') as f:
|
|
2103
|
+
lines = f.readlines()
|
|
2104
|
+
|
|
2105
|
+
section_start = len(lines)
|
|
2106
|
+
for i, line in enumerate(lines):
|
|
2107
|
+
if section_re.match(line):
|
|
2108
|
+
section_start = i
|
|
2109
|
+
break
|
|
2110
|
+
|
|
2111
|
+
for i in range(section_start):
|
|
2112
|
+
match = key_re.match(lines[i])
|
|
2113
|
+
if not match:
|
|
2114
|
+
continue
|
|
2115
|
+
indent = match.group(1) or ''
|
|
2116
|
+
trailing = (match.group(3) or '').rstrip('\\n')
|
|
2117
|
+
lines[i] = f'{indent}project_id = "{expected}"{trailing}\\n'
|
|
2118
|
+
break
|
|
2119
|
+
else:
|
|
2120
|
+
insert_at = section_start
|
|
2121
|
+
lines.insert(insert_at, f'project_id = "{expected}"\\n')
|
|
2122
|
+
|
|
2123
|
+
with open(config_path, 'w', encoding='utf-8') as f:
|
|
2124
|
+
f.writelines(lines)
|
|
2125
|
+
|
|
2126
|
+
|
|
2127
|
+
write_top_level_project_id(sys.argv[1], os.environ['EXPECTED_SUPABASE_PROJECT_ID'])
|
|
2128
|
+
PY
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
validate_supabase_project_identity() {
|
|
2132
|
+
local config_file="\${SUPABASE_PROJECT_DIR}/supabase/config.toml"
|
|
2133
|
+
|
|
2134
|
+
require_expected_supabase_project_identity
|
|
2135
|
+
|
|
2136
|
+
if [[ ! -f "\${config_file}" ]]; then
|
|
2137
|
+
echo "Supabase project identity mismatch."
|
|
2138
|
+
printf 'Expected "%s", found missing.\n' "\${EXPECTED_SUPABASE_PROJECT_ID}"
|
|
2139
|
+
echo "The existing local Supabase project belongs to a different identity."
|
|
2140
|
+
echo "Destroy the invalid local stack and its project-owned resources, then run Infra Up again."
|
|
2141
|
+
exit 1
|
|
2142
|
+
fi
|
|
2143
|
+
|
|
2144
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
2145
|
+
echo "python3 is required to read Supabase project_id in \${config_file}."
|
|
2146
|
+
exit 1
|
|
2147
|
+
fi
|
|
2148
|
+
|
|
2149
|
+
python3 - "\${config_file}" <<'PY'
|
|
2150
|
+
import os
|
|
2151
|
+
import re
|
|
2152
|
+
import sys
|
|
2153
|
+
|
|
2154
|
+
|
|
2155
|
+
def read_top_level_project_id(config_path):
|
|
2156
|
+
key_re = re.compile(r'^\\s*project_id\\s*=\\s*(.*?)\\s*(?:#.*)?$')
|
|
2157
|
+
section_re = re.compile(r'^\\s*\\[[^\\]]+\\]\\s*$')
|
|
2158
|
+
|
|
2159
|
+
with open(config_path, 'r', encoding='utf-8') as f:
|
|
2160
|
+
for line in f:
|
|
2161
|
+
if section_re.match(line):
|
|
2162
|
+
return None
|
|
2163
|
+
match = key_re.match(line)
|
|
2164
|
+
if not match:
|
|
2165
|
+
continue
|
|
2166
|
+
raw = match.group(1).strip()
|
|
2167
|
+
quoted = re.match(r'^["\\']([^"\\']*)["\\']$', raw)
|
|
2168
|
+
return quoted.group(1) if quoted else raw
|
|
2169
|
+
|
|
2170
|
+
return None
|
|
2171
|
+
|
|
2172
|
+
|
|
2173
|
+
expected = os.environ['EXPECTED_SUPABASE_PROJECT_ID']
|
|
2174
|
+
actual = read_top_level_project_id(sys.argv[1])
|
|
2175
|
+
|
|
2176
|
+
if actual == expected:
|
|
2177
|
+
raise SystemExit(0)
|
|
2178
|
+
|
|
2179
|
+
found = 'missing' if actual is None else f'"{actual}"'
|
|
2180
|
+
print('Supabase project identity mismatch.', file=sys.stderr)
|
|
2181
|
+
print(f'Expected "{expected}", found {found}.', file=sys.stderr)
|
|
2182
|
+
print('The existing local Supabase project belongs to a different identity.', file=sys.stderr)
|
|
2183
|
+
print('Destroy the invalid local stack and its project-owned resources, then run Infra Up again.', file=sys.stderr)
|
|
2184
|
+
raise SystemExit(1)
|
|
2185
|
+
PY
|
|
2186
|
+
}
|
|
2187
|
+
|
|
1968
2188
|
assert_supabase_local_ports_available() {
|
|
1969
|
-
local project_label="\${
|
|
2189
|
+
local project_label="\${EXPECTED_SUPABASE_PROJECT_ID}"
|
|
1970
2190
|
|
|
1971
2191
|
ANKH_PROJECT_LABEL="\${project_label}" python3 - <<'PY'
|
|
1972
2192
|
import os
|
|
@@ -2044,8 +2264,10 @@ PY
|
|
|
2044
2264
|
if [[ ! -f "\${SUPABASE_PROJECT_DIR}/supabase/config.toml" ]]; then
|
|
2045
2265
|
echo "No supabase/config.toml found in \${SUPABASE_PROJECT_DIR}. Initializing Supabase project..."
|
|
2046
2266
|
supabase --workdir "\${SUPABASE_PROJECT_DIR}" init --yes >/dev/null
|
|
2267
|
+
write_supabase_project_identity_for_new_config
|
|
2047
2268
|
fi
|
|
2048
2269
|
|
|
2270
|
+
validate_supabase_project_identity
|
|
2049
2271
|
configure_supabase_local_ports
|
|
2050
2272
|
|
|
2051
2273
|
STATUS_ENV="$(
|
|
@@ -2117,6 +2339,7 @@ start_supabase_local_stack() {
|
|
|
2117
2339
|
}
|
|
2118
2340
|
|
|
2119
2341
|
if [[ -z "\${STATUS_ENV}" ]]; then
|
|
2342
|
+
validate_supabase_project_identity
|
|
2120
2343
|
assert_supabase_local_ports_available
|
|
2121
2344
|
echo "Supabase local stack not detected. Starting with 'supabase start'..."
|
|
2122
2345
|
start_supabase_local_stack
|
|
@@ -2130,6 +2353,7 @@ if [[ -z "\${SUPABASE_DB_URL}" ]]; then
|
|
|
2130
2353
|
fi
|
|
2131
2354
|
|
|
2132
2355
|
echo "Applying pending immutable Supabase migrations..."
|
|
2356
|
+
validate_supabase_project_identity
|
|
2133
2357
|
run_checked_command \
|
|
2134
2358
|
"Immutable Supabase migration application" \
|
|
2135
2359
|
"" \
|
|
@@ -8,9 +8,11 @@ import type {
|
|
|
8
8
|
import { generateAuthProviderArtifacts } from './auth';
|
|
9
9
|
import { generateAuthorizationArtifacts } from './authz';
|
|
10
10
|
import { generateMinikubeBaseArtifacts } from './base';
|
|
11
|
+
import { generateSecretStoreArtifacts } from './secrets';
|
|
11
12
|
import { generateStorageArtifacts } from './storage';
|
|
12
13
|
|
|
13
14
|
const DEFAULT_NAMESPACE = 'ankh-app';
|
|
15
|
+
const CANONICAL_PROJECT_SLUG_RE = /^[a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?$/;
|
|
14
16
|
|
|
15
17
|
export function generateMinikubeInfra(
|
|
16
18
|
manifest: InfraManifestInput,
|
|
@@ -29,6 +31,9 @@ export function generateMinikubeInfra(
|
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
const namespace = resolveNamespace(manifest, options);
|
|
34
|
+
const supabaseProjectId = isSupabaseLocalEnabled(manifest)
|
|
35
|
+
? validateCanonicalProjectSlug(options.appManifest?.metadata.slug)
|
|
36
|
+
: null;
|
|
32
37
|
|
|
33
38
|
const authArtifacts = generateAuthProviderArtifacts({ manifest, namespace });
|
|
34
39
|
const authzArtifacts = generateAuthorizationArtifacts({
|
|
@@ -37,6 +42,7 @@ export function generateMinikubeInfra(
|
|
|
37
42
|
appManifest: options.appManifest,
|
|
38
43
|
});
|
|
39
44
|
const storageArtifacts = generateStorageArtifacts({ manifest, namespace });
|
|
45
|
+
const secretStoreArtifacts = generateSecretStoreArtifacts({ manifest, namespace });
|
|
40
46
|
const apiArtifacts = generateApiInfrastructureArtifacts({
|
|
41
47
|
data: options.appManifest?.data,
|
|
42
48
|
databaseProvider: manifest.database?.provider,
|
|
@@ -46,16 +52,19 @@ export function generateMinikubeInfra(
|
|
|
46
52
|
...authArtifacts.resources,
|
|
47
53
|
...authzArtifacts.resources,
|
|
48
54
|
...storageArtifacts.resources,
|
|
55
|
+
...secretStoreArtifacts.resources,
|
|
49
56
|
]);
|
|
50
57
|
const extraEnvEntries = unique([
|
|
51
58
|
...authArtifacts.envEntries,
|
|
52
59
|
...authzArtifacts.envEntries,
|
|
53
60
|
...storageArtifacts.envEntries,
|
|
61
|
+
...secretStoreArtifacts.envEntries,
|
|
54
62
|
]);
|
|
55
63
|
|
|
56
64
|
const baseFiles = generateMinikubeBaseArtifacts({
|
|
57
65
|
manifest,
|
|
58
66
|
namespace,
|
|
67
|
+
supabaseProjectId,
|
|
59
68
|
extraResources,
|
|
60
69
|
extraEnvEntries,
|
|
61
70
|
});
|
|
@@ -65,6 +74,7 @@ export function generateMinikubeInfra(
|
|
|
65
74
|
...authArtifacts.warnings,
|
|
66
75
|
...authzArtifacts.warnings,
|
|
67
76
|
...storageArtifacts.warnings,
|
|
77
|
+
...secretStoreArtifacts.warnings,
|
|
68
78
|
...apiArtifacts.warnings,
|
|
69
79
|
]);
|
|
70
80
|
|
|
@@ -74,6 +84,7 @@ export function generateMinikubeInfra(
|
|
|
74
84
|
...authArtifacts.files,
|
|
75
85
|
...authzArtifacts.files,
|
|
76
86
|
...storageArtifacts.files,
|
|
87
|
+
...secretStoreArtifacts.files,
|
|
77
88
|
...apiArtifacts.files,
|
|
78
89
|
],
|
|
79
90
|
warnings,
|
|
@@ -94,6 +105,7 @@ function collectProviders(manifest: InfraManifestInput): string[] {
|
|
|
94
105
|
}
|
|
95
106
|
if (manifest.database?.provider) providers.add(manifest.database.provider);
|
|
96
107
|
if (manifest.storage?.provider) providers.add(manifest.storage.provider);
|
|
108
|
+
if (manifest.secretStore?.provider) providers.add(manifest.secretStore.provider);
|
|
97
109
|
if (manifest.state?.provider) providers.add(manifest.state.provider);
|
|
98
110
|
|
|
99
111
|
return [...providers].sort();
|
|
@@ -129,6 +141,38 @@ function collectWarnings(manifest: InfraManifestInput): string[] {
|
|
|
129
141
|
return warnings;
|
|
130
142
|
}
|
|
131
143
|
|
|
144
|
+
function isSupabaseLocalEnabled(manifest: InfraManifestInput): boolean {
|
|
145
|
+
const authProvider = manifest.auth?.provider;
|
|
146
|
+
const databaseProvider = manifest.database?.provider;
|
|
147
|
+
const storageProvider = manifest.storage?.provider;
|
|
148
|
+
const secretStoreProvider = manifest.secretStore?.provider;
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
authProvider === 'supabase' ||
|
|
152
|
+
databaseProvider === 'supabase' ||
|
|
153
|
+
storageProvider === 'supabase' ||
|
|
154
|
+
(storageProvider === 'auto' &&
|
|
155
|
+
(authProvider === 'supabase' || databaseProvider === 'supabase')) ||
|
|
156
|
+
secretStoreProvider === 'supabase-vault'
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function validateCanonicalProjectSlug(slug: string | undefined): string {
|
|
161
|
+
if (!slug) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
'Cannot generate local Supabase infrastructure: appManifest.metadata.slug is required.',
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (slug.trim() !== slug || !CANONICAL_PROJECT_SLUG_RE.test(slug)) {
|
|
168
|
+
throw new Error(
|
|
169
|
+
'Cannot generate local Supabase infrastructure: appManifest.metadata.slug must be a canonical lowercase slug up to 40 characters using only a-z, 0-9, and hyphens, without leading or trailing hyphens.',
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return slug;
|
|
174
|
+
}
|
|
175
|
+
|
|
132
176
|
function resolveNamespace(
|
|
133
177
|
manifest: InfraManifestInput,
|
|
134
178
|
options: InfrastructureGenerationOptions,
|