UNPKG

iop

Version:
519 lines 20 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.statusCommand = statusCommand; const config_1 = require("../config"); const docker_1 = require("../docker"); const ssh_1 = require("../ssh"); const logger_1 = require("../utils/logger"); const proxy_checker_1 = require("../utils/proxy-checker"); // Module-level logger that gets configured when statusCommand runs let logger; /** * Parses command line arguments for status command */ function parseStatusArgs(args, verbose = false) { const entryNames = []; // Parse arguments, filtering out flags for (const arg of args || []) { if (!arg.startsWith("-")) { entryNames.push(arg); } } return { entryNames, verboseFlag: verbose, }; } /** * 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) { if (error instanceof Error && error.message.includes("ENOENT")) { logger.error("Configuration files not found."); logger.error(""); logger.error("To fix this:"); logger.error(" iop init # Create configuration files"); logger.error(" # Edit iop.yml with your settings"); logger.error(" iop status # Check status again"); } else if (error instanceof Error && error.message.includes("Invalid configuration")) { // Validation errors are already displayed by loadConfig, just exit throw error; } else { logger.error("Failed to load configuration/secrets", error); } throw error; } } /** * Normalizes configuration entries from object/array format to array format */ function normalizeConfigEntries(entries) { if (!entries) return []; if (Array.isArray(entries)) return entries; return Object.entries(entries).map(([name, entry]) => ({ ...entry, name })); } /** * Establishes SSH connection to a server for status checking */ async function establishSSHConnection(serverHostname, context) { const sshCreds = await (0, ssh_1.getSSHCredentials)(serverHostname, context.config, context.secrets, context.verboseFlag); const sshOptions = { ...sshCreds, host: sshCreds.host || serverHostname, username: sshCreds.username || "root", port: sshCreds.port || 22, }; const sshClient = await ssh_1.SSHClient.create(sshOptions); await sshClient.connect(); // Store verbose message instead of logging immediately if (context.verboseFlag) { context.verboseMessages.push(`SSH connection established to ${serverHostname}`); } return sshClient; } /** * Gets container information for any entry (app or service) on a specific server */ async function getEntryContainersOnServer(entry, entryType, dockerClient, projectName) { const labelKey = entryType === "app" ? "iop.app" : "iop.service"; const allContainers = await dockerClient.findContainersByLabelAndProject(`${labelKey}=${entry.name}`, projectName); const blueContainers = []; const greenContainers = []; const runningContainers = []; const containerDetails = {}; for (const containerName of allContainers) { const isRunning = await dockerClient.containerIsRunning(containerName); if (isRunning) { runningContainers.push(containerName); // Always get full container details const details = await dockerClient.getContainerDetails(containerName); if (details) { containerDetails[containerName] = details; } } // Only check for color labels for apps (services don't use blue/green deployment) if (entryType === "app") { const labels = await dockerClient.getContainerLabels(containerName); const color = labels["iop.color"]; if (color === "blue") { blueContainers.push(containerName); } else if (color === "green") { greenContainers.push(containerName); } } } return { activeColor: entryType === "app" ? undefined : undefined, // Will be set later for apps blueContainers: entryType === "app" ? blueContainers : undefined, greenContainers: entryType === "app" ? greenContainers : undefined, runningContainers, totalContainers: allContainers, containerDetails: containerDetails, // Always include container details }; } /** * Gets proxy status across all servers */ async function getProxyStatusSummary(context) { const proxyStatuses = []; // Get unique servers from all services const allServers = new Set(); const services = normalizeConfigEntries(context.config.services); services.forEach((service) => allServers.add(service.server)); if (allServers.size === 0) { return { proxyStatuses: [] }; } for (const serverHostname of allServers) { let sshClient; try { sshClient = await establishSSHConnection(serverHostname, context); if (context.verboseFlag) { context.verboseMessages.push(`Checking proxy status on ${serverHostname}...`); } const proxyStatus = await (0, proxy_checker_1.checkProxyStatus)(serverHostname, sshClient, context.verboseFlag); proxyStatuses.push(proxyStatus); } catch (error) { if (context.verboseFlag) { context.verboseMessages.push(`Failed to check proxy status on ${serverHostname}: ${error}`); } // Add error status for this server proxyStatuses.push({ running: false, containerName: "iop-proxy", serverId: serverHostname, ports: [], error: `Failed to connect to server: ${error}`, }); } finally { if (sshClient) { await sshClient.close(); } } } return { proxyStatuses }; } /** * Gets status information for any entry (app or service) on a specific server */ async function getEntryStatusOnServer(entry, entryType, serverHostname, context) { let sshClient; try { sshClient = await establishSSHConnection(serverHostname, context); const dockerClient = new docker_1.DockerClient(sshClient, serverHostname, context.verboseFlag); // Get container information const containerInfo = await getEntryContainersOnServer(entry, entryType, dockerClient, context.projectName); // Get active color for apps only if (entryType === "app") { const activeColor = await dockerClient.getCurrentActiveColorForProject(entry.name, context.projectName); containerInfo.activeColor = activeColor; } return containerInfo; } catch (error) { // Store verbose message instead of logging immediately if (context.verboseFlag) { context.verboseMessages.push(`Failed to get status for ${entryType} ${entry.name} on ${serverHostname}: ${error}`); } return { activeColor: entryType === "app" ? null : undefined, blueContainers: entryType === "app" ? [] : undefined, greenContainers: entryType === "app" ? [] : undefined, runningContainers: [], totalContainers: [], containerDetails: {}, }; } finally { if (sshClient) { await sshClient.close(); } } } /** * Aggregates server statuses to determine overall entry status */ function aggregateEntryStatus(entry, entryType, serverStatuses) { let totalRunning = 0; let totalContainers = 0; let totalBlue = 0; let totalGreen = 0; let activeColors = []; let uptime = null; let resourceUsage = null; let additionalInfo = null; for (const serverStatus of serverStatuses) { totalRunning += serverStatus.runningContainers.length; totalContainers += serverStatus.totalContainers.length; if (entryType === "app" && serverStatus.blueContainers && serverStatus.greenContainers) { totalBlue += serverStatus.blueContainers.length; totalGreen += serverStatus.greenContainers.length; if (serverStatus.activeColor !== undefined) { activeColors.push(serverStatus.activeColor); } } // Extract uptime and resource usage from first running container if (!uptime && serverStatus.containerDetails && serverStatus.runningContainers.length > 0) { const firstRunningContainer = serverStatus.runningContainers[0]; const containerDetail = serverStatus.containerDetails[firstRunningContainer]; if (containerDetail) { uptime = containerDetail.uptime; if (containerDetail.stats) { resourceUsage = { cpu: containerDetail.stats.cpuPercent, memory: containerDetail.stats.memoryUsage, }; } // Extract additional info additionalInfo = { exactImage: containerDetail.image || "", restartCount: containerDetail.restartCount, exitCode: containerDetail.exitCode, ports: containerDetail.ports, volumes: containerDetail.volumes, }; } } } // Determine overall active color (should be consistent across servers) const uniqueActiveColors = [ ...new Set(activeColors.filter((c) => c !== null)), ]; const activeColor = entryType === "app" && uniqueActiveColors.length === 1 ? uniqueActiveColors[0] : null; // Determine overall status // Services don't have replicas property, so default to 1 const expectedReplicas = entry.replicas || 1; let status; if (totalRunning === 0) { status = "stopped"; } else if (totalRunning >= expectedReplicas) { status = "running"; } else { status = "mixed"; } return { totalRunning, totalContainers, totalBlue, totalGreen, activeColor, status, uptime, resourceUsage, additionalInfo, }; } /** * Gets comprehensive status for any entry (app or service) across all its servers */ async function getEntryStatus(entry, entryType, context) { const serverStatuses = await Promise.all([entry.server].map((server) => getEntryStatusOnServer(entry, entryType, server, context))); const aggregated = aggregateEntryStatus(entry, entryType, serverStatuses); const baseStatus = { name: entry.name, type: entryType, status: aggregated.status, replicas: { total: entryType === "app" ? aggregated.totalBlue + aggregated.totalGreen : aggregated.totalContainers, running: aggregated.totalRunning, }, servers: [entry.server], uptime: aggregated.uptime || undefined, resourceUsage: aggregated.resourceUsage || undefined, }; if (entryType === "app") { baseStatus.activeColor = aggregated.activeColor; baseStatus.replicas.blue = aggregated.totalBlue; baseStatus.replicas.green = aggregated.totalGreen; } else { baseStatus.image = entry.image; } // Add additional info if (aggregated.additionalInfo) { baseStatus.additionalInfo = aggregated.additionalInfo; } return baseStatus; } /** * Displays status information for any entry (app or service) in a formatted way */ function displayEntryStatus(entryStatus) { const statusIcon = { running: "[✓]", stopped: "[✗]", mixed: "[!]", unknown: "[?]", }[entryStatus.status]; const entryTypeCapitalized = entryStatus.type.charAt(0).toUpperCase() + entryStatus.type.slice(1); console.log(` └─ ${entryTypeCapitalized}: ${entryStatus.name}`); if (entryStatus.type === "app") { const versionDisplay = entryStatus.activeColor ? `(${entryStatus.activeColor} active)` : "(no active version)"; console.log(` ├─ Status: ${statusIcon} ${entryStatus.status.toUpperCase()} ${versionDisplay}`); } else { console.log(` ├─ Status: ${statusIcon} ${entryStatus.status.toUpperCase()}`); } if (entryStatus.replicas.total > 0) { console.log(` ├─ Replicas: ${entryStatus.replicas.running}/${entryStatus.replicas.total} running`); } // Show uptime and resource usage (always included) if (entryStatus.uptime) { console.log(` ├─ Uptime: ${entryStatus.uptime}`); } if (entryStatus.resourceUsage) { console.log(` ├─ Resources:`); console.log(` │ ├─ CPU: ${entryStatus.resourceUsage.cpu}`); console.log(` │ └─ Memory: ${entryStatus.resourceUsage.memory}`); } // Show additional info (always included) if (entryStatus.additionalInfo) { const info = entryStatus.additionalInfo; console.log(` ├─ Image: ${info.exactImage}`); if (info.restartCount > 0) { console.log(` ├─ Restarts: ${info.restartCount}`); } if (info.exitCode !== null && info.exitCode !== undefined && info.exitCode !== 0) { console.log(` ├─ Exit Code: ${info.exitCode}`); } if (info.ports.length > 0) { // Remove duplicates and format ports for better readability const uniquePorts = [...new Set(info.ports)]; const formattedPorts = uniquePorts.map((port) => { // Handle format like "5433:5432/tcp" if (port.includes(":")) { const [hostPort, containerPortWithProtocol] = port.split(":"); const [containerPort, protocol] = containerPortWithProtocol.split("/"); const protocolDisplay = protocol ? `/${protocol}` : ""; return `${hostPort}${containerPort}${protocolDisplay}`; } return port; }); if (formattedPorts.length === 1) { console.log(` ├─ Port: ${formattedPorts[0]} (host → container)`); } else { console.log(` ├─ Ports (${formattedPorts.length}):`); for (const port of formattedPorts) { console.log(` │ ├─ ${port} (host → container)`); } } } if (info.volumes.length > 0) { console.log(` ├─ Volumes (${info.volumes.length}):`); for (const volume of info.volumes) { const modeDisplay = volume.mode ? ` (${volume.mode})` : ""; console.log(` │ ├─ ${volume.source}${volume.destination}${modeDisplay}`); } } } console.log(` └─ Servers: ${entryStatus.servers.join(", ")}`); console.log(); // Add spacing between entries } /** * Filters apps and services based on requested entry names */ function filterEntriesByNames(entryNames, apps, services) { if (entryNames.length === 0) { return { filteredApps: apps, filteredServices: services }; } const filteredApps = apps.filter((app) => entryNames.includes(app.name)); const filteredServices = services.filter((service) => entryNames.includes(service.name)); return { filteredApps, filteredServices }; } /** * Validates that requested entries exist */ function validateRequestedEntries(entryNames, filteredApps, filteredServices) { if (entryNames.length > 0 && filteredApps.length === 0 && filteredServices.length === 0) { logger.error(`No apps or services found with names: ${entryNames.join(", ")}`); return false; } return true; } /** * Handles the main status checking and display logic */ async function checkAndDisplayStatus(parsedArgs, context) { const services = normalizeConfigEntries(context.config.services); const { filteredApps, filteredServices } = filterEntriesByNames(parsedArgs.entryNames, [], services); if (!validateRequestedEntries(parsedArgs.entryNames, filteredApps, filteredServices)) { return; } // Check if no services are configured if (services.length === 0) { logger.info("No services configured."); return; } // Collect service statuses and proxy status in parallel const [serviceStatuses, proxyStatusSummary] = await Promise.all([ Promise.all(filteredServices.map((service) => getEntryStatus(service, "service", context))), getProxyStatusSummary(context), ]); // Complete the checking phase before displaying results logger.phaseComplete("Checking deployment status"); // Display any collected verbose messages if (context.verboseFlag && context.verboseMessages.length > 0) { for (const message of context.verboseMessages) { logger.verboseLog(message); } } // Now display the collected results displayCollectedEntryStatuses(filteredServices, serviceStatuses, "Services"); displayProxyStatus(proxyStatusSummary); } /** * Displays status for entries using pre-collected status data */ function displayCollectedEntryStatuses(entries, entryStatuses, sectionTitle) { if (entries.length === 0) { return; } console.log(`${sectionTitle} (${entries.length}):`); for (const entryStatus of entryStatuses) { displayEntryStatus(entryStatus); } } /** * Displays proxy status information */ function displayProxyStatus(proxyStatusSummary) { const proxyStatuses = proxyStatusSummary.proxyStatuses; if (proxyStatuses.length === 0) { logger.info("No proxy statuses found."); return; } console.log(`Proxy Statuses (${proxyStatuses.length}):`); for (const proxyStatus of proxyStatuses) { displayProxyStatusInfo(proxyStatus); } } /** * Displays proxy status information for a single proxy status */ function displayProxyStatusInfo(proxyStatus) { const formattedLines = (0, proxy_checker_1.formatProxyStatus)(proxyStatus); for (const line of formattedLines) { console.log(line); } console.log(); // Add spacing between proxy statuses } /** * Main status command that orchestrates the entire status checking process */ async function statusCommand(args, verbose = false) { try { const parsedArgs = parseStatusArgs(args, verbose); // Initialize logger with verbose flag logger = new logger_1.Logger({ verbose: parsedArgs.verboseFlag }); logger.phase("Checking deployment status"); // Load configuration and secrets const { config, secrets } = await loadConfigurationAndSecrets(); const context = { config, secrets, verboseFlag: parsedArgs.verboseFlag, verboseMessages: [], // Initialize verboseMessages projectName: config.name, }; // Check and display status (this will complete the phase internally) await checkAndDisplayStatus(parsedArgs, context); console.log("[✓] Status check complete!"); } catch (error) { logger.error("Failed to get status", error); process.exit(1); } finally { logger.cleanup(); } } //# sourceMappingURL=status.js.map