@ankhorage/infra 0.4.0 → 0.4.2
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 +13 -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 +245 -26
- 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/auth/supabase/profile.integration.test.ts +170 -2
- package/src/adapters/minikube/base/index.test.ts +552 -0
- package/src/adapters/minikube/base/index.ts +249 -25
- 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.
|
|
@@ -1059,13 +1067,21 @@ $$;`;
|
|
|
1059
1067
|
|
|
1060
1068
|
function getSupabaseProfileDisabledGuardSql(): string {
|
|
1061
1069
|
return `do $$
|
|
1070
|
+
declare
|
|
1071
|
+
has_profile_state boolean := false;
|
|
1062
1072
|
begin
|
|
1063
|
-
if to_regclass('ankhorage_internal.generated_schema_state') is not null
|
|
1064
|
-
|
|
1065
|
-
select
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1073
|
+
if to_regclass('ankhorage_internal.generated_schema_state') is not null then
|
|
1074
|
+
execute
|
|
1075
|
+
'select exists (
|
|
1076
|
+
select 1
|
|
1077
|
+
from ankhorage_internal.generated_schema_state
|
|
1078
|
+
where artifact_key = $1
|
|
1079
|
+
)'
|
|
1080
|
+
into has_profile_state
|
|
1081
|
+
using 'auth.profile';
|
|
1082
|
+
end if;
|
|
1083
|
+
|
|
1084
|
+
if has_profile_state then
|
|
1069
1085
|
raise exception 'manifest auth.profile.table was removed but local generated auth.profile state still exists; reset local Supabase or add an explicit cleanup migration';
|
|
1070
1086
|
end if;
|
|
1071
1087
|
end;
|
|
@@ -1414,10 +1430,11 @@ $$;`;
|
|
|
1414
1430
|
|
|
1415
1431
|
function getStatusScript(args: {
|
|
1416
1432
|
defaultNamespace: string;
|
|
1433
|
+
supabaseProjectId: string | null;
|
|
1417
1434
|
supabaseLocalEnabled: boolean;
|
|
1418
1435
|
profileModel: ResolvedProfileModel;
|
|
1419
1436
|
}): string {
|
|
1420
|
-
const { defaultNamespace, supabaseLocalEnabled, profileModel } = args;
|
|
1437
|
+
const { defaultNamespace, supabaseProjectId, supabaseLocalEnabled, profileModel } = args;
|
|
1421
1438
|
const profileStatusSql = profileModel.enabled ? getSupabaseProfileChecksumSql(profileModel) : '';
|
|
1422
1439
|
const profileVerificationSql = profileModel.enabled
|
|
1423
1440
|
? getSupabaseProfileVerificationSql(profileModel)
|
|
@@ -1441,8 +1458,28 @@ AUTH_RUNTIME_MODE="\${AUTH_RUNTIME_MODE:-local}"
|
|
|
1441
1458
|
SUPABASE_LOCAL_ENABLED="\${SUPABASE_LOCAL_ENABLED:-${supabaseLocalEnabled ? 'true' : 'false'}}"
|
|
1442
1459
|
SUPABASE_SECRET_SYNC_ENABLED="\${SUPABASE_SECRET_SYNC_ENABLED:-true}"
|
|
1443
1460
|
SUPABASE_PROJECT_DIR="\${SUPABASE_PROJECT_DIR:-\${ROOT_DIR}}"
|
|
1461
|
+
EXPECTED_SUPABASE_PROJECT_ID="${supabaseProjectId ?? ''}"
|
|
1444
1462
|
SUPABASE_PROFILE_ENABLED="${profileModel.enabled ? 'true' : 'false'}"
|
|
1445
1463
|
SUPABASE_PROFILE_RECONCILE_FILE="\${SUPABASE_PROJECT_DIR}/supabase/generated/auth_profiles.sql"
|
|
1464
|
+
export EXPECTED_SUPABASE_PROJECT_ID
|
|
1465
|
+
|
|
1466
|
+
reject_supabase_project_id_override() {
|
|
1467
|
+
if [[ -n "\${SUPABASE_PROJECT_ID:-}" ]]; then
|
|
1468
|
+
echo "SUPABASE_PROJECT_ID must not be set for generated local Infra scripts."
|
|
1469
|
+
echo "Unset SUPABASE_PROJECT_ID and use supabase/config.toml project_id instead."
|
|
1470
|
+
return 1
|
|
1471
|
+
fi
|
|
1472
|
+
|
|
1473
|
+
unset SUPABASE_PROJECT_ID
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
require_expected_supabase_project_identity() {
|
|
1477
|
+
if [[ -z "\${EXPECTED_SUPABASE_PROJECT_ID}" ]]; then
|
|
1478
|
+
echo "Cannot run local Supabase infrastructure: expected Supabase project identity is empty."
|
|
1479
|
+
echo "Regenerate Infra with appManifest.metadata.slug for Supabase-backed local services."
|
|
1480
|
+
return 1
|
|
1481
|
+
fi
|
|
1482
|
+
}
|
|
1446
1483
|
|
|
1447
1484
|
if ! command -v kubectl >/dev/null 2>&1; then
|
|
1448
1485
|
echo "kubectl is required but not installed."
|
|
@@ -1606,6 +1643,61 @@ run_supabase_sql() {
|
|
|
1606
1643
|
return 1
|
|
1607
1644
|
}
|
|
1608
1645
|
|
|
1646
|
+
validate_supabase_project_identity() {
|
|
1647
|
+
local config_file="\${SUPABASE_PROJECT_DIR}/supabase/config.toml"
|
|
1648
|
+
|
|
1649
|
+
if [[ -z "\${EXPECTED_SUPABASE_PROJECT_ID}" ]]; then
|
|
1650
|
+
return 0
|
|
1651
|
+
fi
|
|
1652
|
+
|
|
1653
|
+
if [[ ! -f "\${config_file}" ]]; then
|
|
1654
|
+
echo "- local Supabase project identity: config missing at \${config_file}"
|
|
1655
|
+
return 1
|
|
1656
|
+
fi
|
|
1657
|
+
|
|
1658
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
1659
|
+
echo "- local Supabase project identity: python3 is required to read \${config_file}"
|
|
1660
|
+
return 1
|
|
1661
|
+
fi
|
|
1662
|
+
|
|
1663
|
+
python3 - "\${config_file}" <<'PY'
|
|
1664
|
+
import os
|
|
1665
|
+
import re
|
|
1666
|
+
import sys
|
|
1667
|
+
|
|
1668
|
+
|
|
1669
|
+
def read_top_level_project_id(config_path):
|
|
1670
|
+
key_re = re.compile(r'^\\s*project_id\\s*=\\s*(.*?)\\s*(?:#.*)?$')
|
|
1671
|
+
section_re = re.compile(r'^\\s*\\[[^\\]]+\\]\\s*$')
|
|
1672
|
+
|
|
1673
|
+
with open(config_path, 'r', encoding='utf-8') as f:
|
|
1674
|
+
for line in f:
|
|
1675
|
+
if section_re.match(line):
|
|
1676
|
+
return None
|
|
1677
|
+
match = key_re.match(line)
|
|
1678
|
+
if not match:
|
|
1679
|
+
continue
|
|
1680
|
+
raw = match.group(1).strip()
|
|
1681
|
+
quoted = re.match(r'^["\\']([^"\\']*)["\\']$', raw)
|
|
1682
|
+
return quoted.group(1) if quoted else raw
|
|
1683
|
+
|
|
1684
|
+
return None
|
|
1685
|
+
|
|
1686
|
+
|
|
1687
|
+
expected = os.environ['EXPECTED_SUPABASE_PROJECT_ID']
|
|
1688
|
+
actual = read_top_level_project_id(sys.argv[1])
|
|
1689
|
+
|
|
1690
|
+
if actual == expected:
|
|
1691
|
+
raise SystemExit(0)
|
|
1692
|
+
|
|
1693
|
+
found = 'missing' if actual is None else f'"{actual}"'
|
|
1694
|
+
print('Supabase project identity mismatch.', file=sys.stderr)
|
|
1695
|
+
print(f'Expected "{expected}", found {found}.', file=sys.stderr)
|
|
1696
|
+
print('Destroy the invalid local stack and run Infra Up again.', file=sys.stderr)
|
|
1697
|
+
raise SystemExit(1)
|
|
1698
|
+
PY
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1609
1701
|
check_immutable_migrations_applied() {
|
|
1610
1702
|
local migrations_dir="\${SUPABASE_PROJECT_DIR}/supabase/migrations"
|
|
1611
1703
|
local applied_versions
|
|
@@ -1640,8 +1732,14 @@ if [[ "\${AUTH_RUNTIME_MODE}" == "local" && "\${SUPABASE_LOCAL_ENABLED}" == "tru
|
|
|
1640
1732
|
echo "Local Supabase database checks:"
|
|
1641
1733
|
status_failed=0
|
|
1642
1734
|
|
|
1643
|
-
if
|
|
1644
|
-
|
|
1735
|
+
if ! require_expected_supabase_project_identity; then
|
|
1736
|
+
status_failed=1
|
|
1737
|
+
elif ! reject_supabase_project_id_override; then
|
|
1738
|
+
status_failed=1
|
|
1739
|
+
elif require_supabase_cli_capabilities; then
|
|
1740
|
+
if ! validate_supabase_project_identity; then
|
|
1741
|
+
status_failed=1
|
|
1742
|
+
elif supabase --workdir "\${SUPABASE_PROJECT_DIR}" status >/dev/null 2>&1; then
|
|
1645
1743
|
STATUS_ENV="$(supabase --workdir "\${SUPABASE_PROJECT_DIR}" status -o env)"
|
|
1646
1744
|
SUPABASE_DB_URL="$(read_env_value "\${STATUS_ENV}" DB_URL)"
|
|
1647
1745
|
if [[ -z "\${SUPABASE_DB_URL}" ]]; then
|
|
@@ -1713,10 +1811,11 @@ fi
|
|
|
1713
1811
|
}
|
|
1714
1812
|
|
|
1715
1813
|
function getSupabaseLocalEnvScript(args: {
|
|
1814
|
+
supabaseProjectId: string | null;
|
|
1716
1815
|
supabaseLocalPorts: SupabaseLocalPorts;
|
|
1717
1816
|
profileModel: ResolvedProfileModel;
|
|
1718
1817
|
}): string {
|
|
1719
|
-
const { supabaseLocalPorts, profileModel } = args;
|
|
1818
|
+
const { supabaseProjectId, supabaseLocalPorts, profileModel } = args;
|
|
1720
1819
|
const profileStatusSql = profileModel.enabled ? getSupabaseProfileChecksumSql(profileModel) : '';
|
|
1721
1820
|
const profileVerificationSql = profileModel.enabled
|
|
1722
1821
|
? getSupabaseProfileVerificationSql(profileModel)
|
|
@@ -1739,9 +1838,29 @@ fi
|
|
|
1739
1838
|
|
|
1740
1839
|
APP_SOURCE_DIR="\${APP_SOURCE_DIR:-$(cd "\${ROOT_DIR}/../.." && pwd)}"
|
|
1741
1840
|
SUPABASE_PROJECT_DIR="\${SUPABASE_PROJECT_DIR:-\${ROOT_DIR}}"
|
|
1841
|
+
EXPECTED_SUPABASE_PROJECT_ID="${supabaseProjectId ?? ''}"
|
|
1742
1842
|
APP_SUPABASE_ENV_FILE="\${APP_SUPABASE_ENV_FILE:-\${APP_SOURCE_DIR}/.env.local}"
|
|
1743
1843
|
SUPABASE_PROFILE_ENABLED="${profileModel.enabled ? 'true' : 'false'}"
|
|
1744
1844
|
SUPABASE_PROFILE_RECONCILE_FILE="\${SUPABASE_PROJECT_DIR}/supabase/generated/auth_profiles.sql"
|
|
1845
|
+
export EXPECTED_SUPABASE_PROJECT_ID
|
|
1846
|
+
|
|
1847
|
+
reject_supabase_project_id_override() {
|
|
1848
|
+
if [[ -n "\${SUPABASE_PROJECT_ID:-}" ]]; then
|
|
1849
|
+
echo "SUPABASE_PROJECT_ID must not be set for generated local Infra scripts."
|
|
1850
|
+
echo "Unset SUPABASE_PROJECT_ID and use supabase/config.toml project_id instead."
|
|
1851
|
+
exit 1
|
|
1852
|
+
fi
|
|
1853
|
+
|
|
1854
|
+
unset SUPABASE_PROJECT_ID
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
require_expected_supabase_project_identity() {
|
|
1858
|
+
if [[ -z "\${EXPECTED_SUPABASE_PROJECT_ID}" ]]; then
|
|
1859
|
+
echo "Cannot run local Supabase infrastructure: expected Supabase project identity is empty."
|
|
1860
|
+
echo "Regenerate Infra with appManifest.metadata.slug for Supabase-backed local services."
|
|
1861
|
+
exit 1
|
|
1862
|
+
fi
|
|
1863
|
+
}
|
|
1745
1864
|
|
|
1746
1865
|
SUPABASE_LOCAL_PORT_BASE="\${SUPABASE_LOCAL_PORT_BASE:-${supabaseLocalPorts.base}}"
|
|
1747
1866
|
SUPABASE_LOCAL_SHADOW_PORT="\${SUPABASE_LOCAL_SHADOW_PORT:-\${SUPABASE_LOCAL_PORT_BASE}}"
|
|
@@ -1787,6 +1906,11 @@ require_supabase_cli_capabilities() {
|
|
|
1787
1906
|
exit 1
|
|
1788
1907
|
fi
|
|
1789
1908
|
|
|
1909
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
1910
|
+
echo "python3 is required to reconcile supabase/config.toml before local Supabase startup."
|
|
1911
|
+
exit 1
|
|
1912
|
+
fi
|
|
1913
|
+
|
|
1790
1914
|
local version
|
|
1791
1915
|
version="$(supabase --version | awk '{print $NF}')"
|
|
1792
1916
|
if ! version_ge "\${version}" "2.106.0"; then
|
|
@@ -1848,6 +1972,8 @@ run_checked_sql_file() {
|
|
|
1848
1972
|
psql "\${SUPABASE_DB_URL}" -v ON_ERROR_STOP=1 -q -f "\${sql_file}"
|
|
1849
1973
|
}
|
|
1850
1974
|
|
|
1975
|
+
require_expected_supabase_project_identity
|
|
1976
|
+
reject_supabase_project_id_override
|
|
1851
1977
|
require_supabase_cli_capabilities
|
|
1852
1978
|
|
|
1853
1979
|
if [[ ! -d "\${SUPABASE_PROJECT_DIR}" ]]; then
|
|
@@ -1858,18 +1984,6 @@ fi
|
|
|
1858
1984
|
configure_supabase_local_ports() {
|
|
1859
1985
|
local config_file="\${SUPABASE_PROJECT_DIR}/supabase/config.toml"
|
|
1860
1986
|
|
|
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
1987
|
python3 - "\${config_file}" <<'PY'
|
|
1874
1988
|
import os
|
|
1875
1989
|
import re
|
|
@@ -1973,8 +2087,114 @@ if __name__ == "__main__":
|
|
|
1973
2087
|
PY
|
|
1974
2088
|
}
|
|
1975
2089
|
|
|
2090
|
+
write_supabase_project_identity_for_new_config() {
|
|
2091
|
+
local config_file="\${SUPABASE_PROJECT_DIR}/supabase/config.toml"
|
|
2092
|
+
|
|
2093
|
+
require_expected_supabase_project_identity
|
|
2094
|
+
|
|
2095
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
2096
|
+
echo "python3 is required to write Supabase project_id in \${config_file}."
|
|
2097
|
+
exit 1
|
|
2098
|
+
fi
|
|
2099
|
+
|
|
2100
|
+
python3 - "\${config_file}" <<'PY'
|
|
2101
|
+
import os
|
|
2102
|
+
import re
|
|
2103
|
+
import sys
|
|
2104
|
+
|
|
2105
|
+
|
|
2106
|
+
def write_top_level_project_id(config_path, expected):
|
|
2107
|
+
key_re = re.compile(r'^(\\s*)project_id\\s*=\\s*(.*?)(\\s*(#.*)?)?$')
|
|
2108
|
+
section_re = re.compile(r'^\\s*\\[[^\\]]+\\]\\s*$')
|
|
2109
|
+
|
|
2110
|
+
with open(config_path, 'r', encoding='utf-8') as f:
|
|
2111
|
+
lines = f.readlines()
|
|
2112
|
+
|
|
2113
|
+
section_start = len(lines)
|
|
2114
|
+
for i, line in enumerate(lines):
|
|
2115
|
+
if section_re.match(line):
|
|
2116
|
+
section_start = i
|
|
2117
|
+
break
|
|
2118
|
+
|
|
2119
|
+
for i in range(section_start):
|
|
2120
|
+
match = key_re.match(lines[i])
|
|
2121
|
+
if not match:
|
|
2122
|
+
continue
|
|
2123
|
+
indent = match.group(1) or ''
|
|
2124
|
+
trailing = (match.group(3) or '').rstrip('\\n')
|
|
2125
|
+
lines[i] = f'{indent}project_id = "{expected}"{trailing}\\n'
|
|
2126
|
+
break
|
|
2127
|
+
else:
|
|
2128
|
+
insert_at = section_start
|
|
2129
|
+
lines.insert(insert_at, f'project_id = "{expected}"\\n')
|
|
2130
|
+
|
|
2131
|
+
with open(config_path, 'w', encoding='utf-8') as f:
|
|
2132
|
+
f.writelines(lines)
|
|
2133
|
+
|
|
2134
|
+
|
|
2135
|
+
write_top_level_project_id(sys.argv[1], os.environ['EXPECTED_SUPABASE_PROJECT_ID'])
|
|
2136
|
+
PY
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
validate_supabase_project_identity() {
|
|
2140
|
+
local config_file="\${SUPABASE_PROJECT_DIR}/supabase/config.toml"
|
|
2141
|
+
|
|
2142
|
+
require_expected_supabase_project_identity
|
|
2143
|
+
|
|
2144
|
+
if [[ ! -f "\${config_file}" ]]; then
|
|
2145
|
+
echo "Supabase project identity mismatch."
|
|
2146
|
+
printf 'Expected "%s", found missing.\n' "\${EXPECTED_SUPABASE_PROJECT_ID}"
|
|
2147
|
+
echo "The existing local Supabase project belongs to a different identity."
|
|
2148
|
+
echo "Destroy the invalid local stack and its project-owned resources, then run Infra Up again."
|
|
2149
|
+
exit 1
|
|
2150
|
+
fi
|
|
2151
|
+
|
|
2152
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
2153
|
+
echo "python3 is required to read Supabase project_id in \${config_file}."
|
|
2154
|
+
exit 1
|
|
2155
|
+
fi
|
|
2156
|
+
|
|
2157
|
+
python3 - "\${config_file}" <<'PY'
|
|
2158
|
+
import os
|
|
2159
|
+
import re
|
|
2160
|
+
import sys
|
|
2161
|
+
|
|
2162
|
+
|
|
2163
|
+
def read_top_level_project_id(config_path):
|
|
2164
|
+
key_re = re.compile(r'^\\s*project_id\\s*=\\s*(.*?)\\s*(?:#.*)?$')
|
|
2165
|
+
section_re = re.compile(r'^\\s*\\[[^\\]]+\\]\\s*$')
|
|
2166
|
+
|
|
2167
|
+
with open(config_path, 'r', encoding='utf-8') as f:
|
|
2168
|
+
for line in f:
|
|
2169
|
+
if section_re.match(line):
|
|
2170
|
+
return None
|
|
2171
|
+
match = key_re.match(line)
|
|
2172
|
+
if not match:
|
|
2173
|
+
continue
|
|
2174
|
+
raw = match.group(1).strip()
|
|
2175
|
+
quoted = re.match(r'^["\\']([^"\\']*)["\\']$', raw)
|
|
2176
|
+
return quoted.group(1) if quoted else raw
|
|
2177
|
+
|
|
2178
|
+
return None
|
|
2179
|
+
|
|
2180
|
+
|
|
2181
|
+
expected = os.environ['EXPECTED_SUPABASE_PROJECT_ID']
|
|
2182
|
+
actual = read_top_level_project_id(sys.argv[1])
|
|
2183
|
+
|
|
2184
|
+
if actual == expected:
|
|
2185
|
+
raise SystemExit(0)
|
|
2186
|
+
|
|
2187
|
+
found = 'missing' if actual is None else f'"{actual}"'
|
|
2188
|
+
print('Supabase project identity mismatch.', file=sys.stderr)
|
|
2189
|
+
print(f'Expected "{expected}", found {found}.', file=sys.stderr)
|
|
2190
|
+
print('The existing local Supabase project belongs to a different identity.', file=sys.stderr)
|
|
2191
|
+
print('Destroy the invalid local stack and its project-owned resources, then run Infra Up again.', file=sys.stderr)
|
|
2192
|
+
raise SystemExit(1)
|
|
2193
|
+
PY
|
|
2194
|
+
}
|
|
2195
|
+
|
|
1976
2196
|
assert_supabase_local_ports_available() {
|
|
1977
|
-
local project_label="\${
|
|
2197
|
+
local project_label="\${EXPECTED_SUPABASE_PROJECT_ID}"
|
|
1978
2198
|
|
|
1979
2199
|
ANKH_PROJECT_LABEL="\${project_label}" python3 - <<'PY'
|
|
1980
2200
|
import os
|
|
@@ -2052,8 +2272,10 @@ PY
|
|
|
2052
2272
|
if [[ ! -f "\${SUPABASE_PROJECT_DIR}/supabase/config.toml" ]]; then
|
|
2053
2273
|
echo "No supabase/config.toml found in \${SUPABASE_PROJECT_DIR}. Initializing Supabase project..."
|
|
2054
2274
|
supabase --workdir "\${SUPABASE_PROJECT_DIR}" init --yes >/dev/null
|
|
2275
|
+
write_supabase_project_identity_for_new_config
|
|
2055
2276
|
fi
|
|
2056
2277
|
|
|
2278
|
+
validate_supabase_project_identity
|
|
2057
2279
|
configure_supabase_local_ports
|
|
2058
2280
|
|
|
2059
2281
|
STATUS_ENV="$(
|
|
@@ -2125,6 +2347,7 @@ start_supabase_local_stack() {
|
|
|
2125
2347
|
}
|
|
2126
2348
|
|
|
2127
2349
|
if [[ -z "\${STATUS_ENV}" ]]; then
|
|
2350
|
+
validate_supabase_project_identity
|
|
2128
2351
|
assert_supabase_local_ports_available
|
|
2129
2352
|
echo "Supabase local stack not detected. Starting with 'supabase start'..."
|
|
2130
2353
|
start_supabase_local_stack
|
|
@@ -2138,6 +2361,7 @@ if [[ -z "\${SUPABASE_DB_URL}" ]]; then
|
|
|
2138
2361
|
fi
|
|
2139
2362
|
|
|
2140
2363
|
echo "Applying pending immutable Supabase migrations..."
|
|
2364
|
+
validate_supabase_project_identity
|
|
2141
2365
|
run_checked_command \
|
|
2142
2366
|
"Immutable Supabase migration application" \
|
|
2143
2367
|
"" \
|
|
@@ -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
|
+
}
|