@alchemy.run/docker 2.0.0-beta.84 → 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.
Files changed (98) hide show
  1. package/README.md +16 -0
  2. package/lib/Bootstrap.d.ts.map +1 -1
  3. package/lib/Bootstrap.js +2 -1
  4. package/lib/Bootstrap.js.map +1 -1
  5. package/lib/BuildCache.d.ts +87 -0
  6. package/lib/BuildCache.d.ts.map +1 -0
  7. package/lib/BuildCache.js +39 -0
  8. package/lib/BuildCache.js.map +1 -0
  9. package/lib/BuildHash.d.ts +20 -3
  10. package/lib/BuildHash.d.ts.map +1 -1
  11. package/lib/BuildHash.js +60 -21
  12. package/lib/BuildHash.js.map +1 -1
  13. package/lib/BuildPlan.d.ts +202 -0
  14. package/lib/BuildPlan.d.ts.map +1 -0
  15. package/lib/BuildPlan.js +514 -0
  16. package/lib/BuildPlan.js.map +1 -0
  17. package/lib/Container.d.ts +122 -1
  18. package/lib/Container.d.ts.map +1 -1
  19. package/lib/Container.js +232 -4
  20. package/lib/Container.js.map +1 -1
  21. package/lib/Context.d.ts +20 -1
  22. package/lib/Context.d.ts.map +1 -1
  23. package/lib/Context.js +15 -0
  24. package/lib/Context.js.map +1 -1
  25. package/lib/Docker.d.ts +113 -1
  26. package/lib/Docker.d.ts.map +1 -1
  27. package/lib/Docker.js +50 -8
  28. package/lib/Docker.js.map +1 -1
  29. package/lib/Dockerfile.d.ts +0 -2
  30. package/lib/Dockerfile.d.ts.map +1 -1
  31. package/lib/Dockerfile.js.map +1 -1
  32. package/lib/DockerfileParser.d.ts +125 -0
  33. package/lib/DockerfileParser.d.ts.map +1 -0
  34. package/lib/DockerfileParser.js +1040 -0
  35. package/lib/DockerfileParser.js.map +1 -0
  36. package/lib/HealthcheckCommand.d.ts.map +1 -1
  37. package/lib/HealthcheckCommand.js.map +1 -1
  38. package/lib/Image.d.ts +69 -13
  39. package/lib/Image.d.ts.map +1 -1
  40. package/lib/Image.js +59 -9
  41. package/lib/Image.js.map +1 -1
  42. package/lib/ImageArchive.d.ts +41 -0
  43. package/lib/ImageArchive.d.ts.map +1 -0
  44. package/lib/ImageArchive.js +54 -0
  45. package/lib/ImageArchive.js.map +1 -0
  46. package/lib/ImageSource.d.ts +151 -8
  47. package/lib/ImageSource.d.ts.map +1 -1
  48. package/lib/ImageSource.js +247 -72
  49. package/lib/ImageSource.js.map +1 -1
  50. package/lib/Network.d.ts +175 -3
  51. package/lib/Network.d.ts.map +1 -1
  52. package/lib/Network.js +296 -8
  53. package/lib/Network.js.map +1 -1
  54. package/lib/Providers.d.ts.map +1 -1
  55. package/lib/Providers.js.map +1 -1
  56. package/lib/Registry.d.ts.map +1 -1
  57. package/lib/Registry.js.map +1 -1
  58. package/lib/RegistryError.d.ts.map +1 -1
  59. package/lib/RegistryError.js.map +1 -1
  60. package/lib/RemoteImage.d.ts.map +1 -1
  61. package/lib/RemoteImage.js +1 -1
  62. package/lib/RemoteImage.js.map +1 -1
  63. package/lib/Service.d.ts +24 -3
  64. package/lib/Service.d.ts.map +1 -1
  65. package/lib/Service.js +154 -12
  66. package/lib/Service.js.map +1 -1
  67. package/lib/ServiceImage.d.ts.map +1 -1
  68. package/lib/ServiceImage.js.map +1 -1
  69. package/lib/ShellWord.d.ts +28 -0
  70. package/lib/ShellWord.d.ts.map +1 -0
  71. package/lib/ShellWord.js +301 -0
  72. package/lib/ShellWord.js.map +1 -0
  73. package/lib/Swarm.d.ts.map +1 -1
  74. package/lib/Swarm.js.map +1 -1
  75. package/lib/Volume.d.ts +24 -1
  76. package/lib/Volume.d.ts.map +1 -1
  77. package/lib/Volume.js +16 -0
  78. package/lib/Volume.js.map +1 -1
  79. package/lib/index.d.ts.map +1 -1
  80. package/lib/index.js.map +1 -1
  81. package/package.json +9 -9
  82. package/src/Bootstrap.ts +2 -0
  83. package/src/BuildCache.ts +111 -0
  84. package/src/BuildHash.ts +85 -26
  85. package/src/BuildPlan.ts +758 -0
  86. package/src/Container.ts +361 -5
  87. package/src/Context.ts +27 -1
  88. package/src/Docker.ts +157 -9
  89. package/src/Dockerfile.ts +0 -2
  90. package/src/DockerfileParser.ts +1152 -0
  91. package/src/Image.ts +75 -15
  92. package/src/ImageArchive.ts +77 -0
  93. package/src/ImageSource.ts +391 -99
  94. package/src/Network.ts +432 -10
  95. package/src/RemoteImage.ts +1 -1
  96. package/src/Service.ts +246 -62
  97. package/src/ShellWord.ts +326 -0
  98. package/src/Volume.ts +33 -1
