格式化硬盘脚本

格式化硬盘脚本

#!/bin/bash
# =============================================================================
# 脚本名称: disk.sh
# 脚本功能: 生产环境磁盘初始化(格式化为 XFS、挂载、配置 fstab、创建数据目录)
# 使用说明: disk.sh -d <设备> [-m <挂载点>] [-D <数据子目录>] [-y] [-v] [-h]
# =============================================================================

set -uo pipefail   # 注意: 不加 -e,由各函数自行处理错误,避免 set -e 吞掉错误信息

# ---------- 常量 ----------
readonly SCRIPT_NAME=$(basename "$0")
readonly LOG_DIR="/var/log"
readonly LOG_FILE="${LOG_DIR}/disk_init.log"
readonly FSTAB="/etc/fstab"

# ---------- 默认参数 ----------
DISK=""
MOUNT_POINT="/export"
DATA_SUBDIR="Data"
YES=false
VERBOSE=false

# ---------- 颜色 ----------
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'

# ---------- 日志函数 ----------
_ts() { date '+%Y-%m-%d %H:%M:%S'; }

log_info()    { local m="[INFO]    $(_ts) $*"; echo -e "${BLUE}${m}${NC}";   _log_file "$m"; }
log_success() { local m="[SUCCESS] $(_ts) $*"; echo -e "${GREEN}${m}${NC}";  _log_file "$m"; }
log_warn()    { local m="[WARN]    $(_ts) $*"; echo -e "${YELLOW}${m}${NC}"; _log_file "$m"; }
log_error()   { local m="[ERROR]   $(_ts) $*"; echo -e "${RED}${m}${NC}" >&2; _log_file "$m"; }
log_debug()   { [[ "$VERBOSE" == "true" ]] && { local m="[DEBUG]   $(_ts) $*"; echo -e "${BLUE}${m}${NC}"; _log_file "$m"; }; true; }

_log_file() {
    # 写日志文件;若目录不可写则静默跳过,不中断流程
    if [[ -w "$LOG_DIR" ]] || mkdir -p "$LOG_DIR" 2>/dev/null; then
        echo "$1" >> "$LOG_FILE" 2>/dev/null || true
    fi
}

# ---------- 错误退出 ----------
die() {
    log_error "$1"
    exit "${2:-1}"
}

# ---------- 用法说明 ----------
usage() {
    cat <<EOF
用法: $SCRIPT_NAME -d <设备> [-m <挂载点>] [-D <数据子目录>] [-y] [-v] [-h]

选项:
  -d <device>    磁盘设备路径(必填),如 /dev/vdb
  -m <path>      挂载点路径(默认: /export)
  -D <subdir>    挂载点下的数据子目录名(默认: Data)
  -y             跳过交互确认(非交互 / CI 环境使用)
  -v             开启详细日志
  -h             显示此帮助信息

安全说明:
  1. 脚本会永久擦除指定磁盘的全部数据
  2. 系统盘(根分区所在设备)会被自动识别并拒绝操作
  3. 操作前会自动备份 /etc/fstab

示例:
  $SCRIPT_NAME -d /dev/vdb
  $SCRIPT_NAME -d /dev/vdb -m /data -D app_data -y
EOF
}

# ---------- 参数解析 ----------
parse_args() {
    while getopts ":d:m:D:yvh" opt; do
        case "$opt" in
            d) DISK="$OPTARG" ;;
            m) MOUNT_POINT="$OPTARG" ;;
            D) DATA_SUBDIR="$OPTARG" ;;
            y) YES=true ;;
            v) VERBOSE=true ;;
            h) usage; exit 0 ;;
            :) die "选项 -$OPTARG 需要参数" 1 ;;
            \?) die "未知选项: -$OPTARG" 1 ;;
        esac
    done

    [[ -z "$DISK" ]] && { usage; die "缺少必填参数: -d <设备>" 1; }
}

