Skip to content

Add total price to cluster installation confirmation message #714

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

Merged
merged 4 commits into from
Dec 31, 2019
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
39 changes: 31 additions & 8 deletions cli/cmd/lib_cluster_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,18 +268,40 @@ func confirmInstallClusterConfig(clusterConfig *clusterconfig.Config, awsCreds *
}
}

operatorInstancePrice := aws.InstanceMetadatas[*clusterConfig.Region]["t3.medium"].Price
operatorEBSPrice := aws.EBSMetadatas[*clusterConfig.Region].Price * 20 / 30 / 24
elbPrice := aws.ELBMetadatas[*clusterConfig.Region].Price
natPrice := aws.NATMetadatas[*clusterConfig.Region].Price

fmt.Printf("cortex will use your %s aws access key id to provision the following resources in the %s region of your aws account:\n\n", s.MaskString(awsCreds.AWSAccessKeyID, 4), *clusterConfig.Region)
fmt.Printf("○ an s3 bucket named %s\n", *clusterConfig.Bucket)
fmt.Printf("○ a cloudwatch log group named %s\n", clusterConfig.LogGroup)
fmt.Printf("○ an eks cluster named %s ($0.20 per hour)\n", clusterConfig.ClusterName)
fmt.Printf("○ a t3.medium ec2 instance for the operator ($%s per hour)\n", s.Float64(aws.InstanceMetadatas[*clusterConfig.Region]["t3.medium"].Price))
fmt.Printf("○ a 20gb ebs volume for the operator ($%s per hour)\n", s.Round(aws.EBSMetadatas[*clusterConfig.Region].Price*20/30/24, 3, false))
fmt.Printf("○ an elb for the operator and an elb for apis ($%s per hour each)\n", s.Float64(aws.ELBMetadatas[*clusterConfig.Region].Price))
fmt.Printf("○ a nat gateway ($%s per hour)\n", s.Float64(aws.NATMetadatas[*clusterConfig.Region].Price))
fmt.Printf("○ a t3.medium ec2 instance for the operator (%s per hour)\n", s.DollarsMaxPrecision(operatorInstancePrice))
fmt.Printf("○ a 20gb ebs volume for the operator (%s per hour)\n", s.DollarsAndTenthsOfCents(operatorEBSPrice))
fmt.Printf("○ an elb for the operator and an elb for apis (%s per hour each)\n", s.DollarsMaxPrecision(elbPrice))
fmt.Printf("○ a nat gateway (%s per hour)\n", s.DollarsMaxPrecision(natPrice))
fmt.Println(workloadInstancesStr(clusterConfig, spotPrice))

fmt.Println()

apiInstancePrice := aws.InstanceMetadatas[*clusterConfig.Region][*clusterConfig.InstanceType].Price
apiEBSPrice := aws.EBSMetadatas[*clusterConfig.Region].Price * float64(clusterConfig.InstanceVolumeSize) / 30 / 24
fixedPrice := 0.20 + operatorInstancePrice + operatorEBSPrice + 2*elbPrice + natPrice
totalMinPrice := fixedPrice + float64(*clusterConfig.MinInstances)*(apiInstancePrice+apiEBSPrice)
totalMaxPrice := fixedPrice + float64(*clusterConfig.MaxInstances)*(apiInstancePrice+apiEBSPrice)

spotSuffix := ""
if clusterConfig.Spot != nil && *clusterConfig.Spot {
spotSuffix = " (not accounting for spot instances)"
}

if *clusterConfig.MinInstances == *clusterConfig.MaxInstances {
fmt.Printf("this cluster will cost %s per hour%s\n\n", s.DollarsAndCents(totalMaxPrice), spotSuffix)
} else {
fmt.Printf("this cluster will cost %s per hour if the minimum number of instances are running and %s per hour if the maximum number of instances are running%s\n\n", s.DollarsAndCents(totalMinPrice), s.DollarsAndCents(totalMaxPrice), spotSuffix)
}

