Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a semver-transformer annotation to assist in converting sloppy tags to semvers #413

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
16 changes: 14 additions & 2 deletions cmd/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ func newTestCommand() *cobra.Command {
registriesConfPath string
logLevel string
allowTags string
semverTransformer string
credentials string
kubeConfig string
disableKubernetes bool
Expand Down Expand Up @@ -75,6 +76,7 @@ argocd-image-updater test nginx --allow-tags '^1.19.\d+(\-.*)*$' --update-strate
}

img := image.NewFromIdentifier(args[0])
logCtx := img.LogContext()

vc := &image.VersionConstraint{
Constraint: semverConstraint,
Expand All @@ -84,12 +86,21 @@ argocd-image-updater test nginx --allow-tags '^1.19.\d+(\-.*)*$' --update-strate
vc.Strategy = img.ParseUpdateStrategy(strategy)

if allowTags != "" {
vc.MatchFunc, vc.MatchArgs = img.ParseMatchfunc(allowTags)
vc.MatchFunc, err = img.ParseMatchfunc(allowTags)
if err != nil {
logCtx.Fatalf("failed to parse match func: %v", err)
}
}

if semverTransformer != "" {
vc.SemVerTransformFunc, err = img.ParseSemVerTransformFunc(semverTransformer)
if err != nil {
logCtx.Fatalf("failed to parse semver transformer: %v", err)
}
}

vc.IgnoreList = ignoreTags

logCtx := img.LogContext()
logCtx.Infof("retrieving information about image")

vc.Options = options.NewManifestOptions()
Expand Down Expand Up @@ -176,6 +187,7 @@ argocd-image-updater test nginx --allow-tags '^1.19.\d+(\-.*)*$' --update-strate
runCmd.Flags().StringVar(&semverConstraint, "semver-constraint", "", "only consider tags matching semantic version constraint")
runCmd.Flags().StringVar(&allowTags, "allow-tags", "", "only consider tags in registry that satisfy the match function")
runCmd.Flags().StringArrayVar(&ignoreTags, "ignore-tags", nil, "ignore tags in registry that match given glob pattern")
runCmd.Flags().StringVar(&semverTransformer, "semver-transformer", "", "transform tags before parsing semvers")
runCmd.Flags().StringVar(&strategy, "update-strategy", "semver", "update strategy to use, one of: semver, latest)")
runCmd.Flags().StringVar(&registriesConfPath, "registries-conf-path", "", "path to registries configuration")
runCmd.Flags().StringVar(&logLevel, "loglevel", "debug", "log level to use (one of trace, debug, info, warn, error)")
Expand Down
11 changes: 10 additions & 1 deletion pkg/argocd/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,16 @@ func UpdateApplication(updateConf *UpdateConfiguration, state *SyncIterationStat
}

vc.Strategy = applicationImage.GetParameterUpdateStrategy(updateConf.UpdateApp.Application.Annotations)
vc.MatchFunc, vc.MatchArgs = applicationImage.GetParameterMatch(updateConf.UpdateApp.Application.Annotations)
if vc.MatchFunc, err = applicationImage.GetParameterMatch(updateConf.UpdateApp.Application.Annotations); err != nil {
imgCtx.Errorf("Could not parse match parameter: %v", err)
result.NumErrors += 1
continue
}
if vc.SemVerTransformFunc, err = applicationImage.GetParameterSemVerTransformer(updateConf.UpdateApp.Application.Annotations); err != nil {
imgCtx.Errorf("Could not parse transform parameter: %v", err)
result.NumErrors += 1
continue
}
vc.IgnoreList = applicationImage.GetParameterIgnoreTags(updateConf.UpdateApp.Application.Annotations)
vc.Options = applicationImage.
GetPlatformOptions(updateConf.UpdateApp.Application.Annotations, updateConf.IgnorePlatforms).
Expand Down
2 changes: 2 additions & 0 deletions pkg/common/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const (
UpdateStrategyAnnotation = ImageUpdaterAnnotationPrefix + "/%s.update-strategy"
PullSecretAnnotation = ImageUpdaterAnnotationPrefix + "/%s.pull-secret"
PlatformsAnnotation = ImageUpdaterAnnotationPrefix + "/%s.platforms"
SemverTransformAnnotation = ImageUpdaterAnnotationPrefix + "/%s.semver-transformer"
)

// Application-wide update strategy related annotations
Expand All @@ -44,6 +45,7 @@ const (
ApplicationWideForceUpdateOptionAnnotation = ImageUpdaterAnnotationPrefix + "/force-update"
ApplicationWideUpdateStrategyAnnotation = ImageUpdaterAnnotationPrefix + "/update-strategy"
ApplicationWidePullSecretAnnotation = ImageUpdaterAnnotationPrefix + "/pull-secret"
ApplicationWideSemverTransformAnnotation = ImageUpdaterAnnotationPrefix + "/semver-transformer"
)

// Application update configuration related annotations
Expand Down
6 changes: 6 additions & 0 deletions pkg/image/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"strings"
"time"

"github.com/Masterminds/semver"
"github.com/argoproj-labs/argocd-image-updater/pkg/log"
"github.com/argoproj-labs/argocd-image-updater/pkg/tag"

Expand Down Expand Up @@ -54,6 +55,11 @@ func NewFromIdentifier(identifier string) *ContainerImage {
TagName: tagged.Tag(),
}
}

