mirror of
https://github.com/ergochat/ergo.git
synced 2024-11-11 06:29:29 +01:00
Merge pull request #1352 from slingamn/environment_variables.1
fix #1049
This commit is contained in:
commit
94e7cfc080
@ -922,3 +922,7 @@ history:
|
|||||||
#blacklist:
|
#blacklist:
|
||||||
# - "+draft/typing"
|
# - "+draft/typing"
|
||||||
# - "typing"
|
# - "typing"
|
||||||
|
|
||||||
|
# whether to allow customization of the config at runtime using environment variables,
|
||||||
|
# e.g., ORAGONO__SERVER__MAX_SENDQ=128k. see the manual for more details.
|
||||||
|
allow-environment-overrides: true
|
||||||
|
117
irc/config.go
117
irc/config.go
@ -15,6 +15,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -478,6 +479,8 @@ type TorListenersConfig struct {
|
|||||||
|
|
||||||
// Config defines the overall configuration.
|
// Config defines the overall configuration.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
AllowEnvironmentOverrides bool `yaml:"allow-environment-overrides"`
|
||||||
|
|
||||||
Network struct {
|
Network struct {
|
||||||
Name string
|
Name string
|
||||||
}
|
}
|
||||||
@ -870,6 +873,105 @@ func LoadRawConfig(filename string) (config *Config, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// convert, e.g., "ALLOWED_ORIGINS" to "allowed-origins"
|
||||||
|
func screamingSnakeToKebab(in string) (out string) {
|
||||||
|
var buf strings.Builder
|
||||||
|
for i := 0; i < len(in); i++ {
|
||||||
|
c := in[i]
|
||||||
|
switch {
|
||||||
|
case c == '_':
|
||||||
|
buf.WriteByte('-')
|
||||||
|
case 'A' <= c && c <= 'Z':
|
||||||
|
buf.WriteByte(c + ('a' - 'A'))
|
||||||
|
default:
|
||||||
|
buf.WriteByte(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func isExported(field reflect.StructField) bool {
|
||||||
|
return field.PkgPath == "" // https://golang.org/pkg/reflect/#StructField
|
||||||
|
}
|
||||||
|
|
||||||
|
// errors caused by config overrides
|
||||||
|
type configPathError struct {
|
||||||
|
name string
|
||||||
|
desc string
|
||||||
|
fatalErr error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ce *configPathError) Error() string {
|
||||||
|
if ce.fatalErr != nil {
|
||||||
|
return fmt.Sprintf("Couldn't apply config override `%s`: %s: %v", ce.name, ce.desc, ce.fatalErr)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("Couldn't apply config override `%s`: %s", ce.name, ce.desc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mungeFromEnvironment(config *Config, envPair string) (applied bool, err *configPathError) {
|
||||||
|
equalIdx := strings.IndexByte(envPair, '=')
|
||||||
|
name, value := envPair[:equalIdx], envPair[equalIdx+1:]
|
||||||
|
if !strings.HasPrefix(name, "ORAGONO__") {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
name = strings.TrimPrefix(name, "ORAGONO__")
|
||||||
|
pathComponents := strings.Split(name, "__")
|
||||||
|
for i, pathComponent := range pathComponents {
|
||||||
|
pathComponents[i] = screamingSnakeToKebab(pathComponent)
|
||||||
|
}
|
||||||
|
|
||||||
|
v := reflect.Indirect(reflect.ValueOf(config))
|
||||||
|
t := v.Type()
|
||||||
|
for _, component := range pathComponents {
|
||||||
|
if component == "" {
|
||||||
|
return false, &configPathError{name, "invalid", nil}
|
||||||
|
}
|
||||||
|
if v.Kind() != reflect.Struct {
|
||||||
|
return false, &configPathError{name, "index into non-struct", nil}
|
||||||
|
}
|
||||||
|
var nextField reflect.StructField
|
||||||
|
success := false
|
||||||
|
n := t.NumField()
|
||||||
|
// preferentially get a field with an exact yaml tag match,
|
||||||
|
// then fall back to case-insensitive comparison of field names
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
field := t.Field(i)
|
||||||
|
if isExported(field) && field.Tag.Get("yaml") == component {
|
||||||
|
nextField = field
|
||||||
|
success = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !success {
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
field := t.Field(i)
|
||||||
|
if isExported(field) && strings.ToLower(field.Name) == component {
|
||||||
|
nextField = field
|
||||||
|
success = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !success {
|
||||||
|
return false, &configPathError{name, fmt.Sprintf("couldn't resolve path component: `%s`", component), nil}
|
||||||
|
}
|
||||||
|
v = v.FieldByName(nextField.Name)
|
||||||
|
// dereference pointer field if necessary, initialize new value if necessary
|
||||||
|
if v.Kind() == reflect.Ptr {
|
||||||
|
if v.IsNil() {
|
||||||
|
v.Set(reflect.New(v.Type().Elem()))
|
||||||
|
}
|
||||||
|
v = reflect.Indirect(v)
|
||||||
|
}
|
||||||
|
t = v.Type()
|
||||||
|
}
|
||||||
|
yamlErr := yaml.Unmarshal([]byte(value), v.Addr().Interface())
|
||||||
|
if yamlErr != nil {
|
||||||
|
return false, &configPathError{name, "couldn't deserialize YAML", yamlErr}
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
// LoadConfig loads the given YAML configuration file.
|
// LoadConfig loads the given YAML configuration file.
|
||||||
func LoadConfig(filename string) (config *Config, err error) {
|
func LoadConfig(filename string) (config *Config, err error) {
|
||||||
config, err = LoadRawConfig(filename)
|
config, err = LoadRawConfig(filename)
|
||||||
@ -877,6 +979,21 @@ func LoadConfig(filename string) (config *Config, err error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if config.AllowEnvironmentOverrides {
|
||||||
|
for _, envPair := range os.Environ() {
|
||||||
|
applied, envErr := mungeFromEnvironment(config, envPair)
|
||||||
|
if envErr != nil {
|
||||||
|
if envErr.fatalErr != nil {
|
||||||
|
return nil, envErr
|
||||||
|
} else {
|
||||||
|
log.Println(envErr.Error())
|
||||||
|
}
|
||||||
|
} else if applied {
|
||||||
|
log.Printf("applied environment override: %s\n", envPair)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
config.Filename = filename
|
config.Filename = filename
|
||||||
|
|
||||||
if config.Network.Name == "" {
|
if config.Network.Name == "" {
|
||||||
|
101
irc/config_test.go
Normal file
101
irc/config_test.go
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
// Copyright (c) 2020 Shivaram Lingamneni
|
||||||
|
// released under the MIT license
|
||||||
|
|
||||||
|
package irc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEnvironmentOverrides(t *testing.T) {
|
||||||
|
var config Config
|
||||||
|
config.Server.Compatibility.SendUnprefixedSasl = true
|
||||||
|
config.History.Enabled = true
|
||||||
|
defaultUserModes := "+i"
|
||||||
|
config.Accounts.DefaultUserModes = &defaultUserModes
|
||||||
|
config.Server.WebSockets.AllowedOrigins = []string{"https://www.ircv3.net"}
|
||||||
|
config.Server.MOTD = "long.motd.txt" // overwrite this
|
||||||
|
env := []string{
|
||||||
|
`USER=shivaram`, // unrelated var
|
||||||
|
`ORAGONO_USER=oragono`, // this should be ignored as well
|
||||||
|
`ORAGONO__NETWORK__NAME=example.com`,
|
||||||
|
`ORAGONO__SERVER__COMPATIBILITY__FORCE_TRAILING=false`,
|
||||||
|
`ORAGONO__SERVER__COERCE_IDENT="~user"`,
|
||||||
|
`ORAGONO__SERVER__MOTD=short.motd.txt`,
|
||||||
|
`ORAGONO__ACCOUNTS__NICK_RESERVATION__ENABLED=true`,
|
||||||
|
`ORAGONO__ACCOUNTS__DEFAULT_USER_MODES="+iR"`,
|
||||||
|
`ORAGONO__SERVER__IP_CLOAKING={"enabled": true, "enabled-for-always-on": true, "netname": "irc", "cidr-len-ipv4": 32, "cidr-len-ipv6": 64, "num-bits": 64}`,
|
||||||
|
}
|
||||||
|
for _, envPair := range env {
|
||||||
|
_, err := mungeFromEnvironment(&config, envPair)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("couldn't apply override `%s`: %v", envPair, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Network.Name != "example.com" {
|
||||||
|
t.Errorf("unexpected value of network.name: %s", config.Network.Name)
|
||||||
|
}
|
||||||
|
if config.Server.CoerceIdent != "~user" {
|
||||||
|
t.Errorf("unexpected value of coerce-ident: %s", config.Server.CoerceIdent)
|
||||||
|
}
|
||||||
|
if config.Server.MOTD != "short.motd.txt" {
|
||||||
|
t.Errorf("unexpected value of motd: %s", config.Server.MOTD)
|
||||||
|
}
|
||||||
|
if !config.Accounts.NickReservation.Enabled {
|
||||||
|
t.Errorf("did not set bool as expected")
|
||||||
|
}
|
||||||
|
if !config.Server.Compatibility.SendUnprefixedSasl {
|
||||||
|
t.Errorf("overwrote unrelated field")
|
||||||
|
}
|
||||||
|
if !config.History.Enabled {
|
||||||
|
t.Errorf("overwrote unrelated field")
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(config.Server.WebSockets.AllowedOrigins, []string{"https://www.ircv3.net"}) {
|
||||||
|
t.Errorf("overwrote unrelated field: %#v", config.Server.WebSockets.AllowedOrigins)
|
||||||
|
}
|
||||||
|
|
||||||
|
cloakConf := config.Server.Cloaks
|
||||||
|
if !(cloakConf.Enabled == true && cloakConf.EnabledForAlwaysOn == true && cloakConf.Netname == "irc" && cloakConf.CidrLenIPv6 == 64) {
|
||||||
|
t.Errorf("bad value of Cloaks: %#v", config.Server.Cloaks)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *config.Server.Compatibility.ForceTrailing != false {
|
||||||
|
t.Errorf("couldn't set unset ptr field to false")
|
||||||
|
}
|
||||||
|
|
||||||
|
if *config.Accounts.DefaultUserModes != "+iR" {
|
||||||
|
t.Errorf("couldn't override pre-set ptr field")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEnvironmentOverrideErrors(t *testing.T) {
|
||||||
|
var config Config
|
||||||
|
config.Server.Compatibility.SendUnprefixedSasl = true
|
||||||
|
config.History.Enabled = true
|
||||||
|
|
||||||
|
invalidEnvs := []string{
|
||||||
|
`ORAGONO__=asdf`,
|
||||||
|
`ORAGONO__SERVER__=asdf`,
|
||||||
|
`ORAGONO__SERVER____=asdf`,
|
||||||
|
`ORAGONO__NONEXISTENT_KEY=1`,
|
||||||
|
`ORAGONO__SERVER__NONEXISTENT_KEY=1`,
|
||||||
|
// invalid yaml:
|
||||||
|
`ORAGONO__SERVER__IP_CLOAKING__NETNAME="`,
|
||||||
|
// invalid type:
|
||||||
|
`ORAGONO__SERVER__IP_CLOAKING__NUM_BITS=asdf`,
|
||||||
|
`ORAGONO__SERVER__STS=[]`,
|
||||||
|
// index into non-struct:
|
||||||
|
`ORAGONO__NETWORK__NAME__QUX=1`,
|
||||||
|
// private field:
|
||||||
|
`ORAGONO__SERVER__PASSWORDBYTES="asdf"`,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, env := range invalidEnvs {
|
||||||
|
success, err := mungeFromEnvironment(&config, env)
|
||||||
|
if err == nil || success {
|
||||||
|
t.Errorf("accepted invalid env override `%s`", env)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -895,3 +895,7 @@ history:
|
|||||||
#blacklist:
|
#blacklist:
|
||||||
# - "+draft/typing"
|
# - "+draft/typing"
|
||||||
# - "typing"
|
# - "typing"
|
||||||
|
|
||||||
|
# whether to allow customization of the config at runtime using environment variables,
|
||||||
|
# e.g., ORAGONO__SERVER__MAX_SENDQ=128k. see the manual for more details.
|
||||||
|
allow-environment-overrides: true
|
||||||
|
Loading…
Reference in New Issue
Block a user