@alchemy.run/docker 2.0.0-beta.85 → 2.0.0-beta.86
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/README.md +16 -0
- package/lib/Bootstrap.d.ts.map +1 -1
- package/lib/Bootstrap.js +2 -1
- package/lib/Bootstrap.js.map +1 -1
- package/lib/BuildCache.d.ts +87 -0
- package/lib/BuildCache.d.ts.map +1 -0
- package/lib/BuildCache.js +39 -0
- package/lib/BuildCache.js.map +1 -0
- package/lib/BuildHash.d.ts +9 -1
- package/lib/BuildHash.d.ts.map +1 -1
- package/lib/BuildHash.js +51 -19
- package/lib/BuildHash.js.map +1 -1
- package/lib/BuildPlan.d.ts +202 -0
- package/lib/BuildPlan.d.ts.map +1 -0
- package/lib/BuildPlan.js +514 -0
- package/lib/BuildPlan.js.map +1 -0
- package/lib/Container.d.ts +24 -1
- package/lib/Container.d.ts.map +1 -1
- package/lib/Container.js +14 -0
- package/lib/Container.js.map +1 -1
- package/lib/Context.d.ts +20 -1
- package/lib/Context.d.ts.map +1 -1
- package/lib/Context.js +15 -0
- package/lib/Context.js.map +1 -1
- package/lib/Docker.d.ts +33 -0
- package/lib/Docker.d.ts.map +1 -1
- package/lib/Docker.js +23 -2
- package/lib/Docker.js.map +1 -1
- package/lib/Dockerfile.d.ts +0 -2
- package/lib/Dockerfile.d.ts.map +1 -1
- package/lib/Dockerfile.js.map +1 -1
- package/lib/DockerfileParser.d.ts +125 -0
- package/lib/DockerfileParser.d.ts.map +1 -0
- package/lib/DockerfileParser.js +1040 -0
- package/lib/DockerfileParser.js.map +1 -0
- package/lib/HealthcheckCommand.d.ts.map +1 -1
- package/lib/HealthcheckCommand.js.map +1 -1
- package/lib/Image.d.ts +40 -12
- package/lib/Image.d.ts.map +1 -1
- package/lib/Image.js +41 -9
- package/lib/Image.js.map +1 -1
- package/lib/ImageArchive.d.ts +41 -0
- package/lib/ImageArchive.d.ts.map +1 -0
- package/lib/ImageArchive.js +54 -0
- package/lib/ImageArchive.js.map +1 -0
- package/lib/ImageSource.d.ts +100 -11
- package/lib/ImageSource.d.ts.map +1 -1
- package/lib/ImageSource.js +219 -73
- package/lib/ImageSource.js.map +1 -1
- package/lib/Network.d.ts +175 -3
- package/lib/Network.d.ts.map +1 -1
- package/lib/Network.js +296 -8
- package/lib/Network.js.map +1 -1
- package/lib/Providers.d.ts.map +1 -1
- package/lib/Providers.js.map +1 -1
- package/lib/Registry.d.ts.map +1 -1
- package/lib/Registry.js.map +1 -1
- package/lib/RegistryError.d.ts.map +1 -1
- package/lib/RegistryError.js.map +1 -1
- package/lib/RemoteImage.d.ts.map +1 -1
- package/lib/RemoteImage.js.map +1 -1
- package/lib/Service.d.ts.map +1 -1
- package/lib/Service.js.map +1 -1
- package/lib/ServiceImage.d.ts.map +1 -1
- package/lib/ServiceImage.js.map +1 -1
- package/lib/ShellWord.d.ts +28 -0
- package/lib/ShellWord.d.ts.map +1 -0
- package/lib/ShellWord.js +301 -0
- package/lib/ShellWord.js.map +1 -0
- package/lib/Swarm.d.ts.map +1 -1
- package/lib/Swarm.js.map +1 -1
- package/lib/Volume.d.ts +24 -1
- package/lib/Volume.d.ts.map +1 -1
- package/lib/Volume.js +16 -0
- package/lib/Volume.js.map +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js.map +1 -1
- package/package.json +9 -9
- package/src/Bootstrap.ts +2 -0
- package/src/BuildCache.ts +111 -0
- package/src/BuildHash.ts +73 -24
- package/src/BuildPlan.ts +758 -0
- package/src/Container.ts +31 -1
- package/src/Context.ts +27 -1
- package/src/Docker.ts +56 -2
- package/src/Dockerfile.ts +0 -2
- package/src/DockerfileParser.ts +1152 -0
- package/src/Image.ts +45 -14
- package/src/ImageArchive.ts +77 -0
- package/src/ImageSource.ts +322 -103
- package/src/Network.ts +432 -10
- package/src/ShellWord.ts +326 -0
- package/src/Volume.ts +33 -1
package/lib/Network.d.ts
CHANGED
|
@@ -2,6 +2,29 @@ import * as Provider from "alchemy/Provider";
|
|
|
2
2
|
import { Resource } from "alchemy/Resource";
|
|
3
3
|
import { Docker } from "./Docker.ts";
|
|
4
4
|
import type { Providers } from "./Providers.ts";
|
|
5
|
+
/** Address allocation settings for one Docker network subnet. */
|
|
6
|
+
export interface NetworkIpamConfig {
|
|
7
|
+
/** IPv4 or IPv6 CIDR network prefix, without host bits. */
|
|
8
|
+
subnet: string;
|
|
9
|
+
/** Gateway address in the subnet's address family. Docker chooses one when omitted. */
|
|
10
|
+
gateway?: string;
|
|
11
|
+
/** CIDR sub-range from which Docker allocates container addresses. */
|
|
12
|
+
ipRange?: string;
|
|
13
|
+
/** Map of reservation names to IP addresses in the subnet's address family. */
|
|
14
|
+
auxAddresses?: Record<string, string>;
|
|
15
|
+
}
|
|
16
|
+
/** Docker network IP Address Management (IPAM) configuration. */
|
|
17
|
+
export interface NetworkIpam {
|
|
18
|
+
/** IPAM driver. @default "default" */
|
|
19
|
+
driver?: string;
|
|
20
|
+
/** IPAM driver-specific key/value options. */
|
|
21
|
+
options?: Record<string, string>;
|
|
22
|
+
/**
|
|
23
|
+
* Subnet configurations. Docker allocates a subnet automatically when omitted;
|
|
24
|
+
* drivers differ in how many explicit subnet entries they accept.
|
|
25
|
+
*/
|
|
26
|
+
config?: NetworkIpamConfig[];
|
|
27
|
+
}
|
|
5
28
|
export interface NetworkProps {
|
|
6
29
|
/**
|
|
7
30
|
* Docker network name.
|
|
@@ -11,9 +34,23 @@ export interface NetworkProps {
|
|
|
11
34
|
name?: string;
|
|
12
35
|
/** Network driver. @default "bridge" */
|
|
13
36
|
driver?: "bridge" | "host" | "none" | "overlay" | "macvlan" | (string & {});
|
|
37
|
+
/**
|
|
38
|
+
* Restrict external connectivity through this network. Containers connected
|
|
39
|
+
* to other networks can still have an external route.
|
|
40
|
+
* @default false
|
|
41
|
+
*/
|
|
42
|
+
internal?: boolean;
|
|
43
|
+
/** Allow standalone containers to attach to an overlay network. @default false */
|
|
44
|
+
attachable?: boolean;
|
|
45
|
+
/** Enable IPv4 address allocation. Explicit `false` requires IPv6. @default true */
|
|
46
|
+
enableIPv4?: boolean;
|
|
14
47
|
/** Enable IPv6 on the network. @default false */
|
|
15
48
|
enableIPv6?: boolean;
|
|
16
|
-
/** Network
|
|
49
|
+
/** Network-driver-specific key/value options, as passed to Docker `--opt`. */
|
|
50
|
+
driverOpts?: Record<string, string>;
|
|
51
|
+
/** Explicit IPAM configuration. Docker allocates missing subnet/gateway values. */
|
|
52
|
+
ipam?: NetworkIpam;
|
|
53
|
+
/** User labels. The `alchemy::*` prefix is reserved for resource ownership. */
|
|
17
54
|
labels?: Record<string, string>;
|
|
18
55
|
/**
|
|
19
56
|
* The engine the network is created on: a Docker context name, a
|
|
@@ -30,13 +67,40 @@ export interface Network extends Resource<"Docker.Network", NetworkProps, {
|
|
|
30
67
|
name: string;
|
|
31
68
|
/** Network driver. */
|
|
32
69
|
driver: string;
|
|
70
|
+
/** Whether the network is isolated from external connectivity. */
|
|
71
|
+
internal: boolean;
|
|
72
|
+
/** Whether manual container attachment is allowed. */
|
|
73
|
+
attachable: boolean;
|
|
74
|
+
/** Whether IPv4 address allocation is enabled. */
|
|
75
|
+
enableIPv4: boolean;
|
|
33
76
|
/** Whether IPv6 is enabled. */
|
|
34
77
|
enableIPv6: boolean;
|
|
78
|
+
/** Driver options reported by Docker. */
|
|
79
|
+
driverOpts: Record<string, string>;
|
|
80
|
+
/** IPAM configuration reported by Docker (including automatically allocated values). */
|
|
81
|
+
ipam: {
|
|
82
|
+
driver: string;
|
|
83
|
+
options: Record<string, string>;
|
|
84
|
+
config: NetworkIpamConfig[];
|
|
85
|
+
};
|
|
35
86
|
/** Labels reported by Docker. */
|
|
36
87
|
labels: Record<string, string>;
|
|
37
88
|
/** Creation timestamp in milliseconds since epoch. */
|
|
38
89
|
createdAt: number;
|
|
39
|
-
}, never, Providers> {
|
|
90
|
+
}, never, Providers, NetworkLookup> {
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Props for {@link Network.lookup}: find a network this stack
|
|
94
|
+
* doesn't manage by its name.
|
|
95
|
+
*/
|
|
96
|
+
export interface NetworkLookup {
|
|
97
|
+
/** Network name. */
|
|
98
|
+
name: string;
|
|
99
|
+
/**
|
|
100
|
+
* Docker context the engine is reached through.
|
|
101
|
+
* @default the current context
|
|
102
|
+
*/
|
|
103
|
+
context?: string;
|
|
40
104
|
}
|
|
41
105
|
/**
|
|
42
106
|
* A Docker network managed through the active Docker context.
|
|
@@ -44,7 +108,6 @@ export interface Network extends Resource<"Docker.Network", NetworkProps, {
|
|
|
44
108
|
* Existing same-name networks are treated as foreign unless the engine is
|
|
45
109
|
* explicitly allowed to adopt them with `--adopt` or `adopt(true)`.
|
|
46
110
|
*
|
|
47
|
-
*
|
|
48
111
|
* ### Creating Networks
|
|
49
112
|
* **Example:** Basic bridge network
|
|
50
113
|
* ```typescript
|
|
@@ -53,6 +116,76 @@ export interface Network extends Resource<"Docker.Network", NetworkProps, {
|
|
|
53
116
|
* });
|
|
54
117
|
* ```
|
|
55
118
|
*
|
|
119
|
+
* ### Isolation and address allocation
|
|
120
|
+
* **Example:** Internal bridge network with an explicit subnet and driver option
|
|
121
|
+
* ```typescript
|
|
122
|
+
* const network = yield* Docker.Network("private-network", {
|
|
123
|
+
* internal: true,
|
|
124
|
+
* attachable: true,
|
|
125
|
+
* enableIPv4: true,
|
|
126
|
+
* driverOpts: { "com.docker.network.driver.mtu": "1420" },
|
|
127
|
+
* ipam: {
|
|
128
|
+
* config: [{
|
|
129
|
+
* subnet: "10.50.0.0/24",
|
|
130
|
+
* gateway: "10.50.0.1",
|
|
131
|
+
* ipRange: "10.50.0.128/25",
|
|
132
|
+
* auxAddresses: { reserved: "10.50.0.5" },
|
|
133
|
+
* }],
|
|
134
|
+
* },
|
|
135
|
+
* });
|
|
136
|
+
* ```
|
|
137
|
+
*
|
|
138
|
+
* An internal network restricts external connectivity through that network;
|
|
139
|
+
* additional container network attachments can still provide external routes.
|
|
140
|
+
* If IPAM is omitted, Docker allocates a subnet and gateway. Alchemy does not
|
|
141
|
+
* treat those automatically assigned values as configuration drift.
|
|
142
|
+
* IPAM subnet and range values must be valid CIDR network prefixes (for
|
|
143
|
+
* example, `10.50.0.0/24`, not `10.50.0.1/24`). Declared IPv4/IPv6 subnets
|
|
144
|
+
* require the corresponding address family to be enabled. Alchemy validates
|
|
145
|
+
* address containment and overlapping subnets before a destructive replacement,
|
|
146
|
+
* and compares IPv6 addresses by their network value rather than text.
|
|
147
|
+
* Invalid options fail with `InvalidNetworkOptions`, whose `path` identifies
|
|
148
|
+
* the rejected field (for example, `ipam.config[0].gateway`).
|
|
149
|
+
* Labels beginning with `alchemy::` are reserved for resource ownership and
|
|
150
|
+
* cannot be overridden by user-supplied labels.
|
|
151
|
+
*
|
|
152
|
+
* ### IPv6-only networks
|
|
153
|
+
* **Example:** Disable IPv4 address allocation
|
|
154
|
+
* ```typescript
|
|
155
|
+
* const network = yield* Docker.Network("ipv6-only", {
|
|
156
|
+
* enableIPv4: false,
|
|
157
|
+
* enableIPv6: true,
|
|
158
|
+
* });
|
|
159
|
+
* ```
|
|
160
|
+
*
|
|
161
|
+
* ### Dual-stack networks
|
|
162
|
+
* **Example:** Allocate addresses from both families on a bridge network
|
|
163
|
+
* ```typescript
|
|
164
|
+
* const network = yield* Docker.Network("dual-stack", {
|
|
165
|
+
* enableIPv6: true,
|
|
166
|
+
* ipam: {
|
|
167
|
+
* config: [
|
|
168
|
+
* { subnet: "10.51.0.0/24", gateway: "10.51.0.1" },
|
|
169
|
+
* { subnet: "fd51:3456:789a::/64", gateway: "fd51:3456:789a::1" },
|
|
170
|
+
* ],
|
|
171
|
+
* },
|
|
172
|
+
* });
|
|
173
|
+
* ```
|
|
174
|
+
*
|
|
175
|
+
* ### Driver-specific IPAM options
|
|
176
|
+
* **Example:** Pass IPAM driver-specific settings
|
|
177
|
+
* ```typescript
|
|
178
|
+
* const network = yield* Docker.Network("ipam-network", {
|
|
179
|
+
* ipam: {
|
|
180
|
+
* driver: "default",
|
|
181
|
+
* options: { "my-option": "my-value" },
|
|
182
|
+
* },
|
|
183
|
+
* });
|
|
184
|
+
* ```
|
|
185
|
+
*
|
|
186
|
+
* Network driver and IPAM driver options are passed to Docker without
|
|
187
|
+
* interpreting driver-specific semantics.
|
|
188
|
+
*
|
|
56
189
|
* ### Adoption
|
|
57
190
|
* **Example:** Adopt a pre-existing network
|
|
58
191
|
* ```typescript
|
|
@@ -61,10 +194,49 @@ export interface Network extends Resource<"Docker.Network", NetworkProps, {
|
|
|
61
194
|
* }).pipe(adopt(true));
|
|
62
195
|
* ```
|
|
63
196
|
*
|
|
197
|
+
* ### Looking up an existing network
|
|
198
|
+
* `Network.lookup` references a network Alchemy doesn't manage (created with
|
|
199
|
+
* the Docker CLI, Compose or another stack). It is read during plan/deploy,
|
|
200
|
+
* never created, updated or removed.
|
|
201
|
+
*
|
|
202
|
+
* **Example:** Attach a container to an existing network
|
|
203
|
+
* ```typescript
|
|
204
|
+
* const shared = Docker.Network.lookup({ name: "compose_default" });
|
|
205
|
+
* const api = yield* Docker.Container("Api", {
|
|
206
|
+
* image: "nginx:alpine",
|
|
207
|
+
* networks: [{ name: shared.name }],
|
|
208
|
+
* });
|
|
209
|
+
* ```
|
|
210
|
+
*
|
|
211
|
+
* ### Replacement
|
|
212
|
+
* Network creation settings are immutable. Changing them replaces the network.
|
|
213
|
+
* Docker refuses to delete a network while it has attached endpoints; Alchemy
|
|
214
|
+
* does not forcibly disconnect unrelated containers to complete replacement.
|
|
215
|
+
* A managed network deleted outside Alchemy is recreated on the next deploy.
|
|
216
|
+
* If another network takes over its name, Alchemy refuses to claim it: the
|
|
217
|
+
* recorded Docker ID must still match, even if the network's settings do.
|
|
218
|
+
* Replacement is not transactional: a custom network or IPAM driver may reject
|
|
219
|
+
* a configuration after Docker has already removed the old network. Alchemy
|
|
220
|
+
* validates known IPAM constraints ahead of time, but the network driver
|
|
221
|
+
* determines which settings take effect. Some drivers may accept options
|
|
222
|
+
* without applying them; inspect the resulting network for advanced settings.
|
|
223
|
+
*
|
|
64
224
|
* @resource
|
|
65
225
|
* @product Network
|
|
66
226
|
*/
|
|
67
227
|
export declare const Network: import("alchemy/Resource").ResourceClass<Network>;
|
|
68
228
|
export declare const NetworkProvider: () => import("effect/Layer").Layer<Provider.Provider<Network>, never, Docker | import("alchemy/Stack").Stack | import("alchemy/Stage").Stage>;
|
|
229
|
+
declare const InvalidNetworkOptions_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
230
|
+
readonly _tag: "InvalidNetworkOptions";
|
|
231
|
+
} & Readonly<A>;
|
|
232
|
+
/** Invalid user-supplied network settings, detected before Docker is called. */
|
|
233
|
+
export declare class InvalidNetworkOptions extends InvalidNetworkOptions_base<{
|
|
234
|
+
/** Property path in the declared Docker.Network configuration. */
|
|
235
|
+
readonly path: string;
|
|
236
|
+
/** Human-readable reason for rejecting the network configuration. */
|
|
237
|
+
readonly message: string;
|
|
238
|
+
}> {
|
|
239
|
+
}
|
|
69
240
|
export declare const toNetworkAttributes: (info: Docker.Network) => Network["Attributes"];
|
|
241
|
+
export {};
|
|
70
242
|
//# sourceMappingURL=Network.d.ts.map
|
package/lib/Network.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Network.d.ts","
|
|
1
|
+
{"version":3,"file":"Network.d.ts","sources":["../src/Network.ts"],"names":[],"mappings":"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"}
|
package/lib/Network.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
import { Unowned } from "alchemy/AdoptPolicy";
|
|
1
|
+
import { OwnedBySomeoneElse, Unowned } from "alchemy/AdoptPolicy";
|
|
2
2
|
import { isResolved } from "alchemy/Diff";
|
|
3
3
|
import * as Provider from "alchemy/Provider";
|
|
4
4
|
import { Resource } from "alchemy/Resource";
|
|
5
5
|
import { createInternalTags, hasAlchemyTags, stripInternalTags } from "alchemy/Tags";
|
|
6
|
+
import * as Data from "effect/Data";
|
|
6
7
|
import * as Effect from "effect/Effect";
|
|
7
8
|
import * as Equal from "effect/Equal";
|
|
9
|
+
import * as Net from "effect/net";
|
|
10
|
+
import * as Result from "effect/Result";
|
|
11
|
+
import * as Schema from "effect/Schema";
|
|
8
12
|
import { Docker, dockerEngineContextName, dockerPhysicalName } from "./Docker.js";
|
|
9
13
|
/**
|
|
10
14
|
* A Docker network managed through the active Docker context.
|
|
@@ -12,7 +16,6 @@ import { Docker, dockerEngineContextName, dockerPhysicalName } from "./Docker.js
|
|
|
12
16
|
* Existing same-name networks are treated as foreign unless the engine is
|
|
13
17
|
* explicitly allowed to adopt them with `--adopt` or `adopt(true)`.
|
|
14
18
|
*
|
|
15
|
-
*
|
|
16
19
|
* ### Creating Networks
|
|
17
20
|
* **Example:** Basic bridge network
|
|
18
21
|
* ```typescript
|
|
@@ -21,6 +24,76 @@ import { Docker, dockerEngineContextName, dockerPhysicalName } from "./Docker.js
|
|
|
21
24
|
* });
|
|
22
25
|
* ```
|
|
23
26
|
*
|
|
27
|
+
* ### Isolation and address allocation
|
|
28
|
+
* **Example:** Internal bridge network with an explicit subnet and driver option
|
|
29
|
+
* ```typescript
|
|
30
|
+
* const network = yield* Docker.Network("private-network", {
|
|
31
|
+
* internal: true,
|
|
32
|
+
* attachable: true,
|
|
33
|
+
* enableIPv4: true,
|
|
34
|
+
* driverOpts: { "com.docker.network.driver.mtu": "1420" },
|
|
35
|
+
* ipam: {
|
|
36
|
+
* config: [{
|
|
37
|
+
* subnet: "10.50.0.0/24",
|
|
38
|
+
* gateway: "10.50.0.1",
|
|
39
|
+
* ipRange: "10.50.0.128/25",
|
|
40
|
+
* auxAddresses: { reserved: "10.50.0.5" },
|
|
41
|
+
* }],
|
|
42
|
+
* },
|
|
43
|
+
* });
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* An internal network restricts external connectivity through that network;
|
|
47
|
+
* additional container network attachments can still provide external routes.
|
|
48
|
+
* If IPAM is omitted, Docker allocates a subnet and gateway. Alchemy does not
|
|
49
|
+
* treat those automatically assigned values as configuration drift.
|
|
50
|
+
* IPAM subnet and range values must be valid CIDR network prefixes (for
|
|
51
|
+
* example, `10.50.0.0/24`, not `10.50.0.1/24`). Declared IPv4/IPv6 subnets
|
|
52
|
+
* require the corresponding address family to be enabled. Alchemy validates
|
|
53
|
+
* address containment and overlapping subnets before a destructive replacement,
|
|
54
|
+
* and compares IPv6 addresses by their network value rather than text.
|
|
55
|
+
* Invalid options fail with `InvalidNetworkOptions`, whose `path` identifies
|
|
56
|
+
* the rejected field (for example, `ipam.config[0].gateway`).
|
|
57
|
+
* Labels beginning with `alchemy::` are reserved for resource ownership and
|
|
58
|
+
* cannot be overridden by user-supplied labels.
|
|
59
|
+
*
|
|
60
|
+
* ### IPv6-only networks
|
|
61
|
+
* **Example:** Disable IPv4 address allocation
|
|
62
|
+
* ```typescript
|
|
63
|
+
* const network = yield* Docker.Network("ipv6-only", {
|
|
64
|
+
* enableIPv4: false,
|
|
65
|
+
* enableIPv6: true,
|
|
66
|
+
* });
|
|
67
|
+
* ```
|
|
68
|
+
*
|
|
69
|
+
* ### Dual-stack networks
|
|
70
|
+
* **Example:** Allocate addresses from both families on a bridge network
|
|
71
|
+
* ```typescript
|
|
72
|
+
* const network = yield* Docker.Network("dual-stack", {
|
|
73
|
+
* enableIPv6: true,
|
|
74
|
+
* ipam: {
|
|
75
|
+
* config: [
|
|
76
|
+
* { subnet: "10.51.0.0/24", gateway: "10.51.0.1" },
|
|
77
|
+
* { subnet: "fd51:3456:789a::/64", gateway: "fd51:3456:789a::1" },
|
|
78
|
+
* ],
|
|
79
|
+
* },
|
|
80
|
+
* });
|
|
81
|
+
* ```
|
|
82
|
+
*
|
|
83
|
+
* ### Driver-specific IPAM options
|
|
84
|
+
* **Example:** Pass IPAM driver-specific settings
|
|
85
|
+
* ```typescript
|
|
86
|
+
* const network = yield* Docker.Network("ipam-network", {
|
|
87
|
+
* ipam: {
|
|
88
|
+
* driver: "default",
|
|
89
|
+
* options: { "my-option": "my-value" },
|
|
90
|
+
* },
|
|
91
|
+
* });
|
|
92
|
+
* ```
|
|
93
|
+
*
|
|
94
|
+
* Network driver and IPAM driver options are passed to Docker without
|
|
95
|
+
* interpreting driver-specific semantics.
|
|
96
|
+
*
|
|
24
97
|
* ### Adoption
|
|
25
98
|
* **Example:** Adopt a pre-existing network
|
|
26
99
|
* ```typescript
|
|
@@ -29,6 +102,33 @@ import { Docker, dockerEngineContextName, dockerPhysicalName } from "./Docker.js
|
|
|
29
102
|
* }).pipe(adopt(true));
|
|
30
103
|
* ```
|
|
31
104
|
*
|
|
105
|
+
* ### Looking up an existing network
|
|
106
|
+
* `Network.lookup` references a network Alchemy doesn't manage (created with
|
|
107
|
+
* the Docker CLI, Compose or another stack). It is read during plan/deploy,
|
|
108
|
+
* never created, updated or removed.
|
|
109
|
+
*
|
|
110
|
+
* **Example:** Attach a container to an existing network
|
|
111
|
+
* ```typescript
|
|
112
|
+
* const shared = Docker.Network.lookup({ name: "compose_default" });
|
|
113
|
+
* const api = yield* Docker.Container("Api", {
|
|
114
|
+
* image: "nginx:alpine",
|
|
115
|
+
* networks: [{ name: shared.name }],
|
|
116
|
+
* });
|
|
117
|
+
* ```
|
|
118
|
+
*
|
|
119
|
+
* ### Replacement
|
|
120
|
+
* Network creation settings are immutable. Changing them replaces the network.
|
|
121
|
+
* Docker refuses to delete a network while it has attached endpoints; Alchemy
|
|
122
|
+
* does not forcibly disconnect unrelated containers to complete replacement.
|
|
123
|
+
* A managed network deleted outside Alchemy is recreated on the next deploy.
|
|
124
|
+
* If another network takes over its name, Alchemy refuses to claim it: the
|
|
125
|
+
* recorded Docker ID must still match, even if the network's settings do.
|
|
126
|
+
* Replacement is not transactional: a custom network or IPAM driver may reject
|
|
127
|
+
* a configuration after Docker has already removed the old network. Alchemy
|
|
128
|
+
* validates known IPAM constraints ahead of time, but the network driver
|
|
129
|
+
* determines which settings take effect. Some drivers may accept options
|
|
130
|
+
* without applying them; inspect the resulting network for advanced settings.
|
|
131
|
+
*
|
|
32
132
|
* @resource
|
|
33
133
|
* @product Network
|
|
34
134
|
*/
|
|
@@ -37,17 +137,25 @@ export const NetworkProvider = () => Provider.effect(Network, Effect.gen(functio
|
|
|
37
137
|
const docker = yield* Docker;
|
|
38
138
|
return Network.Provider.of({
|
|
39
139
|
list: () => Effect.succeed([]),
|
|
140
|
+
lookup: Effect.fn(function* ({ name, context }) {
|
|
141
|
+
const info = yield* docker.network
|
|
142
|
+
.inspect(name, context)
|
|
143
|
+
.pipe(Effect.catchReason("PlatformError", "NotFound", () => Effect.undefined));
|
|
144
|
+
return info ? toNetworkAttributes(info) : undefined;
|
|
145
|
+
}),
|
|
40
146
|
read: Effect.fn(function* ({ id, instanceId, olds, output }) {
|
|
41
147
|
const context = dockerEngineContextName(olds?.context);
|
|
42
|
-
const name = yield*
|
|
148
|
+
const name = yield* networkName(id, olds, instanceId);
|
|
43
149
|
const info = yield* docker.network
|
|
44
150
|
.inspect(name, context)
|
|
45
151
|
.pipe(Effect.catchReason("PlatformError", "NotFound", () => Effect.undefined));
|
|
46
152
|
if (!info)
|
|
47
153
|
return undefined;
|
|
48
154
|
const attrs = toNetworkAttributes(info);
|
|
155
|
+
// A different network may have taken over the same name after the
|
|
156
|
+
// managed network was removed out of band. Never silently adopt it.
|
|
49
157
|
if (output)
|
|
50
|
-
return attrs;
|
|
158
|
+
return output.id === attrs.id ? attrs : Unowned(attrs);
|
|
51
159
|
// Without prior state, only adopt a network that carries our branding;
|
|
52
160
|
// anything else is foreign and gated behind `--adopt`.
|
|
53
161
|
const owned = yield* hasAlchemyTags(id, info.Labels ?? undefined);
|
|
@@ -56,35 +164,61 @@ export const NetworkProvider = () => Provider.effect(Network, Effect.gen(functio
|
|
|
56
164
|
diff: Effect.fn(function* ({ id, output, instanceId, news, olds }) {
|
|
57
165
|
if (!isResolved(news) || !output)
|
|
58
166
|
return undefined;
|
|
167
|
+
const args = yield* makeNetworkArgs(id, news, instanceId);
|
|
59
168
|
if (dockerEngineContextName(olds?.context) !== dockerEngineContextName(news?.context)) {
|
|
60
169
|
return { action: "replace", deleteFirst: true };
|
|
61
170
|
}
|
|
62
|
-
const args = yield* makeNetworkArgs(id, news, instanceId);
|
|
63
171
|
// Auto-generated names are engine-owned: the deployed name stays
|
|
64
172
|
// authoritative even if the generator would name this id differently
|
|
65
173
|
// today. Only an explicit user-provided name can force a replace.
|
|
66
174
|
const desiredName = news?.name ?? output.name;
|
|
67
175
|
if (output.name !== desiredName ||
|
|
68
176
|
output.driver !== args.driver ||
|
|
177
|
+
(output.internal ?? false) !== (args.internal ?? false) ||
|
|
178
|
+
(output.attachable ?? false) !== (args.attachable ?? false) ||
|
|
179
|
+
(output.enableIPv4 ?? true) !== (args.ipv4 ?? true) ||
|
|
69
180
|
output.enableIPv6 !== args.ipv6 ||
|
|
181
|
+
!matchesOptions(output.driverOpts ?? {}, args.opt ?? {}) ||
|
|
182
|
+
hasRemovedOptions(olds?.driverOpts, news?.driverOpts) ||
|
|
183
|
+
!matchesIpam(output.ipam ?? { driver: "default", options: {}, config: [] }, news?.ipam, olds?.ipam) ||
|
|
70
184
|
// Compare only user labels; internal `alchemy::*` branding lives on
|
|
71
185
|
// the observed network but must not drive replacement.
|
|
72
186
|
!Equal.equals(stripInternalTags(output.labels), args.label ?? {})) {
|
|
73
187
|
return { action: "replace", deleteFirst: true };
|
|
74
188
|
}
|
|
189
|
+
// The persisted attributes do not prove that the same physical
|
|
190
|
+
// network still exists. Another actor can delete it and reuse the
|
|
191
|
+
// name, so check the Docker ID even when props have not changed.
|
|
192
|
+
const current = yield* docker.network
|
|
193
|
+
.inspect(output.name, dockerEngineContextName(olds?.context))
|
|
194
|
+
.pipe(Effect.catchReason("PlatformError", "NotFound", () => Effect.undefined));
|
|
195
|
+
if (current && current.Id !== output.id) {
|
|
196
|
+
return yield* new OwnedBySomeoneElse({
|
|
197
|
+
message: `Network '${output.name}' was replaced outside Alchemy: ` +
|
|
198
|
+
`expected Docker ID ${output.id}, found ${current.Id}. ` +
|
|
199
|
+
"Choose a different name or explicitly resolve the ownership conflict.",
|
|
200
|
+
resourceType: "Docker.Network",
|
|
201
|
+
logicalId: id,
|
|
202
|
+
physicalName: output.name,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
// Reconcile can recreate a missing network. An update avoids
|
|
206
|
+
// deleting a generation that has already disappeared.
|
|
207
|
+
if (!current)
|
|
208
|
+
return { action: "update" };
|
|
75
209
|
}),
|
|
76
210
|
reconcile: Effect.fn(function* ({ output, id, instanceId, news }) {
|
|
77
211
|
const context = dockerEngineContextName(news?.context);
|
|
212
|
+
const args = yield* makeNetworkArgs(id, news, instanceId);
|
|
78
213
|
if (output) {
|
|
79
214
|
const refreshed = yield* docker.network.inspect(output.id, context).pipe(Effect.map(toNetworkAttributes), Effect.catchReason("PlatformError", "NotFound", () => Effect.undefined));
|
|
80
215
|
if (refreshed)
|
|
81
216
|
return refreshed;
|
|
82
217
|
}
|
|
83
|
-
const args = yield* makeNetworkArgs(id, news, instanceId);
|
|
84
218
|
const internalTags = yield* createInternalTags(id);
|
|
85
219
|
const { stdout: createdId } = yield* docker.network.create({
|
|
86
220
|
...args,
|
|
87
|
-
label: { ...
|
|
221
|
+
label: { ...args.label, ...internalTags },
|
|
88
222
|
context,
|
|
89
223
|
});
|
|
90
224
|
return toNetworkAttributes(yield* docker.network.inspect(createdId, context));
|
|
@@ -94,17 +228,171 @@ export const NetworkProvider = () => Provider.effect(Network, Effect.gen(functio
|
|
|
94
228
|
.pipe(Effect.catchReason("PlatformError", "NotFound", () => Effect.void))),
|
|
95
229
|
});
|
|
96
230
|
}));
|
|
97
|
-
|
|
231
|
+
/**
|
|
232
|
+
* Swarm (overlay) network names are capped at 63 characters, like service
|
|
233
|
+
* names; other drivers accept much longer ones. An overlay network over the
|
|
234
|
+
* cap could never be created, so capping it renames nothing.
|
|
235
|
+
*/
|
|
236
|
+
const networkName = (id, props, instanceId) => dockerPhysicalName(id, props, instanceId, props?.driver === "overlay" ? 63 : undefined);
|
|
237
|
+
/** Invalid user-supplied network settings, detected before Docker is called. */
|
|
238
|
+
export class InvalidNetworkOptions extends Data.TaggedError("InvalidNetworkOptions") {
|
|
239
|
+
}
|
|
240
|
+
const invalidNetworkOption = (path, detail) => Effect.fail(new InvalidNetworkOptions({ path, message: `Docker.Network ${path}: ${detail}` }));
|
|
241
|
+
const invalidMapEntry = (entries) => Object.entries(entries ?? {}).find(([key, value]) => !key.trim() || key.includes("=") || !value.trim());
|
|
242
|
+
// Effect's network schemas reject CIDRs with host bits set and normalize
|
|
243
|
+
// equivalent IPv6 spellings. Use the same semantics for input validation and
|
|
244
|
+
// observed-state comparisons rather than maintaining a second IP parser.
|
|
245
|
+
const parseNetwork = Schema.decodeUnknownResult(Schema.IpNetworkFromString);
|
|
246
|
+
const parseIpAddress = Schema.decodeUnknownResult(Schema.IpAddressFromString);
|
|
247
|
+
const sameNetwork = (left, right) => {
|
|
248
|
+
const a = parseNetwork(left);
|
|
249
|
+
const b = parseNetwork(right);
|
|
250
|
+
return Result.isSuccess(a) && Result.isSuccess(b) && Equal.equals(a.success, b.success);
|
|
251
|
+
};
|
|
252
|
+
const sameIpAddress = (left, right) => {
|
|
253
|
+
const a = parseIpAddress(left);
|
|
254
|
+
const b = parseIpAddress(right);
|
|
255
|
+
return Result.isSuccess(a) && Result.isSuccess(b) && Equal.equals(a.success, b.success);
|
|
256
|
+
};
|
|
257
|
+
const validateNetworkOptions = (props) => {
|
|
258
|
+
if (props?.enableIPv4 === false && props.enableIPv6 !== true) {
|
|
259
|
+
return invalidNetworkOption("enableIPv4", "disabling IPv4 requires enableIPv6=true");
|
|
260
|
+
}
|
|
261
|
+
const reservedLabel = Object.keys(props?.labels ?? {}).find((key) => key.startsWith("alchemy::"));
|
|
262
|
+
if (reservedLabel !== undefined) {
|
|
263
|
+
return invalidNetworkOption(`labels[${JSON.stringify(reservedLabel)}]`, "the alchemy:: label prefix is reserved for resource ownership");
|
|
264
|
+
}
|
|
265
|
+
for (const [path, entries] of [
|
|
266
|
+
["driverOpts", props?.driverOpts],
|
|
267
|
+
["ipam.options", props?.ipam?.options],
|
|
268
|
+
]) {
|
|
269
|
+
const invalid = invalidMapEntry(entries);
|
|
270
|
+
if (invalid) {
|
|
271
|
+
return invalidNetworkOption(`${path}[${JSON.stringify(invalid[0])}]`, "option names must be nonempty and exclude '=', and values must be nonempty");
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (props?.ipam?.driver !== undefined && !props.ipam.driver.trim()) {
|
|
275
|
+
return invalidNetworkOption("ipam.driver", "driver cannot be empty");
|
|
276
|
+
}
|
|
277
|
+
const subnets = [];
|
|
278
|
+
for (const [index, entry] of (props?.ipam?.config ?? []).entries()) {
|
|
279
|
+
const path = `ipam.config[${index}]`;
|
|
280
|
+
const subnet = parseNetwork(entry.subnet);
|
|
281
|
+
if (Result.isFailure(subnet)) {
|
|
282
|
+
return invalidNetworkOption(`${path}.subnet`, subnet.failure.message);
|
|
283
|
+
}
|
|
284
|
+
if (Net.IpNetwork.isIpv4Network(subnet.success) && props?.enableIPv4 === false) {
|
|
285
|
+
return invalidNetworkOption(`${path}.subnet`, `IPv4 subnet ${entry.subnet} requires enableIPv4 to be enabled`);
|
|
286
|
+
}
|
|
287
|
+
if (Net.IpNetwork.isIpv6Network(subnet.success) && props?.enableIPv6 !== true) {
|
|
288
|
+
return invalidNetworkOption(`${path}.subnet`, `IPv6 subnet ${entry.subnet} requires enableIPv6=true`);
|
|
289
|
+
}
|
|
290
|
+
const overlapping = subnets.findIndex((previous) => Net.IpNetwork.overlaps(previous, subnet.success));
|
|
291
|
+
if (overlapping !== -1) {
|
|
292
|
+
return invalidNetworkOption(`${path}.subnet`, `subnet ${entry.subnet} overlaps ipam.config[${overlapping}].subnet`);
|
|
293
|
+
}
|
|
294
|
+
subnets.push(subnet.success);
|
|
295
|
+
if (entry.gateway !== undefined) {
|
|
296
|
+
const gateway = parseIpAddress(entry.gateway);
|
|
297
|
+
if (Result.isFailure(gateway)) {
|
|
298
|
+
return invalidNetworkOption(`${path}.gateway`, gateway.failure.message);
|
|
299
|
+
}
|
|
300
|
+
if (!Net.IpNetwork.contains(subnet.success, gateway.success)) {
|
|
301
|
+
return invalidNetworkOption(`${path}.gateway`, `gateway ${entry.gateway} must be inside subnet ${entry.subnet}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (entry.ipRange !== undefined) {
|
|
305
|
+
const range = parseNetwork(entry.ipRange);
|
|
306
|
+
if (Result.isFailure(range)) {
|
|
307
|
+
return invalidNetworkOption(`${path}.ipRange`, range.failure.message);
|
|
308
|
+
}
|
|
309
|
+
if (!Net.IpNetwork.containsNetwork(subnet.success, range.success)) {
|
|
310
|
+
return invalidNetworkOption(`${path}.ipRange`, `range ${entry.ipRange} must be inside subnet ${entry.subnet}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
const invalidAux = invalidMapEntry(entry.auxAddresses);
|
|
314
|
+
if (invalidAux) {
|
|
315
|
+
return invalidNetworkOption(`${path}.auxAddresses[${JSON.stringify(invalidAux[0])}]`, "reservation names must be nonempty and exclude '=', and addresses must be nonempty");
|
|
316
|
+
}
|
|
317
|
+
for (const [name, address] of Object.entries(entry.auxAddresses ?? {})) {
|
|
318
|
+
const auxPath = `${path}.auxAddresses[${JSON.stringify(name)}]`;
|
|
319
|
+
const reserved = parseIpAddress(address);
|
|
320
|
+
if (Result.isFailure(reserved)) {
|
|
321
|
+
return invalidNetworkOption(auxPath, reserved.failure.message);
|
|
322
|
+
}
|
|
323
|
+
if (!Net.IpNetwork.contains(subnet.success, reserved.success)) {
|
|
324
|
+
return invalidNetworkOption(auxPath, `address ${address} must be inside subnet ${entry.subnet}`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return Effect.void;
|
|
329
|
+
};
|
|
330
|
+
const makeNetworkArgs = (id, props, instanceId) => networkName(id, props, instanceId).pipe(Effect.tap(() => validateNetworkOptions(props)), Effect.map((name) => ({
|
|
98
331
|
name,
|
|
99
332
|
driver: props?.driver ?? "bridge",
|
|
333
|
+
internal: props?.internal,
|
|
334
|
+
attachable: props?.attachable,
|
|
335
|
+
ipv4: props?.enableIPv4,
|
|
100
336
|
ipv6: props?.enableIPv6 ?? false,
|
|
337
|
+
opt: props?.driverOpts,
|
|
338
|
+
ipam: props?.ipam,
|
|
101
339
|
label: props?.labels ?? {},
|
|
102
340
|
})));
|
|
341
|
+
const matchesOptions = (actual, desired) => Object.entries(desired).every(([key, value]) => actual[key] === value);
|
|
342
|
+
const hasRemovedOptions = (previous, current) => Object.keys(previous ?? {}).some((key) => !(key in (current ?? {})));
|
|
343
|
+
const matchesIpam = (observed, desired, previous) => {
|
|
344
|
+
if (desired?.driver !== undefined && observed.driver !== desired.driver)
|
|
345
|
+
return false;
|
|
346
|
+
if (previous?.driver !== undefined && desired?.driver === undefined)
|
|
347
|
+
return false;
|
|
348
|
+
if (!matchesOptions(observed.options, desired?.options ?? {}))
|
|
349
|
+
return false;
|
|
350
|
+
if (hasRemovedOptions(previous?.options, desired?.options))
|
|
351
|
+
return false;
|
|
352
|
+
const config = desired?.config ?? [];
|
|
353
|
+
for (const entry of config) {
|
|
354
|
+
const live = observed.config.find((item) => sameNetwork(item.subnet, entry.subnet));
|
|
355
|
+
if (!live ||
|
|
356
|
+
(entry.gateway !== undefined &&
|
|
357
|
+
(live.gateway === undefined || !sameIpAddress(live.gateway, entry.gateway))) ||
|
|
358
|
+
(entry.ipRange !== undefined &&
|
|
359
|
+
(live.ipRange === undefined || !sameNetwork(live.ipRange, entry.ipRange))) ||
|
|
360
|
+
!Object.entries(entry.auxAddresses ?? {}).every(([key, value]) => live.auxAddresses?.[key] !== undefined && sameIpAddress(live.auxAddresses[key], value))) {
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
// Docker auto-allocates IPAM entries when config is omitted. Compare only
|
|
365
|
+
// explicitly declared entries, but detect removal of formerly managed ones.
|
|
366
|
+
for (const old of previous?.config ?? []) {
|
|
367
|
+
const now = config.find((item) => sameNetwork(item.subnet, old.subnet));
|
|
368
|
+
if (!now ||
|
|
369
|
+
(old.gateway !== undefined && now.gateway === undefined) ||
|
|
370
|
+
(old.ipRange !== undefined && now.ipRange === undefined) ||
|
|
371
|
+
hasRemovedOptions(old.auxAddresses, now.auxAddresses)) {
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return true;
|
|
376
|
+
};
|
|
103
377
|
export const toNetworkAttributes = (info) => ({
|
|
104
378
|
id: info.Id,
|
|
105
379
|
name: info.Name,
|
|
106
380
|
driver: info.Driver,
|
|
381
|
+
internal: info.Internal ?? false,
|
|
382
|
+
attachable: info.Attachable ?? false,
|
|
383
|
+
enableIPv4: info.EnableIPv4 ?? true,
|
|
107
384
|
enableIPv6: info.EnableIPv6,
|
|
385
|
+
driverOpts: info.Options ?? {},
|
|
386
|
+
ipam: {
|
|
387
|
+
driver: info.IPAM?.Driver ?? "default",
|
|
388
|
+
options: info.IPAM?.Options ?? {},
|
|
389
|
+
config: (info.IPAM?.Config ?? []).map((entry) => ({
|
|
390
|
+
subnet: entry.Subnet,
|
|
391
|
+
gateway: entry.Gateway,
|
|
392
|
+
ipRange: entry.IPRange,
|
|
393
|
+
auxAddresses: entry.AuxiliaryAddresses ?? {},
|
|
394
|
+
})),
|
|
395
|
+
},
|
|
108
396
|
labels: info.Labels ?? {},
|
|
109
397
|
createdAt: Date.parse(info.Created) || Date.now(),
|
|
110
398
|
});
|