Skip to content
Merged
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Fix sqlc-test-setup for real-world CCR environments and add idempotency
Key fixes:
- Use apt-get install -f to resolve dpkg dependency issues (libaio1t64,
  libmecab2, libnuma1) instead of expecting all dpkg -i to succeed
- Remove /etc/init.d/mysql chmod (not present in systemd environments)
- Use mysqld_safe to start MySQL (works without systemd/init.d)
- Use caching_sha2_password plugin instead of auth_socket for TCP access
- Add waitForMySQL polling loop for reliable startup detection

Idempotency:
- install: Skips apt proxy, PostgreSQL, and MySQL if already present
- start: Detects running MySQL via mysqladmin ping, skips pg_hba.conf
  entry if already configured, skips password setup if already correct,
  skips MySQL data dir initialization if already done

Tested: both commands succeed on first run and on subsequent re-runs.

https://claude.ai/code/session_01CsyRwSkRxBcQoaQFVkMQsJ
  • Loading branch information
claude committed Feb 19, 2026
commit a57b64784257ef7fa0d5f70b99b552b4fdc38947
182 changes: 157 additions & 25 deletions cmd/sqlc-test-setup/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import (
"log"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
)

func main() {
Expand Down Expand Up @@ -50,6 +52,14 @@ func runOutput(name string, args ...string) (string, error) {
return string(out), err
}

// commandExists checks if a binary is available in PATH.
func commandExists(name string) bool {
_, err := exec.LookPath(name)
return err == nil
}

// ---- install ----

func runInstall() error {
log.Println("=== Installing PostgreSQL and MySQL for test setup ===")

Expand All @@ -76,6 +86,12 @@ func installAptProxy() error {
return nil
}

const confPath = "/etc/apt/apt.conf.d/99proxy"
if _, err := os.Stat(confPath); err == nil {
log.Printf("apt proxy config already exists at %s, skipping", confPath)
return nil
}

log.Printf("configuring apt proxy to use %s", proxy)
proxyConf := fmt.Sprintf("Acquire::http::Proxy \"%s\";", proxy)
cmd := fmt.Sprintf("echo '%s' | sudo tee /etc/apt/apt.conf.d/99proxy", proxyConf)
Expand All @@ -85,6 +101,15 @@ func installAptProxy() error {
func installPostgreSQL() error {
log.Println("--- Installing PostgreSQL ---")

if commandExists("psql") {
out, err := runOutput("psql", "--version")
if err == nil {
log.Printf("postgresql is already installed: %s", strings.TrimSpace(out))
log.Println("skipping postgresql installation")
return nil
}
}

log.Println("updating apt package lists")
if err := run("sudo", "apt-get", "update", "-qq"); err != nil {
return fmt.Errorf("apt-get update: %w", err)
Expand All @@ -102,13 +127,26 @@ func installPostgreSQL() error {
func installMySQL() error {
log.Println("--- Installing MySQL 9 ---")

if commandExists("mysqld") {
out, err := runOutput("mysqld", "--version")
if err == nil {
log.Printf("mysql is already installed: %s", strings.TrimSpace(out))
log.Println("skipping mysql installation")
return nil
}
}

bundleURL := "https://dev.mysql.com/get/Downloads/MySQL-9.1/mysql-server_9.1.0-1ubuntu24.04_amd64.deb-bundle.tar"
bundleTar := "/tmp/mysql-server-bundle.tar"
extractDir := "/tmp/mysql9"

log.Printf("downloading MySQL 9 bundle from %s", bundleURL)
if err := run("curl", "-L", "-o", bundleTar, bundleURL); err != nil {
return fmt.Errorf("downloading mysql bundle: %w", err)
if _, err := os.Stat(bundleTar); err != nil {
log.Printf("downloading MySQL 9 bundle from %s", bundleURL)
if err := run("curl", "-L", "-o", bundleTar, bundleURL); err != nil {
return fmt.Errorf("downloading mysql bundle: %w", err)
}
} else {
log.Printf("mysql bundle already downloaded at %s, skipping download", bundleTar)
}

log.Printf("extracting bundle to %s", extractDir)
Expand All @@ -119,7 +157,9 @@ func installMySQL() error {
return fmt.Errorf("extracting mysql bundle: %w", err)
}

// Install packages in dependency order
// Install packages in dependency order using dpkg.
// Some packages may fail due to missing dependencies, which is expected.
// We fix them all at the end with apt-get install -f.
packages := []string{
"mysql-common_*.deb",
"mysql-community-client-plugins_*.deb",
Expand All @@ -132,23 +172,24 @@ func installMySQL() error {
}

for _, pkg := range packages {
log.Printf("installing %s", pkg)
// Use shell glob expansion via bash -c
log.Printf("installing %s (dependency errors will be fixed afterwards)", pkg)
cmd := fmt.Sprintf("sudo dpkg -i %s/%s", extractDir, pkg)
if err := run("bash", "-c", cmd); err != nil {
return fmt.Errorf("installing %s: %w", pkg, err)
log.Printf("dpkg reported errors for %s (will fix with apt-get install -f)", pkg)
}
}

log.Println("making mysql init script executable")
if err := run("sudo", "chmod", "+x", "/etc/init.d/mysql"); err != nil {
return fmt.Errorf("chmod mysql init script: %w", err)
log.Println("fixing missing dependencies with apt-get install -f")
if err := run("sudo", "apt-get", "install", "-f", "-y"); err != nil {
return fmt.Errorf("apt-get install -f: %w", err)
}

log.Println("mysql 9 installed successfully")
return nil
}

// ---- start ----

func runStart() error {
log.Println("=== Starting PostgreSQL and MySQL ===")

Expand Down Expand Up @@ -185,10 +226,7 @@ func startPostgreSQL() error {
return fmt.Errorf("detecting pg_hba.conf path: %w", err)
}

log.Printf("enabling md5 authentication in %s", hbaPath)
hbaLine := "host all all 127.0.0.1/32 md5"
cmd := fmt.Sprintf("echo '%s' | sudo tee -a %s", hbaLine, hbaPath)
if err := run("bash", "-c", cmd); err != nil {
if err := ensurePgHBAEntry(hbaPath); err != nil {
return fmt.Errorf("configuring pg_hba.conf: %w", err)
}

Expand Down Expand Up @@ -220,30 +258,124 @@ func detectPgHBAPath() (string, error) {
return path, nil
}

// ensurePgHBAEntry adds the md5 auth line to pg_hba.conf if it's not already present.
func ensurePgHBAEntry(hbaPath string) error {
hbaLine := "host all all 127.0.0.1/32 md5"

out, err := runOutput("sudo", "cat", hbaPath)
if err != nil {
return fmt.Errorf("reading pg_hba.conf: %w", err)
}

if strings.Contains(out, "127.0.0.1/32 md5") {
log.Println("md5 authentication for 127.0.0.1/32 already configured in pg_hba.conf, skipping")
return nil
}

log.Printf("enabling md5 authentication in %s", hbaPath)
cmd := fmt.Sprintf("echo '%s' | sudo tee -a %s", hbaLine, hbaPath)
return run("bash", "-c", cmd)
}

func startMySQL() error {
log.Println("--- Starting MySQL ---")

log.Println("initializing mysql data directory")
if err := run("sudo", "mysqld", "--initialize-insecure", "--user=mysql"); err != nil {
return fmt.Errorf("mysqld --initialize-insecure: %w", err)
// Check if MySQL is already running and accessible with the expected password
if mysqlReady() {
log.Println("mysql is already running and accepting connections")
return verifyMySQL()
}

log.Println("starting mysql service")
if err := run("sudo", "/etc/init.d/mysql", "start"); err != nil {
return fmt.Errorf("starting mysql: %w", err)
// Check if data directory already exists and has been initialized
if mysqlInitialized() {
log.Println("mysql data directory already initialized, skipping initialization")
} else {
log.Println("initializing mysql data directory")
if err := run("sudo", "mysqld", "--initialize-insecure", "--user=mysql"); err != nil {
return fmt.Errorf("mysqld --initialize-insecure: %w", err)
}
}

// Ensure the run directory exists for the socket/pid file
if err := run("sudo", "mkdir", "-p", "/var/run/mysqld"); err != nil {
return fmt.Errorf("creating /var/run/mysqld: %w", err)
}
if err := run("sudo", "chown", "mysql:mysql", "/var/run/mysqld"); err != nil {
return fmt.Errorf("chowning /var/run/mysqld: %w", err)
}

log.Println("setting mysql root password")
if err := run("mysql", "-u", "root", "-e",
"ALTER USER 'root'@'localhost' IDENTIFIED BY 'mysecretpassword'; FLUSH PRIVILEGES;"); err != nil {
return fmt.Errorf("setting mysql root password: %w", err)
log.Println("starting mysql via mysqld_safe")
// mysqld_safe runs in the foreground, so we launch it in the background
cmd := exec.Command("sudo", "mysqld_safe", "--user=mysql")
cmd.Stdout = os.Stderr
cmd.Stderr = os.Stderr
if err := cmd.Start(); err != nil {
return fmt.Errorf("starting mysqld_safe: %w", err)
}

// Wait for MySQL to become ready
log.Println("waiting for mysql to accept connections")
if err := waitForMySQL(30 * time.Second); err != nil {
return fmt.Errorf("mysql did not start in time: %w", err)
}
log.Println("mysql is accepting connections")

// Set root password.
// The debconf-based install may configure auth_socket plugin which only
// works via Unix socket. We need caching_sha2_password for TCP access.
log.Println("configuring mysql root password for TCP access")
if err := run("mysql", "-h", "127.0.0.1", "-u", "root", "-pmysecretpassword", "-e", "SELECT 1;"); err == nil {
log.Println("mysql root password already set to expected value, skipping")
} else {
log.Println("setting mysql root password with caching_sha2_password plugin")
// Try via socket (works when auth_socket is the plugin or password is blank)
if err := run("mysql", "-u", "root", "-e",
"ALTER USER 'root'@'localhost' IDENTIFIED WITH caching_sha2_password BY 'mysecretpassword'; FLUSH PRIVILEGES;"); err != nil {
return fmt.Errorf("setting mysql root password: %w", err)
}
}

return verifyMySQL()
}

// mysqlReady checks if MySQL is running and accepting connections with the expected password.
func mysqlReady() bool {
err := exec.Command("mysqladmin", "-h", "127.0.0.1", "-u", "root", "-pmysecretpassword", "ping").Run()
return err == nil
}

// waitForMySQL polls until MySQL accepts connections or the timeout expires.
func waitForMySQL(timeout time.Duration) error {
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
// Try connecting without password (fresh) or with password (already configured)
if exec.Command("mysqladmin", "-u", "root", "ping").Run() == nil {
return nil
}
if exec.Command("mysqladmin", "-h", "127.0.0.1", "-u", "root", "-pmysecretpassword", "ping").Run() == nil {
return nil
}
time.Sleep(500 * time.Millisecond)
}
return fmt.Errorf("timed out after %s waiting for mysql", timeout)
}

func verifyMySQL() error {
log.Println("verifying mysql connection")
if err := run("mysql", "-h", "127.0.0.1", "-u", "root", "-pmysecretpassword", "-e", "SELECT VERSION();"); err != nil {
return fmt.Errorf("mysql connection test failed: %w", err)
}

log.Println("mysql is running and configured")
return nil
}

// mysqlInitialized checks if the MySQL data directory has been initialized.
func mysqlInitialized() bool {
dataDir := "/var/lib/mysql"
entries, err := filepath.Glob(filepath.Join(dataDir, "*.pem"))
if err != nil {
return false
}
// MySQL creates TLS certificate files during initialization
return len(entries) > 0
}