Tekton Mastery:
Practical Reference

A practical reference β€” YAMLs, commands, and demo steps for every topic. Designed for engineers who will contribute to or work on Konflux.

9Topics
40+YAML Files
3Difficulty Levels
Architecture & Setup
Beginner ⏱ 20 min

Tekton Architecture & Installation β€” The Kubernetes-Native CI/CD Engine

What You'll Learn

Why Tekton exists and how it differs from Jenkins/GitHub Actions
Tekton's four main components: Pipelines, Triggers, Chains, Dashboard
How Tekton maps to Kubernetes primitives (CRDs, Pods, PVCs)
Install Tekton on a local cluster (Kind, minikube, CRC, or OpenShift) and verify the setup
Install and use the tkn CLI
Browse Tekton CRDs and understand the API surface

Prerequisites

No prior Tekton knowledge needed kubectl installed Kind, minikube, CRC, or OpenShift cluster Basic Kubernetes familiarity (Pods, Namespaces)

☸ Local Cluster Options β€” Any Works with Tekton

Tekton runs on any Kubernetes-compatible cluster. This video uses Kind (fastest local setup). Pick whichever fits your environment β€” all kubectl and tkn commands are identical after cluster creation.

Kind β€” Kubernetes in Docker

Lightweight clusters using Docker containers as nodes. Setup in ~30 seconds, no VM required, multi-node support. Recommended for this series.

Minikube

Single-node cluster with built-in addons (ingress, metrics-server, dashboard). Supports Docker, VirtualBox, and HyperKit drivers.

CRC β€” OpenShift Local

Full OpenShift 4 experience on your laptop. Closest to Konflux's production environment. Requires β‰₯18 GB RAM and a Red Hat account.

OpenShift / Any Kubernetes

Cloud or on-prem clusters (EKS, GKE, AKS, OpenShift). Tekton is pre-installed on OpenShift as OpenShift Pipelines β€” no extra setup needed.

Demo Steps

  1. Create a Kind cluster using a config file. Verify it's ready with kubectl cluster-info --context kind-tekton-demo and kubectl get nodes.
  2. Install Tekton Pipelines by applying the release manifest. Wait for all pods to be Running in the tekton-pipelines namespace.
  3. Install Tekton Dashboard by applying its release manifest.
  4. Install the tkn CLI β€” show both Homebrew (Mac) and curl (Linux) methods.
  5. Verify installation: run tkn version, show the CRDs with kubectl get crds | grep tekton.
  6. Port-forward the Dashboard and open it in a browser. Show the empty state.
  7. Explore the API: run kubectl explain task, kubectl explain pipeline.

Commands

kind-config.yaml (create this file before running kind create cluster)
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
name: tekton-demo
nodes:
  - role: control-plane
    # Expose ports for NodePort services (useful for EventListeners in Video 5)
    extraPortMappings:
      - containerPort: 30000
        hostPort: 30000
        protocol: TCP
      - containerPort: 30001
        hostPort: 30001
        protocol: TCP
    kubeadmConfigPatches:
      - |
        kind: InitConfiguration
        nodeRegistration:
          kubeletExtraArgs:
            node-labels: "ingress-ready=true"
# Kind uses Docker β€” set resources in Docker Desktop:
# Settings > Resources:  CPUs: 4+   Memory: 8 GB+   Disk: 30 GB+
terminal β€” run in order
# ── STEP 1: Install Kind ──────────────────────────────────────
# macOS
brew install kind

# Linux
curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.25.0/kind-linux-amd64
chmod +x ./kind && sudo mv ./kind /usr/local/bin/kind

# Windows (PowerShell)
# choco install kind

# Verify
kind --version

# ── STEP 2: Create the Kind cluster ───────────────────────────
kind create cluster --config kind-config.yaml

# Verify the cluster is up and kubectl context is set
kubectl cluster-info --context kind-tekton-demo
kubectl get nodes

# ── STEP 3: Install Tekton Pipelines (latest stable) ──────────
kubectl apply -f https://storage.googleapis.com/tekton-releases/pipeline/latest/release.yaml

# Wait for Tekton controller to be ready
kubectl wait --for=condition=ready pod \
  --selector=app=tekton-pipelines-controller \
  --namespace=tekton-pipelines \
  --timeout=180s

# Verify all Tekton pods are Running
kubectl get pods -n tekton-pipelines

# ── STEP 4: Install Tekton Dashboard ──────────────────────────
kubectl apply -f https://storage.googleapis.com/tekton-releases/dashboard/latest/release.yaml

# ── STEP 5: Install tkn CLI ───────────────────────────────────
# macOS
brew install tektoncd-cli

# Linux β€” check https://github.com/tektoncd/cli/releases for the latest version
curl -LO https://github.com/tektoncd/cli/releases/latest/download/tkn_Linux_x86_64.tar.gz
tar xzf tkn_Linux_x86_64.tar.gz -C /usr/local/bin tkn

# ── STEP 6: Verify everything ─────────────────────────────────
tkn version
kubectl get pods -n tekton-pipelines

# Explore Tekton CRDs installed in the cluster
kubectl get crds | grep tekton

# Explain the Task CRD fields
kubectl explain task.spec
kubectl explain task.spec.steps

# ── STEP 7: Open the Dashboard ────────────────────────────────
kubectl port-forward -n tekton-pipelines svc/tekton-dashboard 9097:9097
# Open: http://localhost:9097

# ── ALTERNATIVE cluster setups (commands above are identical after this) ──
# Minikube:
#   minikube start --cpus=4 --memory=8192 --disk-size=30g
#   minikube addons enable ingress
#
# CRC (OpenShift Local):
#   crc start --cpus 4 --memory 12288
#   eval $(crc oc-env)          # OpenShift Pipelines (Tekton) already installed
#
# OpenShift / Any Kubernetes:
#   oc login https://api.your-cluster.example.com
#   # On OpenShift, install OpenShift Pipelines Operator from OperatorHub instead:
#   # OperatorHub > OpenShift Pipelines > Install

Konflux Connection

βš™ How This Applies to Konflux

Konflux runs on OpenShift with Tekton Pipelines, Triggers, and Chains all installed cluster-wide. The exact CRDs you see here β€” Task, Pipeline, PipelineRun β€” are what Konflux creates when you push code to a Component. The Tekton Dashboard you just set up mirrors the Pipeline Runs tab in the Konflux UI. Whenever a Konflux build fails, you can look at the raw PipelineRun object to understand what happened at the Tekton level.

Key Takeaways

  • Tekton is Kubernetes-native β€” everything is a CRD
  • Four components: Pipelines, Triggers, Chains, Dashboard
  • Every pipeline run becomes real Kubernetes Pods
  • tkn CLI is the primary tool for interacting with Tekton
  • Same tech stack that powers Konflux in production
  • Tekton v1 API is stable β€” use tekton.dev/v1
Tasks & TaskRuns
Beginner ⏱ 25 min

Tasks & TaskRuns β€” The Atomic Building Blocks of Tekton

What You'll Learn

Task anatomy: steps, params, results, workspaces, stepTemplate
How each Step becomes a container in a Pod
Declaring and using Parameters with defaults and descriptions
Writing Task Results β€” passing values downstream
Creating TaskRuns imperatively and declaratively
Streaming logs and inspecting TaskRun status

Prerequisites

Video 1 complete Tekton Pipelines installed tkn CLI working

Demo Steps

  1. Create the hello-world task β€” apply 02-hello-task.yaml, inspect with tkn task describe.
  2. Run it declaratively β€” apply 02-taskrun.yaml, watch with tkn taskrun logs -f.
  3. Inspect the Pod β€” run kubectl get pods and show the init containers Tekton injects.
  4. Run it with tkn CLI β€” use tkn task start hello-world --param name=Konflux to demonstrate the imperative approach.
  5. Create the parameterized multi-step task β€” apply 02-build-info-task.yaml.
  6. Run it and show Results β€” use tkn taskrun describe to see the captured result values.
  7. Show failure handling β€” demonstrate what happens when a step exits non-zero.

YAML Files

02-hello-task.yaml
apiVersion: tekton.dev/v1
kind: Task
metadata:
  name: hello-world
  labels:
    app.kubernetes.io/version: "1.0"
  annotations:
    tekton.dev/displayName: "Hello World"
    tekton.dev/description: "A simple hello world to demonstrate Task structure"
spec:
  description: "Greets a user and shows basic Tekton Task concepts"
  params:
    - name: name
      type: string
      default: "Tekton Learner"
      description: "The name of the person to greet"
    - name: language
      type: string
      default: "english"
      description: "Greeting language: english, spanish, french"
  results:
    - name: greeting
      description: "The full greeting message that was printed"
  steps:
    - name: greet
      image: alpine:3.18
      env:
        - name: GREETING_NAME
          value: "$(params.name)"
        - name: LANG_CHOICE
          value: "$(params.language)"
      script: |
        #!/bin/sh
        set -e
        case "$LANG_CHOICE" in
          spanish) MSG="Β‘Hola, $GREETING_NAME! Bienvenido a Tekton." ;;
          french)  MSG="Bonjour, $GREETING_NAME! Bienvenue sur Tekton." ;;
          *)       MSG="Hello, $GREETING_NAME! Welcome to Tekton." ;;
        esac
        echo "$MSG"
        # Write the greeting as a Task Result
        printf '%s' "$MSG" | tee "$(results.greeting.path)"
    - name: show-tekton-facts
      image: alpine:3.18
      script: |
        #!/bin/sh
        echo ""
        echo "=== About This Execution ==="
        echo "This step runs AFTER the greet step in the SAME Pod."
        echo "Hostname (Pod name): $(hostname)"
        echo "Date: $(date -u '+%Y-%m-%dT%H:%M:%SZ')"
        echo "Tekton injects metadata via the downward API."
        echo "Each Step is a separate container init that runs sequentially."
02-taskrun.yaml
apiVersion: tekton.dev/v1
kind: TaskRun
metadata:
  name: hello-world-run-001
  labels:
    # Labels make it easy to query related runs
    task-name: hello-world
    run-by: demo-video-2
spec:
  taskRef:
    name: hello-world
  params:
    - name: name
      value: "Konflux Engineer"
    - name: language
      value: "english"
02-build-info-task.yaml (demonstrates results and stepTemplate)
apiVersion: tekton.dev/v1
kind: Task
metadata:
  name: build-info
  annotations:
    tekton.dev/displayName: "Build Info Collector"
