This is an automated email from the ASF dual-hosted git repository.

villebro pushed a commit to branch main
in repository 
https://gitbox.apache.org/repos/asf/superset-kubernetes-operator.git


The following commit(s) were added to refs/heads/main by this push:
     new 67d9487  fix(lifecycle): detect image changes in pending/running task 
pods (#28)
67d9487 is described below

commit 67d94870b1fae50c62b413dd1a414828fce75ce0
Author: Ville Brofeldt <[email protected]>
AuthorDate: Fri May 8 10:05:02 2026 -0700

    fix(lifecycle): detect image changes in pending/running task pods (#28)
---
 docs/installation.md                               |  12 ++-
 .../controller/supersetlifecycletask_controller.go |  17 ++++
 .../supersetlifecycletask_controller_test.go       | 106 +++++++++++++++++++++
 3 files changed, 134 insertions(+), 1 deletion(-)

diff --git a/docs/installation.md b/docs/installation.md
index 2d54a36..24529f3 100644
--- a/docs/installation.md
+++ b/docs/installation.md
@@ -43,7 +43,17 @@ helm install superset-operator \
 ```
 
 Replace `<version>` with a published chart version (e.g., `0.1.0`). Use
-`0.0.0-dev` for the latest build from main.
+`0.0.0-dev` for the latest build from main. When using the `dev` version,
+set `image.pullPolicy=Always` to ensure you always get the latest image:
+
+```bash
+helm install superset-operator \
+  oci://ghcr.io/apache/superset-kubernetes-operator/charts/superset-operator \
+  --version 0.0.0-dev \
+  --namespace superset-operator-system \
+  --create-namespace \
+  --set image.pullPolicy=Always
+```
 
 ### From a source checkout
 
diff --git a/internal/controller/supersetlifecycletask_controller.go 
b/internal/controller/supersetlifecycletask_controller.go
index a5ee7c1..f4a38f0 100644
--- a/internal/controller/supersetlifecycletask_controller.go
+++ b/internal/controller/supersetlifecycletask_controller.go
@@ -106,6 +106,7 @@ func (r *SupersetLifecycleTaskReconciler) 
reconcileInitPod(ctx context.Context,
                        if err := r.resetForConfigChange(ctx, log, taskCR, 
resourceBaseName); err != nil {
                                return ctrl.Result{}, err
                        }
+                       taskCR.Status.Image = image
                } else {
                        return ctrl.Result{}, nil
                }
@@ -126,6 +127,22 @@ func (r *SupersetLifecycleTaskReconciler) 
reconcileInitPod(ctx context.Context,
        if existingPod != nil {
                taskCR.Status.PodName = existingPod.Name
 
+               // If the desired image changed (e.g., tag was corrected), 
delete the
+               // stale pod so it gets recreated with the updated image.
+               if taskCR.Status.Image != "" && taskCR.Status.Image != image {
+                       log.Info("Image changed, deleting stale pod", "old", 
taskCR.Status.Image, "new", image)
+                       if err := r.Delete(ctx, existingPod); 
client.IgnoreNotFound(err) != nil {
+                               return ctrl.Result{}, err
+                       }
+                       taskCR.Status.State = initStatePending
+                       taskCR.Status.Image = image
+                       taskCR.Status.PodName = ""
+                       taskCR.Status.Message = "Image changed, re-running task"
+                       r.Recorder.Eventf(taskCR, nil, corev1.EventTypeNormal, 
"ImageChanged", "Reconcile",
+                               "Image changed from %s to %s, re-running task", 
taskCR.Status.Image, image)
+                       return ctrl.Result{RequeueAfter: time.Second}, nil
+               }
+
                switch existingPod.Status.Phase {
                case corev1.PodSucceeded:
                        log.Info("Init pod succeeded", "pod", existingPod.Name)
diff --git a/internal/controller/supersetlifecycletask_controller_test.go 
b/internal/controller/supersetlifecycletask_controller_test.go
index 86c7abc..431f6ea 100644
--- a/internal/controller/supersetlifecycletask_controller_test.go
+++ b/internal/controller/supersetlifecycletask_controller_test.go
@@ -694,6 +694,112 @@ func 
TestInitReconcile_FailedExhausted_ConfigChanged_ReRunsInit(t *testing.T) {
        }
 }
 
+func TestInitReconcile_ImageChanged_DeletesStalePod(t *testing.T) {
+       scheme := testScheme(t)
+       initCR := minimalInitCR()
+       initCR.Spec.Image.Tag = "new-tag"
+       initCR.Status.State = initStateRunning
+       initCR.Status.Image = "apache/superset:old-tag"
+
+       stalePod := &corev1.Pod{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      "test-init-stale",
+                       Namespace: "default",
+                       Labels: map[string]string{
+                               labelInitInstance: "test-init",
+                               labelInitTask:     initTaskName,
+                       },
+               },
+               Status: corev1.PodStatus{Phase: corev1.PodPending},
+       }
+
+       c := fake.NewClientBuilder().
+               WithScheme(scheme).
+               WithObjects(initCR, stalePod).
+               WithStatusSubresource(initCR).
+               Build()
+
+       r := &SupersetLifecycleTaskReconciler{Client: c, Scheme: scheme, 
Recorder: events.NewFakeRecorder(10)}
+
+       result, err := r.Reconcile(context.Background(), reconcile.Request{
+               NamespacedName: types.NamespacedName{Name: "test-init", 
Namespace: "default"},
+       })
+       if err != nil {
+               t.Fatalf("reconcile: %v", err)
+       }
+       if result.RequeueAfter == 0 {
+               t.Error("expected RequeueAfter > 0 after image change")
+       }
+
+       // Stale pod should be deleted.
+       podList := &corev1.PodList{}
+       if err := c.List(context.Background(), podList); err != nil {
+               t.Fatalf("list pods: %v", err)
+       }
+       if len(podList.Items) != 0 {
+               t.Errorf("expected stale pod to be deleted, got %d pods", 
len(podList.Items))
+       }
+
+       // Status should be reset.
+       updatedCR := &supersetv1alpha1.SupersetLifecycleTask{}
+       if err := c.Get(context.Background(), types.NamespacedName{Name: 
"test-init", Namespace: "default"}, updatedCR); err != nil {
+               t.Fatalf("get updated CR: %v", err)
+       }
+       if updatedCR.Status.State != initStatePending {
+               t.Errorf("expected state Pending, got %s", 
updatedCR.Status.State)
+       }
+       if updatedCR.Status.Image != "apache/superset:new-tag" {
+               t.Errorf("expected image apache/superset:new-tag, got %s", 
updatedCR.Status.Image)
+       }
+       if updatedCR.Status.PodName != "" {
+               t.Errorf("expected podName cleared, got %s", 
updatedCR.Status.PodName)
+       }
+}
+
+func TestInitReconcile_ImageUnchanged_NoReset(t *testing.T) {
+       scheme := testScheme(t)
+       initCR := minimalInitCR()
+       initCR.Spec.Image.Tag = "latest"
+       initCR.Status.State = initStateRunning
+       initCR.Status.Image = "apache/superset:latest"
+
+       pod := &corev1.Pod{
+               ObjectMeta: metav1.ObjectMeta{
+                       Name:      "test-init-abc",
+                       Namespace: "default",
+                       Labels: map[string]string{
+                               labelInitInstance: "test-init",
+                               labelInitTask:     initTaskName,
+                       },
+               },
+               Status: corev1.PodStatus{Phase: corev1.PodPending},
+       }
+
+       c := fake.NewClientBuilder().
+               WithScheme(scheme).
+               WithObjects(initCR, pod).
+               WithStatusSubresource(initCR).
+               Build()
+
+       r := &SupersetLifecycleTaskReconciler{Client: c, Scheme: scheme, 
Recorder: events.NewFakeRecorder(10)}
+
+       _, err := r.Reconcile(context.Background(), reconcile.Request{
+               NamespacedName: types.NamespacedName{Name: "test-init", 
Namespace: "default"},
+       })
+       if err != nil {
+               t.Fatalf("reconcile: %v", err)
+       }
+
+       // Pod should still exist (not deleted).
+       podList := &corev1.PodList{}
+       if err := c.List(context.Background(), podList); err != nil {
+               t.Fatalf("list pods: %v", err)
+       }
+       if len(podList.Items) != 1 {
+               t.Errorf("expected pod to still exist, got %d pods", 
len(podList.Items))
+       }
+}
+
 func TestInitReconcile_NotFound(t *testing.T) {
        scheme := testScheme(t)
        c := fake.NewClientBuilder().WithScheme(scheme).Build()

Reply via email to