Do a large chunk of the move to go
This commit moves a huge chunk of the code to go. The TODO.md document indicates what is left to be done.
This commit is contained in:
parent
db2ad9b4bd
commit
7b38fdd801
93 changed files with 4689 additions and 645 deletions
153
internal/config/config.go
Normal file
153
internal/config/config.go
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
// Package config implements reading and validating a WissKIDistillery configuration file.
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Config represents the configuration of a distillery instance
|
||||
type Config struct {
|
||||
// Several docker-compose files are created to manage global services and the system itself.
|
||||
// On top of this all real-system space will be created under this directory.
|
||||
DeployRoot string `env:"DEPLOY_ROOT" default:"/var/www/deploy" validator:"is_valid_abspath"`
|
||||
|
||||
// Each created Drupal Instance corresponds to a single domain name.
|
||||
// These domain names should either be a complete domain name or a sub-domain of a default domain.
|
||||
// This setting configures the default domain-name to create subdomains of.
|
||||
DefaultDomain string `env:"DEFAULT_DOMAIN" default:"localhost.kwarc.info" validator:"is_valid_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:"" validator:"is_valid_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:"" validator:"is_valid_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:"" validator:"is_valid_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:"" validator:"is_valid_email"`
|
||||
|
||||
// Maximum age for backup
|
||||
MaxBackupAge int `env:"MAX_BACKUP_AGE" default:"" validator:"is_valid_number"`
|
||||
|
||||
// Each Drupal instance requires a corresponding system user, database users and databases.
|
||||
// These are also set by the appropriate domain name.
|
||||
// To differentiate them from other users of the system, these names can be prefixed.
|
||||
// The prefix to use can be configured here.
|
||||
// When changing these please consider that no system user may exist that has the same name as a mysql user.
|
||||
// This is a MariaDB restriction.
|
||||
MysqlUserPrefix string `env:"MYSQL_USER_PREFIX" default:"mysql-factory-" validator:"is_valid_slug"`
|
||||
MysqlDatabasePrefix string `env:"MYSQL_DATABASE_PREFIX" default:"mysql-factory-" validator:"is_valid_slug"`
|
||||
GraphDBUserPrefix string `env:"GRAPHDB_USER_PREFIX" default:"mysql-factory-" validator:"is_valid_slug"`
|
||||
GraphDBRepoPrefix string `env:"GRAPHDB_REPO_PREFIX" default:"mysql-factory-" validator:"is_valid_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.
|
||||
DistilleryBookkeepingDatabase string `env:"DISTILLERY_BOOKKEEPING_DATABASE" default:"distillery" validator:"is_valid_slug"`
|
||||
DistilleryBookkeepingTable string `env:"DISTILLERY_BOOKKEEPING_TABLE" default:"distillery" validator:"is_valid_slug"`
|
||||
|
||||
// Various components use password-based-authentication.
|
||||
// These passwords are generated automatically.
|
||||
// This variable can be used to determine their length.
|
||||
PasswordLength int `env:"PASSWORD_LENGTH" default:"64" validator:"is_valid_number"`
|
||||
|
||||
// A file to be used for global authorized_keys for the ssh server.
|
||||
GlobalAuthorizedKeysFile string `env:"GLOBAL_AUTHORIZED_KEYS_FILE" default:"/distillery/authorized_keys" validator:"is_valid_file"`
|
||||
|
||||
// admin credentials for graphdb
|
||||
TriplestoreAdminUser string `env:"GRAPHDB_ADMIN_USER" default:"admin" validator:"is_nonempty"`
|
||||
TriplestoreAdminPassword string `env:"GRAPHDB_ADMIN_PASSWORD" default:"" validator:"is_nonempty"`
|
||||
|
||||
// admin credentials for the Mysql database
|
||||
MysqlAdminUser string `env:"MYSQL_ADMIN_USER" default:"admin" validator:"is_nonempty"`
|
||||
MysqlAdminPassword string `env:"MYSQL_ADMIN_PASSWORD" default:"admin" validator:"is_nonempty"`
|
||||
}
|
||||
|
||||
func (config Config) String() string {
|
||||
values := &strings.Builder{}
|
||||
|
||||
vConfig := reflect.ValueOf(config)
|
||||
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)
|
||||
|
||||
fmt.Fprintf(values, "%s=%v\n", tField.Tag.Get("env"), vField.Interface())
|
||||
}
|
||||
|
||||
return values.String()
|
||||
}
|
||||
|
||||
func (config *Config) Unmarshal(src io.Reader) error {
|
||||
// read all the values!
|
||||
values, err := 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)
|
||||
|
||||
env := tField.Tag.Get("env")
|
||||
dflt := tField.Tag.Get("default")
|
||||
validator := tField.Tag.Get("validator")
|
||||
|
||||
// read the value with a default
|
||||
value, ok := values[env]
|
||||
if !ok || value == "" {
|
||||
if dflt == "" {
|
||||
continue
|
||||
}
|
||||
value = dflt
|
||||
}
|
||||
|
||||
// use the validator
|
||||
vFunc, ok := knownValidators[validator]
|
||||
if vFunc == nil || !ok {
|
||||
return errors.Errorf("Unable to read %q refers to unknown validator %s", env, validator)
|
||||
}
|
||||
|
||||
// get the parsed value
|
||||
checked, err := vFunc(value)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Unable to read %q: Validator %s", env, validator)
|
||||
}
|
||||
|
||||
// set the value of the field
|
||||
var errSet interface{}
|
||||
func() {
|
||||
defer func() {
|
||||
errSet = recover()
|
||||
}()
|
||||
vField.Set(reflect.ValueOf(checked))
|
||||
}()
|
||||
|
||||
// capture any error
|
||||
if errSet != nil {
|
||||
return errors.Errorf("Unable to parse %q: validator %s returned %q", tField.Name, validator, errSet)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
76
internal/config/file.go
Normal file
76
internal/config/file.go
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Scanner scans an io.Reader for a source file
|
||||
type Scanner struct {
|
||||
src *bufio.Scanner
|
||||
|
||||
key string
|
||||
value string
|
||||
}
|
||||
|
||||
func NewScanner(r io.Reader) *Scanner {
|
||||
return &Scanner{
|
||||
src: bufio.NewScanner(r),
|
||||
}
|
||||
}
|
||||
|
||||
// Scanner advances the scanner to the next variable
|
||||
func (scanner *Scanner) Scan() bool {
|
||||
for scanner.src.Scan() {
|
||||
// check that we don't have an empty or comment only line
|
||||
tokens := strings.TrimSpace(scanner.src.Text())
|
||||
if len(tokens) == 0 || tokens[0] == '#' || strings.HasPrefix(tokens, "//") {
|
||||
continue
|
||||
}
|
||||
|
||||
// check that we have a 'key=value' pair
|
||||
values := strings.SplitN(tokens, "=", 2)
|
||||
if len(values) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
// got a key = value
|
||||
scanner.key = strings.TrimSpace(values[0])
|
||||
scanner.value = strings.TrimSpace(values[1])
|
||||
return true
|
||||
}
|
||||
scanner.key = ""
|
||||
scanner.value = ""
|
||||
return false
|
||||
}
|
||||
|
||||
// Data reads the current value from the scanner.
|
||||
// When Scan() has not been called, or returned false, returns two empty strings.
|
||||
func (scanner Scanner) Data() (key, value string) {
|
||||
return scanner.key, scanner.value
|
||||
}
|
||||
|
||||
// Error returns an error (if any)
|
||||
func (scanner Scanner) Error() error {
|
||||
return scanner.src.Err()
|
||||
}
|
||||
|
||||
// ReadAll reads all key-value pairs from r.
|
||||
// If a key occurs more than once, a later occurance overwrites a previous one.
|
||||
func ReadAll(r io.Reader) (values map[string]string, err error) {
|
||||
scanner := NewScanner(r)
|
||||
|
||||
// read and store all values
|
||||
values = make(map[string]string)
|
||||
for scanner.Scan() {
|
||||
key, value := scanner.Data()
|
||||
values[key] = value
|
||||
}
|
||||
|
||||
// check if there was an error!
|
||||
if err := scanner.Error(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
105
internal/config/validators.go
Normal file
105
internal/config/validators.go
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/FAU-CDI/wisski-distillery/internal/fsx"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Validator reads from the configuration file
|
||||
type Validator func(s string) (interface{}, error)
|
||||
|
||||
var knownValidators map[string]Validator = map[string]Validator{
|
||||
"is_valid_abspath": IsValidAbspath,
|
||||
"is_valid_domain": IsValidDomain,
|
||||
"is_valid_domains": IsValidDomains,
|
||||
"is_valid_number": IsValidNumber,
|
||||
"is_valid_https_url": IsValidHttpsURL,
|
||||
"is_valid_slug": IsValidSlug,
|
||||
"is_valid_file": IsValidFile,
|
||||
"is_valid_email": IsValidEmail,
|
||||
"is_nonempty": IsNonEmpty,
|
||||
}
|
||||
|
||||
func IsValidAbspath(s string) (interface{}, error) {
|
||||
if !fsx.IsDirectory(s) {
|
||||
return nil, errors.Errorf("%q does not exist or is not a directory", s)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func IsValidFile(s string) (interface{}, error) {
|
||||
if !fsx.IsFile(s) {
|
||||
return nil, errors.Errorf("%q does not exist or is not a regular file", s)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func IsNonEmpty(s string) (interface{}, error) {
|
||||
if s == "" {
|
||||
return nil, errors.New("value is empty")
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
var regexpDomain = regexp.MustCompile(`^([a-zA-Z0-9][-a-zA-Z0-9]*\.)*[a-zA-Z0-9][-a-zA-Z0-9]*$`) // TODO: Make this regexp nicer!
|
||||
|
||||
func IsValidDomain(s string) (interface{}, error) {
|
||||
if !regexpDomain.MatchString(s) {
|
||||
return nil, errors.Errorf("%q is not a valid domain", s)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
func IsValidDomains(s string) (interface{}, error) {
|
||||
if len(s) == 0 {
|
||||
return []string{}, nil
|
||||
}
|
||||
domains := strings.Split(s, ",")
|
||||
for _, d := range domains {
|
||||
if !regexpDomain.MatchString(d) {
|
||||
return nil, errors.Errorf("%q is not a valid domain", d)
|
||||
}
|
||||
}
|
||||
return domains, nil
|
||||
}
|
||||
|
||||
func IsValidNumber(s string) (interface{}, error) {
|
||||
value, err := strconv.ParseInt(s, 10, 64)
|
||||
return int(value), err
|
||||
}
|
||||
|
||||
func IsValidHttpsURL(s string) (interface{}, error) {
|
||||
url, err := url.Parse(s)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "%q is not a valid URL", s)
|
||||
}
|
||||
if url.Scheme != "https" {
|
||||
return nil, errors.Errorf("%q is not a valid https URL (%q)", s, url.Scheme)
|
||||
}
|
||||
return url, nil
|
||||
}
|
||||
|
||||
var regexpEmail = regexp.MustCompile(`^([-a-zA-Z0-9]+)\@([a-zA-Z0-9][-a-zA-Z0-9]*\.)*[a-zA-Z0-9][-a-zA-Z0-9]*$`) // TODO: Make this regexp nicer!
|
||||
|
||||
func IsValidEmail(s string) (interface{}, error) {
|
||||
if s == "" { // no email provided
|
||||
return "", nil
|
||||
}
|
||||
if !regexpEmail.MatchString(s) {
|
||||
return nil, errors.Errorf("%q is not a valid email", s)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
var regexpSlug = regexp.MustCompile(`^[a-zA-Z0-9][-a-zA-Z0-9]*$`) // TODO: Make this regexp nicer!
|
||||
|
||||
func IsValidSlug(s string) (interface{}, error) {
|
||||
if !regexpSlug.MatchString(s) {
|
||||
return nil, errors.Errorf("%q is not a valid slug", s)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue