diff --git a/pkg/apis/pipeline/v1beta1/pipeline_validation.go b/pkg/apis/pipeline/v1beta1/pipeline_validation.go index 168090eb398..ea12e6450a3 100644 --- a/pkg/apis/pipeline/v1beta1/pipeline_validation.go +++ b/pkg/apis/pipeline/v1beta1/pipeline_validation.go @@ -41,16 +41,18 @@ func (p *Pipeline) Validate(ctx context.Context) *apis.FieldError { return p.Spec.Validate(ctx) } -func validateDeclaredResources(ps *PipelineSpec) error { +// validateDeclaredResources ensures that the specified resources have unique names and +// validates that all the resources referenced by pipeline tasks are declared in the pipeline +func validateDeclaredResources(resources []PipelineDeclaredResource, tasks []PipelineTask) error { encountered := map[string]struct{}{} - for _, r := range ps.Resources { + for _, r := range resources { if _, ok := encountered[r.Name]; ok { return fmt.Errorf("resource with name %q appears more than once", r.Name) } encountered[r.Name] = struct{}{} } required := []string{} - for _, t := range ps.Tasks { + for _, t := range tasks { if t.Resources != nil { for _, input := range t.Resources.Inputs { required = append(required, input.Resource) @@ -67,8 +69,8 @@ func validateDeclaredResources(ps *PipelineSpec) error { } } - provided := make([]string, 0, len(ps.Resources)) - for _, resource := range ps.Resources { + provided := make([]string, 0, len(resources)) + for _, resource := range resources { provided = append(provided, resource.Name) } missing := list.DiffLeft(required, provided) @@ -150,7 +152,7 @@ func (ps *PipelineSpec) Validate(ctx context.Context) *apis.FieldError { // All declared resources should be used, and the Pipeline shouldn't try to use any resources // that aren't declared - if err := validateDeclaredResources(ps); err != nil { + if err := validateDeclaredResources(ps.Resources, ps.Tasks); err != nil { return apis.ErrInvalidValue(err.Error(), "spec.resources") } @@ -186,6 +188,8 @@ func (ps *PipelineSpec) Validate(ctx context.Context) *apis.FieldError { return nil } +// validatePipelineTasks ensures that pipeline tasks has unique label, pipeline tasks has specified one of +// taskRef or taskSpec, and in case of a pipeline task with taskRef, it has a reference to a valid task (task name) func validatePipelineTasks(ctx context.Context, tasks []PipelineTask) *apis.FieldError { // Names cannot be duplicated taskNames := map[string]struct{}{} @@ -239,6 +243,8 @@ func validatePipelineTaskName(ctx context.Context, prefix string, i int, t Pipel return nil } +// validatePipelineWorkspaces validates the specified workspaces, ensuring having unique name without any empty string, +// and validates that all the referenced workspaces (by pipeline tasks) are specified in the pipeline func validatePipelineWorkspaces(wss []WorkspacePipelineDeclaration, pts []PipelineTask) *apis.FieldError { // Workspace names must be non-empty and unique. wsTable := make(map[string]struct{}) @@ -267,6 +273,9 @@ func validatePipelineWorkspaces(wss []WorkspacePipelineDeclaration, pts []Pipeli return nil } +// validatePipelineParameterVariables validates parameters with those specified by each pipeline task, +// (1) it validates the type of parameter is either string or array (2) parameter default value matches +// with the type of that param (3) ensures that the referenced param variable is defined is part of the param declarations func validatePipelineParameterVariables(tasks []PipelineTask, params []ParamSpec) *apis.FieldError { parameterNames := map[string]struct{}{} arrayParameterNames := map[string]struct{}{} @@ -342,7 +351,7 @@ func validatePipelineArraysIsolated(name, value, prefix string, vars map[string] return substitution.ValidateVariableIsolated(name, value, prefix, "task parameter", "pipelinespec.params", vars) } -// validateParamResults ensure that task result variables are properly configured +// validateParamResults ensures that task result variables are properly configured func validateParamResults(tasks []PipelineTask) error { for _, task := range tasks { for _, param := range task.Params { @@ -371,7 +380,7 @@ func filter(arr []string, cond func(string) bool) []string { return result } -// validatePipelineResults ensure that task result variables are properly configured +// validatePipelineResults ensure that pipeline result variables are properly configured func validatePipelineResults(results []PipelineResult) error { for _, result := range results { expressions, ok := GetVarSubstitutionExpressionsForPipelineResult(result) diff --git a/pkg/apis/pipeline/v1beta1/pipeline_validation_test.go b/pkg/apis/pipeline/v1beta1/pipeline_validation_test.go index 88ac440197b..06a3574a77d 100644 --- a/pkg/apis/pipeline/v1beta1/pipeline_validation_test.go +++ b/pkg/apis/pipeline/v1beta1/pipeline_validation_test.go @@ -14,745 +14,1034 @@ See the License for the specific language governing permissions and limitations under the License. */ -package v1beta1_test +package v1beta1 import ( "context" "testing" + "time" - "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func TestPipeline_Validate(t *testing.T) { +func TestPipeline_Validate_Success(t *testing.T) { tests := []struct { - name string - p *v1beta1.Pipeline - failureExpected bool + name string + p *Pipeline }{{ name: "valid metadata", - p: &v1beta1.Pipeline{ + p: &Pipeline{ ObjectMeta: metav1.ObjectMeta{Name: "pipeline"}, - Spec: v1beta1.PipelineSpec{ - Tasks: []v1beta1.PipelineTask{{Name: "foo", TaskRef: &v1beta1.TaskRef{Name: "foo-task"}}}, + Spec: PipelineSpec{ + Tasks: []PipelineTask{{Name: "foo", TaskRef: &TaskRef{Name: "foo-task"}}}, }, }, - failureExpected: false, }, { - name: "valid resource declarations and usage", - p: &v1beta1.Pipeline{ + name: "valid pipeline with params, resources, workspaces, task results, and pipeline results", + p: &Pipeline{ ObjectMeta: metav1.ObjectMeta{Name: "pipeline"}, - Spec: v1beta1.PipelineSpec{ - Resources: []v1beta1.PipelineDeclaredResource{{ - Name: "great-resource", Type: v1beta1.PipelineResourceTypeGit, + Spec: PipelineSpec{ + Description: "this is a valid pipeline with all possible fields initialized", + Resources: []PipelineDeclaredResource{{ + Name: "app-repo", + Type: "git", + Optional: false, }, { - Name: "wonderful-resource", Type: v1beta1.PipelineResourceTypeImage, - }}, - Tasks: []v1beta1.PipelineTask{{ - Name: "bar", - TaskRef: &v1beta1.TaskRef{Name: "bar-task"}, - Resources: &v1beta1.PipelineTaskResources{ - Inputs: []v1beta1.PipelineTaskInputResource{{ - Name: "some-workspace", Resource: "great-resource", + Name: "app-image", + Type: "git", + Optional: false, + }}, + Tasks: []PipelineTask{{ + Name: "my-task", + TaskRef: &TaskRef{Name: "foo-task"}, + Retries: 5, + Resources: &PipelineTaskResources{ + Inputs: []PipelineTaskInputResource{{ + Name: "task-app-repo", + Resource: "app-repo", }}, - Outputs: []v1beta1.PipelineTaskOutputResource{{ - Name: "some-imagee", Resource: "wonderful-resource", + Outputs: []PipelineTaskOutputResource{{ + Name: "task-app-image", + Resource: "app-image", }}, }, - Conditions: []v1beta1.PipelineTaskCondition{{ - ConditionRef: "some-condition", - Resources: []v1beta1.PipelineTaskInputResource{{ - Name: "some-workspace", Resource: "great-resource", - }}, + Params: []Param{{ + Name: "param1", + Value: ArrayOrString{}, }}, - }, { - Name: "foo", - TaskRef: &v1beta1.TaskRef{Name: "foo-task"}, - Resources: &v1beta1.PipelineTaskResources{ - Inputs: []v1beta1.PipelineTaskInputResource{{ - Name: "some-imagee", Resource: "wonderful-resource", From: []string{"bar"}, - }}, - }, - Conditions: []v1beta1.PipelineTaskCondition{{ - ConditionRef: "some-condition-2", - Resources: []v1beta1.PipelineTaskInputResource{{ - Name: "some-image", Resource: "wonderful-resource", From: []string{"bar"}, - }}, + Workspaces: []WorkspacePipelineTaskBinding{{ + Name: "task-shared-workspace", + Workspace: "shared-workspace", }}, + Timeout: &metav1.Duration{Duration: 5 * time.Minute}, + }}, + Params: []ParamSpec{{ + Name: "param1", + Type: ParamType("string"), + Description: "this is my param", + Default: &ArrayOrString{ + Type: ParamType("string"), + StringVal: "pipeline-default", + }, + }}, + Workspaces: []WorkspacePipelineDeclaration{{ + Name: "shared-workspace", + Description: "this is my shared workspace", + }}, + Results: []PipelineResult{{ + Name: "pipeline-result", + Description: "this is my pipeline result", + Value: "pipeline-result-default", }}, }, }, - failureExpected: false, - }, { + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.p.Validate(context.Background()) + if err != nil { + t.Errorf("Pipeline.Validate() returned error: %v", err) + } + }) + } +} + +func TestPipeline_Validate_Failure(t *testing.T) { + tests := []struct { + name string + p *Pipeline + }{{ name: "period in name", - p: &v1beta1.Pipeline{ + p: &Pipeline{ ObjectMeta: metav1.ObjectMeta{Name: "pipe.line"}, - Spec: v1beta1.PipelineSpec{ - Tasks: []v1beta1.PipelineTask{{Name: "foo", TaskRef: &v1beta1.TaskRef{Name: "foo-task"}}}, + Spec: PipelineSpec{ + Tasks: []PipelineTask{{Name: "foo", TaskRef: &TaskRef{Name: "foo-task"}}}, }, }, - failureExpected: true, }, { name: "pipeline name too long", - p: &v1beta1.Pipeline{ + p: &Pipeline{ ObjectMeta: metav1.ObjectMeta{Name: "asdf123456789012345678901234567890123456789012345678901234567890"}, }, - failureExpected: true, }, { name: "pipeline spec missing", - p: &v1beta1.Pipeline{ + p: &Pipeline{ ObjectMeta: metav1.ObjectMeta{Name: "pipeline"}, }, - failureExpected: true, - }, { - name: "pipeline spec missing taskref and taskspec", - p: &v1beta1.Pipeline{ - ObjectMeta: metav1.ObjectMeta{Name: "pipeline"}, - Spec: v1beta1.PipelineSpec{ - Tasks: []v1beta1.PipelineTask{ - {Name: "foo"}, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.p.Validate(context.Background()) + if err == nil { + t.Error("Pipeline.Validate() did not return error, wanted error") + } + }) + } +} + +func TestPipelineSpec_Validate_Failure(t *testing.T) { + tests := []struct { + name string + ps *PipelineSpec + }{{ + name: "invalid pipeline with one pipeline task having taskRef and taskSpec both", + ps: &PipelineSpec{ + Description: "this is an invalid pipeline with invalid pipeline task", + Tasks: []PipelineTask{{ + Name: "valid-pipeline-task", + TaskRef: &TaskRef{Name: "foo-task"}, + }, { + Name: "invalid-pipeline-task", + TaskRef: &TaskRef{Name: "foo-task"}, + TaskSpec: &TaskSpec{ + Steps: []Step{{ + Container: corev1.Container{Name: "foo", Image: "bar"}, + }}, }, - }, + }}, }, - failureExpected: true, }, { - name: "pipeline spec with taskref and taskspec", - p: &v1beta1.Pipeline{ - ObjectMeta: metav1.ObjectMeta{Name: "pipeline"}, - Spec: v1beta1.PipelineSpec{ - Tasks: []v1beta1.PipelineTask{ - { - Name: "foo", - TaskRef: &v1beta1.TaskRef{Name: "foo-task"}, - TaskSpec: &v1beta1.TaskSpec{ - Steps: []v1beta1.Step{{ - Container: corev1.Container{Name: "foo", Image: "bar"}, - }}, - }, - }, + name: "invalid pipeline with pipeline task having reference to resources which does not exist", + ps: &PipelineSpec{ + Resources: []PipelineDeclaredResource{{ + Name: "great-resource", Type: PipelineResourceTypeGit, + }, { + Name: "wonderful-resource", Type: PipelineResourceTypeImage, + }}, + Tasks: []PipelineTask{{ + Name: "bar", + TaskRef: &TaskRef{Name: "bar-task"}, + Resources: &PipelineTaskResources{ + Inputs: []PipelineTaskInputResource{{ + Name: "some-workspace", Resource: "missing-great-resource", + }}, + Outputs: []PipelineTaskOutputResource{{ + Name: "some-imagee", Resource: "missing-wonderful-resource", + }}, }, - }, - }, - failureExpected: true, - }, { - name: "pipeline spec invalid taskspec", - p: &v1beta1.Pipeline{ - ObjectMeta: metav1.ObjectMeta{Name: "pipeline"}, - Spec: v1beta1.PipelineSpec{ - Tasks: []v1beta1.PipelineTask{ - { - Name: "foo", - TaskSpec: &v1beta1.TaskSpec{}, - }, + Conditions: []PipelineTaskCondition{{ + ConditionRef: "some-condition", + Resources: []PipelineTaskInputResource{{ + Name: "some-workspace", Resource: "missing-great-resource", + }}, + }}, + }, { + Name: "foo", + TaskRef: &TaskRef{Name: "foo-task"}, + Resources: &PipelineTaskResources{ + Inputs: []PipelineTaskInputResource{{ + Name: "some-image", Resource: "wonderful-resource", + }}, }, - }, + Conditions: []PipelineTaskCondition{{ + ConditionRef: "some-condition-2", + Resources: []PipelineTaskInputResource{{ + Name: "some-image", Resource: "wonderful-resource", + }}, + }}, + }}, }, - failureExpected: true, }, { - name: "pipeline spec valid taskspec", - p: &v1beta1.Pipeline{ - ObjectMeta: metav1.ObjectMeta{Name: "pipeline"}, - Spec: v1beta1.PipelineSpec{ - Tasks: []v1beta1.PipelineTask{ - { - Name: "foo", - TaskSpec: &v1beta1.TaskSpec{ - Steps: []v1beta1.Step{{ - Container: corev1.Container{Name: "foo", Image: "bar"}, - }}, - }, - }, + name: "invalid pipeline spec - from referring to a pipeline task which does not exist", + ps: &PipelineSpec{ + Tasks: []PipelineTask{{ + Name: "baz", TaskRef: &TaskRef{Name: "baz-task"}, + }, { + Name: "foo", + TaskRef: &TaskRef{Name: "foo-task"}, + Resources: &PipelineTaskResources{ + Inputs: []PipelineTaskInputResource{{ + Name: "the-resource", Resource: "great-resource", From: []string{"bar"}, + }}, }, - }, + }}, }, - failureExpected: false, }, { - name: "pipeline spec invalid (duplicate tasks)", - p: &v1beta1.Pipeline{ - ObjectMeta: metav1.ObjectMeta{Name: "pipeline"}, - Spec: v1beta1.PipelineSpec{ - Tasks: []v1beta1.PipelineTask{ - {Name: "foo", TaskRef: &v1beta1.TaskRef{Name: "foo-task"}}, - {Name: "foo", TaskRef: &v1beta1.TaskRef{Name: "foo-task"}}, - }, - }, + name: "invalid pipeline spec with DAG having cyclic dependency", + ps: &PipelineSpec{ + Tasks: []PipelineTask{{ + Name: "foo", TaskRef: &TaskRef{Name: "foo-task"}, RunAfter: []string{"bar"}, + }, { + Name: "bar", TaskRef: &TaskRef{Name: "bar-task"}, RunAfter: []string{"foo"}, + }}, }, - failureExpected: true, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.ps.Validate(context.Background()) + if err == nil { + t.Error("PipelineSpec.Validate() did not return error, wanted error") + } + }) + } +} + +func TestValidatePipelineTasks_Success(t *testing.T) { + tests := []struct { + name string + tasks []PipelineTask + }{{ + name: "pipeline task with valid taskref name", + tasks: []PipelineTask{{ + Name: "foo", + TaskRef: &TaskRef{Name: "example.com/my-foo-task"}, + }}, }, { - name: "pipeline spec empty task name", - p: &v1beta1.Pipeline{ - ObjectMeta: metav1.ObjectMeta{Name: "pipeline"}, - Spec: v1beta1.PipelineSpec{ - Tasks: []v1beta1.PipelineTask{{Name: "", TaskRef: &v1beta1.TaskRef{Name: "foo-task"}}}, + name: "pipeline task with valid taskspec", + tasks: []PipelineTask{{ + Name: "foo", + TaskSpec: &TaskSpec{ + Steps: []Step{{ + Container: corev1.Container{Name: "foo", Image: "bar"}, + }}, }, - }, - failureExpected: true, + }}, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validatePipelineTasks(context.Background(), tt.tasks) + if err != nil { + t.Errorf("Pipeline.validatePipelineTasks() returned error: %v", err) + } + }) + } +} + +func TestValidatePipelineTasks_Failure(t *testing.T) { + tests := []struct { + name string + tasks []PipelineTask + }{{ + name: "pipeline task missing taskref and taskspec", + tasks: []PipelineTask{{ + Name: "foo", + }}, }, { - name: "pipeline spec invalid task name", - p: &v1beta1.Pipeline{ - ObjectMeta: metav1.ObjectMeta{Name: "pipeline"}, - Spec: v1beta1.PipelineSpec{ - Tasks: []v1beta1.PipelineTask{{Name: "_foo", TaskRef: &v1beta1.TaskRef{Name: "foo-task"}}}, + name: "pipeline task with both taskref and taskspec", + tasks: []PipelineTask{{ + Name: "foo", + TaskRef: &TaskRef{Name: "foo-task"}, + TaskSpec: &TaskSpec{ + Steps: []Step{{ + Container: corev1.Container{Name: "foo", Image: "bar"}, + }}, }, - }, - failureExpected: true, + }}, }, { - name: "pipeline spec invalid task name 2", - p: &v1beta1.Pipeline{ - ObjectMeta: metav1.ObjectMeta{Name: "pipeline"}, - Spec: v1beta1.PipelineSpec{ - Tasks: []v1beta1.PipelineTask{{Name: "fooTask", TaskRef: &v1beta1.TaskRef{Name: "foo-task"}}}, - }, - }, - failureExpected: true, + name: "pipeline task with invalid taskspec", + tasks: []PipelineTask{{ + Name: "foo", + TaskSpec: &TaskSpec{}, + }}, }, { - name: "pipeline spec invalid taskref name", - p: &v1beta1.Pipeline{ - ObjectMeta: metav1.ObjectMeta{Name: "pipeline"}, - Spec: v1beta1.PipelineSpec{ - Tasks: []v1beta1.PipelineTask{{Name: "foo", TaskRef: &v1beta1.TaskRef{Name: "_foo-task"}}}, - }, + name: "pipeline tasks invalid (duplicate tasks)", + tasks: []PipelineTask{ + {Name: "foo", TaskRef: &TaskRef{Name: "foo-task"}}, + {Name: "foo", TaskRef: &TaskRef{Name: "foo-task"}}, }, - failureExpected: true, }, { - name: "valid condition only resource", - p: &v1beta1.Pipeline{ - ObjectMeta: metav1.ObjectMeta{Name: "pipeline"}, - Spec: v1beta1.PipelineSpec{ - Resources: []v1beta1.PipelineDeclaredResource{{ - Name: "great-resource", Type: v1beta1.PipelineResourceTypeGit, - }}, - Tasks: []v1beta1.PipelineTask{{ - Name: "bar", - TaskRef: &v1beta1.TaskRef{Name: "bar-task"}, - Conditions: []v1beta1.PipelineTaskCondition{{ - ConditionRef: "some-condition", - Resources: []v1beta1.PipelineTaskInputResource{{ - Name: "sowe-workspace", Resource: "great-resource", - }}, - }}, - }}, - }, - }, - failureExpected: false, + name: "pipeline task with empty task name", + tasks: []PipelineTask{{Name: "", TaskRef: &TaskRef{Name: "foo-task"}}}, }, { - name: "valid parameter variables", - p: &v1beta1.Pipeline{ - ObjectMeta: metav1.ObjectMeta{Name: "pipeline"}, - Spec: v1beta1.PipelineSpec{ - Params: []v1beta1.ParamSpec{{ - Name: "baz", Type: v1beta1.ParamTypeString, - }, { - Name: "foo-is-baz", Type: v1beta1.ParamTypeString, - }}, - Tasks: []v1beta1.PipelineTask{{ - Name: "bar", - TaskRef: &v1beta1.TaskRef{Name: "bar-task"}, - Params: []v1beta1.Param{{ - Name: "a-param", Value: v1beta1.ArrayOrString{StringVal: "$(baz) and $(foo-is-baz)"}, - }}, - }}, - }, - }, - failureExpected: false, + name: "pipeline task with invalid task name", + tasks: []PipelineTask{{Name: "_foo", TaskRef: &TaskRef{Name: "foo-task"}}}, }, { - name: "valid array parameter variables", - p: &v1beta1.Pipeline{ - ObjectMeta: metav1.ObjectMeta{Name: "pipeline"}, - Spec: v1beta1.PipelineSpec{ - Params: []v1beta1.ParamSpec{{ - Name: "baz", Type: v1beta1.ParamTypeArray, Default: &v1beta1.ArrayOrString{Type: v1beta1.ParamTypeArray, ArrayVal: []string{"some", "default"}}, - }, { - Name: "foo-is-baz", Type: v1beta1.ParamTypeArray, + name: "pipeline task with invalid task name (camel case)", + tasks: []PipelineTask{{Name: "fooTask", TaskRef: &TaskRef{Name: "foo-task"}}}, + }, { + name: "pipeline task with invalid taskref name", + tasks: []PipelineTask{{Name: "foo", TaskRef: &TaskRef{Name: "_foo-task"}}}, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validatePipelineTasks(context.Background(), tt.tasks) + if err == nil { + t.Error("Pipeline.validatePipelineTasks() did not return error, wanted error") + } + }) + } +} + +func TestValidateFrom_Success(t *testing.T) { + tests := []struct { + name string + tasks []PipelineTask + }{{ + name: "valid pipeline task - from resource referring to valid output resource of the pipeline task", + tasks: []PipelineTask{{ + Name: "bar", + TaskRef: &TaskRef{Name: "bar-task"}, + Resources: &PipelineTaskResources{ + Inputs: []PipelineTaskInputResource{{ + Name: "some-resource", Resource: "some-resource", }}, - Tasks: []v1beta1.PipelineTask{{ - Name: "bar", - TaskRef: &v1beta1.TaskRef{Name: "bar-task"}, - Params: []v1beta1.Param{{ - Name: "a-param", Value: v1beta1.ArrayOrString{ArrayVal: []string{"$(baz)", "and", "$(foo-is-baz)"}}, - }}, + Outputs: []PipelineTaskOutputResource{{ + Name: "output-resource", Resource: "output-resource", }}, }, - }, - failureExpected: false, - }, { - name: "valid star array parameter variables", - p: &v1beta1.Pipeline{ - ObjectMeta: metav1.ObjectMeta{Name: "pipeline"}, - Spec: v1beta1.PipelineSpec{ - Params: []v1beta1.ParamSpec{{ - Name: "baz", Type: v1beta1.ParamTypeArray, Default: &v1beta1.ArrayOrString{Type: v1beta1.ParamTypeArray, ArrayVal: []string{"some", "default"}}, - }, { - Name: "foo-is-baz", Type: v1beta1.ParamTypeArray, - }}, - Tasks: []v1beta1.PipelineTask{{ - Name: "bar", - TaskRef: &v1beta1.TaskRef{Name: "bar-task"}, - Params: []v1beta1.Param{{ - Name: "a-param", Value: v1beta1.ArrayOrString{ArrayVal: []string{"$(baz[*])", "and", "$(foo-is-baz[*])"}}, - }}, + }, { + Name: "foo", + TaskRef: &TaskRef{Name: "foo-task"}, + Resources: &PipelineTaskResources{ + Inputs: []PipelineTaskInputResource{{ + Name: "wow-image", Resource: "output-resource", From: []string{"bar"}, }}, }, - }, - failureExpected: false, - }, { - name: "pipeline parameter nested in task parameter", - p: &v1beta1.Pipeline{ - ObjectMeta: metav1.ObjectMeta{Name: "pipeline"}, - Spec: v1beta1.PipelineSpec{ - Params: []v1beta1.ParamSpec{{ - Name: "baz", Type: v1beta1.ParamTypeString, - }}, - Tasks: []v1beta1.PipelineTask{{ - Name: "bar", - TaskRef: &v1beta1.TaskRef{Name: "bar-task"}, - Params: []v1beta1.Param{{ - Name: "a-param", Value: v1beta1.ArrayOrString{StringVal: "$(input.workspace.$(baz))"}, - }}, + }}, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateFrom(tt.tasks) + if err != nil { + t.Errorf("Pipeline.validateFrom() returned error: %v", err) + } + }) + } +} + +func TestValidateFrom_Failure(t *testing.T) { + tests := []struct { + name string + tasks []PipelineTask + }{{ + name: "invalid pipeline task - from in a pipeline with single pipeline task", + tasks: []PipelineTask{{ + Name: "foo", + TaskRef: &TaskRef{Name: "foo-task"}, + Resources: &PipelineTaskResources{ + Inputs: []PipelineTaskInputResource{{ + Name: "the-resource", Resource: "great-resource", From: []string{"bar"}, }}, }, }, - failureExpected: false, + }, }, { - name: "from is on first task", - p: &v1beta1.Pipeline{ - ObjectMeta: metav1.ObjectMeta{Name: "pipeline"}, - Spec: v1beta1.PipelineSpec{ - Resources: []v1beta1.PipelineDeclaredResource{{ - Name: "great-resource", Type: v1beta1.PipelineResourceTypeGit, - }}, - Tasks: []v1beta1.PipelineTask{{ - Name: "foo", - TaskRef: &v1beta1.TaskRef{Name: "foo-task"}, - Resources: &v1beta1.PipelineTaskResources{ - Inputs: []v1beta1.PipelineTaskInputResource{{ - Name: "the-resource", Resource: "great-resource", From: []string{"bar"}, - }}, - }, + name: "invalid pipeline task - from referencing pipeline task which does not exist", + tasks: []PipelineTask{{ + Name: "baz", TaskRef: &TaskRef{Name: "baz-task"}, + }, { + Name: "foo", + TaskRef: &TaskRef{Name: "foo-task"}, + Resources: &PipelineTaskResources{ + Inputs: []PipelineTaskInputResource{{ + Name: "the-resource", Resource: "great-resource", From: []string{"bar"}, }}, }, - }, - failureExpected: true, + }}, }, { - name: "from task doesnt exist", - p: &v1beta1.Pipeline{ - ObjectMeta: metav1.ObjectMeta{Name: "pipeline"}, - Spec: v1beta1.PipelineSpec{ - Resources: []v1beta1.PipelineDeclaredResource{{ - Name: "great-resource", Type: v1beta1.PipelineResourceTypeGit, + name: "invalid pipeline task - pipeline task condition resource does not exist", + tasks: []PipelineTask{{ + Name: "foo", TaskRef: &TaskRef{Name: "foo-task"}, + }, { + Name: "bar", + TaskRef: &TaskRef{Name: "bar-task"}, + Conditions: []PipelineTaskCondition{{ + ConditionRef: "some-condition", + Resources: []PipelineTaskInputResource{{ + Name: "some-workspace", Resource: "missing-resource", From: []string{"foo"}, }}, - Tasks: []v1beta1.PipelineTask{{ - Name: "baz", TaskRef: &v1beta1.TaskRef{Name: "baz-task"}, - }, { - Name: "foo", - TaskRef: &v1beta1.TaskRef{Name: "foo-task"}, - Resources: &v1beta1.PipelineTaskResources{ - Inputs: []v1beta1.PipelineTaskInputResource{{ - Name: "the-resource", Resource: "great-resource", From: []string{"bar"}, - }}, - }, + }}, + }}, + }, { + name: "invalid pipeline task - from resource referring to a pipeline task which has no output", + tasks: []PipelineTask{{ + Name: "bar", + TaskRef: &TaskRef{Name: "bar-task"}, + Resources: &PipelineTaskResources{ + Inputs: []PipelineTaskInputResource{{ + Name: "some-resource", Resource: "great-resource", }}, }, - }, - failureExpected: true, - }, { - name: "duplicate resource declaration", - p: &v1beta1.Pipeline{ - ObjectMeta: metav1.ObjectMeta{Name: "pipeline"}, - Spec: v1beta1.PipelineSpec{ - Resources: []v1beta1.PipelineDeclaredResource{{ - Name: "duplicate-resource", Type: v1beta1.PipelineResourceTypeGit, - }, { - Name: "duplicate-resource", Type: v1beta1.PipelineResourceTypeGit, - }}, - Tasks: []v1beta1.PipelineTask{{ - Name: "foo", - TaskRef: &v1beta1.TaskRef{Name: "foo-task"}, - Resources: &v1beta1.PipelineTaskResources{ - Inputs: []v1beta1.PipelineTaskInputResource{{ - Name: "the-resource", Resource: "duplicate-resource", - }}, - }, + }, { + Name: "foo", + TaskRef: &TaskRef{Name: "foo-task"}, + Resources: &PipelineTaskResources{ + Inputs: []PipelineTaskInputResource{{ + Name: "wow-image", Resource: "wonderful-resource", From: []string{"bar"}, }}, }, - }, - failureExpected: true, + }}, }, { - name: "output resources missing from declaration", - p: &v1beta1.Pipeline{ - ObjectMeta: metav1.ObjectMeta{Name: "pipeline"}, - Spec: v1beta1.PipelineSpec{ - Resources: []v1beta1.PipelineDeclaredResource{{ - Name: "great-resource", Type: v1beta1.PipelineResourceTypeGit, - }}, - Tasks: []v1beta1.PipelineTask{{ - Name: "foo", - TaskRef: &v1beta1.TaskRef{Name: "foo-task"}, - Resources: &v1beta1.PipelineTaskResources{ - Inputs: []v1beta1.PipelineTaskInputResource{{ - Name: "the-resource", Resource: "great-resource", - }}, - Outputs: []v1beta1.PipelineTaskOutputResource{{ - Name: "the-magic-resource", Resource: "missing-resource", - }}, - }, + name: "invalid pipeline task - from resource referring to input resource of the pipeline task instead of output", + tasks: []PipelineTask{{ + Name: "bar", + TaskRef: &TaskRef{Name: "bar-task"}, + Resources: &PipelineTaskResources{ + Inputs: []PipelineTaskInputResource{{ + Name: "some-resource", Resource: "great-resource", + }}, + Outputs: []PipelineTaskOutputResource{{ + Name: "output-resource", Resource: "great-output-resource", }}, }, - }, - failureExpected: true, - }, { - name: "input resources missing from declaration", - p: &v1beta1.Pipeline{ - ObjectMeta: metav1.ObjectMeta{Name: "pipeline"}, - Spec: v1beta1.PipelineSpec{ - Resources: []v1beta1.PipelineDeclaredResource{{ - Name: "great-resource", Type: v1beta1.PipelineResourceTypeGit, - }}, - Tasks: []v1beta1.PipelineTask{{ - Name: "foo", - TaskRef: &v1beta1.TaskRef{Name: "foo-task"}, - Resources: &v1beta1.PipelineTaskResources{ - Inputs: []v1beta1.PipelineTaskInputResource{{ - Name: "the-resource", Resource: "missing-resource", - }}, - Outputs: []v1beta1.PipelineTaskOutputResource{{ - Name: "the-magic-resource", Resource: "great-resource", - }}, - }, + }, { + Name: "foo", + TaskRef: &TaskRef{Name: "foo-task"}, + Resources: &PipelineTaskResources{ + Inputs: []PipelineTaskInputResource{{ + Name: "wow-image", Resource: "some-resource", From: []string{"bar"}, }}, }, - }, - failureExpected: true, - }, { - name: "invalid condition only resource", - p: &v1beta1.Pipeline{ - ObjectMeta: metav1.ObjectMeta{Name: "pipeline"}, - Spec: v1beta1.PipelineSpec{ - Tasks: []v1beta1.PipelineTask{{ - Name: "bar", - TaskRef: &v1beta1.TaskRef{Name: "bar-task"}, - Conditions: []v1beta1.PipelineTaskCondition{{ - ConditionRef: "some-condition", - Resources: []v1beta1.PipelineTaskInputResource{{ - Name: "sowe-workspace", Resource: "missing-resource", - }}, - }}, + }}, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateFrom(tt.tasks) + if err == nil { + t.Error("Pipeline.validateFrom() did not return error, wanted error") + } + }) + } +} + +func TestValidateDeclaredResources_Success(t *testing.T) { + tests := []struct { + name string + resources []PipelineDeclaredResource + tasks []PipelineTask + }{{ + name: "valid resource declarations and usage", + resources: []PipelineDeclaredResource{{ + Name: "great-resource", Type: PipelineResourceTypeGit, + }, { + Name: "wonderful-resource", Type: PipelineResourceTypeImage, + }}, + tasks: []PipelineTask{{ + Name: "bar", + TaskRef: &TaskRef{Name: "bar-task"}, + Resources: &PipelineTaskResources{ + Inputs: []PipelineTaskInputResource{{ + Name: "some-workspace", Resource: "great-resource", + }}, + Outputs: []PipelineTaskOutputResource{{ + Name: "some-imagee", Resource: "wonderful-resource", }}, }, - }, - failureExpected: true, - }, { - name: "invalid from in condition", - p: &v1beta1.Pipeline{ - ObjectMeta: metav1.ObjectMeta{Name: "pipeline"}, - Spec: v1beta1.PipelineSpec{ - Tasks: []v1beta1.PipelineTask{{ - Name: "foo", TaskRef: &v1beta1.TaskRef{Name: "foo-task"}, - }, { - Name: "bar", - TaskRef: &v1beta1.TaskRef{Name: "bar-task"}, - Conditions: []v1beta1.PipelineTaskCondition{{ - ConditionRef: "some-condition", - Resources: []v1beta1.PipelineTaskInputResource{{ - Name: "sowe-workspace", Resource: "missing-resource", From: []string{"foo"}, - }}, - }}, + Conditions: []PipelineTaskCondition{{ + ConditionRef: "some-condition", + Resources: []PipelineTaskInputResource{{ + Name: "some-workspace", Resource: "great-resource", + }}, + }}, + }, { + Name: "foo", + TaskRef: &TaskRef{Name: "foo-task"}, + Resources: &PipelineTaskResources{ + Inputs: []PipelineTaskInputResource{{ + Name: "some-image", Resource: "wonderful-resource", From: []string{"bar"}, }}, }, - }, - failureExpected: true, + Conditions: []PipelineTaskCondition{{ + ConditionRef: "some-condition-2", + Resources: []PipelineTaskInputResource{{ + Name: "some-image", Resource: "wonderful-resource", From: []string{"bar"}, + }}, + }}, + }}, }, { - name: "from resource isn't output by task", - p: &v1beta1.Pipeline{ - ObjectMeta: metav1.ObjectMeta{Name: "pipeline"}, - Spec: v1beta1.PipelineSpec{ - Resources: []v1beta1.PipelineDeclaredResource{{ - Name: "great-resource", Type: v1beta1.PipelineResourceTypeGit, - }, { - Name: "wonderful-resource", Type: v1beta1.PipelineResourceTypeImage, - }}, - Tasks: []v1beta1.PipelineTask{{ - Name: "bar", - TaskRef: &v1beta1.TaskRef{Name: "bar-task"}, - Resources: &v1beta1.PipelineTaskResources{ - Inputs: []v1beta1.PipelineTaskInputResource{{ - Name: "some-resource", Resource: "great-resource", - }}, - }, - }, { - Name: "foo", - TaskRef: &v1beta1.TaskRef{Name: "foo-task"}, - Resources: &v1beta1.PipelineTaskResources{ - Inputs: []v1beta1.PipelineTaskInputResource{{ - Name: "wow-image", Resource: "wonderful-resource", From: []string{"bar"}, - }}, - }, + name: "valid resource declaration with single reference in the pipeline task condition", + resources: []PipelineDeclaredResource{{ + Name: "great-resource", Type: PipelineResourceTypeGit, + }}, + tasks: []PipelineTask{{ + Name: "bar", + TaskRef: &TaskRef{Name: "bar-task"}, + Conditions: []PipelineTaskCondition{{ + ConditionRef: "some-condition", + Resources: []PipelineTaskInputResource{{ + Name: "some-workspace", Resource: "great-resource", }}, - }, - }, - failureExpected: true, + }}, + }}, }, { - name: "not defined parameter variable", - p: &v1beta1.Pipeline{ - ObjectMeta: metav1.ObjectMeta{Name: "pipeline"}, - Spec: v1beta1.PipelineSpec{ - Tasks: []v1beta1.PipelineTask{{ - Name: "foo", - TaskRef: &v1beta1.TaskRef{Name: "foo-task"}, - Params: []v1beta1.Param{{ - Name: "a-param", Value: v1beta1.ArrayOrString{Type: v1beta1.ParamTypeString, StringVal: "$(params.does-not-exist)"}, - }}, + name: "valid resource declarations with extra resources, not used in any pipeline task", + resources: []PipelineDeclaredResource{{ + Name: "great-resource", Type: PipelineResourceTypeGit, + }, { + Name: "awesome-resource", Type: PipelineResourceTypeImage, + }, { + Name: "yet-another-great-resource", Type: PipelineResourceTypeGit, + }, { + Name: "yet-another-awesome-resource", Type: PipelineResourceTypeImage, + }}, + tasks: []PipelineTask{{ + Name: "foo", + TaskRef: &TaskRef{Name: "foo-task"}, + Resources: &PipelineTaskResources{ + Inputs: []PipelineTaskInputResource{{ + Name: "the-resource", Resource: "great-resource", + }}, + Outputs: []PipelineTaskOutputResource{{ + Name: "the-awesome-resource", Resource: "awesome-resource", }}, }, - }, - failureExpected: true, - }, { - name: "not defined parameter variable with defined", - p: &v1beta1.Pipeline{ - ObjectMeta: metav1.ObjectMeta{Name: "pipeline"}, - Spec: v1beta1.PipelineSpec{ - Params: []v1beta1.ParamSpec{{ - Name: "foo", Type: v1beta1.ParamTypeString, - }}, - Tasks: []v1beta1.PipelineTask{{ - Name: "foo", - TaskRef: &v1beta1.TaskRef{Name: "foo-task"}, - Params: []v1beta1.Param{{ - Name: "a-param", Value: v1beta1.ArrayOrString{Type: v1beta1.ParamTypeString, StringVal: "$(params.foo) and $(params.does-not-exist)"}, - }}, + }}, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateDeclaredResources(tt.resources, tt.tasks) + if err != nil { + t.Errorf("Pipeline.validateDeclaredResources() returned error: %v", err) + } + }) + } +} + +func TestValidateDeclaredResources_Failure(t *testing.T) { + tests := []struct { + name string + resources []PipelineDeclaredResource + tasks []PipelineTask + }{{ + name: "duplicate resource declaration - resource declarations must be unique", + resources: []PipelineDeclaredResource{{ + Name: "duplicate-resource", Type: PipelineResourceTypeGit, + }, { + Name: "duplicate-resource", Type: PipelineResourceTypeGit, + }}, + tasks: []PipelineTask{{ + Name: "foo", + TaskRef: &TaskRef{Name: "foo-task"}, + Resources: &PipelineTaskResources{ + Inputs: []PipelineTaskInputResource{{ + Name: "the-resource", Resource: "duplicate-resource", }}, }, - }, - failureExpected: true, + }}, }, { - name: "invalid parameter type", - p: &v1beta1.Pipeline{ - ObjectMeta: metav1.ObjectMeta{Name: "pipeline"}, - Spec: v1beta1.PipelineSpec{ - Params: []v1beta1.ParamSpec{{ - Name: "foo", Type: "invalidtype", + name: "output resource is missing from resource declarations", + resources: []PipelineDeclaredResource{{ + Name: "great-resource", Type: PipelineResourceTypeGit, + }}, + tasks: []PipelineTask{{ + Name: "foo", + TaskRef: &TaskRef{Name: "foo-task"}, + Resources: &PipelineTaskResources{ + Inputs: []PipelineTaskInputResource{{ + Name: "the-resource", Resource: "great-resource", }}, - Tasks: []v1beta1.PipelineTask{{ - Name: "foo", - TaskRef: &v1beta1.TaskRef{Name: "foo-task"}, + Outputs: []PipelineTaskOutputResource{{ + Name: "the-magic-resource", Resource: "missing-resource", }}, }, - }, - failureExpected: true, + }}, }, { - name: "array parameter mismatching default type", - p: &v1beta1.Pipeline{ - ObjectMeta: metav1.ObjectMeta{Name: "pipeline"}, - Spec: v1beta1.PipelineSpec{ - Params: []v1beta1.ParamSpec{{ - Name: "foo", Type: v1beta1.ParamTypeArray, Default: &v1beta1.ArrayOrString{Type: v1beta1.ParamTypeString, StringVal: "astring"}, + name: "input resource is missing from resource declarations", + resources: []PipelineDeclaredResource{{ + Name: "great-resource", Type: PipelineResourceTypeGit, + }}, + tasks: []PipelineTask{{ + Name: "foo", + TaskRef: &TaskRef{Name: "foo-task"}, + Resources: &PipelineTaskResources{ + Inputs: []PipelineTaskInputResource{{ + Name: "the-resource", Resource: "missing-resource", }}, - Tasks: []v1beta1.PipelineTask{{ - Name: "foo", - TaskRef: &v1beta1.TaskRef{Name: "foo-task"}, + Outputs: []PipelineTaskOutputResource{{ + Name: "the-magic-resource", Resource: "great-resource", }}, }, - }, - failureExpected: true, + }}, }, { - name: "string parameter mismatching default type", - p: &v1beta1.Pipeline{ - ObjectMeta: metav1.ObjectMeta{Name: "pipeline"}, - Spec: v1beta1.PipelineSpec{ - Params: []v1beta1.ParamSpec{{ - Name: "foo", Type: v1beta1.ParamTypeString, Default: &v1beta1.ArrayOrString{Type: v1beta1.ParamTypeArray, ArrayVal: []string{"anarray", "elements"}}, + name: "invalid condition only resource -" + + " pipeline task condition referring to a resource which is missing from resource declarations", + tasks: []PipelineTask{{ + Name: "bar", + TaskRef: &TaskRef{Name: "bar-task"}, + Conditions: []PipelineTaskCondition{{ + ConditionRef: "some-condition", + Resources: []PipelineTaskInputResource{{ + Name: "some-workspace", Resource: "missing-resource", }}, - Tasks: []v1beta1.PipelineTask{{ - Name: "foo", - TaskRef: &v1beta1.TaskRef{Name: "foo-task"}, + }}, + }}, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateDeclaredResources(tt.resources, tt.tasks) + if err == nil { + t.Error("Pipeline.validateDeclaredResources() did not return error, wanted error") + } + }) + } +} + +func TestValidateGraph_Success(t *testing.T) { + tests := []struct { + name string + tasks []PipelineTask + }{{ + name: "valid dependency graph with multiple tasks", + tasks: []PipelineTask{{ + Name: "foo", TaskRef: &TaskRef{Name: "foo-task"}, + }, { + Name: "bar", TaskRef: &TaskRef{Name: "bar-task"}, + }, { + Name: "foo1", TaskRef: &TaskRef{Name: "foo-task"}, RunAfter: []string{"foo"}, + }, { + Name: "bar1", TaskRef: &TaskRef{Name: "bar-task"}, RunAfter: []string{"bar"}, + }, { + Name: "foo-bar", TaskRef: &TaskRef{Name: "bar-task"}, RunAfter: []string{"foo1", "bar1"}, + }}, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateGraph(tt.tasks) + if err != nil { + t.Errorf("Pipeline.validateGraph() returned error: %v", err) + } + }) + } +} + +func TestValidateGraph_Failure(t *testing.T) { + tests := []struct { + name string + tasks []PipelineTask + }{{ + name: "invalid dependency graph between the tasks with cyclic dependency", + tasks: []PipelineTask{{ + Name: "foo", TaskRef: &TaskRef{Name: "foo-task"}, RunAfter: []string{"bar"}, + }, { + Name: "bar", TaskRef: &TaskRef{Name: "bar-task"}, RunAfter: []string{"foo"}, + }}, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateGraph(tt.tasks) + if err == nil { + t.Error("Pipeline.validateGraph() did not return error, wanted error") + } + }) + } +} + +func TestValidateParamResults_Success(t *testing.T) { + tests := []struct { + name string + tasks []PipelineTask + }{{ + name: "invalid pipeline task referencing task result along with parameter variable", + tasks: []PipelineTask{{ + TaskSpec: &TaskSpec{ + Results: []TaskResult{{ + Name: "output", + }}, + Steps: []Step{{ + Container: corev1.Container{Name: "foo", Image: "bar"}, }}, }, - }, - failureExpected: true, + Name: "a-task", + }, { + Name: "foo", + TaskRef: &TaskRef{Name: "foo-task"}, + Params: []Param{{ + Name: "a-param", Value: ArrayOrString{Type: ParamTypeString, StringVal: "$(params.foo) and $(tasks.a-task.results.output)"}, + }}, + }}, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateParamResults(tt.tasks) + if err != nil { + t.Errorf("Pipeline.validateParamResults() returned error: %v", err) + } + }) + } +} + +func TestValidateParamResults_Failure(t *testing.T) { + tests := []struct { + name string + tasks []PipelineTask + }{{ + name: "invalid pipeline task referencing task results with malformed variable substitution expression", + tasks: []PipelineTask{{ + Name: "a-task", TaskRef: &TaskRef{Name: "a-task"}, + }, { + Name: "b-task", TaskRef: &TaskRef{Name: "b-task"}, + Params: []Param{{ + Name: "a-param", Value: ArrayOrString{Type: ParamTypeString, StringVal: "$(tasks.a-task.resultTypo.bResult)"}}}, + }}, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateParamResults(tt.tasks) + if err == nil { + t.Error("Pipeline.validateParamResults() did not return error, wanted error") + } + }) + } +} + +func TestValidatePipelineResults_Success(t *testing.T) { + tests := []struct { + name string + results []PipelineResult + }{{ + name: "valid pipeline result", + results: []PipelineResult{{ + Name: "my-pipeline-result", + Description: "this is my pipeline result", + Value: "$(tasks.a-task.results.output)", + }}, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validatePipelineResults(tt.results) + if err != nil { + t.Errorf("Pipeline.validatePipelineResults() returned error: %v", err) + } + }) + } +} + +func TestValidatePipelineResults_Failure(t *testing.T) { + tests := []struct { + name string + results []PipelineResult + }{{ + name: "invalid pipeline result reference", + results: []PipelineResult{{ + Name: "my-pipeline-result", + Description: "this is my pipeline result", + Value: "$(tasks.a-task.results.output.output)", + }}, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validatePipelineResults(tt.results) + if err == nil { + t.Error("Pipeline.validatePipelineResults() did not return error, wanted error") + } + }) + } +} + +func TestValidatePipelineParameterVariables_Success(t *testing.T) { + tests := []struct { + name string + params []ParamSpec + tasks []PipelineTask + }{{ + name: "valid string parameter variables", + params: []ParamSpec{{ + Name: "baz", Type: ParamTypeString, + }, { + Name: "foo-is-baz", Type: ParamTypeString, + }}, + tasks: []PipelineTask{{ + Name: "bar", + TaskRef: &TaskRef{Name: "bar-task"}, + Params: []Param{{ + Name: "a-param", Value: ArrayOrString{StringVal: "$(baz) and $(foo-is-baz)"}, + }}, + }}, + }, { + name: "valid array parameter variables", + params: []ParamSpec{{ + Name: "baz", Type: ParamTypeArray, Default: &ArrayOrString{Type: ParamTypeArray, ArrayVal: []string{"some", "default"}}, + }, { + Name: "foo-is-baz", Type: ParamTypeArray, + }}, + tasks: []PipelineTask{{ + Name: "bar", + TaskRef: &TaskRef{Name: "bar-task"}, + Params: []Param{{ + Name: "a-param", Value: ArrayOrString{ArrayVal: []string{"$(baz)", "and", "$(foo-is-baz)"}}, + }}, + }}, + }, { + name: "valid star array parameter variables", + params: []ParamSpec{{ + Name: "baz", Type: ParamTypeArray, Default: &ArrayOrString{Type: ParamTypeArray, ArrayVal: []string{"some", "default"}}, + }, { + Name: "foo-is-baz", Type: ParamTypeArray, + }}, + tasks: []PipelineTask{{ + Name: "bar", + TaskRef: &TaskRef{Name: "bar-task"}, + Params: []Param{{ + Name: "a-param", Value: ArrayOrString{ArrayVal: []string{"$(baz[*])", "and", "$(foo-is-baz[*])"}}, + }}, + }}, + }, { + name: "pipeline parameter nested in task parameter", + params: []ParamSpec{{ + Name: "baz", Type: ParamTypeString, + }}, + tasks: []PipelineTask{{ + Name: "bar", + TaskRef: &TaskRef{Name: "bar-task"}, + Params: []Param{{ + Name: "a-param", Value: ArrayOrString{StringVal: "$(input.workspace.$(baz))"}, + }}, + }}, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validatePipelineParameterVariables(tt.tasks, tt.params) + if err != nil { + t.Errorf("Pipeline.validatePipelineParameterVariables() returned error: %v", err) + } + }) + } +} + +func TestValidatePipelineParameterVariables_Failure(t *testing.T) { + tests := []struct { + name string + params []ParamSpec + tasks []PipelineTask + }{{ + name: "invalid pipeline task with a parameter which is missing from the param declarations", + tasks: []PipelineTask{{ + Name: "foo", + TaskRef: &TaskRef{Name: "foo-task"}, + Params: []Param{{ + Name: "a-param", Value: ArrayOrString{Type: ParamTypeString, StringVal: "$(params.does-not-exist)"}, + }}, + }}, + }, { + name: "invalid pipeline task with a parameter combined with missing param from the param declarations", + params: []ParamSpec{{ + Name: "foo", Type: ParamTypeString, + }}, + tasks: []PipelineTask{{ + Name: "foo-task", + TaskRef: &TaskRef{Name: "foo-task"}, + Params: []Param{{ + Name: "a-param", Value: ArrayOrString{Type: ParamTypeString, StringVal: "$(params.foo) and $(params.does-not-exist)"}, + }}, + }}, + }, { + name: "invalid pipeline task with two parameters and one of them missing from the param declarations", + params: []ParamSpec{{ + Name: "foo", Type: ParamTypeString, + }}, + tasks: []PipelineTask{{ + Name: "foo-task", + TaskRef: &TaskRef{Name: "foo-task"}, + Params: []Param{{ + Name: "a-param", Value: ArrayOrString{Type: ParamTypeString, StringVal: "$(params.foo)"}, + }, { + Name: "b-param", Value: ArrayOrString{Type: ParamTypeString, StringVal: "$(params.does-not-exist)"}, + }}, + }}, + }, { + name: "invalid parameter type", + params: []ParamSpec{{ + Name: "foo", Type: "invalidtype", + }}, + tasks: []PipelineTask{{ + Name: "foo", + TaskRef: &TaskRef{Name: "foo-task"}, + }}, + }, { + name: "array parameter mismatching default type", + params: []ParamSpec{{ + Name: "foo", Type: ParamTypeArray, Default: &ArrayOrString{Type: ParamTypeString, StringVal: "astring"}, + }}, + tasks: []PipelineTask{{ + Name: "foo", + TaskRef: &TaskRef{Name: "foo-task"}, + }}, + }, { + name: "string parameter mismatching default type", + params: []ParamSpec{{ + Name: "foo", Type: ParamTypeString, Default: &ArrayOrString{Type: ParamTypeArray, ArrayVal: []string{"anarray", "elements"}}, + }}, + tasks: []PipelineTask{{ + Name: "foo", + TaskRef: &TaskRef{Name: "foo-task"}, + }}, }, { name: "array parameter used as string", - p: &v1beta1.Pipeline{ - ObjectMeta: metav1.ObjectMeta{Name: "pipeline"}, - Spec: v1beta1.PipelineSpec{ - Params: []v1beta1.ParamSpec{{ - Name: "baz", Type: v1beta1.ParamTypeString, Default: &v1beta1.ArrayOrString{Type: v1beta1.ParamTypeArray, ArrayVal: []string{"anarray", "elements"}}, - }}, - Tasks: []v1beta1.PipelineTask{{ - Name: "bar", - TaskRef: &v1beta1.TaskRef{Name: "bar-task"}, - Params: []v1beta1.Param{{ - Name: "a-param", Value: v1beta1.ArrayOrString{Type: v1beta1.ParamTypeString, StringVal: "$(params.baz)"}, - }}, - }}, - }, - }, - failureExpected: true, + params: []ParamSpec{{ + Name: "baz", Type: ParamTypeString, Default: &ArrayOrString{Type: ParamTypeArray, ArrayVal: []string{"anarray", "elements"}}, + }}, + tasks: []PipelineTask{{ + Name: "bar", + TaskRef: &TaskRef{Name: "bar-task"}, + Params: []Param{{ + Name: "a-param", Value: ArrayOrString{Type: ParamTypeString, StringVal: "$(params.baz)"}, + }}, + }}, }, { name: "star array parameter used as string", - p: &v1beta1.Pipeline{ - ObjectMeta: metav1.ObjectMeta{Name: "pipeline"}, - Spec: v1beta1.PipelineSpec{ - Params: []v1beta1.ParamSpec{{ - Name: "baz", Type: v1beta1.ParamTypeString, Default: &v1beta1.ArrayOrString{Type: v1beta1.ParamTypeArray, ArrayVal: []string{"anarray", "elements"}}, - }}, - Tasks: []v1beta1.PipelineTask{{ - Name: "bar", - TaskRef: &v1beta1.TaskRef{Name: "bar-task"}, - Params: []v1beta1.Param{{ - Name: "a-param", Value: v1beta1.ArrayOrString{Type: v1beta1.ParamTypeString, StringVal: "$(params.baz[*])"}, - }}, - }}, - }, - }, - failureExpected: true, + params: []ParamSpec{{ + Name: "baz", Type: ParamTypeString, Default: &ArrayOrString{Type: ParamTypeArray, ArrayVal: []string{"anarray", "elements"}}, + }}, + tasks: []PipelineTask{{ + Name: "bar", + TaskRef: &TaskRef{Name: "bar-task"}, + Params: []Param{{ + Name: "a-param", Value: ArrayOrString{Type: ParamTypeString, StringVal: "$(params.baz[*])"}, + }}, + }}, }, { name: "array parameter string template not isolated", - p: &v1beta1.Pipeline{ - ObjectMeta: metav1.ObjectMeta{Name: "pipeline"}, - Spec: v1beta1.PipelineSpec{ - Params: []v1beta1.ParamSpec{{ - Name: "baz", Type: v1beta1.ParamTypeString, Default: &v1beta1.ArrayOrString{Type: v1beta1.ParamTypeArray, ArrayVal: []string{"anarray", "elements"}}, - }}, - Tasks: []v1beta1.PipelineTask{{ - Name: "bar", - TaskRef: &v1beta1.TaskRef{Name: "bar-task"}, - Params: []v1beta1.Param{{ - Name: "a-param", Value: v1beta1.ArrayOrString{Type: v1beta1.ParamTypeArray, ArrayVal: []string{"value: $(params.baz)", "last"}}, - }}, - }}, - }, - }, - failureExpected: true, + params: []ParamSpec{{ + Name: "baz", Type: ParamTypeString, Default: &ArrayOrString{Type: ParamTypeArray, ArrayVal: []string{"anarray", "elements"}}, + }}, + tasks: []PipelineTask{{ + Name: "bar", + TaskRef: &TaskRef{Name: "bar-task"}, + Params: []Param{{ + Name: "a-param", Value: ArrayOrString{Type: ParamTypeArray, ArrayVal: []string{"value: $(params.baz)", "last"}}, + }}, + }}, }, { name: "star array parameter string template not isolated", - p: &v1beta1.Pipeline{ - ObjectMeta: metav1.ObjectMeta{Name: "pipeline"}, - Spec: v1beta1.PipelineSpec{ - Params: []v1beta1.ParamSpec{{ - Name: "baz", Type: v1beta1.ParamTypeString, Default: &v1beta1.ArrayOrString{Type: v1beta1.ParamTypeArray, ArrayVal: []string{"anarray", "elements"}}, - }}, - Tasks: []v1beta1.PipelineTask{{ - Name: "bar", - TaskRef: &v1beta1.TaskRef{Name: "bar-task"}, - Params: []v1beta1.Param{{ - Name: "a-param", Value: v1beta1.ArrayOrString{Type: v1beta1.ParamTypeArray, ArrayVal: []string{"value: $(params.baz[*])", "last"}}, - }}, - }}, - }, - }, - failureExpected: true, - }, { - name: "invalid dependency graph between the tasks", - p: &v1beta1.Pipeline{ - ObjectMeta: metav1.ObjectMeta{Name: "pipeline"}, - Spec: v1beta1.PipelineSpec{ - Tasks: []v1beta1.PipelineTask{{ - Name: "foo", TaskRef: &v1beta1.TaskRef{Name: "foo-task"}, RunAfter: []string{"bar"}, - }, { - Name: "bar", TaskRef: &v1beta1.TaskRef{Name: "bar-task"}, RunAfter: []string{"foo"}, - }}, - }, - }, - failureExpected: true, - }, { + params: []ParamSpec{{ + Name: "baz", Type: ParamTypeString, Default: &ArrayOrString{Type: ParamTypeArray, ArrayVal: []string{"anarray", "elements"}}, + }}, + tasks: []PipelineTask{{ + Name: "bar", + TaskRef: &TaskRef{Name: "bar-task"}, + Params: []Param{{ + Name: "a-param", Value: ArrayOrString{Type: ParamTypeArray, ArrayVal: []string{"value: $(params.baz[*])", "last"}}, + }}, + }}, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validatePipelineParameterVariables(tt.tasks, tt.params) + if err == nil { + t.Error("Pipeline.validatePipelineParameterVariables() did not return error, wanted error") + } + }) + } +} + +func TestValidatePipelineWorkspaces_Success(t *testing.T) { + tests := []struct { + name string + workspaces []WorkspacePipelineDeclaration + tasks []PipelineTask + }{{ name: "unused pipeline spec workspaces do not cause an error", - p: &v1beta1.Pipeline{ - ObjectMeta: metav1.ObjectMeta{Name: "pipeline"}, - Spec: v1beta1.PipelineSpec{ - Tasks: []v1beta1.PipelineTask{{ - Name: "foo", TaskRef: &v1beta1.TaskRef{Name: "foo"}, - }}, - Workspaces: []v1beta1.WorkspacePipelineDeclaration{{ - Name: "foo", - }, { - Name: "bar", - }}, - }, - }, - failureExpected: false, - }, { + workspaces: []WorkspacePipelineDeclaration{{ + Name: "foo", + }, { + Name: "bar", + }}, + tasks: []PipelineTask{{ + Name: "foo", TaskRef: &TaskRef{Name: "foo"}, + }}, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validatePipelineWorkspaces(tt.workspaces, tt.tasks) + if err != nil { + t.Errorf("Pipeline.validatePipelineWorkspaces() returned error: %v", err) + } + }) + } +} + +func TestValidatePipelineWorkspaces_Failure(t *testing.T) { + tests := []struct { + name string + workspaces []WorkspacePipelineDeclaration + tasks []PipelineTask + }{{ name: "workspace bindings relying on a non-existent pipeline workspace cause an error", - p: &v1beta1.Pipeline{ - ObjectMeta: metav1.ObjectMeta{Name: "pipeline"}, - Spec: v1beta1.PipelineSpec{ - Tasks: []v1beta1.PipelineTask{{ - Name: "foo", TaskRef: &v1beta1.TaskRef{Name: "foo"}, - Workspaces: []v1beta1.WorkspacePipelineTaskBinding{{ - Name: "taskWorkspaceName", - Workspace: "pipelineWorkspaceName", - }}, - }}, - Workspaces: []v1beta1.WorkspacePipelineDeclaration{{ - Name: "foo", - }}, - }, - }, - failureExpected: true, + workspaces: []WorkspacePipelineDeclaration{{ + Name: "foo", + }}, + tasks: []PipelineTask{{ + Name: "foo", TaskRef: &TaskRef{Name: "foo"}, + Workspaces: []WorkspacePipelineTaskBinding{{ + Name: "taskWorkspaceName", + Workspace: "pipelineWorkspaceName", + }}, + }}, }, { name: "multiple workspaces sharing the same name are not allowed", - p: &v1beta1.Pipeline{ - ObjectMeta: metav1.ObjectMeta{Name: "pipeline"}, Spec: v1beta1.PipelineSpec{ - Workspaces: []v1beta1.WorkspacePipelineDeclaration{{ - Name: "foo", - }, { - Name: "foo", - }}, - }, - }, - failureExpected: true, + workspaces: []WorkspacePipelineDeclaration{{ + Name: "foo", + }, { + Name: "foo", + }}, + tasks: []PipelineTask{{ + Name: "foo", TaskRef: &TaskRef{Name: "foo"}, + }}, }, { - name: "task params results malformed variable substitution expression", - p: &v1beta1.Pipeline{ - ObjectMeta: metav1.ObjectMeta{Name: "name"}, Spec: v1beta1.PipelineSpec{ - Tasks: []v1beta1.PipelineTask{{ - Name: "a-task", TaskRef: &v1beta1.TaskRef{Name: "a-task"}, - }, { - Name: "b-task", TaskRef: &v1beta1.TaskRef{Name: "b-task"}, - Params: []v1beta1.Param{{ - Name: "a-param", Value: v1beta1.ArrayOrString{Type: v1beta1.ParamTypeString, StringVal: "$(tasks.a-task.resultTypo.bResult)"}}}, - }}, - }, - }, - failureExpected: true, - }, { - name: "not defined parameter variable with defined", - p: &v1beta1.Pipeline{ - ObjectMeta: metav1.ObjectMeta{Name: "pipeline"}, - Spec: v1beta1.PipelineSpec{ - Params: []v1beta1.ParamSpec{{ - Name: "foo", Type: v1beta1.ParamTypeString, - }}, - Tasks: []v1beta1.PipelineTask{{ - TaskSpec: &v1beta1.TaskSpec{ - Results: []v1beta1.TaskResult{{ - Name: "output", - }}, - Steps: []v1beta1.Step{{ - Container: corev1.Container{Name: "foo", Image: "bar"}, - }}, - }, - Name: "a-task", - }, { - Name: "foo", - TaskRef: &v1beta1.TaskRef{Name: "foo-task"}, - Params: []v1beta1.Param{{ - Name: "a-param", Value: v1beta1.ArrayOrString{Type: v1beta1.ParamTypeString, StringVal: "$(params.foo) and $(tasks.a-task.results.output)"}, - }}, - }}, - }, - }, - failureExpected: false, + name: "workspace name must not be empty", + workspaces: []WorkspacePipelineDeclaration{{ + Name: "", + }}, + tasks: []PipelineTask{{ + Name: "foo", TaskRef: &TaskRef{Name: "foo"}, + }}, }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := tt.p.Validate(context.Background()) - if (!tt.failureExpected) && (err != nil) { - t.Errorf("Pipeline.Validate() returned error: %v", err) - } - - if tt.failureExpected && (err == nil) { - t.Error("Pipeline.Validate() did not return error, wanted error") + err := validatePipelineWorkspaces(tt.workspaces, tt.tasks) + if err == nil { + t.Error("Pipeline.validatePipelineWorkspaces() did not return error, wanted error") } }) }