summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/configuration/images.md18
-rw-r--r--pkg/argocd/update_test.go2
-rw-r--r--pkg/common/constants.go11
-rw-r--r--pkg/image/options.go109
-rw-r--r--pkg/image/options_test.go133
5 files changed, 232 insertions, 41 deletions
diff --git a/docs/configuration/images.md b/docs/configuration/images.md
index af08962..b291434 100644
--- a/docs/configuration/images.md
+++ b/docs/configuration/images.md
@@ -402,3 +402,21 @@ must be prefixed with `argocd-image-updater.argoproj.io`.
|`<image_alias>.helm.image-name`|`image.name`|Name of the Helm parameter used for specifying the image name, i.e. holds `image/name`|
|`<image_alias>.helm.image-tag`|`image.tag`|Name of the Helm parameter used for specifying the image tag, i.e. holds `1.0`|
|`<image_alias>.kustomize.image-name`|*original name of image*|Name of Kustomize image parameter to set during updates|
+
+### Application-wide defaults
+
+If you want to update multiple images in an Application, that all share common
+settings (such as, update strategy, allowed tags, etc), you can define common
+options. These options are valid for all images, unless an image overrides it
+with specific configuration.
+
+The following annotations are available. Please note, all annotations must be
+prefixed with `argocd-image-updater.argoproj.io/`.
+
+|Annotation name|Description|
+|---------------|-----------|
+|`update-strategy`|The update strategy to be used for all images|
+|`force-update`|If set to "true" (with quotes), even images that are not currently deployed will be updated|
+|`allow-tags`|A function to match tag names from registry against to be considered for update|
+|`ignore-tags`|A comma-separated list of glob patterns that when match ignore a certain tag from the registry|
+|`pull-secret`|A reference to a secret to be used as registry credentials for this image|
diff --git a/pkg/argocd/update_test.go b/pkg/argocd/update_test.go
index ff8abf8..811f4b6 100644
--- a/pkg/argocd/update_test.go
+++ b/pkg/argocd/update_test.go
@@ -419,7 +419,7 @@ func Test_UpdateApplication(t *testing.T) {
Name: "guestbook",
Namespace: "guestbook",
Annotations: map[string]string{
- fmt.Sprintf(common.SecretListAnnotation, "dummy"): "secret:foo/bar#creds",
+ fmt.Sprintf(common.PullSecretAnnotation, "dummy"): "secret:foo/bar#creds",
},
},
Spec: v1alpha1.ApplicationSpec{
diff --git a/pkg/common/constants.go b/pkg/common/constants.go
index 84f39c8..a342cd4 100644
--- a/pkg/common/constants.go
+++ b/pkg/common/constants.go
@@ -26,19 +26,24 @@ const (
KustomizeApplicationNameAnnotation = ImageUpdaterAnnotationPrefix + "/%s.kustomize.image-name"
)
-// Upgrade strategy related annotations
+// Image specific configuration annotations
const (
OldMatchOptionAnnotation = ImageUpdaterAnnotationPrefix + "/%s.tag-match" // Deprecated and will be removed
AllowTagsOptionAnnotation = ImageUpdaterAnnotationPrefix + "/%s.allow-tags"
IgnoreTagsOptionAnnotation = ImageUpdaterAnnotationPrefix + "/%s.ignore-tags"
ForceUpdateOptionAnnotation = ImageUpdaterAnnotationPrefix + "/%s.force-update"
UpdateStrategyAnnotation = ImageUpdaterAnnotationPrefix + "/%s.update-strategy"
+ PullSecretAnnotation = ImageUpdaterAnnotationPrefix + "/%s.pull-secret"
PlatformsAnnotation = ImageUpdaterAnnotationPrefix + "/%s.platforms"
)
-// Image pull secret related annotations
+// Application-wide update strategy related annotations
const (
- SecretListAnnotation = ImageUpdaterAnnotationPrefix + "/%s.pull-secret"
+ ApplicationWideAllowTagsOptionAnnotation = ImageUpdaterAnnotationPrefix + "/allow-tags"
+ ApplicationWideIgnoreTagsOptionAnnotation = ImageUpdaterAnnotationPrefix + "/ignore-tags"
+ ApplicationWideForceUpdateOptionAnnotation = ImageUpdaterAnnotationPrefix + "/force-update"
+ ApplicationWideUpdateStrategyAnnotation = ImageUpdaterAnnotationPrefix + "/update-strategy"
+ ApplicationWidePullSecretAnnotation = ImageUpdaterAnnotationPrefix + "/pull-secret"
)
// Application update configuration related annotations
diff --git a/pkg/image/options.go b/pkg/image/options.go
index f149b81..df357c3 100644
--- a/pkg/image/options.go
+++ b/pkg/image/options.go
@@ -57,24 +57,42 @@ func (img *ContainerImage) GetParameterKustomizeImageName(annotations map[string
// HasForceUpdateOptionAnnotation gets the value for force-update option for the
// image from a set of annotations
func (img *ContainerImage) HasForceUpdateOptionAnnotation(annotations map[string]string) bool {
- key := fmt.Sprintf(common.ForceUpdateOptionAnnotation, img.normalizedSymbolicName())
- val, ok := annotations[key]
- return ok && val == "true"
+ forceUpdateAnnotations := []string{
+ fmt.Sprintf(common.ForceUpdateOptionAnnotation, img.normalizedSymbolicName()),
+ common.ApplicationWideForceUpdateOptionAnnotation,
+ }
+ var forceUpdateVal = ""
+ for _, key := range forceUpdateAnnotations {
+ if val, ok := annotations[key]; ok {
+ forceUpdateVal = val
+ break
+ }
+ }
+ return forceUpdateVal == "true"
}
// GetParameterSort gets and validates the value for the sort option for the
// image from a set of annotations
func (img *ContainerImage) GetParameterUpdateStrategy(annotations map[string]string) UpdateStrategy {
+ updateStrategyAnnotations := []string{
+ fmt.Sprintf(common.UpdateStrategyAnnotation, img.normalizedSymbolicName()),
+ common.ApplicationWideUpdateStrategyAnnotation,
+ }
+ var updateStrategyVal = ""
+ for _, key := range updateStrategyAnnotations {
+ if val, ok := annotations[key]; ok {
+ updateStrategyVal = val
+ break
+ }
+ }
logCtx := img.LogContext()
- key := fmt.Sprintf(common.UpdateStrategyAnnotation, img.normalizedSymbolicName())
- val, ok := annotations[key]
- if !ok {
+ if updateStrategyVal == "" {
+ logCtx.Tracef("No sort option found")
// Default is sort by version
- logCtx.Tracef("No sort option %s found", key)
return StrategySemVer
}
- logCtx.Tracef("found update strategy %s in %s", val, key)
- return img.ParseUpdateStrategy(val)
+ logCtx.Tracef("Found update strategy %s", updateStrategyVal)
+ return img.ParseUpdateStrategy(updateStrategyVal)
}
func (img *ContainerImage) ParseUpdateStrategy(val string) UpdateStrategy {
@@ -92,30 +110,39 @@ func (img *ContainerImage) ParseUpdateStrategy(val string) UpdateStrategy {
logCtx.Warnf("Unknown sort option %s -- using semver", val)
return StrategySemVer
}
-
}
// GetParameterMatch returns the match function and pattern to use for matching
// tag names. If an invalid option is found, it returns MatchFuncNone as the
// default, to prevent accidental matches.
func (img *ContainerImage) GetParameterMatch(annotations map[string]string) (MatchFuncFn, interface{}) {
+ allowTagsAnnotations := []string{
+ fmt.Sprintf(common.AllowTagsOptionAnnotation, img.normalizedSymbolicName()),
+ common.ApplicationWideAllowTagsOptionAnnotation,
+ }
+ var allowTagsVal = ""
+ for _, key := range allowTagsAnnotations {
+ if val, ok := annotations[key]; ok {
+ allowTagsVal = val
+ break
+ }
+ }
logCtx := img.LogContext()
- key := fmt.Sprintf(common.AllowTagsOptionAnnotation, img.normalizedSymbolicName())
- val, ok := annotations[key]
- if !ok {
+ if allowTagsVal == "" {
// The old match-tag annotation is deprecated and will be subject to removal
// in a future version.
- key = fmt.Sprintf(common.OldMatchOptionAnnotation, img.normalizedSymbolicName())
- val, ok = annotations[key]
- if !ok {
- logCtx.Tracef("No match annotation %s found", key)
- return MatchFuncAny, ""
- } else {
+ key := fmt.Sprintf(common.OldMatchOptionAnnotation, img.normalizedSymbolicName())
+ val, ok := annotations[key]
+ if ok {
logCtx.Warnf("The 'tag-match' annotation is deprecated and subject to removal. Please use 'allow-tags' annotation instead.")
+ allowTagsVal = val
}
}
-
- return img.ParseMatchfunc(val)
+ if allowTagsVal == "" {
+ logCtx.Tracef("No match annotation found")
+ return MatchFuncAny, ""
+ }
+ return img.ParseMatchfunc(allowTagsVal)
}
// ParseMatchfunc returns a matcher function and its argument from given value
@@ -148,16 +175,25 @@ func (img *ContainerImage) ParseMatchfunc(val string) (MatchFuncFn, interface{})
// GetParameterPullSecret retrieves an image's pull secret credentials
func (img *ContainerImage) GetParameterPullSecret(annotations map[string]string) *CredentialSource {
+ pullSecretAnnotations := []string{
+ fmt.Sprintf(common.PullSecretAnnotation, img.normalizedSymbolicName()),
+ common.ApplicationWidePullSecretAnnotation,
+ }
+ var pullSecretVal = ""
+ for _, key := range pullSecretAnnotations {
+ if val, ok := annotations[key]; ok {
+ pullSecretVal = val
+ break
+ }
+ }
logCtx := img.LogContext()
- key := fmt.Sprintf(common.SecretListAnnotation, img.normalizedSymbolicName())
- val, ok := annotations[key]
- if !ok {
- logCtx.Tracef("No secret annotation %s found", key)
+ if pullSecretVal == "" {
+ logCtx.Tracef("No pull-secret annotation found")
return nil
}
- credSrc, err := ParseCredentialSource(val, false)
+ credSrc, err := ParseCredentialSource(pullSecretVal, false)
if err != nil {
- logCtx.Warnf("Invalid credential reference specified: %s", val)
+ logCtx.Warnf("Invalid credential reference specified: %s", pullSecretVal)
return nil
}
return credSrc
@@ -165,15 +201,24 @@ func (img *ContainerImage) GetParameterPullSecret(annotations map[string]string)
// GetParameterIgnoreTags retrieves a list of tags to ignore from a comma-separated string
func (img *ContainerImage) GetParameterIgnoreTags(annotations map[string]string) []string {
+ ignoreTagsAnnotations := []string{
+ fmt.Sprintf(common.IgnoreTagsOptionAnnotation, img.normalizedSymbolicName()),
+ common.ApplicationWideIgnoreTagsOptionAnnotation,
+ }
+ var ignoreTagsVal = ""
+ for _, key := range ignoreTagsAnnotations {
+ if val, ok := annotations[key]; ok {
+ ignoreTagsVal = val
+ break
+ }
+ }
logCtx := img.LogContext()
- key := fmt.Sprintf(common.IgnoreTagsOptionAnnotation, img.normalizedSymbolicName())
- val, ok := annotations[key]
- if !ok {
- logCtx.Tracef("No ignore-tags annotation %s found", key)
+ if ignoreTagsVal == "" {
+ logCtx.Tracef("No ignore-tags annotation found")
return nil
}
ignoreList := make([]string, 0)
- tags := strings.Split(strings.TrimSpace(val), ",")
+ tags := strings.Split(strings.TrimSpace(ignoreTagsVal), ",")
for _, tag := range tags {
// We ignore empty tags
trimmed := strings.TrimSpace(tag)
diff --git a/pkg/image/options_test.go b/pkg/image/options_test.go
index c1883c5..a563eec 100644
--- a/pkg/image/options_test.go
+++ b/pkg/image/options_test.go
@@ -76,7 +76,6 @@ func Test_GetKustomizeOptions(t *testing.T) {
}
func Test_GetSortOption(t *testing.T) {
-
t.Run("Get update strategy semver for configured application", func(t *testing.T) {
annotations := map[string]string{
fmt.Sprintf(common.UpdateStrategyAnnotation, "dummy"): "semver",
@@ -119,10 +118,28 @@ func Test_GetSortOption(t *testing.T) {
sortMode := img.GetParameterUpdateStrategy(annotations)
assert.Equal(t, StrategySemVer, sortMode)
})
+
+ t.Run("Prefer update strategy option from image-specific annotation", func(t *testing.T) {
+ annotations := map[string]string{
+ fmt.Sprintf(common.UpdateStrategyAnnotation, "dummy"): "name",
+ common.ApplicationWideUpdateStrategyAnnotation: "latest",
+ }
+ img := NewFromIdentifier("dummy=foo/bar:1.12")
+ sortMode := img.GetParameterUpdateStrategy(annotations)
+ assert.Equal(t, StrategyName, sortMode)
+ })
+
+ t.Run("Get update strategy option from application-wide annotation", func(t *testing.T) {
+ annotations := map[string]string{
+ common.ApplicationWideUpdateStrategyAnnotation: "latest",
+ }
+ img := NewFromIdentifier("dummy=foo/bar:1.12")
+ sortMode := img.GetParameterUpdateStrategy(annotations)
+ assert.Equal(t, StrategyLatest, sortMode)
+ })
}
func Test_GetMatchOption(t *testing.T) {
-
t.Run("Get regexp match option for configured application", func(t *testing.T) {
annotations := map[string]string{
fmt.Sprintf(common.AllowTagsOptionAnnotation, "dummy"): "regexp:a-z",
@@ -155,12 +172,38 @@ func Test_GetMatchOption(t *testing.T) {
assert.Nil(t, matchArgs)
})
+ t.Run("Prefer match option from image-specific annotation", func(t *testing.T) {
+ annotations := map[string]string{
+ fmt.Sprintf(common.AllowTagsOptionAnnotation, "dummy"): "regexp:^[0-9]",
+ common.ApplicationWideAllowTagsOptionAnnotation: "regexp:^v",
+ }
+ img := NewFromIdentifier("dummy=foo/bar:1.12")
+ matchFunc, matchArgs := img.GetParameterMatch(annotations)
+ require.NotNil(t, matchFunc)
+ require.NotNil(t, matchArgs)
+ assert.IsType(t, &regexp.Regexp{}, matchArgs)
+ assert.True(t, matchFunc("0.0.1", matchArgs))
+ assert.False(t, matchFunc("v0.0.1", matchArgs))
+ })
+
+ t.Run("Get match option from application-wide annotation", func(t *testing.T) {
+ annotations := map[string]string{
+ common.ApplicationWideAllowTagsOptionAnnotation: "regexp:^v",
+ }
+ img := NewFromIdentifier("dummy=foo/bar:1.12")
+ matchFunc, matchArgs := img.GetParameterMatch(annotations)
+ require.NotNil(t, matchFunc)
+ require.NotNil(t, matchArgs)
+ assert.IsType(t, &regexp.Regexp{}, matchArgs)
+ assert.False(t, matchFunc("0.0.1", matchArgs))
+ assert.True(t, matchFunc("v0.0.1", matchArgs))
+ })
}
func Test_GetSecretOption(t *testing.T) {
t.Run("Get cred source from annotation", func(t *testing.T) {
annotations := map[string]string{
- fmt.Sprintf(common.SecretListAnnotation, "dummy"): "pullsecret:foo/bar",
+ fmt.Sprintf(common.PullSecretAnnotation, "dummy"): "pullsecret:foo/bar",
}
img := NewFromIdentifier("dummy=foo/bar:1.12")
credSrc := img.GetParameterPullSecret(annotations)
@@ -173,16 +216,43 @@ func Test_GetSecretOption(t *testing.T) {
t.Run("Invalid reference in annotation", func(t *testing.T) {
annotations := map[string]string{
- fmt.Sprintf(common.SecretListAnnotation, "dummy"): "foo/bar",
+ fmt.Sprintf(common.PullSecretAnnotation, "dummy"): "foo/bar",
}
img := NewFromIdentifier("dummy=foo/bar:1.12")
credSrc := img.GetParameterPullSecret(annotations)
require.Nil(t, credSrc)
})
+
+ t.Run("Prefer cred source from image-specific annotation", func(t *testing.T) {
+ annotations := map[string]string{
+ fmt.Sprintf(common.PullSecretAnnotation, "dummy"): "pullsecret:image/specific",
+ common.ApplicationWidePullSecretAnnotation: "pullsecret:app/wide",
+ }
+ img := NewFromIdentifier("dummy=foo/bar:1.12")
+ credSrc := img.GetParameterPullSecret(annotations)
+ require.NotNil(t, credSrc)
+ assert.Equal(t, CredentialSourcePullSecret, credSrc.Type)
+ assert.Equal(t, "image", credSrc.SecretNamespace)
+ assert.Equal(t, "specific", credSrc.SecretName)
+ assert.Equal(t, ".dockerconfigjson", credSrc.SecretField)
+ })
+
+ t.Run("Get cred source from application-wide annotation", func(t *testing.T) {
+ annotations := map[string]string{
+ common.ApplicationWidePullSecretAnnotation: "pullsecret:app/wide",
+ }
+ img := NewFromIdentifier("dummy=foo/bar:1.12")
+ credSrc := img.GetParameterPullSecret(annotations)
+ require.NotNil(t, credSrc)
+ assert.Equal(t, CredentialSourcePullSecret, credSrc.Type)
+ assert.Equal(t, "app", credSrc.SecretNamespace)
+ assert.Equal(t, "wide", credSrc.SecretName)
+ assert.Equal(t, ".dockerconfigjson", credSrc.SecretField)
+ })
}
func Test_GetIgnoreTags(t *testing.T) {
- t.Run("Get list of tags to ignore from annotation", func(t *testing.T) {
+ t.Run("Get list of tags to ignore from image-specific annotation", func(t *testing.T) {
annotations := map[string]string{
fmt.Sprintf(common.IgnoreTagsOptionAnnotation, "dummy"): "tag1, ,tag2, tag3 , tag4",
}
@@ -194,6 +264,59 @@ func Test_GetIgnoreTags(t *testing.T) {
assert.Equal(t, "tag3", tags[2])
assert.Equal(t, "tag4", tags[3])
})
+
+ t.Run("Prefer list of tags to ignore from image-specific annotation", func(t *testing.T) {
+ annotations := map[string]string{
+ fmt.Sprintf(common.IgnoreTagsOptionAnnotation, "dummy"): "tag1, tag2",
+ common.ApplicationWideIgnoreTagsOptionAnnotation: "tag3, tag4",
+ }
+ img := NewFromIdentifier("dummy=foo/bar:1.12")
+ tags := img.GetParameterIgnoreTags(annotations)
+ require.Len(t, tags, 2)
+ assert.Equal(t, "tag1", tags[0])
+ assert.Equal(t, "tag2", tags[1])
+ })
+
+ t.Run("Get list of tags to ignore from application-wide annotation", func(t *testing.T) {
+ annotations := map[string]string{
+ common.ApplicationWideIgnoreTagsOptionAnnotation: "tag3, tag4",
+ }
+ img := NewFromIdentifier("dummy=foo/bar:1.12")
+ tags := img.GetParameterIgnoreTags(annotations)
+ require.Len(t, tags, 2)
+ assert.Equal(t, "tag3", tags[0])
+ assert.Equal(t, "tag4", tags[1])
+ })
+}
+
+func Test_HasForceUpdateOptionAnnotation(t *testing.T) {
+ t.Run("Get force-update option from image-specific annotation", func(t *testing.T) {
+ annotations := map[string]string{
+ fmt.Sprintf(common.ForceUpdateOptionAnnotation, "dummy"): "true",
+ }
+ img := NewFromIdentifier("dummy=foo/bar:1.12")
+ forceUpdate := img.HasForceUpdateOptionAnnotation(annotations)
+ assert.True(t, forceUpdate)
+ })
+
+ t.Run("Prefer force-update option from image-specific annotation", func(t *testing.T) {
+ annotations := map[string]string{
+ fmt.Sprintf(common.ForceUpdateOptionAnnotation, "dummy"): "true",
+ common.ApplicationWideForceUpdateOptionAnnotation: "false",
+ }
+ img := NewFromIdentifier("dummy=foo/bar:1.12")
+ forceUpdate := img.HasForceUpdateOptionAnnotation(annotations)
+ assert.True(t, forceUpdate)
+ })
+
+ t.Run("Get force-update option from application-wide annotation", func(t *testing.T) {
+ annotations := map[string]string{
+ common.ApplicationWideForceUpdateOptionAnnotation: "false",
+ }
+ img := NewFromIdentifier("dummy=foo/bar:1.12")
+ forceUpdate := img.HasForceUpdateOptionAnnotation(annotations)
+ assert.False(t, forceUpdate)
+ })
}
func Test_GetPlatformOptions(t *testing.T) {