@git.zone/tsdocker 1.17.4 → 2.1.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 +2 -2
- package/dist_ts/classes.dockerfile.d.ts +2 -0
- package/dist_ts/classes.dockerfile.js +7 -1
- package/dist_ts/classes.globalconfig.d.ts +13 -0
- package/dist_ts/classes.globalconfig.js +66 -0
- package/dist_ts/classes.sshtunnel.d.ts +23 -0
- package/dist_ts/classes.sshtunnel.js +66 -0
- package/dist_ts/classes.tsdockermanager.d.ts +21 -1
- package/dist_ts/classes.tsdockermanager.js +74 -3
- package/dist_ts/interfaces/index.d.ts +15 -7
- package/dist_ts/tsdocker.cli.js +140 -33
- package/dist_ts/tsdocker.config.d.ts +1 -4
- package/dist_ts/tsdocker.config.js +3 -22
- package/dist_ts/tsdocker.paths.d.ts +0 -1
- package/dist_ts/tsdocker.paths.js +1 -2
- package/dist_ts/tsdocker.plugins.d.ts +1 -5
- package/dist_ts/tsdocker.plugins.js +2 -6
- package/package.json +8 -20
- package/readme.hints.md +1 -10
- package/readme.md +227 -78
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dockerfile.ts +7 -1
- package/ts/classes.globalconfig.ts +76 -0
- package/ts/classes.sshtunnel.ts +77 -0
- package/ts/classes.tsdockermanager.ts +97 -3
- package/ts/interfaces/index.ts +17 -8
- package/ts/tsdocker.cli.ts +145 -37
- package/ts/tsdocker.config.ts +4 -29
- package/ts/tsdocker.paths.ts +0 -1
- package/ts/tsdocker.plugins.ts +0 -8
- package/assets/Dockerfile +0 -6
- package/dist_ts/tsdocker.docker.d.ts +0 -2
- package/dist_ts/tsdocker.docker.js +0 -145
- package/dist_ts/tsdocker.snippets.d.ts +0 -5
- package/dist_ts/tsdocker.snippets.js +0 -26
- package/ts/tsdocker.docker.ts +0 -169
- package/ts/tsdocker.snippets.ts +0 -34
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as plugins from './tsdocker.plugins.js';
|
|
3
|
+
import { logger } from './tsdocker.logging.js';
|
|
4
|
+
import type { IGlobalConfig, IRemoteBuilder } from './interfaces/index.js';
|
|
5
|
+
|
|
6
|
+
const CONFIG_DIR = plugins.path.join(
|
|
7
|
+
process.env.HOME || process.env.USERPROFILE || '~',
|
|
8
|
+
'.git.zone',
|
|
9
|
+
'tsdocker',
|
|
10
|
+
);
|
|
11
|
+
const CONFIG_PATH = plugins.path.join(CONFIG_DIR, 'config.json');
|
|
12
|
+
|
|
13
|
+
const DEFAULT_CONFIG: IGlobalConfig = {
|
|
14
|
+
remoteBuilders: [],
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export class GlobalConfig {
|
|
18
|
+
static getConfigPath(): string {
|
|
19
|
+
return CONFIG_PATH;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
static load(): IGlobalConfig {
|
|
23
|
+
try {
|
|
24
|
+
const raw = fs.readFileSync(CONFIG_PATH, 'utf-8');
|
|
25
|
+
const parsed = JSON.parse(raw);
|
|
26
|
+
return {
|
|
27
|
+
...DEFAULT_CONFIG,
|
|
28
|
+
...parsed,
|
|
29
|
+
};
|
|
30
|
+
} catch {
|
|
31
|
+
return { ...DEFAULT_CONFIG };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
static save(config: IGlobalConfig): void {
|
|
36
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
37
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
static addBuilder(builder: IRemoteBuilder): void {
|
|
41
|
+
const config = GlobalConfig.load();
|
|
42
|
+
const existing = config.remoteBuilders.findIndex((b) => b.name === builder.name);
|
|
43
|
+
if (existing >= 0) {
|
|
44
|
+
config.remoteBuilders[existing] = builder;
|
|
45
|
+
logger.log('info', `Updated remote builder: ${builder.name}`);
|
|
46
|
+
} else {
|
|
47
|
+
config.remoteBuilders.push(builder);
|
|
48
|
+
logger.log('info', `Added remote builder: ${builder.name}`);
|
|
49
|
+
}
|
|
50
|
+
GlobalConfig.save(config);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
static removeBuilder(name: string): void {
|
|
54
|
+
const config = GlobalConfig.load();
|
|
55
|
+
const before = config.remoteBuilders.length;
|
|
56
|
+
config.remoteBuilders = config.remoteBuilders.filter((b) => b.name !== name);
|
|
57
|
+
if (config.remoteBuilders.length < before) {
|
|
58
|
+
logger.log('info', `Removed remote builder: ${name}`);
|
|
59
|
+
} else {
|
|
60
|
+
logger.log('warn', `Remote builder not found: ${name}`);
|
|
61
|
+
}
|
|
62
|
+
GlobalConfig.save(config);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
static getBuilders(): IRemoteBuilder[] {
|
|
66
|
+
return GlobalConfig.load().remoteBuilders;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Returns remote builders that match any of the requested platforms
|
|
71
|
+
*/
|
|
72
|
+
static getBuildersForPlatforms(platforms: string[]): IRemoteBuilder[] {
|
|
73
|
+
const builders = GlobalConfig.getBuilders();
|
|
74
|
+
return builders.filter((b) => platforms.includes(b.platform));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import * as plugins from './tsdocker.plugins.js';
|
|
2
|
+
import { logger } from './tsdocker.logging.js';
|
|
3
|
+
import type { IRemoteBuilder } from './interfaces/index.js';
|
|
4
|
+
|
|
5
|
+
const smartshellInstance = new plugins.smartshell.Smartshell({
|
|
6
|
+
executor: 'bash',
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Manages SSH reverse tunnels for remote builder nodes.
|
|
11
|
+
* Opens tunnels so that the local staging registry (localhost:<port>)
|
|
12
|
+
* is accessible as localhost:<port> on each remote machine.
|
|
13
|
+
*/
|
|
14
|
+
export class SshTunnelManager {
|
|
15
|
+
private tunnelPids: number[] = [];
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Opens a reverse SSH tunnel to make localPort accessible on the remote machine.
|
|
19
|
+
* ssh -f -N -o StrictHostKeyChecking=no -o ExitOnForwardFailure=yes
|
|
20
|
+
* -R <localPort>:localhost:<localPort> [-i keyPath] user@host
|
|
21
|
+
*/
|
|
22
|
+
async openTunnel(builder: IRemoteBuilder, localPort: number): Promise<void> {
|
|
23
|
+
const keyOpt = builder.sshKeyPath ? `-i ${builder.sshKeyPath} ` : '';
|
|
24
|
+
const cmd = [
|
|
25
|
+
'ssh -f -N',
|
|
26
|
+
'-o StrictHostKeyChecking=no',
|
|
27
|
+
'-o ExitOnForwardFailure=yes',
|
|
28
|
+
`-R ${localPort}:localhost:${localPort}`,
|
|
29
|
+
`${keyOpt}${builder.host}`,
|
|
30
|
+
].join(' ');
|
|
31
|
+
|
|
32
|
+
logger.log('info', `Opening SSH tunnel to ${builder.host} for port ${localPort}...`);
|
|
33
|
+
const result = await smartshellInstance.exec(cmd);
|
|
34
|
+
|
|
35
|
+
if (result.exitCode !== 0) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`Failed to open SSH tunnel to ${builder.host}: ${result.stderr || 'unknown error'}`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Find the PID of the tunnel process we just started
|
|
42
|
+
const pidResult = await smartshellInstance.exec(
|
|
43
|
+
`pgrep -f "ssh.*-R ${localPort}:localhost:${localPort}.*${builder.host}" | tail -1`
|
|
44
|
+
);
|
|
45
|
+
if (pidResult.exitCode === 0 && pidResult.stdout.trim()) {
|
|
46
|
+
const pid = parseInt(pidResult.stdout.trim(), 10);
|
|
47
|
+
if (!isNaN(pid)) {
|
|
48
|
+
this.tunnelPids.push(pid);
|
|
49
|
+
logger.log('ok', `SSH tunnel to ${builder.host} established (PID ${pid})`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Opens tunnels for all provided remote builders
|
|
56
|
+
*/
|
|
57
|
+
async openTunnels(builders: IRemoteBuilder[], localPort: number): Promise<void> {
|
|
58
|
+
for (const builder of builders) {
|
|
59
|
+
await this.openTunnel(builder, localPort);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Closes all tunnel processes
|
|
65
|
+
*/
|
|
66
|
+
async closeAll(): Promise<void> {
|
|
67
|
+
for (const pid of this.tunnelPids) {
|
|
68
|
+
try {
|
|
69
|
+
process.kill(pid, 'SIGTERM');
|
|
70
|
+
logger.log('info', `Closed SSH tunnel (PID ${pid})`);
|
|
71
|
+
} catch {
|
|
72
|
+
// Process may have already exited
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
this.tunnelPids = [];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -8,7 +8,9 @@ import { TsDockerCache } from './classes.tsdockercache.js';
|
|
|
8
8
|
import { DockerContext } from './classes.dockercontext.js';
|
|
9
9
|
import { TsDockerSession } from './classes.tsdockersession.js';
|
|
10
10
|
import { RegistryCopy } from './classes.registrycopy.js';
|
|
11
|
-
import
|
|
11
|
+
import { GlobalConfig } from './classes.globalconfig.js';
|
|
12
|
+
import { SshTunnelManager } from './classes.sshtunnel.js';
|
|
13
|
+
import type { ITsDockerConfig, IBuildCommandOptions, IRemoteBuilder } from './interfaces/index.js';
|
|
12
14
|
|
|
13
15
|
const smartshellInstance = new plugins.smartshell.Smartshell({
|
|
14
16
|
executor: 'bash',
|
|
@@ -24,6 +26,8 @@ export class TsDockerManager {
|
|
|
24
26
|
public dockerContext: DockerContext;
|
|
25
27
|
public session!: TsDockerSession;
|
|
26
28
|
private dockerfiles: Dockerfile[] = [];
|
|
29
|
+
private activeRemoteBuilders: IRemoteBuilder[] = [];
|
|
30
|
+
private sshTunnelManager?: SshTunnelManager;
|
|
27
31
|
|
|
28
32
|
constructor(config: ITsDockerConfig) {
|
|
29
33
|
this.config = config;
|
|
@@ -235,6 +239,7 @@ export class TsDockerManager {
|
|
|
235
239
|
const total = toBuild.length;
|
|
236
240
|
const overallStart = Date.now();
|
|
237
241
|
await Dockerfile.startLocalRegistry(this.session, this.dockerContext.contextInfo?.isRootless);
|
|
242
|
+
await this.openRemoteTunnels();
|
|
238
243
|
|
|
239
244
|
try {
|
|
240
245
|
if (options?.parallel) {
|
|
@@ -332,6 +337,7 @@ export class TsDockerManager {
|
|
|
332
337
|
}
|
|
333
338
|
}
|
|
334
339
|
} finally {
|
|
340
|
+
await this.closeRemoteTunnels();
|
|
335
341
|
await Dockerfile.stopLocalRegistry(this.session);
|
|
336
342
|
}
|
|
337
343
|
|
|
@@ -347,6 +353,8 @@ export class TsDockerManager {
|
|
|
347
353
|
isRootless: this.dockerContext.contextInfo?.isRootless,
|
|
348
354
|
parallel: options?.parallel,
|
|
349
355
|
parallelConcurrency: options?.parallelConcurrency,
|
|
356
|
+
onRegistryStarted: () => this.openRemoteTunnels(),
|
|
357
|
+
onBeforeRegistryStop: () => this.closeRemoteTunnels(),
|
|
350
358
|
});
|
|
351
359
|
}
|
|
352
360
|
|
|
@@ -373,13 +381,76 @@ export class TsDockerManager {
|
|
|
373
381
|
}
|
|
374
382
|
|
|
375
383
|
/**
|
|
376
|
-
* Ensures Docker buildx is set up for multi-architecture builds
|
|
384
|
+
* Ensures Docker buildx is set up for multi-architecture builds.
|
|
385
|
+
* When remote builders are configured in the global config, creates a multi-node
|
|
386
|
+
* builder with native nodes instead of relying on QEMU emulation.
|
|
377
387
|
*/
|
|
378
388
|
private async ensureBuildx(): Promise<void> {
|
|
379
389
|
const builderName = this.dockerContext.getBuilderName() + (this.session?.config.builderSuffix || '');
|
|
380
390
|
const platforms = this.config.platforms?.join(', ') || 'default';
|
|
381
391
|
logger.log('info', `Setting up Docker buildx [${platforms}]...`);
|
|
382
392
|
logger.log('info', `Builder: ${builderName}`);
|
|
393
|
+
|
|
394
|
+
// Check for remote builders matching our target platforms
|
|
395
|
+
const requestedPlatforms = this.config.platforms || ['linux/amd64'];
|
|
396
|
+
const remoteBuilders = GlobalConfig.getBuildersForPlatforms(requestedPlatforms);
|
|
397
|
+
|
|
398
|
+
if (remoteBuilders.length > 0) {
|
|
399
|
+
await this.ensureBuildxWithRemoteNodes(builderName, requestedPlatforms, remoteBuilders);
|
|
400
|
+
} else {
|
|
401
|
+
await this.ensureBuildxLocal(builderName);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
logger.log('ok', `Docker buildx ready (builder: ${builderName}, platforms: ${platforms})`);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Creates a multi-node buildx builder with local + remote SSH nodes.
|
|
409
|
+
*/
|
|
410
|
+
private async ensureBuildxWithRemoteNodes(
|
|
411
|
+
builderName: string,
|
|
412
|
+
requestedPlatforms: string[],
|
|
413
|
+
remoteBuilders: IRemoteBuilder[],
|
|
414
|
+
): Promise<void> {
|
|
415
|
+
const remotePlatforms = new Set(remoteBuilders.map((b) => b.platform));
|
|
416
|
+
const localPlatforms = requestedPlatforms.filter((p) => !remotePlatforms.has(p));
|
|
417
|
+
|
|
418
|
+
logger.log('info', `Remote builders: ${remoteBuilders.map((b) => `${b.name} (${b.platform} @ ${b.host})`).join(', ')}`);
|
|
419
|
+
if (localPlatforms.length > 0) {
|
|
420
|
+
logger.log('info', `Local platforms: ${localPlatforms.join(', ')}`);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Always recreate the builder to ensure correct node topology
|
|
424
|
+
await smartshellInstance.execSilent(`docker buildx rm ${builderName} 2>/dev/null || true`);
|
|
425
|
+
|
|
426
|
+
// Create the local node
|
|
427
|
+
const localPlatformFlag = localPlatforms.length > 0 ? ` --platform ${localPlatforms.join(',')}` : '';
|
|
428
|
+
await smartshellInstance.exec(
|
|
429
|
+
`docker buildx create --name ${builderName} --driver docker-container --driver-opt network=host${localPlatformFlag} --use`
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
// Append remote nodes
|
|
433
|
+
for (const builder of remoteBuilders) {
|
|
434
|
+
logger.log('info', `Appending remote node: ${builder.name} (${builder.platform}) via ssh://${builder.host}`);
|
|
435
|
+
const appendResult = await smartshellInstance.exec(
|
|
436
|
+
`docker buildx create --append --name ${builderName} --driver docker-container --driver-opt network=host --platform ${builder.platform} --node ${builder.name} ssh://${builder.host}`
|
|
437
|
+
);
|
|
438
|
+
if (appendResult.exitCode !== 0) {
|
|
439
|
+
throw new Error(`Failed to append remote builder ${builder.name}: ${appendResult.stderr}`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Bootstrap all nodes
|
|
444
|
+
await smartshellInstance.exec('docker buildx inspect --bootstrap');
|
|
445
|
+
|
|
446
|
+
// Store active remote builders for SSH tunnel setup during build
|
|
447
|
+
this.activeRemoteBuilders = remoteBuilders;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Creates a single-node local buildx builder (original behavior, uses QEMU for cross-platform).
|
|
452
|
+
*/
|
|
453
|
+
private async ensureBuildxLocal(builderName: string): Promise<void> {
|
|
383
454
|
const inspectResult = await smartshellInstance.exec(`docker buildx inspect ${builderName} 2>/dev/null`);
|
|
384
455
|
|
|
385
456
|
if (inspectResult.exitCode !== 0) {
|
|
@@ -401,7 +472,30 @@ export class TsDockerManager {
|
|
|
401
472
|
await smartshellInstance.exec(`docker buildx use ${builderName}`);
|
|
402
473
|
}
|
|
403
474
|
}
|
|
404
|
-
|
|
475
|
+
this.activeRemoteBuilders = [];
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Opens SSH reverse tunnels for remote builders so they can reach the local registry.
|
|
480
|
+
*/
|
|
481
|
+
private async openRemoteTunnels(): Promise<void> {
|
|
482
|
+
if (this.activeRemoteBuilders.length === 0) return;
|
|
483
|
+
|
|
484
|
+
this.sshTunnelManager = new SshTunnelManager();
|
|
485
|
+
await this.sshTunnelManager.openTunnels(
|
|
486
|
+
this.activeRemoteBuilders,
|
|
487
|
+
this.session.config.registryPort,
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Closes any active SSH tunnels.
|
|
493
|
+
*/
|
|
494
|
+
private async closeRemoteTunnels(): Promise<void> {
|
|
495
|
+
if (this.sshTunnelManager) {
|
|
496
|
+
await this.sshTunnelManager.closeAll();
|
|
497
|
+
this.sshTunnelManager = undefined;
|
|
498
|
+
}
|
|
405
499
|
}
|
|
406
500
|
|
|
407
501
|
/**
|
package/ts/interfaces/index.ts
CHANGED
|
@@ -1,15 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Configuration interface for tsdocker
|
|
3
|
-
* Extends legacy config with new Docker build capabilities
|
|
4
3
|
*/
|
|
5
4
|
export interface ITsDockerConfig {
|
|
6
|
-
// Legacy (backward compatible)
|
|
7
|
-
baseImage: string;
|
|
8
|
-
command: string;
|
|
9
|
-
dockerSock: boolean;
|
|
10
|
-
keyValueObject: { [key: string]: any };
|
|
11
|
-
|
|
12
|
-
// New Docker build config
|
|
13
5
|
registries?: string[];
|
|
14
6
|
registryRepoMap?: { [registry: string]: string };
|
|
15
7
|
buildArgEnvMap?: { [dockerArg: string]: string };
|
|
@@ -103,3 +95,20 @@ export interface IDockerContextInfo {
|
|
|
103
95
|
dockerHost?: string; // value of DOCKER_HOST env var, if set
|
|
104
96
|
topology?: 'socket-mount' | 'dind' | 'local';
|
|
105
97
|
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* A remote builder node for native cross-platform builds
|
|
101
|
+
*/
|
|
102
|
+
export interface IRemoteBuilder {
|
|
103
|
+
name: string; // e.g., "arm64-builder"
|
|
104
|
+
host: string; // e.g., "armbuilder@192.168.190.216"
|
|
105
|
+
platform: string; // e.g., "linux/arm64"
|
|
106
|
+
sshKeyPath?: string; // e.g., "~/.ssh/id_ed25519"
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Global tsdocker configuration stored at ~/.git.zone/tsdocker/config.json
|
|
111
|
+
*/
|
|
112
|
+
export interface IGlobalConfig {
|
|
113
|
+
remoteBuilders: IRemoteBuilder[];
|
|
114
|
+
}
|
package/ts/tsdocker.cli.ts
CHANGED
|
@@ -3,27 +3,97 @@ import * as paths from './tsdocker.paths.js';
|
|
|
3
3
|
|
|
4
4
|
// modules
|
|
5
5
|
import * as ConfigModule from './tsdocker.config.js';
|
|
6
|
-
import * as DockerModule from './tsdocker.docker.js';
|
|
7
6
|
|
|
8
7
|
import { logger, ora } from './tsdocker.logging.js';
|
|
9
8
|
import { TsDockerManager } from './classes.tsdockermanager.js';
|
|
10
9
|
import { DockerContext } from './classes.dockercontext.js';
|
|
10
|
+
import { GlobalConfig } from './classes.globalconfig.js';
|
|
11
11
|
import type { IBuildCommandOptions } from './interfaces/index.js';
|
|
12
12
|
import { commitinfo } from './00_commitinfo_data.js';
|
|
13
13
|
|
|
14
14
|
const tsdockerCli = new plugins.smartcli.Smartcli();
|
|
15
15
|
tsdockerCli.addVersion(commitinfo.version);
|
|
16
16
|
|
|
17
|
+
const printManPage = () => {
|
|
18
|
+
const manPage = `
|
|
19
|
+
TSDOCKER(1) User Commands TSDOCKER(1)
|
|
20
|
+
|
|
21
|
+
NAME
|
|
22
|
+
tsdocker - build, test, and push Docker images
|
|
23
|
+
|
|
24
|
+
VERSION
|
|
25
|
+
${commitinfo.version}
|
|
26
|
+
|
|
27
|
+
SYNOPSIS
|
|
28
|
+
tsdocker <command> [options]
|
|
29
|
+
|
|
30
|
+
COMMANDS
|
|
31
|
+
build [patterns...] [flags] Build Dockerfiles in dependency order
|
|
32
|
+
push [patterns...] [flags] Build and push images to registries
|
|
33
|
+
pull <registry-url> Pull images from a registry
|
|
34
|
+
test [flags] Build and run container test scripts
|
|
35
|
+
login Authenticate with configured registries
|
|
36
|
+
list List discovered Dockerfiles
|
|
37
|
+
config <subcommand> [flags] Manage global tsdocker configuration
|
|
38
|
+
clean [-y] [--all] Interactive Docker resource cleanup
|
|
39
|
+
|
|
40
|
+
BUILD / PUSH OPTIONS
|
|
41
|
+
--platform=<p> Target platform (e.g. linux/arm64)
|
|
42
|
+
--timeout=<s> Build timeout in seconds
|
|
43
|
+
--no-cache Rebuild without Docker layer cache
|
|
44
|
+
--cached Skip builds when Dockerfile is unchanged
|
|
45
|
+
--verbose Stream raw docker build output
|
|
46
|
+
--parallel[=<n>] Parallel builds (optional concurrency limit)
|
|
47
|
+
--context=<name> Docker context to use
|
|
48
|
+
|
|
49
|
+
PUSH-ONLY OPTIONS
|
|
50
|
+
--registry=<url> Push to a specific registry
|
|
51
|
+
--no-build Push already-built images (skip build step)
|
|
52
|
+
|
|
53
|
+
CLEAN OPTIONS
|
|
54
|
+
-y Auto-confirm all prompts
|
|
55
|
+
--all Include all images and volumes (not just dangling)
|
|
56
|
+
|
|
57
|
+
CONFIG SUBCOMMANDS
|
|
58
|
+
add-builder Add a remote builder node
|
|
59
|
+
--name=<n> Builder name (e.g. arm64-builder)
|
|
60
|
+
--host=<h> SSH host (e.g. user@192.168.1.100)
|
|
61
|
+
--platform=<p> Platform (e.g. linux/arm64)
|
|
62
|
+
--ssh-key=<path> SSH key path (optional)
|
|
63
|
+
remove-builder Remove a remote builder by name
|
|
64
|
+
--name=<n> Builder name to remove
|
|
65
|
+
list-builders List all configured remote builders
|
|
66
|
+
show Show full global config
|
|
67
|
+
|
|
68
|
+
CONFIGURATION
|
|
69
|
+
Configure via npmextra.json under the "@git.zone/tsdocker" key:
|
|
70
|
+
|
|
71
|
+
registries Array of registry URLs to push to
|
|
72
|
+
registryRepoMap Map of registry URL to repo path overrides
|
|
73
|
+
buildArgEnvMap Map of Docker build-arg names to env var names
|
|
74
|
+
platforms Array of target platforms (default: ["linux/amd64"])
|
|
75
|
+
push Boolean, auto-push after build
|
|
76
|
+
testDir Directory containing test_*.sh scripts
|
|
77
|
+
|
|
78
|
+
Global config is stored at ~/.git.zone/tsdocker/config.json
|
|
79
|
+
and managed via the "config" command.
|
|
80
|
+
|
|
81
|
+
EXAMPLES
|
|
82
|
+
tsdocker build
|
|
83
|
+
tsdocker build Dockerfile_app --platform=linux/arm64
|
|
84
|
+
tsdocker push --registry=ghcr.io
|
|
85
|
+
tsdocker test --verbose
|
|
86
|
+
tsdocker clean -y --all
|
|
87
|
+
tsdocker config add-builder --name=arm64 --host=user@host --platform=linux/arm64
|
|
88
|
+
tsdocker config list-builders
|
|
89
|
+
`;
|
|
90
|
+
console.log(manPage);
|
|
91
|
+
};
|
|
92
|
+
|
|
17
93
|
export let run = () => {
|
|
18
|
-
// Default command:
|
|
19
|
-
tsdockerCli.standardCommand().subscribe(async
|
|
20
|
-
|
|
21
|
-
if (configArg.exitCode === 0) {
|
|
22
|
-
logger.log('success', 'container ended all right!');
|
|
23
|
-
} else {
|
|
24
|
-
logger.log('error', `container ended with error! Exit Code is ${configArg.exitCode}`);
|
|
25
|
-
process.exit(1);
|
|
26
|
-
}
|
|
94
|
+
// Default command: print man page
|
|
95
|
+
tsdockerCli.standardCommand().subscribe(async () => {
|
|
96
|
+
printManPage();
|
|
27
97
|
});
|
|
28
98
|
|
|
29
99
|
/**
|
|
@@ -229,21 +299,73 @@ export let run = () => {
|
|
|
229
299
|
});
|
|
230
300
|
|
|
231
301
|
/**
|
|
232
|
-
*
|
|
302
|
+
* Manage global tsdocker configuration (remote builders, etc.)
|
|
303
|
+
* Usage: tsdocker config <subcommand> [--name=...] [--host=...] [--platform=...] [--ssh-key=...]
|
|
233
304
|
*/
|
|
234
|
-
tsdockerCli.addCommand('
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
305
|
+
tsdockerCli.addCommand('config').subscribe(async argvArg => {
|
|
306
|
+
try {
|
|
307
|
+
const subcommand = argvArg._[1] as string;
|
|
308
|
+
|
|
309
|
+
switch (subcommand) {
|
|
310
|
+
case 'add-builder': {
|
|
311
|
+
const name = argvArg.name as string;
|
|
312
|
+
const host = argvArg.host as string;
|
|
313
|
+
const platform = argvArg.platform as string;
|
|
314
|
+
const sshKeyPath = argvArg['ssh-key'] as string | undefined;
|
|
315
|
+
|
|
316
|
+
if (!name || !host || !platform) {
|
|
317
|
+
logger.log('error', 'Required: --name, --host, --platform');
|
|
318
|
+
logger.log('info', 'Usage: tsdocker config add-builder --name=arm64-builder --host=user@host --platform=linux/arm64 [--ssh-key=~/.ssh/id_ed25519]');
|
|
319
|
+
process.exit(1);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
GlobalConfig.addBuilder({ name, host, platform, sshKeyPath });
|
|
323
|
+
logger.log('success', `Remote builder "${name}" configured: ${platform} via ssh://${host}`);
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
case 'remove-builder': {
|
|
328
|
+
const name = argvArg.name as string;
|
|
329
|
+
if (!name) {
|
|
330
|
+
logger.log('error', 'Required: --name');
|
|
331
|
+
logger.log('info', 'Usage: tsdocker config remove-builder --name=arm64-builder');
|
|
332
|
+
process.exit(1);
|
|
333
|
+
}
|
|
334
|
+
GlobalConfig.removeBuilder(name);
|
|
335
|
+
logger.log('success', `Remote builder "${name}" removed`);
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
case 'list-builders': {
|
|
340
|
+
const builders = GlobalConfig.getBuilders();
|
|
341
|
+
if (builders.length === 0) {
|
|
342
|
+
logger.log('info', 'No remote builders configured');
|
|
343
|
+
} else {
|
|
344
|
+
logger.log('info', `${builders.length} remote builder(s):`);
|
|
345
|
+
for (const b of builders) {
|
|
346
|
+
const keyInfo = b.sshKeyPath ? ` (key: ${b.sshKeyPath})` : '';
|
|
347
|
+
logger.log('info', ` ${b.name}: ${b.platform} via ssh://${b.host}${keyInfo}`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
case 'show': {
|
|
354
|
+
const config = GlobalConfig.load();
|
|
355
|
+
logger.log('info', `Config file: ${GlobalConfig.getConfigPath()}`);
|
|
356
|
+
console.log(JSON.stringify(config, null, 2));
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
default:
|
|
361
|
+
logger.log('error', `Unknown config subcommand: ${subcommand || '(none)'}`);
|
|
362
|
+
logger.log('info', 'Available: add-builder, remove-builder, list-builders, show');
|
|
363
|
+
process.exit(1);
|
|
245
364
|
}
|
|
246
|
-
})
|
|
365
|
+
} catch (err) {
|
|
366
|
+
logger.log('error', `Config failed: ${(err as Error).message}`);
|
|
367
|
+
process.exit(1);
|
|
368
|
+
}
|
|
247
369
|
});
|
|
248
370
|
|
|
249
371
|
tsdockerCli.addCommand('clean').subscribe(async argvArg => {
|
|
@@ -443,19 +565,5 @@ export let run = () => {
|
|
|
443
565
|
}
|
|
444
566
|
});
|
|
445
567
|
|
|
446
|
-
tsdockerCli.addCommand('vscode').subscribe(async argvArg => {
|
|
447
|
-
const smartshellInstance = new plugins.smartshell.Smartshell({
|
|
448
|
-
executor: 'bash'
|
|
449
|
-
});
|
|
450
|
-
logger.log('ok', `Starting vscode in cwd ${paths.cwd}`);
|
|
451
|
-
await smartshellInstance.execAndWaitForLine(
|
|
452
|
-
`docker run -p 127.0.0.1:8443:8443 -v "${
|
|
453
|
-
paths.cwd
|
|
454
|
-
}:/home/coder/project" registry.gitlab.com/hosttoday/ht-docker-vscode --allow-http --no-auth`,
|
|
455
|
-
/Connected to shared process/
|
|
456
|
-
);
|
|
457
|
-
await plugins.smartopen.openUrl('testing-vscode.git.zone:8443');
|
|
458
|
-
});
|
|
459
|
-
|
|
460
568
|
tsdockerCli.startParse();
|
|
461
569
|
};
|
package/ts/tsdocker.config.ts
CHANGED
|
@@ -1,34 +1,10 @@
|
|
|
1
1
|
import * as plugins from './tsdocker.plugins.js';
|
|
2
2
|
import * as paths from './tsdocker.paths.js';
|
|
3
|
-
import * as fs from 'fs';
|
|
4
3
|
import type { ITsDockerConfig } from './interfaces/index.js';
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
export type IConfig = ITsDockerConfig & {
|
|
8
|
-
exitCode?: number;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
const getQenvKeyValueObject = async () => {
|
|
12
|
-
let qenvKeyValueObjectArray: { [key: string]: string | number };
|
|
13
|
-
if (fs.existsSync(plugins.path.join(paths.cwd, 'qenv.yml'))) {
|
|
14
|
-
qenvKeyValueObjectArray = new plugins.qenv.Qenv(paths.cwd, '.nogit/').keyValueObject;
|
|
15
|
-
} else {
|
|
16
|
-
qenvKeyValueObjectArray = {};
|
|
17
|
-
}
|
|
18
|
-
return qenvKeyValueObjectArray;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
const buildConfig = async (qenvKeyValueObjectArg: { [key: string]: string | number }) => {
|
|
5
|
+
const buildConfig = async (): Promise<ITsDockerConfig> => {
|
|
22
6
|
const npmextra = new plugins.npmextra.Npmextra(paths.cwd);
|
|
23
|
-
const config = npmextra.dataFor<
|
|
24
|
-
// Legacy options (backward compatible)
|
|
25
|
-
baseImage: 'hosttoday/ht-docker-node:npmdocker',
|
|
26
|
-
init: 'rm -rf node_nodules/ && yarn install',
|
|
27
|
-
command: 'npmci npm test',
|
|
28
|
-
dockerSock: false,
|
|
29
|
-
keyValueObject: qenvKeyValueObjectArg,
|
|
30
|
-
|
|
31
|
-
// New Docker build options
|
|
7
|
+
const config = npmextra.dataFor<ITsDockerConfig>('@git.zone/tsdocker', {
|
|
32
8
|
registries: [],
|
|
33
9
|
registryRepoMap: {},
|
|
34
10
|
buildArgEnvMap: {},
|
|
@@ -39,7 +15,6 @@ const buildConfig = async (qenvKeyValueObjectArg: { [key: string]: string | numb
|
|
|
39
15
|
return config;
|
|
40
16
|
};
|
|
41
17
|
|
|
42
|
-
export let run = async (): Promise<
|
|
43
|
-
|
|
44
|
-
return config;
|
|
18
|
+
export let run = async (): Promise<ITsDockerConfig> => {
|
|
19
|
+
return buildConfig();
|
|
45
20
|
};
|
package/ts/tsdocker.paths.ts
CHANGED
|
@@ -11,4 +11,3 @@ export let cwd = process.cwd();
|
|
|
11
11
|
export let packageBase = plugins.path.join(__dirname, '../');
|
|
12
12
|
export let assets = plugins.path.join(packageBase, 'assets/');
|
|
13
13
|
fs.mkdirSync(assets, { recursive: true });
|
|
14
|
-
export let npmdockerFile = plugins.path.join(cwd, 'npmdocker');
|
package/ts/tsdocker.plugins.ts
CHANGED
|
@@ -3,17 +3,13 @@ import * as lik from '@push.rocks/lik';
|
|
|
3
3
|
import * as npmextra from '@push.rocks/npmextra';
|
|
4
4
|
import * as path from 'path';
|
|
5
5
|
import * as projectinfo from '@push.rocks/projectinfo';
|
|
6
|
-
import * as smartpromise from '@push.rocks/smartpromise';
|
|
7
|
-
import * as qenv from '@push.rocks/qenv';
|
|
8
6
|
import * as smartcli from '@push.rocks/smartcli';
|
|
9
7
|
import { SmartFs, SmartFsProviderNode } from '@push.rocks/smartfs';
|
|
10
8
|
import * as smartlog from '@push.rocks/smartlog';
|
|
11
9
|
import * as smartlogDestinationLocal from '@push.rocks/smartlog-destination-local';
|
|
12
10
|
import * as smartlogSouceOra from '@push.rocks/smartlog-source-ora';
|
|
13
|
-
import * as smartopen from '@push.rocks/smartopen';
|
|
14
11
|
import * as smartinteract from '@push.rocks/smartinteract';
|
|
15
12
|
import * as smartshell from '@push.rocks/smartshell';
|
|
16
|
-
import * as smartstring from '@push.rocks/smartstring';
|
|
17
13
|
|
|
18
14
|
// Create smartfs instance
|
|
19
15
|
export const smartfs = new SmartFs(new SmartFsProviderNode());
|
|
@@ -23,14 +19,10 @@ export {
|
|
|
23
19
|
npmextra,
|
|
24
20
|
path,
|
|
25
21
|
projectinfo,
|
|
26
|
-
smartpromise,
|
|
27
|
-
qenv,
|
|
28
22
|
smartcli,
|
|
29
23
|
smartinteract,
|
|
30
24
|
smartlog,
|
|
31
25
|
smartlogDestinationLocal,
|
|
32
26
|
smartlogSouceOra,
|
|
33
|
-
smartopen,
|
|
34
27
|
smartshell,
|
|
35
|
-
smartstring
|
|
36
28
|
};
|
package/assets/Dockerfile
DELETED