From 09431c486915c8fbe97226349eb35aaa06c28269 Mon Sep 17 00:00:00 2001 From: Tom Wiesing Date: Mon, 5 Sep 2022 15:50:23 +0200 Subject: [PATCH] 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. --- TODO.md | 1 - cmd/backup.go | 4 +- cmd/make_mysql_account.go | 2 +- cmd/mysql.go | 2 +- cmd/prefix.go | 5 +- cmd/provision.go | 4 +- cmd/purge.go | 11 ++-- cmd/system_update.go | 11 ++-- env/instances.go | 19 +++--- env/stack.go | 29 +++++++--- env/stack_resolver.go | 46 ++++++++++----- env/stack_self.go | 34 +++++++---- env/stack_sql.go | 118 ++++++++++++++++++++++---------------- env/stack_ssh.go | 23 ++++++-- env/stack_triplestore.go | 80 ++++++++++++++++---------- env/stack_web.go | 24 ++++++-- 16 files changed, 265 insertions(+), 148 deletions(-) diff --git a/TODO.md b/TODO.md index 2d074b0..ce33208 100644 --- a/TODO.md +++ b/TODO.md @@ -18,7 +18,6 @@ Work in progress. ## Future Work - Move `provision_entrypoint.sh` into go -- Clean up the distillery code, by moving to seperate structs per component - Rename backups to 'snapshots' and make them restorable - Snapshot the docker images being used also! - Avoid running `docker compose` executable and shift it to a library diff --git a/cmd/backup.go b/cmd/backup.go index 222b2fb..454c887 100644 --- a/cmd/backup.go +++ b/cmd/backup.go @@ -156,7 +156,7 @@ func (bi backupInstance) makeSnapshot(context wisski_distillery.Context, path st defer nquads.Close() // TODO: Add a progress bar? - _, err = dis.TriplestoreBackup(nquads, instance.GraphDBRepository) + _, err = dis.Triplestore().Backup(nquads, instance.GraphDBRepository) return err }, context.IOStream, "Backing up Triplestore"); err != nil { return errBackupFailed.Wrap(err) @@ -175,7 +175,7 @@ func (bi backupInstance) makeSnapshot(context wisski_distillery.Context, path st defer sql.Close() // TODO: Add a progress bar? - return dis.SQLBackup(context.IOStream, sql, instance.SqlDatabase) + return dis.SQL().Backup(context.IOStream, sql, instance.SqlDatabase) }, context.IOStream, "Backing up Triplestore"); err != nil { return errBackupFailed.Wrap(err) } diff --git a/cmd/make_mysql_account.go b/cmd/make_mysql_account.go index 5aebf5c..a512f93 100644 --- a/cmd/make_mysql_account.go +++ b/cmd/make_mysql_account.go @@ -55,7 +55,7 @@ func (mma makeMysqlAccount) Run(context wisski_distillery.Context) error { if err != nil { return err } - code, err := context.Environment.SQLShell(context.IOStream, "-e", query) + code, err := context.Environment.SQL().OpenShell(context.IOStream, "-e", query) if err != nil { return err } diff --git a/cmd/mysql.go b/cmd/mysql.go index 2c29dda..10b7d31 100644 --- a/cmd/mysql.go +++ b/cmd/mysql.go @@ -32,7 +32,7 @@ func (mysql) Description() wisski_distillery.Description { } func (ms mysql) Run(context wisski_distillery.Context) error { - code, err := context.Environment.SQLShell(context.IOStream, ms.Positionals.Args...) + code, err := context.Environment.SQL().OpenShell(context.IOStream, ms.Positionals.Args...) if err != nil { return err } diff --git a/cmd/prefix.go b/cmd/prefix.go index f2bd88b..4b46ec8 100644 --- a/cmd/prefix.go +++ b/cmd/prefix.go @@ -38,7 +38,8 @@ func (upc updateprefixconfig) Run(context wisski_distillery.Context) error { return errPrefixUpdateFailed.WithMessageF(err) } - target := dis.ResolverPrefixConfig() + resolver := dis.Resolver() + target := resolver.ConfigPath() // print the configuration config, err := os.OpenFile(target, os.O_WRONLY, fs.ModePerm) @@ -69,7 +70,7 @@ func (upc updateprefixconfig) Run(context wisski_distillery.Context) error { // and restart the resolver to apply the config! logging.LogMessage(context.IOStream, "restarting resolver stack") - if err := dis.ResolverStack().Restart(context.IOStream); err != nil { + if err := resolver.Stack().Restart(context.IOStream); err != nil { return errPrefixUpdateFailed.WithMessageF(err) } diff --git a/cmd/provision.go b/cmd/provision.go index 022288c..2b5653e 100644 --- a/cmd/provision.go +++ b/cmd/provision.go @@ -74,7 +74,7 @@ func (p provision) Run(context wisski_distillery.Context) error { // create the sql if err := logging.LogOperation(func() error { - if err := dis.SQLProvision(instance.SqlDatabase, instance.SqlUser, instance.SqlPassword); err != nil { + if err := dis.SQL().Provision(instance.SqlDatabase, instance.SqlUser, instance.SqlPassword); err != nil { return errProvisionGeneric.WithMessageF(slug, err) } @@ -85,7 +85,7 @@ func (p provision) Run(context wisski_distillery.Context) error { // create the triplestore if err := logging.LogOperation(func() error { - if err := dis.TriplestoreProvision(instance.GraphDBRepository, instance.Domain(), instance.GraphDBUser, instance.GraphDBPassword); err != nil { + if err := dis.Triplestore().Provision(instance.GraphDBRepository, instance.Domain(), instance.GraphDBUser, instance.GraphDBPassword); err != nil { return errProvisionGeneric.WithMessageF(slug, err) } diff --git a/cmd/purge.go b/cmd/purge.go index e7baf57..9d8f512 100644 --- a/cmd/purge.go +++ b/cmd/purge.go @@ -77,14 +77,15 @@ func (p purge) Run(context wisski_distillery.Context) error { } // remove the triplestore + ts := dis.Triplestore() logging.LogOperation(func() error { logging.LogMessage(context.IOStream, "Removing user %s", instance.GraphDBUser) - if err := dis.TriplestorePurgeUser(instance.GraphDBUser); err != nil { + if err := ts.PurgeUser(instance.GraphDBUser); err != nil { context.EPrintln(err) } logging.LogMessage(context.IOStream, "Removing repository %s", instance.GraphDBRepository) - if err := dis.TriplestorePurgeRepo(instance.GraphDBRepository); err != nil { + if err := ts.PurgeRepo(instance.GraphDBRepository); err != nil { context.EPrintln(err) } @@ -93,13 +94,15 @@ func (p purge) Run(context wisski_distillery.Context) error { // remove the sql logging.LogOperation(func() error { + sql := dis.SQL() + logging.LogMessage(context.IOStream, "Removing user %s", instance.SqlUser) - if err := dis.SQLPurgeUser(instance.SqlUser); err != nil { + if err := sql.PurgeUser(instance.SqlUser); err != nil { context.EPrintln(err) } logging.LogMessage(context.IOStream, "Removing database %s", instance.SqlDatabase) - if err := dis.SQLPurgeDatabase(instance.SqlDatabase); err != nil { + if err := sql.PurgeUser(instance.SqlDatabase); err != nil { context.EPrintln(err) } diff --git a/cmd/system_update.go b/cmd/system_update.go index a8c1c5e..5133324 100644 --- a/cmd/system_update.go +++ b/cmd/system_update.go @@ -122,16 +122,17 @@ func (si systemupdate) Run(context wisski_distillery.Context) error { } if err := logging.LogOperation(func() error { - for _, stack := range dis.Stacks() { + for _, component := range dis.Components() { + stack := component.Stack() if err := logging.LogOperation(func() error { return stack.Install(context.IOStream, ctx) - }, context.IOStream, "Installing docker stack %q", stack.Dir); err != nil { + }, context.IOStream, "Installing docker stack %q", component.Name()); err != nil { return err } if err := logging.LogOperation(func() error { return stack.Update(context.IOStream, true) - }, context.IOStream, "Updating docker stack %q", stack.Dir); err != nil { + }, context.IOStream, "Updating docker stack %q", component.Name()); err != nil { return err } } @@ -149,13 +150,13 @@ func (si systemupdate) Run(context wisski_distillery.Context) error { } if err := logging.LogOperation(func() error { - return dis.SQLBootstrap(context.IOStream) + return dis.SQL().Bootstrap(context.IOStream) }, context.IOStream, "Bootstraping SQL database"); err != nil { return errBootstrapSQL.WithMessageF(err) } if err := logging.LogOperation(func() error { - return dis.TriplestoreBootstrap(context.IOStream) + return dis.Triplestore().Bootstrap(context.IOStream) }, context.IOStream, "Bootstraping Triplestore"); err != nil { return errBootstrapTriplestore.WithMessageF(err) } diff --git a/env/instances.go b/env/instances.go index 5238644..5538644 100644 --- a/env/instances.go +++ b/env/instances.go @@ -37,11 +37,12 @@ var errSQL = exit.Error{ // Instance returns the instance of the WissKI Distillery with the provided slug func (dis *Distillery) Instance(slug string) (i Instance, err error) { - if err := dis.SQLWaitForConnection(); err != nil { + sql := dis.SQL() + if err := sql.Wait(); err != nil { return i, err } - table, err := dis.sqlBkTable(false) + table, err := sql.OpenBookkeeping(false) if err != nil { return i, err } @@ -61,11 +62,12 @@ func (dis *Distillery) Instance(slug string) (i Instance, err error) { // HasInstance checks if the provided instance exists in the bookeeping table func (dis *Distillery) HasInstance(slug string) (ok bool, err error) { - if err := dis.SQLWaitForConnection(); err != nil { + sql := dis.SQL() + if err := sql.Wait(); err != nil { return false, err } - table, err := dis.sqlBkTable(false) + table, err := sql.OpenBookkeeping(false) if err != nil { return false, err } @@ -104,12 +106,13 @@ func (dis *Distillery) InstancesWith(slugs ...string) ([]Instance, error) { // findInstances finds instance objects based on a query in the bookkeeping table func (dis *Distillery) findInstances(order bool, query func(table *gorm.DB) *gorm.DB) (instances []Instance, err error) { - if err := dis.SQLWaitForConnection(); err != nil { + sql := dis.SQL() + if err := sql.Wait(); err != nil { return nil, err } // open the bookkeeping table - table, err := dis.sqlBkTable(false) + table, err := sql.OpenBookkeeping(false) if err != nil { return nil, err } @@ -153,7 +156,7 @@ type Instance struct { // Update updates the bookkeeping table with this instance. func (instance *Instance) Update() error { - db, err := instance.dis.sqlBkTable(false) + db, err := instance.dis.SQL().OpenBookkeeping(false) if err != nil { return err } @@ -169,7 +172,7 @@ func (instance *Instance) Update() error { // Delete deletes this instance from the bookkeeping table func (instance *Instance) Delete() error { - db, err := instance.dis.sqlBkTable(false) + db, err := instance.dis.SQL().OpenBookkeeping(false) if err != nil { return err } diff --git a/env/stack.go b/env/stack.go index abf5902..5594e8c 100644 --- a/env/stack.go +++ b/env/stack.go @@ -9,20 +9,31 @@ import ( // TODO: Move everything into specific subpackages // Stacks returns the Stacks of this distillery -func (dis *Distillery) Stacks() []stack.Installable { +func (dis *Distillery) Components() []Component { // TODO: Do we want to cache these stacks? - return []stack.Installable{ - dis.WebStack(), - dis.SelfStack(), - dis.ResolverStack(), - dis.SSHStack(), - dis.TriplestoreStack(), - dis.SQLStack(), + return []Component{ + dis.Web(), + dis.Self(), + dis.Resolver(), + dis.SSH(), + dis.Triplestore(), + dis.SQL(), } } +// Component represents a component of the distillery +type Component interface { + Name() string // Name is the name of this component + + Stack() stack.Installable // Stack returns the installable stack representing this component + + Path() string // Path returns the path to this component +} + // asCoreStack treats the provided stack as a core component of this distillery. -func (dis *Distillery) asCoreStack(name string, stack stack.Installable) stack.Installable { +func (dis *Distillery) makeComponentStack(component Component, stack stack.Installable) stack.Installable { + name := component.Name() + stack.Dir = filepath.Join(dis.Config.DeployRoot, "core", name) stack.ContextResource = filepath.Join("resources", "compose", name) diff --git a/env/stack_resolver.go b/env/stack_resolver.go index c6f17c8..374a846 100644 --- a/env/stack_resolver.go +++ b/env/stack_resolver.go @@ -7,29 +7,47 @@ import ( "github.com/FAU-CDI/wisski-distillery/internal/stack" ) -const ResolverPrefixFile = "prefix.cfg" +// ResolverComponent represents the 'resolver' layer belonging to a distillery +type ResolverComponent struct { + ConfigName string // Filename of the configuration file -func (dis *Distillery) ResolverStack() stack.Installable { - stack := dis.asCoreStack("resolver", stack.Installable{ + dis *Distillery +} + +// Resolver returns the ResolverComponent belonging to this distillery +func (dis *Distillery) Resolver() ResolverComponent { + return ResolverComponent{ + ConfigName: "prefix.cfg", + + dis: dis, + } +} + +func (ResolverComponent) Name() string { + return "resolver" +} + +func (resolver ResolverComponent) Stack() stack.Installable { + stack := resolver.dis.makeComponentStack(resolver, stack.Installable{ EnvFileContext: map[string]string{ - "VIRTUAL_HOST": dis.DefaultVirtualHost(), - "LETSENCRYPT_HOST": dis.DefaultLetsencryptHost(), - "LETSENCRYPT_EMAIL": dis.Config.CertbotEmail, + "VIRTUAL_HOST": resolver.dis.DefaultVirtualHost(), + "LETSENCRYPT_HOST": resolver.dis.DefaultLetsencryptHost(), + "LETSENCRYPT_EMAIL": resolver.dis.Config.CertbotEmail, "PREFIX_FILE": "", // set below! - "DEFAULT_DOMAIN": dis.Config.DefaultDomain, - "LEGACY_DOMAIN": strings.Join(dis.Config.SelfExtraDomains, ","), + "DEFAULT_DOMAIN": resolver.dis.Config.DefaultDomain, + "LEGACY_DOMAIN": strings.Join(resolver.dis.Config.SelfExtraDomains, ","), }, - TouchFiles: []string{ResolverPrefixFile}, + TouchFiles: []string{resolver.ConfigName}, }) - stack.EnvFileContext["PREFIX_FILE"] = filepath.Join(stack.Dir, ResolverPrefixFile) + stack.EnvFileContext["PREFIX_FILE"] = filepath.Join(stack.Dir, resolver.ConfigName) return stack } -func (dis *Distillery) ResolverStackPath() string { - return dis.ResolverStack().Dir +func (resolver ResolverComponent) Path() string { + return resolver.Stack().Dir } -func (dis Distillery) ResolverPrefixConfig() string { - return filepath.Join(dis.ResolverStackPath(), ResolverPrefixFile) +func (resolver ResolverComponent) ConfigPath() string { + return filepath.Join(resolver.Path(), resolver.ConfigName) } diff --git a/env/stack_self.go b/env/stack_self.go index 25e463e..8cd69da 100644 --- a/env/stack_self.go +++ b/env/stack_self.go @@ -2,23 +2,37 @@ package env import "github.com/FAU-CDI/wisski-distillery/internal/stack" -func (dis *Distillery) SelfStack() stack.Installable { +// SelfComponent represents the 'self' layer belonging to a distillery +type SelfComponent struct { + dis *Distillery +} + +// Self returns the SelfComponent belonging to this distillery +func (dis *Distillery) Self() SelfComponent { + return SelfComponent{dis: dis} +} + +func (SelfComponent) Name() string { + return "self" +} + +func (sc SelfComponent) Stack() stack.Installable { TARGET := "https://github.com/FAU-CDI/wisski-distillery" - if dis.Config.SelfRedirect != nil { - TARGET = dis.Config.SelfRedirect.String() + if sc.dis.Config.SelfRedirect != nil { + TARGET = sc.dis.Config.SelfRedirect.String() } - return dis.asCoreStack("self", stack.Installable{ + return sc.dis.makeComponentStack(sc, stack.Installable{ EnvFileContext: map[string]string{ - "VIRTUAL_HOST": dis.DefaultVirtualHost(), - "LETSENCRYPT_HOST": dis.DefaultLetsencryptHost(), - "LETSENCRYPT_EMAIL": dis.Config.CertbotEmail, + "VIRTUAL_HOST": sc.dis.DefaultVirtualHost(), + "LETSENCRYPT_HOST": sc.dis.DefaultLetsencryptHost(), + "LETSENCRYPT_EMAIL": sc.dis.Config.CertbotEmail, "TARGET": TARGET, - "OVERRIDES_FILE": dis.Config.SelfOverridesFile, + "OVERRIDES_FILE": sc.dis.Config.SelfOverridesFile, }, }) } -func (dis *Distillery) SelfStackPath() string { - return dis.SelfStack().Dir +func (sc SelfComponent) Path() string { + return sc.Stack().Dir } diff --git a/env/stack_sql.go b/env/stack_sql.go index 2689f73..1372a63 100644 --- a/env/stack_sql.go +++ b/env/stack_sql.go @@ -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) } diff --git a/env/stack_ssh.go b/env/stack_ssh.go index 385637b..bf0d5a8 100644 --- a/env/stack_ssh.go +++ b/env/stack_ssh.go @@ -2,11 +2,24 @@ package env import "github.com/FAU-CDI/wisski-distillery/internal/stack" -func (dis *Distillery) SSHStack() stack.Installable { - // TODO: Ensure that .env is copied if needed - return dis.asCoreStack("ssh", stack.Installable{}) +// SSHComponent represents the 'ssh' layer belonging to a distillery +type SSHComponent struct { + dis *Distillery } -func (dis *Distillery) SSHStackPath() string { - return dis.SSHStack().Dir +// SSH returns the SSHComponent belonging to this distillery +func (dis *Distillery) SSH() SSHComponent { + return SSHComponent{dis: dis} +} + +func (SSHComponent) Name() string { + return "ssh" +} + +func (ssh SSHComponent) Stack() stack.Installable { + return ssh.dis.makeComponentStack(ssh, stack.Installable{}) +} + +func (ssh SSHComponent) Path() string { + return ssh.Stack().Dir } diff --git a/env/stack_triplestore.go b/env/stack_triplestore.go index 105cf29..8ba64e9 100644 --- a/env/stack_triplestore.go +++ b/env/stack_triplestore.go @@ -20,8 +20,31 @@ import ( "github.com/tkw1536/goprogram/stream" ) -func (dis *Distillery) TriplestoreStack() stack.Installable { - return dis.asCoreStack("triplestore", stack.Installable{ +// TriplestoreComponent represents the triplestore belonging to a distillery +type TriplestoreComponent struct { + BaseURL string // the base url of the api + PollInterval time.Duration // duration to wait during wait! + + dis *Distillery +} + +// Triplestore returns the TriplestoreComponent belonging to this distillery +func (dis *Distillery) Triplestore() TriplestoreComponent { + return TriplestoreComponent{ + BaseURL: "http://127.0.0.1:7200", + PollInterval: time.Second, + + dis: dis, + } +} + +func (TriplestoreComponent) Name() string { + return "triplestore" +} + +// Stack returns the installable Triplestore stack +func (ts TriplestoreComponent) Stack() stack.Installable { + return ts.dis.makeComponentStack(ts, stack.Installable{ CopyContextFiles: []string{"graphdb.zip"}, MakeDirsPerm: fs.ModeDir | fs.ModePerm, @@ -33,8 +56,8 @@ func (dis *Distillery) TriplestoreStack() stack.Installable { }) } -func (dis *Distillery) TriplestoreStackPath() string { - return dis.TriplestoreStack().Dir +func (ts TriplestoreComponent) Path() string { + return ts.Stack().Dir } type TriplestoreUserPayload struct { @@ -50,14 +73,11 @@ type TriplestoreUserAppSettings struct { ExecuteCount bool `json:"EXECUTE_COUNT"` } -const triplestoreBaseURL = "http://127.0.0.1:7200" -const waitTSInterval = 1 * time.Second - -// triplestoreCall makes a request to the triplestore. +// OpenRaw makes an http request to the triplestore api. // // When bodyName is non-empty, expect body to be a byte slice representing a multipart/form-data upload with the given name. // When bodyName is empty, simply marshal body as application/json -func (dis *Distillery) triplestoreRequest(method, url string, body interface{}, bodyName string, accept string) (*http.Response, error) { +func (ts TriplestoreComponent) OpenRaw(method, url string, body interface{}, bodyName string, accept string) (*http.Response, error) { var reader io.Reader var contentType string @@ -87,7 +107,7 @@ func (dis *Distillery) triplestoreRequest(method, url string, body interface{}, } // create the request object - req, err := http.NewRequest(method, triplestoreBaseURL+url, reader) + req, err := http.NewRequest(method, ts.BaseURL+url, reader) if err != nil { return nil, err } @@ -99,21 +119,23 @@ func (dis *Distillery) triplestoreRequest(method, url string, body interface{}, if contentType != "" { req.Header.Set("Content-Type", contentType) } - req.SetBasicAuth(dis.Config.TriplestoreAdminUser, dis.Config.TriplestoreAdminPassword) + req.SetBasicAuth(ts.dis.Config.TriplestoreAdminUser, ts.dis.Config.TriplestoreAdminPassword) // and send it return http.DefaultClient.Do(req) } -func (dis *Distillery) TriplestoreWaitForConnection() error { +// Wait waits for the connection to the Triplestore to succeed. +// This is achieved using a polling strategy. +func (ts TriplestoreComponent) Wait() error { return wait.Wait(func() bool { - res, err := dis.triplestoreRequest("GET", "/rest/repositories", nil, "", "") + res, err := ts.OpenRaw("GET", "/rest/repositories", nil, "", "") if err != nil { return false } defer res.Body.Close() return true - }, waitTSInterval, dis.Context()) + }, ts.PollInterval, ts.dis.Context()) } var errTripleStoreFailedRepository = exit.Error{ @@ -121,8 +143,8 @@ var errTripleStoreFailedRepository = exit.Error{ ExitCode: exit.ExitGeneric, } -func (dis *Distillery) TriplestoreProvision(name, domain, user, password string) error { - if err := dis.TriplestoreWaitForConnection(); err != nil { +func (ts TriplestoreComponent) Provision(name, domain, user, password string) error { + if err := ts.Wait(); err != nil { return err } @@ -137,7 +159,7 @@ func (dis *Distillery) TriplestoreProvision(name, domain, user, password string) // do the create! { - res, err := dis.triplestoreRequest("POST", "/rest/repositories", createRepo, "config", "") + res, err := ts.OpenRaw("POST", "/rest/repositories", createRepo, "config", "") if err != nil { return errTripleStoreFailedRepository.WithMessageF(err) } @@ -149,7 +171,7 @@ func (dis *Distillery) TriplestoreProvision(name, domain, user, password string) // create the user and grant them access { - res, err := dis.triplestoreRequest("POST", "/rest/security/users/"+user, TriplestoreUserPayload{ + res, err := ts.OpenRaw("POST", "/rest/security/users/"+user, TriplestoreUserPayload{ Password: password, AppSettings: TriplestoreUserAppSettings{ DefaultInference: true, @@ -177,8 +199,8 @@ func (dis *Distillery) TriplestoreProvision(name, domain, user, password string) } // TriplestorePurgeUser deletes the specified user from the triplestore -func (dis *Distillery) TriplestorePurgeUser(user string) error { - res, err := dis.triplestoreRequest("DELETE", "/rest/security/users/"+user, nil, "", "") +func (ts TriplestoreComponent) PurgeUser(user string) error { + res, err := ts.OpenRaw("DELETE", "/rest/security/users/"+user, nil, "", "") if err != nil { return err } @@ -189,8 +211,8 @@ func (dis *Distillery) TriplestorePurgeUser(user string) error { } // TriplestorePurgeRepo deletes the specified repo from the triplestore -func (dis *Distillery) TriplestorePurgeRepo(repo string) error { - res, err := dis.triplestoreRequest("DELETE", "/rest/repositories/"+repo, nil, "", "") +func (ts TriplestoreComponent) PurgeRepo(repo string) error { + res, err := ts.OpenRaw("DELETE", "/rest/repositories/"+repo, nil, "", "") if err != nil { return err } @@ -203,8 +225,8 @@ func (dis *Distillery) TriplestorePurgeRepo(repo string) error { var errTSBackupWrongStatusCode = errors.New("Distillery.Backup: Wrong status code") // TriplestoreBackup backs up the repository named repo into the writer dst. -func (dis *Distillery) TriplestoreBackup(dst io.Writer, repo string) (int64, error) { - res, err := dis.triplestoreRequest("GET", "/repositories/"+repo+"/statements?infer=false", nil, "", "application/n-quads") +func (ts TriplestoreComponent) Backup(dst io.Writer, repo string) (int64, error) { + res, err := ts.OpenRaw("GET", "/repositories/"+repo+"/statements?infer=false", nil, "", "application/n-quads") if err != nil { return 0, err } @@ -217,16 +239,16 @@ func (dis *Distillery) TriplestoreBackup(dst io.Writer, repo string) (int64, err var errTriplestoreFailedSecurity = errors.New("failed to enable triplestore security: request did not succeed with HTTP 200 OK") -func (dis *Distillery) TriplestoreBootstrap(io stream.IOStream) error { +func (ts TriplestoreComponent) Bootstrap(io stream.IOStream) error { logging.LogMessage(io, "Waiting for Triplestore") - if err := dis.TriplestoreWaitForConnection(); err != nil { + if err := ts.Wait(); err != nil { return err } logging.LogMessage(io, "Resetting admin user password") { - res, err := dis.triplestoreRequest("PUT", "/rest/security/users/"+dis.Config.TriplestoreAdminUser, TriplestoreUserPayload{ - Password: dis.Config.TriplestoreAdminPassword, + res, err := ts.OpenRaw("PUT", "/rest/security/users/"+ts.dis.Config.TriplestoreAdminUser, TriplestoreUserPayload{ + Password: ts.dis.Config.TriplestoreAdminPassword, AppSettings: TriplestoreUserAppSettings{ DefaultInference: true, DefaultVisGraphSchema: true, @@ -257,7 +279,7 @@ func (dis *Distillery) TriplestoreBootstrap(io stream.IOStream) error { logging.LogMessage(io, "Enabling Triplestore security") { - res, err := dis.triplestoreRequest("POST", "/rest/security", true, "", "") + res, err := ts.OpenRaw("POST", "/rest/security", true, "", "") if err != nil { return fmt.Errorf("failed to enable triplestore security: %s", err) } diff --git a/env/stack_web.go b/env/stack_web.go index 0f1b5a4..0156349 100644 --- a/env/stack_web.go +++ b/env/stack_web.go @@ -2,14 +2,28 @@ package env import "github.com/FAU-CDI/wisski-distillery/internal/stack" -func (dis *Distillery) WebStack() stack.Installable { - return dis.asCoreStack("web", stack.Installable{ +// WebComponent represents the 'web' layer belonging to a distillery +type WebComponent struct { + dis *Distillery +} + +// Web returns the WebComponent belonging to this distillery +func (dis *Distillery) Web() WebComponent { + return WebComponent{dis: dis} +} + +func (WebComponent) Name() string { + return "web" +} + +func (web WebComponent) Stack() stack.Installable { + return web.dis.makeComponentStack(web, stack.Installable{ EnvFileContext: map[string]string{ - "DEFAULT_HOST": dis.Config.DefaultDomain, + "DEFAULT_HOST": web.dis.Config.DefaultDomain, }, }) } -func (dis *Distillery) WebStackPath() string { - return dis.WebStack().Dir +func (web WebComponent) Path() string { + return web.Stack().Dir }