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:
Jens Langhammer 2022-07-26 11:33:35 +02:00
parent d08856c1fe
commit 10b48b27b0
10 changed files with 149 additions and 47 deletions

View File

@ -34,28 +34,15 @@ func main() {
})
go debug.EnableDebugServer()
l := log.WithField("logger", "authentik.root")
config.DefaultConfig()
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()
config.Get().Setup("./authentik/lib/default.yml", "./local.env.yml")
if config.G.ErrorReporting.Enabled {
if config.Get().ErrorReporting.Enabled {
err := sentry.Init(sentry.ClientOptions{
Dsn: config.G.ErrorReporting.DSN,
Dsn: config.Get().ErrorReporting.DSN,
AttachStacktrace: true,
TracesSampler: sentryutils.SamplerFunc(config.G.ErrorReporting.SampleRate),
TracesSampler: sentryutils.SamplerFunc(config.Get().ErrorReporting.SampleRate),
Release: fmt.Sprintf("authentik@%s", constants.VERSION),
Environment: config.G.ErrorReporting.Environment,
Environment: config.Get().ErrorReporting.Environment,
HTTPTransport: webutils.NewUserAgentTransport(constants.UserAgent(), http.DefaultTransport),
IgnoreErrors: []string{
http.ErrAbortHandler.Error(),
@ -74,7 +61,7 @@ func main() {
g := gounicorn.NewGoUnicorn()
ws := web.NewWebServer(g)
g.HealthyCallback = func() {
if !config.G.Web.DisableEmbeddedOutpost {
if !config.Get().Web.DisableEmbeddedOutpost {
go attemptProxyStart(ws, u)
}
}
@ -105,7 +92,7 @@ func attemptProxyStart(ws *web.WebServer, u *url.URL) {
l := log.WithField("logger", "authentik.server")
for {
l.Debug("attempting to init outpost")
ac := ak.NewAPIController(*u, config.G.SecretKey)
ac := ak.NewAPIController(*u, config.Get().SecretKey)
if ac == nil {
attempt += 1
time.Sleep(1 * time.Second)

View File

@ -3,6 +3,9 @@ package config
import (
"fmt"
"io/ioutil"
"net/url"
"os"
"reflect"
"strings"
env "github.com/Netflix/go-env"
@ -11,10 +14,17 @@ import (
"gopkg.in/yaml.v2"
)
var G Config
var cfg *Config
func DefaultConfig() {
G = Config{
func Get() *Config {
if cfg == nil {
cfg = defaultConfig()
}
return cfg
}
func defaultConfig() *Config {
return &Config{
Debug: false,
Web: WebConfig{
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)
if err != nil {
return fmt.Errorf("Failed to load config file: %w", err)
@ -42,28 +63,83 @@ func LoadConfig(path string) error {
if err != nil {
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)
}
c.walkScheme(c)
log.WithField("path", path).Debug("Loaded config")
return nil
}
func FromEnv() error {
func (c *Config) fromEnv() error {
nc := Config{}
_, err := env.UnmarshalFromEnviron(&nc)
if err != nil {
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)
}
c.walkScheme(c)
log.Debug("Loaded config from environment")
return nil
}
func ConfigureLogger() {
switch strings.ToLower(G.LogLevel) {
func (c *Config) walkScheme(v interface{}) {
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":
log.SetLevel(log.TraceLevel)
case "debug":
@ -83,7 +159,7 @@ func ConfigureLogger() {
log.FieldKeyTime: "timestamp",
}
if G.Debug {
if c.Debug {
log.SetFormatter(&log.TextFormatter{FieldMap: fm})
} else {
log.SetFormatter(&log.JSONFormatter{FieldMap: fm, DisableHTMLEscape: true})

View 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)
}

View File

@ -39,7 +39,7 @@ func NewGoUnicorn() *GoUnicorn {
func (g *GoUnicorn) initCmd() {
command := "gunicorn"
args := []string{"-c", "./lifecycle/gunicorn.conf.py", "authentik.root.asgi:application"}
if config.G.Debug {
if config.Get().Debug {
command = "./manage.py"
args = []string{"runserver"}
}

View File

@ -15,8 +15,8 @@ import (
func (a *Application) getStore(p api.ProxyOutpostConfig, externalHost *url.URL) sessions.Store {
var store sessions.Store
if config.G.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))
if config.Get().Redis.Host != "" {
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 {
panic(err)
}

View File

@ -37,7 +37,7 @@ func RunMetricsServer() {
l.WithError(err).Warning("failed to get upstream metrics")
return
}
re.SetBasicAuth("monitor", config.G.SecretKey)
re.SetBasicAuth("monitor", config.Get().SecretKey)
res, err := http.DefaultClient.Do(re)
if err != nil {
l.WithError(err).Warning("failed to get upstream metrics")
@ -54,10 +54,10 @@ func RunMetricsServer() {
return
}
})
l.WithField("listen", config.G.Web.ListenMetrics).Info("Starting Metrics server")
err := http.ListenAndServe(config.G.Web.ListenMetrics, m)
l.WithField("listen", config.Get().Web.ListenMetrics).Info("Starting Metrics server")
err := http.ListenAndServe(config.Get().Web.ListenMetrics, m)
if err != nil {
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")
}

View File

@ -14,7 +14,7 @@ type SentryRequest struct {
}
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")
rw.WriteHeader(http.StatusBadRequest)
return
@ -37,8 +37,8 @@ func (ws *WebServer) APISentryProxy(rw http.ResponseWriter, r *http.Request) {
rw.WriteHeader(http.StatusBadRequest)
return
}
if sd.DSN != config.G.ErrorReporting.DSN {
ws.log.WithField("have", sd.DSN).WithField("expected", config.G.ErrorReporting.DSN).Debug("invalid DSN")
if sd.DSN != config.Get().ErrorReporting.DSN {
ws.log.WithField("have", sd.DSN).WithField("expected", config.Get().ErrorReporting.DSN).Debug("invalid DSN")
rw.WriteHeader(http.StatusBadRequest)
return
}

View File

@ -17,7 +17,7 @@ func (ws *WebServer) configureStatic() {
indexLessRouter := statRouter.NewRoute().Subrouter()
indexLessRouter.Use(web.DisableIndex)
// 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"))
distHandler := http.StripPrefix("/static/dist/", distFs)
authentikHandler := http.StripPrefix("/static/authentik/", http.FileServer(http.Dir("./web/authentik")))

View File

@ -41,7 +41,7 @@ func (ws *WebServer) listenTLS() {
GetCertificate: ws.GetCertificate(),
}
ln, err := net.Listen("tcp", config.G.Web.ListenTLS)
ln, err := net.Listen("tcp", config.Get().Web.ListenTLS)
if err != nil {
ws.log.WithError(err).Fatalf("failed to listen (TLS)")
return
@ -50,7 +50,7 @@ func (ws *WebServer) listenTLS() {
defer proxyListener.Close()
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.log.WithField("listen", config.G.Web.ListenTLS).Info("Stopping HTTPS server")
ws.log.WithField("listen", config.Get().Web.ListenTLS).Info("Stopping HTTPS server")
}

View File

@ -68,16 +68,16 @@ func (ws *WebServer) Shutdown() {
}
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 {
ws.log.WithError(err).Fatal("failed to listen")
}
proxyListener := &proxyproto.Listener{Listener: ln}
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.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) {