diff --git a/.gitignore b/.gitignore index d7e2edd43d..0f576c8ac1 100644 --- a/.gitignore +++ b/.gitignore @@ -229,6 +229,11 @@ source_docs/ ### Golang ### /vendor/ +server +proxy +ldap +rac +radius ### Docker ### tests/openid_conformance/exports/*.zip diff --git a/Cargo.lock b/Cargo.lock index 6bf6ed25be..0b047f8e25 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -191,6 +191,7 @@ dependencies = [ "tokio", "tracing", "uuid", + "which", ] [[package]] @@ -4512,6 +4513,15 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "which" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81995fafaaaf6ae47a7d0cc83c67caf92aeb7e5331650ae6ff856f7c0c60c459" +dependencies = [ + "libc", +] + [[package]] name = "whoami" version = "1.6.1" diff --git a/Cargo.toml b/Cargo.toml index 78c7772ad5..518ec85040 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -113,6 +113,7 @@ tracing-subscriber = { version = "= 0.3.23", features = [ ] } url = "= 2.5.8" uuid = { version = "= 1.23.1", features = ["serde", "v4"] } +which = "= 8.0.2" ak-axum = { package = "authentik-axum", version = "2026.5.0-rc1", path = "./packages/ak-axum" } ak-client = { package = "authentik-client", version = "2026.5.0-rc1", path = "./packages/client-rust" } @@ -282,6 +283,7 @@ sqlx = { workspace = true, optional = true } tokio.workspace = true tracing.workspace = true uuid.workspace = true +which.workspace = true [lints] workspace = true diff --git a/Makefile b/Makefile index 0d7fb55883..acd3259110 100644 --- a/Makefile +++ b/Makefile @@ -109,14 +109,11 @@ i18n-extract: core-i18n-extract web-i18n-extract ## Extract strings that requir aws-cfn: cd lifecycle/aws && npm i && $(UV) run npm run aws-cfn -run-server: ## Run the main authentik server process - $(UV) run ak server +run: ## Run the main authentik server and worker processes + $(UV) run ak allinone -run-worker: ## Run the main authentik worker process - $(UV) run ak worker - -run-worker-watch: ## Run the authentik worker, with auto reloading - watchexec --on-busy-update=restart --stop-signal=SIGINT --exts py,rs --no-meta --notify -- $(UV) run ak worker +run-watch: ## Run the authentik server and worker, with auto reloading + watchexec --on-busy-update=restart --stop-signal=SIGINT --exts py,rs,go --no-meta --notify -- $(UV) run ak allinone core-i18n-extract: $(UV) run ak makemessages \ diff --git a/cmd/server/healthcheck.go b/cmd/server/healthcheck.go index f9a80eb9b6..9238310b52 100644 --- a/cmd/server/healthcheck.go +++ b/cmd/server/healthcheck.go @@ -26,6 +26,8 @@ var healthcheckCmd = &cobra.Command{ exitCode := 1 log.WithField("mode", mode).Debug("checking health") switch strings.ToLower(mode) { + case "allinone": + fallthrough case "server": exitCode = check(fmt.Sprintf("http://localhost%s-/health/live/", config.Get().Web.Path)) case "worker": diff --git a/lifecycle/ak b/lifecycle/ak index 02373703fd..8b9848a296 100755 --- a/lifecycle/ak +++ b/lifecycle/ak @@ -31,7 +31,7 @@ function run_authentik { echo go run ./cmd/server "$@" fi ;; - worker) + allinone | worker) if [[ -x "$(command -v authentik)" ]]; then echo authentik "$@" else @@ -105,7 +105,7 @@ elif [[ "$1" == "test-all" ]]; then prepare_debug chmod 777 /root check_if_root_and_run manage test authentik -elif [[ "$1" == "server" ]] || [[ "$1" == "worker" ]]; then +elif [[ "$1" == "allinone" ]] || [[ "$1" == "server" ]] || [[ "$1" == "worker" ]]; then wait_for_db check_if_root_and_run "$@" elif [[ "$1" == "healthcheck" ]]; then diff --git a/packages/ak-axum/src/server.rs b/packages/ak-axum/src/server.rs index 5debeeddb1..5b3713d5cb 100644 --- a/packages/ak-axum/src/server.rs +++ b/packages/ak-axum/src/server.rs @@ -1,6 +1,6 @@ //! Utilities to run an axum server. -use std::{net, os::unix}; +use std::{net, os::unix, path::PathBuf}; use ak_common::arbiter::{Arbiter, Tasks}; use axum::Router; @@ -21,26 +21,20 @@ async fn run_plain( name: &str, router: Router, addr: net::SocketAddr, - allow_failure: bool, ) -> Result<()> { info!(addr = addr.to_string(), "starting {name} server"); let handle = Handle::new(); arbiter.add_net_handle(handle.clone()).await; - let res = axum_server::Server::bind(addr) + axum_server::Server::bind(addr) .acceptor(CatchPanicAcceptor::new( ProxyProtocolAcceptor::new().acceptor(DefaultAcceptor::new()), arbiter.clone(), )) .handle(handle) .serve(router.into_make_service_with_connect_info::()) - .await; - if res.is_err() && allow_failure { - arbiter.shutdown().await; - return Ok(()); - } - res?; + .await?; Ok(()) } @@ -49,60 +43,59 @@ async fn run_plain( /// /// `name` is only used for observability purposes and should describe which module is starting the /// server. -/// -/// `allow_failure` allows the server to fail silently. pub fn start_plain( tasks: &mut Tasks, name: &'static str, router: Router, addr: net::SocketAddr, - allow_failure: bool, ) -> Result<()> { let arbiter = tasks.arbiter(); tasks .build_task() .name(&format!("{}::run_plain({name}, {addr})", module_path!())) - .spawn(run_plain(arbiter, name, router, addr, allow_failure))?; + .spawn(run_plain(arbiter, name, router, addr))?; Ok(()) } +struct UnixSocketGuard(PathBuf); + +impl Drop for UnixSocketGuard { + fn drop(&mut self) { + trace!(path = ?self.0, "removing socket"); + if let Err(err) = std::fs::remove_file(&self.0) { + trace!(?err, "failed to remove socket, ignoring"); + } + } +} + pub(crate) async fn run_unix( arbiter: Arbiter, name: &str, router: Router, addr: unix::net::SocketAddr, - allow_failure: bool, ) -> Result<()> { info!(?addr, "starting {name} server"); let handle = Handle::new(); arbiter.add_unix_handle(handle.clone()).await; - if !allow_failure && let Some(path) = addr.as_pathname() { + let _guard = if let Some(path) = addr.as_pathname() { trace!(?addr, "removing socket"); if let Err(err) = std::fs::remove_file(path) { trace!(?err, "failed to remove socket, ignoring"); } - } - let res = axum_server::Server::bind(addr.clone()) + Some(UnixSocketGuard(path.to_owned())) + } else { + None + }; + axum_server::Server::bind(addr.clone()) .acceptor(CatchPanicAcceptor::new( DefaultAcceptor::new(), arbiter.clone(), )) .handle(handle) .serve(router.into_make_service()) - .await; - if !allow_failure && let Some(path) = addr.as_pathname() { - trace!(?addr, "removing socket"); - if let Err(err) = std::fs::remove_file(path) { - trace!(?err, "failed to remove socket, ignoring"); - } - } - if res.is_err() && allow_failure { - arbiter.shutdown().await; - return Ok(()); - } - res?; + .await?; Ok(()) } @@ -111,20 +104,17 @@ pub(crate) async fn run_unix( /// /// `name` is only used for observability purposes and should describe which module is starting the /// server. -/// -/// `allow_failure` allows the server to fail silently. pub fn start_unix( tasks: &mut Tasks, name: &'static str, router: Router, addr: unix::net::SocketAddr, - allow_failure: bool, ) -> Result<()> { let arbiter = tasks.arbiter(); tasks .build_task() .name(&format!("{}::run_unix({name}, {addr:?})", module_path!())) - .spawn(run_unix(arbiter, name, router, addr, allow_failure))?; + .spawn(run_unix(arbiter, name, router, addr))?; Ok(()) } diff --git a/src/main.rs b/src/main.rs index 9600a7b0a0..956c97e96e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,10 +23,23 @@ struct Cli { #[derive(Debug, FromArgs, PartialEq)] #[argh(subcommand)] enum Command { + #[cfg(feature = "core")] + AllInOne(AllInOne), + #[cfg(feature = "core")] + Server(server::Cli), #[cfg(feature = "core")] Worker(worker::Cli), } +#[derive(Debug, FromArgs, PartialEq)] +/// Run both the authentik server and worker. +#[argh(subcommand, name = "allinone")] +#[expect( + clippy::empty_structs_with_brackets, + reason = "argh doesn't support unit structs" +)] +pub(crate) struct AllInOne {} + fn main() -> Result<()> { let tracing_crude = ak_tracing::install_crude(); info!(version = authentik_full_version(), "authentik is starting"); @@ -34,6 +47,10 @@ fn main() -> Result<()> { let cli: Cli = argh::from_env(); match &cli.command { + #[cfg(feature = "core")] + Command::AllInOne(_) => Mode::set(Mode::AllInOne)?, + #[cfg(feature = "core")] + Command::Server(_) => Mode::set(Mode::Server)?, #[cfg(feature = "core")] Command::Worker(_) => Mode::set(Mode::Worker)?, } @@ -76,6 +93,16 @@ fn main() -> Result<()> { } match cli.command { + #[cfg(feature = "core")] + Command::AllInOne(_) => { + server::start(server::Cli::default(), &mut tasks).await?; + let workers = worker::start(worker::Cli::default(), &mut tasks)?; + metrics.workers.store(Some(workers)); + } + #[cfg(feature = "core")] + Command::Server(args) => { + server::start(args, &mut tasks).await?; + } #[cfg(feature = "core")] Command::Worker(args) => { let workers = worker::start(args, &mut tasks)?; diff --git a/src/metrics/mod.rs b/src/metrics/mod.rs index 460097c0d3..8e1c4f7a98 100644 --- a/src/metrics/mod.rs +++ b/src/metrics/mod.rs @@ -2,6 +2,7 @@ use std::{env::temp_dir, os::unix, path::PathBuf, sync::Arc}; use ak_axum::{router::wrap_router, server}; use ak_common::{ + Mode, arbiter::{Arbiter, Tasks}, config, }; @@ -77,25 +78,20 @@ pub(crate) fn start(tasks: &mut Tasks) -> Result> { .name(&format!("{}::run_upkeep", module_path!())) .spawn(run_upkeep(arbiter, Arc::clone(&metrics)))?; - for addr in config::get().listen.metrics.iter().copied() { - server::start_plain( + // Only run HTTP server in worker mode, in server or allinone mode, they're handled by the + // server. + if Mode::get() == Mode::Worker { + for addr in config::get().listen.metrics.iter().copied() { + server::start_plain(tasks, "metrics", router.clone(), addr)?; + } + + server::start_unix( tasks, "metrics", - router.clone(), - addr, - config::get().debug, /* Allow failure in case the server is running on the same - * machine, like in dev */ + router, + unix::net::SocketAddr::from_pathname(socket_path())?, )?; } - server::start_unix( - tasks, - "metrics", - router, - unix::net::SocketAddr::from_pathname(socket_path())?, - config::get().debug, /* Allow failure in case the server is running on the same machine, - * like in dev */ - )?; - Ok(metrics) } diff --git a/src/server/mod.rs b/src/server/mod.rs index abdb7ba298..6c5b4348a2 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -1,5 +1,124 @@ -use std::{env::temp_dir, path::PathBuf}; +use std::{env::temp_dir, path::PathBuf, process::Stdio, sync::Arc}; + +use ak_common::{Arbiter, Tasks, config}; +use argh::FromArgs; +use eyre::{Result, eyre}; +use nix::{ + sys::signal::{Signal, kill}, + unistd::Pid, +}; +use tokio::{ + process::{Child, Command}, + sync::Mutex, + time::{Duration, sleep, timeout}, +}; +use tracing::{info, warn}; + +#[derive(Debug, Default, FromArgs, PartialEq, Eq)] +/// Run the authentik server. +#[argh(subcommand, name = "server")] +#[expect( + clippy::empty_structs_with_brackets, + reason = "argh doesn't support unit structs" +)] +pub(crate) struct Cli {} pub(crate) fn socket_path() -> PathBuf { temp_dir().join("authentik.sock") } + +pub(crate) struct Server { + server: Mutex, +} + +impl Server { + async fn new() -> Result { + info!("starting server"); + + let server = if config::get().debug && which::which("authentik-server").is_err() { + let build_status = Command::new("go") + .args(["build", "-o", "server", "./cmd/server"]) + .stdin(Stdio::null()) + .status() + .await?; + if !build_status.success() { + return Err(eyre!("golang server failed to compile")); + } + Command::new("./server") + .kill_on_drop(true) + .stdin(Stdio::null()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .spawn()? + } else { + Command::new("authentik-server") + .kill_on_drop(true) + .stdin(Stdio::null()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .spawn()? + }; + + Ok(Self { + server: Mutex::new(server), + }) + } + + async fn shutdown(&self) -> Result<()> { + info!("shutting down server"); + let mut server = self.server.lock().await; + if let Some(id) = server.id() { + kill(Pid::from_raw(id.cast_signed()), Signal::SIGINT)?; + } + timeout(Duration::from_secs(1), server.wait()).await??; + Ok(()) + } + + async fn is_alive(&self) -> bool { + let try_wait = self.server.lock().await.try_wait(); + match try_wait { + Ok(Some(code)) => { + warn!(?code, "server has exited"); + false + } + Ok(None) => true, + Err(err) => { + warn!( + ?err, + "failed to check the status of server process, ignoring" + ); + true + } + } + } +} + +async fn watch_server(arbiter: Arbiter, server: Arc) -> Result<()> { + info!("starting server watcher"); + loop { + tokio::select! { + () = sleep(Duration::from_secs(5)) => { + if !server.is_alive().await { + return Err(eyre!("server has exited unexpectedly")); + } + } + () = arbiter.shutdown() => { + server.shutdown().await?; + return Ok(()); + } + } + } +} + +pub(crate) async fn start(_cli: Cli, tasks: &mut Tasks) -> Result> { + let arbiter = tasks.arbiter(); + + let server = Arc::new(Server::new().await?); + + tasks + .build_task() + .name(&format!("{}::watch_server", module_path!())) + .spawn(watch_server(arbiter.clone(), Arc::clone(&server)))?; + + Ok(server) +} diff --git a/src/worker/mod.rs b/src/worker/mod.rs index 65d92350f7..94db0ea0ac 100644 --- a/src/worker/mod.rs +++ b/src/worker/mod.rs @@ -343,14 +343,7 @@ pub(crate) fn start(_cli: Cli, tasks: &mut Tasks) -> Result> { let router = healthcheck::build_router(Arc::clone(&workers)); for addr in config::get().listen.http.iter().copied() { - ak_axum::server::start_plain( - tasks, - "worker", - router.clone(), - addr, - config::get().debug, /* Allow failure in case the server is running on the same - * machine, like in dev. */ - )?; + ak_axum::server::start_plain(tasks, "worker", router.clone(), addr)?; } ak_axum::server::start_unix( @@ -358,8 +351,6 @@ pub(crate) fn start(_cli: Cli, tasks: &mut Tasks) -> Result> { "worker", router, unix::net::SocketAddr::from_pathname(socket_path())?, - config::get().debug, /* Allow failure in case the server is running on the same - * machine, like in dev. */ )?; } diff --git a/website/docs/developer-docs/setup/full-dev-environment.mdx b/website/docs/developer-docs/setup/full-dev-environment.mdx index da52d31c89..4221cc8ef4 100644 --- a/website/docs/developer-docs/setup/full-dev-environment.mdx +++ b/website/docs/developer-docs/setup/full-dev-environment.mdx @@ -145,30 +145,24 @@ If you ever want to start over, use `make dev-reset`, which drops and restores t ## 5. Running authentik -Now that the backend has been set up and built, you can start authentik. In two different tabs in your terminal, run the following commands from the root of your installation directory: +Now that the backend has been set up and built, you can start authentik. Run the following command from the root of your installation directory: ```shell -make run-server -``` - -```shell -make run-worker +make run ``` :::info -The very first time a worker runs, it might need some time to clear the initial task queue. Adjust [`AUTHENTIK_WORKER__THREADS`](../../../install-config/configuration/#authentik_worker__threads) as required. +The very first time authentik runs, it might need some time to clear the initial task queue. Adjust [`AUTHENTIK_WORKER__THREADS`](../../../install-config/configuration/#authentik_worker__threads) as required. ::: -Both processes need to run to get a fully functioning authentik development environment. - ### Hot-reloading When `AUTHENTIK_DEBUG` is set to `true` (the default for the development environment), the authentik server automatically reloads whenever changes are made to the code. -For the authentik worker, install [watchexec](https://github.com/watchexec/watchexec), and run: +Install [watchexec](https://github.com/watchexec/watchexec), and run: ```shell -make run-worker-watch +make run-watch ``` ## 6. Build the frontend