Files
drouter/devtools/retag-docker-image.py
T
liuzhuohua 5150f4c2e6 feat(docker): 补 compose 编排与使用说明,修正镜像标签与 license
新增
  · packaging/docker/docker-compose.yml   现成编排,含健康检查与命名卷
  · packaging/docker/DOCKER-使用说明.md    完整使用文档(含常见问题)
  · devtools/retag-docker-image.py        镜像重打标签工具

修正三个真实缺陷
1. 镜像标签
   原本是 buildah 留下的 `localhost/drouter:1.0.0`。`localhost/` 是 Podman
   的本地镜像约定,Docker 用户拿到会很困惑(像某个私服地址)。
   改为 `drouter:1.0.0` + 别名 `liuzhuohua/drouter:1.0.0`。

2. license 标签写错
   镜像 LABEL 里 `org.opencontainers.image.licenses` 是 **GPL-3.0**,
   但项目实际是 **MIT**(Dockerfile 与镜像 config 都已改)。

3. 缺 HEALTHCHECK
   没有它,compose 里 `depends_on: condition: service_healthy` 无从等起,
   `docker ps` 也看不出容器是否真的活着。
   补上探活 /api/health —— 注意面板走 HTTPS + 自签证书,curl 必须 -k,
   否则永远 unhealthy。

compose 要点
  · network_mode: host 是硬要求(bridge 下看不到真实网卡、管不了宿主 NAT)
  · cap_add 只给 NET_ADMIN + NET_RAW,不用 privileged
  · host 模式下 ports 会被忽略,端口由 /etc/drouter/web-port 决定,
    故用 expose 表达意图而非 ports
  · 四个命名卷必须挂:配置 / 数据 / 快照 / 日志

重打标签采用流式重写 tar(只替换 manifest.json、repositories 与 config
三个小文件,layer.tar 原样透传),已逐层校验 3 个 layer 的 sha256 与
原包完全一致。
2026-09-30 21:25:28 +08:00

