@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/gcloud.ts
CHANGED
|
@@ -86,6 +86,71 @@ export async function activeAccount(
|
|
|
86
86
|
return value.length > 0 ? value : null;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/**
|
|
90
|
+
* gcloud's stderr when the stored credentials can no longer mint a token:
|
|
91
|
+
* an expired or revoked refresh token, a reauth policy that wants the
|
|
92
|
+
* password again, or a reauth prompt it may not show because prompts are off
|
|
93
|
+
* ({@link gcloudEnv}). Every one is fixed by logging in again, not by
|
|
94
|
+
* retrying (#630).
|
|
95
|
+
*/
|
|
96
|
+
const GCLOUD_AUTH_ERROR =
|
|
97
|
+
/Reauthentication|reauth|invalid_grant|cannot prompt|refreshing your current auth tokens|do not currently have an active account/i;
|
|
98
|
+
|
|
99
|
+
/** Whether a failed gcloud call failed because its credentials expired (#630). */
|
|
100
|
+
export function isGcloudAuthError(stderr: string): boolean {
|
|
101
|
+
return GCLOUD_AUTH_ERROR.test(stderr);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The fix for expired credentials on the Owner's machine or in Cloud Shell. */
|
|
105
|
+
export const GCLOUD_REAUTH_HINT =
|
|
106
|
+
"gcloud credentials expired. Run `gcloud auth login` (and `gcloud auth application-default login` where Pulumi runs), then run this command again.";
|
|
107
|
+
|
|
108
|
+
/** The first non-empty line of gcloud's stderr, for a one-line error. */
|
|
109
|
+
export function gcloudErrorLine(stderr: string): string {
|
|
110
|
+
return (
|
|
111
|
+
stderr
|
|
112
|
+
.split("\n")
|
|
113
|
+
.map((l) => l.replace(/^ERROR:\s*/, "").trim())
|
|
114
|
+
.find((l) => l.length > 0) ?? "no error output"
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export type GcloudAuthCheck =
|
|
119
|
+
| { readonly ok: true; readonly account: string }
|
|
120
|
+
| { readonly ok: false; readonly error: string };
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Pre-flight for every lifecycle command that talks to GCP: an active
|
|
124
|
+
* account AND a token gcloud can mint now. {@link activeAccount} alone
|
|
125
|
+
* passes with expired credentials (`auth list` reads local config only), so
|
|
126
|
+
* the first real lookup failed and read as "service not found" (#630).
|
|
127
|
+
* The token itself is discarded.
|
|
128
|
+
*/
|
|
129
|
+
export async function checkGcloudAuth(
|
|
130
|
+
run: (args: string[]) => Promise<GcloudResult> = gcloud,
|
|
131
|
+
): Promise<GcloudAuthCheck> {
|
|
132
|
+
const account = await activeAccount(run);
|
|
133
|
+
if (!account) {
|
|
134
|
+
return {
|
|
135
|
+
ok: false,
|
|
136
|
+
error:
|
|
137
|
+
"No gcloud account is logged in. Run `gcloud auth login` with the Owner's Google account, then run this command again.",
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const token = await run(["auth", "print-access-token", "--quiet"]);
|
|
141
|
+
if (token.ok) return { ok: true, account };
|
|
142
|
+
if (isGcloudAuthError(token.stderr)) {
|
|
143
|
+
return {
|
|
144
|
+
ok: false,
|
|
145
|
+
error: `${GCLOUD_REAUTH_HINT} (${account}: ${gcloudErrorLine(token.stderr)})`,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
ok: false,
|
|
150
|
+
error: `gcloud could not get an access token for ${account}: ${gcloudErrorLine(token.stderr)}. Check \`gcloud auth list\` and the network, then run this command again.`,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
89
154
|
export interface BillingAccount {
|
|
90
155
|
id: string;
|
|
91
156
|
displayName: string;
|
|
@@ -116,6 +181,23 @@ export async function projectExists(projectId: string): Promise<boolean> {
|
|
|
116
181
|
return r.ok && r.stdout.trim() === projectId;
|
|
117
182
|
}
|
|
118
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Human-readable GCP project name for an install, e.g. `Caelo searchviu-com`.
|
|
186
|
+
* Carries the domain so an operator with several installs can tell them apart
|
|
187
|
+
* in the Cloud Console project picker (every install used to be "Caelo CMS").
|
|
188
|
+
* GCP display names allow only letters, digits, spaces, hyphens, quotes and
|
|
189
|
+
* `!`, at 4–30 chars — so dots become hyphens and long domains are cut.
|
|
190
|
+
*/
|
|
191
|
+
export function projectDisplayName(domain: string): string {
|
|
192
|
+
const prefix = "Caelo ";
|
|
193
|
+
const slug = domain
|
|
194
|
+
.toLowerCase()
|
|
195
|
+
.replace(/[^a-z0-9-]+/g, "-")
|
|
196
|
+
.slice(0, 30 - prefix.length)
|
|
197
|
+
.replace(/^-+|-+$/g, "");
|
|
198
|
+
return `${prefix}${slug}`;
|
|
199
|
+
}
|
|
200
|
+
|
|
119
201
|
export async function createProject(projectId: string, displayName: string): Promise<GcloudResult> {
|
|
120
202
|
return gcloud(["projects", "create", projectId, "--name", displayName]);
|
|
121
203
|
}
|
package/src/index.test.ts
CHANGED
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
import { describe, expect, it } from "bun:test";
|
|
4
4
|
import { readFileSync } from "node:fs";
|
|
5
5
|
import { join } from "node:path";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
CONTENT_HASHED_PATH_PATTERN,
|
|
8
|
+
IMMUTABLE_CACHE_CONTROL,
|
|
9
|
+
SVG_DOCUMENT_CSP,
|
|
10
|
+
} from "@caelo-cms/shared";
|
|
7
11
|
import { generateBootstrapToken } from "./bootstrap-token.js";
|
|
8
12
|
import { generateCaddyfile } from "./caddy.js";
|
|
9
13
|
import { loadCdnCopyAdapter, selfHostedCdnCopy } from "./cdn-copy.js";
|
|
@@ -92,7 +96,37 @@ describe("Caddy cache policy for content-hashed assets", () => {
|
|
|
92
96
|
);
|
|
93
97
|
expect(body).toContain(`header @content_hashed Cache-Control "${IMMUTABLE_CACHE_CONTROL}"`);
|
|
94
98
|
expect(body).toContain('\t@ico path *.ico\n\theader @ico Content-Type "image/x-icon"\n');
|
|
99
|
+
// #891 — the same security headers as the generated vhosts.
|
|
100
|
+
expect(body).toContain('\theader X-Content-Type-Options "nosniff"\n');
|
|
101
|
+
expect(body).toContain(
|
|
102
|
+
`\t@svg path *.svg\n\theader @svg Content-Security-Policy "${SVG_DOCUMENT_CSP}"\n`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// #891 regression: the public vhost served uploaded SVGs without a CSP, so
|
|
108
|
+
// a script that got past sanitizeSvg ran on the site's origin (which also
|
|
109
|
+
// serves /api/*) when the SVG's URL was opened.
|
|
110
|
+
it("public vhosts (live and staging) sandbox SVGs and nosniff everything; admin vhosts are untouched", () => {
|
|
111
|
+
const out = generateCaddyfile({
|
|
112
|
+
...spec,
|
|
113
|
+
domains: [
|
|
114
|
+
...spec.domains,
|
|
115
|
+
{ hostname: "staging.example.com", kind: "public", env: "staging" },
|
|
116
|
+
],
|
|
117
|
+
});
|
|
118
|
+
const blocks = [
|
|
119
|
+
out.match(/\nexample\.com \{[\s\S]*?\n\}/)?.[0] ?? "",
|
|
120
|
+
out.match(/\nstaging\.example\.com \{[\s\S]*?\n\}/)?.[0] ?? "",
|
|
121
|
+
];
|
|
122
|
+
for (const block of blocks) {
|
|
123
|
+
expect(block).toContain(' header X-Content-Type-Options "nosniff"\n');
|
|
124
|
+
expect(block).toContain(
|
|
125
|
+
` @svg path *.svg\n header @svg Content-Security-Policy "${SVG_DOCUMENT_CSP}"\n`,
|
|
126
|
+
);
|
|
95
127
|
}
|
|
128
|
+
const adminBlock = out.match(/admin\.example\.com \{[\s\S]*?\n\}/)?.[0] ?? "";
|
|
129
|
+
expect(adminBlock).not.toContain("Content-Security-Policy");
|
|
96
130
|
});
|
|
97
131
|
|
|
98
132
|
it("public vhosts serve .ico favicons as image/x-icon; admin vhosts are untouched", () => {
|
|
@@ -117,6 +151,7 @@ describe("docker-compose generator", () => {
|
|
|
117
151
|
postgresPassword: "supersecret",
|
|
118
152
|
rolePasswords: { admin: "adminpw", public: "publicpw" },
|
|
119
153
|
caeloSecretKek: "0".repeat(64),
|
|
154
|
+
internalSecret: "i".repeat(48),
|
|
120
155
|
anthropicApiKey: "sk-ant-…",
|
|
121
156
|
diskSize: "20Gi",
|
|
122
157
|
images: RELEASE_IMAGES,
|
|
@@ -138,6 +173,7 @@ describe("docker-compose generator", () => {
|
|
|
138
173
|
postgresPassword: "supersecret",
|
|
139
174
|
rolePasswords: { admin: "adminpw", public: "publicpw" },
|
|
140
175
|
caeloSecretKek: "0".repeat(64),
|
|
176
|
+
internalSecret: "i".repeat(48),
|
|
141
177
|
diskSize: "20Gi",
|
|
142
178
|
images: RELEASE_IMAGES,
|
|
143
179
|
ports: { http: 80, https: 443 },
|
|
@@ -162,6 +198,7 @@ describe("docker-compose generator", () => {
|
|
|
162
198
|
postgresPassword: 'pa"ss',
|
|
163
199
|
rolePasswords: { admin: "a", public: "b" },
|
|
164
200
|
caeloSecretKek: "0".repeat(64),
|
|
201
|
+
internalSecret: "i".repeat(48),
|
|
165
202
|
diskSize: "1Gi",
|
|
166
203
|
images: RELEASE_IMAGES,
|
|
167
204
|
ports: { http: 80, https: 443 },
|
|
@@ -20,8 +20,8 @@ describe("infrastructure contract steps", () => {
|
|
|
20
20
|
);
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
-
it("is at
|
|
24
|
-
expect(INFRA_CONTRACT_VERSION).toBe(
|
|
23
|
+
it("is at 13: public_role revoke (3), URL-map redirects role (4), no gateway cms_admin login (5), ops-bucket list (6), gateway proxy hops (7), quality-audit job (8), converge job (9), remote MCP (10), Google sign-in (11), project-level sign-in client (12), edge security headers (13)", () => {
|
|
24
|
+
expect(INFRA_CONTRACT_VERSION).toBe(13);
|
|
25
25
|
expect(INFRA_CONTRACT_STEPS[3]).toContain("#633");
|
|
26
26
|
expect(INFRA_CONTRACT_STEPS[4]).toContain("#587");
|
|
27
27
|
expect(INFRA_CONTRACT_STEPS[5]).toContain("#635");
|
|
@@ -32,6 +32,7 @@ describe("infrastructure contract steps", () => {
|
|
|
32
32
|
expect(INFRA_CONTRACT_STEPS[10]).toContain("#806");
|
|
33
33
|
expect(INFRA_CONTRACT_STEPS[11]).toContain("#914");
|
|
34
34
|
expect(INFRA_CONTRACT_STEPS[12]).toContain("project's IAP settings");
|
|
35
|
+
expect(INFRA_CONTRACT_STEPS[13]).toContain("#891");
|
|
35
36
|
});
|
|
36
37
|
|
|
37
38
|
it("lists the pending steps oldest first", () => {
|
|
@@ -42,6 +43,6 @@ describe("infrastructure contract steps", () => {
|
|
|
42
43
|
});
|
|
43
44
|
|
|
44
45
|
it("names a version a newer release added by number", () => {
|
|
45
|
-
expect(infraStepsText(
|
|
46
|
+
expect(infraStepsText(13, 14)).toBe("infrastructure step 14 (see that release's notes)");
|
|
46
47
|
});
|
|
47
48
|
});
|
package/src/infra-contract.ts
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* whose app-roll references infrastructure an install does not have yet fails
|
|
23
23
|
* at roll time, after migrations ran.
|
|
24
24
|
*/
|
|
25
|
-
export const INFRA_CONTRACT_VERSION =
|
|
25
|
+
export const INFRA_CONTRACT_VERSION = 13;
|
|
26
26
|
|
|
27
27
|
// History (why each bump happened — an install below a number needs the
|
|
28
28
|
// converge that introduced it):
|
|
@@ -93,6 +93,14 @@ export const INFRA_CONTRACT_VERSION = 12;
|
|
|
93
93
|
// (`iap.projects.getSettings` / `updateSettings`) on the project. New
|
|
94
94
|
// IAM the in-cloud upgrader cannot grant; without it the new job image
|
|
95
95
|
// fails every Google sign-in sync with 403.
|
|
96
|
+
// 13 — #891: the public site's security headers at the edge. On `gcp` the
|
|
97
|
+
// static backend bucket gets the `X-Content-Type-Options: nosniff`
|
|
98
|
+
// custom response header, the live SVG objects get
|
|
99
|
+
// `Content-Disposition: attachment` and Cloud CDN is purged once
|
|
100
|
+
// (static-svg-gcp.ts); aws / azure gain response-header policies /
|
|
101
|
+
// a Front Door rule set (static-security-headers.ts). Backend-bucket,
|
|
102
|
+
// object-metadata and CDN writes the in-cloud upgrader holds no rights
|
|
103
|
+
// for, so the converge applies them.
|
|
96
104
|
|
|
97
105
|
/**
|
|
98
106
|
* What each contract version adds, in operator terms — the upgrader's
|
|
@@ -112,6 +120,7 @@ export const INFRA_CONTRACT_STEPS: Readonly<Record<number, string>> = {
|
|
|
112
120
|
10: "remote MCP: the OAuth client and seal-key secrets, their IAM, the gateway's secret file and timeout, IAP programmatic-client sync and the load-balancer rules (#806)",
|
|
113
121
|
11: "Google sign-in: the renamed sign-in client secret, its IAM and mounts, the operator-access job's read access to it, and the admin's URL and organisation domain (#914)",
|
|
114
122
|
12: "Google sign-in at project level: the operator-access job's permission to set the sign-in client in the project's IAP settings (#914)",
|
|
123
|
+
13: "the public site's security headers at the edge: nosniff on every file, uploaded SVGs no longer run as documents on the site's origin, and a one-time CDN purge (#891)",
|
|
115
124
|
};
|
|
116
125
|
|
|
117
126
|
/**
|
package/src/install-job.ts
CHANGED
|
@@ -69,11 +69,14 @@ export const OPS_IMAGE_REPOSITORY =
|
|
|
69
69
|
* tail it. Granted only to installs that use the job. Not an escalation: the
|
|
70
70
|
* provisioner already holds `resourcemanager.projectIamAdmin`.
|
|
71
71
|
*/
|
|
72
|
-
export const INSTALL_JOB_ROLES
|
|
72
|
+
export const INSTALL_JOB_ROLES = [
|
|
73
73
|
"roles/iam.roleAdmin",
|
|
74
74
|
"roles/cloudscheduler.admin",
|
|
75
75
|
"roles/logging.logWriter",
|
|
76
|
-
];
|
|
76
|
+
] as const;
|
|
77
|
+
|
|
78
|
+
/** One of {@link INSTALL_JOB_ROLES}. */
|
|
79
|
+
export type InstallJobRole = (typeof INSTALL_JOB_ROLES)[number];
|
|
77
80
|
|
|
78
81
|
/** Resources the job runs with: Pulumi + the Node language host want memory. */
|
|
79
82
|
const JOB_CPU = "2";
|
package/src/install-state.ts
CHANGED
|
@@ -230,6 +230,19 @@ export function installRoot(installId: string): string {
|
|
|
230
230
|
return join(homedir(), `${ROOT_PREFIX}${installId}`);
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
+
/**
|
|
234
|
+
* Where Pulumi runs for one install (#736): `<installRoot>/stack/<provider>/`.
|
|
235
|
+
* It holds the install's own stack settings file (`Pulumi.production.yaml`)
|
|
236
|
+
* and a project file pointing at the package's stack program
|
|
237
|
+
* (wizards/gcp-pulumi.ts `prepareStackWorkDir`). The settings file used to
|
|
238
|
+
* live in the package's stack dir, shared by every install of the provider
|
|
239
|
+
* on the machine: two CLI runs for different installs at once could preview
|
|
240
|
+
* or `up` one install's state with the other's config.
|
|
241
|
+
*/
|
|
242
|
+
export function stackWorkDir(installRoot: string, provider: "gcp" | "gcp-firebase"): string {
|
|
243
|
+
return join(installRoot, "stack", provider);
|
|
244
|
+
}
|
|
245
|
+
|
|
233
246
|
/**
|
|
234
247
|
* Stable install id from the (provider, projectId-or-domain) pair so a re-run
|
|
235
248
|
* with the same inputs always lands on the same `~/.caelo-<id>/` directory.
|
package/src/lifecycle.ts
CHANGED
|
@@ -16,11 +16,11 @@
|
|
|
16
16
|
* - emits human-readable progress + a final summary
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { join
|
|
19
|
+
import { join } from "node:path";
|
|
20
20
|
import { provisioningUpgradeCommand } from "@caelo-cms/shared";
|
|
21
21
|
import { cancel, confirm, isCancel, log, note, select, spinner } from "@clack/prompts";
|
|
22
22
|
import { bold, cyan, dim, green, red, yellow } from "kleur/colors";
|
|
23
|
-
import { activeAccount, gcloud } from "./gcloud.js";
|
|
23
|
+
import { activeAccount, checkGcloudAuth, gcloud } from "./gcloud.js";
|
|
24
24
|
import { type GcloudRunner, realSleep } from "./gcloud-retry.js";
|
|
25
25
|
import { GCP_STACK_ENV } from "./gcp-names.js";
|
|
26
26
|
import { INFRA_CONTRACT_VERSION } from "./infra-contract.js";
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
recordInfraContractVersion,
|
|
40
40
|
scrubInstallProgress,
|
|
41
41
|
selectInstall,
|
|
42
|
+
stackWorkDir,
|
|
42
43
|
writeMetadata,
|
|
43
44
|
} from "./install-state.js";
|
|
44
45
|
import { runMigrationsViaCloudRunJob } from "./migration-runner.js";
|
|
@@ -73,11 +74,12 @@ import {
|
|
|
73
74
|
type RollSlug,
|
|
74
75
|
} from "./upgrade/app-roll.js";
|
|
75
76
|
import { restoreCommand, takeSqlBackup } from "./upgrade/backup.js";
|
|
76
|
-
import { findServiceUrl, resolveGcpResourceName } from "./upgrade/gcp-lookup.js";
|
|
77
|
+
import { findServiceUrl, lookupFailure, resolveGcpResourceName } from "./upgrade/gcp-lookup.js";
|
|
77
78
|
import { createIapHealthProbe, type HealthProbe } from "./upgrade/health.js";
|
|
78
79
|
import { recordRolledDigests } from "./upgrade/record-rolled.js";
|
|
79
80
|
import {
|
|
80
81
|
canonicalReleaseTag,
|
|
82
|
+
ensureCosignAvailable,
|
|
81
83
|
fetchLatestReleaseTag,
|
|
82
84
|
RELEASE_REGISTRY,
|
|
83
85
|
releaseImageRef,
|
|
@@ -148,14 +150,22 @@ async function chooseInstalls(opts: {
|
|
|
148
150
|
|
|
149
151
|
function exitNoInstall(message: string): never {
|
|
150
152
|
log.error(red(message));
|
|
151
|
-
|
|
152
|
-
log.warn(
|
|
153
|
-
`Run ${bold(selfCommand())} first to provision an install, OR copy ${dim("~/.caelo-<install-id>/")} from the provisioning machine.`,
|
|
154
|
-
);
|
|
155
|
-
}
|
|
153
|
+
log.warn(noInstallHint());
|
|
156
154
|
process.exit(1);
|
|
157
155
|
}
|
|
158
156
|
|
|
157
|
+
/**
|
|
158
|
+
* What to do when this machine has no copy of the install. A Google Cloud
|
|
159
|
+
* install keeps its records in its own project, so the fix is `attach`, not
|
|
160
|
+
* the installer: the installer would provision a new site (#816).
|
|
161
|
+
*/
|
|
162
|
+
export function noInstallHint(): string {
|
|
163
|
+
return [
|
|
164
|
+
`A Google Cloud install keeps its records in its own project: run ${bold(selfCommand("attach", "--project", "<gcp-project-id>"))} once on this machine (or pass ${bold("--project <gcp-project-id>")} to upgrade, which attaches it first).`,
|
|
165
|
+
`A self-hosted install is managed on its own server. To create a new install instead, run ${bold(selfCommand())}.`,
|
|
166
|
+
].join("\n");
|
|
167
|
+
}
|
|
168
|
+
|
|
159
169
|
/** The one install a single-install command (rotate-secret, truncate, destroy) acts on. */
|
|
160
170
|
async function requireInstall(verb: string): Promise<{ installId: string; meta: InstallMetadata }> {
|
|
161
171
|
const [meta] = await chooseInstalls({ verb, allowAll: false });
|
|
@@ -212,6 +222,27 @@ function installRegion(meta: InstallMetadata, requested?: string): string {
|
|
|
212
222
|
}
|
|
213
223
|
}
|
|
214
224
|
|
|
225
|
+
/**
|
|
226
|
+
* Pre-flight for a lifecycle command on a GCP install, before any lock,
|
|
227
|
+
* lookup or change: gcloud can mint a token now (#630: expired credentials
|
|
228
|
+
* otherwise surface as "service not found" half-way through) and, for an
|
|
229
|
+
* upgrade that verifies signatures, cosign is on PATH (#816).
|
|
230
|
+
*
|
|
231
|
+
* @returns false when the command must stop (the reason is printed)
|
|
232
|
+
*/
|
|
233
|
+
async function gcpPreflight(opts: { readonly cosign: boolean }): Promise<boolean> {
|
|
234
|
+
const auth = await checkGcloudAuth();
|
|
235
|
+
if (!auth.ok) {
|
|
236
|
+
log.error(red(`Nothing was changed: ${auth.error}`));
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
if (opts.cosign && !(await ensureCosignAvailable())) {
|
|
240
|
+
log.error(red("Nothing was changed: the release images cannot be verified without cosign."));
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
|
|
215
246
|
// =========================================================================
|
|
216
247
|
// status — health check + monthly cost
|
|
217
248
|
// =========================================================================
|
|
@@ -264,27 +295,34 @@ async function getLatestReleaseTag(): Promise<string | null> {
|
|
|
264
295
|
|
|
265
296
|
async function gcpStatus(meta: InstallMetadata): Promise<void> {
|
|
266
297
|
if (!meta.projectId) return;
|
|
298
|
+
if (!(await gcpPreflight({ cosign: false }))) return;
|
|
267
299
|
const s = spinner();
|
|
268
300
|
s.start("Resolving deployed resources + checking health...");
|
|
269
301
|
|
|
270
302
|
const region = installRegion(meta);
|
|
271
|
-
const
|
|
303
|
+
const admin = await resolveGcpResourceName(
|
|
272
304
|
gcloud,
|
|
273
305
|
"run-service",
|
|
274
306
|
"caelo-production-admin",
|
|
275
307
|
meta.projectId,
|
|
276
308
|
region,
|
|
277
309
|
);
|
|
278
|
-
const
|
|
310
|
+
const sql = await resolveGcpResourceName(
|
|
279
311
|
gcloud,
|
|
280
312
|
"sql-instance",
|
|
281
313
|
"caelo-production-pg",
|
|
282
314
|
meta.projectId,
|
|
283
315
|
);
|
|
284
|
-
if (
|
|
285
|
-
s.stop(red("
|
|
316
|
+
if (admin.status !== "found") {
|
|
317
|
+
s.stop(red(lookupFailure(admin, "caelo-production-admin* Cloud Run service", "operator")));
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (sql.status !== "found") {
|
|
321
|
+
s.stop(red(lookupFailure(sql, "caelo-production-pg* Cloud SQL instance", "operator")));
|
|
286
322
|
return;
|
|
287
323
|
}
|
|
324
|
+
const adminName = admin.name;
|
|
325
|
+
const sqlName = sql.name;
|
|
288
326
|
|
|
289
327
|
const adminUri = await gcloud([
|
|
290
328
|
"run",
|
|
@@ -366,9 +404,43 @@ interface UpgradeOpts {
|
|
|
366
404
|
* `--install` or the picker (chooseInstalls).
|
|
367
405
|
*/
|
|
368
406
|
readonly installId?: string;
|
|
407
|
+
/**
|
|
408
|
+
* #816 — the GCP project the install lives in. The upgrade attaches it
|
|
409
|
+
* from the project's ops bucket first (state-command.ts attachProject),
|
|
410
|
+
* so it works on a machine (or a fresh Cloud Shell) with no local copy.
|
|
411
|
+
*/
|
|
412
|
+
readonly project?: string;
|
|
369
413
|
}
|
|
370
414
|
|
|
371
415
|
export async function upgradeCommand(opts: UpgradeOpts = {}): Promise<void> {
|
|
416
|
+
if (opts.project) {
|
|
417
|
+
if (!(await gcpPreflight({ cosign: false }))) {
|
|
418
|
+
process.exitCode = 1;
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
const { attachProject } = await import("./state-command.js");
|
|
422
|
+
const attached = attachProject(opts.project);
|
|
423
|
+
let wanted: string | undefined;
|
|
424
|
+
try {
|
|
425
|
+
wanted = installFlag(process.argv);
|
|
426
|
+
} catch (e) {
|
|
427
|
+
log.error(red(e instanceof Error ? e.message : String(e)));
|
|
428
|
+
process.exit(2);
|
|
429
|
+
}
|
|
430
|
+
const selected = selectInstall(listInstalls(), wanted ?? attached);
|
|
431
|
+
if (!selected.ok) return exitNoInstall(selected.message);
|
|
432
|
+
if (selected.meta.installId !== attached) {
|
|
433
|
+
log.error(
|
|
434
|
+
red(
|
|
435
|
+
`--install ${wanted} is not the install in project ${opts.project} (${attached}). Drop one of the two flags.`,
|
|
436
|
+
),
|
|
437
|
+
);
|
|
438
|
+
process.exit(2);
|
|
439
|
+
}
|
|
440
|
+
const meta = currentMetadata(selected.meta);
|
|
441
|
+
if (!(await upgradeInstall(meta.installId, meta, opts))) process.exitCode = 1;
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
372
444
|
if (opts.installId) {
|
|
373
445
|
const selected = selectInstall(listInstalls(), opts.installId);
|
|
374
446
|
if (!selected.ok) return exitNoInstall(selected.message);
|
|
@@ -493,6 +565,7 @@ async function upgradeInstall(
|
|
|
493
565
|
if (!meta.projectId) return false;
|
|
494
566
|
const provider = meta.provider;
|
|
495
567
|
const projectId = meta.projectId;
|
|
568
|
+
if (!(await gcpPreflight({ cosign: opts.skipVerify !== true }))) return false;
|
|
496
569
|
|
|
497
570
|
// #815 — the install's state lock, the one the in-app upgrader job,
|
|
498
571
|
// `pulumi up` and `state migrate` take: a Cloud Shell upgrade must not
|
|
@@ -569,6 +642,7 @@ async function upgradeLockedInstall(
|
|
|
569
642
|
infraContractVersion: INFRA_CONTRACT_VERSION,
|
|
570
643
|
skipVerify: opts.skipVerify === true,
|
|
571
644
|
skipHealthCheck: opts.skipHealthCheck === true,
|
|
645
|
+
credentials: "operator",
|
|
572
646
|
},
|
|
573
647
|
{
|
|
574
648
|
run,
|
|
@@ -578,7 +652,7 @@ async function upgradeLockedInstall(
|
|
|
578
652
|
verifyImages: verifyCosignAll,
|
|
579
653
|
migrate: (imageRef) =>
|
|
580
654
|
runMigrationsViaCloudRunJob({ projectId, region, imageOverride: imageRef }, { run }),
|
|
581
|
-
healthProbe: (plan) => gcpHealthProbe(run, http, plan),
|
|
655
|
+
healthProbe: (plan, context) => gcpHealthProbe(run, http, plan, context),
|
|
582
656
|
recordDigests: async (digests) => {
|
|
583
657
|
recorded = await recordRolledRelease(
|
|
584
658
|
installId,
|
|
@@ -621,6 +695,7 @@ async function gcpHealthProbe(
|
|
|
621
695
|
run: GcloudRunner,
|
|
622
696
|
http: HttpFetch,
|
|
623
697
|
plan: AppRollPlan,
|
|
698
|
+
context: { readonly accessJustChanged: boolean },
|
|
624
699
|
): Promise<{ ok: true; probe: HealthProbe } | { ok: false; error: string }> {
|
|
625
700
|
const { install } = plan;
|
|
626
701
|
const urls: Partial<Record<RollSlug, string>> = {};
|
|
@@ -644,7 +719,7 @@ async function gcpHealthProbe(
|
|
|
644
719
|
adminServiceUrl: urls.admin as string,
|
|
645
720
|
gatewayServiceUrl: urls.gateway as string,
|
|
646
721
|
},
|
|
647
|
-
{ run, http, sleep: realSleep },
|
|
722
|
+
{ run, http, sleep: realSleep, accessJustChanged: context.accessJustChanged },
|
|
648
723
|
),
|
|
649
724
|
};
|
|
650
725
|
}
|
|
@@ -676,7 +751,12 @@ export function recordRolledRelease(
|
|
|
676
751
|
readPassphrase: () => readSecret(installId, "pulumi-passphrase"),
|
|
677
752
|
});
|
|
678
753
|
const { writeImageDigestsToStack } = await import("./wizards/gcp-pulumi.js");
|
|
679
|
-
return writeImageDigestsToStack({
|
|
754
|
+
return writeImageDigestsToStack({
|
|
755
|
+
installRoot: installRoot(installId),
|
|
756
|
+
backend,
|
|
757
|
+
provider,
|
|
758
|
+
digests: d,
|
|
759
|
+
});
|
|
680
760
|
},
|
|
681
761
|
stackRecovery: (d) => stackConfigRecovery(installId, local ?? {}, provider, d),
|
|
682
762
|
installJsonLocation: bucket
|
|
@@ -702,7 +782,7 @@ function stackConfigRecovery(
|
|
|
702
782
|
digests: ImageDigests,
|
|
703
783
|
): string {
|
|
704
784
|
const env = backendShellEnv(meta, installRoot(installId));
|
|
705
|
-
const cwd = `--cwd ${
|
|
785
|
+
const cwd = `--cwd ${stackWorkDir(installRoot(installId), provider)} --stack ${GCP_STACK_ENV}`;
|
|
706
786
|
const lines = Object.entries(digests).map(
|
|
707
787
|
([svc, d]) => ` ${env} pulumi config set ${cwd} caelo-${provider}:image-digest-${svc} ${d}`,
|
|
708
788
|
);
|
|
@@ -730,12 +810,13 @@ async function backupOf(meta: InstallMetadata): Promise<boolean> {
|
|
|
730
810
|
return false;
|
|
731
811
|
}
|
|
732
812
|
if (!meta.projectId) return false;
|
|
813
|
+
if (!(await gcpPreflight({ cosign: false }))) return false;
|
|
733
814
|
|
|
734
815
|
const s = spinner();
|
|
735
816
|
s.start("Resolving Cloud SQL instance + triggering on-demand backup...");
|
|
736
817
|
const r = await takeSqlBackup(
|
|
737
818
|
gcloud,
|
|
738
|
-
{ projectId: meta.projectId, env: GCP_STACK_ENV },
|
|
819
|
+
{ projectId: meta.projectId, env: GCP_STACK_ENV, credentials: "operator" },
|
|
739
820
|
"backup",
|
|
740
821
|
);
|
|
741
822
|
if (!r.ok) {
|
|
@@ -776,21 +857,28 @@ export async function rotateSecretCommand(
|
|
|
776
857
|
}
|
|
777
858
|
if (!meta.projectId) return;
|
|
778
859
|
const region = installRegion(meta);
|
|
860
|
+
if (!(await gcpPreflight({ cosign: false }))) {
|
|
861
|
+
process.exitCode = 1;
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
779
864
|
|
|
780
865
|
if (name === SEAL_KEY_SECRET) {
|
|
781
|
-
const
|
|
866
|
+
const gateway = await resolveGcpResourceName(
|
|
782
867
|
gcloud,
|
|
783
868
|
"run-service",
|
|
784
869
|
"caelo-production-gateway",
|
|
785
870
|
meta.projectId,
|
|
786
871
|
region,
|
|
787
872
|
);
|
|
788
|
-
if (
|
|
873
|
+
if (gateway.status !== "found") {
|
|
789
874
|
log.error(
|
|
790
|
-
red(
|
|
875
|
+
red(
|
|
876
|
+
`${lookupFailure(gateway, "caelo-production-gateway* Cloud Run service", "operator")} — nothing rotated`,
|
|
877
|
+
),
|
|
791
878
|
);
|
|
792
879
|
return;
|
|
793
880
|
}
|
|
881
|
+
const gatewayService = gateway.name;
|
|
794
882
|
const target = { projectId: meta.projectId, region, env: GCP_STACK_ENV, gatewayService };
|
|
795
883
|
const report = opts.finish
|
|
796
884
|
? await finishSealKeyRotation(target, {}, { now: opts.now === true })
|
|
@@ -807,23 +895,41 @@ export async function rotateSecretCommand(
|
|
|
807
895
|
s.start(`Rotating ${secret}...`);
|
|
808
896
|
const services: Partial<Record<"admin" | "gateway", string>> = {};
|
|
809
897
|
for (const slug of ["admin", "gateway"] as const) {
|
|
810
|
-
const
|
|
898
|
+
const service = await resolveGcpResourceName(
|
|
811
899
|
gcloud,
|
|
812
900
|
"run-service",
|
|
813
901
|
`caelo-production-${slug}`,
|
|
814
902
|
meta.projectId,
|
|
815
903
|
region,
|
|
816
904
|
);
|
|
817
|
-
if (
|
|
818
|
-
s.stop(
|
|
905
|
+
if (service.status !== "found") {
|
|
906
|
+
s.stop(
|
|
907
|
+
red(
|
|
908
|
+
`${lookupFailure(service, `caelo-production-${slug}* Cloud Run service`, "operator")} — nothing rotated`,
|
|
909
|
+
),
|
|
910
|
+
);
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
services[slug] = service.name;
|
|
914
|
+
}
|
|
915
|
+
let sqlInstance: string | null = null;
|
|
916
|
+
if (rolesOfSecret(secret).length > 0) {
|
|
917
|
+
const sql = await resolveGcpResourceName(
|
|
918
|
+
gcloud,
|
|
919
|
+
"sql-instance",
|
|
920
|
+
"caelo-production-pg",
|
|
921
|
+
meta.projectId,
|
|
922
|
+
);
|
|
923
|
+
if (sql.status !== "found") {
|
|
924
|
+
s.stop(
|
|
925
|
+
red(
|
|
926
|
+
`${lookupFailure(sql, "caelo-production-pg* Cloud SQL instance", "operator")} — nothing rotated`,
|
|
927
|
+
),
|
|
928
|
+
);
|
|
819
929
|
return;
|
|
820
930
|
}
|
|
821
|
-
|
|
931
|
+
sqlInstance = sql.name;
|
|
822
932
|
}
|
|
823
|
-
const sqlInstance =
|
|
824
|
-
rolesOfSecret(secret).length > 0
|
|
825
|
-
? await resolveGcpResourceName(gcloud, "sql-instance", "caelo-production-pg", meta.projectId)
|
|
826
|
-
: null;
|
|
827
933
|
const report = await rotateRuntimeSecret(
|
|
828
934
|
{
|
|
829
935
|
projectId: meta.projectId,
|