Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use DescribeInstanceTypes API to get EC2 instance type details #4468

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion cluster-autoscaler/cloudprovider/aws/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ The following policy provides the minimum privileges necessary for Cluster Autos
"autoscaling:DescribeAutoScalingInstances",
"autoscaling:DescribeLaunchConfigurations",
"autoscaling:SetDesiredCapacity",
"autoscaling:TerminateInstanceInAutoScalingGroup"
"autoscaling:TerminateInstanceInAutoScalingGroup",
"ec2:DescribeInstanceTypes"
AustinSiu marked this conversation as resolved.
Show resolved Hide resolved
],
"Resource": ["*"]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ var getInstanceTypeForAsg = func(m *asgCache, group *asg) (string, error) {
return result[group.AwsRef.Name], nil
}

return "", fmt.Errorf("Could not find instance type for %s", group.AwsRef.Name)
return "", fmt.Errorf("could not find instance type for %s", group.AwsRef.Name)
}

// Fetch explicitly configured ASGs. These ASGs should never be unregistered
Expand Down
2 changes: 1 addition & 1 deletion cluster-autoscaler/cloudprovider/aws/aws_cloud_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ func (ng *AwsNodeGroup) DeleteNodes(nodes []*apiv1.Node) error {
if err != nil {
return err
}
if belongs != true {
if !belongs {
return fmt.Errorf("%s belongs to a different asg than %s", node.Name, ng.Id())
}
awsref, err := AwsRefFromProviderId(node.Spec.ProviderID)
Expand Down
196 changes: 46 additions & 150 deletions cluster-autoscaler/cloudprovider/aws/aws_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,104 +17,40 @@ limitations under the License.
package aws

import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"regexp"
"strconv"
"strings"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/ec2metadata"
"github.com/aws/aws-sdk-go/aws/endpoints"
"github.com/aws/aws-sdk-go/aws/session"

klog "k8s.io/klog/v2"
"github.com/aws/aws-sdk-go/service/ec2"
)

var (
ec2MetaDataServiceUrl = "http://169.254.169.254"
ec2PricingServiceUrlTemplate = "https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AmazonEC2/current/%s/index.json"
ec2PricingServiceUrlTemplateCN = "https://pricing.cn-north-1.amazonaws.com.cn/offers/v1.0/cn/AmazonEC2/current/%s/index.json"
ec2Arm64Processors = []string{"AWS Graviton Processor", "AWS Graviton2 Processor"}
ec2MetaDataServiceUrl = "http://169.254.169.254"
)

type response struct {
Products map[string]product `json:"products"`
}

type product struct {
Attributes productAttributes `json:"attributes"`
}

type productAttributes struct {
InstanceType string `json:"instanceType"`
VCPU string `json:"vcpu"`
Memory string `json:"memory"`
GPU string `json:"gpu"`
Architecture string `json:"physicalProcessor"`
}

// GenerateEC2InstanceTypes returns a map of ec2 resources
func GenerateEC2InstanceTypes(region string) (map[string]*InstanceType, error) {
var pricingUrlTemplate string
if strings.HasPrefix(region, "cn-") {
pricingUrlTemplate = ec2PricingServiceUrlTemplateCN
} else {
pricingUrlTemplate = ec2PricingServiceUrlTemplate
sess, err := session.NewSession(&aws.Config{
Region: aws.String(region)},
)
if err != nil {
return nil, err
}

ec2Client := ec2.New(sess)
input := ec2.DescribeInstanceTypesInput{}
instanceTypes := make(map[string]*InstanceType)

resolver := endpoints.DefaultResolver()
partitions := resolver.(endpoints.EnumPartitions).Partitions()

for _, p := range partitions {
for _, r := range p.Regions() {
if region != "" && region != r.ID() {
continue
}

url := fmt.Sprintf(pricingUrlTemplate, r.ID())
klog.V(1).Infof("fetching %s\n", url)
res, err := http.Get(url)
if err != nil {
klog.Warningf("Error fetching %s skipping...\n%s\n", url, err)
continue
}

defer res.Body.Close()

unmarshalled, err := unmarshalProductsResponse(res.Body)
if err != nil {
klog.Warningf("Error parsing %s skipping...\n%s\n", url, err)
continue
}

for _, product := range unmarshalled.Products {
attr := product.Attributes
if attr.InstanceType != "" {
instanceTypes[attr.InstanceType] = &InstanceType{
InstanceType: attr.InstanceType,
}
if attr.Memory != "" && attr.Memory != "NA" {
instanceTypes[attr.InstanceType].MemoryMb = parseMemory(attr.Memory)
}
if attr.VCPU != "" {
instanceTypes[attr.InstanceType].VCPU = parseCPU(attr.VCPU)
}
if attr.GPU != "" {
instanceTypes[attr.InstanceType].GPU = parseCPU(attr.GPU)
}
if attr.Architecture != "" {
instanceTypes[attr.InstanceType].Architecture = parseArchitecture(attr.Architecture)
}
}
}
if err = ec2Client.DescribeInstanceTypesPages(&input, func(page *ec2.DescribeInstanceTypesOutput, isLastPage bool) bool {
for _, rawInstanceType := range page.InstanceTypes {
instanceTypes[*rawInstanceType.InstanceType] = transformInstanceType(rawInstanceType)
}
return !isLastPage
}); err != nil {
return nil, err
}

if len(instanceTypes) == 0 {
Expand All @@ -129,88 +65,48 @@ func GetStaticEC2InstanceTypes() (map[string]*InstanceType, string) {
return InstanceTypes, StaticListLastUpdateTime
}

func unmarshalProductsResponse(r io.Reader) (*response, error) {
dec := json.NewDecoder(r)
t, err := dec.Token()
if err != nil {
return nil, err
func transformInstanceType(rawInstanceType *ec2.InstanceTypeInfo) *InstanceType {
instanceType := &InstanceType{
InstanceType: *rawInstanceType.InstanceType,
}
if delim, ok := t.(json.Delim); !ok || delim.String() != "{" {
return nil, errors.New("Invalid products json")
if rawInstanceType.MemoryInfo != nil && rawInstanceType.MemoryInfo.SizeInMiB != nil {
instanceType.MemoryMb = *rawInstanceType.MemoryInfo.SizeInMiB
}

unmarshalled := response{map[string]product{}}

for dec.More() {
t, err = dec.Token()
if err != nil {
return nil, err
}

if t == "products" {
tt, err := dec.Token()
if err != nil {
return nil, err
}
if delim, ok := tt.(json.Delim); !ok || delim.String() != "{" {
return nil, errors.New("Invalid products json")
}
for dec.More() {
productCode, err := dec.Token()
if err != nil {
return nil, err
}

prod := product{}
if err = dec.Decode(&prod); err != nil {
return nil, err
}
unmarshalled.Products[productCode.(string)] = prod
}
}
}

t, err = dec.Token()
if err != nil {
return nil, err
}
if delim, ok := t.(json.Delim); !ok || delim.String() != "}" {
return nil, errors.New("Invalid products json")
if rawInstanceType.VCpuInfo != nil && rawInstanceType.VCpuInfo.DefaultVCpus != nil {
instanceType.VCPU = *rawInstanceType.VCpuInfo.DefaultVCpus
}

return &unmarshalled, nil
}

func parseMemory(memory string) int64 {
reg, err := regexp.Compile("[^0-9\\.]+")
if err != nil {
klog.Fatal(err)
if rawInstanceType.GpuInfo != nil && len(rawInstanceType.GpuInfo.Gpus) > 0 {
instanceType.GPU = getGpuCount(rawInstanceType.GpuInfo)
}

parsed := strings.TrimSpace(reg.ReplaceAllString(memory, ""))
mem, err := strconv.ParseFloat(parsed, 64)
if err != nil {
klog.Fatal(err)
if rawInstanceType.ProcessorInfo != nil && len(rawInstanceType.ProcessorInfo.SupportedArchitectures) > 0 {
instanceType.Architecture = interpretEc2SupportedArchitecure(*rawInstanceType.ProcessorInfo.SupportedArchitectures[0])
}

return int64(mem * float64(1024))
return instanceType
}

func parseCPU(cpu string) int64 {
i, err := strconv.ParseInt(cpu, 10, 64)
if err != nil {
klog.Fatal(err)
func getGpuCount(gpuInfo *ec2.GpuInfo) int64 {
var gpuCountSum int64
for _, gpu := range gpuInfo.Gpus {
if gpu.Count != nil {
gpuCountSum += *gpu.Count
}
}
return i
return gpuCountSum
}

func parseArchitecture(archName string) string {
for _, processor := range ec2Arm64Processors {
if archName == processor {
return "arm64"
}
func interpretEc2SupportedArchitecure(archName string) string {
switch archName {
case "arm64":
return "arm64"
case "i386":
return "amd64"
case "x86_64":
return "amd64"
case "x86_64_mac":
return "amd64"
default:
return "amd64"
}
return "amd64"
}

// GetCurrentAwsRegion return region of current cluster without building awsManager
Expand Down
Loading