Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions docs/configuration/images.md
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,10 @@ If the `<image_alias>.helm.image-spec` annotation is set, the two other
annotations `<image_alias>.helm.image-name` and `<image_alias>.helm.image-tag`
will be ignored.

If the image is in the yaml list, then the index can be specified
in the annotations `<image_alias>.helm.image-spec`, `<image_alias>.helm.image-name`
or `<image_alias>.helm.image-tag` in square brackets.

## Examples

### Following an image's patch branch
Expand Down Expand Up @@ -445,6 +449,28 @@ Argo CD Image Updater will update your configuration to use the SHA256 sum of
the image, and Kubernetes will restart your pods automatically to have them
use the new image.

### Updating the image in the yaml list

*Scenario:* You want to automatically update the image `nginx:1.19` that is inside the yaml list, e.g.

```yaml
foo:
- name: foo-1
image: busybox:latest
command: ['sh', '-c', 'echo "Custom container running"']
- name: foo-2
image: nginx:1.19
```

*Solution:* Use the index in square brackets of the item that needs to be updated, i.e.

```yaml
argocd-image-updater.argoproj.io/fooalias.helm.image-spec: foo[1].image
```

This works for annotations `<image_alias>.helm.image-name`, `<image_alias>.helm.image-tag` and `<image_alias>.helm.image-spec`.


## Appendix

### <a name="appendix-annotations"></a>Available annotations
Expand Down
36 changes: 34 additions & 2 deletions pkg/argocd/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"context"
"fmt"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync"
"text/template"
Expand Down Expand Up @@ -632,15 +634,45 @@ func setHelmValue(currentValues *yaml.Node, key string, value interface{}) error

var err error
keys := strings.Split(key, ".")

// any-string[1]
pattern := `^(.*)\[(.*)\]$`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we may want to use a more specific pattern to avoid invalid values like foo[x]:

pattern := `^([^[]+)\[(\d+)\]$`

Copy link
Contributor Author

@AntonShadrinNN AntonShadrinNN Aug 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original version looked like this, but it makes it harder to return a clear error like this . The problem is that the search is based on the key. If it doesn't fit the pattern, then the search will follow it entirely, which means it will fall into the else block and simply write this value. Should I take a different approach, such as checking the validity of the strings and parsing them separately?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a small difference, in that case, let's just keep what you have now.

re := regexp.MustCompile(pattern)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about moving the regexp compilation to the pkg level var? This func is called multiple times per image update and we should avoid recompiling the regexp repeatedly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point. Fixed it.

for i, k := range keys {
if idx, found := findHelmValuesKey(current, k); found {
// pointer is needed to determine that the id has indeed been passed.
var idPtr *int
// by default, the search is based on the key without changes, but
// if string matches pattern, we consider it is an id in YAML list.
key := k
matches := re.FindStringSubmatch(k)
if matches != nil {
idStr := matches[2]
id, err := strconv.Atoi(idStr)
if err != nil {
return fmt.Errorf("id \"%s\" in yaml array must match pattern ^(.*)\\[(.*)\\]$", idStr)
}
idPtr = &id
key = matches[1]
}
if idx, found := findHelmValuesKey(current, key); found {
// Navigate deeper into the map
current = (*current).Content[idx]
// unpack one level of alias; an alias of an alias is not supported
if current.Kind == yaml.AliasNode {
current = current.Alias
}
if current.Kind != yaml.SequenceNode && idPtr != nil {
return fmt.Errorf("id %d provided when \"%s\" is not an yaml array", *idPtr, key)
}
if current.Kind == yaml.SequenceNode {
if idPtr == nil {
return fmt.Errorf("no id provided for yaml array \"%s\"", key)
}
currentContent := (*current).Content
if *idPtr < 0 || *idPtr >= len(currentContent) {
return fmt.Errorf("id %d is out of range [0, %d)", *idPtr, len(currentContent))
}
current = (*current).Content[*idPtr]
}
if i == len(keys)-1 {
// If we're at the final key, set the value and return
if current.Kind == yaml.ScalarNode {
Expand Down
260 changes: 260 additions & 0 deletions pkg/argocd/update_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2438,6 +2438,266 @@ image:
require.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(output)))
})

t.Run("yaml list is correctly parsed", func(t *testing.T) {
expected := `
images:
- name: image-1
attributes:
name: repo-name
tag: 2.0.0
`

inputData := []byte(`
images:
- name: image-1
attributes:
name: repo-name
tag: 1.0.0
`)
input := yaml.Node{}
err := yaml.Unmarshal(inputData, &input)
require.NoError(t, err)

key := "images[0].attributes.tag"
value := "2.0.0"

err = setHelmValue(&input, key, value)
require.NoError(t, err)

output, err := marshalWithIndent(&input, defaultIndent)
require.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(output)))
})

