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
97 changes: 97 additions & 0 deletions cmd/deploy_proxysql.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// DBDeployer - The MySQL Sandbox
// Copyright © 2006-2021 Giuseppe Maxia
//
// 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 (
"fmt"
"path"

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

func deploySandboxProxySQL(cmd *cobra.Command, args []string) {
flags := cmd.Flags()
port, _ := flags.GetInt("port")
adminUser, _ := flags.GetString("admin-user")
adminPassword, _ := flags.GetString("admin-password")
skipStart, _ := flags.GetBool("skip-start")
Comment on lines +29 to +33
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.

This command always uses the requested/default admin port (6032) without checking for conflicts or reserving the required 2-port range (admin + mysql interface). Consider using common.GetInstalledPorts + common.FindFreePort (howMany=2), at least when the user did not explicitly set --port.

Copilot uses AI. Check for mistakes.

p, err := providers.DefaultRegistry.Get("proxysql")
Comment on lines +30 to +35
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.

--port can be set to 0/negative, which will produce an invalid config (ProxySQL provider will end up with adminPort=0 and mysqlPort=1). Validate that port is >0 (or explicitly support port=0 as "auto" and implement free-port selection) before continuing.

Copilot uses AI. Check for mistakes.
if err != nil {
common.Exitf(1, "ProxySQL provider not available: %s", err)
}

if _, err := p.FindBinary(""); err != nil {
common.Exitf(1, "proxysql binary not found in PATH: %s", err)
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.

The error from p.FindBinary already includes "proxysql binary not found in PATH". Wrapping it with the same phrase here results in a duplicated message ("... in PATH: proxysql binary not found in PATH: ..."). Consider using a shorter wrapper like "proxysql binary not found" or just printing err directly.

Suggested change
common.Exitf(1, "proxysql binary not found in PATH: %s", err)
common.Exitf(1, "%s", err)

Copilot uses AI. Check for mistakes.
}

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

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

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

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

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

fmt.Printf("ProxySQL sandbox deployed in %s (admin port: %d, mysql port: %d)\n", sandboxDir, port, port+1)
}

var deployProxySQLCmd = &cobra.Command{
Use: "proxysql",
Short: "deploys a ProxySQL sandbox",
Long: `proxysql deploys a standalone ProxySQL instance as a sandbox.
It creates a sandbox directory with configuration, start/stop scripts, and a
client script for administration.

Example:
dbdeployer deploy proxysql
dbdeployer deploy proxysql --port=16032
dbdeployer deploy proxysql --admin-user=myadmin --admin-password=secret
`,
Run: deploySandboxProxySQL,
}

func init() {
deployCmd.AddCommand(deployProxySQLCmd)
deployProxySQLCmd.Flags().Int("port", 6032, "ProxySQL admin port")
deployProxySQLCmd.Flags().String("admin-user", "admin", "ProxySQL admin user")
deployProxySQLCmd.Flags().String("admin-password", "admin", "ProxySQL admin password")
deployProxySQLCmd.Flags().Bool("skip-start", false, "Do not start ProxySQL after deployment")
}
36 changes: 36 additions & 0 deletions cmd/replication.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@
package cmd

