Skip to content

Commit 6e731a7

Browse files
chansukechengfang
authored andcommitted
feat: fix force-update for zero-replica deployments by reading image tags from spec
Signed-off-by: chansuke <[email protected]>
1 parent da586ce commit 6e731a7

File tree

3 files changed

+142
-4
lines changed

3 files changed

+142
-4
lines changed

pkg/argocd/argocd.go

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -592,7 +592,14 @@ func GetImagesFromApplication(app *v1alpha1.Application) image.ContainerImageLis
592592
annotations := app.Annotations
593593
for _, img := range *parseImageList(annotations) {
594594
if img.HasForceUpdateOptionAnnotation(annotations, common.ImageUpdaterAnnotationPrefix) {
595-
img.ImageTag = nil // the tag from the image list will be a version constraint, which isn't a valid tag
595+
// for force-update images, try to get the current image tag from the spec
596+
// this helps handle cases where there are 0 replicas
597+
currentImage := getImageFromSpec(app, img)
598+
if currentImage != nil {
599+
img.ImageTag = currentImage.ImageTag
600+
} else {
601+
img.ImageTag = nil
602+
}
596603
images = append(images, img)
597604
}
598605
}
@@ -727,3 +734,51 @@ func (a ApplicationType) String() string {
727734
return "Unknown"
728735
}
729736
}
737+
738+
// getImageFromSpec tries to find the current image tag from the application spec
739+
func getImageFromSpec(app *v1alpha1.Application, targetImage *image.ContainerImage) *image.ContainerImage {
740+
appType := getApplicationType(app)
741+
source := getApplicationSource(app)
742+
743+
if source == nil {
744+
return nil
745+
}
746+
747+
switch appType {
748+
case ApplicationTypeHelm:
749+
if source.Helm != nil && source.Helm.Parameters != nil {
750+
for _, param := range source.Helm.Parameters {
751+
if param.Name == "image.tag" || param.Name == "image.version" {
752+
foundImage := image.NewFromIdentifier(fmt.Sprintf("%s:%s", targetImage.ImageName, param.Value))
753+
if foundImage != nil && foundImage.ImageName == targetImage.ImageName {
754+
return foundImage
755+
}
756+
}
757+
if param.Name == "image" || param.Name == "image.repository" {
758+
foundImage := image.NewFromIdentifier(param.Value)
759+
if foundImage != nil && foundImage.ImageName == targetImage.ImageName {
760+
return foundImage
761+
}
762+
}
763+
}
764+
}
765+
case ApplicationTypeKustomize:
766+
if source.Kustomize != nil && source.Kustomize.Images != nil {
767+
for _, kustomizeImage := range source.Kustomize.Images {
768+
imageStr := string(kustomizeImage)
769+
if strings.Contains(imageStr, "=") {
770+
parts := strings.SplitN(imageStr, "=", 2)
771+
if len(parts) == 2 {
772+
imageStr = parts[1]
773+
}
774+
}
775+
foundImage := image.NewFromIdentifier(imageStr)
776+
if foundImage != nil && foundImage.ImageName == targetImage.ImageName {
777+
return foundImage
778+
}
779+
}
780+
}
781+
}
782+
783+
return nil
784+
}

