iop
Version:
Ship Docker Anywhere
434 lines • 19.8 kB
JavaScript
;
// SSH client wrapper logic will go here
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SSHClient = exports.getSSHCredentials = void 0;
const ssh2_promise_1 = __importDefault(require("ssh2-promise"));
const utils_1 = require("./utils");
Object.defineProperty(exports, "getSSHCredentials", { enumerable: true, get: function () { return utils_1.getSSHCredentials; } });
const promises_1 = require("fs/promises");
class SSHClient {
constructor(connectOptions) {
this.verbose = false;
this.suppressConnectionErrors = false;
this.connectOptions = connectOptions;
this.ssh = new ssh2_promise_1.default(this.connectOptions);
this.host = connectOptions.host;
}
static async create(options) {
const connectOpts = {
host: options.host,
port: options.port || 22,
username: options.username,
password: options.password,
passphrase: options.passphrase,
agent: options.agent,
};
// Skip host key verification for fresh servers
if (options.skipHostKeyVerification) {
connectOpts.hostHash = 'sha256';
connectOpts.hostVerifier = () => true;
}
// ssh2-promise allows 'identity' for path, ssh2 'privateKey' for content
const ssh2PromiseConfig = { ...connectOpts };
if (options.identity) {
ssh2PromiseConfig.identity = options.identity;
}
else if (options.privateKey) {
ssh2PromiseConfig.privateKey = options.privateKey;
}
// Add ssh2-promise specific options
ssh2PromiseConfig.reconnect = true;
ssh2PromiseConfig.reconnectDelay = 2000;
ssh2PromiseConfig.reconnectTries = 5;
const client = new SSHClient(ssh2PromiseConfig);
client.setVerbose(options.verbose || false);
client.setSuppressConnectionErrors(options.suppressConnectionErrors || false);
return client;
}
setVerbose(verbose) {
this.verbose = verbose;
}
setSuppressConnectionErrors(suppress) {
this.suppressConnectionErrors = suppress;
}
async connect() {
try {
await this.ssh.connect();
if (this.verbose) {
console.log(`SSH connection established to ${this.host}`);
}
}
catch (err) {
if (!this.suppressConnectionErrors) {
console.error(`SSH connection failed to ${this.host}:`, err);
}
throw err;
}
}
async exec(command) {
// Check if this is a sensitive command containing credentials
const isSensitiveCommand = command.includes("password") ||
command.includes("login") ||
command.includes('echo "') ||
command.includes("cat >");
// Create a sanitized version for logging
const sanitizedCommand = isSensitiveCommand
? command
.replace(/echo ".*?"/g, 'echo "***REDACTED***"')
.replace(/cat > .*?<< ['"]?EOF/g, "cat > ***REDACTED*** << EOF")
: command;
if (this.verbose) {
console.log(`[${this.host}] Executing: ${sanitizedCommand}`);
}
return new Promise(async (resolve, reject) => {
try {
// Use shell command with explicit exit code checking
// This command runs the original command and captures both stdout/stderr and exit code
const wrappedCommand = `${command}; echo "EXIT_CODE:$?"`;
const result = await this.ssh.exec(wrappedCommand);
// Parse the result to extract exit code
const lines = result.split('\n');
let exitCodeLine = '';
let output = '';
// Find the exit code line (should be last non-empty line)
for (let i = lines.length - 1; i >= 0; i--) {
if (lines[i].trim()) {
if (lines[i].startsWith('EXIT_CODE:')) {
exitCodeLine = lines[i];
// Everything before this line is the actual output
output = lines.slice(0, i).join('\n');
break;
}
}
}
const exitCode = exitCodeLine ? parseInt(exitCodeLine.replace('EXIT_CODE:', '')) : 0;
if (exitCode === 0) {
// Command succeeded
resolve(output);
}
else {
// Command failed
if (this.verbose) {
console.error(`Command "${sanitizedCommand}" on ${this.host} failed with exit code ${exitCode}`);
}
reject(new Error(`Command failed with exit code ${exitCode}: ${output}`));
}
}
catch (err) {
// Handle the original ssh2-promise behavior for stderr as error
const errorMessage = String(err);
// If this is just a Docker warning or similar, treat as success
if (errorMessage.includes("WARNING: No swap limit support") ||
(errorMessage.includes("WARNING:") && !errorMessage.includes("error") && !errorMessage.includes("failed"))) {
if (this.verbose) {
console.warn(`[${this.host}] Command "${sanitizedCommand}" succeeded but had warnings:\n${errorMessage}`);
}
resolve(errorMessage);
}
else {
// This is a real error
if (this.verbose) {
console.error(`Error executing command "${sanitizedCommand}" on ${this.host}:`, errorMessage);
}
reject(err);
}
}
});
}
// Helper method to sanitize potentially sensitive output
sanitizeErrorOutput(output) {
// Replace potential Docker login password in command
output = output.replace(/echo ".*?" \| docker login/g, 'echo "***REDACTED***" | docker login');
// Replace other sensitive patterns
output = output.replace(/password=.*?( |$|\n)/gi, "password=***REDACTED*** ");
output = output.replace(/password:.*?( |$|\n)/gi, "password:***REDACTED*** ");
output = output.replace(/--password[-_]?\w*\s+["']?[\w!@#$%^&*(),.?;:|<>]*["']?/gi, "--password ***REDACTED***");
return output;
}
async close() {
try {
await this.ssh.close();
if (this.verbose) {
console.log(`SSH connection closed to ${this.host}`);
}
}
catch (err) {
if (this.verbose) {
console.error(`Error closing SSH connection to ${this.host}:`, err);
}
}
}
/**
* Detects the platform architecture of the remote server
* @returns Promise<string> Platform string (e.g., "linux/amd64", "linux/arm64")
*/
async detectServerPlatform() {
if (this.platformCache) {
return this.platformCache;
}
try {
if (this.verbose) {
console.log(`[${this.host}] Detecting server platform...`);
}
// Get architecture and OS information
const arch = await this.exec("uname -m");
const os = await this.exec("uname -s");
// Map common architectures to Docker platform format
const cleanArch = arch.trim().toLowerCase();
const cleanOs = os.trim().toLowerCase();
let dockerArch;
switch (cleanArch) {
case "x86_64":
case "amd64":
dockerArch = "amd64";
break;
case "aarch64":
case "arm64":
dockerArch = "arm64";
break;
case "armv7l":
case "armv7":
dockerArch = "arm/v7";
break;
case "armv6l":
case "armv6":
dockerArch = "arm/v6";
break;
case "i386":
case "i686":
dockerArch = "386";
break;
default:
// Default to amd64 for unknown architectures
if (this.verbose) {
console.warn(`[${this.host}] Unknown architecture '${cleanArch}', defaulting to amd64`);
}
dockerArch = "amd64";
}
// Construct platform string (typically linux/amd64, linux/arm64, etc.)
let platform;
if (cleanOs === "linux") {
platform = `linux/${dockerArch}`;
}
else if (cleanOs === "darwin") {
platform = `darwin/${dockerArch}`;
}
else {
// Default to linux for unknown OS
if (this.verbose) {
console.warn(`[${this.host}] Unknown OS '${cleanOs}', defaulting to linux`);
}
platform = `linux/${dockerArch}`;
}
// Cache the result
this.platformCache = platform;
if (this.verbose) {
console.log(`[${this.host}] Detected platform: ${platform} (arch: ${cleanArch}, os: ${cleanOs})`);
}
return platform;
}
catch (error) {
if (this.verbose) {
console.warn(`[${this.host}] Failed to detect server platform, defaulting to linux/amd64:`, error);
}
// Default fallback
this.platformCache = "linux/amd64";
return this.platformCache;
}
}
/**
* Upload a file to the remote server using native rsync or scp command (fastest for large files)
*/
async uploadFile(localPath, remotePath, onProgress) {
if (this.verbose) {
console.log(`[${this.host}] Uploading ${localPath} to ${remotePath} via rsync or SCP`);
}
try {
// Get file stats for progress info
const stats = await (0, promises_1.stat)(localPath);
const totalSize = stats.size;
if (this.verbose) {
console.log(`[${this.host}] File size: ${(totalSize / 1024 / 1024).toFixed(2)} MB`);
}
// Create remote directory first via SSH
await this.ssh.exec(`mkdir -p $(dirname "${remotePath}")`);
// Use native rsync or scp command for maximum speed
// Try rsync first (faster for large files), fall back to scp
const rsyncCommand = `rsync -avz --progress "${localPath}" ${this.connectOptions.username}@${this.host}:"${remotePath}"`;
const scpCommand = `scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -C "${localPath}" ${this.connectOptions.username}@${this.host}:"${remotePath}"`;
if (this.verbose) {
console.log(`[${this.host}] Trying rsync for faster transfer`);
}
// Execute rsync or scp command directly
const { spawn } = await Promise.resolve().then(() => __importStar(require("child_process")));
try {
// Try rsync first with progress monitoring
try {
if (this.verbose) {
console.log(`[${this.host}] Executing rsync: ${rsyncCommand.replace(localPath, "***")}`);
}
await new Promise((resolve, reject) => {
const rsyncArgs = ['-avz', '--progress', localPath, `${this.connectOptions.username}@${this.host}:${remotePath}`];
const rsyncProcess = spawn('rsync', rsyncArgs);
let lastProgress = 0;
rsyncProcess.stdout?.on('data', (data) => {
const output = data.toString();
if (this.verbose) {
console.log(`[${this.host}] rsync stdout:`, output.trim());
}
if (onProgress) {
// Parse rsync progress output
const progressMatch = output.match(/(\d+)\s+(\d+)%\s+(\d+\.\d+[kMG]B\/s)/);
if (progressMatch) {
const percentage = parseInt(progressMatch[2]);
const transferred = Math.floor((percentage / 100) * totalSize);
if (transferred > lastProgress) {
lastProgress = transferred;
onProgress(transferred, totalSize);
}
}
}
});
rsyncProcess.stderr?.on('data', (data) => {
if (this.verbose) {
console.log(`[${this.host}] rsync stderr:`, data.toString().trim());
}
});
rsyncProcess.on('close', (code) => {
if (code === 0) {
onProgress?.(totalSize, totalSize); // Final progress update
resolve();
}
else {
reject(new Error(`rsync exited with code ${code}`));
}
});
rsyncProcess.on('error', (err) => {
reject(err);
});
});
if (this.verbose) {
console.log(`[${this.host}] rsync upload completed: ${remotePath}`);
}
}
catch (rsyncError) {
// Fallback to SCP if rsync fails
if (this.verbose) {
console.log(`[${this.host}] rsync failed, falling back to SCP: ${rsyncError}`);
}
await new Promise((resolve, reject) => {
const scpArgs = ['-o', 'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=/dev/null', '-C', localPath, `${this.connectOptions.username}@${this.host}:${remotePath}`];
const scpProcess = spawn('scp', scpArgs);
// For SCP, simulate progress based on time (not ideal but better than nothing)
let progressInterval;
if (onProgress) {
const startTime = Date.now();
const estimatedDurationMs = Math.max(1000, totalSize / (1024 * 1024) * 1000); // Rough estimate: 1MB/s
progressInterval = setInterval(() => {
const elapsed = Date.now() - startTime;
const progress = Math.min(0.9, elapsed / estimatedDurationMs); // Cap at 90% until complete
const transferred = Math.floor(progress * totalSize);
onProgress(transferred, totalSize);
}, 200);
}
scpProcess.stdout?.on('data', (data) => {
if (this.verbose) {
console.log(`[${this.host}] scp stdout:`, data.toString().trim());
}
});
scpProcess.stderr?.on('data', (data) => {
if (this.verbose) {
console.log(`[${this.host}] scp stderr:`, data.toString().trim());
}
});
scpProcess.on('close', (code) => {
if (progressInterval) {
clearInterval(progressInterval);
}
if (code === 0) {
onProgress?.(totalSize, totalSize); // Final progress update
resolve();
}
else {
reject(new Error(`scp exited with code ${code}`));
}
});
scpProcess.on('error', (err) => {
if (progressInterval) {
clearInterval(progressInterval);
}
reject(err);
});
});
if (this.verbose) {
console.log(`[${this.host}] SCP upload completed: ${remotePath}`);
}
}
}
catch (transferError) {
throw transferError;
}
}
catch (err) {
console.error(`[${this.host}] Failed to upload file ${localPath} to ${remotePath} via rsync or SCP:`, err);
throw err;
}
}
/**
* Download a file from the remote server using SFTP
*/
async downloadFile(remotePath, localPath) {
if (this.verbose) {
console.log(`[${this.host}] Downloading ${remotePath} to ${localPath}`);
}
try {
const sftp = await this.ssh.sftp();
await sftp.fastGet(remotePath, localPath);
if (this.verbose) {
console.log(`[${this.host}] Download completed: ${localPath}`);
}
}
catch (err) {
console.error(`[${this.host}] Failed to download file ${remotePath} to ${localPath}:`, err);
throw err;
}
}
}
exports.SSHClient = SSHClient;
//# sourceMappingURL=index.js.map