From 23f8487681ad1c354d072d15b34275da0bdf10b2 Mon Sep 17 00:00:00 2001 From: Brandon Wagner Date: Wed, 3 Feb 2021 10:16:26 -0600 Subject: [PATCH] Add Services Support for EKS and EMR (#72) * support service aggregate filters for eks and emr * add eks tests * add virt type test * add service tests * add emr tests * address pr comments * add semver license --- THIRD_PARTY_LICENSES | 60 + cmd/main.go | 16 +- go.mod | 2 + go.sum | 6 + pkg/selector/aggregates.go | 10 +- pkg/selector/eks.go | 137 ++ pkg/selector/eks_test.go | 130 ++ pkg/selector/emr.go | 363 ++++ pkg/selector/emr_test.go | 146 ++ pkg/selector/selector.go | 32 +- pkg/selector/selector_test.go | 20 + pkg/selector/services.go | 92 + pkg/selector/services_test.go | 134 ++ pkg/selector/types.go | 13 +- .../pv_instances.json | 1591 +++++++++++++++++ .../amazon-eks-ami-20210125.zip | Bin 0 -> 52886 bytes 16 files changed, 2744 insertions(+), 8 deletions(-) create mode 100644 pkg/selector/eks.go create mode 100644 pkg/selector/eks_test.go create mode 100644 pkg/selector/emr.go create mode 100644 pkg/selector/emr_test.go create mode 100644 pkg/selector/services.go create mode 100644 pkg/selector/services_test.go create mode 100644 test/static/DescribeInstanceTypesPages/pv_instances.json create mode 100644 test/static/GithubEKSAMIRelease/amazon-eks-ami-20210125.zip diff --git a/THIRD_PARTY_LICENSES b/THIRD_PARTY_LICENSES index 318fd32..f882138 100644 --- a/THIRD_PARTY_LICENSES +++ b/THIRD_PARTY_LICENSES @@ -784,3 +784,63 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +------ + +** github.com/imdario/mergo; version v0.3.11 -- +https://github.com/imdario/mergo + +Copyright (c) 2013 Dario Castañé. All rights reserved. +Copyright (c) 2012 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +------ + +** github.com/blang/semver; version v4.0.0 -- +https://github.com/blang/semver + +The MIT License + +Copyright (c) 2014 Benedikt Lang + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go index f727ea7..bbf427a 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -68,12 +68,14 @@ const ( networkPerformance = "network-performance" allowList = "allow-list" denyList = "deny-list" + virtualizationType = "virtualization-type" ) // Aggregate Filter Flags const ( instanceTypeBase = "base-instance-type" flexible = "flexible" + service = "service" ) // Configuration Flag Constants @@ -121,29 +123,31 @@ Full docs can be found at github.com/aws/amazon-` + binName cli.IntMinMaxRangeFlags(vcpus, cli.StringMe("c"), nil, "Number of vcpus available to the instance type.") cli.ByteQuantityMinMaxRangeFlags(memory, cli.StringMe("m"), nil, "Amount of Memory available (Example: 4 GiB)") cli.RatioFlag(vcpusToMemoryRatio, nil, nil, "The ratio of vcpus to GiBs of memory. (Example: 1:2)") - cli.StringFlag(cpuArchitecture, cli.StringMe("a"), nil, "CPU architecture [x86_64/amd64, i386, or arm64]", nil) + cli.StringOptionsFlag(cpuArchitecture, cli.StringMe("a"), nil, "CPU architecture [x86_64/amd64, i386, or arm64]", []string{"x86_64", "amd64", "i386", "arm64"}) cli.IntMinMaxRangeFlags(gpus, cli.StringMe("g"), nil, "Total Number of GPUs (Example: 4)") cli.ByteQuantityMinMaxRangeFlags(gpuMemoryTotal, nil, nil, "Number of GPUs' total memory (Example: 4 GiB)") - cli.StringFlag(placementGroupStrategy, nil, nil, "Placement group strategy: [cluster, partition, spread]", nil) - cli.StringFlag(usageClass, cli.StringMe("u"), nil, "Usage class: [spot or on-demand]", nil) - cli.StringFlag(rootDeviceType, nil, nil, "Supported root device types: [ebs or instance-store]", nil) + cli.StringOptionsFlag(placementGroupStrategy, nil, nil, "Placement group strategy: [cluster, partition, spread]", []string{"cluster", "partition", "spread"}) + cli.StringOptionsFlag(usageClass, cli.StringMe("u"), nil, "Usage class: [spot or on-demand]", []string{"spot", "on-demand"}) + cli.StringOptionsFlag(rootDeviceType, nil, nil, "Supported root device types: [ebs or instance-store]", []string{"ebs", "instance-store"}) cli.BoolFlag(enaSupport, cli.StringMe("e"), nil, "Instance types where ENA is supported or required") cli.BoolFlag(hibernationSupport, nil, nil, "Hibernation supported") cli.BoolFlag(baremetal, nil, nil, "Bare Metal instance types (.metal instances)") cli.BoolFlag(fpgaSupport, cli.StringMe("f"), nil, "FPGA instance types") cli.BoolFlag(burstSupport, cli.StringMe("b"), nil, "Burstable instance types") - cli.StringFlag(hypervisor, nil, nil, "Hypervisor: [xen or nitro]", nil) + cli.StringOptionsFlag(hypervisor, nil, nil, "Hypervisor: [xen or nitro]", []string{"xen", "nitro"}) cli.StringSliceFlag(availabilityZones, cli.StringMe("z"), nil, "Availability zones or zone ids to check EC2 capacity offered in specific AZs") cli.BoolFlag(currentGeneration, nil, nil, "Current generation instance types (explicitly set this to false to not return current generation instance types)") cli.IntMinMaxRangeFlags(networkInterfaces, nil, nil, "Number of network interfaces (ENIs) that can be attached to the instance") cli.IntMinMaxRangeFlags(networkPerformance, nil, nil, "Bandwidth in Gib/s of network performance (Example: 100)") cli.RegexFlag(allowList, nil, nil, "List of allowed instance types to select from w/ regex syntax (Example: m[3-5]\\.*)") cli.RegexFlag(denyList, nil, nil, "List of instance types which should be excluded w/ regex syntax (Example: m[1-2]\\.*)") + cli.StringOptionsFlag(virtualizationType, nil, nil, "Virtualization Type supported: [hvm or pv]", []string{"hvm", "paravirtual", "pv"}) // Suite Flags - higher level aggregate filters that return opinionated result cli.SuiteStringFlag(instanceTypeBase, nil, nil, "Instance Type used to retrieve similarly spec'd instance types", nil) cli.SuiteBoolFlag(flexible, nil, nil, "Retrieves a group of instance types spanning multiple generations based on opinionated defaults and user overridden resource filters") + cli.SuiteStringFlag(service, nil, nil, "Filter instance types based on service support (Example: eks, eks-20201211, or emr-5.20.0)", nil) // Configuration Flags - These will be grouped at the bottom of the help flags @@ -206,6 +210,8 @@ Full docs can be found at github.com/aws/amazon-` + binName DenyList: cli.RegexMe(flags[denyList]), InstanceTypeBase: cli.StringMe(flags[instanceTypeBase]), Flexible: cli.BoolMe(flags[flexible]), + Service: cli.StringMe(flags[service]), + VirtualizationType: cli.StringMe(flags[virtualizationType]), } if flags[verbose] != nil { diff --git a/go.mod b/go.mod index 36348d2..814dbf3 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,10 @@ go 1.15 require ( github.com/aws/aws-sdk-go v1.31.12 + github.com/blang/semver/v4 v4.0.0 github.com/ghodss/yaml v1.0.0 github.com/hashicorp/hcl v1.0.0 + github.com/imdario/mergo v0.3.11 github.com/mitchellh/go-homedir v1.1.0 github.com/smartystreets/goconvey v1.6.4 // indirect github.com/spf13/cobra v0.0.7 diff --git a/go.sum b/go.sum index b44ebbc..7f3aac8 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ github.com/aws/aws-sdk-go v1.31.12 h1:SxRRGyhlCagI0DYkhOg+FgdXGXzRTE3vEX/gsgFaiK github.com/aws/aws-sdk-go v1.31.12/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= @@ -49,6 +51,8 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgf github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc= @@ -166,4 +170,6 @@ gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bl gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/pkg/selector/aggregates.go b/pkg/selector/aggregates.go index 4dbd35b..b17bf06 100644 --- a/pkg/selector/aggregates.go +++ b/pkg/selector/aggregates.go @@ -48,7 +48,7 @@ func (itf Selector) TransformBaseInstanceType(filters Filters) (Filters, error) if filters.BareMetal == nil { filters.BareMetal = instanceTypeInfo.BareMetal } - if filters.CPUArchitecture == nil { + if filters.CPUArchitecture == nil && len(instanceTypeInfo.ProcessorInfo.SupportedArchitectures) == 1 { filters.CPUArchitecture = instanceTypeInfo.ProcessorInfo.SupportedArchitectures[0] } if filters.Fpga == nil { @@ -72,6 +72,9 @@ func (itf Selector) TransformBaseInstanceType(filters Filters) (Filters, error) upperBound := int(float64(*instanceTypeInfo.VCpuInfo.DefaultVCpus) * AggregateHighPercentile) filters.VCpusRange = &IntRangeFilter{LowerBound: lowerBound, UpperBound: upperBound} } + if filters.VirtualizationType == nil && len(instanceTypeInfo.SupportedVirtualizationTypes) == 1 { + filters.VirtualizationType = instanceTypeInfo.SupportedVirtualizationTypes[0] + } filters.InstanceTypeBase = nil return filters, nil @@ -107,3 +110,8 @@ func (itf Selector) TransformFlexible(filters Filters) (Filters, error) { return filters, nil } + +// TransformForService transforms lower level filters based on the service +func (itf Selector) TransformForService(filters Filters) (Filters, error) { + return itf.ServiceRegistry.ExecuteTransforms(filters) +} diff --git a/pkg/selector/eks.go b/pkg/selector/eks.go new file mode 100644 index 0000000..e951cd5 --- /dev/null +++ b/pkg/selector/eks.go @@ -0,0 +1,137 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package selector + +import ( + "archive/zip" + "bytes" + "fmt" + "io/ioutil" + "log" + "net/http" + "strings" + + "github.com/aws/aws-sdk-go/aws" +) + +const ( + eksAMIRepoURL = "https://github.com/awslabs/amazon-eks-ami" + eksFallbackLatestAMIVersion = "v20210125" + eksInstanceTypesFile = "eni-max-pods.txt" +) + +// EKS is a Service type for a custom service filter transform +type EKS struct { + AMIRepoURL string +} + +// Filters implements the Service interface contract for EKS +func (e *EKS) Filters(version string) (Filters, error) { + if e.AMIRepoURL == "" { + e.AMIRepoURL = eksAMIRepoURL + } + filters := Filters{} + + if version == "" { + var err error + version, err = e.getLatestAMIVersion() + if err != nil { + log.Printf("There was a problem fetching the latest EKS AMI version, using hardcoded fallback version %s\n", eksFallbackLatestAMIVersion) + version = eksFallbackLatestAMIVersion + } + } + if !strings.HasPrefix(version, "v") { + version = fmt.Sprintf("v%s", version) + } + supportedInstanceTypes, err := e.getSupportedInstanceTypes(version) + if err != nil { + log.Printf("Unable to retrieve EKS supported instance types for version %s: %v", version, err) + return filters, err + } + filters.InstanceTypes = &supportedInstanceTypes + filters.VirtualizationType = aws.String("hvm") + return filters, nil +} + +func (e *EKS) getSupportedInstanceTypes(version string) ([]string, error) { + supportedInstanceTypes := []string{} + resp, err := http.Get(fmt.Sprintf("%s/archive/%s.zip", e.AMIRepoURL, version)) + if err != nil { + return supportedInstanceTypes, err + } + + defer resp.Body.Close() + if resp.StatusCode != 200 { + return supportedInstanceTypes, fmt.Errorf("Unable to retrieve EKS supported instance types, got non-200 status code: %d", resp.StatusCode) + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return supportedInstanceTypes, err + } + + zipReader, err := zip.NewReader(bytes.NewReader(body), int64(len(body))) + if err != nil { + return supportedInstanceTypes, err + } + + // Read all the files from zip archive + for _, zipFile := range zipReader.File { + filePathParts := strings.Split(zipFile.Name, "/") + fileName := filePathParts[len(filePathParts)-1] + if fileName == eksInstanceTypesFile { + unzippedFileBytes, err := readZipFile(zipFile) + if err != nil { + log.Println(err) + continue + } + supportedInstanceTypesFileBody := string(unzippedFileBytes) + for _, line := range strings.Split(strings.Replace(supportedInstanceTypesFileBody, "\r\n", "\n", -1), "\n") { + if !strings.HasPrefix(line, "#") { + instanceType := strings.Split(line, " ")[0] + supportedInstanceTypes = append(supportedInstanceTypes, instanceType) + } + } + } + } + return supportedInstanceTypes, nil +} + +func (e EKS) getLatestAMIVersion() (string, error) { + client := &http.Client{ + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + } + // Get latest version + resp, err := client.Get(fmt.Sprintf("%s/releases/latest", e.AMIRepoURL)) + if err != nil { + return "", err + } + if resp.StatusCode != 302 { + return "", fmt.Errorf("Can't retrieve latest release from github because redirect was not sent") + } + versionRedirect := resp.Header.Get("location") + pathParts := strings.Split(versionRedirect, "/") + return pathParts[len(pathParts)-1], nil +} + +func readZipFile(zf *zip.File) ([]byte, error) { + f, err := zf.Open() + if err != nil { + return nil, err + } + defer f.Close() + return ioutil.ReadAll(f) +} diff --git a/pkg/selector/eks_test.go b/pkg/selector/eks_test.go new file mode 100644 index 0000000..fc7c5cd --- /dev/null +++ b/pkg/selector/eks_test.go @@ -0,0 +1,130 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package selector_test + +import ( + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + + "github.com/aws/amazon-ec2-instance-selector/v2/pkg/selector" + h "github.com/aws/amazon-ec2-instance-selector/v2/pkg/test" +) + +const ( + githubStaticReleasesDir = "GithubEKSAMIRelease" + githubReleaseVersion = "20210125" + githubZipFileName = "amazon-eks-ami-20210125.zip" +) + +// Tests + +func TestEKSDefaultService(t *testing.T) { + ghServer := eksGithubReleaseHTTPServer(false, false) + defer ghServer.Close() + + registry := selector.NewRegistry() + registry.Register("eks", &selector.EKS{ + AMIRepoURL: ghServer.URL, + }) + + eks := "eks" + filters := selector.Filters{ + Service: &eks, + } + + transformedFilters, err := registry.ExecuteTransforms(filters) + h.Ok(t, err) + h.Assert(t, transformedFilters != filters, " Filters should have been modified") + h.Assert(t, len(*transformedFilters.InstanceTypes) == 389, "389 instance types should be supported, but got %d", len(*transformedFilters.InstanceTypes)) + h.Assert(t, *transformedFilters.VirtualizationType == "hvm", "eks should only support hvm") + + eks = "eks-v" + githubReleaseVersion + filters.Service = &eks + transformedFilters, err = registry.ExecuteTransforms(filters) + h.Ok(t, err) + h.Assert(t, transformedFilters != filters, " Filters should have been modified") + h.Assert(t, len(*transformedFilters.InstanceTypes) == 389, "389 instance types should be supported, but got %d", len(*transformedFilters.InstanceTypes)) + h.Assert(t, *transformedFilters.VirtualizationType == "hvm", "eks should only support hvm") +} + +func TestEKSDefaultService_FailLatestReleaseUseFallbackStaticVersion(t *testing.T) { + ghServer := eksGithubReleaseHTTPServer(true, false) + defer ghServer.Close() + + registry := selector.NewRegistry() + registry.Register("eks", &selector.EKS{ + AMIRepoURL: ghServer.URL, + }) + + eks := "eks" + filters := selector.Filters{ + Service: &eks, + } + + transformedFilters, err := registry.ExecuteTransforms(filters) + h.Ok(t, err) + h.Assert(t, transformedFilters != filters, " Filters should have been modified") + h.Assert(t, len(*transformedFilters.InstanceTypes) == 389, "389 instance types should be supported, but got %d", len(*transformedFilters.InstanceTypes)) + h.Assert(t, *transformedFilters.VirtualizationType == "hvm", "eks should only support hvm") +} + +func TestEKSDefaultService_FailLatestReleaseAndFailExactVersionLookup(t *testing.T) { + ghServer := eksGithubReleaseHTTPServer(true, true) + defer ghServer.Close() + + registry := selector.NewRegistry() + registry.Register("eks", &selector.EKS{ + AMIRepoURL: ghServer.URL, + }) + + eks := "eks" + filters := selector.Filters{ + Service: &eks, + } + + _, err := registry.ExecuteTransforms(filters) + h.Nok(t, err) +} + +// Test Helpers Functions + +func eksGithubReleaseHTTPServer(failLatestRelease bool, failExactRelease bool) *httptest.Server { + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/releases/latest" { + if failLatestRelease { + w.WriteHeader(404) + return + } + w.WriteHeader(302) + w.Header().Add("location", "/releases/tag/v"+githubReleaseVersion) + return + } + if r.URL.Path == "/archive/v"+githubReleaseVersion+".zip" { + if failExactRelease { + w.WriteHeader(404) + return + } + ghReleaseZipPath := fmt.Sprintf("%s/%s/%s", mockFilesPath, githubStaticReleasesDir, githubZipFileName) + eksAMIReleaseZipFile, err := ioutil.ReadFile(ghReleaseZipPath) + if err != nil { + panic("Could not read EKS AMI release zip file") + } + w.Write(eksAMIReleaseZipFile) + return + } + })) +} diff --git a/pkg/selector/emr.go b/pkg/selector/emr.go new file mode 100644 index 0000000..2385d6e --- /dev/null +++ b/pkg/selector/emr.go @@ -0,0 +1,363 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package selector + +import ( + "fmt" + "strings" + + "github.com/aws/aws-sdk-go/aws" + "github.com/blang/semver/v4" +) + +const ( + fallbackVersion = "5.20.0" +) + +// EMR is a Service type for a custom service filter transform +type EMR struct{} + +// Filters implements the Service interface contract for EMR +func (e EMR) Filters(version string) (Filters, error) { + filters := Filters{} + if version == "" { + version = fallbackVersion + } + semanticVersion, err := semver.Make(version) + if err != nil { + return filters, err + } + if err := semanticVersion.Validate(); err != nil { + return filters, fmt.Errorf("Invalid semantic version passed for EMR") + } + instanceTypes, err := e.getEMRInstanceTypes(semanticVersion) + if err != nil { + return filters, err + } + filters.InstanceTypes = &instanceTypes + filters.RootDeviceType = aws.String("ebs") + filters.VirtualizationType = aws.String("hvm") + return filters, nil +} + +// getEMRInstanceTypes returns a list of instance types that emr supports +func (e EMR) getEMRInstanceTypes(version semver.Version) ([]string, error) { + instanceTypes := []string{} + + for _, instanceType := range e.getAllEMRInstanceTypes() { + if semver.MustParseRange(">=5.25.0")(version) { + instanceTypes = append(instanceTypes, instanceType) + } else if semver.MustParseRange(">=5.20.0 <5.25.0")(version) { + if e.isOnlyEMR_5_25_0_plus(instanceType) { + continue + } + instanceTypes = append(instanceTypes, instanceType) + } else if semver.MustParseRange(">=5.15.0 <5.20.0")(version) { + if instanceType == "c1.medium" { + continue + } + if e.isOnlyEMR_5_20_0_plus(instanceType) { + continue + } + if e.isOnlyEMR_5_25_0_plus(instanceType) { + continue + } + instanceTypes = append(instanceTypes, instanceType) + } else if semver.MustParseRange(">=5.13.0 <5.15.0")(version) { + if e.isOnlyEMR_5_20_0_plus(instanceType) { + continue + } + if e.isOnlyEMR_5_25_0_plus(instanceType) { + continue + } + instanceTypes = append(instanceTypes, instanceType) + } else if semver.MustParseRange(">=5.9.0 <5.13.0")(version) { + if e.isEMR_5_13_0_plus(instanceType) { + continue + } + if e.isOnlyEMR_5_20_0_plus(instanceType) { + continue + } + if e.isOnlyEMR_5_25_0_plus(instanceType) { + continue + } + instanceTypes = append(instanceTypes, instanceType) + } else { + if e.isEMR_5_13_0_plus(instanceType) { + continue + } + if e.isOnlyEMR_5_20_0_plus(instanceType) { + continue + } + if e.isOnlyEMR_5_25_0_plus(instanceType) { + continue + } + if strings.HasPrefix(instanceType, "i3") { + continue + } + instanceTypes = append(instanceTypes, instanceType) + } + } + return instanceTypes, nil +} + +func (EMR) isEMR_5_13_0_plus(instanceType string) bool { + prefixes := []string{ + "m5.", + "m5d.", + "c5.", + "c5d.", + "r5.", + "r5d.", + } + for _, prefix := range prefixes { + if strings.HasPrefix(instanceType, prefix) { + return true + } + } + return false +} + +func (EMR) isOnlyEMR_5_20_0_plus(instanceType string) bool { + prefixes := []string{ + "m5a.", + "c5n.", + "r5a.", + } + for _, prefix := range prefixes { + if strings.HasPrefix(instanceType, prefix) { + return true + } + } + return false +} + +func (EMR) isOnlyEMR_5_25_0_plus(instanceType string) bool { + prefixes := []string{ + "i3en.", + } + for _, prefix := range prefixes { + if strings.HasPrefix(instanceType, prefix) { + return true + } + } + return false +} + +func (EMR) getAllEMRInstanceTypes() []string { + return []string{ + "c1.medium", + "c1.xlarge", + "c3.2xlarge", + "c3.4xlarge", + "c3.8xlarge", + "c3.xlarge", + "c4.2xlarge", + "c4.4xlarge", + "c4.8xlarge", + "c4.large", + "c4.xlarge", + "c5.12xlarge", + "c5.18xlarge", + "c5.24xlarge", + "c5.2xlarge", + "c5.4xlarge", + "c5.9xlarge", + "c5.xlarge", + "c5a.12xlarge", + "c5a.16xlarge", + "c5a.24xlarge", + "c5a.2xlarge", + "c5a.4xlarge", + "c5a.8xlarge", + "c5a.xlarge", + "c5d.12xlarge", + "c5d.18xlarge", + "c5d.24xlarge", + "c5d.2xlarge", + "c5d.4xlarge", + "c5d.9xlarge", + "c5d.xlarge", + "c5n.18xlarge", + "c5n.2xlarge", + "c5n.4xlarge", + "c5n.9xlarge", + "c5n.xlarge", + "c6g.12xlarge", + "c6g.16xlarge", + "c6g.2xlarge", + "c6g.4xlarge", + "c6g.8xlarge", + "c6g.xlarge", + "cc2.8xlarge", + "d2.2xlarge", + "d2.4xlarge", + "d2.8xlarge", + "d2.xlarge", + "g2.2xlarge", + "g3.16xlarge", + "g3.4xlarge", + "g3.8xlarge", + "g3s.xlarge", + "g4dn.12xlarge", + "g4dn.16xlarge", + "g4dn.2xlarge", + "g4dn.4xlarge", + "g4dn.8xlarge", + "g4dn.xlarge", + "h1.16xlarge", + "h1.2xlarge", + "h1.4xlarge", + "h1.8xlarge", + "i2.2xlarge", + "i2.4xlarge", + "i2.8xlarge", + "i2.xlarge", + "i3.16xlarge", + "i3.2xlarge", + "i3.4xlarge", + "i3.8xlarge", + "i3.xlarge", + "i3en.12xlarge", + "i3en.24xlarge", + "i3en.2xlarge", + "i3en.3xlarge", + "i3en.6xlarge", + "i3en.xlarge", + "m1.large", + "m1.medium", + "m1.small", + "m1.xlarge", + "m2.2xlarge", + "m2.4xlarge", + "m2.xlarge", + "m3.2xlarge", + "m3.xlarge", + "m4.10xlarge", + "m4.16xlarge", + "m4.2xlarge", + "m4.4xlarge", + "m4.large", + "m4.xlarge", + "m5.12xlarge", + "m5.16xlarge", + "m5.24xlarge", + "m5.2xlarge", + "m5.4xlarge", + "m5.8xlarge", + "m5.metal", + "m5.xlarge", + "m5a.12xlarge", + "m5a.16xlarge", + "m5a.24xlarge", + "m5a.2xlarge", + "m5a.4xlarge", + "m5a.8xlarge", + "m5a.xlarge", + "m5ad.12xlarge", + "m5ad.16xlarge", + "m5ad.24xlarge", + "m5ad.2xlarge", + "m5ad.4xlarge", + "m5ad.8xlarge", + "m5ad.xlarge", + "m5d.12xlarge", + "m5d.16xlarge", + "m5d.24xlarge", + "m5d.2xlarge", + "m5d.4xlarge", + "m5d.8xlarge", + "m5d.metal", + "m5d.xlarge", + "m5dn.12xlarge", + "m5dn.16xlarge", + "m5dn.24xlarge", + "m5dn.2xlarge", + "m5dn.4xlarge", + "m5dn.8xlarge", + "m5dn.xlarge", + "m5n.12xlarge", + "m5n.16xlarge", + "m5n.24xlarge", + "m5n.2xlarge", + "m5n.4xlarge", + "m5n.8xlarge", + "m5n.xlarge", + "m6g.12xlarge", + "m6g.16xlarge", + "m6g.2xlarge", + "m6g.4xlarge", + "m6g.8xlarge", + "m6g.xlarge", + "mac1.metal", + "p2.16xlarge", + "p2.8xlarge", + "p2.xlarge", + "p3.16xlarge", + "p3.2xlarge", + "p3.8xlarge", + "p3dn.24xlarge", + "r3.2xlarge", + "r3.4xlarge", + "r3.8xlarge", + "r3.xlarge", + "r4.16xlarge", + "r4.2xlarge", + "r4.4xlarge", + "r4.8xlarge", + "r4.xlarge", + "r5.12xlarge", + "r5.16xlarge", + "r5.24xlarge", + "r5.2xlarge", + "r5.4xlarge", + "r5.8xlarge", + "r5.metal", + "r5.xlarge", + "r5a.12xlarge", + "r5a.16xlarge", + "r5a.24xlarge", + "r5a.2xlarge", + "r5a.4xlarge", + "r5a.8xlarge", + "r5a.xlarge", + "r5d.12xlarge", + "r5d.16xlarge", + "r5d.24xlarge", + "r5d.2xlarge", + "r5d.4xlarge", + "r5d.8xlarge", + "r5d.metal", + "r5d.xlarge", + "r5n.12xlarge", + "r5n.16xlarge", + "r5n.24xlarge", + "r5n.2xlarge", + "r5n.4xlarge", + "r5n.8xlarge", + "r5n.xlarge", + "r6g.12xlarge", + "r6g.16xlarge", + "r6g.2xlarge", + "r6g.4xlarge", + "r6g.8xlarge", + "r6g.xlarge", + "x1.32xlarge", + "z1d.12xlarge", + "z1d.2xlarge", + "z1d.3xlarge", + "z1d.6xlarge", + "z1d.xlarge", + } +} diff --git a/pkg/selector/emr_test.go b/pkg/selector/emr_test.go new file mode 100644 index 0000000..72c13a6 --- /dev/null +++ b/pkg/selector/emr_test.go @@ -0,0 +1,146 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package selector_test + +import ( + "testing" + + "github.com/aws/amazon-ec2-instance-selector/v2/pkg/selector" + h "github.com/aws/amazon-ec2-instance-selector/v2/pkg/test" +) + +// Tests +var emr = "emr" + +func TestEMRDefaultService(t *testing.T) { + registry := selector.NewRegistry() + registry.Register("emr", &selector.EMR{}) + + filters := selector.Filters{ + Service: &emr, + } + + transformedFilters, err := registry.ExecuteTransforms(filters) + h.Ok(t, err) + h.Assert(t, transformedFilters != filters, " Filters should have been modified") + h.Assert(t, *transformedFilters.RootDeviceType == "ebs", "emr should only supports ebs") + h.Assert(t, *transformedFilters.VirtualizationType == "hvm", "emr should only support hvm") + + emrWithVersion := "emr-" + "5.20.0" + filters.Service = &emrWithVersion + transformedFilters, err = registry.ExecuteTransforms(filters) + h.Ok(t, err) + h.Assert(t, transformedFilters != filters, " Filters should have been modified") + h.Assert(t, *transformedFilters.RootDeviceType == "ebs", "emr should only supports ebs") + h.Assert(t, *transformedFilters.VirtualizationType == "hvm", "emr should only support hvm") +} + +func TestFilters_Version5_25_0(t *testing.T) { + registry := selector.NewRegistry() + registry.Register("emr", &selector.EMR{}) + + filters := selector.Filters{ + Service: &emr, + } + + emrWithVersion := "emr-" + "5.25.0" + filters.Service = &emrWithVersion + transformedFilters, err := registry.ExecuteTransforms(filters) + h.Ok(t, err) + h.Assert(t, transformedFilters != filters, " Filters should have been modified") + h.Assert(t, *transformedFilters.RootDeviceType == "ebs", "emr should only supports ebs") + h.Assert(t, *transformedFilters.VirtualizationType == "hvm", "emr should only support hvm") + h.Assert(t, contains(*transformedFilters.InstanceTypes, "i3en.xlarge"), "emr version 5.25.0 should include i3en.xlarge") +} + +func TestFilters_Version5_15_0(t *testing.T) { + registry := selector.NewRegistry() + registry.Register("emr", &selector.EMR{}) + + filters := selector.Filters{ + Service: &emr, + } + + emrWithVersion := "emr-" + "5.15.0" + filters.Service = &emrWithVersion + transformedFilters, err := registry.ExecuteTransforms(filters) + h.Ok(t, err) + h.Assert(t, transformedFilters != filters, " Filters should have been modified") + h.Assert(t, *transformedFilters.RootDeviceType == "ebs", "emr should only supports ebs") + h.Assert(t, *transformedFilters.VirtualizationType == "hvm", "emr should only support hvm") + h.Assert(t, !contains(*transformedFilters.InstanceTypes, "c1.medium"), "emr version 5.15.0 should not include c1.medium") +} + +func TestFilters_Version5_13_0(t *testing.T) { + registry := selector.NewRegistry() + registry.Register("emr", &selector.EMR{}) + + filters := selector.Filters{ + Service: &emr, + } + + emrWithVersion := "emr-" + "5.13.0" + filters.Service = &emrWithVersion + transformedFilters, err := registry.ExecuteTransforms(filters) + h.Ok(t, err) + h.Assert(t, transformedFilters != filters, " Filters should have been modified") + h.Assert(t, *transformedFilters.RootDeviceType == "ebs", "emr should only supports ebs") + h.Assert(t, *transformedFilters.VirtualizationType == "hvm", "emr should only support hvm") + h.Assert(t, !contains(*transformedFilters.InstanceTypes, "m5a.xlarge"), "emr version 5.13.0 should not include m5a.xlarge") +} + +func TestFilters_Version5_9_0(t *testing.T) { + registry := selector.NewRegistry() + registry.Register("emr", &selector.EMR{}) + + filters := selector.Filters{ + Service: &emr, + } + + emrWithVersion := "emr-" + "5.9.0" + filters.Service = &emrWithVersion + transformedFilters, err := registry.ExecuteTransforms(filters) + h.Ok(t, err) + h.Assert(t, transformedFilters != filters, " Filters should have been modified") + h.Assert(t, *transformedFilters.RootDeviceType == "ebs", "emr should only supports ebs") + h.Assert(t, *transformedFilters.VirtualizationType == "hvm", "emr should only support hvm") + h.Assert(t, !contains(*transformedFilters.InstanceTypes, "m5a.xlarge"), "emr version 5.9.0 should not include m5a.xlarge") +} + +func TestFilters_Version5_8_0(t *testing.T) { + registry := selector.NewRegistry() + registry.Register("emr", &selector.EMR{}) + + filters := selector.Filters{ + Service: &emr, + } + + emrWithVersion := "emr-" + "5.8.0" + filters.Service = &emrWithVersion + transformedFilters, err := registry.ExecuteTransforms(filters) + h.Ok(t, err) + h.Assert(t, transformedFilters != filters, " Filters should have been modified") + h.Assert(t, *transformedFilters.RootDeviceType == "ebs", "emr should only supports ebs") + h.Assert(t, *transformedFilters.VirtualizationType == "hvm", "emr should only support hvm") + h.Assert(t, !contains(*transformedFilters.InstanceTypes, "i3.xlarge"), "emr version 5.8.0 should not include i3.xlarge") +} + +func contains(arr []string, input string) bool { + for _, entry := range arr { + if entry == input { + return true + } + } + return false +} diff --git a/pkg/selector/selector.go b/pkg/selector/selector.go index 7b22dea..3f75d11 100644 --- a/pkg/selector/selector.go +++ b/pkg/selector/selector.go @@ -62,18 +62,26 @@ const ( networkPerformance = "networkPerformance" allowList = "allowList" denyList = "denyList" + instanceTypes = "instanceTypes" + virtualizationType = "virtualizationType" cpuArchitectureAMD64 = "amd64" cpuArchitectureX8664 = "x86_64" + + virtualizationTypeParaVirtual = "paravirtual" + virtualizationTypePV = "pv" ) // New creates an instance of Selector provided an aws session func New(sess *session.Session) *Selector { + serviceRegistry := NewRegistry() + serviceRegistry.RegisterAWSServices() userAgentTag := fmt.Sprintf("%s-v%s", sdkName, versionID) userAgentHandler := request.MakeAddToUserAgentFreeFormHandler(userAgentTag) sess.Handlers.Build.PushBack(userAgentHandler) return &Selector{ - EC2: ec2.New(sess), + EC2: ec2.New(sess), + ServiceRegistry: serviceRegistry, } } @@ -124,6 +132,7 @@ func (itf Selector) AggregateFilterTransform(filters Filters) (Filters, error) { transforms := []FiltersTransform{ TransformFn(itf.TransformBaseInstanceType), TransformFn(itf.TransformFlexible), + TransformFn(itf.TransformForService), } var err error for _, transform := range transforms { @@ -147,6 +156,9 @@ func (itf Selector) rawFilter(filters Filters) ([]*ec2.InstanceTypeInfo, error) if filters.CPUArchitecture != nil && *filters.CPUArchitecture == cpuArchitectureAMD64 { *filters.CPUArchitecture = cpuArchitectureX8664 } + if filters.VirtualizationType != nil && *filters.VirtualizationType == virtualizationTypePV { + *filters.VirtualizationType = virtualizationTypeParaVirtual + } if filters.AvailabilityZones != nil { locations = *filters.AvailabilityZones @@ -190,6 +202,8 @@ func (itf Selector) rawFilter(filters Filters) ([]*ec2.InstanceTypeInfo, error) currentGeneration: {filters.CurrentGeneration, instanceTypeInfo.CurrentGeneration}, networkInterfaces: {filters.NetworkInterfaces, instanceTypeInfo.NetworkInfo.MaximumNetworkInterfaces}, networkPerformance: {filters.NetworkPerformance, getNetworkPerformance(instanceTypeInfo.NetworkInfo.NetworkPerformance)}, + instanceTypes: {filters.InstanceTypes, instanceTypeInfo.InstanceType}, + virtualizationType: {filters.VirtualizationType, instanceTypeInfo.SupportedVirtualizationTypes}, } if isInDenyList(filters.DenyList, instanceTypeName) || !isInAllowList(filters.AllowList, instanceTypeName) { @@ -324,6 +338,22 @@ func (itf Selector) executeFilters(filterToInstanceSpecMapping map[string]filter default: return false, fmt.Errorf(invalidInstanceSpecTypeMsg) } + case *[]string: + switch iSpec := instanceSpec.(type) { + case *string: + filterOfPtrs := []*string{} + for _, f := range *filter { + // this allows us to copy a static pointer to f into filterOfPtrs + // since the pointer to f is updated on each loop iteration + temp := f + filterOfPtrs = append(filterOfPtrs, &temp) + } + if !isSupportedFromStrings(filterOfPtrs, iSpec) { + return false, nil + } + default: + return false, fmt.Errorf(invalidInstanceSpecTypeMsg) + } default: return false, fmt.Errorf("No filter handler found for %s", filterDetailsMsg) } diff --git a/pkg/selector/selector_test.go b/pkg/selector/selector_test.go index 75702ab..785a20d 100644 --- a/pkg/selector/selector_test.go +++ b/pkg/selector/selector_test.go @@ -571,3 +571,23 @@ func TestFilter_X8664_AMD64(t *testing.T) { h.Assert(t, len(results) == 1, "Should only return 1 instance type with x86_64/amd64 cpu architecture") h.Assert(t, results[0] == "t3.micro", "Should return t3.micro, got %s instead", results[0]) } + +func TestFilter_VirtType_PV(t *testing.T) { + ec2Mock := setupMock(t, describeInstanceTypesPages, "pv_instances.json") + itf := selector.Selector{ + EC2: ec2Mock, + } + filters := selector.Filters{ + VirtualizationType: aws.String("pv"), + } + results, err := itf.Filter(filters) + h.Ok(t, err) + h.Assert(t, len(results) > 0, "Should return at least 1 instance type when filtering with VirtualizationType: pv") + + filters = selector.Filters{ + VirtualizationType: aws.String("paravirtual"), + } + results, err = itf.Filter(filters) + h.Ok(t, err) + h.Assert(t, len(results) > 0, "Should return at least 1 instance type when filtering with VirtualizationType: paravirtual") +} diff --git a/pkg/selector/services.go b/pkg/selector/services.go new file mode 100644 index 0000000..b1ddf8a --- /dev/null +++ b/pkg/selector/services.go @@ -0,0 +1,92 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package selector + +import ( + "fmt" + "strings" + + "github.com/imdario/mergo" +) + +// Service is used to write custom service filter transforms +type Service interface { + Filters(version string) (Filters, error) +} + +// ServiceFiltersFn is the func type definition for the Service interface +type ServiceFiltersFn func(version string) (Filters, error) + +// Filters implements the Service interface on ServiceFiltersFn +// This allows any ServiceFiltersFn to be passed into funcs accepting the Service interface +func (fn ServiceFiltersFn) Filters(version string) (Filters, error) { + return fn(version) +} + +// ServiceRegistry is used to register service filter transforms +type ServiceRegistry struct { + services map[string]*Service +} + +// NewRegistry creates a new instance of a ServiceRegistry +func NewRegistry() ServiceRegistry { + return ServiceRegistry{ + services: make(map[string]*Service), + } +} + +// Register takes a service name and Service implementation that will be executed on an ExecuteTransforms call +func (sr *ServiceRegistry) Register(name string, service Service) { + if sr.services == nil { + sr.services = make(map[string]*Service) + } + if name == "" { + return + } + sr.services[name] = &service +} + +// RegisterAWSServices registers the built-in AWS service filter transforms +func (sr *ServiceRegistry) RegisterAWSServices() { + sr.Register("eks", &EKS{}) + sr.Register("emr", &EMR{}) +} + +// ExecuteTransforms will execute the ServiceRegistry's registered service filter transforms +// Filters.Service will be parsed as - and passed to Service.Filters +func (sr *ServiceRegistry) ExecuteTransforms(filters Filters) (Filters, error) { + if filters.Service == nil || *filters.Service == "" { + return filters, nil + } + serviceAndVersion := strings.ToLower(*filters.Service) + versionParts := strings.Split(serviceAndVersion, "-") + serviceName := versionParts[0] + version := "" + if len(versionParts) >= 2 { + version = strings.Join(versionParts[1:], "-") + } + service, ok := sr.services[serviceName] + if !ok { + return filters, fmt.Errorf("Service %s is not registered", serviceName) + } + + serviceFilters, err := (*service).Filters(version) + if err != nil { + return filters, err + } + if err := mergo.Merge(&filters, serviceFilters); err != nil { + return filters, err + } + return filters, nil +} diff --git a/pkg/selector/services_test.go b/pkg/selector/services_test.go new file mode 100644 index 0000000..70a3431 --- /dev/null +++ b/pkg/selector/services_test.go @@ -0,0 +1,134 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file is distributed +// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +// express or implied. See the License for the specific language governing +// permissions and limitations under the License. + +package selector_test + +import ( + "testing" + + "github.com/aws/amazon-ec2-instance-selector/v2/pkg/selector" + h "github.com/aws/amazon-ec2-instance-selector/v2/pkg/test" + "github.com/aws/aws-sdk-go/aws" +) + +// Tests + +func TestDefaultRegistry(t *testing.T) { + registry := selector.NewRegistry() + registry.RegisterAWSServices() + + emr := "emr" + filters := selector.Filters{ + Service: &emr, + } + + transformedFilters, err := registry.ExecuteTransforms(filters) + h.Ok(t, err) + h.Assert(t, transformedFilters != filters, " Filters should have been modified") +} + +func TestRegister_LazyInit(t *testing.T) { + registry := selector.ServiceRegistry{} + registry.RegisterAWSServices() + + emr := "emr" + filters := selector.Filters{ + Service: &emr, + } + + transformedFilters, err := registry.ExecuteTransforms(filters) + h.Ok(t, err) + h.Assert(t, transformedFilters != filters, " Filters should have been modified") +} + +func TestExecuteTransforms_OnUnrecognizedService(t *testing.T) { + registry := selector.NewRegistry() + registry.RegisterAWSServices() + + nes := "nonexistentservice" + filters := selector.Filters{ + Service: &nes, + } + + _, err := registry.ExecuteTransforms(filters) + h.Nok(t, err) +} + +func TestRegister_CustomService(t *testing.T) { + registry := selector.NewRegistry() + customServiceFn := func(version string) (filters selector.Filters, err error) { + filters.BareMetal = aws.Bool(true) + return filters, nil + } + + registry.Register("myservice", selector.ServiceFiltersFn(customServiceFn)) + + myService := "myservice" + filters := selector.Filters{ + Service: &myService, + } + + transformedFilters, err := registry.ExecuteTransforms(filters) + h.Ok(t, err) + h.Assert(t, *transformedFilters.BareMetal == true, "custom service should have transformed BareMetal to true") +} + +func TestExecuteTransforms_ShortCircuitOnEmptyService(t *testing.T) { + registry := selector.NewRegistry() + registry.RegisterAWSServices() + + emr := "" + filters := selector.Filters{ + Service: &emr, + } + + transformedFilters, err := registry.ExecuteTransforms(filters) + h.Ok(t, err) + h.Assert(t, transformedFilters == filters, " Filters should not be modified") +} + +func TestExecuteTransforms_ValidVersionParsing(t *testing.T) { + registry := selector.NewRegistry() + customServiceFn := func(version string) (filters selector.Filters, err error) { + h.Assert(t, version == "myversion", "version should have been parsed as myversion but got %s", version) + return filters, nil + } + + registry.Register("myservice", selector.ServiceFiltersFn(customServiceFn)) + + myService := "myservice-myversion" + filters := selector.Filters{ + Service: &myService, + } + + _, err := registry.ExecuteTransforms(filters) + h.Ok(t, err) +} + +func TestExecuteTransforms_LongVersionWithExtraDash(t *testing.T) { + registry := selector.NewRegistry() + customServiceFn := func(version string) (filters selector.Filters, err error) { + h.Assert(t, version == "myversion-test", "version should have been parsed as myversion-test but got %s", version) + return filters, nil + } + + registry.Register("myservice", selector.ServiceFiltersFn(customServiceFn)) + + myService := "myservice-myversion-test" + filters := selector.Filters{ + Service: &myService, + } + + _, err := registry.ExecuteTransforms(filters) + h.Ok(t, err) +} diff --git a/pkg/selector/types.go b/pkg/selector/types.go index ac66c00..835e461 100644 --- a/pkg/selector/types.go +++ b/pkg/selector/types.go @@ -38,7 +38,8 @@ func (fn InstanceTypesOutputFn) Output(instanceTypes []*ec2.InstanceTypeInfo) [] // Selector is used to filter instance type resource specs type Selector struct { - EC2 ec2iface.EC2API + EC2 ec2iface.EC2API + ServiceRegistry ServiceRegistry } // IntRangeFilter holds an upper and lower bound int @@ -179,4 +180,14 @@ type Filters struct { // Flexible finds an opinionated set of general (c, m, r, t, a, etc.) instance types that match a criteria specified // or defaults to 4 vcpus Flexible *bool + + // Service filters instance types based on a service's supported list of instance types + // Example: eks or emr + Service *string + + // InstanceTypes filters instance types and only allows instance types in this slice + InstanceTypes *[]string + + // VirtualizationType is used to return instance types that match either hvm or pv virtualization types + VirtualizationType *string } diff --git a/test/static/DescribeInstanceTypesPages/pv_instances.json b/test/static/DescribeInstanceTypesPages/pv_instances.json new file mode 100644 index 0000000..054bcb5 --- /dev/null +++ b/test/static/DescribeInstanceTypesPages/pv_instances.json @@ -0,0 +1,1591 @@ +{ + "InstanceTypes": [ + { + "AutoRecoverySupported": false, + "BareMetal": false, + "BurstablePerformanceSupported": false, + "CurrentGeneration": false, + "DedicatedHostsSupported": false, + "EbsInfo": { + "EbsOptimizedInfo": null, + "EbsOptimizedSupport": "unsupported", + "EncryptionSupport": "unsupported" + }, + "FpgaInfo": null, + "FreeTierEligible": false, + "GpuInfo": null, + "HibernationSupported": false, + "Hypervisor": "xen", + "InferenceAcceleratorInfo": null, + "InstanceStorageInfo": { + "Disks": [ + { + "Count": 1, + "SizeInGB": 350, + "Type": "hdd" + } + ], + "TotalSizeInGB": 350 + }, + "InstanceStorageSupported": true, + "InstanceType": "c1.medium", + "MemoryInfo": { + "SizeInMiB": 1740 + }, + "NetworkInfo": { + "EfaSupported": false, + "EnaSupport": "unsupported", + "Ipv4AddressesPerInterface": 6, + "Ipv6AddressesPerInterface": 0, + "Ipv6Supported": false, + "MaximumNetworkInterfaces": 2, + "NetworkPerformance": "Moderate" + }, + "PlacementGroupInfo": { + "SupportedStrategies": [ + "partition", + "spread" + ] + }, + "ProcessorInfo": { + "SupportedArchitectures": [ + "i386", + "x86_64" + ], + "SustainedClockSpeedInGhz": null + }, + "SupportedRootDeviceTypes": [ + "ebs", + "instance-store" + ], + "SupportedUsageClasses": [ + "on-demand", + "spot" + ], + "SupportedVirtualizationTypes": [ + "hvm", + "paravirtual" + ], + "VCpuInfo": { + "DefaultCores": 2, + "DefaultThreadsPerCore": 1, + "DefaultVCpus": 2, + "ValidCores": [ + 1, + 2 + ], + "ValidThreadsPerCore": [ + 1 + ] + } + }, + { + "AutoRecoverySupported": false, + "BareMetal": false, + "BurstablePerformanceSupported": false, + "CurrentGeneration": false, + "DedicatedHostsSupported": false, + "EbsInfo": { + "EbsOptimizedInfo": { + "BaselineBandwidthInMbps": 1000, + "BaselineIops": 8000, + "BaselineThroughputInMBps": 125, + "MaximumBandwidthInMbps": 1000, + "MaximumIops": 8000, + "MaximumThroughputInMBps": 125 + }, + "EbsOptimizedSupport": "supported", + "EncryptionSupport": "unsupported" + }, + "FpgaInfo": null, + "FreeTierEligible": false, + "GpuInfo": null, + "HibernationSupported": false, + "Hypervisor": "xen", + "InferenceAcceleratorInfo": null, + "InstanceStorageInfo": { + "Disks": [ + { + "Count": 4, + "SizeInGB": 420, + "Type": "hdd" + } + ], + "TotalSizeInGB": 1680 + }, + "InstanceStorageSupported": true, + "InstanceType": "c1.xlarge", + "MemoryInfo": { + "SizeInMiB": 7168 + }, + "NetworkInfo": { + "EfaSupported": false, + "EnaSupport": "unsupported", + "Ipv4AddressesPerInterface": 15, + "Ipv6AddressesPerInterface": 0, + "Ipv6Supported": false, + "MaximumNetworkInterfaces": 4, + "NetworkPerformance": "High" + }, + "PlacementGroupInfo": { + "SupportedStrategies": [ + "partition", + "spread" + ] + }, + "ProcessorInfo": { + "SupportedArchitectures": [ + "x86_64" + ], + "SustainedClockSpeedInGhz": null + }, + "SupportedRootDeviceTypes": [ + "ebs", + "instance-store" + ], + "SupportedUsageClasses": [ + "on-demand", + "spot" + ], + "SupportedVirtualizationTypes": [ + "hvm", + "paravirtual" + ], + "VCpuInfo": { + "DefaultCores": 8, + "DefaultThreadsPerCore": 1, + "DefaultVCpus": 8, + "ValidCores": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ], + "ValidThreadsPerCore": [ + 1 + ] + } + }, + { + "AutoRecoverySupported": true, + "BareMetal": false, + "BurstablePerformanceSupported": false, + "CurrentGeneration": false, + "DedicatedHostsSupported": true, + "EbsInfo": { + "EbsOptimizedInfo": { + "BaselineBandwidthInMbps": 1000, + "BaselineIops": 8000, + "BaselineThroughputInMBps": 125, + "MaximumBandwidthInMbps": 1000, + "MaximumIops": 8000, + "MaximumThroughputInMBps": 125 + }, + "EbsOptimizedSupport": "supported", + "EncryptionSupport": "supported" + }, + "FpgaInfo": null, + "FreeTierEligible": false, + "GpuInfo": null, + "HibernationSupported": true, + "Hypervisor": "xen", + "InferenceAcceleratorInfo": null, + "InstanceStorageInfo": { + "Disks": [ + { + "Count": 2, + "SizeInGB": 80, + "Type": "ssd" + } + ], + "TotalSizeInGB": 160 + }, + "InstanceStorageSupported": true, + "InstanceType": "c3.2xlarge", + "MemoryInfo": { + "SizeInMiB": 15360 + }, + "NetworkInfo": { + "EfaSupported": false, + "EnaSupport": "unsupported", + "Ipv4AddressesPerInterface": 15, + "Ipv6AddressesPerInterface": 15, + "Ipv6Supported": true, + "MaximumNetworkInterfaces": 4, + "NetworkPerformance": "High" + }, + "PlacementGroupInfo": { + "SupportedStrategies": [ + "cluster", + "partition", + "spread" + ] + }, + "ProcessorInfo": { + "SupportedArchitectures": [ + "x86_64" + ], + "SustainedClockSpeedInGhz": 2.8 + }, + "SupportedRootDeviceTypes": [ + "ebs", + "instance-store" + ], + "SupportedUsageClasses": [ + "on-demand", + "spot" + ], + "SupportedVirtualizationTypes": [ + "paravirtual", + "hvm" + ], + "VCpuInfo": { + "DefaultCores": 4, + "DefaultThreadsPerCore": 2, + "DefaultVCpus": 8, + "ValidCores": [ + 1, + 2, + 3, + 4 + ], + "ValidThreadsPerCore": [ + 1, + 2 + ] + } + }, + { + "AutoRecoverySupported": true, + "BareMetal": false, + "BurstablePerformanceSupported": false, + "CurrentGeneration": false, + "DedicatedHostsSupported": true, + "EbsInfo": { + "EbsOptimizedInfo": { + "BaselineBandwidthInMbps": 2000, + "BaselineIops": 16000, + "BaselineThroughputInMBps": 250, + "MaximumBandwidthInMbps": 2000, + "MaximumIops": 16000, + "MaximumThroughputInMBps": 250 + }, + "EbsOptimizedSupport": "supported", + "EncryptionSupport": "supported" + }, + "FpgaInfo": null, + "FreeTierEligible": false, + "GpuInfo": null, + "HibernationSupported": true, + "Hypervisor": "xen", + "InferenceAcceleratorInfo": null, + "InstanceStorageInfo": { + "Disks": [ + { + "Count": 2, + "SizeInGB": 160, + "Type": "ssd" + } + ], + "TotalSizeInGB": 320 + }, + "InstanceStorageSupported": true, + "InstanceType": "c3.4xlarge", + "MemoryInfo": { + "SizeInMiB": 30720 + }, + "NetworkInfo": { + "EfaSupported": false, + "EnaSupport": "unsupported", + "Ipv4AddressesPerInterface": 30, + "Ipv6AddressesPerInterface": 30, + "Ipv6Supported": true, + "MaximumNetworkInterfaces": 8, + "NetworkPerformance": "High" + }, + "PlacementGroupInfo": { + "SupportedStrategies": [ + "cluster", + "partition", + "spread" + ] + }, + "ProcessorInfo": { + "SupportedArchitectures": [ + "x86_64" + ], + "SustainedClockSpeedInGhz": 2.8 + }, + "SupportedRootDeviceTypes": [ + "ebs", + "instance-store" + ], + "SupportedUsageClasses": [ + "on-demand", + "spot" + ], + "SupportedVirtualizationTypes": [ + "paravirtual", + "hvm" + ], + "VCpuInfo": { + "DefaultCores": 8, + "DefaultThreadsPerCore": 2, + "DefaultVCpus": 16, + "ValidCores": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ], + "ValidThreadsPerCore": [ + 1, + 2 + ] + } + }, + { + "AutoRecoverySupported": true, + "BareMetal": false, + "BurstablePerformanceSupported": false, + "CurrentGeneration": false, + "DedicatedHostsSupported": true, + "EbsInfo": { + "EbsOptimizedInfo": null, + "EbsOptimizedSupport": "unsupported", + "EncryptionSupport": "supported" + }, + "FpgaInfo": null, + "FreeTierEligible": false, + "GpuInfo": null, + "HibernationSupported": true, + "Hypervisor": "xen", + "InferenceAcceleratorInfo": null, + "InstanceStorageInfo": { + "Disks": [ + { + "Count": 2, + "SizeInGB": 320, + "Type": "ssd" + } + ], + "TotalSizeInGB": 640 + }, + "InstanceStorageSupported": true, + "InstanceType": "c3.8xlarge", + "MemoryInfo": { + "SizeInMiB": 61440 + }, + "NetworkInfo": { + "EfaSupported": false, + "EnaSupport": "unsupported", + "Ipv4AddressesPerInterface": 30, + "Ipv6AddressesPerInterface": 30, + "Ipv6Supported": true, + "MaximumNetworkInterfaces": 8, + "NetworkPerformance": "10 Gigabit" + }, + "PlacementGroupInfo": { + "SupportedStrategies": [ + "cluster", + "partition", + "spread" + ] + }, + "ProcessorInfo": { + "SupportedArchitectures": [ + "x86_64" + ], + "SustainedClockSpeedInGhz": 2.8 + }, + "SupportedRootDeviceTypes": [ + "ebs", + "instance-store" + ], + "SupportedUsageClasses": [ + "on-demand", + "spot" + ], + "SupportedVirtualizationTypes": [ + "paravirtual", + "hvm" + ], + "VCpuInfo": { + "DefaultCores": 16, + "DefaultThreadsPerCore": 2, + "DefaultVCpus": 32, + "ValidCores": [ + 2, + 4, + 6, + 8, + 10, + 12, + 14, + 16 + ], + "ValidThreadsPerCore": [ + 1, + 2 + ] + } + }, + { + "AutoRecoverySupported": true, + "BareMetal": false, + "BurstablePerformanceSupported": false, + "CurrentGeneration": false, + "DedicatedHostsSupported": true, + "EbsInfo": { + "EbsOptimizedInfo": null, + "EbsOptimizedSupport": "unsupported", + "EncryptionSupport": "supported" + }, + "FpgaInfo": null, + "FreeTierEligible": false, + "GpuInfo": null, + "HibernationSupported": true, + "Hypervisor": "xen", + "InferenceAcceleratorInfo": null, + "InstanceStorageInfo": { + "Disks": [ + { + "Count": 2, + "SizeInGB": 16, + "Type": "ssd" + } + ], + "TotalSizeInGB": 32 + }, + "InstanceStorageSupported": true, + "InstanceType": "c3.large", + "MemoryInfo": { + "SizeInMiB": 3840 + }, + "NetworkInfo": { + "EfaSupported": false, + "EnaSupport": "unsupported", + "Ipv4AddressesPerInterface": 10, + "Ipv6AddressesPerInterface": 10, + "Ipv6Supported": true, + "MaximumNetworkInterfaces": 3, + "NetworkPerformance": "Moderate" + }, + "PlacementGroupInfo": { + "SupportedStrategies": [ + "cluster", + "partition", + "spread" + ] + }, + "ProcessorInfo": { + "SupportedArchitectures": [ + "i386", + "x86_64" + ], + "SustainedClockSpeedInGhz": 2.8 + }, + "SupportedRootDeviceTypes": [ + "ebs", + "instance-store" + ], + "SupportedUsageClasses": [ + "on-demand", + "spot" + ], + "SupportedVirtualizationTypes": [ + "paravirtual", + "hvm" + ], + "VCpuInfo": { + "DefaultCores": 1, + "DefaultThreadsPerCore": 2, + "DefaultVCpus": 2, + "ValidCores": [ + 1 + ], + "ValidThreadsPerCore": [ + 1, + 2 + ] + } + }, + { + "AutoRecoverySupported": true, + "BareMetal": false, + "BurstablePerformanceSupported": false, + "CurrentGeneration": false, + "DedicatedHostsSupported": true, + "EbsInfo": { + "EbsOptimizedInfo": { + "BaselineBandwidthInMbps": 500, + "BaselineIops": 4000, + "BaselineThroughputInMBps": 62.5, + "MaximumBandwidthInMbps": 500, + "MaximumIops": 4000, + "MaximumThroughputInMBps": 62.5 + }, + "EbsOptimizedSupport": "supported", + "EncryptionSupport": "supported" + }, + "FpgaInfo": null, + "FreeTierEligible": false, + "GpuInfo": null, + "HibernationSupported": true, + "Hypervisor": "xen", + "InferenceAcceleratorInfo": null, + "InstanceStorageInfo": { + "Disks": [ + { + "Count": 2, + "SizeInGB": 40, + "Type": "ssd" + } + ], + "TotalSizeInGB": 80 + }, + "InstanceStorageSupported": true, + "InstanceType": "c3.xlarge", + "MemoryInfo": { + "SizeInMiB": 7680 + }, + "NetworkInfo": { + "EfaSupported": false, + "EnaSupport": "unsupported", + "Ipv4AddressesPerInterface": 15, + "Ipv6AddressesPerInterface": 15, + "Ipv6Supported": true, + "MaximumNetworkInterfaces": 4, + "NetworkPerformance": "Moderate" + }, + "PlacementGroupInfo": { + "SupportedStrategies": [ + "cluster", + "partition", + "spread" + ] + }, + "ProcessorInfo": { + "SupportedArchitectures": [ + "x86_64" + ], + "SustainedClockSpeedInGhz": 2.8 + }, + "SupportedRootDeviceTypes": [ + "ebs", + "instance-store" + ], + "SupportedUsageClasses": [ + "on-demand", + "spot" + ], + "SupportedVirtualizationTypes": [ + "hvm", + "paravirtual" + ], + "VCpuInfo": { + "DefaultCores": 2, + "DefaultThreadsPerCore": 2, + "DefaultVCpus": 4, + "ValidCores": [ + 1, + 2 + ], + "ValidThreadsPerCore": [ + 1, + 2 + ] + } + }, + { + "AutoRecoverySupported": false, + "BareMetal": false, + "BurstablePerformanceSupported": false, + "CurrentGeneration": false, + "DedicatedHostsSupported": false, + "EbsInfo": { + "EbsOptimizedInfo": { + "BaselineBandwidthInMbps": 500, + "BaselineIops": 4000, + "BaselineThroughputInMBps": 62.5, + "MaximumBandwidthInMbps": 500, + "MaximumIops": 4000, + "MaximumThroughputInMBps": 62.5 + }, + "EbsOptimizedSupport": "supported", + "EncryptionSupport": "unsupported" + }, + "FpgaInfo": null, + "FreeTierEligible": false, + "GpuInfo": null, + "HibernationSupported": false, + "Hypervisor": "xen", + "InferenceAcceleratorInfo": null, + "InstanceStorageInfo": { + "Disks": [ + { + "Count": 2, + "SizeInGB": 420, + "Type": "hdd" + } + ], + "TotalSizeInGB": 840 + }, + "InstanceStorageSupported": true, + "InstanceType": "m1.large", + "MemoryInfo": { + "SizeInMiB": 7680 + }, + "NetworkInfo": { + "EfaSupported": false, + "EnaSupport": "unsupported", + "Ipv4AddressesPerInterface": 10, + "Ipv6AddressesPerInterface": 0, + "Ipv6Supported": false, + "MaximumNetworkInterfaces": 3, + "NetworkPerformance": "Moderate" + }, + "PlacementGroupInfo": { + "SupportedStrategies": [ + "partition", + "spread" + ] + }, + "ProcessorInfo": { + "SupportedArchitectures": [ + "x86_64" + ], + "SustainedClockSpeedInGhz": null + }, + "SupportedRootDeviceTypes": [ + "ebs", + "instance-store" + ], + "SupportedUsageClasses": [ + "on-demand", + "spot" + ], + "SupportedVirtualizationTypes": [ + "hvm", + "paravirtual" + ], + "VCpuInfo": { + "DefaultCores": 2, + "DefaultThreadsPerCore": 1, + "DefaultVCpus": 2, + "ValidCores": [ + 1, + 2 + ], + "ValidThreadsPerCore": [ + 1 + ] + } + }, + { + "AutoRecoverySupported": false, + "BareMetal": false, + "BurstablePerformanceSupported": false, + "CurrentGeneration": false, + "DedicatedHostsSupported": false, + "EbsInfo": { + "EbsOptimizedInfo": null, + "EbsOptimizedSupport": "unsupported", + "EncryptionSupport": "unsupported" + }, + "FpgaInfo": null, + "FreeTierEligible": false, + "GpuInfo": null, + "HibernationSupported": false, + "Hypervisor": "xen", + "InferenceAcceleratorInfo": null, + "InstanceStorageInfo": { + "Disks": [ + { + "Count": 1, + "SizeInGB": 410, + "Type": "hdd" + } + ], + "TotalSizeInGB": 410 + }, + "InstanceStorageSupported": true, + "InstanceType": "m1.medium", + "MemoryInfo": { + "SizeInMiB": 3788 + }, + "NetworkInfo": { + "EfaSupported": false, + "EnaSupport": "unsupported", + "Ipv4AddressesPerInterface": 6, + "Ipv6AddressesPerInterface": 0, + "Ipv6Supported": false, + "MaximumNetworkInterfaces": 2, + "NetworkPerformance": "Moderate" + }, + "PlacementGroupInfo": { + "SupportedStrategies": [ + "partition", + "spread" + ] + }, + "ProcessorInfo": { + "SupportedArchitectures": [ + "i386", + "x86_64" + ], + "SustainedClockSpeedInGhz": null + }, + "SupportedRootDeviceTypes": [ + "ebs", + "instance-store" + ], + "SupportedUsageClasses": [ + "on-demand", + "spot" + ], + "SupportedVirtualizationTypes": [ + "hvm", + "paravirtual" + ], + "VCpuInfo": { + "DefaultCores": 1, + "DefaultThreadsPerCore": 1, + "DefaultVCpus": 1, + "ValidCores": [ + 1 + ], + "ValidThreadsPerCore": [ + 1 + ] + } + }, + { + "AutoRecoverySupported": false, + "BareMetal": false, + "BurstablePerformanceSupported": false, + "CurrentGeneration": false, + "DedicatedHostsSupported": false, + "EbsInfo": { + "EbsOptimizedInfo": null, + "EbsOptimizedSupport": "unsupported", + "EncryptionSupport": "unsupported" + }, + "FpgaInfo": null, + "FreeTierEligible": false, + "GpuInfo": null, + "HibernationSupported": false, + "Hypervisor": "xen", + "InferenceAcceleratorInfo": null, + "InstanceStorageInfo": { + "Disks": [ + { + "Count": 1, + "SizeInGB": 160, + "Type": "hdd" + } + ], + "TotalSizeInGB": 160 + }, + "InstanceStorageSupported": true, + "InstanceType": "m1.small", + "MemoryInfo": { + "SizeInMiB": 1740 + }, + "NetworkInfo": { + "EfaSupported": false, + "EnaSupport": "unsupported", + "Ipv4AddressesPerInterface": 4, + "Ipv6AddressesPerInterface": 0, + "Ipv6Supported": false, + "MaximumNetworkInterfaces": 2, + "NetworkPerformance": "Low" + }, + "PlacementGroupInfo": { + "SupportedStrategies": [ + "partition", + "spread" + ] + }, + "ProcessorInfo": { + "SupportedArchitectures": [ + "i386", + "x86_64" + ], + "SustainedClockSpeedInGhz": null + }, + "SupportedRootDeviceTypes": [ + "ebs", + "instance-store" + ], + "SupportedUsageClasses": [ + "on-demand", + "spot" + ], + "SupportedVirtualizationTypes": [ + "hvm", + "paravirtual" + ], + "VCpuInfo": { + "DefaultCores": 1, + "DefaultThreadsPerCore": 1, + "DefaultVCpus": 1, + "ValidCores": [ + 1 + ], + "ValidThreadsPerCore": [ + 1 + ] + } + }, + { + "AutoRecoverySupported": false, + "BareMetal": false, + "BurstablePerformanceSupported": false, + "CurrentGeneration": false, + "DedicatedHostsSupported": false, + "EbsInfo": { + "EbsOptimizedInfo": { + "BaselineBandwidthInMbps": 1000, + "BaselineIops": 8000, + "BaselineThroughputInMBps": 125, + "MaximumBandwidthInMbps": 1000, + "MaximumIops": 8000, + "MaximumThroughputInMBps": 125 + }, + "EbsOptimizedSupport": "supported", + "EncryptionSupport": "unsupported" + }, + "FpgaInfo": null, + "FreeTierEligible": false, + "GpuInfo": null, + "HibernationSupported": false, + "Hypervisor": "xen", + "InferenceAcceleratorInfo": null, + "InstanceStorageInfo": { + "Disks": [ + { + "Count": 4, + "SizeInGB": 420, + "Type": "hdd" + } + ], + "TotalSizeInGB": 1680 + }, + "InstanceStorageSupported": true, + "InstanceType": "m1.xlarge", + "MemoryInfo": { + "SizeInMiB": 15360 + }, + "NetworkInfo": { + "EfaSupported": false, + "EnaSupport": "unsupported", + "Ipv4AddressesPerInterface": 15, + "Ipv6AddressesPerInterface": 0, + "Ipv6Supported": false, + "MaximumNetworkInterfaces": 4, + "NetworkPerformance": "High" + }, + "PlacementGroupInfo": { + "SupportedStrategies": [ + "partition", + "spread" + ] + }, + "ProcessorInfo": { + "SupportedArchitectures": [ + "x86_64" + ], + "SustainedClockSpeedInGhz": null + }, + "SupportedRootDeviceTypes": [ + "ebs", + "instance-store" + ], + "SupportedUsageClasses": [ + "on-demand", + "spot" + ], + "SupportedVirtualizationTypes": [ + "hvm", + "paravirtual" + ], + "VCpuInfo": { + "DefaultCores": 4, + "DefaultThreadsPerCore": 1, + "DefaultVCpus": 4, + "ValidCores": [ + 1, + 2, + 3, + 4 + ], + "ValidThreadsPerCore": [ + 1 + ] + } + }, + { + "AutoRecoverySupported": false, + "BareMetal": false, + "BurstablePerformanceSupported": false, + "CurrentGeneration": false, + "DedicatedHostsSupported": false, + "EbsInfo": { + "EbsOptimizedInfo": { + "BaselineBandwidthInMbps": 500, + "BaselineIops": 4000, + "BaselineThroughputInMBps": 62.5, + "MaximumBandwidthInMbps": 500, + "MaximumIops": 4000, + "MaximumThroughputInMBps": 62.5 + }, + "EbsOptimizedSupport": "supported", + "EncryptionSupport": "unsupported" + }, + "FpgaInfo": null, + "FreeTierEligible": false, + "GpuInfo": null, + "HibernationSupported": false, + "Hypervisor": "xen", + "InferenceAcceleratorInfo": null, + "InstanceStorageInfo": { + "Disks": [ + { + "Count": 1, + "SizeInGB": 850, + "Type": "hdd" + } + ], + "TotalSizeInGB": 850 + }, + "InstanceStorageSupported": true, + "InstanceType": "m2.2xlarge", + "MemoryInfo": { + "SizeInMiB": 35020 + }, + "NetworkInfo": { + "EfaSupported": false, + "EnaSupport": "unsupported", + "Ipv4AddressesPerInterface": 30, + "Ipv6AddressesPerInterface": 0, + "Ipv6Supported": false, + "MaximumNetworkInterfaces": 4, + "NetworkPerformance": "Moderate" + }, + "PlacementGroupInfo": { + "SupportedStrategies": [ + "partition", + "spread" + ] + }, + "ProcessorInfo": { + "SupportedArchitectures": [ + "x86_64" + ], + "SustainedClockSpeedInGhz": null + }, + "SupportedRootDeviceTypes": [ + "ebs", + "instance-store" + ], + "SupportedUsageClasses": [ + "on-demand", + "spot" + ], + "SupportedVirtualizationTypes": [ + "hvm", + "paravirtual" + ], + "VCpuInfo": { + "DefaultCores": 4, + "DefaultThreadsPerCore": 1, + "DefaultVCpus": 4, + "ValidCores": [ + 1, + 2, + 3, + 4 + ], + "ValidThreadsPerCore": [ + 1 + ] + } + }, + { + "AutoRecoverySupported": false, + "BareMetal": false, + "BurstablePerformanceSupported": false, + "CurrentGeneration": false, + "DedicatedHostsSupported": false, + "EbsInfo": { + "EbsOptimizedInfo": { + "BaselineBandwidthInMbps": 1000, + "BaselineIops": 8000, + "BaselineThroughputInMBps": 125, + "MaximumBandwidthInMbps": 1000, + "MaximumIops": 8000, + "MaximumThroughputInMBps": 125 + }, + "EbsOptimizedSupport": "supported", + "EncryptionSupport": "unsupported" + }, + "FpgaInfo": null, + "FreeTierEligible": false, + "GpuInfo": null, + "HibernationSupported": false, + "Hypervisor": "xen", + "InferenceAcceleratorInfo": null, + "InstanceStorageInfo": { + "Disks": [ + { + "Count": 2, + "SizeInGB": 840, + "Type": "hdd" + } + ], + "TotalSizeInGB": 1680 + }, + "InstanceStorageSupported": true, + "InstanceType": "m2.4xlarge", + "MemoryInfo": { + "SizeInMiB": 70041 + }, + "NetworkInfo": { + "EfaSupported": false, + "EnaSupport": "unsupported", + "Ipv4AddressesPerInterface": 30, + "Ipv6AddressesPerInterface": 0, + "Ipv6Supported": false, + "MaximumNetworkInterfaces": 8, + "NetworkPerformance": "High" + }, + "PlacementGroupInfo": { + "SupportedStrategies": [ + "partition", + "spread" + ] + }, + "ProcessorInfo": { + "SupportedArchitectures": [ + "x86_64" + ], + "SustainedClockSpeedInGhz": null + }, + "SupportedRootDeviceTypes": [ + "ebs", + "instance-store" + ], + "SupportedUsageClasses": [ + "on-demand", + "spot" + ], + "SupportedVirtualizationTypes": [ + "paravirtual", + "hvm" + ], + "VCpuInfo": { + "DefaultCores": 8, + "DefaultThreadsPerCore": 1, + "DefaultVCpus": 8, + "ValidCores": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ], + "ValidThreadsPerCore": [ + 1 + ] + } + }, + { + "AutoRecoverySupported": false, + "BareMetal": false, + "BurstablePerformanceSupported": false, + "CurrentGeneration": false, + "DedicatedHostsSupported": false, + "EbsInfo": { + "EbsOptimizedInfo": null, + "EbsOptimizedSupport": "unsupported", + "EncryptionSupport": "unsupported" + }, + "FpgaInfo": null, + "FreeTierEligible": false, + "GpuInfo": null, + "HibernationSupported": false, + "Hypervisor": "xen", + "InferenceAcceleratorInfo": null, + "InstanceStorageInfo": { + "Disks": [ + { + "Count": 1, + "SizeInGB": 420, + "Type": "hdd" + } + ], + "TotalSizeInGB": 420 + }, + "InstanceStorageSupported": true, + "InstanceType": "m2.xlarge", + "MemoryInfo": { + "SizeInMiB": 17510 + }, + "NetworkInfo": { + "EfaSupported": false, + "EnaSupport": "unsupported", + "Ipv4AddressesPerInterface": 15, + "Ipv6AddressesPerInterface": 0, + "Ipv6Supported": false, + "MaximumNetworkInterfaces": 4, + "NetworkPerformance": "Moderate" + }, + "PlacementGroupInfo": { + "SupportedStrategies": [ + "partition", + "spread" + ] + }, + "ProcessorInfo": { + "SupportedArchitectures": [ + "x86_64" + ], + "SustainedClockSpeedInGhz": null + }, + "SupportedRootDeviceTypes": [ + "ebs", + "instance-store" + ], + "SupportedUsageClasses": [ + "on-demand", + "spot" + ], + "SupportedVirtualizationTypes": [ + "hvm", + "paravirtual" + ], + "VCpuInfo": { + "DefaultCores": 2, + "DefaultThreadsPerCore": 1, + "DefaultVCpus": 2, + "ValidCores": [ + 1, + 2 + ], + "ValidThreadsPerCore": [ + 1 + ] + } + }, + { + "AutoRecoverySupported": true, + "BareMetal": false, + "BurstablePerformanceSupported": false, + "CurrentGeneration": false, + "DedicatedHostsSupported": true, + "EbsInfo": { + "EbsOptimizedInfo": { + "BaselineBandwidthInMbps": 1000, + "BaselineIops": 8000, + "BaselineThroughputInMBps": 125, + "MaximumBandwidthInMbps": 1000, + "MaximumIops": 8000, + "MaximumThroughputInMBps": 125 + }, + "EbsOptimizedSupport": "supported", + "EncryptionSupport": "supported" + }, + "FpgaInfo": null, + "FreeTierEligible": false, + "GpuInfo": null, + "HibernationSupported": true, + "Hypervisor": "xen", + "InferenceAcceleratorInfo": null, + "InstanceStorageInfo": { + "Disks": [ + { + "Count": 2, + "SizeInGB": 80, + "Type": "ssd" + } + ], + "TotalSizeInGB": 160 + }, + "InstanceStorageSupported": true, + "InstanceType": "m3.2xlarge", + "MemoryInfo": { + "SizeInMiB": 30720 + }, + "NetworkInfo": { + "EfaSupported": false, + "EnaSupport": "unsupported", + "Ipv4AddressesPerInterface": 30, + "Ipv6AddressesPerInterface": 0, + "Ipv6Supported": false, + "MaximumNetworkInterfaces": 4, + "NetworkPerformance": "High" + }, + "PlacementGroupInfo": { + "SupportedStrategies": [ + "partition", + "spread" + ] + }, + "ProcessorInfo": { + "SupportedArchitectures": [ + "x86_64" + ], + "SustainedClockSpeedInGhz": 2.5 + }, + "SupportedRootDeviceTypes": [ + "ebs", + "instance-store" + ], + "SupportedUsageClasses": [ + "on-demand", + "spot" + ], + "SupportedVirtualizationTypes": [ + "paravirtual", + "hvm" + ], + "VCpuInfo": { + "DefaultCores": 4, + "DefaultThreadsPerCore": 2, + "DefaultVCpus": 8, + "ValidCores": [ + 1, + 2, + 3, + 4 + ], + "ValidThreadsPerCore": [ + 1, + 2 + ] + } + }, + { + "AutoRecoverySupported": true, + "BareMetal": false, + "BurstablePerformanceSupported": false, + "CurrentGeneration": false, + "DedicatedHostsSupported": true, + "EbsInfo": { + "EbsOptimizedInfo": null, + "EbsOptimizedSupport": "unsupported", + "EncryptionSupport": "supported" + }, + "FpgaInfo": null, + "FreeTierEligible": false, + "GpuInfo": null, + "HibernationSupported": true, + "Hypervisor": "xen", + "InferenceAcceleratorInfo": null, + "InstanceStorageInfo": { + "Disks": [ + { + "Count": 1, + "SizeInGB": 32, + "Type": "ssd" + } + ], + "TotalSizeInGB": 32 + }, + "InstanceStorageSupported": true, + "InstanceType": "m3.large", + "MemoryInfo": { + "SizeInMiB": 7680 + }, + "NetworkInfo": { + "EfaSupported": false, + "EnaSupport": "unsupported", + "Ipv4AddressesPerInterface": 10, + "Ipv6AddressesPerInterface": 0, + "Ipv6Supported": false, + "MaximumNetworkInterfaces": 3, + "NetworkPerformance": "Moderate" + }, + "PlacementGroupInfo": { + "SupportedStrategies": [ + "partition", + "spread" + ] + }, + "ProcessorInfo": { + "SupportedArchitectures": [ + "x86_64" + ], + "SustainedClockSpeedInGhz": 2.5 + }, + "SupportedRootDeviceTypes": [ + "ebs", + "instance-store" + ], + "SupportedUsageClasses": [ + "on-demand", + "spot" + ], + "SupportedVirtualizationTypes": [ + "hvm", + "paravirtual" + ], + "VCpuInfo": { + "DefaultCores": 2, + "DefaultThreadsPerCore": 1, + "DefaultVCpus": 2, + "ValidCores": [ + 1, + 2 + ], + "ValidThreadsPerCore": [ + 1 + ] + } + }, + { + "AutoRecoverySupported": true, + "BareMetal": false, + "BurstablePerformanceSupported": false, + "CurrentGeneration": false, + "DedicatedHostsSupported": true, + "EbsInfo": { + "EbsOptimizedInfo": null, + "EbsOptimizedSupport": "unsupported", + "EncryptionSupport": "supported" + }, + "FpgaInfo": null, + "FreeTierEligible": false, + "GpuInfo": null, + "HibernationSupported": true, + "Hypervisor": "xen", + "InferenceAcceleratorInfo": null, + "InstanceStorageInfo": { + "Disks": [ + { + "Count": 1, + "SizeInGB": 4, + "Type": "ssd" + } + ], + "TotalSizeInGB": 4 + }, + "InstanceStorageSupported": true, + "InstanceType": "m3.medium", + "MemoryInfo": { + "SizeInMiB": 3840 + }, + "NetworkInfo": { + "EfaSupported": false, + "EnaSupport": "unsupported", + "Ipv4AddressesPerInterface": 6, + "Ipv6AddressesPerInterface": 0, + "Ipv6Supported": false, + "MaximumNetworkInterfaces": 2, + "NetworkPerformance": "Moderate" + }, + "PlacementGroupInfo": { + "SupportedStrategies": [ + "partition", + "spread" + ] + }, + "ProcessorInfo": { + "SupportedArchitectures": [ + "x86_64" + ], + "SustainedClockSpeedInGhz": 2.5 + }, + "SupportedRootDeviceTypes": [ + "ebs", + "instance-store" + ], + "SupportedUsageClasses": [ + "on-demand", + "spot" + ], + "SupportedVirtualizationTypes": [ + "hvm", + "paravirtual" + ], + "VCpuInfo": { + "DefaultCores": 1, + "DefaultThreadsPerCore": 1, + "DefaultVCpus": 1, + "ValidCores": [ + 1 + ], + "ValidThreadsPerCore": [ + 1 + ] + } + }, + { + "AutoRecoverySupported": true, + "BareMetal": false, + "BurstablePerformanceSupported": false, + "CurrentGeneration": false, + "DedicatedHostsSupported": true, + "EbsInfo": { + "EbsOptimizedInfo": { + "BaselineBandwidthInMbps": 500, + "BaselineIops": 4000, + "BaselineThroughputInMBps": 62.5, + "MaximumBandwidthInMbps": 500, + "MaximumIops": 4000, + "MaximumThroughputInMBps": 62.5 + }, + "EbsOptimizedSupport": "supported", + "EncryptionSupport": "supported" + }, + "FpgaInfo": null, + "FreeTierEligible": false, + "GpuInfo": null, + "HibernationSupported": true, + "Hypervisor": "xen", + "InferenceAcceleratorInfo": null, + "InstanceStorageInfo": { + "Disks": [ + { + "Count": 2, + "SizeInGB": 40, + "Type": "ssd" + } + ], + "TotalSizeInGB": 80 + }, + "InstanceStorageSupported": true, + "InstanceType": "m3.xlarge", + "MemoryInfo": { + "SizeInMiB": 15360 + }, + "NetworkInfo": { + "EfaSupported": false, + "EnaSupport": "unsupported", + "Ipv4AddressesPerInterface": 15, + "Ipv6AddressesPerInterface": 0, + "Ipv6Supported": false, + "MaximumNetworkInterfaces": 4, + "NetworkPerformance": "High" + }, + "PlacementGroupInfo": { + "SupportedStrategies": [ + "partition", + "spread" + ] + }, + "ProcessorInfo": { + "SupportedArchitectures": [ + "x86_64" + ], + "SustainedClockSpeedInGhz": 2.5 + }, + "SupportedRootDeviceTypes": [ + "ebs", + "instance-store" + ], + "SupportedUsageClasses": [ + "on-demand", + "spot" + ], + "SupportedVirtualizationTypes": [ + "hvm", + "paravirtual" + ], + "VCpuInfo": { + "DefaultCores": 4, + "DefaultThreadsPerCore": 1, + "DefaultVCpus": 4, + "ValidCores": [ + 1, + 2, + 3, + 4 + ], + "ValidThreadsPerCore": [ + 1 + ] + } + }, + { + "AutoRecoverySupported": false, + "BareMetal": false, + "BurstablePerformanceSupported": false, + "CurrentGeneration": false, + "DedicatedHostsSupported": false, + "EbsInfo": { + "EbsOptimizedInfo": null, + "EbsOptimizedSupport": "unsupported", + "EncryptionSupport": "unsupported" + }, + "FpgaInfo": null, + "FreeTierEligible": true, + "GpuInfo": null, + "HibernationSupported": false, + "Hypervisor": "xen", + "InferenceAcceleratorInfo": null, + "InstanceStorageInfo": null, + "InstanceStorageSupported": false, + "InstanceType": "t1.micro", + "MemoryInfo": { + "SizeInMiB": 627 + }, + "NetworkInfo": { + "EfaSupported": false, + "EnaSupport": "unsupported", + "Ipv4AddressesPerInterface": 2, + "Ipv6AddressesPerInterface": 0, + "Ipv6Supported": false, + "MaximumNetworkInterfaces": 2, + "NetworkPerformance": "Very Low" + }, + "PlacementGroupInfo": { + "SupportedStrategies": [ + "partition", + "spread" + ] + }, + "ProcessorInfo": { + "SupportedArchitectures": [ + "i386", + "x86_64" + ], + "SustainedClockSpeedInGhz": null + }, + "SupportedRootDeviceTypes": [ + "ebs" + ], + "SupportedUsageClasses": [ + "on-demand", + "spot" + ], + "SupportedVirtualizationTypes": [ + "paravirtual", + "hvm" + ], + "VCpuInfo": { + "DefaultCores": 1, + "DefaultThreadsPerCore": 1, + "DefaultVCpus": 1, + "ValidCores": [ + 1 + ], + "ValidThreadsPerCore": [ + 1 + ] + } + } + ] +} \ No newline at end of file diff --git a/test/static/GithubEKSAMIRelease/amazon-eks-ami-20210125.zip b/test/static/GithubEKSAMIRelease/amazon-eks-ami-20210125.zip new file mode 100644 index 0000000000000000000000000000000000000000..92caeca2d7891e421560970fe551bacbdb64d267 GIT binary patch literal 52886 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 literal 0 HcmV?d00001