78 lines
2.4 KiB
TypeScript
78 lines
2.4 KiB
TypeScript
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 = [];
|
|
}
|
|
}
|