C2(authentik-source-build): impl assemble components into container image

Wire webui → authentik-django → authentik-server and replace
pkgs.authentik with custom source-built derivations. The ak wrapper
sets PATH/VIRTUAL_ENV and delegates to lifecycle/ak. Tested on
ringtail with nix-build test-build.nix -A assembled.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Erich Blume 2026-03-01 13:19:09 -08:00
commit 6ea5011739
3 changed files with 68 additions and 17 deletions

View file

@ -9,13 +9,20 @@
#
# autoPatchelfHook restores RPATHs that were stripped in the FOD.
#
# Optional input: webui derivation. When provided, resolves @webui@ store
# path placeholders in Python source. When null (default), leaves placeholders
# for isolated testing.
#
# Output:
# $out/bin/python3.14 venv python (symlink to nix python314)
# $out/lib/python3.14/site-packages/ all Python packages
# $out/lifecycle/ lifecycle scripts (symlink)
# $out/blueprints/ YAML blueprints
# $out/manage.py Django management script
{ pkgs ? import <nixpkgs> { }, sources ? import ./sources.nix { inherit pkgs; } }:
{ pkgs ? import <nixpkgs> { }
, sources ? import ./sources.nix { inherit pkgs; }
, webui ? null
}:
let
python-deps = import ./python-deps.nix { inherit pkgs sources; };
@ -28,6 +35,9 @@ let
hash = "sha256-Q6SJed0K6eIrqQ9mNAD4RGx+YCJvnI5E+0KGp5fBtTU=";
};
# When webui is provided, resolve paths directly; otherwise use placeholder
webuiPath = if webui != null then "${webui}" else "@webui@";
sp = "$out/lib/python3.14/site-packages";
in
@ -122,9 +132,9 @@ pkgs.stdenv.mkDerivation {
# Web asset paths: placeholder @webui@ for Go server card to resolve
substituteInPlace ${sp}/authentik/stages/email/utils.py \
--replace-fail 'Path("web/icons/icon_left_brand.png")' \
'Path("@webui@/icons/icon_left_brand.png")' \
'Path("${webuiPath}/icons/icon_left_brand.png")' \
--replace-fail 'Path("web/dist/assets/icons/icon_left_brand.png")' \
'Path("@webui@/dist/assets/icons/icon_left_brand.png")'
'Path("${webuiPath}/dist/assets/icons/icon_left_brand.png")'
# Lifecycle bash script: use Nix store bash (no /usr/bin/env in containers)
substituteInPlace ${sp}/lifecycle/ak \

View file

@ -1,19 +1,39 @@
# Nix-built Authentik identity provider
# Uses nixpkgs authentik package (ak entrypoint wrapping Go server + Python worker)
# Built with dockerTools.buildLayeredImage for efficient layer caching
# Nix-built Authentik identity provider (from source)
#
# Assembles four component derivations into a container image:
# 1. webui — Lit frontend (esbuild + rollup)
# 2. authentik-django — Python backend + lifecycle scripts
# 3. authentik-server — Go HTTP server binary
# 4. ak wrapper — sets PATH/VIRTUAL_ENV, delegates to lifecycle/ak
#
# Built with dockerTools.buildLayeredImage for efficient layer caching.
{ pkgs ? import <nixpkgs> { } }:
let
# Wrapper entrypoint that sets up /blueprints symlinks before running ak.
# buildLayeredImage's extraCommands can't access store paths from contents (they're
# in separate layers), so we create the symlinks at container start instead.
sources = import ./sources.nix { inherit pkgs; };
webui = import ./webui.nix { inherit pkgs sources; };
authentik-django = import ./authentik-django.nix { inherit pkgs sources webui; };
authentik-server = import ./authentik-server.nix { inherit pkgs sources authentik-django webui; };
# Wrapper that provides bin/ak with the correct runtime environment.
# lifecycle/ak dispatches: "server" → Go binary, "worker"/"migrate"/etc → Python.
ak = pkgs.writeShellScriptBin "ak" ''
export PYTHONDONTWRITEBYTECODE=1
export PATH="${authentik-server}/bin:${authentik-django}/bin:$PATH"
export VIRTUAL_ENV="${authentik-django}"
cd "${authentik-django}"
exec "${authentik-django}/lifecycle/ak" "$@"
'';
# Container entrypoint: symlink built-in blueprints then run ak.
# buildLayeredImage's extraCommands can't access store paths from contents
# (they're in separate layers), so we create the symlinks at container start.
entrypoint = pkgs.writeShellScript "authentik-entrypoint" ''
# Link built-in blueprint dirs from the Nix store into /blueprints
for item in /nix/store/*authentik-django*/blueprints/*/; do
for item in ${authentik-django}/blueprints/*/; do
name=$(basename "$item")
[ ! -e "/blueprints/$name" ] && ln -s "$item" "/blueprints/$name" 2>/dev/null || true
done
exec ${pkgs.authentik}/bin/ak "$@"
exec ${ak}/bin/ak "$@"
'';
in
@ -22,7 +42,9 @@ pkgs.dockerTools.buildLayeredImage {
tag = "latest";
contents = [
pkgs.authentik
ak
authentik-django
authentik-server
pkgs.bashInteractive
pkgs.coreutils
pkgs.cacert
@ -30,9 +52,8 @@ pkgs.dockerTools.buildLayeredImage {
];
# Create /blueprints as world-writable so user 65534 can create symlinks at runtime.
# The nixpkgs authentik-django package hardcodes blueprints_dir to its Nix store path,
# making custom blueprints mounted at /blueprints/custom invisible. The entrypoint
# wrapper populates this directory with symlinks to built-in blueprints on each start.
# authentik-django hardcodes blueprints_dir to $out/blueprints; the AUTHENTIK_BLUEPRINTS_DIR
# env var overrides it to /blueprints, where custom blueprints are mounted by k8s ConfigMap.
extraCommands = ''
mkdir -p blueprints
chmod 777 blueprints

View file

@ -9,16 +9,36 @@
# nix-build test-build.nix -A authentik-server --extra-experimental-features 'nix-command flakes'
# nix-build test-build.nix -A webui-deps --extra-experimental-features 'nix-command flakes'
# nix-build test-build.nix -A webui --extra-experimental-features 'nix-command flakes'
# nix-build test-build.nix -A assembled --extra-experimental-features 'nix-command flakes'
let
pkgs = (builtins.getFlake "nixpkgs").legacyPackages.x86_64-linux;
sources = import ./sources.nix { inherit pkgs; };
# Individual components (isolated, no cross-wiring)
_webui = import ./webui.nix { inherit pkgs sources; };
# Fully wired assembly (webui → authentik-django → authentik-server)
_authentik-django-assembled = import ./authentik-django.nix { inherit pkgs sources; webui = _webui; };
_authentik-server-assembled = import ./authentik-server.nix {
inherit pkgs sources;
authentik-django = _authentik-django-assembled;
webui = _webui;
};
in
{
# Individual component builds (for debugging in isolation)
python-deps = import ./python-deps.nix { inherit pkgs sources; };
authentik-django = import ./authentik-django.nix { inherit pkgs sources; };
client-go = import ./client-go.nix { inherit pkgs sources; };
client-ts = import ./client-ts.nix { inherit pkgs sources; };
authentik-server = import ./authentik-server.nix { inherit pkgs sources; };
webui-deps = import ./webui-deps.nix { inherit pkgs sources; };
webui = import ./webui.nix { inherit pkgs sources; };
webui = _webui;
# Fully assembled stack — tests that all components wire together
assembled = pkgs.linkFarm "authentik-assembled-${sources.version}" [
{ name = "authentik-django"; path = _authentik-django-assembled; }
{ name = "authentik-server"; path = _authentik-server-assembled; }
{ name = "webui"; path = _webui; }
];
}