1import * as Docker from "@/Docker";
2import * as Provider from "@/Provider";
3import { inMemoryState } from "@/State";
4import * as Test from "@/Test/Alchemy";
5import { describe, expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7
8const { test } = Test.make({
9 providers: Docker.providers(),
10 state: inMemoryState(),
11});
12
13test.provider(
14 "diff replaces a context when name changes",
15 () =>
16 Effect.gen(function* () {
17 const contextProvider = yield* Provider.findProvider(Docker.Context);
18 const contextDiff = yield* contextProvider.diff!({
19 id: "build-context",
20 fqn: "build-context",
21 instanceId: "instance",
22 olds: { name: "old-context", description: "old" },
23 news: { name: "new-context", description: "old" },
24 oldBindings: [],
25 newBindings: [],
26 output: {
27 id: "old-context",
28 name: "old-context",
29 description: "old",
30 docker: undefined,
31 },
32 });
33 expect(contextDiff).toEqual({ action: "replace", deleteFirst: true });
34 }),
35 { tags: ["provider:docker", "provider:docker:context", "local"] },
36);
37
38test.provider(
39 "diff updates a context when description changes",
40 () =>
41 Effect.gen(function* () {
42 const contextProvider = yield* Provider.findProvider(Docker.Context);
43 const contextDiff = yield* contextProvider.diff!({
44 id: "build-context",
45 fqn: "build-context",
46 instanceId: "instance",
47 olds: {
48 name: "build-context",
49 description: "old description",
50 docker: "host=unix:///var/run/docker.sock",
51 },
52 news: {
53 name: "build-context",
54 description: "new description",
55 docker: "host=unix:///var/run/docker.sock",
56 },
57 oldBindings: [],
58 newBindings: [],
59 output: {
60 id: "build-context",
61 name: "build-context",
62 description: "old description",
63 docker: "host=unix:///var/run/docker.sock",
64 },
65 });
66 expect(contextDiff).toEqual({ action: "update" });
67 }),
68 { tags: ["provider:docker", "provider:docker:context", "local"] },
69);
70
71test.provider(
72 "diff replaces a context when docker endpoint is removed",
73 () =>
74 Effect.gen(function* () {
75 const contextProvider = yield* Provider.findProvider(Docker.Context);
76 const contextDiff = yield* contextProvider.diff!({
77 id: "build-context",
78 fqn: "build-context",
79 instanceId: "instance",
80 olds: {
81 name: "build-context",
82 description: "dev context",
83 docker: "host=ssh://user@example.com",
84 },
85 news: {
86 name: "build-context",
87 description: "dev context",
88 },
89 oldBindings: [],
90 newBindings: [],
91 output: {
92 id: "build-context",
93 name: "build-context",
94 description: "dev context",
95 docker: "host=ssh://user@example.com",
96 },
97 });
98 expect(contextDiff).toEqual({ action: "replace", deleteFirst: true });
99 }),
100 { tags: ["provider:docker", "provider:docker:context", "local"] },
101);
102
103describe(
104 "Docker.Context",
105 {
106 tags: ["provider:docker", "provider:docker:context", "local"],
107 concurrent: false,
108 },
109 () => {
110 test.provider("creates a context with description and endpoint", (stack) =>
111 Effect.gen(function* () {
112 const docker = yield* Docker.Docker;
113 const contextName = "alchemy-test-context-create";
114
115 yield* Effect.addFinalizer(() =>
116 docker.context.remove(contextName, true).pipe(Effect.ignore),
117 );
118
119 const context = yield* stack.deploy(
120 Docker.Context("created-context", {
121 name: contextName,
122 description: "created by alchemy tests",
123 docker: "host=unix:///var/run/docker.sock",
124 }),
125 );
126
127 expect(context.name).toBe(contextName);
128 expect(context.id).toBe(contextName);
129 expect(context.description).toBe("created by alchemy tests");
130 expect(extractDockerHost(context.docker)).toBe(
131 "unix:///var/run/docker.sock",
132 );
133 }),
134 );
135
136 test.provider("updates an existing context without replacing it", (stack) =>
137 Effect.gen(function* () {
138 const contextName = "alchemy-test-context-update";
139
140 const first = yield* stack.deploy(
141 Docker.Context("updatable-context", {
142 name: contextName,
143 description: "v1",
144 docker: "host=unix:///var/run/docker.sock",
145 }),
146 );
147
148 const second = yield* stack.deploy(
149 Docker.Context("updatable-context", {
150 name: contextName,
151 description: "v2",
152 docker: "host=unix:///var/run/docker.sock",
153 }),
154 );
155
156 expect(second.id).toBe(first.id);
157 expect(second.name).toBe(first.name);
158 expect(second.description).toBe("v2");
159 expect(extractDockerHost(second.docker)).toBe(
160 "unix:///var/run/docker.sock",
161 );
162 }),
163 );
164
165 test.provider("plans a replace when docker endpoint is cleared", (stack) =>
166 Effect.gen(function* () {
167 const base = Docker.Context("planned-context", {
168 description: "with endpoint",
169 docker: "host=ssh://user@example.com",
170 });
171
172 const changed = Docker.Context("planned-context", {
173 description: "with endpoint",
174 });
175
176 yield* stack.deploy(base);
177 const plan = yield* stack.plan(changed);
178 expect(plan.resources["planned-context"]).toMatchObject({
179 action: "replace",
180 });
181 }),
182 );
183 },
184);
185
186const extractDockerHost = (docker: unknown): string | undefined => {
187 if (typeof docker === "string") {
188 return docker.startsWith("host=") ? docker.slice("host=".length) : docker;
189 }
190 if (typeof docker === "object" && docker !== null && "Host" in docker) {
191 const host = (docker as { Host?: unknown }).Host;
192 return typeof host === "string" ? host : undefined;
193 }
194 return undefined;
195};