From a695f097524f9d2e9e76b180f2f24b3a0246fd1e Mon Sep 17 00:00:00 2001 From: Scott Date: Thu, 23 Jul 2020 11:05:32 -0400 Subject: [PATCH] Add context variables for PipelineRun and TaskRun UIDs 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 --- docs/variables.md | 2 + .../pipelineruns/using_context_variables.yaml | 34 ++++++ .../taskruns/using_context_variables.yaml | 16 +++ .../pipeline/v1beta1/pipeline_validation.go | 35 ++++++ .../v1beta1/pipeline_validation_test.go | 100 ++++++++++++++++++ pkg/apis/pipeline/v1beta1/task_validation.go | 20 ++++ .../pipeline/v1beta1/task_validation_test.go | 69 ++++++++++++ pkg/reconciler/pipelinerun/resources/apply.go | 12 ++- .../pipelinerun/resources/apply_test.go | 17 +++ pkg/reconciler/taskrun/resources/apply.go | 12 ++- .../taskrun/resources/apply_test.go | 26 +++++ pkg/substitution/substitution.go | 2 +- pkg/substitution/substitution_test.go | 10 ++ 13 files changed, 345 insertions(+), 10 deletions(-) create mode 100644 examples/v1beta1/pipelineruns/using_context_variables.yaml create mode 100644 examples/v1beta1/taskruns/using_context_variables.yaml diff --git a/docs/variables.md b/docs/variables.md index 974c6642c6a..1e9a5599538 100644 --- a/docs/variables.md +++ b/docs/variables.md @@ -16,6 +16,7 @@ This page documents the variable substitions supported by `Tasks` and `Pipelines | `tasks..results.` | 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` . | @@ -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` diff --git a/examples/v1beta1/pipelineruns/using_context_variables.yaml b/examples/v1beta1/pipelineruns/using_context_variables.yaml new file mode 100644 index 00000000000..d05cf49b3a5 --- /dev/null +++ b/examples/v1beta1/pipelineruns/using_context_variables.yaml @@ -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)" \ No newline at end of file diff --git a/examples/v1beta1/taskruns/using_context_variables.yaml b/examples/v1beta1/taskruns/using_context_variables.yaml new file mode 100644 index 00000000000..46f1a3af01e --- /dev/null +++ b/examples/v1beta1/taskruns/using_context_variables.yaml @@ -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)" diff --git a/pkg/apis/pipeline/v1beta1/pipeline_validation.go b/pkg/apis/pipeline/v1beta1/pipeline_validation.go index 8019dc401e2..a82544ec1cc 100644 --- a/pkg/apis/pipeline/v1beta1/pipeline_validation.go +++ b/pkg/apis/pipeline/v1beta1/pipeline_validation.go @@ -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 @@ -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 { diff --git a/pkg/apis/pipeline/v1beta1/pipeline_validation_test.go b/pkg/apis/pipeline/v1beta1/pipeline_validation_test.go index 5c6150d32e5..708f19c92fa 100644 --- a/pkg/apis/pipeline/v1beta1/pipeline_validation_test.go +++ b/pkg/apis/pipeline/v1beta1/pipeline_validation_test.go @@ -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) + } + }) + } +} \ No newline at end of file diff --git a/pkg/apis/pipeline/v1beta1/task_validation.go b/pkg/apis/pipeline/v1beta1/task_validation.go index 846faf315d8..f5673bba672 100644 --- a/pkg/apis/pipeline/v1beta1/task_validation.go +++ b/pkg/apis/pipeline/v1beta1/task_validation.go @@ -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 } @@ -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 diff --git a/pkg/apis/pipeline/v1beta1/task_validation_test.go b/pkg/apis/pipeline/v1beta1/task_validation_test.go index 5431cf546db..37bbbad2849 100644 --- a/pkg/apis/pipeline/v1beta1/task_validation_test.go +++ b/pkg/apis/pipeline/v1beta1/task_validation_test.go @@ -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) { @@ -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) { diff --git a/pkg/reconciler/pipelinerun/resources/apply.go b/pkg/reconciler/pipelinerun/resources/apply.go index 4962f727b05..dd12c092151 100644 --- a/pkg/reconciler/pipelinerun/resources/apply.go +++ b/pkg/reconciler/pipelinerun/resources/apply.go @@ -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 diff --git a/pkg/reconciler/pipelinerun/resources/apply_test.go b/pkg/reconciler/pipelinerun/resources/apply_test.go index 003b5beef6b..3be1f59fa05 100644 --- a/pkg/reconciler/pipelinerun/resources/apply_test.go +++ b/pkg/reconciler/pipelinerun/resources/apply_test.go @@ -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) diff --git a/pkg/reconciler/taskrun/resources/apply.go b/pkg/reconciler/taskrun/resources/apply.go index e8706acfb69..6893a5a481d 100644 --- a/pkg/reconciler/taskrun/resources/apply.go +++ b/pkg/reconciler/taskrun/resources/apply.go @@ -97,11 +97,15 @@ func ApplyResources(spec *v1beta1.TaskSpec, resolvedResources map[string]v1beta1 } // ApplyContexts applies the substitution from $(context.(taskRun|task).*) with the specified values. -// Currently supports only name substitution. Uses "" as a default if name is not specified. +// Uses "" as a default if a value is not available. func ApplyContexts(spec *v1beta1.TaskSpec, rtr *ResolvedTaskResources, tr *v1beta1.TaskRun) *v1beta1.TaskSpec { - return ApplyReplacements(spec, - map[string]string{"context.taskRun.name": tr.Name, "context.task.name": rtr.TaskName, "context.taskRun.namespace": tr.Namespace}, - map[string][]string{}) + replacements := map[string]string{ + "context.taskRun.name": tr.Name, + "context.task.name": rtr.TaskName, + "context.taskRun.namespace": tr.Namespace, + "context.taskRun.uid": string(tr.ObjectMeta.UID), + } + return ApplyReplacements(spec, replacements, map[string][]string{}) } // ApplyWorkspaces applies the substitution from paths that the workspaces in w are mounted to, the diff --git a/pkg/reconciler/taskrun/resources/apply_test.go b/pkg/reconciler/taskrun/resources/apply_test.go index afbdf46f934..40625a752c7 100644 --- a/pkg/reconciler/taskrun/resources/apply_test.go +++ b/pkg/reconciler/taskrun/resources/apply_test.go @@ -928,6 +928,32 @@ func TestContext(t *testing.T) { }, }}, }, + }, { + description: "context UID replacement", + rtr: resources.ResolvedTaskResources{ + TaskName: "Task1", + }, + tr: v1beta1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{ + UID: "UID-1", + }, + }, + spec: v1beta1.TaskSpec{ + Steps: []v1beta1.Step{{ + Container: corev1.Container{ + Name: "ImageName", + Image: "$(context.taskRun.uid)", + }, + }}, + }, + want: v1beta1.TaskSpec{ + Steps: []v1beta1.Step{{ + Container: corev1.Container{ + Name: "ImageName", + Image: "UID-1", + }, + }}, + }, }} { t.Run(tc.description, func(t *testing.T) { got := resources.ApplyContexts(&tc.spec, &tc.rtr, &tc.tr) diff --git a/pkg/substitution/substitution.go b/pkg/substitution/substitution.go index 30d027074a1..06d5b96c0a5 100644 --- a/pkg/substitution/substitution.go +++ b/pkg/substitution/substitution.go @@ -27,7 +27,7 @@ import ( const parameterSubstitution = `[_a-zA-Z][_a-zA-Z0-9.-]*(\[\*\])?` -const braceMatchingRegex = "(\\$(\\(%s.(?P%s)\\)))" +const braceMatchingRegex = "(\\$(\\(%s\\.(?P%s)\\)))" func ValidateVariable(name, value, prefix, locationName, path string, vars sets.String) *apis.FieldError { if vs, present := extractVariablesFromString(value, prefix); present { diff --git a/pkg/substitution/substitution_test.go b/pkg/substitution/substitution_test.go index c218fda0612..1c7f497b579 100644 --- a/pkg/substitution/substitution_test.go +++ b/pkg/substitution/substitution_test.go @@ -49,6 +49,16 @@ func TestValidateVariables(t *testing.T) { vars: sets.NewString("baz"), }, expectedError: nil, + }, { + name: "valid variable uid", + args: args{ + input: "--flag=$(context.taskRun.uid)", + prefix: "context.taskRun", + locationName: "step", + path: "taskspec.steps", + vars: sets.NewString("uid"), + }, + expectedError: nil, }, { name: "multiple variables", args: args{