Skip to content

Commit d6a2cdb

Browse files
l-qingtekton-robot
authored andcommitted
fix(taskrun): block taskrun spec updates once the taskrun has started
fix #8148 Typically, the spec field of a TaskRun resource is not allowed to be updated after creation, such as the `timeout` configuration. Only a few fields, such as `status` `statusMessage`, are allowed to be updated.
1 parent 9ee73be commit d6a2cdb

File tree

4 files changed

+358
-0
lines changed

4 files changed

+358
-0
lines changed

pkg/apis/pipeline/v1/taskrun_validation.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"github.com/tektoncd/pipeline/pkg/apis/validate"
2727
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
2828
corev1 "k8s.io/api/core/v1"
29+
"k8s.io/apimachinery/pkg/api/equality"
2930
"k8s.io/apimachinery/pkg/util/sets"
3031
"k8s.io/utils/strings/slices"
3132
"knative.dev/pkg/apis"
@@ -50,6 +51,9 @@ func (tr *TaskRun) Validate(ctx context.Context) *apis.FieldError {
5051

5152
// Validate taskrun spec
5253
func (ts *TaskRunSpec) Validate(ctx context.Context) (errs *apis.FieldError) {
54+
// Validate the spec changes
55+
errs = errs.Also(ts.ValidateUpdate(ctx))
56+
5357
// Must have exactly one of taskRef and taskSpec.
5458
if ts.TaskRef == nil && ts.TaskSpec == nil {
5559
errs = errs.Also(apis.ErrMissingOneOf("taskRef", "taskSpec"))
@@ -118,6 +122,34 @@ func (ts *TaskRunSpec) Validate(ctx context.Context) (errs *apis.FieldError) {
118122
return errs
119123
}
120124

125+
// ValidateUpdate validates the update of a TaskRunSpec
126+
func (ts *TaskRunSpec) ValidateUpdate(ctx context.Context) (errs *apis.FieldError) {
127+
if !apis.IsInUpdate(ctx) {
128+
return
129+
}
130+
oldObj, ok := apis.GetBaseline(ctx).(*TaskRun)
131+
if !ok || oldObj == nil {
132+
return
133+
}
134+
old := &oldObj.Spec
135+
136+
// If already in the done state, the spec cannot be modified.
137+
// Otherwise, only the status, statusMessage field can be modified.
138+
tips := "Once the TaskRun is complete, no updates are allowed"
139+
if !oldObj.IsDone() {
140+
old = old.DeepCopy()
141+
old.Status = ts.Status
142+
old.StatusMessage = ts.StatusMessage
143+
tips = "Once the TaskRun has started, only status and statusMessage updates are allowed"
144+
}
145+
146+
if !equality.Semantic.DeepEqual(old, ts) {
147+
errs = errs.Also(apis.ErrInvalidValue(tips, ""))
148+
}
149+
150+
return
151+
}
152+
121153
// validateInlineParameters validates that any parameters called in the
122154
// Task spec are declared in the TaskRun.
123155
// This is crucial for propagated parameters because the parameters could

pkg/apis/pipeline/v1/taskrun_validation_test.go

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"time"
2323

2424
"github.com/google/go-cmp/cmp"
25+
"github.com/google/go-cmp/cmp/cmpopts"
2526
"github.com/tektoncd/pipeline/pkg/apis/config"
2627
cfgtesting "github.com/tektoncd/pipeline/pkg/apis/config/testing"
2728
"github.com/tektoncd/pipeline/pkg/apis/pipeline/pod"
@@ -31,6 +32,7 @@ import (
3132
corev1resources "k8s.io/apimachinery/pkg/api/resource"
3233
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3334
"knative.dev/pkg/apis"
35+
duckv1 "knative.dev/pkg/apis/duck/v1"
3436
)
3537

3638
func TestTaskRun_Invalidate(t *testing.T) {
@@ -968,3 +970,148 @@ func TestTaskRunSpec_Validate(t *testing.T) {
968970
})
969971
}
970972
}
973+
974+
func TestTaskRunSpec_ValidateUpdate(t *testing.T) {
975+
tests := []struct {
976+
name string
977+
isCreate bool
978+
isUpdate bool
979+
baselineTaskRun *v1.TaskRun
980+
taskRun *v1.TaskRun
981+
expectedError apis.FieldError
982+
}{
983+
{
984+
name: "is create ctx",
985+
taskRun: &v1.TaskRun{
986+
Spec: v1.TaskRunSpec{},
987+
},
988+
isCreate: true,
989+
isUpdate: false,
990+
expectedError: apis.FieldError{},
991+
}, {
992+
name: "is update ctx, no changes",
993+
baselineTaskRun: &v1.TaskRun{
994+
Spec: v1.TaskRunSpec{
995+
Status: "TaskRunCancelled",
996+
},
997+
},
998+
taskRun: &v1.TaskRun{
999+
Spec: v1.TaskRunSpec{
1000+
Status: "TaskRunCancelled",
1001+
},
1002+
},
1003+
isCreate: false,
1004+
isUpdate: true,
1005+
expectedError: apis.FieldError{},
1006+
}, {
1007+
name: "is update ctx, baseline is nil, skip validation",
1008+
baselineTaskRun: nil,
1009+
taskRun: &v1.TaskRun{
1010+
Spec: v1.TaskRunSpec{
1011+
Timeout: &metav1.Duration{Duration: 1},
1012+
},
1013+
},
1014+
isCreate: false,
1015+
isUpdate: true,
1016+
expectedError: apis.FieldError{},
1017+
}, {
1018+
name: "is update ctx, baseline is unknown, only status changes",
1019+
baselineTaskRun: &v1.TaskRun{
1020+
Spec: v1.TaskRunSpec{
1021+
Status: "",
1022+
StatusMessage: "",
1023+
},
1024+
Status: v1.TaskRunStatus{
1025+
Status: duckv1.Status{
1026+
Conditions: duckv1.Conditions{
1027+
{Type: apis.ConditionSucceeded, Status: corev1.ConditionUnknown},
1028+
},
1029+
},
1030+
},
1031+
},
1032+
taskRun: &v1.TaskRun{
1033+
Spec: v1.TaskRunSpec{
1034+
Status: "TaskRunCancelled",
1035+
StatusMessage: "TaskRun is cancelled",
1036+
},
1037+
},
1038+
isCreate: false,
1039+
isUpdate: true,
1040+
expectedError: apis.FieldError{},
1041+
}, {
1042+
name: "is update ctx, baseline is unknown, status and timeout changes",
1043+
baselineTaskRun: &v1.TaskRun{
1044+
Spec: v1.TaskRunSpec{
1045+
Status: "",
1046+
StatusMessage: "",
1047+
Timeout: &metav1.Duration{Duration: 0},
1048+
},
1049+
Status: v1.TaskRunStatus{
1050+
Status: duckv1.Status{
1051+
Conditions: duckv1.Conditions{
1052+
{Type: apis.ConditionSucceeded, Status: corev1.ConditionUnknown},
1053+
},
1054+
},
1055+
},
1056+
},
1057+
taskRun: &v1.TaskRun{
1058+
Spec: v1.TaskRunSpec{
1059+
Status: "TaskRunCancelled",
1060+
StatusMessage: "TaskRun is cancelled",
1061+
Timeout: &metav1.Duration{Duration: 1},
1062+
},
1063+
},
1064+
isCreate: false,
1065+
isUpdate: true,
1066+
expectedError: apis.FieldError{
1067+
Message: `invalid value: Once the TaskRun has started, only status and statusMessage updates are allowed`,
1068+
Paths: []string{""},
1069+
},
1070+
}, {
1071+
name: "is update ctx, baseline is done, status changes",
1072+
baselineTaskRun: &v1.TaskRun{
1073+
Spec: v1.TaskRunSpec{
1074+
Status: "",
1075+
},
1076+
Status: v1.TaskRunStatus{
1077+
Status: duckv1.Status{
1078+
Conditions: duckv1.Conditions{
1079+
{Type: apis.ConditionSucceeded, Status: corev1.ConditionTrue},
1080+
},
1081+
},
1082+
},
1083+
},
1084+
taskRun: &v1.TaskRun{
1085+
Spec: v1.TaskRunSpec{
1086+
Status: "TaskRunCancelled",
1087+
},
1088+
},
1089+
isCreate: false,
1090+
isUpdate: true,
1091+
expectedError: apis.FieldError{
1092+
Message: `invalid value: Once the TaskRun is complete, no updates are allowed`,
1093+
Paths: []string{""},
1094+
},
1095+
},
1096+
}
1097+
1098+
for _, tt := range tests {
1099+
t.Run(tt.name, func(t *testing.T) {
1100+
ctx := config.ToContext(context.Background(), &config.Config{
1101+
FeatureFlags: &config.FeatureFlags{},
1102+
Defaults: &config.Defaults{},
1103+
})
1104+
if tt.isCreate {
1105+
ctx = apis.WithinCreate(ctx)
1106+
}
1107+
if tt.isUpdate {
1108+
ctx = apis.WithinUpdate(ctx, tt.baselineTaskRun)
1109+
}
1110+
tr := tt.taskRun
1111+
err := tr.Spec.ValidateUpdate(ctx)
1112+
if d := cmp.Diff(tt.expectedError.Error(), err.Error(), cmpopts.IgnoreUnexported(apis.FieldError{})); d != "" {
1113+
t.Errorf("TaskRunSpec.ValidateUpdate() errors diff %s", diff.PrintWantGot(d))
1114+
}
1115+
})
1116+
}
1117+
}

pkg/apis/pipeline/v1beta1/taskrun_validation.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"github.com/tektoncd/pipeline/pkg/apis/validate"
2727
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
2828
corev1 "k8s.io/api/core/v1"
29+
"k8s.io/apimachinery/pkg/api/equality"
2930
"k8s.io/apimachinery/pkg/util/sets"
3031
"k8s.io/utils/strings/slices"
3132
"knative.dev/pkg/apis"
@@ -50,6 +51,9 @@ func (tr *TaskRun) Validate(ctx context.Context) *apis.FieldError {
5051

5152
// Validate taskrun spec
5253
func (ts *TaskRunSpec) Validate(ctx context.Context) (errs *apis.FieldError) {
54+
// Validate the spec changes
55+
errs = errs.Also(ts.ValidateUpdate(ctx))
56+
5357
// Must have exactly one of taskRef and taskSpec.
5458
if ts.TaskRef == nil && ts.TaskSpec == nil {
5559
errs = errs.Also(apis.ErrMissingOneOf("taskRef", "taskSpec"))
@@ -118,6 +122,34 @@ func (ts *TaskRunSpec) Validate(ctx context.Context) (errs *apis.FieldError) {
118122
return errs
119123
}
120124

125+
// ValidateUpdate validates the update of a TaskRunSpec
126+
func (ts *TaskRunSpec) ValidateUpdate(ctx context.Context) (errs *apis.FieldError) {
127+
if !apis.IsInUpdate(ctx) {
128+
return
129+
}
130+
oldObj, ok := apis.GetBaseline(ctx).(*TaskRun)
131+
if !ok || oldObj == nil {
132+
return
133+
}
134+
old := &oldObj.Spec
135+
136+
// If already in the done state, the spec cannot be modified.
137+
// Otherwise, only the status, statusMessage field can be modified.
138+
tips := "Once the TaskRun is complete, no updates are allowed"
139+
if !oldObj.IsDone() {
140+
old = old.DeepCopy()
141+
old.Status = ts.Status
142+
old.StatusMessage = ts.StatusMessage
143+
tips = "Once the TaskRun has started, only status and statusMessage updates are allowed"
144+
}
145+
146+
if !equality.Semantic.DeepEqual(old, ts) {
147+
errs = errs.Also(apis.ErrInvalidValue(tips, ""))
148+
}
149+
150+
return
151+
}
152+
121153
// validateInlineParameters validates that any parameters called in the
122154
// Task spec are declared in the TaskRun.
123155
// This is crucial for propagated parameters because the parameters could

0 commit comments

Comments
 (0)