@@ -0,0 +1,326 @@
1
+ /**
2
+ * Docker's word processing (BuildKit `frontend/dockerfile/shell/lex.go`),
3
+ * without an environment: quotes and escapes are resolved and every variable
4
+ * reference is kept as written (`$NAME`, `${NAME:-default}`), as BuildKit
5
+ * does with `SkipUnsetEnv`. Expanding variables is left to the builder.
6
+ */
7
+
8
+ import * as Data from "effect/Data";
9
+ import * as Result from "effect/Result";
10
+
11
+ /** A word Docker cannot process: an unterminated quote or a bad `${…}`. */
12
+ export class ShellWordError extends Data.TaggedError("ShellWordError")<{
13
+ readonly message: string;
14
+ }> {}
15
+
16
+ export interface ShellWordOptions {
17
+ readonly escape: string;
18
+ /** Keep quote characters in the result. */
19
+ readonly rawQuotes?: boolean;
20
+ /** Keep escape characters in the result. */
21
+ readonly rawEscapes?: boolean;
22
+ }
23
+
24
+ const EOF = undefined;
25
+ const isSpace = (ch: string) => /\s/u.test(ch);
26
+ const isLetter = (ch: string) => /\p{L}/u.test(ch);
27
+ const isDigit = (ch: string) => /\p{Nd}/u.test(ch);
28
+ const isSpecialParam = (ch: string) => "@*#?-$!0".includes(ch);
29
+ const isWhitespace = (ch: string | undefined) => ch === "\t" || ch === "\r" || ch === " ";
30
+
31
+ /**
32
+ * The word being built. BuildKit's lexer has one per lexer, and every
33
+ * `processStopOn` resets it, including the nested ones inside `${…}`: so
34
+ * `he${X:-y}z` splits to `${X:-y}z`. Docker splits words this way, so we do.
35
+ */
36
+ interface WordBuffer {
37
+ value: string;
38
+ }
39
+
40
+ class Words {
41
+ private inWord = false;
42
+ constructor(private readonly shared: WordBuffer) {}
43
+ private get buf() {
44
+ return this.shared.value;
45
+ }
46
+ private set buf(value: string) {
47
+ this.shared.value = value;
48
+ }
49
+ readonly words: string[] = [];
50
+ addChar(ch: string) {
51
+ if (isSpace(ch) && this.inWord) {
52
+ if (this.buf.length !== 0) {
53
+ this.words.push(this.buf);
54
+ this.buf = "";
55
+ this.inWord = false;
56
+ }
57
+ } else if (!isSpace(ch)) {
58
+ this.addRawChar(ch);
59
+ }
60
+ }
61
+ addRawChar(ch: string) {
62
+ this.buf += ch;
63
+ this.inWord = true;
64
+ }
65
+ addString(str: string) {
66
+ for (const ch of str) this.addChar(ch);
67
+ }
68
+ addRawString(str: string) {
69
+ this.buf += str;
70
+ this.inWord = true;
71
+ }
72
+ get(): string[] {
73
+ if (this.buf.length > 0) {
74
+ this.words.push(this.buf);
75
+ this.buf = "";
76
+ this.inWord = false;
77
+ }
78
+ return this.words;
79
+ }
80
+ }
81
+
82
+ class Lexer {
83
+ private readonly chars: string[];
84
+ private pos = 0;
85
+ private rawEscapes: boolean;
86
+ private readonly wordBuffer: WordBuffer = { value: "" };
87
+ /** The first error. Failing also ends the input, so every loop stops. */
88
+ error: ShellWordError | undefined;
89
+
90
+ constructor(
91
+ input: string,
92
+ private readonly options: ShellWordOptions,
93
+ ) {
94
+ this.chars = Array.from(input);
95
+ this.rawEscapes = options.rawEscapes ?? false;
96
+ }
97
+
98
+ private peek(): string | undefined {
99
+ return this.chars[this.pos];
100
+ }
101
+ private next(): string | undefined {
102
+ return this.chars[this.pos++];
103
+ }
104
+ private fail(message: string): string {
105
+ this.error ??= new ShellWordError({ message });
106
+ this.pos = this.chars.length;
107
+ return "";
108
+ }
109
+
110
+ process(): { result: string; words: string[] } {
111
+ return this.processStopOn(EOF, this.rawEscapes, false);
112
+ }
113
+
114
+ private processStopOn(
115
+ stop: string | undefined,
116
+ rawEscapes: boolean,
117
+ preservePatternEscapes: boolean,
118
+ ): { result: string; words: string[] } {
119
+ let result = "";
120
+ this.wordBuffer.value = "";
121
+ const words = new Words(this.wordBuffer);
122
+ const saved = this.rawEscapes;
123
+ this.rawEscapes = rawEscapes;
124
+ while (this.peek() !== EOF) {
125
+ const ch = this.peek()!;
126
+ if (stop !== EOF && ch === stop) {
127
+ this.next();
128
+ this.rawEscapes = saved;
129
+ return { result, words: words.get() };
130
+ }
131
+ if (ch === "$") {
132
+ const tmp = this.processDollar();
133
+ result += tmp;
134
+ words.addString(tmp);
135
+ } else if (ch === "<") {
136
+ const tmp = this.processPossibleHeredoc();
137
+ result += tmp;
138
+ words.addRawString(tmp);
139
+ } else if (ch === "'") {
140
+ const tmp = this.processSingleQuote(preservePatternEscapes);
141
+ result += tmp;
142
+ words.addRawString(tmp);
143
+ } else if (ch === '"') {
144
+ const tmp = this.processDoubleQuote(preservePatternEscapes);
145
+ result += tmp;
146
+ words.addRawString(tmp);
147
+ } else {
148
+ let c = this.next()!;
149
+ if (c === this.options.escape) {
150
+ if (this.rawEscapes) {
151
+ words.addRawChar(c);
152
+ result += c;
153
+ }
154
+ const escaped = this.next();
155
+ if (escaped === EOF) break;
156
+ c = escaped;
157
+ words.addRawChar(c);
158
+ } else {
159
+ words.addChar(c);
160
+ }
161
+ result += c;
162
+ }
163
+ }
164
+ this.rawEscapes = saved;
165
+ if (stop !== EOF) {
166
+ this.fail(`unexpected end of statement while looking for matching ${stop}`);
167
+ }
168
+ return { result, words: words.get() };
169
+ }
170
+
171
+ private processSingleQuote(preservePatternEscapes: boolean): string {
172
+ let result = "";
173
+ const open = this.next()!;
174
+ if (this.options.rawQuotes) result += open;
175
+ for (;;) {
176
+ const ch = this.next();
177
+ if (ch === EOF) {
178
+ return this.fail("unexpected end of statement while looking for matching single-quote");
179
+ }
180
+ if (ch === "'") {
181
+ if (this.options.rawQuotes) result += ch;
182
+ return result;
183
+ }
184
+ if (preservePatternEscapes && ch === this.options.escape) result += ch;
185
+ result += ch;
186
+ }
187
+ }
188
+
189
+ private processDoubleQuote(preservePatternEscapes: boolean): string {
190
+ let result = "";
191
+ const open = this.next()!;
192
+ if (this.options.rawQuotes) result += open;
193
+ for (;;) {
194
+ const peeked = this.peek();
195
+ if (peeked === EOF) {
196
+ return this.fail("unexpected end of statement while looking for matching double-quote");
197
+ }
198
+ if (peeked === '"') {
199
+ const close = this.next()!;
200
+ if (this.options.rawQuotes) result += close;
201
+ return result;
202
+ }
203
+ if (peeked === "$") {
204
+ result += this.processDollar();
205
+ continue;
206
+ }
207
+ let ch = this.next()!;
208
+ if (ch === this.options.escape) {
209
+ const after = this.peek();
210
+ if (after === EOF) continue;
211
+ if (after === '"' || after === "$" || after === this.options.escape) {
212
+ if (this.rawEscapes) result += ch;
213
+ ch = this.next()!;
214
+ } else if (this.rawEscapes && (!this.options.rawQuotes || preservePatternEscapes)) {
215
+ result += ch;
216
+ }
217
+ }
218
+ result += ch;
219
+ }
220
+ }
221
+
222
+ private processDollar(): string {
223
+ this.next();
224
+ if (this.peek() !== "{") {
225
+ const name = this.processName();
226
+ return name === "" ? "$" : `$${name}`;
227
+ }
228
+ this.next();
229
+ const first = this.peek();
230
+ if (first === EOF) return this.fail("syntax error: missing '}'");
231
+ if (first === "{" || first === "}" || first === ":") {
232
+ return this.fail("syntax error: bad substitution");
233
+ }
234
+ const name = this.processName();
235
+ let ch = this.next();
236
+ let chs = ch ?? "\uFFFD";
237
+ let nullIsUnset = false;
238
+ switch (ch) {
239
+ case "}":
240
+ return `\${${name}}`;
241
+ case ":":
242
+ case "+":
243
+ case "-":
244
+ case "?":
245
+ case "#":
246
+ case "%": {
247
+ if (ch === ":") {
248
+ nullIsUnset = true;
249
+ ch = this.next();
250
+ chs += ch ?? "\uFFFD";
251
+ }
252
+ const raw = ch === "#" || ch === "%";
253
+ if (nullIsUnset && raw) {
254
+ return this.fail(`unsupported modifier (${chs}) in substitution`);
255
+ }
256
+ const word = this.substitutionPart("}", raw, raw, "syntax error: missing '}'");
257
+ return `\${${name}${chs}${word}}`;
258
+ }
259
+ case "/": {
260
+ if (this.peek() === "/") this.next();
261
+ const pattern = this.substitutionPart("/", true, true, "syntax error: missing '/' in ${}");
262
+ const replacement = this.substitutionPart("}", true, false, "syntax error: missing '}'");
263
+ return `\${${name}/${pattern}/${replacement}}`;
264
+ }
265
+ default:
266
+ return this.fail(`unsupported modifier (${chs}) in substitution`);
267
+ }
268
+ }
269
+
270
+ private substitutionPart(
271
+ stop: string,
272
+ rawEscapes: boolean,
273
+ preservePatternEscapes: boolean,
274
+ missing: string,
275
+ ): string {
276
+ const before = this.error;
277
+ const part = this.processStopOn(stop, rawEscapes, preservePatternEscapes).result;
278
+ // An unterminated part reports the missing closer, as BuildKit does.
279
+ if (before === undefined && this.error?.message.startsWith("unexpected end")) {
280
+ this.error = new ShellWordError({ message: missing });
281
+ }
282
+ return part;
283
+ }
284
+
285
+ private processName(): string {
286
+ let name = "";
287
+ while (this.peek() !== EOF) {
288
+ const ch = this.peek()!;
289
+ if (name.length === 0 && isDigit(ch)) {
290
+ while (this.peek() !== EOF && isDigit(this.peek()!)) name += this.next();
291
+ return name;
292
+ }
293
+ if (name.length === 0 && isSpecialParam(ch)) return this.next()!;
294
+ if (!isLetter(ch) && !isDigit(ch) && ch !== "_") break;
295
+ name += this.next();
296
+ }
297
+ return name;
298
+ }
299
+
300
+ private processPossibleHeredoc(): string {
301
+ this.next();
302
+ if (this.peek() !== "<") return "<";
303
+ this.next();
304
+ let space = "";
305
+ while (isWhitespace(this.peek())) space += this.next();
306
+ return `<<${space}`;
307
+ }
308
+ }
309
+
310
+ const run = <A>(
311
+ input: string,
312
+ options: ShellWordOptions,
313
+ pick: (out: { result: string; words: string[] }) => A,
314
+ ): Result.Result<A, ShellWordError> => {
315
+ const lexer = new Lexer(input, options);
316
+ const out = lexer.process();
317
+ return lexer.error === undefined ? Result.succeed(pick(out)) : Result.fail(lexer.error);
318
+ };
319
+
320
+ /** The word with quotes and escapes resolved. */
321
+ export const processWord = (word: string, options: ShellWordOptions) =>
322
+ run(word, options, (out) => out.result);
323
+
324
+ /** The input split into words, as a shell would. */
325
+ export const processWords = (input: string, options: ShellWordOptions) =>
326
+ run(input, options, (out) => out.words);
package/src/Volume.ts CHANGED
@@ -52,9 +52,24 @@ export interface Volume extends Resource<
52
52
  createdAt: number;
