diff --git a/cmd/blind_update.go b/cmd/blind_update.go index 24ac3cf..b92f9bf 100644 --- a/cmd/blind_update.go +++ b/cmd/blind_update.go @@ -33,7 +33,7 @@ var errBlindUpdateFailed = exit.Error{ } func (bu blindUpdate) Run(context wisski_distillery.Context) error { - instances, err := context.Environment.Instances(bu.Positionals.Slug...) + instances, err := context.Environment.Instances().Load(bu.Positionals.Slug...) if err != nil { return err } diff --git a/cmd/cron.go b/cmd/cron.go index 74b9c47..8fe75f0 100644 --- a/cmd/cron.go +++ b/cmd/cron.go @@ -32,7 +32,7 @@ var errCronFailed = exit.Error{ } func (cr cron) Run(context wisski_distillery.Context) error { - instances, err := context.Environment.Instances(cr.Positionals.Slug...) + instances, err := context.Environment.Instances().Load(cr.Positionals.Slug...) if err != nil { return err } diff --git a/cmd/info.go b/cmd/info.go index ac0848d..82b6149 100644 --- a/cmd/info.go +++ b/cmd/info.go @@ -25,7 +25,7 @@ func (info) Description() wisski_distillery.Description { } func (i info) Run(context wisski_distillery.Context) error { - instance, err := context.Environment.Instance(i.Positionals.Slug) + instance, err := context.Environment.Instances().WissKI(i.Positionals.Slug) if err != nil { return err } @@ -34,11 +34,11 @@ func (i info) Run(context wisski_distillery.Context) error { context.Printf("Base directory: %s\n", instance.FilesystemBase) context.Printf("SQL Database: %s\n", instance.SqlDatabase) - context.Printf("SQL Username: %s\n", instance.SqlUser) + context.Printf("SQL Username: %s\n", instance.SqlUsername) context.Printf("SQL Password: %s\n", instance.SqlPassword) context.Printf("GraphDB Repository: %s\n", instance.GraphDBRepository) - context.Printf("GraphDB Username: %s\n", instance.GraphDBUser) + context.Printf("GraphDB Username: %s\n", instance.GraphDBUsername) context.Printf("GraphDB Password: %s\n", instance.GraphDBPassword) return nil diff --git a/cmd/ls.go b/cmd/ls.go index 1e0d952..70a8bd2 100644 --- a/cmd/ls.go +++ b/cmd/ls.go @@ -25,7 +25,7 @@ func (ls) Description() wisski_distillery.Description { } func (l ls) Run(context wisski_distillery.Context) error { - instances, err := context.Environment.Instances(l.Positionals.Slug...) + instances, err := context.Environment.Instances().Load(l.Positionals.Slug...) if err != nil { return err } diff --git a/cmd/provision.go b/cmd/provision.go index e5ca3b7..b6aaa7e 100644 --- a/cmd/provision.go +++ b/cmd/provision.go @@ -45,12 +45,12 @@ func (p provision) Run(context wisski_distillery.Context) error { // check that it doesn't already exist logging.LogMessage(context.IOStream, "Provisioning new WissKI instance %s", slug) - if exists, err := dis.HasInstance(slug); err != nil || exists { + if exists, err := dis.Instances().Has(slug); err != nil || exists { return errProvisionAlreadyExists.WithMessageF(slug) } // make it in-memory - instance, err := dis.NewInstance(slug) + instance, err := dis.Instances().Create(slug) if err != nil { return errProvisionGeneric.WithMessageF(slug, err) } @@ -63,7 +63,7 @@ func (p provision) Run(context wisski_distillery.Context) error { // Store in bookkeeping if err := logging.LogOperation(func() error { - if err := instance.Update(); err != nil { + if err := instance.Save(); err != nil { return errProvisionGeneric.WithMessageF(slug, err) } @@ -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.SQL().Provision(instance.SqlDatabase, instance.SqlUser, instance.SqlPassword); err != nil { + if err := dis.SQL().Provision(instance.SqlDatabase, instance.SqlUsername, 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.Triplestore().Provision(instance.GraphDBRepository, instance.Domain(), instance.GraphDBUser, instance.GraphDBPassword); err != nil { + if err := dis.Triplestore().Provision(instance.GraphDBRepository, instance.Domain(), instance.GraphDBUsername, instance.GraphDBPassword); err != nil { return errProvisionGeneric.WithMessageF(slug, err) } diff --git a/cmd/purge.go b/cmd/purge.go index 97a4ac5..60f61ca 100644 --- a/cmd/purge.go +++ b/cmd/purge.go @@ -4,8 +4,8 @@ import ( "os" wisski_distillery "github.com/FAU-CDI/wisski-distillery" + "github.com/FAU-CDI/wisski-distillery/internal/component/instances" "github.com/FAU-CDI/wisski-distillery/internal/core" - "github.com/FAU-CDI/wisski-distillery/internal/wisski" "github.com/FAU-CDI/wisski-distillery/pkg/logging" "github.com/tkw1536/goprogram/exit" ) @@ -56,10 +56,10 @@ func (p purge) Run(context wisski_distillery.Context) error { // load the instance (first via bookkeeping, then via defaults) logging.LogMessage(context.IOStream, "Checking bookkeeping table") - instance, err := dis.Instance(slug) - if err == wisski.ErrInstanceNotFound { + instance, err := dis.Instances().WissKI(slug) + if err == instances.ErrWissKINotFound { context.Println("Not found in bookkeeping table, assuming defaults") - instance, err = dis.NewInstance(slug) + instance, err = dis.Instances().Create(slug) } if err != nil { return errPurgeNoDetails.WithMessageF(err) @@ -80,8 +80,8 @@ 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 := ts.PurgeUser(instance.GraphDBUser); err != nil { + logging.LogMessage(context.IOStream, "Removing user %s", instance.GraphDBUsername) + if err := ts.PurgeUser(instance.GraphDBUsername); err != nil { context.EPrintln(err) } @@ -97,8 +97,8 @@ func (p purge) Run(context wisski_distillery.Context) error { logging.LogOperation(func() error { sql := dis.SQL() - logging.LogMessage(context.IOStream, "Removing user %s", instance.SqlUser) - if err := sql.PurgeUser(instance.SqlUser); err != nil { + logging.LogMessage(context.IOStream, "Removing user %s", instance.SqlUsername) + if err := sql.PurgeUser(instance.SqlUsername); err != nil { context.EPrintln(err) } diff --git a/cmd/rebuild.go b/cmd/rebuild.go index 38f90ef..bf90be3 100644 --- a/cmd/rebuild.go +++ b/cmd/rebuild.go @@ -33,7 +33,7 @@ var errRebuildFailed = exit.Error{ } func (rb rebuild) Run(context wisski_distillery.Context) error { - instances, err := context.Environment.Instances(rb.Positionals.Slug...) + instances, err := context.Environment.Instances().Load(rb.Positionals.Slug...) if err != nil { return err } diff --git a/cmd/reserve.go b/cmd/reserve.go index e95f9f1..16a307f 100644 --- a/cmd/reserve.go +++ b/cmd/reserve.go @@ -46,12 +46,12 @@ func (r reserve) Run(context wisski_distillery.Context) error { // check that it doesn't already exist logging.LogMessage(context.IOStream, "Reserving new WissKI instance %s", slug) - if exists, err := dis.HasInstance(slug); err != nil || exists { + if exists, err := dis.Instances().Has(slug); err != nil || exists { return errProvisionAlreadyExists.WithMessageF(slug) } // make it in-memory - instance, err := dis.NewInstance(slug) + instance, err := dis.Instances().Create(slug) if err != nil { return errProvisionGeneric.WithMessageF(slug, err) } diff --git a/cmd/shell.go b/cmd/shell.go index c57faeb..68b4277 100644 --- a/cmd/shell.go +++ b/cmd/shell.go @@ -38,7 +38,7 @@ var errShell = exit.Error{ } func (sh shell) Run(context wisski_distillery.Context) error { - instance, err := context.Environment.Instance(sh.Positionals.Slug) + instance, err := context.Environment.Instances().WissKI(sh.Positionals.Slug) if err != nil { return err } diff --git a/cmd/snapshot.go b/cmd/snapshot.go index ffefce7..5b24d0d 100644 --- a/cmd/snapshot.go +++ b/cmd/snapshot.go @@ -42,7 +42,7 @@ var errSnapshotFailed = exit.Error{ func (bi snapshot) Run(context wisski_distillery.Context) error { dis := context.Environment - instance, err := dis.Instance(bi.Positionals.Slug) + instance, err := dis.Instances().WissKI(bi.Positionals.Slug) if err != nil { return err } @@ -86,7 +86,7 @@ func (bi snapshot) Run(context wisski_distillery.Context) error { // take a snapshot into the staging area! logging.LogOperation(func() error { - sreport := instance.Snapshot(context.IOStream, wisski.SnapshotDescription{ + sreport := dis.Snapshot(instance, context.IOStream, wisski.SnapshotDescription{ Dest: sPath, Keepalive: bi.Keepalive, }) diff --git a/cmd/system_update.go b/cmd/system_update.go index ab1633b..0154a16 100644 --- a/cmd/system_update.go +++ b/cmd/system_update.go @@ -80,7 +80,7 @@ func (si systemupdate) Run(context wisski_distillery.Context) error { logging.LogMessage(context.IOStream, "Ensuring distillery installation directories exist") for _, d := range []string{ dis.Config.DeployRoot, - dis.InstancesDir(), + dis.Instances().Path(), dis.SnapshotsStagingPath(), dis.SnapshotsArchivePath(), } { diff --git a/cmd/update_prefix_config.go b/cmd/update_prefix_config.go index 77eb5fd..2b31eaf 100644 --- a/cmd/update_prefix_config.go +++ b/cmd/update_prefix_config.go @@ -33,7 +33,7 @@ var errPrefixUpdateFailed = exit.Error{ func (upc updateprefixconfig) Run(context wisski_distillery.Context) error { dis := context.Environment - instances, err := dis.AllInstances() + instances, err := dis.Instances().All() if err != nil { return errPrefixUpdateFailed.WithMessageF(err) } diff --git a/pkg/bookkeeping/bookkeeping.go b/internal/bookkeeping/bookkeeping.go similarity index 94% rename from pkg/bookkeeping/bookkeeping.go rename to internal/bookkeeping/bookkeeping.go index 690e180..cd91628 100644 --- a/pkg/bookkeeping/bookkeeping.go +++ b/internal/bookkeeping/bookkeeping.go @@ -33,12 +33,12 @@ type Instance struct { // SQL Database credentials for the system SqlDatabase string `gorm:"column:sql_database;not null"` - SqlUser string `gorm:"column:sql_user;not null"` + SqlUsername string `gorm:"column:sql_user;not null"` SqlPassword string `gorm:"column:sql_password;not null"` // GraphDB Repository GraphDBRepository string `gorm:"column:graphdb_repository;not null"` - GraphDBUser string `gorm:"column:graphdb_user;not null"` + GraphDBUsername string `gorm:"column:graphdb_user;not null"` GraphDBPassword string `gorm:"column:graphdb_password;not null"` } diff --git a/internal/component/component.go b/internal/component/component.go index 08664df..1e1de32 100644 --- a/internal/component/component.go +++ b/internal/component/component.go @@ -26,29 +26,28 @@ type Component interface { // By convention it is /var/www/deploy/internal/core/${Name()} Path() string - // Context returns a new InstallationContext to be used during installation from the command line. - // Typically this should just pass through the parent, but might perform other tasks. - Context(parent InstallationContext) InstallationContext - // Base() returns a reference to a base component // This is implemented by an embedding on ComponentBase Base() *ComponentBase } -// ComponentWithStack implements a component with a Stack method. -type ComponentWithStack interface { +// InstallableComponent implements an installable component +type InstallableComponent interface { Component // Stack can be used to gain access to the "docker compose" stack. // // This should internally call Stack() Installable + + // Context returns a new InstallationContext to be used during installation from the command line. + // Typically this should just pass through the parent, but might perform other tasks. + Context(parent InstallationContext) InstallationContext } // ComponentBase implements base functionality for a component type ComponentBase struct { - Dir string // Dir is the directory this component lives in - + Dir string // Dir is the directory this component lives in Config *config.Config // Config is the configuration of the underlying distillery } diff --git a/internal/component/instances/instances.go b/internal/component/instances/instances.go new file mode 100644 index 0000000..2cc4ad4 --- /dev/null +++ b/internal/component/instances/instances.go @@ -0,0 +1,143 @@ +package instances + +import ( + "errors" + + "github.com/FAU-CDI/wisski-distillery/internal/bookkeeping" + "github.com/FAU-CDI/wisski-distillery/internal/component" + "github.com/FAU-CDI/wisski-distillery/internal/component/sql" + "github.com/FAU-CDI/wisski-distillery/internal/component/triplestore" + "github.com/tkw1536/goprogram/exit" + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +// Instances manages multiple WissKI Instances. +type Instances struct { + component.ComponentBase + + TS *triplestore.Triplestore + SQL *sql.SQL +} + +func (Instances) Name() string { + return "instances" +} + +// ErrWissKINotFound is returned when a WissKI is not found +var ErrWissKINotFound = errors.New("WissKI not found") + +var errSQL = exit.Error{ + Message: "Unknown SQL Error %s", + ExitCode: exit.ExitGeneric, +} + +// WissKI returns the WissKI with the provided slug, if it exists. +// It the WissKI does not exist, returns ErrWissKINotFound. +func (instances *Instances) WissKI(slug string) (i WissKI, err error) { + sql := instances.SQL + if err := sql.Wait(); err != nil { + return i, err + } + + table, err := sql.OpenBookkeeping(false) + if err != nil { + return i, err + } + + // find the instance by slug + query := table.Where(&bookkeeping.Instance{Slug: slug}).Find(&i.Instance) + switch { + case query.Error != nil: + return i, errSQL.WithMessageF(query.Error) + case query.RowsAffected == 0: + return i, ErrWissKINotFound + default: + i.instances = instances + return i, nil + } +} + +// Has checks if a WissKI with the provided slug exists inside the database. +// It does not perform any checks on the WissKI itself. +func (instances *Instances) Has(slug string) (ok bool, err error) { + sql := instances.SQL + if err := sql.Wait(); err != nil { + return false, err + } + + table, err := sql.OpenBookkeeping(false) + if err != nil { + return false, err + } + + query := table.Select("count(*) > 0").Where("slug = ?", slug).Find(&ok) + if query.Error != nil { + return false, errSQL.WithMessageF(query.Error) + } + return +} + +// All returns all instances of the WissKI Distillery in consistent order. +// +// There is no guarantee that this order remains identical between different api releases; however subsequent invocations are guaranteed to return the same order. +func (instances *Instances) All() ([]WissKI, error) { + return instances.find(true, func(table *gorm.DB) *gorm.DB { + return table + }) +} + +// WissKIs returns the WissKI instances with the provides slugs. +// If a slug does not exist, it is omitted from the result. +func (instances *Instances) WissKIs(slugs ...string) ([]WissKI, error) { + return instances.find(true, func(table *gorm.DB) *gorm.DB { + return table.Where("slug IN ?", slugs) + }) +} + +// Load is like All, except that when no slugs are provided, it calls All. +func (instances *Instances) Load(slugs ...string) ([]WissKI, error) { + if len(slugs) == 0 { + return instances.All() + } + return instances.WissKIs(slugs...) +} + +// find finds instances based on the provided query +func (instances *Instances) find(order bool, query func(table *gorm.DB) *gorm.DB) (results []WissKI, err error) { + sql := instances.SQL + if err := sql.Wait(); err != nil { + return nil, err + } + + // open the bookkeeping table + table, err := sql.OpenBookkeeping(false) + if err != nil { + return nil, err + } + + // prepare a query + find := table + if order { + find = find.Order(clause.OrderByColumn{Column: clause.Column{Name: "slug"}, Desc: false}) + } + if query != nil { + find = query(find) + } + + // fetch bookkeeping instances + var bks []bookkeeping.Instance + find = find.Find(&bks) + if find.Error != nil { + return nil, errSQL.WithMessageF(find.Error) + } + + // make proper instances + results = make([]WissKI, len(bks)) + for i, bk := range bks { + results[i].Instance = bk + results[i].instances = instances + } + + return results, nil +} diff --git a/internal/wisski/instances/barrel.env b/internal/component/instances/instances/barrel.env similarity index 100% rename from internal/wisski/instances/barrel.env rename to internal/component/instances/instances/barrel.env diff --git a/internal/wisski/instances/barrel/.dockerignore b/internal/component/instances/instances/barrel/.dockerignore similarity index 100% rename from internal/wisski/instances/barrel/.dockerignore rename to internal/component/instances/instances/barrel/.dockerignore diff --git a/internal/wisski/instances/barrel/.env.sample b/internal/component/instances/instances/barrel/.env.sample similarity index 100% rename from internal/wisski/instances/barrel/.env.sample rename to internal/component/instances/instances/barrel/.env.sample diff --git a/internal/wisski/instances/barrel/Dockerfile b/internal/component/instances/instances/barrel/Dockerfile similarity index 100% rename from internal/wisski/instances/barrel/Dockerfile rename to internal/component/instances/instances/barrel/Dockerfile diff --git a/internal/wisski/instances/barrel/conf/ports.conf b/internal/component/instances/instances/barrel/conf/ports.conf similarity index 100% rename from internal/wisski/instances/barrel/conf/ports.conf rename to internal/component/instances/instances/barrel/conf/ports.conf diff --git a/internal/wisski/instances/barrel/conf/wisski.conf b/internal/component/instances/instances/barrel/conf/wisski.conf similarity index 100% rename from internal/wisski/instances/barrel/conf/wisski.conf rename to internal/component/instances/instances/barrel/conf/wisski.conf diff --git a/internal/wisski/instances/barrel/conf/wisski.ini b/internal/component/instances/instances/barrel/conf/wisski.ini similarity index 100% rename from internal/wisski/instances/barrel/conf/wisski.ini rename to internal/component/instances/instances/barrel/conf/wisski.ini diff --git a/internal/wisski/instances/barrel/docker-compose.yml b/internal/component/instances/instances/barrel/docker-compose.yml similarity index 100% rename from internal/wisski/instances/barrel/docker-compose.yml rename to internal/component/instances/instances/barrel/docker-compose.yml diff --git a/internal/wisski/instances/barrel/patch/easyrdf.patch b/internal/component/instances/instances/barrel/patch/easyrdf.patch similarity index 100% rename from internal/wisski/instances/barrel/patch/easyrdf.patch rename to internal/component/instances/instances/barrel/patch/easyrdf.patch diff --git a/internal/wisski/instances/barrel/patch/triples.patch b/internal/component/instances/instances/barrel/patch/triples.patch similarity index 100% rename from internal/wisski/instances/barrel/patch/triples.patch rename to internal/component/instances/instances/barrel/patch/triples.patch diff --git a/internal/wisski/instances/barrel/scripts/entrypoint.sh b/internal/component/instances/instances/barrel/scripts/entrypoint.sh similarity index 100% rename from internal/wisski/instances/barrel/scripts/entrypoint.sh rename to internal/component/instances/instances/barrel/scripts/entrypoint.sh diff --git a/internal/wisski/instances/barrel/scripts/provision_container.sh b/internal/component/instances/instances/barrel/scripts/provision_container.sh similarity index 100% rename from internal/wisski/instances/barrel/scripts/provision_container.sh rename to internal/component/instances/instances/barrel/scripts/provision_container.sh diff --git a/internal/wisski/instances/barrel/scripts/user_shell.sh b/internal/component/instances/instances/barrel/scripts/user_shell.sh similarity index 100% rename from internal/wisski/instances/barrel/scripts/user_shell.sh rename to internal/component/instances/instances/barrel/scripts/user_shell.sh diff --git a/internal/wisski/instances/barrel/wisskiutils/create_adapter.php b/internal/component/instances/instances/barrel/wisskiutils/create_adapter.php similarity index 100% rename from internal/wisski/instances/barrel/wisskiutils/create_adapter.php rename to internal/component/instances/instances/barrel/wisskiutils/create_adapter.php diff --git a/internal/wisski/instances/barrel/wisskiutils/export_pathbuilder.php b/internal/component/instances/instances/barrel/wisskiutils/export_pathbuilder.php similarity index 100% rename from internal/wisski/instances/barrel/wisskiutils/export_pathbuilder.php rename to internal/component/instances/instances/barrel/wisskiutils/export_pathbuilder.php diff --git a/internal/wisski/instances/barrel/wisskiutils/list_uri_prefixes.php b/internal/component/instances/instances/barrel/wisskiutils/list_uri_prefixes.php similarity index 100% rename from internal/wisski/instances/barrel/wisskiutils/list_uri_prefixes.php rename to internal/component/instances/instances/barrel/wisskiutils/list_uri_prefixes.php diff --git a/internal/wisski/instances/barrel/wisskiutils/set_trusted_host.sh b/internal/component/instances/instances/barrel/wisskiutils/set_trusted_host.sh similarity index 100% rename from internal/wisski/instances/barrel/wisskiutils/set_trusted_host.sh rename to internal/component/instances/instances/barrel/wisskiutils/set_trusted_host.sh diff --git a/internal/wisski/instances/barrel/wisskiutils/settings_php_get.sh b/internal/component/instances/instances/barrel/wisskiutils/settings_php_get.sh similarity index 100% rename from internal/wisski/instances/barrel/wisskiutils/settings_php_get.sh rename to internal/component/instances/instances/barrel/wisskiutils/settings_php_get.sh diff --git a/internal/wisski/instances/barrel/wisskiutils/settings_php_set.sh b/internal/component/instances/instances/barrel/wisskiutils/settings_php_set.sh similarity index 100% rename from internal/wisski/instances/barrel/wisskiutils/settings_php_set.sh rename to internal/component/instances/instances/barrel/wisskiutils/settings_php_set.sh diff --git a/internal/wisski/instances/reserve.env b/internal/component/instances/instances/reserve.env similarity index 100% rename from internal/wisski/instances/reserve.env rename to internal/component/instances/instances/reserve.env diff --git a/internal/wisski/instances/reserve/docker-compose.yml b/internal/component/instances/instances/reserve/docker-compose.yml similarity index 100% rename from internal/wisski/instances/reserve/docker-compose.yml rename to internal/component/instances/instances/reserve/docker-compose.yml diff --git a/internal/wisski/instances/reserve/index.html b/internal/component/instances/instances/reserve/index.html similarity index 100% rename from internal/wisski/instances/reserve/index.html rename to internal/component/instances/instances/reserve/index.html diff --git a/internal/component/instances/wisski_create.go b/internal/component/instances/wisski_create.go new file mode 100644 index 0000000..7ff29df --- /dev/null +++ b/internal/component/instances/wisski_create.go @@ -0,0 +1,61 @@ +package instances + +import ( + "errors" + "path/filepath" + + "github.com/FAU-CDI/wisski-distillery/pkg/stringparser" +) + +var errInvalidSlug = errors.New("not a valid slug") + +// Create fills the struct for a new WissKI instance. +// It validates that slug is a valid name for an instance. +// +// It does not perform any checks if the instance already exists, or does the creation in the database. +func (instances *Instances) Create(slug string) (wisski WissKI, err error) { + + // make sure that the slug is valid! + if _, err := stringparser.ParseSlug(slug); err != nil { + return wisski, errInvalidSlug + } + + wisski.Instance.Slug = slug + wisski.Instance.FilesystemBase = filepath.Join(instances.Dir, slug) + + wisski.Instance.OwnerEmail = "" + wisski.Instance.AutoBlindUpdateEnabled = true + + // sql + + wisski.Instance.SqlDatabase = instances.Config.MysqlDatabasePrefix + slug + wisski.Instance.SqlUsername = instances.Config.MysqlUserPrefix + slug + + wisski.Instance.SqlPassword, err = instances.Config.NewPassword() + if err != nil { + return WissKI{}, err + } + + // triplestore + + wisski.Instance.GraphDBRepository = instances.Config.GraphDBRepoPrefix + slug + wisski.Instance.GraphDBUsername = instances.Config.GraphDBUserPrefix + slug + + wisski.Instance.GraphDBPassword, err = instances.Config.NewPassword() + if err != nil { + return WissKI{}, err + } + + // drupal + + wisski.DrupalUsername = "admin" // TODO: Change this! + + wisski.DrupalPassword, err = instances.Config.NewPassword() + if err != nil { + return wisski, err + } + + // store the instance in the object and return it! + wisski.instances = instances + return wisski, nil +} diff --git a/internal/component/instances/wisski_db.go b/internal/component/instances/wisski_db.go new file mode 100644 index 0000000..cfec213 --- /dev/null +++ b/internal/component/instances/wisski_db.go @@ -0,0 +1,285 @@ +package instances + +import ( + "bytes" + "embed" + "encoding/json" + "errors" + "fmt" + "io" + "io/fs" + "net/url" + "os" + "path/filepath" + "strings" + + "github.com/FAU-CDI/wisski-distillery/internal/bookkeeping" + "github.com/FAU-CDI/wisski-distillery/internal/component" + "github.com/FAU-CDI/wisski-distillery/pkg/fsx" + "github.com/alessio/shellescape" + "github.com/tkw1536/goprogram/stream" + "golang.org/x/exp/maps" + "golang.org/x/exp/slices" +) + +// WissKI represents a single WissKI Instance +type WissKI struct { + // Whatever is stored inside the bookkeeping database + bookkeeping.Instance + + // Credentials to Drupal + DrupalUsername string + DrupalPassword string + + // reference to the component! + instances *Instances +} + +// Save saves this instance in the bookkeeping table +func (wisski *WissKI) Save() error { + db, err := wisski.instances.SQL.OpenBookkeeping(false) + if err != nil { + return err + } + + // it has never been created => we need to create it in the database + if wisski.Instance.Created.IsZero() { + return db.Create(&wisski.Instance).Error + } + + // Update based on the primary key! + return db.Where("pk = ?", wisski.Instance.Pk).Updates(&wisski.Instance).Error +} + +// Delete deletes this instance from the bookkeeping table +func (wisski *WissKI) Delete() error { + db, err := wisski.instances.SQL.OpenBookkeeping(false) + if err != nil { + return err + } + + // doesn't exist => nothing to delete + if wisski.Instance.Created.IsZero() { + return nil + } + + // delete it directly + return db.Delete(&wisski.Instance).Error +} + +// Shell executes a shell command inside the +func (wisski WissKI) Shell(io stream.IOStream, argv ...string) (int, error) { + return wisski.Stack().Exec(io, "barrel", "/bin/sh", append([]string{"/user_shell.sh"}, argv...)...) +} + +// Domain returns the full domain name of this instance +func (wisski WissKI) Domain() string { + return fmt.Sprintf("%s.%s", wisski.Slug, wisski.instances.Config.DefaultDomain) +} + +// URL returns the public URL of this instance +func (wisski WissKI) URL() *url.URL { + // setup domain and path + url := &url.URL{ + Host: wisski.Domain(), + Path: "/", + } + + // use http or https scheme depending on if the distillery has it enabled + if wisski.instances.Config.HTTPSEnabled() { + url.Scheme = "https" + } else { + url.Scheme = "http" + } + + return url +} + +//go:embed all:instances/barrel instances/barrel.env +var barrelResources embed.FS + +// Stack represents a stack representing this instance +func (wisski WissKI) Stack() component.Installable { + return component.Installable{ + Stack: component.Stack{ + Dir: wisski.FilesystemBase, + }, + + Resources: barrelResources, + ContextPath: filepath.Join("instances", "barrel"), + EnvPath: filepath.Join("instances", "barrel.env"), + + EnvContext: map[string]string{ + "DATA_PATH": filepath.Join(wisski.FilesystemBase, "data"), + + "SLUG": wisski.Slug, + "VIRTUAL_HOST": wisski.Domain(), + + "LETSENCRYPT_HOST": wisski.instances.Config.IfHttps(wisski.Domain()), + "LETSENCRYPT_EMAIL": wisski.instances.Config.IfHttps(wisski.instances.Config.CertbotEmail), + + "RUNTIME_DIR": wisski.instances.Config.RuntimeDir(), + "GLOBAL_AUTHORIZED_KEYS_FILE": wisski.instances.Config.GlobalAuthorizedKeysFile, + }, + + MakeDirsPerm: fs.ModeDir | fs.ModePerm, + MakeDirs: []string{"data", ".composer"}, + + TouchFiles: []string{ + filepath.Join("data", "authorized_keys"), + }, + } +} + +//go:embed all:instances/reserve instances/reserve.env +var reserveResources embed.FS + +func (wisski WissKI) ReserveStack() component.Installable { + return component.Installable{ + Stack: component.Stack{ + Dir: wisski.FilesystemBase, + }, + + Resources: reserveResources, + ContextPath: filepath.Join("instances", "reserve"), + EnvPath: filepath.Join("instances", "reserve.env"), + + EnvContext: map[string]string{ + "VIRTUAL_HOST": wisski.Domain(), + + "LETSENCRYPT_HOST": wisski.instances.Config.IfHttps(wisski.Domain()), + "LETSENCRYPT_EMAIL": wisski.instances.Config.IfHttps(wisski.instances.Config.CertbotEmail), + }, + } +} + +// Provision provisions an instance, assuming that the required databases already exist. +func (wisski WissKI) Provision(io stream.IOStream) error { + + // create the basic st! + st := wisski.Stack() + if err := st.Install(io, component.InstallationContext{}); err != nil { + return err + } + + // Pull and build the stack! + if err := st.Update(io, false); err != nil { + return err + } + + provisionParams := []string{ + wisski.Domain(), + + wisski.SqlDatabase, + wisski.SqlUsername, + wisski.SqlPassword, + + wisski.GraphDBRepository, + wisski.GraphDBUsername, + wisski.GraphDBPassword, + + wisski.DrupalUsername, + wisski.DrupalPassword, + + "", // TODO: DrupalVersion + "", // TODO: WissKIVersion + } + + // escape the parameter + for i, param := range provisionParams { + provisionParams[i] = shellescape.Quote(param) + } + + // figure out the provision script + // TODO: Move the provision script into the control plane! + provisionScript := "sudo PATH=$PATH -u www-data /bin/bash /provision_container.sh " + strings.Join(provisionParams, " ") + + code, err := st.Run(io, true, "barrel", "/bin/bash", "-c", provisionScript) + if err != nil { + return err + } + if code != 0 { + return errors.New("unable to run provision script") + } + + return nil +} + +// NoPrefix checks if this WissKI instance is excluded from generating prefixes +func (wisski *WissKI) NoPrefix() bool { + return fsx.IsFile(filepath.Join(wisski.FilesystemBase, "prefixes.skip")) +} + +var errPrefixExecFailed = errors.New("PrefixConfig: Failed to call list_uri_prefixes") + +// PrefixConfig returns the prefix config belonging to this instance. +func (wisski *WissKI) PrefixConfig() (config string, err error) { + // if the user requested to skip the prefix, then don't do anything with it! + if wisski.NoPrefix() { + return "", nil + } + + var builder strings.Builder + + // domain + builder.WriteString(wisski.URL().String() + ":") + builder.WriteString("\n") + + // default prefixes + wu := stream.NewIOStream(&builder, nil, nil, 0) + code, err := wisski.Stack().Exec(wu, "barrel", "/bin/bash", "/user_shell.sh", "-c", "drush php:script /wisskiutils/list_uri_prefixes.php") + if err != nil || code != 0 { + return "", errPrefixExecFailed + } + + // custom prefixes + prefixPath := filepath.Join(wisski.FilesystemBase, "prefixes") + if fsx.IsFile(prefixPath) { + prefix, err := os.Open(prefixPath) + if err != nil { + return "", err + } + defer prefix.Close() + if _, err := io.Copy(&builder, prefix); err != nil { + return "", err + } + builder.WriteString("\n") + } + + // and done! + return builder.String(), nil +} + +var errPathbuildersExecFailed = errors.New("ExportPathbuilders: Failed to call export_pathbuilder") + +// ExportPathbuilders writes pathbuilders into the directory dest +func (wisski *WissKI) ExportPathbuilders(dest string) error { + // export all the pathbuilders into the buffer + var buffer bytes.Buffer + wu := stream.NewIOStream(&buffer, nil, nil, 0) + code, err := wisski.Stack().Exec(wu, "barrel", "/bin/bash", "/user_shell.sh", "-c", "drush php:script /wisskiutils/export_pathbuilder.php") + if err != nil || code != 0 { + return errPathbuildersExecFailed + } + + // decode them as a json array + var pathbuilders map[string]string + if err := json.NewDecoder(&buffer).Decode(&pathbuilders); err != nil { + return err + } + + // sort the names of the pathbuilders + names := maps.Keys(pathbuilders) + slices.Sort(names) + + // write each into a file! + for _, name := range names { + pbxml := []byte(pathbuilders[name]) + name := filepath.Join(dest, fmt.Sprintf("%s.xml", name)) + if err := os.WriteFile(name, pbxml, fs.ModePerm); err != nil { + return err + } + } + + return nil +} diff --git a/internal/component/sql/database.go b/internal/component/sql/database.go index 09d8faf..31e6dea 100644 --- a/internal/component/sql/database.go +++ b/internal/component/sql/database.go @@ -5,7 +5,7 @@ import ( "fmt" "io" - "github.com/FAU-CDI/wisski-distillery/pkg/bookkeeping" + "github.com/FAU-CDI/wisski-distillery/internal/bookkeeping" "github.com/FAU-CDI/wisski-distillery/pkg/logging" "github.com/FAU-CDI/wisski-distillery/pkg/sqle" "github.com/FAU-CDI/wisski-distillery/pkg/wait" @@ -165,7 +165,7 @@ func (sql SQL) PurgeDatabase(db string) error { } var errSQLUnableToCreateUser = errors.New("unable to create administrative user") -var errSQLUnsafeDatabaseName = errors.New("Bookkeeping database has an unsafe name") +var errSQLUnsafeDatabaseName = errors.New("bookkeeping database has an unsafe name") var errSQLUnableToCreate = errors.New("unable to create bookkeeping database") // Bootstrap bootstraps the SQL database, and makes sure that the bookkeeping table is up-to-date diff --git a/internal/wisski/backup.go b/internal/wisski/backup.go index 9b0b25b..75d4ed2 100644 --- a/internal/wisski/backup.go +++ b/internal/wisski/backup.go @@ -205,7 +205,7 @@ func (backup *Backup) run(io stream.IOStream, dis *Distillery) { } // list all instances - instances, err := dis.AllInstances() + instances, err := dis.Instances().All() if err != nil { backup.InstanceListErr = err return @@ -224,7 +224,7 @@ func (backup *Backup) run(io stream.IOStream, dis *Distillery) { } files <- dir - return instance.Snapshot(iochild, SnapshotDescription{ + return dis.Snapshot(instance, iochild, SnapshotDescription{ Dest: dir, }) }() diff --git a/internal/wisski/component.go b/internal/wisski/component.go index 9e14686..fc21a22 100644 --- a/internal/wisski/component.go +++ b/internal/wisski/component.go @@ -8,6 +8,7 @@ import ( "github.com/FAU-CDI/wisski-distillery/internal/component" "github.com/FAU-CDI/wisski-distillery/internal/component/dis" + "github.com/FAU-CDI/wisski-distillery/internal/component/instances" "github.com/FAU-CDI/wisski-distillery/internal/component/resolver" "github.com/FAU-CDI/wisski-distillery/internal/component/self" "github.com/FAU-CDI/wisski-distillery/internal/component/sql" @@ -23,7 +24,7 @@ type components struct { // m protects the fields below m sync.Mutex - // each component is only initialized once + // installable components web *web.Web self *self.Self resolver *resolver.Resolver @@ -31,6 +32,9 @@ type components struct { ssh *ssh.SSH ts *triplestore.Triplestore sql *sql.SQL + + // other components + instances *instances.Instances } // makeComponent makes or returns a component inside the [component] struct of the distillery @@ -73,8 +77,8 @@ func makeComponent[C component.Component](dis *Distillery, field *C, init func(C } // Components returns all components that have a stack function -func (dis *Distillery) Components() []component.ComponentWithStack { - return []component.ComponentWithStack{ +func (dis *Distillery) Components() []component.InstallableComponent { + return []component.InstallableComponent{ dis.Web(), dis.Self(), dis.Resolver(), @@ -122,3 +126,10 @@ func (dis *Distillery) Triplestore() *triplestore.Triplestore { ts.PollInterval = time.Second }) } + +func (dis *Distillery) Instances() *instances.Instances { + return makeComponent(dis, &dis.components.instances, func(instances *instances.Instances) { + instances.SQL = dis.SQL() + instances.TS = dis.Triplestore() + }) +} diff --git a/internal/wisski/instances.go b/internal/wisski/instances.go deleted file mode 100644 index e111cf8..0000000 --- a/internal/wisski/instances.go +++ /dev/null @@ -1,408 +0,0 @@ -package wisski - -import ( - "bytes" - "embed" - "encoding/json" - "fmt" - "io" - "io/fs" - "net/url" - "os" - "path/filepath" - "strings" - - "github.com/FAU-CDI/wisski-distillery/internal/component" - "github.com/FAU-CDI/wisski-distillery/pkg/bookkeeping" - "github.com/FAU-CDI/wisski-distillery/pkg/fsx" - "github.com/alessio/shellescape" - "github.com/pkg/errors" - "github.com/tkw1536/goprogram/exit" - "github.com/tkw1536/goprogram/stream" - "golang.org/x/exp/maps" - "golang.org/x/exp/slices" - "gorm.io/gorm" - "gorm.io/gorm/clause" -) - -var errNoBookkeeping = exit.Error{ - Message: "instance %q does not exist in bookkeeping table", - ExitCode: exit.ExitGeneric, -} - -var ErrInstanceNotFound = exit.Error{ - Message: "instance not found", - ExitCode: exit.ExitGeneric, -} - -var errSQL = exit.Error{ - Message: "Unknown SQL Error %s", - ExitCode: exit.ExitGeneric, -} - -// Instance returns the instance of the WissKI Distillery with the provided slug -func (dis *Distillery) Instance(slug string) (i Instance, err error) { - sql := dis.SQL() - if err := sql.Wait(); err != nil { - return i, err - } - - table, err := sql.OpenBookkeeping(false) - if err != nil { - return i, err - } - - // find the instance by slug - query := table.Where(&bookkeeping.Instance{Slug: slug}).Find(&i.Instance) - switch { - case query.Error != nil: - return i, errSQL.WithMessageF(query.Error) - case query.RowsAffected == 0: - return i, ErrInstanceNotFound - default: - i.dis = dis - return i, nil - } -} - -// HasInstance checks if the provided instance exists in the bookeeping table -func (dis *Distillery) HasInstance(slug string) (ok bool, err error) { - sql := dis.SQL() - if err := sql.Wait(); err != nil { - return false, err - } - - table, err := sql.OpenBookkeeping(false) - if err != nil { - return false, err - } - - query := table.Select("count(*) > 0").Where("slug = ?", slug).Find(&ok) - if query.Error != nil { - return false, errSQL.WithMessageF(query.Error) - } - return -} - -// Instances is like InstancesWith, except that when no slugs are provided, it calls AllInstances. -func (dis *Distillery) Instances(slugs ...string) ([]Instance, error) { - if len(slugs) == 0 { - return dis.AllInstances() - } - return dis.InstancesWith(slugs...) -} - -// AllInstances returns all instances of the WissKI Distillery in consistent order. -// -// There is no guarantee that this order remains identical between different api releases; however subsequent invocations are guaranteed to return the same order. -func (dis *Distillery) AllInstances() ([]Instance, error) { - return dis.findInstances(true, func(table *gorm.DB) *gorm.DB { - return table - }) -} - -// InstancesWith returns all instances where the slug is in the provided list of names. -// The returned instances are reordered in a consistent order. -func (dis *Distillery) InstancesWith(slugs ...string) ([]Instance, error) { - return dis.findInstances(true, func(table *gorm.DB) *gorm.DB { - return table.Where("slug IN ?", slugs) - }) -} - -// 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) { - sql := dis.SQL() - if err := sql.Wait(); err != nil { - return nil, err - } - - // open the bookkeeping table - table, err := sql.OpenBookkeeping(false) - if err != nil { - return nil, err - } - - // prepare a query - find := table - if order { - find = find.Order(clause.OrderByColumn{Column: clause.Column{Name: "slug"}, Desc: false}) - } - if query != nil { - find = query(find) - } - - // fetch bookkeeping instances - var bks []bookkeeping.Instance - find = find.Find(&bks) - if find.Error != nil { - return nil, errSQL.WithMessageF(find.Error) - } - - // make proper instances - instances = make([]Instance, len(bks)) - for i, bk := range bks { - instances[i].Instance = bk - instances[i].dis = dis - } - - return instances, nil -} - -// Instance represents a bookkeeping instance -type Instance struct { - bookkeeping.Instance - - // Credentials for the drupal instance - DrupalUsername string - DrupalPassword string - - dis *Distillery -} - -// Update updates the bookkeeping table with this instance. -func (instance *Instance) Update() error { - db, err := instance.dis.SQL().OpenBookkeeping(false) - if err != nil { - return err - } - - // it has never been created => we need to create it in the database - if instance.Instance.Created.IsZero() { - return db.Create(&instance.Instance).Error - } - - // Update based on the primary key! - return db.Where("pk = ?", instance.Instance.Pk).Updates(&instance.Instance).Error -} - -// Delete deletes this instance from the bookkeeping table -func (instance *Instance) Delete() error { - db, err := instance.dis.SQL().OpenBookkeeping(false) - if err != nil { - return err - } - - // doesn't exist => nothing to delete - if instance.Instance.Created.IsZero() { - return nil - } - - // delete it directly - return db.Delete(&instance.Instance).Error -} - -// Shell executes a shell command inside the -func (instance Instance) Shell(io stream.IOStream, argv ...string) (int, error) { - return instance.Stack().Exec(io, "barrel", "/bin/sh", append([]string{"/user_shell.sh"}, argv...)...) -} - -// Domain returns the full domain name of this instance -func (instance Instance) Domain() string { - return fmt.Sprintf("%s.%s", instance.Slug, instance.dis.Config.DefaultDomain) -} - -// URL returns the public URL of this instance -func (instance Instance) URL() *url.URL { - // setup domain and path - url := &url.URL{ - Host: instance.Domain(), - Path: "/", - } - - // use http or https scheme depending on if the distillery has it enabled - if instance.dis.Config.HTTPSEnabled() { - url.Scheme = "https" - } else { - url.Scheme = "http" - } - - return url -} - -//go:embed all:instances/barrel instances/barrel.env -var barrelResources embed.FS - -// Stack represents a stack representing this instance -func (instance Instance) Stack() component.Installable { - return component.Installable{ - Stack: component.Stack{ - Dir: instance.FilesystemBase, - }, - - Resources: barrelResources, - ContextPath: filepath.Join("instances", "barrel"), - EnvPath: filepath.Join("instances", "barrel.env"), - - EnvContext: map[string]string{ - "DATA_PATH": filepath.Join(instance.FilesystemBase, "data"), - - "SLUG": instance.Slug, - "VIRTUAL_HOST": instance.Domain(), - - "LETSENCRYPT_HOST": instance.dis.Config.IfHttps(instance.Domain()), - "LETSENCRYPT_EMAIL": instance.dis.Config.IfHttps(instance.dis.Config.CertbotEmail), - - "RUNTIME_DIR": instance.dis.Config.RuntimeDir(), - "GLOBAL_AUTHORIZED_KEYS_FILE": instance.dis.Config.GlobalAuthorizedKeysFile, - }, - - MakeDirsPerm: fs.ModeDir | fs.ModePerm, - MakeDirs: []string{"data", ".composer"}, - - TouchFiles: []string{ - filepath.Join("data", "authorized_keys"), - }, - } -} - -//go:embed all:instances/reserve instances/reserve.env -var reserveResources embed.FS - -func (instance Instance) ReserveStack() component.Installable { - return component.Installable{ - Stack: component.Stack{ - Dir: instance.FilesystemBase, - }, - - Resources: reserveResources, - ContextPath: filepath.Join("instances", "reserve"), - EnvPath: filepath.Join("instances", "reserve.env"), - - EnvContext: map[string]string{ - "VIRTUAL_HOST": instance.Domain(), - - "LETSENCRYPT_HOST": instance.dis.Config.IfHttps(instance.Domain()), - "LETSENCRYPT_EMAIL": instance.dis.Config.IfHttps(instance.dis.Config.CertbotEmail), - }, - } -} - -// Provision provisions an instance, assuming that the required databases already exist. -func (instance Instance) Provision(io stream.IOStream) error { - - // create the basic st! - st := instance.Stack() - if err := st.Install(io, component.InstallationContext{}); err != nil { - return err - } - - // Pull and build the stack! - if err := st.Update(io, false); err != nil { - return err - } - - provisionParams := []string{ - instance.Domain(), - - instance.SqlDatabase, - instance.SqlUser, - instance.SqlPassword, - - instance.GraphDBRepository, - instance.GraphDBUser, - instance.GraphDBPassword, - - instance.DrupalUsername, - instance.DrupalPassword, - - "", // TODO: DrupalVersion - "", // TODO: WissKIVersion - } - - // escape the parameter - for i, param := range provisionParams { - provisionParams[i] = shellescape.Quote(param) - } - - // figure out the provision script - // TODO: Move the provision script into the control plane! - provisionScript := "sudo PATH=$PATH -u www-data /bin/bash /provision_container.sh " + strings.Join(provisionParams, " ") - - code, err := st.Run(io, true, "barrel", "/bin/bash", "-c", provisionScript) - if err != nil { - return err - } - if code != 0 { - return errors.New("Unable to run provision script") - } - - return nil -} - -func (instance *Instance) NoPrefix() bool { - return fsx.IsFile(filepath.Join(instance.FilesystemBase, "prefixes.skip")) -} - -var errPrefixExecFailed = errors.New("PrefixConfig: Failed to call list_uri_prefixes") - -// PrefixConfig returns the prefix config belonging to this instance. -func (instance *Instance) PrefixConfig() (config string, err error) { - // if the user requested to skip the prefix, then don't do anything with it! - if instance.NoPrefix() { - return "", nil - } - - var builder strings.Builder - - // domain - builder.WriteString(instance.URL().String() + ":") - builder.WriteString("\n") - - // default prefixes - wu := stream.NewIOStream(&builder, nil, nil, 0) - code, err := instance.Stack().Exec(wu, "barrel", "/bin/bash", "/user_shell.sh", "-c", "drush php:script /wisskiutils/list_uri_prefixes.php") - if err != nil || code != 0 { - return "", errPrefixExecFailed - } - - // custom prefixes - prefixPath := filepath.Join(instance.FilesystemBase, "prefixes") - if fsx.IsFile(prefixPath) { - prefix, err := os.Open(prefixPath) - if err != nil { - return "", err - } - defer prefix.Close() - if _, err := io.Copy(&builder, prefix); err != nil { - return "", err - } - builder.WriteString("\n") - } - - // and done! - return builder.String(), nil -} - -var errPathbuildersExecFailed = errors.New("ExportPathbuilders: Failed to call export_pathbuilder") - -// ExportPathbuilders writes pathbuilders into the directory dest -func (instance *Instance) ExportPathbuilders(dest string) error { - // export all the pathbuilders into the buffer - var buffer bytes.Buffer - wu := stream.NewIOStream(&buffer, nil, nil, 0) - code, err := instance.Stack().Exec(wu, "barrel", "/bin/bash", "/user_shell.sh", "-c", "drush php:script /wisskiutils/export_pathbuilder.php") - if err != nil || code != 0 { - return errPathbuildersExecFailed - } - - // decode them as a json array - var pathbuilders map[string]string - if err := json.NewDecoder(&buffer).Decode(&pathbuilders); err != nil { - return err - } - - // sort the names of the pathbuilders - names := maps.Keys(pathbuilders) - slices.Sort(names) - - // write each into a file! - for _, name := range names { - pbxml := []byte(pathbuilders[name]) - name := filepath.Join(dest, fmt.Sprintf("%s.xml", name)) - if err := os.WriteFile(name, pbxml, fs.ModePerm); err != nil { - return err - } - } - - return nil -} diff --git a/internal/wisski/instances_provision.go b/internal/wisski/instances_provision.go deleted file mode 100644 index aeaaae8..0000000 --- a/internal/wisski/instances_provision.go +++ /dev/null @@ -1,90 +0,0 @@ -package wisski - -import ( - "path/filepath" - - "github.com/FAU-CDI/wisski-distillery/pkg/bookkeeping" - "github.com/FAU-CDI/wisski-distillery/pkg/stringparser" - "github.com/pkg/errors" -) - -func (dis *Distillery) InstancesDir() string { - return filepath.Join(dis.Config.DeployRoot, "instances") -} - -func (dis *Distillery) InstanceDir(slug string) string { - return filepath.Join(dis.InstancesDir(), slug) -} - -func (dis *Distillery) InstanceSQL(slug string) (database, user string) { - database = dis.Config.MysqlDatabasePrefix + slug - user = dis.Config.MysqlUserPrefix + slug - return -} - -func (dis *Distillery) InstanceGraphDB(slug string) (repo, user string) { - repo = dis.Config.GraphDBRepoPrefix + slug - user = dis.Config.GraphDBUserPrefix + slug - return -} - -var errInvalidSlug = errors.New("Not a valid slug") - -// NewInstance fills the struct for a new distillery instance. -// It validates that slug is a valid name for an instance. -// -// It does not perform any checks if the instance already exists, or does the creation in the database. -func (dis *Distillery) NewInstance(slug string) (i Instance, err error) { - - // make sure that the slug is valid! - if _, err := stringparser.ParseSlug(slug); err != nil { - return i, errInvalidSlug - } - - // generate sql data - sqlPassword, err := dis.Config.NewPassword() - if err != nil { - return i, err - } - sqlDB, sqlUser := dis.InstanceSQL(slug) - - // generate ts data - tsPassword, err := dis.Config.NewPassword() - if err != nil { - return i, err - } - tsRepo, tsUser := dis.InstanceGraphDB(slug) - - // generate drupal data - drPassword, err := dis.Config.NewPassword() - if err != nil { - return i, err - } - drUser := "admin" - - // make the instance object! - instance := bookkeeping.Instance{ - Slug: slug, - - OwnerEmail: "", - AutoBlindUpdateEnabled: true, - - FilesystemBase: dis.InstanceDir(slug), - - SqlDatabase: sqlDB, - SqlUser: sqlUser, - SqlPassword: sqlPassword, - - GraphDBRepository: tsRepo, - GraphDBUser: tsUser, - GraphDBPassword: tsPassword, - } - - i.DrupalUsername = drUser - i.DrupalPassword = drPassword - - // store the instance in the object and return it! - i.Instance = instance - i.dis = dis - return i, nil -} diff --git a/internal/wisski/server.go b/internal/wisski/server.go index 03f9654..da06a36 100644 --- a/internal/wisski/server.go +++ b/internal/wisski/server.go @@ -19,7 +19,7 @@ func (dis *Distillery) Server() *Server { } func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { - instances, err := s.dis.AllInstances() + instances, err := s.dis.Instances().All() if err != nil { w.WriteHeader(http.StatusInternalServerError) io.WriteString(w, "Something went wrong") diff --git a/internal/wisski/snapshot.go b/internal/wisski/snapshot.go index 7ec04d3..9d992d6 100644 --- a/internal/wisski/snapshot.go +++ b/internal/wisski/snapshot.go @@ -10,7 +10,8 @@ import ( "strings" "time" - "github.com/FAU-CDI/wisski-distillery/pkg/bookkeeping" + "github.com/FAU-CDI/wisski-distillery/internal/bookkeeping" + "github.com/FAU-CDI/wisski-distillery/internal/component/instances" "github.com/FAU-CDI/wisski-distillery/pkg/countwriter" "github.com/FAU-CDI/wisski-distillery/pkg/fsx" "github.com/FAU-CDI/wisski-distillery/pkg/logging" @@ -161,7 +162,7 @@ func (snapshot Snapshot) Report(w io.Writer) (int, error) { } // Snapshot creates a new snapshot of this instance into dest -func (instance Instance) Snapshot(io stream.IOStream, desc SnapshotDescription) (snapshot Snapshot) { +func (dis *Distillery) Snapshot(instance instances.WissKI, io stream.IOStream, desc SnapshotDescription) (snapshot Snapshot) { // setup the snapshot snapshot.Description = desc snapshot.Instance = instance.Instance @@ -175,8 +176,8 @@ func (instance Instance) Snapshot(io stream.IOStream, desc SnapshotDescription) logging.LogOperation(func() error { snapshot.StartTime = time.Now() - snapshot.makeBlackbox(io, instance) - snapshot.makeWhitebox(io, instance) + snapshot.makeBlackbox(io, dis, instance) + snapshot.makeWhitebox(io, dis, instance) snapshot.EndTime = time.Now() return nil @@ -188,7 +189,7 @@ func (instance Instance) Snapshot(io stream.IOStream, desc SnapshotDescription) // makeBlackbox runs the blackbox backup of the system. // It pauses the Instance, if a consistent state is required. -func (snapshot *Snapshot) makeBlackbox(io stream.IOStream, instance Instance) { +func (snapshot *Snapshot) makeBlackbox(io stream.IOStream, dis *Distillery, instance instances.WissKI) { stack := instance.Stack() og := opgroup.NewOpGroup[string](4) @@ -243,7 +244,7 @@ func (snapshot *Snapshot) makeBlackbox(io stream.IOStream, instance Instance) { defer nquads.Close() // directly store the result - _, err = instance.dis.Triplestore().Backup(nquads, instance.GraphDBRepository) + _, err = dis.Triplestore().Backup(nquads, instance.GraphDBRepository) return err }, &snapshot.ErrTriplestore) @@ -259,7 +260,7 @@ func (snapshot *Snapshot) makeBlackbox(io stream.IOStream, instance Instance) { defer sql.Close() // directly store the result - return instance.dis.SQL().Backup(io, sql, instance.SqlDatabase) + return dis.SQL().Backup(io, sql, instance.SqlDatabase) }, &snapshot.ErrSQL) // wait for the group! @@ -268,7 +269,7 @@ func (snapshot *Snapshot) makeBlackbox(io stream.IOStream, instance Instance) { // makeWhitebox runs the whitebox backup of the system. // The instance should be running during this step. -func (snapshot *Snapshot) makeWhitebox(io stream.IOStream, instance Instance) { +func (snapshot *Snapshot) makeWhitebox(io stream.IOStream, dis *Distillery, instance instances.WissKI) { og := opgroup.NewOpGroup[string](1) // write pathbuilders