Skip to content

Commit 0977eca

Browse files
feat: add recreate-stateful-strategy, orphan, background, foreground(default) (#1286)
* feat: add recreate-stateful-strategy, orphan, background, foreground(default) Signed-off-by: Husni Alhamdani <[email protected]> * feat: add recreate-stateful-strategy, orphan, background, foreground(default) Signed-off-by: Husni Alhamdani <[email protected]> * typo Signed-off-by: yangw <[email protected]> * common function Signed-off-by: yangw <[email protected]> * docs Signed-off-by: yangw <[email protected]> --------- Signed-off-by: Husni Alhamdani <[email protected]> Signed-off-by: yangw <[email protected]> Co-authored-by: yangw <[email protected]>
1 parent 4af5f1d commit 0977eca

File tree

9 files changed

+106
-14
lines changed

9 files changed

+106
-14
lines changed

docs/content/en/docs/Advance Configuration/Upgrading/_index.md

+45
Original file line numberDiff line numberDiff line change
@@ -170,3 +170,48 @@ spec:
170170
- If application is highly critical, in such scenarios it would make sense to create a new cluster and migrate the application pointing to it
171171
{{< /alert >}}
172172

173+
## StatefulSet Recreation Strategy
174+
175+
In some scenarios, you may need to recreate the StatefulSet completely, for example when you need to change immutable fields. Redis Operator provides an annotation `redis.opstreelabs.in/recreate-statefulset` that can be set to `true` to recreate the StatefulSet.
176+
177+
Redis Operator supports specifying the deletion propagation strategy when recreating StatefulSets. This allows more control over how dependent resources like Pods are handled during the recreation process.
178+
179+
### Available Strategies
180+
181+
You can control the deletion behavior using the annotation `redis.opstreelabs.in/recreate-statefulset-strategy` with the following values:
182+
183+
- **foreground** (default): The StatefulSet and its dependent objects (like Pods) are deleted synchronously. Kubernetes waits for all dependent objects to be deleted before finalizing the StatefulSet deletion.
184+
- **background**: The StatefulSet is deleted immediately, and dependent objects are deleted asynchronously in the background.
185+
- **orphan**: The StatefulSet is deleted but its dependent objects (Pods) are kept running. This provides the most control and minimizes downtime, but requires manual cleanup later.
186+
187+
### Example Usage
188+
189+
Here's how to configure Redis with a specific StatefulSet recreation strategy:
190+
191+
```yaml
192+
apiVersion: redis.redis.opstreelabs.in/v1beta2
193+
kind: Redis
194+
metadata:
195+
name: redis-standalone
196+
annotations:
197+
redis.opstreelabs.in/recreate-statefulset: "true"
198+
redis.opstreelabs.in/recreate-statefulset-strategy: "orphan"
199+
spec:
200+
kubernetesConfig:
201+
image: "quay.io/opstree/redis:v7.0.5"
202+
imagePullPolicy: "IfNotPresent"
203+
# ... other configuration ...
204+
```
205+
206+
### When to Use Different Strategies
207+
208+
- **foreground**: When you want to ensure a complete, clean recreation of the Redis system and can tolerate downtime.
209+
- **background**: When you want to minimize the time the StatefulSet object itself exists in a "Terminating" state and don't need to wait for dependent objects to be deleted.
210+
- **orphan**: When you need to avoid downtime during StatefulSet recreation, especially in production environments. The existing Pods will continue to serve traffic while the new StatefulSet is being created.
211+
212+
{{< alert color="warning" title="Warning" >}}
213+
Using the "orphan" strategy will keep the Pods running, but they won't be managed by the StatefulSet until the new one adopts them. If the Pods need to be updated, you may need to manually delete them so the new StatefulSet can create fresh Pods.
214+
{{< /alert >}}
215+
216+
This feature is useful when upgrading Redis with changes to immutable StatefulSet fields, allowing administrators to control the recreation process according to their operational requirements.
217+

docs/content/en/docs/CRD Reference/Redis API/_index.md

+17
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,23 @@ This page documents the Redis API Schema definitions for the redis API group.
1919

2020
Package v1beta2 contains API Schema definitions for the redis v1beta2 API group
2121

22+
### Annotations
23+
24+
Redis Operator supports the following annotations that can be added to Redis, RedisCluster, RedisReplication, and RedisSentinel resources:
25+
26+
| Annotation | Description | Default | Values |
27+
| --- | --- | --- | --- |
28+
| `redis.opstreelabs.in/recreate-statefulset` | Controls whether the StatefulSet should be recreated when changed | `false` | `"true"`, `"false"` |
29+
| `redis.opstreelabs.in/recreate-statefulset-strategy` | Controls how dependent resources are handled when the StatefulSet is recreated | `foreground` | `"foreground"`, `"background"`, `"orphan"` |
30+
31+
#### Deletion Propagation Strategies
32+
33+
When `redis.opstreelabs.in/recreate-statefulset` is set to `"true"`, you can control the deletion behavior using the `redis.opstreelabs.in/recreate-statefulset-strategy` annotation:
34+
35+
- **foreground**: The StatefulSet and its dependent objects (like Pods) are deleted synchronously
36+
- **background**: The StatefulSet is deleted immediately, and dependent objects are deleted asynchronously
37+
- **orphan**: The StatefulSet is deleted but its dependent objects (Pods) are kept running
38+
2239
### Resource Types
2340

2441
- [Redis](#redis)

pkg/k8sutils/const.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
package k8sutils
22

33
const (
4-
AnnotationKeyRecreateStatefulset = "redis.opstreelabs.in/recreate-statefulset"
4+
AnnotationKeyRecreateStatefulset = "redis.opstreelabs.in/recreate-statefulset"
5+
AnnotationKeyRecreateStatefulsetStrategy = "redis.opstreelabs.in/recreate-statefulset-strategy"
56
)
67

78
const (

pkg/k8sutils/redis-cluster.go

+1
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ func generateRedisClusterParams(ctx context.Context, cr *redisv1beta2.RedisClust
7373
}
7474
if value, found := cr.ObjectMeta.GetAnnotations()[AnnotationKeyRecreateStatefulset]; found && value == "true" {
7575
res.RecreateStatefulSet = true
76+
res.RecreateStatefulsetStrategy = getDeletionPropagationStrategy(cr.ObjectMeta.GetAnnotations())
7677
}
7778
return res
7879
}

pkg/k8sutils/redis-replication.go

+1
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ func generateRedisReplicationParams(cr *redisv1beta2.RedisReplication) statefulS
123123
}
124124
if value, found := cr.ObjectMeta.GetAnnotations()[AnnotationKeyRecreateStatefulset]; found && value == "true" {
125125
res.RecreateStatefulSet = true
126+
res.RecreateStatefulsetStrategy = getDeletionPropagationStrategy(cr.ObjectMeta.GetAnnotations())
126127
}
127128
return res
128129
}

