Skip to content

Commit f332c9e

Browse files
committed
Unify k0s version format
Signed-off-by: apedriza <[email protected]>
1 parent f1c73fc commit f332c9e

File tree

10 files changed

+188
-64
lines changed

10 files changed

+188
-64
lines changed

api/k0smotron.io/v1beta1/k0smotroncluster_types.go

Lines changed: 21 additions & 13 deletions
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"
@@ -174,27 +175,34 @@ type Mount struct {
174175
}
175176

176177
const (
177-
defaultK0SImage = "quay.io/k0sproject/k0s"
178-
DefaultK0SVersion = "v1.27.9-k0s.0"
179-
DefaultK0SSuffix = "k0s.0"
180-
DefaultEtcdImage = "quay.io/k0sproject/etcd:v3.5.13"
178+
defaultK0SRepository = "quay.io/k0sproject/k0s"
179+
// DefaultK0SVersion is the default version used (kubernetes version + k0s version).
180+
DefaultK0SVersion = "v1.27.9+k0s.0"
181+
// DefaultK0SSuffix is the default k0s version used.
182+
DefaultK0SSuffix = "k0s.0"
183+
// DefaultEtcdImage is the default etcd image reference used.
184+
DefaultEtcdImage = "quay.io/k0sproject/etcd:v3.5.13"
181185
)
182186

