- API网关
- 后端
- 云原生
【免费下载链接】tyk
Open Source API and AI Gateway supporting REST, GraphQL, TCP, gRPC and MCP (Model Context Protocol)
本文以 Tyk 开源网关仓库中的 ci/smoke-tests/README.md 为骨架,结合
smoke-tests/plugin-aliasing目录下的真实测试脚本、插件源码、API 定义与 docker-compose 编排,系统讲解 Tyk 冒烟测试的组织规范,并深入剖析"插件别名(plugin aliasing)"场景如何验证 Go 插件编译器、插件的多 API 复用,以及网关侧版本感知的.so插件加载机制。读完本文,你将能独立为 Tyk 编写一个以退出码报告成败的冒烟测试,并理解 goplugin 从编译到挂载再到运行的完整链路。
一、什么是 Tyk 的冒烟测试,它与常规 CI 测试有何区别
在 Tyk 仓库中,ci/smoke-tests/是一个特殊的测试目录。README.md 开篇就明确了它的定位:
The tests under this directory (
smoke-tests/) are smoke tests maintained by a squads to test a very specific functionality during the build. Regular tests that should be part of the ci process should go toci/tests/
也就是说,smoke-tests 由各个 squad(功能团队)维护,用于在构建过程中验证某个非常具体的功能是否可用;而应当纳入常规 CI 流程的回归测试、单元测试等,应放到ci/tests/目录。两者分工明确:
| 目录 | 定位 | 维护方 |
|---|---|---|
ci/smoke-tests/ | 构建期冒烟验证,聚焦单一特定功能 | 各功能 squad |
ci/tests/ | 常规 CI 测试(回归、集成等) | 全体开发 |
这种拆分保证了冒烟测试轻量、快速、聚焦,不会因覆盖全部功能而拖慢构建反馈。
二、如何编写一个冒烟测试:三条约定
README 用最简练的方式给出了冒烟测试的编写规范,总共三条:
- 为你的测试创建一个子目录(sub-directory),测试相关文件全部收敛在该目录内;
- 在该目录中实现一个
test.sh脚本,脚本负责运行测试,并通过退出码(exit code)报告失败——非零退出码即视为测试失败; - 如有必要,在本 README 中记录该测试(说明它验证什么、如何运行)。
仓库现状完全遵循这一约定:ci/smoke-tests/下仅有plugin-aliasing/一个测试目录,其中包含test.sh、docker-compose.yml以及foobar-plugin/、helloworld-plugin/两个插件子目录,目录结构如下:
ci/smoke-tests/ ├── README.md # 本说明文档(编写规范 + plugin-aliasing 简介) └── plugin-aliasing/ ├── test.sh # 冒烟测试入口脚本 ├── docker-compose.yml # 网关 + Redis + httpbin 编排 ├── foobar-plugin/ │ ├── main.go # 添加 Foo: Bar 请求头的插件 │ ├── go.mod / go.sum │ ├── README.md │ ├── foobar-plugin-1.json # 挂载 foobar 插件的 API 定义 1 │ └── foobar-plugin-2.json # 挂载 foobar 插件的 API 定义 2 └── helloworld-plugin/ ├── main.go # 添加 Hello: World 请求头的插件 ├── go.mod / go.sum ├── README.md ├── helloworld-plugin-1.json └── helloworld-plugin-2.json这条约定同样适用于你后续新增的任何冒烟测试:一个目录 + 一个test.sh+ 必要的资源文件,就构成了最小完备的冒烟测试单元。
三、plugin-aliasing 测试:验证什么
README 对plugin-aliasing测试的目标描述得非常明确,它是本文的核心案例:
- 使用合适的plugin-compiler(插件编译器)编译
foobar-plugin/main.go与helloworld-plugin/main.go两个 Go 插件; - 将
foobar-plugin与helloworld-plugin下的API 定义挂载进apps/(网关的 API 配置目录); - 用2 个 API 挂载 foobar 插件、2 个 API 挂载 helloworld 插件,从而同时验证两件事:
- 插件编译器可以一次编译多个插件(foobar 与 helloworld 分别独立编译成功);
- 同一个插件可以被多个 API 复用(同一份
.so被两个 API 定义同时引用并生效)。
两个插件的行为非常直观:
- foobar 插件:为所有请求添加请求头
Foo: Bar; - helloworld 插件:为所有请求添加请求头
Hello: World。
因此,验证方式就是在网关暴露的 4 个端点上分别发起请求,断言响应回显的请求头是否包含对应值。由于 httpbin 的/headers接口会回显收到的请求头,测试可以非常方便地进行断言。
四、插件源码剖析
两个插件的源码几乎同构,均位于仓库 ci/smoke-tests/plugin-aliasing/foobar-plugin/main.go 与 ci/smoke-tests/plugin-aliasing/helloworld-plugin/main.go。以 foobar 为例:
package main import ( "html/template" "net/http" // Example of package with different version in go.mod "github.com/Masterminds/sprig/v3" // Example of package which is not part of Gateway "github.com/kr/pretty" "github.com/TykTechnologies/tyk/ctx" "github.com/TykTechnologies/tyk/log" ) var logger = log.Get() // AddFooBarHeader adds custom "Foo: Bar" header to the request // //nolint:deadcode func AddFooBarHeader(rw http.ResponseWriter, r *http.Request) { r.Header.Add("Foo", "Bar") logger.Info("Test") api := ctx.GetDefinition(r) if api != nil { logger.Info("API Definition", pretty.Sprint(api)) } // Set up variables and template. tpl := `Hello {{.Name | trim | lower}}` // Get the Sprig function map. template.Must(template.New("test").Funcs(sprig.FuncMap()).Parse(tpl)) } func main() {}这段代码有几个值得注意的技术点:
- 导出函数即插件入口:
AddFooBarHeader(rw http.ResponseWriter, r *http.Request)是导出函数,签名与标准http.HandlerFunc一致,这正是 Tyk goplugin 约定识别的函数形态。main函数为空,仅用于满足 Go 可执行程序的要求。 - 插件可以依赖网关内部包:插件导入了
github.com/TykTechnologies/tyk/ctx与github.com/TykTechnologies/tyk/log,分别用于通过ctx.GetDefinition(r)获取当前请求对应的 API 定义(并借助kr/pretty格式化打印),以及获取网关日志器输出日志。这说明插件编译时与网关共享同一套 API 上下文,插件代码可以访问请求上下文中的网关数据。 - 依赖多样性验证:注释明确指出
sprig/v3是一个"与 go.mod 中版本不同的包"示例,kr/pretty是"不属于网关"的包示例——它们在编译期被解析并打进.so,用于验证插件编译器能正确处理网关外部依赖。 - 模板函数可用性:代码用
sprig.FuncMap()注册了 Sprig 模板函数并解析了一个模板(Hello {{.Name | trim | lower}}),虽然没有实际渲染输出,但确保插件在加载阶段不会因模板初始化失败而崩溃。
对应 go.mod 内容如下:
module github.com/TykTechnologies/tyk/smoke-tests/plugin-compiler/foobar-plugin go 1.22 require github.com/kr/pretty v0.3.1 // indirect插件模块声明为go 1.22,只显式 require 了kr/pretty,其余依赖(sprig、ctx、log)由 go.mod 的传递依赖解析。
helloworld 插件的差异仅在函数名与注入的请求头:AddHelloWorldHeader执行r.Header.Add("Hello", "World"),其余逻辑完全一致。两个插件各自维护独立的 go.mod / go.sum,互不干扰。
五、编译阶段:如何使用 plugin-compiler 编译插件
测试脚本 ci/smoke-tests/plugin-aliasing/test.sh 是整条链路的核心,它完成"准备镜像 → 编译插件 → 启动网关 → 请求断言"四步。先看它的镜像准备与编译部分:
#!/bin/bash set -eo pipefail function setup { local tag=${1:-"v0.0.0"} # Setup required env vars for docker compose export GATEWAY_IMAGE=${GATEWAY_IMAGE:-"tykio/tyk-gateway:${tag}"} export PLUGIN_COMPILER_IMAGE=${PLUGIN_COMPILER_IMAGE:-"tykio/tyk-plugin-compiler:${tag}"} docker pull -q $GATEWAY_IMAGE || true docker pull -q $PLUGIN_COMPILER_IMAGE || true } setup $1要点说明:
- 脚本接收一个版本参数(
./test.sh <version>),默认值为v0.0.0;README 强调该版本需在 Docker Hub 上可用,因为脚本会用docker pull拉取对应 tag 的tykio/tyk-gateway与tykio/tyk-plugin-compiler镜像。 - 两个镜像均可通过环境变量
GATEWAY_IMAGE、PLUGIN_COMPILER_IMAGE覆盖,便于在本地构建镜像后直接注入测试。 set -eo pipefail保证任一步骤失败都会立即中断脚本并返回非零退出码——这正是"用退出码报告失败"约定的落地实现。
随后脚本提取网关版本并清理旧的编译产物:
GATEWAY_VERSION=$(docker run --rm -t $GATEWAY_IMAGE --version 2>&1) GATEWAY_VERSION=$(echo $GATEWAY_VERSION | perl -n -e'/(\d+).(\d+).(\d+)/'' && print "v$1\.$2\.$3"') rm -rfv foobar-plugin/*.so helloworld-plugin/*.so这里用docker run ... --version从网关镜像中读出版本号,再用 perl 正则提取出vX.Y.Z形式的主版本三元组——该版本号稍后会被拼进插件文件名,是"版本感知插件命名"的关键输入。
接下来是两个插件的编译命令:
docker volume create plugin-aliasing-go-mod-cache docker volume create plugin-aliasing-go-build-cache cache_args="-v plugin-aliasing-go-mod-cache:/go/pkg/mod -v plugin-aliasing-go-build-cache:/root/.cache/go-build" docker run --rm -e GO_GET=1 $cache_args -v `pwd`/foobar-plugin:/plugin-source $PLUGIN_COMPILER_IMAGE foobar-plugin.so docker run --rm -e GO_GET=1 $cache_args -v `pwd`/helloworld-plugin:/plugin-source $PLUGIN_COMPILER_IMAGE helloworld-plugin.so要点说明:
- 两个插件分别独立编译:通过
-v $(pwd)/foobar-plugin:/plugin-source将插件源码目录挂载进 plugin-compiler 容器,容器输出.so文件写回宿主机插件目录,产物文件名由最后一个参数指定(foobar-plugin.so/helloworld-plugin.so)。 -e GO_GET=1指示编译器允许在编译过程中通过go get拉取依赖;- 通过两个命名 volume 缓存 Go 模块与构建缓存,加速重复构建,也体现了 CI 场景下的工程细节。
编译完成后,脚本把插件参数导出为环境变量,供 docker-compose 挂载使用:
# if params were not sent, then attempt to get them from env vars if [[ $GOOS == "" ]] && [[ $GOARCH == "" ]]; then GOOS=$(go env GOOS) GOARCH=$(go env GOARCH) fi # pass plugin params export plugin_version=${GATEWAY_VERSION} export plugin_os=${GOOS} export plugin_arch=${GOARCH} docker compose up -d --wait --force-recreate || { docker compose logs gw; exit 1; }plugin_version取自网关镜像版本,plugin_os/plugin_arch默认取本机go env的 GOOS/GOARCH(也可由外部环境变量覆盖)。docker compose up -d --wait --force-recreate启动并等待服务就绪,启动失败则输出网关日志并退出。
六、编排与加载:docker-compose 如何把插件挂进网关
ci/smoke-tests/plugin-aliasing/docker-compose.yml 定义了三个服务:redis(网关依赖的存储)、gw(被测网关)、httpbin.org(上游回显服务,使用kennethreitz/httpbin镜像)。网关服务的关键挂载如下:
gw: image: ${GATEWAY_IMAGE} volumes: - ./foobar-plugin/foobar-plugin_${plugin_version}_${plugin_os}_${plugin_arch}.so:/opt/tyk-gateway/middleware/foobar-plugin.so - ./helloworld-plugin/helloworld-plugin_${plugin_version}_${plugin_os}_${plugin_arch}.so:/opt/tyk-gateway/middleware/helloworld-plugin.so - ./helloworld-plugin/helloworld-plugin-1.json:/opt/tyk-gateway/apps/helloworld-plugin-1.json - ./helloworld-plugin/helloworld-plugin-2.json:/opt/tyk-gateway/apps/helloworld-plugin-2.json - ./foobar-plugin/foobar-plugin-1.json:/opt/tyk-gateway/apps/foorbar-plugin-1.json - ./foobar-plugin/foobar-plugin-2.json:/opt/tyk-gateway/apps/foorbar-plugin-2.json ports: - "0.0.0.0:8080:8080" environment: - TYK_LOGLEVEL=debug - TYK_DB_REDISHOST=redis这里有两类挂载,含义各不相同:
.so插件文件:宿主机上按{插件名}_{plugin_version}_{plugin_os}_{plugin_arch}.so命名(例如foobar-plugin_v5.0.0_linux_amd64.so)的产物,被挂载到容器内的普通文件名(/opt/tyk-gateway/middleware/foobar-plugin.so)。也就是说,docker-compose 负责把版本感知命名的文件"重命名"为 API 定义中引用的路径。- API 定义 JSON:4 个 API 定义被挂载进网关的
apps/目录,网关启动时自动加载这些 API。
同时,网关以TYK_LOGLEVEL=debug输出调试日志,TYK_DB_REDISHOST=redis指向同栈的 Redis 服务,端口 8080 暴露到宿主机供测试请求。
七、API 定义:一个插件如何挂到多个 API 上
以 foobar-plugin-1.json 为例,API 定义中与插件相关的核心片段是custom_middleware块:
"custom_middleware": { "pre": [], "post": [ { "name": "AddFooBarHeader", "path": "/opt/tyk-gateway/middleware/foobar-plugin.so", "require_session": false } ], "post_key_auth": [], "auth_check": {}, "response": [], "driver": "goplugin", "id_extractor": { "extract_from": "", "extract_with": "", "extractor_config": {} } }关键字段说明:
driver: "goplugin":声明中间件驱动为 Go 插件;post数组:定义 post 阶段执行的 Go 插件中间件,name对应插件导出的函数名(AddFooBarHeader),path指向容器内.so文件路径,require_session: false表示该中间件不要求会话(配合use_keyless: true的无鉴权模式);- 其余中间件槽位(
pre、post_key_auth、auth_check、response)均为空,测试聚焦 post 阶段的头注入。
API 定义的其余部分采用 keyless 直通配置:use_keyless: true、use_go_plugin_auth: false,proxy块将listen_path设为/goplugin-foobar-1/(第二个 API 为/goplugin-foobar-2/),target_url指向http://httpbin.org/,strip_listen_path: true。
对比 helloworld-plugin-1.json 与 helloworld-plugin-2.json,结构完全一致,仅name换为AddHelloWorldHeader、path指向helloworld-plugin.so、listen_path为/goplugin-helloworld-1//goplugin-helloworld-2。两个插件各被两份 API 定义引用,且每个插件只编译出一份.so——这正是"同一插件可被多个 API 复用"的验证载体。
八、运行与验证:curl + jq 断言
启动就绪后,脚本对 4 个端点逐一发起请求并断言响应头:
curl -vvv http://localhost:8080/goplugin-helloworld-1/headers curl http://localhost:8080/goplugin-helloworld-1/headers | jq -e '.headers.Hello == "World"' || { docker compose logs gw; exit 1; } curl -vvv http://localhost:8080/goplugin-helloworld-2/headers curl http://localhost:8080/goplugin-helloworld-2/headers | jq -e '.headers.Hello == "World"' || { docker compose logs gw; exit 1; } curl -vvv http://localhost:8080/goplugin-foobar-1/headers curl http://localhost:8080/goplugin-foobar-1/headers | jq -e '.headers.Foo == "Bar"' || { docker compose logs gw; exit 1; } curl -vvv http://localhost:8080/goplugin-foobar-2/headers curl http://localhost:8080/goplugin-foobar-2/headers | jq -e '.headers.Foo == "Bar"' || { docker compose logs gw; exit 1; }断言逻辑说明:
- 请求经网关转发到 httpbin,
/headers返回 JSON 格式的请求头回显; jq -e '.headers.Hello == "World"'在断言成立时输出true并以 0 退出,断言失败则以非零退出码触发||分支:打印网关日志并exit 1;- 4 组断言覆盖了"两个插件 × 各两个 API"的全部组合:只要任意一个 API 上的插件头注入失效(例如
.so未加载、插件符号找不到、API 定义未生效),测试即失败。
脚本末尾还有一处工程细节:
trap "docker compose down" EXIT无论测试成功还是失败,退出时都会通过 trap 自动拆除 docker compose 环境,避免 CI 机器上残留容器污染后续任务。
九、网关侧机制:版本感知的插件加载(源码佐证)
为什么.so文件要按{插件名}_{版本}_{OS}_{架构}.so命名?这与网关的插件加载机制直接相关。在 gateway/mw_go_plugin.go 中,GoPluginMiddleware负责加载并执行 Go 插件,其loadPlugin()的注释明确描述了加载策略:
loadPlugin loads the plugin file from m.Path, it will try converting it to the tyk version aware format:
{plugin_name}_{tyk_version}_{os}_{arch}.so... later, it will try with m.path which can be{plugin_name}.so
即网关会优先尝试加载版本感知命名的.so,找不到再回退到 API 定义中给出的原始路径。命名格式的具体实现在 goplugin/plugin_name_builder.go:
// getPluginNameFromTykVersion builds a name of plugin based on tyk version, // GOOS, and GOARCH of the build. The structure of the plugin name looks like: // {plugin-dir}/{plugin-name}_{GW-version}_{OS}_{arch}.so func getPluginNameFromTykVersion(version string, pluginPath string) string { ... // produce a `name_{version}_{goos}_{goarch}` for loading newPluginName := strings.Join([]string{pluginName, version, runtime.GOOS, runtime.GOARCH}, "_") newPluginPath := pluginDir + "/" + newPluginName + ".so" return newPluginPath }而 GetPluginFileNameToLoad 依次尝试三种形态:
- 新版命名格式且版本号带
v前缀:{name}_{v版本}_{goos}_{goarch}.so; - 新版命名格式但版本号不带
v前缀; - 直接使用 API 定义中给出的原始路径(例如
foobar-plugin.so)。
此外getPrefixedVersion()会为版本号补v前缀,并清理-rc15之类的后缀,确保插件名中的版本段是纯净的vX.Y.Z。
回到 smoke-test 场景,这条机制就形成了一条完整自洽的闭环:测试脚本用plugin_version、plugin_os、plugin_arch拼接出版本感知文件名供 docker-compose 挂载;docker-compose 将其"重命名"为 API 定义引用的普通路径;网关启动时loadPlugin()先按版本感知命名查找(容器内不存在),再回退到普通路径命中挂载文件。同时,插件文件名中的版本、OS、架构与网关构建信息一一对应,从机制上规避了插件与网关版本/平台不匹配导致的加载失败。
十、小结:新增一个冒烟测试的 Checklist
综合 README 约定与 plugin-aliasing 的工程实践,为 Tyk 新增冒烟测试时可遵循以下清单:
- 建目录:在
ci/smoke-tests/下创建以功能命名的子目录; - 写
test.sh:脚本必须以非零退出码报告失败(推荐set -eo pipefail+ 关键步骤|| exit 1),必要时用trap清理环境; - 准备资源:插件源码、API 定义 JSON、docker-compose 编排等全部收敛在测试目录内;
- 文档化:在 ci/smoke-tests/README.md 中补充一段"该测试验证什么、如何运行"的说明;
- 遵循职责边界:冒烟测试只覆盖构建期需快速验证的特定功能;需要长期回归的测试应放入
ci/tests/; - 理解加载约定:
.so命名遵循{插件名}_{网关版本}_{GOOS}_{GOARCH}.so,与网关 goplugin/plugin_name_builder.go 的加载逻辑保持一致。
plugin-aliasing 作为 Tyk 仓库中冒烟测试的样板案例,用不到百行脚本加 4 份 API 定义,就完整验证了"多插件编译、单插件多 API 复用、版本感知加载"三个关键能力,是理解 Tyk goplugin 体系从构建到运行时全链路的最佳起点。
- API网关
- 后端
- 云原生
【免费下载链接】tyk
Open Source API and AI Gateway supporting REST, GraphQL, TCP, gRPC and MCP (Model Context Protocol)
相关推荐
Mastra Server 部署验证测试指南:`--test server` 从部署到 API 冒烟验证
Mastra Server 部署验证测试指南: test server 从部署到 API 冒烟验证 本指南是 Mastra 项目冒烟测试体系中 server 专
人工智能Agent 框架AI AgentRAG后端Mastra MCP 冒烟测试指南:从 Studio 页面检查到 `/api/mcp` API 验证(`--test mcp`)
Mastra MCP 冒烟测试指南:从 Studio 页面检查到 /api/mcp API 验证( test mcp ) 导读 本文围绕 Mastra 仓库中
人工智能Agent 框架AI AgentRAG后端Deep-TEMPEST 快速上手指南:Conda 与 Pyenv 两种环境搭建全流程
Deep TEMPEST 快速上手指南:Conda 与 Pyenv 两种环境搭建全流程 Deep TEMPEST 是一个利用深度学习从 HDMI 电磁辐射中恢复
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考