@alchemy.run/docker 0.0.0 → 2.0.0-beta.85
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/LICENSE +201 -0
- package/README.md +46 -4
- package/lib/Bootstrap.d.ts +7 -0
- package/lib/Bootstrap.d.ts.map +1 -0
- package/lib/Bootstrap.js +25 -0
- package/lib/Bootstrap.js.map +1 -0
- package/lib/BuildHash.d.ts +56 -0
- package/lib/BuildHash.d.ts.map +1 -0
- package/lib/BuildHash.js +172 -0
- package/lib/BuildHash.js.map +1 -0
- package/lib/Container.d.ts +450 -0
- package/lib/Container.d.ts.map +1 -0
- package/lib/Container.js +745 -0
- package/lib/Container.js.map +1 -0
- package/lib/Context.d.ts +86 -0
- package/lib/Context.d.ts.map +1 -0
- package/lib/Context.js +146 -0
- package/lib/Context.js.map +1 -0
- package/lib/Docker.d.ts +527 -0
- package/lib/Docker.d.ts.map +1 -0
- package/lib/Docker.js +437 -0
- package/lib/Docker.js.map +1 -0
- package/lib/Dockerfile.d.ts +72 -0
- package/lib/Dockerfile.d.ts.map +1 -0
- package/lib/Dockerfile.js +12 -0
- package/lib/Dockerfile.js.map +1 -0
- package/lib/HealthcheckCommand.d.ts +17 -0
- package/lib/HealthcheckCommand.d.ts.map +1 -0
- package/lib/HealthcheckCommand.js +33 -0
- package/lib/HealthcheckCommand.js.map +1 -0
- package/lib/Image.d.ts +179 -0
- package/lib/Image.d.ts.map +1 -0
- package/lib/Image.js +210 -0
- package/lib/Image.js.map +1 -0
- package/lib/ImageSource.d.ts +288 -0
- package/lib/ImageSource.d.ts.map +1 -0
- package/lib/ImageSource.js +498 -0
- package/lib/ImageSource.js.map +1 -0
- package/lib/Network.d.ts +70 -0
- package/lib/Network.d.ts.map +1 -0
- package/lib/Network.js +111 -0
- package/lib/Network.js.map +1 -0
- package/lib/Providers.d.ts +21 -0
- package/lib/Providers.d.ts.map +1 -0
- package/lib/Providers.js +26 -0
- package/lib/Providers.js.map +1 -0
- package/lib/Registry.d.ts +47 -0
- package/lib/Registry.d.ts.map +1 -0
- package/lib/Registry.js +67 -0
- package/lib/Registry.js.map +1 -0
- package/lib/RegistryError.d.ts +21 -0
- package/lib/RegistryError.d.ts.map +1 -0
- package/lib/RegistryError.js +28 -0
- package/lib/RegistryError.js.map +1 -0
- package/lib/RemoteImage.d.ts +118 -0
- package/lib/RemoteImage.d.ts.map +1 -0
- package/lib/RemoteImage.js +164 -0
- package/lib/RemoteImage.js.map +1 -0
- package/lib/Service.d.ts +499 -0
- package/lib/Service.d.ts.map +1 -0
- package/lib/Service.js +730 -0
- package/lib/Service.js.map +1 -0
- package/lib/ServiceImage.d.ts +78 -0
- package/lib/ServiceImage.d.ts.map +1 -0
- package/lib/ServiceImage.js +162 -0
- package/lib/ServiceImage.js.map +1 -0
- package/lib/Swarm.d.ts +119 -0
- package/lib/Swarm.d.ts.map +1 -0
- package/lib/Swarm.js +148 -0
- package/lib/Swarm.js.map +1 -0
- package/lib/Volume.d.ts +94 -0
- package/lib/Volume.d.ts.map +1 -0
- package/lib/Volume.js +133 -0
- package/lib/Volume.js.map +1 -0
- package/lib/index.d.ts +14 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +13 -0
- package/lib/index.js.map +1 -0
- package/package.json +58 -3
- package/src/Bootstrap.ts +51 -0
- package/src/BuildHash.ts +265 -0
- package/src/Container.ts +1118 -0
- package/src/Context.ts +215 -0
- package/src/Docker.ts +1173 -0
- package/src/Dockerfile.ts +81 -0
- package/src/HealthcheckCommand.ts +35 -0
- package/src/Image.ts +322 -0
- package/src/ImageSource.ts +842 -0
- package/src/Network.ts +169 -0
- package/src/Providers.ts +46 -0
- package/src/Registry.ts +91 -0
- package/src/RegistryError.ts +48 -0
- package/src/RemoteImage.ts +255 -0
- package/src/Service.ts +1264 -0
- package/src/ServiceImage.ts +252 -0
- package/src/Swarm.ts +221 -0
- package/src/Volume.ts +193 -0
- package/src/index.ts +17 -0
package/lib/Volume.d.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import * as Provider from "alchemy/Provider";
|
|
2
|
+
import { Resource } from "alchemy/Resource";
|
|
3
|
+
import { Docker } from "./Docker.ts";
|
|
4
|
+
import type { Providers } from "./Providers.ts";
|
|
5
|
+
export interface VolumeLabel {
|
|
6
|
+
/** Label name. */
|
|
7
|
+
name: string;
|
|
8
|
+
/** Label value. */
|
|
9
|
+
value: string;
|
|
10
|
+
}
|
|
11
|
+
export interface VolumeProps {
|
|
12
|
+
/**
|
|
13
|
+
* Docker volume name.
|
|
14
|
+
*
|
|
15
|
+
* @default Generated from stack, stage, logical id, and instance id.
|
|
16
|
+
*/
|
|
17
|
+
name?: string;
|
|
18
|
+
/** Volume driver. @default "local" */
|
|
19
|
+
driver?: string;
|
|
20
|
+
/** Driver-specific options. */
|
|
21
|
+
driverOpts?: Record<string, string>;
|
|
22
|
+
/** Custom metadata labels. */
|
|
23
|
+
labels?: Record<string, string>;
|
|
24
|
+
/** Docker context name or context resource. */
|
|
25
|
+
context?: Docker.ContextRef;
|
|
26
|
+
}
|
|
27
|
+
export interface Volume extends Resource<"Docker.Volume", VolumeProps, {
|
|
28
|
+
/** Docker volume name. */
|
|
29
|
+
id: string;
|
|
30
|
+
/** Docker volume name. */
|
|
31
|
+
name: string;
|
|
32
|
+
/** Volume driver. */
|
|
33
|
+
driver: string;
|
|
34
|
+
/** Driver-specific options reported by Docker. */
|
|
35
|
+
driverOpts: Record<string, string>;
|
|
36
|
+
/** Labels reported by Docker. */
|
|
37
|
+
labels: Record<string, string>;
|
|
38
|
+
/** Host mountpoint path. */
|
|
39
|
+
mountpoint?: string;
|
|
40
|
+
/** Creation timestamp in milliseconds since epoch. */
|
|
41
|
+
createdAt: number;
|
|
42
|
+
}, never, Providers> {
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* A Docker volume managed through the active Docker context.
|
|
46
|
+
*
|
|
47
|
+
* Pre-existing same-name volumes are treated as foreign until the engine is
|
|
48
|
+
* allowed to adopt them with `--adopt` or `adopt(true)`.
|
|
49
|
+
*
|
|
50
|
+
*
|
|
51
|
+
* ### Creating Volumes
|
|
52
|
+
* **Example:** Basic volume
|
|
53
|
+
* ```typescript
|
|
54
|
+
* const data = yield* Docker.Volume("data", {
|
|
55
|
+
* name: "app-data",
|
|
56
|
+
* });
|
|
57
|
+
* ```
|
|
58
|
+
*
|
|
59
|
+
* **Example:** PostgreSQL data volume
|
|
60
|
+
* ```typescript
|
|
61
|
+
* const data = yield* Docker.Volume("postgres-data");
|
|
62
|
+
* ```
|
|
63
|
+
*
|
|
64
|
+
* **Example:** Driver options and labels
|
|
65
|
+
* ```typescript
|
|
66
|
+
* const data = yield* Docker.Volume("db-data", {
|
|
67
|
+
* driver: "local",
|
|
68
|
+
* driverOpts: {
|
|
69
|
+
* type: "nfs",
|
|
70
|
+
* o: "addr=10.0.0.1,rw",
|
|
71
|
+
* device: ":/path/to/dir",
|
|
72
|
+
* },
|
|
73
|
+
* labels: {
|
|
74
|
+
* "com.example.usage": "database",
|
|
75
|
+
* },
|
|
76
|
+
* });
|
|
77
|
+
* ```
|
|
78
|
+
*
|
|
79
|
+
* ### Docker Context
|
|
80
|
+
* **Example:** Create a volume in a named Docker context
|
|
81
|
+
* ```typescript
|
|
82
|
+
* const data = yield* Docker.Volume("data", {
|
|
83
|
+
* name: "app-data",
|
|
84
|
+
* context: "remote-build",
|
|
85
|
+
* });
|
|
86
|
+
* ```
|
|
87
|
+
*
|
|
88
|
+
* @resource
|
|
89
|
+
* @product Volume
|
|
90
|
+
*/
|
|
91
|
+
export declare const Volume: import("alchemy/Resource").ResourceClass<Volume>;
|
|
92
|
+
export declare const VolumeProvider: () => import("effect/Layer").Layer<Provider.Provider<Volume>, never, Docker | import("alchemy/Stack").Stack | import("alchemy/Stage").Stage>;
|
|
93
|
+
export declare const toVolumeAttributes: (info: Docker.Volume) => Volume["Attributes"];
|
|
94
|
+
//# sourceMappingURL=Volume.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Volume.d.ts","sourceRoot":"","sources":["../src/Volume.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAI5C,OAAO,EAAE,MAAM,EAAyC,MAAM,aAAa,CAAC;AAC5E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD,MAAM,WAAW,WAAW;IAC1B,kBAAkB;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,mBAAmB;IACnB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,sCAAsC;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,+BAA+B;IAC/B,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,8BAA8B;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,+CAA+C;IAC/C,OAAO,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CAC7B;AAED,MAAM,WAAW,MAAO,SAAQ,QAAQ,CACtC,eAAe,EACf,WAAW,EACX;IACE,0BAA0B;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,0BAA0B;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,qBAAqB;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,kDAAkD;IAClD,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,iCAAiC;IACjC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,4BAA4B;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,sDAAsD;IACtD,SAAS,EAAE,MAAM,CAAC;CACnB,EACD,KAAK,EACL,SAAS,CACV;CAAG;AAEJ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,eAAO,MAAM,MAAM,kDAAoC,CAAC;AAExD,eAAO,MAAM,cAAc,8IAiExB,CAAC;AAaJ,eAAO,MAAM,kBAAkB,SAAU,MAAM,CAAC,MAAM,KAAG,MAAM,CAAC,YAAY,CAQ1E,CAAC"}
|
package/lib/Volume.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { Unowned } from "alchemy/AdoptPolicy";
|
|
2
|
+
import { isResolved } from "alchemy/Diff";
|
|
3
|
+
import * as Provider from "alchemy/Provider";
|
|
4
|
+
import { Resource } from "alchemy/Resource";
|
|
5
|
+
import { createInternalTags, hasAlchemyTags, stripInternalTags } from "alchemy/Tags";
|
|
6
|
+
import * as Effect from "effect/Effect";
|
|
7
|
+
import * as Equal from "effect/Equal";
|
|
8
|
+
import { Docker, dockerContextName, dockerPhysicalName } from "./Docker.js";
|
|
9
|
+
/**
|
|
10
|
+
* A Docker volume managed through the active Docker context.
|
|
11
|
+
*
|
|
12
|
+
* Pre-existing same-name volumes are treated as foreign until the engine is
|
|
13
|
+
* allowed to adopt them with `--adopt` or `adopt(true)`.
|
|
14
|
+
*
|
|
15
|
+
*
|
|
16
|
+
* ### Creating Volumes
|
|
17
|
+
* **Example:** Basic volume
|
|
18
|
+
* ```typescript
|
|
19
|
+
* const data = yield* Docker.Volume("data", {
|
|
20
|
+
* name: "app-data",
|
|
21
|
+
* });
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* **Example:** PostgreSQL data volume
|
|
25
|
+
* ```typescript
|
|
26
|
+
* const data = yield* Docker.Volume("postgres-data");
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* **Example:** Driver options and labels
|
|
30
|
+
* ```typescript
|
|
31
|
+
* const data = yield* Docker.Volume("db-data", {
|
|
32
|
+
* driver: "local",
|
|
33
|
+
* driverOpts: {
|
|
34
|
+
* type: "nfs",
|
|
35
|
+
* o: "addr=10.0.0.1,rw",
|
|
36
|
+
* device: ":/path/to/dir",
|
|
37
|
+
* },
|
|
38
|
+
* labels: {
|
|
39
|
+
* "com.example.usage": "database",
|
|
40
|
+
* },
|
|
41
|
+
* });
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* ### Docker Context
|
|
45
|
+
* **Example:** Create a volume in a named Docker context
|
|
46
|
+
* ```typescript
|
|
47
|
+
* const data = yield* Docker.Volume("data", {
|
|
48
|
+
* name: "app-data",
|
|
49
|
+
* context: "remote-build",
|
|
50
|
+
* });
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* @resource
|
|
54
|
+
* @product Volume
|
|
55
|
+
*/
|
|
56
|
+
export const Volume = Resource("Docker.Volume");
|
|
57
|
+
export const VolumeProvider = () => Provider.effect(Volume, Effect.gen(function* () {
|
|
58
|
+
const docker = yield* Docker;
|
|
59
|
+
return Volume.Provider.of({
|
|
60
|
+
list: () => Effect.succeed([]),
|
|
61
|
+
read: Effect.fn(function* ({ id, instanceId, olds, output }) {
|
|
62
|
+
const context = dockerContextName(olds?.context);
|
|
63
|
+
const name = yield* dockerPhysicalName(id, olds, instanceId);
|
|
64
|
+
const info = yield* docker.volume
|
|
65
|
+
.inspect(name, context)
|
|
66
|
+
.pipe(Effect.catchReason("PlatformError", "NotFound", () => Effect.undefined));
|
|
67
|
+
if (!info)
|
|
68
|
+
return undefined;
|
|
69
|
+
const attrs = toVolumeAttributes(info);
|
|
70
|
+
if (output)
|
|
71
|
+
return attrs;
|
|
72
|
+
// Without prior state, only adopt a volume that carries our branding;
|
|
73
|
+
// anything else is foreign and gated behind `--adopt`.
|
|
74
|
+
const owned = yield* hasAlchemyTags(id, info.Labels ?? undefined);
|
|
75
|
+
return owned ? attrs : Unowned(attrs);
|
|
76
|
+
}),
|
|
77
|
+
diff: Effect.fn(function* ({ id, instanceId, output, news, olds }) {
|
|
78
|
+
if (!isResolved(news))
|
|
79
|
+
return undefined;
|
|
80
|
+
if (dockerContextName(olds?.context) !== dockerContextName(news?.context)) {
|
|
81
|
+
return { action: "replace", deleteFirst: true };
|
|
82
|
+
}
|
|
83
|
+
const args = yield* makeVolumeArgs(id, news, instanceId);
|
|
84
|
+
// Auto-generated names are engine-owned: the deployed name stays
|
|
85
|
+
// authoritative even if the generator would name this id differently
|
|
86
|
+
// today. Only an explicit user-provided name can force a replace.
|
|
87
|
+
const desiredName = news?.name ?? output?.name ?? args.name;
|
|
88
|
+
if (output?.name !== desiredName ||
|
|
89
|
+
output?.driver !== args.driver ||
|
|
90
|
+
!Equal.equals(output?.driverOpts ?? {}, args.opt ?? {}) ||
|
|
91
|
+
// Compare only user labels; internal `alchemy::*` branding lives on
|
|
92
|
+
// the observed volume but must not drive replacement.
|
|
93
|
+
!Equal.equals(stripInternalTags(output?.labels), args.label ?? {})) {
|
|
94
|
+
return { action: "replace", deleteFirst: true };
|
|
95
|
+
}
|
|
96
|
+
}),
|
|
97
|
+
reconcile: Effect.fn(function* ({ id, instanceId, news, output }) {
|
|
98
|
+
const context = dockerContextName(news?.context);
|
|
99
|
+
const args = yield* makeVolumeArgs(id, news, instanceId);
|
|
100
|
+
// Prefer the deployed name: regenerating would target a different
|
|
101
|
+
// volume if the generator's output for this id ever drifts.
|
|
102
|
+
const name = news?.name ?? output?.name ?? args.name;
|
|
103
|
+
const internalTags = yield* createInternalTags(id);
|
|
104
|
+
const result = yield* docker.volume.create({
|
|
105
|
+
...args,
|
|
106
|
+
name,
|
|
107
|
+
label: { ...internalTags, ...args.label },
|
|
108
|
+
context,
|
|
109
|
+
});
|
|
110
|
+
return toVolumeAttributes(yield* docker.volume.inspect(result.stdout, context));
|
|
111
|
+
}),
|
|
112
|
+
delete: Effect.fn(({ olds, output }) => docker.volume
|
|
113
|
+
.remove(output.name, dockerContextName(olds?.context))
|
|
114
|
+
.pipe(Effect.catchReason("PlatformError", "NotFound", () => Effect.void))),
|
|
115
|
+
});
|
|
116
|
+
}));
|
|
117
|
+
// Every prop is optional, so `Docker.Volume("data")` arrives with no props.
|
|
118
|
+
const makeVolumeArgs = (id, props, instanceId) => dockerPhysicalName(id, props, instanceId).pipe(Effect.map((name) => ({
|
|
119
|
+
name,
|
|
120
|
+
driver: props?.driver ?? "local",
|
|
121
|
+
opt: props?.driverOpts,
|
|
122
|
+
label: props?.labels,
|
|
123
|
+
})));
|
|
124
|
+
export const toVolumeAttributes = (info) => ({
|
|
125
|
+
id: info.Name,
|
|
126
|
+
name: info.Name,
|
|
127
|
+
driver: info.Driver,
|
|
128
|
+
driverOpts: info.Options ?? {},
|
|
129
|
+
labels: info.Labels ?? {},
|
|
130
|
+
mountpoint: info.Mountpoint,
|
|
131
|
+
createdAt: Date.parse(info.CreatedAt) || Date.now(),
|
|
132
|
+
});
|
|
133
|
+
//# sourceMappingURL=Volume.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Volume.js","sourceRoot":"","sources":["../src/Volume.ts"],"names":[],"mappings":"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"}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export * from "./Container.ts";
|
|
2
|
+
export * from "./Context.ts";
|
|
3
|
+
export * from "./Docker.ts";
|
|
4
|
+
export * as Dockerfile from "./Dockerfile.ts";
|
|
5
|
+
export * from "./Image.ts";
|
|
6
|
+
export * from "./Network.ts";
|
|
7
|
+
export * from "./Providers.ts";
|
|
8
|
+
export type { ImageRegistry } from "./Registry.ts";
|
|
9
|
+
export { DockerRegistryBlobUnknown, DockerRegistryUnavailable, type DockerImagePublicationError, } from "./RegistryError.ts";
|
|
10
|
+
export * from "./RemoteImage.ts";
|
|
11
|
+
export * from "./Service.ts";
|
|
12
|
+
export * from "./Swarm.ts";
|
|
13
|
+
export * from "./Volume.ts";
|
|
14
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,OAAO,KAAK,UAAU,MAAM,iBAAiB,CAAC;AAC9C,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC;AAC/B,YAAY,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EACL,yBAAyB,EACzB,yBAAyB,EACzB,KAAK,2BAA2B,GACjC,MAAM,oBAAoB,CAAC;AAC5B,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC"}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export * from "./Container.js";
|
|
2
|
+
export * from "./Context.js";
|
|
3
|
+
export * from "./Docker.js";
|
|
4
|
+
export * as Dockerfile from "./Dockerfile.js";
|
|
5
|
+
export * from "./Image.js";
|
|
6
|
+
export * from "./Network.js";
|
|
7
|
+
export * from "./Providers.js";
|
|
8
|
+
export { DockerRegistryBlobUnknown, DockerRegistryUnavailable, } from "./RegistryError.js";
|
|
9
|
+
export * from "./RemoteImage.js";
|
|
10
|
+
export * from "./Service.js";
|
|
11
|
+
export * from "./Swarm.js";
|
|
12
|
+
export * from "./Volume.js";
|
|
13
|
+
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,OAAO,KAAK,UAAU,MAAM,iBAAiB,CAAC;AAC9C,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC;AAE/B,OAAO,EACL,yBAAyB,EACzB,yBAAyB,GAE1B,MAAM,oBAAoB,CAAC;AAC5B,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alchemy.run/docker",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "2.0.0-beta.85",
|
|
4
4
|
"description": "Docker images, containers, networks, volumes and registries for Alchemy, and the image builds container platforms share.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"alchemy",
|
|
@@ -9,12 +9,67 @@
|
|
|
9
9
|
"effect",
|
|
10
10
|
"infrastructure-as-code"
|
|
11
11
|
],
|
|
12
|
+
"homepage": "https://alchemy.run",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/alchemy-run/alchemy/issues"
|
|
15
|
+
},
|
|
12
16
|
"license": "Apache-2.0",
|
|
13
17
|
"author": "Sam Goodwin <sam@alchemy.run>",
|
|
14
|
-
"homepage": "https://alchemy.run",
|
|
15
18
|
"repository": {
|
|
16
19
|
"type": "git",
|
|
17
20
|
"url": "git+https://github.com/alchemy-run/alchemy.git",
|
|
18
21
|
"directory": "packages/docker"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"lib",
|
|
25
|
+
"src"
|
|
26
|
+
],
|
|
27
|
+
"type": "module",
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./lib/index.d.ts",
|
|
32
|
+
"bun": "./src/index.ts",
|
|
33
|
+
"default": "./lib/index.js"
|
|
34
|
+
},
|
|
35
|
+
"./Bootstrap": {
|
|
36
|
+
"types": "./src/Bootstrap.ts",
|
|
37
|
+
"bun": "./src/Bootstrap.ts",
|
|
38
|
+
"default": "./src/Bootstrap.ts"
|
|
39
|
+
},
|
|
40
|
+
"./*": {
|
|
41
|
+
"types": "./lib/*.d.ts",
|
|
42
|
+
"bun": "./src/*.ts",
|
|
43
|
+
"default": "./lib/*.js"
|
|
44
|
+
},
|
|
45
|
+
"./package.json": "./package.json"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"rolldown": "1.2.13"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@effect/platform-bun": "^4.1.0",
|
|
55
|
+
"@types/bun": "1.4.3",
|
|
56
|
+
"@types/node": "26.6.5",
|
|
57
|
+
"alchemy": "2.0.0-beta.85",
|
|
58
|
+
"effect": "^4.1.0",
|
|
59
|
+
"typescript": "^7.0.2"
|
|
60
|
+
},
|
|
61
|
+
"peerDependencies": {
|
|
62
|
+
"@effect/platform-bun": "^4.1.0",
|
|
63
|
+
"alchemy": "2.0.0-beta.85",
|
|
64
|
+
"effect": "^4.1.0"
|
|
65
|
+
},
|
|
66
|
+
"peerDependenciesMeta": {
|
|
67
|
+
"@effect/platform-bun": {
|
|
68
|
+
"optional": true
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"scripts": {
|
|
72
|
+
"build": "pnpm -r --filter \"{.}...\" run build:package",
|
|
73
|
+
"build:package": "node scripts/build.ts"
|
|
19
74
|
}
|
|
20
|
-
}
|
|
75
|
+
}
|
package/src/Bootstrap.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process bootstrap for `Docker.Service` (a bun process serving the
|
|
3
|
+
* bundled program). The generated entry imports this module and the user's
|
|
4
|
+
* `main`, nothing else — see `alchemy/Runtime/Bootstrap` for why.
|
|
5
|
+
*/
|
|
6
|
+
import { BunServices } from "@effect/platform-bun";
|
|
7
|
+
import { BunHttpServer } from "alchemy/Http";
|
|
8
|
+
import { reifyBoundConfigProvider } from "alchemy/Runtime";
|
|
9
|
+
import {
|
|
10
|
+
entrypointLayer,
|
|
11
|
+
resolveProgram,
|
|
12
|
+
runProcess,
|
|
13
|
+
stackFromEnv,
|
|
14
|
+
} from "alchemy/Runtime/Bootstrap";
|
|
15
|
+
import * as ConfigProvider from "effect/ConfigProvider";
|
|
16
|
+
import * as Effect from "effect/Effect";
|
|
17
|
+
import * as FetchHttpClient from "effect/http/FetchHttpClient";
|
|
18
|
+
import * as Layer from "effect/Layer";
|
|
19
|
+
import * as Logger from "effect/Logger";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Resolve the bundled program (the runners registered via `host.run` /
|
|
23
|
+
* serve) and run it with a Bun HTTP server bound to `PORT`, so the returned
|
|
24
|
+
* `{ fetch }` handler is actually served and `host.run` loops stay alive.
|
|
25
|
+
*/
|
|
26
|
+
export const bootstrap = (entrypoint: unknown): Promise<void> => {
|
|
27
|
+
const platform = Layer.mergeAll(
|
|
28
|
+
BunServices.layer,
|
|
29
|
+
FetchHttpClient.layer,
|
|
30
|
+
Logger.layer([Logger.consolePretty()]),
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const program = resolveProgram("program").pipe(
|
|
34
|
+
Effect.provide(
|
|
35
|
+
entrypointLayer(entrypoint).pipe(
|
|
36
|
+
Layer.provideMerge(stackFromEnv),
|
|
37
|
+
Layer.provideMerge(BunHttpServer()),
|
|
38
|
+
Layer.provideMerge(platform),
|
|
39
|
+
Layer.provideMerge(
|
|
40
|
+
Layer.succeed(
|
|
41
|
+
ConfigProvider.ConfigProvider,
|
|
42
|
+
reifyBoundConfigProvider(ConfigProvider.fromEnv(), process.env),
|
|
43
|
+
),
|
|
44
|
+
),
|
|
45
|
+
),
|
|
46
|
+
),
|
|
47
|
+
Effect.scoped,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
return runProcess("Docker service", program);
|
|
51
|
+
};
|
package/src/BuildHash.ts
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* INTERNAL — deterministic identity for a Docker build: context + Dockerfile
|
|
3
|
+
* + platform + build args, with Docker's own semantics (`.dockerignore`
|
|
4
|
+
* pattern matching, symlink targets, permission bits, empty directories).
|
|
5
|
+
* Cloud-agnostic — consumed by `AWS.ECR.Image` and `AWS.Lambda.Function`
|
|
6
|
+
* image packaging; NOT exported from the Docker barrel.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as crypto from "node:crypto";
|
|
10
|
+
import {
|
|
11
|
+
type IgnoreRules,
|
|
12
|
+
listFileSystemDirectory,
|
|
13
|
+
normalizeRelativePath,
|
|
14
|
+
parseIgnoreRules,
|
|
15
|
+
walkIgnoring,
|
|
16
|
+
} from "alchemy/Util/Ignore";
|
|
17
|
+
import * as Data from "effect/Data";
|
|
18
|
+
import * as Effect from "effect/Effect";
|
|
19
|
+
import * as FileSystem from "effect/FileSystem";
|
|
20
|
+
import * as Path from "effect/Path";
|
|
21
|
+
import * as Result from "effect/Result";
|
|
22
|
+
import * as Stream from "effect/Stream";
|
|
23
|
+
|
|
24
|
+
export class BuildHashInvalidProps extends Data.TaggedError("BuildHashInvalidProps")<{
|
|
25
|
+
readonly message: string;
|
|
26
|
+
readonly cause?: unknown;
|
|
27
|
+
}> {}
|
|
28
|
+
|
|
29
|
+
export interface DockerBuildSource {
|
|
30
|
+
context: string;
|
|
31
|
+
dockerfile: string;
|
|
32
|
+
platform: string;
|
|
33
|
+
buildArgs?: Record<string, string>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type DockerBuildHashMode = "all" | "effective";
|
|
37
|
+
|
|
38
|
+
interface DockerIgnore {
|
|
39
|
+
content: string;
|
|
40
|
+
path?: string;
|
|
41
|
+
rules: IgnoreRules;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Resolve and validate a Docker build context and Dockerfile.
|
|
46
|
+
*
|
|
47
|
+
* Dockerfile paths are relative to the build context unless absolute.
|
|
48
|
+
*/
|
|
49
|
+
export const resolveDockerBuildPaths = Effect.fn(function* (
|
|
50
|
+
source: Pick<DockerBuildSource, "context" | "dockerfile">,
|
|
51
|
+
) {
|
|
52
|
+
const fs = yield* FileSystem.FileSystem;
|
|
53
|
+
const path = yield* Path.Path;
|
|
54
|
+
const context = path.resolve(source.context);
|
|
55
|
+
const dockerfile = path.isAbsolute(source.dockerfile)
|
|
56
|
+
? source.dockerfile
|
|
57
|
+
: path.resolve(context, source.dockerfile);
|
|
58
|
+
|
|
59
|
+
if (!(yield* fs.exists(context))) {
|
|
60
|
+
return yield* new BuildHashInvalidProps({
|
|
61
|
+
message: `Docker build context does not exist: ${context}`,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
if (!(yield* fs.exists(dockerfile))) {
|
|
65
|
+
return yield* new BuildHashInvalidProps({
|
|
66
|
+
message: `Dockerfile does not exist: ${dockerfile}`,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { context, dockerfile };
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const resolveDockerIgnore = Effect.fn(function* ({
|
|
74
|
+
context,
|
|
75
|
+
dockerfile,
|
|
76
|
+
}: {
|
|
77
|
+
context: string;
|
|
78
|
+
dockerfile: string;
|
|
79
|
+
}) {
|
|
80
|
+
const fs = yield* FileSystem.FileSystem;
|
|
81
|
+
const path = yield* Path.Path;
|
|
82
|
+
const dockerfileIgnore = `${dockerfile}.dockerignore`;
|
|
83
|
+
const contextIgnore = path.join(context, ".dockerignore");
|
|
84
|
+
const ignoreFile = (yield* fs.exists(dockerfileIgnore))
|
|
85
|
+
? dockerfileIgnore
|
|
86
|
+
: (yield* fs.exists(contextIgnore))
|
|
87
|
+
? contextIgnore
|
|
88
|
+
: undefined;
|
|
89
|
+
|
|
90
|
+
if (ignoreFile === undefined) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const content = yield* fs.readFileString(ignoreFile);
|
|
95
|
+
const relativePath = normalizeRelativePath(path.relative(context, ignoreFile));
|
|
96
|
+
return {
|
|
97
|
+
content,
|
|
98
|
+
path:
|
|
99
|
+
relativePath === ".." || relativePath.startsWith("../") || path.isAbsolute(relativePath)
|
|
100
|
+
? undefined
|
|
101
|
+
: relativePath,
|
|
102
|
+
rules: parseIgnoreRules(content, "dockerignore"),
|
|
103
|
+
} satisfies DockerIgnore;
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
interface DockerBuildContextSelection {
|
|
107
|
+
/** Absolute build-context directory. */
|
|
108
|
+
readonly context: string;
|
|
109
|
+
/** Absolute Dockerfile path. */
|
|
110
|
+
readonly dockerfile: string;
|
|
111
|
+
/** Dockerfile path relative to the context, if it is inside the context. */
|
|
112
|
+
readonly dockerfilePath: string | undefined;
|
|
113
|
+
/** Return whether an entry must be sent to the Docker builder. */
|
|
114
|
+
readonly includes: (relativePath: string) => boolean;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Select the effective files sent for a Docker build.
|
|
119
|
+
*
|
|
120
|
+
* Dockerfile-specific ignore files take precedence over `.dockerignore`.
|
|
121
|
+
* The Dockerfile and selected ignore file stay in the upload even when an
|
|
122
|
+
* ignore rule matches them, as Docker clients must send both to the builder.
|
|
123
|
+
*/
|
|
124
|
+
export const selectDockerBuildContext = Effect.fn(function* (
|
|
125
|
+
source: Pick<DockerBuildSource, "context" | "dockerfile">,
|
|
126
|
+
) {
|
|
127
|
+
const path = yield* Path.Path;
|
|
128
|
+
const { context, dockerfile } = yield* resolveDockerBuildPaths(source);
|
|
129
|
+
const dockerignore = yield* resolveDockerIgnore({ context, dockerfile });
|
|
130
|
+
const relativeDockerfile = normalizeRelativePath(path.relative(context, dockerfile));
|
|
131
|
+
const dockerfilePath =
|
|
132
|
+
relativeDockerfile === ".." ||
|
|
133
|
+
relativeDockerfile.startsWith("../") ||
|
|
134
|
+
path.isAbsolute(relativeDockerfile)
|
|
135
|
+
? undefined
|
|
136
|
+
: relativeDockerfile;
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
context,
|
|
140
|
+
dockerfile,
|
|
141
|
+
dockerfilePath,
|
|
142
|
+
includes: (relativePath: string) => {
|
|
143
|
+
const normalized = normalizeRelativePath(relativePath);
|
|
144
|
+
if (normalized === dockerfilePath || normalized === dockerignore?.path) {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
return dockerignore === undefined || !dockerignore.rules.ignores(normalized);
|
|
148
|
+
},
|
|
149
|
+
} satisfies DockerBuildContextSelection;
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Hash a Docker build context together with the Dockerfile, platform, and
|
|
154
|
+
* build arguments. In `effective` mode, the selected `.dockerignore` is
|
|
155
|
+
* applied exactly once before files are hashed. Absolute paths do not
|
|
156
|
+
* participate.
|
|
157
|
+
*/
|
|
158
|
+
export const hashDockerBuildInputs = Effect.fn(function* (
|
|
159
|
+
source: DockerBuildSource,
|
|
160
|
+
mode: DockerBuildHashMode,
|
|
161
|
+
) {
|
|
162
|
+
const fs = yield* FileSystem.FileSystem;
|
|
163
|
+
const path = yield* Path.Path;
|
|
164
|
+
const { context, dockerfile } = yield* resolveDockerBuildPaths(source);
|
|
165
|
+
const dockerignore =
|
|
166
|
+
mode === "effective" ? yield* resolveDockerIgnore({ context, dockerfile }) : undefined;
|
|
167
|
+
const hasher = yield* Effect.sync(() => crypto.createHash("sha256"));
|
|
168
|
+
|
|
169
|
+
yield* Effect.sync(() =>
|
|
170
|
+
hasher.update(
|
|
171
|
+
JSON.stringify({
|
|
172
|
+
platform: source.platform,
|
|
173
|
+
buildArgs: Object.entries(source.buildArgs ?? {}).sort(([a], [b]) =>
|
|
174
|
+
a < b ? -1 : a > b ? 1 : 0,
|
|
175
|
+
),
|
|
176
|
+
dockerignore:
|
|
177
|
+
dockerignore === undefined
|
|
178
|
+
? undefined
|
|
179
|
+
: {
|
|
180
|
+
content: dockerignore.content,
|
|
181
|
+
path: dockerignore.path,
|
|
182
|
+
},
|
|
183
|
+
}),
|
|
184
|
+
),
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
const dockerfileContent = yield* fs.readFile(dockerfile);
|
|
188
|
+
yield* Effect.sync(() => {
|
|
189
|
+
hasher.update("Dockerfile\0");
|
|
190
|
+
hasher.update(dockerfileContent);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const forcedPaths = [
|
|
194
|
+
dockerignore?.path,
|
|
195
|
+
normalizeRelativePath(path.relative(context, dockerfile)),
|
|
196
|
+
].filter(
|
|
197
|
+
(entry): entry is string =>
|
|
198
|
+
entry !== undefined && entry !== ".." && !entry.startsWith("../") && !path.isAbsolute(entry),
|
|
199
|
+
);
|
|
200
|
+
// Walk one level at a time so an excluded directory is discarded before
|
|
201
|
+
// its contents are read; entries come back sorted, preserving hash order.
|
|
202
|
+
const entries = yield* walkIgnoring({
|
|
203
|
+
list: yield* listFileSystemDirectory(context),
|
|
204
|
+
rules: dockerignore?.rules,
|
|
205
|
+
force: forcedPaths,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
for (const { path: entry } of entries) {
|
|
209
|
+
// The ignore file and Dockerfile contents are hashed above; excluded
|
|
210
|
+
// entries were only walked so re-included descendants could be found.
|
|
211
|
+
if (
|
|
212
|
+
dockerignore !== undefined &&
|
|
213
|
+
(entry === dockerignore.path || dockerignore.rules.ignores(entry))
|
|
214
|
+
) {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const fullPath = path.join(context, entry);
|
|
219
|
+
const hashedEntry = entry;
|
|
220
|
+
// FileSystem.stat follows symbolic links, so probe the link itself first.
|
|
221
|
+
// Docker preserves the link in the build context; hashing the target file
|
|
222
|
+
// would miss retargets between files with identical contents and metadata.
|
|
223
|
+
const link = yield* Effect.result(fs.readLink(fullPath));
|
|
224
|
+
if (Result.isSuccess(link)) {
|
|
225
|
+
yield* Effect.sync(() =>
|
|
226
|
+
hasher.update(
|
|
227
|
+
`${JSON.stringify({
|
|
228
|
+
path: hashedEntry,
|
|
229
|
+
type: "SymbolicLink",
|
|
230
|
+
target: link.success,
|
|
231
|
+
})}\0`,
|
|
232
|
+
),
|
|
233
|
+
);
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const info = yield* fs.stat(fullPath);
|
|
238
|
+
|
|
239
|
+
// Docker COPY preserves entry types and permission bits, including empty
|
|
240
|
+
// directories. Include that metadata before any file bytes so changes such
|
|
241
|
+
// as making a Lambda bootstrap executable invalidate the image tag.
|
|
242
|
+
yield* Effect.sync(() =>
|
|
243
|
+
hasher.update(
|
|
244
|
+
`${JSON.stringify({
|
|
245
|
+
path: hashedEntry,
|
|
246
|
+
type: info.type,
|
|
247
|
+
mode: info.mode & 0o7777,
|
|
248
|
+
size: info.type === "File" ? String(info.size) : undefined,
|
|
249
|
+
})}\0`,
|
|
250
|
+
),
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
if (info.type === "File") {
|
|
254
|
+
yield* fs.stream(fullPath).pipe(
|
|
255
|
+
Stream.runForEach((chunk) =>
|
|
256
|
+
Effect.sync(() => {
|
|
257
|
+
hasher.update(chunk);
|
|
258
|
+
}),
|
|
259
|
+
),
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return (yield* Effect.sync(() => hasher.digest("hex"))).slice(0, 32);
|
|
265
|
+
});
|