From 391489fbdef2fae611eb57b5516e4146e8ed8950 Mon Sep 17 00:00:00 2001 From: Dipankar Das <65275144+dipankardas011@users.noreply.github.com> Date: Sun, 23 Mar 2025 23:07:15 +0530 Subject: [PATCH 1/9] added initial work Signed-off-by: Dipankar Das <65275144+dipankardas011@users.noreply.github.com> --- cmd/create.go | 31 +++++++++++++++++++++++++++++-- cmd/handle_meta.go | 4 +++- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/cmd/create.go b/cmd/create.go index 55a8e27..dd9160f 100644 --- a/cmd/create.go +++ b/cmd/create.go @@ -58,6 +58,31 @@ ksctl create --help return cmd } +func (k *KsctlCommand) findCostAcrossRegions( + meta controller.Metadata, + availRegions []provider.RegionOutput, + cpSku, wpSku, dsSku, lbSku string, + noCP, noWP, noDS int, +) { + for _, region := range availRegions { + m_cpy := meta + regSku := region.Sku + go func() { + metaClient, err := controllerMeta.NewController( + k.Ctx, + k.l, + &controller.Client{ + Metadata: m_cpy, + }, + ) + if err != nil { + k.l.Warn(k.Ctx, "Failed to create the controller", "Reason", err, "region", regSku) + } + _ = metaClient + }() + } +} + func (k *KsctlCommand) metadataForSelfManagedCluster( meta *controller.Metadata, ) { @@ -73,7 +98,7 @@ func (k *KsctlCommand) metadataForSelfManagedCluster( os.Exit(1) } - k.handleRegionSelection(metaClient, meta) + allAvailRegions := k.handleRegionSelection(metaClient, meta) cp := k.handleInstanceTypeSelection(metaClient, meta, provider.ComputeIntensive, "Select instance_type for Control Plane") etcd := k.handleInstanceTypeSelection(metaClient, meta, provider.MemoryIntensive, "Select instance_type for Etcd Nodes") @@ -118,6 +143,8 @@ func (k *KsctlCommand) metadataForSelfManagedCluster( meta.NoDS = v } + go k.findCostAcrossRegions(*meta, allAvailRegions, cp.Sku, wp.Sku, etcd.Sku, lb.Sku, meta.NoCP, meta.NoWP, meta.NoDS) + bootstrapVers, err := metaClient.ListAllBootstrapVersions() if err != nil { k.l.Error("Failed to get the list of bootstrap versions", "Reason", err) @@ -237,7 +264,7 @@ func (k *KsctlCommand) metadataForManagedCluster( } if meta.Provider != consts.CloudLocal { - k.handleRegionSelection(metaClient, meta) + _ = k.handleRegionSelection(metaClient, meta) category := provider.Unknown if meta.Provider != consts.CloudLocal { diff --git a/cmd/handle_meta.go b/cmd/handle_meta.go index e35bd9e..65c52f1 100644 --- a/cmd/handle_meta.go +++ b/cmd/handle_meta.go @@ -63,7 +63,7 @@ func (k *KsctlCommand) baseMetadataFields(m *controller.Metadata) { } } -func (k *KsctlCommand) handleRegionSelection(meta *controllerMeta.Controller, m *controller.Metadata) { +func (k *KsctlCommand) handleRegionSelection(meta *controllerMeta.Controller, m *controller.Metadata) []provider.RegionOutput { ss := k.menuDriven.GetProgressAnimation() ss.Start("Fetching the region list") @@ -80,6 +80,8 @@ func (k *KsctlCommand) handleRegionSelection(meta *controllerMeta.Controller, m } else { m.Region = v } + + return listOfRegions } func (k *KsctlCommand) handleInstanceCategorySelection() provider.MachineCategory { From df615664fc8ea78bbf6223d3d726809f2db37ed6 Mon Sep 17 00:00:00 2001 From: Dipankar Das <65275144+dipankardas011@users.noreply.github.com> Date: Mon, 24 Mar 2025 22:27:54 +0530 Subject: [PATCH 2/9] now working with vm prices based on regions Signed-off-by: Dipankar Das <65275144+dipankardas011@users.noreply.github.com> --- cmd/create.go | 20 +++++++++++++++----- go.mod | 2 ++ go.sum | 2 -- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/cmd/create.go b/cmd/create.go index dd9160f..a0841ef 100644 --- a/cmd/create.go +++ b/cmd/create.go @@ -16,6 +16,7 @@ package cmd import ( "os" + "sync" "github.com/ksctl/ksctl/v2/pkg/consts" "github.com/ksctl/ksctl/v2/pkg/provider" @@ -61,13 +62,15 @@ ksctl create --help func (k *KsctlCommand) findCostAcrossRegions( meta controller.Metadata, availRegions []provider.RegionOutput, - cpSku, wpSku, dsSku, lbSku string, - noCP, noWP, noDS int, + instanceSku string, ) { + wg := &sync.WaitGroup{} + wg.Add(len(availRegions)) for _, region := range availRegions { m_cpy := meta regSku := region.Sku go func() { + defer wg.Done() metaClient, err := controllerMeta.NewController( k.Ctx, k.l, @@ -76,11 +79,18 @@ func (k *KsctlCommand) findCostAcrossRegions( }, ) if err != nil { - k.l.Warn(k.Ctx, "Failed to create the controller", "Reason", err, "region", regSku) + k.l.Warn(k.Ctx, "Failed to create the controller", "Reason", err, "region", regSku, "instance_type", instanceSku) + } + p, err := metaClient.GetPriceForInstance(regSku, instanceSku) + if err != nil { + k.l.Warn(k.Ctx, "Failed to get the price", "Reason", err, "region", regSku, "instance_type", instanceSku) + } else { + k.l.Print(k.Ctx, "Price for the instance", "region", regSku, "price", p, "instance_type", instanceSku) } - _ = metaClient }() } + + wg.Wait() } func (k *KsctlCommand) metadataForSelfManagedCluster( @@ -143,7 +153,7 @@ func (k *KsctlCommand) metadataForSelfManagedCluster( meta.NoDS = v } - go k.findCostAcrossRegions(*meta, allAvailRegions, cp.Sku, wp.Sku, etcd.Sku, lb.Sku, meta.NoCP, meta.NoWP, meta.NoDS) + k.findCostAcrossRegions(*meta, allAvailRegions, cp.Sku) bootstrapVers, err := metaClient.ListAllBootstrapVersions() if err != nil { diff --git a/go.mod b/go.mod index 4239bce..5b97b75 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,8 @@ require ( k8s.io/client-go v0.32.2 ) +replace github.com/ksctl/ksctl/v2 => ../ksctl + require ( al.essio.dev/pkg/shellescape v1.5.1 // indirect atomicgo.dev/cursor v0.2.0 // indirect diff --git a/go.sum b/go.sum index 7e30648..5ccd75e 100644 --- a/go.sum +++ b/go.sum @@ -351,8 +351,6 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/ksctl/ksctl/v2 v2.1.0 h1:RuS+KA5Ab0/WdKIj2d+l5GnafGoIuCPZnVd/Nz3LrNY= -github.com/ksctl/ksctl/v2 v2.1.0/go.mod h1:w9I+7Jm8umrF6JbeiShOYnj4PNRE+534Ji6QAifAr4s= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= From 842e4bc7f63005394bc4ce7fc95827dda7f5e7d4 Mon Sep 17 00:00:00 2001 From: Dipankar Das <65275144+dipankardas011@users.noreply.github.com> Date: Tue, 25 Mar 2025 21:28:42 +0530 Subject: [PATCH 3/9] improved the object creation Signed-off-by: Dipankar Das <65275144+dipankardas011@users.noreply.github.com> --- cmd/create.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/cmd/create.go b/cmd/create.go index a0841ef..08693f4 100644 --- a/cmd/create.go +++ b/cmd/create.go @@ -66,21 +66,21 @@ func (k *KsctlCommand) findCostAcrossRegions( ) { wg := &sync.WaitGroup{} wg.Add(len(availRegions)) + metaClient, err := controllerMeta.NewController( + k.Ctx, + k.l, + &controller.Client{ + Metadata: meta, + }, + ) + if err != nil { + k.l.Warn(k.Ctx, "Failed to create the controller", "Reason", err, "instance_type", instanceSku) + } + for _, region := range availRegions { - m_cpy := meta regSku := region.Sku go func() { defer wg.Done() - metaClient, err := controllerMeta.NewController( - k.Ctx, - k.l, - &controller.Client{ - Metadata: m_cpy, - }, - ) - if err != nil { - k.l.Warn(k.Ctx, "Failed to create the controller", "Reason", err, "region", regSku, "instance_type", instanceSku) - } p, err := metaClient.GetPriceForInstance(regSku, instanceSku) if err != nil { k.l.Warn(k.Ctx, "Failed to get the price", "Reason", err, "region", regSku, "instance_type", instanceSku) From 27f782ecb2f10f5f274af20a7e4e952d05690225 Mon Sep 17 00:00:00 2001 From: Dipankar Das <65275144+dipankardas011@users.noreply.github.com> Date: Wed, 26 Mar 2025 00:15:35 +0530 Subject: [PATCH 4/9] added recommendation system for instancetype wrt to region for cost Signed-off-by: Dipankar Das <65275144+dipankardas011@users.noreply.github.com> --- cmd/create.go | 200 ++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 184 insertions(+), 16 deletions(-) diff --git a/cmd/create.go b/cmd/create.go index 08693f4..7b1b759 100644 --- a/cmd/create.go +++ b/cmd/create.go @@ -15,9 +15,12 @@ package cmd import ( + "cmp" + "fmt" "os" - "sync" + "slices" + "github.com/fatih/color" "github.com/ksctl/ksctl/v2/pkg/consts" "github.com/ksctl/ksctl/v2/pkg/provider" @@ -59,13 +62,12 @@ ksctl create --help return cmd } +// findCostAcrossRegions it returns a map of K[V] where K is the region and V is the cost of the instance func (k *KsctlCommand) findCostAcrossRegions( meta controller.Metadata, availRegions []provider.RegionOutput, instanceSku string, -) { - wg := &sync.WaitGroup{} - wg.Add(len(availRegions)) +) (map[string]float64, error) { metaClient, err := controllerMeta.NewController( k.Ctx, k.l, @@ -74,23 +76,170 @@ func (k *KsctlCommand) findCostAcrossRegions( }, ) if err != nil { - k.l.Warn(k.Ctx, "Failed to create the controller", "Reason", err, "instance_type", instanceSku) + return nil, err } + resultChan := make(chan struct { + region string + price float64 + err error + }, len(availRegions)) + for _, region := range availRegions { regSku := region.Sku - go func() { - defer wg.Done() - p, err := metaClient.GetPriceForInstance(regSku, instanceSku) - if err != nil { - k.l.Warn(k.Ctx, "Failed to get the price", "Reason", err, "region", regSku, "instance_type", instanceSku) - } else { - k.l.Print(k.Ctx, "Price for the instance", "region", regSku, "price", p, "instance_type", instanceSku) - } - }() + go func(sku string) { + price, err := metaClient.GetPriceForInstance(sku, instanceSku) + resultChan <- struct { + region string + price float64 + err error + }{sku, price, err} + }(regSku) + } + + cost := make(map[string]float64, len(availRegions)) + for i := 0; i < len(availRegions); i++ { + result := <-resultChan + if result.err == nil { + cost[result.region] = result.price + } + } + + return cost, nil +} + +type RecommendationSelfManagedCost struct { + region string + totalCost float64 + + cpCost float64 + wpCost float64 + etcdCost float64 + lbCost float64 +} + +func (k *KsctlCommand) getBestRegionsWithTotalCostSelfManaged( + allAvailRegions []provider.RegionOutput, + costForCP map[string]float64, + costForWP map[string]float64, + costForDS map[string]float64, + costForLB map[string]float64, +) []RecommendationSelfManagedCost { + + checkRegion := func(region string, m map[string]float64) bool { + _, ok := m[region] + return ok + } + + var costForCluster []RecommendationSelfManagedCost + + for _, region := range allAvailRegions { + if !checkRegion(region.Sku, costForCP) || + !checkRegion(region.Sku, costForWP) || + !checkRegion(region.Sku, costForDS) || + !checkRegion(region.Sku, costForLB) { + continue + } + + totalCost := costForCP[region.Sku] + costForWP[region.Sku] + costForDS[region.Sku] + costForLB[region.Sku] + + costForCluster = append(costForCluster, RecommendationSelfManagedCost{ + region: region.Sku, + cpCost: costForCP[region.Sku], + wpCost: costForWP[region.Sku], + etcdCost: costForDS[region.Sku], + lbCost: costForLB[region.Sku], + totalCost: totalCost, + }) + } + + slices.SortFunc(costForCluster, func(a, b RecommendationSelfManagedCost) int { + return cmp.Compare(a.totalCost, b.totalCost) + }) + + if len(costForCluster) < 3 { + return costForCluster + } + + return costForCluster +} + +func (k *KsctlCommand) OptimizeInstanceRegion( + meta *controller.Metadata, + allAvailRegions []provider.RegionOutput, + cp provider.InstanceRegionOutput, + wp provider.InstanceRegionOutput, + etcd provider.InstanceRegionOutput, + lb provider.InstanceRegionOutput, +) []RecommendationSelfManagedCost { + cpInstanceCosts, err := k.findCostAcrossRegions(*meta, allAvailRegions, cp.Sku) + if err != nil { + k.l.Error("Failed to get the cost of control plane instances", "Reason", err) + } + + wpInstanceCosts, err := k.findCostAcrossRegions(*meta, allAvailRegions, wp.Sku) + if err != nil { + k.l.Error("Failed to get the cost of worker plane instances", "Reason", err) + } + + etcdInstanceCosts, err := k.findCostAcrossRegions(*meta, allAvailRegions, etcd.Sku) + if err != nil { + k.l.Error("Failed to get the cost of etcd instances", "Reason", err) } - wg.Wait() + lbInstanceCosts, err := k.findCostAcrossRegions(*meta, allAvailRegions, lb.Sku) + if err != nil { + k.l.Error("Failed to get the cost of load balancer instances", "Reason", err) + } + + return k.getBestRegionsWithTotalCostSelfManaged( + allAvailRegions, + cpInstanceCosts, + wpInstanceCosts, + etcdInstanceCosts, + lbInstanceCosts, + ) +} + +func (k *KsctlCommand) PrintRecommendationSelfManagedCost( + costs []RecommendationSelfManagedCost, + noOfCP int, + noOfWP int, + noOfDS int, + instanceTypeCP string, + instanceTypeWP string, + instanceTypeDS string, + instanceTypeLB string, +) { + k.l.Print(k.Ctx, + "Here is your recommendation", + "Parameter", "Region wise cost", + "OptimizedRegion", color.HiCyanString(costs[0].region), + ) + + headers := []string{ + "Region", + fmt.Sprintf("Control Plane << %s >>", instanceTypeCP), + fmt.Sprintf("Worker Plane << %s >>", instanceTypeWP), + fmt.Sprintf("Etcd Nodes << %s >>", instanceTypeDS), + fmt.Sprintf("Load Balancer << %s >>", instanceTypeLB), + "Total Monthly Cost", + } + + var data [][]string + for _, cost := range costs { + total := cost.cpCost*float64(noOfCP) + cost.wpCost*float64(noOfWP) + cost.etcdCost*float64(noOfDS) + cost.lbCost + data = append(data, []string{ + cost.region, + fmt.Sprintf("$%.2f X %d", cost.cpCost, noOfCP), + fmt.Sprintf("$%.2f X %d", cost.wpCost, noOfWP), + fmt.Sprintf("$%.2f X %d", cost.etcdCost, noOfDS), + fmt.Sprintf("$%.2f X 1", cost.lbCost), + fmt.Sprintf("$%.2f", total), + }) + } + + k.l.Table(k.Ctx, headers, data) } func (k *KsctlCommand) metadataForSelfManagedCluster( @@ -153,13 +302,19 @@ func (k *KsctlCommand) metadataForSelfManagedCluster( meta.NoDS = v } - k.findCostAcrossRegions(*meta, allAvailRegions, cp.Sku) + var isOptimizeInstanceRegionReady chan []RecommendationSelfManagedCost + isOptimizeInstanceRegionReady = make(chan []RecommendationSelfManagedCost) + + go func() { + isOptimizeInstanceRegionReady <- k.OptimizeInstanceRegion(meta, allAvailRegions, cp, wp, etcd, lb) + }() bootstrapVers, err := metaClient.ListAllBootstrapVersions() if err != nil { k.l.Error("Failed to get the list of bootstrap versions", "Reason", err) os.Exit(1) } + if v, err := k.menuDriven.DropDownList("Select the bootstrap version", bootstrapVers, cli.WithDefaultValue(bootstrapVers[0])); err != nil { k.l.Error("Failed to get the bootstrap version", "Reason", err) os.Exit(1) @@ -181,6 +336,7 @@ func (k *KsctlCommand) metadataForSelfManagedCluster( meta.EtcdVersion = v } + k.l.Print(k.Ctx, "Current Selection will cost you") _, err = metaClient.PriceCalculator( controllerMeta.PriceCalculatorInput{ Currency: cp.Price.Currency, @@ -197,6 +353,18 @@ func (k *KsctlCommand) metadataForSelfManagedCluster( os.Exit(1) } + // TODO: add spinner + k.PrintRecommendationSelfManagedCost( + <-isOptimizeInstanceRegionReady, + meta.NoCP, + meta.NoWP, + meta.NoDS, + cp.Sku, + wp.Sku, + etcd.Sku, + lb.Sku, + ) + managedCNI, defaultCNI, ksctlCNI, defaultKsctl, err := metaClient.ListBootstrapCNIs() if err != nil { k.l.Error("Failed to get the list of self managed CNIs", "Reason", err) From 2541acbeb4e4ee72246d814298aef019f5ddc18c Mon Sep 17 00:00:00 2001 From: Dipankar Das <65275144+dipankardas011@users.noreply.github.com> Date: Wed, 26 Mar 2025 00:18:28 +0530 Subject: [PATCH 5/9] some minor changes in the printing Signed-off-by: Dipankar Das <65275144+dipankardas011@users.noreply.github.com> --- cmd/create.go | 8 ++++---- cmd/userinput.go | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cmd/create.go b/cmd/create.go index 7b1b759..d2a0257 100644 --- a/cmd/create.go +++ b/cmd/create.go @@ -219,10 +219,10 @@ func (k *KsctlCommand) PrintRecommendationSelfManagedCost( headers := []string{ "Region", - fmt.Sprintf("Control Plane << %s >>", instanceTypeCP), - fmt.Sprintf("Worker Plane << %s >>", instanceTypeWP), - fmt.Sprintf("Etcd Nodes << %s >>", instanceTypeDS), - fmt.Sprintf("Load Balancer << %s >>", instanceTypeLB), + fmt.Sprintf("Control Plane (%s)", instanceTypeCP), + fmt.Sprintf("Worker Plane (%s)", instanceTypeWP), + fmt.Sprintf("Etcd Nodes (%s)", instanceTypeDS), + fmt.Sprintf("Load Balancer (%s)", instanceTypeLB), "Total Monthly Cost", } diff --git a/cmd/userinput.go b/cmd/userinput.go index 3fc1013..b6a88ef 100644 --- a/cmd/userinput.go +++ b/cmd/userinput.go @@ -105,7 +105,7 @@ func (k *KsctlCommand) getSelectedInstanceCategory(categories map[string]provide for k, _v := range categories { useCases := strings.Join(_v.UseCases(), ", ") - key := fmt.Sprintf(">>> %s <<<\n Used for: %s\n", k, useCases) + key := fmt.Sprintf("%s\n Used for: %s\n", k, useCases) vr[key] = string(_v) } From 8bd70da81567b26f0aeffe549036cc2306501527 Mon Sep 17 00:00:00 2001 From: Dipankar Das <65275144+dipankardas011@users.noreply.github.com> Date: Wed, 26 Mar 2025 10:30:49 +0530 Subject: [PATCH 6/9] added region switcher Signed-off-by: Dipankar Das <65275144+dipankardas011@users.noreply.github.com> --- cmd/create.go | 54 ++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 43 insertions(+), 11 deletions(-) diff --git a/cmd/create.go b/cmd/create.go index d2a0257..9d4a64c 100644 --- a/cmd/create.go +++ b/cmd/create.go @@ -19,6 +19,7 @@ import ( "fmt" "os" "slices" + "time" "github.com/fatih/color" "github.com/ksctl/ksctl/v2/pkg/consts" @@ -353,17 +354,48 @@ func (k *KsctlCommand) metadataForSelfManagedCluster( os.Exit(1) } - // TODO: add spinner - k.PrintRecommendationSelfManagedCost( - <-isOptimizeInstanceRegionReady, - meta.NoCP, - meta.NoWP, - meta.NoDS, - cp.Sku, - wp.Sku, - etcd.Sku, - lb.Sku, - ) + func() { + ticker := time.NewTicker(2 * time.Second) + defer ticker.Stop() + for { + select { + case o := <-isOptimizeInstanceRegionReady: + k.PrintRecommendationSelfManagedCost( + o, + meta.NoCP, + meta.NoWP, + meta.NoDS, + cp.Sku, + wp.Sku, + etcd.Sku, + lb.Sku, + ) + + availRegions := []string{"No don't change"} + for _, _o := range o[:5] { + availRegions = append(availRegions, _o.region) + } + + v, err := k.menuDriven.DropDownList(fmt.Sprintf("Region Switch. Currently set (%s)", meta.Region), availRegions, + cli.WithDefaultValue("Don't change"), + ) + if err != nil { + k.l.Error("Skipping it becuase failed to get the region switch", "Reason", err) + return + } + if v == "Don't change" { + return + } + + k.l.Print(k.Ctx, "changed the region", "from", color.HiRedString(meta.Region), "to", color.HiGreenString(v)) + meta.Region = v + + return + case <-ticker.C: + k.l.Print(k.Ctx, "Still optimizing instance types...") + } + } + }() managedCNI, defaultCNI, ksctlCNI, defaultKsctl, err := metaClient.ListBootstrapCNIs() if err != nil { From 429bc4ec75d8f9caf1b9f6e7b3d195398bac127b Mon Sep 17 00:00:00 2001 From: Dipankar Das <65275144+dipankardas011@users.noreply.github.com> Date: Wed, 26 Mar 2025 23:24:01 +0530 Subject: [PATCH 7/9] feat: added the cost optimization of instancetype across region Signed-off-by: Dipankar Das <65275144+dipankardas011@users.noreply.github.com> --- cmd/create.go | 244 ++++++++------------------------ cmd/optimize.go | 367 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 426 insertions(+), 185 deletions(-) create mode 100644 cmd/optimize.go diff --git a/cmd/create.go b/cmd/create.go index 9d4a64c..3103a8e 100644 --- a/cmd/create.go +++ b/cmd/create.go @@ -15,7 +15,6 @@ package cmd import ( - "cmp" "fmt" "os" "slices" @@ -63,186 +62,6 @@ ksctl create --help return cmd } -// findCostAcrossRegions it returns a map of K[V] where K is the region and V is the cost of the instance -func (k *KsctlCommand) findCostAcrossRegions( - meta controller.Metadata, - availRegions []provider.RegionOutput, - instanceSku string, -) (map[string]float64, error) { - metaClient, err := controllerMeta.NewController( - k.Ctx, - k.l, - &controller.Client{ - Metadata: meta, - }, - ) - if err != nil { - return nil, err - } - - resultChan := make(chan struct { - region string - price float64 - err error - }, len(availRegions)) - - for _, region := range availRegions { - regSku := region.Sku - go func(sku string) { - price, err := metaClient.GetPriceForInstance(sku, instanceSku) - resultChan <- struct { - region string - price float64 - err error - }{sku, price, err} - }(regSku) - } - - cost := make(map[string]float64, len(availRegions)) - for i := 0; i < len(availRegions); i++ { - result := <-resultChan - if result.err == nil { - cost[result.region] = result.price - } - } - - return cost, nil -} - -type RecommendationSelfManagedCost struct { - region string - totalCost float64 - - cpCost float64 - wpCost float64 - etcdCost float64 - lbCost float64 -} - -func (k *KsctlCommand) getBestRegionsWithTotalCostSelfManaged( - allAvailRegions []provider.RegionOutput, - costForCP map[string]float64, - costForWP map[string]float64, - costForDS map[string]float64, - costForLB map[string]float64, -) []RecommendationSelfManagedCost { - - checkRegion := func(region string, m map[string]float64) bool { - _, ok := m[region] - return ok - } - - var costForCluster []RecommendationSelfManagedCost - - for _, region := range allAvailRegions { - if !checkRegion(region.Sku, costForCP) || - !checkRegion(region.Sku, costForWP) || - !checkRegion(region.Sku, costForDS) || - !checkRegion(region.Sku, costForLB) { - continue - } - - totalCost := costForCP[region.Sku] + costForWP[region.Sku] + costForDS[region.Sku] + costForLB[region.Sku] - - costForCluster = append(costForCluster, RecommendationSelfManagedCost{ - region: region.Sku, - cpCost: costForCP[region.Sku], - wpCost: costForWP[region.Sku], - etcdCost: costForDS[region.Sku], - lbCost: costForLB[region.Sku], - totalCost: totalCost, - }) - } - - slices.SortFunc(costForCluster, func(a, b RecommendationSelfManagedCost) int { - return cmp.Compare(a.totalCost, b.totalCost) - }) - - if len(costForCluster) < 3 { - return costForCluster - } - - return costForCluster -} - -func (k *KsctlCommand) OptimizeInstanceRegion( - meta *controller.Metadata, - allAvailRegions []provider.RegionOutput, - cp provider.InstanceRegionOutput, - wp provider.InstanceRegionOutput, - etcd provider.InstanceRegionOutput, - lb provider.InstanceRegionOutput, -) []RecommendationSelfManagedCost { - cpInstanceCosts, err := k.findCostAcrossRegions(*meta, allAvailRegions, cp.Sku) - if err != nil { - k.l.Error("Failed to get the cost of control plane instances", "Reason", err) - } - - wpInstanceCosts, err := k.findCostAcrossRegions(*meta, allAvailRegions, wp.Sku) - if err != nil { - k.l.Error("Failed to get the cost of worker plane instances", "Reason", err) - } - - etcdInstanceCosts, err := k.findCostAcrossRegions(*meta, allAvailRegions, etcd.Sku) - if err != nil { - k.l.Error("Failed to get the cost of etcd instances", "Reason", err) - } - - lbInstanceCosts, err := k.findCostAcrossRegions(*meta, allAvailRegions, lb.Sku) - if err != nil { - k.l.Error("Failed to get the cost of load balancer instances", "Reason", err) - } - - return k.getBestRegionsWithTotalCostSelfManaged( - allAvailRegions, - cpInstanceCosts, - wpInstanceCosts, - etcdInstanceCosts, - lbInstanceCosts, - ) -} - -func (k *KsctlCommand) PrintRecommendationSelfManagedCost( - costs []RecommendationSelfManagedCost, - noOfCP int, - noOfWP int, - noOfDS int, - instanceTypeCP string, - instanceTypeWP string, - instanceTypeDS string, - instanceTypeLB string, -) { - k.l.Print(k.Ctx, - "Here is your recommendation", - "Parameter", "Region wise cost", - "OptimizedRegion", color.HiCyanString(costs[0].region), - ) - - headers := []string{ - "Region", - fmt.Sprintf("Control Plane (%s)", instanceTypeCP), - fmt.Sprintf("Worker Plane (%s)", instanceTypeWP), - fmt.Sprintf("Etcd Nodes (%s)", instanceTypeDS), - fmt.Sprintf("Load Balancer (%s)", instanceTypeLB), - "Total Monthly Cost", - } - - var data [][]string - for _, cost := range costs { - total := cost.cpCost*float64(noOfCP) + cost.wpCost*float64(noOfWP) + cost.etcdCost*float64(noOfDS) + cost.lbCost - data = append(data, []string{ - cost.region, - fmt.Sprintf("$%.2f X %d", cost.cpCost, noOfCP), - fmt.Sprintf("$%.2f X %d", cost.wpCost, noOfWP), - fmt.Sprintf("$%.2f X %d", cost.etcdCost, noOfDS), - fmt.Sprintf("$%.2f X 1", cost.lbCost), - fmt.Sprintf("$%.2f", total), - }) - } - - k.l.Table(k.Ctx, headers, data) -} - func (k *KsctlCommand) metadataForSelfManagedCluster( meta *controller.Metadata, ) { @@ -303,11 +122,10 @@ func (k *KsctlCommand) metadataForSelfManagedCluster( meta.NoDS = v } - var isOptimizeInstanceRegionReady chan []RecommendationSelfManagedCost - isOptimizeInstanceRegionReady = make(chan []RecommendationSelfManagedCost) + isOptimizeInstanceRegionReady := make(chan []RecommendationSelfManagedCost) go func() { - isOptimizeInstanceRegionReady <- k.OptimizeInstanceRegion(meta, allAvailRegions, cp, wp, etcd, lb) + isOptimizeInstanceRegionReady <- k.OptimizeSelfManagedInstanceTypesAcrossRegions(meta, allAvailRegions, cp, wp, etcd, lb) }() bootstrapVers, err := metaClient.ListAllBootstrapVersions() @@ -370,6 +188,10 @@ func (k *KsctlCommand) metadataForSelfManagedCluster( etcd.Sku, lb.Sku, ) + pos := slices.IndexFunc(o, func(i RecommendationSelfManagedCost) bool { + return i.region == meta.Region + }) + o = append(o[:pos], o[pos+1:]...) availRegions := []string{"No don't change"} for _, _o := range o[:5] { @@ -473,8 +295,10 @@ func (k *KsctlCommand) metadataForManagedCluster( meta.NoMP = v } + isOptimizeInstanceRegionReady := make(chan []RecommendationManagedCost) + if meta.Provider != consts.CloudLocal { - _ = k.handleRegionSelection(metaClient, meta) + allAvailRegions := k.handleRegionSelection(metaClient, meta) category := provider.Unknown if meta.Provider != consts.CloudLocal { @@ -503,6 +327,12 @@ func (k *KsctlCommand) metadataForManagedCluster( offeringSelected = v } + go func() { + isOptimizeInstanceRegionReady <- k.OptimizeManagedOfferingsAcrossRegions(meta, allAvailRegions, listOfOfferings[offeringSelected], vm) + }() + + k.l.Print(k.Ctx, "Current Selection will cost you") + _, err = metaClient.PriceCalculator( controllerMeta.PriceCalculatorInput{ ManagedControlPlaneMachine: listOfOfferings[offeringSelected], @@ -513,6 +343,50 @@ func (k *KsctlCommand) metadataForManagedCluster( k.l.Error("Failed to calculate the price", "Reason", err) os.Exit(1) } + + func() { + ticker := time.NewTicker(2 * time.Second) + defer ticker.Stop() + for { + select { + case o := <-isOptimizeInstanceRegionReady: + k.PrintRecommendationManagedCost( + o, + meta.NoMP, + listOfOfferings[offeringSelected].Sku, + vm.Sku, + ) + + pos := slices.IndexFunc(o, func(i RecommendationManagedCost) bool { + return i.region == meta.Region + }) + o = append(o[:pos], o[pos+1:]...) + + availRegions := []string{"No don't change"} + for _, _o := range o[:5] { + availRegions = append(availRegions, _o.region) + } + + v, err := k.menuDriven.DropDownList(fmt.Sprintf("Region Switch. Currently set (%s)", meta.Region), availRegions, + cli.WithDefaultValue("Don't change"), + ) + if err != nil { + k.l.Error("Skipping it becuase failed to get the region switch", "Reason", err) + return + } + if v == "Don't change" { + return + } + + k.l.Print(k.Ctx, "changed the region", "from", color.HiRedString(meta.Region), "to", color.HiGreenString(v)) + meta.Region = v + + return + case <-ticker.C: + k.l.Print(k.Ctx, "Still optimizing instance types...") + } + } + }() } managedCNI, defaultCNI, ksctlCNI, defaultKsctl, err := metaClient.ListManagedCNIs() diff --git a/cmd/optimize.go b/cmd/optimize.go new file mode 100644 index 0000000..57be3fc --- /dev/null +++ b/cmd/optimize.go @@ -0,0 +1,367 @@ +// Copyright 2025 Ksctl Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "cmp" + "fmt" + "slices" + + "github.com/fatih/color" + "github.com/ksctl/ksctl/v2/pkg/handler/cluster/controller" + controllerMeta "github.com/ksctl/ksctl/v2/pkg/handler/cluster/metadata" + "github.com/ksctl/ksctl/v2/pkg/provider" +) + +func (k *KsctlCommand) findManagedOfferingCostAcrossRegions( + meta controller.Metadata, + availRegions []provider.RegionOutput, + managedOfferingSku string, +) (map[string]float64, error) { + metaClient, err := controllerMeta.NewController( + k.Ctx, + k.l, + &controller.Client{ + Metadata: meta, + }, + ) + if err != nil { + return nil, err + } + resultChan := make(chan struct { + region string + price float64 + err error + }, len(availRegions)) + + for _, region := range availRegions { + regSku := region.Sku + go func(sku string) { + _price, err := metaClient.ListAllManagedClusterManagementOfferings(regSku, nil) + if err == nil { + v, ok := _price[managedOfferingSku] + if ok { + resultChan <- struct { + region string + price float64 + err error + }{sku, v.GetCost(), nil} + } else { + resultChan <- struct { + region string + price float64 + err error + }{sku, 0.0, fmt.Errorf("managed offering not found")} + } + + } else { + resultChan <- struct { + region string + price float64 + err error + }{sku, 0.0, err} + } + }(regSku) + } + + cost := make(map[string]float64, len(availRegions)) + for i := 0; i < len(availRegions); i++ { + result := <-resultChan + if result.err == nil { + cost[result.region] = result.price + } + } + + return cost, nil +} + +// findInstanceCostAcrossRegions it returns a map of K[V] where K is the region and V is the cost of the instance +func (k *KsctlCommand) findInstanceCostAcrossRegions( + meta controller.Metadata, + availRegions []provider.RegionOutput, + instanceSku string, +) (map[string]float64, error) { + metaClient, err := controllerMeta.NewController( + k.Ctx, + k.l, + &controller.Client{ + Metadata: meta, + }, + ) + if err != nil { + return nil, err + } + + resultChan := make(chan struct { + region string + price float64 + err error + }, len(availRegions)) + + for _, region := range availRegions { + regSku := region.Sku + go func(sku string) { + price, err := metaClient.GetPriceForInstance(sku, instanceSku) + resultChan <- struct { + region string + price float64 + err error + }{sku, price, err} + }(regSku) + } + + cost := make(map[string]float64, len(availRegions)) + for i := 0; i < len(availRegions); i++ { + result := <-resultChan + if result.err == nil { + cost[result.region] = result.price + } + } + + return cost, nil +} + +type RecommendationManagedCost struct { + region string + totalCost float64 + + cpCost float64 + wpCost float64 +} + +func (k *KsctlCommand) getBestRegionsWithTotalCostManaged( + allAvailRegions []provider.RegionOutput, + costForCP map[string]float64, + costForWP map[string]float64, +) []RecommendationManagedCost { + + checkRegion := func(region string, m map[string]float64) bool { + _, ok := m[region] + return ok + } + + var costForCluster []RecommendationManagedCost + + for _, region := range allAvailRegions { + if !checkRegion(region.Sku, costForCP) || + !checkRegion(region.Sku, costForWP) { + continue + } + + totalCost := costForCP[region.Sku] + costForWP[region.Sku] + + costForCluster = append(costForCluster, RecommendationManagedCost{ + region: region.Sku, + cpCost: costForCP[region.Sku], + wpCost: costForWP[region.Sku], + totalCost: totalCost, + }) + } + + slices.SortFunc(costForCluster, func(a, b RecommendationManagedCost) int { + return cmp.Compare(a.totalCost, b.totalCost) + }) + + return costForCluster +} + +type RecommendationSelfManagedCost struct { + region string + totalCost float64 + + cpCost float64 + wpCost float64 + etcdCost float64 + lbCost float64 +} + +func (k *KsctlCommand) getBestRegionsWithTotalCostSelfManaged( + allAvailRegions []provider.RegionOutput, + costForCP map[string]float64, + costForWP map[string]float64, + costForDS map[string]float64, + costForLB map[string]float64, +) []RecommendationSelfManagedCost { + + checkRegion := func(region string, m map[string]float64) bool { + _, ok := m[region] + return ok + } + + var costForCluster []RecommendationSelfManagedCost + + for _, region := range allAvailRegions { + if !checkRegion(region.Sku, costForCP) || + !checkRegion(region.Sku, costForWP) || + !checkRegion(region.Sku, costForDS) || + !checkRegion(region.Sku, costForLB) { + continue + } + + totalCost := costForCP[region.Sku] + costForWP[region.Sku] + costForDS[region.Sku] + costForLB[region.Sku] + + costForCluster = append(costForCluster, RecommendationSelfManagedCost{ + region: region.Sku, + cpCost: costForCP[region.Sku], + wpCost: costForWP[region.Sku], + etcdCost: costForDS[region.Sku], + lbCost: costForLB[region.Sku], + totalCost: totalCost, + }) + } + + slices.SortFunc(costForCluster, func(a, b RecommendationSelfManagedCost) int { + return cmp.Compare(a.totalCost, b.totalCost) + }) + + return costForCluster +} + +// OptimizeSelfManagedInstanceTypesAcrossRegions it returns a sorted list of regions based on the total cost in ascending order across all the regions +// +// It is a core function that is used to optimize the cost of the self-managed cluster instanceType across all the regions (Cost Optimization) +func (k *KsctlCommand) OptimizeSelfManagedInstanceTypesAcrossRegions( + meta *controller.Metadata, + allAvailRegions []provider.RegionOutput, + cp provider.InstanceRegionOutput, + wp provider.InstanceRegionOutput, + etcd provider.InstanceRegionOutput, + lb provider.InstanceRegionOutput, +) []RecommendationSelfManagedCost { + cpInstanceCosts, err := k.findInstanceCostAcrossRegions(*meta, allAvailRegions, cp.Sku) + if err != nil { + k.l.Error("Failed to get the cost of control plane instances", "Reason", err) + } + + wpInstanceCosts, err := k.findInstanceCostAcrossRegions(*meta, allAvailRegions, wp.Sku) + if err != nil { + k.l.Error("Failed to get the cost of worker plane instances", "Reason", err) + } + + etcdInstanceCosts, err := k.findInstanceCostAcrossRegions(*meta, allAvailRegions, etcd.Sku) + if err != nil { + k.l.Error("Failed to get the cost of etcd instances", "Reason", err) + } + + lbInstanceCosts, err := k.findInstanceCostAcrossRegions(*meta, allAvailRegions, lb.Sku) + if err != nil { + k.l.Error("Failed to get the cost of load balancer instances", "Reason", err) + } + + return k.getBestRegionsWithTotalCostSelfManaged( + allAvailRegions, + cpInstanceCosts, + wpInstanceCosts, + etcdInstanceCosts, + lbInstanceCosts, + ) +} + +func (k *KsctlCommand) OptimizeManagedOfferingsAcrossRegions( + meta *controller.Metadata, + allAvailRegions []provider.RegionOutput, + cp provider.ManagedClusterOutput, + wp provider.InstanceRegionOutput, +) []RecommendationManagedCost { + wpInstanceCosts, err := k.findInstanceCostAcrossRegions(*meta, allAvailRegions, wp.Sku) + if err != nil { + k.l.Error("Failed to get the cost of worker plane instances", "Reason", err) + } + + cpInstanceCosts, err := k.findManagedOfferingCostAcrossRegions(*meta, allAvailRegions, cp.Sku) + if err != nil { + k.l.Error("Failed to get the cost of control plane managed offerings", "Reason", err) + } + + return k.getBestRegionsWithTotalCostManaged( + allAvailRegions, + cpInstanceCosts, + wpInstanceCosts, + ) +} + +func (k *KsctlCommand) PrintRecommendationSelfManagedCost( + costs []RecommendationSelfManagedCost, + noOfCP int, + noOfWP int, + noOfDS int, + instanceTypeCP string, + instanceTypeWP string, + instanceTypeDS string, + instanceTypeLB string, +) { + k.l.Print(k.Ctx, + "Here is your recommendation", + "Parameter", "Region wise cost", + "OptimizedRegion", color.HiCyanString(costs[0].region), + ) + + headers := []string{ + "Region", + fmt.Sprintf("Control Plane (%s)", instanceTypeCP), + fmt.Sprintf("Worker Plane (%s)", instanceTypeWP), + fmt.Sprintf("Etcd Nodes (%s)", instanceTypeDS), + fmt.Sprintf("Load Balancer (%s)", instanceTypeLB), + "Total Monthly Cost", + } + + var data [][]string + for _, cost := range costs { + total := cost.cpCost*float64(noOfCP) + cost.wpCost*float64(noOfWP) + cost.etcdCost*float64(noOfDS) + cost.lbCost + data = append(data, []string{ + cost.region, + fmt.Sprintf("$%.2f X %d", cost.cpCost, noOfCP), + fmt.Sprintf("$%.2f X %d", cost.wpCost, noOfWP), + fmt.Sprintf("$%.2f X %d", cost.etcdCost, noOfDS), + fmt.Sprintf("$%.2f X 1", cost.lbCost), + fmt.Sprintf("$%.2f", total), + }) + } + + k.l.Table(k.Ctx, headers, data) +} + +func (k *KsctlCommand) PrintRecommendationManagedCost( + costs []RecommendationManagedCost, + noOfWP int, + managedOfferingCP string, + instanceTypeWP string, +) { + k.l.Print(k.Ctx, + "Here is your recommendation", + "Parameter", "Region wise cost", + "OptimizedRegion", color.HiCyanString(costs[0].region), + ) + + headers := []string{ + "Region", + fmt.Sprintf("Control Plane (%s)", managedOfferingCP), + fmt.Sprintf("Worker Plane (%s)", instanceTypeWP), + "Total Monthly Cost", + } + + var data [][]string + for _, cost := range costs { + total := cost.cpCost + cost.wpCost*float64(noOfWP) + data = append(data, []string{ + cost.region, + fmt.Sprintf("$%.2f X 1", cost.cpCost), + fmt.Sprintf("$%.2f X %d", cost.wpCost, noOfWP), + fmt.Sprintf("$%.2f", total), + }) + } + + k.l.Table(k.Ctx, headers, data) +} From 6ccf021df0b928832f770f06a7c5bd6bc179e830 Mon Sep 17 00:00:00 2001 From: Dipankar Das <65275144+dipankardas011@users.noreply.github.com> Date: Thu, 27 Mar 2025 12:21:24 +0530 Subject: [PATCH 8/9] bumped up mod verion for ksctl core Signed-off-by: Dipankar Das <65275144+dipankardas011@users.noreply.github.com> --- go.mod | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 5b97b75..dec43c1 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/Delta456/box-cli-maker/v2 v2.3.0 github.com/creack/pty v1.1.24 github.com/fatih/color v1.18.0 - github.com/ksctl/ksctl/v2 v2.1.0 + github.com/ksctl/ksctl/v2 v2.2.0 github.com/pterm/pterm v0.12.80 github.com/rodaine/table v1.3.0 github.com/spf13/cobra v1.9.1 @@ -17,8 +17,6 @@ require ( k8s.io/client-go v0.32.2 ) -replace github.com/ksctl/ksctl/v2 => ../ksctl - require ( al.essio.dev/pkg/shellescape v1.5.1 // indirect atomicgo.dev/cursor v0.2.0 // indirect From 914ca743de0c077705447f076fbc4bb7a2f87cef Mon Sep 17 00:00:00 2001 From: Dipankar Das <65275144+dipankardas011@users.noreply.github.com> Date: Thu, 27 Mar 2025 15:25:21 +0530 Subject: [PATCH 9/9] fixed the module deps Signed-off-by: Dipankar Das <65275144+dipankardas011@users.noreply.github.com> --- go.sum | 2 ++ 1 file changed, 2 insertions(+) diff --git a/go.sum b/go.sum index 5ccd75e..2b8bbbe 100644 --- a/go.sum +++ b/go.sum @@ -351,6 +351,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/ksctl/ksctl/v2 v2.2.0 h1:yi732F7viIGxJimtq8C58NYxVW3/1dXMe5N6XYt7nY4= +github.com/ksctl/ksctl/v2 v2.2.0/go.mod h1:w9I+7Jm8umrF6JbeiShOYnj4PNRE+534Ji6QAifAr4s= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=