Repository: argoproj-labs/argocd-image-updater

Issue:
Image Updater crashes if the contents of `.argocd-source-image-updater-<image>.yaml` are deleted


## **Summary**

In Argo CD Image Updater v1.0.0, when it writes back image updates to Git, it creates a metadata file such as:

```
.argocd-source-image-updater-<name>.yaml
```

This file stores internal state for the updater.

If the **contents of this file are deleted** (file exists but is empty ), the Image Updater pod immediately goes into **CrashLoopBackOff** due to a **nil pointer error** during processing.

Expected behavior is that Image Updater should handle this scenario gracefully and **recreate or repopulate the metadata file**, instead of crashing.


## **Version Information**

* **Argo CD:** 3.2.0
* **Image Updater:** 1.0.0
* **Write-back method:** git
* **Image Source:** ECR


## **Steps to Reproduce**

1. Configure Image Updater to track an image (e.g., `abc-dev/proxy`).
2. Allow Image Updater to perform an update.
   It creates the metadata file:

   ```
   .argocd-source-image-updater-proxy.yaml
   ```
3. Commit/push the update normally.
4. Manually **erase the contents of the file** (make it empty).
   Example:

   ```
   > .argocd-source-image-updater-proxy.yaml
   ```
5. Push the empty file to the Git repo.
6. Image Updater syncs again.


## **Actual Behavior**

* Image Updater pod crashes.
* Pod enters **CrashLoopBackOff**.
* Logs show **nil pointer dereference** or similar error.
* Updater stops functioning entirely.


## **Expected Behavior**

* Image Updater should **not crash** when the metadata file exists but contains no data.
* It should:

  * Detect that the file is empty,
  * Recreate it with valid metadata, or
  * Continue normal update logic without throwing a nil error.
* Pod should remain healthy.




