diff --git a/cmd/config_migrate.go b/cmd/config_migrate.go deleted file mode 100644 index b2bd47d..0000000 --- a/cmd/config_migrate.go +++ /dev/null @@ -1,63 +0,0 @@ -package cmd - -import ( - "fmt" - "os" - - 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" -) - -// 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 { - // open the legacy file - file, err := os.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 - // migrate the legacy config - if err := legacy.Migrate(&cfg, file); err != nil { - return err - } - - // validate it! - if err := cfg.Validate(); err != nil { - return err - } - - // marshal the config - bytes, err := config.Marshal(&cfg, nil) - if err != nil { - return err - } - - // and print it! - fmt.Println(string(bytes)) - return nil -} diff --git a/cmd/wdcli/main.go b/cmd/wdcli/main.go index f9d8024..6305517 100644 --- a/cmd/wdcli/main.go +++ b/cmd/wdcli/main.go @@ -18,7 +18,6 @@ var wdcli = wisski_distillery.NewProgram() func init() { // self commands wdcli.Register(cmd.Config) - wdcli.Register(cmd.ConfigMigrate) wdcli.Register(cmd.License) // setup commands diff --git a/internal/config/legacy/envreader/envreader.go b/internal/config/legacy/envreader/envreader.go deleted file mode 100644 index ca7e69b..0000000 --- a/internal/config/legacy/envreader/envreader.go +++ /dev/null @@ -1,121 +0,0 @@ -// Package envreader provides Scanner. -// It is deprecated and will be removed in a future release. -package envreader - -import ( - "bufio" - "io" - "strings" -) - -// Scanner is a scanner for environment files. -// To create a new scanner use [NewScanner]. -// -// It scans through a reader and reads environment variables from it. -// Reads may be internally buffered. -// -// An environment variable is of the form: -// -// KEY=VALUE -// -// on a separate line. -// Keys and values are case-sensitive and may contain anything except for newline characters. -// Spaces around key and value are trimmed using [strings.TrimSpace]. -// Keys may not contain an '='. -// Lines not containing a '=' (e.g. blank lines) and those starting with '#' and '//' are ignored. -// -// To advance the scanner to the next key, value pair use [Scan]. -// To get the current (key, value) pair, use [Data]. -// -// A typical use-case of a scanner is as follows: -// -// scanner := NewScanner(r) -// for scanner.Scan() { -// // process any data .... -// fmt.Println(scanner.Data()) -// } -// if err := scanner.Err(); err != nil { -// // handle errors -// } -// -// For the common use case of reading a set of distinct keys from a file see [ReadAll]. -type Scanner struct { - s *bufio.Scanner - - // current key and value - key string - value string -} - -// NewScanner creates a new scanner from the underlying Reader -func NewScanner(r io.Reader) *Scanner { - return &Scanner{ - s: bufio.NewScanner(r), - } -} - -// Scanner advances the scanner until the next KEY=VALUE pair. -// -// If there are no more values left (e.g. the underlying reader returned io.EOF) -// or when an unexpected error occured, returns false. -// -// A caller should always check Err() to see if there was an error. -func (scanner *Scanner) Scan() bool { - var found bool - for scanner.s.Scan() { - // check that we don't have an empty or comment only line - tokens := strings.TrimSpace(scanner.s.Text()) - if len(tokens) == 0 || tokens[0] == '#' || strings.HasPrefix(tokens, "//") { - continue - } - - // check that we have a 'key=value' pair - scanner.key, scanner.value, found = strings.Cut(tokens, "=") - if !found { - continue - } - - // got a key = value - scanner.key = strings.TrimSpace(scanner.key) - scanner.value = strings.TrimSpace(scanner.value) - return true - } - - // nothing found - scanner.key = "" - scanner.value = "" - return false -} - -// Data reads the current value from the scanner. -// When Scan() has not been called, or returned false, returns two empty strings. -func (scanner Scanner) Data() (key, value string) { - return scanner.key, scanner.value -} - -// Err returns any error that occured on the underlying read. -// -// When no error occured, or the underlying read is io.EOF, returns nil. -func (scanner Scanner) Err() error { - return scanner.s.Err() -} - -// ReadAll creates a new [Scanner], and then reads all key/value pairs from r. -// If a key occurs more than once, only the last value is set in the returned map. -func ReadAll(r io.Reader) (values map[string]string, err error) { - // TODO: This is no longer used - scanner := NewScanner(r) - - // read and store all values - values = make(map[string]string) - for scanner.Scan() { - key, value := scanner.Data() - values[key] = value - } - - // check if there was an error! - if err := scanner.Err(); err != nil { - return nil, err - } - return values, nil -} diff --git a/internal/config/legacy/envreader/envreader_test.go b/internal/config/legacy/envreader/envreader_test.go deleted file mode 100644 index bb9aeb2..0000000 --- a/internal/config/legacy/envreader/envreader_test.go +++ /dev/null @@ -1,42 +0,0 @@ -// Package envreader -package envreader - -import ( - "fmt" - "strings" -) - -func ExampleNewScanner() { - scanner := NewScanner(strings.NewReader(` -lines without an equal sign are ignored - -// this line is a comment, even with an = sign -KEY=VALUE - -# this is also a comment = -spaces in keys = spaces in values -multiple=equal=signs -CaSe = SenSitiVe -empty value= -=empty key -`)) - - for scanner.Scan() { - key, value := scanner.Data() - fmt.Printf("%q %q\n", key, value) - } - - if err := scanner.Err(); err != nil { - fmt.Println(scanner.Err()) - } else { - fmt.Println("no error") - } - - // Output: "KEY" "VALUE" - // "spaces in keys" "spaces in values" - // "multiple" "equal=signs" - // "CaSe" "SenSitiVe" - // "empty value" "" - // "" "empty key" - // no error -} diff --git a/internal/config/legacy/legacy.go b/internal/config/legacy/legacy.go deleted file mode 100644 index eb34ade..0000000 --- a/internal/config/legacy/legacy.go +++ /dev/null @@ -1,150 +0,0 @@ -// 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/pkg/errors" -) - -// Migrate parses a configuration from an old configuration. -func Migrate(config *config.Config, src io.Reader) error { - var legacy Legacy - if err := legacy.Unmarshal(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.Listen.Ports = []uint16{80, legacy.PublicSSHPort} - if legacy.CertbotEmail != "" { - cfg.Listen.Ports = append(cfg.Listen.Ports, 443) - } - cfg.Listen.SSHPort = 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(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(tParser, value, vField); err != nil { - return errors.Errorf("Legacy.Unmarshal: Setting %q, Parser %q: %s", tEnv, tParser, err) - } - } - - return nil -} diff --git a/internal/config/legacy/stringparser/parse.go b/internal/config/legacy/stringparser/parse.go deleted file mode 100644 index 92b8c73..0000000 --- a/internal/config/legacy/stringparser/parse.go +++ /dev/null @@ -1,64 +0,0 @@ -package stringparser - -import ( - "reflect" - "strings" - - "github.com/pkg/errors" -) - -var errUnknownParser = errors.New("unknown parser") - -// Parse parses the provided value with the parser. -func Parse(name, value string, vField reflect.Value) error { - - // use the validator - parser, ok := knownParsers[strings.ToLower(name)] - if parser == nil || !ok { - return errUnknownParser - } - - // get the parsed value - checked, err := parser(value) - if err != nil { - return err - } - - // set the value of the field - var errSet interface{} - func() { - defer func() { - errSet = recover() - }() - vField.Set(reflect.ValueOf(checked)) - }() - - // capture any error - if errSet != nil { - return errors.Errorf("set returned %v", name, errSet) - } - - return nil -} - -// knownParsers holds the known parsers -var knownParsers map[string]Parser[any] = map[string]Parser[any]{ - "abspath": asGenericParser(ParseAbspath), - "domain": asGenericParser(ParseValidDomain), - "domains": asGenericParser(ParseValidDomains), - "duration": asGenericParser(ParseDuration), - "number": asGenericParser(ParseNumber), - "port": asGenericParser(ParsePort), - "https_url": asGenericParser(ParseHttpsURL), - "slug": asGenericParser(ParseSlug), - "file": asGenericParser(ParseFile), - "email": asGenericParser(ParseEmail), - "nonempty": asGenericParser(ParseNonEmpty), -} - -func asGenericParser[T any](parser Parser[T]) Parser[any] { - return func(s string) (value any, err error) { - value, err = parser(s) - return - } -} diff --git a/internal/config/legacy/stringparser/stringparser.go b/internal/config/legacy/stringparser/stringparser.go deleted file mode 100644 index c3dee39..0000000 --- a/internal/config/legacy/stringparser/stringparser.go +++ /dev/null @@ -1,124 +0,0 @@ -// Package stringparser provides Parser. -// It is deprecated and will be removed in a future release. -package stringparser - -import ( - "net/url" - "regexp" - "strconv" - "strings" - "time" - - "github.com/FAU-CDI/wisski-distillery/pkg/fsx" - "github.com/pkg/errors" -) - -// Parser is used to read a value from a string and turn it into a golang value. -// It is simultaniously used to validate particular setting. -// -// Parsers can be found in this package as functions called Parse*. -// They are refered to by their name, e.g. ParseNonempty can be refered to by the name 'Nonempty'. -// See [Parse]. -type Parser[T any] func(s string) (T, error) - -// ParseAbspath checks that s is an absolute path and returns it as-is -func ParseAbspath(s string) (string, error) { - if !fsx.IsDirectory(s) { - return "", errors.Errorf("%q does not exist or is not a directory", s) - } - return s, nil -} - -// ParseFile checks that s is a valid file and returns it as-is -func ParseFile(s string) (string, error) { - if !fsx.IsFile(s) { - return "", errors.Errorf("%q does not exist or is not a regular file", s) - } - return s, nil -} - -var errEmptyString = errors.New("value is empty") - -// ParseNonEmpty checks that s is a non-empty string and returns it as-is -func ParseNonEmpty(s string) (string, error) { - if s == "" { - return "", errEmptyString - } - return s, nil -} - -var regexpDomain = regexp.MustCompile(`^([a-zA-Z0-9][-a-zA-Z0-9]*\.)*[a-zA-Z0-9][-a-zA-Z0-9]*$`) // TODO: Make this regexp nicer! - -// ParseValidDomain checks that s is a valid domain and returns it in lowercase -func ParseValidDomain(s string) (string, error) { - if !regexpDomain.MatchString(s) { - return "", errors.Errorf("%q is not a valid domain", s) - } - return strings.ToLower(s), nil -} - -// ParseValidDomains checks that s is a comma-seperated list of valid domains and returns them in lower case -func ParseValidDomains(s string) ([]string, error) { - if len(s) == 0 { - return []string{}, nil - } - domains := strings.Split(strings.ToLower(s), ",") - for _, d := range domains { - if !regexpDomain.MatchString(d) { - return nil, errors.Errorf("%q is not a valid domain", d) - } - } - return domains, nil -} - -// ParseNumber parses s as a decimal integer -func ParseNumber(s string) (int, error) { - value, err := strconv.ParseInt(s, 10, 64) - return int(value), err -} - -// ParsePort parses s as a port -func ParsePort(s string) (uint16, error) { - value, err := strconv.ParseUint(s, 10, 16) - return uint16(value), err -} - -// ParseHttpsURL parses a string into a url that starts with 'https://' -func ParseHttpsURL(s string) (*url.URL, error) { - url, err := url.Parse(s) - if err != nil { - return nil, errors.Wrapf(err, "%q is not a valid URL", s) - } - if url.Scheme != "https" { - return nil, errors.Errorf("%q is not a valid https URL (%q)", s, url.Scheme) - } - return url, nil -} - -var regexpEmail = regexp.MustCompile(`^([-a-zA-Z0-9]+)\@([a-zA-Z0-9][-a-zA-Z0-9]*\.)*[a-zA-Z0-9][-a-zA-Z0-9]*$`) // TODO: Make this regexp nicer! - -// ParseEmail checks that s represents an email, and then returns it as is. -func ParseEmail(s string) (string, error) { - if s == "" { // no email provided - return "", nil - } - if !regexpEmail.MatchString(s) { - return "", errors.Errorf("%q is not a valid email", s) - } - return s, nil -} - -var regexpSlug = regexp.MustCompile(`^[a-zA-Z0-9][-a-zA-Z0-9]*$`) // TODO: Make this regexp nicer! - -// ParseSlug parses s as a slug and returns it as is. -func ParseSlug(s string) (string, error) { - if !regexpSlug.MatchString(s) { - return "", errors.Errorf("%q is not a valid slug", s) - } - return s, nil -} - -// ParseDuration parses a time.Duration -func ParseDuration(s string) (time.Duration, error) { - return time.ParseDuration(s) -}