borgmatic: migrate backups to native settings

Add programs.borgmatic.backups.<name>.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.
This commit is contained in:
Austin Horstman
2026-09-23 05:13:05 -05:00
parent 31830be4fc
commit 7d46509d83
16 changed files with 940 additions and 236 deletions

View File

@@ -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

View File

@@ -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.<name>.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`.
'';
}

View File

@@ -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 <https://github.com/borgmatic-collective/borgmatic/blob/main/borgmatic/config/schema.yaml>
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;

View File

@@ -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

View File

@@ -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"
'';
}

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"
'';
}

View File

@@ -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
'';
}

View File

@@ -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'.
''
];
}

View File

@@ -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

View File

@@ -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"
'';
}

View File

@@ -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
);
}

View File

@@ -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