forked from coop-cloud/abra
feat: add vendored tagcmp temporarily
See coop-cloud/coopcloud.tech#20.
This commit is contained in:
parent
5ac4604f8a
commit
260edad142
2
tagcmp/README.md
Normal file
2
tagcmp/README.md
Normal file
@ -0,0 +1,2 @@
|
||||
Will be taken out of our local source tree and referenced in go.mod once we
|
||||
solve https://git.coopcloud.tech/coop-cloud/coopcloud.tech/issues/20
|
308
tagcmp/tagcmp.go
Normal file
308
tagcmp/tagcmp.go
Normal file
@ -0,0 +1,308 @@
|
||||
// Package tagcmp provides image tag comparison operations.
|
||||
package tagcmp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Tag struct {
|
||||
Major string `json:",omitempty"` // major semver part
|
||||
Minor string `json:",omitempty"` // minor semver part
|
||||
MissingMinor bool // whether or not the minor semver part was left out
|
||||
Patch string `json:",omitempty"` // patch semver part
|
||||
MissingPatch bool // whether or not he patch semver part was left out
|
||||
Suffix string // tag suffix (e.g. "-alpine")
|
||||
UsesV bool // whether or not the tag uses the "v" prefix
|
||||
}
|
||||
|
||||
// ByTag sorts tags in asc/desc order where the last element is the latest tag.
|
||||
type ByTag []Tag
|
||||
|
||||
func (t ByTag) Len() int { return len(t) }
|
||||
func (t ByTag) Swap(i, j int) { t[i], t[j] = t[j], t[i] }
|
||||
func (t ByTag) Less(i, j int) bool {
|
||||
return t[i].IsLessThan(t[j])
|
||||
}
|
||||
|
||||
// IsGreaterThan tests if a tag is greater than another. There are some
|
||||
// tag-isms to take into account here, shorter is bigger (i.e. 2.1 > 2.1.1 ==
|
||||
// true, 2 > 2.1 == true).
|
||||
func (t Tag) IsGreaterThan(tag Tag) bool {
|
||||
// shorter is bigger, i.e. 2.1 > 2.1.1
|
||||
if t.MissingPatch && !tag.MissingPatch || t.MissingMinor && !tag.MissingMinor {
|
||||
return true
|
||||
}
|
||||
if tag.MissingPatch && !t.MissingPatch || tag.MissingMinor && !t.MissingMinor {
|
||||
return false
|
||||
}
|
||||
|
||||
// ignore errors since Parse already handled
|
||||
mj1, _ := strconv.Atoi(t.Major)
|
||||
mj2, _ := strconv.Atoi(tag.Major)
|
||||
if mj1 > mj2 {
|
||||
return true
|
||||
}
|
||||
if mj2 > mj1 {
|
||||
return false
|
||||
}
|
||||
|
||||
mn1, _ := strconv.Atoi(t.Minor)
|
||||
mn2, _ := strconv.Atoi(tag.Minor)
|
||||
if mn1 > mn2 {
|
||||
return true
|
||||
}
|
||||
if mn2 > mn1 {
|
||||
return false
|
||||
}
|
||||
|
||||
p1, _ := strconv.Atoi(t.Patch)
|
||||
p2, _ := strconv.Atoi(tag.Patch)
|
||||
if p1 > p2 {
|
||||
return true
|
||||
}
|
||||
if p2 > p1 {
|
||||
return false
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// IsLessThan tests if a tag is less than another. There are some tag-isms to
|
||||
// take into account here, shorter is bigger (i.e. 2.1 < 2.1.1 == false, 2 <
|
||||
// 2.1 == false).
|
||||
func (t Tag) IsLessThan(tag Tag) bool {
|
||||
if t.IsGreaterThan(tag) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Equals tests Tag equality
|
||||
func (t Tag) Equals(tag Tag) bool {
|
||||
if t.MissingPatch && !tag.MissingPatch || t.MissingMinor && !tag.MissingMinor {
|
||||
return false
|
||||
}
|
||||
|
||||
if tag.MissingPatch && !t.MissingPatch || tag.MissingMinor && !t.MissingMinor {
|
||||
return false
|
||||
}
|
||||
|
||||
// ignore errors since Parse already handled
|
||||
mj1, _ := strconv.Atoi(t.Major)
|
||||
mj2, _ := strconv.Atoi(tag.Major)
|
||||
if mj1 != mj2 {
|
||||
return false
|
||||
}
|
||||
|
||||
mn1, _ := strconv.Atoi(t.Minor)
|
||||
mn2, _ := strconv.Atoi(tag.Minor)
|
||||
if mn1 != mn2 {
|
||||
return false
|
||||
}
|
||||
|
||||
p1, _ := strconv.Atoi(t.Patch)
|
||||
p2, _ := strconv.Atoi(tag.Patch)
|
||||
if p1 != p2 {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// String formats a Tag correctly in string representation
|
||||
func (t Tag) String() string {
|
||||
var repr string
|
||||
|
||||
if t.UsesV {
|
||||
repr += "v"
|
||||
}
|
||||
|
||||
repr += t.Major
|
||||
|
||||
if !t.MissingMinor {
|
||||
repr += fmt.Sprintf(".%s", t.Minor)
|
||||
}
|
||||
|
||||
if !t.MissingPatch {
|
||||
repr += fmt.Sprintf(".%s", t.Patch)
|
||||
}
|
||||
|
||||
if t.Suffix != "" {
|
||||
repr += fmt.Sprintf("-%s", t.Suffix)
|
||||
}
|
||||
|
||||
return repr
|
||||
}
|
||||
|
||||
// IsCompatible determines if two tags can be compared together
|
||||
func (t Tag) IsCompatible(tag Tag) bool {
|
||||
if t.UsesV && !tag.UsesV || tag.UsesV && !t.UsesV {
|
||||
return false
|
||||
}
|
||||
|
||||
if t.Suffix != "" && tag.Suffix == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
if t.Suffix != "" && tag.Suffix != "" {
|
||||
if t.Suffix != tag.Suffix {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if t.MissingMinor && !tag.MissingMinor || tag.MissingMinor && !t.MissingMinor {
|
||||
return false
|
||||
}
|
||||
|
||||
if t.MissingPatch && !tag.MissingPatch || tag.MissingPatch && !t.MissingPatch {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// CommitHashPattern matches commit-like hash tags
|
||||
var CommitHashPattern = "^[a-f0-9]{7,40}$"
|
||||
|
||||
// DotPattern matches tags which contain multiple versions
|
||||
var DotPattern = "([0-9]+)\\.([0-9]+)"
|
||||
|
||||
// EmptyPattern matches when tags are missing
|
||||
var EmptyPattern = "^$"
|
||||
|
||||
// ParametrizedPattern matches when tags are parametrized
|
||||
var ParametrizedPattern = "\\${.+}"
|
||||
|
||||
// StringPattern matches when tags are only made up of alphabetic characters
|
||||
var StringPattern = "^[a-zA-Z]+$"
|
||||
|
||||
// patternMatches determines if a tag matches unsupported patterns
|
||||
func patternMatches(tag string) error {
|
||||
unsupported := []string{
|
||||
CommitHashPattern,
|
||||
EmptyPattern,
|
||||
ParametrizedPattern,
|
||||
StringPattern,
|
||||
}
|
||||
|
||||
for _, pattern := range unsupported {
|
||||
if match, _ := regexp.Match(pattern, []byte(tag)); match {
|
||||
return fmt.Errorf("'%s' is not supported (%s)", tag, pattern)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// patternCounts determines if tags match unsupported patterns by counting occurences of matches
|
||||
func patternCounts(tag string) error {
|
||||
v := regexp.MustCompile(DotPattern)
|
||||
if m := v.FindAllStringIndex(tag, -1); len(m) > 1 {
|
||||
return fmt.Errorf("'%s' is not supported (%s)", tag, DotPattern)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseVersionPart converts a semver version part to an integer
|
||||
func parseVersionPart(part string) (int, error) {
|
||||
p, err := strconv.Atoi(part)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// Parse converts an image tag into a structured data format. It aims to to
|
||||
// support the general case of tags which are "semver-like" and/or stable and
|
||||
// parseable by heuristics. Image tags follow no formal specification and
|
||||
// therefore this is a best-effort implementation. Examples of tags this
|
||||
// function can parse are: "5", "5.2", "v4", "v5.3.6", "4-alpine",
|
||||
// "v3.2.1-debian".
|
||||
func Parse(tag string) (Tag, error) {
|
||||
if err := patternMatches(tag); err != nil {
|
||||
return Tag{}, err
|
||||
}
|
||||
|
||||
if err := patternCounts(tag); err != nil {
|
||||
return Tag{}, err
|
||||
}
|
||||
|
||||
usesV := false
|
||||
if string(tag[0]) == "v" {
|
||||
tag = strings.TrimPrefix(tag, "v")
|
||||
usesV = true
|
||||
}
|
||||
|
||||
var suffix string
|
||||
splits := strings.SplitN(tag, "-", 2)
|
||||
if len(splits) > 1 {
|
||||
tag = splits[0]
|
||||
suffix = splits[1]
|
||||
}
|
||||
|
||||
var major, minor, patch string
|
||||
var missingMinor, missingPatch bool
|
||||
parts := strings.Split(tag, ".")
|
||||
switch {
|
||||
case len(parts) == 1:
|
||||
if _, err := parseVersionPart(parts[0]); err != nil {
|
||||
return Tag{}, fmt.Errorf("couldn't parse major part of '%s': '%s'", tag, parts[0])
|
||||
}
|
||||
major = parts[0]
|
||||
missingMinor = true
|
||||
missingPatch = true
|
||||
case len(parts) == 2:
|
||||
if _, err := parseVersionPart(parts[0]); err != nil {
|
||||
return Tag{}, fmt.Errorf("couldn't parse major part of '%s': '%s'", tag, parts[0])
|
||||
}
|
||||
major = parts[0]
|
||||
|
||||
if _, err := parseVersionPart(parts[1]); err != nil {
|
||||
return Tag{}, fmt.Errorf("couldn't parse minor part of '%s': '%s'", tag, parts[1])
|
||||
}
|
||||
minor = parts[1]
|
||||
missingPatch = true
|
||||
case len(parts) == 3:
|
||||
if _, err := parseVersionPart(parts[0]); err != nil {
|
||||
return Tag{}, fmt.Errorf("couldn't parse major part of '%s': '%s'", tag, parts[0])
|
||||
}
|
||||
major = parts[0]
|
||||
|
||||
if _, err := parseVersionPart(parts[1]); err != nil {
|
||||
return Tag{}, fmt.Errorf("couldn't parse minor part of '%s': '%s'", tag, parts[1])
|
||||
}
|
||||
minor = parts[1]
|
||||
|
||||
if _, err := parseVersionPart(parts[2]); err != nil {
|
||||
return Tag{}, fmt.Errorf("couldn't parse patch part of '%s': '%s'", tag, parts[2])
|
||||
}
|
||||
patch = parts[2]
|
||||
default:
|
||||
return Tag{}, fmt.Errorf("couldn't parse semver of '%s", tag)
|
||||
}
|
||||
|
||||
parsedTag := Tag{
|
||||
Major: major,
|
||||
Minor: minor,
|
||||
MissingMinor: missingMinor,
|
||||
Patch: patch,
|
||||
MissingPatch: missingPatch,
|
||||
UsesV: usesV,
|
||||
Suffix: suffix,
|
||||
}
|
||||
|
||||
return parsedTag, nil
|
||||
}
|
||||
|
||||
// IsParsable determines if a tag is supported by this library
|
||||
func IsParsable(tag string) bool {
|
||||
if _, err := Parse(tag); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
Loading…
Reference in New Issue
Block a user