@ankhorage/infra 0.4.0 → 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 +6 -0
- package/README.md +1 -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 +231 -20
- 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 +26 -0
- package/dist/adapters/minikube/index.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters/minikube/base/index.test.ts +456 -0
- package/src/adapters/minikube/base/index.ts +235 -19
- package/src/adapters/minikube/index.ts +37 -0
- package/src/adapters/minikube/secrets/index.test.ts +37 -1
- package/src/index.test.ts +151 -12
|
@@ -30,17 +30,18 @@ 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';
|
|
@@ -68,6 +69,7 @@ export function generateMinikubeBaseArtifacts(args: {
|
|
|
68
69
|
authzEngine,
|
|
69
70
|
extraResources,
|
|
70
71
|
supabaseLocalPorts,
|
|
72
|
+
supabaseProjectId,
|
|
71
73
|
supabaseLocalEnabled,
|
|
72
74
|
profileModel,
|
|
73
75
|
}),
|
|
@@ -140,6 +142,7 @@ export function generateMinikubeBaseArtifacts(args: {
|
|
|
140
142
|
path: `${scriptsRoot}/status.sh`,
|
|
141
143
|
content: getStatusScript({
|
|
142
144
|
defaultNamespace: namespace,
|
|
145
|
+
supabaseProjectId,
|
|
143
146
|
supabaseLocalEnabled,
|
|
144
147
|
profileModel,
|
|
145
148
|
}),
|
|
@@ -148,6 +151,7 @@ export function generateMinikubeBaseArtifacts(args: {
|
|
|
148
151
|
{
|
|
149
152
|
path: `${scriptsRoot}/supabase-local-env.sh`,
|
|
150
153
|
content: getSupabaseLocalEnvScript({
|
|
154
|
+
supabaseProjectId,
|
|
151
155
|
supabaseLocalPorts,
|
|
152
156
|
profileModel,
|
|
153
157
|
}),
|
|
@@ -168,6 +172,7 @@ function getReadmeMarkdown(args: {
|
|
|
168
172
|
authzEngine: string;
|
|
169
173
|
extraResources: string[];
|
|
170
174
|
supabaseLocalPorts: SupabaseLocalPorts;
|
|
175
|
+
supabaseProjectId: string | null;
|
|
171
176
|
supabaseLocalEnabled: boolean;
|
|
172
177
|
profileModel: ResolvedProfileModel;
|
|
173
178
|
}): string {
|
|
@@ -179,6 +184,7 @@ function getReadmeMarkdown(args: {
|
|
|
179
184
|
authzEngine,
|
|
180
185
|
extraResources,
|
|
181
186
|
supabaseLocalPorts,
|
|
187
|
+
supabaseProjectId,
|
|
182
188
|
supabaseLocalEnabled,
|
|
183
189
|
profileModel,
|
|
184
190
|
} = args;
|
|
@@ -245,6 +251,7 @@ ${resourceLines}
|
|
|
245
251
|
## Runtime Conventions
|
|
246
252
|
|
|
247
253
|
- Namespace: \`${namespace}\`
|
|
254
|
+
- Supabase local project identity: \`${supabaseProjectId ?? 'unused'}\`
|
|
248
255
|
- Monitoring requested: \`${monitoringEnabled ? 'true' : 'false'}\`
|
|
249
256
|
- Auth provider: \`${authProvider}\`
|
|
250
257
|
- Authorization engine: \`${authzEngine}\`
|
|
@@ -332,6 +339,7 @@ Build flow:
|
|
|
332
339
|
|
|
333
340
|
- \`./scripts/up.sh\` calls \`./scripts/build-app-image.sh\` when \`APP_BUILD_ENABLED=true\`.
|
|
334
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.
|
|
335
343
|
- Immutable migrations live in \`supabase/migrations/\` and are applied with \`supabase migration up --local\`.
|
|
336
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\`.
|
|
337
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.
|
|
@@ -1414,10 +1422,11 @@ $$;`;
|
|
|
1414
1422
|
|
|
1415
1423
|
function getStatusScript(args: {
|
|
1416
1424
|
defaultNamespace: string;
|
|
1425
|
+
supabaseProjectId: string | null;
|
|
1417
1426
|
supabaseLocalEnabled: boolean;
|
|
1418
1427
|
profileModel: ResolvedProfileModel;
|
|
1419
1428
|
}): string {
|
|
1420
|
-
const { defaultNamespace, supabaseLocalEnabled, profileModel } = args;
|
|
1429
|
+
const { defaultNamespace, supabaseProjectId, supabaseLocalEnabled, profileModel } = args;
|
|
1421
1430
|
const profileStatusSql = profileModel.enabled ? getSupabaseProfileChecksumSql(profileModel) : '';
|
|
1422
1431
|
const profileVerificationSql = profileModel.enabled
|
|
1423
1432
|
? getSupabaseProfileVerificationSql(profileModel)
|
|
@@ -1441,8 +1450,28 @@ AUTH_RUNTIME_MODE="\${AUTH_RUNTIME_MODE:-local}"
|
|
|
1441
1450
|
SUPABASE_LOCAL_ENABLED="\${SUPABASE_LOCAL_ENABLED:-${supabaseLocalEnabled ? 'true' : 'false'}}"
|
|
1442
1451
|
SUPABASE_SECRET_SYNC_ENABLED="\${SUPABASE_SECRET_SYNC_ENABLED:-true}"
|
|
1443
1452
|
SUPABASE_PROJECT_DIR="\${SUPABASE_PROJECT_DIR:-\${ROOT_DIR}}"
|
|
1453
|
+
EXPECTED_SUPABASE_PROJECT_ID="${supabaseProjectId ?? ''}"
|
|
1444
1454
|
SUPABASE_PROFILE_ENABLED="${profileModel.enabled ? 'true' : 'false'}"
|
|
1445
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
|
+
}
|
|
1446
1475
|
|
|
1447
1476
|
if ! command -v kubectl >/dev/null 2>&1; then
|
|
1448
1477
|
echo "kubectl is required but not installed."
|
|
@@ -1606,6 +1635,61 @@ run_supabase_sql() {
|
|
|
1606
1635
|
return 1
|
|
1607
1636
|
}
|
|
1608
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
|
+
|
|
1609
1693
|
check_immutable_migrations_applied() {
|
|
1610
1694
|
local migrations_dir="\${SUPABASE_PROJECT_DIR}/supabase/migrations"
|
|
1611
1695
|
local applied_versions
|
|
@@ -1640,8 +1724,14 @@ if [[ "\${AUTH_RUNTIME_MODE}" == "local" && "\${SUPABASE_LOCAL_ENABLED}" == "tru
|
|
|
1640
1724
|
echo "Local Supabase database checks:"
|
|
1641
1725
|
status_failed=0
|
|
1642
1726
|
|
|
1643
|
-
if
|
|
1644
|
-
|
|
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
|
|
1645
1735
|
STATUS_ENV="$(supabase --workdir "\${SUPABASE_PROJECT_DIR}" status -o env)"
|
|
1646
1736
|
SUPABASE_DB_URL="$(read_env_value "\${STATUS_ENV}" DB_URL)"
|
|
1647
1737
|
if [[ -z "\${SUPABASE_DB_URL}" ]]; then
|
|
@@ -1713,10 +1803,11 @@ fi
|
|
|
1713
1803
|
}
|
|
1714
1804
|
|
|
1715
1805
|
function getSupabaseLocalEnvScript(args: {
|
|
1806
|
+
supabaseProjectId: string | null;
|
|
1716
1807
|
supabaseLocalPorts: SupabaseLocalPorts;
|
|
1717
1808
|
profileModel: ResolvedProfileModel;
|
|
1718
1809
|
}): string {
|
|
1719
|
-
const { supabaseLocalPorts, profileModel } = args;
|
|
1810
|
+
const { supabaseProjectId, supabaseLocalPorts, profileModel } = args;
|
|
1720
1811
|
const profileStatusSql = profileModel.enabled ? getSupabaseProfileChecksumSql(profileModel) : '';
|
|
1721
1812
|
const profileVerificationSql = profileModel.enabled
|
|
1722
1813
|
? getSupabaseProfileVerificationSql(profileModel)
|
|
@@ -1739,9 +1830,29 @@ fi
|
|
|
1739
1830
|
|
|
1740
1831
|
APP_SOURCE_DIR="\${APP_SOURCE_DIR:-$(cd "\${ROOT_DIR}/../.." && pwd)}"
|
|
1741
1832
|
SUPABASE_PROJECT_DIR="\${SUPABASE_PROJECT_DIR:-\${ROOT_DIR}}"
|
|
1833
|
+
EXPECTED_SUPABASE_PROJECT_ID="${supabaseProjectId ?? ''}"
|
|
1742
1834
|
APP_SUPABASE_ENV_FILE="\${APP_SUPABASE_ENV_FILE:-\${APP_SOURCE_DIR}/.env.local}"
|
|
1743
1835
|
SUPABASE_PROFILE_ENABLED="${profileModel.enabled ? 'true' : 'false'}"
|
|
1744
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
|
+
}
|
|
1745
1856
|
|
|
1746
1857
|
SUPABASE_LOCAL_PORT_BASE="\${SUPABASE_LOCAL_PORT_BASE:-${supabaseLocalPorts.base}}"
|
|
1747
1858
|
SUPABASE_LOCAL_SHADOW_PORT="\${SUPABASE_LOCAL_SHADOW_PORT:-\${SUPABASE_LOCAL_PORT_BASE}}"
|
|
@@ -1787,6 +1898,11 @@ require_supabase_cli_capabilities() {
|
|
|
1787
1898
|
exit 1
|
|
1788
1899
|
fi
|
|
1789
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
|
+
|
|
1790
1906
|
local version
|
|
1791
1907
|
version="$(supabase --version | awk '{print $NF}')"
|
|
1792
1908
|
if ! version_ge "\${version}" "2.106.0"; then
|
|
@@ -1848,6 +1964,8 @@ run_checked_sql_file() {
|
|
|
1848
1964
|
psql "\${SUPABASE_DB_URL}" -v ON_ERROR_STOP=1 -q -f "\${sql_file}"
|
|
1849
1965
|
}
|
|
1850
1966
|
|
|
1967
|
+
require_expected_supabase_project_identity
|
|
1968
|
+
reject_supabase_project_id_override
|
|
1851
1969
|
require_supabase_cli_capabilities
|
|
1852
1970
|
|
|
1853
1971
|
if [[ ! -d "\${SUPABASE_PROJECT_DIR}" ]]; then
|
|
@@ -1858,18 +1976,6 @@ fi
|
|
|
1858
1976
|
configure_supabase_local_ports() {
|
|
1859
1977
|
local config_file="\${SUPABASE_PROJECT_DIR}/supabase/config.toml"
|
|
1860
1978
|
|
|
1861
|
-
if ! command -v python3 >/dev/null 2>&1; then
|
|
1862
|
-
echo "python3 is required to patch supabase/config.toml with generated local port defaults."
|
|
1863
|
-
echo "Install python3 (recommended) or manually edit \${config_file} ports:"
|
|
1864
|
-
echo "- [api] port = \${SUPABASE_LOCAL_API_PORT}"
|
|
1865
|
-
echo "- [db] port = \${SUPABASE_LOCAL_DB_PORT}"
|
|
1866
|
-
echo "- [db] shadow_port = \${SUPABASE_LOCAL_SHADOW_PORT}"
|
|
1867
|
-
echo "- [studio] port = \${SUPABASE_LOCAL_STUDIO_PORT}"
|
|
1868
|
-
echo "- [inbucket] port = \${SUPABASE_LOCAL_INBUCKET_PORT}"
|
|
1869
|
-
echo "- [analytics] port = \${SUPABASE_LOCAL_ANALYTICS_PORT}"
|
|
1870
|
-
exit 1
|
|
1871
|
-
fi
|
|
1872
|
-
|
|
1873
1979
|
python3 - "\${config_file}" <<'PY'
|
|
1874
1980
|
import os
|
|
1875
1981
|
import re
|
|
@@ -1973,8 +2079,114 @@ if __name__ == "__main__":
|
|
|
1973
2079
|
PY
|
|
1974
2080
|
}
|
|
1975
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
|
+
|
|
1976
2188
|
assert_supabase_local_ports_available() {
|
|
1977
|
-
local project_label="\${
|
|
2189
|
+
local project_label="\${EXPECTED_SUPABASE_PROJECT_ID}"
|
|
1978
2190
|
|
|
1979
2191
|
ANKH_PROJECT_LABEL="\${project_label}" python3 - <<'PY'
|
|
1980
2192
|
import os
|
|
@@ -2052,8 +2264,10 @@ PY
|
|
|
2052
2264
|
if [[ ! -f "\${SUPABASE_PROJECT_DIR}/supabase/config.toml" ]]; then
|
|
2053
2265
|
echo "No supabase/config.toml found in \${SUPABASE_PROJECT_DIR}. Initializing Supabase project..."
|
|
2054
2266
|
supabase --workdir "\${SUPABASE_PROJECT_DIR}" init --yes >/dev/null
|
|
2267
|
+
write_supabase_project_identity_for_new_config
|
|
2055
2268
|
fi
|
|
2056
2269
|
|
|
2270
|
+
validate_supabase_project_identity
|
|
2057
2271
|
configure_supabase_local_ports
|
|
2058
2272
|
|
|
2059
2273
|
STATUS_ENV="$(
|
|
@@ -2125,6 +2339,7 @@ start_supabase_local_stack() {
|
|
|
2125
2339
|
}
|
|
2126
2340
|
|
|
2127
2341
|
if [[ -z "\${STATUS_ENV}" ]]; then
|
|
2342
|
+
validate_supabase_project_identity
|
|
2128
2343
|
assert_supabase_local_ports_available
|
|
2129
2344
|
echo "Supabase local stack not detected. Starting with 'supabase start'..."
|
|
2130
2345
|
start_supabase_local_stack
|
|
@@ -2138,6 +2353,7 @@ if [[ -z "\${SUPABASE_DB_URL}" ]]; then
|
|
|
2138
2353
|
fi
|
|
2139
2354
|
|
|
2140
2355
|
echo "Applying pending immutable Supabase migrations..."
|
|
2356
|
+
validate_supabase_project_identity
|
|
2141
2357
|
run_checked_command \
|
|
2142
2358
|
"Immutable Supabase migration application" \
|
|
2143
2359
|
"" \
|
|
@@ -12,6 +12,7 @@ import { generateSecretStoreArtifacts } from './secrets';
|
|
|
12
12
|
import { generateStorageArtifacts } from './storage';
|
|
13
13
|
|
|
14
14
|
const DEFAULT_NAMESPACE = 'ankh-app';
|
|
15
|
+
const CANONICAL_PROJECT_SLUG_RE = /^[a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?$/;
|
|
15
16
|
|
|
16
17
|
export function generateMinikubeInfra(
|
|
17
18
|
manifest: InfraManifestInput,
|
|
@@ -30,6 +31,9 @@ export function generateMinikubeInfra(
|
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
const namespace = resolveNamespace(manifest, options);
|
|
34
|
+
const supabaseProjectId = isSupabaseLocalEnabled(manifest)
|
|
35
|
+
? validateCanonicalProjectSlug(options.appManifest?.metadata.slug)
|
|
36
|
+
: null;
|
|
33
37
|
|
|
34
38
|
const authArtifacts = generateAuthProviderArtifacts({ manifest, namespace });
|
|
35
39
|
const authzArtifacts = generateAuthorizationArtifacts({
|
|
@@ -60,6 +64,7 @@ export function generateMinikubeInfra(
|
|
|
60
64
|
const baseFiles = generateMinikubeBaseArtifacts({
|
|
61
65
|
manifest,
|
|
62
66
|
namespace,
|
|
67
|
+
supabaseProjectId,
|
|
63
68
|
extraResources,
|
|
64
69
|
extraEnvEntries,
|
|
65
70
|
});
|
|
@@ -136,6 +141,38 @@ function collectWarnings(manifest: InfraManifestInput): string[] {
|
|
|
136
141
|
return warnings;
|
|
137
142
|
}
|
|
138
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
|
+
|
|
139
176
|
function resolveNamespace(
|
|
140
177
|
manifest: InfraManifestInput,
|
|
141
178
|
options: InfrastructureGenerationOptions,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AppManifest } from '@ankhorage/contracts';
|
|
1
2
|
import { SUPABASE_VAULT_MIGRATION_SQL } from '@ankhorage/supabase-vault';
|
|
2
3
|
import { describe, expect, test } from 'bun:test';
|
|
3
4
|
|
|
@@ -21,7 +22,9 @@ function createManifest(overrides: Partial<InfraManifestInput> = {}): InfraManif
|
|
|
21
22
|
|
|
22
23
|
describe('minikube secret-store generation', () => {
|
|
23
24
|
test('generates the released Supabase Vault migration through the existing lifecycle', () => {
|
|
24
|
-
const result = generateInfrastructure(createManifest()
|
|
25
|
+
const result = generateInfrastructure(createManifest(), {
|
|
26
|
+
appManifest: createAppManifest('vault-app'),
|
|
27
|
+
});
|
|
25
28
|
const migration = result.files.find((file) => file.path === SUPABASE_VAULT_MIGRATION_PATH);
|
|
26
29
|
const envExample = result.files.find((file) => file.path === 'infra/minikube/.env.example');
|
|
27
30
|
const upScript = result.files.find((file) => file.path === 'infra/minikube/scripts/up.sh');
|
|
@@ -55,6 +58,7 @@ describe('minikube secret-store generation', () => {
|
|
|
55
58
|
},
|
|
56
59
|
},
|
|
57
60
|
}),
|
|
61
|
+
{ appManifest: createAppManifest('vault-app') },
|
|
58
62
|
),
|
|
59
63
|
).toThrow('does not define credentialsRef');
|
|
60
64
|
});
|
|
@@ -81,6 +85,7 @@ describe('minikube secret-store generation', () => {
|
|
|
81
85
|
},
|
|
82
86
|
},
|
|
83
87
|
}),
|
|
88
|
+
{ appManifest: createAppManifest('vault-app') },
|
|
84
89
|
);
|
|
85
90
|
|
|
86
91
|
const serialized = JSON.stringify(result.files);
|
|
@@ -101,3 +106,34 @@ describe('minikube secret-store generation', () => {
|
|
|
101
106
|
).toThrow('Unsupported secret-store provider for minikube adapter');
|
|
102
107
|
});
|
|
103
108
|
});
|
|
109
|
+
|
|
110
|
+
function createAppManifest(
|
|
111
|
+
slug: string,
|
|
112
|
+
): Pick<AppManifest, 'metadata' | 'navigator' | 'screens' | 'settings'> {
|
|
113
|
+
return {
|
|
114
|
+
metadata: {
|
|
115
|
+
name: slug,
|
|
116
|
+
slug,
|
|
117
|
+
version: '1.0.0',
|
|
118
|
+
themeId: 'default',
|
|
119
|
+
},
|
|
120
|
+
navigator: {
|
|
121
|
+
type: 'stack',
|
|
122
|
+
routes: [],
|
|
123
|
+
},
|
|
124
|
+
screens: {},
|
|
125
|
+
settings: {
|
|
126
|
+
localization: {
|
|
127
|
+
defaultLocale: 'en',
|
|
128
|
+
locales: ['en'],
|
|
129
|
+
},
|
|
130
|
+
authFlow: {
|
|
131
|
+
signInRoute: '/sign-in',
|
|
132
|
+
signUpRoute: '/sign-up',
|
|
133
|
+
signOutRoute: '/sign-out',
|
|
134
|
+
unauthorizedRoute: '/sign-in',
|
|
135
|
+
postSignInRoute: '/',
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|