发 v1.0.9 前发现两个真问题,都会直接影响用户拿到的附件。 一、docker-compose.yml 里的镜像标签是硬编码的 1.0.8 这两个文件是**直接挂到 GitHub Release 上的附件**,用户照着抄命令。 早先它们硬编码具体版本号,每次发版要人工 sed 一遍 —— 这次就漏了: 附件里写着 `image: drouter:1.0.8`,而镜像 tar 加载后的标签是 `drouter:1.0.9`, `docker compose up` 直接报「找不到镜像」。DOCKER-GUIDE.md 另有 14 处同样问题。 改成 __VERSION__ 模板,由 build-docker.sh 渲染(版本从 packaging/VERSION 读)。 渲染分两步,顺序不能反:先删 TEMPLATE-NOTE 标记块(那是给维护者看的 「这是模板」提示,留着用户会看到「1.0.9 是占位符」这种自相矛盾的话), 再替换占位符。先替换的话,标记块里的 __VERSION__ 也会被换掉。 retag-docker-image.py 同样问题:NEW_TAGS 硬编码。改完参数化。 这个 bug 更隐蔽 —— 拿 1.0.9 的镜像跑重标记会打上 1.0.8 的标签, 用户 load 完看到的标签与附件说明对不上。 t-packaging.py 补 12 条判据卡住这些(4 条已做反向验证:注入即红、还原即绿)。 判据只抓「紧挨 drouter 标识或产物文件名」的版本号 —— 一刀切拦所有 x.y 会 误伤「1.0.1 起 Web 终端绕开 init」这种历史沿革说明和「GPL-3.0」许可证名。 二、t-worktree-clean.py 抓不到「注释掉整行代码」型注入 跑回归时 t-107 红了 1 项,查出来是 t-107-inject-wg.py 把 `'env': _vpn_env(),` 注释掉了没还原(SIGTERM 打断,finally 没执行)—— 和 v1.0.8 那次 `/nonexistent` 残留一模一样的坑,但**形态不同**: 不含任何哨兵字样,原判据的 4 个哨兵全都抓不到。 后果很实:_vpn_status 不返回 env,前端拿不到四态诊断, VPN 页面的三个修复按钮全废。这正是 v1.0.8 声称修好的那个 bug。 补两条判据: - 扫「被注释掉的整行代码」:去掉 # 后能 ast.parse 成功,且含调用/赋值/return/dict 项 - 扫 CRLF:Windows 工具写文件会整篇改行尾,sync.sh 预检才拦太晚 判据本身踩了三个坑,都记在注释里: - dict 项 `'env': _vpn_env(),` 单独 ast.parse 会 SyntaxError(没大括号), 真正的注入反而被放过 → 补大括号再解析 - dict 项的键**通常带引号**,正则只写 ^[A-Za-z_] 认不出来 → 改成同时认引号键 - 分隔线注释 `# ------ radvd` 里的 ----- 会被解析成负号表达式 → 按前缀排掉 双向验证:4 种注入形态(赋值/dict项/bool/列表)全部精确报到行号, 基线零误报(排掉了分隔线注释和「# module: (service, action, 中文名)」这类 长得像 dict 项的文档注释)。 三、_dev/dl-verify.py(新增) 从目标机往 Windows 拉大产物时发现的坑,记成脚本省得下次重踩: - `ssh host 'cat f' > out` 在 Windows 上做**文本模式换行转换**,二进制被破坏 (实测 deb 少 4 字节,sha256 直接对不上)→ 走 base64 再本地解码 - Windows Python 看不到 Git Bash 的 /tmp,必须给 C:/ 绝对路径 - scp 传完一定要判 sha256 —— scp/ssh 都可能「返回 0 却落地 0 字节」 - 落地走 .part + os.replace,避免写一半失败留下半个产物 顺带修了 build-docker.sh 里一处路径笔误 (packaging/docker/packaging/docker/DOCKER-GUIDE.md)和步骤编号 1/3→1/4。
136 lines
6.4 KiB
Python
136 lines
6.4 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
重打 Docker 镜像标签 + 修正 license 标签 + 补上 HEALTHCHECK/端口标注。
|
||
|
||
为什么需要这个脚本
|
||
------------------
|
||
镜像原本是用 **buildah** 以 root 构建的,标签被钉成 `localhost/drouter:<版本>`。
|
||
`localhost/` 是 Podman 的"本地镜像"约定,Docker 虽然能 load 进来,但这个前缀
|
||
既不好看也容易让人误解(像是某个私服地址)。所以这里把标签改成:
|
||
主标签 drouter:<版本> ← docker load 后直接用这个跑
|
||
别名 liuzhuohua/drouter:<版本> ← 同时打一个带用户名的,方便以后推 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
|
||
|
||
# ⚠️ 版本号**必须显式传**,不要再写死默认值。
|
||
# 之前这里硬编码 1.0.8,于是「拿 1.0.9 的镜像跑重标记」会打上 1.0.8 的标签,
|
||
# 用户 docker load 完看到的是 drouter:1.0.8,与 Release 附件里的说明对不上。
|
||
# 现在版本从 packaging/VERSION 读(唯一真源),命令行可覆盖。
|
||
_HERE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||
_VF = os.path.join(_HERE, "packaging", "VERSION")
|
||
if not os.path.isfile(_VF):
|
||
sys.exit(f"缺少 {_VF}(版本号唯一真源)")
|
||
with open(_VF, encoding="utf-8") as f:
|
||
VER = f.read().strip()
|
||
if not VER:
|
||
sys.exit(f"{_VF} 为空")
|
||
|
||
SRC = sys.argv[1] if len(sys.argv) > 1 else f"dist/drouter-{VER}-docker.tar"
|
||
DST = sys.argv[2] if len(sys.argv) > 2 else f"dist/drouter-{VER}-docker.tar.new"
|
||
|
||
NEW_TAGS = [f"drouter:{VER}", f"liuzhuohua/drouter:{VER}"]
|
||
|
||
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}")
|