Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,39 @@ kubectl patch dragonfly dragonfly-sample --type merge -p '{"spec":{"resources":{

To add authentication to the dragonfly pods, you either set the `DFLY_requirepass` environment variable, or add the `--requirepass` argument.

### Customising health-check probe scripts

The operator generates default liveness, readiness, and startup probe scripts and mounts them via ConfigMaps. You can replace any probe with your own script using the `custom*ProbeConfigMap` fields.

Scripts run inside the Dragonfly container and have access to `HEALTHCHECK_PORT` (admin port 9999 — no TLS, no auth).

```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: dragonfly-sample-probes
namespace: default
data:
liveness-check.sh: |
#!/bin/sh
RESPONSE=$(timeout 4 redis-cli -h localhost -p ${HEALTHCHECK_PORT:-9999} PING 2>/dev/null)
case "$RESPONSE" in
PONG|*LOADING*) exit 0 ;;
*) exit 1 ;;
esac
---
apiVersion: dragonflydb.io/v1alpha1
kind: Dragonfly
metadata:
name: dragonfly-sample
spec:
replicas: 1
customLivenessProbeConfigMap:
name: dragonfly-sample-probes
```

> **Override precedence:** `spec.additionalVolumes` with a matching volume name (`liveness-probe`, `readiness-probe`, `startup-probe`) takes precedence over `custom*ProbeConfigMap`. Do not use both for the same probe.

### Deleting a Dragonfly instance

To delete a Dragonfly instance, you can run
Expand Down
15 changes: 15 additions & 0 deletions api/v1alpha1/dragonfly_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,21 @@ type DragonflySpec struct {
// +optional
// +kubebuilder:validation:Optional
Pdb *PdbSpec `json:"pdb,omitempty"`

// (Optional) Custom ConfigMap with key "liveness-check.sh" to override the default liveness probe.
// +optional
// +kubebuilder:validation:Optional
CustomLivenessProbeConfigMap *corev1.LocalObjectReference `json:"customLivenessProbeConfigMap,omitempty"`

// (Optional) Custom ConfigMap with key "readiness-check.sh" to override the default readiness probe.
// +optional
// +kubebuilder:validation:Optional
CustomReadinessProbeConfigMap *corev1.LocalObjectReference `json:"customReadinessProbeConfigMap,omitempty"`

// (Optional) Custom ConfigMap with key "startup-check.sh" to override the default startup probe.
// +optional
// +kubebuilder:validation:Optional
CustomStartupProbeConfigMap *corev1.LocalObjectReference `json:"customStartupProbeConfigMap,omitempty"`
}

