- 添加 Dockerfile 与多套 docker-compose 配置(开发/生产环境) - 集成 Laravel Octane (Swoole) 提升性能 - 新增健康检查、监控脚本及部署文档 - 新增 Docker 镜像离线导入包(MySQL/Redis/Meilisearch) - 优化文档转换、预览服务及队列任务 - 添加 CreateAdminUser 命令与路由健康检查接口 - 新增 Swoole 队列兼容性测试套件 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
496 lines
13 KiB
Bash
Executable File
496 lines
13 KiB
Bash
Executable File
#!/bin/bash
|
||
|
||
# Docker镜像导入和验证脚本
|
||
# 用于导入Docker镜像并验证其完整性和兼容性
|
||
|
||
set -e
|
||
|
||
# 脚本配置
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
DEFAULT_IMAGES_DIR="$(dirname "$SCRIPT_DIR")/docker-images"
|
||
LOG_FILE="${DEFAULT_IMAGES_DIR}/import-verify.log"
|
||
|
||
# 颜色输出
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[1;33m'
|
||
BLUE='\033[0;34m'
|
||
NC='\033[0m' # No Color
|
||
|
||
# 日志函数
|
||
log() {
|
||
echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1" | tee -a "$LOG_FILE"
|
||
}
|
||
|
||
log_success() {
|
||
echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')] ✓${NC} $1" | tee -a "$LOG_FILE"
|
||
}
|
||
|
||
log_warning() {
|
||
echo -e "${YELLOW}[$(date +'%Y-%m-%d %H:%M:%S')] ⚠${NC} $1" | tee -a "$LOG_FILE"
|
||
}
|
||
|
||
log_error() {
|
||
echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] ✗${NC} $1" | tee -a "$LOG_FILE"
|
||
}
|
||
|
||
# 显示帮助信息
|
||
show_help() {
|
||
cat << EOF
|
||
Docker镜像导入和验证脚本
|
||
|
||
用法: $0 [选项] [镜像目录]
|
||
|
||
选项:
|
||
-h, --help 显示此帮助信息
|
||
-v, --verify-only 仅验证,不导入
|
||
-f, --force 强制导入,覆盖现有镜像
|
||
-c, --check-manifest 检查清单文件
|
||
--skip-compatibility 跳过兼容性检查
|
||
--parallel N 并行导入数量 (默认: 2)
|
||
--test-run 导入后运行测试容器
|
||
|
||
参数:
|
||
镜像目录 包含Docker镜像文件的目录 (默认: ./docker-images)
|
||
|
||
示例:
|
||
$0 # 导入默认目录中的所有镜像
|
||
$0 -v /path/to/images # 仅验证镜像文件
|
||
$0 -f --test-run # 强制导入并测试
|
||
$0 --check-manifest # 检查清单文件完整性
|
||
|
||
EOF
|
||
}
|
||
|
||
# 默认配置
|
||
VERIFY_ONLY=false
|
||
FORCE_IMPORT=false
|
||
CHECK_MANIFEST=false
|
||
SKIP_COMPATIBILITY=false
|
||
PARALLEL_JOBS=2
|
||
TEST_RUN=false
|
||
IMAGES_DIR="$DEFAULT_IMAGES_DIR"
|
||
|
||
# 解析命令行参数
|
||
while [[ $# -gt 0 ]]; do
|
||
case $1 in
|
||
-h|--help)
|
||
show_help
|
||
exit 0
|
||
;;
|
||
-v|--verify-only)
|
||
VERIFY_ONLY=true
|
||
shift
|
||
;;
|
||
-f|--force)
|
||
FORCE_IMPORT=true
|
||
shift
|
||
;;
|
||
-c|--check-manifest)
|
||
CHECK_MANIFEST=true
|
||
shift
|
||
;;
|
||
--skip-compatibility)
|
||
SKIP_COMPATIBILITY=true
|
||
shift
|
||
;;
|
||
--parallel)
|
||
PARALLEL_JOBS="$2"
|
||
if [[ ! "$PARALLEL_JOBS" =~ ^[1-9][0-9]*$ ]]; then
|
||
log_error "并行任务数必须是正整数"
|
||
exit 1
|
||
fi
|
||
shift 2
|
||
;;
|
||
--test-run)
|
||
TEST_RUN=true
|
||
shift
|
||
;;
|
||
-*)
|
||
log_error "未知参数: $1"
|
||
show_help
|
||
exit 1
|
||
;;
|
||
*)
|
||
IMAGES_DIR="$1"
|
||
shift
|
||
;;
|
||
esac
|
||
done
|
||
|
||
# 检查输入目录
|
||
if [[ ! -d "$IMAGES_DIR" ]]; then
|
||
log_error "镜像目录不存在: $IMAGES_DIR"
|
||
exit 1
|
||
fi
|
||
|
||
# 创建日志目录
|
||
mkdir -p "$(dirname "$LOG_FILE")"
|
||
|
||
log "开始Docker镜像导入和验证..."
|
||
log "镜像目录: $IMAGES_DIR"
|
||
log "仅验证: $VERIFY_ONLY"
|
||
log "强制导入: $FORCE_IMPORT"
|
||
log "并行任务: $PARALLEL_JOBS"
|
||
|
||
# 检查Docker是否运行
|
||
check_docker() {
|
||
log "检查Docker环境..."
|
||
|
||
if ! command -v docker >/dev/null 2>&1; then
|
||
log_error "Docker未安装"
|
||
exit 1
|
||
fi
|
||
|
||
if ! docker info >/dev/null 2>&1; then
|
||
log_error "Docker未运行或无法访问"
|
||
exit 1
|
||
fi
|
||
|
||
local docker_version=$(docker --version)
|
||
log_success "Docker环境正常: $docker_version"
|
||
|
||
# 检查系统架构
|
||
local system_arch=$(uname -m)
|
||
log "系统架构: $system_arch"
|
||
|
||
if [[ "$system_arch" != "x86_64" ]] && [[ "$SKIP_COMPATIBILITY" == false ]]; then
|
||
log_warning "系统架构不是x86_64,可能存在兼容性问题"
|
||
fi
|
||
}
|
||
|
||
# 检查清单文件
|
||
check_manifest_file() {
|
||
if [[ "$CHECK_MANIFEST" == false ]]; then
|
||
return 0
|
||
fi
|
||
|
||
local manifest_file="${IMAGES_DIR}/images-manifest.txt"
|
||
|
||
log "检查清单文件..."
|
||
|
||
if [[ ! -f "$manifest_file" ]]; then
|
||
log_warning "清单文件不存在: $manifest_file"
|
||
return 1
|
||
fi
|
||
|
||
log "验证清单文件中的镜像..."
|
||
|
||
# 解析清单文件
|
||
local current_file=""
|
||
local expected_checksum=""
|
||
local verification_failed=0
|
||
|
||
while IFS= read -r line; do
|
||
if [[ "$line" =~ ^文件:\ (.+)$ ]]; then
|
||
current_file="${BASH_REMATCH[1]}"
|
||
elif [[ "$line" =~ ^SHA256:\ (.+)$ ]]; then
|
||
expected_checksum="${BASH_REMATCH[1]}"
|
||
|
||
if [[ -n "$current_file" ]] && [[ -n "$expected_checksum" ]]; then
|
||
local file_path="${IMAGES_DIR}/${current_file}"
|
||
|
||
if [[ -f "$file_path" ]]; then
|
||
log "验证文件: $current_file"
|
||
local actual_checksum=$(sha256sum "$file_path" | cut -d' ' -f1)
|
||
|
||
if [[ "$actual_checksum" == "$expected_checksum" ]]; then
|
||
log_success "校验和匹配: $current_file"
|
||
else
|
||
log_error "校验和不匹配: $current_file"
|
||
log_error "期望: $expected_checksum"
|
||
log_error "实际: $actual_checksum"
|
||
((verification_failed++))
|
||
fi
|
||
else
|
||
log_error "文件不存在: $current_file"
|
||
((verification_failed++))
|
||
fi
|
||
|
||
current_file=""
|
||
expected_checksum=""
|
||
fi
|
||
fi
|
||
done < "$manifest_file"
|
||
|
||
if [[ $verification_failed -eq 0 ]]; then
|
||
log_success "清单文件验证通过"
|
||
return 0
|
||
else
|
||
log_error "清单文件验证失败,$verification_failed 个文件有问题"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# 验证镜像文件
|
||
verify_image_file() {
|
||
local file="$1"
|
||
local filename=$(basename "$file")
|
||
|
||
log "验证镜像文件: $filename"
|
||
|
||
# 检查文件是否存在
|
||
if [[ ! -f "$file" ]]; then
|
||
log_error "文件不存在: $filename"
|
||
return 1
|
||
fi
|
||
|
||
# 检查文件大小
|
||
local file_size=$(du -h "$file" | cut -f1)
|
||
log "文件大小: $file_size"
|
||
|
||
# 验证文件完整性
|
||
if [[ "$file" == *.tar.gz ]]; then
|
||
# 验证gzip文件
|
||
if gzip -t "$file" 2>/dev/null; then
|
||
log_success "压缩文件完整性验证通过: $filename"
|
||
else
|
||
log_error "压缩文件完整性验证失败: $filename"
|
||
return 1
|
||
fi
|
||
elif [[ "$file" == *.tar ]]; then
|
||
# 验证tar文件
|
||
if tar -tf "$file" >/dev/null 2>&1; then
|
||
log_success "tar文件完整性验证通过: $filename"
|
||
else
|
||
log_error "tar文件完整性验证失败: $filename"
|
||
return 1
|
||
fi
|
||
else
|
||
log_warning "未知文件类型,跳过验证: $filename"
|
||
return 1
|
||
fi
|
||
|
||
return 0
|
||
}
|
||
|
||
# 导入镜像文件
|
||
import_image_file() {
|
||
local file="$1"
|
||
local filename=$(basename "$file")
|
||
|
||
log "导入镜像文件: $filename"
|
||
|
||
# 首先验证文件
|
||
if ! verify_image_file "$file"; then
|
||
log_error "文件验证失败,跳过导入: $filename"
|
||
return 1
|
||
fi
|
||
|
||
# 检查是否需要强制导入
|
||
local import_args=""
|
||
if [[ "$FORCE_IMPORT" == true ]]; then
|
||
import_args="--quiet"
|
||
fi
|
||
|
||
# 导入镜像
|
||
local import_output
|
||
if [[ "$file" == *.tar.gz ]]; then
|
||
# 解压并导入
|
||
import_output=$(gunzip -c "$file" | docker load 2>&1)
|
||
else
|
||
# 直接导入
|
||
import_output=$(docker load -i "$file" 2>&1)
|
||
fi
|
||
|
||
if [[ $? -eq 0 ]]; then
|
||
log_success "镜像导入成功: $filename"
|
||
|
||
# 提取导入的镜像名称
|
||
local imported_image=$(echo "$import_output" | grep "Loaded image" | sed 's/Loaded image: //')
|
||
if [[ -n "$imported_image" ]]; then
|
||
log "导入的镜像: $imported_image"
|
||
|
||
# 验证镜像架构
|
||
if [[ "$SKIP_COMPATIBILITY" == false ]]; then
|
||
verify_image_architecture "$imported_image"
|
||
fi
|
||
fi
|
||
|
||
return 0
|
||
else
|
||
log_error "镜像导入失败: $filename"
|
||
log_error "错误信息: $import_output"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# 验证镜像架构
|
||
verify_image_architecture() {
|
||
local image="$1"
|
||
|
||
log "验证镜像架构: $image"
|
||
|
||
# 获取镜像信息
|
||
local image_info=$(docker image inspect "$image" 2>/dev/null)
|
||
|
||
if [[ $? -ne 0 ]]; then
|
||
log_error "无法获取镜像信息: $image"
|
||
return 1
|
||
fi
|
||
|
||
# 提取架构信息
|
||
local architecture=$(echo "$image_info" | grep -o '"Architecture":"[^"]*"' | cut -d'"' -f4)
|
||
local os=$(echo "$image_info" | grep -o '"Os":"[^"]*"' | cut -d'"' -f4)
|
||
|
||
log "镜像架构: $os/$architecture"
|
||
|
||
# 检查架构兼容性
|
||
local system_arch=$(uname -m)
|
||
local expected_arch="amd64"
|
||
|
||
if [[ "$system_arch" == "x86_64" ]]; then
|
||
expected_arch="amd64"
|
||
elif [[ "$system_arch" == "aarch64" ]]; then
|
||
expected_arch="arm64"
|
||
fi
|
||
|
||
if [[ "$architecture" == "$expected_arch" ]]; then
|
||
log_success "镜像架构兼容: $architecture"
|
||
return 0
|
||
else
|
||
log_warning "镜像架构可能不兼容: $architecture (系统: $system_arch)"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# 测试镜像运行
|
||
test_image_run() {
|
||
local image="$1"
|
||
|
||
log "测试镜像运行: $image"
|
||
|
||
# 根据镜像类型选择测试命令
|
||
local test_command=""
|
||
local container_name="test-$(echo "$image" | sed 's/[\/:]/_/g')-$$"
|
||
|
||
case "$image" in
|
||
*mysql*)
|
||
test_command="docker run --rm --name $container_name -e MYSQL_ROOT_PASSWORD=test -d $image"
|
||
;;
|
||
*redis*)
|
||
test_command="docker run --rm --name $container_name -d $image"
|
||
;;
|
||
*meilisearch*)
|
||
test_command="docker run --rm --name $container_name -e MEILI_MASTER_KEY=test -d $image"
|
||
;;
|
||
*knowledge-base-app*)
|
||
# 应用镜像需要更复杂的测试
|
||
log_warning "应用镜像测试需要完整环境,跳过单独测试"
|
||
return 0
|
||
;;
|
||
*)
|
||
log_warning "未知镜像类型,跳过运行测试: $image"
|
||
return 0
|
||
;;
|
||
esac
|
||
|
||
# 运行测试容器
|
||
if eval "$test_command"; then
|
||
log "测试容器启动成功: $container_name"
|
||
|
||
# 等待容器启动
|
||
sleep 5
|
||
|
||
# 检查容器状态
|
||
if docker ps | grep -q "$container_name"; then
|
||
log_success "镜像运行测试通过: $image"
|
||
|
||
# 停止测试容器
|
||
docker stop "$container_name" >/dev/null 2>&1 || true
|
||
return 0
|
||
else
|
||
log_error "测试容器启动失败: $image"
|
||
|
||
# 显示容器日志
|
||
docker logs "$container_name" 2>/dev/null || true
|
||
docker rm "$container_name" >/dev/null 2>&1 || true
|
||
return 1
|
||
fi
|
||
else
|
||
log_error "无法启动测试容器: $image"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# 处理单个镜像文件
|
||
process_image_file() {
|
||
local file="$1"
|
||
local filename=$(basename "$file")
|
||
|
||
log "处理镜像文件: $filename"
|
||
|
||
if [[ "$VERIFY_ONLY" == true ]]; then
|
||
verify_image_file "$file"
|
||
return $?
|
||
else
|
||
if import_image_file "$file"; then
|
||
# 如果需要测试运行
|
||
if [[ "$TEST_RUN" == true ]]; then
|
||
# 提取镜像名称进行测试
|
||
local image_name=$(echo "$filename" | sed 's/\.tar.*$//' | sed 's/_/:/g')
|
||
test_image_run "$image_name" || true
|
||
fi
|
||
return 0
|
||
else
|
||
return 1
|
||
fi
|
||
fi
|
||
}
|
||
|
||
# 主处理流程
|
||
main() {
|
||
check_docker
|
||
check_manifest_file
|
||
|
||
# 查找镜像文件
|
||
local image_files=($(find "$IMAGES_DIR" -name "*.tar*" -type f))
|
||
|
||
if [[ ${#image_files[@]} -eq 0 ]]; then
|
||
log_error "在 $IMAGES_DIR 中没有找到镜像文件"
|
||
exit 1
|
||
fi
|
||
|
||
log "找到 ${#image_files[@]} 个镜像文件"
|
||
|
||
# 处理镜像文件
|
||
local processed=0
|
||
local failed=0
|
||
local total=${#image_files[@]}
|
||
|
||
# 使用并行处理
|
||
export -f process_image_file verify_image_file import_image_file verify_image_architecture test_image_run
|
||
export -f log log_success log_warning log_error
|
||
export VERIFY_ONLY FORCE_IMPORT SKIP_COMPATIBILITY TEST_RUN LOG_FILE
|
||
export RED GREEN YELLOW BLUE NC
|
||
|
||
for file in "${image_files[@]}"; do
|
||
if process_image_file "$file"; then
|
||
((processed++))
|
||
else
|
||
((failed++))
|
||
fi
|
||
done
|
||
|
||
# 显示导入的镜像
|
||
if [[ "$VERIFY_ONLY" == false ]]; then
|
||
log "当前Docker镜像列表:"
|
||
docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}\t{{.CreatedAt}}"
|
||
fi
|
||
|
||
# 显示总结
|
||
log_success "处理完成!"
|
||
log "总文件数: $total"
|
||
log "成功处理: $processed"
|
||
log "失败数量: $failed"
|
||
|
||
if [[ $failed -gt 0 ]]; then
|
||
log_warning "有 $failed 个文件处理失败,请检查日志"
|
||
exit 1
|
||
else
|
||
log_success "所有文件处理成功"
|
||
fi
|
||
}
|
||
|
||
# 执行主流程
|
||
main |