Skip to content

Commit

Permalink
Use DescribeInstanceTypes API to get EC2 instance type details
Browse files Browse the repository at this point in the history
  • Loading branch information
AustinSiu committed Nov 16, 2021
1 parent c6ca727 commit 955eaef
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 311 deletions.
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"
],
"Resource": ["*"]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,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 @@ -285,7 +285,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
195 changes: 48 additions & 147 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"}
)

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,53 @@ 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
}
}
if rawInstanceType.VCpuInfo != nil && rawInstanceType.VCpuInfo.DefaultVCpus != nil {
instanceType.VCPU = *rawInstanceType.VCpuInfo.DefaultVCpus
}

t, err = dec.Token()
if err != nil {
return nil, err
if rawInstanceType.GpuInfo != nil && len(rawInstanceType.GpuInfo.Gpus) > 0 {
instanceType.GPU = getGpuCount(rawInstanceType.GpuInfo)
}
if delim, ok := t.(json.Delim); !ok || delim.String() != "}" {
return nil, errors.New("Invalid products json")
if rawInstanceType.ProcessorInfo != nil && len(rawInstanceType.ProcessorInfo.SupportedArchitectures) > 0 {
instanceType.Architecture = interpretEc2SupportedArchitecure(*rawInstanceType.ProcessorInfo.SupportedArchitectures[0])
}

return &unmarshalled, nil
return instanceType
}

func parseMemory(memory string) int64 {
reg, err := regexp.Compile("[^0-9\\.]+")
if err != nil {
klog.Fatal(err)
}

parsed := strings.TrimSpace(reg.ReplaceAllString(memory, ""))
mem, err := strconv.ParseFloat(parsed, 64)
if err != nil {
klog.Fatal(err)
}

return int64(mem * float64(1024))
// GetStaticEC2InstanceTypes return pregenerated ec2 instance type list
func GetStaticEC2InstanceTypes() (map[string]*InstanceType, string) {
return InstanceTypes, staticListLastUpdateTime
}

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

0 comments on commit 955eaef

Please sign in to comment.