Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Consolidate validation for Task/Pipeline beta features #6719

Merged
merged 1 commit into from
May 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 0 additions & 12 deletions pkg/apis/pipeline/v1/pipeline_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,18 +276,6 @@ func TestPipelineTask_ValidateRegularTask_Failure(t *testing.T) {
Message: `invalid value: taskRef must specify name`,
Paths: []string{"taskRef.name"},
},
}, {
name: "pipeline task - use of resolver without the feature flag set",
task: PipelineTask{
TaskRef: &TaskRef{Name: "boo", ResolverRef: ResolverRef{Resolver: "bar"}},
},
expectedError: *apis.ErrDisallowedFields("taskref.resolver"),
}, {
name: "pipeline task - use of resolver params without the feature flag set",
task: PipelineTask{
TaskRef: &TaskRef{Name: "boo", ResolverRef: ResolverRef{Params: Params{{}}}},
},
expectedError: *apis.ErrDisallowedFields("taskref.params"),
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down
80 changes: 55 additions & 25 deletions pkg/apis/pipeline/v1/pipeline_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ func (ps *PipelineSpec) Validate(ctx context.Context) (errs *apis.FieldError) {
errs = errs.Also(validatePipelineContextVariables(ps.Tasks).ViaField("tasks"))
errs = errs.Also(validatePipelineContextVariables(ps.Finally).ViaField("finally"))
errs = errs.Also(validateExecutionStatusVariables(ps.Tasks, ps.Finally))
errs = errs.Also(ps.ValidateBetaFeaturesEnabledForParamArrayIndexing(ctx))
errs = errs.Also(ps.validateBetaFields(ctx))
// Validate the pipeline's workspaces.
errs = errs.Also(validatePipelineWorkspacesDeclarations(ps.Workspaces))
// Validate the pipeline's results
Expand All @@ -84,6 +84,60 @@ func (ps *PipelineSpec) Validate(ctx context.Context) (errs *apis.FieldError) {
return errs
}

// validateBetaFields returns an error if the Pipeline spec uses beta features but does not
// have "enable-api-fields" set to "alpha" or "beta".
func (ps *PipelineSpec) validateBetaFields(ctx context.Context) *apis.FieldError {
var errs *apis.FieldError
// Object parameters
for i, p := range ps.Params {
if p.Type == ParamTypeObject {
errs = errs.Also(version.ValidateEnabledAPIFields(ctx, "object type parameter", config.BetaAPIFields).ViaFieldIndex("params", i))
}
}
// Indexing into array parameters
arrayParamIndexingRefs := ps.GetIndexingReferencesToArrayParams()
if len(arrayParamIndexingRefs) != 0 && !config.CheckAlphaOrBetaAPIFields(ctx) {
errs = errs.Also(apis.ErrGeneric(fmt.Sprintf("cannot index into array parameters when 'enable-api-fields' is 'stable', but found indexing references: %s", arrayParamIndexingRefs)))
}
// array and object results
for i, result := range ps.Results {
switch result.Type {
case ResultsTypeObject:
errs = errs.Also(version.ValidateEnabledAPIFields(ctx, "object results", config.BetaAPIFields).ViaFieldIndex("results", i))
case ResultsTypeArray:
errs = errs.Also(version.ValidateEnabledAPIFields(ctx, "array results", config.BetaAPIFields).ViaFieldIndex("results", i))
case ResultsTypeString:
default:
}
}
for i, pt := range ps.Tasks {
errs = errs.Also(pt.validateBetaFields(ctx).ViaFieldIndex("tasks", i))
}
for i, pt := range ps.Finally {
errs = errs.Also(pt.validateBetaFields(ctx).ViaFieldIndex("tasks", i))
}

return errs
}

// validateBetaFields returns an error if the PipelineTask uses beta features but does not
// have "enable-api-fields" set to "alpha" or "beta".
func (pt *PipelineTask) validateBetaFields(ctx context.Context) *apis.FieldError {
var errs *apis.FieldError
if pt.TaskRef != nil {
// Resolvers
if pt.TaskRef.Resolver != "" {
errs = errs.Also(version.ValidateEnabledAPIFields(ctx, "taskref.resolver", config.BetaAPIFields))
}
if len(pt.TaskRef.Params) > 0 {
errs = errs.Also(version.ValidateEnabledAPIFields(ctx, "taskref.params", config.BetaAPIFields))
}
} else if pt.TaskSpec != nil {
errs = errs.Also(pt.TaskSpec.validateBetaFields(ctx))
}
return errs
}

// 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, finalTasks []PipelineTask) *apis.FieldError {
Expand Down Expand Up @@ -263,7 +317,6 @@ func (pt PipelineTask) validateCustomTask() (errs *apis.FieldError) {

// validateTask validates a pipeline task or a final task for taskRef and taskSpec
func (pt PipelineTask) validateTask(ctx context.Context) (errs *apis.FieldError) {
cfg := config.FromContextOrDefaults(ctx)
// Validate TaskSpec if it's present
if pt.TaskSpec != nil {
errs = errs.Also(pt.TaskSpec.Validate(ctx).ViaField("taskSpec"))
Expand All @@ -277,15 +330,6 @@ func (pt PipelineTask) validateTask(ctx context.Context) (errs *apis.FieldError)
} else if pt.TaskRef.Resolver == "" {
errs = errs.Also(apis.ErrInvalidValue("taskRef must specify name", "taskRef.name"))
}
if cfg.FeatureFlags.EnableAPIFields != config.BetaAPIFields && cfg.FeatureFlags.EnableAPIFields != config.AlphaAPIFields {
// fail if resolver or resource are present when enable-api-fields is false.
if pt.TaskRef.Resolver != "" {
errs = errs.Also(apis.ErrDisallowedFields("taskref.resolver"))
}
if len(pt.TaskRef.Params) > 0 {
errs = errs.Also(apis.ErrDisallowedFields("taskref.params"))
}
}
}
return errs
}
Expand Down Expand Up @@ -724,20 +768,6 @@ func (ps *PipelineSpec) ValidateParamArrayIndex(ctx context.Context, params Para
return validateOutofBoundArrayParams(arrayIndexParamRefs, arrayParamsLengths)
}

// ValidateBetaFeaturesEnabledForParamArrayIndexing validates that "enable-api-fields" is set to "alpha" or "beta" if the pipeline spec
// contains indexing references to array params.
// This can be removed when array param indexing is moved to "stable".
func (ps *PipelineSpec) ValidateBetaFeaturesEnabledForParamArrayIndexing(ctx context.Context) (errs *apis.FieldError) {
if config.CheckAlphaOrBetaAPIFields(ctx) {
return nil
}
arrayParamIndexingRefs := ps.GetIndexingReferencesToArrayParams()
if len(arrayParamIndexingRefs) == 0 {
return nil
}
return apis.ErrGeneric(fmt.Sprintf("cannot index into array parameters when 'enable-api-fields' is 'stable', but found indexing references: %s", arrayParamIndexingRefs))
}

// GetIndexingReferencesToArrayParams returns all strings referencing indices of PipelineRun array parameters
// from parameters, workspaces, and when expressions defined in the Pipeline's Tasks and Finally Tasks.
// For example, if a Task in the Pipeline has a parameter with a value "$(params.array-param-name[1])",
Expand Down
172 changes: 169 additions & 3 deletions pkg/apis/pipeline/v1/pipeline_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3522,7 +3522,7 @@ func TestPipelineWithBetaFields(t *testing.T) {
name string
spec PipelineSpec
}{{
name: "array indexing",
name: "array indexing in Tasks",
spec: PipelineSpec{
Params: []ParamSpec{
{Name: "first-param", Type: ParamTypeArray, Default: NewStructuredValues("default-value", "default-value-again")},
Expand All @@ -3535,15 +3535,181 @@ func TestPipelineWithBetaFields(t *testing.T) {
TaskRef: &TaskRef{Name: "foo"},
}},
},
}, {
name: "array indexing in Finally",
spec: PipelineSpec{
Params: []ParamSpec{
{Name: "first-param", Type: ParamTypeArray, Default: NewStructuredValues("default-value", "default-value-again")},
},
Tasks: []PipelineTask{{
Name: "foo",
TaskRef: &TaskRef{Name: "foo"},
}},
Finally: []PipelineTask{{
Name: "bar",
Params: Params{
{Name: "first-task-first-param", Value: *NewStructuredValues("$(params.first-param[0])")},
},
TaskRef: &TaskRef{Name: "bar"},
}},
},
}, {
name: "pipeline tasks - use of resolver without the feature flag set",
spec: PipelineSpec{
Tasks: []PipelineTask{{
Name: "uses-resolver",
TaskRef: &TaskRef{Name: "boo", ResolverRef: ResolverRef{Resolver: "bar"}},
}},
},
}, {
name: "pipeline tasks - use of resolver params without the feature flag set",
spec: PipelineSpec{
Tasks: []PipelineTask{{
Name: "uses-resolver-params",
TaskRef: &TaskRef{Name: "boo", ResolverRef: ResolverRef{Params: Params{{}}}},
}},
},
}, {
name: "finally tasks - use of resolver without the feature flag set",
spec: PipelineSpec{
Tasks: []PipelineTask{{
Name: "valid-pipeline-task",
TaskRef: &TaskRef{Name: "foo-task"},
}},
Finally: []PipelineTask{{
Name: "uses-resolver",
TaskRef: &TaskRef{Name: "boo", ResolverRef: ResolverRef{Resolver: "bar"}},
}},
},
}, {
name: "finally tasks - use of resolver params without the feature flag set",
spec: PipelineSpec{
Tasks: []PipelineTask{{
Name: "valid-pipeline-task",
TaskRef: &TaskRef{Name: "foo-task"},
}},
Finally: []PipelineTask{{
Name: "uses-resolver-params",
TaskRef: &TaskRef{Name: "boo", ResolverRef: ResolverRef{Params: Params{{}}}},
}},
},
}, {
name: "object params",
spec: PipelineSpec{
Params: []ParamSpec{
{Name: "first-param", Type: ParamTypeObject, Properties: map[string]PropertySpec{}},
},
Tasks: []PipelineTask{{
Name: "foo",
TaskRef: &TaskRef{Name: "foo"},
}},
},
}, {
name: "object params in Tasks",
spec: PipelineSpec{
Tasks: []PipelineTask{{
Name: "valid-pipeline-task",
TaskSpec: &EmbeddedTask{TaskSpec: TaskSpec{
Steps: []Step{{Image: "busybox", Script: "echo hello"}},
Params: []ParamSpec{{Name: "my-object-param", Type: ParamTypeObject, Properties: map[string]PropertySpec{}}},
}},
}},
},
}, {
name: "object params in Finally",
spec: PipelineSpec{
Tasks: []PipelineTask{{
Name: "foo",
TaskRef: &TaskRef{Name: "foo"},
}},
Finally: []PipelineTask{{
Name: "valid-finally-task",
TaskSpec: &EmbeddedTask{TaskSpec: TaskSpec{
Steps: []Step{{Image: "busybox", Script: "echo hello"}},
Params: []ParamSpec{{Name: "my-object-param", Type: ParamTypeObject, Properties: map[string]PropertySpec{}}},
}},
}},
},
}, {
name: "array results",
spec: PipelineSpec{
Tasks: []PipelineTask{{
Name: "valid-pipeline-task",
TaskRef: &TaskRef{Name: "foo-task"},
}},
Results: []PipelineResult{{Name: "my-array-result", Type: ResultsTypeArray, Value: *NewStructuredValues("$(tasks.valid-pipeline-task.results.foo[*])")}},
},
}, {
name: "array results in Tasks",
spec: PipelineSpec{
Tasks: []PipelineTask{{
Name: "valid-pipeline-task",
TaskSpec: &EmbeddedTask{TaskSpec: TaskSpec{
Steps: []Step{{Image: "busybox", Script: "echo hello"}},
Results: []TaskResult{{Name: "my-array-result", Type: ResultsTypeArray}},
}},
}},
},
}, {
name: "array results in Finally",
spec: PipelineSpec{
Tasks: []PipelineTask{{
Name: "valid-pipeline-task",
TaskRef: &TaskRef{Name: "foo-task"},
}},
Finally: []PipelineTask{{
Name: "valid-finally-task",
TaskSpec: &EmbeddedTask{TaskSpec: TaskSpec{
Steps: []Step{{Image: "busybox", Script: "echo hello"}},
Results: []TaskResult{{Name: "my-array-result", Type: ResultsTypeArray}},
}},
}},
},
}, {
name: "object results",
spec: PipelineSpec{
Tasks: []PipelineTask{{
Name: "valid-pipeline-task",
TaskRef: &TaskRef{Name: "foo-task"},
}},
Results: []PipelineResult{{Name: "my-object-result", Type: ResultsTypeObject, Value: *NewStructuredValues("$(tasks.valid-pipeline-task.results.foo[*])")}},
},
}, {
name: "object results in Tasks",
spec: PipelineSpec{
Tasks: []PipelineTask{{
Name: "valid-pipeline-task",
TaskSpec: &EmbeddedTask{TaskSpec: TaskSpec{
Steps: []Step{{Image: "busybox", Script: "echo hello"}},
Results: []TaskResult{{Name: "my-object-result", Type: ResultsTypeObject, Properties: map[string]PropertySpec{}}},
}},
}},
},
}, {
name: "object results in Finally",
spec: PipelineSpec{
Tasks: []PipelineTask{{
Name: "valid-pipeline-task",
TaskRef: &TaskRef{Name: "foo-task"},
}},
Finally: []PipelineTask{{
Name: "valid-finally-task",
TaskSpec: &EmbeddedTask{TaskSpec: TaskSpec{
Steps: []Step{{Image: "busybox", Script: "echo hello"}},
Results: []TaskResult{{Name: "my-object-result", Type: ResultsTypeObject, Properties: map[string]PropertySpec{}}},
}},
}},
},
}}
for _, tt := range tts {
t.Run(tt.name, func(t *testing.T) {
if err := tt.spec.Validate(context.Background()); err == nil {
pipeline := Pipeline{ObjectMeta: metav1.ObjectMeta{Name: "foo"}, Spec: tt.spec}
if err := pipeline.Validate(context.Background()); err == nil {
t.Errorf("no error when using beta field when `enable-api-fields` is stable")
}

ctx := config.EnableBetaAPIFields(context.Background())
if err := tt.spec.Validate(ctx); err != nil {
if err := pipeline.Validate(ctx); err != nil {
t.Errorf("unexpected error when using beta field: %s", err)
}
})
Expand Down
8 changes: 1 addition & 7 deletions pkg/apis/pipeline/v1/result_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ import (
"fmt"
"regexp"

"github.com/tektoncd/pipeline/pkg/apis/config"
"github.com/tektoncd/pipeline/pkg/apis/version"
"knative.dev/pkg/apis"
)

Expand All @@ -35,15 +33,11 @@ func (tr TaskResult) Validate(ctx context.Context) (errs *apis.FieldError) {
}

switch {
// Object results is beta feature - check if the feature flag is set to "beta" or "alpha"
case tr.Type == ResultsTypeObject:
errs := validateObjectResult(tr)
errs = errs.Also(version.ValidateEnabledAPIFields(ctx, "results type", config.BetaAPIFields))
return errs
// Array results is a beta feature - check if the feature flag is set to "beta" or "alpha"
case tr.Type == ResultsTypeArray:
errs = errs.Also(version.ValidateEnabledAPIFields(ctx, "results type", config.BetaAPIFields))
return errs
return nil
// Resources created before the result. Type was introduced may not have Type set
// and should be considered valid
case tr.Type == "":
Expand Down
Loading