From 75909d4ef28843fbc57463bd94e6787b99c02e7d Mon Sep 17 00:00:00 2001 From: Changelog Bot Date: Wed, 23 Oct 2024 12:58:43 +0000 Subject: [PATCH] chore: Update CHANGELOG.md Signed-off-by: github-actions[bot] --- .changelog/58.txt | 3 + api/v1alpha1/image_types.go | 2 +- .../bases/kimup.cloudavenue.io_images.yaml | 3 + docs/rules/calver.md | 71 ++++++ go.mod | 1 + go.sum | 2 + internal/rules/calver.go | 96 ++++++++ internal/rules/calver_test.go | 211 ++++++++++++++++++ internal/rules/rules.go | 3 + mkdocs.yml | 3 +- 10 files changed, 393 insertions(+), 2 deletions(-) create mode 100644 .changelog/58.txt create mode 100644 docs/rules/calver.md create mode 100644 internal/rules/calver.go create mode 100644 internal/rules/calver_test.go diff --git a/.changelog/58.txt b/.changelog/58.txt new file mode 100644 index 0000000..cd56c81 --- /dev/null +++ b/.changelog/58.txt @@ -0,0 +1,3 @@ +```release-note:feature +`feat` - Add calver Rule Semantic. +``` \ No newline at end of file diff --git a/api/v1alpha1/image_types.go b/api/v1alpha1/image_types.go index 32f6bd4..0a13c49 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=semver-major;semver-minor;semver-patch;regex;always + // +kubebuilder:validation:Enum=calver-major;calver-minor;calver-patch;semver-major;semver-minor;semver-patch;regex;always Type rules.Name `json:"type"` // +kubebuilder:validation:Optional diff --git a/config/crd/bases/kimup.cloudavenue.io_images.yaml b/config/crd/bases/kimup.cloudavenue.io_images.yaml index 12355ea..9d3842a 100644 --- a/config/crd/bases/kimup.cloudavenue.io_images.yaml +++ b/config/crd/bases/kimup.cloudavenue.io_images.yaml @@ -180,6 +180,9 @@ spec: type: string type: enum: + - calver-major + - calver-minor + - calver-patch - semver-major - semver-minor - semver-patch diff --git a/docs/rules/calver.md b/docs/rules/calver.md new file mode 100644 index 0000000..c3a9207 --- /dev/null +++ b/docs/rules/calver.md @@ -0,0 +1,71 @@ +--- +hide: + - toc +--- + +# Semantic Versioning (calver) + +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 [Semantic Versioning](https://calver.org/) specification. +A lot of options are available to match the version you want to update. + +* `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-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) +``` + +1. :man_raising_hand: For more information about the calver range, you can check the [calver documentation](https://calver.org/). + +**`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) +``` + +1. :man_raising_hand: For more information about the calver range, you can check the [calver documentation](https://calver.org/). + +**`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) +``` + +1. :man_raising_hand: For more information about the calver range, you can check the [calver documentation](https://calver.org/#spec-item-6). + +## Who to use + +Create an `Image` resource with the `calver` rule. + +```yaml hl_lines="15 16 17 20 21 22 24 25 26" +apiVersion: kimup.cloudavenue.io/v1alpha1 +kind: Image +metadata: + labels: + app.kubernetes.io/name: kube-image-updater + app.kubernetes.io/managed-by: kustomize + name: image-sample-with-auth +spec: + image: registry.127.0.0.1.nip.io/demo + baseTag: v2024.0.4 + triggers: + - [...] + rules: + - name: Notify when calver major is detected + type: calver-major + actions: + - type: alert-xxx + [...] + - name: Automatic update calver minor + type: calver-minor + actions: + - type: apply + - name: Automatic update calver patch + type: calver-patch + actions: + - type: apply +``` diff --git a/go.mod b/go.mod index 163547d..4e98595 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( github.com/ory/dockertest/v3 v3.11.0 github.com/prometheus/client_golang v1.20.5 github.com/reugn/go-quartz v0.13.0 + github.com/shipengqi/vc v0.2.0 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.9.0 github.com/thanhpk/randstr v1.0.6 diff --git a/go.sum b/go.sum index 9321457..4e03bf5 100644 --- a/go.sum +++ b/go.sum @@ -195,6 +195,8 @@ github.com/reugn/go-quartz v0.13.0 h1:0eMxvj28Qu1npIDdN9Mzg9hwyksGH6XJt4Cz0QB8EU github.com/reugn/go-quartz v0.13.0/go.mod h1:0ghKksELp8MJ4h84T203aTHRF3Kug5BrxEW3ErBvhzY= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/shipengqi/vc v0.2.0 h1:o12S/csSz9siuTU2EmpzKnaHa7LuYxDA5zdjjSEv6F4= +github.com/shipengqi/vc v0.2.0/go.mod h1:OzfQDNheQAkQm5BZ2ZTiRjKhirVn4yaGUFGsN8C4IPY= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= diff --git a/internal/rules/calver.go b/internal/rules/calver.go new file mode 100644 index 0000000..26dbf73 --- /dev/null +++ b/internal/rules/calver.go @@ -0,0 +1,96 @@ +package rules + +import ( + "github.com/shipengqi/vc" + + "github.com/orange-cloudavenue/kube-image-updater/internal/log" +) + +type ( + // calverMajor - The first number in the version. + calverMajor struct { + rule + } + + // calverMinor - The second number in the version. + calverMinor struct { + rule + } + + // calverPatch - The third and usually final number in the version. Sometimes referred to as the "micro" segment. + calverPatch struct { + rule + } +) + +func init() { + register(CalverMajor, &calverMajor{}) + register(CalverMinor, &calverMinor{}) + register(CalverPatch, &calverPatch{}) +} + +func (c *calverMajor) 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.Major() > actualCV.Major() { + return true, t, nil + } + } + + return false, "", nil +} + +func (c *calverMinor) 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.Minor() > actualCV.Minor() && cv.Major() == actualCV.Major() { + return true, t, nil + } + } + + return false, "", nil +} + +func (c *calverPatch) 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.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 new file mode 100644 index 0000000..3b95b74 --- /dev/null +++ b/internal/rules/calver_test.go @@ -0,0 +1,211 @@ +package rules_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/orange-cloudavenue/kube-image-updater/internal/rules" +) + +func TestCalverMajor_Evaluate(t *testing.T) { + tests := []struct { + name string + actualTag string + tagsAvailable []string + expectedMatch bool + expectedTag string + expectedError bool + }{ + { + name: "Valid major version increment", + actualTag: "2024.01.00", + tagsAvailable: []string{"2023.10.00", "2024.01.0", "2025.01.00"}, + expectedMatch: true, + expectedTag: "2025.01.00", + 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: "", + expectedError: false, + }, + { + name: "Invalid actual tag", + actualTag: "invalid", + tagsAvailable: []string{"2023.10.00", "2024.00.01", "2024.01.00"}, + expectedMatch: false, + expectedTag: "", + expectedError: true, + }, + { + name: "Invalid and no available tag", + actualTag: "2024.01.0", + tagsAvailable: []string{"2023.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.CalverMajor) + 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 TestCalverMinor_Evaluate(t *testing.T) { + tests := []struct { + name string + actualTag string + tagsAvailable []string + expectedMatch bool + expectedTag string + expectedError bool + }{ + { + name: "Valid minor version increment", + actualTag: "2024.0.0", + tagsAvailable: []string{"2024.1.0", "2024.0.1"}, + expectedMatch: true, + expectedTag: "2024.1.0", + expectedError: false, + }, + { + name: "No matching minor version", + actualTag: "2024.0.0", + tagsAvailable: []string{"2024.0.1", "2024.0.2"}, + expectedMatch: false, + expectedTag: "", + expectedError: false, + }, + { + name: "Invalid actual tag", + actualTag: "invalid", + tagsAvailable: []string{"2024.1.0", "2024.0.1"}, + 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"}, + expectedMatch: false, + expectedTag: "", + expectedError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r, err := rules.GetRule(rules.CalverMinor) + 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 TestCalverPatch_Evaluate(t *testing.T) { + tests := []struct { + name string + actualTag string + tagsAvailable []string + expectedMatch bool + expectedTag string + expectedError bool + }{ + { + name: "Valid patch version increment", + actualTag: "2024.0.0", + tagsAvailable: []string{"2024.0.1", "2024.0.2"}, + expectedMatch: true, + expectedTag: "2024.0.2", + expectedError: false, + }, + { + name: "No matching patch version", + actualTag: "2024.0.0", + tagsAvailable: []string{"2024.1.0", "2024.2.0"}, + expectedMatch: false, + expectedTag: "", + expectedError: false, + }, + { + name: "Invalid actual tag", + actualTag: "invalid", + tagsAvailable: []string{"2024.0.1", "2024.0.2"}, + expectedMatch: false, + expectedTag: "", + expectedError: true, + }, + { + name: "Invalid available tag", + actualTag: "2024.0.0", + tagsAvailable: []string{"invalid", "2024.0.1"}, + expectedMatch: true, + expectedTag: "2024.0.1", + expectedError: false, + }, + { + name: "only minor version increment", + actualTag: "2024.0.0", + tagsAvailable: []string{"2024.1.0", "2024.0.0"}, + expectedMatch: false, + expectedTag: "", + expectedError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r, err := rules.GetRule(rules.SemverPatch) + 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) + }) + } +} diff --git a/internal/rules/rules.go b/internal/rules/rules.go index 3db916a..47c3b0a 100644 --- a/internal/rules/rules.go +++ b/internal/rules/rules.go @@ -26,6 +26,9 @@ 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" ) diff --git a/mkdocs.yml b/mkdocs.yml index d05e5b0..110f3be 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -18,8 +18,9 @@ nav: - Crontab: triggers/crontab.md - Rules: - Always: rules/always.md - - Semantic Versioning: rules/semver.md + - Calver Versioning: rules/calver.md - Regex: rules/regex.md + - Semantic Versioning: rules/semver.md - Actions: - Apply: actions/apply.md - Alerts: