2021-12-19 17:30:51 +01:00

190 lines
5.0 KiB
Go

package config
import (
"fmt"
"github.com/BurntSushi/toml"
"os"
"path/filepath"
"reflect"
"strconv"
"strings"
)
var globConfig *Config
type Config struct {
Profile struct {
Dir string `toml:"Dir"`
Default struct {
Password string `toml:"Password"`
NetworkMode int `toml:"NetworkMode"`
Configurable bool `toml:"Configurable"`
RunLevel int `toml:"RunLevel"`
StartupInformation bool `toml:"StartupInformation"`
ExitAfter string `toml:"ExitAfter"`
KeepOnExit bool `toml:"KeepOnExit"`
} `toml:"default"`
Dynamic struct {
Enable bool `toml:"Enable"`
Password string `toml:"Password"`
NetworkMode int `toml:"NetworkMode"`
Configurable bool `toml:"Configurable"`
RunLevel int `toml:"RunLevel"`
StartupInformation bool `toml:"StartupInformation"`
ExitAfter string `toml:"ExitAfter"`
KeepOnExit bool `toml:"KeepOnExit"`
} `toml:"dynamic"`
} `toml:"profile"`
Api struct {
Port uint16 `toml:"Port"`
Configure struct {
Binary string `toml:"Binary"`
Man string `toml:"Man"`
} `toml:"configure"`
} `toml:"api"`
SSH struct {
Port uint16 `toml:"Port"`
Keyfile string `toml:"Keyfile"`
Passphrase string `toml:"Passphrase"`
} `toml:"ssh"`
Database struct {
Sqlite3File string `toml:"Sqlite3File"`
} `toml:"Database"`
Network struct {
Default struct {
Subnet string `toml:"Subnet"`
} `toml:"default"`
Isolate struct {
Subnet string `toml:"Subnet"`
} `toml:"isolate"`
} `toml:"network"`
Logging struct {
Level string `toml:"Level"`
OutputFile string `toml:"OutputFile"`
ErrorFile string `toml:"ErrorFile"`
ConsoleOutput bool `toml:"ConsoleOutput"`
ConsoleError bool `toml:"ConsoleError"`
} `toml:"logging"`
}
func InitConfig(includeEnv bool) (*Config, error) {
configFiles := []string{
"./docker4ssh.conf",
"~/.docker4ssh",
"~/.config/docker4ssh.conf",
"/etc/docker4ssh/docker4ssh.conf",
}
for _, file := range configFiles {
if _, err := os.Stat(file); !os.IsNotExist(err) {
return LoadConfig(file, includeEnv)
}
}
return nil, fmt.Errorf("no speicfied config file (%s) could be found", strings.Join(configFiles, ", "))
}
func LoadConfig(file string, includeEnv bool) (*Config, error) {
config := &Config{}
if _, err := toml.DecodeFile(file, config); err != nil {
return nil, err
}
// make paths absolute
dir := filepath.Dir(file)
config.Profile.Dir = absoluteFile(dir, config.Profile.Dir)
config.Api.Configure.Binary = absoluteFile(dir, config.Api.Configure.Binary)
config.Api.Configure.Man = absoluteFile(dir, config.Api.Configure.Man)
config.SSH.Keyfile = absoluteFile(dir, config.SSH.Keyfile)
config.Database.Sqlite3File = absoluteFile(dir, config.Database.Sqlite3File)
config.Logging.OutputFile = absoluteFile(dir, config.Logging.OutputFile)
config.Logging.ErrorFile = absoluteFile(dir, config.Logging.ErrorFile)
if includeEnv {
if err := updateFromEnv(config); err != nil {
return nil, err
}
}
return config, nil
}
func absoluteFile(path, file string) string {
if filepath.IsAbs(file) {
return file
}
return filepath.Join(path, file)
}
// updateFromEnv looks up if specific environment variable are given which can
// also be used to configure the program.
// Every key in the config file can also be specified via environment variables.
// The env variable syntax is SECTION_KEY -> e.g. DEFAULT_PASSWORD or API_PORT
func updateFromEnv(config *Config) error {
re := reflect.ValueOf(config).Elem()
rt := re.Type()
for i := 0; i < re.NumField(); i++ {
rf := re.Field(i)
ree := rt.Field(i)
if err := envParseField(strings.ToUpper(ree.Tag.Get("toml")), rf); err != nil {
return err
}
}
return nil
}
func envParseField(prefix string, value reflect.Value) error {
for j := 0; j < value.NumField(); j++ {
rtt := value.Type().Field(j)
rff := value.Field(j)
if rff.Kind() == reflect.Struct {
if err := envParseField(fmt.Sprintf("%s_%s", prefix, strings.ToUpper(rtt.Tag.Get("toml"))), rff); err != nil {
return err
}
continue
}
envName := fmt.Sprintf("%s_%s", prefix, strings.ToUpper(rtt.Tag.Get("toml")))
val, ok := os.LookupEnv(envName)
if !ok {
continue
}
var expected string
switch rff.Kind() {
case reflect.String:
rff.SetString(val)
continue
case reflect.Bool:
b, err := strconv.ParseBool(val)
if err == nil {
rff.SetBool(b)
continue
}
expected = "true / false (boolean)"
case reflect.Uint16:
ui, err := strconv.ParseUint(val, 10, 16)
if err == nil {
rff.SetUint(ui)
continue
}
expected = "number (uint16)"
default:
return fmt.Errorf("parsed not implemented config type '%s'", rff.Kind())
}
return fmt.Errorf("failed to parse environment variable '%s': cannot parse value '%s' as %s", envName, val, expected)
}
return nil
}
func GetConfig() *Config {
return globConfig
}
func SetConfig(config *Config) {
globConfig = config
}