root: connect to backend via socket (#6720)
* root: connect to gunicorn via socket Signed-off-by: Jens Langhammer <jens@goauthentik.io> * put socket in temp folder Signed-off-by: Jens Langhammer <jens@goauthentik.io> * use non-socket connection for debug Signed-off-by: Jens Langhammer <jens@goauthentik.io> * don't hardcode local url Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix dev_server missing websocket Signed-off-by: Jens Langhammer <jens@goauthentik.io> * dedupe logging config between gunicorn and main app Signed-off-by: Jens Langhammer <jens@goauthentik.io> * slight refactor for proxy errors Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
parent
c04e83c86c
commit
fd561ac802
|
@ -0,0 +1,9 @@
|
||||||
|
"""custom runserver command"""
|
||||||
|
from daphne.management.commands.runserver import Command as RunServer
|
||||||
|
|
||||||
|
|
||||||
|
class Command(RunServer):
|
||||||
|
"""custom runserver command, which doesn't show the misleading django startup message"""
|
||||||
|
|
||||||
|
def on_bind(self, server_port):
|
||||||
|
pass
|
|
@ -1,7 +1,112 @@
|
||||||
"""logging helpers"""
|
"""logging helpers"""
|
||||||
|
import logging
|
||||||
from logging import Logger
|
from logging import Logger
|
||||||
from os import getpid
|
from os import getpid
|
||||||
|
|
||||||
|
import structlog
|
||||||
|
|
||||||
|
from authentik.lib.config import CONFIG
|
||||||
|
|
||||||
|
LOG_PRE_CHAIN = [
|
||||||
|
# Add the log level and a timestamp to the event_dict if the log entry
|
||||||
|
# is not from structlog.
|
||||||
|
structlog.stdlib.add_log_level,
|
||||||
|
structlog.stdlib.add_logger_name,
|
||||||
|
structlog.processors.TimeStamper(),
|
||||||
|
structlog.processors.StackInfoRenderer(),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def get_log_level():
|
||||||
|
"""Get log level, clamp trace to debug"""
|
||||||
|
level = CONFIG.get("log_level").upper()
|
||||||
|
# We could add a custom level to stdlib logging and structlog, but it's not easy or clean
|
||||||
|
# https://stackoverflow.com/questions/54505487/custom-log-level-not-working-with-structlog
|
||||||
|
# Additionally, the entire code uses debug as highest level
|
||||||
|
# so that would have to be re-written too
|
||||||
|
if level == "TRACE":
|
||||||
|
level = "DEBUG"
|
||||||
|
return level
|
||||||
|
|
||||||
|
|
||||||
|
def structlog_configure():
|
||||||
|
"""Configure structlog itself"""
|
||||||
|
structlog.configure_once(
|
||||||
|
processors=[
|
||||||
|
structlog.stdlib.add_log_level,
|
||||||
|
structlog.stdlib.add_logger_name,
|
||||||
|
structlog.contextvars.merge_contextvars,
|
||||||
|
add_process_id,
|
||||||
|
structlog.stdlib.PositionalArgumentsFormatter(),
|
||||||
|
structlog.processors.TimeStamper(fmt="iso", utc=False),
|
||||||
|
structlog.processors.StackInfoRenderer(),
|
||||||
|
structlog.processors.dict_tracebacks,
|
||||||
|
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
|
||||||
|
],
|
||||||
|
logger_factory=structlog.stdlib.LoggerFactory(),
|
||||||
|
wrapper_class=structlog.make_filtering_bound_logger(
|
||||||
|
getattr(logging, get_log_level(), logging.WARNING)
|
||||||
|
),
|
||||||
|
cache_logger_on_first_use=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_logger_config():
|
||||||
|
"""Configure python stdlib's logging"""
|
||||||
|
debug = CONFIG.get_bool("debug")
|
||||||
|
global_level = get_log_level()
|
||||||
|
base_config = {
|
||||||
|
"version": 1,
|
||||||
|
"disable_existing_loggers": False,
|
||||||
|
"formatters": {
|
||||||
|
"json": {
|
||||||
|
"()": structlog.stdlib.ProcessorFormatter,
|
||||||
|
"processor": structlog.processors.JSONRenderer(sort_keys=True),
|
||||||
|
"foreign_pre_chain": LOG_PRE_CHAIN + [structlog.processors.dict_tracebacks],
|
||||||
|
},
|
||||||
|
"console": {
|
||||||
|
"()": structlog.stdlib.ProcessorFormatter,
|
||||||
|
"processor": structlog.dev.ConsoleRenderer(colors=debug),
|
||||||
|
"foreign_pre_chain": LOG_PRE_CHAIN,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"handlers": {
|
||||||
|
"console": {
|
||||||
|
"level": "DEBUG",
|
||||||
|
"class": "logging.StreamHandler",
|
||||||
|
"formatter": "console" if debug else "json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"loggers": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
handler_level_map = {
|
||||||
|
"": global_level,
|
||||||
|
"authentik": global_level,
|
||||||
|
"django": "WARNING",
|
||||||
|
"django.request": "ERROR",
|
||||||
|
"celery": "WARNING",
|
||||||
|
"selenium": "WARNING",
|
||||||
|
"docker": "WARNING",
|
||||||
|
"urllib3": "WARNING",
|
||||||
|
"websockets": "WARNING",
|
||||||
|
"daphne": "WARNING",
|
||||||
|
"kubernetes": "INFO",
|
||||||
|
"asyncio": "WARNING",
|
||||||
|
"redis": "WARNING",
|
||||||
|
"silk": "INFO",
|
||||||
|
"fsevents": "WARNING",
|
||||||
|
"uvicorn": "WARNING",
|
||||||
|
"gunicorn": "INFO",
|
||||||
|
}
|
||||||
|
for handler_name, level in handler_level_map.items():
|
||||||
|
base_config["loggers"][handler_name] = {
|
||||||
|
"handlers": ["console"],
|
||||||
|
"level": level,
|
||||||
|
"propagate": False,
|
||||||
|
}
|
||||||
|
return base_config
|
||||||
|
|
||||||
|
|
||||||
def add_process_id(logger: Logger, method_name: str, event_dict):
|
def add_process_id(logger: Logger, method_name: str, event_dict):
|
||||||
"""Add the current process ID"""
|
"""Add the current process ID"""
|
||||||
|
|
|
@ -172,7 +172,7 @@ class ChannelsLoggingMiddleware:
|
||||||
LOGGER.info(
|
LOGGER.info(
|
||||||
scope["path"],
|
scope["path"],
|
||||||
scheme="ws",
|
scheme="ws",
|
||||||
remote=scope.get("client", [""])[0],
|
remote=headers.get(b"x-forwarded-for", b"").decode(),
|
||||||
user_agent=headers.get(b"user-agent", b"").decode(),
|
user_agent=headers.get(b"user-agent", b"").decode(),
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,25 +1,21 @@
|
||||||
"""root settings for authentik"""
|
"""root settings for authentik"""
|
||||||
|
|
||||||
import importlib
|
import importlib
|
||||||
import logging
|
|
||||||
import os
|
import os
|
||||||
from hashlib import sha512
|
from hashlib import sha512
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from urllib.parse import quote_plus
|
from urllib.parse import quote_plus
|
||||||
|
|
||||||
import structlog
|
|
||||||
from celery.schedules import crontab
|
from celery.schedules import crontab
|
||||||
from sentry_sdk import set_tag
|
from sentry_sdk import set_tag
|
||||||
|
|
||||||
from authentik import ENV_GIT_HASH_KEY, __version__
|
from authentik import ENV_GIT_HASH_KEY, __version__
|
||||||
from authentik.lib.config import CONFIG
|
from authentik.lib.config import CONFIG
|
||||||
from authentik.lib.logging import add_process_id
|
from authentik.lib.logging import get_logger_config, structlog_configure
|
||||||
from authentik.lib.sentry import sentry_init
|
from authentik.lib.sentry import sentry_init
|
||||||
from authentik.lib.utils.reflection import get_env
|
from authentik.lib.utils.reflection import get_env
|
||||||
from authentik.stages.password import BACKEND_APP_PASSWORD, BACKEND_INBUILT, BACKEND_LDAP
|
from authentik.stages.password import BACKEND_APP_PASSWORD, BACKEND_INBUILT, BACKEND_LDAP
|
||||||
|
|
||||||
LOGGER = structlog.get_logger()
|
|
||||||
|
|
||||||
BASE_DIR = Path(__file__).absolute().parent.parent.parent
|
BASE_DIR = Path(__file__).absolute().parent.parent.parent
|
||||||
STATICFILES_DIRS = [BASE_DIR / Path("web")]
|
STATICFILES_DIRS = [BASE_DIR / Path("web")]
|
||||||
MEDIA_ROOT = BASE_DIR / Path("media")
|
MEDIA_ROOT = BASE_DIR / Path("media")
|
||||||
|
@ -368,90 +364,9 @@ MEDIA_URL = "/media/"
|
||||||
|
|
||||||
TEST = False
|
TEST = False
|
||||||
TEST_RUNNER = "authentik.root.test_runner.PytestTestRunner"
|
TEST_RUNNER = "authentik.root.test_runner.PytestTestRunner"
|
||||||
# We can't check TEST here as its set later by the test runner
|
|
||||||
LOG_LEVEL = CONFIG.get("log_level").upper() if "TF_BUILD" not in os.environ else "DEBUG"
|
|
||||||
# We could add a custom level to stdlib logging and structlog, but it's not easy or clean
|
|
||||||
# https://stackoverflow.com/questions/54505487/custom-log-level-not-working-with-structlog
|
|
||||||
# Additionally, the entire code uses debug as highest level so that would have to be re-written too
|
|
||||||
if LOG_LEVEL == "TRACE":
|
|
||||||
LOG_LEVEL = "DEBUG"
|
|
||||||
|
|
||||||
structlog.configure_once(
|
structlog_configure()
|
||||||
processors=[
|
LOGGING = get_logger_config()
|
||||||
structlog.stdlib.add_log_level,
|
|
||||||
structlog.stdlib.add_logger_name,
|
|
||||||
structlog.contextvars.merge_contextvars,
|
|
||||||
add_process_id,
|
|
||||||
structlog.stdlib.PositionalArgumentsFormatter(),
|
|
||||||
structlog.processors.TimeStamper(fmt="iso", utc=False),
|
|
||||||
structlog.processors.StackInfoRenderer(),
|
|
||||||
structlog.processors.dict_tracebacks,
|
|
||||||
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
|
|
||||||
],
|
|
||||||
logger_factory=structlog.stdlib.LoggerFactory(),
|
|
||||||
wrapper_class=structlog.make_filtering_bound_logger(
|
|
||||||
getattr(logging, LOG_LEVEL, logging.WARNING)
|
|
||||||
),
|
|
||||||
cache_logger_on_first_use=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
LOG_PRE_CHAIN = [
|
|
||||||
# Add the log level and a timestamp to the event_dict if the log entry
|
|
||||||
# is not from structlog.
|
|
||||||
structlog.stdlib.add_log_level,
|
|
||||||
structlog.stdlib.add_logger_name,
|
|
||||||
structlog.processors.TimeStamper(),
|
|
||||||
structlog.processors.StackInfoRenderer(),
|
|
||||||
]
|
|
||||||
|
|
||||||
LOGGING = {
|
|
||||||
"version": 1,
|
|
||||||
"disable_existing_loggers": False,
|
|
||||||
"formatters": {
|
|
||||||
"json": {
|
|
||||||
"()": structlog.stdlib.ProcessorFormatter,
|
|
||||||
"processor": structlog.processors.JSONRenderer(sort_keys=True),
|
|
||||||
"foreign_pre_chain": LOG_PRE_CHAIN + [structlog.processors.dict_tracebacks],
|
|
||||||
},
|
|
||||||
"console": {
|
|
||||||
"()": structlog.stdlib.ProcessorFormatter,
|
|
||||||
"processor": structlog.dev.ConsoleRenderer(colors=DEBUG),
|
|
||||||
"foreign_pre_chain": LOG_PRE_CHAIN,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"handlers": {
|
|
||||||
"console": {
|
|
||||||
"level": "DEBUG",
|
|
||||||
"class": "logging.StreamHandler",
|
|
||||||
"formatter": "console" if DEBUG else "json",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"loggers": {},
|
|
||||||
}
|
|
||||||
|
|
||||||
_LOGGING_HANDLER_MAP = {
|
|
||||||
"": LOG_LEVEL,
|
|
||||||
"authentik": LOG_LEVEL,
|
|
||||||
"django": "WARNING",
|
|
||||||
"django.request": "ERROR",
|
|
||||||
"celery": "WARNING",
|
|
||||||
"selenium": "WARNING",
|
|
||||||
"docker": "WARNING",
|
|
||||||
"urllib3": "WARNING",
|
|
||||||
"websockets": "WARNING",
|
|
||||||
"daphne": "WARNING",
|
|
||||||
"kubernetes": "INFO",
|
|
||||||
"asyncio": "WARNING",
|
|
||||||
"redis": "WARNING",
|
|
||||||
"silk": "INFO",
|
|
||||||
"fsevents": "WARNING",
|
|
||||||
}
|
|
||||||
for handler_name, level in _LOGGING_HANDLER_MAP.items():
|
|
||||||
LOGGING["loggers"][handler_name] = {
|
|
||||||
"handlers": ["console"],
|
|
||||||
"level": level,
|
|
||||||
"propagate": False,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
_DISALLOWED_ITEMS = [
|
_DISALLOWED_ITEMS = [
|
||||||
|
|
|
@ -13,7 +13,6 @@ import (
|
||||||
"goauthentik.io/internal/config"
|
"goauthentik.io/internal/config"
|
||||||
"goauthentik.io/internal/constants"
|
"goauthentik.io/internal/constants"
|
||||||
"goauthentik.io/internal/debug"
|
"goauthentik.io/internal/debug"
|
||||||
"goauthentik.io/internal/gounicorn"
|
|
||||||
"goauthentik.io/internal/outpost/ak"
|
"goauthentik.io/internal/outpost/ak"
|
||||||
"goauthentik.io/internal/outpost/proxyv2"
|
"goauthentik.io/internal/outpost/proxyv2"
|
||||||
sentryutils "goauthentik.io/internal/utils/sentry"
|
sentryutils "goauthentik.io/internal/utils/sentry"
|
||||||
|
@ -22,8 +21,6 @@ import (
|
||||||
"goauthentik.io/internal/web/tenant_tls"
|
"goauthentik.io/internal/web/tenant_tls"
|
||||||
)
|
)
|
||||||
|
|
||||||
var running = true
|
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
Use: "authentik",
|
Use: "authentik",
|
||||||
Short: "Start authentik instance",
|
Short: "Start authentik instance",
|
||||||
|
@ -63,38 +60,23 @@ var rootCmd = &cobra.Command{
|
||||||
ex := common.Init()
|
ex := common.Init()
|
||||||
defer common.Defer()
|
defer common.Defer()
|
||||||
|
|
||||||
u, _ := url.Parse("http://localhost:8000")
|
u, err := url.Parse(fmt.Sprintf("http://%s", config.Get().Listen.HTTP))
|
||||||
|
if err != nil {
|
||||||
g := gounicorn.New()
|
panic(err)
|
||||||
defer func() {
|
|
||||||
l.Info("shutting down gunicorn")
|
|
||||||
g.Kill()
|
|
||||||
}()
|
|
||||||
ws := web.NewWebServer(g)
|
|
||||||
g.HealthyCallback = func() {
|
|
||||||
if !config.Get().Outposts.DisableEmbeddedOutpost {
|
|
||||||
go attemptProxyStart(ws, u)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
go web.RunMetricsServer()
|
|
||||||
go attemptStartBackend(g)
|
|
||||||
ws.Start()
|
|
||||||
<-ex
|
|
||||||
running = false
|
|
||||||
l.Info("shutting down webserver")
|
|
||||||
go ws.Shutdown()
|
|
||||||
|
|
||||||
},
|
ws := web.NewWebServer()
|
||||||
}
|
ws.Core().HealthyCallback = func() {
|
||||||
|
if config.Get().Outposts.DisableEmbeddedOutpost {
|
||||||
func attemptStartBackend(g *gounicorn.GoUnicorn) {
|
|
||||||
for {
|
|
||||||
if !running {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err := g.Start()
|
go attemptProxyStart(ws, u)
|
||||||
log.WithField("logger", "authentik.router").WithError(err).Warning("gunicorn process died, restarting")
|
|
||||||
}
|
}
|
||||||
|
ws.Start()
|
||||||
|
<-ex
|
||||||
|
l.Info("shutting down webserver")
|
||||||
|
go ws.Shutdown()
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func attemptProxyStart(ws *web.WebServer, u *url.URL) {
|
func attemptProxyStart(ws *web.WebServer, u *url.URL) {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package gounicorn
|
package gounicorn
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
@ -10,10 +9,10 @@ import (
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"goauthentik.io/internal/config"
|
"goauthentik.io/internal/config"
|
||||||
"goauthentik.io/internal/utils/web"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type GoUnicorn struct {
|
type GoUnicorn struct {
|
||||||
|
Healthcheck func() bool
|
||||||
HealthyCallback func()
|
HealthyCallback func()
|
||||||
|
|
||||||
log *log.Entry
|
log *log.Entry
|
||||||
|
@ -23,9 +22,10 @@ type GoUnicorn struct {
|
||||||
alive bool
|
alive bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func New() *GoUnicorn {
|
func New(healthcheck func() bool) *GoUnicorn {
|
||||||
logger := log.WithField("logger", "authentik.router.unicorn")
|
logger := log.WithField("logger", "authentik.router.unicorn")
|
||||||
g := &GoUnicorn{
|
g := &GoUnicorn{
|
||||||
|
Healthcheck: healthcheck,
|
||||||
log: logger,
|
log: logger,
|
||||||
started: false,
|
started: false,
|
||||||
killed: false,
|
killed: false,
|
||||||
|
@ -41,7 +41,7 @@ func (g *GoUnicorn) initCmd() {
|
||||||
args := []string{"-c", "./lifecycle/gunicorn.conf.py", "authentik.root.asgi:application"}
|
args := []string{"-c", "./lifecycle/gunicorn.conf.py", "authentik.root.asgi:application"}
|
||||||
if config.Get().Debug {
|
if config.Get().Debug {
|
||||||
command = "./manage.py"
|
command = "./manage.py"
|
||||||
args = []string{"runserver"}
|
args = []string{"dev_server"}
|
||||||
}
|
}
|
||||||
g.log.WithField("args", args).WithField("cmd", command).Debug("Starting gunicorn")
|
g.log.WithField("args", args).WithField("cmd", command).Debug("Starting gunicorn")
|
||||||
g.p = exec.Command(command, args...)
|
g.p = exec.Command(command, args...)
|
||||||
|
@ -69,22 +69,11 @@ func (g *GoUnicorn) Start() error {
|
||||||
|
|
||||||
func (g *GoUnicorn) healthcheck() {
|
func (g *GoUnicorn) healthcheck() {
|
||||||
g.log.Debug("starting healthcheck")
|
g.log.Debug("starting healthcheck")
|
||||||
h := &http.Client{
|
|
||||||
Transport: web.NewUserAgentTransport("goauthentik.io/proxy/healthcheck", http.DefaultTransport),
|
|
||||||
}
|
|
||||||
check := func() bool {
|
|
||||||
res, err := h.Get("http://localhost:8000/-/health/live/")
|
|
||||||
if err == nil && res.StatusCode == 204 {
|
|
||||||
g.alive = true
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default healthcheck is every 1 second on startup
|
// Default healthcheck is every 1 second on startup
|
||||||
// once we've been healthy once, increase to 30 seconds
|
// once we've been healthy once, increase to 30 seconds
|
||||||
for range time.Tick(time.Second) {
|
for range time.Tick(time.Second) {
|
||||||
if check() {
|
if g.Healthcheck() {
|
||||||
|
g.alive = true
|
||||||
g.log.Info("backend is alive, backing off with healthchecks")
|
g.log.Info("backend is alive, backing off with healthchecks")
|
||||||
g.HealthyCallback()
|
g.HealthyCallback()
|
||||||
break
|
break
|
||||||
|
@ -92,7 +81,7 @@ func (g *GoUnicorn) healthcheck() {
|
||||||
g.log.Debug("backend not alive yet")
|
g.log.Debug("backend not alive yet")
|
||||||
}
|
}
|
||||||
for range time.Tick(30 * time.Second) {
|
for range time.Tick(30 * time.Second) {
|
||||||
check()
|
g.Healthcheck()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package web
|
package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
@ -26,7 +27,7 @@ var (
|
||||||
}, []string{"dest"})
|
}, []string{"dest"})
|
||||||
)
|
)
|
||||||
|
|
||||||
func RunMetricsServer() {
|
func (ws *WebServer) runMetricsServer() {
|
||||||
m := mux.NewRouter()
|
m := mux.NewRouter()
|
||||||
l := log.WithField("logger", "authentik.router.metrics")
|
l := log.WithField("logger", "authentik.router.metrics")
|
||||||
m.Use(sentry.SentryNoSampleMiddleware)
|
m.Use(sentry.SentryNoSampleMiddleware)
|
||||||
|
@ -38,13 +39,13 @@ func RunMetricsServer() {
|
||||||
).ServeHTTP(rw, r)
|
).ServeHTTP(rw, r)
|
||||||
|
|
||||||
// Get upstream metrics
|
// Get upstream metrics
|
||||||
re, err := http.NewRequest("GET", "http://localhost:8000/-/metrics/", nil)
|
re, err := http.NewRequest("GET", fmt.Sprintf("%s/-/metrics/", ws.ul.String()), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.WithError(err).Warning("failed to get upstream metrics")
|
l.WithError(err).Warning("failed to get upstream metrics")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
re.SetBasicAuth("monitor", config.Get().SecretKey)
|
re.SetBasicAuth("monitor", config.Get().SecretKey)
|
||||||
res, err := http.DefaultClient.Do(re)
|
res, err := ws.upstreamHttpClient().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")
|
||||||
return
|
return
|
||||||
|
|
|
@ -2,10 +2,10 @@ package web
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
"net/url"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
@ -14,10 +14,9 @@ import (
|
||||||
|
|
||||||
func (ws *WebServer) configureProxy() {
|
func (ws *WebServer) configureProxy() {
|
||||||
// Reverse proxy to the application server
|
// Reverse proxy to the application server
|
||||||
u, _ := url.Parse("http://localhost:8000")
|
|
||||||
director := func(req *http.Request) {
|
director := func(req *http.Request) {
|
||||||
req.URL.Scheme = u.Scheme
|
req.URL.Scheme = ws.ul.Scheme
|
||||||
req.URL.Host = u.Host
|
req.URL.Host = ws.ul.Host
|
||||||
if _, ok := req.Header["User-Agent"]; !ok {
|
if _, ok := req.Header["User-Agent"]; !ok {
|
||||||
// explicitly disable User-Agent so it's not set to default value
|
// explicitly disable User-Agent so it's not set to default value
|
||||||
req.Header.Set("User-Agent", "")
|
req.Header.Set("User-Agent", "")
|
||||||
|
@ -27,7 +26,10 @@ func (ws *WebServer) configureProxy() {
|
||||||
}
|
}
|
||||||
ws.log.WithField("url", req.URL.String()).WithField("headers", req.Header).Trace("tracing request to backend")
|
ws.log.WithField("url", req.URL.String()).WithField("headers", req.Header).Trace("tracing request to backend")
|
||||||
}
|
}
|
||||||
rp := &httputil.ReverseProxy{Director: director}
|
rp := &httputil.ReverseProxy{
|
||||||
|
Director: director,
|
||||||
|
Transport: ws.upstreamHttpClient().Transport,
|
||||||
|
}
|
||||||
rp.ErrorHandler = ws.proxyErrorHandler
|
rp.ErrorHandler = ws.proxyErrorHandler
|
||||||
rp.ModifyResponse = ws.proxyModifyResponse
|
rp.ModifyResponse = ws.proxyModifyResponse
|
||||||
ws.m.PathPrefix("/outpost.goauthentik.io").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
ws.m.PathPrefix("/outpost.goauthentik.io").HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -43,14 +45,14 @@ func (ws *WebServer) configureProxy() {
|
||||||
}).Observe(float64(elapsed))
|
}).Observe(float64(elapsed))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ws.proxyErrorHandler(rw, r, fmt.Errorf("proxy not running"))
|
ws.proxyErrorHandler(rw, r, errors.New("proxy not running"))
|
||||||
})
|
})
|
||||||
ws.m.Path("/-/health/live/").HandlerFunc(sentry.SentryNoSample(func(rw http.ResponseWriter, r *http.Request) {
|
ws.m.Path("/-/health/live/").HandlerFunc(sentry.SentryNoSample(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
rw.WriteHeader(204)
|
rw.WriteHeader(204)
|
||||||
}))
|
}))
|
||||||
ws.m.PathPrefix("/").HandlerFunc(sentry.SentryNoSample(func(rw http.ResponseWriter, r *http.Request) {
|
ws.m.PathPrefix("/").HandlerFunc(sentry.SentryNoSample(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
if !ws.p.IsRunning() {
|
if !ws.g.IsRunning() {
|
||||||
ws.proxyErrorHandler(rw, r, fmt.Errorf("authentik core not running yet"))
|
ws.proxyErrorHandler(rw, r, errors.New("authentik starting"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
before := time.Now()
|
before := time.Now()
|
||||||
|
@ -82,17 +84,14 @@ func (ws *WebServer) proxyErrorHandler(rw http.ResponseWriter, req *http.Request
|
||||||
ws.log.WithError(err).Warning("failed to proxy to backend")
|
ws.log.WithError(err).Warning("failed to proxy to backend")
|
||||||
rw.WriteHeader(http.StatusBadGateway)
|
rw.WriteHeader(http.StatusBadGateway)
|
||||||
em := fmt.Sprintf("failed to connect to authentik backend: %v", err)
|
em := fmt.Sprintf("failed to connect to authentik backend: %v", err)
|
||||||
if !ws.p.IsRunning() {
|
|
||||||
em = "authentik starting..."
|
|
||||||
}
|
|
||||||
// return json if the client asks for json
|
// return json if the client asks for json
|
||||||
if req.Header.Get("Accept") == "application/json" {
|
if req.Header.Get("Accept") == "application/json" {
|
||||||
eem, _ := json.Marshal(map[string]string{
|
err = json.NewEncoder(rw).Encode(map[string]string{
|
||||||
"error": em,
|
"error": em,
|
||||||
})
|
})
|
||||||
em = string(eem)
|
} else {
|
||||||
}
|
|
||||||
_, err = rw.Write([]byte(em))
|
_, err = rw.Write([]byte(em))
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ws.log.WithError(err).Warning("failed to write error message")
|
ws.log.WithError(err).Warning("failed to write error message")
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,12 @@ package web
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
|
||||||
"github.com/gorilla/handlers"
|
"github.com/gorilla/handlers"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
@ -26,13 +30,18 @@ type WebServer struct {
|
||||||
ProxyServer *proxyv2.ProxyServer
|
ProxyServer *proxyv2.ProxyServer
|
||||||
TenantTLS *tenant_tls.Watcher
|
TenantTLS *tenant_tls.Watcher
|
||||||
|
|
||||||
|
g *gounicorn.GoUnicorn
|
||||||
|
gr bool
|
||||||
m *mux.Router
|
m *mux.Router
|
||||||
lh *mux.Router
|
lh *mux.Router
|
||||||
log *log.Entry
|
log *log.Entry
|
||||||
p *gounicorn.GoUnicorn
|
uc *http.Client
|
||||||
|
ul *url.URL
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewWebServer(g *gounicorn.GoUnicorn) *WebServer {
|
const UnixSocketName = "authentik-core.sock"
|
||||||
|
|
||||||
|
func NewWebServer() *WebServer {
|
||||||
l := log.WithField("logger", "authentik.router")
|
l := log.WithField("logger", "authentik.router")
|
||||||
mainHandler := mux.NewRouter()
|
mainHandler := mux.NewRouter()
|
||||||
mainHandler.Use(web.ProxyHeaders())
|
mainHandler.Use(web.ProxyHeaders())
|
||||||
|
@ -40,23 +49,80 @@ func NewWebServer(g *gounicorn.GoUnicorn) *WebServer {
|
||||||
loggingHandler := mainHandler.NewRoute().Subrouter()
|
loggingHandler := mainHandler.NewRoute().Subrouter()
|
||||||
loggingHandler.Use(web.NewLoggingHandler(l, nil))
|
loggingHandler.Use(web.NewLoggingHandler(l, nil))
|
||||||
|
|
||||||
|
tmp := os.TempDir()
|
||||||
|
socketPath := path.Join(tmp, "authentik-core.sock")
|
||||||
|
|
||||||
|
// create http client to talk to backend, normal client if we're in debug more
|
||||||
|
// and a client that connects to our socket when in non debug mode
|
||||||
|
var upstreamClient *http.Client
|
||||||
|
if config.Get().Debug {
|
||||||
|
upstreamClient = http.DefaultClient
|
||||||
|
} else {
|
||||||
|
upstreamClient = &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
|
||||||
|
return net.Dial("unix", socketPath)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
u, _ := url.Parse("http://localhost:8000")
|
||||||
|
|
||||||
ws := &WebServer{
|
ws := &WebServer{
|
||||||
m: mainHandler,
|
m: mainHandler,
|
||||||
lh: loggingHandler,
|
lh: loggingHandler,
|
||||||
log: l,
|
log: l,
|
||||||
p: g,
|
gr: true,
|
||||||
|
uc: upstreamClient,
|
||||||
|
ul: u,
|
||||||
}
|
}
|
||||||
ws.configureStatic()
|
ws.configureStatic()
|
||||||
ws.configureProxy()
|
ws.configureProxy()
|
||||||
|
ws.g = gounicorn.New(func() bool {
|
||||||
|
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/-/health/live/", ws.ul.String()), nil)
|
||||||
|
if err != nil {
|
||||||
|
ws.log.WithError(err).Warning("failed to create request for healthcheck")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
req.Header.Set("User-Agent", "goauthentik.io/router/healthcheck")
|
||||||
|
res, err := ws.upstreamHttpClient().Do(req)
|
||||||
|
if err == nil && res.StatusCode == 204 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
return ws
|
return ws
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ws *WebServer) Start() {
|
func (ws *WebServer) Start() {
|
||||||
|
go ws.runMetricsServer()
|
||||||
|
go ws.attemptStartBackend()
|
||||||
go ws.listenPlain()
|
go ws.listenPlain()
|
||||||
go ws.listenTLS()
|
go ws.listenTLS()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ws *WebServer) attemptStartBackend() {
|
||||||
|
for {
|
||||||
|
if !ws.gr {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err := ws.g.Start()
|
||||||
|
log.WithField("logger", "authentik.router").WithError(err).Warning("gunicorn process died, restarting")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ws *WebServer) Core() *gounicorn.GoUnicorn {
|
||||||
|
return ws.g
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ws *WebServer) upstreamHttpClient() *http.Client {
|
||||||
|
return ws.uc
|
||||||
|
}
|
||||||
|
|
||||||
func (ws *WebServer) Shutdown() {
|
func (ws *WebServer) Shutdown() {
|
||||||
|
ws.log.Info("shutting down gunicorn")
|
||||||
|
ws.g.Kill()
|
||||||
ws.stop <- struct{}{}
|
ws.stop <- struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,12 +7,12 @@ from pathlib import Path
|
||||||
from tempfile import gettempdir
|
from tempfile import gettempdir
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import structlog
|
|
||||||
from kubernetes.config.incluster_config import SERVICE_HOST_ENV_NAME
|
from kubernetes.config.incluster_config import SERVICE_HOST_ENV_NAME
|
||||||
from prometheus_client.values import MultiProcessValue
|
from prometheus_client.values import MultiProcessValue
|
||||||
|
|
||||||
from authentik import get_full_version
|
from authentik import get_full_version
|
||||||
from authentik.lib.config import CONFIG
|
from authentik.lib.config import CONFIG
|
||||||
|
from authentik.lib.logging import get_logger_config
|
||||||
from authentik.lib.utils.http import get_http_session
|
from authentik.lib.utils.http import get_http_session
|
||||||
from authentik.lib.utils.reflection import get_env
|
from authentik.lib.utils.reflection import get_env
|
||||||
from authentik.root.install_id import get_install_id_raw
|
from authentik.root.install_id import get_install_id_raw
|
||||||
|
@ -21,57 +21,23 @@ from lifecycle.worker import DjangoUvicornWorker
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from gunicorn.arbiter import Arbiter
|
from gunicorn.arbiter import Arbiter
|
||||||
|
|
||||||
bind = "127.0.0.1:8000"
|
|
||||||
|
|
||||||
_tmp = Path(gettempdir())
|
_tmp = Path(gettempdir())
|
||||||
worker_class = "lifecycle.worker.DjangoUvicornWorker"
|
worker_class = "lifecycle.worker.DjangoUvicornWorker"
|
||||||
worker_tmp_dir = str(_tmp.joinpath("authentik_worker_tmp"))
|
worker_tmp_dir = str(_tmp.joinpath("authentik_worker_tmp"))
|
||||||
prometheus_tmp_dir = str(_tmp.joinpath("authentik_prometheus_tmp"))
|
prometheus_tmp_dir = str(_tmp.joinpath("authentik_prometheus_tmp"))
|
||||||
|
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "authentik.root.settings")
|
|
||||||
os.environ.setdefault("PROMETHEUS_MULTIPROC_DIR", prometheus_tmp_dir)
|
|
||||||
|
|
||||||
makedirs(worker_tmp_dir, exist_ok=True)
|
makedirs(worker_tmp_dir, exist_ok=True)
|
||||||
makedirs(prometheus_tmp_dir, exist_ok=True)
|
makedirs(prometheus_tmp_dir, exist_ok=True)
|
||||||
|
|
||||||
|
bind = f"unix://{str(_tmp.joinpath('authentik-core.sock'))}"
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "authentik.root.settings")
|
||||||
|
os.environ.setdefault("PROMETHEUS_MULTIPROC_DIR", prometheus_tmp_dir)
|
||||||
|
|
||||||
max_requests = 1000
|
max_requests = 1000
|
||||||
max_requests_jitter = 50
|
max_requests_jitter = 50
|
||||||
|
|
||||||
_debug = CONFIG.get_bool("DEBUG", False)
|
logconfig_dict = get_logger_config()
|
||||||
|
|
||||||
logconfig_dict = {
|
|
||||||
"version": 1,
|
|
||||||
"disable_existing_loggers": False,
|
|
||||||
"formatters": {
|
|
||||||
"json": {
|
|
||||||
"()": structlog.stdlib.ProcessorFormatter,
|
|
||||||
"processor": structlog.processors.JSONRenderer(),
|
|
||||||
"foreign_pre_chain": [
|
|
||||||
structlog.stdlib.add_log_level,
|
|
||||||
structlog.stdlib.add_logger_name,
|
|
||||||
structlog.processors.TimeStamper(),
|
|
||||||
structlog.processors.StackInfoRenderer(),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"console": {
|
|
||||||
"()": structlog.stdlib.ProcessorFormatter,
|
|
||||||
"processor": structlog.dev.ConsoleRenderer(colors=True),
|
|
||||||
"foreign_pre_chain": [
|
|
||||||
structlog.stdlib.add_log_level,
|
|
||||||
structlog.stdlib.add_logger_name,
|
|
||||||
structlog.processors.TimeStamper(),
|
|
||||||
structlog.processors.StackInfoRenderer(),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"handlers": {
|
|
||||||
"console": {"class": "logging.StreamHandler", "formatter": "json" if _debug else "console"},
|
|
||||||
},
|
|
||||||
"loggers": {
|
|
||||||
"uvicorn": {"handlers": ["console"], "level": "WARNING", "propagate": False},
|
|
||||||
"gunicorn": {"handlers": ["console"], "level": "INFO", "propagate": False},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
# if we're running in kubernetes, use fixed workers because we can scale with more pods
|
# if we're running in kubernetes, use fixed workers because we can scale with more pods
|
||||||
# otherwise (assume docker-compose), use as much as we can
|
# otherwise (assume docker-compose), use as much as we can
|
||||||
|
|
Reference in New Issue