diff --git a/cmd/pgcreate.go b/cmd/pgcreate.go index 8dc2b00..e690a1e 100644 --- a/cmd/pgcreate.go +++ b/cmd/pgcreate.go @@ -65,6 +65,7 @@ printed as text. Use --output json, yaml, or text for non-interactive output.`, cmd.Flags().Int("disk-size-gb", 0, "Set the disk size in GB. Must be 1 or a multiple of 5. Server picks a sensible default based on compute size if unset.") cmd.Flags().Bool("disk-autoscaling", false, "Enable disk autoscaling") + cmd.Flags().String("connection-pool", "none", "Set connection pool to 'none' or 'pgbouncer'") cmd.Flags().Bool("high-availability", false, "Enable high availability (Pro plans and above)") cmd.Flags().String("datadog-api-key", "", "Set the Datadog API key for monitoring") diff --git a/cmd/pgcreate_test.go b/cmd/pgcreate_test.go index 00b882d..439b550 100644 --- a/cmd/pgcreate_test.go +++ b/cmd/pgcreate_test.go @@ -47,6 +47,7 @@ func TestPGCreate_AllFlags(t *testing.T) { "--database-user", "metrics_user", "--disk-size-gb", "105", "--disk-autoscaling", + "--connection-pool", "pgbouncer", "--high-availability", "--datadog-api-key", "dd-key", "--datadog-site", "US3", @@ -67,6 +68,7 @@ func TestPGCreate_AllFlags(t *testing.T) { require.NotNil(t, pg.DiskSizeGB) assert.Equal(t, 105, *pg.DiskSizeGB) assert.True(t, pg.DiskAutoscalingEnabled) + assert.Equal(t, "pgbouncer", pg.ConnectionPool) assert.True(t, pg.HighAvailabilityEnabled) require.Len(t, pg.IpAllowList, 1) assert.Equal(t, "10.0.0.0/8", pg.IpAllowList[0].CidrBlock) diff --git a/cmd/pgupdate.go b/cmd/pgupdate.go index e493657..9a60245 100644 --- a/cmd/pgupdate.go +++ b/cmd/pgupdate.go @@ -75,6 +75,7 @@ mutually exclusive.`, cmd.Flags().Int("disk-size-gb", 0, "Set the disk size in GB. Must be 1 or a multiple of 5.") cmd.Flags().Bool("disk-autoscaling", false, "Enable disk autoscaling. Pass --disk-autoscaling=false to disable.") + cmd.Flags().String("connection-pool", "none", "Set connection pool to 'none' or 'pgbouncer'") cmd.Flags().Bool("high-availability", false, "Enable high availability (Pro plans and above). Pass --high-availability=false to disable.") cmd.Flags().String("datadog-api-key", "", "Set the Datadog API key for monitoring. Pass an empty string to remove.") diff --git a/internal/fakes/renderapi/server.go b/internal/fakes/renderapi/server.go index 1edc976..2a9dc21 100644 --- a/internal/fakes/renderapi/server.go +++ b/internal/fakes/renderapi/server.go @@ -244,6 +244,7 @@ func postgresListItem(pg *client.PostgresDetail) client.Postgres { DatabaseName: pg.DatabaseName, DatabaseUser: pg.DatabaseUser, DiskAutoscalingEnabled: pg.DiskAutoscalingEnabled, + ConnectionPool: pg.ConnectionPool, DiskSizeGB: pg.DiskSizeGB, EnvironmentId: pg.EnvironmentId, ExpiresAt: pg.ExpiresAt, @@ -853,6 +854,7 @@ func NewServer(t *testing.T) *Server { DatabaseUser: databaseUser, DiskSizeGB: body.DiskSizeGB, DiskAutoscalingEnabled: pointers.ValueOrDefault(body.EnableDiskAutoscaling, false), + ConnectionPool: pointers.ValueOrDefault(body.ConnectionPool, "none"), HighAvailabilityEnabled: pointers.ValueOrDefault(body.EnableHighAvailability, false), EnvironmentId: body.EnvironmentId, IpAllowList: ipAllowList, @@ -991,6 +993,9 @@ func NewServer(t *testing.T) *Server { if body.EnableDiskAutoscaling != nil { pg.DiskAutoscalingEnabled = *body.EnableDiskAutoscaling } + if body.ConnectionPool != nil { + pg.ConnectionPool = *body.ConnectionPool + } if body.EnableHighAvailability != nil { pg.HighAvailabilityEnabled = *body.EnableHighAvailability } diff --git a/pkg/client/events/events_gen.go b/pkg/client/events/events_gen.go index f8036a2..9fc126c 100644 --- a/pkg/client/events/events_gen.go +++ b/pkg/client/events/events_gen.go @@ -355,6 +355,23 @@ type PostgresClusterLeaderChangedEvent struct { LeaderId *string `json:"leaderId,omitempty"` } +// PostgresConnectionPoolChangedEvent defines model for postgresConnectionPoolChangedEvent. +type PostgresConnectionPoolChangedEvent struct { + From string `json:"from"` + To string `json:"to"` + + // User User who triggered the action + User *User `json:"user,omitempty"` +} + +// PostgresConnectionPoolEnabledChangedEvent defines model for postgresConnectionPoolEnabledChangedEvent. +type PostgresConnectionPoolEnabledChangedEvent struct { + Enabled bool `json:"enabled"` + + // User User who triggered the action + User *User `json:"user,omitempty"` +} + // PostgresCreatedEvent defines model for postgresCreatedEvent. type PostgresCreatedEvent struct { // User User who triggered the action @@ -818,6 +835,58 @@ func (t *PostgresEventDetails) MergePostgresClusterLeaderChangedEvent(v Postgres return err } +// AsPostgresConnectionPoolChangedEvent returns the union data inside the PostgresEventDetails as a PostgresConnectionPoolChangedEvent +func (t PostgresEventDetails) AsPostgresConnectionPoolChangedEvent() (PostgresConnectionPoolChangedEvent, error) { + var body PostgresConnectionPoolChangedEvent + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromPostgresConnectionPoolChangedEvent overwrites any union data inside the PostgresEventDetails as the provided PostgresConnectionPoolChangedEvent +func (t *PostgresEventDetails) FromPostgresConnectionPoolChangedEvent(v PostgresConnectionPoolChangedEvent) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergePostgresConnectionPoolChangedEvent performs a merge with any union data inside the PostgresEventDetails, using the provided PostgresConnectionPoolChangedEvent +func (t *PostgresEventDetails) MergePostgresConnectionPoolChangedEvent(v PostgresConnectionPoolChangedEvent) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + +// AsPostgresConnectionPoolEnabledChangedEvent returns the union data inside the PostgresEventDetails as a PostgresConnectionPoolEnabledChangedEvent +func (t PostgresEventDetails) AsPostgresConnectionPoolEnabledChangedEvent() (PostgresConnectionPoolEnabledChangedEvent, error) { + var body PostgresConnectionPoolEnabledChangedEvent + err := json.Unmarshal(t.union, &body) + return body, err +} + +// FromPostgresConnectionPoolEnabledChangedEvent overwrites any union data inside the PostgresEventDetails as the provided PostgresConnectionPoolEnabledChangedEvent +func (t *PostgresEventDetails) FromPostgresConnectionPoolEnabledChangedEvent(v PostgresConnectionPoolEnabledChangedEvent) error { + b, err := json.Marshal(v) + t.union = b + return err +} + +// MergePostgresConnectionPoolEnabledChangedEvent performs a merge with any union data inside the PostgresEventDetails, using the provided PostgresConnectionPoolEnabledChangedEvent +func (t *PostgresEventDetails) MergePostgresConnectionPoolEnabledChangedEvent(v PostgresConnectionPoolEnabledChangedEvent) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + merged, err := runtime.JSONMerge(t.union, b) + t.union = merged + return err +} + // AsPostgresCreatedEvent returns the union data inside the PostgresEventDetails as a PostgresCreatedEvent func (t PostgresEventDetails) AsPostgresCreatedEvent() (PostgresCreatedEvent, error) { var body PostgresCreatedEvent diff --git a/pkg/client/eventtypes/eventtypes_gen.go b/pkg/client/eventtypes/eventtypes_gen.go index cda50ee..4701013 100644 --- a/pkg/client/eventtypes/eventtypes_gen.go +++ b/pkg/client/eventtypes/eventtypes_gen.go @@ -44,6 +44,8 @@ const ( EventTypePostgresBackupFailed EventType = "postgres_backup_failed" EventTypePostgresBackupStarted EventType = "postgres_backup_started" EventTypePostgresClusterLeaderChanged EventType = "postgres_cluster_leader_changed" + EventTypePostgresConnectionPoolChanged EventType = "postgres_connection_pool_changed" + EventTypePostgresConnectionPoolEnabledChanged EventType = "postgres_connection_pool_enabled_changed" EventTypePostgresCreated EventType = "postgres_created" EventTypePostgresDiskAutoscalingEnabledChanged EventType = "postgres_disk_autoscaling_enabled_changed" EventTypePostgresDiskSizeChanged EventType = "postgres_disk_size_changed" diff --git a/pkg/client/types_gen.go b/pkg/client/types_gen.go index a7f521e..e4a3ec7 100644 --- a/pkg/client/types_gen.go +++ b/pkg/client/types_gen.go @@ -1366,7 +1366,9 @@ type Plan string // Postgres defines model for postgres. type Postgres struct { - CreatedAt time.Time `json:"createdAt"` + // ConnectionPool What connection pool to use (if any) out of 'pgbouncer' and 'none' + ConnectionPool string `json:"connectionPool"` + CreatedAt time.Time `json:"createdAt"` // DashboardUrl The URL to view the Postgres instance in the Render Dashboard DashboardUrl string `json:"dashboardUrl"` @@ -1404,15 +1406,19 @@ type PostgresSuspended string // PostgresConnectionInfo defines model for postgresConnectionInfo. type PostgresConnectionInfo struct { - ExternalConnectionString string `json:"externalConnectionString"` - InternalConnectionString string `json:"internalConnectionString"` - Password string `json:"password"` - PsqlCommand string `json:"psqlCommand"` + ExternalConnectionPoolString *string `json:"externalConnectionPoolString,omitempty"` + ExternalConnectionString string `json:"externalConnectionString"` + InternalConnectionPoolString *string `json:"internalConnectionPoolString,omitempty"` + InternalConnectionString string `json:"internalConnectionString"` + Password string `json:"password"` + PsqlCommand string `json:"psqlCommand"` } // PostgresDetail defines model for postgresDetail. type PostgresDetail struct { - CreatedAt time.Time `json:"createdAt"` + // ConnectionPool What connection pool to use (if any) out of 'pgbouncer' and 'none' + ConnectionPool string `json:"connectionPool"` + CreatedAt time.Time `json:"createdAt"` // DashboardUrl The URL to view the Postgres instance in the Render Dashboard DashboardUrl string `json:"dashboardUrl"` @@ -1452,6 +1458,9 @@ type PostgresDetailSuspended string // PostgresPATCHInput defines model for postgresPATCHInput. type PostgresPATCHInput struct { + // ConnectionPool What connection pool to use (if any) out of 'pgbouncer' and 'none' + ConnectionPool *string `json:"connectionPool,omitempty"` + // DatadogAPIKey The Datadog API key for the Datadog agent to monitor the database. Pass empty string to remove. Restarts Postgres on change. DatadogAPIKey *string `json:"datadogAPIKey,omitempty"` @@ -1471,8 +1480,10 @@ type PostgresPATCHInput struct { // PostgresPOSTInput Input for creating a database type PostgresPOSTInput struct { - DatabaseName *string `json:"databaseName,omitempty"` - DatabaseUser *string `json:"databaseUser,omitempty"` + // ConnectionPool What connection pool to use (if any) out of 'pgbouncer' and 'none' + ConnectionPool *string `json:"connectionPool,omitempty"` + DatabaseName *string `json:"databaseName,omitempty"` + DatabaseUser *string `json:"databaseUser,omitempty"` // DatadogAPIKey The Datadog API key for the Datadog agent to monitor the new database. DatadogAPIKey *string `json:"datadogAPIKey,omitempty"` diff --git a/pkg/postgres/create.go b/pkg/postgres/create.go index 4fdffdf..9e9255f 100644 --- a/pkg/postgres/create.go +++ b/pkg/postgres/create.go @@ -36,6 +36,7 @@ type CreateRequestInput struct { HighAvailability *bool DiskSizeGB *int DiskAutoscaling *bool + ConnectionPool *string DatadogAPIKey *string DatadogSite *string IPAllowList []string @@ -69,6 +70,7 @@ func buildRequestInput(in pgtypes.CreatePostgresInput, ownerID string, environme HighAvailability: in.HighAvailability, DiskSizeGB: in.DiskSizeGB, DiskAutoscaling: in.DiskAutoscaling, + ConnectionPool: in.ConnectionPool, DatadogAPIKey: in.DatadogAPIKey, DatadogSite: in.DatadogSite, IPAllowList: in.IPAllowList, @@ -107,6 +109,7 @@ func BuildCreateRequest(input CreateRequestInput) (client.CreatePostgresJSONRequ DatadogSite: input.DatadogSite, DiskSizeGB: input.DiskSizeGB, EnableDiskAutoscaling: input.DiskAutoscaling, + ConnectionPool: input.ConnectionPool, EnableHighAvailability: input.HighAvailability, EnvironmentId: input.EnvironmentID, ReadReplicas: buildReadReplicas(input.ReadReplicas), diff --git a/pkg/postgres/create_test.go b/pkg/postgres/create_test.go index 445d0ce..2e372f3 100644 --- a/pkg/postgres/create_test.go +++ b/pkg/postgres/create_test.go @@ -76,6 +76,7 @@ func TestBuildCreateRequest_AllFieldsSpecified(t *testing.T) { HighAvailability: pointers.From(true), DiskSizeGB: pointers.From(100), DiskAutoscaling: pointers.From(true), + ConnectionPool: pointers.From("pgbouncer"), DatadogAPIKey: pointers.From("dd-key"), DatadogSite: pointers.From("US3"), IPAllowList: []string{ @@ -96,6 +97,7 @@ func TestBuildCreateRequest_AllFieldsSpecified(t *testing.T) { assert.Equal(t, pointers.From(true), body.EnableHighAvailability) assert.Equal(t, pointers.From(100), body.DiskSizeGB) assert.Equal(t, pointers.From(true), body.EnableDiskAutoscaling) + assert.Equal(t, pointers.From("pgbouncer"), body.ConnectionPool) assert.Equal(t, pointers.From("dd-key"), body.DatadogAPIKey) assert.Equal(t, pointers.From("US3"), body.DatadogSite) assert.Equal(t, &envID, body.EnvironmentId) @@ -129,6 +131,7 @@ func TestBuildCreateRequest_OmitsOptionalsWhenUnset(t *testing.T) { assert.Nil(t, body.EnableHighAvailability) assert.Nil(t, body.DiskSizeGB) assert.Nil(t, body.EnableDiskAutoscaling) + assert.Nil(t, body.ConnectionPool) assert.Nil(t, body.DatadogAPIKey) assert.Nil(t, body.DatadogSite) assert.Nil(t, body.EnvironmentId) diff --git a/pkg/postgres/output.go b/pkg/postgres/output.go index c4528d4..5878a12 100644 --- a/pkg/postgres/output.go +++ b/pkg/postgres/output.go @@ -66,6 +66,7 @@ type PostgresUpdateDiff struct { Plan *PostgresFieldDiff[pgclient.PostgresPlans] `json:"plan,omitempty"` DiskSizeGB *PostgresFieldDiff[*int] `json:"diskSizeGB,omitempty"` DiskAutoscalingEnabled *PostgresFieldDiff[bool] `json:"diskAutoscalingEnabled,omitempty"` + ConnectionPool *PostgresFieldDiff[string] `json:"connectionPool,omitempty"` HighAvailabilityEnabled *PostgresFieldDiff[bool] `json:"highAvailabilityEnabled,omitempty"` IPAllowList *PostgresFieldDiff[[]client.CidrBlockAndDescription] `json:"ipAllowList,omitempty"` } @@ -151,6 +152,9 @@ func newPostgresUpdateDiff(before *client.PostgresDetail, after *PostgresOut) Po if before.DiskAutoscalingEnabled != after.DiskAutoscalingEnabled { diff.DiskAutoscalingEnabled = newPostgresFieldDiff(before.DiskAutoscalingEnabled, after.DiskAutoscalingEnabled) } + if before.ConnectionPool != after.ConnectionPool { + diff.ConnectionPool = newPostgresFieldDiff(before.ConnectionPool, after.ConnectionPool) + } if before.HighAvailabilityEnabled != after.HighAvailabilityEnabled { diff.HighAvailabilityEnabled = newPostgresFieldDiff(before.HighAvailabilityEnabled, after.HighAvailabilityEnabled) } diff --git a/pkg/postgres/output_test.go b/pkg/postgres/output_test.go index f84e0f5..a14922f 100644 --- a/pkg/postgres/output_test.go +++ b/pkg/postgres/output_test.go @@ -37,6 +37,7 @@ func TestNewPostgresListOut_ConstructsItems(t *testing.T) { DatabaseUser: "analytics_user", DiskSizeGB: &diskSizeGB, DiskAutoscalingEnabled: true, + ConnectionPool: "pgbouncer", HighAvailabilityEnabled: true, IpAllowList: nil, ReadReplicas: client.ReadReplicas{{Id: "dpg-replica-2", Name: "analytics-replica-2", ParameterOverrides: &replicaOverrides}}, @@ -85,6 +86,7 @@ func TestNewPostgresGetOut_JSONSerialization(t *testing.T) { DatabaseUser: "analytics_user", DiskSizeGB: pointers.From(20), DiskAutoscalingEnabled: true, + ConnectionPool: "none", HighAvailabilityEnabled: true, IpAllowList: []client.CidrBlockAndDescription{{CidrBlock: "203.0.113.5/32", Description: "office"}}, ReadReplicas: client.ReadReplicas{{Id: "dpg-replica", Name: "analytics-replica", ParameterOverrides: &replicaOverrides}}, @@ -131,6 +133,7 @@ func TestNewPostgresGetOut_JSONSerialization(t *testing.T) { "databaseUser": "analytics_user", "diskSizeGB": float64(20), "diskAutoscalingEnabled": true, + "connectionPool": "none", "highAvailabilityEnabled": true, "ipAllowList": []any{map[string]any{"cidrBlock": "203.0.113.5/32", "description": "office"}}, "readReplicas": []any{map[string]any{"id": "dpg-replica", "name": "analytics-replica"}}, @@ -263,6 +266,7 @@ func TestNewPostgresUpdateOut(t *testing.T) { Plan: pgclient.Pro4gb, DiskSizeGB: &beforeDiskSize, DiskAutoscalingEnabled: false, + ConnectionPool: "pgbouncer", HighAvailabilityEnabled: false, IpAllowList: []client.CidrBlockAndDescription{{CidrBlock: "203.0.113.5/32", Description: "office"}}, ParameterOverrides: &beforeOverrides, @@ -276,6 +280,7 @@ func TestNewPostgresUpdateOut(t *testing.T) { Plan: pgclient.Pro8gb, DiskSizeGB: &afterDiskSize, DiskAutoscalingEnabled: true, + ConnectionPool: "none", HighAvailabilityEnabled: true, IpAllowList: []client.CidrBlockAndDescription{}, ParameterOverrides: &afterOverrides, @@ -295,6 +300,7 @@ func TestNewPostgresUpdateOut(t *testing.T) { "plan": map[string]any{"before": "pro_4gb", "after": "pro_8gb"}, "diskSizeGB": map[string]any{"before": float64(10), "after": float64(20)}, "diskAutoscalingEnabled": map[string]any{"before": false, "after": true}, + "connectionPool": map[string]any{"before": "pgbouncer", "after": "none"}, "highAvailabilityEnabled": map[string]any{"before": false, "after": true}, "ipAllowList": map[string]any{ "before": []any{map[string]any{"cidrBlock": "203.0.113.5/32", "description": "office"}}, diff --git a/pkg/postgres/update.go b/pkg/postgres/update.go index ca243a4..14986ec 100644 --- a/pkg/postgres/update.go +++ b/pkg/postgres/update.go @@ -46,6 +46,10 @@ func BuildUpdateRequest(input pgtypes.UpdatePostgresInput) (client.UpdatePostgre body.EnableDiskAutoscaling = input.DiskAutoscaling } + if input.ConnectionPool != nil { + body.ConnectionPool = input.ConnectionPool + } + if input.HighAvailability != nil { body.EnableHighAvailability = input.HighAvailability } diff --git a/pkg/postgres/update_test.go b/pkg/postgres/update_test.go index 59d89fa..c3e970c 100644 --- a/pkg/postgres/update_test.go +++ b/pkg/postgres/update_test.go @@ -24,6 +24,7 @@ func TestBuildUpdateRequest_OnlyNameSet(t *testing.T) { assert.Nil(t, body.Plan) assert.Nil(t, body.DiskSizeGB) assert.Nil(t, body.EnableDiskAutoscaling) + assert.Nil(t, body.ConnectionPool) assert.Nil(t, body.EnableHighAvailability) assert.Nil(t, body.DatadogAPIKey) assert.Nil(t, body.DatadogSite) @@ -82,6 +83,7 @@ func TestBuildUpdateRequest_AllScalarFields(t *testing.T) { Plan: pointers.From("standard"), DiskSizeGB: pointers.From(100), DiskAutoscaling: pointers.From(true), + ConnectionPool: pointers.From("pgbouncer"), HighAvailability: pointers.From(true), DatadogAPIKey: pointers.From("dd-key"), DatadogSite: pointers.From("US3"), @@ -94,6 +96,7 @@ func TestBuildUpdateRequest_AllScalarFields(t *testing.T) { assert.Equal(t, pgclient.PostgresPlans("standard"), *body.Plan) assert.Equal(t, pointers.From(100), body.DiskSizeGB) assert.Equal(t, pointers.From(true), body.EnableDiskAutoscaling) + assert.Equal(t, pointers.From("pgbouncer"), body.ConnectionPool) assert.Equal(t, pointers.From(true), body.EnableHighAvailability) assert.Equal(t, pointers.From("dd-key"), body.DatadogAPIKey) assert.Equal(t, pointers.From("US3"), body.DatadogSite) diff --git a/pkg/text/postgres.go b/pkg/text/postgres.go index 875279c..5aa1508 100644 --- a/pkg/text/postgres.go +++ b/pkg/text/postgres.go @@ -61,6 +61,7 @@ func PostgresDetail(pg *postgres.PostgresOut) string { lines = append(lines, fmt.Sprintf("Disk size: %d GB", *pg.DiskSizeGB)) } lines = append(lines, fmt.Sprintf("Disk autoscaling: %s", boolLabel(pg.DiskAutoscalingEnabled))) + lines = append(lines, fmt.Sprintf("Connection pool: %s", pg.ConnectionPool)) lines = append(lines, fmt.Sprintf("High availability: %s", boolLabel(pg.HighAvailabilityEnabled))) lines = append(lines, fmt.Sprintf("Dashboard: %s", pg.DashboardUrl)) if block := readReplicasBlock(pg.ReadReplicas); block != "" { @@ -138,6 +139,9 @@ func PostgresUpdateDiff(diff postgres.PostgresUpdateDiff) string { if diff.DiskAutoscalingEnabled != nil { lines = append(lines, fmt.Sprintf(" %-20s%s → %s", "Disk autoscaling:", boolLabel(diff.DiskAutoscalingEnabled.Before), boolLabel(diff.DiskAutoscalingEnabled.After))) } + if diff.ConnectionPool != nil { + lines = append(lines, fmt.Sprintf(" %-20s%s → %s", "Connection pool:", diff.ConnectionPool.Before, diff.ConnectionPool.After)) + } if diff.HighAvailabilityEnabled != nil { lines = append(lines, fmt.Sprintf(" %-20s%s → %s", "High availability:", boolLabel(diff.HighAvailabilityEnabled.Before), boolLabel(diff.HighAvailabilityEnabled.After))) } diff --git a/pkg/text/postgres_test.go b/pkg/text/postgres_test.go index 86ee3a9..9c1b758 100644 --- a/pkg/text/postgres_test.go +++ b/pkg/text/postgres_test.go @@ -112,6 +112,13 @@ func TestPostgresDetail_OptionalFields(t *testing.T) { out := basicPostgresOut(pg) assert.Contains(t, text.PostgresDetail(&out), "Environment: evm-123") }) + + t.Run("includes connection pool when set", func(t *testing.T) { + pg := basicPostgres() + pg.ConnectionPool = pointers.From("pgbouncer") + out := basicPostgresOut(pg) + assert.Contains(t, text.PostgresDetail(&out), "Connection Pool: pgbouncer") + }) } func TestPostgresDetail_BoolLabels(t *testing.T) { diff --git a/pkg/tui/views/pgcreate.go b/pkg/tui/views/pgcreate.go index cc15f9a..405c677 100644 --- a/pkg/tui/views/pgcreate.go +++ b/pkg/tui/views/pgcreate.go @@ -81,6 +81,7 @@ type pgCreateDraft struct { region string ha bool diskAutoscaling bool + connectionPool string } // pgCreateLoadedData holds data fetched asynchronously from the server. @@ -313,6 +314,23 @@ var pgCreateSteps = []pgCreateWizardStep{ } }, }, + { + label: "Connection Pool", + buildForm: func(m *PostgresCreateModel) *huh.Form { return m.buildConnectionPoolForm() }, + commit: func(m *PostgresCreateModel) pgStepCommit { + auto := m.confirmValue + label := "none" + if auto { + label = "pgbouncer" + } + return pgStepCommit{ + displayValue: label, + apply: func(d *pgCreateDraft) { + d.connectionPool = label + }, + } + }, + }, { label: "Confirm", buildForm: func(m *PostgresCreateModel) *huh.Form { return m.buildConfirmForm() }, @@ -573,6 +591,7 @@ func (m *PostgresCreateModel) createCmd() tea.Cmd { func (m *PostgresCreateModel) createRequestInput() postgres.CreateRequestInput { haVal := m.draft.ha diskAutoVal := m.draft.diskAutoscaling + connectionPoolVal := m.draft.connectionPool return postgres.CreateRequestInput{ Name: m.draft.name, OwnerID: m.draft.workspaceID, @@ -582,6 +601,7 @@ func (m *PostgresCreateModel) createRequestInput() postgres.CreateRequestInput { EnvironmentID: m.draft.environmentID, HighAvailability: &haVal, DiskAutoscaling: &diskAutoVal, + ConnectionPool: &connectionPoolVal, // Flag-only fields carried through from the original parsed input. DiskSizeGB: m.flagInput.DiskSizeGB, DatabaseName: m.flagInput.DatabaseName, @@ -741,6 +761,16 @@ func (m *PostgresCreateModel) buildDiskAutoscalingForm() *huh.Form { )).WithShowHelp(false) } +func (m *PostgresCreateModel) buildConnectionPoolForm() *huh.Form { + m.confirmValue = false + return huh.NewForm(huh.NewGroup( + huh.NewConfirm(). + Title("Enable Connection Pooling?"). + Description("Pools connections to port :6432."). + Value(&m.confirmValue), + )).WithShowHelp(false) +} + func (m *PostgresCreateModel) buildConfirmForm() *huh.Form { m.confirmValue = false return huh.NewForm(huh.NewGroup( diff --git a/pkg/tui/views/pgcreate_test.go b/pkg/tui/views/pgcreate_test.go index fc8149e..69f090b 100644 --- a/pkg/tui/views/pgcreate_test.go +++ b/pkg/tui/views/pgcreate_test.go @@ -115,6 +115,9 @@ func TestPostgresCreateWizardHappyPathCreatesDatabase(t *testing.T) { testhelper.WaitForContains(t, tm.Output(), "Enable Disk Autoscaling?") tm.Send(tea.KeyMsg{Type: tea.KeyEnter}) // no + testhelper.WaitForContains(t, tm.Output(), "Enable Connection Pooling?") + tm.Send(tea.KeyMsg{Type: tea.KeyEnter}) // no + testhelper.WaitForContains(t, tm.Output(), "Create this Postgres instance?") tm.Send(tea.KeyMsg{Type: tea.KeyRight}) tm.Send(tea.KeyMsg{Type: tea.KeyEnter}) // yes @@ -135,5 +138,6 @@ func TestPostgresCreateWizardHappyPathCreatesDatabase(t *testing.T) { assert.Equal(t, client.Oregon, pg.Region) assert.False(t, pg.HighAvailabilityEnabled) assert.False(t, pg.DiskAutoscalingEnabled) + assert.Equal(t, "none", pg.ConnectionPool) assert.Nil(t, pg.DiskSizeGB) } diff --git a/pkg/types/postgres/create.go b/pkg/types/postgres/create.go index f3beb47..3126be0 100644 --- a/pkg/types/postgres/create.go +++ b/pkg/types/postgres/create.go @@ -24,6 +24,7 @@ type CreatePostgresInput struct { HighAvailability *bool `cli:"high-availability"` DiskSizeGB *int `cli:"disk-size-gb"` DiskAutoscaling *bool `cli:"disk-autoscaling"` + ConnectionPool *string `cli:"connection-pool"` DatadogAPIKey *string `cli:"datadog-api-key"` DatadogSite *string `cli:"datadog-site"` IPAllowList []string `cli:"ip-allow-list"` @@ -34,6 +35,9 @@ func (c CreatePostgresInput) Validate(interactive bool) error { if err := ValidateDiskSizeGB(c.DiskSizeGB); err != nil { return err } + if err := ValidateConnectionPool(c.ConnectionPool); err != nil { + return err + } for _, entry := range c.IPAllowList { if _, _, err := types.ParseIPAllowListEntry(entry); err != nil { return err @@ -57,3 +61,16 @@ func ValidateDiskSizeGB(size *int) error { } return fmt.Errorf("invalid --disk-size-gb %d: must be 1 or a multiple of 5", v) } + +// ValidateConnectionPool enforces the enum +func ValidateConnectionPool(connectionPool *string) error { + if connectionPool == nil { + return nil + } + v := *connectionPool + switch v { + case "none", "pgbouncer": + return nil + } + return fmt.Errorf("invalid --connection-pool %s: must be 'none' or 'pgbouncer'", v) +} diff --git a/pkg/types/postgres/update.go b/pkg/types/postgres/update.go index 14f7c48..0728107 100644 --- a/pkg/types/postgres/update.go +++ b/pkg/types/postgres/update.go @@ -24,6 +24,7 @@ type UpdatePostgresInput struct { HighAvailability *bool `cli:"high-availability"` DiskSizeGB *int `cli:"disk-size-gb"` DiskAutoscaling *bool `cli:"disk-autoscaling"` + ConnectionPool *string `cli:"connection-pool"` DatadogAPIKey *string `cli:"datadog-api-key"` DatadogSite *string `cli:"datadog-site"` IPAllowList []string `cli:"ip-allow-list"` @@ -40,6 +41,7 @@ func (u UpdatePostgresInput) Validate(interactive bool) error { u.HighAvailability != nil || u.DiskSizeGB != nil || u.DiskAutoscaling != nil || + u.ConnectionPool != nil || u.DatadogAPIKey != nil || u.DatadogSite != nil || len(u.IPAllowList) > 0 || @@ -52,6 +54,10 @@ func (u UpdatePostgresInput) Validate(interactive bool) error { return err } + if err := ValidateConnectionPool(u.ConnectionPool); err != nil { + return err + } + if len(u.IPAllowList) > 0 && u.ClearIPAllowList { return fmt.Errorf("--ip-allow-list and --clear-ip-allow-list are mutually exclusive") }