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/cli/internal"
|
||||||
"coopcloud.tech/abra/client"
|
"coopcloud.tech/abra/client"
|
||||||
"coopcloud.tech/abra/config"
|
"coopcloud.tech/abra/config"
|
||||||
"coopcloud.tech/abra/tagcmp"
|
"coopcloud.tech/tagcmp"
|
||||||
|
|
||||||
"github.com/AlecAivazis/survey/v2"
|
"github.com/AlecAivazis/survey/v2"
|
||||||
"github.com/docker/distribution/reference"
|
"github.com/docker/distribution/reference"
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -3,6 +3,7 @@ module coopcloud.tech/abra
|
||||||
go 1.16
|
go 1.16
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
coopcloud.tech/tagcmp v0.0.0-20210809131701-f81a7c03b97c
|
||||||
github.com/AlecAivazis/survey/v2 v2.2.15
|
github.com/AlecAivazis/survey/v2 v2.2.15
|
||||||
github.com/Autonomic-Cooperative/godotenv v1.3.1-0.20210731170023-c37c0920d1a4
|
github.com/Autonomic-Cooperative/godotenv v1.3.1-0.20210731170023-c37c0920d1a4
|
||||||
github.com/containerd/containerd v1.5.5 // indirect
|
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.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.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
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=
|
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 h1:6UNMnk+YGegYFiPfdTOyZDIN+m08x2nGnqOn15BWcEQ=
|
||||||
github.com/AlecAivazis/survey/v2 v2.2.15/go.mod h1:TH2kPCDU3Kqq7pLbnCWwZXDBjnhZtmsCle5EiYDJ2fg=
|
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