@sanoma-ai/connector-cloudflare 0.1.5 → 0.2.0
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/README.md +35 -27
- package/dist/connector.d.ts +6 -2
- package/dist/connector.js +10 -3
- package/dist/connector.js.map +1 -1
- package/dist/driver.js +48 -35
- package/dist/driver.js.map +1 -1
- package/dist/fake.d.ts +20 -6
- package/dist/fake.js +84 -26
- package/dist/fake.js.map +1 -1
- package/dist/ops.d.ts +68 -24
- package/dist/ops.js +183 -59
- package/dist/ops.js.map +1 -1
- package/dist/registry.d.ts +15 -5
- package/dist/registry.js +44 -12
- package/dist/registry.js.map +1 -1
- package/dist/resources.d.ts +1 -0
- package/dist/steps.js +2 -2
- package/dist/steps.js.map +1 -1
- package/dist/worker.d.ts +1 -1
- package/dist/worker.js +1 -1
- package/dist/worker.js.map +1 -1
- package/features/cloudflare.feature +4 -4
- package/package.json +3 -3
- package/worker/instance.d.ts +14 -2
- package/worker/instance.js +68 -13
package/dist/registry.js
CHANGED
|
@@ -2,10 +2,11 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { DriverError, errorMessage, retryableStatus } from "@sanoma-ai/workflows";
|
|
3
3
|
/*
|
|
4
4
|
* The runtime image copied into the account's registry, registry to registry over the OCI
|
|
5
|
-
* distribution API with `fetch`, no Docker: a container application
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* distribution API with `fetch`, no Docker: a container application of the `durable_object`
|
|
6
|
+
* scheduling policy runs only an image of its own account's registry, pinned by digest, which
|
|
7
|
+
* Cloudflare pulls without a limit and caches. The source is pulled anonymously
|
|
8
|
+
* (`ghcr.io/sanoma-ai/runtime`, public), the target pushed to with the temporary credentials the
|
|
9
|
+
* driver gets from Cloudflare.
|
|
9
10
|
*/
|
|
10
11
|
/** Cloudflare's registry. An image already in the account's runs as declared; any other is copied in. */
|
|
11
12
|
export const CLOUDFLARE_REGISTRY = "registry.cloudflare.com";
|
|
@@ -44,20 +45,38 @@ export function accountImage(image, account, registry = CLOUDFLARE_REGISTRY) {
|
|
|
44
45
|
const { name, sep, reference } = parse(image);
|
|
45
46
|
return `${registry}/${account}/${name.replaceAll("/", "-")}${sep}${reference}`;
|
|
46
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* True when `image` is in the account's registry and pinned by digest: what the application runs
|
|
50
|
+
* as it is, with nothing to copy or ask.
|
|
51
|
+
*/
|
|
52
|
+
export function accountPinned(image, account, registry = CLOUDFLARE_REGISTRY) {
|
|
53
|
+
return image.startsWith(`${registry}/${account}/`) && parse(image).sep === "@";
|
|
54
|
+
}
|
|
55
|
+
/** `image`'s repository at `digest`: `<registry>/<path>@sha256:…`, the one reference the application takes. */
|
|
56
|
+
export function pinImage(image, digest) {
|
|
57
|
+
const { host, name } = parse(image);
|
|
58
|
+
return `${host}/${name}@${digest}`;
|
|
59
|
+
}
|
|
47
60
|
/** A registry on this machine is spoken to over HTTP, as Docker does; any other over HTTPS. */
|
|
48
61
|
const origin = (host) => `${/^(localhost|127\.0\.0\.1)(:\d+)?$/.test(host) ? "http" : "https"}://${host}`;
|
|
49
62
|
/**
|
|
50
63
|
* Copies `image` into the account's registry as `accountImage(image)`, unless the target's
|
|
51
|
-
* manifest by that tag has the source's digest already
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
64
|
+
* manifest by that tag has the source's digest already, and returns the copy pinned by that
|
|
65
|
+
* digest (`pinImage`). Each blob the target lacks is streamed from the source; the manifest goes
|
|
66
|
+
* last, its bytes and media type unchanged, so its digest is the source's. An image in the
|
|
67
|
+
* account's registry already is not copied: pinned by digest it is returned as it is, by a tag
|
|
68
|
+
* its manifest's digest is asked for. Each request may take `timeoutMs` (default ten minutes: a
|
|
69
|
+
* layer streams through). A failure is a `DriverError`, retryable as its status is; the
|
|
70
|
+
* credentials are in no message.
|
|
55
71
|
*/
|
|
56
72
|
export async function copyImage(image, target, timeoutMs = 10 * 60_000) {
|
|
57
73
|
const source = parse(image);
|
|
58
|
-
const
|
|
74
|
+
const copy = accountImage(image, target.account, target.registry);
|
|
75
|
+
const to = parse(copy);
|
|
59
76
|
const ref = source.reference;
|
|
60
77
|
const refuse = (why) => new DriverError(`cloudflare: copying ${image}: ${why}`, { retryable: false });
|
|
78
|
+
if (accountPinned(image, target.account, target.registry))
|
|
79
|
+
return image;
|
|
61
80
|
// Docker Hub is not copied from: its API is on another host, and its pulls are limited.
|
|
62
81
|
if (!/[.:]/.test(source.host) || source.host === "docker.io") {
|
|
63
82
|
throw refuse("name the registry it is in, not Docker Hub: ghcr.io/sanoma-ai/runtime:<version>");
|
|
@@ -115,6 +134,20 @@ export async function copyImage(image, target, timeoutMs = 10 * 60_000) {
|
|
|
115
134
|
throw refuse(`${url.origin}${url.pathname} gave no token`);
|
|
116
135
|
return { authorization: `Bearer ${token ?? access_token}` };
|
|
117
136
|
};
|
|
137
|
+
const basic = `Basic ${Buffer.from(`${target.username}:${target.password}`).toString("base64")}`;
|
|
138
|
+
const into = `${origin(to.host)}/v2/${to.name}`;
|
|
139
|
+
if (copy === image) {
|
|
140
|
+
// In the account by a tag (pushed by hand): only its digest is asked for.
|
|
141
|
+
const auth = await authorize(to.host, `repository:${to.name}:pull`, basic);
|
|
142
|
+
const there = await ask(`${into}/manifests/${ref}`, {
|
|
143
|
+
method: "HEAD",
|
|
144
|
+
headers: { ...auth, accept: MANIFESTS.join(", ") },
|
|
145
|
+
});
|
|
146
|
+
const digest = there.headers.get("docker-content-digest");
|
|
147
|
+
if (!digest?.startsWith("sha256:"))
|
|
148
|
+
throw refuse("the account's registry named no digest for it");
|
|
149
|
+
return pinImage(image, digest);
|
|
150
|
+
}
|
|
118
151
|
const pull = await authorize(source.host, `repository:${source.name}:pull`);
|
|
119
152
|
const from = `${origin(source.host)}/v2/${source.name}`;
|
|
120
153
|
const got = await send(`${from}/manifests/${ref}`, { headers: { ...pull, accept: MANIFESTS.join(", ") } });
|
|
@@ -125,12 +158,10 @@ export async function copyImage(image, target, timeoutMs = 10 * 60_000) {
|
|
|
125
158
|
const digest = `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
|
|
126
159
|
if (source.sep === "@" && digest !== ref)
|
|
127
160
|
throw refuse(`its registry answered a manifest of digest ${digest}`);
|
|
128
|
-
const basic = `Basic ${Buffer.from(`${target.username}:${target.password}`).toString("base64")}`;
|
|
129
161
|
const push = await authorize(to.host, `repository:${to.name}:pull,push`, basic);
|
|
130
|
-
const into = `${origin(to.host)}/v2/${to.name}`;
|
|
131
162
|
const there = await ask(`${into}/manifests/${ref}`, { method: "HEAD", headers: { ...push, accept: mediaType } }, [404]);
|
|
132
163
|
if (there.ok && there.headers.get("docker-content-digest") === digest)
|
|
133
|
-
return;
|
|
164
|
+
return pinImage(copy, digest);
|
|
134
165
|
const manifest = JSON.parse(new TextDecoder().decode(bytes));
|
|
135
166
|
for (const { digest: blob, size } of [manifest.config, ...manifest.layers]) {
|
|
136
167
|
const head = await ask(`${into}/blobs/${blob}`, { method: "HEAD", headers: push }, [404]);
|
|
@@ -156,5 +187,6 @@ export async function copyImage(image, target, timeoutMs = 10 * 60_000) {
|
|
|
156
187
|
headers: { ...push, "content-type": mediaType },
|
|
157
188
|
body: bytes,
|
|
158
189
|
});
|
|
190
|
+
return pinImage(copy, digest);
|
|
159
191
|
}
|
|
160
192
|
//# sourceMappingURL=registry.js.map
|
package/dist/registry.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAElF
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"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"}
|
package/dist/resources.d.ts
CHANGED
|
@@ -27,6 +27,7 @@ export declare const cloudflare: {
|
|
|
27
27
|
at: import("zod").ZodOptional<import("zod").ZodNullable<import("zod").ZodString>>;
|
|
28
28
|
rang: import("zod").ZodOptional<import("zod").ZodNullable<import("zod").ZodString>>;
|
|
29
29
|
healthy: import("zod").ZodOptional<import("zod").ZodNullable<import("zod").ZodBoolean>>;
|
|
30
|
+
token: import("zod").ZodOptional<import("zod").ZodNullable<import("zod").ZodString>>;
|
|
30
31
|
}, import("zod/v4/core").$strip>, "cloudflare.alarm", {
|
|
31
32
|
readonly host: {
|
|
32
33
|
readonly type: readonly ["cloudflare.container"];
|
package/dist/steps.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { defineSteps } from "@sanoma-ai/workflows";
|
|
2
2
|
import { cloudflareOps } from "./connector.js";
|
|
3
3
|
const { create } = cloudflareOps.container;
|
|
4
|
-
/** The runtime image the steps' containers name: one already in
|
|
5
|
-
const IMAGE = "registry.cloudflare.com/0123456789abcdef0123456789abcdef/sanoma-ai-runtime:
|
|
4
|
+
/** The runtime image the steps' containers name: one already in the account's registry, pinned by digest, so nothing copies or asks for it. */
|
|
5
|
+
const IMAGE = "registry.cloudflare.com/0123456789abcdef0123456789abcdef/sanoma-ai-runtime@sha256:7777777777777777777777777777777777777777777777777777777777777777";
|
|
6
6
|
/**
|
|
7
7
|
* Cloudflare's steps for Gherkin tests: a company's container before the run (named `host`, so
|
|
8
8
|
* an operation's own test can use `$host.state.url`), its secrets carrying the wakeup token.
|
package/dist/steps.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"steps.js","sourceRoot":"","sources":["../src/steps.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAE/C,MAAM,EAAE,MAAM,EAAE,GAAG,aAAa,CAAC,SAAS,CAAC;AAE3C
|
|
1
|
+
{"version":3,"file":"steps.js","sourceRoot":"","sources":["../src/steps.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAE/C,MAAM,EAAE,MAAM,EAAE,GAAG,aAAa,CAAC,SAAS,CAAC;AAE3C,+IAA+I;AAC/I,MAAM,KAAK,GACT,oJAAoJ,CAAC;AAEvJ;;;GAGG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,WAAW,CAAC,YAAY,EAAE,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE;IACrE,KAAK,CAAC,0DAA0D,EAAE,CAAC,IAAY,EAAE,QAAgB,EAAE,EAAE,CAAC,CAAC;QACrG,EAAE,EAAE,MAAM;QACV,KAAK,EAAE;YACL,OAAO,EAAE;gBACP,IAAI;gBACJ,KAAK,EAAE,KAAK;gBACZ,MAAM,EAAE,2BAA2B,IAAI,MAAM;gBAC7C,GAAG,EAAE,MAAM;gBACX,QAAQ;gBACR,IAAI,EAAE,IAAI;gBACV,OAAO,EAAE,EAAE,mBAAmB,EAAE,EAAE,OAAO,EAAE,qBAAqB,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE;aACrF;SACF;QACD,EAAE,EAAE,MAAM;KACX,CAAC,CAAC,CAAC;AACN,CAAC,CAAC,CAAC"}
|
package/dist/worker.d.ts
CHANGED
|
@@ -9,4 +9,4 @@ export interface WorkerModule {
|
|
|
9
9
|
}
|
|
10
10
|
/** The Worker's modules, read from the package once. */
|
|
11
11
|
export declare function workerModules(): Promise<WorkerModule[]>;
|
|
12
|
-
export { HEALTH_PATH, INACTIVITY_MS, Instance, NAMESPACE, type ObjectState,
|
|
12
|
+
export { HEALTH_PATH, IMAGE, INACTIVITY_MS, Instance, NAMESPACE, type ObjectState, worker, } from "../worker/instance.js";
|
package/dist/worker.js
CHANGED
|
@@ -17,5 +17,5 @@ export function workerModules() {
|
|
|
17
17
|
});
|
|
18
18
|
return modules;
|
|
19
19
|
}
|
|
20
|
-
export { HEALTH_PATH, INACTIVITY_MS, Instance, NAMESPACE,
|
|
20
|
+
export { HEALTH_PATH, IMAGE, INACTIVITY_MS, Instance, NAMESPACE, worker, } from "../worker/instance.js";
|
|
21
21
|
//# sourceMappingURL=worker.js.map
|
package/dist/worker.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worker.js","sourceRoot":"","sources":["../src/worker.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE5C,+FAA+F;AAC/F,uEAAuE;AAEvE,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,YAAY,EAAE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;AAEtD,uFAAuF;AACvF,MAAM,CAAC,MAAM,WAAW,GAAG,UAAU,CAAC;AACtC,6CAA6C;AAC7C,MAAM,OAAO,GAAG,CAAC,WAAW,EAAE,aAAa,CAAU,CAAC;AAEtD,gGAAgG;AAChG,MAAM,CAAC,MAAM,YAAY,GAAG,gBAAgB,CAAC;AAQ7C,IAAI,OAA4C,CAAC;AAEjD,wDAAwD;AACxD,MAAM,UAAU,aAAa;IAC3B,OAAO,KAAK,OAAO,CAAC,GAAG,CACrB,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC,IAAI,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC,CAChG,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;QACvB,OAAO,GAAG,SAAS,CAAC;QACpB,MAAM,GAAG,CAAC;IACZ,CAAC,CAAC,CAAC;IACH,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,OAAO,EACL,WAAW,EACX,aAAa,EACb,QAAQ,EACR,SAAS,EAET,
|
|
1
|
+
{"version":3,"file":"worker.js","sourceRoot":"","sources":["../src/worker.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE5C,+FAA+F;AAC/F,uEAAuE;AAEvE,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,YAAY,EAAE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;AAEtD,uFAAuF;AACvF,MAAM,CAAC,MAAM,WAAW,GAAG,UAAU,CAAC;AACtC,6CAA6C;AAC7C,MAAM,OAAO,GAAG,CAAC,WAAW,EAAE,aAAa,CAAU,CAAC;AAEtD,gGAAgG;AAChG,MAAM,CAAC,MAAM,YAAY,GAAG,gBAAgB,CAAC;AAQ7C,IAAI,OAA4C,CAAC;AAEjD,wDAAwD;AACxD,MAAM,UAAU,aAAa;IAC3B,OAAO,KAAK,OAAO,CAAC,GAAG,CACrB,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC,IAAI,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC,CAChG,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;QACvB,OAAO,GAAG,SAAS,CAAC;QACpB,MAAM,GAAG,CAAC;IACZ,CAAC,CAAC,CAAC;IACH,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,OAAO,EACL,WAAW,EACX,KAAK,EACL,aAAa,EACb,QAAQ,EACR,SAAS,EAET,MAAM,GACP,MAAM,uBAAuB,CAAC"}
|
|
@@ -10,7 +10,7 @@ Feature: Cloudflare containers and alarms
|
|
|
10
10
|
{
|
|
11
11
|
"desired": {
|
|
12
12
|
"name": "company",
|
|
13
|
-
"image": "registry.cloudflare.com/0123456789abcdef0123456789abcdef/sanoma-ai-runtime:
|
|
13
|
+
"image": "registry.cloudflare.com/0123456789abcdef0123456789abcdef/sanoma-ai-runtime@sha256:7777777777777777777777777777777777777777777777777777777777777777",
|
|
14
14
|
"source": "https://github.com/acme/company.git",
|
|
15
15
|
"ref": "main",
|
|
16
16
|
"revision": "abc123",
|
|
@@ -33,7 +33,7 @@ Feature: Cloudflare containers and alarms
|
|
|
33
33
|
"gone": false,
|
|
34
34
|
"state": {
|
|
35
35
|
"name": "company",
|
|
36
|
-
"image": "registry.cloudflare.com/0123456789abcdef0123456789abcdef/sanoma-ai-runtime:
|
|
36
|
+
"image": "registry.cloudflare.com/0123456789abcdef0123456789abcdef/sanoma-ai-runtime@sha256:7777777777777777777777777777777777777777777777777777777777777777",
|
|
37
37
|
"source": "https://github.com/acme/company.git",
|
|
38
38
|
"ref": "main",
|
|
39
39
|
"revision": "abc123",
|
|
@@ -51,7 +51,7 @@ Feature: Cloudflare containers and alarms
|
|
|
51
51
|
"id": "company",
|
|
52
52
|
"desired": {
|
|
53
53
|
"name": "company",
|
|
54
|
-
"image": "registry.cloudflare.com/0123456789abcdef0123456789abcdef/sanoma-ai-runtime:
|
|
54
|
+
"image": "registry.cloudflare.com/0123456789abcdef0123456789abcdef/sanoma-ai-runtime@sha256:8888888888888888888888888888888888888888888888888888888888888888",
|
|
55
55
|
"source": "https://github.com/acme/company.git",
|
|
56
56
|
"ref": "main",
|
|
57
57
|
"revision": "abc123",
|
|
@@ -62,7 +62,7 @@ Feature: Cloudflare containers and alarms
|
|
|
62
62
|
"""
|
|
63
63
|
Then it returns
|
|
64
64
|
"""
|
|
65
|
-
{ "action": "update", "replace": [], "planned": { "image": "registry.cloudflare.com/0123456789abcdef0123456789abcdef/sanoma-ai-runtime:
|
|
65
|
+
{ "action": "update", "replace": [], "planned": { "image": "registry.cloudflare.com/0123456789abcdef0123456789abcdef/sanoma-ai-runtime@sha256:8888888888888888888888888888888888888888888888888888888888888888" } }
|
|
66
66
|
"""
|
|
67
67
|
|
|
68
68
|
Scenario: Delete
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanoma-ai/connector-cloudflare",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Cloudflare for Sanoma: a Container host that stops when quiet, and a Durable Object alarm that wakes it, from one Worker",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare",
|
|
@@ -61,12 +61,12 @@
|
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@sanoma-ai/testing": "0.1.0",
|
|
64
|
-
"@sanoma-ai/workflows": "0.
|
|
64
|
+
"@sanoma-ai/workflows": "0.10.3",
|
|
65
65
|
"msw": "^3.0.3",
|
|
66
66
|
"zod": "^4.6.5"
|
|
67
67
|
},
|
|
68
68
|
"peerDependencies": {
|
|
69
|
-
"@sanoma-ai/workflows": "^0.
|
|
69
|
+
"@sanoma-ai/workflows": "^0.10.3",
|
|
70
70
|
"zod": "^4"
|
|
71
71
|
},
|
|
72
72
|
"engines": {
|
package/worker/instance.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export declare const HEALTH_PATH: "/__sanoma/health";
|
|
|
8
8
|
export declare const INACTIVITY_MS: number;
|
|
9
9
|
/** The binding that names the object, in the Worker's env. */
|
|
10
10
|
export declare const NAMESPACE: "SANOMA_INSTANCE";
|
|
11
|
+
/** The name the upload gives the runtime image (`containers[].images`), which the object starts. */
|
|
12
|
+
export declare const IMAGE: "runtime";
|
|
11
13
|
|
|
12
14
|
/** The label naming an image: a digest of it. */
|
|
13
15
|
export declare function imageLabel(image: string): Promise<string>;
|
|
@@ -16,11 +18,21 @@ export declare function imageLabel(image: string): Promise<string>;
|
|
|
16
18
|
export interface ObjectState {
|
|
17
19
|
container: {
|
|
18
20
|
readonly running: boolean;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
/** The images the Worker's upload names (`containers[].images`), by name. */
|
|
22
|
+
readonly images: Record<string, string>;
|
|
23
|
+
start(options: {
|
|
24
|
+
image: string;
|
|
25
|
+
instance: string;
|
|
26
|
+
env: Record<string, string>;
|
|
27
|
+
enableInternet: boolean;
|
|
28
|
+
labels: Record<string, string>;
|
|
29
|
+
}): void;
|
|
30
|
+
inspect(): Promise<{ image?: string; labels: Record<string, string> } | null>;
|
|
21
31
|
destroy(reason?: string): Promise<void>;
|
|
22
32
|
getTcpPort(port: number): { fetch(input: string | Request, init?: RequestInit): Promise<Response> };
|
|
23
33
|
setInactivityTimeout(ms: number): Promise<void>;
|
|
34
|
+
/** Settles when the container started last ends: resolved on exit 0, rejected on any other. */
|
|
35
|
+
monitor?(): Promise<void>;
|
|
24
36
|
};
|
|
25
37
|
storage: {
|
|
26
38
|
get<T>(key: string): Promise<T | undefined>;
|
package/worker/instance.js
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
// a stubbed `ctx`, so both kits check this code. No imports, so Node loads it as it is.
|
|
4
4
|
//
|
|
5
5
|
// The object runs the company's container (`ctx.container`): it starts it on a request, at the
|
|
6
|
-
//
|
|
7
|
-
//
|
|
6
|
+
// Worker's `SANOMA_REVISION` and the image its upload names (`ctx.container.images.runtime`, as
|
|
7
|
+
// the `durable_object` scheduling policy has it: the object says what runs), a running one at
|
|
8
|
+
// another destroyed and started again (a deploy); it waits for `/healthz`, and proxies every
|
|
8
9
|
// request to it. It answers `/__sanoma/health` without starting it (with `?start=1`, once it
|
|
9
10
|
// runs), and holds the company's one wake (`/__sanoma/wake`) as an alarm, which starts the
|
|
10
11
|
// container at `at` and calls its `/wake`.
|
|
@@ -27,11 +28,16 @@ const MAX_POLL_MS = 1_000;
|
|
|
27
28
|
const WAKE_KEY = "wake";
|
|
28
29
|
/** The binding that names the object, in the Worker's env. */
|
|
29
30
|
export const NAMESPACE = "SANOMA_INSTANCE";
|
|
31
|
+
/** The name the upload gives the runtime image (`containers[].images`), which the object starts. */
|
|
32
|
+
export const IMAGE = "runtime";
|
|
33
|
+
/** The container's size, one of the named types `start` takes: ½ vCPU, 4 GiB (`basic`, ¼ vCPU, is refused under this policy). */
|
|
34
|
+
const INSTANCE_TYPE = "standard-1";
|
|
30
35
|
|
|
31
36
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
32
37
|
const json = (status, body) =>
|
|
33
38
|
new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
|
34
|
-
|
|
39
|
+
/** A refusal; with `exit`, how the container ended: a deploy stops waiting on it. */
|
|
40
|
+
const problem = (status, error, exit) => json(status, { error, ...(exit !== undefined && { exit }) });
|
|
35
41
|
|
|
36
42
|
/** The Worker's text bindings (plain text and secrets): the container's environment. */
|
|
37
43
|
function containerEnv(env) {
|
|
@@ -64,6 +70,12 @@ export class Instance {
|
|
|
64
70
|
#starting;
|
|
65
71
|
/** True once a container this object started (or found) answered `/healthz`. */
|
|
66
72
|
#ready = false;
|
|
73
|
+
/** What the last try of a start saw, for the health route while the container is not ready. */
|
|
74
|
+
#last;
|
|
75
|
+
/** How the container this object started ended, when it did: what `monitor` told. */
|
|
76
|
+
#exit;
|
|
77
|
+
/** The start whose `monitor` may set `#exit`: an earlier container's end is not this one's. */
|
|
78
|
+
#started;
|
|
67
79
|
|
|
68
80
|
constructor(ctx, env) {
|
|
69
81
|
this.ctx = ctx;
|
|
@@ -86,13 +98,15 @@ export class Instance {
|
|
|
86
98
|
try {
|
|
87
99
|
await this.ensureRunning();
|
|
88
100
|
} catch (error) {
|
|
89
|
-
return problem(503, `the company's container did not start: ${error?.message ?? error}
|
|
101
|
+
return problem(503, `the company's container did not start: ${error?.message ?? error}`, this.#exit);
|
|
90
102
|
}
|
|
91
103
|
if (health) return this.health();
|
|
92
104
|
const headers = new Headers(request.headers);
|
|
93
|
-
|
|
105
|
+
// The port's fetch reaches the container whatever the URL's host, which becomes the request's
|
|
106
|
+
// Host header: the one asked for, so the company serves the public site declared on it.
|
|
107
|
+
headers.set("x-forwarded-proto", url.protocol.slice(0, -1));
|
|
94
108
|
const body = request.method === "GET" || request.method === "HEAD" ? undefined : request.body;
|
|
95
|
-
const forwarded = new Request(new URL(url.pathname + url.search,
|
|
109
|
+
const forwarded = new Request(new URL(url.pathname + url.search, `http://${url.host}`), {
|
|
96
110
|
method: request.method,
|
|
97
111
|
headers,
|
|
98
112
|
body,
|
|
@@ -110,7 +124,13 @@ export class Instance {
|
|
|
110
124
|
|
|
111
125
|
/** What the Worker runs, and whether its container does. */
|
|
112
126
|
health() {
|
|
113
|
-
return json(200, {
|
|
127
|
+
return json(200, {
|
|
128
|
+
revision: this.env.SANOMA_REVISION ?? "",
|
|
129
|
+
running: this.ctx.container.running,
|
|
130
|
+
ready: this.#ready,
|
|
131
|
+
...(this.#last !== undefined && !this.#ready && { last: this.#last }),
|
|
132
|
+
...(this.#exit !== undefined && { exit: this.#exit }),
|
|
133
|
+
});
|
|
114
134
|
}
|
|
115
135
|
|
|
116
136
|
/** Starts the container at the Worker's revision and image unless it runs them and answered; one start at a time. */
|
|
@@ -122,13 +142,29 @@ export class Instance {
|
|
|
122
142
|
|
|
123
143
|
async #start() {
|
|
124
144
|
this.#ready = false;
|
|
145
|
+
this.#exit = undefined;
|
|
125
146
|
const container = this.ctx.container;
|
|
126
|
-
const
|
|
147
|
+
const image = container.images?.[IMAGE];
|
|
148
|
+
// Not one to retry: the Worker was uploaded without its image.
|
|
149
|
+
if (!image) throw new Error(`the Worker names no "${IMAGE}" image among its containers' images`);
|
|
150
|
+
const labels = { revision: this.env.SANOMA_REVISION ?? "", image: await imageLabel(image) };
|
|
127
151
|
const until = Date.now() + START_TIMEOUT_MS;
|
|
128
152
|
let last;
|
|
153
|
+
const saw = (what) => (last = this.#last = what);
|
|
129
154
|
let wait = POLL_MS;
|
|
130
155
|
let checked = false;
|
|
131
156
|
while (Date.now() < until) {
|
|
157
|
+
if (this.#exit) {
|
|
158
|
+
// Right after the application is made, the platform may not have assigned it to the
|
|
159
|
+
// namespace yet: that start fails and is tried again. Any other end is the answer, as a
|
|
160
|
+
// container that ended is not started again and again. Outside the try: a try's failure
|
|
161
|
+
// is what one try saw, and would otherwise swallow this.
|
|
162
|
+
if (!this.#exit.includes("no container application assigned")) {
|
|
163
|
+
throw new Error(`${this.#exit} before answering /healthz on port ${this.port} (${last})`);
|
|
164
|
+
}
|
|
165
|
+
saw(this.#exit);
|
|
166
|
+
this.#exit = undefined;
|
|
167
|
+
}
|
|
132
168
|
try {
|
|
133
169
|
if (!checked && container.running) {
|
|
134
170
|
// One running from before: kept only if it says it started at these (a deploy otherwise).
|
|
@@ -138,7 +174,25 @@ export class Instance {
|
|
|
138
174
|
}
|
|
139
175
|
checked = true;
|
|
140
176
|
if (!container.running) {
|
|
141
|
-
container.start({
|
|
177
|
+
container.start({
|
|
178
|
+
image,
|
|
179
|
+
instance: INSTANCE_TYPE,
|
|
180
|
+
env: containerEnv(this.env),
|
|
181
|
+
enableInternet: true,
|
|
182
|
+
labels,
|
|
183
|
+
});
|
|
184
|
+
// `start` returns once its options are checked: a start that fails later, or a container
|
|
185
|
+
// that exits, is what `monitor` tells, and the next try reports it. Only this start's, and
|
|
186
|
+
// only until it answered: an earlier container's monitor may settle after this one started,
|
|
187
|
+
// and one that answered and then stopped (Cloudflare's own stop, a crash) is started again
|
|
188
|
+
// by the next request, not reported as a start that failed.
|
|
189
|
+
this.#exit = undefined;
|
|
190
|
+
const started = (this.#started = {});
|
|
191
|
+
const ended = (how) => void (this.#started === started && !this.#ready && (this.#exit = how));
|
|
192
|
+
container.monitor?.().then(
|
|
193
|
+
() => ended("the container exited"),
|
|
194
|
+
(error) => ended(`the container failed: ${error?.message ?? error}`),
|
|
195
|
+
);
|
|
142
196
|
}
|
|
143
197
|
// A try of its own time: a hung one would hold every request past the deadline.
|
|
144
198
|
const res = await container
|
|
@@ -150,16 +204,17 @@ export class Instance {
|
|
|
150
204
|
this.#ready = true;
|
|
151
205
|
return;
|
|
152
206
|
}
|
|
153
|
-
|
|
207
|
+
saw(`/healthz answered ${res.status}`);
|
|
154
208
|
} catch (error) {
|
|
155
209
|
// Not listening yet, or not yet free to start again after a destroy.
|
|
156
|
-
|
|
210
|
+
saw(error?.message ?? String(error));
|
|
157
211
|
}
|
|
158
|
-
// Doubling to a second: a container that
|
|
212
|
+
// Doubling to a second: a container that is slow to listen is not asked four times a second.
|
|
159
213
|
await sleep(wait);
|
|
160
214
|
wait = Math.min(wait * 2, MAX_POLL_MS);
|
|
161
215
|
}
|
|
162
|
-
|
|
216
|
+
const ended = this.#exit ? `; ${this.#exit}` : "";
|
|
217
|
+
throw new Error(`no answer from /healthz on port ${this.port} in ${START_TIMEOUT_MS / 1000} s (${last}${ended})`);
|
|
163
218
|
}
|
|
164
219
|
|
|
165
220
|
/** `/__sanoma/wake`, with the company's `SANOMA_WAKEUP_TOKEN`: the wake this object holds. */
|