Madagascar / scripts / deploy-project.sh
f16725e 3 months ago History
1 contributor
256 lines | 6.208kb
#!/bin/bash

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
CONFIG_PATH="${ROOT_DIR}/cluster-context/madagascar.json"
CLUSTER_NAME="madagascar"
COMMAND="install"
REMOTE_USER="root"
DRY_RUN=0
PROJECT_NAME=""
PROJECT_DIR=""
DEPLOY_MODE=""
NODE_FILTERS=()
TARGETS=()

usage() {
    cat <<EOF
Usage: $0 <project> [command] [options]

Commands:
  install      Deploy/install project on selected nodes (default)
  uninstall    Remove project from selected nodes
  status       Query project status on selected nodes (deploy.sh projects only)
  start        Start services on selected nodes (deploy.sh projects only)
  restart      Restart services on selected nodes (deploy.sh projects only)
  stop         Stop services on selected nodes (deploy.sh projects only)

Options:
  --cluster <name>    Cluster name in cluster-context/madagascar.json (default: madagascar)
  --node <name|ip>    Restrict to one node. Can be repeated.
  --user <user>       Remote SSH user for setup.sh projects (default: root)
  --dry-run           Show resolved targets and commands without executing
  -h, --help          Show this help

Examples:
  $0 pve-guests-state
  $0 pve-guests-state install --node ebony
  $0 autoNAS install
  $0 autoSMART status --node 192.168.2.92
EOF
}

require_config() {
    if [[ ! -f "${CONFIG_PATH}" ]]; then
        echo "ERROR: missing cluster config: ${CONFIG_PATH}" >&2
        exit 1
    fi
}

require_project() {
    PROJECT_DIR="${ROOT_DIR}/projects/${PROJECT_NAME}"
    if [[ ! -d "${PROJECT_DIR}" ]]; then
        echo "ERROR: unknown project: ${PROJECT_NAME}" >&2
        exit 1
    fi

    if [[ -x "${PROJECT_DIR}/setup.sh" ]]; then
        DEPLOY_MODE="setup"
        return
    fi

    if [[ -x "${PROJECT_DIR}/deploy.sh" ]]; then
        DEPLOY_MODE="deploy"
        return
    fi

    echo "ERROR: project ${PROJECT_NAME} has neither setup.sh nor deploy.sh" >&2
    exit 1
}

load_targets() {
    local entry=""
    TARGETS=()

    while IFS= read -r entry; do
        [[ -n "${entry}" ]] && TARGETS+=("${entry}")
    done < <(
        jq -r --arg cluster "${CLUSTER_NAME}" '
            .clusters[$cluster].nodes
            | to_entries[]
            | .key + "\t" + ((.value.ip // .value.wan.vmbr443.address // empty) | split("/")[0])
        ' "${CONFIG_PATH}"
    )

    if [[ ${#TARGETS[@]} -eq 0 ]]; then
        echo "ERROR: no targets found for cluster ${CLUSTER_NAME}" >&2
        exit 1
    fi
}

match_filter() {
    local filter="$1"
    local node_name="$2"
    local node_ip="$3"

    [[ "${filter}" == "${node_name}" || "${filter}" == "${node_ip}" ]]
}

filter_targets() {
    local filtered=()
    local entry=""
    local filter=""
    local node_name=""
    local node_ip=""

    if [[ ${#NODE_FILTERS[@]} -eq 0 ]]; then
        return
    fi

    for entry in "${TARGETS[@]}"; do
        node_name="${entry%%$'\t'*}"
        node_ip="${entry#*$'\t'}"
        for filter in "${NODE_FILTERS[@]}"; do
            if match_filter "${filter}" "${node_name}" "${node_ip}"; then
                filtered+=("${entry}")
                break
            fi
        done
    done

    TARGETS=("${filtered[@]}")

    if [[ ${#TARGETS[@]} -eq 0 ]]; then
        echo "ERROR: no targets matched the provided --node filters" >&2
        exit 1
    fi
}

run_setup_project() {
    local node_name="$1"
    local node_ip="$2"
    local cmd=()

    case "${COMMAND}" in
        install)
            cmd=(bash "${PROJECT_DIR}/setup.sh" --user "${REMOTE_USER}" "${node_ip}")
            ;;
        uninstall)
            cmd=(bash "${PROJECT_DIR}/setup.sh" --user "${REMOTE_USER}" --uninstall "${node_ip}")
            ;;
        *)
            echo "ERROR: command ${COMMAND} is not supported for setup.sh-only projects" >&2
            exit 1
            ;;
    esac

    echo "==> ${PROJECT_NAME}: ${COMMAND} on ${node_name} (${node_ip})"
    if [[ "${DRY_RUN}" -eq 1 ]]; then
        printf 'DRY-RUN:'
        printf ' %q' "${cmd[@]}"
        echo
        return
    fi

    (cd "${PROJECT_DIR}" && "${cmd[@]}")
}

run_deploy_project() {
    local node_name="$1"
    local node_ip="$2"
    local cmd=(bash "${PROJECT_DIR}/deploy.sh" "${COMMAND}" "${node_ip}")

    echo "==> ${PROJECT_NAME}: ${COMMAND} on ${node_name} (${node_ip})"
    if [[ "${DRY_RUN}" -eq 1 ]]; then
        printf 'DRY-RUN:'
        printf ' %q' "${cmd[@]}"
        echo
        return
    fi

    (cd "${PROJECT_DIR}" && "${cmd[@]}")
}

parse_args() {
    if [[ $# -lt 1 ]]; then
        usage
        exit 1
    fi

    PROJECT_NAME="$1"
    shift

    if [[ $# -gt 0 ]]; then
        case "$1" in
            install|uninstall|status|start|restart|stop)
                COMMAND="$1"
                shift
                ;;
        esac
    fi

    while [[ $# -gt 0 ]]; do
        case "$1" in
            --cluster)
                CLUSTER_NAME="$2"
                shift 2
                ;;
            --node)
                NODE_FILTERS+=("$2")
                shift 2
                ;;
            --user)
                REMOTE_USER="$2"
                shift 2
                ;;
            --dry-run)
                DRY_RUN=1
                shift
                ;;
            -h|--help)
                usage
                exit 0
                ;;
            *)
                echo "ERROR: unknown option: $1" >&2
                usage
                exit 1
                ;;
        esac
    done
}

main() {
    parse_args "$@"
    require_config
    require_project
    load_targets
    filter_targets

    echo "Project: ${PROJECT_NAME}"
    echo "Mode: ${DEPLOY_MODE}"
    echo "Command: ${COMMAND}"
    echo "Cluster: ${CLUSTER_NAME}"
    echo "Targets:"
    printf '  %s\n' "${TARGETS[@]}"
    echo

    local entry=""
    local node_name=""
    local node_ip=""

    for entry in "${TARGETS[@]}"; do
        node_name="${entry%%$'\t'*}"
        node_ip="${entry#*$'\t'}"
        if [[ "${DEPLOY_MODE}" == "setup" ]]; then
            run_setup_project "${node_name}" "${node_ip}"
        else
            run_deploy_project "${node_name}" "${node_ip}"
        fi
        echo
    done
}

main "$@"