StepActions

Overview

🌱 StepActions is an beta feature. The enable-step-actions feature flag must be set to "true" to specify a StepAction in a Step.

A StepAction is the reusable and scriptable unit of work that is performed by a Step.

A Step is not reusable, the work it performs is reusable and referenceable. Steps are in-lined in the Task definition and either perform work directly or perform a StepAction. A StepAction cannot be run stand-alone (unlike a TaskRun or a PipelineRun). It has to be referenced by a Step. Another way to think about this is that a Step is not composed of StepActions (unlike a Task being composed of Steps and Sidecars). Instead, a Step is an actionable component, meaning that it has the ability to refer to a StepAction. The author of the StepAction must be able to compose a Step using a StepAction and provide all the necessary context (or orchestration) to it.

Configuring a StepAction

A StepAction definition supports the following fields:

  • Required
    • apiVersion - Specifies the API version. For example, tekton.dev/v1alpha1.
    • kind - Identifies this resource object as a StepAction object.
    • metadata - Specifies metadata that uniquely identifies the StepAction resource object. For example, a name.
    • spec - Specifies the configuration information for this StepAction resource object.
    • image - Specifies the image to use for the Step.
  • Optional

The example below demonstrates the use of most of the above-mentioned fields:

apiVersion: tekton.dev/v1beta1
kind: StepAction
metadata:
  name: example-stepaction-name
spec:
  env:
    - name: HOME
      value: /home
  image: ubuntu
  command: ["ls"]
  args: ["-lh"]

Declaring Parameters

Like with Tasks, a StepAction must declare all the parameters that it uses. The same rules for Parameter name, type (including object, array and string) apply as when declaring them in Tasks. A StepAction can also provide default value to a Parameter.

Parameters are passed to the StepAction from its corresponding Step referencing it.

apiVersion: tekton.dev/v1beta1
kind: StepAction
metadata:
  name: stepaction-using-params
spec:
  params:
    - name: gitrepo
      type: object
      properties:
        url:
          type: string
        commit:
          type: string
    - name: flags
      type: array
    - name: outputPath
      type: string
      default: "/workspace"
  image: some-git-image
  args: [
    "-url=$(params.gitrepo.url)",
    "-revision=$(params.gitrepo.commit)",
    "-output=$(params.outputPath)",
    "$(params.flags[*])",
  ]

🌱 params cannot be directly used in a script in StepActions. Directly substituting params in scripts makes the workload prone to shell attacks. Therefore, we do not allow direct usage of params in scripts in StepActions. Instead, rely on passing params to env variables and reference them in scripts. We cannot do the same for inlined-steps because it breaks v1 API compatibility for existing users.

Passing Params to StepAction

A StepAction may require params. In this case, a Task needs to ensure that the StepAction has access to all the required params. When referencing a StepAction, a Step can also provide it with params, just like how a TaskRun provides params to the underlying Task.

apiVersion: tekton.dev/v1
kind: Task
metadata:
  name: step-action
spec:
  params:
    - name: param-for-step-action
      description: "this is a param that the step action needs."
  steps:
    - name: action-runner
      ref:
        name: step-action
      params:
        - name: step-action-param
          value: $(params.param-for-step-action)

Note: If a Step declares params for an inlined Step, it will also lead to a validation error. This is because an inlined Step gets its params from the TaskRun.

Emitting Results

A StepAction also declares the results that it will emit.

apiVersion: tekton.dev/v1alpha1
kind: StepAction
metadata:
  name: stepaction-declaring-results
spec:
  results:
    - name: current-date-unix-timestamp
      description: The current date in unix timestamp format
    - name: current-date-human-readable
      description: The current date in human readable format
  image: bash:latest
  script: |
    #!/usr/bin/env bash
    date +%s | tee $(results.current-date-unix-timestamp.path)
    date | tee $(results.current-date-human-readable.path)    

It is possible that a StepAction with Results is used multiple times in the same Task or multiple StepActions in the same Task produce Results with the same name. Resolving the Result names becomes critical otherwise there could be unexpected outcomes. The Task needs to be able to resolve these Result names clashes by mapping it to a different Result name. For this reason, we introduce the capability to store results on a Step level.

StepActions can also emit Results to $(step.results.<resultName>.path).

apiVersion: tekton.dev/v1alpha1
kind: StepAction
metadata:
  name: stepaction-declaring-results
