diff --git a/cmd/config.go b/cmd/config.go
index c4e9831..726a359 100644
--- a/cmd/config.go
+++ b/cmd/config.go
@@ -21,6 +21,5 @@ func (c cfg) Description() wisski_distillery.Description {
}
func (cfg) Run(context wisski_distillery.Context) error {
- context.Printf("%#v", context.Environment.Config)
- return nil
+ return context.Environment.Config.Marshal(context.Stdout)
}
diff --git a/cmd/config_migrate.go b/cmd/config_migrate.go
new file mode 100644
index 0000000..481a1a4
--- /dev/null
+++ b/cmd/config_migrate.go
@@ -0,0 +1,64 @@
+package cmd
+
+import (
+ "bytes"
+
+ wisski_distillery "github.com/FAU-CDI/wisski-distillery"
+ "github.com/FAU-CDI/wisski-distillery/internal/cli"
+ "github.com/FAU-CDI/wisski-distillery/internal/config"
+ "github.com/FAU-CDI/wisski-distillery/internal/config/legacy"
+ "github.com/FAU-CDI/wisski-distillery/pkg/environment"
+)
+
+// ConfigMigrate is the config-migrate command
+var ConfigMigrate wisski_distillery.Command = cfgMigrate{}
+
+type cfgMigrate struct {
+ Positionals struct {
+ Input string `positional-arg-name:"input" required:"1-1" description:"old config to migrate"`
+ } `positional-args:"true"`
+}
+
+func (cfgMigrate) Description() wisski_distillery.Description {
+ return wisski_distillery.Description{
+ Requirements: cli.Requirements{
+ NeedsDistillery: false,
+ },
+ Command: "config_migrate",
+ Description: "migrate legacy configuration",
+ }
+}
+
+func (c cfgMigrate) Run(context wisski_distillery.Context) error {
+ // migration environment is the native environment!
+ env := new(environment.Native)
+
+ // open the legacy file
+ file, err := env.Open(c.Positionals.Input)
+ if err != nil {
+ return err
+ }
+ defer file.Close()
+
+ // migrate from a legacy configuration
+ // then marshal, and re-read
+
+ var cfg config.Config
+ {
+ var mconfig config.Config
+ var output bytes.Buffer
+
+ if err := legacy.Migrate(&mconfig, env, file); err != nil {
+ return err
+ }
+ if err := mconfig.Marshal(&output); err != nil {
+ return err
+ }
+ if err := cfg.Unmarshal(env, &output); err != nil {
+ return err
+ }
+ }
+
+ // do a final marshal
+ return cfg.Marshal(context.Stdout)
+}
diff --git a/cmd/system_update.go b/cmd/system_update.go
index 5e0af71..8f86528 100644
--- a/cmd/system_update.go
+++ b/cmd/system_update.go
@@ -68,7 +68,7 @@ func (si systemupdate) Run(context wisski_distillery.Context) error {
// create all the other directories
logging.LogMessage(context.Stderr, context.Context, "Ensuring distillery installation directories exist")
for _, d := range []string{
- dis.Config.DeployRoot,
+ dis.Config.Paths.Root,
dis.Instances().Path(),
dis.Exporter().StagingPath(),
dis.Exporter().ArchivePath(),
@@ -102,7 +102,7 @@ func (si systemupdate) Run(context wisski_distillery.Context) error {
}
logging.LogMessage(context.Stderr, context.Context, "Checking that 'docker' is installed")
- if err := si.mustExec(context, "", "docker", "--version", dis.Config.DockerNetworkName); err != nil {
+ if err := si.mustExec(context, "", "docker", "--version"); err != nil {
return err
}
@@ -114,7 +114,7 @@ func (si systemupdate) Run(context wisski_distillery.Context) error {
// create the docker network
// TODO: Use docker API for this
logging.LogMessage(context.Stderr, context.Context, "Updating Docker Configuration")
- si.mustExec(context, "", "docker", "network", "create", dis.Config.DockerNetworkName)
+ si.mustExec(context, "", "docker", "network", "create", dis.Config.Docker.Network)
// install and update the various stacks!
ctx := component.InstallationContext{
@@ -193,7 +193,7 @@ var errMustExecFailed = exit.Error{
func (si systemupdate) mustExec(context wisski_distillery.Context, workdir string, exe string, argv ...string) error {
dis := context.Environment
if workdir == "" {
- workdir = dis.Config.DeployRoot
+ workdir = dis.Config.Paths.Root
}
code := dis.Still.Environment.Exec(context.Context, context.IOStream, workdir, exe, argv...)()
if code != 0 {
diff --git a/cmd/wdcli/main.go b/cmd/wdcli/main.go
index 6305517..f9d8024 100644
--- a/cmd/wdcli/main.go
+++ b/cmd/wdcli/main.go
@@ -18,6 +18,7 @@ var wdcli = wisski_distillery.NewProgram()
func init() {
// self commands
wdcli.Register(cmd.Config)
+ wdcli.Register(cmd.ConfigMigrate)
wdcli.Register(cmd.License)
// setup commands
diff --git a/go.mod b/go.mod
index 18fc7d5..a616f0c 100644
--- a/go.mod
+++ b/go.mod
@@ -24,6 +24,7 @@ require (
golang.org/x/exp v0.0.0-20230105202349-8879d0199aa3
golang.org/x/sync v0.1.0
golang.org/x/term v0.3.0
+ gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/mysql v1.4.4
gorm.io/gorm v1.24.2
)
diff --git a/go.sum b/go.sum
index cf131af..6d733be 100644
--- a/go.sum
+++ b/go.sum
@@ -101,6 +101,8 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.4.4 h1:MX0K9Qvy0Na4o7qSC/YI7XxqUw5KDw01umqgID+svdQ=
gorm.io/driver/mysql v1.4.4/go.mod h1:BCg8cKI+R0j/rZRQxeKis/forqRwRSYOR8OM3Wo6hOM=
gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
diff --git a/internal/bootstrap/bootstrap.go b/internal/bootstrap/bootstrap.go
index 016be21..bb87fe5 100644
--- a/internal/bootstrap/bootstrap.go
+++ b/internal/bootstrap/bootstrap.go
@@ -15,7 +15,7 @@ const Executable = "wdcli"
// ConfigFile is the name of the config file.
// It should be located inside the deployment directory.
-const ConfigFile = ".env"
+const ConfigFile = "distillery.yaml"
// OverridesJSON is the name of the json overrides file.
// It should be located inside the deployment directory.
diff --git a/internal/config/config.go b/internal/config/config.go
index 237b2d5..03d4e4e 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -5,7 +5,6 @@ import (
"fmt"
"hash/fnv"
"math/rand"
- "net/url"
"reflect"
"time"
@@ -19,80 +18,33 @@ import (
// Config contains many methods that do not require any interaction with any running components.
// Methods that require running components are instead store inside the [Distillery] or an appropriate [Component].
type Config struct {
- // Several docker-compose files are created to manage global services and the system itself.
- // On top of this all real-system space will be created under this directory.
- DeployRoot string `env:"DEPLOY_ROOT" default:"/var/www/deploy" parser:"abspath"`
+ Paths PathsConfig `yaml:"paths" recurse:"true"`
+ HTTP HTTPConfig `yaml:"http" recurse:"true"`
+ Theme ThemeConfig `yaml:"theme" recurse:"true"`
+ Docker DockerConfig `yaml:"docker" recurse:"true"`
- // Each created Drupal Instance corresponds to a single domain name.
- // These domain names should either be a complete domain name or a sub-domain of a default domain.
- // This setting configures the default domain-name to create subdomains of.
- DefaultDomain string `env:"DEFAULT_DOMAIN" default:"localhost.kwarc.info" parser:"domain"`
-
- // By default, the default domain redirects to the distillery repository.
- // If you want to change this, set an alternate domain name here.
- SelfRedirect *url.URL `env:"SELF_REDIRECT" default:"https://github.com/FAU-CDI/wisski-distillery" parser:"https_url"`
-
- // By default, only the 'self' domain above is caught.
- // To catch additional domains, add them here (comma seperated)
- SelfExtraDomains []string `env:"SELF_EXTRA_DOMAINS" default:"" parser:"domains"`
-
- // You can override individual URLS in the homepage
- // Do this by adding URLs (without trailing '/'s) into a JSON file
- SelfOverridesFile string `env:"SELF_OVERRIDES_FILE" default:"" parser:"file"`
-
- // You can block specific prefixes from being picked up by the resolver.
- // Do this by adding one prefix per file.
- SelfResolverBlockFile string `env:"SELF_RESOLVER_BLOCK_FILE" default:"" parser:"file"`
-
- // The system can support setting up certificate(s) automatically.
- // It can be enabled by setting an email for certbot certificates.
- // This email address can be configured here.
- CertbotEmail string `env:"CERTBOT_EMAIL" default:"" parser:"email"`
+ SQL SQLConfig `yaml:"sql" recurse:"true"`
+ TS TSConfig `yaml:"triplestore" recurse:"true"`
// Maximum age for backup in days
- MaxBackupAge int `env:"MAX_BACKUP_AGE" default:"" parser:"number"`
-
- // Each Drupal instance requires a corresponding system user, database users and databases.
- // These are also set by the appropriate domain name.
- // To differentiate them from other users of the system, these names can be prefixed.
- // The prefix to use can be configured here.
- // When changing these please consider that no system user may exist that has the same name as a mysql user.
- // This is a MariaDB restriction.
- MysqlUserPrefix string `env:"MYSQL_USER_PREFIX" default:"mysql-factory-" parser:"slug"`
- MysqlDatabasePrefix string `env:"MYSQL_DATABASE_PREFIX" default:"mysql-factory-" parser:"slug"`
- GraphDBUserPrefix string `env:"GRAPHDB_USER_PREFIX" default:"mysql-factory-" parser:"slug"`
- GraphDBRepoPrefix string `env:"GRAPHDB_REPO_PREFIX" default:"mysql-factory-" parser:"slug"`
-
- // In addition to the filesystem the WissKI distillery requires a single SQL table.
- // It uses this database to store a list of installed things.
- DistilleryDatabase string `env:"DISTILLERY_BOOKKEEPING_DATABASE" default:"distillery" parser:"slug"`
+ MaxBackupAge time.Duration `yaml:"age" validate:"duration"`
// Various components use password-based-authentication.
// These passwords are generated automatically.
// This variable can be used to determine their length.
- PasswordLength int `env:"PASSWORD_LENGTH" default:"64" parser:"number"`
+ PasswordLength int `yaml:"password_length" default:"64" validate:"positive"`
// Public port to use for the ssh server
- PublicSSHPort uint16 `env:"SSH_PORT" default:"2222" parser:"port"`
+ PublicSSHPort uint16 `yaml:"ssh_port" default:"2222" validate:"port"`
- // admin credentials for graphdb
- TriplestoreAdminUser string `env:"GRAPHDB_ADMIN_USER" default:"admin" parser:"nonempty"`
- TriplestoreAdminPassword string `env:"GRAPHDB_ADMIN_PASSWORD" default:"" parser:"nonempty"`
-
- // admin credentials for the Mysql database
- MysqlAdminUser string `env:"MYSQL_ADMIN_USER" default:"admin" parser:"nonempty"`
- MysqlAdminPassword string `env:"MYSQL_ADMIN_PASSWORD" default:"" parser:"nonempty"`
// session secret holds the secret for login
- SessionSecret string `env:"SESSION_SECRET" default:"" parser:"nonempty"`
-
- // name of docker network to use
- DockerNetworkName string `env:"DOCKER_NETWORK_NAME" default:"distillery" parser:"nonempty"`
+ SessionSecret string `yaml:"session_secret" default:"" validate:"nonempty"`
// interval to trigger distillery cron tasks in
- CronInterval time.Duration `env:"CRON_INTERVAL" default:"10m" parser:"duration"`
+ CronInterval time.Duration `env:"cron_interval" default:"10m" validate:"duration"`
// ConfigPath is the path this configuration was loaded from (if any)
- ConfigPath string
+ ConfigPath string `yaml:"-"`
}
// CSRFSecret return the csrfSecret derived from the session secret
diff --git a/internal/config/config_template b/internal/config/config_template
deleted file mode 100644
index c48c2c5..0000000
--- a/internal/config/config_template
+++ /dev/null
@@ -1,73 +0,0 @@
-# Several docker-compose files are created to manage global services and the system itself.
-# On top of this all real-system space will be created under this directory.
-DEPLOY_ROOT=${DEPLOY_ROOT}
-
-# The name of the (global) docker network to run the distillery services in.
-DOCKER_NETWORK_NAME=${DOCKER_NETWORK_NAME}
-
-# Each created Drupal Instance corresponds to a single domain name.
-# These domain names should either be a complete domain name or a sub-domain of a default domain.
-# This setting configures the default domain-name to create subdomains of.
-DEFAULT_DOMAIN=${DEFAULT_DOMAIN}
-
-# By default, the default domain redirects to the distillery repository.
-# If you want to change this, set an alternate domain name here.
-SELF_REDIRECT=
-
-# By default, only the 'self' domain above is caught.
-# To catch additional domains, add them here (comma seperated)
-SELF_EXTRA_DOMAINS=
-
-# You can override individual URLS in the homepage.
-# Do this by adding URLs (without trailing '/'s) into a JSON file
-SELF_OVERRIDES_FILE=${SELF_OVERRIDES_FILE}
-
-# You can block specific prefixes within Triplestore from showing up in the resolver configuration file.
-# Do this by adding one prefix per line in this file.
-SELF_RESOLVER_BLOCK_FILE=${SELF_RESOLVER_BLOCK_FILE}
-
-# The system can support setting up certificate(s) automatically.
-# It can be enabled by setting an email for certbot certificates.
-# This email address can be configured here.
-CERTBOT_EMAIL=
-
-# The maximum age (in days) for backups to be kept.
-# Backups older than this will be removed when a new backup is made.
-MAX_BACKUP_AGE=30
-
-# Each Drupal instance requires a corresponding system user, database users and databases.
-# These are also set by the appropriate domain name.
-# To differentiate them from other users of the system, these names can be prefixed.
-# The prefix to use can be configured here.
-# When changing these please consider that no system user may exist that has the same name as a mysql user.
-# This is a MariaDB restriction.
-MYSQL_USER_PREFIX=mysql-factory-
-MYSQL_DATABASE_PREFIX=mysql-factory-
-GRAPHDB_USER_PREFIX=graphdb-factory-
-GRAPHDB_REPO_PREFIX=graphdb-factory-
-
-# In addition to the filesystem the WissKI distillery requires a 'bookkeeping' database.
-# This is used to store several settings.
-DISTILLERY_BOOKKEEPING_DATABASE=distillery
-
-# Various components use password-based-authentication.
-# These passwords are generated automatically.
-# This variable can be used to determine their length.
-PASSWORD_LENGTH=64
-
-# the port to use for the ssh server
-SSH_PORT=2222
-
-# The admin user and password of the GraphDB interface, to be used for queries
-GRAPHDB_ADMIN_USER=${GRAPHDB_ADMIN_USER}
-GRAPHDB_ADMIN_PASSWORD=${GRAPHDB_ADMIN_PASSWORD}
-
-# The admin user and password of the MySQL interface, to be used for provisioning
-MYSQL_ADMIN_USER=${MYSQL_ADMIN_USER}
-MYSQL_ADMIN_PASSWORD=${MYSQL_ADMIN_PASSWORD}
-
-# the interval to run cron in
-CRON_INTERVAL=10m
-
-# The secret for sessions (for login etc)
-SESSION_SECRET=${SESSION_SECRET}
diff --git a/internal/config/config_template.yml b/internal/config/config_template.yml
new file mode 100644
index 0000000..42f38f9
--- /dev/null
+++ b/internal/config/config_template.yml
@@ -0,0 +1,83 @@
+paths:
+ # Several files are required to manage the system
+ # On top of this all real-system space will be created under this directory.
+ root: ${DEPLOY_ROOT}
+
+ # You can override individual URLS in the homepage.
+ # Do this by adding URLs (without trailing '/'s) into a JSON file.
+ # This is the path to that file.
+ overrides: ${SELF_OVERRIDES_FILE}
+
+ # You can block specific prefixes within Triplestore from showing up in the global resolver.
+ # Do this by adding one prefix per line in this file.
+ # Lines starting with '#' and blank lines are ignored.
+ blocks: ${SELF_RESOLVER_BLOCK_FILE}
+
+http:
+ # Each created Drupal Instance corresponds to a single domain name.
+ # These domain names should either be a complete domain name or a sub-domain of a default domain.
+ # This setting configures the default domain-name to create subdomains of.
+ domain: ${DEFAULT_DOMAIN}
+
+ # By default, only the 'domain' domain above is caught.
+ # To catch additional domains, add them here
+ domains: []
+
+ # The system can support setting up certificate(s) automatically.
+ # It can be enabled by setting an email for certbot certificates.
+ # This email address can be configured here.
+ certbot_email: ""
+
+# By default, the default domain redirects to the distillery repository.
+# If you want to change this, set an alternate domain name here.
+home: ""
+
+docker:
+ # The name of the (global) docker network to run the distillery services in.
+ network: ${DOCKER_NETWORK_NAME}
+
+# Configuration of the sql backend
+sql:
+ # username and password for the sql administrative user.
+ # this user is automatically created.
+ username: ${MYSQL_ADMIN_USER}
+ password: ${MYSQL_ADMIN_PASSWORD}
+
+ # prefixes for the data and users to be created and managed
+ # one of these is created per WissKI instance.
+ user_prefix: "mysql-factory-"
+ data_prefix: "mysql-factory-"
+
+ # database used for internal configuration
+ database: "distillery"
+
+# configuration of the triplestore backend
+triplestore:
+ # admin user and password of the graphdb interface
+ # this will be created automatically.
+ username: ${GRAPHDB_ADMIN_USER}
+ password: ${GRAPHDB_ADMIN_PASSWORD}
+
+ # prefixes for the users and repositories to be created
+ user_prefix: "graphdb-factory-"
+ data_prefix: "graphdb-factory-"
+
+# The maximum agefor backups to be kept.
+# Backups older than this will be removed when a new backup is made.
+# The default here is 720hours (== 30 days)
+age: '720h'
+
+
+# Various components use password-based-authentication.
+# These passwords are generated automatically.
+# This variable can be used to determine their length.
+password_length: 64
+
+# the port to use for the ssh server
+ssh_port: 2222
+
+# The secret for sessions (for login etc)
+session_secret: ${SESSION_SECRET}
+
+# the interval to run cron in
+cron_interval: "10m"
\ No newline at end of file
diff --git a/internal/config/database.go b/internal/config/database.go
new file mode 100644
index 0000000..df4a12b
--- /dev/null
+++ b/internal/config/database.go
@@ -0,0 +1,23 @@
+package config
+
+type DatabaseConfig struct {
+ // Credentials for the admin user.
+ // Is automatically created if it does not exist.
+ AdminUsername string `yaml:"username" default:"admin" validate:"nonempty"`
+ AdminPassword string `yaml:"password" validate:"nonempty"`
+
+ // Prefix for new users and data setss
+ UserPrefix string `yaml:"user_prefix" default:"wisski-distillery-" validate:"slug"`
+ DataPrefix string `yaml:"fragment_prefix" default:"wisski-distillery-" validate:"slug"`
+}
+
+type SQLConfig struct {
+ DatabaseConfig `yaml:",inline" recurse:"true"`
+
+ // Database to use to store distillery datastructures
+ Database string `yaml:"database" default:"distillery" validate:"slug"`
+}
+
+type TSConfig struct {
+ DatabaseConfig `yaml:",inline" recurse:"true"`
+}
diff --git a/internal/config/docker.go b/internal/config/docker.go
new file mode 100644
index 0000000..bb8a699
--- /dev/null
+++ b/internal/config/docker.go
@@ -0,0 +1,6 @@
+package config
+
+type DockerConfig struct {
+ // name of docker network to use
+ Network string `yaml:"network" default:"distillery" validate:"nonempty"`
+}
diff --git a/internal/config/executable.go b/internal/config/executable.go
deleted file mode 100644
index 0042a80..0000000
--- a/internal/config/executable.go
+++ /dev/null
@@ -1,33 +0,0 @@
-package config
-
-import (
- "path/filepath"
-
- "github.com/FAU-CDI/wisski-distillery/internal/bootstrap"
- "github.com/FAU-CDI/wisski-distillery/pkg/environment"
- "github.com/FAU-CDI/wisski-distillery/pkg/fsx"
-)
-
-// ExecutablePath returns the path to the executable of this distillery.
-func (cfg Config) ExecutablePath() string {
- return filepath.Join(cfg.DeployRoot, bootstrap.Executable)
-}
-
-// UsingDistilleryExecutable checks if the current process is using the distillery executable
-func (cfg Config) UsingDistilleryExecutable(env environment.Environment) bool {
- exe, err := env.Executable()
- if err != nil {
- return false
- }
- return fsx.SameFile(env, exe, cfg.ExecutablePath())
-}
-
-// CurrentExecutable returns the path to the current executable being used.
-// When it does not exist, falls back to the default executable.
-func (cfg Config) CurrentExecutable(env environment.Environment) string {
- exe, err := env.Executable()
- if err != nil || !fsx.IsFile(env, exe) {
- return cfg.ExecutablePath()
- }
- return exe
-}
diff --git a/internal/config/home.go b/internal/config/home.go
new file mode 100644
index 0000000..58e1f79
--- /dev/null
+++ b/internal/config/home.go
@@ -0,0 +1,10 @@
+package config
+
+import "github.com/FAU-CDI/wisski-distillery/internal/config/validators"
+
+// ThemeConfig determines theming options
+type ThemeConfig struct {
+ // By default, the default domain redirects to the distillery repository.
+ // If you want to change this, set an alternate domain name here.
+ SelfRedirect *validators.URL `yaml:"home" default:"https://github.com/FAU-CDI/wisski-distillery" validate:"https"`
+}
diff --git a/internal/config/domains.go b/internal/config/http.go
similarity index 52%
rename from internal/config/domains.go
rename to internal/config/http.go
index 42601a5..dca71ee 100644
--- a/internal/config/domains.go
+++ b/internal/config/http.go
@@ -7,24 +7,30 @@ import (
"github.com/tkw1536/goprogram/lib/collection"
)
-// This file contains domain related derived configuration values.
+type HTTPConfig struct {
+ // Each created Drupal Instance corresponds to a single domain name.
+ // These domain names should either be a complete domain name or a sub-domain of a default domain.
+ // This setting configures the default domain-name to create subdomains of.
+ PrimaryDomain string `yaml:"domain" default:"localhost.kwarc.info" validate:"domain"`
-// HTTPSEnabled returns if the distillery has HTTPS enabled, and false otherwise.
-func (cfg Config) HTTPSEnabled() bool {
- return cfg.CertbotEmail != ""
+ // By default, only the 'self' domain above is caught.
+ // To catch additional domains, add them here (comma seperated)
+ ExtraDomains []string `yaml:"domains" validate:"domains"`
+
+ // The system can support setting up certificate(s) automatically.
+ // It can be enabled by setting an email for certbot certificates.
+ // This email address can be configured here.
+ CertbotEmail string `yaml:"certbot_email" validate:"email"`
}
-// HostRequirement returns a traefik rule for the given names
-func (Config) HostRule(names ...string) string {
- quoted := collection.MapSlice(names, func(name string) string {
- return "`" + name + "`"
- })
- return fmt.Sprintf("Host(%s)", strings.Join(quoted, ","))
+// HTTPSEnabled returns if the distillery has HTTPS enabled, and false otherwise.
+func (hcfg HTTPConfig) HTTPSEnabled() bool {
+ return hcfg.CertbotEmail != ""
}
// HTTPSEnabledEnv returns "true" if https is enabled, and "false" otherwise.
-func (cfg Config) HTTPSEnabledEnv() string {
- if cfg.HTTPSEnabled() {
+func (hcfg HTTPConfig) HTTPSEnabledEnv() string {
+ if hcfg.HTTPSEnabled() {
return "true"
}
return "false"
@@ -32,24 +38,18 @@ func (cfg Config) HTTPSEnabledEnv() string {
// HostFromSlug returns the hostname belonging to a given slug.
// When the slug is empty, returns the default (top-level) domain.
-func (cfg Config) HostFromSlug(slug string) string {
+func (cfg HTTPConfig) HostFromSlug(slug string) string {
if slug == "" {
- return cfg.DefaultDomain
+ return cfg.PrimaryDomain
}
- return fmt.Sprintf("%s.%s", slug, cfg.DefaultDomain)
-}
-
-// DefaultHostRule returns the default traefik hostname rule for this distillery.
-// This consists of the [DefaultDomain] as well as [ExtraDomains].
-func (cfg Config) DefaultHostRule() string {
- return cfg.HostRule(append([]string{cfg.DefaultDomain}, cfg.SelfExtraDomains...)...)
+ return fmt.Sprintf("%s.%s", slug, cfg.PrimaryDomain)
}
// SlugFromHost returns the slug belonging to the appropriate host.'
//
// When host is a top-level domain, returns "", true.
// When no slug is found, returns "", false.
-func (cfg Config) SlugFromHost(host string) (slug string, ok bool) {
+func (cfg HTTPConfig) SlugFromHost(host string) (slug string, ok bool) {
// extract an ':port' that happens to be in the host.
domain, _, _ := strings.Cut(host, ":")
domain = TrimSuffixFold(domain, ".wisski") // remove optional ".wisski" ending that is used inside docker
@@ -57,7 +57,7 @@ func (cfg Config) SlugFromHost(host string) (slug string, ok bool) {
domainL := strings.ToLower(domain)
// check all the possible domain endings
- for _, suffix := range append([]string{cfg.DefaultDomain}, cfg.SelfExtraDomains...) {
+ for _, suffix := range append([]string{cfg.PrimaryDomain}, cfg.ExtraDomains...) {
suffixL := strings.ToLower(suffix)
if domainL == suffixL {
return "", true
@@ -77,3 +77,18 @@ func TrimSuffixFold(s string, suffix string) string {
}
return s
}
+
+// HostRule returns a traefik rule for the given names
+// TODO: Move this over!
+func HostRule(names ...string) string {
+ quoted := collection.MapSlice(names, func(name string) string {
+ return "`" + name + "`"
+ })
+ return fmt.Sprintf("Host(%s)", strings.Join(quoted, ","))
+}
+
+// DefaultHostRule returns the default traefik hostname rule for this distillery.
+// This consists of the [DefaultDomain] as well as [ExtraDomains].
+func (cfg HTTPConfig) DefaultHostRule() string {
+ return HostRule(append([]string{cfg.PrimaryDomain}, cfg.ExtraDomains...)...)
+}
diff --git a/pkg/envreader/envreader.go b/internal/config/legacy/envreader/envreader.go
similarity index 89%
rename from pkg/envreader/envreader.go
rename to internal/config/legacy/envreader/envreader.go
index f75af34..ca7e69b 100644
--- a/pkg/envreader/envreader.go
+++ b/internal/config/legacy/envreader/envreader.go
@@ -1,4 +1,5 @@
-// Package envreader
+// Package envreader provides Scanner.
+// It is deprecated and will be removed in a future release.
package envreader
import (
@@ -14,7 +15,9 @@ import (
// Reads may be internally buffered.
//
// An environment variable is of the form:
-// KEY=VALUE
+//
+// KEY=VALUE
+//
// on a separate line.
// Keys and values are case-sensitive and may contain anything except for newline characters.
// Spaces around key and value are trimmed using [strings.TrimSpace].
@@ -26,14 +29,14 @@ import (
//
// A typical use-case of a scanner is as follows:
//
-// scanner := NewScanner(r)
-// for scanner.Scan() {
-// // process any data ....
-// fmt.Println(scanner.Data())
-// }
-// if err := scanner.Err(); err != nil {
-// // handle errors
-// }
+// scanner := NewScanner(r)
+// for scanner.Scan() {
+// // process any data ....
+// fmt.Println(scanner.Data())
+// }
+// if err := scanner.Err(); err != nil {
+// // handle errors
+// }
//
// For the common use case of reading a set of distinct keys from a file see [ReadAll].
type Scanner struct {
@@ -100,6 +103,7 @@ func (scanner Scanner) Err() error {
// ReadAll creates a new [Scanner], and then reads all key/value pairs from r.
// If a key occurs more than once, only the last value is set in the returned map.
func ReadAll(r io.Reader) (values map[string]string, err error) {
+ // TODO: This is no longer used
scanner := NewScanner(r)
// read and store all values
diff --git a/pkg/envreader/envreader_test.go b/internal/config/legacy/envreader/envreader_test.go
similarity index 100%
rename from pkg/envreader/envreader_test.go
rename to internal/config/legacy/envreader/envreader_test.go
diff --git a/internal/config/legacy/legacy.go b/internal/config/legacy/legacy.go
new file mode 100644
index 0000000..07b3242
--- /dev/null
+++ b/internal/config/legacy/legacy.go
@@ -0,0 +1,147 @@
+// Package legacy provides support for reading legacy configuration.
+// It is deprecated and will be removed in a future release.
+package legacy
+
+import (
+ "io"
+ "net/url"
+ "reflect"
+ "time"
+
+ "github.com/FAU-CDI/wisski-distillery/internal/config"
+ "github.com/FAU-CDI/wisski-distillery/internal/config/legacy/envreader"
+ "github.com/FAU-CDI/wisski-distillery/internal/config/legacy/stringparser"
+ "github.com/FAU-CDI/wisski-distillery/internal/config/validators"
+ "github.com/FAU-CDI/wisski-distillery/pkg/environment"
+ "github.com/pkg/errors"
+)
+
+// Migrate parses a configuration from an old configuration.
+func Migrate(config *config.Config, env environment.Environment, src io.Reader) error {
+ var legacy Legacy
+ if err := legacy.Unmarshal(env, src); err != nil {
+ return nil
+ }
+ return legacy.Migrate(config)
+}
+
+// Legacy represents a legacy configuration file.
+//
+// NOTE(twiesing): This will be deprecated soon.
+type Legacy struct {
+ DeployRoot string `env:"DEPLOY_ROOT" default:"/var/www/deploy" parser:"abspath"`
+
+ DefaultDomain string `env:"DEFAULT_DOMAIN" default:"localhost.kwarc.info" parser:"domain"`
+
+ SelfRedirect *url.URL `env:"SELF_REDIRECT" default:"https://github.com/FAU-CDI/wisski-distillery" parser:"https_url"`
+
+ SelfExtraDomains []string `env:"SELF_EXTRA_DOMAINS" default:"" parser:"domains"`
+
+ SelfOverridesFile string `env:"SELF_OVERRIDES_FILE" default:"" parser:"file"`
+
+ SelfResolverBlockFile string `env:"SELF_RESOLVER_BLOCK_FILE" default:"" parser:"file"`
+
+ CertbotEmail string `env:"CERTBOT_EMAIL" default:"" parser:"email"`
+
+ MaxBackupAge int `env:"MAX_BACKUP_AGE" default:"" parser:"number"`
+
+ MysqlUserPrefix string `env:"MYSQL_USER_PREFIX" default:"mysql-factory-" parser:"slug"`
+ MysqlDatabasePrefix string `env:"MYSQL_DATABASE_PREFIX" default:"mysql-factory-" parser:"slug"`
+ GraphDBUserPrefix string `env:"GRAPHDB_USER_PREFIX" default:"mysql-factory-" parser:"slug"`
+ GraphDBRepoPrefix string `env:"GRAPHDB_REPO_PREFIX" default:"mysql-factory-" parser:"slug"`
+
+ DistilleryDatabase string `env:"DISTILLERY_BOOKKEEPING_DATABASE" default:"distillery" parser:"slug"`
+
+ PasswordLength int `env:"PASSWORD_LENGTH" default:"64" parser:"number"`
+
+ PublicSSHPort uint16 `env:"SSH_PORT" default:"2222" parser:"port"`
+
+ TriplestoreAdminUser string `env:"GRAPHDB_ADMIN_USER" default:"admin" parser:"nonempty"`
+ TriplestoreAdminPassword string `env:"GRAPHDB_ADMIN_PASSWORD" default:"" parser:"nonempty"`
+
+ MysqlAdminUser string `env:"MYSQL_ADMIN_USER" default:"admin" parser:"nonempty"`
+ MysqlAdminPassword string `env:"MYSQL_ADMIN_PASSWORD" default:"" parser:"nonempty"`
+
+ SessionSecret string `env:"SESSION_SECRET" default:"" parser:"nonempty"`
+
+ // name of docker network to use
+ DockerNetworkName string `env:"DOCKER_NETWORK_NAME" default:"distillery" parser:"nonempty"`
+ CronInterval time.Duration `env:"CRON_INTERVAL" default:"10m" parser:"duration"`
+}
+
+// Migrate migrates this LegacyConfig into a new configuration.
+func (legacy *Legacy) Migrate(cfg *config.Config) error {
+ cfg.Paths.Root = legacy.DeployRoot
+ cfg.HTTP.PrimaryDomain = legacy.DefaultDomain
+ cfg.Theme.SelfRedirect = (*validators.URL)(legacy.SelfRedirect)
+ cfg.HTTP.ExtraDomains = legacy.SelfExtraDomains
+ cfg.Paths.OverridesJSON = legacy.SelfOverridesFile
+ cfg.Paths.ResolverBlocks = legacy.SelfResolverBlockFile
+ cfg.HTTP.CertbotEmail = legacy.CertbotEmail
+ cfg.MaxBackupAge = time.Duration(legacy.MaxBackupAge) * 24 * time.Hour
+ cfg.SQL.UserPrefix = legacy.MysqlUserPrefix
+ cfg.SQL.DataPrefix = legacy.MysqlDatabasePrefix
+ cfg.TS.UserPrefix = legacy.GraphDBUserPrefix
+ cfg.TS.DataPrefix = legacy.GraphDBRepoPrefix
+ cfg.SQL.Database = legacy.DistilleryDatabase
+ cfg.PasswordLength = legacy.PasswordLength
+ cfg.PublicSSHPort = legacy.PublicSSHPort
+ cfg.TS.AdminUsername = legacy.TriplestoreAdminUser
+ cfg.TS.AdminPassword = legacy.TriplestoreAdminPassword
+ cfg.SQL.AdminUsername = legacy.MysqlAdminUser
+ cfg.SQL.AdminPassword = legacy.MysqlAdminPassword
+ cfg.SessionSecret = legacy.SessionSecret
+ cfg.Docker.Network = legacy.DockerNetworkName
+ cfg.CronInterval = legacy.CronInterval
+ return nil
+}
+
+// Unmarshal opens a legacy configuration file.
+//
+// Data is read using the [envreader.ReadAll] method, see the appropriate documentation for the file format.
+//
+// The `env` and `parser` reflect tags of the [Config] struct determine the keys to read from, and the types to expect.
+// When a key is missing, it is set to the default value.
+//
+// See also [stringparser.Parse].
+func (config *Legacy) Unmarshal(env environment.Environment, src io.Reader) error {
+ // read all the values!
+ values, err := envreader.ReadAll(src)
+ if err != nil {
+ return err
+ }
+
+ vConfig := reflect.ValueOf(config).Elem()
+ tConfig := vConfig.Type()
+
+ // iterate over the types
+ numValues := tConfig.NumField()
+ for i := 0; i < numValues; i++ {
+ tField := tConfig.Field(i)
+ vField := vConfig.FieldByName(tField.Name)
+
+ tEnv := tField.Tag.Get("env")
+ tDefault := tField.Tag.Get("default")
+ tParser := tField.Tag.Get("parser")
+
+ // skip it if it isn't loaded!
+ if tEnv == "" {
+ continue
+ }
+
+ // read the value with a default
+ value, ok := values[tEnv]
+ if !ok || value == "" {
+ if tDefault != "" {
+ value = tDefault
+ }
+ }
+
+ // parse the value!
+ if err := stringparser.Parse(env, tParser, value, vField); err != nil {
+ return errors.Errorf("Legacy.Unmarshal: Setting %q, Parser %q: %s", tEnv, tParser, err)
+ }
+ }
+
+ return nil
+}
diff --git a/pkg/stringparser/parse.go b/internal/config/legacy/stringparser/parse.go
similarity index 100%
rename from pkg/stringparser/parse.go
rename to internal/config/legacy/stringparser/parse.go
diff --git a/pkg/stringparser/stringparser.go b/internal/config/legacy/stringparser/stringparser.go
similarity index 97%
rename from pkg/stringparser/stringparser.go
rename to internal/config/legacy/stringparser/stringparser.go
index 07b4f11..5d73a3d 100644
--- a/pkg/stringparser/stringparser.go
+++ b/internal/config/legacy/stringparser/stringparser.go
@@ -1,4 +1,5 @@
-// Package stringparser provides Parser
+// Package stringparser provides Parser.
+// It is deprecated and will be removed in a future release.
package stringparser
import (
diff --git a/internal/config/paths.go b/internal/config/paths.go
new file mode 100644
index 0000000..5fd94a6
--- /dev/null
+++ b/internal/config/paths.go
@@ -0,0 +1,52 @@
+package config
+
+import (
+ "path/filepath"
+
+ "github.com/FAU-CDI/wisski-distillery/internal/bootstrap"
+ "github.com/FAU-CDI/wisski-distillery/pkg/environment"
+ "github.com/FAU-CDI/wisski-distillery/pkg/fsx"
+)
+
+type PathsConfig struct {
+ // Several docker-compose files are created to manage global services and the system itself.
+ // On top of this all real-system space will be created under this directory.
+ Root string `yaml:"root" default:"/var/www/deploy" validate:"directory"`
+
+ // You can override individual URLS in the homepage
+ // Do this by adding URLs (without trailing '/'s) into a JSON file
+ OverridesJSON string `yaml:"overrides" validate:"file"`
+
+ // You can block specific prefixes from being picked up by the resolver.
+ // Do this by adding one prefix per file.
+ ResolverBlocks string `yaml:"blocks" validate:"file"`
+}
+
+// RuntimeDir returns the path to the runtime directory
+func (pcfg PathsConfig) RuntimeDir() string {
+ return filepath.Join(pcfg.Root, "runtime")
+}
+
+// ExecutablePath returns the path to the executable of this distillery.
+func (pcfg PathsConfig) ExecutablePath() string {
+ return filepath.Join(pcfg.Root, bootstrap.Executable)
+}
+
+// UsingDistilleryExecutable checks if the current process is using the distillery executable
+func (pcfg PathsConfig) UsingDistilleryExecutable(env environment.Environment) bool {
+ exe, err := env.Executable()
+ if err != nil {
+ return false
+ }
+ return fsx.SameFile(env, exe, pcfg.ExecutablePath())
+}
+
+// CurrentExecutable returns the path to the current executable being used.
+// When it does not exist, falls back to the default executable.
+func (pcfg PathsConfig) CurrentExecutable(env environment.Environment) string {
+ exe, err := env.Executable()
+ if err != nil || !fsx.IsFile(env, exe) {
+ return pcfg.ExecutablePath()
+ }
+ return exe
+}
diff --git a/internal/config/read.go b/internal/config/read.go
index 649f31c..f82352f 100644
--- a/internal/config/read.go
+++ b/internal/config/read.go
@@ -2,60 +2,31 @@ package config
import (
"io"
- "reflect"
+ "github.com/FAU-CDI/wisski-distillery/internal/config/validators"
"github.com/FAU-CDI/wisski-distillery/pkg/environment"
- "github.com/FAU-CDI/wisski-distillery/pkg/envreader"
- "github.com/FAU-CDI/wisski-distillery/pkg/stringparser"
- "github.com/pkg/errors"
+ "github.com/FAU-CDI/wisski-distillery/pkg/validator"
+ "gopkg.in/yaml.v3"
)
-// Unmarshal updates this configuration from the provided [io.Reader].
-//
-// Data is read using the [envreader.ReadAll] method, see the appropriate documentation for the file format.
-//
-// The `env` and `parser` reflect tags of the [Config] struct determine the keys to read from, and the types to expect.
-// When a key is missing, it is set to the default value.
-//
-// See also [stringparser.Parse].
+// Unmarshal reads configuration from the provided io.Reader, and then validates it.
+// Configuration is read in yaml format.
func (config *Config) Unmarshal(env environment.Environment, src io.Reader) error {
- // read all the values!
- values, err := envreader.ReadAll(src)
- if err != nil {
- return err
- }
-
- vConfig := reflect.ValueOf(config).Elem()
- tConfig := vConfig.Type()
-
- // iterate over the types
- numValues := tConfig.NumField()
- for i := 0; i < numValues; i++ {
- tField := tConfig.Field(i)
- vField := vConfig.FieldByName(tField.Name)
-
- tEnv := tField.Tag.Get("env")
- tDefault := tField.Tag.Get("default")
- tParser := tField.Tag.Get("parser")
-
- // skip it if it isn't loaded!
- if tEnv == "" {
- continue
- }
-
- // read the value with a default
- value, ok := values[tEnv]
- if !ok || value == "" {
- if tDefault != "" {
- value = tDefault
- }
- }
-
- // parse the value!
- if err := stringparser.Parse(env, tParser, value, vField); err != nil {
- return errors.Errorf("Config.Unmarshal: Setting %q, Parser %q: %s", tEnv, tParser, err)
+ // read yaml!
+ {
+ decoder := yaml.NewDecoder(src)
+ decoder.KnownFields(true)
+ if err := decoder.Decode(config); err != nil {
+ return err
}
}
- return nil
+ // do the validator
+ return validator.Validate(config, validators.New(env))
+}
+
+func (config *Config) Marshal(dest io.Writer) error {
+ encoder := yaml.NewEncoder(dest)
+ encoder.SetIndent(4)
+ return encoder.Encode(config)
}
diff --git a/internal/config/runtime.go b/internal/config/runtime.go
deleted file mode 100644
index 48db811..0000000
--- a/internal/config/runtime.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package config
-
-import (
- "path/filepath"
-)
-
-// RuntimeDir returns the path to the runtime directory
-func (cfg Config) RuntimeDir() string {
- return filepath.Join(cfg.DeployRoot, "runtime")
-}
diff --git a/internal/config/template.go b/internal/config/template.go
index ba71554..088acc5 100644
--- a/internal/config/template.go
+++ b/internal/config/template.go
@@ -11,6 +11,7 @@ import (
"github.com/FAU-CDI/wisski-distillery/pkg/hostname"
"github.com/FAU-CDI/wisski-distillery/pkg/password"
"github.com/FAU-CDI/wisski-distillery/pkg/unpack"
+ "gopkg.in/yaml.v3"
_ "embed"
)
@@ -87,7 +88,7 @@ func (tpl *Template) SetDefaults(env environment.Environment) (err error) {
return nil
}
-//go:embed config_template
+//go:embed config_template.yml
var templateBytes []byte
// MarshalTo marshals this template into dst
@@ -100,10 +101,15 @@ func (tpl Template) MarshalTo(dst io.Writer) error {
field := tplType.Field(i)
key := field.Tag.Get("env")
- value := tplVal.FieldByName(field.Name).String()
+ value := tplVal.FieldByName(field.Name).Interface()
- context[key] = value
+ bytes, err := yaml.Marshal(value)
+ if err != nil {
+ return err
+ }
+ context[key] = string(bytes)
}
+ // TODO: CONFIG: Update template writing
return unpack.WriteTemplate(dst, context, bytes.NewReader(templateBytes))
}
diff --git a/internal/config/validators/collection.go b/internal/config/validators/collection.go
new file mode 100644
index 0000000..a544e5e
--- /dev/null
+++ b/internal/config/validators/collection.go
@@ -0,0 +1,32 @@
+package validators
+
+import (
+ "github.com/FAU-CDI/wisski-distillery/pkg/environment"
+ "github.com/FAU-CDI/wisski-distillery/pkg/validator"
+)
+
+// New creates a new set of standard validators for the configuration
+func New(env environment.Environment) validator.Collection {
+ coll := make(validator.Collection)
+
+ validator.Add(coll, "nonempty", ValidateNonempty)
+
+ validator.Add(coll, "directory", func(value *string, dflt string) error {
+ return ValidateDirectory(env, value, dflt)
+ })
+ validator.Add(coll, "file", func(value *string, dflt string) error {
+ return ValidateFile(env, value, dflt)
+ })
+
+ validator.Add(coll, "domain", ValidateDomain)
+ validator.AddSlice(coll, "domains", ",", ValidateDomain)
+ validator.Add(coll, "https", ValidateHTTPSURL)
+ validator.Add(coll, "slug", ValidateSlug)
+ validator.Add(coll, "email", ValidateEmail)
+
+ validator.Add(coll, "positive", ValidatePositive)
+ validator.Add(coll, "port", ValidatePort)
+
+ validator.Add(coll, "duration", ValidateDuration)
+ return coll
+}
diff --git a/internal/config/validators/domain.go b/internal/config/validators/domain.go
new file mode 100644
index 0000000..8fe6aff
--- /dev/null
+++ b/internal/config/validators/domain.go
@@ -0,0 +1,21 @@
+package validators
+
+import (
+ "regexp"
+ "strings"
+
+ "github.com/pkg/errors"
+)
+
+var regexpDomain = regexp.MustCompile(`^([a-zA-Z0-9][-a-zA-Z0-9]*\.)*[a-zA-Z0-9][-a-zA-Z0-9]*$`) // TODO: Make this regexp nicer!
+
+func ValidateDomain(domain *string, dflt string) error {
+ if *domain == "" {
+ *domain = dflt
+ }
+ if !regexpDomain.MatchString(*domain) {
+ return errors.Errorf("%q is not a valid domain", *domain)
+ }
+ *domain = strings.ToLower(*domain)
+ return nil
+}
diff --git a/internal/config/validators/duration.go b/internal/config/validators/duration.go
new file mode 100644
index 0000000..4922fcc
--- /dev/null
+++ b/internal/config/validators/duration.go
@@ -0,0 +1,14 @@
+package validators
+
+import "time"
+
+func ValidateDuration(d *time.Duration, dflt string) error {
+ if *d == 0 {
+ var err error
+ *d, err = time.ParseDuration(dflt)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
diff --git a/internal/config/validators/email.go b/internal/config/validators/email.go
new file mode 100644
index 0000000..1c93efa
--- /dev/null
+++ b/internal/config/validators/email.go
@@ -0,0 +1,24 @@
+package validators
+
+import (
+ "regexp"
+
+ "github.com/pkg/errors"
+)
+
+var regexpEmail = regexp.MustCompile(`^([-a-zA-Z0-9]+)\@([a-zA-Z0-9][-a-zA-Z0-9]*\.)*[a-zA-Z0-9][-a-zA-Z0-9]*$`) // TODO: Make this regexp nicer!
+
+// ValidateEmail checks that s represents an email, and then returns it as is.
+func ValidateEmail(email *string, dflt string) error {
+ if *email == "" {
+ *email = dflt
+ }
+ if *email == "" { // no email provided => ok
+ return nil
+ }
+
+ if !regexpEmail.MatchString(*email) {
+ return errors.Errorf("%q is not a valid email", *email)
+ }
+ return nil
+}
diff --git a/internal/config/validators/files.go b/internal/config/validators/files.go
new file mode 100644
index 0000000..e189659
--- /dev/null
+++ b/internal/config/validators/files.go
@@ -0,0 +1,27 @@
+package validators
+
+import (
+ "github.com/FAU-CDI/wisski-distillery/pkg/environment"
+ "github.com/FAU-CDI/wisski-distillery/pkg/fsx"
+ "github.com/pkg/errors"
+)
+
+func ValidateFile(env environment.Environment, path *string, dflt string) error {
+ if *path == "" {
+ *path = dflt
+ }
+ if !fsx.IsFile(env, *path) {
+ return errors.Errorf("%q does not exist or is not a file", *path)
+ }
+ return nil
+}
+
+func ValidateDirectory(env environment.Environment, path *string, dflt string) error {
+ if *path == "" {
+ *path = dflt
+ }
+ if !fsx.IsDirectory(env, *path) {
+ return errors.Errorf("%q does not exist or is not a directory", *path)
+ }
+ return nil
+}
diff --git a/internal/config/validators/int.go b/internal/config/validators/int.go
new file mode 100644
index 0000000..36998f7
--- /dev/null
+++ b/internal/config/validators/int.go
@@ -0,0 +1,32 @@
+package validators
+
+import (
+ "strconv"
+
+ "github.com/pkg/errors"
+)
+
+func ValidatePositive(value *int, dflt string) (err error) {
+ if *value == 0 && dflt != "" {
+ v, err := strconv.ParseInt(dflt, 10, 64)
+ if err != nil {
+ return err
+ }
+ *value = int(v)
+ }
+ if *value <= 0 {
+ return errors.Errorf("%d is not a positive value", *value)
+ }
+ return nil
+}
+
+func ValidatePort(value *uint16, dflt string) (err error) {
+ if *value == 0 && dflt != "" {
+ v, err := strconv.ParseUint(dflt, 10, 16)
+ if err != nil {
+ return err
+ }
+ *value = uint16(v)
+ }
+ return nil
+}
diff --git a/internal/config/validators/slug.go b/internal/config/validators/slug.go
new file mode 100644
index 0000000..57da756
--- /dev/null
+++ b/internal/config/validators/slug.go
@@ -0,0 +1,24 @@
+package validators
+
+import (
+ "regexp"
+ "strings"
+
+ "github.com/pkg/errors"
+)
+
+var regexpSlug = regexp.MustCompile(`^[a-zA-Z0-9][-a-zA-Z0-9]*$`) // TODO: Make this regexp nicer!
+
+var ErrInvalidSlug = errors.New("invalid slug")
+
+// ValidateSlug validates a slug and normalizes it.
+func ValidateSlug(s *string, dflt string) error {
+ if *s == "" {
+ *s = dflt
+ }
+ *s = strings.ToLower(*s)
+ if !regexpSlug.MatchString(*s) {
+ return ErrInvalidSlug
+ }
+ return nil
+}
diff --git a/internal/config/validators/string.go b/internal/config/validators/string.go
new file mode 100644
index 0000000..b26b813
--- /dev/null
+++ b/internal/config/validators/string.go
@@ -0,0 +1,16 @@
+package validators
+
+import "github.com/pkg/errors"
+
+var errEmpty = errors.New("value is empty")
+
+func ValidateNonempty(value *string, dflt string) error {
+ if *value == "" {
+ *value = dflt
+ }
+
+ if *value == "" {
+ return errEmpty
+ }
+ return nil
+}
diff --git a/internal/config/validators/url.go b/internal/config/validators/url.go
new file mode 100644
index 0000000..c8478c3
--- /dev/null
+++ b/internal/config/validators/url.go
@@ -0,0 +1,46 @@
+package validators
+
+import (
+ "net/url"
+
+ "github.com/pkg/errors"
+)
+
+// URL represents a url.URL that is marshaled as a string representing the url.
+type URL url.URL
+
+func (u *URL) MarshalText() (text []byte, err error) {
+ return []byte(u.String()), nil
+}
+
+func (u *URL) String() string {
+ if u == nil {
+ return ""
+ }
+ return (*url.URL)(u).String()
+}
+
+func (u *URL) UnmarshalText(text []byte) error {
+ if len(text) == 0 {
+ return nil
+ }
+ pu, err := url.Parse(string(text))
+ if err != nil {
+ return err
+ }
+ *u = URL(*pu)
+ return nil
+}
+
+func ValidateHTTPSURL(url **URL, dflt string) error {
+ if (*url).String() == "" {
+ *url = new(URL)
+ if err := (*url).UnmarshalText([]byte(dflt)); err != nil {
+ return err
+ }
+ }
+ if (*url).Scheme != "https" {
+ return errors.Errorf("%q is not a valid https URL (%q)", *url, (*url).Scheme)
+ }
+ return nil
+}
diff --git a/internal/dis/component/auth/next/next.go b/internal/dis/component/auth/next/next.go
index 9b00040..7415c26 100644
--- a/internal/dis/component/auth/next/next.go
+++ b/internal/dis/component/auth/next/next.go
@@ -55,7 +55,7 @@ func (next *Next) getInstance(r *http.Request) (wisski *wisski.WissKI, path stri
}
// find the slug
- slug, ok := next.Config.SlugFromHost(url.Host)
+ slug, ok := next.Config.HTTP.SlugFromHost(url.Host)
if slug == "" || !ok {
return nil, "", httpx.ErrBadRequest
}
diff --git a/internal/dis/component/auth/panel/ssh.go b/internal/dis/component/auth/panel/ssh.go
index 7d95d6b..846daa7 100644
--- a/internal/dis/component/auth/panel/ssh.go
+++ b/internal/dis/component/auth/panel/ssh.go
@@ -58,7 +58,7 @@ func (panel *UserPanel) sshRoute(ctx context.Context) http.Handler {
return sc, err
}
- sc.Domain = panel.Config.DefaultDomain
+ sc.Domain = panel.Config.HTTP.PrimaryDomain
sc.Port = panel.Config.PublicSSHPort
// pick the first domain that the user has access to as an example
@@ -68,7 +68,7 @@ func (panel *UserPanel) sshRoute(ctx context.Context) http.Handler {
} else {
sc.Slug = "example"
}
- sc.Hostname = panel.Config.HostFromSlug(sc.Slug)
+ sc.Hostname = panel.Config.HTTP.HostFromSlug(sc.Slug)
sc.Keys, err = panel.Dependencies.Keys.Keys(r.Context(), user.User.User)
if err != nil {
diff --git a/internal/dis/component/exporter/exporter.go b/internal/dis/component/exporter/exporter.go
index e80830e..2f60cd1 100644
--- a/internal/dis/component/exporter/exporter.go
+++ b/internal/dis/component/exporter/exporter.go
@@ -30,7 +30,7 @@ type Exporter struct {
// Path returns the path that contains all snapshot related data.
func (dis *Exporter) Path() string {
- return filepath.Join(dis.Config.DeployRoot, "snapshots")
+ return filepath.Join(dis.Config.Paths.Root, "snapshots")
}
// StagingPath returns the path to the directory containing a temporary staging area for snapshots.
diff --git a/internal/dis/component/exporter/extras_config.go b/internal/dis/component/exporter/extras_config.go
index f91cdc6..b6a62fc 100644
--- a/internal/dis/component/exporter/extras_config.go
+++ b/internal/dis/component/exporter/extras_config.go
@@ -38,8 +38,8 @@ func (control *Config) Backup(scontext component.StagingContext) error {
func (control *Config) backupFiles() []string {
return []string{
control.Config.ConfigPath,
- control.Config.ExecutablePath(),
- control.Config.SelfOverridesFile,
- control.Config.SelfResolverBlockFile,
+ control.Config.Paths.ExecutablePath(),
+ control.Config.Paths.OverridesJSON,
+ control.Config.Paths.ResolverBlocks,
}
}
diff --git a/internal/dis/component/exporter/prune.go b/internal/dis/component/exporter/prune.go
index cb0cf83..5f374f5 100644
--- a/internal/dis/component/exporter/prune.go
+++ b/internal/dis/component/exporter/prune.go
@@ -12,7 +12,7 @@ import (
// ShouldPrune determines if a file with the provided modification time should be
// removed from the export log.
func (exporter *Exporter) ShouldPrune(modtime time.Time) bool {
- return time.Since(modtime) > time.Duration(exporter.Config.MaxBackupAge)*24*time.Hour
+ return time.Since(modtime) > exporter.Config.MaxBackupAge
}
// Prune prunes all old exports
diff --git a/internal/dis/component/instances/create.go b/internal/dis/component/instances/create.go
index 5600e5a..b1bfab6 100644
--- a/internal/dis/component/instances/create.go
+++ b/internal/dis/component/instances/create.go
@@ -5,8 +5,8 @@ import (
"path/filepath"
"strings"
+ "github.com/FAU-CDI/wisski-distillery/internal/config/validators"
"github.com/FAU-CDI/wisski-distillery/internal/wisski"
- "github.com/FAU-CDI/wisski-distillery/pkg/stringparser"
)
var (
@@ -37,8 +37,8 @@ func (instances *Instances) Create(slug string) (wissKI *wisski.WissKI, err erro
// sql
- wissKI.Liquid.Instance.SqlDatabase = instances.Config.MysqlDatabasePrefix + slug
- wissKI.Liquid.Instance.SqlUsername = instances.Config.MysqlUserPrefix + slug
+ wissKI.Liquid.Instance.SqlDatabase = instances.Config.SQL.DataPrefix + slug
+ wissKI.Liquid.Instance.SqlUsername = instances.Config.SQL.UserPrefix + slug
wissKI.Liquid.Instance.SqlPassword, err = instances.Config.NewPassword()
if err != nil {
@@ -47,8 +47,8 @@ func (instances *Instances) Create(slug string) (wissKI *wisski.WissKI, err erro
// triplestore
- wissKI.Liquid.Instance.GraphDBRepository = instances.Config.GraphDBRepoPrefix + slug
- wissKI.Liquid.Instance.GraphDBUsername = instances.Config.GraphDBUserPrefix + slug
+ wissKI.Liquid.Instance.GraphDBRepository = instances.Config.TS.DataPrefix + slug
+ wissKI.Liquid.Instance.GraphDBUsername = instances.Config.TS.UserPrefix + slug
wissKI.Liquid.Instance.GraphDBPassword, err = instances.Config.NewPassword()
if err != nil {
@@ -73,7 +73,7 @@ var restrictedSlugs = []string{"www", "admin"}
// IsValidSlug checks if slug represents a valid slug for an instance.
func (instances *Instances) IsValidSlug(slug string) (string, error) {
// check that it is a slug
- slug, err := stringparser.ParseSlug(instances.Environment, slug)
+ err := validators.ValidateSlug(&slug, "")
if err != nil {
return "", errInvalidSlug
}
@@ -84,5 +84,5 @@ func (instances *Instances) IsValidSlug(slug string) (string, error) {
}
// return the slug
- return strings.ToLower(slug), nil
+ return slug, nil
}
diff --git a/internal/dis/component/instances/instances.go b/internal/dis/component/instances/instances.go
index 3c1053a..f8ceffb 100644
--- a/internal/dis/component/instances/instances.go
+++ b/internal/dis/component/instances/instances.go
@@ -28,7 +28,7 @@ type Instances struct {
}
func (instances *Instances) Path() string {
- return filepath.Join(instances.Still.Config.DeployRoot, "instances")
+ return filepath.Join(instances.Still.Config.Paths.Root, "instances")
}
// ErrWissKINotFound is returned when a WissKI is not found
diff --git a/internal/dis/component/instances/runtime.go b/internal/dis/component/instances/runtime.go
index 1812c2d..3def554 100644
--- a/internal/dis/component/instances/runtime.go
+++ b/internal/dis/component/instances/runtime.go
@@ -22,7 +22,7 @@ var runtimeResources embed.FS
// Update installs or updates runtime components needed by this component.
func (instances *Instances) Update(ctx context.Context, progress io.Writer) error {
- err := unpack.InstallDir(instances.Still.Environment, instances.Config.RuntimeDir(), "runtime", runtimeResources, func(dst, src string) {
+ err := unpack.InstallDir(instances.Still.Environment, instances.Config.Paths.RuntimeDir(), "runtime", runtimeResources, func(dst, src string) {
logging.ProgressF(progress, ctx, "[copy] %s\n", dst)
})
if err != nil {
diff --git a/internal/dis/component/resolver/resolver.go b/internal/dis/component/resolver/resolver.go
index 08412ee..0062378 100644
--- a/internal/dis/component/resolver/resolver.go
+++ b/internal/dis/component/resolver/resolver.go
@@ -84,14 +84,14 @@ func (resolver *Resolver) HandleRoute(ctx context.Context, route string) (http.H
}
// handle the default domain name!
- domainName := resolver.Config.DefaultDomain
+ domainName := resolver.Config.HTTP.PrimaryDomain
if domainName != "" {
fallback.Data[fmt.Sprintf("^https?://(.*)\\.%s", regexp.QuoteMeta(domainName))] = fmt.Sprintf("https://$1.%s", domainName)
logger.Info().Str("name", domainName).Msg("registering default domain")
}
// handle the extra domains!
- for _, domain := range resolver.Config.SelfExtraDomains {
+ for _, domain := range resolver.Config.HTTP.ExtraDomains {
fallback.Data[fmt.Sprintf("^https?://(.*)\\.%s", regexp.QuoteMeta(domain))] = fmt.Sprintf("https://$1.%s", domainName)
logger.Info().Str("name", domainName).Msg("registering legacy domain")
}
diff --git a/internal/dis/component/server/admin/html/index.html b/internal/dis/component/server/admin/html/index.html
index 2fe2a0b..e3cc4a4 100644
--- a/internal/dis/component/server/admin/html/index.html
+++ b/internal/dis/component/server/admin/html/index.html
@@ -119,7 +119,7 @@
root
{{.Config.DeployRoot}}
+ {{.Config.Paths.Root}}
{{.Config.DockerNetworkName}}
+ {{.Config.Docker.Network}}