Some checks failed
kotlin / test (push) Has been skipped
python / test (push) Has been skipped
typescript / test (push) Has been skipped
go / build darwin/amd64 (push) Has been cancelled
go / build linux/amd64 (push) Has been cancelled
go / build windows/amd64 (push) Has been cancelled
go / build darwin/arm64 (push) Has been cancelled
go / build linux/arm64 (push) Has been cancelled
go / build windows/arm64 (push) Has been cancelled
go / test (push) Has been cancelled
go / vuln_check (push) Has been cancelled
718 lines
23 KiB
Go
718 lines
23 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
// pgVersion is the PostgreSQL version to install.
|
|
pgVersion = "18.2.0"
|
|
)
|
|
|
|
// pgBinary contains the download information for a PostgreSQL binary release.
|
|
type pgBinary struct {
|
|
URL string
|
|
SHA256 string
|
|
}
|
|
|
|
// pgBinaries maps "<GOOS>/<GOARCH>" to the corresponding binary download info.
|
|
var pgBinaries = map[string]pgBinary{
|
|
"linux/amd64": {
|
|
URL: "https://github.com/theseus-rs/postgresql-binaries/releases/download/" + pgVersion + "/postgresql-" + pgVersion + "-x86_64-unknown-linux-gnu.tar.gz",
|
|
SHA256: "cc2674e1641aa2a62b478971a22c131a768eb783f313e6a3385888f58a604074",
|
|
},
|
|
"linux/arm64": {
|
|
URL: "https://github.com/theseus-rs/postgresql-binaries/releases/download/" + pgVersion + "/postgresql-" + pgVersion + "-aarch64-unknown-linux-gnu.tar.gz",
|
|
SHA256: "8b415a11c7a5484e5fbf7a57fca71554d2d1d7acd34faf066606d2fee1261854",
|
|
},
|
|
}
|
|
|
|
func main() {
|
|
log.SetFlags(log.Ltime)
|
|
log.SetPrefix("[sqlc-test-setup] ")
|
|
|
|
if len(os.Args) < 2 {
|
|
fmt.Fprintln(os.Stderr, "usage: sqlc-test-setup <install|start>")
|
|
os.Exit(1)
|
|
}
|
|
|
|
switch os.Args[1] {
|
|
case "install":
|
|
if err := runInstall(); err != nil {
|
|
log.Fatalf("install failed: %s", err)
|
|
}
|
|
case "start":
|
|
if err := runStart(); err != nil {
|
|
log.Fatalf("start failed: %s", err)
|
|
}
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "unknown command: %s\nusage: sqlc-test-setup <install|start>\n", os.Args[1])
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// run executes a command with verbose logging, streaming output to stderr.
|
|
func run(name string, args ...string) error {
|
|
log.Printf("exec: %s %s", name, strings.Join(args, " "))
|
|
cmd := exec.Command(name, args...)
|
|
cmd.Stdout = os.Stderr
|
|
cmd.Stderr = os.Stderr
|
|
cmd.Stdin = os.Stdin
|
|
return cmd.Run()
|
|
}
|
|
|
|
// runOutput executes a command and returns its combined output.
|
|
func runOutput(name string, args ...string) (string, error) {
|
|
log.Printf("exec: %s %s", name, strings.Join(args, " "))
|
|
cmd := exec.Command(name, args...)
|
|
out, err := cmd.CombinedOutput()
|
|
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
|
|
}
|
|
|
|
// The MySQL release the tests run against. Everything that names a MySQL
|
|
// version — the docker-compose service, the goldeneye dialect generator and
|
|
// its gen workflow — is pinned to the same release.
|
|
const (
|
|
mysqlMajor = 26
|
|
mysqlVersion = "26.7.0"
|
|
)
|
|
|
|
// isMySQLVersionOK checks if the mysqld --version output indicates the
|
|
// pinned major release or a later one.
|
|
// Example version string: "/usr/sbin/mysqld Ver 8.0.44-0ubuntu0.24.04.2 ..."
|
|
func isMySQLVersionOK(versionOutput string) bool {
|
|
// Look for "Ver X.Y.Z" pattern
|
|
fields := strings.Fields(versionOutput)
|
|
for i, f := range fields {
|
|
if strings.EqualFold(f, "Ver") && i+1 < len(fields) {
|
|
ver := strings.Split(fields[i+1], ".")
|
|
if len(ver) > 0 {
|
|
major, err := strconv.Atoi(ver[0])
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return major >= mysqlMajor
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// pgBaseDir returns the sqlc-specific directory where PostgreSQL is installed,
|
|
// using the user's cache directory (~/.cache/sqlc/postgresql on Linux).
|
|
func pgBaseDir() string {
|
|
cacheDir, err := os.UserCacheDir()
|
|
if err != nil {
|
|
cacheDir = filepath.Join(os.Getenv("HOME"), ".cache")
|
|
}
|
|
return filepath.Join(cacheDir, "sqlc", "postgresql")
|
|
}
|
|
|
|
// pgBinDir returns the path to the PostgreSQL bin directory.
|
|
func pgBinDir() string {
|
|
return filepath.Join(pgBaseDir(), "bin")
|
|
}
|
|
|
|
// pgDataDir returns the path to the PostgreSQL data directory.
|
|
func pgDataDir() string {
|
|
return filepath.Join(pgBaseDir(), "data")
|
|
}
|
|
|
|
// pgBin returns the full path to a PostgreSQL binary.
|
|
func pgBin(name string) string {
|
|
return filepath.Join(pgBinDir(), name)
|
|
}
|
|
|
|
// ---- install ----
|
|
|
|
func runInstall() error {
|
|
log.Println("=== Installing PostgreSQL and MySQL for test setup ===")
|
|
|
|
if err := installAptProxy(); err != nil {
|
|
return fmt.Errorf("configuring apt proxy: %w", err)
|
|
}
|
|
|
|
if err := installPostgreSQL(); err != nil {
|
|
return fmt.Errorf("installing postgresql: %w", err)
|
|
}
|
|
|
|
if err := installMySQL(); err != nil {
|
|
return fmt.Errorf("installing mysql: %w", err)
|
|
}
|
|
|
|
log.Println("=== Install complete ===")
|
|
return nil
|
|
}
|
|
|
|
func installAptProxy() error {
|
|
proxy := os.Getenv("http_proxy")
|
|
if proxy == "" {
|
|
log.Println("http_proxy is not set, skipping apt proxy configuration")
|
|
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)
|
|
return run("bash", "-c", cmd)
|
|
}
|
|
|
|
func installPostgreSQL() error {
|
|
log.Println("--- Installing PostgreSQL ---")
|
|
|
|
// Install runtime dependencies needed by PostgreSQL extensions (e.g.
|
|
// uuid-ossp requires libossp-uuid16).
|
|
if err := installPgDeps(); err != nil {
|
|
return fmt.Errorf("installing postgresql dependencies: %w", err)
|
|
}
|
|
|
|
// Check if already installed in our directory
|
|
if _, err := os.Stat(pgBin("postgres")); err == nil {
|
|
out, err := runOutput(pgBin("postgres"), "--version")
|
|
if err == nil {
|
|
log.Printf("postgresql is already installed: %s", strings.TrimSpace(out))
|
|
log.Println("skipping postgresql installation")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
platform := runtime.GOOS + "/" + runtime.GOARCH
|
|
bin, ok := pgBinaries[platform]
|
|
if !ok {
|
|
return fmt.Errorf("unsupported platform: %s (supported: %s)", platform, supportedPlatforms())
|
|
}
|
|
|
|
// Download to a temp file
|
|
tarball := filepath.Join(os.TempDir(), fmt.Sprintf("postgresql-%s.tar.gz", pgVersion))
|
|
|
|
if _, err := os.Stat(tarball); err != nil {
|
|
log.Printf("downloading PostgreSQL %s from %s", pgVersion, bin.URL)
|
|
if err := downloadFile(tarball, bin.URL); err != nil {
|
|
os.Remove(tarball)
|
|
return fmt.Errorf("downloading postgresql: %w", err)
|
|
}
|
|
} else {
|
|
log.Printf("postgresql tarball already downloaded at %s", tarball)
|
|
}
|
|
|
|
// Verify SHA256 checksum
|
|
log.Printf("verifying SHA256 checksum")
|
|
actualHash, err := sha256File(tarball)
|
|
if err != nil {
|
|
return fmt.Errorf("computing sha256: %w", err)
|
|
}
|
|
if actualHash != bin.SHA256 {
|
|
os.Remove(tarball)
|
|
return fmt.Errorf("SHA256 mismatch: expected %s, got %s", bin.SHA256, actualHash)
|
|
}
|
|
log.Printf("SHA256 checksum verified: %s", actualHash)
|
|
|
|
baseDir := pgBaseDir()
|
|
|
|
// Create the base directory in the user cache
|
|
if err := os.MkdirAll(baseDir, 0o755); err != nil {
|
|
return fmt.Errorf("creating %s: %w", baseDir, err)
|
|
}
|
|
|
|
// Extract the tarball - it contains a top-level directory like
|
|
// postgresql-18.2.0-x86_64-unknown-linux-gnu/ with bin/, lib/, share/ inside.
|
|
// We strip that top-level directory and extract directly into the base dir.
|
|
log.Printf("extracting postgresql to %s", baseDir)
|
|
if err := run("tar", "-xzf", tarball, "-C", baseDir, "--strip-components=1"); err != nil {
|
|
return fmt.Errorf("extracting postgresql: %w", err)
|
|
}
|
|
|
|
// Verify the binary works
|
|
out, err := runOutput(pgBin("postgres"), "--version")
|
|
if err != nil {
|
|
return fmt.Errorf("postgres --version failed after install: %w", err)
|
|
}
|
|
log.Printf("postgresql installed successfully: %s", strings.TrimSpace(out))
|
|
return nil
|
|
}
|
|
|
|
// installPgDeps installs shared libraries required by PostgreSQL extensions at
|
|
// runtime (e.g. libossp-uuid16 for uuid-ossp).
|
|
func installPgDeps() error {
|
|
log.Println("installing postgresql runtime dependencies")
|
|
if err := run("sudo", "apt-get", "install", "-y", "--no-install-recommends", "libossp-uuid16"); err != nil {
|
|
return fmt.Errorf("apt-get install libossp-uuid16: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// supportedPlatforms returns a comma-separated list of supported platforms.
|
|
func supportedPlatforms() string {
|
|
platforms := make([]string, 0, len(pgBinaries))
|
|
for p := range pgBinaries {
|
|
platforms = append(platforms, p)
|
|
}
|
|
return strings.Join(platforms, ", ")
|
|
}
|
|
|
|
// downloadFile downloads a URL to a local file path.
|
|
func downloadFile(filepath string, url string) error {
|
|
resp, err := http.Get(url)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, resp.Status)
|
|
}
|
|
|
|
out, err := os.Create(filepath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer out.Close()
|
|
|
|
_, err = io.Copy(out, resp.Body)
|
|
return err
|
|
}
|
|
|
|
// sha256File computes the SHA256 hash of a file and returns the hex string.
|
|
func sha256File(path string) (string, error) {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer f.Close()
|
|
|
|
h := sha256.New()
|
|
if _, err := io.Copy(h, f); err != nil {
|
|
return "", err
|
|
}
|
|
return hex.EncodeToString(h.Sum(nil)), nil
|
|
}
|
|
|
|
func installMySQL() error {
|
|
log.Printf("--- Installing MySQL %s ---", mysqlVersion)
|
|
|
|
if commandExists("mysqld") {
|
|
out, err := runOutput("mysqld", "--version")
|
|
if err == nil {
|
|
version := strings.TrimSpace(out)
|
|
log.Printf("mysql is already installed: %s", version)
|
|
if isMySQLVersionOK(version) {
|
|
log.Printf("mysql version is %d+, skipping installation", mysqlMajor)
|
|
return nil
|
|
}
|
|
log.Printf("mysql version is too old, upgrading to MySQL %s", mysqlVersion)
|
|
// Stop existing MySQL before upgrading
|
|
_ = exec.Command("sudo", "service", "mysql", "stop").Run()
|
|
_ = exec.Command("sudo", "pkill", "-f", "mysqld").Run()
|
|
time.Sleep(2 * time.Second)
|
|
// Remove old MySQL packages to avoid conflicts
|
|
log.Println("removing old mysql packages")
|
|
_ = run("sudo", "apt-get", "remove", "-y", "mysql-server", "mysql-client", "mysql-common",
|
|
"mysql-server-core-*", "mysql-client-core-*")
|
|
// Clear old data directory so the new release can initialize fresh
|
|
log.Println("clearing old mysql data directory")
|
|
_ = run("sudo", "rm", "-rf", "/var/lib/mysql")
|
|
_ = run("sudo", "mkdir", "-p", "/var/lib/mysql")
|
|
_ = run("sudo", "chown", "mysql:mysql", "/var/lib/mysql")
|
|
}
|
|
}
|
|
|
|
major, minor, _ := strings.Cut(mysqlVersion, ".")
|
|
minor, _, _ = strings.Cut(minor, ".")
|
|
bundleURL := fmt.Sprintf("https://dev.mysql.com/get/Downloads/MySQL-%s.%s/mysql-server_%s-1ubuntu24.04_amd64.deb-bundle.tar", major, minor, mysqlVersion)
|
|
bundleTar := fmt.Sprintf("/tmp/mysql-server-%s-bundle.tar", mysqlVersion)
|
|
extractDir := "/tmp/mysql-server-" + mysqlVersion
|
|
|
|
if _, err := os.Stat(bundleTar); err != nil {
|
|
log.Printf("downloading MySQL %s bundle from %s", mysqlVersion, 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)
|
|
if err := os.MkdirAll(extractDir, 0o755); err != nil {
|
|
return fmt.Errorf("creating extract dir: %w", err)
|
|
}
|
|
if err := run("tar", "-xf", bundleTar, "-C", extractDir); err != nil {
|
|
return fmt.Errorf("extracting mysql bundle: %w", err)
|
|
}
|
|
|
|
// 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",
|
|
"mysql-community-client-core_*.deb",
|
|
"mysql-community-client_*.deb",
|
|
"mysql-client_*.deb",
|
|
"mysql-community-server-core_*.deb",
|
|
"mysql-community-server_*.deb",
|
|
"mysql-server_*.deb",
|
|
}
|
|
|
|
for _, pkg := range packages {
|
|
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 {
|
|
log.Printf("dpkg reported errors for %s (will fix with apt-get install -f)", pkg)
|
|
}
|
|
}
|
|
|
|
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.Printf("mysql %s installed successfully", mysqlVersion)
|
|
return nil
|
|
}
|
|
|
|
// ---- start ----
|
|
|
|
func runStart() error {
|
|
log.Println("=== Starting PostgreSQL and MySQL ===")
|
|
|
|
if err := startPostgreSQL(); err != nil {
|
|
return fmt.Errorf("starting postgresql: %w", err)
|
|
}
|
|
|
|
if err := startMySQL(); err != nil {
|
|
return fmt.Errorf("starting mysql: %w", err)
|
|
}
|
|
|
|
log.Println("=== Both databases are running and configured ===")
|
|
log.Println("PostgreSQL: postgres://postgres:postgres@127.0.0.1:5432/postgres?sslmode=disable")
|
|
log.Println("MySQL: root:mysecretpassword@tcp(127.0.0.1:3306)/mysql")
|
|
return nil
|
|
}
|
|
|
|
func startPostgreSQL() error {
|
|
log.Println("--- Starting PostgreSQL ---")
|
|
|
|
dataDir := pgDataDir()
|
|
logFile := filepath.Join(pgBaseDir(), "postgresql.log")
|
|
|
|
// Check if already running
|
|
if pgIsReady() {
|
|
log.Println("postgresql is already running and accepting connections")
|
|
return nil
|
|
}
|
|
|
|
// Initialize data directory if needed
|
|
if _, err := os.Stat(filepath.Join(dataDir, "PG_VERSION")); os.IsNotExist(err) {
|
|
log.Println("initializing postgresql data directory")
|
|
if err := os.MkdirAll(dataDir, 0o700); err != nil {
|
|
return fmt.Errorf("creating data directory: %w", err)
|
|
}
|
|
if err := run(pgBin("initdb"),
|
|
"-D", dataDir,
|
|
"--username=postgres",
|
|
"--auth=trust",
|
|
); err != nil {
|
|
return fmt.Errorf("initdb: %w", err)
|
|
}
|
|
|
|
// Configure pg_hba.conf for md5 password authentication on TCP
|
|
hbaPath := filepath.Join(dataDir, "pg_hba.conf")
|
|
if err := configurePgHBA(hbaPath); err != nil {
|
|
return fmt.Errorf("configuring pg_hba.conf: %w", err)
|
|
}
|
|
|
|
// Configure postgresql.conf to listen on localhost
|
|
confPath := filepath.Join(dataDir, "postgresql.conf")
|
|
if err := appendToFile(confPath,
|
|
"\n# sqlc-test-setup configuration\n"+
|
|
"listen_addresses = '127.0.0.1'\n"+
|
|
"port = 5432\n",
|
|
); err != nil {
|
|
return fmt.Errorf("configuring postgresql.conf: %w", err)
|
|
}
|
|
} else {
|
|
log.Println("postgresql data directory already initialized")
|
|
}
|
|
|
|
// Start PostgreSQL using pg_ctl
|
|
log.Println("starting postgresql")
|
|
if err := run(pgBin("pg_ctl"),
|
|
"-D", dataDir,
|
|
"-l", logFile,
|
|
"-o", fmt.Sprintf("-k %s", dataDir),
|
|
"start",
|
|
); err != nil {
|
|
return fmt.Errorf("pg_ctl start: %w", err)
|
|
}
|
|
|
|
// Wait for PostgreSQL to be ready
|
|
log.Println("waiting for postgresql to accept connections")
|
|
if err := waitForPostgreSQL(30 * time.Second); err != nil {
|
|
return fmt.Errorf("postgresql did not start in time: %w", err)
|
|
}
|
|
|
|
// Set the postgres user password
|
|
log.Println("setting password for postgres user")
|
|
if err := run(pgBin("psql"),
|
|
"-h", "127.0.0.1",
|
|
"-U", "postgres",
|
|
"-c", "ALTER USER postgres PASSWORD 'postgres';",
|
|
); err != nil {
|
|
return fmt.Errorf("setting postgres password: %w", err)
|
|
}
|
|
|
|
// Update pg_hba.conf to require md5 auth now that password is set
|
|
hbaPath := filepath.Join(dataDir, "pg_hba.conf")
|
|
if err := configurePgHBAWithMD5(hbaPath); err != nil {
|
|
return fmt.Errorf("updating pg_hba.conf for md5: %w", err)
|
|
}
|
|
|
|
// Reload configuration
|
|
log.Println("reloading postgresql configuration")
|
|
if err := run(pgBin("pg_ctl"), "-D", dataDir, "reload"); err != nil {
|
|
return fmt.Errorf("pg_ctl reload: %w", err)
|
|
}
|
|
|
|
// Verify connection with password
|
|
log.Println("verifying postgresql connection")
|
|
cmd := exec.Command(pgBin("psql"),
|
|
"-h", "127.0.0.1",
|
|
"-U", "postgres",
|
|
"-c", "SELECT 1;",
|
|
)
|
|
cmd.Env = append(os.Environ(), "PGPASSWORD=postgres")
|
|
cmd.Stdout = os.Stderr
|
|
cmd.Stderr = os.Stderr
|
|
if err := cmd.Run(); err != nil {
|
|
return fmt.Errorf("postgresql connection test failed: %w", err)
|
|
}
|
|
|
|
log.Println("postgresql is running and configured")
|
|
return nil
|
|
}
|
|
|
|
// configurePgHBA writes a pg_hba.conf that allows trust auth initially (for
|
|
// setting the password), then we switch to md5.
|
|
func configurePgHBA(hbaPath string) error {
|
|
content := `# pg_hba.conf - generated by sqlc-test-setup
|
|
# TYPE DATABASE USER ADDRESS METHOD
|
|
local all all trust
|
|
host all all 127.0.0.1/32 trust
|
|
host all all ::1/128 trust
|
|
`
|
|
return os.WriteFile(hbaPath, []byte(content), 0o600)
|
|
}
|
|
|
|
// configurePgHBAWithMD5 rewrites pg_hba.conf to use md5 for TCP connections.
|
|
func configurePgHBAWithMD5(hbaPath string) error {
|
|
content := `# pg_hba.conf - generated by sqlc-test-setup
|
|
# TYPE DATABASE USER ADDRESS METHOD
|
|
local all all trust
|
|
host all all 127.0.0.1/32 md5
|
|
host all all ::1/128 md5
|
|
`
|
|
return os.WriteFile(hbaPath, []byte(content), 0o600)
|
|
}
|
|
|
|
// appendToFile appends text to a file.
|
|
func appendToFile(path, text string) error {
|
|
f, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0o644)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
_, err = f.WriteString(text)
|
|
return err
|
|
}
|
|
|
|
// pgIsReady checks if PostgreSQL is running and accepting connections.
|
|
func pgIsReady() bool {
|
|
cmd := exec.Command(pgBin("pg_isready"), "-h", "127.0.0.1", "-p", "5432")
|
|
return cmd.Run() == nil
|
|
}
|
|
|
|
// waitForPostgreSQL polls until PostgreSQL accepts connections or times out.
|
|
func waitForPostgreSQL(timeout time.Duration) error {
|
|
deadline := time.Now().Add(timeout)
|
|
for time.Now().Before(deadline) {
|
|
if pgIsReady() {
|
|
return nil
|
|
}
|
|
time.Sleep(500 * time.Millisecond)
|
|
}
|
|
return fmt.Errorf("timed out after %s waiting for postgresql", timeout)
|
|
}
|
|
|
|
func startMySQL() error {
|
|
log.Println("--- Starting MySQL ---")
|
|
|
|
// 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()
|
|
}
|
|
|
|
// Stop any existing MySQL service that might be running (e.g. pre-installed
|
|
// on GitHub Actions runners) to avoid port conflicts.
|
|
log.Println("stopping any existing mysql service")
|
|
_ = exec.Command("sudo", "service", "mysql", "stop").Run()
|
|
_ = exec.Command("sudo", "mysqladmin", "shutdown").Run()
|
|
// Give MySQL time to fully shut down
|
|
time.Sleep(2 * time.Second)
|
|
|
|
if err := ensureMySQLDirs(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Check if data directory already exists and has been initialized
|
|
needsPasswordReset := false
|
|
if mysqlInitialized() {
|
|
log.Println("mysql data directory already initialized, skipping initialization")
|
|
// Existing data dir may have an unknown root password (e.g. pre-installed
|
|
// MySQL on GitHub Actions). We'll need to use --skip-grant-tables to reset it.
|
|
needsPasswordReset = true
|
|
} 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)
|
|
}
|
|
}
|
|
|
|
if needsPasswordReset {
|
|
// Start with --skip-grant-tables to reset the unknown root password.
|
|
if err := startMySQLDaemon("--skip-grant-tables"); err != nil {
|
|
return err
|
|
}
|
|
|
|
log.Println("resetting root password via --skip-grant-tables")
|
|
resetSQL := "FLUSH PRIVILEGES; ALTER USER 'root'@'localhost' IDENTIFIED WITH caching_sha2_password BY 'mysecretpassword';"
|
|
if err := run("mysql", "-u", "root", "-e", resetSQL); err != nil {
|
|
return fmt.Errorf("resetting mysql root password: %w", err)
|
|
}
|
|
|
|
// Restart without --skip-grant-tables
|
|
log.Println("restarting mysql normally")
|
|
if err := run("sudo", "mysqladmin", "-u", "root", "-pmysecretpassword", "shutdown"); err != nil {
|
|
// If mysqladmin fails, try killing the process directly
|
|
_ = run("sudo", "pkill", "-f", "mysqld")
|
|
}
|
|
time.Sleep(2 * time.Second)
|
|
|
|
if err := startMySQLDaemon(); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
// Fresh initialization — start normally and set password
|
|
if err := startMySQLDaemon(); err != nil {
|
|
return err
|
|
}
|
|
|
|
log.Println("setting mysql root password")
|
|
alterSQL := "ALTER USER 'root'@'localhost' IDENTIFIED WITH caching_sha2_password BY 'mysecretpassword'; FLUSH PRIVILEGES;"
|
|
if err := run("mysql", "-u", "root", "-e", alterSQL); err != nil {
|
|
return fmt.Errorf("setting mysql root password: %w", err)
|
|
}
|
|
}
|
|
|
|
return verifyMySQL()
|
|
}
|
|
|
|
// ensureMySQLDirs creates the directories MySQL needs at runtime.
|
|
func ensureMySQLDirs() error {
|
|
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)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// startMySQLDaemon starts mysqld_safe in the background and waits for it to
|
|
// accept connections. Extra args (e.g. "--skip-grant-tables") are appended.
|
|
func startMySQLDaemon(extraArgs ...string) error {
|
|
args := append([]string{"mysqld_safe", "--user=mysql"}, extraArgs...)
|
|
log.Printf("starting mysql via mysqld_safe %v", extraArgs)
|
|
cmd := exec.Command("sudo", args...)
|
|
cmd.Stdout = os.Stderr
|
|
cmd.Stderr = os.Stderr
|
|
if err := cmd.Start(); err != nil {
|
|
return fmt.Errorf("starting mysqld_safe: %w", err)
|
|
}
|
|
|
|
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")
|
|
return nil
|
|
}
|
|
|
|
// 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.
|
|
// We use sudo ls because /var/lib/mysql is typically only readable by the
|
|
// mysql user, so filepath.Glob from a non-root process would silently fail.
|
|
func mysqlInitialized() bool {
|
|
out, err := exec.Command("sudo", "ls", "/var/lib/mysql").CombinedOutput()
|
|
if err != nil {
|
|
return false
|
|
}
|
|
// If the directory has any contents, consider it initialized.
|
|
// mysqld --initialize-insecure requires an empty directory.
|
|
return strings.TrimSpace(string(out)) != ""
|
|
}
|