alchemy 0.40.0 → 0.41.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/lib/build-date.d.ts +1 -1
- package/lib/build-date.js +1 -1
- package/lib/cloudflare/bindings.d.ts +2 -1
- package/lib/cloudflare/bindings.d.ts.map +1 -1
- package/lib/cloudflare/bindings.js.map +1 -1
- package/lib/cloudflare/bound.d.ts +2 -1
- package/lib/cloudflare/bound.d.ts.map +1 -1
- package/lib/cloudflare/bundle/bundle-worker-dev.d.ts.map +1 -1
- package/lib/cloudflare/bundle/bundle-worker-dev.js +2 -1
- package/lib/cloudflare/bundle/bundle-worker-dev.js.map +1 -1
- package/lib/cloudflare/bundle/bundle-worker.d.ts.map +1 -1
- package/lib/cloudflare/bundle/bundle-worker.js +2 -1
- package/lib/cloudflare/bundle/bundle-worker.js.map +1 -1
- package/lib/cloudflare/container.d.ts +509 -0
- package/lib/cloudflare/container.d.ts.map +1 -0
- package/lib/cloudflare/container.js +274 -0
- package/lib/cloudflare/container.js.map +1 -0
- package/lib/cloudflare/index.d.ts +1 -0
- package/lib/cloudflare/index.d.ts.map +1 -1
- package/lib/cloudflare/index.js +1 -0
- package/lib/cloudflare/index.js.map +1 -1
- package/lib/cloudflare/worker/miniflare-worker-options.d.ts.map +1 -1
- package/lib/cloudflare/worker/miniflare-worker-options.js +28 -0
- package/lib/cloudflare/worker/miniflare-worker-options.js.map +1 -1
- package/lib/cloudflare/worker-metadata.d.ts +3 -0
- package/lib/cloudflare/worker-metadata.d.ts.map +1 -1
- package/lib/cloudflare/worker-metadata.js +22 -3
- package/lib/cloudflare/worker-metadata.js.map +1 -1
- package/lib/cloudflare/worker.d.ts +2 -1
- package/lib/cloudflare/worker.d.ts.map +1 -1
- package/lib/cloudflare/worker.js +39 -3
- package/lib/cloudflare/worker.js.map +1 -1
- package/lib/cloudflare/wrangler.json.js +11 -0
- package/lib/cloudflare/wrangler.json.js.map +1 -1
- package/lib/docker/api.d.ts +15 -0
- package/lib/docker/api.d.ts.map +1 -1
- package/lib/docker/api.js +59 -0
- package/lib/docker/api.js.map +1 -1
- package/lib/docker/image.d.ts +22 -4
- package/lib/docker/image.d.ts.map +1 -1
- package/lib/docker/image.js +68 -24
- package/lib/docker/image.js.map +1 -1
- package/lib/github/index.d.ts +1 -0
- package/lib/github/index.d.ts.map +1 -1
- package/lib/github/index.js +1 -0
- package/lib/github/index.js.map +1 -1
- package/lib/github/repository-webhook.d.ts +123 -0
- package/lib/github/repository-webhook.d.ts.map +1 -0
- package/lib/github/repository-webhook.js +146 -0
- package/lib/github/repository-webhook.js.map +1 -0
- package/lib/scope.d.ts +2 -0
- package/lib/scope.d.ts.map +1 -1
- package/lib/scope.js +4 -0
- package/lib/scope.js.map +1 -1
- package/package.json +3 -2
- package/src/build-date.ts +1 -1
- package/src/cloudflare/bindings.ts +2 -0
- package/src/cloudflare/bound.ts +9 -1
- package/src/cloudflare/bundle/bundle-worker-dev.ts +2 -1
- package/src/cloudflare/bundle/bundle-worker.ts +2 -1
- package/src/cloudflare/container.ts +843 -0
- package/src/cloudflare/index.ts +1 -0
- package/src/cloudflare/worker/miniflare-worker-options.ts +28 -0
- package/src/cloudflare/worker-metadata.ts +28 -6
- package/src/cloudflare/worker.ts +67 -16
- package/src/cloudflare/wrangler.json.ts +12 -0
- package/src/docker/api.ts +74 -0
- package/src/docker/image.ts +99 -29
- package/src/github/index.ts +1 -0
- package/src/github/repository-webhook.ts +269 -0
- package/src/scope.ts +5 -0
|
@@ -0,0 +1,843 @@
|
|
|
1
|
+
import type { Context } from "../context.ts";
|
|
2
|
+
import { Image, type ImageProps, type ImageRegistry } from "../docker/image.ts";
|
|
3
|
+
import { Resource } from "../resource.ts";
|
|
4
|
+
import { secret } from "../secret.ts";
|
|
5
|
+
import {
|
|
6
|
+
type CloudflareApi,
|
|
7
|
+
type CloudflareApiOptions,
|
|
8
|
+
createCloudflareApi,
|
|
9
|
+
} from "./api.ts";
|
|
10
|
+
|
|
11
|
+
export interface ContainerProps
|
|
12
|
+
extends Omit<ImageProps, "registry" | "skipPush">,
|
|
13
|
+
Partial<CloudflareApiOptions> {
|
|
14
|
+
className: string;
|
|
15
|
+
maxInstances?: number;
|
|
16
|
+
scriptName?: string;
|
|
17
|
+
instanceType?: InstanceType;
|
|
18
|
+
observability?: DeploymentObservability;
|
|
19
|
+
schedulingPolicy?: SchedulingPolicy;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @see https://developers.cloudflare.com/containers/pricing/
|
|
24
|
+
*/
|
|
25
|
+
export type InstanceType = "dev" | "basic" | "standard" | (string & {});
|
|
26
|
+
|
|
27
|
+
export function isContainer(binding: any): binding is Container {
|
|
28
|
+
return binding.type === "container";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type Container<T = any> = {
|
|
32
|
+
type: "container";
|
|
33
|
+
id: string;
|
|
34
|
+
name?: string;
|
|
35
|
+
className: string;
|
|
36
|
+
image: Image;
|
|
37
|
+
maxInstances?: number;
|
|
38
|
+
scriptName?: string;
|
|
39
|
+
sqlite?: true;
|
|
40
|
+
instanceType?: InstanceType;
|
|
41
|
+
observability?: DeploymentObservability;
|
|
42
|
+
schedulingPolicy?: SchedulingPolicy;
|
|
43
|
+
/**
|
|
44
|
+
* @internal
|
|
45
|
+
*/
|
|
46
|
+
__phantom?: T;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export async function Container<T>(
|
|
50
|
+
id: string,
|
|
51
|
+
props: ContainerProps,
|
|
52
|
+
): Promise<Container<T>> {
|
|
53
|
+
// Otherwise, obtain Cloudflare registry credentials automatically
|
|
54
|
+
const api = await createCloudflareApi(props);
|
|
55
|
+
const creds = await getContainerCredentials(api);
|
|
56
|
+
|
|
57
|
+
const registry: ImageRegistry = {
|
|
58
|
+
server: "registry.cloudflare.com",
|
|
59
|
+
username: creds.username || creds.user!,
|
|
60
|
+
password: secret(creds.password),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Ensure repository name is namespaced with accountId
|
|
64
|
+
const repoBase = props.name ?? id;
|
|
65
|
+
const repoName = repoBase.includes("/")
|
|
66
|
+
? repoBase
|
|
67
|
+
: `${api.accountId}/${repoBase}`;
|
|
68
|
+
|
|
69
|
+
// Replace disallowed "latest" tag with timestamp
|
|
70
|
+
const finalTag =
|
|
71
|
+
props.tag === undefined || props.tag === "latest"
|
|
72
|
+
? `latest-${Date.now()}`
|
|
73
|
+
: props.tag;
|
|
74
|
+
|
|
75
|
+
const image = await Image(id, {
|
|
76
|
+
build: props.build,
|
|
77
|
+
name: repoName,
|
|
78
|
+
tag: finalTag,
|
|
79
|
+
skipPush: false,
|
|
80
|
+
registry,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
type: "container",
|
|
85
|
+
id,
|
|
86
|
+
name: props.name ?? id,
|
|
87
|
+
className: props.className,
|
|
88
|
+
image,
|
|
89
|
+
maxInstances: props.maxInstances,
|
|
90
|
+
scriptName: props.scriptName,
|
|
91
|
+
instanceType: props.instanceType,
|
|
92
|
+
observability: props.observability,
|
|
93
|
+
schedulingPolicy: props.schedulingPolicy,
|
|
94
|
+
sqlite: true,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface ContainerApplicationRollout {
|
|
99
|
+
strategy: "rolling";
|
|
100
|
+
kind?: "full_auto";
|
|
101
|
+
stepPercentage: number;
|
|
102
|
+
targetConfiguration: {
|
|
103
|
+
image: string;
|
|
104
|
+
instance_type?: InstanceType;
|
|
105
|
+
observability: {
|
|
106
|
+
logs: {
|
|
107
|
+
enabled: boolean;
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface ContainerApplicationProps extends CloudflareApiOptions {
|
|
114
|
+
name: string;
|
|
115
|
+
schedulingPolicy?: SchedulingPolicy;
|
|
116
|
+
instances?: number;
|
|
117
|
+
/**
|
|
118
|
+
* The instance type to be used for the deployment.
|
|
119
|
+
*
|
|
120
|
+
* @default "dev"
|
|
121
|
+
*/
|
|
122
|
+
instanceType?: InstanceType;
|
|
123
|
+
/**
|
|
124
|
+
* The observability configuration for the deployment.
|
|
125
|
+
*/
|
|
126
|
+
observability?: DeploymentObservability;
|
|
127
|
+
/**
|
|
128
|
+
* The maximum number of instances to be used for the deployment.
|
|
129
|
+
*/
|
|
130
|
+
maxInstances?: number;
|
|
131
|
+
image: Image;
|
|
132
|
+
registryId?: string;
|
|
133
|
+
durableObjects?: {
|
|
134
|
+
namespaceId: string;
|
|
135
|
+
};
|
|
136
|
+
rollout?: ContainerApplicationRollout;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export type SchedulingPolicy =
|
|
140
|
+
| "moon"
|
|
141
|
+
| "gpu"
|
|
142
|
+
| "regional"
|
|
143
|
+
| "fill_metals"
|
|
144
|
+
| "default";
|
|
145
|
+
|
|
146
|
+
export interface ContainerApplication
|
|
147
|
+
extends Resource<"cloudflare::ContainerApplication"> {
|
|
148
|
+
id: string;
|
|
149
|
+
name: string;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Deploy and manage container applications on Cloudflare's global network.
|
|
154
|
+
*
|
|
155
|
+
* ContainerApplication creates a managed container deployment that runs your Docker images
|
|
156
|
+
* with automatic scaling, scheduling, and integration with Cloudflare's services.
|
|
157
|
+
*
|
|
158
|
+
* @example
|
|
159
|
+
* // Deploy a simple web application container
|
|
160
|
+
* const webApp = await ContainerApplication("my-web-app", {
|
|
161
|
+
* name: "my-web-app",
|
|
162
|
+
* image: await Image("web-app", {
|
|
163
|
+
* name: "web-app",
|
|
164
|
+
* build: {
|
|
165
|
+
* context: "./docker/web-app"
|
|
166
|
+
* }
|
|
167
|
+
* }),
|
|
168
|
+
* instances: 1,
|
|
169
|
+
* maxInstances: 3
|
|
170
|
+
* });
|
|
171
|
+
*
|
|
172
|
+
* @example
|
|
173
|
+
* // Deploy a container with GPU support for AI workloads
|
|
174
|
+
* const aiApp = await ContainerApplication("ai-inference", {
|
|
175
|
+
* name: "ai-inference",
|
|
176
|
+
* image: await Image("ai-model", {
|
|
177
|
+
* name: "ai-model",
|
|
178
|
+
* build: {
|
|
179
|
+
* context: "./docker/ai"
|
|
180
|
+
* }
|
|
181
|
+
* }),
|
|
182
|
+
* schedulingPolicy: "gpu",
|
|
183
|
+
* instances: 2,
|
|
184
|
+
* maxInstances: 5
|
|
185
|
+
* });
|
|
186
|
+
*
|
|
187
|
+
* @example
|
|
188
|
+
* // Deploy a container integrated with Durable Objects
|
|
189
|
+
* const doApp = await ContainerApplication("stateful-app", {
|
|
190
|
+
* name: "stateful-app",
|
|
191
|
+
* image: await Image("do-app", {
|
|
192
|
+
* name: "do-app",
|
|
193
|
+
* build: {
|
|
194
|
+
* context: "./container"
|
|
195
|
+
* }
|
|
196
|
+
* }),
|
|
197
|
+
* durableObjects: {
|
|
198
|
+
* namespaceId: myDurableObjectNamespace.id
|
|
199
|
+
* },
|
|
200
|
+
* instances: 1,
|
|
201
|
+
* maxInstances: 10
|
|
202
|
+
* });
|
|
203
|
+
*
|
|
204
|
+
* @example
|
|
205
|
+
* // Create a Container binding for use in a Worker
|
|
206
|
+
* const worker = await Worker("my-worker", {
|
|
207
|
+
* name: "my-worker",
|
|
208
|
+
* entrypoint: "./src/worker.ts",
|
|
209
|
+
* bindings: {
|
|
210
|
+
* MY_CONTAINER: new Container("my-container", {
|
|
211
|
+
* className: "MyContainerClass",
|
|
212
|
+
* image: await Image("container-do", {
|
|
213
|
+
* name: "container-do",
|
|
214
|
+
* context: "./docker/durable-object"
|
|
215
|
+
* }),
|
|
216
|
+
* maxInstances: 100,
|
|
217
|
+
* name: "my-container-do"
|
|
218
|
+
* })
|
|
219
|
+
* }
|
|
220
|
+
* });
|
|
221
|
+
*/
|
|
222
|
+
export const ContainerApplication = Resource(
|
|
223
|
+
"cloudflare::ContainerApplication",
|
|
224
|
+
async function (
|
|
225
|
+
this: Context<ContainerApplication, ContainerApplicationProps>,
|
|
226
|
+
_id: string,
|
|
227
|
+
props: ContainerApplicationProps,
|
|
228
|
+
): Promise<ContainerApplication> {
|
|
229
|
+
const api = await createCloudflareApi(props);
|
|
230
|
+
if (this.phase === "delete") {
|
|
231
|
+
if (this.output?.id) {
|
|
232
|
+
// Delete the container application
|
|
233
|
+
await deleteContainerApplication(api, this.output.id);
|
|
234
|
+
}
|
|
235
|
+
return this.destroy();
|
|
236
|
+
}
|
|
237
|
+
// Prefer the immutable repo digest if present. Falls back to the tag reference.
|
|
238
|
+
const imageReference = props.image.repoDigest ?? props.image.imageRef;
|
|
239
|
+
|
|
240
|
+
const configuration = {
|
|
241
|
+
image: imageReference,
|
|
242
|
+
instance_type: props.instanceType ?? "dev",
|
|
243
|
+
observability: {
|
|
244
|
+
logs: {
|
|
245
|
+
enabled: true,
|
|
246
|
+
},
|
|
247
|
+
logging: {
|
|
248
|
+
enabled: true,
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
if (this.phase === "update") {
|
|
253
|
+
const application = await updateContainerApplication(
|
|
254
|
+
api,
|
|
255
|
+
this.output.id,
|
|
256
|
+
{
|
|
257
|
+
instances: props.instances ?? 1,
|
|
258
|
+
max_instances: props.maxInstances ?? 10,
|
|
259
|
+
scheduling_policy: props.schedulingPolicy ?? "default",
|
|
260
|
+
configuration,
|
|
261
|
+
},
|
|
262
|
+
);
|
|
263
|
+
// TODO(sam): should we wait for the rollout to complete?
|
|
264
|
+
await createContainerApplicationRollout(api, application.id, {
|
|
265
|
+
description: "Progressive update",
|
|
266
|
+
strategy: "rolling",
|
|
267
|
+
kind: props.rollout?.kind ?? "full_auto",
|
|
268
|
+
step_percentage: props.rollout?.stepPercentage ?? 25,
|
|
269
|
+
target_configuration: configuration,
|
|
270
|
+
});
|
|
271
|
+
return this({
|
|
272
|
+
id: application.id,
|
|
273
|
+
name: application.name,
|
|
274
|
+
});
|
|
275
|
+
} else {
|
|
276
|
+
const application = await createContainerApplication(api, {
|
|
277
|
+
name: props.name,
|
|
278
|
+
scheduling_policy: props.schedulingPolicy ?? "default",
|
|
279
|
+
instances: props.instances ?? 1,
|
|
280
|
+
max_instances: props.maxInstances ?? 1,
|
|
281
|
+
durable_objects: props.durableObjects
|
|
282
|
+
? {
|
|
283
|
+
namespace_id: props.durableObjects.namespaceId,
|
|
284
|
+
}
|
|
285
|
+
: undefined,
|
|
286
|
+
constraints: {
|
|
287
|
+
tier: 1,
|
|
288
|
+
},
|
|
289
|
+
configuration: {
|
|
290
|
+
image: imageReference,
|
|
291
|
+
instance_type: props.instanceType ?? "dev",
|
|
292
|
+
observability: {
|
|
293
|
+
logs: {
|
|
294
|
+
enabled: true,
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
return this({
|
|
301
|
+
id: application.id,
|
|
302
|
+
name: application.name,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
export interface ContainerApplicationData {
|
|
309
|
+
name: string;
|
|
310
|
+
scheduling_policy: string;
|
|
311
|
+
instances: number;
|
|
312
|
+
max_instances: number;
|
|
313
|
+
constraints: {
|
|
314
|
+
tier: number;
|
|
315
|
+
[key: string]: any;
|
|
316
|
+
};
|
|
317
|
+
configuration: {
|
|
318
|
+
image: string;
|
|
319
|
+
location: string;
|
|
320
|
+
vcpu: number;
|
|
321
|
+
memory_mib: number;
|
|
322
|
+
disk: any;
|
|
323
|
+
network: any;
|
|
324
|
+
command: string[];
|
|
325
|
+
entrypoint: string[];
|
|
326
|
+
runtime: string;
|
|
327
|
+
deployment_type: string;
|
|
328
|
+
observability: any;
|
|
329
|
+
memory: string;
|
|
330
|
+
[key: string]: any;
|
|
331
|
+
};
|
|
332
|
+
durable_objects: {
|
|
333
|
+
namespace_id: string;
|
|
334
|
+
[key: string]: any;
|
|
335
|
+
};
|
|
336
|
+
id: string;
|
|
337
|
+
account_id: string;
|
|
338
|
+
created_at: string;
|
|
339
|
+
version: number;
|
|
340
|
+
durable_object_namespace_id: string;
|
|
341
|
+
health: {
|
|
342
|
+
instances: any;
|
|
343
|
+
[key: string]: any;
|
|
344
|
+
};
|
|
345
|
+
[key: string]: any;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export async function listContainerApplications(
|
|
349
|
+
api: CloudflareApi,
|
|
350
|
+
): Promise<ContainerApplicationData[]> {
|
|
351
|
+
const deployments = await api.get(
|
|
352
|
+
`/accounts/${api.accountId}/containers/applications`,
|
|
353
|
+
);
|
|
354
|
+
const response = (await deployments.json()) as any as {
|
|
355
|
+
result: ContainerApplicationData[];
|
|
356
|
+
errors: { message: string }[];
|
|
357
|
+
};
|
|
358
|
+
if (deployments.ok) {
|
|
359
|
+
return response.result;
|
|
360
|
+
}
|
|
361
|
+
throw Error(
|
|
362
|
+
`Failed to list container applications: ${response.errors.map((e) => e.message).join(", ")}`,
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export interface CreateContainerApplicationBody {
|
|
367
|
+
name: string;
|
|
368
|
+
max_instances: number;
|
|
369
|
+
configuration: DeploymentConfiguration;
|
|
370
|
+
durable_objects?: {
|
|
371
|
+
namespace_id: string;
|
|
372
|
+
};
|
|
373
|
+
instances?: number;
|
|
374
|
+
scheduling_policy?: string;
|
|
375
|
+
constraints?: { tier: number };
|
|
376
|
+
[key: string]: any;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export async function createContainerApplication(
|
|
380
|
+
api: CloudflareApi,
|
|
381
|
+
body: CreateContainerApplicationBody,
|
|
382
|
+
) {
|
|
383
|
+
const response = await api.post(
|
|
384
|
+
`/accounts/${api.accountId}/containers/applications`,
|
|
385
|
+
body,
|
|
386
|
+
);
|
|
387
|
+
const result = (await response.json()) as {
|
|
388
|
+
result: ContainerApplicationData;
|
|
389
|
+
errors: { message: string }[];
|
|
390
|
+
};
|
|
391
|
+
if (response.ok) {
|
|
392
|
+
return result.result;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
throw Error(
|
|
396
|
+
`Failed to create container application: ${result.errors?.map((e: { message: string }) => e.message).join(", ") ?? "Unknown error"}`,
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
type Region =
|
|
401
|
+
| "AFR"
|
|
402
|
+
| "APAC"
|
|
403
|
+
| "EEUR"
|
|
404
|
+
| "ENAM"
|
|
405
|
+
| "WNAM"
|
|
406
|
+
| "ME"
|
|
407
|
+
| "OC"
|
|
408
|
+
| "SAM"
|
|
409
|
+
| "WEUR"
|
|
410
|
+
| (string & {});
|
|
411
|
+
|
|
412
|
+
type City =
|
|
413
|
+
| "AFR"
|
|
414
|
+
| "APAC"
|
|
415
|
+
| "EEUR"
|
|
416
|
+
| "ENAM"
|
|
417
|
+
| "WNAM"
|
|
418
|
+
| "ME"
|
|
419
|
+
| "OC"
|
|
420
|
+
| "SAM"
|
|
421
|
+
| "WEUR"
|
|
422
|
+
| (string & {});
|
|
423
|
+
|
|
424
|
+
export type UpdateApplicationRequestBody = {
|
|
425
|
+
/**
|
|
426
|
+
* Number of deployments to maintain within this applicaiton. This can be used to scale the appliation up/down.
|
|
427
|
+
*/
|
|
428
|
+
instances?: number;
|
|
429
|
+
max_instances?: number;
|
|
430
|
+
affinities?: {
|
|
431
|
+
colocation?: "datacenter";
|
|
432
|
+
};
|
|
433
|
+
scheduling_policy?: SchedulingPolicy;
|
|
434
|
+
constraints?: {
|
|
435
|
+
region?: Region;
|
|
436
|
+
tier?: number;
|
|
437
|
+
regions?: Array<Region>;
|
|
438
|
+
cities?: Array<City>;
|
|
439
|
+
};
|
|
440
|
+
/**
|
|
441
|
+
* The deployment configuration of all deployments created by this application.
|
|
442
|
+
* Right now, if you modify the application configuration, only new deployments
|
|
443
|
+
* created will have the new configuration. You can delete old deployments to
|
|
444
|
+
* release new instances.
|
|
445
|
+
*
|
|
446
|
+
* TODO(sam): should this trigger a replacement?
|
|
447
|
+
*/
|
|
448
|
+
configuration?: DeploymentConfiguration;
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
export async function updateContainerApplication(
|
|
452
|
+
api: CloudflareApi,
|
|
453
|
+
applicationId: string,
|
|
454
|
+
body: UpdateApplicationRequestBody,
|
|
455
|
+
) {
|
|
456
|
+
const response = await api.patch(
|
|
457
|
+
`/accounts/${api.accountId}/containers/applications/${applicationId}`,
|
|
458
|
+
body,
|
|
459
|
+
);
|
|
460
|
+
const result = (await response.json()) as {
|
|
461
|
+
result: ContainerApplicationData;
|
|
462
|
+
errors: { message: string }[];
|
|
463
|
+
};
|
|
464
|
+
if (response.ok) {
|
|
465
|
+
return result.result;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
throw Error(
|
|
469
|
+
`Failed to create container application: ${result.errors?.map((e: { message: string }) => e.message).join(", ") ?? "Unknown error"}`,
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
export async function deleteContainerApplication(
|
|
474
|
+
api: CloudflareApi,
|
|
475
|
+
applicationId: string,
|
|
476
|
+
) {
|
|
477
|
+
const response = await api.delete(
|
|
478
|
+
`/accounts/${api.accountId}/containers/applications/${applicationId}`,
|
|
479
|
+
);
|
|
480
|
+
const result = (await response.json()) as any;
|
|
481
|
+
if (response.ok) {
|
|
482
|
+
return result.result;
|
|
483
|
+
}
|
|
484
|
+
throw Error(
|
|
485
|
+
`Failed to delete container application: ${result.errors?.map((e: { message: string }) => e.message).join(", ") ?? "Unknown error"}`,
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
interface CreateRolloutApplicationRequest {
|
|
490
|
+
description: string;
|
|
491
|
+
strategy: "rolling";
|
|
492
|
+
kind?: "full_auto";
|
|
493
|
+
step_percentage: number;
|
|
494
|
+
target_configuration: DeploymentConfiguration;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
interface CreateRolloutApplicationResponse {
|
|
498
|
+
id: string;
|
|
499
|
+
created_at: string;
|
|
500
|
+
last_updated_at: string;
|
|
501
|
+
description: string;
|
|
502
|
+
status: "progressing" | "completed" | "failed" | (string & {});
|
|
503
|
+
health: {
|
|
504
|
+
instances: {
|
|
505
|
+
healthy: number;
|
|
506
|
+
failed: number;
|
|
507
|
+
starting: number;
|
|
508
|
+
scheduling: number;
|
|
509
|
+
};
|
|
510
|
+
};
|
|
511
|
+
kind: "full_auto" | (string & {});
|
|
512
|
+
strategy: "rolling" | (string & {});
|
|
513
|
+
current_configuration: {
|
|
514
|
+
image: string;
|
|
515
|
+
observability?: {
|
|
516
|
+
logs?: {
|
|
517
|
+
enabled: boolean;
|
|
518
|
+
};
|
|
519
|
+
logging?: {
|
|
520
|
+
enabled: boolean;
|
|
521
|
+
};
|
|
522
|
+
};
|
|
523
|
+
};
|
|
524
|
+
target_configuration: DeploymentConfiguration;
|
|
525
|
+
current_version: number;
|
|
526
|
+
target_version: number;
|
|
527
|
+
steps: Array<{
|
|
528
|
+
id: number;
|
|
529
|
+
status: "progressing" | "pending" | "completed" | "failed" | (string & {});
|
|
530
|
+
step_size: {
|
|
531
|
+
percentage: number;
|
|
532
|
+
};
|
|
533
|
+
description: string;
|
|
534
|
+
started_at?: string;
|
|
535
|
+
}>;
|
|
536
|
+
progress: {
|
|
537
|
+
total_steps: number;
|
|
538
|
+
current_step: number;
|
|
539
|
+
updated_instances: number;
|
|
540
|
+
total_instances: number;
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
export async function createContainerApplicationRollout(
|
|
545
|
+
api: CloudflareApi,
|
|
546
|
+
applicationId: string,
|
|
547
|
+
body: CreateRolloutApplicationRequest,
|
|
548
|
+
) {
|
|
549
|
+
const response = await api.post(
|
|
550
|
+
`/accounts/${api.accountId}/containers/applications/${applicationId}/rollouts`,
|
|
551
|
+
body,
|
|
552
|
+
);
|
|
553
|
+
const result = (await response.json()) as {
|
|
554
|
+
result: CreateRolloutApplicationResponse;
|
|
555
|
+
errors: { message: string }[];
|
|
556
|
+
};
|
|
557
|
+
if (response.ok) {
|
|
558
|
+
return result.result;
|
|
559
|
+
}
|
|
560
|
+
throw Error(
|
|
561
|
+
`Failed to create container application rollout: ${result.errors.map((e: { message: string }) => e.message).join(", ")}`,
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
export type ImageRegistryCredentialsConfiguration = {
|
|
566
|
+
permissions: Array<"pull" | "push">;
|
|
567
|
+
expiration_minutes: number;
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
export async function getContainerCredentials(
|
|
571
|
+
api: CloudflareApi,
|
|
572
|
+
registryId = "registry.cloudflare.com",
|
|
573
|
+
) {
|
|
574
|
+
const credentials = await api.post(
|
|
575
|
+
`/accounts/${api.accountId}/containers/registries/${registryId}/credentials`,
|
|
576
|
+
{
|
|
577
|
+
permissions: ["pull", "push"],
|
|
578
|
+
expiration_minutes: 60,
|
|
579
|
+
} satisfies ImageRegistryCredentialsConfiguration,
|
|
580
|
+
);
|
|
581
|
+
const result = (await credentials.json()) as {
|
|
582
|
+
result: {
|
|
583
|
+
user?: string;
|
|
584
|
+
username?: string;
|
|
585
|
+
password: string;
|
|
586
|
+
};
|
|
587
|
+
errors: { message: string }[];
|
|
588
|
+
};
|
|
589
|
+
if (credentials.ok) {
|
|
590
|
+
return result.result;
|
|
591
|
+
}
|
|
592
|
+
throw Error(
|
|
593
|
+
`Failed to get container credentials: ${result.errors.map((e: { message: string }) => e.message).join(", ")}`,
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// The Cloudflare managed registry is special in that the namespaces for repos should always
|
|
598
|
+
// start with the Cloudflare Account tag
|
|
599
|
+
// This is a helper to generate the image tag with correct namespace attached to the Cloudflare Registry host
|
|
600
|
+
export const getCloudflareRegistryWithAccountNamespace = (
|
|
601
|
+
accountID: string,
|
|
602
|
+
tag: string,
|
|
603
|
+
): string => {
|
|
604
|
+
return `${getCloudflareContainerRegistry()}/${accountID}/${tag}`;
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
// default cloudflare managed registry, can be overriden with the env var - CLOUDFLARE_CONTAINER_REGISTRY
|
|
608
|
+
export const getCloudflareContainerRegistry = () => {
|
|
609
|
+
// previously defaulted to registry.cloudchamber.cfdata.org
|
|
610
|
+
return process.env.CLOUDFLARE_CONTAINER_REGISTRY ?? "registry.cloudflare.com";
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Given a container image that is a registry link, this function
|
|
615
|
+
* returns true if the link points the Cloudflare container registry
|
|
616
|
+
* (defined as per `getCloudflareContainerRegistry` above)
|
|
617
|
+
*/
|
|
618
|
+
export function isCloudflareRegistryLink(image: string) {
|
|
619
|
+
const cfRegistry = getCloudflareContainerRegistry();
|
|
620
|
+
return image.includes(cfRegistry);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/** Prefixes with the cloudflare-dev namespace. The name should be the container's DO classname, and the tag a build uuid. */
|
|
624
|
+
export const getDevContainerImageName = (name: string, tag: string) => {
|
|
625
|
+
return `${MF_DEV_CONTAINER_PREFIX}/${name.toLowerCase()}:${tag}`;
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
export const MF_DEV_CONTAINER_PREFIX = "cloudflare-dev";
|
|
629
|
+
|
|
630
|
+
export interface ContainerIdentity {
|
|
631
|
+
account_id: string;
|
|
632
|
+
external_account_id: string;
|
|
633
|
+
legacy_identity: string;
|
|
634
|
+
capabilities: string[];
|
|
635
|
+
limits: {
|
|
636
|
+
account_id: string;
|
|
637
|
+
vcpu_per_deployment: number;
|
|
638
|
+
memory_mib_per_deployment: number;
|
|
639
|
+
memory_per_deployment: string;
|
|
640
|
+
disk_per_deployment: string;
|
|
641
|
+
disk_mb_per_deployment: number;
|
|
642
|
+
total_vcpu: number;
|
|
643
|
+
total_memory_mib: number;
|
|
644
|
+
node_group: string;
|
|
645
|
+
ipv4s: number;
|
|
646
|
+
network_modes: string[];
|
|
647
|
+
total_disk_mb: number;
|
|
648
|
+
total_memory: string;
|
|
649
|
+
};
|
|
650
|
+
locations: any[];
|
|
651
|
+
defaults: {
|
|
652
|
+
vcpus: number;
|
|
653
|
+
memory_mib: number;
|
|
654
|
+
memory: string;
|
|
655
|
+
disk_mb: number;
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
export async function getContainerIdentity(api: CloudflareApi) {
|
|
660
|
+
const metrics = await api.get(`/accounts/${api.accountId}/containers/me`);
|
|
661
|
+
const result = (await metrics.json()) as {
|
|
662
|
+
result: ContainerIdentity;
|
|
663
|
+
errors: { message: string }[];
|
|
664
|
+
};
|
|
665
|
+
if (metrics.ok) {
|
|
666
|
+
return result.result;
|
|
667
|
+
}
|
|
668
|
+
throw Error(
|
|
669
|
+
`Failed to get container me: ${result.errors.map((e: { message: string }) => e.message).join(", ")}`,
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Duration string. From Go documentation:
|
|
675
|
+
* A string representing the duration in the form "3d1h3m". Leading zero units are omitted.
|
|
676
|
+
* As a special case, durations less than one second format use a smaller unit (milli-, micro-, or nanoseconds)
|
|
677
|
+
* to ensure that the leading digit is non-zero.
|
|
678
|
+
*/
|
|
679
|
+
export type Duration = string;
|
|
680
|
+
|
|
681
|
+
interface DeploymentObservability {
|
|
682
|
+
logs?: {
|
|
683
|
+
enabled: boolean;
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
export type DeploymentConfiguration = {
|
|
688
|
+
/**
|
|
689
|
+
* The image to be used for the deployment.
|
|
690
|
+
*/
|
|
691
|
+
image: string;
|
|
692
|
+
/**
|
|
693
|
+
* The instance type to be used for the deployment.
|
|
694
|
+
*/
|
|
695
|
+
instance_type?: InstanceType;
|
|
696
|
+
/**
|
|
697
|
+
* The observability configuration for the deployment.
|
|
698
|
+
*/
|
|
699
|
+
observability?: DeploymentObservability;
|
|
700
|
+
/**
|
|
701
|
+
* A list of SSH public key IDs from the account
|
|
702
|
+
*/
|
|
703
|
+
ssh_public_key_ids?: Array<string>;
|
|
704
|
+
/**
|
|
705
|
+
* A list of objects with secret names and the their access types from the account
|
|
706
|
+
*/
|
|
707
|
+
secrets?: Array<{
|
|
708
|
+
/**
|
|
709
|
+
* The name of the secret within the container
|
|
710
|
+
*/
|
|
711
|
+
name: string;
|
|
712
|
+
type: "env";
|
|
713
|
+
/**
|
|
714
|
+
* Corresponding secret name from the account
|
|
715
|
+
*/
|
|
716
|
+
secret: string;
|
|
717
|
+
}>;
|
|
718
|
+
/**
|
|
719
|
+
* Specify the vcpu to be used for the deployment. The default will be the one configured for the account.
|
|
720
|
+
*/
|
|
721
|
+
vcpu?: number;
|
|
722
|
+
/**
|
|
723
|
+
* Specify the memory to be used for the deployment. The default will be the one configured for the account.
|
|
724
|
+
*/
|
|
725
|
+
memory?: string;
|
|
726
|
+
/**
|
|
727
|
+
* The disk configuration for this deployment
|
|
728
|
+
*/
|
|
729
|
+
disk?: {
|
|
730
|
+
size: string;
|
|
731
|
+
};
|
|
732
|
+
/**
|
|
733
|
+
* Container environment variables
|
|
734
|
+
*/
|
|
735
|
+
environment_variables?: Array<{
|
|
736
|
+
name: string;
|
|
737
|
+
value: string;
|
|
738
|
+
}>;
|
|
739
|
+
/**
|
|
740
|
+
* Deployment labels
|
|
741
|
+
*/
|
|
742
|
+
labels?: Array<{
|
|
743
|
+
name: string;
|
|
744
|
+
value: string;
|
|
745
|
+
}>;
|
|
746
|
+
network?: {
|
|
747
|
+
/**
|
|
748
|
+
* Assign an IPv4 address to the deployment. One of 'none' (default), 'predefined' (allocate one from a set of IPv4 addresses in the global pool), 'account' (allocate one from a set of IPv4 addresses preassigned in the account pool). Only applicable to "public" mode.
|
|
749
|
+
*
|
|
750
|
+
*/
|
|
751
|
+
assign_ipv4?: "none" | "predefined" | "account";
|
|
752
|
+
/**
|
|
753
|
+
* Assign an IPv6 address to the deployment. One of 'predefined' (allocate one from a set of IPv6 addresses in the global pool), 'account' (allocate one from a set of IPv6 addresses preassigned in the account pool). The container will always be assigned to an IPv6 if the networking mode is "public".
|
|
754
|
+
*
|
|
755
|
+
*/
|
|
756
|
+
assign_ipv6?: "none" | "predefined" | "account";
|
|
757
|
+
mode?: "public" | "private";
|
|
758
|
+
};
|
|
759
|
+
command?: string[];
|
|
760
|
+
entrypoint?: string[];
|
|
761
|
+
dns?: {
|
|
762
|
+
/**
|
|
763
|
+
* List of DNS servers that the deployment will use to resolve domain names. You can only specify a maximum of 3.
|
|
764
|
+
*/
|
|
765
|
+
servers?: Array<string>;
|
|
766
|
+
/**
|
|
767
|
+
* The container resolver will append these domains to every resolve query. For example, if you have 'google.com',
|
|
768
|
+
* and your deployment queries 'web', it will append 'google.com' to 'web' in the search query before trying 'web'.
|
|
769
|
+
* Limited to 6 domains.
|
|
770
|
+
*/
|
|
771
|
+
searches?: Array<string>;
|
|
772
|
+
};
|
|
773
|
+
ports?: Array<{
|
|
774
|
+
/**
|
|
775
|
+
* The name of the port. The port name should be unique for each deployment. Minimum length of 1 and maximum length of 15. No consecutive dashes. If the name is 'web-ui', the container will receive an environment variable as follows:
|
|
776
|
+
* - CLOUDFLARE_PORT_WEB_UI: Port inside the container
|
|
777
|
+
* - CLOUDFLARE_HOST_PORT_WEB_UI: Port outside the container
|
|
778
|
+
* - CLOUDFLARE_HOST_IP_WEB_UI: Address of the external network interface the port is allocated on
|
|
779
|
+
* - CLOUDFLARE_HOST_ADDR_WEB_UI: CLOUDFLARE_HOST_ADDR_WEB_UI ':' CLOUDFLARE_HOST_PORT_WEB_UI
|
|
780
|
+
*
|
|
781
|
+
*/
|
|
782
|
+
name: string;
|
|
783
|
+
/**
|
|
784
|
+
* Optional port number, it's assigned only if the user specified it. If it's not specified, the datacenter scheduler will decide it.
|
|
785
|
+
*/
|
|
786
|
+
port?: number;
|
|
787
|
+
}>;
|
|
788
|
+
/**
|
|
789
|
+
* Health and readiness checks for this deployment.
|
|
790
|
+
*/
|
|
791
|
+
checks?: Array<{
|
|
792
|
+
/**
|
|
793
|
+
* Optional name for the check. If omitted, a name will be generated automatically.
|
|
794
|
+
*/
|
|
795
|
+
name?: string;
|
|
796
|
+
/**
|
|
797
|
+
* The type of check to perform. A TCP check succeeds if it can connect to the provided port. An HTTP check succeeds if it receives a successful HTTP response (2XX)
|
|
798
|
+
*/
|
|
799
|
+
type: "http" | "tcp";
|
|
800
|
+
/**
|
|
801
|
+
* Connect to the port using TLS
|
|
802
|
+
*/
|
|
803
|
+
tls?: boolean;
|
|
804
|
+
/**
|
|
805
|
+
* The name of the port defined in the "ports" property of the deployment
|
|
806
|
+
*/
|
|
807
|
+
port: string;
|
|
808
|
+
/**
|
|
809
|
+
* Configuration for HTTP checks. Only valid when "type" is "http"
|
|
810
|
+
*/
|
|
811
|
+
http?: {
|
|
812
|
+
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "OPTIONS" | "HEAD";
|
|
813
|
+
/**
|
|
814
|
+
* If the method is one of POST, PATCH or PUT, this is required. It's the body that will be passed to the HTTP healthcheck request.
|
|
815
|
+
*/
|
|
816
|
+
body?: string;
|
|
817
|
+
/**
|
|
818
|
+
* Path that will be used to perform the healthcheck.
|
|
819
|
+
*/
|
|
820
|
+
path?: string;
|
|
821
|
+
/**
|
|
822
|
+
* HTTP headers to include in the request.
|
|
823
|
+
*/
|
|
824
|
+
headers?: Record<string, any>;
|
|
825
|
+
};
|
|
826
|
+
/**
|
|
827
|
+
* How often the check should be performed
|
|
828
|
+
*/
|
|
829
|
+
interval: Duration;
|
|
830
|
+
/**
|
|
831
|
+
* The amount of time to wait for the check to complete before considering the check to have failed
|
|
832
|
+
*/
|
|
833
|
+
timeout: Duration;
|
|
834
|
+
/**
|
|
835
|
+
* Number of times to attempt the check before considering it to have failed
|
|
836
|
+
*/
|
|
837
|
+
retries?: number;
|
|
838
|
+
/**
|
|
839
|
+
* The kind of check. A failed "healthy" check affects a deployment's "healthy" status, while a failed "ready" check affects a deployment's "ready" status.
|
|
840
|
+
*/
|
|
841
|
+
kind: "health" | "ready";
|
|
842
|
+
}>;
|
|
843
|
+
};
|