Skip to content

Commit 9724f1c

Browse files
Copilotsunny0826
andauthored
Add Docker image source selector with availability checking (#32)
* Initial plan * Add Docker image selector feature with availability checking Co-authored-by: sunny0826 <24563928+sunny0826@users.noreply.github.com> * Fix TypeScript interface and linting issues Co-authored-by: sunny0826 <24563928+sunny0826@users.noreply.github.com> * Address code review feedback and improve image availability check Co-authored-by: sunny0826 <24563928+sunny0826@users.noreply.github.com> * Add comprehensive documentation for Docker image selector feature Co-authored-by: sunny0826 <24563928+sunny0826@users.noreply.github.com> * Fix image name parsing to handle multi-level registry paths correctly Co-authored-by: sunny0826 <24563928+sunny0826@users.noreply.github.com> * Fix container startup issue and update image sources to support ghcr.io - Fix JSON binding issue in startCourse API that prevented container startup - Remove Chinese cloud mirror sources (Aliyun, Tencent, NetEase) - Add GitHub Container Registry (ghcr.io) support - Update documentation to reflect new image source configuration Co-authored-by: sunny0826 <24563928+sunny0826@users.noreply.github.com> * Restore Aliyun mirror and add enhanced logging for image checks - Add back Aliyun (阿里云镜像加速) to image sources - Add detailed logging to checkImageExists for better debugging - Update documentation to reflect Aliyun support Co-authored-by: sunny0826 <24563928+sunny0826@users.noreply.github.com> * Fix image availability check timeout issues for ghcr.io - Increase timeout from 30s to 60s for slower network conditions - Add better error handling for timeout and TLS handshake errors - Provide more user-friendly error messages (network timeout vs image not found) - Improve logging for different failure scenarios Co-authored-by: sunny0826 <24563928+sunny0826@users.noreply.github.com> * feat: Add image source selector functionality and optimised environment checks - Added image source availability checks supporting Docker Hub, ghcr.io and Aliyun ACR - Optimised image selector UI to display current source labels and persist selections - Improved environment check commands with added image source availability detection - Refactored image reference processing logic to support multi-format image name resolution - Updated documentation and screenshots, removing obsolete usage guide content * add e2e --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: sunny0826 <24563928+sunny0826@users.noreply.github.com> Co-authored-by: Xudong Guo <guoxudong.dev@gmail.com>
1 parent 2f22756 commit 9724f1c

File tree

17 files changed

+1166
-227
lines changed

17 files changed

+1166
-227
lines changed

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,17 @@ KWDB Playground 是一个面向学习与演示的交互式课程平台,支持
4040

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

43+
### Docker 镜像源选择器(v0.4.1 新增)
44+
45+
支持灵活的镜像源选择,提高容器启动速度:
46+
47+
- **多镜像源支持**:Docker Hub、GitHub Container Registry (ghcr.io)、阿里云等镜像源
48+
- **可用性检查**:启动前验证镜像源的可访问性和响应速度
49+
- **自定义源**:支持配置任意 Docker 镜像仓库地址
50+
- **配置持久化**:自动保存镜像源选择,下次访问时自动恢复
51+
52+
![Docker 镜像源选择器](./docs/images/ImageSelector.png)
53+
4354
## 快速开始
4455

4556
### 发布版使用(推荐)

cmd/check/check.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,12 @@ func NewCommand(staticFiles embed.FS) *cobra.Command {
3131
cmd := &cobra.Command{
3232
Use: "check",
3333
Short: "检查本地开发环境",
34-
Long: "全面检查本地开发环境:\n1) Docker 环境是否可用\n2) 指定端口是否被占用\n3) 课程资源加载与数据完整性\n4) Playground 服务运行与健康状态",
34+
Long: `全面检查本地开发环境:
35+
1) Docker 环境是否可用
36+
2) 镜像源可用性(Docker Hub/ghcr.io/Aliyun ACR)
37+
3) 指定端口是否被占用
38+
4) 课程资源加载与数据完整性
39+
5) Playground 服务运行与健康状态`,
3540
RunE: func(cmd *cobra.Command, args []string) error {
3641
// 静默模式:禁用标准库日志输出,避免内部模块在检查期间输出日志
3742
// 注意:仅影响该命令的执行周期,结束后通过 defer 恢复,避免影响其他命令

docker/build_and_push.sh

Lines changed: 55 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,25 @@
33
set -e
44

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

18-
# 检查 DOCKERHUB_USER 是否已设置
19-
if [ -z "$DOCKERHUB_USER" ]; then
20-
echo "错误:请在脚本中设置 DOCKERHUB_USER 变量。" >&2
22+
# 检查 NAMESPACE 是否已设置
23+
if [ -z "$NAMESPACE" ]; then
24+
echo "错误:请在脚本中设置 NAMESPACE 变量。" >&2
2125
exit 1
2226
fi
2327

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

36-
# 检查是否已登录到 Docker Hub
40+
# 确保至少有一个注册表
41+
if [ ${#REGISTRIES[@]} -eq 0 ]; then
42+
REGISTRIES=("docker.io")
43+
fi
44+
45+
# 检查 Docker Hub 登录状态 (仅当包含 docker.io 时)
3746
# 注意:此检查依赖于 `docker info` 的输出格式,可能不完全可靠
38-
if ! docker info | grep -q "Username: ${DOCKERHUB_USER}"; then
39-
echo "提示:您似乎尚未登录到 Docker Hub 的 '${DOCKERHUB_USER}' 账户。"
40-
echo "请输入您的凭据以继续:"
41-
docker login -u "$DOCKERHUB_USER"
47+
if [[ " ${REGISTRIES[*]} " =~ " docker.io " ]]; then
48+
if ! docker info | grep -q "Username: ${NAMESPACE}"; then
49+
echo "提示:您似乎尚未登录到 Docker Hub 的 '${NAMESPACE}' 账户。"
50+
echo "请输入您的凭据以继续:"
51+
docker login -u "$NAMESPACE"
52+
fi
53+
fi
54+
55+
# 提示其他仓库登录
56+
# 简单提示用户确保已登录到所有目标仓库
57+
if [ ${#REGISTRIES[@]} -gt 1 ] || [[ ! " ${REGISTRIES[*]} " =~ " docker.io " ]]; then
58+
echo "请确保您已登录到所有目标仓库:"
59+
for REGISTRY in "${REGISTRIES[@]}"; do
60+
echo " - $REGISTRY"
61+
done
62+
echo ""
4263
fi
4364

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

58-
FULL_IMAGE_NAME="${DOCKERHUB_USER}/${REPO_NAME}:${IMAGE_TAG}"
59-
60-
# 构建并推送多架构镜像
79+
# 构建标签参数
80+
TAG_ARGS=()
6181
echo ""
6282
echo "=================================================="
63-
echo "开始构建并推送多架构镜像${FULL_IMAGE_NAME}"
83+
echo "开始构建并推送多架构镜像"
6484
echo "目标平台: ${PLATFORMS}"
85+
echo "目标镜像:"
86+
87+
for REGISTRY in "${REGISTRIES[@]}"; do
88+
if [ "$REGISTRY" == "docker.io" ]; then
89+
FULL_IMAGE_NAME="${NAMESPACE}/${REPO_NAME}:${IMAGE_TAG}"
90+
else
91+
FULL_IMAGE_NAME="${REGISTRY}/${NAMESPACE}/${REPO_NAME}:${IMAGE_TAG}"
92+
fi
93+
TAG_ARGS+=("-t" "${FULL_IMAGE_NAME}")
94+
echo " - ${FULL_IMAGE_NAME}"
95+
done
6596
echo "=================================================="
6697

98+
# 构建并推送多架构镜像
6799
docker buildx build \
68100
--platform "${PLATFORMS}" \
69-
-t "${FULL_IMAGE_NAME}" \
101+
"${TAG_ARGS[@]}" \
70102
--push \
71103
"${DOCKERFILE_PATH}"
72104

@@ -77,5 +109,11 @@ docker buildx build \
77109
echo ""
78110
echo "✅ 多架构镜像推送成功!"
79111
echo "您可以通过以下命令在不同架构的机器上拉取和使用它:"
80-
echo " docker pull ${FULL_IMAGE_NAME}"
81-
112+
for REGISTRY in "${REGISTRIES[@]}"; do
113+
if [ "$REGISTRY" == "docker.io" ]; then
114+
FULL_IMAGE_NAME="${NAMESPACE}/${REPO_NAME}:${IMAGE_TAG}"
115+
else
116+
FULL_IMAGE_NAME="${REGISTRY}/${NAMESPACE}/${REPO_NAME}:${IMAGE_TAG}"
117+
fi
118+
echo " docker pull ${FULL_IMAGE_NAME}"
119+
done

docs/images/ImageSelector.png

287 KB
Loading

internal/api/routes.go

Lines changed: 116 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,13 @@ func (h *Handler) SetupRoutes(r *gin.Engine) {
108108
containers.POST("/:id/stop", h.stopContainerByID)
109109
}
110110

111+
// 镜像相关路由
112+
images := api.Group("/images")
113+
{
114+
images.POST("/check-availability", h.checkImageAvailability)
115+
images.GET("/sources", h.getImageSources)
116+
}
117+
111118
// SQL 信息与健康(REST 信息类)
112119
api.GET("/sql/info", h.sqlInfo)
113120
api.GET("/sql/health", h.sqlHealth)
@@ -267,6 +274,10 @@ func (h *Handler) envCheck(c *gin.Context) {
267274
dockerOK, dockerMsg := check.DockerEnv()
268275
items = append(items, check.Item{Name: "Docker 环境", OK: dockerOK, Message: dockerMsg})
269276

277+
// 镜像源可用性
278+
imageOK, imageMsg, imageDetails := check.ImageSourcesAvailability()
279+
items = append(items, check.Item{Name: "镜像源可用性", OK: imageOK, Message: imageMsg, Details: imageDetails})
280+
270281
// 课程完整性(使用已加载的服务)
271282
coursesOK, coursesMsg := check.CoursesIntegrity(h.courseService)
272283
items = append(items, check.Item{Name: "课程加载与完整性", OK: coursesOK, Message: coursesMsg})
@@ -361,6 +372,13 @@ func (h *Handler) startCourse(c *gin.Context) {
361372
return
362373
}
363374

375+
// 解析请求体,获取可选的镜像参数
376+
var requestBody struct {
377+
Image string `json:"image"`
378+
}
379+
// 尝试解析JSON,如果失败(例如空body)则忽略错误
380+
_ = c.ShouldBindJSON(&requestBody)
381+
364382
// 使用互斥锁防止并发创建容器
365383
h.containerMutex.Lock()
366384
defer h.containerMutex.Unlock()
@@ -384,8 +402,11 @@ func (h *Handler) startCourse(c *gin.Context) {
384402
// 构建容器配置
385403
imageName := "kwdb/kwdb:latest" // 默认镜像
386404

387-
// 如果课程配置中指定了镜像,使用课程指定的镜像
388-
if course.Backend.ImageID != "" {
405+
// 优先级:1. 请求体中的自定义镜像 2. 课程配置的镜像 3. 默认镜像
406+
if requestBody.Image != "" {
407+
imageName = requestBody.Image
408+
h.logger.Debug("[startCourse] 使用请求中指定的镜像: %s", imageName)
409+
} else if course.Backend.ImageID != "" {
389410
imageName = course.Backend.ImageID
390411
h.logger.Debug("[startCourse] 使用课程指定镜像: %s", imageName)
391412
} else {
@@ -1465,3 +1486,96 @@ func (h *Handler) cleanupCourseContainers(c *gin.Context) {
14651486
"message": cleanupResult.Message,
14661487
})
14671488
}
1489+
1490+
// checkImageAvailability 检查镜像可用性
1491+
// POST /api/images/check-availability
1492+
// 请求体: {"imageName": "kwdb/kwdb:latest"}
1493+
// 响应:
1494+
//
1495+
// 200: 镜像可用性检查结果
1496+
// 400: 参数错误
1497+
// 500: 检查失败
1498+
func (h *Handler) checkImageAvailability(c *gin.Context) {
1499+
var req struct {
1500+
ImageName string `json:"imageName" binding:"required"`
1501+
}
1502+
1503+
if err := c.ShouldBindJSON(&req); err != nil {
1504+
h.logger.Error("[checkImageAvailability] 参数解析失败: %v", err)
1505+
c.JSON(http.StatusBadRequest, gin.H{
1506+
"error": "镜像名称不能为空",
1507+
})
1508+
return
1509+
}
1510+
1511+
h.logger.Info("[checkImageAvailability] 检查镜像可用性: %s", req.ImageName)
1512+
1513+
// 检查Docker控制器是否可用
1514+
if h.dockerController == nil {
1515+
h.logger.Error("[checkImageAvailability] Docker控制器未初始化")
1516+
c.JSON(http.StatusServiceUnavailable, gin.H{
1517+
"error": "Docker服务暂不可用",
1518+
})
1519+
return
1520+
}
1521+
1522+
// 调用Docker控制器检查镜像可用性
1523+
ctx := context.Background()
1524+
availability, err := h.dockerController.CheckImageAvailability(ctx, req.ImageName)
1525+
if err != nil {
1526+
h.logger.Error("[checkImageAvailability] 检查失败: %v", err)
1527+
c.JSON(http.StatusInternalServerError, gin.H{
1528+
"error": fmt.Sprintf("检查镜像可用性失败: %v", err),
1529+
})
1530+
return
1531+
}
1532+
1533+
h.logger.Info("[checkImageAvailability] 检查完成: %s, 可用: %v", req.ImageName, availability.Available)
1534+
1535+
c.JSON(http.StatusOK, availability)
1536+
}
1537+
1538+
// getImageSources 获取可用的镜像源列表
1539+
// GET /api/images/sources
1540+
// 响应:
1541+
//
1542+
// 200: 镜像源列表
1543+
func (h *Handler) getImageSources(c *gin.Context) {
1544+
h.logger.Info("[getImageSources] 获取镜像源列表")
1545+
1546+
// 定义常用的镜像源
1547+
sources := []gin.H{
1548+
{
1549+
"id": "docker-hub",
1550+
"name": "Docker Hub (默认)",
1551+
"prefix": "",
1552+
"description": "Docker官方镜像仓库",
1553+
"example": "kwdb/kwdb:latest",
1554+
},
1555+
{
1556+
"id": "ghcr",
1557+
"name": "GitHub Container Registry",
1558+
"prefix": "ghcr.io/",
1559+
"description": "GitHub 容器镜像仓库",
1560+
"example": "ghcr.io/kwdb/kwdb:latest",
1561+
},
1562+
{
1563+
"id": "aliyun",
1564+
"name": "阿里云 ACR",
1565+
"prefix": "registry.cn-hangzhou.aliyuncs.com/",
1566+
"description": "阿里云容器镜像服务",
1567+
"example": "registry.cn-hangzhou.aliyuncs.com/kwdb/kwdb:latest",
1568+
},
1569+
{
1570+
"id": "custom",
1571+
"name": "自定义源",
1572+
"prefix": "",
1573+
"description": "使用自定义的镜像仓库地址",
1574+
"example": "your-registry.com/kwdb/kwdb:latest",
1575+
},
1576+
}
1577+
1578+
c.JSON(http.StatusOK, gin.H{
1579+
"sources": sources,
1580+
})
1581+
}

0 commit comments

Comments
 (0)