☰
编写 Tekton Resolver 完全指南:从硬编码 Demo 到自定义远程资源解析器
2026/9/25 20:13:49 网站建设 项目流程
  • 云原生
  • CI/CD
  • DevOps
  • 后端

【免费下载链接】pipeline

A cloud-native Pipeline resource.

项目地址:https://gitcode.com/gh_mirrors/pipelin/pipeline
点击查看免费下载

本文以 docs/how-to-write-a-resolver.md 为主体骨架,结合仓库中真实的框架源码、官方resolver-template模板与 Git Resolver 实现,从零讲解如何编写一个可部署、可验证的 Tekton Resolver。

Tekton Resolution 是 Tekton Pipelines 生态中的一项核心能力:它允许用户通过pipelineRef/taskRef直接引用存放在远程位置(Git 仓库、OCI Bundle、HTTP、Hub 等)的Task与Pipeline,而 Resolver 正是负责"按需拉取远程 YAML 并交还给 Tekton Pipelines"的插件程序。本文将以编写一个返回硬编码 YAML 的最小 Resolver 为例,完整走一遍从项目初始化、实现framework.Resolver接口、编写部署清单到端到端验证的每一步,并深入到仓库源码中解释每个方法背后的路由、校验与返回契约。读完本文,你将掌握编写自有 Resolver 的全部要点,并能直接复用仓库中现成的模板快速起步。

什么是 Resolver?

在深入代码之前,先明确概念:Resolver 是一个运行在 Kubernetes 集群中、与 Tekton Pipelines 协同工作的程序,它的职责是把"远程资源请求"解析为具体的Task/PipelineYAML 内容。举例来说,如果用户提交的PipelineRun需要引用一个存放在 Git 仓库中的 Pipeline YAML,那么负责从 Git 拉取该文件并返回给 Tekton Pipelines 的就是一个 Resolver。

这一模式的价值在于可扩展性:支持新的版本控制系统、云存储桶或其他存储后端时,开发人员只需要编写一个新的 Resolver,完全不需要改动 Tekton Pipelines 本身。Resolver 通过实现框架定义的统一接口接入集群,框架负责处理控制器样板代码、请求路由、状态更新等公共逻辑(见 pkg/remoteresolution/resolver/framework/controller.go 中NewController的注释:"This sets up a lot of the boilerplate that individual resolvers shouldn't need to be concerned with")。

如果你想先看现成的完整可运行示例,可以直接查看仓库中的 docs/resolver-template/ 目录——它是本文全部代码的落盘版本,内置了demo类型的 Resolver 以及对应的部署与测试 YAML。关于如何在PipelineRun中指定远程 Pipeline,可参考 docs/pipelineruns.md。

前置条件

动手之前,需要具备以下基础:

  • Go 开发能力,并大致理解 Tekton Resolution 的工作方式;
  • 安装了kubectl和ko的电脑;
  • 一个Kubernetes 1.28+集群(本地开发使用kind集群即可);使用kind时,请确保KO_DOCKER_REPO环境变量设置为kind.local;
  • 一个可推送镜像的镜像仓库;
  • 集群中已安装Tekton Pipelines 与远程解析能力,安装说明见 docs/install.md。

需要特别注意的是,仓库中真实部署的远程解析器运行在tekton-pipelines-resolvers命名空间(见 config/resolvers/resolvers-deployment.yaml),其 Deployment 以ko://前缀的镜像启动,并对环境变量、ServiceAccount 有约定要求。我们自己的 Resolver 也要遵循同样的约定(详见下文"部署配置"一节)。

第一步:初始化项目结构

与普通 Go 服务一样,Resolver 本质上就是"运行在集群里的一个程序"。首先创建项目目录、初始化 Go module 并建立两个子目录:

$ mkdir demoresolver $ cd demoresolver $ go mod init example.com/demoresolver $ mkdir -p cmd/demoresolver $ mkdir config

