feat: add modify docker config

This commit is contained in:
2026-07-02 12:33:40 +08:00
parent b08868cf16
commit ca88abbfc9
3 changed files with 293 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
{
"log-driver": "json-file",
"log-opts": {
"max-size": "50m",
"max-file": "3"
}
}
+5
View File
@@ -0,0 +1,5 @@
{
"registry-mirrors": [
"https://docker.1ms.run"
]
}
+281
View File
@@ -0,0 +1,281 @@
#!/usr/bin/env python3
# 按类别增量更新 /etc/docker/daemon.json(仅补缺失项,不覆盖已有配置)
from __future__ import annotations
import argparse
import copy
import json
import os
import shutil
import subprocess
import sys
from datetime import datetime
from pathlib import Path
from typing import Any
DAEMON_JSON = Path("/etc/docker/daemon.json")
PATCH_DIR_NAME = Path("docker") / "daemon.d"
CATEGORIES: dict[str, str] = {
"base": "日志轮转等基础项(log-driver / log-opts)",
"mirror": "镜像加速(registry-mirrors)",
}
def dotfile_root() -> Path:
return Path(__file__).resolve().parent.parent
def usage_text() -> str:
cats = "\n".join(f" {name:<8} {desc}" for name, desc in CATEGORIES.items())
return f"""用法: docker-daemon-config [选项] [类别 ...]
将 ~/.dotfile/docker/daemon.d/ 下对应片段合并进 /etc/docker/daemon.json。
已有 key 保留不变;仅插入缺失的 key(嵌套对象递归补全)。
类别:
{cats}
all 应用上述全部类别
示例:
sudo ff script docker-daemon-config base
sudo ff script docker-daemon-config mirror
sudo ff script docker-daemon-config all
sudo ff script docker-daemon-config base --dry-run
选项:
-h, --help 显示此帮助
--list 列出可用类别
--dry-run 只打印合并结果,不写文件
--no-backup 不备份现有 daemon.json
--no-restart 合并后不尝试 reload/restart docker
"""
def fill_missing(base: Any, patch: Any) -> Any:
"""仅补 patch 中有、base 中没有的 key;已有 key 不覆盖。"""
if not isinstance(patch, dict):
return base
if not isinstance(base, dict):
return copy.deepcopy(patch)
out = copy.deepcopy(base)
for key, patch_value in patch.items():
if key not in out:
out[key] = copy.deepcopy(patch_value)
elif isinstance(out[key], dict) and isinstance(patch_value, dict):
out[key] = fill_missing(out[key], patch_value)
return out
def load_json(path: Path) -> dict[str, Any]:
text = path.read_text(encoding="utf-8").strip()
if not text:
return {}
data = json.loads(text)
if not isinstance(data, dict):
raise ValueError(f"{path} 根节点必须是 JSON 对象")
return data
def resolve_categories(names: list[str]) -> list[str]:
if not names:
return []
if "all" in names:
return list(CATEGORIES.keys())
unknown = [name for name in names if name not in CATEGORIES]
if unknown:
raise ValueError(f"未知类别: {', '.join(unknown)}")
# 去重并保持顺序
seen: set[str] = set()
ordered: list[str] = []
for name in names:
if name not in seen:
seen.add(name)
ordered.append(name)
return ordered
def patch_path(root: Path, category: str) -> Path:
return root / PATCH_DIR_NAME / f"{category}.json"
def collect_patches(root: Path, categories: list[str]) -> dict[str, dict[str, Any]]:
patches: dict[str, dict[str, Any]] = {}
for category in categories:
path = patch_path(root, category)
if not path.is_file():
raise FileNotFoundError(f"缺少配置片段: {path}")
patches[category] = load_json(path)
return patches
def diff_added(before: dict[str, Any], after: dict[str, Any], prefix: str = "") -> list[str]:
added: list[str] = []
for key, value in after.items():
path = f"{prefix}{key}"
if key not in before:
added.append(path)
continue
if isinstance(value, dict) and isinstance(before.get(key), dict):
added.extend(diff_added(before[key], value, prefix=f"{path}."))
return added
def validate_daemon_json(path: Path) -> None:
with path.open(encoding="utf-8") as fh:
json.load(fh)
dockerd = shutil.which("dockerd")
if not dockerd:
return
try:
subprocess.run(
[dockerd, "--validate", "--config-file", str(path)],
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
text=True,
)
except subprocess.CalledProcessError as exc:
detail = (exc.stderr or "").strip()
raise RuntimeError(f"dockerd --validate 失败: {detail or exc}") from exc
def reload_docker() -> None:
if shutil.which("systemctl") is None:
print("[INFO] 未找到 systemctl,请手动重启 Docker 使配置生效")
return
for action in ("reload", "restart"):
result = subprocess.run(
["systemctl", action, "docker"],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
)
if result.returncode == 0:
print(f"[INFO] 已执行 systemctl {action} docker")
return
print("[WARN] systemctl reload/restart docker 失败,请手动重启 Docker")
if result.stdout.strip():
print(result.stdout.strip())
def write_daemon_json(content: dict[str, Any], dry_run: bool, backup: bool) -> None:
serialized = json.dumps(content, indent=2, ensure_ascii=False) + "\n"
if dry_run:
print(serialized)
return
if os.geteuid() != 0:
print("[ERROR] 写入 /etc/docker/daemon.json 需要 root,请使用 sudo 运行")
sys.exit(1)
DAEMON_JSON.parent.mkdir(parents=True, exist_ok=True)
tmp_path = DAEMON_JSON.with_suffix(".json.tmp")
tmp_path.write_text(serialized, encoding="utf-8")
try:
validate_daemon_json(tmp_path)
except Exception:
tmp_path.unlink(missing_ok=True)
raise
if backup and DAEMON_JSON.is_file():
stamp = datetime.now().strftime("%Y%m%d%H%M%S")
backup_path = DAEMON_JSON.with_name(f"{DAEMON_JSON.name}.bak.{stamp}")
shutil.copy2(DAEMON_JSON, backup_path)
print(f"[INFO] 已备份到 {backup_path}")
tmp_path.replace(DAEMON_JSON)
os.chmod(DAEMON_JSON, 0o644)
print(f"[INFO] 已写入 {DAEMON_JSON}")
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument("categories", nargs="*", help="要应用的类别")
parser.add_argument("-h", "--help", action="store_true")
parser.add_argument("--list", action="store_true", help=argparse.SUPPRESS)
parser.add_argument("--dry-run", action="store_true")
parser.add_argument("--no-backup", action="store_true")
parser.add_argument("--no-restart", action="store_true")
return parser.parse_args(argv)
def main(argv: list[str] | None = None) -> int:
args = parse_args(argv)
if args.help:
print(usage_text())
return 0
if args.list:
for name, desc in CATEGORIES.items():
print(f"{name}\t{desc}")
print("all\t应用全部类别")
return 0
try:
categories = resolve_categories(args.categories)
except ValueError as exc:
print(f"[ERROR] {exc}")
print("可用类别:", ", ".join([*CATEGORIES.keys(), "all"]))
return 1
if not categories:
print(usage_text())
return 1
root = dotfile_root()
patch_root = root / PATCH_DIR_NAME
if not patch_root.is_dir():
print(f"[ERROR] 配置目录不存在: {patch_root}")
return 1
existing: dict[str, Any] = {}
if DAEMON_JSON.is_file():
try:
existing = load_json(DAEMON_JSON)
except json.JSONDecodeError as exc:
print(f"[ERROR] {DAEMON_JSON} 不是合法 JSON: {exc}")
return 1
try:
patches = collect_patches(root, categories)
except (FileNotFoundError, ValueError) as exc:
print(f"[ERROR] {exc}")
return 1
merged = copy.deepcopy(existing)
for category in categories:
before = copy.deepcopy(merged)
merged = fill_missing(merged, patches[category])
added = diff_added(before, merged)
if added:
print(f"[INFO] 类别 {category} 新增: {', '.join(added)}")
else:
print(f"[INFO] 类别 {category} 无需变更(相关项已存在)")
if merged == existing:
print("[INFO] daemon.json 无变化")
return 0
try:
write_daemon_json(merged, dry_run=args.dry_run, backup=not args.no_backup)
except Exception as exc:
print(f"[ERROR] {exc}")
return 1
if not args.dry_run and not args.no_restart:
reload_docker()
return 0
if __name__ == "__main__":
sys.exit(main())