Unverified Commit 62ea5b9a authored by Lin Yinfeng's avatar Lin Yinfeng
Browse files

angrr: 0.1.5 -> 0.2.0

angrr now uses TOML configuration, and also adds the ability to define
profile policies.

1. Update the package itself.
2. Update the NixOS module to create, validate, and install config file.
3. Update the NixOS test of angrr to test new functionalities.
parent a317f674
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -53,6 +53,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}
+247 −22
Original line number Diff line number Diff line
@@ -8,36 +8,242 @@
let
  cfg = config.services.angrr;
  direnvCfg = config.programs.direnv.angrr;
in
{
  meta.maintainers = pkgs.angrr.meta.maintainers;
  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 = {
    services.angrr = {
      enable = lib.mkEnableOption "angrr";
      package = lib.mkPackageOption pkgs "angrr" { };
      period = lib.mkOption {
      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 <https://docs.rs/ignore/latest/ignore/overrides/struct.OverrideBuilder.html#method.add>.
          '';
        };
      };
    };
  };
  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;
        default = "7d";
        example = "2weeks";
        description = ''
          The retention period of auto GC roots.
          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.
        '';
      };
      removeRoot = lib.mkOption {
    };
  };
  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 pass the `--remove-root` option to angrr.
          Whether to keep the current system generation. Only useful for system profiles.
        '';
      };
      ownedOnly = lib.mkOption {
      keep-booted-system = lib.mkOption {
        type = lib.types.bool;
        default = false;
        description = ''
          Control the `--remove-root=<true|false>` option of angrr.
          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.
        '';
        apply = b: if b then "true" else "false";
      };
      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 <https://github.com/linyinfeng/angrr/tree/main?tab=readme-ov-file#nixos-module-usage> 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" { };
      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 "$@"
        '';
      })
    ]
+162 −10
Original line number Diff line number Diff line
{ ... }:
{ 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;
        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 '8 days'")
    machine.succeed("cd /tmp/test-direnv; direnv exec . true")
    machine.systemctl("start nix-gc.service")
    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("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
  '';
}
+9 −6
Original line number Diff line number Diff line
{
  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 ];