feat(chv-guest): Gitea 同步变量经 seed 进 guest-env(执行器集成)

- 新增 cloud/chv/gitea_sync.py:6 键白名单,三源合并
  (宿主 CHV_GITEA_* < 任务 gitea_sync < extra_env),
  必填 4 键缺失即 fail-closed,空即不同步
- ssh_transport:GITEA_TOKEN 进密钥集(argv 拒绝),其余
  5 键进 guest-env 集(split_guest_env)
- hostkeys.write_seed:guest_env 与 secret_env 合并写同一
  条目,仍无 runcmd、0600
- config:_gitea_fragment 产 gitea_sync_env(函数内 import,
  叶子模块顶层只许标准库)
- executor.wake_vm:起 VMM 前合并+校验(gitea_sync_incomplete),
  guest-env 随 seed 投递;provision_host_key 透传 guest_env
- worker:_run_attempt 仅对 CHV 执行器把同步变量并进 gateway_env
- 测试:cloud/tests/test_chv_gitea_sync.py 20 项
- 边界:tasks 表无 JSON 列,DB 往返丢任务 gitea_sync;
  生产主来源是宿主 CHV_GITEA_* 环境变量
This commit is contained in:
host committed 2026-11-06 16:55:32 +00:00
1 parent 67ca0b86cd
commit e7f699c62b
7 files changed
+785 -37

No files matched your search

+126
View File
@@ -0,0 +1,126 @@
"""Gitea 同步变量供给(CHV guest-env 集成,叶子模块)。
背景:客内 chv-sync boot 在 cloud-init 时运行、SSH 之前,
只能读 seed 投递的 /etc/chv/guest-env,看不到 SSH env 前缀。
因此同步所需的 6 个变量必须随 seed 进 guest-env,不能只靠
extra_env 的 SSH 前缀;其中 GITEA_TOKEN 是密钥,只进 seed,
绝不进 argv/日志/镜像。
变量与来源(优先级低→高,后者覆盖前者):
1. 宿主配置环境变量(主来源,生产口径):CHV_GITEA_URL、
CHV_GITEA_USER、CHV_GITEA_TOKEN、CHV_SYNC_REPO、
CHV_SYNC_BRANCH、CHV_GITEA_EMAIL。由部署该 worker/执行器
的运维在宿主环境里设置,与 CHV_SSH_KEY、CHV_EGRESS_PROXY
同款口径;CHV 是单租户单 VM 模型,一台宿主对应一个
Gitea 身份。密钥来源绝不硬编码进代码。
2. 任务级覆盖(内存态):任务 dict 的 `gitea_sync` 子 dict,
键即 6 个变量名。仅在调用方以内存态构造任务 dict 时生效
(直接调 wake_vm、测试、手工驱动);tasks 表目前没有
JSON/元数据列,DB 往返会丢掉它——这是已知边界,不做
静默声称,需要每任务不同仓库时再做任务表迁移。
3. 显式 extra_env:调用方直接传给 wake_vm/dispatch 的
extra_env 里的同名键,优先级最高。
缺失语义(fail-closed,不跳过同步):
- 6 个键一个都没有 → 本次启动不同步(chv-sync boot 见无
guest-env 静默成功,VM 照常用)。
- 任一键出现、但必填 4 键(GITEA_URL、GITEA_USER、
GITEA_TOKEN、SYNC_REPO)缺失 → 校验失败,wake 在起 VMM
之前直接拒绝;半份同步配置绝不能让 VM 带着“我以为同步
了”的状态启动。SYNC_BRANCH、GITEA_EMAIL 可选。
- 校验错误只含缺失的键名,绝不含任何变量值。
本模块是纯函数,不读文件、不跑 VM、不含任何密钥值。
"""
# 同步变量全集(guest-env 里应有的键)。
GITEA_SYNC_KEYS = frozenset({
"GITEA_URL", "GITEA_USER", "GITEA_TOKEN",
"SYNC_REPO", "SYNC_BRANCH", "GITEA_EMAIL",
})
# 其中密钥:只进 seed,绝不进 argv(见 ssh_transport 的
# _CHV_SECRET_ENV_NAMES)。
GITEA_SYNC_SECRET_KEYS = frozenset({"GITEA_TOKEN"})
# 其中非密钥、但 boot 时就要用、也必须进 guest-env 的键
#(SSH env 前缀到不了 cloud-init 时运行的 chv-sync boot)。
GITEA_SYNC_PLAIN_KEYS = GITEA_SYNC_KEYS - GITEA_SYNC_SECRET_KEYS
# 必填键:缺一即 fail-closed(见模块头缺失语义)。
GITEA_SYNC_REQUIRED_KEYS = frozenset({
"GITEA_URL", "GITEA_USER", "GITEA_TOKEN", "SYNC_REPO",
})
# 宿主配置环境变量名 → guest-env 键名。
CHV_GITEA_ENV_MAP = {
"CHV_GITEA_URL": "GITEA_URL",
"CHV_GITEA_USER": "GITEA_USER",
"CHV_GITEA_TOKEN": "GITEA_TOKEN",
"CHV_SYNC_REPO": "SYNC_REPO",
"CHV_SYNC_BRANCH": "SYNC_BRANCH",
"CHV_GITEA_EMAIL": "GITEA_EMAIL",
}
def _clean(mapping: dict | None) -> dict:
"""只留白名单键:值须为非空字符串(strip 后),其它
类型/空值/未知键一律丢掉——调用方拼错的键名绝不静默
进 seed 变成客内无人读的脏变量。"""
cleaned = {}
for key, value in dict(mapping or {}).items():
if key not in GITEA_SYNC_KEYS:
continue
if not isinstance(value, str):
continue
value = value.strip()
if not value:
continue
cleaned[key] = value
return cleaned
def config_gitea_sync_env(env: dict | None) -> dict:
"""宿主配置环境变量(CHV_GITEA_*)→ guest-env 键名。"""
mapped = {}
for env_name, key in CHV_GITEA_ENV_MAP.items():
value = (env or {}).get(env_name)
if value is not None:
mapped[key] = value
return _clean(mapped)
def task_gitea_sync_env(task: dict | None) -> dict:
"""任务 dict 的 `gitea_sync` 子 dict → 白名单后的键值。
不是 dict(或缺失)即空——绝不把任务其它字段误读成
同步变量。"""
if not isinstance(task, dict):
return {}
return _clean(task.get("gitea_sync"))
def extract_gitea_sync_env(task: dict | None = None,
extra_env: dict | None = None,
config_sync: dict | None = None) -> dict:
"""三源合并(优先级:config_sync < 任务 gitea_sync <
extra_env 同名键)。返回白名单后的 dict,可能为空
(空 = 本次启动不同步)。"""
merged = {}
merged.update(_clean(config_sync))
merged.update(task_gitea_sync_env(task))
merged.update(_clean(extra_env))
return merged
def validate_gitea_sync_env(env: dict | None) -> None:
"""校验同步变量完整性:空即不同步(通过);非空但必填
键缺失即 ValueError(只含缺失键名,不含值)。调用方
(执行器 wake_vm)在起 VMM 前调本函数做 fail-closed。"""
env = dict(env or {})
if not env:
return
missing = sorted(GITEA_SYNC_REQUIRED_KEYS - set(env))
if missing:
raise ValueError(
"Gitea 同步变量不完整,缺失:%s(同步已请求但必填"
"键不全,拒绝启动)" % ",".join(missing))