feat(cli): add global remote builder configuration and native SSH buildx nodes for multi-platform builds
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user