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