Skip to content

Commit 5148302

Browse files
vishalbolludeliahu
authored andcommitted
Use table for cluster pricing (#775)
1 parent 92ddc70 commit 5148302

File tree

2 files changed

+79
-65
lines changed

2 files changed

+79
-65
lines changed

cli/cmd/lib_cluster_config.go

Lines changed: 51 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
cr "github.com/cortexlabs/cortex/pkg/lib/configreader"
2828
"github.com/cortexlabs/cortex/pkg/lib/errors"
2929
"github.com/cortexlabs/cortex/pkg/lib/files"
30+
"github.com/cortexlabs/cortex/pkg/lib/pointer"
3031
"github.com/cortexlabs/cortex/pkg/lib/prompt"
3132
"github.com/cortexlabs/cortex/pkg/lib/sets/strset"
3233
s "github.com/cortexlabs/cortex/pkg/lib/strings"
@@ -282,54 +283,78 @@ func getClusterUpdateConfig(cachedClusterConfig clusterconfig.Config, awsCreds A
282283
}
283284

284285
func confirmInstallClusterConfig(clusterConfig *clusterconfig.Config, awsCreds AWSCredentials, awsClient *aws.Client) {
285-
var spotPrice float64
286-
if clusterConfig.Spot != nil && *clusterConfig.Spot {
287-
var err error
288-
spotPrice, err = awsClient.SpotInstancePrice(*clusterConfig.Region, *clusterConfig.InstanceType)
289-
if err != nil {
290-
spotPrice = 0
291-
}
292-
}
293-
294286
operatorInstancePrice := aws.InstanceMetadatas[*clusterConfig.Region]["t3.medium"].Price
295287
operatorEBSPrice := aws.EBSMetadatas[*clusterConfig.Region].Price * 20 / 30 / 24
296288
elbPrice := aws.ELBMetadatas[*clusterConfig.Region].Price
297289
natPrice := aws.NATMetadatas[*clusterConfig.Region].Price
298-
299-
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)
300-
fmt.Printf("○ an s3 bucket named %s\n", *clusterConfig.Bucket)
301-
fmt.Printf("○ a cloudwatch log group named %s\n", clusterConfig.LogGroup)
302-
fmt.Printf("○ an eks cluster named %s ($0.20 per hour)\n", clusterConfig.ClusterName)
303-
fmt.Printf("○ a t3.medium ec2 instance for the operator (%s per hour)\n", s.DollarsMaxPrecision(operatorInstancePrice))
304-
fmt.Printf("○ a 20gb ebs volume for the operator (%s per hour)\n", s.DollarsAndTenthsOfCents(operatorEBSPrice))
305-
fmt.Printf("○ an elb for the operator and an elb for apis (%s per hour each)\n", s.DollarsMaxPrecision(elbPrice))
306-
fmt.Printf("○ a nat gateway (%s per hour)\n", s.DollarsMaxPrecision(natPrice))
307-
fmt.Println(workloadInstancesStr(clusterConfig, spotPrice))
308-
309-
fmt.Println()
310-
311290
apiInstancePrice := aws.InstanceMetadatas[*clusterConfig.Region][*clusterConfig.InstanceType].Price
312291
apiEBSPrice := aws.EBSMetadatas[*clusterConfig.Region].Price * float64(clusterConfig.InstanceVolumeSize) / 30 / 24
313292
fixedPrice := 0.20 + operatorInstancePrice + operatorEBSPrice + 2*elbPrice + natPrice
314293
totalMinPrice := fixedPrice + float64(*clusterConfig.MinInstances)*(apiInstancePrice+apiEBSPrice)
315294
totalMaxPrice := fixedPrice + float64(*clusterConfig.MaxInstances)*(apiInstancePrice+apiEBSPrice)
316295

296+
fmt.Printf("aws access key id %s will be used to provision a cluster (%s) in %s:\n\n", s.MaskString(awsCreds.AWSAccessKeyID, 4), clusterConfig.ClusterName, *clusterConfig.Region)
297+
298+
headers := []table.Header{
299+
{Title: "aws resource"},
300+
{Title: "cost per hour"},
301+
}
302+
303+
rows := [][]interface{}{}
304+
rows = append(rows, []interface{}{"1 eks cluster", "$0.20"})
305+
rows = append(rows, []interface{}{"1 20gb ebs volume for the operator", s.DollarsAndTenthsOfCents(operatorEBSPrice)})
306+
rows = append(rows, []interface{}{"1 t3.medium for the operator", s.DollarsMaxPrecision(operatorInstancePrice)})
307+
rows = append(rows, []interface{}{"1 nat gateway", s.DollarsMaxPrecision(natPrice)})
308+
rows = append(rows, []interface{}{"2 elastic load balancers", s.DollarsMaxPrecision(elbPrice) + " each"})
309+
310+
instanceStr := "instances"
311+
volumeStr := "volumes"
312+
if *clusterConfig.MinInstances == 1 && *clusterConfig.MaxInstances == 1 {
313+
instanceStr = "instance"
314+
volumeStr = "volume"
315+
}
316+
workerInstanceStr := fmt.Sprintf("%d - %d %s %s for your apis", *clusterConfig.MinInstances, *clusterConfig.MaxInstances, *clusterConfig.InstanceType, instanceStr)
317+
ebsInstanceStr := fmt.Sprintf("%d - %d %dgb ebs %s for your apis", *clusterConfig.MinInstances, *clusterConfig.MaxInstances, clusterConfig.InstanceVolumeSize, volumeStr)
318+
if *clusterConfig.MinInstances == *clusterConfig.MaxInstances {
319+
workerInstanceStr = fmt.Sprintf("%d %s %s for your apis", *clusterConfig.MinInstances, *clusterConfig.InstanceType, instanceStr)
320+
ebsInstanceStr = fmt.Sprintf("%d %dgb ebs %s for your apis", *clusterConfig.MinInstances, clusterConfig.InstanceVolumeSize, volumeStr)
321+
}
322+
323+
workerPriceStr := s.DollarsMaxPrecision(apiInstancePrice) + " each"
317324
spotSuffix := ""
318325
if clusterConfig.Spot != nil && *clusterConfig.Spot {
319-
spotSuffix = " (on-demand pricing)"
326+
spotPrice, err := awsClient.SpotInstancePrice(*clusterConfig.Region, *clusterConfig.InstanceType)
327+
if err == nil {
328+
workerPriceStr += " (spot pricing unavailable)"
329+
}
330+
if spotPrice != 0 {
331+
workerPriceStr = fmt.Sprintf("%s - %s each (varies based on spot price)", s.DollarsMaxPrecision(spotPrice), s.DollarsMaxPrecision(apiInstancePrice))
332+
}
333+
spotSuffix = " (assuming 100% on-demand instances, will be less if spot instances are available)"
320334
}
321335

336+
rows = append(rows, []interface{}{ebsInstanceStr, s.DollarsAndTenthsOfCents(apiEBSPrice) + " each"})
337+
rows = append(rows, []interface{}{workerInstanceStr, workerPriceStr})
338+
339+
items := table.Table{
340+
Headers: headers,
341+
Rows: rows,
342+
}
343+
fmt.Println(items.MustFormat(&table.Opts{Sort: pointer.Bool(false)}))
344+
322345
if *clusterConfig.MinInstances == *clusterConfig.MaxInstances {
323346
fmt.Printf("this cluster will cost %s per hour%s\n\n", s.DollarsAndCents(totalMaxPrice), spotSuffix)
324347
} else {
325348
fmt.Printf("this cluster will cost %s - %s per hour based on the cluster size%s\n\n", s.DollarsAndCents(totalMinPrice), s.DollarsAndCents(totalMaxPrice), spotSuffix)
326349
}
327350

328-
if clusterConfig.Spot != nil && *clusterConfig.Spot && clusterConfig.SpotConfig.OnDemandBackup != nil && *clusterConfig.SpotConfig.OnDemandBackup {
351+
fmt.Printf("cortex will also create an s3 bucket (%s) and a cloudwatch log group (%s)\n\n", *clusterConfig.Bucket, clusterConfig.LogGroup)
352+
353+
if clusterConfig.Spot != nil && *clusterConfig.Spot && clusterConfig.SpotConfig.OnDemandBackup != nil && !*clusterConfig.SpotConfig.OnDemandBackup {
329354
if *clusterConfig.SpotConfig.OnDemandBaseCapacity == 0 && *clusterConfig.SpotConfig.OnDemandPercentageAboveBaseCapacity == 0 {
330-
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://cortex.dev/v/%s/cluster-management/spot-instances for more information\n", clusterconfig.OnDemandBaseCapacityKey, clusterconfig.OnDemandPercentageAboveBaseCapacityKey, consts.CortexVersionMinor)
355+
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://cortex.dev/v/%s/cluster-management/spot-instances for more information\n", clusterconfig.OnDemandBaseCapacityKey, clusterconfig.OnDemandPercentageAboveBaseCapacityKey, consts.CortexVersionMinor)
331356
} else {
332-
fmt.Printf("WARNING: you've enabled spot instances; spot instances are not guaranteed to be available so please take that into account for production clusters; see https://cortex.dev/v/%s/cluster-management/spot-instances for more information\n", consts.CortexVersionMinor)
357+
fmt.Printf("warning: you've enabled spot instances; spot instances are not guaranteed to be available so please take that into account for production clusters; see https://cortex.dev/v/%s/cluster-management/spot-instances for more information\n", consts.CortexVersionMinor)
333358
}
334359
fmt.Println()
335360
}
@@ -468,38 +493,3 @@ func clusterConfigConfirmaionStr(clusterConfig clusterconfig.Config, awsCreds AW
468493

469494
return items.String()
470495
}
471-
472-
func workloadInstancesStr(clusterConfig *clusterconfig.Config, spotPrice float64) string {
473-
instanceRangeStr := fmt.Sprintf("an autoscaling group of %d - %d", *clusterConfig.MinInstances, *clusterConfig.MaxInstances)
474-
volumeRangeStr := fmt.Sprintf("%d - %d", *clusterConfig.MinInstances, *clusterConfig.MaxInstances)
475-
if *clusterConfig.MinInstances == *clusterConfig.MaxInstances {
476-
instanceRangeStr = s.Int64(*clusterConfig.MinInstances)
477-
volumeRangeStr = s.Int64(*clusterConfig.MinInstances)
478-
}
479-
480-
instancesStr := "instances"
481-
volumesStr := "volumes"
482-
if *clusterConfig.MinInstances == 1 && *clusterConfig.MaxInstances == 1 {
483-
instancesStr = "instance"
484-
volumesStr = "volume"
485-
}
486-
487-
instanceTypeStr := *clusterConfig.InstanceType
488-
instancePrice := aws.InstanceMetadatas[*clusterConfig.Region][*clusterConfig.InstanceType].Price
489-
instancePriceStr := fmt.Sprintf("(%s per hour each)", s.DollarsMaxPrecision(instancePrice))
490-
491-
if clusterConfig.Spot != nil && *clusterConfig.Spot {
492-
instanceTypeStr = s.StrsOr(clusterConfig.SpotConfig.InstanceDistribution)
493-
spotPriceStr := "spot pricing not available"
494-
if spotPrice != 0 {
495-
spotPriceStr = fmt.Sprintf("~%s per hour spot", s.DollarsMaxPrecision(spotPrice))
496-
}
497-
instancePriceStr = fmt.Sprintf("(%s: %s per hour on-demand, %s)", *clusterConfig.InstanceType, s.DollarsMaxPrecision(instancePrice), spotPriceStr)
498-
}
499-
500-
ebsPrice := aws.EBSMetadatas[*clusterConfig.Region].Price * float64(clusterConfig.InstanceVolumeSize) / 30 / 24
501-
502-
str := fmt.Sprintf("○ %s %s ec2 %s for apis %s\n", instanceRangeStr, instanceTypeStr, instancesStr, instancePriceStr)
503-
str += fmt.Sprintf("○ %s %dgb ebs %s, one for each api instance (%s per hour each)", volumeRangeStr, clusterConfig.InstanceVolumeSize, volumesStr, s.DollarsAndTenthsOfCents(ebsPrice))
504-
return str
505-
}

pkg/lib/table/table.go

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323

2424
"github.com/cortexlabs/cortex/pkg/lib/console"
2525
"github.com/cortexlabs/cortex/pkg/lib/errors"
26+
"github.com/cortexlabs/cortex/pkg/lib/pointer"
2627
"github.com/cortexlabs/cortex/pkg/lib/slices"
2728
s "github.com/cortexlabs/cortex/pkg/lib/strings"
2829
)
@@ -40,6 +41,26 @@ type Header struct {
4041
Hidden bool
4142
}
4243

44+
type Opts struct {
45+
Sort *bool // default is true
46+
}
47+
48+
func mergeTableOptions(options ...*Opts) Opts {
49+
mergedOpts := Opts{}
50+
51+
for _, opt := range options {
52+
if opt != nil && opt.Sort != nil {
53+
mergedOpts.Sort = opt.Sort
54+
}
55+
}
56+
57+
if mergedOpts.Sort == nil {
58+
mergedOpts.Sort = pointer.Bool(true)
59+
}
60+
61+
return mergedOpts
62+
}
63+
4364
func validate(t Table) error {
4465
numCols := len(t.Headers)
4566

@@ -72,15 +93,16 @@ func (t *Table) MustPrint() {
7293
}
7394

7495
// Return the error message as a string
75-
func (t *Table) MustFormat() string {
76-
str, err := t.Format()
96+
func (t *Table) MustFormat(opts ...*Opts) string {
97+
str, err := t.Format(opts...)
7798
if err != nil {
7899
return "error: " + errors.Message(err)
79100
}
80101
return str
81102
}
82103

83-
func (t *Table) Format() (string, error) {
104+
func (t *Table) Format(opts ...*Opts) (string, error) {
105+
mergedOpts := mergeTableOptions(opts...)
84106
if err := validate(*t); err != nil {
85107
return "", err
86108
}
@@ -156,7 +178,9 @@ func (t *Table) Format() (string, error) {
156178
rowStrs[rowNum] = rowStr
157179
}
158180

159-
sort.Strings(rowStrs)
181+
if *mergedOpts.Sort {
182+
sort.Strings(rowStrs)
183+
}
160184

161185
return headerStr + "\n" + strings.Join(rowStrs, "\n") + "\n", nil
162186
}

0 commit comments

Comments
 (0)