Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions cmd/cluster/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import (
"github.com/yugabyte/ybm-cli/cmd/cluster/node"
pitrconfig "github.com/yugabyte/ybm-cli/cmd/cluster/pitr-config"
readreplica "github.com/yugabyte/ybm-cli/cmd/cluster/read-replica"
"github.com/yugabyte/ybm-cli/cmd/util"
)

// getCmd represents the list command
Expand Down Expand Up @@ -75,7 +74,7 @@ func init() {
pitrconfig.PitrConfigCmd.PersistentFlags().StringVarP(&pitrconfig.ClusterName, "cluster-name", "c", "", "[REQUIRED] The name of the cluster.")
pitrconfig.PitrConfigCmd.MarkPersistentFlagRequired("cluster-name")

util.AddCommandIfFeatureFlag(ClusterCmd, connectionpooling.ConnectionPoolingCmd, util.CONNECTION_POOLING)
ClusterCmd.AddCommand(connectionpooling.ConnectionPoolingCmd)
connectionpooling.ConnectionPoolingCmd.PersistentFlags().StringVarP(&connectionpooling.ClusterName, "cluster-name", "c", "", "[REQUIRED] The name of the cluster.")
connectionpooling.ConnectionPoolingCmd.MarkPersistentFlagRequired("cluster-name")
}
19 changes: 9 additions & 10 deletions cmd/cluster/connection-pooling/connection_pooling.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ 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"
Expand All @@ -31,26 +30,26 @@ var ClusterName string

var ConnectionPoolingCmd = &cobra.Command{
Use: "connection-pooling",
Short: "Manage Connection Pooling",
Long: "Manage Connection Pooling",
Short: "Manage Connection Pooling for a cluster",
Long: "Manage Connection Pooling for a cluster",
Run: func(cmd *cobra.Command, args []string) {
cmd.Help()
},
}

var enableConnectionPoolingCmd = &cobra.Command{
Use: "enable",
Short: "Enable Connection Pooling",
Long: "Enable Connection Pooling",
Short: "Enable Connection Pooling for a cluster",
Long: "Enable Connection Pooling for a cluster",
Run: func(cmd *cobra.Command, args []string) {
performConnectionPoolingOperation("enable", cmd, args)
},
}

