Skip to content

Commit

Permalink
Add context variables for PipelineRun and TaskRun UIDs
Browse files Browse the repository at this point in the history
A user may want to tag an oci image with the TaskRun or PipelineRun UIDs.
Currently, they can't do that because `metadata.uid` for TaskRuns and
PipelineRuns are not exposed.

In this PR, we add the UID context variable for TaskRuns and
PipelineRun. Users can now use `$(context.taskRun.uid)` and
`$(context.pipelineRun.uid)` to access and use UIDs.

In addition, we add validation for all context variables that are
supported so far -- `context.task.name`, `context.taskRun.name`,
`context.taskRun.namespace`, `context.taskRun.uid`,
`context.pipeline.name`, `context.pipelineRun.name`,
`context.pipelineRun.namespace`, `context.pipelineRun.uid`.

Partially fixes #2958
  • Loading branch information
Scott authored and jerop committed Aug 3, 2020
1 parent ae87d00 commit 5c0d1b0
Show file tree
Hide file tree
Showing 13 changed files with 345 additions and 10 deletions.
2 changes: 2 additions & 0 deletions docs/variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ This page documents the variable substitions supported by `Tasks` and `Pipelines
| `tasks.<taskName>.results.<resultName>` | The value of the `Task's` result. Can alter `Task` execution order within a `Pipeline`.) |
| `context.pipelineRun.name` | The name of the `PipelineRun` that this `Pipeline` is running in. |
| `context.pipelineRun.namespace` | The namespace of the `PipelineRun` that this `Pipeline` is running in. |
| `context.pipelineRun.uid` | The uid of the `PipelineRun` that this `Pipeline` is running in. |
| `context.pipeline.name` | The name of this `Pipeline` . |


