github.com/openshift/installer@v1.4.17/pkg/asset/installconfig/openstack/validation/machinepool_test.go (about) 1 package validation 2 3 import ( 4 "testing" 5 6 "github.com/gophercloud/gophercloud/v2/openstack/compute/v2/flavors" 7 logrusTest "github.com/sirupsen/logrus/hooks/test" 8 "github.com/stretchr/testify/assert" 9 "k8s.io/apimachinery/pkg/util/validation/field" 10 11 "github.com/openshift/installer/pkg/types/openstack" 12 ) 13 14 const ( 15 validZone = "valid-zone" 16 invalidZone = "invalid-zone" 17 18 validCtrlPlaneFlavor = "valid-control-plane-flavor" 19 validComputeFlavor = "valid-compute-flavor" 20 21 notExistFlavor = "non-existant-flavor" 22 23 invalidComputeFlavor = "invalid-compute-flavor" 24 invalidCtrlPlaneFlavor = "invalid-control-plane-flavor" 25 warningComputeFlavor = "warning-compute-flavor" 26 warningCtrlPlaneFlavor = "warning-control-plane-flavor" 27 28 baremetalFlavor = "baremetal-flavor" 29 30 invalidType = "invalid-type" 31 volumeSmallSize = 10 32 volumeMediumSize = 40 33 volumeLargeSize = 100 34 ) 35 36 var volumeTypes = []string{"performance", "standard"} 37 var invalidVolumeTypes = []string{"performance", "invalid-type"} 38 var volumeType = volumeTypes[0] 39 40 func validMachinePool() *openstack.MachinePool { 41 return &openstack.MachinePool{ 42 FlavorName: validCtrlPlaneFlavor, 43 Zones: []string{""}, 44 } 45 } 46 47 func invalidMachinePoolSmallVolume() *openstack.MachinePool { 48 return &openstack.MachinePool{ 49 FlavorName: validCtrlPlaneFlavor, 50 Zones: []string{""}, 51 RootVolume: &openstack.RootVolume{ 52 Size: volumeSmallSize, 53 Types: volumeTypes, 54 Zones: []string{""}, 55 }, 56 } 57 } 58 59 func warningMachinePoolMediumVolume() *openstack.MachinePool { 60 return &openstack.MachinePool{ 61 FlavorName: validCtrlPlaneFlavor, 62 Zones: []string{""}, 63 RootVolume: &openstack.RootVolume{ 64 Size: volumeMediumSize, 65 Types: volumeTypes, 66 Zones: []string{""}, 67 }, 68 } 69 } 70 71 func validMachinePoolLargeVolume() *openstack.MachinePool { 72 return &openstack.MachinePool{ 73 FlavorName: validCtrlPlaneFlavor, 74 Zones: []string{""}, 75 RootVolume: &openstack.RootVolume{ 76 Size: volumeLargeSize, 77 Types: volumeTypes, 78 Zones: []string{validZone}, 79 }, 80 } 81 } 82 83 func validMpoolCloudInfo() *CloudInfo { 84 return &CloudInfo{ 85 Flavors: map[string]Flavor{ 86 validCtrlPlaneFlavor: { 87 Flavor: flavors.Flavor{ 88 Name: validCtrlPlaneFlavor, 89 RAM: 16384, 90 Disk: 100, 91 VCPUs: 4, 92 }, 93 }, 94 validComputeFlavor: { 95 Flavor: flavors.Flavor{ 96 Name: validComputeFlavor, 97 RAM: 8192, 98 Disk: 100, 99 VCPUs: 2, 100 }, 101 }, 102 invalidCtrlPlaneFlavor: { 103 Flavor: flavors.Flavor{ 104 Name: invalidCtrlPlaneFlavor, 105 RAM: 8192, // too low 106 Disk: 100, 107 VCPUs: 2, // too low 108 }, 109 }, 110 invalidComputeFlavor: { 111 Flavor: flavors.Flavor{ 112 Name: invalidComputeFlavor, 113 RAM: 8192, 114 Disk: 10, // too low 115 VCPUs: 2, 116 }, 117 }, 118 warningCtrlPlaneFlavor: { 119 Flavor: flavors.Flavor{ 120 Name: warningCtrlPlaneFlavor, 121 RAM: 16384, 122 Disk: 40, // not recommended 123 VCPUs: 4, 124 }, 125 }, 126 warningComputeFlavor: { 127 Flavor: flavors.Flavor{ 128 Name: invalidComputeFlavor, 129 RAM: 8192, 130 Disk: 40, // not recommended 131 VCPUs: 2, 132 }, 133 }, 134 baremetalFlavor: { 135 Flavor: flavors.Flavor{ 136 Name: baremetalFlavor, 137 RAM: 8192, // too low 138 Disk: 10, // too low 139 VCPUs: 2, // too low 140 }, 141 Baremetal: true, 142 }, 143 }, 144 ComputeZones: []string{ 145 validZone, 146 }, 147 VolumeZones: []string{ 148 validZone, 149 }, 150 VolumeTypes: volumeTypes, 151 } 152 } 153 154 func TestOpenStackMachinepoolValidation(t *testing.T) { 155 cases := []struct { 156 name string 157 controlPlane bool // only matters for flavor 158 mpool *openstack.MachinePool 159 cloudInfo *CloudInfo 160 expectedError bool 161 expectedErrMsg string // NOTE: this is a REGEXP 162 expectedWarnMsg string //NOTE: this is a REGEXP 163 }{ 164 { 165 name: "valid control plane", 166 controlPlane: true, 167 mpool: validMachinePool(), 168 cloudInfo: validMpoolCloudInfo(), 169 expectedError: false, 170 expectedErrMsg: "", 171 }, 172 { 173 name: "valid zone", 174 mpool: func() *openstack.MachinePool { 175 mp := validMachinePool() 176 mp.Zones = []string{validZone} 177 return mp 178 }(), 179 cloudInfo: validMpoolCloudInfo(), 180 expectedError: false, 181 expectedErrMsg: "", 182 }, 183 { 184 name: "invalid zone", 185 mpool: func() *openstack.MachinePool { 186 mp := validMachinePool() 187 mp.Zones = []string{"invalid-zone"} 188 return mp 189 }(), 190 cloudInfo: validMpoolCloudInfo(), 191 expectedError: true, 192 expectedErrMsg: "Zone either does not exist in this cloud, or is not available", 193 }, 194 { 195 name: "valid compute", 196 controlPlane: false, 197 mpool: validMachinePool(), 198 cloudInfo: validMpoolCloudInfo(), 199 expectedError: false, 200 expectedErrMsg: "", 201 }, 202 { 203 name: "not found control plane flavorName", 204 controlPlane: true, 205 mpool: func() *openstack.MachinePool { 206 mp := validMachinePool() 207 mp.FlavorName = notExistFlavor 208 return mp 209 }(), 210 cloudInfo: func() *CloudInfo { 211 ci := validMpoolCloudInfo() 212 return ci 213 }(), 214 expectedError: true, 215 expectedErrMsg: "controlPlane.platform.openstack.type: Not found: \"non-existant-flavor\"", 216 }, 217 { 218 name: "not found compute flavorName", 219 mpool: func() *openstack.MachinePool { 220 mp := validMachinePool() 221 mp.FlavorName = notExistFlavor 222 return mp 223 }(), 224 cloudInfo: func() *CloudInfo { 225 ci := validMpoolCloudInfo() 226 return ci 227 }(), 228 expectedError: true, 229 expectedErrMsg: `compute\[0\].platform.openstack.type: Not found: "non-existant-flavor"`, 230 }, 231 { 232 name: "no flavor name", 233 mpool: func() *openstack.MachinePool { 234 mp := validMachinePool() 235 mp.FlavorName = "" 236 return mp 237 }(), 238 cloudInfo: func() *CloudInfo { 239 ci := validMpoolCloudInfo() 240 return ci 241 }(), 242 expectedError: true, 243 expectedErrMsg: `compute\[0\].platform.openstack.type: Required value: Flavor name must be provided`, 244 }, 245 { 246 name: "invalid control plane flavorName", 247 controlPlane: true, 248 mpool: func() *openstack.MachinePool { 249 mp := validMachinePool() 250 mp.FlavorName = invalidCtrlPlaneFlavor 251 return mp 252 }(), 253 cloudInfo: validMpoolCloudInfo(), 254 expectedError: true, 255 expectedErrMsg: "controlPlane.platform.openstack.type: Invalid value: \"invalid-control-plane-flavor\": Flavor did not meet the following minimum requirements: Must have minimum of 16384 MB RAM, had 8192 MB; Must have minimum of 4 VCPUs, had 2", 256 }, 257 { 258 name: "invalid compute flavorName", 259 controlPlane: false, 260 mpool: func() *openstack.MachinePool { 261 mp := validMachinePool() 262 mp.FlavorName = invalidComputeFlavor 263 return mp 264 }(), 265 cloudInfo: validMpoolCloudInfo(), 266 expectedError: true, 267 expectedErrMsg: `compute\[0\].platform.openstack.type: Invalid value: "invalid-compute-flavor": Flavor did not meet the following minimum requirements: Must have minimum of 25 GB Disk, had 10 GB`, 268 }, 269 { 270 name: "warning control plane flavorName", 271 controlPlane: true, 272 mpool: func() *openstack.MachinePool { 273 mp := validMachinePool() 274 mp.FlavorName = warningCtrlPlaneFlavor 275 return mp 276 }(), 277 cloudInfo: validMpoolCloudInfo(), 278 expectedWarnMsg: `Flavor does not meet the following recommended requirements: It is recommended to have 100 GB Disk, had 40 GB`, 279 }, 280 { 281 name: "warning compute flavorName", 282 controlPlane: false, 283 mpool: func() *openstack.MachinePool { 284 mp := validMachinePool() 285 mp.FlavorName = warningComputeFlavor 286 return mp 287 }(), 288 cloudInfo: validMpoolCloudInfo(), 289 expectedWarnMsg: `Flavor does not meet the following recommended requirements: It is recommended to have 100 GB Disk, had 40 GB`, 290 }, 291 { 292 name: "valid baremetal compute", 293 controlPlane: false, 294 mpool: func() *openstack.MachinePool { 295 mp := validMachinePool() 296 mp.FlavorName = baremetalFlavor 297 return mp 298 }(), 299 cloudInfo: validMpoolCloudInfo(), 300 expectedError: false, 301 expectedErrMsg: "", 302 }, 303 { 304 name: "volume too small", 305 controlPlane: false, 306 mpool: func() *openstack.MachinePool { 307 mp := invalidMachinePoolSmallVolume() 308 mp.FlavorName = invalidCtrlPlaneFlavor 309 return mp 310 }(), 311 cloudInfo: validMpoolCloudInfo(), 312 expectedError: true, 313 expectedErrMsg: "Volume size must be greater than 25 GB to use root volumes, had 10 GB", 314 }, 315 { 316 name: "volume not recommended", 317 controlPlane: false, 318 mpool: func() *openstack.MachinePool { 319 mp := warningMachinePoolMediumVolume() 320 mp.FlavorName = invalidCtrlPlaneFlavor 321 return mp 322 }(), 323 cloudInfo: validMpoolCloudInfo(), 324 expectedWarnMsg: "Volume size is recommended to be greater than 100 GB to use root volumes, had 40 GB", 325 }, 326 { 327 name: "volume big enough", 328 controlPlane: false, 329 mpool: func() *openstack.MachinePool { 330 mp := validMachinePoolLargeVolume() 331 mp.FlavorName = invalidCtrlPlaneFlavor 332 return mp 333 }(), 334 cloudInfo: validMpoolCloudInfo(), 335 expectedError: false, 336 expectedErrMsg: "", 337 }, 338 { 339 name: "valid root volume az", 340 controlPlane: false, 341 mpool: func() *openstack.MachinePool { 342 mp := validMachinePoolLargeVolume() 343 return mp 344 }(), 345 cloudInfo: validMpoolCloudInfo(), 346 expectedError: false, 347 expectedErrMsg: "", 348 }, 349 { 350 name: "invalid root volume az", 351 controlPlane: false, 352 mpool: func() *openstack.MachinePool { 353 mp := validMachinePoolLargeVolume() 354 mp.RootVolume.Zones = []string{invalidZone} 355 return mp 356 }(), 357 cloudInfo: validMpoolCloudInfo(), 358 expectedError: true, 359 expectedErrMsg: `compute\[0\].platform.openstack.rootVolume.zones.zone\[0\]: Invalid value: \"invalid-zone\": Zone either does not exist in this cloud, or is not available`, 360 }, 361 { 362 name: "volume and compute zones number mismatch", 363 controlPlane: false, 364 mpool: func() *openstack.MachinePool { 365 mp := validMachinePoolLargeVolume() 366 mp.RootVolume.Zones = []string{"AZ1", "AZ2"} 367 return mp 368 }(), 369 cloudInfo: validMpoolCloudInfo(), 370 expectedError: true, 371 expectedErrMsg: `compute\[0\].platform.openstack.rootVolume.zones: Invalid value: \[\]string{"AZ1", "AZ2"}: there must be either just one volume availability zone common to all nodes or the number of compute and volume availability zones must be equal`, 372 }, 373 { 374 name: "invalid volume types", 375 controlPlane: true, 376 mpool: func() *openstack.MachinePool { 377 mp := validMachinePoolLargeVolume() 378 mp.RootVolume.Types = invalidVolumeTypes 379 return mp 380 }(), 381 cloudInfo: validMpoolCloudInfo(), 382 expectedError: true, 383 expectedErrMsg: "controlPlane.platform.openstack.rootVolume.types: Invalid value: \"invalid-type\": Volume type either does not exist in this cloud, or is not available", 384 }, 385 { 386 name: "valid volume type", 387 controlPlane: true, 388 mpool: func() *openstack.MachinePool { 389 mp := validMachinePoolLargeVolume() 390 mp.RootVolume.DeprecatedType = volumeType 391 mp.RootVolume.Types = []string{} 392 return mp 393 }(), 394 cloudInfo: validMpoolCloudInfo(), 395 expectedError: false, 396 expectedErrMsg: "", 397 }, 398 { 399 name: "valid volume types", 400 controlPlane: true, 401 mpool: func() *openstack.MachinePool { 402 mp := validMachinePoolLargeVolume() 403 mp.RootVolume.Types = volumeTypes 404 return mp 405 }(), 406 cloudInfo: validMpoolCloudInfo(), 407 expectedError: false, 408 expectedErrMsg: "", 409 }, 410 } 411 412 for _, tc := range cases { 413 t.Run(tc.name, func(t *testing.T) { 414 var fieldPath *field.Path 415 if tc.controlPlane { 416 fieldPath = field.NewPath("controlPlane", "platform", "openstack") 417 } else { 418 fieldPath = field.NewPath("compute").Index(0).Child("platform", "openstack") 419 } 420 421 hook := logrusTest.NewGlobal() 422 aggregatedErrors := ValidateMachinePool(tc.mpool, tc.cloudInfo, tc.controlPlane, fieldPath).ToAggregate() 423 if tc.expectedError { 424 assert.Regexp(t, tc.expectedErrMsg, aggregatedErrors) 425 } else { 426 assert.NoError(t, aggregatedErrors) 427 } 428 if len(tc.expectedWarnMsg) > 0 { 429 assert.Regexp(t, tc.expectedWarnMsg, hook.LastEntry().Message) 430 } 431 }) 432 } 433 }