Skip to content

Commit 8a49e24

Browse files
authored
CLOUDP-59612: Configure StatefulSet UpdatePolicy (#31)
1 parent f2b3dc8 commit 8a49e24

File tree

7 files changed

+227
-34
lines changed

7 files changed

+227
-34
lines changed

pkg/apis/mongodb/v1/mongodb_types.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ const (
1919
Running Phase = "Running"
2020
)
2121

22+
const (
23+
// LastVersionAnnotationKey should indicate which version of MongoDB was last
24+
// configured
25+
LastVersionAnnotationKey = "lastVersion"
26+
)
27+
2228
// MongoDBSpec defines the desired state of MongoDB
2329
type MongoDBSpec struct {
2430
// Members is the number of members in the replica set
@@ -59,6 +65,13 @@ func (m *MongoDB) UpdateSuccess() {
5965
m.Status.Phase = Running
6066
}
6167

68+
func (m MongoDB) ChangingVersion() bool {
69+
if lastVersion, ok := m.Annotations[LastVersionAnnotationKey]; ok {
70+
return (m.Spec.Version != lastVersion) && lastVersion != ""
71+
}
72+
return false
73+
}
74+
6275
// MongoURI returns a mongo uri which can be used to connect to this deployment
6376
func (m MongoDB) MongoURI() string {
6477
members := make([]string, m.Spec.Members)

pkg/controller/mongodb/mongodb_controller.go

Lines changed: 98 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"os"
99
"time"
1010

11+
"github.com/mongodb/mongodb-kubernetes-operator/pkg/controller/predicates"
12+
1113
mdbv1 "github.com/mongodb/mongodb-kubernetes-operator/pkg/apis/mongodb/v1"
1214
"github.com/mongodb/mongodb-kubernetes-operator/pkg/automationconfig"
1315
mdbClient "github.com/mongodb/mongodb-kubernetes-operator/pkg/kube/client"
@@ -57,6 +59,7 @@ func newReconciler(mgr manager.Manager, manifestProvider ManifestProvider) recon
5759
client: mdbClient.NewClient(mgrClient),
5860
scheme: mgr.GetScheme(),
5961
manifestProvider: manifestProvider,
62+
log: zap.S(),
6063
}
6164
}
6265

@@ -69,7 +72,7 @@ func add(mgr manager.Manager, r reconcile.Reconciler) error {
6972
}
7073

7174
// Watch for changes to primary resource MongoDB
72-
err = c.Watch(&source.Kind{Type: &mdbv1.MongoDB{}}, &handler.EnqueueRequestForObject{})
75+
err = c.Watch(&source.Kind{Type: &mdbv1.MongoDB{}}, &handler.EnqueueRequestForObject{}, predicates.OnlyOnSpecChange())
7376
if err != nil {
7477
return err
7578
}
@@ -86,6 +89,7 @@ type ReplicaSetReconciler struct {
8689
client mdbClient.Client
8790
scheme *runtime.Scheme
8891
manifestProvider func() (automationconfig.VersionManifest, error)
92+
log *zap.SugaredLogger
8993
}
9094

9195
// Reconcile reads that state of the cluster for a MongoDB object and makes changes based on the state read
@@ -94,8 +98,8 @@ type ReplicaSetReconciler struct {
9498
// The Controller will requeue the Request to be processed again if the returned error is non-nil or
9599
// Result.Requeue is true, otherwise upon completion it will remove the work from the queue.
96100
func (r *ReplicaSetReconciler) Reconcile(request reconcile.Request) (reconcile.Result, error) {
97-
log := zap.S().With("ReplicaSet", request.NamespacedName)
98-
log.Info("Reconciling MongoDB")
101+
r.log = zap.S().With("ReplicaSet", request.NamespacedName)
102+
r.log.Info("Reconciling MongoDB")
99103

100104
// TODO: generalize preparation for resource
101105
// Fetch the MongoDB instance
@@ -108,61 +112,116 @@ func (r *ReplicaSetReconciler) Reconcile(request reconcile.Request) (reconcile.R
108112
// Return and don't requeue
109113
return reconcile.Result{}, nil
110114
}
111-
log.Errorf("error reconciling MongoDB resource: %s", err)
115+
r.log.Errorf("error reconciling MongoDB resource: %s", err)
112116
// Error reading the object - requeue the request.
113117
return reconcile.Result{}, err
114118
}
115119

116120
// TODO: Read current automation config version from config map
117121
if err := r.ensureAutomationConfig(mdb); err != nil {
118-
log.Warnf("error creating automation config config map: %s", err)
122+
r.log.Infof("error creating automation config config map: %s", err)
119123
return reconcile.Result{}, err
120124
}
121125

122126
svc := buildService(mdb)
123127
if err = r.client.CreateOrUpdate(&svc); err != nil {
124-
log.Warnf("The service already exists... moving forward: %s", err)
128+
r.log.Infof("The service already exists... moving forward: %s", err)
125129
}
126130

127-
sts, err := buildStatefulSet(mdb)
128-
if err != nil {
129-
log.Infof("Error building StatefulSet: %s", err)
130-
return reconcile.Result{}, nil
131+
if err := r.createOrUpdateStatefulSet(mdb); err != nil {
132+
r.log.Infof("Error creating/updating StatefulSet: %+v", err)
133+
return reconcile.Result{}, err
131134
}
132135

133-
if err = r.client.CreateOrUpdate(&sts); err != nil {
134-
log.Infof("Error creating/updating StatefulSet: %s", err)
136+
if ready, err := r.isStatefulSetReady(mdb); err != nil {
137+
r.log.Infof("error checking StatefulSet status: %+v", err)
135138
return reconcile.Result{}, err
136-
} else {
137-
log.Infof("StatefulSet successfully Created/Updated")
139+
} else if !ready {
140+
r.log.Infof("StatefulSet %s/%s is not yet ready, retrying in 10 seconds", mdb.Namespace, mdb.Name)
141+
return reconcile.Result{RequeueAfter: time.Second * 10}, nil
138142
}
139143

140-
log.Debugf("waiting for StatefulSet %s/%s to reach ready state", mdb.Namespace, mdb.Name)
141-
set := appsv1.StatefulSet{}
142-
if err := r.client.Get(context.TODO(), types.NamespacedName{Name: mdb.Name, Namespace: mdb.Namespace}, &set); err != nil {
143-
log.Infof("Error getting StatefulSet: %s", err)
144+
if err := r.resetStatefulSetUpdateStrategy(mdb); err != nil {
145+
r.log.Infof("error resetting StatefulSet UpdateStrategyType: %+v", err)
144146
return reconcile.Result{}, err
145147
}
146148

147-
if !statefulset.IsReady(set) {
148-
log.Infof("Stateful Set has not yet reached the ready state, requeuing reconciliation")
149-
return reconcile.Result{RequeueAfter: time.Second * 10}, nil
149+
if err := r.setAnnotation(types.NamespacedName{Name: mdb.Name, Namespace: mdb.Namespace}, mdbv1.LastVersionAnnotationKey, mdb.Spec.Version); err != nil {
150+
r.log.Infof("Error setting annotation: %+v", err)
151+
return reconcile.Result{}, err
150152
}
151153

152-
log.Infof("Stateful Set reached ready state!")
153-
154154
if err := r.updateStatusSuccess(&mdb); err != nil {
155-
log.Infof("Error updating the status of the MongoDB resource: %+v", err)
156-
return reconcile.Result{}, nil
155+
r.log.Infof("Error updating the status of the MongoDB resource: %+v", err)
156+
return reconcile.Result{}, err
157157
}
158158

159-
log.Info("Successfully finished reconciliation", "MongoDB.Spec:", mdb.Spec, "MongoDB.Status", mdb.Status)
159+
r.log.Info("Successfully finished reconciliation", "MongoDB.Spec:", mdb.Spec, "MongoDB.Status", mdb.Status)
160160
return reconcile.Result{}, nil
161161
}
162162

163+
// resetStatefulSetUpdateStrategy ensures the stateful set is configured back to using RollingUpdateStatefulSetStrategyType
164+
// and does not keep using OnDelete
165+
func (r *ReplicaSetReconciler) resetStatefulSetUpdateStrategy(mdb mdbv1.MongoDB) error {
166+
if !mdb.ChangingVersion() {
167+
return nil
168+
}
169+
// if we changed the version, we need to reset the UpdatePolicy back to OnUpdate
170+
sts := &appsv1.StatefulSet{}
171+
return r.client.GetAndUpdate(types.NamespacedName{Name: mdb.Name, Namespace: mdb.Namespace}, sts, func() {
172+
sts.Spec.UpdateStrategy.Type = appsv1.RollingUpdateStatefulSetStrategyType
173+
})
174+
}
175+
176+
// isStatefulSetReady checks to see if the stateful set corresponding to the given MongoDB resource
177+
// is currently in the ready state
178+
func (r *ReplicaSetReconciler) isStatefulSetReady(mdb mdbv1.MongoDB) (bool, error) {
179+
set := appsv1.StatefulSet{}
180+
if err := r.client.Get(context.TODO(), types.NamespacedName{Name: mdb.Name, Namespace: mdb.Namespace}, &set); err != nil {
181+
return false, fmt.Errorf("error getting StatefulSet: %s", err)
182+
}
183+
return statefulset.IsReady(set), nil
184+
}
185+
186+
func (r *ReplicaSetReconciler) createOrUpdateStatefulSet(mdb mdbv1.MongoDB) error {
187+
sts, err := buildStatefulSet(mdb)
188+
if err != nil {
189+
return fmt.Errorf("error building StatefulSet: %s", err)
190+
}
191+
if err = r.client.CreateOrUpdate(&sts); err != nil {
192+
return fmt.Errorf("error creating/updating StatefulSet: %s", err)
193+
}
194+
195+
r.log.Debugf("Waiting for StatefulSet %s/%s to reach ready state", mdb.Namespace, mdb.Name)
196+
set := appsv1.StatefulSet{}
197+
if err := r.client.Get(context.TODO(), types.NamespacedName{Name: mdb.Name, Namespace: mdb.Namespace}, &set); err != nil {
198+
return fmt.Errorf("error getting StatefulSet: %s", err)
199+
}
200+
return nil
201+
}
202+
203+
// setAnnotation updates the monogdb resource with the given namespaced name and sets the annotation
204+
// "key" with the provided value "val"
205+
func (r ReplicaSetReconciler) setAnnotation(nsName types.NamespacedName, key, val string) error {
206+
mdb := mdbv1.MongoDB{}
207+
return r.client.GetAndUpdate(nsName, &mdb, func() {
208+
if mdb.Annotations == nil {
209+
mdb.Annotations = map[string]string{}
210+
}
211+
mdb.Annotations[key] = val
212+
})
213+
}
214+
215+
// updateStatusSuccess should be called after a successful reconciliation
216+
// the resource's status is updated to reflect to the state, and any other cleanup
217+
// operators should be performed here
163218
func (r ReplicaSetReconciler) updateStatusSuccess(mdb *mdbv1.MongoDB) error {
164-
mdb.UpdateSuccess()
165-
if err := r.client.Status().Update(context.TODO(), mdb); err != nil {
219+
newMdb := &mdbv1.MongoDB{}
220+
if err := r.client.Get(context.TODO(), types.NamespacedName{Name: mdb.Name, Namespace: mdb.Namespace}, newMdb); err != nil {
221+
return fmt.Errorf("error getting resource: %+v", err)
222+
}
223+
newMdb.UpdateSuccess()
224+
if err := r.client.Status().Update(context.TODO(), newMdb); err != nil {
166225
return fmt.Errorf("error updating status: %+v", err)
167226
}
168227
return nil
@@ -293,6 +352,15 @@ func defaultReadinessProbe() corev1.Probe {
293352
}
294353
}
295354

355+
// getUpdateStrategyType returns the type of RollingUpgradeStrategy that the StatefulSet
356+
// should be configured with
357+
func getUpdateStrategyType(mdb mdbv1.MongoDB) appsv1.StatefulSetUpdateStrategyType {
358+
if !mdb.ChangingVersion() {
359+
return appsv1.RollingUpdateStatefulSetStrategyType
360+
}
361+
return appsv1.OnDeleteStatefulSetStrategyType
362+
}
363+
296364
// buildStatefulSet takes a MongoDB resource and converts it into
297365
// the corresponding stateful set
298366
func buildStatefulSet(mdb mdbv1.MongoDB) (appsv1.StatefulSet, error) {
@@ -316,7 +384,8 @@ func buildStatefulSet(mdb mdbv1.MongoDB) (appsv1.StatefulSet, error) {
316384
SetReplicas(mdb.Spec.Members).
317385
SetLabels(labels).
318386
SetMatchLabels(labels).
319-
SetServiceName(mdb.ServiceName())
387+
SetServiceName(mdb.ServiceName()).
388+
SetUpdateStrategy(getUpdateStrategyType(mdb))
320389

321390
// TODO: Add this section to architecture document.
322391
// The design of the multi-container and the different volumes mounted to them is as follows:

pkg/controller/mongodb/replicaset_controller_test.go

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ func init() {
2626
func newTestReplicaSet() mdbv1.MongoDB {
2727
return mdbv1.MongoDB{
2828
ObjectMeta: metav1.ObjectMeta{
29-
Name: "my-rs",
30-
Namespace: "my-ns",
29+
Name: "my-rs",
30+
Namespace: "my-ns",
31+
Annotations: map[string]string{},
3132
},
3233
Spec: mdbv1.MongoDBSpec{
3334
Members: 3,
@@ -102,3 +103,62 @@ func TestStatefulSet_IsCorrectlyConfigured(t *testing.T) {
102103

103104
assert.Equal(t, resourcerequirements.Defaults(), agentContainer.Resources)
104105
}
106+
107+
func TestChangingVersion_ResultsInRollingUpdateStrategyType(t *testing.T) {
108+
mdb := newTestReplicaSet()
109+
mgr := client.NewManager(&mdb)
110+
mgrClient := mgr.GetClient()
111+
r := newReconciler(mgr, mockManifestProvider(mdb.Spec.Version))
112+
res, err := r.Reconcile(reconcile.Request{NamespacedName: types.NamespacedName{Namespace: mdb.Namespace, Name: mdb.Name}})
113+
assertReconciliationSuccessful(t, res, err)
114+
115+
// fetch updated resource after first reconciliation
116+
_ = mgrClient.Get(context.TODO(), types.NamespacedName{Name: mdb.Name, Namespace: mdb.Namespace}, &mdb)
117+
118+
sts := appsv1.StatefulSet{}
119+
err = mgrClient.Get(context.TODO(), types.NamespacedName{Name: mdb.Name, Namespace: mdb.Namespace}, &sts)
120+
assert.NoError(t, err)
121+
assert.Equal(t, appsv1.RollingUpdateStatefulSetStrategyType, sts.Spec.UpdateStrategy.Type)
122+
123+
mdbRef := &mdb
124+
mdbRef.Spec.Version = "4.2.3"
125+
126+
_ = mgrClient.Update(context.TODO(), &mdb)
127+
128+
res, err = r.Reconcile(reconcile.Request{NamespacedName: types.NamespacedName{Namespace: mdb.Namespace, Name: mdb.Name}})
129+
assertReconciliationSuccessful(t, res, err)
130+
131+
sts = appsv1.StatefulSet{}
132+
err = mgrClient.Get(context.TODO(), types.NamespacedName{Name: mdb.Name, Namespace: mdb.Namespace}, &sts)
133+
assert.NoError(t, err)
134+
135+
assert.Equal(t, appsv1.RollingUpdateStatefulSetStrategyType, sts.Spec.UpdateStrategy.Type,
136+
"The StatefulSet should have be re-configured to use RollingUpdates after it reached the ready state")
137+
}
138+
139+
func TestBuildStatefulSet_ConfiguresUpdateStrategyCorrectly(t *testing.T) {
140+
t.Run("On No Version Change, Same Version", func(t *testing.T) {
141+
mdb := newTestReplicaSet()
142+
mdb.Spec.Version = "4.0.0"
143+
mdb.Annotations[mdbv1.LastVersionAnnotationKey] = "4.0.0"
144+
sts, err := buildStatefulSet(mdb)
145+
assert.NoError(t, err)
146+
assert.Equal(t, appsv1.RollingUpdateStatefulSetStrategyType, sts.Spec.UpdateStrategy.Type)
147+
})
148+
t.Run("On No Version Change, First Version", func(t *testing.T) {
149+
mdb := newTestReplicaSet()
150+
mdb.Spec.Version = "4.0.0"
151+
delete(mdb.Annotations, mdbv1.LastVersionAnnotationKey)
152+
sts, err := buildStatefulSet(mdb)
153+
assert.NoError(t, err)
154+
assert.Equal(t, appsv1.RollingUpdateStatefulSetStrategyType, sts.Spec.UpdateStrategy.Type)
155+
})
156+
t.Run("On Version Change", func(t *testing.T) {
157+
mdb := newTestReplicaSet()
158+
mdb.Spec.Version = "4.0.0"
159+
mdb.Annotations[mdbv1.LastVersionAnnotationKey] = "4.2.0"
160+
sts, err := buildStatefulSet(mdb)
161+
assert.NoError(t, err)
162+
assert.Equal(t, appsv1.OnDeleteStatefulSetStrategyType, sts.Spec.UpdateStrategy.Type)
163+
})
164+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package predicates
2+
3+
import (
4+
"reflect"
5+
6+
mdbv1 "github.com/mongodb/mongodb-kubernetes-operator/pkg/apis/mongodb/v1"
7+
"sigs.k8s.io/controller-runtime/pkg/event"
8+
"sigs.k8s.io/controller-runtime/pkg/predicate"
9+
)
10+
11+
// OnlyOnSpecChange returns a set of predicates indicating
12+
// that reconciliations should only happen on changes to the Spec of the resource.
13+
// any other changes won't trigger a reconciliation. This allows us to freely update the annotations
14+
// of the resource without triggering unintentional reconciliations.
15+
func OnlyOnSpecChange() predicate.Funcs {
16+
return predicate.Funcs{
17+
UpdateFunc: func(e event.UpdateEvent) bool {
18+
oldResource := e.ObjectOld.(*mdbv1.MongoDB)
19+
newResource := e.ObjectNew.(*mdbv1.MongoDB)
20+
specChanged := !reflect.DeepEqual(oldResource.Spec, newResource.Spec)
21+
return specChanged
22+
},
23+
}
24+
}

pkg/kube/client/client.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ func NewClient(c k8sClient.Client) Client {
1919
type Client interface {
2020
k8sClient.Client
2121
CreateOrUpdate(obj runtime.Object) error
22+
GetAndUpdate(nsName types.NamespacedName, obj runtime.Object, updateFunc func()) error
2223
}
2324

2425
type client struct {
@@ -39,6 +40,19 @@ func (c client) CreateOrUpdate(obj runtime.Object) error {
3940
return c.Update(context.TODO(), obj)
4041
}
4142

43+
// GetAndUpdate fetches the most recent version of the runtime.Object with the provided
44+
// nsName and applies the update function. The update function should update "obj" from
45+
// an outer scope
46+
func (c client) GetAndUpdate(nsName types.NamespacedName, obj runtime.Object, updateFunc func()) error {
47+
err := c.Get(context.TODO(), nsName, obj)
48+
if err != nil {
49+
return err
50+
}
51+
// apply the function on the most recent version of the resource
52+
updateFunc()
53+
return c.Update(context.TODO(), obj)
54+
}
55+
4256
func namespacedNameFromObject(obj runtime.Object) types.NamespacedName {
4357
ns := reflect.ValueOf(obj).Elem().FieldByName("Namespace").String()
4458
name := reflect.ValueOf(obj).Elem().FieldByName("Name").String()

pkg/kube/client/mocked_client.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,15 +59,15 @@ func (m *mockedClient) Create(_ context.Context, obj runtime.Object, _ ...k8sCli
5959

6060
switch v := obj.(type) {
6161
case *appsv1.StatefulSet:
62-
onStatefulsetUpdate(v)
62+
makeStatefulSetReady(v)
6363
}
6464

6565
relevantMap[objKey] = obj
6666
return nil
6767
}
6868

69-
// onStatefulsetUpdate configures the statefulset to be in the running state.
70-
func onStatefulsetUpdate(set *appsv1.StatefulSet) {
69+
// makeStatefulSetReady configures the statefulset to be in the running state.
70+
func makeStatefulSetReady(set *appsv1.StatefulSet) {
7171
set.Status.UpdatedReplicas = *set.Spec.Replicas
7272
set.Status.ReadyReplicas = *set.Spec.Replicas
7373
}
@@ -86,6 +86,10 @@ func (m *mockedClient) Update(_ context.Context, obj runtime.Object, _ ...k8sCli
8686
if err != nil {
8787
return err
8888
}
89+
switch v := obj.(type) {
90+
case *appsv1.StatefulSet:
91+
makeStatefulSetReady(v)
92+
}
8993
relevantMap[objKey] = obj
9094
return nil
9195
}

0 commit comments

Comments
 (0)