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:
parent
568c005d15
commit
945329a080
70 changed files with 1150 additions and 350 deletions
|
|
@ -21,6 +21,5 @@ func (c cfg) Description() wisski_distillery.Description {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg) Run(context wisski_distillery.Context) error {
|
func (cfg) Run(context wisski_distillery.Context) error {
|
||||||
context.Printf("%#v", context.Environment.Config)
|
return context.Environment.Config.Marshal(context.Stdout)
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
64
cmd/config_migrate.go
Normal file
64
cmd/config_migrate.go
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -68,7 +68,7 @@ func (si systemupdate) Run(context wisski_distillery.Context) error {
|
||||||
// create all the other directories
|
// create all the other directories
|
||||||
logging.LogMessage(context.Stderr, context.Context, "Ensuring distillery installation directories exist")
|
logging.LogMessage(context.Stderr, context.Context, "Ensuring distillery installation directories exist")
|
||||||
for _, d := range []string{
|
for _, d := range []string{
|
||||||
dis.Config.DeployRoot,
|
dis.Config.Paths.Root,
|
||||||
dis.Instances().Path(),
|
dis.Instances().Path(),
|
||||||
dis.Exporter().StagingPath(),
|
dis.Exporter().StagingPath(),
|
||||||
dis.Exporter().ArchivePath(),
|
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")
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -114,7 +114,7 @@ func (si systemupdate) Run(context wisski_distillery.Context) error {
|
||||||
// create the docker network
|
// create the docker network
|
||||||
// TODO: Use docker API for this
|
// TODO: Use docker API for this
|
||||||
logging.LogMessage(context.Stderr, context.Context, "Updating Docker Configuration")
|
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!
|
// install and update the various stacks!
|
||||||
ctx := component.InstallationContext{
|
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 {
|
func (si systemupdate) mustExec(context wisski_distillery.Context, workdir string, exe string, argv ...string) error {
|
||||||
dis := context.Environment
|
dis := context.Environment
|
||||||
if workdir == "" {
|
if workdir == "" {
|
||||||
workdir = dis.Config.DeployRoot
|
workdir = dis.Config.Paths.Root
|
||||||
}
|
}
|
||||||
code := dis.Still.Environment.Exec(context.Context, context.IOStream, workdir, exe, argv...)()
|
code := dis.Still.Environment.Exec(context.Context, context.IOStream, workdir, exe, argv...)()
|
||||||
if code != 0 {
|
if code != 0 {
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ var wdcli = wisski_distillery.NewProgram()
|
||||||
func init() {
|
func init() {
|
||||||
// self commands
|
// self commands
|
||||||
wdcli.Register(cmd.Config)
|
wdcli.Register(cmd.Config)
|
||||||
|
wdcli.Register(cmd.ConfigMigrate)
|
||||||
wdcli.Register(cmd.License)
|
wdcli.Register(cmd.License)
|
||||||
|
|
||||||
// setup commands
|
// setup commands
|
||||||
|
|
|
||||||
1
go.mod
1
go.mod
|
|
@ -24,6 +24,7 @@ require (
|
||||||
golang.org/x/exp v0.0.0-20230105202349-8879d0199aa3
|
golang.org/x/exp v0.0.0-20230105202349-8879d0199aa3
|
||||||
golang.org/x/sync v0.1.0
|
golang.org/x/sync v0.1.0
|
||||||
golang.org/x/term v0.3.0
|
golang.org/x/term v0.3.0
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
gorm.io/driver/mysql v1.4.4
|
gorm.io/driver/mysql v1.4.4
|
||||||
gorm.io/gorm v1.24.2
|
gorm.io/gorm v1.24.2
|
||||||
)
|
)
|
||||||
|
|
|
||||||
2
go.sum
2
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/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 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
||||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
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 h1:MX0K9Qvy0Na4o7qSC/YI7XxqUw5KDw01umqgID+svdQ=
|
||||||
gorm.io/driver/mysql v1.4.4/go.mod h1:BCg8cKI+R0j/rZRQxeKis/forqRwRSYOR8OM3Wo6hOM=
|
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=
|
gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ const Executable = "wdcli"
|
||||||
|
|
||||||
// ConfigFile is the name of the config file.
|
// ConfigFile is the name of the config file.
|
||||||
// It should be located inside the deployment directory.
|
// It should be located inside the deployment directory.
|
||||||
const ConfigFile = ".env"
|
const ConfigFile = "distillery.yaml"
|
||||||
|
|
||||||
// OverridesJSON is the name of the json overrides file.
|
// OverridesJSON is the name of the json overrides file.
|
||||||
// It should be located inside the deployment directory.
|
// It should be located inside the deployment directory.
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"hash/fnv"
|
"hash/fnv"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/url"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -19,80 +18,33 @@ import (
|
||||||
// Config contains many methods that do not require any interaction with any running components.
|
// 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].
|
// Methods that require running components are instead store inside the [Distillery] or an appropriate [Component].
|
||||||
type Config struct {
|
type Config struct {
|
||||||
// Several docker-compose files are created to manage global services and the system itself.
|
Paths PathsConfig `yaml:"paths" recurse:"true"`
|
||||||
// On top of this all real-system space will be created under this directory.
|
HTTP HTTPConfig `yaml:"http" recurse:"true"`
|
||||||
DeployRoot string `env:"DEPLOY_ROOT" default:"/var/www/deploy" parser:"abspath"`
|
Theme ThemeConfig `yaml:"theme" recurse:"true"`
|
||||||
|
Docker DockerConfig `yaml:"docker" recurse:"true"`
|
||||||
|
|
||||||
// Each created Drupal Instance corresponds to a single domain name.
|
SQL SQLConfig `yaml:"sql" recurse:"true"`
|
||||||
// These domain names should either be a complete domain name or a sub-domain of a default domain.
|
TS TSConfig `yaml:"triplestore" recurse:"true"`
|
||||||
// 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"`
|
|
||||||
|
|
||||||
// Maximum age for backup in days
|
// Maximum age for backup in days
|
||||||
MaxBackupAge int `env:"MAX_BACKUP_AGE" default:"" parser:"number"`
|
MaxBackupAge time.Duration `yaml:"age" validate:"duration"`
|
||||||
|
|
||||||
// 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"`
|
|
||||||
|
|
||||||
// Various components use password-based-authentication.
|
// Various components use password-based-authentication.
|
||||||
// These passwords are generated automatically.
|
// These passwords are generated automatically.
|
||||||
// This variable can be used to determine their length.
|
// 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
|
// 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
|
// session secret holds the secret for login
|
||||||
SessionSecret string `env:"SESSION_SECRET" default:"" parser:"nonempty"`
|
SessionSecret string `yaml:"session_secret" default:"" validate:"nonempty"`
|
||||||
|
|
||||||
// name of docker network to use
|
|
||||||
DockerNetworkName string `env:"DOCKER_NETWORK_NAME" default:"distillery" parser:"nonempty"`
|
|
||||||
|
|
||||||
// interval to trigger distillery cron tasks in
|
// 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 is the path this configuration was loaded from (if any)
|
||||||
ConfigPath string
|
ConfigPath string `yaml:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CSRFSecret return the csrfSecret derived from the session secret
|
// CSRFSecret return the csrfSecret derived from the session secret
|
||||||
|
|
|
||||||
|
|
@ -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}
|
|
||||||
83
internal/config/config_template.yml
Normal file
83
internal/config/config_template.yml
Normal 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"
|
||||||
23
internal/config/database.go
Normal file
23
internal/config/database.go
Normal 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"`
|
||||||
|
}
|
||||||
6
internal/config/docker.go
Normal file
6
internal/config/docker.go
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
type DockerConfig struct {
|
||||||
|
// name of docker network to use
|
||||||
|
Network string `yaml:"network" default:"distillery" validate:"nonempty"`
|
||||||
|
}
|
||||||
|
|
@ -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
10
internal/config/home.go
Normal 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"`
|
||||||
|
}
|
||||||
|
|
@ -7,24 +7,30 @@ import (
|
||||||
"github.com/tkw1536/goprogram/lib/collection"
|
"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.
|
// By default, only the 'self' domain above is caught.
|
||||||
func (cfg Config) HTTPSEnabled() bool {
|
// To catch additional domains, add them here (comma seperated)
|
||||||
return cfg.CertbotEmail != ""
|
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
|
// HTTPSEnabled returns if the distillery has HTTPS enabled, and false otherwise.
|
||||||
func (Config) HostRule(names ...string) string {
|
func (hcfg HTTPConfig) HTTPSEnabled() bool {
|
||||||
quoted := collection.MapSlice(names, func(name string) string {
|
return hcfg.CertbotEmail != ""
|
||||||
return "`" + name + "`"
|
|
||||||
})
|
|
||||||
return fmt.Sprintf("Host(%s)", strings.Join(quoted, ","))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTPSEnabledEnv returns "true" if https is enabled, and "false" otherwise.
|
// HTTPSEnabledEnv returns "true" if https is enabled, and "false" otherwise.
|
||||||
func (cfg Config) HTTPSEnabledEnv() string {
|
func (hcfg HTTPConfig) HTTPSEnabledEnv() string {
|
||||||
if cfg.HTTPSEnabled() {
|
if hcfg.HTTPSEnabled() {
|
||||||
return "true"
|
return "true"
|
||||||
}
|
}
|
||||||
return "false"
|
return "false"
|
||||||
|
|
@ -32,24 +38,18 @@ func (cfg Config) HTTPSEnabledEnv() string {
|
||||||
|
|
||||||
// HostFromSlug returns the hostname belonging to a given slug.
|
// HostFromSlug returns the hostname belonging to a given slug.
|
||||||
// When the slug is empty, returns the default (top-level) domain.
|
// 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 == "" {
|
if slug == "" {
|
||||||
return cfg.DefaultDomain
|
return cfg.PrimaryDomain
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%s.%s", slug, cfg.DefaultDomain)
|
return fmt.Sprintf("%s.%s", slug, cfg.PrimaryDomain)
|
||||||
}
|
|
||||||
|
|
||||||
// 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...)...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SlugFromHost returns the slug belonging to the appropriate host.'
|
// SlugFromHost returns the slug belonging to the appropriate host.'
|
||||||
//
|
//
|
||||||
// When host is a top-level domain, returns "", true.
|
// When host is a top-level domain, returns "", true.
|
||||||
// When no slug is found, returns "", false.
|
// 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.
|
// extract an ':port' that happens to be in the host.
|
||||||
domain, _, _ := strings.Cut(host, ":")
|
domain, _, _ := strings.Cut(host, ":")
|
||||||
domain = TrimSuffixFold(domain, ".wisski") // remove optional ".wisski" ending that is used inside docker
|
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)
|
domainL := strings.ToLower(domain)
|
||||||
|
|
||||||
// check all the possible domain endings
|
// 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)
|
suffixL := strings.ToLower(suffix)
|
||||||
if domainL == suffixL {
|
if domainL == suffixL {
|
||||||
return "", true
|
return "", true
|
||||||
|
|
@ -77,3 +77,18 @@ func TrimSuffixFold(s string, suffix string) string {
|
||||||
}
|
}
|
||||||
return s
|
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...)...)
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
// Package envreader
|
// Package envreader provides Scanner.
|
||||||
|
// It is deprecated and will be removed in a future release.
|
||||||
package envreader
|
package envreader
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
@ -14,7 +15,9 @@ import (
|
||||||
// Reads may be internally buffered.
|
// Reads may be internally buffered.
|
||||||
//
|
//
|
||||||
// An environment variable is of the form:
|
// An environment variable is of the form:
|
||||||
// KEY=VALUE
|
//
|
||||||
|
// KEY=VALUE
|
||||||
|
//
|
||||||
// on a separate line.
|
// on a separate line.
|
||||||
// Keys and values are case-sensitive and may contain anything except for newline characters.
|
// Keys and values are case-sensitive and may contain anything except for newline characters.
|
||||||
// Spaces around key and value are trimmed using [strings.TrimSpace].
|
// 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:
|
// A typical use-case of a scanner is as follows:
|
||||||
//
|
//
|
||||||
// scanner := NewScanner(r)
|
// scanner := NewScanner(r)
|
||||||
// for scanner.Scan() {
|
// for scanner.Scan() {
|
||||||
// // process any data ....
|
// // process any data ....
|
||||||
// fmt.Println(scanner.Data())
|
// fmt.Println(scanner.Data())
|
||||||
// }
|
// }
|
||||||
// if err := scanner.Err(); err != nil {
|
// if err := scanner.Err(); err != nil {
|
||||||
// // handle errors
|
// // handle errors
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// For the common use case of reading a set of distinct keys from a file see [ReadAll].
|
// For the common use case of reading a set of distinct keys from a file see [ReadAll].
|
||||||
type Scanner struct {
|
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.
|
// 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.
|
// 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) {
|
func ReadAll(r io.Reader) (values map[string]string, err error) {
|
||||||
|
// TODO: This is no longer used
|
||||||
scanner := NewScanner(r)
|
scanner := NewScanner(r)
|
||||||
|
|
||||||
// read and store all values
|
// read and store all values
|
||||||
147
internal/config/legacy/legacy.go
Normal file
147
internal/config/legacy/legacy.go
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -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
|
package stringparser
|
||||||
|
|
||||||
import (
|
import (
|
||||||
52
internal/config/paths.go
Normal file
52
internal/config/paths.go
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -2,60 +2,31 @@ package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"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/environment"
|
||||||
"github.com/FAU-CDI/wisski-distillery/pkg/envreader"
|
"github.com/FAU-CDI/wisski-distillery/pkg/validator"
|
||||||
"github.com/FAU-CDI/wisski-distillery/pkg/stringparser"
|
"gopkg.in/yaml.v3"
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Unmarshal updates this configuration from the provided [io.Reader].
|
// Unmarshal reads configuration from the provided io.Reader, and then validates it.
|
||||||
//
|
// Configuration is read in yaml format.
|
||||||
// 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 *Config) Unmarshal(env environment.Environment, src io.Reader) error {
|
func (config *Config) Unmarshal(env environment.Environment, src io.Reader) error {
|
||||||
// read all the values!
|
// read yaml!
|
||||||
values, err := envreader.ReadAll(src)
|
{
|
||||||
if err != nil {
|
decoder := yaml.NewDecoder(src)
|
||||||
return err
|
decoder.KnownFields(true)
|
||||||
}
|
if err := decoder.Decode(config); 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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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")
|
|
||||||
}
|
|
||||||
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"github.com/FAU-CDI/wisski-distillery/pkg/hostname"
|
"github.com/FAU-CDI/wisski-distillery/pkg/hostname"
|
||||||
"github.com/FAU-CDI/wisski-distillery/pkg/password"
|
"github.com/FAU-CDI/wisski-distillery/pkg/password"
|
||||||
"github.com/FAU-CDI/wisski-distillery/pkg/unpack"
|
"github.com/FAU-CDI/wisski-distillery/pkg/unpack"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
_ "embed"
|
_ "embed"
|
||||||
)
|
)
|
||||||
|
|
@ -87,7 +88,7 @@ func (tpl *Template) SetDefaults(env environment.Environment) (err error) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
//go:embed config_template
|
//go:embed config_template.yml
|
||||||
var templateBytes []byte
|
var templateBytes []byte
|
||||||
|
|
||||||
// MarshalTo marshals this template into dst
|
// MarshalTo marshals this template into dst
|
||||||
|
|
@ -100,10 +101,15 @@ func (tpl Template) MarshalTo(dst io.Writer) error {
|
||||||
field := tplType.Field(i)
|
field := tplType.Field(i)
|
||||||
|
|
||||||
key := field.Tag.Get("env")
|
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))
|
return unpack.WriteTemplate(dst, context, bytes.NewReader(templateBytes))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
32
internal/config/validators/collection.go
Normal file
32
internal/config/validators/collection.go
Normal 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
|
||||||
|
}
|
||||||
21
internal/config/validators/domain.go
Normal file
21
internal/config/validators/domain.go
Normal 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
|
||||||
|
}
|
||||||
14
internal/config/validators/duration.go
Normal file
14
internal/config/validators/duration.go
Normal 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
|
||||||
|
}
|
||||||
24
internal/config/validators/email.go
Normal file
24
internal/config/validators/email.go
Normal 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
|
||||||
|
}
|
||||||
27
internal/config/validators/files.go
Normal file
27
internal/config/validators/files.go
Normal 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
|
||||||
|
}
|
||||||
32
internal/config/validators/int.go
Normal file
32
internal/config/validators/int.go
Normal 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
|
||||||
|
}
|
||||||
24
internal/config/validators/slug.go
Normal file
24
internal/config/validators/slug.go
Normal 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
|
||||||
|
}
|
||||||
16
internal/config/validators/string.go
Normal file
16
internal/config/validators/string.go
Normal 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
|
||||||
|
}
|
||||||
46
internal/config/validators/url.go
Normal file
46
internal/config/validators/url.go
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -55,7 +55,7 @@ func (next *Next) getInstance(r *http.Request) (wisski *wisski.WissKI, path stri
|
||||||
}
|
}
|
||||||
|
|
||||||
// find the slug
|
// find the slug
|
||||||
slug, ok := next.Config.SlugFromHost(url.Host)
|
slug, ok := next.Config.HTTP.SlugFromHost(url.Host)
|
||||||
if slug == "" || !ok {
|
if slug == "" || !ok {
|
||||||
return nil, "", httpx.ErrBadRequest
|
return nil, "", httpx.ErrBadRequest
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ func (panel *UserPanel) sshRoute(ctx context.Context) http.Handler {
|
||||||
return sc, err
|
return sc, err
|
||||||
}
|
}
|
||||||
|
|
||||||
sc.Domain = panel.Config.DefaultDomain
|
sc.Domain = panel.Config.HTTP.PrimaryDomain
|
||||||
sc.Port = panel.Config.PublicSSHPort
|
sc.Port = panel.Config.PublicSSHPort
|
||||||
|
|
||||||
// pick the first domain that the user has access to as an example
|
// 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 {
|
} else {
|
||||||
sc.Slug = "example"
|
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)
|
sc.Keys, err = panel.Dependencies.Keys.Keys(r.Context(), user.User.User)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ type Exporter struct {
|
||||||
|
|
||||||
// Path returns the path that contains all snapshot related data.
|
// Path returns the path that contains all snapshot related data.
|
||||||
func (dis *Exporter) Path() string {
|
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.
|
// StagingPath returns the path to the directory containing a temporary staging area for snapshots.
|
||||||
|
|
|
||||||
|
|
@ -38,8 +38,8 @@ func (control *Config) Backup(scontext component.StagingContext) error {
|
||||||
func (control *Config) backupFiles() []string {
|
func (control *Config) backupFiles() []string {
|
||||||
return []string{
|
return []string{
|
||||||
control.Config.ConfigPath,
|
control.Config.ConfigPath,
|
||||||
control.Config.ExecutablePath(),
|
control.Config.Paths.ExecutablePath(),
|
||||||
control.Config.SelfOverridesFile,
|
control.Config.Paths.OverridesJSON,
|
||||||
control.Config.SelfResolverBlockFile,
|
control.Config.Paths.ResolverBlocks,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import (
|
||||||
// ShouldPrune determines if a file with the provided modification time should be
|
// ShouldPrune determines if a file with the provided modification time should be
|
||||||
// removed from the export log.
|
// removed from the export log.
|
||||||
func (exporter *Exporter) ShouldPrune(modtime time.Time) bool {
|
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
|
// Prune prunes all old exports
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/FAU-CDI/wisski-distillery/internal/config/validators"
|
||||||
"github.com/FAU-CDI/wisski-distillery/internal/wisski"
|
"github.com/FAU-CDI/wisski-distillery/internal/wisski"
|
||||||
"github.com/FAU-CDI/wisski-distillery/pkg/stringparser"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
@ -37,8 +37,8 @@ func (instances *Instances) Create(slug string) (wissKI *wisski.WissKI, err erro
|
||||||
|
|
||||||
// sql
|
// sql
|
||||||
|
|
||||||
wissKI.Liquid.Instance.SqlDatabase = instances.Config.MysqlDatabasePrefix + slug
|
wissKI.Liquid.Instance.SqlDatabase = instances.Config.SQL.DataPrefix + slug
|
||||||
wissKI.Liquid.Instance.SqlUsername = instances.Config.MysqlUserPrefix + slug
|
wissKI.Liquid.Instance.SqlUsername = instances.Config.SQL.UserPrefix + slug
|
||||||
|
|
||||||
wissKI.Liquid.Instance.SqlPassword, err = instances.Config.NewPassword()
|
wissKI.Liquid.Instance.SqlPassword, err = instances.Config.NewPassword()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -47,8 +47,8 @@ func (instances *Instances) Create(slug string) (wissKI *wisski.WissKI, err erro
|
||||||
|
|
||||||
// triplestore
|
// triplestore
|
||||||
|
|
||||||
wissKI.Liquid.Instance.GraphDBRepository = instances.Config.GraphDBRepoPrefix + slug
|
wissKI.Liquid.Instance.GraphDBRepository = instances.Config.TS.DataPrefix + slug
|
||||||
wissKI.Liquid.Instance.GraphDBUsername = instances.Config.GraphDBUserPrefix + slug
|
wissKI.Liquid.Instance.GraphDBUsername = instances.Config.TS.UserPrefix + slug
|
||||||
|
|
||||||
wissKI.Liquid.Instance.GraphDBPassword, err = instances.Config.NewPassword()
|
wissKI.Liquid.Instance.GraphDBPassword, err = instances.Config.NewPassword()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -73,7 +73,7 @@ var restrictedSlugs = []string{"www", "admin"}
|
||||||
// IsValidSlug checks if slug represents a valid slug for an instance.
|
// IsValidSlug checks if slug represents a valid slug for an instance.
|
||||||
func (instances *Instances) IsValidSlug(slug string) (string, error) {
|
func (instances *Instances) IsValidSlug(slug string) (string, error) {
|
||||||
// check that it is a slug
|
// check that it is a slug
|
||||||
slug, err := stringparser.ParseSlug(instances.Environment, slug)
|
err := validators.ValidateSlug(&slug, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errInvalidSlug
|
return "", errInvalidSlug
|
||||||
}
|
}
|
||||||
|
|
@ -84,5 +84,5 @@ func (instances *Instances) IsValidSlug(slug string) (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// return the slug
|
// return the slug
|
||||||
return strings.ToLower(slug), nil
|
return slug, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ type Instances struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (instances *Instances) Path() string {
|
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
|
// ErrWissKINotFound is returned when a WissKI is not found
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ var runtimeResources embed.FS
|
||||||
|
|
||||||
// Update installs or updates runtime components needed by this component.
|
// Update installs or updates runtime components needed by this component.
|
||||||
func (instances *Instances) Update(ctx context.Context, progress io.Writer) error {
|
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)
|
logging.ProgressF(progress, ctx, "[copy] %s\n", dst)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -84,14 +84,14 @@ func (resolver *Resolver) HandleRoute(ctx context.Context, route string) (http.H
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle the default domain name!
|
// handle the default domain name!
|
||||||
domainName := resolver.Config.DefaultDomain
|
domainName := resolver.Config.HTTP.PrimaryDomain
|
||||||
if domainName != "" {
|
if domainName != "" {
|
||||||
fallback.Data[fmt.Sprintf("^https?://(.*)\\.%s", regexp.QuoteMeta(domainName))] = fmt.Sprintf("https://$1.%s", 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")
|
logger.Info().Str("name", domainName).Msg("registering default domain")
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle the extra domains!
|
// 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)
|
fallback.Data[fmt.Sprintf("^https?://(.*)\\.%s", regexp.QuoteMeta(domain))] = fmt.Sprintf("https://$1.%s", domainName)
|
||||||
logger.Info().Str("name", domainName).Msg("registering legacy domain")
|
logger.Info().Str("name", domainName).Msg("registering legacy domain")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,7 @@
|
||||||
<code>root</code>
|
<code>root</code>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<code>{{.Config.DeployRoot}}</code>
|
<code>{{.Config.Paths.Root}}</code>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
@ -161,7 +161,7 @@
|
||||||
Docker Network Name
|
Docker Network Name
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<code>{{.Config.DockerNetworkName}}</code>
|
<code>{{.Config.Docker.Network}}</code>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ func (home *Home) HandleRoute(ctx context.Context, route string) (http.Handler,
|
||||||
dflt.Fallback = home.publicHandler(ctx)
|
dflt.Fallback = home.publicHandler(ctx)
|
||||||
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
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 {
|
switch {
|
||||||
case !ok:
|
case !ok:
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ func (home *Home) publicHandler(ctx context.Context) http.Handler {
|
||||||
// prepare about
|
// prepare about
|
||||||
pc.aboutContext.Logo = logoHTML
|
pc.aboutContext.Logo = logoHTML
|
||||||
pc.aboutContext.Instances = home.homeInstances.Get(nil)
|
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
|
// render the about template
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ func (home *Home) loadRedirect(ctx context.Context) (redirect Redirect, err erro
|
||||||
redirect.Permanent = false
|
redirect.Permanent = false
|
||||||
|
|
||||||
// load the overrides file
|
// load the overrides file
|
||||||
overrides, err := home.Environment.Open(home.Config.SelfOverridesFile)
|
overrides, err := home.Environment.Open(home.Config.Paths.OverridesJSON)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return redirect, err
|
return redirect, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ func (server *Server) Server(ctx context.Context, progress io.Writer) (public ht
|
||||||
|
|
||||||
var publicM, internalM mux.Mux[component.RouteContext]
|
var publicM, internalM mux.Mux[component.RouteContext]
|
||||||
publicM.Context = func(r *http.Request) 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{
|
return component.RouteContext{
|
||||||
DefaultDomain: slug == "" && ok,
|
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
|
// CSRF returns a CSRF handler for the given function
|
||||||
func (server *Server) csrf() func(http.Handler) http.Handler {
|
func (server *Server) csrf() func(http.Handler) http.Handler {
|
||||||
var opts []csrf.Option
|
var opts []csrf.Option
|
||||||
if !server.Config.HTTPSEnabled() {
|
if !server.Config.HTTP.HTTPSEnabled() {
|
||||||
opts = append(opts, csrf.Secure(false))
|
opts = append(opts, csrf.Secure(false))
|
||||||
}
|
}
|
||||||
opts = append(opts, csrf.SameSite(csrf.SameSiteStrictMode))
|
opts = append(opts, csrf.SameSite(csrf.SameSiteStrictMode))
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func (control Server) Path() string {
|
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
|
//go:embed all:server server.env
|
||||||
|
|
@ -26,15 +26,15 @@ func (server *Server) Stack(env environment.Environment) component.StackWithReso
|
||||||
EnvPath: "server.env",
|
EnvPath: "server.env",
|
||||||
|
|
||||||
EnvContext: map[string]string{
|
EnvContext: map[string]string{
|
||||||
"DOCKER_NETWORK_NAME": server.Config.DockerNetworkName,
|
"DOCKER_NETWORK_NAME": server.Config.Docker.Network,
|
||||||
"HOST_RULE": server.Config.DefaultHostRule(),
|
"HOST_RULE": server.Config.HTTP.DefaultHostRule(),
|
||||||
"HTTPS_ENABLED": server.Config.HTTPSEnabledEnv(),
|
"HTTPS_ENABLED": server.Config.HTTP.HTTPSEnabledEnv(),
|
||||||
|
|
||||||
"CONFIG_PATH": server.Config.ConfigPath,
|
"CONFIG_PATH": server.Config.ConfigPath,
|
||||||
"DEPLOY_ROOT": server.Config.DeployRoot,
|
"DEPLOY_ROOT": server.Config.Paths.Root,
|
||||||
|
|
||||||
"SELF_OVERRIDES_FILE": server.Config.SelfOverridesFile,
|
"SELF_OVERRIDES_FILE": server.Config.Paths.OverridesJSON,
|
||||||
"SELF_RESOLVER_BLOCK_FILE": server.Config.SelfResolverBlockFile,
|
"SELF_RESOLVER_BLOCK_FILE": server.Config.Paths.ResolverBlocks,
|
||||||
|
|
||||||
"CUSTOM_ASSETS_PATH": server.Dependencies.Templating.CustomAssetsPath(),
|
"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 {
|
func (server *Server) Context(parent component.InstallationContext) component.InstallationContext {
|
||||||
return 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?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import (
|
||||||
|
|
||||||
// CustomAssetsPath is the path custom assets are stored at
|
// CustomAssetsPath is the path custom assets are stored at
|
||||||
func (tpl *Templating) CustomAssetsPath() string {
|
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 {
|
func (tpl *Templating) CustomAssetPath(name string) string {
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Solr) Path() string {
|
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 {
|
func (*Solr) Context(parent component.InstallationContext) component.InstallationContext {
|
||||||
|
|
@ -40,7 +40,7 @@ func (solr *Solr) Stack(env environment.Environment) component.StackWithResource
|
||||||
|
|
||||||
EnvPath: "solr.env",
|
EnvPath: "solr.env",
|
||||||
EnvContext: map[string]string{
|
EnvContext: map[string]string{
|
||||||
"DOCKER_NETWORK_NAME": solr.Config.DockerNetworkName,
|
"DOCKER_NETWORK_NAME": solr.Config.Docker.Network,
|
||||||
},
|
},
|
||||||
|
|
||||||
MakeDirs: []string{
|
MakeDirs: []string{
|
||||||
|
|
|
||||||
|
|
@ -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
|
// 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) {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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
|
// dsn returns a dsn fof connecting to the database
|
||||||
func (sql *SQL) dsn(database string) string {
|
func (sql *SQL) dsn(database string) string {
|
||||||
user := sql.Config.MysqlAdminUser
|
user := sql.Config.SQL.AdminUsername
|
||||||
pass := sql.Config.MysqlAdminPassword
|
pass := sql.Config.SQL.AdminPassword
|
||||||
network := sql.network()
|
network := sql.network()
|
||||||
server := sql.ServerURL
|
server := sql.ServerURL
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
func (sql *SQL) Path() string {
|
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 {
|
func (*SQL) Context(parent component.InstallationContext) component.InstallationContext {
|
||||||
|
|
@ -49,8 +49,8 @@ func (sql *SQL) Stack(env environment.Environment) component.StackWithResources
|
||||||
|
|
||||||
EnvPath: "sql.env",
|
EnvPath: "sql.env",
|
||||||
EnvContext: map[string]string{
|
EnvContext: map[string]string{
|
||||||
"DOCKER_NETWORK_NAME": sql.Config.DockerNetworkName,
|
"DOCKER_NETWORK_NAME": sql.Config.Docker.Network,
|
||||||
"HTTPS_ENABLED": sql.Config.HTTPSEnabledEnv(),
|
"HTTPS_ENABLED": sql.Config.HTTP.HTTPSEnabledEnv(),
|
||||||
},
|
},
|
||||||
|
|
||||||
MakeDirsPerm: environment.DefaultDirPerm,
|
MakeDirsPerm: environment.DefaultDirPerm,
|
||||||
|
|
|
||||||
|
|
@ -54,8 +54,8 @@ func (sql *SQL) Update(ctx context.Context, progress io.Writer) error {
|
||||||
}
|
}
|
||||||
logging.LogMessage(progress, ctx, "Creating administrative user")
|
logging.LogMessage(progress, ctx, "Creating administrative user")
|
||||||
{
|
{
|
||||||
username := sql.Config.MysqlAdminUser
|
username := sql.Config.SQL.AdminUsername
|
||||||
password := sql.Config.MysqlAdminPassword
|
password := sql.Config.SQL.AdminPassword
|
||||||
if err := sql.CreateSuperuser(ctx, username, password, true); err != nil {
|
if err := sql.CreateSuperuser(ctx, username, password, true); err != nil {
|
||||||
return errSQLUnableToCreateUser
|
return errSQLUnableToCreateUser
|
||||||
}
|
}
|
||||||
|
|
@ -65,10 +65,10 @@ func (sql *SQL) Update(ctx context.Context, progress io.Writer) error {
|
||||||
// create the admin user
|
// create the admin user
|
||||||
logging.LogMessage(progress, ctx, "Creating sql database")
|
logging.LogMessage(progress, ctx, "Creating sql database")
|
||||||
{
|
{
|
||||||
if !sqle.IsSafeDatabaseLiteral(sql.Config.DistilleryDatabase) {
|
if !sqle.IsSafeDatabaseLiteral(sql.Config.SQL.Database) {
|
||||||
return errSQLUnsafeDatabaseName
|
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 {
|
if err := sql.Exec(createDBSQL); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ func (ssh2 *SSH2) HandleRoute(ctx context.Context, path string) (http.Handler, e
|
||||||
}
|
}
|
||||||
|
|
||||||
// find the host
|
// find the host
|
||||||
slug, ok := ssh2.Config.SlugFromHost(r.Host)
|
slug, ok := ssh2.Config.HTTP.SlugFromHost(r.Host)
|
||||||
if slug == "" || !ok {
|
if slug == "" || !ok {
|
||||||
httpx.TextInterceptor.Intercept(w, r, httpx.ErrNotFound)
|
httpx.TextInterceptor.Intercept(w, r, httpx.ErrNotFound)
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -36,14 +36,15 @@ func (ssh2 *SSH2) handleDirectTCP(srv *ssh.Server, conn *gossh.ServerConn, newCh
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
slug, ok := ssh2.Config.SlugFromHost(d.DestAddr)
|
slug, ok := ssh2.Config.HTTP.SlugFromHost(d.DestAddr)
|
||||||
if !ok || d.DestPort != 22 || !hasPermission(ctx, slug) {
|
if !ok || d.DestPort != 22 || !hasPermission(ctx, slug) {
|
||||||
newChan.Reject(gossh.Prohibited, "permission denied")
|
newChan.Reject(gossh.Prohibited, "permission denied")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: move this into an instance function somewhere
|
// 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
|
var dialer net.Dialer
|
||||||
dconn, err := dialer.DialContext(ctx, "tcp", dest)
|
dconn, err := dialer.DialContext(ctx, "tcp", dest)
|
||||||
|
|
|
||||||
|
|
@ -80,8 +80,8 @@ func (ssh2 *SSH2) handleConnection(session ssh.Session) {
|
||||||
banner := welcomeMessage
|
banner := welcomeMessage
|
||||||
for _, oldnew := range [][2]string{
|
for _, oldnew := range [][2]string{
|
||||||
{"${SLUG}", slug},
|
{"${SLUG}", slug},
|
||||||
{"${DOMAIN}", ssh2.Config.DefaultDomain},
|
{"${DOMAIN}", ssh2.Config.HTTP.PrimaryDomain},
|
||||||
{"${HOSTNAME}", slug + "." + ssh2.Config.DefaultDomain},
|
{"${HOSTNAME}", slug + "." + ssh2.Config.HTTP.PrimaryDomain},
|
||||||
{"${PORT}", strconv.FormatUint(uint64(ssh2.Config.PublicSSHPort), 10)},
|
{"${PORT}", strconv.FormatUint(uint64(ssh2.Config.PublicSSHPort), 10)},
|
||||||
} {
|
} {
|
||||||
banner = strings.ReplaceAll(banner, oldnew[0], oldnew[1])
|
banner = strings.ReplaceAll(banner, oldnew[0], oldnew[1])
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func (ssh SSH2) Path() string {
|
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
|
//go:embed all:ssh2 ssh2.env
|
||||||
|
|
@ -24,15 +24,15 @@ func (ssh *SSH2) Stack(env environment.Environment) component.StackWithResources
|
||||||
EnvPath: "ssh2.env",
|
EnvPath: "ssh2.env",
|
||||||
|
|
||||||
EnvContext: map[string]string{
|
EnvContext: map[string]string{
|
||||||
"DOCKER_NETWORK_NAME": ssh.Config.DockerNetworkName,
|
"DOCKER_NETWORK_NAME": ssh.Config.Docker.Network,
|
||||||
"HOST_RULE": ssh.Config.DefaultHostRule(),
|
"HOST_RULE": ssh.Config.HTTP.DefaultHostRule(),
|
||||||
"HTTPS_ENABLED": ssh.Config.HTTPSEnabledEnv(),
|
"HTTPS_ENABLED": ssh.Config.HTTP.HTTPSEnabledEnv(),
|
||||||
|
|
||||||
"CONFIG_PATH": ssh.Config.ConfigPath,
|
"CONFIG_PATH": ssh.Config.ConfigPath,
|
||||||
"DEPLOY_ROOT": ssh.Config.DeployRoot,
|
"DEPLOY_ROOT": ssh.Config.Paths.Root,
|
||||||
|
|
||||||
"SELF_OVERRIDES_FILE": ssh.Config.SelfOverridesFile,
|
"SELF_OVERRIDES_FILE": ssh.Config.Paths.OverridesJSON,
|
||||||
"SELF_RESOLVER_BLOCK_FILE": ssh.Config.SelfResolverBlockFile,
|
"SELF_RESOLVER_BLOCK_FILE": ssh.Config.Paths.ResolverBlocks,
|
||||||
|
|
||||||
"SSH_PORT": strconv.FormatUint(uint64(ssh.Config.PublicSSHPort), 10),
|
"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 {
|
func (ssh SSH2) Context(parent component.InstallationContext) component.InstallationContext {
|
||||||
return 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?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,7 @@ func (ts Triplestore) OpenRaw(ctx context.Context, method, url string, body any,
|
||||||
if contentType != "" {
|
if contentType != "" {
|
||||||
req.Header.Set("Content-Type", 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
|
// and send it
|
||||||
return client.Do(req)
|
return client.Do(req)
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
func (ts *Triplestore) Path() string {
|
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 {
|
func (Triplestore) Context(parent component.InstallationContext) component.InstallationContext {
|
||||||
|
|
@ -45,7 +45,7 @@ func (ts *Triplestore) Stack(env environment.Environment) component.StackWithRes
|
||||||
|
|
||||||
EnvPath: "triplestore.env",
|
EnvPath: "triplestore.env",
|
||||||
EnvContext: map[string]string{
|
EnvContext: map[string]string{
|
||||||
"DOCKER_NETWORK_NAME": ts.Config.DockerNetworkName,
|
"DOCKER_NETWORK_NAME": ts.Config.Docker.Network,
|
||||||
},
|
},
|
||||||
|
|
||||||
MakeDirs: []string{
|
MakeDirs: []string{
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,8 @@ func (ts Triplestore) Update(ctx context.Context, progress io.Writer) error {
|
||||||
|
|
||||||
logging.LogMessage(progress, ctx, "Resetting admin user password")
|
logging.LogMessage(progress, ctx, "Resetting admin user password")
|
||||||
{
|
{
|
||||||
res, err := ts.OpenRaw(ctx, "PUT", "/rest/security/users/"+ts.Config.TriplestoreAdminUser, TriplestoreUserPayload{
|
res, err := ts.OpenRaw(ctx, "PUT", "/rest/security/users/"+ts.Config.TS.AdminUsername, TriplestoreUserPayload{
|
||||||
Password: ts.Config.TriplestoreAdminPassword,
|
Password: ts.Config.TS.AdminPassword,
|
||||||
AppSettings: TriplestoreUserAppSettings{
|
AppSettings: TriplestoreUserAppSettings{
|
||||||
DefaultInference: true,
|
DefaultInference: true,
|
||||||
DefaultVisGraphSchema: true,
|
DefaultVisGraphSchema: true,
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ var (
|
||||||
)
|
)
|
||||||
|
|
||||||
func (web *Web) Path() string {
|
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 {
|
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 {
|
func (web Web) Stack(env environment.Environment) component.StackWithResources {
|
||||||
if web.Config.HTTPSEnabled() {
|
if web.Config.HTTP.HTTPSEnabled() {
|
||||||
return web.stackHTTPS(env)
|
return web.stackHTTPS(env)
|
||||||
} else {
|
} else {
|
||||||
return web.stackHTTP(env)
|
return web.stackHTTP(env)
|
||||||
|
|
@ -46,8 +46,8 @@ func (web *Web) stackHTTPS(env environment.Environment) component.StackWithResou
|
||||||
EnvPath: "web.env",
|
EnvPath: "web.env",
|
||||||
|
|
||||||
EnvContext: map[string]string{
|
EnvContext: map[string]string{
|
||||||
"DOCKER_NETWORK_NAME": web.Config.DockerNetworkName,
|
"DOCKER_NETWORK_NAME": web.Config.Docker.Network,
|
||||||
"CERT_EMAIL": web.Config.CertbotEmail,
|
"CERT_EMAIL": web.Config.HTTP.CertbotEmail,
|
||||||
},
|
},
|
||||||
TouchFilesPerm: 0600,
|
TouchFilesPerm: 0600,
|
||||||
TouchFiles: []string{"acme.json"},
|
TouchFiles: []string{"acme.json"},
|
||||||
|
|
@ -65,8 +65,8 @@ func (web *Web) stackHTTP(env environment.Environment) component.StackWithResour
|
||||||
EnvPath: "web.env",
|
EnvPath: "web.env",
|
||||||
|
|
||||||
EnvContext: map[string]string{
|
EnvContext: map[string]string{
|
||||||
"DOCKER_NETWORK_NAME": web.Config.DockerNetworkName,
|
"DOCKER_NETWORK_NAME": web.Config.Docker.Network,
|
||||||
"CERT_EMAIL": web.Config.CertbotEmail,
|
"CERT_EMAIL": web.Config.HTTP.CertbotEmail,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ var errNoConfigFile = exit.Error{
|
||||||
|
|
||||||
var errOpenConfig = exit.Error{
|
var errOpenConfig = exit.Error{
|
||||||
ExitCode: exit.ExitGeneralArguments,
|
ExitCode: exit.ExitGeneralArguments,
|
||||||
Message: "error loading configuration file: %s",
|
Message: "error loading configuration file: %q",
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDistillery creates a new distillery from the provided flags
|
// NewDistillery creates a new distillery from the provided flags
|
||||||
|
|
|
||||||
|
|
@ -23,14 +23,14 @@ func (barrel *Barrel) Stack() component.StackWithResources {
|
||||||
EnvPath: filepath.Join("barrel.env"),
|
EnvPath: filepath.Join("barrel.env"),
|
||||||
|
|
||||||
EnvContext: map[string]string{
|
EnvContext: map[string]string{
|
||||||
"DOCKER_NETWORK_NAME": barrel.Malt.Config.DockerNetworkName,
|
"DOCKER_NETWORK_NAME": barrel.Malt.Config.Docker.Network,
|
||||||
|
|
||||||
"SLUG": barrel.Slug,
|
"SLUG": barrel.Slug,
|
||||||
"VIRTUAL_HOST": barrel.Domain(),
|
"VIRTUAL_HOST": barrel.Domain(),
|
||||||
"HTTPS_ENABLED": barrel.Malt.Config.HTTPSEnabledEnv(),
|
"HTTPS_ENABLED": barrel.Malt.Config.HTTP.HTTPSEnabledEnv(),
|
||||||
|
|
||||||
"DATA_PATH": filepath.Join(barrel.FilesystemBase, "data"),
|
"DATA_PATH": filepath.Join(barrel.FilesystemBase, "data"),
|
||||||
"RUNTIME_DIR": barrel.Malt.Config.RuntimeDir(),
|
"RUNTIME_DIR": barrel.Malt.Config.Paths.RuntimeDir(),
|
||||||
},
|
},
|
||||||
|
|
||||||
MakeDirs: []string{"data", ".composer"},
|
MakeDirs: []string{"data", ".composer"},
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ func (wisski *Prefixes) database(ctx context.Context, server *phpx.Server) (pref
|
||||||
func (prefixes *Prefixes) blocked() ([]string, error) {
|
func (prefixes *Prefixes) blocked() ([]string, error) {
|
||||||
// open the resolver block file
|
// open the resolver block file
|
||||||
// TODO: move this to the distillery
|
// 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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,11 +30,11 @@ func (reserve *Reserve) Stack() component.StackWithResources {
|
||||||
EnvPath: filepath.Join("reserve.env"),
|
EnvPath: filepath.Join("reserve.env"),
|
||||||
|
|
||||||
EnvContext: map[string]string{
|
EnvContext: map[string]string{
|
||||||
"DOCKER_NETWORK_NAME": reserve.Malt.Config.DockerNetworkName,
|
"DOCKER_NETWORK_NAME": reserve.Malt.Config.Docker.Network,
|
||||||
|
|
||||||
"SLUG": reserve.Slug,
|
"SLUG": reserve.Slug,
|
||||||
"VIRTUAL_HOST": reserve.Domain(),
|
"VIRTUAL_HOST": reserve.Domain(),
|
||||||
"HTTPS_ENABLED": reserve.Malt.Config.HTTPSEnabledEnv(),
|
"HTTPS_ENABLED": reserve.Malt.Config.HTTP.HTTPSEnabledEnv(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import (
|
||||||
|
|
||||||
// Domain returns the full domain name of this WissKI
|
// Domain returns the full domain name of this WissKI
|
||||||
func (liquid *Liquid) Domain() string {
|
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
|
// 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
|
// 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"
|
url.Scheme = "https"
|
||||||
} else {
|
} else {
|
||||||
url.Scheme = "http"
|
url.Scheme = "http"
|
||||||
|
|
|
||||||
88
pkg/validator/validator.go
Normal file
88
pkg/validator/validator.go
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
package validator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
validateTag = "validate"
|
||||||
|
recurseTag = "recurse"
|
||||||
|
dfltTag = "default"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Validate validates an object of type T, setting defaults where appropriate.
|
||||||
|
//
|
||||||
|
// T must be a struct type, when this is not the case, returns ErrNotAStruct.
|
||||||
|
// validators should contain a set of validators.
|
||||||
|
//
|
||||||
|
// Validate iterates over the fields and tags of those fields as follows:
|
||||||
|
// - If the 'validate' tag is not the empty string, read the appropriate validator from the map, and call the function.
|
||||||
|
// If the element in the validators map does not exist, returns an error that unwraps to type UnknownValidator.
|
||||||
|
// If the element in the validators map is not a validator, returns an error that unwraps to type NotAValidator.
|
||||||
|
// If the type of validator function does not match the field type, returns an error that unwraps to type IncompatibleValidator.
|
||||||
|
// - If the 'recurse' tag is not the empty string, recurse into the struct type by calling Validate on it.
|
||||||
|
// If the annotated field is not a struct, return an error.
|
||||||
|
//
|
||||||
|
// Any error is wrapped in a FieldError, indicating the field they occured in.
|
||||||
|
// Recursive validate calls may result in FieldError wraps.
|
||||||
|
// For a description of struct tags, see [reflect.StructTag].
|
||||||
|
func Validate[T any](data *T, validators map[string]any) error {
|
||||||
|
return validate(reflect.ValueOf(data).Elem(), validators)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FieldError wraps an error to indicate which field it occured in.
|
||||||
|
type FieldError struct {
|
||||||
|
Field string
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fe FieldError) Error() string {
|
||||||
|
return fmt.Sprintf("field %q: %s", fe.Field, fe.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fe FieldError) Unwrap() error {
|
||||||
|
return fe.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrNotAStruct = errors.New("validate called on non-struct type")
|
||||||
|
|
||||||
|
func validate(datum reflect.Value, validators Collection) error {
|
||||||
|
// make sure that we have a struct type
|
||||||
|
typ := datum.Type()
|
||||||
|
if typ.Kind() != reflect.Struct {
|
||||||
|
return ErrNotAStruct
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldC := typ.NumField()
|
||||||
|
for i := 0; i < fieldC; i++ {
|
||||||
|
field := typ.Field(i)
|
||||||
|
|
||||||
|
// if the recurse tag is set, do the recursion!
|
||||||
|
if field.Tag.Get(recurseTag) != "" {
|
||||||
|
if err := validate(datum.FieldByName(field.Name), validators); err != nil {
|
||||||
|
return FieldError{Field: field.Name, Err: err}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if there is a validator associated with this tag
|
||||||
|
// and if not, skip it!
|
||||||
|
validator := field.Tag.Get(validateTag)
|
||||||
|
if validator == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// call the actual validator
|
||||||
|
if err := validators.Call(
|
||||||
|
validator,
|
||||||
|
datum.FieldByName(field.Name),
|
||||||
|
field.Tag.Get(dfltTag),
|
||||||
|
); err != nil {
|
||||||
|
return FieldError{Field: field.Name, Err: err}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
123
pkg/validator/validator_test.go
Normal file
123
pkg/validator/validator_test.go
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
package validator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExampleValidate() {
|
||||||
|
var value struct {
|
||||||
|
Number int `validate:"positive" default:"234"`
|
||||||
|
String string `validate:"nonempty" default:"stuff"`
|
||||||
|
Recursive struct {
|
||||||
|
Number int `validate:"positive" default:"45"`
|
||||||
|
String string `validate:"nonempty" default:"more"`
|
||||||
|
} `recurse:"true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
collection := make(Collection, 2)
|
||||||
|
Add(collection, "positive", func(value *int, dflt string) error {
|
||||||
|
if *value == 0 {
|
||||||
|
i, err := strconv.ParseInt(dflt, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*value = int(i)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if *value < 0 {
|
||||||
|
return errors.New("not positive")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
Add(collection, "nonempty", func(value *string, dflt string) error {
|
||||||
|
if *value == "" {
|
||||||
|
*value = dflt
|
||||||
|
}
|
||||||
|
if *value == "" {
|
||||||
|
return errors.New("empty string")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
err := Validate(&value, collection)
|
||||||
|
fmt.Printf("%v\n", value)
|
||||||
|
fmt.Println(err)
|
||||||
|
// Output: {234 stuff {45 more}}
|
||||||
|
// <nil>
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleValidate_fail() {
|
||||||
|
var value struct {
|
||||||
|
Number int `validate:"positive" default:"12"`
|
||||||
|
String string `validate:"nonempty" default:"stuff"`
|
||||||
|
Recursive struct {
|
||||||
|
Number int `validate:"positive" default:"12"`
|
||||||
|
String string `validate:"nonempty"`
|
||||||
|
} `recurse:"true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
collection := make(Collection, 2)
|
||||||
|
Add(collection, "positive", func(value *int, dflt string) error {
|
||||||
|
if *value == 0 {
|
||||||
|
i, err := strconv.ParseInt(dflt, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*value = int(i)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if *value < 0 {
|
||||||
|
return errors.New("not positive")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
Add(collection, "nonempty", func(value *string, dflt string) error {
|
||||||
|
if *value == "" {
|
||||||
|
*value = dflt
|
||||||
|
}
|
||||||
|
if *value == "" {
|
||||||
|
return errors.New("empty string")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
err := Validate(&value, collection)
|
||||||
|
fmt.Printf("%v\n", value)
|
||||||
|
fmt.Println(err)
|
||||||
|
// Output: {12 stuff {12 }}
|
||||||
|
// field "Recursive": field "String": empty string
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleValidate_notastruct() {
|
||||||
|
var value int
|
||||||
|
err := Validate(&value, nil)
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleValidate_notavalidator() {
|
||||||
|
var value struct {
|
||||||
|
Field int `validate:"generic"`
|
||||||
|
}
|
||||||
|
collection := make(Collection, 2)
|
||||||
|
collection["generic"] = func(x, y int) error {
|
||||||
|
panic("never reached")
|
||||||
|
}
|
||||||
|
err := Validate(&value, collection)
|
||||||
|
fmt.Println(err)
|
||||||
|
// Output: field "Field": entry "generic" in validators is not a valiator
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleValidate_invalid() {
|
||||||
|
var value struct {
|
||||||
|
Field int `validate:"string"`
|
||||||
|
}
|
||||||
|
collection := make(Collection, 2)
|
||||||
|
collection["string"] = func(value *string, dflt string) error {
|
||||||
|
panic("never reached")
|
||||||
|
}
|
||||||
|
err := Validate(&value, collection)
|
||||||
|
fmt.Println(err)
|
||||||
|
// Output: field "Field": validator "string": got type string, expected type int
|
||||||
|
}
|
||||||
131
pkg/validator/vmap.go
Normal file
131
pkg/validator/vmap.go
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
package validator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/tkw1536/goprogram/lib/reflectx"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Collection represents a set of validators.
|
||||||
|
// The zero value is not ready to use; it should be created using make().
|
||||||
|
//
|
||||||
|
// A validator is a non-nil function with signature func(value *F, dflt string) error.
|
||||||
|
// Here F is the type of a value of a field.
|
||||||
|
// The value is the initialized value to be validated.
|
||||||
|
// The validator may perform abitrary normalization on the value.
|
||||||
|
// dflt is the default value (read from the 'default' tag).
|
||||||
|
// error should be an appropriate error that occured.
|
||||||
|
//
|
||||||
|
// A validator function is applied by calling it.
|
||||||
|
type Collection map[string]any
|
||||||
|
|
||||||
|
// Add adds a Validator to the provided collection of validators.
|
||||||
|
// Any previously validator of the same name is overwritten.
|
||||||
|
func Add[F any](coll Collection, name string, validator func(value *F, dflt string) error) {
|
||||||
|
coll[name] = validator
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSlice adds a Validator to the provided collection of validators that validates a slice of the given type. The default is seperated by seperator.
|
||||||
|
func AddSlice[F any](coll Collection, name string, sep string, validator func(value *F, dflt string) error) {
|
||||||
|
Add(coll, name, func(value *[]F, dflt string) error {
|
||||||
|
// some value is set, so we do not need to set the default!
|
||||||
|
if *value != nil {
|
||||||
|
for i := range *value {
|
||||||
|
if err := validator(&(*value)[i], ""); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// no default provided => set if to an empty slice
|
||||||
|
if dflt == "" {
|
||||||
|
*value = make([]F, 0)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// some default provided => iterate over the underlying validator
|
||||||
|
dflts := strings.Split(dflt, sep)
|
||||||
|
*value = make([]F, len(dflts))
|
||||||
|
for i := range *value {
|
||||||
|
if err := validator(&(*value)[i], dflts[i]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
errTyp = reflectx.TypeOf[error]()
|
||||||
|
strTyp = reflectx.TypeOf[string]()
|
||||||
|
)
|
||||||
|
|
||||||
|
// UnknownValidator is an error returned from Validate if a validator does not exist
|
||||||
|
type UnknownValidator string
|
||||||
|
|
||||||
|
func (uv UnknownValidator) Error() string {
|
||||||
|
return fmt.Sprintf("unknown validator %q", string(uv))
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotAValidator is an error returned from Validate if an entry in the validators map is not a validator
|
||||||
|
type NotAValidator string
|
||||||
|
|
||||||
|
func (nv NotAValidator) Error() string {
|
||||||
|
return fmt.Sprintf("entry %q in validators is not a valiator", string(nv))
|
||||||
|
}
|
||||||
|
|
||||||
|
// IncompatibleValidator is returned when a validator in the validators map is incompatible
|
||||||
|
type IncompatibleValidator struct {
|
||||||
|
Validator string
|
||||||
|
GotType reflect.Type
|
||||||
|
ExpectedType reflect.Type
|
||||||
|
}
|
||||||
|
|
||||||
|
func (iv IncompatibleValidator) Error() string {
|
||||||
|
return fmt.Sprintf("validator %q: got type %s, expected type %s", iv.Validator, iv.GotType, iv.ExpectedType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call calls the validator with the given name, on the given value, and with the provided default.
|
||||||
|
// See documentation of [Validate] for details.
|
||||||
|
func (coll Collection) Call(name string, field reflect.Value, dflt string) error {
|
||||||
|
validator, ok := coll[name]
|
||||||
|
if !ok {
|
||||||
|
return UnknownValidator(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the type of the validator
|
||||||
|
vFunc := reflect.ValueOf(validator)
|
||||||
|
vTyp := vFunc.Type()
|
||||||
|
|
||||||
|
// ensure that vTyp is of type func(*F,string) error
|
||||||
|
// where T is the type of the field
|
||||||
|
//
|
||||||
|
// - the first if assumes checks for some type F
|
||||||
|
// - the second if checks if the F is the right one
|
||||||
|
if validator == nil || vTyp.Kind() != reflect.Func || // func
|
||||||
|
vTyp.NumIn() != 2 || vTyp.In(0).Kind() != reflect.Pointer || vTyp.In(1) != strTyp || // (*F,string)
|
||||||
|
vTyp.NumOut() != 1 || vTyp.Out(0) != errTyp { // error
|
||||||
|
return NotAValidator(name)
|
||||||
|
}
|
||||||
|
if vTyp.In(0).Elem() != field.Type() { // the correct *F
|
||||||
|
return IncompatibleValidator{
|
||||||
|
Validator: name,
|
||||||
|
GotType: vTyp.In(0).Elem(),
|
||||||
|
ExpectedType: field.Type(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// call the validator function, and return an error
|
||||||
|
results := vFunc.Call([]reflect.Value{field.Addr(), reflect.ValueOf(dflt)})
|
||||||
|
|
||||||
|
// turn the result into an error
|
||||||
|
// NOTE: We can't just .(error) here because that panic()s on err == nil
|
||||||
|
err := results[0].Interface()
|
||||||
|
if err, ok := err.(error); ok {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -72,8 +72,8 @@ func NewProgram() Program {
|
||||||
|
|
||||||
// when not running inside docker and we need a distillery
|
// when not running inside docker and we need a distillery
|
||||||
// then we should warn if we are not using the distillery executable.
|
// then we should warn if we are not using the distillery executable.
|
||||||
if dis := context.Environment; !context.Args.Flags.InternalInDocker && context.Description.Requirements.NeedsDistillery && !dis.Config.UsingDistilleryExecutable(dis.Environment) {
|
if dis := context.Environment; !context.Args.Flags.InternalInDocker && context.Description.Requirements.NeedsDistillery && !dis.Config.Paths.UsingDistilleryExecutable(dis.Environment) {
|
||||||
context.EPrintf(warnNoDeployWdcli, bootstrap.Executable, dis.Config.ExecutablePath())
|
context.EPrintf(warnNoDeployWdcli, bootstrap.Executable, dis.Config.Paths.ExecutablePath())
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue