From f7f641b7360a66682d202a86ef65780a9725a23d Mon Sep 17 00:00:00 2001 From: Ethan Mosbaugh Date: Tue, 23 Jul 2024 21:04:41 -0700 Subject: [PATCH] feat: add support for image tag and digest Signed-off-by: Ethan Mosbaugh --- .../k0s/v1beta1/clusterconfig_types_test.go | 27 ++++++++++ pkg/apis/k0s/v1beta1/images.go | 5 +- pkg/apis/k0s/v1beta1/images_test.go | 51 +++++++++++++++++++ pkg/apis/k0s/v1beta1/nllb_test.go | 2 +- .../k0s.k0sproject.io_clusterconfigs.yaml | 24 ++++----- 5 files changed, 94 insertions(+), 15 deletions(-) diff --git a/pkg/apis/k0s/v1beta1/clusterconfig_types_test.go b/pkg/apis/k0s/v1beta1/clusterconfig_types_test.go index f9b7e4365855..181a6e29153c 100644 --- a/pkg/apis/k0s/v1beta1/clusterconfig_types_test.go +++ b/pkg/apis/k0s/v1beta1/clusterconfig_types_test.go @@ -18,10 +18,12 @@ package v1beta1 import ( "encoding/json" + "fmt" "testing" "github.com/k0sproject/k0s/internal/pkg/iface" "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" "sigs.k8s.io/yaml" ) @@ -65,6 +67,31 @@ func TestEmptyClusterSpec(t *testing.T) { assert.Nil(t, errs) } +func TestClusterSpecCustomImages(t *testing.T) { + underTest := ClusterConfig{ + Spec: &ClusterSpec{ + Images: &ClusterImages{ + DefaultPullPolicy: string(corev1.PullIfNotPresent), + Konnectivity: ImageSpec{ + Image: "foo", + Version: "v1", + }, + PushGateway: ImageSpec{ + Image: "bar", + Version: "v2@sha256:0000000000000000000000000000000000000000000000000000000000000000", + }, + MetricsServer: ImageSpec{ + Image: "baz", + Version: "sha256:0000000000000000000000000000000000000000000000000000000000000000", + }, + }, + }, + } + + errs := underTest.Validate() + assert.Nil(t, errs, fmt.Sprintf("%v", errs)) +} + func TestEtcdDefaults(t *testing.T) { yamlData := ` apiVersion: k0s.k0sproject.io/v1beta1 diff --git a/pkg/apis/k0s/v1beta1/images.go b/pkg/apis/k0s/v1beta1/images.go index 4fd1fd3311a9..3307836ae0b3 100644 --- a/pkg/apis/k0s/v1beta1/images.go +++ b/pkg/apis/k0s/v1beta1/images.go @@ -35,7 +35,7 @@ type ImageSpec struct { // +kubebuilder:validation:MinLength=1 Image string `json:"image"` - // +kubebuilder:validation:Pattern="[\\w][\\w.-]{0,127}" + // +kubebuilder:validation:Pattern="^[\\w][\\w.-]{0,127}(?:@[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,})?$" Version string `json:"version"` } @@ -51,7 +51,8 @@ func (s *ImageSpec) Validate(path *field.Path) (errs field.ErrorList) { errs = append(errs, field.Invalid(path.Child("image"), s.Image, "must not have leading or trailing whitespace")) } - versionRe := regexp.MustCompile(`^` + reference.TagRegexp.String() + `$`) + // Validate the image contains a tag and optional digest + versionRe := regexp.MustCompile(`^` + reference.TagRegexp.String() + `(?:@` + reference.DigestRegexp.String() + `)?$`) if !versionRe.MatchString(s.Version) { errs = append(errs, field.Invalid(path.Child("version"), s.Version, "must match regular expression: "+versionRe.String())) } diff --git a/pkg/apis/k0s/v1beta1/images_test.go b/pkg/apis/k0s/v1beta1/images_test.go index 780afc6ce867..ceaef67c3afe 100644 --- a/pkg/apis/k0s/v1beta1/images_test.go +++ b/pkg/apis/k0s/v1beta1/images_test.go @@ -23,6 +23,7 @@ import ( "github.com/k0sproject/k0s/pkg/constant" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/util/validation/field" "sigs.k8s.io/yaml" ) @@ -117,3 +118,53 @@ func TestOverrideFunction(t *testing.T) { assert.Equal(t, tc.Output, overrideRepository(repository, tc.Input)) } } + +func TestImageSpec_Validate(t *testing.T) { + validTestCases := []struct { + Image string + Version string + }{ + {"my.registry/repo/image", "v1.0.0"}, + {"my.registry/repo/image", "latest"}, + {"my.registry/repo/image", "v1.0.0-rc1"}, + {"my.registry/repo/image", "v1.0.0@sha256:0000000000000000000000000000000000000000000000000000000000000000"}, + } + for _, tc := range validTestCases { + t.Run(tc.Image+":"+tc.Version+"_valid", func(t *testing.T) { + s := &ImageSpec{ + Image: tc.Image, + Version: tc.Version, + } + errs := s.Validate(field.NewPath("image")) + assert.Empty(t, errs) + }) + } + + errVersionRe := `must match regular expression: ^[\w][\w.-]{0,127}(?:@[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,})?$` + + invalidTestCases := []struct { + Image string + Version string + Errs field.ErrorList + }{ + { + "my.registry/repo/image", "", + field.ErrorList{field.Invalid(field.NewPath("image").Child("version"), "", errVersionRe)}, + }, + // digest only is currently not supported + { + "my.registry/repo/image", "sha256:0000000000000000000000000000000000000000000000000000000000000000", + field.ErrorList{field.Invalid(field.NewPath("image").Child("version"), "sha256:0000000000000000000000000000000000000000000000000000000000000000", errVersionRe)}, + }, + } + for _, tc := range invalidTestCases { + t.Run(tc.Image+":"+tc.Version+"_valid", func(t *testing.T) { + s := &ImageSpec{ + Image: tc.Image, + Version: tc.Version, + } + errs := s.Validate(field.NewPath("image")) + assert.Equal(t, tc.Errs, errs) + }) + } +} diff --git a/pkg/apis/k0s/v1beta1/nllb_test.go b/pkg/apis/k0s/v1beta1/nllb_test.go index 65d1067ad1ed..dcf4dd948861 100644 --- a/pkg/apis/k0s/v1beta1/nllb_test.go +++ b/pkg/apis/k0s/v1beta1/nllb_test.go @@ -100,7 +100,7 @@ spec: }, { "version", `"*"`, - `network: nodeLocalLoadBalancing.envoyProxy.image.version: Invalid value: "*": must match regular expression: ^[\w][\w.-]{0,127}$`, + `network: nodeLocalLoadBalancing.envoyProxy.image.version: Invalid value: "*": must match regular expression: ^[\w][\w.-]{0,127}(?:@[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,})?$`, }, } { t.Run(test.field+"_invalid", func(t *testing.T) { diff --git a/static/manifests/k0s/CustomResourceDefinition/k0s.k0sproject.io_clusterconfigs.yaml b/static/manifests/k0s/CustomResourceDefinition/k0s.k0sproject.io_clusterconfigs.yaml index 1dd7eb803c7d..acf5612e9536 100644 --- a/static/manifests/k0s/CustomResourceDefinition/k0s.k0sproject.io_clusterconfigs.yaml +++ b/static/manifests/k0s/CustomResourceDefinition/k0s.k0sproject.io_clusterconfigs.yaml @@ -232,7 +232,7 @@ spec: minLength: 1 type: string version: - pattern: '[\w][\w.-]{0,127}' + pattern: ^[\w][\w.-]{0,127}(?:@[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,})?$ type: string required: - image @@ -245,7 +245,7 @@ spec: minLength: 1 type: string version: - pattern: '[\w][\w.-]{0,127}' + pattern: ^[\w][\w.-]{0,127}(?:@[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,})?$ type: string required: - image @@ -258,7 +258,7 @@ spec: minLength: 1 type: string version: - pattern: '[\w][\w.-]{0,127}' + pattern: ^[\w][\w.-]{0,127}(?:@[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,})?$ type: string required: - image @@ -272,7 +272,7 @@ spec: minLength: 1 type: string version: - pattern: '[\w][\w.-]{0,127}' + pattern: ^[\w][\w.-]{0,127}(?:@[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,})?$ type: string required: - image @@ -292,7 +292,7 @@ spec: minLength: 1 type: string version: - pattern: '[\w][\w.-]{0,127}' + pattern: ^[\w][\w.-]{0,127}(?:@[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,})?$ type: string required: - image @@ -305,7 +305,7 @@ spec: minLength: 1 type: string version: - pattern: '[\w][\w.-]{0,127}' + pattern: ^[\w][\w.-]{0,127}(?:@[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,})?$ type: string required: - image @@ -322,7 +322,7 @@ spec: minLength: 1 type: string version: - pattern: '[\w][\w.-]{0,127}' + pattern: ^[\w][\w.-]{0,127}(?:@[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,})?$ type: string required: - image @@ -335,7 +335,7 @@ spec: minLength: 1 type: string version: - pattern: '[\w][\w.-]{0,127}' + pattern: ^[\w][\w.-]{0,127}(?:@[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,})?$ type: string required: - image @@ -349,7 +349,7 @@ spec: minLength: 1 type: string version: - pattern: '[\w][\w.-]{0,127}' + pattern: ^[\w][\w.-]{0,127}(?:@[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,})?$ type: string required: - image @@ -362,7 +362,7 @@ spec: minLength: 1 type: string version: - pattern: '[\w][\w.-]{0,127}' + pattern: ^[\w][\w.-]{0,127}(?:@[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,})?$ type: string required: - image @@ -375,7 +375,7 @@ spec: minLength: 1 type: string version: - pattern: '[\w][\w.-]{0,127}' + pattern: ^[\w][\w.-]{0,127}(?:@[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,})?$ type: string required: - image @@ -775,7 +775,7 @@ spec: minLength: 1 type: string version: - pattern: '[\w][\w.-]{0,127}' + pattern: ^[\w][\w.-]{0,127}(?:@[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,})?$ type: string required: - image