1import { adopt, OwnedBySomeoneElse } from "@/AdoptPolicy";
2import * as Docker from "@/Docker";
3import * as Provider from "@/Provider";
4import { inMemoryState } from "@/State";
5import * as Test from "@/Test/Alchemy";
6import { describe, expect } from "alchemy-test";
7import * as Cause from "effect/Cause";
8import * as Effect from "effect/Effect";
9
10const { test } = Test.make({
11 providers: Docker.providers(),
12 state: inMemoryState(),
13});
14
15test.provider(
16 "diff replaces a network when labels change",
17 () =>
18 Effect.gen(function* () {
19 const networkProvider = yield* Provider.findProvider(Docker.Network);
20 const networkDiff = yield* networkProvider.diff!({
21 id: "app",
22 fqn: "app",
23 instanceId: "instance",
24 olds: { name: "app", labels: { usage: "old" } },
25 news: { name: "app", labels: { usage: "new" } },
26 oldBindings: [],
27 newBindings: [],
28 output: {
29 id: "app",
30 name: "app",
31 driver: "bridge",
32 enableIPv6: false,
33 labels: { usage: "old" },
34 createdAt: 0,
35 },
36 });
37 expect(networkDiff).toEqual({ action: "replace", deleteFirst: true });
38 }),
39 { tags: ["provider:docker", "provider:docker:network", "local"] },
40);
41
42test.provider(
43 "diff replaces a network when its Docker context changes",
44 () =>
45 Effect.gen(function* () {
46 const networkProvider = yield* Provider.findProvider(Docker.Network);
47 const networkDiff = yield* networkProvider.diff!({
48 id: "app",
49 fqn: "app",
50 instanceId: "instance",
51 olds: {
52 name: "app",
53 context: "default",
54 },
55 news: {
56 name: "app",
57 context: "remote-build",
58 },
59 oldBindings: [],
60 newBindings: [],
61 output: {
62 id: "app",
63 name: "app",
64 driver: "bridge",
65 enableIPv6: false,
66 labels: {},
67 createdAt: 0,
68 },
69 });
70 expect(networkDiff).toEqual({ action: "replace", deleteFirst: true });
71 }),
72 { tags: ["provider:docker", "provider:docker:network", "local"] },
73);
74
75describe(
76 "Docker.Network",
77 {
78 tags: ["provider:docker", "provider:docker:network", "local"],
79 concurrent: false,
80 },
81 () => {
82 test.provider("creates a bridge network", (stack) =>
83 Effect.gen(function* () {
84 const network = yield* stack.deploy(
85 Docker.Network("created-network", {
86 labels: { "com.alchemy.test": "true" },
87 }),
88 );
89 expect(network).toMatchObject({
90 name: expect.any(String),
91 driver: "bridge",
92 id: expect.any(String),
93 labels: { "com.alchemy.test": "true" },
94 });
95 }),
96 );
97
98 test.provider(
99 "refuses a pre-existing network unless explicitly adopted",
100 (stack) =>
101 Effect.gen(function* () {
102 const docker = yield* Docker.Docker;
103 const networkName = "alchemy-test-network-adoption";
104 yield* Effect.addFinalizer(() =>
105 docker.network.remove(networkName).pipe(Effect.ignore),
106 );
107 yield* docker.network
108 .remove(networkName)
109 .pipe(
110 Effect.catchReason(
111 "PlatformError",
112 "NotFound",
113 () => Effect.void,
114 ),
115 );
116 yield* docker.network.create({ name: networkName, driver: "bridge" });
117
118 const error = yield* stack
119 .deploy(Docker.Network("existing-network", { name: networkName }))
120 .pipe(
121 Effect.as(undefined),
122 Effect.catchCause((cause) =>
123 Effect.succeed(findOwnedError(cause)),
124 ),
125 );
126 expect(error).toBeInstanceOf(OwnedBySomeoneElse);
127
128 const network = yield* stack.deploy(
129 Docker.Network("existing-network", { name: networkName }).pipe(
130 adopt(true),
131 ),
132 );
133 expect(network.name).toBe(networkName);
134 expect(network.id.length).toBeGreaterThan(0);
135 }),
136 );
137
138 test.provider(
139 "adopts an existing same-name network with stack adoption",
140 (stack) =>
141 Effect.gen(function* () {
142 const docker = yield* Docker.Docker;
143 const networkName = "alchemy-test-network-adopt-existing";
144 yield* Effect.addFinalizer(() =>
145 docker.network.remove(networkName).pipe(Effect.ignore),
146 );
147 yield* docker.network
148 .remove(networkName)
149 .pipe(
150 Effect.catchReason(
151 "PlatformError",
152 "NotFound",
153 () => Effect.void,
154 ),
155 );
156 yield* docker.network.create({ name: networkName, driver: "bridge" });
157
158 const network = yield* stack.deploy(
159 Docker.Network("existing-network", {
160 name: networkName,
161 driver: "bridge",
162 }).pipe(adopt(true)),
163 );
164 expect(network.name).toBe(networkName);
165 expect(network.id.length).toBeGreaterThan(0);
166 }),
167 );
168
169 test.provider("replaces a network when its labels change", (stack) =>
170 Effect.gen(function* () {
171 const docker = yield* Docker.Docker;
172 const name = "alchemy-test-network-replace";
173 yield* docker.network
174 .remove(name)
175 .pipe(
176 Effect.catchReason("PlatformError", "NotFound", () => Effect.void),
177 );
178 const first = yield* stack.deploy(
179 Docker.Network("replaceable-network", {
180 name,
181 labels: { generation: "1" },
182 }),
183 );
184 const second = yield* stack.deploy(
185 Docker.Network("replaceable-network", {
186 name,
187 labels: { generation: "2" },
188 }),
189 );
190 expect(second.id).not.toBe(first.id);
191 expect(second.labels.generation).toBe("2");
192 }),
193 );
194 },
195);
196
197const findOwnedError = (
198 cause: Cause.Cause<unknown>,
199): OwnedBySomeoneElse | undefined =>
200 cause.reasons
201 .map((reason) =>
202 Cause.isFailReason(reason)
203 ? reason.error
204 : Cause.isDieReason(reason)
205 ? reason.defect
206 : undefined,
207 )
208 .find(
209 (value): value is OwnedBySomeoneElse =>
210 value instanceof OwnedBySomeoneElse,
211 );