diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..9d357fc --- /dev/null +++ b/.drone.yml @@ -0,0 +1,30 @@ +--- +kind: secret +name: local_username +get: + path: local + name: username +--- +kind: secret +name: local_password +get: + path: local + name: password +--- +kind: pipeline +type: kubernetes +name: publish +steps: + - name: build + image: local.io/jcollie/nixos-runner:latest + pull: always + commands: + - nix build .#docker + - nix run .#push-container -- result + settings: + registry: r.ocj.io + repository: backup/restic + username: + from_secret: local_username + password: + from_secret: local_password diff --git a/entrypoint.nu b/entrypoint.nu new file mode 100644 index 0000000..0fdba90 --- /dev/null +++ b/entrypoint.nu @@ -0,0 +1,176 @@ + +def healthcheck_start [ + url: record +] { + if not ($env | get -i HEALTHCHECK_URL | is-empty ) { + http get --full --max-time 10 ($url | update path $"($in.path)/start" | url join) | ignore + } +} + +def healthcheck_log [ + url: record + log: string +] { + if not ($url | is-empty ) { + http post --max-time 10 ($url | update path $"($in.path)/log" | url join) $log | ignore + } +} + +def healthcheck_fail [ + url: record + log: string +] { + if not ($url | is-empty ) { + http post --full --max-time 10 ($url | update path $"($in.path)/fail" | url join) $log | ignore + } +} + +def healthcheck_stop [ + url: record +] { + if not ($url | is-empty ) { + http get --full --max-time 10 ($url | url join) | ignore + } +} + +def main [ + --rclone: string # path to rclone binary + --restic: string # path to restic binary + --cache-dir: string # path to cache dir +] { + let rid = (random uuid) + + let healthcheck_url = if not ($env | get -i HEALTHCHECK_URL | is-empty) { + $env.HEALTHCHECK_URL | url parse | update params ($in | get params | insert rid $rid) + } else { + {} + } + + healthcheck_start $healthcheck_url + + alias rclone = ^$rclone + alias restic = ^$restic --cache-dir $cache_dir --option "b2.connections=128" + + if ($env | get -i BACKUP_PATHS | is-empty) { + print "BACKUP_PATHS not set." + healthcheck_fail $healthcheck_url "BACKUP_PATHS not set." + exit 1 + } + + let paths = ($env.BACKUP_PATHS | split row " ") + + let not_exists = ($paths | path exists | enumerate | filter { |r| not $r.item }) + + if ($not_exists | length ) > 0 { + let error = ("error: some backup paths do not exist:\n\n" + ($not_exists | each { |r| ($paths | get $r.index) } | str join "\n") + "\n") + print ($error | str trim --right) + healthcheck_fail $healthcheck_url $error + exit 1 + } + + $paths | each { |r| print $"BACKUP_PATH: ($r)" } + + if ($env | get -i B2_ACCOUNT_ID | is-empty) { + print "B2_ACCOUNT_ID not set." + healthcheck_fail $healthcheck_url "B2_ACCOUNT_ID not set." + exit 1 + } + + if ($env | get -i B2_ACCOUNT_KEY | is-empty) { + print "B2_ACCOUNT_KEY not set." + healthcheck_fail $healthcheck_url "B2_ACCOUNT_KEY not set." + exit 1 + } + + if ($env | get -i B2_BUCKET | is-empty) { + print "B2_BUCKET not set." + healthcheck_fail $healthcheck_url "B2_BUCKET not set." + exit 1 + } + + print $"B2_BUCKET: ($env.B2_BUCKET)" + + if ($env | get -i HOSTNAME | is-empty) { + print "HOSTNAME not set." + healthcheck_fail $healthcheck_url "HOSTNAME not set." + exit 1 + } + + print $"HOSTNAME: ($env.HOSTNAME)" + + if ($env | get -i RESTIC_PASSWORD_FILE | is-empty) { + print "RESTIC_PASSWORD_FILE not set." + healthcheck_fail $healthcheck_url "RESTIC_PASSWORD_FILE not set." + exit 1 + } + + # $env.RESTIC_PASSWORD | save --force --raw "/tmp/password" + + let-env RESTIC_PASSWORD_FILE = "/tmp/password" + + let-env RESTIC_REPOSITORY = $"b2:($env.B2_BUCKET):($env.HOSTNAME)" + + let-env RCLONE_CONFIG = "/dev/null" + let-env RCLONE_CONFIG_B2_TYPE = "b2" + let-env RCLONE_CONFIG_B2_ACCOUNT = $env.B2_ACCOUNT_ID + let-env RCLONE_CONFIG_B2_KEY = $env.B2_ACCOUNT_KEY + + if (rclone lsjson $"b2:($env.B2_BUCKET)/($env.HOSTNAME)/config" | from json | length | $in == 0) { + let result = (do {restic init} | complete) + if $result.exit_code != 0 { + print "restic init failed" + print ($result.stderr | str trim --right) + healthcheck_fail $healthcheck_url $result.stderr + exit 1 + } else { + healthcheck_log $healthcheck_url "restic repository initialized" + } + } + + let result = (do {restic backup --host $env.HOSTNAME $paths} | complete) + if $result.exit_code == 1 { + print "restic backup failed" + print ($result.stderr | str trim --right) + healthcheck_fail $healthcheck_url $result.stderr + exit 1 + } else if $result.exit_code == 3 { + print "restic backup incomplete" + print ($result.stderr | str trim --right) + healthcheck_log $healthcheck_url $result.stderr + } else { + print ($result.stdout | str trim --right) + healthcheck_log $healthcheck_url $result.stdout + } + + let result = (do {restic forget --host $env.HOSTNAME --prune --keep-within 14d} | complete) + if $result.exit_code != 0 { + print "restic forget failed" + print ($result.stderr | str trim --right) + healthcheck_fail $healthcheck_url $result.stderr + exit 1 + } + print ($result.stdout | str trim --right) + healthcheck_log $healthcheck_url $result.stdout + + let result = (do {restic check} | complete) + if $result.exit_code != 0 { + print "restic check failed" + print ($result.stderr | str trim --right) + healthcheck_fail $healthcheck_url $result.stderr + exit 1 + } + print ($result.stdout | str trim --right) + healthcheck_log $healthcheck_url $result.stdout + + let result = (do {restic cache --cleanup} | complete) + if $result.exit_code != 0 { + print "restic cache failed" + print ($result.stderr | str trim) + healthcheck_fail $healthcheck_url $result.stderr + exit 1 + } + print ($result.stdout | str trim --right) + healthcheck_log $healthcheck_url $result.stdout + + healthcheck_stop $healthcheck_url +} diff --git a/flake.lock b/flake.lock index 828f5ed..875ab23 100644 --- a/flake.lock +++ b/flake.lock @@ -1,12 +1,15 @@ { "nodes": { "flake-utils": { + "inputs": { + "systems": "systems" + }, "locked": { - "lastModified": 1676283394, - "narHash": "sha256-XX2f9c3iySLCw54rJ/CZs+ZK6IQy7GXNY4nSOyu2QG4=", + "lastModified": 1681202837, + "narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=", "owner": "numtide", "repo": "flake-utils", - "rev": "3db36a8b464d0c4532ba1c7dda728f4576d6d073", + "rev": "cfacdce06f30d2b68473a46042957675eebb3401", "type": "github" }, "original": { @@ -17,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1678470307, - "narHash": "sha256-OEeMUr3ueLIXyW/OaFUX5jUdimyQwMg/7e+/Q0gC/QE=", + "lastModified": 1681920287, + "narHash": "sha256-+/d6XQQfhhXVfqfLROJoqj3TuG38CAeoT6jO1g9r1k0=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "0c4800d579af4ed98ecc47d464a5e7b0870c4b1f", + "rev": "645bc49f34fa8eff95479f0345ff57e55b53437e", "type": "github" }, "original": { @@ -35,6 +38,21 @@ "flake-utils": "flake-utils", "nixpkgs": "nixpkgs" } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index 6016f0d..3908d4c 100644 --- a/flake.nix +++ b/flake.nix @@ -23,17 +23,17 @@ let pname = "restic"; version = "0.15.1"; - sha256 = "sha256-KdPslVJHH+xdUuFfmLZumP2lHzkDrrAvpDaj38SuP8o="; - vendorSha256 = "sha256-oetaCiXWEBUEf382l4sjO0SCPxkoh+bMTgIf/qJTQms="; + hash = "sha256-KdPslVJHH+xdUuFfmLZumP2lHzkDrrAvpDaj38SuP8o="; + vendorHash = "sha256-oetaCiXWEBUEf382l4sjO0SCPxkoh+bMTgIf/qJTQms="; in pkgs.buildGoModule { - inherit pname version; + inherit pname version vendorHash; src = pkgs.fetchFromGitHub { owner = "restic"; repo = "restic"; rev = "v${version}"; - sha256 = sha256; + hash = hash; }; patches = [ @@ -41,11 +41,12 @@ ./0001-Skip-testing-restore-with-permission-failure.patch ]; - vendorSha256 = vendorSha256; - subPackages = [ "cmd/restic" ]; - nativeBuildInputs = [ pkgs.installShellFiles pkgs.makeWrapper ]; + nativeBuildInputs = [ + pkgs.installShellFiles + pkgs.makeWrapper + ]; passthru.tests.restic = pkgs.nixosTests.restic; @@ -72,6 +73,43 @@ maintainers = [ maintainers.mbrgm ]; }; }; + + 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" + ]; + Env = [ + "TZ=UTC" + "SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt" + ]; + ExposedPorts = { }; + Volumes = { + "/cache" = { }; + }; + }; + }; }; } ) // {