endia 0.0.0-stage → 0.0.2
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/LICENSE +202 -0
- package/README.md +34 -2
- package/dist/archives/archive-file.d.ts +36 -0
- package/dist/archives/archive-file.js +71 -0
- package/dist/archives/build-context.d.ts +63 -0
- package/dist/archives/build-context.js +132 -0
- package/dist/archives/docker-ignore.d.ts +57 -0
- package/dist/archives/docker-ignore.js +250 -0
- package/dist/archives/fleet-archive.d.ts +33 -0
- package/dist/archives/fleet-archive.js +134 -0
- package/dist/compose-model.d.ts +32 -0
- package/dist/compose-model.js +39 -0
- package/dist/contract/archives.d.ts +14 -0
- package/dist/contract/archives.js +1 -0
- package/dist/contract/bases.d.ts +48 -0
- package/dist/contract/bases.js +1 -0
- package/dist/contract/clients.d.ts +29 -0
- package/dist/contract/clients.js +1 -0
- package/dist/contract/common.d.ts +30 -0
- package/dist/contract/common.js +1 -0
- package/dist/contract/compose.d.ts +27 -0
- package/dist/contract/compose.js +1 -0
- package/dist/contract/desktop.d.ts +269 -0
- package/dist/contract/desktop.js +1 -0
- package/dist/contract/execs.d.ts +61 -0
- package/dist/contract/execs.js +1 -0
- package/dist/contract/fleets.d.ts +136 -0
- package/dist/contract/fleets.js +1 -0
- package/dist/contract/images.d.ts +130 -0
- package/dist/contract/images.js +1 -0
- package/dist/contract/machines.d.ts +199 -0
- package/dist/contract/machines.js +1 -0
- package/dist/contract/operations.d.ts +28 -0
- package/dist/contract/operations.js +1 -0
- package/dist/contract/processes.d.ts +75 -0
- package/dist/contract/processes.js +1 -0
- package/dist/contract/purposes.d.ts +62 -0
- package/dist/contract/purposes.js +1 -0
- package/dist/contract/space.d.ts +114 -0
- package/dist/contract/space.js +1 -0
- package/dist/contract/stats.d.ts +58 -0
- package/dist/contract/stats.js +1 -0
- package/dist/contract/status.d.ts +151 -0
- package/dist/contract/status.js +1 -0
- package/dist/contract/volumes.d.ts +22 -0
- package/dist/contract/volumes.js +1 -0
- package/dist/eastindia-client.d.ts +118 -0
- package/dist/eastindia-client.js +142 -0
- package/dist/environment.d.ts +52 -0
- package/dist/environment.js +133 -0
- package/dist/errors.d.ts +81 -0
- package/dist/errors.js +108 -0
- package/dist/exec/exec-frame.d.ts +38 -0
- package/dist/exec/exec-frame.js +50 -0
- package/dist/exec/exec-session.d.ts +64 -0
- package/dist/exec/exec-session.js +117 -0
- package/dist/groups/archives.d.ts +78 -0
- package/dist/groups/archives.js +117 -0
- package/dist/groups/bases.d.ts +28 -0
- package/dist/groups/bases.js +39 -0
- package/dist/groups/certificates.d.ts +32 -0
- package/dist/groups/certificates.js +43 -0
- package/dist/groups/desktop.d.ts +143 -0
- package/dist/groups/desktop.js +182 -0
- package/dist/groups/execs.d.ts +110 -0
- package/dist/groups/execs.js +119 -0
- package/dist/groups/fleets.d.ts +206 -0
- package/dist/groups/fleets.js +255 -0
- package/dist/groups/images.d.ts +130 -0
- package/dist/groups/images.js +109 -0
- package/dist/groups/machines.d.ts +124 -0
- package/dist/groups/machines.js +127 -0
- package/dist/groups/operations.d.ts +127 -0
- package/dist/groups/operations.js +174 -0
- package/dist/groups/processes.d.ts +77 -0
- package/dist/groups/processes.js +75 -0
- package/dist/groups/stats.d.ts +29 -0
- package/dist/groups/stats.js +33 -0
- package/dist/groups/system.d.ts +33 -0
- package/dist/groups/system.js +40 -0
- package/dist/groups/volumes.d.ts +36 -0
- package/dist/groups/volumes.js +40 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +20 -0
- package/dist/purposes.d.ts +17 -0
- package/dist/purposes.js +21 -0
- package/dist/routes.d.ts +71 -0
- package/dist/routes.js +73 -0
- package/dist/transport/api-connection.d.ts +185 -0
- package/dist/transport/api-connection.js +396 -0
- package/dist/transport/connection-failures.d.ts +14 -0
- package/dist/transport/connection-failures.js +57 -0
- package/dist/transport/json-lines.d.ts +10 -0
- package/dist/transport/json-lines.js +23 -0
- package/dist/transport/query-text.d.ts +49 -0
- package/dist/transport/query-text.js +62 -0
- package/dist/transport/service-address.d.ts +30 -0
- package/dist/transport/service-address.js +37 -0
- package/dist/transport/tls-files.d.ts +9 -0
- package/dist/transport/tls-files.js +1 -0
- package/package.json +44 -5
package/LICENSE
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
+
|
|
181
|
+
To apply the Apache License to your work, attach the following
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
+
replaced with your own identifying information. (Don't include
|
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
+
comment syntax for the file format. We also recommend that a
|
|
186
|
+
file or class name and description of purpose be included on the
|
|
187
|
+
same "printed page" as the copyright notice for easier
|
|
188
|
+
identification within third-party archives.
|
|
189
|
+
|
|
190
|
+
Copyright [yyyy] [name of copyright owner]
|
|
191
|
+
|
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
+
you may not use this file except in compliance with the License.
|
|
194
|
+
You may obtain a copy of the License at
|
|
195
|
+
|
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
+
|
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
+
See the License for the specific language governing permissions and
|
|
202
|
+
limitations under the License.
|
package/README.md
CHANGED
|
@@ -1,3 +1,35 @@
|
|
|
1
|
-
#
|
|
1
|
+
# endia
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Client for EastIndia, which runs Windows desktop test machines that tests drive like Docker containers. Early preview: the product is not released yet.
|
|
4
|
+
|
|
5
|
+
The package needs Node 20 or later and ships ES modules with TypeScript declarations. It calls the EastIndia service's HTTPS API, so a Node test runner can build images, bring fleets of machines up and down, and run programs in them without the `endia` command line.
|
|
6
|
+
|
|
7
|
+
## Example
|
|
8
|
+
|
|
9
|
+
`ledger/Endiafile` holds a recipe of two stages; each stage begins with a `FROM`, and `AS` names it:
|
|
10
|
+
|
|
11
|
+
```dockerfile
|
|
12
|
+
FROM eastindia/development:latest AS tools
|
|
13
|
+
RUN choco install -y git
|
|
14
|
+
|
|
15
|
+
FROM tools AS app
|
|
16
|
+
COPY publish/ C:/app/
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```js
|
|
20
|
+
import { EastIndiaClient } from "endia";
|
|
21
|
+
|
|
22
|
+
// Connects where the endia command line would: EASTINDIA_HOST and EASTINDIA_CERT_PATH, or its current context.
|
|
23
|
+
const client = EastIndiaClient.fromEnv();
|
|
24
|
+
console.log((await client.version()).version);
|
|
25
|
+
|
|
26
|
+
const build = await client.images.build({ folder: "./ledger" }, { tags: ["lab/ledger:1"] });
|
|
27
|
+
const built = await build.result({ onLog: (text) => process.stdout.write(text) });
|
|
28
|
+
console.log(built.digest);
|
|
29
|
+
|
|
30
|
+
// A recipe of several stages: target names the stage to build, here the first.
|
|
31
|
+
const tools = await (await client.images.build({ folder: "./ledger" }, { target: "tools", tags: ["lab/tools:1"] })).result();
|
|
32
|
+
console.log(tools.stages?.map((stage) => [stage.name, stage.cacheHit]));
|
|
33
|
+
|
|
34
|
+
client.close();
|
|
35
|
+
```
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { RequestBody } from "../transport/api-connection.js";
|
|
2
|
+
/** One entry of an archive: the file, folder or link on disk, and the name the entry takes in the archive. */
|
|
3
|
+
export interface ArchiveEntry {
|
|
4
|
+
/** The full path on disk. A link is archived as a link, never as what it points at. */
|
|
5
|
+
readonly source: string;
|
|
6
|
+
/** The name in the archive, with `/` separators; a folder's name ends in `/`. */
|
|
7
|
+
readonly name: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* A tar archive written to a temporary file, so that a request that is sent again after a failed connection sends the same bytes.
|
|
11
|
+
* The archive belongs to the caller that wrote it, which deletes it with `delete` once the request has been answered.
|
|
12
|
+
*/
|
|
13
|
+
export declare class ArchiveFile {
|
|
14
|
+
private readonly folder;
|
|
15
|
+
/** The archive's full path. */
|
|
16
|
+
readonly path: string;
|
|
17
|
+
/** The archive's size in bytes. */
|
|
18
|
+
readonly size: number;
|
|
19
|
+
private constructor();
|
|
20
|
+
/**
|
|
21
|
+
* Writes an archive of the given entries, in the given order, in the POSIX pax format with UTF-8 names.
|
|
22
|
+
*
|
|
23
|
+
* @param entries - The entries. A folder entry holds only itself; each file under it needs its own entry.
|
|
24
|
+
* @param texts - Files that exist only in memory, such as a resolved Compose model, each written under its name.
|
|
25
|
+
* @param followLinks - Whether a link is written as what it points at, rather than as a link.
|
|
26
|
+
* @returns The archive.
|
|
27
|
+
*/
|
|
28
|
+
static write(entries: readonly ArchiveEntry[], texts?: readonly {
|
|
29
|
+
name: string;
|
|
30
|
+
text: string;
|
|
31
|
+
}[], followLinks?: boolean): Promise<ArchiveFile>;
|
|
32
|
+
/** The archive as a request body, opened afresh for each attempt. */
|
|
33
|
+
get body(): RequestBody;
|
|
34
|
+
/** Deletes the archive and its temporary folder. */
|
|
35
|
+
delete(): Promise<void>;
|
|
36
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { createReadStream } from "node:fs";
|
|
2
|
+
import { mkdtemp, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { create } from "tar";
|
|
6
|
+
import { MediaTypes } from "../routes.js";
|
|
7
|
+
/** The prefix of the temporary folder that holds an archive while it is sent. */
|
|
8
|
+
const TemporaryFolderPrefix = "eastindia-client-";
|
|
9
|
+
/** The file name of the archive inside its temporary folder. */
|
|
10
|
+
const ArchiveFileName = "archive.tar";
|
|
11
|
+
/**
|
|
12
|
+
* A tar archive written to a temporary file, so that a request that is sent again after a failed connection sends the same bytes.
|
|
13
|
+
* The archive belongs to the caller that wrote it, which deletes it with `delete` once the request has been answered.
|
|
14
|
+
*/
|
|
15
|
+
export class ArchiveFile {
|
|
16
|
+
folder;
|
|
17
|
+
/** The archive's full path. */
|
|
18
|
+
path;
|
|
19
|
+
/** The archive's size in bytes. */
|
|
20
|
+
size;
|
|
21
|
+
constructor(folder, size) {
|
|
22
|
+
this.folder = folder;
|
|
23
|
+
this.path = path.join(folder, ArchiveFileName);
|
|
24
|
+
this.size = size;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Writes an archive of the given entries, in the given order, in the POSIX pax format with UTF-8 names.
|
|
28
|
+
*
|
|
29
|
+
* @param entries - The entries. A folder entry holds only itself; each file under it needs its own entry.
|
|
30
|
+
* @param texts - Files that exist only in memory, such as a resolved Compose model, each written under its name.
|
|
31
|
+
* @param followLinks - Whether a link is written as what it points at, rather than as a link.
|
|
32
|
+
* @returns The archive.
|
|
33
|
+
*/
|
|
34
|
+
static async write(entries, texts = [], followLinks = false) {
|
|
35
|
+
const folder = await mkdtemp(path.join(tmpdir(), TemporaryFolderPrefix));
|
|
36
|
+
try {
|
|
37
|
+
// The texts are written beside the archive and added like any other file, under their own names.
|
|
38
|
+
const written = await Promise.all(texts.map(async (entry, index) => {
|
|
39
|
+
const source = path.join(folder, `text-${String(index)}`);
|
|
40
|
+
await writeFile(source, entry.text, "utf8");
|
|
41
|
+
return { source, name: entry.name };
|
|
42
|
+
}));
|
|
43
|
+
const all = [...written, ...entries];
|
|
44
|
+
const names = new Map(all.map((entry) => [path.resolve(entry.source), entry.name]));
|
|
45
|
+
await create({
|
|
46
|
+
file: path.join(folder, ArchiveFileName),
|
|
47
|
+
preservePaths: true,
|
|
48
|
+
noDirRecurse: true,
|
|
49
|
+
follow: followLinks,
|
|
50
|
+
// Each entry is read from its full path and renamed to its name in the archive.
|
|
51
|
+
onWriteEntry: (entry) => {
|
|
52
|
+
entry.path = names.get(entry.absolute) ?? entry.path;
|
|
53
|
+
},
|
|
54
|
+
}, all.map((entry) => entry.source));
|
|
55
|
+
const size = (await stat(path.join(folder, ArchiveFileName))).size;
|
|
56
|
+
return new ArchiveFile(folder, size);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
await rm(folder, { recursive: true, force: true });
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** The archive as a request body, opened afresh for each attempt. */
|
|
64
|
+
get body() {
|
|
65
|
+
return { contentType: MediaTypes.tar, open: () => createReadStream(this.path), length: this.size };
|
|
66
|
+
}
|
|
67
|
+
/** Deletes the archive and its temporary folder. */
|
|
68
|
+
async delete() {
|
|
69
|
+
await rm(this.folder, { recursive: true, force: true });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { ArchiveFile, type ArchiveEntry } from "./archive-file.js";
|
|
2
|
+
/** The archive of a build context, and the recipe's path inside it, which the build request names as `dockerfile`. */
|
|
3
|
+
export interface PackedContext {
|
|
4
|
+
readonly archive: ArchiveFile;
|
|
5
|
+
readonly recipe: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Packs a build context folder as `endia build` sends it: a tar archive of every file, folder and link in the folder that the
|
|
9
|
+
* ignore file does not exclude. A link is archived as a link, never as what it points at. The recipe and the ignore file are always
|
|
10
|
+
* sent.
|
|
11
|
+
*
|
|
12
|
+
* The ignore file is chosen as `ignoreFileFor` chooses it. A recipe outside the folder is added at the archive's root as
|
|
13
|
+
* `.dockerfile.` followed by 16 random hexadecimal digits, as Docker adds it, with its own ignore file beside it under that name
|
|
14
|
+
* followed by `.endiaignore` or `.dockerignore`, as the file on disk is named.
|
|
15
|
+
*
|
|
16
|
+
* @param folder - The context folder.
|
|
17
|
+
* @param recipe - The recipe's path, inside the folder or outside it; when undefined, `Endiafile` in the folder when it exists,
|
|
18
|
+
* and `Dockerfile` otherwise.
|
|
19
|
+
* @returns The archive and the recipe's path inside it; the caller deletes the archive.
|
|
20
|
+
* @throws Error when the folder does not exist, when a recipe outside the folder does not exist, or when the ignore file holds a
|
|
21
|
+
* malformed pattern.
|
|
22
|
+
*/
|
|
23
|
+
export declare function packBuildContext(folder: string, recipe?: string): Promise<PackedContext>;
|
|
24
|
+
/**
|
|
25
|
+
* Lists the entries of a context folder that a build sends, each folder followed by what it holds, in ordinal order of name within
|
|
26
|
+
* each folder. `compose up` sends each service's build context with the same entries.
|
|
27
|
+
*
|
|
28
|
+
* @param root - The context folder's full path.
|
|
29
|
+
* @param recipe - The recipe's path inside the folder, with `/` separators, or undefined when it lies outside.
|
|
30
|
+
* @param ignoreFile - The ignore file's full path, which may not exist.
|
|
31
|
+
* @returns Each entry's full path and its path inside the folder; a folder's path ends in `/`.
|
|
32
|
+
*/
|
|
33
|
+
export declare function contextEntries(root: string, recipe: string | undefined, ignoreFile: string): Promise<ArchiveEntry[]>;
|
|
34
|
+
/**
|
|
35
|
+
* Gives the recipe of a build that names none, as the `endia` CLI chooses it: `Endiafile` at the root of the context folder when it
|
|
36
|
+
* exists, and `Dockerfile` otherwise.
|
|
37
|
+
*
|
|
38
|
+
* @param root - The context folder's full path.
|
|
39
|
+
* @returns The recipe's file name.
|
|
40
|
+
*/
|
|
41
|
+
export declare function defaultRecipe(root: string): string;
|
|
42
|
+
/**
|
|
43
|
+
* Gives the ignore file that applies to a recipe, as the `endia` CLI chooses it: the first of these that exists. A recipe's own
|
|
44
|
+
* ignore file is named as the recipe's file name followed by the ignore file's name, beside the recipe.
|
|
45
|
+
*
|
|
46
|
+
* 1. The recipe's own `.endiaignore`.
|
|
47
|
+
* 2. The recipe's own `.dockerignore`.
|
|
48
|
+
* 3. `.endiaignore` at the root of the folder.
|
|
49
|
+
* 4. `.dockerignore` at the root of the folder.
|
|
50
|
+
*
|
|
51
|
+
* @param root - The context folder's full path.
|
|
52
|
+
* @param recipe - The recipe's full path.
|
|
53
|
+
* @returns The ignore file's full path; `.dockerignore` at the root, which does not exist, when none of the four exists.
|
|
54
|
+
*/
|
|
55
|
+
export declare function ignoreFileFor(root: string, recipe: string): string;
|
|
56
|
+
/**
|
|
57
|
+
* Gives a path's place inside a folder.
|
|
58
|
+
*
|
|
59
|
+
* @param root - The folder's full path.
|
|
60
|
+
* @param target - A full path.
|
|
61
|
+
* @returns The path inside the folder with `/` separators, or undefined when it lies outside the folder.
|
|
62
|
+
*/
|
|
63
|
+
export declare function pathInside(root: string, target: string): string | undefined;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { existsSync, statSync } from "node:fs";
|
|
3
|
+
import { lstat, readdir, readFile } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { ArchiveFile } from "./archive-file.js";
|
|
6
|
+
import { DockerIgnore, EndiaIgnoreFileName, IgnoreFileName } from "./docker-ignore.js";
|
|
7
|
+
/** EastIndia's name for a recipe, which a build that names none reads first. */
|
|
8
|
+
const Endiafile = "Endiafile";
|
|
9
|
+
/** Docker's name for a recipe, which a build that names none reads when the context holds no `Endiafile`. */
|
|
10
|
+
const Dockerfile = "Dockerfile";
|
|
11
|
+
/** The name prefix of a recipe that lies outside the context folder, which Docker adds at the archive's root under a random name. */
|
|
12
|
+
const OutsideRecipePrefix = ".dockerfile.";
|
|
13
|
+
/** The number of random bytes in that name; each byte gives two hexadecimal digits, so the name ends in 16 digits. */
|
|
14
|
+
const OutsideRecipeRandomBytes = 8;
|
|
15
|
+
/**
|
|
16
|
+
* Packs a build context folder as `endia build` sends it: a tar archive of every file, folder and link in the folder that the
|
|
17
|
+
* ignore file does not exclude. A link is archived as a link, never as what it points at. The recipe and the ignore file are always
|
|
18
|
+
* sent.
|
|
19
|
+
*
|
|
20
|
+
* The ignore file is chosen as `ignoreFileFor` chooses it. A recipe outside the folder is added at the archive's root as
|
|
21
|
+
* `.dockerfile.` followed by 16 random hexadecimal digits, as Docker adds it, with its own ignore file beside it under that name
|
|
22
|
+
* followed by `.endiaignore` or `.dockerignore`, as the file on disk is named.
|
|
23
|
+
*
|
|
24
|
+
* @param folder - The context folder.
|
|
25
|
+
* @param recipe - The recipe's path, inside the folder or outside it; when undefined, `Endiafile` in the folder when it exists,
|
|
26
|
+
* and `Dockerfile` otherwise.
|
|
27
|
+
* @returns The archive and the recipe's path inside it; the caller deletes the archive.
|
|
28
|
+
* @throws Error when the folder does not exist, when a recipe outside the folder does not exist, or when the ignore file holds a
|
|
29
|
+
* malformed pattern.
|
|
30
|
+
*/
|
|
31
|
+
export async function packBuildContext(folder, recipe) {
|
|
32
|
+
const root = path.resolve(folder);
|
|
33
|
+
if (!existsSync(root) || !statSync(root).isDirectory()) {
|
|
34
|
+
throw new Error(`The build context '${folder}' is not a folder.`);
|
|
35
|
+
}
|
|
36
|
+
const recipePath = path.resolve(root, recipe ?? defaultRecipe(root));
|
|
37
|
+
const inside = pathInside(root, recipePath);
|
|
38
|
+
if (inside === undefined && !existsSync(recipePath)) {
|
|
39
|
+
throw new Error(`The recipe '${recipePath}' does not exist.`);
|
|
40
|
+
}
|
|
41
|
+
const ignoreFile = ignoreFileFor(root, recipePath);
|
|
42
|
+
const entries = await contextEntries(root, inside, ignoreFile);
|
|
43
|
+
const recipeName = inside ?? OutsideRecipePrefix + randomBytes(OutsideRecipeRandomBytes).toString("hex");
|
|
44
|
+
if (inside === undefined) {
|
|
45
|
+
entries.push({ source: recipePath, name: recipeName });
|
|
46
|
+
// Only a recipe's own ignore file lies outside the folder; the root one is already among the entries.
|
|
47
|
+
if (pathInside(root, ignoreFile) === undefined) {
|
|
48
|
+
entries.push({ source: ignoreFile, name: recipeName + ignoreFile.slice(recipePath.length) });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return { archive: await ArchiveFile.write(entries), recipe: recipeName };
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Lists the entries of a context folder that a build sends, each folder followed by what it holds, in ordinal order of name within
|
|
55
|
+
* each folder. `compose up` sends each service's build context with the same entries.
|
|
56
|
+
*
|
|
57
|
+
* @param root - The context folder's full path.
|
|
58
|
+
* @param recipe - The recipe's path inside the folder, with `/` separators, or undefined when it lies outside.
|
|
59
|
+
* @param ignoreFile - The ignore file's full path, which may not exist.
|
|
60
|
+
* @returns Each entry's full path and its path inside the folder; a folder's path ends in `/`.
|
|
61
|
+
*/
|
|
62
|
+
export async function contextEntries(root, recipe, ignoreFile) {
|
|
63
|
+
const ignore = existsSync(ignoreFile) ? DockerIgnore.parse(await readFile(ignoreFile, "utf8"), ignoreFile) : DockerIgnore.empty;
|
|
64
|
+
const alwaysSent = new Set([recipe, pathInside(root, ignoreFile)].filter((name) => name !== undefined));
|
|
65
|
+
const entries = [];
|
|
66
|
+
await collectEntries(root, "", ignore, alwaysSent, entries);
|
|
67
|
+
return entries;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Gives the recipe of a build that names none, as the `endia` CLI chooses it: `Endiafile` at the root of the context folder when it
|
|
71
|
+
* exists, and `Dockerfile` otherwise.
|
|
72
|
+
*
|
|
73
|
+
* @param root - The context folder's full path.
|
|
74
|
+
* @returns The recipe's file name.
|
|
75
|
+
*/
|
|
76
|
+
export function defaultRecipe(root) {
|
|
77
|
+
return isFile(path.join(root, Endiafile)) ? Endiafile : Dockerfile;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Gives the ignore file that applies to a recipe, as the `endia` CLI chooses it: the first of these that exists. A recipe's own
|
|
81
|
+
* ignore file is named as the recipe's file name followed by the ignore file's name, beside the recipe.
|
|
82
|
+
*
|
|
83
|
+
* 1. The recipe's own `.endiaignore`.
|
|
84
|
+
* 2. The recipe's own `.dockerignore`.
|
|
85
|
+
* 3. `.endiaignore` at the root of the folder.
|
|
86
|
+
* 4. `.dockerignore` at the root of the folder.
|
|
87
|
+
*
|
|
88
|
+
* @param root - The context folder's full path.
|
|
89
|
+
* @param recipe - The recipe's full path.
|
|
90
|
+
* @returns The ignore file's full path; `.dockerignore` at the root, which does not exist, when none of the four exists.
|
|
91
|
+
*/
|
|
92
|
+
export function ignoreFileFor(root, recipe) {
|
|
93
|
+
const candidates = [recipe + EndiaIgnoreFileName, recipe + IgnoreFileName, path.join(root, EndiaIgnoreFileName), path.join(root, IgnoreFileName)];
|
|
94
|
+
return candidates.find(isFile) ?? path.join(root, IgnoreFileName);
|
|
95
|
+
}
|
|
96
|
+
function isFile(file) {
|
|
97
|
+
return existsSync(file) && statSync(file).isFile();
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Gives a path's place inside a folder.
|
|
101
|
+
*
|
|
102
|
+
* @param root - The folder's full path.
|
|
103
|
+
* @param target - A full path.
|
|
104
|
+
* @returns The path inside the folder with `/` separators, or undefined when it lies outside the folder.
|
|
105
|
+
*/
|
|
106
|
+
export function pathInside(root, target) {
|
|
107
|
+
const relative = path.relative(root, target).split(path.sep).join("/");
|
|
108
|
+
return relative === ".." || relative.startsWith("../") || path.isAbsolute(relative) ? undefined : relative;
|
|
109
|
+
}
|
|
110
|
+
// Lists a folder's entries and, for each sub-folder that needs it, the sub-folder's entries. A sub-folder the ignore file leaves out
|
|
111
|
+
// is still listed when the recipe or the ignore file lies under it, or when the ignore file has a `!` line, which could re-include a
|
|
112
|
+
// file under it; Docker's own walk does the same. A link is never followed.
|
|
113
|
+
async function collectEntries(folder, prefix, ignore, alwaysSent, entries) {
|
|
114
|
+
const names = (await readdir(folder)).sort(compareOrdinal);
|
|
115
|
+
for (const name of names) {
|
|
116
|
+
const relative = prefix + name;
|
|
117
|
+
const source = path.join(folder, name);
|
|
118
|
+
const item = await lstat(source);
|
|
119
|
+
const sent = alwaysSent.has(relative) || !ignore.isIgnored(relative);
|
|
120
|
+
const walked = item.isDirectory() && (sent || ignore.reincludes || [...alwaysSent].some((kept) => kept.startsWith(`${relative}/`)));
|
|
121
|
+
if (sent) {
|
|
122
|
+
entries.push({ source, name: item.isDirectory() ? `${relative}/` : relative });
|
|
123
|
+
}
|
|
124
|
+
if (walked) {
|
|
125
|
+
await collectEntries(source, `${relative}/`, ignore, alwaysSent, entries);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// Ordinal order compares UTF-16 code units, as the CLI orders names, rather than the locale's order.
|
|
130
|
+
function compareOrdinal(first, second) {
|
|
131
|
+
return first < second ? -1 : first > second ? 1 : 0;
|
|
132
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Docker's name for an ignore file. A recipe may have its own ignore file beside it, named as the recipe's file name followed by
|
|
3
|
+
* this name, such as `recipes/dotnet.Dockerfile.dockerignore`.
|
|
4
|
+
*/
|
|
5
|
+
export declare const IgnoreFileName = ".dockerignore";
|
|
6
|
+
/** EastIndia's name for an ignore file, which a build reads in preference to a `.dockerignore` in the same place. */
|
|
7
|
+
export declare const EndiaIgnoreFileName = ".endiaignore";
|
|
8
|
+
/**
|
|
9
|
+
* A build context's ignore file: which of the context's files a build leaves out of the archive it sends, so that no `COPY` can copy
|
|
10
|
+
* them and they do not change the build key. It matches paths by the same rules as the `endia` CLI, which follows Docker's
|
|
11
|
+
* `.dockerignore` matching. A folder sent by this client therefore gives the same archive as the same folder sent by the CLI.
|
|
12
|
+
*
|
|
13
|
+
* Patterns are cleaned and anchored at the context root, and a leading `/` is dropped. `*` and `?` stop at `/`, `**` spans folders,
|
|
14
|
+
* and `[...]` is a character class. A later line overrides an earlier one, and `!` re-includes. A path is ignored when the pattern
|
|
15
|
+
* that decides it matches the path itself or any of its parent folders.
|
|
16
|
+
*/
|
|
17
|
+
export declare class DockerIgnore {
|
|
18
|
+
private readonly patterns;
|
|
19
|
+
private constructor();
|
|
20
|
+
/** An ignore file that excludes nothing, for a context without one. */
|
|
21
|
+
static readonly empty: DockerIgnore;
|
|
22
|
+
/**
|
|
23
|
+
* Reads the text of an ignore file. Lines end in LF or CRLF. Blank lines and lines whose first non-blank character is `#` are
|
|
24
|
+
* skipped.
|
|
25
|
+
*
|
|
26
|
+
* @param text - The file's content.
|
|
27
|
+
* @param file - The file's path, which error messages name.
|
|
28
|
+
* @returns The ignore file.
|
|
29
|
+
* @throws Error naming the file, the line and the column of every malformed pattern.
|
|
30
|
+
*/
|
|
31
|
+
static parse(text: string, file?: string): DockerIgnore;
|
|
32
|
+
/**
|
|
33
|
+
* Whether any line re-includes with `!`. Only then can a path under a left-out folder be sent, so only then must a walk of the
|
|
34
|
+
* context look inside a left-out folder.
|
|
35
|
+
*/
|
|
36
|
+
get reincludes(): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Decides whether a context path is left out. The patterns apply in file order, and the last pattern that matches decides. The
|
|
39
|
+
* context root itself is never left out.
|
|
40
|
+
*
|
|
41
|
+
* @param path - A path relative to the context root, with `/` separators.
|
|
42
|
+
* @returns True when the path, or one of its parent folders, is left out.
|
|
43
|
+
* @throws Error when the path leads outside the context.
|
|
44
|
+
*/
|
|
45
|
+
isIgnored(path: string): boolean;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Cleans a path with `/` separators by the rules of Go's `path.Clean`, which Docker applies to ignore patterns.
|
|
49
|
+
*
|
|
50
|
+
* - Empty and `.` segments are dropped.
|
|
51
|
+
* - A `..` removes the segment before it; at the start of a relative path it stays.
|
|
52
|
+
* - An empty result is `.`.
|
|
53
|
+
*
|
|
54
|
+
* @param path - The path.
|
|
55
|
+
* @returns The cleaned path.
|
|
56
|
+
*/
|
|
57
|
+
export declare function cleanPath(path: string): string;
|