Move to yaml-based configuration

This commit updates the configuration to be yaml-based and updates the
configuration to read in a yaml file.
This commit is contained in:
Tom Wiesing 2023-02-12 18:13:52 +01:00
parent 568c005d15
commit 945329a080
No known key found for this signature in database
70 changed files with 1150 additions and 350 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,6 @@
package config
type DockerConfig struct {
// name of docker network to use
Network string `yaml:"network" default:"distillery" validate:"nonempty"`
}

View file

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

10
internal/config/home.go Normal file
View file

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

View file

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

View file

@ -0,0 +1,121 @@
// Package envreader provides Scanner.
// It is deprecated and will be removed in a future release.
package envreader
import (
"bufio"
"io"
"strings"
)
// Scanner is a scanner for environment files.
// To create a new scanner use [NewScanner].
//
// It scans through a reader and reads environment variables from it.
// Reads may be internally buffered.
//
// An environment variable is of the form:
//
// 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].
// Keys may not contain an '='.
// Lines not containing a '=' (e.g. blank lines) and those starting with '#' and '//' are ignored.
//
// To advance the scanner to the next key, value pair use [Scan].
// To get the current (key, value) pair, use [Data].
//
// 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
// }
//
// For the common use case of reading a set of distinct keys from a file see [ReadAll].
type Scanner struct {
s *bufio.Scanner
// current key and value
key string
value string
}
// NewScanner creates a new scanner from the underlying Reader
func NewScanner(r io.Reader) *Scanner {
return &Scanner{
s: bufio.NewScanner(r),
}
}
// Scanner advances the scanner until the next KEY=VALUE pair.
//
// If there are no more values left (e.g. the underlying reader returned io.EOF)
// or when an unexpected error occured, returns false.
//
// A caller should always check Err() to see if there was an error.
func (scanner *Scanner) Scan() bool {
var found bool
for scanner.s.Scan() {
// check that we don't have an empty or comment only line
tokens := strings.TrimSpace(scanner.s.Text())
if len(tokens) == 0 || tokens[0] == '#' || strings.HasPrefix(tokens, "//") {
continue
}
// check that we have a 'key=value' pair
scanner.key, scanner.value, found = strings.Cut(tokens, "=")
if !found {
continue
}
// got a key = value
scanner.key = strings.TrimSpace(scanner.key)
scanner.value = strings.TrimSpace(scanner.value)
return true
}
// nothing found
scanner.key = ""
scanner.value = ""
return false
}
// Data reads the current value from the scanner.
// When Scan() has not been called, or returned false, returns two empty strings.
func (scanner Scanner) Data() (key, value string) {
return scanner.key, scanner.value
}
// Err returns any error that occured on the underlying read.
//
// When no error occured, or the underlying read is io.EOF, returns nil.
func (scanner Scanner) Err() error {
return scanner.s.Err()
}
// 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
values = make(map[string]string)
for scanner.Scan() {
key, value := scanner.Data()
values[key] = value
}
// check if there was an error!
if err := scanner.Err(); err != nil {
return nil, err
}
return values, nil
}

View file

@ -0,0 +1,42 @@
// Package envreader
package envreader
import (
"fmt"
"strings"
)
func ExampleNewScanner() {
scanner := NewScanner(strings.NewReader(`
lines without an equal sign are ignored
// this line is a comment, even with an = sign
KEY=VALUE
# this is also a comment =
spaces in keys = spaces in values
multiple=equal=signs
CaSe = SenSitiVe
empty value=
=empty key
`))
for scanner.Scan() {
key, value := scanner.Data()
fmt.Printf("%q %q\n", key, value)
}
if err := scanner.Err(); err != nil {
fmt.Println(scanner.Err())
} else {
fmt.Println("no error")
}
// Output: "KEY" "VALUE"
// "spaces in keys" "spaces in values"
// "multiple" "equal=signs"
// "CaSe" "SenSitiVe"
// "empty value" ""
// "" "empty key"
// no error
}

View file

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

View file

