287 lines
11 KiB
Bash
287 lines
11 KiB
Bash
#!/usr/bin/env zsh
|
||
|
||
# Docker prerequisites for planet.sh; lifecycle and logging remain in planet.sh.
|
||
DOCKER_MIN_BUILDX_VERSION="0.17.0"
|
||
DOCKER_COMMAND_RUNNER="${${(%):-%N}:A:h:h}/docker_command.py"
|
||
|
||
docker_probe() {
|
||
local log_file="$(mktemp "$PLANET_STATE_DIR/docker-probe.XXXXXX.log")"
|
||
local label="检查 Docker"
|
||
case "$1 $2" in
|
||
'docker compose') label="检查 Docker Compose" ;;
|
||
'docker info') label="检查 Docker 服务" ;;
|
||
'docker buildx') label="检查 Docker Buildx" ;;
|
||
'docker context') label="检查 Docker context" ;;
|
||
'docker inspect') label="检查容器 ${@[-1]}" ;;
|
||
'docker start'|'docker restart') label="启动容器 ${@[-1]}" ;;
|
||
esac
|
||
run_docker_observed "$label" "${PLANET_DOCKER_PROBE_TIMEOUT:-15}" "$log_file" "$@"
|
||
}
|
||
|
||
run_docker_observed() {
|
||
setopt localtraps
|
||
local label="$1" deadline="$2" log_file="$3"
|
||
shift 3
|
||
local detail_file="${log_file}.status" command_status=0
|
||
LAST_COMMAND_ERROR=""
|
||
python3 "$DOCKER_COMMAND_RUNNER" --label "$label" --timeout "$deadline" \
|
||
--log "$log_file" --status-file "$detail_file" -- "$@" >/dev/null 2>&1 &
|
||
local command_pid=$!
|
||
trap 'kill -TERM "$command_pid" 2>/dev/null || true; wait "$command_pid" 2>/dev/null || true; return 130' INT
|
||
trap 'kill -TERM "$command_pid" 2>/dev/null || true; wait "$command_pid" 2>/dev/null || true; return 143' TERM
|
||
while kill -0 "$command_pid" 2>/dev/null; do
|
||
if (( $+functions[set_wait_detail] )); then
|
||
local detail="$label"
|
||
[ ! -f "$detail_file" ] || detail="$(<"$detail_file")"
|
||
set_wait_detail "$detail"
|
||
fi
|
||
sleep 0.125
|
||
done
|
||
wait "$command_pid" || command_status=$?
|
||
if [ "$command_status" -ne 0 ]; then
|
||
LAST_COMMAND_ERROR="$label"
|
||
[ ! -s "$detail_file" ] || LAST_COMMAND_ERROR="$(<"$detail_file")"
|
||
fi
|
||
rm -f "$detail_file"
|
||
if (( $+functions[set_wait_detail] )); then
|
||
set_wait_detail "${LAST_COMMAND_ERROR:-}"
|
||
fi
|
||
cat "$log_file"
|
||
return "$command_status"
|
||
}
|
||
|
||
compose_available() {
|
||
docker_probe docker compose version >/dev/null
|
||
}
|
||
|
||
compose_v1_available() {
|
||
command -v docker-compose >/dev/null 2>&1
|
||
}
|
||
|
||
buildx_version() {
|
||
docker_probe docker buildx version | awk '{print $2}' | sed 's/^v//'
|
||
}
|
||
|
||
buildx_meets_minimum() {
|
||
local current_version="$1"
|
||
local required_version="$2"
|
||
|
||
[ -n "$current_version" ] || return 1
|
||
[ -n "$required_version" ] || return 1
|
||
|
||
if [ "$(printf '%s\n%s\n' "$required_version" "$current_version" | sort -V | head -n 1)" = "$required_version" ]; then
|
||
return 0
|
||
fi
|
||
|
||
return 1
|
||
}
|
||
|
||
docker_daemon_available() {
|
||
command -v docker >/dev/null 2>&1 && docker_probe docker info --format '{{.ServerVersion}}' >/dev/null
|
||
}
|
||
|
||
docker_uses_local_engine() {
|
||
local endpoint="${DOCKER_HOST:-}"
|
||
if [ -n "${DOCKER_CONTEXT:-}" ] || [ -z "$endpoint" ]; then
|
||
if command -v docker >/dev/null 2>&1; then
|
||
endpoint="$(docker_probe docker context inspect --format '{{.Endpoints.docker.Host}}')" || return 1
|
||
elif [ -n "${DOCKER_CONTEXT:-}" ]; then
|
||
return 1
|
||
else
|
||
endpoint="unix:///var/run/docker.sock"
|
||
fi
|
||
fi
|
||
case "$endpoint" in
|
||
unix:///var/run/docker.sock|unix:///run/docker.sock) return 0 ;;
|
||
*) return 1 ;;
|
||
esac
|
||
}
|
||
|
||
docker_desktop_present() {
|
||
local docker_path=""
|
||
[ -d /mnt/wsl/docker-desktop ] && return 0
|
||
docker_path="$(command -v docker 2>/dev/null || true)"
|
||
[ -n "$docker_path" ] || return 1
|
||
docker_path="$(readlink -f "$docker_path" 2>/dev/null || true)"
|
||
[[ "$docker_path" == */docker-desktop/* ]]
|
||
}
|
||
|
||
docker_systemd_unit_exists() {
|
||
command -v systemctl >/dev/null 2>&1 || return 1
|
||
[ "$(systemctl show --property=LoadState --value "$1" 2>/dev/null)" = "loaded" ]
|
||
}
|
||
|
||
docker_socket_needs_group_access() {
|
||
[ -S /var/run/docker.sock ] && { [ ! -r /var/run/docker.sock ] || [ ! -w /var/run/docker.sock ]; }
|
||
}
|
||
|
||
docker_require_sudo() {
|
||
[ "$(id -u)" -eq 0 ] && return 0
|
||
if ! command -v sudo >/dev/null 2>&1; then
|
||
log_error "缺少 sudo,无法安装或配置 Docker;请先由管理员安装 sudo 并授予当前用户权限。"
|
||
return 1
|
||
fi
|
||
# Authenticate in the foreground, before command output is redirected to a log.
|
||
sudo -v
|
||
}
|
||
|
||
docker_as_root() {
|
||
if [ "$(id -u)" -eq 0 ]; then
|
||
"$@"
|
||
else
|
||
sudo -- "$@"
|
||
fi
|
||
}
|
||
|
||
refresh_docker_group() {
|
||
[ "$(id -u)" -ne 0 ] || return 0
|
||
command -v docker >/dev/null 2>&1 || return 0
|
||
docker_uses_local_engine || return 0
|
||
docker_socket_needs_group_access || return 0
|
||
|
||
local username="$(id -un)"
|
||
local groups="$(id -nG "$username")"
|
||
[[ " $groups " == *" docker "* ]] || return 0
|
||
if [ "${PLANET_DOCKER_GROUP_REEXEC:-0}" = "1" ]; then
|
||
log_error "刷新组权限后仍无法访问 Docker socket,请检查 socket 的属组和权限。"
|
||
return 1
|
||
fi
|
||
docker_require_sudo || return 1
|
||
log_note "刷新 Docker 组权限,以当前用户继续执行。"
|
||
# Preserve WSL interop/tool paths even when sudo applies secure_path.
|
||
# argv stays an array: repository paths and arguments are never evaluated as shell text.
|
||
exec sudo -E -u "$username" -g docker -- env PLANET_DOCKER_GROUP_REEXEC=1 "PATH=$PATH" \
|
||
"$(command -v zsh)" "$SCRIPT_DIR/planet.sh" "$@"
|
||
}
|
||
|
||
docker_bootstrap_os_id() {
|
||
[ -r /etc/os-release ] || return 1
|
||
(. /etc/os-release; printf '%s\n' "$ID")
|
||
}
|
||
|
||
install_docker_prerequisites() {
|
||
local log_file="$PLANET_STATE_DIR/docker_install.log"
|
||
local engine_package="docker.io"
|
||
local compose_package="docker-compose-v2"
|
||
local buildx_package="docker-buildx"
|
||
local -a packages=()
|
||
|
||
if [ "$(docker_bootstrap_os_id)" != "ubuntu" ] || ! command -v apt-get >/dev/null 2>&1; then
|
||
log_error "Docker 自动安装目前支持 Ubuntu / Ubuntu WSL;请先安装本系统的 Docker Engine、Compose v2 和 Buildx。"
|
||
return 1
|
||
fi
|
||
if command -v dpkg-query >/dev/null 2>&1 &&
|
||
[ "$(dpkg-query -W -f='${Status}' docker-ce-cli 2>/dev/null)" = "install ok installed" ]; then
|
||
engine_package="docker-ce"
|
||
compose_package="docker-compose-plugin"
|
||
buildx_package="docker-buildx-plugin"
|
||
fi
|
||
if ! command -v docker >/dev/null 2>&1 ||
|
||
{ ! docker_daemon_available && ! command -v dockerd >/dev/null 2>&1; }; then
|
||
packages+=("$engine_package")
|
||
fi
|
||
compose_available || packages+=("$compose_package")
|
||
buildx_meets_minimum "$(buildx_version)" "$DOCKER_MIN_BUILDX_VERSION" || packages+=("$buildx_package")
|
||
[ "${#packages[@]}" -gt 0 ] || return 0
|
||
|
||
docker_require_sudo || return 1
|
||
log_step "安装 Docker 依赖: ${packages[*]}"
|
||
if ! run_command_quiet_unless_verbose "$log_file" docker_as_root apt-get update --error-on=any ||
|
||
! run_command_quiet_unless_verbose "$log_file" docker_as_root apt-get install -y "${packages[@]}"; then
|
||
log_error "Docker 依赖安装失败,请检查软件源、网络和 apt 错误。"
|
||
tail -20 "$log_file" 2>/dev/null || true
|
||
return 1
|
||
fi
|
||
rehash
|
||
}
|
||
|
||
start_local_docker_engine() {
|
||
local log_file="$PLANET_STATE_DIR/docker_service.log"
|
||
if ! docker_systemd_unit_exists docker.service; then
|
||
log_error "没有可用的 docker.service;请确认 Docker Engine 已安装且 systemd 正常运行。"
|
||
log_note "Ubuntu WSL 可在 /etc/wsl.conf 的 [boot] 下启用 systemd=true,再重启 WSL。"
|
||
return 1
|
||
fi
|
||
docker_require_sudo || return 1
|
||
if ! run_command_quiet_unless_verbose "$log_file" docker_as_root systemctl enable --now docker.service; then
|
||
log_error "Docker Engine 启动失败。"
|
||
tail -20 "$log_file" 2>/dev/null || true
|
||
log_note "查看原因: journalctl -u docker.service -n 50 --no-pager"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
ensure_docker_user_access() {
|
||
[ "$(id -u)" -ne 0 ] || return 0
|
||
docker_socket_needs_group_access || return 0
|
||
docker_require_sudo || return 1
|
||
ensure_system_command usermod passwd "usermod" || return 1
|
||
log_note "将当前用户加入 docker 组(该组可管理本机 Docker)。"
|
||
docker_as_root usermod -aG docker "$(id -un)" || return 1
|
||
refresh_docker_group "$@"
|
||
}
|
||
|
||
ensure_docker_runtime() {
|
||
if docker_daemon_available && compose_available &&
|
||
buildx_meets_minimum "$(buildx_version)" "$DOCKER_MIN_BUILDX_VERSION"; then
|
||
return 0
|
||
fi
|
||
if docker_desktop_present; then
|
||
log_error "检测到 Docker Desktop,但当前 WSL 的 Docker / Compose / Buildx 尚不可用。"
|
||
log_note "请启动 Docker Desktop,并为当前发行版启用 WSL Integration。"
|
||
return 1
|
||
fi
|
||
if ! docker_uses_local_engine; then
|
||
log_error "当前 Docker 使用其他 context 或远程/rootless endpoint,请先检查该环境的连接和插件。"
|
||
return 1
|
||
fi
|
||
install_docker_prerequisites || return 1
|
||
if ! command -v docker >/dev/null 2>&1 || ! compose_available ||
|
||
! buildx_meets_minimum "$(buildx_version)" "$DOCKER_MIN_BUILDX_VERSION"; then
|
||
log_error "安装后 Docker CLI、Compose v2 或 Buildx >= ${DOCKER_MIN_BUILDX_VERSION} 仍不可用。"
|
||
return 1
|
||
fi
|
||
if ! docker_daemon_available; then
|
||
start_local_docker_engine || return 1
|
||
ensure_docker_user_access "$@" || return 1
|
||
fi
|
||
if ! docker_daemon_available; then
|
||
log_error "Docker 已安装,但当前用户仍无法连接 daemon。"
|
||
log_docker_daemon_diagnostics
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
log_docker_daemon_diagnostics() {
|
||
if ! command -v docker >/dev/null 2>&1; then
|
||
log_note "未找到 Docker CLI,尚未安装 Docker;Ubuntu / WSL 请先执行 ./planet.sh init。"
|
||
return 0
|
||
fi
|
||
if docker_desktop_present; then
|
||
log_note "请启动 Docker Desktop,并确认当前 WSL 已启用 WSL Integration。"
|
||
return 0
|
||
fi
|
||
if ! docker_uses_local_engine; then
|
||
log_note "当前使用其他 Docker endpoint;请检查 docker context ls 和 docker info。"
|
||
return 0
|
||
fi
|
||
if docker_socket_needs_group_access; then
|
||
log_note "Docker socket 存在,但当前用户没有读写权限;请执行 ./planet.sh init 配置 docker 组。"
|
||
return 0
|
||
fi
|
||
if ! docker_systemd_unit_exists docker.service; then
|
||
log_note "未检测到可用的 docker.service;Ubuntu / WSL 请执行 ./planet.sh init 检查安装。"
|
||
return 0
|
||
fi
|
||
if [ "$(systemctl is-active docker.service 2>/dev/null)" = "active" ]; then
|
||
log_note "Docker 服务已启动,但 CLI 无法连接;请检查 docker context ls 和 docker info。"
|
||
return 0
|
||
fi
|
||
if docker_systemd_unit_exists docker.socket; then
|
||
log_note "可先执行: sudo systemctl enable --now docker.socket && sudo systemctl restart docker.service"
|
||
else
|
||
log_note "可先执行: sudo systemctl enable --now docker.service"
|
||
fi
|
||
log_note "如仍失败,查看: journalctl -u docker.service -n 50 --no-pager"
|
||
}
|