@caelo-cms/provisioning 0.11.0 → 0.11.1
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 +252 -64
- package/dist/cdn-copy.d.ts +3 -4
- package/dist/cdn-copy.d.ts.map +1 -1
- package/dist/cdn-copy.js.map +1 -1
- package/dist/stack-contract.d.ts +13 -0
- package/dist/stack-contract.d.ts.map +1 -1
- package/dist/stack-contract.js +18 -0
- package/dist/stack-contract.js.map +1 -1
- package/dist/stack-converge.d.ts +13 -0
- package/dist/stack-converge.d.ts.map +1 -1
- package/dist/stack-converge.js +26 -0
- package/dist/stack-converge.js.map +1 -1
- package/dist/upgrade/app-roll.d.ts +4 -1
- package/dist/upgrade/app-roll.d.ts.map +1 -1
- package/dist/upgrade/app-roll.js +7 -3
- package/dist/upgrade/app-roll.js.map +1 -1
- package/dist/wizards/gcp-cost.d.ts.map +1 -1
- package/dist/wizards/gcp-cost.js +5 -2
- package/dist/wizards/gcp-cost.js.map +1 -1
- package/package.json +2 -2
- package/src/cdn-copy.ts +3 -4
- package/src/cpu-idle.test.ts +64 -0
- package/src/stack-contract.ts +19 -0
- package/src/stack-converge.ts +31 -0
- package/src/upgrade/app-roll.ts +12 -2
- package/src/upgrade/upgrade.test.ts +17 -1
- package/src/wizards/gcp-cost.ts +5 -2
- package/stacks/gcp/index.ts +6 -1
- package/stacks/gcp-firebase/index.ts +6 -1
package/src/cdn-copy.ts
CHANGED
|
@@ -11,13 +11,12 @@
|
|
|
11
11
|
* respects the long TTL.
|
|
12
12
|
* - Azure → Front Door purge + warm endpoint.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* registered for the active provider.
|
|
14
|
+
* A worker that drains the per-asset CDN-pin queue (none ships yet) uses
|
|
15
|
+
* the adapter registered for the active provider.
|
|
17
16
|
*
|
|
18
17
|
* Each provider's stack publishes a `CdnCopyAdapter` instance via its
|
|
19
18
|
* Pulumi outputs (the actual implementation is dynamically imported by
|
|
20
|
-
*
|
|
19
|
+
* that worker at runtime so `@caelo-cms/provisioning` doesn't pull in
|
|
21
20
|
* `@aws-sdk/...`/`@google-cloud/...`/`@azure/...` as static deps). The
|
|
22
21
|
* interface defined here is the only thing both sides agree on.
|
|
23
22
|
*/
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Cloud Run CPU allocation. A missing `cpuIdle` in the v2 API means
|
|
5
|
+
* always-on CPU once `resources` is set, so both stacks ran the admin and
|
|
6
|
+
* the gateway billed around the clock. The stacks now set it from
|
|
7
|
+
* SERVICE_CPU_IDLE, and an upgrade moves existing installs there.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, expect, it } from "bun:test";
|
|
11
|
+
import { readFileSync } from "node:fs";
|
|
12
|
+
import { resolve } from "node:path";
|
|
13
|
+
import { SERVICE_CPU_IDLE } from "./stack-contract.js";
|
|
14
|
+
import { liveCpuIdle, planCpuIdle } from "./stack-converge.js";
|
|
15
|
+
|
|
16
|
+
const described = (annotations?: Record<string, string>) =>
|
|
17
|
+
JSON.stringify({ spec: { template: { metadata: annotations ? { annotations } : {} } } });
|
|
18
|
+
|
|
19
|
+
describe("liveCpuIdle", () => {
|
|
20
|
+
it("reads the revision's cpu-throttling annotation", () => {
|
|
21
|
+
expect(liveCpuIdle(described({ "run.googleapis.com/cpu-throttling": "false" }))).toBe(false);
|
|
22
|
+
expect(liveCpuIdle(described({ "run.googleapis.com/cpu-throttling": "true" }))).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("treats a missing annotation as Cloud Run's default, request-based CPU", () => {
|
|
26
|
+
expect(liveCpuIdle(described())).toBe(true);
|
|
27
|
+
expect(liveCpuIdle(described({ "autoscaling.knative.dev/maxScale": "5" }))).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("planCpuIdle", () => {
|
|
32
|
+
it("moves an always-on service to request-based CPU", () => {
|
|
33
|
+
expect(planCpuIdle(false, true)).toEqual({
|
|
34
|
+
ok: true,
|
|
35
|
+
flags: ["--cpu-throttling"],
|
|
36
|
+
changes: ["CPU: always allocated → only during requests"],
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("changes nothing when the service already matches", () => {
|
|
41
|
+
expect(planCpuIdle(true, true)).toEqual({ ok: true, flags: [], changes: [] });
|
|
42
|
+
expect(planCpuIdle(false, false)).toEqual({ ok: true, flags: [], changes: [] });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("never switches a service to always-on CPU", () => {
|
|
46
|
+
expect(planCpuIdle(true, false)).toEqual({ ok: true, flags: [], changes: [] });
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("the stacks set cpuIdle", () => {
|
|
51
|
+
it("the gateway runs request-billed", () => {
|
|
52
|
+
expect(SERVICE_CPU_IDLE.gateway).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
for (const provider of ["gcp", "gcp-firebase"]) {
|
|
56
|
+
it(`${provider} takes cpuIdle from the contract`, () => {
|
|
57
|
+
const stack = readFileSync(
|
|
58
|
+
resolve(import.meta.dir, `../stacks/${provider}/index.ts`),
|
|
59
|
+
"utf8",
|
|
60
|
+
);
|
|
61
|
+
expect(stack).toContain("cpuIdle: SERVICE_CPU_IDLE[args.serviceName]");
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
});
|
package/src/stack-contract.ts
CHANGED
|
@@ -620,6 +620,25 @@ export function gatewayEnvContract<V>(inputs: RuntimeEnvInputs<V>): CloudRunEnvV
|
|
|
620
620
|
*/
|
|
621
621
|
export const GATEWAY_TIMEOUT_SECONDS = 3600;
|
|
622
622
|
|
|
623
|
+
/**
|
|
624
|
+
* Whether each Cloud Run service gets CPU only while it handles a request
|
|
625
|
+
* (Cloud Run's request-based billing, `cpuIdle: true`) or for its whole
|
|
626
|
+
* lifetime (`false`, instance-based billing). Set explicitly: the v2 API
|
|
627
|
+
* reads a missing `cpuIdle` as `false` as soon as a service declares
|
|
628
|
+
* `resources`, which is how both services ran with always-on CPU without
|
|
629
|
+
* anyone choosing it — billed for every idle minute an instance stayed up.
|
|
630
|
+
*
|
|
631
|
+
* The gateway only does work inside requests. The admin still runs its
|
|
632
|
+
* background work (Tier 5: audit worker, importer, plugin cron, GC) on
|
|
633
|
+
* in-process timers that need CPU between requests.
|
|
634
|
+
*/
|
|
635
|
+
export const SERVICE_CPU_IDLE: Readonly<Record<CloudRunSlug, boolean>> = {
|
|
636
|
+
// FIXME(issue #1140): true once the admin's background work no longer
|
|
637
|
+
// depends on CPU between requests (jobs, scheduler, plugin host).
|
|
638
|
+
admin: false,
|
|
639
|
+
gateway: true,
|
|
640
|
+
};
|
|
641
|
+
|
|
623
642
|
/** Admin env var naming the Secret Manager secret the Owner writes the Google sign-in client to. */
|
|
624
643
|
export const OPERATOR_OAUTH_CLIENT_STORE_ENV = "CAELO_OPERATOR_OAUTH_CLIENT_STORE";
|
|
625
644
|
|
package/src/stack-converge.ts
CHANGED
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
* - {@link planSecretFiles} / {@link planRequestTimeout} (#806): the
|
|
26
26
|
* gateway's secret file (the remote-MCP OAuth client) and its request
|
|
27
27
|
* timeout, in the same `services update`.
|
|
28
|
+
* - {@link planCpuIdle}: request-based CPU where the contract wants it
|
|
29
|
+
* (`SERVICE_CPU_IDLE`), in the same `services update`.
|
|
28
30
|
*/
|
|
29
31
|
|
|
30
32
|
import { removeDefaultFirewallRules } from "./default-firewall.js";
|
|
@@ -1152,6 +1154,35 @@ export function planRequestTimeout(live: number | null, desired: number): RollAd
|
|
|
1152
1154
|
};
|
|
1153
1155
|
}
|
|
1154
1156
|
|
|
1157
|
+
/**
|
|
1158
|
+
* Whether a described service gets CPU only during requests. Cloud Run keeps
|
|
1159
|
+
* the setting in the `run.googleapis.com/cpu-throttling` revision
|
|
1160
|
+
* annotation; a missing annotation is Cloud Run's default, throttled.
|
|
1161
|
+
*/
|
|
1162
|
+
export function liveCpuIdle(serviceJson: string): boolean {
|
|
1163
|
+
const svc = JSON.parse(serviceJson) as {
|
|
1164
|
+
spec?: { template?: { metadata?: { annotations?: Record<string, string> } } };
|
|
1165
|
+
};
|
|
1166
|
+
return (
|
|
1167
|
+
svc.spec?.template?.metadata?.annotations?.["run.googleapis.com/cpu-throttling"] !== "false"
|
|
1168
|
+
);
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
/**
|
|
1172
|
+
* `--cpu-throttling` when the contract wants request-based CPU and the
|
|
1173
|
+
* service still runs always-on. Never the other way: always-on CPU is
|
|
1174
|
+
* billed around the clock, so a service the operator already moved to
|
|
1175
|
+
* request-based CPU keeps it.
|
|
1176
|
+
*/
|
|
1177
|
+
export function planCpuIdle(live: boolean, desired: boolean): RollAdditionsPlan {
|
|
1178
|
+
if (!desired || live) return { ok: true, flags: [], changes: [] };
|
|
1179
|
+
return {
|
|
1180
|
+
ok: true,
|
|
1181
|
+
flags: ["--cpu-throttling"],
|
|
1182
|
+
changes: ["CPU: always allocated → only during requests"],
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1155
1186
|
/**
|
|
1156
1187
|
* The single `gcloud run services update` that rolls a service to a new
|
|
1157
1188
|
* image, its run SA and its env changes, so all land in one new revision.
|
package/src/upgrade/app-roll.ts
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
type CloudRunEnvVar,
|
|
33
33
|
GATEWAY_TIMEOUT_SECONDS,
|
|
34
34
|
RETIRED_SECRET_FILES,
|
|
35
|
+
SERVICE_CPU_IDLE,
|
|
35
36
|
SERVICE_SECRET_FILES,
|
|
36
37
|
} from "../stack-contract.js";
|
|
37
38
|
import {
|
|
@@ -42,10 +43,12 @@ import {
|
|
|
42
43
|
type LiveVolumes,
|
|
43
44
|
liveContainerEnv,
|
|
44
45
|
liveContainerMemory,
|
|
46
|
+
liveCpuIdle,
|
|
45
47
|
liveTimeoutSeconds,
|
|
46
48
|
liveVolumes,
|
|
47
49
|
planAdminMemory,
|
|
48
50
|
planContractEnv,
|
|
51
|
+
planCpuIdle,
|
|
49
52
|
planMediaVolume,
|
|
50
53
|
planRequestTimeout,
|
|
51
54
|
planSecretFiles,
|
|
@@ -105,6 +108,8 @@ export interface ServicePlan {
|
|
|
105
108
|
readonly liveVolumes: LiveVolumes;
|
|
106
109
|
/** Request timeout now, in seconds (null = Cloud Run's default). */
|
|
107
110
|
readonly liveTimeout: number | null;
|
|
111
|
+
/** Whether the service gets CPU only during requests now. */
|
|
112
|
+
readonly liveCpuIdle: boolean;
|
|
108
113
|
}
|
|
109
114
|
|
|
110
115
|
/** A service plan plus the changes its roll applies. */
|
|
@@ -113,7 +118,8 @@ export interface RollPlan extends ServicePlan {
|
|
|
113
118
|
readonly envChanges: readonly EnvChange[];
|
|
114
119
|
/**
|
|
115
120
|
* #553 — e.g. `--memory=2Gi` when the admin runs below the stack default;
|
|
116
|
-
* #806 — `--timeout=3600` when the gateway runs below its contract timeout
|
|
121
|
+
* #806 — `--timeout=3600` when the gateway runs below its contract timeout;
|
|
122
|
+
* `--cpu-throttling` when the contract wants request-based CPU.
|
|
117
123
|
*/
|
|
118
124
|
readonly resourceFlags: readonly string[];
|
|
119
125
|
readonly resourceChanges: readonly string[];
|
|
@@ -203,6 +209,7 @@ export async function planAppRoll(
|
|
|
203
209
|
liveMemory: liveContainerMemory(json),
|
|
204
210
|
liveVolumes: liveVolumes(json),
|
|
205
211
|
liveTimeout: liveTimeoutSeconds(json),
|
|
212
|
+
liveCpuIdle: liveCpuIdle(json),
|
|
206
213
|
});
|
|
207
214
|
}
|
|
208
215
|
const env = planEnv(install, services, {});
|
|
@@ -264,14 +271,17 @@ export async function planAppRoll(
|
|
|
264
271
|
s.slug === "gateway" ? planRequestTimeout(s.liveTimeout, GATEWAY_TIMEOUT_SECONDS) : null;
|
|
265
272
|
const memoryFlags = memory?.ok ? memory.flags : [];
|
|
266
273
|
const timeoutFlags = timeout?.ok ? timeout.flags : [];
|
|
274
|
+
const cpu = planCpuIdle(s.liveCpuIdle, SERVICE_CPU_IDLE[s.slug]);
|
|
275
|
+
const cpuFlags = cpu.ok ? cpu.flags : [];
|
|
267
276
|
return {
|
|
268
277
|
...s,
|
|
269
278
|
envFlags: env.services[s.slug].flags,
|
|
270
279
|
envChanges: env.services[s.slug].changes,
|
|
271
|
-
resourceFlags: [...memoryFlags, ...timeoutFlags],
|
|
280
|
+
resourceFlags: [...memoryFlags, ...timeoutFlags, ...cpuFlags],
|
|
272
281
|
resourceChanges: [
|
|
273
282
|
...memoryFlags.map((f) => `${s.liveMemory ?? "(default)"} → ${f}`),
|
|
274
283
|
...(timeout?.ok ? timeout.changes : []),
|
|
284
|
+
...(cpu.ok ? cpu.changes : []),
|
|
275
285
|
],
|
|
276
286
|
volumeFlags: [...(s.slug === "admin" ? media.flags : []), ...secretFiles[s.slug].flags],
|
|
277
287
|
volumeChanges: [...(s.slug === "admin" ? media.changes : []), ...secretFiles[s.slug].changes],
|
|
@@ -50,7 +50,8 @@ const secretRef = (name: string, secret: string) => ({
|
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
52
|
* A described service, as `gcloud run services describe --format=json`
|
|
53
|
-
* returns it. `ingress`: the service-level setting (#732).
|
|
53
|
+
* returns it. `ingress`: the service-level setting (#732). Always-on CPU,
|
|
54
|
+
* as every install the stacks created before `cpuIdle` was set runs.
|
|
54
55
|
*/
|
|
55
56
|
const serviceJson = (image: string, env: unknown[], ingress = "all") =>
|
|
56
57
|
JSON.stringify({
|
|
@@ -59,6 +60,7 @@ const serviceJson = (image: string, env: unknown[], ingress = "all") =>
|
|
|
59
60
|
template: {
|
|
60
61
|
metadata: {
|
|
61
62
|
annotations: {
|
|
63
|
+
"run.googleapis.com/cpu-throttling": "false",
|
|
62
64
|
"run.googleapis.com/network-interfaces": JSON.stringify([
|
|
63
65
|
{ network: "caelo-production-vpc", subnetwork: "caelo-production-subnet" },
|
|
64
66
|
]),
|
|
@@ -698,6 +700,20 @@ describe("upgradeGcpInstall — converge, then app-roll", () => {
|
|
|
698
700
|
expect(calls.filter((c) => /^run services update /.test(c))).toHaveLength(2);
|
|
699
701
|
});
|
|
700
702
|
|
|
703
|
+
it("moves the gateway to request-based CPU in its image roll; the admin keeps its CPU", async () => {
|
|
704
|
+
const events: string[] = [];
|
|
705
|
+
const { deps, calls } = upgradeDeps(events);
|
|
706
|
+
expect(await upgradeGcpInstall(TARGET, deps)).toBe(true);
|
|
707
|
+
const rolls = calls.filter((c) =>
|
|
708
|
+
/^run services update \S+ --region europe-west1 .*--image/.test(c),
|
|
709
|
+
);
|
|
710
|
+
const admin = rolls.find((c) => c.startsWith(`run services update ${ADMIN} `)) ?? "";
|
|
711
|
+
const gateway = rolls.find((c) => c.startsWith(`run services update ${GATEWAY} `)) ?? "";
|
|
712
|
+
expect(gateway).toContain("--cpu-throttling");
|
|
713
|
+
expect(admin).not.toContain("cpu-throttling");
|
|
714
|
+
expect(calls.filter((c) => /^run services update /.test(c))).toHaveLength(2);
|
|
715
|
+
});
|
|
716
|
+
|
|
701
717
|
it("#806 — on gcp, routes the discovery paths and adds the Cloud Armor rule before the roll", async () => {
|
|
702
718
|
const events: string[] = [];
|
|
703
719
|
const urlMap = {
|
package/src/wizards/gcp-cost.ts
CHANGED
|
@@ -91,13 +91,16 @@ export function estimateGcpCost(inputs: CostEstimateInputs): {
|
|
|
91
91
|
},
|
|
92
92
|
{
|
|
93
93
|
name: `Cloud Run admin (${adminMemory})`,
|
|
94
|
+
// FIXME(issue #1140): ~$1 once the admin runs request-billed CPU
|
|
95
|
+
// (SERVICE_CPU_IDLE). Until then each editing session is billed for
|
|
96
|
+
// as long as the instance stays up, ~15 minutes past its last request.
|
|
94
97
|
monthlyUsd:
|
|
95
98
|
inputs.adminMinInstances === 0
|
|
96
|
-
?
|
|
99
|
+
? 7
|
|
97
100
|
: Math.round(1 + inputs.adminMinInstances * perMinInstanceUsd),
|
|
98
101
|
notes:
|
|
99
102
|
inputs.adminMinInstances === 0
|
|
100
|
-
? "scale-to-zero; ~$
|
|
103
|
+
? "scale-to-zero, CPU always allocated while running; ~$7/mo light editorial use"
|
|
101
104
|
: `${inputs.adminMinInstances} min-instance${inputs.adminMinInstances > 1 ? "s" : ""} (no cold start)`,
|
|
102
105
|
},
|
|
103
106
|
{
|
package/stacks/gcp/index.ts
CHANGED
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
type RuntimeSecret,
|
|
49
49
|
remoteMcpIdentityStackEnv,
|
|
50
50
|
runtimeSecretBindings,
|
|
51
|
+
SERVICE_CPU_IDLE,
|
|
51
52
|
STATIC_CDN_POLICY,
|
|
52
53
|
secretFileVolumesTemplate,
|
|
53
54
|
} from "../../dist/stack-contract.js";
|
|
@@ -738,7 +739,11 @@ function cloudRunService(args: CloudRunArgs): gcp.cloudrunv2.Service {
|
|
|
738
739
|
...(args.mediaVolume?.volumeMounts ?? []),
|
|
739
740
|
...secretFileVolumesTemplate(args.serviceName, env).volumeMounts,
|
|
740
741
|
],
|
|
741
|
-
|
|
742
|
+
// Explicit: a missing cpuIdle means always-on CPU (stack-contract.ts).
|
|
743
|
+
resources: {
|
|
744
|
+
limits: { cpu: "1", memory: args.memory },
|
|
745
|
+
cpuIdle: SERVICE_CPU_IDLE[args.serviceName],
|
|
746
|
+
},
|
|
742
747
|
},
|
|
743
748
|
],
|
|
744
749
|
vpcAccess: {
|
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
type RuntimeSecret,
|
|
54
54
|
remoteMcpIdentityStackEnv,
|
|
55
55
|
runtimeSecretBindings,
|
|
56
|
+
SERVICE_CPU_IDLE,
|
|
56
57
|
secretFileVolumesTemplate,
|
|
57
58
|
} from "../../dist/stack-contract.js";
|
|
58
59
|
|
|
@@ -598,7 +599,11 @@ function cloudRunService(args: CloudRunArgs): gcp.cloudrunv2.Service {
|
|
|
598
599
|
...(args.mediaVolume?.volumeMounts ?? []),
|
|
599
600
|
...secretFileVolumesTemplate(args.serviceName, env).volumeMounts,
|
|
600
601
|
],
|
|
601
|
-
|
|
602
|
+
// Explicit: a missing cpuIdle means always-on CPU (stack-contract.ts).
|
|
603
|
+
resources: {
|
|
604
|
+
limits: { cpu: "1", memory: args.memory },
|
|
605
|
+
cpuIdle: SERVICE_CPU_IDLE[args.serviceName],
|
|
606
|
+
},
|
|
602
607
|
},
|
|
603
608
|
],
|
|
604
609
|
vpcAccess: {
|