1import * as Docker from "@/Docker";
2import { inMemoryState } from "@/State";
3import * as Test from "@/Test/Alchemy";
4import { describe, expect } from "alchemy-test";
5import * as Effect from "effect/Effect";
6import * as FileSystem from "effect/FileSystem";
7import * as Path from "effect/Path";
8
9const { test } = Test.make({
10 providers: Docker.providers(),
11 state: inMemoryState(),
12});
13
14describe(
15 "Docker.Image",
16 {
17 tags: ["provider:docker", "provider:docker:image", "local"],
18 concurrent: false,
19 },
20 () => {
21 test.provider("plans an update when the Docker context changes", (stack) =>
22 Effect.gen(function* () {
23 const fs = yield* FileSystem.FileSystem;
24 const path = yield* Path.Path;
25 const root = yield* fs.makeTempDirectoryScoped({
26 prefix: "alchemy-docker-context-plan-",
27 });
28 yield* fs.writeFileString(
29 path.join(root, "Dockerfile"),
30 "FROM scratch\n",
31 );
32
33 const base = Docker.Image("context-image", {
34 tag: "latest",
35 context: "default",
36 build: { context: root },
37 });
38 const changed = Docker.Image("context-image", {
39 tag: "latest",
40 context: "remote-build",
41 build: { context: root },
42 });
43
44 yield* stack.deploy(base);
45 const plan = yield* stack.plan(changed);
46 expect(plan.resources["context-image"]).toMatchObject({
47 action: "update",
48 });
49 }),
50 );
51
52 test.provider(
53 "builds a tiny Dockerfile with an auto-generated name",
54 (stack) =>
55 Effect.gen(function* () {
56 const fs = yield* FileSystem.FileSystem;
57 const path = yield* Path.Path;
58 const root = yield* fs.makeTempDirectoryScoped({
59 prefix: "alchemy-docker-image-",
60 });
61 yield* fs.writeFileString(
62 path.join(root, "Dockerfile"),
63 "FROM scratch\nLABEL alchemy.test=true\n",
64 );
65 // No explicit name: the engine auto-generates the physical name.
66 const image = yield* stack.deploy(
67 Docker.Image("tiny-image", {
68 tag: "latest",
69 build: { context: root },
70 }),
71 );
72 expect(image.imageRef.endsWith(":latest")).toBe(true);
73 expect(image.imageId.length).toBeGreaterThan(0);
74 }),
75 );
76
77 test.provider("updates when the build context changes", (stack) =>
78 Effect.gen(function* () {
79 const fs = yield* FileSystem.FileSystem;
80 const path = yield* Path.Path;
81 const root = yield* fs.makeTempDirectoryScoped({
82 prefix: "alchemy-docker-canary-",
83 });
84 yield* fs.writeFileString(
85 path.join(root, "Dockerfile"),
86 "FROM scratch\n",
87 );
88 yield* fs.writeFileString(
89 path.join(root, "Dockerfile"),
90 "FROM scratch\nLABEL alchemy.test=1\n",
91 );
92
93 const makeStack = Docker.Image("tiny-image", {
94 tag: "latest",
95 build: { context: root },
96 });
97
98 yield* stack.deploy(makeStack);
99 const plan1 = yield* stack.plan(makeStack);
100 expect(plan1.resources["tiny-image"]).toMatchObject({ action: "noop" });
101 yield* fs.writeFileString(
102 path.join(root, "Dockerfile"),
103 "FROM scratch\nLABEL alchemy.test=2\n",
104 );
105 const plan2 = yield* stack.plan(makeStack);
106 expect(plan2.resources["tiny-image"]).toMatchObject({
107 action: "update",
108 });
109 }),
110 );
111
112 test.provider("builds with an explicit repository name and tag", (stack) =>
113 Effect.gen(function* () {
114 const fs = yield* FileSystem.FileSystem;
115 const path = yield* Path.Path;
116 const root = yield* fs.makeTempDirectoryScoped({
117 prefix: "alchemy-docker-image-named-",
118 });
119 yield* fs.writeFileString(
120 path.join(root, "Dockerfile"),
121 "FROM scratch\nLABEL alchemy.test=named\n",
122 );
123 const image = yield* stack.deploy(
124 Docker.Image("named-image", {
125 name: "alchemy-test-named",
126 tag: "v1",
127 build: { context: root },
128 }),
129 );
130 expect(image.name).toBe("alchemy-test-named");
131 expect(image.imageRef).toBe("alchemy-test-named:v1");
132 expect(image.tag).toBe("v1");
133 }),
134 );
135
136 test.provider("rebuilds when the build context changes", (stack) =>
137 Effect.gen(function* () {
138 const fs = yield* FileSystem.FileSystem;
139 const path = yield* Path.Path;
140 const root = yield* fs.makeTempDirectoryScoped({
141 prefix: "alchemy-docker-image-rebuild-",
142 });
143 const dockerfile = path.join(root, "Dockerfile");
144
145 yield* fs.writeFileString(dockerfile, "FROM scratch\nLABEL gen=1\n");
146 const first = yield* stack.deploy(
147 Docker.Image("rebuilt-image", {
148 tag: "latest",
149 build: { context: root },
150 }),
151 );
152
153 yield* fs.writeFileString(dockerfile, "FROM scratch\nLABEL gen=2\n");
154 const second = yield* stack.deploy(
155 Docker.Image("rebuilt-image", {
156 tag: "latest",
157 build: { context: root },
158 }),
159 );
160
161 expect(second.imageRef).toBe(first.imageRef);
162 }),
163 );
164 },
165);