package image import ( "path/filepath" "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/log" "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/options" "github.com/argoproj-labs/argocd-image-updater/registry-scanner/pkg/tag" "github.com/Masterminds/semver/v3" ) // VersionSortMode defines the method to sort a list of tags type UpdateStrategy int const ( // VersionSortSemVer sorts tags using semver sorting (the default) StrategySemVer UpdateStrategy = 0 // VersionSortLatest sorts tags after their creation date StrategyNewestBuild UpdateStrategy = 1 // VersionSortName sorts tags alphabetically by name StrategyAlphabetical UpdateStrategy = 2 // VersionSortDigest uses latest digest of an image StrategyDigest UpdateStrategy = 3 ) func (us UpdateStrategy) String() string { switch us { case StrategySemVer: return "semver" case StrategyNewestBuild: return "newest-build" case StrategyAlphabetical: return "alphabetical" case StrategyDigest: return "digest" } return "unknown" } // ConstraintMatchMode defines how the constraint should be matched type ConstraintMatchMode int const ( // ConstraintMatchSemVer uses semver to match a constraint ConstraintMatchSemver ConstraintMatchMode = 0 // ConstraintMatchRegExp uses regexp to match a constraint ConstraintMatchRegExp ConstraintMatchMode = 1 // ConstraintMatchNone does not enforce a constraint ConstraintMatchNone ConstraintMatchMode = 2 ) // VersionConstraint defines a constraint for comparing versions type VersionConstraint struct { Constraint string MatchFunc MatchFuncFn MatchArgs interface{} IgnoreList []string Strategy UpdateStrategy Options *options.ManifestOptions } type MatchFuncFn func(tagName string, pattern interface{}) bool // String returns the string representation of VersionConstraint func (vc *VersionConstraint) String() string { return vc.Constraint } func NewVersionConstraint() *VersionConstraint { return &VersionConstraint{ MatchFunc: MatchFuncNone, Strategy: StrategySemVer, Options: options.NewManifestOptions(), } } // GetNewestVersionFromTags returns the latest available version from a list of // tags while optionally taking a semver constraint into account. Returns the // original version if no new version could be found from the list of tags. func (img *ContainerImage) GetNewestVersionFromTags(vc *VersionConstraint, tagList *tag.ImageTagList) (*tag.ImageTag, error) { logCtx := log.NewContext() logCtx.AddField("image", img.String()) var availableTags tag.SortableImageTagList switch vc.Strategy { case StrategySemVer: availableTags = tagList.SortBySemVer() case StrategyAlphabetical: availableTags = tagList.SortAlphabetically() case StrategyNewestBuild: availableTags = tagList.SortByDate() case StrategyDigest: availableTags = tagList.SortAlphabetically() } considerTags := tag.SortableImageTagList{} // It makes no sense to proceed if we have no available tags if len(availableTags) == 0 { return img.ImageTag, nil } // The given constraint MUST match a semver constraint var semverConstraint *semver.Constraints var err error if vc.Strategy == StrategySemVer { // TODO: Shall we really ensure a valid semver on the current tag? // This prevents updating from a non-semver tag currently. if img.ImageTag != nil && img.ImageTag.TagName != "" { _, err := semver.NewVersion(img.ImageTag.TagName) if err != nil { return nil, err } } if vc.Constraint != "" { if vc.Strategy == StrategySemVer { semverConstraint, err = semver.NewConstraint(vc.Constraint) if err != nil { logCtx.Errorf("invalid constraint '%s' given: '%v'", vc, err) return nil, err } } } } // Loop through all tags to check whether it's an update candidate. for _, tag := range availableTags { logCtx.Tracef("Finding out whether to consider %s for being updateable", tag.TagName) if vc.Strategy == StrategySemVer { // Non-parseable tag does not mean error - just skip it ver, err := semver.NewVersion(tag.TagName) if err != nil { logCtx.Tracef("Not a valid version: %s", tag.TagName) continue } // If we have a version constraint, check image tag against it. If the // constraint is not satisfied, skip tag. if semverConstraint != nil { if !semverConstraint.Check(ver) { logCtx.Tracef("%s did not match constraint %s", ver.Original(), vc.Constraint) continue } } } else if vc.Strategy == StrategyDigest { if tag.TagName != vc.Constraint { logCtx.Tracef("%s did not match contraint %s", tag.TagName, vc.Constraint) continue } } // Append tag as update candidate considerTags = append(considerTags, tag) } logCtx.Debugf("found %d from %d tags eligible for consideration", len(considerTags), len(availableTags)) // If we found tags to consider, return the most recent tag found according // to the update strategy. if len(considerTags) > 0 { return considerTags[len(considerTags)-1], nil } return nil, nil } // IsTagIgnored matches tag against the patterns in IgnoreList and returns true if one of them matches func (vc *VersionConstraint) IsTagIgnored(tag string) bool { for _, t := range vc.IgnoreList { if match, err := filepath.Match(t, tag); err == nil && match { log.Tracef("tag %s is ignored by pattern %s", tag, t) return true } } return false } // IsCacheable returns true if we can safely cache tags for strategy s func (s UpdateStrategy) IsCacheable() bool { switch s { case StrategyDigest: return false default: return true } } // NeedsMetadata returns true if strategy s requires image metadata to work correctly func (s UpdateStrategy) NeedsMetadata() bool { switch s { case StrategyNewestBuild: return true default: return false } } // NeedsVersionConstraint returns true if strategy s requires a version constraint to be defined func (s UpdateStrategy) NeedsVersionConstraint() bool { switch s { case StrategyDigest: return true default: return false } } // WantsOnlyConstraintTag returns true if strategy s only wants to inspect the tag specified by the constraint func (s UpdateStrategy) WantsOnlyConstraintTag() bool { switch s { case StrategyDigest: return true default: return false } }