From 0dd0643508fa9ef50697c6c64fac8ad294949faa Mon Sep 17 00:00:00 2001 From: Jan Pieper Date: Mon, 10 Jan 2022 09:13:35 +0100 Subject: [PATCH 1/2] feat: support app-wide update-strategy annotations --- docs/configuration/images.md | 18 +++++ pkg/common/constants.go | 11 ++- pkg/image/options.go | 109 ++++++++++++++++++++--------- pkg/image/options_test.go | 129 ++++++++++++++++++++++++++++++++++- 4 files changed, 229 insertions(+), 38 deletions(-) diff --git a/docs/configuration/images.md b/docs/configuration/images.md index af089620..b2914345 100644 --- a/docs/configuration/images.md +++ b/docs/configuration/images.md @@ -402,3 +402,21 @@ must be prefixed with `argocd-image-updater.argoproj.io`. |`.helm.image-name`|`image.name`|Name of the Helm parameter used for specifying the image name, i.e. holds `image/name`| |`.helm.image-tag`|`image.tag`|Name of the Helm parameter used for specifying the image tag, i.e. holds `1.0`| |`.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/common/constants.go b/pkg/common/constants.go index 84f39c82..8b7e3c4e 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" + SecretListAnnotation = 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" + ApplicationWideSecretListAnnotation = ImageUpdaterAnnotationPrefix + "/pull-secret" ) // Application update configuration related annotations diff --git a/pkg/image/options.go b/pkg/image/options.go index f149b812..e31f4a9e 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.SecretListAnnotation, img.normalizedSymbolicName()), + common.ApplicationWideSecretListAnnotation, + } + 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 c1883c58..7fd82a5e 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,6 +172,32 @@ 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, ®exp.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, ®exp.Regexp{}, matchArgs) + assert.False(t, matchFunc("0.0.1", matchArgs)) + assert.True(t, matchFunc("v0.0.1", matchArgs)) + }) } func Test_GetSecretOption(t *testing.T) { @@ -179,10 +222,37 @@ func Test_GetSecretOption(t *testing.T) { 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.SecretListAnnotation, "dummy"): "pullsecret:image/specific", + common.ApplicationWideSecretListAnnotation: "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.ApplicationWideSecretListAnnotation: "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) { From 7a5a57fcd530ae70346a24a34134eae83b43de03 Mon Sep 17 00:00:00 2001 From: Jan Pieper Date: Sat, 29 Jan 2022 13:36:42 +0100 Subject: [PATCH 2/2] refactor: rename constants for pull-secret annotations --- pkg/argocd/update_test.go | 2 +- pkg/common/constants.go | 4 ++-- pkg/image/options.go | 4 ++-- pkg/image/options_test.go | 10 +++++----- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/pkg/argocd/update_test.go b/pkg/argocd/update_test.go index ff8abf86..811f4b6d 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 8b7e3c4e..a342cd4a 100644 --- a/pkg/common/constants.go +++ b/pkg/common/constants.go @@ -33,7 +33,7 @@ const ( IgnoreTagsOptionAnnotation = ImageUpdaterAnnotationPrefix + "/%s.ignore-tags" ForceUpdateOptionAnnotation = ImageUpdaterAnnotationPrefix + "/%s.force-update" UpdateStrategyAnnotation = ImageUpdaterAnnotationPrefix + "/%s.update-strategy" - SecretListAnnotation = ImageUpdaterAnnotationPrefix + "/%s.pull-secret" + PullSecretAnnotation = ImageUpdaterAnnotationPrefix + "/%s.pull-secret" PlatformsAnnotation = ImageUpdaterAnnotationPrefix + "/%s.platforms" ) @@ -43,7 +43,7 @@ const ( ApplicationWideIgnoreTagsOptionAnnotation = ImageUpdaterAnnotationPrefix + "/ignore-tags" ApplicationWideForceUpdateOptionAnnotation = ImageUpdaterAnnotationPrefix + "/force-update" ApplicationWideUpdateStrategyAnnotation = ImageUpdaterAnnotationPrefix + "/update-strategy" - ApplicationWideSecretListAnnotation = ImageUpdaterAnnotationPrefix + "/pull-secret" + ApplicationWidePullSecretAnnotation = ImageUpdaterAnnotationPrefix + "/pull-secret" ) // Application update configuration related annotations diff --git a/pkg/image/options.go b/pkg/image/options.go index e31f4a9e..df357c38 100644 --- a/pkg/image/options.go +++ b/pkg/image/options.go @@ -176,8 +176,8 @@ 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.SecretListAnnotation, img.normalizedSymbolicName()), - common.ApplicationWideSecretListAnnotation, + fmt.Sprintf(common.PullSecretAnnotation, img.normalizedSymbolicName()), + common.ApplicationWidePullSecretAnnotation, } var pullSecretVal = "" for _, key := range pullSecretAnnotations { diff --git a/pkg/image/options_test.go b/pkg/image/options_test.go index 7fd82a5e..a563eec3 100644 --- a/pkg/image/options_test.go +++ b/pkg/image/options_test.go @@ -203,7 +203,7 @@ func Test_GetMatchOption(t *testing.T) { 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) @@ -216,7 +216,7 @@ 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) @@ -225,8 +225,8 @@ func Test_GetSecretOption(t *testing.T) { t.Run("Prefer cred source from image-specific annotation", func(t *testing.T) { annotations := map[string]string{ - fmt.Sprintf(common.SecretListAnnotation, "dummy"): "pullsecret:image/specific", - common.ApplicationWideSecretListAnnotation: "pullsecret:app/wide", + fmt.Sprintf(common.PullSecretAnnotation, "dummy"): "pullsecret:image/specific", + common.ApplicationWidePullSecretAnnotation: "pullsecret:app/wide", } img := NewFromIdentifier("dummy=foo/bar:1.12") credSrc := img.GetParameterPullSecret(annotations) @@ -239,7 +239,7 @@ func Test_GetSecretOption(t *testing.T) { t.Run("Get cred source from application-wide annotation", func(t *testing.T) { annotations := map[string]string{ - common.ApplicationWideSecretListAnnotation: "pullsecret:app/wide", + common.ApplicationWidePullSecretAnnotation: "pullsecret:app/wide", } img := NewFromIdentifier("dummy=foo/bar:1.12") credSrc := img.GetParameterPullSecret(annotations)