206 lines
5.9 KiB
Go
206 lines
5.9 KiB
Go
package env
|
|
|
|
import (
|
|
"fmt"
|
|
"io/fs"
|
|
"time"
|
|
|
|
"github.com/FAU-CDI/wisski-distillery/internal/bookkeeping"
|
|
"github.com/FAU-CDI/wisski-distillery/internal/logging"
|
|
"github.com/FAU-CDI/wisski-distillery/internal/sqle"
|
|
"github.com/FAU-CDI/wisski-distillery/internal/stack"
|
|
"github.com/FAU-CDI/wisski-distillery/internal/wait"
|
|
"github.com/pkg/errors"
|
|
"github.com/tkw1536/goprogram/stream"
|
|
"gorm.io/driver/mysql"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/logger"
|
|
)
|
|
|
|
// SQLStack returns the docker stack that handles the sql database.
|
|
func (dis *Distillery) SQLStack() stack.Installable {
|
|
return dis.asCoreStack("sql", stack.Installable{
|
|
MakeDirsPerm: fs.ModeDir | fs.ModePerm,
|
|
MakeDirs: []string{
|
|
"data",
|
|
},
|
|
})
|
|
}
|
|
|
|
// SQLStackPath returns the path the SQLStack() lives at.
|
|
func (dis *Distillery) SQLStackPath() string {
|
|
return dis.SQLStack().Dir
|
|
}
|
|
|
|
// sqlOpen opens a new sql connection to the provided database using the administrative credentials
|
|
func (env Distillery) sqlOpen(database string, config *gorm.Config) (*gorm.DB, error) {
|
|
sql := mysql.Config{
|
|
DSN: fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8&parseTime=True&loc=Local", env.Config.MysqlAdminUser, env.Config.MysqlAdminPassword, "127.0.0.1:3306", database),
|
|
DefaultStringSize: 256,
|
|
}
|
|
|
|
db, err := gorm.Open(mysql.New(sql), config)
|
|
if err != nil {
|
|
return db, err
|
|
}
|
|
|
|
gdb, err := db.DB()
|
|
if err != nil {
|
|
return db, err
|
|
}
|
|
gdb.SetMaxIdleConns(0)
|
|
|
|
return db, nil
|
|
}
|
|
|
|
// sqlBkTable returns a gorm connection to the bookkeeping database.
|
|
func (dis *Distillery) sqlBkTable(silent bool) (*gorm.DB, error) {
|
|
|
|
config := &gorm.Config{}
|
|
if silent {
|
|
config.Logger = logger.Default.LogMode(logger.Silent)
|
|
}
|
|
|
|
// open the database
|
|
db, err := dis.sqlOpen(dis.Config.DistilleryBookkeepingDatabase, config)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// load the table
|
|
table := db.Table(dis.Config.DistilleryBookkeepingTable)
|
|
if table.Error != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return table, nil
|
|
}
|
|
|
|
// SQLShell executes a mysql shell inside the SQLStack.
|
|
func (dis *Distillery) SQLShell(io stream.IOStream, argv ...string) (int, error) {
|
|
return dis.SQLStack().Exec(io, "sql", "mysql", argv...)
|
|
}
|
|
|
|
const waitSQLInterval = 1 * time.Second
|
|
|
|
// SQLWaitForShell waits for the sql database to be reachable via a docker-compose shell
|
|
func (dis *Distillery) SQLWaitForShell() error {
|
|
n := stream.FromNil()
|
|
return wait.Wait(func() bool {
|
|
code, err := dis.SQLShell(n, "-e", "show databases;")
|
|
return err == nil && code == 0
|
|
}, waitSQLInterval, dis.Context())
|
|
}
|
|
|
|
// SQLWaitForConnection waits for the sql connection to be alive
|
|
func (dis *Distillery) SQLWaitForConnection() error {
|
|
return wait.Wait(func() bool {
|
|
_, err := dis.sqlBkTable(true)
|
|
return err == nil
|
|
}, waitSQLInterval, dis.Context())
|
|
}
|
|
|
|
var errInvalidDatabaseName = errors.New("SQLProvision: Invalid database name")
|
|
|
|
func (dis *Distillery) sqlRaw(query string, args ...interface{}) bool {
|
|
sql := sqle.Format(query, args...)
|
|
code, err := dis.SQLShell(stream.FromNil(), "-e", sql)
|
|
return err == nil && code == 0
|
|
}
|
|
|
|
// SQLProvision provisions a new sql database and user
|
|
func (dis *Distillery) SQLProvision(name, user, password string) error {
|
|
// wait for the database
|
|
if err := dis.SQLWaitForShell(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// it's not a safe database name!
|
|
if !sqle.IsSafeDatabaseName(name) {
|
|
return errInvalidDatabaseName
|
|
}
|
|
|
|
// create the database and user!
|
|
if !dis.sqlRaw("CREATE DATABASE `"+name+"`; CREATE USER ?@`%` IDENTIFIED BY ?; GRANT ALL PRIVILEGES ON `"+name+"`.* TO ?@`%`; FLUSH PRIVILEGES;", user, password, user) {
|
|
return errors.New("SQLProvision: Failed to create user")
|
|
}
|
|
|
|
// and done!
|
|
return nil
|
|
}
|
|
|
|
var errSQLPurgeUser = errors.New("unable to delete user")
|
|
|
|
// SQLPurgeUser deletes the specified user from the database
|
|
func (dis *Distillery) SQLPurgeUser(user string) error {
|
|
if !dis.sqlRaw("DROP USER IF EXISTS ?@`%`; FLUSH PRIVILEGES; ", user) {
|
|
return errSQLPurgeUser
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
var errSQLPurgeDB = errors.New("unable to drop database")
|
|
|
|
// SQLPurgeDatabase deletes the specified db from the database
|
|
func (dis *Distillery) SQLPurgeDatabase(db string) error {
|
|
if !sqle.IsSafeDatabaseName(db) {
|
|
return errSQLPurgeDB
|
|
}
|
|
if !dis.sqlRaw("DROP DATABASE IF EXISTS `" + db + "`") {
|
|
return errSQLPurgeDB
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var errSQLUnableToCreateUser = errors.New("unable to create administrative user")
|
|
var errSQLUnsafeDatabaseName = errors.New("Bookkeeping database has an unsafe name")
|
|
var errSQLUnableToCreate = errors.New("unable to create bookkeeping database")
|
|
|
|
// SQLBootstrap bootstraps the SQL database, and makes sure that the bookkeeping table is up-to-date
|
|
func (dis *Distillery) SQLBootstrap(io stream.IOStream) error {
|
|
if err := dis.SQLWaitForShell(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// create the admin user
|
|
logging.LogMessage(io, "Creating administrative user")
|
|
{
|
|
username := dis.Config.MysqlAdminUser
|
|
password := dis.Config.MysqlAdminPassword
|
|
if !dis.sqlRaw("CREATE USER IF NOT EXISTS ?@'%' IDENTIFIED BY ?; GRANT ALL PRIVILEGES ON *.* TO ?@`%` WITH GRANT OPTION; FLUSH PRIVILEGES;", username, password, username) {
|
|
return errSQLUnableToCreateUser
|
|
}
|
|
}
|
|
|
|
// create the admin user
|
|
logging.LogMessage(io, "Creating sql database")
|
|
{
|
|
if !sqle.IsSafeDatabaseName(dis.Config.DistilleryBookkeepingDatabase) {
|
|
return errSQLUnsafeDatabaseName
|
|
}
|
|
createDBSQL := fmt.Sprintf("CREATE DATABASE IF NOT EXISTS `%s`;", dis.Config.DistilleryBookkeepingDatabase)
|
|
if !dis.sqlRaw(createDBSQL) {
|
|
return errSQLUnableToCreate
|
|
}
|
|
}
|
|
|
|
// wait for the database to come up
|
|
logging.LogMessage(io, "Waiting for database update to be complete")
|
|
dis.SQLWaitForConnection()
|
|
|
|
// open the database
|
|
logging.LogMessage(io, "Migrating bookkeeping table")
|
|
{
|
|
db, err := dis.sqlBkTable(false)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to access bookkeeping table: %s", err)
|
|
}
|
|
|
|
if err := db.AutoMigrate(&bookkeeping.Instance{}); err != nil {
|
|
return fmt.Errorf("unable to migrate bookkeeping table: %s", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|