Skip to content

Commit 6bb3dc7

Browse files
authored
Add support for VolumeClaimDeletePolicies for Elasticsearch clusters (#4050)
Approach inspired by kubernetes/enhancements#1915 Adds an new volumeClaimDeletePolicy to the Elasticsearch Spec on the cluster level (not per NodeSet) apiVersion: elasticsearch.k8s.elastic.co/v1 kind: Elasticsearch metadata: name: es spec: version: 7.10.1 volumeClaimDeletePolicy: DeleteOnScaledownAndClusterDeletion nodeSets: - name: default count: 2 Possible values are DeleteOnScaledownAndClusterDeletion (default), DeleteOnScaledownOnly. RemoveOnScaledownAndClusterDeletion relies on an owner reference pointing to the Elasticsearch resource to garbage collect PVCs once the Elasticsearch cluster has been deleted (existing behaviour). It also runs additional garbage collection to remove PVCs on each reconciliation that are no longer in use because either the whole node set has been removed or individual nodes have been scaled down (existing behaviour). RemoveOnScaledownOnly means the PVCs are kept around after the cluster has been deleted. This is implemented by removing the owner reference. Removal of PVCs on scale down happens as before. Switching from one to the other strategy is allowed and is implemented by avoiding the StatefulSet templating mechanism. This is mainly because the PVC template in StatefulSets are considered immutable and it would require StatefulSets to be recreated in order to change the PVC ownership. Instead the operator edits the PVCs after they have been created by the StatefulSet controller.
1 parent ce4b20c commit 6bb3dc7

File tree

22 files changed

+688
-416
lines changed

22 files changed

+688
-416
lines changed

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,7 @@ e2e-local: LOCAL_E2E_CTX := /tmp/e2e-local.json
460460
e2e-local:
461461
@go run test/e2e/cmd/main.go run \
462462
--test-run-name=e2e \
463+
--operator-image=$(OPERATOR_IMAGE) \
463464
--test-context-out=$(LOCAL_E2E_CTX) \
464465
--test-license=$(TEST_LICENSE) \
465466
--test-license-pkey-path=$(TEST_LICENSE_PKEY_PATH) \

config/crds/all-crds.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2387,6 +2387,14 @@ spec:
23872387
version:
23882388
description: Version of Elasticsearch.
23892389
type: string
2390+
volumeClaimDeletePolicy:
2391+
description: VolumeClaimDeletePolicy sets the policy for handling deletion
2392+
of PersistentVolumeClaims for all NodeSets. Possible values are DeleteOnScaledownOnly
2393+
and DeleteOnScaledownAndClusterDeletion. Defaults to DeleteOnScaledownAndClusterDeletion.
2394+
enum:
2395+
- DeleteOnScaledownOnly
2396+
- DeleteOnScaledownAndClusterDeletion
2397+
type: string
23902398
required:
23912399
- nodeSets
23922400
- version

config/crds/bases/elasticsearch.k8s.elastic.co_elasticsearches.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4418,6 +4418,15 @@ spec:
44184418
version:
44194419
description: Version of Elasticsearch.
44204420
type: string
4421+
volumeClaimDeletePolicy:
4422+
description: VolumeClaimDeletePolicy sets the policy for handling
4423+
deletion of PersistentVolumeClaims for all NodeSets. Possible values
4424+
are DeleteOnScaledownOnly and DeleteOnScaledownAndClusterDeletion.
4425+
Defaults to DeleteOnScaledownAndClusterDeletion.
4426+
enum:
4427+
- DeleteOnScaledownOnly
4428+
- DeleteOnScaledownAndClusterDeletion
4429+
type: string
44214430
required:
44224431
- nodeSets
44234432
- version

deploy/eck-operator/charts/eck-operator-crds/templates/all-crds.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2411,6 +2411,14 @@ spec:
24112411
version:
24122412
description: Version of Elasticsearch.
24132413
type: string
2414+
volumeClaimDeletePolicy:
2415+
description: VolumeClaimDeletePolicy sets the policy for handling deletion
2416+
of PersistentVolumeClaims for all NodeSets. Possible values are DeleteOnScaledownOnly
2417+
and DeleteOnScaledownAndClusterDeletion. Defaults to DeleteOnScaledownAndClusterDeletion.
2418+
enum:
2419+
- DeleteOnScaledownOnly
2420+
- DeleteOnScaledownAndClusterDeletion
2421+
type: string
24142422
required:
24152423
- nodeSets
24162424
- version

docs/reference/api-docs.asciidoc

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -895,6 +895,7 @@ ElasticsearchSpec holds the specification of an Elasticsearch cluster.
895895
| *`secureSettings`* __xref:{anchor_prefix}-github-com-elastic-cloud-on-k8s-pkg-apis-common-v1-secretsource[$$SecretSource$$]__ | SecureSettings is a list of references to Kubernetes secrets containing sensitive configuration options for Elasticsearch.
896896
| *`serviceAccountName`* __string__ | ServiceAccountName is used to check access from the current resource to a resource (eg. a remote Elasticsearch cluster) in a different namespace. Can only be used if ECK is enforcing RBAC on references.
897897
| *`remoteClusters`* __xref:{anchor_prefix}-github-com-elastic-cloud-on-k8s-pkg-apis-elasticsearch-v1-remotecluster[$$RemoteCluster$$] array__ | RemoteClusters enables you to establish uni-directional connections to a remote Elasticsearch cluster.
898+
| *`volumeClaimDeletePolicy`* __xref:{anchor_prefix}-github-com-elastic-cloud-on-k8s-pkg-apis-elasticsearch-v1-volumeclaimdeletepolicy[$$VolumeClaimDeletePolicy$$]__ | VolumeClaimDeletePolicy sets the policy for handling deletion of PersistentVolumeClaims for all NodeSets. Possible values are DeleteOnScaledownOnly and DeleteOnScaledownAndClusterDeletion. Defaults to DeleteOnScaledownAndClusterDeletion.
898899
|===
899900

900901

@@ -1030,6 +1031,18 @@ UpdateStrategy specifies how updates to the cluster should be performed.
10301031
|===
10311032

10321033

1034+
[id="{anchor_prefix}-github-com-elastic-cloud-on-k8s-pkg-apis-elasticsearch-v1-volumeclaimdeletepolicy"]
1035+
=== VolumeClaimDeletePolicy (string)
1036+
1037+
VolumeClaimDeletePolicy describes the delete policy for handling PersistentVolumeClaims that hold Elasticsearch data. Inspired by https://github.com/kubernetes/enhancements/pull/2440
1038+
1039+
.Appears In:
1040+
****
1041+
- xref:{anchor_prefix}-github-com-elastic-cloud-on-k8s-pkg-apis-elasticsearch-v1-elasticsearchspec[$$ElasticsearchSpec$$]
1042+
****
1043+
1044+
1045+
10331046

10341047

10351048

pkg/apis/elasticsearch/v1/elasticsearch_types.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,26 @@ type ElasticsearchSpec struct {
6666
// RemoteClusters enables you to establish uni-directional connections to a remote Elasticsearch cluster.
6767
// +optional
6868
RemoteClusters []RemoteCluster `json:"remoteClusters,omitempty"`
69+
70+
// VolumeClaimDeletePolicy sets the policy for handling deletion of PersistentVolumeClaims for all NodeSets.
71+
// Possible values are DeleteOnScaledownOnly and DeleteOnScaledownAndClusterDeletion. Defaults to DeleteOnScaledownAndClusterDeletion.
72+
// +kubebuilder:validation:Optional
73+
// +kubebuilder:validation:Enum=DeleteOnScaledownOnly;DeleteOnScaledownAndClusterDeletion
74+
VolumeClaimDeletePolicy VolumeClaimDeletePolicy `json:"volumeClaimDeletePolicy,omitempty"`
6975
}
7076

77+
// VolumeClaimDeletePolicy describes the delete policy for handling PersistentVolumeClaims that hold Elasticsearch data.
78+
// Inspired by https://github.com/kubernetes/enhancements/pull/2440
79+
type VolumeClaimDeletePolicy string
80+
81+
const (
82+
// DeleteOnScaledownAndClusterDeletionPolicy remove PersistentVolumeClaims when the corresponding Elasticsearch node is removed.
83+
DeleteOnScaledownAndClusterDeletionPolicy VolumeClaimDeletePolicy = "DeleteOnScaledownAndClusterDeletion"
84+
// DeleteOnScaledownOnlyPolicy removes PersistentVolumeClaims on scale down of Elasticsearch nodes but retains all
85+
// current PersistenVolumeClaims when the Elasticsearch cluster has been deleted.
86+
DeleteOnScaledownOnlyPolicy VolumeClaimDeletePolicy = "DeleteOnScaledownOnly"
87+
)
88+
7189
// TransportConfig holds the transport layer settings for Elasticsearch.
7290
type TransportConfig struct {
7391
// Service defines the template for the associated Kubernetes Service object.
@@ -118,6 +136,13 @@ func (es ElasticsearchSpec) NodeCount() int32 {
118136
return count
119137
}
120138

139+
func (es ElasticsearchSpec) VolumeClaimDeletePolicyOrDefault() VolumeClaimDeletePolicy {
140+
if es.VolumeClaimDeletePolicy == "" {
141+
return DeleteOnScaledownAndClusterDeletionPolicy
142+
}
143+
return es.VolumeClaimDeletePolicy
144+
}
145+
121146
// Auth contains user authentication and authorization security settings for Elasticsearch.
122147
type Auth struct {
123148
// Roles to propagate to the Elasticsearch cluster.

pkg/controller/common/reconciler/secret.go

Lines changed: 2 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import (
1313
corev1 "k8s.io/api/core/v1"
1414
apierrors "k8s.io/apimachinery/pkg/api/errors"
1515
"k8s.io/apimachinery/pkg/api/meta"
16-
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1716
"k8s.io/apimachinery/pkg/runtime"
1817
"k8s.io/apimachinery/pkg/types"
1918
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -118,7 +117,7 @@ func ReconcileSecretNoOwnerRef(c k8s.Client, expected corev1.Secret, softOwner r
118117
// or if secret data is not strictly equal
119118
!reflect.DeepEqual(expected.Data, reconciled.Data) ||
120119
// or if an existing owner should be removed
121-
hasOwner(&reconciled, ownerMeta)
120+
k8s.HasOwner(&reconciled, ownerMeta)
122121
},
123122
UpdateReconciled: func() {
124123
// set expected annotations and labels, but don't remove existing ones
@@ -127,7 +126,7 @@ func ReconcileSecretNoOwnerRef(c k8s.Client, expected corev1.Secret, softOwner r
127126
reconciled.Annotations = maps.Merge(reconciled.Annotations, expected.Annotations)
128127
reconciled.Data = expected.Data
129128
// remove existing owner
130-
removeOwner(&reconciled, ownerMeta)
129+
k8s.RemoveOwner(&reconciled, ownerMeta)
131130
},
132131
}); err != nil {
133132
return corev1.Secret{}, err
@@ -221,38 +220,3 @@ func GarbageCollectAllSoftOwnedOrphanSecrets(c k8s.Client, ownerKinds map[string
221220
}
222221
return nil
223222
}
224-
225-
func hasOwner(resource metav1.Object, owner metav1.Object) bool {
226-
if owner == nil || resource == nil {
227-
return false
228-
}
229-
found, _ := findOwner(resource, owner)
230-
return found
231-
}
232-
233-
func removeOwner(resource metav1.Object, owner metav1.Object) {
234-
if resource == nil || owner == nil {
235-
return
236-
}
237-
found, index := findOwner(resource, owner)
238-
if !found {
239-
return
240-
}
241-
owners := resource.GetOwnerReferences()
242-
// remove the owner at index i from the slice
243-
newOwners := append(owners[:index], owners[index+1:]...)
244-
resource.SetOwnerReferences(newOwners)
245-
}
246-
247-
func findOwner(resource metav1.Object, owner metav1.Object) (found bool, index int) {
248-
if owner == nil || resource == nil {
249-
return false, 0
250-
}
251-
ownerRefs := resource.GetOwnerReferences()
252-
for i := range ownerRefs {
253-
if ownerRefs[i].Name == owner.GetName() && ownerRefs[i].UID == owner.GetUID() {
254-
return true, i
255-
}
256-
}
257-
return false, 0
258-
}

pkg/controller/common/reconciler/secret_test.go

Lines changed: 0 additions & 182 deletions
Original file line numberDiff line numberDiff line change
@@ -245,188 +245,6 @@ func addOwner(secret *corev1.Secret, name string, uid types.UID) *corev1.Secret
245245
return secret
246246
}
247247

248-
func Test_hasOwner(t *testing.T) {
249-
owner := sampleOwner()
250-
type args struct {
251-
resource metav1.Object
252-
owner metav1.Object
253-
}
254-
tests := []struct {
255-
name string
256-
args args
257-
want bool
258-
}{
259-
{
260-
name: "owner is referenced (same name and uid)",
261-
args: args{
262-
resource: addOwner(&corev1.Secret{}, owner.Name, owner.UID),
263-
owner: owner,
264-
},
265-
want: true,
266-
},
267-
{
268-
name: "owner referenced among other owner references",
269-
args: args{
270-
resource: addOwner(addOwner(&corev1.Secret{}, "another-name", types.UID("another-id")), owner.Name, owner.UID),
271-
owner: owner,
272-
},
273-
want: true,
274-
},
275-
{
276-
name: "owner not referenced",
277-
args: args{
278-
resource: addOwner(addOwner(&corev1.Secret{}, "another-name", types.UID("another-id")), "yet-another-name", "yet-another-uid"),
279-
owner: owner,
280-
},
281-
want: false,
282-
},
283-
{
284-
name: "no owner ref",
285-
args: args{
286-
resource: &corev1.Secret{},
287-
owner: owner,
288-
},
289-
want: false,
290-
},
291-
}
292-
for _, tt := range tests {
293-
t.Run(tt.name, func(t *testing.T) {
294-
if got := hasOwner(tt.args.resource, tt.args.owner); got != tt.want {
295-
t.Errorf("hasOwner() = %v, want %v", got, tt.want)
296-
}
297-
})
298-
}
299-
}
300-
301-
func Test_removeOwner(t *testing.T) {
302-
type args struct {
303-
resource metav1.Object
304-
owner metav1.Object
305-
}
306-
tests := []struct {
307-
name string
308-
args args
309-
wantResource *corev1.Secret
310-
}{
311-
{
312-
name: "no owner: no-op",
313-
args: args{
314-
resource: &corev1.Secret{},
315-
owner: sampleOwner(),
316-
},
317-
wantResource: &corev1.Secret{},
318-
},
319-
{
320-
name: "different owner: no-op",
321-
args: args{
322-
resource: addOwner(&corev1.Secret{}, "another-owner-name", "another-owner-id"),
323-
owner: sampleOwner(),
324-
},
325-
wantResource: addOwner(&corev1.Secret{}, "another-owner-name", "another-owner-id"),
326-
},
327-
{
328-
name: "remove the single owner",
329-
args: args{
330-
resource: addOwner(&corev1.Secret{}, sampleOwner().Name, sampleOwner().UID),
331-
owner: sampleOwner(),
332-
},
333-
wantResource: &corev1.Secret{ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{}}},
334-
},
335-
{
336-
name: "remove the owner from a list of owners",
337-
args: args{
338-
resource: addOwner(addOwner(&corev1.Secret{}, sampleOwner().Name, sampleOwner().UID), "another-owner", "another-uid"),
339-
owner: sampleOwner(),
340-
},
341-
wantResource: addOwner(&corev1.Secret{}, "another-owner", "another-uid"),
342-
},
343-
{
344-
name: "owner listed twice in the list (shouldn't happen): remove the first occurrence",
345-
args: args{
346-
resource: addOwner(addOwner(&corev1.Secret{}, sampleOwner().Name, sampleOwner().UID), sampleOwner().Name, sampleOwner().UID),
347-
owner: sampleOwner(),
348-
},
349-
wantResource: addOwner(&corev1.Secret{}, sampleOwner().Name, sampleOwner().UID),
350-
},
351-
}
352-
for _, tt := range tests {
353-
t.Run(tt.name, func(t *testing.T) {
354-
removeOwner(tt.args.resource, tt.args.owner)
355-
require.Equal(t, tt.wantResource, tt.args.resource)
356-
})
357-
}
358-
}
359-
360-
func Test_findOwner(t *testing.T) {
361-
type args struct {
362-
resource metav1.Object
363-
owner metav1.Object
364-
}
365-
tests := []struct {
366-
name string
367-
args args
368-
wantFound bool
369-
wantIndex int
370-
}{
371-
{
372-
name: "no owner: not found",
373-
args: args{
374-
resource: &corev1.Secret{},
375-
owner: sampleOwner(),
376-
},
377-
wantFound: false,
378-
wantIndex: 0,
379-
},
380-
{
381-
name: "different owner: not found",
382-
args: args{
383-
resource: addOwner(&corev1.Secret{}, "another-owner-name", "another-owner-id"),
384-
owner: sampleOwner(),
385-
},
386-
wantFound: false,
387-
wantIndex: 0,
388-
},
389-
{
390-
name: "owner at index 0",
391-
args: args{
392-
resource: addOwner(&corev1.Secret{}, sampleOwner().Name, sampleOwner().UID),
393-
owner: sampleOwner(),
394-
},
395-
wantFound: true,
396-
wantIndex: 0,
397-
},
398-
{
399-
name: "owner at index 1",
400-
args: args{
401-
resource: addOwner(addOwner(&corev1.Secret{}, "another-owner", "another-uid"), sampleOwner().Name, sampleOwner().UID),
402-
owner: sampleOwner(),
403-
},
404-
wantFound: true,
405-
wantIndex: 1,
406-
},
407-
{
408-
name: "owner listed twice in the list (shouldn't happen): return the first occurrence (index 0)",
409-
args: args{
410-
resource: addOwner(addOwner(&corev1.Secret{}, sampleOwner().Name, sampleOwner().UID), sampleOwner().Name, sampleOwner().UID),
411-
owner: sampleOwner(),
412-
},
413-
wantFound: true,
414-
wantIndex: 0,
415-
},
416-
}
417-
for _, tt := range tests {
418-
t.Run(tt.name, func(t *testing.T) {
419-
gotFound, gotIndex := findOwner(tt.args.resource, tt.args.owner)
420-
if gotFound != tt.wantFound {
421-
t.Errorf("findOwner() gotFound = %v, want %v", gotFound, tt.wantFound)
422-
}
423-
if gotIndex != tt.wantIndex {
424-
t.Errorf("findOwner() gotIndex = %v, want %v", gotIndex, tt.wantIndex)
425-
}
426-
})
427-
}
428-
}
429-
430248
func ownedSecret(namespace, name, ownerNs, ownerName, ownerKind string) *corev1.Secret {
431249
return &corev1.Secret{
432250
ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: name, Labels: map[string]string{

pkg/controller/elasticsearch/driver/nodes.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,10 @@ func (d *defaultDriver) reconcileNodeSpecs(
120120
return results.WithError(err)
121121
}
122122

123+
if err := reconcilePVCOwnerRefs(d.K8sClient(), d.ES); err != nil {
124+
return results.WithError(err)
125+
}
126+
123127
if err := GarbageCollectPVCs(d.K8sClient(), d.ES, actualStatefulSets, expectedResources.StatefulSets()); err != nil {
124128
return results.WithError(err)
125129
}

0 commit comments

Comments
 (0)