Skip to content

Commit 31785ef

Browse files
authored
Reject changes that would create an invalid StatefulSet patch (#552)
1 parent 97f2545 commit 31785ef

File tree

5 files changed

+64
-17
lines changed

5 files changed

+64
-17
lines changed

api/v1/coherence_webhook.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@ import (
1010
"fmt"
1111
"github.com/go-test/deep"
1212
"github.com/oracle/coherence-operator/pkg/operator"
13+
appsv1 "k8s.io/api/apps/v1"
14+
apiequality "k8s.io/apimachinery/pkg/api/equality"
1315
"k8s.io/apimachinery/pkg/runtime"
1416
"k8s.io/apimachinery/pkg/util/intstr"
17+
"k8s.io/apimachinery/pkg/util/validation/field"
1518
"k8s.io/utils/pointer"
1619
ctrl "sigs.k8s.io/controller-runtime"
1720
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
@@ -131,6 +134,7 @@ func (in *Coherence) ValidateUpdate(previous runtime.Object) error {
131134
return err
132135
}
133136
prev := previous.(*Coherence)
137+
134138
if err := in.validatePersistence(prev); err != nil {
135139
return err
136140
}
@@ -140,6 +144,14 @@ func (in *Coherence) ValidateUpdate(previous runtime.Object) error {
140144
if err := in.validateNodePorts(); err != nil {
141145
return err
142146
}
147+
148+
sts := in.Spec.CreateStatefulSet(in)
149+
stsOld := prev.Spec.CreateStatefulSet(prev)
150+
errorList := ValidateStatefulSetUpdate(&sts, &stsOld)
151+
if len(errorList) > 0 {
152+
return fmt.Errorf("rejecting update as it would have resulted in an invalid statefuleset: %v", errorList)
153+
}
154+
143155
return nil
144156
}
145157

@@ -212,3 +224,23 @@ func (in *Coherence) validateNodePorts() error {
212224

213225
return nil
214226
}
227+
228+
// ValidateStatefulSetUpdate tests if required fields in the StatefulSet are set.
229+
func ValidateStatefulSetUpdate(statefulSet, oldStatefulSet *appsv1.StatefulSet) field.ErrorList {
230+
var allErrs field.ErrorList
231+
232+
// statefulset updates aren't super common and general updates are likely to be touching spec, so we'll do this
233+
// deep copy right away. This avoids mutating our inputs
234+
newStatefulSetClone := statefulSet.DeepCopy()
235+
newStatefulSetClone.Spec.Replicas = oldStatefulSet.Spec.Replicas // +k8s:verify-mutation:reason=clone
236+
newStatefulSetClone.Spec.Template = oldStatefulSet.Spec.Template // +k8s:verify-mutation:reason=clone
237+
newStatefulSetClone.Spec.UpdateStrategy = oldStatefulSet.Spec.UpdateStrategy // +k8s:verify-mutation:reason=clone
238+
newStatefulSetClone.Spec.MinReadySeconds = oldStatefulSet.Spec.MinReadySeconds // +k8s:verify-mutation:reason=clone
239+
240+
newStatefulSetClone.Spec.PersistentVolumeClaimRetentionPolicy = oldStatefulSet.Spec.PersistentVolumeClaimRetentionPolicy // +k8s:verify-mutation:reason=clone
241+
if !apiequality.Semantic.DeepEqual(newStatefulSetClone.Spec, oldStatefulSet.Spec) {
242+
allErrs = append(allErrs, field.Forbidden(field.NewPath("spec"), "updates to statefulset spec for fields other than 'replicas', 'template', 'updateStrategy', 'persistentVolumeClaimRetentionPolicy' and 'minReadySeconds' are forbidden"))
243+
}
244+
245+
return allErrs
246+
}

api/v1/coherenceresourcespec_types.go

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -564,7 +564,7 @@ func (in *CoherenceResourceSpec) CreateKubernetesResources(d *Coherence) (Resour
564564
res = append(res, in.CreateHeadlessService(d))
565565

566566
// Create the StatefulSet
567-
res = append(res, in.CreateStatefulSet(d))
567+
res = append(res, in.CreateStatefulSetResource(d))
568568

569569
// Create the Services for each port (and optionally ServiceMonitors)
570570
res = append(res, in.CreateServicesForPort(d)...)
@@ -722,8 +722,19 @@ func (in *CoherenceResourceSpec) CreateHeadlessService(deployment *Coherence) Re
722722
}
723723
}
724724

725+
// CreateStatefulSetResource creates the deployment's StatefulSet resource.
726+
func (in *CoherenceResourceSpec) CreateStatefulSetResource(deployment *Coherence) Resource {
727+
sts := in.CreateStatefulSet(deployment)
728+
729+
return Resource{
730+
Kind: ResourceTypeStatefulSet,
731+
Name: sts.GetName(),
732+
Spec: &sts,
733+
}
734+
}
735+
725736
// CreateStatefulSet creates the deployment's StatefulSet.
726-
func (in *CoherenceResourceSpec) CreateStatefulSet(deployment *Coherence) Resource {
737+
func (in *CoherenceResourceSpec) CreateStatefulSet(deployment *Coherence) appsv1.StatefulSet {
727738
sts := appsv1.StatefulSet{
728739
ObjectMeta: metav1.ObjectMeta{
729740
Namespace: deployment.GetNamespace(),
@@ -830,11 +841,7 @@ func (in *CoherenceResourceSpec) CreateStatefulSet(deployment *Coherence) Resour
830841
sts.Spec.VolumeClaimTemplates = append(sts.Spec.VolumeClaimTemplates, v.ToPVC())
831842
}
832843

833-
return Resource{
834-
Kind: ResourceTypeStatefulSet,
835-
Name: sts.GetName(),
836-
Spec: &sts,
837-
}
844+
return sts
838845
}
839846

840847
func (in *CoherenceResourceSpec) GetImagePullSecrets() []corev1.LocalObjectReference {

api/v1/common_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,6 @@ func assertStatefulSetCreation(t *testing.T, deployment *coh.Coherence, stsExpec
473473
viper.Set(operator.FlagCoherenceImage, testCoherenceImage)
474474
viper.Set(operator.FlagOperatorImage, testOperatorImage)
475475

476-
res := deployment.Spec.CreateStatefulSet(deployment)
476+
res := deployment.Spec.CreateStatefulSetResource(deployment)
477477
assertStatefulSet(t, res, stsExpected)
478478
}

controllers/statefulset/statefulset_controller.go

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ const (
4545
)
4646

4747
// blank assignment to verify that ReconcileStatefulSet implements reconcile.Reconciler.
48-
// If the reconcile.Reconciler API was to change then we'd get a compile error here.
48+
// If the `reconcile.Reconciler` API was to change then we'd get a compile error here.
4949
var _ reconcile.Reconciler = &ReconcileStatefulSet{}
5050

5151
var log = logf.Log.WithName(controllerName)
@@ -397,12 +397,12 @@ func (in *ReconcileStatefulSet) updateStatefulSet(ctx context.Context, deploymen
397397
switch {
398398
case currentReplicas < desiredReplicas:
399399
// scale up - if also updating we do the rolling upgrade first followed by the
400-
// scale up so we do not do a rolling upgrade of the bigger scaled up cluster
400+
// scale up so that we do not do a rolling upgrade of the bigger scaled up cluster
401401

402402
// try the patch first
403403
result, err = in.patchStatefulSet(ctx, deployment, current, desired, storage, logger)
404404
if err == nil && !result.Requeue {
405-
// there was nothing else to patch so we can do the scale up
405+
// there was nothing else to patch, so we can do the scale up
406406
result, err = in.scale(ctx, deployment, current, currentReplicas, desiredReplicas)
407407
}
408408
case currentReplicas > desiredReplicas:
@@ -411,7 +411,7 @@ func (in *ReconcileStatefulSet) updateStatefulSet(ctx context.Context, deploymen
411411

412412
// do the scale down
413413
_, err = in.scale(ctx, deployment, current, currentReplicas, desiredReplicas)
414-
// requeue the request so we do any upgrade next time around
414+
// requeue the request so that we do any upgrade next time around
415415
result.Requeue = true
416416
default:
417417
// just an update
@@ -441,8 +441,16 @@ func (in *ReconcileStatefulSet) patchStatefulSet(ctx context.Context, deployment
441441
original = desired
442442
}
443443

444+
errorList := coh.ValidateStatefulSetUpdate(desired, original)
445+
if len(errorList) > 0 {
446+
msg := fmt.Sprintf("upddates to the statefuleset would have been invalid, the update will not be re-queued: %v", errorList)
447+
events := in.GetEventRecorder()
448+
events.Event(deployment, corev1.EventTypeWarning, reconciler.EventReasonUpdated, msg)
449+
return reconcile.Result{Requeue: false}, fmt.Errorf(msg)
450+
}
451+
444452
// We NEVER change the replicas or Status in an update.
445-
// Replicas is handled by scaling so we always set the desired replicas to match the current replicas
453+
// Replicas is handled by scaling, so we always set the desired replicas to match the current replicas
446454
desired.Spec.Replicas = current.Spec.Replicas
447455
original.Spec.Replicas = current.Spec.Replicas
448456

@@ -451,7 +459,7 @@ func (in *ReconcileStatefulSet) patchStatefulSet(ctx context.Context, deployment
451459
desired.ObjectMeta.Finalizers = current.ObjectMeta.Finalizers
452460

453461
// We need to ensure we do not create a patch due to differences in
454-
// StatefulSet Status so we blank out the status fields
462+
// StatefulSet Status, so we blank out the status fields
455463
desired.Status = appsv1.StatefulSetStatus{}
456464
current.Status = appsv1.StatefulSetStatus{}
457465
original.Status = appsv1.StatefulSetStatus{}
@@ -465,7 +473,7 @@ func (in *ReconcileStatefulSet) patchStatefulSet(ctx context.Context, deployment
465473

466474
// ensure we do not patch any fields that may be set by a previous version of the Operator
467475
// as this will cause a rolling update of the Pods, typically these are fields where
468-
// the Operator sets defaults and we changed the default behaviour
476+
// the Operator sets defaults, and we changed the default behaviour
469477
in.blankContainerFields(deployment, desired)
470478
in.blankContainerFields(deployment, current)
471479
in.blankContainerFields(deployment, original)
@@ -663,7 +671,7 @@ func (in *ReconcileStatefulSet) blankOperatorInitContainerFields(sts *appsv1.Sta
663671

664672
// Blanks out any fields that may have been set by a previous Operator version.
665673
// DO NOT blank out anything that the user has control over as they may have
666-
// updated them so we need to include them in the patch
674+
// updated them, so we need to include them in the patch
667675
func (in *ReconcileStatefulSet) blankCoherenceContainerFields(sts *appsv1.StatefulSet) {
668676
for i := range sts.Spec.Template.Spec.Containers {
669677
c := sts.Spec.Template.Spec.Containers[i]

pkg/runner/runner_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ func EnvVarsFromDeployment(d *coh.Coherence) map[string]string {
173173
d.Spec.JVM = &coh.JVMSpec{}
174174
}
175175

176-
res := d.Spec.CreateStatefulSet(d)
176+
res := d.Spec.CreateStatefulSetResource(d)
177177
sts := res.Spec.(*appsv1.StatefulSet)
178178
c := coh.FindContainer(coh.ContainerNameCoherence, sts)
179179
for _, ev := range c.Env {

0 commit comments

Comments
 (0)