@git.zone/tsdocker 1.3.0 → 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/dist_ts/00_commitinfo_data.js +1 -1
- 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/interfaces/index.d.ts +68 -0
- package/dist_ts/interfaces/index.js +2 -0
- package/dist_ts/tsdocker.cli.js +115 -8
- package/dist_ts/tsdocker.config.d.ts +3 -8
- package/dist_ts/tsdocker.config.js +10 -2
- package/dist_ts/tsdocker.plugins.d.ts +2 -1
- package/dist_ts/tsdocker.plugins.js +3 -2
- package/package.json +2 -2
- package/readme.hints.md +95 -26
- package/readme.md +0 -8
- 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/interfaces/index.ts +70 -0
- package/ts/tsdocker.cli.ts +123 -10
- package/ts/tsdocker.config.ts +14 -7
- package/ts/tsdocker.plugins.ts +2 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as lik from '@push.rocks/lik';
|
|
1
2
|
import * as npmextra from '@push.rocks/npmextra';
|
|
2
3
|
import * as path from 'path';
|
|
3
4
|
import * as projectinfo from '@push.rocks/projectinfo';
|
|
@@ -12,4 +13,4 @@ import * as smartopen from '@push.rocks/smartopen';
|
|
|
12
13
|
import * as smartshell from '@push.rocks/smartshell';
|
|
13
14
|
import * as smartstring from '@push.rocks/smartstring';
|
|
14
15
|
export declare const smartfs: SmartFs;
|
|
15
|
-
export { npmextra, path, projectinfo, smartpromise, qenv, smartcli, smartlog, smartlogDestinationLocal, smartlogSouceOra, smartopen, smartshell, smartstring };
|
|
16
|
+
export { lik, npmextra, path, projectinfo, smartpromise, qenv, smartcli, smartlog, smartlogDestinationLocal, smartlogSouceOra, smartopen, smartshell, smartstring };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// push.rocks scope
|
|
2
|
+
import * as lik from '@push.rocks/lik';
|
|
2
3
|
import * as npmextra from '@push.rocks/npmextra';
|
|
3
4
|
import * as path from 'path';
|
|
4
5
|
import * as projectinfo from '@push.rocks/projectinfo';
|
|
@@ -14,5 +15,5 @@ import * as smartshell from '@push.rocks/smartshell';
|
|
|
14
15
|
import * as smartstring from '@push.rocks/smartstring';
|
|
15
16
|
// Create smartfs instance
|
|
16
17
|
export const smartfs = new SmartFs(new SmartFsProviderNode());
|
|
17
|
-
export { npmextra, path, projectinfo, smartpromise, qenv, smartcli, smartlog, smartlogDestinationLocal, smartlogSouceOra, smartopen, smartshell, smartstring };
|
|
18
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
18
|
+
export { lik, npmextra, path, projectinfo, smartpromise, qenv, smartcli, smartlog, smartlogDestinationLocal, smartlogSouceOra, smartopen, smartshell, smartstring };
|
|
19
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidHNkb2NrZXIucGx1Z2lucy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL3RzZG9ja2VyLnBsdWdpbnMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsbUJBQW1CO0FBQ25CLE9BQU8sS0FBSyxHQUFHLE1BQU0saUJBQWlCLENBQUM7QUFDdkMsT0FBTyxLQUFLLFFBQVEsTUFBTSxzQkFBc0IsQ0FBQztBQUNqRCxPQUFPLEtBQUssSUFBSSxNQUFNLE1BQU0sQ0FBQztBQUM3QixPQUFPLEtBQUssV0FBVyxNQUFNLHlCQUF5QixDQUFDO0FBQ3ZELE9BQU8sS0FBSyxZQUFZLE1BQU0sMEJBQTBCLENBQUM7QUFDekQsT0FBTyxLQUFLLElBQUksTUFBTSxrQkFBa0IsQ0FBQztBQUN6QyxPQUFPLEtBQUssUUFBUSxNQUFNLHNCQUFzQixDQUFDO0FBQ2pELE9BQU8sRUFBRSxPQUFPLEVBQUUsbUJBQW1CLEVBQUUsTUFBTSxxQkFBcUIsQ0FBQztBQUNuRSxPQUFPLEtBQUssUUFBUSxNQUFNLHNCQUFzQixDQUFDO0FBQ2pELE9BQU8sS0FBSyx3QkFBd0IsTUFBTSx3Q0FBd0MsQ0FBQztBQUNuRixPQUFPLEtBQUssZ0JBQWdCLE1BQU0saUNBQWlDLENBQUM7QUFDcEUsT0FBTyxLQUFLLFNBQVMsTUFBTSx1QkFBdUIsQ0FBQztBQUNuRCxPQUFPLEtBQUssVUFBVSxNQUFNLHdCQUF3QixDQUFDO0FBQ3JELE9BQU8sS0FBSyxXQUFXLE1BQU0seUJBQXlCLENBQUM7QUFFdkQsMEJBQTBCO0FBQzFCLE1BQU0sQ0FBQyxNQUFNLE9BQU8sR0FBRyxJQUFJLE9BQU8sQ0FBQyxJQUFJLG1CQUFtQixFQUFFLENBQUMsQ0FBQztBQUU5RCxPQUFPLEVBQ0wsR0FBRyxFQUNILFFBQVEsRUFDUixJQUFJLEVBQ0osV0FBVyxFQUNYLFlBQVksRUFDWixJQUFJLEVBQ0osUUFBUSxFQUNSLFFBQVEsRUFDUix3QkFBd0IsRUFDeEIsZ0JBQWdCLEVBQ2hCLFNBQVMsRUFDVCxVQUFVLEVBQ1YsV0FBVyxFQUNaLENBQUMifQ==
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@git.zone/tsdocker",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "develop npm modules cross platform with docker",
|
|
6
6
|
"main": "dist_ts/index.js",
|
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
"build": "(tsbuild)",
|
|
14
14
|
"testIntegration": "(npm run clean && npm run setupCheck && npm run testStandard)",
|
|
15
15
|
"testStandard": "(cd test/ && tsx ../ts/index.ts)",
|
|
16
|
-
"testSpeed": "(cd test/ && tsx ../ts/index.ts speedtest)",
|
|
17
16
|
"testClean": "(cd test/ && tsx ../ts/index.ts clean --all)",
|
|
18
17
|
"testVscode": "(cd test/ && tsx ../ts/index.ts vscode)",
|
|
19
18
|
"clean": "(rm -rf test/)",
|
|
@@ -41,6 +40,7 @@
|
|
|
41
40
|
"@types/node": "^25.0.9"
|
|
42
41
|
},
|
|
43
42
|
"dependencies": {
|
|
43
|
+
"@push.rocks/lik": "^6.2.2",
|
|
44
44
|
"@push.rocks/npmextra": "^5.3.3",
|
|
45
45
|
"@push.rocks/projectinfo": "^5.0.2",
|
|
46
46
|
"@push.rocks/qenv": "^6.1.3",
|
package/readme.hints.md
CHANGED
|
@@ -2,39 +2,108 @@
|
|
|
2
2
|
|
|
3
3
|
## Module Purpose
|
|
4
4
|
|
|
5
|
-
tsdocker is a
|
|
5
|
+
tsdocker is a comprehensive Docker development and building tool. It provides:
|
|
6
|
+
- Testing npm modules in clean Docker environments (legacy feature)
|
|
7
|
+
- Building Dockerfiles with dependency ordering
|
|
8
|
+
- Multi-registry push/pull support
|
|
9
|
+
- Multi-architecture builds (amd64/arm64)
|
|
6
10
|
|
|
7
|
-
##
|
|
11
|
+
## New CLI Commands (2026-01-19)
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
- Removed tslint and tslint-config-prettier (no longer needed)
|
|
13
|
+
| Command | Description |
|
|
14
|
+
|---------|-------------|
|
|
15
|
+
| `tsdocker` | Run tests in container (legacy default behavior) |
|
|
16
|
+
| `tsdocker build` | Build all Dockerfiles with dependency ordering |
|
|
17
|
+
| `tsdocker push [registry]` | Push images to configured registries |
|
|
18
|
+
| `tsdocker pull <registry>` | Pull images from registry |
|
|
19
|
+
| `tsdocker test` | Run container tests (test scripts) |
|
|
20
|
+
| `tsdocker login` | Login to configured registries |
|
|
21
|
+
| `tsdocker list` | List discovered Dockerfiles and dependencies |
|
|
22
|
+
| `tsdocker clean --all` | Clean up Docker environment |
|
|
23
|
+
| `tsdocker vscode` | Start VS Code in Docker |
|
|
21
24
|
|
|
22
|
-
##
|
|
25
|
+
## Configuration
|
|
23
26
|
|
|
24
|
-
|
|
27
|
+
Configure in `package.json` under `@git.zone/tsdocker`:
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"@git.zone/tsdocker": {
|
|
32
|
+
"registries": ["registry.gitlab.com", "docker.io"],
|
|
33
|
+
"registryRepoMap": {
|
|
34
|
+
"registry.gitlab.com": "host.today/ht-docker-node"
|
|
35
|
+
},
|
|
36
|
+
"buildArgEnvMap": {
|
|
37
|
+
"NODE_VERSION": "NODE_VERSION"
|
|
38
|
+
},
|
|
39
|
+
"platforms": ["linux/amd64", "linux/arm64"],
|
|
40
|
+
"push": false,
|
|
41
|
+
"testDir": "./test"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
```
|
|
30
45
|
|
|
31
|
-
|
|
46
|
+
### Configuration Options
|
|
32
47
|
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
-
|
|
48
|
+
- `baseImage`: Base Docker image for testing (legacy)
|
|
49
|
+
- `command`: Command to run in container (legacy)
|
|
50
|
+
- `dockerSock`: Mount Docker socket (legacy)
|
|
51
|
+
- `registries`: Array of registry URLs to push to
|
|
52
|
+
- `registryRepoMap`: Map registry URLs to different repo paths
|
|
53
|
+
- `buildArgEnvMap`: Map Docker build ARGs to environment variables
|
|
54
|
+
- `platforms`: Target architectures for buildx
|
|
55
|
+
- `push`: Auto-push after build
|
|
56
|
+
- `testDir`: Directory containing test scripts
|
|
57
|
+
|
|
58
|
+
## Registry Authentication
|
|
59
|
+
|
|
60
|
+
Set environment variables for registry login:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# Pipe-delimited format (numbered 1-10)
|
|
64
|
+
export DOCKER_REGISTRY_1="registry.gitlab.com|username|password"
|
|
65
|
+
export DOCKER_REGISTRY_2="docker.io|username|password"
|
|
66
|
+
|
|
67
|
+
# Or individual registry format
|
|
68
|
+
export DOCKER_REGISTRY_URL="registry.gitlab.com"
|
|
69
|
+
export DOCKER_REGISTRY_USER="username"
|
|
70
|
+
export DOCKER_REGISTRY_PASSWORD="password"
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## File Structure
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
ts/
|
|
77
|
+
├── index.ts (entry point)
|
|
78
|
+
├── tsdocker.cli.ts (CLI commands)
|
|
79
|
+
├── tsdocker.config.ts (configuration)
|
|
80
|
+
├── tsdocker.plugins.ts (plugin imports)
|
|
81
|
+
├── tsdocker.docker.ts (legacy test runner)
|
|
82
|
+
├── tsdocker.snippets.ts (Dockerfile generation)
|
|
83
|
+
├── classes.dockerfile.ts (Dockerfile management)
|
|
84
|
+
├── classes.dockerregistry.ts (registry authentication)
|
|
85
|
+
├── classes.registrystorage.ts (registry storage)
|
|
86
|
+
├── classes.tsdockermanager.ts (orchestrator)
|
|
87
|
+
└── interfaces/
|
|
88
|
+
└── index.ts (type definitions)
|
|
89
|
+
```
|
|
37
90
|
|
|
38
91
|
## Dependencies
|
|
39
92
|
|
|
40
|
-
|
|
93
|
+
- `@push.rocks/lik`: Object mapping utilities
|
|
94
|
+
- `@push.rocks/smartfs`: Filesystem operations
|
|
95
|
+
- `@push.rocks/smartshell`: Shell command execution
|
|
96
|
+
- `@push.rocks/smartcli`: CLI framework
|
|
97
|
+
- `@push.rocks/projectinfo`: Project metadata
|
|
98
|
+
|
|
99
|
+
## Build Status
|
|
100
|
+
|
|
101
|
+
- Build: ✅ Passes
|
|
102
|
+
- Legacy test functionality preserved
|
|
103
|
+
- New Docker build functionality added
|
|
104
|
+
|
|
105
|
+
## Previous Upgrades (2025-11-22)
|
|
106
|
+
|
|
107
|
+
- Updated all @git.zone/_ dependencies to @git.zone/_ scope
|
|
108
|
+
- Updated all @pushrocks/_ dependencies to @push.rocks/_ scope
|
|
109
|
+
- Migrated from smartfile v8 to smartfs v1.1.0
|
package/readme.md
CHANGED
|
@@ -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
|
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
|
+
}
|