feat: Move install lifecycle to single remote script (ds_install.sh)
This commit is contained in:
139
source/agent-remote/ds_install.sh
Executable file
139
source/agent-remote/ds_install.sh
Executable file
@@ -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} ──"
|
||||
@@ -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<std::string> 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;
|
||||
|
||||
// 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;
|
||||
error << "Failed to sync template 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))
|
||||
// Sync config files to staging
|
||||
if (!rsync_tree_to_remote(localpath::service(server, service), staging_config,
|
||||
server_env, true, user))
|
||||
{
|
||||
error << "Failed to run install script for service '" << service << "' on server '" << server << "'" << std::endl;
|
||||
error << "Failed to sync config to staging" << 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;
|
||||
error << "Script path: " << install_script_path << std::endl;
|
||||
error << "Check that the script is executable and has no syntax errors" << std::endl;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// print health tick
|
||||
info << "Health: " << shared_commands::healthtick(server, service) << std::endl;
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user