# ---------- 检查依赖工具 ----------
check_dependencies() {
    local missing=()
    for cmd in blkid mkfs.xfs lsblk fuser; do
        command -v "$cmd" &>/dev/null || missing+=("$cmd")
    done
    if (( ${#missing[@]} > 0 )); then
        die "缺少必要工具: ${missing[*]},请先安装后重试" 5
    fi
    log_debug "依赖检查通过"
}

# ---------- 检查 root 权限 ----------
check_root() {
    if [[ $EUID -ne 0 ]]; then
        die "此脚本需要 root 权限,请使用 sudo 运行" 1
    fi
}

# ---------- 获取根分区所在物理设备 ----------
get_root_device() {
    # 取 / 挂载所在设备,再去掉末尾数字得到物理盘
    local root_dev
    root_dev=$(df --output=source / 2>/dev/null | tail -1)
    # 去掉分区号(如 /dev/sda1 -> /dev/sda,/dev/nvme0n1p1 -> /dev/nvme0n1)
    root_dev=$(echo "$root_dev" | sed -E 's/p?[0-9]+$//')
    echo "$root_dev"
}

# ---------- 参数校验 ----------
validate_parameters() {
    # 块设备检查
    if [[ ! -b "$DISK" ]]; then
        die "设备 $DISK 不存在或不是块设备" 2
    fi

    # 禁止操作系统盘
    local root_device
    root_device=$(get_root_device)
    log_debug "根分区所在设备: $root_device"

    local disk_base
    disk_base=$(echo "$DISK" | sed -E 's/p?[0-9]+$//')
    if [[ "$disk_base" == "$root_device" ]]; then
        die "安全拒绝: $DISK 属于系统盘 ($root_device),禁止操作!" 3
    fi

    # 挂载点不能是根目录
    if [[ "$MOUNT_POINT" == "/" ]]; then
        die "挂载点不能为根目录 /" 1
    fi

    log_debug "参数校验通过: disk=$DISK, mount=$MOUNT_POINT, data_subdir=$DATA_SUBDIR"
}

# ---------- 安全卸载 ----------
safe_umount() {
    local target="$1"   # 设备或挂载点均可
    local mount_point

    mount_point=$(findmnt -nr -o TARGET "$target" 2>/dev/null || true)
    [[ -z "$mount_point" ]] && return 0

    log_info "检测到已挂载: $target -> $mount_point,尝试卸载..."

    # 终止占用进程
    if fuser -s "$mount_point" 2>/dev/null; then
        log_warn "有进程正在使用 $mount_point,尝试终止..."
        fuser -km "$mount_point" 2>/dev/null || true
        sleep 2
    fi

    if ! umount "$target" 2>/dev/null; then
        # 尝试 lazy unmount 兜底
        log_warn "umount 失败,尝试 lazy umount..."
        umount -l "$target" 2>/dev/null || die "无法卸载 $target,请手动处理后重试" 4
    fi
    log_info "卸载成功: $target"
}

# ---------- 用户确认 ----------
confirm_operation() {
    if [[ "$YES" == "true" ]]; then
        log_info "非交互模式 (-y),跳过确认"
        return 0
    fi

    local disk_info
    disk_info=$(lsblk -dn -o SIZE,MODEL "$DISK" 2>/dev/null || echo "未知")

    echo ""
    echo "========================================"
    echo "  !!! 警告:以下操作不可逆 !!!"
    echo "========================================"
    echo "  设备:    $DISK  ($disk_info)"
    echo "  操作:    格式化为 XFS 文件系统"
    echo "  挂载点:  $MOUNT_POINT"
    echo "  数据目录: ${MOUNT_POINT}/${DATA_SUBDIR}"
    echo "  影响:    设备上所有数据将被永久删除!"
    echo "========================================"
    echo ""
    read -r -p "请输入 'YES' 确认继续(其他任意输入取消): " confirmation
    if [[ "$confirmation" != "YES" ]]; then
        log_info "用户取消操作"
        exit 0
    fi
}

# ---------- 格式化磁盘 ----------
format_disk() {
    log_info "开始格式化 $DISK 为 XFS 文件系统..."

    # 若已有文件系统,记录警告
    local current_fs
    current_fs=$(blkid -s TYPE -o value "$DISK" 2>/dev/null || true)
    if [[ -n "$current_fs" ]]; then
        log_warn "设备 $DISK 当前文件系统: $current_fs,即将覆盖"
    fi

    # 先卸载(如已挂载)
    safe_umount "$DISK"

    # 格式化:-f 强制;普通磁盘不使用 RAID 条带参数
    if ! mkfs.xfs -f -i size=512 -l size=128m "$DISK"; then
        die "磁盘格式化失败" 6
    fi

    # 确保内核元数据同步
    udevadm settle 2>/dev/null || true
    blockdev --rereadpt "$DISK" 2>/dev/null || true

    log_success "磁盘格式化完成: $DISK"
}

# ---------- 准备挂载点 ----------
prepare_mountpoint() {
    log_info "准备挂载点: $MOUNT_POINT"

    # 若挂载点已被其他设备占用,先卸载
    if findmnt -n "$MOUNT_POINT" &>/dev/null; then
        local occupied_by
        occupied_by=$(findmnt -nr -o SOURCE "$MOUNT_POINT" 2>/dev/null || true)
        log_warn "挂载点 $MOUNT_POINT 已被 ${occupied_by:-未知设备} 使用,尝试卸载..."
        safe_umount "$MOUNT_POINT"
    fi

    if ! mkdir -p "$MOUNT_POINT"; then
        die "创建挂载点目录失败: $MOUNT_POINT" 8
    fi

    log_debug "挂载点就绪: $MOUNT_POINT"
}

# ---------- 挂载磁盘 ----------
mount_disk() {
    log_info "挂载 $DISK 到 $MOUNT_POINT"

    if ! mount -t xfs -o defaults,noatime,nodiratime "$DISK" "$MOUNT_POINT"; then
        die "磁盘挂载失败" 9
    fi

    if ! mountpoint -q "$MOUNT_POINT"; then
        die "挂载验证失败: $MOUNT_POINT 不是有效挂载点" 10
    fi

    log_success "挂载成功: $DISK -> $MOUNT_POINT"
}

# ---------- 配置 fstab 自动挂载 ----------
configure_fstab() {
    log_info "配置 fstab 自动挂载..."

    local uuid
    uuid=$(blkid -s UUID -o value "$DISK" 2>/dev/null)
    if [[ -z "$uuid" ]]; then
        die "获取磁盘 UUID 失败: $DISK" 11
    fi
    log_debug "磁盘 UUID: $uuid"

    # 备份 fstab(带时间戳,唯一文件名)
    local backup="${FSTAB}.bak.$(date +%Y%m%d_%H%M%S)"
    if ! cp "$FSTAB" "$backup"; then
        die "备份 $FSTAB 失败" 11
    fi
    log_info "fstab 已备份至: $backup"

    local fstab_entry="UUID=${uuid}  ${MOUNT_POINT}  xfs  defaults,noatime,nodiratime  0 0"

    # 先删除与本次 UUID 或挂载点相关的旧条目(避免重复)
    local tmp_fstab
    tmp_fstab=$(mktemp)
    grep -Ev "^[[:space:]]*UUID=${uuid}[[:space:]]|[[:space:]]${MOUNT_POINT}[[:space:]]" \
        "$FSTAB" > "$tmp_fstab" || true
    echo "$fstab_entry" >> "$tmp_fstab"

    # 用 mount -a 验证新 fstab(仅新增条目),失败则不写入
    if ! mount --fake -a -T "$tmp_fstab" 2>/dev/null; then
        log_warn "mount --fake 验证不可用,跳过预验证(内核版本较旧)"
    fi

    # 原子替换 fstab
    if ! mv "$tmp_fstab" "$FSTAB"; then
        rm -f "$tmp_fstab"
        die "写入 $FSTAB 失败" 12
    fi

    log_success "fstab 配置完成 (UUID=$uuid)"
}

# ---------- 创建数据目录 ----------
create_data_directory() {
    local data_dir="${MOUNT_POINT}/${DATA_SUBDIR}"
    log_info "创建数据目录: $data_dir"

    if ! mkdir -p "$data_dir"; then
        die "创建数据目录失败: $data_dir" 13
    fi
    chmod 755 "$data_dir"
    log_success "数据目录创建完成: $data_dir"
}

# ---------- 结果摘要 ----------
show_summary() {
    local uuid
    uuid=$(blkid -s UUID -o value "$DISK" 2>/dev/null || echo "未知")
    local fs_type
    fs_type=$(blkid -s TYPE -o value "$DISK" 2>/dev/null || echo "未知")

    echo ""
    echo "========================================"
    echo "            操作完成摘要"
    echo "========================================"
    printf "  %-12s %s\n" "设备:"      "$DISK"
    printf "  %-12s %s\n" "文件系统:"  "$fs_type"
    printf "  %-12s %s\n" "UUID:"      "$uuid"
    printf "  %-12s %s\n" "挂载点:"    "$MOUNT_POINT"
    printf "  %-12s %s\n" "数据目录:"  "${MOUNT_POINT}/${DATA_SUBDIR}"
    printf "  %-12s %s\n" "挂载状态:"  "$(mountpoint -q "$MOUNT_POINT" && echo '已挂载' || echo '未挂载')"
    echo "========================================"
    echo "  磁盘空间:"
    df -h "$MOUNT_POINT" | awk 'NR==1{printf "  %s\n",$0} NR>1{printf "  %s\n",$0}'
    echo "========================================"
    echo ""
}

# ---------- 信号处理 ----------
trap 'log_error "脚本被中断 (信号)"; exit 130' INT TERM

# ---------- 主流程 ----------
main() {
    log_info "=== $SCRIPT_NAME 开始执行 (PID=$$) ==="
    log_debug "原始参数: $*"

    parse_args "$@"
    check_root
    check_dependencies
    validate_parameters

    log_info "目标设备: $DISK | 挂载点: $MOUNT_POINT | 数据子目录: $DATA_SUBDIR"

    confirm_operation
    format_disk
    prepare_mountpoint
    mount_disk
    configure_fstab
    create_data_directory
    show_summary

    log_success "=== 磁盘初始化完成 ==="
}

main "$@"

启动单一脚本

#!/usr/bin/env bash
# ==============================================================================
# dmsdocs.sh - 生产环境 dms-doc 容器部署脚本
#
# 功能:
#   - 自动检测容器运行时:优先 docker,无 docker 则使用 nerdctl
#   - 拉取指定 tag 的 dmsdocs 镜像并以受控方式启动容器
#   - 支持镜像拉取重试、滚动替换(先备份旧容器,新容器启动失败自动回滚)
#   - 支持 dry-run、参数化配置、健康检查、日志限额等生产化能力
#
# 退出码:
#   0  成功
#   1  参数错误
#   2  环境/依赖检查失败
#   3  镜像拉取失败
#   4  容器启动失败 / 健康检查失败(已尝试回滚)
# ==============================================================================

set -Eeuo pipefail

# ------------------------------ 默认配置 --------------------------------------
# 允许通过环境变量覆盖,便于 CI/CD 与多环境复用
readonly SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_VERSION="2.1.0"

IMAGE_PREFIX="${DMSDOCS_IMAGE_PREFIX:-hub-vpc.jdcloud.com/tj-deliver/dmsdocs-openeuler22-amd64}"
CONTAINER_NAME="${DMSDOCS_CONTAINER_NAME:-dms-doc}"
NETWORK_MODE="${DMSDOCS_NETWORK:-host}"
RESTART_POLICY="${DMSDOCS_RESTART:-unless-stopped}"
LOG_MAX_SIZE="${DMSDOCS_LOG_MAX_SIZE:-100m}"
LOG_MAX_FILE="${DMSDOCS_LOG_MAX_FILE:-3}"
PULL_RETRY="${DMSDOCS_PULL_RETRY:-3}"
PULL_RETRY_INTERVAL="${DMSDOCS_PULL_RETRY_INTERVAL:-5}"
HEALTH_CHECK_TIMEOUT="${DMSDOCS_HEALTH_TIMEOUT:-30}"
STOP_TIMEOUT="${DMSDOCS_STOP_TIMEOUT:-30}"
# 容器运行时:auto|docker|nerdctl,默认 auto 自动探测
RUNTIME_PREF="${DMSDOCS_RUNTIME:-auto}"
# nerdctl 使用的 namespace(默认 k8s.io 与 containerd CRI 一致,可改为 default)
NERDCTL_NAMESPACE="${DMSDOCS_NERDCTL_NAMESPACE:-k8s.io}"

TAG=""
FORCE_PULL=false
DRY_RUN=false
SKIP_HEALTH=false

# 运行时相关全局变量(由 detect_runtime 设置)
RUNTIME_BIN=""        # 实际使用的命令,如 docker / nerdctl
RUNTIME_NAME=""       # 名称标识: docker / nerdctl
RUNTIME_CMD=()        # 完整调用前缀(数组),含 nerdctl 的 -n namespace

# ------------------------------ 日志 ------------------------------------------
# 仅在 stderr 是 TTY 时启用颜色,避免日志文件出现转义码
if [[ -t 2 ]]; then
    readonly C_RED='\033[0;31m'
    readonly C_GRN='\033[0;32m'
    readonly C_YEL='\033[1;33m'
    readonly C_NC='\033[0m'
else
    readonly C_RED='' C_GRN='' C_YEL='' C_NC=''
fi

_log() {
    local level="$1"; shift
    local color="$1"; shift
    printf '%b[%s] [%s]%b %s\n' \
        "${color}" "$(date '+%Y-%m-%d %H:%M:%S')" "${level}" "${C_NC}" "$*" >&2
}
log_info()  { _log "INFO"  "${C_GRN}" "$@"; }
log_warn()  { _log "WARN"  "${C_YEL}" "$@"; }
log_error() { _log "ERROR" "${C_RED}" "$@"; }

# ------------------------------ 错误处理 --------------------------------------
on_error() {
    local exit_code=$?
    local line_no=$1
    log_error "脚本在第 ${line_no} 行异常退出 (exit=${exit_code})"
    exit "${exit_code}"
}
trap 'on_error ${LINENO}' ERR

# ------------------------------ 帮助信息 --------------------------------------
usage() {
    cat <<EOF
${SCRIPT_NAME} v${SCRIPT_VERSION} - dms-doc 生产部署脚本

使用方法:
    ${SCRIPT_NAME} -o <镜像tag> [选项]

必选参数:
    -o <tag>        指定镜像 tag

可选参数:
    -f              强制重新拉取镜像(即使本地已存在)
    -n <name>       自定义容器名 (默认: ${CONTAINER_NAME})
    -p <prefix>     自定义镜像前缀 (默认: ${IMAGE_PREFIX})
    -r <runtime>    指定运行时: auto|docker|nerdctl (默认: ${RUNTIME_PREF})
    -d              dry-run 模式,仅打印将要执行的命令
    -s              跳过容器健康检查(不推荐)
    -h              显示本帮助

环境变量(可覆盖默认值):
    DMSDOCS_IMAGE_PREFIX, DMSDOCS_CONTAINER_NAME, DMSDOCS_NETWORK,
    DMSDOCS_RESTART, DMSDOCS_LOG_MAX_SIZE, DMSDOCS_LOG_MAX_FILE,
    DMSDOCS_PULL_RETRY, DMSDOCS_PULL_RETRY_INTERVAL,
    DMSDOCS_HEALTH_TIMEOUT, DMSDOCS_STOP_TIMEOUT,
    DMSDOCS_RUNTIME, DMSDOCS_NERDCTL_NAMESPACE

运行时选择策略:
    auto    优先使用 docker;若 docker 不可用则回退到 nerdctl
    docker  强制使用 docker
    nerdctl 强制使用 nerdctl(默认 namespace=${NERDCTL_NAMESPACE})

示例:
    ${SCRIPT_NAME} -o v1.0.0-aistack-v3-404ee75
    ${SCRIPT_NAME} -o v1.0.0-aistack-v3-404ee75 -f
    ${SCRIPT_NAME} -o v1.0.0 -r nerdctl
    DMSDOCS_CONTAINER_NAME=dms-doc-staging ${SCRIPT_NAME} -o v1.0.0 -d
EOF
}

# ------------------------------ 工具函数 --------------------------------------
run_cmd() {
    if ${DRY_RUN}; then
        log_info "[dry-run] $*"
        return 0
    fi
    "$@"
}

# 调用容器运行时(docker 或 nerdctl)
rt() {
    "${RUNTIME_CMD[@]}" "$@"
}

# 在 run_cmd 包裹下调用运行时(支持 dry-run)
run_rt() {
    run_cmd "${RUNTIME_CMD[@]}" "$@"
}

require_cmd() {
    if ! command -v "$1" >/dev/null 2>&1; then
        log_error "缺少依赖命令: $1"
        exit 2
    fi
}

# 检查给定运行时是否可用(命令存在且 daemon 可达)
runtime_available() {
    local bin="$1"
    command -v "${bin}" >/dev/null 2>&1 || return 1
    case "${bin}" in
        docker)
            docker info >/dev/null 2>&1
            ;;
        nerdctl)
            # nerdctl 需要能连上 containerd,使用指定 namespace 探测
            nerdctl -n "${NERDCTL_NAMESPACE}" info >/dev/null 2>&1
            ;;
        *)
            return 1
            ;;
    esac
}