var disableConnectionPoolingCmd = &cobra.Command{
Use: "disable",
Short: "Disable Connection Pooling",
Long: "Disable Connection Pooling",
Short: "Disable Connection Pooling for a cluster",
Long: "Disable Connection Pooling for a cluster",
Run: func(cmd *cobra.Command, args []string) {
performConnectionPoolingOperation("disable", cmd, args)
},
Expand Down Expand Up @@ -103,7 +102,7 @@ func performConnectionPoolingOperation(operationName string, cmd *cobra.Command,
}

func init() {
util.AddCommandIfFeatureFlag(ConnectionPoolingCmd, enableConnectionPoolingCmd, util.CONNECTION_POOLING)

util.AddCommandIfFeatureFlag(ConnectionPoolingCmd, disableConnectionPoolingCmd, util.CONNECTION_POOLING)
ConnectionPoolingCmd.AddCommand()
ConnectionPoolingCmd.AddCommand(enableConnectionPoolingCmd)
ConnectionPoolingCmd.AddCommand(disableConnectionPoolingCmd)
}
6 changes: 1 addition & 5 deletions cmd/cluster/create_cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,14 +132,10 @@ var createClusterCmd = &cobra.Command{
// Enable connection pooling feature if requested
enableConnectionPooling, _ := cmd.Flags().GetBool("enable-connection-pooling")
if enableConnectionPooling {
if !util.IsFeatureFlagEnabled(util.CONNECTION_POOLING) {
logrus.Fatalf("Connection pooling feature is not enabled yet — it will be available soon.")
}
// Set connection pooling in features array for cluster creation
features := []ybmclient.CreateClusterFeatureEnum{ybmclient.CREATECLUSTERFEATUREENUM_ENABLE_CONNECTION_POOLING}
createClusterRequest.SetFeatures(features)
logrus.Debugf("Features array set to: %v", features)
logrus.Info("Connection pooling will be enabled during cluster creation")
}

if cmkSpec != nil {
Expand Down Expand Up @@ -237,5 +233,5 @@ func init() {
createClusterCmd.MarkFlagRequired("region-info")
createClusterCmd.Flags().String("preferred-region", "", "[OPTIONAL] The preferred region in a multi region cluster. The preferred region handles all read and write requests from clients.")
createClusterCmd.Flags().String("default-region", "", "[OPTIONAL] The primary region in a partition-by-region cluster. The primary region is where all the tables not created in a tablespace reside.")
createClusterCmd.Flags().Bool("enable-connection-pooling", false, "[OPTIONAL] Enable connection pooling for the cluster after creation. Default false.")
createClusterCmd.Flags().Bool("enable-connection-pooling", false, "[OPTIONAL] Enable connection pooling for the cluster during creation. Default false.")
}
111 changes: 4 additions & 107 deletions cmd/cluster_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ var _ = Describe("Cluster", func() {
Expect(err).ToNot(HaveOccurred())
os.Setenv("YBM_HOST", fmt.Sprintf("http://%s", server.Addr()))
os.Setenv("YBM_APIKEY", "test-token")
os.Setenv("YBM_FF_CONNECTION_POOLING", "true")
})

Describe("Pausing cluster", func() {
Expand Down Expand Up @@ -208,20 +207,18 @@ var _ = Describe("Cluster", func() {
})
Context("with a valid Api token and default output table", func() {
It("should return list of cluster", func() {
os.Setenv("YBM_FF_CONNECTION_POOLING", "true")
cmd := exec.Command(compiledCLIPath, "cluster", "list")
session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter)
Expect(err).NotTo(HaveOccurred())
session.Wait(2)
o := string(session.Out.Contents()[:])
expected := `Name Tier Version State Health Provider Regions Nodes Node Res.(Vcpu/Mem/DiskGB/IOPS) Connection Pooling Enabled
stunning-sole Dedicated 2.16.0.1-b7 ACTIVE 💚 AWS us-west-2 1 2 / 8GB / 100GB / - false` + "\n"
expected := `Name Tier Version State Health Provider Regions Nodes Node Res.(Vcpu/Mem/DiskGB/IOPS) Connection Pooling
stunning-sole Dedicated 2.16.0.1-b7 ACTIVE 💚 AWS us-west-2 1 2 / 8GB / 100GB / - ` + "\n"
Expect(o).Should(Equal(expected))
session.Kill()
})

It("should return detailed summary of cluster if cluster-name is specified", func() {
os.Setenv("YBM_FF_CONNECTION_POOLING", "true")
statusCode = 200
err := loadJson("./test/fixtures/allow-list.json", &responseNetworkAllowList)
Expect(err).ToNot(HaveOccurred())
Expand Down Expand Up @@ -257,8 +254,8 @@ stunning-sole Dedicated 2.16.0.1-b7 ACTIVE 💚 AWS us-we
Name ID Version State Health
stunning-sole 5f80730f-ba3f-4f7e-8c01-f8fa4c90dad8 2.16.0.1-b7 ACTIVE 💚

Provider Tier Fault Tolerance Nodes Node Res.(Vcpu/Mem/DiskGB/IOPS) Connection Pooling Enabled
AWS Dedicated NONE, RF 1 1 2 / 8GB / 100GB / - false
Provider Tier Fault Tolerance Nodes Node Res.(Vcpu/Mem/DiskGB/IOPS) Connection Pooling
AWS Dedicated NONE, RF 1 1 2 / 8GB / 100GB / -


Regions
Expand Down Expand Up @@ -313,106 +310,6 @@ test-cli-2-n3 us-west-2[us-west-2c] 💚 ❌ ✅ ❌
})

Describe("Creating cluster with connection pooling", func() {
// Note: We don't need a BeforeEach here because cluster creation doesn't start with a list clusters call
// The base server setup in newGhttpServer already handles the initial account/project calls
Context("when connection pooling flag is provided", func() {
It("should include ENABLE_CONNECTION_POOLING in features array", func() {
statusCode = 200
err := loadJson("./test/fixtures/create-cluster-with-cp.json", &responseCluster)
Expect(err).ToNot(HaveOccurred())

// Capture the request body to verify features array
var capturedRequestBody string
server.AppendHandlers(
// First, the CLI validates supported node configurations
ghttp.CombineHandlers(
ghttp.VerifyRequest(http.MethodGet, "/api/public/v1/accounts/340af43a-8a7c-4659-9258-4876fd6a207b/clusters/supported-node-configurations"),
ghttp.RespondWith(http.StatusOK, `{"data": [{"cloud": "GCP", "tier": "PAID", "regions": [{"name": "asia-south1"}], "node_configurations": [{"num_cores": 2, "memory_mb": 8192, "disk_size_gb": 50}]}]}`),
),
// Then the POST request for cluster creation
ghttp.CombineHandlers(
ghttp.VerifyRequest(http.MethodPost, "/api/public/v1/accounts/340af43a-8a7c-4659-9258-4876fd6a207b/projects/78d4459c-0f45-47a5-899a-45ddf43eba6e/clusters"),
ghttp.VerifyContentType("application/json"),
func(w http.ResponseWriter, req *http.Request) {
// Read and capture the request body
buf := make([]byte, req.ContentLength)
req.Body.Read(buf)
capturedRequestBody = string(buf)
},
ghttp.RespondWithJSONEncodedPtr(&statusCode, responseCluster),
),
)

cmd := exec.Command(compiledCLIPath, "cluster", "create",
"--cluster-name", "test-cp-cluster",
"--credentials", "username=admin,password=Secret123",
"--region-info", "region=asia-south1,num-nodes=1,num-cores=2,disk-size-gb=50",
"--cloud-provider", "GCP",
"--cluster-tier", "Dedicated",
"--enable-connection-pooling")

session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter)
Expect(err).NotTo(HaveOccurred())
session.Wait(5) // Allow time for cluster creation

// In test environment, the command may fail due to mock server limitations
// But we can verify that the CLI started and tried to run (exit code != 0 due to API errors is expected)
// The important thing is that the feature flag was enabled, so the CLI tried to proceed

if len(capturedRequestBody) > 0 {
// If we captured a request body, verify it contains connection pooling features
Expect(capturedRequestBody).Should(ContainSubstring("ENABLE_CONNECTION_POOLING"))
Expect(capturedRequestBody).Should(ContainSubstring("features"))
} else {
// In test environment, CLI may fail with API errors, but that's expected
// The fact that it attempted to run means our feature flag validation passed
output := string(session.Out.Contents())
errorOutput := string(session.Err.Contents())

// Either the command succeeded, or it failed due to API issues (both are acceptable)
hasAttemptedToRun := len(output) > 0 || len(errorOutput) > 0
Expect(hasAttemptedToRun).To(BeTrue(), "CLI should have attempted to run")
}

session.Kill()
})

It("should fail when feature flag is disabled", func() {
// Temporarily disable the feature flag
os.Setenv("YBM_FF_CONNECTION_POOLING", "false")
defer os.Setenv("YBM_FF_CONNECTION_POOLING", "true")

// Add handler for the GET request the CLI makes before attempting to create cluster
server.AppendHandlers(
ghttp.CombineHandlers(
ghttp.VerifyRequest(http.MethodGet, "/api/public/v1/accounts/340af43a-8a7c-4659-9258-4876fd6a207b/clusters/supported-node-configurations"),
ghttp.RespondWith(http.StatusOK, `{"data": [{"cloud": "GCP", "tier": "PAID", "regions": [{"name": "asia-south1"}], "node_configurations": [{"num_cores": 2, "memory_mb": 8192, "disk_size_gb": 50}]}]}`),
),
)

cmd := exec.Command(compiledCLIPath, "cluster", "create",
"--cluster-name", "test-cp-cluster-fail",
"--credentials", "username=admin,password=Secret123",
"--region-info", "region=asia-south1,num-nodes=1,num-cores=2,disk-size-gb=50",
"--cloud-provider", "GCP",
"--cluster-tier", "Dedicated",
"--enable-connection-pooling")

session, err := gexec.Start(cmd, GinkgoWriter, GinkgoWriter)
Expect(err).NotTo(HaveOccurred())
session.Wait(5)

// Verify that the command fails (feature flag should prevent execution)
// We expect either a feature flag error or any other error indicating the CLI attempted to run
output := string(session.Err.Contents())

// The important thing is that the command failed when feature flag is disabled
Expect(session.ExitCode()).ToNot(Equal(0), "Command should have failed when feature flag disabled. Error output: %s", output)

session.Kill()
})
})

Context("when creating cluster with connection pooling enabled", func() {
It("should successfully create cluster with connection pooling feature", func() {
statusCode = 200
Expand Down
1 change: 0 additions & 1 deletion cmd/connection_pooling_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ var _ = Describe("Connection Pooling", func() {
Expect(err).ToNot(HaveOccurred())
os.Setenv("YBM_HOST", fmt.Sprintf("http://%s", server.Addr()))
os.Setenv("YBM_APIKEY", "test-token")
os.Setenv("YBM_FF_CONNECTION_POOLING", "true")
statusCode = 200
err = loadJson("./test/fixtures/list-clusters.json", &responseListClusters)
Expect(err).ToNot(HaveOccurred())
Expand Down
1 change: 0 additions & 1 deletion cmd/util/feature_flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ const (
AZURE_CIDR_ALLOWED FeatureFlag = "AZURE_CIDR_ALLOWED"
ENTERPRISE_SECURITY FeatureFlag = "ENTERPRISE_SECURITY"
PITR_RESTORE FeatureFlag = "PITR_RESTORE"
CONNECTION_POOLING FeatureFlag = "CONNECTION_POOLING"
DR FeatureFlag = "DR"
)

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ 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
github.com/yugabyte/yugabytedb-managed-go-client-internal v0.0.0-20250809052019-ecd3dfbae333
github.com/yugabyte/yugabytedb-managed-go-client-internal v0.0.0-20250818090100-de6585242c8e
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
golang.org/x/mod v0.20.0
golang.org/x/term v0.25.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -282,8 +282,8 @@ 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=
github.com/yugabyte/yugabytedb-managed-go-client-internal v0.0.0-20250809052019-ecd3dfbae333 h1:h6QDVQtff2itBwi1TXbLyzI/r09FgBuLrmEyHiJ1C9Q=
github.com/yugabyte/yugabytedb-managed-go-client-internal v0.0.0-20250809052019-ecd3dfbae333/go.mod h1:5vW0xIzIZw+1djkiWKx0qqNmqbRBSf4mjc4qw8lIMik=
github.com/yugabyte/yugabytedb-managed-go-client-internal v0.0.0-20250818090100-de6585242c8e h1:DAwXWlzWoG8iqdzm1QuKP4+yJki4A3ML1qC9CoK7Maw=
github.com/yugabyte/yugabytedb-managed-go-client-internal v0.0.0-20250818090100-de6585242c8e/go.mod h1:5vW0xIzIZw+1djkiWKx0qqNmqbRBSf4mjc4qw8lIMik=
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=
Expand Down
53 changes: 20 additions & 33 deletions internal/formatter/clusters.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,18 @@ import (
"github.com/enescakir/emoji"
"github.com/inhies/go-bytesize"
"github.com/sirupsen/logrus"
"github.com/yugabyte/ybm-cli/cmd/util"
ybmclient "github.com/yugabyte/yugabytedb-managed-go-client-internal"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
)

const (
defaultClusterListing = "table {{.Name}}\t{{.Tier}}\t{{.SoftwareVersion}}\t{{.State}}\t{{.HealthState}}\t{{.Provider}}\t{{.Regions}}\t{{.Nodes}}\t{{.NodesSpec}}"
defaultClusterListingV2 = "table {{.Name}}\t{{.Tier}}\t{{.SoftwareVersion}}\t{{.State}}\t{{.HealthState}}\t{{.Provider}}\t{{.Regions}}\t{{.Nodes}}\t{{.NodesSpec}}\t{{.ConnectionPoolingStatus}}"
defaultClusterListing = "table {{.Name}}\t{{.Tier}}\t{{.SoftwareVersion}}\t{{.State}}\t{{.HealthState}}\t{{.Provider}}\t{{.Regions}}\t{{.Nodes}}\t{{.NodesSpec}}\t{{.ConnectionPoolingStatus}}"
numNodesHeader = "Nodes"
nodeInfoHeader = "Node Res.(Vcpu/Mem/DiskGB/IOPS)"
healthStateHeader = "Health"
tierHeader = "Tier"
connectionPoolingHeader = "Connection Pooling Enabled"
connectionPoolingHeader = "Connection Pooling"
)

type ClusterContext struct {
Expand All @@ -52,9 +50,6 @@ func NewClusterFormat(source string) Format {
switch source {
case "table", "":
format := defaultClusterListing
if util.IsFeatureFlagEnabled(util.CONNECTION_POOLING) {
format = defaultClusterListingV2
}
return Format(format)
default: // custom format or json or pretty
return Format(source)
Expand All @@ -76,34 +71,11 @@ func ClusterWrite(ctx Context, clusters []ybmclient.ClusterData) error {

clusterContext := NewClusterContext()

if util.IsFeatureFlagEnabled(util.CONNECTION_POOLING) {
clusterContext = NewClusterContextV2()
}

return ctx.Write(clusterContext, render)
}

// NewClusterContext creates a new context for rendering cluster
func NewClusterContext() *ClusterContext {
clusterCtx := ClusterContext{}
clusterCtx.Header = SubHeaderContext{
"Name": nameHeader,
"ID": "ID",
"Regions": regionsHeader,
"Nodes": numNodesHeader,
"NodesSpec": nodeInfoHeader,
"SoftwareVersion": softwareVersionHeader,
"State": stateHeader,
"HealthState": healthStateHeader,
"Provider": providerHeader,
"FaultTolerance": faultToleranceHeader,
"DataDistribution": dataDistributionHeader,
"Tier": tierHeader,
}
return &clusterCtx
}

func NewClusterContextV2() *ClusterContext {
clusterCtx := ClusterContext{}
clusterCtx.Header = SubHeaderContext{
"Name": nameHeader,
Expand Down Expand Up @@ -162,11 +134,26 @@ func (c *ClusterContext) SoftwareVersion() string {
return ""
}

func (c *ClusterContext) ConnectionPoolingStatus() bool {
func (c *ClusterContext) ConnectionPoolingStatus() string {
if v, ok := c.c.Info.GetIsConnectionPoolingEnabledOk(); ok {
return *v
return ConnectionPoolingStatusToEmoji(*v)
}
return ConnectionPoolingStatusToEmoji(false)
}

func ConnectionPoolingStatusToEmoji(cpStatus bool) string {
// Windows terminal do not support emoji So we return directly the connection pooling state
if runtime.GOOS == "windows" {
return fmt.Sprintf("%t", cpStatus)
}
switch cpStatus {
case true:
return emoji.Parse(":white_check_mark:")
case false:
return emoji.CrossMark.String()
default:
return fmt.Sprintf("%t", cpStatus)
}
return false
}

func (c *ClusterContext) HealthState() string {
Expand Down
Loading