internal: walk config in go, check, parse and load from scheme like in python
closes #2719 Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
parent
d08856c1fe
commit
10b48b27b0
|
@ -34,28 +34,15 @@ func main() {
|
||||||
})
|
})
|
||||||
go debug.EnableDebugServer()
|
go debug.EnableDebugServer()
|
||||||
l := log.WithField("logger", "authentik.root")
|
l := log.WithField("logger", "authentik.root")
|
||||||
config.DefaultConfig()
|
config.Get().Setup("./authentik/lib/default.yml", "./local.env.yml")
|
||||||
err := config.LoadConfig("./authentik/lib/default.yml")
|
|
||||||
if err != nil {
|
|
||||||
l.WithError(err).Warning("failed to load default config")
|
|
||||||
}
|
|
||||||
err = config.LoadConfig("./local.env.yml")
|
|
||||||
if err != nil {
|
|
||||||
l.WithError(err).Debug("no local config to load")
|
|
||||||
}
|
|
||||||
err = config.FromEnv()
|
|
||||||
if err != nil {
|
|
||||||
l.WithError(err).Debug("failed to environment variables")
|
|
||||||
}
|
|
||||||
config.ConfigureLogger()
|
|
||||||
|
|
||||||
if config.G.ErrorReporting.Enabled {
|
if config.Get().ErrorReporting.Enabled {
|
||||||
err := sentry.Init(sentry.ClientOptions{
|
err := sentry.Init(sentry.ClientOptions{
|
||||||
Dsn: config.G.ErrorReporting.DSN,
|
Dsn: config.Get().ErrorReporting.DSN,
|
||||||
AttachStacktrace: true,
|
AttachStacktrace: true,
|
||||||
TracesSampler: sentryutils.SamplerFunc(config.G.ErrorReporting.SampleRate),
|
TracesSampler: sentryutils.SamplerFunc(config.Get().ErrorReporting.SampleRate),
|
||||||
Release: fmt.Sprintf("authentik@%s", constants.VERSION),
|
Release: fmt.Sprintf("authentik@%s", constants.VERSION),
|
||||||
Environment: config.G.ErrorReporting.Environment,
|
Environment: config.Get().ErrorReporting.Environment,
|
||||||
HTTPTransport: webutils.NewUserAgentTransport(constants.UserAgent(), http.DefaultTransport),
|
HTTPTransport: webutils.NewUserAgentTransport(constants.UserAgent(), http.DefaultTransport),
|
||||||
IgnoreErrors: []string{
|
IgnoreErrors: []string{
|
||||||
http.ErrAbortHandler.Error(),
|
http.ErrAbortHandler.Error(),
|
||||||
|
@ -74,7 +61,7 @@ func main() {
|
||||||
g := gounicorn.NewGoUnicorn()
|
g := gounicorn.NewGoUnicorn()
|
||||||
ws := web.NewWebServer(g)
|
ws := web.NewWebServer(g)
|
||||||
g.HealthyCallback = func() {
|
g.HealthyCallback = func() {
|
||||||
if !config.G.Web.DisableEmbeddedOutpost {
|
if !config.Get().Web.DisableEmbeddedOutpost {
|
||||||
go attemptProxyStart(ws, u)
|
go attemptProxyStart(ws, u)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -105,7 +92,7 @@ func attemptProxyStart(ws *web.WebServer, u *url.URL) {
|
||||||
l := log.WithField("logger", "authentik.server")
|
l := log.WithField("logger", "authentik.server")
|
||||||
for {
|
for {
|
||||||
l.Debug("attempting to init outpost")
|
l.Debug("attempting to init outpost")
|
||||||
ac := ak.NewAPIController(*u, config.G.SecretKey)
|
ac := ak.NewAPIController(*u, config.Get().SecretKey)
|
||||||
if ac == nil {
|
if ac == nil {
|
||||||
attempt += 1
|
attempt += 1
|
||||||
time.Sleep(1 * time.Second)
|
time.Sleep(1 * time.Second)
|
||||||
|
|
|
@ -3,6 +3,9 @@ package config
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
env "github.com/Netflix/go-env"
|
env "github.com/Netflix/go-env"
|
||||||
|
@ -11,10 +14,17 @@ import (
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var G Config
|
var cfg *Config
|
||||||
|
|
||||||
func DefaultConfig() {
|
func Get() *Config {
|
||||||
G = Config{
|
if cfg == nil {
|
||||||
|
cfg = defaultConfig()
|
||||||
|
}
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultConfig() *Config {
|
||||||
|
return &Config{
|
||||||
Debug: false,
|
Debug: false,
|
||||||
Web: WebConfig{
|
Web: WebConfig{
|
||||||
Listen: "localhost:9000",
|
Listen: "localhost:9000",
|
||||||
|
@ -32,7 +42,18 @@ func DefaultConfig() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadConfig(path string) error {
|
func (c *Config) Setup(paths ...string) {
|
||||||
|
for _, path := range paths {
|
||||||
|
err := c.LoadConfig(path)
|
||||||
|
if err != nil {
|
||||||
|
log.WithError(err).Info("failed to load config, skipping")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.fromEnv()
|
||||||
|
c.configureLogger()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) LoadConfig(path string) error {
|
||||||
raw, err := ioutil.ReadFile(path)
|
raw, err := ioutil.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed to load config file: %w", err)
|
return fmt.Errorf("Failed to load config file: %w", err)
|
||||||
|
@ -42,28 +63,83 @@ func LoadConfig(path string) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed to parse YAML: %w", err)
|
return fmt.Errorf("Failed to parse YAML: %w", err)
|
||||||
}
|
}
|
||||||
if err := mergo.Merge(&G, nc, mergo.WithOverride); err != nil {
|
if err := mergo.Merge(c, nc, mergo.WithOverride); err != nil {
|
||||||
return fmt.Errorf("failed to overlay config: %w", err)
|
return fmt.Errorf("failed to overlay config: %w", err)
|
||||||
}
|
}
|
||||||
|
c.walkScheme(c)
|
||||||
log.WithField("path", path).Debug("Loaded config")
|
log.WithField("path", path).Debug("Loaded config")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func FromEnv() error {
|
func (c *Config) fromEnv() error {
|
||||||
nc := Config{}
|
nc := Config{}
|
||||||
_, err := env.UnmarshalFromEnviron(&nc)
|
_, err := env.UnmarshalFromEnviron(&nc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to load environment variables: %w", err)
|
return fmt.Errorf("failed to load environment variables: %w", err)
|
||||||
}
|
}
|
||||||
if err := mergo.Merge(&G, nc, mergo.WithOverride); err != nil {
|
if err := mergo.Merge(c, nc, mergo.WithOverride); err != nil {
|
||||||
return fmt.Errorf("failed to overlay config: %w", err)
|
return fmt.Errorf("failed to overlay config: %w", err)
|
||||||
}
|
}
|
||||||
|
c.walkScheme(c)
|
||||||
log.Debug("Loaded config from environment")
|
log.Debug("Loaded config from environment")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ConfigureLogger() {
|
func (c *Config) walkScheme(v interface{}) {
|
||||||
switch strings.ToLower(G.LogLevel) {
|
rv := reflect.ValueOf(v)
|
||||||
|
if rv.Kind() != reflect.Ptr || rv.IsNil() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rv = rv.Elem()
|
||||||
|
if rv.Kind() != reflect.Struct {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t := rv.Type()
|
||||||
|
for i := 0; i < rv.NumField(); i++ {
|
||||||
|
valueField := rv.Field(i)
|
||||||
|
switch valueField.Kind() {
|
||||||
|
case reflect.Struct:
|
||||||
|
if !valueField.Addr().CanInterface() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
iface := valueField.Addr().Interface()
|
||||||
|
c.walkScheme(iface)
|
||||||
|
}
|
||||||
|
|
||||||
|
typeField := t.Field(i)
|
||||||
|
if typeField.Type.Kind() != reflect.String {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
valueField.SetString(c.parseScheme(valueField.String()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) parseScheme(rawVal string) string {
|
||||||
|
u, err := url.Parse(rawVal)
|
||||||
|
if err != nil {
|
||||||
|
return rawVal
|
||||||
|
}
|
||||||
|
if u.Scheme == "env" {
|
||||||
|
e, ok := os.LookupEnv(u.Host)
|
||||||
|
if ok {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
return u.RawQuery
|
||||||
|
} else if u.Scheme == "file" {
|
||||||
|
d, err := ioutil.ReadFile(u.Path)
|
||||||
|
if err != nil {
|
||||||
|
return u.RawQuery
|
||||||
|
}
|
||||||
|
return string(d)
|
||||||
|
}
|
||||||
|
return rawVal
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) configureLogger() {
|
||||||
|
switch strings.ToLower(c.LogLevel) {
|
||||||
case "trace":
|
case "trace":
|
||||||
log.SetLevel(log.TraceLevel)
|
log.SetLevel(log.TraceLevel)
|
||||||
case "debug":
|
case "debug":
|
||||||
|
@ -83,7 +159,7 @@ func ConfigureLogger() {
|
||||||
log.FieldKeyTime: "timestamp",
|
log.FieldKeyTime: "timestamp",
|
||||||
}
|
}
|
||||||
|
|
||||||
if G.Debug {
|
if c.Debug {
|
||||||
log.SetFormatter(&log.TextFormatter{FieldMap: fm})
|
log.SetFormatter(&log.TextFormatter{FieldMap: fm})
|
||||||
} else {
|
} else {
|
||||||
log.SetFormatter(&log.JSONFormatter{FieldMap: fm, DisableHTMLEscape: true})
|
log.SetFormatter(&log.JSONFormatter{FieldMap: fm, DisableHTMLEscape: true})
|
||||||
|
|
39
internal/config/config_test.go
Normal file
39
internal/config/config_test.go
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConfigEnv(t *testing.T) {
|
||||||
|
os.Setenv("AUTHENTIK_SECRET_KEY", "bar")
|
||||||
|
cfg = nil
|
||||||
|
Get().fromEnv()
|
||||||
|
assert.Equal(t, "bar", Get().SecretKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigEnv_Scheme(t *testing.T) {
|
||||||
|
os.Setenv("foo", "bar")
|
||||||
|
os.Setenv("AUTHENTIK_SECRET_KEY", "env://foo")
|
||||||
|
cfg = nil
|
||||||
|
Get().fromEnv()
|
||||||
|
assert.Equal(t, "bar", Get().SecretKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigEnv_File(t *testing.T) {
|
||||||
|
file, err := os.CreateTemp("", "")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.Remove(file.Name())
|
||||||
|
file.Write([]byte("bar"))
|
||||||
|
|
||||||
|
os.Setenv("AUTHENTIK_SECRET_KEY", fmt.Sprintf("file://%s", file.Name()))
|
||||||
|
cfg = nil
|
||||||
|
Get().fromEnv()
|
||||||
|
assert.Equal(t, "bar", Get().SecretKey)
|
||||||
|
}
|
|
@ -39,7 +39,7 @@ func NewGoUnicorn() *GoUnicorn {
|
||||||
func (g *GoUnicorn) initCmd() {
|
func (g *GoUnicorn) initCmd() {
|
||||||
command := "gunicorn"
|
command := "gunicorn"
|
||||||
args := []string{"-c", "./lifecycle/gunicorn.conf.py", "authentik.root.asgi:application"}
|
args := []string{"-c", "./lifecycle/gunicorn.conf.py", "authentik.root.asgi:application"}
|
||||||
if config.G.Debug {
|
if config.Get().Debug {
|
||||||
command = "./manage.py"
|
command = "./manage.py"
|
||||||
args = []string{"runserver"}
|
args = []string{"runserver"}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,8 +15,8 @@ import (
|
||||||
|
|
||||||
func (a *Application) getStore(p api.ProxyOutpostConfig, externalHost *url.URL) sessions.Store {
|
func (a *Application) getStore(p api.ProxyOutpostConfig, externalHost *url.URL) sessions.Store {
|
||||||
var store sessions.Store
|
var store sessions.Store
|
||||||
if config.G.Redis.Host != "" {
|
if config.Get().Redis.Host != "" {
|
||||||
rs, err := redistore.NewRediStoreWithDB(10, "tcp", fmt.Sprintf("%s:%d", config.G.Redis.Host, config.G.Redis.Port), config.G.Redis.Password, strconv.Itoa(config.G.Redis.OutpostSessionDB), []byte(*p.CookieSecret))
|
rs, err := redistore.NewRediStoreWithDB(10, "tcp", fmt.Sprintf("%s:%d", config.Get().Redis.Host, config.Get().Redis.Port), config.Get().Redis.Password, strconv.Itoa(config.Get().Redis.OutpostSessionDB), []byte(*p.CookieSecret))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,7 @@ func RunMetricsServer() {
|
||||||
l.WithError(err).Warning("failed to get upstream metrics")
|
l.WithError(err).Warning("failed to get upstream metrics")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
re.SetBasicAuth("monitor", config.G.SecretKey)
|
re.SetBasicAuth("monitor", config.Get().SecretKey)
|
||||||
res, err := http.DefaultClient.Do(re)
|
res, err := http.DefaultClient.Do(re)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.WithError(err).Warning("failed to get upstream metrics")
|
l.WithError(err).Warning("failed to get upstream metrics")
|
||||||
|
@ -54,10 +54,10 @@ func RunMetricsServer() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
l.WithField("listen", config.G.Web.ListenMetrics).Info("Starting Metrics server")
|
l.WithField("listen", config.Get().Web.ListenMetrics).Info("Starting Metrics server")
|
||||||
err := http.ListenAndServe(config.G.Web.ListenMetrics, m)
|
err := http.ListenAndServe(config.Get().Web.ListenMetrics, m)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.WithError(err).Warning("Failed to start metrics server")
|
l.WithError(err).Warning("Failed to start metrics server")
|
||||||
}
|
}
|
||||||
l.WithField("listen", config.G.Web.ListenMetrics).Info("Stopping Metrics server")
|
l.WithField("listen", config.Get().Web.ListenMetrics).Info("Stopping Metrics server")
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ type SentryRequest struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ws *WebServer) APISentryProxy(rw http.ResponseWriter, r *http.Request) {
|
func (ws *WebServer) APISentryProxy(rw http.ResponseWriter, r *http.Request) {
|
||||||
if !config.G.ErrorReporting.Enabled {
|
if !config.Get().ErrorReporting.Enabled {
|
||||||
ws.log.Debug("error reporting disabled")
|
ws.log.Debug("error reporting disabled")
|
||||||
rw.WriteHeader(http.StatusBadRequest)
|
rw.WriteHeader(http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
|
@ -37,8 +37,8 @@ func (ws *WebServer) APISentryProxy(rw http.ResponseWriter, r *http.Request) {
|
||||||
rw.WriteHeader(http.StatusBadRequest)
|
rw.WriteHeader(http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if sd.DSN != config.G.ErrorReporting.DSN {
|
if sd.DSN != config.Get().ErrorReporting.DSN {
|
||||||
ws.log.WithField("have", sd.DSN).WithField("expected", config.G.ErrorReporting.DSN).Debug("invalid DSN")
|
ws.log.WithField("have", sd.DSN).WithField("expected", config.Get().ErrorReporting.DSN).Debug("invalid DSN")
|
||||||
rw.WriteHeader(http.StatusBadRequest)
|
rw.WriteHeader(http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ func (ws *WebServer) configureStatic() {
|
||||||
indexLessRouter := statRouter.NewRoute().Subrouter()
|
indexLessRouter := statRouter.NewRoute().Subrouter()
|
||||||
indexLessRouter.Use(web.DisableIndex)
|
indexLessRouter.Use(web.DisableIndex)
|
||||||
// Media files, always local
|
// Media files, always local
|
||||||
fs := http.FileServer(http.Dir(config.G.Paths.Media))
|
fs := http.FileServer(http.Dir(config.Get().Paths.Media))
|
||||||
distFs := http.FileServer(http.Dir("./web/dist"))
|
distFs := http.FileServer(http.Dir("./web/dist"))
|
||||||
distHandler := http.StripPrefix("/static/dist/", distFs)
|
distHandler := http.StripPrefix("/static/dist/", distFs)
|
||||||
authentikHandler := http.StripPrefix("/static/authentik/", http.FileServer(http.Dir("./web/authentik")))
|
authentikHandler := http.StripPrefix("/static/authentik/", http.FileServer(http.Dir("./web/authentik")))
|
||||||
|
|
|
@ -41,7 +41,7 @@ func (ws *WebServer) listenTLS() {
|
||||||
GetCertificate: ws.GetCertificate(),
|
GetCertificate: ws.GetCertificate(),
|
||||||
}
|
}
|
||||||
|
|
||||||
ln, err := net.Listen("tcp", config.G.Web.ListenTLS)
|
ln, err := net.Listen("tcp", config.Get().Web.ListenTLS)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ws.log.WithError(err).Fatalf("failed to listen (TLS)")
|
ws.log.WithError(err).Fatalf("failed to listen (TLS)")
|
||||||
return
|
return
|
||||||
|
@ -50,7 +50,7 @@ func (ws *WebServer) listenTLS() {
|
||||||
defer proxyListener.Close()
|
defer proxyListener.Close()
|
||||||
|
|
||||||
tlsListener := tls.NewListener(proxyListener, tlsConfig)
|
tlsListener := tls.NewListener(proxyListener, tlsConfig)
|
||||||
ws.log.WithField("listen", config.G.Web.ListenTLS).Info("Starting HTTPS server")
|
ws.log.WithField("listen", config.Get().Web.ListenTLS).Info("Starting HTTPS server")
|
||||||
ws.serve(tlsListener)
|
ws.serve(tlsListener)
|
||||||
ws.log.WithField("listen", config.G.Web.ListenTLS).Info("Stopping HTTPS server")
|
ws.log.WithField("listen", config.Get().Web.ListenTLS).Info("Stopping HTTPS server")
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,16 +68,16 @@ func (ws *WebServer) Shutdown() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ws *WebServer) listenPlain() {
|
func (ws *WebServer) listenPlain() {
|
||||||
ln, err := net.Listen("tcp", config.G.Web.Listen)
|
ln, err := net.Listen("tcp", config.Get().Web.Listen)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ws.log.WithError(err).Fatal("failed to listen")
|
ws.log.WithError(err).Fatal("failed to listen")
|
||||||
}
|
}
|
||||||
proxyListener := &proxyproto.Listener{Listener: ln}
|
proxyListener := &proxyproto.Listener{Listener: ln}
|
||||||
defer proxyListener.Close()
|
defer proxyListener.Close()
|
||||||
|
|
||||||
ws.log.WithField("listen", config.G.Web.Listen).Info("Starting HTTP server")
|
ws.log.WithField("listen", config.Get().Web.Listen).Info("Starting HTTP server")
|
||||||
ws.serve(proxyListener)
|
ws.serve(proxyListener)
|
||||||
ws.log.WithField("listen", config.G.Web.Listen).Info("Stopping HTTP server")
|
ws.log.WithField("listen", config.Get().Web.Listen).Info("Stopping HTTP server")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ws *WebServer) serve(listener net.Listener) {
|
func (ws *WebServer) serve(listener net.Listener) {
|
||||||
|
|
Reference in a new issue