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

AWS estimation #9

Merged
merged 10 commits into from
Dec 10, 2020
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 aws/ingester.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/shopspring/decimal"

"github.com/cycloidio/cost-estimation/aws/field"
"github.com/cycloidio/cost-estimation/aws/region"
"github.com/cycloidio/cost-estimation/price"
"github.com/cycloidio/cost-estimation/product"
)
Expand Down Expand Up @@ -248,7 +249,7 @@ func newProduct(values map[field.Field]string) *product.Product {
SKU: values[field.SKU],
Service: values[field.ServiceCode],
Family: values[field.ProductFamily],
Location: regionMap[values[field.Location]],
Location: region.NewFromName(values[field.Location]).String(),
Attributes: attributes,
}
return prod
Expand Down
31 changes: 31 additions & 0 deletions aws/region/code.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package region
xescugc marked this conversation as resolved.
Show resolved Hide resolved

// Code represents an AWS region code.
type Code string

// NewFromZone returns the region code of the given zone or empty string if invalid.
func NewFromZone(zone string) Code {
if len(zone) < 1 {
return ""
}
return Code(zone[:len(zone)-1])
}

// NewFromName returns the region code from its name or empty string if invalid.
func NewFromName(name string) Code {
return nameToCode[name]
}

// Valid returns true if the region exists and is supported, false otherwise.
func (c Code) Valid() bool {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As Code can also be "" it might be worth checking that ahead of iterating?

if c == "" {
return false
}
_, ok := codeToName[c]
return ok
}

// String returns the code of the region as a string.
func (c Code) String() string {
return string(c)
}
53 changes: 53 additions & 0 deletions aws/region/code_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package region_test

import (
"testing"

"github.com/stretchr/testify/assert"

"github.com/cycloidio/cost-estimation/aws/region"
)

func TestNewFromZone(t *testing.T) {
testcases := []struct{ in, out string }{
{"", ""},
{"us-east-1c", "us-east-1"},
{"eu-west-3a", "eu-west-3"},
}
for _, tc := range testcases {
t.Run(tc.in, func(t *testing.T) {
actual := region.NewFromZone(tc.in)
assert.Equal(t, tc.out, actual.String())
})
}
}

func TestNewFromName(t *testing.T) {
testcases := []struct{ in, out string }{
{"", ""},
{"US East (N. Virginia)", "us-east-1"},
{"EU (Paris)", "eu-west-3"},
}
for _, tc := range testcases {
t.Run(tc.in, func(t *testing.T) {
actual := region.NewFromName(tc.in)
assert.Equal(t, tc.out, actual.String())
})
}
}

func TestCode_Valid(t *testing.T) {
testcases := []struct {
in string
out bool
}{
{"", false},
{"us-east-1", true},
{"us-invalid-42", false},
}
for _, tc := range testcases {
t.Run(tc.in, func(t *testing.T) {
assert.Equal(t, tc.out, region.Code(tc.in).Valid())
})
}
}
12 changes: 10 additions & 2 deletions aws/regions.go → aws/region/regions.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package aws
package region

