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