t.Run("yaml list is correctly parsed when multiple values", func(t *testing.T) {
expected := `
images:
- name: image-1
attributes:
name: repo-name
tag: 1.0.0
- name: image-2
attributes:
name: repo-name
tag: 2.0.0
`

inputData := []byte(`
images:
- name: image-1
attributes:
name: repo-name
tag: 1.0.0
- name: image-2
attributes:
name: repo-name
tag: 1.0.0
`)
input := yaml.Node{}
err := yaml.Unmarshal(inputData, &input)
require.NoError(t, err)

key := "images[1].attributes.tag"
value := "2.0.0"

err = setHelmValue(&input, key, value)
require.NoError(t, err)

output, err := marshalWithIndent(&input, defaultIndent)
require.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(output)))
})

t.Run("yaml list is correctly parsed when inside map", func(t *testing.T) {
expected := `
extraContainers:
images:
- name: image-1
attributes:
name: repo-name
tag: 2.0.0
`

inputData := []byte(`
extraContainers:
images:
- name: image-1
attributes:
name: repo-name
tag: 1.0.0
`)
input := yaml.Node{}
err := yaml.Unmarshal(inputData, &input)
require.NoError(t, err)

key := "extraContainers.images[0].attributes.tag"
value := "2.0.0"

err = setHelmValue(&input, key, value)
require.NoError(t, err)

output, err := marshalWithIndent(&input, defaultIndent)
require.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(output)))
})

t.Run("yaml list is correctly parsed when list name contains digits", func(t *testing.T) {
expected := `
extraContainers:
images123:
- name: image-1
attributes:
name: repo-name
tag: 2.0.0
`

inputData := []byte(`
extraContainers:
images123:
- name: image-1
attributes:
name: repo-name
tag: 1.0.0
`)
input := yaml.Node{}
err := yaml.Unmarshal(inputData, &input)
require.NoError(t, err)

key := "extraContainers.images123[0].attributes.tag"
value := "2.0.0"

err = setHelmValue(&input, key, value)
require.NoError(t, err)

output, err := marshalWithIndent(&input, defaultIndent)
require.NoError(t, err)
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(output)))
})

t.Run("id for yaml list is lower than 0", func(t *testing.T) {
inputData := []byte(`
images:
- name: image-1
attributes:
name: repo-name
tag: 1.0.0
`)
input := yaml.Node{}
err := yaml.Unmarshal(inputData, &input)
require.NoError(t, err)

key := "images[-1].attributes.tag"
value := "2.0.0"

err = setHelmValue(&input, key, value)

require.Error(t, err)
assert.Equal(t, "id -1 is out of range [0, 1)", err.Error())
})

t.Run("id for yaml list is greater than length of list", func(t *testing.T) {
inputData := []byte(`
images:
- name: image-1
attributes:
name: repo-name
tag: 1.0.0
`)
input := yaml.Node{}
err := yaml.Unmarshal(inputData, &input)
require.NoError(t, err)

key := "images[1].attributes.tag"
value := "2.0.0"

err = setHelmValue(&input, key, value)

require.Error(t, err)
assert.Equal(t, "id 1 is out of range [0, 1)", err.Error())
})

t.Run("id for YAML list is not a valid integer", func(t *testing.T) {
inputData := []byte(`
images:
- name: image-1
attributes:
name: repo-name
tag: 1.0.0
`)
input := yaml.Node{}
err := yaml.Unmarshal(inputData, &input)
require.NoError(t, err)

key := "images[invalid].attributes.tag"
value := "2.0.0"

err = setHelmValue(&input, key, value)

require.Error(t, err)
assert.Equal(t, "id \"invalid\" in yaml array must match pattern ^(.*)\\[(.*)\\]$", err.Error())
})

t.Run("no id for yaml list given", func(t *testing.T) {
inputData := []byte(`
images:
- name: image-1
attributes:
name: repo-name
tag: 1.0.0
`)
input := yaml.Node{}
err := yaml.Unmarshal(inputData, &input)
require.NoError(t, err)

key := "images.attributes.tag"
value := "2.0.0"

err = setHelmValue(&input, key, value)

require.Error(t, err)
assert.Equal(t, "no id provided for yaml array \"images\"", err.Error())
})

t.Run("id given when node is not an yaml list", func(t *testing.T) {
inputData := []byte(`
image:
attributes:
name: repo-name
tag: 1.0.0
`)
input := yaml.Node{}
err := yaml.Unmarshal(inputData, &input)
require.NoError(t, err)

key := "image[0].attributes.tag"
value := "2.0.0"

err = setHelmValue(&input, key, value)

require.Error(t, err)
assert.Equal(t, "id 0 provided when \"image\" is not an yaml array", err.Error())
})

t.Run("invalid id given when node is not an yaml list", func(t *testing.T) {
inputData := []byte(`
image:
attributes:
name: repo-name
tag: 1.0.0
`)
input := yaml.Node{}
err := yaml.Unmarshal(inputData, &input)
require.NoError(t, err)

key := "image[invalid].attributes.tag"
value := "2.0.0"

err = setHelmValue(&input, key, value)

require.Error(t, err)
assert.Equal(t, "id \"invalid\" in yaml array must match pattern ^(.*)\\[(.*)\\]$", err.Error())
})
}

func Test_GetWriteBackConfig(t *testing.T) {
Expand Down