-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
initial work on resource allocation and cost split
- Loading branch information
1 parent
caf4b0d
commit 645370c
Showing
8 changed files
with
376 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
package global | ||
|
||
import ( | ||
"context" | ||
"strings" | ||
"sync" | ||
|
||
"github.com/aws/aws-sdk-go-v2/aws" | ||
"github.com/aws/aws-sdk-go-v2/config" | ||
"github.com/aws/aws-sdk-go-v2/service/ssm" | ||
"github.com/pkg/errors" | ||
) | ||
|
||
type Region struct { | ||
ID string | ||
LongName string | ||
} | ||
|
||
var ( | ||
once sync.Once | ||
regionMap map[string]Region | ||
) | ||
|
||
// GetRegionMap returns a map of region ID to Region | ||
func GetRegionMap(ctx context.Context) (map[string]Region, error) { | ||
var err error | ||
// load the region map from SSM parameter store, do it only once | ||
once.Do(func() { | ||
regionMap, err = loadRegionMap(ctx) | ||
}) | ||
return regionMap, err | ||
} | ||
|
||
// loadRegionMap lazy load the region map from SSM parameter store | ||
func loadRegionMap(ctx context.Context) (map[string]Region, error) { | ||
// create a new Amazon SSM client | ||
cfg, err := config.LoadDefaultConfig(ctx) | ||
if err != nil { | ||
return nil, errors.Wrap(err, "loading AWS config") | ||
} | ||
svc := ssm.NewFromConfig(cfg) | ||
// initialize the region map | ||
regionMap = make(map[string]Region) | ||
// get the region map from SSM parameter store | ||
// /aws/service/global-infrastructure/regions | ||
var nextToken *string | ||
for { | ||
// Request all regions, paginating the results if needed | ||
input := &ssm.GetParametersByPathInput{ | ||
Path: aws.String("/aws/service/global-infrastructure/regions"), | ||
NextToken: nextToken, | ||
} | ||
output, err := svc.GetParametersByPath(ctx, input) | ||
if err != nil { | ||
return nil, errors.Wrap(err, "getting region map from SSM parameter store") | ||
} | ||
|
||
// construct parameter names from the output | ||
names := make([]string, 0, len(output.Parameters)) | ||
for _, param := range output.Parameters { | ||
region := (*param.Name)[strings.LastIndex(*param.Name, "/")+1:] | ||
names = append(names, "/aws/service/global-infrastructure/regions/"+region+"/longName") | ||
} | ||
|
||
paramsOutput, err := svc.GetParameters(ctx, &ssm.GetParametersInput{Names: names}) | ||
if err != nil { | ||
return nil, errors.Wrap(err, "getting region longName from SSM parameter store") | ||
} | ||
|
||
for _, param := range paramsOutput.Parameters { | ||
// get the region ID from the parameter name two before the last slash | ||
tokens := strings.Split(*param.Name, "/") | ||
region := tokens[len(tokens)-2] | ||
regionMap[region] = Region{ | ||
ID: region, | ||
LongName: *param.Value, | ||
} | ||
} | ||
|
||
// if there are more regions, get the next page | ||
nextToken = output.NextToken | ||
if nextToken == nil { | ||
break | ||
} | ||
} | ||
return regionMap, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
package price | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"net/http" | ||
"net/url" | ||
"strconv" | ||
"strings" | ||
|
||
"github.com/doitintl/eks-lens-agent/internal/aws/global" | ||
"github.com/pkg/errors" | ||
) | ||
|
||
/* | ||
Case-sensitive URL addresses for EC2 pricing: | ||
Linux: https://b0.p.awsstatic.com/pricing/2.0/meteredUnitMaps/ec2/USD/current/ec2-ondemand-without-sec-sel/$region/Linux/index.json | ||
Windows: https://b0.p.awsstatic.com/pricing/2.0/meteredUnitMaps/ec2/USD/current/ec2-ondemand-without-sec-sel/$region/Windows/index.json | ||
RHEL: https://b0.p.awsstatic.com/pricing/2.0/meteredUnitMaps/ec2/USD/current/ec2-ondemand-without-sec-sel/$region/RHEL/index.json | ||
SUSE: https://b0.p.awsstatic.com/pricing/2.0/meteredUnitMaps/ec2/USD/current/ec2-ondemand-without-sec-sel/$region/SUSE/index.json | ||
Ubuntu Pro: https://b0.p.awsstatic.com/pricing/2.0/meteredUnitMaps/ec2/USD/current/ec2-ondemand-without-sec-sel/US%20East%20(Ohio)/Ubuntu%20Pro/index.json | ||
where region is URL encoded long name of the region, e.g. us-east-2 is US%20East%20(Ohio) | ||
*/ | ||
|
||
type Prices map[string]float64 | ||
|
||
var ( | ||
regionOSPrices = map[string]Prices{} | ||
) | ||
|
||
func GetInstancePrice(ctx context.Context, regionID, os, osImage, instanceType string) (float64, error) { | ||
// load regions map | ||
regions, err := global.GetRegionMap(ctx) | ||
if err != nil { | ||
return 0, errors.Wrap(err, "loading regions map") | ||
} | ||
// get regionID long name | ||
region, ok := regions[regionID] | ||
if !ok { | ||
return 0, errors.Errorf("regionID %s not found", regionID) | ||
} | ||
|
||
// construct key = regionID/os | ||
key := fmt.Sprintf("%s/%s", regionID, os) | ||
// lazy load pricing for regionID and os | ||
if _, ok = regionOSPrices[key]; !ok { | ||
prices, err := loadEC2Pricing(region.LongName, getOSName(os, osImage)) | ||
if err != nil { | ||
return 0, errors.Wrap(err, "loading EC2 pricing") | ||
} | ||
regionOSPrices[key] = prices | ||
} | ||
// get price for instance type | ||
price, ok := regionOSPrices[key][instanceType] | ||
if !ok { | ||
return 0, errors.Errorf("instance type %s not found", instanceType) | ||
} | ||
return price, nil | ||
} | ||
|
||
func getOSName(os, osImage string) string { | ||
const defaultOS = "Linux" | ||
switch os { | ||
case "linux": | ||
if strings.HasPrefix(osImage, "Red Hat") { | ||
return "RHEL" | ||
} | ||
if strings.HasPrefix(osImage, "SLES") { | ||
return "SUSE" | ||
} | ||
if strings.HasPrefix(osImage, "Ubuntu") { | ||
return "Ubuntu Pro" | ||
} | ||
return defaultOS | ||
case "windows": | ||
return "Windows" | ||
default: | ||
return defaultOS | ||
} | ||
} | ||
|
||
func loadEC2Pricing(regionLongName, os string) (Prices, error) { | ||
// ec2 pricing address | ||
const ec2PricingURL = "https://b0.p.awsstatic.com/pricing/2.0/meteredUnitMaps/ec2/USD/current/ec2-ondemand-without-sec-sel/%s/%s/index.json" | ||
// URL encode regionLongName name | ||
name := url.QueryEscape(regionLongName) | ||
// build URL | ||
address := fmt.Sprintf(ec2PricingURL, name, os) | ||
// load pricing using http client | ||
resp, err := http.Get(address) //nolint:gosec | ||
defer func() { | ||
if resp != nil && resp.Body != nil { | ||
resp.Body.Close() | ||
} | ||
}() | ||
if err != nil { | ||
return nil, errors.Wrap(err, "loading EC2 pricing") | ||
} | ||
// parse pricing | ||
var pricing map[string]interface{} | ||
if err = json.NewDecoder(resp.Body).Decode(&pricing); err != nil { | ||
return nil, errors.Wrap(err, "parsing EC2 pricing") | ||
} | ||
// build map of instance type to price | ||
prices := make(map[string]float64) | ||
for _, r := range pricing["regions"].(map[string]interface{}) { | ||
for _, p := range r.(map[string]interface{}) { | ||
v := p.(map[string]interface{}) | ||
instanceType := v["Instance Type"].(string) | ||
price := v["price"].(string) | ||
// convert price to float64 | ||
prices[instanceType], err = strconv.ParseFloat(price, 64) | ||
if err != nil { | ||
return nil, errors.Wrap(err, "parsing EC2 price") | ||
} | ||
} | ||
} | ||
return prices, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
package price | ||
|
||
import ( | ||
"context" | ||
"testing" | ||
) | ||
|
||
func Test_loadEC2Pricing(t *testing.T) { | ||
type args struct { | ||
region string | ||
os string | ||
} | ||
tests := []struct { | ||
name string | ||
args args | ||
wantErr bool | ||
}{ | ||
{ | ||
name: "t4g.xlarge Linux in US East (Ohio)", | ||
args: args{ | ||
region: "US East (Ohio)", | ||
os: "Linux", | ||
}, | ||
}, | ||
} | ||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
got, err := loadEC2Pricing(tt.args.region, tt.args.os) | ||
if (err != nil) != tt.wantErr { | ||
t.Errorf("loadEC2Pricing() error = %v, wantErr %v", err, tt.wantErr) | ||
return | ||
} | ||
if price, ok := got["t4g.xlarge"]; !ok || price == 0 { | ||
t.Errorf("loadEC2Pricing() = %v", got) | ||
} | ||
t.Log("t4g.xlarge price:", got["t4g.xlarge"]) | ||
}) | ||
} | ||
} | ||
|
||
func TestGetInstancePrice(t *testing.T) { | ||
type args struct { | ||
ctx context.Context | ||
regionID string | ||
os string | ||
osImage string | ||
instanceType string | ||
} | ||
tests := []struct { | ||
name string | ||
args args | ||
wantErr bool | ||
}{ | ||
{ | ||
// t4g.xlarge Linux in US East (Ohio) | ||
name: "t4g.xlarge Linux in US East (Ohio)", | ||
args: args{ | ||
ctx: context.Background(), | ||
regionID: "us-east-2", | ||
os: "linux", | ||
osImage: "Amazon Linux 2", | ||
instanceType: "t4g.xlarge", | ||
}, | ||
}, | ||
{ | ||
// m5a.4xlarge Windows in US West (Oregon) | ||
name: "m5a.4xlarge Windows in US West (Oregon)", | ||
args: args{ | ||
ctx: context.Background(), | ||
regionID: "us-west-2", | ||
os: "windows", | ||
osImage: "Windows Server 2019 Base", | ||
instanceType: "m5a.4xlarge", | ||
}, | ||
}, | ||
} | ||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
got, err := GetInstancePrice(tt.args.ctx, tt.args.regionID, tt.args.os, tt.args.osImage, tt.args.instanceType) | ||
if (err != nil) != tt.wantErr { | ||
t.Errorf("GetInstancePrice() error = %v, wantErr %v", err, tt.wantErr) | ||
return | ||
} | ||
if got == 0 { | ||
t.Errorf("GetInstancePrice() = %v", got) | ||
} | ||
t.Log(tt.args.instanceType, " price:", got) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.