55 "log"
66 "os"
77 "os/exec"
8+ "path/filepath"
89 "strings"
10+ "time"
911)
1012
1113func 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+
5363func 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 {
85101func 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 {
102127func 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+
152193func 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+
223280func 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