@ankhorage/infra 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +17 -0
- package/dist/adapters/minikube/auth/index.d.ts +7 -0
- package/dist/adapters/minikube/auth/index.d.ts.map +1 -0
- package/dist/adapters/minikube/auth/index.js +14 -0
- package/dist/adapters/minikube/auth/index.js.map +1 -0
- package/dist/adapters/minikube/auth/supabase/index.d.ts +7 -0
- package/dist/adapters/minikube/auth/supabase/index.d.ts.map +1 -0
- package/dist/adapters/minikube/auth/supabase/index.js +213 -0
- package/dist/adapters/minikube/auth/supabase/index.js.map +1 -0
- package/dist/adapters/minikube/authz/cerbos/index.d.ts +9 -0
- package/dist/adapters/minikube/authz/cerbos/index.d.ts.map +1 -0
- package/dist/adapters/minikube/authz/cerbos/index.js +360 -0
- package/dist/adapters/minikube/authz/cerbos/index.js.map +1 -0
- package/dist/adapters/minikube/authz/index.d.ts +9 -0
- package/dist/adapters/minikube/authz/index.d.ts.map +1 -0
- package/dist/adapters/minikube/authz/index.js +23 -0
- package/dist/adapters/minikube/authz/index.js.map +1 -0
- package/dist/adapters/minikube/base/index.d.ts +9 -0
- package/dist/adapters/minikube/base/index.d.ts.map +1 -0
- package/dist/adapters/minikube/base/index.js +1371 -0
- package/dist/adapters/minikube/base/index.js.map +1 -0
- package/dist/adapters/minikube/contracts.d.ts +9 -0
- package/dist/adapters/minikube/contracts.d.ts.map +1 -0
- package/dist/adapters/minikube/contracts.js +9 -0
- package/dist/adapters/minikube/contracts.js.map +1 -0
- package/dist/adapters/minikube/index.d.ts +3 -0
- package/dist/adapters/minikube/index.d.ts.map +1 -0
- package/dist/adapters/minikube/index.js +114 -0
- package/dist/adapters/minikube/index.js.map +1 -0
- package/dist/adapters/minikube/storage/index.d.ts +7 -0
- package/dist/adapters/minikube/storage/index.d.ts.map +1 -0
- package/dist/adapters/minikube/storage/index.js +77 -0
- package/dist/adapters/minikube/storage/index.js.map +1 -0
- package/dist/adapters/minikube/storage/supabase/index.d.ts +7 -0
- package/dist/adapters/minikube/storage/supabase/index.d.ts.map +1 -0
- package/dist/adapters/minikube/storage/supabase/index.js +81 -0
- package/dist/adapters/minikube/storage/supabase/index.js.map +1 -0
- package/dist/datasets.d.ts +11 -0
- package/dist/datasets.d.ts.map +1 -0
- package/dist/datasets.js +397 -0
- package/dist/datasets.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +70 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +86 -0
- package/src/adapters/minikube/README.md +73 -0
- package/src/adapters/minikube/auth/index.ts +23 -0
- package/src/adapters/minikube/auth/supabase/index.ts +269 -0
- package/src/adapters/minikube/authz/cerbos/index.ts +458 -0
- package/src/adapters/minikube/authz/index.ts +32 -0
- package/src/adapters/minikube/base/index.test.ts +103 -0
- package/src/adapters/minikube/base/index.ts +1478 -0
- package/src/adapters/minikube/contracts.ts +17 -0
- package/src/adapters/minikube/index.ts +149 -0
- package/src/adapters/minikube/storage/index.ts +91 -0
- package/src/adapters/minikube/storage/supabase/index.ts +112 -0
- package/src/datasets.test.ts +178 -0
- package/src/datasets.ts +562 -0
- package/src/index.test.ts +679 -0
- package/src/index.ts +43 -0
- package/src/minikubeImageLifecycle.test.ts +34 -0
- package/src/types.ts +82 -0
|
@@ -0,0 +1,1478 @@
|
|
|
1
|
+
import type { GeneratedInfrastructureFile } from '../../../types';
|
|
2
|
+
import type { InfraManifestInput } from '../../../types';
|
|
3
|
+
|
|
4
|
+
interface SupabaseLocalPorts {
|
|
5
|
+
base: number;
|
|
6
|
+
shadow: number;
|
|
7
|
+
api: number;
|
|
8
|
+
db: number;
|
|
9
|
+
studio: number;
|
|
10
|
+
inbucket: number;
|
|
11
|
+
analytics: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface AppInfraStorageMetadata {
|
|
15
|
+
provider: 'supabase';
|
|
16
|
+
bucketsCsv: string;
|
|
17
|
+
defaultBucket: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const SUPABASE_LOCAL_PORT_BASE = 55320;
|
|
21
|
+
const SUPABASE_LOCAL_PORT_BUCKET_SIZE = 10;
|
|
22
|
+
const SUPABASE_LOCAL_PORT_BUCKET_COUNT = 1000;
|
|
23
|
+
const SUPABASE_LOCAL_PORT_REFERENCE_PROJECT = 'my-app';
|
|
24
|
+
|
|
25
|
+
export function generateMinikubeBaseArtifacts(args: {
|
|
26
|
+
manifest: InfraManifestInput;
|
|
27
|
+
namespace: string;
|
|
28
|
+
extraResources: string[];
|
|
29
|
+
extraEnvEntries: string[];
|
|
30
|
+
}): GeneratedInfrastructureFile[] {
|
|
31
|
+
const { manifest, namespace, extraResources, extraEnvEntries } = args;
|
|
32
|
+
|
|
33
|
+
const root = 'infra/minikube';
|
|
34
|
+
const k8sRoot = `${root}/k8s`;
|
|
35
|
+
const scriptsRoot = `${root}/scripts`;
|
|
36
|
+
const appImageRoot = `${root}/app-image`;
|
|
37
|
+
const defaultAppImage = getDefaultAppImage(namespace);
|
|
38
|
+
const supabaseLocalPorts = resolveSupabaseLocalPorts(namespace);
|
|
39
|
+
|
|
40
|
+
const authScope = manifest.auth?.scope ?? 'none';
|
|
41
|
+
const authProvider = manifest.auth?.provider ?? 'none';
|
|
42
|
+
const authzEngine = manifest.auth?.authorization.engine ?? 'none';
|
|
43
|
+
const databaseProvider = manifest.database?.provider ?? 'none';
|
|
44
|
+
const storageMetadata = resolveAppInfraStorageMetadata(manifest);
|
|
45
|
+
const monitoringEnabled = manifest.deployment?.monitoring === true;
|
|
46
|
+
const domain = manifest.networking?.domain ?? '';
|
|
47
|
+
|
|
48
|
+
return [
|
|
49
|
+
{
|
|
50
|
+
path: `${root}/README.md`,
|
|
51
|
+
content: getReadmeMarkdown({
|
|
52
|
+
namespace,
|
|
53
|
+
defaultAppImage,
|
|
54
|
+
monitoringEnabled,
|
|
55
|
+
authProvider,
|
|
56
|
+
authzEngine,
|
|
57
|
+
extraResources,
|
|
58
|
+
supabaseLocalPorts,
|
|
59
|
+
}),
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
path: `${root}/.env.example`,
|
|
63
|
+
content: getEnvExample({
|
|
64
|
+
namespace,
|
|
65
|
+
domain,
|
|
66
|
+
extraEnvEntries,
|
|
67
|
+
defaultAppImage,
|
|
68
|
+
supabaseLocalPorts,
|
|
69
|
+
}),
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
path: `${k8sRoot}/namespace.yaml`,
|
|
73
|
+
content: getNamespaceManifest(namespace),
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
path: `${k8sRoot}/app.configmap.yaml`,
|
|
77
|
+
content: getAppConfigMap({
|
|
78
|
+
namespace,
|
|
79
|
+
authScope,
|
|
80
|
+
authProvider,
|
|
81
|
+
databaseProvider,
|
|
82
|
+
storageMetadata,
|
|
83
|
+
monitoringEnabled,
|
|
84
|
+
domain,
|
|
85
|
+
}),
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
path: `${k8sRoot}/app/deployment.yaml`,
|
|
89
|
+
content: getAppDeploymentManifest({ namespace, defaultAppImage }),
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
path: `${k8sRoot}/app/service.yaml`,
|
|
93
|
+
content: getAppServiceManifest(namespace),
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
path: `${k8sRoot}/kustomization.yaml`,
|
|
97
|
+
content: getKustomizationManifest(extraResources),
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
path: `${scriptsRoot}/up.sh`,
|
|
101
|
+
content: getUpScript({ defaultNamespace: namespace, defaultAppImage }),
|
|
102
|
+
executable: true,
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
path: `${scriptsRoot}/build-app-image.sh`,
|
|
106
|
+
content: getBuildAppImageScript({ defaultNamespace: namespace, defaultAppImage }),
|
|
107
|
+
executable: true,
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
path: `${scriptsRoot}/port-forward.sh`,
|
|
111
|
+
content: getPortForwardScript(namespace),
|
|
112
|
+
executable: true,
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
path: `${scriptsRoot}/down.sh`,
|
|
116
|
+
content: getDownScript(),
|
|
117
|
+
executable: true,
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
path: `${scriptsRoot}/status.sh`,
|
|
121
|
+
content: getStatusScript(namespace),
|
|
122
|
+
executable: true,
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
path: `${scriptsRoot}/supabase-local-env.sh`,
|
|
126
|
+
content: getSupabaseLocalEnvScript(supabaseLocalPorts),
|
|
127
|
+
executable: true,
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
path: `${appImageRoot}/Dockerfile`,
|
|
131
|
+
content: getAppImageDockerfile(),
|
|
132
|
+
},
|
|
133
|
+
];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function getReadmeMarkdown(args: {
|
|
137
|
+
namespace: string;
|
|
138
|
+
defaultAppImage: string;
|
|
139
|
+
monitoringEnabled: boolean;
|
|
140
|
+
authProvider: string;
|
|
141
|
+
authzEngine: string;
|
|
142
|
+
extraResources: string[];
|
|
143
|
+
supabaseLocalPorts: SupabaseLocalPorts;
|
|
144
|
+
}): string {
|
|
145
|
+
const {
|
|
146
|
+
namespace,
|
|
147
|
+
defaultAppImage,
|
|
148
|
+
monitoringEnabled,
|
|
149
|
+
authProvider,
|
|
150
|
+
authzEngine,
|
|
151
|
+
extraResources,
|
|
152
|
+
supabaseLocalPorts,
|
|
153
|
+
} = args;
|
|
154
|
+
const resourceLines = [
|
|
155
|
+
'namespace.yaml',
|
|
156
|
+
'app.configmap.yaml',
|
|
157
|
+
'app/deployment.yaml',
|
|
158
|
+
'app/service.yaml',
|
|
159
|
+
...extraResources,
|
|
160
|
+
]
|
|
161
|
+
.map((r) => `- \`k8s/${r}\``)
|
|
162
|
+
.join('\n');
|
|
163
|
+
|
|
164
|
+
return `# Minikube Infra
|
|
165
|
+
|
|
166
|
+
This directory is generated from \`ankh.config.json\` (infra manifest).
|
|
167
|
+
|
|
168
|
+
## Requirements
|
|
169
|
+
|
|
170
|
+
- minikube
|
|
171
|
+
- kubectl
|
|
172
|
+
- docker (when using the default driver)
|
|
173
|
+
- bun (for Expo web export in app image build flow)
|
|
174
|
+
- python3 (for patching generated local Supabase port defaults and checking port availability)
|
|
175
|
+
|
|
176
|
+
## Quick Start
|
|
177
|
+
|
|
178
|
+
1. Copy env template:
|
|
179
|
+
\`\`\`bash
|
|
180
|
+
cp .env.example .env
|
|
181
|
+
\`\`\`
|
|
182
|
+
2. Optional: import local Supabase credentials from \`supabase status\` output:
|
|
183
|
+
\`\`\`bash
|
|
184
|
+
./scripts/supabase-local-env.sh
|
|
185
|
+
\`\`\`
|
|
186
|
+
3. Apply baseline infra:
|
|
187
|
+
\`\`\`bash
|
|
188
|
+
./scripts/up.sh
|
|
189
|
+
\`\`\`
|
|
190
|
+
4. Start app port-forward:
|
|
191
|
+
\`\`\`bash
|
|
192
|
+
./scripts/port-forward.sh
|
|
193
|
+
\`\`\`
|
|
194
|
+
5. Check status:
|
|
195
|
+
\`\`\`bash
|
|
196
|
+
./scripts/status.sh
|
|
197
|
+
\`\`\`
|
|
198
|
+
6. Tear down:
|
|
199
|
+
\`\`\`bash
|
|
200
|
+
./scripts/down.sh
|
|
201
|
+
\`\`\`
|
|
202
|
+
|
|
203
|
+
## Generated Resources
|
|
204
|
+
|
|
205
|
+
${resourceLines}
|
|
206
|
+
|
|
207
|
+
## Generated Helpers
|
|
208
|
+
|
|
209
|
+
- \`scripts/build-app-image.sh\`
|
|
210
|
+
- \`scripts/port-forward.sh\`
|
|
211
|
+
- \`scripts/supabase-local-env.sh\`
|
|
212
|
+
- \`app-image/Dockerfile\`
|
|
213
|
+
|
|
214
|
+
## Runtime Conventions
|
|
215
|
+
|
|
216
|
+
- Namespace: \`${namespace}\`
|
|
217
|
+
- Monitoring requested: \`${monitoringEnabled ? 'true' : 'false'}\`
|
|
218
|
+
- Auth provider: \`${authProvider}\`
|
|
219
|
+
- Authorization engine: \`${authzEngine}\`
|
|
220
|
+
- Namespace source: \`infra.networking.domain\` first, otherwise project slug hint from CLI.
|
|
221
|
+
|
|
222
|
+
## App Runtime
|
|
223
|
+
|
|
224
|
+
Default app runtime manifests are generated:
|
|
225
|
+
|
|
226
|
+
- \`k8s/app/deployment.yaml\`
|
|
227
|
+
- \`k8s/app/service.yaml\`
|
|
228
|
+
|
|
229
|
+
Deployment env wiring includes:
|
|
230
|
+
|
|
231
|
+
- \`ConfigMap/app-infra-config\`
|
|
232
|
+
- \`ConfigMap/app-runtime-auth-env\` (optional)
|
|
233
|
+
- \`ConfigMap/app-runtime-storage-env\` (optional)
|
|
234
|
+
- \`Secret/supabase-auth-secrets\` (optional)
|
|
235
|
+
|
|
236
|
+
If \`auth.provider\` is \`supabase\`, use:
|
|
237
|
+
|
|
238
|
+
- \`k8s/auth/supabase/supabase-auth.secret.yaml\`
|
|
239
|
+
- \`k8s/auth/supabase/app-runtime-auth.env.configmap.yaml\`
|
|
240
|
+
- \`auth/supabase-runtime-wiring.md\`
|
|
241
|
+
|
|
242
|
+
The app deployment should import runtime config sources in this order:
|
|
243
|
+
|
|
244
|
+
\`\`\`yaml
|
|
245
|
+
envFrom:
|
|
246
|
+
- configMapRef:
|
|
247
|
+
name: app-runtime-auth-env
|
|
248
|
+
- configMapRef:
|
|
249
|
+
name: app-runtime-storage-env
|
|
250
|
+
- secretRef:
|
|
251
|
+
name: supabase-auth-secrets
|
|
252
|
+
\`\`\`
|
|
253
|
+
|
|
254
|
+
Default runtime knobs in \`.env\`:
|
|
255
|
+
|
|
256
|
+
- \`APP_BUILD_ENABLED\`
|
|
257
|
+
- \`APP_SOURCE_DIR\`
|
|
258
|
+
- \`APP_WEB_EXPORT_DIR\`
|
|
259
|
+
- \`APP_IMAGE\`
|
|
260
|
+
- \`APP_IMAGE_SYNC_STRATEGY\`
|
|
261
|
+
- \`APP_IMAGE_CLEANUP_ON_DOWN\`
|
|
262
|
+
- \`APP_IMAGE_CLEANUP_MINIKUBE\`
|
|
263
|
+
- \`APP_IMAGE_CLEANUP_DOCKER\`
|
|
264
|
+
- \`AUTH_RUNTIME_MODE\`
|
|
265
|
+
- \`APP_PORT_FORWARD_LOCAL_PORT\`
|
|
266
|
+
- \`APP_PORT_FORWARD_REMOTE_PORT\`
|
|
267
|
+
- \`APP_REPLICAS\`
|
|
268
|
+
- \`APP_FORCE_ROLLOUT_RESTART\`
|
|
269
|
+
- \`SUPABASE_PROJECT_DIR\` (optional supabase CLI project path)
|
|
270
|
+
- \`SUPABASE_LOCAL_PORT_BASE\` (generated default \`${supabaseLocalPorts.base}\`)
|
|
271
|
+
- \`SUPABASE_LOCAL_API_PORT\` (generated default \`${supabaseLocalPorts.api}\`)
|
|
272
|
+
- \`SUPABASE_LOCAL_DB_PORT\` (generated default \`${supabaseLocalPorts.db}\`)
|
|
273
|
+
- \`SUPABASE_LOCAL_STUDIO_PORT\` (generated default \`${supabaseLocalPorts.studio}\`)
|
|
274
|
+
- \`SUPABASE_LOCAL_INBUCKET_PORT\` (generated default \`${supabaseLocalPorts.inbucket}\`)
|
|
275
|
+
- \`SUPABASE_LOCAL_ANALYTICS_PORT\` (generated default \`${supabaseLocalPorts.analytics}\`)
|
|
276
|
+
- \`SUPABASE_LOCAL_SHADOW_PORT\` (generated default \`${supabaseLocalPorts.shadow}\`)
|
|
277
|
+
- \`APP_IMAGE_PULL_SECRET_NAME\` + credentials (optional, private registry)
|
|
278
|
+
- \`SUPABASE_SECRET_SYNC_ENABLED\`
|
|
279
|
+
|
|
280
|
+
Generated app Supabase local defaults:
|
|
281
|
+
|
|
282
|
+
- Port base: \`${supabaseLocalPorts.base}\`
|
|
283
|
+
- API: \`http://127.0.0.1:${supabaseLocalPorts.api}\`
|
|
284
|
+
- DB: \`127.0.0.1:${supabaseLocalPorts.db}\`
|
|
285
|
+
- Studio: \`http://127.0.0.1:${supabaseLocalPorts.studio}\`
|
|
286
|
+
- Inbucket: \`http://127.0.0.1:${supabaseLocalPorts.inbucket}\`
|
|
287
|
+
- Analytics: \`http://127.0.0.1:${supabaseLocalPorts.analytics}\`
|
|
288
|
+
- Shadow DB: \`127.0.0.1:${supabaseLocalPorts.shadow}\`
|
|
289
|
+
|
|
290
|
+
Override with:
|
|
291
|
+
|
|
292
|
+
- \`SUPABASE_LOCAL_PORT_BASE\`
|
|
293
|
+
- \`SUPABASE_LOCAL_API_PORT\`
|
|
294
|
+
- \`SUPABASE_LOCAL_DB_PORT\`
|
|
295
|
+
- \`SUPABASE_LOCAL_STUDIO_PORT\`
|
|
296
|
+
- \`SUPABASE_LOCAL_INBUCKET_PORT\`
|
|
297
|
+
- \`SUPABASE_LOCAL_ANALYTICS_PORT\`
|
|
298
|
+
- \`SUPABASE_LOCAL_SHADOW_PORT\`
|
|
299
|
+
|
|
300
|
+
Build flow:
|
|
301
|
+
|
|
302
|
+
- \`./scripts/up.sh\` calls \`./scripts/build-app-image.sh\` when \`APP_BUILD_ENABLED=true\`.
|
|
303
|
+
- \`./scripts/supabase-local-env.sh\` can populate Supabase keys in \`.env\` from local \`supabase status -o env\`.
|
|
304
|
+
- \`./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.
|
|
305
|
+
- With \`AUTH_RUNTIME_MODE=local\` (default), \`up.sh\` auto-runs \`supabase-local-env.sh\` when Supabase keys are missing.
|
|
306
|
+
- \`supabase-local-env.sh\` checks configured host ports before starting a missing local Supabase stack.
|
|
307
|
+
- \`build-app-image.sh\` runs \`bunx expo export --platform web\` from app source.
|
|
308
|
+
- \`app-image/Dockerfile\` builds an nginx image from exported web artifacts.
|
|
309
|
+
- \`build-app-image.sh\` labels the generated Docker image with Ankhorage metadata for diagnostics.
|
|
310
|
+
- \`up.sh\` syncs runtime image by strategy:
|
|
311
|
+
- \`APP_IMAGE_SYNC_STRATEGY=docker-load\` (default): load prebuilt local Docker image into Minikube.
|
|
312
|
+
- \`APP_IMAGE_SYNC_STRATEGY=minikube-build\`: build directly into Minikube image store.
|
|
313
|
+
- \`APP_IMAGE_SYNC_STRATEGY=none\`: skip local image sync.
|
|
314
|
+
- \`up.sh\` applies image/replica changes and (by default) restarts \`deployment/app-runtime\`.
|
|
315
|
+
- If Supabase keys are provided, \`up.sh\` syncs \`Secret/supabase-auth-secrets\` from env.
|
|
316
|
+
|
|
317
|
+
Default generated app image tag: \`${defaultAppImage}\`.
|
|
318
|
+
|
|
319
|
+
## Notes
|
|
320
|
+
|
|
321
|
+
- Auth/database/authorization resources are generated in adapter modules.
|
|
322
|
+
- Cerbos route/screen policies derive from app navigator + authFlow intent when available.
|
|
323
|
+
- Re-generate infra by saving the manifest or calling the bridge infra endpoint.
|
|
324
|
+
`;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function getEnvExample(args: {
|
|
328
|
+
namespace: string;
|
|
329
|
+
domain: string;
|
|
330
|
+
extraEnvEntries: string[];
|
|
331
|
+
defaultAppImage: string;
|
|
332
|
+
supabaseLocalPorts: SupabaseLocalPorts;
|
|
333
|
+
}): string {
|
|
334
|
+
const { namespace, domain, extraEnvEntries, defaultAppImage, supabaseLocalPorts } = args;
|
|
335
|
+
|
|
336
|
+
const baseEntries = [
|
|
337
|
+
'# Minikube runtime configuration',
|
|
338
|
+
`ANKH_NAMESPACE=${namespace}`,
|
|
339
|
+
'MINIKUBE_PROFILE=minikube',
|
|
340
|
+
'MINIKUBE_DRIVER=docker',
|
|
341
|
+
'APP_BUILD_ENABLED=true',
|
|
342
|
+
'APP_SOURCE_DIR=',
|
|
343
|
+
'APP_WEB_EXPORT_DIR=.ankh/web-export',
|
|
344
|
+
'SUPABASE_PROJECT_DIR=',
|
|
345
|
+
`SUPABASE_LOCAL_PORT_BASE=${supabaseLocalPorts.base}`,
|
|
346
|
+
`SUPABASE_LOCAL_SHADOW_PORT=${supabaseLocalPorts.shadow}`,
|
|
347
|
+
`SUPABASE_LOCAL_API_PORT=${supabaseLocalPorts.api}`,
|
|
348
|
+
`SUPABASE_LOCAL_DB_PORT=${supabaseLocalPorts.db}`,
|
|
349
|
+
`SUPABASE_LOCAL_STUDIO_PORT=${supabaseLocalPorts.studio}`,
|
|
350
|
+
`SUPABASE_LOCAL_INBUCKET_PORT=${supabaseLocalPorts.inbucket}`,
|
|
351
|
+
`SUPABASE_LOCAL_ANALYTICS_PORT=${supabaseLocalPorts.analytics}`,
|
|
352
|
+
`APP_IMAGE=${defaultAppImage}`,
|
|
353
|
+
'APP_IMAGE_SYNC_STRATEGY=docker-load',
|
|
354
|
+
'APP_IMAGE_CLEANUP_ON_DOWN=true',
|
|
355
|
+
'APP_IMAGE_CLEANUP_MINIKUBE=true',
|
|
356
|
+
'APP_IMAGE_CLEANUP_DOCKER=true',
|
|
357
|
+
'AUTH_RUNTIME_MODE=local',
|
|
358
|
+
'APP_PORT_FORWARD_LOCAL_PORT=18080',
|
|
359
|
+
'APP_PORT_FORWARD_REMOTE_PORT=80',
|
|
360
|
+
'APP_REPLICAS=1',
|
|
361
|
+
'APP_FORCE_ROLLOUT_RESTART=true',
|
|
362
|
+
'',
|
|
363
|
+
'# Optional private registry auth for APP_IMAGE',
|
|
364
|
+
'APP_IMAGE_PULL_SECRET_NAME=',
|
|
365
|
+
'APP_IMAGE_PULL_SECRET_SERVER=ghcr.io',
|
|
366
|
+
'APP_IMAGE_PULL_SECRET_USERNAME=',
|
|
367
|
+
'APP_IMAGE_PULL_SECRET_PASSWORD=',
|
|
368
|
+
'APP_IMAGE_PULL_SECRET_EMAIL=',
|
|
369
|
+
'',
|
|
370
|
+
'# Optional app networking domain',
|
|
371
|
+
`APP_DOMAIN=${domain}`,
|
|
372
|
+
'',
|
|
373
|
+
'# Adapter-provided runtime keys',
|
|
374
|
+
];
|
|
375
|
+
|
|
376
|
+
const merged = [...baseEntries, ...extraEnvEntries];
|
|
377
|
+
const unique: string[] = [];
|
|
378
|
+
for (const entry of merged) {
|
|
379
|
+
if (!unique.includes(entry)) unique.push(entry);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return `${unique.join('\n')}\n`;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function getNamespaceManifest(namespace: string): string {
|
|
386
|
+
return `apiVersion: v1
|
|
387
|
+
kind: Namespace
|
|
388
|
+
metadata:
|
|
389
|
+
name: ${namespace}
|
|
390
|
+
labels:
|
|
391
|
+
app.kubernetes.io/part-of: ankhorage
|
|
392
|
+
`;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function getAppConfigMap(args: {
|
|
396
|
+
namespace: string;
|
|
397
|
+
authScope: string;
|
|
398
|
+
authProvider: string;
|
|
399
|
+
databaseProvider: string;
|
|
400
|
+
storageMetadata: AppInfraStorageMetadata | null;
|
|
401
|
+
monitoringEnabled: boolean;
|
|
402
|
+
domain: string;
|
|
403
|
+
}): string {
|
|
404
|
+
const {
|
|
405
|
+
namespace,
|
|
406
|
+
authScope,
|
|
407
|
+
authProvider,
|
|
408
|
+
databaseProvider,
|
|
409
|
+
storageMetadata,
|
|
410
|
+
monitoringEnabled,
|
|
411
|
+
domain,
|
|
412
|
+
} = args;
|
|
413
|
+
|
|
414
|
+
const storageLines = storageMetadata
|
|
415
|
+
? ` STORAGE_PROVIDER: "${storageMetadata.provider}"
|
|
416
|
+
STORAGE_BUCKETS: "${escapeYamlDoubleQuoted(storageMetadata.bucketsCsv)}"
|
|
417
|
+
STORAGE_DEFAULT_BUCKET: "${escapeYamlDoubleQuoted(storageMetadata.defaultBucket)}"
|
|
418
|
+
`
|
|
419
|
+
: '';
|
|
420
|
+
|
|
421
|
+
return `apiVersion: v1
|
|
422
|
+
kind: ConfigMap
|
|
423
|
+
metadata:
|
|
424
|
+
name: app-infra-config
|
|
425
|
+
namespace: ${namespace}
|
|
426
|
+
data:
|
|
427
|
+
DEPLOYMENT_TARGET: "minikube"
|
|
428
|
+
MONITORING_ENABLED: "${monitoringEnabled ? 'true' : 'false'}"
|
|
429
|
+
AUTH_SCOPE: "${authScope}"
|
|
430
|
+
AUTH_PROVIDER: "${authProvider}"
|
|
431
|
+
DATABASE_PROVIDER: "${databaseProvider}"
|
|
432
|
+
${storageLines} NETWORK_DOMAIN: "${domain}"
|
|
433
|
+
`;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function resolveAppInfraStorageMetadata(
|
|
437
|
+
manifest: InfraManifestInput,
|
|
438
|
+
): AppInfraStorageMetadata | null {
|
|
439
|
+
const spec = manifest.storage;
|
|
440
|
+
if (!spec) return null;
|
|
441
|
+
|
|
442
|
+
const buckets = normalizeBuckets(spec.buckets);
|
|
443
|
+
if (buckets.length === 0) return null;
|
|
444
|
+
const [defaultBucket] = buckets;
|
|
445
|
+
if (!defaultBucket) return null;
|
|
446
|
+
|
|
447
|
+
if (spec.provider === 'supabase') {
|
|
448
|
+
return {
|
|
449
|
+
provider: 'supabase',
|
|
450
|
+
bucketsCsv: buckets.join(','),
|
|
451
|
+
defaultBucket,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (spec.provider === 'auto') {
|
|
456
|
+
const shouldResolveToSupabase =
|
|
457
|
+
manifest.database?.provider === 'supabase' || manifest.auth?.provider === 'supabase';
|
|
458
|
+
|
|
459
|
+
if (!shouldResolveToSupabase) return null;
|
|
460
|
+
|
|
461
|
+
return {
|
|
462
|
+
provider: 'supabase',
|
|
463
|
+
bucketsCsv: buckets.join(','),
|
|
464
|
+
defaultBucket,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function normalizeBuckets(rawBuckets: readonly string[]): string[] {
|
|
472
|
+
const normalized: string[] = [];
|
|
473
|
+
const seen = new Set<string>();
|
|
474
|
+
|
|
475
|
+
for (const raw of rawBuckets) {
|
|
476
|
+
const bucket = raw.trim();
|
|
477
|
+
if (!bucket) continue;
|
|
478
|
+
if (seen.has(bucket)) continue;
|
|
479
|
+
seen.add(bucket);
|
|
480
|
+
normalized.push(bucket);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return normalized;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function escapeYamlDoubleQuoted(value: string): string {
|
|
487
|
+
return value.replaceAll('\\', '\\\\').replaceAll('"', '\\"');
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function getKustomizationManifest(extraResources: string[]): string {
|
|
491
|
+
const resources = [
|
|
492
|
+
'namespace.yaml',
|
|
493
|
+
'app.configmap.yaml',
|
|
494
|
+
'app/deployment.yaml',
|
|
495
|
+
'app/service.yaml',
|
|
496
|
+
...extraResources,
|
|
497
|
+
];
|
|
498
|
+
const resourceLines = resources.map((r) => ` - ${r}`).join('\n');
|
|
499
|
+
|
|
500
|
+
return `apiVersion: kustomize.config.k8s.io/v1beta1
|
|
501
|
+
kind: Kustomization
|
|
502
|
+
resources:
|
|
503
|
+
${resourceLines}
|
|
504
|
+
`;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function getAppDeploymentManifest(args: { namespace: string; defaultAppImage: string }): string {
|
|
508
|
+
const { namespace, defaultAppImage } = args;
|
|
509
|
+
|
|
510
|
+
return `apiVersion: apps/v1
|
|
511
|
+
kind: Deployment
|
|
512
|
+
metadata:
|
|
513
|
+
name: app-runtime
|
|
514
|
+
namespace: ${namespace}
|
|
515
|
+
labels:
|
|
516
|
+
app.kubernetes.io/name: app-runtime
|
|
517
|
+
spec:
|
|
518
|
+
replicas: 1
|
|
519
|
+
selector:
|
|
520
|
+
matchLabels:
|
|
521
|
+
app.kubernetes.io/name: app-runtime
|
|
522
|
+
template:
|
|
523
|
+
metadata:
|
|
524
|
+
labels:
|
|
525
|
+
app.kubernetes.io/name: app-runtime
|
|
526
|
+
spec:
|
|
527
|
+
containers:
|
|
528
|
+
- name: app
|
|
529
|
+
image: ${defaultAppImage}
|
|
530
|
+
imagePullPolicy: IfNotPresent
|
|
531
|
+
ports:
|
|
532
|
+
- containerPort: 8080
|
|
533
|
+
name: http
|
|
534
|
+
envFrom:
|
|
535
|
+
- configMapRef:
|
|
536
|
+
name: app-infra-config
|
|
537
|
+
- configMapRef:
|
|
538
|
+
name: app-runtime-auth-env
|
|
539
|
+
optional: true
|
|
540
|
+
- configMapRef:
|
|
541
|
+
name: app-runtime-storage-env
|
|
542
|
+
optional: true
|
|
543
|
+
- secretRef:
|
|
544
|
+
name: supabase-auth-secrets
|
|
545
|
+
optional: true
|
|
546
|
+
readinessProbe:
|
|
547
|
+
httpGet:
|
|
548
|
+
path: /
|
|
549
|
+
port: http
|
|
550
|
+
initialDelaySeconds: 5
|
|
551
|
+
periodSeconds: 10
|
|
552
|
+
livenessProbe:
|
|
553
|
+
httpGet:
|
|
554
|
+
path: /
|
|
555
|
+
port: http
|
|
556
|
+
initialDelaySeconds: 15
|
|
557
|
+
periodSeconds: 20
|
|
558
|
+
`;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function getAppServiceManifest(namespace: string): string {
|
|
562
|
+
return `apiVersion: v1
|
|
563
|
+
kind: Service
|
|
564
|
+
metadata:
|
|
565
|
+
name: app-runtime
|
|
566
|
+
namespace: ${namespace}
|
|
567
|
+
labels:
|
|
568
|
+
app.kubernetes.io/name: app-runtime
|
|
569
|
+
spec:
|
|
570
|
+
type: ClusterIP
|
|
571
|
+
selector:
|
|
572
|
+
app.kubernetes.io/name: app-runtime
|
|
573
|
+
ports:
|
|
574
|
+
- name: http
|
|
575
|
+
port: 80
|
|
576
|
+
targetPort: http
|
|
577
|
+
`;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function getUpScript(args: { defaultNamespace: string; defaultAppImage: string }): string {
|
|
581
|
+
const { defaultNamespace, defaultAppImage } = args;
|
|
582
|
+
|
|
583
|
+
return `#!/usr/bin/env bash
|
|
584
|
+
set -euo pipefail
|
|
585
|
+
|
|
586
|
+
SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
|
|
587
|
+
ROOT_DIR="$(cd "\${SCRIPT_DIR}/.." && pwd)"
|
|
588
|
+
K8S_DIR="\${ROOT_DIR}/k8s"
|
|
589
|
+
BUILD_SCRIPT="\${SCRIPT_DIR}/build-app-image.sh"
|
|
590
|
+
|
|
591
|
+
if [[ -f "\${ROOT_DIR}/.env" ]]; then
|
|
592
|
+
set -a
|
|
593
|
+
# shellcheck disable=SC1090
|
|
594
|
+
source "\${ROOT_DIR}/.env"
|
|
595
|
+
set +a
|
|
596
|
+
fi
|
|
597
|
+
|
|
598
|
+
PROFILE="\${MINIKUBE_PROFILE:-minikube}"
|
|
599
|
+
DRIVER="\${MINIKUBE_DRIVER:-docker}"
|
|
600
|
+
NAMESPACE="\${ANKH_NAMESPACE:-${defaultNamespace}}"
|
|
601
|
+
APP_BUILD_ENABLED="\${APP_BUILD_ENABLED:-true}"
|
|
602
|
+
APP_SOURCE_DIR="\${APP_SOURCE_DIR:-$(cd "\${ROOT_DIR}/../.." && pwd)}"
|
|
603
|
+
APP_WEB_EXPORT_DIR="\${APP_WEB_EXPORT_DIR:-.ankh/web-export}"
|
|
604
|
+
APP_IMAGE="\${APP_IMAGE:-${defaultAppImage}}"
|
|
605
|
+
APP_IMAGE_SYNC_STRATEGY="\${APP_IMAGE_SYNC_STRATEGY:-docker-load}"
|
|
606
|
+
APP_REPLICAS="\${APP_REPLICAS:-1}"
|
|
607
|
+
APP_FORCE_ROLLOUT_RESTART="\${APP_FORCE_ROLLOUT_RESTART:-true}"
|
|
608
|
+
APP_IMAGE_PULL_SECRET_NAME="\${APP_IMAGE_PULL_SECRET_NAME:-}"
|
|
609
|
+
APP_IMAGE_PULL_SECRET_SERVER="\${APP_IMAGE_PULL_SECRET_SERVER:-ghcr.io}"
|
|
610
|
+
APP_IMAGE_PULL_SECRET_USERNAME="\${APP_IMAGE_PULL_SECRET_USERNAME:-}"
|
|
611
|
+
APP_IMAGE_PULL_SECRET_PASSWORD="\${APP_IMAGE_PULL_SECRET_PASSWORD:-}"
|
|
612
|
+
APP_IMAGE_PULL_SECRET_EMAIL="\${APP_IMAGE_PULL_SECRET_EMAIL:-}"
|
|
613
|
+
AUTH_RUNTIME_MODE="\${AUTH_RUNTIME_MODE:-local}"
|
|
614
|
+
SUPABASE_SECRET_SYNC_ENABLED="\${SUPABASE_SECRET_SYNC_ENABLED:-true}"
|
|
615
|
+
SUPABASE_LOCAL_ENV_SCRIPT="\${SCRIPT_DIR}/supabase-local-env.sh"
|
|
616
|
+
SUPABASE_URL="\${SUPABASE_URL:-}"
|
|
617
|
+
SUPABASE_ANON_KEY="\${SUPABASE_ANON_KEY:-}"
|
|
618
|
+
SUPABASE_SERVICE_ROLE_KEY="\${SUPABASE_SERVICE_ROLE_KEY:-}"
|
|
619
|
+
SUPABASE_JWT_SECRET="\${SUPABASE_JWT_SECRET:-}"
|
|
620
|
+
SUPABASE_PUBLIC_URL="\${EXPO_PUBLIC_SUPABASE_URL:-\${SUPABASE_URL}}"
|
|
621
|
+
SUPABASE_PUBLIC_ANON_KEY="\${EXPO_PUBLIC_SUPABASE_ANON_KEY:-\${SUPABASE_ANON_KEY}}"
|
|
622
|
+
|
|
623
|
+
refresh_supabase_env() {
|
|
624
|
+
SUPABASE_URL="\${SUPABASE_URL:-}"
|
|
625
|
+
SUPABASE_ANON_KEY="\${SUPABASE_ANON_KEY:-}"
|
|
626
|
+
SUPABASE_SERVICE_ROLE_KEY="\${SUPABASE_SERVICE_ROLE_KEY:-}"
|
|
627
|
+
SUPABASE_JWT_SECRET="\${SUPABASE_JWT_SECRET:-}"
|
|
628
|
+
SUPABASE_PUBLIC_URL="\${EXPO_PUBLIC_SUPABASE_URL:-\${SUPABASE_URL}}"
|
|
629
|
+
SUPABASE_PUBLIC_ANON_KEY="\${EXPO_PUBLIC_SUPABASE_ANON_KEY:-\${SUPABASE_ANON_KEY}}"
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
has_required_supabase_env() {
|
|
633
|
+
[[ -n "\${SUPABASE_URL}" && -n "\${SUPABASE_ANON_KEY}" && -n "\${SUPABASE_SERVICE_ROLE_KEY}" && -n "\${SUPABASE_JWT_SECRET}" ]]
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
refresh_supabase_env
|
|
637
|
+
|
|
638
|
+
if [[ "\${AUTH_RUNTIME_MODE}" != "local" && "\${AUTH_RUNTIME_MODE}" != "remote" ]]; then
|
|
639
|
+
echo "Unsupported AUTH_RUNTIME_MODE=\${AUTH_RUNTIME_MODE}. Use local or remote."
|
|
640
|
+
exit 1
|
|
641
|
+
fi
|
|
642
|
+
|
|
643
|
+
if ! command -v minikube >/dev/null 2>&1; then
|
|
644
|
+
echo "minikube is required but not installed."
|
|
645
|
+
exit 1
|
|
646
|
+
fi
|
|
647
|
+
|
|
648
|
+
if ! command -v kubectl >/dev/null 2>&1; then
|
|
649
|
+
echo "kubectl is required but not installed."
|
|
650
|
+
exit 1
|
|
651
|
+
fi
|
|
652
|
+
|
|
653
|
+
HOST_STATUS="$(minikube -p "\${PROFILE}" status --format='{{.Host}}' 2>/dev/null || true)"
|
|
654
|
+
if [[ "\${HOST_STATUS}" != "Running" ]]; then
|
|
655
|
+
minikube start -p "\${PROFILE}" --driver="\${DRIVER}"
|
|
656
|
+
fi
|
|
657
|
+
|
|
658
|
+
if ! has_required_supabase_env && [[ "\${AUTH_RUNTIME_MODE}" == "local" ]]; then
|
|
659
|
+
if [[ ! -x "\${SUPABASE_LOCAL_ENV_SCRIPT}" ]]; then
|
|
660
|
+
echo "Supabase local bootstrap skipped before app build: missing executable \${SUPABASE_LOCAL_ENV_SCRIPT}."
|
|
661
|
+
else
|
|
662
|
+
echo "Supabase keys missing before app build; running \${SUPABASE_LOCAL_ENV_SCRIPT}."
|
|
663
|
+
if ! "\${SUPABASE_LOCAL_ENV_SCRIPT}"; then
|
|
664
|
+
echo "Supabase local bootstrap failed before app build; continuing."
|
|
665
|
+
fi
|
|
666
|
+
if [[ -f "\${ROOT_DIR}/.env" ]]; then
|
|
667
|
+
set -a
|
|
668
|
+
# shellcheck disable=SC1090
|
|
669
|
+
source "\${ROOT_DIR}/.env"
|
|
670
|
+
set +a
|
|
671
|
+
fi
|
|
672
|
+
refresh_supabase_env
|
|
673
|
+
fi
|
|
674
|
+
fi
|
|
675
|
+
|
|
676
|
+
if [[ "\${APP_BUILD_ENABLED}" == "true" ]]; then
|
|
677
|
+
if [[ ! -x "\${BUILD_SCRIPT}" ]]; then
|
|
678
|
+
echo "Missing build helper script: \${BUILD_SCRIPT}"
|
|
679
|
+
exit 1
|
|
680
|
+
fi
|
|
681
|
+
|
|
682
|
+
"\${BUILD_SCRIPT}"
|
|
683
|
+
fi
|
|
684
|
+
|
|
685
|
+
if [[ "\${APP_WEB_EXPORT_DIR}" = /* ]]; then
|
|
686
|
+
EXPORT_DIR="\${APP_WEB_EXPORT_DIR}"
|
|
687
|
+
else
|
|
688
|
+
EXPORT_DIR="\${APP_SOURCE_DIR}/\${APP_WEB_EXPORT_DIR}"
|
|
689
|
+
fi
|
|
690
|
+
|
|
691
|
+
docker_load_image_to_minikube() {
|
|
692
|
+
if command -v docker >/dev/null 2>&1 && docker image inspect "\${APP_IMAGE}" >/dev/null 2>&1; then
|
|
693
|
+
minikube -p "\${PROFILE}" image load --daemon=true --overwrite=true "\${APP_IMAGE}" >/dev/null
|
|
694
|
+
return 0
|
|
695
|
+
fi
|
|
696
|
+
|
|
697
|
+
return 1
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
case "\${APP_IMAGE_SYNC_STRATEGY}" in
|
|
701
|
+
minikube-build)
|
|
702
|
+
if [[ ! -f "\${EXPORT_DIR}/index.html" ]]; then
|
|
703
|
+
echo "Expected web export output not found at \${EXPORT_DIR}/index.html"
|
|
704
|
+
echo "Run build-app-image.sh first or switch APP_IMAGE_SYNC_STRATEGY=docker-load."
|
|
705
|
+
exit 1
|
|
706
|
+
fi
|
|
707
|
+
|
|
708
|
+
if minikube -p "\${PROFILE}" image build -t "\${APP_IMAGE}" -f "\${ROOT_DIR}/app-image/Dockerfile" "\${EXPORT_DIR}" >/dev/null; then
|
|
709
|
+
if ! minikube -p "\${PROFILE}" image ls 2>/dev/null | grep -Fq "\${APP_IMAGE}"; then
|
|
710
|
+
echo "minikube image build did not register \${APP_IMAGE}; falling back to docker-load."
|
|
711
|
+
if ! docker_load_image_to_minikube; then
|
|
712
|
+
echo "Fallback docker-load failed: local Docker image \${APP_IMAGE} not found."
|
|
713
|
+
exit 1
|
|
714
|
+
fi
|
|
715
|
+
fi
|
|
716
|
+
else
|
|
717
|
+
echo "minikube image build failed for \${APP_IMAGE}; falling back to docker-load."
|
|
718
|
+
if ! docker_load_image_to_minikube; then
|
|
719
|
+
echo "Fallback docker-load failed: local Docker image \${APP_IMAGE} not found."
|
|
720
|
+
exit 1
|
|
721
|
+
fi
|
|
722
|
+
fi
|
|
723
|
+
;;
|
|
724
|
+
docker-load)
|
|
725
|
+
if ! docker_load_image_to_minikube; then
|
|
726
|
+
echo "Skipping docker-load image sync; local Docker image \${APP_IMAGE} not found."
|
|
727
|
+
fi
|
|
728
|
+
;;
|
|
729
|
+
none)
|
|
730
|
+
echo "Skipping local image sync (APP_IMAGE_SYNC_STRATEGY=none)."
|
|
731
|
+
;;
|
|
732
|
+
*)
|
|
733
|
+
echo "Unsupported APP_IMAGE_SYNC_STRATEGY=\${APP_IMAGE_SYNC_STRATEGY}. Use minikube-build, docker-load, or none."
|
|
734
|
+
exit 1
|
|
735
|
+
;;
|
|
736
|
+
esac
|
|
737
|
+
|
|
738
|
+
kubectl apply -k "\${K8S_DIR}"
|
|
739
|
+
|
|
740
|
+
if [[ "\${SUPABASE_SECRET_SYNC_ENABLED}" == "true" ]]; then
|
|
741
|
+
if ! has_required_supabase_env && [[ "\${AUTH_RUNTIME_MODE}" == "local" ]]; then
|
|
742
|
+
if [[ ! -x "\${SUPABASE_LOCAL_ENV_SCRIPT}" ]]; then
|
|
743
|
+
echo "Supabase local bootstrap skipped: missing executable \${SUPABASE_LOCAL_ENV_SCRIPT}."
|
|
744
|
+
else
|
|
745
|
+
echo "Supabase keys missing; running \${SUPABASE_LOCAL_ENV_SCRIPT}."
|
|
746
|
+
if ! "\${SUPABASE_LOCAL_ENV_SCRIPT}"; then
|
|
747
|
+
echo "Supabase local bootstrap failed; continuing without local credential import."
|
|
748
|
+
fi
|
|
749
|
+
if [[ -f "\${ROOT_DIR}/.env" ]]; then
|
|
750
|
+
set -a
|
|
751
|
+
# shellcheck disable=SC1090
|
|
752
|
+
source "\${ROOT_DIR}/.env"
|
|
753
|
+
set +a
|
|
754
|
+
fi
|
|
755
|
+
refresh_supabase_env
|
|
756
|
+
fi
|
|
757
|
+
fi
|
|
758
|
+
|
|
759
|
+
if ! has_required_supabase_env; then
|
|
760
|
+
if [[ "\${AUTH_RUNTIME_MODE}" == "local" ]]; then
|
|
761
|
+
echo "Supabase secret sync skipped: local credentials still missing after bootstrap."
|
|
762
|
+
echo "Run ./scripts/supabase-local-env.sh and retry."
|
|
763
|
+
else
|
|
764
|
+
echo "Supabase secret sync skipped: set SUPABASE_URL, SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY, and SUPABASE_JWT_SECRET in .env."
|
|
765
|
+
fi
|
|
766
|
+
else
|
|
767
|
+
TMP_SUPABASE_ENV_FILE="$(mktemp)"
|
|
768
|
+
trap 'rm -f "\${TMP_SUPABASE_ENV_FILE}"' EXIT
|
|
769
|
+
|
|
770
|
+
cat > "\${TMP_SUPABASE_ENV_FILE}" <<EOF
|
|
771
|
+
SUPABASE_URL=\${SUPABASE_URL}
|
|
772
|
+
SUPABASE_ANON_KEY=\${SUPABASE_ANON_KEY}
|
|
773
|
+
SUPABASE_SERVICE_ROLE_KEY=\${SUPABASE_SERVICE_ROLE_KEY}
|
|
774
|
+
SUPABASE_JWT_SECRET=\${SUPABASE_JWT_SECRET}
|
|
775
|
+
EXPO_PUBLIC_SUPABASE_URL=\${SUPABASE_PUBLIC_URL}
|
|
776
|
+
EXPO_PUBLIC_SUPABASE_ANON_KEY=\${SUPABASE_PUBLIC_ANON_KEY}
|
|
777
|
+
EOF
|
|
778
|
+
|
|
779
|
+
kubectl -n "\${NAMESPACE}" create secret generic supabase-auth-secrets --from-env-file="\${TMP_SUPABASE_ENV_FILE}" --dry-run=client -o yaml | kubectl -n "\${NAMESPACE}" apply -f -
|
|
780
|
+
|
|
781
|
+
rm -f "\${TMP_SUPABASE_ENV_FILE}"
|
|
782
|
+
trap - EXIT
|
|
783
|
+
echo "Synchronized supabase-auth-secrets from environment values."
|
|
784
|
+
fi
|
|
785
|
+
fi
|
|
786
|
+
|
|
787
|
+
if [[ -n "\${APP_IMAGE_PULL_SECRET_NAME}" ]]; then
|
|
788
|
+
if [[ -z "\${APP_IMAGE_PULL_SECRET_USERNAME}" || -z "\${APP_IMAGE_PULL_SECRET_PASSWORD}" ]]; then
|
|
789
|
+
echo "APP_IMAGE_PULL_SECRET_NAME is set, but username/password are missing."
|
|
790
|
+
exit 1
|
|
791
|
+
fi
|
|
792
|
+
|
|
793
|
+
SECRET_EMAIL_ARG=()
|
|
794
|
+
if [[ -n "\${APP_IMAGE_PULL_SECRET_EMAIL}" ]]; then
|
|
795
|
+
SECRET_EMAIL_ARG=(--docker-email="\${APP_IMAGE_PULL_SECRET_EMAIL}")
|
|
796
|
+
fi
|
|
797
|
+
|
|
798
|
+
kubectl -n "\${NAMESPACE}" create secret docker-registry "\${APP_IMAGE_PULL_SECRET_NAME}" --docker-server="\${APP_IMAGE_PULL_SECRET_SERVER}" --docker-username="\${APP_IMAGE_PULL_SECRET_USERNAME}" --docker-password="\${APP_IMAGE_PULL_SECRET_PASSWORD}" "\${SECRET_EMAIL_ARG[@]}" --dry-run=client -o yaml | kubectl -n "\${NAMESPACE}" apply -f -
|
|
799
|
+
|
|
800
|
+
PULL_SECRET_PATCH="$(cat <<EOF
|
|
801
|
+
{"spec":{"template":{"spec":{"imagePullSecrets":[{"name":"\${APP_IMAGE_PULL_SECRET_NAME}"}]}}}}
|
|
802
|
+
EOF
|
|
803
|
+
)"
|
|
804
|
+
kubectl -n "\${NAMESPACE}" patch deployment app-runtime --type=merge --patch "\${PULL_SECRET_PATCH}" >/dev/null
|
|
805
|
+
else
|
|
806
|
+
kubectl -n "\${NAMESPACE}" patch deployment app-runtime --type=json --patch='[{"op":"remove","path":"/spec/template/spec/imagePullSecrets"}]' >/dev/null 2>&1 || true
|
|
807
|
+
fi
|
|
808
|
+
|
|
809
|
+
kubectl -n "\${NAMESPACE}" set image deployment/app-runtime app="\${APP_IMAGE}" >/dev/null
|
|
810
|
+
kubectl -n "\${NAMESPACE}" scale deployment/app-runtime --replicas="\${APP_REPLICAS}" >/dev/null
|
|
811
|
+
|
|
812
|
+
if [[ "\${APP_FORCE_ROLLOUT_RESTART}" == "true" ]]; then
|
|
813
|
+
kubectl -n "\${NAMESPACE}" rollout restart deployment/app-runtime >/dev/null
|
|
814
|
+
fi
|
|
815
|
+
|
|
816
|
+
kubectl -n "\${NAMESPACE}" rollout status deployment/app-runtime --timeout=180s >/dev/null
|
|
817
|
+
echo "Minikube infrastructure applied."
|
|
818
|
+
`;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function getBuildAppImageScript(args: {
|
|
822
|
+
defaultNamespace: string;
|
|
823
|
+
defaultAppImage: string;
|
|
824
|
+
}): string {
|
|
825
|
+
const { defaultNamespace, defaultAppImage } = args;
|
|
826
|
+
|
|
827
|
+
return `#!/usr/bin/env bash
|
|
828
|
+
set -euo pipefail
|
|
829
|
+
|
|
830
|
+
SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
|
|
831
|
+
ROOT_DIR="$(cd "\${SCRIPT_DIR}/.." && pwd)"
|
|
832
|
+
DOCKERFILE_PATH="\${ROOT_DIR}/app-image/Dockerfile"
|
|
833
|
+
|
|
834
|
+
if [[ -f "\${ROOT_DIR}/.env" ]]; then
|
|
835
|
+
set -a
|
|
836
|
+
# shellcheck disable=SC1090
|
|
837
|
+
source "\${ROOT_DIR}/.env"
|
|
838
|
+
set +a
|
|
839
|
+
fi
|
|
840
|
+
|
|
841
|
+
NAMESPACE="\${ANKH_NAMESPACE:-${defaultNamespace}}"
|
|
842
|
+
APP_BUILD_ENABLED="\${APP_BUILD_ENABLED:-true}"
|
|
843
|
+
APP_SOURCE_DIR="\${APP_SOURCE_DIR:-$(cd "\${ROOT_DIR}/../.." && pwd)}"
|
|
844
|
+
APP_WEB_EXPORT_DIR="\${APP_WEB_EXPORT_DIR:-.ankh/web-export}"
|
|
845
|
+
APP_IMAGE="\${APP_IMAGE:-${defaultAppImage}}"
|
|
846
|
+
|
|
847
|
+
if [[ "\${APP_BUILD_ENABLED}" != "true" ]]; then
|
|
848
|
+
echo "Skipping app image build (APP_BUILD_ENABLED=\${APP_BUILD_ENABLED})."
|
|
849
|
+
exit 0
|
|
850
|
+
fi
|
|
851
|
+
|
|
852
|
+
if ! command -v docker >/dev/null 2>&1; then
|
|
853
|
+
echo "docker is required to build app image."
|
|
854
|
+
exit 1
|
|
855
|
+
fi
|
|
856
|
+
|
|
857
|
+
if ! command -v bunx >/dev/null 2>&1; then
|
|
858
|
+
echo "bunx is required to export the app for container image build."
|
|
859
|
+
exit 1
|
|
860
|
+
fi
|
|
861
|
+
|
|
862
|
+
if [[ ! -f "\${APP_SOURCE_DIR}/package.json" ]]; then
|
|
863
|
+
echo "APP_SOURCE_DIR does not look like an app root: \${APP_SOURCE_DIR}"
|
|
864
|
+
exit 1
|
|
865
|
+
fi
|
|
866
|
+
|
|
867
|
+
if [[ ! -f "\${DOCKERFILE_PATH}" ]]; then
|
|
868
|
+
echo "Dockerfile for app image build is missing: \${DOCKERFILE_PATH}"
|
|
869
|
+
exit 1
|
|
870
|
+
fi
|
|
871
|
+
|
|
872
|
+
(
|
|
873
|
+
cd "\${APP_SOURCE_DIR}"
|
|
874
|
+
bunx expo export --platform web --output-dir "\${APP_WEB_EXPORT_DIR}"
|
|
875
|
+
)
|
|
876
|
+
|
|
877
|
+
if [[ "\${APP_WEB_EXPORT_DIR}" = /* ]]; then
|
|
878
|
+
EXPORT_DIR="\${APP_WEB_EXPORT_DIR}"
|
|
879
|
+
else
|
|
880
|
+
EXPORT_DIR="\${APP_SOURCE_DIR}/\${APP_WEB_EXPORT_DIR}"
|
|
881
|
+
fi
|
|
882
|
+
|
|
883
|
+
if [[ ! -f "\${EXPORT_DIR}/index.html" ]]; then
|
|
884
|
+
echo "Expected web export output not found at \${EXPORT_DIR}/index.html"
|
|
885
|
+
exit 1
|
|
886
|
+
fi
|
|
887
|
+
|
|
888
|
+
docker build -t "\${APP_IMAGE}" \
|
|
889
|
+
--label "ankhorage.kind=generated-app" \
|
|
890
|
+
--label "ankhorage.namespace=\${NAMESPACE}" \
|
|
891
|
+
--label "ankhorage.image=\${APP_IMAGE}" \
|
|
892
|
+
-f "\${DOCKERFILE_PATH}" \
|
|
893
|
+
"\${EXPORT_DIR}"
|
|
894
|
+
echo "Built app image: \${APP_IMAGE} (namespace: \${NAMESPACE})"
|
|
895
|
+
`;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function getAppImageDockerfile(): string {
|
|
899
|
+
return `FROM nginx:1.27-alpine
|
|
900
|
+
|
|
901
|
+
RUN cat <<'EOF' > /etc/nginx/conf.d/default.conf
|
|
902
|
+
server {
|
|
903
|
+
listen 8080;
|
|
904
|
+
server_name _;
|
|
905
|
+
root /usr/share/nginx/html;
|
|
906
|
+
index index.html;
|
|
907
|
+
|
|
908
|
+
location / {
|
|
909
|
+
try_files $uri $uri/ /index.html;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
EOF
|
|
913
|
+
|
|
914
|
+
COPY . /usr/share/nginx/html
|
|
915
|
+
|
|
916
|
+
EXPOSE 8080
|
|
917
|
+
|
|
918
|
+
CMD ["nginx", "-g", "daemon off;"]
|
|
919
|
+
`;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
function getPortForwardScript(defaultNamespace: string): string {
|
|
923
|
+
return `#!/usr/bin/env bash
|
|
924
|
+
set -euo pipefail
|
|
925
|
+
|
|
926
|
+
SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
|
|
927
|
+
ROOT_DIR="$(cd "\${SCRIPT_DIR}/.." && pwd)"
|
|
928
|
+
|
|
929
|
+
if [[ -f "\${ROOT_DIR}/.env" ]]; then
|
|
930
|
+
set -a
|
|
931
|
+
# shellcheck disable=SC1090
|
|
932
|
+
source "\${ROOT_DIR}/.env"
|
|
933
|
+
set +a
|
|
934
|
+
fi
|
|
935
|
+
|
|
936
|
+
NAMESPACE="\${ANKH_NAMESPACE:-${defaultNamespace}}"
|
|
937
|
+
LOCAL_PORT="\${APP_PORT_FORWARD_LOCAL_PORT:-18080}"
|
|
938
|
+
REMOTE_PORT="\${APP_PORT_FORWARD_REMOTE_PORT:-80}"
|
|
939
|
+
|
|
940
|
+
if ! command -v kubectl >/dev/null 2>&1; then
|
|
941
|
+
echo "kubectl is required but not installed."
|
|
942
|
+
exit 1
|
|
943
|
+
fi
|
|
944
|
+
|
|
945
|
+
if ! kubectl get namespace "\${NAMESPACE}" >/dev/null 2>&1; then
|
|
946
|
+
echo "Namespace '\${NAMESPACE}' was not found."
|
|
947
|
+
echo "Run ./scripts/up.sh to create infrastructure first."
|
|
948
|
+
exit 1
|
|
949
|
+
fi
|
|
950
|
+
|
|
951
|
+
if ! kubectl -n "\${NAMESPACE}" get service app-runtime >/dev/null 2>&1; then
|
|
952
|
+
echo "Service 'app-runtime' was not found in namespace '\${NAMESPACE}'."
|
|
953
|
+
echo "Run ./scripts/up.sh to apply runtime resources."
|
|
954
|
+
exit 1
|
|
955
|
+
fi
|
|
956
|
+
|
|
957
|
+
echo "Forwarding app-runtime from namespace '\${NAMESPACE}' to http://127.0.0.1:\${LOCAL_PORT}"
|
|
958
|
+
echo "Press Ctrl+C to stop."
|
|
959
|
+
kubectl -n "\${NAMESPACE}" port-forward service/app-runtime "\${LOCAL_PORT}:\${REMOTE_PORT}"
|
|
960
|
+
`;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function getDownScript(): string {
|
|
964
|
+
return `#!/usr/bin/env bash
|
|
965
|
+
set -euo pipefail
|
|
966
|
+
|
|
967
|
+
SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
|
|
968
|
+
ROOT_DIR="$(cd "\${SCRIPT_DIR}/.." && pwd)"
|
|
969
|
+
K8S_DIR="\${ROOT_DIR}/k8s"
|
|
970
|
+
|
|
971
|
+
if [[ -f "\${ROOT_DIR}/.env" ]]; then
|
|
972
|
+
set -a
|
|
973
|
+
# shellcheck disable=SC1090
|
|
974
|
+
source "\${ROOT_DIR}/.env"
|
|
975
|
+
set +a
|
|
976
|
+
fi
|
|
977
|
+
|
|
978
|
+
kubectl delete -k "\${K8S_DIR}" --ignore-not-found
|
|
979
|
+
echo "Minikube infrastructure removed."
|
|
980
|
+
`;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function getStatusScript(defaultNamespace: string): string {
|
|
984
|
+
return `#!/usr/bin/env bash
|
|
985
|
+
set -euo pipefail
|
|
986
|
+
|
|
987
|
+
SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
|
|
988
|
+
ROOT_DIR="$(cd "\${SCRIPT_DIR}/.." && pwd)"
|
|
989
|
+
|
|
990
|
+
if [[ -f "\${ROOT_DIR}/.env" ]]; then
|
|
991
|
+
set -a
|
|
992
|
+
# shellcheck disable=SC1090
|
|
993
|
+
source "\${ROOT_DIR}/.env"
|
|
994
|
+
set +a
|
|
995
|
+
fi
|
|
996
|
+
|
|
997
|
+
NAMESPACE="\${ANKH_NAMESPACE:-${defaultNamespace}}"
|
|
998
|
+
AUTH_RUNTIME_MODE="\${AUTH_RUNTIME_MODE:-local}"
|
|
999
|
+
SUPABASE_SECRET_SYNC_ENABLED="\${SUPABASE_SECRET_SYNC_ENABLED:-true}"
|
|
1000
|
+
|
|
1001
|
+
if ! command -v kubectl >/dev/null 2>&1; then
|
|
1002
|
+
echo "kubectl is required but not installed."
|
|
1003
|
+
exit 1
|
|
1004
|
+
fi
|
|
1005
|
+
|
|
1006
|
+
if ! kubectl config current-context >/dev/null 2>&1; then
|
|
1007
|
+
echo "No active kubectl context is configured."
|
|
1008
|
+
echo "Run ./scripts/up.sh to start Minikube and apply manifests."
|
|
1009
|
+
exit 1
|
|
1010
|
+
fi
|
|
1011
|
+
|
|
1012
|
+
if ! kubectl cluster-info >/dev/null 2>&1; then
|
|
1013
|
+
CONTEXT="$(kubectl config current-context 2>/dev/null || echo "unknown")"
|
|
1014
|
+
echo "Cannot reach Kubernetes API server for context '\${CONTEXT}'."
|
|
1015
|
+
echo "Run ./scripts/up.sh or verify your kubeconfig context."
|
|
1016
|
+
exit 1
|
|
1017
|
+
fi
|
|
1018
|
+
|
|
1019
|
+
if ! kubectl get namespace "\${NAMESPACE}" >/dev/null 2>&1; then
|
|
1020
|
+
echo "Namespace '\${NAMESPACE}' was not found in the current cluster."
|
|
1021
|
+
echo "Run ./scripts/up.sh to create it and apply resources."
|
|
1022
|
+
exit 1
|
|
1023
|
+
fi
|
|
1024
|
+
|
|
1025
|
+
kubectl get all -n "\${NAMESPACE}"
|
|
1026
|
+
|
|
1027
|
+
echo
|
|
1028
|
+
echo "Runtime auth checks:"
|
|
1029
|
+
echo "- AUTH_RUNTIME_MODE=\${AUTH_RUNTIME_MODE}"
|
|
1030
|
+
echo "- SUPABASE_SECRET_SYNC_ENABLED=\${SUPABASE_SECRET_SYNC_ENABLED}"
|
|
1031
|
+
|
|
1032
|
+
if kubectl -n "\${NAMESPACE}" get configmap app-runtime-auth-env >/dev/null 2>&1; then
|
|
1033
|
+
echo "- configmap/app-runtime-auth-env: present"
|
|
1034
|
+
else
|
|
1035
|
+
echo "- configmap/app-runtime-auth-env: missing"
|
|
1036
|
+
fi
|
|
1037
|
+
|
|
1038
|
+
if [[ "\${SUPABASE_SECRET_SYNC_ENABLED}" == "true" ]]; then
|
|
1039
|
+
if kubectl -n "\${NAMESPACE}" get secret supabase-auth-secrets >/dev/null 2>&1; then
|
|
1040
|
+
echo "- secret/supabase-auth-secrets: present"
|
|
1041
|
+
|
|
1042
|
+
has_secret_key() {
|
|
1043
|
+
local key="$1"
|
|
1044
|
+
local value
|
|
1045
|
+
value="$(kubectl -n "\${NAMESPACE}" get secret supabase-auth-secrets -o "jsonpath={.data.\${key}}" 2>/dev/null || true)"
|
|
1046
|
+
[[ -n "\${value}" ]]
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
required_secret_keys=(
|
|
1050
|
+
SUPABASE_URL
|
|
1051
|
+
SUPABASE_ANON_KEY
|
|
1052
|
+
SUPABASE_SERVICE_ROLE_KEY
|
|
1053
|
+
SUPABASE_JWT_SECRET
|
|
1054
|
+
EXPO_PUBLIC_SUPABASE_URL
|
|
1055
|
+
EXPO_PUBLIC_SUPABASE_ANON_KEY
|
|
1056
|
+
)
|
|
1057
|
+
|
|
1058
|
+
for key in "\${required_secret_keys[@]}"; do
|
|
1059
|
+
if has_secret_key "\${key}"; then
|
|
1060
|
+
echo " - \${key}: present"
|
|
1061
|
+
else
|
|
1062
|
+
echo " - \${key}: missing"
|
|
1063
|
+
fi
|
|
1064
|
+
done
|
|
1065
|
+
else
|
|
1066
|
+
echo "- secret/supabase-auth-secrets: missing"
|
|
1067
|
+
fi
|
|
1068
|
+
else
|
|
1069
|
+
echo "- secret/supabase-auth-secrets: skipped (SUPABASE_SECRET_SYNC_ENABLED=false)"
|
|
1070
|
+
fi
|
|
1071
|
+
`;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
function getSupabaseLocalEnvScript(supabaseLocalPorts: SupabaseLocalPorts): string {
|
|
1075
|
+
return `#!/usr/bin/env bash
|
|
1076
|
+
set -euo pipefail
|
|
1077
|
+
|
|
1078
|
+
SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
|
|
1079
|
+
ROOT_DIR="$(cd "\${SCRIPT_DIR}/.." && pwd)"
|
|
1080
|
+
ENV_FILE="\${ROOT_DIR}/.env"
|
|
1081
|
+
ENV_EXAMPLE_FILE="\${ROOT_DIR}/.env.example"
|
|
1082
|
+
|
|
1083
|
+
if [[ -f "\${ENV_FILE}" ]]; then
|
|
1084
|
+
set -a
|
|
1085
|
+
# shellcheck disable=SC1090
|
|
1086
|
+
source "\${ENV_FILE}"
|
|
1087
|
+
set +a
|
|
1088
|
+
fi
|
|
1089
|
+
|
|
1090
|
+
APP_SOURCE_DIR="\${APP_SOURCE_DIR:-$(cd "\${ROOT_DIR}/../.." && pwd)}"
|
|
1091
|
+
SUPABASE_PROJECT_DIR="\${SUPABASE_PROJECT_DIR:-\${APP_SOURCE_DIR}}"
|
|
1092
|
+
APP_SUPABASE_ENV_FILE="\${APP_SUPABASE_ENV_FILE:-\${APP_SOURCE_DIR}/.env.local}"
|
|
1093
|
+
|
|
1094
|
+
SUPABASE_LOCAL_PORT_BASE="\${SUPABASE_LOCAL_PORT_BASE:-${supabaseLocalPorts.base}}"
|
|
1095
|
+
SUPABASE_LOCAL_SHADOW_PORT="\${SUPABASE_LOCAL_SHADOW_PORT:-\${SUPABASE_LOCAL_PORT_BASE}}"
|
|
1096
|
+
SUPABASE_LOCAL_API_PORT="\${SUPABASE_LOCAL_API_PORT:-$((SUPABASE_LOCAL_PORT_BASE + 1))}"
|
|
1097
|
+
SUPABASE_LOCAL_DB_PORT="\${SUPABASE_LOCAL_DB_PORT:-$((SUPABASE_LOCAL_PORT_BASE + 2))}"
|
|
1098
|
+
SUPABASE_LOCAL_STUDIO_PORT="\${SUPABASE_LOCAL_STUDIO_PORT:-$((SUPABASE_LOCAL_PORT_BASE + 3))}"
|
|
1099
|
+
SUPABASE_LOCAL_INBUCKET_PORT="\${SUPABASE_LOCAL_INBUCKET_PORT:-$((SUPABASE_LOCAL_PORT_BASE + 4))}"
|
|
1100
|
+
SUPABASE_LOCAL_ANALYTICS_PORT="\${SUPABASE_LOCAL_ANALYTICS_PORT:-$((SUPABASE_LOCAL_PORT_BASE + 5))}"
|
|
1101
|
+
export SUPABASE_LOCAL_PORT_BASE
|
|
1102
|
+
export SUPABASE_LOCAL_SHADOW_PORT
|
|
1103
|
+
export SUPABASE_LOCAL_API_PORT
|
|
1104
|
+
export SUPABASE_LOCAL_DB_PORT
|
|
1105
|
+
export SUPABASE_LOCAL_STUDIO_PORT
|
|
1106
|
+
export SUPABASE_LOCAL_INBUCKET_PORT
|
|
1107
|
+
export SUPABASE_LOCAL_ANALYTICS_PORT
|
|
1108
|
+
if ! command -v supabase >/dev/null 2>&1; then
|
|
1109
|
+
echo "supabase CLI is required but not installed."
|
|
1110
|
+
echo "Install: https://supabase.com/docs/guides/local-development/cli/getting-started"
|
|
1111
|
+
exit 1
|
|
1112
|
+
fi
|
|
1113
|
+
|
|
1114
|
+
configure_supabase_local_ports() {
|
|
1115
|
+
local config_file="\${SUPABASE_PROJECT_DIR}/supabase/config.toml"
|
|
1116
|
+
|
|
1117
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
1118
|
+
echo "python3 is required to patch supabase/config.toml with generated local port defaults."
|
|
1119
|
+
echo "Install python3 (recommended) or manually edit \${config_file} ports:"
|
|
1120
|
+
echo "- [api] port = \${SUPABASE_LOCAL_API_PORT}"
|
|
1121
|
+
echo "- [db] port = \${SUPABASE_LOCAL_DB_PORT}"
|
|
1122
|
+
echo "- [db] shadow_port = \${SUPABASE_LOCAL_SHADOW_PORT}"
|
|
1123
|
+
echo "- [studio] port = \${SUPABASE_LOCAL_STUDIO_PORT}"
|
|
1124
|
+
echo "- [inbucket] port = \${SUPABASE_LOCAL_INBUCKET_PORT}"
|
|
1125
|
+
echo "- [analytics] port = \${SUPABASE_LOCAL_ANALYTICS_PORT}"
|
|
1126
|
+
exit 1
|
|
1127
|
+
fi
|
|
1128
|
+
|
|
1129
|
+
python3 - "\${config_file}" <<'PY'
|
|
1130
|
+
import os
|
|
1131
|
+
import re
|
|
1132
|
+
import sys
|
|
1133
|
+
from dataclasses import dataclass
|
|
1134
|
+
|
|
1135
|
+
|
|
1136
|
+
@dataclass(frozen=True)
|
|
1137
|
+
class Patch:
|
|
1138
|
+
section: str
|
|
1139
|
+
key: str
|
|
1140
|
+
value: str
|
|
1141
|
+
|
|
1142
|
+
|
|
1143
|
+
def _env_required(name):
|
|
1144
|
+
value = os.environ.get(name)
|
|
1145
|
+
if value is None or value == "":
|
|
1146
|
+
raise RuntimeError(f"Missing required env var: {name}")
|
|
1147
|
+
return value
|
|
1148
|
+
|
|
1149
|
+
|
|
1150
|
+
def _is_section_header(line):
|
|
1151
|
+
return re.match(r"^\\s*\\[[^\\]]+\\]\\s*$", line) is not None
|
|
1152
|
+
|
|
1153
|
+
|
|
1154
|
+
def _section_name(line):
|
|
1155
|
+
match = re.match(r"^\\s*\\[([^\\]]+)\\]\\s*$", line)
|
|
1156
|
+
return match.group(1) if match else None
|
|
1157
|
+
|
|
1158
|
+
|
|
1159
|
+
def _patch_section(lines, section, key, value):
|
|
1160
|
+
current_section = None
|
|
1161
|
+
section_start = None
|
|
1162
|
+
|
|
1163
|
+
for i, line in enumerate(lines):
|
|
1164
|
+
if _is_section_header(line):
|
|
1165
|
+
current_section = _section_name(line)
|
|
1166
|
+
if current_section == section and section_start is None:
|
|
1167
|
+
section_start = i
|
|
1168
|
+
|
|
1169
|
+
if section_start is None:
|
|
1170
|
+
if lines and not lines[-1].endswith("\\n"):
|
|
1171
|
+
lines[-1] = lines[-1] + "\\n"
|
|
1172
|
+
if lines and lines[-1].strip() != "":
|
|
1173
|
+
lines.append("\\n")
|
|
1174
|
+
lines.append(f"[{section}]\\n")
|
|
1175
|
+
lines.append(f"{key} = {value}\\n")
|
|
1176
|
+
return lines
|
|
1177
|
+
|
|
1178
|
+
section_end = len(lines)
|
|
1179
|
+
for j in range(section_start + 1, len(lines)):
|
|
1180
|
+
if _is_section_header(lines[j]):
|
|
1181
|
+
section_end = j
|
|
1182
|
+
break
|
|
1183
|
+
|
|
1184
|
+
key_re = re.compile(rf"^(\\s*){re.escape(key)}\\s*=\\s*(.*?)(\\s*(#.*)?)?$")
|
|
1185
|
+
for k in range(section_start + 1, section_end):
|
|
1186
|
+
match = key_re.match(lines[k])
|
|
1187
|
+
if match:
|
|
1188
|
+
indent = match.group(1) or ""
|
|
1189
|
+
trailing = match.group(3) or ""
|
|
1190
|
+
suffix = trailing.rstrip("\\n")
|
|
1191
|
+
lines[k] = f"{indent}{key} = {value}{suffix}\\n"
|
|
1192
|
+
return lines
|
|
1193
|
+
|
|
1194
|
+
insert_at = section_end
|
|
1195
|
+
while insert_at > section_start + 1 and lines[insert_at - 1].strip() == "":
|
|
1196
|
+
insert_at -= 1
|
|
1197
|
+
lines.insert(insert_at, f"{key} = {value}\\n")
|
|
1198
|
+
return lines
|
|
1199
|
+
|
|
1200
|
+
|
|
1201
|
+
def main():
|
|
1202
|
+
if len(sys.argv) != 2:
|
|
1203
|
+
raise RuntimeError("Usage: patch_config.py <supabase/config.toml>")
|
|
1204
|
+
|
|
1205
|
+
config_path = sys.argv[1]
|
|
1206
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
|
1207
|
+
lines = f.readlines()
|
|
1208
|
+
|
|
1209
|
+
patches = [
|
|
1210
|
+
Patch("api", "port", _env_required("SUPABASE_LOCAL_API_PORT")),
|
|
1211
|
+
Patch("db", "port", _env_required("SUPABASE_LOCAL_DB_PORT")),
|
|
1212
|
+
Patch("db", "shadow_port", _env_required("SUPABASE_LOCAL_SHADOW_PORT")),
|
|
1213
|
+
Patch("studio", "port", _env_required("SUPABASE_LOCAL_STUDIO_PORT")),
|
|
1214
|
+
Patch("inbucket", "port", _env_required("SUPABASE_LOCAL_INBUCKET_PORT")),
|
|
1215
|
+
Patch("analytics", "port", _env_required("SUPABASE_LOCAL_ANALYTICS_PORT")),
|
|
1216
|
+
]
|
|
1217
|
+
|
|
1218
|
+
for p in patches:
|
|
1219
|
+
lines = _patch_section(lines, p.section, p.key, p.value)
|
|
1220
|
+
|
|
1221
|
+
with open(config_path, "w", encoding="utf-8") as f:
|
|
1222
|
+
f.writelines(lines)
|
|
1223
|
+
|
|
1224
|
+
return 0
|
|
1225
|
+
|
|
1226
|
+
|
|
1227
|
+
if __name__ == "__main__":
|
|
1228
|
+
raise SystemExit(main())
|
|
1229
|
+
PY
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
assert_supabase_local_ports_available() {
|
|
1233
|
+
local project_label="\${ANKH_NAMESPACE:-$(basename "\${APP_SOURCE_DIR}")}"
|
|
1234
|
+
|
|
1235
|
+
ANKH_PROJECT_LABEL="\${project_label}" python3 - <<'PY'
|
|
1236
|
+
import os
|
|
1237
|
+
import socket
|
|
1238
|
+
import sys
|
|
1239
|
+
|
|
1240
|
+
|
|
1241
|
+
PORT_KEYS = [
|
|
1242
|
+
"SUPABASE_LOCAL_SHADOW_PORT",
|
|
1243
|
+
"SUPABASE_LOCAL_API_PORT",
|
|
1244
|
+
"SUPABASE_LOCAL_DB_PORT",
|
|
1245
|
+
"SUPABASE_LOCAL_STUDIO_PORT",
|
|
1246
|
+
"SUPABASE_LOCAL_INBUCKET_PORT",
|
|
1247
|
+
"SUPABASE_LOCAL_ANALYTICS_PORT",
|
|
1248
|
+
]
|
|
1249
|
+
|
|
1250
|
+
|
|
1251
|
+
def read_port(key):
|
|
1252
|
+
value = os.environ.get(key, "")
|
|
1253
|
+
try:
|
|
1254
|
+
port = int(value)
|
|
1255
|
+
except ValueError:
|
|
1256
|
+
raise RuntimeError(f"{key} must be an integer, got {value!r}") from None
|
|
1257
|
+
|
|
1258
|
+
if port < 1 or port > 65535:
|
|
1259
|
+
raise RuntimeError(f"{key} must be between 1 and 65535, got {port}")
|
|
1260
|
+
|
|
1261
|
+
return port
|
|
1262
|
+
|
|
1263
|
+
|
|
1264
|
+
def can_bind(port):
|
|
1265
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
1266
|
+
try:
|
|
1267
|
+
sock.bind(("0.0.0.0", port))
|
|
1268
|
+
except OSError:
|
|
1269
|
+
return False
|
|
1270
|
+
return True
|
|
1271
|
+
|
|
1272
|
+
|
|
1273
|
+
def main():
|
|
1274
|
+
project_label = os.environ.get("ANKH_PROJECT_LABEL", "unknown")
|
|
1275
|
+
ports = [(key, read_port(key)) for key in PORT_KEYS]
|
|
1276
|
+
|
|
1277
|
+
seen = {}
|
|
1278
|
+
duplicates = []
|
|
1279
|
+
for key, port in ports:
|
|
1280
|
+
existing = seen.get(port)
|
|
1281
|
+
if existing is not None:
|
|
1282
|
+
duplicates.append((key, existing, port))
|
|
1283
|
+
else:
|
|
1284
|
+
seen[port] = key
|
|
1285
|
+
|
|
1286
|
+
blocked = [(key, port) for key, port in ports if not can_bind(port)]
|
|
1287
|
+
|
|
1288
|
+
if not duplicates and not blocked:
|
|
1289
|
+
return 0
|
|
1290
|
+
|
|
1291
|
+
print(f"Supabase local port preflight failed for project '{project_label}'.", file=sys.stderr)
|
|
1292
|
+
|
|
1293
|
+
for key, existing, port in duplicates:
|
|
1294
|
+
print(f"- {key} duplicates {existing} on port {port}.", file=sys.stderr)
|
|
1295
|
+
|
|
1296
|
+
for key, port in blocked:
|
|
1297
|
+
print(f"- {key}={port} is already in use on this host.", file=sys.stderr)
|
|
1298
|
+
|
|
1299
|
+
print("Override SUPABASE_LOCAL_PORT_BASE or the specific SUPABASE_LOCAL_*_PORT values in infra/minikube/.env and retry.", file=sys.stderr)
|
|
1300
|
+
return 1
|
|
1301
|
+
|
|
1302
|
+
|
|
1303
|
+
if __name__ == "__main__":
|
|
1304
|
+
raise SystemExit(main())
|
|
1305
|
+
PY
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
if [[ ! -f "\${SUPABASE_PROJECT_DIR}/supabase/config.toml" ]]; then
|
|
1309
|
+
echo "No supabase/config.toml found in \${SUPABASE_PROJECT_DIR}. Initializing Supabase project..."
|
|
1310
|
+
(
|
|
1311
|
+
cd "\${SUPABASE_PROJECT_DIR}"
|
|
1312
|
+
supabase init --yes >/dev/null
|
|
1313
|
+
)
|
|
1314
|
+
fi
|
|
1315
|
+
|
|
1316
|
+
configure_supabase_local_ports
|
|
1317
|
+
|
|
1318
|
+
STATUS_ENV="$(
|
|
1319
|
+
cd "\${SUPABASE_PROJECT_DIR}"
|
|
1320
|
+
supabase status -o env 2>/dev/null || true
|
|
1321
|
+
)"
|
|
1322
|
+
|
|
1323
|
+
start_supabase_local_stack() {
|
|
1324
|
+
if supabase start >/dev/null; then
|
|
1325
|
+
return 0
|
|
1326
|
+
fi
|
|
1327
|
+
|
|
1328
|
+
echo "Supabase local start failed. Stopping stale local stack and retrying once..."
|
|
1329
|
+
supabase stop --no-backup >/dev/null 2>&1 || true
|
|
1330
|
+
|
|
1331
|
+
if supabase start >/dev/null; then
|
|
1332
|
+
return 0
|
|
1333
|
+
fi
|
|
1334
|
+
|
|
1335
|
+
echo "Supabase local start failed after retry."
|
|
1336
|
+
return 1
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
if [[ -z "\${STATUS_ENV}" ]]; then
|
|
1340
|
+
assert_supabase_local_ports_available
|
|
1341
|
+
echo "Supabase local stack not detected. Starting with 'supabase start'..."
|
|
1342
|
+
(
|
|
1343
|
+
cd "\${SUPABASE_PROJECT_DIR}"
|
|
1344
|
+
start_supabase_local_stack
|
|
1345
|
+
)
|
|
1346
|
+
|
|
1347
|
+
STATUS_ENV="$(
|
|
1348
|
+
cd "\${SUPABASE_PROJECT_DIR}"
|
|
1349
|
+
supabase status -o env
|
|
1350
|
+
)"
|
|
1351
|
+
fi
|
|
1352
|
+
|
|
1353
|
+
read_env_value() {
|
|
1354
|
+
local key="$1"
|
|
1355
|
+
local line
|
|
1356
|
+
line="$(printf '%s\\n' "\${STATUS_ENV}" | awk -v key="\${key}" '
|
|
1357
|
+
$0 ~ "^(export[[:space:]]+)?" key "=" {print}
|
|
1358
|
+
' | tail -n1)"
|
|
1359
|
+
|
|
1360
|
+
if [[ -z "\${line}" ]]; then
|
|
1361
|
+
echo ""
|
|
1362
|
+
return
|
|
1363
|
+
fi
|
|
1364
|
+
|
|
1365
|
+
line="\${line#export }"
|
|
1366
|
+
line="\${line#\${key}=}"
|
|
1367
|
+
line="\${line%\\"}"
|
|
1368
|
+
line="\${line#\\"}"
|
|
1369
|
+
printf '%s' "\${line}"
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
API_URL="$(read_env_value API_URL)"
|
|
1373
|
+
ANON_KEY="$(read_env_value ANON_KEY)"
|
|
1374
|
+
SERVICE_ROLE_KEY="$(read_env_value SERVICE_ROLE_KEY)"
|
|
1375
|
+
JWT_SECRET="$(read_env_value JWT_SECRET)"
|
|
1376
|
+
|
|
1377
|
+
if [[ -z "\${API_URL}" || -z "\${ANON_KEY}" || -z "\${SERVICE_ROLE_KEY}" || -z "\${JWT_SECRET}" ]]; then
|
|
1378
|
+
echo "Unable to read required Supabase values from 'supabase status -o env'."
|
|
1379
|
+
echo "Expected API_URL, ANON_KEY, SERVICE_ROLE_KEY, JWT_SECRET."
|
|
1380
|
+
exit 1
|
|
1381
|
+
fi
|
|
1382
|
+
|
|
1383
|
+
upsert_env() {
|
|
1384
|
+
local file="$1"
|
|
1385
|
+
local key="$2"
|
|
1386
|
+
local value="$3"
|
|
1387
|
+
local tmp_file
|
|
1388
|
+
tmp_file="$(mktemp)"
|
|
1389
|
+
|
|
1390
|
+
awk -v key="\${key}" -v value="\${value}" '
|
|
1391
|
+
BEGIN { replaced = 0 }
|
|
1392
|
+
index($0, key "=") == 1 {
|
|
1393
|
+
print key "=" value
|
|
1394
|
+
replaced = 1
|
|
1395
|
+
next
|
|
1396
|
+
}
|
|
1397
|
+
{ print }
|
|
1398
|
+
END {
|
|
1399
|
+
if (replaced == 0) {
|
|
1400
|
+
print key "=" value
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
' "\${file}" > "\${tmp_file}"
|
|
1404
|
+
|
|
1405
|
+
mv "\${tmp_file}" "\${file}"
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
if [[ ! -f "\${ENV_FILE}" ]]; then
|
|
1409
|
+
cp "\${ENV_EXAMPLE_FILE}" "\${ENV_FILE}"
|
|
1410
|
+
fi
|
|
1411
|
+
|
|
1412
|
+
if [[ ! -f "\${APP_SUPABASE_ENV_FILE}" ]]; then
|
|
1413
|
+
touch "\${APP_SUPABASE_ENV_FILE}"
|
|
1414
|
+
fi
|
|
1415
|
+
|
|
1416
|
+
upsert_env "\${ENV_FILE}" "SUPABASE_SECRET_SYNC_ENABLED" "true"
|
|
1417
|
+
upsert_env "\${ENV_FILE}" "SUPABASE_URL" "\${API_URL}"
|
|
1418
|
+
upsert_env "\${ENV_FILE}" "SUPABASE_ANON_KEY" "\${ANON_KEY}"
|
|
1419
|
+
upsert_env "\${ENV_FILE}" "SUPABASE_SERVICE_ROLE_KEY" "\${SERVICE_ROLE_KEY}"
|
|
1420
|
+
upsert_env "\${ENV_FILE}" "SUPABASE_JWT_SECRET" "\${JWT_SECRET}"
|
|
1421
|
+
upsert_env "\${ENV_FILE}" "EXPO_PUBLIC_SUPABASE_URL" "\${API_URL}"
|
|
1422
|
+
upsert_env "\${ENV_FILE}" "EXPO_PUBLIC_SUPABASE_ANON_KEY" "\${ANON_KEY}"
|
|
1423
|
+
upsert_env "\${ENV_FILE}" "SUPABASE_PROJECT_DIR" "\${SUPABASE_PROJECT_DIR}"
|
|
1424
|
+
upsert_env "\${ENV_FILE}" "SUPABASE_LOCAL_PORT_BASE" "\${SUPABASE_LOCAL_PORT_BASE}"
|
|
1425
|
+
upsert_env "\${ENV_FILE}" "SUPABASE_LOCAL_SHADOW_PORT" "\${SUPABASE_LOCAL_SHADOW_PORT}"
|
|
1426
|
+
upsert_env "\${ENV_FILE}" "SUPABASE_LOCAL_API_PORT" "\${SUPABASE_LOCAL_API_PORT}"
|
|
1427
|
+
upsert_env "\${ENV_FILE}" "SUPABASE_LOCAL_DB_PORT" "\${SUPABASE_LOCAL_DB_PORT}"
|
|
1428
|
+
upsert_env "\${ENV_FILE}" "SUPABASE_LOCAL_STUDIO_PORT" "\${SUPABASE_LOCAL_STUDIO_PORT}"
|
|
1429
|
+
upsert_env "\${ENV_FILE}" "SUPABASE_LOCAL_INBUCKET_PORT" "\${SUPABASE_LOCAL_INBUCKET_PORT}"
|
|
1430
|
+
upsert_env "\${ENV_FILE}" "SUPABASE_LOCAL_ANALYTICS_PORT" "\${SUPABASE_LOCAL_ANALYTICS_PORT}"
|
|
1431
|
+
|
|
1432
|
+
upsert_env "\${APP_SUPABASE_ENV_FILE}" "EXPO_PUBLIC_SUPABASE_URL" "\${API_URL}"
|
|
1433
|
+
upsert_env "\${APP_SUPABASE_ENV_FILE}" "EXPO_PUBLIC_SUPABASE_ANON_KEY" "\${ANON_KEY}"
|
|
1434
|
+
upsert_env "\${APP_SUPABASE_ENV_FILE}" "SUPABASE_URL" "\${API_URL}"
|
|
1435
|
+
upsert_env "\${APP_SUPABASE_ENV_FILE}" "SUPABASE_ANON_KEY" "\${ANON_KEY}"
|
|
1436
|
+
|
|
1437
|
+
echo "Updated \${ENV_FILE} with local Supabase credentials from \${SUPABASE_PROJECT_DIR}."
|
|
1438
|
+
echo "Updated \${APP_SUPABASE_ENV_FILE} with Expo Supabase credentials for local app runs."
|
|
1439
|
+
echo "If Expo is already running, restart it to pick up updated EXPO_PUBLIC_* variables."
|
|
1440
|
+
echo "Next: run ./scripts/up.sh to sync Kubernetes secrets and apply infrastructure."
|
|
1441
|
+
`;
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
function getDefaultAppImage(namespace: string): string {
|
|
1445
|
+
return `ankh/${namespace}:dev`;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
function resolveSupabaseLocalPorts(namespace: string): SupabaseLocalPorts {
|
|
1449
|
+
const base =
|
|
1450
|
+
SUPABASE_LOCAL_PORT_BASE +
|
|
1451
|
+
resolveSupabaseLocalPortBucket(namespace) * SUPABASE_LOCAL_PORT_BUCKET_SIZE;
|
|
1452
|
+
|
|
1453
|
+
return {
|
|
1454
|
+
base,
|
|
1455
|
+
shadow: base,
|
|
1456
|
+
api: base + 1,
|
|
1457
|
+
db: base + 2,
|
|
1458
|
+
studio: base + 3,
|
|
1459
|
+
inbucket: base + 4,
|
|
1460
|
+
analytics: base + 5,
|
|
1461
|
+
};
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
function resolveSupabaseLocalPortBucket(namespace: string): number {
|
|
1465
|
+
const rawBucket = hashProjectId(namespace) - hashProjectId(SUPABASE_LOCAL_PORT_REFERENCE_PROJECT);
|
|
1466
|
+
return (rawBucket + SUPABASE_LOCAL_PORT_BUCKET_COUNT) % SUPABASE_LOCAL_PORT_BUCKET_COUNT;
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
function hashProjectId(value: string): number {
|
|
1470
|
+
const source = value.trim().length > 0 ? value.trim() : 'app';
|
|
1471
|
+
let hash = 0;
|
|
1472
|
+
|
|
1473
|
+
for (const char of source) {
|
|
1474
|
+
hash = (hash * 31 + char.charCodeAt(0)) % SUPABASE_LOCAL_PORT_BUCKET_COUNT;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
return hash;
|
|
1478
|
+
}
|