其中cmd/demoresolver存放 Resolver 的程序代码,config目录最终会存放部署到 Kubernetes 的 YAML 清单。仓库中的 docs/resolver-template/ 正是按此结构组织的:cmd/demoresolver/与cmd/resolver/分别对应新旧两套框架的完整实现,config/demo-resolver-deployment.yaml为部署清单,test-resolver-template.yaml为测试用ResolutionRequest。

第二步:初始化 Resolver 二进制

Resolver 本身不需要任何命令行参数或特殊环境变量,因此main.go只需极少量样板代码。创建cmd/demoresolver/main.go:

最新框架(推荐):

package main import ( "context" "github.com/tektoncd/pipeline/pkg/remoteresolution/resolver/framework" "knative.dev/pkg/injection/sharedmain" ) func main() { sharedmain.Main("controller", framework.NewController(context.Background(), &resolver{}), ) } type resolver struct {}

旧框架(已废弃):

package main import ( "context" "github.com/tektoncd/pipeline/pkg/resolution/resolver/framework" "knative.dev/pkg/injection/sharedmain" ) func main() { sharedmain.Main("controller", framework.NewController(context.Background(), &resolver{}), ) } type resolver struct {}

这段代码还不能编译,先下载依赖:

# 视 Go 版本不同,可能不需要 -compat 标志 $ go mod tidy -compat=1.17

仓库模板 docs/resolver-template/cmd/resolver/main.go 在主函数中多做了一步:通过filteredinformerfactory.WithSelectors(context.Background(), v1beta1.ManagedByLabelKey)为 informer 注入标签选择器,再以sharedmain.MainWithContext启动,这样控制器只会监听由 Tekton 管理的资源,减少不必要的集群事件。虽然我们的最小示例可以省略,但这是真实 Resolver 的推荐写法。

第三步:实现 framework.Resolver 接口

此时执行go build -o /dev/null ./cmd/demoresolver会得到如下编译错误:

cmd/demoresolver/main.go:11:78: cannot use &resolver{} (type *resolver) as type framework.Resolver in argument to framework.NewController: *resolver does not implement framework.Resolver (missing GetName method)

这是因为我们已经定义了resolver类型,但它尚未实现framework.Resolver接口。新版接口定义在 pkg/remoteresolution/resolver/framework/interface.go:

type Resolver interface { Initialize(ctx context.Context) error GetName(ctx context.Context) string GetSelector(ctx context.Context) map[string]string Validate(ctx context.Context, req *v1beta1.ResolutionRequestSpec) error Resolve(ctx context.Context, req *v1beta1.ResolutionRequestSpec) (framework.ResolvedResource, error) }

旧版接口定义在 pkg/resolution/resolver/framework/interface.go,其ValidateParams方法接收的是[]pipelinev1.Param。两套接口的差异详见后文"新老框架对照"一节。下面逐个实现这些方法。

Initialize:初始化依赖

Initialize在 Resolver 控制器实例化时被调用,适合初始化需要的基础库、资源 lister 等前置条件。本示例不需要任何依赖,直接返回nil:

// Initialize sets up any dependencies needed by the resolver. None atm. func (r *resolver) Initialize(context.Context) error { return nil }

作为对比,仓库中的 Git Resolver(pkg/remoteresolution/resolver/git/resolver.go)在Initialize中获取 kube client、日志器,并初始化了一个 1024 条、TTL 5 分钟的 LRU 缓存与 SCM 客户端工厂——可见这里是"干正事"的地方。

GetName:返回 Resolver 名称

GetName返回一个字符串名称,用于在日志等场景中标识该 Resolver。本示例返回"Demo":

// GetName returns a string name to refer to this resolver by. func (r *resolver) GetName(context.Context) string { return "Demo" }

Git Resolver 的对应实现返回"Git"(见 pkg/remoteresolution/resolver/git/resolver.go 中的gitResolverName常量)。

GetSelector:请求路由标签

GetSelector返回一组标签键值对,框架据此把请求路由到当前 Resolver。我们只关心tektoncd/resolution这一标签:

// GetSelector returns a map of labels to match requests to this resolver. func (r *resolver) GetSelector(context.Context) map[string]string { return map[string]string{ common.LabelKeyResolverType: "demo", } }