if img.ImageTag != nil && img.ImageTag.TagName != "" {
img.ImageTag.TagVersion, _ = semver.NewVersion(img.ImageTag.TagName)
}

img.original = identifier
return &img
}
Expand Down
15 changes: 5 additions & 10 deletions pkg/image/matchfunc.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,21 @@ package image

import (
"regexp"

"github.com/argoproj-labs/argocd-image-updater/pkg/log"
)

// MatchFuncAny matches any pattern, i.e. always returns true
func MatchFuncAny(tagName string, args interface{}) bool {
func MatchFuncAny(tagName string) bool {
return true
}

// MatchFuncNone matches no pattern, i.e. always returns false
func MatchFuncNone(tagName string, args interface{}) bool {
func MatchFuncNone(tagName string) bool {
return false
}

// MatchFuncRegexp matches the tagName against regexp pattern and returns the result
func MatchFuncRegexp(tagName string, args interface{}) bool {
pattern, ok := args.(*regexp.Regexp)
if !ok {
log.Errorf("args is not a RegExp")
return false
func MatchFuncRegexpFactory(pattern *regexp.Regexp) MatchFuncFn {
return func(tagName string) bool {
return pattern.Match([]byte(tagName))
}
return pattern.Match([]byte(tagName))
}
11 changes: 4 additions & 7 deletions pkg/image/matchfunc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,17 @@ import (
)

func Test_MatchFuncAny(t *testing.T) {
assert.True(t, MatchFuncAny("whatever", nil))
assert.True(t, MatchFuncAny("whatever"))
}

func Test_MatchFuncNone(t *testing.T) {
assert.False(t, MatchFuncNone("whatever", nil))
assert.False(t, MatchFuncNone("whatever"))
}

func Test_MatchFuncRegexp(t *testing.T) {
t.Run("Test with valid expression", func(t *testing.T) {
re := regexp.MustCompile("[a-z]+")
assert.True(t, MatchFuncRegexp("lemon", re))
assert.False(t, MatchFuncRegexp("31337", re))
})
t.Run("Test with invalid type", func(t *testing.T) {
assert.False(t, MatchFuncRegexp("lemon", "[a-z]+"))
assert.True(t, MatchFuncRegexpFactory(re)("lemon"))
assert.False(t, MatchFuncRegexpFactory(re)("31337"))
})
}
60 changes: 48 additions & 12 deletions pkg/image/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/argoproj-labs/argocd-image-updater/pkg/common"
"github.com/argoproj-labs/argocd-image-updater/pkg/options"
"sigs.k8s.io/kustomize/kyaml/errors"
)

// GetParameterHelmImageName gets the value for image-name option for the image
Expand Down Expand Up @@ -112,10 +113,33 @@ func (img *ContainerImage) ParseUpdateStrategy(val string) UpdateStrategy {
}
}

// GetParameterSemVerTransformer returns the transform function and pattern to use for transforming
// tagnames before matching or sorting.
func (img *ContainerImage) GetParameterSemVerTransformer(annotations map[string]string) (SemVerTransformFuncFn, error) {
allowTagsAnnotations := []string{
fmt.Sprintf(common.SemverTransformAnnotation, img.normalizedSymbolicName()),
common.ApplicationWideSemverTransformAnnotation,
}
var allowTagsVal = ""
for _, key := range allowTagsAnnotations {
if val, ok := annotations[key]; ok {
allowTagsVal = val
break
}
}
logCtx := img.LogContext()
if allowTagsVal == "" {
logCtx.Tracef("No match annotation found")
return SemVerTransformFuncNone, nil
}
return img.ParseSemVerTransformFunc(allowTagsVal)

}

// 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{}) {
func (img *ContainerImage) GetParameterMatch(annotations map[string]string) (MatchFuncFn, error) {
allowTagsAnnotations := []string{
fmt.Sprintf(common.AllowTagsOptionAnnotation, img.normalizedSymbolicName()),
common.ApplicationWideAllowTagsOptionAnnotation,
Expand All @@ -140,36 +164,48 @@ func (img *ContainerImage) GetParameterMatch(annotations map[string]string) (Mat
}
if allowTagsVal == "" {
logCtx.Tracef("No match annotation found")
return MatchFuncAny, ""
return MatchFuncAny, nil
}
return img.ParseMatchfunc(allowTagsVal)
}

// ParseMatchfunc returns a matcher function and its argument from given value
func (img *ContainerImage) ParseMatchfunc(val string) (MatchFuncFn, interface{}) {
logCtx := img.LogContext()

func (img *ContainerImage) ParseMatchfunc(val string) (MatchFuncFn, error) {
// The special value "any" doesn't take any parameter
if strings.ToLower(val) == "any" {
return MatchFuncAny, nil
}

opt := strings.SplitN(val, ":", 2)
if len(opt) != 2 {
logCtx.Warnf("Invalid match option syntax '%s', ignoring", val)
return MatchFuncNone, nil
return nil, fmt.Errorf("invalid match option syntax %q", val)
}
switch strings.ToLower(opt[0]) {
case "regexp":
re, err := regexp.Compile(opt[1])
if err != nil {
return nil, fmt.Errorf("could not compile regexp %q: %w", opt[1], err)
}
return MatchFuncRegexpFactory(re), nil
default:
return nil, fmt.Errorf("unknown match function: %q", opt[0])
}
}

func (img *ContainerImage) ParseSemVerTransformFunc(val string) (SemVerTransformFuncFn, error) {
opt := strings.SplitN(val, ":", 2)
if len(opt) != 2 {
return nil, fmt.Errorf("invalid match option syntax %q", val)
}
switch strings.ToLower(opt[0]) {
case "regexp":
re, err := regexp.Compile(opt[1])
if err != nil {
logCtx.Warnf("Could not compile regexp '%s'", opt[1])
return MatchFuncNone, nil
return nil, errors.Errorf("could not compile regexp %q: %w", opt[1], err)
}
return MatchFuncRegexp, re
return SemVerTransformerFuncRegexpFactory(re), nil
default:
logCtx.Warnf("Unknown match function: %s", opt[0])
return MatchFuncNone, nil
return nil, fmt.Errorf("unknown match function: %q", opt[0])
}
}

Expand Down
Loading