spec:
  description: "Collects build metadata and surfaces it as Task Results for downstream tasks"
  params:
    - name: image-name
      type: string
      description: "The name of the image being built (e.g. myapp)"
    - name: git-revision
      type: string
      default: "main"
      description: "Git revision being built"
    - name: registry
      type: string
      default: "quay.io/myorg"
      description: "Target container registry"
  results:
    - name: image-url
      description: "Full image URL including registry and name"
    - name: build-timestamp
      description: "Unix timestamp when the build started"
    - name: image-tag
      description: "The tag that will be applied to the image"
  # stepTemplate applies these fields to ALL steps β€” avoids repetition
  stepTemplate:
    image: alpine:3.18
    env:
      - name: IMAGE_NAME
        value: "$(params.image-name)"
      - name: GIT_REVISION
        value: "$(params.git-revision)"
      - name: REGISTRY
        value: "$(params.registry)"
  steps:
    - name: compute-tag
      script: |
        #!/bin/sh
        set -e
        # In real pipelines this comes from git describe or the commit SHA
        TAG="$(date -u '+%Y%m%d')-${GIT_REVISION}"
        echo "Computed image tag: $TAG"
        printf '%s' "$TAG" | tee "$(results.image-tag.path)"

    - name: compute-image-url
      script: |
        #!/bin/sh
        set -e
        TAG=$(cat "$(results.image-tag.path)")
        IMAGE_URL="${REGISTRY}/${IMAGE_NAME}:${TAG}"
        echo "Full image URL: $IMAGE_URL"
        printf '%s' "$IMAGE_URL" | tee "$(results.image-url.path)"

    - name: record-timestamp
      script: |
        #!/bin/sh
        set -e
        TS=$(date -u '+%s')
        echo "Build timestamp: $TS"
        printf '%s' "$TS" | tee "$(results.build-timestamp.path)"

    - name: print-summary
      script: |
        #!/bin/sh
        echo ""
        echo "=== Build Info Summary ==="
        echo "Image:     $(cat $(results.image-url.path))"
        echo "Tag:       $(cat $(results.image-tag.path))"
        echo "Timestamp: $(cat $(results.build-timestamp.path))"
        echo ""
        echo "These results are now available to any downstream Task"
        echo "in a Pipeline via: \$(tasks.build-info.results.image-url)"

Commands

terminal
# Apply the Task definition
kubectl apply -f 02-hello-task.yaml

# List tasks in the cluster
tkn task list

# Describe the task (shows params, results, steps)
tkn task describe hello-world

# Run declaratively
kubectl apply -f 02-taskrun.yaml

# Follow logs in real time
tkn taskrun logs hello-world-run-001 -f

# Describe the run β€” see status, results, pod name
tkn taskrun describe hello-world-run-001

# Inspect the underlying Pod
kubectl get pods | grep hello-world-run-001
kubectl describe pod hello-world-run-001-pod

# Run imperatively with the CLI (creates TaskRun automatically)
tkn task start hello-world \
  --param name="Konflux Engineer" \
  --param language="spanish" \
  --showlog

# Apply and run the second task
kubectl apply -f 02-build-info-task.yaml
tkn task start build-info \
  --param image-name="my-app" \
  --param git-revision="abc1234" \
  --param registry="quay.io/myorg" \
  --showlog

# See the results captured by Tekton
tkn taskrun describe --last

Konflux Connection

βš™ How This Applies to Konflux

Konflux build pipelines consist of ~12 Tasks chained together. Each Task follows exactly this pattern. Key examples:

  • The clone-repository task produces a commit result (the full git SHA) that flows into build tasks
  • The build-container task (using Buildah) produces an IMAGE_DIGEST result that Tekton Chains uses to sign the image
  • The IMAGE_URL and IMAGE_DIGEST results from the build pipeline become part of the Konflux Snapshot
  • Every Konflux Task uses stepTemplate to inject a common security context (non-root, read-only root FS)

Key Takeaways

  • Task = reusable unit of work defined as a CRD
  • Steps run sequentially in the same Pod
  • Params use $(params.name) substitution syntax
  • Results written to $(results.name.path)
  • TaskRun = one execution instance of a Task
  • stepTemplate reduces repetition across steps
Pipelines & PipelineRuns
Beginner ⏱ 30 min

Pipelines & PipelineRuns β€” Orchestrating Tasks Into a Real Workflow

What You'll Learn

Pipeline structure and how it references Tasks
Sequential vs. parallel task execution with runAfter
Passing Task results to downstream tasks
Pipeline-level params and results
Creating and monitoring PipelineRuns
Visualizing the DAG in Tekton Dashboard

Demo Steps

  1. Apply the three tasks individually: validate-input, run-checks (used for both lint and test), generate-report.
  2. Apply the pipeline β€” inspect with tkn pipeline describe and show the DAG.
  3. Start the PipelineRun with tkn pipeline start and follow logs.
  4. Open the Dashboard and watch the parallel tasks (lint + test) execute simultaneously, then report waits for both.
  5. Inspect the PipelineRun β€” show how results from individual task runs are accessible.
  6. Demonstrate failure β€” change the repo URL to something invalid and show the pipeline failing at the validation step, with lint and test never starting.

YAML Files

03-tasks.yaml (three task definitions used by the pipeline)
---
# Task 1: Validates the repository URL and extracts the repo name
apiVersion: tekton.dev/v1
kind: Task
metadata:
  name: validate-repo
spec:
  description: "Validates a Git repository URL and extracts the project name"
  params:
    - name: repo-url
      type: string
  results:
    - name: repo-name
      description: "Short repository name extracted from the URL"
    - name: repo-host
      description: "The hosting service: github or gitlab"
  steps:
    - name: validate-and-extract
      image: alpine:3.18
      env:
        - name: REPO_URL
          value: "$(params.repo-url)"
      script: |
        #!/bin/sh
        set -e
        echo "Validating: $REPO_URL"
        if echo "$REPO_URL" | grep -q "github.com"; then
          HOST="github"
        elif echo "$REPO_URL" | grep -q "gitlab.com"; then
          HOST="gitlab"
        else
          echo "ERROR: Unsupported host. Use github.com or gitlab.com"
          exit 1
        fi
        REPO_NAME=$(echo "$REPO_URL" | sed 's|.*/||' | sed 's|\.git$||')
        echo "Host: $HOST"
        echo "Repo name: $REPO_NAME"
        printf '%s' "$HOST"      | tee "$(results.repo-host.path)"
        printf '%s' "$REPO_NAME" | tee "$(results.repo-name.path)"
---
# Task 2: Simulates a code check (used for both lint and test steps)
apiVersion: tekton.dev/v1
kind: Task
metadata:
  name: run-check
spec:
  description: "Simulates running a code quality check (lint or test)"
  params:
    - name: check-type
      type: string
      description: "Type of check: lint or test"
    - name: repo-name
      type: string
  results:
    - name: status
      description: "Check result: pass or fail"
    - name: finding-count
      description: "Number of findings (0 = clean)"
  steps:
    - name: execute-check
      image: alpine:3.18
      env:
        - name: CHECK_TYPE
          value: "$(params.check-type)"
        - name: REPO_NAME
          value: "$(params.repo-name)"
      script: |
        #!/bin/sh
        set -e
        echo "Running $CHECK_TYPE check for: $REPO_NAME"
        echo "Simulating work..."
        sleep 2
        COUNT=0
        echo "$CHECK_TYPE complete. Findings: $COUNT"
        printf '%s' "pass"  | tee "$(results.status.path)"
        printf '%s' "$COUNT" | tee "$(results.finding-count.path)"
---
# Task 3: Generates a summary report from all previous task results
apiVersion: tekton.dev/v1
kind: Task
metadata:
  name: quality-report
spec:
  description: "Generates a final quality report from lint and test results"
  params:
    - name: repo-name
      type: string
    - name: repo-host
      type: string
    - name: lint-status
      type: string
    - name: lint-findings
      type: string
    - name: test-status
      type: string
    - name: test-findings
      type: string
  results:
    - name: summary
      description: "One-line quality summary"
  steps:
    - name: generate
      image: alpine:3.18
      script: |
        #!/bin/sh
        REPO="$(params.repo-name)"
        HOST="$(params.repo-host)"
        LINT="$(params.lint-status) ($(params.lint-findings) issues)"
        TEST="$(params.test-status) ($(params.test-findings) findings)"
        SUMMARY="[$HOST] $REPO β€” Lint: $LINT | Tests: $TEST"
        echo ""
        echo "=========================================="
        echo " QUALITY REPORT"
        echo "=========================================="
        echo " Repository : $REPO"
        echo " Host       : $HOST"
        echo " Lint       : $LINT"
        echo " Tests      : $TEST"
        echo "=========================================="
        printf '%s' "$SUMMARY" | tee "$(results.summary.path)"
03-pipeline.yaml
apiVersion: tekton.dev/v1
kind: Pipeline
metadata:
  name: code-quality-pipeline
  annotations:
    tekton.dev/displayName: "Code Quality Pipeline"
    tekton.dev/description: "Validates repo, runs lint + test in parallel, reports results"
spec:
  description: "Demonstrates task chaining, parallel execution, and result passing"
  params:
    - name: repo-url
      type: string
      description: "Full URL of the Git repository to check"
      default: "https://github.com/tektoncd/pipeline"
    - name: revision
      type: string
      default: "main"
  # Pipeline surfaces a result from one of its tasks to callers
  results:
    - name: quality-summary
      description: "Final quality report from the pipeline run"
      value: "$(tasks.generate-report.results.summary)"

  tasks:
    # STEP 1 β€” Validate input (runs first, blocks everything else)
    - name: validate
      taskRef:
        name: validate-repo
      params:
        - name: repo-url
          value: "$(params.repo-url)"

    # STEP 2a β€” Lint (runs in parallel with test, after validate)
    - name: lint
      runAfter:
        - validate
      taskRef:
        name: run-check
      params:
        - name: check-type
          value: "lint"
        - name: repo-name
          # Reference the result from the validate task
          value: "$(tasks.validate.results.repo-name)"

    # STEP 2b β€” Test (runs in parallel with lint, after validate)
    - name: test
      runAfter:
        - validate
      taskRef:
        name: run-check
      params:
        - name: check-type
          value: "test"
        - name: repo-name
          value: "$(tasks.validate.results.repo-name)"

    # STEP 3 β€” Report (runs after BOTH lint and test complete)
    - name: generate-report
      runAfter:
        - lint
        - test
      taskRef:
        name: quality-report
      params:
        - name: repo-name
          value: "$(tasks.validate.results.repo-name)"
        - name: repo-host
          value: "$(tasks.validate.results.repo-host)"
        - name: lint-status
          value: "$(tasks.lint.results.status)"
        - name: lint-findings
          value: "$(tasks.lint.results.finding-count)"
        - name: test-status
          value: "$(tasks.test.results.status)"
        - name: test-findings
          value: "$(tasks.test.results.finding-count)"