# 自动探测/选择运行时
detect_runtime() {
    case "${RUNTIME_PREF}" in
        docker)
            if ! runtime_available docker; then
                log_error "已指定 docker,但 docker 不可用(命令缺失或 daemon 不可达)"
                exit 2
            fi
            RUNTIME_BIN="docker"; RUNTIME_NAME="docker"
            RUNTIME_CMD=(docker)
            ;;
        nerdctl)
            if ! runtime_available nerdctl; then
                log_error "已指定 nerdctl,但 nerdctl 不可用(命令缺失或 containerd 不可达)"
                exit 2
            fi
            RUNTIME_BIN="nerdctl"; RUNTIME_NAME="nerdctl"
            RUNTIME_CMD=(nerdctl -n "${NERDCTL_NAMESPACE}")
            ;;
        auto|"")
            if runtime_available docker; then
                RUNTIME_BIN="docker"; RUNTIME_NAME="docker"
                RUNTIME_CMD=(docker)
            elif runtime_available nerdctl; then
                RUNTIME_BIN="nerdctl"; RUNTIME_NAME="nerdctl"
                RUNTIME_CMD=(nerdctl -n "${NERDCTL_NAMESPACE}")
                log_warn "未检测到可用的 docker,回退使用 nerdctl (namespace=${NERDCTL_NAMESPACE})"
            else
                log_error "未检测到可用的容器运行时(docker / nerdctl 均不可用)"
                exit 2
            fi
            ;;
        *)
            log_error "无效的运行时配置: ${RUNTIME_PREF} (可选: auto|docker|nerdctl)"
            exit 1
            ;;
    esac

    local ver
    ver="$(rt version --format '{{.Server.Version}}' 2>/dev/null \
        || rt --version 2>/dev/null \
        || echo 'unknown')"
    log_info "使用运行时: ${RUNTIME_NAME} (${ver})"
}

