alchemy 0.85.0 → 0.85.2
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/bin/alchemy.js +32 -23
- package/lib/cloudflare/account-api-token.d.ts +5 -0
- package/lib/cloudflare/account-api-token.d.ts.map +1 -1
- package/lib/cloudflare/account-api-token.js +13 -2
- package/lib/cloudflare/account-api-token.js.map +1 -1
- package/lib/cloudflare/ai-crawler.d.ts +27 -0
- package/lib/cloudflare/ai-crawler.d.ts.map +1 -0
- package/lib/cloudflare/ai-crawler.js +75 -0
- package/lib/cloudflare/ai-crawler.js.map +1 -0
- package/lib/cloudflare/ai-search-token.d.ts +150 -0
- package/lib/cloudflare/ai-search-token.d.ts.map +1 -0
- package/lib/cloudflare/ai-search-token.js +171 -0
- package/lib/cloudflare/ai-search-token.js.map +1 -0
- package/lib/cloudflare/ai-search.d.ts +397 -0
- package/lib/cloudflare/ai-search.d.ts.map +1 -0
- package/lib/cloudflare/ai-search.js +307 -0
- package/lib/cloudflare/ai-search.js.map +1 -0
- package/lib/cloudflare/auth.d.ts +3 -2
- package/lib/cloudflare/auth.d.ts.map +1 -1
- package/lib/cloudflare/auth.js +17 -14
- package/lib/cloudflare/auth.js.map +1 -1
- package/lib/cloudflare/bucket-custom-domain.d.ts.map +1 -1
- package/lib/cloudflare/bucket-custom-domain.js +4 -2
- package/lib/cloudflare/bucket-custom-domain.js.map +1 -1
- package/lib/cloudflare/bucket.d.ts.map +1 -1
- package/lib/cloudflare/bucket.js +8 -2
- package/lib/cloudflare/bucket.js.map +1 -1
- package/lib/cloudflare/index.d.ts +3 -0
- package/lib/cloudflare/index.d.ts.map +1 -1
- package/lib/cloudflare/index.js +3 -0
- package/lib/cloudflare/index.js.map +1 -1
- package/lib/cloudflare/permission-groups.d.ts +5 -0
- package/lib/cloudflare/permission-groups.d.ts.map +1 -1
- package/lib/cloudflare/permission-groups.js +6 -0
- package/lib/cloudflare/permission-groups.js.map +1 -1
- package/lib/cloudflare/sveltekit/plugin.d.ts.map +1 -1
- package/lib/cloudflare/sveltekit/plugin.js +11 -5
- package/lib/cloudflare/sveltekit/plugin.js.map +1 -1
- package/lib/cloudflare/vite/vite.d.ts.map +1 -1
- package/lib/cloudflare/vite/vite.js +2 -1
- package/lib/cloudflare/vite/vite.js.map +1 -1
- package/lib/docker/api.d.ts +33 -2
- package/lib/docker/api.d.ts.map +1 -1
- package/lib/docker/api.js +8 -4
- package/lib/docker/api.js.map +1 -1
- package/lib/docker/container.d.ts +1 -1
- package/lib/docker/container.d.ts.map +1 -1
- package/lib/docker/container.js +244 -15
- package/lib/docker/container.js.map +1 -1
- package/lib/scope.d.ts +1 -1
- package/lib/scope.d.ts.map +1 -1
- package/lib/scope.js +10 -5
- package/lib/scope.js.map +1 -1
- package/lib/test/bun.d.ts +17 -0
- package/lib/test/bun.d.ts.map +1 -1
- package/lib/test/bun.js +2 -1
- package/lib/test/bun.js.map +1 -1
- package/lib/test/vitest.d.ts +17 -0
- package/lib/test/vitest.d.ts.map +1 -1
- package/lib/test/vitest.js +4 -1
- package/lib/test/vitest.js.map +1 -1
- package/lib/util/snake-to-camel.d.ts +21 -0
- package/lib/util/snake-to-camel.d.ts.map +1 -0
- package/lib/util/snake-to-camel.js +34 -0
- package/lib/util/snake-to-camel.js.map +1 -0
- package/package.json +1 -1
- package/src/cloudflare/account-api-token.ts +23 -6
- package/src/cloudflare/ai-crawler.ts +89 -0
- package/src/cloudflare/ai-search-token.ts +324 -0
- package/src/cloudflare/ai-search.ts +952 -0
- package/src/cloudflare/auth.ts +17 -14
- package/src/cloudflare/bucket-custom-domain.ts +4 -1
- package/src/cloudflare/bucket.ts +9 -2
- package/src/cloudflare/index.ts +3 -0
- package/src/cloudflare/permission-groups.ts +6 -0
- package/src/cloudflare/sveltekit/plugin.ts +17 -8
- package/src/cloudflare/vite/vite.ts +4 -2
- package/src/docker/api.ts +47 -4
- package/src/docker/container.ts +318 -18
- package/src/scope.ts +14 -5
- package/src/test/bun.ts +26 -1
- package/src/test/vitest.ts +34 -1
- package/src/util/snake-to-camel.ts +59 -0
- package/workers/tunnel-proxy.js +1 -1
package/src/cloudflare/auth.ts
CHANGED
|
@@ -75,8 +75,9 @@ export namespace CloudflareAuth {
|
|
|
75
75
|
"queues:write": "See and change Cloudflare Queues settings and data",
|
|
76
76
|
"r2_catalog:write": "Grants write level access to R2 Data Catalog",
|
|
77
77
|
"radar:read": "Grants access to read Cloudflare Radar data",
|
|
78
|
-
"
|
|
79
|
-
"
|
|
78
|
+
"ai-search:read": "Grants read level access to AI Search",
|
|
79
|
+
"ai-search:write": "Grants write level access to AI Search",
|
|
80
|
+
"ai-search:run": "Grants run level access to AI Search",
|
|
80
81
|
"secrets_store:read": "Grants read level access to Secrets Store",
|
|
81
82
|
"secrets_store:write": "Grants write level access to Secrets Store",
|
|
82
83
|
"ssl_certs:write":
|
|
@@ -120,24 +121,26 @@ export namespace CloudflareAuth {
|
|
|
120
121
|
export type Scope = keyof typeof ALL_SCOPES;
|
|
121
122
|
export const DEFAULT_SCOPES = [
|
|
122
123
|
"account:read",
|
|
124
|
+
"ai-search:write",
|
|
125
|
+
"ai-search:run",
|
|
126
|
+
"ai:write",
|
|
127
|
+
"cloudchamber:write",
|
|
128
|
+
"connectivity:admin",
|
|
129
|
+
"containers:write",
|
|
130
|
+
"d1:write",
|
|
131
|
+
"pages:write",
|
|
132
|
+
"pipelines:write",
|
|
133
|
+
"queues:write",
|
|
134
|
+
"secrets_store:write",
|
|
135
|
+
"ssl_certs:write",
|
|
123
136
|
"user:read",
|
|
124
|
-
"
|
|
137
|
+
"vectorize:write",
|
|
125
138
|
"workers_kv:write",
|
|
126
139
|
"workers_routes:write",
|
|
127
140
|
"workers_scripts:write",
|
|
128
141
|
"workers_tail:read",
|
|
129
|
-
"
|
|
130
|
-
"pages:write",
|
|
142
|
+
"workers:write",
|
|
131
143
|
"zone:read",
|
|
132
|
-
"ssl_certs:write",
|
|
133
|
-
"ai:write",
|
|
134
|
-
"queues:write",
|
|
135
|
-
"pipelines:write",
|
|
136
|
-
"secrets_store:write",
|
|
137
|
-
"containers:write",
|
|
138
|
-
"cloudchamber:write",
|
|
139
|
-
"vectorize:write",
|
|
140
|
-
"connectivity:admin",
|
|
141
144
|
];
|
|
142
145
|
|
|
143
146
|
/**
|
|
@@ -100,6 +100,8 @@ export type R2BucketCustomDomain = {
|
|
|
100
100
|
minTLS?: "1.0" | "1.1" | "1.2" | "1.3" | (string & {});
|
|
101
101
|
};
|
|
102
102
|
|
|
103
|
+
const DEV_NOOP_ZONE = "noop-zone";
|
|
104
|
+
|
|
103
105
|
export const R2BucketCustomDomain = Resource(
|
|
104
106
|
"cloudflare::R2BucketCustomDomain",
|
|
105
107
|
async function (
|
|
@@ -114,7 +116,7 @@ export const R2BucketCustomDomain = Resource(
|
|
|
114
116
|
zoneId:
|
|
115
117
|
typeof props.zone === "string"
|
|
116
118
|
? props.zone
|
|
117
|
-
: (props.zone?.id ??
|
|
119
|
+
: (props.zone?.id ?? DEV_NOOP_ZONE),
|
|
118
120
|
ciphers: props.ciphers,
|
|
119
121
|
minTLS: props.minTLS,
|
|
120
122
|
};
|
|
@@ -158,6 +160,7 @@ export const R2BucketCustomDomain = Resource(
|
|
|
158
160
|
}
|
|
159
161
|
case "update": {
|
|
160
162
|
if (
|
|
163
|
+
this.output.zoneId !== DEV_NOOP_ZONE && // dev mode placeholder should not trigger replacement
|
|
161
164
|
diff(this.output, payload).some(
|
|
162
165
|
(property) => property === "domain" || property === "zoneId",
|
|
163
166
|
)
|
package/src/cloudflare/bucket.ts
CHANGED
|
@@ -689,6 +689,13 @@ const _R2Bucket = Resource(
|
|
|
689
689
|
|
|
690
690
|
if (this.phase === "delete") {
|
|
691
691
|
if (props.delete !== false) {
|
|
692
|
+
// Use bucket's actual jurisdiction from output, falling back to props
|
|
693
|
+
// This ensures EU/FedRAMP buckets are correctly addressed during cleanup
|
|
694
|
+
const deleteProps: BucketProps = {
|
|
695
|
+
...props,
|
|
696
|
+
jurisdiction: this.output?.jurisdiction ?? props.jurisdiction,
|
|
697
|
+
};
|
|
698
|
+
|
|
692
699
|
if (this.output?.catalog) {
|
|
693
700
|
await disableDataCatalog(api, bucketName);
|
|
694
701
|
}
|
|
@@ -696,9 +703,9 @@ const _R2Bucket = Resource(
|
|
|
696
703
|
await deleteMiniflareBinding(this.scope, "r2", this.output.dev.id);
|
|
697
704
|
}
|
|
698
705
|
if (props.empty) {
|
|
699
|
-
await emptyBucket(api, bucketName,
|
|
706
|
+
await emptyBucket(api, bucketName, deleteProps);
|
|
700
707
|
}
|
|
701
|
-
await deleteBucket(api, bucketName,
|
|
708
|
+
await deleteBucket(api, bucketName, deleteProps);
|
|
702
709
|
}
|
|
703
710
|
return this.destroy();
|
|
704
711
|
}
|
package/src/cloudflare/index.ts
CHANGED
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
export * from "./account-api-token.ts";
|
|
4
4
|
export * from "./account-id.ts";
|
|
5
|
+
export * from "./ai-crawler.ts";
|
|
5
6
|
export * from "./ai-gateway.ts";
|
|
7
|
+
export * from "./ai-search-token.ts";
|
|
8
|
+
export * from "./ai-search.ts";
|
|
6
9
|
export * from "./ai.ts";
|
|
7
10
|
export * from "./analytics-engine.ts";
|
|
8
11
|
export * from "./api-error.ts";
|
|
@@ -98,6 +98,12 @@ export const PERMISSION_GROUPS = [
|
|
|
98
98
|
description: "Grants access to reading AI Gateways",
|
|
99
99
|
scopes: ["com.cloudflare.api.account"],
|
|
100
100
|
},
|
|
101
|
+
{
|
|
102
|
+
id: "9e9b428a0bcd46fd80e580b46a69963c",
|
|
103
|
+
name: "AI Search Index Engine",
|
|
104
|
+
description: "Grants access to run AI Search Index Engine",
|
|
105
|
+
scopes: ["com.cloudflare.api.account"],
|
|
106
|
+
},
|
|
101
107
|
{
|
|
102
108
|
id: "644535f4ed854494a59cb289d634b257",
|
|
103
109
|
name: "AI Gateway Run",
|
|
@@ -2,17 +2,11 @@ import adapter, { type AdapterOptions } from "@sveltejs/adapter-cloudflare";
|
|
|
2
2
|
import type { Adapter } from "@sveltejs/kit";
|
|
3
3
|
import { getPlatformProxyOptions } from "../cloudflare-env-proxy.ts";
|
|
4
4
|
|
|
5
|
-
const isSvelteLanguageServer = !!process.argv.find((arg) =>
|
|
6
|
-
arg.includes("svelte-language-server"),
|
|
7
|
-
);
|
|
8
|
-
|
|
9
5
|
export default (options?: AdapterOptions): Adapter => {
|
|
10
|
-
if (
|
|
11
|
-
// the svelte language server runs from the root and therefore may be running in a different cwd than the `Website.cwd`
|
|
12
|
-
// for now, we work around this by returning a noop adapter to avoid breaking svelte intellisense
|
|
6
|
+
if (shouldDisable()) {
|
|
13
7
|
return {
|
|
14
|
-
adapt() {},
|
|
15
8
|
name: "alchemy-noop",
|
|
9
|
+
adapt() {},
|
|
16
10
|
};
|
|
17
11
|
}
|
|
18
12
|
const { platformProxy: proxyOptions, ...config } = options ?? {};
|
|
@@ -23,3 +17,18 @@ export default (options?: AdapterOptions): Adapter => {
|
|
|
23
17
|
...config,
|
|
24
18
|
});
|
|
25
19
|
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Return true for `svelte-language-server`, `svelte-check`, and `svelte-kit sync`. This is because:
|
|
23
|
+
* - The svelte language server runs from the root and therefore may be running in a different cwd than the `Website.cwd`, so disable to avoid breaking intellisense
|
|
24
|
+
* - The `svelte-check` and `svelte-kit sync` commands do not require Cloudflare-specific configuration, so disable to avoid breaking the commands
|
|
25
|
+
*/
|
|
26
|
+
function shouldDisable(argv = process.argv): boolean {
|
|
27
|
+
return (
|
|
28
|
+
argv.some(
|
|
29
|
+
(arg) =>
|
|
30
|
+
arg.includes("svelte-check") || arg.includes("svelte-language-server"),
|
|
31
|
+
) ||
|
|
32
|
+
(argv.some((arg) => arg.includes("svelte-kit")) && argv.includes("sync"))
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -2,6 +2,7 @@ import path from "pathe";
|
|
|
2
2
|
import { getPackageManagerRunner } from "../../util/detect-package-manager.ts";
|
|
3
3
|
import type { Assets } from "../assets.ts";
|
|
4
4
|
import type { Bindings } from "../bindings.ts";
|
|
5
|
+
import { withSkipPathValidation } from "../miniflare/paths.ts";
|
|
5
6
|
import {
|
|
6
7
|
spreadBuildProps,
|
|
7
8
|
spreadDevProps,
|
|
@@ -34,8 +35,9 @@ export async function Vite<B extends Bindings>(
|
|
|
34
35
|
port = args[index + 1];
|
|
35
36
|
} else {
|
|
36
37
|
try {
|
|
37
|
-
const config = await
|
|
38
|
-
|
|
38
|
+
const config = await withSkipPathValidation(
|
|
39
|
+
() =>
|
|
40
|
+
import(path.resolve(props.cwd ?? process.cwd(), "vite.config.ts")),
|
|
39
41
|
);
|
|
40
42
|
port = config.default?.server?.port ?? 5173;
|
|
41
43
|
} catch {}
|
package/src/docker/api.ts
CHANGED
|
@@ -69,10 +69,44 @@ type VolumeInfo = {
|
|
|
69
69
|
Scope: string;
|
|
70
70
|
};
|
|
71
71
|
|
|
72
|
-
type ContainerInfo = {
|
|
72
|
+
export type ContainerInfo = {
|
|
73
73
|
Id: string;
|
|
74
74
|
State: { Status: "created" | "running" | "paused" | "stopped" | "exited" };
|
|
75
75
|
Created: string;
|
|
76
|
+
Config: {
|
|
77
|
+
Image: string;
|
|
78
|
+
Cmd: string[] | null;
|
|
79
|
+
Env: string[] | null;
|
|
80
|
+
Healthcheck?: {
|
|
81
|
+
Test: string[] | null;
|
|
82
|
+
Interval?: number;
|
|
83
|
+
Timeout?: number;
|
|
84
|
+
Retries?: number;
|
|
85
|
+
StartPeriod?: number;
|
|
86
|
+
StartInterval?: number;
|
|
87
|
+
} | null;
|
|
88
|
+
};
|
|
89
|
+
HostConfig: {
|
|
90
|
+
PortBindings: Record<
|
|
91
|
+
string,
|
|
92
|
+
Array<{ HostIp: string; HostPort: string }> | null
|
|
93
|
+
> | null;
|
|
94
|
+
Binds: string[] | null;
|
|
95
|
+
RestartPolicy: {
|
|
96
|
+
Name: string;
|
|
97
|
+
MaximumRetryCount: number;
|
|
98
|
+
};
|
|
99
|
+
AutoRemove: boolean;
|
|
100
|
+
};
|
|
101
|
+
NetworkSettings: {
|
|
102
|
+
Networks: Record<
|
|
103
|
+
string,
|
|
104
|
+
{
|
|
105
|
+
NetworkID: string;
|
|
106
|
+
Aliases: string[] | null;
|
|
107
|
+
}
|
|
108
|
+
> | null;
|
|
109
|
+
};
|
|
76
110
|
};
|
|
77
111
|
|
|
78
112
|
/**
|
|
@@ -103,6 +137,7 @@ export class DockerApi {
|
|
|
103
137
|
async exec(
|
|
104
138
|
args: string[],
|
|
105
139
|
remainingAttempts = 3,
|
|
140
|
+
quiet = false,
|
|
106
141
|
): Promise<{ stdout: string; stderr: string }> {
|
|
107
142
|
// If a custom config directory is provided, ensure all commands use it by
|
|
108
143
|
// setting the DOCKER_CONFIG env variable for the spawned process.
|
|
@@ -124,13 +159,17 @@ export class DockerApi {
|
|
|
124
159
|
|
|
125
160
|
// Stream stdout in real-time
|
|
126
161
|
subprocess.stdout?.on("data", (chunk: string) => {
|
|
127
|
-
|
|
162
|
+
if (!quiet) {
|
|
163
|
+
process.stdout.write(chunk);
|
|
164
|
+
}
|
|
128
165
|
stdout += chunk;
|
|
129
166
|
});
|
|
130
167
|
|
|
131
168
|
// Stream stderr in real-time
|
|
132
169
|
subprocess.stderr?.on("data", (chunk: string) => {
|
|
133
|
-
|
|
170
|
+
if (!quiet) {
|
|
171
|
+
process.stderr.write(chunk);
|
|
172
|
+
}
|
|
134
173
|
stderr += chunk;
|
|
135
174
|
});
|
|
136
175
|
|
|
@@ -364,7 +403,11 @@ export class DockerApi {
|
|
|
364
403
|
* @returns Container details in JSON format
|
|
365
404
|
*/
|
|
366
405
|
async inspectContainer(containerId: string): Promise<ContainerInfo[]> {
|
|
367
|
-
const { stdout } = await this.exec(
|
|
406
|
+
const { stdout } = await this.exec(
|
|
407
|
+
["container", "inspect", containerId],
|
|
408
|
+
undefined,
|
|
409
|
+
true,
|
|
410
|
+
);
|
|
368
411
|
try {
|
|
369
412
|
return JSON.parse(stdout.trim()) as ContainerInfo[];
|
|
370
413
|
} catch (_error) {
|
package/src/docker/container.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Context } from "../context.ts";
|
|
2
2
|
import { Resource } from "../resource.ts";
|
|
3
|
-
import { DockerApi } from "./api.ts";
|
|
3
|
+
import { DockerApi, normalizeDuration, type ContainerInfo } from "./api.ts";
|
|
4
4
|
import type { Image } from "./image.ts";
|
|
5
5
|
import type { RemoteImage } from "./remote-image.ts";
|
|
6
6
|
|
|
@@ -201,7 +201,7 @@ export interface Container extends ContainerProps {
|
|
|
201
201
|
/**
|
|
202
202
|
* Container state
|
|
203
203
|
*/
|
|
204
|
-
state
|
|
204
|
+
state: "created" | "running" | "paused" | "stopped" | "exited";
|
|
205
205
|
|
|
206
206
|
/**
|
|
207
207
|
* Time when the container was created
|
|
@@ -277,6 +277,7 @@ export interface Container extends ContainerProps {
|
|
|
277
277
|
*/
|
|
278
278
|
export const Container = Resource(
|
|
279
279
|
"docker::Container",
|
|
280
|
+
{ alwaysUpdate: true },
|
|
280
281
|
async function (
|
|
281
282
|
this: Context<Container>,
|
|
282
283
|
id: string,
|
|
@@ -311,39 +312,63 @@ export const Container = Resource(
|
|
|
311
312
|
return this.destroy();
|
|
312
313
|
}
|
|
313
314
|
|
|
314
|
-
let containerState:
|
|
315
|
+
let containerState: Container["state"] = "created";
|
|
315
316
|
|
|
316
317
|
// Check if container already exists
|
|
317
318
|
const containerExists = await api.containerExists(containerName);
|
|
318
319
|
|
|
319
320
|
if (containerExists) {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
321
|
+
// Create phase - check for adoption
|
|
322
|
+
if (this.phase === "create" && !props.adopt) {
|
|
323
|
+
throw new Error(
|
|
324
|
+
`Container "${containerName}" already exists. Use adopt: true to adopt it.`,
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const [containerInfo] = await api.inspectContainer(containerName);
|
|
329
|
+
|
|
330
|
+
// Compute what changes are needed
|
|
331
|
+
if (shouldReplace(imageRef, props, containerInfo)) {
|
|
332
|
+
// Need to recreate - remove existing container
|
|
333
|
+
if (this.phase === "update") {
|
|
334
|
+
// In update phase, we can replace the resource
|
|
335
|
+
// Force because we need to delete the old one first if the name is the same
|
|
336
|
+
return this.replace(true);
|
|
337
|
+
} else {
|
|
338
|
+
// In create phase, we cannot replace the resource, so manually delete instead
|
|
339
|
+
await api.removeContainer(containerName, true);
|
|
340
|
+
}
|
|
323
341
|
} else {
|
|
324
|
-
//
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
342
|
+
// Apply incremental changes without recreating the container
|
|
343
|
+
|
|
344
|
+
const { toConnect, toDisconnect } = getNetworkChanges(
|
|
345
|
+
props,
|
|
346
|
+
containerInfo,
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
// Apply network disconnections
|
|
350
|
+
for (const network of toDisconnect) {
|
|
351
|
+
await api.disconnectNetwork(containerInfo.Id, network);
|
|
329
352
|
}
|
|
330
353
|
|
|
331
|
-
//
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
354
|
+
// Apply network connections
|
|
355
|
+
for (const network of toConnect) {
|
|
356
|
+
await api.connectNetwork(containerInfo.Id, network.name, {
|
|
357
|
+
aliases: network.aliases,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
335
360
|
|
|
336
361
|
// Optionally start the container if requested
|
|
337
362
|
if (props.start && containerInfo.State.Status !== "running") {
|
|
338
|
-
await api.startContainer(
|
|
339
|
-
|
|
363
|
+
await api.startContainer(containerInfo.Id);
|
|
364
|
+
containerState = "running";
|
|
340
365
|
}
|
|
341
366
|
|
|
342
367
|
return {
|
|
343
368
|
...props,
|
|
344
369
|
id: containerInfo.Id,
|
|
345
370
|
name: containerName,
|
|
346
|
-
state:
|
|
371
|
+
state: containerState,
|
|
347
372
|
createdAt: new Date(containerInfo.Created).getTime(),
|
|
348
373
|
};
|
|
349
374
|
}
|
|
@@ -402,3 +427,278 @@ export const Container = Resource(
|
|
|
402
427
|
};
|
|
403
428
|
},
|
|
404
429
|
);
|
|
430
|
+
|
|
431
|
+
function getNetworkChanges(
|
|
432
|
+
props: ContainerProps,
|
|
433
|
+
containerInfo: ContainerInfo,
|
|
434
|
+
): { toConnect: NetworkMapping[]; toDisconnect: string[] } {
|
|
435
|
+
const currentNetworks = new Set(
|
|
436
|
+
Object.keys(containerInfo.NetworkSettings.Networks || {}),
|
|
437
|
+
);
|
|
438
|
+
const desiredNetworks = new Map(
|
|
439
|
+
(props.networks || []).map((n) => [n.name, n]),
|
|
440
|
+
);
|
|
441
|
+
const toConnect: NetworkMapping[] = [];
|
|
442
|
+
const toDisconnect: string[] = [];
|
|
443
|
+
for (const network of currentNetworks) {
|
|
444
|
+
if (!desiredNetworks.has(network) && network !== "bridge") {
|
|
445
|
+
toDisconnect.push(network);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
for (const [name, config] of desiredNetworks) {
|
|
449
|
+
if (!currentNetworks.has(name)) {
|
|
450
|
+
toConnect.push(config);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return { toConnect, toDisconnect };
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function shouldReplace(
|
|
457
|
+
imageRef: string,
|
|
458
|
+
props: ContainerProps,
|
|
459
|
+
containerInfo: ContainerInfo,
|
|
460
|
+
): boolean {
|
|
461
|
+
// Check immutable properties that require recreation
|
|
462
|
+
|
|
463
|
+
// Image change - compare the image ID/digest if available
|
|
464
|
+
// The container stores the resolved image ID, so we compare against imageRef
|
|
465
|
+
if (containerInfo.Config.Image !== imageRef) {
|
|
466
|
+
return true;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Command change
|
|
470
|
+
const containerCmd = containerInfo.Config.Cmd || [];
|
|
471
|
+
if (
|
|
472
|
+
props.command && // only compare if command is set; otherwise we'd be comparing against the image's default command
|
|
473
|
+
(props.command.length !== containerCmd.length ||
|
|
474
|
+
!props.command.every((c, i) => c === containerCmd[i]))
|
|
475
|
+
) {
|
|
476
|
+
return true;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Environment variables
|
|
480
|
+
if (!compareEnv(props.environment, containerInfo.Config.Env)) {
|
|
481
|
+
return true;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Port bindings
|
|
485
|
+
if (!comparePorts(props.ports, containerInfo.HostConfig.PortBindings)) {
|
|
486
|
+
return true;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Volume bindings
|
|
490
|
+
if (!compareVolumes(props.volumes, containerInfo.HostConfig.Binds)) {
|
|
491
|
+
return true;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Healthcheck
|
|
495
|
+
if (
|
|
496
|
+
!compareHealthcheck(props.healthcheck, containerInfo.Config.Healthcheck)
|
|
497
|
+
) {
|
|
498
|
+
return true;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Restart policy
|
|
502
|
+
if (
|
|
503
|
+
!compareRestartPolicy(props.restart, containerInfo.HostConfig.RestartPolicy)
|
|
504
|
+
) {
|
|
505
|
+
return true;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// AutoRemove (removeOnExit)
|
|
509
|
+
if ((props.removeOnExit || false) !== containerInfo.HostConfig.AutoRemove) {
|
|
510
|
+
return true;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return false;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Normalize port mappings to a comparable format
|
|
518
|
+
* @internal
|
|
519
|
+
*/
|
|
520
|
+
function normalizePortMappings(
|
|
521
|
+
ports: PortMapping[] | undefined,
|
|
522
|
+
): Map<string, string> {
|
|
523
|
+
const map = new Map<string, string>();
|
|
524
|
+
if (!ports) return map;
|
|
525
|
+
for (const port of ports) {
|
|
526
|
+
const protocol = port.protocol || "tcp";
|
|
527
|
+
map.set(`${port.external}`, `${port.internal}/${protocol}`);
|
|
528
|
+
}
|
|
529
|
+
return map;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Normalize volume mappings to a comparable format
|
|
534
|
+
* @internal
|
|
535
|
+
*/
|
|
536
|
+
function normalizeVolumeMappings(
|
|
537
|
+
volumes: VolumeMapping[] | undefined,
|
|
538
|
+
): Set<string> {
|
|
539
|
+
const set = new Set<string>();
|
|
540
|
+
if (!volumes) return set;
|
|
541
|
+
for (const volume of volumes) {
|
|
542
|
+
const readOnlyFlag = volume.readOnly ? ":ro" : "";
|
|
543
|
+
set.add(`${volume.hostPath}:${volume.containerPath}${readOnlyFlag}`);
|
|
544
|
+
}
|
|
545
|
+
return set;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Compare environment variables
|
|
550
|
+
* @internal
|
|
551
|
+
*/
|
|
552
|
+
function compareEnv(
|
|
553
|
+
propsEnv: Record<string, string> | undefined,
|
|
554
|
+
containerEnv: string[] | null,
|
|
555
|
+
): boolean {
|
|
556
|
+
const propsEntries = Object.entries(propsEnv || {}).sort(([a], [b]) =>
|
|
557
|
+
a.localeCompare(b),
|
|
558
|
+
);
|
|
559
|
+
const containerEntries = (containerEnv || [])
|
|
560
|
+
.map((e) => {
|
|
561
|
+
const idx = e.indexOf("=");
|
|
562
|
+
return idx >= 0 ? ([e.slice(0, idx), e.slice(idx + 1)] as const) : null;
|
|
563
|
+
})
|
|
564
|
+
.filter((e): e is [string, string] => e !== null)
|
|
565
|
+
// Filter out PATH and other system env vars that Docker adds
|
|
566
|
+
.filter(([key]) => propsEnv && key in propsEnv)
|
|
567
|
+
.sort(([a], [b]) => a.localeCompare(b));
|
|
568
|
+
|
|
569
|
+
if (propsEntries.length !== containerEntries.length) return false;
|
|
570
|
+
for (let i = 0; i < propsEntries.length; i++) {
|
|
571
|
+
if (
|
|
572
|
+
propsEntries[i][0] !== containerEntries[i][0] ||
|
|
573
|
+
propsEntries[i][1] !== containerEntries[i][1]
|
|
574
|
+
) {
|
|
575
|
+
return false;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return true;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Compare port bindings
|
|
583
|
+
* @internal
|
|
584
|
+
*/
|
|
585
|
+
function comparePorts(
|
|
586
|
+
propsPorts: PortMapping[] | undefined,
|
|
587
|
+
containerPorts: Record<
|
|
588
|
+
string,
|
|
589
|
+
Array<{ HostIp: string; HostPort: string }> | null
|
|
590
|
+
> | null,
|
|
591
|
+
): boolean {
|
|
592
|
+
const propsMap = normalizePortMappings(propsPorts);
|
|
593
|
+
|
|
594
|
+
// Extract container port mappings
|
|
595
|
+
const containerMap = new Map<string, string>();
|
|
596
|
+
if (containerPorts) {
|
|
597
|
+
for (const [containerPort, bindings] of Object.entries(containerPorts)) {
|
|
598
|
+
if (bindings && bindings.length > 0) {
|
|
599
|
+
containerMap.set(bindings[0].HostPort, containerPort);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (propsMap.size !== containerMap.size) return false;
|
|
605
|
+
for (const [hostPort, containerPort] of propsMap) {
|
|
606
|
+
if (containerMap.get(hostPort) !== containerPort) return false;
|
|
607
|
+
}
|
|
608
|
+
return true;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Compare volume bindings
|
|
613
|
+
* @internal
|
|
614
|
+
*/
|
|
615
|
+
function compareVolumes(
|
|
616
|
+
propsVolumes: VolumeMapping[] | undefined,
|
|
617
|
+
containerBinds: string[] | null,
|
|
618
|
+
): boolean {
|
|
619
|
+
const propsSet = normalizeVolumeMappings(propsVolumes);
|
|
620
|
+
const containerSet = new Set(containerBinds || []);
|
|
621
|
+
|
|
622
|
+
if (propsSet.size !== containerSet.size) return false;
|
|
623
|
+
for (const bind of propsSet) {
|
|
624
|
+
if (!containerSet.has(bind)) return false;
|
|
625
|
+
}
|
|
626
|
+
return true;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Compare healthcheck configuration
|
|
631
|
+
* @internal
|
|
632
|
+
*/
|
|
633
|
+
function compareHealthcheck(
|
|
634
|
+
propsHc: HealthcheckConfig | undefined,
|
|
635
|
+
containerHc:
|
|
636
|
+
| {
|
|
637
|
+
Test: string[] | null;
|
|
638
|
+
Interval?: number;
|
|
639
|
+
Timeout?: number;
|
|
640
|
+
Retries?: number;
|
|
641
|
+
StartPeriod?: number;
|
|
642
|
+
StartInterval?: number;
|
|
643
|
+
}
|
|
644
|
+
| null
|
|
645
|
+
| undefined,
|
|
646
|
+
): boolean {
|
|
647
|
+
// Both undefined/null
|
|
648
|
+
if (!propsHc && !containerHc) return true;
|
|
649
|
+
// One defined, one not
|
|
650
|
+
if (!propsHc || !containerHc) return false;
|
|
651
|
+
|
|
652
|
+
// Compare command
|
|
653
|
+
const propsCmd = Array.isArray(propsHc.cmd)
|
|
654
|
+
? propsHc.cmd.join(" ")
|
|
655
|
+
: propsHc.cmd;
|
|
656
|
+
const containerCmd = containerHc.Test
|
|
657
|
+
? containerHc.Test.slice(1).join(" ")
|
|
658
|
+
: "";
|
|
659
|
+
if (propsCmd !== containerCmd) return false;
|
|
660
|
+
|
|
661
|
+
// Helper to convert Duration to nanoseconds for comparison
|
|
662
|
+
const toNanos = (d: Duration | undefined): number => {
|
|
663
|
+
if (d === undefined) return 0;
|
|
664
|
+
const str = normalizeDuration(d);
|
|
665
|
+
const match = str.match(/^(\d+(?:\.\d+)?)(ms|s|m|h)$/);
|
|
666
|
+
if (!match) return 0;
|
|
667
|
+
const value = parseFloat(match[1]);
|
|
668
|
+
const unit = match[2];
|
|
669
|
+
switch (unit) {
|
|
670
|
+
case "ms":
|
|
671
|
+
return value * 1_000_000;
|
|
672
|
+
case "s":
|
|
673
|
+
return value * 1_000_000_000;
|
|
674
|
+
case "m":
|
|
675
|
+
return value * 60 * 1_000_000_000;
|
|
676
|
+
case "h":
|
|
677
|
+
return value * 3600 * 1_000_000_000;
|
|
678
|
+
default:
|
|
679
|
+
return 0;
|
|
680
|
+
}
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
if (toNanos(propsHc.interval) !== (containerHc.Interval || 0)) return false;
|
|
684
|
+
if (toNanos(propsHc.timeout) !== (containerHc.Timeout || 0)) return false;
|
|
685
|
+
if ((propsHc.retries || 0) !== (containerHc.Retries || 0)) return false;
|
|
686
|
+
if (toNanos(propsHc.startPeriod) !== (containerHc.StartPeriod || 0))
|
|
687
|
+
return false;
|
|
688
|
+
if (toNanos(propsHc.startInterval) !== (containerHc.StartInterval || 0))
|
|
689
|
+
return false;
|
|
690
|
+
|
|
691
|
+
return true;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Compare restart policy
|
|
696
|
+
* @internal
|
|
697
|
+
*/
|
|
698
|
+
function compareRestartPolicy(
|
|
699
|
+
propsRestart: ContainerProps["restart"] | undefined,
|
|
700
|
+
containerRestart: { Name: string; MaximumRetryCount: number },
|
|
701
|
+
): boolean {
|
|
702
|
+
const propsPolicy = propsRestart || "no";
|
|
703
|
+
return propsPolicy === containerRestart.Name;
|
|
704
|
+
}
|
package/src/scope.ts
CHANGED
|
@@ -342,12 +342,21 @@ export class Scope {
|
|
|
342
342
|
return state !== undefined && (type === undefined || state.kind === type);
|
|
343
343
|
}
|
|
344
344
|
|
|
345
|
-
public createPhysicalName(
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
345
|
+
public createPhysicalName(
|
|
346
|
+
id: string,
|
|
347
|
+
delimiter = "-",
|
|
348
|
+
maxLength?: number,
|
|
349
|
+
): string {
|
|
350
|
+
let name = [this.appName, ...this.chain.slice(2), id, this.stage]
|
|
351
|
+
.map((part) => part.replaceAll(/[^a-z0-9_-]/gi, delimiter))
|
|
350
352
|
.join(delimiter);
|
|
353
|
+
while (maxLength && name.length > maxLength) {
|
|
354
|
+
name = name
|
|
355
|
+
.split(delimiter)
|
|
356
|
+
.map((part) => part.slice(0, -1))
|
|
357
|
+
.join(delimiter);
|
|
358
|
+
}
|
|
359
|
+
return name;
|
|
351
360
|
}
|
|
352
361
|
|
|
353
362
|
public async spawn<
|