-
Notifications
You must be signed in to change notification settings - Fork 1k
Expand file tree
/
Copy pathmysql.go
More file actions
203 lines (179 loc) · 5.54 KB
/
mysql.go
File metadata and controls
203 lines (179 loc) · 5.54 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
package native
import (
"context"
"database/sql"
"fmt"
"log/slog"
"os/exec"
"time"
_ "github.com/go-sql-driver/mysql"
"golang.org/x/sync/singleflight"
)
var mysqlFlight singleflight.Group
var mysqlURI string
// StartMySQLServer starts an existing MySQL installation natively (without Docker).
func StartMySQLServer(ctx context.Context) (string, error) {
if err := Supported(); err != nil {
return "", err
}
if mysqlURI != "" {
return mysqlURI, nil
}
value, err, _ := mysqlFlight.Do("mysql", func() (interface{}, error) {
uri, err := startMySQLServer(ctx)
if err != nil {
return "", err
}
mysqlURI = uri
return uri, nil
})
if err != nil {
return "", err
}
data, ok := value.(string)
if !ok {
return "", fmt.Errorf("returned value was not a string")
}
return data, nil
}
func startMySQLServer(ctx context.Context) (string, error) {
// Standard URI for test MySQL
uri := "root:mysecretpassword@tcp(localhost:3306)/mysql?multiStatements=true&parseTime=true"
// Try to connect first - it might already be running
if err := waitForMySQL(ctx, uri, 500*time.Millisecond); err == nil {
slog.Info("native/mysql", "status", "already running")
return uri, nil
}
// Also try without password (default MySQL installation)
uriNoPassword := "root@tcp(localhost:3306)/mysql?multiStatements=true&parseTime=true"
if err := waitForMySQL(ctx, uriNoPassword, 500*time.Millisecond); err == nil {
slog.Info("native/mysql", "status", "already running (no password)")
// MySQL is running without password, try to set one
if err := setMySQLPassword(ctx); err != nil {
slog.Debug("native/mysql", "set-password-error", err)
// Return without password if we can't set one
return uriNoPassword, nil
}
// Try again with password
if err := waitForMySQL(ctx, uri, 1*time.Second); err == nil {
return uri, nil
}
// If password didn't work, use no password
return uriNoPassword, nil
}
// Try to start existing MySQL service (might be installed but not running)
if _, err := exec.LookPath("mysqld"); err == nil {
slog.Info("native/mysql", "status", "starting existing service")
if err := startMySQLService(); err != nil {
slog.Debug("native/mysql", "start-error", err)
} else {
// Wait for MySQL to be ready
waitCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
// Try with password first
if err := waitForMySQL(waitCtx, uri, 15*time.Second); err == nil {
return uri, nil
}
// Try without password
if err := waitForMySQL(waitCtx, uriNoPassword, 15*time.Second); err == nil {
if err := setMySQLPassword(ctx); err != nil {
slog.Debug("native/mysql", "set-password-error", err)
return uriNoPassword, nil
}
if err := waitForMySQL(ctx, uri, 1*time.Second); err == nil {
return uri, nil
}
return uriNoPassword, nil
}
}
}
return "", fmt.Errorf("MySQL is not installed or could not be started")
}
func startMySQLService() error {
// Try systemctl first
cmd := exec.Command("sudo", "systemctl", "start", "mysql")
if err := cmd.Run(); err == nil {
// Give MySQL time to fully initialize
time.Sleep(2 * time.Second)
return nil
}
// Try mysqld
cmd = exec.Command("sudo", "systemctl", "start", "mysqld")
if err := cmd.Run(); err == nil {
time.Sleep(2 * time.Second)
return nil
}
// Try service command
cmd = exec.Command("sudo", "service", "mysql", "start")
if err := cmd.Run(); err == nil {
time.Sleep(2 * time.Second)
return nil
}
cmd = exec.Command("sudo", "service", "mysqld", "start")
if err := cmd.Run(); err == nil {
time.Sleep(2 * time.Second)
return nil
}
return fmt.Errorf("could not start MySQL service")
}
func setMySQLPassword(ctx context.Context) error {
// Connect without password
db, err := sql.Open("mysql", "root@tcp(localhost:3306)/mysql")
if err != nil {
return err
}
defer db.Close()
// Set root password using mysql_native_password for broader compatibility
_, err = db.ExecContext(ctx, "ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'mysecretpassword';")
if err != nil {
// Try without specifying auth plugin
_, err = db.ExecContext(ctx, "ALTER USER 'root'@'localhost' IDENTIFIED BY 'mysecretpassword';")
if err != nil {
// Try older MySQL syntax
_, err = db.ExecContext(ctx, "SET PASSWORD FOR 'root'@'localhost' = PASSWORD('mysecretpassword');")
if err != nil {
return fmt.Errorf("could not set MySQL password: %w", err)
}
}
}
// Flush privileges
_, _ = db.ExecContext(ctx, "FLUSH PRIVILEGES;")
return nil
}
func waitForMySQL(ctx context.Context, uri string, timeout time.Duration) error {
deadline := time.Now().Add(timeout)
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop()
// Make an immediate first attempt before waiting for the ticker
if err := tryMySQLConnection(ctx, uri); err == nil {
return nil
}
var lastErr error
for {
select {
case <-ctx.Done():
return fmt.Errorf("context cancelled: %w (last error: %v)", ctx.Err(), lastErr)
case <-ticker.C:
if time.Now().After(deadline) {
return fmt.Errorf("timeout waiting for MySQL (last error: %v)", lastErr)
}
if err := tryMySQLConnection(ctx, uri); err != nil {
lastErr = err
continue
}
return nil
}
}
}
func tryMySQLConnection(ctx context.Context, uri string) error {
db, err := sql.Open("mysql", uri)
if err != nil {
slog.Debug("native/mysql", "open-attempt", err)
return err
}
defer db.Close()
// Use a short timeout for ping to avoid hanging
pingCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
return db.PingContext(pingCtx)
}