diff --git a/nixos/doc/manual/release-notes/rl-2605.section.md b/nixos/doc/manual/release-notes/rl-2605.section.md index ceac6f600655..fc7801aa0c1c 100644 --- a/nixos/doc/manual/release-notes/rl-2605.section.md +++ b/nixos/doc/manual/release-notes/rl-2605.section.md @@ -55,6 +55,8 @@ of pulling the upstream container image from Docker Hub. If you want the old beh - The Bash implementation of the `nixos-rebuild` program is removed. All switchable systems now use the Python rewrite. Any prior usage of `system.rebuild.enableNg` must now be removed. If you have any outstanding issues with the new implementation, please open an issue on GitHub. +- `services.angrr` now uses TOML for configuration. Define policies with `services.angrr.settings` (generate TOML file) or point to a file using `services.angrr.configFile`. The legacy options `services.angrr.period`, `services.angrr.ownedOnly`, and `services.angrr.removeRoot` have been removed. See `man 5 angrr` and the description of `services.angrr.settings` options for examples and details. + - `services.pingvin-share` has been removed as the `pingvin-share.backend` package was broken and the project was archived upstream. ## Other Notable Changes {#sec-release-26.05-notable-changes} diff --git a/nixos/modules/services/misc/angrr.nix b/nixos/modules/services/misc/angrr.nix index c2ed4348a02d..92e371efdf2d 100644 --- a/nixos/modules/services/misc/angrr.nix +++ b/nixos/modules/services/misc/angrr.nix @@ -8,36 +8,242 @@ let cfg = config.services.angrr; direnvCfg = config.programs.direnv.angrr; + toml = pkgs.formats.toml { }; + exampleSettings = { + temporary-root-policies = { + direnv = { + path-regex = "/\\.direnv/"; + period = "14d"; + }; + result = { + path-regex = "/result[^/]*$"; + period = "3d"; + }; + }; + profile-policies = { + system = { + profile-paths = [ "/nix/var/nix/profiles/system" ]; + keep-since = "14d"; + keep-latest-n = 5; + keep-booted-system = true; + keep-current-system = true; + }; + user = { + enable = false; + profile-paths = [ + "~/.local/state/nix/profiles/profile" + "/nix/var/nix/profiles/per-user/root/profile" + ]; + keep-since = "1d"; + keep-latest-n = 1; + keep-booted-system = false; + keep-current-system = false; + }; + }; + }; + settingsOptions = { + freeformType = toml.type; + options = { + owned-only = lib.mkOption { + type = + with lib.types; + enum [ + "auto" + "true" + "false" + ]; + default = "auto"; + description = '' + Only monitors owned symbolic link target of GC roots. + + - "auto": behaves like true for normal users, false for root. + - "true": only monitor GC roots owned by the current user. + - "false": monitor all GC roots. + ''; + }; + temporary-root-policies = lib.mkOption { + type = with lib.types; attrsOf (submodule temporaryRootPolicyOptions); + default = { }; + description = '' + Policies for temporary GC roots(e.g. result and direnv). + ''; + }; + profile-policies = lib.mkOption { + type = with lib.types; attrsOf (submodule profilePolicyOptions); + default = { }; + description = '' + Profile GC root policies. + ''; + }; + touch = { + project-globs = lib.mkOption { + type = with lib.types; listOf str; + default = [ + "!.git" + ]; + description = '' + List of glob patterns to include or exclude files when touching GC roots. + + Only applied when `angrr touch` is invoked with the `--project` flag. + Patterns use an inverted gitignore-style semantics. + See . + ''; + }; + }; + }; + }; + commonPolicyOptions = { + options = { + enable = lib.mkEnableOption "this angrr policy" // { + default = true; + example = false; + }; + }; + }; + temporaryRootPolicyOptions = { + freeformType = toml.type; + imports = [ commonPolicyOptions ]; + options = { + path-regex = lib.mkOption { + type = lib.types.str; + description = '' + Regex pattern to match the GC root path. + ''; + }; + period = lib.mkOption { + type = with lib.types; nullOr str; + default = null; + description = '' + Retention period for the GC roots matched by this policy. + ''; + }; + priority = lib.mkOption { + type = lib.types.int; + default = 100; + description = '' + Priority of this policy. + + Lower number means higher priority, if multiple policies monitor the + same path, the one with higher priority will be applied. + ''; + }; + filter = lib.mkOption { + type = with lib.types; nullOr (submodule filterOptions); + default = null; + description = '' + External filter program to further filter GC roots matched by this policy. + ''; + }; + ignore-prefixes = lib.mkOption { + type = with lib.types; nullOr (listOf str); + default = null; + description = '' + List of path prefixes to ignore. + + If null is specified, angrr builtin settings will be used. + ''; + }; + ignore-prefixes-in-home = lib.mkOption { + type = with lib.types; nullOr (listOf str); + default = null; + description = '' + Path prefixes to ignore under home directory. + + If null is specified, angrr builtin settings will be used. + ''; + }; + }; + }; + profilePolicyOptions = { + freeformType = toml.type; + imports = [ commonPolicyOptions ]; + options = { + profile-paths = lib.mkOption { + type = with lib.types; listOf str; + description = '' + Paths to the Nix profile. + + When angrr runs in owned-only mode, and the option begins with `~`, + it will be expanded to the home directory of the current user. + + When angrr does not run in owned-only mode, and the option begins with `~`, + it will be expanded to the home of all users discovered respectively. + ''; + }; + keep-since = lib.mkOption { + type = with lib.types; nullOr str; + default = null; + description = '' + Retention period for the GC roots in this profile. + ''; + }; + keep-latest-n = lib.mkOption { + type = with lib.types; nullOr int; + default = null; + description = '' + Keep the latest N GC roots in this profile. + ''; + }; + keep-current-system = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Whether to keep the current system generation. Only useful for system profiles. + ''; + }; + keep-booted-system = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Whether to keep the last booted system generation. Only useful for system profiles. + ''; + }; + }; + }; + filterOptions = { + freeformType = toml.type; + options = { + program = lib.mkOption { + type = lib.types.str; + description = '' + Path to the external filter program. + ''; + }; + arguments = lib.mkOption { + type = with lib.types; listOf str; + default = [ ]; + description = '' + Extra command-line arguments pass to the external filter program. + ''; + }; + }; + }; + + # toml.generate does not support null values, we need to filter them out first + filteredSettings = lib.filterAttrsRecursive (name: value: value != null) cfg.settings; + originalConfigFile = toml.generate "angrr.toml" filteredSettings; + validatedConfigFile = pkgs.runCommand "angrr-config.toml" { } '' + ${lib.getExe cfg.package} validate --config "${originalConfigFile}" > $out + ''; + + configFileMigrationMsg = '' + This option has been removed since angrr 0.2.0. + Please use `services.angrr.settings` to configure retention policies through configuration file. + + See for a configuration example. + ''; in { meta.maintainers = pkgs.angrr.meta.maintainers; + imports = [ + (lib.mkRemovedOptionModule [ "services" "angrr" "period" ] configFileMigrationMsg) + (lib.mkRemovedOptionModule [ "services" "angrr" "removeRoot" ] configFileMigrationMsg) + (lib.mkRemovedOptionModule [ "services" "angrr" "ownedOnly" ] configFileMigrationMsg) + ]; options = { services.angrr = { enable = lib.mkEnableOption "angrr"; package = lib.mkPackageOption pkgs "angrr" { }; - period = lib.mkOption { - type = lib.types.str; - default = "7d"; - example = "2weeks"; - description = '' - The retention period of auto GC roots. - ''; - }; - removeRoot = lib.mkOption { - type = lib.types.bool; - default = false; - description = '' - Whether to pass the `--remove-root` option to angrr. - ''; - }; - ownedOnly = lib.mkOption { - type = lib.types.bool; - default = false; - description = '' - Control the `--remove-root=` option of angrr. - ''; - apply = b: if b then "true" else "false"; - }; logLevel = lib.mkOption { type = with lib.types; @@ -61,10 +267,28 @@ in Extra command-line arguments pass to angrr. ''; }; + settings = lib.mkOption { + type = lib.types.submodule settingsOptions; + example = exampleSettings; + description = '' + Global configuration for angrr in TOML format. + ''; + }; + configFile = lib.mkOption { + type = with lib.types; nullOr path; + default = validatedConfigFile; + defaultText = "TOML file generated from {option}`services.angrr.settings`"; + description = '' + Path to the angrr configuration file in TOML format. + + If not set, the configuration generated from {option}`services.angrr.settings` will be used. + If specified, {option}`services.angrr.settings` will be ignored. + ''; + }; enableNixGcIntegration = lib.mkOption { type = lib.types.bool; description = '' - Whether to enable nix-gc.service integration + Whether to enable nix-gc.service integration. ''; }; timer = { @@ -107,16 +331,17 @@ in } { + environment.etc."angrr/config.toml".source = cfg.configFile; + systemd.services.angrr = { description = "Auto Nix GC Roots Retention"; script = '' ${lib.getExe cfg.package} run \ --log-level "${cfg.logLevel}" \ - --period "${cfg.period}" \ - ${lib.optionalString cfg.removeRoot "--remove-root"} \ - --owned-only="${cfg.ownedOnly}" \ - --no-prompt ${lib.escapeShellArgs cfg.extraArgs} + --no-prompt \ + ${lib.escapeShellArgs cfg.extraArgs} ''; + environment.ANGRR_LOG_STYLE = "systemd"; serviceConfig = { Type = "oneshot"; }; @@ -144,7 +369,7 @@ in (lib.mkIf (config.programs.direnv.enable && direnvCfg.enable) { environment.etc."direnv/lib/angrr.sh".source = "${cfg.package}/share/direnv/lib/angrr.sh"; programs.direnv.direnvrcExtra = lib.mkIf direnvCfg.autoUse '' - use angrr + _angrr_auto_use "$@" ''; }) ] diff --git a/nixos/tests/angrr.nix b/nixos/tests/angrr.nix index 2c2baefd8e86..300f3643b0ba 100644 --- a/nixos/tests/angrr.nix +++ b/nixos/tests/angrr.nix @@ -1,11 +1,52 @@ -{ ... }: +{ pkgs, ... }: +let + drvForTest = + name: + pkgs.runCommand "angrr-test-${name}" { } '' + mkdir --parents "$out" + echo "${name}" >"$out/${name}" + ''; +in { name = "angrr"; nodes = { machine = { services.angrr = { enable = true; - period = "7d"; + settings = { + temporary-root-policies = { + result = { + path-regex = "/result[^/]*$"; + period = "7d"; + }; + direnv = { + path-regex = "/\\.direnv/"; + period = "14d"; + }; + }; + profile-policies = { + system = { + profile-paths = [ "/nix/var/nix/profiles/system" ]; + keep-since = "7d"; # do not keep based on time + keep-latest-n = 2; # keep latest + keep-current-system = true; + keep-booted-system = true; + }; + user = { + profile-paths = [ + "~/.local/state/nix/profiles/profile" + "/nix/var/nix/profiles/per-user/root/profile" + ]; + # keep-since = "0d"; # do not keep based on time + keep-latest-n = 2; + }; + }; + touch = { + project-globs = [ + "!result-glob-ignored" + ]; + }; + }; }; # `angrr.service` integrates to `nix-gc.service` by default nix.gc.automatic = true; @@ -19,7 +60,23 @@ # Test direnv integration programs.direnv.enable = true; # Verbose logging for angrr in direnv - environment.variables.ANGRR_DIRENV_LOG = "angrr=debug"; + environment.variables.ANGRR_DIRENV_LOG = "debug"; + + # Add some store paths to machine for test + environment.etc."drvs-for-test".text = '' + ${drvForTest "drv1"} + ${drvForTest "drv2"} + ${drvForTest "drv3"} + ${drvForTest "drv4"} + ${drvForTest "drv5"} + ${drvForTest "drv6"} + ${drvForTest "drv7"} + ${drvForTest "drv8"} + ${drvForTest "fake-booted-system"} + ''; + + # Unit start limit workaround + systemd.services.angrr.unitConfig.StartLimitBurst = 10; }; }; @@ -51,7 +108,7 @@ machine.succeed("touch /tmp/result-root-auto-gc-root-2 --no-dereference") machine.succeed("touch /tmp/result-user-auto-gc-root-2 --no-dereference") - machine.systemctl("start nix-gc.service") + machine.systemctl("start angrr.service") # Only GC roots `-1` are removed machine.succeed("test ! -e /tmp/result-root-auto-gc-root-1") machine.succeed("readlink /tmp/result-root-auto-gc-root-2") @@ -60,7 +117,7 @@ # Change time again machine.succeed("date -s '8 days'") - machine.systemctl("start nix-gc.service") + machine.systemctl("start angrr.service") # All auto GC roots are removed machine.succeed("test ! -e /tmp/result-root-auto-gc-root-2") machine.succeed("test ! -e /tmp/result-user-auto-gc-root-2") @@ -69,20 +126,115 @@ machine.succeed("mkdir /tmp/test-direnv") machine.succeed("echo >/tmp/test-direnv/.envrc") # Simply create an empty .envrc machine.succeed("nix build /run/current-system --out-link /tmp/test-direnv/.direnv/gc-root") + machine.succeed("nix build /run/current-system --out-link /tmp/test-direnv/result") machine.succeed("cd /tmp/test-direnv; direnv allow; direnv exec . true") # The root will be removed if we does not use the direnv recently - machine.succeed("date -s '8 days'") - machine.systemctl("start nix-gc.service") + machine.succeed("date -s '15 days'") + machine.systemctl("start angrr.service") machine.succeed("test ! -e /tmp/test-direnv/.direnv/gc-root") + machine.succeed("test ! -e /tmp/test-direnv/result") # Recreate the root machine.succeed("nix build /run/current-system --out-link /tmp/test-direnv/.direnv/gc-root") + machine.succeed("nix build /run/current-system --out-link /tmp/test-direnv/result") + machine.succeed("nix build /run/current-system --out-link /tmp/test-direnv/result-glob-ignored") + machine.succeed("nix build /run/current-system --out-link /tmp/test-outside-direnv/result") # The root will not be remove if we use the direnv recently + machine.succeed("date -s '15 days'") + # test the case that $PWD is different from project root + machine.succeed("cd /tmp; direnv exec /tmp/test-direnv true") + machine.systemctl("start angrr.service") + machine.succeed("readlink /tmp/test-direnv/.direnv/gc-root") + machine.succeed("readlink /tmp/test-direnv/result") + machine.succeed("test ! -e /tmp/test-direnv/result-glob-ignored") + machine.succeed("test ! -e /tmp/test-outside-direnv/result") + + # System profile policy test + # Create a profile for test + machine.succeed("mkdir /tmp/profile-test") + machine.succeed("nix-env --profile /nix/var/nix/profiles/system --set ${drvForTest "drv1"}") # generation 1 + machine.succeed("nix-env --profile /nix/var/nix/profiles/system --set ${drvForTest "drv2"}") # generation 2 + machine.succeed("nix-env --profile /nix/var/nix/profiles/system --set ${drvForTest "drv3"}") # generation 3 + machine.succeed("ln --symbolic --force --no-dereference ${drvForTest "fake-booted-system"} /run/booted-system") + machine.succeed("nix-env --profile /nix/var/nix/profiles/system --set /run/booted-system") # generation 4 + machine.succeed("nix-env --profile /nix/var/nix/profiles/system --set /run/current-system") # generation 5 machine.succeed("date -s '8 days'") - machine.succeed("cd /tmp/test-direnv; direnv exec . true") - machine.systemctl("start nix-gc.service") - machine.succeed("readlink /tmp/test-direnv/.direnv/gc-root") + machine.succeed("nix-env --profile /nix/var/nix/profiles/system --set ${drvForTest "drv4"}") # generation 6 + machine.succeed("nix-env --profile /nix/var/nix/profiles/system --set ${drvForTest "drv5"}") # generation 7 + machine.succeed("nix-env --profile /nix/var/nix/profiles/system --set ${drvForTest "drv6"}") # generation 8 + machine.succeed("nix-env --profile /nix/var/nix/profiles/system --set ${drvForTest "drv7"}") # generation 9 + machine.succeed("nix-env --profile /nix/var/nix/profiles/system --set ${drvForTest "drv8"}") # generation 10 + # Rollback to generation 2 to simulate current system + for _ in range(0, 10 - 2): + machine.succeed("nix-env --rollback --profile /nix/var/nix/profiles/system") + + # Run policy + machine.systemctl("start angrr.service") + + # Test + machine.succeed("sh -c 'test $(readlink /nix/var/nix/profiles/system) = system-2-link'") + machine.succeed("test ! -e /nix/var/nix/profiles/system-1-link") + machine.succeed("readlink /nix/var/nix/profiles/system-2-link") # Keep since it is current generation + machine.succeed("test ! -e /nix/var/nix/profiles/system-3-link") + machine.succeed("readlink /nix/var/nix/profiles/system-4-link") # Keep by keep-booted-system + machine.succeed("readlink /nix/var/nix/profiles/system-5-link") # Keep by keep-current-system + machine.succeed("readlink /nix/var/nix/profiles/system-6-link") # Keep by keep-since + machine.succeed("readlink /nix/var/nix/profiles/system-7-link") # Keep by keep-since + machine.succeed("readlink /nix/var/nix/profiles/system-8-link") # Keep by keep-since + machine.succeed("readlink /nix/var/nix/profiles/system-9-link") # Keep by keep-latest-n + machine.succeed("readlink /nix/var/nix/profiles/system-10-link") # Keep by keep-latest-n + + # User profile policy test 1 + # Normal user + machine.succeed("su normal --command 'nix profile add ${drvForTest "drv1"}'") + machine.succeed("su normal --command 'nix profile add ${drvForTest "drv2"}'") + machine.succeed("su normal --command 'nix profile add ${drvForTest "drv3"}'") + # Root user + machine.succeed("nix profile add ${drvForTest "drv1"}") + machine.succeed("nix profile add ${drvForTest "drv2"}") + machine.succeed("nix profile add ${drvForTest "drv3"}") + + # Run policy + machine.systemctl("start angrr.service") + + # Test + machine.succeed("sh -c 'test $(readlink ~normal/.local/state/nix/profiles/profile) = profile-3-link'") + machine.succeed("test ! -e ~normal/.local/state/nix/profiles/profile-1-link") + machine.succeed("readlink ~normal/.local/state/nix/profiles/profile-2-link") # Keep by keep-latest-n + machine.succeed("readlink ~normal/.local/state/nix/profiles/profile-3-link") # Keep since it is current generation + machine.succeed("sh -c 'test $(readlink /nix/var/nix/profiles/per-user/root/profile) = profile-3-link'") + machine.succeed("test ! -e /nix/var/nix/profiles/per-user/root/profile-1-link") + machine.succeed("readlink /nix/var/nix/profiles/per-user/root/profile-2-link") # Keep by keep-latest-n + machine.succeed("readlink /nix/var/nix/profiles/per-user/root/profile-3-link") # Keep since it is current generation + + # User profile policy test 2 + # Create GC roots again + machine.succeed("su normal --command 'nix profile add ${drvForTest "drv1"}'") + machine.succeed("su normal --command 'nix profile add ${drvForTest "drv2"}'") + machine.succeed("su normal --command 'nix profile add ${drvForTest "drv3"}'") + machine.succeed("nix profile add ${drvForTest "drv1"}") + machine.succeed("nix profile add ${drvForTest "drv2"}") + machine.succeed("nix profile add ${drvForTest "drv3"}") + + # Run policy in owned-only mode as normal user + machine.succeed("su normal --command 'angrr run --no-prompt'") + + # Test + machine.succeed("sh -c 'test $(readlink ~normal/.local/state/nix/profiles/profile) = profile-6-link'") + machine.succeed("test ! -e ~normal/.local/state/nix/profiles/profile-1-link") + machine.succeed("test ! -e ~normal/.local/state/nix/profiles/profile-2-link") + machine.succeed("test ! -e ~normal/.local/state/nix/profiles/profile-3-link") + machine.succeed("test ! -e ~normal/.local/state/nix/profiles/profile-4-link") + machine.succeed("readlink ~normal/.local/state/nix/profiles/profile-5-link") # Keep by keep-latest-n + machine.succeed("readlink ~normal/.local/state/nix/profiles/profile-6-link") # Keep since it is current generation + machine.succeed("sh -c 'test $(readlink /nix/var/nix/profiles/per-user/root/profile) = profile-6-link'") + machine.succeed("test ! -e /nix/var/nix/profiles/per-user/root/profile-1-link") + machine.succeed("readlink /nix/var/nix/profiles/per-user/root/profile-2-link") + machine.succeed("readlink /nix/var/nix/profiles/per-user/root/profile-3-link") + machine.succeed("readlink /nix/var/nix/profiles/per-user/root/profile-4-link") # Not monitored + machine.succeed("readlink /nix/var/nix/profiles/per-user/root/profile-5-link") # Not monitored + machine.succeed("readlink /nix/var/nix/profiles/per-user/root/profile-6-link") # Not monitored ''; } diff --git a/pkgs/by-name/an/angrr/package.nix b/pkgs/by-name/an/angrr/package.nix index 36ec20e30d36..8c1d4c2c4367 100644 --- a/pkgs/by-name/an/angrr/package.nix +++ b/pkgs/by-name/an/angrr/package.nix @@ -1,30 +1,33 @@ { lib, - stdenv, rustPlatform, fetchFromGitHub, installShellFiles, nixosTests, testers, nix-update-script, + go-md2man, }: rustPlatform.buildRustPackage (finalAttrs: { pname = "angrr"; - version = "0.1.5"; + version = "0.2.0"; src = fetchFromGitHub { owner = "linyinfeng"; repo = "angrr"; tag = "v${finalAttrs.version}"; - hash = "sha256-PT3oCNPRvEroyVNiICeO0hSHDzKUC6KcP9HnIw1kMQE="; + hash = "sha256-Z+B0MO5ZoPJveO571mlzNVedBEac7P4RE7Cq8e/9bJk="; }; - cargoHash = "sha256-lDOH4Ceap69fX6VWbgQoQfmYWZI+jPE0LJiXmqrTRn8="; + cargoHash = "sha256-j36vyfIP63Qmd55vaVb9buqrCItXwFalelzU8BlKm9s="; buildAndTestSubdir = "angrr"; - nativeBuildInputs = [ installShellFiles ]; + nativeBuildInputs = [ + go-md2man + installShellFiles + ]; postBuild = '' mkdir --parents build/{man-pages,shell-completions} cargo xtask man-pages --out build/man-pages @@ -50,7 +53,7 @@ rustPlatform.buildRustPackage (finalAttrs: { }; meta = { - description = "Temporary GC Roots Cleaner"; + description = "Auto Nix GC Root Retention"; homepage = "https://github.com/linyinfeng/angrr"; license = [ lib.licenses.mit ]; maintainers = with lib.maintainers; [ yinfeng ];