if clusterConfig.Spot != nil && *clusterConfig.Spot {
if *clusterConfig.SpotConfig.OnDemandBaseCapacity == 0 && *clusterConfig.SpotConfig.OnDemandPercentageAboveBaseCapacity == 0 {
fmt.Printf("WARNING: you've disabled on-demand instances (%s=0 and %s=0); spot instances are not guaranteed to be available so please take that into account for production clusters; see https://www.cortex.dev/v/%s/cluster-management/spot-instances for more information\n", clusterconfig.OnDemandBaseCapacityKey, clusterconfig.OnDemandPercentageAboveBaseCapacityKey, consts.CortexVersionMinor)
Expand Down Expand Up @@ -436,20 +458,21 @@ func workloadInstancesStr(clusterConfig *clusterconfig.Config, spotPrice float64
}

instanceTypeStr := *clusterConfig.InstanceType
instancePriceStr := fmt.Sprintf("($%s per hour each)", s.Float64(aws.InstanceMetadatas[*clusterConfig.Region][*clusterConfig.InstanceType].Price))
instancePrice := aws.InstanceMetadatas[*clusterConfig.Region][*clusterConfig.InstanceType].Price
instancePriceStr := fmt.Sprintf("(%s per hour each)", s.DollarsMaxPrecision(instancePrice))

if clusterConfig.Spot != nil && *clusterConfig.Spot {
instanceTypeStr = s.StrsOr(clusterConfig.SpotConfig.InstanceDistribution)
spotPriceStr := "spot pricing not available"
if spotPrice != 0 {
spotPriceStr = fmt.Sprintf("~$%s per hour spot", s.Float64(spotPrice))
spotPriceStr = fmt.Sprintf("~%s per hour spot", s.DollarsMaxPrecision(spotPrice))
}
instancePriceStr = fmt.Sprintf("(%s: $%s per hour on-demand, %s)", *clusterConfig.InstanceType, s.Float64(aws.InstanceMetadatas[*clusterConfig.Region][*clusterConfig.InstanceType].Price), spotPriceStr)
instancePriceStr = fmt.Sprintf("(%s: %s per hour on-demand, %s)", *clusterConfig.InstanceType, s.DollarsMaxPrecision(instancePrice), spotPriceStr)
}

ebsPrice := aws.EBSMetadatas[*clusterConfig.Region].Price * float64(clusterConfig.InstanceVolumeSize) / 30 / 24

str := fmt.Sprintf("○ %s %s ec2 %s for apis %s\n", instanceRangeStr, instanceTypeStr, instancesStr, instancePriceStr)
str += fmt.Sprintf("○ %s %dgb ebs %s for apis ($%s per hour each)", volumeRangeStr, clusterConfig.InstanceVolumeSize, volumesStr, s.Round(ebsPrice, 3, false))
str += fmt.Sprintf("○ %s %dgb ebs %s, one for each api instance (%s per hour each)", volumeRangeStr, clusterConfig.InstanceVolumeSize, volumesStr, s.DollarsAndTenthsOfCents(ebsPrice))
return str
}
21 changes: 18 additions & 3 deletions pkg/lib/strings/stringify.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,10 @@ func Uintptr(val uintptr) string {
return fmt.Sprint(val)
}

func Round(val float64, decimalPlaces int, pad bool) string {
func Round(val float64, decimalPlaces int, padToDecimalPlaces int) string {
rounded := math.Round(val*math.Pow10(decimalPlaces)) / math.Pow10(decimalPlaces)
str := strconv.FormatFloat(rounded, 'f', -1, 64)
if !pad || decimalPlaces == 0 {
if padToDecimalPlaces == 0 {
return str
}
split := strings.Split(str, ".")
Expand All @@ -116,10 +116,25 @@ func Round(val float64, decimalPlaces int, pad bool) string {
if len(split) > 1 {
decVal = split[1]
}
numZeros := decimalPlaces - len(decVal)
if len(decVal) >= padToDecimalPlaces {
return str
}
numZeros := padToDecimalPlaces - len(decVal)
return intVal + "." + decVal + strings.Repeat("0", numZeros)
}

func DollarsAndCents(val float64) string {
return "$" + Round(val, 2, 2)
}

func DollarsAndTenthsOfCents(val float64) string {
return "$" + Round(val, 3, 2)
}

func DollarsMaxPrecision(val float64) string {
return "$" + Round(val, 100, 2)
}

// This is similar to json.Marshal, but handles non-string keys (which we support). It should be valid YAML since we use it in templates
func strIndent(val interface{}, indent string, currentIndent string, newlineChar string, quoteStr string) string {
if val == nil {
Expand Down
62 changes: 37 additions & 25 deletions pkg/lib/strings/stringify_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,29 +258,41 @@ func TestObj(t *testing.T) {
}

func TestRound(t *testing.T) {
require.Equal(t, Round(1.111, 2, false), "1.11")
require.Equal(t, Round(1.111, 3, false), "1.111")
require.Equal(t, Round(1.111, 4, false), "1.111")
require.Equal(t, Round(1.555, 2, false), "1.56")
require.Equal(t, Round(1.555, 3, false), "1.555")
require.Equal(t, Round(1.555, 4, false), "1.555")
require.Equal(t, Round(1.100, 2, false), "1.1")

require.Equal(t, Round(1.111, 2, true), "1.11")
require.Equal(t, Round(1.111, 3, true), "1.111")
require.Equal(t, Round(1.111, 4, true), "1.1110")
require.Equal(t, Round(1.555, 2, true), "1.56")
require.Equal(t, Round(1.555, 3, true), "1.555")
require.Equal(t, Round(1.555, 4, true), "1.5550")
require.Equal(t, Round(1.100, 2, true), "1.10")

require.Equal(t, Round(30, 0, true), "30")
require.Equal(t, Round(2, 1, true), "2.0")
require.Equal(t, Round(1, 2, true), "1.00")
require.Equal(t, Round(20, 3, true), "20.000")

require.Equal(t, Round(30, 0, false), "30")
require.Equal(t, Round(2, 1, false), "2")
require.Equal(t, Round(1, 2, false), "1")
require.Equal(t, Round(20, 3, false), "20")
require.Equal(t, Round(1.111, 2, 0), "1.11")
require.Equal(t, Round(1.111, 3, 0), "1.111")
require.Equal(t, Round(1.111, 4, 0), "1.111")
require.Equal(t, Round(1.555, 2, 0), "1.56")
require.Equal(t, Round(1.555, 3, 0), "1.555")
require.Equal(t, Round(1.555, 4, 0), "1.555")
require.Equal(t, Round(1.100, 2, 0), "1.1")

require.Equal(t, Round(1.111, 2, 2), "1.11")
require.Equal(t, Round(1.111, 3, 3), "1.111")
require.Equal(t, Round(1.111, 4, 4), "1.1110")
require.Equal(t, Round(1.555, 2, 2), "1.56")
require.Equal(t, Round(1.555, 3, 3), "1.555")
require.Equal(t, Round(1.555, 4, 4), "1.5550")
require.Equal(t, Round(1.100, 2, 2), "1.10")

require.Equal(t, Round(30, 0, 0), "30")
require.Equal(t, Round(2, 1, 1), "2.0")
require.Equal(t, Round(1, 2, 2), "1.00")
require.Equal(t, Round(20, 3, 3), "20.000")

require.Equal(t, Round(1.5555, 3, 2), "1.556")
require.Equal(t, Round(1.5, 3, 2), "1.50")
require.Equal(t, Round(1, 3, 2), "1.00")

require.Equal(t, Round(1.5555, 3, 1), "1.556")
require.Equal(t, Round(1.5, 3, 1), "1.5")
require.Equal(t, Round(1, 3, 1), "1.0")

require.Equal(t, Round(1.5555, 3, 4), "1.5560")
require.Equal(t, Round(1.5, 3, 4), "1.5000")
require.Equal(t, Round(1, 3, 4), "1.0000")

require.Equal(t, Round(30, 0, 0), "30")
require.Equal(t, Round(2, 1, 0), "2")
require.Equal(t, Round(1, 2, 0), "1")
require.Equal(t, Round(20, 3, 0), "20")
}