feat(cli): add global remote builder configuration and native SSH buildx nodes for multi-platform builds
This commit is contained in:
77
ts/classes.sshtunnel.ts
Normal file
77
ts/classes.sshtunnel.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import * as plugins from './tsdocker.plugins.js';
|
||||
import { logger } from './tsdocker.logging.js';
|
||||
import type { IRemoteBuilder } from './interfaces/index.js';
|
||||
|
||||
const smartshellInstance = new plugins.smartshell.Smartshell({
|
||||
executor: 'bash',
|
||||
});
|
||||
|
||||
/**
|
||||
* Manages SSH reverse tunnels for remote builder nodes.
|
||||
* Opens tunnels so that the local staging registry (localhost:<port>)
|
||||
* is accessible as localhost:<port> on each remote machine.
|
||||
*/
|
||||
export class SshTunnelManager {
|
||||
private tunnelPids: number[] = [];
|
||||
|
||||
/**
|
||||
* Opens a reverse SSH tunnel to make localPort accessible on the remote machine.
|
||||
* ssh -f -N -o StrictHostKeyChecking=no -o ExitOnForwardFailure=yes
|
||||
* -R <localPort>:localhost:<localPort> [-i keyPath] user@host
|
||||
*/
|
||||
async openTunnel(builder: IRemoteBuilder, localPort: number): Promise<void> {
|
||||
const keyOpt = builder.sshKeyPath ? `-i ${builder.sshKeyPath} ` : '';
|
||||
const cmd = [
|
||||
'ssh -f -N',
|
||||
'-o StrictHostKeyChecking=no',
|
||||
'-o ExitOnForwardFailure=yes',
|
||||
`-R ${localPort}:localhost:${localPort}`,
|
||||
`${keyOpt}${builder.host}`,
|
||||
].join(' ');
|
||||
|
||||
logger.log('info', `Opening SSH tunnel to ${builder.host} for port ${localPort}...`);
|
||||
const result = await smartshellInstance.exec(cmd);
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(
|
||||
`Failed to open SSH tunnel to ${builder.host}: ${result.stderr || 'unknown error'}`
|
||||
);
|
||||
}
|
||||
|
||||
// Find the PID of the tunnel process we just started
|
||||
const pidResult = await smartshellInstance.exec(
|
||||
`pgrep -f "ssh.*-R ${localPort}:localhost:${localPort}.*${builder.host}" | tail -1`
|
||||
);
|
||||
if (pidResult.exitCode === 0 && pidResult.stdout.trim()) {
|
||||
const pid = parseInt(pidResult.stdout.trim(), 10);
|
||||
if (!isNaN(pid)) {
|
||||
this.tunnelPids.push(pid);
|
||||
logger.log('ok', `SSH tunnel to ${builder.host} established (PID ${pid})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens tunnels for all provided remote builders
|
||||
*/
|
||||
async openTunnels(builders: IRemoteBuilder[], localPort: number): Promise<void> {
|
||||
for (const builder of builders) {
|
||||
await this.openTunnel(builder, localPort);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes all tunnel processes
|
||||
*/
|
||||
async closeAll(): Promise<void> {
|
||||
for (const pid of this.tunnelPids) {
|
||||
try {
|
||||
process.kill(pid, 'SIGTERM');
|
||||
logger.log('info', `Closed SSH tunnel (PID ${pid})`);
|
||||
} catch {
|
||||
// Process may have already exited
|
||||
}
|
||||
}
|
||||
this.tunnelPids = [];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user