feat: add modify docker config
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"log-driver": "json-file",
|
||||||
|
"log-opts": {
|
||||||
|
"max-size": "50m",
|
||||||
|
"max-file": "3"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"registry-mirrors": [
|
||||||
|
"https://docker.1ms.run"
|
||||||
|
]
|
||||||
|
}
|
||||||
Executable
+281
@@ -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())
|
||||||
Reference in New Issue
Block a user