Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
8e0e623
feat: extend Provider interface with SupportedTopologies and CreateRe…
renecannao Mar 24, 2026
aa7a81e
feat: add PostgreSQL provider core structure
renecannao Mar 24, 2026
4be2b3b
feat: add PostgreSQL config generation (postgresql.conf, pg_hba.conf)
renecannao Mar 24, 2026
65e5551
feat: implement PostgreSQL CreateSandbox with initdb, config gen, and…
renecannao Mar 24, 2026
1c8d32b
feat: add PostgreSQL deb extraction for binary management
renecannao Mar 24, 2026
ba2c992
feat: implement PostgreSQL CreateReplica with pg_basebackup and monit…
renecannao Mar 24, 2026
fa6e1d8
feat: add --provider flag and PostgreSQL routing to deploy commands
renecannao Mar 24, 2026
a93fb3c
feat: add --provider=postgresql support to dbdeployer unpack for deb …
renecannao Mar 24, 2026
8038ebd
feat: add ProxySQL PostgreSQL backend wiring (pgsql_servers/pgsql_use…
renecannao Mar 24, 2026
768d9c4
feat: add cross-database topology constraint validation
renecannao Mar 24, 2026
99dd2b9
feat: add 'dbdeployer deploy postgresql' standalone command
renecannao Mar 24, 2026
4eb2fd3
test: add PostgreSQL integration tests (build-tagged)
renecannao Mar 24, 2026
9f74117
fix: update export test for new postgresql deploy subcommand
renecannao Mar 24, 2026
00451ca
fix: pass -L share dir to initdb for deb-extracted binaries
renecannao Mar 25, 2026
d6ec6aa
fix: create log dir after initdb (initdb requires empty data dir)
renecannao Mar 25, 2026
cadfe55
fix: copy share files to compat path for postgres binary (timezonesets)
renecannao Mar 25, 2026
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
92 changes: 92 additions & 0 deletions cmd/deploy_postgresql.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package cmd

import (
"fmt"
"path"

"github.com/ProxySQL/dbdeployer/common"
"github.com/ProxySQL/dbdeployer/defaults"
"github.com/ProxySQL/dbdeployer/providers"
"github.com/ProxySQL/dbdeployer/providers/postgresql"
"github.com/spf13/cobra"
)

func deploySandboxPostgreSQL(cmd *cobra.Command, args []string) {
version := args[0]
flags := cmd.Flags()
skipStart, _ := flags.GetBool("skip-start")

p, err := providers.DefaultRegistry.Get("postgresql")
if err != nil {
common.Exitf(1, "PostgreSQL provider not available: %s", err)
}

if err := p.ValidateVersion(version); err != nil {
common.Exitf(1, "invalid version: %s", err)
}

if _, err := p.FindBinary(version); err != nil {
common.Exitf(1, "PostgreSQL binaries not found: %s\nRun: dbdeployer unpack --provider=postgresql <server.deb> <client.deb>", err)
}

port, err := postgresql.VersionToPort(version)
if err != nil {
common.Exitf(1, "error computing port: %s", err)
}
freePort, portErr := common.FindFreePort(port, []int{}, 1)
if portErr == nil {
port = freePort
}
Comment on lines +36 to +39
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current error handling for common.FindFreePort is problematic. If FindFreePort returns an error (meaning it couldn't find a free port starting from the suggested one), the original port value is retained. This port might already be in use, leading to a subsequent failure when CreateSandbox attempts to bind to it. It would be more robust to either exit with an error if no free port can be found or implement a retry mechanism to find an alternative port.

Suggested change
freePort, portErr := common.FindFreePort(port, []int{}, 1)
if portErr == nil {
port = freePort
}
freePort, portErr := common.FindFreePort(port, []int{}, 1)
if portErr != nil {
common.Exitf(1, "error finding a free port: %s", portErr)
}
port = freePort


sandboxHome := defaults.Defaults().SandboxHome
sandboxDir := path.Join(sandboxHome, fmt.Sprintf("pg_sandbox_%d", port))

if common.DirExists(sandboxDir) {
common.Exitf(1, "sandbox directory %s already exists", sandboxDir)
}

config := providers.SandboxConfig{
Version: version,
Dir: sandboxDir,
Port: port,
Host: "127.0.0.1",
DbUser: "postgres",
DbPassword: "",
Options: map[string]string{},
}

if _, err := p.CreateSandbox(config); err != nil {
common.Exitf(1, "error creating PostgreSQL sandbox: %s", err)
}

if !skipStart {
if err := p.StartSandbox(sandboxDir); err != nil {
common.Exitf(1, "error starting PostgreSQL: %s", err)
}
}

fmt.Printf("PostgreSQL %s sandbox deployed in %s (port: %d)\n", version, sandboxDir, port)
}

var deployPostgreSQLCmd = &cobra.Command{
Use: "postgresql version",
Short: "deploys a PostgreSQL sandbox",
Long: `postgresql deploys a standalone PostgreSQL instance as a sandbox.
It creates a sandbox directory with data, configuration, start/stop scripts, and a
psql client script.

Requires PostgreSQL binaries to be extracted first:
dbdeployer unpack --provider=postgresql postgresql-16_*.deb postgresql-client-16_*.deb

Example:
dbdeployer deploy postgresql 16.13
dbdeployer deploy postgresql 17.1 --skip-start
`,
Args: cobra.ExactArgs(1),
Run: deploySandboxPostgreSQL,
}

func init() {
deployCmd.AddCommand(deployPostgreSQLCmd)
deployPostgreSQLCmd.Flags().Bool("skip-start", false, "Do not start PostgreSQL after deployment")
}
10 changes: 9 additions & 1 deletion cmd/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ func TestExportImport(t *testing.T) {
subCommandName: "",
expectedName: "deploy",
expectedAncestors: 2,
expectedSubCommands: 4,
expectedSubCommands: 5,
expectedArgument: "",
},
{
Expand All @@ -192,6 +192,14 @@ func TestExportImport(t *testing.T) {
expectedSubCommands: 0,
expectedArgument: globals.ExportVersionDir,
},
{
commandName: "deploy",
subCommandName: "postgresql",
expectedName: "postgresql",
expectedAncestors: 3,
expectedSubCommands: 0,
expectedArgument: "",
},
{
commandName: "export",
subCommandName: "",
Expand Down
92 changes: 91 additions & 1 deletion cmd/multiple.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,106 @@
package cmd

import (
"fmt"
"os"
"path"
"strings"

"github.com/ProxySQL/dbdeployer/common"
"github.com/ProxySQL/dbdeployer/defaults"
"github.com/ProxySQL/dbdeployer/globals"
"github.com/ProxySQL/dbdeployer/providers"
"github.com/ProxySQL/dbdeployer/providers/postgresql"
"github.com/ProxySQL/dbdeployer/sandbox"
"github.com/spf13/cobra"
)

func deployMultipleNonMySQL(cmd *cobra.Command, args []string, providerName string) {
flags := cmd.Flags()
version := args[0]
nodes, _ := flags.GetInt(globals.NodesLabel)
Comment on lines +33 to +36
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing argument validation before accessing args[0].

Same pattern as other deploy commands: args[0] is accessed at line 35 without validation. The common.CheckOrigin(args) at line 118 is bypassed by the early return at line 114.

Move the validation before the provider check in multipleSandbox:

 func multipleSandbox(cmd *cobra.Command, args []string) {
 	flags := cmd.Flags()
+	common.CheckOrigin(args)
 	providerName, _ := flags.GetString(globals.ProviderLabel)

 	if providerName != "mysql" {
 		deployMultipleNonMySQL(cmd, args, providerName)
 		return
 	}
-
-	common.CheckOrigin(args)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/multiple.go` around lines 33 - 36, The code accesses args[0] in
deployMultipleNonMySQL without validating args length, so add an args validation
(e.g., check len(args) > 0 or use cobra.MinimumNArgs) at the start of
deployMultipleNonMySQL before referencing args[0]; additionally move the
common.CheckOrigin(args) call inside multipleSandbox to run before the provider
check/early return so the origin validation is not skipped, and update any early
returns to occur only after validation passes. Ensure you touch the
deployMultipleNonMySQL function and multipleSandbox function names mentioned so
the fix is applied where args[0] and common.CheckOrigin are used.


p, err := providers.DefaultRegistry.Get(providerName)
if err != nil {
common.Exitf(1, "provider error: %s", err)
}

flavor, _ := flags.GetString(globals.FlavorLabel)
if flavor != "" {
common.Exitf(1, "--flavor is only valid with --provider=mysql")
}

if !providers.ContainsString(p.SupportedTopologies(), "multiple") {
common.Exitf(1, "provider %q does not support topology \"multiple\"\nSupported topologies: %s",
providerName, strings.Join(p.SupportedTopologies(), ", "))
}

if err := p.ValidateVersion(version); err != nil {
common.Exitf(1, "version validation failed: %s", err)
}

if _, err := p.FindBinary(version); err != nil {
common.Exitf(1, "binaries not found: %s", err)
}

basePort := p.DefaultPorts().BasePort
if providerName == "postgresql" {
basePort, _ = postgresql.VersionToPort(version)
}

sandboxHome := defaults.Defaults().SandboxHome
topologyDir := path.Join(sandboxHome, fmt.Sprintf("%s_multi_%d", providerName, basePort))
if common.DirExists(topologyDir) {
common.Exitf(1, "sandbox directory %s already exists", topologyDir)
}
os.MkdirAll(topologyDir, 0755)

Check failure on line 71 in cmd/multiple.go

View workflow job for this annotation

GitHub Actions / Lint

Error return value of `os.MkdirAll` is not checked (errcheck)

Check failure on line 71 in cmd/multiple.go

View workflow job for this annotation

GitHub Actions / Lint

Error return value of `os.MkdirAll` is not checked (errcheck)
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

os.MkdirAll(topologyDir, 0755) is called without checking the returned error. If directory creation fails, subsequent sandbox creation will fail with less clear errors. Please handle the error and abort early.

Suggested change
os.MkdirAll(topologyDir, 0755)
if err := os.MkdirAll(topologyDir, 0755); err != nil {
common.Exitf(1, "error creating sandbox directory %s: %s", topologyDir, err)
}

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Unchecked error from os.MkdirAll causes CI failure.

The pipeline failure confirms this issue. The error return value must be checked.

🐛 Proposed fix
-	os.MkdirAll(topologyDir, 0755)
+	if err := os.MkdirAll(topologyDir, 0755); err != nil {
+		common.Exitf(1, "error creating topology directory %s: %s", topologyDir, err)
+	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
os.MkdirAll(topologyDir, 0755)
if err := os.MkdirAll(topologyDir, 0755); err != nil {
common.Exitf(1, "error creating topology directory %s: %s", topologyDir, err)
}
🧰 Tools
🪛 GitHub Actions: CI

[error] 71-71: golangci-lint: Error return value of os.MkdirAll is not checked (errcheck)

🪛 GitHub Check: Lint

[failure] 71-71:
Error return value of os.MkdirAll is not checked (errcheck)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/multiple.go` at line 71, The call to os.MkdirAll(topologyDir, 0755)
currently ignores its error; update the code around that call to capture the
returned error (err := os.MkdirAll(...)) and handle it appropriately — for
example, return the error up the call stack or log it and exit (using the
surrounding command's logger or fmt.Errorf/wrapping). Locate the os.MkdirAll
invocation in cmd/multiple.go and ensure the function (where the call appears)
propagates or handles the error instead of discarding it.


skipStart, _ := flags.GetBool(globals.SkipStartLabel)

for i := 1; i <= nodes; i++ {
port := basePort + i
freePort, err := common.FindFreePort(port, []int{}, 1)
if err == nil {
port = freePort
}
Comment on lines +77 to +80
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Similar to deploy_postgresql.go, the error handling for common.FindFreePort here is not robust. If FindFreePort fails to find a free port, the original port (which might be in use) is kept. This can lead to subsequent errors during sandbox creation. Consider exiting or implementing a more robust port allocation strategy if a free port cannot be found.

Suggested change
freePort, err := common.FindFreePort(port, []int{}, 1)
if err == nil {
port = freePort
}
freePort, err := common.FindFreePort(port, []int{}, 1)
if err != nil {
common.Exitf(1, "error finding a free port for node %d: %s", i, err)
}
port = freePort


nodeDir := path.Join(topologyDir, fmt.Sprintf("node%d", i))
config := providers.SandboxConfig{
Version: version,
Dir: nodeDir,
Port: port,
Host: "127.0.0.1",
DbUser: "postgres",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The DbUser is hardcoded to "postgres". While this is a common default for PostgreSQL sandboxes, it might be beneficial to allow this to be configurable via a flag for greater flexibility, similar to how MySQL sandboxes handle db-user.

Options: map[string]string{},
}

if _, err := p.CreateSandbox(config); err != nil {
common.Exitf(1, "error creating node %d: %s", i, err)
}

if !skipStart {
if err := p.StartSandbox(nodeDir); err != nil {
common.Exitf(1, "error starting node %d: %s", i, err)
}
}

fmt.Printf(" Node %d deployed in %s (port: %d)\n", i, nodeDir, port)
}

fmt.Printf("%s multiple sandbox (%d nodes) deployed in %s\n", providerName, nodes, topologyDir)
}

func multipleSandbox(cmd *cobra.Command, args []string) {
flags := cmd.Flags()
providerName, _ := flags.GetString(globals.ProviderLabel)

if providerName != "mysql" {
deployMultipleNonMySQL(cmd, args, providerName)
return
}

var sd sandbox.SandboxDef
common.CheckOrigin(args)
flags := cmd.Flags()
sd, err := fillSandboxDefinition(cmd, args, false)
common.ErrCheckExitf(err, 1, "error filling sandbox definition")
// Validate version with provider
Expand Down Expand Up @@ -69,4 +158,5 @@
func init() {
deployCmd.AddCommand(multipleCmd)
multipleCmd.PersistentFlags().IntP(globals.NodesLabel, "n", globals.NodesValue, "How many nodes will be installed")
multipleCmd.PersistentFlags().String(globals.ProviderLabel, globals.ProviderValue, "Database provider (mysql, postgresql)")
}
Loading
Loading