03-pipelinerun.yaml
apiVersion: tekton.dev/v1
kind: PipelineRun
metadata:
  name: quality-pipeline-run-001
  labels:
    pipeline-name: code-quality-pipeline
spec:
  pipelineRef:
    name: code-quality-pipeline
  params:
    - name: repo-url
      value: "https://github.com/tektoncd/pipeline"
    - name: revision
      value: "main"
  # TaskRunSpecs lets you set compute limits per task
  taskRunSpecs:
    - pipelineTaskName: lint
      computeResources:
        requests:
          memory: "64Mi"
          cpu: "100m"
    - pipelineTaskName: test
      computeResources:
        requests:
          memory: "128Mi"
          cpu: "200m"

Commands

terminal
# Apply all task definitions
kubectl apply -f 03-tasks.yaml

# Verify tasks are registered
tkn task list

# Apply the pipeline
kubectl apply -f 03-pipeline.yaml

# Describe pipeline β€” shows param definitions and task graph
tkn pipeline describe code-quality-pipeline

# Start pipeline interactively (tkn prompts for param values)
tkn pipeline start code-quality-pipeline --showlog

# Or apply the PipelineRun declaratively
kubectl apply -f 03-pipelinerun.yaml

# Watch the PipelineRun status
tkn pipelinerun list
tkn pipelinerun logs quality-pipeline-run-001 -f

# Describe the run β€” see task results and pipeline result
tkn pipelinerun describe quality-pipeline-run-001

# See the underlying TaskRuns created by the pipeline
kubectl get taskruns --selector=tekton.dev/pipelineRun=quality-pipeline-run-001

# See the DAG in the Dashboard
kubectl port-forward -n tekton-pipelines svc/tekton-dashboard 9097:9097
# Open http://localhost:9097 and navigate to the PipelineRun

Konflux Connection

βš™ How This Applies to Konflux

The Konflux build pipeline follows exactly this pattern. When a Konflux Component build starts:

  • The init task runs first (validates component config)
  • The clone-repository task produces a commit result
  • Vulnerability scanning and SAST checks run in parallel after the build
  • The IMAGE_DIGEST result from build-container flows into all subsequent signing and scanning tasks
  • The Konflux Snapshot is created using the final image URL + digest results from the pipeline
  • PipelineRun labels like appstudio.openshift.io/component link the run back to the Konflux component

Key Takeaways

  • Pipeline = ordered DAG of Task instances
  • Tasks without shared runAfter run in parallel
  • Results flow via $(tasks.<name>.results.<key>)
  • PipelineRun = one execution of a Pipeline
  • Use taskRunSpecs to set per-task resource limits
  • Dashboard shows the live DAG and per-task logs
Workspaces
Intermediate ⏱ 28 min

Workspaces β€” Sharing Files, Secrets, and Config Between Tasks

What You'll Learn

Why results aren't enough β€” you need workspaces for large data
Four workspace types: PVC, emptyDir, ConfigMap, Secret
Declaring workspaces in Tasks and Pipelines
Binding workspace types in PipelineRun
The classic clone-then-build pattern
Optional workspaces with conditional logic

Demo Steps

  1. Create the PVC β€” apply 04-workspace-pvc.yaml, verify with kubectl get pvc.
  2. Create a ConfigMap for configuration that will be injected as a workspace.
  3. Apply the tasks: writer task and reader task, both declaring workspaces.
  4. Apply the pipeline that wires workspace slots to task workspace names.
  5. Apply the PipelineRun that binds the PVC and ConfigMap to workspace slots.
  6. Watch execution β€” show that the reader task can see files written by the writer task.
  7. Show emptyDir variant β€” discuss when to use emptyDir vs PVC.

YAML Files

04-workspace-resources.yaml (PVC + ConfigMap)
---
# PersistentVolumeClaim β€” shared storage for Tasks in the pipeline
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pipeline-workspace-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 500Mi
  # On Minikube the default StorageClass provisions hostPath volumes
  # On cloud clusters (EKS, GKE, OpenShift) use the cluster's default SC
---
# ConfigMap used as a read-only config workspace
apiVersion: v1
kind: ConfigMap
metadata:
  name: build-config
data:
  config.yaml: |
    version: "1.0"
    build:
      output_dir: /workspace/source/dist
      optimization: true
      target: linux/amd64
    lint:
      max_issues: 0
      strict: true
  .golangci.yml: |
    run:
      timeout: 5m
    linters:
      enable:
        - errcheck
        - gosimple
        - staticcheck
04-workspace-pipeline.yaml
apiVersion: tekton.dev/v1
kind: Pipeline
metadata:
  name: workspace-demo-pipeline
spec:
  description: "Demonstrates workspace usage: PVC for source, ConfigMap for config, Secret for creds"
  params:
    - name: app-name
      type: string
      default: "my-app"
  # Pipeline-level workspace declarations β€” these are the slots
  workspaces:
    - name: source
      description: "Shared PVC workspace for source files β€” all tasks share this"
    - name: build-config
      description: "ConfigMap workspace with build configuration (read-only)"
    - name: registry-credentials
      description: "Secret workspace with container registry credentials"
      optional: true

  tasks:
    # Task 1: Write simulated source files to the shared workspace
    - name: write-source
      workspaces:
        - name: source         # task's workspace name
          workspace: source    # pipeline's workspace slot name
        - name: config
          workspace: build-config
      taskSpec:
        workspaces:
          - name: source
            description: "Where to write source files"
          - name: config
            description: "Build configuration (read-only)"
        params:
          - name: app-name
            type: string
        steps:
          - name: create-source
            image: alpine:3.18
            env:
              - name: APP
                value: "$(params.app-name)"
            script: |
              #!/bin/sh
              set -e
              echo "Writing source to: $(workspaces.source.path)"
              mkdir -p "$(workspaces.source.path)/src"
              mkdir -p "$(workspaces.source.path)/tests"

              # Simulate writing application code
              cat > "$(workspaces.source.path)/src/main.go" << 'EOF'
              package main

              import "fmt"

              func main() {
                  fmt.Println("Hello from", "$(APP)")
              }
              EOF

              cat > "$(workspaces.source.path)/Makefile" << 'EOF'
              test:
              	go test ./...
              build:
              	go build -o dist/$(APP) ./src
              EOF

              echo "Reading config from workspace..."
              cat "$(workspaces.config.path)/config.yaml"
              echo ""
              echo "Source tree:"
              find "$(workspaces.source.path)" -type f
      params:
        - name: app-name
          value: "$(params.app-name)"

    # Task 2: Read source from workspace and 'build' it
    - name: build
      runAfter:
        - write-source
      workspaces:
        - name: source
          workspace: source
        - name: config
          workspace: build-config
        - name: credentials
          workspace: registry-credentials
      taskSpec:
        workspaces:
          - name: source
          - name: config
          - name: credentials
            optional: true
        params:
          - name: app-name
            type: string
        results:
          - name: artifact-path
            description: "Path to the built artifact in the workspace"
        steps:
          - name: build-app
            image: alpine:3.18
            env:
              - name: APP
                value: "$(params.app-name)"
            script: |
              #!/bin/sh
              set -e
              echo "=== Build Stage ==="
              echo "Reading source from: $(workspaces.source.path)"
              echo ""
              echo "Source files found:"
              find "$(workspaces.source.path)" -type f
              echo ""
              echo "Build config:"
              cat "$(workspaces.config.path)/config.yaml"
              echo ""

              # Simulate building
              mkdir -p "$(workspaces.source.path)/dist"
              echo "#!/bin/sh" > "$(workspaces.source.path)/dist/$APP"
              echo "echo 'I am $APP'" >> "$(workspaces.source.path)/dist/$APP"
              chmod +x "$(workspaces.source.path)/dist/$APP"

              ARTIFACT="$(workspaces.source.path)/dist/$APP"
              echo "Built artifact: $ARTIFACT"
              printf '%s' "$ARTIFACT" | tee "$(results.artifact-path.path)"
              echo ""

              # Check if credentials workspace was provided
              if [ "$(workspaces.credentials.bound)" = "true" ]; then
                echo "Registry credentials are available"
                ls "$(workspaces.credentials.path)/"
              else
                echo "No registry credentials β€” skipping push (dry-run mode)"
              fi
      params:
        - name: app-name
          value: "$(params.app-name)"

    # Task 3: Verify the artifact exists in the workspace
    - name: verify
      runAfter:
        - build
      workspaces:
        - name: source
          workspace: source
      taskSpec:
        workspaces:
          - name: source
        steps:
          - name: check-artifact
            image: alpine:3.18
            script: |
              #!/bin/sh
              echo "=== Verification ==="
              echo "Checking workspace contents after build:"
              find "$(workspaces.source.path)" -type f -exec ls -la {} +
              echo ""
              echo "All files persisted correctly across tasks!"
              echo "This is the power of shared PVC workspaces."
04-pipelinerun.yaml
apiVersion: tekton.dev/v1
kind: PipelineRun
metadata:
  name: workspace-demo-run-001
spec:
  pipelineRef:
    name: workspace-demo-pipeline
  params:
    - name: app-name
      value: "myapp-v1"
  # Bind the workspace slots to actual Kubernetes resources
  workspaces:
    - name: source
      # Bind to our pre-created PVC
      persistentVolumeClaim:
        claimName: pipeline-workspace-pvc
    - name: build-config
      # Bind to our ConfigMap β€” mounted as files
      configMap:
        name: build-config
    # NOTE: registry-credentials is optional, so omitting it is fine
    # To include it you would add:
    # - name: registry-credentials
    #   secret:
    #     secretName: registry-push-secret

