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:
Tom Wiesing 2022-09-11 15:41:11 +02:00
parent 2ee90bf462
commit 7b2f79bea1
No known key found for this signature in database
44 changed files with 579 additions and 559 deletions

78
env/component.go vendored
View file

@ -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
View file

@ -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
}

View file

@ -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
View file

@ -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
View file

@ -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
View file

@ -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
}

View file

@ -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
View file

@ -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
View file

@ -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
View file

@ -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
View file

@ -5,6 +5,8 @@ import (
"net/http"
)
// TODO: Move this into dis!
// Server represents a server for this distillery
type Server struct {
dis *Distillery