diff --git a/modules/blocks/postgresql.nix b/modules/blocks/postgresql.nix index 0ade725..4bb1ea5 100644 --- a/modules/blocks/postgresql.nix +++ b/modules/blocks/postgresql.nix @@ -52,35 +52,23 @@ in databasebackup = lib.mkOption { description = '' - Backup configuration. This is an output option. - - Use it to initialize a block implementing the "backup" contract. - For example, with the restic block: - - ``` - shb.restic.instances."postgresql" = { - request = config.shb.postgresl.backup; - settings = { - enable = true; - }; - }; - ``` + Backup configuration. ''; - type = contracts.databasebackup.request; + type = lib.types.submodule { + options = contracts.databasebackup.mkRequester { + user = "postgres"; - default = { - user = "postgres"; + backupName = "postgres.sql"; - backupName = "postgres.sql"; + backupCmd = '' + ${pkgs.postgresql}/bin/pg_dumpall | ${pkgs.gzip}/bin/gzip --rsyncable + ''; - backupCmd = '' - ${pkgs.postgresql}/bin/pg_dumpall | ${pkgs.gzip}/bin/gzip --rsyncable - ''; - - restoreCmd = '' - ${pkgs.gzip}/bin/gunzip | ${pkgs.postgresql}/bin/psql postgres - ''; + restoreCmd = '' + ${pkgs.gzip}/bin/gunzip | ${pkgs.postgresql}/bin/psql postgres + ''; + }; }; }; @@ -181,5 +169,10 @@ in (upgrade-script 15 16) ]; } + { + # Seems superfluous but otherwise we get: + # The option `shb.postgresql.databasebackup' was accessed but has no value defined. + shb.postgresql.databasebackup = {}; + } ]); } diff --git a/modules/blocks/restic.nix b/modules/blocks/restic.nix index 6d38e8e..783ef38 100644 --- a/modules/blocks/restic.nix +++ b/modules/blocks/restic.nix @@ -137,17 +137,7 @@ in description = "Databases to backup following the [database backup contract](./contracts-databasebackup.html)."; default = {}; type = attrsOf (submodule ({ name, config, ... }: { - options = { - request = mkOption { - description = '' - Request part of the backup contract. - - Accepts values from a requester. - ''; - - type = contracts.databasebackup.request; - }; - + options = contracts.databasebackup.mkProvider { settings = mkOption { description = '' Settings specific to the Restic provider. @@ -158,26 +148,12 @@ in }; }; - result = mkOption { - description = '' - Result part of the backup contract. + resultCfg = { + restoreScript = fullName name config.settings.repository; + restoreScriptText = "${fullName "" { path = "path/to/repository"; }}"; - Contains the output of the Restic provider. - ''; - default = { - restoreScript = fullName name config.settings.repository; - backupService = "${fullName name config.settings.repository}.service"; - }; - defaultText = { - restoreScriptText = "${fullName "" { path = "path/to/repository"; }}"; - backupServiceText = "${fullName "" { path = "path/to/repository"; }}.service"; - }; - type = contracts.databasebackup.result { - restoreScript = fullName name config.settings.repository; - backupService = "${fullName name config.settings.repository}.service"; - restoreScriptText = "${fullName "" { path = "path/to/repository"; }}"; - backupServiceText = "${fullName "" { path = "path/to/repository"; }}.service"; - }; + backupService = "${fullName name config.settings.repository}.service"; + backupServiceText = "${fullName "" { path = "path/to/repository"; }}.service"; }; }; })); diff --git a/modules/contracts/databasebackup.nix b/modules/contracts/databasebackup.nix index b7d4b6c..19a07dd 100644 --- a/modules/contracts/databasebackup.nix +++ b/modules/contracts/databasebackup.nix @@ -1,87 +1,160 @@ -{ lib, ... }: +{ pkgs, lib, ... }: let - inherit (lib) mkIf mkOption literalExpression; - inherit (lib.types) anything submodule str; + inherit (lib) mkOption literalExpression literalMD optionalAttrs optionalString; + inherit (lib.types) submodule str; + + shblib = pkgs.callPackage ../../lib {}; + inherit (shblib) anyNotNull; in { - request = submodule { - options = { - user = mkOption { - description = '' - Unix user doing the backups. + mkRequest = + { user ? "root", + userText ? null, + backupName ? "dump", + backupNameText ? null, + backupCmd ? "", + backupCmdText ? null, + restoreCmd ? "", + restoreCmdText ? null, + }: mkOption { + description = '' + Request part of the backup contract. - This should be an admin user having access to all databases. - ''; - type = str; - example = "postgres"; + Options set by the requester module + enforcing how to backup files. + ''; + + default = { + inherit user backupName backupCmd restoreCmd; }; - backupName = mkOption { - description = "Name of the backup in the repository."; - type = str; - default = "dump"; - example = "postgresql.sql"; - }; + defaultText = optionalString (anyNotNull [ + userText + backupNameText + backupCmdText + restoreCmdText + ]) (literalMD '' + { + user = ${if userText != null then userText else user}; + backupName = ${if backupNameText != null then backupNameText else backupName}; + backupCmd = ${if backupCmdText != null then backupCmdText else backupCmd}; + restoreCmd = ${if restoreCmdText != null then restoreCmdText else restoreCmd}; + } + ''); - backupCmd = mkOption { - description = "Command that produces the database dump on stdout."; - type = str; - example = literalExpression '' - ''${pkgs.postgresql}/bin/pg_dumpall | ''${pkgs.gzip}/bin/gzip --rsyncable - ''; - }; + type = submodule { + options = { + user = mkOption { + description = '' + Unix user doing the backups. - restoreCmd = mkOption { - description = "Command that reads the database dump on stdin and restores the database."; - type = str; - example = literalExpression '' - ''${pkgs.gzip}/bin/gunzip | ''${pkgs.postgresql}/bin/psql postgres - ''; + This should be an admin user having access to all databases. + ''; + type = str; + example = "postgres"; + default = user; + } // optionalAttrs (userText != null) { + defaultText = literalMD userText; + }; + + backupName = mkOption { + description = "Name of the backup in the repository."; + type = str; + example = "postgresql.sql"; + default = backupName; + } // optionalAttrs (backupNameText != null) { + defaultText = literalMD backupNameText; + }; + + backupCmd = mkOption { + description = "Command that produces the database dump on stdout."; + type = str; + example = literalExpression '' + ''${pkgs.postgresql}/bin/pg_dumpall | ''${pkgs.gzip}/bin/gzip --rsyncable + ''; + default = backupCmd; + } // optionalAttrs (backupCmdText != null) { + defaultText = literalMD backupCmdText; + }; + + restoreCmd = mkOption { + description = "Command that reads the database dump on stdin and restores the database."; + type = str; + example = literalExpression '' + ''${pkgs.gzip}/bin/gunzip | ''${pkgs.postgresql}/bin/psql postgres + ''; + default = restoreCmd; + } // optionalAttrs (restoreCmdText != null) { + defaultText = literalMD restoreCmdText; + }; + }; }; }; - }; - result = { - restoreScript, + mkResult = { + restoreScript ? "restore", restoreScriptText ? null, - backupService, + backupService ? "backup.service", backupServiceText ? null, - }: submodule { - options = { - restoreScript = mkOption { - description = '' - Name of script that can restore the database. - One can then list snapshots with: + }: mkOption { + description = '' + Result part of the backup contract. - ```bash - $ ${if restoreScriptText != null then restoreScriptText else restoreScript} snapshots - ``` + Options set by the provider module that indicates the name of the backup and restor scripts. + ''; + default = { + inherit restoreScript backupService; + }; - And restore the database with: + defaultText = optionalString (anyNotNull [ + restoreScriptText + backupServiceText + ]) (literalMD '' + { + restoreScript = ${if restoreScriptText != null then restoreScriptText else restoreScript}; + backupService = ${if backupServiceText != null then backupServiceText else backupService}; + } + ''); - ```bash - $ ${if restoreScriptText != null then restoreScriptText else restoreScript} restore latest - ``` - ''; - type = str; - default = restoreScript; - defaultText = restoreScriptText; - }; + type = submodule { + options = { + restoreScript = mkOption { + description = '' + Name of script that can restore the database. + One can then list snapshots with: - backupService = mkOption { - description = '' - Name of service backing up the database. + ```bash + $ ${if restoreScriptText != null then restoreScriptText else restoreScript} snapshots + ``` - This script can be ran manually to backup the database: + And restore the database with: - ```bash - $ systemctl start ${if backupServiceText != null then backupServiceText else backupService} - ``` - ''; - type = str; - default = backupService; - defaultText = backupServiceText; + ```bash + $ ${if restoreScriptText != null then restoreScriptText else restoreScript} restore latest + ``` + ''; + type = str; + default = restoreScript; + } // optionalAttrs (restoreScriptText != null) { + defaultText = literalMD restoreScriptText; + }; + + backupService = mkOption { + description = '' + Name of service backing up the database. + + This script can be ran manually to backup the database: + + ```bash + $ systemctl start ${if backupServiceText != null then backupServiceText else backupService} + ``` + ''; + type = str; + default = backupService; + } // optionalAttrs (backupServiceText != null) { + defaultText = literalMD backupServiceText; + }; }; }; }; diff --git a/modules/contracts/databasebackup/dummyModule.nix b/modules/contracts/databasebackup/dummyModule.nix index fdf3672..05f4bac 100644 --- a/modules/contracts/databasebackup/dummyModule.nix +++ b/modules/contracts/databasebackup/dummyModule.nix @@ -3,7 +3,7 @@ let contracts = pkgs.callPackage ../. {}; inherit (lib) mkOption; - inherit (lib.types) anything submodule; + inherit (lib.types) submodule; in { options.shb.contracts.databasebackup = mkOption { @@ -23,24 +23,7 @@ in ''; type = submodule { - options = { - request = mkOption { - description = '' - Options set by a requester module of the database backup contract. - ''; - type = contracts.databasebackup.request; - }; - - result = mkOption { - description = '' - Options set by a provider module of the database backup contract. - ''; - type = contracts.databasebackup.result { - restoreScript = "my_restore_script"; - backupService = "my_backup_service.service"; - }; - }; - }; + options = contracts.databasebackup.contract; }; }; } diff --git a/modules/contracts/databasebackup/test.nix b/modules/contracts/databasebackup/test.nix index bd09fa5..da585d0 100644 --- a/modules/contracts/databasebackup/test.nix +++ b/modules/contracts/databasebackup/test.nix @@ -20,7 +20,7 @@ in imports = ( testLib.baseImports pkgs' ) ++ modules; config = lib.mkMerge [ (setAttrByPath providerRoot { - request = (getAttrFromPath requesterRoot config).databasebackup; + request = (getAttrFromPath requesterRoot config).request; settings = settings { inherit config; repository = "/opt/repos/database"; @@ -38,7 +38,7 @@ in }; testScript = { nodes, ... }: let - provider = (getAttrFromPath providerRoot nodes.machine).result; + provider = getAttrFromPath providerRoot nodes.machine; in '' import csv @@ -69,17 +69,17 @@ in cmp_tables(res, table) with subtest("backup"): - print(machine.succeed("systemctl cat ${provider.backupService}")) + print(machine.succeed("systemctl cat ${provider.result.backupService}")) print(machine.succeed("ls -l /run/hardcodedsecrets/hardcodedsecret_passphrase")) - machine.succeed("systemctl start ${provider.backupService}") + machine.succeed("systemctl start ${provider.result.backupService}") with subtest("drop database"): machine.succeed(peer_cmd("DROP DATABASE ${database}", db="postgres")) machine.fail(peer_cmd("SELECT * FROM test")) with subtest("restore"): - print(machine.succeed("readlink -f $(type ${provider.restoreScript})")) - machine.succeed("${provider.restoreScript} restore latest ") + print(machine.succeed("readlink -f $(type ${provider.result.restoreScript})")) + machine.succeed("${provider.result.restoreScript} restore latest ") with subtest("check restoration"): res = query("SELECT * FROM test") diff --git a/modules/contracts/default.nix b/modules/contracts/default.nix index 1c0642d..889bcd6 100644 --- a/modules/contracts/default.nix +++ b/modules/contracts/default.nix @@ -45,9 +45,7 @@ let }; in { - inherit mkContractFunctions; - - databasebackup = import ./databasebackup.nix { inherit lib; }; + databasebackup = importContract ./databasebackup.nix; backup = importContract ./backup.nix; mount = import ./mount.nix { inherit lib; }; secret = importContract ./secret.nix; diff --git a/modules/contracts/secret.nix b/modules/contracts/secret.nix index ef3ac6c..72af943 100644 --- a/modules/contracts/secret.nix +++ b/modules/contracts/secret.nix @@ -1,14 +1,19 @@ -{ lib, ... }: +{ pkgs, lib, ... }: let inherit (lib) concatStringsSep literalMD mkOption optionalAttrs optionalString; inherit (lib.types) listOf submodule str; + + shblib = pkgs.callPackage ../../lib {}; + inherit (shblib) anyNotNull; in { mkRequest = { mode ? "0400", + modeText ? null, owner ? "root", ownerText ? null, group ? "root", + groupText ? null, restartUnits ? [], restartUnitsText ? null, }: mkOption { @@ -23,11 +28,16 @@ in inherit mode owner group restartUnits; }; - defaultText = optionalString (ownerText != null || restartUnitsText != null) (literalMD '' + defaultText = optionalString (anyNotNull [ + modeText + ownerText + groupText + restartUnitsText + ]) (literalMD '' { - mode = ${mode}; + mode = ${if modeText != null then modeText else mode}; owner = ${if ownerText != null then ownerText else owner}; - group = ${group}; + group = ${if groupText != null then groupText else group}; restartUnits = ${if restartUnitsText != null then restartUnitsText else "[ " + concatStringsSep " " restartUnits + " ]"}; } ''); @@ -40,6 +50,8 @@ in ''; type = str; default = mode; + } // optionalAttrs (modeText != null) { + defaultText = literalMD modeText; }; owner = mkOption ({ @@ -58,6 +70,8 @@ in ''; type = str; default = group; + } // optionalAttrs (groupText != null) { + defaultText = literalMD groupText; }; restartUnits = mkOption ({ diff --git a/modules/services/vaultwarden.nix b/modules/services/vaultwarden.nix index 2579a60..09415ab 100644 --- a/modules/services/vaultwarden.nix +++ b/modules/services/vaultwarden.nix @@ -150,10 +150,6 @@ in }; config = lib.mkIf cfg.enable { - # Seems superfluous but otherwise we get: - # The option `shb.vaultwarden.backup' was accessed but has no value defined. - shb.vaultwarden.backup = {}; - services.vaultwarden = { enable = true; dbBackend = "postgresql"; @@ -235,5 +231,9 @@ in # TODO: make this work. # It does not work because it leads to infinite recursion. # ${cfg.mount}.path = dataFolder; + + # Seems superfluous but otherwise we get: + # The option `shb.vaultwarden.backup' was accessed but has no value defined. + shb.vaultwarden.backup = {}; }; } diff --git a/test/contracts/databasebackup.nix b/test/contracts/databasebackup.nix index aa6d3a4..aba1575 100644 --- a/test/contracts/databasebackup.nix +++ b/test/contracts/databasebackup.nix @@ -5,7 +5,7 @@ in { restic_postgres = contracts.test.databasebackup { name = "restic_postgres"; - requesterRoot = [ "shb" "postgresql" ]; + requesterRoot = [ "shb" "postgresql" "databasebackup" ]; providerRoot = [ "shb" "restic" "databases" "postgresql" ]; modules = [ ../../modules/blocks/postgresql.nix