Skip to content

Commit

Permalink
initial work on resource allocation and cost split
Browse files Browse the repository at this point in the history
  • Loading branch information
alexei-led committed Sep 5, 2023
1 parent caf4b0d commit 645370c
Show file tree
Hide file tree
Showing 8 changed files with 376 additions and 27 deletions.
8 changes: 5 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ module github.com/doitintl/eks-lens-agent
go 1.20

require (
github.com/aws/aws-sdk-go-v2 v1.17.6
github.com/aws/aws-sdk-go-v2 v1.18.0
github.com/aws/aws-sdk-go-v2/config v1.18.18
github.com/aws/aws-sdk-go-v2/service/firehose v1.16.7
github.com/aws/aws-sdk-go-v2/service/ssm v1.36.4
github.com/pkg/errors v0.9.1
github.com/sirupsen/logrus v1.9.0
github.com/stretchr/testify v1.8.0
Expand All @@ -19,8 +20,8 @@ require (
require (
github.com/aws/aws-sdk-go-v2/credentials v1.13.17 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.0 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.30 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.24 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.31 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.24 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.12.5 // indirect
Expand All @@ -41,6 +42,7 @@ require (
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/gofuzz v1.1.0 // indirect
github.com/imdario/mergo v0.3.6 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
Expand Down
13 changes: 10 additions & 3 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -33,24 +33,29 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/aws/aws-sdk-go-v2 v1.17.6 h1:Y773UK7OBqhzi5VDXMi1zVGsoj+CVHs2eaC2bDsLwi0=
github.com/aws/aws-sdk-go-v2 v1.17.6/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
github.com/aws/aws-sdk-go-v2 v1.18.0 h1:882kkTpSFhdgYRKVZ/VCgf7sd0ru57p2JCxz4/oN5RY=
github.com/aws/aws-sdk-go-v2 v1.18.0/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
github.com/aws/aws-sdk-go-v2/config v1.18.18 h1:/ePABXvXl3ESlzUGnkkvvNnRFw3Gh13dyqaq0Qo3JcU=
github.com/aws/aws-sdk-go-v2/config v1.18.18/go.mod h1:Lj3E7XcxJnxMa+AYo89YiL68s1cFJRGduChynYU67VA=
github.com/aws/aws-sdk-go-v2/credentials v1.13.17 h1:IubQO/RNeIVKF5Jy77w/LfUvmmCxTnk2TP1UZZIMiF4=
github.com/aws/aws-sdk-go-v2/credentials v1.13.17/go.mod h1:K9xeFo1g/YPMguMUD69YpwB4Nyi6W/5wn706xIInJFg=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.0 h1:/2Cb3SK3xVOQA7Xfr5nCWCo5H3UiNINtsVvVdk8sQqA=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.0/go.mod h1:neYVaeKr5eT7BzwULuG2YbLhzWZ22lpjKdCybR7AXrQ=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.30 h1:y+8n9AGDjikyXoMBTRaHHHSaFEB8267ykmvyPodJfys=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.30/go.mod h1:LUBAO3zNXQjoONBKn/kR1y0Q4cj/D02Ts0uHYjcCQLM=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.24 h1:r+Kv+SEJquhAZXaJ7G4u44cIwXV3f8K+N482NNAzJZA=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33 h1:kG5eQilShqmJbv11XL1VpyDbaEJzWxd4zRiCG30GSn4=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33/go.mod h1:7i0PF1ME/2eUPFcjkVIwq+DOygHEoK92t5cDqNgYbIw=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.24/go.mod h1:gAuCezX/gob6BSMbItsSlMb6WZGV7K2+fWOvk8xBSto=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27 h1:vFQlirhuM8lLlpI7imKOMsjdQLuN9CPi+k44F/OFVsk=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27/go.mod h1:UrHnn3QV/d0pBZ6QBAEQcqFLf8FAzLmoUfPVIueOvoM=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.31 h1:hf+Vhp5WtTdcSdE+yEcUz8L73sAzN0R+0jQv+Z51/mI=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.31/go.mod h1:5zUjguZfG5qjhG9/wqmuyHRyUftl2B5Cp6NNxNC6kRA=
github.com/aws/aws-sdk-go-v2/service/firehose v1.16.7 h1:gC7Y0VCjtytM8EOSJIvZOH9PN6sOPW5JEBwY2DUP1qA=
github.com/aws/aws-sdk-go-v2/service/firehose v1.16.7/go.mod h1:5aiWy3ROWJO7NaoQ3gFK5TlQAybg3on4q/ubpoQkpj0=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.24 h1:c5qGfdbCHav6viBwiyDns3OXqhqAbGjfIB4uVu2ayhk=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.24/go.mod h1:HMA4FZG6fyib+NDo5bpIxX1EhYjrAOveZJY2YR0xrNE=
github.com/aws/aws-sdk-go-v2/service/ssm v1.36.4 h1:3AjvCuRS8OnNVRC/UBagp1Jo2feR94+VAIKO4lz8gOQ=
github.com/aws/aws-sdk-go-v2/service/ssm v1.36.4/go.mod h1:p6MaesK9061w6NTiFmZpUzEkKUY5blKlwD2zYyErxKA=
github.com/aws/aws-sdk-go-v2/service/sso v1.12.5 h1:bdKIX6SVF3nc3xJFw6Nf0igzS6Ff/louGq8Z6VP/3Hs=
github.com/aws/aws-sdk-go-v2/service/sso v1.12.5/go.mod h1:vuWiaDB30M/QTC+lI3Wj6S/zb7tpUK2MSYgy3Guh2L0=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.5 h1:xLPZMyuZ4GuqRCIec/zWuIhRFPXh2UOJdLXBSi64ZWQ=
Expand Down Expand Up @@ -159,7 +164,9 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28=
github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
Expand Down
87 changes: 87 additions & 0 deletions internal/aws/global/infrastructure.go
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
}
122 changes: 122 additions & 0 deletions internal/aws/price/ec2.go
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
}
90 changes: 90 additions & 0 deletions internal/aws/price/ec2_test.go
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)
})
}
}
2 changes: 2 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ type Config struct {
StreamName string `json:"stream-name"`
// DevelopMode mode
DevelopMode bool `json:"develop-mode"`
// Weight Model

}

func LoadConfig(c *cli.Context) Config {
Expand Down
Loading

0 comments on commit 645370c

Please sign in to comment.