iop
Version:
Ship Docker Anywhere
488 lines • 18.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.proxyCommand = proxyCommand;
const config_1 = require("../config");
const ssh_1 = require("../ssh");
const index_1 = require("../setup-proxy/index");
const logger_1 = require("../utils/logger");
const proxy_checker_1 = require("../utils/proxy-checker");
// Module-level logger that gets configured when proxy commands run
let logger;
/**
* Converts object or array format configuration entries to a normalized array
*/
function normalizeConfigEntries(entries) {
if (!entries)
return [];
// If it's already an array, return it
if (Array.isArray(entries)) {
return entries;
}
// If it's an object, convert to array with name property
return Object.entries(entries).map(([name, entry]) => ({
...entry,
name,
}));
}
/**
* Parses command line arguments for proxy command
*/
function parseProxyArgs(args) {
const verboseFlag = args.includes("--verbose");
let host;
let lines;
const cleanArgs = [];
for (let i = 0; i < args.length; i++) {
if (args[i] === "--verbose") {
continue;
}
else if (args[i] === "--host" && i + 1 < args.length) {
host = args[i + 1];
i++; // Skip the next argument since it's the host value
}
else if (args[i] === "--lines" && i + 1 < args.length) {
lines = parseInt(args[i + 1], 10);
i++; // Skip the next argument since it's the lines value
}
else {
cleanArgs.push(args[i]);
}
}
const subcommand = cleanArgs[0] || "";
return {
subcommand,
verboseFlag,
host,
lines,
};
}
/**
* Loads and validates IOP configuration and secrets files
*/
async function loadConfigurationAndSecrets() {
try {
const config = await (0, config_1.loadConfig)();
const secrets = await (0, config_1.loadSecrets)();
return { config, secrets };
}
catch (error) {
logger.error("Failed to load configuration/secrets", error);
throw error;
}
}
/**
* Collects all unique servers from apps and services configuration
*/
function collectAllServers(config) {
const configuredServices = normalizeConfigEntries(config.services);
const allServers = new Set();
// Add servers from services
configuredServices.forEach((service) => {
if (service.server) {
allServers.add(service.server);
}
});
return allServers;
}
/**
* Filters servers based on specified entry names
*/
function filterServersByEntries(entryNames, config) {
if (entryNames.length === 0) {
return collectAllServers(config);
}
logger.verboseLog(`Targeting entries: ${entryNames.join(", ")}`);
const configuredServices = normalizeConfigEntries(config.services);
const targetServers = new Set();
entryNames.forEach((name) => {
const service = configuredServices.find((s) => s.name === name);
if (service && service.server) {
targetServers.add(service.server);
}
if (!service) {
logger.warn(`Entry "${name}" not found in services configuration. Skipping.`);
}
});
return targetServers;
}
/**
* Establishes SSH connection to a server
*/
async function establishSSHConnection(serverHostname, context) {
const sshCredentials = await (0, ssh_1.getSSHCredentials)(serverHostname, context.config, context.secrets, context.verboseFlag);
if (!sshCredentials.username) {
throw new Error("Could not determine SSH username");
}
logger.verboseLog(`Connecting to ${serverHostname}...`);
const sshClientOptions = {
...sshCredentials,
host: serverHostname,
username: sshCredentials.username,
debug: context.verboseFlag
? (message) => {
logger.verboseLog(`SSH_DEBUG: ${message}`);
}
: undefined,
};
const sshClient = await ssh_1.SSHClient.create(sshClientOptions);
await sshClient.connect();
return sshClient;
}
/**
* Gets current proxy image digest from the server
*/
async function getCurrentProxyImageDigest(sshClient, serverHostname) {
try {
// Get the image digest of the running proxy container
const inspectCmd = `docker inspect ${index_1.IOP_PROXY_NAME} --format='{{.Image}}'`;
const currentImageId = await sshClient.exec(inspectCmd);
// Get the full digest
const digestCmd = `docker images --digests --no-trunc | grep ${currentImageId.trim()} | awk '{print $3}'`;
const digest = await sshClient.exec(digestCmd);
return digest.trim() || null;
}
catch (error) {
logger.verboseLog(`Could not get current proxy image digest on ${serverHostname}: ${error}`);
return null;
}
}
/**
* Gets latest proxy image digest from registry
*/
async function getLatestProxyImageDigest(sshClient, proxyImage) {
try {
// Pull the latest image to get its digest
const pullCmd = `docker pull ${proxyImage}`;
await sshClient.exec(pullCmd);
// Get the digest of the latest pulled image
const inspectCmd = `docker inspect ${proxyImage} --format='{{index .RepoDigests 0}}'`;
const digest = await sshClient.exec(inspectCmd);
return digest.trim() || null;
}
catch (error) {
logger.verboseLog(`Could not get latest proxy image digest: ${error}`);
return null;
}
}
/**
* Checks if proxy needs update by comparing image digests
*/
async function checkProxyNeedsUpdate(sshClient, serverHostname, proxyImage) {
logger.verboseLog(`Checking if proxy needs update on ${serverHostname}...`);
const currentDigest = await getCurrentProxyImageDigest(sshClient, serverHostname);
const latestDigest = await getLatestProxyImageDigest(sshClient, proxyImage);
if (!currentDigest || !latestDigest) {
logger.verboseLog("Could not determine image digests, assuming update needed");
return {
needsUpdate: true,
currentDigest: currentDigest || undefined,
latestDigest: latestDigest || undefined,
};
}
const needsUpdate = currentDigest !== latestDigest;
logger.verboseLog(`Current digest: ${currentDigest}`);
logger.verboseLog(`Latest digest: ${latestDigest}`);
logger.verboseLog(`Update needed: ${needsUpdate}`);
return { needsUpdate, currentDigest, latestDigest };
}
/**
* Status subcommand - shows proxy status on all servers
*/
async function proxyStatusSubcommand(context) {
logger.phase("Checking proxy status");
const targetServers = collectAllServers(context.config);
if (targetServers.size === 0) {
logger.info("No servers found in configuration.");
return;
}
const proxyStatuses = [];
for (const serverHostname of targetServers) {
let sshClient;
try {
logger.verboseLog(`Checking proxy on ${serverHostname}...`);
sshClient = await establishSSHConnection(serverHostname, context);
const proxyStatus = await (0, proxy_checker_1.checkProxyStatus)(serverHostname, sshClient, context.verboseFlag);
proxyStatuses.push(proxyStatus);
// If proxy is running, check if it needs update
if (proxyStatus.running) {
const proxyImage = context.config.proxy?.image || "elitan/iop-proxy:latest";
const updateCheck = await checkProxyNeedsUpdate(sshClient, serverHostname, proxyImage);
if (updateCheck.needsUpdate) {
logger.info(`Proxy on ${serverHostname} can be updated`);
}
else {
logger.verboseLog(`Proxy on ${serverHostname} is up to date`);
}
}
}
catch (error) {
logger.verboseLog(`Failed to check proxy on ${serverHostname}: ${error}`);
proxyStatuses.push({
running: false,
containerName: index_1.IOP_PROXY_NAME,
serverId: serverHostname,
ports: [],
error: `Failed to connect: ${error}`,
});
}
finally {
if (sshClient) {
await sshClient.close();
}
}
}
// Display status for all servers
logger.phaseComplete("Proxy status check complete");
console.log(`\nProxy Statuses (${proxyStatuses.length}):`);
for (const status of proxyStatuses) {
const formattedLines = (0, proxy_checker_1.formatProxyStatus)(status);
for (const line of formattedLines) {
console.log(line);
}
console.log(); // Add spacing between proxy statuses
}
}
/**
* Update subcommand - updates proxy on all servers
*/
async function proxyUpdateSubcommand(context) {
logger.phase("Updating proxy");
const targetServers = collectAllServers(context.config);
if (targetServers.size === 0) {
logger.info("No servers found in configuration.");
return;
}
let updatedCount = 0;
let skippedCount = 0;
for (const serverHostname of targetServers) {
let sshClient;
try {
logger.server(serverHostname);
sshClient = await establishSSHConnection(serverHostname, context);
const proxyImage = context.config.proxy?.image || "elitan/iop-proxy:latest";
// Check if update is needed
const updateCheck = await checkProxyNeedsUpdate(sshClient, serverHostname, proxyImage);
if (!updateCheck.needsUpdate) {
logger.verboseLog(`Proxy is already up to date on ${serverHostname}`);
skippedCount++;
continue;
}
logger.serverStep("Updating IOP Proxy");
const updateResult = await (0, index_1.setupIopProxy)(serverHostname, sshClient, context.verboseFlag, true // Force update for proxy command
);
if (updateResult) {
logger.serverStepComplete("Proxy updated successfully");
updatedCount++;
}
else {
logger.serverStepError("Failed to update proxy", undefined);
}
}
catch (error) {
logger.serverStepError(`Failed to update proxy`, error);
}
finally {
if (sshClient) {
await sshClient.close();
}
}
}
logger.phaseComplete("Proxy update complete");
console.log(`\nUpdate Summary:`);
console.log(` Updated: ${updatedCount} server(s)`);
console.log(` Skipped: ${skippedCount} server(s) (already up to date)`);
if (updatedCount > 0) {
console.log(`\nProxy successfully updated on ${updatedCount} server(s)!`);
}
}
/**
* Delete-host subcommand - removes a host from proxy configuration
*/
async function proxyDeleteHostSubcommand(context, host) {
if (!host) {
logger.error("Host is required for delete-host command. Use --host <hostname>");
return;
}
logger.phase(`Deleting host: ${host}`);
const targetServers = collectAllServers(context.config);
if (targetServers.size === 0) {
logger.info("No servers found in configuration.");
return;
}
let deletedCount = 0;
let notFoundCount = 0;
for (const serverHostname of targetServers) {
let sshClient;
try {
logger.server(serverHostname);
sshClient = await establishSSHConnection(serverHostname, context);
// Check if proxy is running
const proxyStatus = await (0, proxy_checker_1.checkProxyStatus)(serverHostname, sshClient, context.verboseFlag);
if (!proxyStatus.running) {
logger.verboseLog(`Proxy not running on ${serverHostname}, skipping`);
continue;
}
logger.serverStep(`Deleting host: ${host}`);
// Use the proxy CLI to delete the host (using 'remove' command which is actually implemented)
const deleteCmd = `docker exec ${index_1.IOP_PROXY_NAME} /usr/local/bin/iop-proxy remove --host ${host}`;
const result = await sshClient.exec(deleteCmd);
if (result.includes("Host deleted successfully") || result.includes("deleted")) {
logger.serverStepComplete(`Host ${host} deleted successfully`);
deletedCount++;
}
else if (result.includes("not found") || result.includes("does not exist")) {
logger.verboseLog(`Host ${host} not found on ${serverHostname}`);
notFoundCount++;
}
else {
logger.serverStepError(`Unexpected response: ${result}`, undefined);
}
}
catch (error) {
logger.serverStepError(`Failed to delete host ${host}`, error);
}
finally {
if (sshClient) {
await sshClient.close();
}
}
}
logger.phaseComplete("Host deletion complete");
console.log(`\nDeletion Summary:`);
console.log(` Deleted: ${deletedCount} server(s)`);
console.log(` Not found: ${notFoundCount} server(s)`);
if (deletedCount > 0) {
console.log(`\nHost ${host} successfully deleted from ${deletedCount} server(s)!`);
}
}
/**
* Logs subcommand - shows proxy logs from all servers
*/
async function proxyLogsSubcommand(context, lines = 50) {
logger.phase(`Showing proxy logs (${lines} lines)`);
const targetServers = collectAllServers(context.config);
if (targetServers.size === 0) {
logger.info("No servers found in configuration.");
return;
}
for (const serverHostname of targetServers) {
let sshClient;
try {
logger.server(serverHostname);
sshClient = await establishSSHConnection(serverHostname, context);
// Check if proxy is running
const proxyStatus = await (0, proxy_checker_1.checkProxyStatus)(serverHostname, sshClient, context.verboseFlag);
if (!proxyStatus.running) {
logger.info(`Proxy not running on ${serverHostname}`);
continue;
}
logger.serverStep(`Fetching logs (${lines} lines)`);
// Get proxy logs
const logsCmd = `docker logs --tail ${lines} ${index_1.IOP_PROXY_NAME}`;
const logs = await sshClient.exec(logsCmd);
console.log(`\n=== Proxy Logs from ${serverHostname} ===`);
if (logs.trim()) {
console.log(logs);
}
else {
console.log("No logs available");
}
console.log(`=== End logs from ${serverHostname} ===\n`);
logger.serverStepComplete("Logs fetched successfully");
}
catch (error) {
logger.serverStepError(`Failed to fetch logs`, error);
}
finally {
if (sshClient) {
await sshClient.close();
}
}
}
logger.phaseComplete("Log retrieval complete");
}
/**
* Shows help for proxy command
*/
function showProxyHelp() {
console.log("IOP Proxy Management");
console.log("====================");
console.log("");
console.log("USAGE:");
console.log(" iop proxy <subcommand> [flags]");
console.log("");
console.log("SUBCOMMANDS:");
console.log(" status Show proxy status on all servers (default)");
console.log(" update Update proxy to latest version on all servers");
console.log(" delete-host Remove a host from proxy configuration");
console.log(" logs Show proxy logs from all servers");
console.log("");
console.log("FLAGS:");
console.log(" --verbose Show detailed output");
console.log(" --host <host> Target specific host (for delete-host)");
console.log(" --lines <n> Number of log lines to show (for logs, default: 50)");
console.log("");
console.log("EXAMPLES:");
console.log(" iop proxy status # Check status on all servers");
console.log(" iop proxy update --verbose # Update proxy on all servers with details");
console.log(" iop proxy delete-host --host api.example.com # Remove a specific host");
console.log(" iop proxy logs --lines 100 # Show last 100 log lines from all servers");
}
/**
* Main proxy command that handles subcommands
*/
async function proxyCommand(args) {
try {
const parsedArgs = parseProxyArgs(args);
// Show help for help subcommand, empty args, or unknown commands
if (parsedArgs.subcommand === "help" ||
parsedArgs.subcommand === "" ||
![
"status",
"update",
"delete-host",
"logs"
].includes(parsedArgs.subcommand)) {
showProxyHelp();
return;
}
// Initialize logger with verbose flag
logger = new logger_1.Logger({ verbose: parsedArgs.verboseFlag });
// Load configuration and secrets
const { config, secrets } = await loadConfigurationAndSecrets();
const context = {
config,
secrets,
verboseFlag: parsedArgs.verboseFlag,
};
// Handle subcommands
switch (parsedArgs.subcommand) {
case "status":
await proxyStatusSubcommand(context);
break;
case "update":
await proxyUpdateSubcommand(context);
break;
case "delete-host":
if (!parsedArgs.host) {
logger.error("Host is required for delete-host command. Use --host <hostname>");
return;
}
await proxyDeleteHostSubcommand(context, parsedArgs.host);
break;
case "logs":
await proxyLogsSubcommand(context, parsedArgs.lines || 50);
break;
}
}
catch (error) {
logger.error("Proxy command failed", error);
process.exit(1);
}
finally {
if (logger) {
logger.cleanup();
}
}
}
//# sourceMappingURL=proxy.js.map