// PdbSpec defines the desired state of the PodDisruptionBudget
Expand Down
15 changes: 15 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 45 additions & 0 deletions config/crd/bases/dragonflydb.io_dragonflies.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4461,6 +4461,51 @@ spec:
type: string
type: object
type: object
customLivenessProbeConfigMap:
description: (Optional) Custom ConfigMap with key "liveness-check.sh"
to override the default liveness probe.
properties:
name:
default: ""
description: |-
Name of the referent.
This field is effectively required, but due to backwards compatibility is
allowed to be empty. Instances of this type with an empty value here are
almost certainly wrong.
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
type: string
type: object
x-kubernetes-map-type: atomic
customReadinessProbeConfigMap:
description: (Optional) Custom ConfigMap with key "readiness-check.sh"
to override the default readiness probe.
properties:
name:
default: ""
description: |-
Name of the referent.
This field is effectively required, but due to backwards compatibility is
allowed to be empty. Instances of this type with an empty value here are
almost certainly wrong.
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
type: string
type: object
x-kubernetes-map-type: atomic
customStartupProbeConfigMap:
description: (Optional) Custom ConfigMap with key "startup-check.sh"
to override the default startup probe.
properties:
name:
default: ""
description: |-
Name of the referent.
This field is effectively required, but due to backwards compatibility is
allowed to be empty. Instances of this type with an empty value here are
almost certainly wrong.
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
type: string
type: object
x-kubernetes-map-type: atomic
enableReplicationReadinessGate:
description: |-
(Optional) When enabled, adds a custom readiness gate to pods that prevents
Expand Down
15 changes: 8 additions & 7 deletions config/rbac/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,7 @@ rules:
- apiGroups:
- ""
resources:
- events
verbs:
- create
- patch
- apiGroups:
- ""
resources:
- configmaps
- pods
- services
verbs:
Expand All @@ -24,6 +18,13 @@ rules:
- patch
- update
- watch
- apiGroups:
- ""
resources:
- events
verbs:
- create
- patch
- apiGroups:
- ""
resources:
Expand Down
2 changes: 2 additions & 0 deletions internal/controller/dragonfly_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ type DragonflyReconciler struct {
//+kubebuilder:rbac:groups="",resources=services,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups="",resources=pods,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups="",resources=events,verbs=create;patch
//+kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;create;update;patch;delete
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You also need to add .Owns(&corev1.ConfigMap{},...) in SetupWithManager function in the dragonfly_controller.go. Else, reconciler won't be triggered when configmaps are deleted/updated.

//+kubebuilder:rbac:groups=networking.k8s.io,resources=networkpolicies,verbs=get;list;watch;create;update;patch;delete

// Reconcile is part of the main kubernetes reconciliation loop which aims to
Expand Down Expand Up @@ -158,6 +159,7 @@ func (r *DragonflyReconciler) SetupWithManager(mgr ctrl.Manager) error {
For(&dfv1alpha1.Dragonfly{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})).
Owns(&appsv1.StatefulSet{}, builder.MatchEveryOwner).
Owns(&corev1.Service{}, builder.MatchEveryOwner).
Owns(&corev1.ConfigMap{}, builder.MatchEveryOwner).
Owns(&networkingv1.NetworkPolicy{}, builder.MatchEveryOwner).
Named("Dragonfly").
Complete(r)
Expand Down
6 changes: 6 additions & 0 deletions internal/controller/dragonfly_instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,12 @@ func resourceSpecsEqual(desired, existing client.Object) bool {
if !reflect.DeepEqual(desired.GetLabels(), existing.GetLabels()) || !reflect.DeepEqual(desired.GetAnnotations(), existing.GetAnnotations()) {
return false
}
// ConfigMaps store content in .Data, not .Spec — compare Data directly.
if cmDesired, ok := desired.(*corev1.ConfigMap); ok {
if cmExisting, ok := existing.(*corev1.ConfigMap); ok {
return reflect.DeepEqual(cmDesired.Data, cmExisting.Data)
}
}
// Compare only the .Spec field using reflection
desiredV := reflect.ValueOf(desired).Elem()
existingV := reflect.ValueOf(existing).Elem()
Expand Down
18 changes: 18 additions & 0 deletions internal/resources/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,24 @@ const (

OperatorControlPlaneLabelKey = "control-plane"
OperatorControlPlaneLabelValue = "controller-manager"

// Probe ConfigMap suffixes — appended to df.Name
LivenessProbeConfigMapSuffix = "liveness-probe"
ReadinessProbeConfigMapSuffix = "readiness-probe"
StartupProbeConfigMapSuffix = "startup-probe"

// Script keys — must match the filename in the ConfigMap data
LivenessScriptKey = "liveness-check.sh"
ReadinessScriptKey = "readiness-check.sh"
StartupScriptKey = "startup-check.sh"

// ProbeMountPath is the directory where all probe scripts are mounted
ProbeMountPath = "/etc/dragonfly/probes"

// Volume names for the three probe ConfigMaps
LivenessProbeVolumeName = "liveness-probe"
ReadinessProbeVolumeName = "readiness-probe"
StartupProbeVolumeName = "startup-probe"
)

var DefaultDragonflyArgs = []string{
Expand Down
114 changes: 114 additions & 0 deletions internal/resources/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,76 @@ var (
dflyUserGroup int64 = 999
)

func hasCustomProbes(df *resourcesv1.Dragonfly) bool {
return (df.Spec.CustomLivenessProbeConfigMap != nil && df.Spec.CustomLivenessProbeConfigMap.Name != "") ||
(df.Spec.CustomReadinessProbeConfigMap != nil && df.Spec.CustomReadinessProbeConfigMap.Name != "") ||
(df.Spec.CustomStartupProbeConfigMap != nil && df.Spec.CustomStartupProbeConfigMap.Name != "")
}

// returns the custom ConfigMap name if set, or "<dfName>-<suffix>" otherwise
func probeConfigMapName(dfName, suffix string, custom *corev1.LocalObjectReference) string {
if custom != nil && custom.Name != "" {
return custom.Name
}
return fmt.Sprintf("%s-%s", dfName, suffix)
}

// append probe ConfigMap volumes and mounts into the StatefulSet
func appendProbeVolumesAndMounts(sts *appsv1.StatefulSet, livenessCM, readinessCM, startupCM string) {
probes := []struct {
volumeName string
configMapName string
scriptKey string
}{
{LivenessProbeVolumeName, livenessCM, LivenessScriptKey},
{ReadinessProbeVolumeName, readinessCM, ReadinessScriptKey},
{StartupProbeVolumeName, startupCM, StartupScriptKey},
}
for _, p := range probes {
sts.Spec.Template.Spec.Volumes = append(sts.Spec.Template.Spec.Volumes,
corev1.Volume{
Name: p.volumeName,
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{Name: p.configMapName},
},
},
},
)
sts.Spec.Template.Spec.Containers[0].VolumeMounts = append(
sts.Spec.Template.Spec.Containers[0].VolumeMounts,
corev1.VolumeMount{
Name: p.volumeName,
MountPath: ProbeMountPath + "/" + p.scriptKey,
SubPath: p.scriptKey,
},
)
}
}

// generateProbeConfigMap builds a ConfigMap owned by the Dragonfly instance for a single probe script.
func generateProbeConfigMap(df *resourcesv1.Dragonfly, name, key, script string) *corev1.ConfigMap {
return &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: df.Namespace,
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: df.APIVersion,
Kind: df.Kind,
Name: df.Name,
UID: df.UID,
},
},
Labels: generateResourceLabels(df),
Annotations: generateResourceAnnotations(df),
},
Data: map[string]string{
key: script,
},
}
}