spec:
  results:
    - name: current-date-unix-timestamp
      description: The current date in unix timestamp format
    - name: current-date-human-readable
      description: The current date in human readable format
  image: bash:latest
  script: |
    #!/usr/bin/env bash
    date +%s | tee $(step.results.current-date-unix-timestamp.path)
    date | tee $(step.results.current-date-human-readable.path)    

Results from the above StepAction can be fetched by the Task or in another Step/StepAction via $(steps.<stepName>.results.<resultName>).

Fetching Emitted Results from StepActions

A Task can fetch Results produced by the StepActions (i.e. only Results emitted to $(step.results.<resultName>.path), NOT $(results.<resultName>.path)) using variable replacement syntax. We introduce a field to Task Results called Value whose value can be set to the variable $(steps.<stepName>.results.<resultName>).

apiVersion: tekton.dev/v1
kind: Task
metadata:
  name: task-fetching-results
spec:
  results:
    - name: git-url
      description: "url of git repo"
      value: $(steps.git-clone.results.url)
    - name: registry-url
      description: "url of docker registry"
      value: $(steps.kaniko.results.url)
  steps:
    - name: git-clone
      ref:
        name: clone-step-action
    - name: kaniko
      ref:
        name: kaniko-step-action

Results emitted to $(step.results.<resultName>.path) are not automatically available as TaskRun Results. The Task must explicitly fetch it from the underlying Step referencing StepActions.

For example, lets assume that in the previous example, the “kaniko” StepAction also produced a Result named “digest”. In that case, the Task should also fetch the “digest” from “kaniko” Step.

apiVersion: tekton.dev/v1
kind: Task
metadata:
  name: task-fetching-results
spec:
  results:
    - name: git-url
      description: "url of git repo"
      value: $(steps.git-clone.results.url)
    - name: registry-url
      description: "url of docker registry"
      value: $(steps.kaniko.results.url)
    - name: digest
      description: "digest of the image"
      value: $(steps.kaniko.results.digest)
  steps:
    - name: git-clone
      ref:
        name: clone-step-action
    - name: kaniko
      ref:
        name: kaniko-step-action

Passing Results between Steps

StepResults (i.e. results written to $(step.results.<result-name>.path), NOT $(results.<result-name>.path)) can be shared with following steps via replacement variable $(steps.<step-name>.results.<result-name>).

Pipeline supports two new types of results and parameters: array []string and object map[string]string.

Result Type Parameter Type Specification enable-api-fields
string string $(steps.<step-name>.results.<result-name>) stable
array array $(steps.<step-name>.results.<result-name>[*]) alpha or beta
array string $(steps.<step-name>.results.<result-name>[i]) alpha or beta
object string $(tasks.<task-name>.results.<result-name>.key) alpha or beta

Note: Whole Array Results (using star notation) cannot be referred in script and env.

The example below shows how you could pass step results from a step into following steps, in this case, into a StepAction.

apiVersion: tekton.dev/v1
kind: TaskRun
metadata:
  name: step-action-run
spec:
  TaskSpec:
    steps:
      - name: inline-step
        results:
          - name: result1
            type: array
          - name: result2
            type: string
          - name: result3
            type: object
            properties:
              IMAGE_URL:
                type: string
              IMAGE_DIGEST:
                type: string
        image: alpine
        script: |
          echo -n "[\"image1\", \"image2\", \"image3\"]" | tee $(step.results.result1.path)
          echo -n "foo" | tee $(step.results.result2.path)
          echo -n "{\"IMAGE_URL\":\"ar.com\", \"IMAGE_DIGEST\":\"sha234\"}" | tee $(step.results.result3.path)          
      - name: action-runner
        ref:
          name: step-action
        params:
          - name: param1
            value: $(steps.inline-step.results.result1[*])
          - name: param2
            value: $(steps.inline-step.results.result2)
          - name: param3
            value: $(steps.inline-step.results.result3[*])

Note: Step Results can only be referenced in a Step's/StepAction's env, command and args. Referencing in any other field will throw an error.

Declaring WorkingDir

You can declare workingDir in a StepAction:

apiVersion: tekton.dev/v1alpha1
kind: StepAction
metadata:
  name: example-stepaction-name
spec:
  image:  gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init:latest
  workingDir: /workspace
  script: |
    # clone the repo
    ...    

