nixos-restic/flake.nix

428 lines
15 KiB
Nix
Raw Normal View History

2023-01-26 14:29:12 -06:00
{
2023-03-13 09:01:16 -05:00
description = "Restic";
2023-01-26 14:29:12 -06:00
inputs = {
nixpkgs = {
2024-01-14 14:33:16 -06:00
url = "nixpkgs/nixos-23.11";
2023-01-26 14:29:12 -06:00
};
flake-utils = {
url = "github:numtide/flake-utils";
};
};
2023-11-03 12:19:05 -05:00
outputs = {
self,
nixpkgs,
flake-utils,
...
} @ inputs:
2023-01-26 14:29:12 -06:00
flake-utils.lib.eachDefaultSystem
2023-11-03 12:19:05 -05:00
(
system: let
pkgs = import nixpkgs {
inherit system;
};
in {
packages = {
restic = let
pname = "restic";
2024-02-07 20:32:44 -06:00
version = "0.16.4";
hash = "sha256-TSUhrtSgGIPM/cUzA6WDtCpqCyjtnM5BZDhK6udR0Ek=";
vendorHash = "sha256-E+Erf8AdlMBdep1g2SpI8JKIdJuKqmyWEUmh8Rs5R/o=";
2023-11-03 12:19:05 -05:00
in
pkgs.buildGoModule {
inherit pname version vendorHash;
src = pkgs.fetchFromGitHub {
owner = "restic";
repo = "restic";
rev = "v${version}";
hash = hash;
};
2023-03-13 09:01:16 -05:00
2023-11-03 12:19:05 -05:00
env = {
RESTIC_TEST_FUSE = "false";
};
2023-07-31 22:09:36 -05:00
2023-11-03 12:19:05 -05:00
patches = [
# The TestRestoreWithPermissionFailure test fails in Nix's build sandbox
./0001-Skip-testing-restore-with-permission-failure.patch
];
2023-03-13 09:01:16 -05:00
2023-11-03 12:19:05 -05:00
subPackages = ["cmd/restic"];
2023-03-13 09:01:16 -05:00
2023-11-03 12:19:05 -05:00
nativeBuildInputs = [
pkgs.installShellFiles
pkgs.makeWrapper
];
2023-03-13 09:01:16 -05:00
2023-11-03 12:19:05 -05:00
passthru.tests.restic = pkgs.nixosTests.restic;
2023-03-13 09:01:16 -05:00
2023-11-03 12:19:05 -05:00
postPatch = ''
# rm cmd/restic/integration_fuse_test.go
'';
2023-03-13 09:01:16 -05:00
2023-11-03 12:19:05 -05:00
postInstall =
''
2023-03-13 09:01:16 -05:00
wrapProgram $out/bin/restic --prefix PATH : '${pkgs.rclone}/bin'
2023-11-03 12:19:05 -05:00
''
+ pkgs.lib.optionalString (pkgs.stdenv.hostPlatform == pkgs.stdenv.buildPlatform) ''
2023-03-13 09:01:16 -05:00
$out/bin/restic generate \
--bash-completion restic.bash \
2023-11-03 12:19:05 -05:00
--fish-completion restic.fish \
2023-03-13 09:01:16 -05:00
--zsh-completion restic.zsh \
--man .
2023-11-03 12:19:05 -05:00
installShellCompletion restic.{bash,fish,zsh}
2023-03-13 09:01:16 -05:00
installManPage *.1
'';
2023-11-03 12:19:05 -05:00
meta = with pkgs.lib; {
homepage = "https://restic.net";
description = "A backup program that is fast, efficient and secure";
platforms = platforms.linux ++ platforms.darwin;
license = licenses.bsd2;
maintainers = [maintainers.mbrgm];
2023-03-13 09:01:16 -05:00
};
2023-04-22 18:38:58 -05:00
};
2023-11-03 12:19:05 -05:00
entrypoint = pkgs.writeTextFile {
name = "entrypoint";
destination = "/bin/entrypoint";
text = pkgs.lib.concatStringsSep "\n" [
"#!${pkgs.nushell}/bin/nu"
(builtins.readFile ./entrypoint.nu)
];
executable = true;
};
docker = pkgs.dockerTools.buildLayeredImage {
name = "restic";
tag = "latest";
maxLayers = 2;
contents = [
];
config = {
Cmd = [
"${self.packages.${system}.entrypoint}/bin/entrypoint"
"--rclone"
"${pkgs.rclone}/bin/rclone"
"--restic"
"${self.packages.${system}.restic}/bin/restic"
"--cache-dir"
"/cache"
2023-04-22 18:38:58 -05:00
];
2023-11-03 12:19:05 -05:00
Env = [
"TZ=UTC"
"SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"
];
ExposedPorts = {};
Volumes = {
"/cache" = {};
2023-04-22 18:38:58 -05:00
};
};
2023-03-13 09:01:16 -05:00
};
2023-11-03 12:19:05 -05:00
};
}
)
// {
2023-03-13 09:01:16 -05:00
nixosModules = {
2023-11-03 12:19:05 -05:00
restic = {
config,
lib,
pkgs,
...
}: let
cfg = config.restic;
package = self.packages.${pkgs.system}.restic;
in {
options = {
restic = lib.options.mkOption {
type = lib.types.submodule {
options = {
enable = lib.options.mkEnableOption "Restic";
passwordFile = lib.options.mkOption {
type = lib.types.path;
};
storage = lib.options.mkOption {
type = lib.types.enum [
"azure"
"b2"
];
};
healthcheck = lib.options.mkOption {
type = lib.types.submodule {
options = {
enable = lib.options.mkEnableOption "use healthcheck";
apiUrl = lib.options.mkOption {
type = lib.types.str;
};
apiKeyFile = lib.options.mkOption {
type = lib.types.str;
};
timeout = lib.options.mkOption {
type = lib.types.int;
default = 86400;
};
grace = lib.options.mkOption {
type = lib.types.int;
default = 14400;
};
};
2023-01-26 14:29:12 -06:00
};
2023-11-03 12:19:05 -05:00
};
b2 = lib.options.mkOption {
type = lib.types.submodule {
options = {
bucket = lib.options.mkOption {
type = lib.types.str;
};
accountId = lib.options.mkOption {
type = lib.types.str;
};
accountKeyFile = lib.options.mkOption {
type = lib.types.str;
};
};
2023-03-13 09:01:16 -05:00
};
2023-11-03 12:19:05 -05:00
};
azure = lib.options.mkOption {
type = lib.types.submodule {
options = {
accountName = lib.options.mkOption {
type = lib.types.str;
};
accountKeyFile = lib.options.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
2023-01-26 14:29:12 -06:00
};
};
};
2023-11-03 12:19:05 -05:00
};
backups = lib.options.mkOption {
type = lib.types.listOf (
lib.types.submodule {
2023-03-13 09:01:16 -05:00
options = {
2023-11-03 12:19:05 -05:00
oneFileSystem = lib.options.mkOption {
type = lib.types.bool;
default = false;
2023-03-13 09:01:16 -05:00
};
2023-11-03 12:19:05 -05:00
excludes = lib.options.mkOption {
type = lib.types.listOf lib.types.str;
default = [];
2023-03-13 09:01:16 -05:00
};
2023-11-03 12:19:05 -05:00
paths = lib.options.mkOption {
type = lib.types.listOf lib.types.str;
default = [];
2023-03-13 09:01:16 -05:00
};
2023-11-03 12:19:05 -05:00
preCommand = lib.options.mkOption {
type = lib.types.lines;
default = "";
2023-01-26 14:29:12 -06:00
};
2023-11-03 12:19:05 -05:00
postCommand = lib.options.mkOption {
type = lib.types.lines;
default = "";
2023-01-26 14:29:12 -06:00
};
};
2023-11-03 12:19:05 -05:00
}
);
2023-01-26 14:29:12 -06:00
};
};
};
};
2023-11-03 12:19:05 -05:00
};
2023-04-04 16:34:42 -05:00
2023-11-03 12:19:05 -05:00
config = let
bucket =
{
"b2" = cfg.b2.bucket;
"azure" = builtins.replaceStrings ["."] ["-"] "${config.networking.hostName}.${config.networking.domain}";
}
.${cfg.storage};
directory =
{
"b2" = "${config.networking.hostName}.${config.networking.domain}";
"azure" = "/";
}
.${cfg.storage};
in
lib.mkIf cfg.enable {
environment.systemPackages = [
package
];
2023-04-04 16:34:42 -05:00
2023-11-03 12:19:05 -05:00
systemd.services.restic = let
curlOptions = "--fail --silent --show-error --max-time 10 --retry 5";
hcPayload =
if cfg.healthcheck.enable
then
(
pkgs.writeText "healthcheck-payload"
2023-04-08 09:38:55 -05:00
(
2023-11-03 12:19:05 -05:00
builtins.toJSON
{
name = "${config.networking.hostName}.${config.networking.domain}";
timeout = cfg.healthcheck.timeout;
grace = cfg.healthcheck.grace;
unique = ["name"];
channels = "*";
}
)
)
else null;
hcSetup =
if cfg.healthcheck.enable
then ''
HC_URL=''$(${pkgs.curl}/bin/curl ${curlOptions} --request POST --header 'Content-Type: application/json' --header "X-Api-Key: ''$(<${cfg.healthcheck.apiKeyFile})" --data @${hcPayload} "${cfg.healthcheck.apiUrl}" | ${pkgs.jq}/bin/jq -r .ping_url)
''
else "";
hcStart =
if cfg.healthcheck.enable
then ''
if [ ! -z "''${HC_URL}" ]
then
${pkgs.curl}/bin/curl ${curlOptions} --output /dev/null "''${HC_URL}/start" || true
fi
''
else "";
hcStop =
if cfg.healthcheck.enable
then ''
if [ ! -z "''${HC_URL}" ]
then
${pkgs.curl}/bin/curl ${curlOptions} --output /dev/null "''${HC_URL}" || true
fi
''
else "";
repositoryConfig =
{
"b2" = "b2:${bucket}:${directory}";
"azure" = "azure:${bucket}:${directory}";
}
.${cfg.storage};
resticConfig =
''
export RESTIC_CACHE_DIR=/var/cache/restic
export RESTIC_PASSWORD_FILE=${cfg.passwordFile}
export RESTIC_REPOSITORY=${repositoryConfig}
''
+ {
"b2" = ''
export B2_ACCOUNT_ID='${cfg.b2.accountId}'
export B2_ACCOUNT_KEY="''$(<${cfg.b2.accountKeyFile})"
'';
"azure" = ''
export AZURE_ACCOUNT_NAME='${cfg.azure.accountName}'
export AZURE_ACCOUNT_KEY="''$(<${cfg.azure.accountKeyFile})"
'';
}
.${cfg.storage};
backupCommands =
lib.strings.concatStringsSep "\n"
(
map
(
backup: let
oneFileSystem =
if backup.oneFileSystem
then ["--one-file-system"]
else [];
excludes =
map
(
exclude: ''--exclude="${exclude}"''
)
backup.excludes;
paths =
map
2023-04-08 09:38:55 -05:00
(
2023-11-03 12:19:05 -05:00
path: ''"${path}"''
2023-04-08 09:38:55 -05:00
)
2023-11-03 12:19:05 -05:00
backup.paths;
arguments = lib.strings.concatStringsSep " " (
oneFileSystem ++ excludes ++ paths
);
in ''
${backup.preCommand}
${package}/bin/restic backup ${arguments}
${backup.postCommand}
''
)
cfg.backups
);
initCheck =
{
"b2" = ''
export RCLONE_CONFIG=/dev/null
export RCLONE_CONFIG_B2_TYPE=b2
export RCLONE_CONFIG_B2_ACCOUNT='${cfg.b2.accountId}'
export RCLONE_CONFIG_B2_KEY="''$(<${cfg.b2.accountKeyFile})"
2023-04-04 16:34:42 -05:00
2023-11-03 12:19:05 -05:00
config=''$(${pkgs.rclone}/bin/rclone lsjson 'b2:${bucket}/${directory}/config' | ${pkgs.jq}/bin/jq -r '. | length | . > 0')
2023-01-26 14:29:12 -06:00
2023-11-03 12:19:05 -05:00
if [ "''${config}" != 'true' ]
then
${package}/bin/restic init
fi
'';
2023-01-26 14:29:12 -06:00
2023-11-03 12:19:05 -05:00
"azure" = ''
export RCLONE_CONFIG=/dev/null
export RCLONE_CONFIG_AZURE_TYPE=azureblob
export RCLONE_CONFIG_AZURE_ACCOUNT='${cfg.azure.accountName}'
export RCLONE_CONFIG_AZURE_KEY="''$(<${cfg.azure.accountKeyFile})"
2023-04-04 16:34:42 -05:00
2023-11-03 12:19:05 -05:00
container=''$(${pkgs.rclone}/bin/rclone lsjson 'azure:' | ${pkgs.jq}/bin/jq -r 'map(select(.Path=="${bucket}" and .IsBucket)) | length | . > 0')
2023-04-04 16:34:42 -05:00
2023-11-03 12:19:05 -05:00
if [ "''${container}" != 'true' ]
then
${pkgs.rclone}/bin/rclone mkdir 'azure:${bucket}'
fi
2023-04-04 16:34:42 -05:00
2023-11-03 12:19:05 -05:00
config=$(${pkgs.rclone}/bin/rclone lsjson 'azure:${bucket}/config' | ${pkgs.jq}/bin/jq -r '. | length | . > 0')
2023-04-04 16:34:42 -05:00
2023-11-03 12:19:05 -05:00
if [ "''${config}" != 'true' ]
then
${package}/bin/restic init
fi
'';
}
.${cfg.storage};
in {
serviceConfig = {
Type = "oneshot";
CacheDirectory = "restic";
};
script = ''
${hcSetup}
${hcStart}
2023-01-26 14:29:12 -06:00
2023-11-03 12:19:05 -05:00
${resticConfig}
2023-04-04 16:34:42 -05:00
2023-11-03 12:19:05 -05:00
${initCheck}
2023-01-26 14:29:12 -06:00
2023-11-03 12:19:05 -05:00
${backupCommands}
2023-01-26 14:29:12 -06:00
2023-11-03 12:19:05 -05:00
${package}/bin/restic forget --prune --keep-daily 14
${package}/bin/restic check
${package}/bin/restic cache --cleanup
2023-01-26 14:29:12 -06:00
2023-11-03 12:19:05 -05:00
${hcStop}
'';
};
2023-01-26 14:29:12 -06:00
2023-11-03 12:19:05 -05:00
systemd.timers.restic = {
timerConfig = {
OnCalendar = "*-*-* 02:00:00";
RandomizedDelaySec = "60m";
2023-01-26 14:29:12 -06:00
};
2023-11-03 12:19:05 -05:00
wantedBy = ["multi-user.target"];
2023-01-26 14:29:12 -06:00
};
2023-11-03 12:19:05 -05:00
};
};
2023-03-13 09:01:16 -05:00
};
2023-01-26 14:29:12 -06:00
};
}