use secrets contract for restic (#349)
This commit is contained in:
parent
503fcef74b
commit
a04dca9973
17 changed files with 391 additions and 177 deletions
|
|
@ -4,12 +4,76 @@ A contract decouples modules that use a functionality from modules that provide
|
||||||
intuition for contracts is they are generally related to accessing a shared resource.
|
intuition for contracts is they are generally related to accessing a shared resource.
|
||||||
|
|
||||||
A few examples of contracts are generating SSL certificates, creating a user or knowing which files
|
A few examples of contracts are generating SSL certificates, creating a user or knowing which files
|
||||||
and folders to backup. Indeed, when generating certificates, the service using those do not care how
|
and folders to backup.
|
||||||
they were created. They just need to know where the certificate files are located.
|
Indeed, when generating certificates, the service using those do not care how they were created.
|
||||||
|
They just need to know where the certificate files are located.
|
||||||
|
|
||||||
In practice, a contract is a set of options that any user of a contract expects to exist. Also, the
|
A contract is made between a requester module and a provider module.
|
||||||
values of these options dictate the behavior of the implementation. This is enforced with NixOS VM
|
For example, a backup contract can be made between the Nextcloud service and the Restic service.
|
||||||
tests.
|
The former is the requester - the one wanted to be backed up -
|
||||||
|
and the latter is the provider of the contract - the one backing up files.
|
||||||
|
|
||||||
|
## Schema {#contracts-schema}
|
||||||
|
|
||||||
|
In practice, a contract is an attrset of options with a defined behavior.
|
||||||
|
Currently, the schema for a requester is:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
let
|
||||||
|
inherit (lib) mkOption;
|
||||||
|
inherit (lib.types) submodule;
|
||||||
|
in
|
||||||
|
config.${requester}.${contractname} = submodule {
|
||||||
|
request = mkOption {
|
||||||
|
type = contracts.${contractname}.request;
|
||||||
|
default = {
|
||||||
|
# Values set by the requester
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
result = mkOption {
|
||||||
|
type = contracts.${contractname}.result;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
For a provider, it is:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
let
|
||||||
|
inherit (lib) mkOption;
|
||||||
|
inherit (lib.types) anything submodule;
|
||||||
|
in
|
||||||
|
config.${provider}.${contractname} = submodule ({ options, ... }: {
|
||||||
|
request = mkOption {
|
||||||
|
type = contracts.${contractname}.request;
|
||||||
|
};
|
||||||
|
|
||||||
|
result = mkOption {
|
||||||
|
type = contracts.${contractname}.result;
|
||||||
|
default = {
|
||||||
|
# Values set by the provider
|
||||||
|
# Can depend on values set by the requester through the `options` variable.
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
settings = mkOption {
|
||||||
|
type = anything;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contract Tests {#contracts-test}
|
||||||
|
|
||||||
|
To make sure all providers module of a contract have the same behavior,
|
||||||
|
generic NixOS VM tests exist per contract.
|
||||||
|
They are generic because they work on any module,
|
||||||
|
as long as the module implements the contract of course.
|
||||||
|
|
||||||
|
For example, the [generic test][generic] for backup contract is instantiated for Restic [here][restic test].
|
||||||
|
|
||||||
|
[generic]: @REPO@/modules/contracts/backup/test.nix
|
||||||
|
[restic test]: @REPO@/test/contracts/backup.nix
|
||||||
|
|
||||||
## Videos {#contracts-videos}
|
## Videos {#contracts-videos}
|
||||||
|
|
||||||
|
|
@ -20,6 +84,59 @@ and the second at [NixCon in Berlin in fall of 2024][NixConBerlin2024].
|
||||||
[NixConNA2024]: https://www.youtube.com/watch?v=lw7PgphB9qM
|
[NixConNA2024]: https://www.youtube.com/watch?v=lw7PgphB9qM
|
||||||
[NixConBerlin2024]: https://www.youtube.com/watch?v=CP0hR6w1csc
|
[NixConBerlin2024]: https://www.youtube.com/watch?v=CP0hR6w1csc
|
||||||
|
|
||||||
|
## Why do we need this new concept? {#contracts-why}
|
||||||
|
|
||||||
|
Currently in nixpkgs, every module needing access to a shared resource must implement the logic
|
||||||
|
needed to setup that resource themselves. Similarly, if the module is mature enough to let the user
|
||||||
|
select a particular implementation, the code lives inside that module.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
This has a few disadvantages:
|
||||||
|
|
||||||
|
- This leads to a lot of **duplicated code**. If a module wants to support a new implementation of a
|
||||||
|
contract, the maintainers of that module must write code to make that happen.
|
||||||
|
- This also leads to **tight coupling**. The code written by the maintainers cannot be reused in
|
||||||
|
other modules, apart from copy pasting.
|
||||||
|
- There is also a **lack of separation of concerns**. The maintainers of a service must be experts
|
||||||
|
in all implementations they let the users choose from.
|
||||||
|
- Finally, this is **not extensible**. If you, the user of the module, want to use another
|
||||||
|
implementation that is not supported, you are out of luck. You can always dive into the module's
|
||||||
|
code and extend it, but that is not an optimal experience.
|
||||||
|
|
||||||
|
We do believe that the decoupling contracts provides helps alleviate all the issues outlined above
|
||||||
|
which makes it an essential step towards more adoption of Nix, if only in the self hosting scene.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Indeed, contracts allow:
|
||||||
|
|
||||||
|
- **Reuse of code**.
|
||||||
|
Since the implementation of a contract lives outside of modules using it,
|
||||||
|
using that implementation elsewhere is trivial.
|
||||||
|
- **Loose coupling**.
|
||||||
|
Modules that use a contract do not care how they are implemented,
|
||||||
|
as long as the implementation follows the behavior outlined by the contract.
|
||||||
|
- Full **separation of concerns** (see diagram below).
|
||||||
|
Now, each party's concern is separated with a clear boundary.
|
||||||
|
The maintainer of a module using a contract can be different from the maintainers
|
||||||
|
of the implementation, allowing them to be experts in their own respective fields.
|
||||||
|
But more importantly, the contracts themselves can be created and maintained by the community.
|
||||||
|
- Full **extensibility**.
|
||||||
|
The final user themselves can choose an implementation,
|
||||||
|
even new custom implementations not available in nixpkgs, without changing existing code.
|
||||||
|
- **Incremental adoption**.
|
||||||
|
Contracts can help bridge a NixOS system with any non-NixOS one.
|
||||||
|
For that, one can hardcode a requester or provider module to match
|
||||||
|
how the non-NixOS system is configured.
|
||||||
|
The responsability falls of course on the user to make sure both system agree on the configuration.
|
||||||
|
- Last but not least, **Testability**.
|
||||||
|
Thanks to NixOS VM test, we can even go one step further
|
||||||
|
by ensuring each implementation of a contract, even custom ones,
|
||||||
|
provides required options and behaves as the contract requires.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## Provided contracts {#contracts-provided}
|
## Provided contracts {#contracts-provided}
|
||||||
|
|
||||||
Self Host Blocks is a proving ground of contracts. This repository adds a layer on top of services
|
Self Host Blocks is a proving ground of contracts. This repository adds a layer on top of services
|
||||||
|
|
@ -53,49 +170,6 @@ modules/contracts/databasebackup/docs/default.md
|
||||||
modules/contracts/secret/docs/default.md
|
modules/contracts/secret/docs/default.md
|
||||||
```
|
```
|
||||||
|
|
||||||
## Why do we need this new concept? {#contracts-why}
|
|
||||||
|
|
||||||
Currently in nixpkgs, every module needing access to a shared resource must implement the logic
|
|
||||||
needed to setup that resource themselves. Similarly, if the module is mature enough to let the user
|
|
||||||
select a particular implementation, the code lives inside that module.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
This has a few disadvantages:
|
|
||||||
|
|
||||||
- This leads to a lot of **duplicated code**. If a module wants to support a new implementation of a
|
|
||||||
contract, the maintainers of that module must write code to make that happen.
|
|
||||||
- This also leads to **tight coupling**. The code written by the maintainers cannot be reused in
|
|
||||||
other modules, apart from copy pasting.
|
|
||||||
- There is also a **lack of separation of concerns**. The maintainers of a service must be experts
|
|
||||||
in all implementations they let the users choose from.
|
|
||||||
- Finally, this is **not extensible**. If you, the user of the module, want to use another
|
|
||||||
implementation that is not supported, you are out of luck. You can always dive into the module's
|
|
||||||
code and extend it, but that is not an optimal experience.
|
|
||||||
|
|
||||||
We do believe that the decoupling contracts provides helps alleviate all the issues outlined above
|
|
||||||
which makes it an essential step towards more adoption of Nix, if only in the self hosting scene.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Indeed, contracts allow:
|
|
||||||
|
|
||||||
- **Reuse of code**. Since the implementation of a contract lives outside of modules using it, using
|
|
||||||
that implementation elsewhere is trivial.
|
|
||||||
- **Loose coupling**. Modules that use a contract do not care how they are implemented, as long as
|
|
||||||
the implementation follows the behavior outlined by the contract.
|
|
||||||
- Full **separation of concerns** (see diagram below). Now, each party's concern is separated with a
|
|
||||||
clear boundary. The maintainer of a module using a contract can be different from the maintainers
|
|
||||||
of the implementation, allowing them to be experts in their own respective fields. But more
|
|
||||||
importantly, the contracts themselves can be created and maintained by the community.
|
|
||||||
- Full **extensibility**. The final user themselves can choose an implementation, even new custom
|
|
||||||
implementations not available in nixpkgs, without changing existing code.
|
|
||||||
- Last but not least, **Testability**. Thanks to NixOS VM test, we can even go one step further by
|
|
||||||
ensuring each implementation of a contract, even custom ones, provides required options and
|
|
||||||
behaves as the contract requires.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## Are there contracts in nixpkgs already? {#contracts-nixpkgs}
|
## Are there contracts in nixpkgs already? {#contracts-nixpkgs}
|
||||||
|
|
||||||
Actually not quite, but close. There are some ubiquitous options in nixpkgs. Those I found are:
|
Actually not quite, but close. There are some ubiquitous options in nixpkgs. Those I found are:
|
||||||
|
|
|
||||||
|
|
@ -12,27 +12,28 @@ information is provided in the respective manual sections.
|
||||||
|
|
||||||
| Service | Backup | Reverse Proxy | SSO | LDAP | Monitoring | Profiling |
|
| Service | Backup | Reverse Proxy | SSO | LDAP | Monitoring | Profiling |
|
||||||
|-----------------------|--------|---------------|-----|-------|------------|-----------|
|
|-----------------------|--------|---------------|-----|-------|------------|-----------|
|
||||||
| [Nextcloud Server][1] | P (1) | Y | Y | Y | Y | P (2) |
|
| [Nextcloud Server][1] | Y (1) | Y | Y | Y | Y (2) | P (3) |
|
||||||
| [Vaultwarden][2] | P (1) | Y | Y | Y | N | N |
|
| [Vaultwarden][2] | Y (1) | Y | Y | Y | Y (2) | N |
|
||||||
| [Forgejo][3] | Y | Y | Y | Y | N | N |
|
| [Forgejo][3] | Y (1) | Y | Y | Y | Y (2) | N |
|
||||||
|
|
||||||
Legend: **N**: no but WIP; **P**: partial; **Y**: yes
|
Legend: **N**: no but WIP; **P**: partial; **Y**: yes
|
||||||
|
|
||||||
1. Does not backup the database yet.
|
1. Database and data files are backed up separately.
|
||||||
2. Works but the traces are not exported to Grafana yet.
|
2. Dashboard is common to all services.
|
||||||
|
3. Works but the traces are not exported to Grafana yet.
|
||||||
|
|
||||||
[1]: services-nextcloud.html
|
[1]: services-nextcloud.html
|
||||||
[2]: services-vaultwarden.html
|
[2]: services-vaultwarden.html
|
||||||
[3]: services-forgejo.html
|
[3]: services-forgejo.html
|
||||||
|
|
||||||
```{=include=} chapters html:into-file=//services-vaultwarden.html
|
|
||||||
modules/services/vaultwarden/docs/default.md
|
|
||||||
```
|
|
||||||
|
|
||||||
```{=include=} chapters html:into-file=//services-nextcloud.html
|
```{=include=} chapters html:into-file=//services-nextcloud.html
|
||||||
modules/services/nextcloud-server/docs/default.md
|
modules/services/nextcloud-server/docs/default.md
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```{=include=} chapters html:into-file=//services-vaultwarden.html
|
||||||
|
modules/services/vaultwarden/docs/default.md
|
||||||
|
```
|
||||||
|
|
||||||
```{=include=} chapters html:into-file=//services-forgejo.html
|
```{=include=} chapters html:into-file=//services-forgejo.html
|
||||||
modules/services/forgejo/docs/default.md
|
modules/services/forgejo/docs/default.md
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -67,12 +67,12 @@ in
|
||||||
```
|
```
|
||||||
'';
|
'';
|
||||||
|
|
||||||
type = contracts.databasebackup.requestType;
|
type = contracts.databasebackup.request;
|
||||||
|
|
||||||
default = {
|
default = {
|
||||||
user = "postgres";
|
user = "postgres";
|
||||||
|
|
||||||
backupFile = "postgres.sql";
|
backupName = "postgres.sql";
|
||||||
|
|
||||||
backupCmd = ''
|
backupCmd = ''
|
||||||
${pkgs.postgresql}/bin/pg_dumpall | ${pkgs.gzip}/bin/gzip --rsyncable
|
${pkgs.postgresql}/bin/pg_dumpall | ${pkgs.gzip}/bin/gzip --rsyncable
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ let
|
||||||
inherit (lib) generators hasPrefix mkIf nameValuePair optionalAttrs removePrefix;
|
inherit (lib) generators hasPrefix mkIf nameValuePair optionalAttrs removePrefix;
|
||||||
inherit (lib.types) attrsOf enum int ints listOf oneOf nonEmptyListOf nonEmptyStr nullOr path str submodule;
|
inherit (lib.types) attrsOf enum int ints listOf oneOf nonEmptyListOf nonEmptyStr nullOr path str submodule;
|
||||||
|
|
||||||
commonOptions = {
|
commonOptions = { name, options, prefix, ... }: {
|
||||||
enable = mkEnableOption ''
|
enable = mkEnableOption ''
|
||||||
this backup intance.
|
this backup intance.
|
||||||
|
|
||||||
|
|
@ -18,9 +18,13 @@ let
|
||||||
but still provides the helper tool to restore snapshots
|
but still provides the helper tool to restore snapshots
|
||||||
'';
|
'';
|
||||||
|
|
||||||
passphraseFile = mkOption {
|
passphrase = contracts.secret.mkOption {
|
||||||
description = "Encryption key for the backups.";
|
description = "Encryption key for the backup repository.";
|
||||||
type = path;
|
mode = "0400";
|
||||||
|
owner = options.request.value.user;
|
||||||
|
ownerText = "[shb.restic.${prefix}.<name>.request.user](#blocks-restic-options-shb.restic.${prefix}._name_.request.user)";
|
||||||
|
restartUnits = [ (fullName name options.settings.value.repository) ];
|
||||||
|
restartUnitsText = "[ [shb.restic.${prefix}.<name>.settings.repository](#blocks-restic-options-shb.restic.${prefix}._name_.settings.repository) ]";
|
||||||
};
|
};
|
||||||
|
|
||||||
repository = mkOption {
|
repository = mkOption {
|
||||||
|
|
@ -101,7 +105,7 @@ in
|
||||||
{
|
{
|
||||||
options.shb.restic = {
|
options.shb.restic = {
|
||||||
instances = mkOption {
|
instances = mkOption {
|
||||||
description = "Each instance is backing up some directories to one repository.";
|
description = "Files to backup following the [backup contract](./contracts-backup.html).";
|
||||||
default = {};
|
default = {};
|
||||||
type = attrsOf (submodule ({ name, options, ... }: {
|
type = attrsOf (submodule ({ name, options, ... }: {
|
||||||
options = {
|
options = {
|
||||||
|
|
@ -121,7 +125,7 @@ in
|
||||||
'';
|
'';
|
||||||
|
|
||||||
type = submodule {
|
type = submodule {
|
||||||
options = commonOptions;
|
options = commonOptions { inherit name options; prefix = "instances"; };
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -131,24 +135,27 @@ in
|
||||||
|
|
||||||
Contains the output of the Restic provider.
|
Contains the output of the Restic provider.
|
||||||
'';
|
'';
|
||||||
type = lib.types.anything; # contracts.databasebackup.result;
|
|
||||||
default = {
|
default = {
|
||||||
restoreScript = fullName name options.settings.value.repository;
|
restoreScript = fullName name options.settings.value.repository;
|
||||||
backupService = "${fullName name options.settings.value.repository}.service";
|
backupService = "${fullName name options.settings.value.repository}.service";
|
||||||
};
|
};
|
||||||
defaultText = literalExpression ''
|
defaultText = {
|
||||||
{
|
restoreScriptText = "${fullName "<name>" { path = "path/to/repository"; }}";
|
||||||
restoreScript = "${fullName "<name>" { path = "path/to/repository"; }}";
|
backupServiceText = "${fullName "<name>" { path = "path/to/repository"; }}.service";
|
||||||
backupService = "${fullName "<name>" { path = "path/to/repository"; }}.service";
|
};
|
||||||
}
|
type = contracts.backup.result {
|
||||||
'';
|
restoreScript = fullName name options.settings.value.repository;
|
||||||
|
backupService = "${fullName name options.settings.value.repository}.service";
|
||||||
|
restoreScriptText = "${fullName "<name>" { path = "path/to/repository"; }}";
|
||||||
|
backupServiceText = "${fullName "<name>" { path = "path/to/repository"; }}.service";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
databases = mkOption {
|
databases = mkOption {
|
||||||
description = "Databases to backup following the database backup contract.";
|
description = "Databases to backup following the [database backup contract](./contracts-databasebackup.html).";
|
||||||
default = {};
|
default = {};
|
||||||
type = attrsOf (submodule ({ name, options, ... }: {
|
type = attrsOf (submodule ({ name, options, ... }: {
|
||||||
options = {
|
options = {
|
||||||
|
|
@ -159,7 +166,7 @@ in
|
||||||
Accepts values from a requester.
|
Accepts values from a requester.
|
||||||
'';
|
'';
|
||||||
|
|
||||||
type = contracts.databasebackup.requestType;
|
type = contracts.databasebackup.request;
|
||||||
};
|
};
|
||||||
|
|
||||||
settings = mkOption {
|
settings = mkOption {
|
||||||
|
|
@ -168,7 +175,7 @@ in
|
||||||
'';
|
'';
|
||||||
|
|
||||||
type = submodule {
|
type = submodule {
|
||||||
options = commonOptions;
|
options = commonOptions { inherit name options; prefix = "databases"; };
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -178,17 +185,20 @@ in
|
||||||
|
|
||||||
Contains the output of the Restic provider.
|
Contains the output of the Restic provider.
|
||||||
'';
|
'';
|
||||||
type = contracts.databasebackup.resultType;
|
|
||||||
default = {
|
default = {
|
||||||
restoreScript = fullName name options.settings.value.repository;
|
restoreScript = fullName name options.settings.value.repository;
|
||||||
backupService = "${fullName name options.settings.value.repository}.service";
|
backupService = "${fullName name options.settings.value.repository}.service";
|
||||||
};
|
};
|
||||||
defaultText = literalExpression ''
|
defaultText = {
|
||||||
{
|
restoreScriptText = "${fullName "<name>" { path = "path/to/repository"; }}";
|
||||||
restoreScript = "${fullName "<name>" { path = "path/to/repository"; }}";
|
backupServiceText = "${fullName "<name>" { path = "path/to/repository"; }}.service";
|
||||||
backupService = "${fullName "<name>" { path = "path/to/repository"; }}.service";
|
};
|
||||||
}
|
type = contracts.databasebackup.result {
|
||||||
'';
|
restoreScript = fullName name options.settings.value.repository;
|
||||||
|
backupService = "${fullName name options.settings.value.repository}.service";
|
||||||
|
restoreScriptText = "${fullName "<name>" { path = "path/to/repository"; }}";
|
||||||
|
backupServiceText = "${fullName "<name>" { path = "path/to/repository"; }}.service";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}));
|
}));
|
||||||
|
|
@ -249,7 +259,7 @@ in
|
||||||
|
|
||||||
paths = instance.request.sourceDirectories;
|
paths = instance.request.sourceDirectories;
|
||||||
|
|
||||||
passwordFile = toString instance.settings.passphraseFile;
|
passwordFile = toString instance.settings.passphrase.result.path;
|
||||||
|
|
||||||
initialize = true;
|
initialize = true;
|
||||||
|
|
||||||
|
|
@ -288,7 +298,7 @@ in
|
||||||
|
|
||||||
dynamicFilesFrom = "echo";
|
dynamicFilesFrom = "echo";
|
||||||
|
|
||||||
passwordFile = toString instance.settings.passphraseFile;
|
passwordFile = toString instance.settings.passphrase.result.path;
|
||||||
|
|
||||||
initialize = true;
|
initialize = true;
|
||||||
|
|
||||||
|
|
@ -310,7 +320,7 @@ in
|
||||||
cmd = pkgs.writeShellScriptBin "dump.sh" instance.request.backupCmd;
|
cmd = pkgs.writeShellScriptBin "dump.sh" instance.request.backupCmd;
|
||||||
in
|
in
|
||||||
[
|
[
|
||||||
"--stdin-filename ${instance.request.backupFile} --stdin-from-command -- ${cmd}/bin/dump.sh"
|
"--stdin-filename ${instance.request.backupName} --stdin-from-command -- ${cmd}/bin/dump.sh"
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -371,7 +381,7 @@ in
|
||||||
serviceConfig.Type = "oneshot";
|
serviceConfig.Type = "oneshot";
|
||||||
script = (shblib.replaceSecrets {
|
script = (shblib.replaceSecrets {
|
||||||
userConfig = instance.settings.repository.secrets // {
|
userConfig = instance.settings.repository.secrets // {
|
||||||
RESTIC_PASSWORD_FILE = instance.settings.passphraseFile;
|
RESTIC_PASSWORD_FILE = toString instance.settings.passphrase.result.path;
|
||||||
RESTIC_REPOSITORY = instance.settings.repository.path;
|
RESTIC_REPOSITORY = instance.settings.repository.path;
|
||||||
};
|
};
|
||||||
resultPath = "/run/secrets_restic_env/${fullName name instance.settings.repository}";
|
resultPath = "/run/secrets_restic_env/${fullName name instance.settings.repository}";
|
||||||
|
|
@ -414,7 +424,7 @@ in
|
||||||
sudo --preserve-env -u ${instance.request.user} ${pkgs.restic}/bin/restic $@
|
sudo --preserve-env -u ${instance.request.user} ${pkgs.restic}/bin/restic $@
|
||||||
else
|
else
|
||||||
shift
|
shift
|
||||||
sudo --preserve-env -u ${instance.request.user} sh -c "${pkgs.restic}/bin/restic dump $@ ${instance.request.backupFile} | ${instance.request.restoreCmd}"
|
sudo --preserve-env -u ${instance.request.user} sh -c "${pkgs.restic}/bin/restic dump $@ ${instance.request.backupName} | ${instance.request.restoreCmd}"
|
||||||
fi
|
fi
|
||||||
'';
|
'';
|
||||||
in
|
in
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
Defined in [`/modules/blocks/restic.nix`](@REPO@/modules/blocks/restic.nix).
|
Defined in [`/modules/blocks/restic.nix`](@REPO@/modules/blocks/restic.nix).
|
||||||
|
|
||||||
This block sets up a backup job using [Restic][restic].
|
This block sets up a backup job using [Restic][].
|
||||||
|
|
||||||
[restic]: https://restic.net/
|
[restic]: https://restic.net/
|
||||||
|
|
||||||
|
|
@ -12,18 +12,38 @@ Specific integration tests are defined in [`/test/blocks/restic.nix`](@REPO@/tes
|
||||||
|
|
||||||
## Provider Contracts {#blocks-restic-contract-provider}
|
## Provider Contracts {#blocks-restic-contract-provider}
|
||||||
|
|
||||||
This block implements the [backup](contracts-backup.html) and [database backup](contracts-databasebackup.html) contracts.
|
This block provides:
|
||||||
|
|
||||||
Contract integration tests are defined in [`/test/contracts/backup.nix`](@REPO@/test/contracts/backup.nix).
|
- [backup contract](contracts-backup.html) under the [`shb.restic.instances`][instances] option.
|
||||||
|
It is tested with [contract tests][backup contract tests].
|
||||||
|
- [database backup contract](contracts-databasebackup.html) under the [`shb.restic.databases`][databases] option.
|
||||||
|
It is tested with [contract tests][database backup contract tests].
|
||||||
|
|
||||||
### One folder backed up to mounted hard drives {#blocks-restic-contract-provider-one}
|
[instances]: #blocks-restic-options-shb.restic.instances
|
||||||
|
[databases]: #blocks-restic-options-shb.restic.databases
|
||||||
|
[backup contract tests]: @REPO@/test/contracts/backup.nix
|
||||||
|
[database backup contract tests]: @REPO@/test/contracts/databasebackup.nix
|
||||||
|
|
||||||
|
As requested by those two contracts, when setting up a backup with Restic,
|
||||||
|
a backup Systemd service and restore script are provided.
|
||||||
|
The restore script has all the secrets needed to access the repo,
|
||||||
|
the only requirement to run it is to be able to `sudo` in the expected user.
|
||||||
|
|
||||||
|
## Usage {#blocks-restic-usage}
|
||||||
|
|
||||||
|
The following examples assume usage of SOPS to provide secrets
|
||||||
|
although any blocks providing the [secrets contract][] works too.
|
||||||
|
The [secrets setup section](usage.html#usage-secrets) explains
|
||||||
|
how to setup SOPS.
|
||||||
|
|
||||||
|
### One folder backed up manually {#blocks-restic-usage-provider-manual}
|
||||||
|
|
||||||
The following snippet shows how to configure
|
The following snippet shows how to configure
|
||||||
the backup of 1 folder to 1 repository.
|
the backup of 1 folder to 1 repository.
|
||||||
We assume that the folder is used by the `myservice` service and is owned by a user of the same name.
|
We assume that the folder `/var/lib/myfolder` of the service `myservice` must be backed up.
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
shb.restic.instances.myservice = {
|
shb.restic.instances."myservice" = {
|
||||||
request = {
|
request = {
|
||||||
user = "myservice";
|
user = "myservice";
|
||||||
|
|
||||||
|
|
@ -56,7 +76,41 @@ shb.restic.instances.myservice = {
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
### One folder backed up to S3 {#blocks-restic-contract-provider-remote}
|
### One folder backed up with contract {#blocks-restic-usage-provider-contract}
|
||||||
|
|
||||||
|
With the same example as before but assuming the `myservice` service
|
||||||
|
has a `myservice.backup` option that is a requester for the backup contract,
|
||||||
|
the snippet above becomes:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
shb.restic.instances."myservice" = {
|
||||||
|
request = config.myservice.backup;
|
||||||
|
|
||||||
|
settings = {
|
||||||
|
enable = true;
|
||||||
|
|
||||||
|
passphraseFile = "<path/to/passphrase>";
|
||||||
|
|
||||||
|
repository = {
|
||||||
|
path = "/srv/backups/myservice";
|
||||||
|
timerConfig = {
|
||||||
|
OnCalendar = "00:00:00";
|
||||||
|
RandomizedDelaySec = "3h";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
retention = {
|
||||||
|
keep_within = "1d";
|
||||||
|
keep_hourly = 24;
|
||||||
|
keep_daily = 7;
|
||||||
|
keep_weekly = 4;
|
||||||
|
keep_monthly = 6;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### One folder backed up to S3 {#blocks-restic-usage-provider-remote}
|
||||||
|
|
||||||
Here we will only highlight the differences with the previous configuration.
|
Here we will only highlight the differences with the previous configuration.
|
||||||
|
|
||||||
|
|
@ -81,46 +135,6 @@ This assumes you have access to such a remote S3 store, for example by using [Ba
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Secrets {#blocks-restic-secrets}
|
|
||||||
|
|
||||||
To be secure, the secrets should deployed out of band, otherwise they will be world-readable in the nix store.
|
|
||||||
|
|
||||||
To achieve that, I recommend [sops](usage.html#usage-secrets) although other methods work great too.
|
|
||||||
The code to backup to Backblaze with secrets stored in Sops would look like so:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
shb.restic.instances.myfolder.passphraseFile = config.sops.secrets."myservice/backup/passphrase".path;
|
|
||||||
shb.restic.instances.myfolder.repository = {
|
|
||||||
path = "s3:s3.us-west-000.backblazeb2.com/<mybucket>";
|
|
||||||
secrets = {
|
|
||||||
AWS_ACCESS_KEY_ID.source = config.sops.secrets."backup/b2/access_key_id".path;
|
|
||||||
AWS_SECRET_ACCESS_KEY.source = config.sops.secrets."backup/b2/secret_access_key".path;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
sops.secrets."myservice/backup/passphrase" = {
|
|
||||||
sopsFile = ./secrets.yaml;
|
|
||||||
mode = "0400";
|
|
||||||
owner = "myservice";
|
|
||||||
group = "myservice";
|
|
||||||
};
|
|
||||||
sops.secrets."backup/b2/access_key_id" = {
|
|
||||||
sopsFile = ./secrets.yaml;
|
|
||||||
mode = "0400";
|
|
||||||
owner = "myservice";
|
|
||||||
group = "myservice";
|
|
||||||
};
|
|
||||||
sops.secrets."backup/b2/secret_access_key" = {
|
|
||||||
sopsFile = ./secrets.yaml;
|
|
||||||
mode = "0400";
|
|
||||||
owner = "myservice";
|
|
||||||
group = "myservice";
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
Pay attention that the owner must be the `myservice` user, the one owning the files to be backed up.
|
|
||||||
A `secrets` contract is in progress that will allow one to not care about such details.
|
|
||||||
|
|
||||||
## Multiple directories to multiple destinations {#blocks-restic-usage-multiple}
|
## Multiple directories to multiple destinations {#blocks-restic-usage-multiple}
|
||||||
|
|
||||||
The following snippet shows how to configure backup of any number of folders to 3 repositories,
|
The following snippet shows how to configure backup of any number of folders to 3 repositories,
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,11 @@ in
|
||||||
request = submodule {
|
request = submodule {
|
||||||
options = {
|
options = {
|
||||||
user = mkOption {
|
user = mkOption {
|
||||||
description = "Unix user doing the backups.";
|
description = ''
|
||||||
|
Unix user doing the backups.
|
||||||
|
|
||||||
|
Most of the time, this should be the user owning the files.
|
||||||
|
'';
|
||||||
type = str;
|
type = str;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -17,7 +21,7 @@ in
|
||||||
};
|
};
|
||||||
|
|
||||||
excludePatterns = mkOption {
|
excludePatterns = mkOption {
|
||||||
description = "Patterns to exclude.";
|
description = "File patterns to exclude.";
|
||||||
type = listOf str;
|
type = listOf str;
|
||||||
default = [];
|
default = [];
|
||||||
};
|
};
|
||||||
|
|
@ -28,13 +32,13 @@ in
|
||||||
type = submodule {
|
type = submodule {
|
||||||
options = {
|
options = {
|
||||||
before_backup = mkOption {
|
before_backup = mkOption {
|
||||||
description = "Hooks to run before backup";
|
description = "Hooks to run before backup.";
|
||||||
type = listOf str;
|
type = listOf str;
|
||||||
default = [];
|
default = [];
|
||||||
};
|
};
|
||||||
|
|
||||||
after_backup = mkOption {
|
after_backup = mkOption {
|
||||||
description = "Hooks to run after backup";
|
description = "Hooks to run after backup.";
|
||||||
type = listOf str;
|
type = listOf str;
|
||||||
default = [];
|
default = [];
|
||||||
};
|
};
|
||||||
|
|
@ -44,16 +48,46 @@ in
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
result = submodule {
|
result = {
|
||||||
|
restoreScript,
|
||||||
|
restoreScriptText ? null,
|
||||||
|
backupService,
|
||||||
|
backupServiceText ? null,
|
||||||
|
}: submodule {
|
||||||
options = {
|
options = {
|
||||||
restoreScript = mkOption {
|
restoreScript = mkOption {
|
||||||
description = "Name of script that can restore the database.";
|
description = ''
|
||||||
|
Name of script that can restore the database.
|
||||||
|
One can then list snapshots with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ ${if restoreScriptText != null then restoreScriptText else restoreScript} snapshots
|
||||||
|
```
|
||||||
|
|
||||||
|
And restore the database with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ ${if restoreScriptText != null then restoreScriptText else restoreScript} restore latest
|
||||||
|
```
|
||||||
|
'';
|
||||||
type = str;
|
type = str;
|
||||||
|
default = restoreScript;
|
||||||
|
defaultText = restoreScriptText;
|
||||||
};
|
};
|
||||||
|
|
||||||
backupService = mkOption {
|
backupService = mkOption {
|
||||||
description = "Name of service backing up the database.";
|
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;
|
type = str;
|
||||||
|
default = backupService;
|
||||||
|
defaultText = backupServiceText;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,44 @@ let
|
||||||
contracts = pkgs.callPackage ../. {};
|
contracts = pkgs.callPackage ../. {};
|
||||||
|
|
||||||
inherit (lib) mkOption;
|
inherit (lib) mkOption;
|
||||||
|
inherit (lib.types) anything submodule;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.shb.contracts.backup = mkOption {
|
options.shb.contracts.backup = mkOption {
|
||||||
description = "Contract for backups.";
|
description = ''
|
||||||
type = contracts.backup.request;
|
Contract for backing up files
|
||||||
|
between a requester module and a provider module.
|
||||||
|
|
||||||
|
The requester communicates to the provider
|
||||||
|
what files to backup
|
||||||
|
through the `request` options.
|
||||||
|
|
||||||
|
The provider reads from the `request` options
|
||||||
|
and backs up the requested files.
|
||||||
|
It communicates to the requester what script is used
|
||||||
|
to backup and restore the files
|
||||||
|
through the `result` options.
|
||||||
|
'';
|
||||||
|
|
||||||
|
type = submodule {
|
||||||
|
options = {
|
||||||
|
request = mkOption {
|
||||||
|
description = ''
|
||||||
|
Options set by a requester module of the backup contract.
|
||||||
|
'';
|
||||||
|
type = contracts.backup.request;
|
||||||
|
};
|
||||||
|
|
||||||
|
result = mkOption {
|
||||||
|
description = ''
|
||||||
|
Options set by a provider module of the backup contract.
|
||||||
|
'';
|
||||||
|
type = contracts.backup.result {
|
||||||
|
restoreScript = "my_restore_script";
|
||||||
|
backupService = "my_backup_service.service";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,8 @@ in
|
||||||
"/opt/files/A"
|
"/opt/files/A"
|
||||||
"/opt/files/B"
|
"/opt/files/B"
|
||||||
],
|
],
|
||||||
settings, # repository -> attrset
|
settings, # { repository, config } -> attrset
|
||||||
|
extraConfig ? null, # { username } -> attrset
|
||||||
}: pkgs.testers.runNixOSTest {
|
}: pkgs.testers.runNixOSTest {
|
||||||
inherit name;
|
inherit name;
|
||||||
|
|
||||||
|
|
@ -28,7 +29,10 @@ in
|
||||||
inherit sourceDirectories;
|
inherit sourceDirectories;
|
||||||
user = username;
|
user = username;
|
||||||
};
|
};
|
||||||
settings = settings "/opt/repos/${name}";
|
settings = settings {
|
||||||
|
inherit config;
|
||||||
|
repository = "/opt/repos/${name}";
|
||||||
|
};
|
||||||
})
|
})
|
||||||
(mkIf (username != "root") {
|
(mkIf (username != "root") {
|
||||||
users.users.${username} = {
|
users.users.${username} = {
|
||||||
|
|
@ -37,6 +41,7 @@ in
|
||||||
group = "root";
|
group = "root";
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
(optionalAttrs (extraConfig != null) (extraConfig { inherit username; }))
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -44,10 +49,7 @@ in
|
||||||
skipTypeCheck = true;
|
skipTypeCheck = true;
|
||||||
|
|
||||||
testScript = { nodes, ... }: let
|
testScript = { nodes, ... }: let
|
||||||
provider = getAttrFromPath providerRoot nodes.machine;
|
provider = (getAttrFromPath providerRoot nodes.machine).result;
|
||||||
backupService = provider.result.backupService;
|
|
||||||
restoreScript = provider.result.restoreScript;
|
|
||||||
onAllSourceDirectories = f: concatMapStringsSep "\n" (path: indent 4 (f path)) sourceDirectories;
|
|
||||||
in ''
|
in ''
|
||||||
from dictdiffer import diff
|
from dictdiffer import diff
|
||||||
|
|
||||||
|
|
@ -88,8 +90,8 @@ in
|
||||||
})
|
})
|
||||||
|
|
||||||
with subtest("First backup in repo"):
|
with subtest("First backup in repo"):
|
||||||
print(machine.succeed("systemctl cat ${backupService}"))
|
print(machine.succeed("systemctl cat ${provider.backupService}"))
|
||||||
machine.succeed("systemctl start ${backupService}")
|
machine.succeed("systemctl start ${provider.backupService}")
|
||||||
|
|
||||||
with subtest("New content"):
|
with subtest("New content"):
|
||||||
for path in sourceDirectories:
|
for path in sourceDirectories:
|
||||||
|
|
@ -110,7 +112,7 @@ in
|
||||||
assert_files(path, {})
|
assert_files(path, {})
|
||||||
|
|
||||||
with subtest("Restore initial content from repo"):
|
with subtest("Restore initial content from repo"):
|
||||||
machine.succeed("""${restoreScript} restore latest""")
|
machine.succeed("""${provider.restoreScript} restore latest""")
|
||||||
|
|
||||||
for path in sourceDirectories:
|
for path in sourceDirectories:
|
||||||
assert_files(path, {
|
assert_files(path, {
|
||||||
|
|
|
||||||
|
|
@ -4,16 +4,20 @@ let
|
||||||
inherit (lib.types) anything submodule str;
|
inherit (lib.types) anything submodule str;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
requestType = submodule {
|
request = submodule {
|
||||||
options = {
|
options = {
|
||||||
user = mkOption {
|
user = mkOption {
|
||||||
description = "Unix user doing the backups.";
|
description = ''
|
||||||
|
Unix user doing the backups.
|
||||||
|
|
||||||
|
This should be an admin user having access to all databases.
|
||||||
|
'';
|
||||||
type = str;
|
type = str;
|
||||||
example = "postgres";
|
example = "postgres";
|
||||||
};
|
};
|
||||||
|
|
||||||
backupFile = mkOption {
|
backupName = mkOption {
|
||||||
description = "Filename of the backup.";
|
description = "Name of the backup in the repository.";
|
||||||
type = str;
|
type = str;
|
||||||
default = "dump";
|
default = "dump";
|
||||||
example = "postgresql.sql";
|
example = "postgresql.sql";
|
||||||
|
|
@ -38,7 +42,12 @@ in
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
resultType = submodule {
|
result = {
|
||||||
|
restoreScript,
|
||||||
|
restoreScriptText ? null,
|
||||||
|
backupService,
|
||||||
|
backupServiceText ? null,
|
||||||
|
}: submodule {
|
||||||
options = {
|
options = {
|
||||||
restoreScript = mkOption {
|
restoreScript = mkOption {
|
||||||
description = ''
|
description = ''
|
||||||
|
|
@ -46,17 +55,18 @@ in
|
||||||
One can then list snapshots with:
|
One can then list snapshots with:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ my_restore_script snapshots
|
$ ${if restoreScriptText != null then restoreScriptText else restoreScript} snapshots
|
||||||
```
|
```
|
||||||
|
|
||||||
And restore the database with:
|
And restore the database with:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ my_restore_script restore latest
|
$ ${if restoreScriptText != null then restoreScriptText else restoreScript} restore latest
|
||||||
```
|
```
|
||||||
'';
|
'';
|
||||||
type = str;
|
type = str;
|
||||||
example = "my_restore_script";
|
default = restoreScript;
|
||||||
|
defaultText = restoreScriptText;
|
||||||
};
|
};
|
||||||
|
|
||||||
backupService = mkOption {
|
backupService = mkOption {
|
||||||
|
|
@ -66,11 +76,12 @@ in
|
||||||
This script can be ran manually to backup the database:
|
This script can be ran manually to backup the database:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ systemctl start my_backup_service
|
$ systemctl start ${if backupServiceText != null then backupServiceText else backupService}
|
||||||
```
|
```
|
||||||
'';
|
'';
|
||||||
type = str;
|
type = str;
|
||||||
example = "my_backup_service.service";
|
default = backupService;
|
||||||
|
defaultText = backupServiceText;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -26,16 +26,19 @@ in
|
||||||
options = {
|
options = {
|
||||||
request = mkOption {
|
request = mkOption {
|
||||||
description = ''
|
description = ''
|
||||||
Options set by a requester module of the database contract.
|
Options set by a requester module of the database backup contract.
|
||||||
'';
|
'';
|
||||||
type = contracts.databasebackup.requestType;
|
type = contracts.databasebackup.request;
|
||||||
};
|
};
|
||||||
|
|
||||||
result = mkOption {
|
result = mkOption {
|
||||||
description = ''
|
description = ''
|
||||||
Options set by a provider module of the database contract.
|
Options set by a provider module of the database backup contract.
|
||||||
'';
|
'';
|
||||||
type = contracts.databasebackup.resultType;
|
type = contracts.databasebackup.result {
|
||||||
|
restoreScript = "my_restore_script";
|
||||||
|
backupService = "my_backup_service.service";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ in
|
||||||
{ name,
|
{ name,
|
||||||
requesterRoot,
|
requesterRoot,
|
||||||
providerRoot,
|
providerRoot,
|
||||||
providerExtraConfig ? null, # { username, database } -> attrset
|
extraConfig ? null, # { username, database } -> attrset
|
||||||
modules ? [],
|
modules ? [],
|
||||||
username ? "me",
|
username ? "me",
|
||||||
database ? "me",
|
database ? "me",
|
||||||
|
|
@ -31,7 +31,7 @@ in
|
||||||
group = "root";
|
group = "root";
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
(optionalAttrs (providerExtraConfig != null) (providerExtraConfig { inherit username database; }))
|
(optionalAttrs (extraConfig != null) (extraConfig { inherit username database; }))
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -72,6 +72,7 @@ in
|
||||||
|
|
||||||
with subtest("drop database"):
|
with subtest("drop database"):
|
||||||
machine.succeed(peer_cmd("DROP DATABASE me", db="postgres"))
|
machine.succeed(peer_cmd("DROP DATABASE me", db="postgres"))
|
||||||
|
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.restoreScript})"))
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,10 @@
|
||||||
{ description,
|
{ description,
|
||||||
mode ? "0400",
|
mode ? "0400",
|
||||||
owner ? "root",
|
owner ? "root",
|
||||||
|
ownerText ? null,
|
||||||
group ? "root",
|
group ? "root",
|
||||||
restartUnits ? [],
|
restartUnits ? [],
|
||||||
|
restartUnitsText ? null,
|
||||||
}: lib.mkOption {
|
}: lib.mkOption {
|
||||||
inherit description;
|
inherit description;
|
||||||
|
|
||||||
|
|
@ -16,6 +18,15 @@
|
||||||
inherit mode owner group restartUnits;
|
inherit mode owner group restartUnits;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
defaultText = lib.optionalString (ownerText != null || restartUnitsText != null) (lib.literalMD ''
|
||||||
|
{
|
||||||
|
mode = ${mode};
|
||||||
|
owner = ${if ownerText != null then ownerText else owner};
|
||||||
|
group = ${group};
|
||||||
|
restartUnits = ${if restartUnitsText != null then restartUnitsText else "[ " + lib.concatStringsSep " " restartUnits + " ]"};
|
||||||
|
}
|
||||||
|
'');
|
||||||
|
|
||||||
readOnly = true;
|
readOnly = true;
|
||||||
|
|
||||||
description = ''
|
description = ''
|
||||||
|
|
@ -56,6 +67,7 @@
|
||||||
'';
|
'';
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
default = owner;
|
default = owner;
|
||||||
|
defaultText = if ownerText != null then lib.literalMD ownerText else null;
|
||||||
};
|
};
|
||||||
|
|
||||||
group = lib.mkOption {
|
group = lib.mkOption {
|
||||||
|
|
@ -72,6 +84,7 @@
|
||||||
'';
|
'';
|
||||||
type = lib.types.listOf lib.types.str;
|
type = lib.types.listOf lib.types.str;
|
||||||
default = restartUnits;
|
default = restartUnits;
|
||||||
|
defaultText = if restartUnitsText != null then lib.literalMD restartUnitsText else null;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ let
|
||||||
settings = {
|
settings = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|
||||||
passphraseFile = toString (pkgs.writeText "passphrase" "PassPhrase");
|
passphrase.result.path = pkgs.writeText "passphrase" "PassPhrase";
|
||||||
|
|
||||||
repository = {
|
repository = {
|
||||||
path = "/opt/repos/A";
|
path = "/opt/repos/A";
|
||||||
|
|
|
||||||
|
|
@ -149,7 +149,7 @@ in
|
||||||
shb.hardcodedsecret.ldapUserPassword = config.shb.ldap.ldapUserPassword.request // {
|
shb.hardcodedsecret.ldapUserPassword = config.shb.ldap.ldapUserPassword.request // {
|
||||||
content = "ldapUserPassword";
|
content = "ldapUserPassword";
|
||||||
};
|
};
|
||||||
shb.hardcodedsecret.jwtSecret = config.shb.ldap.ldapUserPassword.request // {
|
shb.hardcodedsecret.jwtSecret = config.shb.ldap.jwtSecret.request // {
|
||||||
content = "jwtSecrets";
|
content = "jwtSecrets";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,11 @@ in
|
||||||
providerRoot = [ "shb" "restic" "instances" "mytest" ];
|
providerRoot = [ "shb" "restic" "instances" "mytest" ];
|
||||||
modules = [
|
modules = [
|
||||||
../../modules/blocks/restic.nix
|
../../modules/blocks/restic.nix
|
||||||
|
../../modules/blocks/hardcodedsecret.nix
|
||||||
];
|
];
|
||||||
settings = repository: {
|
settings = { repository, config, ... }: {
|
||||||
enable = true;
|
enable = true;
|
||||||
passphraseFile = toString (pkgs.writeText "passphrase" "PassPhrase");
|
passphrase.result.path = config.shb.hardcodedsecret.passphrase.path;
|
||||||
repository = {
|
repository = {
|
||||||
path = repository;
|
path = repository;
|
||||||
timerConfig = {
|
timerConfig = {
|
||||||
|
|
@ -20,6 +21,12 @@ in
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
extraConfig = { username, ... }: {
|
||||||
|
shb.hardcodedsecret.passphrase = {
|
||||||
|
owner = username;
|
||||||
|
content = "passphrase";
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
restic_me = contracts.test.backup {
|
restic_me = contracts.test.backup {
|
||||||
|
|
@ -28,10 +35,11 @@ in
|
||||||
providerRoot = [ "shb" "restic" "instances" "mytest" ];
|
providerRoot = [ "shb" "restic" "instances" "mytest" ];
|
||||||
modules = [
|
modules = [
|
||||||
../../modules/blocks/restic.nix
|
../../modules/blocks/restic.nix
|
||||||
|
../../modules/blocks/hardcodedsecret.nix
|
||||||
];
|
];
|
||||||
settings = repository: {
|
settings = { repository, config, ... }: {
|
||||||
enable = true;
|
enable = true;
|
||||||
passphraseFile = toString (pkgs.writeText "passphrase" "PassPhrase");
|
passphrase.result.path = config.shb.hardcodedsecret.passphrase.path;
|
||||||
repository = {
|
repository = {
|
||||||
path = repository;
|
path = repository;
|
||||||
timerConfig = {
|
timerConfig = {
|
||||||
|
|
@ -39,5 +47,11 @@ in
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
extraConfig = { username, ... }: {
|
||||||
|
shb.hardcodedsecret.passphrase = {
|
||||||
|
owner = username;
|
||||||
|
content = "passphrase";
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ in
|
||||||
];
|
];
|
||||||
settings = repository: {
|
settings = repository: {
|
||||||
enable = true;
|
enable = true;
|
||||||
passphraseFile = toString (pkgs.writeText "passphrase" "PassPhrase");
|
passphrase.result.path = pkgs.writeText "passphrase" "PassPhrase";
|
||||||
repository = {
|
repository = {
|
||||||
path = repository;
|
path = repository;
|
||||||
timerConfig = {
|
timerConfig = {
|
||||||
|
|
@ -21,7 +21,7 @@ in
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
providerExtraConfig = { username, database, ... }: {
|
extraConfig = { username, database, ... }: {
|
||||||
shb.postgresql.ensures = [
|
shb.postgresql.ensures = [
|
||||||
{
|
{
|
||||||
inherit username database;
|
inherit username database;
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,7 @@ let
|
||||||
request = config.shb.vaultwarden.backup;
|
request = config.shb.vaultwarden.backup;
|
||||||
settings = {
|
settings = {
|
||||||
enable = true;
|
enable = true;
|
||||||
passphraseFile = toString (pkgs.writeText "passphrase" "PassPhrase");
|
passphrase.result.path = config.shb.hardcodedsecret.passphrase.path;
|
||||||
repository = {
|
repository = {
|
||||||
path = "/opt/repos/A";
|
path = "/opt/repos/A";
|
||||||
timerConfig = {
|
timerConfig = {
|
||||||
|
|
@ -107,6 +107,9 @@ let
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
shb.hardcodedsecret.passphrase = config.shb.restic.instances."testinstance".settings.passphrase.request // {
|
||||||
|
content = "PassPhrase";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue