# Commit-confirm for deployments. `router-arm` starts a timer; unless # `router-confirm` stops it in time, `router-rollback` re-activates the last # confirmed generation. Pair it with `nixos-rebuild test` (activate without # changing the boot default): a reboot also comes back to the confirmed system. { config, lib, pkgs, ... }: let cfg = config.router.rollback; profile = "/nix/var/nix/profiles/system"; tools = [ pkgs.coreutils config.systemd.package ]; # The confirmed system: the profile, or what booted if there is none. confirmed = '' target=${profile} [ -e "$target/bin/switch-to-configuration" ] || target=/run/booted-system ''; rollback = pkgs.writeShellApplication { name = "router-rollback"; runtimeInputs = tools; text = '' ${confirmed} echo "router: rolling back to $(readlink -f "$target")" # A failing hook must not stop the rollback. hook() { : ${cfg.onRollback} } hook || echo "router: a rollback hook failed" >&2 exec "$target/bin/switch-to-configuration" test ''; }; arm = pkgs.writeShellApplication { name = "router-arm"; runtimeInputs = tools; text = '' timeout=''${1:-${cfg.timeout}} systemctl stop router-rollback.timer router-rollback.service 2>/dev/null || true systemd-run --quiet --unit=router-rollback --on-active="$timeout" \ --timer-property=AccuracySec=1s ${rollback}/bin/router-rollback echo "router: rollback in $timeout unless confirmed (router-confirm)" ''; }; confirm = pkgs.writeShellApplication { name = "router-confirm"; runtimeInputs = tools; text = '' systemctl stop router-rollback.timer current=$(readlink -f /run/current-system) ${config.nix.package}/bin/nix-env -p ${profile} --set "$current" "$current/bin/switch-to-configuration" boot ${cfg.onConfirm} echo "router: confirmed $current" ''; }; in { options.router.rollback = { enable = lib.mkOption { type = lib.types.bool; default = true; description = "Install the commit-confirm helpers `router-arm`, `router-confirm` and `router-rollback`."; }; onConfirm = lib.mkOption { type = lib.types.lines; default = ""; internal = true; description = "Shell commands `router-confirm` runs once the current system is confirmed."; }; onRollback = lib.mkOption { type = lib.types.lines; default = ""; internal = true; description = "Shell commands `router-rollback` runs before it re-activates the confirmed system."; }; timeout = lib.mkOption { type = lib.types.str; default = "5min"; example = "90s"; description = "How long a deployment may stay unconfirmed before it is rolled back (systemd time span)."; }; }; config = lib.mkIf (config.router.enable && cfg.enable) { environment.systemPackages = [ arm confirm rollback ]; }; }