This is an automated email from the ASF dual-hosted git repository. adheipsingh pushed a commit to branch 1.3.0-pvc-annotations in repository https://gitbox.apache.org/repos/asf/druid-operator.git
commit c3072f98059a557c59bff0c9764b70cc4ca46515 Author: AdheipSingh <[email protected]> AuthorDate: Tue Sep 30 17:00:50 2025 +0530 add support to add pvc annotations on UPDATES to druid CR --- apis/druid/v1alpha1/druid_types.go | 6 + chart/crds/druid.apache.org_druids.yaml | 6 + config/crd/bases/druid.apache.org_druids.yaml | 6 + controllers/druid/druid_controller.go | 6 +- controllers/druid/handler.go | 14 ++ controllers/druid/interface.go | 5 + controllers/druid/volume_annotation_update.go | 278 +++++++++++++++++++++ controllers/druid/volume_annotation_update_test.go | 203 +++++++++++++++ docs/api_specifications/druid.md | 26 ++ docs/features.md | 74 +++++- go.mod | 1 + go.sum | 1 + 12 files changed, 622 insertions(+), 4 deletions(-) diff --git a/apis/druid/v1alpha1/druid_types.go b/apis/druid/v1alpha1/druid_types.go index 0fd457c..aacc91f 100644 --- a/apis/druid/v1alpha1/druid_types.go +++ b/apis/druid/v1alpha1/druid_types.go @@ -115,6 +115,12 @@ type DruidSpec struct { // +kubebuilder:default:=false DisablePVCDeletionFinalizer bool `json:"disablePVCDeletionFinalizer,omitempty"` + // DisablePVCAnnotationUpdate When set to true, operator will not patch PVC annotations when VolumeClaimTemplate annotations are updated. + // When disabled (false), updating VolumeClaimTemplate annotations will patch existing PVCs and recreate the StatefulSet. + // +optional + // +kubebuilder:default:=false + DisablePVCAnnotationUpdate bool `json:"disablePVCAnnotationUpdate,omitempty"` + // DeleteOrphanPvc Orphaned (unmounted PVCs) shall be cleaned up by the operator. // +optional // +kubebuilder:default:=true diff --git a/chart/crds/druid.apache.org_druids.yaml b/chart/crds/druid.apache.org_druids.yaml index 742756d..d14ed98 100644 --- a/chart/crds/druid.apache.org_druids.yaml +++ b/chart/crds/druid.apache.org_druids.yaml @@ -1496,6 +1496,12 @@ spec: description: DeleteOrphanPvc Orphaned (unmounted PVCs) shall be cleaned up by the operator. type: boolean + disablePVCAnnotationUpdate: + default: false + description: |- + DisablePVCAnnotationUpdate When set to true, operator will not patch PVC annotations when VolumeClaimTemplate annotations are updated. + When disabled (false), updating VolumeClaimTemplate annotations will patch existing PVCs and recreate the StatefulSet. + type: boolean disablePVCDeletionFinalizer: default: false description: DisablePVCDeletionFinalizer Whether PVCs shall be deleted diff --git a/config/crd/bases/druid.apache.org_druids.yaml b/config/crd/bases/druid.apache.org_druids.yaml index 742756d..d14ed98 100644 --- a/config/crd/bases/druid.apache.org_druids.yaml +++ b/config/crd/bases/druid.apache.org_druids.yaml @@ -1496,6 +1496,12 @@ spec: description: DeleteOrphanPvc Orphaned (unmounted PVCs) shall be cleaned up by the operator. type: boolean + disablePVCAnnotationUpdate: + default: false + description: |- + DisablePVCAnnotationUpdate When set to true, operator will not patch PVC annotations when VolumeClaimTemplate annotations are updated. + When disabled (false), updating VolumeClaimTemplate annotations will patch existing PVCs and recreate the StatefulSet. + type: boolean disablePVCDeletionFinalizer: default: false description: DisablePVCDeletionFinalizer Whether PVCs shall be deleted diff --git a/controllers/druid/druid_controller.go b/controllers/druid/druid_controller.go index 4959d00..1cd6073 100644 --- a/controllers/druid/druid_controller.go +++ b/controllers/druid/druid_controller.go @@ -78,9 +78,9 @@ func (r *DruidReconciler) Reconcile(ctx context.Context, request reconcile.Reque } // Update Druid Dynamic Configs - if err := updateDruidDynamicConfigs(ctx, r.Client, instance, emitEvent); err != nil { - return ctrl.Result{}, err - } + // if err := updateDruidDynamicConfigs(ctx, r.Client, instance, emitEvent); err != nil { + // return ctrl.Result{}, err + // } // If both operations succeed, requeue after specified wait time return ctrl.Result{RequeueAfter: r.ReconcileWait}, nil diff --git a/controllers/druid/handler.go b/controllers/druid/handler.go index c47032c..5d8807f 100644 --- a/controllers/druid/handler.go +++ b/controllers/druid/handler.go @@ -165,6 +165,20 @@ func deployDruidCluster(ctx context.Context, sdk client.Client, m *v1alpha1.Drui } } + // Handle VolumeClaimTemplate annotation updates (enabled by default unless disabled) + // If StatefulSet was deleted for annotation updates, skip creation in this reconcile to avoid race condition + if m.Generation > 1 { + deleted, err := patchStatefulSetVolumeClaimTemplateAnnotations(ctx, sdk, m, &nodeSpec, emitEvents, nodeSpecUniqueStr) + if err != nil { + return err + } + if deleted { + // StatefulSet was deleted, skip creation in this reconcile loop + // It will be created in the next reconcile with updated annotations + continue + } + } + // Create/Update StatefulSet if stsCreateUpdateStatus, err := sdkCreateOrUpdateAsNeeded(ctx, sdk, func() (object, error) { diff --git a/controllers/druid/interface.go b/controllers/druid/interface.go index 6f32b2d..07a7446 100644 --- a/controllers/druid/interface.go +++ b/controllers/druid/interface.go @@ -49,6 +49,11 @@ const ( druidConfigComparisonFailed druidEventReason = "DruidAPIConfigComparisonFailed" druidUpdateConfigsFailed druidEventReason = "DruidAPIUpdateConfigsFailed" druidUpdateConfigsSuccess druidEventReason = "DruidAPIUpdateConfigsSuccess" + + // PVC Annotation Update Events + druidPvcAnnotationChangeDetected druidEventReason = "DruidOperatorPvcAnnotationChangeDetected" + druidPvcAnnotationsUpdated druidEventReason = "DruidOperatorPvcAnnotationsUpdated" + druidStsOrphanedForAnnotations druidEventReason = "DruidOperatorStsOrphanedForAnnotations" ) // Reader Interface diff --git a/controllers/druid/volume_annotation_update.go b/controllers/druid/volume_annotation_update.go new file mode 100644 index 0000000..f024def --- /dev/null +++ b/controllers/druid/volume_annotation_update.go @@ -0,0 +1,278 @@ +package druid + +import ( + "context" + "fmt" + "reflect" + "strings" + + "github.com/datainfrahq/druid-operator/apis/druid/v1alpha1" + appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// patchStatefulSetVolumeClaimTemplateAnnotations handles annotation updates for StatefulSet VolumeClaimTemplates +// Returns (deleted bool, error) - deleted indicates if the StatefulSet was deleted in this call +func patchStatefulSetVolumeClaimTemplateAnnotations(ctx context.Context, sdk client.Client, m *v1alpha1.Druid, + nodeSpec *v1alpha1.DruidNodeSpec, emitEvent EventEmitter, nodeSpecUniqueStr string) (bool, error) { + + // Skip if PVC annotation update is disabled + if m.Spec.DisablePVCAnnotationUpdate { + return false, nil + } + + // Only process StatefulSets (default kind is StatefulSet when not specified or anything other than "Deployment") + if nodeSpec.Kind == "Deployment" { + return false, nil + } + + // Get the existing StatefulSet + sts, err := readers.Get(ctx, sdk, nodeSpecUniqueStr, m, func() object { return &appsv1.StatefulSet{} }, emitEvent) + if err != nil { + // StatefulSet doesn't exist yet, will be created with proper annotations + // This is expected after we delete it for recreation + return false, nil + } + + statefulSet := sts.(*appsv1.StatefulSet) + + // Check if annotation changes are needed + annotationChangesNeeded, annotationDetails := detectAnnotationChanges(statefulSet, nodeSpec) + if !annotationChangesNeeded { + return false, nil + } + + // Before proceeding, check if PVCs already have the desired annotations + // This prevents re-processing after StatefulSet deletion + pvcAlreadyUpdated, err := checkPVCAnnotationsAlreadyUpdated(ctx, sdk, statefulSet, nodeSpec, m, emitEvent, nodeSpecUniqueStr) + if err != nil { + return false, err + } + if pvcAlreadyUpdated { + // PVCs already have the desired annotations, likely from a previous reconcile + // The StatefulSet will be recreated with correct annotations by sdkCreateOrUpdateAsNeeded + return false, nil + } + + // Don't proceed unless all statefulsets are up and running + getSTSList, err := readers.List(ctx, sdk, m, makeLabelsForDruid(m), emitEvent, func() objectList { return &appsv1.StatefulSetList{} }, func(listObj runtime.Object) []object { + items := listObj.(*appsv1.StatefulSetList).Items + result := make([]object, len(items)) + for i := 0; i < len(items); i++ { + result[i] = &items[i] + } + return result + }) + if err != nil { + return false, nil + } + + for _, sts := range getSTSList { + if sts.(*appsv1.StatefulSet).Status.Replicas != sts.(*appsv1.StatefulSet).Status.ReadyReplicas { + return false, nil + } + } + + // Emit event for annotation change detection + msg := fmt.Sprintf("Detected annotation changes in VolumeClaimTemplates for StatefulSet [%s] in Namespace [%s]: %s", + statefulSet.Name, statefulSet.Namespace, annotationDetails) + emitEvent.EmitEventGeneric(m, string(druidPvcAnnotationChangeDetected), msg, nil) + + // First, delete the StatefulSet with cascade=orphan (similar to volume expansion) + msg = fmt.Sprintf("Deleting StatefulSet [%s] with cascade=orphan to apply annotation changes", statefulSet.Name) + emitEvent.EmitEventGeneric(m, string(druidStsOrphanedForAnnotations), msg, nil) + + if err := writers.Delete(ctx, sdk, m, statefulSet, emitEvent, client.PropagationPolicy(metav1.DeletePropagationOrphan)); err != nil { + return false, err + } + + msg = fmt.Sprintf("StatefulSet [%s] successfully deleted with cascade=orphan for annotation updates", statefulSet.Name) + emitEvent.EmitEventGeneric(m, string(druidStsOrphanedForAnnotations), msg, nil) + + // Then update PVC annotations after deletion (similar to volume expansion) + if err := patchPVCAnnotations(ctx, sdk, statefulSet, nodeSpec, m, emitEvent, nodeSpecUniqueStr); err != nil { + return false, err + } + + // Return true to indicate StatefulSet was deleted + return true, nil +} + +// detectAnnotationChanges compares current and desired annotations for VolumeClaimTemplates +func detectAnnotationChanges(sts *appsv1.StatefulSet, nodeSpec *v1alpha1.DruidNodeSpec) (bool, string) { + var changeDetails []string + + // Create a map of current VCT annotations by name + currentVCTAnnotations := make(map[string]map[string]string) + for _, vct := range sts.Spec.VolumeClaimTemplates { + currentVCTAnnotations[vct.Name] = vct.ObjectMeta.Annotations + } + + // Check each desired VCT for annotation changes + for _, desiredVCT := range nodeSpec.VolumeClaimTemplates { + currentAnnotations, exists := currentVCTAnnotations[desiredVCT.Name] + + // If VCT doesn't exist in current StatefulSet, skip (it's a new VCT) + if !exists { + continue + } + + // Compare annotations + if !reflect.DeepEqual(currentAnnotations, desiredVCT.ObjectMeta.Annotations) { + changeDetails = append(changeDetails, fmt.Sprintf("VCT %s: annotations changed", desiredVCT.Name)) + } + } + + if len(changeDetails) > 0 { + return true, strings.Join(changeDetails, "; ") + } + + return false, "" +} + +// patchPVCAnnotations patches existing PVCs with new annotations from VolumeClaimTemplates +func patchPVCAnnotations(ctx context.Context, sdk client.Client, sts *appsv1.StatefulSet, + nodeSpec *v1alpha1.DruidNodeSpec, m *v1alpha1.Druid, emitEvent EventEmitter, nodeSpecUniqueStr string) error { + + // Get PVCs for this StatefulSet + pvcLabels := map[string]string{ + "nodeSpecUniqueStr": nodeSpecUniqueStr, + } + + pvcList, err := readers.List(ctx, sdk, m, pvcLabels, emitEvent, func() objectList { return &v1.PersistentVolumeClaimList{} }, func(listObj runtime.Object) []object { + items := listObj.(*v1.PersistentVolumeClaimList).Items + result := make([]object, len(items)) + for i := 0; i < len(items); i++ { + result[i] = &items[i] + } + return result + }) + if err != nil { + return err + } + + // Create a map of desired annotations by VCT name + desiredAnnotationsByVCT := make(map[string]map[string]string) + for _, vct := range nodeSpec.VolumeClaimTemplates { + desiredAnnotationsByVCT[vct.Name] = vct.ObjectMeta.Annotations + } + + // Patch each PVC with new annotations + for _, pvcObj := range pvcList { + pvc := pvcObj.(*v1.PersistentVolumeClaim) + + // Determine which VolumeClaimTemplate this PVC belongs to + vctName := extractVCTNameFromPVC(pvc.Name, sts.Name) + if vctName == "" { + continue + } + + desiredAnnotations, exists := desiredAnnotationsByVCT[vctName] + if !exists { + continue + } + + // Check if annotations need updating + if reflect.DeepEqual(pvc.ObjectMeta.Annotations, desiredAnnotations) { + continue + } + + // Create patch for annotations + pvcCopy := pvc.DeepCopy() + patch := client.MergeFrom(pvcCopy) + + // Update or set annotations + if pvc.ObjectMeta.Annotations == nil { + pvc.ObjectMeta.Annotations = make(map[string]string) + } + + // Apply desired annotations + for key, value := range desiredAnnotations { + pvc.ObjectMeta.Annotations[key] = value + } + + // Remove annotations that are not in desired state + for key := range pvc.ObjectMeta.Annotations { + if _, exists := desiredAnnotations[key]; !exists { + delete(pvc.ObjectMeta.Annotations, key) + } + } + + // Patch the PVC + if err := writers.Patch(ctx, sdk, m, pvc, false, patch, emitEvent); err != nil { + return err + } + + msg := fmt.Sprintf("PVC [%s] successfully patched with updated annotations", pvc.Name) + emitEvent.EmitEventGeneric(m, string(druidPvcAnnotationsUpdated), msg, nil) + } + + return nil +} + +// checkPVCAnnotationsAlreadyUpdated checks if PVCs already have the desired annotations +func checkPVCAnnotationsAlreadyUpdated(ctx context.Context, sdk client.Client, sts *appsv1.StatefulSet, + nodeSpec *v1alpha1.DruidNodeSpec, m *v1alpha1.Druid, emitEvent EventEmitter, nodeSpecUniqueStr string) (bool, error) { + + // Get PVCs for this StatefulSet + pvcLabels := map[string]string{ + "nodeSpecUniqueStr": nodeSpecUniqueStr, + } + + pvcList, err := readers.List(ctx, sdk, m, pvcLabels, emitEvent, func() objectList { return &v1.PersistentVolumeClaimList{} }, func(listObj runtime.Object) []object { + items := listObj.(*v1.PersistentVolumeClaimList).Items + result := make([]object, len(items)) + for i := 0; i < len(items); i++ { + result[i] = &items[i] + } + return result + }) + if err != nil { + return false, err + } + + // Create a map of desired annotations by VCT name + desiredAnnotationsByVCT := make(map[string]map[string]string) + for _, vct := range nodeSpec.VolumeClaimTemplates { + desiredAnnotationsByVCT[vct.Name] = vct.ObjectMeta.Annotations + } + + // Check each PVC to see if it already has the desired annotations + for _, pvcObj := range pvcList { + pvc := pvcObj.(*v1.PersistentVolumeClaim) + + // Determine which VolumeClaimTemplate this PVC belongs to + vctName := extractVCTNameFromPVC(pvc.Name, sts.Name) + if vctName == "" { + continue + } + + desiredAnnotations, exists := desiredAnnotationsByVCT[vctName] + if !exists { + continue + } + + // If any PVC doesn't have the desired annotations, return false + if !reflect.DeepEqual(pvc.ObjectMeta.Annotations, desiredAnnotations) { + return false, nil + } + } + + // All PVCs have the desired annotations + return true, nil +} + +// extractVCTNameFromPVC extracts the VolumeClaimTemplate name from a PVC name +// PVC naming format: {vctName}-{statefulSetName}-{ordinal} +func extractVCTNameFromPVC(pvcName, stsName string) string { + // Remove the StatefulSet name and ordinal suffix + suffix := fmt.Sprintf("-%s-", stsName) + idx := strings.LastIndex(pvcName, suffix) + if idx == -1 { + return "" + } + return pvcName[:idx] +} diff --git a/controllers/druid/volume_annotation_update_test.go b/controllers/druid/volume_annotation_update_test.go new file mode 100644 index 0000000..2a81685 --- /dev/null +++ b/controllers/druid/volume_annotation_update_test.go @@ -0,0 +1,203 @@ +package druid + +import ( + "context" + "testing" + + "github.com/datainfrahq/druid-operator/apis/druid/v1alpha1" + "github.com/stretchr/testify/assert" + appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// mockEventEmitter provides a no-op implementation of EventEmitter for testing +type mockEventEmitter struct{} + +func (m *mockEventEmitter) EmitEventGeneric(obj object, eventReason, msg string, err error) {} +func (m *mockEventEmitter) EmitEventRollingDeployWait(obj, k8sObj object, nodeSpecUniqueStr string) {} +func (m *mockEventEmitter) EmitEventOnGetError(obj, getObj object, err error) {} +func (m *mockEventEmitter) EmitEventOnUpdate(obj, updateObj object, err error) {} +func (m *mockEventEmitter) EmitEventOnDelete(obj, deleteObj object, err error) {} +func (m *mockEventEmitter) EmitEventOnCreate(obj, createObj object, err error) {} +func (m *mockEventEmitter) EmitEventOnPatch(obj, patchObj object, err error) {} +func (m *mockEventEmitter) EmitEventOnList(obj object, listObj objectList, err error) {} + +func TestDetectAnnotationChanges(t *testing.T) { + tests := []struct { + name string + currentStatefulSet *appsv1.StatefulSet + nodeSpec *v1alpha1.DruidNodeSpec + expectChanges bool + expectDetails string + }{ + { + name: "No changes when annotations are identical", + currentStatefulSet: &appsv1.StatefulSet{ + Spec: appsv1.StatefulSetSpec{ + VolumeClaimTemplates: []v1.PersistentVolumeClaim{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "segment-cache", + Annotations: map[string]string{ + "volume.beta.kubernetes.io/storage-class": "fast-ssd", + }, + }, + }, + }, + }, + }, + nodeSpec: &v1alpha1.DruidNodeSpec{ + VolumeClaimTemplates: []v1.PersistentVolumeClaim{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "segment-cache", + Annotations: map[string]string{ + "volume.beta.kubernetes.io/storage-class": "fast-ssd", + }, + }, + }, + }, + }, + expectChanges: false, + expectDetails: "", + }, + { + name: "Detect annotation changes", + currentStatefulSet: &appsv1.StatefulSet{ + Spec: appsv1.StatefulSetSpec{ + VolumeClaimTemplates: []v1.PersistentVolumeClaim{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "segment-cache", + Annotations: map[string]string{ + "volume.beta.kubernetes.io/storage-class": "fast-ssd", + }, + }, + }, + }, + }, + }, + nodeSpec: &v1alpha1.DruidNodeSpec{ + VolumeClaimTemplates: []v1.PersistentVolumeClaim{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "segment-cache", + Annotations: map[string]string{ + "volume.beta.kubernetes.io/storage-class": "ultra-fast-ssd", + "backup.policy": "enabled", + }, + }, + }, + }, + }, + expectChanges: true, + expectDetails: "VCT segment-cache: annotations changed", + }, + { + name: "No changes when VCT is new", + currentStatefulSet: &appsv1.StatefulSet{ + Spec: appsv1.StatefulSetSpec{ + VolumeClaimTemplates: []v1.PersistentVolumeClaim{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "segment-cache", + }, + }, + }, + }, + }, + nodeSpec: &v1alpha1.DruidNodeSpec{ + VolumeClaimTemplates: []v1.PersistentVolumeClaim{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "segment-cache", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "new-volume", + Annotations: map[string]string{ + "new": "annotation", + }, + }, + }, + }, + }, + expectChanges: false, + expectDetails: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + hasChanges, details := detectAnnotationChanges(tt.currentStatefulSet, tt.nodeSpec) + assert.Equal(t, tt.expectChanges, hasChanges) + assert.Equal(t, tt.expectDetails, details) + }) + } +} + +func TestExtractVCTNameFromPVC(t *testing.T) { + tests := []struct { + name string + pvcName string + stsName string + expectedVCT string + }{ + { + name: "Extract VCT name from standard PVC", + pvcName: "segment-cache-druid-historicals-0", + stsName: "druid-historicals", + expectedVCT: "segment-cache", + }, + { + name: "Extract VCT name with complex naming", + pvcName: "data-volume-druid-middlemanager-2", + stsName: "druid-middlemanager", + expectedVCT: "data-volume", + }, + { + name: "Return empty when pattern doesn't match", + pvcName: "random-pvc-name", + stsName: "druid-historicals", + expectedVCT: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := extractVCTNameFromPVC(tt.pvcName, tt.stsName) + assert.Equal(t, tt.expectedVCT, result) + }) + } +} + +// TestPatchStatefulSetVolumeClaimTemplateAnnotations_Integration would be better suited as an integration test +// The actual function requires complex mocking of readers/writers which are global variables + +func TestPatchStatefulSetVolumeClaimTemplateAnnotationsDisabled(t *testing.T) { + ctx := context.TODO() + + druid := &v1alpha1.Druid{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-druid", + Namespace: "default", + }, + Spec: v1alpha1.DruidSpec{ + DisablePVCAnnotationUpdate: true, // Feature disabled + }, + } + + nodeSpec := &v1alpha1.DruidNodeSpec{ + Kind: "", // Default is StatefulSet + } + + // Mock event emitter - we just need a no-op implementation + emitter := &mockEventEmitter{} + + // Test should return immediately when feature is disabled + deleted, err := patchStatefulSetVolumeClaimTemplateAnnotations(ctx, nil, druid, nodeSpec, emitter, "test") + assert.NoError(t, err) + assert.False(t, deleted) // Should not delete when disabled +} diff --git a/docs/api_specifications/druid.md b/docs/api_specifications/druid.md index fd57ded..9778fc8 100644 --- a/docs/api_specifications/druid.md +++ b/docs/api_specifications/druid.md @@ -399,6 +399,19 @@ bool </tr> <tr> <td> +<code>disablePVCAnnotationUpdate</code><br> +<em> +bool +</em> +</td> +<td> +<em>(Optional)</em> +<p>DisablePVCAnnotationUpdate When set to true, operator will not patch PVC annotations when VolumeClaimTemplate annotations are updated. +When disabled (false), updating VolumeClaimTemplate annotations will patch existing PVCs and recreate the StatefulSet.</p> +</td> +</tr> +<tr> +<td> <code>deleteOrphanPvc</code><br> <em> bool @@ -2118,6 +2131,19 @@ bool </tr> <tr> <td> +<code>disablePVCAnnotationUpdate</code><br> +<em> +bool +</em> +</td> +<td> +<em>(Optional)</em> +<p>DisablePVCAnnotationUpdate When set to true, operator will not patch PVC annotations when VolumeClaimTemplate annotations are updated. +When disabled (false), updating VolumeClaimTemplate annotations will patch existing PVCs and recreate the StatefulSet.</p> +</td> +</tr> +<tr> +<td> <code>deleteOrphanPvc</code><br> <em> bool diff --git a/docs/features.md b/docs/features.md index 4038b6c..441b6e3 100644 --- a/docs/features.md +++ b/docs/features.md @@ -8,6 +8,7 @@ - [Force Delete of Sts Pods](#force-delete-of-sts-pods) - [Horizontal Scaling of Druid Pods](#horizontal-scaling-of-druid-pods) - [Volume Expansion of Druid Pods Running As StatefulSets](#volume-expansion-of-druid-pods-running-as-statefulsets) +- [PVC Annotation Updates for StatefulSets](#pvc-annotation-updates-for-statefulsets) - [Add Additional Containers to Druid Pods](#add-additional-containers-to-druid-pods) - [Default Yet Configurable Probes](#default-yet-configurable-probes) @@ -95,9 +96,80 @@ in the druid CR, only then will it perform expansion. This feature is disabled by default. To enable it set `scalePvcSts: true` in the Druid CR. By default, this feature is disabled. ``` -IMPORTANT: Shrinkage of pvc's isnt supported - desiredSize cannot be less than currentSize as well as counts. +IMPORTANT: Shrinkage of pvc's isnt supported - desiredSize cannot be less than currentSize as well as counts. ``` +## PVC Annotation Updates for StatefulSets + +The Druid operator supports updating annotations on PersistentVolumeClaims (PVCs) that are managed by StatefulSets. This feature is useful for scenarios such as: +- Adding backup policies to existing PVCs +- Updating storage tier annotations +- Adding monitoring or compliance labels +- Modifying cloud provider-specific annotations (e.g., AWS EBS volume tags) + +### How It Works + +When you update the annotations in `volumeClaimTemplates` within the Druid CR, the operator will: +1. Detect the annotation changes in the VolumeClaimTemplates +2. Delete the StatefulSet with `cascade=orphan` (preserving pods and PVCs) +3. Patch existing PVCs with the new annotations +4. Recreate the StatefulSet with the updated VolumeClaimTemplate specifications + +This process ensures no downtime as the pods continue running throughout the update. + +### Configuration + +This feature is **enabled by default**. To disable it, set `disablePVCAnnotationUpdate: true` in the Druid CR: + +```yaml +apiVersion: druid.apache.org/v1alpha1 +kind: Druid +metadata: + name: tiny-cluster +spec: + # Disable PVC annotation updates (default is false - enabled) + disablePVCAnnotationUpdate: false + + nodes: + historicals: + nodeType: historical + druid.port: 8088 + kind: StatefulSet + replicas: 1 + volumeClaimTemplates: + - metadata: + name: data-volume + annotations: + # These annotations will be applied to the PVCs + volume.beta.kubernetes.io/storage-class: "fast-ssd" + backup.velero.io/backup-volumes: "data-volume" + custom.annotation/tier: "hot" + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 10Gi + storageClassName: standard +``` + +### Important Notes + +- This feature only applies to StatefulSets (not Deployments) +- The operator will wait for all StatefulSets to be ready before performing the update +- Changes are applied using Kubernetes' orphan deletion strategy to avoid pod disruption +- The StatefulSet will be unavailable briefly during recreation, but pods continue serving traffic +- PVC annotations can only be added or updated, not removed (due to Kubernetes limitations) + +### Comparison with Volume Expansion + +| Feature | Volume Expansion | PVC Annotation Updates | +|---------|-----------------|------------------------| +| Default State | Disabled (`scalePvcSts: false`) | Enabled (`disablePVCAnnotationUpdate: false`) | +| What Changes | PVC storage size | PVC annotations | +| StatefulSet Deletion | Yes (orphan) | Yes (orphan) | +| Pod Disruption | No | No | +| Rollback Support | No (size can't shrink) | Yes (update annotations again) | + ## Add Additional Containers to Druid Pods The operator supports adding additional containers to run along with the druid pods. This helps support co-located, co-managed helper processes for the primary druid application. This can be used for init containers, sidecars, diff --git a/go.mod b/go.mod index 14ca62f..01f3448 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.9.0 // indirect + github.com/evanphx/json-patch v4.12.0+incompatible // indirect github.com/evanphx/json-patch/v5 v5.6.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/go-logr/zapr v1.2.4 // indirect diff --git a/go.sum b/go.sum index 76aaa3b..43ed29b 100644 --- a/go.sum +++ b/go.sum @@ -23,6 +23,7 @@ github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= +github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
