Skip to content

Commit

Permalink
feat: retain tag format on chart upgrade
Browse files Browse the repository at this point in the history
Signed-off-by: Richard Gee <[email protected]>
  • Loading branch information
rgee0 authored and alexellis committed Nov 5, 2024
1 parent 7ca35d9 commit 49a75a2
Show file tree
Hide file tree
Showing 2 changed files with 271 additions and 20 deletions.
69 changes: 53 additions & 16 deletions cmd/chart/upgrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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,
}
}
222 changes: 218 additions & 4 deletions cmd/chart/upgrade_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,57 +104,271 @@ 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,
},
}

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)
}
})
}
}

0 comments on commit 49a75a2

Please sign in to comment.