后端文档【免费下载链接】readthedocs.orgThe source code that powers readthedocs.org项目地址https://gitcode.com/gh_mirrors/re/readthedocs.org点击查看免费下载在 Read the Docs 的构建流水线中用户无法执行任意命令但许多文档构建如 Sphinx autodoc、Jupyter Notebook 渲染确实需要额外安装 C 库等系统级依赖。本文以官方设计文档 system-packages.rst 为主线完整还原build.apt_packages这一配置项的设计动机、安全边界与最终落地形态并结合 配置校验、构建调度器 与 Docker 执行环境 的源码讲清楚这些 apt 包是如何在构建容器里被安全地安装起来的。一、问题背景构建过程为什么缺系统包设计文档开篇明确了当时的约束Read the Docs 不允许在构建流程中执行任意命令而最普遍的诉求其实是安装额外的依赖。文档记录了几个现实痛点Sphinx 有变通方案MkDocs 没有Sphinx 用户可以通过在conf.py里执行代码来曲线运行命令而使用 autodoc 或构建 Jupyter Notebook 时用户往往需要安装一些额外的依赖MkDocs 生态下这类问题反而更常见。部分依赖必须借助apt一些依赖需要 root 权限或者直接用apt安装更简单。大多数 CI 服务允许使用apt或以sudo执行任意命令用户已经熟悉这个工作流。Conda 只是部分人的解法有用户改用 Conda 来规避这些系统依赖问题但并非所有 pip 用户都熟悉 Conda也不希望仅仅为了使用 Read the Docs而迁移到 Conda。这些背景同样反映在面向用户的文档中faq.rst 在解释依赖 C 模块的库为什么报 import 错误时给出的标准答案就是先尝试用build.apt_packages安装缺失的 C 库如libevent、mysql。二、设计目标只开放apt install不开放任意命令设计文档的Security concerns一节点出了核心风险构建运行在 Docker 容器里但控制容器的应用与容器位于同一台服务器上。如果允许用户以超级用户身份执行任意命令就可能引入安全漏洞。因此设计结论是暂不开放 root 权限的任意命令执行而是只允许通过apt安装额外的包。具体的设计要点如下这些要点几乎全部被后续实现忠实继承通过配置文件暴露用户在配置文件中提供一份包名列表固定执行两条命令apt update -y与apt install -y {packages}执行时机在 clone 步骤之后、Python 环境搭建Python setup step之前包名必须校验防止用户注入额外选项例如-v这类会被 apt 解释为参数而非包名的内容借助docker exec的 user 参数当前实现就是用docker exec在运行中的容器里执行命令而docker exec本身支持指定执行用户——这意味着可以在现有容器里临时以超级用户身份运行apt命令而不必让整条构建链路都跑在 root 下。备选方案方面设计文档评估了两种更彻底的方向其一像 Travis CI 那样允许容器以 root 运行——通过一个工具把配置文件转成 shell 脚本、另一个工具拉起容器执行脚本并回传日志其二基于 AWS Lambda 的类似方案。文档对两者都标注了需要大量工作仅作为未来可能的演进方向——当前仓库的源码也印证了这一点主路径始终停留在限定 apt 包名 临时 root 执行的安全模型上没有引入任意脚本执行能力。三、配置文件的最终形态build.apt_packages设计文档最终选定的键名是build.apt_packages值为待安装的包名列表version: 2 build: apt_packages: - cmatrix - mysql-server设计文档明确记录了命名决策过程build.packages、build.extra_packages、build.system_packages三个候选名都被否决原因是要避免与配置文件中已有的键混淆并且要显式地表达包的类型——最终名apt_packages直接点明了这些是 apt/Debian 风格的系统包。面向用户的参考文档 config-file/v2.rstbuild.apt_packages一节在此基础上给出了完整约束类型list默认值[]官方示例version: 2 build: apt_packages: - libclang - cmake仓库限制构建服务器运行多个版本的 Ubuntu LTS只安装了默认的包仓库当前不支持 PPA 或其他自定义仓库——这正是设计文档Possible problems一节预见到的限制有些用户可能需要传额外 flag 或从 ppa 安装最终以明确不支持的方式落地使用建议尽可能不要用 apt 安装 Python 包如python3-numpy而应改用 pip 或 conda互斥限制当前build.apt_packages不能与build.commands一起使用build.commands是被build.jobs取代的旧式写法。四、源码级实现从配置解析到容器内执行4.1 配置校验两层防线阻止参数注入设计文档包名必须校验这一要求在 config.py 中落成了两层校验validate_apt_packages与validate_apt_package见 L438-L500第一层是类型校验build.apt_packages必须是一个列表否则抛出INVALID_LIST错误第二层是逐包名校验这正是对防止注入额外选项的完整实现包名先经validate_string确认为字符串并做strip()前缀黑名单以-开头的包名会被拒绝不允许伪装成 apt 的命令行选项、以/开头会被拒绝不允许从本地路径安装 .deb 文件、以.开头会被拒绝避免相对路径等歧义。命中前缀黑名单时抛出ConfigError.APT_INVALID_PACKAGE_NAME_PREFIX错误 id 定义在 exceptions.py面向用户的错误文案在 notifications.py 中字符白名单正则^[a-zA-Z0-9][a-zA-Z0-9.-]*$——包名必须以字母或数字开头后续只允许字母、数字、.、、-。这个字符集的依据在源码注释中写明参考了 Ubuntu 的apt-get(8)man page 与 Debian 参考手册中关于包名合法字符的规定。不匹配时抛出ConfigError.APT_INVALID_PACKAGE_NAME。一个值得注意的细节是分发版限定不被允许cmatrix/bionic这类指定来源版本的写法会因前缀/格式校验失败而被拒绝测试用例见 test_config.py L861# We dont allow specifying distributions for now.。这些校验规则有完整的测试覆盖见 test_config.pytest_build_apt_packages_check_valid[cmatrix]、[Mysql, cmatrix, postgresql-dev]等合法列表通过校验且build.build.apt_packages原样返回test_build_apt_packages_invalid_type3、string、{}等非列表值触发INVALID_LIST且错误定位到build.apt_packages键test_build_apt_packages_invalid_value参数化的非法包名触发APT_INVALID_PACKAGE_NAME_PREFIX错误定位精确到build.apt_packages.{index}这样的下标路径。4.2 数据模型apt_packages 与前后置 hooks校验通过后配置被组装进BuildWithOs模型models.py其中apt_packages: list[str] []与os、tools、jobs、commands平级存放。另一个设计文档中不存在、但实现里自然生长的扩展是BuildJobs模型为系统依赖步骤提供了前后置钩子models.py L46-L62class BuildJobs(ConfigBaseModel): pre_checkout: list[str] [] post_checkout: list[str] [] pre_system_dependencies: list[str] [] post_system_dependencies: list[str] [] ...也就是说用户可以在 apt 安装之前/之后各插入自定义命令这些自定义命令以普通构建用户身份运行而 apt 安装本身仍以 root 运行——两者的权限边界在下一小节可见。用户文档 builds.rst 也将system_dependencies描述为独立构建步骤安装操作系统与运行时依赖……包括语言特定版本以及 apt 包。4.3 执行时序clone 之后、Python 环境之前设计文档要求 apt 命令在 Python setup 步骤之前、clone 步骤之后运行。在构建调度器 director.py 中这一时序由setup_environment()精确保证L137-L177def setup_environment(self): 1. install OS dependencies (apt) 2. create language (e.g. Python) environment 3. install dependencies into the environment ... self.run_build_job(pre_system_dependencies) self.system_dependencies() self.run_build_job(post_system_dependencies) # Install all build.tools specified by the user self.install_build_tools() self.run_build_job(pre_create_environment) self.create_environment() ...而setup_environment()本身是在checkout()clone 读取.readthedocs.yaml与post_checkout任务之后才被调用的与设计文档描述的时序完全一致。核心的system_dependencies()方法L312-L346实现了设计文档中的两条命令并有若干超出设计文档的工程细节def system_dependencies(self): Install apt packages from the config file. We dont allow to pass custom options or install from a path. The packages names are already validated when reading the config file. .. note:: --quiet wont suppress the output, it would just remove the progress bar. packages self.data.config.build.apt_packages if packages: self.build_environment.run( apt-get, update, --assume-yes, --quiet, usersettings.RTD_DOCKER_SUPER_USER, ) # put -- to end all command arguments. self.build_environment.run( apt-get, install, --assume-yes, --quiet, --, *packages, usersettings.RTD_DOCKER_SUPER_USER, )与设计文档对照可以看到三处设计意图 → 实现细节的对应设计中的apt update -y/apt install -y被具体化为apt-get update/install --assume-yes --quiet并追加--作为参数终止符——这是双保险即使包名校验被绕过包名列表之后的任何内容也不会再被 apt 解释为选项源码注释强调--quiet只去掉进度条、不会吞掉输出保证构建日志中能看到 apt 的完整过程用户可据此排查安装失败方法上方注释特别说明system_dependencies不应被用户覆盖因为它以RTD_DOCKER_SUPER_USERroot身份执行——用户能影响的入口只有权限较低的pre/post_system_dependenciesjobs。测试 test_build_tasks.py 的 test_install_apt_packages 端到端验证了这一点配置apt_packages: [clangd, cmatrix]触发构建后断言容器内实际发出的命令序列就是apt-get update --assume-yes --quiet (userroot:root)与apt-get install --assume-yes --quiet -- clangd cmatrix (userroot:root)。4.4 docker exec 的 user 参数临时 root 是如何实现的设计文档Using docker exec一节提到的机制在 environments.py 的容器命令执行路径中实现命令通过 Docker API 的exec_create下发到运行中的容器userself.user将每次执行的身份透传给容器exec_cmd client.exec_create( containerself.build_env.container_id, cmdself.get_wrapped_command(), environmentself._environment, userself.user, workdirself.cwd, stdoutTrue, stderrTrue, )而RTD_DOCKER_SUPER_USER的具体取值定义在 settings/base.pyRTD_DOCKER_USER docs:docs RTD_DOCKER_SUPER_USER root:root RTD_DOCKER_WORKDIR /home/docs/这套设计的效果是构建的绝大部分命令checkout、jobs、pip 安装、sphinx 构建都以非特权用户docs:docs运行只有在system_dependencies()这一次、且仅针对已校验过的包名列表才临时切换到root:root。这与设计文档用 docker exec 的 user 参数在现有容器里临时以超级用户运行 apt 命令的方案一一对应也是不开放任意 root 命令这一安全承诺在代码上的落点。五、已知限制与后续观测设计文档Possible problems一节列出的限制在当前仓库中大多以明确的产品边界形式存在PPA/自定义仓库不支持config-file/v2.rst 明确声明构建服务器只带默认仓库不支持 PPA包名不能指定来源版本cmatrix/bionic式写法被校验直接拒绝见 4.1 节测试安装后的额外配置设计文档提到有些包安装后需要额外 setup——这一需求由post_system_dependenciesjob 承接但它以普通用户身份运行需要 root 的收尾操作仍无法完成与build.commands互斥用户文档中保留了这一警告设计文档提到的 Travis 式 root 容器 / AWS Lambda 方案未见落地属于文档中自述的需要大量工作的未来方向当前实现仍以限定 apt 临时 root 为唯一路径。此外实现还扩展了一个设计文档未涉及的可观测性能力遥测采集器 collectors.py 的_get_apt_packages会在构建结束后同时记录用户声明安装的 apt 包来自config.build.apt_packages与容器内全部已安装的 apt 包及版本通过dpkg-query --show采集把用户包与全量包区分开上报用于构建环境的可复现性分析与问题排查。六、小结build.apt_packages的演进过程是一个典型的受限能力开放案例设计文档从用户需要 apt 但平台不允许任意 root 命令这一矛盾出发用固定命令模板 包名白名单校验 docker exec 临时提权三件套在几乎不扩大攻击面的前提下补齐了系统依赖这一缺口。阅读 system-packages.rst 的设计思路再对照 config.py 的校验、director.py 的时序编排、environments.py 的 exec 提权与 test_build_tasks.py 的命令级断言可以完整看到一个设计决策是如何逐层落地为可验证的工程实现的。赞分享后端文档【免费下载链接】readthedocs.orgThe source code that powers readthedocs.org项目地址https://gitcode.com/gh_mirrors/re/readthedocs.org点击查看免费下载相关推荐一条链接、零服务器存储FilePizza 让浏览器直接 P2P 传大文件一条链接、零服务器存储FilePizza 让浏览器直接 P2P 传大文件 FilePizza 是一个浏览器 P2P 文件传输工具。发送方和接收方各自打开网页Read the Docs 平台源码研读从「Docs as Code」理念到文档构建流水线的实现Read the Docs 平台源码研读从「Docs as Code」理念到文档构建流水线的实现 本文以 Read the Docs 仓库的 README.r后端文档Read the Docs Pull Request 构建器设计从设计文档到 external version 的完整落地Read the Docs Pull Request 构建器设计从设计文档到 external version 的完整落地 Read the Docs 的「P后端文档上一篇SLADE编译与构建指南从源码到可执行文件的完整流程下一篇数据库高可用实战DBeaver自动故障转移配置指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考