@caelo-cms/provisioning 0.10.61 → 0.10.62
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/caddy/Caddyfile.production +7 -0
- package/caddy/Caddyfile.staging +7 -0
- package/dist/admin-internal-answer.d.ts +12 -0
- package/dist/admin-internal-answer.d.ts.map +1 -0
- package/dist/admin-internal-answer.js +32 -0
- package/dist/admin-internal-answer.js.map +1 -0
- package/dist/admin-internal-google.d.ts +4 -0
- package/dist/admin-internal-google.d.ts.map +1 -0
- package/dist/admin-internal-google.js +161 -0
- package/dist/admin-internal-google.js.map +1 -0
- package/dist/admin-internal-self-hosted.d.ts +30 -0
- package/dist/admin-internal-self-hosted.d.ts.map +1 -0
- package/dist/admin-internal-self-hosted.js +92 -0
- package/dist/admin-internal-self-hosted.js.map +1 -0
- package/dist/admin-internal.d.ts +33 -12
- package/dist/admin-internal.d.ts.map +1 -1
- package/dist/admin-internal.js +27 -159
- package/dist/admin-internal.js.map +1 -1
- package/dist/agent/command.d.ts +7 -0
- package/dist/agent/command.d.ts.map +1 -1
- package/dist/agent/command.js +1 -0
- package/dist/agent/command.js.map +1 -1
- package/dist/agent/lock.d.ts +13 -1
- package/dist/agent/lock.d.ts.map +1 -1
- package/dist/agent/lock.js +62 -5
- package/dist/agent/lock.js.map +1 -1
- package/dist/agent/run.d.ts +5 -1
- package/dist/agent/run.d.ts.map +1 -1
- package/dist/agent/run.js +8 -1
- package/dist/agent/run.js.map +1 -1
- package/dist/agent-db.js +192 -90
- package/dist/caddy.d.ts.map +1 -1
- package/dist/caddy.js +11 -1
- package/dist/caddy.js.map +1 -1
- package/dist/cli-routing.js +1 -1
- package/dist/cli-routing.js.map +1 -1
- package/dist/cli.js +95 -38
- package/dist/cli.js.map +1 -1
- package/dist/compose.d.ts +7 -0
- package/dist/compose.d.ts.map +1 -1
- package/dist/compose.js +5 -0
- package/dist/compose.js.map +1 -1
- package/dist/converge-job-gcp.d.ts.map +1 -1
- package/dist/converge-job-gcp.js +10 -7
- package/dist/converge-job-gcp.js.map +1 -1
- package/dist/converge-job-preflight.d.ts +21 -0
- package/dist/converge-job-preflight.d.ts.map +1 -0
- package/dist/converge-job-preflight.js +111 -0
- package/dist/converge-job-preflight.js.map +1 -0
- package/dist/converge-job.d.ts +8 -2
- package/dist/converge-job.d.ts.map +1 -1
- package/dist/converge-job.js +14 -0
- package/dist/converge-job.js.map +1 -1
- package/dist/gateway-credentials.d.ts +5 -3
- package/dist/gateway-credentials.d.ts.map +1 -1
- package/dist/gateway-credentials.js +5 -8
- package/dist/gateway-credentials.js.map +1 -1
- package/dist/gcloud.d.ts +29 -0
- package/dist/gcloud.d.ts.map +1 -1
- package/dist/gcloud.js +66 -0
- package/dist/gcloud.js.map +1 -1
- package/dist/infra-contract.d.ts +1 -1
- package/dist/infra-contract.d.ts.map +1 -1
- package/dist/infra-contract.js +10 -1
- package/dist/infra-contract.js.map +1 -1
- package/dist/install-job.d.ts +3 -1
- package/dist/install-job.d.ts.map +1 -1
- package/dist/install-job.js.map +1 -1
- package/dist/install-state.d.ts +10 -0
- package/dist/install-state.d.ts.map +1 -1
- package/dist/install-state.js +12 -0
- package/dist/install-state.js.map +1 -1
- package/dist/lifecycle.d.ts +12 -0
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +113 -28
- package/dist/lifecycle.js.map +1 -1
- package/dist/mcp-iap.d.ts +2 -0
- package/dist/mcp-iap.d.ts.map +1 -1
- package/dist/mcp-iap.js +50 -17
- package/dist/mcp-iap.js.map +1 -1
- package/dist/owner-reset.d.ts +16 -19
- package/dist/owner-reset.d.ts.map +1 -1
- package/dist/owner-reset.js +92 -19
- package/dist/owner-reset.js.map +1 -1
- package/dist/preview-plan.d.ts +71 -0
- package/dist/preview-plan.d.ts.map +1 -0
- package/dist/preview-plan.js +168 -0
- package/dist/preview-plan.js.map +1 -0
- package/dist/public-role-isolation.d.ts +2 -0
- package/dist/public-role-isolation.d.ts.map +1 -1
- package/dist/public-role-isolation.js +9 -3
- package/dist/public-role-isolation.js.map +1 -1
- package/dist/release-contract.d.ts.map +1 -1
- package/dist/release-contract.js +4 -0
- package/dist/release-contract.js.map +1 -1
- package/dist/self-hosted-internal-secret.d.ts +27 -0
- package/dist/self-hosted-internal-secret.d.ts.map +1 -0
- package/dist/self-hosted-internal-secret.js +69 -0
- package/dist/self-hosted-internal-secret.js.map +1 -0
- package/dist/self-hosted-release.d.ts +3 -1
- package/dist/self-hosted-release.d.ts.map +1 -1
- package/dist/self-hosted-release.js +6 -2
- package/dist/self-hosted-release.js.map +1 -1
- package/dist/self-hosted-upgrade.d.ts +13 -1
- package/dist/self-hosted-upgrade.d.ts.map +1 -1
- package/dist/self-hosted-upgrade.js +26 -2
- package/dist/self-hosted-upgrade.js.map +1 -1
- package/dist/stack-converge.d.ts +5 -2
- package/dist/stack-converge.d.ts.map +1 -1
- package/dist/stack-converge.js +73 -8
- package/dist/stack-converge.js.map +1 -1
- package/dist/staging-admin.d.ts +4 -1
- package/dist/staging-admin.d.ts.map +1 -1
- package/dist/staging-admin.js.map +1 -1
- package/dist/state-backend.d.ts +3 -2
- package/dist/state-backend.d.ts.map +1 -1
- package/dist/state-backend.js +3 -2
- package/dist/state-backend.js.map +1 -1
- package/dist/state-command.d.ts +17 -0
- package/dist/state-command.d.ts.map +1 -1
- package/dist/state-command.js +34 -5
- package/dist/state-command.js.map +1 -1
- package/dist/state-migrate.d.ts +14 -3
- package/dist/state-migrate.d.ts.map +1 -1
- package/dist/state-migrate.js +31 -7
- package/dist/state-migrate.js.map +1 -1
- package/dist/state-pulumi.d.ts +0 -2
- package/dist/state-pulumi.d.ts.map +1 -1
- package/dist/state-pulumi.js +19 -20
- package/dist/state-pulumi.js.map +1 -1
- package/dist/static-security-headers.d.ts +86 -0
- package/dist/static-security-headers.d.ts.map +1 -0
- package/dist/static-security-headers.js +126 -0
- package/dist/static-security-headers.js.map +1 -0
- package/dist/static-svg-gcp.d.ts +28 -0
- package/dist/static-svg-gcp.d.ts.map +1 -0
- package/dist/static-svg-gcp.js +106 -0
- package/dist/static-svg-gcp.js.map +1 -0
- package/dist/upgrade/app-roll.d.ts +3 -0
- package/dist/upgrade/app-roll.d.ts.map +1 -1
- package/dist/upgrade/app-roll.js +12 -5
- package/dist/upgrade/app-roll.js.map +1 -1
- package/dist/upgrade/backup.d.ts +2 -0
- package/dist/upgrade/backup.d.ts.map +1 -1
- package/dist/upgrade/backup.js +5 -4
- package/dist/upgrade/backup.js.map +1 -1
- package/dist/upgrade/converge.d.ts +7 -0
- package/dist/upgrade/converge.d.ts.map +1 -1
- package/dist/upgrade/converge.js +19 -7
- package/dist/upgrade/converge.js.map +1 -1
- package/dist/upgrade/gcp-lookup.d.ts +38 -3
- package/dist/upgrade/gcp-lookup.d.ts.map +1 -1
- package/dist/upgrade/gcp-lookup.js +31 -9
- package/dist/upgrade/gcp-lookup.js.map +1 -1
- package/dist/upgrade/health.d.ts +31 -5
- package/dist/upgrade/health.d.ts.map +1 -1
- package/dist/upgrade/health.js +114 -25
- package/dist/upgrade/health.js.map +1 -1
- package/dist/upgrade/release-images.d.ts +17 -0
- package/dist/upgrade/release-images.d.ts.map +1 -1
- package/dist/upgrade/release-images.js +23 -12
- package/dist/upgrade/release-images.js.map +1 -1
- package/dist/upgrade/reporter.d.ts +2 -0
- package/dist/upgrade/reporter.d.ts.map +1 -1
- package/dist/upgrade/reporter.js +1 -0
- package/dist/upgrade/reporter.js.map +1 -1
- package/dist/upgrade/upgrade.d.ts +11 -1
- package/dist/upgrade/upgrade.d.ts.map +1 -1
- package/dist/upgrade/upgrade.js +5 -2
- package/dist/upgrade/upgrade.js.map +1 -1
- package/dist/upgrader-gcp.d.ts +12 -2
- package/dist/upgrader-gcp.d.ts.map +1 -1
- package/dist/upgrader-gcp.js +22 -13
- package/dist/upgrader-gcp.js.map +1 -1
- package/dist/upgrader-iam-preflight.d.ts +16 -3
- package/dist/upgrader-iam-preflight.d.ts.map +1 -1
- package/dist/upgrader-iam-preflight.js +9 -4
- package/dist/upgrader-iam-preflight.js.map +1 -1
- package/dist/upgrader-job.d.ts.map +1 -1
- package/dist/upgrader-job.js +10 -8
- package/dist/upgrader-job.js.map +1 -1
- package/dist/upgrader-reporter.d.ts.map +1 -1
- package/dist/upgrader-reporter.js +1 -0
- package/dist/upgrader-reporter.js.map +1 -1
- package/dist/wizards/gcp-pulumi.d.ts +70 -23
- package/dist/wizards/gcp-pulumi.d.ts.map +1 -1
- package/dist/wizards/gcp-pulumi.js +118 -39
- package/dist/wizards/gcp-pulumi.js.map +1 -1
- package/dist/wizards/gcp.d.ts.map +1 -1
- package/dist/wizards/gcp.js +67 -11
- package/dist/wizards/gcp.js.map +1 -1
- package/package.json +2 -2
- package/src/admin-internal-answer.ts +43 -0
- package/src/admin-internal-google.ts +187 -0
- package/src/admin-internal-self-hosted.test.ts +70 -0
- package/src/admin-internal-self-hosted.ts +128 -0
- package/src/admin-internal.ts +59 -174
- package/src/admin-memory.test.ts +1 -0
- package/src/agent/command.ts +3 -0
- package/src/agent/lock-config.test.ts +57 -2
- package/src/agent/lock.ts +68 -5
- package/src/agent/run.test.ts +18 -1
- package/src/agent/run.ts +17 -6
- package/src/caddy-redirects.test.ts +1 -0
- package/src/caddy.ts +12 -0
- package/src/cli-routing.ts +1 -1
- package/src/cli.ts +102 -44
- package/src/compose.ts +12 -0
- package/src/converge-job-gcp.ts +16 -11
- package/src/converge-job-preflight.test.ts +108 -0
- package/src/converge-job-preflight.ts +124 -0
- package/src/converge-job.test.ts +50 -0
- package/src/converge-job.ts +38 -2
- package/src/gateway-credentials.test.ts +4 -3
- package/src/gateway-credentials.ts +6 -10
- package/src/gateway-env-isolation.test.ts +1 -0
- package/src/gcloud.test.ts +93 -1
- package/src/gcloud.ts +82 -0
- package/src/index.test.ts +38 -1
- package/src/infra-contract.test.ts +4 -3
- package/src/infra-contract.ts +10 -1
- package/src/install-job.ts +5 -2
- package/src/install-state.ts +13 -0
- package/src/lifecycle.ts +134 -28
- package/src/mcp-iap.test.ts +66 -5
- package/src/mcp-iap.ts +55 -17
- package/src/owner-reset-providers.test.ts +370 -0
- package/src/owner-reset.test.ts +2 -2
- package/src/owner-reset.ts +108 -19
- package/src/preview-plan.test.ts +233 -0
- package/src/preview-plan.ts +226 -0
- package/src/public-role-isolation.test.ts +13 -2
- package/src/public-role-isolation.ts +14 -4
- package/src/release-contract.ts +4 -0
- package/src/self-hosted-compose.test.ts +25 -0
- package/src/self-hosted-internal-secret.test.ts +61 -0
- package/src/self-hosted-internal-secret.ts +80 -0
- package/src/self-hosted-release.ts +10 -3
- package/src/self-hosted-upgrade.test.ts +35 -1
- package/src/self-hosted-upgrade.ts +40 -3
- package/src/stack-contract.test.ts +1 -0
- package/src/stack-converge.test.ts +89 -5
- package/src/stack-converge.ts +81 -8
- package/src/staging-admin.test.ts +1 -1
- package/src/staging-admin.ts +2 -1
- package/src/state-backend.ts +3 -2
- package/src/state-command.test.ts +31 -0
- package/src/state-command.ts +35 -8
- package/src/state-migrate.test.ts +58 -7
- package/src/state-migrate.ts +48 -10
- package/src/state-pulumi.ts +20 -21
- package/src/static-security-headers.test.ts +124 -0
- package/src/static-security-headers.ts +154 -0
- package/src/static-svg-gcp.test.ts +133 -0
- package/src/static-svg-gcp.ts +140 -0
- package/src/upgrade/app-roll.ts +18 -7
- package/src/upgrade/backup.ts +14 -5
- package/src/upgrade/converge.ts +25 -7
- package/src/upgrade/gcp-lookup.test.ts +80 -1
- package/src/upgrade/gcp-lookup.ts +61 -11
- package/src/upgrade/health.test.ts +132 -8
- package/src/upgrade/health.ts +169 -31
- package/src/upgrade/release-images.test.ts +17 -0
- package/src/upgrade/release-images.ts +26 -14
- package/src/upgrade/reporter.ts +3 -0
- package/src/upgrade/upgrade.test.ts +137 -8
- package/src/upgrade/upgrade.ts +14 -2
- package/src/upgrader-gcp.ts +28 -13
- package/src/upgrader-iam-preflight.test.ts +6 -3
- package/src/upgrader-iam-preflight.ts +26 -5
- package/src/upgrader-job.ts +14 -6
- package/src/upgrader-reporter.ts +1 -0
- package/src/wizards/gcp-pulumi.test.ts +1 -1
- package/src/wizards/gcp-pulumi.ts +147 -38
- package/src/wizards/gcp.ts +89 -9
- package/src/wizards/stack-work-dir.test.ts +175 -0
- package/stacks/aws/index.ts +40 -0
- package/stacks/azure/index.ts +22 -1
- package/stacks/gcp/README.md +4 -0
- package/stacks/gcp/index.ts +6 -2
- package/stacks/gcp/Pulumi.production.yaml +0 -16
package/src/converge-job-gcp.ts
CHANGED
|
@@ -48,10 +48,10 @@ import { publicSiteUrl, STAGING_HOST_ENV } from "./stack-contract.js";
|
|
|
48
48
|
import { attachInstall } from "./state-attach.js";
|
|
49
49
|
import { hasLocalPulumiState, opsBucketName } from "./state-backend.js";
|
|
50
50
|
import { acquireBucketStateLock, GcsStateStore, realSyncGcloud } from "./state-store.js";
|
|
51
|
-
import { resolveGcpResourceName } from "./upgrade/gcp-lookup.js";
|
|
51
|
+
import { lookupFailure, resolveGcpResourceName } from "./upgrade/gcp-lookup.js";
|
|
52
52
|
import { releaseImageRef, verifyCosignAll } from "./upgrade/release-images.js";
|
|
53
53
|
import { upgradeGcpInstallOutcome } from "./upgrade/upgrade.js";
|
|
54
|
-
import { healthProbeFor, type UpgraderPlacement } from "./upgrader-gcp.js";
|
|
54
|
+
import { gcloudIamTester, healthProbeFor, type UpgraderPlacement } from "./upgrader-gcp.js";
|
|
55
55
|
import type { JobLog } from "./upgrader-reporter.js";
|
|
56
56
|
|
|
57
57
|
const DIGEST = /@(sha256:[0-9a-f]{64})/;
|
|
@@ -273,14 +273,16 @@ export function gcpConvergeDeps(
|
|
|
273
273
|
projectId,
|
|
274
274
|
region,
|
|
275
275
|
);
|
|
276
|
-
const described =
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
?
|
|
283
|
-
:
|
|
276
|
+
const described =
|
|
277
|
+
admin.status === "found"
|
|
278
|
+
? await run(["run", "services", "describe", admin.name, ...location, "--format=json"])
|
|
279
|
+
: null;
|
|
280
|
+
const siteCheck =
|
|
281
|
+
admin.status !== "found"
|
|
282
|
+
? lookupFailure(admin, `${gcpNamePrefix(GCP_STACK_ENV)}-admin* Cloud Run service`, "job")
|
|
283
|
+
: described?.ok
|
|
284
|
+
? deployedSiteMismatch(described.stdout, meta, region)
|
|
285
|
+
: `the admin's description could not be read (${described?.stderr.trim() ?? ""})`;
|
|
284
286
|
if (siteCheck) {
|
|
285
287
|
reporter.error(
|
|
286
288
|
`Nothing was changed: the install's records name ${meta.domain} in ${meta.region}, which this update could not confirm against what the admin serves today (${siteCheck}). Run the update once from Cloud Shell or the computer you installed from.`,
|
|
@@ -304,6 +306,7 @@ export function gcpConvergeDeps(
|
|
|
304
306
|
skipVerify: false,
|
|
305
307
|
skipHealthCheck: false,
|
|
306
308
|
migrationsContract: m.migrations.contract,
|
|
309
|
+
credentials: "job",
|
|
307
310
|
},
|
|
308
311
|
{
|
|
309
312
|
run,
|
|
@@ -327,7 +330,7 @@ export function gcpConvergeDeps(
|
|
|
327
330
|
{ projectId, region: meta.region as string, imageOverride: imageRef },
|
|
328
331
|
{ run },
|
|
329
332
|
),
|
|
330
|
-
healthProbe: (plan) => healthProbeFor(run, http, plan),
|
|
333
|
+
healthProbe: (plan, context) => healthProbeFor(run, http, plan, context),
|
|
331
334
|
recordDigests: async (digests) => {
|
|
332
335
|
recorded = await recordRolledRelease(
|
|
333
336
|
installId,
|
|
@@ -361,6 +364,8 @@ export function gcpConvergeDeps(
|
|
|
361
364
|
return {
|
|
362
365
|
queue,
|
|
363
366
|
projectId,
|
|
367
|
+
region,
|
|
368
|
+
iam: gcloudIamTester(run, http),
|
|
364
369
|
execution: placement.execution,
|
|
365
370
|
ownVersion: CAELO_VERSION,
|
|
366
371
|
// The default deps: the fixed registry and signer, never overridden.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The converge job's IAM self-check list (#884): a provisioner holding what
|
|
5
|
+
* the install and converge-job-setup.ts grant passes it, each role the setup
|
|
6
|
+
* grants on top is checked, and losing one is named with what it is for.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, expect, it } from "bun:test";
|
|
10
|
+
import {
|
|
11
|
+
CONVERGE_PREFLIGHT_VOICE,
|
|
12
|
+
convergeAccessChecks,
|
|
13
|
+
INSTALL_ROLE_USES,
|
|
14
|
+
} from "./converge-job-preflight.js";
|
|
15
|
+
import { PROVISIONER_ROLE_LIST } from "./gcloud.js";
|
|
16
|
+
import { INSTALL_JOB_ROLES } from "./install-job.js";
|
|
17
|
+
import {
|
|
18
|
+
type IamTester,
|
|
19
|
+
preflightMissingReason,
|
|
20
|
+
resourceName,
|
|
21
|
+
runIamPreflight,
|
|
22
|
+
} from "./upgrader-iam-preflight.js";
|
|
23
|
+
|
|
24
|
+
const TARGET = { projectId: "acme", region: "europe-west1", env: "production" };
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The permissions of the predefined roles the provisioner holds, as far as
|
|
28
|
+
* the self-check asks for them (cloud.google.com/iam/docs/understanding-roles).
|
|
29
|
+
*/
|
|
30
|
+
const PREDEFINED: Record<string, readonly string[]> = {
|
|
31
|
+
"roles/run.admin": ["run.jobs.get", "run.jobs.update", "run.jobs.run", "run.executions.get"],
|
|
32
|
+
"roles/cloudsql.admin": ["cloudsql.instances.get", "cloudsql.instances.login"],
|
|
33
|
+
"roles/storage.admin": [
|
|
34
|
+
"storage.objects.list",
|
|
35
|
+
"storage.objects.get",
|
|
36
|
+
"storage.objects.create",
|
|
37
|
+
"storage.objects.delete",
|
|
38
|
+
],
|
|
39
|
+
"roles/iam.serviceAccountUser": ["iam.serviceAccounts.actAs"],
|
|
40
|
+
"roles/iam.roleAdmin": ["iam.roles.create", "iam.roles.get", "iam.roles.update"],
|
|
41
|
+
"roles/cloudscheduler.admin": [
|
|
42
|
+
"cloudscheduler.jobs.create",
|
|
43
|
+
"cloudscheduler.jobs.get",
|
|
44
|
+
"cloudscheduler.jobs.update",
|
|
45
|
+
],
|
|
46
|
+
"roles/logging.logWriter": ["logging.logEntries.create"],
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A tester for a provisioner holding `roles` project-wide: unconditional
|
|
51
|
+
* project roles reach every resource of the project, so each answers alike.
|
|
52
|
+
*/
|
|
53
|
+
function provisioner(roles: readonly string[]): IamTester {
|
|
54
|
+
const held = new Set(roles.flatMap((r) => PREDEFINED[r] ?? []));
|
|
55
|
+
return {
|
|
56
|
+
granted: async (_resource, permissions) => ({
|
|
57
|
+
ok: true,
|
|
58
|
+
granted: permissions.filter((p) => held.has(p)),
|
|
59
|
+
}),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
describe("convergeAccessChecks", () => {
|
|
64
|
+
it("a provisioner with its install roles and the converge setup's roles passes", async () => {
|
|
65
|
+
const r = await runIamPreflight(
|
|
66
|
+
convergeAccessChecks(TARGET),
|
|
67
|
+
provisioner([...PROVISIONER_ROLE_LIST, ...INSTALL_JOB_ROLES]),
|
|
68
|
+
);
|
|
69
|
+
expect(r).toEqual({ kind: "ok" });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("checks every role converge-job-setup.ts grants the provisioner", () => {
|
|
73
|
+
const checks = convergeAccessChecks(TARGET);
|
|
74
|
+
for (const role of INSTALL_JOB_ROLES) {
|
|
75
|
+
expect(checks).toContainEqual({
|
|
76
|
+
resource: { kind: "project", projectId: "acme" },
|
|
77
|
+
...INSTALL_ROLE_USES[role],
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("asks about the converge job, the ops bucket and the provisioner's own account", () => {
|
|
83
|
+
const where = new Set(convergeAccessChecks(TARGET).map((c) => resourceName(c.resource)));
|
|
84
|
+
expect(where).toEqual(
|
|
85
|
+
new Set([
|
|
86
|
+
"projects/acme",
|
|
87
|
+
"gs://acme-caelo-ops",
|
|
88
|
+
"projects/acme/locations/europe-west1/jobs/caelo-production-converge",
|
|
89
|
+
"projects/acme/serviceAccounts/caelo-provisioner@acme.iam.gserviceaccount.com",
|
|
90
|
+
]),
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("a provisioner without the install-job roles is stopped, each named with what it is for", async () => {
|
|
95
|
+
const r = await runIamPreflight(
|
|
96
|
+
convergeAccessChecks(TARGET),
|
|
97
|
+
provisioner(PROVISIONER_ROLE_LIST),
|
|
98
|
+
);
|
|
99
|
+
expect(r.kind).toBe("missing");
|
|
100
|
+
if (r.kind !== "missing") return;
|
|
101
|
+
expect(r.missing.flatMap((m) => m.missing).sort()).toEqual(
|
|
102
|
+
INSTALL_JOB_ROLES.flatMap((role) => INSTALL_ROLE_USES[role].permissions).sort(),
|
|
103
|
+
);
|
|
104
|
+
const reason = preflightMissingReason(r.missing, CONVERGE_PREFLIGHT_VOICE);
|
|
105
|
+
expect(reason).toContain("keeps the admin's access list in sync");
|
|
106
|
+
expect(reason).toContain("Cloud Shell or the computer you installed from");
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The converge job's IAM self-check (#884): before the job takes the state
|
|
5
|
+
* lock, hands over or backs anything up, it asks Google Cloud whether its
|
|
6
|
+
* identity — the install's `caelo-provisioner` — holds what the converge
|
|
7
|
+
* relies on, and stops with every missing permission named when it does
|
|
8
|
+
* not. Same mechanism and types as the upgrader's (upgrader-iam-preflight.ts,
|
|
9
|
+
* #818); only the list and the voice differ.
|
|
10
|
+
*
|
|
11
|
+
* What is checked is what converge-job-setup.ts grants or relies on, derived
|
|
12
|
+
* from the same constants so the two cannot drift:
|
|
13
|
+
*
|
|
14
|
+
* - every role of {@link INSTALL_JOB_ROLES} that `ensureConvergeJob`
|
|
15
|
+
* grants the provisioner, each by the permissions the converge uses from
|
|
16
|
+
* it ({@link INSTALL_ROLE_USES} is keyed by the role, so a new role
|
|
17
|
+
* without a check does not compile);
|
|
18
|
+
* - the ops bucket: install.json + the state lock under `install/`, the
|
|
19
|
+
* checkpoint under `converge/`. The provisioner holds them through the
|
|
20
|
+
* project's roles/storage.admin, which carries no IAM condition, so the
|
|
21
|
+
* bucket's own testIamPermissions answers for every prefix (unlike the
|
|
22
|
+
* upgrader's `install/`-conditioned grant, which has to be probed);
|
|
23
|
+
* - the converge job itself: stage 1 points it at the target's ops image
|
|
24
|
+
* and starts a new execution, and the job keeps running as the
|
|
25
|
+
* provisioner (actAs on its own account);
|
|
26
|
+
* - the Cloud SQL IAM login `ensureConvergeJob` creates (member of
|
|
27
|
+
* {@link UPGRADER_DB_ROLE}): the request row and the timeline.
|
|
28
|
+
*
|
|
29
|
+
* Not checked: the permissions `pulumi up` needs for the stack. They are the
|
|
30
|
+
* provisioner's PROVISIONER_ROLE_LIST (gcloud.ts), granted at install by the
|
|
31
|
+
* Owner's own account; a converge cannot repair them and Pulumi names what
|
|
32
|
+
* it lacks precisely.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { CONVERGE_STATE_PREFIX } from "./converge-checkpoint.js";
|
|
36
|
+
import { convergeJobName } from "./gcp-names.js";
|
|
37
|
+
import { INSTALL_JOB_ROLES, type InstallJobRole } from "./install-job.js";
|
|
38
|
+
import { provisionerServiceAccountEmail } from "./provisioner-identity.js";
|
|
39
|
+
import { OPS_INSTALL_PREFIX, opsBucketName } from "./state-backend.js";
|
|
40
|
+
import type { AccessCheck, PreflightVoice } from "./upgrader-iam-preflight.js";
|
|
41
|
+
import { UPGRADER_DB_ROLE } from "./upgrader-setup.js";
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* What a converge does with each role `ensureConvergeJob` grants the
|
|
45
|
+
* provisioner on top of its install roles, and the permissions of that role
|
|
46
|
+
* it uses (operator-access.ts creates the custom role and the scheduler
|
|
47
|
+
* job; the job's lines go to Cloud Logging).
|
|
48
|
+
*/
|
|
49
|
+
export const INSTALL_ROLE_USES: Record<
|
|
50
|
+
InstallJobRole,
|
|
51
|
+
{ readonly need: string; readonly permissions: readonly string[] }
|
|
52
|
+
> = {
|
|
53
|
+
"roles/iam.roleAdmin": {
|
|
54
|
+
need: "create and update the custom role that decides who may open the admin",
|
|
55
|
+
permissions: ["iam.roles.create", "iam.roles.get", "iam.roles.update"],
|
|
56
|
+
},
|
|
57
|
+
"roles/cloudscheduler.admin": {
|
|
58
|
+
need: "create and update the schedule that keeps the admin's access list in sync",
|
|
59
|
+
permissions: [
|
|
60
|
+
"cloudscheduler.jobs.create",
|
|
61
|
+
"cloudscheduler.jobs.get",
|
|
62
|
+
"cloudscheduler.jobs.update",
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
"roles/logging.logWriter": {
|
|
66
|
+
need: "write the update's progress to Cloud Logging",
|
|
67
|
+
permissions: ["logging.logEntries.create"],
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** The converge job's voice: the setup is re-converged by a terminal upgrade. */
|
|
72
|
+
export const CONVERGE_PREFLIGHT_VOICE: PreflightVoice = {
|
|
73
|
+
subject: "the converge job",
|
|
74
|
+
repair:
|
|
75
|
+
"Run the update once from Cloud Shell or the computer you installed from (Settings → Updates shows the command); it repairs the converge job's access.",
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** Every permission a converge run relies on after its decision (see the file comment). */
|
|
79
|
+
export function convergeAccessChecks(target: {
|
|
80
|
+
readonly projectId: string;
|
|
81
|
+
readonly region: string;
|
|
82
|
+
readonly env: string;
|
|
83
|
+
}): AccessCheck[] {
|
|
84
|
+
const { projectId, region, env } = target;
|
|
85
|
+
const project = { kind: "project", projectId } as const;
|
|
86
|
+
const bucket = opsBucketName(projectId);
|
|
87
|
+
return [
|
|
88
|
+
...INSTALL_JOB_ROLES.map((role) => ({ resource: project, ...INSTALL_ROLE_USES[role] })),
|
|
89
|
+
{
|
|
90
|
+
need: `read and write the install's records and state lock (${OPS_INSTALL_PREFIX}/) and the update's checkpoint (${CONVERGE_STATE_PREFIX}/) in the ops bucket`,
|
|
91
|
+
resource: { kind: "bucket", bucket },
|
|
92
|
+
permissions: [
|
|
93
|
+
"storage.objects.list",
|
|
94
|
+
"storage.objects.get",
|
|
95
|
+
"storage.objects.create",
|
|
96
|
+
"storage.objects.delete",
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
need: "point the converge job at the new release's installer and start it",
|
|
101
|
+
resource: { kind: "run-job", projectId, region, name: convergeJobName(env) },
|
|
102
|
+
permissions: ["run.jobs.get", "run.jobs.update", "run.jobs.run"],
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
need: "see whether an earlier run of the converge job has ended",
|
|
106
|
+
resource: project,
|
|
107
|
+
permissions: ["run.executions.get"],
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
need: "keep the converge job running as the install's provisioner account",
|
|
111
|
+
resource: {
|
|
112
|
+
kind: "service-account",
|
|
113
|
+
projectId,
|
|
114
|
+
email: provisionerServiceAccountEmail(projectId),
|
|
115
|
+
},
|
|
116
|
+
permissions: ["iam.serviceAccounts.actAs"],
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
need: `sign in to the database as its ${UPGRADER_DB_ROLE} login (the update's request and timeline)`,
|
|
120
|
+
resource: project,
|
|
121
|
+
permissions: ["cloudsql.instances.get", "cloudsql.instances.login"],
|
|
122
|
+
},
|
|
123
|
+
];
|
|
124
|
+
}
|
package/src/converge-job.test.ts
CHANGED
|
@@ -25,6 +25,7 @@ import type { InstallMetadata } from "./install-state.js";
|
|
|
25
25
|
import type { ReleaseManifest, ReleaseManifestResult } from "./release-manifest.js";
|
|
26
26
|
import { StateLockHeldError } from "./state-store.js";
|
|
27
27
|
import type { UpgradeCheckpoint } from "./upgrade/upgrade.js";
|
|
28
|
+
import { type IamTester, resourceName } from "./upgrader-iam-preflight.js";
|
|
28
29
|
import type { UpdateEnd, UpdateFacts } from "./upgrader-job.js";
|
|
29
30
|
|
|
30
31
|
const D = (c: string) => `sha256:${c.repeat(64)}`;
|
|
@@ -144,6 +145,8 @@ function harness(
|
|
|
144
145
|
lock?: () => () => void;
|
|
145
146
|
outcome?: ConvergeUpgradeOutcome;
|
|
146
147
|
imageSigned?: boolean;
|
|
148
|
+
/** What IAM answers the self-check; every permission asked for by default. */
|
|
149
|
+
iam?: IamTester;
|
|
147
150
|
} = {},
|
|
148
151
|
): Harness {
|
|
149
152
|
const calls: string[] = [];
|
|
@@ -154,6 +157,10 @@ function harness(
|
|
|
154
157
|
const deps: ConvergeJobDeps = {
|
|
155
158
|
queue: q.queue,
|
|
156
159
|
projectId: "acme",
|
|
160
|
+
region: "europe-west1",
|
|
161
|
+
iam: opts.iam ?? {
|
|
162
|
+
granted: async (_resource, permissions) => ({ ok: true, granted: permissions }),
|
|
163
|
+
},
|
|
157
164
|
execution: "caelo-production-converge-run1",
|
|
158
165
|
ownVersion: opts.ownVersion ?? "0.11.0",
|
|
159
166
|
fetchManifest: async (v) => {
|
|
@@ -250,6 +257,49 @@ describe("runConvergeJob — what stops the job before any change", () => {
|
|
|
250
257
|
expect(h.calls.some((c) => /^(lock|point|start|upgrade|checkpoint)/.test(c))).toBe(false);
|
|
251
258
|
};
|
|
252
259
|
|
|
260
|
+
it("#884: a missing grant stops either stage before the lock, every missing one named", async () => {
|
|
261
|
+
// The provisioner lost cloudscheduler.admin and actAs on itself.
|
|
262
|
+
const iam: IamTester = {
|
|
263
|
+
granted: async (resource, permissions) => ({
|
|
264
|
+
ok: true,
|
|
265
|
+
granted: permissions.filter(
|
|
266
|
+
(p) =>
|
|
267
|
+
!p.startsWith("cloudscheduler.") &&
|
|
268
|
+
!(resource.kind === "service-account" && p === "iam.serviceAccounts.actAs"),
|
|
269
|
+
),
|
|
270
|
+
}),
|
|
271
|
+
};
|
|
272
|
+
for (const stage of ["0.10.53", "0.11.0"]) {
|
|
273
|
+
const h = harness({ ownVersion: stage, iam });
|
|
274
|
+
const r = await runConvergeJob(h.deps);
|
|
275
|
+
expect(r.status).toBe("needs_cloud_shell");
|
|
276
|
+
nothingChanged(h);
|
|
277
|
+
expect(h.q.ends[0]?.codes).toEqual(["iam-preflight"]);
|
|
278
|
+
const reason = h.q.ends[0]?.reason ?? "";
|
|
279
|
+
expect(reason).toStartWith("Nothing was changed: the converge job checked its access");
|
|
280
|
+
expect(reason).toContain("cloudscheduler.jobs.create");
|
|
281
|
+
expect(reason).toContain(
|
|
282
|
+
"projects/acme/serviceAccounts/caelo-provisioner@acme.iam.gserviceaccount.com",
|
|
283
|
+
);
|
|
284
|
+
expect(reason).toContain("repairs the converge job's access");
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("#884: IAM that cannot be asked fails the run before the lock", async () => {
|
|
289
|
+
const h = harness({
|
|
290
|
+
iam: {
|
|
291
|
+
granted: async (resource) =>
|
|
292
|
+
resource.kind === "bucket" ? { ok: false, error: "HTTP 503" } : { ok: true, granted: [] },
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
const r = await runConvergeJob(h.deps);
|
|
296
|
+
expect(r.status).toBe("failed");
|
|
297
|
+
nothingChanged(h);
|
|
298
|
+
expect(h.q.ends[0]?.reason).toContain(
|
|
299
|
+
`${resourceName({ kind: "bucket", bucket: "acme-caelo-ops" })}: HTTP 503`,
|
|
300
|
+
);
|
|
301
|
+
});
|
|
302
|
+
|
|
253
303
|
it("a tampered or unsigned manifest", async () => {
|
|
254
304
|
for (const stage of ["0.10.53", "0.11.0"]) {
|
|
255
305
|
const h = harness({
|
package/src/converge-job.ts
CHANGED
|
@@ -15,8 +15,9 @@
|
|
|
15
15
|
* fetches the target's release manifest from the FIXED registry and
|
|
16
16
|
* verifies its signature (release-manifest.ts), and decides
|
|
17
17
|
* (upgrader-assess.ts, `route: "converge"`): no downgrade, nothing
|
|
18
|
-
* below minFromVersion, no automatic converge
|
|
19
|
-
*
|
|
18
|
+
* below minFromVersion, no automatic converge unless the policy allows
|
|
19
|
+
* it; then it checks its own IAM access (converge-job-preflight.ts).
|
|
20
|
+
* Then it verifies the target's ops image, points this job at it BY DIGEST and starts a new
|
|
20
21
|
* execution, handing the request over to it. It changes nothing else:
|
|
21
22
|
* the converge logic always comes from the release it installs.
|
|
22
23
|
*
|
|
@@ -45,6 +46,8 @@
|
|
|
45
46
|
*/
|
|
46
47
|
|
|
47
48
|
import type { ConvergeCheckpoint } from "./converge-checkpoint.js";
|
|
49
|
+
import { CONVERGE_PREFLIGHT_VOICE, convergeAccessChecks } from "./converge-job-preflight.js";
|
|
50
|
+
import { GCP_STACK_ENV } from "./gcp-names.js";
|
|
48
51
|
import { decideInstallJob, type ExecutionState } from "./install-job.js";
|
|
49
52
|
import type { InstallMetadata } from "./install-state.js";
|
|
50
53
|
import {
|
|
@@ -55,6 +58,12 @@ import {
|
|
|
55
58
|
import { StateLockHeldError } from "./state-store.js";
|
|
56
59
|
import type { UpgradeCheckpoint } from "./upgrade/upgrade.js";
|
|
57
60
|
import { assessUpgradeRequest } from "./upgrader-assess.js";
|
|
61
|
+
import {
|
|
62
|
+
type IamTester,
|
|
63
|
+
preflightErrorReason,
|
|
64
|
+
preflightMissingReason,
|
|
65
|
+
runIamPreflight,
|
|
66
|
+
} from "./upgrader-iam-preflight.js";
|
|
58
67
|
import {
|
|
59
68
|
type ClaimedUpdate,
|
|
60
69
|
recordUpdateEnd,
|
|
@@ -111,6 +120,10 @@ export interface ConvergeJobDeps {
|
|
|
111
120
|
readonly queue: ConvergeQueue;
|
|
112
121
|
/** From the metadata server — never from env or argv. */
|
|
113
122
|
readonly projectId: string;
|
|
123
|
+
/** The region this job runs in (metadata server). */
|
|
124
|
+
readonly region: string;
|
|
125
|
+
/** Asks IAM as this job's own identity (the IAM self-check, #884). */
|
|
126
|
+
readonly iam: IamTester;
|
|
114
127
|
/** This Cloud Run execution; null outside Cloud Run. */
|
|
115
128
|
readonly execution: string | null;
|
|
116
129
|
/** The Caelo release this code belongs to (CAELO_VERSION of the image). */
|
|
@@ -297,6 +310,29 @@ export async function runConvergeJob(deps: ConvergeJobDeps): Promise<UpgraderRun
|
|
|
297
310
|
for (const w of decision.warnings) r.warn(w);
|
|
298
311
|
const m = decision.manifest;
|
|
299
312
|
|
|
313
|
+
// #884 — decided to run: ask IAM for what the rest needs before the
|
|
314
|
+
// lock, the hand-over or the backup (converge-job-preflight.ts). Both
|
|
315
|
+
// stages ask; stage 2's list is the target release's own.
|
|
316
|
+
const access = await runIamPreflight(
|
|
317
|
+
convergeAccessChecks({ projectId: deps.projectId, region: deps.region, env: GCP_STACK_ENV }),
|
|
318
|
+
deps.iam,
|
|
319
|
+
);
|
|
320
|
+
if (access.kind === "missing") {
|
|
321
|
+
return stopWith(
|
|
322
|
+
"needs_cloud_shell",
|
|
323
|
+
["iam-preflight"],
|
|
324
|
+
preflightMissingReason(access.missing, CONVERGE_PREFLIGHT_VOICE),
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
if (access.kind === "error") {
|
|
328
|
+
return stopWith(
|
|
329
|
+
"failed",
|
|
330
|
+
["iam-preflight"],
|
|
331
|
+
preflightErrorReason(access.errors, CONVERGE_PREFLIGHT_VOICE),
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
r.info("The converge job has every permission this update needs.");
|
|
335
|
+
|
|
300
336
|
if (compareSemver(deps.ownVersion, m.version) !== 0) {
|
|
301
337
|
return await handOver(m);
|
|
302
338
|
}
|
|
@@ -141,9 +141,10 @@ describe("planPublicRoleSwitch", () => {
|
|
|
141
141
|
describe("findSqlInstance", () => {
|
|
142
142
|
it("finds the Pulumi-suffixed instance by the stack's prefix", async () => {
|
|
143
143
|
const { run, lines } = fakeGcloud({});
|
|
144
|
-
expect(await findSqlInstance({ projectId: "acme", env: "production" }, { run })).
|
|
145
|
-
"
|
|
146
|
-
|
|
144
|
+
expect(await findSqlInstance({ projectId: "acme", env: "production" }, { run })).toEqual({
|
|
145
|
+
status: "found",
|
|
146
|
+
name: "caelo-production-pg-1a2b3c",
|
|
147
|
+
});
|
|
147
148
|
expect(lines).toContain(
|
|
148
149
|
"sql instances list --project=acme --filter=name~^caelo-production-pg --format=value(name)",
|
|
149
150
|
);
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
} from "./runtime-secrets.js";
|
|
33
33
|
import { DATABASE_ROLE_SECRET } from "./stack-contract.js";
|
|
34
34
|
import type { LiveEnvValue } from "./stack-converge.js";
|
|
35
|
+
import { type GcpLookup, lookupFromListing } from "./upgrade/gcp-lookup.js";
|
|
35
36
|
|
|
36
37
|
/**
|
|
37
38
|
* Whether this roll moves `public_role` onto its own password: true while
|
|
@@ -107,13 +108,14 @@ export function planPublicRoleSwitch(
|
|
|
107
108
|
}
|
|
108
109
|
|
|
109
110
|
/**
|
|
110
|
-
* The install's Cloud SQL instance (Pulumi suffixes its name)
|
|
111
|
-
*
|
|
111
|
+
* The install's Cloud SQL instance (Pulumi suffixes its name). A failed
|
|
112
|
+
* listing comes back as an error with gcloud's stderr, never as "not found"
|
|
113
|
+
* (#630).
|
|
112
114
|
*/
|
|
113
115
|
export async function findSqlInstance(
|
|
114
116
|
install: { readonly projectId: string; readonly env: string },
|
|
115
117
|
deps: { readonly run?: GcloudRunner } = {},
|
|
116
|
-
): Promise<
|
|
118
|
+
): Promise<GcpLookup> {
|
|
117
119
|
const run = deps.run ?? defaultGcloud;
|
|
118
120
|
const r = await run([
|
|
119
121
|
"sql",
|
|
@@ -123,11 +125,5 @@ export async function findSqlInstance(
|
|
|
123
125
|
`--filter=name~^${gcpNamePrefix(install.env)}-pg`,
|
|
124
126
|
"--format=value(name)",
|
|
125
127
|
]);
|
|
126
|
-
|
|
127
|
-
return (
|
|
128
|
-
r.stdout
|
|
129
|
-
.split("\n")
|
|
130
|
-
.map((l) => l.trim())
|
|
131
|
-
.find((l) => l.length > 0) ?? null
|
|
132
|
-
);
|
|
128
|
+
return lookupFromListing(r);
|
|
133
129
|
}
|
|
@@ -135,6 +135,7 @@ describe("self-hosted: the generated compose gateway service (#635)", () => {
|
|
|
135
135
|
postgresPassword: "superpw",
|
|
136
136
|
rolePasswords: { admin: "adminpw", public: "publicpw" },
|
|
137
137
|
caeloSecretKek: "0".repeat(64),
|
|
138
|
+
internalSecret: "i".repeat(48),
|
|
138
139
|
diskSize: "20Gi",
|
|
139
140
|
images: {
|
|
140
141
|
mode: "release" as const,
|
package/src/gcloud.test.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MPL-2.0
|
|
2
2
|
|
|
3
3
|
import { describe, expect, it } from "bun:test";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
checkGcloudAuth,
|
|
6
|
+
type GcloudResult,
|
|
7
|
+
grantProvisionerRoles,
|
|
8
|
+
isGcloudAuthError,
|
|
9
|
+
PROVISIONER_ROLE_LIST,
|
|
10
|
+
projectDisplayName,
|
|
11
|
+
} from "./gcloud.js";
|
|
5
12
|
|
|
6
13
|
const ok: GcloudResult = { ok: true, stdout: "", stderr: "", exitCode: 0 };
|
|
7
14
|
const fail = (stderr: string): GcloudResult => ({ ok: false, stdout: "", stderr, exitCode: 1 });
|
|
@@ -97,3 +104,88 @@ describe("grantProvisionerRoles", () => {
|
|
|
97
104
|
expect(result.failed).toEqual([{ role, error: SA_NOT_YET_VISIBLE }]);
|
|
98
105
|
});
|
|
99
106
|
});
|
|
107
|
+
|
|
108
|
+
// Mirrors the Resource Manager displayName rule: 4–30 chars of letters,
|
|
109
|
+
// digits, spaces, hyphens, quotes and `!`.
|
|
110
|
+
const GCP_DISPLAY_NAME = /^[A-Za-z0-9 '"!-]{4,30}$/;
|
|
111
|
+
|
|
112
|
+
describe("projectDisplayName", () => {
|
|
113
|
+
it("names the project after the domain so installs are distinguishable", () => {
|
|
114
|
+
expect(projectDisplayName("searchviu.com")).toBe("Caelo searchviu-com");
|
|
115
|
+
expect(projectDisplayName("docs.caelo-cms.com")).toBe("Caelo docs-caelo-cms-com");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("stays within the GCP displayName rules for long or odd domains", () => {
|
|
119
|
+
for (const domain of [
|
|
120
|
+
"a-very-long-subdomain.of-an-even-longer-domain.example.co.uk",
|
|
121
|
+
"Example.COM",
|
|
122
|
+
"xn--mnchen-3ya.de",
|
|
123
|
+
"ab.c",
|
|
124
|
+
]) {
|
|
125
|
+
const name = projectDisplayName(domain);
|
|
126
|
+
expect(name).toMatch(GCP_DISPLAY_NAME);
|
|
127
|
+
expect(name).not.toMatch(/-$/);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("checkGcloudAuth (#630)", () => {
|
|
133
|
+
const account = { ...ok, stdout: "owner@acme.com\n" };
|
|
134
|
+
const runner = (token: GcloudResult, list: GcloudResult = account) => {
|
|
135
|
+
const calls: string[] = [];
|
|
136
|
+
const run = async (args: string[]): Promise<GcloudResult> => {
|
|
137
|
+
calls.push(args.join(" "));
|
|
138
|
+
return args[1] === "list" ? list : token;
|
|
139
|
+
};
|
|
140
|
+
return { run, calls };
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
it("passes when gcloud can mint a token", async () => {
|
|
144
|
+
const { run, calls } = runner({ ...ok, stdout: "ya29.secret" });
|
|
145
|
+
expect(await checkGcloudAuth(run)).toEqual({ ok: true, account: "owner@acme.com" });
|
|
146
|
+
expect(calls[1]).toBe("auth print-access-token --quiet");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("expired credentials pass `auth list` but fail here, with the login fix", async () => {
|
|
150
|
+
const { run } = runner(
|
|
151
|
+
fail(
|
|
152
|
+
"ERROR: (gcloud.auth.print-access-token) There was a problem refreshing your current auth tokens: Reauthentication failed. cannot prompt during non-interactive execution.",
|
|
153
|
+
),
|
|
154
|
+
);
|
|
155
|
+
const r = await checkGcloudAuth(run);
|
|
156
|
+
expect(r.ok).toBe(false);
|
|
157
|
+
if (r.ok) return;
|
|
158
|
+
expect(r.error).toContain("gcloud auth login");
|
|
159
|
+
expect(r.error).toContain("owner@acme.com");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("no logged-in account", async () => {
|
|
163
|
+
const { run } = runner(ok, ok);
|
|
164
|
+
const r = await checkGcloudAuth(run);
|
|
165
|
+
expect(r).toEqual({
|
|
166
|
+
ok: false,
|
|
167
|
+
error:
|
|
168
|
+
"No gcloud account is logged in. Run `gcloud auth login` with the Owner's Google account, then run this command again.",
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("another failure names gcloud's error, not expired credentials", async () => {
|
|
173
|
+
const { run } = runner(fail("ERROR: network unreachable"));
|
|
174
|
+
const r = await checkGcloudAuth(run);
|
|
175
|
+
expect(r.ok).toBe(false);
|
|
176
|
+
if (r.ok) return;
|
|
177
|
+
expect(r.error).toContain("network unreachable");
|
|
178
|
+
expect(r.error).not.toContain("expired");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("recognises the reauth failures gcloud prints", () => {
|
|
182
|
+
for (const stderr of [
|
|
183
|
+
"Reauthentication failed. cannot prompt during non-interactive execution.",
|
|
184
|
+
"google.auth.exceptions.RefreshError: ('invalid_grant: Bad Request', ...)",
|
|
185
|
+
"Please run: $ gcloud auth login to obtain new credentials. reauth related error (invalid_rapt)",
|
|
186
|
+
]) {
|
|
187
|
+
expect(isGcloudAuthError(stderr)).toBe(true);
|
|
188
|
+
}
|
|
189
|
+
expect(isGcloudAuthError("PERMISSION_DENIED: run.services.list")).toBe(false);
|
|
190
|
+
});
|
|
191
|
+
});
|