validate_tag() {
    # 镜像 tag 合法字符: [A-Za-z0-9_.-],长度 <=128
    if [[ ! "$1" =~ ^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$ ]]; then
        log_error "非法的镜像 tag: '$1'"
        exit 1
    fi
}

container_exists() {
    rt ps -a --format '{{.Names}}' | grep -Fxq "$1"
}

container_running() {
    rt ps --format '{{.Names}}' | grep -Fxq "$1"
}

# ------------------------------ 业务步骤 --------------------------------------
preflight_check() {
    require_cmd grep
    require_cmd date
    detect_runtime
    log_info "环境检查通过"
}

pull_image() {
    local image="$1"
    local attempt=1

    if ! ${FORCE_PULL} && rt image inspect "${image}" >/dev/null 2>&1; then
        log_info "本地已存在镜像 ${image},跳过拉取(使用 -f 强制拉取)"
        return 0
    fi

    while (( attempt <= PULL_RETRY )); do
        log_info "拉取镜像 ${image} (第 ${attempt}/${PULL_RETRY} 次)"
        if run_rt pull "${image}"; then
            log_info "镜像拉取成功"
            return 0
        fi
        log_warn "拉取失败,${PULL_RETRY_INTERVAL}s 后重试..."
        sleep "${PULL_RETRY_INTERVAL}"
        (( attempt++ ))
    done

    log_error "镜像拉取失败,已重试 ${PULL_RETRY} 次: ${image}"
    exit 3
}