pkg/k8sutils/redis-sentinel.go

+1
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ func generateRedisSentinelParams(ctx context.Context, cr *redisv1beta2.RedisSent
119119
}
120120
if value, found := cr.ObjectMeta.GetAnnotations()[AnnotationKeyRecreateStatefulset]; found && value == "true" {
121121
res.RecreateStatefulSet = true
122+
res.RecreateStatefulsetStrategy = getDeletionPropagationStrategy(cr.ObjectMeta.GetAnnotations())
122123
}
123124
return res
124125
}

pkg/k8sutils/redis-standalone.go

+1
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ func generateRedisStandaloneParams(cr *redisv1beta2.Redis) statefulSetParameters
131131
}
132132
if value, found := cr.ObjectMeta.GetAnnotations()[AnnotationKeyRecreateStatefulset]; found && value == "true" {
133133
res.RecreateStatefulSet = true
134+
res.RecreateStatefulsetStrategy = getDeletionPropagationStrategy(cr.ObjectMeta.GetAnnotations())
134135
}
135136
return res
136137
}

pkg/k8sutils/statefulset.go

+30-6
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ type statefulSetParameters struct {
110110
ServiceAccountName *string
111111
UpdateStrategy appsv1.StatefulSetUpdateStrategy
112112
RecreateStatefulSet bool
113+
RecreateStatefulsetStrategy *metav1.DeletionPropagation
113114
TerminationGracePeriodSeconds *int64
114115
IgnoreAnnotations []string
115116
HostNetwork bool
@@ -174,11 +175,11 @@ func CreateOrUpdateStateFul(ctx context.Context, cl kubernetes.Interface, namesp
174175
}
175176
return err
176177
}
177-
return patchStatefulSet(ctx, storedStateful, statefulSetDef, namespace, params.RecreateStatefulSet, cl)
178+
return patchStatefulSet(ctx, storedStateful, statefulSetDef, namespace, params.RecreateStatefulSet, params.RecreateStatefulsetStrategy, cl)
178179
}
179180

