Learn how to develop custom resolvers for Tekton Pipelines to integrate with proprietary storage systems or version control platforms.
What is a Resolver?
A Resolver is a program that runs in a Kubernetes cluster alongside Tekton Pipelines and “resolves” requests for Tasks and Pipelines from remote locations. For example, if a user submits a PipelineRun that needs a Pipeline YAML stored in a custom storage system, your resolver would be responsible for fetching the YAML and returning it to Tekton Pipelines.
This pattern allows integration with any storage backend without modifying Tekton Pipelines itself.
Prerequisites
Before developing a custom resolver, you’ll need:
Proficiency in Go programming
Understanding of Tekton Resolution concepts
A Kubernetes cluster running Kubernetes 1.28 or later
kubectl installed
ko installed for building container images
An image registry for pushing images (or kind.local for local development)
Tekton Pipelines v0.41.0+ and remote resolvers installed
Architecture Overview
A resolver consists of:
Go binary - Implements the resolver logic
Kubernetes Deployment - Runs the resolver in the cluster
Label-based routing - Directs ResolutionRequests to your resolver
Framework integration - Uses Tekton’s resolver framework
Project Setup
Create the initial directory structure:
mkdir demoresolver
cd demoresolver
go mod init example.com/demoresolver
mkdir -p cmd/demoresolver
mkdir config
cmd/demoresolver Contains the resolver implementation
config Contains Kubernetes deployment manifests
Implementing the Resolver
Create cmd/demoresolver/main.go with the following framework:
Main Entry Point
Latest Framework
Previous Framework (Deprecated)
package main
import (
" context "
" errors "
" fmt "
neturl " net/url "
" github.com/tektoncd/pipeline/pkg/resolution/common "
" github.com/tektoncd/pipeline/pkg/remoteresolution/resolver/framework "
pipelinev1 " github.com/tektoncd/pipeline/pkg/apis/pipeline/v1 "
" github.com/tektoncd/pipeline/pkg/apis/resolution/v1beta1 "
" knative.dev/pkg/injection/sharedmain "
)
func main () {
sharedmain . Main ( "controller" ,
framework . NewController ( context . Background (), & resolver {}),
)
}
type resolver struct {}
Required Interface Methods
Your resolver must implement the framework.Resolver interface:
Initialize Method
// Initialize sets up any dependencies needed by the resolver
func ( r * resolver ) Initialize ( context . Context ) error {
return nil
}
GetName Method
// GetName returns a string name to refer to this resolver by
func ( r * resolver ) GetName ( context . Context ) string {
return "Demo"
}
GetSelector Method
// 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" ,
}
}
This tells the framework that any ResolutionRequest with label "resolution.tekton.dev/type": "demo" should be routed to your resolver.
Validate Method
Latest Framework
Previous Framework (Deprecated)
// Validate ensures that the resolution spec is valid
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
}
Resolve Method
Latest Framework
Previous Framework (Deprecated)
// Resolve fetches the resource from your storage backend
func ( r * resolver ) Resolve ( ctx context . Context , req * v1beta1 . ResolutionRequestSpec ) ( framework . ResolvedResource , error ) {
return & myResolvedResource {}, nil
}
Implementing ResolvedResource
Create a type that implements framework.ResolvedResource:
// Hard-coded pipeline for demonstration
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 to return to Pipelines
type myResolvedResource struct {}
// Data returns the bytes of the resolved resource
func ( * myResolvedResource ) Data () [] byte {
return [] byte ( pipeline )
}
// Annotations returns any metadata needed alongside the data
func ( * myResolvedResource ) Annotations () map [ string ] string {
return nil
}
// RefSource returns source reference for provenance tracking
func ( * myResolvedResource ) RefSource () * pipelinev1 . RefSource {
return nil
}
Best Practice: Implementing RefSource
For supply chain security (Tekton Chains integration), implement RefSource():
func ( * myResolvedResource ) RefSource () * pipelinev1 . RefSource {
return & pipelinev1 . RefSource {
URI : "https://github.com/user/example" ,
Digest : map [ string ] string {
"sha1" : "aeb957601cf41c012be462827053a21a420befca" ,
},
EntryPoint : "foo/bar/task.yaml" ,
}
}
URI Source location of the resource
Digest Hash of the resource content
EntryPoint Path to the specific resource
Deployment Configuration
Create 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
Building and Deploying
Install dependencies:
Verify the code compiles:
go build -o /dev/null ./cmd/demoresolver
Deploy to Kubernetes using ko:
ko apply -f ./config/demo-resolver-deployment.yaml
Verify deployment:
kubectl get deployments -n tekton-pipelines-resolvers
Expected output:
NAME READY UP-TO-DATE AVAILABLE AGE
controller 1/1 1 1 2d21h
demoresolver 1/1 1 1 91s
webhook 1/1 1 1 2d21h
Testing Your Resolver
Create test-request.yaml:
apiVersion : resolution.tekton.dev/v1beta1
kind : ResolutionRequest
metadata :
name : test-request
labels :
resolution.tekton.dev/type : demo
Submit the request:
kubectl apply -f ./test-request.yaml && kubectl get --watch resolutionrequests
Expected output:
resolutionrequest.resolution.tekton.dev/test-request created
NAME SUCCEEDED REASON
test-request True
View the resolved resource:
kubectl get resolutionrequest test-request -o jsonpath="{.status.data}" | base64 -d
You should see the hard-coded pipeline YAML.
Using in PipelineRuns
Test your resolver with a real PipelineRun:
apiVersion : tekton.dev/v1beta1
kind : PipelineRun
metadata :
name : demo-pipeline-run
spec :
pipelineRef :
resolver : demo
Advanced Topics
Accepting Parameters
Modify the Validate and Resolve methods to accept parameters:
Latest Framework
Previous Framework (Deprecated)
func ( r * resolver ) Validate ( ctx context . Context , req * v1beta1 . ResolutionRequestSpec ) error {
for _ , param := range req . Params {
if param . Name == "resource-name" {
if param . Value . StringVal == "" {
return errors . New ( "resource-name cannot be empty" )
}
}
}
return nil
}
func ( r * resolver ) Resolve ( ctx context . Context , req * v1beta1 . ResolutionRequestSpec ) ( framework . ResolvedResource , error ) {
resourceName := ""
for _ , param := range req . Params {
if param . Name == "resource-name" {
resourceName = param . Value . StringVal
}
}
// Fetch resource using resourceName
return & myResolvedResource { name : resourceName }, nil
}
Adding Configuration
Create a ConfigMap for resolver settings:
apiVersion : v1
kind : ConfigMap
metadata :
name : demo-resolver-config
namespace : tekton-pipelines-resolvers
data :
default-timeout : "5m"
api-endpoint : "https://api.example.com"
Error Handling
Return descriptive errors to help users troubleshoot:
func ( r * resolver ) Resolve ( ctx context . Context , req * v1beta1 . ResolutionRequestSpec ) ( framework . ResolvedResource , error ) {
resource , err := fetchFromBackend ( ctx , req )
if err != nil {
if errors . Is ( err , ErrNotFound ) {
return nil , fmt . Errorf ( "resource not found: %w " , err )
}
if errors . Is ( err , ErrUnauthorized ) {
return nil , fmt . Errorf ( "authentication failed: check credentials: %w " , err )
}
return nil , fmt . Errorf ( "failed to fetch resource: %w " , err )
}
return resource , nil
}
Example: Real-World Resolver
For a production-ready example, see the Git Resolver source code .
Next Steps
Expand Resolve() Implement fetching from your actual storage backend
Add Configuration Create ConfigMaps for resolver settings
Implement Caching Add caching for frequently accessed resources
Add Authentication Implement authentication for protected backends
Resolver Template
For a complete resolver template to get started quickly, visit the resolver-template in the Tekton Pipeline repository.
Framework Differences
The previous framework (using pkg/resolution/resolver/framework) is deprecated. New resolvers should use pkg/remoteresolution/resolver/framework.
Key Changes
Previous Framework Latest Framework ValidateParams(params []Param)Validate(req *ResolutionRequestSpec)Resolve(params []Param)Resolve(req *ResolutionRequestSpec)Parameters only Full request spec with URL support
Best Practices
Validate Inputs Always validate parameters and URLs thoroughly
Return Provenance Implement RefSource() for supply chain security
Handle Errors Provide clear, actionable error messages
Use Timeouts Respect the global 1-minute timeout limit
Secure Credentials Store API tokens in Kubernetes secrets
Test Thoroughly Test with various inputs and edge cases
Start with a simple hard-coded resolver and gradually add features like parameter handling, authentication, and caching.