Move code into new component package
This commit cleans up the resources in the 'embed' package, and instead moves them into subpackages of a new 'compose' package. This makes sure that '.env' templates and docker compose contexts are located in the same location.
This commit is contained in:
parent
2ee90bf462
commit
7b2f79bea1
44 changed files with 579 additions and 559 deletions
78
env/component.go
vendored
78
env/component.go
vendored
|
|
@ -2,15 +2,27 @@ package env
|
|||
|
||||
import (
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/component"
|
||||
"github.com/FAU-CDI/wisski-distillery/component/dis"
|
||||
"github.com/FAU-CDI/wisski-distillery/component/resolver"
|
||||
"github.com/FAU-CDI/wisski-distillery/component/self"
|
||||
"github.com/FAU-CDI/wisski-distillery/component/sql"
|
||||
"github.com/FAU-CDI/wisski-distillery/component/ssh"
|
||||
"github.com/FAU-CDI/wisski-distillery/component/triplestore"
|
||||
"github.com/FAU-CDI/wisski-distillery/component/web"
|
||||
"github.com/FAU-CDI/wisski-distillery/embed"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/stack"
|
||||
)
|
||||
|
||||
// TODO: Remove me when migration is complete
|
||||
type Component = component.Component
|
||||
|
||||
// TODO: Move everything into specific subpackages
|
||||
|
||||
// Stacks returns the Stacks of this distillery
|
||||
func (dis *Distillery) Components() []Component {
|
||||
func (dis *Distillery) Components() []component.Component {
|
||||
// TODO: Do we want to cache these components?
|
||||
return []Component{
|
||||
dis.Web(),
|
||||
|
|
@ -23,17 +35,69 @@ func (dis *Distillery) Components() []Component {
|
|||
}
|
||||
}
|
||||
|
||||
// Component represents a component of the distillery
|
||||
type Component interface {
|
||||
Name() string // Name is the name of this component
|
||||
// Web returns the web component belonging to this distillery
|
||||
func (dis *Distillery) Web() (web web.Web) {
|
||||
dis.makeComponent(web, &web.ComponentBase)
|
||||
return
|
||||
}
|
||||
|
||||
Stack() stack.Installable // Stack returns the installable stack representing this component
|
||||
Context(parent stack.InstallationContext) stack.InstallationContext // context for installation
|
||||
// Self returns the self component belonging to this distillery
|
||||
func (dis *Distillery) Self() (self self.Self) {
|
||||
dis.makeComponent(self, &self.ComponentBase)
|
||||
return
|
||||
}
|
||||
|
||||
Path() string // Path returns the path to this component
|
||||
// Resolver returns the resolver component belonging to this distillery
|
||||
func (dis *Distillery) Resolver() (resolver resolver.Resolver) {
|
||||
resolver.ConfigName = "prefix.cfg" // TODO: Move into core?
|
||||
resolver.Executable = dis.CurrentExecutable()
|
||||
|
||||
dis.makeComponent(resolver, &resolver.ComponentBase)
|
||||
return
|
||||
}
|
||||
|
||||
// Dis returns the dis component belonging to this distillery
|
||||
func (dis *Distillery) Dis() (ddis dis.Dis) {
|
||||
ddis.Executable = dis.CurrentExecutable()
|
||||
|
||||
dis.makeComponent(ddis, &ddis.ComponentBase)
|
||||
return
|
||||
}
|
||||
|
||||
// SSH returns the SSH component belonging to this distillery
|
||||
func (dis *Distillery) SSH() (ssh ssh.SSH) {
|
||||
dis.makeComponent(ssh, &ssh.ComponentBase)
|
||||
return
|
||||
}
|
||||
|
||||
// SQL returns the SQL component belonging to this distillery
|
||||
func (dis *Distillery) SQL() (sql sql.SQL) {
|
||||
sql.ServerURL = dis.Upstream.SQL
|
||||
sql.PollContext = dis.Context()
|
||||
sql.PollInterval = time.Second
|
||||
|
||||
dis.makeComponent(sql, &sql.ComponentBase)
|
||||
return
|
||||
}
|
||||
|
||||
// Triplestore returns the TriplestoreComponent belonging to this distillery
|
||||
func (dis *Distillery) Triplestore() (ts triplestore.Triplestore) {
|
||||
ts.BaseURL = "http://" + dis.Upstream.Triplestore
|
||||
ts.PollContext = dis.Context()
|
||||
ts.PollInterval = time.Second
|
||||
|
||||
dis.makeComponent(ts, &ts.ComponentBase)
|
||||
return
|
||||
}
|
||||
|
||||
// makeComponent updates the baseComponent belonging to component
|
||||
func (dis *Distillery) makeComponent(component component.Component, base *component.ComponentBase) {
|
||||
base.Dir = dis.getComponentPath(component)
|
||||
base.Config = dis.Config
|
||||
}
|
||||
|
||||
// asCoreStack treats the provided stack as a core component of this distillery.
|
||||
// TODO: this should no longer be used
|
||||
func (dis *Distillery) makeComponentStack(component Component, stack stack.Installable) stack.Installable {
|
||||
stack.Dir = dis.getComponentPath(component)
|
||||
|
||||
|
|
|
|||
47
env/component_dis.go
vendored
47
env/component_dis.go
vendored
|
|
@ -1,47 +0,0 @@
|
|||
package env
|
||||
|
||||
import (
|
||||
"github.com/FAU-CDI/wisski-distillery/core"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/stack"
|
||||
)
|
||||
|
||||
// DisComponent represents the 'dis' layer belonging to a distillery
|
||||
type DisComponent struct {
|
||||
dis *Distillery
|
||||
}
|
||||
|
||||
// Dis returns the DisComponent belonging to this distillery
|
||||
func (dis *Distillery) Dis() DisComponent {
|
||||
return DisComponent{dis: dis}
|
||||
}
|
||||
|
||||
func (DisComponent) Name() string {
|
||||
return "dis"
|
||||
}
|
||||
|
||||
func (dis DisComponent) Stack() stack.Installable {
|
||||
return dis.dis.makeComponentStack(dis, stack.Installable{
|
||||
EnvContext: map[string]string{
|
||||
"VIRTUAL_HOST": dis.dis.DefaultVirtualHost(),
|
||||
"LETSENCRYPT_HOST": dis.dis.DefaultLetsencryptHost(),
|
||||
"LETSENCRYPT_EMAIL": dis.dis.Config.CertbotEmail,
|
||||
|
||||
"CONFIG_PATH": dis.dis.Config.ConfigPath,
|
||||
"DEPLOY_ROOT": dis.dis.Config.DeployRoot,
|
||||
|
||||
"GLOBAL_AUTHORIZED_KEYS_FILE": dis.dis.Config.GlobalAuthorizedKeysFile,
|
||||
"SELF_OVERRIDES_FILE": dis.dis.Config.SelfOverridesFile,
|
||||
},
|
||||
CopyContextFiles: []string{core.Executable},
|
||||
})
|
||||
}
|
||||
|
||||
func (dis DisComponent) Context(parent stack.InstallationContext) stack.InstallationContext {
|
||||
return stack.InstallationContext{
|
||||
core.Executable: dis.dis.CurrentExecutable(),
|
||||
}
|
||||
}
|
||||
|
||||
func (dis DisComponent) Path() string {
|
||||
return dis.Stack().Dir
|
||||
}
|
||||
112
env/component_resolver.go
vendored
112
env/component_resolver.go
vendored
|
|
@ -1,112 +0,0 @@
|
|||
package env
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
|
||||
"github.com/FAU-CDI/wdresolve"
|
||||
"github.com/FAU-CDI/wdresolve/resolvers"
|
||||
"github.com/FAU-CDI/wisski-distillery/core"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/stack"
|
||||
"github.com/tkw1536/goprogram/stream"
|
||||
)
|
||||
|
||||
// ResolverComponent represents the 'resolver' layer belonging to a distillery
|
||||
type ResolverComponent struct {
|
||||
ConfigName string // Filename of the configuration file
|
||||
|
||||
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 {
|
||||
return resolver.dis.makeComponentStack(resolver, stack.Installable{
|
||||
EnvContext: map[string]string{
|
||||
"VIRTUAL_HOST": resolver.dis.DefaultVirtualHost(),
|
||||
"LETSENCRYPT_HOST": resolver.dis.DefaultLetsencryptHost(),
|
||||
"LETSENCRYPT_EMAIL": resolver.dis.Config.CertbotEmail,
|
||||
|
||||
"CONFIG_PATH": resolver.dis.Config.ConfigPath,
|
||||
"DEPLOY_ROOT": resolver.dis.Config.DeployRoot,
|
||||
|
||||
"GLOBAL_AUTHORIZED_KEYS_FILE": resolver.dis.Config.GlobalAuthorizedKeysFile,
|
||||
"SELF_OVERRIDES_FILE": resolver.dis.Config.SelfOverridesFile,
|
||||
"RESOLVER_CONFIG": resolver.ConfigPath(),
|
||||
},
|
||||
TouchFiles: []string{resolver.ConfigName},
|
||||
CopyContextFiles: []string{core.Executable},
|
||||
})
|
||||
}
|
||||
|
||||
func (resolver ResolverComponent) Context(parent stack.InstallationContext) stack.InstallationContext {
|
||||
return stack.InstallationContext{
|
||||
core.Executable: resolver.dis.CurrentExecutable(),
|
||||
}
|
||||
}
|
||||
|
||||
func (resolver ResolverComponent) Server(io stream.IOStream) (p wdresolve.ResolveHandler, err error) {
|
||||
p.TrustXForwardedProto = true
|
||||
|
||||
fallback := &resolvers.Regexp{
|
||||
Data: map[string]string{},
|
||||
}
|
||||
|
||||
// handle the default domain name!
|
||||
domainName := resolver.dis.Config.DefaultDomain
|
||||
if domainName != "" {
|
||||
fallback.Data[fmt.Sprintf("^https?://(.*)\\.%s", regexp.QuoteMeta(domainName))] = fmt.Sprintf("https://$1.%s", domainName)
|
||||
io.Printf("registering default domain %s\n", domainName)
|
||||
}
|
||||
|
||||
// handle the extra domains!
|
||||
for _, domain := range resolver.dis.Config.SelfExtraDomains {
|
||||
fallback.Data[fmt.Sprintf("^https?://(.*)\\.%s", regexp.QuoteMeta(domain))] = fmt.Sprintf("https://$1.%s", domainName)
|
||||
io.Printf("registering legacy domain %s\n", domain)
|
||||
}
|
||||
|
||||
// open the prefix file
|
||||
prefixFile := resolver.ConfigPath()
|
||||
fs, err := os.Open(prefixFile)
|
||||
io.Println("loading prefixes from ", prefixFile)
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
defer fs.Close()
|
||||
|
||||
// read the prefixes
|
||||
// TODO: Do we want to load these without a file?
|
||||
prefixes, err := resolvers.ReadPrefixes(fs)
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
|
||||
// and use that as the resolver!
|
||||
p.Resolver = resolvers.InOrder{
|
||||
prefixes,
|
||||
fallback,
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (resolver ResolverComponent) Path() string {
|
||||
return resolver.dis.getComponentPath(resolver)
|
||||
}
|
||||
|
||||
func (resolver ResolverComponent) ConfigPath() string {
|
||||
return filepath.Join(resolver.Path(), resolver.ConfigName)
|
||||
}
|
||||
42
env/component_self.go
vendored
42
env/component_self.go
vendored
|
|
@ -1,42 +0,0 @@
|
|||
package env
|
||||
|
||||
import "github.com/FAU-CDI/wisski-distillery/internal/stack"
|
||||
|
||||
// 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 (SelfComponent) Context(parent stack.InstallationContext) stack.InstallationContext {
|
||||
return parent
|
||||
}
|
||||
|
||||
func (sc SelfComponent) Stack() stack.Installable {
|
||||
TARGET := "https://github.com/FAU-CDI/wisski-distillery"
|
||||
if sc.dis.Config.SelfRedirect != nil {
|
||||
TARGET = sc.dis.Config.SelfRedirect.String()
|
||||
}
|
||||
|
||||
return sc.dis.makeComponentStack(sc, stack.Installable{
|
||||
EnvContext: map[string]string{
|
||||
"VIRTUAL_HOST": sc.dis.DefaultVirtualHost(),
|
||||
"LETSENCRYPT_HOST": sc.dis.DefaultLetsencryptHost(),
|
||||
"LETSENCRYPT_EMAIL": sc.dis.Config.CertbotEmail,
|
||||
"TARGET": TARGET,
|
||||
"OVERRIDES_FILE": sc.dis.Config.SelfOverridesFile,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (sc SelfComponent) Path() string {
|
||||
return sc.Stack().Dir
|
||||
}
|
||||
262
env/component_sql.go
vendored
262
env/component_sql.go
vendored
|
|
@ -1,262 +0,0 @@
|
|||
package env
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"time"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/bookkeeping"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/logging"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/sqle"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/stack"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/wait"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/tkw1536/goprogram/stream"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
// SQLComponent represents the 'sql' layer belonging to a distillery
|
||||
type SQLComponent struct {
|
||||
ServerURL string
|
||||
|
||||
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{
|
||||
ServerURL: dis.Upstream.SQL,
|
||||
PollInterval: time.Second,
|
||||
|
||||
dis: dis,
|
||||
}
|
||||
}
|
||||
|
||||
func (SQLComponent) Name() string {
|
||||
return "sql"
|
||||
}
|
||||
|
||||
func (SQLComponent) Context(parent stack.InstallationContext) stack.InstallationContext {
|
||||
return parent
|
||||
}
|
||||
|
||||
// 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",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// SQLStackPath returns the path the SQLStack() lives at.
|
||||
func (sql SQLComponent) Path() string {
|
||||
return sql.Stack().Dir
|
||||
}
|
||||
|
||||
// sqlOpen opens a new sql connection to the provided database using the administrative credentials
|
||||
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, sql.ServerURL, database),
|
||||
DefaultStringSize: 256,
|
||||
}
|
||||
|
||||
db, err := gorm.Open(mysql.New(cfg), config)
|
||||
if err != nil {
|
||||
return db, err
|
||||
}
|
||||
|
||||
gdb, err := db.DB()
|
||||
if err != nil {
|
||||
return db, err
|
||||
}
|
||||
gdb.SetMaxIdleConns(0)
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
// OpenBookkeeping opens a connection to the bookkeeping database
|
||||
func (sql SQLComponent) OpenBookkeeping(silent bool) (*gorm.DB, error) {
|
||||
|
||||
config := &gorm.Config{}
|
||||
if silent {
|
||||
config.Logger = logger.Default.LogMode(logger.Silent)
|
||||
}
|
||||
|
||||
// open the database
|
||||
db, err := sql.openDatabase(sql.dis.Config.DistilleryBookkeepingDatabase, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// load the table
|
||||
table := db.Table(sql.dis.Config.DistilleryBookkeepingTable)
|
||||
if table.Error != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return table, nil
|
||||
}
|
||||
|
||||
var errSQLBackup = errors.New("SQLBackup: Mysqldump returned non-zero exit code")
|
||||
|
||||
// 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 := sql.Stack().Exec(io, "sql", "mysqldump", "--databases", database)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if code != 0 {
|
||||
return errSQLBackup
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BackupAll makes a backup of all sql databases
|
||||
func (sql SQLComponent) BackupAll(io stream.IOStream, dest io.Writer) error {
|
||||
io = stream.NewIOStream(dest, io.Stderr, nil, 0)
|
||||
|
||||
code, err := sql.Stack().Exec(io, "sql", "mysqldump", "--all-databases")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if code != 0 {
|
||||
return errSQLBackup
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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...)
|
||||
}
|
||||
|
||||
// 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 := sql.OpenShell(n, "-e", "show databases;")
|
||||
return err == nil && code == 0
|
||||
}, sql.PollInterval, sql.dis.Context())
|
||||
}
|
||||
|
||||
// Wait waits for a connection to the bookkeeping table to suceed
|
||||
func (sql SQLComponent) Wait() error {
|
||||
return wait.Wait(func() bool {
|
||||
_, err := sql.OpenBookkeeping(true)
|
||||
return err == nil
|
||||
}, sql.PollInterval, sql.dis.Context())
|
||||
}
|
||||
|
||||
var errInvalidDatabaseName = errors.New("SQLProvision: Invalid database name")
|
||||
|
||||
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 (sql SQLComponent) Provision(name, user, password string) error {
|
||||
// wait for the database
|
||||
if err := sql.WaitShell(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// it's not a safe database name!
|
||||
if !sqle.IsSafeDatabaseName(name) {
|
||||
return errInvalidDatabaseName
|
||||
}
|
||||
|
||||
// create the database and 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")
|
||||
}
|
||||
|
||||
// and done!
|
||||
return nil
|
||||
}
|
||||
|
||||
var errSQLPurgeUser = errors.New("unable to delete user")
|
||||
|
||||
// SQLPurgeUser deletes the specified user from the database
|
||||
func (sql SQLComponent) PurgeUser(user string) error {
|
||||
if !sql.Query("DROP USER IF EXISTS ?@`%`; FLUSH PRIVILEGES; ", user) {
|
||||
return errSQLPurgeUser
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var errSQLPurgeDB = errors.New("unable to drop database")
|
||||
|
||||
// SQLPurgeDatabase deletes the specified db from the database
|
||||
func (sql SQLComponent) PurgeDatabase(db string) error {
|
||||
if !sqle.IsSafeDatabaseName(db) {
|
||||
return errSQLPurgeDB
|
||||
}
|
||||
if !sql.Query("DROP DATABASE IF EXISTS `" + db + "`") {
|
||||
return errSQLPurgeDB
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var errSQLUnableToCreateUser = errors.New("unable to create administrative user")
|
||||
var errSQLUnsafeDatabaseName = errors.New("Bookkeeping database has an unsafe name")
|
||||
var errSQLUnableToCreate = errors.New("unable to create bookkeeping database")
|
||||
|
||||
// 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 := 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
|
||||
}
|
||||
}
|
||||
|
||||
// create the admin user
|
||||
logging.LogMessage(io, "Creating sql database")
|
||||
{
|
||||
if !sqle.IsSafeDatabaseName(sql.dis.Config.DistilleryBookkeepingDatabase) {
|
||||
return errSQLUnsafeDatabaseName
|
||||
}
|
||||
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")
|
||||
sql.Wait()
|
||||
|
||||
// open the database
|
||||
logging.LogMessage(io, "Migrating bookkeeping table")
|
||||
{
|
||||
db, err := sql.OpenBookkeeping(false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to access bookkeeping table: %s", err)
|
||||
}
|
||||
|
||||
if err := db.AutoMigrate(&bookkeeping.Instance{}); err != nil {
|
||||
return fmt.Errorf("unable to migrate bookkeeping table: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
29
env/component_ssh.go
vendored
29
env/component_ssh.go
vendored
|
|
@ -1,29 +0,0 @@
|
|||
package env
|
||||
|
||||
import "github.com/FAU-CDI/wisski-distillery/internal/stack"
|
||||
|
||||
// SSHComponent represents the 'ssh' layer belonging to a distillery
|
||||
type SSHComponent struct {
|
||||
dis *Distillery
|
||||
}
|
||||
|
||||
// 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 (SSHComponent) Context(parent stack.InstallationContext) stack.InstallationContext {
|
||||
return parent
|
||||
}
|
||||
|
||||
func (ssh SSHComponent) Path() string {
|
||||
return ssh.Stack().Dir
|
||||
}
|
||||
361
env/component_triplestore.go
vendored
361
env/component_triplestore.go
vendored
|
|
@ -1,361 +0,0 @@
|
|||
package env
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/embed"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/fsx"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/logging"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/stack"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/unpack"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/wait"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/tkw1536/goprogram/exit"
|
||||
"github.com/tkw1536/goprogram/stream"
|
||||
)
|
||||
|
||||
// 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://" + dis.Upstream.Triplestore,
|
||||
PollInterval: time.Second,
|
||||
|
||||
dis: dis,
|
||||
}
|
||||
}
|
||||
|
||||
func (TriplestoreComponent) Name() string {
|
||||
return "triplestore"
|
||||
}
|
||||
|
||||
func (TriplestoreComponent) Context(parent stack.InstallationContext) stack.InstallationContext {
|
||||
return parent
|
||||
}
|
||||
|
||||
// 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,
|
||||
MakeDirs: []string{
|
||||
filepath.Join("data", "data"),
|
||||
filepath.Join("data", "work"),
|
||||
filepath.Join("data", "logs"),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (ts TriplestoreComponent) Path() string {
|
||||
return ts.Stack().Dir
|
||||
}
|
||||
|
||||
type TriplestoreUserPayload struct {
|
||||
Password string `json:"password"`
|
||||
AppSettings TriplestoreUserAppSettings `json:"appSettings"`
|
||||
GrantedAuthorities []string `json:"grantedAuthorities"`
|
||||
}
|
||||
type TriplestoreUserAppSettings struct {
|
||||
DefaultInference bool `json:"DEFAULT_INFERENCE"`
|
||||
DefaultVisGraphSchema bool `json:"DEFAULT_VIS_GRAPH_SCHEMA"`
|
||||
DefaultSameas bool `json:"DEFAULT_SAMEAS"`
|
||||
IgnoreSharedQueries bool `json:"IGNORE_SHARED_QUERIES"`
|
||||
ExecuteCount bool `json:"EXECUTE_COUNT"`
|
||||
}
|
||||
|
||||
// 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 (ts TriplestoreComponent) OpenRaw(method, url string, body interface{}, bodyName string, accept string) (*http.Response, error) {
|
||||
var reader io.Reader
|
||||
|
||||
var contentType string
|
||||
|
||||
// for "PUT" and "POST" we setup a body
|
||||
if method == "PUT" || method == "POST" {
|
||||
if bodyName != "" {
|
||||
buffer := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(buffer)
|
||||
contentType = writer.FormDataContentType()
|
||||
|
||||
part, err := writer.CreateFormFile(bodyName, "filename.txt")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
io.Copy(part, bytes.NewReader(body.([]byte)))
|
||||
writer.Close()
|
||||
reader = buffer
|
||||
} else {
|
||||
contentType = "application/json"
|
||||
mbytes, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reader = bytes.NewReader(mbytes)
|
||||
}
|
||||
}
|
||||
|
||||
// create the request object
|
||||
req, err := http.NewRequest(method, ts.BaseURL+url, reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Setup configuration!
|
||||
if accept != "" {
|
||||
req.Header.Set("Accept", accept)
|
||||
}
|
||||
if contentType != "" {
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
}
|
||||
req.SetBasicAuth(ts.dis.Config.TriplestoreAdminUser, ts.dis.Config.TriplestoreAdminPassword)
|
||||
|
||||
// and send it
|
||||
return http.DefaultClient.Do(req)
|
||||
}
|
||||
|
||||
// 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 := ts.OpenRaw("GET", "/rest/repositories", nil, "", "")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer res.Body.Close()
|
||||
return true
|
||||
}, ts.PollInterval, ts.dis.Context())
|
||||
}
|
||||
|
||||
var errTripleStoreFailedRepository = exit.Error{
|
||||
Message: "Failed to create repository: %s",
|
||||
ExitCode: exit.ExitGeneric,
|
||||
}
|
||||
|
||||
func (ts TriplestoreComponent) Provision(name, domain, user, password string) error {
|
||||
if err := ts.Wait(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// prepare the create repo request
|
||||
// TODO: Move this into a seperate file
|
||||
createRepo, _, err := unpack.UnpackTemplate(
|
||||
map[string]string{
|
||||
"GRAPHDB_REPO": name,
|
||||
"INSTANCE_DOMAIN": domain,
|
||||
},
|
||||
fsx.OpenFS(filepath.Join("resources", "templates", "repository", "graphdb-repo.ttl"), embed.ResourceEmbed),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// do the create!
|
||||
{
|
||||
res, err := ts.OpenRaw("POST", "/rest/repositories", createRepo, "config", "")
|
||||
if err != nil {
|
||||
return errTripleStoreFailedRepository.WithMessageF(err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusCreated {
|
||||
return errTripleStoreFailedRepository.WithMessageF("Repo create did not return status code 201")
|
||||
}
|
||||
}
|
||||
|
||||
// create the user and grant them access
|
||||
{
|
||||
res, err := ts.OpenRaw("POST", "/rest/security/users/"+user, TriplestoreUserPayload{
|
||||
Password: password,
|
||||
AppSettings: TriplestoreUserAppSettings{
|
||||
DefaultInference: true,
|
||||
DefaultVisGraphSchema: true,
|
||||
DefaultSameas: true,
|
||||
IgnoreSharedQueries: false,
|
||||
ExecuteCount: true,
|
||||
},
|
||||
GrantedAuthorities: []string{
|
||||
"ROLE_USER",
|
||||
"READ_REPO_" + name,
|
||||
"WRITE_REPO_" + name,
|
||||
},
|
||||
}, "", "")
|
||||
if err != nil {
|
||||
return errTripleStoreFailedRepository.WithMessageF(err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusCreated {
|
||||
return errTripleStoreFailedRepository.WithMessageF("User create did not return status code 201")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TriplestorePurgeUser deletes the specified user from the triplestore
|
||||
func (ts TriplestoreComponent) PurgeUser(user string) error {
|
||||
res, err := ts.OpenRaw("DELETE", "/rest/security/users/"+user, nil, "", "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if res.StatusCode != http.StatusNoContent {
|
||||
return errors.Errorf("Delete returned code %d", res.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TriplestorePurgeRepo deletes the specified repo from the triplestore
|
||||
func (ts TriplestoreComponent) PurgeRepo(repo string) error {
|
||||
res, err := ts.OpenRaw("DELETE", "/rest/repositories/"+repo, nil, "", "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return errors.Errorf("Delete returned code %d", res.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var errTSBackupWrongStatusCode = errors.New("Distillery.Backup: Wrong status code")
|
||||
|
||||
// TriplestoreBackup backs up the repository named repo into the writer dst.
|
||||
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
|
||||
}
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return 0, errTSBackupWrongStatusCode
|
||||
}
|
||||
defer res.Body.Close()
|
||||
return io.Copy(dst, res.Body)
|
||||
}
|
||||
|
||||
type Repository struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
URI string `json:"uri"`
|
||||
Type string `json:"type"`
|
||||
SesameType string `json:"sesameType"`
|
||||
Location string `json:"location"`
|
||||
Readable bool `json:"readable"`
|
||||
Writable bool `json:"writable"`
|
||||
Local bool `json:"local"`
|
||||
}
|
||||
|
||||
func (ts TriplestoreComponent) listRepositories() (repos []Repository, err error) {
|
||||
res, err := ts.OpenRaw("GET", "/rest/repositories", nil, "", "application/json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
err = json.NewDecoder(res.Body).Decode(&repos)
|
||||
return
|
||||
}
|
||||
|
||||
// TriplestoreBackup backs up every graphdb instance into dst
|
||||
func (ts TriplestoreComponent) BackupAll(dst string) error {
|
||||
// list all the repositories
|
||||
repos, err := ts.listRepositories()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// create the base directory
|
||||
if err := os.Mkdir(dst, fs.ModeDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// iterate over all the repositories
|
||||
for _, repo := range repos {
|
||||
if rErr := (func(repo Repository) error {
|
||||
name := filepath.Join(dst, repo.ID+".nq")
|
||||
|
||||
dest, err := os.Create(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dest.Close()
|
||||
|
||||
_, err = ts.Backup(dest, repo.ID)
|
||||
return err
|
||||
}(repo)); err == nil && rErr != nil {
|
||||
err = rErr
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
var errTriplestoreFailedSecurity = errors.New("failed to enable triplestore security: request did not succeed with HTTP 200 OK")
|
||||
|
||||
func (ts TriplestoreComponent) Bootstrap(io stream.IOStream) error {
|
||||
logging.LogMessage(io, "Waiting for Triplestore")
|
||||
if err := ts.Wait(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logging.LogMessage(io, "Resetting admin user password")
|
||||
{
|
||||
res, err := ts.OpenRaw("PUT", "/rest/security/users/"+ts.dis.Config.TriplestoreAdminUser, TriplestoreUserPayload{
|
||||
Password: ts.dis.Config.TriplestoreAdminPassword,
|
||||
AppSettings: TriplestoreUserAppSettings{
|
||||
DefaultInference: true,
|
||||
DefaultVisGraphSchema: true,
|
||||
DefaultSameas: true,
|
||||
IgnoreSharedQueries: false,
|
||||
ExecuteCount: true,
|
||||
},
|
||||
GrantedAuthorities: []string{"ROLE_ADMIN"},
|
||||
}, "", "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create triplestore user: %s", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
switch res.StatusCode {
|
||||
case http.StatusOK:
|
||||
// we set the password => requests are unauthorized
|
||||
// so we still need to enable security (see below!)
|
||||
case http.StatusUnauthorized:
|
||||
// a password is needed => security is already enabled.
|
||||
// the password may or may not work, but that's a problem for later
|
||||
logging.LogMessage(io, "Security is already enabled")
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("failed to create triplestore user: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
logging.LogMessage(io, "Enabling Triplestore security")
|
||||
{
|
||||
res, err := ts.OpenRaw("POST", "/rest/security", true, "", "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to enable triplestore security: %s", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return errTriplestoreFailedSecurity
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
39
env/component_web.go
vendored
39
env/component_web.go
vendored
|
|
@ -1,39 +0,0 @@
|
|||
package env
|
||||
|
||||
import "github.com/FAU-CDI/wisski-distillery/internal/stack"
|
||||
|
||||
// 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 {
|
||||
HTTPS_METHOD := "nohttp"
|
||||
if web.dis.HTTPSEnabled() {
|
||||
HTTPS_METHOD = "redirect"
|
||||
}
|
||||
|
||||
return web.dis.makeComponentStack(web, stack.Installable{
|
||||
EnvContext: map[string]string{
|
||||
"DEFAULT_HOST": web.dis.Config.DefaultDomain,
|
||||
"HTTPS_METHOD": HTTPS_METHOD,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (WebComponent) Context(parent stack.InstallationContext) stack.InstallationContext {
|
||||
return parent
|
||||
}
|
||||
|
||||
func (web WebComponent) Path() string {
|
||||
return web.Stack().Dir
|
||||
}
|
||||
21
env/distillery.go
vendored
21
env/distillery.go
vendored
|
|
@ -3,7 +3,6 @@ package env
|
|||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/core"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/config"
|
||||
|
|
@ -22,26 +21,6 @@ type Upstream struct {
|
|||
Triplestore string
|
||||
}
|
||||
|
||||
func (dis Distillery) HTTPSEnabled() bool {
|
||||
return dis.Config.CertbotEmail != ""
|
||||
}
|
||||
|
||||
// Returns the default virtual host
|
||||
func (dis Distillery) DefaultVirtualHost() string {
|
||||
VIRTUAL_HOST := dis.Config.DefaultDomain
|
||||
if len(dis.Config.SelfExtraDomains) > 0 {
|
||||
VIRTUAL_HOST += "," + strings.Join(dis.Config.SelfExtraDomains, ",")
|
||||
}
|
||||
return VIRTUAL_HOST
|
||||
}
|
||||
|
||||
func (dis Distillery) DefaultLetsencryptHost() string {
|
||||
if !dis.HTTPSEnabled() {
|
||||
return ""
|
||||
}
|
||||
return dis.DefaultVirtualHost()
|
||||
}
|
||||
|
||||
// Context returns a new Context belonging to this distillery
|
||||
func (dis Distillery) Context() context.Context {
|
||||
return context.Background()
|
||||
|
|
|
|||
8
env/instances.go
vendored
8
env/instances.go
vendored
|
|
@ -11,6 +11,7 @@ import (
|
|||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/embed"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/bookkeeping"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/fsx"
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/stack"
|
||||
|
|
@ -201,9 +202,9 @@ func (instance Instance) Domain() string {
|
|||
}
|
||||
|
||||
// IfHttps returns value if the distillery has https enabled, the empty string otherwise
|
||||
// TODO: Fix this to be in a proper place
|
||||
// TODO: Fix this into config!
|
||||
func (dis *Distillery) IfHttps(value string) string {
|
||||
if !dis.HTTPSEnabled() {
|
||||
if !dis.Config.HTTPSEnabled() {
|
||||
return ""
|
||||
}
|
||||
return value
|
||||
|
|
@ -218,7 +219,7 @@ func (instance Instance) URL() *url.URL {
|
|||
}
|
||||
|
||||
// use http or https scheme depending on if the distillery has it enabled
|
||||
if instance.dis.HTTPSEnabled() {
|
||||
if instance.dis.Config.HTTPSEnabled() {
|
||||
url.Scheme = "https"
|
||||
} else {
|
||||
url.Scheme = "http"
|
||||
|
|
@ -233,6 +234,7 @@ func (instance Instance) Stack() stack.Installable {
|
|||
Stack: stack.Stack{
|
||||
Dir: instance.FilesystemBase,
|
||||
},
|
||||
Resources: embed.ResourceEmbed, // TODO: Move this over
|
||||
ContextPath: filepath.Join("resources", "compose", "barrel"),
|
||||
|
||||
EnvPath: filepath.Join("resources", "templates", "docker-env", "barrel"),
|
||||
|
|
|
|||
2
env/server.go
vendored
2
env/server.go
vendored
|
|
@ -5,6 +5,8 @@ import (
|
|||
"net/http"
|
||||
)
|
||||
|
||||
// TODO: Move this into dis!
|
||||
|
||||
// Server represents a server for this distillery
|
||||
type Server struct {
|
||||
dis *Distillery
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue