forked from coop-cloud/abra
refactor: de-vendor tagcmp into its own repo
This commit is contained in:
parent
d5b893d9de
commit
98ec23761f
@ -15,7 +15,7 @@ import (
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/client"
|
||||
"coopcloud.tech/abra/config"
|
||||
"coopcloud.tech/abra/tagcmp"
|
||||
"coopcloud.tech/tagcmp"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/docker/distribution/reference"
|
||||
|
1
go.mod
1
go.mod
@ -3,6 +3,7 @@ module coopcloud.tech/abra
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
coopcloud.tech/tagcmp v0.0.0-20210809131701-f81a7c03b97c
|
||||
github.com/AlecAivazis/survey/v2 v2.2.15
|
||||
github.com/Autonomic-Cooperative/godotenv v1.3.1-0.20210731170023-c37c0920d1a4
|
||||
github.com/containerd/containerd v1.5.5 // indirect
|
||||
|
2
go.sum
2
go.sum
@ -21,6 +21,8 @@ cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIA
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||
coopcloud.tech/tagcmp v0.0.0-20210809131701-f81a7c03b97c h1:j4y6MBImeCU/nH7vFt9vIkFKJFcbtdSHk3OST79Mfj4=
|
||||
coopcloud.tech/tagcmp v0.0.0-20210809131701-f81a7c03b97c/go.mod h1:ESVm0wQKcbcFi06jItF3rI7enf4Jt2PvbkWpDDHk1DQ=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/AlecAivazis/survey/v2 v2.2.15 h1:6UNMnk+YGegYFiPfdTOyZDIN+m08x2nGnqOn15BWcEQ=
|
||||
github.com/AlecAivazis/survey/v2 v2.2.15/go.mod h1:TH2kPCDU3Kqq7pLbnCWwZXDBjnhZtmsCle5EiYDJ2fg=
|
||||
|
@ -1,2 +0,0 @@
|
||||
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
|
300
tagcmp/tagcmp.go
300
tagcmp/tagcmp.go
@ -1,300 +0,0 @@
|
||||
// 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 {
|
||||
return !t.IsGreaterThan(tag)
|
||||
}
|
||||
|
||||
// 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)
|
||||
return p1 == p2
|
||||
}
|
||||
|
||||
// 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 == "" || 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