Expand All @@ -33,6 +34,7 @@ This page documents the variable substitions supported by `Tasks` and `Pipelines
| `credentials.path` | The path to credentials injected from Secrets with matching annotations. |
| `context.taskRun.name` | The name of the `TaskRun` that this `Task` is running in. |
| `context.taskRun.namespace` | The namespace of the `TaskRun` that this `Task` is running in. |
| `context.taskRun.uid` | The uid of the `TaskRun` that this `Task` is running in. |
| `context.task.name` | The name of this `Task`. |

### `PipelineResource` variables available in a `Task`
Expand Down
34 changes: 34 additions & 0 deletions examples/v1beta1/pipelineruns/using_context_variables.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
kind: PipelineRun
apiVersion: tekton.dev/v1beta1
metadata:
generateName: test-pipelinerun-
spec:
serviceAccountName: 'default'
pipelineSpec:
tasks:
- name: task1
params:
- name: pipeline-uid
value: "$(context.pipelineRun.uid)"
- name: pipeline-name
value: "$(context.pipeline.name)"
- name: pipelineRun-name
value: "$(context.pipelineRun.name)"
taskSpec:
params:
- name: pipeline-uid
- name: pipeline-name
- name: pipelineRun-name
steps:
- image: ubuntu
name: print-uid
script: |
echo "TaskRun UID: $(context.taskRun.uid)"
echo "PipelineRun UID from params: $(params.pipeline-uid)"
- image: ubuntu
name: print-names
script: |
echo "Task name: $(context.task.name)"
echo "TaskRun name: $(context.taskRun.name)"
echo "Pipeline name from params: $(params.pipeline-name)"
echo "PipelineRun name from params: $(params.pipelineRun-name)"
16 changes: 16 additions & 0 deletions examples/v1beta1/taskruns/using_context_variables.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
kind: TaskRun
apiVersion: tekton.dev/v1beta1
metadata:
generateName: test-taskrun-
spec:
taskSpec:
steps:
- image: ubuntu
name: print-uid
script: |
echo "TaskRunUID name: $(context.taskRun.uid)"
- image: ubuntu
name: print-names
script: |
echo "Task name: $(context.task.name)"
echo "TaskRun name: $(context.taskRun.name)"
35 changes: 35 additions & 0 deletions pkg/apis/pipeline/v1beta1/pipeline_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,10 @@ func (ps *PipelineSpec) Validate(ctx context.Context) *apis.FieldError {
return err
}

if err := validatePipelineContextVariables(ps.Tasks); err != nil {
return err
}

// Validate the pipeline's workspaces.
if err := validatePipelineWorkspaces(ps.Workspaces, ps.Tasks, ps.Finally); err != nil {
return err
Expand Down Expand Up @@ -392,6 +396,37 @@ func validatePipelineArraysIsolated(name, value, prefix string, vars sets.String
return substitution.ValidateVariableIsolated(name, value, prefix, "task parameter", "pipelinespec.params", vars)
}

func validatePipelineContextVariables(tasks []PipelineTask) *apis.FieldError {
pipelineRunContextNames := sets.NewString().Insert(
"name",
"namespace",
"uid",
)
pipelineContextNames := sets.NewString().Insert(
"name",
)
var paramValues []string
for _, task := range tasks {
for _, param := range task.Params {
paramValues = append(paramValues, param.Value.StringVal)
paramValues = append(paramValues, param.Value.ArrayVal...)
}
}
if err := validatePipelineContextVariablesInParamValues(paramValues, "context\\.pipelineRun", pipelineRunContextNames); err != nil {
return err
}
return validatePipelineContextVariablesInParamValues(paramValues, "context\\.pipeline", pipelineContextNames)
}

func validatePipelineContextVariablesInParamValues(paramValues []string, prefix string, contextNames sets.String) *apis.FieldError {
for _, paramValue := range paramValues {
if err := substitution.ValidateVariable(fmt.Sprintf("param[%s]", paramValue), paramValue, prefix, "params", "pipelinespec.params", contextNames); err != nil {
return err
}
}
return nil
}

// validateParamResults ensures that task result variables are properly configured
func validateParamResults(tasks []PipelineTask) error {
for _, task := range tasks {
Expand Down
100 changes: 100 additions & 0 deletions pkg/apis/pipeline/v1beta1/pipeline_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1391,3 +1391,103 @@ func TestValidateFinalTasks_Failure(t *testing.T) {
})
}
}

func TestContextValid(t *testing.T) {
tests := []struct {
name string
tasks []PipelineTask
}{{
name: "valid string context variable for task name",
tasks: []PipelineTask{{
Name: "bar",
TaskRef: &TaskRef{Name: "bar-task"},
Params: []Param{{
Name: "a-param", Value: ArrayOrString{StringVal: "$(context.pipeline.name)"},
}},
}},
}, {
name: "valid string context variable for taskrun name",
tasks: []PipelineTask{{
Name: "bar",
TaskRef: &TaskRef{Name: "bar-task"},
Params: []Param{{
Name: "a-param", Value: ArrayOrString{StringVal: "$(context.pipelineRun.name)"},
}},
}},
}, {
name: "valid string context variable for taskRun namespace",
tasks: []PipelineTask{{
Name: "bar",
TaskRef: &TaskRef{Name: "bar-task"},
Params: []Param{{
Name: "a-param", Value: ArrayOrString{StringVal: "$(context.pipelineRun.namespace)"},
}},
}},
}, {
name: "valid string context variable for taskRun uid",
tasks: []PipelineTask{{
Name: "bar",
TaskRef: &TaskRef{Name: "bar-task"},
Params: []Param{{
Name: "a-param", Value: ArrayOrString{StringVal: "$(context.pipelineRun.uid)"},
}},
}},
}, {
name: "valid array context variables for task and taskRun names",
tasks: []PipelineTask{{
Name: "bar",
TaskRef: &TaskRef{Name: "bar-task"},
Params: []Param{{
Name: "a-param", Value: ArrayOrString{ArrayVal: []string{"$(context.pipeline.name)", "and", "$(context.pipelineRun.name)"}},
}},
}},
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := validatePipelineContextVariables(tt.tasks); err != nil {
t.Errorf("Pipeline.validatePipelineContextVariables() returned error for valid pipeline context variables: %s: %v", tt.name, err)
}
})
}
}

func TestContextInvalid(t *testing.T) {
tests := []struct {
name string
tasks []PipelineTask
}{{
name: "invalid string context variable for pipeline",
tasks: []PipelineTask{{
Name: "bar",
TaskRef: &TaskRef{Name: "bar-task"},
Params: []Param{{
Name: "a-param", Value: ArrayOrString{StringVal: "$(context.pipeline.missing)"},
}},
}},
}, {
name: "invalid string context variable for pipelineRun",
tasks: []PipelineTask{{
Name: "bar",
TaskRef: &TaskRef{Name: "bar-task"},
Params: []Param{{
Name: "a-param", Value: ArrayOrString{StringVal: "$(context.pipelineRun.missing)"},
}},
}},
}, {
name: "invalid array context variables for pipeline and pipelineRun",
tasks: []PipelineTask{{
Name: "bar",
TaskRef: &TaskRef{Name: "bar-task"},
Params: []Param{{
Name: "a-param", Value: ArrayOrString{ArrayVal: []string{"$(context.pipeline.missing)", "and", "$(context.pipelineRun.missing)"}},
}},
}},
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := validatePipelineContextVariables(tt.tasks); err == nil {
t.Errorf("Pipeline.validatePipelineContextVariables() did not return error for invalid pipeline parameters: %s, %s", tt.name, tt.tasks[0].Params)
}
})
}
}
20 changes: 20 additions & 0 deletions pkg/apis/pipeline/v1beta1/task_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@ func (ts *TaskSpec) Validate(ctx context.Context) *apis.FieldError {
if err := ValidateResults(ts.Results); err != nil {
return err
}

if err := validateTaskContextVariables(ts.Steps); err != nil {
return err
}

return nil
}

Expand Down Expand Up @@ -248,6 +253,21 @@ func ValidateParameterVariables(steps []Step, params []ParamSpec) *apis.FieldErr
return validateArrayUsage(steps, "params", arrayParameterNames)
}

func validateTaskContextVariables(steps []Step) *apis.FieldError {
taskRunContextNames := sets.NewString().Insert(
"name",
"namespace",
"uid",
)
taskContextNames := sets.NewString().Insert(
"name",
)
if err := validateVariables(steps, "context\\.taskRun", taskRunContextNames); err != nil {
return err
}
return validateVariables(steps, "context\\.task", taskContextNames)
}

func ValidateResourcesVariables(steps []Step, resources *TaskResources) *apis.FieldError {
if resources == nil {
return nil
Expand Down
69 changes: 69 additions & 0 deletions pkg/apis/pipeline/v1beta1/task_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,58 @@ func TestTaskSpecValidate(t *testing.T) {
Description: "my great result",
}},
},
}, {
name: "valid task name context",
fields: fields{
Steps: []v1beta1.Step{{
Container: corev1.Container{
Image: "my-image",
Args: []string{"arg"},
},
Script: `
#!/usr/bin/env bash
hello "$(context.task.name)"`,
}},
},
}, {
name: "valid taskrun name context",
fields: fields{
Steps: []v1beta1.Step{{
Container: corev1.Container{
Image: "my-image",
Args: []string{"arg"},
},
Script: `
#!/usr/bin/env bash
hello "$(context.taskRun.name)"`,
}},
},
}, {
name: "valid taskrun uid context",
fields: fields{
Steps: []v1beta1.Step{{
Container: corev1.Container{
Image: "my-image",
Args: []string{"arg"},
},
Script: `
#!/usr/bin/env bash
hello "$(context.taskRun.uid)"`,
}},
},
}, {
name: "valid context",
fields: fields{
Steps: []v1beta1.Step{{
Container: corev1.Container{
Image: "my-image",
Args: []string{"arg"},
},
Script: `
#!/usr/bin/env bash
hello "$(context.taskRun.namespace)"`,
}},
},
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down Expand Up @@ -852,6 +904,23 @@ func TestTaskSpecValidateError(t *testing.T) {
Paths: []string{"results[0].name"},
Details: "Name must consist of alphanumeric characters, '-', '_', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my-name', or 'my_name', regex used for validation is '^([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]$')",
},
}, {
name: "context not validate",
fields: fields{
Steps: []v1beta1.Step{{
Container: corev1.Container{
Image: "my-image",
Args: []string{"arg"},
},
Script: `
#!/usr/bin/env bash
hello "$(context.task.missing)"`,
}},
},
expectedError: apis.FieldError{
Message: `non-existent variable in "\n\t\t\t\t#!/usr/bin/env bash\n\t\t\t\thello \"$(context.task.missing)\"" for step script`,
Paths: []string{"taskspec.steps.script"},
},
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down
12 changes: 7 additions & 5 deletions pkg/reconciler/pipelinerun/resources/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,13 @@ func ApplyParameters(p *v1beta1.PipelineSpec, pr *v1beta1.PipelineRun) *v1beta1.
// ApplyContexts applies the substitution from $(context.(pipelineRun|pipeline).*) with the specified values.
// Currently supports only name substitution. Uses "" as a default if name is not specified.
func ApplyContexts(spec *v1beta1.PipelineSpec, pipelineName string, pr *v1beta1.PipelineRun) *v1beta1.PipelineSpec {
return ApplyReplacements(spec,
map[string]string{"context.pipelineRun.name": pr.Name,
"context.pipeline.name": pipelineName,
"context.pipelineRun.namespace": pr.Namespace},
map[string][]string{})
replacements := map[string]string{
"context.pipelineRun.name": pr.Name,
"context.pipeline.name": pipelineName,
"context.pipelineRun.namespace": pr.Namespace,
"context.pipelineRun.uid": string(pr.ObjectMeta.UID),
}
return ApplyReplacements(spec, replacements, map[string][]string{})
}

// ApplyTaskResults applies the ResolvedResultRef to each PipelineTask.Params in targets
Expand Down
17 changes: 17 additions & 0 deletions pkg/reconciler/pipelinerun/resources/apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,23 @@ func TestContext(t *testing.T) {
tb.PipelineTask("first-task-1", "first-task",
tb.PipelineTaskParam("first-task-first-param", "-1"),
))),
}, {
description: "context pipeline name replacement with pipelinerun uid",
pr: &v1beta1.PipelineRun{
ObjectMeta: metav1.ObjectMeta{
UID: "UID-1",
},
},
original: tb.Pipeline("test-pipeline",
tb.PipelineSpec(
tb.PipelineTask("first-task-1", "first-task",
tb.PipelineTaskParam("first-task-first-param", "$(context.pipelineRun.uid)"),
))),
expected: tb.Pipeline("test-pipeline",
tb.PipelineSpec(
tb.PipelineTask("first-task-1", "first-task",
tb.PipelineTaskParam("first-task-first-param", "UID-1"),
))),
}} {
t.Run(tc.description, func(t *testing.T) {
got := ApplyContexts(&tc.original.Spec, tc.original.Name, tc.pr)
Expand Down
Loading

0 comments on commit 5c0d1b0

Please sign in to comment.