Repository: argoproj-labs/argocd-image-updater (Go)

Issue:
Same image is updated multiple times, 1.20 --> 1.20.0 --> 1.20 --> 1.20.0
Seems like the image updater is stuck in a loop of updating the same image back and forth between the same version, 1.20 --> 1.20.0 --> 1.20 --> 1.20.0 etc

I have only seen it happen with this one image this far, and I have multiple other images that are tagged the same way. Image mask is :v1.x

ArgoCD Image updater: v0.11.3
ArgoCD: v2.2.4
Registry: Azure container registry

> time="2022-02-08T08:46:25Z" level=info msg="Starting image update cycle, considering 1 annotated application(s) for update"
time="2022-02-08T08:46:25Z" level=info msg="Setting new image to my-registry/my-image:v1.20" alias=my-alias application=integration-test image_name=my-image image_tag=v1.20.0 registry=my-registry
time="2022-02-08T08:46:25Z" level=info msg="Successfully updated image 'my-registry/my-image:v1.20.0' to 'my-registry/my-image:v1.20', but pending spec update (dry run=false)" alias=my-alias application=integration-test image_name=my-image image_tag=v1.20.0 registry=my-registry
time="2022-02-08T08:46:26Z" level=info msg="Committing 1 parameter update(s) for application integration-test" application=integration-test
time="2022-02-08T08:46:26Z" level=info msg="Successfully updated the live application spec" application=integration-test
time="2022-02-08T08:46:26Z" level=info msg="Processing results: applications=1 images_considered=6 images_skipped=1 images_updated=1 errors=0"
time="2022-02-08T08:48:26Z" level=info msg="Starting image update cycle, considering 1 annotated application(s) for update"
time="2022-02-08T08:48:26Z" level=info msg="Setting new image to my-registry/my-image:v1.20.0" alias=my-alias application=integration-test image_name=my-image image_tag=v1.20 registry=my-registry
time="2022-02-08T08:48:26Z" level=info msg="Successfully updated image 'my-registry/my-image:v1.20' to 'my-registry/my-image:v1.20.0', but pending spec update (dry run=false)" alias=my-alias application=integration-test image_name=my-image image_tag=v1.20 registry=my-registry
time="2022-02-08T08:48:27Z" level=info msg="Committing 1 parameter update(s) for application integration-test" application=integration-test
time="2022-02-08T08:48:27Z" level=info msg="Successfully updated the live application spec" application=integration-test
time="2022-02-08T08:48:27Z" level=info msg="Processing results: applications=1 images_considered=6 images_skipped=1 images_updated=1 errors=0"
time="2022-02-08T08:50:27Z" level=info msg="Starting image update cycle, considering 1 annotated application(s) for update"
time="2022-02-08T08:50:28Z" level=info msg="Processing results: applications=1 images_considered=6 images_skipped=1 images_updated=0 errors=0"
time="2022-02-08T08:52:28Z" level=info msg="Starting image update cycle, considering 1 annotated application(s) for update"
time="2022-02-08T08:52:30Z" level=info msg="Processing results: applications=1 images_considered=6 images_skipped=1 images_updated=0 errors=0"
time="2022-02-08T08:54:30Z" level=info msg="Starting image update cycle, considering 1 annotated application(s) for update"
time="2022-02-08T08:54:31Z" level=info msg="Processing results: applications=1 images_considered=6 images_skipped=1 images_updated=0 errors=0"
time="2022-02-08T08:56:31Z" level=info msg="Starting image update cycle, considering 1 annotated application(s) for update"
time="2022-02-08T08:56:32Z" level=info msg="Setting new image to my-registry/my-image:v1.20" alias=my-alias application=integration-test image_name=my-image image_tag=v1.20.0 registry=my-registry
time="2022-02-08T08:56:32Z" level=info msg="Successfully updated image 'my-registry/my-image:v1.20.0' to 'my-registry/my-image:v1.20', but pending spec update (dry run=false)" alias=my-alias application=integration-test image_name=my-image image_tag=v1.20.0 registry=my-registry
time="2022-02-08T08:56:32Z" level=info msg="Committing 1 parameter update(s) for application integration-test" application=integration-test
time="2022-02-08T08:56:32Z" level=info msg="Successfully updated the live application spec" application=integration-test
time="2022-02-08T08:56:32Z" level=info msg="Processing results: applications=1 images_considered=6 images_skipped=1 images_updated=1 errors=0"
time="2022-02-08T08:58:32Z" level=info msg="Starting image update cycle, considering 1 annotated application(s) for update"
time="2022-02-08T08:58:33Z" level=info msg="Processing results: applications=1 images_considered=6 images_skipped=1 images_updated=0 errors=0"
time="2022-02-08T09:00:34Z" level=info msg="Starting image update cycle, considering 1 annotated application(s) for update"
time="2022-02-08T09:00:34Z" level=info msg="Setting new image to my-registry/my-image:v1.20.0" alias=my-alias application=integration-test image_name=my-image image_tag=v1.20 registry=my-registry
time="2022-02-08T09:00:34Z" level=info msg="Successfully updated image 'my-registry/my-image:v1.20' to 'my-registry/my-image:v1.20.0', but pending spec update (dry run=false)" alias=my-alias application=integration-test image_name=my-image image_tag=v1.20 registry=my-registry
time="2022-02-08T09:00:35Z" level=info msg="Committing 1 parameter update(s) for application integration-test" application=integration-test
time="2022-02-08T09:00:35Z" level=info msg="Successfully updated the live application spec" application=integration-test
time="2022-02-08T09:00:35Z" level=info msg="Processing results: applications=1 images_considered=6 images_skipped=1 images_updated=1 errors=0"
time="2022-02-08T09:02:35Z" level=info msg="Starting image update cycle, considering 1 annotated application(s) for update"
time="2022-02-08T09:02:36Z" level=info msg="Processing results: applications=1 images_considered=6 images_skipped=1 images_updated=0 errors=0"
time="2022-02-08T09:04:37Z" level=info msg="Starting image update cycle, considering 1 annotated application(s) for update"
time="2022-02-08T09:04:37Z" level=info msg="Setting new image to my-registry/my-image:v1.20" alias=my-alias application=integration-test image_name=my-image image_tag=v1.20.0 registry=my-registry
time="2022-02-08T09:04:37Z" level=info msg="Successfully updated image 'my-registry/my-image:v1.20.0' to 'my-registry/my-image:v1.20', but pending spec update (dry run=false)" alias=my-alias application=integration-test image_name=my-image image_tag=v1.20.0 registry=my-registry
time="2022-02-08T09:04:38Z" level=info msg="Committing 1 parameter update(s) for application integration-test" application=integration-test
time="2022-02-08T09:04:38Z" level=info msg="Successfully updated the live application spec" application=integration-test
time="2022-02-08T09:04:38Z" level=info msg="Processing results: applications=1 images_considered=6 images_skipped=1 images_updated=1 errors=0"
time="2022-02-08T09:06:38Z" level=info msg="Starting image update cycle, considering 1 annotated application(s) for update"
time="2022-02-08T09:06:39Z" level=info msg="Setting new image to my-registry/my-image:v1.20.0" alias=my-alias application=integration-test image_name=my-image image_tag=v1.20 registry=my-registry
time="2022-02-08T09:06:39Z" level=info msg="Successfully updated image 'my-registry/my-image:v1.20' to 'my-registry/my-image:v1.20.0', but pending spec update (dry run=false)" alias=my-alias application=integration-test image_name=my-image image_tag=v1.20 registry=my-registry
time="2022-02-08T09:06:39Z" level=info msg="Committing 1 parameter update(s) for application integration-test" application=integration-test
time="2022-02-08T09:06:39Z" level=info msg="Successfully updated the live application spec" application=integration-test
time="2022-02-08T09:06:39Z" level=info msg="Processing results: applications=1 images_considered=6 images_skipped=1 images_updated=1 errors=0"
time="2022-02-08T09:08:40Z" level=info msg="Starting image update cycle, considering 1 annotated application(s) for update"
time="2022-02-08T09:08:41Z" level=info msg="Processing results: applications=1 images_considered=6 images_skipped=1 images_updated=0 errors=0"