The Task using the StepAction has more context about how the Steps have been orchestrated. As such, the Task should be able to update the workingDir of the StepAction so that the StepAction is executed from the correct location. The StepAction can parametrize the workingDir and work relative to it. This way, the Task does not really need control over the workingDir, it just needs to pass the path as a parameter.

apiVersion: tekton.dev/v1alpha1
kind: StepAction
metadata:
  name: example-stepaction-name
spec:
  image: ubuntu
  params:
    - name: source
      description: "The path to the source code."
  workingDir: $(params.source)

Declaring SecurityContext

You can declare securityContext in a StepAction:

apiVersion: tekton.dev/v1alpha1
kind: StepAction
metadata:
  name: example-stepaction-name
spec:
  image:  gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init:latest
  securityContext:
      runAsUser: 0
  script: |
    # clone the repo
    ...    

Note that the securityContext from StepAction will overwrite the securityContext from TaskRun.

Declaring VolumeMounts

You can define VolumeMounts in StepActions. The name of the VolumeMount MUST be a single reference to a string Parameter. For example, $(params.registryConfig) is valid while $(params.registryConfig)-foo and "unparametrized-name" are invalid. This is to ensure reusability of StepActions such that Task authors have control of which Volumes they bind to the VolumeMounts.

apiVersion: tekton.dev/v1alpha1
kind: StepAction
metadata:
  name: myStep
spec:
  params:
    - name: registryConfig
    - name: otherConfig
  volumeMounts:
    - name: $(params.registryConfig)
      mountPath: /registry-config
    - name: $(params.otherConfig)
      mountPath: /other-config
  image: ...
  script: ...

Referencing a StepAction

StepActions can be referenced from the Step using the ref field, as follows:

apiVersion: tekton.dev/v1
kind: TaskRun
metadata:
  name: step-action-run
spec:
  taskSpec:
    steps:
      - name: action-runner
        ref:
          name: step-action

Upon resolution and execution of the TaskRun, the Status will look something like:

status:
  completionTime: "2023-10-24T20:28:42Z"
  conditions:
  - lastTransitionTime: "2023-10-24T20:28:42Z"
    message: All Steps have completed executing
    reason: Succeeded
    status: "True"
    type: Succeeded
  podName: step-action-run-pod
  provenance:
    featureFlags:
      EnableStepActions: true
      ...
  startTime: "2023-10-24T20:28:32Z"
  steps:
  - container: step-action-runner
    imageID: docker.io/library/alpine@sha256:eece025e432126ce23f223450a0326fbebde39cdf496a85d8c016293fc851978
    name: action-runner
    terminationReason: Completed
    terminated:
      containerID: containerd://46a836588967202c05b594696077b147a0eb0621976534765478925bb7ce57f6
      exitCode: 0
      finishedAt: "2023-10-24T20:28:42Z"
      reason: Completed
      startedAt: "2023-10-24T20:28:42Z"
  taskSpec:
    steps:
    - computeResources: {}
      image: alpine
      name: action-runner

If a Step is referencing a StepAction, it cannot contain the fields supported by StepActions. This includes:

  • image
  • command
  • args
  • script
  • env
  • volumeMounts

Using any of the above fields and referencing a StepAction in the same Step is not allowed and will cause a validation error.

# This is not allowed and will result in a validation error
# because the image is expected to be provided by the StepAction
# and not inlined.
apiVersion: tekton.dev/v1
kind: TaskRun
metadata:
  name: step-action-run
spec:
  taskSpec:
    steps:
      - name: action-runner
        ref:
          name: step-action
        image: ubuntu

Executing the above TaskRun will result in an error that looks like:

Error from server (BadRequest): error when creating "STDIN": admission webhook "validation.webhook.pipeline.tekton.dev" denied the request: validation failed: image cannot be used with Ref: spec.taskSpec.steps[0].image

When a Step is referencing a StepAction, it can contain the following fields:

  • computeResources
  • workspaces (Isolated workspaces)
  • volumeDevices
  • imagePullPolicy
  • onError
  • stdoutConfig
  • stderrConfig
  • securityContext
  • envFrom
  • timeout
  • ref
  • params

Using any of the above fields and referencing a StepAction is allowed and will not cause an error. For example, the TaskRun below will execute without any errors:

apiVersion: tekton.dev/v1
kind: TaskRun
metadata:
  name: step-action-run