Commands

terminal
# Apply storage resources
kubectl apply -f 04-workspace-resources.yaml

# Verify PVC was created (it will be Pending until a task claims it)
kubectl get pvc pipeline-workspace-pvc

# Apply the pipeline
kubectl apply -f 04-workspace-pipeline.yaml

# Start the pipeline run
kubectl apply -f 04-pipelinerun.yaml

# Follow logs across all tasks
tkn pipelinerun logs workspace-demo-run-001 -f

# Inspect workspace bindings in the PipelineRun spec
kubectl get pipelinerun workspace-demo-run-001 -o yaml | grep -A 20 workspaces

# After completion β€” PVC is still there with the built artifact
kubectl get pvc pipeline-workspace-pvc

# Show workspace types: emptyDir variant
tkn pipeline start workspace-demo-pipeline \
  --workspace name=source,emptyDir="" \
  --workspace name=build-config,config=build-config \
  --param app-name="emptydir-test" \
  --showlog

# Create a registry secret to demonstrate optional workspace binding
kubectl create secret docker-registry registry-push-secret \
  --docker-server=quay.io \
  --docker-username=myuser \
  --docker-password=mypassword

Konflux Connection

βš™ How This Applies to Konflux

Workspaces are central to Konflux's pipeline design:

  • workspace (PVC) β€” all Konflux build tasks share this single PVC. The clone-repository task writes the full source tree here; build-container reads it.
  • git-auth (Secret) β€” injected by Konflux with the credentials to clone private repositories. Your Task can read $(workspaces.git-auth.path)/.git-credentials.
  • registry-auth (Secret) β€” Konflux mounts push credentials here so the Buildah task can push images without hardcoding any passwords.
  • netrc (Secret, optional) β€” for private Go modules or npm registries in hermetic builds.

When you see workspaces.workspace.bound checks in Konflux tasks, that's the optional workspace pattern you just learned.

Key Takeaways

  • Workspaces enable file sharing across Tasks in a Pipeline
  • PVC = persistent, shared; emptyDir = ephemeral, per-TaskRun
  • ConfigMap/Secret workspaces inject config as files
  • Pipeline binds workspace slots; PipelineRun provides actual values
  • Optional workspaces allow conditional feature activation
  • Always use $(workspaces.name.path) not hardcoded paths
Triggers
Intermediate⏱ 32 min

Tekton Triggers β€” Event-Driven Pipelines with GitHub Webhooks

What You'll Learn

The three Trigger CRDs: EventListener, TriggerBinding, TriggerTemplate
How HTTP webhooks become PipelineRuns
Extracting values from webhook payloads with TriggerBinding
RBAC setup required for Triggers to create pipeline resources
Testing triggers locally with curl and ngrok
GitHub webhook configuration end-to-end

Demo Steps

  1. Install Tekton Triggers β€” apply the release manifest, verify pods in tekton-pipelines namespace.
  2. Apply RBAC β€” ServiceAccount, Role, and RoleBinding from 05-triggers-rbac.yaml.
  3. Apply TriggerBinding and TriggerTemplate β€” explain how payload fields map to template params.
  4. Apply EventListener β€” port-forward to reach it locally.
  5. Test with curl β€” simulate a GitHub push event payload and watch the PipelineRun get created automatically.
  6. Use ngrok to expose the EventListener, add webhook in GitHub settings, and push a real commit to trigger the pipeline.

YAML Files

05-triggers-rbac.yaml
---
# ServiceAccount used by the EventListener Pod
apiVersion: v1
kind: ServiceAccount
metadata:
  name: tekton-triggers-sa
  namespace: default
---
# Role granting the SA permission to create pipeline resources
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: tekton-triggers-role
  namespace: default
rules:
  # Allow creating PipelineRuns
  - apiGroups: ["tekton.dev"]
    resources: ["pipelineruns", "taskruns"]
    verbs: ["create", "get", "list", "watch"]
  # Allow reading existing Pipelines and Tasks
  - apiGroups: ["tekton.dev"]
    resources: ["pipelines", "tasks"]
    verbs: ["get", "list"]
  # Allow creating PVCs for workspaces
  - apiGroups: [""]
    resources: ["persistentvolumeclaims"]
    verbs: ["create", "delete", "get", "list"]
  # Allow reading Secrets (for webhook secret validation)
  - apiGroups: [""]
    resources: ["secrets"]
    verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: tekton-triggers-rolebinding
  namespace: default
subjects:
  - kind: ServiceAccount
    name: tekton-triggers-sa
    namespace: default
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: tekton-triggers-role
---
# ClusterRole granting access to cluster-scoped Triggers resources.
# ClusterInterceptors live at the cluster scope so a Role (namespace-only)
# cannot grant access to them β€” a ClusterRole is required.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: tekton-triggers-eventlistener-clusterrole
rules:
  - apiGroups: ["triggers.tekton.dev"]
    resources:
      - clusterinterceptors       # cluster-scoped β€” the key missing permission
      - clustertriggerbindings
      - eventlisteners
      - triggerbindings
      - triggertemplates
      - triggers
      - interceptors
    verbs: ["get", "list", "watch"]
---
# Bind the EventListener SA to the ClusterRole above
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: tekton-triggers-cluster-binding
subjects:
  - kind: ServiceAccount
    name: tekton-triggers-sa
    namespace: default
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: tekton-triggers-eventlistener-clusterrole
05-triggerbinding.yaml
apiVersion: triggers.tekton.dev/v1beta1
kind: TriggerBinding
metadata:
  name: github-push-binding
spec:
  params:
    # Extract values from the GitHub push webhook JSON payload
    - name: git-repo-url
      value: $(body.repository.clone_url)
    - name: git-repo-name
      value: $(body.repository.name)
    - name: git-revision
      value: $(body.head_commit.id)
    - name: git-ref
      value: $(body.ref)
    - name: pusher-name
      value: $(body.pusher.name)
05-triggertemplate.yaml
apiVersion: triggers.tekton.dev/v1beta1
kind: TriggerTemplate
metadata:
  name: github-push-template
spec:
  # These params receive values from the TriggerBinding
  params:
    - name: git-repo-url
      description: "URL of the repository that was pushed to"
    - name: git-repo-name
      description: "Short name of the repository"
    - name: git-revision
      description: "Commit SHA that was pushed"
      default: "main"
    - name: git-ref
      description: "Git ref (branch or tag)"
      default: "refs/heads/main"
    - name: pusher-name
      description: "GitHub username of who pushed"
      default: "unknown"
  resourcetemplates:
    # This is the resource that gets created for each event
    - apiVersion: tekton.dev/v1
      kind: PipelineRun
      metadata:
        # generateName adds a random suffix for uniqueness
        generateName: triggered-build-
        labels:
          triggers.tekton.dev/trigger: github-push
          app.kubernetes.io/managed-by: tekton-triggers
        annotations:
          triggered-by: "$(tt.params.pusher-name)"
          git-revision: "$(tt.params.git-revision)"
      spec:
        pipelineRef:
          name: code-quality-pipeline
        params:
          - name: repo-url
            value: "$(tt.params.git-repo-url)"
          - name: revision
            value: "$(tt.params.git-revision)"
        # code-quality-pipeline (Video 3) has no workspace declarations,
        # so no workspaces block is needed here.
05-eventlistener.yaml
apiVersion: triggers.tekton.dev/v1beta1
kind: EventListener
metadata:
  name: github-push-listener
spec:
  serviceAccountName: tekton-triggers-sa
  triggers:
    - name: github-push-trigger
      # Interceptors run BEFORE the binding β€” they validate and filter events
      interceptors:
        - ref:
            name: github
            kind: ClusterInterceptor
          params:
            # Validate the webhook secret (set this in GitHub webhook settings)
            - name: secretRef
              value:
                secretName: github-webhook-secret
                secretKey: secret
            # Only process push events (not PRs, issues, etc.)
            - name: eventTypes
              value:
                - push
        - ref:
            name: cel
            kind: ClusterInterceptor
          params:
            # CEL filter: only trigger for pushes to main branch
            - name: filter
              value: "body.ref == 'refs/heads/main'"
            # CEL overlays: add computed values to the event context
            - name: overlays
              value:
                - key: branch_name
                  expression: "body.ref.split('/')[2]"
      # Map the interceptor output to template params
      bindings:
        - ref: github-push-binding
      template:
        ref: github-push-template
05-webhook-secret.yaml (create before applying EventListener)
apiVersion: v1
kind: Secret
metadata:
  name: github-webhook-secret
type: Opaque
stringData:
  # Use this same value in GitHub webhook settings β†’ Secret field
  secret: "my-super-secret-webhook-key-change-this"

Commands

terminal
# Install Tekton Triggers
kubectl apply -f https://storage.googleapis.com/tekton-releases/triggers/latest/release.yaml
kubectl apply -f https://storage.googleapis.com/tekton-releases/triggers/latest/interceptors.yaml

# Wait for triggers components to be ready
kubectl wait --for=condition=ready pod \
  --selector=app=tekton-triggers-controller \
  --namespace=tekton-pipelines \
  --timeout=120s

# Apply RBAC, secret, and trigger resources in order
kubectl apply -f 05-webhook-secret.yaml
kubectl apply -f 05-triggers-rbac.yaml
kubectl apply -f 05-triggerbinding.yaml
kubectl apply -f 05-triggertemplate.yaml
kubectl apply -f 05-eventlistener.yaml

# Verify EventListener pod is running
kubectl get pods | grep el-github-push-listener
kubectl get eventlistener github-push-listener

# Port-forward the EventListener service for local testing
kubectl port-forward svc/el-github-push-listener 8080:8080 &

# Simulate a GitHub push webhook with curl.
# The GitHub interceptor validates the HMAC-SHA256 signature β€” it MUST match
# the webhook secret. Compute it from the exact payload bytes before sending.

PAYLOAD='{"ref":"refs/heads/main","head_commit":{"id":"abc123def456"},"repository":{"name":"my-repo","clone_url":"https://github.com/myorg/my-repo"},"pusher":{"name":"developer1"}}'
SECRET="my-super-secret-webhook-key-change-this"
SIG=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')

curl -v -X POST http://localhost:8080 \
  -H 'Content-Type: application/json' \
  -H 'X-GitHub-Event: push' \
  -H "X-Hub-Signature-256: sha256=${SIG}" \
  -d "$PAYLOAD"

