diff --git a/cmd/chart/upgrade.go b/cmd/chart/upgrade.go index 66fe3fb3a..e9e1d7ece 100644 --- a/cmd/chart/upgrade.go +++ b/cmd/chart/upgrade.go @@ -16,6 +16,21 @@ import ( "github.com/spf13/cobra" ) +type tagAttributes struct { + hasSuffix bool + hasMajor bool + hasMinor bool + hasPatch bool + original string +} + +func (c *tagAttributes) attributesMatch(n tagAttributes) bool { + return c.hasMajor == n.hasMajor && + c.hasMinor == n.hasMinor && + c.hasPatch == n.hasPatch && + c.hasSuffix == n.hasSuffix +} + func MakeUpgrade() *cobra.Command { var command = &cobra.Command{ Use: "upgrade", @@ -162,56 +177,78 @@ func updateImages(iName string, v bool) (bool, string, error) { return false, iName, errors.New("unable to list tags for " + imageName) } - latestTag, hasSemVerTag := getLatestTag(ref) + candidateTag, hasSemVerTag := getCandidateTag(ref, tag) if !hasSemVerTag { - return false, iName, fmt.Errorf("no valid semver tags found for %s", imageName) + return false, iName, fmt.Errorf("no valid semver tags of current format found for %s", imageName) } laterVersionB := false // AE: Don't upgrade to an RC tag, even if it's newer. - if tagIsUpgradeable(tag, latestTag) { + if tagIsUpgradeable(tag, candidateTag) { laterVersionB = true - iName = fmt.Sprintf("%s:%s", imageName, latestTag) + iName = fmt.Sprintf("%s:%s", imageName, candidateTag) if v { - log.Printf("[%s] %s => %s", imageName, tag, latestTag) + log.Printf("[%s] %s => %s", imageName, tag, candidateTag) } } return laterVersionB, iName, nil } -func tagIsUpgradeable(currentTag, latestTag string) bool { +func tagIsUpgradeable(current, candidate string) bool { - if strings.EqualFold(currentTag, "latest") { + if strings.EqualFold(current, "latest") { return false } - currentSemVer, _ := semver.NewVersion(currentTag) - latestSemVer, _ := semver.NewVersion(latestTag) + currentSemVer, _ := semver.NewVersion(current) + candidateSemVer, _ := semver.NewVersion(candidate) - return latestSemVer.Compare(currentSemVer) == 1 && latestSemVer.Prerelease() == currentSemVer.Prerelease() + return candidateSemVer.Compare(currentSemVer) == 1 && candidateSemVer.Prerelease() == currentSemVer.Prerelease() } -func getLatestTag(discoveredTags []string) (string, bool) { +func getCandidateTag(discoveredTags []string, currentTag string) (string, bool) { - var vs []*semver.Version + var candidateTags []*semver.Version for _, tag := range discoveredTags { v, err := semver.NewVersion(tag) if err == nil { - vs = append(vs, v) + candidateTags = append(candidateTags, v) } } - if len(vs) > 0 { - sort.Sort(sort.Reverse(semver.Collection(vs))) - return vs[0].Original(), true + if len(candidateTags) > 0 { + + currentTagAttr := getTagAttributes(currentTag) + sort.Sort(sort.Reverse(semver.Collection(candidateTags))) + + for _, candidate := range candidateTags { + candidateTagAttr := getTagAttributes(candidate.Original()) + if currentTagAttr.attributesMatch(candidateTagAttr) { + return candidate.Original(), true + } + } } return "", false } + +func getTagAttributes(t string) tagAttributes { + + tagParts := strings.Split(t, "-") + tagLevels := strings.Split(tagParts[0], ".") + + return tagAttributes{ + hasSuffix: len(tagParts) > 1, + hasMajor: len(tagLevels) >= 1 && tagLevels[0] != "", + hasMinor: len(tagLevels) >= 2, + hasPatch: len(tagLevels) == 3, + original: t, + } +} diff --git a/cmd/chart/upgrade_test.go b/cmd/chart/upgrade_test.go index 9532ce374..e22589b3a 100644 --- a/cmd/chart/upgrade_test.go +++ b/cmd/chart/upgrade_test.go @@ -104,46 +104,67 @@ func Test_tagIsUpgradable(t *testing.T) { } } -func TestGetLatestTag(t *testing.T) { +func TestGetCandidateTag(t *testing.T) { tests := []struct { name string discoveredTags []string + currentTag string expectedTagVal string expectedIsSemVer bool }{ { name: "Valid semantic tags", discoveredTags: []string{"v1.0.0", "v2.1.0", "v2.3.4", "v2.3.3"}, + currentTag: "v2.3.3", expectedTagVal: "v2.3.4", expectedIsSemVer: true, }, { name: "No valid semantic tags", discoveredTags: []string{"invalid", "v.a.b", "xyz"}, + currentTag: "v2.3.3", expectedTagVal: "", expectedIsSemVer: false, }, { name: "Empty list", discoveredTags: []string{}, + currentTag: "v2.3.3", expectedTagVal: "", expectedIsSemVer: false, }, { name: "Mixed valid and invalid tags", - discoveredTags: []string{"v1.0.0", "invalid", "v2.1.0-beta", "v1.2.3"}, - expectedTagVal: "v2.1.0-beta", + discoveredTags: []string{"v1.2", "v1.0.0", "invalid", "v2.1.0-beta", "v1.2.4"}, + currentTag: "v1.2.3", + expectedTagVal: "v1.2.4", + expectedIsSemVer: true, + }, + { + name: "Mixed valid and invalid tags", + discoveredTags: []string{"v1.2", "v1.0.0", "invalid", "v2.1.0-beta", "v1.3.3"}, + currentTag: "v1.2.3", + expectedTagVal: "v1.3.3", expectedIsSemVer: true, }, { name: "similar tag values", discoveredTags: []string{"17", "17.0", "17.0.0"}, + currentTag: "16", expectedTagVal: "17", expectedIsSemVer: true, }, { name: "similar tag values different arrival order", discoveredTags: []string{"17.0", "17", "17.0.0"}, + currentTag: "16", + expectedTagVal: "17", + expectedIsSemVer: true, + }, + { + name: "similar tag values different current format", + discoveredTags: []string{"17.0", "17", "17.0.0"}, + currentTag: "16.0", expectedTagVal: "17.0", expectedIsSemVer: true, }, @@ -151,10 +172,203 @@ func TestGetLatestTag(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - tagVal, isSemVer := getLatestTag(tc.discoveredTags) + tagVal, isSemVer := getCandidateTag(tc.discoveredTags, tc.currentTag) if tagVal != tc.expectedTagVal || isSemVer != tc.expectedIsSemVer { t.Fatalf("\nwant: (%s, %v) \n got: (%s, %v)\n", tc.expectedTagVal, tc.expectedIsSemVer, tagVal, isSemVer) } }) } } + +func TestAttributesMatch(t *testing.T) { + tests := []struct { + name string + c tagAttributes + n tagAttributes + expected bool + }{ + { + name: "All matching attributes", + c: tagAttributes{ + hasSuffix: true, + hasMajor: true, + hasMinor: true, + hasPatch: true, + original: "v1.2.3-beta", + }, + n: tagAttributes{ + hasSuffix: true, + hasMajor: true, + hasMinor: true, + hasPatch: true, + original: "v1.2.3-beta", + }, + expected: true, + }, + { + name: "Different hasSuffix", + c: tagAttributes{ + hasSuffix: true, + hasMajor: true, + hasMinor: true, + hasPatch: true, + original: "v1.2.3-beta", + }, + n: tagAttributes{ + hasSuffix: false, + hasMajor: true, + hasMinor: true, + hasPatch: true, + original: "v1.2.3", + }, + expected: false, + }, + { + name: "Different hasMajor", + c: tagAttributes{ + hasSuffix: false, + hasMajor: true, + hasMinor: true, + hasPatch: true, + original: "v1.2.3", + }, + n: tagAttributes{ + hasSuffix: false, + hasMajor: false, + hasMinor: true, + hasPatch: true, + original: "v0.2.3", + }, + expected: false, + }, + { + name: "All attributes false", + c: tagAttributes{ + hasSuffix: false, + hasMajor: false, + hasMinor: false, + hasPatch: false, + original: "", + }, + n: tagAttributes{ + hasSuffix: false, + hasMajor: false, + hasMinor: false, + hasPatch: false, + original: "any", + }, + expected: true, + }, + { + name: "Different hasMinor and hasPatch", + c: tagAttributes{ + hasSuffix: false, + hasMajor: true, + hasMinor: true, + hasPatch: false, + original: "v1.2", + }, + n: tagAttributes{ + hasSuffix: false, + hasMajor: true, + hasMinor: false, + hasPatch: true, + original: "v1.0.1", + }, + expected: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + result := tc.c.attributesMatch(tc.n) + if result != tc.expected { + t.Fatalf("\nwant: %t \n got: %t\n", tc.expected, result) + } + }) + } +} + +func TestGetTagAttributes(t *testing.T) { + tests := []struct { + name string + tag string + expected tagAttributes + }{ + { + name: "Full semantic version with suffix", + tag: "v1.2.3-beta", + expected: tagAttributes{ + hasSuffix: true, + hasMajor: true, + hasMinor: true, + hasPatch: true, + original: "v1.2.3-beta", + }, + }, + { + name: "Full semantic version without suffix", + tag: "v1.2.3", + expected: tagAttributes{ + hasSuffix: false, + hasMajor: true, + hasMinor: true, + hasPatch: true, + original: "v1.2.3", + }, + }, + { + name: "Major and minor version without suffix", + tag: "v1.2", + expected: tagAttributes{ + hasSuffix: false, + hasMajor: true, + hasMinor: true, + hasPatch: false, + original: "v1.2", + }, + }, + { + name: "Only major version without suffix", + tag: "v1", + expected: tagAttributes{ + hasSuffix: false, + hasMajor: true, + hasMinor: false, + hasPatch: false, + original: "v1", + }, + }, + { + name: "Empty string", + tag: "", + expected: tagAttributes{ + hasSuffix: false, + hasMajor: false, + hasMinor: false, + hasPatch: false, + original: "", + }, + }, + { + name: "Version with suffix only", + tag: "v1-beta", + expected: tagAttributes{ + hasSuffix: true, + hasMajor: true, + hasMinor: false, + hasPatch: false, + original: "v1-beta", + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + result := getTagAttributes(tc.tag) + if result != tc.expected { + t.Fatalf("\nwant: %v \n got: %v\n", tc.expected, result) + } + }) + } +}