UNPKG

iop

Version:
810 lines 36.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.setupCommand = setupCommand; const config_1 = require("../config"); const ssh_1 = require("../ssh"); const docker_1 = require("../docker"); const index_1 = require("../setup-proxy/index"); const logger_1 = require("../utils/logger"); const utils_1 = require("../utils"); // Module-level logger that gets configured when setupCommand runs let logger; /** * Attempts to bootstrap a fresh server as root user */ async function bootstrapFreshServer(serverHostname, context) { logger.phase(`Bootstrapping fresh server: ${serverHostname}`); try { // Try to connect as root const rootCredentials = await (0, ssh_1.getSSHCredentials)(serverHostname, { ...context.config, ssh: { ...context.config.ssh, username: "root" } }, context.secrets, context.verboseFlag); const sshClient = await ssh_1.SSHClient.create({ ...rootCredentials, host: serverHostname, username: "root", skipHostKeyVerification: true, // Fresh servers need this }); await sshClient.connect(); logger.verboseLog("Successfully connected as root"); // Get the target username from config const targetUsername = context.config.ssh?.username || "lightform"; // Bootstrap the server await performBootstrapSteps(sshClient, targetUsername, context); await sshClient.close(); logger.phaseComplete(`Server bootstrapped successfully`); return true; } catch (error) { // Check if it's just debconf warnings or user already exists (which are not real failures) const errorMessage = error?.toString() || ""; const isDebconfWarning = errorMessage.includes("debconf: unable to initialize frontend"); const isUserExists = errorMessage.includes("already exists"); const hasRealError = errorMessage.includes("E:") || errorMessage.includes("ERROR") || errorMessage.includes("fatal:"); if ((isDebconfWarning || isUserExists) && !hasRealError) { logger.verboseLog("Non-critical warnings detected (normal for server setup)"); logger.phaseComplete(`Server bootstrapped successfully`); return true; } logger.error("Failed to bootstrap server", error); return false; } } /** * Performs the actual bootstrap steps on the server */ async function performBootstrapSteps(sshClient, targetUsername, context) { logger.serverStep("Creating lightform user"); // Create user (handle if already exists) try { await sshClient.exec(`adduser --disabled-password --gecos "" ${targetUsername}`); logger.verboseLog(`Created user: ${targetUsername}`); } catch (error) { if (String(error).includes("already exists")) { logger.verboseLog(`User ${targetUsername} already exists, configuring...`); } else { logger.verboseLog(`Warning during user creation: ${String(error).slice(0, 100)}...`); } } try { await sshClient.exec(`usermod -aG sudo ${targetUsername}`); logger.verboseLog("→ Added user to sudo group"); } catch (error) { logger.verboseLog(`Warning: ${String(error).slice(0, 50)}...`); } // Configure passwordless sudo for lightform user try { await sshClient.exec(`echo "${targetUsername} ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/${targetUsername}`); await sshClient.exec(`chmod 440 /etc/sudoers.d/${targetUsername}`); logger.verboseLog("→ Configured passwordless sudo access"); } catch (error) { logger.verboseLog(`Warning configuring sudo: ${String(error).slice(0, 50)}...`); } logger.serverStep("Installing Docker"); // Check if Docker already installed try { await sshClient.exec("docker --version"); logger.verboseLog("Docker already installed, skipping..."); } catch (error) { logger.verboseLog("Installing Docker Engine..."); try { // Install Docker with progress feedback await sshClient.exec("export DEBIAN_FRONTEND=noninteractive && apt-get update"); logger.verboseLog("→ Updated package lists"); await sshClient.exec("export DEBIAN_FRONTEND=noninteractive && apt-get install -y ca-certificates curl"); logger.verboseLog("→ Installed prerequisites"); await sshClient.exec("install -m 0755 -d /etc/apt/keyrings"); await sshClient.exec("curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc"); await sshClient.exec("chmod a+r /etc/apt/keyrings/docker.asc"); logger.verboseLog("→ Added Docker GPG key"); await sshClient.exec(`echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null`); await sshClient.exec("export DEBIAN_FRONTEND=noninteractive && apt-get update"); logger.verboseLog("→ Added Docker repository"); await sshClient.exec("export DEBIAN_FRONTEND=noninteractive && apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin pigz"); logger.verboseLog("→ Installed Docker packages and compression tools"); } catch (dockerError) { // Don't fail bootstrap for debconf warnings if (String(dockerError).includes("debconf:")) { logger.verboseLog("→ Docker installed (ignoring debconf warnings)"); } else { throw dockerError; } } } try { await sshClient.exec(`usermod -aG docker ${targetUsername}`); logger.verboseLog("→ Added user to docker group"); } catch (error) { logger.verboseLog(`Warning: ${String(error).slice(0, 50)}...`); } logger.serverStep("Setting up SSH keys"); // Setup SSH directory for target user await sshClient.exec(`mkdir -p /home/${targetUsername}/.ssh`); await sshClient.exec(`chmod 700 /home/${targetUsername}/.ssh`); logger.verboseLog("→ Created SSH directory"); // Copy root's authorized_keys to target user await sshClient.exec(`cp /root/.ssh/authorized_keys /home/${targetUsername}/.ssh/authorized_keys 2>/dev/null || true`); await sshClient.exec(`chown -R ${targetUsername}:${targetUsername} /home/${targetUsername}/.ssh`); await sshClient.exec(`chmod 600 /home/${targetUsername}/.ssh/authorized_keys`); logger.verboseLog("→ Configured SSH access"); logger.serverStep("Securing SSH configuration"); // Secure SSH config await sshClient.exec(`sed -i 's/#PermitRootLogin yes/PermitRootLogin no/' /etc/ssh/sshd_config`); await sshClient.exec(`sed -i 's/#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config`); await sshClient.exec(`sed -i 's/PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config`); logger.verboseLog("→ Updated SSH config"); await sshClient.exec(`systemctl restart ssh`); logger.verboseLog("→ Restarted SSH service"); logger.serverStep("Installing Fail2Ban"); // Check if Fail2Ban already installed try { await sshClient.exec("fail2ban-client --version"); logger.verboseLog("Fail2Ban already installed"); } catch (error) { await sshClient.exec(`export DEBIAN_FRONTEND=noninteractive && apt-get install -y fail2ban`); logger.verboseLog("→ Installed Fail2Ban"); } try { await sshClient.exec(`systemctl enable fail2ban`); logger.verboseLog("→ Enabled Fail2Ban service"); } catch (error) { // Ignore systemctl enable output (it's not an error) if (String(error).includes("Synchronizing state") || String(error).includes("Executing:")) { logger.verboseLog("→ Enabled Fail2Ban service"); } else { logger.verboseLog(`Warning enabling Fail2Ban: ${String(error).slice(0, 50)}...`); } } try { await sshClient.exec(`systemctl start fail2ban`); logger.verboseLog("→ Started Fail2Ban service"); } catch (error) { logger.verboseLog(`Warning starting Fail2Ban: ${String(error).slice(0, 50)}...`); } logger.serverStep("Enabling automatic security updates"); // Install and configure unattended upgrades try { await sshClient.exec(`export DEBIAN_FRONTEND=noninteractive && apt-get install -y unattended-upgrades`); await sshClient.exec(`echo 'Unattended-Upgrade::Automatic-Reboot "false";' >> /etc/apt/apt.conf.d/50unattended-upgrades`); await sshClient.exec(`systemctl enable unattended-upgrades`); logger.verboseLog("→ Enabled automatic security updates"); } catch (error) { logger.verboseLog(`Warning configuring auto-updates: ${String(error).slice(0, 50)}...`); } logger.serverStep("Basic system hardening"); // Disable unused services try { await sshClient.exec(`systemctl disable --now avahi-daemon 2>/dev/null || true`); logger.verboseLog("→ Disabled mDNS service"); } catch (error) { // Ignore if service doesn't exist } try { await sshClient.exec(`systemctl disable --now cups 2>/dev/null || true`); logger.verboseLog("→ Disabled print service"); } catch (error) { // Ignore if service doesn't exist } // Secure SSH config permissions try { await sshClient.exec(`chmod 600 /etc/ssh/sshd_config`); logger.verboseLog("→ Secured SSH config permissions"); } catch (error) { logger.verboseLog(`Warning securing SSH config: ${String(error).slice(0, 50)}...`); } // Enable NTP time synchronization try { await sshClient.exec(`timedatectl set-ntp true`); logger.verboseLog("→ Enabled NTP time sync"); } catch (error) { logger.verboseLog(`Warning configuring NTP: ${String(error).slice(0, 50)}...`); } // Set stricter umask for new files try { await sshClient.exec(`echo "umask 027" >> /etc/profile`); logger.verboseLog("→ Set secure file permissions"); } catch (error) { logger.verboseLog(`Warning setting umask: ${String(error).slice(0, 50)}...`); } logger.serverStepComplete("Bootstrap completed"); } /** * Checks if a server appears to be fresh (no lightform user configured) */ async function detectFreshServer(serverHostname, context) { try { const sshCredentials = await (0, ssh_1.getSSHCredentials)(serverHostname, context.config, context.secrets, context.verboseFlag); if (!sshCredentials.username) { return false; } // Try to connect with configured user const sshClient = await ssh_1.SSHClient.create({ ...sshCredentials, host: serverHostname, username: sshCredentials.username, skipHostKeyVerification: true, // For fresh server detection suppressConnectionErrors: true, // Don't show errors during detection }); await sshClient.connect(); await sshClient.close(); // If connection succeeds, it's not a fresh server return false; } catch (error) { // If connection fails, it might be a fresh server logger.verboseLog(`Connection failed with configured user, server might be fresh: ${error}`); return true; } } /** * 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 setup command */ function parseSetupArgs(entryNames, verbose = false, forceBootstrap = false) { return { entryNames: entryNames || [], verboseFlag: verbose, forceBootstrap, }; } /** * Loads and validates Lightform 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(" lightform init # Create configuration files"); logger.error(" # Edit lightform.yml with your server settings"); logger.error(" lightform setup # Run setup 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; } } /** * Collects all unique servers from apps and services configuration */ function collectAllServers(config) { const configuredApps = normalizeConfigEntries(config.apps); const configuredServices = normalizeConfigEntries(config.services); const allServers = new Set(); // Add servers from apps configuredApps.forEach((app) => { if (app.server) { allServers.add(app.server); } }); // 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 configuredApps = normalizeConfigEntries(config.apps); const configuredServices = normalizeConfigEntries(config.services); const targetServers = new Set(); entryNames.forEach((name) => { const app = configuredApps.find((a) => a.name === name); if (app && app.server) { targetServers.add(app.server); } const service = configuredServices.find((s) => s.name === name); if (service && service.server) { targetServers.add(service.server); } if (!app && !service) { logger.warn(`Entry "${name}" not found in apps or services configuration. Skipping.`); } }); return targetServers; } /** * Establishes SSH connection to a server with proper error handling */ 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"); } // Security check for root user (allow during bootstrap) if (sshCredentials.username === "root" && !context.allowBootstrap) { logger.serverStepError(`Using root for SSH access is not recommended`, undefined, true); logger.verboseLog(`Please see https://github.com/elitan/lightform for security best practices.`); throw new Error("Root SSH access not recommended"); } logger.verboseLog(`Resolved SSH username: ${sshCredentials.username}`); logSSHCredentials(sshCredentials); const sshClientOptions = { ...sshCredentials, host: serverHostname, username: sshCredentials.username, debug: context.verboseFlag ? (message) => { logger.verboseLog(`SSH_DEBUG: ${message}`); } : undefined, }; if (context.verboseFlag) { logSSHClientOptions(sshClientOptions); } const sshClient = await ssh_1.SSHClient.create(sshClientOptions); await sshClient.connect(); return { sshClient, sshCredentials }; } /** * Logs SSH credential information (safely) */ function logSSHCredentials(sshCredentials) { if (sshCredentials.identity) { logger.verboseLog(`Using SSH key path: ${sshCredentials.identity}`); } else if (sshCredentials.password) { logger.verboseLog(`Using SSH password (not displayed).`); } else { logger.verboseLog(`No specific key or password configured; attempting agent/default key authentication.`); } } /** * Logs SSH client options (safely hiding sensitive data) */ function logSSHClientOptions(sshClientOptions) { const loggableSshClientOptions = { ...sshClientOptions, password: sshClientOptions.password ? "***hidden***" : undefined, debug: "debug-function", }; logger.verboseLog(`SSHClient options: ${JSON.stringify(loggableSshClientOptions, null, 2)}`); } /** * Checks Docker installation and installs if needed */ async function ensureDockerInstallation(dockerClient, serverHostname) { logger.serverStep("Checking Docker installation"); const dockerInstalled = await dockerClient.checkInstallation(); if (!dockerInstalled) { logger.verboseLog(`Will attempt to install Docker...`); const installSuccess = await dockerClient.install(); if (!installSuccess) { logger.serverStepError(`Docker installation failed`, undefined, true); logger.verboseLog(`Please install Docker manually. See https://github.com/elitan/lightform for server setup instructions.`); throw new Error("Docker installation failed"); } } logger.serverStepComplete(`Docker ${dockerInstalled ? "verified" : "installed"}`); } /** * Configures Docker registry authentication */ async function configureDockerRegistry(dockerClient, context) { logger.serverStep("Configuring Docker registry"); const dockerRegistry = context.config.docker?.registry || "docker.io"; const dockerUsername = context.config.docker?.username; const dockerPassword = context.secrets.DOCKER_REGISTRY_PASSWORD; logger.verboseLog(`Using Docker registry: ${dockerRegistry} (default: docker.io)`); if (dockerUsername && dockerPassword) { await dockerClient.login(dockerRegistry, dockerUsername, dockerPassword); logger.verboseLog(`Successfully logged into Docker registry`); } else { logger.verboseLog(`Docker registry username or password not configured/found in secrets. Skipping Docker login.`); logger.verboseLog(`Please ensure DOCKER_REGISTRY_PASSWORD is in .lightform/secrets and docker.username is in lightform.yml if login is required.`); } logger.serverStepComplete(`Docker registry configured`); } /** * Creates the project network for the server */ async function createProjectNetwork(dockerClient, context) { if (!context.config.name) { const errorMsg = `Project name is required in lightform.yml`; logger.serverStepError(errorMsg, undefined, true); logger.verboseLog(`The project name is used to create a unique Docker network for your services.`); throw new Error(`Project name not specified in configuration. Please add 'name: your_project_name' to your lightform.yml file.`); } logger.serverStep("Creating project network"); const networkName = (0, utils_1.getProjectNetworkName)(context.config.name); await dockerClient.createNetwork({ name: networkName }); logger.serverStepComplete(`Network ${networkName} ready`); return networkName; } /** * Sets up and configures the Lightform Proxy */ async function setupAndConfigureLightformProxy(sshClient, dockerClient, networkName, serverHostname, context) { logger.serverStep("Setting up Lightform Proxy"); const proxySetupResult = await (0, index_1.setupLightformProxy)(serverHostname, sshClient, context.verboseFlag); if (!proxySetupResult) { logger.serverStepError(`Failed to set up Lightform Proxy`, undefined); logger.verboseLog(`Some services may not work correctly.`); } else { logger.verboseLog(`Lightform Proxy is ready.`); await connectProxyToNetwork(sshClient, dockerClient, networkName); } logger.serverStepComplete(`Lightform Proxy ${proxySetupResult ? "configured" : "failed"}`); } /** * Connects the Lightform Proxy to the project network */ async function connectProxyToNetwork(sshClient, dockerClient, networkName) { logger.verboseLog(`Connecting Lightform Proxy to the project network...`); try { const proxyExists = await dockerClient.containerExists(index_1.LIGHTFORM_PROXY_NAME); if (!proxyExists) { throw new Error(`Lightform Proxy container (${index_1.LIGHTFORM_PROXY_NAME}) does not exist despite setup reporting success`); } // Check if the proxy is already connected to the network const checkNetworkCmd = `docker inspect ${index_1.LIGHTFORM_PROXY_NAME} --format "{{json .NetworkSettings.Networks}}"`; const networkOutput = await sshClient.exec(checkNetworkCmd); const networks = JSON.parse(networkOutput.trim()); if (networks && networks[networkName]) { logger.verboseLog(`Lightform Proxy is already connected to network: ${networkName}`); } else { const connectCmd = `docker network connect ${networkName} ${index_1.LIGHTFORM_PROXY_NAME}`; await sshClient.exec(connectCmd); logger.verboseLog(`Successfully connected Lightform Proxy to network: ${networkName}`); } } catch (error) { logger.verboseLog(`Failed to connect Lightform Proxy to network: ${error}`); logger.verboseLog(`Some services may not be accessible through the proxy.`); } } /** * Gets services that should be deployed to a specific server */ function getServicesForServer(serverHostname, context) { return Object.entries(context.config.services || {}) .filter(([_, service]) => service.server === serverHostname) .map(([name, service]) => ({ name, ...service })); } /** * Authenticates with service-specific Docker registries */ async function authenticateServiceRegistries(services, dockerClient, context) { const dockerRegistry = context.config.docker?.registry || "docker.io"; const serviceRegistryMap = new Map(); for (const service of services) { if (service.registry && typeof service.registry === "object" && service.registry.username) { const serviceRegistry = service.registry.url || dockerRegistry; const serviceUsername = service.registry.username; logger.verboseLog(`Service ${service.name} uses registry: ${serviceRegistry}`); if (service.registry.password_secret && !serviceRegistryMap.has(serviceRegistry)) { const servicePassword = context.secrets[service.registry.password_secret]; if (servicePassword) { serviceRegistryMap.set(serviceRegistry, { registry: serviceRegistry, username: serviceUsername, password: servicePassword, }); } else { logger.verboseLog(`Service ${service.name}: Secret ${service.registry.password_secret} not found in .lightform/secrets. Skipping registry login.`); } } } } // Login to all service-specific registries for (const [registry, credentials] of serviceRegistryMap.entries()) { await dockerClient.login(registry, credentials.username, credentials.password); } } /** * Deploys a single service to the server */ async function deployService(service, dockerClient, context) { logger.verboseLog(`Setting up service: ${service.name}...`); try { // Pull the latest image await dockerClient.pullImage(service.image); // Create container options from the service definition const containerOptions = docker_1.DockerClient.serviceToContainerOptions(service, context.config.name, context.secrets); // Check if container already exists (running or not) const containerExists = await dockerClient.containerExists(containerOptions.name); if (containerExists) { await handleExistingServiceContainer(containerOptions.name, dockerClient, containerOptions); } else { await createNewServiceContainer(containerOptions, dockerClient); } } catch (error) { logger.error(`Failed to start service ${service.name}`, error); throw error; } } /** * Handles existing service containers (recreate with updated config) */ async function handleExistingServiceContainer(containerName, dockerClient, containerOptions) { logger.verboseLog(`Container ${containerName} already exists. Recreating with updated configuration...`); // Stop and remove the existing container const containerRunning = await dockerClient.containerIsRunning(containerName); if (containerRunning) { await dockerClient.stopContainer(containerName); } await dockerClient.removeContainer(containerName); // Create the container with new configuration await dockerClient.createContainer(containerOptions); } /** * Creates a new service container */ async function createNewServiceContainer(containerOptions, dockerClient) { logger.verboseLog(`Creating new container: ${containerOptions.name}`); await dockerClient.createContainer(containerOptions); } /** * Sets up all services for a server */ async function setupServicesOnServer(dockerClient, serverHostname, context) { logger.serverStep("Setting up services", true); const servicesOnThisServer = getServicesForServer(serverHostname, context); // Check if there are apps on this server to provide better messaging const configuredApps = normalizeConfigEntries(context.config.apps); const appsOnThisServer = configuredApps.filter((app) => app.server === serverHostname); if (servicesOnThisServer.length === 0) { if (appsOnThisServer.length > 0) { logger.verboseLog(`No services configured for this server, but ${appsOnThisServer.length} app(s) found. Infrastructure setup is complete for app deployments.`); } else { logger.verboseLog(`No services found for this server`); } } else { logger.verboseLog(`Found ${servicesOnThisServer.length} services to start`); // Authenticate with service-specific registries await authenticateServiceRegistries(servicesOnThisServer, dockerClient, context); // Deploy each service for (const service of servicesOnThisServer) { await deployService(service, dockerClient, context); } } const statusMessage = servicesOnThisServer.length > 0 ? `Services configured` : appsOnThisServer.length > 0 ? `Infrastructure ready for apps` : `No services configured`; logger.serverStepComplete(statusMessage, undefined, true); } /** * Handles setup errors with specific error messages and suggestions */ function handleSetupError(error, serverHostname, sshCredentials, context) { const portForError = context.config.ssh?.port || 22; logger.serverStepError(`Error during server setup (connecting to ${sshCredentials.username}@${serverHostname}:${portForError})`, error, true); if (error.level === "client-authentication" || (error.message && error.message.includes("All configured authentication methods failed"))) { logger.verboseLog(`Authentication failure. Please verify: 1. SSH key path ('identity') in Lightform config/secrets is correct and accessible. 2. If using a password, it's correct in Lightform secrets. 3. SSH agent (if used) is configured correctly with the right keys. 4. The user '${sshCredentials.username}' is allowed to SSH to '${serverHostname}'.`); logger.verboseLog(""); logger.verboseLog("Quick fixes:"); logger.verboseLog(` ssh ${sshCredentials.username}@${serverHostname} # Test SSH manually`); logger.verboseLog(" # Check your SSH key: ls ~/.ssh/"); logger.verboseLog(" # Add key to agent: ssh-add ~/.ssh/your_key"); } else if (error.code === "ECONNREFUSED") { logger.verboseLog(`Connection refused. Ensure an SSH server is running on '${serverHostname}' and accessible on port ${portForError}.`); logger.verboseLog(""); logger.verboseLog("Quick fixes:"); logger.verboseLog(` ping ${serverHostname} # Test basic connectivity`); logger.verboseLog(` telnet ${serverHostname} ${portForError} # Test SSH port`); logger.verboseLog(" # Check firewall settings on the server"); } else if (error.code === "ETIMEDOUT" || (error.message && error.message.toLowerCase().includes("timeout"))) { logger.verboseLog(`Connection timed out. Check network connectivity to '${serverHostname}', server load, and any firewalls.`); logger.verboseLog(""); logger.verboseLog("Quick fixes:"); logger.verboseLog(` ping ${serverHostname} # Test basic connectivity`); logger.verboseLog(" # Check server is powered on and accessible"); logger.verboseLog(" # Try from a different network"); } else if (error.message && error.message.includes("ENOTFOUND")) { logger.verboseLog(`Hostname not found. Ensure '${serverHostname}' is a valid and resolvable hostname.`); logger.verboseLog(""); logger.verboseLog("Quick fixes:"); logger.verboseLog(` nslookup ${serverHostname} # Check DNS resolution`); logger.verboseLog(" # Verify hostname spelling in lightform.yml"); logger.verboseLog(" # Try using IP address instead of hostname"); } } /** * Sets up a single server with all required infrastructure and services */ async function setupServer(serverHostname, context) { let sshClient; let sshCredentials; try { logger.server(serverHostname); // Check if this is a fresh server const isFreshServer = await detectFreshServer(serverHostname, context); if (isFreshServer) { logger.verboseLog("Server appears to be fresh (configured user cannot connect)"); logger.verboseLog("🚀 Fresh server detected - bootstrapping automatically with security best practices"); logger.verboseLog(" → Creating lightform user with sudo access"); logger.verboseLog(" → Installing Docker Engine"); logger.verboseLog(" → Setting up SSH keys"); logger.verboseLog(" → Securing SSH configuration"); logger.verboseLog(" → Installing Fail2Ban"); logger.verboseLog(" → Enabling automatic security updates"); logger.verboseLog(" → Basic system hardening"); const bootstrapSuccess = await bootstrapFreshServer(serverHostname, context); if (!bootstrapSuccess) { logger.serverStepError("Bootstrap failed", undefined, true); return; } logger.verboseLog("✅ Server bootstrapped successfully, continuing with setup..."); // Give the server a moment to apply changes await new Promise((resolve) => setTimeout(resolve, 2000)); } // Establish SSH connection (now with potentially bootstrapped server) logger.verboseLog("Connecting with configured user..."); const connectionResult = await establishSSHConnection(serverHostname, context); sshClient = connectionResult.sshClient; sshCredentials = connectionResult.sshCredentials; logger.verboseLog("SSH connection established successfully"); // Create Docker client const dockerClient = new docker_1.DockerClient(sshClient, serverHostname, context.verboseFlag); // Setup infrastructure await ensureDockerInstallation(dockerClient, serverHostname); await configureDockerRegistry(dockerClient, context); const networkName = await createProjectNetwork(dockerClient, context); await setupAndConfigureLightformProxy(sshClient, dockerClient, networkName, serverHostname, context); // Services are now deployed via 'lightform deploy' instead of setup await sshClient.close(); } catch (error) { handleSetupError(error, serverHostname, sshCredentials, context); } finally { await sshClient?.close(); } } /** * Provides a summary of what will be set up */ function logSetupSummary(targetServers, config, verboseFlag) { const configuredApps = normalizeConfigEntries(config.apps); const configuredServices = normalizeConfigEntries(config.services); const appsCount = configuredApps.filter((app) => targetServers.has(app.server)).length; const servicesCount = configuredServices.filter((service) => targetServers.has(service.server)).length; if (verboseFlag) { logger.verboseLog(`Setup will prepare infrastructure for:`); if (appsCount > 0) { logger.verboseLog(` - ${appsCount} app(s) ready for deployment`); } if (servicesCount > 0) { logger.verboseLog(` - ${servicesCount} service(s) ready for deployment`); } if (appsCount === 0 && servicesCount === 0) { logger.verboseLog(` - Server infrastructure only (no apps or services configured for these servers)`); } logger.verboseLog(`Note: Use 'lightform deploy' to deploy apps and services after setup.`); } } /** * Main setup command that orchestrates the entire setup process */ async function setupCommand(entryNames, verbose = false) { try { const { entryNames: parsedEntryNames, verboseFlag } = parseSetupArgs(entryNames, verbose); // Initialize logger with verbose flag logger = new logger_1.Logger({ verbose: verboseFlag }); logger.setupStart("Starting infrastructure setup"); // Load configuration and secrets const { config, secrets } = await loadConfigurationAndSecrets(); const context = { config, secrets, verboseFlag, allowBootstrap: true, }; // Determine target servers const targetServers = filterServersByEntries(parsedEntryNames, config); if (targetServers.size === 0) { logger.info("No servers to set up. Please check your lightform.yml configuration has apps or services with servers defined."); return; } // Check what types of entries we're setting up for better messaging const configuredApps = normalizeConfigEntries(config.apps); const configuredServices = normalizeConfigEntries(config.services); const hasApps = configuredApps.some((app) => targetServers.has(app.server)); const hasServices = configuredServices.some((service) => targetServers.has(service.server)); let phaseMessage = `Setting up ${targetServers.size} server(s) infrastructure`; if (hasApps && hasServices) { phaseMessage += ` (apps and services ready for deployment)`; } else if (hasApps) { phaseMessage += ` (apps ready for deployment)`; } else if (hasServices) { phaseMessage += ` (services ready for deployment)`; } logger.phase(phaseMessage); logger.verboseLog(`Target servers: ${Array.from(targetServers).join(", ")}`); // Provide setup summary logSetupSummary(targetServers, config, verboseFlag); // Setup each server for (const serverHostname of targetServers) { await setupServer(serverHostname, context); } logger.phaseComplete(phaseMessage); logger.setupComplete(); } catch (error) { logger.setupFailed(error); process.exit(1); } finally { logger.cleanup(); } } //# sourceMappingURL=setup.js.map