From 16231099afed30ae582b21a546951d021cb8c003 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 | 35 +++ .../taskruns/using_context_variables.yaml | 16 ++ pkg/apis/pipeline/v1beta1/task_validation.go | 6 +- pkg/reconciler/pipelinerun/pipelinerun.go | 11 + pkg/reconciler/pipelinerun/resources/apply.go | 12 +- .../pipelinerun/resources/apply_test.go | 17 ++ .../resources/validate_contexts.go | 87 ++++++++ .../resources/validate_contexts_test.go | 110 +++++++++ pkg/reconciler/taskrun/resources/apply.go | 12 +- .../taskrun/resources/apply_test.go | 54 +++++ pkg/reconciler/taskrun/taskrun.go | 6 + pkg/reconciler/taskrun/validate_contexts.go | 40 ++++ .../taskrun/validate_contexts_test.go | 211 ++++++++++++++++++ pkg/substitution/substitution.go | 2 +- pkg/substitution/substitution_test.go | 10 + 16 files changed, 618 insertions(+), 13 deletions(-) create mode 100644 examples/v1beta1/pipelineruns/using_context_variables.yaml create mode 100644 examples/v1beta1/taskruns/using_context_variables.yaml create mode 100644 pkg/reconciler/pipelinerun/resources/validate_contexts.go create mode 100644 pkg/reconciler/pipelinerun/resources/validate_contexts_test.go create mode 100644 pkg/reconciler/taskrun/validate_contexts.go create mode 100644 pkg/reconciler/taskrun/validate_contexts_test.go 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..f0f369397a6 --- /dev/null +++ b/examples/v1beta1/pipelineruns/using_context_variables.yaml @@ -0,0 +1,35 @@ +kind: PipelineRun +apiVersion: tekton.dev/v1beta1 +metadata: + generateName: test-pipelinerun- +spec: + 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)" + - name: pipelineRun-name-uid + value: ["$(context.pipelineRun.name)", "$(context.pipelineRun.uid)"] + 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)" 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/task_validation.go b/pkg/apis/pipeline/v1beta1/task_validation.go index 846faf315d8..19a3c47b461 100644 --- a/pkg/apis/pipeline/v1beta1/task_validation.go +++ b/pkg/apis/pipeline/v1beta1/task_validation.go @@ -242,7 +242,7 @@ func ValidateParameterVariables(steps []Step, params []ParamSpec) *apis.FieldErr } } - if err := validateVariables(steps, "params", parameterNames); err != nil { + if err := ValidateVariables(steps, "params", parameterNames); err != nil { return err } return validateArrayUsage(steps, "params", arrayParameterNames) @@ -263,7 +263,7 @@ func ValidateResourcesVariables(steps []Step, resources *TaskResources) *apis.Fi resourceNames.Insert(r.Name) } } - return validateVariables(steps, "resources.(?:inputs|outputs)", resourceNames) + return ValidateVariables(steps, "resources.(?:inputs|outputs)", resourceNames) } func validateArrayUsage(steps []Step, prefix string, vars sets.String) *apis.FieldError { @@ -310,7 +310,7 @@ func validateArrayUsage(steps []Step, prefix string, vars sets.String) *apis.Fie return nil } -func validateVariables(steps []Step, prefix string, vars sets.String) *apis.FieldError { +func ValidateVariables(steps []Step, prefix string, vars sets.String) *apis.FieldError { for _, step := range steps { if err := validateTaskVariable("name", step.Name, prefix, vars); err != nil { return err diff --git a/pkg/reconciler/pipelinerun/pipelinerun.go b/pkg/reconciler/pipelinerun/pipelinerun.go index 01ca1e31fe9..2689391dd56 100644 --- a/pkg/reconciler/pipelinerun/pipelinerun.go +++ b/pkg/reconciler/pipelinerun/pipelinerun.go @@ -97,6 +97,9 @@ const ( // ReasonCouldntCancel indicates that a PipelineRun was cancelled but attempting to update // all of the running TaskRuns as cancelled failed. ReasonCouldntCancel = "PipelineRunCouldntCancel" + // ReasonFailedContextValidation indicates that the reason for failure is that the PipelineRun + // failed to resolve the contexts. + ReasonFailedContextValidation = "ReasonFailedContextValidation" ) // Reconciler implements controller.Reconciler for Configuration resources. @@ -362,6 +365,14 @@ func (c *Reconciler) reconcile(ctx context.Context, pr *v1beta1.PipelineRun) err return controller.NewPermanentError(err) } + // Ensure that the context variables in the PipelineRun parameters are valid. + if err := resources.ValidateContextVariables(pipelineSpec, pr); err != nil { + pr.Status.MarkFailed(ReasonFailedContextValidation, + "PipelineRun %s/%s parameters has invalid context variable %s/%s's: %s", + pr.Namespace, pr.Name, pr.Namespace, pipelineMeta.Name, err) + return controller.NewPermanentError(err) + } + // Ensure that the ServiceAccountNames defined correct. if err := resources.ValidateServiceaccountMapping(pipelineSpec, pr); err != nil { pr.Status.MarkFailed(ReasonInvalidServiceAccountMapping, 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..389f9e93b93 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/pipelinerun/resources/validate_contexts.go b/pkg/reconciler/pipelinerun/resources/validate_contexts.go new file mode 100644 index 00000000000..0ff3f042455 --- /dev/null +++ b/pkg/reconciler/pipelinerun/resources/validate_contexts.go @@ -0,0 +1,87 @@ +/* +Copyright 2020 The Tekton Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package resources + +import ( + "fmt" + + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" + "github.com/tektoncd/pipeline/pkg/substitution" + "k8s.io/apimachinery/pkg/util/sets" + "knative.dev/pkg/apis" +) + +// ValidateContextVariables validate that context variables used in Pipelines and PipelineRuns are valid +func ValidateContextVariables(ps *v1beta1.PipelineSpec, pr *v1beta1.PipelineRun) *apis.FieldError { + var paramValues []string + + for _, param := range(pr.Spec.Params) { + newParamValues := getParameterValues(param) + paramValues = append(paramValues, newParamValues...) + } + + if err := validatePipelineRunContextVariables(paramValues); err != nil { + return err + } + if err := validatePipelineContextVariables(¶mValues); err != nil { + return err + } + return nil +} + + +func validatePipelineRunContextVariables(paramValues []string) *apis.FieldError { + pipelineRunContextNames := sets.NewString().Insert( + "name", + "namespace", + "uid", + ) + + for _, paramValue := range(paramValues) { + if err := substitution.ValidateVariable(fmt.Sprintf("param[%s]", paramValue), paramValue, "context\\.pipelineRun", "params", "pipelinespec.params", pipelineRunContextNames); err != nil { + return err + } + } + + return nil +} + +func validatePipelineContextVariables(paramValues *[]string) *apis.FieldError { + pipelineContextNames := sets.NewString().Insert( + "name", + ) + + for _, paramValue := range(*paramValues) { + if err := substitution.ValidateVariable(fmt.Sprintf("param[%s]", paramValue), paramValue, "context\\.pipeline", "params", "pipelinespec.params", pipelineContextNames); err != nil { + return err + } + } + + return nil +} + +func getParameterValues(param v1beta1.Param) []string{ + var paramValues []string + + if param.Value.Type == v1beta1.ParamTypeString { + paramValues = append(paramValues, param.Value.StringVal) + } else { + paramValues = append(paramValues, param.Value.ArrayVal...) + } + + return paramValues +} diff --git a/pkg/reconciler/pipelinerun/resources/validate_contexts_test.go b/pkg/reconciler/pipelinerun/resources/validate_contexts_test.go new file mode 100644 index 00000000000..2b4dca87e95 --- /dev/null +++ b/pkg/reconciler/pipelinerun/resources/validate_contexts_test.go @@ -0,0 +1,110 @@ +/* +Copyright 2020 The Tekton Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package resources + +import ( + "testing" + + tb "github.com/tektoncd/pipeline/internal/builder/v1beta1" + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" +) + +func TestContextValid(t *testing.T) { + tcs := []struct { + name string + p *v1beta1.Pipeline + original *v1beta1.PipelineRun + }{{ + name: "param pipeline name context variable", + p: tb.Pipeline("a-pipeline", tb.PipelineSpec( + tb.PipelineParamSpec("pipeline-name", v1beta1.ParamTypeString))), + original: tb.PipelineRun("a-pipelinerun", tb.PipelineRunSpec( + "test-pipeline", + tb.PipelineRunParam("pipeline-name", "$(context.pipeline.name)"))), + }, { + name: "param pipelinerun name context variable", + p: tb.Pipeline("a-pipeline", tb.PipelineSpec( + tb.PipelineParamSpec("pipelinerun-name", v1beta1.ParamTypeString))), + original: tb.PipelineRun("a-pipelinerun", tb.PipelineRunSpec( + "test-pipeline", + tb.PipelineRunParam("pipelinerun-name", "$(context.pipelineRun.name)"))), + }, { + name: "param pipelinerun uid context variable", + p: tb.Pipeline("a-pipeline", tb.PipelineSpec( + tb.PipelineParamSpec("pipelinerun-uid", v1beta1.ParamTypeString))), + original: tb.PipelineRun("a-pipelinerun", tb.PipelineRunSpec( + "test-pipeline", + tb.PipelineRunParam("pipelinerun-uid", "$(context.pipelineRun.uid)"))), + }, { + name: "param pipelinerun name, uid context variable", + p: tb.Pipeline("a-pipeline", tb.PipelineSpec( + tb.PipelineParamSpec("pipelinerun-name-uid", v1beta1.ParamTypeString))), + original: tb.PipelineRun("a-pipelinerun", tb.PipelineRunSpec( + "test-pipeline", + tb.PipelineRunParam("pipelinerun-name-uid", "$(context.pipelineRun.name)", "$(context.pipelineRun.uid)"))), + }} + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + if err := ValidateContextVariables(&tc.p.Spec, tc.original); err != nil { + t.Errorf("Did not expect to see error when validating valid contexts but saw %v", err) + } + }) + } +} + +func TestContextInvalid(t *testing.T) { + tcs := []struct { + name string + p *v1beta1.Pipeline + pr *v1beta1.PipelineRun + }{{ + name: "param pipeline name context variable", + p: tb.Pipeline("a-pipeline", tb.PipelineSpec( + tb.PipelineParamSpec("pipeline-name", v1beta1.ParamTypeString))), + pr: tb.PipelineRun("a-pipelinerun", tb.PipelineRunSpec( + "test-pipeline", + tb.PipelineRunParam("pipeline-name", "$(context.pipeline.missing)"))), + }, { + name: "param pipelinerun name context variable", + p: tb.Pipeline("a-pipeline", tb.PipelineSpec( + tb.PipelineParamSpec("pipelinerun-name", v1beta1.ParamTypeString))), + pr: tb.PipelineRun("a-pipelinerun", tb.PipelineRunSpec( + "test-pipeline", + tb.PipelineRunParam("pipelinerun-name", "$(context.pipelineRun.missing)"))), + }, { + name: "param pipelinerun uid context variable", + p: tb.Pipeline("a-pipeline", tb.PipelineSpec( + tb.PipelineParamSpec("pipelinerun-uid", v1beta1.ParamTypeString))), + pr: tb.PipelineRun("a-pipelinerun", tb.PipelineRunSpec( + "test-pipeline", + tb.PipelineRunParam("pipelinerun-uid", "$(context.pipelineRun.missing)"))), + }, { + name: "param pipelinerun name, uid context variable", + p: tb.Pipeline("a-pipeline", tb.PipelineSpec( + tb.PipelineParamSpec("pipelinerun-name-uid", v1beta1.ParamTypeString))), + pr: tb.PipelineRun("a-pipelinerun", tb.PipelineRunSpec( + "test-pipeline", + tb.PipelineRunParam("pipelinerun-name-uid", "$(context.pipelineRun.missing)", "$(context.pipelineRun.missing)"))), + }} + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + if err := ValidateContextVariables(&tc.p.Spec, tc.pr); err == nil { + t.Errorf("expected to see error when validating invalid contexts") + } + }) + } +} 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 e30587b0013..b6d4a75cd44 100644 --- a/pkg/reconciler/taskrun/resources/apply_test.go +++ b/pkg/reconciler/taskrun/resources/apply_test.go @@ -929,6 +929,60 @@ 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", + }, + }}, + }, + }, { + description: "context annotations replacement", + rtr: resources.ResolvedTaskResources{ + TaskName: "Task1", + }, + tr: v1beta1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "hello-world.dev/my.test-annotation_foo": "test-annotation-bar", + }, + }, + }, + spec: v1beta1.TaskSpec{ + Steps: []v1beta1.Step{{ + Container: corev1.Container{ + Name: "ImageName", + Image: "$(context.taskRun.annotations.hello-world.dev/my.test-annotation_foo)", + }, + }}, + }, + want: v1beta1.TaskSpec{ + Steps: []v1beta1.Step{{ + Container: corev1.Container{ + Name: "ImageName", + Image: "test-annotation-bar", + }, + }}, + }, }} { t.Run(tc.description, func(t *testing.T) { got := resources.ApplyContexts(&tc.spec, &tc.rtr, &tc.tr) diff --git a/pkg/reconciler/taskrun/taskrun.go b/pkg/reconciler/taskrun/taskrun.go index cee6d5cb489..7fa4aac5bcd 100644 --- a/pkg/reconciler/taskrun/taskrun.go +++ b/pkg/reconciler/taskrun/taskrun.go @@ -294,6 +294,12 @@ func (c *Reconciler) prepare(ctx context.Context, tr *v1beta1.TaskRun) (*v1beta1 return nil, nil, controller.NewPermanentError(err) } + if err := validateContextVariables(taskSpec, tr); err != nil { + logger.Errorf("TaskRun %q context is invalid: %v", tr.Name, err) + tr.Status.MarkResourceFailed(podconvert.ReasonFailedValidation, err) + return nil, nil, controller.NewPermanentError(err) + } + // Initialize the cloud events if at least a CloudEventResource is defined // and they have not been initialized yet. // FIXME(afrittoli) This resource specific logic will have to be replaced diff --git a/pkg/reconciler/taskrun/validate_contexts.go b/pkg/reconciler/taskrun/validate_contexts.go new file mode 100644 index 00000000000..2130936d99b --- /dev/null +++ b/pkg/reconciler/taskrun/validate_contexts.go @@ -0,0 +1,40 @@ +/* +Copyright 2020 The Tekton Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package taskrun + +import ( + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" + "k8s.io/apimachinery/pkg/util/sets" + "knative.dev/pkg/apis" +) + +func validateContextVariables(ts *v1beta1.TaskSpec, tr *v1beta1.TaskRun) *apis.FieldError { + taskRunContextNames := sets.NewString().Insert( + "name", + "namespace", + "uid", + ) + taskContextNames := sets.NewString().Insert( + "name", + ) + if err := v1beta1.ValidateVariables(ts.Steps, "context\\.taskRun", taskRunContextNames); err != nil { + return err + } + + return v1beta1.ValidateVariables(ts.Steps, "context\\.task", taskContextNames) + +} diff --git a/pkg/reconciler/taskrun/validate_contexts_test.go b/pkg/reconciler/taskrun/validate_contexts_test.go new file mode 100644 index 00000000000..6856f3ebba6 --- /dev/null +++ b/pkg/reconciler/taskrun/validate_contexts_test.go @@ -0,0 +1,211 @@ +/* +Copyright 2020 The Tekton Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package taskrun + +import ( + "testing" + + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" + "github.com/tektoncd/pipeline/pkg/reconciler/taskrun/resources" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestContextValid(t *testing.T) { + for _, tc := range []struct { + description string + rtr resources.ResolvedTaskResources + tr v1beta1.TaskRun + spec v1beta1.TaskSpec + }{{ + description: "context taskName replacement without taskRun in spec container", + rtr: resources.ResolvedTaskResources{ + TaskName: "Task1", + }, + tr: v1beta1.TaskRun{}, + spec: v1beta1.TaskSpec{ + Steps: []v1beta1.Step{{ + Container: corev1.Container{ + Name: "ImageName", + Image: "$(context.task.name)-1", + }, + }}, + }, + }, { + description: "context taskName replacement with taskRun in spec container", + rtr: resources.ResolvedTaskResources{ + TaskName: "Task1", + }, + tr: v1beta1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: "taskrunName", + }, + }, + spec: v1beta1.TaskSpec{ + Steps: []v1beta1.Step{{ + Container: corev1.Container{ + Name: "ImageName", + Image: "$(context.task.name)-1", + }, + }}, + }, + }, { + description: "context taskRunName replacement with defined taskRun in spec container", + rtr: resources.ResolvedTaskResources{ + TaskName: "Task1", + }, + tr: v1beta1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: "taskrunName", + }, + }, + spec: v1beta1.TaskSpec{ + Steps: []v1beta1.Step{{ + Container: corev1.Container{ + Name: "ImageName", + Image: "$(context.taskRun.name)-1", + }, + }}, + }, + }, { + description: "context taskRunName replacement with no defined taskRun name in spec container", + rtr: resources.ResolvedTaskResources{ + TaskName: "Task1", + }, + tr: v1beta1.TaskRun{}, + spec: v1beta1.TaskSpec{ + Steps: []v1beta1.Step{{ + Container: corev1.Container{ + Name: "ImageName", + Image: "$(context.taskRun.name)-1", + }, + }}, + }, + }, { + description: "context taskRun namespace replacement with no defined namespace in spec container", + rtr: resources.ResolvedTaskResources{ + TaskName: "Task1", + }, + tr: v1beta1.TaskRun{}, + spec: v1beta1.TaskSpec{ + Steps: []v1beta1.Step{{ + Container: corev1.Container{ + Name: "ImageName", + Image: "$(context.taskRun.namespace)-1", + }, + }}, + }, + }, { + description: "context taskRun namespace replacement with defined namespace in spec container", + rtr: resources.ResolvedTaskResources{ + TaskName: "Task1", + }, + tr: v1beta1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: "taskrunName", + Namespace: "trNamespace", + }, + }, + spec: v1beta1.TaskSpec{ + Steps: []v1beta1.Step{{ + Container: corev1.Container{ + Name: "ImageName", + Image: "$(context.taskRun.namespace)-1", + }, + }}, + }, + }, { + description: "context taskRunName replacement with no defined taskName in spec container", + rtr: resources.ResolvedTaskResources{}, + tr: v1beta1.TaskRun{}, + spec: v1beta1.TaskSpec{ + Steps: []v1beta1.Step{{ + Container: corev1.Container{ + Name: "ImageName", + Image: "$(context.task.name)-1", + }, + }}, + }, + }, { + description: "context UID replacement valid", + 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)", + }, + }}, + }, + }} { + t.Run(tc.description, func(t *testing.T) { + if err := validateContextVariables(&tc.spec, &tc.tr); err != nil { + t.Fatalf("Did not expect to see error when validating valid contexts but saw %v", err) + } + }) + } +} + +func TestContextInvalid(t *testing.T) { + for _, tc := range []struct { + description string + rtr resources.ResolvedTaskResources + tr v1beta1.TaskRun + spec v1beta1.TaskSpec + }{{ + description: "context taskRun replacement invalid", + rtr: resources.ResolvedTaskResources{ + TaskName: "Task1", + }, + tr: v1beta1.TaskRun{}, + spec: v1beta1.TaskSpec{ + Steps: []v1beta1.Step{{ + Container: corev1.Container{ + Name: "ImageName", + Image: "$(context.taskRun.missing)", + }, + }}, + }, + }, { + description: "context task replacement invalid", + rtr: resources.ResolvedTaskResources{ + TaskName: "Task1", + }, + tr: v1beta1.TaskRun{}, + spec: v1beta1.TaskSpec{ + Steps: []v1beta1.Step{{ + Container: corev1.Container{ + Name: "ImageName", + Image: "$(context.task.missing)", + }, + }}, + }, + }} { + t.Run(tc.description, func(t *testing.T) { + if err := validateContextVariables(&tc.spec, &tc.tr); err == nil { + t.Fatalf("expected to see error when validating invalid contexts but saw %v", err) + } + }) + } +} 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{