123 lines
5.7 KiB
Python
Raw 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 python3
# -*- coding: utf-8 -*-
"""
重打 Docker 镜像标签 + 修正 license 标签 + 补上 HEALTHCHECK/端口标注。
为什么需要这个脚本
------------------
镜像原本是用 **buildah** 以 root 构建的,标签被钉成 `localhost/drouter:1.0.0`。
`localhost/` 是 Podman 的"本地镜像"约定,Docker 虽然能 load 进来,但这个前缀
既不好看也容易让人误解(像是某个私服地址)。所以这里把标签改成:
主标签 drouter:1.0.0 ← docker load 后直接用这个跑
别名 liuzhuohua/drouter:1.0.0 ← 同时打一个带用户名的,方便以后推 Docker Hub
顺带修两个真实缺陷:
1. 镜像 LABEL 里 `org.opencontainers.image.licenses` 写的是 **GPL-3.0**,
但项目实际是 **MIT**。这是构建时抄错模板留下的。
2. 缺 `HEALTHCHECK`:compose 里写 `depends_on: condition: service_healthy`
就没东西可等;`docker ps` 也看不出容器是不是真的活着。
实现方式
--------
不解压、不重新打包整包(176MB,重打包有损坏风险),
而是**流式重写 tar**:只替换 manifest.json / repositories / config json
三个小文件,layer.tar 原样透传。这样:
· 大文件零拷贝,速度快
· layer 的 sha256 不变(docker load 会校验 layer 摘要)
"""
import json
import os
import sys
import tarfile
SRC = sys.argv[1] if len(sys.argv) > 1 else "dist/drouter-1.0.0-docker.tar"
DST = sys.argv[2] if len(sys.argv) > 2 else "dist/drouter-1.0.0-docker.tar.new"
NEW_TAGS = ["drouter:1.0.0", "liuzhuohua/drouter:1.0.0"]
with tarfile.open(SRC, "r:") as tin:
members = tin.getmembers()
by_name = {m.name: m for m in members}
# ── 读 manifest.json ────────────────────────────────────────────
mf = json.load(tin.extractfile("manifest.json"))
if len(mf) != 1:
sys.exit(f"预期 manifest 只有 1 个条目,实际 {len(mf)} 个,不敢改")
config_name = mf[0]["Config"]
print(f" config 文件 : {config_name}")
print(f" layer 数 : {len(mf[0]['Layers'])}")
print(f" 原 RepoTags : {mf[0].get('RepoTags')}")
# ── 读并修改 config json ────────────────────────────────────────
cfg_raw = tin.extractfile(config_name).read()
cfg = json.loads(cfg_raw)
# 1) 修 license 标签:GPL-3.0 → MIT(与项目 LICENSE 文件一致)
labels = cfg.get("config", {}).get("Labels", {})
old_lic = labels.get("org.opencontainers.image.licenses")
if old_lic != "MIT":
labels["org.opencontainers.image.licenses"] = "MIT"
cfg["config"]["Labels"] = labels
print(f" license 标签 : {old_lic} → MIT")
else:
print(" license 标签 : 已是 MIT,跳过")
# 2) 补 healthcheck:
# 镜像里没有 curl 之外的探活手段,但 /api/health 是**免鉴权**的
# (见 drouter-web.py:/api/health 与 /api/login 不要求 X-Token)。
# ⚠️ 但 web 是 HTTPS 且用自签证书 → curl 必须 -k,否则永远 unhealthy。
# 端口从 /etc/drouter/web-port 读,默认 8443。
if not cfg["config"].get("Healthcheck"):
cfg["config"]["Healthcheck"] = {
"Test": [
"CMD-SHELL",
'PORT=$(cat /etc/drouter/web-port 2>/dev/null || echo 8443); '
'curl -fsSk --max-time 4 "https://127.0.0.1:${PORT}/api/health" '
'>/dev/null 2>&1 || exit 1',
],
"Interval": 30000000000, # 30s(纳秒,docker 格式)
"Timeout": 5000000000, # 5s
"StartPeriod": 20000000000, # 20s,给首次启动留时间
"Retries": 3,
}
print(" healthcheck : 已补(/api/health,自签证书走 -k)")
else:
print(" healthcheck : 已存在,跳过")
new_cfg_raw = json.dumps(cfg, ensure_ascii=False,
separators=(",", ":")).encode("utf-8")
# ── 写新 tar ────────────────────────────────────────────────────
with tarfile.open(DST, "w", format=tarfile.GNU_FORMAT) as tout:
for m in members:
if m.name == config_name:
m.size = len(new_cfg_raw)
tout.addfile(m, __import__("io").BytesIO(new_cfg_raw))
elif m.name == "manifest.json":
mf[0]["RepoTags"] = NEW_TAGS
blob = json.dumps(mf, ensure_ascii=False,
separators=(",", ":")).encode("utf-8")
m.size = len(blob)
tout.addfile(m, __import__("io").BytesIO(blob))
elif m.name == "repositories":
# docker save 的老式 tag 索引,格式 {repo: {tag: layerid}}
old = json.load(tin.extractfile("repositories"))
layer_id = None
for repo, tags in old.items():
layer_id = list(tags.values())[0]
new_repos = {}
for t in NEW_TAGS:
repo, _, tag = t.rpartition(":")
new_repos.setdefault(repo, {})[tag] = layer_id
blob = json.dumps(new_repos, ensure_ascii=False,
separators=(",", ":")).encode("utf-8")
m.size = len(blob)
tout.addfile(m, __import__("io").BytesIO(blob))
else:
# layer.tar 等大文件:直接从原 tar 流式拷贝,零解压
tout.addfile(m, tin.extractfile(m))
print(f"\n ✅ 写出:{DST}")
print(f" 新标签:{NEW_TAGS}")