backup_old_container() {
    # 滚动替换:将旧容器重命名为备份,启动失败时可回滚
    local backup="${CONTAINER_NAME}-backup-$(date +%s)"
    if container_exists "${CONTAINER_NAME}"; then
        log_warn "发现旧容器 ${CONTAINER_NAME},重命名为 ${backup} 以便回滚"
        if container_running "${CONTAINER_NAME}"; then
            run_rt stop --time "${STOP_TIMEOUT}" "${CONTAINER_NAME}" >/dev/null
        fi
        run_rt rename "${CONTAINER_NAME}" "${backup}" >/dev/null
        echo "${backup}"
    else
        echo ""
    fi
}

rollback() {
    local backup="$1"
    [[ -z "${backup}" ]] && return 0
    log_warn "执行回滚: 恢复容器 ${backup} -> ${CONTAINER_NAME}"
    # 清理可能残留的同名失败容器
    if container_exists "${CONTAINER_NAME}"; then
        run_rt rm -f "${CONTAINER_NAME}" >/dev/null || true
    fi
    run_rt rename "${backup}" "${CONTAINER_NAME}" >/dev/null
    run_rt start "${CONTAINER_NAME}" >/dev/null || true
    log_info "回滚完成"
}

cleanup_backup() {
    local backup="$1"
    [[ -z "${backup}" ]] && return 0
    log_info "清理备份容器 ${backup}"
    run_rt rm -f "${backup}" >/dev/null || true
}