Discussion:
From a semver perspective, 1.20.0 and 1.20 are the same versions, but I agree, the result for such situations should be stable and predictable.

As a workaround, you probably want an additional `allow-tags` restriction to allow only tags matching `x.y.z`, e.g. `<youralias>.allow-tags: regexp:^[v]?[0-9]+\.[0-9]+\.[0-9]+$`
Thanks, it works fine using the workaround!

It seems to be an intermittent issue as it at one point stopped updating back and forth some days back (I was not at work so no one touched the system explicitly). But logs still said the following every 2-6th iteration:

> time="2022-02-10T06:34:22Z" level=info msg="Successfully updated image 'my-registry/my-image:v1.20' to 'my-registry/my-image:v1.20.0', but pending spec update (dry run=false)" alias=my-alias application=integration-test image_name=my-image image_tag=v1.20 registry=my-registry

Dug a bit more and found the previous replica which didn't have any pods, but ArgoCD still insisted it had a pod in processing state. After deleting the rs it quited down and with the workaround everything looks normal. 
I get this behaviour from the Docker hub image "sensu/sensu" (monitoring)
ArgoCD keeps replacing 6.8 with 6.8.0 and back.

https://hub.docker.com/r/sensu/sensu/tags?page=1&name=6.8.
I am seeing this exact behavior. We tag with major, major.minor, and major.minor.patch with every release. I am watching argocd loop between 1.4 and 1.4.0 over and over.

