Skip to content

Commit 5954a02

Browse files
authored
Merge branch 'master' into feat/update-behavior
2 parents 4d7ded9 + e816c49 commit 5954a02

File tree

3 files changed

+323
-2
lines changed

3 files changed

+323
-2
lines changed

docs/configuration/images.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,10 @@ If the `<image_alias>.helm.image-spec` annotation is set, the two other
358358
annotations `<image_alias>.helm.image-name` and `<image_alias>.helm.image-tag`
359359
will be ignored.
360360

361+
If the image is in the yaml list, then the index can be specified
362+
in the annotations `<image_alias>.helm.image-spec`, `<image_alias>.helm.image-name`
363+
or `<image_alias>.helm.image-tag` in square brackets.
364+
361365
## Examples
362366

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

452+
### Updating the image in the yaml list
453+
454+
*Scenario:* You want to automatically update the image `nginx:1.19` that is inside the yaml list, e.g.
455+
456+
```yaml
457+
foo:
458+
- name: foo-1
459+
image: busybox:latest
460+
command: ['sh', '-c', 'echo "Custom container running"']
461+
- name: foo-2
462+
image: nginx:1.19
463+
```
464+
465+
*Solution:* Use the index in square brackets of the item that needs to be updated, i.e.
466+
467+
```yaml
468+
argocd-image-updater.argoproj.io/fooalias.helm.image-spec: foo[1].image
469+
```
470+
471+
This works for annotations `<image_alias>.helm.image-name`, `<image_alias>.helm.image-tag` and `<image_alias>.helm.image-spec`.
472+
473+
448474
## Appendix
449475

450476
### <a name="appendix-annotations"></a>Available annotations

