From 7d46509d83e9737cbb4786183df09bb29e23745c Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Wed, 23 Sep 2026 05:13:05 -0500 Subject: [PATCH] borgmatic: migrate backups to native settings Add programs.borgmatic.backups..settings for native borgmatic YAML and generate each backup file from it. The location, storage, retention, and consistency options and the section extraConfig options become deprecated aliases into settings. Warnings name the affected backup. Legacy configurations without overlapping keys produce the same YAML, and Home Manager symlink exclusions still append to exclude_from. Drop the assertions that rejected backups setting both or neither of sourceDirectories and patterns, so native source patterns and database-only backups work. --- docs/release-notes/rl-2611.md | 16 + ...2026-09-21_00-00-00-borgmatic-settings.nix | 12 + modules/programs/borgmatic.nix | 406 +++++++++--------- .../borgmatic/basic-configuration.nix | 45 +- .../both-sourcedirectories-and-patterns.nix | 46 +- tests/modules/programs/borgmatic/default.nix | 5 +- .../exclude-hm-symlinks-nothing-else.nix | 17 +- .../borgmatic/exclude-hm-symlinks.nix | 18 +- .../borgmatic/include-hm-symlinks.nix | 18 +- .../programs/borgmatic/legacy-defaults.nix | 134 ++++++ .../programs/borgmatic/native-settings.nix | 74 ++++ ...neither-sourcedirectories-nor-patterns.nix | 18 - .../borgmatic/patterns-configuration.nix | 22 +- .../borgmatic/settings-assertions.nix | 45 ++ .../borgmatic/settings-compatibility.nix | 279 ++++++++++++ tests/modules/programs/borgmatic/warnings.nix | 21 + 16 files changed, 940 insertions(+), 236 deletions(-) create mode 100644 modules/misc/news/2026/09/2026-09-21_00-00-00-borgmatic-settings.nix create mode 100644 tests/modules/programs/borgmatic/legacy-defaults.nix create mode 100644 tests/modules/programs/borgmatic/native-settings.nix delete mode 100644 tests/modules/programs/borgmatic/neither-sourcedirectories-nor-patterns.nix create mode 100644 tests/modules/programs/borgmatic/settings-assertions.nix create mode 100644 tests/modules/programs/borgmatic/settings-compatibility.nix create mode 100644 tests/modules/programs/borgmatic/warnings.nix diff --git a/docs/release-notes/rl-2611.md b/docs/release-notes/rl-2611.md index 603fabf1b9..dc2efe3f44 100644 --- a/docs/release-notes/rl-2611.md +++ b/docs/release-notes/rl-2611.md @@ -63,6 +63,22 @@ This release has the following notable changes: `services.grobi.settings.rules`. The deprecated aliases preserve list order and definition priorities. The generated JSON is now pretty-printed. +- Borgmatic backups now use + [](#opt-programs.borgmatic.backups._name_.settings) for native YAML settings. + Deprecated options warn with the backup name and move to their snake_case + keys, such as `retention.keepDaily` to `settings.keep_daily`. Repository + strings become `{ path = "..."; }`, and each section's `extraConfig` moves + directly into `settings`. `location.excludeHomeManagerSymlinks` remains + available. + + Legacy configurations without overlapping keys keep their generated YAML, + except that null check frequencies are now omitted. Overlapping + `extraConfig` keys no longer take the last section's value. Distinct + scalars at the same priority conflict and lists concatenate, so resolve + overlaps in `settings`. Backups no longer need exactly one of + `sourceDirectories` and `patterns`, which allows native patterns and + database-only backups. `repositories` is still required. + ## State Version Changes {#sec-release-26.11-state-version-changes} The state version in this release includes the changes below. These diff --git a/modules/misc/news/2026/09/2026-09-21_00-00-00-borgmatic-settings.nix b/modules/misc/news/2026/09/2026-09-21_00-00-00-borgmatic-settings.nix new file mode 100644 index 0000000000..9e966f72d1 --- /dev/null +++ b/modules/misc/news/2026/09/2026-09-21_00-00-00-borgmatic-settings.nix @@ -0,0 +1,12 @@ +{ config, ... }: +{ + time = "2026-09-21T00:00:00+00:00"; + condition = config.programs.borgmatic.enable; + message = '' + Borgmatic backups now use `programs.borgmatic.backups..settings` + for native YAML settings. Legacy `location`, `storage`, `retention`, and + `consistency` options (except `location.excludeHomeManagerSymlinks`) and + the six `extraConfig` options remain as deprecated aliases and warn when + used. Move their values into `settings`. + ''; +} diff --git a/modules/programs/borgmatic.nix b/modules/programs/borgmatic.nix index 9d073e2e7b..104e804679 100644 --- a/modules/programs/borgmatic.nix +++ b/modules/programs/borgmatic.nix @@ -33,20 +33,6 @@ let removeNullValues repo ) repos; - mkRetentionOption = - frequency: - mkNullableOption { - type = types.int; - description = "Number of ${frequency} archives to keep. Use -1 for no limit."; - example = 3; - }; - - extraConfigOption = mkOption { - inherit (yamlFormat) type; - default = { }; - description = "Extra settings."; - }; - repositoryOption = types.submodule { options = { path = mkOption { @@ -88,143 +74,202 @@ let }; }; + repositoriesOption = mkOption { + type = types.nullOr (types.listOf (types.either types.str repositoryOption)); + default = [ ]; + apply = lib.mapNullable cleanRepositories; + example = literalExpression '' + [ + { + "path" = "ssh://myuser@myrepo.myserver.com/./repo"; + "label" = "server"; + } + { + "path" = "/var/lib/backups/local.borg"; + "label" = "local"; + } + ] + ''; + description = "List of local or remote repositories with paths and optional labels."; + }; + + checksOption = mkOption { + type = types.listOf consistencyCheckModule; + default = [ ]; + description = "Consistency checks to run"; + example = literalExpression '' + [ + { name = "repository"; frequency = "2 weeks"; } + { name = "archives"; frequency = "4 weeks"; } + { name = "data"; frequency = "6 weeks"; } + { name = "extract"; frequency = "6 weeks"; } + ] + ''; + }; + configModule = types.submodule ( - { config, ... }: - { - config.location.extraConfig.exclude_from = lib.mkIf config.location.excludeHomeManagerSymlinks ( - lib.mkAfter [ (toString hmExcludeFile) ] - ); - options = { - location = { - sourceDirectories = mkNullableOption { - type = types.listOf types.str; - default = null; - description = '' - Directories to backup. - - Mutually exclusive with [](#opt-programs.borgmatic.backups._name_.location.patterns). - ''; - example = literalExpression "[config.home.homeDirectory]"; - }; - - patterns = mkNullableOption { - type = types.listOf types.str; - default = null; - description = '' - Patterns to include/exclude. - - See the output of `borg help patterns` for the syntax. Pattern paths - are relative to `/` even when a different recursion root is set. - - Mutually exclusive with [](#opt-programs.borgmatic.backups._name_.location.sourceDirectories). - ''; - example = [ - "R /home/user" - "- home/user/.cache" - "- home/user/Downloads" - "+ home/user/Videos/Important Video" - "- home/user/Videos" - ]; - }; - - repositories = mkOption { - type = types.listOf (types.either types.str repositoryOption); - apply = cleanRepositories; - example = literalExpression '' - [ - { - "path" = "ssh://myuser@myrepo.myserver.com/./repo"; - "label" = "server"; - } - { - "path" = "/var/lib/backups/local.borg"; - "label" = "local"; - } - ] - ''; - description = '' - List of local or remote repositories with paths and optional labels. - ''; - }; - - excludeHomeManagerSymlinks = mkOption { - type = types.bool; - description = '' - Whether to exclude Home Manager generated symbolic links from - the backups. This facilitates restoring the whole home - directory when the Nix store doesn't contain the latest - Home Manager generation. - ''; - default = false; - example = true; - }; - - extraConfig = extraConfigOption; - }; - - storage = { - encryptionPasscommand = mkNullableOption { - type = types.str; - description = "Command writing the passphrase to standard output."; - example = literalExpression ''"''${pkgs.pass}/bin/pass borg-repo"''; - }; - extraConfig = extraConfigOption; - }; - - retention = { - keepWithin = mkNullableOption { - type = types.strMatching "[[:digit:]]+[Hdwmy]"; - description = "Keep all archives within this time interval."; - example = "2d"; - }; - - keepSecondly = mkRetentionOption "secondly"; - keepMinutely = mkRetentionOption "minutely"; - keepHourly = mkRetentionOption "hourly"; - keepDaily = mkRetentionOption "daily"; - keepWeekly = mkRetentionOption "weekly"; - keepMonthly = mkRetentionOption "monthly"; - keepYearly = mkRetentionOption "yearly"; - - extraConfig = extraConfigOption; - }; - - consistency = { - checks = mkOption { - type = types.listOf consistencyCheckModule; - default = [ ]; - description = "Consistency checks to run"; - example = literalExpression '' - [ - { - name = "repository"; - frequency = "2 weeks"; - } - { - name = "archives"; - frequency = "4 weeks"; - } - { - name = "data"; - frequency = "6 weeks"; - } - { - name = "extract"; - frequency = "6 weeks"; - } + { config, options, ... }: + let + overlays = + map + ( + section: + lib.hm.deprecations.mkSettingsOverlay { + inherit options; + from = [ + section + "extraConfig" ]; - ''; + to = [ "settings" ]; + } + ) + [ + "location" + "storage" + "retention" + "consistency" + "output" + "hooks" + ]; + overlayKeys = lib.concatMap (overlay: overlay.keys) overlays; + in + { + imports = + lib.hm.deprecations.mkSettingsRenamedOptionModules [ ] [ "settings" ] + { + priority = 1400; + preserveOrder = true; + } + [ + { + old = [ + "location" + "sourceDirectories" + ]; + new = "source_directories"; + fallback = null; + shadowed = lib.elem "source_directories" overlayKeys; + } + { + old = [ + "location" + "patterns" + ]; + new = "patterns"; + fallback = null; + shadowed = lib.elem "patterns" overlayKeys; + } + { + old = [ + "storage" + "encryptionPasscommand" + ]; + new = "encryption_passcommand"; + fallback = null; + shadowed = lib.elem "encryption_passcommand" overlayKeys; + } + ] + ++ + lib.hm.deprecations.mkSettingsRenamedOptionModules [ "retention" ] [ "settings" ] + { priority = 1400; } + ( + map + (name: { + old = name; + new = lib.hm.strings.toSnakeCase name; + fallback = null; + shadowed = lib.elem (lib.hm.strings.toSnakeCase name) overlayKeys; + }) + [ + "keepWithin" + "keepSecondly" + "keepMinutely" + "keepHourly" + "keepDaily" + "keepWeekly" + "keepMonthly" + "keepYearly" + ] + ) + ++ [ + (lib.hm.deprecations.mkSettingsChangedOptionModule { + from = [ + "location" + "repositories" + ]; + to = [ "settings" ]; + key = "repositories"; + priority = 1400; + applyDefault = value: value != [ ]; + oldOption = repositoriesOption; + shadowed = lib.elem "repositories" overlayKeys; + convert = lib.id; + }) + (lib.hm.deprecations.mkSettingsChangedOptionModule { + from = [ + "consistency" + "checks" + ]; + to = [ "settings" ]; + key = "checks"; + priority = 1400; + applyDefault = value: value != [ ]; + oldOption = checksOption; + shadowed = lib.elem "checks" overlayKeys; + convert = map removeNullValues; + }) + ] + ++ map (overlay: overlay.module) overlays; + + options = { + warnings = mkOption { + type = types.listOf types.str; + default = [ ]; + internal = true; + visible = false; + }; + + settings = mkOption { + type = types.submodule { + freeformType = yamlFormat.type; + config = { + # Legacy backups always wrote an empty checks list, even if checks was unset. + checks = lib.mkIf (config.warnings != [ ]) (lib.mkOptionDefault [ ]); + exclude_from = lib.mkIf config.location.excludeHomeManagerSymlinks ( + lib.mkAfter [ (toString hmExcludeFile) ] + ); + }; }; + default = { }; + description = '' + Native borgmatic YAML settings for this backup. Deprecated section + options are forwarded into settings and emit migration warnings. + Define new configuration here to avoid the legacy aliases. - extraConfig = extraConfigOption; + Home Manager exclusions append to `exclude_from`; use `lib.mkForce` + to replace the list. + + Top-level null values are omitted, so enable hooks that take no + options, such as `zfs`, `btrfs`, or `lvm`, with `{ }` instead of + `null`. Deprecated aliases define settings at normal priority, so + a `lib.mkDefault` on the whole settings value is ignored while any + alias is set; apply it to individual keys instead. + + See + for the available settings. + ''; }; - output = { - extraConfig = extraConfigOption; - }; - - hooks = { - extraConfig = extraConfigOption; + location.excludeHomeManagerSymlinks = mkOption { + type = types.bool; + description = '' + Whether to exclude Home Manager generated symbolic links from + the backups. This facilitates restoring the whole home + directory when the Nix store doesn't contain the latest + Home Manager generation. + ''; + default = false; + example = true; }; }; } @@ -239,33 +284,6 @@ let ''; hmExcludePatterns = lib.concatMapStrings hmExcludePattern hmSymlinks; hmExcludeFile = pkgs.writeText "hm-symlinks.txt" hmExcludePatterns; - - writeConfig = - config: - lib.generators.toYAML { } ( - removeNullValues ( - { - source_directories = config.location.sourceDirectories; - inherit (config.location) patterns repositories; - checks = map removeNullValues config.consistency.checks; - encryption_passcommand = config.storage.encryptionPasscommand; - keep_within = config.retention.keepWithin; - keep_secondly = config.retention.keepSecondly; - keep_minutely = config.retention.keepMinutely; - keep_hourly = config.retention.keepHourly; - keep_daily = config.retention.keepDaily; - keep_weekly = config.retention.keepWeekly; - keep_monthly = config.retention.keepMonthly; - keep_yearly = config.retention.keepYearly; - } - // config.location.extraConfig - // config.storage.extraConfig - // config.retention.extraConfig - // config.consistency.extraConfig - // config.output.extraConfig - // config.hooks.extraConfig - ) - ); in { meta.maintainers = [ lib.maintainers.DamienCassou ]; @@ -284,17 +302,13 @@ in ''; example = literalExpression '' { - personal = { - location = { - sourceDirectories = [ "/home/me/personal" ]; - repositories = [ "ssh://myuser@myserver.com/./personal-repo" ]; - }; + personal.settings = { + source_directories = [ "/home/me/personal" ]; + repositories = [ { path = "ssh://myuser@myserver.com/./personal-repo"; } ]; }; - work = { - location = { - sourceDirectories = [ "/home/me/work" ]; - repositories = [ "ssh://myuser@myserver.com/./work-repo" ]; - }; + work.settings = { + source_directories = [ "/home/me/work" ]; + repositories = [ { path = "ssh://myuser@myserver.com/./work-repo"; } ]; }; }; ''; @@ -303,26 +317,26 @@ in }; config = lib.mkIf cfg.enable { - assertions = - (lib.mapAttrsToList (backup: opts: { - assertion = opts.location.sourceDirectories == null || opts.location.patterns == null; - message = '' - Borgmatic backup configuration "${backup}" cannot specify both 'location.sourceDirectories' and 'location.patterns'. - ''; - }) cfg.backups) - ++ (lib.mapAttrsToList (backup: opts: { - assertion = !(opts.location.sourceDirectories == null && opts.location.patterns == null); - message = '' - Borgmatic backup configuration "${backup}" must specify one of 'location.sourceDirectories' or 'location.patterns'. - ''; - }) cfg.backups); + warnings = lib.concatLists ( + lib.mapAttrsToList ( + name: backup: + map (warning: "Borgmatic backup `programs.borgmatic.backups.${name}`: ${warning}") backup.warnings + ) cfg.backups + ); + + assertions = lib.mapAttrsToList (backup: opts: { + assertion = (opts.settings.repositories or null) != null; + message = '' + Borgmatic backup configuration "${backup}" must specify 'settings.repositories' (or the deprecated 'location.repositories'). + ''; + }) cfg.backups; xdg.configFile = with lib.attrsets; mapAttrs' ( configName: config: nameValuePair ("borgmatic.d/" + configName + ".yaml") { - text = writeConfig config; + text = lib.generators.toYAML { } (removeNullValues config.settings); } ) cfg.backups; diff --git a/tests/modules/programs/borgmatic/basic-configuration.nix b/tests/modules/programs/borgmatic/basic-configuration.nix index 3cdef2420d..ef51a2839f 100644 --- a/tests/modules/programs/borgmatic/basic-configuration.nix +++ b/tests/modules/programs/borgmatic/basic-configuration.nix @@ -1,4 +1,9 @@ -{ config, realPkgs, ... }: +{ + config, + lib, + realPkgs, + ... +}: let @@ -75,6 +80,44 @@ in }; }; + test.asserts.warnings.expected = (import ./warnings.nix { inherit lib; }) { + file = ./basic-configuration.nix; + entries = [ + { from = "hooks.extraConfig"; } + { from = "output.extraConfig"; } + { from = "consistency.extraConfig"; } + { from = "retention.extraConfig"; } + { from = "storage.extraConfig"; } + { from = "location.extraConfig"; } + { + from = "consistency.checks"; + to = "settings.checks"; + changed = true; + } + { + from = "location.repositories"; + to = "settings.repositories"; + changed = true; + } + { + from = "retention.keepSecondly"; + to = "settings.keep_secondly"; + } + { + from = "retention.keepWithin"; + to = "settings.keep_within"; + } + { + from = "storage.encryptionPasscommand"; + to = "settings.encryption_passcommand"; + } + { + from = "location.sourceDirectories"; + to = "settings.source_directories"; + } + ]; + }; + nmt.script = '' config_file=$TESTED/home-files/.config/borgmatic.d/main.yaml assertFileExists $config_file diff --git a/tests/modules/programs/borgmatic/both-sourcedirectories-and-patterns.nix b/tests/modules/programs/borgmatic/both-sourcedirectories-and-patterns.nix index d3a477f30b..6d24a40250 100644 --- a/tests/modules/programs/borgmatic/both-sourcedirectories-and-patterns.nix +++ b/tests/modules/programs/borgmatic/both-sourcedirectories-and-patterns.nix @@ -1,23 +1,37 @@ +{ lib, realPkgs, ... }: { programs.borgmatic = { enable = true; - backups = { - main = { - location = { - sourceDirectories = [ "/my-stuff-to-backup" ]; - patterns = [ - "R /" - "+ my-stuff-to-backup" - ]; - repositories = [ "/mnt/disk1" ]; - }; - }; + backups.main.location = { + sourceDirectories = [ "/my-stuff-to-backup" ]; + extraConfig.patterns = [ "- **/.cache" ]; + repositories = [ "/mnt/disk1" ]; }; }; - test.asserts.assertions.expected = [ - '' - Borgmatic backup configuration "main" cannot specify both 'location.sourceDirectories' and 'location.patterns'. - '' - ]; + test.asserts.warnings.expected = (import ./warnings.nix { inherit lib; }) { + file = ./both-sourcedirectories-and-patterns.nix; + entries = [ + { from = "location.extraConfig"; } + { + from = "location.repositories"; + to = "settings.repositories"; + changed = true; + } + { + from = "location.sourceDirectories"; + to = "settings.source_directories"; + } + ]; + }; + + nmt.script = '' + config_file=$TESTED/home-files/.config/borgmatic.d/main.yaml + assertFileExists "$config_file" + ${realPkgs.jq}/bin/jq --exit-status ' + .source_directories == ["/my-stuff-to-backup"] and + .patterns == ["- **/.cache"] and + .repositories == [{"path": "/mnt/disk1"}] + ' "$config_file" + ''; } diff --git a/tests/modules/programs/borgmatic/default.nix b/tests/modules/programs/borgmatic/default.nix index e0faadf210..8aa2cc91e2 100644 --- a/tests/modules/programs/borgmatic/default.nix +++ b/tests/modules/programs/borgmatic/default.nix @@ -2,8 +2,11 @@ borgmatic-program-basic-configuration = ./basic-configuration.nix; borgmatic-program-patterns-configuration = ./patterns-configuration.nix; borgmatic-program-both-sourcedirectories-and-patterns = ./both-sourcedirectories-and-patterns.nix; - borgmatic-program-neither-sourcedirectories-nor-patterns = ./neither-sourcedirectories-nor-patterns.nix; borgmatic-program-include-hm-symlinks = ./include-hm-symlinks.nix; borgmatic-program-exclude-hm-symlinks = ./exclude-hm-symlinks.nix; borgmatic-program-exclude-hm-symlinks-nothing-else = ./exclude-hm-symlinks-nothing-else.nix; + borgmatic-program-native-settings = ./native-settings.nix; + borgmatic-program-legacy-defaults = ./legacy-defaults.nix; + borgmatic-program-settings-compatibility = ./settings-compatibility.nix; + borgmatic-program-settings-assertions = ./settings-assertions.nix; } diff --git a/tests/modules/programs/borgmatic/exclude-hm-symlinks-nothing-else.nix b/tests/modules/programs/borgmatic/exclude-hm-symlinks-nothing-else.nix index e04d051577..04e4ebb349 100644 --- a/tests/modules/programs/borgmatic/exclude-hm-symlinks-nothing-else.nix +++ b/tests/modules/programs/borgmatic/exclude-hm-symlinks-nothing-else.nix @@ -1,4 +1,4 @@ -{ realPkgs, ... }: +{ lib, realPkgs, ... }: { programs.borgmatic = { @@ -14,6 +14,21 @@ }; }; + test.asserts.warnings.expected = (import ./warnings.nix { inherit lib; }) { + file = ./exclude-hm-symlinks-nothing-else.nix; + entries = [ + { + from = "location.repositories"; + to = "settings.repositories"; + changed = true; + } + { + from = "location.sourceDirectories"; + to = "settings.source_directories"; + } + ]; + }; + nmt.script = '' config_file=$TESTED/home-files/.config/borgmatic.d/main.yaml assertFileExists $config_file diff --git a/tests/modules/programs/borgmatic/exclude-hm-symlinks.nix b/tests/modules/programs/borgmatic/exclude-hm-symlinks.nix index e284f3ce3d..89ceb6ac92 100644 --- a/tests/modules/programs/borgmatic/exclude-hm-symlinks.nix +++ b/tests/modules/programs/borgmatic/exclude-hm-symlinks.nix @@ -1,4 +1,4 @@ -{ realPkgs, ... }: +{ lib, realPkgs, ... }: let @@ -22,6 +22,22 @@ in }; }; + test.asserts.warnings.expected = (import ./warnings.nix { inherit lib; }) { + file = ./exclude-hm-symlinks.nix; + entries = [ + { from = "location.extraConfig"; } + { + from = "location.repositories"; + to = "settings.repositories"; + changed = true; + } + { + from = "location.sourceDirectories"; + to = "settings.source_directories"; + } + ]; + }; + nmt.script = '' config_file=$TESTED/home-files/.config/borgmatic.d/main.yaml assertFileExists $config_file diff --git a/tests/modules/programs/borgmatic/include-hm-symlinks.nix b/tests/modules/programs/borgmatic/include-hm-symlinks.nix index 757a6b724b..8a3b6fb95d 100644 --- a/tests/modules/programs/borgmatic/include-hm-symlinks.nix +++ b/tests/modules/programs/borgmatic/include-hm-symlinks.nix @@ -1,4 +1,4 @@ -{ realPkgs, ... }: +{ lib, realPkgs, ... }: let @@ -22,6 +22,22 @@ in }; }; + test.asserts.warnings.expected = (import ./warnings.nix { inherit lib; }) { + file = ./include-hm-symlinks.nix; + entries = [ + { from = "location.extraConfig"; } + { + from = "location.repositories"; + to = "settings.repositories"; + changed = true; + } + { + from = "location.sourceDirectories"; + to = "settings.source_directories"; + } + ]; + }; + nmt.script = '' config_file=$TESTED/home-files/.config/borgmatic.d/main.yaml assertFileExists $config_file diff --git a/tests/modules/programs/borgmatic/legacy-defaults.nix b/tests/modules/programs/borgmatic/legacy-defaults.nix new file mode 100644 index 0000000000..04788d4c95 --- /dev/null +++ b/tests/modules/programs/borgmatic/legacy-defaults.nix @@ -0,0 +1,134 @@ +{ lib, realPkgs, ... }: +let + expectWarnings = import ./warnings.nix { inherit lib; }; + # Option-default definitions merge with the option's own default, so the + # warning names the module as well. + optionDefaultFiles = [ + ../../../../modules/programs/borgmatic.nix + ./legacy-defaults.nix + ]; +in +{ + programs.borgmatic = { + enable = true; + package = null; + backups = { + legacy.location = { + sourceDirectories = [ "/source" ]; + repositories = [ { path = "/repo"; } ]; + }; + legacyNull = { + location = { + sourceDirectories = lib.mkForce null; + patterns = [ "R /" ]; + repositories = [ "/repo" ]; + }; + retention.keepDaily = lib.mkForce null; + }; + native.settings = { + source_directories = [ "/source" ]; + repositories = [ { path = "/repo"; } ]; + }; + optionDefault = { + location = { + sourceDirectories = [ "/source" ]; + repositories = lib.mkOptionDefault [ "/repo" ]; + }; + consistency.checks = lib.mkOptionDefault [ + { + name = "data"; + frequency = "always"; + } + ]; + }; + }; + }; + + test.asserts.warnings.expected = + expectWarnings { + file = ./legacy-defaults.nix; + backup = "legacy"; + entries = [ + { + from = "location.repositories"; + to = "settings.repositories"; + changed = true; + } + { + from = "location.sourceDirectories"; + to = "settings.source_directories"; + } + ]; + } + ++ expectWarnings { + file = ./legacy-defaults.nix; + backup = "legacyNull"; + entries = [ + { + from = "location.repositories"; + to = "settings.repositories"; + changed = true; + } + { + from = "retention.keepDaily"; + to = "settings.keep_daily"; + } + { + from = "location.patterns"; + to = "settings.patterns"; + } + { + from = "location.sourceDirectories"; + to = "settings.source_directories"; + } + ]; + } + ++ expectWarnings { + file = ./legacy-defaults.nix; + backup = "optionDefault"; + entries = [ + { + from = "consistency.checks"; + to = "settings.checks"; + changed = true; + files = optionDefaultFiles; + } + { + from = "location.repositories"; + to = "settings.repositories"; + changed = true; + files = optionDefaultFiles; + } + { + from = "location.sourceDirectories"; + to = "settings.source_directories"; + } + ]; + }; + + nmt.script = '' + legacy=$TESTED/home-files/.config/borgmatic.d/legacy.yaml + legacyNull=$TESTED/home-files/.config/borgmatic.d/legacyNull.yaml + native=$TESTED/home-files/.config/borgmatic.d/native.yaml + optionDefault=$TESTED/home-files/.config/borgmatic.d/optionDefault.yaml + assertFileExists "$legacy" + assertFileExists "$legacyNull" + assertFileExists "$native" + assertFileExists "$optionDefault" + ${realPkgs.jq}/bin/jq --exit-status ' + .source_directories == ["/source"] and + .repositories == [{"path": "/repo"}] and + .checks == [] + ' "$legacy" + ${realPkgs.jq}/bin/jq --exit-status ' + (has("source_directories") | not) and + (has("keep_daily") | not) and + .patterns == ["R /"] + ' "$legacyNull" + ${realPkgs.jq}/bin/jq --exit-status 'has("checks") | not' "$native" + ${realPkgs.jq}/bin/jq --exit-status ' + .repositories == [{"path": "/repo"}] and + .checks == [{"name": "data", "frequency": "always"}] + ' "$optionDefault" + ''; +} diff --git a/tests/modules/programs/borgmatic/native-settings.nix b/tests/modules/programs/borgmatic/native-settings.nix new file mode 100644 index 0000000000..a54bf8fe40 --- /dev/null +++ b/tests/modules/programs/borgmatic/native-settings.nix @@ -0,0 +1,74 @@ +{ lib, realPkgs, ... }: +{ + programs.borgmatic = { + enable = true; + backups.databaseOnly.settings = { + repositories = [ { path = "/mnt/disk1"; } ]; + postgresql_databases = [ { name = "app"; } ]; + }; + backups.native.settings = lib.mkDefault ( + lib.mkMerge [ + { + source_directories = [ "/native" ]; + repositories = [ { path = "/native-repository"; } ]; + encryption_passcommand = "pass native"; + checks = [ { name = "repository"; } ]; + keep_daily = 7; + constants.first = "one"; + } + { constants.second = "two"; } + ] + ); + backups.hm = { + settings = { + source_directories = [ "/hm" ]; + repositories = [ { path = "/hm-repository"; } ]; + exclude_from = [ "/native-exclude" ]; + }; + location.excludeHomeManagerSymlinks = true; + }; + backups.hmForced = { + settings = { + repositories = [ { path = "/hm-repository"; } ]; + exclude_from = lib.mkForce [ ]; + }; + location.excludeHomeManagerSymlinks = true; + }; + }; + + test.asserts.warnings.expected = [ ]; + + nmt.script = '' + config_file=$TESTED/home-files/.config/borgmatic.d/databaseOnly.yaml + assertFileExists "$config_file" + ${realPkgs.jq}/bin/jq --exit-status ' + (has("source_directories") | not) and + (has("patterns") | not) and + .postgresql_databases == [{"name": "app"}] and + .repositories == [{"path": "/mnt/disk1"}] + ' "$config_file" + + native=$TESTED/home-files/.config/borgmatic.d/native.yaml + jq=${realPkgs.jq}/bin/jq + + assertFileExists $native + $jq --exit-status ' + .source_directories == ["/native"] and + .repositories == [{"path": "/native-repository"}] and + .encryption_passcommand == "pass native" and + .checks == [{"name": "repository"}] and + .keep_daily == 7 and + .constants == {"first": "one", "second": "two"} + ' $native + hm=$TESTED/home-files/.config/borgmatic.d/hm.yaml + assertFileExists $hm + $jq --exit-status ' + (.exclude_from | length) == 2 and .exclude_from[0] == "/native-exclude" + ' $hm + grep --quiet '/home/hm-user/.config/borgmatic.d/hm.yaml' "$($jq --raw-output '.exclude_from[1]' $hm)" + + hm_forced=$TESTED/home-files/.config/borgmatic.d/hmForced.yaml + assertFileExists $hm_forced + $jq --exit-status '.exclude_from == []' $hm_forced + ''; +} diff --git a/tests/modules/programs/borgmatic/neither-sourcedirectories-nor-patterns.nix b/tests/modules/programs/borgmatic/neither-sourcedirectories-nor-patterns.nix deleted file mode 100644 index 90361297d0..0000000000 --- a/tests/modules/programs/borgmatic/neither-sourcedirectories-nor-patterns.nix +++ /dev/null @@ -1,18 +0,0 @@ -{ - programs.borgmatic = { - enable = true; - backups = { - main = { - location = { - repositories = [ "/mnt/disk1" ]; - }; - }; - }; - }; - - test.asserts.assertions.expected = [ - '' - Borgmatic backup configuration "main" must specify one of 'location.sourceDirectories' or 'location.patterns'. - '' - ]; -} diff --git a/tests/modules/programs/borgmatic/patterns-configuration.nix b/tests/modules/programs/borgmatic/patterns-configuration.nix index ab555f73e5..fcc5cbf92a 100644 --- a/tests/modules/programs/borgmatic/patterns-configuration.nix +++ b/tests/modules/programs/borgmatic/patterns-configuration.nix @@ -1,4 +1,9 @@ -{ config, realPkgs, ... }: +{ + config, + lib, + realPkgs, + ... +}: let @@ -23,6 +28,21 @@ in }; }; + test.asserts.warnings.expected = (import ./warnings.nix { inherit lib; }) { + file = ./patterns-configuration.nix; + entries = [ + { + from = "location.repositories"; + to = "settings.repositories"; + changed = true; + } + { + from = "location.patterns"; + to = "settings.patterns"; + } + ]; + }; + nmt.script = '' config_file=$TESTED/home-files/.config/borgmatic.d/main.yaml assertFileExists $config_file diff --git a/tests/modules/programs/borgmatic/settings-assertions.nix b/tests/modules/programs/borgmatic/settings-assertions.nix new file mode 100644 index 0000000000..9deb74401b --- /dev/null +++ b/tests/modules/programs/borgmatic/settings-assertions.nix @@ -0,0 +1,45 @@ +{ lib, realPkgs, ... }: +{ + programs.borgmatic = { + enable = true; + package = null; + backups = { + missingRepository.settings.source_directories = [ "/source" ]; + missingLegacyRepository.location.sourceDirectories = [ "/source" ]; + effective.settings = { + repositories = [ { path = "/repo"; } ]; + postgresql_databases = [ { name = "native"; } ]; + }; + }; + }; + test.asserts.assertions.expected = [ + '' + Borgmatic backup configuration "missingLegacyRepository" must specify 'settings.repositories' (or the deprecated 'location.repositories'). + '' + '' + Borgmatic backup configuration "missingRepository" must specify 'settings.repositories' (or the deprecated 'location.repositories'). + '' + ]; + + test.asserts.warnings.expected = (import ./warnings.nix { inherit lib; }) { + file = ./settings-assertions.nix; + backup = "missingLegacyRepository"; + entries = [ + { + from = "location.sourceDirectories"; + to = "settings.source_directories"; + } + ]; + }; + + nmt.script = '' + config_file=$TESTED/home-files/.config/borgmatic.d/effective.yaml + assertFileExists "$config_file" + ${realPkgs.jq}/bin/jq --exit-status ' + (has("source_directories") | not) and + (has("patterns") | not) and + .postgresql_databases == [{"name": "native"}] and + .repositories == [{"path": "/repo"}] + ' "$config_file" + ''; +} diff --git a/tests/modules/programs/borgmatic/settings-compatibility.nix b/tests/modules/programs/borgmatic/settings-compatibility.nix new file mode 100644 index 0000000000..2bb6be8290 --- /dev/null +++ b/tests/modules/programs/borgmatic/settings-compatibility.nix @@ -0,0 +1,279 @@ +{ + config, + lib, + realPkgs, + ... +}: +let + base = { + source_directories = [ "/source" ]; + repositories = [ { path = "/repo"; } ]; + checks = [ ]; + }; + cases = { + basic = { + repoDefinitions = 2; + backup = { + location.repositories = [ + "/repo" + { + path = "/labeled"; + label = "remote"; + } + ]; + storage.encryptionPasscommand = "pass borg"; + retention = { + keepWithin = "2d"; + keepSecondly = 2; + keepMinutely = 3; + keepHourly = 4; + keepDaily = 5; + keepWeekly = 6; + keepMonthly = 7; + keepYearly = 8; + }; + }; + expected = { + repositories = [ + { path = "/repo"; } + { + path = "/labeled"; + label = "remote"; + } + { path = "/repo"; } + ]; + encryption_passcommand = "pass borg"; + keep_within = "2d"; + keep_secondly = 2; + keep_minutely = 3; + keep_hourly = 4; + keep_daily = 5; + keep_weekly = 6; + keep_monthly = 7; + keep_yearly = 8; + }; + warnings = [ + { + from = "retention.keepYearly"; + to = "settings.keep_yearly"; + } + { + from = "retention.keepMonthly"; + to = "settings.keep_monthly"; + } + { + from = "retention.keepWeekly"; + to = "settings.keep_weekly"; + } + { + from = "retention.keepDaily"; + to = "settings.keep_daily"; + } + { + from = "retention.keepHourly"; + to = "settings.keep_hourly"; + } + { + from = "retention.keepMinutely"; + to = "settings.keep_minutely"; + } + { + from = "retention.keepSecondly"; + to = "settings.keep_secondly"; + } + { + from = "retention.keepWithin"; + to = "settings.keep_within"; + } + { + from = "storage.encryptionPasscommand"; + to = "settings.encryption_passcommand"; + } + ]; + }; + derivation = + let + script = realPkgs.writeShellScript "borgmatic-ssh" ''exec ssh "$@"''; + in + { + backup.storage.extraConfig.ssh_command = script; + expected.ssh_command = toString script; + warnings = [ { from = "storage.extraConfig"; } ]; + }; + checks = { + checkDefinitions = 2; + backup.consistency.checks = [ + { + name = "repository"; + frequency = "always"; + } + { name = "data"; } + ]; + expected.checks = [ + { + name = "repository"; + frequency = "always"; + } + { name = "data"; } + ]; + }; + sections = { + backup = { + location.extraConfig.one_file_system = true; + storage.extraConfig.checkpoint_interval = 200; + retention.extraConfig.prefix = "hostname"; + consistency.extraConfig.archive_name_format = "archive"; + output.extraConfig.color = false; + hooks.extraConfig.before_actions = [ "echo start" ]; + }; + expected = { + one_file_system = true; + checkpoint_interval = 200; + prefix = "hostname"; + archive_name_format = "archive"; + color = false; + before_actions = [ "echo start" ]; + }; + warnings = map (section: { from = "${section}.extraConfig"; }) [ + "hooks" + "output" + "consistency" + "retention" + "storage" + "location" + ]; + }; + overlayTyped = { + backup = { + retention.keepDaily = lib.mkForce 30; + location.extraConfig.keep_daily = lib.mkDefault 2; + }; + expected.keep_daily = 2; + warnings = [ + { from = "location.extraConfig"; } + { + from = "retention.keepDaily"; + to = "settings.keep_daily"; + } + ]; + }; + nativePriority = { + backup = { + retention.keepDaily = 30; + settings.keep_daily = 7; + }; + expected.keep_daily = 7; + warnings = [ + { + from = "retention.keepDaily"; + to = "settings.keep_daily"; + } + ]; + }; + overlayCollision = { + backup = { + settings.exclude_patterns = [ "- /native" ]; + location.extraConfig.exclude_patterns = [ "- /legacy" ]; + }; + expected.exclude_patterns = [ + "- /native" + "- /legacy" + ]; + warnings = [ { from = "location.extraConfig"; } ]; + }; + legacyOrder = { + checkDefinitions = 3; + backup.consistency.checks = lib.mkMerge [ + (lib.mkAfter [ + { + name = "data"; + frequency = "always"; + } + ]) + (lib.mkBefore [ { name = "repository"; } ]) + ]; + expected.checks = [ + { name = "repository"; } + { + name = "data"; + frequency = "always"; + } + ]; + }; + }; + expected = case: base // case.expected; + warningEntries = + case: + let + entries = lib.partition (entry: lib.hasSuffix ".extraConfig" entry.from) (case.warnings or [ ]); + in + entries.right + ++ [ + { + from = "consistency.checks"; + to = "settings.checks"; + changed = true; + definitions = case.checkDefinitions or 1; + } + { + from = "location.repositories"; + to = "settings.repositories"; + changed = true; + definitions = case.repoDefinitions or 1; + } + ] + ++ entries.wrong + ++ [ + { + from = "location.sourceDirectories"; + to = "settings.source_directories"; + } + ]; +in +{ + programs.borgmatic = { + enable = true; + package = null; + backups = lib.mapAttrs ( + _: case: + lib.mkMerge [ + { + location = { + sourceDirectories = [ "/source" ]; + repositories = [ "/repo" ]; + }; + consistency.checks = [ ]; + } + case.backup + ] + ) cases; + }; + + test.asserts.warnings.expected = lib.concatLists ( + lib.mapAttrsToList ( + name: case: + (import ./warnings.nix { inherit lib; }) { + file = ./settings-compatibility.nix; + backup = name; + entries = warningEntries case; + } + ) cases + ); + + assertions = lib.mapAttrsToList (name: case: { + assertion = + builtins.toJSON config.programs.borgmatic.backups.${name}.settings + == builtins.toJSON (expected case); + message = "Unexpected canonical settings for borgmatic ${name}"; + }) cases; + + nmt.script = lib.concatStrings ( + lib.mapAttrsToList (name: case: '' + file=$TESTED/home-files/.config/borgmatic.d/${name}.yaml + assertFileExists "$file" + ${realPkgs.jq}/bin/jq --exit-status \ + --argjson expected ${lib.escapeShellArg (builtins.toJSON (expected case))} \ + '. == $expected' "$file" + '') cases + ); +} diff --git a/tests/modules/programs/borgmatic/warnings.nix b/tests/modules/programs/borgmatic/warnings.nix new file mode 100644 index 0000000000..02e0ccce25 --- /dev/null +++ b/tests/modules/programs/borgmatic/warnings.nix @@ -0,0 +1,21 @@ +{ lib }: +{ + file, + entries, + backup ? "main", +}: +map ( + entry: + let + to = entry.to or "settings"; + files = lib.concatMapStringsSep " and " (path: "`${toString path}'") ( + entry.files or (lib.replicate (entry.definitions or 1) file) + ); + suffix = + if entry.changed or false then + "has been changed to `${to}' that has a different type. Please read `${to}' documentation and update your configuration accordingly." + else + "has been renamed to `${to}'."; + in + "Borgmatic backup `programs.borgmatic.backups.${backup}`: The option `${entry.from}' defined in ${files} ${suffix}" +) entries