k8s.io/kubernetes@v1.29.3/pkg/apis/resource/validation/validation_resourceclaimtemplate_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/pointer" 28 ) 29 30 func testClaimTemplate(name, namespace string, spec resource.ResourceClaimSpec) *resource.ResourceClaimTemplate { 31 return &resource.ResourceClaimTemplate{ 32 ObjectMeta: metav1.ObjectMeta{ 33 Name: name, 34 Namespace: namespace, 35 }, 36 Spec: resource.ResourceClaimTemplateSpec{ 37 Spec: spec, 38 }, 39 } 40 } 41 42 func TestValidateClaimTemplate(t *testing.T) { 43 validMode := resource.AllocationModeImmediate 44 invalidMode := resource.AllocationMode("invalid") 45 goodName := "foo" 46 badName := "!@#$%^" 47 goodNS := "ns" 48 goodClaimSpec := resource.ResourceClaimSpec{ 49 ResourceClassName: goodName, 50 AllocationMode: validMode, 51 } 52 now := metav1.Now() 53 badValue := "spaces not allowed" 54 55 scenarios := map[string]struct { 56 template *resource.ResourceClaimTemplate 57 wantFailures field.ErrorList 58 }{ 59 "good-claim": { 60 template: testClaimTemplate(goodName, goodNS, goodClaimSpec), 61 }, 62 "missing-name": { 63 wantFailures: field.ErrorList{field.Required(field.NewPath("metadata", "name"), "name or generateName is required")}, 64 template: testClaimTemplate("", goodNS, goodClaimSpec), 65 }, 66 "bad-name": { 67 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])?)*')")}, 68 template: testClaimTemplate(badName, goodNS, goodClaimSpec), 69 }, 70 "missing-namespace": { 71 wantFailures: field.ErrorList{field.Required(field.NewPath("metadata", "namespace"), "")}, 72 template: testClaimTemplate(goodName, "", goodClaimSpec), 73 }, 74 "generate-name": { 75 template: func() *resource.ResourceClaimTemplate { 76 template := testClaimTemplate(goodName, goodNS, goodClaimSpec) 77 template.GenerateName = "pvc-" 78 return template 79 }(), 80 }, 81 "uid": { 82 template: func() *resource.ResourceClaimTemplate { 83 template := testClaimTemplate(goodName, goodNS, goodClaimSpec) 84 template.UID = "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d" 85 return template 86 }(), 87 }, 88 "resource-version": { 89 template: func() *resource.ResourceClaimTemplate { 90 template := testClaimTemplate(goodName, goodNS, goodClaimSpec) 91 template.ResourceVersion = "1" 92 return template 93 }(), 94 }, 95 "generation": { 96 template: func() *resource.ResourceClaimTemplate { 97 template := testClaimTemplate(goodName, goodNS, goodClaimSpec) 98 template.Generation = 100 99 return template 100 }(), 101 }, 102 "creation-timestamp": { 103 template: func() *resource.ResourceClaimTemplate { 104 template := testClaimTemplate(goodName, goodNS, goodClaimSpec) 105 template.CreationTimestamp = now 106 return template 107 }(), 108 }, 109 "deletion-grace-period-seconds": { 110 template: func() *resource.ResourceClaimTemplate { 111 template := testClaimTemplate(goodName, goodNS, goodClaimSpec) 112 template.DeletionGracePeriodSeconds = pointer.Int64(10) 113 return template 114 }(), 115 }, 116 "owner-references": { 117 template: func() *resource.ResourceClaimTemplate { 118 template := testClaimTemplate(goodName, goodNS, goodClaimSpec) 119 template.OwnerReferences = []metav1.OwnerReference{ 120 { 121 APIVersion: "v1", 122 Kind: "pod", 123 Name: "foo", 124 UID: "ac051fac-2ead-46d9-b8b4-4e0fbeb7455d", 125 }, 126 } 127 return template 128 }(), 129 }, 130 "finalizers": { 131 template: func() *resource.ResourceClaimTemplate { 132 template := testClaimTemplate(goodName, goodNS, goodClaimSpec) 133 template.Finalizers = []string{ 134 "example.com/foo", 135 } 136 return template 137 }(), 138 }, 139 "managed-fields": { 140 template: func() *resource.ResourceClaimTemplate { 141 template := testClaimTemplate(goodName, goodNS, goodClaimSpec) 142 template.ManagedFields = []metav1.ManagedFieldsEntry{ 143 { 144 FieldsType: "FieldsV1", 145 Operation: "Apply", 146 APIVersion: "apps/v1", 147 Manager: "foo", 148 }, 149 } 150 return template 151 }(), 152 }, 153 "good-labels": { 154 template: func() *resource.ResourceClaimTemplate { 155 template := testClaimTemplate(goodName, goodNS, goodClaimSpec) 156 template.Labels = map[string]string{ 157 "apps.kubernetes.io/name": "test", 158 } 159 return template 160 }(), 161 }, 162 "bad-labels": { 163 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])?')")}, 164 template: func() *resource.ResourceClaimTemplate { 165 template := testClaimTemplate(goodName, goodNS, goodClaimSpec) 166 template.Labels = map[string]string{ 167 "hello-world": badValue, 168 } 169 return template 170 }(), 171 }, 172 "good-annotations": { 173 template: func() *resource.ResourceClaimTemplate { 174 template := testClaimTemplate(goodName, goodNS, goodClaimSpec) 175 template.Annotations = map[string]string{ 176 "foo": "bar", 177 } 178 return template 179 }(), 180 }, 181 "bad-annotations": { 182 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]')")}, 183 template: func() *resource.ResourceClaimTemplate { 184 template := testClaimTemplate(goodName, goodNS, goodClaimSpec) 185 template.Annotations = map[string]string{ 186 badName: "hello world", 187 } 188 return template 189 }(), 190 }, 191 "bad-classname": { 192 wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec", "spec", "resourceClassName"), 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])?)*')")}, 193 template: func() *resource.ResourceClaimTemplate { 194 template := testClaimTemplate(goodName, goodNS, goodClaimSpec) 195 template.Spec.Spec.ResourceClassName = badName 196 return template 197 }(), 198 }, 199 "bad-mode": { 200 wantFailures: field.ErrorList{field.NotSupported(field.NewPath("spec", "spec", "allocationMode"), invalidMode, supportedAllocationModes.List())}, 201 template: func() *resource.ResourceClaimTemplate { 202 template := testClaimTemplate(goodName, goodNS, goodClaimSpec) 203 template.Spec.Spec.AllocationMode = invalidMode 204 return template 205 }(), 206 }, 207 "good-parameters": { 208 template: func() *resource.ResourceClaimTemplate { 209 template := testClaimTemplate(goodName, goodNS, goodClaimSpec) 210 template.Spec.Spec.ParametersRef = &resource.ResourceClaimParametersReference{ 211 Kind: "foo", 212 Name: "bar", 213 } 214 return template 215 }(), 216 }, 217 "missing-parameters-kind": { 218 wantFailures: field.ErrorList{field.Required(field.NewPath("spec", "spec", "parametersRef", "kind"), "")}, 219 template: func() *resource.ResourceClaimTemplate { 220 template := testClaimTemplate(goodName, goodNS, goodClaimSpec) 221 template.Spec.Spec.ParametersRef = &resource.ResourceClaimParametersReference{ 222 Name: "bar", 223 } 224 return template 225 }(), 226 }, 227 "missing-parameters-name": { 228 wantFailures: field.ErrorList{field.Required(field.NewPath("spec", "spec", "parametersRef", "name"), "")}, 229 template: func() *resource.ResourceClaimTemplate { 230 template := testClaimTemplate(goodName, goodNS, goodClaimSpec) 231 template.Spec.Spec.ParametersRef = &resource.ResourceClaimParametersReference{ 232 Kind: "foo", 233 } 234 return template 235 }(), 236 }, 237 } 238 239 for name, scenario := range scenarios { 240 t.Run(name, func(t *testing.T) { 241 errs := ValidateClaimTemplate(scenario.template) 242 assert.Equal(t, scenario.wantFailures, errs) 243 }) 244 } 245 } 246 247 func TestValidateClaimTemplateUpdate(t *testing.T) { 248 name := "valid" 249 parameters := &resource.ResourceClaimParametersReference{ 250 Kind: "foo", 251 Name: "bar", 252 } 253 validClaimTemplate := testClaimTemplate("foo", "ns", resource.ResourceClaimSpec{ 254 ResourceClassName: name, 255 AllocationMode: resource.AllocationModeImmediate, 256 ParametersRef: parameters, 257 }) 258 259 scenarios := map[string]struct { 260 oldClaimTemplate *resource.ResourceClaimTemplate 261 update func(claim *resource.ResourceClaimTemplate) *resource.ResourceClaimTemplate 262 wantFailures field.ErrorList 263 }{ 264 "valid-no-op-update": { 265 oldClaimTemplate: validClaimTemplate, 266 update: func(claim *resource.ResourceClaimTemplate) *resource.ResourceClaimTemplate { return claim }, 267 }, 268 "invalid-update-class": { 269 wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec"), func() resource.ResourceClaimTemplateSpec { 270 spec := validClaimTemplate.Spec.DeepCopy() 271 spec.Spec.ResourceClassName += "2" 272 return *spec 273 }(), "field is immutable")}, 274 oldClaimTemplate: validClaimTemplate, 275 update: func(template *resource.ResourceClaimTemplate) *resource.ResourceClaimTemplate { 276 template.Spec.Spec.ResourceClassName += "2" 277 return template 278 }, 279 }, 280 "invalid-update-remove-parameters": { 281 wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec"), func() resource.ResourceClaimTemplateSpec { 282 spec := validClaimTemplate.Spec.DeepCopy() 283 spec.Spec.ParametersRef = nil 284 return *spec 285 }(), "field is immutable")}, 286 oldClaimTemplate: validClaimTemplate, 287 update: func(template *resource.ResourceClaimTemplate) *resource.ResourceClaimTemplate { 288 template.Spec.Spec.ParametersRef = nil 289 return template 290 }, 291 }, 292 "invalid-update-mode": { 293 wantFailures: field.ErrorList{field.Invalid(field.NewPath("spec"), func() resource.ResourceClaimTemplateSpec { 294 spec := validClaimTemplate.Spec.DeepCopy() 295 spec.Spec.AllocationMode = resource.AllocationModeWaitForFirstConsumer 296 return *spec 297 }(), "field is immutable")}, 298 oldClaimTemplate: validClaimTemplate, 299 update: func(template *resource.ResourceClaimTemplate) *resource.ResourceClaimTemplate { 300 template.Spec.Spec.AllocationMode = resource.AllocationModeWaitForFirstConsumer 301 return template 302 }, 303 }, 304 } 305 306 for name, scenario := range scenarios { 307 t.Run(name, func(t *testing.T) { 308 scenario.oldClaimTemplate.ResourceVersion = "1" 309 errs := ValidateClaimTemplateUpdate(scenario.update(scenario.oldClaimTemplate.DeepCopy()), scenario.oldClaimTemplate) 310 assert.Equal(t, scenario.wantFailures, errs) 311 }) 312 } 313 }