pkg/argocd/update.go

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"context"
66
"fmt"
77
"path/filepath"
8+
"regexp"
9+
"strconv"
810
"strings"
911
"sync"
1012
"text/template"
@@ -63,6 +65,12 @@ const (
6365

6466
const defaultIndent = 2
6567

68+
// listElementPattern is a regular expression for searching for an element in a yaml array.
69+
// example: any-string[1]
70+
const listElementPattern = `^(.*)\[(.*)\]$`
71+
72+
var re = regexp.MustCompile(listElementPattern)
73+
6674
// WriteBackConfig holds information on how to write back the changes to an Application
6775
type WriteBackConfig struct {
6876
Method WriteBackMethod
@@ -646,15 +654,42 @@ func setHelmValue(currentValues *yaml.Node, key string, value interface{}) error
646654

647655
var err error
648656
keys := strings.Split(key, ".")
649-
650657
for i, k := range keys {
651-
if idx, found := findHelmValuesKey(current, k); found {
658+
// pointer is needed to determine that the id has indeed been passed.
659+
var idPtr *int
660+
// by default, the search is based on the key without changes, but
661+
// if string matches pattern, we consider it is an id in YAML list.
662+
key := k
663+
matches := re.FindStringSubmatch(k)
664+
if matches != nil {
665+
idStr := matches[2]
666+
id, err := strconv.Atoi(idStr)
667+
if err != nil {
668+
return fmt.Errorf("id \"%s\" in yaml array must match pattern ^(.*)\\[(.*)\\]$", idStr)
669+
}
670+
idPtr = &id
671+
key = matches[1]
672+
}
673+
if idx, found := findHelmValuesKey(current, key); found {
652674
// Navigate deeper into the map
653675
current = (*current).Content[idx]
654676
// unpack one level of alias; an alias of an alias is not supported
655677
if current.Kind == yaml.AliasNode {
656678
current = current.Alias
657679
}
680+
if current.Kind != yaml.SequenceNode && idPtr != nil {
681+
return fmt.Errorf("id %d provided when \"%s\" is not an yaml array", *idPtr, key)
682+
}
683+
if current.Kind == yaml.SequenceNode {
684+
if idPtr == nil {
685+
return fmt.Errorf("no id provided for yaml array \"%s\"", key)
686+
}
687+
currentContent := (*current).Content
688+
if *idPtr < 0 || *idPtr >= len(currentContent) {
689+
return fmt.Errorf("id %d is out of range [0, %d)", *idPtr, len(currentContent))
690+
}
691+
current = (*current).Content[*idPtr]
692+
}
658693
if i == len(keys)-1 {
659694
// If we're at the final key, set the value and return
660695
if current.Kind == yaml.ScalarNode {

pkg/argocd/update_test.go

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2438,6 +2438,266 @@ image:
24382438
require.NoError(t, err)
24392439
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(output)))
24402440
})
2441+
2442+
t.Run("yaml list is correctly parsed", func(t *testing.T) {
2443+
expected := `
2444+
images:
2445+
- name: image-1
2446+
attributes:
2447+
name: repo-name
2448+
tag: 2.0.0
2449+
`
2450+
2451+
inputData := []byte(`
2452+
images:
2453+
- name: image-1
2454+
attributes:
2455+
name: repo-name
2456+
tag: 1.0.0
2457+
`)
2458+
input := yaml.Node{}
2459+
err := yaml.Unmarshal(inputData, &input)
2460+
require.NoError(t, err)
2461+
2462+
key := "images[0].attributes.tag"
2463+
value := "2.0.0"
2464+
2465+
err = setHelmValue(&input, key, value)
2466+
require.NoError(t, err)
2467+
2468+
output, err := marshalWithIndent(&input, defaultIndent)
2469+
require.NoError(t, err)
2470+
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(output)))
2471+
})
2472+
2473+
t.Run("yaml list is correctly parsed when multiple values", func(t *testing.T) {
2474+
expected := `
2475+
images:
2476+
- name: image-1
2477+
attributes:
2478+
name: repo-name
2479+
tag: 1.0.0
2480+
- name: image-2
2481+
attributes:
2482+
name: repo-name
2483+
tag: 2.0.0
2484+
`
2485+
2486+
inputData := []byte(`
2487+
images:
2488+
- name: image-1
2489+
attributes:
2490+
name: repo-name
2491+
tag: 1.0.0
2492+
- name: image-2
2493+
attributes:
2494+
name: repo-name
2495+
tag: 1.0.0
2496+
`)
2497+
input := yaml.Node{}
2498+
err := yaml.Unmarshal(inputData, &input)
2499+
require.NoError(t, err)
2500+
2501+
key := "images[1].attributes.tag"
2502+
value := "2.0.0"
2503+
2504+
err = setHelmValue(&input, key, value)
2505+
require.NoError(t, err)
2506+
2507+
output, err := marshalWithIndent(&input, defaultIndent)
2508+
require.NoError(t, err)
2509+
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(output)))
2510+
})
2511+
2512+
t.Run("yaml list is correctly parsed when inside map", func(t *testing.T) {
2513+
expected := `
2514+
extraContainers:
2515+
images:
2516+
- name: image-1
2517+
attributes:
2518+
name: repo-name
2519+
tag: 2.0.0
2520+
`
2521+
2522+
inputData := []byte(`
2523+
extraContainers:
2524+
images:
2525+
- name: image-1
2526+
attributes:
2527+
name: repo-name
2528+
tag: 1.0.0
2529+
`)
2530+
input := yaml.Node{}
2531+
err := yaml.Unmarshal(inputData, &input)
2532+
require.NoError(t, err)
2533+
2534+
key := "extraContainers.images[0].attributes.tag"
2535+
value := "2.0.0"
2536+
2537+
err = setHelmValue(&input, key, value)
2538+
require.NoError(t, err)
2539+
2540+
output, err := marshalWithIndent(&input, defaultIndent)
2541+
require.NoError(t, err)
2542+
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(output)))
2543+
})
2544+
2545+
t.Run("yaml list is correctly parsed when list name contains digits", func(t *testing.T) {
2546+
expected := `
2547+
extraContainers:
2548+
images123:
2549+
- name: image-1
2550+
attributes:
2551+
name: repo-name
2552+
tag: 2.0.0
2553+
`
2554+
2555+
inputData := []byte(`
2556+
extraContainers:
2557+
images123:
2558+
- name: image-1
2559+
attributes:
2560+
name: repo-name
2561+
tag: 1.0.0
2562+
`)
2563+
input := yaml.Node{}
2564+
err := yaml.Unmarshal(inputData, &input)
2565+
require.NoError(t, err)
2566+
2567+
key := "extraContainers.images123[0].attributes.tag"
2568+
value := "2.0.0"
2569+
2570+
err = setHelmValue(&input, key, value)
2571+
require.NoError(t, err)
2572+
2573+
output, err := marshalWithIndent(&input, defaultIndent)
2574+
require.NoError(t, err)
2575+
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(output)))
2576+
})
2577+
2578+
t.Run("id for yaml list is lower than 0", func(t *testing.T) {
2579+
inputData := []byte(`
2580+
images:
2581+
- name: image-1
2582+
attributes:
2583+
name: repo-name
2584+
tag: 1.0.0
2585+
`)
2586+
input := yaml.Node{}
2587+
err := yaml.Unmarshal(inputData, &input)
2588+
require.NoError(t, err)
2589+
2590+
key := "images[-1].attributes.tag"
2591+
value := "2.0.0"
2592+
2593+
err = setHelmValue(&input, key, value)
2594+
2595+
require.Error(t, err)
2596+
assert.Equal(t, "id -1 is out of range [0, 1)", err.Error())
2597+
})
2598+
2599+
t.Run("id for yaml list is greater than length of list", func(t *testing.T) {
2600+
inputData := []byte(`
2601+
images:
2602+
- name: image-1
2603+
attributes:
2604+
name: repo-name
2605+
tag: 1.0.0
2606+
`)
2607+
input := yaml.Node{}
2608+
err := yaml.Unmarshal(inputData, &input)
2609+
require.NoError(t, err)
2610+
2611+
key := "images[1].attributes.tag"
2612+
value := "2.0.0"
2613+
2614+
err = setHelmValue(&input, key, value)
2615+
2616+
require.Error(t, err)
2617+
assert.Equal(t, "id 1 is out of range [0, 1)", err.Error())
2618+
})
2619+
2620+
t.Run("id for YAML list is not a valid integer", func(t *testing.T) {
2621+
inputData := []byte(`
2622+
images:
2623+
- name: image-1
2624+
attributes:
2625+
name: repo-name
2626+
tag: 1.0.0
2627+
`)
2628+
input := yaml.Node{}
2629+
err := yaml.Unmarshal(inputData, &input)
2630+
require.NoError(t, err)
2631+
2632+
key := "images[invalid].attributes.tag"
2633+
value := "2.0.0"
2634+
2635+
err = setHelmValue(&input, key, value)
2636+
2637+
require.Error(t, err)
2638+
assert.Equal(t, "id \"invalid\" in yaml array must match pattern ^(.*)\\[(.*)\\]$", err.Error())
2639+
})
2640+
2641+
t.Run("no id for yaml list given", func(t *testing.T) {
2642+
inputData := []byte(`
2643+
images:
2644+
- name: image-1
2645+
attributes:
2646+
name: repo-name
2647+
tag: 1.0.0
2648+
`)
2649+
input := yaml.Node{}
2650+
err := yaml.Unmarshal(inputData, &input)
2651+
require.NoError(t, err)
2652+
2653+
key := "images.attributes.tag"
2654+
value := "2.0.0"
2655+
2656+
err = setHelmValue(&input, key, value)
2657+
2658+
require.Error(t, err)
2659+
assert.Equal(t, "no id provided for yaml array \"images\"", err.Error())
2660+
})
2661+
2662+
t.Run("id given when node is not an yaml list", func(t *testing.T) {
2663+
inputData := []byte(`
2664+
image:
2665+
attributes:
2666+
name: repo-name
2667+
tag: 1.0.0
2668+
`)
2669+
input := yaml.Node{}
2670+
err := yaml.Unmarshal(inputData, &input)
2671+
require.NoError(t, err)
2672+
2673+
key := "image[0].attributes.tag"
2674+
value := "2.0.0"
2675+
2676+
err = setHelmValue(&input, key, value)
2677+
2678+
require.Error(t, err)
2679+
assert.Equal(t, "id 0 provided when \"image\" is not an yaml array", err.Error())
2680+
})
2681+
2682+
t.Run("invalid id given when node is not an yaml list", func(t *testing.T) {
2683+
inputData := []byte(`
2684+
image:
2685+
attributes:
2686+
name: repo-name
2687+
tag: 1.0.0
2688+
`)
2689+
input := yaml.Node{}
2690+
err := yaml.Unmarshal(inputData, &input)
2691+
require.NoError(t, err)
2692+
2693+
key := "image[invalid].attributes.tag"
2694+
value := "2.0.0"
2695+
2696+
err = setHelmValue(&input, key, value)
2697+
2698+
require.Error(t, err)
2699+
assert.Equal(t, "id \"invalid\" in yaml array must match pattern ^(.*)\\[(.*)\\]$", err.Error())
2700+
})
24412701
}
24422702

24432703
func Test_GetWriteBackConfig(t *testing.T) {

0 commit comments

Comments
 (0)