diff --git a/charts/dragonfly-operator/templates/deployment.yaml b/charts/dragonfly-operator/templates/deployment.yaml index 269b1aee..aac7a59c 100644 --- a/charts/dragonfly-operator/templates/deployment.yaml +++ b/charts/dragonfly-operator/templates/deployment.yaml @@ -62,6 +62,9 @@ spec: - name: manager args: - --leader-elect + {{- if .Values.dragonflyImage }} + - --dragonfly-image={{ .Values.dragonflyImage }} + {{- end }} {{- if .Values.rbacProxy.enabled }} - --metrics-bind-address=127.0.0.1:8080 {{- end }} diff --git a/charts/dragonfly-operator/values.yaml b/charts/dragonfly-operator/values.yaml index b8245c78..1c1fae78 100644 --- a/charts/dragonfly-operator/values.yaml +++ b/charts/dragonfly-operator/values.yaml @@ -14,6 +14,9 @@ crds: nameOverride: "" fullnameOverride: "" +# -- Default dragonfly image to use +dragonflyImage: "" + # -- Additional labels to add to all resources additionalLabels: {} # app: dragonfly-operator diff --git a/cmd/main.go b/cmd/main.go index 5e47ebc2..6ed6724b 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -70,8 +70,10 @@ func main() { var probeAddr string var versionFlag bool var watchCurrentNamespace bool + var dragonflyImage string flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") + flag.StringVar(&dragonflyImage, "dragonfly-image", "", "The default dragonfly image to use.") flag.BoolVar(&enableLeaderElection, "leader-elect", false, "Enable leader election for controller manager. "+ "Enabling this will ensure there is only one active controller manager.") @@ -145,9 +147,10 @@ func main() { if err = (&controller.DragonflyReconciler{ Reconciler: controller.Reconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - EventRecorder: eventRecorder, + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + EventRecorder: eventRecorder, + DefaultDragonflyImage: dragonflyImage, }, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Dragonfly") @@ -156,9 +159,10 @@ func main() { if err = (&controller.DfPodLifeCycleReconciler{ Reconciler: controller.Reconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - EventRecorder: eventRecorder, + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + EventRecorder: eventRecorder, + DefaultDragonflyImage: dragonflyImage, }, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Health") diff --git a/internal/controller/base_controller.go b/internal/controller/base_controller.go index 9d74b025..9d83fc15 100644 --- a/internal/controller/base_controller.go +++ b/internal/controller/base_controller.go @@ -18,6 +18,7 @@ package controller import ( "context" + dfv1alpha1 "github.com/dragonflydb/dragonfly-operator/api/v1alpha1" "github.com/go-logr/logr" "k8s.io/apimachinery/pkg/runtime" @@ -27,9 +28,10 @@ import ( ) type Reconciler struct { - Client client.Client - Scheme *runtime.Scheme - EventRecorder record.EventRecorder + Client client.Client + Scheme *runtime.Scheme + EventRecorder record.EventRecorder + DefaultDragonflyImage string } func (r *Reconciler) getDragonflyInstance(ctx context.Context, namespacedName types.NamespacedName, log logr.Logger) (*DragonflyInstance, error) { @@ -41,10 +43,11 @@ func (r *Reconciler) getDragonflyInstance(ctx context.Context, namespacedName ty } return &DragonflyInstance{ - df: &df, - client: r.Client, - log: log, - scheme: r.Scheme, - eventRecorder: r.EventRecorder, + df: &df, + client: r.Client, + log: log, + scheme: r.Scheme, + eventRecorder: r.EventRecorder, + defaultDragonflyImage: r.DefaultDragonflyImage, }, nil } diff --git a/internal/controller/dragonfly_instance.go b/internal/controller/dragonfly_instance.go index 21d0547c..b1b638b1 100644 --- a/internal/controller/dragonfly_instance.go +++ b/internal/controller/dragonfly_instance.go @@ -46,10 +46,11 @@ type DragonflyInstance struct { // Dragonfly is the relevant Dragonfly CRD that it performs actions over df *dfv1alpha1.Dragonfly - client client.Client - log logr.Logger - scheme *runtime.Scheme - eventRecorder record.EventRecorder + client client.Client + log logr.Logger + scheme *runtime.Scheme + eventRecorder record.EventRecorder + defaultDragonflyImage string } // configureReplication configures the given pod as a master and other pods as replicas @@ -487,7 +488,7 @@ func (dfi *DragonflyInstance) replicaOfNoOne(ctx context.Context, pod *corev1.Po // reconcileResources creates or updates the dragonfly resources func (dfi *DragonflyInstance) reconcileResources(ctx context.Context) error { - dfResources, err := resources.GenerateDragonflyResources(dfi.df) + dfResources, err := resources.GenerateDragonflyResources(dfi.df, dfi.defaultDragonflyImage) if err != nil { return fmt.Errorf("failed to generate dragonfly resources") } diff --git a/internal/resources/image_test.go b/internal/resources/image_test.go new file mode 100644 index 00000000..6a12fedd --- /dev/null +++ b/internal/resources/image_test.go @@ -0,0 +1,62 @@ +package resources + +import ( + "fmt" + "testing" + + resourcesv1 "github.com/dragonflydb/dragonfly-operator/api/v1alpha1" + "github.com/stretchr/testify/assert" + appsv1 "k8s.io/api/apps/v1" +) + +func TestGenerateDragonflyResources_ImageResolution(t *testing.T) { + tests := []struct { + name string + crdImage string + defaultImage string + expectedImage string + }{ + { + name: "CRD image takes precedence", + crdImage: "crd-image:v1", + defaultImage: "default-image:v1", + expectedImage: "crd-image:v1", + }, + { + name: "Default image used when CRD image is empty", + crdImage: "", + defaultImage: "default-image:v1", + expectedImage: "default-image:v1", + }, + { + name: "Hardcoded default used when both are empty", + crdImage: "", + defaultImage: "", + expectedImage: fmt.Sprintf("%s:%s", DragonflyImage, Version), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + df := &resourcesv1.Dragonfly{ + Spec: resourcesv1.DragonflySpec{ + Image: tt.crdImage, + }, + } + + objs, err := GenerateDragonflyResources(df, tt.defaultImage) + assert.NoError(t, err) + + var sts *appsv1.StatefulSet + for _, obj := range objs { + if s, ok := obj.(*appsv1.StatefulSet); ok { + sts = s + break + } + } + + assert.NotNil(t, sts) + assert.Equal(t, tt.expectedImage, sts.Spec.Template.Spec.Containers[0].Image) + }) + } +} diff --git a/internal/resources/resources.go b/internal/resources/resources.go index 70796e1a..fc505482 100644 --- a/internal/resources/resources.go +++ b/internal/resources/resources.go @@ -34,12 +34,16 @@ var ( // GenerateDragonflyResources returns the resources required for a Dragonfly // Instance -func GenerateDragonflyResources(df *resourcesv1.Dragonfly) ([]client.Object, error) { +func GenerateDragonflyResources(df *resourcesv1.Dragonfly, defaultDragonflyImage string) ([]client.Object, error) { var resources []client.Object image := df.Spec.Image if image == "" { - image = fmt.Sprintf("%s:%s", DragonflyImage, Version) + if defaultDragonflyImage != "" { + image = defaultDragonflyImage + } else { + image = fmt.Sprintf("%s:%s", DragonflyImage, Version) + } } // Create a StatefulSet, Headless Service