@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.
Files changed (98) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +46 -4
  3. package/lib/Bootstrap.d.ts +7 -0
  4. package/lib/Bootstrap.d.ts.map +1 -0
  5. package/lib/Bootstrap.js +25 -0
  6. package/lib/Bootstrap.js.map +1 -0
  7. package/lib/BuildHash.d.ts +56 -0
  8. package/lib/BuildHash.d.ts.map +1 -0
  9. package/lib/BuildHash.js +172 -0
  10. package/lib/BuildHash.js.map +1 -0
  11. package/lib/Container.d.ts +450 -0
  12. package/lib/Container.d.ts.map +1 -0
  13. package/lib/Container.js +745 -0
  14. package/lib/Container.js.map +1 -0
  15. package/lib/Context.d.ts +86 -0
  16. package/lib/Context.d.ts.map +1 -0
  17. package/lib/Context.js +146 -0
  18. package/lib/Context.js.map +1 -0
  19. package/lib/Docker.d.ts +527 -0
  20. package/lib/Docker.d.ts.map +1 -0
  21. package/lib/Docker.js +437 -0
  22. package/lib/Docker.js.map +1 -0
  23. package/lib/Dockerfile.d.ts +72 -0
  24. package/lib/Dockerfile.d.ts.map +1 -0
  25. package/lib/Dockerfile.js +12 -0
  26. package/lib/Dockerfile.js.map +1 -0
  27. package/lib/HealthcheckCommand.d.ts +17 -0
  28. package/lib/HealthcheckCommand.d.ts.map +1 -0
  29. package/lib/HealthcheckCommand.js +33 -0
  30. package/lib/HealthcheckCommand.js.map +1 -0
  31. package/lib/Image.d.ts +179 -0
  32. package/lib/Image.d.ts.map +1 -0
  33. package/lib/Image.js +210 -0
  34. package/lib/Image.js.map +1 -0
  35. package/lib/ImageSource.d.ts +288 -0
  36. package/lib/ImageSource.d.ts.map +1 -0
  37. package/lib/ImageSource.js +498 -0
  38. package/lib/ImageSource.js.map +1 -0
  39. package/lib/Network.d.ts +70 -0
  40. package/lib/Network.d.ts.map +1 -0
  41. package/lib/Network.js +111 -0
  42. package/lib/Network.js.map +1 -0
  43. package/lib/Providers.d.ts +21 -0
  44. package/lib/Providers.d.ts.map +1 -0
  45. package/lib/Providers.js +26 -0
  46. package/lib/Providers.js.map +1 -0
  47. package/lib/Registry.d.ts +47 -0
  48. package/lib/Registry.d.ts.map +1 -0
  49. package/lib/Registry.js +67 -0
  50. package/lib/Registry.js.map +1 -0
  51. package/lib/RegistryError.d.ts +21 -0
  52. package/lib/RegistryError.d.ts.map +1 -0
  53. package/lib/RegistryError.js +28 -0
  54. package/lib/RegistryError.js.map +1 -0
  55. package/lib/RemoteImage.d.ts +118 -0
  56. package/lib/RemoteImage.d.ts.map +1 -0
  57. package/lib/RemoteImage.js +164 -0
  58. package/lib/RemoteImage.js.map +1 -0
  59. package/lib/Service.d.ts +499 -0
  60. package/lib/Service.d.ts.map +1 -0
  61. package/lib/Service.js +730 -0
  62. package/lib/Service.js.map +1 -0
  63. package/lib/ServiceImage.d.ts +78 -0
  64. package/lib/ServiceImage.d.ts.map +1 -0
  65. package/lib/ServiceImage.js +162 -0
  66. package/lib/ServiceImage.js.map +1 -0
  67. package/lib/Swarm.d.ts +119 -0
  68. package/lib/Swarm.d.ts.map +1 -0
  69. package/lib/Swarm.js +148 -0
  70. package/lib/Swarm.js.map +1 -0
  71. package/lib/Volume.d.ts +94 -0
  72. package/lib/Volume.d.ts.map +1 -0
  73. package/lib/Volume.js +133 -0
  74. package/lib/Volume.js.map +1 -0
  75. package/lib/index.d.ts +14 -0
  76. package/lib/index.d.ts.map +1 -0
  77. package/lib/index.js +13 -0
  78. package/lib/index.js.map +1 -0
  79. package/package.json +58 -3
  80. package/src/Bootstrap.ts +51 -0
  81. package/src/BuildHash.ts +265 -0
  82. package/src/Container.ts +1118 -0
  83. package/src/Context.ts +215 -0
  84. package/src/Docker.ts +1173 -0
  85. package/src/Dockerfile.ts +81 -0
  86. package/src/HealthcheckCommand.ts +35 -0
  87. package/src/Image.ts +322 -0
  88. package/src/ImageSource.ts +842 -0
  89. package/src/Network.ts +169 -0
  90. package/src/Providers.ts +46 -0
  91. package/src/Registry.ts +91 -0
  92. package/src/RegistryError.ts +48 -0
  93. package/src/RemoteImage.ts +255 -0
  94. package/src/Service.ts +1264 -0
  95. package/src/ServiceImage.ts +252 -0
  96. package/src/Swarm.ts +221 -0
  97. package/src/Volume.ts +193 -0
  98. package/src/index.ts +17 -0
@@ -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
@@ -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.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
+ }
@@ -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
+ };
@@ -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
+ });