@ -0,0 +1,65 @@
package stringparser
import (
"reflect"
"strings"
"github.com/FAU-CDI/wisski-distillery/pkg/environment"
"github.com/pkg/errors"
)
var errUnknownParser = errors.New("unknown parser")
// Parse parses the provided value with the parser.
func Parse(env environment.Environment, name, value string, vField reflect.Value) error {
// use the validator
parser, ok := knownParsers[strings.ToLower(name)]
if parser == nil || !ok {
return errUnknownParser
}
// get the parsed value
checked, err := parser(env, value)
if err != nil {
return err
}
// set the value of the field
var errSet interface{}
func() {
defer func() {
errSet = recover()
}()
vField.Set(reflect.ValueOf(checked))
}()
// capture any error
if errSet != nil {
return errors.Errorf("set returned %v", name, errSet)
}
return nil
}
// knownParsers holds the known parsers
var knownParsers map[string]Parser[any] = map[string]Parser[any]{
"abspath": asGenericParser(ParseAbspath),
"domain": asGenericParser(ParseValidDomain),
"domains": asGenericParser(ParseValidDomains),
"duration": asGenericParser(ParseDuration),
"number": asGenericParser(ParseNumber),
"port": asGenericParser(ParsePort),
"https_url": asGenericParser(ParseHttpsURL),
"slug": asGenericParser(ParseSlug),
"file": asGenericParser(ParseFile),
"email": asGenericParser(ParseEmail),
"nonempty": asGenericParser(ParseNonEmpty),
}
func asGenericParser[T any](parser Parser[T]) Parser[any] {
return func(env environment.Environment, s string) (value any, err error) {
value, err = parser(env, s)
return
}
}

View file

@ -0,0 +1,125 @@
// Package stringparser provides Parser.
// It is deprecated and will be removed in a future release.
package stringparser
import (
"net/url"
"regexp"
"strconv"
"strings"
"time"
"github.com/FAU-CDI/wisski-distillery/pkg/environment"
"github.com/FAU-CDI/wisski-distillery/pkg/fsx"
"github.com/pkg/errors"
)
// Parser is used to read a value from a string and turn it into a golang value.
// It is simultaniously used to validate particular setting.
//
// Parsers can be found in this package as functions called Parse*.
// They are refered to by their name, e.g. ParseNonempty can be refered to by the name 'Nonempty'.
// See [Parse].
type Parser[T any] func(env environment.Environment, s string) (T, error)
// ParseAbspath checks that s is an absolute path and returns it as-is
func ParseAbspath(env environment.Environment, s string) (string, error) {
if !fsx.IsDirectory(env, s) {
return "", errors.Errorf("%q does not exist or is not a directory", s)
}
return s, nil
}
// ParseFile checks that s is a valid file and returns it as-is
func ParseFile(env environment.Environment, s string) (string, error) {
if !fsx.IsFile(env, s) {
return "", errors.Errorf("%q does not exist or is not a regular file", s)
}
return s, nil
}
var errEmptyString = errors.New("value is empty")
// ParseNonEmpty checks that s is a non-empty string and returns it as-is
func ParseNonEmpty(env environment.Environment, s string) (string, error) {
if s == "" {
return "", errEmptyString
}
return s, nil
}
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!
// ParseValidDomain checks that s is a valid domain and returns it in lowercase
func ParseValidDomain(env environment.Environment, s string) (string, error) {
if !regexpDomain.MatchString(s) {
return "", errors.Errorf("%q is not a valid domain", s)
}
return strings.ToLower(s), nil
}
// ParseValidDomains checks that s is a comma-seperated list of valid domains and returns them in lower case
func ParseValidDomains(env environment.Environment, s string) ([]string, error) {
if len(s) == 0 {
return []string{}, nil
}
domains := strings.Split(strings.ToLower(s), ",")
for _, d := range domains {
if !regexpDomain.MatchString(d) {
return nil, errors.Errorf("%q is not a valid domain", d)
}
}
return domains, nil
}
// ParseNumber parses s as a decimal integer
func ParseNumber(env environment.Environment, s string) (int, error) {
value, err := strconv.ParseInt(s, 10, 64)
return int(value), err
}
// ParsePort parses s as a port
func ParsePort(env environment.Environment, s string) (uint16, error) {
value, err := strconv.ParseUint(s, 10, 16)
return uint16(value), err
}
// ParseHttpsURL parses a string into a url that starts with 'https://'
func ParseHttpsURL(env environment.Environment, s string) (*url.URL, error) {
url, err := url.Parse(s)
if err != nil {
return nil, errors.Wrapf(err, "%q is not a valid URL", s)
}
if url.Scheme != "https" {
return nil, errors.Errorf("%q is not a valid https URL (%q)", s, url.Scheme)
}
return url, nil
}
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!
// ParseEmail checks that s represents an email, and then returns it as is.
func ParseEmail(env environment.Environment, s string) (string, error) {
if s == "" { // no email provided
return "", nil
}
if !regexpEmail.MatchString(s) {
return "", errors.Errorf("%q is not a valid email", s)
}
return s, nil
}
var regexpSlug = regexp.MustCompile(`^[a-zA-Z0-9][-a-zA-Z0-9]*$`) // TODO: Make this regexp nicer!
// ParseSlug parses s as a slug and returns it as is.
func ParseSlug(env environment.Environment, s string) (string, error) {
if !regexpSlug.MatchString(s) {
return "", errors.Errorf("%q is not a valid slug", s)
}
return s, nil
}
// ParseDuration parses a time.Duration
func ParseDuration(env environment.Environment, s string) (time.Duration, error) {
return time.ParseDuration(s)
}

