k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/pkg/apis/resource/validation/validation_resourceslice_test.go (about) 1 /* 2 Copyright 2022 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package validation 18 19 import ( 20 "testing" 21 22 "github.com/stretchr/testify/assert" 23 24 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 "k8s.io/apimachinery/pkg/util/validation/field" 26 "k8s.io/kubernetes/pkg/apis/resource" 27 "k8s.io/utils/ptr" 28 ) 29 30 func testResourceSlice(name, nodeName, driverName string) *resource.ResourceSlice { 31 return &resource.ResourceSlice{ 32 ObjectMeta: metav1.ObjectMeta{ 33 Name: name, 34 }, 35 NodeName: nodeName, 36 DriverName: driverName, 37 ResourceModel: resource.ResourceModel{ 38 NamedResources: &resource.NamedResourcesResources{}, 39 }, 40 } 41 } 42 43 func TestValidateResourceSlice(t *testing.T) { 44 goodName := "foo" 45 badName := "!@#$%^" 46 driverName := "test.example.com" 47 now := metav1.Now() 48 badValue := "spaces not allowed" 49 50 scenarios := map[string]struct { 51 slice *resource.ResourceSlice 52 wantFailures field.ErrorList 53 }{ 54 "good": { 55 slice: testResourceSlice(goodName, goodName, driverName), 56 }, 57 "missing-name": { 58 wantFailures: field.ErrorList{field.Required(field.NewPath("metadata", "name"), "name or generateName is required")}, 59 slice: testResourceSlice("", goodName, driverName), 60 }, 61 "bad-name": { 62 wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "name"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")}, 63 slice: testResourceSlice(badName, goodName, driverName), 64 }, 65 "generate-name": { 66 slice: func() *resource.ResourceSlice { 67 slice := testResourceSlice(goodName, goodName, driverName) 68 slice.GenerateName = "prefix-" 69 return slice 70 }(), 71 }, 72 "uid": { 73 slice: func() *resource.ResourceSlice { 74 slice := testResourceSlice(goodName, goodName, driverName) 75 slice.UID = "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d" 76 return slice 77 }(), 78 }, 79 "resource-version": { 80 slice: func() *resource.ResourceSlice { 81 slice := testResourceSlice(goodName, goodName, driverName) 82 slice.ResourceVersion = "1" 83 return slice 84 }(), 85 }, 86 "generation": { 87 slice: func() *resource.ResourceSlice { 88 slice := testResourceSlice(goodName, goodName, driverName) 89 slice.Generation = 100 90 return slice 91 }(), 92 }, 93 "creation-timestamp": { 94 slice: func() *resource.ResourceSlice { 95 slice := testResourceSlice(goodName, goodName, driverName) 96 slice.CreationTimestamp = now 97 return slice 98 }(), 99 }, 100 "deletion-grace-period-seconds": { 101 slice: func() *resource.ResourceSlice { 102 slice := testResourceSlice(goodName, goodName, driverName) 103 slice.DeletionGracePeriodSeconds = ptr.To[int64](10) 104 return slice 105 }(), 106 }, 107 "owner-references": { 108 slice: func() *resource.ResourceSlice { 109 slice := testResourceSlice(goodName, goodName, driverName) 110 slice.OwnerReferences = []metav1.OwnerReference{ 111 { 112 APIVersion: "v1", 113 Kind: "pod", 114 Name: "foo", 115 UID: "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d", 116 }, 117 } 118 return slice 119 }(), 120 }, 121 "finalizers": { 122 slice: func() *resource.ResourceSlice { 123 slice := testResourceSlice(goodName, goodName, driverName) 124 slice.Finalizers = []string{ 125 "example.com/foo", 126 } 127 return slice 128 }(), 129 }, 130 "managed-fields": { 131 slice: func() *resource.ResourceSlice { 132 slice := testResourceSlice(goodName, goodName, driverName) 133 slice.ManagedFields = []metav1.ManagedFieldsEntry{ 134 { 135 FieldsType: "FieldsV1", 136 Operation: "Apply", 137 APIVersion: "apps/v1", 138 Manager: "foo", 139 }, 140 } 141 return slice 142 }(), 143 }, 144 "good-labels": { 145 slice: func() *resource.ResourceSlice { 146 slice := testResourceSlice(goodName, goodName, driverName) 147 slice.Labels = map[string]string{ 148 "apps.kubernetes.io/name": "test", 149 } 150 return slice 151 }(), 152 }, 153 "bad-labels": { 154 wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "labels"), badValue, "a valid label must be an empty string or consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyValue', or 'my_value', or '12345', regex used for validation is '(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?')")}, 155 slice: func() *resource.ResourceSlice { 156 slice := testResourceSlice(goodName, goodName, driverName) 157 slice.Labels = map[string]string{ 158 "hello-world": badValue, 159 } 160 return slice 161 }(), 162 }, 163 "good-annotations": { 164 slice: func() *resource.ResourceSlice { 165 slice := testResourceSlice(goodName, goodName, driverName) 166 slice.Annotations = map[string]string{ 167 "foo": "bar", 168 } 169 return slice 170 }(), 171 }, 172 "bad-annotations": { 173 wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "annotations"), badName, "name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')")}, 174 slice: func() *resource.ResourceSlice { 175 slice := testResourceSlice(goodName, goodName, driverName) 176 slice.Annotations = map[string]string{ 177 badName: "hello world", 178 } 179 return slice 180 }(), 181 }, 182 "bad-nodename": { 183 wantFailures: field.ErrorList{field.Invalid(field.NewPath("nodeName"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")}, 184 slice: testResourceSlice(goodName, badName, driverName), 185 }, 186 "bad-drivername": { 187 wantFailures: field.ErrorList{field.Invalid(field.NewPath("driverName"), badName, "a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')")}, 188 slice: testResourceSlice(goodName, goodName, badName), 189 }, 190 191 "empty-model": { 192 wantFailures: field.ErrorList{field.Required(nil, "exactly one structured model field must be set")}, 193 slice: func() *resource.ResourceSlice { 194 slice := testResourceSlice(goodName, goodName, driverName) 195 slice.ResourceModel = resource.ResourceModel{} 196 return slice 197 }(), 198 }, 199 } 200 201 for name, scenario := range scenarios { 202 t.Run(name, func(t *testing.T) { 203 errs := ValidateResourceSlice(scenario.slice) 204 assert.Equal(t, scenario.wantFailures, errs) 205 }) 206 } 207 } 208 209 func TestValidateResourceSliceUpdate(t *testing.T) { 210 name := "valid" 211 validResourceSlice := testResourceSlice(name, name, name) 212 213 scenarios := map[string]struct { 214 oldResourceSlice *resource.ResourceSlice 215 update func(slice *resource.ResourceSlice) *resource.ResourceSlice 216 wantFailures field.ErrorList 217 }{ 218 "valid-no-op-update": { 219 oldResourceSlice: validResourceSlice, 220 update: func(slice *resource.ResourceSlice) *resource.ResourceSlice { return slice }, 221 }, 222 "invalid-name-update": { 223 oldResourceSlice: validResourceSlice, 224 update: func(slice *resource.ResourceSlice) *resource.ResourceSlice { 225 slice.Name += "-update" 226 return slice 227 }, 228 wantFailures: field.ErrorList{field.Invalid(field.NewPath("metadata", "name"), name+"-update", "field is immutable")}, 229 }, 230 "invalid-update-nodename": { 231 wantFailures: field.ErrorList{field.Invalid(field.NewPath("nodeName"), name+"-updated", "field is immutable")}, 232 oldResourceSlice: validResourceSlice, 233 update: func(slice *resource.ResourceSlice) *resource.ResourceSlice { 234 slice.NodeName += "-updated" 235 return slice 236 }, 237 }, 238 "invalid-update-drivername": { 239 wantFailures: field.ErrorList{field.Invalid(field.NewPath("driverName"), name+"-updated", "field is immutable")}, 240 oldResourceSlice: validResourceSlice, 241 update: func(slice *resource.ResourceSlice) *resource.ResourceSlice { 242 slice.DriverName += "-updated" 243 return slice 244 }, 245 }, 246 } 247 248 for name, scenario := range scenarios { 249 t.Run(name, func(t *testing.T) { 250 scenario.oldResourceSlice.ResourceVersion = "1" 251 errs := ValidateResourceSliceUpdate(scenario.update(scenario.oldResourceSlice.DeepCopy()), scenario.oldResourceSlice) 252 assert.Equal(t, scenario.wantFailures, errs) 253 }) 254 } 255 }