Discussion:
@dkarpele: Thanks for reporting. Could you paste nil pointer exception trace here?@anishbista60: ```
time="2025-11-12T09:30:05Z" level=info msg="Initializing git@github.com:enkryptai/enkryptai-apiaas.git to /tmp/git-redteam-proxy1517082338" application=argocd/redteam-proxy controller=imageupdater controllerGroup=argocd-image-updater.argoproj.io controllerKind=ImageUpdater imageUpdater_name=all-apps-image-updater imageUpdater_namespace=argocd logger=reconcile
time="2025-11-12T09:30:05Z" level=warning msg="temporarily disabling strict host key checking (i.e. '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null'), please don't use in production" application=argocd/redteam-proxy controller=imageupdater controllerGroup=argocd-image-updater.argoproj.io controllerKind=ImageUpdater imageUpdater_name=all-apps-image-updater imageUpdater_namespace=argocd logger=reconcile
time="2025-11-12T09:30:05Z" level=info msg="git fetch origin dev --force --prune --depth 1" dir=/tmp/git-redteam-proxy1517082338 execID=46149
{"args":"[git fetch origin dev --force --prune --depth 1]","dir":"/tmp/git-redteam-proxy1517082338","level":"info","msg":"Trace","operation_name":"exec git","time":"2025-11-12T09:30:10Z","time_ms":4892.455515}
time="2025-11-12T09:30:10Z" level=info msg="git checkout --force dev" dir=/tmp/git-redteam-proxy1517082338 execID=c81b7
{"args":"[git checkout --force dev]","dir":"/tmp/git-redteam-proxy1517082338","level":"info","msg":"Trace","operation_name":"exec git","time":"2025-11-12T09:30:12Z","time_ms":1531.432543}
time="2025-11-12T09:30:12Z" level=info msg="git clean -ffdx" dir=/tmp/git-redteam-proxy1517082338 execID=c5c0e
{"args":"[git clean -ffdx]","dir":"/tmp/git-redteam-proxy1517082338","level":"info","msg":"Trace","operation_name":"exec git","time":"2025-11-12T09:30:12Z","time_ms":17.700633}
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x25274e4]

goroutine 217 [running]:
github.com/argoproj-labs/argocd-image-updater/pkg/argocd.mergeKustomizeOverride(0xc0005762a8, 0xc0015f7050?)
    /src/argocd-image-updater/pkg/argocd/update.go:517 +0x84
github.com/argoproj-labs/argocd-image-updater/pkg/argocd.marshalParamsOverride({0x3836ad0, 0xc0015f7050}, 0xc000bbb208, {0xc000324a00, 0x1, 0x200})
    /src/argocd-image-updater/pkg/argocd/update.go:375 +0x65c
github.com/argoproj-labs/argocd-image-updater/pkg/argocd.writeOverrides({0x3836ad0, 0xc0015f7050}, 0xc000bbb208, {0x386bd40, 0xc0005183f0})
    /src/argocd-image-updater/pkg/argocd/git.go:328 +0x1b9
github.com/argoproj-labs/argocd-image-updater/pkg/argocd.commitChangesGit({0x3836ad0, 0xc0015f7050}, 0xc000bbb208, {0xc000883410, 0x1, 0x1}, 0x34d08e8)
    /src/argocd-image-updater/pkg/argocd/git.go:254 +0x84c
github.com/argoproj-labs/argocd-image-updater/pkg/argocd.commitChanges({0x3836ad0, 0xc0015f7050}, 0xc000bbb208, {0xc000883410, 0x1, 0x1})
    /src/argocd-image-updater/pkg/argocd/update.go:846 +0x127
github.com/argoproj-labs/argocd-image-updater/pkg/argocd.commitChangesLocked({0x3836ad0?, 0xc0015f7050?}, 0xc000bbb208?, 0xc000a72420?, {0xc000883410?, 0x1?, 0x1?})
    /src/argocd-image-updater/pkg/argocd/update.go:820 +0x11c
github.com/argoproj-labs/argocd-image-updater/pkg/argocd.UpdateApplication({0x3836ad0, 0xc0015f7050}, 0xc000f9dae0, 0xc000dc66b0)
    /src/argocd-image-updater/pkg/argocd/update.go:239 +0x1639
github.com/argoproj-labs/argocd-image-updater/internal/controller.(*ImageUpdaterReconciler).RunImageUpdater.func1({_, _}, {{{{0x290459b, 0xb}, {0xc000b00a08, 0x14}}, {{0xc000a72420, 0xd}, {0x0, 0x0}, ...}, ...}, ...})
    /src/argocd-image-updater/internal/controller/reconcile.go:93 +0x33d
created by github.com/argoproj-labs/argocd-image-updater/internal/controller.(*ImageUpdaterReconciler).RunImageUpdater in goroutine 157
    /src/argocd-image-updater/internal/controller/reconcile.go:72 +0x7b8
stream closed EOF for argocd-image-updater-system/argocd-image-updater-controller-7db5894b54-q2bjw (argocd-image-updater-controller)

Solution patch:
diff --git a/pkg/argocd/update.go b/pkg/argocd/update.go
index 13b03a5..bb9aa97 100644
--- a/pkg/argocd/update.go
+++ b/pkg/argocd/update.go
@@ -631,6 +631,13 @@ func mergeHelmOverride(t *helmOverride, o *helmOverride) {
 }
 
 func mergeKustomizeOverride(t *kustomizeOverride, o *kustomizeOverride) {
+	if o.Kustomize.Images == nil {
+		return
+	}
+	if t.Kustomize.Images == nil {
+		emptyImages := make(v1alpha1.KustomizeImages, 0)
+		t.Kustomize.Images = &emptyImages
+	}
 	for _, newImage := range *o.Kustomize.Images {
 		found := false
 		newContainerImage := image.NewFromIdentifier(string(newImage))



Test patch:
diff --git a/pkg/argocd/update_test.go b/pkg/argocd/update_test.go
index 80ebc19..0d15829 100644
--- a/pkg/argocd/update_test.go
+++ b/pkg/argocd/update_test.go
@@ -4453,57 +4453,74 @@ func Test_GetRepositoryLock(t *testing.T) {
 func Test_mergeKustomizeOverride(t *testing.T) {
 	tests := []struct {
 		name     string
-		existing v1alpha1.KustomizeImages
-		new      v1alpha1.KustomizeImages
-		expected v1alpha1.KustomizeImages
+		existing *v1alpha1.KustomizeImages
+		new      *v1alpha1.KustomizeImages
+		expected *v1alpha1.KustomizeImages
 	}{
-		{"with-tag", []v1alpha1.KustomizeImage{"nginx:foo"},
-			[]v1alpha1.KustomizeImage{"nginx:foo"},
-			[]v1alpha1.KustomizeImage{"nginx:foo"}},
-		{"no-tag", []v1alpha1.KustomizeImage{"nginx:foo"},
-			[]v1alpha1.KustomizeImage{"nginx"},
-			[]v1alpha1.KustomizeImage{"nginx:foo"}},
-		{"with-tag-1", []v1alpha1.KustomizeImage{"nginx"},
-			[]v1alpha1.KustomizeImage{"nginx:latest"},
-			[]v1alpha1.KustomizeImage{"nginx:latest"}},
-		{"with-tag-sha", []v1alpha1.KustomizeImage{"nginx:latest"},
-			[]v1alpha1.KustomizeImage{"nginx:latest@sha256:91734281c0ebfc6f1aea979cffeed5079cfe786228a71cc6f1f46a228cde6e34"},
-			[]v1alpha1.KustomizeImage{"nginx:latest@sha256:91734281c0ebfc6f1aea979cffeed5079cfe786228a71cc6f1f46a228cde6e34"}},
-
-		{"2-images", []v1alpha1.KustomizeImage{"nginx:latest",
+		{"with-tag", &v1alpha1.KustomizeImages{"nginx:foo"},
+			&v1alpha1.KustomizeImages{"nginx:foo"},
+			&v1alpha1.KustomizeImages{"nginx:foo"}},
+		{"no-tag", &v1alpha1.KustomizeImages{"nginx:foo"},
+			&v1alpha1.KustomizeImages{"nginx"},
+			&v1alpha1.KustomizeImages{"nginx:foo"}},
+		{"with-tag-1", &v1alpha1.KustomizeImages{"nginx"},
+			&v1alpha1.KustomizeImages{"nginx:latest"},
+			&v1alpha1.KustomizeImages{"nginx:latest"}},
+		{"with-tag-sha", &v1alpha1.KustomizeImages{"nginx:latest"},
+			&v1alpha1.KustomizeImages{"nginx:latest@sha256:91734281c0ebfc6f1aea979cffeed5079cfe786228a71cc6f1f46a228cde6e34"},
+			&v1alpha1.KustomizeImages{"nginx:latest@sha256:91734281c0ebfc6f1aea979cffeed5079cfe786228a71cc6f1f46a228cde6e34"}},
+
+		{"2-images", &v1alpha1.KustomizeImages{"nginx:latest",
 			"bitnami/nginx:latest@sha256:1a2fe3f9f6d1d38d5a7ee35af732fdb7d15266ec3dbc79bbc0355742cd24d3ec"},
-			[]v1alpha1.KustomizeImage{"nginx:latest@sha256:91734281c0ebfc6f1aea979cffeed5079cfe786228a71cc6f1f46a228cde6e34",
+			&v1alpha1.KustomizeImages{"nginx:latest@sha256:91734281c0ebfc6f1aea979cffeed5079cfe786228a71cc6f1f46a228cde6e34",
 				"bitnami/nginx@sha256:1a2fe3f9f6d1d38d5a7ee35af732fdb7d15266ec3dbc79bbc0355742cd24d3ec"},
-			[]v1alpha1.KustomizeImage{"nginx:latest@sha256:91734281c0ebfc6f1aea979cffeed5079cfe786228a71cc6f1f46a228cde6e34",
+			&v1alpha1.KustomizeImages{"nginx:latest@sha256:91734281c0ebfc6f1aea979cffeed5079cfe786228a71cc6f1f46a228cde6e34",
 				"bitnami/nginx:latest@sha256:1a2fe3f9f6d1d38d5a7ee35af732fdb7d15266ec3dbc79bbc0355742cd24d3ec"}},
 
-		{"with-registry", []v1alpha1.KustomizeImage{"quay.io/nginx:latest"},
-			[]v1alpha1.KustomizeImage{"quay.io/nginx:latest"},
-			[]v1alpha1.KustomizeImage{"quay.io/nginx:latest"}},
-		{"with-registry-1", []v1alpha1.KustomizeImage{"quay.io/nginx:latest"},
-			[]v1alpha1.KustomizeImage{"docker.io/nginx:latest"},
-			[]v1alpha1.KustomizeImage{"docker.io/nginx:latest", "quay.io/nginx:latest"}},
+		{"with-registry", &v1alpha1.KustomizeImages{"quay.io/nginx:latest"},
+			&v1alpha1.KustomizeImages{"quay.io/nginx:latest"},
+			&v1alpha1.KustomizeImages{"quay.io/nginx:latest"}},
+		{"with-registry-1", &v1alpha1.KustomizeImages{"quay.io/nginx:latest"},
+			&v1alpha1.KustomizeImages{"docker.io/nginx:latest"},
+			&v1alpha1.KustomizeImages{"docker.io/nginx:latest", "quay.io/nginx:latest"}},
+		{"o_is_nil", &v1alpha1.KustomizeImages{"nginx:foo"},
+			nil,
+			&v1alpha1.KustomizeImages{"nginx:foo"}},
+		{"t_is_nil", nil,
+			&v1alpha1.KustomizeImages{"nginx:foo"},
+			&v1alpha1.KustomizeImages{"nginx:foo"}},
+		{"both_are_nil", nil,
+			nil,
+			nil},
+		{"add_to_empty", &v1alpha1.KustomizeImages{},
+			&v1alpha1.KustomizeImages{"nginx:foo"},
+			&v1alpha1.KustomizeImages{"nginx:foo"}},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			existingImages := kustomizeOverride{
 				Kustomize: kustomizeImages{
-					Images: &tt.existing,
+					Images: tt.existing,
 				},
 			}
 			newImages := kustomizeOverride{
 				Kustomize: kustomizeImages{
-					Images: &tt.new,
+					Images: tt.new,
 				},
 			}
 			expectedImages := kustomizeOverride{
 				Kustomize: kustomizeImages{
-					Images: &tt.expected,
+					Images: tt.expected,
 				},
 			}
 
 			mergeKustomizeOverride(&existingImages, &newImages)
-			assert.ElementsMatch(t, *expectedImages.Kustomize.Images, *existingImages.Kustomize.Images)
+			if expectedImages.Kustomize.Images == nil {
+				assert.Nil(t, existingImages.Kustomize.Images)
+			} else {
+				require.NotNil(t, existingImages.Kustomize.Images)
+				assert.ElementsMatch(t, *expectedImages.Kustomize.Images, *existingImages.Kustomize.Images)
+			}
 		})
 	}
 }