Component: argocd-image-updater
Version: v1.1.1
Configuration method: CR-based (ImageUpdater custom resource)
Update strategy: digest
Write-back method: git
Summary
When a single ArgoCD application has two containers that use the same image name (registry + repository) but different tags, and both containers are tracked as separate aliases in an ImageUpdater CR, the updater incorrectly reads the current tag/digest of alias A from the container belonging to alias B. This causes:
- The wrong tag name to be recorded as the "current" tag for the misidentified alias.
- A wrong tag prefix (e.g.
latest) to be written into the git write-back override file for the other container.
- An infinite commit loop — the updater commits a "change" every reconcile cycle even though the desired state is already in place, because it perpetually reads a stale/wrong digest for one of the aliases.
Environment
- ArgoCD image-updater: v1.1.1
- ArgoCD: (standard deployment,
argocd namespace)
- Write-back method: git
- Helm-based application
- Two containers in one
Deployment, both pulling from the same registry+repository path, under different named tags (tag-a and tag-b)
Reproduction Steps
1. ArgoCD application values (helm)
Two containers in the same deployment, same image repository, different tags:
# values.yaml (simplified)
containers:
app:
image:
name: registry.example.com/myrepo/myimage
tag: prod
app-nginx:
image:
name: registry.example.com/myrepo/myimage
tag: nginx-variant
2. ImageUpdater CR
apiVersion: argocd-image-updater.argoproj.io/v1alpha1
kind: ImageUpdater
metadata:
name: argocd-image-updater
spec:
writeBackConfig:
method: git
applicationRefs:
- namePattern: "my-app-prod"
images:
- alias: app
imageName: registry.example.com/myrepo/myimage:prod
commonUpdateSettings:
updateStrategy: digest
allowTags: "regexp:^prod$"
manifestTargets:
helm:
name: containers.app.image.name
tag: containers.app.image.tag
- alias: app-nginx
imageName: registry.example.com/myrepo/myimage:nginx-variant
commonUpdateSettings:
updateStrategy: digest
allowTags: "regexp:^nginx-variant$"
manifestTargets:
helm:
name: containers.app-nginx.image.name
tag: containers.app-nginx.image.tag
3. Trigger
Add the app-nginx alias to the CR for an application where app is already being tracked. On the next reconcile, the bug manifests immediately.
Observed Behaviour
Phase 1 — First reconcile: wrong image_tag read for app alias
The app alias (which tracks tag prod) logs image_tag=nginx-variant, indicating it read the current tag from the app-nginx container rather than its own container. Despite this, both manifestTargets write correctly in the first commit.
Relevant logs (first reconcile, warmup-cache pass):
time="...T05:44:06Z" level=info
msg="Setting new image to registry.example.com/myrepo/myimage:prod@sha256:<DIGEST-PROD>"
image_alias=app
image_name=registry.example.com/myrepo/myimage
image_tag=nginx-variant ← WRONG: should be "prod"
logger=warmup-cache
time="...T05:44:06Z" level=info
msg="Successfully updated image 'registry.example.com/myrepo/myimage:nginx-variant' to
'registry.example.com/myrepo/myimage:prod@sha256:<DIGEST-PROD>', but pending spec update"
image_alias=app
image_tag=nginx-variant ← WRONG source tag reported
logger=warmup-cache
The commit message generated for this first commit is also wrong — it reports both aliases as updating from nginx-variant:
build: automatic update of my-app-prod
updates image myrepo/myimage tag 'nginx-variant' to 'sha256:<DIGEST-PROD>'
updates image myrepo/myimage tag 'nginx-variant' to 'sha256:<DIGEST-NGINX>'
Both lines reference nginx-variant as the source tag. The first line should have referenced prod as the source tag for the app alias.
Phase 2 — Subsequent reconciles: infinite commit loop (2 parameter updates per cycle)
Every ~2 minutes, image-updater commits 2 parameter updates for the application, even though the desired state is already written correctly. The app alias continues reading image_tag=nginx-variant:
time="...T05:46:15Z" level=info
msg="Setting new image to registry.example.com/myrepo/myimage:prod@sha256:<DIGEST-PROD>"
image_alias=app
image_tag=nginx-variant ← still wrong every cycle
logger=reconcile
time="...T05:46:16Z" level=info
msg="Setting new image to registry.example.com/myrepo/myimage:nginx-variant@sha256:<DIGEST-NGINX>"
image_alias=app-nginx
image_tag=nginx-variant ← correct
logger=reconcile
msg="Committing 2 parameter update(s) for application my-app-prod"
This repeats at 05:44, 05:46, 05:48 — every reconcile cycle.
Phase 3 — After ArgoCD sync: tag prefix corruption and 1-parameter loop
After ArgoCD syncs the application and the containers start running with their pinned digests, the running image in the ArgoCD application spec is stored as a bare digest (no tag name prefix). At this point the app alias shifts to reading by digest:
time="...T05:50:27Z" level=info
msg="Setting new image to registry.example.com/myrepo/myimage:prod@sha256:<DIGEST-PROD>"
image_alias=app
image_digest="sha256:<DIGEST-NGINX>" ← reading nginx-variant's digest as app's current digest
logger=reconcile
time="...T05:50:27Z" level=info
msg="Successfully updated image 'registry.example.com/myrepo/myimage@sha256:<DIGEST-NGINX>'
to 'registry.example.com/myrepo/myimage:prod@sha256:<DIGEST-PROD>'"
image_alias=app
image_digest="sha256:<DIGEST-NGINX>"
The updater now commits 1 parameter update per cycle (down from 2). The single parameter changed is containers.app-nginx.image.tag — it is corrupted from:
nginx-variant@sha256:<DIGEST-NGINX>
to:
latest@sha256:<DIGEST-NGINX>
The tag prefix nginx-variant is replaced with latest. The digest itself does not change. This is data corruption: latest may point to a completely different image layer in the registry, causing the wrong binary to be deployed if the override is ever re-applied without a running container pinning the digest.
The infinite loop then continues: every reconcile, containers.app-nginx.image.tag is rewritten from nginx-variant@sha256:<DIGEST-NGINX> to latest@sha256:<DIGEST-NGINX>, generating 21+ spurious git commits over the observed log window.
Git Write-Back: Before and After
Override file before corruption (correct state):
helm:
parameters:
- name: containers.app-nginx.image.name
value: registry.example.com/myrepo/myimage
forcestring: true
- name: containers.app-nginx.image.tag
value: nginx-variant@sha256:<DIGEST-NGINX> # correct
forcestring: true
- name: containers.app.image.name
value: registry.example.com/myrepo/myimage
forcestring: true
- name: containers.app.image.tag
value: prod@sha256:<DIGEST-PROD> # correct
forcestring: true
Override file after corruption (commit introduced by image-updater):
- name: containers.app-nginx.image.tag
value: latest@sha256:<DIGEST-NGINX> # WRONG prefix: 'nginx-variant' → 'latest'
Root Cause (Hypothesis)
During the read phase, when image-updater resolves the current image for each alias from the ArgoCD application spec, it appears to match containers by image name only (registry + repository path), without considering the tag. When two containers share the same image name, both are matched by both aliases — the first (or last) matched container's tag is then attributed to whichever alias happens to process it, causing the cross-alias confusion.
The write phase (using manifestTargets) is respected and writes to the correct helm parameters, but because the read phase fed the wrong current digest into the alias's state, the updater perpetually believes an update is needed and also corrupts the tag prefix of the sibling alias's override parameter.
Expected Behaviour
When two aliases in the same application reference the same image name but different tags, and each alias has explicit manifestTargets pointing to distinct helm parameters, the updater should:
- Read the current image state for each alias from its own
manifestTargets helm parameter (rather than scanning all containers by image name).
- Correctly attribute
image_tag=prod to the app alias and image_tag=nginx-variant to the app-nginx alias.
- Stop committing after the write-back override already reflects the desired state.
- Never write a tag prefix (
latest) that was not configured in allowTags for that alias.
Workaround
Remove one of the conflicting aliases from the ImageUpdater CR so that only one alias tracks the shared image name. The removed alias must be managed with manual digest pins in the git override file.
Additional Notes
- The
allowTags filter is set correctly for both aliases (regexp:^prod$ and regexp:^nginx-variant$). The bug occurs before allowTags filtering — in the current-state read phase.
- The bug is reproducible deterministically: it triggers immediately on the first reconcile after adding a second alias whose image name matches an existing alias.
- The problem does not occur when the two containers use images from different repositories.
Component: argocd-image-updater
Version: v1.1.1
Configuration method: CR-based (
ImageUpdatercustom resource)Update strategy:
digestWrite-back method: git
Summary
When a single ArgoCD application has two containers that use the same image name (registry + repository) but different tags, and both containers are tracked as separate aliases in an
ImageUpdaterCR, the updater incorrectly reads the current tag/digest of aliasAfrom the container belonging to aliasB. This causes:latest) to be written into the git write-back override file for the other container.Environment
argocdnamespace)Deployment, both pulling from the same registry+repository path, under different named tags (tag-aandtag-b)Reproduction Steps
1. ArgoCD application values (helm)
Two containers in the same deployment, same image repository, different tags:
2. ImageUpdater CR
3. Trigger
Add the
app-nginxalias to the CR for an application whereappis already being tracked. On the next reconcile, the bug manifests immediately.Observed Behaviour
Phase 1 — First reconcile: wrong
image_tagread forappaliasThe
appalias (which tracks tagprod) logsimage_tag=nginx-variant, indicating it read the current tag from theapp-nginxcontainer rather than its own container. Despite this, bothmanifestTargetswrite correctly in the first commit.Relevant logs (first reconcile, warmup-cache pass):
The commit message generated for this first commit is also wrong — it reports both aliases as updating from
nginx-variant:Both lines reference
nginx-variantas the source tag. The first line should have referencedprodas the source tag for theappalias.Phase 2 — Subsequent reconciles: infinite commit loop (2 parameter updates per cycle)
Every ~2 minutes, image-updater commits 2 parameter updates for the application, even though the desired state is already written correctly. The
appalias continues readingimage_tag=nginx-variant:This repeats at 05:44, 05:46, 05:48 — every reconcile cycle.
Phase 3 — After ArgoCD sync: tag prefix corruption and 1-parameter loop
After ArgoCD syncs the application and the containers start running with their pinned digests, the running image in the ArgoCD application spec is stored as a bare digest (no tag name prefix). At this point the
appalias shifts to reading by digest:The updater now commits 1 parameter update per cycle (down from 2). The single parameter changed is
containers.app-nginx.image.tag— it is corrupted from:to:
The tag prefix
nginx-variantis replaced withlatest. The digest itself does not change. This is data corruption:latestmay point to a completely different image layer in the registry, causing the wrong binary to be deployed if the override is ever re-applied without a running container pinning the digest.The infinite loop then continues: every reconcile,
containers.app-nginx.image.tagis rewritten fromnginx-variant@sha256:<DIGEST-NGINX>tolatest@sha256:<DIGEST-NGINX>, generating 21+ spurious git commits over the observed log window.Git Write-Back: Before and After
Override file before corruption (correct state):
Override file after corruption (commit introduced by image-updater):
Root Cause (Hypothesis)
During the read phase, when image-updater resolves the current image for each alias from the ArgoCD application spec, it appears to match containers by image name only (registry + repository path), without considering the tag. When two containers share the same image name, both are matched by both aliases — the first (or last) matched container's tag is then attributed to whichever alias happens to process it, causing the cross-alias confusion.
The write phase (using
manifestTargets) is respected and writes to the correct helm parameters, but because the read phase fed the wrong current digest into the alias's state, the updater perpetually believes an update is needed and also corrupts the tag prefix of the sibling alias's override parameter.Expected Behaviour
When two aliases in the same application reference the same image name but different tags, and each alias has explicit
manifestTargetspointing to distinct helm parameters, the updater should:manifestTargetshelm parameter (rather than scanning all containers by image name).image_tag=prodto theappalias andimage_tag=nginx-variantto theapp-nginxalias.latest) that was not configured inallowTagsfor that alias.Workaround
Remove one of the conflicting aliases from the
ImageUpdaterCR so that only one alias tracks the shared image name. The removed alias must be managed with manual digest pins in the git override file.Additional Notes
allowTagsfilter is set correctly for both aliases (regexp:^prod$andregexp:^nginx-variant$). The bug occurs beforeallowTagsfiltering — in the current-state read phase.