@caelo-cms/provisioning 0.3.8 → 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.8",
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.8",
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";
@@ -464,10 +465,24 @@ const adminSvc = cloudRunService({
464
465
  // v0.3.1 — Firebase Hosting uses a Google-managed service identity
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
- // The SA name follows the standard `service-<project-number>@
468
- // gcp-sa-firebasehosting.iam.gserviceaccount.com` shape. We fetch
469
- // the project number via `gcp.organizations.getProject` rather than
470
- // hardcoding it.
468
+ //
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(
479
+ `${namePrefix}-firebasehosting-identity`,
480
+ {
481
+ create: pulumi.interpolate`gcloud beta services identity create --service=firebasehosting.googleapis.com --project=${project} --quiet`,
482
+ },
483
+ opts,
484
+ );
485
+
471
486
  const projectInfo = gcp.organizations.getProjectOutput({ projectId: project }, opts);
472
487
  const firebaseHostingSa = pulumi.interpolate`serviceAccount:service-${projectInfo.number}@gcp-sa-firebasehosting.iam.gserviceaccount.com`;
473
488
 
@@ -480,25 +495,41 @@ new gcp.cloudrun.IamMember(
480
495
  role: "roles/run.invoker",
481
496
  member: firebaseHostingSa,
482
497
  },
483
- opts,
498
+ { ...opts, dependsOn: [firebaseHostingIdentityCmd] },
484
499
  );
485
500
 
486
501
  // =========================================================================
487
502
  // Admin custom domain — Cloud Run domain mapping
488
503
  // =========================================================================
489
-
490
- const adminDomainMapping = new gcp.cloudrun.DomainMapping(
491
- `${namePrefix}-admin-domain`,
492
- {
493
- location: region,
494
- name: adminDomain,
495
- metadata: { namespace: project },
496
- spec: {
497
- routeName: adminSvc.name,
498
- },
499
- },
500
- { ...opts, dependsOn: [adminSvc] },
501
- );
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;
502
533
 
503
534
  // =========================================================================
504
535
  // IAP on the admin Cloud Run service (no LB)
@@ -515,13 +546,19 @@ const adminDomainMapping = new gcp.cloudrun.DomainMapping(
515
546
  // shipped. If your provider build is older than v8.x, run
516
547
  // `gcloud beta run services update <admin-service> --iap` as a
517
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`.
518
555
  for (const principal of iapAllowlist) {
519
- new gcp.cloudrun.IamMember(
556
+ new gcp.cloudrunv2.ServiceIamMember(
520
557
  `${namePrefix}-admin-iap-${principal.replace(/[^a-z0-9]/gi, "-").slice(0, 40)}`,
521
558
  {
522
559
  location: region,
523
560
  project,
524
- service: adminSvc.name,
561
+ name: adminSvc.name,
525
562
  role: "roles/iap.httpsResourceAccessor",
526
563
  member: principal,
527
564
  },
@@ -533,12 +570,22 @@ for (const principal of iapAllowlist) {
533
570
  // Outputs — DNS records the operator wires manually
534
571
  // =========================================================================
535
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
+
536
581
  const dnsRecords: pulumi.Output<DnsRecord[]> = pulumi
537
- .all([adminDomainMapping.statuses, firebaseCustomDomain.requiredDnsUpdates])
582
+ .all([adminMappingStatuses, firebaseCustomDomain.requiredDnsUpdates])
538
583
  .apply(([adminStatuses, firebaseUpdates]) => {
539
584
  const out: DnsRecord[] = [];
540
585
 
541
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.
542
589
  const adminTargets =
543
590
  adminStatuses
544
591
  ?.flatMap((s) => s.resourceRecords ?? [])