Files
CmiiDeploy/998-常用脚本/a-部署脚本/c-联网-docker安装-centos-失败.sh
2026-05-19 14:28:56 +08:00

1453 lines
41 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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")"
# > 优先使用镜像站 gpgkeystrict 模式下若失败,再回退官方 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
# ==============================================================================