1453 lines
41 KiB
Bash
1453 lines
41 KiB
Bash
#!/usr/bin/env bash
|
||
# ==============================================================================
|
||
# Metadata
|
||
# ==============================================================================
|
||
# Author : ChatGPT
|
||
# Version : 1.1.0
|
||
# License : MIT
|
||
# Last Update : 2026-03-02
|
||
# Bash : >= 5.0
|
||
# Description : 生产级 Docker 在线安装脚本(CentOS 7 / CentOS 8 / openEuler 20.03 / 22.03)
|
||
#
|
||
# Assumptions
|
||
# 1) 默认使用 YUM 作为包管理工具(按你的要求),即使系统存在 dnf,也优先走 yum。
|
||
# 2) openEuler 使用 openEuler Everything 临时仓补齐依赖;若仍不足,再回退到 CentOS 8 Vault 兼容仓。
|
||
# 3) Docker 仓库按 el7 / el8 路径适配;openEuler 20.03 / 22.03 映射为 el8。
|
||
# 4) GPG 支持三种模式:
|
||
# - strict(默认):启用 gpgcheck,并优先使用镜像站 gpgkey(或官方 gpgkey)
|
||
# - mirror:显式使用镜像站 gpgkey
|
||
# - skip:跳过 repo 包签名校验(gpgcheck=0),适合受限网络或 key 拉取失败场景
|
||
# 5) 为安全起见,默认不覆盖已有 daemon.json;如需覆盖,请加 --force。
|
||
#
|
||
# ==============================================================================
|
||
# Call Graph (ASCII)
|
||
# ==============================================================================
|
||
# main
|
||
# |
|
||
# +--> init_runtime
|
||
# | +--> init_traps
|
||
# | +--> detect_os
|
||
# | +--> validate_arch
|
||
# |
|
||
# +--> parse_args
|
||
# +--> validate_args
|
||
# +--> check_requirements
|
||
# +--> prepare_proxy_env
|
||
# +--> cleanup_conflicting_packages
|
||
# +--> configure_docker_repo
|
||
# +--> handle_gpg_key
|
||
# +--> install_prerequisites
|
||
# | +--> add_openeuler_everything_repo
|
||
# | +--> add_el8_compat_repo
|
||
# |
|
||
# +--> resolve_docker_version
|
||
# +--> install_docker_packages
|
||
# +--> configure_docker_daemon
|
||
# +--> enable_and_start_docker
|
||
# +--> verify_installation
|
||
# +--> print_summary
|
||
#
|
||
# ==============================================================================
|
||
# Strict Mode / Safety
|
||
# ==============================================================================
|
||
set -Eeuo pipefail
|
||
set -o errtrace
|
||
IFS=$' \t\n'
|
||
|
||
# ==============================================================================
|
||
# Constants
|
||
# ==============================================================================
|
||
readonly SCRIPT_NAME="$(basename "$0")"
|
||
readonly SCRIPT_VERSION="1.1.0"
|
||
|
||
readonly EC_OK=0
|
||
readonly EC_GENERAL=1
|
||
readonly EC_USAGE=2
|
||
readonly EC_UNSUPPORTED_OS=10
|
||
readonly EC_REQUIREMENT=11
|
||
readonly EC_PERMISSION=12
|
||
readonly EC_NETWORK=20
|
||
readonly EC_REPO=21
|
||
readonly EC_INSTALL=22
|
||
readonly EC_SERVICE=23
|
||
readonly EC_VERIFY=24
|
||
|
||
readonly LOG_DEBUG=10
|
||
readonly LOG_INFO=20
|
||
readonly LOG_WARN=30
|
||
readonly LOG_ERROR=40
|
||
|
||
readonly DEFAULT_LOG_LEVEL="INFO"
|
||
readonly DEFAULT_MIRROR_PROVIDER="aliyun"
|
||
readonly DEFAULT_DOCKER_VERSION="20.10"
|
||
readonly DEFAULT_GPG_MODE="strict"
|
||
|
||
readonly TMP_ROOT_PREFIX="/tmp/docker-online-install"
|
||
readonly DOCKER_REPO_FILE="/etc/yum.repos.d/docker-ce.repo"
|
||
readonly DOCKER_DAEMON_CFG="/etc/docker/daemon.json"
|
||
readonly OE_PREREQ_REPO_FILE="/etc/yum.repos.d/openeuler-prereq-tmp.repo"
|
||
readonly EL8_COMPAT_REPO_FILE="/etc/yum.repos.d/el8-prereq-compat-tmp.repo"
|
||
|
||
readonly MIRROR_URL_ALIYUN="https://mirrors.aliyun.com/docker-ce/linux/centos"
|
||
readonly MIRROR_URL_TENCENT="https://mirrors.tencent.com/docker-ce/linux/centos"
|
||
readonly MIRROR_URL_HUAWEI="https://repo.huaweicloud.com/docker-ce/linux/centos"
|
||
readonly MIRROR_URL_OFFICIAL="https://download.docker.com/linux/centos"
|
||
readonly OFFICIAL_DOCKER_GPG_KEY_URL="https://download.docker.com/linux/centos/gpg"
|
||
|
||
readonly OE_REPO_BASE="https://repo.openeuler.org"
|
||
|
||
# ==============================================================================
|
||
# Globals
|
||
# ==============================================================================
|
||
LOG_LEVEL="${LOG_LEVEL:-$DEFAULT_LOG_LEVEL}"
|
||
DRY_RUN=0
|
||
FORCE=0
|
||
QUIET=0
|
||
CONFIRM=0
|
||
|
||
MIRROR_PROVIDER="$DEFAULT_MIRROR_PROVIDER"
|
||
DOCKER_VERSION="${DOCKER_VERSION:-$DEFAULT_DOCKER_VERSION}"
|
||
GPG_MODE="${GPG_MODE:-$DEFAULT_GPG_MODE}"
|
||
PROXY_URL="${PROXY_URL:-}"
|
||
|
||
SKIP_START=0
|
||
SKIP_ENABLE=0
|
||
CONFIGURE_DAEMON=1
|
||
REMOVE_CONFLICTS=1
|
||
|
||
OS_ID_RAW=""
|
||
OS_ID=""
|
||
OS_VERSION_ID=""
|
||
OS_EL_VER=""
|
||
OS_PRETTY_NAME=""
|
||
OS_ARCH=""
|
||
PKG_MGR="yum"
|
||
|
||
MIRROR_BASE_URL=""
|
||
DOCKER_GPG_KEY_URL=""
|
||
TMP_DIR=""
|
||
|
||
INSTALL_FLAGS=()
|
||
PKG_GLOBAL_OPTS=()
|
||
PKG_MGR_STABLE_OPTS=()
|
||
|
||
# Bash 数组用于安全传参,避免 eval。
|
||
PREREQ_PKGS=(
|
||
"container-selinux"
|
||
"fuse-overlayfs"
|
||
"slirp4netns"
|
||
)
|
||
|
||
# ==============================================================================
|
||
# Logging
|
||
# ==============================================================================
|
||
### 将日志级别转换为数值
|
||
### @param level string 日志级别
|
||
### @return 0 输出对应数值
|
||
### @require none
|
||
log_level_to_num() {
|
||
case "${1:-INFO}" in
|
||
DEBUG) printf '%s\n' "$LOG_DEBUG" ;;
|
||
INFO) printf '%s\n' "$LOG_INFO" ;;
|
||
WARN) printf '%s\n' "$LOG_WARN" ;;
|
||
ERROR) printf '%s\n' "$LOG_ERROR" ;;
|
||
*) printf '%s\n' "$LOG_INFO" ;;
|
||
esac
|
||
}
|
||
|
||
### 统一日志输出
|
||
### @param level string 日志级别
|
||
### @param message string 消息
|
||
### @return 0 成功
|
||
### @require date
|
||
### @side_effect 向 stderr 输出
|
||
log_msg() {
|
||
local level="${1:?level required}"
|
||
shift
|
||
local message="${*:-}"
|
||
local current_num target_num timestamp
|
||
|
||
current_num="$(log_level_to_num "$LOG_LEVEL")"
|
||
target_num="$(log_level_to_num "$level")"
|
||
|
||
if [[ "$target_num" -lt "$current_num" ]]; then
|
||
return 0
|
||
fi
|
||
|
||
if [[ "$QUIET" -eq 1 && "$level" != "ERROR" ]]; then
|
||
return 0
|
||
fi
|
||
|
||
timestamp="$(date '+%F %T')"
|
||
printf '%s [%s] %s: %s\n' "$timestamp" "$level" "$SCRIPT_NAME" "$message" >&2
|
||
}
|
||
|
||
### DEBUG 日志
|
||
### @param message string 日志消息
|
||
### @return 0 成功
|
||
### @require none
|
||
log_debug() { log_msg "DEBUG" "$@"; }
|
||
|
||
### INFO 日志
|
||
### @param message string 日志消息
|
||
### @return 0 成功
|
||
### @require none
|
||
log_info() { log_msg "INFO" "$@"; }
|
||
|
||
### WARN 日志
|
||
### @param message string 日志消息
|
||
### @return 0 成功
|
||
### @require none
|
||
log_warn() { log_msg "WARN" "$@"; }
|
||
|
||
### ERROR 日志
|
||
### @param message string 日志消息
|
||
### @return 0 成功
|
||
### @require none
|
||
log_error() { log_msg "ERROR" "$@"; }
|
||
|
||
# ==============================================================================
|
||
# Error / Trap / Cleanup
|
||
# ==============================================================================
|
||
### 统一失败退出
|
||
### @param message string 错误消息
|
||
### @param exit_code int 退出码
|
||
### @return <exit_code> 失败退出
|
||
### @require none
|
||
die() {
|
||
local message="${1:-unknown error}"
|
||
local exit_code="${2:-$EC_GENERAL}"
|
||
log_error "$message"
|
||
exit "$exit_code"
|
||
}
|
||
|
||
### 输出函数调用栈
|
||
### @param none
|
||
### @return 0 成功
|
||
### @require none
|
||
print_call_stack() {
|
||
local i
|
||
for ((i=1; i<${#FUNCNAME[@]}; i++)); do
|
||
printf ' at %s() %s:%s\n' \
|
||
"${FUNCNAME[$i]}" \
|
||
"${BASH_SOURCE[$i]:-unknown}" \
|
||
"${BASH_LINENO[$((i-1))]:-unknown}" >&2
|
||
done
|
||
}
|
||
|
||
### ERR trap 处理器
|
||
### @param none
|
||
### @return 0 由 trap 调用
|
||
### @require none
|
||
on_err() {
|
||
local exit_code="$?"
|
||
local cmd="${BASH_COMMAND:-unknown}"
|
||
local line_no="${BASH_LINENO[0]:-unknown}"
|
||
local src="${BASH_SOURCE[1]:-${BASH_SOURCE[0]:-unknown}}"
|
||
|
||
log_error "命令执行失败:${cmd}"
|
||
log_error "失败位置:${src}:${line_no}"
|
||
log_error "退出码:${exit_code}"
|
||
print_call_stack
|
||
exit "$exit_code"
|
||
}
|
||
|
||
### 信号处理器
|
||
### @param signal string 信号名
|
||
### @return 130 中断退出
|
||
### @require none
|
||
on_signal() {
|
||
local signal_name="${1:-SIGNAL}"
|
||
log_warn "收到信号:${signal_name},开始退出。"
|
||
exit 130
|
||
}
|
||
|
||
### EXIT trap 处理器
|
||
### @param none
|
||
### @return 0 成功
|
||
### @require none
|
||
### @side_effect 清理临时目录
|
||
on_exit() {
|
||
local exit_code="$?"
|
||
cleanup || true
|
||
if [[ "$exit_code" -eq 0 ]]; then
|
||
log_info "脚本执行完成。"
|
||
else
|
||
log_warn "脚本退出,状态码:${exit_code}"
|
||
fi
|
||
}
|
||
|
||
### 清理临时文件
|
||
### @param none
|
||
### @return 0 成功
|
||
### @require rm
|
||
### @side_effect 删除临时目录、临时 repo
|
||
cleanup() {
|
||
# if [[ -f "$OE_PREREQ_REPO_FILE" ]]; then
|
||
# rm -f -- "$OE_PREREQ_REPO_FILE"
|
||
# fi
|
||
# if [[ -f "$EL8_COMPAT_REPO_FILE" ]]; then
|
||
# rm -f -- "$EL8_COMPAT_REPO_FILE"
|
||
# fi
|
||
if [[ -n "${TMP_DIR:-}" && -d "${TMP_DIR:-}" ]]; then
|
||
rm -rf -- "$TMP_DIR"
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
### 初始化 trap
|
||
### @param none
|
||
### @return 0 成功
|
||
### @require none
|
||
init_traps() {
|
||
trap 'on_err' ERR
|
||
trap 'on_signal INT' INT
|
||
trap 'on_signal TERM' TERM
|
||
trap 'on_exit' EXIT
|
||
}
|
||
|
||
# ==============================================================================
|
||
# Helpers
|
||
# ==============================================================================
|
||
### 输出帮助信息
|
||
### @param none
|
||
### @return 0 成功
|
||
### @require cat
|
||
usage() {
|
||
cat <<EOF
|
||
用法:
|
||
bash ${SCRIPT_NAME} [选项]
|
||
|
||
选项:
|
||
-h, --help 显示帮助
|
||
--dry-run 仅打印将执行的动作,不实际执行
|
||
--force 允许覆盖 docker repo / daemon.json
|
||
--verbose 输出 DEBUG 日志
|
||
--quiet 仅输出 ERROR 日志
|
||
--log-level <LEVEL> DEBUG|INFO|WARN|ERROR
|
||
--mirror <PROVIDER> aliyun|tencent|huawei|official(默认:aliyun)
|
||
--proxy <URL> 代理地址,例如:http://192.168.1.10:3128
|
||
--docker-version <VERSION> Docker 版本前缀(默认:20.10)
|
||
--gpg-mode <MODE> strict|mirror|skip(默认:strict)
|
||
--skip-start 安装后不启动 Docker
|
||
--skip-enable 安装后不设置开机自启
|
||
--skip-daemon-config 不写入 /etc/docker/daemon.json
|
||
--keep-conflicts 不移除旧 Docker 冲突包
|
||
--yes 非交互确认
|
||
|
||
说明:
|
||
1) 支持:CentOS 7 / CentOS 8 / openEuler 20.03 / openEuler 22.03
|
||
2) openEuler 会优先启用临时 Everything 仓补齐依赖,再回退 CentOS 8 Vault 仓
|
||
3) 包管理工具固定使用 yum
|
||
4) --gpg-mode skip 可跳过 repo 包签名校验(适合 GPG key 获取困难场景)
|
||
|
||
示例:
|
||
sudo bash ${SCRIPT_NAME} --mirror aliyun
|
||
sudo bash ${SCRIPT_NAME} --mirror tencent --proxy http://192.168.1.10:3128
|
||
sudo bash ${SCRIPT_NAME} --gpg-mode skip --docker-version 24.0
|
||
sudo bash ${SCRIPT_NAME} --dry-run --mirror huawei
|
||
EOF
|
||
}
|
||
|
||
### 安全执行外部命令(支持 dry-run,并输出真实失败命令)
|
||
### @param ... command 命令及参数
|
||
### @return 0 成功;非 0 失败
|
||
### @require none
|
||
run_cmd() {
|
||
local -a cmd=( "$@" )
|
||
local rc=0
|
||
|
||
[[ "${#cmd[@]}" -gt 0 ]] || die "run_cmd 收到空命令。" "$EC_GENERAL"
|
||
log_debug "执行命令:${cmd[*]}"
|
||
|
||
if [[ "$DRY_RUN" -eq 1 ]]; then
|
||
printf '[DRY-RUN] %s\n' "${cmd[*]}" >&2
|
||
return 0
|
||
fi
|
||
|
||
# > 在函数内接管错误输出,避免 ERR trap 只能拿到 "${cmd[@]}" 这种包装表达式
|
||
set +e
|
||
"${cmd[@]}"
|
||
rc=$?
|
||
set -e
|
||
|
||
if [[ "$rc" -ne 0 ]]; then
|
||
log_error "外部命令执行失败:${cmd[*]}"
|
||
log_error "命令退出码:${rc}"
|
||
return "$rc"
|
||
fi
|
||
|
||
return 0
|
||
}
|
||
|
||
### 以 root 权限执行命令
|
||
### @param ... command 命令及参数
|
||
### @return 0 成功;非 0 失败
|
||
### @require sudo(非 root 时)
|
||
run_as_root() {
|
||
local -a cmd=( "$@" )
|
||
if [[ "$(id -u)" -eq 0 ]]; then
|
||
run_cmd "${cmd[@]}"
|
||
else
|
||
command -v sudo >/dev/null 2>&1 || die "当前非 root,且缺少 sudo。" "$EC_PERMISSION"
|
||
run_cmd sudo "${cmd[@]}"
|
||
fi
|
||
}
|
||
|
||
### 校验命令是否存在
|
||
### @param cmd string 命令名
|
||
### @return 0 存在;非 0 不存在
|
||
### @require none
|
||
require_cmd() {
|
||
local cmd="${1:?command required}"
|
||
command -v "$cmd" >/dev/null 2>&1
|
||
}
|
||
|
||
### 安全写文件(原子替换)
|
||
### @param target string 目标路径
|
||
### @param content string 内容
|
||
### @return 0 成功;非 0 失败
|
||
### @require mktemp install dirname
|
||
### @side_effect 写入文件
|
||
write_file_atomic() {
|
||
local target="${1:?target required}"
|
||
local content="${2:-}"
|
||
local tmp_file=""
|
||
|
||
tmp_file="$(mktemp "${TMP_DIR}/write.XXXXXX")"
|
||
printf '%s' "$content" > "$tmp_file"
|
||
|
||
run_as_root mkdir -p "$(dirname "$target")"
|
||
run_as_root install -m 0644 "$tmp_file" "$target"
|
||
rm -f -- "$tmp_file"
|
||
}
|
||
|
||
### 读取确认输入
|
||
### @param prompt string 提示信息
|
||
### @return 0 同意;1 拒绝
|
||
### @require read
|
||
confirm_action() {
|
||
local prompt="${1:-确认继续?}"
|
||
local answer=""
|
||
|
||
if [[ "$CONFIRM" -eq 1 ]]; then
|
||
return 0
|
||
fi
|
||
|
||
printf '%s [y/N]: ' "$prompt" >&2
|
||
read -r answer
|
||
case "$answer" in
|
||
y|Y|yes|YES) return 0 ;;
|
||
*) return 1 ;;
|
||
esac
|
||
}
|
||
|
||
# ==============================================================================
|
||
# Arg Parsing
|
||
# ==============================================================================
|
||
### 解析命令行参数
|
||
### @param ... args 参数列表
|
||
### @return 0 成功;非 0 失败
|
||
### @require none
|
||
parse_args() {
|
||
while [[ "$#" -gt 0 ]]; do
|
||
case "$1" in
|
||
-h|--help)
|
||
usage
|
||
exit 0
|
||
;;
|
||
--dry-run)
|
||
DRY_RUN=1
|
||
;;
|
||
--force)
|
||
FORCE=1
|
||
;;
|
||
--verbose)
|
||
LOG_LEVEL="DEBUG"
|
||
;;
|
||
--quiet)
|
||
QUIET=1
|
||
LOG_LEVEL="ERROR"
|
||
;;
|
||
--log-level)
|
||
shift
|
||
[[ "$#" -gt 0 ]] || die "--log-level 缺少参数。" "$EC_USAGE"
|
||
LOG_LEVEL="$1"
|
||
;;
|
||
--mirror)
|
||
shift
|
||
[[ "$#" -gt 0 ]] || die "--mirror 缺少参数。" "$EC_USAGE"
|
||
MIRROR_PROVIDER="$1"
|
||
;;
|
||
--proxy)
|
||
shift
|
||
[[ "$#" -gt 0 ]] || die "--proxy 缺少参数。" "$EC_USAGE"
|
||
PROXY_URL="$1"
|
||
;;
|
||
--docker-version)
|
||
shift
|
||
[[ "$#" -gt 0 ]] || die "--docker-version 缺少参数。" "$EC_USAGE"
|
||
DOCKER_VERSION="$1"
|
||
;;
|
||
--gpg-mode)
|
||
shift
|
||
[[ "$#" -gt 0 ]] || die "--gpg-mode 缺少参数。" "$EC_USAGE"
|
||
GPG_MODE="$1"
|
||
;;
|
||
--skip-start)
|
||
SKIP_START=1
|
||
;;
|
||
--skip-enable)
|
||
SKIP_ENABLE=1
|
||
;;
|
||
--skip-daemon-config)
|
||
CONFIGURE_DAEMON=0
|
||
;;
|
||
--keep-conflicts)
|
||
REMOVE_CONFLICTS=0
|
||
;;
|
||
--yes)
|
||
CONFIRM=1
|
||
;;
|
||
-*)
|
||
die "未知参数:$1" "$EC_USAGE"
|
||
;;
|
||
*)
|
||
die "不支持的位置参数:$1" "$EC_USAGE"
|
||
;;
|
||
esac
|
||
shift
|
||
done
|
||
}
|
||
|
||
### 校验参数合法性
|
||
### @param none
|
||
### @return 0 成功;非 0 失败
|
||
### @require none
|
||
validate_args() {
|
||
case "$LOG_LEVEL" in
|
||
DEBUG|INFO|WARN|ERROR) ;;
|
||
*) die "无效的 --log-level:${LOG_LEVEL}" "$EC_USAGE" ;;
|
||
esac
|
||
|
||
case "$MIRROR_PROVIDER" in
|
||
aliyun|tencent|huawei|official) ;;
|
||
*) die "无效的 --mirror:${MIRROR_PROVIDER}" "$EC_USAGE" ;;
|
||
esac
|
||
|
||
case "$GPG_MODE" in
|
||
strict|mirror|skip) ;;
|
||
*) die "无效的 --gpg-mode:${GPG_MODE}" "$EC_USAGE" ;;
|
||
esac
|
||
|
||
if [[ -n "$PROXY_URL" && ! "$PROXY_URL" =~ ^https?://[^[:space:]]+$ ]]; then
|
||
die "无效的 --proxy:${PROXY_URL}" "$EC_USAGE"
|
||
fi
|
||
}
|
||
|
||
# ==============================================================================
|
||
# OS / Environment
|
||
# ==============================================================================
|
||
### 检测操作系统
|
||
### @param none
|
||
### @return 0 成功;非 0 失败
|
||
### @require /etc/os-release
|
||
detect_os() {
|
||
[[ -r "/etc/os-release" ]] || die "缺少 /etc/os-release。" "$EC_UNSUPPORTED_OS"
|
||
|
||
# shellcheck disable=SC1091
|
||
. /etc/os-release
|
||
|
||
OS_ID_RAW="${ID:-}"
|
||
OS_ID="$(printf '%s' "$OS_ID_RAW" | tr '[:upper:]' '[:lower:]')"
|
||
OS_VERSION_ID="${VERSION_ID:-}"
|
||
OS_PRETTY_NAME="${PRETTY_NAME:-${NAME:-unknown}}"
|
||
OS_ARCH="$(uname -m)"
|
||
|
||
log_info "系统识别:ID=${OS_ID_RAW}(规范化=${OS_ID}), VERSION_ID=${OS_VERSION_ID}, ARCH=${OS_ARCH}"
|
||
|
||
case "$OS_ID" in
|
||
centos)
|
||
case "${OS_VERSION_ID%%.*}" in
|
||
7) OS_EL_VER="7" ;;
|
||
8) OS_EL_VER="8" ;;
|
||
*) die "不支持的 CentOS 版本:${OS_VERSION_ID}" "$EC_UNSUPPORTED_OS" ;;
|
||
esac
|
||
;;
|
||
openeuler)
|
||
# > 修复 openEuler 大小写问题:ID="openEuler" 会先转小写再判断
|
||
case "${OS_VERSION_ID%%.*}" in
|
||
20|22) OS_EL_VER="8" ;;
|
||
*) die "不支持的 openEuler 版本:${OS_VERSION_ID}" "$EC_UNSUPPORTED_OS" ;;
|
||
esac
|
||
log_info "openEuler 已映射到 Docker el8 仓路径。"
|
||
;;
|
||
*)
|
||
die "不支持的系统:${OS_PRETTY_NAME}" "$EC_UNSUPPORTED_OS"
|
||
;;
|
||
esac
|
||
}
|
||
|
||
### 校验架构
|
||
### @param none
|
||
### @return 0 成功;非 0 失败
|
||
### @require none
|
||
validate_arch() {
|
||
case "$OS_ARCH" in
|
||
x86_64|aarch64) ;;
|
||
*) die "不支持的架构:${OS_ARCH}" "$EC_UNSUPPORTED_OS" ;;
|
||
esac
|
||
}
|
||
|
||
### 初始化运行时
|
||
### @param none
|
||
### @return 0 成功
|
||
### @require mktemp
|
||
init_runtime() {
|
||
init_traps
|
||
TMP_DIR="$(mktemp -d "${TMP_ROOT_PREFIX}.XXXXXX")"
|
||
detect_os
|
||
validate_arch
|
||
}
|
||
|
||
# ==============================================================================
|
||
# Requirements / Proxy
|
||
# ==============================================================================
|
||
### 检查依赖
|
||
### @param none
|
||
### @return 0 成功;非 0 失败
|
||
### @require 基础命令
|
||
check_requirements() {
|
||
local required_cmds=( curl sed awk grep tee uname rpm systemctl yum )
|
||
local cmd=""
|
||
|
||
for cmd in "${required_cmds[@]}"; do
|
||
require_cmd "$cmd" || die "缺少依赖命令:${cmd}" "$EC_REQUIREMENT"
|
||
done
|
||
|
||
if [[ "$(id -u)" -ne 0 ]] && ! require_cmd sudo; then
|
||
die "当前非 root,且缺少 sudo。" "$EC_PERMISSION"
|
||
fi
|
||
}
|
||
|
||
### 准备代理环境
|
||
### @param none
|
||
### @return 0 成功
|
||
### @require none
|
||
prepare_proxy_env() {
|
||
PKG_GLOBAL_OPTS=()
|
||
PKG_MGR_STABLE_OPTS=(
|
||
"--setopt=timeout=60"
|
||
"--setopt=retries=10"
|
||
"--setopt=minrate=1k"
|
||
)
|
||
|
||
if [[ -n "$PROXY_URL" ]]; then
|
||
export http_proxy="$PROXY_URL"
|
||
export https_proxy="$PROXY_URL"
|
||
export HTTP_PROXY="$PROXY_URL"
|
||
export HTTPS_PROXY="$PROXY_URL"
|
||
export no_proxy="${no_proxy:-localhost,127.0.0.1}"
|
||
export NO_PROXY="${NO_PROXY:-localhost,127.0.0.1}"
|
||
|
||
PKG_GLOBAL_OPTS+=( "--setopt=proxy=${PROXY_URL}" )
|
||
log_info "已启用代理:${PROXY_URL}"
|
||
else
|
||
log_info "未启用代理。"
|
||
fi
|
||
}
|
||
|
||
### 获取镜像基地址
|
||
### @param provider string 镜像提供商
|
||
### @return 0 成功输出 URL;非 0 失败
|
||
### @require none
|
||
get_mirror_base_url() {
|
||
local provider="${1:?provider required}"
|
||
case "$provider" in
|
||
aliyun) printf '%s\n' "$MIRROR_URL_ALIYUN" ;;
|
||
tencent) printf '%s\n' "$MIRROR_URL_TENCENT" ;;
|
||
huawei) printf '%s\n' "$MIRROR_URL_HUAWEI" ;;
|
||
official) printf '%s\n' "$MIRROR_URL_OFFICIAL" ;;
|
||
*) return 1 ;;
|
||
esac
|
||
}
|
||
|
||
### 获取 gpgkey URL
|
||
### @param provider string 镜像提供商
|
||
### @return 0 成功输出 URL;非 0 失败
|
||
### @require none
|
||
get_gpg_key_url() {
|
||
local provider="${1:?provider required}"
|
||
|
||
case "$provider" in
|
||
aliyun) printf '%s\n' "${MIRROR_URL_ALIYUN}/gpg" ;;
|
||
tencent) printf '%s\n' "${MIRROR_URL_TENCENT}/gpg" ;;
|
||
huawei) printf '%s\n' "${MIRROR_URL_HUAWEI}/gpg" ;;
|
||
official) printf '%s\n' "${OFFICIAL_DOCKER_GPG_KEY_URL}" ;;
|
||
*) return 1 ;;
|
||
esac
|
||
}
|
||
|
||
# ==============================================================================
|
||
# YUM Wrappers
|
||
# ==============================================================================
|
||
### 执行 yum 命令
|
||
### @param ... args yum 参数
|
||
### @return 0 成功;非 0 失败
|
||
### @require yum
|
||
pkg_mgr() {
|
||
local -a cmd=( "$PKG_MGR" "${PKG_GLOBAL_OPTS[@]}" "$@" )
|
||
run_as_root "${cmd[@]}"
|
||
}
|
||
|
||
### 安装软件包
|
||
### @param ... pkgs 包名列表
|
||
### @return 0 成功;非 0 失败
|
||
### @require yum
|
||
install_pkgs() {
|
||
local -a packages=( "$@" )
|
||
[[ "${#packages[@]}" -gt 0 ]] || die "install_pkgs 收到空包列表。" "$EC_GENERAL"
|
||
|
||
pkg_mgr install -y "${PKG_MGR_STABLE_OPTS[@]}" "${packages[@]}"
|
||
}
|
||
|
||
### 卸载软件包
|
||
### @param ... pkgs 包名列表
|
||
### @return 0 成功;非 0 失败
|
||
### @require yum
|
||
remove_pkgs() {
|
||
local -a packages=( "$@" )
|
||
[[ "${#packages[@]}" -gt 0 ]] || die "remove_pkgs 收到空包列表。" "$EC_GENERAL"
|
||
|
||
pkg_mgr remove -y "${packages[@]}"
|
||
}
|
||
|
||
### 刷新缓存(Docker 镜像失败时自动回退官方源)
|
||
### @param none
|
||
### @return 0 成功;非 0 失败
|
||
### @require yum
|
||
make_cache() {
|
||
log_info "刷新系统软件仓缓存..."
|
||
if ! pkg_mgr makecache -y "${PKG_MGR_STABLE_OPTS[@]}"; then
|
||
log_warn "全量 makecache 失败,尝试仅刷新 Docker 仓..."
|
||
fi
|
||
|
||
log_info "刷新 docker-ce-stable 元数据..."
|
||
if pkg_mgr makecache -y "${PKG_MGR_STABLE_OPTS[@]}" --disablerepo='*' --enablerepo='docker-ce-stable'; then
|
||
log_info "docker-ce-stable 元数据刷新成功。"
|
||
return 0
|
||
fi
|
||
|
||
log_warn "当前 Docker 镜像源不可用:${MIRROR_PROVIDER} (${MIRROR_BASE_URL})"
|
||
|
||
# > 国内镜像失败时,自动回退到官方源,避免直接中断
|
||
if [[ "$MIRROR_PROVIDER" != "official" ]]; then
|
||
log_warn "开始自动回退到 Docker 官方源..."
|
||
MIRROR_PROVIDER="official"
|
||
|
||
configure_docker_repo
|
||
handle_gpg_key
|
||
|
||
if pkg_mgr makecache -y "${PKG_MGR_STABLE_OPTS[@]}" --disablerepo='*' --enablerepo='docker-ce-stable'; then
|
||
log_info "已成功回退到 Docker 官方源。"
|
||
return 0
|
||
fi
|
||
fi
|
||
|
||
die "docker-ce-stable 元数据刷新失败:国内镜像与官方源均不可用,请检查网络/代理/仓库地址。" "$EC_REPO"
|
||
}
|
||
|
||
# ==============================================================================
|
||
# Repo / GPG
|
||
# ==============================================================================
|
||
### 移除旧冲突包
|
||
### @param none
|
||
### @return 0 成功;非 0 失败
|
||
### @require yum
|
||
cleanup_conflicting_packages() {
|
||
local -a conflicts=(
|
||
docker
|
||
docker-common
|
||
docker-client
|
||
docker-client-latest
|
||
docker-latest
|
||
docker-latest-logrotate
|
||
docker-logrotate
|
||
docker-engine
|
||
docker-ce
|
||
docker-ce-cli
|
||
containerd.io
|
||
docker-buildx-plugin
|
||
docker-compose-plugin
|
||
)
|
||
|
||
if [[ "$REMOVE_CONFLICTS" -ne 1 ]]; then
|
||
log_info "已按参数要求跳过冲突包清理。"
|
||
return 0
|
||
fi
|
||
|
||
remove_pkgs "${conflicts[@]}" || log_warn "冲突包清理未完全成功,继续尝试。"
|
||
}
|
||
|
||
### 配置 Docker repo
|
||
### @param none
|
||
### @return 0 成功;非 0 失败
|
||
### @require none
|
||
### @side_effect 写入 /etc/yum.repos.d/docker-ce.repo
|
||
configure_docker_repo() {
|
||
local gpgcheck_value="1"
|
||
local repo_content=""
|
||
|
||
MIRROR_BASE_URL="$(get_mirror_base_url "$MIRROR_PROVIDER")" \
|
||
|| die "获取镜像 URL 失败。" "$EC_REPO"
|
||
|
||
if [[ "$GPG_MODE" == "skip" ]]; then
|
||
gpgcheck_value="0"
|
||
DOCKER_GPG_KEY_URL=""
|
||
else
|
||
if [[ "$GPG_MODE" == "mirror" ]]; then
|
||
DOCKER_GPG_KEY_URL="$(get_gpg_key_url "$MIRROR_PROVIDER")" || die "获取镜像 GPG Key URL 失败。" "$EC_REPO"
|
||
else
|
||
# strict 默认也优先用镜像站 key,失败时导入阶段再回退官方
|
||
DOCKER_GPG_KEY_URL="$(get_gpg_key_url "$MIRROR_PROVIDER")" || die "获取 GPG Key URL 失败。" "$EC_REPO"
|
||
fi
|
||
fi
|
||
|
||
if [[ -f "$DOCKER_REPO_FILE" && "$FORCE" -ne 1 ]]; then
|
||
log_warn "检测到已有 ${DOCKER_REPO_FILE},未使用 --force,跳过覆盖。"
|
||
return 0
|
||
fi
|
||
|
||
if [[ -f "$DOCKER_REPO_FILE" && "$FORCE" -eq 1 ]]; then
|
||
confirm_action "检测到已有 ${DOCKER_REPO_FILE},是否覆盖?" || die "用户取消覆盖 repo 文件。" "$EC_USAGE"
|
||
fi
|
||
|
||
repo_content="[docker-ce-stable]
|
||
name=Docker CE Stable - \$basearch
|
||
baseurl=${MIRROR_BASE_URL}/${OS_EL_VER}/\$basearch/stable
|
||
enabled=1
|
||
gpgcheck=${gpgcheck_value}
|
||
"
|
||
|
||
if [[ "$gpgcheck_value" == "1" ]]; then
|
||
repo_content="${repo_content}gpgkey=${DOCKER_GPG_KEY_URL}
|
||
"
|
||
fi
|
||
|
||
repo_content="${repo_content}retries=10
|
||
timeout=60
|
||
minrate=1k
|
||
skip_if_unavailable=0
|
||
|
||
[docker-ce-test]
|
||
name=Docker CE Test - \$basearch
|
||
baseurl=${MIRROR_BASE_URL}/${OS_EL_VER}/\$basearch/test
|
||
enabled=0
|
||
gpgcheck=${gpgcheck_value}
|
||
"
|
||
|
||
if [[ "$gpgcheck_value" == "1" ]]; then
|
||
repo_content="${repo_content}gpgkey=${DOCKER_GPG_KEY_URL}
|
||
"
|
||
fi
|
||
|
||
write_file_atomic "$DOCKER_REPO_FILE" "$repo_content"
|
||
log_info "Docker repo 已写入:${DOCKER_REPO_FILE}"
|
||
}
|
||
|
||
### 处理 GPG key 导入
|
||
### @param none
|
||
### @return 0 成功;非 0 失败
|
||
### @require curl rpm
|
||
handle_gpg_key() {
|
||
local gpg_tmp=""
|
||
local try_url=""
|
||
|
||
if [[ "$GPG_MODE" == "skip" ]]; then
|
||
log_warn "已启用 --gpg-mode skip:跳过 GPG key 导入与 repo 包签名校验。"
|
||
return 0
|
||
fi
|
||
|
||
if [[ "$DRY_RUN" -eq 1 ]]; then
|
||
log_info "DRY-RUN:跳过实际导入 GPG key。"
|
||
return 0
|
||
fi
|
||
|
||
gpg_tmp="$(mktemp "${TMP_DIR}/docker-gpg.XXXXXX")"
|
||
|
||
# > 优先使用镜像站 gpgkey;strict 模式下若失败,再回退官方 key
|
||
if curl -fsSL --connect-timeout 15 --retry 3 "$DOCKER_GPG_KEY_URL" -o "$gpg_tmp"; then
|
||
try_url="$DOCKER_GPG_KEY_URL"
|
||
elif [[ "$GPG_MODE" == "strict" ]] && curl -fsSL --connect-timeout 15 --retry 3 "$OFFICIAL_DOCKER_GPG_KEY_URL" -o "$gpg_tmp"; then
|
||
try_url="$OFFICIAL_DOCKER_GPG_KEY_URL"
|
||
log_warn "镜像站 GPG key 获取失败,已回退官方 key。"
|
||
else
|
||
rm -f -- "$gpg_tmp"
|
||
die "GPG key 下载失败;可改用 --gpg-mode skip 跳过校验。" "$EC_REPO"
|
||
fi
|
||
|
||
rpm --import "$gpg_tmp" || {
|
||
rm -f -- "$gpg_tmp"
|
||
die "导入 GPG key 失败(来源:${try_url})。可使用 --gpg-mode skip。" "$EC_REPO"
|
||
}
|
||
|
||
rm -f -- "$gpg_tmp"
|
||
log_info "GPG key 导入成功:${try_url}"
|
||
}
|
||
|
||
# ==============================================================================
|
||
# Prerequisites
|
||
# ==============================================================================
|
||
### 添加 openEuler 临时仓(优先 OS,其次 Everything)
|
||
### @param none
|
||
### @return 0 成功;非 0 失败
|
||
### @require none
|
||
### @side_effect 写入临时 repo 文件
|
||
add_openeuler_everything_repo() {
|
||
local os_baseurl=""
|
||
local everything_baseurl=""
|
||
|
||
[[ "$OS_ID" == "openeuler" ]] || return 1
|
||
[[ ! -f "$OE_PREREQ_REPO_FILE" ]] || return 0
|
||
|
||
case "${OS_VERSION_ID%%.*}" in
|
||
20)
|
||
os_baseurl="${OE_REPO_BASE}/openEuler-20.03-LTS-SP3/OS/\$basearch/"
|
||
everything_baseurl="${OE_REPO_BASE}/openEuler-20.03-LTS-SP3/everything/\$basearch/"
|
||
;;
|
||
22)
|
||
os_baseurl="${OE_REPO_BASE}/openEuler-22.03-LTS-SP3/OS/\$basearch/"
|
||
everything_baseurl="${OE_REPO_BASE}/openEuler-22.03-LTS-SP3/everything/\$basearch/"
|
||
;;
|
||
*)
|
||
return 1
|
||
;;
|
||
esac
|
||
|
||
write_file_atomic "$OE_PREREQ_REPO_FILE" "[openeuler-os-tmp]
|
||
name=openEuler OS (temporary)
|
||
baseurl=${os_baseurl}
|
||
enabled=0
|
||
gpgcheck=0
|
||
skip_if_unavailable=1
|
||
|
||
[openeuler-everything-tmp]
|
||
name=openEuler Everything (temporary)
|
||
baseurl=${everything_baseurl}
|
||
enabled=0
|
||
gpgcheck=0
|
||
skip_if_unavailable=1
|
||
"
|
||
log_info "已添加 openEuler 临时仓:${OE_PREREQ_REPO_FILE}(优先 OS,其次 Everything)"
|
||
}
|
||
|
||
### 添加 EL8 兼容仓(仅用于 openEuler 依赖兜底,优先国内镜像,默认禁用)
|
||
### @param none
|
||
### @return 0 成功;非 0 失败
|
||
### @require none
|
||
### @side_effect 写入临时 repo 文件
|
||
add_el8_compat_repo() {
|
||
local base_root=""
|
||
|
||
# > 该兼容仓是给 openEuler 补 Docker 依赖的兜底方案,不用于普通 CentOS 主流程
|
||
[[ "$OS_ID" == "openeuler" ]] || return 1
|
||
[[ "$OS_EL_VER" == "8" ]] || return 1
|
||
[[ ! -f "$EL8_COMPAT_REPO_FILE" ]] || return 0
|
||
|
||
case "$MIRROR_PROVIDER" in
|
||
aliyun)
|
||
base_root="https://mirrors.aliyun.com/centos-vault/centos/8.5.2111"
|
||
;;
|
||
tencent)
|
||
base_root="https://mirrors.tencent.com/centos-vault/8.5.2111"
|
||
;;
|
||
huawei)
|
||
base_root="https://repo.huaweicloud.com/centos-vault/8.5.2111"
|
||
;;
|
||
official|*)
|
||
base_root="https://vault.centos.org/8.5.2111"
|
||
;;
|
||
esac
|
||
|
||
write_file_atomic "$EL8_COMPAT_REPO_FILE" "[el8-compat-baseos-tmp]
|
||
name=EL8 Compatibility BaseOS (temporary for openEuler)
|
||
baseurl=${base_root}/BaseOS/\$basearch/os/
|
||
enabled=0
|
||
gpgcheck=0
|
||
skip_if_unavailable=1
|
||
|
||
[el8-compat-appstream-tmp]
|
||
name=EL8 Compatibility AppStream (temporary for openEuler)
|
||
baseurl=${base_root}/AppStream/\$basearch/os/
|
||
enabled=0
|
||
gpgcheck=0
|
||
skip_if_unavailable=1
|
||
|
||
[el8-compat-powertools-tmp]
|
||
name=EL8 Compatibility PowerTools (temporary for openEuler)
|
||
baseurl=${base_root}/PowerTools/\$basearch/os/
|
||
enabled=0
|
||
gpgcheck=0
|
||
skip_if_unavailable=1
|
||
|
||
[el8-compat-extras-tmp]
|
||
name=EL8 Compatibility Extras (temporary for openEuler)
|
||
baseurl=${base_root}/extras/\$basearch/os/
|
||
enabled=0
|
||
gpgcheck=0
|
||
skip_if_unavailable=1
|
||
"
|
||
log_info "已添加 openEuler 专用 EL8 兼容临时仓:${EL8_COMPAT_REPO_FILE}(${base_root})"
|
||
}
|
||
|
||
### 尝试安装单个依赖包(container-selinux 安装后再校验版本约束)
|
||
### @param pkg string 包名
|
||
### @param ... extra_flags string 额外 yum 参数
|
||
### @return 0 成功;1 失败
|
||
### @require yum rpm
|
||
try_install_pkg() {
|
||
local pkg="${1:?pkg required}"
|
||
shift
|
||
local -a extra_flags=( "$@" )
|
||
|
||
if ! pkg_mgr install -y "${PKG_MGR_STABLE_OPTS[@]}" "${extra_flags[@]}" "$pkg" >/dev/null 2>&1; then
|
||
return 1
|
||
fi
|
||
|
||
if [[ "$pkg" == "container-selinux" ]]; then
|
||
is_container_selinux_satisfied
|
||
return $?
|
||
fi
|
||
|
||
rpm -q "$pkg" >/dev/null 2>&1
|
||
}
|
||
|
||
### 判断 container-selinux 是否满足 Docker 所需依赖(直接使用 RPM 依赖语义)
|
||
### @param none
|
||
### @return 0 满足;1 不满足
|
||
### @require rpm
|
||
is_container_selinux_satisfied() {
|
||
# > 不再手写 epoch/version 比较,直接让 rpm 按依赖表达式判断
|
||
if rpm -q --whatprovides 'container-selinux >= 2:2.74' >/dev/null 2>&1; then
|
||
return 0
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
### 安装前置依赖(openEuler 优先 OS 仓安装 container-selinux)
|
||
### @param none
|
||
### @return 0 成功;非 0 失败
|
||
### @require yum rpm
|
||
install_prerequisites() {
|
||
local -a missing=()
|
||
local -a still_missing=()
|
||
local -a unresolved=()
|
||
local pkg=""
|
||
|
||
log_info "检查前置依赖:${PREREQ_PKGS[*]}"
|
||
|
||
for pkg in "${PREREQ_PKGS[@]}"; do
|
||
pkg="$(printf '%s' "$pkg" | tr -d '\r' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
|
||
|
||
if [[ "$pkg" == "container-selinux" ]]; then
|
||
if is_container_selinux_satisfied; then
|
||
log_info "依赖已满足:container-selinux >= 2:2.74"
|
||
else
|
||
missing+=( "container-selinux" )
|
||
log_warn "container-selinux 未安装或版本不足(需要 >= 2:2.74)"
|
||
fi
|
||
continue
|
||
fi
|
||
|
||
if rpm -q "$pkg" >/dev/null 2>&1; then
|
||
log_info "依赖已存在:${pkg}"
|
||
else
|
||
missing+=( "$pkg" )
|
||
log_warn "缺少依赖:${pkg}"
|
||
fi
|
||
done
|
||
|
||
if [[ "${#missing[@]}" -eq 0 ]]; then
|
||
log_info "前置依赖已满足。"
|
||
return 0
|
||
fi
|
||
|
||
# 1) 当前系统仓
|
||
for pkg in "${missing[@]}"; do
|
||
if try_install_pkg "$pkg"; then
|
||
log_info "${pkg}:已从当前系统仓安装/升级。"
|
||
else
|
||
still_missing+=( "$pkg" )
|
||
fi
|
||
done
|
||
|
||
if [[ "${#still_missing[@]}" -eq 0 ]]; then
|
||
return 0
|
||
fi
|
||
|
||
# 2) openEuler:优先临时仓
|
||
if [[ "$OS_ID" == "openeuler" ]]; then
|
||
add_openeuler_everything_repo || log_warn "添加 openEuler 临时仓失败。"
|
||
unresolved=()
|
||
|
||
for pkg in "${still_missing[@]}"; do
|
||
if [[ "$pkg" == "container-selinux" ]]; then
|
||
# > container-selinux 优先从 OS 仓拿,OS 仓已明确存在满足版本的包
|
||
if try_install_pkg "$pkg" "--enablerepo=openeuler-os-tmp"; then
|
||
log_info "container-selinux:已从 openEuler OS 仓安装/升级。"
|
||
elif try_install_pkg "$pkg" "--enablerepo=openeuler-everything-tmp"; then
|
||
log_info "container-selinux:已从 openEuler Everything 仓安装/升级。"
|
||
else
|
||
unresolved+=( "container-selinux" )
|
||
fi
|
||
else
|
||
if try_install_pkg "$pkg" "--enablerepo=openeuler-everything-tmp"; then
|
||
log_info "${pkg}:已从 openEuler Everything 仓安装。"
|
||
else
|
||
unresolved+=( "$pkg" )
|
||
fi
|
||
fi
|
||
done
|
||
|
||
still_missing=( "${unresolved[@]}" )
|
||
fi
|
||
|
||
if [[ "${#still_missing[@]}" -eq 0 ]]; then
|
||
return 0
|
||
fi
|
||
|
||
# 3) openEuler:再用 EL8 兼容仓兜底
|
||
if [[ "$OS_ID" == "openeuler" && "$OS_EL_VER" == "8" ]]; then
|
||
add_el8_compat_repo || log_warn "添加 EL8 兼容仓失败。"
|
||
unresolved=()
|
||
|
||
for pkg in "${still_missing[@]}"; do
|
||
if try_install_pkg "$pkg" \
|
||
"--enablerepo=el8-compat-baseos-tmp" \
|
||
"--enablerepo=el8-compat-appstream-tmp" \
|
||
"--enablerepo=el8-compat-powertools-tmp" \
|
||
"--enablerepo=el8-compat-extras-tmp"; then
|
||
log_info "${pkg}:已从 EL8 兼容仓安装。"
|
||
else
|
||
unresolved+=( "$pkg" )
|
||
fi
|
||
done
|
||
|
||
still_missing=( "${unresolved[@]}" )
|
||
fi
|
||
|
||
if [[ "${#still_missing[@]}" -gt 0 ]]; then
|
||
die "前置依赖仍缺失或版本不足:${still_missing[*]}(尤其 container-selinux 需满足 'container-selinux >= 2:2.74')" "$EC_INSTALL"
|
||
fi
|
||
}
|
||
|
||
# ==============================================================================
|
||
# Docker Version
|
||
# ==============================================================================
|
||
### 解析 Docker 可用版本
|
||
### @param none
|
||
### @return 0 成功
|
||
### @require yum awk sed grep sort tail
|
||
resolve_docker_version() {
|
||
local available=""
|
||
|
||
log_info "刷新 docker-ce-stable 元数据..."
|
||
pkg_mgr makecache -y "${PKG_MGR_STABLE_OPTS[@]}" --disablerepo='*' --enablerepo='docker-ce-stable' || log_warn "仅 docker-ce-stable 刷新失败,继续。"
|
||
|
||
available="$(
|
||
yum "${PKG_GLOBAL_OPTS[@]}" list available docker-ce --showduplicates 2>/dev/null \
|
||
| awk '/^docker-ce\./ {print $2}' \
|
||
| sed 's/^[0-9]*://' \
|
||
| grep "^${DOCKER_VERSION}" \
|
||
| sort -t'.' -k1,1n -k2,2n -k3,3n \
|
||
| tail -1
|
||
)" || true
|
||
|
||
if [[ -z "$available" ]]; then
|
||
log_warn "未解析到精确版本,将使用通配安装:${DOCKER_VERSION}*"
|
||
DOCKER_VERSION=""
|
||
else
|
||
DOCKER_VERSION="$available"
|
||
log_info "已解析 Docker 版本:${DOCKER_VERSION}"
|
||
fi
|
||
}
|
||
|
||
### 将 Docker repo 的 gpgcheck / repo_gpgcheck 临时关闭
|
||
### @param none
|
||
### @return 0 成功;非 0 失败
|
||
### @require sed grep
|
||
### @side_effect 修改 /etc/yum.repos.d/docker-ce.repo
|
||
disable_docker_repo_gpgcheck() {
|
||
[[ -f "$DOCKER_REPO_FILE" ]] || die "Docker repo 文件不存在:${DOCKER_REPO_FILE}" "$EC_REPO"
|
||
|
||
if [[ "$DRY_RUN" -eq 1 ]]; then
|
||
printf '[DRY-RUN] disable gpg checks in %s\n' "$DOCKER_REPO_FILE" >&2
|
||
return 0
|
||
fi
|
||
|
||
run_as_root sed -i \
|
||
-e 's/^gpgcheck=.*/gpgcheck=0/g' \
|
||
-e 's/^repo_gpgcheck=.*/repo_gpgcheck=0/g' \
|
||
"$DOCKER_REPO_FILE"
|
||
|
||
if ! grep -q '^repo_gpgcheck=' "$DOCKER_REPO_FILE"; then
|
||
run_as_root bash -c "printf '\nrepo_gpgcheck=0\n' >> '$DOCKER_REPO_FILE'"
|
||
fi
|
||
|
||
log_warn "已关闭 Docker repo 的 gpgcheck / repo_gpgcheck:${DOCKER_REPO_FILE}"
|
||
|
||
make_cache
|
||
}
|
||
|
||
### 执行 yum 安装并记录日志,避免管道触发 ERR trap
|
||
### @param log_file string 日志文件路径
|
||
### @param ... args string yum install 后续参数
|
||
### @return 0 成功;非 0 失败(返回 yum 的退出码)
|
||
### @require yum tee
|
||
### @side_effect 写入日志文件
|
||
run_yum_install_with_log() {
|
||
local log_file="${1:?log_file required}"
|
||
shift
|
||
local rc=0
|
||
|
||
# > 用 if 包裹失败管道,避免 set -E/ERR trap 把失败点记录成 tee
|
||
if yum "${PKG_GLOBAL_OPTS[@]}" install -y \
|
||
"${PKG_MGR_STABLE_OPTS[@]}" \
|
||
"${INSTALL_FLAGS[@]}" \
|
||
"$@" 2>&1 | tee "$log_file"; then
|
||
return 0
|
||
else
|
||
rc=${PIPESTATUS[0]:-1}
|
||
return "$rc"
|
||
fi
|
||
}
|
||
|
||
### 安装 Docker 软件包(处理未签名包 / GPG 校验失败,并避免 tee 管道触发 ERR trap)
|
||
### @param none
|
||
### @return 0 成功;非 0 失败
|
||
### @require yum grep mktemp
|
||
install_docker_packages() {
|
||
local pkg_ce=""
|
||
local pkg_cli=""
|
||
local install_log=""
|
||
local rc=0
|
||
|
||
if [[ -n "$DOCKER_VERSION" && "$DOCKER_VERSION" =~ - ]]; then
|
||
pkg_ce="docker-ce-${DOCKER_VERSION}"
|
||
pkg_cli="docker-ce-cli-${DOCKER_VERSION}"
|
||
elif [[ -n "$DOCKER_VERSION" ]]; then
|
||
pkg_ce="docker-ce-${DOCKER_VERSION}*"
|
||
pkg_cli="docker-ce-cli-${DOCKER_VERSION}*"
|
||
else
|
||
pkg_ce="docker-ce"
|
||
pkg_cli="docker-ce-cli"
|
||
fi
|
||
|
||
log_info "开始安装 Docker:${pkg_ce} ${pkg_cli}"
|
||
|
||
if [[ "$DRY_RUN" -eq 1 ]]; then
|
||
pkg_mgr install -y \
|
||
"${PKG_MGR_STABLE_OPTS[@]}" \
|
||
"${INSTALL_FLAGS[@]}" \
|
||
"$pkg_ce" \
|
||
"$pkg_cli" \
|
||
containerd.io \
|
||
docker-compose-plugin
|
||
return 0
|
||
fi
|
||
|
||
install_log="$(mktemp "${TMP_DIR}/yum-install.XXXXXX")"
|
||
|
||
if run_yum_install_with_log \
|
||
"$install_log" \
|
||
"$pkg_ce" \
|
||
"$pkg_cli" \
|
||
containerd.io \
|
||
docker-compose-plugin; then
|
||
rm -f -- "$install_log"
|
||
log_info "Docker 软件包安装成功。"
|
||
return 0
|
||
fi
|
||
|
||
rc=$?
|
||
|
||
# > 两类都要处理:
|
||
# > 1) 包本身未签名:is not signed
|
||
# > 2) 包有签名但校验失败:GPG check FAILED
|
||
if grep -Eiq 'is not signed|Package .* is not signed|GPG check FAILED|Public key for .* is not installed' "$install_log"; then
|
||
log_warn "检测到 Docker 仓包签名校验失败,准备临时关闭 Docker 仓 gpgcheck 后重试。"
|
||
|
||
disable_docker_repo_gpgcheck
|
||
|
||
if run_yum_install_with_log \
|
||
"$install_log" \
|
||
"$pkg_ce" \
|
||
"$pkg_cli" \
|
||
containerd.io \
|
||
docker-compose-plugin; then
|
||
rm -f -- "$install_log"
|
||
log_warn "已通过关闭 Docker 仓 gpgcheck 成功完成安装。"
|
||
return 0
|
||
fi
|
||
|
||
rc=$?
|
||
rm -f -- "$install_log"
|
||
die "关闭 gpgcheck 后重试仍失败,退出码:${rc}" "$EC_INSTALL"
|
||
fi
|
||
|
||
rm -f -- "$install_log"
|
||
die "Docker 安装失败,且并非签名校验类错误,退出码:${rc}" "$EC_INSTALL"
|
||
}
|
||
|
||
|
||
# ==============================================================================
|
||
# Docker Config / Service
|
||
# ==============================================================================
|
||
### 配置 Docker daemon.json
|
||
### @param none
|
||
### @return 0 成功;非 0 失败
|
||
### @require none
|
||
### @side_effect 写入 /etc/docker/daemon.json
|
||
configure_docker_daemon() {
|
||
local content=""
|
||
|
||
if [[ "$CONFIGURE_DAEMON" -ne 1 ]]; then
|
||
log_info "已按参数要求跳过 daemon.json 配置。"
|
||
return 0
|
||
fi
|
||
|
||
content='{
|
||
"registry-mirrors": [
|
||
"https://docker.xuanyuan.me",
|
||
"https://docker.1ms.run",
|
||
"https://docker.m.daocloud.io"
|
||
],
|
||
"log-driver": "json-file",
|
||
"log-opts": {
|
||
"max-size": "300m",
|
||
"max-file": "3"
|
||
},
|
||
"storage-driver": "overlay2",
|
||
"live-restore": true
|
||
}
|
||
'
|
||
|
||
if [[ -f "$DOCKER_DAEMON_CFG" && "$FORCE" -ne 1 ]]; then
|
||
log_warn "检测到已有 ${DOCKER_DAEMON_CFG},未使用 --force,跳过覆盖。"
|
||
return 0
|
||
fi
|
||
|
||
if [[ -f "$DOCKER_DAEMON_CFG" && "$FORCE" -eq 1 ]]; then
|
||
confirm_action "检测到已有 ${DOCKER_DAEMON_CFG},是否覆盖?" || die "用户取消覆盖 daemon.json。" "$EC_USAGE"
|
||
fi
|
||
|
||
write_file_atomic "$DOCKER_DAEMON_CFG" "$content"
|
||
log_info "已写入 Docker daemon 配置:${DOCKER_DAEMON_CFG}"
|
||
}
|
||
|
||
### 启用并启动 Docker
|
||
### @param none
|
||
### @return 0 成功;非 0 失败
|
||
### @require systemctl
|
||
enable_and_start_docker() {
|
||
run_as_root systemctl daemon-reload
|
||
|
||
if [[ "$SKIP_ENABLE" -ne 1 ]]; then
|
||
run_as_root systemctl enable docker || die "设置 Docker 开机自启失败。" "$EC_SERVICE"
|
||
else
|
||
log_info "已跳过开机自启。"
|
||
fi
|
||
|
||
if [[ "$SKIP_START" -ne 1 ]]; then
|
||
run_as_root systemctl start docker || die "启动 Docker 失败。" "$EC_SERVICE"
|
||
else
|
||
log_info "已跳过启动 Docker。"
|
||
fi
|
||
}
|
||
|
||
### 验证安装结果
|
||
### @param none
|
||
### @return 0 成功;非 0 失败
|
||
### @require docker
|
||
verify_installation() {
|
||
if [[ "$SKIP_START" -eq 1 ]]; then
|
||
log_warn "已跳过启动,因此不做运行态校验。"
|
||
return 0
|
||
fi
|
||
|
||
require_cmd docker || die "未检测到 docker 命令。" "$EC_VERIFY"
|
||
run_cmd docker --version || die "docker --version 校验失败。" "$EC_VERIFY"
|
||
run_cmd docker info >/dev/null 2>&1 || die "docker info 校验失败。" "$EC_VERIFY"
|
||
|
||
if run_cmd docker compose version >/dev/null 2>&1; then
|
||
log_info "docker compose 插件可用。"
|
||
else
|
||
log_warn "docker compose 插件校验失败,但主安装已完成。"
|
||
fi
|
||
}
|
||
|
||
### 打印摘要
|
||
### @param none
|
||
### @return 0 成功
|
||
### @require none
|
||
print_summary() {
|
||
log_info "安装摘要:"
|
||
log_info " 系统 : ${OS_PRETTY_NAME}"
|
||
log_info " OS ID : ${OS_ID_RAW} -> ${OS_ID}"
|
||
log_info " 架构 : ${OS_ARCH}"
|
||
log_info " EL 映射版本 : el${OS_EL_VER}"
|
||
log_info " 包管理器 : ${PKG_MGR}"
|
||
log_info " 镜像提供方 : ${MIRROR_PROVIDER}"
|
||
log_info " Docker 仓地址 : ${MIRROR_BASE_URL}"
|
||
log_info " GPG 模式 : ${GPG_MODE}"
|
||
log_info " 代理 : ${PROXY_URL:-<none>}"
|
||
log_info " container-selinux : $(rpm -q container-selinux 2>/dev/null || echo not-installed)"
|
||
log_info " fuse-overlayfs : $(rpm -q fuse-overlayfs 2>/dev/null || echo not-installed)"
|
||
log_info " slirp4netns : $(rpm -q slirp4netns 2>/dev/null || echo not-installed)"
|
||
}
|
||
|
||
# ==============================================================================
|
||
# Main
|
||
# ==============================================================================
|
||
### 主流程
|
||
### @param ... args 参数列表
|
||
### @return 0 成功;非 0 失败
|
||
### @require none
|
||
main() {
|
||
parse_args "$@"
|
||
validate_args
|
||
init_runtime
|
||
check_requirements
|
||
prepare_proxy_env
|
||
|
||
log_info "开始执行 Docker 在线安装脚本 v${SCRIPT_VERSION}"
|
||
|
||
cleanup_conflicting_packages
|
||
configure_docker_repo
|
||
handle_gpg_key
|
||
make_cache
|
||
install_prerequisites
|
||
resolve_docker_version
|
||
install_docker_packages
|
||
configure_docker_daemon
|
||
enable_and_start_docker
|
||
verify_installation
|
||
print_summary
|
||
|
||
exit "$EC_OK"
|
||
}
|
||
|
||
main "$@"
|
||
|
||
# ==============================================================================
|
||
# Minimal Self-Test
|
||
# ==============================================================================
|
||
# 1) 正常路径:
|
||
# sudo bash docker-online-install.sh --mirror aliyun --yes
|
||
#
|
||
# 2) 参数错误:
|
||
# bash docker-online-install.sh --gpg-mode invalid
|
||
#
|
||
# 3) dry-run:
|
||
# sudo bash docker-online-install.sh --mirror huawei --proxy http://192.168.1.10:3128 --dry-run
|
||
# ============================================================================== |