feat(cli): add global remote builder configuration and native SSH buildx nodes for multi-platform builds

This commit is contained in:
2026-03-15 20:15:12 +00:00
parent 732e9e5cac
commit 3e0eb5e198
11 changed files with 2904 additions and 3099 deletions

View File

@@ -8,7 +8,9 @@ import { TsDockerCache } from './classes.tsdockercache.js';
import { DockerContext } from './classes.dockercontext.js';
import { TsDockerSession } from './classes.tsdockersession.js';
import { RegistryCopy } from './classes.registrycopy.js';
import type { ITsDockerConfig, IBuildCommandOptions } from './interfaces/index.js';
import { GlobalConfig } from './classes.globalconfig.js';
import { SshTunnelManager } from './classes.sshtunnel.js';
import type { ITsDockerConfig, IBuildCommandOptions, IRemoteBuilder } from './interfaces/index.js';
const smartshellInstance = new plugins.smartshell.Smartshell({
executor: 'bash',
@@ -24,6 +26,8 @@ export class TsDockerManager {
public dockerContext: DockerContext;
public session!: TsDockerSession;
private dockerfiles: Dockerfile[] = [];
private activeRemoteBuilders: IRemoteBuilder[] = [];
private sshTunnelManager?: SshTunnelManager;
constructor(config: ITsDockerConfig) {
this.config = config;
@@ -235,6 +239,7 @@ export class TsDockerManager {
const total = toBuild.length;
const overallStart = Date.now();
await Dockerfile.startLocalRegistry(this.session, this.dockerContext.contextInfo?.isRootless);
await this.openRemoteTunnels();
try {
if (options?.parallel) {
@@ -332,6 +337,7 @@ export class TsDockerManager {
}
}
} finally {
await this.closeRemoteTunnels();
await Dockerfile.stopLocalRegistry(this.session);
}
@@ -347,6 +353,8 @@ export class TsDockerManager {
isRootless: this.dockerContext.contextInfo?.isRootless,
parallel: options?.parallel,
parallelConcurrency: options?.parallelConcurrency,
onRegistryStarted: () => this.openRemoteTunnels(),
onBeforeRegistryStop: () => this.closeRemoteTunnels(),
});
}
@@ -373,13 +381,76 @@ export class TsDockerManager {
}
/**
* Ensures Docker buildx is set up for multi-architecture builds
* Ensures Docker buildx is set up for multi-architecture builds.
* When remote builders are configured in the global config, creates a multi-node
* builder with native nodes instead of relying on QEMU emulation.
*/
private async ensureBuildx(): Promise<void> {
const builderName = this.dockerContext.getBuilderName() + (this.session?.config.builderSuffix || '');
const platforms = this.config.platforms?.join(', ') || 'default';
logger.log('info', `Setting up Docker buildx [${platforms}]...`);
logger.log('info', `Builder: ${builderName}`);
// Check for remote builders matching our target platforms
const requestedPlatforms = this.config.platforms || ['linux/amd64'];
const remoteBuilders = GlobalConfig.getBuildersForPlatforms(requestedPlatforms);
if (remoteBuilders.length > 0) {
await this.ensureBuildxWithRemoteNodes(builderName, requestedPlatforms, remoteBuilders);
} else {
await this.ensureBuildxLocal(builderName);
}
logger.log('ok', `Docker buildx ready (builder: ${builderName}, platforms: ${platforms})`);
}
/**
* Creates a multi-node buildx builder with local + remote SSH nodes.
*/
private async ensureBuildxWithRemoteNodes(
builderName: string,
requestedPlatforms: string[],
remoteBuilders: IRemoteBuilder[],
): Promise<void> {
const remotePlatforms = new Set(remoteBuilders.map((b) => b.platform));
const localPlatforms = requestedPlatforms.filter((p) => !remotePlatforms.has(p));
logger.log('info', `Remote builders: ${remoteBuilders.map((b) => `${b.name} (${b.platform} @ ${b.host})`).join(', ')}`);
if (localPlatforms.length > 0) {
logger.log('info', `Local platforms: ${localPlatforms.join(', ')}`);
}
// Always recreate the builder to ensure correct node topology
await smartshellInstance.execSilent(`docker buildx rm ${builderName} 2>/dev/null || true`);
// Create the local node
const localPlatformFlag = localPlatforms.length > 0 ? ` --platform ${localPlatforms.join(',')}` : '';
await smartshellInstance.exec(
`docker buildx create --name ${builderName} --driver docker-container --driver-opt network=host${localPlatformFlag} --use`
);
// Append remote nodes
for (const builder of remoteBuilders) {
logger.log('info', `Appending remote node: ${builder.name} (${builder.platform}) via ssh://${builder.host}`);
const appendResult = await smartshellInstance.exec(
`docker buildx create --append --name ${builderName} --driver docker-container --driver-opt network=host --platform ${builder.platform} --node ${builder.name} ssh://${builder.host}`
);
if (appendResult.exitCode !== 0) {
throw new Error(`Failed to append remote builder ${builder.name}: ${appendResult.stderr}`);
}
}
// Bootstrap all nodes
await smartshellInstance.exec('docker buildx inspect --bootstrap');
// Store active remote builders for SSH tunnel setup during build
this.activeRemoteBuilders = remoteBuilders;
}
/**
* Creates a single-node local buildx builder (original behavior, uses QEMU for cross-platform).
*/
private async ensureBuildxLocal(builderName: string): Promise<void> {
const inspectResult = await smartshellInstance.exec(`docker buildx inspect ${builderName} 2>/dev/null`);
if (inspectResult.exitCode !== 0) {
@@ -401,7 +472,30 @@ export class TsDockerManager {
await smartshellInstance.exec(`docker buildx use ${builderName}`);
}
}
logger.log('ok', `Docker buildx ready (builder: ${builderName}, platforms: ${platforms})`);
this.activeRemoteBuilders = [];
}
/**
* Opens SSH reverse tunnels for remote builders so they can reach the local registry.
*/
private async openRemoteTunnels(): Promise<void> {
if (this.activeRemoteBuilders.length === 0) return;
this.sshTunnelManager = new SshTunnelManager();
await this.sshTunnelManager.openTunnels(
this.activeRemoteBuilders,
this.session.config.registryPort,
);
}
/**
* Closes any active SSH tunnels.
*/
private async closeRemoteTunnels(): Promise<void> {
if (this.sshTunnelManager) {
await this.sshTunnelManager.closeAll();
this.sshTunnelManager = undefined;
}
}
/**