k8s.io/kubernetes@v1.29.3/pkg/registry/storage/csinode/strategy_test.go (about) 1 /* 2 Copyright 2019 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 csinode 18 19 import ( 20 "reflect" 21 "testing" 22 23 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 24 "k8s.io/apimachinery/pkg/util/validation/field" 25 genericapirequest "k8s.io/apiserver/pkg/endpoints/request" 26 "k8s.io/kubernetes/pkg/apis/storage" 27 utilpointer "k8s.io/utils/pointer" 28 ) 29 30 func TestPrepareForCreate(t *testing.T) { 31 valid := getValidCSINode("foo") 32 emptyAllocatable := &storage.CSINode{ 33 ObjectMeta: metav1.ObjectMeta{ 34 Name: "foo", 35 }, 36 Spec: storage.CSINodeSpec{ 37 Drivers: []storage.CSINodeDriver{ 38 { 39 Name: "valid-driver-name", 40 NodeID: "valid-node", 41 TopologyKeys: []string{"company.com/zone1", "company.com/zone2"}, 42 }, 43 }, 44 }, 45 } 46 47 volumeLimitsCases := []struct { 48 name string 49 obj *storage.CSINode 50 expected *storage.CSINode 51 }{ 52 { 53 "empty allocatable", 54 emptyAllocatable, 55 emptyAllocatable, 56 }, 57 { 58 "valid allocatable", 59 valid, 60 valid, 61 }, 62 } 63 64 for _, test := range volumeLimitsCases { 65 t.Run(test.name, func(t *testing.T) { 66 testPrepareForCreate(t, test.obj, test.expected) 67 }) 68 } 69 } 70 71 func testPrepareForCreate(t *testing.T, obj, expected *storage.CSINode) { 72 ctx := genericapirequest.WithRequestInfo(genericapirequest.NewContext(), &genericapirequest.RequestInfo{ 73 APIGroup: "storage.k8s.io", 74 APIVersion: "v1beta1", 75 Resource: "csinodes", 76 }) 77 Strategy.PrepareForCreate(ctx, obj) 78 if !reflect.DeepEqual(*expected, *obj) { 79 t.Errorf("Object mismatch! Expected:\n%#v\ngot:\n%#v", *expected, *obj) 80 } 81 } 82 83 func TestPrepareForUpdate(t *testing.T) { 84 valid := getValidCSINode("foo") 85 differentAllocatable := &storage.CSINode{ 86 ObjectMeta: metav1.ObjectMeta{ 87 Name: "foo", 88 }, 89 Spec: storage.CSINodeSpec{ 90 Drivers: []storage.CSINodeDriver{ 91 { 92 Name: "valid-driver-name", 93 NodeID: "valid-node", 94 TopologyKeys: []string{"company.com/zone1", "company.com/zone2"}, 95 Allocatable: &storage.VolumeNodeResources{Count: utilpointer.Int32Ptr(20)}, 96 }, 97 }, 98 }, 99 } 100 emptyAllocatable := &storage.CSINode{ 101 ObjectMeta: metav1.ObjectMeta{ 102 Name: "foo", 103 }, 104 Spec: storage.CSINodeSpec{ 105 Drivers: []storage.CSINodeDriver{ 106 { 107 Name: "valid-driver-name", 108 NodeID: "valid-node", 109 TopologyKeys: []string{"company.com/zone1", "company.com/zone2"}, 110 }, 111 }, 112 }, 113 } 114 115 volumeLimitsCases := []struct { 116 name string 117 old *storage.CSINode 118 new *storage.CSINode 119 expected *storage.CSINode 120 }{ 121 { 122 "allow empty allocatable when it's not set", 123 emptyAllocatable, 124 emptyAllocatable, 125 emptyAllocatable, 126 }, 127 { 128 "allow valid allocatable when it's already set", 129 valid, 130 differentAllocatable, 131 differentAllocatable, 132 }, 133 { 134 "allow valid allocatable when it's not set", 135 emptyAllocatable, 136 valid, 137 valid, 138 }, 139 } 140 141 for _, test := range volumeLimitsCases { 142 t.Run(test.name, func(t *testing.T) { 143 testPrepareForUpdate(t, test.new, test.old, test.expected) 144 }) 145 } 146 } 147 148 func testPrepareForUpdate(t *testing.T, obj, old, expected *storage.CSINode) { 149 ctx := genericapirequest.WithRequestInfo(genericapirequest.NewContext(), &genericapirequest.RequestInfo{ 150 APIGroup: "storage.k8s.io", 151 APIVersion: "v1beta1", 152 Resource: "csinodes", 153 }) 154 Strategy.PrepareForUpdate(ctx, obj, old) 155 if !reflect.DeepEqual(*expected, *obj) { 156 t.Errorf("Object mismatch! Expected:\n%#v\ngot:\n%#v", *expected, *obj) 157 } 158 } 159 160 func TestCSINodeStrategy(t *testing.T) { 161 ctx := genericapirequest.WithRequestInfo(genericapirequest.NewContext(), &genericapirequest.RequestInfo{ 162 APIGroup: "storage.k8s.io", 163 APIVersion: "v1beta1", 164 Resource: "csinodes", 165 }) 166 if Strategy.NamespaceScoped() { 167 t.Errorf("CSINode must not be namespace scoped") 168 } 169 if Strategy.AllowCreateOnUpdate() { 170 t.Errorf("CSINode should not allow create on update") 171 } 172 173 csiNode := getValidCSINode("valid-csinode") 174 175 Strategy.PrepareForCreate(ctx, csiNode) 176 177 errs := Strategy.Validate(ctx, csiNode) 178 if len(errs) != 0 { 179 t.Errorf("unexpected error validating %v", errs) 180 } 181 182 // Update of spec is allowed 183 newCSINode := csiNode.DeepCopy() 184 newCSINode.Spec.Drivers[0].NodeID = "valid-node-2" 185 186 Strategy.PrepareForUpdate(ctx, newCSINode, csiNode) 187 188 errs = Strategy.ValidateUpdate(ctx, newCSINode, csiNode) 189 if len(errs) == 0 { 190 t.Errorf("expected validation error") 191 } 192 } 193 194 func TestCSINodeValidation(t *testing.T) { 195 tests := []struct { 196 name string 197 csiNode *storage.CSINode 198 expectError bool 199 }{ 200 { 201 "valid csinode", 202 getValidCSINode("foo"), 203 false, 204 }, 205 { 206 "valid csinode with empty allocatable", 207 &storage.CSINode{ 208 ObjectMeta: metav1.ObjectMeta{ 209 Name: "foo", 210 }, 211 Spec: storage.CSINodeSpec{ 212 Drivers: []storage.CSINodeDriver{ 213 { 214 Name: "valid-driver-name", 215 NodeID: "valid-node", 216 TopologyKeys: []string{"company.com/zone1", "company.com/zone2"}, 217 }, 218 }, 219 }, 220 }, 221 false, 222 }, 223 { 224 "valid csinode with missing volume limits", 225 &storage.CSINode{ 226 ObjectMeta: metav1.ObjectMeta{ 227 Name: "foo", 228 }, 229 Spec: storage.CSINodeSpec{ 230 Drivers: []storage.CSINodeDriver{ 231 { 232 Name: "valid-driver-name", 233 NodeID: "valid-node", 234 TopologyKeys: []string{"company.com/zone1", "company.com/zone2"}, 235 Allocatable: &storage.VolumeNodeResources{Count: nil}, 236 }, 237 }, 238 }, 239 }, 240 false, 241 }, 242 { 243 "invalid driver name", 244 &storage.CSINode{ 245 ObjectMeta: metav1.ObjectMeta{ 246 Name: "foo", 247 }, 248 Spec: storage.CSINodeSpec{ 249 Drivers: []storage.CSINodeDriver{ 250 { 251 Name: "$csi-driver@", 252 NodeID: "valid-node", 253 TopologyKeys: []string{"company.com/zone1", "company.com/zone2"}, 254 Allocatable: &storage.VolumeNodeResources{Count: utilpointer.Int32Ptr(10)}, 255 }, 256 }, 257 }, 258 }, 259 true, 260 }, 261 { 262 "empty node id", 263 &storage.CSINode{ 264 ObjectMeta: metav1.ObjectMeta{ 265 Name: "foo", 266 }, 267 Spec: storage.CSINodeSpec{ 268 Drivers: []storage.CSINodeDriver{ 269 { 270 Name: "valid-driver-name", 271 NodeID: "", 272 TopologyKeys: []string{"company.com/zone1", "company.com/zone2"}, 273 Allocatable: &storage.VolumeNodeResources{Count: utilpointer.Int32Ptr(10)}, 274 }, 275 }, 276 }, 277 }, 278 true, 279 }, 280 { 281 "invalid allocatable with negative volumes limit", 282 &storage.CSINode{ 283 ObjectMeta: metav1.ObjectMeta{ 284 Name: "foo", 285 }, 286 Spec: storage.CSINodeSpec{ 287 Drivers: []storage.CSINodeDriver{ 288 { 289 Name: "valid-driver-name", 290 NodeID: "valid-node", 291 TopologyKeys: []string{"company.com/zone1", "company.com/zone2"}, 292 Allocatable: &storage.VolumeNodeResources{Count: utilpointer.Int32Ptr(-1)}, 293 }, 294 }, 295 }, 296 }, 297 true, 298 }, 299 { 300 "invalid topology keys", 301 &storage.CSINode{ 302 ObjectMeta: metav1.ObjectMeta{ 303 Name: "foo", 304 }, 305 Spec: storage.CSINodeSpec{ 306 Drivers: []storage.CSINodeDriver{ 307 { 308 Name: "valid-driver-name", 309 NodeID: "valid-node", 310 TopologyKeys: []string{"company.com/zone1", ""}, 311 Allocatable: &storage.VolumeNodeResources{Count: utilpointer.Int32Ptr(10)}, 312 }, 313 }, 314 }, 315 }, 316 true, 317 }, 318 } 319 320 for _, test := range tests { 321 t.Run(test.name, func(t *testing.T) { 322 323 testValidation := func(csiNode *storage.CSINode, apiVersion string) field.ErrorList { 324 ctx := genericapirequest.WithRequestInfo(genericapirequest.NewContext(), &genericapirequest.RequestInfo{ 325 APIGroup: "storage.k8s.io", 326 APIVersion: "v1beta1", 327 Resource: "csinodes", 328 }) 329 return Strategy.Validate(ctx, csiNode) 330 } 331 332 betaErr := testValidation(test.csiNode, "v1beta1") 333 if len(betaErr) > 0 && !test.expectError { 334 t.Errorf("Validation of v1beta1 object failed: %+v", betaErr) 335 } 336 if len(betaErr) == 0 && test.expectError { 337 t.Errorf("Validation of v1beta1 object unexpectedly succeeded") 338 } 339 }) 340 } 341 } 342 343 func getValidCSINode(name string) *storage.CSINode { 344 return &storage.CSINode{ 345 ObjectMeta: metav1.ObjectMeta{ 346 Name: name, 347 }, 348 Spec: storage.CSINodeSpec{ 349 Drivers: []storage.CSINodeDriver{ 350 { 351 Name: "valid-driver-name", 352 NodeID: "valid-node", 353 TopologyKeys: []string{"company.com/zone1", "company.com/zone2"}, 354 Allocatable: &storage.VolumeNodeResources{Count: utilpointer.Int32Ptr(10)}, 355 }, 356 }, 357 }, 358 } 359 }