# Watch the PipelineRun get created automatically
kubectl get pipelineruns -w

# For real GitHub integration β€” use ngrok to expose the service
ngrok http 8080
# Copy the https ngrok URL and add it in:
# GitHub repo Settings > Webhooks > Add webhook
# Payload URL: https://your-ngrok-url.ngrok.io
# Content type: application/json
# Secret: my-super-secret-webhook-key-change-this
# Events: Just the push event

Konflux Connection

βš™ How This Applies to Konflux

Konflux uses Tekton Triggers extensively but wraps them in a higher-level abstraction:

  • Konflux has a PaC (Pipeline as Code) controller that registers webhooks on your repositories automatically when you add a Component
  • When you push code, GitHub calls Konflux's webhook receiver, which validates the event and creates a PipelineRun β€” exactly what our EventListener does here
  • The PaC controller handles the TriggerBinding/TriggerTemplate logic internally, extracting repo URL, commit SHA, and PR number from the webhook payload
  • Konflux also supports .tekton/ directory in your repo where you can define pipeline customizations using Pipeline as Code syntax
  • Understanding Triggers is key to debugging why a push to your Component didn't trigger a build

Key Takeaways

  • EventListener = HTTP server that receives webhook events
  • TriggerBinding = extracts fields from the webhook payload
  • TriggerTemplate = defines what resources to create per event
  • Interceptors validate/filter events before processing
  • RBAC is required for the EventListener to create PipelineRuns
  • CEL expressions enable powerful event filtering logic
Artifact Hub
Intermediate⏱ 30 min

Artifact Hub & Community Tasks β€” Building Real CI Without Starting from Scratch

What You'll Learn

Why Tekton Hub is deprecated and how Artifact Hub replaces it
Browsing and installing community Tasks from Artifact Hub
Using the git-clone catalog task correctly
Building container images with the buildah catalog task
Wiring catalog tasks into a coherent pipeline
Understanding resolver-based task references (http/git/bundle resolvers)

Demo Steps

  1. Browse Artifact Hub β€” open artifacthub.io/packages/search?kind=7, find git-clone and buildah tasks, show the YAML and version list.
  2. Find tasks on Artifact Hub β€” open artifacthub.io and search for git-clone and buildah; show the Install tab with the raw GitHub URL.
  3. Create the pipeline that wires git-clone β†’ buildah via the http resolver (no cluster installation needed).
  4. Create a container registry secret for pushing images.
  5. Run the pipeline against a sample public repository with a Containerfile.
  6. Show the bundle resolver approach as used by Konflux β€” task definitions fetched from OCI at runtime.

YAML Files

06-registry-secret.yaml (create your own credentials first)
---
# The buildah catalog task mounts the dockerconfig workspace and looks for
# a file named exactly "config.json" at the workspace root.
#
# DO NOT use "kubectl create secret docker-registry" β€” that command creates
# a secret with the key ".dockerconfigjson", which buildah cannot find.
#
# CORRECT approach β€” key must be "config.json":
#
# Option A: reuse your local Docker / Podman login (simplest):
#   kubectl create secret generic registry-credentials \
#     --from-file=config.json=$HOME/.docker/config.json
#
# Option B: create from scratch for docker.io:
#   AUTH=$(echo -n "YOUR_USERNAME:YOUR_TOKEN" | base64)
#   kubectl create secret generic registry-credentials \
#     --from-literal=config.json="{\"auths\":{\"https://index.docker.io/v1/\":{\"auth\":\"$AUTH\"}}}"
#
# Option C: create from scratch for quay.io:
#   AUTH=$(echo -n "YOUR_USERNAME:YOUR_TOKEN" | base64)
#   kubectl create secret generic registry-credentials \
#     --from-literal=config.json="{\"auths\":{\"quay.io\":{\"auth\":\"$AUTH\"}}}"
#
# The YAML structure for reference (apply after base64-encoding config.json):
apiVersion: v1
kind: Secret
metadata:
  name: registry-credentials
type: Opaque
data:
  # base64 of the full config.json contents (NOT the dockerconfigjson format)
  config.json: BASE64_OF_CONFIG_JSON_HERE
06-ci-pipeline-catalog.yaml (uses catalog tasks via http resolver β€” no hub install needed)
apiVersion: tekton.dev/v1
kind: Pipeline
metadata:
  name: container-ci-pipeline
  annotations:
    tekton.dev/displayName: "Container CI Pipeline"
    tekton.dev/description: "Clone, build, and push a container image using catalog tasks"
spec:
  params:
    - name: git-url
      type: string
      description: "Repository URL to clone and build"
    - name: git-revision
      type: string
      default: ""   # empty = HEAD of default branch (works for main, master, etc.)
    - name: image-name
      type: string
      description: "Target image (e.g. quay.io/myorg/myapp)"
    - name: dockerfile-path
      type: string
      default: "./Dockerfile"
      description: "Path to the Dockerfile relative to repo root"
    - name: context-path
      type: string
      default: "."
      description: "Build context path"
  results:
    - name: image-url
      description: "Full URL of the pushed image"
      value: "$(tasks.build-image.results.IMAGE_URL)"
    - name: image-digest
      description: "SHA256 digest of the pushed image"
      value: "$(tasks.build-image.results.IMAGE_DIGEST)"
    - name: git-commit
      description: "Exact commit SHA that was built"
      value: "$(tasks.clone-repo.results.commit)"
  workspaces:
    - name: source
      description: "Shared workspace: source code lives here"
    - name: registry-auth
      description: "Docker config secret for registry authentication"
  tasks:
    # TASK 1: Clone the repository
    - name: clone-repo
      taskRef:
        # Use the resolver to reference the catalog task directly
        # No kubectl apply needed β€” Tekton fetches it from the catalog
        resolver: http
        params:
          - name: url
            value: https://raw.githubusercontent.com/tektoncd/catalog/main/task/git-clone/0.9/git-clone.yaml
      params:
        - name: url
          value: "$(params.git-url)"
        - name: revision
          value: "$(params.git-revision)"
        - name: deleteExisting
          value: "true"
      workspaces:
        - name: output
          workspace: source

    # TASK 2: Build and push the container image with Buildah
    - name: build-image
      runAfter:
        - clone-repo
      taskRef:
        resolver: http
        params:
          - name: url
            value: https://raw.githubusercontent.com/tektoncd/catalog/main/task/buildah/0.7/buildah.yaml
      params:
        - name: IMAGE
          value: "$(params.image-name):$(tasks.clone-repo.results.commit)"
        - name: DOCKERFILE
          value: "$(params.dockerfile-path)"
        - name: CONTEXT
          value: "$(params.context-path)"
        - name: TLSVERIFY
          value: "false"
        - name: FORMAT
          value: "oci"
      workspaces:
        - name: source
          workspace: source
        - name: dockerconfig
          workspace: registry-auth
06-pipeline-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: ci-pipeline-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
06-pipelinerun.yaml
apiVersion: tekton.dev/v1
kind: PipelineRun
metadata:
  name: container-ci-run-001
spec:
  pipelineRef:
    name: container-ci-pipeline
  params:
    # traefik/whoami β€” tiny Go HTTP server, minimal self-contained Dockerfile,
    # no external dependencies, builds in seconds. Good demo target.
    # Swap git-url for your own application repo when ready.
    # git-revision is left empty so git-clone uses HEAD of the default branch
    # automatically β€” works whether the repo uses main, master, or anything else.
    - name: git-url
      value: "https://github.com/traefik/whoami"
    - name: git-revision
      value: ""
    # Replace with your own registry + image name
    - name: image-name
      value: "quay.io/YOURORG/whoami-demo"
    - name: dockerfile-path
      value: "./Dockerfile"
    - name: context-path
      value: "."
  workspaces:
    - name: source
      persistentVolumeClaim:
        claimName: ci-pipeline-pvc
    - name: registry-auth
      secret:
        secretName: registry-credentials

Commands

terminal
# ── Tekton Hub is fully deprecated β€” tkn hub commands do NOT work ──
# The hub.tekton.dev API is offline. The tkn hub CLI has no working
# replacement for search/install yet. Use one of these two approaches:

# ── OPTION A: Browse Artifact Hub in your browser ───────────────
# https://artifacthub.io/packages/search?kind=7
# Find a task, click it, copy the raw GitHub YAML URL from the Install tab.

# ── OPTION B (preferred): Apply task YAML directly from GitHub ───
# No hub needed β€” point kubectl at the raw file in tektoncd/catalog.
kubectl apply -f https://raw.githubusercontent.com/tektoncd/catalog/main/task/git-clone/0.9/git-clone.yaml
kubectl apply -f https://raw.githubusercontent.com/tektoncd/catalog/main/task/buildah/0.7/buildah.yaml

# Verify tasks landed in the cluster
tkn task list
tkn task describe git-clone   # shows params, workspaces, results

# ── OPTION C (most modern): http resolver β€” zero installation ────
# The pipeline in 06-ci-pipeline-catalog.yaml already uses this.
# Tekton fetches the task YAML from GitHub at runtime per PipelineRun,
# so you never need to kubectl apply the tasks at all.

# Create the PVC
kubectl apply -f 06-pipeline-pvc.yaml

# Create registry credentials.
# IMPORTANT: buildah looks for a file named "config.json" at the workspace
# root β€” NOT ".dockerconfigjson". Use "kubectl create secret generic" with
# --from-file=config.json, NOT "kubectl create secret docker-registry".

# Option A β€” reuse an existing local Docker/Podman login (simplest):
kubectl create secret generic registry-credentials \
  --from-file=config.json=$HOME/.docker/config.json

# Option B β€” create inline for docker.io (replace USER and TOKEN):
AUTH=$(echo -n "YOUR_USERNAME:YOUR_TOKEN" | base64)
kubectl create secret generic registry-credentials \
  --from-literal=config.json="{\"auths\":{\"https://index.docker.io/v1/\":{\"auth\":\"$AUTH\"}}}"

# Option C β€” create inline for quay.io:
AUTH=$(echo -n "YOUR_USERNAME:YOUR_TOKEN" | base64)
kubectl create secret generic registry-credentials \
  --from-literal=config.json="{\"auths\":{\"quay.io\":{\"auth\":\"$AUTH\"}}}"

