Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,17 @@ KWDB Playground 是一个面向学习与演示的交互式课程平台,支持

![SQL 终端类型](./docs/images/SqlTerminal.gif)

### Docker 镜像源选择器(v0.4.1 新增)

支持灵活的镜像源选择,提高容器启动速度:

- **多镜像源支持**:Docker Hub、GitHub Container Registry (ghcr.io)、阿里云等镜像源
- **可用性检查**:启动前验证镜像源的可访问性和响应速度
- **自定义源**:支持配置任意 Docker 镜像仓库地址
- **配置持久化**:自动保存镜像源选择,下次访问时自动恢复

![Docker 镜像源选择器](./docs/images/ImageSelector.png)

## 快速开始

### 发布版使用(推荐)
Expand Down
7 changes: 6 additions & 1 deletion cmd/check/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,12 @@ func NewCommand(staticFiles embed.FS) *cobra.Command {
cmd := &cobra.Command{
Use: "check",
Short: "检查本地开发环境",
Long: "全面检查本地开发环境:\n1) Docker 环境是否可用\n2) 指定端口是否被占用\n3) 课程资源加载与数据完整性\n4) Playground 服务运行与健康状态",
Long: `全面检查本地开发环境:
1) Docker 环境是否可用
2) 镜像源可用性(Docker Hub/ghcr.io/Aliyun ACR)
3) 指定端口是否被占用
4) 课程资源加载与数据完整性
5) Playground 服务运行与健康状态`,
RunE: func(cmd *cobra.Command, args []string) error {
// 静默模式:禁用标准库日志输出,避免内部模块在检查期间输出日志
// 注意:仅影响该命令的执行周期,结束后通过 defer 恢复,避免影响其他命令
Expand Down
72 changes: 55 additions & 17 deletions docker/build_and_push.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,25 @@
set -e

# --- 配置信息 --- #
# Docker Hub 用户名
DOCKERHUB_USER="kwdb"
# 镜像命名空间 (用户名/组织名)
NAMESPACE="kwdb"
# 镜像仓库名
REPO_NAME="ubuntu"
# 镜像标签
IMAGE_TAG="20.04"
# 目标架构列表 (amd64, arm64 是 Docker buildx 的标准命名)
ARCHITECTURES=("amd64" "arm64")
# 目标仓库地址列表
# 支持推送到多个仓库。默认包含 "docker.io" (Docker Hub)
# 示例: REGISTRIES=("docker.io" "ghcr.io" "quay.io")
REGISTRIES=("docker.io" "ghcr.io" "registry.cn-hangzhou.aliyuncs.com")
# Dockerfile 所在路径
DOCKERFILE_PATH="."
# --- 配置结束 --- #

# 检查 DOCKERHUB_USER 是否已设置
if [ -z "$DOCKERHUB_USER" ]; then
echo "错误:请在脚本中设置 DOCKERHUB_USER 变量。" >&2
# 检查 NAMESPACE 是否已设置
if [ -z "$NAMESPACE" ]; then
echo "错误:请在脚本中设置 NAMESPACE 变量。" >&2
exit 1
fi

Expand All @@ -33,12 +37,29 @@ if ! docker buildx version > /dev/null 2>&1; then
exit 1
fi

# 检查是否已登录到 Docker Hub
# 确保至少有一个注册表
if [ ${#REGISTRIES[@]} -eq 0 ]; then
REGISTRIES=("docker.io")
fi

# 检查 Docker Hub 登录状态 (仅当包含 docker.io 时)
# 注意:此检查依赖于 `docker info` 的输出格式,可能不完全可靠
if ! docker info | grep -q "Username: ${DOCKERHUB_USER}"; then
echo "提示:您似乎尚未登录到 Docker Hub 的 '${DOCKERHUB_USER}' 账户。"
echo "请输入您的凭据以继续:"
docker login -u "$DOCKERHUB_USER"
if [[ " ${REGISTRIES[*]} " =~ " docker.io " ]]; then
if ! docker info | grep -q "Username: ${NAMESPACE}"; then
echo "提示:您似乎尚未登录到 Docker Hub 的 '${NAMESPACE}' 账户。"
echo "请输入您的凭据以继续:"
docker login -u "$NAMESPACE"
fi
fi

# 提示其他仓库登录
# 简单提示用户确保已登录到所有目标仓库
if [ ${#REGISTRIES[@]} -gt 1 ] || [[ ! " ${REGISTRIES[*]} " =~ " docker.io " ]]; then
echo "请确保您已登录到所有目标仓库:"
for REGISTRY in "${REGISTRIES[@]}"; do
echo " - $REGISTRY"
done
echo ""
fi

# 创建或复用一个 buildx 构建器实例
Expand All @@ -55,18 +76,29 @@ docker buildx inspect --bootstrap
# 将架构数组转换为逗号分隔的平台字符串,例如:linux/amd64,linux/arm64
PLATFORMS=$(printf "linux/%s," "${ARCHITECTURES[@]}" | sed 's/,$//')

FULL_IMAGE_NAME="${DOCKERHUB_USER}/${REPO_NAME}:${IMAGE_TAG}"

# 构建并推送多架构镜像
# 构建标签参数
TAG_ARGS=()
echo ""
echo "=================================================="
echo "开始构建并推送多架构镜像:${FULL_IMAGE_NAME}"
echo "开始构建并推送多架构镜像"
echo "目标平台: ${PLATFORMS}"
echo "目标镜像:"

for REGISTRY in "${REGISTRIES[@]}"; do
if [ "$REGISTRY" == "docker.io" ]; then
FULL_IMAGE_NAME="${NAMESPACE}/${REPO_NAME}:${IMAGE_TAG}"
else
FULL_IMAGE_NAME="${REGISTRY}/${NAMESPACE}/${REPO_NAME}:${IMAGE_TAG}"
fi
TAG_ARGS+=("-t" "${FULL_IMAGE_NAME}")
echo " - ${FULL_IMAGE_NAME}"
done
echo "=================================================="

# 构建并推送多架构镜像
docker buildx build \
--platform "${PLATFORMS}" \
-t "${FULL_IMAGE_NAME}" \
"${TAG_ARGS[@]}" \
--push \
"${DOCKERFILE_PATH}"

Expand All @@ -77,5 +109,11 @@ docker buildx build \
echo ""
echo "✅ 多架构镜像推送成功!"
echo "您可以通过以下命令在不同架构的机器上拉取和使用它:"
echo " docker pull ${FULL_IMAGE_NAME}"

for REGISTRY in "${REGISTRIES[@]}"; do
if [ "$REGISTRY" == "docker.io" ]; then
FULL_IMAGE_NAME="${NAMESPACE}/${REPO_NAME}:${IMAGE_TAG}"
else
FULL_IMAGE_NAME="${REGISTRY}/${NAMESPACE}/${REPO_NAME}:${IMAGE_TAG}"
fi
echo " docker pull ${FULL_IMAGE_NAME}"
done
Binary file added docs/images/ImageSelector.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
118 changes: 116 additions & 2 deletions internal/api/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,13 @@ func (h *Handler) SetupRoutes(r *gin.Engine) {
containers.POST("/:id/stop", h.stopContainerByID)
}

// 镜像相关路由
images := api.Group("/images")
{
images.POST("/check-availability", h.checkImageAvailability)
images.GET("/sources", h.getImageSources)
}

// SQL 信息与健康(REST 信息类)
api.GET("/sql/info", h.sqlInfo)
api.GET("/sql/health", h.sqlHealth)
Expand Down Expand Up @@ -267,6 +274,10 @@ func (h *Handler) envCheck(c *gin.Context) {
dockerOK, dockerMsg := check.DockerEnv()
items = append(items, check.Item{Name: "Docker 环境", OK: dockerOK, Message: dockerMsg})

// 镜像源可用性
imageOK, imageMsg, imageDetails := check.ImageSourcesAvailability()
items = append(items, check.Item{Name: "镜像源可用性", OK: imageOK, Message: imageMsg, Details: imageDetails})

// 课程完整性(使用已加载的服务)
coursesOK, coursesMsg := check.CoursesIntegrity(h.courseService)
items = append(items, check.Item{Name: "课程加载与完整性", OK: coursesOK, Message: coursesMsg})
Expand Down Expand Up @@ -361,6 +372,13 @@ func (h *Handler) startCourse(c *gin.Context) {
return
}

// 解析请求体,获取可选的镜像参数
var requestBody struct {
Image string `json:"image"`
}
// 尝试解析JSON,如果失败(例如空body)则忽略错误
_ = c.ShouldBindJSON(&requestBody)

// 使用互斥锁防止并发创建容器
h.containerMutex.Lock()
defer h.containerMutex.Unlock()
Expand All @@ -384,8 +402,11 @@ func (h *Handler) startCourse(c *gin.Context) {
// 构建容器配置
imageName := "kwdb/kwdb:latest" // 默认镜像

// 如果课程配置中指定了镜像,使用课程指定的镜像
if course.Backend.ImageID != "" {
// 优先级:1. 请求体中的自定义镜像 2. 课程配置的镜像 3. 默认镜像
if requestBody.Image != "" {
imageName = requestBody.Image
h.logger.Debug("[startCourse] 使用请求中指定的镜像: %s", imageName)
} else if course.Backend.ImageID != "" {
imageName = course.Backend.ImageID
h.logger.Debug("[startCourse] 使用课程指定镜像: %s", imageName)
} else {
Expand Down Expand Up @@ -1465,3 +1486,96 @@ func (h *Handler) cleanupCourseContainers(c *gin.Context) {
"message": cleanupResult.Message,
})
}

// checkImageAvailability 检查镜像可用性
// POST /api/images/check-availability
// 请求体: {"imageName": "kwdb/kwdb:latest"}
// 响应:
//
// 200: 镜像可用性检查结果
// 400: 参数错误
// 500: 检查失败
func (h *Handler) checkImageAvailability(c *gin.Context) {
var req struct {
ImageName string `json:"imageName" binding:"required"`
}

if err := c.ShouldBindJSON(&req); err != nil {
h.logger.Error("[checkImageAvailability] 参数解析失败: %v", err)
c.JSON(http.StatusBadRequest, gin.H{
"error": "镜像名称不能为空",
})
return
}

h.logger.Info("[checkImageAvailability] 检查镜像可用性: %s", req.ImageName)

// 检查Docker控制器是否可用
if h.dockerController == nil {
h.logger.Error("[checkImageAvailability] Docker控制器未初始化")
c.JSON(http.StatusServiceUnavailable, gin.H{
"error": "Docker服务暂不可用",
})
return
}

// 调用Docker控制器检查镜像可用性
ctx := context.Background()
availability, err := h.dockerController.CheckImageAvailability(ctx, req.ImageName)
if err != nil {
h.logger.Error("[checkImageAvailability] 检查失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{
"error": fmt.Sprintf("检查镜像可用性失败: %v", err),
})
return
}

h.logger.Info("[checkImageAvailability] 检查完成: %s, 可用: %v", req.ImageName, availability.Available)

c.JSON(http.StatusOK, availability)
}

// getImageSources 获取可用的镜像源列表
// GET /api/images/sources
// 响应:
//
// 200: 镜像源列表
func (h *Handler) getImageSources(c *gin.Context) {
h.logger.Info("[getImageSources] 获取镜像源列表")

// 定义常用的镜像源
sources := []gin.H{
{
"id": "docker-hub",
"name": "Docker Hub (默认)",
"prefix": "",
"description": "Docker官方镜像仓库",
"example": "kwdb/kwdb:latest",
},
{
"id": "ghcr",
"name": "GitHub Container Registry",
"prefix": "ghcr.io/",
"description": "GitHub 容器镜像仓库",
"example": "ghcr.io/kwdb/kwdb:latest",
},
{
"id": "aliyun",
"name": "阿里云 ACR",
"prefix": "registry.cn-hangzhou.aliyuncs.com/",
"description": "阿里云容器镜像服务",
"example": "registry.cn-hangzhou.aliyuncs.com/kwdb/kwdb:latest",
},
{
"id": "custom",
"name": "自定义源",
"prefix": "",
"description": "使用自定义的镜像仓库地址",
"example": "your-registry.com/kwdb/kwdb:latest",
},
}

c.JSON(http.StatusOK, gin.H{
"sources": sources,
})
}
Loading