// GenerateDragonflyResources returns the resources required for a Dragonfly
// Instance
func GenerateDragonflyResources(df *resourcesv1.Dragonfly, defaultDragonflyImage string) ([]client.Object, error) {
Expand Down Expand Up @@ -103,9 +173,13 @@ func GenerateDragonflyResources(df *resourcesv1.Dragonfly, defaultDragonflyImage
},
Args: DefaultDragonflyArgs,
Env: append(df.Spec.Env, corev1.EnvVar{
// Use the admin port for health checks — it never requires TLS,
// making probes work correctly on TLS-enabled clusters.
Name: "HEALTHCHECK_PORT",
Value: fmt.Sprintf("%d", DragonflyAdminPort),
}),
// Default probes use the image-embedded healthcheck script.
// When custom probe ConfigMaps are set, these are replaced below.
ReadinessProbe: &corev1.Probe{
ProbeHandler: corev1.ProbeHandler{
Exec: &corev1.ExecAction{
Expand Down Expand Up @@ -398,6 +472,46 @@ func GenerateDragonflyResources(df *resourcesv1.Dragonfly, defaultDragonflyImage
}
}

// When custom probe ConfigMaps are set, switch probes to use mounted scripts
// and wire volumes/mounts. Otherwise keep the default image-embedded healthcheck
// to avoid triggering a rolling update on operator upgrade.
if hasCustomProbes(df) {
c := &statefulset.Spec.Template.Spec.Containers[0]
c.ReadinessProbe.Exec.Command = []string{"/bin/sh", ProbeMountPath + "/" + ReadinessScriptKey}
c.LivenessProbe.Exec.Command = []string{"/bin/sh", ProbeMountPath + "/" + LivenessScriptKey}
c.StartupProbe = &corev1.Probe{
ProbeHandler: corev1.ProbeHandler{
Exec: &corev1.ExecAction{
Command: []string{"/bin/sh", ProbeMountPath + "/" + StartupScriptKey},
},
},
FailureThreshold: 60,
InitialDelaySeconds: 0,
PeriodSeconds: 5,
SuccessThreshold: 1,
TimeoutSeconds: 5,
}

livenessConfigMapName := probeConfigMapName(df.Name, LivenessProbeConfigMapSuffix, df.Spec.CustomLivenessProbeConfigMap)
readinessConfigMapName := probeConfigMapName(df.Name, ReadinessProbeConfigMapSuffix, df.Spec.CustomReadinessProbeConfigMap)
startupConfigMapName := probeConfigMapName(df.Name, StartupProbeConfigMapSuffix, df.Spec.CustomStartupProbeConfigMap)

appendProbeVolumesAndMounts(&statefulset,
livenessConfigMapName, readinessConfigMapName, startupConfigMapName,
)

// Generate default ConfigMaps for probes not overridden by the user.
if df.Spec.CustomLivenessProbeConfigMap == nil || df.Spec.CustomLivenessProbeConfigMap.Name == "" {
resources = append(resources, generateProbeConfigMap(df, livenessConfigMapName, LivenessScriptKey, defaultLivenessScript))
}
if df.Spec.CustomReadinessProbeConfigMap == nil || df.Spec.CustomReadinessProbeConfigMap.Name == "" {
resources = append(resources, generateProbeConfigMap(df, readinessConfigMapName, ReadinessScriptKey, defaultReadinessScript))
}
if df.Spec.CustomStartupProbeConfigMap == nil || df.Spec.CustomStartupProbeConfigMap.Name == "" {
resources = append(resources, generateProbeConfigMap(df, startupConfigMapName, StartupScriptKey, defaultStartupScript))
}
}

statefulset.Spec.Template.Spec.Containers = mergeNamedSlices(
statefulset.Spec.Template.Spec.Containers, df.Spec.AdditionalContainers,
func(c corev1.Container) string { return c.Name })
Expand Down
Loading