@git.zone/tsdocker 1.2.43 → 1.4.0
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/cli.js +1 -1
- package/dist_ts/00_commitinfo_data.d.ts +8 -0
- package/dist_ts/00_commitinfo_data.js +9 -0
- package/dist_ts/classes.dockerfile.d.ts +85 -0
- package/dist_ts/classes.dockerfile.js +366 -0
- package/dist_ts/classes.dockerregistry.d.ts +29 -0
- package/dist_ts/classes.dockerregistry.js +83 -0
- package/dist_ts/classes.registrystorage.d.ts +35 -0
- package/dist_ts/classes.registrystorage.js +76 -0
- package/dist_ts/classes.tsdockermanager.d.ts +53 -0
- package/dist_ts/classes.tsdockermanager.js +222 -0
- package/dist_ts/index.d.ts +1 -0
- package/dist_ts/index.js +4 -0
- package/dist_ts/interfaces/index.d.ts +68 -0
- package/dist_ts/interfaces/index.js +2 -0
- package/dist_ts/tsdocker.cli.d.ts +1 -0
- package/dist_ts/tsdocker.cli.js +179 -0
- package/dist_ts/tsdocker.config.d.ts +5 -0
- package/dist_ts/tsdocker.config.js +37 -0
- package/dist_ts/tsdocker.docker.d.ts +2 -0
- package/dist_ts/tsdocker.docker.js +145 -0
- package/dist_ts/tsdocker.logging.d.ts +3 -0
- package/dist_ts/tsdocker.logging.js +15 -0
- package/dist_ts/tsdocker.paths.d.ts +4 -0
- package/dist_ts/tsdocker.paths.js +13 -0
- package/dist_ts/tsdocker.plugins.d.ts +16 -0
- package/dist_ts/tsdocker.plugins.js +19 -0
- package/dist_ts/tsdocker.snippets.d.ts +5 -0
- package/dist_ts/tsdocker.snippets.js +26 -0
- package/npmextra.json +12 -6
- package/package.json +26 -21
- package/readme.hints.md +95 -26
- package/readme.md +32 -33
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dockerfile.ts +462 -0
- package/ts/classes.dockerregistry.ts +91 -0
- package/ts/classes.registrystorage.ts +83 -0
- package/ts/classes.tsdockermanager.ts +254 -0
- package/ts/index.ts +2 -2
- package/ts/interfaces/index.ts +70 -0
- package/ts/tsdocker.cli.ts +129 -16
- package/ts/tsdocker.config.ts +17 -10
- package/ts/tsdocker.docker.ts +6 -6
- package/ts/tsdocker.logging.ts +1 -1
- package/ts/tsdocker.paths.ts +6 -1
- package/ts/tsdocker.plugins.ts +2 -0
- package/ts/tsdocker.snippets.ts +5 -8
package/readme.md
CHANGED
|
@@ -35,13 +35,13 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
|
|
35
35
|
|
|
36
36
|
- Full TypeScript support with excellent IntelliSense
|
|
37
37
|
- Type-safe configuration
|
|
38
|
-
- Modern async/await patterns throughout
|
|
38
|
+
- Modern ESM with async/await patterns throughout
|
|
39
39
|
|
|
40
40
|
## Installation
|
|
41
41
|
|
|
42
42
|
```bash
|
|
43
|
-
npm install
|
|
44
|
-
# or
|
|
43
|
+
npm install -g @git.zone/tsdocker
|
|
44
|
+
# or for project-local installation
|
|
45
45
|
pnpm install --save-dev @git.zone/tsdocker
|
|
46
46
|
```
|
|
47
47
|
|
|
@@ -53,9 +53,9 @@ Create an `npmextra.json` file in your project root:
|
|
|
53
53
|
|
|
54
54
|
```json
|
|
55
55
|
{
|
|
56
|
-
"
|
|
57
|
-
"baseImage": "
|
|
58
|
-
"command": "
|
|
56
|
+
"@git.zone/tsdocker": {
|
|
57
|
+
"baseImage": "node:20",
|
|
58
|
+
"command": "npm test",
|
|
59
59
|
"dockerSock": false
|
|
60
60
|
}
|
|
61
61
|
}
|
|
@@ -64,7 +64,7 @@ Create an `npmextra.json` file in your project root:
|
|
|
64
64
|
### 2. Run Your Tests
|
|
65
65
|
|
|
66
66
|
```bash
|
|
67
|
-
|
|
67
|
+
tsdocker
|
|
68
68
|
```
|
|
69
69
|
|
|
70
70
|
That's it! tsdocker will:
|
|
@@ -128,14 +128,6 @@ tsdocker vscode
|
|
|
128
128
|
|
|
129
129
|
Launches a containerized VS Code instance accessible via browser at `testing-vscode.git.zone:8443`.
|
|
130
130
|
|
|
131
|
-
### Speed Test
|
|
132
|
-
|
|
133
|
-
```bash
|
|
134
|
-
tsdocker speedtest
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
Runs a network speed test inside a Docker container.
|
|
138
|
-
|
|
139
131
|
## Advanced Usage
|
|
140
132
|
|
|
141
133
|
### Docker-in-Docker Testing
|
|
@@ -144,7 +136,7 @@ If you need to run Docker commands inside your test container (e.g., testing Doc
|
|
|
144
136
|
|
|
145
137
|
```json
|
|
146
138
|
{
|
|
147
|
-
"
|
|
139
|
+
"@git.zone/tsdocker": {
|
|
148
140
|
"baseImage": "docker:latest",
|
|
149
141
|
"command": "docker run hello-world",
|
|
150
142
|
"dockerSock": true
|
|
@@ -160,7 +152,7 @@ You can use any Docker image as your base:
|
|
|
160
152
|
|
|
161
153
|
```json
|
|
162
154
|
{
|
|
163
|
-
"
|
|
155
|
+
"@git.zone/tsdocker": {
|
|
164
156
|
"baseImage": "node:20-alpine",
|
|
165
157
|
"command": "npm test"
|
|
166
158
|
}
|
|
@@ -171,13 +163,13 @@ Popular choices:
|
|
|
171
163
|
|
|
172
164
|
- `node:20` — Official Node.js images
|
|
173
165
|
- `node:20-alpine` — Lightweight Alpine-based images
|
|
174
|
-
- `
|
|
166
|
+
- `node:lts` — Long-term support Node.js version
|
|
175
167
|
|
|
176
168
|
### CI Integration
|
|
177
169
|
|
|
178
170
|
tsdocker automatically detects CI environments (via `CI=true` env var) and adjusts behavior:
|
|
179
171
|
|
|
180
|
-
-
|
|
172
|
+
- Copies project files into container in CI (instead of mounting)
|
|
181
173
|
- Optimizes for CI execution patterns
|
|
182
174
|
|
|
183
175
|
## Why tsdocker?
|
|
@@ -207,7 +199,7 @@ tsdocker ensures every test run happens in a **clean, reproducible environment**
|
|
|
207
199
|
tsdocker is built with TypeScript and provides full type definitions:
|
|
208
200
|
|
|
209
201
|
```typescript
|
|
210
|
-
import { IConfig } from '@git.zone/tsdocker/
|
|
202
|
+
import type { IConfig } from '@git.zone/tsdocker/dist_ts/tsdocker.config.js';
|
|
211
203
|
|
|
212
204
|
const config: IConfig = {
|
|
213
205
|
baseImage: 'node:20',
|
|
@@ -222,7 +214,7 @@ const config: IConfig = {
|
|
|
222
214
|
## Requirements
|
|
223
215
|
|
|
224
216
|
- **Docker**: Docker must be installed and accessible via CLI
|
|
225
|
-
- **Node.js**: Version
|
|
217
|
+
- **Node.js**: Version 18 or higher (ESM support required)
|
|
226
218
|
|
|
227
219
|
## How It Works
|
|
228
220
|
|
|
@@ -271,20 +263,20 @@ sudo usermod -aG docker $USER
|
|
|
271
263
|
|
|
272
264
|
```json
|
|
273
265
|
{
|
|
274
|
-
"
|
|
266
|
+
"@git.zone/tsdocker": {
|
|
275
267
|
"baseImage": "node:20",
|
|
276
268
|
"command": "npm test"
|
|
277
269
|
}
|
|
278
270
|
}
|
|
279
271
|
```
|
|
280
272
|
|
|
281
|
-
###
|
|
273
|
+
### Running pnpm tests
|
|
282
274
|
|
|
283
275
|
```json
|
|
284
276
|
{
|
|
285
|
-
"
|
|
286
|
-
"baseImage": "
|
|
287
|
-
"command": "
|
|
277
|
+
"@git.zone/tsdocker": {
|
|
278
|
+
"baseImage": "node:20",
|
|
279
|
+
"command": "corepack enable && pnpm install && pnpm test"
|
|
288
280
|
}
|
|
289
281
|
}
|
|
290
282
|
```
|
|
@@ -293,7 +285,7 @@ sudo usermod -aG docker $USER
|
|
|
293
285
|
|
|
294
286
|
```json
|
|
295
287
|
{
|
|
296
|
-
"
|
|
288
|
+
"@git.zone/tsdocker": {
|
|
297
289
|
"baseImage": "docker:latest",
|
|
298
290
|
"command": "sh -c 'docker version && docker ps'",
|
|
299
291
|
"dockerSock": true
|
|
@@ -307,25 +299,32 @@ sudo usermod -aG docker $USER
|
|
|
307
299
|
🚀 **Layer caching**: Docker caches image layers — your base image only downloads once
|
|
308
300
|
🚀 **Prune regularly**: Run `docker system prune` periodically to reclaim disk space
|
|
309
301
|
|
|
310
|
-
## Migration from legacy npmdocker
|
|
302
|
+
## Migration from legacy npmdocker
|
|
311
303
|
|
|
312
|
-
This package was previously published under the `npmdocker` name
|
|
304
|
+
This package was previously published under the `npmdocker` name. It is now available as `@git.zone/tsdocker` with modernized ESM support and updated dependencies.
|
|
305
|
+
|
|
306
|
+
Key changes:
|
|
307
|
+
- Configuration key changed from `npmdocker` to `@git.zone/tsdocker` in `npmextra.json`
|
|
308
|
+
- CLI command is now `tsdocker` instead of `npmdocker`
|
|
309
|
+
- Full ESM support with `.js` extensions in imports
|
|
313
310
|
|
|
314
311
|
## License and Legal Information
|
|
315
312
|
|
|
316
|
-
This repository contains open-source code
|
|
313
|
+
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
|
|
317
314
|
|
|
318
315
|
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
|
319
316
|
|
|
320
317
|
### Trademarks
|
|
321
318
|
|
|
322
|
-
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein.
|
|
319
|
+
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
|
|
320
|
+
|
|
321
|
+
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
|
|
323
322
|
|
|
324
323
|
### Company Information
|
|
325
324
|
|
|
326
325
|
Task Venture Capital GmbH
|
|
327
|
-
Registered at District
|
|
326
|
+
Registered at District Court Bremen HRB 35230 HB, Germany
|
|
328
327
|
|
|
329
|
-
For any legal inquiries or
|
|
328
|
+
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
|
330
329
|
|
|
331
330
|
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
import * as plugins from './tsdocker.plugins.js';
|
|
2
|
+
import * as paths from './tsdocker.paths.js';
|
|
3
|
+
import { logger } from './tsdocker.logging.js';
|
|
4
|
+
import { DockerRegistry } from './classes.dockerregistry.js';
|
|
5
|
+
import type { IDockerfileOptions, ITsDockerConfig } from './interfaces/index.js';
|
|
6
|
+
import type { TsDockerManager } from './classes.tsdockermanager.js';
|
|
7
|
+
|
|
8
|
+
const smartshellInstance = new plugins.smartshell.Smartshell({
|
|
9
|
+
executor: 'bash',
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Class Dockerfile represents a Dockerfile on disk
|
|
14
|
+
*/
|
|
15
|
+
export class Dockerfile {
|
|
16
|
+
// STATIC METHODS
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Creates instances of class Dockerfile for all Dockerfiles in cwd
|
|
20
|
+
*/
|
|
21
|
+
public static async readDockerfiles(managerRef: TsDockerManager): Promise<Dockerfile[]> {
|
|
22
|
+
const entries = await plugins.smartfs.directory(paths.cwd).filter('Dockerfile*').list();
|
|
23
|
+
const fileTree = entries
|
|
24
|
+
.filter(entry => entry.isFile)
|
|
25
|
+
.map(entry => plugins.path.join(paths.cwd, entry.name));
|
|
26
|
+
|
|
27
|
+
const readDockerfilesArray: Dockerfile[] = [];
|
|
28
|
+
logger.log('info', `found ${fileTree.length} Dockerfiles:`);
|
|
29
|
+
console.log(fileTree);
|
|
30
|
+
|
|
31
|
+
for (const dockerfilePath of fileTree) {
|
|
32
|
+
const myDockerfile = new Dockerfile(managerRef, {
|
|
33
|
+
filePath: dockerfilePath,
|
|
34
|
+
read: true,
|
|
35
|
+
});
|
|
36
|
+
readDockerfilesArray.push(myDockerfile);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return readDockerfilesArray;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Sorts Dockerfiles into a build order based on dependencies (topological sort)
|
|
44
|
+
*/
|
|
45
|
+
public static async sortDockerfiles(dockerfiles: Dockerfile[]): Promise<Dockerfile[]> {
|
|
46
|
+
logger.log('info', 'Sorting Dockerfiles based on dependencies...');
|
|
47
|
+
|
|
48
|
+
// Map from cleanTag to Dockerfile instance for quick lookup
|
|
49
|
+
const tagToDockerfile = new Map<string, Dockerfile>();
|
|
50
|
+
dockerfiles.forEach((dockerfile) => {
|
|
51
|
+
tagToDockerfile.set(dockerfile.cleanTag, dockerfile);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Build the dependency graph
|
|
55
|
+
const graph = new Map<Dockerfile, Dockerfile[]>();
|
|
56
|
+
dockerfiles.forEach((dockerfile) => {
|
|
57
|
+
const dependencies: Dockerfile[] = [];
|
|
58
|
+
const baseImage = dockerfile.baseImage;
|
|
59
|
+
|
|
60
|
+
// Check if the baseImage is among the local Dockerfiles
|
|
61
|
+
if (tagToDockerfile.has(baseImage)) {
|
|
62
|
+
const baseDockerfile = tagToDockerfile.get(baseImage)!;
|
|
63
|
+
dependencies.push(baseDockerfile);
|
|
64
|
+
dockerfile.localBaseImageDependent = true;
|
|
65
|
+
dockerfile.localBaseDockerfile = baseDockerfile;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
graph.set(dockerfile, dependencies);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Perform topological sort
|
|
72
|
+
const sortedDockerfiles: Dockerfile[] = [];
|
|
73
|
+
const visited = new Set<Dockerfile>();
|
|
74
|
+
const tempMarked = new Set<Dockerfile>();
|
|
75
|
+
|
|
76
|
+
const visit = (dockerfile: Dockerfile) => {
|
|
77
|
+
if (tempMarked.has(dockerfile)) {
|
|
78
|
+
throw new Error(`Circular dependency detected involving ${dockerfile.cleanTag}`);
|
|
79
|
+
}
|
|
80
|
+
if (!visited.has(dockerfile)) {
|
|
81
|
+
tempMarked.add(dockerfile);
|
|
82
|
+
const dependencies = graph.get(dockerfile) || [];
|
|
83
|
+
dependencies.forEach((dep) => visit(dep));
|
|
84
|
+
tempMarked.delete(dockerfile);
|
|
85
|
+
visited.add(dockerfile);
|
|
86
|
+
sortedDockerfiles.push(dockerfile);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
dockerfiles.forEach((dockerfile) => {
|
|
92
|
+
if (!visited.has(dockerfile)) {
|
|
93
|
+
visit(dockerfile);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
} catch (error) {
|
|
97
|
+
logger.log('error', (error as Error).message);
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Log the sorted order
|
|
102
|
+
sortedDockerfiles.forEach((dockerfile, index) => {
|
|
103
|
+
logger.log(
|
|
104
|
+
'info',
|
|
105
|
+
`Build order ${index + 1}: ${dockerfile.cleanTag} with base image ${dockerfile.baseImage}`
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return sortedDockerfiles;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Maps local Dockerfiles dependencies to the corresponding Dockerfile class instances
|
|
114
|
+
*/
|
|
115
|
+
public static async mapDockerfiles(sortedDockerfileArray: Dockerfile[]): Promise<Dockerfile[]> {
|
|
116
|
+
sortedDockerfileArray.forEach((dockerfileArg) => {
|
|
117
|
+
if (dockerfileArg.localBaseImageDependent) {
|
|
118
|
+
sortedDockerfileArray.forEach((dockfile2: Dockerfile) => {
|
|
119
|
+
if (dockfile2.cleanTag === dockerfileArg.baseImage) {
|
|
120
|
+
dockerfileArg.localBaseDockerfile = dockfile2;
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
return sortedDockerfileArray;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Builds the corresponding real docker image for each Dockerfile class instance
|
|
130
|
+
*/
|
|
131
|
+
public static async buildDockerfiles(sortedArrayArg: Dockerfile[]): Promise<Dockerfile[]> {
|
|
132
|
+
for (const dockerfileArg of sortedArrayArg) {
|
|
133
|
+
await dockerfileArg.build();
|
|
134
|
+
}
|
|
135
|
+
return sortedArrayArg;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Tests all Dockerfiles by calling Dockerfile.test()
|
|
140
|
+
*/
|
|
141
|
+
public static async testDockerfiles(sortedArrayArg: Dockerfile[]): Promise<Dockerfile[]> {
|
|
142
|
+
for (const dockerfileArg of sortedArrayArg) {
|
|
143
|
+
await dockerfileArg.test();
|
|
144
|
+
}
|
|
145
|
+
return sortedArrayArg;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Returns a version for a docker file
|
|
150
|
+
* Dockerfile_latest -> latest
|
|
151
|
+
* Dockerfile_v1.0.0 -> v1.0.0
|
|
152
|
+
* Dockerfile -> latest
|
|
153
|
+
*/
|
|
154
|
+
public static dockerFileVersion(
|
|
155
|
+
dockerfileInstanceArg: Dockerfile,
|
|
156
|
+
dockerfileNameArg: string
|
|
157
|
+
): string {
|
|
158
|
+
let versionString: string;
|
|
159
|
+
const versionRegex = /Dockerfile_(.+)$/;
|
|
160
|
+
const regexResultArray = versionRegex.exec(dockerfileNameArg);
|
|
161
|
+
if (regexResultArray && regexResultArray.length === 2) {
|
|
162
|
+
versionString = regexResultArray[1];
|
|
163
|
+
} else {
|
|
164
|
+
versionString = 'latest';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Replace ##version## placeholder with actual package version if available
|
|
168
|
+
if (dockerfileInstanceArg.managerRef?.projectInfo?.npm?.version) {
|
|
169
|
+
versionString = versionString.replace(
|
|
170
|
+
'##version##',
|
|
171
|
+
dockerfileInstanceArg.managerRef.projectInfo.npm.version
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return versionString;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Extracts the base image from a Dockerfile content
|
|
180
|
+
* Handles ARG substitution for variable base images
|
|
181
|
+
*/
|
|
182
|
+
public static dockerBaseImage(dockerfileContentArg: string): string {
|
|
183
|
+
const lines = dockerfileContentArg.split(/\r?\n/);
|
|
184
|
+
const args: { [key: string]: string } = {};
|
|
185
|
+
|
|
186
|
+
for (const line of lines) {
|
|
187
|
+
const trimmedLine = line.trim();
|
|
188
|
+
|
|
189
|
+
// Skip empty lines and comments
|
|
190
|
+
if (trimmedLine === '' || trimmedLine.startsWith('#')) {
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Match ARG instructions
|
|
195
|
+
const argMatch = trimmedLine.match(/^ARG\s+([^\s=]+)(?:=(.*))?$/i);
|
|
196
|
+
if (argMatch) {
|
|
197
|
+
const argName = argMatch[1];
|
|
198
|
+
const argValue = argMatch[2] !== undefined ? argMatch[2] : process.env[argName] || '';
|
|
199
|
+
args[argName] = argValue;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Match FROM instructions
|
|
204
|
+
const fromMatch = trimmedLine.match(/^FROM\s+(.+?)(?:\s+AS\s+[^\s]+)?$/i);
|
|
205
|
+
if (fromMatch) {
|
|
206
|
+
let baseImage = fromMatch[1].trim();
|
|
207
|
+
|
|
208
|
+
// Substitute variables in the base image name
|
|
209
|
+
baseImage = Dockerfile.substituteVariables(baseImage, args);
|
|
210
|
+
|
|
211
|
+
return baseImage;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
throw new Error('No FROM instruction found in Dockerfile');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Substitutes variables in a string, supporting default values like ${VAR:-default}
|
|
220
|
+
*/
|
|
221
|
+
private static substituteVariables(str: string, vars: { [key: string]: string }): string {
|
|
222
|
+
return str.replace(/\${([^}:]+)(:-([^}]+))?}/g, (_, varName, __, defaultValue) => {
|
|
223
|
+
if (vars[varName] !== undefined) {
|
|
224
|
+
return vars[varName];
|
|
225
|
+
} else if (defaultValue !== undefined) {
|
|
226
|
+
return defaultValue;
|
|
227
|
+
} else {
|
|
228
|
+
return '';
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Returns the docker tag string for a given registry and repo
|
|
235
|
+
*/
|
|
236
|
+
public static getDockerTagString(
|
|
237
|
+
managerRef: TsDockerManager,
|
|
238
|
+
registryArg: string,
|
|
239
|
+
repoArg: string,
|
|
240
|
+
versionArg: string,
|
|
241
|
+
suffixArg?: string
|
|
242
|
+
): string {
|
|
243
|
+
// Determine whether the repo should be mapped according to the registry
|
|
244
|
+
const config = managerRef.config;
|
|
245
|
+
const mappedRepo = config.registryRepoMap?.[registryArg];
|
|
246
|
+
const repo = mappedRepo || repoArg;
|
|
247
|
+
|
|
248
|
+
// Determine whether the version contains a suffix
|
|
249
|
+
let version = versionArg;
|
|
250
|
+
if (suffixArg) {
|
|
251
|
+
version = versionArg + '_' + suffixArg;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const tagString = `${registryArg}/${repo}:${version}`;
|
|
255
|
+
return tagString;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Gets build args from environment variable mapping
|
|
260
|
+
*/
|
|
261
|
+
public static async getDockerBuildArgs(managerRef: TsDockerManager): Promise<string> {
|
|
262
|
+
logger.log('info', 'checking for env vars to be supplied to the docker build');
|
|
263
|
+
let buildArgsString: string = '';
|
|
264
|
+
const config = managerRef.config;
|
|
265
|
+
|
|
266
|
+
if (config.buildArgEnvMap) {
|
|
267
|
+
for (const dockerArgKey of Object.keys(config.buildArgEnvMap)) {
|
|
268
|
+
const dockerArgOuterEnvVar = config.buildArgEnvMap[dockerArgKey];
|
|
269
|
+
logger.log(
|
|
270
|
+
'note',
|
|
271
|
+
`docker ARG "${dockerArgKey}" maps to outer env var "${dockerArgOuterEnvVar}"`
|
|
272
|
+
);
|
|
273
|
+
const targetValue = process.env[dockerArgOuterEnvVar];
|
|
274
|
+
if (targetValue) {
|
|
275
|
+
buildArgsString = `${buildArgsString} --build-arg ${dockerArgKey}="${targetValue}"`;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return buildArgsString;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// INSTANCE PROPERTIES
|
|
283
|
+
public managerRef: TsDockerManager;
|
|
284
|
+
public filePath!: string;
|
|
285
|
+
public repo: string;
|
|
286
|
+
public version: string;
|
|
287
|
+
public cleanTag: string;
|
|
288
|
+
public buildTag: string;
|
|
289
|
+
public pushTag!: string;
|
|
290
|
+
public containerName: string;
|
|
291
|
+
public content!: string;
|
|
292
|
+
public baseImage: string;
|
|
293
|
+
public localBaseImageDependent: boolean;
|
|
294
|
+
public localBaseDockerfile!: Dockerfile;
|
|
295
|
+
|
|
296
|
+
constructor(managerRefArg: TsDockerManager, options: IDockerfileOptions) {
|
|
297
|
+
this.managerRef = managerRefArg;
|
|
298
|
+
this.filePath = options.filePath!;
|
|
299
|
+
|
|
300
|
+
// Build repo name from project info or directory name
|
|
301
|
+
const projectInfo = this.managerRef.projectInfo;
|
|
302
|
+
if (projectInfo?.npm?.name) {
|
|
303
|
+
// Use package name, removing scope if present
|
|
304
|
+
const packageName = projectInfo.npm.name.replace(/^@[^/]+\//, '');
|
|
305
|
+
this.repo = packageName;
|
|
306
|
+
} else {
|
|
307
|
+
// Fallback to directory name
|
|
308
|
+
this.repo = plugins.path.basename(paths.cwd);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
this.version = Dockerfile.dockerFileVersion(this, plugins.path.parse(this.filePath).base);
|
|
312
|
+
this.cleanTag = this.repo + ':' + this.version;
|
|
313
|
+
this.buildTag = this.cleanTag;
|
|
314
|
+
this.containerName = 'dockerfile-' + this.version;
|
|
315
|
+
|
|
316
|
+
if (options.filePath && options.read) {
|
|
317
|
+
const fs = require('fs');
|
|
318
|
+
this.content = fs.readFileSync(plugins.path.resolve(options.filePath), 'utf-8');
|
|
319
|
+
} else if (options.fileContents) {
|
|
320
|
+
this.content = options.fileContents;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
this.baseImage = Dockerfile.dockerBaseImage(this.content);
|
|
324
|
+
this.localBaseImageDependent = false;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Builds the Dockerfile
|
|
329
|
+
*/
|
|
330
|
+
public async build(): Promise<void> {
|
|
331
|
+
logger.log('info', 'now building Dockerfile for ' + this.cleanTag);
|
|
332
|
+
const buildArgsString = await Dockerfile.getDockerBuildArgs(this.managerRef);
|
|
333
|
+
const config = this.managerRef.config;
|
|
334
|
+
|
|
335
|
+
let buildCommand: string;
|
|
336
|
+
|
|
337
|
+
// Check if multi-platform build is needed
|
|
338
|
+
if (config.platforms && config.platforms.length > 1) {
|
|
339
|
+
// Multi-platform build using buildx
|
|
340
|
+
const platformString = config.platforms.join(',');
|
|
341
|
+
buildCommand = `docker buildx build --platform ${platformString} -t ${this.buildTag} -f ${this.filePath} ${buildArgsString} .`;
|
|
342
|
+
|
|
343
|
+
if (config.push) {
|
|
344
|
+
buildCommand += ' --push';
|
|
345
|
+
} else {
|
|
346
|
+
buildCommand += ' --load';
|
|
347
|
+
}
|
|
348
|
+
} else {
|
|
349
|
+
// Standard build
|
|
350
|
+
const versionLabel = this.managerRef.projectInfo?.npm?.version || 'unknown';
|
|
351
|
+
buildCommand = `docker build --label="version=${versionLabel}" -t ${this.buildTag} -f ${this.filePath} ${buildArgsString} .`;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const result = await smartshellInstance.exec(buildCommand);
|
|
355
|
+
if (result.exitCode !== 0) {
|
|
356
|
+
logger.log('error', `Build failed for ${this.cleanTag}`);
|
|
357
|
+
console.log(result.stdout);
|
|
358
|
+
throw new Error(`Build failed for ${this.cleanTag}`);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
logger.log('ok', `Built ${this.cleanTag}`);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Pushes the Dockerfile to a registry
|
|
366
|
+
*/
|
|
367
|
+
public async push(dockerRegistryArg: DockerRegistry, versionSuffix?: string): Promise<void> {
|
|
368
|
+
this.pushTag = Dockerfile.getDockerTagString(
|
|
369
|
+
this.managerRef,
|
|
370
|
+
dockerRegistryArg.registryUrl,
|
|
371
|
+
this.repo,
|
|
372
|
+
this.version,
|
|
373
|
+
versionSuffix
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
await smartshellInstance.exec(`docker tag ${this.buildTag} ${this.pushTag}`);
|
|
377
|
+
const pushResult = await smartshellInstance.exec(`docker push ${this.pushTag}`);
|
|
378
|
+
|
|
379
|
+
if (pushResult.exitCode !== 0) {
|
|
380
|
+
logger.log('error', `Push failed for ${this.pushTag}`);
|
|
381
|
+
throw new Error(`Push failed for ${this.pushTag}`);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Get image digest
|
|
385
|
+
const inspectResult = await smartshellInstance.exec(
|
|
386
|
+
`docker inspect --format="{{index .RepoDigests 0}}" ${this.pushTag}`
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
if (inspectResult.exitCode === 0 && inspectResult.stdout.includes('@')) {
|
|
390
|
+
const imageDigest = inspectResult.stdout.split('@')[1]?.trim();
|
|
391
|
+
console.log(`The image ${this.pushTag} has digest ${imageDigest}`);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
logger.log('ok', `Pushed ${this.pushTag}`);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Pulls the Dockerfile from a registry
|
|
399
|
+
*/
|
|
400
|
+
public async pull(registryArg: DockerRegistry, versionSuffixArg?: string): Promise<void> {
|
|
401
|
+
const pullTag = Dockerfile.getDockerTagString(
|
|
402
|
+
this.managerRef,
|
|
403
|
+
registryArg.registryUrl,
|
|
404
|
+
this.repo,
|
|
405
|
+
this.version,
|
|
406
|
+
versionSuffixArg
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
await smartshellInstance.exec(`docker pull ${pullTag}`);
|
|
410
|
+
await smartshellInstance.exec(`docker tag ${pullTag} ${this.buildTag}`);
|
|
411
|
+
|
|
412
|
+
logger.log('ok', `Pulled and tagged ${pullTag} as ${this.buildTag}`);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Tests the Dockerfile by running a test script if it exists
|
|
417
|
+
*/
|
|
418
|
+
public async test(): Promise<void> {
|
|
419
|
+
const testDir = this.managerRef.config.testDir || plugins.path.join(paths.cwd, 'test');
|
|
420
|
+
const testFile = plugins.path.join(testDir, 'test_' + this.version + '.sh');
|
|
421
|
+
|
|
422
|
+
const fs = require('fs');
|
|
423
|
+
const testFileExists = fs.existsSync(testFile);
|
|
424
|
+
|
|
425
|
+
if (testFileExists) {
|
|
426
|
+
logger.log('info', `Running tests for ${this.cleanTag}`);
|
|
427
|
+
|
|
428
|
+
// Run tests in container
|
|
429
|
+
await smartshellInstance.exec(
|
|
430
|
+
`docker run --name tsdocker_test_container --entrypoint="bash" ${this.buildTag} -c "mkdir /tsdocker_test"`
|
|
431
|
+
);
|
|
432
|
+
await smartshellInstance.exec(`docker cp ${testFile} tsdocker_test_container:/tsdocker_test/test.sh`);
|
|
433
|
+
await smartshellInstance.exec(`docker commit tsdocker_test_container tsdocker_test_image`);
|
|
434
|
+
|
|
435
|
+
const testResult = await smartshellInstance.exec(
|
|
436
|
+
`docker run --entrypoint="bash" tsdocker_test_image -x /tsdocker_test/test.sh`
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
// Cleanup
|
|
440
|
+
await smartshellInstance.exec(`docker rm tsdocker_test_container`);
|
|
441
|
+
await smartshellInstance.exec(`docker rmi --force tsdocker_test_image`);
|
|
442
|
+
|
|
443
|
+
if (testResult.exitCode !== 0) {
|
|
444
|
+
throw new Error(`Tests failed for ${this.cleanTag}`);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
logger.log('ok', `Tests passed for ${this.cleanTag}`);
|
|
448
|
+
} else {
|
|
449
|
+
logger.log('warn', `Skipping tests for ${this.cleanTag} because no test file was found at ${testFile}`);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Gets the ID of a built Docker image
|
|
455
|
+
*/
|
|
456
|
+
public async getId(): Promise<string> {
|
|
457
|
+
const result = await smartshellInstance.exec(
|
|
458
|
+
'docker inspect --type=image --format="{{.Id}}" ' + this.buildTag
|
|
459
|
+
);
|
|
460
|
+
return result.stdout.trim();
|
|
461
|
+
}
|
|
462
|
+
}
|