UNPKG

iop

Version:
1,625 lines 69.5 kB
"use strict";
// Docker CLI 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;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.DockerClient = void 0;
exports.createProxyLoggingConfig = createProxyLoggingConfig;
exports.createServiceLoggingConfig = createServiceLoggingConfig;
const child_process_1 = require("child_process");
const util_1 = require("util");
const utils_1 = require("../utils");
const execAsync = (0, util_1.promisify)(child_process_1.exec);
class DockerClient {
    constructor(sshClient, serverHostname, verbose = false) {
        this.verbose = false;
        this.sshClient = sshClient;
        this.serverHostname = serverHostname;
        this.verbose = verbose;
    }
    /**
     * Log a message with server hostname prefix or general log if no server context
     */
    log(message) {
        if (!this.verbose)
            return;
        if (this.serverHostname) {
            console.log(`[${this.serverHostname}] ${message}`);
        }
        else {
            console.log(message);
        }
    }
    /**
     * Log a warning with server hostname prefix or general warning if no server context
     */
    logWarn(message) {
        if (!this.verbose)
            return;
        if (this.serverHostname) {
            console.warn(`[${this.serverHostname}] ${message}`);
        }
        else {
            console.warn(message);
        }
    }
    /**
     * Log an error with server hostname prefix or general error if no server context
     */
    logError(message) {
        if (this.serverHostname) {
            console.error(`[${this.serverHostname}] ${message}`);
        }
        else {
            console.error(message);
        }
    }
    /**
     * Execute a Docker command via SSH if sshClient is available
     */
    async execRemote(command) {
        if (!this.sshClient) {
            throw new Error("SSH client is not initialized for remote Docker command.");
        }
        try {
            return await this.sshClient.exec(`docker ${command}`);
        }
        catch (error) {
            // Check if this is a "non-error" Docker message (like image pull progress)
            const errorMessage = String(error);
            // Docker outputs image pull information to stderr, but it's not actually an error
            if (errorMessage.includes("Status: Downloaded newer image for") ||
                errorMessage.includes("Status: Image is up to date")) {
                this.log(`Docker image pull completed successfully (from stderr): ${errorMessage}`);
                // Return a successful response
                return "success";
            }
            // For exec commands that might be successful proxy operations, don't log immediately
            // Let the calling method (like execInContainer) determine if it's actually an error
            if (command.includes("exec")) {
                throw error; // Rethrow without logging to let execInContainer handle it
            }
            this.logError(`Remote Docker command failed: ${error}`);
            throw error;
        }
    }
    /**
     * Execute a command locally
     */
    static async _runLocalCommand(command, verbose = false) {
        if (verbose) {
            console.log(`Executing local command: ${command}`);
        }
        try {
            const { stdout, stderr } = await execAsync(command);
            if (stderr && verbose) {
                // Docker often prints non-error info to stderr, so log it but don't always throw
                console.warn(`Local command stderr: ${stderr}`);
            }
            return { stdout, stderr };
        }
        catch (error) {
            // Always show command and error details for build failures, even without --verbose
            // This ensures users can see what went wrong without needing to use --verbose flag
            console.error(`Local command failed: ${command}`);
            console.error(error);
            throw error;
        }
    }
    // --- Static methods for Local Docker Operations ---
    static async build(options) {
        let buildCommand = "docker build";
        if (options.dockerfile) {
            buildCommand += ` -f \"${options.dockerfile}\"`;
        }
        if (options.tags && options.tags.length > 0) {
            options.tags.forEach((tag) => {
                buildCommand += ` -t \"${tag}\"`;
            });
        }
        if (options.buildArgs) {
            Object.entries(options.buildArgs).forEach(([key, value]) => {
                buildCommand += ` --build-arg \"${key}=${value}\"`;
            });
        }
        if (options.target) {
            buildCommand += ` --target \"${options.target}\"`;
        }
        if (options.platform) {
            buildCommand += ` --platform \"${options.platform}\"`;
        }
        buildCommand += ` \"${options.context}\"`;
        if (options.verbose) {
            console.log(`Attempting to build image with command: ${buildCommand}`);
        }
        await DockerClient._runLocalCommand(buildCommand, options.verbose);
        if (options.verbose) {
            console.log("Docker build process completed.");
        }
    }
    static async tag(sourceImage, targetImage, verbose = false) {
        const command = `docker tag \"${sourceImage}\" \"${targetImage}\"`;
        if (verbose) {
            console.log(`Attempting to tag image: ${command}`);
        }
        await DockerClient._runLocalCommand(command, verbose);
        if (verbose) {
            console.log(`Successfully tagged \"${sourceImage}\" as \"${targetImage}\".`);
        }
    }
    /**
     * Save a Docker image to a tar archive
     */
    static async save(imageName, outputPath, verbose = false) {
        const command = `docker save \"${imageName}\" -o \"${outputPath}\"`;
        if (verbose) {
            console.log(`Saving image to archive: ${command}`);
        }
        await DockerClient._runLocalCommand(command, verbose);
        if (verbose) {
            console.log(`Successfully saved image \"${imageName}\" to \"${outputPath}\".`);
        }
    }
    /**
     * Save a Docker image to a compressed tar.gz archive for faster transfer
     * Tries pigz (parallel gzip) first for faster compression, falls back to gzip, then uncompressed tar
     */
    static async saveCompressed(imageName, outputPath, verbose = false) {
        try {
            const { exec } = await Promise.resolve().then(() => __importStar(require("child_process")));
            const { promisify } = await Promise.resolve().then(() => __importStar(require("util")));
            const execPromise = promisify(exec);
            // Try pigz first (parallel gzip - much faster)
            try {
                await execPromise("which pigz");
                if (verbose) {
                    console.log("pigz is available, using parallel compression");
                }
                const pigzCommand = `docker save "${imageName}" | pigz > "${outputPath}"`;
                if (verbose) {
                    console.log(`Saving compressed image to archive: ${pigzCommand}`);
                }
                await DockerClient._runLocalCommand(pigzCommand, verbose);
                if (verbose) {
                    console.log(`Successfully saved compressed image "${imageName}" to "${outputPath}" using pigz.`);
                }
                return;
            }
            catch (pigzCheckError) {
                if (verbose) {
                    console.log("pigz not available, trying gzip");
                }
            }
            // Fall back to standard gzip
            try {
                await execPromise("which gzip");
                if (verbose) {
                    console.log("gzip is available, using standard compression");
                }
                const gzipCommand = `docker save "${imageName}" | gzip > "${outputPath}"`;
                if (verbose) {
                    console.log(`Saving compressed image to archive: ${gzipCommand}`);
                }
                await DockerClient._runLocalCommand(gzipCommand, verbose);
                if (verbose) {
                    console.log(`Successfully saved compressed image "${imageName}" to "${outputPath}" using gzip.`);
                }
                return;
            }
            catch (gzipCheckError) {
                if (verbose) {
                    console.log("gzip not available, falling back to uncompressed tar");
                }
                // Fall back to regular save if gzip is not available
                const uncompressedPath = outputPath.replace(".tar.gz", ".tar");
                await DockerClient.save(imageName, uncompressedPath, verbose);
                return;
            }
        }
        catch (error) {
            if (verbose) {
                console.log(`Compression failed, falling back to uncompressed tar: ${error}`);
            }
            // Fall back to regular save if compression fails
            const uncompressedPath = outputPath.replace(".tar.gz", ".tar");
            await DockerClient.save(imageName, uncompressedPath, verbose);
        }
    }
    static async push(imageName, registry, verbose = false) {
        // If a specific registry (not Docker Hub) is provided, the imageName should already include it for push.
        // However, Docker CLI push can take the full image name including registry.
        // Example: my.registry.com/namespace/image:tag
        // If registry param is just hostname, it assumes official library image if no user/org part.
        // For simplicity, ensure imageName is the full path if not Docker Hub.
        const command = `docker push \"${imageName}\"`;
        if (verbose) {
            console.log(`Attempting to push image: ${command}`);
        }
        await DockerClient._runLocalCommand(command, verbose);
        if (verbose) {
            console.log(`Successfully pushed \"${imageName}\"${registry ? " to " + registry : ""}.`);
        }
    }
    // --- Instance methods for Remote Docker Operations (via SSH) ---
    /**
     * Check if Docker is installed and working on remote server
     */
    async checkInstallation() {
        try {
            await this.execRemote("info");
            this.log("Docker is installed and accessible.");
            return true;
        }
        catch (error) {
            this.logError(`Docker not found or 'docker info' failed: ${error}`);
            return false;
        }
    }
    /**
     * Install Docker if not already installed on remote server
     */
    async install() {
        if (!this.sshClient)
            throw new Error("SSH client required for remote install.");
        this.log("Installing Docker, curl, and git...");
        try {
            this.log("Running apt update...");
            await this.sshClient.exec("sudo apt update");
            this.log("Running apt upgrade...");
            await this.sshClient.exec("sudo apt upgrade -y");
            this.log("Installing docker.io, curl, git, pigz...");
            await this.sshClient.exec("sudo apt install -y docker.io curl git pigz");
            this.log("Adding user to docker group...");
            // Get current SSH username
            const currentUser = await this.sshClient.exec("whoami");
            await this.sshClient.exec(`sudo usermod -a -G docker ${currentUser.trim()}`);
            this.log("Successfully installed required packages and compression tools.");
            return true;
        }
        catch (error) {
            this.logError(`Failed to install Docker and dependencies: ${error}`);
            return false;
        }
    }
    /**
     * Login to Docker registry
     */
    async login(registry, username, password) {
        this.log(`Logging into Docker registry: ${registry}...`);
        try {
            if (!this.sshClient) {
                this.logError("SSH client not available for Docker login operation.");
                return false;
            }
            // Create a temporary file with the password on the remote server
            const tempFile = `/tmp/docker_login_${Date.now()}.tmp`;
            // Write password to temporary file without echoing it
            await this.sshClient.exec(`cat > ${tempFile} << 'EOF'
${password}
EOF`);
            // Set restrictive permissions
            await this.sshClient.exec(`chmod 600 ${tempFile}`);
            // Login using the file and immediately remove it
            try {
                await this.sshClient.exec(`cat ${tempFile} | docker login ${registry} -u "${username}" --password-stdin && rm -f ${tempFile}`);
                this.log(`Successfully logged into Docker registry: ${registry}.`);
                return true;
            }
            catch (loginError) {
                // Delete the temp file even if login fails
                await this.sshClient.exec(`rm -f ${tempFile}`);
                // Handle the common warning about unencrypted storage
                const errorMessage = String(loginError);
                if (errorMessage.includes("WARNING! Your password will be stored unencrypted")) {
                    // Still treat as success but don't mention the warning in user-facing logs
                    this.log(`Successfully logged into Docker registry: ${registry}.`);
                    return true;
                }
                // Don't include the error message which might contain sensitive info
                throw new Error("Docker login failed (see server logs for details)");
            }
        }
        catch (error) {
            const errorStr = String(error);
            // Sanitize any error message to prevent password disclosure
            if (errorStr.includes(password)) {
                this.logError(`Failed to log into Docker registry ${registry}: [Password redacted from logs]`);
            }
            else {
                this.logError(`Failed to log into Docker registry ${registry}: ${error}`);
            }
            return false;
        }
    }
    /**
     * Logout from Docker registry
     */
    async logout(registry) {
        this.log(`Logging out from Docker registry: ${registry}...`);
        try {
            await this.execRemote(`logout ${registry}`);
            this.log(`Successfully logged out from Docker registry: ${registry}.`);
            return true;
        }
        catch (error) {
            this.logError(`Failed to log out from Docker registry ${registry}: ${error}`);
            return false;
        }
    }
    /**
     * Pull a Docker image
     */
    async pullImage(image) {
        this.log(`Pulling image ${image}...`);
        try {
            await this.execRemote(`pull ${image}`);
            this.log(`Successfully pulled image ${image}.`);
            return true;
        }
        catch (error) {
            this.logError(`Failed to pull image ${image}: ${error}`);
            return false;
        }
    }
    /**
     * Force pull a Docker image, ensuring we get the latest version from the registry
     * This removes the image first if it exists locally, then pulls it again
     */
    async forcePullImage(image) {
        this.log(`Force pulling latest image ${image}...`);
        try {
            // First try to remove the image (ignoring errors if it doesn't exist)
            try {
                await this.execRemote(`rmi ${image}`);
                this.log(`Removed existing image ${image} to ensure we get the latest version.`);
            }
            catch (rmError) {
                // Ignore errors - the image might not exist locally or might be in use
                // Only log this in verbose mode since it's expected
                if (this.verbose) {
                    this.log(`Could not remove existing image ${image} (expected if image doesn't exist locally), will pull fresh copy.`);
                }
            }
            // Now pull the image
            await this.execRemote(`pull ${image}`);
            this.log(`Successfully pulled latest image ${image}.`);
            return true;
        }
        catch (error) {
            this.logError(`Failed to force pull image ${image}: ${error}`);
            return false;
        }
    }
    /**
     * Load a Docker image from a tar archive
     */
    async loadImage(archivePath) {
        this.log(`Loading image from archive ${archivePath}...`);
        try {
            // Check if it's a compressed archive by file extension
            if (archivePath.endsWith(".tar.gz") || archivePath.endsWith(".tgz")) {
                return await this.loadCompressedImage(archivePath);
            }
            await this.execRemote(`load -i "${archivePath}"`);
            this.log(`Successfully loaded image from ${archivePath}.`);
            return true;
        }
        catch (error) {
            this.logError(`Failed to load image from ${archivePath}: ${error}`);
            return false;
        }
    }
    /**
     * Load a Docker image from a compressed tar.gz archive
     * Tries pigz (parallel decompression) first, then gunzip, then uncompressed fallback
     */
    async loadCompressedImage(archivePath) {
        this.log(`Loading compressed image from archive ${archivePath}...`);
        try {
            // Try pigz first (parallel decompression - much faster)
            try {
                await this.sshClient?.exec("which pigz");
                this.log("pigz is available on remote server, using parallel decompression");
                const pigzCommand = `sh -c "pigz -dc '${archivePath}' | docker load"`;
                await this.sshClient?.exec(pigzCommand);
                this.log(`Successfully loaded compressed image from ${archivePath} using pigz.`);
                return true;
            }
            catch (pigzCheckError) {
                this.log("pigz not available on remote server, trying gunzip");
            }
            // Fall back to standard gunzip
            try {
                await this.sshClient?.exec("which gunzip");
                this.log("gunzip is available on remote server, using standard decompression");
                const gunzipCommand = `sh -c "gunzip -c '${archivePath}' | docker load"`;
                await this.sshClient?.exec(gunzipCommand);
                this.log(`Successfully loaded compressed image from ${archivePath} using gunzip.`);
                return true;
            }
            catch (gunzipCheckError) {
                this.log("gunzip not available on remote server, falling back to regular docker load");
                // Fall back to regular docker load (assuming file might not be compressed)
                await this.execRemote(`load -i "${archivePath}"`);
                this.log(`Successfully loaded image from ${archivePath} (fallback method).`);
                return true;
            }
        }
        catch (error) {
            this.logError(`Failed to load compressed image from ${archivePath}: ${error}`);
            // Try one more fallback - maybe the file isn't actually compressed
            try {
                this.log(`Attempting fallback: treating ${archivePath} as uncompressed`);
                await this.execRemote(`load -i "${archivePath}"`);
                this.log(`Successfully loaded image from ${archivePath} (fallback method).`);
                return true;
            }
            catch (fallbackError) {
                this.logError(`All load attempts failed for ${archivePath}: ${fallbackError}`);
                return false;
            }
        }
    }
    /**
     * Check if a network exists
     */
    async networkExists(name) {
        try {
            const result = await this.execRemote(`network ls --filter name=${name} --format "{{.Name}}"`);
            // Split by lines and check if any line exactly matches the network name
            // This handles the case where Docker's name filter returns multiple partial matches
            const networks = result
                .trim()
                .split("\n")
                .filter((line) => line.trim());
            return networks.includes(name);
        }
        catch {
            return false;
        }
    }
    /**
     * Create a Docker network
     */
    async createNetwork(options) {
        try {
            const exists = await this.networkExists(options.name);
            if (exists) {
                this.log(`Docker network ${options.name} already exists.`);
                return true;
            }
            // Ensure network name is project specific if generated via new utility elsewhere
            this.log(`Creating Docker network: ${options.name}`);
            await this.execRemote(`network create ${options.name}`);
            this.log(`Created Docker network: ${options.name}`);
            return true;
        }
        catch (error) {
            this.logError(`Failed to create Docker network: ${error}`);
            return false;
        }
    }
    /**
     * Check if a container is connected to a specific network
     */
    async isContainerConnectedToNetwork(containerName, networkName) {
        try {
            const result = await this.execRemote(`inspect ${containerName} --format "{{range .NetworkSettings.Networks}}{{.NetworkID}} {{end}}"`);
            // Get the network ID for the network name
            const networkInfo = await this.execRemote(`network inspect ${networkName} --format "{{.Id}}"`);
            const networkId = networkInfo.trim();
            // Check if the container's networks include this network ID
            return result.includes(networkId);
        }
        catch {
            return false;
        }
    }
    /**
     * Connect a container to a network
     */
    async connectContainerToNetwork(containerName, networkName) {
        try {
            await this.execRemote(`network connect ${networkName} ${containerName}`);
            this.log(`Connected ${containerName} to network ${networkName}`);
            return true;
        }
        catch (error) {
            this.logError(`Failed to connect ${containerName} to network ${networkName}: ${error}`);
            return false;
        }
    }
    /**
     * Check if a container exists
     */
    async containerExists(name) {
        try {
            const result = await this.execRemote(`ps -a --filter "name=${name}" --format "{{.Names}}"`);
            return result.trim() === name;
        }
        catch {
            return false;
        }
    }
    /**
     * Check if a container is running
     */
    async containerIsRunning(name) {
        try {
            const result = await this.execRemote(`ps --filter "name=${name}" --format "{{.Names}}"`);
            return result.trim() === name;
        }
        catch {
            return false;
        }
    }
    /**
     * Start an existing container
     */
    async startContainer(name) {
        try {
            await this.execRemote(`start ${name}`);
            this.log(`Started container ${name}.`);
            return true;
        }
        catch (error) {
            this.logError(`Failed to start container ${name}: ${error}`);
            return false;
        }
    }
    /**
     * Stop a container
     */
    async stopContainer(name) {
        try {
            await this.execRemote(`stop ${name}`);
            this.log(`Stopped container ${name}.`);
            return true;
        }
        catch (error) {
            this.logError(`Failed to stop container ${name}: ${error}`);
            return false;
        }
    }
    /**
     * Remove a container
     */
    async removeContainer(name) {
        try {
            await this.execRemote(`rm ${name}`);
            this.log(`Removed container ${name}.`);
            return true;
        }
        catch (error) {
            this.logError(`Failed to remove container ${name}: ${error}`);
            return false;
        }
    }
    /**
     * Create and run a new container
     */
    async createContainer(options) {
        try {
            // Build the docker run command
            let cmd = `run -d --name ${options.name}`;
            // Add labels if specified
            if (options.labels) {
                Object.entries(options.labels).forEach(([key, value]) => {
                    cmd += ` --label ${key}="${value}"`;
                });
            }
            // Add network if specified
            if (options.network) {
                cmd += ` --network ${options.network}`;
                // Add network aliases if specified
                if (options.networkAliases && options.networkAliases.length > 0) {
                    options.networkAliases.forEach((alias) => {
                        cmd += ` --network-alias ${alias}`;
                    });
                }
            }
            // Add restart policy
            if (options.restart) {
                cmd += ` --restart ${options.restart}`;
            }
            else {
                cmd += ` --restart unless-stopped`;
            }
            // Add ports
            if (options.ports && options.ports.length > 0) {
                options.ports.forEach((port) => {
                    cmd += ` -p ${port}`;
                });
            }
            // Add volumes
            if (options.volumes && options.volumes.length > 0) {
                options.volumes.forEach((volume) => {
                    cmd += ` -v ${volume}`;
                });
            }
            // Add environment variables
            if (options.envVars) {
                Object.entries(options.envVars).forEach(([key, value]) => {
                    // Escape special characters in value
                    const escapedValue = value.replace(/[\$"`\\]/g, "\\$&");
                    cmd += ` -e ${key}="${escapedValue}"`;
                });
            }
            // Add logging configuration
            if (options.logDriver) {
                cmd += ` --log-driver ${options.logDriver}`;
                if (options.logOpts) {
                    Object.entries(options.logOpts).forEach(([key, value]) => {
                        cmd += ` --log-opt ${key}=${value}`;
                    });
                }
            }
            // Add the image
            cmd += ` ${options.image}`;
            // Add custom command if specified
            if (options.command) {
                cmd += ` ${options.command}`;
            }
            // Execute the command
            await this.execRemote(cmd);
            this.log(`Created and started container ${options.name}.`);
            return true;
        }
        catch (error) {
            this.logError(`Failed to create container ${options.name}: ${error}`);
            return false;
        }
    }
    /**
     * Ensure a container is running - creates it if it doesn't exist, starts it if stopped
     */
    async ensureContainer(options) {
        const exists = await this.containerExists(options.name);
        if (exists) {
            const running = await this.containerIsRunning(options.name);
            if (running) {
                this.log(`Container ${options.name} is already running.`);
                return true;
            }
            else {
                return await this.startContainer(options.name);
            }
        }
        else {
            return await this.createContainer(options);
        }
    }
    /**
     * Get the health status of a container.
     * Returns 'healthy', 'unhealthy', 'starting', or null if health status is not available.
     */
    async getContainerHealth(containerName) {
        this.log(`Checking health of container ${containerName}...`);
        try {
            // Verify the container exists before trying to inspect it
            const containerExists = await this.containerExists(containerName);
            if (!containerExists) {
                this.logError(`Container ${containerName} does not exist. Cannot perform health check.`);
                return null;
            }
            // The --format '{{json .State.Health}}' will output the Health object as a JSON string or an empty string if no health check.
            // If there's no Health object (e.g. container doesn't have a health check configured), it might return GO template error or empty.
            // A more robust way is to get the full state and check for the Health property.
            const inspectOutput = await this.execRemote(`inspect ${containerName}`);
            const inspectJson = JSON.parse(inspectOutput);
            if (inspectJson &&
                inspectJson.length > 0 &&
                inspectJson[0].State &&
                inspectJson[0].State.Health) {
                const healthStatus = inspectJson[0].State.Health.Status;
                this.log(`Container ${containerName} health status: ${healthStatus}`);
                return healthStatus; // e.g., "healthy", "unhealthy", "starting"
            }
            this.log(`Container ${containerName} does not have health check information.`);
            return null; // No health check configured or found
        }
        catch (error) {
            this.logError(`Failed to get health status for container ${containerName}: ${error}`);
            // If container doesn't exist, inspect will fail. This should be handled as an error or a specific status.
            // For now, assume an error means we can't determine health.
            return "unhealthy"; // Treat errors in fetching health as unhealthy for safety
        }
    }
    /**
     * Find all containers (running or stopped) whose names match a prefix
     * @param namePrefix The container name prefix to match
     * @returns Array of container names
     */
    async findContainersByPrefix(namePrefix) {
        try {
            const result = await this.execRemote(`ps -a --filter "name=${namePrefix}" --format "{{.Names}}"`);
            if (!result.trim()) {
                return [];
            }
            return result.trim().split("\n");
        }
        catch (error) {
            this.logError(`Failed to find containers by prefix ${namePrefix}: ${error}`);
            return [];
        }
    }
    /**
     * Run a health check using project-specific DNS targets
     * @param proxyContainerName Name of the iop-proxy container (should be "iop-proxy")
     * @param targetNetworkAlias Network alias of the container to check (e.g., "web") - DEPRECATED, use projectSpecificTarget
     * @param targetContainerName Name of the container to check
     * @param projectName The project name for network isolation
     * @param appPort The port the app is listening on (default: 80)
     * @param healthCheckPath The health check endpoint path (default: "/up")
     * @returns true if the health check endpoint returns 200, false otherwise
     */
    async checkHealthWithIopProxy(proxyContainerName, targetNetworkAlias, targetContainerName, projectName, appPort = 80, healthCheckPath = "/up") {
        try {
            // Use project-specific target directly (dual alias solution)
            const appName = targetNetworkAlias; // Assuming targetNetworkAlias is the app name
            const projectSpecificTarget = `${projectName}-${appName}`;
            this.log(`Using project-specific health check for ${targetContainerName} (project: ${projectName}, target: ${projectSpecificTarget}:${appPort}${healthCheckPath})`);
            // Retry the health check if it fails. Give containers up to 30 seconds to start up.
            let statusCode = "";
            let success = false;
            const maxAttempts = 30;
            for (let attempt = 0; attempt < maxAttempts; attempt++) {
                try {
                    // Use project-specific DNS target directly
                    const targetURL = `http://${projectSpecificTarget}:${appPort}${healthCheckPath}`;
                    // Install curl if needed and run health check
                    const installCurlCmd = `exec ${proxyContainerName} sh -c "command -v curl >/dev/null 2>&1 || (apt-get update && apt-get install -y curl)"`;
                    await this.execRemote(installCurlCmd);
                    // Separate health check command with proper quote escaping and newline separation
                    const healthCheckCmd = `exec ${proxyContainerName} sh -c "curl -s -o /dev/null -w '%{http_code}\\n' --connect-timeout 3 --max-time 5 ${targetURL}"`;
                    statusCode = await this.execRemote(healthCheckCmd);
                    // Check if we got a successful status code
                    const cleanStatusCode = statusCode.trim();
                    if (cleanStatusCode === "200") {
                        success = true;
                        this.log(`Health check for ${targetContainerName} passed on attempt ${attempt + 1}/${maxAttempts} (project: ${projectName}, target: ${projectSpecificTarget}:${appPort}${healthCheckPath})`);
                        break;
                    }
                    else {
                        if (attempt < maxAttempts - 1) {
                            this.log(`Health check attempt ${attempt + 1}/${maxAttempts} returned status: ${cleanStatusCode}, retrying in 1 second...`);
                        }
                    }
                }
                catch (execError) {
                    if (attempt < maxAttempts - 1) {
                        this.log(`Health check attempt ${attempt + 1}/${maxAttempts} failed: ${execError}, retrying in 1 second...`);
                    }
                    else {
                        this.logError(`Health check attempt ${attempt + 1}/${maxAttempts} failed: ${execError}`);
                    }
                }
                // Wait 1 second before next attempt (unless this was the last attempt)
                if (attempt < maxAttempts - 1) {
                    await new Promise((resolve) => setTimeout(resolve, 1000));
                }
            }
            if (!success) {
                this.logError(`All ${maxAttempts} health check attempts failed for ${targetContainerName} in project ${projectName}`);
                return false;
            }
            return true;
        }
        catch (error) {
            this.logError(`Health check failed for ${targetContainerName}: ${error}`);
            return false;
        }
    }
    /**
     * Execute a command inside a running container
     */
    async execInContainer(containerName, command) {
        this.log(`Executing command in container ${containerName}: ${command}`);
        try {
            const output = await this.execRemote(`exec ${containerName} ${command}`);
            this.log(`Successfully executed command in container ${containerName}.`);
            return { success: true, output };
        }
        catch (error) {
            const errorOutput = String(error);
            // Check if this is actually a successful operation based on output patterns
            // This handles cases where commands work correctly but return non-zero exit codes
            if (errorOutput.includes("Route deployed successfully") ||
                errorOutput.includes("successfully configured") ||
                errorOutput.includes("SSL certificate obtained") ||
                errorOutput.includes("Health status for") ||
                errorOutput.includes("updated to true") ||
                errorOutput.includes("updated to false") ||
                (errorOutput.includes("Domain") &&
                    errorOutput.includes("added to certificate manager")) ||
                errorOutput.includes("SSL certificate will be provisioned automatically") ||
                errorOutput.includes("Adding domain to certificate manager") ||
                errorOutput.includes("Created certificate reload trigger") ||
                errorOutput.includes("Scheduling SSL certificate provisioning") ||
                (errorOutput.includes("Added") &&
                    errorOutput.includes("to certificate retry queue")) ||
                (errorOutput.includes("Added") &&
                    errorOutput.includes("to background certificate retry queue")) ||
                (errorOutput.includes("Route for host") &&
                    errorOutput.includes("successfully configured")) ||
                errorOutput.includes("✅") ||
                errorOutput.includes("📋") // Success emojis from proxy
            ) {
                // For proxy commands, only show detailed output in verbose mode
                if (this.verbose) {
                    this.log(`Command succeeded despite non-zero exit code in container ${containerName}:`);
                    console.log(errorOutput); // Show the full output in verbose mode
                }
                else {
                    this.log(`Command succeeded despite non-zero exit code in container ${containerName}.`);
                }
                return { success: true, output: errorOutput };
            }
            this.logError(`Failed to execute command in container ${containerName}: ${errorOutput}`);
            return { success: false, output: errorOutput };
        }
    }
    /**
     * Prune unused Docker resources (containers, networks, images, build cache) on the remote server.
     */
    async prune() {
        this.log("Pruning Docker resources (system prune -af)...");
        try {
            await this.execRemote("system prune -af");
            this.log("Successfully pruned Docker resources.");
            return true;
        }
        catch (error) {
            this.logError(`Failed to prune Docker resources: ${error}`);
            return false;
        }
    }
    /**
     * Prune all unused Docker images on the remote server.
     */
    async pruneImages() {
        try {
            await this.execRemote("image prune -a -f");
            return true;
        }
        catch (error) {
            this.logError(`Failed to prune Docker images: ${error}`);
            return false;
        }
    }
    /**
     * Check available disk space and return info about usage
     */
    async checkDiskSpace() {
        if (!this.sshClient) {
            this.logError("SSH client not available for disk space check");
            return { available: 0, used: 0, total: 0, usedPercent: 100 };
        }
        try {
            const output = await this.sshClient.exec("df -BG / | tail -1");
            // Parse output like: /dev/vda1      25G   15G   10G  60% /
            const parts = output.trim().split(/\s+/);
            if (parts.length >= 6) {
                const total = parseInt(parts[1].replace('G', ''));
                const used = parseInt(parts[2].replace('G', ''));
                const available = parseInt(parts[3].replace('G', ''));
                const usedPercent = parseInt(parts[4].replace('%', ''));
                return { available, used, total, usedPercent };
            }
            throw new Error("Could not parse disk space output");
        }
        catch (error) {
            this.logError(`Failed to check disk space: ${error}`);
            return { available: 0, used: 0, total: 0, usedPercent: 100 };
        }
    }
    /**
     * Clean up old iop deployment artifacts from /tmp
     */
    async cleanupTempFiles() {
        if (!this.sshClient) {
            this.logError("SSH client not available for temp file cleanup");
            return false;
        }
        try {
            await this.sshClient.exec("find /tmp -name 'iop-*.tar.gz' -mtime +1 -delete 2>/dev/null || true");
            await this.sshClient.exec("find /tmp -name 'iop-*.tar' -mtime +1 -delete 2>/dev/null || true");
            return true;
        }
        catch (error) {
            this.logError(`Failed to cleanup temp files: ${error}`);
            return false;
        }
    }
    /**
     * Perform comprehensive disk cleanup before deployment
     */
    async performPreDeploymentCleanup() {
        const spaceInfoBefore = await this.checkDiskSpace();
        // Always clean up temp files - they're just leftover artifacts
        const tempFilesCleanedUp = await this.cleanupTempFiles();
        let dockerImagesCleanedUp = false;
        // Only perform Docker image cleanup if disk usage is high (> 80%) or available space is low (< 2GB)
        if (spaceInfoBefore.usedPercent > 80 || spaceInfoBefore.available < 2) {
            dockerImagesCleanedUp = await this.pruneImages();
        }
        const spaceInfoAfter = await this.checkDiskSpace();
        return {
            success: true,
            spaceBefore: spaceInfoBefore.available,
            spaceAfter: spaceInfoAfter.available,
            tempFilesCleanedUp,
            dockerImagesCleanedUp
        };
    }
    /**
     * Inspect a container and return its configuration
     */
    async inspectContainer(containerName) {
        try {
            const inspectOutput = await this.execRemote(`inspect ${containerName}`);
            const inspectData = JSON.parse(inspectOutput);
            if (!inspectData || inspectData.length === 0) {
                return null;
            }
            return inspectData[0];
        }
        catch (error) {
            this.logError(`Failed to inspect container ${containerName}: ${error}`);
            return null;
        }
    }
    /**
     * Convert a iop service definition to Docker container options
     */
    static serviceToContainerOptions(service, projectName, secrets) {
        const containerName = `${projectName}-${service.name}`;
        const options = {
            name: containerName,
            image: service.image,
            network: `${projectName}-network`,
            networkAliases: [service.name], // Add service name as network alias (e.g., "db")
            ports: service.ports,
            volumes: (0, utils_1.processVolumes)(service.volumes, projectName),
            envVars: {},
            command: service.command,
            labels: {
                "iop.managed": "true",
                "iop.project": projectName,
                "iop.type": "service",
                "iop.service": service.name,
            },
        };
        // Add environment variables
        if (service.environment?.plain) {
            for (const envVar of service.environment.plain) {
                const [key, ...valueParts] = envVar.split("=");
                if (key && valueParts.length > 0) {
                    options.envVars[key] = valueParts.join("=");
                }
            }
        }
        // Secret environment variables
        if (service.environment?.secret) {
            service.environment.secret.forEach((secretName) => {
                const secretValue = secrets[secretName];
                if (secretValue) {
                    options.envVars[secretName] = secretValue;
                }
            });
        }
        // Add logging configuration for user services
        const loggingConfig = createServiceLoggingConfig();
        options.logDriver = loggingConfig.logDriver;
        options.logOpts = loggingConfig.logOpts;
        return options;
    }
    /**
     * Find containers by label filter
     * @param labelFilter Docker label filter string (e.g., "iop.app=blog", "iop.color=blue")
     * @returns Array of container names matching the filter
     */
    async findContainersByLabel(labelFilter) {
        try {
            const result = await this.execRemote(`ps -a --filter "label=${labelFilter}" --format "{{.Names}}"`);
            if (!result.trim()) {
                return [];
            }
            return result.trim().split("\n");
        }
        catch (error) {
            this.logError(`Failed to find containers by label ${labelFilter}: ${error}`);
            return [];
        }
    }
    /**
     * Find containers by label filter within a specific project
     * @param labelFilter Docker label filter string (e.g., "iop.app=web", "iop.color=blue")
     * @param projectName Project name to scope the search to
     * @returns Array of container names matching the filter within the project
     */
    async findContainersByLabelAndProject(labelFilter, projectName) {
        try {
            const result = await this.execRemote(`ps -a --filter "label=${labelFilter}" --filter "label=iop.project=${projectName}" --format "{{.Names}}"`);
            if (!result.trim()) {
                return [];
            }
            return result.trim().split("\n");
        }
        catch (error) {
            this.logError(`Failed to find containers by label ${labelFilter} in project ${projectName}: ${error}`);
            return [];
        }
    }
    /**
     * Get container labels
     * @param containerName Name of the container
     * @returns Object with container labels or empty object if none found
     */
    async getContainerLabels(containerName) {
        try {
            const result = await this.execRemote(`inspect ${containerName} --format '{{json .Config.Labels}}'`);
            if (!result.trim() || result.trim() === "null") {
                return {};
            }
            return JSON.parse(result.trim());
        }
        catch (error) {
            this.logError(`Failed to get labels for container ${containerName}: ${error}`);
            return {};
        }
    }
    /**
     * Determines the current active color (blue/green) for an app
     * @param appName Name of the app
     * @returns 'blue', 'green', or null if no containers exist
     */
    async getCurrentActiveColor(appName) {
        try {
            const containers = await this.findContainersByLabel(`iop.app=${appName}`);
            if (containers.length === 0) {
                return null; // No containers exist
            }
            // First, try to find containers explicitly marked as active
            for (const containerName of containers) {
                const labels = await this.getContainerLabels(containerName);
                if (labels["iop.active"] === "true") {
                    return labels["iop.color"];
                }
            }
            // If no containers are marked as active, check which containers have the main network alias
            // This handles the case where containers exist but active labels aren't set
            for (const containerName of containers) {
                try {
                    // Check if this container has the main network alias (indicating it's active)
                    const inspectOutput = await this.execRemote(`inspect ${containerName} --format "{{json .NetworkSettings.Networks}}"`);
                    const networks = JSON.parse(inspectOutput);
                    // Look for the main app alias in any network
                    for (const networkData of Object.values(networks)) {
                        if (networkData.Aliases && networkData.Aliases.includes(appName)) {
                            const labels = await this.getContainerLabels(containerName);
                            return labels["iop.color"];
                        }
                    }
                }
                catch (error) {
                    // Continue checking other containers if one fails
                    continue;
                }
            }
            // If we still can't determine active color, return the color of the first running container
            for (const containerName of containers) {
                const isRunning = await this.containerIsRunning(containerName);
                if (isRunning) {
                    const labels = await this.getContainerLabels(containerName);
                    return labels["iop.color"];
                }
            }
            return null;
        }
        catch (error) {
            this.logError(`Failed to determine active color for app ${appName}: ${error}`);
            return null;
        }
    }
    /**
     * Determines the current active color (blue/green) for an app within a specific project
     * @param appName Name of the app
     * @param projectName Project name to scope the search to
     * @returns 'blue', 'green', or null if no containers exist
     */
    async getCurrentActiveColorForProject(appName, projectName) {
        try {
            const containers = await this.findContainersByLabelAndProject(`iop.app=${appName}`, projectName);
            if (containers.length === 0) {
                return null; // No containers exist
            }
            // First, try to find containers explicitly marked as active
            for (const containerName of containers) {
                const labels = await this.getContainerLabels(containerName);
                if (labels["iop.active"] === "true") {
                    return labels["iop.color"];
                }
            }
            // If no containers are marked as active, check which containers have the main network alias
            // This handles the case where containers exist but active labels aren't set
            for (const containerName of containers) {
                try {
                    // Check if this container has the main network alias (indicating it's active)
                    const inspectOutput = await this.execRemote(`inspect ${containerName} --format "{{json .NetworkSettings.Networks}}"`);
                    const networks = JSON.parse(inspectOutput);
                    // Look for the main app alias in any network
                    for (const networkData of Object.values(networks)) {
                        if (networkData.Aliases && networkData.Aliases.includes(appName)) {
                            const labels = await this.getContainerLabels(containerName);
                            return labels["iop.color"];
                        }
                    }
                }
                catch (error) {
                    // Continue checking other containers if one fails
                    continue;
                }
            }
            // If we still can't determine active color, return the color of the first running container
            for (const containerName of containers) {
                const isRunning = await this.containerIsRunning(containerName);
                if (isRunning) {
                    const labels = await this.getContainerLabels(containerName);
                    return labels["iop.color"];
                }
            }
            return null;
        }
        catch (error) {
            this.logError(`Failed to determine active color for app ${appName} in project ${projectName}: ${error}`);
            return null;
        }
    }
    /**
     * Gets the inactive color (opposite of current active)
     * @param appName Name of the app
     * @returns 'blue' or 'green' - the color that should be used for new deployment
     */
    async getInactiveColor(appName) {
        const activeColor = await this.getCurrentActiveColor(appName);
        return activeColor === "blue" ? "green" : "blue";
    }
    /**
     * Gets the inactive color (opposite of current active) for a specific project
     * @param appName Name of the app
     * @param projectName Project name to scope the search to
     * @returns 'blue' or 'green' - the color that should be used for new deployment
     */
    async getInactiveColorForProject(appName, projectName) {
        const activeColor = await this.getCurrentActiveColorForProject(appName, projectName);
        return activeColor === "blue" ? "green" : "blue";
    }
    /**
     * Creates a container with zero-downtime deployment labels
     * @param options Standard container options
     * @param appName Application name
     * @param color Color for this deployment (blue/green)
     * @param replicaIndex Replica index (1-based)
     * @param active Whether this container is currently active
     * @returns true if container was created successfully
     */
    async createContainerWithLabels(options, appName, color, replicaIndex, active) {
        try {
            // Add zero-downtime deployment labels
            const extendedOptions = {
                ...options,
                labels: {
                    ...options.labels,
                    "iop.app": appName,
                    "iop.color": color,
                    "iop.replica": replicaIndex.toString(),
                    "iop.active": active.toString(),
                    "iop.managed": "true",
                },
            };
            this.log(`Creating container: ${options.name}`);
            return await this.createContainer(extendedOptions);
        }
        catch (error) {
            this.logError(`Failed to create container with labels: ${error}`);
            return false;
        }
    }
    /**
     * Switches network aliases from old color to new color atomically
     * @param appName Name of the app
     * @param newColor The color to switch to
     * @param networkName The network name
     * @returns true if successful
     */
    async switchNetworkAlias(appName, newColor, networkName) {
        try {
            this.log(`Switching network alias for ${appName} to ${newColor} containers`);
            // Get all containers for the new color
            const newContainers = await this.findContainersByLabel(`iop.app=${appName}`);
            const newColorContainers = [];
            for (const containerName of newContainers) {
                const labels = await this.getContainerLabels(containerName);
                if (labels["iop.color"] === newColor) {
                    newColorContainers.push(containerName);
                }
            }
            if (newColorContainers.length === 0) {
                this.logError(`No ${newColor} containers found for app ${appName}`);
                return false;
            }
            // For each new container, disconnect from network and reconnect with main alias
            for (const containerName of newColorContainers) {
                try {
                    // Disconnect from network (removes temporary alias)
                    await this.execRemote(`network disconnect ${networkName} ${containerName} || true`);
                    // Reconnect with the main alias
                    await this.execRemote(`network connect --alias ${appName} ${networkName} ${containerName}`);
                    this.log(`Updated network alias for container ${containerName}`);
                }
                catch (error) {
                    this.logError(`Failed to update network alias for ${containerName}: ${error}`);
                    return false;
                }
            }
            return true;
        }
        catch (error) {
            this.logError(`Failed to switch network alias for ${appName}: ${error}`);
            return false;
        }
    }
    /**
     * Switches network aliases from old color to new color atomically within a specific project
     * @param appName Name of the app
     * @param newColor The color to switch to
     * @param networkName The network name
     * @param projectName Project name to scope the search to
     * @returns true if successful
     */
    async switchNetworkAliasForProject(appName, newColor, networkName, projectName) {
        try {
            this.log(`Switching network alias for ${appName} to ${newColor} containers in project ${projectName}`);
            // Get all containers for the new color within the project
            const newContainers = await this.findContainersByLabelAndProject(`iop.app=${appName}`, projectName);
            const newColorContainers = [];
            for (const containerName of newContainers) {
                const labels = await this.getContainerLabels(containerName);
                if (labels["iop.color"] === newColor) {
                    newColorContainers.push(containerName);
                }
            }
            if (newColorContainers.length === 0) {
                this.logError(`No ${newColor} containers found for app ${appName} in project ${projectName}`);
                return false;
            }
            // For each new container, disconnect from network and reconnect with dual aliases
            for (const containerName of newColorContainers) {
                try {
                    // Disconnect from network (removes temporary alias)
                    await this.execRemote(`network disconnect ${networkName} ${containerName} || true`);
                    // Reconnect with both aliases (dual alias approach)
                    const projectSpecificAlias = `${projectName}-${appName}`;
                    await this.execRemote(`network connect --alias ${appName} --alias ${projectSpecificAlias} ${networkName} ${containerName}`);
                    this.log(`Updated network alias for container ${containerName}`);
                }
                catch (error) {
                    this.logError(`Failed to update network alias for ${containerName}: ${error}`);
                    return false;
                }
            }
            return true;
        }
        catch (error) {
            this.logError(`Failed to switch network alias for ${appName} in project ${projectName}: ${error}`);
            return false;
        }
    }
    /**
     * Updates container labels to mark them as active/inactive
     * Note: Docker doesn't support updating labels after container creation,
     * so this function is a no-op. Labels are set correctly during creation.
     * @param appName Name of the app
     * @param activeColor The color to mark as active
     * @returns true (always successful since labels are set during creation)
     */
    async updateActiveLabels(appName, activeColor) {
        this.log(`Active labels for ${appName} are set correctly during container creation`);
        // Docker doesn't support updating labels after creation
        // Labels are already set correctly in createContainerWithLabels()
        return true;
    }
    /**
     * Performs graceful shutdown of containers with SIGTERM
     * @param containerNames Array of container names to shut down
     * @param gracefulTimeoutSeconds Time to wait for graceful shutdown (default: 30)
     * @returns true if all containers shut down successfully
     */
    async gracefulShutdown(containerNames, gracefulTimeoutSeconds = 30) {
        try {
            this.log(`Starting graceful shutdown of ${containerNames.length} containers with ${gracefulTimeoutSeconds}s timeout`);
            for (const containerName of containerNames) {
                try {
                    // Send SIGTERM and wait for graceful shutdown
                    await this.execRemote(`stop --time ${gracefulTimeoutSeconds} ${containerName}`);
                    this.log(`Gracefully stopped container ${containerName}`);
                }
                catch (error) {
                    this.logWarn(`Failed to gracefully stop ${containerName}: ${error}`);
                }
            }
            return true;
        }
        catch (error) {
            this.logError(`Failed during graceful shutdown: ${error}`);
            return false;
        }
    }
    /**
     * Find all containers managed by iop for a specific project
     * @param projectName The project name to filter by
     * @returns Array of container names belonging to the project
     */
    async findProjectContainers(projectName) {
        try {
            return await this.findContainersByLabel(`iop.project=${projectName}`);
        }
        catch (error) {
            this.logError(`Failed to find containers for project ${projectName}: ${error}`);
            return [];
        }
    }
    /**
     * Find all app containers for a specific project
     * @param projectName The project name to filter by
     * @returns Array of container names for apps in the project
     */
    async findProjectAppContainers(projectName) {
        try {
            const result = await this.execRemote(`ps -a --filter "label=iop.project=${projectName}" --filter "label=iop.type=app" --format "{{.Names}}"`);
            if (!result.trim()) {
                return [];
            }
            return result.trim().split("\n");
        }
        catch (error) {
            this.logError(`Failed to find app containers for project ${projectName}: ${error}`);
            return [];
        }
    }
    /**
     * Find all service containers for a specific project
     * @param projectName The project name to filter by
     * @returns Array of container names for services in the project
     */
    async findProjectServiceContainers(projectName) {
        try {
            const result = await this.execRemote(`ps -a --filter "label=iop.project=${projectName}" --filter "label=iop.type=service" --format "{{.Names}}"`);
            if (!result.trim()) {
                return [];
            }
            return result.trim().split("\n");
        }
        catch (error) {
            this.logError(`Failed to find service containers for project ${projectName}: ${error}`);
            return [];
        }
    }
    /**
     * Get detailed information about a project's containers
     * @param projectName The project name to analyze
     * @returns Object with apps and services currently deployed
     */
    async getProjectCurrentState(projectName) {
        const state = {
            apps: {},
            services: {},
        };
        try {
            const allContainers = await this.findProjectContainers(projectName);
            for (const containerName of allContainers) {
                const labels = await this.getContainerLabels(containerName);
                if (labels["iop.type"] === "app" && labels["iop.app"]) {
                    const appName = labels["iop.app"];
                    if (!state.apps[appName]) {
                        state.apps[appName] = [];
                    }
                    state.apps[appName].push(containerName);
                }
                else if (labels["iop.type"] === "service" &&
                    labels["iop.service"]) {
                    const serviceName = labels["iop.service"];
                    state.services[serviceName] = containerName;
                }
            }
            return state;
        }
        catch (error) {
            this.logError(`Failed to get current state for project ${projectName}: ${error}`);
            return state;
        }
    }
    /**
     * Get container uptime in a human-readable format
     * @param containerName Name of the container
     * @returns Human-readable uptime string (e.g., "2h 15m", "3 days") or null if error
     */
    async getContainerUptime(containerName) {
        try {
            const inspectOutput = await this.execRemote(`inspect ${containerName} --format '{{.State.StartedAt}}'`);
            if (!inspectOutput.trim()) {
                return null;
            }
            const startedAt = new Date(inspectOutput.trim());
            const now = new Date();
            const uptimeMs = now.getTime() - startedAt.getTime();
            // Convert to human-readable format
            const seconds = Math.floor(uptimeMs / 1000);
            const minutes = Math.floor(seconds / 60);
            const hours = Math.floor(minutes / 60);
            const days = Math.floor(hours / 24);
            if (days > 0) {
                return `${days}d ${hours % 24}h`;
            }
            else if (hours > 0) {
                return `${hours}h ${minutes % 60}m`;
            }
            else if (minutes > 0) {
                return `${minutes}m`;
            }
            else {
                return `${seconds}s`;
            }
        }
        catch (error) {
            this.logError(`Failed to get uptime for container ${containerName}: ${error}`);
            return null;
        }
    }
    /**
     * Get container resource usage (CPU and memory)
     * @param containerName Name of the container
     * @returns Object with CPU and memory usage or null if error
     */
    async getContainerStats(containerName) {
        try {
            // Use docker stats with --no-stream to get current stats
            const statsOutput = await this.execRemote(`stats ${containerName} --no-stream --format "{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}"`);
            const lines = statsOutput.trim().split("\n");
            if (lines.length < 1) {
                return null;
            }
            // Get data from first line (no header when using custom format)
            const dataLine = lines[0];
            const [cpuPercent, memoryUsage, memoryPercent] = dataLine.split("\t");
            return {
                cpuPercent: cpuPercent?.trim() || "0%",
                memoryUsage: memoryUsage?.trim() || "0B / 0B",
                memoryPercent: memoryPercent?.trim() || "0%",
            };
        }
        catch (error) {
            this.logError(`Failed to get stats for container ${containerName}: ${error}`);
            return null;
        }
    }
    /**
     * Get detailed container information for status display
     * @param containerName Name of the container
     * @returns Detailed container info or null if error
     */
    async getContainerDetails(containerName) {
        try {
            const inspectOutput = await this.execRemote(`inspect ${containerName}`);
            const inspectData = JSON.parse(inspectOutput);
            if (!inspectData || inspectData.length === 0) {
                return null;
            }
            const container = inspectData[0];
            // Get uptime and stats in parallel
            const [uptime, stats] = await Promise.all([
                this.getContainerUptime(containerName),
                this.getContainerStats(containerName),
            ]);
            // Extract detailed info
            const image = container.Config?.Image || null;
            const createdAt = container.Created || null;
            const restartCount = container.RestartCount || 0;
            const exitCode = container.State?.ExitCode || null;
            // Extract port mappings
            const ports = [];
            if (container.NetworkSettings?.Ports) {
                for (const [containerPort, hostBindings] of Object.entries(container.NetworkSettings.Ports)) {
                    if (hostBindings && Array.isArray(hostBindings)) {
                        for (const binding of hostBindings) {
                            ports.push(`${binding.HostPort}:${containerPort}`);
                        }
                    }
                }
            }
            // Extract volume mounts with detailed information
            const volumes = [];
            if (container.Mounts && Array.isArray(container.Mounts)) {
                for (const mount of container.Mounts) {
                    volumes.push({
                        source: mount.Name || mount.Source || "unknown",
                        destination: mount.Destination || "unknown",
                        mode: mount.Mode,
                    });
                }
            }
            return {
                uptime,
                stats,
                image,
                createdAt,
                restartCount,
                exitCode,
                ports,
                volumes,
            };
        }
        catch (error) {
            this.logError(`Failed to get details for container ${containerName}: ${error}`);
            return null;
        }
    }
}
exports.DockerClient = DockerClient;
/**
 * Create logging configuration for proxy containers (5MB max size, 5 files)
 */
function createProxyLoggingConfig() {
    return {
        logDriver: "json-file",
        logOpts: {
            "max-size": "5m",
            "max-file": "5"
        }
    };
}
/**
 * Create logging configuration for user service containers (10MB max size, 3 files)
 */
function createServiceLoggingConfig() {
    return {
        logDriver: "json-file",
        logOpts: {
            "max-size": "10m",
            "max-file": "3"
        }
    };
}
//# sourceMappingURL=index.js.map