github.com/openshift/installer@v1.4.17/pkg/asset/installconfig/aws/validation_test.go (about) 1 package aws 2 3 import ( 4 "context" 5 "fmt" 6 "os" 7 "sort" 8 "testing" 9 10 "github.com/aws/aws-sdk-go/service/ec2" 11 "github.com/aws/aws-sdk-go/service/route53" 12 "github.com/golang/mock/gomock" 13 "github.com/stretchr/testify/assert" 14 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 15 "k8s.io/apimachinery/pkg/util/validation/field" 16 "k8s.io/utils/ptr" 17 18 "github.com/openshift/installer/pkg/asset/installconfig/aws/mock" 19 "github.com/openshift/installer/pkg/ipnet" 20 "github.com/openshift/installer/pkg/types" 21 "github.com/openshift/installer/pkg/types/aws" 22 ) 23 24 var ( 25 validCIDR = "10.0.0.0/16" 26 validRegion = "us-east-1" 27 validCallerRef = "valid-caller-reference" 28 validDSId = "valid-delegation-set-id" 29 validNameServers = []string{"valid-name-server"} 30 validHostedZoneName = "valid-private-subnet-a" 31 invalidHostedZoneName = "invalid-hosted-zone" 32 validDomainName = "valid-base-domain" 33 invalidBaseDomain = "invalid-base-domain" 34 metaName = "ClusterMetaName" 35 36 publishInternal = func(ic *types.InstallConfig) { ic.Publish = types.InternalPublishingStrategy } 37 clearHostedZone = func(ic *types.InstallConfig) { ic.AWS.HostedZone = "" } 38 invalidateHostedZone = func(ic *types.InstallConfig) { ic.AWS.HostedZone = invalidHostedZoneName } 39 invalidateBaseDomain = func(ic *types.InstallConfig) { ic.BaseDomain = invalidBaseDomain } 40 clearBaseDomain = func(ic *types.InstallConfig) { ic.BaseDomain = "" } 41 invalidateRegion = func(ic *types.InstallConfig) { ic.AWS.Region = "us-east4" } 42 ) 43 44 type editFunctions []func(ic *types.InstallConfig) 45 46 func validInstallConfig() *types.InstallConfig { 47 return &types.InstallConfig{ 48 Networking: &types.Networking{ 49 MachineNetwork: []types.MachineNetworkEntry{ 50 {CIDR: *ipnet.MustParseCIDR(validCIDR)}, 51 }, 52 }, 53 BaseDomain: validDomainName, 54 Publish: types.ExternalPublishingStrategy, 55 Platform: types.Platform{ 56 AWS: &aws.Platform{ 57 Region: "us-east-1", 58 Subnets: []string{ 59 "valid-private-subnet-a", 60 "valid-private-subnet-b", 61 "valid-private-subnet-c", 62 "valid-public-subnet-a", 63 "valid-public-subnet-b", 64 "valid-public-subnet-c", 65 }, 66 HostedZone: validHostedZoneName, 67 }, 68 }, 69 ControlPlane: &types.MachinePool{ 70 Architecture: types.ArchitectureAMD64, 71 Replicas: ptr.To[int64](3), 72 Platform: types.MachinePoolPlatform{ 73 AWS: &aws.MachinePool{ 74 Zones: []string{"a", "b", "c"}, 75 }, 76 }, 77 }, 78 Compute: []types.MachinePool{{ 79 Name: types.MachinePoolComputeRoleName, 80 Architecture: types.ArchitectureAMD64, 81 Replicas: ptr.To[int64](3), 82 Platform: types.MachinePoolPlatform{ 83 AWS: &aws.MachinePool{ 84 Zones: []string{"a", "b", "c"}, 85 }, 86 }, 87 }}, 88 ObjectMeta: metav1.ObjectMeta{ 89 Name: metaName, 90 }, 91 } 92 } 93 94 // validInstallConfigEdgeSubnets returns install-config for edge compute pool 95 // for existing VPC (subnets). 96 func validInstallConfigEdgeSubnets() *types.InstallConfig { 97 ic := validInstallConfig() 98 edgeSubnets := validEdgeSubnets() 99 for subnet := range edgeSubnets { 100 ic.Platform.AWS.Subnets = append(ic.Platform.AWS.Subnets, subnet) 101 } 102 ic.Compute = append(ic.Compute, types.MachinePool{ 103 Name: types.MachinePoolEdgeRoleName, 104 Platform: types.MachinePoolPlatform{ 105 AWS: &aws.MachinePool{}, 106 }, 107 }) 108 return ic 109 } 110 111 func validAvailZones() []string { 112 return []string{"a", "b", "c"} 113 } 114 115 func validAvailZonesWithEdge() []string { 116 return []string{"a", "b", "c", "edge-a", "edge-b", "edge-c"} 117 } 118 119 func validAvailZonesOnlyEdge() []string { 120 return []string{"edge-a", "edge-b", "edge-c"} 121 } 122 123 func validPrivateSubnets() Subnets { 124 return Subnets{ 125 "valid-private-subnet-a": { 126 Zone: &Zone{Name: "a"}, 127 CIDR: "10.0.1.0/24", 128 }, 129 "valid-private-subnet-b": { 130 Zone: &Zone{Name: "b"}, 131 CIDR: "10.0.2.0/24", 132 }, 133 "valid-private-subnet-c": { 134 Zone: &Zone{Name: "c"}, 135 CIDR: "10.0.3.0/24", 136 }, 137 } 138 } 139 140 func validPublicSubnets() Subnets { 141 return Subnets{ 142 "valid-public-subnet-a": { 143 Zone: &Zone{Name: "a"}, 144 CIDR: "10.0.4.0/24", 145 }, 146 "valid-public-subnet-b": { 147 Zone: &Zone{Name: "b"}, 148 CIDR: "10.0.5.0/24", 149 }, 150 "valid-public-subnet-c": { 151 Zone: &Zone{Name: "c"}, 152 CIDR: "10.0.6.0/24", 153 }, 154 } 155 } 156 157 func validEdgeSubnets() Subnets { 158 return Subnets{ 159 "valid-public-subnet-edge-a": { 160 Zone: &Zone{Name: "edge-a"}, 161 CIDR: "10.0.7.0/24", 162 }, 163 "valid-public-subnet-edge-b": { 164 Zone: &Zone{Name: "edge-b"}, 165 CIDR: "10.0.8.0/24", 166 }, 167 "valid-public-subnet-edge-c": { 168 Zone: &Zone{Name: "edge-c"}, 169 CIDR: "10.0.9.0/24", 170 }, 171 } 172 } 173 174 func validServiceEndpoints() []aws.ServiceEndpoint { 175 return []aws.ServiceEndpoint{{ 176 Name: "ec2", 177 URL: "e2e.local", 178 }, { 179 Name: "s3", 180 URL: "e2e.local", 181 }, { 182 Name: "iam", 183 URL: "e2e.local", 184 }, { 185 Name: "elasticloadbalancing", 186 URL: "e2e.local", 187 }, { 188 Name: "tagging", 189 URL: "e2e.local", 190 }, { 191 Name: "route53", 192 URL: "e2e.local", 193 }, { 194 Name: "sts", 195 URL: "e2e.local", 196 }} 197 } 198 199 func invalidServiceEndpoint() []aws.ServiceEndpoint { 200 return []aws.ServiceEndpoint{{ 201 Name: "testing", 202 URL: "testing", 203 }, { 204 Name: "test", 205 URL: "http://testing.non", 206 }} 207 } 208 209 func validInstanceTypes() map[string]InstanceType { 210 return map[string]InstanceType{ 211 "t2.small": { 212 DefaultVCpus: 1, 213 MemInMiB: 2048, 214 Arches: []string{ec2.ArchitectureTypeX8664}, 215 }, 216 "m5.large": { 217 DefaultVCpus: 2, 218 MemInMiB: 8192, 219 Arches: []string{ec2.ArchitectureTypeX8664}, 220 }, 221 "m5.xlarge": { 222 DefaultVCpus: 4, 223 MemInMiB: 16384, 224 Arches: []string{ec2.ArchitectureTypeX8664}, 225 }, 226 "m6g.xlarge": { 227 DefaultVCpus: 4, 228 MemInMiB: 16384, 229 Arches: []string{ec2.ArchitectureTypeArm64}, 230 }, 231 } 232 } 233 234 func createBaseDomainHostedZone() route53.HostedZone { 235 return route53.HostedZone{ 236 CallerReference: &validCallerRef, 237 Id: &validDSId, 238 Name: &validDomainName, 239 } 240 } 241 242 func createValidHostedZone() route53.GetHostedZoneOutput { 243 ptrValidNameServers := []*string{} 244 for i := range validNameServers { 245 ptrValidNameServers = append(ptrValidNameServers, &validNameServers[i]) 246 } 247 248 validDelegationSet := route53.DelegationSet{CallerReference: &validCallerRef, Id: &validDSId, NameServers: ptrValidNameServers} 249 validHostedZone := route53.HostedZone{CallerReference: &validCallerRef, Id: &validDSId, Name: &validHostedZoneName} 250 validVPCs := []*route53.VPC{{VPCId: &validHostedZoneName, VPCRegion: &validRegion}} 251 252 return route53.GetHostedZoneOutput{ 253 DelegationSet: &validDelegationSet, 254 HostedZone: &validHostedZone, 255 VPCs: validVPCs, 256 } 257 } 258 259 func TestValidate(t *testing.T) { 260 tests := []struct { 261 name string 262 installConfig *types.InstallConfig 263 availZones []string 264 edgeZones []string 265 privateSubnets Subnets 266 publicSubnets Subnets 267 edgeSubnets Subnets 268 instanceTypes map[string]InstanceType 269 proxy string 270 expectErr string 271 }{{ 272 name: "valid no byo", 273 installConfig: func() *types.InstallConfig { 274 c := validInstallConfig() 275 c.Platform.AWS = &aws.Platform{Region: "us-east-1"} 276 return c 277 }(), 278 availZones: validAvailZones(), 279 }, { 280 name: "valid no byo", 281 installConfig: func() *types.InstallConfig { 282 c := validInstallConfig() 283 c.Platform.AWS.Subnets = nil 284 return c 285 }(), 286 availZones: validAvailZones(), 287 }, { 288 name: "valid no byo", 289 installConfig: func() *types.InstallConfig { 290 c := validInstallConfig() 291 c.Platform.AWS.Subnets = []string{} 292 return c 293 }(), 294 availZones: validAvailZones(), 295 }, { 296 name: "valid byo", 297 installConfig: validInstallConfig(), 298 availZones: validAvailZones(), 299 privateSubnets: validPrivateSubnets(), 300 publicSubnets: validPublicSubnets(), 301 }, { 302 name: "valid byo", 303 installConfig: validInstallConfigEdgeSubnets(), 304 availZones: validAvailZones(), 305 privateSubnets: validPrivateSubnets(), 306 publicSubnets: validPublicSubnets(), 307 edgeSubnets: validEdgeSubnets(), 308 }, { 309 name: "valid byo", 310 installConfig: func() *types.InstallConfig { 311 c := validInstallConfig() 312 c.Publish = types.InternalPublishingStrategy 313 c.Platform.AWS.Subnets = []string{ 314 "valid-private-subnet-a", 315 "valid-private-subnet-b", 316 "valid-private-subnet-c", 317 } 318 return c 319 }(), 320 availZones: validAvailZones(), 321 privateSubnets: validPrivateSubnets(), 322 }, { 323 name: "valid instance types", 324 installConfig: func() *types.InstallConfig { 325 c := validInstallConfig() 326 c.Platform.AWS = &aws.Platform{ 327 Region: "us-east-1", 328 DefaultMachinePlatform: &aws.MachinePool{ 329 InstanceType: "m5.xlarge", 330 }, 331 } 332 c.ControlPlane.Platform.AWS.InstanceType = "m5.xlarge" 333 c.Compute[0].Platform.AWS.InstanceType = "m5.large" 334 return c 335 }(), 336 availZones: validAvailZones(), 337 instanceTypes: validInstanceTypes(), 338 }, { 339 name: "invalid control plane instance type", 340 installConfig: func() *types.InstallConfig { 341 c := validInstallConfig() 342 c.Platform.AWS = &aws.Platform{Region: "us-east-1"} 343 c.ControlPlane.Platform.AWS.InstanceType = "t2.small" 344 c.Compute[0].Platform.AWS.InstanceType = "m5.large" 345 return c 346 }(), 347 availZones: validAvailZones(), 348 instanceTypes: validInstanceTypes(), 349 expectErr: `^\Q[controlPlane.platform.aws.type: Invalid value: "t2.small": instance type does not meet minimum resource requirements of 4 vCPUs, controlPlane.platform.aws.type: Invalid value: "t2.small": instance type does not meet minimum resource requirements of 16384 MiB Memory]\E$`, 350 }, { 351 name: "invalid compute instance type", 352 installConfig: func() *types.InstallConfig { 353 c := validInstallConfig() 354 c.Platform.AWS = &aws.Platform{Region: "us-east-1"} 355 c.ControlPlane.Platform.AWS.InstanceType = "m5.xlarge" 356 c.Compute[0].Platform.AWS.InstanceType = "t2.small" 357 return c 358 }(), 359 availZones: validAvailZones(), 360 instanceTypes: validInstanceTypes(), 361 expectErr: `^\Q[compute[0].platform.aws.type: Invalid value: "t2.small": instance type does not meet minimum resource requirements of 2 vCPUs, compute[0].platform.aws.type: Invalid value: "t2.small": instance type does not meet minimum resource requirements of 8192 MiB Memory]\E$`, 362 }, { 363 name: "undefined compute instance type", 364 installConfig: func() *types.InstallConfig { 365 c := validInstallConfig() 366 c.Platform.AWS = &aws.Platform{Region: "us-east-1"} 367 c.Compute[0].Platform.AWS.InstanceType = "m5.dummy" 368 return c 369 }(), 370 availZones: validAvailZones(), 371 instanceTypes: validInstanceTypes(), 372 expectErr: `^\Qcompute[0].platform.aws.type: Invalid value: "m5.dummy": instance type m5.dummy not found\E$`, 373 }, { 374 name: "mismatched instance architecture", 375 installConfig: func() *types.InstallConfig { 376 c := validInstallConfig() 377 c.Platform.AWS = &aws.Platform{ 378 Region: "us-east-1", 379 DefaultMachinePlatform: &aws.MachinePool{InstanceType: "m5.xlarge"}, 380 } 381 c.ControlPlane.Architecture = types.ArchitectureARM64 382 c.Compute[0].Platform.AWS.InstanceType = "m6g.xlarge" 383 c.Compute[0].Architecture = types.ArchitectureAMD64 384 return c 385 }(), 386 availZones: validAvailZones(), 387 instanceTypes: validInstanceTypes(), 388 expectErr: `^\[controlPlane.platform.aws.type: Invalid value: "m5.xlarge": instance type supported architectures \[amd64\] do not match specified architecture arm64, compute\[0\].platform.aws.type: Invalid value: "m6g.xlarge": instance type supported architectures \[arm64\] do not match specified architecture amd64\]$`, 389 }, { 390 name: "invalid no private subnets", 391 installConfig: func() *types.InstallConfig { 392 c := validInstallConfig() 393 c.Platform.AWS.Subnets = []string{ 394 "valid-public-subnet-a", 395 "valid-public-subnet-b", 396 "valid-public-subnet-c", 397 } 398 return c 399 }(), 400 availZones: validAvailZones(), 401 publicSubnets: validPublicSubnets(), 402 expectErr: `^\[platform\.aws\.subnets: Invalid value: \[\]string{\"valid-public-subnet-a\", \"valid-public-subnet-b\", \"valid-public-subnet-c\"}: No private subnets found, controlPlane\.platform\.aws\.zones: Invalid value: \[\]string{\"a\", \"b\", \"c\"}: No subnets provided for zones \[a b c\], compute\[0\]\.platform\.aws\.zones: Invalid value: \[\]string{\"a\", \"b\", \"c\"}: No subnets provided for zones \[a b c\]\]$`, 403 }, { 404 name: "invalid no public subnets", 405 installConfig: func() *types.InstallConfig { 406 c := validInstallConfig() 407 c.Platform.AWS.Subnets = []string{ 408 "valid-private-subnet-a", 409 "valid-private-subnet-b", 410 "valid-private-subnet-c", 411 } 412 return c 413 }(), 414 availZones: validAvailZones(), 415 privateSubnets: validPrivateSubnets(), 416 expectErr: `^platform\.aws\.subnets: Invalid value: \[\]string{\"valid-private-subnet-a\", \"valid-private-subnet-b\", \"valid-private-subnet-c\"}: No public subnet provided for zones \[a b c\]$`, 417 }, { 418 name: "invalid cidr does not belong to machine CIDR", 419 installConfig: func() *types.InstallConfig { 420 c := validInstallConfig() 421 c.Platform.AWS.Subnets = append(c.Platform.AWS.Subnets, "invalid-cidr-subnet") 422 return c 423 }(), 424 availZones: func() []string { 425 zones := validAvailZones() 426 return append(zones, "zone-for-invalid-cidr-subnet") 427 }(), 428 privateSubnets: validPrivateSubnets(), 429 publicSubnets: func() Subnets { 430 s := validPublicSubnets() 431 s["invalid-cidr-subnet"] = Subnet{ 432 Zone: &Zone{Name: "zone-for-invalid-cidr-subnet"}, 433 CIDR: "192.168.126.0/24", 434 } 435 return s 436 }(), 437 expectErr: `^platform\.aws\.subnets\[6\]: Invalid value: \"invalid-cidr-subnet\": subnet's CIDR range start 192.168.126.0 is outside of the specified machine networks$`, 438 }, { 439 name: "invalid cidr does not belong to machine CIDR", 440 installConfig: func() *types.InstallConfig { 441 c := validInstallConfig() 442 c.Platform.AWS.Subnets = append(c.Platform.AWS.Subnets, "invalid-private-cidr-subnet", "invalid-public-cidr-subnet") 443 return c 444 }(), 445 availZones: func() []string { 446 zones := validAvailZones() 447 return append(zones, "zone-for-invalid-cidr-subnet") 448 }(), 449 privateSubnets: func() Subnets { 450 s := validPrivateSubnets() 451 s["invalid-private-cidr-subnet"] = Subnet{ 452 Zone: &Zone{Name: "zone-for-invalid-cidr-subnet"}, 453 CIDR: "192.168.126.0/24", 454 } 455 return s 456 }(), 457 publicSubnets: func() Subnets { 458 s := validPublicSubnets() 459 s["invalid-public-cidr-subnet"] = Subnet{ 460 Zone: &Zone{Name: "zone-for-invalid-cidr-subnet"}, 461 CIDR: "192.168.127.0/24", 462 } 463 return s 464 }(), 465 expectErr: `^\[platform\.aws\.subnets\[6\]: Invalid value: \"invalid-private-cidr-subnet\": subnet's CIDR range start 192.168.126.0 is outside of the specified machine networks, platform\.aws\.subnets\[7\]: Invalid value: \"invalid-public-cidr-subnet\": subnet's CIDR range start 192.168.127.0 is outside of the specified machine networks\]$`, 466 }, { 467 name: "invalid missing public subnet in a zone", 468 installConfig: func() *types.InstallConfig { 469 c := validInstallConfig() 470 c.Platform.AWS.Subnets = append(c.Platform.AWS.Subnets, "no-matching-public-private-zone") 471 return c 472 }(), 473 availZones: validAvailZones(), 474 privateSubnets: func() Subnets { 475 s := validPrivateSubnets() 476 s["no-matching-public-private-zone"] = Subnet{ 477 Zone: &Zone{Name: "f"}, 478 CIDR: "10.0.7.0/24", 479 } 480 return s 481 }(), 482 publicSubnets: validPublicSubnets(), 483 expectErr: `^platform\.aws\.subnets: Invalid value: \[\]string{\"valid-private-subnet-a\", \"valid-private-subnet-b\", \"valid-private-subnet-c\", \"valid-public-subnet-a\", \"valid-public-subnet-b\", \"valid-public-subnet-c\", \"no-matching-public-private-zone\"}: No public subnet provided for zones \[f\]$`, 484 }, { 485 name: "invalid multiple private in same zone", 486 installConfig: func() *types.InstallConfig { 487 c := validInstallConfig() 488 c.Platform.AWS.Subnets = append(c.Platform.AWS.Subnets, "valid-private-zone-c-2") 489 return c 490 }(), 491 availZones: validAvailZones(), 492 privateSubnets: func() Subnets { 493 s := validPrivateSubnets() 494 s["valid-private-zone-c-2"] = Subnet{ 495 Zone: &Zone{Name: "c"}, 496 CIDR: "10.0.7.0/24", 497 } 498 return s 499 }(), 500 publicSubnets: validPublicSubnets(), 501 expectErr: `^platform\.aws\.subnets\[6\]: Invalid value: \"valid-private-zone-c-2\": private subnet valid-private-subnet-c is also in zone c$`, 502 }, { 503 name: "invalid multiple public in same zone", 504 installConfig: func() *types.InstallConfig { 505 c := validInstallConfig() 506 c.Platform.AWS.Subnets = append(c.Platform.AWS.Subnets, "valid-public-zone-c-2") 507 return c 508 }(), 509 availZones: validAvailZones(), 510 privateSubnets: validPrivateSubnets(), 511 publicSubnets: func() Subnets { 512 s := validPublicSubnets() 513 s["valid-public-zone-c-2"] = Subnet{ 514 Zone: &Zone{Name: "c"}, 515 CIDR: "10.0.7.0/24", 516 } 517 return s 518 }(), 519 expectErr: `^platform\.aws\.subnets\[6\]: Invalid value: \"valid-public-zone-c-2\": public subnet valid-public-subnet-c is also in zone c$`, 520 }, { 521 name: "invalid multiple public edge in same zone", 522 installConfig: func() *types.InstallConfig { 523 c := validInstallConfigEdgeSubnets() 524 c.Platform.AWS.Subnets = append(c.Platform.AWS.Subnets, "valid-public-zone-edge-c-2") 525 return c 526 }(), 527 availZones: validAvailZonesWithEdge(), 528 privateSubnets: validPrivateSubnets(), 529 publicSubnets: validPublicSubnets(), 530 edgeSubnets: func() Subnets { 531 s := validEdgeSubnets() 532 s["valid-public-zone-edge-c-2"] = Subnet{ 533 Zone: &Zone{Name: "edge-c", Type: aws.LocalZoneType}, 534 CIDR: "10.0.9.0/24", 535 } 536 return s 537 }(), 538 expectErr: `^platform\.aws\.subnets\[9\]: Invalid value: \"valid-public-zone-edge-c-2\": edge subnet valid-public-subnet-edge-c is also in zone edge-c$`, 539 }, { 540 name: "invalid edge pool missing valid subnets", 541 installConfig: validInstallConfigEdgeSubnets(), 542 availZones: validAvailZonesWithEdge(), 543 privateSubnets: validPrivateSubnets(), 544 publicSubnets: validPublicSubnets(), 545 edgeSubnets: Subnets{}, 546 expectErr: `^compute\[1\]\.platform\.aws: Required value: the provided subnets must include valid subnets for the specified edge zones$`, 547 }, { 548 name: "invalid edge pool missing zones", 549 installConfig: func() *types.InstallConfig { 550 ic := validInstallConfig() 551 ic.Platform.AWS.Subnets = []string{} 552 ic.ControlPlane = &types.MachinePool{} 553 edgePool := types.MachinePool{ 554 Name: types.MachinePoolEdgeRoleName, 555 Platform: types.MachinePoolPlatform{ 556 AWS: &aws.MachinePool{}, 557 }, 558 } 559 ic.Compute = []types.MachinePool{edgePool} 560 return ic 561 }(), 562 expectErr: `^compute\[0\]\.platform\.aws: Required value: zone is required when using edge machine pools$`, 563 }, { 564 name: "invalid edge pool empty zones", 565 installConfig: func() *types.InstallConfig { 566 ic := validInstallConfig() 567 ic.Platform.AWS.Subnets = []string{} 568 ic.ControlPlane = &types.MachinePool{} 569 edgePool := types.MachinePool{ 570 Name: types.MachinePoolEdgeRoleName, 571 Platform: types.MachinePoolPlatform{ 572 AWS: &aws.MachinePool{ 573 Zones: []string{}, 574 }, 575 }, 576 } 577 ic.Compute = []types.MachinePool{edgePool} 578 return ic 579 }(), 580 expectErr: `^compute\[0\]\.platform\.aws: Required value: zone is required when using edge machine pools$`, 581 }, { 582 name: "invalid edge pool missing platform definition", 583 installConfig: func() *types.InstallConfig { 584 ic := validInstallConfig() 585 ic.Platform.AWS.Subnets = []string{} 586 ic.ControlPlane = &types.MachinePool{} 587 edgePool := types.MachinePool{ 588 Name: types.MachinePoolEdgeRoleName, 589 Platform: types.MachinePoolPlatform{}, 590 } 591 ic.Compute = []types.MachinePool{edgePool} 592 return ic 593 }(), 594 expectErr: `^\[compute\[0\]\.platform\.aws: Required value: edge compute pools are only supported on the AWS platform, compute\[0\].platform.aws: Required value: zone is required when using edge machine pools\]$`, 595 }, { 596 name: "invalid edge pool missing subnets on availability zones", 597 installConfig: func() *types.InstallConfig { 598 c := validInstallConfigEdgeSubnets() 599 c.Platform.AWS.Subnets = []string{} 600 edgeSubnets := validEdgeSubnets() 601 for subnet := range edgeSubnets { 602 c.Platform.AWS.Subnets = append(c.Platform.AWS.Subnets, subnet) 603 } 604 sort.Strings(c.Platform.AWS.Subnets) 605 return c 606 }(), 607 availZones: validAvailZonesOnlyEdge(), 608 privateSubnets: Subnets{}, 609 publicSubnets: Subnets{}, 610 edgeSubnets: validEdgeSubnets(), 611 expectErr: `^\[platform\.aws\.subnets: Invalid value: \[\]string{\"valid-public-subnet-edge-a\", \"valid-public-subnet-edge-b\", \"valid-public-subnet-edge-c\"}: No private subnets found, controlPlane\.platform\.aws\.zones: Invalid value: \[\]string{\"a\", \"b\", \"c\"}: No subnets provided for zones \[a b c\], compute\[0\]\.platform\.aws\.zones: Invalid value: \[\]string{\"a\", \"b\", \"c\"}: No subnets provided for zones \[a b c\]]$`, 612 }, { 613 name: "invalid no subnet for control plane zones", 614 installConfig: func() *types.InstallConfig { 615 c := validInstallConfig() 616 c.ControlPlane.Platform.AWS.Zones = append(c.ControlPlane.Platform.AWS.Zones, "d") 617 return c 618 }(), 619 availZones: validAvailZones(), 620 privateSubnets: validPrivateSubnets(), 621 publicSubnets: validPublicSubnets(), 622 expectErr: `^controlPlane\.platform\.aws\.zones: Invalid value: \[\]string{\"a\", \"b\", \"c\", \"d\"}: No subnets provided for zones \[d\]$`, 623 }, { 624 name: "invalid no subnet for control plane zones", 625 installConfig: func() *types.InstallConfig { 626 c := validInstallConfig() 627 c.ControlPlane.Platform.AWS.Zones = append(c.ControlPlane.Platform.AWS.Zones, "d", "e") 628 return c 629 }(), 630 availZones: validAvailZones(), 631 privateSubnets: validPrivateSubnets(), 632 publicSubnets: validPublicSubnets(), 633 expectErr: `^controlPlane\.platform\.aws\.zones: Invalid value: \[\]string{\"a\", \"b\", \"c\", \"d\", \"e\"}: No subnets provided for zones \[d e\]$`, 634 }, { 635 name: "invalid no subnet for compute[0] zones", 636 installConfig: func() *types.InstallConfig { 637 c := validInstallConfig() 638 c.Compute[0].Platform.AWS.Zones = append(c.ControlPlane.Platform.AWS.Zones, "d") 639 return c 640 }(), 641 availZones: validAvailZones(), 642 privateSubnets: validPrivateSubnets(), 643 publicSubnets: validPublicSubnets(), 644 expectErr: `^compute\[0\]\.platform\.aws\.zones: Invalid value: \[\]string{\"a\", \"b\", \"c\", \"d\"}: No subnets provided for zones \[d\]$`, 645 }, { 646 name: "invalid no subnet for compute zone", 647 installConfig: func() *types.InstallConfig { 648 c := validInstallConfig() 649 c.Compute[0].Platform.AWS.Zones = append(c.ControlPlane.Platform.AWS.Zones, "d") 650 c.Compute = append(c.Compute, types.MachinePool{ 651 Architecture: types.ArchitectureAMD64, 652 Platform: types.MachinePoolPlatform{ 653 AWS: &aws.MachinePool{ 654 Zones: []string{"a", "b", "e"}, 655 }, 656 }, 657 }) 658 return c 659 }(), 660 availZones: validAvailZones(), 661 privateSubnets: validPrivateSubnets(), 662 publicSubnets: validPublicSubnets(), 663 expectErr: `^\[compute\[0\]\.platform\.aws\.zones: Invalid value: \[\]string{\"a\", \"b\", \"c\", \"d\"}: No subnets provided for zones \[d\], compute\[1\]\.platform\.aws\.zones: Invalid value: \[\]string{\"a\", \"b\", \"e\"}: No subnets provided for zones \[e\]\]$`, 664 }, { 665 name: "custom region invalid service endpoints none provided", 666 installConfig: func() *types.InstallConfig { 667 c := validInstallConfig() 668 c.Platform.AWS.Region = "test-region" 669 c.Platform.AWS.AMIID = "dummy-id" 670 return c 671 }(), 672 availZones: validAvailZones(), 673 privateSubnets: validPrivateSubnets(), 674 publicSubnets: validPublicSubnets(), 675 expectErr: `^platform\.aws\.serviceEndpoints: Invalid value: (.|\n)*: \[failed to find endpoint for service "ec2": (.|\n)*, failed to find endpoint for service "elasticloadbalancing": (.|\n)*, failed to find endpoint for service "iam": (.|\n)*, failed to find endpoint for service "route53": (.|\n)*, failed to find endpoint for service "s3": (.|\n)*, failed to find endpoint for service "sts": (.|\n)*, failed to find endpoint for service "tagging": (.|\n)*\]$`, 676 }, { 677 name: "custom region invalid service endpoints some provided", 678 installConfig: func() *types.InstallConfig { 679 c := validInstallConfig() 680 c.Platform.AWS.Region = "test-region" 681 c.Platform.AWS.AMIID = "dummy-id" 682 c.Platform.AWS.ServiceEndpoints = validServiceEndpoints()[:3] 683 return c 684 }(), 685 availZones: validAvailZones(), 686 privateSubnets: validPrivateSubnets(), 687 publicSubnets: validPublicSubnets(), 688 expectErr: `^platform\.aws\.serviceEndpoints: Invalid value: (.|\n)*: \[failed to find endpoint for service "elasticloadbalancing": (.|\n)*, failed to find endpoint for service "route53": (.|\n)*, failed to find endpoint for service "sts": (.|\n)*, failed to find endpoint for service "tagging": (.|\n)*$`, 689 }, { 690 name: "custom region valid service endpoints", 691 installConfig: func() *types.InstallConfig { 692 c := validInstallConfig() 693 c.Platform.AWS.Region = "test-region" 694 c.Platform.AWS.AMIID = "dummy-id" 695 c.Platform.AWS.ServiceEndpoints = validServiceEndpoints() 696 return c 697 }(), 698 availZones: validAvailZones(), 699 privateSubnets: validPrivateSubnets(), 700 publicSubnets: validPublicSubnets(), 701 }, { 702 name: "AMI omitted for new region in standard partition", 703 installConfig: func() *types.InstallConfig { 704 c := validInstallConfig() 705 c.Platform.AWS.Region = "us-newregion-1" 706 c.Platform.AWS.ServiceEndpoints = validServiceEndpoints() 707 return c 708 }(), 709 availZones: validAvailZones(), 710 privateSubnets: validPrivateSubnets(), 711 publicSubnets: validPublicSubnets(), 712 }, { 713 name: "accept platform-level AMI", 714 installConfig: func() *types.InstallConfig { 715 c := validInstallConfig() 716 c.Platform.AWS.Region = "us-gov-east-1" 717 c.Platform.AWS.AMIID = "custom-ami" 718 return c 719 }(), 720 availZones: validAvailZones(), 721 privateSubnets: validPrivateSubnets(), 722 publicSubnets: validPublicSubnets(), 723 }, { 724 name: "accept AMI from default machine platform", 725 installConfig: func() *types.InstallConfig { 726 c := validInstallConfig() 727 c.Platform.AWS.Region = "us-gov-east-1" 728 c.Platform.AWS.DefaultMachinePlatform = &aws.MachinePool{AMIID: "custom-ami"} 729 return c 730 }(), 731 availZones: validAvailZones(), 732 privateSubnets: validPrivateSubnets(), 733 publicSubnets: validPublicSubnets(), 734 }, { 735 name: "accept AMIs specified for each machine pool", 736 installConfig: func() *types.InstallConfig { 737 c := validInstallConfig() 738 c.Platform.AWS.Region = "us-gov-east-1" 739 c.ControlPlane.Platform.AWS.AMIID = "custom-ami" 740 c.Compute[0].Platform.AWS.AMIID = "custom-ami" 741 return c 742 }(), 743 availZones: validAvailZones(), 744 privateSubnets: validPrivateSubnets(), 745 publicSubnets: validPublicSubnets(), 746 }, { 747 name: "AMI omitted for compute with no replicas", 748 installConfig: func() *types.InstallConfig { 749 c := validInstallConfig() 750 c.Platform.AWS.Region = "us-gov-east-1" 751 c.ControlPlane.Platform.AWS.AMIID = "custom-ami" 752 c.Compute[0].Replicas = ptr.To[int64](0) 753 return c 754 }(), 755 availZones: validAvailZones(), 756 privateSubnets: validPrivateSubnets(), 757 publicSubnets: validPublicSubnets(), 758 }, { 759 name: "AMI not provided for unknown region", 760 installConfig: func() *types.InstallConfig { 761 c := validInstallConfig() 762 c.Platform.AWS.Region = "test-region" 763 c.Platform.AWS.ServiceEndpoints = validServiceEndpoints() 764 return c 765 }(), 766 availZones: validAvailZones(), 767 privateSubnets: validPrivateSubnets(), 768 publicSubnets: validPublicSubnets(), 769 expectErr: `^platform\.aws\.amiID: Required value: AMI must be provided$`, 770 }, { 771 name: "invalid endpoint URL", 772 installConfig: func() *types.InstallConfig { 773 c := validInstallConfig() 774 c.Platform.AWS.Region = "us-east-1" 775 c.Platform.AWS.ServiceEndpoints = invalidServiceEndpoint() 776 c.Platform.AWS.AMIID = "custom-ami" 777 return c 778 }(), 779 availZones: validAvailZones(), 780 privateSubnets: validPrivateSubnets(), 781 publicSubnets: validPublicSubnets(), 782 expectErr: `^\Q[platform.aws.serviceEndpoints[0].url: Invalid value: "testing": Head "testing": unsupported protocol scheme "", platform.aws.serviceEndpoints[1].url: Invalid value: "http://testing.non": Head "http://testing.non": dial tcp: lookup testing.non\E.*: no such host\]$`, 783 }, { 784 name: "invalid proxy URL but valid URL", 785 installConfig: func() *types.InstallConfig { 786 c := validInstallConfig() 787 c.Platform.AWS.Region = "us-east-1" 788 c.Platform.AWS.AMIID = "custom-ami" 789 c.Platform.AWS.ServiceEndpoints = []aws.ServiceEndpoint{{Name: "test", URL: "http://testing.com"}} 790 return c 791 }(), 792 availZones: validAvailZones(), 793 privateSubnets: validPrivateSubnets(), 794 publicSubnets: validPublicSubnets(), 795 proxy: "proxy", 796 }, { 797 name: "invalid proxy URL and invalid URL", 798 installConfig: func() *types.InstallConfig { 799 c := validInstallConfig() 800 c.Platform.AWS.Region = "us-east-1" 801 c.Platform.AWS.AMIID = "custom-ami" 802 c.Platform.AWS.ServiceEndpoints = []aws.ServiceEndpoint{{Name: "test", URL: "http://test"}} 803 return c 804 }(), 805 availZones: validAvailZones(), 806 privateSubnets: validPrivateSubnets(), 807 publicSubnets: validPublicSubnets(), 808 proxy: "http://proxy.com", 809 expectErr: `^\Qplatform.aws.serviceEndpoints[0].url: Invalid value: "http://test": Head "http://test": dial tcp: lookup test\E.*: no such host$`, 810 }, { 811 name: "invalid public ipv4 pool private installation", 812 installConfig: func() *types.InstallConfig { 813 c := validInstallConfig() 814 c.Publish = types.InternalPublishingStrategy 815 c.Platform.AWS.PublicIpv4Pool = "ipv4pool-ec2-123" 816 c.Platform.AWS.Subnets = []string{} 817 return c 818 }(), 819 availZones: validAvailZones(), 820 expectErr: `^platform.aws.publicIpv4PoolId: Invalid value: "ipv4pool-ec2-123": publish strategy Internal can't be used with custom Public IPv4 Pools$`, 821 }} 822 823 for _, test := range tests { 824 t.Run(test.name, func(t *testing.T) { 825 meta := &Metadata{ 826 availabilityZones: test.availZones, 827 privateSubnets: test.privateSubnets, 828 publicSubnets: test.publicSubnets, 829 edgeSubnets: test.edgeSubnets, 830 instanceTypes: test.instanceTypes, 831 Subnets: test.installConfig.Platform.AWS.Subnets, 832 } 833 if test.proxy != "" { 834 os.Setenv("HTTP_PROXY", test.proxy) 835 } else { 836 os.Unsetenv("HTTP_PROXY") 837 } 838 err := Validate(context.TODO(), meta, test.installConfig) 839 if test.expectErr == "" { 840 assert.NoError(t, err) 841 } else { 842 if assert.Error(t, err) { 843 assert.Regexp(t, test.expectErr, err.Error()) 844 } 845 } 846 }) 847 } 848 } 849 850 func TestIsHostedZoneDomainParentOfClusterDomain(t *testing.T) { 851 cases := []struct { 852 name string 853 hostedZoneDomain string 854 clusterDomain string 855 expected bool 856 }{{ 857 name: "same", 858 hostedZoneDomain: "c.b.a.", 859 clusterDomain: "c.b.a.", 860 expected: true, 861 }, { 862 name: "strict parent", 863 hostedZoneDomain: "b.a.", 864 clusterDomain: "c.b.a.", 865 expected: true, 866 }, { 867 name: "grandparent", 868 hostedZoneDomain: "a.", 869 clusterDomain: "c.b.a.", 870 expected: true, 871 }, { 872 name: "not parent", 873 hostedZoneDomain: "f.e.d.", 874 clusterDomain: "c.b.a.", 875 expected: false, 876 }, { 877 name: "child", 878 hostedZoneDomain: "d.c.b.a.", 879 clusterDomain: "c.b.a.", 880 expected: false, 881 }, { 882 name: "suffix but not parent", 883 hostedZoneDomain: "b.a.", 884 clusterDomain: "cb.a.", 885 expected: false, 886 }} 887 for _, tc := range cases { 888 t.Run(tc.name, func(t *testing.T) { 889 zone := &route53.HostedZone{Name: &tc.hostedZoneDomain} 890 actual := isHostedZoneDomainParentOfClusterDomain(zone, tc.clusterDomain) 891 assert.Equal(t, tc.expected, actual) 892 }) 893 } 894 } 895 896 func TestValidateForProvisioning(t *testing.T) { 897 cases := []struct { 898 name string 899 edits editFunctions 900 expectedErr string 901 }{{ 902 // This really should test for nil, as nothing happened, but no errors were provided 903 name: "internal publish strategy no hosted zone", 904 edits: editFunctions{publishInternal, clearHostedZone}, 905 }, { 906 name: "external publish strategy no hosted zone invalid (empty) base domain", 907 edits: editFunctions{clearHostedZone, clearBaseDomain}, 908 expectedErr: "baseDomain: Invalid value: \"\": cannot find base domain", 909 }, { 910 name: "external publish strategy no hosted zone invalid base domain", 911 edits: editFunctions{clearHostedZone, invalidateBaseDomain}, 912 expectedErr: "baseDomain: Invalid value: \"invalid-base-domain\": cannot find base domain", 913 }, { 914 name: "external publish strategy no hosted zone valid base domain", 915 edits: editFunctions{clearHostedZone}, 916 }, { 917 name: "internal publish strategy valid hosted zone", 918 edits: editFunctions{publishInternal}, 919 }, { 920 name: "internal publish strategy invalid hosted zone", 921 edits: editFunctions{publishInternal, invalidateHostedZone}, 922 expectedErr: "aws.hostedZone: Invalid value: \"invalid-hosted-zone\": unable to retrieve hosted zone", 923 }, { 924 name: "external publish strategy valid hosted zone", 925 }, { 926 name: "external publish strategy invalid hosted zone", 927 edits: editFunctions{invalidateHostedZone}, 928 expectedErr: "aws.hostedZone: Invalid value: \"invalid-hosted-zone\": unable to retrieve hosted zone", 929 }} 930 931 mockCtrl := gomock.NewController(t) 932 defer mockCtrl.Finish() 933 934 route53Client := mock.NewMockAPI(mockCtrl) 935 936 validHostedZoneOutput := createValidHostedZone() 937 validDomainOutput := createBaseDomainHostedZone() 938 939 route53Client.EXPECT().GetBaseDomain(validDomainName).Return(&validDomainOutput, nil).AnyTimes() 940 route53Client.EXPECT().GetBaseDomain("").Return(nil, fmt.Errorf("invalid value: \"\": cannot find base domain")).AnyTimes() 941 route53Client.EXPECT().GetBaseDomain(invalidBaseDomain).Return(nil, fmt.Errorf("invalid value: \"%s\": cannot find base domain", invalidBaseDomain)).AnyTimes() 942 943 route53Client.EXPECT().ValidateZoneRecords(&validDomainOutput, gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(field.ErrorList{}).AnyTimes() 944 route53Client.EXPECT().ValidateZoneRecords(gomock.Any(), validHostedZoneName, gomock.Any(), gomock.Any(), gomock.Any()).Return(field.ErrorList{}).AnyTimes() 945 946 // An invalid hosted zone should provide an error 947 route53Client.EXPECT().GetHostedZone(validHostedZoneName, gomock.Any()).Return(&validHostedZoneOutput, nil).AnyTimes() 948 route53Client.EXPECT().GetHostedZone(gomock.Not(validHostedZoneName), gomock.Any()).Return(nil, fmt.Errorf("invalid value: \"invalid-hosted-zone\": cannot find hosted zone")).AnyTimes() 949 950 for _, test := range cases { 951 t.Run(test.name, func(t *testing.T) { 952 editedInstallConfig := validInstallConfig() 953 for _, edit := range test.edits { 954 edit(editedInstallConfig) 955 } 956 957 meta := &Metadata{ 958 availabilityZones: validAvailZones(), 959 privateSubnets: validPrivateSubnets(), 960 publicSubnets: validPublicSubnets(), 961 instanceTypes: validInstanceTypes(), 962 Region: editedInstallConfig.AWS.Region, 963 vpc: "valid-private-subnet-a", 964 Subnets: editedInstallConfig.Platform.AWS.Subnets, 965 } 966 967 err := ValidateForProvisioning(route53Client, editedInstallConfig, meta) 968 if test.expectedErr == "" { 969 assert.NoError(t, err) 970 } else { 971 if assert.Error(t, err) { 972 assert.Regexp(t, test.expectedErr, err.Error()) 973 } 974 } 975 }) 976 } 977 } 978 979 func TestGetSubDomainDNSRecords(t *testing.T) { 980 cases := []struct { 981 name string 982 baseDomain string 983 problematicRecords []string 984 expectedErr string 985 }{{ 986 name: "empty cluster domain", 987 expectedErr: fmt.Sprintf("hosted zone domain %s is not a parent of the cluster domain %s", validDomainName, ""), 988 }, { 989 name: "period cluster domain", 990 baseDomain: ".", 991 expectedErr: fmt.Sprintf("hosted zone domain %s is not a parent of the cluster domain %s", validDomainName, "."), 992 }, { 993 name: "valid dns record no problems", 994 baseDomain: validDomainName + ".", 995 }, { 996 name: "valid dns record with problems", 997 baseDomain: validDomainName, 998 problematicRecords: []string{"test1.ClusterMetaName.valid-base-domain."}, 999 }, { 1000 name: "valid dns record with skipped problems", 1001 baseDomain: validDomainName, 1002 problematicRecords: []string{"test1.ClusterMetaName.valid-base-domain.", "ClusterMetaName.xxxxx-xxxx-xxxxxx."}, 1003 }, 1004 } 1005 1006 validDomainOutput := createBaseDomainHostedZone() 1007 1008 mockCtrl := gomock.NewController(t) 1009 defer mockCtrl.Finish() 1010 route53Client := mock.NewMockAPI(mockCtrl) 1011 1012 for _, test := range cases { 1013 1014 t.Run(test.name, func(t *testing.T) { 1015 1016 ic := validInstallConfig() 1017 ic.BaseDomain = test.baseDomain 1018 1019 if test.expectedErr != "" { 1020 if test.problematicRecords == nil { 1021 route53Client.EXPECT().GetSubDomainDNSRecords(&validDomainOutput, ic, gomock.Any()).Return(nil, fmt.Errorf(test.expectedErr)).AnyTimes() 1022 } else { 1023 // mimic the results of what should happen in the internal function passed to 1024 // ListResourceRecordSetsPages by GetSubDomainDNSRecords. Skip certain problematicRecords 1025 returnedProblems := make([]string, 0, len(test.problematicRecords)) 1026 expectedName := ic.ClusterDomain() + "." 1027 for _, pr := range test.problematicRecords { 1028 if len(pr) != len(expectedName) { 1029 returnedProblems = append(returnedProblems, pr) 1030 } 1031 } 1032 route53Client.EXPECT().GetSubDomainDNSRecords(&validDomainOutput, ic, gomock.Any()).Return(returnedProblems, fmt.Errorf(test.expectedErr)).AnyTimes() 1033 } 1034 } else { 1035 route53Client.EXPECT().GetSubDomainDNSRecords(&validDomainOutput, ic, gomock.Any()).Return(nil, nil).AnyTimes() 1036 } 1037 1038 _, err := route53Client.GetSubDomainDNSRecords(&validDomainOutput, ic, nil) 1039 if test.expectedErr == "" { 1040 assert.NoError(t, err) 1041 } else { 1042 if assert.Error(t, err) { 1043 assert.Regexp(t, test.expectedErr, err.Error()) 1044 } 1045 } 1046 }) 1047 } 1048 } 1049 1050 func TestSkipRecords(t *testing.T) { 1051 cases := []struct { 1052 name string 1053 recordName string 1054 expectedResult bool 1055 }{{ 1056 name: "record not part of cluster", 1057 recordName: fmt.Sprintf("%s.test.domain.", metaName), 1058 expectedResult: true, 1059 }, { 1060 name: "record and cluster domain are same", 1061 recordName: fmt.Sprintf("%s.%s.", metaName, validDomainName), 1062 expectedResult: true, 1063 }, { 1064 name: "record not part of cluster bad suffix", 1065 // The parent below does not have a dot following it on purpose - do not Remove 1066 recordName: fmt.Sprintf("parent%s.%s.", metaName, validDomainName), 1067 expectedResult: true, 1068 }, { 1069 name: "record part of cluster bad suffix", 1070 // The parent below does not have a dot following it on purpose - do not Remove 1071 recordName: fmt.Sprintf("parent.%s.%s.", metaName, validDomainName), 1072 expectedResult: false, 1073 }, 1074 } 1075 1076 // create the dottedClusterDomain in the same manner that it will be used in GetSubDomainDNSRecords 1077 ic := validInstallConfig() 1078 ic.BaseDomain = validDomainName 1079 dottedClusterDomain := ic.ClusterDomain() + "." 1080 1081 for _, test := range cases { 1082 t.Run(test.name, func(t *testing.T) { 1083 assert.Equal(t, test.expectedResult, skipRecord(test.recordName, dottedClusterDomain)) 1084 }) 1085 } 1086 }