# Apply and run the pipeline
kubectl apply -f 06-ci-pipeline-catalog.yaml
kubectl apply -f 06-pipelinerun.yaml

# Follow the logs
tkn pipelinerun logs container-ci-run-001 -f

# When complete, check the results (image URL and digest)
tkn pipelinerun describe container-ci-run-001

Konflux Connection

βš™ How This Applies to Konflux

The Konflux build pipeline is a more sophisticated version of exactly this pattern:

  • Konflux uses OCI bundle references for all its tasks: quay.io/konflux-ci/tekton-catalog/task-git-clone:0.1
  • The bundle resolver fetches the task definition from OCI at runtime β€” no installation needed, and you get full immutability via digest pinning
  • Konflux's build-container task uses Buildah exactly like this demo, with the same IMAGE, DOCKERFILE, and CONTEXT params
  • The IMAGE_DIGEST result from build-container is the lynchpin of the whole supply chain: it flows into Tekton Chains for signing, into the vulnerability scanner, and eventually into the Konflux Snapshot
  • To see real Konflux task definitions: oc get task -n konflux-ci or browse the Konflux tekton-catalog repository

Key Takeaways

  • Tekton Hub is fully deprecated β€” use Artifact Hub or direct GitHub URLs
  • kubectl apply -f <raw-github-url> is the reliable way to install catalog tasks
  • Resolvers (http, git, bundle) fetch tasks at runtime β€” no cluster install needed
  • git-clone produces a commit result β€” tag images with it
  • Buildah is daemonless and preferred for Kubernetes-native builds
  • Pin task versions for reproducible pipelines
Advanced Patterns
Advanced⏱ 35 min

Advanced Pipeline Patterns β€” When, Finally, Matrix, and Embedded Tasks

What You'll Learn

when expressions for conditional task execution
finally block for guaranteed cleanup and notifications
matrix for fan-out parallel execution over a list
Inline taskSpec vs external taskRef
Accessing pipeline run status in finally tasks
timeout and retry fields on pipeline tasks

Demo Steps

  1. Build the advanced pipeline step by step β€” start with just the main tasks, then add when conditions.
  2. Run with skip-tests=true β€” show that the test task is skipped but all others run.
  3. Add the finally block β€” show it running even when a task fails.
  4. Add matrix β€” show three parallel TaskRuns being created from one pipeline task definition.
  5. Add retries and timeout β€” demonstrate a flaky task being retried automatically.

YAML Files

07-advanced-pipeline.yaml
apiVersion: tekton.dev/v1
kind: Pipeline
metadata:
  name: advanced-patterns-pipeline
spec:
  params:
    - name: skip-tests
      type: string
      default: "false"
      description: "Set to 'true' to skip the test stage (use with caution)"
    - name: run-security-scan
      type: string
      default: "true"
      description: "Set to 'false' to skip security scanning"
    - name: platforms
      type: array
      default:
        - "linux/amd64"
        - "linux/arm64"
      description: "List of platforms to build for"
    - name: app-name
      type: string
      default: "myapp"

  workspaces:
    - name: source

  tasks:
    # Always runs β€” validates configuration
    - name: validate
      taskSpec:
        steps:
          - name: check
            image: alpine:3.18
            script: |
              #!/bin/sh
              echo "Validating pipeline configuration..."
              echo "App: $(params.app-name)"
              echo "Skip tests: $(params.skip-tests)"
              echo "Validation passed."
      params:
        - name: app-name
          value: "$(params.app-name)"
        - name: skip-tests
          value: "$(params.skip-tests)"

    # Conditional: only runs if skip-tests != "true"
    - name: run-tests
      runAfter:
        - validate
      when:
        - input: "$(params.skip-tests)"
          operator: in
          values: ["false", "False", "FALSE", ""]
      taskSpec:
        params:
          - name: app-name
        steps:
          - name: test
            image: alpine:3.18
            script: |
              #!/bin/sh
              echo "Running unit tests for $(params.app-name)..."
              sleep 3
              echo "42 tests passed, 0 failed."
      params:
        - name: app-name
          value: "$(params.app-name)"

    # Matrix: builds for each platform in parallel
    # One TaskRun is created per platform value
    - name: build-multiarch
      runAfter:
        - validate
      matrix:
        params:
          - name: platform
            value:
              - "$(params.platforms[0])"
              - "$(params.platforms[1])"
      taskSpec:
        params:
          - name: platform
            type: string
          - name: app-name
            type: string
        results:
          - name: build-status
        steps:
          - name: build-for-platform
            image: alpine:3.18
            script: |
              #!/bin/sh
              PLATFORM="$(params.platform)"
              APP="$(params.app-name)"
              echo "Building $APP for platform: $PLATFORM"
              sleep 4
              echo "Build complete for $PLATFORM"
              printf '%s' "success-$PLATFORM" | tee "$(results.build-status.path)"
      params:
        - name: app-name
          value: "$(params.app-name)"

    # Conditional: skip if run-security-scan is false
    # Also uses retry β€” will retry up to 2 times on failure
    - name: security-scan
      runAfter:
        - build-multiarch
      when:
        - input: "$(params.run-security-scan)"
          operator: in
          values: ["true", "True", "TRUE"]
      retries: 2
      timeout: "10m"
      taskSpec:
        params:
          - name: app-name
        steps:
          - name: scan
            image: alpine:3.18
            script: |
              #!/bin/sh
              echo "Running security scan on $(params.app-name)..."
              sleep 2
              echo "Scan complete. No critical vulnerabilities found."
      params:
        - name: app-name
          value: "$(params.app-name)"

  # FINALLY block β€” always executes, even if above tasks fail
  finally:
    - name: notify-result
      taskSpec:
        params:
          - name: pipeline-run-name
            type: string
        steps:
          - name: send-notification
            image: alpine:3.18
            env:
              - name: PIPELINERUN_NAME
                value: "$(params.pipeline-run-name)"
            script: |
              #!/bin/sh
              echo "=== Pipeline Completion Notification ==="
              echo "PipelineRun: $PIPELINERUN_NAME"
              echo "Namespace: $(context.pipelineRun.namespace)"
              echo ""
              # In a real pipeline this would call a Slack webhook or
              # post a GitHub status comment
              echo "Status: Pipeline finished (check PipelineRun for final status)"
              echo "Timestamp: $(date -u)"
              echo "This finally task always runs, regardless of success or failure."
      params:
        - name: pipeline-run-name
          value: "$(context.pipelineRun.name)"

    # Cleanup: delete temporary resources
    - name: cleanup-workspace
      taskSpec:
        steps:
          - name: cleanup
            image: alpine:3.18
            script: |
              #!/bin/sh
              echo "Cleaning up temporary files..."
              echo "In a real pipeline: rm -rf /workspace/source/tmp"
              echo "Or delete temporary PVCs, ConfigMaps, etc."
              echo "Cleanup complete."
07-pipelinerun-skip-tests.yaml (demonstrate when expression)
apiVersion: tekton.dev/v1
kind: PipelineRun
metadata:
  name: advanced-run-skip-tests
spec:
  pipelineRef:
    name: advanced-patterns-pipeline
  params:
    - name: app-name
      value: "demo-app"
    - name: skip-tests
      # This will cause the run-tests task to be SKIPPED (not failed)
      value: "true"
    - name: run-security-scan
      value: "true"
    - name: platforms
      value:
        - "linux/amd64"
        - "linux/arm64"
  workspaces:
    - name: source
      emptyDir: {}

Commands

terminal
# Apply the pipeline
kubectl apply -f 07-advanced-pipeline.yaml

# Run 1: Normal run (all tasks execute)
tkn pipeline start advanced-patterns-pipeline \
  --param app-name="myapp" \
  --param skip-tests="false" \
  --param run-security-scan="true" \
  --workspace name=source,emptyDir="" \
  --showlog

# Run 2: Skip tests using when expression
kubectl apply -f 07-pipelinerun-skip-tests.yaml
tkn pipelinerun logs advanced-run-skip-tests -f

# After run β€” see which tasks were SKIPPED vs COMPLETED
tkn pipelinerun describe advanced-run-skip-tests

# See that finally tasks ran even if main tasks are in any state
kubectl get taskruns --selector=tekton.dev/pipelineRun=advanced-run-skip-tests

# Test retry behavior β€” temporarily break a task to see retries
# Edit the pipeline, change security-scan to exit 1, apply, run again

# Check matrix TaskRuns (one per platform)
kubectl get taskruns --selector=tekton.dev/pipelineTask=build-multiarch

Konflux Connection

βš™ How This Applies to Konflux

  • when expressions: Konflux uses these extensively. The skip-checks param in Konflux pipelines skips non-critical scanning tasks. SAST and ClamAV tasks have when guards based on component type and configuration.
  • finally: Konflux's finally block contains tasks that upload SBOM data and post the pipeline run result back to the Konflux API β€” regardless of whether the build succeeded.
  • matrix: Multi-arch Konflux builds use matrix to invoke Buildah for each target platform, then merge the manifests into a multi-arch manifest list.
  • retries: Konflux sets retries: 0 on most tasks β€” failures are intentionally surfaced immediately. The exception is tasks that call external services (like SBOM upload) which may have retries: 3.

Key Takeaways

  • when skips tasks conditionally β€” it's not a failure
  • finally always runs β€” use for cleanup and notifications
  • matrix fans out one task over a list in parallel
  • retries automatically re-runs a failed task N times
  • timeout kills a runaway task after a duration
  • Finally tasks can read $(context.pipelineRun.name) and namespace
Chains & Security
Advanced⏱ 30 min

Tekton Chains β€” Supply Chain Security, SLSA Attestations & Image Signing

What You'll Learn

What SLSA is and why supply chain security matters
How Tekton Chains works as a passive observer of TaskRuns
Installing and configuring Tekton Chains
Generating a cosign key pair for image signing
Verifying image signatures and SLSA attestations
How Konflux uses Chains for SLSA Level 3 compliance

Demo Steps

  1. Install Tekton Chains β€” apply the release manifest, verify the controller pod.
  2. Generate a cosign key pair with cosign generate-key-pair k8s://tekton-chains/signing-secrets.
  3. Configure Chains β€” patch the chains-config ConfigMap to set storage backend and signing format.
  4. Run a build pipeline β€” use the pipeline from Video 6 to build and push a real image.
  5. Watch Chains process the TaskRun β€” show the chains.tekton.dev/signed annotation appearing.
  6. Verify the signature with cosign verify and inspect the attestation with cosign verify-attestation.

