Skip to content

Commit 95fbf31

Browse files
l-qingtekton-robot
authored andcommitted
fix(pipelinerun): block pipelinerun spec updates once the pipelinerun has started
Typically, the spec field of a PipelineRun resource is not allowed to be updated after creation, such as the `timeouts` configuration. Only a few fields, such as `status`, are allowed to be updated.
1 parent d6a2cdb commit 95fbf31

File tree

4 files changed

+415
-0
lines changed

4 files changed

+415
-0
lines changed

pkg/apis/pipeline/v1/pipelinerun_validation.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"github.com/tektoncd/pipeline/pkg/apis/validate"
2828
"github.com/tektoncd/pipeline/pkg/internal/resultref"
2929
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
30+
"k8s.io/apimachinery/pkg/api/equality"
3031
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3132
"knative.dev/pkg/apis"
3233
"knative.dev/pkg/webhook/resourcesemantics"
@@ -55,6 +56,9 @@ func (pr *PipelineRun) Validate(ctx context.Context) *apis.FieldError {
5556

5657
// Validate pipelinerun spec
5758
func (ps *PipelineRunSpec) Validate(ctx context.Context) (errs *apis.FieldError) {
59+
// Validate the spec changes
60+
errs = errs.Also(ps.ValidateUpdate(ctx))
61+
5862
// Must have exactly one of pipelineRef and pipelineSpec.
5963
if ps.PipelineRef == nil && ps.PipelineSpec == nil {
6064
errs = errs.Also(apis.ErrMissingOneOf("pipelineRef", "pipelineSpec"))
@@ -124,6 +128,31 @@ func (ps *PipelineRunSpec) Validate(ctx context.Context) (errs *apis.FieldError)
124128
return errs
125129
}
126130

131+
// ValidateUpdate validates the update of a PipelineRunSpec
132+
func (ps *PipelineRunSpec) ValidateUpdate(ctx context.Context) (errs *apis.FieldError) {
133+
if !apis.IsInUpdate(ctx) {
134+
return
135+
}
136+
oldObj, ok := apis.GetBaseline(ctx).(*PipelineRun)
137+
if !ok || oldObj == nil {
138+
return
139+
}
140+
old := &oldObj.Spec
141+
142+
// If already in the done state, the spec cannot be modified. Otherwise, only the status field can be modified.
143+
tips := "Once the PipelineRun is complete, no updates are allowed"
144+
if !oldObj.IsDone() {
145+
old = old.DeepCopy()
146+
old.Status = ps.Status
147+
tips = "Once the PipelineRun has started, only status updates are allowed"
148+
}
149+
if !equality.Semantic.DeepEqual(old, ps) {
150+
errs = errs.Also(apis.ErrInvalidValue(tips, ""))
151+
}
152+
153+
return
154+
}
155+
127156
func (ps *PipelineRunSpec) validatePipelineRunParameters(ctx context.Context) (errs *apis.FieldError) {
128157
if len(ps.Params) == 0 {
129158
return errs

pkg/apis/pipeline/v1/pipelinerun_validation_test.go

Lines changed: 179 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 TestPipelineRun_Invalid(t *testing.T) {
@@ -1511,3 +1513,180 @@ func TestPipelineRunSpecBetaFeatures(t *testing.T) {
15111513
})
15121514
}
15131515
}
1516+
1517+
func TestPipelineRunSpec_ValidateUpdate(t *testing.T) {
1518+
tests := []struct {
1519+
name string
1520+
isCreate bool
1521+
isUpdate bool
1522+
baselinePipelineRun *v1.PipelineRun
1523+
pipelineRun *v1.PipelineRun
1524+
expectedError apis.FieldError
1525+
}{
1526+
{
1527+
name: "is create ctx",
1528+
pipelineRun: &v1.PipelineRun{
1529+
Spec: v1.PipelineRunSpec{},
1530+
},
1531+
isCreate: true,
1532+
isUpdate: false,
1533+
expectedError: apis.FieldError{},
1534+
}, {
1535+
name: "is update ctx, no changes",
1536+
baselinePipelineRun: &v1.PipelineRun{
1537+
Spec: v1.PipelineRunSpec{
1538+
Status: "",
1539+
},
1540+
},
1541+
pipelineRun: &v1.PipelineRun{
1542+
Spec: v1.PipelineRunSpec{
1543+
Status: "",
1544+
},
1545+
},
1546+
isCreate: false,
1547+
isUpdate: true,
1548+
expectedError: apis.FieldError{},
1549+
}, {
1550+
name: "is update ctx, baseline is nil, skip validation",
1551+
baselinePipelineRun: nil,
1552+
pipelineRun: &v1.PipelineRun{
1553+
Spec: v1.PipelineRunSpec{
1554+
Timeouts: &v1.TimeoutFields{
1555+
Pipeline: &metav1.Duration{Duration: 1},
1556+
},
1557+
},
1558+
},
1559+
isCreate: false,
1560+
isUpdate: true,
1561+
expectedError: apis.FieldError{},
1562+
}, {
1563+
name: "is update ctx, baseline is unknown, status changes from Empty to Cancelled",
1564+
baselinePipelineRun: &v1.PipelineRun{
1565+
Spec: v1.PipelineRunSpec{
1566+
Status: "",
1567+
},
1568+
Status: v1.PipelineRunStatus{
1569+
Status: duckv1.Status{
1570+
Conditions: duckv1.Conditions{
1571+
{Type: apis.ConditionSucceeded, Status: corev1.ConditionUnknown},
1572+
},
1573+
},
1574+
},
1575+
},
1576+
pipelineRun: &v1.PipelineRun{
1577+
Spec: v1.PipelineRunSpec{
1578+
Status: "Cancelled",
1579+
},
1580+
},
1581+
isCreate: false,
1582+
isUpdate: true,
1583+
expectedError: apis.FieldError{},
1584+
}, {
1585+
name: "is update ctx, baseline is unknown, timeouts changes",
1586+
baselinePipelineRun: &v1.PipelineRun{
1587+
Spec: v1.PipelineRunSpec{
1588+
Status: "",
1589+
Timeouts: &v1.TimeoutFields{
1590+
Pipeline: &metav1.Duration{Duration: 0},
1591+
},
1592+
},
1593+
Status: v1.PipelineRunStatus{
1594+
Status: duckv1.Status{
1595+
Conditions: duckv1.Conditions{
1596+
{Type: apis.ConditionSucceeded, Status: corev1.ConditionUnknown},
1597+
},
1598+
},
1599+
},
1600+
},
1601+
pipelineRun: &v1.PipelineRun{
1602+
Spec: v1.PipelineRunSpec{
1603+
Timeouts: &v1.TimeoutFields{
1604+
Pipeline: &metav1.Duration{Duration: 1},
1605+
},
1606+
},
1607+
},
1608+
isCreate: false,
1609+
isUpdate: true,
1610+
expectedError: apis.FieldError{
1611+
Message: `invalid value: Once the PipelineRun has started, only status updates are allowed`,
1612+
Paths: []string{""},
1613+
},
1614+
}, {
1615+
name: "is update ctx, baseline is unknown, status changes from PipelineRunPending to Empty, and timeouts changes",
1616+
baselinePipelineRun: &v1.PipelineRun{
1617+
Spec: v1.PipelineRunSpec{
1618+
Status: "PipelineRunPending",
1619+
Timeouts: &v1.TimeoutFields{
1620+
Pipeline: &metav1.Duration{Duration: 0},
1621+
},
1622+
},
1623+
Status: v1.PipelineRunStatus{
1624+
Status: duckv1.Status{
1625+
Conditions: duckv1.Conditions{
1626+
{Type: apis.ConditionSucceeded, Status: corev1.ConditionUnknown},
1627+
},
1628+
},
1629+
},
1630+
},
1631+
pipelineRun: &v1.PipelineRun{
1632+
Spec: v1.PipelineRunSpec{
1633+
Status: "",
1634+
Timeouts: &v1.TimeoutFields{
1635+
Pipeline: &metav1.Duration{Duration: 1},
1636+
},
1637+
},
1638+
},
1639+
isCreate: false,
1640+
isUpdate: true,
1641+
expectedError: apis.FieldError{
1642+
Message: `invalid value: Once the PipelineRun has started, only status updates are allowed`,
1643+
Paths: []string{""},
1644+
},
1645+
}, {
1646+
name: "is update ctx, baseline is done, status changes",
1647+
baselinePipelineRun: &v1.PipelineRun{
1648+
Spec: v1.PipelineRunSpec{
1649+
Status: "PipelineRunPending",
1650+
},
1651+
Status: v1.PipelineRunStatus{
1652+
Status: duckv1.Status{
1653+
Conditions: duckv1.Conditions{
1654+
{Type: apis.ConditionSucceeded, Status: corev1.ConditionTrue},
1655+
},
1656+
},
1657+
},
1658+
},
1659+
pipelineRun: &v1.PipelineRun{
1660+
Spec: v1.PipelineRunSpec{
1661+
Status: "TaskRunCancelled",
1662+
},
1663+
},
1664+
isCreate: false,
1665+
isUpdate: true,
1666+
expectedError: apis.FieldError{
1667+
Message: `invalid value: Once the PipelineRun is complete, no updates are allowed`,
1668+
Paths: []string{""},
1669+
},
1670+
},
1671+
}
1672+
1673+
for _, tt := range tests {
1674+
t.Run(tt.name, func(t *testing.T) {
1675+
ctx := config.ToContext(context.Background(), &config.Config{
1676+
FeatureFlags: &config.FeatureFlags{},
1677+
Defaults: &config.Defaults{},
1678+
})
1679+
if tt.isCreate {
1680+
ctx = apis.WithinCreate(ctx)
1681+
}
1682+
if tt.isUpdate {
1683+
ctx = apis.WithinUpdate(ctx, tt.baselinePipelineRun)
1684+
}
1685+
pr := tt.pipelineRun
1686+
err := pr.Spec.ValidateUpdate(ctx)
1687+
if d := cmp.Diff(tt.expectedError.Error(), err.Error(), cmpopts.IgnoreUnexported(apis.FieldError{})); d != "" {
1688+
t.Errorf("PipelineRunSpec.ValidateUpdate() errors diff %s", diff.PrintWantGot(d))
1689+
}
1690+
})
1691+
}
1692+
}

pkg/apis/pipeline/v1beta1/pipelinerun_validation.go

Lines changed: 29 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
"github.com/tektoncd/pipeline/pkg/internal/resultref"
2828
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
29+
"k8s.io/apimachinery/pkg/api/equality"
2930
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3031
"k8s.io/apimachinery/pkg/util/sets"
3132
"k8s.io/utils/strings/slices"
@@ -60,6 +61,9 @@ func (pr *PipelineRun) Validate(ctx context.Context) *apis.FieldError {
6061

6162
// Validate pipelinerun spec
6263
func (ps *PipelineRunSpec) Validate(ctx context.Context) (errs *apis.FieldError) {
64+
// Validate the spec changes
65+
errs = errs.Also(ps.ValidateUpdate(ctx))
66+
6367
// Must have exactly one of pipelineRef and pipelineSpec.
6468
if ps.PipelineRef == nil && ps.PipelineSpec == nil {
6569
errs = errs.Also(apis.ErrMissingOneOf("pipelineRef", "pipelineSpec"))
@@ -145,6 +149,31 @@ func (ps *PipelineRunSpec) Validate(ctx context.Context) (errs *apis.FieldError)
145149
return errs
146150
}
147151

152+
// ValidateUpdate validates the update of a PipelineRunSpec
153+
func (ps *PipelineRunSpec) ValidateUpdate(ctx context.Context) (errs *apis.FieldError) {
154+
if !apis.IsInUpdate(ctx) {
155+
return
156+
}
157+
oldObj, ok := apis.GetBaseline(ctx).(*PipelineRun)
158+
if !ok || oldObj == nil {
159+
return
160+
}
161+
old := &oldObj.Spec
162+
163+
// If already in the done state, the spec cannot be modified. Otherwise, only the status field can be modified.
164+
tips := "Once the PipelineRun is complete, no updates are allowed"
165+
if !oldObj.IsDone() {
166+
old = old.DeepCopy()
167+
old.Status = ps.Status
168+
tips = "Once the PipelineRun has started, only status updates are allowed"
169+
}
170+
if !equality.Semantic.DeepEqual(old, ps) {
171+
errs = errs.Also(apis.ErrInvalidValue(tips, ""))
172+
}
173+
174+
return
175+
}
176+
148177
func (ps *PipelineRunSpec) validatePipelineRunParameters(ctx context.Context) (errs *apis.FieldError) {
149178
if len(ps.Params) == 0 {
150179
return errs

0 commit comments

Comments
 (0)