Skip to content

Commit a57b647

Browse files
committed
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
1 parent e117e60 commit a57b647

File tree

1 file changed

+157
-25
lines changed

1 file changed

+157
-25
lines changed

cmd/sqlc-test-setup/main.go

Lines changed: 157 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import (
55
"log"
66
"os"
77
"os/exec"
8+
"path/filepath"
89
"strings"
10+
"time"
911
)
1012

1113
func main() {
@@ -50,6 +52,14 @@ func runOutput(name string, args ...string) (string, error) {
5052
return string(out), err
5153
}
5254

55+
// commandExists checks if a binary is available in PATH.
56+
func commandExists(name string) bool {
57+
_, err := exec.LookPath(name)
58+
return err == nil
59+
}
60+
61+
// ---- install ----
62+
5363
func runInstall() error {
5464
log.Println("=== Installing PostgreSQL and MySQL for test setup ===")
5565

@@ -76,6 +86,12 @@ func installAptProxy() error {
7686
return nil
7787
}
7888

89+
const confPath = "/etc/apt/apt.conf.d/99proxy"
90+
if _, err := os.Stat(confPath); err == nil {
91+
log.Printf("apt proxy config already exists at %s, skipping", confPath)
92+
return nil
93+
}
94+
7995
log.Printf("configuring apt proxy to use %s", proxy)
8096
proxyConf := fmt.Sprintf("Acquire::http::Proxy \"%s\";", proxy)
8197
cmd := fmt.Sprintf("echo '%s' | sudo tee /etc/apt/apt.conf.d/99proxy", proxyConf)
@@ -85,6 +101,15 @@ func installAptProxy() error {
85101
func installPostgreSQL() error {
86102
log.Println("--- Installing PostgreSQL ---")
87103

104+
if commandExists("psql") {
105+
out, err := runOutput("psql", "--version")
106+
if err == nil {
107+
log.Printf("postgresql is already installed: %s", strings.TrimSpace(out))
108+
log.Println("skipping postgresql installation")
109+
return nil
110+
}
111+
}
112+
88113
log.Println("updating apt package lists")
89114
if err := run("sudo", "apt-get", "update", "-qq"); err != nil {
90115
return fmt.Errorf("apt-get update: %w", err)
@@ -102,13 +127,26 @@ func installPostgreSQL() error {
102127
func installMySQL() error {
103128
log.Println("--- Installing MySQL 9 ---")
104129

130+
if commandExists("mysqld") {
131+
out, err := runOutput("mysqld", "--version")
132+
if err == nil {
133+
log.Printf("mysql is already installed: %s", strings.TrimSpace(out))
134+
log.Println("skipping mysql installation")
135+
return nil
136+
}
137+
}
138+
105139
bundleURL := "https://dev.mysql.com/get/Downloads/MySQL-9.1/mysql-server_9.1.0-1ubuntu24.04_amd64.deb-bundle.tar"
106140
bundleTar := "/tmp/mysql-server-bundle.tar"
107141
extractDir := "/tmp/mysql9"
108142

109-
log.Printf("downloading MySQL 9 bundle from %s", bundleURL)
110-
if err := run("curl", "-L", "-o", bundleTar, bundleURL); err != nil {
111-
return fmt.Errorf("downloading mysql bundle: %w", err)
143+
if _, err := os.Stat(bundleTar); err != nil {
144+
log.Printf("downloading MySQL 9 bundle from %s", bundleURL)
145+
if err := run("curl", "-L", "-o", bundleTar, bundleURL); err != nil {
146+
return fmt.Errorf("downloading mysql bundle: %w", err)
147+
}
148+
} else {
149+
log.Printf("mysql bundle already downloaded at %s, skipping download", bundleTar)
112150
}
113151

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

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

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

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

148187
log.Println("mysql 9 installed successfully")
149188
return nil
150189
}
151190

191+
// ---- start ----
192+
152193
func runStart() error {
153194
log.Println("=== Starting PostgreSQL and MySQL ===")
154195

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

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

@@ -220,30 +258,124 @@ func detectPgHBAPath() (string, error) {
220258
return path, nil
221259
}
222260

261+
// ensurePgHBAEntry adds the md5 auth line to pg_hba.conf if it's not already present.
262+
func ensurePgHBAEntry(hbaPath string) error {
263+
hbaLine := "host all all 127.0.0.1/32 md5"
264+
265+
out, err := runOutput("sudo", "cat", hbaPath)
266+
if err != nil {
267+
return fmt.Errorf("reading pg_hba.conf: %w", err)
268+
}
269+
270+
if strings.Contains(out, "127.0.0.1/32 md5") {
271+
log.Println("md5 authentication for 127.0.0.1/32 already configured in pg_hba.conf, skipping")
272+
return nil
273+
}
274+
275+
log.Printf("enabling md5 authentication in %s", hbaPath)
276+
cmd := fmt.Sprintf("echo '%s' | sudo tee -a %s", hbaLine, hbaPath)
277+
return run("bash", "-c", cmd)
278+
}
279+
223280
func startMySQL() error {
224281
log.Println("--- Starting MySQL ---")
225282

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

231-
log.Println("starting mysql service")
232-
if err := run("sudo", "/etc/init.d/mysql", "start"); err != nil {
233-
return fmt.Errorf("starting mysql: %w", err)
289+
// Check if data directory already exists and has been initialized
290+
if mysqlInitialized() {
291+
log.Println("mysql data directory already initialized, skipping initialization")
292+
} else {
293+
log.Println("initializing mysql data directory")
294+
if err := run("sudo", "mysqld", "--initialize-insecure", "--user=mysql"); err != nil {
295+
return fmt.Errorf("mysqld --initialize-insecure: %w", err)
296+
}
297+
}
298+
299+
// Ensure the run directory exists for the socket/pid file
300+
if err := run("sudo", "mkdir", "-p", "/var/run/mysqld"); err != nil {
301+
return fmt.Errorf("creating /var/run/mysqld: %w", err)
302+
}
303+
if err := run("sudo", "chown", "mysql:mysql", "/var/run/mysqld"); err != nil {
304+
return fmt.Errorf("chowning /var/run/mysqld: %w", err)
234305
}
235306

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

316+
// Wait for MySQL to become ready
317+
log.Println("waiting for mysql to accept connections")
318+
if err := waitForMySQL(30 * time.Second); err != nil {
319+
return fmt.Errorf("mysql did not start in time: %w", err)
320+
}
321+
log.Println("mysql is accepting connections")
322+
323+
// Set root password.
324+
// The debconf-based install may configure auth_socket plugin which only
325+
// works via Unix socket. We need caching_sha2_password for TCP access.
326+
log.Println("configuring mysql root password for TCP access")
327+
if err := run("mysql", "-h", "127.0.0.1", "-u", "root", "-pmysecretpassword", "-e", "SELECT 1;"); err == nil {
328+
log.Println("mysql root password already set to expected value, skipping")
329+
} else {
330+
log.Println("setting mysql root password with caching_sha2_password plugin")
331+
// Try via socket (works when auth_socket is the plugin or password is blank)
332+
if err := run("mysql", "-u", "root", "-e",
333+
"ALTER USER 'root'@'localhost' IDENTIFIED WITH caching_sha2_password BY 'mysecretpassword'; FLUSH PRIVILEGES;"); err != nil {
334+
return fmt.Errorf("setting mysql root password: %w", err)
335+
}
336+
}
337+
338+
return verifyMySQL()
339+
}
340+
341+
// mysqlReady checks if MySQL is running and accepting connections with the expected password.
342+
func mysqlReady() bool {
343+
err := exec.Command("mysqladmin", "-h", "127.0.0.1", "-u", "root", "-pmysecretpassword", "ping").Run()
344+
return err == nil
345+
}
346+
347+
// waitForMySQL polls until MySQL accepts connections or the timeout expires.
348+
func waitForMySQL(timeout time.Duration) error {
349+
deadline := time.Now().Add(timeout)
350+
for time.Now().Before(deadline) {
351+
// Try connecting without password (fresh) or with password (already configured)
352+
if exec.Command("mysqladmin", "-u", "root", "ping").Run() == nil {
353+
return nil
354+
}
355+
if exec.Command("mysqladmin", "-h", "127.0.0.1", "-u", "root", "-pmysecretpassword", "ping").Run() == nil {
356+
return nil
357+
}
358+
time.Sleep(500 * time.Millisecond)
359+
}
360+
return fmt.Errorf("timed out after %s waiting for mysql", timeout)
361+
}
362+
363+
func verifyMySQL() error {
242364
log.Println("verifying mysql connection")
243365
if err := run("mysql", "-h", "127.0.0.1", "-u", "root", "-pmysecretpassword", "-e", "SELECT VERSION();"); err != nil {
244366
return fmt.Errorf("mysql connection test failed: %w", err)
245367
}
246-
247368
log.Println("mysql is running and configured")
248369
return nil
249370
}
371+
372+
// mysqlInitialized checks if the MySQL data directory has been initialized.
373+
func mysqlInitialized() bool {
374+
dataDir := "/var/lib/mysql"
375+
entries, err := filepath.Glob(filepath.Join(dataDir, "*.pem"))
376+
if err != nil {
377+
return false
378+
}
379+
// MySQL creates TLS certificate files during initialization
380+
return len(entries) > 0
381+
}

0 commit comments

Comments
 (0)