YAML Files

08-chains-config.yaml (patch the chains-config ConfigMap)
apiVersion: v1
kind: ConfigMap
metadata:
  name: chains-config
  namespace: tekton-chains
data:
  # Where to store the signed attestations.
  # "oci" attaches them as OCI referrers alongside the image (recommended).
  artifacts.oci.storage: "oci"
  artifacts.taskrun.storage: "oci"
  artifacts.pipelinerun.storage: "oci"

  # Signing format for OCI images.
  # artifacts.oci.format ONLY accepts "simplesigning" β€” it is an image signing
  # format, not an attestation format. "slsa/v1" is invalid here and will crash
  # the Chains controller on startup.
  artifacts.oci.format: "simplesigning"

  # Attestation format for TaskRun and PipelineRun provenance documents.
  # "in-toto" is supported across all current Chains versions.
  # "slsa/v1" requires Chains v0.21+ β€” use "in-toto" for broad compatibility.
  artifacts.taskrun.format: "in-toto"
  artifacts.pipelinerun.format: "in-toto"

  # Transparency log β€” set to false for local/air-gapped dev clusters.
  # Set to true in production to publish to the Sigstore Rekor public log.
  transparency.enabled: "false"
  transparency.url: "https://rekor.sigstore.dev"
08-signing-pipeline.yaml (a Task that emits IMAGE_URL and IMAGE_DIGEST results so Chains can pick them up)
apiVersion: tekton.dev/v1
kind: Task
metadata:
  name: simulate-build-and-push
  annotations:
    # Tell Chains which results contain the image coordinates
    chains.tekton.dev/output-image-name: IMAGE_URL
    chains.tekton.dev/output-image-digest: IMAGE_DIGEST
spec:
  description: |
    Simulates building and pushing a container image.
    In a real pipeline this would be the Buildah task.
    Chains watches for the IMAGE_URL and IMAGE_DIGEST results.
  params:
    - name: image-name
      type: string
      description: "Full image reference including tag (e.g. quay.io/org/app:v1.0)"
  results:
    # Chains REQUIRES these specific result names to detect the image
    - name: IMAGE_URL
      description: "The full URL of the built image"
    - name: IMAGE_DIGEST
      description: "The SHA256 digest of the pushed image (sha256:...)"
  steps:
    - name: build-and-push
      image: alpine:3.18
      env:
        - name: IMAGE
          value: "$(params.image-name)"
      script: |
        #!/bin/sh
        set -e
        echo "=== Simulating Build & Push ==="
        echo "Image: $IMAGE"
        echo "Simulating Buildah build..."
        sleep 2

        # In a real scenario, the registry returns this digest after push
        # Here we simulate a plausible digest
        DIGEST="sha256:$(echo $IMAGE | md5sum | head -c 64)"

        echo "Image built and pushed successfully!"
        echo "Image URL:    $IMAGE"
        echo "Image Digest: $DIGEST"

        # Write results β€” Chains reads these after the TaskRun completes
        printf '%s' "$IMAGE"  | tee "$(results.IMAGE_URL.path)"
        printf '%s' "$DIGEST" | tee "$(results.IMAGE_DIGEST.path)"

Commands

terminal
# Install Tekton Chains
kubectl apply -f https://storage.googleapis.com/tekton-releases/chains/latest/release.yaml

# Wait for Chains controller to be ready
kubectl wait --for=condition=ready pod \
  --selector=app=tekton-chains-controller \
  --namespace=tekton-chains \
  --timeout=120s

# Install cosign
brew install cosign  # macOS
# or: go install github.com/sigstore/cosign/cmd/cosign@latest

# Generate a cosign key pair, stored directly in Kubernetes as a Secret
cosign generate-key-pair k8s://tekton-chains/signing-secrets

# The public key is in cosign.pub (save this for verification)
# The private key is in the 'signing-secrets' Secret in tekton-chains namespace

# Configure Chains
kubectl apply -f 08-chains-config.yaml

# Restart Chains to pick up the new config
kubectl rollout restart deployment tekton-chains-controller -n tekton-chains

# Apply and run the signing task
kubectl apply -f 08-signing-pipeline.yaml
tkn task start simulate-build-and-push \
  --param image-name="quay.io/myorg/myapp:v1.0" \
  --showlog

# Watch for Chains to annotate the TaskRun (may take 30-60 seconds)
kubectl get taskruns --watch
# Look for: chains.tekton.dev/signed: "true"

# Once signed, verify the image signature
cosign verify --key cosign.pub quay.io/myorg/myapp:v1.0

# Verify and inspect the SLSA attestation
cosign verify-attestation \
  --key cosign.pub \
  --type slsaprovenance \
  quay.io/myorg/myapp:v1.0 | jq .

# The attestation contains:
# - builder.id (your Tekton pipeline)
# - invocation.parameters (what params were used)
# - materials (the git repo and commit SHA)
# - buildType (tekton.dev/v1/TaskRun)

Konflux Connection

βš™ How This Applies to Konflux

Tekton Chains is central to Konflux's security model:

  • Every Konflux build automatically gets SLSA Level 3 provenance β€” Chains signs the image and attestation with the cluster's signing key
  • Konflux uses Sigstore transparency log (Rekor) to make all attestations publicly auditable
  • The IMAGE_DIGEST result from Buildah is precisely what Chains uses to create the attestation β€” this is why that result is non-negotiable in any Konflux-compatible task
  • Enterprise Contract (EC) β€” another Konflux component β€” reads the Chains-generated attestations to enforce organizational policies before an image can be deployed
  • You can view the attestation for any Konflux-built image: cosign verify-attestation --certificate-identity-regexp ".*" --certificate-oidc-issuer "https://token.actions.githubusercontent.com" IMAGE

Key Takeaways

  • Chains is a passive observer β€” zero pipeline changes needed
  • It signs images & generates SLSA attestations automatically
  • Results named IMAGE_URL and IMAGE_DIGEST are the trigger
  • Attestations are stored as OCI artifacts β€” no separate DB
  • cosign verify and cosign verify-attestation for validation
  • Foundation for SLSA Level 3 β€” what Konflux achieves in prod
Container Build Pipeline
Advanced⏱ 40 min

Container Build Pipeline End-to-End β€” Clone, Test, Build, Scan, Sign, Push

What You'll Learn

Compose everything from Videos 1–8 into one production pipeline
The correct task ordering for a secure CI/CD pipeline
SBOM generation as part of the build
Vulnerability scanning with Trivy before pushing
Pipeline as Code β€” storing pipeline definitions in the repo
Parameterization for reuse across multiple applications

Demo Steps

  1. Create a sample application repo with a Go or Python app and a Containerfile.
  2. Apply the full pipeline β€” walk through each task and explain why it's in that position.
  3. Run it end-to-end against the sample repo and follow logs task by task.
  4. Show the Dashboard β€” demonstrate the full DAG with parallel and sequential tasks.
  5. Introduce the .tekton directory approach β€” show how to store the pipeline in the repo.
  6. Trigger via webhook using the Trigger setup from Video 5.

YAML Files

09-production-pipeline.yaml (full reference pipeline)
apiVersion: tekton.dev/v1
kind: Pipeline
metadata:
  name: production-container-pipeline
  annotations:
    tekton.dev/displayName: "Production Container CI Pipeline"
    tekton.dev/description: |
      Full CI pipeline: clone, test, build, scan, sign.
      Mirrors the structure of Konflux's default build pipeline.