52
internal/config/paths.go Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -119,7 +119,7 @@
<code>root</code>
</td>
<td>
<code>{{.Config.DeployRoot}}</code>
<code>{{.Config.Paths.Root}}</code>
</td>
</tr>
<tr>
@ -161,7 +161,7 @@
Docker Network Name
</td>
<td>
<code>{{.Config.DockerNetworkName}}</code>
<code>{{.Config.Docker.Network}}</code>
</td>
</tr>
<tr>

View file

@ -51,7 +51,7 @@ func (home *Home) HandleRoute(ctx context.Context, route string) (http.Handler,
dflt.Fallback = home.publicHandler(ctx)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
slug, ok := home.Config.SlugFromHost(r.Host)
slug, ok := home.Config.HTTP.SlugFromHost(r.Host)
switch {
case !ok:
http.NotFound(w, r)

View file

@ -68,7 +68,7 @@ func (home *Home) publicHandler(ctx context.Context) http.Handler {
// prepare about
pc.aboutContext.Logo = logoHTML
pc.aboutContext.Instances = home.homeInstances.Get(nil)
pc.aboutContext.SelfRedirect = home.Config.SelfRedirect.String()
pc.aboutContext.SelfRedirect = home.Config.Theme.SelfRedirect.String()
// render the about template

View file

@ -18,7 +18,7 @@ func (home *Home) loadRedirect(ctx context.Context) (redirect Redirect, err erro
redirect.Permanent = false
// load the overrides file
overrides, err := home.Environment.Open(home.Config.SelfOverridesFile)
overrides, err := home.Environment.Open(home.Config.Paths.OverridesJSON)
if err != nil {
return redirect, err
}

View file

@ -39,7 +39,7 @@ func (server *Server) Server(ctx context.Context, progress io.Writer) (public ht
var publicM, internalM mux.Mux[component.RouteContext]
publicM.Context = func(r *http.Request) component.RouteContext {
slug, ok := server.Still.Config.SlugFromHost(r.Host)
slug, ok := server.Still.Config.HTTP.SlugFromHost(r.Host)
return component.RouteContext{
DefaultDomain: slug == "" && ok,
}
@ -112,7 +112,7 @@ func (server *Server) Server(ctx context.Context, progress io.Writer) (public ht
// CSRF returns a CSRF handler for the given function
func (server *Server) csrf() func(http.Handler) http.Handler {
var opts []csrf.Option
if !server.Config.HTTPSEnabled() {
if !server.Config.HTTP.HTTPSEnabled() {
opts = append(opts, csrf.Secure(false))
}
opts = append(opts, csrf.SameSite(csrf.SameSiteStrictMode))

View file

@ -13,7 +13,7 @@ import (
)
func (control Server) Path() string {
return filepath.Join(control.Still.Config.DeployRoot, "core", "dis")
return filepath.Join(control.Still.Config.Paths.Root, "core", "dis")
}
//go:embed all:server server.env
@ -26,15 +26,15 @@ func (server *Server) Stack(env environment.Environment) component.StackWithReso
EnvPath: "server.env",
EnvContext: map[string]string{
"DOCKER_NETWORK_NAME": server.Config.DockerNetworkName,
"HOST_RULE": server.Config.DefaultHostRule(),
"HTTPS_ENABLED": server.Config.HTTPSEnabledEnv(),
"DOCKER_NETWORK_NAME": server.Config.Docker.Network,
"HOST_RULE": server.Config.HTTP.DefaultHostRule(),
"HTTPS_ENABLED": server.Config.HTTP.HTTPSEnabledEnv(),
"CONFIG_PATH": server.Config.ConfigPath,
"DEPLOY_ROOT": server.Config.DeployRoot,
"DEPLOY_ROOT": server.Config.Paths.Root,
"SELF_OVERRIDES_FILE": server.Config.SelfOverridesFile,
"SELF_RESOLVER_BLOCK_FILE": server.Config.SelfResolverBlockFile,
"SELF_OVERRIDES_FILE": server.Config.Paths.OverridesJSON,
"SELF_RESOLVER_BLOCK_FILE": server.Config.Paths.ResolverBlocks,
"CUSTOM_ASSETS_PATH": server.Dependencies.Templating.CustomAssetsPath(),
},
@ -50,6 +50,6 @@ func (server *Server) Trigger(ctx context.Context, env environment.Environment)
func (server *Server) Context(parent component.InstallationContext) component.InstallationContext {
return component.InstallationContext{
bootstrap.Executable: server.Config.CurrentExecutable(server.Environment), // TODO: Does this make sense?
bootstrap.Executable: server.Config.Paths.CurrentExecutable(server.Environment), // TODO: Does this make sense?
}
}

View file

@ -8,7 +8,7 @@ import (
// CustomAssetsPath is the path custom assets are stored at
func (tpl *Templating) CustomAssetsPath() string {
return filepath.Join(tpl.Config.DeployRoot, "core", "assets")
return filepath.Join(tpl.Config.Paths.Root, "core", "assets")
}
func (tpl *Templating) CustomAssetPath(name string) string {

View file

@ -22,7 +22,7 @@ var (
)
func (s *Solr) Path() string {
return filepath.Join(s.Still.Config.DeployRoot, "core", "solr")
return filepath.Join(s.Still.Config.Paths.Root, "core", "solr")
}
func (*Solr) Context(parent component.InstallationContext) component.InstallationContext {
@ -40,7 +40,7 @@ func (solr *Solr) Stack(env environment.Environment) component.StackWithResource
EnvPath: "solr.env",
EnvContext: map[string]string{
"DOCKER_NETWORK_NAME": solr.Config.DockerNetworkName,
"DOCKER_NETWORK_NAME": solr.Config.Docker.Network,
},
MakeDirs: []string{

View file

@ -52,7 +52,7 @@ func (sql *SQL) QueryTable(ctx context.Context, table component.Table) (*gorm.DB
// queryTable returns a gorm.DB to connect to the provided distillery database table
func (sql *SQL) queryTable(ctx context.Context, silent bool, table string) (*gorm.DB, error) {
conn, err := sql.connect(sql.Config.DistilleryDatabase)
conn, err := sql.connect(sql.Config.SQL.Database)
if err != nil {
return nil, err
}
@ -117,8 +117,8 @@ func (ssql *SQL) connect(database string) (*sql.DB, error) {
// dsn returns a dsn fof connecting to the database
func (sql *SQL) dsn(database string) string {
user := sql.Config.MysqlAdminUser
pass := sql.Config.MysqlAdminPassword
user := sql.Config.SQL.AdminUsername
pass := sql.Config.SQL.AdminPassword
network := sql.network()
server := sql.ServerURL

View file

@ -31,7 +31,7 @@ var (
)
func (sql *SQL) Path() string {
return filepath.Join(sql.Still.Config.DeployRoot, "core", "sql")
return filepath.Join(sql.Still.Config.Paths.Root, "core", "sql")
}
func (*SQL) Context(parent component.InstallationContext) component.InstallationContext {
@ -49,8 +49,8 @@ func (sql *SQL) Stack(env environment.Environment) component.StackWithResources
EnvPath: "sql.env",
EnvContext: map[string]string{
"DOCKER_NETWORK_NAME": sql.Config.DockerNetworkName,
"HTTPS_ENABLED": sql.Config.HTTPSEnabledEnv(),
"DOCKER_NETWORK_NAME": sql.Config.Docker.Network,
"HTTPS_ENABLED": sql.Config.HTTP.HTTPSEnabledEnv(),
},
MakeDirsPerm: environment.DefaultDirPerm,

View file

@ -54,8 +54,8 @@ func (sql *SQL) Update(ctx context.Context, progress io.Writer) error {
}
logging.LogMessage(progress, ctx, "Creating administrative user")
{
username := sql.Config.MysqlAdminUser
password := sql.Config.MysqlAdminPassword
username := sql.Config.SQL.AdminUsername
password := sql.Config.SQL.AdminPassword
if err := sql.CreateSuperuser(ctx, username, password, true); err != nil {
return errSQLUnableToCreateUser
}
@ -65,10 +65,10 @@ func (sql *SQL) Update(ctx context.Context, progress io.Writer) error {
// create the admin user
logging.LogMessage(progress, ctx, "Creating sql database")
{
if !sqle.IsSafeDatabaseLiteral(sql.Config.DistilleryDatabase) {
if !sqle.IsSafeDatabaseLiteral(sql.Config.SQL.Database) {
return errSQLUnsafeDatabaseName
}
createDBSQL := fmt.Sprintf("CREATE DATABASE IF NOT EXISTS `%s`;", sql.Config.DistilleryDatabase)
createDBSQL := fmt.Sprintf("CREATE DATABASE IF NOT EXISTS `%s`;", sql.Config.SQL.Database)
if err := sql.Exec(createDBSQL); err != nil {
return err
}

View file

@ -26,7 +26,7 @@ func (ssh2 *SSH2) HandleRoute(ctx context.Context, path string) (http.Handler, e
}
// find the host
slug, ok := ssh2.Config.SlugFromHost(r.Host)
slug, ok := ssh2.Config.HTTP.SlugFromHost(r.Host)
if slug == "" || !ok {
httpx.TextInterceptor.Intercept(w, r, httpx.ErrNotFound)
return

View file

@ -36,14 +36,15 @@ func (ssh2 *SSH2) handleDirectTCP(srv *ssh.Server, conn *gossh.ServerConn, newCh
return
}
slug, ok := ssh2.Config.SlugFromHost(d.DestAddr)
slug, ok := ssh2.Config.HTTP.SlugFromHost(d.DestAddr)
if !ok || d.DestPort != 22 || !hasPermission(ctx, slug) {
newChan.Reject(gossh.Prohibited, "permission denied")
return
}
// TODO: move this into an instance function somewhere
dest := net.JoinHostPort(slug+"."+ssh2.Config.DefaultDomain+".wisski", "22")
// NOTE(twiesing): This should be moved
dest := net.JoinHostPort(slug+"."+ssh2.Config.HTTP.PrimaryDomain+".wisski", "22")
var dialer net.Dialer
dconn, err := dialer.DialContext(ctx, "tcp", dest)

View file

@ -80,8 +80,8 @@ func (ssh2 *SSH2) handleConnection(session ssh.Session) {
banner := welcomeMessage
for _, oldnew := range [][2]string{
{"${SLUG}", slug},
{"${DOMAIN}", ssh2.Config.DefaultDomain},
{"${HOSTNAME}", slug + "." + ssh2.Config.DefaultDomain},
{"${DOMAIN}", ssh2.Config.HTTP.PrimaryDomain},
{"${HOSTNAME}", slug + "." + ssh2.Config.HTTP.PrimaryDomain},
{"${PORT}", strconv.FormatUint(uint64(ssh2.Config.PublicSSHPort), 10)},
} {
banner = strings.ReplaceAll(banner, oldnew[0], oldnew[1])

View file

@ -11,7 +11,7 @@ import (
)
func (ssh SSH2) Path() string {
return filepath.Join(ssh.Still.Config.DeployRoot, "core", "ssh2")
return filepath.Join(ssh.Still.Config.Paths.Root, "core", "ssh2")
}
//go:embed all:ssh2 ssh2.env
@ -24,15 +24,15 @@ func (ssh *SSH2) Stack(env environment.Environment) component.StackWithResources
EnvPath: "ssh2.env",
EnvContext: map[string]string{
"DOCKER_NETWORK_NAME": ssh.Config.DockerNetworkName,
"HOST_RULE": ssh.Config.DefaultHostRule(),
"HTTPS_ENABLED": ssh.Config.HTTPSEnabledEnv(),
"DOCKER_NETWORK_NAME": ssh.Config.Docker.Network,
"HOST_RULE": ssh.Config.HTTP.DefaultHostRule(),
"HTTPS_ENABLED": ssh.Config.HTTP.HTTPSEnabledEnv(),
"CONFIG_PATH": ssh.Config.ConfigPath,
"DEPLOY_ROOT": ssh.Config.DeployRoot,
"DEPLOY_ROOT": ssh.Config.Paths.Root,
"SELF_OVERRIDES_FILE": ssh.Config.SelfOverridesFile,
"SELF_RESOLVER_BLOCK_FILE": ssh.Config.SelfResolverBlockFile,
"SELF_OVERRIDES_FILE": ssh.Config.Paths.OverridesJSON,
"SELF_RESOLVER_BLOCK_FILE": ssh.Config.Paths.ResolverBlocks,
"SSH_PORT": strconv.FormatUint(uint64(ssh.Config.PublicSSHPort), 10),
},
@ -44,6 +44,6 @@ func (ssh *SSH2) Stack(env environment.Environment) component.StackWithResources
func (ssh SSH2) Context(parent component.InstallationContext) component.InstallationContext {
return component.InstallationContext{
bootstrap.Executable: ssh.Config.CurrentExecutable(ssh.Environment), // TODO: Does this make sense?
bootstrap.Executable: ssh.Config.Paths.CurrentExecutable(ssh.Environment), // TODO: Does this make sense?
}
}

View file

@ -86,7 +86,7 @@ func (ts Triplestore) OpenRaw(ctx context.Context, method, url string, body any,
if contentType != "" {
req.Header.Set("Content-Type", contentType)
}
req.SetBasicAuth(ts.Config.TriplestoreAdminUser, ts.Config.TriplestoreAdminPassword)
req.SetBasicAuth(ts.Config.TS.AdminUsername, ts.Config.TS.AdminPassword)
// and send it
return client.Do(req)

View file

@ -25,7 +25,7 @@ var (
)
func (ts *Triplestore) Path() string {
return filepath.Join(ts.Still.Config.DeployRoot, "core", "triplestore")
return filepath.Join(ts.Still.Config.Paths.Root, "core", "triplestore")
}
func (Triplestore) Context(parent component.InstallationContext) component.InstallationContext {
@ -45,7 +45,7 @@ func (ts *Triplestore) Stack(env environment.Environment) component.StackWithRes
EnvPath: "triplestore.env",
EnvContext: map[string]string{
"DOCKER_NETWORK_NAME": ts.Config.DockerNetworkName,
"DOCKER_NETWORK_NAME": ts.Config.Docker.Network,
},
MakeDirs: []string{

View file

@ -20,8 +20,8 @@ func (ts Triplestore) Update(ctx context.Context, progress io.Writer) error {
logging.LogMessage(progress, ctx, "Resetting admin user password")
{
res, err := ts.OpenRaw(ctx, "PUT", "/rest/security/users/"+ts.Config.TriplestoreAdminUser, TriplestoreUserPayload{
Password: ts.Config.TriplestoreAdminPassword,
res, err := ts.OpenRaw(ctx, "PUT", "/rest/security/users/"+ts.Config.TS.AdminUsername, TriplestoreUserPayload{
Password: ts.Config.TS.AdminPassword,
AppSettings: TriplestoreUserAppSettings{
DefaultInference: true,
DefaultVisGraphSchema: true,

View file

@ -20,7 +20,7 @@ var (
)
func (web *Web) Path() string {
return filepath.Join(web.Still.Config.DeployRoot, "core", "web")
return filepath.Join(web.Still.Config.Paths.Root, "core", "web")
}
func (*Web) Context(parent component.InstallationContext) component.InstallationContext {
@ -28,7 +28,7 @@ func (*Web) Context(parent component.InstallationContext) component.Installation
}
func (web Web) Stack(env environment.Environment) component.StackWithResources {
if web.Config.HTTPSEnabled() {
if web.Config.HTTP.HTTPSEnabled() {
return web.stackHTTPS(env)
} else {
return web.stackHTTP(env)
@ -46,8 +46,8 @@ func (web *Web) stackHTTPS(env environment.Environment) component.StackWithResou
EnvPath: "web.env",
EnvContext: map[string]string{
"DOCKER_NETWORK_NAME": web.Config.DockerNetworkName,
"CERT_EMAIL": web.Config.CertbotEmail,
"DOCKER_NETWORK_NAME": web.Config.Docker.Network,
"CERT_EMAIL": web.Config.HTTP.CertbotEmail,
},
TouchFilesPerm: 0600,
TouchFiles: []string{"acme.json"},
@ -65,8 +65,8 @@ func (web *Web) stackHTTP(env environment.Environment) component.StackWithResour
EnvPath: "web.env",
EnvContext: map[string]string{
"DOCKER_NETWORK_NAME": web.Config.DockerNetworkName,
"CERT_EMAIL": web.Config.CertbotEmail,
"DOCKER_NETWORK_NAME": web.Config.Docker.Network,
"CERT_EMAIL": web.Config.HTTP.CertbotEmail,
},
})
}

View file

@ -15,7 +15,7 @@ var errNoConfigFile = exit.Error{
var errOpenConfig = exit.Error{
ExitCode: exit.ExitGeneralArguments,
Message: "error loading configuration file: %s",
Message: "error loading configuration file: %q",
}
// NewDistillery creates a new distillery from the provided flags

View file

@ -23,14 +23,14 @@ func (barrel *Barrel) Stack() component.StackWithResources {
EnvPath: filepath.Join("barrel.env"),
EnvContext: map[string]string{
"DOCKER_NETWORK_NAME": barrel.Malt.Config.DockerNetworkName,
"DOCKER_NETWORK_NAME": barrel.Malt.Config.Docker.Network,
"SLUG": barrel.Slug,
"VIRTUAL_HOST": barrel.Domain(),
"HTTPS_ENABLED": barrel.Malt.Config.HTTPSEnabledEnv(),
"HTTPS_ENABLED": barrel.Malt.Config.HTTP.HTTPSEnabledEnv(),
"DATA_PATH": filepath.Join(barrel.FilesystemBase, "data"),
"RUNTIME_DIR": barrel.Malt.Config.RuntimeDir(),
"RUNTIME_DIR": barrel.Malt.Config.Paths.RuntimeDir(),
},
MakeDirs: []string{"data", ".composer"},

View file

@ -82,7 +82,7 @@ func (wisski *Prefixes) database(ctx context.Context, server *phpx.Server) (pref
func (prefixes *Prefixes) blocked() ([]string, error) {
// open the resolver block file
// TODO: move this to the distillery
file, err := prefixes.Malt.Environment.Open(prefixes.Malt.Config.SelfResolverBlockFile)
file, err := prefixes.Malt.Environment.Open(prefixes.Malt.Config.Paths.ResolverBlocks)
if err != nil {
return nil, err
}

View file

@ -30,11 +30,11 @@ func (reserve *Reserve) Stack() component.StackWithResources {
EnvPath: filepath.Join("reserve.env"),
EnvContext: map[string]string{
"DOCKER_NETWORK_NAME": reserve.Malt.Config.DockerNetworkName,
"DOCKER_NETWORK_NAME": reserve.Malt.Config.Docker.Network,
"SLUG": reserve.Slug,
"VIRTUAL_HOST": reserve.Domain(),
"HTTPS_ENABLED": reserve.Malt.Config.HTTPSEnabledEnv(),
"HTTPS_ENABLED": reserve.Malt.Config.HTTP.HTTPSEnabledEnv(),
},
}
}

View file

@ -6,7 +6,7 @@ import (
// Domain returns the full domain name of this WissKI
func (liquid *Liquid) Domain() string {
return liquid.Config.HostFromSlug(liquid.Slug)
return liquid.Config.HTTP.HostFromSlug(liquid.Slug)
}
// URL returns the public URL of this instance
@ -18,7 +18,7 @@ func (liquid *Liquid) URL() *url.URL {
}
// use http or https scheme depending on if the distillery has it enabled
if liquid.Malt.Config.HTTPSEnabled() {
if liquid.Malt.Config.HTTP.HTTPSEnabled() {
url.Scheme = "https"
} else {
url.Scheme = "http"