Podman API v2 HTTP 测试框架完全指南:从运行到编写 `.at` 测试用例

发布时间:2026/9/20 11:31:12
Podman API v2 HTTP 测试框架完全指南:从运行到编写 `.at` 测试用例 Podman API v2 HTTP 测试框架完全指南从运行到编写.at测试用例【免费下载链接】podmanPodman: A tool for managing OCI containers and pods.项目地址: https://gitcode.com/gh_mirrors/po/podman本指南聚焦 Podman 仓库中test/apiv2/目录下的 API v2HTTP测试框架涵盖测试的运行方式、t测试函数的完整语法、URL 版本化规则、POST 参数与 JSON 断言机制并结合test-apiv2主脚本源码与大量.at测试实例帮助读者理解并编写可复现的 Podman REST API 测试用例。一、背景这个目录在测什么Podman 提供了两套 HTTP APIDocker 兼容 APIcompat API与 Docker Engine API 兼容的端点例如/containers/jsonlibpod 原生 APIPodman 独有的扩展端点例如/libpod/pods/create。test/apiv2/目录对应文档为 test/apiv2/README.md就是针对Podman 第二版 APIHTTP编写的端到端测试套件。它直接通过curl向运行中的podman system service发送真实 HTTP 请求校验返回状态码、响应头与 JSON 响应体从而验证 API 的行为、兼容性与错误处理。整个目录的组成见 test/apiv2 目录清单test-apiv2主测试运行脚本BashNN-NAME.at一组按主题划分的测试文件NN是两位数字编号NAME是描述性名称.at是测试文件的扩展名00-TEMPLATE新测试文件的空模板若干.conf文件测试专用的containers.conf变体如containers.host-netns.conf、containers.no_hosts.conf用于在不同默认配置下重启服务python/子目录基于 pytest 的 API 测试test_v2_0_0_*.py等与.at体系并行。二、运行测试2.1 基本用法主测试运行器是test-apiv2用法$ sudo ./test-apiv2 [NAME [...]]其中NAME是一个或多个可选的测试名例如image或pod也可以是两者。默认情况下不带参数test-apiv2会执行目录下所有*.at测试。带参数时NAME会作为通配符与*.at文件匹配脚本中对应的实现片段见 test-apiv2if [ -n $* ]; then shopt -s nullglob for i; do match(${TESTS_DIR}/*${i}*.at) if [ ${#match} -eq 0 ]; then die No match for $TESTS_DIR/*$i*.at fi ... done else tests_to_run($TESTS_DIR/*.at) fi因此sudo ./test-apiv2 20只会运行20-containers.atsudo ./test-apiv2 1会匹配01-basic.at和10-images.at等所有文件名中包含1的文件。2.2 网络与连接约束test-apiv2只连接 localhost且只通过 TCP。框架不支持远程主机也不支持 UNIX socket——这是一个用于测试 API 本身而非所有可能的传输协议的框架。默认端口由环境变量PODMAN_SERVICE_PORT控制缺省为8081见脚本中的PORT${PODMAN_SERVICE_PORT:-8081}。2.3 服务自动启动与清理test-apiv2会启动服务若尚未运行脚本通过start_service检查端口上是否已有监听者利用 Bash 的/dev/tcp/$HOST/$PORT探测没有监听者时用测试专用的存储根目录启动podman system service --time 0 tcp:127.0.0.1:$PORT服务使用--root $WORKDIR/server_root隔离存储避免污染开发机上的真实容器数据测试结束后clean_up_server会podman rm -a、podman rmi -af、停止服务并清理临时目录。2.4 运行前依赖脚本开头的 sanity check 强制要求三个工具存在否则直接报错退出for tool in curl jq podman; do type $tool /dev/null || die $ME: Required tool $tool not found done因此运行测试前需要确保系统安装了curl、jq和podman。2.5 可配置的环境变量从 test-apiv2 脚本头部可以看到一组可自定义的环境变量you can but probably shouldnt customize环境变量默认值作用PODMAN_TEST_IMAGE_REGISTRYquay.io测试镜像的 registryPODMAN_TEST_IMAGE_USERlibpod测试镜像的命名空间PODMAN_TEST_IMAGE_NAMEtestimage测试镜像名PODMAN_TEST_IMAGE_TAG20241011测试镜像标签PODMAN_SERVICE_PORT8081API 服务监听端口PODMAN自动探测podman 二进制路径CONTAINERS_HELPER_BINARY_DIR../../bin外部辅助二进制如rootlessport目录CI_USE_REGISTRY_CACHE空非空时改用registries-cached.confCONTAINERS_STORAGE_CONF空覆盖存储配置PODMAN_TESTS_DUMP_TRACES空非空时向日志倾倒原始 curl 请求/响应字节PODMAN_TESTS_KEEP_WORKDIR空非空时保留测试临时工作目录APIV2_TEST_EXPECT_TIMEOUT空期望 curl 超时的秒数配合状态码999使用测试镜像默认为quay.io/libpod/testimage:20241011脚本中拼出的PODMAN_TEST_IMAGE_FQN。注意测试运行前需要能拉取该镜像CI 环境下会使用本地缓存 registry。三、编写测试t函数完全语法所有.at测试的核心是t函数。它向服务器发起curl请求有 POST 参数时附带请求体并比较返回的状态码与可选的字符串结果。3.1 基本形式t GET /_ping 200 OK ^^^ ^^^^^^ ^^^ ^^ | | | --- 期望的字符串结果 | | ------- 期望的返回码 | -------------- 访问的端点 ------------------ 方法 (GET, POST, DELETE, HEAD)例如 01-basic.at 中最基础的探活测试t GET /_ping 200 OK t HEAD /_ping 200 t GET /libpod/_ping 200 OK t HEAD /libpod/_ping 200第二个示例来自 READMEt POST libpod/volumes/create namefoo 201 .ID~[0-9a-f]\{12\} ^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^ ^^^ ^^^^^^^^^^^^^^^^^^^^ | | | JSON .ID: 期望 12 位十六进制 | | -- 期望的状态码 | ----------- POST 参数 --------------------------------- 注意这里没有前导斜杠3.2 URL 规范化规则v1.40 / v1.44 与 libpod 前缀t对端点路径做了便捷处理若端点以/开头如/_pingt原样保留若没有前导斜杠t会自动补上 API 版本前缀。README 中描述的前缀为/v1.40但当前仓库源码test-apiv2 的 URL 构造逻辑以及 01-basic.at 头部的注释实际使用的是urlhttp://$HOST:$PORT case $path in /*) url$url$path ;; libpod/*) url$url/v6.0.0/$path ;; *) url$url/v1.44/$path ;; esac即以/开头 → 原样使用如/v1.40/containers/foo/attach?...以libpod/开头无前导斜杠→ 自动补/v6.0.0/前缀其他无前导斜杠路径 → 自动补/v1.44/前缀。这也是为什么 README 示例中libpod/volumes/create无前导斜杠可以直接书写而完整 URL 实际是http://localhost:8081/v6.0.0/libpod/volumes/create。撰写新测试时建议遵循无前导斜杠、让t补版本前缀的惯例这样测试会随着 API 版本的演进自动适配若需要显式测试某个历史版本端点则写带斜杠的完整路径如 20-containers.at 中的t POST /v1.40/containers/foo/attach?...。3.3 POST 参数当方法是POST时端点之后可以跟一个或多个keyvalue形式的参数用空格分隔。t会把参数列表转换为 JSON 形式传给服务器t POST myentrypoint 200 ! 无参数 t POST myentrypoint id$id 200 ! 只有一个参数 t POST myentrypoint id$id filter{foo:bar} 200 ! 两个参数其中一个是 JSON t POST myentrypoint name$name badparam[foo,bar] 500 ! 等等参数转换由脚本中的jsonify函数完成它把foobar拆成键值对并根据右侧值的形态决定是否加引号——true/false变成 JSON 布尔值纯数字原样保留已含引号的字符串按原样处理其余加双引号最终组装成{foo:bar,x:y}这样的 JSON 请求体。一个数值型状态码会终止 POST 参数的处理t会从命令行中不断吸收keyvalue形式的参数直到遇到一个三位数字的状态码[1-9][0-9][0-9]其后的所有参数都被视为期望的字符串结果。3.4 文件上传.tar/.yaml/.json与 Content-Type作为特殊情形当某个 POST 参数是以.tar、.yaml或.json结尾的字符串时t会用curl --data-binary PATH发送文件内容并自动设置对应的Content-type。这对build等端点非常有用t POST myentrypoint /mytmpdir/myfile.tar application/foo 400若要覆盖Content-type只需额外传入一个匹配application/*的字符串参数如上面的application/foo。类似地PUT 方法会改用--upload-file脚本中的_add_curl_args分支--data-binary $1vs--upload-file $1。此外脚本还支持-作为 POST 参数表示空请求体避免 curl 默认发送{}例如 10-images.at 中的t POST images/create?fromSrc-repomyimagetagmytag - 200--form...以multipart/form-data方式提交例如上传 tar 归档到 artifact 端点。3.5 期望结果与 JSON 断言最后一个或多个参数是期望的字符串结果普通字符串与服务器响应体做精确字符串比较以.开头的参数t会先对响应输出调用jq取出该字段再与参数等号右侧的值比较分隔符是等号→ 要求精确匹配分隔符是~波浪号→ 使用expr做正则匹配。来自 01-basic.at 的实例t GET /version 200 \ .Components[0].NamePodman Engine \ .Components[0].Details.APIVersion~6[0-9.-]\ \ .Components[0].Details.MinAPIVersion4.0.0 \ .ApiVersion1.44 \ .MinAPIVersion1.24 \ .Oslinux这里精确断言Podman Engine、1.44等值~则用正则匹配 API 版本号的形态。除了与~脚本还支持!字段不等于某值用于负向断言例如 20-containers.at 中验证内存上限未被 cgroup 的无穷大值污染t GET libpod/containers/$CTRNAME/stats?streamfalse 200 \ .memory_stats.limit!18446744073709552000jq表达式支持任意合法的 jq 语法例如取数组元素、管道运算等如 40-pods.at 中的.Containers\|length1与 20-containers.at 中的.Processes.[0].[7]sleep 25。3.6 期望 curl 超时若测试期望curl超时例如验证logs?followtrue在容器不产生日志时持续挂起使用APIV2_TEST_EXPECT_TIMEOUT环境变量并传入特殊状态码999APIV2_TEST_EXPECT_TIMEOUT5 t POST /foo 999脚本会为 curl 加上-m $APIV2_TEST_EXPECT_TIMEOUT超时参数并检查 curl 的退出码是否等于 28超时。20-containers.at 中有完整实例APIV2_TEST_EXPECT_TIMEOUT1 t GET containers/${CTRNAME}/logs?followtruestdouttruestderrtrue 999 is $($WORKDIR/curl.result.out) Container MUST NOT log output3.7 红线永远不要exitREADME 中有一句语气强烈的告诫Never, ever, ever, seriouslyEVERexitfrom a test. Just dont.测试文件中严禁调用exit——这会跳过清理逻辑把系统留在损坏状态。t的失败断言not ok由内部计数器处理并继续执行后续测试需要中止时应该依赖脚本的die/err_handler统一清理而不是自行exit。四、测试文件组织与覆盖范围测试文件按两位数字编号组织从 00-TEMPLATE 模板扩展而来。当前仓库包含的测试文件见 test/apiv2 目录清单文件主题01-basic.at基础探活/_ping、/version、404/405 错误处理、/info响应时间、events10-images.at/12-imagesMore.at镜像列表、inspect、history、pull、manifest 与 VirtualSize 兼容性14-commit.at容器 commit15-manifest.atmanifest 相关端点19-stats.at容器 stats20-containers.at容器生命周期create/start/stop/kill/wait/top/logs/attach/prune/update 等21-restart.at/22-stop.at重启与停止23-containersArchive.at归档copy端点25-containersMore.at/26-containersWait.at/27-containersEvents.at更多容器端点、wait、events28-containersAnnotations.at/29-containersUpdate.at注解与 update30-volumes.at卷的创建、列表、inspect、过滤器35-networks.at/47-subnet-pools.at网络与子网池36-quadlets.atQuadlet 相关37-autoupdate.at自动更新40-pods.atPod 生命周期create/exists/json/start/stop/restart/pause/stats44-mounts.at挂载45-system.atsystem/df、system/prune、libpod/info50-secrets.atsecrets60-auth.at认证registry auth70-short-names.at短名称解析80-kube.atKubernetes YAML 相关/libpod/kube4.1 一个完整测试的解剖01-basic.at01-basic.at 是最早、最基础的测试If any of these fail, life is bad。它验证探活/_ping、/libpod/_ping的 GET/HEAD 返回 200未文档化的 Docker API 响应头通过like检查Ostype:头对应 issue #19767版本信息/version与version无前导斜杠自动补/v1.44返回 Podman Engine 组件、API 版本形态、最小 API 版本等垃圾请求/nonesuch返回 404、不存在的容器路径返回 404方法不允许对/_ping用 POST/DELETE 返回 405对info用 POST 返回 405对containers/create用 GET 返回 405系统信息与响应时间/info检查OSType、DefaultRuntimecrun、MemTotal并对连续 10 次/info请求计时验证服务保持响应want10秒events 端点events?streamfalsesince30s与libpod/events?streamfalsesince30s返回 200。其中like是脚本内置的另一个断言函数expr $actual : $expect做正则比较失败时会显示# expected: ...与# actual: ...便于定位。4.2 容器端点的纵深实例20-containers.at20-containers.at1117 行是覆盖最广的测试文件覆盖大量真实业务场景干净起点断言t GET libpod/containers/json (at start: clean slate) 200 [] length0注意端点路径里可以带括号注释t会剥离路径中的描述性文字attach 与响应头版本差异用/v1.40/、/v1.42/、/v4.6.0/libpod/、/v4.7.0/libpod/多个版本路径请求 attach验证Content-Type从application/vnd.docker.raw-stream到application/vnd.docker.multiplexed-stream的演进以及连接升级101 Upgrade: tcp容器 kill 语义运行中 kill 返回 204已退出容器 kill 返回 409非法参数containers/json?allgarb1age返回 500且.causeschema: error converting value for \all\——这是 schema 校验错误信息被原样暴露到 API 的证明列表参数limit、lastlimit的别名对应 issue #6413、filters含{id:[...]}、{status:[running]}、label与label!正反过滤commitlibpod/commit与 compatcommit端点的 tag/author/comment/changes 参数以及 messages are only compatible with the docker image format 的 500 报错容器状态机created → initialized → running → paused → exited 各状态在 compat 端点State.Status中的映射对应 issue #14700验证 Docker 兼容性healthcheckcompat create 的默认值Interval/Timeout 30000000000ns、Retries 3、带空格参数保留、空Test继承镜像 healthcheck 的三种行为资源限制NanoCpus、Ulimits含-1无限制、MemorySwappiness、tmpfs/bind 挂载选项、update端点Memory/CPU/Device 读写限速generate spec作为 API 输入podman generate spec -f ...导出的 spec 直接 POST 给libpod/containers/create创建容器验证 CLI 与 API 的规格互通多标签镜像容器Config.Image保持创建时指定的多标签名issue #8547。这类测试不仅验证能通还验证了错误信息、响应头、时间戳精度/logs?timestampstrue需包含 9 位纳秒时间戳等细节。4.3 Pod 与系统端点40-pods.at、45-system.at40-pods.at 覆盖 Pod 生命周期create重复创建返回 409pod already exists、exists204/404、jsonContainers\|length1、start/stop/restart幂等时返回 304、pause/unpause、pods/stats?alltrue、pods/stats?namesOrIDsfoo以及--all, --latest and arguments cannot be used together这类参数冲突的 500 错误。45-system.at 验证system/df在三个不同 API 版本下的返回结构差异t GET system/df 200 {LayersSize:0,Images:[],Containers:[],Volumes:[],BuildCache:[]} t GET /v1.52/system/df 200 {ImageUsage:{},ContainerUsage:{},VolumeUsage:{},BuildCacheUsage:{}} t GET libpod/system/df 200 {ImagesSize:0,Images:[],Containers:[],Volumes:[]}并通过创建卷、挂载卷的容器、删除容器验证UsageData.RefCount从 0 → 1 → 0 的变化以及LayersSize/ImagesSize的精确字节值。同时验证VirtualSize字段在 v1.43 中存在、在 v1.44 中被移除的向后兼容契约这一模式在 10-images.at 中也有对镜像VirtualSize、ContainerConfig字段的版本化断言。五、深入test-apiv2脚本测试框架的底层机制5.1 计数与失败跟踪测试计数与失败数保存在工作目录的文件.testcounter、.failures而非变量中因为变量无法从子 shell 带回父 shell.at文件是被source进主脚本执行的。_bump负责递增计数_show_ok输出 TAP 风格的ok N .../not ok N ...行失败时附带期望值与实际值。最终输出1..$test_count作为 TAP 计划行并以失败数作为退出码。5.2 日志系统每次运行的 HTTP 请求/响应都记录到$LOGBASE/tmp/test-apiv2.log并用ln -sf让.log始终指向最新一次运行。日志包含每个请求的完整 curl 命令行响应头剥离\r和空行响应体JSON 响应经jq .格式化以便阅读二进制响应application/octet-stream显示file判定的文件类型每个请求的X-Response-Timetime_total设置PODMAN_TESTS_DUMP_TRACES时还会以od -t x1c倾倒原始 stdout/stderr 字节便于排查不可见字符如 attach 流中的控制字节。5.3 内置 registrystart_registry会在随机端口random_port默认 5001-5999用 podman 启动一个本地 registry 容器quay.io/libpod/registry:2.8.2并生成自签证书支持authnone与authhtpasswd两种认证模式后者生成随机用户名/密码并写入 htpasswd 文件。stop_registry --cleanup在测试结束时停止并删除 registry 容器与镜像。注意random_port故意避开 5000 端口因为 test/registries.conf 中 5000 是受信任的端口而无 SSL 的 push 必须失败。5.4 rootless 支持root()/rootless()两个辅助函数通过查询GET /v1.40/info的.Rootless字段判断服务运行模式测试可以用if root; then ... fi对 root/rootless 行为做差异化断言如 20-containers.at 中 root 下网络模式为bridge、rootless 下为pasta。脚本还会在 rootless 下先执行一次podman unshare true初始化命名空间规避 system-service 首次运行时的 fork-exec 竞态。六、写测试的最佳实践综合 README 与源码编写.at测试时建议遵循以下实践从00-TEMPLATE复制起步模板只包含文件头注释与# vim: filetypesh结尾保持格式统一端点路径尽量不写前导斜杠让t自动补/v1.44或/v6.0.0/libpod前缀只有需要钉死历史版本行为时才写完整路径充分利用/~/!三种断言精确值用形态校验UUID、时间戳、十六进制用~负向断言用!正则中的\需转义如[0-9a-f]\\{64\\}通过podman辅助函数准备前置状态脚本提供了podman包装函数使用与服务器相同的--root和 registry 配置用于拉镜像、创建容器等准备工作用if root区分运行模式对网络、cgroup 等 root/rootless 行为不同的场景做分支断言测试结束清理干净删除创建的容器、镜像、卷、网络否则会影响后续测试文件为回归缺陷写注释现有测试大量标注了 issue 编号如#12904、#8547、#14700、#15765、#25026等这既是回归记录也便于追溯历史 bug 的修复语义绝不exit让失败断言自然记录交给框架统一清理与退出。七、小结test/apiv2/是 Podman 第二版 HTTP API 的端到端验证阵地它用极简的t函数把curljq的断言能力封装成一行式测试 DSL覆盖 Docker 兼容 API 与 libpod 原生 API 的探活、生命周期、错误处理、版本兼容契约与 rootless 差异。无论是为 Podman 贡献新的 API 端点、修复回归缺陷还是想深入理解 Podman REST API 的响应细节阅读并运行这套.at测试都是最直接的入口。入门路径建议先运行sudo ./test-apiv2 01体验基础探活测试再阅读 01-basic.at 与 test-apiv2 对照理解t的每个参数最后用 00-TEMPLATE 为你的新端点编写第一个测试用例。【免费下载链接】podmanPodman: A tool for managing OCI containers and pods.项目地址: https://gitcode.com/gh_mirrors/po/podman创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考