From 260edad142843b450301d14deefc82874ea75404 Mon Sep 17 00:00:00 2001 From: decentral1se Date: Mon, 9 Aug 2021 13:53:40 +0200 Subject: [PATCH] feat: add vendored tagcmp temporarily See https://git.coopcloud.tech/coop-cloud/coopcloud.tech/issues/20. --- tagcmp/README.md | 2 + tagcmp/tagcmp.go | 308 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 310 insertions(+) create mode 100644 tagcmp/README.md create mode 100644 tagcmp/tagcmp.go diff --git a/tagcmp/README.md b/tagcmp/README.md new file mode 100644 index 00000000..a62601fa --- /dev/null +++ b/tagcmp/README.md @@ -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 diff --git a/tagcmp/tagcmp.go b/tagcmp/tagcmp.go new file mode 100644 index 00000000..2e0183d9 --- /dev/null +++ b/tagcmp/tagcmp.go @@ -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 +}