@caelo-cms/provisioning 0.11.3 → 0.11.4
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-db.js +46 -37
- package/dist/audit-job-setup.js +2 -2
- package/dist/audit-job-setup.js.map +1 -1
- package/dist/capture-job-setup.d.ts +46 -0
- package/dist/capture-job-setup.d.ts.map +1 -0
- package/dist/capture-job-setup.js +288 -0
- package/dist/capture-job-setup.js.map +1 -0
- package/dist/gcp-names.d.ts +17 -1
- package/dist/gcp-names.d.ts.map +1 -1
- package/dist/gcp-names.js +24 -0
- package/dist/gcp-names.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 +11 -1
- package/dist/infra-contract.js.map +1 -1
- package/dist/release-contract.d.ts.map +1 -1
- package/dist/release-contract.js +2 -0
- package/dist/release-contract.js.map +1 -1
- package/dist/stack-contract.d.ts +9 -0
- package/dist/stack-contract.d.ts.map +1 -1
- package/dist/stack-contract.js +16 -1
- package/dist/stack-contract.js.map +1 -1
- package/dist/upgrade/admin-image-jobs.d.ts +47 -0
- package/dist/upgrade/admin-image-jobs.d.ts.map +1 -0
- package/dist/upgrade/{audit-job-roll.js → admin-image-jobs.js} +19 -8
- package/dist/upgrade/admin-image-jobs.js.map +1 -0
- package/dist/upgrade/app-roll.d.ts.map +1 -1
- package/dist/upgrade/app-roll.js +31 -28
- package/dist/upgrade/app-roll.js.map +1 -1
- package/dist/upgrade/converge.d.ts.map +1 -1
- package/dist/upgrade/converge.js +31 -1
- package/dist/upgrade/converge.js.map +1 -1
- package/dist/upgrader-iam-preflight.d.ts.map +1 -1
- package/dist/upgrader-iam-preflight.js +8 -2
- package/dist/upgrader-iam-preflight.js.map +1 -1
- package/dist/upgrader-setup.d.ts.map +1 -1
- package/dist/upgrader-setup.js +9 -3
- package/dist/upgrader-setup.js.map +1 -1
- package/dist/wizards/gcp-cost.d.ts.map +1 -1
- package/dist/wizards/gcp-cost.js +5 -0
- package/dist/wizards/gcp-cost.js.map +1 -1
- package/dist/wizards/gcp.d.ts.map +1 -1
- package/dist/wizards/gcp.js +30 -0
- package/dist/wizards/gcp.js.map +1 -1
- package/package.json +2 -2
- package/src/audit-job-setup.ts +2 -2
- package/src/capture-job-setup.test.ts +213 -0
- package/src/capture-job-setup.ts +322 -0
- package/src/gcp-names.ts +29 -1
- package/src/infra-contract.test.ts +4 -3
- package/src/infra-contract.ts +11 -1
- package/src/release-contract.ts +2 -0
- package/src/stack-contract.test.ts +6 -0
- package/src/stack-contract.ts +17 -0
- package/src/stack-converge.test.ts +1 -1
- package/src/upgrade/{audit-job-roll.ts → admin-image-jobs.ts} +46 -16
- package/src/upgrade/app-roll.ts +34 -31
- package/src/upgrade/converge.ts +40 -0
- package/src/upgrade/upgrade.test.ts +40 -2
- package/src/upgrader-iam-preflight.test.ts +21 -0
- package/src/upgrader-iam-preflight.ts +8 -1
- package/src/upgrader-job.test.ts +1 -0
- package/src/upgrader-setup.test.ts +1 -0
- package/src/upgrader-setup.ts +9 -2
- package/src/wizards/gcp-cost.ts +6 -0
- package/src/wizards/gcp.ts +36 -0
- package/stacks/aws/index.ts +9 -0
- package/stacks/azure/index.ts +9 -0
- package/stacks/gcp/index.ts +13 -0
- package/stacks/gcp-firebase/index.ts +13 -0
- package/dist/upgrade/audit-job-roll.d.ts +0 -38
- package/dist/upgrade/audit-job-roll.d.ts.map +0 -1
- package/dist/upgrade/audit-job-roll.js.map +0 -1
package/src/upgrade/app-roll.ts
CHANGED
|
@@ -55,7 +55,7 @@ import {
|
|
|
55
55
|
rollService,
|
|
56
56
|
serviceRollArgs,
|
|
57
57
|
} from "../stack-converge.js";
|
|
58
|
-
import {
|
|
58
|
+
import { adminImageJobs, describeJobImage, setJobImage } from "./admin-image-jobs.js";
|
|
59
59
|
import { restoreCommand, type SqlBackup } from "./backup.js";
|
|
60
60
|
import {
|
|
61
61
|
describeServiceJson,
|
|
@@ -494,15 +494,15 @@ export async function executeAppRoll(
|
|
|
494
494
|
}
|
|
495
495
|
|
|
496
496
|
const rolled: RollPlan[] = [];
|
|
497
|
-
/**
|
|
498
|
-
|
|
497
|
+
/** Jobs this roll moved onto the admin's image (#875, #1217), with their prior image. */
|
|
498
|
+
const movedJobs: { readonly name: string; readonly label: string; readonly prior: string }[] = [];
|
|
499
499
|
const rollBack = async (alsoCurrent?: RollPlan): Promise<void> => {
|
|
500
|
-
|
|
501
|
-
const back = await
|
|
502
|
-
if (back.ok) r.info(`
|
|
500
|
+
for (const j of movedJobs) {
|
|
501
|
+
const back = await setJobImage(run, install, j.name, j.prior);
|
|
502
|
+
if (back.ok) r.info(` ${j.label} image restored to ${j.prior.slice(-19)}`);
|
|
503
503
|
else
|
|
504
504
|
r.error(
|
|
505
|
-
`
|
|
505
|
+
` ${j.label} rollback FAILED (${back.error}) — set its image to ${j.prior} by hand`,
|
|
506
506
|
);
|
|
507
507
|
}
|
|
508
508
|
const services = alsoCurrent ? [...rolled, alsoCurrent] : rolled;
|
|
@@ -589,31 +589,34 @@ export async function executeAppRoll(
|
|
|
589
589
|
rolled.push(p);
|
|
590
590
|
}
|
|
591
591
|
|
|
592
|
-
// #875 — the audit
|
|
593
|
-
// failure here does not undo the release:
|
|
594
|
-
// the version mismatch and the fix, until the next upgrade
|
|
595
|
-
|
|
596
|
-
const
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
sAudit.warned(
|
|
604
|
-
"The install has no quality-audit job yet; Stage audits fail until `cms-provision upgrade` sets it up.",
|
|
605
|
-
);
|
|
606
|
-
} else if (auditPrior === admin.imageRef) {
|
|
607
|
-
sAudit.done("Quality-audit job already on the admin's image");
|
|
608
|
-
} else {
|
|
609
|
-
const moved = await setAuditJobImage(run, install, admin.imageRef);
|
|
610
|
-
if (moved.ok) {
|
|
611
|
-
auditJobPriorImage = auditPrior;
|
|
612
|
-
sAudit.done(`Quality-audit job on ${admin.digest.slice(0, 19)}...`);
|
|
613
|
-
} else {
|
|
614
|
-
sAudit.warned(
|
|
615
|
-
`Quality-audit job not moved (${moved.error}); Stage audits fail until the next upgrade moves it.`,
|
|
592
|
+
// #875 / #1217 — the audit and capture jobs run the admin's image; they
|
|
593
|
+
// follow the admin. A failure here does not undo the release: the job then
|
|
594
|
+
// refuses with the version mismatch and the fix, until the next upgrade
|
|
595
|
+
// moves it.
|
|
596
|
+
for (const job of adminImageJobs(install.env)) {
|
|
597
|
+
const sJob = r.step(`Moving the ${job.label} onto the admin's image...`);
|
|
598
|
+
const described = await describeJobImage(run, install, job.name);
|
|
599
|
+
const prior = described.ok ? described.image : null;
|
|
600
|
+
if (!described.ok) {
|
|
601
|
+
sJob.warned(
|
|
602
|
+
`Could not read the ${job.label} (${described.error}); ${job.lagging} until the next upgrade moves it.`,
|
|
616
603
|
);
|
|
604
|
+
} else if (!prior) {
|
|
605
|
+
sJob.warned(
|
|
606
|
+
`The install has no ${job.label} yet; ${job.lagging} until \`cms-provision upgrade\` sets it up.`,
|
|
607
|
+
);
|
|
608
|
+
} else if (prior === admin.imageRef) {
|
|
609
|
+
sJob.done(`The ${job.label} is already on the admin's image`);
|
|
610
|
+
} else {
|
|
611
|
+
const moved = await setJobImage(run, install, job.name, admin.imageRef);
|
|
612
|
+
if (moved.ok) {
|
|
613
|
+
movedJobs.push({ name: job.name, label: job.label, prior });
|
|
614
|
+
sJob.done(`The ${job.label} is on ${admin.digest.slice(0, 19)}...`);
|
|
615
|
+
} else {
|
|
616
|
+
sJob.warned(
|
|
617
|
+
`The ${job.label} was not moved (${moved.error}); ${job.lagging} until the next upgrade moves it.`,
|
|
618
|
+
);
|
|
619
|
+
}
|
|
617
620
|
}
|
|
618
621
|
}
|
|
619
622
|
|
package/src/upgrade/converge.ts
CHANGED
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
31
|
import { ensureAuditJob, resolveAdminImage } from "../audit-job-setup.js";
|
|
32
|
+
import { ensureCaptureJob } from "../capture-job-setup.js";
|
|
32
33
|
import { ensureConvergeJob } from "../converge-job-setup.js";
|
|
33
34
|
import { findSqlInstance, planPublicRoleSwitch } from "../gateway-credentials.js";
|
|
34
35
|
import type { GcloudRunner, Sleep } from "../gcloud-retry.js";
|
|
@@ -49,6 +50,8 @@ import { selfCommand } from "../self-command.js";
|
|
|
49
50
|
import {
|
|
50
51
|
AUDIT_JOB_ENV_VAR,
|
|
51
52
|
AUDIT_RESULTS_BUCKET_ENV_VAR,
|
|
53
|
+
CAPTURE_BUCKET_ENV_VAR,
|
|
54
|
+
CAPTURE_JOB_ENV_VAR,
|
|
52
55
|
type CloudRunEnvVar,
|
|
53
56
|
CONVERGE_JOB_ENV_VAR,
|
|
54
57
|
MCP_ENV_VAR,
|
|
@@ -331,6 +334,42 @@ export function startConverge(
|
|
|
331
334
|
leaveUntouched.push(AUDIT_JOB_ENV_VAR, AUDIT_RESULTS_BUCKET_ENV_VAR);
|
|
332
335
|
}
|
|
333
336
|
|
|
337
|
+
async function captureJob(): Promise<void> {
|
|
338
|
+
// #1217 — the job every server-side browser render runs in, so no
|
|
339
|
+
// Chromium runs in the admin. Deployed with the admin's CURRENT image
|
|
340
|
+
// (the app-roll then moves both together; the job refuses an order from
|
|
341
|
+
// another version). A failure only costs server-side screenshots, so it
|
|
342
|
+
// warns and the admin keeps its current env.
|
|
343
|
+
const s = r.step("Ensuring the capture job (server-side screenshots outside the admin)...");
|
|
344
|
+
const image = await resolveAdminImage(
|
|
345
|
+
{ projectId: install.projectId, region: install.region, env: install.env },
|
|
346
|
+
{ run },
|
|
347
|
+
);
|
|
348
|
+
const cj = image.ok
|
|
349
|
+
? await ensureCaptureJob(
|
|
350
|
+
{
|
|
351
|
+
provider: install.provider,
|
|
352
|
+
projectId: install.projectId,
|
|
353
|
+
region: install.region,
|
|
354
|
+
env: install.env,
|
|
355
|
+
adminImageRef: image.imageRef,
|
|
356
|
+
},
|
|
357
|
+
runDeps,
|
|
358
|
+
)
|
|
359
|
+
: { ok: false as const, done: [], error: image.error };
|
|
360
|
+
for (const step of cj.done) r.info(` ${step}`);
|
|
361
|
+
if (cj.ok) {
|
|
362
|
+
s.done("Capture job ready (screenshots run outside the admin)");
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
s.warned(`Capture job not set up: ${cj.error}`);
|
|
366
|
+
r.warn(
|
|
367
|
+
"The upgrade continues; server-side screenshots fail with the missing job until this succeeds. Re-run upgrade once the cause is fixed.",
|
|
368
|
+
);
|
|
369
|
+
incomplete = true;
|
|
370
|
+
leaveUntouched.push(CAPTURE_JOB_ENV_VAR, CAPTURE_BUCKET_ENV_VAR);
|
|
371
|
+
}
|
|
372
|
+
|
|
334
373
|
async function remoteMcpEdge(): Promise<void> {
|
|
335
374
|
// #806 — on `gcp` the load balancer routes the remote-MCP discovery
|
|
336
375
|
// paths to the gateway and Cloud Armor lets agent traffic through on
|
|
@@ -592,6 +631,7 @@ export function startConverge(
|
|
|
592
631
|
await mcpAccess();
|
|
593
632
|
await urlMapRedirectsAccess();
|
|
594
633
|
await auditJob();
|
|
634
|
+
await captureJob();
|
|
595
635
|
await remoteMcpEdge();
|
|
596
636
|
return true;
|
|
597
637
|
},
|
|
@@ -112,7 +112,7 @@ function fakeInstall(events: string[], overrides: [RegExp, Answer][] = []) {
|
|
|
112
112
|
[new RegExp(`describe ${GATEWAY} .*--format=json`), ok(GATEWAY_JSON)],
|
|
113
113
|
[/describe (\S+) .*status\.url/, (l) => ok(`https://${l.split(" ")[3]}-xyz.a.run.app\n`)],
|
|
114
114
|
[
|
|
115
|
-
/^run jobs describe caelo-production-audit .*containers\[0\]\.image/,
|
|
115
|
+
/^run jobs describe caelo-production-(audit|capture) .*containers\[0\]\.image/,
|
|
116
116
|
ok(`${OLD_ADMIN_IMAGE}\n`),
|
|
117
117
|
],
|
|
118
118
|
[/^sql instances list/, ok(`${SQL}\n`)],
|
|
@@ -242,7 +242,7 @@ const CONVERGE_WRITE =
|
|
|
242
242
|
/add-iam-policy-binding|remove-iam-policy-binding|set-iam-policy|service-accounts create|secrets create|secrets versions add|services enable|roles create|roles update|roles undelete|firewall-rules delete|users assign-roles/;
|
|
243
243
|
/** Calls that move a service or run migrations — app-roll-only. */
|
|
244
244
|
const APP_ROLL_WRITE =
|
|
245
|
-
/^run services update |^run services update-traffic|jobs execute caelo-migrate|^run jobs update caelo-production-audit /;
|
|
245
|
+
/^run services update |^run services update-traffic|jobs execute caelo-migrate|^run jobs update caelo-production-(audit|capture) /;
|
|
246
246
|
/** Any gcloud call that changes something. */
|
|
247
247
|
const ANY_WRITE =
|
|
248
248
|
/\b(create|update|update-traffic|delete|add-iam-policy-binding|remove-iam-policy-binding|set-iam-policy|execute|enable|add|restore)\b/;
|
|
@@ -331,6 +331,39 @@ describe("upgradeGcpInstall — converge, then app-roll", () => {
|
|
|
331
331
|
).toBe(false);
|
|
332
332
|
});
|
|
333
333
|
|
|
334
|
+
it("sets up the capture job on the CURRENT admin image before the roll, then moves it with the admin (#1217)", async () => {
|
|
335
|
+
const events: string[] = [];
|
|
336
|
+
const { deps, lines } = upgradeDeps(events);
|
|
337
|
+
expect(await upgradeGcpInstall(TARGET, deps)).toBe(true);
|
|
338
|
+
const deploy = indexOf(events, /^gcloud run jobs deploy caelo-production-capture /);
|
|
339
|
+
const adminRoll = indexOf(events, new RegExp(`^gcloud run services update ${ADMIN} `));
|
|
340
|
+
const move = indexOf(events, /^gcloud run jobs update caelo-production-capture /);
|
|
341
|
+
const deep = indexOf(events, /^health deep/);
|
|
342
|
+
expect(events[deploy]).toContain(`--image=${OLD_ADMIN_IMAGE}`);
|
|
343
|
+
expect(deploy < adminRoll && adminRoll < move && move < deep).toBe(true);
|
|
344
|
+
expect(events[move]).toContain(`--image=${IMAGES.admin.imageRef}`);
|
|
345
|
+
expect(events[adminRoll]).toContain(
|
|
346
|
+
"CAELO_CAPTURE_JOB=projects/acme/locations/europe-west1/jobs/caelo-production-capture",
|
|
347
|
+
);
|
|
348
|
+
expect(lines).toContain("done: Capture job ready (screenshots run outside the admin)");
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it("a failed capture-job setup leaves the converge incomplete and the admin's capture env untouched", async () => {
|
|
352
|
+
const events: string[] = [];
|
|
353
|
+
const { deps, recorded, lines } = upgradeDeps(events, {
|
|
354
|
+
overrides: [[/^run jobs deploy caelo-production-capture/, fail("PERMISSION_DENIED")]],
|
|
355
|
+
});
|
|
356
|
+
expect(await upgradeGcpInstall(TARGET, deps)).toBe(true);
|
|
357
|
+
expect(recorded.contract).toEqual([]);
|
|
358
|
+
expect(lines.some((l) => l.includes("Capture job not set up"))).toBe(true);
|
|
359
|
+
expect(
|
|
360
|
+
events.some((e) => /services update caelo-production-admin.*CAELO_CAPTURE_JOB=/.test(e)),
|
|
361
|
+
).toBe(false);
|
|
362
|
+
expect(
|
|
363
|
+
events.some((e) => /services update caelo-production-admin.*CAELO_CAPTURE_BUCKET=/.test(e)),
|
|
364
|
+
).toBe(false);
|
|
365
|
+
});
|
|
366
|
+
|
|
334
367
|
it("an unreadable audit job is reported as an error, not as a missing job, and is not moved", async () => {
|
|
335
368
|
const events: string[] = [];
|
|
336
369
|
const { deps, lines } = upgradeDeps(events, {
|
|
@@ -465,6 +498,11 @@ describe("upgradeGcpInstall — converge, then app-roll", () => {
|
|
|
465
498
|
.slice(deep)
|
|
466
499
|
.filter((e) => e.startsWith("gcloud run jobs update caelo-production-audit "));
|
|
467
500
|
expect(jobBack).toEqual([expect.stringContaining(`--image=${OLD_ADMIN_IMAGE}`)]);
|
|
501
|
+
// ... and so does the capture job (#1217).
|
|
502
|
+
const captureBack = events
|
|
503
|
+
.slice(deep)
|
|
504
|
+
.filter((e) => e.startsWith("gcloud run jobs update caelo-production-capture "));
|
|
505
|
+
expect(captureBack).toEqual([expect.stringContaining(`--image=${OLD_ADMIN_IMAGE}`)]);
|
|
468
506
|
expect(recorded.digests).toEqual([]);
|
|
469
507
|
expect(recorded.contract).toEqual([]);
|
|
470
508
|
const restore = `gcloud sql backups restore ${BACKUP_ID} --restore-instance=${SQL} --project=acme`;
|
|
@@ -85,6 +85,10 @@ function setupGrants(): Map<string, Set<string>> {
|
|
|
85
85
|
`projects/${p}/serviceAccounts/caelo-production-audit@${p}.iam.gserviceaccount.com`,
|
|
86
86
|
of("roles/iam.serviceAccountUser"),
|
|
87
87
|
],
|
|
88
|
+
[
|
|
89
|
+
`projects/${p}/serviceAccounts/caelo-production-capture@${p}.iam.gserviceaccount.com`,
|
|
90
|
+
of("roles/iam.serviceAccountUser"),
|
|
91
|
+
],
|
|
88
92
|
// The project-level caeloUpgraderRun reaches the job too.
|
|
89
93
|
[`projects/${p}/locations/europe-west1/jobs/caelo-production-upgrader`, project],
|
|
90
94
|
[`gs://${p}-caelo-ops`, bucketList],
|
|
@@ -133,6 +137,23 @@ describe("upgraderAccessChecks", () => {
|
|
|
133
137
|
);
|
|
134
138
|
});
|
|
135
139
|
|
|
140
|
+
it("#1217: the capture job's account is checked too, since every app-roll moves that job", async () => {
|
|
141
|
+
const grants = setupGrants();
|
|
142
|
+
grants.delete(
|
|
143
|
+
"projects/acme/serviceAccounts/caelo-production-capture@acme.iam.gserviceaccount.com",
|
|
144
|
+
);
|
|
145
|
+
const r = await runIamPreflight(upgraderAccessChecks(TARGET), tester(grants));
|
|
146
|
+
expect(r.kind).toBe("missing");
|
|
147
|
+
if (r.kind !== "missing") return;
|
|
148
|
+
expect(r.missing.map((m) => [resourceName(m.check.resource), m.missing])).toEqual([
|
|
149
|
+
[
|
|
150
|
+
"projects/acme/serviceAccounts/caelo-production-capture@acme.iam.gserviceaccount.com",
|
|
151
|
+
["iam.serviceAccounts.actAs"],
|
|
152
|
+
],
|
|
153
|
+
]);
|
|
154
|
+
expect(preflightMissingReason(r.missing, UPGRADER_PREFLIGHT_VOICE)).toContain("capture job");
|
|
155
|
+
});
|
|
156
|
+
|
|
136
157
|
it("never needs a permission only a forbidden role would give (no compute, no IAM admin)", () => {
|
|
137
158
|
const all = upgraderAccessChecks(TARGET).flatMap((c) => c.permissions);
|
|
138
159
|
for (const p of all) {
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
import { z } from "zod";
|
|
53
53
|
import {
|
|
54
54
|
auditServiceAccountEmail,
|
|
55
|
+
captureServiceAccountEmail,
|
|
55
56
|
gatewayServiceAccountEmail,
|
|
56
57
|
mcpIapServiceAccountEmail,
|
|
57
58
|
runServiceAccountEmail,
|
|
@@ -187,7 +188,7 @@ export function upgraderAccessChecks(target: {
|
|
|
187
188
|
},
|
|
188
189
|
{
|
|
189
190
|
// #875 — every app-roll moves the audit job onto the new admin image
|
|
190
|
-
// (upgrade/
|
|
191
|
+
// (upgrade/admin-image-jobs.ts), and upgrader-setup.ts grants actAs on
|
|
191
192
|
// the job's account for exactly that.
|
|
192
193
|
need: "move the quality-audit job onto the new admin image",
|
|
193
194
|
resource: project,
|
|
@@ -198,6 +199,12 @@ export function upgraderAccessChecks(target: {
|
|
|
198
199
|
resource: sa(auditServiceAccountEmail(projectId, env)),
|
|
199
200
|
permissions: ["iam.serviceAccounts.actAs"],
|
|
200
201
|
},
|
|
202
|
+
{
|
|
203
|
+
// #1217 — the capture job follows the admin's image the same way.
|
|
204
|
+
need: "run the capture job as its service account after moving it",
|
|
205
|
+
resource: sa(captureServiceAccountEmail(projectId, env)),
|
|
206
|
+
permissions: ["iam.serviceAccounts.actAs"],
|
|
207
|
+
},
|
|
201
208
|
{
|
|
202
209
|
need: "keep the upgrader job running as its own service account after it updates itself",
|
|
203
210
|
resource: sa(upgraderServiceAccountEmail(projectId, env)),
|
package/src/upgrader-job.test.ts
CHANGED
|
@@ -427,6 +427,7 @@ describe("runUpgraderJob", () => {
|
|
|
427
427
|
"iam projects/acme/locations/europe-west1/jobs/caelo-production-upgrader",
|
|
428
428
|
"iam projects/acme/serviceAccounts/caelo-mcp@acme.iam.gserviceaccount.com",
|
|
429
429
|
"iam projects/acme/serviceAccounts/caelo-production-audit@acme.iam.gserviceaccount.com",
|
|
430
|
+
"iam projects/acme/serviceAccounts/caelo-production-capture@acme.iam.gserviceaccount.com",
|
|
430
431
|
"iam projects/acme/serviceAccounts/caelo-production-gateway-sa@acme.iam.gserviceaccount.com",
|
|
431
432
|
"iam projects/acme/serviceAccounts/caelo-production-run-sa@acme.iam.gserviceaccount.com",
|
|
432
433
|
"iam projects/acme/serviceAccounts/caelo-production-upgrader@acme.iam.gserviceaccount.com",
|
|
@@ -84,6 +84,7 @@ describe("ensureUpgraderJob", () => {
|
|
|
84
84
|
"iam service-accounts add-iam-policy-binding caelo-production-upgrader@acme.iam.gserviceaccount.com → roles/iam.serviceAccountUser",
|
|
85
85
|
"iam service-accounts add-iam-policy-binding caelo-mcp@acme.iam.gserviceaccount.com → roles/iam.serviceAccountTokenCreator",
|
|
86
86
|
"iam service-accounts add-iam-policy-binding caelo-production-audit@acme.iam.gserviceaccount.com → roles/iam.serviceAccountUser",
|
|
87
|
+
"iam service-accounts add-iam-policy-binding caelo-production-capture@acme.iam.gserviceaccount.com → roles/iam.serviceAccountUser",
|
|
87
88
|
"storage buckets add-iam-policy-binding gs://acme-caelo-ops → roles/storage.objectUser",
|
|
88
89
|
"storage buckets add-iam-policy-binding gs://acme-caelo-ops → projects/acme/roles/caeloUpgraderOpsList",
|
|
89
90
|
].sort(),
|
package/src/upgrader-setup.ts
CHANGED
|
@@ -32,8 +32,9 @@
|
|
|
32
32
|
* setup is removed.
|
|
33
33
|
* - `roles/iam.serviceAccountUser` on exactly the admin's and the
|
|
34
34
|
* gateway's runtime service accounts (a revision runs as them), on
|
|
35
|
-
* itself (its own job) and on the quality-audit
|
|
36
|
-
* app-roll moves
|
|
35
|
+
* itself (its own job) and on the quality-audit and capture jobs'
|
|
36
|
+
* accounts (the app-roll moves those jobs' images, #875, #1217) — not
|
|
37
|
+
* on the project.
|
|
37
38
|
* - the custom role `caeloUpgraderSqlBackup`: create/read Cloud SQL
|
|
38
39
|
* backups and read the instance — no restore, no instance changes.
|
|
39
40
|
* - `roles/cloudsql.instanceUser` + a Cloud SQL IAM database user that
|
|
@@ -76,6 +77,7 @@ import { gcloud as defaultGcloud, type GcloudResult } from "./gcloud.js";
|
|
|
76
77
|
import { type GcloudRunner, realSleep, runWithRetry, type Sleep } from "./gcloud-retry.js";
|
|
77
78
|
import {
|
|
78
79
|
auditServiceAccountEmail,
|
|
80
|
+
captureServiceAccountEmail,
|
|
79
81
|
gatewayServiceAccountEmail,
|
|
80
82
|
mcpIapServiceAccountEmail,
|
|
81
83
|
runServiceAccountEmail,
|
|
@@ -371,6 +373,11 @@ export async function ensureUpgraderJob(
|
|
|
371
373
|
role: "roles/iam.serviceAccountUser",
|
|
372
374
|
why: "the app-roll moves the quality-audit job, which runs as it, onto the new image",
|
|
373
375
|
},
|
|
376
|
+
{
|
|
377
|
+
account: captureServiceAccountEmail(projectId, env),
|
|
378
|
+
role: "roles/iam.serviceAccountUser",
|
|
379
|
+
why: "the app-roll moves the capture job, which runs as it, onto the new image",
|
|
380
|
+
},
|
|
374
381
|
{
|
|
375
382
|
account: mcpIapServiceAccountEmail(projectId),
|
|
376
383
|
role: "roles/iam.serviceAccountTokenCreator",
|
package/src/wizards/gcp-cost.ts
CHANGED
|
@@ -109,6 +109,12 @@ export function estimateGcpCost(inputs: CostEstimateInputs): {
|
|
|
109
109
|
notes:
|
|
110
110
|
"one container per Lighthouse run, all in parallel (up to 5 pages x 3 runs, ~90 s at 2 vCPU each): ~$0.05 per audited Stage, partly in the free tier; results bucket ~$0",
|
|
111
111
|
},
|
|
112
|
+
{
|
|
113
|
+
name: "Screenshots (Cloud Run capture job)",
|
|
114
|
+
monthlyUsd: 1,
|
|
115
|
+
notes:
|
|
116
|
+
"one short container per server-side screenshot or page render (~20-40 s at 1 vCPU / 2 GiB): ~$0.001 each, a 50-page site import ~$0.05; mostly in the free tier; capture bucket ~$0",
|
|
117
|
+
},
|
|
112
118
|
{
|
|
113
119
|
name: "Cloud Run gateway",
|
|
114
120
|
monthlyUsd: inputs.gatewayMinInstances === 0 ? 1 : 1 + inputs.gatewayMinInstances * 15,
|
package/src/wizards/gcp.ts
CHANGED
|
@@ -35,6 +35,7 @@ import { bold, cyan, dim, green, red, yellow } from "kleur/colors";
|
|
|
35
35
|
import { checkAdcImpersonation } from "../adc-impersonation.js";
|
|
36
36
|
import { type AdoptionCandidate, AdoptionError, formatAdopted } from "../adopt-existing.js";
|
|
37
37
|
import { ensureAuditJob, resolveAdminImage } from "../audit-job-setup.js";
|
|
38
|
+
import { ensureCaptureJob } from "../capture-job-setup.js";
|
|
38
39
|
import { CERT_WAIT_TIMEOUT_MS, lbCertProbe, waitForCert } from "../cert-wait.js";
|
|
39
40
|
import { redactCredentials } from "../checkpoint-secrets.js";
|
|
40
41
|
import { ensureConvergeJob } from "../converge-job-setup.js";
|
|
@@ -507,6 +508,10 @@ export async function runGcpConverge(opts: GcpConvergeOpts): Promise<void> {
|
|
|
507
508
|
// Before the upgrader, which gets actAs on the job's service account.
|
|
508
509
|
await stepAuditJob(projectId, region, provider);
|
|
509
510
|
|
|
511
|
+
// === 11.35. The capture job: server-side screenshots outside the admin (#1217) ===
|
|
512
|
+
// Before the upgrader as well, which gets actAs on its service account.
|
|
513
|
+
await stepCaptureJob(projectId, region, provider);
|
|
514
|
+
|
|
510
515
|
// === 11.4. The upgrader job: Settings → Updates → Install update (#696) ===
|
|
511
516
|
// Before operator access, which keeps the upgrader's caelo-mcp grant.
|
|
512
517
|
const releaseVersion = await releaseVersionForDigest(gcloud, "admin", imageDigests.admin);
|
|
@@ -1149,6 +1154,37 @@ async function stepAuditJob(
|
|
|
1149
1154
|
s.stop(green(`Quality-audit job ready (${r.done.length} steps)`));
|
|
1150
1155
|
}
|
|
1151
1156
|
|
|
1157
|
+
/**
|
|
1158
|
+
* Set up the capture job (capture-job-setup.ts) on the deployed admin's
|
|
1159
|
+
* image. A failure stops the wizard: without the job every server-side
|
|
1160
|
+
* screenshot fails. Idempotent, so it runs on every wizard pass.
|
|
1161
|
+
*/
|
|
1162
|
+
async function stepCaptureJob(
|
|
1163
|
+
projectId: string,
|
|
1164
|
+
region: string,
|
|
1165
|
+
provider: "gcp" | "gcp-firebase",
|
|
1166
|
+
): Promise<void> {
|
|
1167
|
+
const s = spinner();
|
|
1168
|
+
s.start("Setting up the capture job (server-side screenshots outside the admin)...");
|
|
1169
|
+
const image = await resolveAdminImage({ projectId, region, env: GCP_STACK_ENV });
|
|
1170
|
+
const r = image.ok
|
|
1171
|
+
? await ensureCaptureJob({
|
|
1172
|
+
provider,
|
|
1173
|
+
projectId,
|
|
1174
|
+
region,
|
|
1175
|
+
env: GCP_STACK_ENV,
|
|
1176
|
+
adminImageRef: image.imageRef,
|
|
1177
|
+
})
|
|
1178
|
+
: { ok: false as const, done: [], error: image.error };
|
|
1179
|
+
if (!r.ok) {
|
|
1180
|
+
s.stop(red(`Failed: ${r.error}`));
|
|
1181
|
+
log.error("Re-run the wizard once the cause is fixed (this step is idempotent).");
|
|
1182
|
+
cancel("Aborted.");
|
|
1183
|
+
process.exit(1);
|
|
1184
|
+
}
|
|
1185
|
+
s.stop(green(`Capture job ready (${r.done.length} steps)`));
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1152
1188
|
/**
|
|
1153
1189
|
* Whether an upgrader set up by an earlier run exists, so a failed setup
|
|
1154
1190
|
* this time still keeps its caelo-mcp signing grant (the operator-access
|
package/stacks/aws/index.ts
CHANGED
|
@@ -398,6 +398,15 @@ new aws.cloudwatch.LogDelivery(
|
|
|
398
398
|
// (quality/lighthouse-runner-select.ts) instead of measuring on its own
|
|
399
399
|
// shared CPU. Nothing is staged on AWS before #749 either.
|
|
400
400
|
//
|
|
401
|
+
// FIXME(issue #749): no capture task definition yet. On GCP every
|
|
402
|
+
// server-side browser render (screenshots, external page renders, the
|
|
403
|
+
// site-import crawl) runs in its own short-lived container (#1217,
|
|
404
|
+
// src/capture-job-setup.ts) so the admin never starts Chromium; the AWS
|
|
405
|
+
// equivalent is a Fargate task per render behind admin-core
|
|
406
|
+
// capture/capture-client.ts `CaptureLauncher`. Until it exists the admin
|
|
407
|
+
// refuses every server-side render with that gap
|
|
408
|
+
// (capture/capture-backend.ts) instead of running a browser in its own task.
|
|
409
|
+
//
|
|
401
410
|
// FIXME(issue #696): no in-cloud upgrader or converge job (#819) yet. On
|
|
402
411
|
// GCP Settings → Updates hands app-roll releases to the upgrader job and
|
|
403
412
|
// cloud-setup releases to the converge job; on AWS the admin refuses a
|
package/stacks/azure/index.ts
CHANGED
|
@@ -361,6 +361,15 @@ const logWorkspace = new azure.operationalinsights.Workspace(`${namePrefix}-logs
|
|
|
361
361
|
// instead of measuring on its own shared CPU. Nothing is staged on Azure
|
|
362
362
|
// before #750 either.
|
|
363
363
|
//
|
|
364
|
+
// FIXME(issue #750): no capture Container Apps job yet. On GCP every
|
|
365
|
+
// server-side browser render (screenshots, external page renders, the
|
|
366
|
+
// site-import crawl) runs in its own short-lived container (#1217,
|
|
367
|
+
// src/capture-job-setup.ts) so the admin never starts Chromium; the Azure
|
|
368
|
+
// equivalent is a Container Apps job execution per render behind admin-core
|
|
369
|
+
// capture/capture-client.ts `CaptureLauncher`. Until it exists the admin
|
|
370
|
+
// refuses every server-side render with that gap
|
|
371
|
+
// (capture/capture-backend.ts) instead of running a browser in its own app.
|
|
372
|
+
//
|
|
364
373
|
// FIXME(issue #696): no in-cloud upgrader or converge job (#819) yet. On
|
|
365
374
|
// GCP Settings → Updates hands app-roll releases to the upgrader job and
|
|
366
375
|
// cloud-setup releases to the converge job; on Azure the admin refuses a
|
package/stacks/gcp/index.ts
CHANGED
|
@@ -1057,6 +1057,19 @@ new gcp.iap.WebBackendServiceIamMember(
|
|
|
1057
1057
|
// bucket or job both this program and `upgrade` created would collide on
|
|
1058
1058
|
// the next wizard run. The app-roll moves the job's image with the admin's.
|
|
1059
1059
|
|
|
1060
|
+
// =========================================================================
|
|
1061
|
+
// Capture job — deliberately NOT here
|
|
1062
|
+
// =========================================================================
|
|
1063
|
+
//
|
|
1064
|
+
// #1217 — every server-side browser render (screenshots, rendered HTML of
|
|
1065
|
+
// external pages, the site-import crawl) runs in a short-lived Cloud Run job,
|
|
1066
|
+
// one execution per render, so no Chromium runs in the admin. Its service
|
|
1067
|
+
// account may only create results under `out/` of the capture bucket; it
|
|
1068
|
+
// reads its input and a preview's media through URLs the admin signs per
|
|
1069
|
+
// capture. The CLI owns the job, its account, the bucket and the grants
|
|
1070
|
+
// (packages/provisioning/src/capture-job-setup.ts), for the same reason as
|
|
1071
|
+
// the audit job above.
|
|
1072
|
+
|
|
1060
1073
|
// =========================================================================
|
|
1061
1074
|
// Operator access — deliberately NOT here
|
|
1062
1075
|
// =========================================================================
|
|
@@ -964,6 +964,19 @@ new gcp.iap.WebCloudRunServiceIamMember(
|
|
|
964
964
|
// bucket or job both this program and `upgrade` created would collide on
|
|
965
965
|
// the next wizard run. The app-roll moves the job's image with the admin's.
|
|
966
966
|
|
|
967
|
+
// =========================================================================
|
|
968
|
+
// Capture job — deliberately NOT here
|
|
969
|
+
// =========================================================================
|
|
970
|
+
//
|
|
971
|
+
// #1217 — every server-side browser render (screenshots, rendered HTML of
|
|
972
|
+
// external pages, the site-import crawl) runs in a short-lived Cloud Run job,
|
|
973
|
+
// one execution per render, so no Chromium runs in the admin. Its service
|
|
974
|
+
// account may only create results under `out/` of the capture bucket; it
|
|
975
|
+
// reads its input and a preview's media through URLs the admin signs per
|
|
976
|
+
// capture. The CLI owns the job, its account, the bucket and the grants
|
|
977
|
+
// (packages/provisioning/src/capture-job-setup.ts), for the same reason as
|
|
978
|
+
// the audit job above.
|
|
979
|
+
|
|
967
980
|
// =========================================================================
|
|
968
981
|
// Operator access — deliberately NOT here
|
|
969
982
|
// =========================================================================
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* #875 — the quality-audit job runs the admin image (admin-core
|
|
3
|
-
* quality/audit-task.ts) and refuses a batch from an admin on another
|
|
4
|
-
* version. So every app-roll moves the job onto the admin's new image right
|
|
5
|
-
* after the services rolled, and a rollback moves it back.
|
|
6
|
-
*
|
|
7
|
-
* `run jobs update --image` changes nothing else of the job (identity, env,
|
|
8
|
-
* parallelism), so this stays inside the app-roll's rights: the upgrader's
|
|
9
|
-
* `roles/run.developer` plus actAs on the job's service account
|
|
10
|
-
* (upgrader-setup.ts).
|
|
11
|
-
*/
|
|
12
|
-
import type { GcloudRunner } from "../gcloud-retry.js";
|
|
13
|
-
/**
|
|
14
|
-
* The job's image now; `image: null` only when the install has no audit job
|
|
15
|
-
* yet. Any other gcloud failure (permissions, API, network) is an error, not
|
|
16
|
-
* "no job", so the app-roll reports it instead of a misleading hint.
|
|
17
|
-
*/
|
|
18
|
-
export declare function describeAuditJobImage(run: GcloudRunner, install: {
|
|
19
|
-
readonly projectId: string;
|
|
20
|
-
readonly region: string;
|
|
21
|
-
readonly env: string;
|
|
22
|
-
}): Promise<{
|
|
23
|
-
readonly ok: true;
|
|
24
|
-
readonly image: string | null;
|
|
25
|
-
} | {
|
|
26
|
-
readonly ok: false;
|
|
27
|
-
readonly error: string;
|
|
28
|
-
}>;
|
|
29
|
-
/** Point the audit job at `imageRef`. */
|
|
30
|
-
export declare function setAuditJobImage(run: GcloudRunner, install: {
|
|
31
|
-
readonly projectId: string;
|
|
32
|
-
readonly region: string;
|
|
33
|
-
readonly env: string;
|
|
34
|
-
}, imageRef: string): Promise<{
|
|
35
|
-
readonly ok: boolean;
|
|
36
|
-
readonly error?: string;
|
|
37
|
-
}>;
|
|
38
|
-
//# sourceMappingURL=audit-job-roll.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"audit-job-roll.d.ts","sourceRoot":"","sources":["../../src/upgrade/audit-job-roll.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAGvD;;;;GAIG;AACH,wBAAsB,qBAAqB,CACzC,GAAG,EAAE,YAAY,EACjB,OAAO,EAAE;IAAE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GACrF,OAAO,CACN;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GACpD;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CACjD,CAsBA;AAED,yCAAyC;AACzC,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,YAAY,EACjB,OAAO,EAAE;IAAE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CAAE,EACtF,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC;IAAE,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAY5D"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"audit-job-roll.js","sourceRoot":"","sources":["../../src/upgrade/audit-job-roll.ts"],"names":[],"mappings":"AAAA,mCAAmC;AAenC,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAE/C;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACzC,GAAiB,EACjB,OAAsF;IAKtF,MAAM,CAAC,GAAG,MAAM,GAAG,CAAC;QAClB,KAAK;QACL,MAAM;QACN,UAAU;QACV,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC;QACzB,YAAY,OAAO,CAAC,MAAM,EAAE;QAC5B,aAAa,OAAO,CAAC,SAAS,EAAE;QAChC,sEAAsE;KACvE,CAAC,CAAC;IACH,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;QACV,wEAAwE;QACxE,0CAA0C;QAC1C,OAAO,qDAAqD,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC;YACzE,CAAC,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE;YAC3B,CAAC,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC;IAC5C,CAAC;IACD,MAAM,KAAK,GAAG,CAAC,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;IAC9B,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,gDAAgD,EAAE,CAAC;IAChF,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;AAC7B,CAAC;AAED,yCAAyC;AACzC,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,GAAiB,EACjB,OAAsF,EACtF,QAAgB;IAEhB,MAAM,CAAC,GAAG,MAAM,GAAG,CAAC;QAClB,KAAK;QACL,MAAM;QACN,QAAQ;QACR,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC;QACzB,WAAW,QAAQ,EAAE;QACrB,YAAY,OAAO,CAAC,MAAM,EAAE;QAC5B,aAAa,OAAO,CAAC,SAAS,EAAE;QAChC,SAAS;KACV,CAAC,CAAC;IACH,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC;AACrE,CAAC"}
|