Params & Results
Substitution, from PipelineRun to argv
param-paramparam-variable-substitutionparam-typesparam-defaultparam-enumparam-arrayparam-objectparam-validationPipelineRun params · edit me
kind: PipelineRunspec:pipelineRef: { name: build-deploy }params:- name: git-url value: https://github.com/acme/shop- name: build-args value: ["--target=prod", "--snapshot-mode=redo"]- name: image value: { registry: ghcr.io, name: acme/shop, tag: v1.4.2 }- name: env value: staging
kind: Pipeline # build-deployspec:params:- { name: git-url, type: string } # no default → required- { name: build-args, type: array, default: ["--cache=true"] }- name: image type: object properties: { registry: {}, name: {}, tag: {} }- name: env type: string enum: [dev, staging, prod] default: devtasks:- name: build taskRef: { name: kaniko-build } params:- { name: SOURCE, value: "https://github.com/acme/shop" }- { name: IMAGE, value: "ghcr.io/acme/shop:v1.4.2" }- { name: EXTRA_ARGS, value: ["--target=prod", "--snapshot-mode=redo"] }- { name: ENV, value: "staging" }
kind: Task # kaniko-buildspec:params: [SOURCE, IMAGE, {name: EXTRA_ARGS, type: array}, ENV]steps:- name: build image: gcr.io/kaniko-project/executor args:- --context=https://github.com/acme/shop- --destination=ghcr.io/acme/shop:v1.4.2- --target=prod # from $(params.EXTRA_ARGS[*])- --snapshot-mode=redo # from $(params.EXTRA_ARGS[*])- name: announce image: alpine script: |echo "pushed ghcr.io/acme/shop:v1.4.2 to staging"
what the build container actually receives (argv)
blue = items spread from the array param by [*], each its own argument (no shell word-splitting involved)
Try: type a new tag and watch it flow PipelineRun → Pipeline → Task → argv. Add a build-args item to see [*] grow the arg list. Then omit git-url, send build-args as a string, drop the image tag key, or pick env = qa: each fails the PipelineRun with a different reason before anything runs.
Propagated params in inline specs
param-propagatedkind: PipelineRunspec:params:- { name: HELLO, value: "Hello from the PipelineRun!" }pipelineSpec: # inline, declares no paramstasks:- name: echo-hellotaskSpec: # inlinesteps:- name: echo image: alpinescript: echo "$(params.HELLO)"
who supplies $(params.HELLO)?
Try: with an inline taskSpec, the PipelineRun's HELLO reaches the step even though nothing in between declares it. Switch to taskRef and it breaks. Then turn on the Task default and the pipeline task value to see the precedence order: explicit value, then runtime value, then default.
the result channel
kind: Pipelinespec:results:- { name: image-digest, value: $(tasks.build.results.digest) }tasks:- name: build taskSpec: results: [{ name: digest }]steps: - image: gcr.io/go-containerregistry/cranescript: crane digest $IMG | tee $(results.digest.path)- name: lint taskRef: { name: golangci-lint }- name: deploy taskRef: { name: kubectl-deploy }params: [{ name: digest, value: $(tasks.build.results.digest) }]
DAG the controller built
Try: step through and follow the digest from a file inside the Pod to the deploy TaskRun's argv. Untick "deploy references the result": the edge disappears and deploy runs alongside build. Tick "build forgets to write it" to see the PipelineRun fail at the hand-off.
results:- { name: tags, type: array }steps:- script: | printf '%s' '…' > $(results.tags.path)
/tekton/results/tags · edit
consumer task sees
steps:- name: fetch results: [{ name: sha }]script: git rev-parse HEAD | tr -d '\n' > $(step.results.sha.path)- name: build script: echo "building $(steps.fetch.results.sha)"results:- { name: commit, value: $(steps.fetch.results.sha) }
inside the Pod
Each step gets its own results directory, so steps (often StepActions) can hand values to later steps without both knowing a shared Task result name.
Try: with the array type, click "not JSON": the step wrote plain words, the TaskRun fails. Switch to string and compare printf with echo to see the classic trailing-newline bug. Then step through the step-results panel.
result sizes · each written by a different step
each step's termination message (written by its entrypoint)
limit 4096 B per messageThe entrypoint of every step reports all result files present so far, so the last step's message holds every result. Grey = step metadata. Red line = 4096 B.
Try: drag test-report up. Its own step overflows first, even though each result alone is under 4 KB, because the message carries everything written before it. Switch to sidecar-logs: the same sizes pass, until a single result passes max-result-size.
Context variables: what you can reference, and where
param-context-variablestest an expression
variable reference
| variable | what | here |
|---|---|---|
| $(params.<name>) | a string param, or one key/item of an object/array param | v1.4.2 |
| $(params.<name>[*]) | an array (or whole object) param, expanded as a whole list element | ["--target=prod", "--cache=true"] |
| $(params.<name>.<key>) | one key of an object param | ghcr.io |
| $(results.<name>.path) | file a step writes a Task result to | /tekton/results/digest |
| $(step.results.<name>.path) | file a step writes its own step result to | /tekton/steps/step-fetch/results/sha |
| $(steps.<step>.results.<name>) | a step result from an earlier step of the same Task | 3e9a1c0 |
| $(steps.step-<step>.exitCode.path) | file holding an earlier step's exit code | /tekton/steps/step-unit/exitCode |
| $(tasks.<task>.results.<name>) | a result of another pipeline task (adds a DAG edge) | n/a here |
| $(tasks.<task>.results.<name>[*]) | a whole array/object result | n/a here |
| $(tasks.<task>.results.<name>.<key>) | one key of an object result | n/a here |
| $(tasks.<task>.matrix.length) | how many TaskRuns a matrixed task fanned out to | n/a here |
| $(tasks.<task>.status) | outcome of one pipeline task: Succeeded, Failed or None | n/a here |
| $(tasks.<task>.reason) | the reason of that task's TaskRun | n/a here |
| $(tasks.status) | aggregate of the non-finally tasks: Succeeded, Failed, Completed or None | n/a here |
| $(workspaces.<name>.path) | where the workspace is mounted | /workspace/source |
| $(workspaces.<name>.bound) | whether an optional workspace was provided | true |
| $(workspaces.<name>.claim) | the PVC name, or empty for non-PVC sources | pvc-5c2f1e |
| $(workspaces.<name>.volume) | the name of the volume backing it | ws-8k2lp |
| $(context.taskRun.name) | this TaskRun's name | build-deploy-r7x2k-build |
| $(context.taskRun.namespace) | this TaskRun's namespace | ci |
| $(context.taskRun.uid) | this TaskRun's UID | 0d4b1f5e-9c3a-4a8e-b1f7-2e6d8c90aa13 |
| $(context.task.name) | the Task's name | kaniko-build |
| $(context.task.retry-count) | how many times this TaskRun has been retried | 0 |
| $(context.pipelineRun.name) | the PipelineRun's name | n/a here |
| $(context.pipelineRun.namespace) | the PipelineRun's namespace | n/a here |
| $(context.pipelineRun.uid) | the PipelineRun's UID, handy for unique names | n/a here |
| $(context.pipeline.name) | the Pipeline's name | n/a here |
| $(context.pipelineTask.retries) | the retries configured on this pipeline task | n/a here |
| $(credentials.path) | where Tekton writes credential files (.docker, .gitconfig…) | /tekton/creds |
Try: switch between Task step, Pipeline task and finally task and watch rows light up or grey out. $(tasks.status) only exists in finally. Click a row to test it, or try the misspelt $(context.taskRun.nmae).
Substitution is not escaping: script injection
param-script-injectionPipelineRun param (attacker controls this, e.g. a branch name from a webhook)
steps:- name: deploy image: alpine/gitscript: |echo "Deploying $(params.branch)"git checkout "$(params.branch)"
#!/bin/shecho "Deploying "; rm -rf / #"git checkout ""; rm -rf / #"
commands the shell runs
- 1.echo "Deploying "
- 2.rm -rf /
- 3.git checkout ""
- 4.rm -rf /
Try: pick "; rm -rf / #: the quote closes the string, ; starts a new command and # comments out the rest. The $(cat creds) payload runs even inside double quotes. Switch to "via env var": the script text never changes, the shell expands ${BRANCH} as a single word, and every payload stays harmless data.
- takeawayParams are substituted as text into the spec before Pods exist. Validation of presence, type, enum and object keys happens first, so bad inputs fail fast with a named reason.
- takeawayResults are files that ride the termination message into TaskRun.status. Referencing one creates the DAG edge. Keep them tiny: the whole Task gets about 4 KB unless you switch to sidecar logs, and big data belongs on a workspace.
- takeawayNever splice a param into a script. Map it to an env var and quote it, so the script text stays fixed and the value stays data.
Key concepts · 20
book chapter 5- Param
A named input declared in a Task or Pipeline spec and given a value by the TaskRun, PipelineRun or pipeline task that uses it.
Params are how one Task definition serves many runs: the repo URL, the image tag, the target environment. They are declared with a name, a type and optionally a default and description.
spec.params[]PipelineRun spec.params[].valuetkn pipeline start -p git-url=…↑ see it in the visualization - Variable substitution
Tekton replaces $(params.x) and other $(…) expressions with their values in the spec before it creates TaskRuns and Pods.
It is text replacement, not a template language: no loops, no conditionals, no escaping. The container receives the final strings, which is both simple and the root of the injection problem.
$(params.x)$(params["x"])$(params['x'])↑ see it in the visualization - Param types
Every param is a string, an array of strings or an object of string values; string is the default.
The declared type is checked against what the run supplies, and it decides which reference forms are legal: .key for objects, [*] or [i] for arrays.
type: stringtype: arraytype: object↑ see it in the visualization - Default value
The value a param takes when the run does not supply one; a param without a default is required.
Defaults keep PipelineRuns short and make Tasks reusable. Leaving a default off is how a Task says "you must tell me this", and the run fails validation if nobody does.
default: devdefault: ["--cache=true"]↑ see it in the visualization - Param enum
A list of allowed values for a string param; any other value fails the run before it starts (beta).
It moves input checking from a shell `case` statement into the spec, where it is visible, documented and enforced before a Pod is ever scheduled.
enum: [dev, staging, prod]feature-flags: enable-param-enumreason: InvalidParamValue↑ see it in the visualization - Array param
A param holding a list of strings, expanded into separate list items with $(params.name[*]) or indexed with $(params.name[i]).
[*] only works as a whole element of a list field like args or command, and each item becomes its own argv entry. That is safer than joining flags into one string and letting a shell split it.
args: ["$(params.flags[*])"]$(params.flags[0])↑ see it in the visualization - Object param
A param with named string keys declared under properties, read one key at a time with $(params.name.key).
It groups related values (registry, name, tag) into one input. The run must supply every declared key, or the PipelineRun fails with ObjectParameterMissKeys.
properties: {registry: {}, tag: {}}$(params.image.tag)$(params.image[*])↑ see it in the visualization - Param validation
Before creating any TaskRun, the controller checks that required params are present, types match, enum values are allowed and object keys are complete.
A bad input fails fast with a specific reason instead of a confusing error ten minutes into a build. No Pod, no wasted node time.
ParameterMissingParameterTypeMismatchInvalidParamValueObjectParameterMissKeys↑ see it in the visualization - Propagated params
With inline specs (pipelineSpec, taskSpec), params of the outer run are visible to inner specs without being declared or passed at each level.
It removes boilerplate from fully inline PipelineRuns. It does not cross a taskRef: a referenced Task only sees params it declares and that are passed to it. Inner explicit values win over outer ones, and runtime values win over defaults.
pipelineSpectaskSpectaskRef↑ see it in the visualization - Task result
A small named output a Task declares and its steps write to a file, which Tekton copies into TaskRun.status.results.
Results are how tasks hand values to each other without sharing a volume: a digest, a commit SHA, a URL. Declaring a result does not force a step to write it.
spec.results[]TaskRun status.resultstkn taskrun describe↑ see it in the visualization - Result path
$(results.name.path) resolves to /tekton/results/name, a file on a volume shared by all steps of the Pod.
Writing the file is the whole API: any language, any tool. Use printf or echo -n, because a trailing newline becomes part of the value.
$(results.digest.path)/tekton/results/digest↑ see it in the visualization - Termination message
The Kubernetes mechanism Tekton uses by default to ship results: the entrypoint writes them as JSON to /tekton/termination, and the kubelet copies it into the Pod status.
It needs no extra permissions, but Kubernetes caps a container's message at 4096 bytes and each step reports all results written so far, so a Task's results share about 4 KB. Overflowing fails the step and the TaskRun.
/tekton/terminationstate.terminated.messageresults-from: termination-message↑ see it in the visualization - Sidecar logs results
An alternative transport where a sidecar prints result files to its log and the controller reads them from the Pod logs.
It lifts the shared 4 KB ceiling, at the cost of the controller needing pods/log access and extra work per TaskRun. Turned on with results-from: sidecar-logs in the feature-flags ConfigMap.
feature-flags: results-from: sidecar-logssidecar-tekton-log-results↑ see it in the visualization - max-result-size
The per-result byte cap that applies in sidecar-logs mode, 4096 by default.
Raising it allows bigger results, but everything still lands in the TaskRun object in etcd, which is limited to about 1.5 MB. A result over the cap fails the TaskRun with TaskRunResultLargerThanAllowedLimit.
feature-flags: max-result-sizeTaskRunResultLargerThanAllowedLimit↑ see it in the visualization - Result reference
$(tasks.build.results.digest) in another pipeline task's params or when expressions, resolved by the PipelineRun controller.
Referencing a result creates a DAG edge: the consumer waits for the producer, no runAfter needed. If the producer never wrote it, the PipelineRun fails with InvalidTaskResultReference.
$(tasks.build.results.digest)InvalidTaskResultReference↑ see it in the visualization - Pipeline results
Outputs a Pipeline declares from its tasks' results, surfaced in PipelineRun.status.results.
They make a whole Pipeline usable like a function: whatever triggered it (a human, Triggers, Chains, another system) can read the image digest without digging into TaskRuns.
spec.results[].value: $(tasks.build.results.digest)PipelineRun status.results↑ see it in the visualization - Array and object results
Results declared as type array or object, written as JSON and consumed with [*], [i] or .key.
They avoid ad-hoc comma-separated strings. The step must write valid JSON of the declared shape (strings only, every declared key), or the TaskRun fails.
type: array$(tasks.t.results.tags[*])$(tasks.t.results.image.url)↑ see it in the visualization - Step results
Results declared on a single step, written to $(step.results.name.path) and read by later steps as $(steps.step-name.results.name).
They let steps, especially reusable StepActions, pass values within a Task. A Task result can be filled from one: value: $(steps.fetch.results.sha).
steps[].results[]$(step.results.sha.path)$(steps.fetch.results.sha)↑ see it in the visualization - Context variables
Built-in substitutions describing the run itself, such as $(context.taskRun.name), $(context.pipelineRun.uid) and, in finally tasks, $(tasks.status).
They give unique names, retry counts and outcomes without passing them as params. Each is only legal in certain places, and misspelt ones are rejected by validation.
$(context.taskRun.name)$(context.pipelineRun.uid)$(context.task.retry-count)$(tasks.status)↑ see it in the visualization - Script injection
Because substitution is raw text, a param value spliced into a script becomes shell code if it contains quotes, ;, && or $(…).
Branch names and PR titles from webhooks are attacker-controlled. Pass params into scripts through env vars (env: value: $(params.x)) and quote "$X": the script text never changes and the value stays data.
env: [{name: BRANCH, value: $(params.branch)}]"${BRANCH}"↑ see it in the visualization