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.
Tekton Architecture & Installation β The Kubernetes-Native CI/CD Engine
What You'll Learn
tkn CLIPrerequisites
Demo Steps
- Create a Kind cluster using a config file. Verify it's ready with
kubectl cluster-info --context kind-tekton-demoandkubectl get nodes. - Install Tekton Pipelines by applying the release manifest. Wait for all pods to be Running in the
tekton-pipelinesnamespace. - Install Tekton Dashboard by applying its release manifest.
- Install the tkn CLI β show both Homebrew (Mac) and curl (Linux) methods.
- Verify installation: run
tkn version, show the CRDs withkubectl get crds | grep tekton. - Port-forward the Dashboard and open it in a browser. Show the empty state.
- Explore the API: run
kubectl explain task,kubectl explain pipeline.
Commands
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+
# ββ 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 β The Atomic Building Blocks of Tekton
What You'll Learn
Prerequisites
Demo Steps
- Create the hello-world task β apply
02-hello-task.yaml, inspect withtkn task describe. - Run it declaratively β apply
02-taskrun.yaml, watch withtkn taskrun logs -f. - Inspect the Pod β run
kubectl get podsand show the init containers Tekton injects. - Run it with tkn CLI β use
tkn task start hello-world --param name=Konfluxto demonstrate the imperative approach. - Create the parameterized multi-step task β apply
02-build-info-task.yaml. - Run it and show Results β use
tkn taskrun describeto see the captured result values. - Show failure handling β demonstrate what happens when a step exits non-zero.
YAML Files
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."
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"
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
# 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
commitresult (the full git SHA) that flows into build tasks - The build-container task (using Buildah) produces an
IMAGE_DIGESTresult 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
stepTemplateto 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 β Orchestrating Tasks Into a Real Workflow
What You'll Learn
runAfterDemo Steps
- Apply the three tasks individually: validate-input, run-checks (used for both lint and test), generate-report.
- Apply the pipeline β inspect with
tkn pipeline describeand show the DAG. - Start the PipelineRun with
tkn pipeline startand follow logs. - Open the Dashboard and watch the parallel tasks (lint + test) execute simultaneously, then report waits for both.
- Inspect the PipelineRun β show how results from individual task runs are accessible.
- 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
---
# 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)"
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)"
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
# 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
inittask runs first (validates component config) - The
clone-repositorytask produces acommitresult - Vulnerability scanning and SAST checks run in parallel after the build
- The
IMAGE_DIGESTresult frombuild-containerflows 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/componentlink the run back to the Konflux component
Key Takeaways
- Pipeline = ordered DAG of Task instances
- Tasks without shared
runAfterrun 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 β Sharing Files, Secrets, and Config Between Tasks
What You'll Learn
Demo Steps
- Create the PVC β apply
04-workspace-pvc.yaml, verify withkubectl get pvc. - Create a ConfigMap for configuration that will be injected as a workspace.
- Apply the tasks: writer task and reader task, both declaring workspaces.
- Apply the pipeline that wires workspace slots to task workspace names.
- Apply the PipelineRun that binds the PVC and ConfigMap to workspace slots.
- Watch execution β show that the reader task can see files written by the writer task.
- Show emptyDir variant β discuss when to use emptyDir vs PVC.
YAML Files
---
# 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
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."
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
# 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-repositorytask writes the full source tree here;build-containerreads 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
Tekton Triggers β Event-Driven Pipelines with GitHub Webhooks
What You'll Learn
Demo Steps
- Install Tekton Triggers β apply the release manifest, verify pods in
tekton-pipelinesnamespace. - Apply RBAC β ServiceAccount, Role, and RoleBinding from
05-triggers-rbac.yaml. - Apply TriggerBinding and TriggerTemplate β explain how payload fields map to template params.
- Apply EventListener β port-forward to reach it locally.
- Test with curl β simulate a GitHub push event payload and watch the PipelineRun get created automatically.
- Use ngrok to expose the EventListener, add webhook in GitHub settings, and push a real commit to trigger the pipeline.
YAML Files
---
# 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
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)
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.
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
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
# 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 & Community Tasks β Building Real CI Without Starting from Scratch
What You'll Learn
git-clone catalog task correctlybuildah catalog taskDemo Steps
- Browse Artifact Hub β open artifacthub.io/packages/search?kind=7, find git-clone and buildah tasks, show the YAML and version list.
- Find tasks on Artifact Hub β open artifacthub.io and search for git-clone and buildah; show the Install tab with the raw GitHub URL.
- Create the pipeline that wires git-clone β buildah via the
httpresolver (no cluster installation needed). - Create a container registry secret for pushing images.
- Run the pipeline against a sample public repository with a Containerfile.
- Show the bundle resolver approach as used by Konflux β task definitions fetched from OCI at runtime.
YAML Files
---
# 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
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
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: ci-pipeline-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
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
# ββ 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-containertask uses Buildah exactly like this demo, with the same IMAGE, DOCKERFILE, and CONTEXT params - The
IMAGE_DIGESTresult frombuild-containeris 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-cior 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
commitresult β tag images with it - Buildah is daemonless and preferred for Kubernetes-native builds
- Pin task versions for reproducible pipelines
Advanced Pipeline Patterns β When, Finally, Matrix, and Embedded Tasks
What You'll Learn
when expressions for conditional task executionfinally block for guaranteed cleanup and notificationsmatrix for fan-out parallel execution over a listtaskSpec vs external taskRefDemo Steps
- Build the advanced pipeline step by step β start with just the main tasks, then add
whenconditions. - Run with skip-tests=true β show that the test task is skipped but all others run.
- Add the finally block β show it running even when a task fails.
- Add matrix β show three parallel TaskRuns being created from one pipeline task definition.
- Add retries and timeout β demonstrate a flaky task being retried automatically.
YAML Files
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."
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
# 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-checksparam in Konflux pipelines skips non-critical scanning tasks. SAST and ClamAV tasks havewhenguards 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: 0on most tasks β failures are intentionally surfaced immediately. The exception is tasks that call external services (like SBOM upload) which may haveretries: 3.
Key Takeaways
whenskips tasks conditionally β it's not a failurefinallyalways runs β use for cleanup and notificationsmatrixfans out one task over a list in parallelretriesautomatically re-runs a failed task N timestimeoutkills a runaway task after a duration- Finally tasks can read
$(context.pipelineRun.name)and namespace
Tekton Chains β Supply Chain Security, SLSA Attestations & Image Signing
What You'll Learn
Demo Steps
- Install Tekton Chains β apply the release manifest, verify the controller pod.
- Generate a cosign key pair with
cosign generate-key-pair k8s://tekton-chains/signing-secrets. - Configure Chains β patch the chains-config ConfigMap to set storage backend and signing format.
- Run a build pipeline β use the pipeline from Video 6 to build and push a real image.
- Watch Chains process the TaskRun β show the
chains.tekton.dev/signedannotation appearing. - Verify the signature with
cosign verifyand inspect the attestation withcosign verify-attestation.
YAML Files
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"
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
# 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_DIGESTresult 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_URLandIMAGE_DIGESTare the trigger - Attestations are stored as OCI artifacts β no separate DB
cosign verifyandcosign verify-attestationfor validation- Foundation for SLSA Level 3 β what Konflux achieves in prod
Container Build Pipeline End-to-End β Clone, Test, Build, Scan, Sign, Push
What You'll Learn
Demo Steps
- Create a sample application repo with a Go or Python app and a Containerfile.
- Apply the full pipeline β walk through each task and explain why it's in that position.
- Run it end-to-end against the sample repo and follow logs task by task.
- Show the Dashboard β demonstrate the full DAG with parallel and sequential tasks.
- Introduce the .tekton directory approach β show how to store the pipeline in the repo.
- Trigger via webhook using the Trigger setup from Video 5.
YAML Files
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)"
# 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
# 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