Skip to content

Commit e40bda6

Browse files
committed
fix: validate taskRef.apiVersion format for custom tasks
Add validation to ensure taskRef.apiVersion follows the correct format (group/version) when specified. Previously, invalid apiVersion values like 'invalid-api-version' were accepted, causing PipelineRuns to create unhandled CustomRuns that would timeout. Now Pipeline creation fails immediately with a clear error message when an invalid apiVersion is provided. Fixes #9044
1 parent d97e81a commit e40bda6

File tree

4 files changed

+217
-7
lines changed

4 files changed

+217
-7
lines changed

pkg/apis/pipeline/v1/pipeline_types_test.go

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,46 @@ func TestPipelineTask_ValidateCustomTask(t *testing.T) {
483483
Message: `invalid value: custom task ref must specify apiVersion`,
484484
Paths: []string{"taskRef.apiVersion"},
485485
},
486+
}, {
487+
name: "custom task - taskRef with invalid apiVersion format",
488+
task: PipelineTask{Name: "foo", TaskRef: &TaskRef{APIVersion: "invalid-api-version", Kind: "some-kind", Name: ""}},
489+
expectedError: apis.FieldError{
490+
Message: `invalid value: invalid apiVersion format "invalid-api-version", must be in the format "group/version"`,
491+
Paths: []string{"taskRef.apiVersion"},
492+
},
493+
}, {
494+
name: "custom task - taskSpec with invalid apiVersion format",
495+
task: PipelineTask{Name: "foo", TaskSpec: &EmbeddedTask{
496+
TypeMeta: runtime.TypeMeta{
497+
APIVersion: "no-slash-no-dot",
498+
Kind: "some-kind",
499+
},
500+
}},
501+
expectedError: apis.FieldError{
502+
Message: `invalid value: invalid apiVersion format "no-slash-no-dot", must be in the format "group/version"`,
503+
Paths: []string{"taskSpec.apiVersion"},
504+
},
505+
}, {
506+
name: "custom task - taskRef with missing group in apiVersion",
507+
task: PipelineTask{Name: "foo", TaskRef: &TaskRef{APIVersion: "nogroup/v1", Kind: "some-kind", Name: ""}},
508+
expectedError: apis.FieldError{
509+
Message: `invalid value: invalid apiVersion format "nogroup/v1", must be in the format "group/version"`,
510+
Paths: []string{"taskRef.apiVersion"},
511+
},
512+
}, {
513+
name: "custom task - taskRef with empty group in apiVersion",
514+
task: PipelineTask{Name: "foo", TaskRef: &TaskRef{APIVersion: "/v1", Kind: "some-kind", Name: ""}},
515+
expectedError: apis.FieldError{
516+
Message: `invalid value: invalid apiVersion format "/v1", must be in the format "group/version"`,
517+
Paths: []string{"taskRef.apiVersion"},
518+
},
519+
}, {
520+
name: "custom task - taskRef with empty version in apiVersion",
521+
task: PipelineTask{Name: "foo", TaskRef: &TaskRef{APIVersion: "example.dev/", Kind: "some-kind", Name: ""}},
522+
expectedError: apis.FieldError{
523+
Message: `invalid value: invalid apiVersion format "example.dev/", must be in the format "group/version"`,
524+
Paths: []string{"taskRef.apiVersion"},
525+
},
486526
}}
487527
for _, tt := range tests {
488528
t.Run(tt.name, func(t *testing.T) {
@@ -497,6 +537,35 @@ func TestPipelineTask_ValidateCustomTask(t *testing.T) {
497537
}
498538
}
499539

540+
func TestPipelineTask_ValidateCustomTask_ValidAPIVersion(t *testing.T) {
541+
tests := []struct {
542+
name string
543+
task PipelineTask
544+
}{{
545+
name: "custom task - valid apiVersion with group/version",
546+
task: PipelineTask{Name: "foo", TaskRef: &TaskRef{APIVersion: "example.dev/v1", Kind: "Example", Name: "example"}},
547+
}, {
548+
name: "custom task - valid apiVersion with multi-level group",
549+
task: PipelineTask{Name: "foo", TaskRef: &TaskRef{APIVersion: "custom.tekton.dev/v1beta1", Kind: "Custom", Name: "custom"}},
550+
}, {
551+
name: "custom task - valid apiVersion in taskSpec",
552+
task: PipelineTask{Name: "foo", TaskSpec: &EmbeddedTask{
553+
TypeMeta: runtime.TypeMeta{
554+
APIVersion: "example.io/v2",
555+
Kind: "CustomTask",
556+
},
557+
}},
558+
}}
559+
for _, tt := range tests {
560+
t.Run(tt.name, func(t *testing.T) {
561+
err := tt.task.validateCustomTask()
562+
if err != nil {
563+
t.Errorf("PipelineTask.validateCustomTask() returned unexpected error: %v", err)
564+
}
565+
})
566+
}
567+
}
568+
500569
func TestPipelineTask_ValidateRegularTask_Success(t *testing.T) {
501570
tests := []struct {
502571
name string
@@ -691,7 +760,7 @@ func TestPipelineTask_Validate_Failure(t *testing.T) {
691760
name: "custom task reference in taskref missing apiversion Kind",
692761
p: PipelineTask{
693762
Name: "invalid-custom-task",
694-
TaskRef: &TaskRef{APIVersion: "example.com"},
763+
TaskRef: &TaskRef{APIVersion: "example.com/v1"},
695764
},
696765
expectedError: apis.FieldError{
697766
Message: `invalid value: custom task ref must specify kind`,
@@ -701,7 +770,7 @@ func TestPipelineTask_Validate_Failure(t *testing.T) {
701770
name: "custom task reference in taskspec missing kind",
702771
p: PipelineTask{Name: "foo", TaskSpec: &EmbeddedTask{
703772
TypeMeta: runtime.TypeMeta{
704-
APIVersion: "example.com",
773+
APIVersion: "example.com/v1",
705774
}}},
706775
expectedError: *apis.ErrInvalidValue("custom task spec must specify kind", "taskSpec.kind"),
707776
}, {

pkg/apis/pipeline/v1/pipeline_validation.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,31 @@ func (pt PipelineTask) validateRefOrSpec(ctx context.Context) (errs *apis.FieldE
336336
return errs
337337
}
338338

339+
// isValidAPIVersion validates the format of an apiVersion string.
340+
// Valid formats are "group/version" where group contains at least one dot.
341+
// For custom tasks, apiVersion must always be in the "group/version" format.
342+
func isValidAPIVersion(apiVersion string) bool {
343+
parts := strings.Split(apiVersion, "/")
344+
// For custom tasks, apiVersion must be in group/version format (2 parts)
345+
if len(parts) != 2 {
346+
return false
347+
}
348+
349+
group := parts[0]
350+
version := parts[1]
351+
// Group and version should not be empty
352+
if group == "" || version == "" {
353+
return false
354+
}
355+
// Group should contain at least one dot (e.g., tekton.dev)
356+
// This is a common pattern for Kubernetes API groups
357+
if !strings.Contains(group, ".") {
358+
return false
359+
}
360+
361+
return true
362+
}
363+
339364
// validateCustomTask validates custom task specifications - checking kind and fail if not yet supported features specified
340365
func (pt PipelineTask) validateCustomTask() (errs *apis.FieldError) {
341366
if pt.TaskRef != nil && pt.TaskRef.Kind == "" {
@@ -350,6 +375,17 @@ func (pt PipelineTask) validateCustomTask() (errs *apis.FieldError) {
350375
if pt.TaskSpec != nil && pt.TaskSpec.APIVersion == "" {
351376
errs = errs.Also(apis.ErrInvalidValue("custom task spec must specify apiVersion", "taskSpec.apiVersion"))
352377
}
378+
// Validate apiVersion format for custom tasks
379+
if pt.TaskRef != nil && pt.TaskRef.APIVersion != "" {
380+
if !isValidAPIVersion(pt.TaskRef.APIVersion) {
381+
errs = errs.Also(apis.ErrInvalidValue(fmt.Sprintf("invalid apiVersion format %q, must be in the format \"group/version\"", pt.TaskRef.APIVersion), "taskRef.apiVersion"))
382+
}
383+
}
384+
if pt.TaskSpec != nil && pt.TaskSpec.APIVersion != "" {
385+
if !isValidAPIVersion(pt.TaskSpec.APIVersion) {
386+
errs = errs.Also(apis.ErrInvalidValue(fmt.Sprintf("invalid apiVersion format %q, must be in the format \"group/version\"", pt.TaskSpec.APIVersion), "taskSpec.apiVersion"))
387+
}
388+
}
353389
return errs
354390
}
355391

pkg/apis/pipeline/v1beta1/pipeline_types_test.go

Lines changed: 74 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,46 @@ func TestPipelineTask_ValidateCustomTask(t *testing.T) {
483483
Message: `invalid value: custom task ref must specify apiVersion`,
484484
Paths: []string{"taskRef.apiVersion"},
485485
},
486+
}, {
487+
name: "custom task - taskRef with invalid apiVersion format",
488+
task: PipelineTask{Name: "foo", TaskRef: &TaskRef{APIVersion: "invalid-api-version", Kind: "some-kind", Name: ""}},
489+
expectedError: apis.FieldError{
490+
Message: `invalid value: invalid apiVersion format "invalid-api-version", must be in the format "group/version"`,
491+
Paths: []string{"taskRef.apiVersion"},
492+
},
493+
}, {
494+
name: "custom task - taskSpec with invalid apiVersion format",
495+
task: PipelineTask{Name: "foo", TaskSpec: &EmbeddedTask{
496+
TypeMeta: runtime.TypeMeta{
497+
APIVersion: "no-slash-no-dot",
498+
Kind: "some-kind",
499+
},
500+
}},
501+
expectedError: apis.FieldError{
502+
Message: `invalid value: invalid apiVersion format "no-slash-no-dot", must be in the format "group/version"`,
503+
Paths: []string{"taskSpec.apiVersion"},
504+
},
505+
}, {
506+
name: "custom task - taskRef with missing group in apiVersion",
507+
task: PipelineTask{Name: "foo", TaskRef: &TaskRef{APIVersion: "nogroup/v1", Kind: "some-kind", Name: ""}},
508+
expectedError: apis.FieldError{
509+
Message: `invalid value: invalid apiVersion format "nogroup/v1", must be in the format "group/version"`,
510+
Paths: []string{"taskRef.apiVersion"},
511+
},
512+
}, {
513+
name: "custom task - taskRef with empty group in apiVersion",
514+
task: PipelineTask{Name: "foo", TaskRef: &TaskRef{APIVersion: "/v1", Kind: "some-kind", Name: ""}},
515+
expectedError: apis.FieldError{
516+
Message: `invalid value: invalid apiVersion format "/v1", must be in the format "group/version"`,
517+
Paths: []string{"taskRef.apiVersion"},
518+
},
519+
}, {
520+
name: "custom task - taskRef with empty version in apiVersion",
521+
task: PipelineTask{Name: "foo", TaskRef: &TaskRef{APIVersion: "example.dev/", Kind: "some-kind", Name: ""}},
522+
expectedError: apis.FieldError{
523+
Message: `invalid value: invalid apiVersion format "example.dev/", must be in the format "group/version"`,
524+
Paths: []string{"taskRef.apiVersion"},
525+
},
486526
}}
487527
for _, tt := range tests {
488528
t.Run(tt.name, func(t *testing.T) {
@@ -497,6 +537,35 @@ func TestPipelineTask_ValidateCustomTask(t *testing.T) {
497537
}
498538
}
499539

540+
func TestPipelineTask_ValidateCustomTask_ValidAPIVersion(t *testing.T) {
541+
tests := []struct {
542+
name string
543+
task PipelineTask
544+
}{{
545+
name: "custom task - valid apiVersion with group/version",
546+
task: PipelineTask{Name: "foo", TaskRef: &TaskRef{APIVersion: "example.dev/v1", Kind: "Example", Name: "example"}},
547+
}, {
548+
name: "custom task - valid apiVersion with multi-level group",
549+
task: PipelineTask{Name: "foo", TaskRef: &TaskRef{APIVersion: "custom.tekton.dev/v1beta1", Kind: "Custom", Name: "custom"}},
550+
}, {
551+
name: "custom task - valid apiVersion in taskSpec",
552+
task: PipelineTask{Name: "foo", TaskSpec: &EmbeddedTask{
553+
TypeMeta: runtime.TypeMeta{
554+
APIVersion: "example.io/v2",
555+
Kind: "CustomTask",
556+
},
557+
}},
558+
}}
559+
for _, tt := range tests {
560+
t.Run(tt.name, func(t *testing.T) {
561+
err := tt.task.validateCustomTask()
562+
if err != nil {
563+
t.Errorf("PipelineTask.validateCustomTask() returned unexpected error: %v", err)
564+
}
565+
})
566+
}
567+
}
568+
500569
func TestPipelineTask_ValidateRegularTask_Success(t *testing.T) {
501570
tests := []struct {
502571
name string
@@ -687,7 +756,7 @@ func TestPipelineTask_Validate_Failure(t *testing.T) {
687756
name: "custom task reference in taskref missing apiversion Kind",
688757
p: PipelineTask{
689758
Name: "invalid-custom-task",
690-
TaskRef: &TaskRef{APIVersion: "example.com"},
759+
TaskRef: &TaskRef{APIVersion: "example.com/v1"},
691760
},
692761
expectedError: apis.FieldError{
693762
Message: `invalid value: custom task ref must specify kind`,
@@ -697,7 +766,7 @@ func TestPipelineTask_Validate_Failure(t *testing.T) {
697766
name: "custom task reference in taskspec missing kind",
698767
p: PipelineTask{Name: "foo", TaskSpec: &EmbeddedTask{
699768
TypeMeta: runtime.TypeMeta{
700-
APIVersion: "example.com",
769+
APIVersion: "example.com/v1",
701770
},
702771
}},
703772
expectedError: *apis.ErrInvalidValue("custom task spec must specify kind", "taskSpec.kind"),
@@ -846,7 +915,7 @@ func TestPipelineTaskList_Validate(t *testing.T) {
846915
name: "validate all valid custom task, and regular task",
847916
tasks: PipelineTaskList{{
848917
Name: "valid-custom-task",
849-
TaskRef: &TaskRef{APIVersion: "example.com", Kind: "custom"},
918+
TaskRef: &TaskRef{APIVersion: "example.com/v1", Kind: "custom"},
850919
}, {
851920
Name: "valid-task",
852921
TaskRef: &TaskRef{Name: "task"},
@@ -856,7 +925,7 @@ func TestPipelineTaskList_Validate(t *testing.T) {
856925
name: "validate list of tasks with valid custom task and invalid regular task",
857926
tasks: PipelineTaskList{{
858927
Name: "valid-custom-task",
859-
TaskRef: &TaskRef{APIVersion: "example.com", Kind: "custom"},
928+
TaskRef: &TaskRef{APIVersion: "example.com/v1", Kind: "custom"},
860929
}, {
861930
Name: "invalid-task-without-name",
862931
TaskRef: &TaskRef{Name: ""},
@@ -867,7 +936,7 @@ func TestPipelineTaskList_Validate(t *testing.T) {
867936
name: "validate all invalid tasks - custom task and regular task",
868937
tasks: PipelineTaskList{{
869938
Name: "invalid-custom-task",
870-
TaskRef: &TaskRef{APIVersion: "example.com"},
939+
TaskRef: &TaskRef{APIVersion: "example.com/v1"},
871940
}, {
872941
Name: "invalid-task",
873942
TaskRef: &TaskRef{Name: ""},

pkg/apis/pipeline/v1beta1/pipeline_validation.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,31 @@ func (pt PipelineTask) validateRefOrSpec(ctx context.Context) (errs *apis.FieldE
341341
return errs
342342
}
343343

344+
// isValidAPIVersion validates the format of an apiVersion string.
345+
// Valid formats are "group/version" where group contains at least one dot.
346+
// For custom tasks, apiVersion must always be in the "group/version" format.
347+
func isValidAPIVersion(apiVersion string) bool {
348+
parts := strings.Split(apiVersion, "/")
349+
// For custom tasks, apiVersion must be in group/version format (2 parts)
350+
if len(parts) != 2 {
351+
return false
352+
}
353+
354+
group := parts[0]
355+
version := parts[1]
356+
// Group and version should not be empty
357+
if group == "" || version == "" {
358+
return false
359+
}
360+
// Group should contain at least one dot (e.g., tekton.dev)
361+
// This is a common pattern for Kubernetes API groups
362+
if !strings.Contains(group, ".") {
363+
return false
364+
}
365+
366+
return true
367+
}
368+
344369
// validateCustomTask validates custom task specifications - checking kind and fail if not yet supported features specified
345370
func (pt PipelineTask) validateCustomTask() (errs *apis.FieldError) {
346371
if pt.TaskRef != nil && pt.TaskRef.Kind == "" {
@@ -355,6 +380,17 @@ func (pt PipelineTask) validateCustomTask() (errs *apis.FieldError) {
355380
if pt.TaskSpec != nil && pt.TaskSpec.APIVersion == "" {
356381
errs = errs.Also(apis.ErrInvalidValue("custom task spec must specify apiVersion", "taskSpec.apiVersion"))
357382
}
383+
// Validate apiVersion format for custom tasks
384+
if pt.TaskRef != nil && pt.TaskRef.APIVersion != "" {
385+
if !isValidAPIVersion(pt.TaskRef.APIVersion) {
386+
errs = errs.Also(apis.ErrInvalidValue(fmt.Sprintf("invalid apiVersion format %q, must be in the format \"group/version\"", pt.TaskRef.APIVersion), "taskRef.apiVersion"))
387+
}
388+
}
389+
if pt.TaskSpec != nil && pt.TaskSpec.APIVersion != "" {
390+
if !isValidAPIVersion(pt.TaskSpec.APIVersion) {
391+
errs = errs.Also(apis.ErrInvalidValue(fmt.Sprintf("invalid apiVersion format %q, must be in the format \"group/version\"", pt.TaskSpec.APIVersion), "taskSpec.apiVersion"))
392+
}
393+
}
358394
return errs
359395
}
360396

0 commit comments

Comments
 (0)