Skip to content

Commit 32e90c8

Browse files
authored
Add resizing operation (#254)
2 parents 197ac6e + 6121321 commit 32e90c8

File tree

10 files changed

+417
-39
lines changed

10 files changed

+417
-39
lines changed

api/v1alpha1/etcdcluster_webhook.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,11 +107,26 @@ func (r *EtcdCluster) ValidateUpdate(old runtime.Object) (admission.Warnings, er
107107
etcdclusterlog.Info("validate update", "name", r.Name)
108108
var warnings admission.Warnings
109109
oldCluster := old.(*EtcdCluster)
110+
111+
// Check if replicas are being resized
110112
if *oldCluster.Spec.Replicas != *r.Spec.Replicas {
111113
warnings = append(warnings, "cluster resize is not currently supported")
112114
}
113115

114116
var allErrors field.ErrorList
117+
118+
// Check if storage size is being decreased
119+
oldStorage := oldCluster.Spec.Storage.VolumeClaimTemplate.Spec.Resources.Requests[corev1.ResourceStorage]
120+
newStorage := r.Spec.Storage.VolumeClaimTemplate.Spec.Resources.Requests[corev1.ResourceStorage]
121+
if newStorage.Cmp(oldStorage) < 0 {
122+
allErrors = append(allErrors, field.Invalid(
123+
field.NewPath("spec", "storage", "volumeClaimTemplate", "resources", "requests", "storage"),
124+
newStorage.String(),
125+
"decreasing storage size is not allowed"),
126+
)
127+
}
128+
129+
// Check if storage type is changing
115130
if oldCluster.Spec.Storage.EmptyDir == nil && r.Spec.Storage.EmptyDir != nil ||
116131
oldCluster.Spec.Storage.EmptyDir != nil && r.Spec.Storage.EmptyDir == nil {
117132
allErrors = append(allErrors, field.Invalid(
@@ -121,6 +136,7 @@ func (r *EtcdCluster) ValidateUpdate(old runtime.Object) (admission.Warnings, er
121136
)
122137
}
123138

139+
// Validate PodDisruptionBudget
124140
pdbWarnings, pdbErr := r.validatePdb()
125141
if pdbErr != nil {
126142
allErrors = append(allErrors, pdbErr...)
@@ -129,11 +145,13 @@ func (r *EtcdCluster) ValidateUpdate(old runtime.Object) (admission.Warnings, er
129145
warnings = append(warnings, pdbWarnings...)
130146
}
131147

148+
// Validate Security
132149
securityErr := r.validateSecurity()
133150
if securityErr != nil {
134151
allErrors = append(allErrors, securityErr...)
135152
}
136153

154+
// Validate Options
137155
if errOptions := validateOptions(r); errOptions != nil {
138156
allErrors = append(allErrors, field.Invalid(
139157
field.NewPath("spec", "options"),

api/v1alpha1/etcdcluster_webhook_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,46 @@ var _ = Describe("EtcdCluster Webhook", func() {
111111
}
112112
})
113113

114+
It("Should reject decreasing storage size", func() {
115+
etcdCluster := &EtcdCluster{
116+
Spec: EtcdClusterSpec{
117+
Replicas: ptr.To(int32(1)),
118+
Storage: StorageSpec{
119+
VolumeClaimTemplate: EmbeddedPersistentVolumeClaim{
120+
Spec: corev1.PersistentVolumeClaimSpec{
121+
Resources: corev1.VolumeResourceRequirements{
122+
Requests: map[corev1.ResourceName]resource.Quantity{
123+
corev1.ResourceStorage: resource.MustParse("5Gi"),
124+
},
125+
},
126+
},
127+
},
128+
},
129+
},
130+
}
131+
oldCluster := &EtcdCluster{
132+
Spec: EtcdClusterSpec{
133+
Replicas: ptr.To(int32(1)),
134+
Storage: StorageSpec{
135+
VolumeClaimTemplate: EmbeddedPersistentVolumeClaim{
136+
Spec: corev1.PersistentVolumeClaimSpec{
137+
Resources: corev1.VolumeResourceRequirements{
138+
Requests: map[corev1.ResourceName]resource.Quantity{
139+
corev1.ResourceStorage: resource.MustParse("10Gi"),
140+
},
141+
},
142+
},
143+
},
144+
},
145+
},
146+
}
147+
_, err := etcdCluster.ValidateUpdate(oldCluster)
148+
if Expect(err).To(HaveOccurred()) {
149+
statusErr := err.(*errors.StatusError)
150+
Expect(statusErr.ErrStatus.Message).To(ContainSubstring("decreasing storage size is not allowed"))
151+
}
152+
})
153+
114154
It("Should allow changing emptydir size", func() {
115155
etcdCluster := &EtcdCluster{
116156
Spec: EtcdClusterSpec{

charts/etcd-operator/templates/rbac/clusterrole-manager-role.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,22 @@ rules:
5757
- patch
5858
- update
5959
- watch
60+
- apiGroups:
61+
- ""
62+
resources:
63+
- persistentvolumeclaims
64+
verbs:
65+
- get
66+
- list
67+
- patch
68+
- watch
69+
- apiGroups:
70+
- "storage.k8s.io"
71+
resources:
72+
- storageclasses
73+
verbs:
74+
- get
75+
- list
6076
- apiGroups:
6177
- etcd.aenix.io
6278
resources:

config/rbac/role.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,15 @@ rules:
2424
- get
2525
- list
2626
- watch
27+
- apiGroups:
28+
- ""
29+
resources:
30+
- persistentvolumeclaims
31+
verbs:
32+
- get
33+
- list
34+
- patch
35+
- watch
2736
- apiGroups:
2837
- ""
2938
resources:
@@ -94,3 +103,10 @@ rules:
94103
- patch
95104
- update
96105
- watch
106+
- apiGroups:
107+
- storage.k8s.io
108+
resources:
109+
- storageclasses
110+
verbs:
111+
- get
112+
- list

examples/manifests/etcdcluster-persistent.yaml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,13 @@ apiVersion: etcd.aenix.io/v1alpha1
33
kind: EtcdCluster
44
metadata:
55
name: test
6-
namespace: default
76
spec:
87
replicas: 3
98
storage:
109
volumeClaimTemplate:
1110
spec:
12-
storageClassName: gp3
11+
storageClassName: standard-with-expansion
1312
accessModes: [ "ReadWriteOnce" ]
1413
resources:
1514
requests:
16-
storage: 10Gi
15+
storage: 4Gi

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ require (
2626
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
2727
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
2828
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
29+
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
2930
github.com/evanphx/json-patch/v5 v5.9.0 // indirect
3031
github.com/fsnotify/fsnotify v1.7.0 // indirect
3132
github.com/go-logr/zapr v1.3.0 // indirect

internal/controller/etcdcluster_controller.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ type EtcdClusterReconciler struct {
6969
// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch
7070
// +kubebuilder:rbac:groups="apps",resources=statefulsets,verbs=get;create;delete;update;patch;list;watch
7171
// +kubebuilder:rbac:groups="policy",resources=poddisruptionbudgets,verbs=get;create;delete;update;patch;list;watch
72+
// +kubebuilder:rbac:groups="",resources=persistentvolumeclaims,verbs=get;list;patch;watch
73+
// +kubebuilder:rbac:groups=storage.k8s.io,resources=storageclasses,verbs=get;list
7274

7375
// Reconcile checks CR and current cluster state and performs actions to transform current state to desired.
7476
func (r *EtcdClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
@@ -168,6 +170,11 @@ func (r *EtcdClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request)
168170
)
169171
}
170172

173+
// if size is different we have to remove statefulset it will be recreated in the next step
174+
if err := r.checkAndDeleteStatefulSetIfNecessary(ctx, &state, instance); err != nil {
175+
return ctrl.Result{}, err
176+
}
177+
171178
// ensure managed resources
172179
if err = r.ensureConditionalClusterObjects(ctx, instance); err != nil {
173180
return r.updateStatusOnErr(ctx, instance, fmt.Errorf("cannot create Cluster auxiliary objects: %w", err))
@@ -231,6 +238,28 @@ func (r *EtcdClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request)
231238
return r.updateStatus(ctx, instance)
232239
}
233240

241+
// checkAndDeleteStatefulSetIfNecessary deletes the StatefulSet if the specified storage size has changed.
242+
func (r *EtcdClusterReconciler) checkAndDeleteStatefulSetIfNecessary(ctx context.Context, state *observables, instance *etcdaenixiov1alpha1.EtcdCluster) error {
243+
for _, volumeClaimTemplate := range state.statefulSet.Spec.VolumeClaimTemplates {
244+
if volumeClaimTemplate.Name != "data" {
245+
continue
246+
}
247+
currentStorage := volumeClaimTemplate.Spec.Resources.Requests[corev1.ResourceStorage]
248+
desiredStorage := instance.Spec.Storage.VolumeClaimTemplate.Spec.Resources.Requests[corev1.ResourceStorage]
249+
if desiredStorage.Cmp(currentStorage) != 0 {
250+
deletePolicy := metav1.DeletePropagationOrphan
251+
log.Info(ctx, "Deleting StatefulSet due to storage change", "statefulSet", state.statefulSet.Name)
252+
err := r.Delete(ctx, &state.statefulSet, &client.DeleteOptions{PropagationPolicy: &deletePolicy})
253+
if err != nil {
254+
log.Error(ctx, err, "Failed to delete StatefulSet")
255+
return err
256+
}
257+
return nil
258+
}
259+
}
260+
return nil
261+
}
262+
234263
// ensureConditionalClusterObjects creates or updates all objects owned by cluster CR
235264
func (r *EtcdClusterReconciler) ensureConditionalClusterObjects(
236265
ctx context.Context, cluster *etcdaenixiov1alpha1.EtcdCluster) error {
@@ -245,6 +274,11 @@ func (r *EtcdClusterReconciler) ensureConditionalClusterObjects(
245274
log.Error(ctx, err, "reconcile statefulset failed")
246275
return err
247276
}
277+
278+
if err := factory.UpdatePersistentVolumeClaims(ctx, cluster, r.Client); err != nil {
279+
log.Error(ctx, err, "reconcile persistentVolumeClaims failed")
280+
return err
281+
}
248282
log.Debug(ctx, "statefulset reconciled")
249283

250284
return nil

internal/controller/factory/pvc.go

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,19 @@ limitations under the License.
1616

1717
package factory
1818

19-
import etcdaenixiov1alpha1 "github.com/aenix-io/etcd-operator/api/v1alpha1"
19+
import (
20+
"context"
21+
"fmt"
22+
"strings"
23+
24+
corev1 "k8s.io/api/core/v1"
25+
"sigs.k8s.io/controller-runtime/pkg/client"
26+
27+
etcdaenixiov1alpha1 "github.com/aenix-io/etcd-operator/api/v1alpha1"
28+
storagev1 "k8s.io/api/storage/v1"
29+
"k8s.io/apimachinery/pkg/labels"
30+
"k8s.io/apimachinery/pkg/types"
31+
)
2032

2133
func GetPVCName(cluster *etcdaenixiov1alpha1.EtcdCluster) string {
2234
if len(cluster.Spec.Storage.VolumeClaimTemplate.Name) > 0 {
@@ -25,3 +37,57 @@ func GetPVCName(cluster *etcdaenixiov1alpha1.EtcdCluster) string {
2537
//nolint:goconst
2638
return "data"
2739
}
40+
41+
// UpdatePersistentVolumeClaims checks and updates the sizes of PVCs in an EtcdCluster if the specified storage size is larger than the current.
42+
func UpdatePersistentVolumeClaims(ctx context.Context, cluster *etcdaenixiov1alpha1.EtcdCluster, rclient client.Client) error {
43+
labelSelector := labels.SelectorFromSet(labels.Set{
44+
"app.kubernetes.io/instance": cluster.Name,
45+
})
46+
listOptions := &client.ListOptions{
47+
Namespace: cluster.Namespace,
48+
LabelSelector: labelSelector,
49+
}
50+
51+
// List all PVCs in the same namespace as the cluster using the label selector
52+
pvcList := &corev1.PersistentVolumeClaimList{}
53+
err := rclient.List(ctx, pvcList, listOptions)
54+
if err != nil {
55+
return fmt.Errorf("failed to list PVCs: %w", err)
56+
}
57+
58+
// Desired size from the cluster spec
59+
expectedPrefix := fmt.Sprintf("data-%s-", cluster.Name)
60+
desiredSize := cluster.Spec.Storage.VolumeClaimTemplate.Spec.Resources.Requests[corev1.ResourceStorage]
61+
62+
for _, pvc := range pvcList.Items {
63+
// Skip if PVC name does not match expected prefix
64+
if !strings.HasPrefix(pvc.Name, expectedPrefix) {
65+
continue
66+
}
67+
68+
// Skip if specified StorageClass does not support volume expansion
69+
if pvc.Spec.StorageClassName != nil {
70+
sc := &storagev1.StorageClass{}
71+
scName := *pvc.Spec.StorageClassName
72+
err := rclient.Get(ctx, types.NamespacedName{Name: scName}, sc)
73+
if err != nil {
74+
return fmt.Errorf("failed to get StorageClass '%s' for PVC '%s': %w", scName, pvc.Name, err)
75+
}
76+
if sc.AllowVolumeExpansion == nil || !*sc.AllowVolumeExpansion {
77+
continue
78+
}
79+
}
80+
81+
currentSize := pvc.Spec.Resources.Requests[corev1.ResourceStorage]
82+
// Only patch if the desired size is greater than the current size
83+
if desiredSize.Cmp(currentSize) == 1 {
84+
newSizePatch := []byte(fmt.Sprintf(`{"spec": {"resources": {"requests": {"storage": "%s"}}}}`, desiredSize.String()))
85+
err = rclient.Patch(ctx, &pvc, client.RawPatch(types.StrategicMergePatchType, newSizePatch))
86+
if err != nil {
87+
return fmt.Errorf("failed to patch PVC %s for updated size: %w", pvc.Name, err)
88+
}
89+
}
90+
}
91+
92+
return nil
93+
}

0 commit comments

Comments
 (0)