From bf34bbbd8d94bfe82387f868b485ab1fc7d558e8 Mon Sep 17 00:00:00 2001 From: j Date: Sat, 21 Mar 2026 21:21:55 +1300 Subject: [PATCH] feat: Move install lifecycle to single remote script (ds_install.sh) --- source/agent-remote/ds_install.sh | 139 ++++++++++++++++++++++++++++++ source/src/commands/install.cpp | 95 +++++++++----------- 2 files changed, 180 insertions(+), 54 deletions(-) create mode 100755 source/agent-remote/ds_install.sh diff --git a/source/agent-remote/ds_install.sh b/source/agent-remote/ds_install.sh new file mode 100755 index 0000000..4b03bf3 --- /dev/null +++ b/source/agent-remote/ds_install.sh @@ -0,0 +1,139 @@ +#!/bin/bash +set -euo pipefail + +# ds_install.sh SERVICE +# +# Handles the full install lifecycle on the remote server in a single SSH session. +# Called after the local dropshell has rsynced new template + config to a staging folder. +# +# Remote directory layout: +# services/SERVICE/config/ ← live service config +# services/SERVICE/template/ ← live template scripts +# services/SERVICE/_staging/config/ ← new config (rsynced by local) +# services/SERVICE/_staging/template/ ← new template (rsynced by local) +# +# Flow: +# 1. Run install-pre.sh from STAGING (new code + new config) → validate +# 2. Run uninstall.sh from LIVE (old code + old config) → stop old service +# 3. Swap staging → live (atomic replacement) +# 4. Run install.sh from LIVE (now the new version) +# +# If any step fails, the script exits immediately (set -e). +# If install-pre.sh fails, the old service is untouched. + +# -- Determine paths -- +SCRIPT_PATH="$(readlink -f "${BASH_SOURCE[0]}")" +AGENT_PATH="$(dirname "${SCRIPT_PATH}")" +DROPSHELL_DIR="$(dirname "${AGENT_PATH}")" + +# Export for scripts that need them +export AGENT_PATH +export DROPSHELL_DIR + +# -- Source common functions -- +source "${AGENT_PATH}/common.sh" + +# -- Validate arguments -- +if [[ $# -lt 1 ]]; then + echo "Usage: ds_install.sh SERVICE [TEMP_DIR]" >&2 + exit 1 +fi + +SERVICE="$1" +export SERVICE +export TEMP_DIR="${2:-}" +export DOCKER_CLI_HINTS=false + +SERVICE_DIR="${DROPSHELL_DIR}/services/${SERVICE}" +LIVE_CONFIG="${SERVICE_DIR}/config" +LIVE_TEMPLATE="${SERVICE_DIR}/template" +STAGING_DIR="${SERVICE_DIR}/_staging" +STAGING_CONFIG="${STAGING_DIR}/config" +STAGING_TEMPLATE="${STAGING_DIR}/template" + +# -- Validate staging exists -- +[[ -d "${STAGING_CONFIG}" ]] || _die "Staging config not found at ${STAGING_CONFIG}" +[[ -d "${STAGING_TEMPLATE}" ]] || _die "Staging template not found at ${STAGING_TEMPLATE}" +[[ -f "${DROPSHELL_DIR}/server_info.env" ]] || _die "Missing server_info.env" + +# -- Helper: source env for a given template/config pair -- +load_env() { + local template_dir="$1" + local config_dir="$2" + + # Find template_info.env + local tinfo="${template_dir}/template_info.env" + if [[ ! -f "${tinfo}" ]]; then + tinfo="${template_dir}/config/.template_info.env" + fi + [[ -f "${tinfo}" ]] || _die "Missing template_info.env in ${template_dir}" + + # Must have service.env + [[ -f "${config_dir}/service.env" ]] || _die "Missing service.env in ${config_dir}" + + # Source in order: server_info → template_info → service.env + set -a + source "${DROPSHELL_DIR}/server_info.env" + source "${tinfo}" + source "${config_dir}/service.env" + set +a + + export CONFIG_PATH="${config_dir}" + export TEMPLATE_PATH="${template_dir}" +} + +# ═══════════════════════════════════════════════════════════════════ +# STEP 1: Run install-pre.sh from STAGING (new code + new config) +# ═══════════════════════════════════════════════════════════════════ +if [[ -f "${STAGING_TEMPLATE}/install-pre.sh" ]]; then + echo "── Step 1: Running install-pre.sh (new version) ──" + load_env "${STAGING_TEMPLATE}" "${STAGING_CONFIG}" + cd "${STAGING_TEMPLATE}" + + if ! bash ./install-pre.sh; then + echo "install-pre.sh failed — aborting install. Old service is untouched." + rm -rf "${STAGING_DIR}" + exit 1 + fi +else + echo "── Step 1: No install-pre.sh — skipping ──" +fi + +# ═══════════════════════════════════════════════════════════════════ +# STEP 2: Run uninstall.sh from LIVE (old code + old config) +# ═══════════════════════════════════════════════════════════════════ +if [[ -d "${LIVE_TEMPLATE}" ]] && [[ -f "${LIVE_TEMPLATE}/uninstall.sh" ]]; then + echo "── Step 2: Running uninstall.sh (old version) ──" + load_env "${LIVE_TEMPLATE}" "${LIVE_CONFIG}" + cd "${LIVE_TEMPLATE}" + + # Uninstall is best-effort — don't abort if it fails + bash ./uninstall.sh || echo "Warning: uninstall.sh failed, continuing anyway" +else + echo "── Step 2: No existing service to uninstall ──" +fi + +# ═══════════════════════════════════════════════════════════════════ +# STEP 3: Swap staging → live +# ═══════════════════════════════════════════════════════════════════ +echo "── Step 3: Swapping staging to live ──" + +# Remove old live (if exists) +rm -rf "${LIVE_TEMPLATE}" "${LIVE_CONFIG}" + +# Move staging into place +mkdir -p "${SERVICE_DIR}" +mv "${STAGING_CONFIG}" "${LIVE_CONFIG}" +mv "${STAGING_TEMPLATE}" "${LIVE_TEMPLATE}" +rm -rf "${STAGING_DIR}" + +# ═══════════════════════════════════════════════════════════════════ +# STEP 4: Run install.sh from LIVE (now the new version) +# ═══════════════════════════════════════════════════════════════════ +echo "── Step 4: Running install.sh (new version) ──" +load_env "${LIVE_TEMPLATE}" "${LIVE_CONFIG}" +cd "${LIVE_TEMPLATE}" + +bash ./install.sh || _die "install.sh failed" + +echo "── Install complete for ${SERVICE} ──" diff --git a/source/src/commands/install.cpp b/source/src/commands/install.cpp index a198c98..c0e7e65 100644 --- a/source/src/commands/install.cpp +++ b/source/src/commands/install.cpp @@ -110,8 +110,8 @@ namespace dropshell // Validate service.env matches template service.env { - std::filesystem::path template_service_env = tinfo.local_template_service_env_path(); //tinfo.local_template_path() / "config" / "service.env"; - std::filesystem::path template_info_env = tinfo.local_template_info_env_path(); //tinfo.local_template_path() / "config" / ".template_info.env"; + std::filesystem::path template_service_env = tinfo.local_template_service_env_path(); + std::filesystem::path template_info_env = tinfo.local_template_info_env_path(); std::string service_env_file = localfile::service_env(server, service); std::vector missing_vars; @@ -144,72 +144,59 @@ namespace dropshell } } - // Run install-pre.sh if it exists in the local template cache (reduces downtime by e.g. pulling images before uninstall) - // Only sync the single script, NOT the full template — the old uninstall.sh must stay matching the installed version. - bool has_install_pre = std::filesystem::exists(tinfo.local_template_path() / "install-pre.sh"); - if (has_install_pre && server_env.check_remote_dir_exists(remote_service_path, user)) - { - info << "Running pre-install script to prepare for update..." << std::endl; + // ── Stage new template + config to _staging folder on remote ── + std::string staging_path = remote_service_path + "/_staging"; + std::string staging_template = staging_path + "/template"; + std::string staging_config = staging_path + "/config"; - // Copy only install-pre.sh to the existing remote template directory - std::string local_script = (tinfo.local_template_path() / "install-pre.sh").string(); - std::string remote_script = remotepath(server, user).service_template(service) + "/install-pre.sh"; - if (!shared_commands::rsync_file_to_remote(local_script, remote_script, server_env, false, user)) - { - warning << "Failed to copy install-pre.sh to remote, skipping pre-install" << std::endl; - } - else - { - shared_commands::cRemoteTempFolder remote_temp_folder(server_env, user); - if (!server_env.run_remote_template_command(service, "install-pre", {}, false, {{"TEMP_DIR", remote_temp_folder.path()}}, NULL)) - { - warning << "Pre-install script failed, continuing with install..." << std::endl; - } - } - } + info << "Syncing new template and config to staging..." << std::endl; - // Uninstall the old service (this removes the remote service directory) - if (server_env.check_remote_dir_exists(remote_service_path, user)) + // Ensure staging directory exists + std::string mkdir_cmd = "mkdir -p " + quote(staging_path); + if (!execute_ssh_command(server_env.get_SSH_INFO(user), sCommand("", mkdir_cmd, {}), cMode::Silent)) { - info << "Service " << service << " is already installed on " << server << std::endl; - shared_commands::uninstall_service(server_env, service); - } - - // Sync template and config files to remote - if (!shared_commands::rsync_service_config(server_env, service, false)) - { - error << "Failed to sync service configuration to remote" << std::endl; + error << "Failed to create staging directory on remote" << std::endl; return false; } - // Run install script + // Sync template files to staging + if (!rsync_tree_to_remote(tinfo.local_template_path().string(), staging_template, + server_env, true, user)) { - info << "Running " << service_info.template_name << " install script on " << server << "..." << std::endl; + error << "Failed to sync template to staging" << std::endl; + return false; + } - // Check if install.sh exists on remote - std::string install_script_path = remotepath(server,user).service_template(service) + "/install.sh"; - if (!server_env.check_remote_file_exists(install_script_path, user)) - { - error << "Install script not found on remote server: " << install_script_path << std::endl; - error << "Make sure the template '" << service_info.template_name << "' contains an install.sh script" << std::endl; - return false; - } + // Sync config files to staging + if (!rsync_tree_to_remote(localpath::service(server, service), staging_config, + server_env, true, user)) + { + error << "Failed to sync config to staging" << std::endl; + return false; + } - shared_commands::cRemoteTempFolder remote_temp_folder(server_env, user); - if (!server_env.run_remote_template_command(service, "install", {}, false, {{"TEMP_DIR", remote_temp_folder.path()}},NULL)) - { - error << "Failed to run install script for service '" << service << "' on server '" << server << "'" << std::endl; - error << "Template: " << service_info.template_name << std::endl; - error << "Script path: " << install_script_path << std::endl; - error << "Check that the script is executable and has no syntax errors" << std::endl; - return false; - } + // ── Run ds_install.sh on remote (single SSH session) ── + info << "Running remote install..." << std::endl; + + shared_commands::cRemoteTempFolder remote_temp_folder(server_env, user); + std::string ds_install = remotepath(server, user).agent() + "/ds_install.sh"; + sCommand install_cmd("", ds_install + " " + requote(service) + " " + requote(remote_temp_folder.path()), {}); + + // Add agent hash for version checking + std::string agent_hash = get_local_agent_hash(); + if (!agent_hash.empty()) + install_cmd.add_env_var("AGENT_HASH", agent_hash); + + if (!execute_ssh_command(server_env.get_SSH_INFO(user), install_cmd, cMode::Defaults, nullptr)) + { + error << "Failed to install service '" << service << "' on server '" << server << "'" << std::endl; + error << "Template: " << service_info.template_name << std::endl; + return false; } // print health tick info << "Health: " << shared_commands::healthtick(server, service) << std::endl; - return true; }