183-
func (c *ClusterSpec) GetImage() string {
184-
k0sVersion := c.Version
185-
if k0sVersion == "" {
186-
k0sVersion = DefaultK0SVersion
187+
// GetK0sImageRef returns the k0s image reference.
188+
func (c *ClusterSpec) GetK0sImageRef() string {
189+
k0sTag := c.Version
190+
if k0sTag == "" {
191+
k0sTag = DefaultK0SVersion
187192
}
188193

189-
if !strings.Contains(k0sVersion, "-k0s.") {
190-
k0sVersion = fmt.Sprintf("%s-%s", k0sVersion, DefaultK0SSuffix)
194+
if !strings.Contains(k0sTag, "+k0s.") {
195+
k0sTag = fmt.Sprintf("%s+%s", k0sTag, DefaultK0SSuffix)
191196
}
192197

198+
k0sImageRef := fmt.Sprintf("%s:%s", c.Image, k0sTag)
193199
if c.Image == "" {
194-
return fmt.Sprintf("%s:%s", defaultK0SImage, k0sVersion)
200+
k0sImageRef = fmt.Sprintf("%s:%s", defaultK0SRepository, k0sTag)
195201
}
196202

197-
return fmt.Sprintf("%s:%s", c.Image, k0sVersion)
203+
// Mutate image reference to avoid "+" character in the k0s version tag which is not allowed in some
204+
// registries like Docker Hub (https://github.com/distribution/reference/blob/main/reference.go)
205+
return strings.ReplaceAll(k0sImageRef, "+k0s.", "-k0s.")
198206
}
199207

200208
// ClusterStatus defines the observed state of K0smotronCluster

api/k0smotron.io/v1beta1/k0smotroncluster_types_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ func TestClusterSpec_GetImage(t *testing.T) {
3737
{
3838
name: "Only version given with suffix",
3939
spec: &ClusterSpec{
40-
Version: "v1.29.4-k0s.0",
40+
Version: "v1.29.4+k0s.0",
4141
},
4242
want: "quay.io/k0sproject/k0s:v1.29.4-k0s.0",
4343
},
@@ -66,7 +66,7 @@ func TestClusterSpec_GetImage(t *testing.T) {
6666
}
6767
for _, tt := range tests {
6868
t.Run(tt.name, func(t *testing.T) {
69-
if got := tt.spec.GetImage(); got != tt.want {
69+
if got := tt.spec.GetK0sImageRef(); got != tt.want {
7070
require.Equal(t, tt.want, got)
7171
}
7272
})

cmd/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ func main() {
327327
os.Exit(1)
328328
}
329329

330-
if err = (&controlplane.K0smotronControlPlaneValidator{}).SetupK0smotronControlPlaneWebhookWithManager(mgr); err != nil {
330+
if err = controlplane.SetupK0smotronControlPlaneWebhookWithManager(mgr); err != nil {
331331
setupLog.Error(err, "unable to create validation webhook", "webhook", "K0smotronControlPlaneValidator")
332332
os.Exit(1)
333333
}

config/webhook/manifests.yaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,26 @@ webhooks:
2424
resources:
2525
- clusters
2626
sideEffects: None
27+
- admissionReviewVersions:
28+
- v1
29+
clientConfig:
30+
service:
31+
name: webhook-service
32+
namespace: system
33+
path: /mutate-controlplane-cluster-x-k8s-io-v1beta1-k0smotroncontrolplane
34+
failurePolicy: Fail
35+
name: mutate-k0smotroncontrolplane-v1beta1.k0smotron.io
36+
rules:
37+
- apiGroups:
38+
- controlplane.cluster.x-k8s.io
39+
apiVersions:
40+
- v1beta1
41+
operations:
42+
- CREATE
43+
- UPDATE
44+
resources:
45+
- k0smotroncontrolplanes
46+
sideEffects: None
2747
---
2848
apiVersion: admissionregistration.k8s.io/v1
2949
kind: ValidatingWebhookConfiguration

internal/controller/controlplane/k0smotron_controlplane_controller.go

Lines changed: 53 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -417,16 +417,25 @@ func ensureCertificates(ctx context.Context, cluster *clusterv1.Cluster, kcp *cp
417417
}
418418

419419
// 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.
420+
// If spec.version doesn't contain "-k0s." or "+k0s." suffix, it removes the suffix from status.version as well.
421+
// TODO: Remove support for "-k0s." suffix in future versions.
421422
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]
423+
specHasK0sSuffix := strings.Contains(specVersion, "-k0s.") || strings.Contains(specVersion, "+k0s.")
424+
425+
if !specHasK0sSuffix {
426+
if strings.Contains(statusVersion, "-k0s.") {
427+
// If spec.version doesn't have the -k0s. suffix, remove it from status.version as well
428+
parts := strings.Split(statusVersion, "-k0s.")
429+
if len(parts) > 0 {
430+
return parts[0]
431+
}
432+
}
433+
if strings.Contains(statusVersion, "+k0s.") {
434+
// If spec.version doesn't have the +k0s. suffix, remove it from status.version as well
435+
parts := strings.Split(statusVersion, "+k0s.")
436+
if len(parts) > 0 {
437+
return parts[0]
438+
}
430439
}
431440
}
432441

@@ -462,8 +471,8 @@ func (c *K0smotronController) computeStatus(ctx context.Context, cluster *cluste
462471
var updatedReplicas, readyReplicas, unavailableReplicas int
463472

464473
desiredVersionStr := kcp.Spec.Version
465-
if !strings.Contains(desiredVersionStr, "-") {
466-
desiredVersionStr = fmt.Sprintf("%s-%s", desiredVersionStr, kapi.DefaultK0SSuffix)
474+
if !strings.Contains(desiredVersionStr, "-") && !strings.Contains(desiredVersionStr, "+") {
475+
desiredVersionStr = fmt.Sprintf("%s+%s", desiredVersionStr, kapi.DefaultK0SSuffix)
467476
}
468477
desiredVersion, err := version.NewVersion(desiredVersionStr)
469478
if err != nil {
@@ -488,7 +497,12 @@ func (c *K0smotronController) computeStatus(ctx context.Context, cluster *cluste
488497
continue
489498
}
490499

491-
currentVersion, err := scope.getComparableK0sVersionRunningInPod(ctx, &pod)
500+
currentVersion, err := scope.getK0sVersionRunningInPod(ctx, &pod)
501+
if err != nil {
502+
return err
503+
}
504+
// Align version format in spec to the current version format for comparison. DO NOT modify version format in spec.
505+
currentVersion, err = alignToSpecVersionFormat(desiredVersion, currentVersion)
492506
if err != nil {
493507
return err
494508
}
@@ -527,6 +541,32 @@ func (c *K0smotronController) computeStatus(ctx context.Context, cluster *cluste
527541
return nil
528542
}
529543

544+
// alignToSpecVersionFormat ensures that the currentVersion format matches the desiredVersion format.
545+
// TODO: Once the "-k0s." suffix is fully deprecated, this function can be removed.
546+
func alignToSpecVersionFormat(specVersion, currentVersion *version.Version) (*version.Version, error) {
547+
specVersionUsesDefaultK0SSuffix := strings.Contains(specVersion.String(), "+")
548+
currentVersionUsesDefaultK0SSuffix := strings.Contains(currentVersion.String(), "+")
549+
550+
isFormatAligned := (specVersionUsesDefaultK0SSuffix && currentVersionUsesDefaultK0SSuffix) ||
551+
(!specVersionUsesDefaultK0SSuffix && !currentVersionUsesDefaultK0SSuffix)
552+
553+
if isFormatAligned {
554+
return currentVersion, nil
555+
}
556+
557+
currentVersionStr := currentVersion.String()
558+
if !specVersionUsesDefaultK0SSuffix {
559+
// Spec version format is like "vX.Y.Z-k0s.0". Convert currentVersion to match it.
560+
currentVersionAlignedStr := strings.Replace(currentVersionStr, "+k0s.", "-k0s.", 1)
561+
return version.NewVersion(currentVersionAlignedStr)
562+
}
563+
564+
// Current version format is like "vX.Y.Z-k0s.0". Convert currentVersion to match it.
565+
currentVersionAlignedStr := strings.Replace(currentVersionStr, "-k0s.", "+k0s.", 1)
566+
return version.NewVersion(currentVersionAlignedStr)
567+
568+
}
569+
530570
// computeAvailability checks if the control plane is ready by connecting to the API server
531571
// and checking if the control plane is initialized
532572
func (c *K0smotronController) computeAvailability(ctx context.Context, cluster *clusterv1.Cluster, kcp *cpv1beta1.K0smotronControlPlane) {
@@ -574,16 +614,12 @@ func (c *K0smotronController) computeAvailability(ctx context.Context, cluster *
574614
})
575615
}
576616

577-
func (scope *kmcScope) getComparableK0sVersionRunningInPod(ctx context.Context, pod *corev1.Pod) (*version.Version, error) {
617+
func (scope *kmcScope) getK0sVersionRunningInPod(ctx context.Context, pod *corev1.Pod) (*version.Version, error) {
578618
currentVersionOutput, err := exec.PodExecCmdOutput(ctx, scope.clientSet, scope.restConfig, pod.GetName(), pod.GetNamespace(), "k0s version")
579619
if err != nil {
580620
return nil, err
581621
}
582622
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)
587623
return version.NewVersion(currentVersionStr)
588624
}
589625

internal/controller/controlplane/k0smotron_controlplane_controller_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"testing"
55

66
kapi "github.com/k0sproject/k0smotron/api/k0smotron.io/v1beta1"
7+
"github.com/k0sproject/version"
78
"github.com/stretchr/testify/assert"
89
"github.com/stretchr/testify/require"
910
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -247,3 +248,38 @@ func TestIsClusterSpecSynced(t *testing.T) {
247248
})
248249
}
249250
}
251+
252+
func Test_alignToSpecVersionFormat(t *testing.T) {
253+
tests := []struct {
254+
name string
255+
specVersion *version.Version
256+
currentVersion *version.Version
257+
want *version.Version
258+
}{
259+
{
260+
name: "both versions have same format",
261+
specVersion: version.MustParse("v1.33.1-k0s.0"),
262+
currentVersion: version.MustParse("v1.33.1-k0s.1"),
263+
want: version.MustParse("v1.33.1-k0s.1"),
264+
},
265+
{
266+
name: "versions does not have same format: spec with +k0s, current with -k0s",
267+
specVersion: version.MustParse("v1.33.1+k0s.0"),
268+
currentVersion: version.MustParse("v1.33.1-k0s.1"),
269+
want: version.MustParse("v1.33.1+k0s.1"),
270+
},
271+
{
272+
name: "versions does not have same format: spec with -k0s, current with +k0s",
273+
specVersion: version.MustParse("v1.33.1-k0s.0"),
274+
currentVersion: version.MustParse("v1.33.1+k0s.1"),
275+
want: version.MustParse("v1.33.1-k0s.1"),
276+
},
277+
}
278+
for _, tt := range tests {
279+
t.Run(tt.name, func(t *testing.T) {
280+
got, err := alignToSpecVersionFormat(tt.specVersion, tt.currentVersion)
281+
require.NoError(t, err)
282+
require.True(t, tt.want.Equal(got), "alignToSpecVersionFormat() = %v, want %v", got, tt.want)
283+
})
284+
}
285+
}

internal/controller/controlplane/k0smotron_controlplane_webhook.go

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package controlplane
1919
import (
2020
"context"
2121
"fmt"
22+
2223
k0smotronio "github.com/k0sproject/k0smotron/internal/controller/k0smotron.io"
2324
"github.com/k0sproject/version"
2425
"k8s.io/apimachinery/pkg/runtime"
@@ -30,6 +31,7 @@ import (
3031
)
3132

3233
// +kubebuilder:webhook:path=/validate-controlplane-cluster-x-k8s-io-v1beta1-k0smotroncontrolplane,mutating=false,failurePolicy=fail,sideEffects=None,groups=controlplane.cluster.x-k8s.io,resources=k0smotroncontrolplanes,verbs=create;update,versions=v1beta1,name=validate-k0smotroncontrolplane-v1beta1.k0smotron.io,admissionReviewVersions=v1
34+
// +kubebuilder:webhook:path=/mutate-controlplane-cluster-x-k8s-io-v1beta1-k0smotroncontrolplane,mutating=true,failurePolicy=fail,sideEffects=None,groups=controlplane.cluster.x-k8s.io,resources=k0smotroncontrolplanes,verbs=create;update,versions=v1beta1,name=mutate-k0smotroncontrolplane-v1beta1.k0smotron.io,admissionReviewVersions=v1
3335

3436
// K0smotronControlPlaneValidator struct is responsible for validating the K0smotronControlPlane resource when it is created, updated, or deleted.
3537
//
@@ -39,7 +41,25 @@ type K0smotronControlPlaneValidator struct {
3941
cv k0smotronio.ClusterValidator
4042
}
4143

44+
// K0smotronControlPlaneDefaulter struct is responsible for defaulting the K0smotronControlPlane resource when it is created or updated.
45+
//
46+
// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
47+
// as this struct is used only for temporary operations and does not need to be deeply copied.
48+
type K0smotronControlPlaneDefaulter struct {
49+
cv k0smotronio.ClusterDefaulter
50+
}
51+
4252
var _ webhook.CustomValidator = &K0smotronControlPlaneValidator{}
53+
var _ webhook.CustomDefaulter = &K0smotronControlPlaneDefaulter{}
54+
55+
// Default implements webhook.CustomDefaulter so a webhook will be registered for the type K0smotronControlPlane.
56+
func (d *K0smotronControlPlaneDefaulter) Default(_ context.Context, obj runtime.Object) error {
57+
kcp, ok := obj.(*v1beta1.K0smotronControlPlane)
58+
if !ok {
59+
return fmt.Errorf("expected a K0smotronControlPlane object but got %T", obj)
60+
}
61+
return d.cv.DefaultClusterSpec(&kcp.Spec)
62+
}
4363

4464
// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type K0smotronControlPlane.
4565
func (v *K0smotronControlPlaneValidator) ValidateCreate(_ context.Context, obj runtime.Object) (admission.Warnings, error) {
@@ -108,9 +128,10 @@ func (v *K0smotronControlPlaneValidator) ValidateDelete(_ context.Context, _ run
108128
}
109129

110130
// SetupK0smotronControlPlaneWebhookWithManager registers the webhook for K0smotronControlPlane in the manager.
111-
func (v *K0smotronControlPlaneValidator) SetupK0smotronControlPlaneWebhookWithManager(mgr ctrl.Manager) error {
131+
func SetupK0smotronControlPlaneWebhookWithManager(mgr ctrl.Manager) error {
112132
return ctrl.NewWebhookManagedBy(mgr).
113133
For(&v1beta1.K0smotronControlPlane{}).
114-
WithValidator(v).
134+
WithValidator(&K0smotronControlPlaneValidator{}).
135+
WithDefaulter(&K0smotronControlPlaneDefaulter{}).
115136
Complete()
116137
}

internal/controller/k0smotron.io/k0smotroncluster_etcd.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -406,7 +406,7 @@ func initialCluster(kmc *km.Cluster, replicas int32) string {
406406
}
407407

408408
func generateEtcdInitContainers(kmc *km.Cluster, existingSts *apps.StatefulSet) []v1.Container {
409-
checkImage := kmc.Spec.GetImage()
409+
checkImage := kmc.Spec.GetK0sImageRef()
410410
if existingSts != nil {
411411
for _, c := range existingSts.Spec.Template.Spec.InitContainers {
412412
if c.Name == "dns-check" {

internal/controller/k0smotron.io/k0smotroncluster_statefulset.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ func (scope *kmcScope) generateStatefulSet(kmc *km.Cluster) (apps.StatefulSet, e
118118
}},
119119
Containers: []v1.Container{{
120120
Name: "controller",
121-
Image: kmc.Spec.GetImage(),
121+
Image: kmc.Spec.GetK0sImageRef(),
122122
ImagePullPolicy: v1.PullIfNotPresent,
123123
Args: []string{"/bin/sh", "-c", "/k0smotron-entrypoint.sh"},
124124
Ports: []v1.ContainerPort{
@@ -442,7 +442,7 @@ func mountSecrets(kmc *km.Cluster, sfs *apps.StatefulSet) {
442442
// Otherwise k0s will trip over the permissions and RO mounts
443443
sfs.Spec.Template.Spec.InitContainers = append(sfs.Spec.Template.Spec.InitContainers, v1.Container{
444444
Name: "certs-init",
445-
Image: kmc.Spec.GetImage(),
445+
Image: kmc.Spec.GetK0sImageRef(),
446446
Command: []string{
447447
"sh",
448448
"-c",

0 commit comments

Comments
 (0)