180181
// patchStatefulSet patches the Redis StatefulSet by applying changes while maintaining atomicity.
181-
func patchStatefulSet(ctx context.Context, storedStateful, newStateful *appsv1.StatefulSet, namespace string, recreateStatefulSet bool, cl kubernetes.Interface) error {
182+
func patchStatefulSet(ctx context.Context, storedStateful, newStateful *appsv1.StatefulSet, namespace string, recreateStatefulSet bool, deletePropagation *metav1.DeletionPropagation, cl kubernetes.Interface) error {
182183
// Sync system-managed fields to ensure atomic update.
183184
syncManagedFields(storedStateful, newStateful)
184185

@@ -223,7 +224,7 @@ func patchStatefulSet(ctx context.Context, storedStateful, newStateful *appsv1.S
223224
return err
224225
}
225226

226-
return updateStatefulSet(ctx, cl, namespace, newStateful, recreateStatefulSet)
227+
return updateStatefulSet(ctx, cl, namespace, newStateful, recreateStatefulSet, deletePropagation)
227228
}
228229

229230
// syncManagedFields syncs system-managed fields from the stored object to the new object.
@@ -830,7 +831,7 @@ func createStatefulSet(ctx context.Context, cl kubernetes.Interface, namespace s
830831
}
831832

832833
// updateStatefulSet is a method to update statefulset in Kubernetes
833-
func updateStatefulSet(ctx context.Context, cl kubernetes.Interface, namespace string, stateful *appsv1.StatefulSet, recreateStateFulSet bool) error {
834+
func updateStatefulSet(ctx context.Context, cl kubernetes.Interface, namespace string, stateful *appsv1.StatefulSet, recreateStateFulSet bool, deletePropagation *metav1.DeletionPropagation) error {
834835
_, err := cl.AppsV1().StatefulSets(namespace).Update(context.TODO(), stateful, metav1.UpdateOptions{})
835836
if recreateStateFulSet {
836837
sErr, ok := err.(*apierrors.StatusError)
@@ -840,8 +841,7 @@ func updateStatefulSet(ctx context.Context, cl kubernetes.Interface, namespace s
840841
failMsg[messageCount] = cause.Message
841842
}
842843
log.FromContext(ctx).V(1).Info("recreating StatefulSet because the update operation wasn't possible", "reason", strings.Join(failMsg, ", "))
843-
propagationPolicy := metav1.DeletePropagationForeground
844-
if err := cl.AppsV1().StatefulSets(namespace).Delete(context.TODO(), stateful.GetName(), metav1.DeleteOptions{PropagationPolicy: &propagationPolicy}); err != nil { //nolint:gocritic
844+
if err := cl.AppsV1().StatefulSets(namespace).Delete(context.TODO(), stateful.GetName(), metav1.DeleteOptions{PropagationPolicy: deletePropagation}); err != nil { //nolint:gocritic
845845
return errors.Wrap(err, "failed to delete StatefulSet to avoid forbidden action")
846846
}
847847
}
@@ -874,3 +874,27 @@ func getSidecars(sidecars *[]redisv1beta2.Sidecar) []redisv1beta2.Sidecar {
874874
}
875875
return *sidecars
876876
}
877+
878+
// getDeletionPropagationStrategy returns the deletion propagation strategy based on the annotation
879+
func getDeletionPropagationStrategy(annotations map[string]string) *metav1.DeletionPropagation {
880+
if annotations == nil {
881+
return nil
882+
}
883+
884+
if strategy, exists := annotations[AnnotationKeyRecreateStatefulsetStrategy]; exists {
885+
var propagation metav1.DeletionPropagation
886+
887+
switch strings.ToLower(strategy) {
888+
case "orphan":
889+
propagation = metav1.DeletePropagationOrphan
890+
case "background":
891+
propagation = metav1.DeletePropagationBackground
892+
default:
893+
propagation = metav1.DeletePropagationForeground
894+
}
895+
896+
return &propagation
897+
}
898+
899+
return nil
900+
}

pkg/k8sutils/statefulset_test.go

+8-7
Original file line numberDiff line numberDiff line change
@@ -336,12 +336,13 @@ func Test_createStatefulSet(t *testing.T) {
336336

337337
func TestUpdateStatefulSet(t *testing.T) {
338338
tests := []struct {
339-
name string
340-
existingStsSpec appsv1.StatefulSetSpec
341-
updatedStsSpec appsv1.StatefulSetSpec
342-
recreateSts bool
343-
stsPresent bool
344-
expectErr error
339+
name string
340+
existingStsSpec appsv1.StatefulSetSpec
341+
updatedStsSpec appsv1.StatefulSetSpec
342+
recreateSts bool
343+
deletePropagation metav1.DeletionPropagation
344+
stsPresent bool
345+
expectErr error
345346
}{
346347
{
347348
name: "Update StatefulSet without recreate in existing Statefulset",
@@ -439,7 +440,7 @@ func TestUpdateStatefulSet(t *testing.T) {
439440
} else {
440441
client = k8sClientFake.NewSimpleClientset()
441442
}
442-
err := updateStatefulSet(context.TODO(), client, updatedSts.GetNamespace(), &updatedSts, test.recreateSts)
443+
err := updateStatefulSet(context.TODO(), client, updatedSts.GetNamespace(), &updatedSts, test.recreateSts, &test.deletePropagation)
443444
if test.expectErr != nil {
444445
assert.Error(err, "Expected Error while updating Statefulset")
445446
assert.Equal(test.expectErr, err)

0 commit comments

Comments
 (0)