diff --git a/api/v1alpha1/image_types.go b/api/v1alpha1/image_types.go index 0a13c49..3cc37e5 100644 --- a/api/v1alpha1/image_types.go +++ b/api/v1alpha1/image_types.go @@ -76,7 +76,7 @@ type ( Name string `json:"name"` // +kubebuilder:validation:Required - // +kubebuilder:validation:Enum=calver-major;calver-minor;calver-patch;semver-major;semver-minor;semver-patch;regex;always + // +kubebuilder:validation:Enum=calver-major;calver-minor;calver-patch;calver-modifier;calver-prerelease;semver-major;semver-minor;semver-patch;regex;always Type rules.Name `json:"type"` // +kubebuilder:validation:Optional diff --git a/docs/rules/calver.md b/docs/rules/calver.md index 7c052eb..a2f8a92 100644 --- a/docs/rules/calver.md +++ b/docs/rules/calver.md @@ -8,16 +8,47 @@ hide: The `calver` rule allows you to define a rule that will be executed when the image is updated with a new calver version. It follows the [Calendar Versioning](https://calver.org/) specification. A lot of options are available to match the version you want to update. -The format of the version [following this logic regex](https://regex101.com/r/25eVYJ/2). + +The format of the version [following this logic regex](https://regex101.com/r/25eVYJ/7). +```regex +^([0-9]{4}|[0-9]{2})(\.[0-9]{1,2})?(\.[0-9]{1,2})?(-([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?$ +``` + +Format: +```s +# YYYY.MM.XX +2024 +2024-dev +2024.01 +2024.01-dev +2024.01.01 +2024.01.01-dev + +# YY.MM.XX +24 +24-dev +24.01 +24.01-dev + +# YY.M.X +24.1 +24.1.1 +24.1.1-dev + +2024.01.01-dev.prerelease +``` + * `calver-major`: Update the image with the latest major version. * `calver-minor`: Update the image with the latest minor version. * `calver-patch`: Update the image with the latest patch version. +* `calver-modifier`: Update the image with the latest modifier version.(dev, alpha, beta, rc, etc.) +* `calver-prerelease`: Update the image with the latest prerelease version. **`calver-major`** is the most restrictive and will only update the image when the major version is updated [calver documentation](https://calver.org/). ``` { .yaml .no-copy title="calver rule" } version: 2024.0.0 -Match: >=2024.*.* # (1) +Match: >=2025.*.* # (1) ``` 1. :man_raising_hand: For more information about the calver range, you can check the [calver documentation](https://calver.org/). @@ -25,7 +56,7 @@ Match: >=2024.*.* # (1) **`calver-minor`** is less restrictive and will update the image when the minor version is updated. ``` { .yaml .no-copy title="calver rule" } version: 2024.0.0 -Match: >=2024.1.* <2 # (1) +Match: >=2024.1.* # (1) ``` 1. :man_raising_hand: For more information about the calver range, you can check the [calver documentation](https://calver.org/). @@ -33,11 +64,24 @@ Match: >=2024.1.* <2 # (1) **`calver-patch`** is the least restrictive and will update the image when the patch version is updated. ``` { .yaml .no-copy title="calver rule" } version: 2024.0.0 -Match: >=2024.0.1 <2024.1.0 # (1) +Match: >=2024.0.1 and < 2024.1.0 # (1) ``` 1. :man_raising_hand: For more information about the calver range, you can check the [calver documentation](https://calver.org/). +**`calver-modifier`** is the least restrictive and will update the image when the modifier version is updated. +``` { .yaml .no-copy title="calver rule" } +version: 2024.0.0-dev +Match: >2024.0.0-dev and < 2024.0.1-* # (1) +``` + +**`calver-prerelease`** is the least restrictive and will update the image when the prerelease version is updated. +``` { .yaml .no-copy title="calver rule" } +version: 2024.0.0-dev.1 +Match: >2024.0.0-dev.1 and < 2024.0.0-*.1 # + +2. :man_raising_hand: For more information about the calver range, you can check the [calver documentation](https://calver.org/). + ## Who to use Create an `Image` resource with the `calver` rule. diff --git a/internal/rules/calver.go b/internal/rules/calver.go index 26dbf73..ae4eadd 100644 --- a/internal/rules/calver.go +++ b/internal/rules/calver.go @@ -21,12 +21,24 @@ type ( calverPatch struct { rule } + + // calverModifier - The modifier is an optional part of the version. + calverModifier struct { + rule + } + + // calverPrerelease - The prerelease is an optional part of the version. + calverPrerelease struct { + rule + } ) func init() { register(CalverMajor, &calverMajor{}) register(CalverMinor, &calverMinor{}) register(CalverPatch, &calverPatch{}) + register(CalverModifier, &calverModifier{}) + register(CalverPrerelease, &calverPrerelease{}) } func (c *calverMajor) Evaluate() (matchWithRule bool, newTag string, err error) { @@ -94,3 +106,46 @@ func (c *calverPatch) Evaluate() (matchWithRule bool, newTag string, err error) return false, "", nil } + +func (c *calverModifier) Evaluate() (matchWithRule bool, newTag string, err error) { + actualCV, err := vc.NewCalVerStr(c.actualTag) + if err != nil { + log.WithError(err).WithField("tag", c.actualTag).Error("Error parsing actual tag") + return false, "", err + } + + for _, t := range c.tags { + cv, err := vc.NewCalVerStr(t) + if err != nil { + log.WithError(err).WithField("tag", t).Error("Error parsing tag") + continue + } + if cv.Gt(actualCV) && cv.Patch() == actualCV.Patch() && cv.Minor() == actualCV.Minor() && cv.Major() == actualCV.Major() { + return true, t, nil + } + } + + return false, "", nil +} + +func (c *calverPrerelease) Evaluate() (matchWithRule bool, newTag string, err error) { + actualCV, err := vc.NewCalVerStr(c.actualTag) + if err != nil { + log.WithError(err).WithField("tag", c.actualTag).Error("Error parsing actual tag") + return false, "", err + } + + for _, t := range c.tags { + cv, err := vc.NewCalVerStr(t) + if err != nil { + log.WithError(err).WithField("tag", t).Error("Error parsing tag") + continue + } + + if cv.Prerelease() > actualCV.Prerelease() && cv.Patch() == actualCV.Patch() && cv.Minor() == actualCV.Minor() && cv.Major() == actualCV.Major() { + return true, t, nil + } + } + + return false, "", nil +} diff --git a/internal/rules/calver_test.go b/internal/rules/calver_test.go index 3b95b74..c0686f7 100644 --- a/internal/rules/calver_test.go +++ b/internal/rules/calver_test.go @@ -8,6 +8,25 @@ import ( "github.com/orange-cloudavenue/kube-image-updater/internal/rules" ) +var listTest map[string]string = map[string]string{ + "YYYY": "2024", + "YYYY-dev": "2024-dev", + "YYYY.MM": "2024.01", + "YYYY.MM-dev": "2024.01-dev", + "YYYY.MM.DD": "2024.01.01", + "YYYY.MM.DD-dev": "2024.01.01-dev", + "YYYY.MM.DD-dev.prerelease": "2024.01.01-dev.1", + "YY": "24", + "YY-dev": "24-dev", + "YY.MM": "24.01", + "YY.MM-dev": "24.01-dev", + "YY.M": "24.1", + "YY.M.D": "24.1.1", + "YY.M.D-dev": "24.1.1-dev", + "Invalid": "v2024", + "YYYY.MM.DD.WrongPrerelease": "2024.01.01.1", +} + func TestCalverMajor_Evaluate(t *testing.T) { tests := []struct { name string @@ -18,33 +37,35 @@ func TestCalverMajor_Evaluate(t *testing.T) { expectedError bool }{ { - name: "Valid major version increment", - actualTag: "2024.01.00", - tagsAvailable: []string{"2023.10.00", "2024.01.0", "2025.01.00"}, + // Unitary tests + name: "YYYY", + actualTag: listTest["YYYY"], + tagsAvailable: []string{"2023", "2024", "2025"}, expectedMatch: true, - expectedTag: "2025.01.00", + expectedTag: "2025", expectedError: false, }, { - name: "No matching major version", - actualTag: "2024.01.00", - tagsAvailable: []string{"2023.10.00", "2024.10.01", "2024.01.10"}, - expectedMatch: false, - expectedTag: "", + name: "YY", + actualTag: listTest["YY"], + tagsAvailable: []string{"23", "24", "25"}, + expectedMatch: true, + expectedTag: "25", expectedError: false, }, + // Errors tests { - name: "Invalid actual tag", - actualTag: "invalid", - tagsAvailable: []string{"2023.10.00", "2024.00.01", "2024.01.00"}, + name: "Invalid", + actualTag: listTest["Invalid"], + tagsAvailable: []string{"2023.10.00", "2024.01.0", "2025.01.00"}, expectedMatch: false, expectedTag: "", expectedError: true, }, { - name: "Invalid and no available tag", + name: "Invalid available tag", actualTag: "2024.01.0", - tagsAvailable: []string{"2023.01.0", "invalid"}, + tagsAvailable: []string{"v2023.01.0", "invalid"}, expectedMatch: false, expectedTag: "", expectedError: false, @@ -80,41 +101,42 @@ func TestCalverMinor_Evaluate(t *testing.T) { expectedError bool }{ { - name: "Valid minor version increment", - actualTag: "2024.0.0", - tagsAvailable: []string{"2024.1.0", "2024.0.1"}, + name: "YYYY.MM", + actualTag: listTest["YYYY.MM"], + tagsAvailable: []string{"2023.01", "2025.01", "2024.02"}, expectedMatch: true, - expectedTag: "2024.1.0", + expectedTag: "2024.02", expectedError: false, }, { - name: "No matching minor version", - actualTag: "2024.0.0", - tagsAvailable: []string{"2024.0.1", "2024.0.2"}, - expectedMatch: false, - expectedTag: "", + name: "YY.MM", + actualTag: listTest["YY.MM"], + tagsAvailable: []string{"23.01", "25.01", "24.02"}, + expectedMatch: true, + expectedTag: "24.02", + expectedError: false, + }, + { + name: "YY.M", + actualTag: listTest["YY.M"], + tagsAvailable: []string{"23.1", "25.1", "24.2"}, + expectedMatch: true, + expectedTag: "24.2", expectedError: false, }, + // Errors tests { - name: "Invalid actual tag", - actualTag: "invalid", - tagsAvailable: []string{"2024.1.0", "2024.0.1"}, + name: "Invalid", + actualTag: listTest["Invalid"], + tagsAvailable: []string{"2023.10.00", "2024.01.0", "2025.01.00"}, expectedMatch: false, expectedTag: "", expectedError: true, }, { name: "Invalid available tag", - actualTag: "2024.0.0", - tagsAvailable: []string{"invalid", "2024.1.0"}, - expectedMatch: true, - expectedTag: "2024.1.0", - expectedError: false, - }, - { - name: "only major version increment", - actualTag: "2024.0.0", - tagsAvailable: []string{"2025.0.0", "2024.0.0"}, + actualTag: "2024.01.0", + tagsAvailable: []string{"v2023.01.0", "invalid"}, expectedMatch: false, expectedTag: "", expectedError: false, @@ -149,42 +171,196 @@ func TestCalverPatch_Evaluate(t *testing.T) { expectedTag string expectedError bool }{ + // Unitary tests + { + name: "YYYY.MM.DD", + actualTag: listTest["YYYY.MM.DD"], + tagsAvailable: []string{"2023.01.01", "2025.02.01", "2024.01.02"}, + expectedMatch: true, + expectedTag: "2024.01.02", + expectedError: false, + }, { - name: "Valid patch version increment", - actualTag: "2024.0.0", - tagsAvailable: []string{"2024.0.1", "2024.0.2"}, + name: "YY.M.D", + actualTag: listTest["YY.M.D"], + tagsAvailable: []string{"23.1.1", "25.2.1", "24.1.2"}, expectedMatch: true, - expectedTag: "2024.0.2", + expectedTag: "24.1.2", expectedError: false, }, + // Errors tests { - name: "No matching patch version", - actualTag: "2024.0.0", - tagsAvailable: []string{"2024.1.0", "2024.2.0"}, + name: "Invalid", + actualTag: listTest["Invalid"], + tagsAvailable: []string{"2023.10.00", "2024.01.0", "2025.01.00"}, + expectedMatch: false, + expectedTag: "", + expectedError: true, + }, + { + name: "Invalid available tag", + actualTag: "2024.01.0", + tagsAvailable: []string{"v2023.01.0", "invalid"}, expectedMatch: false, expectedTag: "", expectedError: false, }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r, err := rules.GetRule(rules.CalverPatch) + assert.NoError(t, err) + r.Init(tt.actualTag, tt.tagsAvailable, "") + match, newTag, err := r.Evaluate() + + if tt.expectedError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + assert.Equal(t, tt.expectedMatch, match) + assert.Equal(t, tt.expectedTag, newTag) + }) + } +} + +func TestCalverModifier_Evaluate(t *testing.T) { + tests := []struct { + name string + actualTag string + tagsAvailable []string + expectedMatch bool + expectedTag string + expectedError bool + }{ + // Unitary tests + { + name: "YYYY-dev", + actualTag: listTest["YYYY-dev"], + tagsAvailable: []string{"2024-aaa", "2024-bbb", "2024-fff"}, + expectedMatch: true, + expectedTag: "2024-fff", + expectedError: false, + }, { - name: "Invalid actual tag", - actualTag: "invalid", - tagsAvailable: []string{"2024.0.1", "2024.0.2"}, + name: "YYYY.MM-dev", + actualTag: listTest["YYYY.MM-dev"], + tagsAvailable: []string{"2024.01-aaa", "2024.01-bbb", "2024.01-fff"}, + expectedMatch: true, + expectedTag: "2024.01-fff", + expectedError: false, + }, + { + name: "YYYY.MM.DD-dev", + actualTag: listTest["YYYY.MM.DD-dev"], + tagsAvailable: []string{"2024.01.01-aaa", "2024.01.01-dev", "2024.01.01-fff"}, + expectedMatch: true, + expectedTag: "2024.01.01-fff", + expectedError: false, + }, + { + name: "YY-dev", + actualTag: listTest["YY-dev"], + tagsAvailable: []string{"24-aaa", "24-dev", "24-fff"}, + expectedMatch: true, + expectedTag: "24-fff", + expectedError: false, + }, + { + name: "YY.MM-dev", + actualTag: listTest["YY.MM-dev"], + tagsAvailable: []string{"24.01-aaa", "24.01-dev", "24.01-fff"}, + expectedMatch: true, + expectedTag: "24.01-fff", + expectedError: false, + }, + { + name: "YY.M.D-dev", + actualTag: listTest["YY.M.D-dev"], + tagsAvailable: []string{"24.1.1-aaa", "24.1.1-dev", "24.1.1-fff"}, + expectedMatch: true, + expectedTag: "24.1.1-fff", + expectedError: false, + }, + // Errors tests + { + name: "Invalid", + actualTag: listTest["Invalid"], + tagsAvailable: []string{"2023.10.00-dev", "24.01.0-dev", "2025.1.0-dev"}, expectedMatch: false, expectedTag: "", expectedError: true, }, { name: "Invalid available tag", - actualTag: "2024.0.0", - tagsAvailable: []string{"invalid", "2024.0.1"}, + actualTag: "2024.01.0", + tagsAvailable: []string{"v2023.01.0", "invalid"}, + expectedMatch: false, + expectedTag: "", + expectedError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r, err := rules.GetRule(rules.CalverModifier) + assert.NoError(t, err) + r.Init(tt.actualTag, tt.tagsAvailable, "") + match, newTag, err := r.Evaluate() + + if tt.expectedError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + assert.Equal(t, tt.expectedMatch, match) + assert.Equal(t, tt.expectedTag, newTag) + }) + } +} + +func TestCalverPrerelease_Evaluate(t *testing.T) { + tests := []struct { + name string + actualTag string + tagsAvailable []string + expectedMatch bool + expectedTag string + expectedError bool + }{ + // Unitary tests + { + name: "YYYY.MM.DD-dev.prerelease", + actualTag: listTest["YYYY.MM.DD-dev.prerelease"], + tagsAvailable: []string{"2023.01.01-dev.2", "2024.01.01-dev.1", "2024.01.01-dev.2"}, expectedMatch: true, - expectedTag: "2024.0.1", + expectedTag: "2024.01.01-dev.2", expectedError: false, }, + // Errors tests + { + name: "YYYY.MM.DD.WrongPrerelease", + actualTag: listTest["YYYY.MM.DD.WrongPrerelease"], + tagsAvailable: []string{"2024.01.01-aaa", "2024.01.01-dev", "2024.01.01.2"}, + expectedMatch: false, + expectedTag: "", + expectedError: true, + }, + { + name: "Invalid", + actualTag: listTest["Invalid"], + tagsAvailable: []string{"2023.10.00-dev", "24.01.0-dev", "2025.1.0-dev.2"}, + expectedMatch: false, + expectedTag: "", + expectedError: true, + }, { - name: "only minor version increment", - actualTag: "2024.0.0", - tagsAvailable: []string{"2024.1.0", "2024.0.0"}, + name: "Invalid available tag", + actualTag: "2024.01.0", + tagsAvailable: []string{"v2023.01.0.aaa", "invalid.prelease"}, expectedMatch: false, expectedTag: "", expectedError: false, @@ -193,7 +369,7 @@ func TestCalverPatch_Evaluate(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - r, err := rules.GetRule(rules.SemverPatch) + r, err := rules.GetRule(rules.CalverPrerelease) assert.NoError(t, err) r.Init(tt.actualTag, tt.tagsAvailable, "") match, newTag, err := r.Evaluate() diff --git a/internal/rules/rules.go b/internal/rules/rules.go index 47c3b0a..d1c05b4 100644 --- a/internal/rules/rules.go +++ b/internal/rules/rules.go @@ -23,14 +23,16 @@ type ( var rules = make(Rules) const ( - SemverMajor Name = "semver-major" - SemverMinor Name = "semver-minor" - SemverPatch Name = "semver-patch" - CalverMajor Name = "calver-major" - CalverMinor Name = "calver-minor" - CalverPatch Name = "calver-patch" - Regex Name = "regex" - Always Name = "always" + SemverMajor Name = "semver-major" + SemverMinor Name = "semver-minor" + SemverPatch Name = "semver-patch" + CalverMajor Name = "calver-major" + CalverMinor Name = "calver-minor" + CalverPatch Name = "calver-patch" + CalverModifier Name = "calver-modifier" + CalverPrerelease Name = "calver-prerelease" + Regex Name = "regex" + Always Name = "always" ) func register(name Name, rule RuleInterface) { diff --git a/manifests/crd/kimup.cloudavenue.io_images.yaml b/manifests/crd/kimup.cloudavenue.io_images.yaml index 12355ea..de2500a 100644 --- a/manifests/crd/kimup.cloudavenue.io_images.yaml +++ b/manifests/crd/kimup.cloudavenue.io_images.yaml @@ -180,6 +180,11 @@ spec: type: string type: enum: + - calver-major + - calver-minor + - calver-patch + - calver-modifier + - calver-prerelease - semver-major - semver-minor - semver-patch