53
53
  },
54
54
  never,
55
- Providers
55
+ Providers,
56
+ VolumeLookup
56
57
  > {}
57
58
 
59
+ /**
60
+ * Props for {@link Volume.lookup}: find a volume this stack
61
+ * doesn't manage by its name.
62
+ */
63
+ export interface VolumeLookup {
64
+ /** Volume name. */
65
+ name: string;
66
+ /**
67
+ * Docker context the engine is reached through.
68
+ * @default the current context
69
+ */
70
+ context?: string;
71
+ }
72
+
58
73
  /**
59
74
  * A Docker volume managed through the active Docker context.
60
75
  *
@@ -99,6 +114,16 @@ export interface Volume extends Resource<
99
114
  * });
100
115
  * ```
101
116
  *
117
+ * ### Looking up an existing volume
118
+ * `Volume.lookup` references a volume Alchemy doesn't manage (created with
119
+ * the Docker CLI, Compose or another stack). It is read during plan/deploy,
120
+ * never created, updated or removed.
121
+ *
122
+ * **Example:** Reference an existing volume
123
+ * ```typescript
124
+ * const data = Docker.Volume.lookup({ name: "pgdata" });
125
+ * ```
126
+ *
102
127
  * @resource
103
128
  * @product Volume
104
129
  */
@@ -112,6 +137,13 @@ export const VolumeProvider = () =>
112
137
 
113
138
  return Volume.Provider.of({
114
139
  list: () => Effect.succeed([]),
140
+ lookup: Effect.fn(function* ({ name, context }) {
141
+ const info = yield* docker.volume
142
+ .inspect(name, context)
143
+ .pipe(Effect.catchReason("PlatformError", "NotFound", () => Effect.undefined));
144
+ return info ? toVolumeAttributes(info) : undefined;
145
+ }),
146
+
115
147
  read: Effect.fn(function* ({ id, instanceId, olds, output }) {
116
148
  const context = dockerContextName(olds?.context);
117
149
  const name = yield* dockerPhysicalName(id, olds, instanceId);