switch databasebackup contract to new contract style (#372)

Co-authored-by: ibizaman <ibizapeanut@gmail.com>
This commit is contained in:
Pierre Penninckx 2024-11-26 21:07:36 +01:00 committed by GitHub
parent 47dffd2a2f
commit 3e63cb2e12
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 192 additions and 155 deletions

View file

@ -52,35 +52,23 @@ in
databasebackup = lib.mkOption { databasebackup = lib.mkOption {
description = '' description = ''
Backup configuration. This is an output option. Backup configuration.
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;
};
};
```
''; '';
type = contracts.databasebackup.request; type = lib.types.submodule {
options = contracts.databasebackup.mkRequester {
user = "postgres";
default = { backupName = "postgres.sql";
user = "postgres";
backupName = "postgres.sql"; backupCmd = ''
${pkgs.postgresql}/bin/pg_dumpall | ${pkgs.gzip}/bin/gzip --rsyncable
'';
backupCmd = '' restoreCmd = ''
${pkgs.postgresql}/bin/pg_dumpall | ${pkgs.gzip}/bin/gzip --rsyncable ${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) (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 = {};
}
]); ]);
} }

View file

@ -137,17 +137,7 @@ in
description = "Databases to backup following the [database backup contract](./contracts-databasebackup.html)."; description = "Databases to backup following the [database backup contract](./contracts-databasebackup.html).";
default = {}; default = {};
type = attrsOf (submodule ({ name, config, ... }: { type = attrsOf (submodule ({ name, config, ... }: {
options = { options = contracts.databasebackup.mkProvider {
request = mkOption {
description = ''
Request part of the backup contract.
Accepts values from a requester.
'';
type = contracts.databasebackup.request;
};
settings = mkOption { settings = mkOption {
description = '' description = ''
Settings specific to the Restic provider. Settings specific to the Restic provider.
@ -158,26 +148,12 @@ in
}; };
}; };
result = mkOption { resultCfg = {
description = '' restoreScript = fullName name config.settings.repository;
Result part of the backup contract. restoreScriptText = "${fullName "<name>" { path = "path/to/repository"; }}";
Contains the output of the Restic provider. backupService = "${fullName name config.settings.repository}.service";
''; backupServiceText = "${fullName "<name>" { path = "path/to/repository"; }}.service";
default = {
restoreScript = fullName name config.settings.repository;
backupService = "${fullName name config.settings.repository}.service";
};
defaultText = {
restoreScriptText = "${fullName "<name>" { path = "path/to/repository"; }}";
backupServiceText = "${fullName "<name>" { path = "path/to/repository"; }}.service";
};
type = contracts.databasebackup.result {
restoreScript = fullName name config.settings.repository;
backupService = "${fullName name config.settings.repository}.service";
restoreScriptText = "${fullName "<name>" { path = "path/to/repository"; }}";
backupServiceText = "${fullName "<name>" { path = "path/to/repository"; }}.service";
};
}; };
}; };
})); }));

View file

@ -1,87 +1,160 @@
{ lib, ... }: { pkgs, lib, ... }:
let let
inherit (lib) mkIf mkOption literalExpression; inherit (lib) mkOption literalExpression literalMD optionalAttrs optionalString;
inherit (lib.types) anything submodule str; inherit (lib.types) submodule str;
shblib = pkgs.callPackage ../../lib {};
inherit (shblib) anyNotNull;
in in
{ {
request = submodule { mkRequest =
options = { { user ? "root",
user = mkOption { userText ? null,
description = '' backupName ? "dump",
Unix user doing the backups. 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. Options set by the requester module
''; enforcing how to backup files.
type = str; '';
example = "postgres";
default = {
inherit user backupName backupCmd restoreCmd;
}; };
backupName = mkOption { defaultText = optionalString (anyNotNull [
description = "Name of the backup in the repository."; userText
type = str; backupNameText
default = "dump"; backupCmdText
example = "postgresql.sql"; 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 { type = submodule {
description = "Command that produces the database dump on stdout."; options = {
type = str; user = mkOption {
example = literalExpression '' description = ''
''${pkgs.postgresql}/bin/pg_dumpall | ''${pkgs.gzip}/bin/gzip --rsyncable Unix user doing the backups.
'';
};
restoreCmd = mkOption { This should be an admin user having access to all databases.
description = "Command that reads the database dump on stdin and restores the database."; '';
type = str; type = str;
example = literalExpression '' example = "postgres";
''${pkgs.gzip}/bin/gunzip | ''${pkgs.postgresql}/bin/psql 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 = { mkResult = {
restoreScript, restoreScript ? "restore",
restoreScriptText ? null, restoreScriptText ? null,
backupService, backupService ? "backup.service",
backupServiceText ? null, backupServiceText ? null,
}: submodule { }: mkOption {
options = { description = ''
restoreScript = mkOption { Result part of the backup contract.
description = ''
Name of script that can restore the database.
One can then list snapshots with:
```bash Options set by the provider module that indicates the name of the backup and restor scripts.
$ ${if restoreScriptText != null then restoreScriptText else restoreScript} snapshots '';
``` 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 type = submodule {
$ ${if restoreScriptText != null then restoreScriptText else restoreScript} restore latest options = {
``` restoreScript = mkOption {
''; description = ''
type = str; Name of script that can restore the database.
default = restoreScript; One can then list snapshots with:
defaultText = restoreScriptText;
};
backupService = mkOption { ```bash
description = '' $ ${if restoreScriptText != null then restoreScriptText else restoreScript} snapshots
Name of service backing up the database. ```
This script can be ran manually to backup the database: And restore the database with:
```bash ```bash
$ systemctl start ${if backupServiceText != null then backupServiceText else backupService} $ ${if restoreScriptText != null then restoreScriptText else restoreScript} restore latest
``` ```
''; '';
type = str; type = str;
default = backupService; default = restoreScript;
defaultText = backupServiceText; } // 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;
};
}; };
}; };
}; };

