diff --git a/cli/recipe/recipe.go b/cli/recipe/recipe.go index 4aa88d21e..81d209f1e 100644 --- a/cli/recipe/recipe.go +++ b/cli/recipe/recipe.go @@ -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" diff --git a/go.mod b/go.mod index 08e957bd4..38c7021c5 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index aff827a9d..681cfcc16 100644 --- a/go.sum +++ b/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= diff --git a/tagcmp/README.md b/tagcmp/README.md deleted file mode 100644 index a62601fae..000000000 --- a/tagcmp/README.md +++ /dev/null @@ -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 diff --git a/tagcmp/tagcmp.go b/tagcmp/tagcmp.go deleted file mode 100644 index 430d85e04..000000000 --- a/tagcmp/tagcmp.go +++ /dev/null @@ -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 -}