From 745103c91f0c368d972c1c5e4db5da653de572b6 Mon Sep 17 00:00:00 2001 From: vmallepalli Date: Wed, 10 Jan 2024 22:58:21 -0600 Subject: [PATCH] [CLOUDGA-18071] CLI support for asymmetric geo-partitioned cluster creation --- cmd/cluster/create_cluster.go | 63 +++++++-- cmd/cluster/update_cluster.go | 100 ++++++++++---- cmd/util/feature_flags.go | 1 + go.mod | 4 + go.sum | 5 + internal/client/client.go | 188 ++++++++++++++++++++++----- internal/formatter/clusters_full.go | 24 +++- internal/formatter/instance_types.go | 14 +- 8 files changed, 322 insertions(+), 77 deletions(-) diff --git a/cmd/cluster/create_cluster.go b/cmd/cluster/create_cluster.go index 40cc5fd2..45cfffa7 100644 --- a/cmd/cluster/create_cluster.go +++ b/cmd/cluster/create_cluster.go @@ -18,6 +18,7 @@ package cluster import ( "fmt" "os" + "strconv" "strings" "github.com/sirupsen/logrus" @@ -41,6 +42,7 @@ var createClusterCmd = &cobra.Command{ logrus.Fatalf(ybmAuthClient.GetApiErrorDetails(err)) } authApi.GetInfo("", "") + asymmetricGeoEnabled := util.IsFeatureFlagEnabled(util.ASYMMETRIC_GEO) clusterName, _ := cmd.Flags().GetString("cluster-name") credentials, _ := cmd.Flags().GetStringToString("credentials") @@ -48,7 +50,30 @@ var createClusterCmd = &cobra.Command{ username := credentials["username"] password := credentials["password"] regionInfoMapList := []map[string]string{} - if cmd.Flags().Changed("region-info") { + changedRegionInfo := cmd.Flags().Changed("region-info") + changedNodeInfo := cmd.Flags().Changed("node-config") + + defaultNumCores := 0 + defaultDiskSizeGb := 0 + defaultDiskIops := 0 + if changedNodeInfo { + nodeConfig, _ := cmd.Flags().GetStringToInt("node-config") + numCores, ok := nodeConfig["num-cores"] + if !asymmetricGeoEnabled && !ok { + logrus.Fatalln("Number of cores not specified in node config") + } + if asymmetricGeoEnabled && ok { + defaultNumCores = numCores + } + if diskSizeGb, ok := nodeConfig["disk-size-gb"]; ok { + defaultDiskSizeGb = diskSizeGb + } + if diskIops, ok := nodeConfig["disk-iops"]; ok { + defaultDiskIops = diskIops + } + } + + if changedRegionInfo { regionInfoList, _ := cmd.Flags().GetStringArray("region-info") for _, regionInfoString := range regionInfoList { regionInfoMap := map[string]string{} @@ -72,6 +97,18 @@ var createClusterCmd = &cobra.Command{ if len(strings.TrimSpace(val)) != 0 { regionInfoMap["vpc"] = val } + case "num-cores": + if asymmetricGeoEnabled && len(strings.TrimSpace(val)) != 0 { + regionInfoMap["num-cores"] = val + } + case "disk-size-gb": + if asymmetricGeoEnabled && len(strings.TrimSpace(val)) != 0 { + regionInfoMap["disk-size-gb"] = val + } + case "disk-iops": + if asymmetricGeoEnabled && len(strings.TrimSpace(val)) != 0 { + regionInfoMap["disk-iops"] = val + } } } @@ -81,18 +118,20 @@ var createClusterCmd = &cobra.Command{ if _, ok := regionInfoMap["num-nodes"]; !ok { logrus.Fatalln("Number of nodes not specified in region info") } + if _, ok := regionInfoMap["num-cores"]; asymmetricGeoEnabled && !ok && defaultNumCores > 0 { + regionInfoMap["num-cores"] = strconv.Itoa(defaultNumCores) + } + if _, ok := regionInfoMap["disk-size-gb"]; asymmetricGeoEnabled && !ok && defaultDiskSizeGb > 0 { + regionInfoMap["disk-size-gb"] = strconv.Itoa(defaultDiskSizeGb) + } + if _, ok := regionInfoMap["disk-iops"]; asymmetricGeoEnabled && !ok && defaultDiskIops > 0 { + regionInfoMap["disk-iops"] = strconv.Itoa(defaultDiskIops) + } regionInfoMapList = append(regionInfoMapList, regionInfoMap) } } - if cmd.Flags().Changed("node-config") { - nodeConfig, _ := cmd.Flags().GetStringToInt("node-config") - if _, ok := nodeConfig["num-cores"]; !ok { - logrus.Fatalln("Number of cores not specified in node config") - } - } - cmkSpec, err := encryption.GetCmkSpecFromCommand(cmd) if err != nil { logrus.Fatalf("Error while getting CMK spec: %s", err) @@ -176,7 +215,6 @@ func init() { createClusterCmd.Flags().String("database-version", "", "[OPTIONAL] The database version of the cluster. Production, Innovation or Preview. Default depends on cluster tier, Sandbox is Preview, Dedicated is Production.") if util.IsFeatureFlagEnabled(util.ENTERPRISE_SECURITY) { createClusterCmd.Flags().Bool("enterprise-security", false, "[OPTIONAL] The security level of cluster. Advanced security will have security checks for cluster. Default false.") - } createClusterCmd.Flags().String("encryption-spec", "", `[OPTIONAL] The customer managed key spec for the cluster. Please provide key value pairs as follows: @@ -191,7 +229,12 @@ func init() { createClusterCmd.Flags().String("fault-tolerance", "", "[OPTIONAL] The fault tolerance domain of the cluster. The possible values are NONE, NODE, ZONE and REGION. Default NONE.") createClusterCmd.Flags().Int32("num-faults-to-tolerate", 0, "[OPTIONAL] The number of domain faults to tolerate for the level specified. The possible values are 0 for NONE, 1 for ZONE and [1-3] for anything else. Defaults to 0 for NONE, 1 otherwise.") createClusterCmd.Flags().StringToInt("node-config", nil, "[OPTIONAL] Configuration of the cluster nodes. Please provide key value pairs num-cores=,disk-size-gb=,disk-iops= as the value. If specified, num-cores is mandatory, while disk-size-gb and disk-iops are optional.") - createClusterCmd.Flags().StringArray("region-info", []string{}, `[OPTIONAL] Region information for the cluster. Please provide key value pairs region=,num-nodes=,vpc= as the value. If specified, region and num-nodes are mandatory, vpc is optional. Information about multiple regions can be specified by using multiple --region-info arguments. Default if not specified is us-west-2 AWS region.`) + if util.IsFeatureFlagEnabled(util.ASYMMETRIC_GEO) { + createClusterCmd.Flags().MarkDeprecated("node-config", "please use --region-info to specify num-cores, disk-size-gb, and disk-iops") + createClusterCmd.Flags().StringArray("region-info", []string{}, `[OPTIONAL] Region information for the cluster. Please provide key value pairs region=,num-nodes=,vpc=,num-cores=,disk-size-gb=,disk-iops= as the value. If specified, region and num-nodes are mandatory, while num-cores, disk-size-gb, disk-iops, and vpc are optional. Information about multiple regions can be specified by using multiple --region-info arguments. Default if not specified is us-west-2 AWS region.`) + } else { + createClusterCmd.Flags().StringArray("region-info", []string{}, `[OPTIONAL] Region information for the cluster. Please provide key value pairs region=,num-nodes=,vpc= as the value. If specified, region and num-nodes are mandatory, vpc is optional. Information about multiple regions can be specified by using multiple --region-info arguments. Default if not specified is us-west-2 AWS region.`) + } createClusterCmd.Flags().String("preferred-region", "", "[OPTIONAL] The preferred region in a multi region cluster. A preferred region is where all the reads and writes are handled.") createClusterCmd.Flags().String("default-region", "", "[OPTIONAL] The default region in a geo partitioned cluster. A default region is where all the tables not created within a tablespace reside.") diff --git a/cmd/cluster/update_cluster.go b/cmd/cluster/update_cluster.go index 72b3044b..9862a3ae 100644 --- a/cmd/cluster/update_cluster.go +++ b/cmd/cluster/update_cluster.go @@ -24,6 +24,7 @@ import ( "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/viper" + "github.com/yugabyte/ybm-cli/cmd/util" ybmAuthClient "github.com/yugabyte/ybm-cli/internal/client" "github.com/yugabyte/ybm-cli/internal/formatter" ybmclient "github.com/yugabyte/yugabytedb-managed-go-client-internal" @@ -36,6 +37,7 @@ var updateClusterCmd = &cobra.Command{ Long: "Update a cluster", Run: func(cmd *cobra.Command, args []string) { + asymmetricGeoEnabled := util.IsFeatureFlagEnabled(util.ASYMMETRIC_GEO) clusterName, _ := cmd.Flags().GetString("cluster-name") authApi, err := ybmAuthClient.NewAuthApiClient() if err != nil { @@ -62,9 +64,31 @@ var updateClusterCmd = &cobra.Command{ populateFlags(cmd, originalSpec, trackName, authApi) regionInfoMapList := []map[string]string{} - if cmd.Flags().Changed("region-info") { + changedRegionInfo := cmd.Flags().Changed("region-info") + changedNodeInfo := cmd.Flags().Changed("node-config") + + defaultNumCores := 0 + defaultDiskSizeGb := 0 + defaultDiskIops := 0 + if changedNodeInfo { + nodeConfig, _ := cmd.Flags().GetStringToInt("node-config") + numCores, ok := nodeConfig["num-cores"] + if !asymmetricGeoEnabled && !ok { + logrus.Fatalln("Number of cores not specified in node config") + } + if asymmetricGeoEnabled && ok { + defaultNumCores = numCores + } + if diskSizeGb, ok := nodeConfig["disk-size-gb"]; ok { + defaultDiskSizeGb = diskSizeGb + } + if diskIops, ok := nodeConfig["disk-iops"]; ok { + defaultDiskIops = diskIops + } + } + + if changedRegionInfo { regionInfoList, _ := cmd.Flags().GetStringArray("region-info") - regionInfoList = strings.Split(regionInfoList[0], "|") for _, regionInfoString := range regionInfoList { regionInfoMap := map[string]string{} for _, regionInfo := range strings.Split(regionInfoString, ",") { @@ -87,6 +111,18 @@ var updateClusterCmd = &cobra.Command{ if len(strings.TrimSpace(val)) != 0 { regionInfoMap["vpc"] = val } + case "num-cores": + if asymmetricGeoEnabled && len(strings.TrimSpace(val)) != 0 { + regionInfoMap["num-cores"] = val + } + case "disk-size-gb": + if asymmetricGeoEnabled && len(strings.TrimSpace(val)) != 0 { + regionInfoMap["disk-size-gb"] = val + } + case "disk-iops": + if asymmetricGeoEnabled && len(strings.TrimSpace(val)) != 0 { + regionInfoMap["disk-iops"] = val + } } } @@ -96,18 +132,20 @@ var updateClusterCmd = &cobra.Command{ if _, ok := regionInfoMap["num-nodes"]; !ok { logrus.Fatalln("Number of nodes not specified in region info") } + if _, ok := regionInfoMap["num-cores"]; asymmetricGeoEnabled && !ok && defaultNumCores > 0 { + regionInfoMap["num-cores"] = strconv.Itoa(defaultNumCores) + } + if _, ok := regionInfoMap["disk-size-gb"]; asymmetricGeoEnabled && !ok && defaultDiskSizeGb > 0 { + regionInfoMap["disk-size-gb"] = strconv.Itoa(defaultDiskSizeGb) + } + if _, ok := regionInfoMap["disk-iops"]; asymmetricGeoEnabled && !ok && defaultDiskIops > 0 { + regionInfoMap["disk-iops"] = strconv.Itoa(defaultDiskIops) + } regionInfoMapList = append(regionInfoMapList, regionInfoMap) } } - if cmd.Flags().Changed("node-config") { - nodeConfig, _ := cmd.Flags().GetStringToInt("node-config") - if _, ok := nodeConfig["num-cores"]; !ok { - logrus.Fatal("Number of cores not specified in node config\n") - } - } - clusterSpec, err := authApi.CreateClusterSpec(cmd, regionInfoMapList) if err != nil { logrus.Fatalf("Error while creating cluster spec: %v", err) @@ -172,7 +210,12 @@ func init() { updateClusterCmd.Flags().String("cloud-provider", "", "[OPTIONAL] The cloud provider where database needs to be deployed. AWS, AZURE or GCP.") updateClusterCmd.Flags().String("cluster-type", "", "[OPTIONAL] Cluster replication type. SYNCHRONOUS or GEO_PARTITIONED.") updateClusterCmd.Flags().StringToInt("node-config", nil, "[OPTIONAL] Configuration of the cluster nodes. Please provide key value pairs num-cores=,disk-size-gb=,disk-iops= as the value. If provided, num-cores is mandatory, while disk-size-gb and disk-iops are optional.") - updateClusterCmd.Flags().StringArray("region-info", []string{}, `[OPTIONAL] Region information for the cluster. Please provide key value pairs, region=,num-nodes=,vpc= as the value. If provided, region and num-nodes are mandatory, vpc is optional.`) + if util.IsFeatureFlagEnabled(util.ASYMMETRIC_GEO) { + updateClusterCmd.Flags().MarkDeprecated("node-config", "please use --region-info to specify num-cores, disk-size-gb, and disk-iops") + updateClusterCmd.Flags().StringArray("region-info", []string{}, `[OPTIONAL] Region information for the cluster. Please provide key value pairs, region=,num-nodes=,vpc=,num-cores=,disk-size-gb=,disk-iops= as the value. If specified, region and num-nodes are mandatory, while num-cores, disk-size-gb, disk-iops, and vpc are optional.`) + } else { + updateClusterCmd.Flags().StringArray("region-info", []string{}, `[OPTIONAL] Region information for the cluster. Please provide key value pairs, region=,num-nodes=,vpc= as the value. If provided, region and num-nodes are mandatory, vpc is optional.`) + } updateClusterCmd.Flags().String("cluster-tier", "", "[OPTIONAL] The tier of the cluster. Sandbox or Dedicated.") updateClusterCmd.Flags().String("fault-tolerance", "", "[OPTIONAL] The fault tolerance domain of the cluster. The possible values are NONE, NODE, ZONE and REGION.") updateClusterCmd.Flags().String("database-version", "", "[OPTIONAL] The database version of the cluster. Production or Innovation or Preview.") @@ -216,27 +259,29 @@ func populateFlags(cmd *cobra.Command, originalSpec ybmclient.ClusterSpec, track if diskSizeGb, ok := originalSpec.ClusterInfo.NodeInfo.GetDiskSizeGbOk(); ok { nodeConfig += "disk-size-gb=" + strconv.Itoa(int(*diskSizeGb)) } - if diskIops, ok := originalSpec.ClusterInfo.NodeInfo.GetDiskIopsOk(); ok { - if diskIops != nil { - nodeConfig += "disk-iops=" + strconv.Itoa(int(*diskIops)) + if diskIops, ok := originalSpec.ClusterInfo.NodeInfo.GetDiskIopsOk(); ok && diskIops != nil { + if nodeConfig != "" { + nodeConfig += "," } + nodeConfig += "disk-iops=" + strconv.Itoa(int(*diskIops)) } if numCores, ok := originalSpec.ClusterInfo.NodeInfo.GetNumCoresOk(); ok { - nodeConfig += ",num-cores=" + strconv.Itoa(int(*numCores)) + if nodeConfig != "" { + nodeConfig += "," + } + nodeConfig += "num-cores=" + strconv.Itoa(int(*numCores)) } cmd.Flag("node-config").Value.Set(nodeConfig) cmd.Flag("node-config").Changed = true } - regionInfoList := "" - numRegions := len(originalSpec.ClusterRegionInfo) + regionInfoList := []string{} if !cmd.Flags().Changed("region-info") { - for index, clusterRegionInfo := range originalSpec.ClusterRegionInfo { + for _, clusterRegionInfo := range originalSpec.ClusterRegionInfo { regionInfo := "" if region, ok := clusterRegionInfo.PlacementInfo.CloudInfo.GetRegionOk(); ok && region != nil { regionInfo += "region=" + *region } - //logrus.Errorln(clusterRegionInfo.PlacementInfo.GetNumNodes()) if numNodes, ok := clusterRegionInfo.PlacementInfo.GetNumNodesOk(); ok && numNodes != nil { regionInfo += ",num-nodes=" + strconv.Itoa(int(*numNodes)) } @@ -249,12 +294,23 @@ func populateFlags(cmd *cobra.Command, originalSpec ybmclient.ClusterSpec, track } regionInfo += ",vpc=" + vpcName } - regionInfoList += regionInfo - if index < numRegions-1 { - regionInfoList += "|" + + if util.IsFeatureFlagEnabled(util.ASYMMETRIC_GEO) { + if nodeInfo, ok := clusterRegionInfo.GetNodeInfoOk(); ok { + if numCores, ok_ := nodeInfo.GetNumCoresOk(); ok_ { + regionInfo += ",num-cores=" + strconv.Itoa(int(*numCores)) + } + if diskSizeGb, ok_ := nodeInfo.GetDiskSizeGbOk(); ok_ { + regionInfo += ",disk-size-gb=" + strconv.Itoa(int(*diskSizeGb)) + } + if diskIops, ok_ := nodeInfo.GetDiskIopsOk(); ok_ && diskIops != nil { + regionInfo += ",disk-iops=" + strconv.Itoa(int(*diskIops)) + } + } } + regionInfoList = append(regionInfoList, regionInfo) } - cmd.Flag("region-info").Value.Set(regionInfoList) + cmd.Flags().Set("region-info", strings.Join(regionInfoList, " ")) cmd.Flag("region-info").Changed = true } diff --git a/cmd/util/feature_flags.go b/cmd/util/feature_flags.go index 40726796..84e63985 100644 --- a/cmd/util/feature_flags.go +++ b/cmd/util/feature_flags.go @@ -32,6 +32,7 @@ const ( AZURE_CIDR_ALLOWED FeatureFlag = "AZURE_CIDR_ALLOWED" ENTERPRISE_SECURITY FeatureFlag = "ENTERPRISE_SECURITY" INCREMENTAL_BACKUP FeatureFlag = "INCREMENTAL_BACKUP" + ASYMMETRIC_GEO FeatureFlag = "ASYMMETRIC_GEO" ) func (f FeatureFlag) String() string { diff --git a/go.mod b/go.mod index e117fd51..0768ff2f 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,11 @@ require ( github.com/spf13/cobra v1.8.0 github.com/spf13/viper v1.17.0 github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816 +<<<<<<< HEAD github.com/yugabyte/yugabytedb-managed-go-client-internal v0.0.0-20240123191212-6c0c5862cc02 +======= + github.com/yugabyte/yugabytedb-managed-go-client-internal v0.0.0-20240131113907-1ba9d5e67a7a +>>>>>>> ac6bb7f (Update cluster and describe cluster work) golang.org/x/exp v0.0.0-20230905200255-921286631fa9 golang.org/x/mod v0.14.0 golang.org/x/term v0.15.0 diff --git a/go.sum b/go.sum index fa1f1090..b07160c5 100644 --- a/go.sum +++ b/go.sum @@ -282,8 +282,13 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816 h1:J6v8awz+me+xeb/cUTotKgceAYouhIB3pjzgRd6IlGk= github.com/t-tomalak/logrus-easy-formatter v0.0.0-20190827215021-c074f06c5816/go.mod h1:tzym/CEb5jnFI+Q0k4Qq3+LvRF4gO3E2pxS8fHP8jcA= +<<<<<<< HEAD github.com/yugabyte/yugabytedb-managed-go-client-internal v0.0.0-20240123191212-6c0c5862cc02 h1:nE2LaAIy3Tsv9QRVqxwShsqQ2rIkXMGU8tsUHf52fAY= github.com/yugabyte/yugabytedb-managed-go-client-internal v0.0.0-20240123191212-6c0c5862cc02/go.mod h1:5vW0xIzIZw+1djkiWKx0qqNmqbRBSf4mjc4qw8lIMik= +======= +github.com/yugabyte/yugabytedb-managed-go-client-internal v0.0.0-20240131113907-1ba9d5e67a7a h1:Lj0hIO1LuXJ4jO0dEe8dIBX9X06CSvo6xNfdV8spJKs= +github.com/yugabyte/yugabytedb-managed-go-client-internal v0.0.0-20240131113907-1ba9d5e67a7a/go.mod h1:5vW0xIzIZw+1djkiWKx0qqNmqbRBSf4mjc4qw8lIMik= +>>>>>>> ac6bb7f (Update cluster and describe cluster work) github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/internal/client/client.go b/internal/client/client.go index 0d92f160..f7431d19 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -156,8 +156,11 @@ func (a *AuthApiClient) CreateClusterSpec(cmd *cobra.Command, regionInfoList []m var regionInfoProvided bool var err error + asymmetricGeoEnabled := util.IsFeatureFlagEnabled(util.ASYMMETRIC_GEO) + clusterRegionInfo := []ybmclient.ClusterRegionInfo{} totalNodes := 0 + regionNodeInfoMap := map[string]*ybmclient.OptionalClusterNodeInfo{} for _, regionInfo := range regionInfoList { numNodes, err := strconv.ParseInt(regionInfo["num-nodes"], 10, 32) if err != nil { @@ -191,6 +194,32 @@ func (a *AuthApiClient) CreateClusterSpec(cmd *cobra.Command, regionInfoList []m } info.SetIsDefault(false) clusterRegionInfo = append(clusterRegionInfo, info) + + if asymmetricGeoEnabled { + regionNodeInfo := ybmclient.NewOptionalClusterNodeInfo(0, 0, 0) + if numCores, ok := regionInfo["num-cores"]; ok { + i, err := strconv.Atoi(numCores) + if err != nil { + logrus.Fatalf("Unable to parse num-cores integer in %s", region) + } + regionNodeInfo.SetNumCores(int32(i)) + } + if diskSizeGb, ok := regionInfo["disk-size-gb"]; ok { + i, err := strconv.Atoi(diskSizeGb) + if err != nil { + logrus.Fatalf("Unable to parse disk-size-gb integer in %s", region) + } + regionNodeInfo.SetDiskSizeGb(int32(i)) + } + if diskIops, ok := regionInfo["disk-iops"]; ok { + i, err := strconv.Atoi(diskIops) + if err != nil { + logrus.Fatalf("Unable to parse disk-iops integer in %s", region) + } + regionNodeInfo.SetDiskIops(int32(i)) + } + regionNodeInfoMap[region] = regionNodeInfo + } } // This is to populate region in top level cloud info @@ -271,51 +300,112 @@ func (a *AuthApiClient) CreateClusterSpec(cmd *cobra.Command, regionInfoList []m } } clusterInfo.SetIsProduction(isProduction) - clusterInfo.SetNodeInfo(*ybmclient.NewClusterNodeInfoWithDefaults()) - - if cmd.Flags().Changed("node-config") { - nodeConfig, _ := cmd.Flags().GetStringToInt("node-config") - numCores := nodeConfig["num-cores"] - - clusterInfo.NodeInfo.SetNumCores(int32(numCores)) - - if diskSize, ok := nodeConfig["disk-size-gb"]; ok { - diskSizeGb = int32(diskSize) - } - - if diskIopsInt, ok := nodeConfig["disk-iops"]; ok { - diskIops = int32(diskIopsInt) - } + if cmd.Flags().Changed("cluster-type") { + clusterType, _ := cmd.Flags().GetString("cluster-type") + clusterInfo.SetClusterType(ybmclient.ClusterType(clusterType)) } cloud := string(cloudInfo.GetCode()) - region = cloudInfo.GetRegion() tier := string(clusterInfo.GetClusterTier()) - numCores := clusterInfo.NodeInfo.GetNumCores() - memoryMb, err = a.GetFromInstanceType("memory", cloud, tier, region, numCores) - if err != nil { - return nil, err - } - clusterInfo.NodeInfo.SetMemoryMb(memoryMb) + if asymmetricGeoEnabled && regionInfoProvided { + geoPartitioned := clusterInfo.GetClusterType() == "GEO_PARTITIONED" + clusterNodeInfoWithDefaults := *ybmclient.NewClusterNodeInfoWithDefaults() + // Create slice of desired regions. + regions := make([]string, 0, len(regionNodeInfoMap)) + for k, _ := range regionNodeInfoMap { + regions = append(regions, k) + } + // Grab available node configurations by region. + regionNodeConfigsMap := a.GetSupportedNodeConfigurationsV2(cloud, tier, regions, geoPartitioned) + // Create slice of region keys of node configurations response. + nodeConfigurationsRegions := make([]string, 0, len(regionNodeConfigsMap)) + for k, _ := range regionNodeConfigsMap { + nodeConfigurationsRegions = append(nodeConfigurationsRegions, k) + } + // For each desired region, grab appropriate node configuration and set node info. + for _, r := range regions { + nodeConfigs := []ybmclient.NodeConfigurationResponseItem{} + if slices.Contains(nodeConfigurationsRegions, r) { + nodeConfigs = regionNodeConfigsMap[r] + } else { + // Requested region not found in node configurations map. + // In this case, the map key is a string of all (comma-separated) regions, + // and the value is a list of node configurations that are available in all regions. + // So, we just use look through the first map value to find a node configuration to use. + nodeConfigs = regionNodeConfigsMap[nodeConfigurationsRegions[0]] + } + requestedNodeInfo := regionNodeInfoMap[r] + requestedNumCores := requestedNodeInfo.GetNumCores() + userProvidedNumCores := requestedNumCores != 0 - // Computing the default disk size if it is not provided - if diskSizeGb == 0 { - diskSizeGb, err = a.GetFromInstanceType("disk", cloud, tier, region, numCores) + var nodeConfig *ybmclient.NodeConfigurationResponseItem = nil + if !userProvidedNumCores { + requestedNumCores = clusterNodeInfoWithDefaults.GetNumCores() + } + for i, nc := range nodeConfigs { + if nc.GetNumCores() == requestedNumCores { + nodeConfig = &nodeConfigs[i] + break + } + } + if nodeConfig == nil { + logrus.Fatalf("No instance type found with %d cores in region %s\n", requestedNumCores, r) + } + regionNodeInfoMap[r].SetNumCores(nodeConfig.GetNumCores()) + regionNodeInfoMap[r].SetMemoryMb(nodeConfig.GetMemoryMb()) + if requestedNodeInfo.GetDiskSizeGb() == 0 { + // User did not specify a disk size. Default to included disk size. + regionNodeInfoMap[r].SetDiskSizeGb(nodeConfig.GetIncludedDiskSizeGb()) + } + } + // Set per-region node info and cluster node info. + var currRegionNodeInfo *ybmclient.OptionalClusterNodeInfo = nil + for i, regionInfo := range clusterRegionInfo { + r := regionInfo.GetPlacementInfo().CloudInfo.Region + clusterRegionInfo[i].SetNodeInfo(*regionNodeInfoMap[r]) + logrus.Infof("region=%s, node-info=%v\n", r, clusterRegionInfo[i].GetNodeInfo()) + if currRegionNodeInfo != nil && !geoPartitioned && *currRegionNodeInfo != clusterRegionInfo[i].GetNodeInfo() { + // Asymmetric node configurations are only allowed for geo-partitioned clusters. + logrus.Fatalln("Synchronous cluster regions must have identical node configurations") + } + currRegionNodeInfo = (&clusterRegionInfo[i]).NodeInfo.Get() + } + clusterInfo.SetNodeInfo(ToClusterNodeInfo(regionNodeInfoMap[regions[0]])) + } else { + clusterInfo.SetNodeInfo(*ybmclient.NewClusterNodeInfoWithDefaults()) + if cmd.Flags().Changed("node-config") { + nodeConfig, _ := cmd.Flags().GetStringToInt("node-config") + numCores := nodeConfig["num-cores"] + clusterInfo.NodeInfo.SetNumCores(int32(numCores)) + if diskSize, ok := nodeConfig["disk-size-gb"]; ok { + diskSizeGb = int32(diskSize) + } + if diskIopsInt, ok := nodeConfig["disk-iops"]; ok { + diskIops = int32(diskIopsInt) + } + } + region = cloudInfo.GetRegion() + numCores := clusterInfo.NodeInfo.GetNumCores() + memoryMb, err = a.GetFromInstanceType("memory", cloud, tier, region, numCores) if err != nil { return nil, err } - } - clusterInfo.NodeInfo.SetDiskSizeGb(diskSizeGb) + clusterInfo.NodeInfo.SetMemoryMb(memoryMb) - if diskIops > 0 { - clusterInfo.NodeInfo.SetDiskIops(diskIops) - } + // Computing the default disk size if it is not provided + if diskSizeGb == 0 { + diskSizeGb, err = a.GetFromInstanceType("disk", cloud, tier, region, numCores) + if err != nil { + return nil, err + } + } + clusterInfo.NodeInfo.SetDiskSizeGb(diskSizeGb) - if cmd.Flags().Changed("cluster-type") { - clusterType, _ := cmd.Flags().GetString("cluster-type") - clusterInfo.SetClusterType(ybmclient.ClusterType(clusterType)) + if diskIops > 0 { + clusterInfo.NodeInfo.SetDiskIops(diskIops) + } } if cmd.Flags().Changed("default-region") { @@ -358,6 +448,17 @@ func (a *AuthApiClient) CreateClusterSpec(cmd *cobra.Command, regionInfoList []m return clusterSpec, nil } +func ToClusterNodeInfo(opt *ybmclient.OptionalClusterNodeInfo) ybmclient.ClusterNodeInfo { + clusterNodeInfo := *ybmclient.NewClusterNodeInfoWithDefaults() + clusterNodeInfo.SetNumCores(opt.GetNumCores()) + clusterNodeInfo.SetMemoryMb(opt.GetMemoryMb()) + clusterNodeInfo.SetDiskSizeGb(opt.GetDiskSizeGb()) + if iops, _ := opt.GetDiskIopsOk(); iops != nil { + clusterNodeInfo.SetDiskIops(*iops) + } + return clusterNodeInfo +} + func (a *AuthApiClient) GetInfo(providedAccountID string, providedProjectID string) { var err error a.AccountID, err = a.GetAccountID(providedAccountID) @@ -809,6 +910,25 @@ func (a *AuthApiClient) GetSupportedNodeConfigurations(cloud string, tier string return a.ApiClient.ClusterApi.GetSupportedNodeConfigurations(a.ctx).AccountId(a.AccountID).Cloud(cloud).Tier(tier).Regions([]string{region}) } +func (a *AuthApiClient) GetSupportedNodeConfigurationsV2(cloud string, tier string, regions []string, geoPartitioned bool) map[string][]ybmclient.NodeConfigurationResponseItem { + // For single-region clusters, set isMultiRegion = false. + // For azure clusters, set isMultiRegion = false. + // For geo clusters if FF is enabled, set isMultiRegion = false + // For all other clusters, set isMultiRegion = true + isMultiRegion := true + asymmetricGeoEnabled := util.IsFeatureFlagEnabled(util.ASYMMETRIC_GEO) + if len(regions) == 1 || cloud == "AZURE" || (geoPartitioned && asymmetricGeoEnabled) { + isMultiRegion = false + } + instanceResp, resp, err := a.ApiClient.ClusterApi.GetSupportedNodeConfigurations(a.ctx).AccountId(a.AccountID).Cloud(cloud).Tier(tier).Regions(regions).IsMultiRegion(isMultiRegion).Execute() + if err != nil { + b, _ := httputil.DumpResponse(resp, true) + logrus.Debug(b) + logrus.Fatalln(err) + } + return instanceResp.GetData() +} + func (a *AuthApiClient) GetFromInstanceType(resource string, cloud string, tier string, region string, numCores int32) (int32, error) { instanceResp, resp, err := a.GetSupportedNodeConfigurations(cloud, tier, region).Execute() if err != nil { @@ -821,9 +941,7 @@ func (a *AuthApiClient) GetFromInstanceType(resource string, cloud string, tier if !ok || len(nodeConfigList) == 0 { return 0, fmt.Errorf("no instances configured for the given region") } - return getFromNodeConfig(resource, numCores, nodeConfigList) - } func (a *AuthApiClient) CreateCdcSink() ybmclient.ApiCreateCdcSinkRequest { diff --git a/internal/formatter/clusters_full.go b/internal/formatter/clusters_full.go index 150a3958..55dbc12e 100644 --- a/internal/formatter/clusters_full.go +++ b/internal/formatter/clusters_full.go @@ -297,15 +297,33 @@ func (c *clusterInfoRegionsContext) NumNode() string { } func (c *clusterInfoRegionsContext) NumCores() string { - return fmt.Sprintf("%d", c.clusterInfo.NodeInfo.NumCores) + numCores := c.clusterInfo.NodeInfo.NumCores + if c.clusterInfoRegion.NodeInfo.IsSet() { + if nc, ok := c.clusterInfoRegion.NodeInfo.Get().GetNumCoresOk(); ok { + numCores = *nc + } + } + return fmt.Sprintf("%d", numCores) } func (c *clusterInfoRegionsContext) MemoryGb() string { - return convertMbtoGb(c.clusterInfo.NodeInfo.MemoryMb) + memoryMb := c.clusterInfo.NodeInfo.MemoryMb + if c.clusterInfoRegion.NodeInfo.IsSet() { + if mem, ok := c.clusterInfoRegion.NodeInfo.Get().GetMemoryMbOk(); ok { + memoryMb = *mem + } + } + return convertMbtoGb(memoryMb) } func (c *clusterInfoRegionsContext) DiskSizeGb() string { - return fmt.Sprintf("%dGB", c.clusterInfo.NodeInfo.DiskSizeGb) + diskSizeGb := c.clusterInfo.NodeInfo.DiskSizeGb + if c.clusterInfoRegion.NodeInfo.IsSet() { + if ds, ok := c.clusterInfoRegion.NodeInfo.Get().GetDiskSizeGbOk(); ok { + diskSizeGb = *ds + } + } + return fmt.Sprintf("%dGB", diskSizeGb) } func (c *clusterInfoRegionsContext) Region() string { diff --git a/internal/formatter/instance_types.go b/internal/formatter/instance_types.go index 16d7d2a2..62c06602 100644 --- a/internal/formatter/instance_types.go +++ b/internal/formatter/instance_types.go @@ -23,12 +23,12 @@ import ( ) const ( - defaulInstanceTypeListing = "table {{.Cores}}\t{{.Memory}}\t{{.DiskSize}}\t{{.AZs}}\t{{.IsEnabled}}" - coresHeader = "Number of Cores" - memoryHeader = "Memory (MB)" - diskSizeHeader = "Disk Size (GB)" - azsHeader = "Number of Availability Zones" - isEnabledHeader = "Is Enabled" + defaultInstanceTypeListing = "table {{.Cores}}\t{{.Memory}}\t{{.DiskSize}}\t{{.AZs}}\t{{.IsEnabled}}" + coresHeader = "Number of Cores" + memoryHeader = "Memory (MB)" + diskSizeHeader = "Disk Size (GB)" + azsHeader = "Number of Availability Zones" + isEnabledHeader = "Is Enabled" ) type InstanceTypeContext struct { @@ -40,7 +40,7 @@ type InstanceTypeContext struct { func NewInstanceTypeFormat(source string) Format { switch source { case "table", "": - format := defaulInstanceTypeListing + format := defaultInstanceTypeListing return Format(format) default: // custom format or json or pretty return Format(source)