Don't know if it's related of if i have to open another issue, but when trying to update images like *docker.io/traefik:~2* there is a loop between the numeric tag and the 'v' prefixed tag.

```
time="2023-01-17T19:07:13Z" level=info msg="Successfully updated image 'docker.io/traefik:2.9.6' to 'docker.io/traefik:v2.9.6', but pending spec update (dry run=false)" alias= application=traefik image_name=traefik image_tag=2.9.6 registry=docker.io
...
time="2023-01-17T19:24:07Z" level=info msg="Successfully updated image 'docker.io/traefik:v2.9.6' to 'docker.io/traefik:2.9.6', but pending spec update (dry run=false)" alias= application=traefik image_name=traefik image_tag=v2.9.6 registry=docker.io
```

Argocd application configuration:

```yaml
metadata:
  name: traefik
  annotations:
    argocd-image-updater.argoproj.io/image-list: docker.io/traefik:~2
    argocd-image-updater.argoproj.io/traefik.update-strategy: semver
    argocd-image-updater.argoproj.io/traefik.platforms: linux/arm,linux/amd64
    argocd-image-updater.argoproj.io/write-back-method: git
    argocd-image-updater.argoproj.io/write-back-target: kustomization
spec:
  project: default
  # Source of the application manifests
  source:
    repoURL: xxx
    targetRevision: master
    path: .
    ...
```

Argocd v2.5.6+9db2c94
Argocd-image-updater:v0.12.0 



Patch:
diff --git a/pkg/tag/semver.go b/pkg/tag/semver.go
new file mode 100644
index 00000000..71ea3cee
--- /dev/null
+++ b/pkg/tag/semver.go
@@ -0,0 +1,31 @@
+package tag
+
+import "github.com/Masterminds/semver"
+
+// semverCollection is a replacement for semver.Collection that breaks version
+// comparison ties through a lexical comparison of the original version strings.
+// Using this, instead of semver.Collection, when sorting will yield
+// deterministic results that semver.Collection will not yield.
+type semverCollection []*semver.Version
+
+// Len returns the length of a collection. The number of Version instances
+// on the slice.
+func (s semverCollection) Len() int {
+	return len(s)
+}
+
+// Less is needed for the sort interface to compare two Version objects on the
+// slice. If checks if one is less than the other.
+func (s semverCollection) Less(i, j int) bool {
+	comp := s[i].Compare(s[j])
+	if comp != 0 {
+		return comp < 0
+	}
+	return s[i].Original() < s[j].Original()
+}
+
+// Swap is needed for the sort interface to replace the Version objects
+// at two different positions in the slice.
+func (s semverCollection) Swap(i, j int) {
+	s[i], s[j] = s[j], s[i]
+}
diff --git a/pkg/tag/tag.go b/pkg/tag/tag.go
index 5d16024e..36d083d3 100644
--- a/pkg/tag/tag.go
+++ b/pkg/tag/tag.go
@@ -165,7 +165,7 @@ func (il ImageTagList) SortBySemVer() SortableImageTagList {
 		}
 		svl = append(svl, svi)
 	}
-	sort.Sort(semver.Collection(svl))
+	sort.Sort(semverCollection(svl))
 	for _, svi := range svl {
 		sil = append(sil, NewImageTag(svi.Original(), *il.items[svi.Original()].TagDate, il.items[svi.Original()].TagDigest))
 	}


Test patch:
diff --git a/pkg/tag/tag_test.go b/pkg/tag/tag_test.go
index fb5bc02c..23beb6d3 100644
--- a/pkg/tag/tag_test.go
+++ b/pkg/tag/tag_test.go
@@ -106,7 +106,7 @@ func Test_SortableImageTagList(t *testing.T) {
 	})
 
 	t.Run("Sort by semver", func(t *testing.T) {
-		names := []string{"v2.0.2", "v1.0", "v1.0.1", "v2.0.3", "v2.0"}
+		names := []string{"v2.0.2", "v1.0", "v2.0.0", "v1.0.1", "v2.0.3", "v2.0"}
 		il := NewImageTagList()
 		for _, name := range names {
 			tag := NewImageTag(name, time.Now(), "")
@@ -117,8 +117,9 @@ func Test_SortableImageTagList(t *testing.T) {
 		assert.Equal(t, "v1.0", sil[0].TagName)
 		assert.Equal(t, "v1.0.1", sil[1].TagName)
 		assert.Equal(t, "v2.0", sil[2].TagName)
-		assert.Equal(t, "v2.0.2", sil[3].TagName)
-		assert.Equal(t, "v2.0.3", sil[4].TagName)
+		assert.Equal(t, "v2.0.0", sil[3].TagName)
+		assert.Equal(t, "v2.0.2", sil[4].TagName)
+		assert.Equal(t, "v2.0.3", sil[5].TagName)
 	})
 
 	t.Run("Sort by date", func(t *testing.T) {