pkg/argocd/argocd_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,75 @@ func Test_GetImagesFromApplication(t *testing.T) {
8181
assert.Equal(t, "nginx", imageList[0].ImageName)
8282
assert.Nil(t, imageList[0].ImageTag)
8383
})
84+
85+
t.Run("Get list of images from application with force-update and zero replicas - Helm", func(t *testing.T) {
86+
application := &v1alpha1.Application{
87+
ObjectMeta: v1.ObjectMeta{
88+
Name: "test-app",
89+
Namespace: "argocd",
90+
Annotations: map[string]string{
91+
fmt.Sprintf(registryCommon.Prefixed(common.ImageUpdaterAnnotationPrefix, registryCommon.ForceUpdateOptionAnnotationSuffix), "myapp"): "true",
92+
common.ImageUpdaterAnnotation: "myapp=myregistry/myapp",
93+
},
94+
},
95+
Spec: v1alpha1.ApplicationSpec{
96+
Source: &v1alpha1.ApplicationSource{
97+
Helm: &v1alpha1.ApplicationSourceHelm{
98+
Parameters: []v1alpha1.HelmParameter{
99+
{
100+
Name: "image.tag",
101+
Value: "1.2.3",
102+
},
103+
},
104+
},
105+
},
106+
},
107+
Status: v1alpha1.ApplicationStatus{
108+
SourceType: v1alpha1.ApplicationSourceTypeHelm,
109+
Summary: v1alpha1.ApplicationSummary{
110+
Images: []string{}, // Empty - simulating 0 replicas
111+
},
112+
},
113+
}
114+
imageList := GetImagesFromApplication(application)
115+
require.Len(t, imageList, 1)
116+
assert.Equal(t, "myregistry/myapp", imageList[0].ImageName)
117+
assert.NotNil(t, imageList[0].ImageTag)
118+
assert.Equal(t, "1.2.3", imageList[0].ImageTag.TagName)
119+
})
120+
121+
t.Run("Get list of images from application with force-update and zero replicas - Kustomize", func(t *testing.T) {
122+
application := &v1alpha1.Application{
123+
ObjectMeta: v1.ObjectMeta{
124+
Name: "test-app",
125+
Namespace: "argocd",
126+
Annotations: map[string]string{
127+
fmt.Sprintf(registryCommon.Prefixed(common.ImageUpdaterAnnotationPrefix, registryCommon.ForceUpdateOptionAnnotationSuffix), "myapp"): "true",
128+
common.ImageUpdaterAnnotation: "myapp=myregistry/myapp",
129+
},
130+
},
131+
Spec: v1alpha1.ApplicationSpec{
132+
Source: &v1alpha1.ApplicationSource{
133+
Kustomize: &v1alpha1.ApplicationSourceKustomize{
134+
Images: v1alpha1.KustomizeImages{
135+
"myregistry/myapp:2.3.4",
136+
},
137+
},
138+
},
139+
},
140+
Status: v1alpha1.ApplicationStatus{
141+
SourceType: v1alpha1.ApplicationSourceTypeKustomize,
142+
Summary: v1alpha1.ApplicationSummary{
143+
Images: []string{}, // Empty - simulating 0 replicas
144+
},
145+
},
146+
}
147+
imageList := GetImagesFromApplication(application)
148+
require.Len(t, imageList, 1)
149+
assert.Equal(t, "myregistry/myapp", imageList[0].ImageName)
150+
assert.NotNil(t, imageList[0].ImageTag)
151+
assert.Equal(t, "2.3.4", imageList[0].ImageTag.TagName)
152+
})
84153
}
85154

86155
func Test_GetImagesAndAliasesFromApplication(t *testing.T) {

pkg/argocd/update.go

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -167,9 +167,23 @@ func UpdateApplication(updateConf *UpdateConfiguration, state *SyncIterationStat
167167
for _, applicationImage := range updateConf.UpdateApp.Images {
168168
updateableImage := applicationImages.ContainsImage(applicationImage, false)
169169
if updateableImage == nil {
170-
log.WithContext().AddField("application", app).Debugf("Image '%s' seems not to be live in this application, skipping", applicationImage.ImageName)
171-
result.NumSkipped += 1
172-
continue
170+
// for force-update images, we should not skip them even if they're not "live"
171+
// this handles cases like 0-replica deployments or CronJobs without active jobs
172+
if applicationImage.HasForceUpdateOptionAnnotation(updateConf.UpdateApp.Application.Annotations, common.ImageUpdaterAnnotationPrefix) {
173+
// find the image in our list that matches by name
174+
for _, img := range applicationImages {
175+
if img.ImageName == applicationImage.ImageName {
176+
updateableImage = img
177+
break
178+
}
179+
}
180+
}
181+
182+
if updateableImage == nil {
183+
log.WithContext().AddField("application", app).Debugf("Image '%s' seems not to be live in this application, skipping", applicationImage.ImageName)
184+
result.NumSkipped += 1
185+
continue
186+
}
173187
}
174188

175189
// In some cases, the running image has no tag set. We create a dummy

0 commit comments

Comments
 (0)