import (
"fmt"
"path"

"github.com/ProxySQL/dbdeployer/common"
"github.com/ProxySQL/dbdeployer/defaults"
"github.com/ProxySQL/dbdeployer/globals"
"github.com/ProxySQL/dbdeployer/providers"
"github.com/ProxySQL/dbdeployer/sandbox"
Expand Down Expand Up @@ -102,6 +106,37 @@ func replicationSandbox(cmd *cobra.Command, args []string) {
if err != nil {
common.Exitf(1, globals.ErrCreatingSandbox, err)
}

withProxySQL, _ := flags.GetBool("with-proxysql")
if withProxySQL {
// Determine the sandbox directory that was created
sandboxDir := path.Join(sd.SandboxDir, defaults.Defaults().MasterSlavePrefix+common.VersionToName(origin))
if sd.DirName != "" {
sandboxDir = path.Join(sd.SandboxDir, sd.DirName)
}

// Read port info from child sandbox descriptions
masterDesc, err := common.ReadSandboxDescription(path.Join(sandboxDir, defaults.Defaults().MasterName))
Comment on lines +112 to +119
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.

The ProxySQL sidecar logic assumes a master/slave replication layout (master directory name + node%d slaves) and constructs sandboxDir using MasterSlavePrefix regardless of the selected --topology. For non-master-slave topologies (group, fan-in, all-masters, pxc, ndb) this path/layout will be wrong and the subsequent ReadSandboxDescription calls will fail. Either restrict --with-proxysql to globals.MasterSlaveLabel, or compute the sandbox directory/layout based on the chosen topology (mirroring sandbox.CreateReplicationSandbox’s topology switch).

Copilot uses AI. Check for mistakes.
if err != nil {
common.Exitf(1, "could not read master sandbox description: %s", err)
}
masterPort := masterDesc.Port[0]

var slavePorts []int
for i := 1; i < nodes; i++ {
nodeDir := path.Join(sandboxDir, fmt.Sprintf("%s%d", defaults.Defaults().NodePrefix, i))
nodeDesc, err := common.ReadSandboxDescription(nodeDir)
if err != nil {
common.Exitf(1, "could not read node%d sandbox description: %s", i, err)
}
slavePorts = append(slavePorts, nodeDesc.Port[0])
}

err = sandbox.DeployProxySQLForTopology(sandboxDir, masterPort, slavePorts, 0, "127.0.0.1")
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.

ProxySQL is deployed with host hardcoded to "127.0.0.1". If the replication sandboxes were created with a different bind address (via the global --bind-address flag), ProxySQL may not be able to reach the backends. Prefer using the Host from the sandbox descriptions (masterDesc.Host) or the bind-address value used during deployment.

Copilot uses AI. Check for mistakes.
if err != nil {
common.Exitf(1, "ProxySQL deployment failed: %s", err)
}
Comment on lines +135 to +138
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.

If the user deployed replication with --skip-start (sd.SkipStart), this currently still deploys and starts ProxySQL. That creates a sidecar pointing at backends that are not running and diverges from the expected semantics of skip-start. Consider skipping ProxySQL start/deployment when skip-start is set, or propagating skip-start down to the helper.

Copilot uses AI. Check for mistakes.
}
}

var replicationCmd = &cobra.Command{
Expand Down Expand Up @@ -154,4 +189,5 @@ func init() {
replicationCmd.PersistentFlags().BoolP(globals.SuperReadOnlyLabel, "", false, "Set super-read-only for slaves")
replicationCmd.PersistentFlags().Bool(globals.ReplHistoryDirLabel, false, "uses the replication directory to store mysql client history")
setPflag(replicationCmd, globals.ChangeMasterOptions, "", "CHANGE_MASTER_OPTIONS", "", "options to add to CHANGE MASTER TO", true)
replicationCmd.PersistentFlags().Bool("with-proxysql", false, "Deploy ProxySQL alongside the replication sandbox")
}
76 changes: 76 additions & 0 deletions sandbox/proxysql_topology.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package sandbox

import (
Comment on lines +1 to +3
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.

New sandbox file is missing the standard DBDeployer Apache 2.0 license header comment block that is present at the top of other files in this package (e.g. sandbox/sandbox.go). Please add the header for consistency and licensing clarity.

Copilot uses AI. Check for mistakes.
"fmt"
"path"
"strings"

"github.com/ProxySQL/dbdeployer/common"
"github.com/ProxySQL/dbdeployer/providers"
)

// DeployProxySQLForTopology creates a ProxySQL sandbox configured for a MySQL topology.
//
// Parameters:
// - sandboxDir: parent sandbox directory (e.g. ~/sandboxes/rsandbox_8_4_4)
// - masterPort: MySQL master port
// - slavePorts: MySQL slave ports (empty for single topology)
// - proxysqlPort: port for ProxySQL admin interface (0 = auto-assign)
// - host: bind address (typically "127.0.0.1")
func DeployProxySQLForTopology(sandboxDir string, masterPort int, slavePorts []int, proxysqlPort int, host string) error {
reg := providers.DefaultRegistry
p, err := reg.Get("proxysql")
if err != nil {
return fmt.Errorf("ProxySQL provider not available: %w", err)
}

if _, err := p.FindBinary(""); err != nil {
return fmt.Errorf("proxysql binary not found: %w", err)
}

proxysqlDir := path.Join(sandboxDir, "proxysql")

if proxysqlPort == 0 {
proxysqlPort = 6032
// Try to find a free port
freePort, err := common.FindFreePort(proxysqlPort, []int{}, 1)
Comment on lines +35 to +36
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.

ProxySQL uses two consecutive ports (adminPort and adminPort+1 for the MySQL interface). The auto-port selection requests only 1 port, so it may choose an admin port whose +1 is already in use, causing start failures. The free-port search should reserve/check a range of 2 ports.

Suggested change
// Try to find a free port
freePort, err := common.FindFreePort(proxysqlPort, []int{}, 1)
// Try to find a free range of two consecutive ports (admin and MySQL interface)
freePort, err := common.FindFreePort(proxysqlPort, []int{}, 2)

Copilot uses AI. Check for mistakes.
if err == nil {
proxysqlPort = freePort
}
Comment on lines +34 to +39
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.

When proxysqlPort==0, this calls common.FindFreePort with an empty installedPorts list and ignores the returned error. This effectively always selects 6032 even if it conflicts with existing sandboxes, and can silently fall back to a busy port. Use the actual installed ports (e.g. from common.GetInstalledPorts on the sandbox home) and return an error if a free port cannot be found.

Suggested change
proxysqlPort = 6032
// Try to find a free port
freePort, err := common.FindFreePort(proxysqlPort, []int{}, 1)
if err == nil {
proxysqlPort = freePort
}
// Determine currently used ports from the sandbox home (parent of sandboxDir)
sandboxHome := path.Dir(sandboxDir)
installedPorts, err := common.GetInstalledPorts(sandboxHome)
if err != nil {
return fmt.Errorf("cannot determine installed ports in %s: %w", sandboxHome, err)
}
proxysqlPort = 6032
// Try to find a free port starting from the default ProxySQL admin port
freePort, err := common.FindFreePort(proxysqlPort, installedPorts, 1)
if err != nil {
return fmt.Errorf("no free port available for ProxySQL starting at %d: %w", proxysqlPort, err)
}
proxysqlPort = freePort

Copilot uses AI. Check for mistakes.
}

// Build backends: master = HG 0, slaves = HG 1
var backendParts []string
backendParts = append(backendParts, fmt.Sprintf("%s:%d:0", host, masterPort))
for _, slavePort := range slavePorts {
backendParts = append(backendParts, fmt.Sprintf("%s:%d:1", host, slavePort))
}

config := providers.SandboxConfig{
Version: "system",
Dir: proxysqlDir,
Port: proxysqlPort,
AdminPort: proxysqlPort,
Host: host,
DbUser: "admin",
DbPassword: "admin",
Options: map[string]string{
"monitor_user": "msandbox",
"monitor_password": "msandbox",
"backends": strings.Join(backendParts, ","),
},
}
Comment on lines +42 to +62
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.

Consider adding unit tests for DeployProxySQLForTopology, especially for backend string generation (HG0/HG1 mapping) and port selection behavior. The sandbox package has existing tests, but this new helper currently isn't covered.

Copilot uses AI. Check for mistakes.

_, err = p.CreateSandbox(config)
if err != nil {
return fmt.Errorf("creating ProxySQL sandbox: %w", err)
}

// Start ProxySQL
if err := p.StartSandbox(proxysqlDir); err != nil {
return fmt.Errorf("starting ProxySQL: %w", err)
}
Comment on lines +69 to +72
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.

This helper always starts ProxySQL after creating the sandbox. When used from replication deployment, it should respect the user's --skip-start choice (i.e., avoid starting ProxySQL when the replication sandbox was deployed with SkipStart), or accept a skipStart parameter.

Copilot uses AI. Check for mistakes.

fmt.Printf("ProxySQL deployed in %s (admin port: %d, mysql port: %d)\n", proxysqlDir, proxysqlPort, proxysqlPort+1)
return nil
}
Loading