package main

// golang program for getting mysql base backup + replication command for creation of replica

import (
"database/sql"
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"time"

"strings"

"path/filepath"

"regexp"

_ "github.com/go-sql-driver/mysql"
)

func main() {

fmt.Println(curTime() + "Starting master backup for replication")

masterIP := requireEnvVar("MASTER_IP")                // for base backup and replication command - IP of master mysql database
masterPort := requireEnvVar("MASTER_PORT")            // for base backup and replication command - port of master mysql database
masterUser := requireEnvVar("MASTER_USER")            // only for replication command - replication user name with permitions on master mysql database
masterPass := requireEnvVar("MASTER_PASS")            // only for replication command - password of replication user name with permitions on master mysql database
dbName := requireEnvVar("DB_NAME")                    // for base backup - name of the master database
dbUser := requireEnvVar("DB_USER")                    // for base backup - login into master mysql database
dbPass := requireEnvVar("DB_PASS")                    // for base backup - password into master mysql database
dbDumpDir := requireEnvVar("DB_DUMP_DIR")             // for base backup - directory for backup file
dumpFileName := requireEnvVar("MASTER_DUMP_FILENAME") // for base backup - name of dump file
cmdFileName := requireEnvVar("MASTER_CMD_FILENAME")   // for base backup - name of command file (contains replication command for given dump file)
gsBucket := requireEnvVar("GS_REPLICA_BUCKET")        // for base backup - Google storage bucket for storing dump file and command file

// open connection into mysql
db, err := sql.Open("mysql", dbUser+":"+dbPass+"@tcp("+masterIP+":"+masterPort+")/"+dbName)
if err != nil {
log.Fatal(curTime()+"Cannot connect into MySQL", err)
}

// defer will close connection at the end
defer db.Close()

var mysqlTemp string
fmt.Println(curTime() + "checking mysql version")
err = db.QueryRow("SELECT version()").Scan(&mysqlTemp)
if err != nil {
fmt.Println(err)
log.Fatal(curTime() + "Cannot check MySQL version")
}
mysqlVersion := strings.Split(mysqlTemp, "-")[0]
fmt.Println(mysqlVersion)

fmt.Println(curTime() + "locking tables on master")
_, err = db.Query("FLUSH TABLES WITH READ LOCK;")
if err != nil {
fmt.Println(err)
log.Fatal(curTime() + "Cannot do FLUSH TABLES WITH READ LOCK;")
}

fmt.Println(curTime() + "checking master status")
var masterLogFileName, masterLogFilePos, masterDbName, col4 string
err = db.QueryRow("SHOW MASTER STATUS").Scan(&masterLogFileName, &masterLogFilePos, &masterDbName, &col4)
if err != nil {
log.Fatal(curTime() + "Cannot check MySQL master status")
}

// check results if the match expected values
matched, _ := regexp.MatchString("^mysql-bin[.][\\d]{3,}$", masterLogFileName)
if !matched {
log.Fatal(curTime() + "Column masterLogFileName does not have expected value (" + masterLogFileName + ")")
}

matched, _ = regexp.MatchString("^[\\d]{1,}$", masterLogFilePos)
if !matched {
log.Fatal(curTime() + "Column masterLogFilePos does not have expected value (" + masterLogFilePos + ")")
}

fmt.Println(curTime() + "Log file name:" + masterLogFileName + ", Position: " + masterLogFilePos + ", DbName: " + masterDbName)

replicationCommand := "CHANGE MASTER TO MASTER_HOST='" + masterIP +
"', MASTER_PORT=" + masterPort + ", MASTER_USER='" + masterUser +
"', MASTER_PASSWORD='" + masterPass +
"', MASTER_LOG_FILE='" + masterLogFileName +
"', MASTER_LOG_POS=" + masterLogFilePos + ";"
fmt.Println(curTime() + "replication command: " + replicationCommand)

matched, _ = regexp.MatchString(".*[.]gz$", dumpFileName)
if !matched {
dumpFileName += ".gz"
}

cmdFileFullName := filepath.Join(dbDumpDir, cmdFileName)
dumpFileFullName := filepath.Join(dbDumpDir, dumpFileName)
fmt.Println(curTime()+"command file name:", cmdFileFullName)
fmt.Println(curTime()+"dump file name:", dumpFileFullName)

fmt.Println(curTime() + "creating command file")
err = ioutil.WriteFile(cmdFileFullName, []byte("-- master version: "+mysqlVersion+"\n"+replicationCommand), 0600)
if err != nil {
log.Fatal(curTime() + "Cannot write command file")
}

fmt.Println(curTime() + "starting backup")

dumpCmd := "mysqldump -u root -p" + dbPass + " --opt " + dbName + " --single-transaction --routines --triggers | gzip > " + dumpFileFullName
runExternalCmd(dumpCmd)

fmt.Println(curTime() + "removing table lock")
_, _ = db.Query("UNLOCK TABLES;")
//here we do not check for errors - connection will be closed anyway
db.Close()

toBucket := gsBucket
matched, _ = regexp.MatchString("^gs://.*", gsBucket)
if !matched {
toBucket = " gs://" + gsBucket
}
matched, _ = regexp.MatchString(".*/$", toBucket)
if !matched {
toBucket += "/"
}

deleteFromGs(cmdFileName, toBucket)
deleteFromGs(dumpFileName, toBucket)

copyToGs(cmdFileFullName, toBucket)
copyToGs(dumpFileFullName, toBucket)

fmt.Println(curTime() + "DONE OK")
}

func runExternalCmd(commandToRun string) {
fmt.Println(curTime()+"running command:", commandToRun)
cmd := exec.Command("bash", "-c", commandToRun)
err := cmd.Start()
if err != nil {
fmt.Println(err)
log.Fatal(curTime() + "Cannot start external program")
}
fmt.Println(curTime() + "Waiting for command to finish...")
err = cmd.Wait()
if err != nil {
fmt.Println(err)
log.Fatal(curTime()+"Command finished with error: ", err)
}

}

func deleteFromGs(filename string, gsbucket string) {
fmt.Println(curTime() + "Deleting " + filename + " from Google bucket " + gsbucket)
delCmd := "gsutil -m -q rm " + gsbucket + filename
runExternalCmd(delCmd)
}

func copyToGs(localfullfilename string, gsbucket string) {
fmt.Println(curTime() + "Copying " + localfullfilename + " to the Google bucket " + gsbucket)
copyCmd := "gsutil -q -o GSUtil:parallel_composite_upload_threshold=10M cp " + localfullfilename + " " + gsbucket
runExternalCmd(copyCmd)
}

func curTime() string {
return time.Now().Format(time.RFC3339Nano) + ": "
}

func requireEnvVar(s string) string {
env, ok := os.LookupEnv(s)
if !ok {
log.Fatalln(curTime(), "ERROR: variable ", s, " isn't defined")
}
return (env)
}