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()