spec:
  taskSpec:
    steps:
      - name: action-runner
        ref:
          name: step-action
        params:
          - name: step-action-param
            value: hello
        computeResources:
          requests:
            memory: 1Gi
            cpu: 500m
        timeout: 1h
        onError: continue

Specifying Remote StepActions

A ref field may specify a StepAction in a remote location such as git. Support for specific types of remote will depend on the Resolvers your cluster’s operator has installed. For more information including a tutorial, please check resolution docs. The below example demonstrates referencing a StepAction in git:

apiVersion: tekton.dev/v1
kind: TaskRun
metadata:
  generateName: step-action-run-
spec:
  taskSpec:
    steps:
      - name: action-runner
        ref:
          resolver: git
          params:
            - name: url
              value: https://github.com/repo/repo.git
            - name: revision
              value: main
            - name: pathInRepo
              value: remote_step.yaml

The default resolver type can be configured by the default-resolver-type field in the config-defaults ConfigMap (alpha feature). See additional-configs.md for details.

Controlling Step Execution with when Expressions

You can define when in a step to control its execution.

The components of when expressions are input, operator, values, cel:

Component Description Syntax
input Input for the when expression, defaults to an empty string if not provided. * Static values e.g. "ubuntu"
* Variables (parameters or results) e.g. "$(params.image)" or "$(tasks.task1.results.image)" or "$(tasks.task1.results.array-results[1])"
operator operator represents an input’s relationship to a set of values, a valid operator must be provided. in or notin
values An array of string values, the values array must be provided and has to be non-empty. * An array param e.g. ["$(params.images[*])"]
* An array result of a task ["$(tasks.task1.results.array-results[*])"]
* An array result of a step["(steps.step1.results.array-results[*])"]
* values can contain static values e.g. "ubuntu"
* values can contain variables (parameters or results) or a Workspaces’s bound state e.g. ["$(params.image)"] or ["$(steps.step1.results.image)"] or ["$(tasks.task1.results.array-results[1])"] or ["$(steps.step1.results.array-results[1])"]
cel The Common Expression Language (CEL) implements common semantics for expression evaluation, enabling different applications to more easily interoperate. This is an alpha feature, enable-cel-in-whenexpression needs to be set to true to use this feature. cel-syntax

The below example shows how to use when expressions to control step executions:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: my-pvc-2
spec:
  resources:
    requests:
      storage: 5Gi
  volumeMode: Filesystem
  accessModes:
    - ReadWriteOnce
---
apiVersion: tekton.dev/v1
kind: TaskRun
metadata:
  generateName: step-when-example
spec:
  workspaces:
    - name: custom
      persistentVolumeClaim:
        claimName: my-pvc-2
  taskSpec:
    description: |
            A simple task that shows how to use when determine if a step should be executed
    steps:
      - name: should-execute
        image: bash:latest
        script: |
          #!/usr/bin/env bash
          echo "executed..."          
        when:
          - input: "$(workspaces.custom.bound)"
            operator: in
            values: [ "true" ]
      - name: should-skip
        image: bash:latest
        script: |
          #!/usr/bin/env bash
          echo skipskipskip          
        when:
          - input: "$(workspaces.custom2.bound)"
            operator: in
            values: [ "true" ]
      - name: should-continue
        image: bash:latest
        script: |
          #!/usr/bin/env bash
          echo blabalbaba          
      - name: produce-step
        image: alpine
        results:
          - name: result2
            type: string
        script: |
                    echo -n "foo" | tee $(step.results.result2.path)
      - name: run-based-on-step-results
        image: alpine
        script: |
                    echo "wooooooo"
        when:
          - input: "$(steps.produce-step.results.result2)"
            operator: in
            values: [ "bar" ]
    workspaces:
      - name: custom

The StepState for a skipped step looks like something similar to the below:

      {
        "container": "step-run-based-on-step-results",
        "imageID": "docker.io/library/alpine@sha256:c5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b",
        "name": "run-based-on-step-results",
        "terminated": {
          "containerID": "containerd://bf81162e79cf66a2bbc03e3654942d3464db06ff368c0be263a8a70f363a899b",
          "exitCode": 0,
          "finishedAt": "2024-03-26T03:57:47Z",
          "reason": "Completed",
          "startedAt": "2024-03-26T03:57:47Z"
        },
        "terminationReason": "Skipped"
      }

Where terminated.exitCode is 0 and terminationReason is Skipped to indicate the Step exited successfully and was skipped.