From 351454320896762712a89aae48c1daa1a23e0311 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Thu, 23 May 2019 10:59:34 -0700 Subject: [PATCH] pkg/types/aws/defaults/platform: Default us-west-2 to m5 Some regions have per-zone support differences for available instance classes. For example, us-west-2 has m4 instances in zones a through c, but not in zone d. It supports m5 instances in all four zones [1]. Unfortunately, we can't switch to just using the reserved-instance offerings, because Paris claims those for m4.large in all three of its zones: $ AWS_PROFILE=ci aws --region eu-west-3 ec2 describe-reserved-instances-offerings --instance-tenancy default --instance-type m4.large --product-description 'Linux/UNIX' --filters Name=scope,Values='Availability Zone' | jq -r '[.ReservedInstancesOfferings[].AvailabilityZone] | sort | unique[]' eu-west-3a eu-west-3b eu-west-3c but still does not price on-demand compute instances for that region: $ AWS_PROFILE=ci aws --region us-east-1 pricing get-products --service-code AmazonEC2 --filters Field=tenancy,Type=TERM_MATCH,Value=Shared Field=productFamily,Type=TERM_MATCH,Value='Compute Instance' Field=operatingSystem,Type=TERM_MATCH,Value=Linux Field=instanceFamily,Type=TERM_MATCH,Value='General purpose' | jq -r '[.PriceList[] | fromjson | .product.attributes | select(.instanceType == "m4.large").location] | sort | unique[]' AWS GovCloud (US) Asia Pacific (Mumbai) Asia Pacific (Osaka-Local) Asia Pacific (Seoul) Asia Pacific (Singapore) Asia Pacific (Sydney) Asia Pacific (Tokyo) Canada (Central) EU (Frankfurt) EU (Ireland) EU (London) South America (Sao Paulo) US East (N. Virginia) US East (Ohio) US West (N. California) US West (Oregon) So the new test here only assumes a match when both APIs claim support for the instance class in a given region to avoid re-breaking Paris [2]. After rerolling the test to account for that sort of thing, I made the default change to address: $ AWS_PROFILE=ci go test -count 1 . --- FAIL: TestGetDefaultInstanceClass (57.36s) --- FAIL: TestGetDefaultInstanceClass/US_West_(Oregon) (1.22s) default_instance_class_test.go:176: map[m4:map[us-west-2c:{} us-west-2a:{} us-west-2b:{}] m5:map[us-west-2b:{} us-west-2c:{} us-west-2d:{} us-west-2a:{}]] default_instance_class_test.go:177: Error Trace: default_instance_class_test.go:177 Error: Not equal: expected: "m4" actual : "m5" Diff: --- Expected +++ Actual @@ -1 +1 @@ -m4 +m5 Test: TestGetDefaultInstanceClass/US_West_(Oregon) FAIL FAIL github.com/openshift/installer/platformtests/aws 57.374s The zone-error fallback covers regions that I don't have access too, where requesting zones returns errors like (us-gov-east-1): AuthFailure: AWS was not able to validate the provided access credentials and (ap-northeast-3): OptInRequired: You are not subscribed to this service. Please go to http://aws.amazon.com to subscribe. [1]: https://bugzilla.redhat.com/show_bug.cgi?id=1713157#c1 [2]: https://github.com/openshift/installer/issues/1127 --- pkg/types/aws/defaults/platform.go | 1 + .../aws/default_instance_class_test.go | 100 +++++++++++++++--- 2 files changed, 84 insertions(+), 17 deletions(-) diff --git a/pkg/types/aws/defaults/platform.go b/pkg/types/aws/defaults/platform.go index 5907bc75d9c..6c591d23cb0 100644 --- a/pkg/types/aws/defaults/platform.go +++ b/pkg/types/aws/defaults/platform.go @@ -10,6 +10,7 @@ var ( "eu-north-1": "m5", "eu-west-3": "m5", "us-gov-east-1": "m5", + "us-west-2": "m5", } ) diff --git a/platformtests/aws/default_instance_class_test.go b/platformtests/aws/default_instance_class_test.go index 55fd138568f..700e30d4d27 100644 --- a/platformtests/aws/default_instance_class_test.go +++ b/platformtests/aws/default_instance_class_test.go @@ -1,10 +1,13 @@ package aws import ( + "fmt" + "reflect" "strings" "testing" "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/pricing" awsutil "github.com/openshift/installer/pkg/asset/installconfig/aws" "github.com/openshift/installer/pkg/types/aws/defaults" @@ -13,17 +16,18 @@ import ( ) func TestGetDefaultInstanceClass(t *testing.T) { + preferredInstanceClasses := []string{"m4", "m5"} // decreasing precedence + ssn, err := awsutil.GetSession() if err != nil { t.Fatal(err) } exists := struct{}{} - instanceClasses := map[string]map[string]struct{}{} - - client := pricing.New(ssn, aws.NewConfig().WithRegion("us-east-1")) + pricingInstanceClasses := map[string]map[string]struct{}{} - err = client.GetProductsPages( + pricingClient := pricing.New(ssn, aws.NewConfig().WithRegion("us-east-1")) + err = pricingClient.GetProductsPages( &pricing.GetProductsInput{ ServiceCode: aws.String("AmazonEC2"), Filters: []*pricing.Filter{ @@ -57,11 +61,11 @@ func TestGetDefaultInstanceClass(t *testing.T) { instanceType := attr["instanceType"].(string) instanceClassSlice := strings.Split(instanceType, ".") instanceClass := instanceClassSlice[0] - _, ok := instanceClasses[location] + _, ok := pricingInstanceClasses[location] if ok { - instanceClasses[location][instanceClass] = exists + pricingInstanceClasses[location][instanceClass] = exists } else { - instanceClasses[location] = map[string]struct{}{instanceClass: exists} + pricingInstanceClasses[location] = map[string]struct{}{instanceClass: exists} } } return !lastPage @@ -80,7 +84,7 @@ func TestGetDefaultInstanceClass(t *testing.T) { "South America (Sao Paulo)": "sa-east-1", } - for location, classes := range instanceClasses { + for location, classes := range pricingInstanceClasses { t.Run(location, func(t *testing.T) { region, ok := regions[location] if !ok { @@ -96,19 +100,81 @@ func TestGetDefaultInstanceClass(t *testing.T) { } } - class := "" - // ordered list of prefered instance classes - for _, preferredClass := range []string{"m4", "m5"} { - if _, ok := classes[preferredClass]; ok { - class = preferredClass + ec2Client := ec2.New(ssn, aws.NewConfig().WithRegion(region)) + zonesResponse, err := ec2Client.DescribeAvailabilityZones(nil) + if err != nil { + t.Logf("no direct access to region, assuming full support: %v", err) + + var match string + for _, instanceClass := range preferredInstanceClasses { + if _, ok := classes[instanceClass]; ok { + match = instanceClass + break + } + } + + if match == "" { + t.Fatalf("none of the preferred instance classes are priced: %v", classes) + } + + t.Log(classes) + assert.Equal(t, defaults.InstanceClass(region), match) + return + } + + zones := make(map[string]struct{}, len(zonesResponse.AvailabilityZones)) + for _, zone := range zonesResponse.AvailabilityZones { + zones[*zone.ZoneName] = exists + } + + available := make(map[string]map[string]struct{}, len(preferredInstanceClasses)) + var match string + + for _, instanceClass := range preferredInstanceClasses { + if _, ok := classes[instanceClass]; !ok { + t.Logf("skip the unpriced %s", instanceClass) + continue + } + + available[instanceClass] = make(map[string]struct{}, len(zones)) + exampleInstanceType := fmt.Sprintf("%s.large", instanceClass) + err := ec2Client.DescribeReservedInstancesOfferingsPages( + &ec2.DescribeReservedInstancesOfferingsInput{ + Filters: []*ec2.Filter{ + {Name: aws.String("scope"), Values: []*string{aws.String("Availability Zone")}}, + }, + InstanceTenancy: aws.String("default"), + InstanceType: &exampleInstanceType, + ProductDescription: aws.String("Linux/UNIX"), + }, + func(results *ec2.DescribeReservedInstancesOfferingsOutput, lastPage bool) bool { + for _, offering := range results.ReservedInstancesOfferings { + if offering.AvailabilityZone == nil { + continue + } + + available[instanceClass][*offering.AvailabilityZone] = exists + } + + return !lastPage + }, + ) + if err != nil { + t.Fatal(err) + } + + if reflect.DeepEqual(available[instanceClass], zones) { + match = instanceClass break } } - if class == "" { - t.Fatalf("does not support any preferred classes: %v", classes) + + if match == "" { + t.Fatalf("none of the preferred instance classes are fully supported: %v", available) } - defaultClass := defaults.InstanceClass(region) - assert.Equal(t, defaultClass, class) + + t.Log(available) + assert.Equal(t, defaults.InstanceClass(region), match) }) } }