Skip to main content
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:
  1. Go binary - Implements the resolver logic
  2. Kubernetes Deployment - Runs the resolver in the cluster
  3. Label-based routing - Directs ResolutionRequests to your resolver
  4. 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

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

// 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

// 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:
go mod tidy
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:
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 FrameworkLatest Framework
ValidateParams(params []Param)Validate(req *ResolutionRequestSpec)
Resolve(params []Param)Resolve(req *ResolutionRequestSpec)
Parameters onlyFull 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.

Build docs developers (and LLMs) love