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 {
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 = {};
}
]);
}

View file

@ -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 "<name>" { 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 "<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";
};
backupService = "${fullName name config.settings.repository}.service";
backupServiceText = "${fullName "<name>" { path = "path/to/repository"; }}.service";
};
};
}));

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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