Skip to content

Commit 8b3e0ba

Browse files
authored
Merge pull request #1298 from apedriza/fix-cc-version-formats
Fix: support accepted formats for cluster version when using clsuterc…
2 parents fcc82d2 + ce43e9f commit 8b3e0ba

File tree

8 files changed

+143
-84
lines changed

8 files changed

+143
-84
lines changed

api/k0smotron.io/v1beta1/k0smotroncluster_types.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@ package v1beta1
1919
import (
2020
"crypto/md5"
2121
"fmt"
22+
"strings"
23+
2224
"github.com/k0sproject/version"
2325
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
24-
"strings"
2526

2627
v1 "k8s.io/api/core/v1"
2728
"k8s.io/apimachinery/pkg/api/resource"
@@ -186,6 +187,9 @@ func (c *ClusterSpec) GetImage() string {
186187
k0sVersion = DefaultK0SVersion
187188
}
188189

190+
// Ensure any version with +k0s. is converted to -k0s. for image tagging
191+
k0sVersion = strings.Replace(k0sVersion, "+k0s.", "-k0s.", 1)
192+
189193
if !strings.Contains(k0sVersion, "-k0s.") {
190194
k0sVersion = fmt.Sprintf("%s-%s", k0sVersion, DefaultK0SSuffix)
191195
}

api/k0smotron.io/v1beta1/k0smotroncluster_types_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,14 @@ func TestClusterSpec_GetImage(t *testing.T) {
6363
},
6464
want: "foobar/k0s:v1.29.4-k0s.0",
6565
},
66+
{
67+
name: "Image and version given with +k0s. suffix",
68+
spec: &ClusterSpec{
69+
Image: "foobar/k0s",
70+
Version: "v1.29.4+k0s.0",
71+
},
72+
want: "foobar/k0s:v1.29.4-k0s.0",
73+
},
6674
}
6775
for _, tt := range tests {
6876
t.Run(tt.name, func(t *testing.T) {

internal/controller/bootstrap/worker_bootstrap_controller.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,9 +130,15 @@ func (r *Controller) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.
130130
if config.Spec.Version == "" && machine.Spec.Version != nil {
131131
config.Spec.Version = *machine.Spec.Version
132132
}
133+
133134
// If the version does not contain the k0s suffix, append it.
134-
if config.Spec.Version != "" && !strings.Contains(config.Spec.Version, "+k0s.") {
135-
config.Spec.Version = fmt.Sprintf("%s+%s", config.Spec.Version, defaultK0sSuffix)
135+
if config.Spec.Version != "" {
136+
// When machine is created by CAPI, for example by using a clusterclass template, the version
137+
// of the cluster may contain '-k0s.' instead of '+k0s.', so we need to replace it first.
138+
config.Spec.Version = strings.Replace(config.Spec.Version, "-k0s.", "+k0s.", 1)
139+
if !strings.Contains(config.Spec.Version, "+k0s.") {
140+
config.Spec.Version = fmt.Sprintf("%s+%s", config.Spec.Version, defaultK0sSuffix)
141+
}
136142
}
137143

138144
// Lookup the cluster the config owner is associated with

internal/controller/controlplane/k0smotron_controlplane_controller.go

Lines changed: 39 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -416,23 +416,6 @@ func ensureCertificates(ctx context.Context, cluster *clusterv1.Cluster, kcp *cp
416416
return certificates.LookupOrGenerateCached(ctx, scope.secretCachingClient, scope.client, capiutil.ObjectKey(cluster), owner)
417417
}
418418

419-
// FormatStatusVersion formats the status version to match the spec version format.
420-
// If spec.version doesn't contain "-k0s." suffix, it removes the suffix from status.version as well.
421-
func FormatStatusVersion(specVersion, statusVersion string) string {
422-
specHasK0sSuffix := strings.Contains(specVersion, "-k0s.")
423-
424-
// Adjust status.version to match the format of spec.version
425-
if !specHasK0sSuffix && strings.Contains(statusVersion, "-k0s.") {
426-
// If spec.version doesn't have the -k0s. suffix, remove it from status.version as well
427-
parts := strings.Split(statusVersion, "-k0s.")
428-
if len(parts) > 0 {
429-
return parts[0]
430-
}
431-
}
432-
433-
return statusVersion
434-
}
435-
436419
func (c *K0smotronController) computeStatus(ctx context.Context, cluster *clusterv1.Cluster, kcp *cpv1beta1.K0smotronControlPlane, scope *kmcScope) error {
437420
var kmc kapi.Cluster
438421
err := c.Client.Get(ctx, types.NamespacedName{Name: cluster.Name, Namespace: cluster.Namespace}, &kmc)
@@ -462,8 +445,9 @@ func (c *K0smotronController) computeStatus(ctx context.Context, cluster *cluste
462445
var updatedReplicas, readyReplicas, unavailableReplicas int
463446

464447
desiredVersionStr := kcp.Spec.Version
465-
if !strings.Contains(desiredVersionStr, "-") {
466-
desiredVersionStr = fmt.Sprintf("%s-%s", desiredVersionStr, kapi.DefaultK0SSuffix)
448+
if !strings.Contains(desiredVersionStr, "-k0s.") && !strings.Contains(desiredVersionStr, "+k0s.") {
449+
// Use default k0s suffix format when spec version does not contain either -k0s. or +k0s.
450+
desiredVersionStr = fmt.Sprintf("%s+%s", desiredVersionStr, kapi.DefaultK0SSuffix)
467451
}
468452
desiredVersion, err := version.NewVersion(desiredVersionStr)
469453
if err != nil {
@@ -488,7 +472,12 @@ func (c *K0smotronController) computeStatus(ctx context.Context, cluster *cluste
488472
continue
489473
}
490474

491-
currentVersion, err := scope.getComparableK0sVersionRunningInPod(ctx, &pod)
475+
currentVersion, err := scope.getK0sVersionRunningInPod(ctx, &pod)
476+
if err != nil {
477+
return err
478+
}
479+
// Align version format in spec to the current version format for comparison. DO NOT modify version format in spec.
480+
currentVersion, err = alignToSpecVersionFormat(desiredVersion, currentVersion)
492481
if err != nil {
493482
return err
494483
}
@@ -507,9 +496,7 @@ func (c *K0smotronController) computeStatus(ctx context.Context, cluster *cluste
507496
kcp.Status.UnavailableReplicas = int32(unavailableReplicas)
508497

509498
if kcp.Status.ReadyReplicas > 0 {
510-
statusVersion := minimumVersion.String()
511-
// Store the formatted version for Cluster API compatibility
512-
kcp.Status.Version = FormatStatusVersion(kcp.Spec.Version, statusVersion)
499+
kcp.Status.Version = minimumVersion.String()
513500
}
514501

515502
c.computeAvailability(ctx, cluster, kcp)
@@ -519,14 +506,41 @@ func (c *K0smotronController) computeStatus(ctx context.Context, cluster *cluste
519506
// Additionally, if the ControlPlaneReadyCondition is false (e.g., due to DNS resolution failures),
520507
// we should also requeue to retry the connection.
521508
if kcp.Status.UnavailableReplicas > 0 ||
522-
FormatStatusVersion(kcp.Spec.Version, desiredVersion.String()) != kcp.Status.Version ||
509+
desiredVersion.String() != kcp.Status.Version ||
523510
!conditions.IsTrue(kcp, cpv1beta1.ControlPlaneReadyCondition) {
524511
return ErrNotReady
525512
}
526513

527514
return nil
528515
}
529516

517+
// alignToSpecVersionFormat ensures that the currentVersion format matches the desiredVersion format.
518+
func alignToSpecVersionFormat(specVersion, currentVersion *version.Version) (*version.Version, error) {
519+
specVersionUsesDefaultK0SSuffix := strings.Contains(specVersion.String(), "+k0s.")
520+
currentVersionUsesDefaultK0SSuffix := strings.Contains(currentVersion.String(), "+k0s.")
521+
522+
isFormatAligned := (specVersionUsesDefaultK0SSuffix && currentVersionUsesDefaultK0SSuffix) ||
523+
(!specVersionUsesDefaultK0SSuffix && !currentVersionUsesDefaultK0SSuffix)
524+
525+
if isFormatAligned {
526+
return currentVersion, nil
527+
}
528+
529+
currentVersionStr := currentVersion.String()
530+
if !specVersionUsesDefaultK0SSuffix {
531+
// Spec version format is like "vX.Y.Z-k0s.0".
532+
// Current version format is like "vX.Y.Z+k0s.0".
533+
// Convert currentVersion to match it.
534+
currentVersionAlignedStr := strings.Replace(currentVersionStr, "+k0s.", "-k0s.", 1)
535+
return version.NewVersion(currentVersionAlignedStr)
536+
}
537+
// Spec version format is like "vX.Y.Z+k0s.0".
538+
// Current version format is like "vX.Y.Z-k0s.0".
539+
// Convert currentVersion to match it.
540+
currentVersionAlignedStr := strings.Replace(currentVersionStr, "-k0s.", "+k0s.", 1)
541+
return version.NewVersion(currentVersionAlignedStr)
542+
}
543+
530544
// computeAvailability checks if the control plane is ready by connecting to the API server
531545
// and checking if the control plane is initialized
532546
func (c *K0smotronController) computeAvailability(ctx context.Context, cluster *clusterv1.Cluster, kcp *cpv1beta1.K0smotronControlPlane) {
@@ -574,16 +588,12 @@ func (c *K0smotronController) computeAvailability(ctx context.Context, cluster *
574588
})
575589
}
576590

577-
func (scope *kmcScope) getComparableK0sVersionRunningInPod(ctx context.Context, pod *corev1.Pod) (*version.Version, error) {
591+
func (scope *kmcScope) getK0sVersionRunningInPod(ctx context.Context, pod *corev1.Pod) (*version.Version, error) {
578592
currentVersionOutput, err := exec.PodExecCmdOutput(ctx, scope.clientSet, scope.restConfig, pod.GetName(), pod.GetNamespace(), "k0s version")
579593
if err != nil {
580594
return nil, err
581595
}
582596
currentVersionStr, _ := strings.CutSuffix(currentVersionOutput, "\n")
583-
// In order to compare the version reported by the 'k0s version' command executed in the pod running
584-
// the controlplane with the version declared in K0smotronControlPlane.spec this transformation is
585-
// necessary to match their format.
586-
currentVersionStr = strings.Replace(currentVersionStr, "+", "-", 1)
587597
return version.NewVersion(currentVersionStr)
588598
}
589599

internal/controller/controlplane/k0smotron_controlplane_controller_test.go

Lines changed: 36 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -4,59 +4,11 @@ import (
44
"testing"
55

66
kapi "github.com/k0sproject/k0smotron/api/k0smotron.io/v1beta1"
7-
"github.com/stretchr/testify/assert"
7+
"github.com/k0sproject/version"
88
"github.com/stretchr/testify/require"
99
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
1010
)
1111

12-
// TestFormatStatusVersion tests the formatStatusVersion function
13-
func TestFormatStatusVersion(t *testing.T) {
14-
tests := []struct {
15-
name string
16-
specVersion string
17-
statusVersion string
18-
expectedStatus string
19-
description string
20-
}{
21-
{
22-
name: "spec without k0s suffix",
23-
specVersion: "v1.33.1",
24-
statusVersion: "v1.33.1-k0s.0",
25-
expectedStatus: "v1.33.1",
26-
description: "When spec.version doesn't have -k0s suffix, status.version should also not have it",
27-
},
28-
{
29-
name: "spec with k0s suffix",
30-
specVersion: "v1.33.1-k0s.0",
31-
statusVersion: "v1.33.1-k0s.0",
32-
expectedStatus: "v1.33.1-k0s.0",
33-
description: "When spec.version has -k0s suffix, status.version should keep it",
34-
},
35-
{
36-
name: "spec with custom k0s suffix",
37-
specVersion: "v1.33.1-k0s.1",
38-
statusVersion: "v1.33.1-k0s.1",
39-
expectedStatus: "v1.33.1-k0s.1",
40-
description: "When spec.version has custom -k0s suffix, status.version should keep it",
41-
},
42-
{
43-
name: "spec without suffix, status with different suffix",
44-
specVersion: "v1.33.1",
45-
statusVersion: "v1.33.1-k0s.1",
46-
expectedStatus: "v1.33.1",
47-
description: "When spec.version doesn't have -k0s suffix, status.version should remove it even if it's different",
48-
},
49-
}
50-
51-
for _, tt := range tests {
52-
t.Run(tt.name, func(t *testing.T) {
53-
// Use the actual FormatStatusVersion function
54-
result := FormatStatusVersion(tt.specVersion, tt.statusVersion)
55-
assert.Equal(t, tt.expectedStatus, result, tt.description)
56-
})
57-
}
58-
}
59-
6012
func TestIsClusterSpecSynced(t *testing.T) {
6113
testCases := []struct {
6214
name string
@@ -247,3 +199,38 @@ func TestIsClusterSpecSynced(t *testing.T) {
247199
})
248200
}
249201
}
202+
203+
func Test_alignToSpecVersionFormat(t *testing.T) {
204+
tests := []struct {
205+
name string
206+
specVersion *version.Version
207+
currentVersion *version.Version
208+
want *version.Version
209+
}{
210+
{
211+
name: "both versions have same format",
212+
specVersion: version.MustParse("v1.33.1-k0s.0"),
213+
currentVersion: version.MustParse("v1.33.1-k0s.1"),
214+
want: version.MustParse("v1.33.1-k0s.1"),
215+
},
216+
{
217+
name: "versions does not have same format: spec with +k0s, current with -k0s",
218+
specVersion: version.MustParse("v1.33.1+k0s.0"),
219+
currentVersion: version.MustParse("v1.33.1-k0s.1"),
220+
want: version.MustParse("v1.33.1+k0s.1"),
221+
},
222+
{
223+
name: "versions does not have same format: spec with -k0s, current with +k0s",
224+
specVersion: version.MustParse("v1.33.1-k0s.0"),
225+
currentVersion: version.MustParse("v1.33.1+k0s.1"),
226+
want: version.MustParse("v1.33.1-k0s.1"),
227+
},
228+
}
229+
for _, tt := range tests {
230+
t.Run(tt.name, func(t *testing.T) {
231+
got, err := alignToSpecVersionFormat(tt.specVersion, tt.currentVersion)
232+
require.NoError(t, err)
233+
require.True(t, tt.want.Equal(got), "alignToSpecVersionFormat() = %v, want %v", got, tt.want)
234+
})
235+
}
236+
}

internal/controller/k0smotron.io/k0smotroncluster_webhook.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,9 @@ func (c ClusterValidator) ValidateClusterSpec(kcs *km.ClusterSpec) (warnings adm
7676
// validateVersionSuffix checks if the version has a k0s suffix and returns a warning if it doesn't
7777
func (c ClusterValidator) validateVersionSuffix(version string) admission.Warnings {
7878
warnings := admission.Warnings{}
79-
if version != "" && !strings.Contains(version, "-k0s.") {
80-
warnings = append(warnings, fmt.Sprintf("The specified version '%s' requires a k0s suffix (-k0s.<number>). Using '%s-k0s.0' instead.", version, version))
79+
// When using CAPI clusterclass version can be specified with a +k0s. suffix.
80+
if version != "" && (!strings.Contains(version, "-k0s.") && !strings.Contains(version, "+k0s.")) {
81+
warnings = append(warnings, fmt.Sprintf("The specified version '%s' requires a k0s suffix (k0s.<number>). Using '%s-k0s.0' instead.", version, version))
8182
}
8283

8384
return warnings
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package k0smotronio
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
8+
)
9+
10+
func TestClusterValidator_validateVersionSuffix(t *testing.T) {
11+
tests := []struct {
12+
name string
13+
version string
14+
want admission.Warnings
15+
}{
16+
{
17+
name: "version without k0s suffix",
18+
version: "v1.23.4",
19+
want: admission.Warnings{"The specified version 'v1.23.4' requires a k0s suffix (k0s.<number>). Using 'v1.23.4-k0s.0' instead."},
20+
},
21+
{
22+
name: "version with k0s suffix",
23+
version: "v1.23.4-k0s.2",
24+
want: admission.Warnings{},
25+
},
26+
{
27+
name: "empty version",
28+
version: "",
29+
want: admission.Warnings{},
30+
},
31+
{
32+
name: "version with +k0s. suffix",
33+
version: "v1.23.4+k0s.2",
34+
want: admission.Warnings{},
35+
},
36+
}
37+
for _, tt := range tests {
38+
t.Run(tt.name, func(t *testing.T) {
39+
var c ClusterValidator
40+
require.Equal(t, tt.want, c.validateVersionSuffix(tt.version))
41+
})
42+
}
43+
}

inttest/capi-docker-clusterclass-k0smotron/capi_docker_clusterclass_k0smotron_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ func (s *CAPIDockerClusterClassK0smotronSuite) TestCAPIDocker() {
113113
kcp.Status.UnavailableReplicas == 0 &&
114114
kcp.Status.Ready &&
115115
kcp.Status.UpdatedReplicas == 2 &&
116-
kcp.Status.Version == "v1.27.2"
116+
kcp.Status.Version == "v1.27.2+k0s.0"
117117

118118
return ready, nil
119119
})

0 commit comments

Comments
 (0)