start_container() {
    local image="$1"
    log_info "启动新容器 ${CONTAINER_NAME} (image=${image}, runtime=${RUNTIME_NAME})"

    # docker 与 nerdctl 通用的 run 参数
    local -a run_args=(
        run -d
        --name="${CONTAINER_NAME}"
        --network="${NETWORK_MODE}"
        --restart="${RESTART_POLICY}"
        --label "app=dms-doc"
        --label "deployed_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
        --label "deployed_tag=${TAG}"
    )

    # 日志选项:docker 原生支持;nerdctl 也支持 json-file 驱动及 max-size/max-file
    run_args+=(
        --log-driver=json-file
        --log-opt "max-size=${LOG_MAX_SIZE}"
        --log-opt "max-file=${LOG_MAX_FILE}"
    )

    run_args+=("${image}")
    run_rt "${run_args[@]}" >/dev/null
}

health_check() {
    if ${SKIP_HEALTH} || ${DRY_RUN}; then
        log_warn "已跳过健康检查"
        return 0
    fi

    log_info "等待容器进入 running 状态(超时 ${HEALTH_CHECK_TIMEOUT}s)..."
    local elapsed=0
    while (( elapsed < HEALTH_CHECK_TIMEOUT )); do
        local status
        status="$(rt inspect -f '{{.State.Status}}' "${CONTAINER_NAME}" 2>/dev/null || echo 'missing')"
        case "${status}" in
            running)
                # 再观察 3s,防止瞬间退出
                sleep 3
                status="$(rt inspect -f '{{.State.Status}}' "${CONTAINER_NAME}" 2>/dev/null || echo 'missing')"
                if [[ "${status}" == "running" ]]; then
                    log_info "容器健康检查通过"
                    return 0
                fi
                ;;
            exited|dead)
                log_error "容器已退出 (status=${status})"
                rt logs --tail 50 "${CONTAINER_NAME}" >&2 || true
                return 1
                ;;
        esac
        sleep 2
        (( elapsed += 2 ))
    done
    log_error "健康检查超时,容器未达到 running 状态"
    rt logs --tail 50 "${CONTAINER_NAME}" >&2 || true
    return 1
}

print_summary() {
    log_info "部署完成"
    echo "----------------------------------------" >&2
    rt ps -a --filter "name=^${CONTAINER_NAME}$" \
        --format 'table {{.Names}}\t{{.Status}}\t{{.Image}}' >&2 || true
    echo "----------------------------------------" >&2
    local cli="${RUNTIME_CMD[*]}"
    log_info "查看日志:  ${cli} logs -f ${CONTAINER_NAME}"
    log_info "进入容器:  ${cli} exec -it ${CONTAINER_NAME} bash"
}

# ------------------------------ 主流程 ----------------------------------------
main() {
    while getopts "o:n:p:r:fdsh" opt; do
        case "${opt}" in
            o) TAG="${OPTARG}" ;;
            n) CONTAINER_NAME="${OPTARG}" ;;
            p) IMAGE_PREFIX="${OPTARG}" ;;
            r) RUNTIME_PREF="${OPTARG}" ;;
            f) FORCE_PULL=true ;;
            d) DRY_RUN=true ;;
            s) SKIP_HEALTH=true ;;
            h) usage; exit 0 ;;
            \?) usage; exit 1 ;;
        esac
    done

    if [[ -z "${TAG}" ]]; then
        log_error "必须通过 -o 指定镜像 tag"
        usage
        exit 1
    fi
    validate_tag "${TAG}"

    local full_image="${IMAGE_PREFIX}:${TAG}"
    log_info "目标镜像: ${full_image}"
    log_info "容器名:   ${CONTAINER_NAME}"
    ${DRY_RUN} && log_warn "当前为 DRY-RUN 模式,不会真实变更系统"

    preflight_check
    pull_image "${full_image}"

    local backup
    backup="$(backup_old_container)"

    # 关闭 ERR trap,自行处理启动失败以便回滚
    trap - ERR
    if start_container "${full_image}" && health_check; then
        cleanup_backup "${backup}"
        trap 'on_error ${LINENO}' ERR
        print_summary
        exit 0
    else
        log_error "新容器启动/健康检查失败"
        run_rt rm -f "${CONTAINER_NAME}" >/dev/null 2>&1 || true
        rollback "${backup}"
        exit 4
    fi
}

main "$@"
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容