@lunora/container 1.0.0-alpha.3 → 1.0.0-alpha.5
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 +97 -1
- package/dist/do/index.d.mts +551 -3
- package/dist/do/index.d.ts +551 -3
- package/dist/do/index.mjs +189 -3
- package/dist/index.d.mts +99 -7
- package/dist/index.d.ts +99 -7
- package/dist/index.mjs +2 -2
- package/dist/packem_shared/ContainerProxy-DWqUX_re.mjs +1474 -0
- package/dist/packem_shared/{containerBindingName-BGdSdFNA.mjs → containerBindingName-CWmEE_3Y.mjs} +102 -3
- package/dist/packem_shared/createContainerContext-BtzK5gJL.mjs +225 -0
- package/dist/packem_shared/types.d-BlNwNY44.d.mts +250 -0
- package/dist/packem_shared/types.d-BlNwNY44.d.ts +250 -0
- package/package.json +1 -4
- package/dist/packem_shared/createContainerContext-CTpyUQ4J.mjs +0 -133
- package/dist/packem_shared/types.d-D2l2SYol.d.mts +0 -140
- package/dist/packem_shared/types.d-D2l2SYol.d.ts +0 -140
package/dist/do/index.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { Container } from '
|
|
2
|
-
|
|
1
|
+
import { Container } from '../packem_shared/ContainerProxy-DWqUX_re.mjs';
|
|
2
|
+
export { ContainerProxy, outboundParams } from '../packem_shared/ContainerProxy-DWqUX_re.mjs';
|
|
3
|
+
import { resolveContainerEnvVars as resolveContainerEnvVariables, parseDurationSeconds } from '../packem_shared/containerBindingName-CWmEE_3Y.mjs';
|
|
3
4
|
|
|
4
5
|
const LUNORA_EVENT_SOURCE = "lunora";
|
|
5
6
|
const buildContainerLifecycleEvent = (container, instance, event, message) => {
|
|
@@ -73,6 +74,9 @@ const reportContainerLifecycle = async (env, envelope, jurisdiction) => {
|
|
|
73
74
|
}
|
|
74
75
|
};
|
|
75
76
|
|
|
77
|
+
const READINESS_POLL_INTERVAL_MS = 500;
|
|
78
|
+
const READINESS_TIMEOUT_MS = 3e4;
|
|
79
|
+
const HARD_TIMEOUT_GENERATION_KEY = "__lunoraHardTimeoutGeneration";
|
|
76
80
|
class LunoraContainer extends Container {
|
|
77
81
|
/**
|
|
78
82
|
* Data-residency jurisdiction the app's DOs are pinned to (codegen passes the
|
|
@@ -82,17 +86,82 @@ class LunoraContainer extends Container {
|
|
|
82
86
|
lunoraJurisdiction;
|
|
83
87
|
/** The `lunora/containers.ts` export name, for lifecycle log correlation. */
|
|
84
88
|
lunoraName;
|
|
89
|
+
/** Default port the readiness probes target when a check omits its own `port`. */
|
|
90
|
+
lunoraDefaultPort;
|
|
91
|
+
/** Hard-cap lifetime in whole seconds (from the `hardTimeout` config), or `undefined`. */
|
|
92
|
+
lunoraHardTimeoutSeconds;
|
|
93
|
+
/** Declarative readiness probes that gate request proxying (from the `readyOn` config). */
|
|
94
|
+
lunoraReadyOn;
|
|
95
|
+
/** Map of container env-var name → Worker Secrets Store binding name (from the `secretsStore` config). */
|
|
96
|
+
lunoraSecretsStore;
|
|
97
|
+
/** Memoised Secrets Store resolution: run once, then merged into `envVars` before the first start. */
|
|
98
|
+
lunoraSecretsStoreResolved;
|
|
85
99
|
constructor(context, env, definition, exportName, jurisdiction) {
|
|
86
100
|
super(context, env, {
|
|
87
101
|
defaultPort: definition.defaultPort,
|
|
102
|
+
entrypoint: definition.entrypoint ? [...definition.entrypoint] : void 0,
|
|
88
103
|
envVars: resolveContainerEnvVariables(definition, env, exportName),
|
|
89
104
|
sleepAfter: definition.sleepAfter
|
|
90
105
|
});
|
|
91
106
|
if (definition.enableInternet !== void 0) {
|
|
92
107
|
this.enableInternet = definition.enableInternet;
|
|
93
108
|
}
|
|
109
|
+
if (definition.requiredPorts !== void 0) {
|
|
110
|
+
this.requiredPorts = [...definition.requiredPorts];
|
|
111
|
+
}
|
|
112
|
+
if (definition.interceptHttps !== void 0) {
|
|
113
|
+
this.interceptHttps = definition.interceptHttps;
|
|
114
|
+
}
|
|
115
|
+
if (definition.allowedHosts !== void 0) {
|
|
116
|
+
this.allowedHosts = [...definition.allowedHosts];
|
|
117
|
+
}
|
|
118
|
+
if (definition.deniedHosts !== void 0) {
|
|
119
|
+
this.deniedHosts = [...definition.deniedHosts];
|
|
120
|
+
}
|
|
121
|
+
if (definition.pingEndpoint !== void 0) {
|
|
122
|
+
this.pingEndpoint = definition.pingEndpoint;
|
|
123
|
+
}
|
|
124
|
+
if (definition.labels !== void 0) {
|
|
125
|
+
this.labels = { ...definition.labels };
|
|
126
|
+
}
|
|
94
127
|
this.lunoraName = exportName ?? "container";
|
|
95
128
|
this.lunoraJurisdiction = jurisdiction;
|
|
129
|
+
this.lunoraDefaultPort = definition.defaultPort;
|
|
130
|
+
this.lunoraReadyOn = definition.readyOn ? [...definition.readyOn] : [];
|
|
131
|
+
this.lunoraHardTimeoutSeconds = definition.hardTimeout === void 0 ? void 0 : parseDurationSeconds(definition.hardTimeout);
|
|
132
|
+
this.lunoraSecretsStore = definition.secretsStore;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Proxy entry for every `ctx.containers.<name>` fetch. Resolves the
|
|
136
|
+
* `secretsStore` bindings into `envVars` before delegating, so the values
|
|
137
|
+
* are present when the base implicitly starts the container for this
|
|
138
|
+
* request — a no-op when `secretsStore` is unset.
|
|
139
|
+
*/
|
|
140
|
+
async containerFetch(...args) {
|
|
141
|
+
await this.resolveSecretsStoreEnv();
|
|
142
|
+
return super.containerFetch(...args);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Explicit start (`ctx.containers.<name>.get(id).start()`). Resolves the
|
|
146
|
+
* `secretsStore` bindings into `envVars` first, mirroring
|
|
147
|
+
* {@link containerFetch}. A per-instance `start({ envVars })` replaces the
|
|
148
|
+
* env set wholesale (base behavior), so the injected values only apply to a
|
|
149
|
+
* bare `start()` — same as the static `env`/`secrets`. When the caller
|
|
150
|
+
* supplies its own `envVars` we skip resolution entirely: those values would
|
|
151
|
+
* be discarded anyway, so a missing/unreadable binding shouldn't fail a start
|
|
152
|
+
* that never uses them.
|
|
153
|
+
*/
|
|
154
|
+
async start(...args) {
|
|
155
|
+
const [options] = args;
|
|
156
|
+
if (options?.envVars === void 0) {
|
|
157
|
+
await this.resolveSecretsStoreEnv();
|
|
158
|
+
}
|
|
159
|
+
return super.start(...args);
|
|
160
|
+
}
|
|
161
|
+
async onActivityExpired() {
|
|
162
|
+
const envelope = emitContainerLifecycle(this.lunoraName, this.instanceId(), "sleep");
|
|
163
|
+
this.surfaceInStudioLogs(envelope);
|
|
164
|
+
await super.onActivityExpired();
|
|
96
165
|
}
|
|
97
166
|
onError(error) {
|
|
98
167
|
const envelope = emitContainerLifecycle(this.lunoraName, this.instanceId(), "error", error instanceof Error ? error.message : String(error));
|
|
@@ -103,12 +172,129 @@ class LunoraContainer extends Container {
|
|
|
103
172
|
const envelope = emitContainerLifecycle(this.lunoraName, this.instanceId(), "start");
|
|
104
173
|
this.surfaceInStudioLogs(envelope);
|
|
105
174
|
await super.onStart();
|
|
175
|
+
await this.armHardTimeout();
|
|
176
|
+
await this.awaitContainerReadiness();
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Hook run when the container's `hardTimeout` elapses (dispatched by the base
|
|
180
|
+
* scheduler via the run-generation-stamped schedule armed in
|
|
181
|
+
* {@link onStart}). Default: stop the instance. Override to drain/checkpoint
|
|
182
|
+
* first. A stale schedule from a previous run, or an already-stopped
|
|
183
|
+
* instance, is ignored (upstream cloudflare/containers#85).
|
|
184
|
+
*/
|
|
185
|
+
async onHardTimeoutExpired(payload) {
|
|
186
|
+
const current = await this.ctx.storage.get(HARD_TIMEOUT_GENERATION_KEY);
|
|
187
|
+
if (payload?.generation !== void 0 && payload.generation !== current) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (this.ctx.container?.running !== true) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const envelope = emitContainerLifecycle(this.lunoraName, this.instanceId(), "stop", "hard timeout reached");
|
|
194
|
+
this.surfaceInStudioLogs(envelope);
|
|
195
|
+
await this.stop();
|
|
106
196
|
}
|
|
107
197
|
async onStop(parameters) {
|
|
108
198
|
const envelope = emitContainerLifecycle(this.lunoraName, this.instanceId(), "stop", `${parameters.reason} (exit ${String(parameters.exitCode)})`);
|
|
109
199
|
this.surfaceInStudioLogs(envelope);
|
|
110
200
|
await super.onStop(parameters);
|
|
111
201
|
}
|
|
202
|
+
/**
|
|
203
|
+
* Arm the hard-timeout kill via the base scheduler (so it integrates with
|
|
204
|
+
* the container's own alarm machinery instead of fighting it). Bumps the run
|
|
205
|
+
* generation and stamps the schedule with it, so {@link onHardTimeoutExpired}
|
|
206
|
+
* can tell a fresh schedule from a stale one. No-op without a `hardTimeout`.
|
|
207
|
+
*/
|
|
208
|
+
async armHardTimeout() {
|
|
209
|
+
if (this.lunoraHardTimeoutSeconds === void 0) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const generation = (await this.ctx.storage.get(HARD_TIMEOUT_GENERATION_KEY) ?? 0) + 1;
|
|
213
|
+
await this.ctx.storage.put(HARD_TIMEOUT_GENERATION_KEY, generation);
|
|
214
|
+
await this.schedule(this.lunoraHardTimeoutSeconds, "onHardTimeoutExpired", { generation });
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Resolve the `secretsStore` bindings (async `.get()`) once and merge the
|
|
218
|
+
* values into `envVars`, so they're present when the base starts the
|
|
219
|
+
* container. Memoised on the first call — every later start reuses the
|
|
220
|
+
* resolved promise. A missing binding or a non-string value fails fast (the
|
|
221
|
+
* start surfaces the error), the same fail-closed stance the static
|
|
222
|
+
* `secrets` resolution takes for a missing Worker secret. No-op without
|
|
223
|
+
* `secretsStore`.
|
|
224
|
+
*/
|
|
225
|
+
async resolveSecretsStoreEnv() {
|
|
226
|
+
const secretsStore = this.lunoraSecretsStore;
|
|
227
|
+
if (secretsStore === void 0) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
this.lunoraSecretsStoreResolved ??= (async () => {
|
|
231
|
+
const workerEnv = this.env;
|
|
232
|
+
const resolved = {};
|
|
233
|
+
for (const [envName, binding] of Object.entries(secretsStore)) {
|
|
234
|
+
const store = workerEnv[binding];
|
|
235
|
+
if (store === void 0 || typeof store.get !== "function") {
|
|
236
|
+
throw new Error(
|
|
237
|
+
`container "${this.lunoraName}": secretsStore env "${envName}" points at binding "${binding}", which is not a Secrets Store binding on the Worker env. Add a \`secrets_store_secrets\` entry binding "${binding}".`
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
const value = await store.get();
|
|
241
|
+
if (typeof value !== "string") {
|
|
242
|
+
throw new TypeError(
|
|
243
|
+
`container "${this.lunoraName}": Secrets Store binding "${binding}" (env "${envName}") did not resolve to a string value.`
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
resolved[envName] = value;
|
|
247
|
+
}
|
|
248
|
+
this.envVars = { ...this.envVars, ...resolved };
|
|
249
|
+
})();
|
|
250
|
+
await this.lunoraSecretsStoreResolved;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Block until every `readyOn` probe responds with its expected status, or
|
|
254
|
+
* throw once the readiness budget is spent. Probes run in parallel and hit
|
|
255
|
+
* the container's TCP port directly (NOT `containerFetch`, which would
|
|
256
|
+
* recurse back into the start path). No-op without `readyOn`.
|
|
257
|
+
*/
|
|
258
|
+
async awaitContainerReadiness() {
|
|
259
|
+
if (this.lunoraReadyOn.length === 0) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const { container } = this.ctx;
|
|
263
|
+
if (container === void 0) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const deadline = Date.now() + READINESS_TIMEOUT_MS;
|
|
267
|
+
await Promise.all(this.lunoraReadyOn.map(async (check) => this.awaitReadinessCheck(container, check, deadline)));
|
|
268
|
+
}
|
|
269
|
+
/** Poll one readiness probe until it returns its expected status or the shared deadline passes. */
|
|
270
|
+
async awaitReadinessCheck(container, check, deadline) {
|
|
271
|
+
const port = check.port ?? this.lunoraDefaultPort;
|
|
272
|
+
if (port === void 0) {
|
|
273
|
+
throw new Error(
|
|
274
|
+
`container "${this.lunoraName}": readyOn check "${check.path}" has no port — set the check's \`port\` or the container \`defaultPort\`.`
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
const expectedStatus = check.status ?? 200;
|
|
278
|
+
const path = check.path.startsWith("/") ? check.path : `/${check.path}`;
|
|
279
|
+
const tcpPort = container.getTcpPort(port);
|
|
280
|
+
for (; ; ) {
|
|
281
|
+
try {
|
|
282
|
+
const response = await tcpPort.fetch(`http://container${path}`);
|
|
283
|
+
if (response.status === expectedStatus) {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
} catch {
|
|
287
|
+
}
|
|
288
|
+
if (Date.now() >= deadline) {
|
|
289
|
+
throw new Error(
|
|
290
|
+
`container "${this.lunoraName}": readiness check "${check.path}" (port ${String(port)}) did not return ${String(expectedStatus)} within ${String(READINESS_TIMEOUT_MS)}ms`
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
await new Promise((resolve) => {
|
|
294
|
+
setTimeout(resolve, READINESS_POLL_INTERVAL_MS);
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
112
298
|
/**
|
|
113
299
|
* Best-effort push of `envelope` into the root ShardDO's log buffer so it
|
|
114
300
|
* also appears in the Studio Logs panel (the terminal already has it via
|
|
@@ -135,4 +321,4 @@ class LunoraContainer extends Container {
|
|
|
135
321
|
}
|
|
136
322
|
}
|
|
137
323
|
|
|
138
|
-
export { LunoraContainer
|
|
324
|
+
export { LunoraContainer };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export type { B as BuildImageSource, c as ContainerInstanceType, d as
|
|
1
|
+
import { C as ContainerConfig, a as ContainerDefinition, b as ContainerImageSource, N as NormalizedContainerImage } from "./packem_shared/types.d-BlNwNY44.mjs";
|
|
2
|
+
export type { B as BuildImageSource, c as ContainerInstanceType, d as ContainerReadinessCheck, e as ContainerRollout, f as CustomContainerInstanceType, g as NamedContainerInstanceType, R as RegistryImageSource } from "./packem_shared/types.d-BlNwNY44.mjs";
|
|
3
3
|
/**
|
|
4
4
|
* The `ctx.containers` action surface: typed handles over the `CONTAINER_*`
|
|
5
5
|
* Durable Object namespace bindings the config layer reconciles.
|
|
@@ -23,13 +23,25 @@ interface ContainerStartOptions {
|
|
|
23
23
|
/** A container instance's runtime state, as returned by `getState()`. Structural — the platform adds fields over time. */
|
|
24
24
|
interface ContainerInstanceState {
|
|
25
25
|
[key: string]: unknown;
|
|
26
|
+
/** Process exit code, present once the instance has `stopped_with_code`. */
|
|
27
|
+
exitCode?: number;
|
|
28
|
+
/** Epoch-ms of the last state transition. */
|
|
26
29
|
lastChange?: number;
|
|
30
|
+
/** Lifecycle status. Widening union — Cloudflare adds values over time. */
|
|
31
|
+
status?: "healthy" | "running" | "stopped" | "stopped_with_code" | "stopping";
|
|
27
32
|
}
|
|
28
|
-
/** What a handle needs from a Durable Object stub — `fetch` plus the optional lifecycle RPCs the container DO exposes. */
|
|
33
|
+
/** What a handle needs from a Durable Object stub — `fetch` plus the optional lifecycle/egress RPCs the container DO exposes. */
|
|
29
34
|
interface ContainerStubLike {
|
|
35
|
+
allowHost?: (hostname: string) => Promise<void>;
|
|
36
|
+
denyHost?: (hostname: string) => Promise<void>;
|
|
30
37
|
destroy?: () => Promise<void>;
|
|
31
38
|
fetch: (input: Request) => Promise<Response>;
|
|
32
39
|
getState?: () => Promise<ContainerInstanceState>;
|
|
40
|
+
removeAllowedHost?: (hostname: string) => Promise<void>;
|
|
41
|
+
removeDeniedHost?: (hostname: string) => Promise<void>;
|
|
42
|
+
renewActivityTimeout?: () => Promise<void>;
|
|
43
|
+
setAllowedHosts?: (hosts: string[]) => Promise<void>;
|
|
44
|
+
setDeniedHosts?: (hosts: string[]) => Promise<void>;
|
|
33
45
|
start?: (options?: ContainerStartOptions) => Promise<void>;
|
|
34
46
|
stop?: (signal?: number | string) => Promise<void>;
|
|
35
47
|
}
|
|
@@ -57,6 +69,15 @@ interface ContainerHandle {
|
|
|
57
69
|
* `Request`/URL passes through unchanged.
|
|
58
70
|
*/
|
|
59
71
|
fetch: (input: Request | string, init?: RequestInit) => Promise<Response>;
|
|
72
|
+
/**
|
|
73
|
+
* Return a handle that routes every request to `targetPort` on the
|
|
74
|
+
* container instead of the definition's `defaultPort` — for multi-port
|
|
75
|
+
* containers (declare the ports in `requiredPorts`). Sets the
|
|
76
|
+
* `cf-container-target-port` header the way `@cloudflare/containers`'
|
|
77
|
+
* `switchPort` does, so it composes with `.get()`, `.any()`, and `.pool()`:
|
|
78
|
+
* `ctx.containers.app.get("u1").port(9090).fetch("/admin")`.
|
|
79
|
+
*/
|
|
80
|
+
port: (targetPort: number) => ContainerHandle;
|
|
60
81
|
}
|
|
61
82
|
/**
|
|
62
83
|
* A handle on a *named* instance (from `.get(name)`) — `fetch` plus explicit
|
|
@@ -68,23 +89,76 @@ interface ContainerHandle {
|
|
|
68
89
|
interface ContainerInstanceHandle extends ContainerHandle {
|
|
69
90
|
/** Stop and discard the instance (its ephemeral disk is lost). */
|
|
70
91
|
destroy: () => Promise<void>;
|
|
92
|
+
/**
|
|
93
|
+
* Adjust this instance's egress allow/deny lists at runtime — the dynamic
|
|
94
|
+
* counterpart to the static `allowedHosts`/`deniedHosts` config. Useful for
|
|
95
|
+
* per-tenant egress policy. Requires the worker to export `ContainerProxy`
|
|
96
|
+
* (codegen re-exports it from the generated container file whenever any
|
|
97
|
+
* container is defined, so the runtime controls always work).
|
|
98
|
+
*/
|
|
99
|
+
egress: ContainerEgressControls;
|
|
71
100
|
/** Read the instance's current runtime state. */
|
|
72
101
|
getState: () => Promise<ContainerInstanceState>;
|
|
102
|
+
/**
|
|
103
|
+
* Reset the instance's `sleepAfter` idle timer. The platform renews it on
|
|
104
|
+
* each proxied request, and because `@lunora/container` proxies WebSocket
|
|
105
|
+
* frames through the Durable Object, message traffic on an open socket
|
|
106
|
+
* renews it too (the WebSocket-keepalive gap of cloudflare/containers#147 is
|
|
107
|
+
* closed in the bundled base). This manual control is the escape hatch for
|
|
108
|
+
* keeping a container awake during activity that is neither an HTTP request
|
|
109
|
+
* nor a WS message — e.g. a long out-of-band job running inside it.
|
|
110
|
+
*/
|
|
111
|
+
renewActivityTimeout: () => Promise<void>;
|
|
73
112
|
/** Explicitly start the instance, optionally with per-instance env/entrypoint. */
|
|
74
113
|
start: (options?: ContainerStartOptions) => Promise<void>;
|
|
75
114
|
/** Stop the instance (optionally with a signal); it can start again on the next request. */
|
|
76
115
|
stop: (signal?: number | string) => Promise<void>;
|
|
77
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* Runtime egress-firewall controls for a named instance (`handle.egress.*`).
|
|
119
|
+
* Each maps to the corresponding `@cloudflare/containers` `Container` RPC, so
|
|
120
|
+
* an app can tighten or relax a single instance's allowed/denied hosts after
|
|
121
|
+
* start without redeploying.
|
|
122
|
+
*/
|
|
123
|
+
interface ContainerEgressControls {
|
|
124
|
+
/** Add one hostname (or glob) to the allow-list. */
|
|
125
|
+
allow: (hostname: string) => Promise<void>;
|
|
126
|
+
/** Add one hostname (or glob) to the deny-list. */
|
|
127
|
+
deny: (hostname: string) => Promise<void>;
|
|
128
|
+
/** Remove one hostname from the allow-list. */
|
|
129
|
+
removeAllowed: (hostname: string) => Promise<void>;
|
|
130
|
+
/** Remove one hostname from the deny-list. */
|
|
131
|
+
removeDenied: (hostname: string) => Promise<void>;
|
|
132
|
+
/** Replace the entire allow-list. */
|
|
133
|
+
setAllowed: (hosts: ReadonlyArray<string>) => Promise<void>;
|
|
134
|
+
/** Replace the entire deny-list. */
|
|
135
|
+
setDenied: (hosts: ReadonlyArray<string>) => Promise<void>;
|
|
136
|
+
}
|
|
78
137
|
/** The per-definition accessor exposed as `ctx.containers.<exportName>`. */
|
|
79
138
|
interface ContainerAccessor {
|
|
80
139
|
/**
|
|
81
140
|
* A random instance from a fixed pool of `count` (defaults to the
|
|
82
141
|
* definition's `maxInstances`, else 3 — mirroring `getRandom` from
|
|
83
142
|
* `@cloudflare/containers`). For stateless, interchangeable workloads.
|
|
143
|
+
*
|
|
144
|
+
* Like `.get()`, a path/URL-string fetch transparently retries the
|
|
145
|
+
* cold-start "instance is provisioning" transients (cloudflare/containers#45,
|
|
146
|
+
* #139); pass {@link InstanceRetryOptions} to tune or disable it.
|
|
84
147
|
*/
|
|
85
|
-
any: (count?: number) => ContainerHandle;
|
|
86
|
-
/**
|
|
87
|
-
|
|
148
|
+
any: (count?: number, options?: InstanceRetryOptions) => ContainerHandle;
|
|
149
|
+
/**
|
|
150
|
+
* The instance for `name` — one container per entity (user, room, job…),
|
|
151
|
+
* with lifecycle control.
|
|
152
|
+
*
|
|
153
|
+
* A path/URL-string fetch transparently retries the platform's cold-start
|
|
154
|
+
* transients — "there is no Container instance available" / "container is
|
|
155
|
+
* not listening" while an instance is still provisioning
|
|
156
|
+
* (cloudflare/containers#45, #139) — on the *same* instance with backoff,
|
|
157
|
+
* since the request never reached the app. Pass {@link InstanceRetryOptions}
|
|
158
|
+
* to tune attempts/backoff or disable it (`{ attempts: 1 }`). A pre-built
|
|
159
|
+
* `Request` (possibly a one-shot stream body) is sent once, never retried.
|
|
160
|
+
*/
|
|
161
|
+
get: (name: string, options?: InstanceRetryOptions) => ContainerInstanceHandle;
|
|
88
162
|
/**
|
|
89
163
|
* A resilient handle over the pool: each `fetch` picks a random instance and,
|
|
90
164
|
* on a thrown error or a retryable response (5xx by default), retries on a
|
|
@@ -121,6 +195,24 @@ interface PoolOptions {
|
|
|
121
195
|
/** Pool size to spread picks across. Defaults to the definition's `maxInstances`, else 3. */
|
|
122
196
|
size?: number;
|
|
123
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* Tuning for the cold-start retry on a `.get()`/`.any()` handle. The retry fires
|
|
200
|
+
* only on the platform's provisioning transients (no-instance / not-listening /
|
|
201
|
+
* rate-limited — see {@link isColdStartTransient}), which is why it's safe by
|
|
202
|
+
* default: those responses mean the request never reached the container.
|
|
203
|
+
*/
|
|
204
|
+
interface InstanceRetryOptions {
|
|
205
|
+
/**
|
|
206
|
+
* Total attempts on a cold-start transient before the last outcome is
|
|
207
|
+
* surfaced as-is. `1` disables the retry. Default
|
|
208
|
+
* {@link DEFAULT_COLD_START_ATTEMPTS}.
|
|
209
|
+
*/
|
|
210
|
+
attempts?: number;
|
|
211
|
+
/** Base backoff in ms between attempts; doubles each retry (0 disables the wait). Default {@link DEFAULT_COLD_START_BACKOFF_MS}. */
|
|
212
|
+
backoffMs?: number;
|
|
213
|
+
/** Upper bound on a single backoff sleep, in ms. Default {@link DEFAULT_MAX_BACKOFF_MS} (30s). */
|
|
214
|
+
maxBackoffMs?: number;
|
|
215
|
+
}
|
|
124
216
|
/** Wiring info for one definition, emitted by codegen into the generated DO. */
|
|
125
217
|
interface ContainerBindingSpec {
|
|
126
218
|
/** Durable Object binding name, e.g. `CONTAINER_TRANSCODER`. */
|
|
@@ -197,4 +289,4 @@ declare const isContainerDefinition: (value: unknown) => value is ContainerDefin
|
|
|
197
289
|
* credential it was promised yields far worse errors downstream.
|
|
198
290
|
*/
|
|
199
291
|
declare const resolveContainerEnvVariables: (definition: ContainerDefinition, workerEnv: Record<string, unknown>, exportName?: string) => Record<string, string>;
|
|
200
|
-
export { type ContainerAccessor, type ContainerBindingSpec, type ContainerConfig, type ContainerDefinition, type ContainerHandle, type ContainerImageSource, type ContainerInstanceHandle, type ContainerInstanceState, type ContainerNamespaceLike, type ContainerStartOptions, type ContainerTestHandler, type DurableObjectJurisdiction, type NormalizedContainerImage, type PoolOptions, containerBindingName, containerBuildTag, containerClassName, createContainerContext, createContainerTestContext, defineContainer, isContainerDefinition, normalizeContainerImage, resolveContainerEnvVariables as resolveContainerEnvVars };
|
|
292
|
+
export { type ContainerAccessor, type ContainerBindingSpec, type ContainerConfig, type ContainerDefinition, type ContainerEgressControls, type ContainerHandle, type ContainerImageSource, type ContainerInstanceHandle, type ContainerInstanceState, type ContainerNamespaceLike, type ContainerStartOptions, type ContainerTestHandler, type DurableObjectJurisdiction, type InstanceRetryOptions, type NormalizedContainerImage, type PoolOptions, containerBindingName, containerBuildTag, containerClassName, createContainerContext, createContainerTestContext, defineContainer, isContainerDefinition, normalizeContainerImage, resolveContainerEnvVariables as resolveContainerEnvVars };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export type { B as BuildImageSource, c as ContainerInstanceType, d as
|
|
1
|
+
import { C as ContainerConfig, a as ContainerDefinition, b as ContainerImageSource, N as NormalizedContainerImage } from "./packem_shared/types.d-BlNwNY44.js";
|
|
2
|
+
export type { B as BuildImageSource, c as ContainerInstanceType, d as ContainerReadinessCheck, e as ContainerRollout, f as CustomContainerInstanceType, g as NamedContainerInstanceType, R as RegistryImageSource } from "./packem_shared/types.d-BlNwNY44.js";
|
|
3
3
|
/**
|
|
4
4
|
* The `ctx.containers` action surface: typed handles over the `CONTAINER_*`
|
|
5
5
|
* Durable Object namespace bindings the config layer reconciles.
|
|
@@ -23,13 +23,25 @@ interface ContainerStartOptions {
|
|
|
23
23
|
/** A container instance's runtime state, as returned by `getState()`. Structural — the platform adds fields over time. */
|
|
24
24
|
interface ContainerInstanceState {
|
|
25
25
|
[key: string]: unknown;
|
|
26
|
+
/** Process exit code, present once the instance has `stopped_with_code`. */
|
|
27
|
+
exitCode?: number;
|
|
28
|
+
/** Epoch-ms of the last state transition. */
|
|
26
29
|
lastChange?: number;
|
|
30
|
+
/** Lifecycle status. Widening union — Cloudflare adds values over time. */
|
|
31
|
+
status?: "healthy" | "running" | "stopped" | "stopped_with_code" | "stopping";
|
|
27
32
|
}
|
|
28
|
-
/** What a handle needs from a Durable Object stub — `fetch` plus the optional lifecycle RPCs the container DO exposes. */
|
|
33
|
+
/** What a handle needs from a Durable Object stub — `fetch` plus the optional lifecycle/egress RPCs the container DO exposes. */
|
|
29
34
|
interface ContainerStubLike {
|
|
35
|
+
allowHost?: (hostname: string) => Promise<void>;
|
|
36
|
+
denyHost?: (hostname: string) => Promise<void>;
|
|
30
37
|
destroy?: () => Promise<void>;
|
|
31
38
|
fetch: (input: Request) => Promise<Response>;
|
|
32
39
|
getState?: () => Promise<ContainerInstanceState>;
|
|
40
|
+
removeAllowedHost?: (hostname: string) => Promise<void>;
|
|
41
|
+
removeDeniedHost?: (hostname: string) => Promise<void>;
|
|
42
|
+
renewActivityTimeout?: () => Promise<void>;
|
|
43
|
+
setAllowedHosts?: (hosts: string[]) => Promise<void>;
|
|
44
|
+
setDeniedHosts?: (hosts: string[]) => Promise<void>;
|
|
33
45
|
start?: (options?: ContainerStartOptions) => Promise<void>;
|
|
34
46
|
stop?: (signal?: number | string) => Promise<void>;
|
|
35
47
|
}
|
|
@@ -57,6 +69,15 @@ interface ContainerHandle {
|
|
|
57
69
|
* `Request`/URL passes through unchanged.
|
|
58
70
|
*/
|
|
59
71
|
fetch: (input: Request | string, init?: RequestInit) => Promise<Response>;
|
|
72
|
+
/**
|
|
73
|
+
* Return a handle that routes every request to `targetPort` on the
|
|
74
|
+
* container instead of the definition's `defaultPort` — for multi-port
|
|
75
|
+
* containers (declare the ports in `requiredPorts`). Sets the
|
|
76
|
+
* `cf-container-target-port` header the way `@cloudflare/containers`'
|
|
77
|
+
* `switchPort` does, so it composes with `.get()`, `.any()`, and `.pool()`:
|
|
78
|
+
* `ctx.containers.app.get("u1").port(9090).fetch("/admin")`.
|
|
79
|
+
*/
|
|
80
|
+
port: (targetPort: number) => ContainerHandle;
|
|
60
81
|
}
|
|
61
82
|
/**
|
|
62
83
|
* A handle on a *named* instance (from `.get(name)`) — `fetch` plus explicit
|
|
@@ -68,23 +89,76 @@ interface ContainerHandle {
|
|
|
68
89
|
interface ContainerInstanceHandle extends ContainerHandle {
|
|
69
90
|
/** Stop and discard the instance (its ephemeral disk is lost). */
|
|
70
91
|
destroy: () => Promise<void>;
|
|
92
|
+
/**
|
|
93
|
+
* Adjust this instance's egress allow/deny lists at runtime — the dynamic
|
|
94
|
+
* counterpart to the static `allowedHosts`/`deniedHosts` config. Useful for
|
|
95
|
+
* per-tenant egress policy. Requires the worker to export `ContainerProxy`
|
|
96
|
+
* (codegen re-exports it from the generated container file whenever any
|
|
97
|
+
* container is defined, so the runtime controls always work).
|
|
98
|
+
*/
|
|
99
|
+
egress: ContainerEgressControls;
|
|
71
100
|
/** Read the instance's current runtime state. */
|
|
72
101
|
getState: () => Promise<ContainerInstanceState>;
|
|
102
|
+
/**
|
|
103
|
+
* Reset the instance's `sleepAfter` idle timer. The platform renews it on
|
|
104
|
+
* each proxied request, and because `@lunora/container` proxies WebSocket
|
|
105
|
+
* frames through the Durable Object, message traffic on an open socket
|
|
106
|
+
* renews it too (the WebSocket-keepalive gap of cloudflare/containers#147 is
|
|
107
|
+
* closed in the bundled base). This manual control is the escape hatch for
|
|
108
|
+
* keeping a container awake during activity that is neither an HTTP request
|
|
109
|
+
* nor a WS message — e.g. a long out-of-band job running inside it.
|
|
110
|
+
*/
|
|
111
|
+
renewActivityTimeout: () => Promise<void>;
|
|
73
112
|
/** Explicitly start the instance, optionally with per-instance env/entrypoint. */
|
|
74
113
|
start: (options?: ContainerStartOptions) => Promise<void>;
|
|
75
114
|
/** Stop the instance (optionally with a signal); it can start again on the next request. */
|
|
76
115
|
stop: (signal?: number | string) => Promise<void>;
|
|
77
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* Runtime egress-firewall controls for a named instance (`handle.egress.*`).
|
|
119
|
+
* Each maps to the corresponding `@cloudflare/containers` `Container` RPC, so
|
|
120
|
+
* an app can tighten or relax a single instance's allowed/denied hosts after
|
|
121
|
+
* start without redeploying.
|
|
122
|
+
*/
|
|
123
|
+
interface ContainerEgressControls {
|
|
124
|
+
/** Add one hostname (or glob) to the allow-list. */
|
|
125
|
+
allow: (hostname: string) => Promise<void>;
|
|
126
|
+
/** Add one hostname (or glob) to the deny-list. */
|
|
127
|
+
deny: (hostname: string) => Promise<void>;
|
|
128
|
+
/** Remove one hostname from the allow-list. */
|
|
129
|
+
removeAllowed: (hostname: string) => Promise<void>;
|
|
130
|
+
/** Remove one hostname from the deny-list. */
|
|
131
|
+
removeDenied: (hostname: string) => Promise<void>;
|
|
132
|
+
/** Replace the entire allow-list. */
|
|
133
|
+
setAllowed: (hosts: ReadonlyArray<string>) => Promise<void>;
|
|
134
|
+
/** Replace the entire deny-list. */
|
|
135
|
+
setDenied: (hosts: ReadonlyArray<string>) => Promise<void>;
|
|
136
|
+
}
|
|
78
137
|
/** The per-definition accessor exposed as `ctx.containers.<exportName>`. */
|
|
79
138
|
interface ContainerAccessor {
|
|
80
139
|
/**
|
|
81
140
|
* A random instance from a fixed pool of `count` (defaults to the
|
|
82
141
|
* definition's `maxInstances`, else 3 — mirroring `getRandom` from
|
|
83
142
|
* `@cloudflare/containers`). For stateless, interchangeable workloads.
|
|
143
|
+
*
|
|
144
|
+
* Like `.get()`, a path/URL-string fetch transparently retries the
|
|
145
|
+
* cold-start "instance is provisioning" transients (cloudflare/containers#45,
|
|
146
|
+
* #139); pass {@link InstanceRetryOptions} to tune or disable it.
|
|
84
147
|
*/
|
|
85
|
-
any: (count?: number) => ContainerHandle;
|
|
86
|
-
/**
|
|
87
|
-
|
|
148
|
+
any: (count?: number, options?: InstanceRetryOptions) => ContainerHandle;
|
|
149
|
+
/**
|
|
150
|
+
* The instance for `name` — one container per entity (user, room, job…),
|
|
151
|
+
* with lifecycle control.
|
|
152
|
+
*
|
|
153
|
+
* A path/URL-string fetch transparently retries the platform's cold-start
|
|
154
|
+
* transients — "there is no Container instance available" / "container is
|
|
155
|
+
* not listening" while an instance is still provisioning
|
|
156
|
+
* (cloudflare/containers#45, #139) — on the *same* instance with backoff,
|
|
157
|
+
* since the request never reached the app. Pass {@link InstanceRetryOptions}
|
|
158
|
+
* to tune attempts/backoff or disable it (`{ attempts: 1 }`). A pre-built
|
|
159
|
+
* `Request` (possibly a one-shot stream body) is sent once, never retried.
|
|
160
|
+
*/
|
|
161
|
+
get: (name: string, options?: InstanceRetryOptions) => ContainerInstanceHandle;
|
|
88
162
|
/**
|
|
89
163
|
* A resilient handle over the pool: each `fetch` picks a random instance and,
|
|
90
164
|
* on a thrown error or a retryable response (5xx by default), retries on a
|
|
@@ -121,6 +195,24 @@ interface PoolOptions {
|
|
|
121
195
|
/** Pool size to spread picks across. Defaults to the definition's `maxInstances`, else 3. */
|
|
122
196
|
size?: number;
|
|
123
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* Tuning for the cold-start retry on a `.get()`/`.any()` handle. The retry fires
|
|
200
|
+
* only on the platform's provisioning transients (no-instance / not-listening /
|
|
201
|
+
* rate-limited — see {@link isColdStartTransient}), which is why it's safe by
|
|
202
|
+
* default: those responses mean the request never reached the container.
|
|
203
|
+
*/
|
|
204
|
+
interface InstanceRetryOptions {
|
|
205
|
+
/**
|
|
206
|
+
* Total attempts on a cold-start transient before the last outcome is
|
|
207
|
+
* surfaced as-is. `1` disables the retry. Default
|
|
208
|
+
* {@link DEFAULT_COLD_START_ATTEMPTS}.
|
|
209
|
+
*/
|
|
210
|
+
attempts?: number;
|
|
211
|
+
/** Base backoff in ms between attempts; doubles each retry (0 disables the wait). Default {@link DEFAULT_COLD_START_BACKOFF_MS}. */
|
|
212
|
+
backoffMs?: number;
|
|
213
|
+
/** Upper bound on a single backoff sleep, in ms. Default {@link DEFAULT_MAX_BACKOFF_MS} (30s). */
|
|
214
|
+
maxBackoffMs?: number;
|
|
215
|
+
}
|
|
124
216
|
/** Wiring info for one definition, emitted by codegen into the generated DO. */
|
|
125
217
|
interface ContainerBindingSpec {
|
|
126
218
|
/** Durable Object binding name, e.g. `CONTAINER_TRANSCODER`. */
|
|
@@ -197,4 +289,4 @@ declare const isContainerDefinition: (value: unknown) => value is ContainerDefin
|
|
|
197
289
|
* credential it was promised yields far worse errors downstream.
|
|
198
290
|
*/
|
|
199
291
|
declare const resolveContainerEnvVariables: (definition: ContainerDefinition, workerEnv: Record<string, unknown>, exportName?: string) => Record<string, string>;
|
|
200
|
-
export { type ContainerAccessor, type ContainerBindingSpec, type ContainerConfig, type ContainerDefinition, type ContainerHandle, type ContainerImageSource, type ContainerInstanceHandle, type ContainerInstanceState, type ContainerNamespaceLike, type ContainerStartOptions, type ContainerTestHandler, type DurableObjectJurisdiction, type NormalizedContainerImage, type PoolOptions, containerBindingName, containerBuildTag, containerClassName, createContainerContext, createContainerTestContext, defineContainer, isContainerDefinition, normalizeContainerImage, resolveContainerEnvVariables as resolveContainerEnvVars };
|
|
292
|
+
export { type ContainerAccessor, type ContainerBindingSpec, type ContainerConfig, type ContainerDefinition, type ContainerEgressControls, type ContainerHandle, type ContainerImageSource, type ContainerInstanceHandle, type ContainerInstanceState, type ContainerNamespaceLike, type ContainerStartOptions, type ContainerTestHandler, type DurableObjectJurisdiction, type InstanceRetryOptions, type NormalizedContainerImage, type PoolOptions, containerBindingName, containerBuildTag, containerClassName, createContainerContext, createContainerTestContext, defineContainer, isContainerDefinition, normalizeContainerImage, resolveContainerEnvVariables as resolveContainerEnvVars };
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { createContainerContext, createContainerTestContext } from './packem_shared/createContainerContext-
|
|
2
|
-
export { containerBindingName, containerBuildTag, containerClassName, defineContainer, isContainerDefinition, normalizeContainerImage, resolveContainerEnvVars } from './packem_shared/containerBindingName-
|
|
1
|
+
export { createContainerContext, createContainerTestContext } from './packem_shared/createContainerContext-BtzK5gJL.mjs';
|
|
2
|
+
export { containerBindingName, containerBuildTag, containerClassName, defineContainer, isContainerDefinition, normalizeContainerImage, resolveContainerEnvVars } from './packem_shared/containerBindingName-CWmEE_3Y.mjs';
|