// List of regions with their codes can be found here: https://docs.aws.amazon.com/general/latest/gr/ec2-service.html
var regionMap = map[string]string{
var nameToCode = map[string]Code{
"US East (N. Virginia)": "us-east-1",
"US East (Ohio)": "us-east-2",
"US West (N. California)": "us-west-1",
Expand Down Expand Up @@ -29,3 +29,11 @@ var regionMap = map[string]string{
"Middle East (Bahrain)": "me-south-1",
"Africa (Cape Town)": "af-south-1",
}

var codeToName = make(map[Code]string)

func init() {
for name, code := range nameToCode {
codeToName[code] = name
}
}
139 changes: 139 additions & 0 deletions aws/terraform/instance.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package terraform

import (
"github.com/mitchellh/mapstructure"
"github.com/shopspring/decimal"

"github.com/cycloidio/cost-estimation/aws/region"
"github.com/cycloidio/cost-estimation/price"
"github.com/cycloidio/cost-estimation/product"
"github.com/cycloidio/cost-estimation/query"
"github.com/cycloidio/cost-estimation/util"
)

// Instance represents an EC2 instance definition that can be cost-estimated.
type Instance struct {
provider *Provider
region region.Code
instanceType string

// tenancy describes the tenancy of an instance.
// Valid values include: Shared, Dedicated, Host.
// Note: only "Shared" and "Dedicated" are supported at the moment.
tenancy string

// operatingSystem denotes the OS that the instance is using that may affect pricing.
// Valid values include: Linux, RHEL, SUSE, Windows.
// Note: only "Linux" is supported at the moment.
operatingSystem string

// capacityStatus describes the status of capacity reservations.
// Valid values include: Used, UnusedCapacityReservation, AllocatedCapacityReservation.
// Note: only "Used" is supported at the moment.
capacityStatus string

// preInstalledSW denotes any pre-installed software that may affect pricing.
// Valid values include: NA, SQL Std, SQL Web, SQL Ent.
// Note: only "NA" (no pre-installed software) is supported at the moment.
preInstalledSW string

rootVolume *Volume
}

// instanceValues represents the structure of Terraform values for aws_instance resource.
type instanceValues struct {
InstanceType string `mapstructure:"instance_type"`
Tenancy string `mapstructure:"tenancy"`
AvailabilityZone string `mapstructure:"availability_zone"`

RootBlockDevice []struct {
VolumeType string `mapstructure:"volume_type"`
VolumeSize float64 `mapstructure:"volume_size"`
IOPS float64 `mapstructure:"iops"`
} `mapstructure:"root_block_device"`
}

// decodeInstanceValues decodes and returns instanceValues from a Terraform values map.
func decodeInstanceValues(tfVals map[string]interface{}) (instanceValues, error) {
var v instanceValues
if err := mapstructure.Decode(tfVals, &v); err != nil {
return v, err
}
return v, nil
}

// newInstance creates a new Instance from instanceValues.
func (p *Provider) newInstance(vals instanceValues) *Instance {
inst := &Instance{
provider: p,
region: p.region,
tenancy: "Shared",

// Note: every Instance is estimated as a Linux without pre-installed S/W
operatingSystem: "Linux",
capacityStatus: "Used",
preInstalledSW: "NA",

instanceType: vals.InstanceType,
}

if reg := region.NewFromZone(vals.AvailabilityZone); reg.Valid() {
inst.region = reg
}

if vals.Tenancy == "dedicated" {
inst.tenancy = "Dedicated"
}

volVals := volumeValues{AvailabilityZone: vals.AvailabilityZone}
if len(vals.RootBlockDevice) > 0 {
rbd := vals.RootBlockDevice[0]
volVals.Type = rbd.VolumeType
volVals.Size = rbd.VolumeSize
volVals.IOPS = rbd.IOPS
}
inst.rootVolume = p.newVolume(volVals)

return inst
}

// Components returns the price component queries that make up this Instance.
func (inst *Instance) Components() []query.Component {
components := []query.Component{inst.computeComponent()}

if inst.rootVolume != nil {
for _, comp := range inst.rootVolume.Components() {
comp.Name = "Root volume: " + comp.Name
components = append(components, comp)
}
}

return components
}

func (inst *Instance) computeComponent() query.Component {
return query.Component{
Name: "Compute",
Details: []string{"Linux", "on-demand", inst.instanceType},
xlr-8 marked this conversation as resolved.
Show resolved Hide resolved
HourlyQuantity: decimal.NewFromInt(1),
ProductFilter: &product.Filter{
Provider: util.StringPtr(inst.provider.key),
Service: util.StringPtr("AmazonEC2"),
Family: util.StringPtr("Compute Instance"),
xlr-8 marked this conversation as resolved.
Show resolved Hide resolved
Location: util.StringPtr(inst.region.String()),
AttributeFilters: []*product.AttributeFilter{
{Key: "capacitystatus", Value: util.StringPtr(inst.capacityStatus)},
{Key: "instanceType", Value: util.StringPtr(inst.instanceType)},
{Key: "tenancy", Value: util.StringPtr(inst.tenancy)},
{Key: "operatingSystem", Value: util.StringPtr(inst.operatingSystem)},
{Key: "preInstalledSw", Value: util.StringPtr(inst.preInstalledSW)},
},
},
PriceFilter: &price.Filter{
Unit: util.StringPtr("Hrs"),
AttributeFilters: []*price.AttributeFilter{
{Key: "purchaseOption", Value: util.StringPtr("on_demand")},
},
},
}
}
Loading