View file

@ -3,7 +3,7 @@ let
contracts = pkgs.callPackage ../. {}; contracts = pkgs.callPackage ../. {};
inherit (lib) mkOption; inherit (lib) mkOption;
inherit (lib.types) anything submodule; inherit (lib.types) submodule;
in in
{ {
options.shb.contracts.databasebackup = mkOption { options.shb.contracts.databasebackup = mkOption {
@ -23,24 +23,7 @@ in
''; '';
type = submodule { type = submodule {
options = { options = contracts.databasebackup.contract;
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";
};
};
};
}; };
}; };
} }

View file

@ -20,7 +20,7 @@ in
imports = ( testLib.baseImports pkgs' ) ++ modules; imports = ( testLib.baseImports pkgs' ) ++ modules;
config = lib.mkMerge [ config = lib.mkMerge [
(setAttrByPath providerRoot { (setAttrByPath providerRoot {
request = (getAttrFromPath requesterRoot config).databasebackup; request = (getAttrFromPath requesterRoot config).request;
settings = settings { settings = settings {
inherit config; inherit config;
repository = "/opt/repos/database"; repository = "/opt/repos/database";
@ -38,7 +38,7 @@ in
}; };
testScript = { nodes, ... }: let testScript = { nodes, ... }: let
provider = (getAttrFromPath providerRoot nodes.machine).result; provider = getAttrFromPath providerRoot nodes.machine;
in '' in ''
import csv import csv
@ -69,17 +69,17 @@ in
cmp_tables(res, table) cmp_tables(res, table)
with subtest("backup"): 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")) 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"): with subtest("drop database"):
machine.succeed(peer_cmd("DROP DATABASE ${database}", db="postgres")) machine.succeed(peer_cmd("DROP DATABASE ${database}", db="postgres"))
machine.fail(peer_cmd("SELECT * FROM test")) machine.fail(peer_cmd("SELECT * FROM test"))
with subtest("restore"): with subtest("restore"):
print(machine.succeed("readlink -f $(type ${provider.restoreScript})")) print(machine.succeed("readlink -f $(type ${provider.result.restoreScript})"))
machine.succeed("${provider.restoreScript} restore latest ") machine.succeed("${provider.result.restoreScript} restore latest ")
with subtest("check restoration"): with subtest("check restoration"):
res = query("SELECT * FROM test") res = query("SELECT * FROM test")

View file

@ -45,9 +45,7 @@ let
}; };
in in
{ {
inherit mkContractFunctions; databasebackup = importContract ./databasebackup.nix;
databasebackup = import ./databasebackup.nix { inherit lib; };
backup = importContract ./backup.nix; backup = importContract ./backup.nix;
mount = import ./mount.nix { inherit lib; }; mount = import ./mount.nix { inherit lib; };
secret = importContract ./secret.nix; secret = importContract ./secret.nix;

View file

@ -1,14 +1,19 @@
{ lib, ... }: { pkgs, lib, ... }:
let let
inherit (lib) concatStringsSep literalMD mkOption optionalAttrs optionalString; inherit (lib) concatStringsSep literalMD mkOption optionalAttrs optionalString;
inherit (lib.types) listOf submodule str; inherit (lib.types) listOf submodule str;
shblib = pkgs.callPackage ../../lib {};
inherit (shblib) anyNotNull;
in in
{ {
mkRequest = mkRequest =
{ mode ? "0400", { mode ? "0400",
modeText ? null,
owner ? "root", owner ? "root",
ownerText ? null, ownerText ? null,
group ? "root", group ? "root",
groupText ? null,
restartUnits ? [], restartUnits ? [],
restartUnitsText ? null, restartUnitsText ? null,
}: mkOption { }: mkOption {
@ -23,11 +28,16 @@ in
inherit mode owner group restartUnits; 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}; 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 + " ]"}; restartUnits = ${if restartUnitsText != null then restartUnitsText else "[ " + concatStringsSep " " restartUnits + " ]"};
} }
''); '');
@ -40,6 +50,8 @@ in
''; '';
type = str; type = str;
default = mode; default = mode;
} // optionalAttrs (modeText != null) {
defaultText = literalMD modeText;
}; };
owner = mkOption ({ owner = mkOption ({
@ -58,6 +70,8 @@ in
''; '';
type = str; type = str;
default = group; default = group;
} // optionalAttrs (groupText != null) {
defaultText = literalMD groupText;
}; };
restartUnits = mkOption ({ restartUnits = mkOption ({

View file

@ -150,10 +150,6 @@ in
}; };
config = lib.mkIf cfg.enable { 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 = { services.vaultwarden = {
enable = true; enable = true;
dbBackend = "postgresql"; dbBackend = "postgresql";
@ -235,5 +231,9 @@ in
# TODO: make this work. # TODO: make this work.
# It does not work because it leads to infinite recursion. # It does not work because it leads to infinite recursion.
# ${cfg.mount}.path = dataFolder; # ${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 = {};
}; };
} }

View file

@ -5,7 +5,7 @@ in
{ {
restic_postgres = contracts.test.databasebackup { restic_postgres = contracts.test.databasebackup {
name = "restic_postgres"; name = "restic_postgres";
requesterRoot = [ "shb" "postgresql" ]; requesterRoot = [ "shb" "postgresql" "databasebackup" ];
providerRoot = [ "shb" "restic" "databases" "postgresql" ]; providerRoot = [ "shb" "restic" "databases" "postgresql" ];
modules = [ modules = [
../../modules/blocks/postgresql.nix ../../modules/blocks/postgresql.nix