@caelo-cms/provisioning 0.3.9 → 0.3.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@caelo-cms/provisioning",
3
- "version": "0.3.9",
3
+ "version": "0.3.10",
4
4
  "private": false,
5
5
  "license": "MPL-2.0",
6
6
  "description": "Pulumi-driven provisioning for Caelo CMS — `bunx @caelo-cms/provisioning --provider <self-hosted|gcp|aws|azure>` brings up a TLS-served install with Postgres + storage + secrets manager + the admin + the API gateway.",
@@ -38,7 +38,7 @@
38
38
  "prepublishOnly": "tsc -b --force && tsc --project stacks/gcp/tsconfig.json && tsc --project stacks/gcp-firebase/tsconfig.json"
39
39
  },
40
40
  "dependencies": {
41
- "@caelo-cms/shared": "^0.3.9",
41
+ "@caelo-cms/shared": "^0.3.10",
42
42
  "@clack/prompts": "1.3.0",
43
43
  "@pulumi/aws": "^7",
44
44
  "@pulumi/awsx": "^3",
@@ -25,6 +25,7 @@
25
25
  * Shared code lives in `packages/provisioning/src/` (imported here).
26
26
  */
27
27
 
28
+ import * as command from "@pulumi/command";
28
29
  import * as gcp from "@pulumi/gcp";
29
30
  import * as pulumi from "@pulumi/pulumi";
30
31
  import * as random from "@pulumi/random";
@@ -465,18 +466,26 @@ const adminSvc = cloudRunService({
465
466
  // to proxy traffic via `rewrites` to Cloud Run. Grant it
466
467
  // `roles/run.invoker` on the gateway so /api/** requests succeed.
467
468
  //
468
- // v0.3.9 — The Firebase Hosting service identity SA is created
469
- // on-demand, not when the API is merely enabled. Constructing the
470
- // email by name and granting IAM to it fails with "Service account
471
- // ... does not exist". `gcp.projects.ServiceIdentity` triggers
472
- // creation and returns the email; we depend on it so the binding
473
- // always sees the SA. Same pattern the gcp stack uses for IAP.
474
- const firebaseHostingIdentity = new gcp.projects.ServiceIdentity(
469
+ // v0.3.10 — `gcp.projects.ServiceIdentity` is NOT supported for
470
+ // `firebasehosting.googleapis.com` (the GenerateServiceIdentity API
471
+ // returns IAM_SERVICE_NOT_CONFIGURED_FOR_IDENTITIES for it). The
472
+ // `gcloud beta services identity create` CLI command works where
473
+ // the GenerateServiceIdentity API does not — it falls back to a
474
+ // different bootstrap path. We shell out via @pulumi/command,
475
+ // which is idempotent (gcloud returns existing if the SA is
476
+ // already provisioned). The SA email is deterministic from the
477
+ // project number, so we construct it independently.
478
+ const firebaseHostingIdentityCmd = new command.local.Command(
475
479
  `${namePrefix}-firebasehosting-identity`,
476
- { project, service: "firebasehosting.googleapis.com" },
480
+ {
481
+ create: pulumi.interpolate`gcloud beta services identity create --service=firebasehosting.googleapis.com --project=${project} --quiet`,
482
+ },
477
483
  opts,
478
484
  );
479
485
 
486
+ const projectInfo = gcp.organizations.getProjectOutput({ projectId: project }, opts);
487
+ const firebaseHostingSa = pulumi.interpolate`serviceAccount:service-${projectInfo.number}@gcp-sa-firebasehosting.iam.gserviceaccount.com`;
488
+
480
489
  new gcp.cloudrun.IamMember(
481
490
  `${namePrefix}-gateway-firebase-invoker`,
482
491
  {
@@ -484,27 +493,43 @@ new gcp.cloudrun.IamMember(
484
493
  project,
485
494
  service: gatewaySvc.name,
486
495
  role: "roles/run.invoker",
487
- member: pulumi.interpolate`serviceAccount:${firebaseHostingIdentity.email}`,
496
+ member: firebaseHostingSa,
488
497
  },
489
- { ...opts, dependsOn: [firebaseHostingIdentity] },
498
+ { ...opts, dependsOn: [firebaseHostingIdentityCmd] },
490
499
  );
491
500
 
492
501
  // =========================================================================
493
502
  // Admin custom domain — Cloud Run domain mapping
494
503
  // =========================================================================
495
-
496
- const adminDomainMapping = new gcp.cloudrun.DomainMapping(
497
- `${namePrefix}-admin-domain`,
498
- {
499
- location: region,
500
- name: adminDomain,
501
- metadata: { namespace: project },
502
- spec: {
503
- routeName: adminSvc.name,
504
- },
505
- },
506
- { ...opts, dependsOn: [adminSvc] },
507
- );
504
+ //
505
+ // v0.3.10 — Cloud Run DomainMapping requires the operator to verify
506
+ // domain ownership in Google Search Console FIRST. Without that
507
+ // verification the create fails with "Caller is not authorized to
508
+ // administer the domain ..." even when the provisioner SA has full
509
+ // admin rights on the project. This is a one-time manual step we
510
+ // cannot automate.
511
+ //
512
+ // To keep the install working out of the box, the DomainMapping is
513
+ // opt-in via `provisionAdminDomain=true`. When disabled (default),
514
+ // the operator reaches the admin via the Cloud Run-generated URL
515
+ // (`https://<admin-svc>-<hash>-<region>.a.run.app`). They can flip
516
+ // the knob + verify the domain at https://search.google.com/search-console
517
+ // later and run `pulumi up` to bind admin.<domain> → Cloud Run.
518
+ const provisionAdminDomain = cfg.getBoolean("provisionAdminDomain") ?? false;
519
+ const adminDomainMapping = provisionAdminDomain
520
+ ? new gcp.cloudrun.DomainMapping(
521
+ `${namePrefix}-admin-domain`,
522
+ {
523
+ location: region,
524
+ name: adminDomain,
525
+ metadata: { namespace: project },
526
+ spec: {
527
+ routeName: adminSvc.name,
528
+ },
529
+ },
530
+ { ...opts, dependsOn: [adminSvc] },
531
+ )
532
+ : undefined;
508
533
 
509
534
  // =========================================================================
510
535
  // IAP on the admin Cloud Run service (no LB)
@@ -521,13 +546,19 @@ const adminDomainMapping = new gcp.cloudrun.DomainMapping(
521
546
  // shipped. If your provider build is older than v8.x, run
522
547
  // `gcloud beta run services update <admin-service> --iap` as a
523
548
  // post-up step. The stack provisions the IAM bindings either way.
549
+ // v0.3.10 — Cloud Run v1 IAM API (`gcp.cloudrun.IamMember`) rejects
550
+ // `roles/iap.httpsResourceAccessor` with "Role is not supported for
551
+ // this resource". The v2 IAM API (`gcp.cloudrunv2.ServiceIamMember`)
552
+ // is the right binding for native-IAP-on-Cloud-Run, matching
553
+ // Google's documented setup: `gcloud run services
554
+ // add-iam-policy-binding ... --role=roles/iap.httpsResourceAccessor`.
524
555
  for (const principal of iapAllowlist) {
525
- new gcp.cloudrun.IamMember(
556
+ new gcp.cloudrunv2.ServiceIamMember(
526
557
  `${namePrefix}-admin-iap-${principal.replace(/[^a-z0-9]/gi, "-").slice(0, 40)}`,
527
558
  {
528
559
  location: region,
529
560
  project,
530
- service: adminSvc.name,
561
+ name: adminSvc.name,
531
562
  role: "roles/iap.httpsResourceAccessor",
532
563
  member: principal,
533
564
  },
@@ -539,12 +570,22 @@ for (const principal of iapAllowlist) {
539
570
  // Outputs — DNS records the operator wires manually
540
571
  // =========================================================================
541
572
 
573
+ const adminMappingStatuses: pulumi.Output<
574
+ gcp.types.output.cloudrun.DomainMappingStatus[] | undefined
575
+ > = adminDomainMapping
576
+ ? adminDomainMapping.statuses
577
+ : (pulumi.output(undefined) as pulumi.Output<
578
+ gcp.types.output.cloudrun.DomainMappingStatus[] | undefined
579
+ >);
580
+
542
581
  const dnsRecords: pulumi.Output<DnsRecord[]> = pulumi
543
- .all([adminDomainMapping.statuses, firebaseCustomDomain.requiredDnsUpdates])
582
+ .all([adminMappingStatuses, firebaseCustomDomain.requiredDnsUpdates])
544
583
  .apply(([adminStatuses, firebaseUpdates]) => {
545
584
  const out: DnsRecord[] = [];
546
585
 
547
586
  // Admin subdomain — Cloud Run domain mapping prints CNAMEs/As.
587
+ // When `provisionAdminDomain=false`, no mapping exists and the
588
+ // operator reaches admin via the *.run.app URL printed separately.
548
589
  const adminTargets =
549
590
  adminStatuses
550
591
  ?.flatMap((s) => s.resourceRecords ?? [])