Compare commits

...

5 Commits

Author SHA1 Message Date
knoflook b3072cb5e2
test: pin compatiliby check tests 2021-11-01 10:48:47 +01:00
knoflook 7586abc3ca
feat: add pin compatibility check 2021-11-01 10:48:22 +01:00
decentral1se 3cac15cba2
docs: update godoc [ci skip] 2021-10-12 00:07:50 +02:00
knoflook 4f27c74467 test: increase code coverage from 62% to 99.5% 2021-10-11 14:08:27 +00:00
knoflook 0fed62dc8e
refactor!: create and use TagDelta struct et al
add a TagDelta struct with accompanying function String()
rename UpgradeElement() to UpgradeDelta() and change the return type
move UpgradeType() to be a function of TagDelta
add ParseDelta() that returns a TagDelta from string
2021-10-11 14:36:25 +02:00
4 changed files with 365 additions and 65 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
fmtcoverage.html

View File

@ -12,7 +12,6 @@ Package tagcmp provides image tag comparison operations\.
- [Variables](<#variables>)
- [func IsParsable(tag string) bool](<#func-isparsable>)
- [func UpgradeType(t Tag) int](<#func-upgradetype>)
- [type ByTagAsc](<#type-bytagasc>)
- [func (t ByTagAsc) Len() int](<#func-bytagasc-len>)
- [func (t ByTagAsc) Less(i, j int) bool](<#func-bytagasc-less>)
@ -28,7 +27,11 @@ Package tagcmp provides image tag comparison operations\.
- [func (t Tag) IsGreaterThan(tag Tag) bool](<#func-tag-isgreaterthan>)
- [func (t Tag) IsLessThan(tag Tag) bool](<#func-tag-islessthan>)
- [func (t Tag) String() string](<#func-tag-string>)
- [func (curTag Tag) UpgradeElement(newTag Tag) (Tag, error)](<#func-tag-upgradeelement>)
- [func (curTag Tag) UpgradeDelta(newTag Tag) (TagDelta, error)](<#func-tag-upgradedelta>)
- [type TagDelta](<#type-tagdelta>)
- [func ParseDelta(delta string) (TagDelta, error)](<#func-parsedelta>)
- [func (t TagDelta) String() string](<#func-tagdelta-string>)
- [func (d TagDelta) UpgradeType() int](<#func-tagdelta-upgradetype>)
## Variables
@ -71,14 +74,6 @@ func IsParsable(tag string) bool
IsParsable determines if a tag is supported by this library
## func UpgradeType
```go
func UpgradeType(t Tag) int
```
UpgradeType takes exit from UpgradeElemene and returns a numeric representation of upgrade or downgrade 1/\-1: patch 2/\-2: minor 4/\-4: major 0: no change
## type ByTagAsc
ByTagAsc sorts tags in ascending order where the last element is the latest tag\.
@ -140,8 +135,9 @@ type Tag struct {
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")
Suffix string // tag suffix (e.g. "-alpine") [would be release candidate in semver]
UsesV bool // whether or not the tag uses the "v" prefix
Metadata string // metadata: what's after + and after the first "-"
}
```
@ -193,13 +189,45 @@ func (t Tag) String() string
String formats a Tag correctly in string representation
### func \(Tag\) UpgradeElement
### func \(Tag\) UpgradeDelta
```go
func (curTag Tag) UpgradeElement(newTag Tag) (Tag, error)
func (curTag Tag) UpgradeDelta(newTag Tag) (TagDelta, error)
```
UpgradeElement returns a Tag object which is the difference between an old and new tag It can contain negative numbers if comparing with an older tag\.
UpgradeDelta returns a TagDelta object which is the difference between an old and new tag It can contain negative numbers if comparing with an older tag\.
## type TagDelta
```go
type TagDelta struct {
Major int // major semver difference
Minor int // minor semver difference
Patch int // patch semver difference
}
```
### func ParseDelta
```go
func ParseDelta(delta string) (TagDelta, error)
```
ParseDelta converts a tag difference in the format of X\, X\.Y or X\.Y\.Z where X\, Y\, Z are positive or negative integers or 0
### func \(TagDelta\) String
```go
func (t TagDelta) String() string
```
### func \(TagDelta\) UpgradeType
```go
func (d TagDelta) UpgradeType() int
```
UpgradeType takes exit from UpgradeElemene and returns a numeric representation of upgrade or downgrade 1/\-1: patch 2/\-2: minor 4/\-4: major 0: no change

145
tagcmp.go
View File

@ -19,6 +19,12 @@ type Tag struct {
Metadata string // metadata: what's after + and after the first "-"
}
type TagDelta struct {
Major int // major semver difference
Minor int // minor semver difference
Patch int // patch semver difference
}
// ByTagAsc sorts tags in ascending order where the last element is the latest tag.
type ByTagAsc []Tag
@ -148,6 +154,12 @@ func (t Tag) String() string {
return repr
}
func (t TagDelta) String() string {
var repr string
repr = fmt.Sprintf("%d.%d.%d", t.Major, t.Minor, t.Patch)
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 {
@ -175,43 +187,54 @@ func (t Tag) IsCompatible(tag Tag) bool {
return true
}
// UpgradeElement returns a Tag object which is the difference between an old and new tag
// IsUpgradeCompatible chekcs if upTag is compatible with a pinned version tag.
// I.e. pinning to 22-fpm should return true if upTag is 22.2.0-fpm but not 22.2.0-alpine or 23.0.0-fpm
func (pin Tag) IsUpgradeCompatible(upTag Tag) bool {
if pin.Suffix != upTag.Suffix {
return false
}
if pin.Major != upTag.Major {
return false
}
if pin.MissingMinor {
return true
}
if pin.Minor != upTag.Minor {
return false
}
if pin.MissingPatch {
return true
}
if pin.Patch != upTag.Patch {
return false
}
return true
}
// UpgradeDelta returns a TagDelta object which is the difference between an old and new tag
// It can contain negative numbers if comparing with an older tag.
func (curTag Tag) UpgradeElement(newTag Tag) (Tag, error) {
func (curTag Tag) UpgradeDelta(newTag Tag) (TagDelta, error) {
if !curTag.IsCompatible(newTag) {
return Tag{}, fmt.Errorf("%s and %s are not compatible with each other", curTag.String(), newTag.String())
return TagDelta{}, fmt.Errorf("%s and %s are not compatible with each other", curTag.String(), newTag.String())
}
diff := curTag
curMajor, err := strconv.Atoi(curTag.Major)
if err != nil {
return Tag{}, err
diff := TagDelta{
Major: 0,
Minor: 0,
Patch: 0,
}
newMajor, err := strconv.Atoi(newTag.Major)
if err != nil {
return Tag{}, err
}
diff.Major = strconv.Itoa(newMajor - curMajor)
// assuming tags are correctly formatted
curMajor, _ := strconv.Atoi(curTag.Major)
newMajor, _ := strconv.Atoi(newTag.Major)
diff.Major = newMajor - curMajor
if !curTag.MissingMinor {
curMinor, err := strconv.Atoi(curTag.Minor)
if err != nil {
return Tag{}, err
}
newMinor, err := strconv.Atoi(newTag.Minor)
if err != nil {
return Tag{}, err
}
diff.Minor = strconv.Itoa(newMinor - curMinor)
curMinor, _ := strconv.Atoi(curTag.Minor)
newMinor, _ := strconv.Atoi(newTag.Minor)
diff.Minor = newMinor - curMinor
}
if !curTag.MissingPatch {
curPatch, err := strconv.Atoi(curTag.Patch)
if err != nil {
return Tag{}, err
}
newPatch, err := strconv.Atoi(newTag.Patch)
if err != nil {
return Tag{}, err
}
diff.Patch = strconv.Itoa(newPatch - curPatch)
curPatch, _ := strconv.Atoi(curTag.Patch)
newPatch, _ := strconv.Atoi(newTag.Patch)
diff.Patch = newPatch - curPatch
}
return diff, nil
@ -219,35 +242,23 @@ func (curTag Tag) UpgradeElement(newTag Tag) (Tag, error) {
// UpgradeType takes exit from UpgradeElemene and returns a numeric representation of upgrade or downgrade
// 1/-1: patch 2/-2: minor 4/-4: major 0: no change
func UpgradeType(t Tag) int {
var major, minor, patch int
major, _ = strconv.Atoi(t.Major)
if t.MissingMinor {
minor = 0
} else {
minor, _ = strconv.Atoi(t.Minor)
}
if t.MissingPatch {
patch = 0
} else {
patch, _ = strconv.Atoi(t.Patch)
}
if major > 0 {
func (d TagDelta) UpgradeType() int {
if d.Major > 0 {
return 4
}
if major < 0 {
if d.Major < 0 {
return -4
}
if minor > 0 {
if d.Minor > 0 {
return 2
}
if minor < 0 {
if d.Minor < 0 {
return -2
}
if patch > 0 {
if d.Patch > 0 {
return 1
}
if patch < 0 {
if d.Patch < 0 {
return -1
}
return 0
@ -306,6 +317,42 @@ func parseVersionPart(part string) (int, error) {
return p, nil
}
// ParseDelta converts a tag difference in the format of X, X.Y or X.Y.Z where
// X, Y, Z are positive or negative integers or 0
func ParseDelta(delta string) (TagDelta, error) {
tagDelta := TagDelta{
Major: 0,
Minor: 0,
Patch: 0,
}
splits := strings.Split(delta, ".")
if len(splits) > 3 {
return TagDelta{}, fmt.Errorf("'%s' has too much dots", delta)
}
major, err := strconv.Atoi(splits[0])
if err != nil {
return TagDelta{}, fmt.Errorf("Major part of '%s' is not an integer", delta)
}
tagDelta.Major = major
if len(splits) > 1 {
minor, err := strconv.Atoi(splits[1])
if err != nil {
return TagDelta{}, fmt.Errorf("Minor part of '%s' is not an integer", delta)
}
tagDelta.Minor = minor
}
if len(splits) > 2 {
patch, err := strconv.Atoi(splits[2])
if err != nil {
return TagDelta{}, fmt.Errorf("Minor part of '%s' is not an integer", delta)
}
tagDelta.Patch = patch
}
return tagDelta, 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

View File

@ -345,15 +345,38 @@ var supported = []string{
"v6.2.1-alpine",
"v6.2.1-alpine",
// semver with '+'
"5+SDFJ_-2l4x.",
"2.6+SDFJ_-2l4x.",
"4.3.5+SDFJ_-2l4x.",
// semver with '+' and 'v'
"v5+SDFJ_-2l4x.",
"v2.6+SDFJ_-2l4x.",
"v4.3.5+SDFJ_-2l4x.",
// semver with '+' and suffix
"5-alpine+SDFJ_-2l4x.",
"2.6-alpine+SDFJ_-2l4x.",
"4.3.5-alpine+SDFJ_-2l4x.",
// semver with 'v', '+' and suffix
"v5-alpine+SDFJ_-2l4x.",
"v2.6-alpine+SDFJ_-2l4x.",
"v4.3.5-alpine+SDFJ_-2l4x.",
// semver with multiple suffix values
"v6.2.1-alpine-foo",
// semver with multiple suffix values and '+'
"v6.2.1-alpine-foo+68BC1E",
}
var unsupported = []string{
// empty
"",
// patametrized
// parametrized
"${MAILU_VERSION:-master}",
"${PHP_VERSION}-fpm-alpine3.13",
@ -369,6 +392,9 @@ var unsupported = []string{
"r1295",
"version-r1070",
// too much dots
"1.0.0.0.0",
// prerelease
"3.7.0b1",
"3.8.0b1-alpine",
@ -389,6 +415,12 @@ var unsupported = []string{
// multiple - delimters
"apache-debian-1.8-prod",
"version-znc-1.8.2",
// unparsable major/minor/patch parts:
"a.0.0",
"1.a",
"1.a.0",
"1.0.a",
}
func TestParseUnsupported(t *testing.T) {
@ -498,6 +530,22 @@ func TestPatchPart(t *testing.T) {
}
}
func TestIsParsableSupported(t *testing.T) {
for _, tag := range supported {
if !tagcmp.IsParsable(tag) {
t.Errorf("'%s' should be parsable but IsParsable returned false", tag)
}
}
}
func TestIsParsableUnsupported(t *testing.T) {
for _, tag := range unsupported {
if tagcmp.IsParsable(tag) {
t.Errorf("'%s' should not be parsable but IsParsable returned true", tag)
}
}
}
func TestIsGreaterThan(t *testing.T) {
pairs := []struct {
t1 string
@ -505,6 +553,7 @@ func TestIsGreaterThan(t *testing.T) {
expected bool
}{
{"1.2.3", "1.2", false},
{"1.2.3", "1.2.4", false},
{"18.04", "18.1", true},
{"10.1", "10.1.2", true},
{"3", "2", true},
@ -564,10 +613,15 @@ func TestEquals(t *testing.T) {
expected bool
}{
{"1.2.3", "1.2.3", true},
{"1.2.3", "1.2", false},
{"1.2.3", "1", false},
{"18.04", "18.4", true},
{"10.0", "10.0.4", false},
{"3", "4.0", false},
{"1.2", "1.2.3", false},
{"3", "4", false},
{"1.3", "1.4", false},
{"3+FF812B", "3", false},
}
for _, p := range pairs {
p1, err := tagcmp.Parse(p.t1)
@ -730,7 +784,7 @@ func TestSortDesc(t *testing.T) {
}
}
func TestString(t *testing.T) {
func TestTagString(t *testing.T) {
for _, tag := range supported {
p, err := tagcmp.Parse(tag)
if err != nil {
@ -775,3 +829,173 @@ func TestGiteaFilterCompatible(t *testing.T) {
}
}
}
func TestDeltaParse(t *testing.T) {
supportedDeltas := []string{
"1",
"1.0",
"1.0.0",
"-1",
"-1.0",
"-1.0.0",
"-1.0.5",
"-1.2.5",
"1.-2",
}
unsupportedDeltas := []string{
"AAAAAAAAAAAAAAAA",
"1.2.3.4",
"1.ab.2",
"1.2.a",
}
for _, delta := range supportedDeltas {
if _, err := tagcmp.ParseDelta(delta); err != nil {
t.Errorf("'%s' wasn't parsed but it is supported: %s", delta, err)
}
}
for _, delta := range unsupportedDeltas {
if _, err := tagcmp.ParseDelta(delta); err == nil {
t.Errorf("'%s' was parsed but it is not supported", delta)
}
}
}
func TestDeltaString(t *testing.T) {
supportedDeltas := []struct {
in string
out string
}{
{"1", "1.0.0"},
{"1.0", "1.0.0"},
{"1.0.0", "1.0.0"},
{"-1", "-1.0.0"},
{"-1.0", "-1.0.0"},
{"-1.0.0", "-1.0.0"},
{"-1.0.5", "-1.0.5"},
{"-1.2.5", "-1.2.5"},
{"1.-2", "1.-2.0"},
}
for _, test := range supportedDeltas {
parsedDelta, err := tagcmp.ParseDelta(test.in)
if err != nil {
t.Errorf("'%s' was not parsed but it is supported", test.in)
}
if parsedDelta.String() != test.out {
t.Errorf("String() of '%s' didn't render properly: %s", test.in, parsedDelta.String())
}
}
}
func TestIsUpgradeCompatible(t *testing.T) {
testsTrue := [][]string{
{"22-fpm", "22-fpm"},
{"22-fpm", "22.0-fpm"},
{"22-fpm", "22.0.0-fpm"},
{"22-fpm", "22.2.0-fpm"},
{"22-fpm", "22.0.0-fpm"},
{"22.2-fpm", "22.2-fpm"},
{"22.2-fpm", "22.2.0-fpm"},
{"22.2.2-fpm", "22.2.2-fpm"},
}
testsFalse := [][]string{
{"22-fpm", "22-alpine"},
{"22-fpm", "23-fpm"},
{"22-fpm", "21-fpm"},
{"22.2-fpm", "22.0.2-fpm"},
{"22.2.0-fpm", "22.2.2-fpm"},
}
for _, test := range testsTrue {
pin, err := tagcmp.Parse(test[0])
if err != nil {
t.Error(err)
}
upTag, err := tagcmp.Parse(test[1])
if err != nil {
t.Error(err)
}
if !pin.IsUpgradeCompatible(upTag) {
t.Errorf("pin %s should be upgradable to %s but returned false", test[0], test[1])
}
}
for _, test := range testsFalse {
pin, err := tagcmp.Parse(test[0])
if err != nil {
t.Error(err)
}
upTag, err := tagcmp.Parse(test[1])
if err != nil {
t.Error(err)
}
if pin.IsUpgradeCompatible(upTag) {
t.Errorf("pin %s should not be upgradable to %s but returned true", test[0], test[1])
}
}
}
func TestUpgradeDelta(t *testing.T) {
pairs := []struct {
t1 string
t2 string
expected string
throwsErr bool
}{
{"v1.0.0", "1.0.0", "", true},
{"1", "2", "1.0.0", false},
{"1.0", "1.1", "0.1.0", false},
{"1.1.0", "1.1.1", "0.0.1", false},
{"2", "1", "-1.0.0", false},
{"1.1", "1.0", "0.-1.0", false},
{"1.1.1", "1.1.0", "0.0.-1", false},
}
for _, p := range pairs {
p1, err := tagcmp.Parse(p.t1)
if err != nil {
t.Errorf("'%s' should have parsed", p.t1)
}
p2, err := tagcmp.Parse(p.t2)
if err != nil {
t.Errorf("'%s' should have parsed", p.t2)
}
pexpected, _ := tagcmp.ParseDelta(p.expected)
res, err := p1.UpgradeDelta(p2)
if p.throwsErr && (err == nil) {
t.Errorf("(%s).UpgradeDelta(%s) didn't throw an error but should have", p.t1, p.t2)
} else if !p.throwsErr && (err != nil) {
t.Errorf("(%s).UpgradeDelta(%s) threw an error but shouldn't have", p.t1, p.t2)
}
if res != pexpected {
t.Errorf("(%s).UpgradeDelta(%s) gave %s but expected %s", p.t1, p.t2, res.String(), p.expected)
}
}
}
func TestUpgradeType(t *testing.T) {
testSet := []struct {
in string
out int
}{
{"1.0.0", 4},
{"-1.0.0", -4},
{"1.2.0", 4},
{"-1.2.0", -4},
{"0.1.0", 2},
{"0.-1.0", -2},
{"0.1.2", 2},
{"0.-1.2", -2},
{"0.0.1", 1},
{"0.0.-1", -1},
{"0.0.0", 0},
{"-0.-0.-0", 0},
}
for _, test := range testSet {
tagDelta, err := tagcmp.ParseDelta(test.in)
if err != nil {
t.Errorf("tagcmp.ParseDelta couldn't parse '%s': '%s'", test.in, err)
}
upType := tagDelta.UpgradeType()
if upType != test.out {
t.Errorf("(%s).UpgradeType() returned '%d', expected '%d'", test.in, upType, test.out)
}
}
}