switch databasebackup contract to new contract style (#372)
Co-authored-by: ibizaman <ibizapeanut@gmail.com>
This commit is contained in:
parent
47dffd2a2f
commit
3e63cb2e12
9 changed files with 192 additions and 155 deletions
|
|
@ -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 = {};
|
||||||
|
}
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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";
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}));
|
}));
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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 ({
|
||||||
|
|
|
||||||
|
|
@ -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 = {};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue