env: Move each component into a separate struct

This commit cleans up the distillery code by making each component a
distinct struct. Each of these components is also returned by by a new
Component() function that replaces the Stacks() function.
This commit is contained in:
Tom Wiesing 2022-09-05 15:50:23 +02:00
parent 2a14d93d3c
commit 09431c4869
No known key found for this signature in database
16 changed files with 265 additions and 148 deletions

118
env/stack_sql.go vendored
View file

@ -18,9 +18,29 @@ import (
"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{
// SQLComponent represents the 'sql' layer belonging to a distillery
type SQLComponent struct {
PollInterval time.Duration // Duration to wait for during wait
dis *Distillery
}
// SSH returns the SSHComponent belonging to this distillery
func (dis *Distillery) SQL() SQLComponent {
return SQLComponent{
PollInterval: time.Second,
dis: dis,
}
}
func (SQLComponent) Name() string {
return "sql"
}
// Stack returns the docker stack that handles the sql database.
func (sql SQLComponent) Stack() stack.Installable {
return sql.dis.makeComponentStack(sql, stack.Installable{
MakeDirsPerm: fs.ModeDir | fs.ModePerm,
MakeDirs: []string{
"data",
@ -29,18 +49,18 @@ func (dis *Distillery) SQLStack() stack.Installable {
}
// SQLStackPath returns the path the SQLStack() lives at.
func (dis *Distillery) SQLStackPath() string {
return dis.SQLStack().Dir
func (sql SQLComponent) Path() string {
return sql.Stack().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),
func (sql SQLComponent) openDatabase(database string, config *gorm.Config) (*gorm.DB, error) {
cfg := mysql.Config{
DSN: fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8&parseTime=True&loc=Local", sql.dis.Config.MysqlAdminUser, sql.dis.Config.MysqlAdminPassword, "127.0.0.1:3306", database),
DefaultStringSize: 256,
}
db, err := gorm.Open(mysql.New(sql), config)
db, err := gorm.Open(mysql.New(cfg), config)
if err != nil {
return db, err
}
@ -54,8 +74,8 @@ func (env Distillery) sqlOpen(database string, config *gorm.Config) (*gorm.DB, e
return db, nil
}
// sqlBkTable returns a gorm connection to the bookkeeping database.
func (dis *Distillery) sqlBkTable(silent bool) (*gorm.DB, error) {
// OpenBookkeeping opens a connection to the bookkeeping database
func (sql SQLComponent) OpenBookkeeping(silent bool) (*gorm.DB, error) {
config := &gorm.Config{}
if silent {
@ -63,13 +83,13 @@ func (dis *Distillery) sqlBkTable(silent bool) (*gorm.DB, error) {
}
// open the database
db, err := dis.sqlOpen(dis.Config.DistilleryBookkeepingDatabase, config)
db, err := sql.openDatabase(sql.dis.Config.DistilleryBookkeepingDatabase, config)
if err != nil {
return nil, err
}
// load the table
table := db.Table(dis.Config.DistilleryBookkeepingTable)
table := db.Table(sql.dis.Config.DistilleryBookkeepingTable)
if table.Error != nil {
return nil, err
}
@ -79,11 +99,11 @@ func (dis *Distillery) sqlBkTable(silent bool) (*gorm.DB, error) {
var errSQLBackup = errors.New("SQLBackup: Mysqldump returned non-zero exit code")
// SQLBackup makes a backup of the sql database into dest.
func (dis *Distillery) SQLBackup(io stream.IOStream, dest io.Writer, database string) error {
// Backup makes a backup of the sql database into dest.
func (sql SQLComponent) Backup(io stream.IOStream, dest io.Writer, database string) error {
io = stream.NewIOStream(dest, io.Stderr, nil, 0)
code, err := dis.SQLStack().Exec(io, "sql", "mysqldump", "--database", database)
code, err := sql.Stack().Exec(io, "sql", "mysqldump", "--database", database)
if err != nil {
return err
}
@ -93,42 +113,40 @@ func (dis *Distillery) SQLBackup(io stream.IOStream, dest io.Writer, database st
return 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...)
// OpenShell executes a mysql shell command
func (sql SQLComponent) OpenShell(io stream.IOStream, argv ...string) (int, error) {
return sql.Stack().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 {
// WaitShell waits for the sql database to be reachable via a docker-compose shell
func (sql SQLComponent) WaitShell() error {
n := stream.FromNil()
return wait.Wait(func() bool {
code, err := dis.SQLShell(n, "-e", "show databases;")
code, err := sql.OpenShell(n, "-e", "show databases;")
return err == nil && code == 0
}, waitSQLInterval, dis.Context())
}, sql.PollInterval, sql.dis.Context())
}
// SQLWaitForConnection waits for the sql connection to be alive
func (dis *Distillery) SQLWaitForConnection() error {
// Wait waits for a connection to the bookkeeping table to suceed
func (sql SQLComponent) Wait() error {
return wait.Wait(func() bool {
_, err := dis.sqlBkTable(true)
_, err := sql.OpenBookkeeping(true)
return err == nil
}, waitSQLInterval, dis.Context())
}, sql.PollInterval, sql.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)
func (sql SQLComponent) Query(query string, args ...interface{}) bool {
raw := sqle.Format(query, args...)
code, err := sql.OpenShell(stream.FromNil(), "-e", raw)
return err == nil && code == 0
}
// SQLProvision provisions a new sql database and user
func (dis *Distillery) SQLProvision(name, user, password string) error {
func (sql SQLComponent) Provision(name, user, password string) error {
// wait for the database
if err := dis.SQLWaitForShell(); err != nil {
if err := sql.WaitShell(); err != nil {
return err
}
@ -138,7 +156,7 @@ func (dis *Distillery) SQLProvision(name, user, password string) error {
}
// 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) {
if !sql.Query("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")
}
@ -149,8 +167,8 @@ func (dis *Distillery) SQLProvision(name, user, password string) error {
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) {
func (sql SQLComponent) PurgeUser(user string) error {
if !sql.Query("DROP USER IF EXISTS ?@`%`; FLUSH PRIVILEGES; ", user) {
return errSQLPurgeUser
}
@ -160,11 +178,11 @@ func (dis *Distillery) SQLPurgeUser(user string) error {
var errSQLPurgeDB = errors.New("unable to drop database")
// SQLPurgeDatabase deletes the specified db from the database
func (dis *Distillery) SQLPurgeDatabase(db string) error {
func (sql SQLComponent) PurgeDatabase(db string) error {
if !sqle.IsSafeDatabaseName(db) {
return errSQLPurgeDB
}
if !dis.sqlRaw("DROP DATABASE IF EXISTS `" + db + "`") {
if !sql.Query("DROP DATABASE IF EXISTS `" + db + "`") {
return errSQLPurgeDB
}
return nil
@ -174,18 +192,18 @@ 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 {
// Bootstrap bootstraps the SQL database, and makes sure that the bookkeeping table is up-to-date
func (sql SQLComponent) Bootstrap(io stream.IOStream) error {
if err := sql.WaitShell(); 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) {
username := sql.dis.Config.MysqlAdminUser
password := sql.dis.Config.MysqlAdminPassword
if !sql.Query("CREATE USER IF NOT EXISTS ?@'%' IDENTIFIED BY ?; GRANT ALL PRIVILEGES ON *.* TO ?@`%` WITH GRANT OPTION; FLUSH PRIVILEGES;", username, password, username) {
return errSQLUnableToCreateUser
}
}
@ -193,23 +211,23 @@ func (dis *Distillery) SQLBootstrap(io stream.IOStream) error {
// create the admin user
logging.LogMessage(io, "Creating sql database")
{
if !sqle.IsSafeDatabaseName(dis.Config.DistilleryBookkeepingDatabase) {
if !sqle.IsSafeDatabaseName(sql.dis.Config.DistilleryBookkeepingDatabase) {
return errSQLUnsafeDatabaseName
}
createDBSQL := fmt.Sprintf("CREATE DATABASE IF NOT EXISTS `%s`;", dis.Config.DistilleryBookkeepingDatabase)
if !dis.sqlRaw(createDBSQL) {
createDBSQL := fmt.Sprintf("CREATE DATABASE IF NOT EXISTS `%s`;", sql.dis.Config.DistilleryBookkeepingDatabase)
if !sql.Query(createDBSQL) {
return errSQLUnableToCreate
}
}
// wait for the database to come up
logging.LogMessage(io, "Waiting for database update to be complete")
dis.SQLWaitForConnection()
sql.Wait()
// open the database
logging.LogMessage(io, "Migrating bookkeeping table")
{
db, err := dis.sqlBkTable(false)
db, err := sql.OpenBookkeeping(false)
if err != nil {
return fmt.Errorf("unable to access bookkeeping table: %s", err)
}