@caelo-cms/provisioning 0.10.47 → 0.10.48
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/agent/apply.d.ts +45 -0
- package/dist/agent/apply.d.ts.map +1 -0
- package/dist/agent/apply.js +83 -0
- package/dist/agent/apply.js.map +1 -0
- package/dist/agent/cli-provenance.d.ts +42 -0
- package/dist/agent/cli-provenance.d.ts.map +1 -0
- package/dist/agent/cli-provenance.js +138 -0
- package/dist/agent/cli-provenance.js.map +1 -0
- package/dist/agent/command.d.ts +79 -0
- package/dist/agent/command.d.ts.map +1 -0
- package/dist/agent/command.js +454 -0
- package/dist/agent/command.js.map +1 -0
- package/dist/agent/config.d.ts +30 -0
- package/dist/agent/config.d.ts.map +1 -0
- package/dist/agent/config.js +60 -0
- package/dist/agent/config.js.map +1 -0
- package/dist/agent/lock.d.ts +13 -0
- package/dist/agent/lock.d.ts.map +1 -0
- package/dist/agent/lock.js +100 -0
- package/dist/agent/lock.js.map +1 -0
- package/dist/agent/queue.d.ts +35 -0
- package/dist/agent/queue.d.ts.map +1 -0
- package/dist/agent/queue.js +50 -0
- package/dist/agent/queue.js.map +1 -0
- package/dist/agent/run.d.ts +97 -0
- package/dist/agent/run.d.ts.map +1 -0
- package/dist/agent/run.js +198 -0
- package/dist/agent/run.js.map +1 -0
- package/dist/agent/systemd.d.ts +55 -0
- package/dist/agent/systemd.d.ts.map +1 -0
- package/dist/agent/systemd.js +171 -0
- package/dist/agent/systemd.js.map +1 -0
- package/dist/agent/units.d.ts +44 -0
- package/dist/agent/units.d.ts.map +1 -0
- package/dist/agent/units.js +127 -0
- package/dist/agent/units.js.map +1 -0
- package/dist/agent-db.js +18362 -0
- package/dist/cli.js +119 -16
- package/dist/cli.js.map +1 -1
- package/dist/cloudsql-iam-user.d.ts +39 -0
- package/dist/cloudsql-iam-user.d.ts.map +1 -0
- package/dist/cloudsql-iam-user.js +119 -0
- package/dist/cloudsql-iam-user.js.map +1 -0
- package/dist/compose.d.ts +16 -0
- package/dist/compose.d.ts.map +1 -1
- package/dist/compose.js +26 -1
- package/dist/compose.js.map +1 -1
- package/dist/dns/index.d.ts +2 -0
- package/dist/dns/index.d.ts.map +1 -1
- package/dist/dns/index.js +3 -2
- package/dist/dns/index.js.map +1 -1
- package/dist/dns/manual.d.ts +5 -0
- package/dist/dns/manual.d.ts.map +1 -1
- package/dist/dns/manual.js +28 -12
- package/dist/dns/manual.js.map +1 -1
- package/dist/gcloud.d.ts +2 -0
- package/dist/gcloud.d.ts.map +1 -1
- package/dist/gcloud.js +1 -1
- package/dist/gcloud.js.map +1 -1
- package/dist/gcp-names.d.ts +15 -0
- package/dist/gcp-names.d.ts.map +1 -1
- package/dist/gcp-names.js +23 -0
- package/dist/gcp-names.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/infra-contract.d.ts +1 -1
- package/dist/infra-contract.js +7 -1
- package/dist/infra-contract.js.map +1 -1
- package/dist/install-job-runner.d.ts +13 -0
- package/dist/install-job-runner.d.ts.map +1 -0
- package/dist/install-job-runner.js +155 -0
- package/dist/install-job-runner.js.map +1 -0
- package/dist/install-job.d.ts +210 -0
- package/dist/install-job.d.ts.map +1 -0
- package/dist/install-job.js +449 -0
- package/dist/install-job.js.map +1 -0
- package/dist/install-state.d.ts +10 -1
- package/dist/install-state.d.ts.map +1 -1
- package/dist/install-state.js +10 -2
- package/dist/install-state.js.map +1 -1
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +19 -4
- package/dist/lifecycle.js.map +1 -1
- package/dist/migration-runner.d.ts +5 -0
- package/dist/migration-runner.d.ts.map +1 -1
- package/dist/migration-runner.js +1 -1
- package/dist/migration-runner.js.map +1 -1
- package/dist/operator-access.d.ts +14 -8
- package/dist/operator-access.d.ts.map +1 -1
- package/dist/operator-access.js +16 -102
- package/dist/operator-access.js.map +1 -1
- package/dist/run-mode.d.ts +27 -0
- package/dist/run-mode.d.ts.map +1 -0
- package/dist/run-mode.js +22 -0
- package/dist/run-mode.js.map +1 -0
- package/dist/runtime-secrets.d.ts +19 -0
- package/dist/runtime-secrets.d.ts.map +1 -1
- package/dist/runtime-secrets.js +44 -0
- package/dist/runtime-secrets.js.map +1 -1
- package/dist/self-hosted-roles.d.ts +22 -1
- package/dist/self-hosted-roles.d.ts.map +1 -1
- package/dist/self-hosted-roles.js +81 -12
- package/dist/self-hosted-roles.js.map +1 -1
- package/dist/self-hosted-upgrade.d.ts +17 -0
- package/dist/self-hosted-upgrade.d.ts.map +1 -1
- package/dist/self-hosted-upgrade.js +16 -1
- package/dist/self-hosted-upgrade.js.map +1 -1
- package/dist/stack-contract.d.ts +6 -0
- package/dist/stack-contract.d.ts.map +1 -1
- package/dist/stack-contract.js +10 -1
- package/dist/stack-contract.js.map +1 -1
- package/dist/upgrade/converge.d.ts +6 -0
- package/dist/upgrade/converge.d.ts.map +1 -1
- package/dist/upgrade/converge.js +59 -2
- package/dist/upgrade/converge.js.map +1 -1
- package/dist/upgrade/gcp-lookup.d.ts +6 -0
- package/dist/upgrade/gcp-lookup.d.ts.map +1 -1
- package/dist/upgrade/gcp-lookup.js +25 -0
- package/dist/upgrade/gcp-lookup.js.map +1 -1
- package/dist/upgrade/release-images.d.ts +12 -0
- package/dist/upgrade/release-images.d.ts.map +1 -1
- package/dist/upgrade/release-images.js +31 -0
- package/dist/upgrade/release-images.js.map +1 -1
- package/dist/upgrade/upgrade.d.ts +5 -0
- package/dist/upgrade/upgrade.d.ts.map +1 -1
- package/dist/upgrade/upgrade.js +28 -1
- package/dist/upgrade/upgrade.js.map +1 -1
- package/dist/upgrader-assess.d.ts +82 -0
- package/dist/upgrader-assess.d.ts.map +1 -0
- package/dist/upgrader-assess.js +116 -0
- package/dist/upgrader-assess.js.map +1 -0
- package/dist/upgrader-gcp.d.ts +17 -0
- package/dist/upgrader-gcp.d.ts.map +1 -0
- package/dist/upgrader-gcp.js +87 -0
- package/dist/upgrader-gcp.js.map +1 -0
- package/dist/upgrader-job.d.ts +131 -0
- package/dist/upgrader-job.d.ts.map +1 -0
- package/dist/upgrader-job.js +259 -0
- package/dist/upgrader-job.js.map +1 -0
- package/dist/upgrader-reporter.d.ts +35 -0
- package/dist/upgrader-reporter.d.ts.map +1 -0
- package/dist/upgrader-reporter.js +79 -0
- package/dist/upgrader-reporter.js.map +1 -0
- package/dist/upgrader-setup.d.ts +59 -0
- package/dist/upgrader-setup.d.ts.map +1 -0
- package/dist/upgrader-setup.js +308 -0
- package/dist/upgrader-setup.js.map +1 -0
- package/dist/wizard.d.ts +6 -0
- package/dist/wizard.d.ts.map +1 -1
- package/dist/wizard.js +32 -4
- package/dist/wizard.js.map +1 -1
- package/dist/wizards/gcp-firebase.d.ts +3 -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-install-job.d.ts +74 -0
- package/dist/wizards/gcp-install-job.d.ts.map +1 -0
- package/dist/wizards/gcp-install-job.js +398 -0
- package/dist/wizards/gcp-install-job.js.map +1 -0
- package/dist/wizards/gcp-prompts.d.ts +3 -0
- package/dist/wizards/gcp-prompts.d.ts.map +1 -0
- package/dist/wizards/gcp-prompts.js +25 -0
- package/dist/wizards/gcp-prompts.js.map +1 -0
- package/dist/wizards/gcp-pulumi.d.ts +4 -2
- package/dist/wizards/gcp-pulumi.d.ts.map +1 -1
- package/dist/wizards/gcp-pulumi.js +1 -1
- package/dist/wizards/gcp-pulumi.js.map +1 -1
- package/dist/wizards/gcp.d.ts +40 -0
- package/dist/wizards/gcp.d.ts.map +1 -1
- package/dist/wizards/gcp.js +223 -59
- package/dist/wizards/gcp.js.map +1 -1
- package/package.json +6 -4
- package/src/adapter.ts +97 -0
- package/src/adc-impersonation.ts +176 -0
- package/src/admin-domain.test.ts +254 -0
- package/src/admin-domain.ts +245 -0
- package/src/admin-memory.test.ts +151 -0
- package/src/agent/apply.test.ts +185 -0
- package/src/agent/apply.ts +124 -0
- package/src/agent/cli-provenance.test.ts +159 -0
- package/src/agent/cli-provenance.ts +191 -0
- package/src/agent/command.test.ts +77 -0
- package/src/agent/command.ts +564 -0
- package/src/agent/config.ts +86 -0
- package/src/agent/lock-config.test.ts +97 -0
- package/src/agent/lock.ts +110 -0
- package/src/agent/queue.test.ts +10 -0
- package/src/agent/queue.ts +91 -0
- package/src/agent/run.test.ts +382 -0
- package/src/agent/run.ts +295 -0
- package/src/agent/systemd.ts +209 -0
- package/src/agent/units.test.ts +223 -0
- package/src/agent/units.ts +163 -0
- package/src/azure-front-door.test.ts +119 -0
- package/src/azure-front-door.ts +56 -0
- package/src/bootstrap-token.ts +20 -0
- package/src/caddy.ts +112 -0
- package/src/cdn-copy.ts +91 -0
- package/src/cert-wait.test.ts +194 -0
- package/src/cert-wait.ts +163 -0
- package/src/checkpoint-secrets.test.ts +380 -0
- package/src/checkpoint-secrets.ts +283 -0
- package/src/cli-routing.test.ts +45 -0
- package/src/cli-routing.ts +43 -0
- package/src/cli.ts +1491 -0
- package/src/cloudsql-iam-user.ts +147 -0
- package/src/compose.ts +334 -0
- package/src/default-firewall.test.ts +411 -0
- package/src/default-firewall.ts +342 -0
- package/src/deployed-release.test.ts +231 -0
- package/src/deployed-release.ts +220 -0
- package/src/dns/cloudflare.ts +182 -0
- package/src/dns/groups.test.ts +76 -0
- package/src/dns/groups.ts +86 -0
- package/src/dns/index.ts +56 -0
- package/src/dns/manual.ts +130 -0
- package/src/dns/types.ts +27 -0
- package/src/gateway-credentials.test.ts +180 -0
- package/src/gateway-credentials.ts +144 -0
- package/src/gcloud-components.test.ts +76 -0
- package/src/gcloud-components.ts +85 -0
- package/src/gcloud-retry.ts +41 -0
- package/src/gcloud-runner.test.ts +61 -0
- package/src/gcloud.test.ts +99 -0
- package/src/gcloud.ts +322 -0
- package/src/gcp-names.ts +135 -0
- package/src/index.test.ts +332 -0
- package/src/index.ts +114 -0
- package/src/infra-contract.ts +32 -0
- package/src/install-job-runner.ts +166 -0
- package/src/install-job.test.ts +566 -0
- package/src/install-job.ts +542 -0
- package/src/install-state.test.ts +239 -0
- package/src/install-state.ts +583 -0
- package/src/internal-token.ts +35 -0
- package/src/lifecycle.ts +900 -0
- package/src/mcp-iap-stacks.test.ts +58 -0
- package/src/mcp-iap.test.ts +149 -0
- package/src/mcp-iap.ts +131 -0
- package/src/media-volume.test.ts +296 -0
- package/src/migration-runner.ts +365 -0
- package/src/operator-access-stacks.test.ts +55 -0
- package/src/operator-access.test.ts +332 -0
- package/src/operator-access.ts +554 -0
- package/src/ops-backend.test.ts +320 -0
- package/src/ops-backend.ts +414 -0
- package/src/provisioner-identity.test.ts +482 -0
- package/src/provisioner-identity.ts +407 -0
- package/src/redirects-emit.ts +175 -0
- package/src/region-data.ts +428 -0
- package/src/regions.test.ts +277 -0
- package/src/regions.ts +370 -0
- package/src/release-contract.ts +57 -0
- package/src/release-manifest.test.ts +293 -0
- package/src/release-manifest.ts +430 -0
- package/src/run-mode.ts +40 -0
- package/src/runtime-secrets.test.ts +627 -0
- package/src/runtime-secrets.ts +754 -0
- package/src/self-command.test.ts +14 -0
- package/src/self-command.ts +21 -0
- package/src/self-hosted-compose.test.ts +177 -0
- package/src/self-hosted-release.ts +240 -0
- package/src/self-hosted-roles.integration.test.ts +161 -0
- package/src/self-hosted-roles.test.ts +124 -0
- package/src/self-hosted-roles.ts +203 -0
- package/src/self-hosted-upgrade.test.ts +394 -0
- package/src/self-hosted-upgrade.ts +548 -0
- package/src/stack-contract-parity.test.ts +199 -0
- package/src/stack-contract.test.ts +391 -0
- package/src/stack-contract.ts +888 -0
- package/src/stack-converge-secrets.test.ts +421 -0
- package/src/stack-converge.test.ts +833 -0
- package/src/stack-converge.ts +909 -0
- package/src/staging-admin.test.ts +246 -0
- package/src/staging-admin.ts +324 -0
- package/src/state-attach.test.ts +211 -0
- package/src/state-attach.ts +132 -0
- package/src/state-backend.test.ts +145 -0
- package/src/state-backend.ts +262 -0
- package/src/state-command.ts +293 -0
- package/src/state-migrate.test.ts +479 -0
- package/src/state-migrate.ts +434 -0
- package/src/state-pulumi.ts +289 -0
- package/src/state-store.test.ts +212 -0
- package/src/state-store.ts +243 -0
- package/src/upgrade/app-roll.ts +486 -0
- package/src/upgrade/backup.ts +109 -0
- package/src/upgrade/converge.ts +437 -0
- package/src/upgrade/gcp-lookup.ts +167 -0
- package/src/upgrade/health.test.ts +279 -0
- package/src/upgrade/health.ts +305 -0
- package/src/upgrade/release-images.test.ts +119 -0
- package/src/upgrade/release-images.ts +391 -0
- package/src/upgrade/reporter.ts +46 -0
- package/src/upgrade/upgrade.test.ts +626 -0
- package/src/upgrade/upgrade.ts +249 -0
- package/src/upgrader-assess.test.ts +265 -0
- package/src/upgrader-assess.ts +233 -0
- package/src/upgrader-gcp.ts +123 -0
- package/src/upgrader-job.test.ts +408 -0
- package/src/upgrader-job.ts +499 -0
- package/src/upgrader-reporter.test.ts +46 -0
- package/src/upgrader-reporter.ts +105 -0
- package/src/upgrader-setup.test.ts +169 -0
- package/src/upgrader-setup.ts +355 -0
- package/src/wizard.ts +425 -0
- package/src/wizards/gcp-cost.ts +188 -0
- package/src/wizards/gcp-firebase.ts +72 -0
- package/src/wizards/gcp-install-job.ts +506 -0
- package/src/wizards/gcp-prompts.ts +26 -0
- package/src/wizards/gcp-pulumi.test.ts +246 -0
- package/src/wizards/gcp-pulumi.ts +516 -0
- package/src/wizards/gcp.ts +1935 -0
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The install job (epic #696, Phase 3): the long half of a GCP install runs
|
|
5
|
+
* as a Cloud Run job in the install's own project instead of in the
|
|
6
|
+
* operator's terminal, so closing the Cloud Shell tab (or its 40-minute idle
|
|
7
|
+
* timeout) never breaks an install.
|
|
8
|
+
*
|
|
9
|
+
* Split of work:
|
|
10
|
+
* - The operator's terminal (Cloud Shell, or a laptop with
|
|
11
|
+
* `--install-job`) does everything that needs the operator's own Google
|
|
12
|
+
* identity: project, billing, APIs, the keyless provisioner SA, the ops
|
|
13
|
+
* bucket + KMS key, the cost table and its y/N. Then it starts the job
|
|
14
|
+
* and tails it.
|
|
15
|
+
* - The job — `caelo-ops install-job --project <id>` from the signed ops
|
|
16
|
+
* image, pinned by the digest the signed release manifest names, running
|
|
17
|
+
* AS `caelo-provisioner` (its runtime service account: no key, no
|
|
18
|
+
* impersonation) — runs `pulumi up`, the DNS + certificate waits,
|
|
19
|
+
* migrations, the operator-access job and the Owner setup link.
|
|
20
|
+
*
|
|
21
|
+
* Progress lives in the install's ops bucket next to install.json:
|
|
22
|
+
* `install/install-job.json` ({@link InstallJobStatus}) says which execution
|
|
23
|
+
* runs the install and how far it got; progress.json keeps the per-step
|
|
24
|
+
* checkpoints, so a restarted execution resumes where the last one stopped.
|
|
25
|
+
* Re-running the launcher reads the status and attaches to a running
|
|
26
|
+
* execution, shows a finished one, or starts a new one ({@link decideInstallJob}).
|
|
27
|
+
*
|
|
28
|
+
* The Owner setup link is a secret (#656): it never reaches the bucket or
|
|
29
|
+
* the job's logs. The job stores it in Secret Manager
|
|
30
|
+
* ({@link INSTALL_SETUP_LINK_SECRET}); the operator's terminal reads it there.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { z } from "zod";
|
|
34
|
+
import { assertNoSecrets } from "./checkpoint-secrets.js";
|
|
35
|
+
import type { GcloudRunner } from "./gcloud-retry.js";
|
|
36
|
+
import { GCP_STACK_ENV } from "./gcp-names.js";
|
|
37
|
+
import { createInstallSecret } from "./runtime-secrets.js";
|
|
38
|
+
import { OPS_INSTALL_PREFIX } from "./state-backend.js";
|
|
39
|
+
import type { SyncGcloud } from "./state-store.js";
|
|
40
|
+
import { IMAGE_DIGEST } from "./upgrade/release-images.js";
|
|
41
|
+
|
|
42
|
+
/** Name of the Cloud Run job that runs the install. */
|
|
43
|
+
export const INSTALL_JOB_NAME = "caelo-install";
|
|
44
|
+
|
|
45
|
+
/** Secret Manager secret the job stores the Owner setup link in. */
|
|
46
|
+
export const INSTALL_SETUP_LINK_SECRET = "caelo-install-setup-link";
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Secret Manager secret holding the operator's `CLOUDFLARE_API_TOKEN` for
|
|
50
|
+
* the job's automatic DNS (dns/cloudflare.ts). Written by the launcher only
|
|
51
|
+
* when the operator's shell has the token; the job reads it as an env var
|
|
52
|
+
* mounted from the secret, so it never appears in the job definition.
|
|
53
|
+
*/
|
|
54
|
+
export const INSTALL_CLOUDFLARE_TOKEN_SECRET = "caelo-install-cloudflare-token";
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Repository of the `caelo-ops` image on the public Artifact Registry
|
|
58
|
+
* mirror. Cloud Run pulls from Artifact Registry, not from GHCR; the mirror
|
|
59
|
+
* carries the same digests and signatures (release-images.yml, `oras copy -r`).
|
|
60
|
+
*/
|
|
61
|
+
export const OPS_IMAGE_REPOSITORY =
|
|
62
|
+
"europe-west1-docker.pkg.dev/caelo-website/caelo-cms-images/ops";
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Roles the provisioner needs on top of {@link PROVISIONER_ROLE_LIST} when it
|
|
66
|
+
* runs the install job instead of the operator: the operator-access sync job
|
|
67
|
+
* creates a project custom role and a Cloud Scheduler job (operator-access.ts),
|
|
68
|
+
* and the job's own output has to reach Cloud Logging so the terminal can
|
|
69
|
+
* tail it. Granted only to installs that use the job. Not an escalation: the
|
|
70
|
+
* provisioner already holds `resourcemanager.projectIamAdmin`.
|
|
71
|
+
*/
|
|
72
|
+
export const INSTALL_JOB_ROLES: readonly string[] = [
|
|
73
|
+
"roles/iam.roleAdmin",
|
|
74
|
+
"roles/cloudscheduler.admin",
|
|
75
|
+
"roles/logging.logWriter",
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
/** Resources the job runs with: Pulumi + the Node language host want memory. */
|
|
79
|
+
const JOB_CPU = "2";
|
|
80
|
+
const JOB_MEMORY = "4Gi";
|
|
81
|
+
/** DNS (operator at the registrar) + certificate waits can each take an hour. */
|
|
82
|
+
const JOB_TASK_TIMEOUT = "6h";
|
|
83
|
+
|
|
84
|
+
/** The ops image pinned by digest. */
|
|
85
|
+
export function opsImageRef(digest: string): string {
|
|
86
|
+
if (!IMAGE_DIGEST.test(digest)) throw new Error(`not an image digest: ${digest}`);
|
|
87
|
+
return `${OPS_IMAGE_REPOSITORY}@${digest}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** What `install/install-job.json` records. */
|
|
91
|
+
export const InstallJobStatusSchema = z.object({
|
|
92
|
+
statusVersion: z.literal(1),
|
|
93
|
+
installId: z.string().min(1),
|
|
94
|
+
/** The Cloud Run execution running (or last running) the install. */
|
|
95
|
+
execution: z.string().min(1),
|
|
96
|
+
state: z.enum(["running", "succeeded", "failed"]),
|
|
97
|
+
/** Human-readable step the job is on, e.g. "Waiting for DNS". */
|
|
98
|
+
phase: z.string(),
|
|
99
|
+
/** Extra context for the phase (the DNS records to set, a failure reason). */
|
|
100
|
+
detail: z.string().optional(),
|
|
101
|
+
/** Caelo release the job's image belongs to. */
|
|
102
|
+
caeloVersion: z.string(),
|
|
103
|
+
startedAt: z.string(),
|
|
104
|
+
updatedAt: z.string(),
|
|
105
|
+
});
|
|
106
|
+
export type InstallJobStatus = z.infer<typeof InstallJobStatusSchema>;
|
|
107
|
+
|
|
108
|
+
/** `gs://<bucket>/install/install-job.json`. */
|
|
109
|
+
export function installJobStatusUrl(bucket: string): string {
|
|
110
|
+
return `gs://${bucket}/${OPS_INSTALL_PREFIX}/install-job.json`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const NOT_FOUND = /matched no objects|No URLs matched|NotFound|NOT_FOUND|404|does not exist/i;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The install job's status as the ops bucket holds it now (never cached:
|
|
117
|
+
* another process — the job — writes it), or null before the first start.
|
|
118
|
+
* Throws when the bucket can't be read or the document is malformed.
|
|
119
|
+
*/
|
|
120
|
+
export function readInstallJobStatus(bucket: string, run: SyncGcloud): InstallJobStatus | null {
|
|
121
|
+
const url = installJobStatusUrl(bucket);
|
|
122
|
+
const r = run(["storage", "cat", url]);
|
|
123
|
+
if (!r.ok) {
|
|
124
|
+
if (NOT_FOUND.test(r.stderr)) return null;
|
|
125
|
+
throw new Error(`Could not read ${url}: ${r.stderr.trim() || `gcloud exited ${r.exitCode}`}`);
|
|
126
|
+
}
|
|
127
|
+
let json: unknown;
|
|
128
|
+
try {
|
|
129
|
+
json = JSON.parse(r.stdout);
|
|
130
|
+
} catch {
|
|
131
|
+
throw new Error(`${url} is not valid JSON; remove it and re-run to start the install again.`);
|
|
132
|
+
}
|
|
133
|
+
const parsed = InstallJobStatusSchema.safeParse(json);
|
|
134
|
+
if (!parsed.success) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
`${url} is not an install-job status this CLI understands (${parsed.error.message}); remove it and re-run.`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
return parsed.data;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Replace the status document. Refuses secret material (#656). */
|
|
143
|
+
export function writeInstallJobStatus(
|
|
144
|
+
bucket: string,
|
|
145
|
+
status: InstallJobStatus,
|
|
146
|
+
run: SyncGcloud,
|
|
147
|
+
): void {
|
|
148
|
+
const parsed = InstallJobStatusSchema.parse(status);
|
|
149
|
+
assertNoSecrets(parsed, "install-job.json");
|
|
150
|
+
const url = installJobStatusUrl(bucket);
|
|
151
|
+
const r = run(
|
|
152
|
+
["storage", "cp", "-", url, "--content-type=application/json"],
|
|
153
|
+
`${JSON.stringify(parsed, null, 2)}\n`,
|
|
154
|
+
);
|
|
155
|
+
if (!r.ok) {
|
|
156
|
+
throw new Error(`Could not write ${url}: ${r.stderr.trim() || `gcloud exited ${r.exitCode}`}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Write `status` only if the document doesn't name its execution yet and
|
|
162
|
+
* nobody wrote it since this call read it (`--if-generation-match`). The
|
|
163
|
+
* launcher records a just-started execution this way: the execution itself
|
|
164
|
+
* may already have written a newer phase — or its failure — which must win.
|
|
165
|
+
*
|
|
166
|
+
* @returns whether this call's write landed
|
|
167
|
+
*/
|
|
168
|
+
export function claimInstallJobStatus(
|
|
169
|
+
bucket: string,
|
|
170
|
+
status: InstallJobStatus,
|
|
171
|
+
run: SyncGcloud,
|
|
172
|
+
): boolean {
|
|
173
|
+
const url = installJobStatusUrl(bucket);
|
|
174
|
+
const described = run(["storage", "objects", "describe", url, "--format=value(generation)"]);
|
|
175
|
+
let generation: string;
|
|
176
|
+
if (described.ok) generation = described.stdout.trim();
|
|
177
|
+
else if (NOT_FOUND.test(described.stderr)) generation = "0";
|
|
178
|
+
else throw new Error(`Could not read ${url}: ${described.stderr.trim()}`);
|
|
179
|
+
if (!/^\d+$/.test(generation)) throw new Error(`${url}: unexpected generation "${generation}"`);
|
|
180
|
+
if (readInstallJobStatus(bucket, run)?.execution === status.execution) return false;
|
|
181
|
+
const parsed = InstallJobStatusSchema.parse(status);
|
|
182
|
+
assertNoSecrets(parsed, "install-job.json");
|
|
183
|
+
const r = run(
|
|
184
|
+
[
|
|
185
|
+
"storage",
|
|
186
|
+
"cp",
|
|
187
|
+
"-",
|
|
188
|
+
url,
|
|
189
|
+
"--content-type=application/json",
|
|
190
|
+
`--if-generation-match=${generation}`,
|
|
191
|
+
],
|
|
192
|
+
`${JSON.stringify(parsed, null, 2)}\n`,
|
|
193
|
+
);
|
|
194
|
+
if (r.ok) return true;
|
|
195
|
+
if (/PreconditionFailed|412|pre-conditions/i.test(r.stderr)) return false;
|
|
196
|
+
throw new Error(`Could not write ${url}: ${r.stderr.trim() || `gcloud exited ${r.exitCode}`}`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** `gs://<bucket>/install/install-job.lock` — held while a launcher decides and starts. */
|
|
200
|
+
export function installJobLockUrl(bucket: string): string {
|
|
201
|
+
return `gs://${bucket}/${OPS_INSTALL_PREFIX}/install-job.lock`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Take the launch lock (created only if absent). A lock left by a crashed
|
|
206
|
+
* launcher is reported with the command to remove it, never broken
|
|
207
|
+
* automatically — like the install's state lock (state-store.ts).
|
|
208
|
+
*
|
|
209
|
+
* @returns the release function
|
|
210
|
+
*/
|
|
211
|
+
export function acquireLaunchLock(bucket: string, run: SyncGcloud): () => void {
|
|
212
|
+
const url = installJobLockUrl(bucket);
|
|
213
|
+
const body = JSON.stringify({ purpose: "start the install job", at: new Date().toISOString() });
|
|
214
|
+
const r = run(["storage", "cp", "-", url, "--if-generation-match=0"], body);
|
|
215
|
+
if (!r.ok) {
|
|
216
|
+
if (/PreconditionFailed|412|pre-conditions/i.test(r.stderr)) {
|
|
217
|
+
throw new Error(
|
|
218
|
+
`Another terminal is starting this install's job right now. Re-run in a minute to follow it; if no other run is starting it, remove the stale lock: gcloud storage rm ${url}`,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
throw new Error(`Could not take the launch lock ${url}: ${r.stderr.trim()}`);
|
|
222
|
+
}
|
|
223
|
+
return () => {
|
|
224
|
+
const rm = run(["storage", "rm", url]);
|
|
225
|
+
if (!rm.ok) throw new Error(`Could not release ${url}; remove it: gcloud storage rm ${url}`);
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Where a Cloud Run execution stands. */
|
|
230
|
+
export type ExecutionState = "running" | "succeeded" | "failed" | "missing";
|
|
231
|
+
|
|
232
|
+
interface ExecutionDescription {
|
|
233
|
+
status?: {
|
|
234
|
+
completionTime?: string;
|
|
235
|
+
succeededCount?: number;
|
|
236
|
+
failedCount?: number;
|
|
237
|
+
cancelledCount?: number;
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Read one execution of the install job. */
|
|
242
|
+
export async function describeExecution(
|
|
243
|
+
run: GcloudRunner,
|
|
244
|
+
target: { projectId: string; region: string; execution: string },
|
|
245
|
+
): Promise<ExecutionState> {
|
|
246
|
+
const r = await run([
|
|
247
|
+
"run",
|
|
248
|
+
"jobs",
|
|
249
|
+
"executions",
|
|
250
|
+
"describe",
|
|
251
|
+
target.execution,
|
|
252
|
+
`--region=${target.region}`,
|
|
253
|
+
`--project=${target.projectId}`,
|
|
254
|
+
"--format=json",
|
|
255
|
+
]);
|
|
256
|
+
if (!r.ok) {
|
|
257
|
+
if (NOT_FOUND.test(r.stderr)) return "missing";
|
|
258
|
+
throw new Error(
|
|
259
|
+
`Could not read execution ${target.execution}: ${r.stderr.trim() || `gcloud exited ${r.exitCode}`}`,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
const d = JSON.parse(r.stdout || "{}") as ExecutionDescription;
|
|
263
|
+
const s = d.status ?? {};
|
|
264
|
+
if (!s.completionTime) return "running";
|
|
265
|
+
return (s.succeededCount ?? 0) >= 1 && !s.failedCount && !s.cancelledCount
|
|
266
|
+
? "succeeded"
|
|
267
|
+
: "failed";
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** What the launcher does with an install that may already have a job. */
|
|
271
|
+
export type InstallJobDecision =
|
|
272
|
+
| { readonly kind: "start"; readonly previous: InstallJobStatus | null; readonly why: string }
|
|
273
|
+
| { readonly kind: "attach"; readonly status: InstallJobStatus }
|
|
274
|
+
| { readonly kind: "done"; readonly status: InstallJobStatus };
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Decide from the bucket's status and the execution's live state. Only a
|
|
278
|
+
* status the job itself finished as `succeeded` counts as done; a running
|
|
279
|
+
* status whose execution has ended (crash, out of memory, cancelled, task
|
|
280
|
+
* timeout) is started again — the steps are checkpointed, so a new
|
|
281
|
+
* execution resumes where the last one stopped.
|
|
282
|
+
*/
|
|
283
|
+
export function decideInstallJob(
|
|
284
|
+
status: InstallJobStatus | null,
|
|
285
|
+
live: ExecutionState | null,
|
|
286
|
+
): InstallJobDecision {
|
|
287
|
+
if (!status) return { kind: "start", previous: null, why: "no install job has run yet" };
|
|
288
|
+
if (status.state === "succeeded") return { kind: "done", status };
|
|
289
|
+
if (status.state === "running" && live === "running") return { kind: "attach", status };
|
|
290
|
+
if (status.state === "failed") {
|
|
291
|
+
return {
|
|
292
|
+
kind: "start",
|
|
293
|
+
previous: status,
|
|
294
|
+
why: `the last run stopped at "${status.phase}"${status.detail ? `: ${status.detail}` : ""}`,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
kind: "start",
|
|
299
|
+
previous: status,
|
|
300
|
+
why: `execution ${status.execution} ended (${live ?? "unknown"}) while at "${status.phase}"`,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Everything the job definition depends on. */
|
|
305
|
+
export interface InstallJobSpec {
|
|
306
|
+
readonly projectId: string;
|
|
307
|
+
readonly region: string;
|
|
308
|
+
/** `caelo-provisioner@<project>` — the job's runtime identity. */
|
|
309
|
+
readonly serviceAccount: string;
|
|
310
|
+
/** {@link opsImageRef} of the release's signed ops image. */
|
|
311
|
+
readonly image: string;
|
|
312
|
+
/**
|
|
313
|
+
* Mount {@link INSTALL_CLOUDFLARE_TOKEN_SECRET} as `CLOUDFLARE_API_TOKEN`
|
|
314
|
+
* (the launcher stored the operator's token there); without it the job's
|
|
315
|
+
* DNS step prints the records and polls.
|
|
316
|
+
*/
|
|
317
|
+
readonly cloudflareToken: boolean;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* The `gcloud run jobs deploy` argv: creates the job or replaces its
|
|
322
|
+
* definition (image, args, identity) in place. The only argument the job
|
|
323
|
+
* gets is the project; everything else it reads from the ops bucket.
|
|
324
|
+
*/
|
|
325
|
+
export function installJobDeployArgs(spec: InstallJobSpec): string[] {
|
|
326
|
+
return [
|
|
327
|
+
"run",
|
|
328
|
+
"jobs",
|
|
329
|
+
"deploy",
|
|
330
|
+
INSTALL_JOB_NAME,
|
|
331
|
+
`--image=${spec.image}`,
|
|
332
|
+
`--region=${spec.region}`,
|
|
333
|
+
`--project=${spec.projectId}`,
|
|
334
|
+
`--service-account=${spec.serviceAccount}`,
|
|
335
|
+
`--args=install-job,--project,${spec.projectId}`,
|
|
336
|
+
"--tasks=1",
|
|
337
|
+
"--max-retries=0",
|
|
338
|
+
`--task-timeout=${JOB_TASK_TIMEOUT}`,
|
|
339
|
+
`--cpu=${JOB_CPU}`,
|
|
340
|
+
`--memory=${JOB_MEMORY}`,
|
|
341
|
+
// Plain log lines: no colour codes, no spinner frames.
|
|
342
|
+
"--set-env-vars=NO_COLOR=1,CI=true",
|
|
343
|
+
spec.cloudflareToken
|
|
344
|
+
? `--set-secrets=CLOUDFLARE_API_TOKEN=${INSTALL_CLOUDFLARE_TOKEN_SECRET}:latest`
|
|
345
|
+
: "--clear-secrets",
|
|
346
|
+
"--labels=caelo-role=install",
|
|
347
|
+
"--quiet",
|
|
348
|
+
];
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** Create or update the job, then start one execution of it. */
|
|
352
|
+
export async function startInstallJob(
|
|
353
|
+
run: GcloudRunner,
|
|
354
|
+
spec: InstallJobSpec,
|
|
355
|
+
): Promise<{ ok: true; execution: string } | { ok: false; error: string }> {
|
|
356
|
+
const deploy = await run(installJobDeployArgs(spec));
|
|
357
|
+
if (!deploy.ok) {
|
|
358
|
+
return { ok: false, error: `create the install job: ${deploy.stderr.trim()}` };
|
|
359
|
+
}
|
|
360
|
+
const exec = await run([
|
|
361
|
+
"run",
|
|
362
|
+
"jobs",
|
|
363
|
+
"execute",
|
|
364
|
+
INSTALL_JOB_NAME,
|
|
365
|
+
`--region=${spec.region}`,
|
|
366
|
+
`--project=${spec.projectId}`,
|
|
367
|
+
"--async",
|
|
368
|
+
"--format=value(metadata.name)",
|
|
369
|
+
]);
|
|
370
|
+
if (!exec.ok) return { ok: false, error: `start the install job: ${exec.stderr.trim()}` };
|
|
371
|
+
// gcloud prints the resource with --format on stdout; older releases only
|
|
372
|
+
// announce "Execution [name] has successfully started" on stderr.
|
|
373
|
+
const execution =
|
|
374
|
+
exec.stdout.trim().split(/\s+/)[0] || /Execution \[([^\]]+)\]/.exec(exec.stderr)?.[1] || "";
|
|
375
|
+
if (!/^[a-z0-9-]+$/.test(execution)) {
|
|
376
|
+
return {
|
|
377
|
+
ok: false,
|
|
378
|
+
error: `the install job started but gcloud did not name the execution (${exec.stderr.trim()})`,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
return { ok: true, execution };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** `since` minus the lookback window, as an RFC 3339 timestamp. */
|
|
385
|
+
function lookback(since: string): string {
|
|
386
|
+
const t = Date.parse(since);
|
|
387
|
+
if (Number.isNaN(t)) throw new Error(`not a log timestamp: ${since}`);
|
|
388
|
+
return new Date(t - LOG_LOOKBACK_MS).toISOString();
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** One log line of the job, as Cloud Logging returns it. */
|
|
392
|
+
interface LogEntry {
|
|
393
|
+
insertId?: string;
|
|
394
|
+
timestamp?: string;
|
|
395
|
+
textPayload?: string;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* How far before the newest line seen each poll looks again. Cloud Logging
|
|
400
|
+
* can make an entry queryable after a later-timestamped one; the overlap
|
|
401
|
+
* (deduplicated by insert id) lets such a late entry still reach the
|
|
402
|
+
* terminal.
|
|
403
|
+
*/
|
|
404
|
+
const LOG_LOOKBACK_MS = 2 * 60 * 1000;
|
|
405
|
+
/** Entries per `gcloud logging read` page, and pages per poll. */
|
|
406
|
+
const LOG_PAGE_SIZE = 1000;
|
|
407
|
+
const LOG_MAX_PAGES = 20;
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* The execution's log lines not printed yet, oldest first, looking back
|
|
411
|
+
* {@link LOG_LOOKBACK_MS} before `since` (the newest timestamp seen so far,
|
|
412
|
+
* or null for all). `seen` holds the insert ids already printed — the
|
|
413
|
+
* overlap and entries sharing a timestamp would otherwise repeat.
|
|
414
|
+
*/
|
|
415
|
+
export async function readExecutionLogs(
|
|
416
|
+
run: GcloudRunner,
|
|
417
|
+
target: { projectId: string; execution: string },
|
|
418
|
+
since: string | null,
|
|
419
|
+
seen: Set<string>,
|
|
420
|
+
): Promise<{ lines: string[]; cursor: string | null }> {
|
|
421
|
+
const lines: string[] = [];
|
|
422
|
+
let cursor = since;
|
|
423
|
+
// A page that comes back full may hide newer entries behind it (a chatty
|
|
424
|
+
// two-minute window): read on from its last timestamp, page by page.
|
|
425
|
+
let from: string | null = since ? lookback(since) : null;
|
|
426
|
+
for (let page = 0; page < LOG_MAX_PAGES; page++) {
|
|
427
|
+
const filter = [
|
|
428
|
+
'resource.type="cloud_run_job"',
|
|
429
|
+
`resource.labels.job_name="${INSTALL_JOB_NAME}"`,
|
|
430
|
+
`labels."run.googleapis.com/execution_name"="${target.execution}"`,
|
|
431
|
+
...(from ? [`timestamp>="${from}"`] : []),
|
|
432
|
+
].join(" AND ");
|
|
433
|
+
const r = await run([
|
|
434
|
+
"logging",
|
|
435
|
+
"read",
|
|
436
|
+
filter,
|
|
437
|
+
`--project=${target.projectId}`,
|
|
438
|
+
"--order=asc",
|
|
439
|
+
`--limit=${LOG_PAGE_SIZE}`,
|
|
440
|
+
"--freshness=1d",
|
|
441
|
+
"--format=json",
|
|
442
|
+
]);
|
|
443
|
+
if (!r.ok) break;
|
|
444
|
+
const entries = JSON.parse(r.stdout || "[]") as LogEntry[];
|
|
445
|
+
for (const e of entries) {
|
|
446
|
+
const id = e.insertId ?? `${e.timestamp}:${e.textPayload}`;
|
|
447
|
+
if (seen.has(id)) continue;
|
|
448
|
+
seen.add(id);
|
|
449
|
+
if (e.timestamp && (cursor === null || Date.parse(e.timestamp) > Date.parse(cursor))) {
|
|
450
|
+
cursor = e.timestamp;
|
|
451
|
+
}
|
|
452
|
+
if (e.textPayload !== undefined) lines.push(e.textPayload.replace(/\n$/, ""));
|
|
453
|
+
}
|
|
454
|
+
const last = entries.at(-1)?.timestamp ?? null;
|
|
455
|
+
// Not full: nothing newer is hidden. Full but one timestamp throughout:
|
|
456
|
+
// the next page would be the same one.
|
|
457
|
+
if (entries.length < LOG_PAGE_SIZE || last === null || last === from) break;
|
|
458
|
+
from = last;
|
|
459
|
+
}
|
|
460
|
+
return { lines, cursor };
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Store `value` as the newest version of the install secret `secretId`
|
|
465
|
+
* (created with the install's secret replication if missing) and disable
|
|
466
|
+
* the versions before it, so exactly one value is readable. The value goes
|
|
467
|
+
* to gcloud on stdin, never on a command line.
|
|
468
|
+
*/
|
|
469
|
+
export async function storeInstallSecretValue(
|
|
470
|
+
run: GcloudRunner,
|
|
471
|
+
target: { projectId: string; region: string },
|
|
472
|
+
secretId: string,
|
|
473
|
+
value: string,
|
|
474
|
+
): Promise<{ ok: true } | { ok: false; error: string }> {
|
|
475
|
+
const project = `--project=${target.projectId}`;
|
|
476
|
+
const exists = await run(["secrets", "describe", secretId, project, "--format=value(name)"]);
|
|
477
|
+
if (!exists.ok) {
|
|
478
|
+
const created = await createInstallSecret({ ...target, env: GCP_STACK_ENV, secretId }, { run });
|
|
479
|
+
if (!created.ok) return created;
|
|
480
|
+
}
|
|
481
|
+
const add = await run(
|
|
482
|
+
["secrets", "versions", "add", secretId, project, "--data-file=-", "--format=value(name)"],
|
|
483
|
+
{ stdin: value },
|
|
484
|
+
);
|
|
485
|
+
if (!add.ok) return { ok: false, error: `store ${secretId}: ${add.stderr.trim()}` };
|
|
486
|
+
const newest = add.stdout.trim().split("/").pop() ?? "";
|
|
487
|
+
const enabled = await run([
|
|
488
|
+
"secrets",
|
|
489
|
+
"versions",
|
|
490
|
+
"list",
|
|
491
|
+
secretId,
|
|
492
|
+
project,
|
|
493
|
+
"--filter=state:enabled",
|
|
494
|
+
"--format=value(name)",
|
|
495
|
+
]);
|
|
496
|
+
for (const name of enabled.ok ? enabled.stdout.split("\n") : []) {
|
|
497
|
+
const version = name.trim().split("/").pop() ?? "";
|
|
498
|
+
if (version === "" || version === newest) continue;
|
|
499
|
+
await run([
|
|
500
|
+
"secrets",
|
|
501
|
+
"versions",
|
|
502
|
+
"disable",
|
|
503
|
+
version,
|
|
504
|
+
`--secret=${secretId}`,
|
|
505
|
+
project,
|
|
506
|
+
"--quiet",
|
|
507
|
+
]);
|
|
508
|
+
}
|
|
509
|
+
return { ok: true };
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/** Store the Owner setup link in {@link INSTALL_SETUP_LINK_SECRET}. */
|
|
513
|
+
export function storeSetupLink(
|
|
514
|
+
run: GcloudRunner,
|
|
515
|
+
target: { projectId: string; region: string },
|
|
516
|
+
link: string,
|
|
517
|
+
): Promise<{ ok: true } | { ok: false; error: string }> {
|
|
518
|
+
return storeInstallSecretValue(run, target, INSTALL_SETUP_LINK_SECRET, link);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* The Owner setup link the job stored, or null when there is none (the
|
|
523
|
+
* secret or an enabled version is missing).
|
|
524
|
+
*
|
|
525
|
+
* @throws when it can't be read for any other reason (no permission), with
|
|
526
|
+
* what the operator's account needs
|
|
527
|
+
*/
|
|
528
|
+
export async function readSetupLink(run: GcloudRunner, projectId: string): Promise<string | null> {
|
|
529
|
+
const r = await run([
|
|
530
|
+
"secrets",
|
|
531
|
+
"versions",
|
|
532
|
+
"access",
|
|
533
|
+
"latest",
|
|
534
|
+
`--secret=${INSTALL_SETUP_LINK_SECRET}`,
|
|
535
|
+
`--project=${projectId}`,
|
|
536
|
+
]);
|
|
537
|
+
if (r.ok) return r.stdout.trim() === "" ? null : r.stdout.trim();
|
|
538
|
+
if (/NOT_FOUND|not found|DISABLED|DESTROYED|FAILED_PRECONDITION/i.test(r.stderr)) return null;
|
|
539
|
+
throw new Error(
|
|
540
|
+
`Could not read the Owner setup link (secret ${INSTALL_SETUP_LINK_SECRET} in ${projectId}): ${r.stderr.trim()}. Your gcloud account needs secretmanager.versions.access on the project (e.g. roles/secretmanager.secretAccessor or project Owner).`,
|
|
541
|
+
);
|
|
542
|
+
}
|