From 0d0aeab4eead6bf7511474dacbc8846b11642793 Mon Sep 17 00:00:00 2001 From: Marc 'risson' Schmitt Date: Mon, 24 Mar 2025 21:01:34 +0100 Subject: [PATCH] wip Signed-off-by: Marc 'risson' Schmitt --- authentik/lib/default.yml | 1 + internal/config/struct.go | 5 + internal/web/web.go | 2 + internal/worker/worker.go | 198 +++++++++++++++++++++++++++++++++++++ scripts/generate_config.py | 3 + 5 files changed, 209 insertions(+) create mode 100644 internal/worker/worker.go diff --git a/authentik/lib/default.yml b/authentik/lib/default.yml index 18b99e0f9a..f62265d931 100644 --- a/authentik/lib/default.yml +++ b/authentik/lib/default.yml @@ -142,6 +142,7 @@ web: path: / worker: + embedded: false concurrency: 2 storage: diff --git a/internal/config/struct.go b/internal/config/struct.go index 4bd4cd97c3..05e544d844 100644 --- a/internal/config/struct.go +++ b/internal/config/struct.go @@ -7,6 +7,7 @@ type Config struct { ErrorReporting ErrorReportingConfig `yaml:"error_reporting" env:", prefix=AUTHENTIK_ERROR_REPORTING__"` Redis RedisConfig `yaml:"redis" env:", prefix=AUTHENTIK_REDIS__"` Outposts OutpostConfig `yaml:"outposts" env:", prefix=AUTHENTIK_OUTPOSTS__"` + Worker WorkerConfig `yaml:"worker" env:", prefix=AUTHENTIK_WORKER__"` // Config for core and embedded outpost SecretKey string `yaml:"secret_key" env:"AUTHENTIK_SECRET_KEY, overwrite"` @@ -77,3 +78,7 @@ type OutpostConfig struct { type WebConfig struct { Path string `yaml:"path" env:"PATH, overwrite"` } + +type WorkerConfig struct { + Embedded string `yaml:"embedded" env:"EMBEDDED, overwrite"` +} diff --git a/internal/web/web.go b/internal/web/web.go index 6729b688e8..ecf2da149e 100644 --- a/internal/web/web.go +++ b/internal/web/web.go @@ -22,6 +22,7 @@ import ( "goauthentik.io/internal/utils" "goauthentik.io/internal/utils/web" "goauthentik.io/internal/web/brand_tls" + "goauthentik.io/internal/worker" ) type WebServer struct { @@ -35,6 +36,7 @@ type WebServer struct { g *gounicorn.GoUnicorn gunicornReady bool + worker *worker.Worker mainRouter *mux.Router loggingRouter *mux.Router log *log.Entry diff --git a/internal/worker/worker.go b/internal/worker/worker.go new file mode 100644 index 0000000000..6d3d2406e0 --- /dev/null +++ b/internal/worker/worker.go @@ -0,0 +1,198 @@ +package worker + +import ( + "fmt" + "os" + "os/exec" + "os/signal" + "runtime" + "strconv" + "strings" + "syscall" + "time" + + log "github.com/sirupsen/logrus" + + "goauthentik.io/internal/config" + "goauthentik.io/internal/utils" +) + +type Worker struct { + Healthcheck func() bool + HealthyCallback func() + + log *log.Entry + p *exec.Cmd + pidFile string + started bool + killed bool + alive bool +} + +func New(healthcheck func() bool) *Worker { + logger := log.WithField("logger", "authentik.router.unicorn") + w := &Worker{ + Healthcheck: healthcheck, + log: logger, + started: false, + killed: false, + alive: false, + HealthyCallback: func() {}, + } + w.initCmd() + c := make(chan os.Signal, 1) + signal.Notify(c, syscall.SIGHUP, syscall.SIGUSR2) + go func() { + for sig := range c { + if sig == syscall.SIGHUP { + w.log.Info("SIGHUP received, forwarding to gunicorn") + w.Reload() + } else if sig == syscall.SIGUSR2 { + w.log.Info("SIGUSR2 received, restarting gunicorn") + w.Restart() + } + } + }() + return w +} + +func (w *Worker) initCmd() { + command := "./manage.py" + args := []string{"dev_server"} + if !config.Get().Debug { + pidFile, err := os.CreateTemp("", "authentik-gunicorn.*.pid") + if err != nil { + panic(fmt.Errorf("failed to create temporary pid file: %v", err)) + } + w.pidFile = pidFile.Name() + command = "gunicorn" + args = []string{"-c", "./lifecycle/gunicorn.conf.py", "authentik.root.asgi:application"} + if w.pidFile != "" { + args = append(args, "--pid", w.pidFile) + } + } + w.log.WithField("args", args).WithField("cmd", command).Debug("Starting gunicorn") + w.p = exec.Command(command, args...) + w.p.Env = os.Environ() + w.p.Stdout = os.Stdout + w.p.Stderr = os.Stderr +} + +func (w *Worker) IsRunning() bool { + return w.alive +} + +func (w *Worker) Start() error { + if w.started { + w.initCmd() + } + w.killed = false + w.started = true + go w.healthcheck() + return w.p.Run() +} + +func (w *Worker) healthcheck() { + w.log.Debug("starting healthcheck") + // Default healthcheck is every 1 second on startup + // once we've been healthy once, increase to 30 seconds + for range time.NewTicker(time.Second).C { + if w.Healthcheck() { + w.alive = true + w.log.Debug("backend is alive, backing off with healthchecks") + w.HealthyCallback() + break + } + w.log.Debug("backend not alive yet") + } +} + +func (w *Worker) Reload() { + w.log.WithField("method", "reload").Info("reloading gunicorn") + err := w.p.Process.Signal(syscall.SIGHUP) + if err != nil { + w.log.WithError(err).Warning("failed to reload gunicorn") + } +} + +func (w *Worker) Restart() { + w.log.WithField("method", "restart").Info("restart gunicorn") + if w.pidFile == "" { + w.log.Warning("pidfile is non existent, cannot restart") + return + } + + err := w.p.Process.Signal(syscall.SIGUSR2) + if err != nil { + w.log.WithError(err).Warning("failed to restart gunicorn") + return + } + + newPidFile := fmt.Sprintf("%s.2", w.pidFile) + + // Wait for the new PID file to be created + for range time.NewTicker(1 * time.Second).C { + _, err = os.Stat(newPidFile) + if err == nil || !os.IsNotExist(err) { + break + } + w.log.Debugf("waiting for new gunicorn pidfile to appear at %s", newPidFile) + } + if err != nil { + w.log.WithError(err).Warning("failed to find the new gunicorn process, aborting") + return + } + + newPidB, err := os.ReadFile(newPidFile) + if err != nil { + w.log.WithError(err).Warning("failed to find the new gunicorn process, aborting") + return + } + newPidS := strings.TrimSpace(string(newPidB[:])) + newPid, err := strconv.Atoi(newPidS) + if err != nil { + w.log.WithError(err).Warning("failed to find the new gunicorn process, aborting") + return + } + w.log.Warningf("new gunicorn PID is %d", newPid) + + newProcess, err := utils.FindProcess(newPid) + if newProcess == nil || err != nil { + w.log.WithError(err).Warning("failed to find the new gunicorn process, aborting") + return + } + + // The new process has started, let's gracefully kill the old one + w.log.Warning("killing old gunicorn") + err = w.p.Process.Signal(syscall.SIGTERM) + if err != nil { + w.log.Warning("failed to kill old instance of gunicorn") + } + + w.p.Process = newProcess + // No need to close any files and the .2 pid file is deleted by Gunicorn +} + +func (w *Worker) Kill() { + if !w.started { + return + } + var err error + if runtime.GOOS == "darwin" { + w.log.WithField("method", "kill").Warning("stopping gunicorn") + err = w.p.Process.Kill() + } else { + w.log.WithField("method", "sigterm").Warning("stopping gunicorn") + err = syscall.Kill(w.p.Process.Pid, syscall.SIGTERM) + } + if err != nil { + w.log.WithError(err).Warning("failed to stop gunicorn") + } + if w.pidFile != "" { + err := os.Remove(w.pidFile) + if err != nil { + w.log.WithError(err).Warning("failed to remove pidfile") + } + } + w.killed = true +} diff --git a/scripts/generate_config.py b/scripts/generate_config.py index ae8dcc69be..cc7cc30fd1 100755 --- a/scripts/generate_config.py +++ b/scripts/generate_config.py @@ -43,6 +43,9 @@ with open("local.env.yml", "w", encoding="utf-8") as _config: "enabled": False, "api_key": generate_id(), }, + "worker": { + "embedded": True, + }, }, _config, default_flow_style=False,