Skip to content

Commit 5820447

Browse files
committed
Add integration tests, adapt docs
1 parent 567c392 commit 5820447

File tree

2 files changed

+134
-43
lines changed

2 files changed

+134
-43
lines changed
+26-11
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,36 @@
11
# CRD Validations for etcd
22

33
* The validations for the fields within the `etcd` resource are done via [kubebuilder markers for CRD validation](https://book.kubebuilder.io/reference/markers/crd-validation).
4-
* The validations for clusters with kubernetes versions `>= 1.29` are written using a combination of [CEL expressions](https://kubernetes.io/docs/reference/using-api/cel/) via the `x-validation` tag which provides a straightforward syntax to write validation rules for the fields, and pattern matching with the use of the `validation` tag.
4+
* The validations for clusters with kubernetes versions `>= 1.29` are written using a combination of [CEL expressions](https://kubernetes.io/docs/reference/using-api/cel/) via the `XValidation` kubebuilder tag which provides a straightforward syntax to write validation rules for the fields, and pattern matching with the use of the `validation` kubebuilder tag.
55
* The validations for clusters with kubernetes versions `< 1.29` will not contain validations via `CEL` expressions since this is GA for kubernetes version 1.29 or higher.
66
* Upon any changes to the validation rules to the etcd resource, the `yaml` files for the same can be generated by running the `make generate` command.
77

88
## Validation rules:
99
### Type Validation rules:
1010
The validations for fields of types `Duration`(metav1.Duration) and `cron` expressions are done via `regex` matching. These use the `validation:Pattern` marker.(The checking for the `Quantity`(resource.Quantity) fields are done by default, hence, no explicit validation is needed for the fields of this type):
1111
* Duration fields:
12-
`'^([0-9]+([.][0-9]+)?h)?([0-9]+([.][0-9]+)?m)?([0-9]+([.][0-9]+)?s)?([0-9]+([.][0-9]+)?d)?$')`
12+
```
13+
'^([0-9]+([.][0-9]+)?h)?([0-9]+([.][0-9]+)?m)?([0-9]+([.][0-9]+)?s)?([0-9]+([.][0-9]+)?d)?$')
14+
```
1315
* Cron expression:
14-
`^(\*|[1-5]?[0-9]|[1-5]?[0-9]-[1-5]?[0-9]|(?:[1-9]|[1-4][0-9]|5[0-9])\/(?:[1-9]|[1-4][0-9]|5[0-9]|60)|\*\/(?:[1-9]|[1-4][0-9]|5[0-9]|60))\s+(\*|[0-9]|1[0-9]|2[0-3]|[0-9]-(?:[0-9]|1[0-9]|2[0-3])|1[0-9]-(?:1[0-9]|2[0-3])|2[0-3]-2[0-3]|(?:[1-9]|1[0-9]|2[0-3])\/(?:[1-9]|1[0-9]|2[0-4])|\*\/(?:[1-9]|1[0-9]|2[0-4]))\s+(\*|[1-9]|[12][0-9]|3[01]|[1-9]-(?:[1-9]|[12][0-9]|3[01])|[12][0-9]-(?:[12][0-9]|3[01])|3[01]-3[01]|(?:[1-9]|[12][0-9]|30)\/(?:[1-9]|[12][0-9]|3[01])|\*\/(?:[1-9]|[12][0-9]|3[01]))\s+(\*|[1-9]|1[0-2]|[1-9]-(?:[1-9]|1[0-2])|1[0-2]-1[0-2]|(?:[1-9]|1[0-2])\/(?:[1-9]|1[0-2])|\*\/(?:[1-9]|1[0-2]))\s+(\*|[1-7]|[1-6]-[1-7]|[1-6]\/[1-7]|\*\/[1-7])$`
16+
```
17+
^(\*|[1-5]?[0-9]|[1-5]?[0-9]-[1-5]?[0-9]|(?:[1-9]|[1-4][0-9]|5[0-9])\/(?:[1-9]|[1-4][0-9]|5[0-9]|60)|\*\/(?:[1-9]|[1-4][0-9]|5[0-9]|60))\s+(\*|[0-9]|1[0-9]|2[0-3]|[0-9]-(?:[0-9]|1[0-9]|2[0-3])|1[0-9]-(?:1[0-9]|2[0-3])|2[0-3]-2[0-3]|(?:[1-9]|1[0-9]|2[0-3])\/(?:[1-9]|1[0-9]|2[0-4])|\*\/(?:[1-9]|1[0-9]|2[0-4]))\s+(\*|[1-9]|[12][0-9]|3[01]|[1-9]-(?:[1-9]|[12][0-9]|3[01])|[12][0-9]-(?:[12][0-9]|3[01])|3[01]-3[01]|(?:[1-9]|[12][0-9]|30)\/(?:[1-9]|[12][0-9]|3[01])|\*\/(?:[1-9]|[12][0-9]|3[01]))\s+(\*|[1-9]|1[0-2]|[1-9]-(?:[1-9]|1[0-2])|1[0-2]-1[0-2]|(?:[1-9]|1[0-2])\/(?:[1-9]|1[0-2])|\*\/(?:[1-9]|1[0-2]))\s+(\*|[1-7]|[1-6]-[1-7]|[1-6]\/[1-7]|\*\/[1-7])$
18+
```
1519

1620
* **NOTE**: The provided regex does not account for `special strings` such as `@yearly` or `@monthly`. Additionally, it fails to invalidate cases involving the `step operator (x/y)` and the `range operator (x-y)`, where the cron expression is considered valid even if `x > y`. Please ensure these values are validated before passing the expression.
1721

1822
### Update validations
1923
These validations are triggered when an update operation is done on the etcd resource.
20-
* Immutable fields: The fields `etcd.spec.StorageClass` , `etcd.spec.StorageCapacity` and `etcd.spec.VolumeClaimTemplate` are immutable. The immutability is enforced by the CEL expression : `self == oldSelf`.
24+
* Immutable fields: The fields `etcd.spec.StorageClass` , `etcd.spec.StorageCapacity` and `etcd.spec.VolumeClaimTemplate` are immutable. The immutability is enforced by the CEL expression:
25+
```
26+
self == oldSelf
27+
```
2128

22-
* The value set for the field `etcd.spec.replicas` can either be decreased to `0` or increased. This is enforced by the CEL expression:
23-
`self==0 ? true : self < oldSelf ? false : true`
29+
* The value set for the field `etcd.spec.replicas` can either be decreased to `0` or increased, since etcd-druid does not yet support scaling down of etcd cluster size. While scaling up a hibernated etcd cluster from 0 replicas, the `etcd.status.clusterSize` field is checked to ensure that replicas can only be set to the previously recorded cluster size and not higher or lower. This is enforced by the CEL expression:
30+
```
31+
self.spec.replicas == 0 || (oldSelf.spec.replicas == 0 ? (!has(self.status.clusterSize) || (self.spec.replicas == self.status.clusterSize)) : (self.spec.replicas >= oldSelf.spec.replicas))
32+
```
33+
Hibernating the etcd cluster to 0 replicas is always allowed. If replicas field is changed from a non-zero value to another non-zero value, then the rule ensures that the replicas cannot be decreased, since down-scaling of etcd clusters is not currently supported. If the cluster is already hibernated and is attempted to be scaled up, then `etcd.status.clusterSize` field is checked to ensure that replicas can only be set to the previously recorded etcd cluster size. This is required to ensure that the scale-up logic is allowed to be executed correctly. If `etcd.status.clusterSize` is not set, then it is assumed that the etcd cluster has not been created yet and the replicas can be set to any value.
2434

2535
### Field validations
2636
- The fields which expect only a particular set of values are checked by using the kubebuilder marker: `+kubebuilder:validation:Enum=<value1>;<value2>`
@@ -30,9 +40,14 @@ These validations are triggered when an update operation is done on the etcd res
3040
* The `etcd.spec.sharedConfig.autoCompactionMode` can only be set as either `periodic` or `revision`.
3141

3242

33-
* The value of `etcd.spec.backup.garbageCollectionPeriod` must be greater than `etcd.spec.backup.deltaSnapshotPeriod`. This is enforced by the CEL expression
34-
`!(has(self.deltaSnapshotPeriod) && has(self.garbageCollectionPeriod)) || duration(self.deltaSnapshotPeriod).getSeconds() < duration(self.garbageCollectionPeriod).getSeconds()`. The first part of the expression ensures that both the fields are present and then compares the values of the garbageCollectionPeriod and deltaSnapshotPeriod fields, if not, skips the check.
43+
* The value of `etcd.spec.backup.garbageCollectionPeriod` must be greater than `etcd.spec.backup.deltaSnapshotPeriod`. This is enforced by the CEL expression:
44+
```
45+
!(has(self.deltaSnapshotPeriod) && has(self.garbageCollectionPeriod)) || duration(self.deltaSnapshotPeriod).getSeconds() < duration(self.garbageCollectionPeriod).getSeconds()
46+
```
47+
The first part of the expression ensures that both the fields are present and then compares the values of the garbageCollectionPeriod and deltaSnapshotPeriod fields, if not, skips the check.
3548

36-
* The value of `etcd.spec.StorageCapacity` must be more than 3 times that of the `etcd.spec.etcd.quota` if backups are enabled. If not, the value must be greater than that of the `etcd.spec.etcd.quota` field. This is enforced by using the CEL expression:
37-
`has(self.storageCapacity) && has(self.etcd.quota) ? (has(self.backup.store) ? quantity(self.storageCapacity).compareTo(quantity(self.etcd.quota).add(quantity(self.etcd.quota)).add(quantity(self.etcd.quota))) > 0 : quantity(self.storageCapacity).compareTo(quantity(self.etcd.quota)) > 0 ): true`
38-
The check for whether backups are enabled or not is done by checking if the field `etcd.spec.backup.store` exists.
49+
* The value of `etcd.spec.StorageCapacity` must be more than 3 times that of the `etcd.spec.etcd.quota` if backups are enabled. If not, the value must be greater than that of the `etcd.spec.etcd.quota` field. This is enforced by using the CEL expression:
50+
```
51+
has(self.storageCapacity) && has(self.etcd.quota) ? (has(self.backup.store) ? quantity(self.storageCapacity).compareTo(quantity(self.etcd.quota).add(quantity(self.etcd.quota)).add(quantity(self.etcd.quota))) > 0 : quantity(self.storageCapacity).compareTo(quantity(self.etcd.quota)) > 0 ): true
52+
```
53+
The check for whether backups are enabled or not is done by checking if the field `etcd.spec.backup.store` exists.

test/it/crdvalidation/etcd/specupdate_test.go

+108-32
Original file line numberDiff line numberDiff line change
@@ -16,38 +16,38 @@ import (
1616
. "github.com/onsi/gomega"
1717
)
1818

19-
// etcd.spec.storageClass is immutable
19+
// TestValidateUpdateSpecStorageClass tests the immutability of etcd.spec.storageClass
2020
func TestValidateUpdateSpecStorageClass(t *testing.T) {
2121
skipCELTestsForOlderK8sVersions(t)
2222
testNs, g := setupTestEnvironment(t)
2323

2424
tests := []struct {
2525
name string
2626
etcdName string
27-
initalStorageClassName string
27+
initialStorageClassName string
2828
updatedStorageClassName string
2929
expectErr bool
3030
}{
3131
{
3232
name: "Valid #1: Unchanged storageClass",
3333
etcdName: "etcd-valid-1",
34-
initalStorageClassName: "gardener.cloud-fast",
35-
updatedStorageClassName: "gardener.cloud-fast",
34+
initialStorageClassName: "storageClass1",
35+
updatedStorageClassName: "storageClass1",
3636
expectErr: false,
3737
},
3838
{
3939
name: "Invalid #1: Updated storageClass",
4040
etcdName: "etcd-invalid-1",
41-
initalStorageClassName: "gardener.cloud-fast",
42-
updatedStorageClassName: "default",
41+
initialStorageClassName: "storageClass1",
42+
updatedStorageClassName: "storageClass2",
4343
expectErr: true,
4444
},
4545
}
4646

4747
for _, test := range tests {
4848
t.Run(test.name, func(t *testing.T) {
4949
etcd := utils.EtcdBuilderWithoutDefaults(test.etcdName, testNs).WithReplicas(3).Build()
50-
etcd.Spec.StorageClass = &test.initalStorageClassName
50+
etcd.Spec.StorageClass = &test.initialStorageClassName
5151

5252
cl := itTestEnv.GetClient()
5353
ctx := context.Background()
@@ -59,76 +59,152 @@ func TestValidateUpdateSpecStorageClass(t *testing.T) {
5959
}
6060
}
6161

62-
// checks the update on the etcd.spec.replicas field
62+
// TestValidateUpdateSpecReplicas tests the update on the etcd.spec.replicas field
6363
func TestValidateUpdateSpecReplicas(t *testing.T) {
6464
skipCELTestsForOlderK8sVersions(t)
65-
tests := []struct {
65+
testCases := []struct {
6666
name string
6767
etcdName string
68-
initialReplicas int
69-
updatedReplicas int
68+
initialReplicas int32
69+
updatedReplicas int32
70+
clusterSize int32
7071
expectErr bool
7172
}{
7273
{
73-
name: "Valid updation of replicas #1",
74-
etcdName: "etcd-valid-inc",
74+
// allow etcd cluster to be unhibernated
75+
name: "0 -> n, where n > 0 and clusterSize = n",
76+
etcdName: "etcd-valid-replicas-1",
77+
initialReplicas: 0,
78+
updatedReplicas: 3,
79+
clusterSize: 3,
80+
expectErr: false,
81+
},
82+
{
83+
// etcd cluster can be unhibernated only to the size of the cluster, to allow scale-up logic to run
84+
name: "0 -> n, where n > 0 and clusterSize > n",
85+
etcdName: "etcd-invalid-replicas-1",
86+
initialReplicas: 0,
87+
updatedReplicas: 3,
88+
clusterSize: 5,
89+
expectErr: true,
90+
},
91+
{
92+
// etcd cluster can be unhibernated only to the size of the cluster, because the cluster cannot be scale down
93+
name: "0 -> m, where m > 0 and clusterSize < m",
94+
etcdName: "etcd-invalid-replicas-2",
95+
initialReplicas: 0,
96+
updatedReplicas: 3,
97+
clusterSize: 5,
98+
expectErr: true,
99+
},
100+
{
101+
// allow hibernated etcd cluster to continue being hibernated
102+
name: "0 -> 0, where clusterSize = 0 (etcd cluster not started)",
103+
etcdName: "etcd-valid-replicas-2",
104+
initialReplicas: 0,
105+
updatedReplicas: 0,
106+
clusterSize: 0,
107+
expectErr: false,
108+
},
109+
{
110+
// allow hibernated etcd cluster to continue being hibernated
111+
name: "0 -> 0, where clusterSize != 0 (etcd cluster already started)",
112+
etcdName: "etcd-invalid-replicas-3",
113+
initialReplicas: 0,
114+
updatedReplicas: 0,
115+
clusterSize: 1,
116+
expectErr: false,
117+
},
118+
{
119+
// etcd replicas remains the same as cluster size
120+
name: "n -> n, where n > 0",
121+
etcdName: "etcd-valid-replicas-3",
75122
initialReplicas: 3,
123+
updatedReplicas: 3,
124+
clusterSize: 3,
125+
expectErr: false,
126+
},
127+
{
128+
// etcd replicas remains the same, but does not match cluster size
129+
name: "n -> n, where n > 0 and clusterSize != n",
130+
etcdName: "etcd-valid-replicas-4",
131+
initialReplicas: 5,
76132
updatedReplicas: 5,
133+
clusterSize: 3,
77134
expectErr: false,
78135
},
79136
{
80-
name: "Valid updation of replicas #2",
81-
etcdName: "etcd-valid-zero",
137+
// scale up etcd cluster
138+
name: "n -> m, where m > n > 0",
139+
etcdName: "etcd-valid-replicas-5",
140+
initialReplicas: 3,
141+
updatedReplicas: 5,
142+
clusterSize: 3,
143+
expectErr: false,
144+
},
145+
{
146+
// hibernate etcd cluster to 0 replicas
147+
name: "m -> 0, where m > 0",
148+
etcdName: "etcd-valid-replica-6",
82149
initialReplicas: 3,
83150
updatedReplicas: 0,
151+
clusterSize: 3,
84152
expectErr: false,
85153
},
86154
{
87-
name: "Invalid updation of replicas #1",
88-
etcdName: "etcd-invalid-dec",
155+
// etcd cluster cannot be scaled down
156+
name: "m -> n, where m > n > 0",
157+
etcdName: "etcd-invalid-replicas-4",
89158
initialReplicas: 5,
90159
updatedReplicas: 3,
160+
clusterSize: 5,
91161
expectErr: true,
92162
},
93163
}
94164

95165
testNs, g := setupTestEnvironment(t)
166+
t.Parallel()
96167

97-
for _, test := range tests {
98-
t.Run(test.name, func(t *testing.T) {
99-
etcd := utils.EtcdBuilderWithoutDefaults(test.etcdName, testNs).WithReplicas(int32(test.initialReplicas)).Build()
168+
for _, tc := range testCases {
169+
t.Run(tc.name, func(t *testing.T) {
170+
etcd := utils.EtcdBuilderWithoutDefaults(tc.etcdName, testNs).
171+
WithReplicas(tc.initialReplicas).
172+
Build()
100173
cl := itTestEnv.GetClient()
101174
ctx := context.Background()
102175
g.Expect(cl.Create(ctx, etcd)).To(Succeed())
176+
// update status subresource, since status is not created in the object Create() call
177+
etcd.Status.ClusterSize = tc.clusterSize
178+
g.Expect(cl.Status().Update(ctx, etcd)).To(Succeed())
103179

104-
etcd.Spec.Replicas = int32(test.updatedReplicas)
105-
validateEtcdUpdation(t, g, etcd, test.expectErr, ctx, cl)
180+
etcd.Spec.Replicas = tc.updatedReplicas
181+
validateEtcdUpdation(t, g, etcd, tc.expectErr, ctx, cl)
106182
})
107183
}
108184
}
109185

110-
// checks the immutablility of the etcd.spec.StorageCapacity field
186+
// TestValidateUpdateSpecStorageCapacity tests the immutablility of the etcd.spec.StorageCapacity field
111187
func TestValidateUpdateSpecStorageCapacity(t *testing.T) {
112188
skipCELTestsForOlderK8sVersions(t)
113189
testNs, g := setupTestEnvironment(t)
114190
tests := []struct {
115191
name string
116192
etcdName string
117-
initalStorageCapacity resource.Quantity
193+
initialStorageCapacity resource.Quantity
118194
updatedStorageCapacity resource.Quantity
119195
expectErr bool
120196
}{
121197
{
122198
name: "Valid #1: Unchanged storageCapacity",
123199
etcdName: "etcd-valid-1-storagecap",
124-
initalStorageCapacity: resource.MustParse("25Gi"),
200+
initialStorageCapacity: resource.MustParse("25Gi"),
125201
updatedStorageCapacity: resource.MustParse("25Gi"),
126202
expectErr: false,
127203
},
128204
{
129205
name: "Invalid #1: Updated storageCapacity",
130206
etcdName: "etcd-invalid-1-storagecap",
131-
initalStorageCapacity: resource.MustParse("15Gi"),
207+
initialStorageCapacity: resource.MustParse("15Gi"),
132208
updatedStorageCapacity: resource.MustParse("20Gi"),
133209
expectErr: true,
134210
},
@@ -137,7 +213,7 @@ func TestValidateUpdateSpecStorageCapacity(t *testing.T) {
137213
for _, test := range tests {
138214
t.Run(test.name, func(t *testing.T) {
139215
etcd := utils.EtcdBuilderWithoutDefaults(test.etcdName, testNs).WithReplicas(3).Build()
140-
etcd.Spec.StorageCapacity = &test.initalStorageCapacity
216+
etcd.Spec.StorageCapacity = &test.initialStorageCapacity
141217

142218
cl := itTestEnv.GetClient()
143219
ctx := context.Background()
@@ -149,28 +225,28 @@ func TestValidateUpdateSpecStorageCapacity(t *testing.T) {
149225
}
150226
}
151227

152-
// check the immutability of the etcd.spec.VolumeClaimTemplate field
228+
// TestValidateUpdateSpecVolumeClaimTemplate tests the immutability of the etcd.spec.VolumeClaimTemplate field
153229
func TestValidateUpdateSpecVolumeClaimTemplate(t *testing.T) {
154230
skipCELTestsForOlderK8sVersions(t)
155231
testNs, g := setupTestEnvironment(t)
156232
tests := []struct {
157233
name string
158234
etcdName string
159-
initalVolClaimTemp string
235+
initialVolClaimTemp string
160236
updatedVolClaimTemp string
161237
expectErr bool
162238
}{
163239
{
164240
name: "Valid #1: Unchanged volumeClaimTemplate",
165241
etcdName: "etcd-valid-1-volclaim",
166-
initalVolClaimTemp: "main-etcd",
242+
initialVolClaimTemp: "main-etcd",
167243
updatedVolClaimTemp: "main-etcd",
168244
expectErr: false,
169245
},
170246
{
171247
name: "Invalid #1: Updated storageCapacity",
172248
etcdName: "etcd-invalid-1-volclaim",
173-
initalVolClaimTemp: "main-etcd",
249+
initialVolClaimTemp: "main-etcd",
174250
updatedVolClaimTemp: "new-vol-temp",
175251
expectErr: true,
176252
},
@@ -179,7 +255,7 @@ func TestValidateUpdateSpecVolumeClaimTemplate(t *testing.T) {
179255
for _, test := range tests {
180256
t.Run(test.name, func(t *testing.T) {
181257
etcd := utils.EtcdBuilderWithoutDefaults(test.etcdName, testNs).WithReplicas(3).Build()
182-
etcd.Spec.VolumeClaimTemplate = &test.initalVolClaimTemp
258+
etcd.Spec.VolumeClaimTemplate = &test.initialVolClaimTemp
183259

184260
cl := itTestEnv.GetClient()
185261
ctx := context.Background()

0 commit comments

Comments
 (0)