@caelo-cms/provisioning 0.10.29 → 0.10.31
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/dist/admin-domain.d.ts +54 -0
- package/dist/admin-domain.d.ts.map +1 -0
- package/dist/admin-domain.js +122 -0
- package/dist/admin-domain.js.map +1 -0
- package/dist/cli.js +71 -7
- package/dist/cli.js.map +1 -1
- package/dist/compose.d.ts +7 -9
- package/dist/compose.d.ts.map +1 -1
- package/dist/compose.js +17 -1
- package/dist/compose.js.map +1 -1
- package/dist/deployed-release.d.ts +84 -0
- package/dist/deployed-release.d.ts.map +1 -0
- package/dist/deployed-release.js +182 -0
- package/dist/deployed-release.js.map +1 -0
- package/dist/gcloud-retry.d.ts +16 -0
- package/dist/gcloud-retry.d.ts.map +1 -0
- package/dist/gcloud-retry.js +17 -0
- package/dist/gcloud-retry.js.map +1 -0
- package/dist/gcloud.d.ts.map +1 -1
- package/dist/gcloud.js +2 -0
- package/dist/gcloud.js.map +1 -1
- package/dist/gcp-names.d.ts +58 -0
- package/dist/gcp-names.d.ts.map +1 -0
- package/dist/gcp-names.js +88 -0
- package/dist/gcp-names.js.map +1 -0
- package/dist/install-state.d.ts +31 -0
- package/dist/install-state.d.ts.map +1 -1
- package/dist/install-state.js +38 -0
- package/dist/install-state.js.map +1 -1
- package/dist/lifecycle.d.ts +5 -0
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +340 -50
- package/dist/lifecycle.js.map +1 -1
- package/dist/mcp-iap.d.ts +5 -26
- package/dist/mcp-iap.d.ts.map +1 -1
- package/dist/mcp-iap.js +6 -18
- package/dist/mcp-iap.js.map +1 -1
- package/dist/migration-runner.d.ts +25 -0
- package/dist/migration-runner.d.ts.map +1 -1
- package/dist/migration-runner.js +60 -19
- package/dist/migration-runner.js.map +1 -1
- package/dist/operator-access.d.ts +83 -0
- package/dist/operator-access.d.ts.map +1 -0
- package/dist/operator-access.js +562 -0
- package/dist/operator-access.js.map +1 -0
- package/dist/region-data.d.ts +30 -0
- package/dist/region-data.d.ts.map +1 -0
- package/dist/region-data.js +406 -0
- package/dist/region-data.js.map +1 -0
- package/dist/regions.d.ts +130 -0
- package/dist/regions.d.ts.map +1 -0
- package/dist/regions.js +247 -0
- package/dist/regions.js.map +1 -0
- package/dist/runtime-secrets.d.ts +128 -0
- package/dist/runtime-secrets.d.ts.map +1 -0
- package/dist/runtime-secrets.js +444 -0
- package/dist/runtime-secrets.js.map +1 -0
- package/dist/stack-contract.d.ts +231 -0
- package/dist/stack-contract.d.ts.map +1 -0
- package/dist/stack-contract.js +455 -0
- package/dist/stack-contract.js.map +1 -0
- package/dist/stack-converge.d.ts +183 -0
- package/dist/stack-converge.d.ts.map +1 -0
- package/dist/stack-converge.js +513 -0
- package/dist/stack-converge.js.map +1 -0
- package/dist/wizard.d.ts +7 -0
- package/dist/wizard.d.ts.map +1 -1
- package/dist/wizard.js +11 -1
- package/dist/wizard.js.map +1 -1
- package/dist/wizards/gcp-cost.d.ts +10 -10
- package/dist/wizards/gcp-cost.d.ts.map +1 -1
- package/dist/wizards/gcp-cost.js +40 -3
- package/dist/wizards/gcp-cost.js.map +1 -1
- package/dist/wizards/gcp-firebase.d.ts +2 -0
- package/dist/wizards/gcp-firebase.d.ts.map +1 -1
- package/dist/wizards/gcp-firebase.js +1 -0
- package/dist/wizards/gcp-firebase.js.map +1 -1
- package/dist/wizards/gcp-pulumi.d.ts +51 -1
- package/dist/wizards/gcp-pulumi.d.ts.map +1 -1
- package/dist/wizards/gcp-pulumi.js +89 -6
- package/dist/wizards/gcp-pulumi.js.map +1 -1
- package/dist/wizards/gcp.d.ts +2 -0
- package/dist/wizards/gcp.d.ts.map +1 -1
- package/dist/wizards/gcp.js +171 -23
- package/dist/wizards/gcp.js.map +1 -1
- package/package.json +3 -3
- package/stacks/aws/Pulumi.yaml +5 -2
- package/stacks/aws/README.md +3 -1
- package/stacks/aws/index.ts +21 -1
- package/stacks/azure/Pulumi.yaml +6 -3
- package/stacks/azure/README.md +1 -1
- package/stacks/azure/index.ts +22 -3
- package/stacks/gcp/Pulumi.yaml +5 -5
- package/stacks/gcp/README.md +15 -3
- package/stacks/gcp/index.ts +204 -141
- package/stacks/gcp-firebase/Pulumi.yaml +5 -2
- package/stacks/gcp-firebase/index.ts +169 -76
- package/stacks/self-hosted/Pulumi.yaml +4 -0
- package/stacks/self-hosted/index.ts +3 -0
|
@@ -30,12 +30,32 @@ import * as pulumi from "@pulumi/pulumi";
|
|
|
30
30
|
import * as random from "@pulumi/random";
|
|
31
31
|
import type { CloudAdapterOutputs, DnsRecord } from "../../dist/adapter.js";
|
|
32
32
|
import { generateBootstrapToken } from "../../dist/bootstrap-token.js";
|
|
33
|
+
import {
|
|
34
|
+
gatewayServiceAccountId,
|
|
35
|
+
gcpBucketName,
|
|
36
|
+
gcpSecretId,
|
|
37
|
+
MCP_IAP_SERVICE_ACCOUNT_ID,
|
|
38
|
+
runServiceAccountId,
|
|
39
|
+
} from "../../dist/gcp-names.js";
|
|
40
|
+
import {
|
|
41
|
+
ADMIN_MEMORY_DEFAULT,
|
|
42
|
+
adminEnvContract,
|
|
43
|
+
type CloudRunEnvVar,
|
|
44
|
+
type CloudRunSlug,
|
|
45
|
+
databaseUrls,
|
|
46
|
+
gatewayEnvContract,
|
|
47
|
+
type RuntimeSecret,
|
|
48
|
+
runtimeSecretBindings,
|
|
49
|
+
} from "../../dist/stack-contract.js";
|
|
33
50
|
|
|
34
51
|
const cfg = new pulumi.Config();
|
|
35
52
|
const domain = cfg.require("domain");
|
|
36
53
|
const ownerEmail = cfg.require("ownerEmail");
|
|
37
54
|
const project = cfg.require("project");
|
|
38
|
-
|
|
55
|
+
// #607 — chosen by the operator in the wizard and fixed after install; no
|
|
56
|
+
// default, so a stack run without it fails instead of landing in a region
|
|
57
|
+
// nobody picked.
|
|
58
|
+
const region = cfg.require("region");
|
|
39
59
|
|
|
40
60
|
// Where Secret Manager keeps the secret payloads. Fixed at create time —
|
|
41
61
|
// changing it replaces every secret — so the wizard pins it per stack:
|
|
@@ -57,6 +77,9 @@ const cloudSqlHa = cfg.getBoolean("cloudSqlHa") ?? false;
|
|
|
57
77
|
const cloudSqlEdition = cfg.get("cloudSqlEdition") ?? "ENTERPRISE";
|
|
58
78
|
const deletionProtection = cfg.getBoolean("deletionProtection") ?? false;
|
|
59
79
|
const adminMinInstances = Number.parseInt(cfg.get("adminMinInstances") ?? "0", 10);
|
|
80
|
+
// #553 — the admin also runs the Lighthouse quality audit; same knob and
|
|
81
|
+
// default on every adapter (stack-contract.ts ADMIN_MEMORY_DEFAULT).
|
|
82
|
+
const adminMemory = cfg.get("adminMemory") ?? ADMIN_MEMORY_DEFAULT;
|
|
60
83
|
const adminMaxInstances = Number.parseInt(cfg.get("adminMaxInstances") ?? "5", 10);
|
|
61
84
|
const gatewayMinInstances = Number.parseInt(cfg.get("gatewayMinInstances") ?? "0", 10);
|
|
62
85
|
const maxConnections = Number.parseInt(cfg.get("maxConnections") ?? "100", 10);
|
|
@@ -139,15 +162,9 @@ const sqlPeering = new gcp.servicenetworking.Connection(
|
|
|
139
162
|
// =========================================================================
|
|
140
163
|
|
|
141
164
|
const pgPasswordBytes = new random.RandomBytes(`${namePrefix}-pg-password-bytes`, { length: 32 });
|
|
142
|
-
const csrfSecretBytes = new random.RandomBytes(`${namePrefix}-csrf-secret-bytes`, { length: 32 });
|
|
143
|
-
const cookieSecretBytes = new random.RandomBytes(`${namePrefix}-cookie-secret-bytes`, {
|
|
144
|
-
length: 32,
|
|
145
|
-
});
|
|
146
165
|
const kekBytes = new random.RandomBytes(`${namePrefix}-secret-kek-bytes`, { length: 32 });
|
|
147
166
|
|
|
148
167
|
const postgresPassword = pulumi.secret(pgPasswordBytes.hex);
|
|
149
|
-
const csrfSecret = pulumi.secret(csrfSecretBytes.hex);
|
|
150
|
-
const cookieSecret = pulumi.secret(cookieSecretBytes.hex);
|
|
151
168
|
const caeloSecretKek = pulumi.secret(kekBytes.hex);
|
|
152
169
|
// v0.3.2 — anthropic-api-key Secret + Pulumi config dropped. The
|
|
153
170
|
// runtime AI provider configuration lives at /security/ai (key
|
|
@@ -162,19 +179,26 @@ const caeloSecretKek = pulumi.secret(kekBytes.hex);
|
|
|
162
179
|
// in v0.3.0 but never wired to runtime (transport.ts reads from
|
|
163
180
|
// cfg.config.apiKey, never from Secret Manager). Drop to reduce
|
|
164
181
|
// surface area + monthly cost.
|
|
182
|
+
// csrf-secret + cookie-secret were dropped for the same reason: CSRF
|
|
183
|
+
// secrets are per session (in cms_admin) and the gateway's cookie secret
|
|
184
|
+
// lives in site_settings — nothing read the Secret Manager copies.
|
|
185
|
+
// `pulumi up` deletes them; `cms-provision upgrade` leaves them in place.
|
|
186
|
+
//
|
|
187
|
+
// internal-secret (CAELO_INTERNAL_SECRET) and tool-approval-secret
|
|
188
|
+
// (CAELO_TOOL_APPROVAL_SECRET) are generated by the CLI, not here (see the
|
|
189
|
+
// same note in stacks/gcp/index.ts); this program references them by id.
|
|
165
190
|
|
|
166
191
|
interface MadeSecret {
|
|
167
192
|
resource: gcp.secretmanager.Secret;
|
|
168
|
-
version: gcp.secretmanager.SecretVersion
|
|
193
|
+
version: gcp.secretmanager.SecretVersion;
|
|
169
194
|
}
|
|
170
195
|
|
|
171
|
-
function makeSecret(name: string, value: pulumi.Output<string>
|
|
196
|
+
function makeSecret(name: string, value: pulumi.Output<string>): MadeSecret {
|
|
172
197
|
const resource = new gcp.secretmanager.Secret(
|
|
173
198
|
`${namePrefix}-${name}`,
|
|
174
|
-
{ secretId:
|
|
199
|
+
{ secretId: gcpSecretId(env, name), replication: secretReplication },
|
|
175
200
|
opts,
|
|
176
201
|
);
|
|
177
|
-
if (value === null) return { resource, version: null };
|
|
178
202
|
const version = new gcp.secretmanager.SecretVersion(
|
|
179
203
|
`${namePrefix}-${name}-v1`,
|
|
180
204
|
{ secret: resource.name, secretData: value },
|
|
@@ -184,10 +208,17 @@ function makeSecret(name: string, value: pulumi.Output<string> | null): MadeSecr
|
|
|
184
208
|
}
|
|
185
209
|
|
|
186
210
|
const pgSecret = makeSecret("postgres-password", postgresPassword);
|
|
187
|
-
const csrfSecretRes = makeSecret("csrf-secret", csrfSecret);
|
|
188
|
-
const cookieSecretRes = makeSecret("cookie-secret", cookieSecret);
|
|
189
211
|
const kekSecret = makeSecret("secret-kek", caeloSecretKek);
|
|
190
212
|
|
|
213
|
+
// The id each runtime secret is bound by: the Pulumi resource where this
|
|
214
|
+
// program owns the secret, the CLI-generated id otherwise.
|
|
215
|
+
const runtimeSecretIds: Record<RuntimeSecret, pulumi.Input<string>> = {
|
|
216
|
+
"postgres-password": pgSecret.resource.secretId,
|
|
217
|
+
"secret-kek": kekSecret.resource.secretId,
|
|
218
|
+
"internal-secret": gcpSecretId(env, "internal-secret"),
|
|
219
|
+
"tool-approval-secret": gcpSecretId(env, "tool-approval-secret"),
|
|
220
|
+
};
|
|
221
|
+
|
|
191
222
|
// =========================================================================
|
|
192
223
|
// Tier 4 — Cloud SQL Postgres (private IP only; HA configurable)
|
|
193
224
|
// =========================================================================
|
|
@@ -216,7 +247,12 @@ const sqlInstance = new gcp.sql.DatabaseInstance(
|
|
|
216
247
|
privateNetwork: network.id,
|
|
217
248
|
enablePrivatePathForGoogleCloudServices: true,
|
|
218
249
|
},
|
|
219
|
-
databaseFlags: [
|
|
250
|
+
databaseFlags: [
|
|
251
|
+
{ name: "max_connections", value: maxConnections.toString() },
|
|
252
|
+
// The operator-access sync job logs in as its service account (Cloud
|
|
253
|
+
// SQL IAM database authentication, no password). No restart needed.
|
|
254
|
+
{ name: "cloudsql.iam_authentication", value: "on" },
|
|
255
|
+
],
|
|
220
256
|
},
|
|
221
257
|
deletionProtection,
|
|
222
258
|
},
|
|
@@ -245,9 +281,9 @@ new gcp.sql.User(
|
|
|
245
281
|
opts,
|
|
246
282
|
);
|
|
247
283
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const
|
|
284
|
+
// Password-less connection URLs (stack-contract.ts databaseUrls); the
|
|
285
|
+
// services get the password as a Secret Manager reference.
|
|
286
|
+
const dbUrls = sqlInstance.privateIpAddress.apply(databaseUrls);
|
|
251
287
|
|
|
252
288
|
// =========================================================================
|
|
253
289
|
// Media bucket — private, signed-URL access only (same as `gcp` stack)
|
|
@@ -256,7 +292,7 @@ const publicDatabaseUrl = pulumi.interpolate`postgres://public_role:${postgresPa
|
|
|
256
292
|
const mediaBucket = new gcp.storage.Bucket(
|
|
257
293
|
`${namePrefix}-media`,
|
|
258
294
|
{
|
|
259
|
-
name:
|
|
295
|
+
name: gcpBucketName(project, env, "media"),
|
|
260
296
|
location: region.toUpperCase(),
|
|
261
297
|
uniformBucketLevelAccess: true,
|
|
262
298
|
forceDestroy: env !== "production",
|
|
@@ -272,26 +308,47 @@ const mediaBucket = new gcp.storage.Bucket(
|
|
|
272
308
|
const runSa = new gcp.serviceaccount.Account(
|
|
273
309
|
`${namePrefix}-run-sa`,
|
|
274
310
|
{
|
|
275
|
-
accountId:
|
|
311
|
+
accountId: runServiceAccountId(env),
|
|
276
312
|
displayName: `Caelo ${env} Cloud Run service account`,
|
|
277
313
|
},
|
|
278
314
|
opts,
|
|
279
315
|
);
|
|
280
316
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
{
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
317
|
+
// The gateway runs as its own SA so it can read only the secrets it needs
|
|
318
|
+
// (stack-contract.ts SERVICE_SECRET_ENV). `cms-provision upgrade` creates it
|
|
319
|
+
// on older installs, hence createIgnoreAlreadyExists.
|
|
320
|
+
const gatewaySa = new gcp.serviceaccount.Account(
|
|
321
|
+
`${namePrefix}-gateway-sa`,
|
|
322
|
+
{
|
|
323
|
+
accountId: gatewayServiceAccountId(env),
|
|
324
|
+
displayName: `Caelo ${env} gateway`,
|
|
325
|
+
createIgnoreAlreadyExists: true,
|
|
326
|
+
},
|
|
327
|
+
opts,
|
|
328
|
+
);
|
|
329
|
+
const serviceSa: Record<CloudRunSlug, gcp.serviceaccount.Account> = {
|
|
330
|
+
admin: runSa,
|
|
331
|
+
gateway: gatewaySa,
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
// secretAccessor for each service's SA on exactly the secrets its env reads.
|
|
335
|
+
// Cloud Run refuses to deploy a revision whose SA can't read a referenced
|
|
336
|
+
// secret, so each service waits for its bindings.
|
|
337
|
+
const secretBindings: Record<CloudRunSlug, gcp.secretmanager.SecretIamMember[]> = {
|
|
338
|
+
admin: [],
|
|
339
|
+
gateway: [],
|
|
340
|
+
};
|
|
341
|
+
for (const binding of runtimeSecretBindings()) {
|
|
342
|
+
secretBindings[binding.service].push(
|
|
343
|
+
new gcp.secretmanager.SecretIamMember(
|
|
344
|
+
`${namePrefix}-${binding.stackResource}`,
|
|
345
|
+
{
|
|
346
|
+
secretId: runtimeSecretIds[binding.secret],
|
|
347
|
+
role: "roles/secretmanager.secretAccessor",
|
|
348
|
+
member: pulumi.interpolate`serviceAccount:${serviceSa[binding.service].email}`,
|
|
349
|
+
},
|
|
350
|
+
opts,
|
|
351
|
+
),
|
|
295
352
|
);
|
|
296
353
|
}
|
|
297
354
|
|
|
@@ -346,6 +403,24 @@ new gcp.projects.IAMMember(
|
|
|
346
403
|
},
|
|
347
404
|
opts,
|
|
348
405
|
);
|
|
406
|
+
new gcp.projects.IAMMember(
|
|
407
|
+
`${namePrefix}-gateway-log-writer`,
|
|
408
|
+
{
|
|
409
|
+
project,
|
|
410
|
+
role: "roles/logging.logWriter",
|
|
411
|
+
member: pulumi.interpolate`serviceAccount:${gatewaySa.email}`,
|
|
412
|
+
},
|
|
413
|
+
opts,
|
|
414
|
+
);
|
|
415
|
+
new gcp.projects.IAMMember(
|
|
416
|
+
`${namePrefix}-gateway-metric-writer`,
|
|
417
|
+
{
|
|
418
|
+
project,
|
|
419
|
+
role: "roles/monitoring.metricWriter",
|
|
420
|
+
member: pulumi.interpolate`serviceAccount:${gatewaySa.email}`,
|
|
421
|
+
},
|
|
422
|
+
opts,
|
|
423
|
+
);
|
|
349
424
|
|
|
350
425
|
// =========================================================================
|
|
351
426
|
// Tier 1 — Firebase Hosting site
|
|
@@ -394,12 +469,16 @@ const firebaseCustomDomain = new gcp.firebase.HostingCustomDomain(
|
|
|
394
469
|
// =========================================================================
|
|
395
470
|
|
|
396
471
|
interface CloudRunArgs {
|
|
397
|
-
readonly serviceName:
|
|
472
|
+
readonly serviceName: "admin" | "gateway";
|
|
398
473
|
readonly minInstances: number;
|
|
399
474
|
readonly maxInstances: number;
|
|
400
475
|
readonly memory: string;
|
|
401
476
|
readonly timeout: string;
|
|
402
|
-
|
|
477
|
+
/**
|
|
478
|
+
* The service's complete env — plain values and Secret Manager
|
|
479
|
+
* references: its env contract (src/stack-contract.ts).
|
|
480
|
+
*/
|
|
481
|
+
readonly contractEnv: ReadonlyArray<CloudRunEnvVar<pulumi.Input<string>>>;
|
|
403
482
|
/** v0.3.1 — admin: ALL (so IAP gates internet traffic). gateway:
|
|
404
483
|
* INTERNAL_LOAD_BALANCER (locked down; only Firebase Hosting
|
|
405
484
|
* rewrites + run.invoker-authorised callers can reach it). */
|
|
@@ -434,7 +513,7 @@ function cloudRunService(args: CloudRunArgs): gcp.cloudrunv2.Service {
|
|
|
434
513
|
iapEnabled: args.iapEnabled ?? false,
|
|
435
514
|
deletionProtection,
|
|
436
515
|
template: {
|
|
437
|
-
serviceAccount:
|
|
516
|
+
serviceAccount: serviceSa[args.serviceName].email,
|
|
438
517
|
scaling: {
|
|
439
518
|
minInstanceCount: args.minInstances,
|
|
440
519
|
maxInstanceCount: args.maxInstances,
|
|
@@ -443,19 +522,7 @@ function cloudRunService(args: CloudRunArgs): gcp.cloudrunv2.Service {
|
|
|
443
522
|
containers: [
|
|
444
523
|
{
|
|
445
524
|
image: imageRef(args.serviceName),
|
|
446
|
-
envs: [
|
|
447
|
-
{ name: "CAELO_PROVIDER", value: "gcp-firebase" },
|
|
448
|
-
{ name: "CAELO_ENV", value: env },
|
|
449
|
-
{ name: "ADMIN_DATABASE_URL", value: adminDatabaseUrl },
|
|
450
|
-
{ name: "MEDIA_STORAGE_URL", value: pulumi.interpolate`gs://${mediaBucket.name}` },
|
|
451
|
-
{
|
|
452
|
-
name: "CAELO_SECRET_KEK",
|
|
453
|
-
valueSource: {
|
|
454
|
-
secretKeyRef: { secret: kekSecret.resource.secretId, version: "latest" },
|
|
455
|
-
},
|
|
456
|
-
},
|
|
457
|
-
...(args.extraEnv ?? []),
|
|
458
|
-
],
|
|
525
|
+
envs: [...args.contractEnv],
|
|
459
526
|
resources: { limits: { cpu: "1", memory: args.memory } },
|
|
460
527
|
},
|
|
461
528
|
],
|
|
@@ -471,13 +538,31 @@ function cloudRunService(args: CloudRunArgs): gcp.cloudrunv2.Service {
|
|
|
471
538
|
pgAdminUser,
|
|
472
539
|
cmsAdminDb,
|
|
473
540
|
cmsPublicDb,
|
|
474
|
-
|
|
475
|
-
|
|
541
|
+
// The env contract names the media bucket as a string; keep it
|
|
542
|
+
// ordered before the services that use it.
|
|
543
|
+
mediaBucket,
|
|
544
|
+
// Referenced secrets must have a version, and the service's SA
|
|
545
|
+
// must be able to read them, before Cloud Run deploys a revision.
|
|
546
|
+
pgSecret.version,
|
|
547
|
+
kekSecret.version,
|
|
548
|
+
...secretBindings[args.serviceName],
|
|
476
549
|
],
|
|
477
550
|
},
|
|
478
551
|
);
|
|
479
552
|
}
|
|
480
553
|
|
|
554
|
+
// Inputs of the admin + gateway env contracts. The contracts are shared with
|
|
555
|
+
// `cms-provision upgrade`, which applies them to installs it rolls — add new
|
|
556
|
+
// env vars and secrets there, not here.
|
|
557
|
+
const envContractInputs = {
|
|
558
|
+
provider: "gcp-firebase",
|
|
559
|
+
projectId: project,
|
|
560
|
+
env,
|
|
561
|
+
domain,
|
|
562
|
+
region,
|
|
563
|
+
databaseUrls: { admin: dbUrls.admin, publicAdmin: dbUrls.publicAdmin, public: dbUrls.public },
|
|
564
|
+
} as const;
|
|
565
|
+
|
|
481
566
|
// Gateway provisioned first so we can pass its name/region into the
|
|
482
567
|
// admin's env (the Firebase publisher needs them to wire rewrites).
|
|
483
568
|
const gatewaySvc = cloudRunService({
|
|
@@ -490,39 +575,27 @@ const gatewaySvc = cloudRunService({
|
|
|
490
575
|
// rewrites or any caller holding roles/run.invoker on the
|
|
491
576
|
// service. Anonymous internet traffic gets a Cloud Run 403.
|
|
492
577
|
ingress: "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER",
|
|
493
|
-
|
|
578
|
+
contractEnv: gatewayEnvContract(envContractInputs),
|
|
494
579
|
});
|
|
495
580
|
|
|
496
|
-
// Issue #37 — deterministic, so the admin env can carry it as a plain string.
|
|
497
|
-
const mcpServiceAccountEmail = `caelo-mcp@${project}.iam.gserviceaccount.com`;
|
|
498
|
-
|
|
499
581
|
const adminSvc = cloudRunService({
|
|
500
582
|
serviceName: "admin",
|
|
501
583
|
minInstances: adminMinInstances,
|
|
502
584
|
maxInstances: adminMaxInstances,
|
|
503
|
-
memory:
|
|
585
|
+
memory: adminMemory,
|
|
504
586
|
timeout: "3600s",
|
|
505
587
|
// v0.3.1 — admin is internet-reachable but gated by Cloud Run
|
|
506
588
|
// native IAP. iapEnabled flips the IAP integration on the service.
|
|
507
589
|
ingress: "INGRESS_TRAFFIC_ALL",
|
|
508
590
|
iapEnabled: true,
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
{ name: "CAELO_GENERATOR_CLI", value: "/app/apps/static-generator/src/cli.ts" },
|
|
518
|
-
// v0.3.1 — Firebase publisher needs the gateway service name +
|
|
519
|
-
// region to declare the /api/** rewrite when creating each
|
|
520
|
-
// version.
|
|
521
|
-
{ name: "CAELO_GATEWAY_SERVICE", value: gatewaySvc.name },
|
|
522
|
-
{ name: "CAELO_GATEWAY_REGION", value: region },
|
|
523
|
-
// Issue #37 — shown in the /security/mcp `claude mcp add` command.
|
|
524
|
-
{ name: "CAELO_MCP_IAP_SERVICE_ACCOUNT", value: mcpServiceAccountEmail },
|
|
525
|
-
],
|
|
591
|
+
// Use the input string (known at config time) for the site — the
|
|
592
|
+
// resource output is Output<string | undefined>, which Input<string>
|
|
593
|
+
// rejects.
|
|
594
|
+
contractEnv: adminEnvContract({
|
|
595
|
+
...envContractInputs,
|
|
596
|
+
firebaseSiteId,
|
|
597
|
+
gatewayService: gatewaySvc.name,
|
|
598
|
+
}),
|
|
526
599
|
});
|
|
527
600
|
|
|
528
601
|
// =========================================================================
|
|
@@ -613,6 +686,11 @@ new gcp.cloudrunv2.ServiceIamMember(
|
|
|
613
686
|
// (`https://<admin-svc>-<hash>-<region>.a.run.app`). They can flip
|
|
614
687
|
// the knob + verify the domain at https://search.google.com/search-console
|
|
615
688
|
// later and run `pulumi up` to bind admin.<domain> → Cloud Run.
|
|
689
|
+
//
|
|
690
|
+
// The supported path is `cms-provision admin-domain enable`
|
|
691
|
+
// (src/admin-domain.ts): it creates the same mapping as the operator's own
|
|
692
|
+
// gcloud identity — the verified domain owner — which the provisioner SA
|
|
693
|
+
// running this stack never is. Leave this knob false after using it.
|
|
616
694
|
const provisionAdminDomain = cfg.getBoolean("provisionAdminDomain") ?? false;
|
|
617
695
|
const adminDomainMapping = provisionAdminDomain
|
|
618
696
|
? new gcp.cloudrun.DomainMapping(
|
|
@@ -737,7 +815,7 @@ const mcpServiceAccount = new gcp.serviceaccount.Account(
|
|
|
737
815
|
`${namePrefix}-mcp-sa`,
|
|
738
816
|
{
|
|
739
817
|
project,
|
|
740
|
-
accountId:
|
|
818
|
+
accountId: MCP_IAP_SERVICE_ACCOUNT_ID,
|
|
741
819
|
displayName: "Caelo MCP (IAP ingress)",
|
|
742
820
|
description:
|
|
743
821
|
"Signs the IAP credential external MCP clients use to reach the admin (issue #37).",
|
|
@@ -774,6 +852,20 @@ new gcp.iap.WebCloudRunServiceIamMember(
|
|
|
774
852
|
opts,
|
|
775
853
|
);
|
|
776
854
|
|
|
855
|
+
// =========================================================================
|
|
856
|
+
// Operator access — deliberately NOT here
|
|
857
|
+
// =========================================================================
|
|
858
|
+
//
|
|
859
|
+
// Who passes IAP beyond the allowlist above follows the Caelo user list. A
|
|
860
|
+
// Cloud Run job with its own service account keeps the IAP binding and the
|
|
861
|
+
// caelo-mcp token-creator binding in step (admin-core
|
|
862
|
+
// security/operator-access/sync-job.ts); the admin's run SA holds no IAM
|
|
863
|
+
// policy rights and may only start that job. The CLI owns the job, its
|
|
864
|
+
// account, its grants and its schedule (packages/provisioning/src/
|
|
865
|
+
// operator-access.ts, run by the wizard after migrations and by `upgrade`),
|
|
866
|
+
// because the job's database user can only be assigned its role after the
|
|
867
|
+
// migrations have created that role.
|
|
868
|
+
|
|
777
869
|
// =========================================================================
|
|
778
870
|
// Outputs — DNS records the operator wires manually
|
|
779
871
|
// =========================================================================
|
|
@@ -835,8 +927,9 @@ const tokenInfo = generateBootstrapToken();
|
|
|
835
927
|
// plain strings in stack outputs at runtime; the cast bridges the
|
|
836
928
|
// build-time type. Same pattern as packages/provisioning/stacks/gcp/index.ts.
|
|
837
929
|
const out: CloudAdapterOutputs = {
|
|
838
|
-
|
|
839
|
-
|
|
930
|
+
// Password-less — the password lives only in Secret Manager.
|
|
931
|
+
adminDatabaseUrl: dbUrls.admin as unknown as string,
|
|
932
|
+
publicDatabaseUrl: dbUrls.public as unknown as string,
|
|
840
933
|
mediaStorageUrl: pulumi.interpolate`gs://${mediaBucket.name}` as unknown as string,
|
|
841
934
|
// Firebase Hosting serves media via its own CDN; we proxy through
|
|
842
935
|
// the Firebase site's apex domain for canonical URLs.
|
|
@@ -25,3 +25,7 @@ config:
|
|
|
25
25
|
type: string
|
|
26
26
|
default: ./.caelo
|
|
27
27
|
description: Where the generated docker-compose.yml + Caddyfile + secrets live.
|
|
28
|
+
caelo-self-hosted:adminMemory:
|
|
29
|
+
type: string
|
|
30
|
+
default: "2Gi"
|
|
31
|
+
description: Memory reserved for the admin container (it runs the Lighthouse quality audit).
|
|
@@ -31,6 +31,8 @@ const cfg = new pulumi.Config();
|
|
|
31
31
|
const domain = cfg.require("domain");
|
|
32
32
|
const ownerEmail = cfg.require("ownerEmail");
|
|
33
33
|
const caeloDir = cfg.get("caeloDir") ?? "./.caelo";
|
|
34
|
+
// #553 — same knob as every adapter; undefined = stack-contract default.
|
|
35
|
+
const adminMemory = cfg.get("adminMemory");
|
|
34
36
|
|
|
35
37
|
const composePath = resolve(caeloDir, "docker-compose.yml");
|
|
36
38
|
const caddyPath = resolve(caeloDir, "Caddyfile");
|
|
@@ -52,6 +54,7 @@ const composeYaml = pulumi.all([postgresPassword, minioRootPassword]).apply(([pg
|
|
|
52
54
|
anthropicApiKey: process.env.ANTHROPIC_API_KEY,
|
|
53
55
|
resendApiKey: process.env.RESEND_API_KEY,
|
|
54
56
|
diskSize: "20Gi",
|
|
57
|
+
...(adminMemory ? { adminMemory } : {}),
|
|
55
58
|
}),
|
|
56
59
|
);
|
|
57
60
|
|