common.LabelKeyResolverType定义于 pkg/resolution/common/labels.go,其值为"resolution.tekton.dev/type"。上面的代码意味着:任何带有resolution.tekton.dev/type: demo标签的ResolutionRequest对象都会被路由到我们的示例 Resolver。

同时需要在文件顶部补充 import:

最新框架:

import ( "context" // Add this one; it defines LabelKeyResolverType we use in GetSelector "github.com/tektoncd/pipeline/pkg/resolution/common" "github.com/tektoncd/pipeline/pkg/remoteresolution/resolver/framework" "knative.dev/pkg/injection/sharedmain" pipelinev1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" )

旧框架:

import ( "context" // Add this one; it defines LabelKeyResolverType we use in GetSelector "github.com/tektoncd/pipeline/pkg/resolution/common" "github.com/tektoncd/pipeline/pkg/resolution/resolver/framework" "knative.dev/pkg/injection/sharedmain" pipelinev1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" )

Validate / ValidateParams:校验请求参数

Validate负责检查ResolutionRequest携带的解析规范(resolution spec)是否合法。我们的示例 Resolver 不期望任何参数,因此首先拒绝所有带params的请求;同时它期望url的格式为demoscheme://<path>,因此对 URL 的 scheme 与路径进行校验。旧框架中对应方法是ValidateParams。

最新框架:

// Validate ensures that the resolution spec from a request is as expected. func (r *resolver) Validate(ctx context.Context, req *v1beta1.ResolutionRequestSpec) error { if len(req.Params) > 0 { return errors.New("no params allowed") } url := req.URL u, err := neturl.ParseRequestURI(url) if err != nil { return err } if u.Scheme != "demoscheme" { return fmt.Errorf("Invalid Scheme. Want %s, Got %s", "demoscheme", u.Scheme) } if u.Path == "" { return errors.New("Empty path.") } return nil }

别忘了在 import 中加入net/url(别名neturl)、fmt与errors。注意:这里的req.URL字段属于ResolutionRequestSpec——该结构体定义于 pkg/apis/resolution/v1beta1/resolution_request_types.go,包含Params []pipelinev1.Param与URL string两个字段,其中URL目前处于 ALPHA 稳定性级别。

旧框架(已废弃):

// ValidateParams ensures that the params from a request are as expected. func (r *resolver) ValidateParams(ctx context.Context, params []pipelinev1.Param) error { if len(req.Params) > 0 { return errors.New("no params allowed") } return nil }

需要补充"errors"包 import。仓库模板的最新版实现 docs/resolver-template/cmd/resolver/main.go 与上述代码一致:先拒绝所有参数,再通过neturl.ParseRequestURI校验url并强制demoschemescheme。

Resolve:核心解析逻辑

Resolve负责真正"干活"——拉取文件内容并返回。它接收解析请求的 spec 作为输入,返回一个framework.ResolvedResource接口值。由于 Tekton Pipelines 目前只支持通过远程解析获取 Pipeline 资源,我们返回一段硬编码的 Pipeline YAML:

最新框架:

// Resolve uses the given resolution spec to resolve the requested file or resource. func (r *resolver) Resolve(ctx context.Context, req *v1beta1.ResolutionRequestSpec) (framework.ResolvedResource, error) { return &myResolvedResource{}, nil } // our hard-coded resolved file to return const pipeline = ` apiVersion: tekton.dev/v1beta1 kind: Pipeline metadata: name: my-pipeline spec: tasks: - name: hello-world taskSpec: steps: - image: alpine:3.15.1 script: | echo "hello world" ` // myResolvedResource wraps the data we want to return to Pipelines type myResolvedResource struct {} // Data returns the bytes of our hard-coded Pipeline func (*myResolvedResource) Data() []byte { return []byte(pipeline) } // Annotations returns any metadata needed alongside the data. None atm. func (*myResolvedResource) Annotations() map[string]string { return nil } // RefSource is the source reference of the remote data that records where the remote // file came from including the url, digest and the entrypoint. None atm. func (*myResolvedResource) RefSource() *pipelinev1.RefSource { return nil }

旧框架(已废弃):

// Resolve uses the given params to resolve the requested file or resource. func (r *resolver) Resolve(ctx context.Context, params []pipelinev1.Param) (framework.ResolvedResource, error) { return &myResolvedResource{}, nil } // our hard-coded resolved file to return const pipeline = ` apiVersion: tekton.dev/v1beta1 kind: Pipeline metadata: name: my-pipeline spec: tasks: - name: hello-world taskSpec: steps: - image: alpine:3.15.1 script: | echo "hello world" ` // myResolvedResource wraps the data we want to return to Pipelines type myResolvedResource struct {} // Data returns the bytes of our hard-coded Pipeline func (*myResolvedResource) Data() []byte { return []byte(pipeline) } // Annotations returns any metadata needed alongside the data. None atm. func (*myResolvedResource) Annotations() map[string]string { return nil } // RefSource is the source reference of the remote data that records where the remote // file came from including the url, digest and the entrypoint. None atm. func (*myResolvedResource) RefSource() *pipelinev1.RefSource { return nil }

ResolvedResource 接口与 RefSource 最佳实践

ResolvedResource是返回值中需要实现的第二个接口,定义同样位于两个框架的interface.go中,只有三个方法,实现成本很低:

type ResolvedResource interface { Data() []byte Annotations() map[string]string RefSource() *pipelinev1.RefSource }

其中RefSource结构体定义于 pkg/apis/pipeline/v1/provenance.go,包含三个字段:URI(构建定义来源的标识,如https://github.com/tektoncd/catalog)、Digest(内容的密码学摘要集合,如{"sha1": "f99d13e554ffcb696dee719fa85b695cb5b0f428"})、EntryPoint(进入构建的入口点,通常是构建定义文件的路径,如task/git-clone/0.10/git-clone.yaml)。

最佳实践:为了支持 Tekton Chains 在 SLSA provenance 中记录远程数据的来源信息,Resolver 应当实现RefSource()并返回正确的值,而不是nil:

// RefSource is the source reference of the remote data that records where the remote // file came from including the url, digest and the entrypoint. func (*myResolvedResource) RefSource() *pipelinev1.RefSource { return &v1.RefSource{ URI: "https://github.com/user/example", Digest: map[string]string{ "sha1": "example", }, EntryPoint: "foo/bar/task.yaml", } }

从源码还可以看到,框架对返回数据有额外的安全性约束:ValidateResolvedResource(pkg/resolution/resolver/framework/interface.go)会反序列化Data()的内容,确认其apiVersion属于tekton.dev组、kind属于允许的资源类型列表(如 Pipeline、Task),防止把 token 等非 Kubernetes 数据或 Secret 等非 Tekton 对象写入无特权的ResolutionRequest对象。

新老框架对照

当前仓库中同时存在两套 Resolver 框架,写作时务必分清:

维度最新框架(推荐)旧框架(已废弃)
接口路径pkg/remoteresolution/resolver/framework/interface.gopkg/resolution/resolver/framework/interface.go
校验方法Validate(ctx, req *v1beta1.ResolutionRequestSpec) errorValidateParams(ctx, params []pipelinev1.Param) error
解析方法Resolve(ctx, req *v1beta1.ResolutionRequestSpec) (...)Resolve(ctx, params []pipelinev1.Param) (...)
返回类型pkg/resolution/resolver/framework.ResolvedResource(复用旧包类型)同左
对应模板docs/resolver-template/cmd/resolver/main.godocs/resolver-template/cmd/demoresolver/main.go

新框架把"参数数组"升级为完整的ResolutionRequestSpec(含Params与URL),语义更清晰,也让 Resolver 能基于 URL 做出更丰富的校验与解析决策。旧接口仍在源码中存在(pkg/resolution/resolver/framework/interface.go 的Resolver接口已标注Deprecated),新代码一律使用最新框架。

此外,旧框架接口文件中还定义了三个可选增强接口,值得在进阶时了解:

  • ConfigWatcher:实现GetConfigName(ctx) string后,Resolver 可以从同名 ConfigMap(与 Resolver 同命名空间)读取管理员配置,例如仓库/镜像仓库白名单、请求超时、API 端点等。Git Resolver 即通过GetConfigName返回其配置 ConfigMap 名称(见 pkg/remoteresolution/resolver/git/resolver.go)。
  • TimedResolution:实现GetResolutionTimeout(...)可覆盖单次请求的默认超时(默认 1 分钟)。注意核心ResolutionRequest协调器对所有请求还有一个全局超时,它会覆盖任何 Resolver 特定超时,防止配置错误的僵尸请求无限存活。
  • ResolvedResource校验:如前所述,ValidateResolvedResource确保返回数据是合法的 Tekton 资源。

第四步:编写部署配置

Resolver 需要一份 Deployment 清单才能在 Kubernetes 中运行。完整的配置说明超出短教程范围,但核心要点如下:告诉 Kubernetes 运行我们的 Resolver 程序,附带底层knative框架期望的环境变量;应用部署在tekton-pipelines-resolvers命名空间;镜像由ko构建;使用的 ServiceAccount 为tekton-pipelines-resolvers——这是该命名空间中所有 Resolver 共享的默认 ServiceAccount(可在 config/resolvers/ 下的200-role.yaml、200-serviceaccount.yaml、201-rolebinding.yaml中看到其权限配置)。

完整清单如下,将其保存为config/demo-resolver-deployment.yaml:

apiVersion: apps/v1 kind: Deployment metadata: name: demoresolver namespace: tekton-pipelines-resolvers spec: replicas: 1 selector: matchLabels: app: demoresolver template: metadata: labels: app: demoresolver spec: affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - podAffinityTerm: labelSelector: matchLabels: app: demoresolver topologyKey: kubernetes.io/hostname weight: 100 serviceAccountName: tekton-pipelines-resolvers containers: - name: controller image: ko://example.com/demoresolver/cmd/demoresolver resources: requests: cpu: 100m memory: 100Mi limits: cpu: 1000m memory: 1000Mi ports: - name: metrics containerPort: 9090 env: - name: SYSTEM_NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace - name: CONFIG_LOGGING_NAME value: config-logging - name: CONFIG_OBSERVABILITY_NAME value: config-observability - name: METRICS_DOMAIN value: tekton.dev/resolution securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true runAsNonRoot: true capabilities: drop: - all

对照仓库中真实部署的 config/resolvers/resolvers-deployment.yaml,可以看到几个值得注意的差异与演进:

  • 镜像引用:image: ko://...前缀表示由ko构建并注入镜像,模板中对应为ko://github.com/tektoncd/pipeline/docs/resolver-template/cmd/demoresolver(见 docs/resolver-template/config/demo-resolver-deployment.yaml)。复制模板后需将镜像路径改为你自己 go module 的导入路径。
  • 环境变量:SYSTEM_NAMESPACE通过 downward API 取自 Pod 所在命名空间;CONFIG_LOGGING_NAME/CONFIG_OBSERVABILITY_NAME指向集群中的日志与可观测性 ConfigMap;METRICS_DOMAIN设为tekton.dev/resolution。真实部署还额外设置了CONFIG_FEATURE_FLAGS_NAME、CONFIG_LEADERELECTION_NAME以及健康检查端口PROBES_PORT(8080)。
  • 安全加固:securityContext禁用特权提升、只读根文件系统、以非 root 用户运行并丢弃全部 capabilities。真实部署进一步指定runAsUser: 65532与seccompProfile: RuntimeDefault。

第五步:部署并验证

所有代码就绪后,用ko构建并部署:

$ ko apply -f ./config/demo-resolver-deployment.yaml

部署成功后,kubectl get deployments -n tekton-pipelines应能看到类似输出(多出demoresolver):

NAME READY UP-TO-DATE AVAILABLE AGE controller 1/1 1 1 2d21h demoresolver 1/1 1 1 91s webhook 1/1 1 1 2d21

(注意真实环境中远程解析器的 Deployment 名为tekton-pipelines-remote-resolvers,位于tekton-pipelines-resolvers命名空间;本地验证时可用kubectl get deployments -n tekton-pipelines-resolvers观察。)

接下来,创建一个ResolutionRequest来"点名"我们的硬编码 Pipeline。新建文件test-request.yaml:

apiVersion: resolution.tekton.dev/v1beta1 kind: ResolutionRequest metadata: name: test-request labels: resolution.tekton.dev/type: demo

提交并持续观察:

$ kubectl apply -f ./test-request.yaml && kubectl get --watch resolutionrequests

很快就能看到ResolutionRequest的SUCCEEDED列为True:

resolutionrequest.resolution.tekton.dev/test-request created NAME SUCCEEDED REASON test-request True

按 Ctrl-C 返回命令行。此时查看ResolutionRequest的 YAML,会发现硬编码的 Pipeline YAML 位于status.data字段中,只不过它被base64 编码了(Data字段的字符串形式定义见 pkg/apis/resolution/v1beta1/resolution_request_types.go):

$ kubectl get resolutionrequest test-request -o yaml

将其还原为可读 YAML:

$ kubectl get resolutionrequest test-request -o jsonpath="{$.status.data}" | base64 -d

看到apiVersion: tekton.dev/v1beta1, kind: Pipeline, name: my-pipeline的输出,就意味着你的 Resolver 已经端到端跑通!

进阶:从 Demo 走向真实 Resolver

到此为止,你已经从零写出了第一个 Resolver。接下来的扩展方向有三条:

1. 替换Resolve()的实现:把硬编码字符串换成从你选择的存储后端(Git、对象存储、HTTP API 等)真实拉取内容。注意保持校验与返回契约不变——返回的数据必须是通过ValidateResolvedResource的合法 Tekton 资源。

2. 参考完整的 Git Resolver:仓库中的 pkg/remoteresolution/resolver/git/resolver.go 是一个功能完备的参考实现,展示了生产级 Resolver 的多个要素:GetConfigName接入管理员配置、IsImmutable判断 revision 是否为不可变的 commit SHA(40 位 SHA-1 或 64 位 SHA-256)、GetResolutionTimeout自定义超时、基于 LRU 的 secrets 缓存、以及通过ResolveWithRetry的重试逻辑。它还实现了ConfigWatcher、TimedResolution、ImmutabilityChecker等多个可选接口,编译期用var _ framework.Resolver = (*Resolver)(nil)等断言保证接口实现正确。

3. 端到端跑通一个PipelineRun:写一个引用远程解析的PipelineRun,让 Tekton Pipelines 真正执行你 Resolver 返回的硬编码Pipeline。模板 docs/resolver-template/README.md 给出了最简单形态:

apiVersion: tekton.dev/v1beta1 kind: PipelineRun metadata: name: resolver-demo spec: pipelineRef: resolver: demo

4. 复用模板快速起步:将 docs/resolver-template/ 整个子目录复制到新项目,在项目根执行go mod init与go mod tidy(仓库内无需此步,因为它依赖仓库根部的go.mod/go.sum),然后把 docs/resolver-template/config/demo-resolver-deployment.yaml 中的镜像字段改成你自己 module 的导入路径(带ko://前缀),即可用ko apply -f ./config/demo-resolver-deployment.yaml部署。

总结

编写一个 Tekton Resolver 的本质,就是实现一个只有五六个方法的 Go 接口并把它部署进集群:Initialize做初始化、GetName提供日志标识、GetSelector决定请求路由标签、Validate/ValidateParams把关请求合法性、Resolve负责真正拉取并返回ResolvedResource(含Data、Annotations、RefSource三个方法)。框架本身承担了控制器样板、路由、状态上报与数据校验的公共职责,让开发者只需关注"从哪里取、怎么取"这一核心问题。本文的完整可运行代码即仓库中的 docs/resolver-template/,而 pkg/remoteresolution/resolver/git/resolver.go 则展示了生产级 Resolver 的全部进阶姿势,可作为下一步深度学习的范本。

  • 云原生
  • CI/CD
  • DevOps
  • 后端

【免费下载链接】pipeline

A cloud-native Pipeline resource.

项目地址:https://gitcode.com/gh_mirrors/pipelin/pipeline
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询