spec:
  params:
    - name: git-url
      type: string
      description: "Repository URL"
    - name: git-revision
      type: string
      default: "main"
    - name: image-name
      type: string
      description: "Target image, e.g. quay.io/myorg/myapp"
    - name: dockerfile-path
      type: string
      default: "./Containerfile"
    - name: skip-tests
      type: string
      default: "false"
    - name: skip-vuln-scan
      type: string
      default: "false"

  results:
    - name: image-url
      value: "$(tasks.build-container.results.IMAGE_URL)"
    - name: image-digest
      value: "$(tasks.build-container.results.IMAGE_DIGEST)"
    - name: git-commit
      value: "$(tasks.clone-repository.results.commit)"

  workspaces:
    - name: source
      description: "Shared workspace β€” source code and build artifacts"
    - name: registry-auth
      description: "Container registry credentials (docker config secret)"
    # Uncomment for private repositories β€” mount a Secret with .git-credentials
    # or SSH keys so git-clone can authenticate.
    # - name: git-auth
    #   description: "Git credentials for private repos (optional)"
    #   optional: true

  tasks:
    # ── Stage 1: Clone ─────────────────────────────────────────────
    - name: clone-repository
      taskRef:
        resolver: http
        params:
          - name: url
            value: https://raw.githubusercontent.com/tektoncd/catalog/main/task/git-clone/0.9/git-clone.yaml
      params:
        - name: url
          value: "$(params.git-url)"
        - name: revision
          value: "$(params.git-revision)"
        - name: deleteExisting
          value: "true"
      workspaces:
        - name: output
          workspace: source
        # Uncomment for private repositories β€” maps the pipeline's git-auth
        # workspace slot to the git-clone task's basic-auth workspace.
        # - name: basic-auth
        #   workspace: git-auth

    # ── Stage 2: Test (skippable) ──────────────────────────────────
    - name: run-unit-tests
      runAfter:
        - clone-repository
      when:
        - input: "$(params.skip-tests)"
          operator: in
          values: ["false", ""]
      taskSpec:
        workspaces:
          - name: source
        params:
          - name: commit
            type: string
        steps:
          - name: test
            image: golang:1.24-alpine
            workingDir: "$(workspaces.source.path)"
            script: |
              #!/bin/sh
              set -e
              echo "Running unit tests at commit: $(params.commit)"
              # Detect project type
              if [ -f go.mod ]; then
                go test ./... -v -count=1
              elif [ -f package.json ]; then
                npm test
              elif [ -f requirements.txt ]; then
                pip install -r requirements.txt
                python -m pytest
              else
                echo "No recognized test runner β€” skipping"
              fi
      params:
        - name: commit
          value: "$(tasks.clone-repository.results.commit)"
      workspaces:
        - name: source
          workspace: source

    # ── Stage 3: Build Container Image ────────────────────────────
    - name: build-container
      runAfter:
        - run-unit-tests
      taskSpec:
        params:
          - name: image
            type: string
          - name: dockerfile
            type: string
          - name: commit
            type: string
        results:
          - name: IMAGE_URL
            description: "Full image URL with tag"
          - name: IMAGE_DIGEST
            description: "SHA256 digest of pushed image"
        workspaces:
          - name: source
          - name: registry-auth
        steps:
          - name: build-and-push
            image: quay.io/buildah/stable:latest
            securityContext:
              privileged: true
            env:
              - name: IMAGE
                value: "$(params.image):$(params.commit)"
              - name: DOCKERFILE
                value: "$(params.dockerfile)"
              - name: DOCKERCONFIG
                value: "$(workspaces.registry-auth.path)/config.json"
            workingDir: "$(workspaces.source.path)"
            script: |
              #!/bin/bash
              set -euo pipefail
              export REGISTRY_AUTH_FILE="$DOCKERCONFIG"

              echo "Building image: $IMAGE"
              buildah build \
                --file "$DOCKERFILE" \
                --format oci \
                --tag "$IMAGE" \
                .

              echo "Pushing image to registry..."
              buildah push \
                --digestfile /tmp/image-digest \
                "$IMAGE"

              DIGEST=$(cat /tmp/image-digest)
              echo "Image pushed successfully"
              echo "URL:    $IMAGE"
              echo "Digest: $DIGEST"

              printf '%s' "$IMAGE"  | tee "$(results.IMAGE_URL.path)"
              printf '%s' "$DIGEST" | tee "$(results.IMAGE_DIGEST.path)"
      params:
        - name: image
          value: "$(params.image-name)"
        - name: dockerfile
          value: "$(params.dockerfile-path)"
        - name: commit
          value: "$(tasks.clone-repository.results.commit)"
      workspaces:
        - name: source
          workspace: source
        - name: registry-auth
          workspace: registry-auth

    # ── Stage 4a: Vulnerability Scan (skippable) ──────────────────
    - name: vulnerability-scan
      runAfter:
        - build-container
      when:
        - input: "$(params.skip-vuln-scan)"
          operator: in
          values: ["false", ""]
      taskSpec:
        params:
          - name: image-url
          - name: image-digest
        steps:
          - name: trivy-scan
            image: aquasec/trivy:latest
            env:
              - name: TRIVY_NO_PROGRESS
                value: "true"
              - name: TRIVY_EXIT_CODE
                value: "1"
              - name: TRIVY_SEVERITY
                value: "CRITICAL"
            script: |
              #!/bin/sh
              IMAGE="$(params.image-url)@$(params.image-digest)"
              echo "Scanning image: $IMAGE"
              trivy image \
                --exit-code "$TRIVY_EXIT_CODE" \
                --severity "$TRIVY_SEVERITY" \
                --format table \
                "$IMAGE"
      params:
        - name: image-url
          value: "$(tasks.build-container.results.IMAGE_URL)"
        - name: image-digest
          value: "$(tasks.build-container.results.IMAGE_DIGEST)"

    # ── Stage 4b: Generate SBOM ────────────────────────────────────
    - name: generate-sbom
      runAfter:
        - build-container
      taskSpec:
        params:
          - name: image-url
          - name: image-digest
        workspaces:
          - name: source
        steps:
          - name: syft-sbom
            # anchore/syft:latest is distroless β€” no shell, so Tekton cannot
            # inject its script file. Use alpine and install the syft binary.
            image: alpine:3.18
            env:
              - name: IMAGE
                value: "$(params.image-url)@$(params.image-digest)"
            workingDir: "$(workspaces.source.path)"
            script: |
              #!/bin/sh
              set -e
              echo "Installing syft..."
              apk add --no-cache curl ca-certificates
              curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh \
                | sh -s -- -b /usr/local/bin
              echo "Generating SBOM for: $IMAGE"
              syft "$IMAGE" -o cyclonedx-json > sbom.json
              echo "SBOM generated: $(wc -l < sbom.json) lines"
              echo "Top components:"
              head -50 sbom.json
      params:
        - name: image-url
          value: "$(tasks.build-container.results.IMAGE_URL)"
        - name: image-digest
          value: "$(tasks.build-container.results.IMAGE_DIGEST)"
      workspaces:
        - name: source
          workspace: source

  # ── Finally: Always runs ─────────────────────────────────────────
  finally:
    - name: pipeline-notification
      taskSpec:
        params:
          - name: image-url
            default: "unknown"
          - name: run-name
        steps:
          - name: notify
            image: alpine:3.18
            script: |
              #!/bin/sh
              echo "========================================"
              echo " Pipeline Complete"
              echo " Run:   $(params.run-name)"
              echo " Image: $(params.image-url)"
              echo " Time:  $(date -u)"
              echo "========================================"
              # In production: curl -X POST SLACK_WEBHOOK -d "{...}"
      params:
        - name: run-name
          value: "$(context.pipelineRun.name)"
        - name: image-url
          value: "$(tasks.build-container.results.IMAGE_URL)"
.tekton/push.yaml (Pipeline as Code β€” stored in your app repo)
# Store this file at .tekton/push.yaml in your application repository.
# Tekton PaC (Pipeline as Code) detects it and runs it on every push.
#
# Annotations control WHEN this pipeline runs:
apiVersion: tekton.dev/v1
kind: PipelineRun
metadata:
  name: push-build
  annotations:
    # PaC annotation: trigger on push to main
    pipelinesascode.tekton.dev/on-event: "[push]"
    pipelinesascode.tekton.dev/on-target-branch: "[main, release-*]"
    # Maximum number of concurrent runs for this pipeline
    pipelinesascode.tekton.dev/max-keep-runs: "5"
spec:
  pipelineRef:
    name: production-container-pipeline
  params:
    - name: git-url
      value: "{{ repo_url }}"          # PaC template variable
    - name: git-revision
      value: "{{ revision }}"          # PaC template variable
    - name: image-name
      value: "quay.io/myorg/myapp"
    - name: dockerfile-path
      value: "./Containerfile"
  workspaces:
    - name: source
      volumeClaimTemplate:
        spec:
          accessModes: [ReadWriteOnce]
          resources:
            requests:
              storage: 1Gi
    - name: registry-auth
      secret:
        secretName: registry-push-credentials
    # Uncomment for private repositories β€” provide a Secret with
    # .git-credentials or SSH keys for PaC to authenticate to git.
    # - name: git-auth
    #   secret:
    #     secretName: pipelines-as-code-secret

Commands

terminal
# Apply the full pipeline
kubectl apply -f 09-production-pipeline.yaml

# Create a PVC for the shared source workspace
kubectl apply -f - <<EOF
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: production-pipeline-pvc
spec:
  accessModes: [ReadWriteOnce]
  resources:
    requests:
      storage: 1Gi
EOF

# Create registry credentials.
# Key MUST be "config.json" β€” buildah looks for that exact filename.
# "kubectl create secret docker-registry" creates ".dockerconfigjson" which
# buildah cannot find. Use "kubectl create secret generic" instead.

# Option A β€” reuse existing local Docker / Podman login:
kubectl create secret generic registry-push-credentials \
  --from-file=config.json=$HOME/.docker/config.json

# Option B β€” inline for docker.io (replace USER and TOKEN):
AUTH=$(echo -n "YOUR_USERNAME:YOUR_TOKEN" | base64)
kubectl create secret generic registry-push-credentials \
  --from-literal=config.json="{\"auths\":{\"https://index.docker.io/v1/\":{\"auth\":\"$AUTH\"}}}"

# ── Sample repo: traefik/whoami ────────────────────────────────
# A tiny Go HTTP server with a self-contained two-stage Dockerfile.
# - Go project  β†’ unit test step (go test ./...) runs successfully
# - No external pip/npm deps  β†’ fast, reliable build
# - dockerfile-path overrides the pipeline default (./Containerfile)
# - git-revision is empty so git-clone uses HEAD of the default branch
# - git-auth uses emptyDir because the repo is public (no credentials needed)
# - skip-vuln-scan=true skips Trivy for local dev (remove to enable scanning)

tkn pipeline start production-container-pipeline \
  --param git-url="https://github.com/traefik/whoami" \
  --param git-revision="" \
  --param image-name="docker.io/YOUR_DOCKERHUB_USERNAME/whoami-prod" \
  --param dockerfile-path="./Dockerfile" \
  --param skip-tests="false" \
  --param skip-vuln-scan="true" \
  --workspace name=source,claimName=production-pipeline-pvc \
  --workspace name=registry-auth,secret=registry-push-credentials \
  # --workspace name=git-auth,secret=git-credentials \  # uncomment for private repos
  --showlog

# View all task logs in sequence as they complete
tkn pipelinerun logs --last -f

# Debug a specific failed task
tkn taskrun logs --last -f

# After completion β€” verify Chains signed the image
kubectl get taskruns --selector=tekton.dev/pipelineTask=build-container
# Look for: chains.tekton.dev/signed: "true"

# Install Tekton PaC (Pipeline as Code) for the .tekton/ directory approach
kubectl apply -f https://raw.githubusercontent.com/openshift-pipelines/pipelines-as-code/stable/release.k8s.yaml

Konflux Connection

βš™ How This Applies to Konflux

This pipeline IS the Konflux build pipeline, simplified. The differences in Konflux's version:

  • Tasks are referenced as OCI bundle digests from quay.io/konflux-ci/tekton-catalog β€” pinned by digest for reproducibility
  • Konflux adds 10+ security tasks: ecosystem-cert-preflight-checks, clair-scan, sast-snyk-check, clamav-scan, rpms-signature-scan β€” each runs as a separate pipeline task
  • The prefetch-dependencies task (Cachi2) creates a hermetic build environment where all dependencies are pre-fetched β€” no outbound network access during build
  • The source-image task creates an OCI artifact containing the exact source code that was compiled β€” for full auditability
  • The .tekton/ directory you just learned is how Konflux customizes per-component pipelines β€” you can override params, add tasks, and pin to specific task versions

Key Takeaways

  • This pipeline pattern is the industry standard for container CI
  • Scan AFTER build but BEFORE promoting to production registries
  • SBOM generation runs in parallel with scanning β€” saves time
  • Pipeline as Code stores pipelines in the application repo
  • Finally block is critical for cleanup and notifications
  • This structure directly maps to Konflux's production pipeline