├── LICENSE
├── examples
├── system
│ ├── nix-pub.pem
│ ├── nix.key
│ ├── bare.nix
│ ├── configuration.nix
│ ├── README.md
│ ├── common.nix
│ ├── hello.nix
│ ├── flake.nix
│ └── flake.lock
├── darwin
│ ├── README.md
│ └── flake.nix
└── simple
│ ├── flake.nix
│ └── flake.lock
├── .envrc
├── .gitignore
├── .reuse
└── dep5
├── default.nix
├── shell.nix
├── src
├── bin
│ ├── deploy.rs
│ └── activate.rs
├── data.rs
├── push.rs
├── lib.rs
├── deploy.rs
└── cli.rs
├── nix
└── tests
│ ├── server.nix
│ ├── common.nix
│ ├── deploy-flake.nix
│ └── default.nix
├── .github
└── workflows
│ └── check.yml
├── Cargo.toml
├── flake.lock
├── interface.json
├── flake.nix
├── docs
└── logo.svg
├── README.md
├── LICENSES
└── MPL-2.0.txt
└── Cargo.lock
/LICENSE:
--------------------------------------------------------------------------------
1 | ./LICENSES/MPL-2.0.txt
--------------------------------------------------------------------------------
/examples/system/nix-pub.pem:
--------------------------------------------------------------------------------
1 | cache.example.com:ic28PY7OIOQtoU282iaiizvA5WIOtYx5h6c9ePn3hDQ=
--------------------------------------------------------------------------------
/examples/system/nix.key:
--------------------------------------------------------------------------------
1 | cache.example.com:dPNdwv04QPIEpcWnGioZmX9dvaGe7GCo7BZJFymDBnSJzbw9js4g5C2hTbzaJqKLO8DlYg61jHmHpz14+feENA==
--------------------------------------------------------------------------------
/.envrc:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: 2021 Serokell
2 | #
3 | # SPDX-License-Identifier: MPL-2.0
4 |
5 | use flake
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: 2020 Serokell
2 | #
3 | # SPDX-License-Identifier: MPL-2.0
4 |
5 | .direnv
6 | /target
7 | result*
8 | /examples/system/bare-system.qcow2
9 |
--------------------------------------------------------------------------------
/examples/system/bare.nix:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: 2020 Serokell
2 | #
3 | # SPDX-License-Identifier: MPL-2.0
4 |
5 | {
6 | imports = [ ./common.nix ];
7 |
8 | # Use that when deploy scripts asks you for a hostname
9 | networking.hostName = "bare-system";
10 | }
11 |
--------------------------------------------------------------------------------
/examples/system/configuration.nix:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: 2020 Serokell
2 | #
3 | # SPDX-License-Identifier: MPL-2.0
4 |
5 | {
6 | imports = [ ./common.nix ];
7 |
8 | networking.hostName = "example-nixos-system";
9 |
10 | users.users.hello = {
11 | isNormalUser = true;
12 | password = "";
13 | uid = 1010;
14 | };
15 | }
16 |
--------------------------------------------------------------------------------
/.reuse/dep5:
--------------------------------------------------------------------------------
1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
2 | Upstream-Name: deploy-rs
3 | Upstream-Contact: Serokell
4 | Source: https://github.com/serokell/deploy-rs
5 |
6 | Copyright: 2020 Serokell
7 | License: MPL-2.0
8 | Files: *.lock *.json docs/*.svg examples/*
9 |
10 | # Sample paragraph, commented out:
11 | #
12 | # Files: src/*
13 | # Copyright: $YEAR $NAME <$CONTACT>
14 | # License: ...
15 |
--------------------------------------------------------------------------------
/default.nix:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: 2020 Serokell
2 | #
3 | # SPDX-License-Identifier: MPL-2.0
4 |
5 | (import
6 | (
7 | let
8 | lock = builtins.fromJSON (builtins.readFile ./flake.lock);
9 | in
10 | fetchTarball {
11 | url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
12 | sha256 = lock.nodes.flake-compat.locked.narHash;
13 | }
14 | )
15 | {
16 | src = ./.;
17 | }).defaultNix
18 |
--------------------------------------------------------------------------------
/shell.nix:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: 2020 Serokell
2 | #
3 | # SPDX-License-Identifier: MPL-2.0
4 |
5 | (import
6 | (
7 | let
8 | lock = builtins.fromJSON (builtins.readFile ./flake.lock);
9 | in
10 | fetchTarball {
11 | url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
12 | sha256 = lock.nodes.flake-compat.locked.narHash;
13 | }
14 | )
15 | {
16 | src = ./.;
17 | }).shellNix
18 |
--------------------------------------------------------------------------------
/src/bin/deploy.rs:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2020 Serokell
2 | // SPDX-FileCopyrightText: 2021 Yannik Sander
3 | //
4 | // SPDX-License-Identifier: MPL-2.0
5 |
6 | use deploy::cli;
7 | use log::error;
8 |
9 | #[tokio::main]
10 | async fn main() -> Result<(), Box> {
11 | match cli::run(None).await {
12 | Ok(()) => (),
13 | Err(err) => {
14 | error!("{}", err);
15 | std::process::exit(1);
16 | }
17 | }
18 |
19 | Ok(())
20 | }
21 |
--------------------------------------------------------------------------------
/examples/system/README.md:
--------------------------------------------------------------------------------
1 |
6 |
7 | # Example nixos system deployment
8 |
9 | This is an example of how to deploy a full nixos system with a separate user unit to a bare machine.
10 |
11 | 1. Run bare system from `.#nixosConfigurations.bare`
12 | - `nix build .#nixosConfigurations.bare.config.system.build.vm`
13 | - `QEMU_NET_OPTS=hostfwd=tcp::2221-:22 ./result/bin/run-bare-system-vm`
14 | 2. `nix run github:serokell/deploy-rs`
15 | 3. ???
16 | 4. PROFIT!!!
17 |
--------------------------------------------------------------------------------
/examples/darwin/README.md:
--------------------------------------------------------------------------------
1 |
6 |
7 | # Example nix-darwin system deployment
8 |
9 | ## Prerequisites
10 |
11 | 1) Install `nix` and `nix-darwin` (the latter creates `/run` sets up `/etc/nix/nix.conf` symlink and so on)
12 | on the target machine.
13 | 2) Enable remote login on the mac to allow ssh access.
14 | 3) `deploy-rs` doesn't support password provisioning for `sudo`, so the `sshUser` should
15 | have passwordless `sudo` access.
16 |
17 | ## Deploying
18 |
19 | Run `nix run github:serokell/deploy-rs -- --ssh-user `.
--------------------------------------------------------------------------------
/examples/simple/flake.nix:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: 2020 Serokell
2 | #
3 | # SPDX-License-Identifier: MPL-2.0
4 |
5 | {
6 | description = "Deploy GNU hello to localhost";
7 |
8 | inputs.deploy-rs.url = "github:serokell/deploy-rs";
9 |
10 | outputs = { self, nixpkgs, deploy-rs }: {
11 | deploy.nodes.example = {
12 | hostname = "localhost";
13 | profiles.hello = {
14 | user = "balsoft";
15 | path = deploy-rs.lib.x86_64-linux.setActivate nixpkgs.legacyPackages.x86_64-linux.hello "./bin/hello";
16 | };
17 | };
18 |
19 | checks = builtins.mapAttrs (system: deployLib: deployLib.deployChecks self.deploy) deploy-rs.lib;
20 | };
21 | }
22 |
--------------------------------------------------------------------------------
/nix/tests/server.nix:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: 2024 Serokell
2 | #
3 | # SPDX-License-Identifier: MPL-2.0
4 | { pkgs, ... }:
5 | {
6 | nix.settings.trusted-users = [ "deploy" ];
7 | users = let
8 | inherit (import "${pkgs.path}/nixos/tests/ssh-keys.nix" pkgs) snakeOilPublicKey;
9 | in {
10 | mutableUsers = false;
11 | users = {
12 | deploy = {
13 | password = "";
14 | isNormalUser = true;
15 | createHome = true;
16 | openssh.authorizedKeys.keys = [ snakeOilPublicKey ];
17 | };
18 | root.openssh.authorizedKeys.keys = [ snakeOilPublicKey ];
19 | };
20 | };
21 | services.openssh.enable = true;
22 | virtualisation.writableStore = true;
23 | }
24 |
--------------------------------------------------------------------------------
/nix/tests/common.nix:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: 2024 Serokell
2 | #
3 | # SPDX-License-Identifier: MPL-2.0
4 |
5 | {inputs, pkgs, flakes, ...}: {
6 | nix = {
7 | registry.nixpkgs.flake = inputs.nixpkgs;
8 | nixPath = [ "nixpkgs=${inputs.nixpkgs}" ];
9 | extraOptions = ''
10 | experimental-features = ${if flakes then "nix-command flakes" else "nix-command"}
11 | '';
12 | settings = {
13 | trusted-users = [ "root" "@wheel" ];
14 | substituters = pkgs.lib.mkForce [];
15 | };
16 | };
17 |
18 | # The "nixos-test-profile" profile disables the `switch-to-configuration` script by default
19 | system.switch.enable = true;
20 |
21 | virtualisation.graphics = false;
22 | virtualisation.memorySize = 1536;
23 | boot.loader.grub.enable = false;
24 | documentation.enable = false;
25 | }
26 |
--------------------------------------------------------------------------------
/.github/workflows/check.yml:
--------------------------------------------------------------------------------
1 | name: Nix flake check
2 | on: pull_request
3 |
4 | jobs:
5 | get-matrix:
6 | runs-on: [self-hosted, nix]
7 | outputs:
8 | check-matrix: ${{ steps.set-check-matrix.outputs.matrix }}
9 | steps:
10 | - uses: actions/checkout@v4
11 |
12 | - id: set-check-matrix
13 | run: echo "matrix=$(nix eval --json .#check-matrix.x86_64-linux)" >> $GITHUB_OUTPUT
14 |
15 | check:
16 | needs: get-matrix
17 | name: check ${{ matrix.check }}
18 | runs-on: [self-hosted, nix]
19 | strategy:
20 | fail-fast: false
21 | # this matrix consists of the names of all checks defined in flake.nix
22 | matrix: ${{fromJson(needs.get-matrix.outputs.check-matrix)}}
23 | steps:
24 | - uses: actions/checkout@v4
25 |
26 | - name: check
27 | run: nix build -L .#checks.x86_64-linux.${{ matrix.check }}
28 |
--------------------------------------------------------------------------------
/examples/system/common.nix:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: 2020 Serokell
2 | #
3 | # SPDX-License-Identifier: MPL-2.0
4 |
5 | {
6 | boot.loader.systemd-boot.enable = true;
7 |
8 | fileSystems."/" = {
9 | device = "/dev/disk/by-uuid/00000000-0000-0000-0000-000000000000";
10 | fsType = "btrfs";
11 | };
12 |
13 | users.users.admin = {
14 | isNormalUser = true;
15 | extraGroups = [ "wheel" "sudo" ];
16 | password = "123";
17 | };
18 |
19 | services.openssh = { enable = true; };
20 |
21 | # Another option would be root on the server
22 | security.sudo.extraRules = [{
23 | groups = [ "wheel" ];
24 | commands = [{
25 | command = "ALL";
26 | options = [ "NOPASSWD" ];
27 | }];
28 | }];
29 |
30 | nix.binaryCachePublicKeys = [
31 | (builtins.readFile ./nix-pub.pem)
32 | "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
33 | ];
34 | }
35 |
--------------------------------------------------------------------------------
/examples/system/hello.nix:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: 2020 Serokell
2 | #
3 | # SPDX-License-Identifier: MPL-2.0
4 |
5 | nixpkgs:
6 | let
7 | pkgs = nixpkgs.legacyPackages.x86_64-linux;
8 | generateSystemd = type: name: config:
9 | (nixpkgs.lib.nixosSystem {
10 | modules = [{ systemd."${type}s".${name} = config; }];
11 | system = "x86_64-linux";
12 | }).config.systemd.units."${name}.${type}".text;
13 |
14 | mkService = generateSystemd "service";
15 |
16 | service = pkgs.writeTextFile {
17 | name = "hello.service";
18 | text = mkService "hello" {
19 | unitConfig.WantedBy = [ "multi-user.target" ];
20 | path = [ pkgs.hello ];
21 | script = "hello";
22 | };
23 | };
24 | in pkgs.writeShellScriptBin "activate" ''
25 | mkdir -p $HOME/.config/systemd/user
26 | rm $HOME/.config/systemd/user/hello.service
27 | ln -s ${service} $HOME/.config/systemd/user/hello.service
28 | systemctl --user daemon-reload
29 | systemctl --user restart hello
30 | ''
31 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: 2020 Serokell
2 | #
3 | # SPDX-License-Identifier: MPL-2.0
4 |
5 | [package]
6 | name = "deploy-rs"
7 | version = "0.1.0"
8 | authors = ["notgne2 ", "Serokell "]
9 | edition = "2018"
10 |
11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
12 |
13 | [dependencies]
14 | clap = { version = "4", features = [ "wrap_help", "derive" ] }
15 | dirs = "6"
16 | flexi_logger = "0.16"
17 | fork = "0.2"
18 | futures-util = "0.3.31"
19 | log = "0.4"
20 | merge = "0.1.0"
21 | notify = "8.0"
22 | rnix = "0.8"
23 | serde = { version = "1.0.219", features = [ "derive" ] }
24 | serde_json = "1.0.140"
25 | signal-hook = "0.3"
26 | thiserror = "2.0"
27 | tokio = { version = "1.44.0", features = [ "process", "macros", "sync", "rt-multi-thread", "fs", "time", "io-util" ] }
28 | toml = "0.8"
29 | whoami = "1.6"
30 | yn = "0.1"
31 | rpassword = "7.3.1"
32 |
33 |
34 | [lib]
35 | name = "deploy"
36 | path = "src/lib.rs"
37 |
38 | [profile.release]
39 | lto = true
40 | opt-level = "s"
41 | codegen-units = 1
42 |
--------------------------------------------------------------------------------
/examples/darwin/flake.nix:
--------------------------------------------------------------------------------
1 | {
2 | description = "Deploy simple 'darwinSystem' to a darwin machine";
3 |
4 | inputs.deploy-rs.url = "github:serokell/deploy-rs";
5 | inputs.darwin.url = "github:LnL7/nix-darwin";
6 |
7 | outputs = { self, nixpkgs, deploy-rs, darwin }: {
8 | darwinConfigurations.example = darwin.lib.darwinSystem {
9 | system = "x86_64-darwin";
10 | modules = [
11 | ({lib, config, pkgs, ...}: {
12 | services.nix-daemon.enable = true;
13 | nix = {
14 | settings = {
15 | trusted-users = [ "rvem" ];
16 | };
17 | extraOptions = ''
18 | experimental-features = flakes nix-command
19 | '';
20 | };
21 | # nix commands are added to PATH in the zsh config
22 | programs.zsh.enable = true;
23 | })
24 | ];
25 | };
26 | deploy = {
27 | # remoteBuild = true; # Uncomment in case the system you're deploying from is not darwin
28 | nodes.example = {
29 | hostname = "localhost";
30 | profiles.system = {
31 | user = "root";
32 | path = deploy-rs.lib.x86_64-darwin.activate.darwin self.darwinConfigurations.example;
33 | };
34 | };
35 | };
36 |
37 | checks = builtins.mapAttrs (system: deployLib: deployLib.deployChecks self.deploy) deploy-rs.lib;
38 | };
39 | }
40 |
--------------------------------------------------------------------------------
/examples/system/flake.nix:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: 2020 Serokell
2 | #
3 | # SPDX-License-Identifier: MPL-2.0
4 |
5 | {
6 | description = "Deploy a full system with hello service as a separate profile";
7 |
8 | inputs.deploy-rs.url = "github:serokell/deploy-rs";
9 |
10 | outputs = { self, nixpkgs, deploy-rs }: {
11 | nixosConfigurations.example-nixos-system = nixpkgs.lib.nixosSystem {
12 | system = "x86_64-linux";
13 | modules = [ ./configuration.nix ];
14 | };
15 |
16 | nixosConfigurations.bare = nixpkgs.lib.nixosSystem {
17 | system = "x86_64-linux";
18 | modules =
19 | [ ./bare.nix "${nixpkgs}/nixos/modules/virtualisation/qemu-vm.nix" ];
20 | };
21 |
22 | # This is the application we actually want to run
23 | defaultPackage.x86_64-linux = import ./hello.nix nixpkgs;
24 |
25 | deploy.nodes.example = {
26 | sshOpts = [ "-p" "2221" ];
27 | hostname = "localhost";
28 | fastConnection = true;
29 | interactiveSudo = true;
30 | profiles = {
31 | system = {
32 | sshUser = "admin";
33 | path =
34 | deploy-rs.lib.x86_64-linux.activate.nixos self.nixosConfigurations.example-nixos-system;
35 | user = "root";
36 | };
37 | hello = {
38 | sshUser = "hello";
39 | path = deploy-rs.lib.x86_64-linux.activate.custom self.defaultPackage.x86_64-linux "./bin/activate";
40 | user = "hello";
41 | };
42 | };
43 | };
44 |
45 | checks = builtins.mapAttrs (system: deployLib: deployLib.deployChecks self.deploy) deploy-rs.lib;
46 | };
47 | }
48 |
--------------------------------------------------------------------------------
/flake.lock:
--------------------------------------------------------------------------------
1 | {
2 | "nodes": {
3 | "flake-compat": {
4 | "flake": false,
5 | "locked": {
6 | "lastModified": 1733328505,
7 | "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=",
8 | "owner": "edolstra",
9 | "repo": "flake-compat",
10 | "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec",
11 | "type": "github"
12 | },
13 | "original": {
14 | "owner": "edolstra",
15 | "repo": "flake-compat",
16 | "type": "github"
17 | }
18 | },
19 | "nixpkgs": {
20 | "locked": {
21 | "lastModified": 1743014863,
22 | "narHash": "sha256-jAIUqsiN2r3hCuHji80U7NNEafpIMBXiwKlSrjWMlpg=",
23 | "owner": "NixOS",
24 | "repo": "nixpkgs",
25 | "rev": "bd3bac8bfb542dbde7ffffb6987a1a1f9d41699f",
26 | "type": "github"
27 | },
28 | "original": {
29 | "owner": "NixOS",
30 | "ref": "nixpkgs-unstable",
31 | "repo": "nixpkgs",
32 | "type": "github"
33 | }
34 | },
35 | "root": {
36 | "inputs": {
37 | "flake-compat": "flake-compat",
38 | "nixpkgs": "nixpkgs",
39 | "utils": "utils"
40 | }
41 | },
42 | "systems": {
43 | "locked": {
44 | "lastModified": 1681028828,
45 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
46 | "owner": "nix-systems",
47 | "repo": "default",
48 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
49 | "type": "github"
50 | },
51 | "original": {
52 | "owner": "nix-systems",
53 | "repo": "default",
54 | "type": "github"
55 | }
56 | },
57 | "utils": {
58 | "inputs": {
59 | "systems": "systems"
60 | },
61 | "locked": {
62 | "lastModified": 1731533236,
63 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
64 | "owner": "numtide",
65 | "repo": "flake-utils",
66 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
67 | "type": "github"
68 | },
69 | "original": {
70 | "owner": "numtide",
71 | "repo": "flake-utils",
72 | "type": "github"
73 | }
74 | }
75 | },
76 | "root": "root",
77 | "version": 7
78 | }
79 |
--------------------------------------------------------------------------------
/nix/tests/deploy-flake.nix:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: 2024 Serokell
2 | #
3 | # SPDX-License-Identifier: MPL-2.0
4 |
5 | {
6 | inputs = {
7 | # real inputs are substituted in ./default.nix
8 | ##inputs##
9 | };
10 |
11 | outputs = { self, nixpkgs, deploy-rs, ... }@inputs: let
12 | system = "x86_64-linux";
13 | pkgs = inputs.nixpkgs.legacyPackages.${system};
14 | user = "deploy";
15 | in {
16 | nixosConfigurations.server = nixpkgs.lib.nixosSystem {
17 | inherit system pkgs;
18 | specialArgs = { inherit inputs; flakes = import inputs.enable-flakes; };
19 | modules = [
20 | ./server.nix
21 | ./common.nix
22 | # Import the base config used by nixos tests
23 | (pkgs.path + "/nixos/lib/testing/nixos-test-base.nix")
24 | # Deployment breaks the network settings, so we need to restore them
25 | (pkgs.lib.importJSON ./network.json)
26 | # Deploy packages
27 | { environment.systemPackages = [ pkgs.figlet pkgs.hello ]; }
28 | ];
29 | };
30 |
31 | deploy.nodes = {
32 | server = {
33 | hostname = "server";
34 | sshUser = "root";
35 | sshOpts = [
36 | "-o" "StrictHostKeyChecking=no"
37 | "-o" "StrictHostKeyChecking=no"
38 | ];
39 | profiles.system.path = deploy-rs.lib."${system}".activate.nixos self.nixosConfigurations.server;
40 | };
41 | server-override = {
42 | hostname = "override";
43 | sshUser = "override";
44 | user = "override";
45 | sudo = "override";
46 | sshOpts = [ ];
47 | confirmTimeout = 0;
48 | activationTimeout = 0;
49 | profiles.system.path = deploy-rs.lib."${system}".activate.nixos self.nixosConfigurations.server;
50 | };
51 | profile = {
52 | hostname = "server";
53 | sshUser = "${user}";
54 | sshOpts = [
55 | "-o" "UserKnownHostsFile=/dev/null"
56 | "-o" "StrictHostKeyChecking=no"
57 | ];
58 | profiles = {
59 | "hello-world".path = let
60 | activateProfile = pkgs.writeShellScriptBin "activate" ''
61 | set -euo pipefail
62 | mkdir -p /home/${user}/.nix-profile/bin
63 | rm -f -- /home/${user}/.nix-profile/bin/hello /home/${user}/.nix-profile/bin/figlet
64 | ln -s ${pkgs.hello}/bin/hello /home/${user}/.nix-profile/bin/hello
65 | ln -s ${pkgs.figlet}/bin/figlet /home/${user}/.nix-profile/bin/figlet
66 | '';
67 | in deploy-rs.lib.${system}.activate.custom activateProfile "$PROFILE/bin/activate";
68 | };
69 | };
70 | };
71 | };
72 | }
73 |
--------------------------------------------------------------------------------
/src/data.rs:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2020 Serokell
2 | //
3 | // SPDX-License-Identifier: MPL-2.0
4 |
5 | use merge::Merge;
6 | use serde::Deserialize;
7 | use std::collections::HashMap;
8 | use std::path::PathBuf;
9 |
10 | #[derive(Deserialize, Debug, Clone, Merge)]
11 | pub struct GenericSettings {
12 | #[serde(rename(deserialize = "sshUser"))]
13 | pub ssh_user: Option,
14 | pub user: Option,
15 | #[serde(
16 | skip_serializing_if = "Vec::is_empty",
17 | default,
18 | rename(deserialize = "sshOpts")
19 | )]
20 | #[merge(strategy = merge::vec::append)]
21 | pub ssh_opts: Vec,
22 | #[serde(rename(deserialize = "fastConnection"))]
23 | pub fast_connection: Option,
24 | #[serde(rename(deserialize = "autoRollback"))]
25 | pub auto_rollback: Option,
26 | #[serde(rename(deserialize = "confirmTimeout"))]
27 | pub confirm_timeout: Option,
28 | #[serde(rename(deserialize = "activationTimeout"))]
29 | pub activation_timeout: Option,
30 | #[serde(rename(deserialize = "tempPath"))]
31 | pub temp_path: Option,
32 | #[serde(rename(deserialize = "magicRollback"))]
33 | pub magic_rollback: Option,
34 | #[serde(rename(deserialize = "sudo"))]
35 | pub sudo: Option,
36 | #[serde(default,rename(deserialize = "remoteBuild"))]
37 | pub remote_build: Option,
38 | #[serde(rename(deserialize = "interactiveSudo"))]
39 | pub interactive_sudo: Option,
40 | }
41 |
42 | #[derive(Deserialize, Debug, Clone)]
43 | pub struct NodeSettings {
44 | pub hostname: String,
45 | pub profiles: HashMap,
46 | #[serde(
47 | skip_serializing_if = "Vec::is_empty",
48 | default,
49 | rename(deserialize = "profilesOrder")
50 | )]
51 | pub profiles_order: Vec,
52 | }
53 |
54 | #[derive(Deserialize, Debug, Clone)]
55 | pub struct ProfileSettings {
56 | pub path: String,
57 | #[serde(rename(deserialize = "profilePath"))]
58 | pub profile_path: Option,
59 | }
60 |
61 | #[derive(Deserialize, Debug, Clone)]
62 | pub struct Profile {
63 | #[serde(flatten)]
64 | pub profile_settings: ProfileSettings,
65 | #[serde(flatten)]
66 | pub generic_settings: GenericSettings,
67 | }
68 |
69 | #[derive(Deserialize, Debug, Clone)]
70 | pub struct Node {
71 | #[serde(flatten)]
72 | pub generic_settings: GenericSettings,
73 | #[serde(flatten)]
74 | pub node_settings: NodeSettings,
75 | }
76 |
77 | #[derive(Deserialize, Debug, Clone)]
78 | pub struct Data {
79 | #[serde(flatten)]
80 | pub generic_settings: GenericSettings,
81 | pub nodes: HashMap,
82 | }
83 |
--------------------------------------------------------------------------------
/examples/simple/flake.lock:
--------------------------------------------------------------------------------
1 | {
2 | "nodes": {
3 | "deploy-rs": {
4 | "inputs": {
5 | "flake-compat": "flake-compat",
6 | "naersk": "naersk",
7 | "nixpkgs": "nixpkgs",
8 | "utils": "utils"
9 | },
10 | "locked": {
11 | "lastModified": 1603740297,
12 | "narHash": "sha256-yeTrA8AaLzDFICApX725gQhKoHNI2TCqWAeOl9axVZE=",
13 | "owner": "serokell",
14 | "repo": "deploy-rs",
15 | "rev": "426fb3c489dcbb4ccbf98a3ab6a7fe25e71b95ca",
16 | "type": "github"
17 | },
18 | "original": {
19 | "owner": "serokell",
20 | "repo": "deploy-rs",
21 | "type": "github"
22 | }
23 | },
24 | "flake-compat": {
25 | "flake": false,
26 | "locked": {
27 | "lastModified": 1600853454,
28 | "narHash": "sha256-EgsgbcJNZ9AQLVhjhfiegGjLbO+StBY9hfKsCwc8Hw8=",
29 | "owner": "edolstra",
30 | "repo": "flake-compat",
31 | "rev": "94cf59784c73ecec461eaa291918eff0bfb538ac",
32 | "type": "github"
33 | },
34 | "original": {
35 | "owner": "edolstra",
36 | "repo": "flake-compat",
37 | "type": "github"
38 | }
39 | },
40 | "naersk": {
41 | "inputs": {
42 | "nixpkgs": [
43 | "nixpkgs"
44 | ]
45 | },
46 | "locked": {
47 | "lastModified": 1602173141,
48 | "narHash": "sha256-m6wU6lP0wf2OMw3KtJqn27ITtg29+ftciGHicLiVSGE=",
49 | "owner": "nmattia",
50 | "repo": "naersk",
51 | "rev": "22b96210b2433228d42bce460f3befbdcfde7520",
52 | "type": "github"
53 | },
54 | "original": {
55 | "owner": "nmattia",
56 | "ref": "master",
57 | "repo": "naersk",
58 | "type": "github"
59 | }
60 | },
61 | "nixpkgs": {
62 | "locked": {
63 | "lastModified": 1601961544,
64 | "narHash": "sha256-uuh9CkDWkXlXse8IcergqoIM5JffqfQDKsl1uHB7XJI=",
65 | "owner": "NixOS",
66 | "repo": "nixpkgs",
67 | "rev": "89281dd1dfed6839610f0ccad0c0e493606168fe",
68 | "type": "github"
69 | },
70 | "original": {
71 | "owner": "NixOS",
72 | "ref": "nixpkgs-unstable",
73 | "repo": "nixpkgs",
74 | "type": "github"
75 | }
76 | },
77 | "nixpkgs_2": {
78 | "locked": {
79 | "lastModified": 1603739127,
80 | "narHash": "sha256-mdLESpo4jXrAynLp7ypRaqkx6IS1jx2l78f1tg9iiJU=",
81 | "owner": "NixOS",
82 | "repo": "nixpkgs",
83 | "rev": "d699505277b99e4698d90563c5eb1b62ba5ba0ea",
84 | "type": "github"
85 | },
86 | "original": {
87 | "id": "nixpkgs",
88 | "type": "indirect"
89 | }
90 | },
91 | "root": {
92 | "inputs": {
93 | "deploy-rs": "deploy-rs",
94 | "nixpkgs": "nixpkgs_2"
95 | }
96 | },
97 | "utils": {
98 | "locked": {
99 | "lastModified": 1601282935,
100 | "narHash": "sha256-WQAFV6sGGQxrRs3a+/Yj9xUYvhTpukQJIcMbIi7LCJ4=",
101 | "owner": "numtide",
102 | "repo": "flake-utils",
103 | "rev": "588973065fce51f4763287f0fda87a174d78bf48",
104 | "type": "github"
105 | },
106 | "original": {
107 | "owner": "numtide",
108 | "repo": "flake-utils",
109 | "type": "github"
110 | }
111 | }
112 | },
113 | "root": "root",
114 | "version": 7
115 | }
116 |
--------------------------------------------------------------------------------
/examples/system/flake.lock:
--------------------------------------------------------------------------------
1 | {
2 | "nodes": {
3 | "deploy-rs": {
4 | "inputs": {
5 | "flake-compat": "flake-compat",
6 | "naersk": "naersk",
7 | "nixpkgs": "nixpkgs",
8 | "utils": "utils"
9 | },
10 | "locked": {
11 | "lastModified": 1603740297,
12 | "narHash": "sha256-yeTrA8AaLzDFICApX725gQhKoHNI2TCqWAeOl9axVZE=",
13 | "owner": "serokell",
14 | "repo": "deploy-rs",
15 | "rev": "426fb3c489dcbb4ccbf98a3ab6a7fe25e71b95ca",
16 | "type": "github"
17 | },
18 | "original": {
19 | "owner": "serokell",
20 | "repo": "deploy-rs",
21 | "type": "github"
22 | }
23 | },
24 | "flake-compat": {
25 | "flake": false,
26 | "locked": {
27 | "lastModified": 1600853454,
28 | "narHash": "sha256-EgsgbcJNZ9AQLVhjhfiegGjLbO+StBY9hfKsCwc8Hw8=",
29 | "owner": "edolstra",
30 | "repo": "flake-compat",
31 | "rev": "94cf59784c73ecec461eaa291918eff0bfb538ac",
32 | "type": "github"
33 | },
34 | "original": {
35 | "owner": "edolstra",
36 | "repo": "flake-compat",
37 | "type": "github"
38 | }
39 | },
40 | "naersk": {
41 | "inputs": {
42 | "nixpkgs": [
43 | "nixpkgs"
44 | ]
45 | },
46 | "locked": {
47 | "lastModified": 1602173141,
48 | "narHash": "sha256-m6wU6lP0wf2OMw3KtJqn27ITtg29+ftciGHicLiVSGE=",
49 | "owner": "nmattia",
50 | "repo": "naersk",
51 | "rev": "22b96210b2433228d42bce460f3befbdcfde7520",
52 | "type": "github"
53 | },
54 | "original": {
55 | "owner": "nmattia",
56 | "ref": "master",
57 | "repo": "naersk",
58 | "type": "github"
59 | }
60 | },
61 | "nixpkgs": {
62 | "locked": {
63 | "lastModified": 1601961544,
64 | "narHash": "sha256-uuh9CkDWkXlXse8IcergqoIM5JffqfQDKsl1uHB7XJI=",
65 | "owner": "NixOS",
66 | "repo": "nixpkgs",
67 | "rev": "89281dd1dfed6839610f0ccad0c0e493606168fe",
68 | "type": "github"
69 | },
70 | "original": {
71 | "owner": "NixOS",
72 | "ref": "nixpkgs-unstable",
73 | "repo": "nixpkgs",
74 | "type": "github"
75 | }
76 | },
77 | "nixpkgs_2": {
78 | "locked": {
79 | "lastModified": 1603739127,
80 | "narHash": "sha256-mdLESpo4jXrAynLp7ypRaqkx6IS1jx2l78f1tg9iiJU=",
81 | "owner": "NixOS",
82 | "repo": "nixpkgs",
83 | "rev": "d699505277b99e4698d90563c5eb1b62ba5ba0ea",
84 | "type": "github"
85 | },
86 | "original": {
87 | "id": "nixpkgs",
88 | "type": "indirect"
89 | }
90 | },
91 | "root": {
92 | "inputs": {
93 | "deploy-rs": "deploy-rs",
94 | "nixpkgs": "nixpkgs_2"
95 | }
96 | },
97 | "utils": {
98 | "locked": {
99 | "lastModified": 1601282935,
100 | "narHash": "sha256-WQAFV6sGGQxrRs3a+/Yj9xUYvhTpukQJIcMbIi7LCJ4=",
101 | "owner": "numtide",
102 | "repo": "flake-utils",
103 | "rev": "588973065fce51f4763287f0fda87a174d78bf48",
104 | "type": "github"
105 | },
106 | "original": {
107 | "owner": "numtide",
108 | "repo": "flake-utils",
109 | "type": "github"
110 | }
111 | }
112 | },
113 | "root": "root",
114 | "version": 7
115 | }
116 |
--------------------------------------------------------------------------------
/interface.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json-schema.org/draft/2019-09/schema#",
3 | "title": "Deploy",
4 | "description": "Matches a correct deploy attribute of a flake",
5 | "definitions": {
6 | "generic_settings": {
7 | "type": "object",
8 | "properties": {
9 | "sshUser": {
10 | "type": "string"
11 | },
12 | "user": {
13 | "type": "string"
14 | },
15 | "sshOpts": {
16 | "type": "array",
17 | "items": {
18 | "type": "string"
19 | }
20 | },
21 | "fastConnection": {
22 | "type": "boolean"
23 | },
24 | "autoRollback": {
25 | "type": "boolean"
26 | },
27 | "magicRollback": {
28 | "type": "boolean"
29 | },
30 | "confirmTimeout": {
31 | "type": "integer"
32 | },
33 | "activationTimeout": {
34 | "type": "integer"
35 | },
36 | "tempPath": {
37 | "type": "string"
38 | },
39 | "interactiveSudo": {
40 | "type": "boolean"
41 | }
42 | }
43 | },
44 | "node_settings": {
45 | "type": "object",
46 | "properties": {
47 | "hostname": {
48 | "type": "string"
49 | },
50 | "profilesOrder": {
51 | "type": "array",
52 | "items": {
53 | "type": "string"
54 | },
55 | "uniqueItems": true
56 | },
57 | "profiles": {
58 | "type": "object",
59 | "patternProperties": {
60 | "[A-z][A-z0-9_-]*": {
61 | "allOf": [
62 | {
63 | "$ref": "#/definitions/generic_settings"
64 | },
65 | {
66 | "$ref": "#/definitions/profile_settings"
67 | }
68 | ]
69 | }
70 | },
71 | "additionalProperties": false
72 | }
73 | },
74 | "required": [
75 | "hostname"
76 | ]
77 | },
78 | "profile_settings": {
79 | "type": "object",
80 | "properties": {
81 | "path": {
82 | "type": "string"
83 | },
84 | "profilePath": {
85 | "type": "string"
86 | }
87 | },
88 | "required": [
89 | "path"
90 | ]
91 | }
92 | },
93 | "type": "object",
94 | "allOf": [
95 | {
96 | "$ref": "#/definitions/generic_settings"
97 | },
98 | {
99 | "type": "object",
100 | "properties": {
101 | "nodes": {
102 | "type": "object",
103 | "patternProperties": {
104 | "[A-z][A-z0-9_-]*": {
105 | "allOf": [
106 | {
107 | "$ref": "#/definitions/generic_settings"
108 | },
109 | {
110 | "$ref": "#/definitions/node_settings"
111 | }
112 | ]
113 | }
114 | },
115 | "additionalProperties": false
116 | }
117 | }
118 | }
119 | ]
120 | }
121 |
--------------------------------------------------------------------------------
/nix/tests/default.nix:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: 2024 Serokell
2 | #
3 | # SPDX-License-Identifier: MPL-2.0
4 |
5 | { pkgs , inputs , ... }:
6 | let
7 | inherit (pkgs) lib;
8 |
9 | inherit (import "${pkgs.path}/nixos/tests/ssh-keys.nix" pkgs) snakeOilPrivateKey;
10 |
11 | # Include all build dependencies to be able to build profiles offline
12 | allDrvOutputs = pkg: pkgs.runCommand "allDrvOutputs" { refs = pkgs.writeClosure pkg.drvPath; } ''
13 | touch $out
14 | while read ref; do
15 | case $ref in
16 | *.drv)
17 | cat $ref >>$out
18 | ;;
19 | esac
20 | done <$refs
21 | '';
22 |
23 | mkTest = { name ? "", user ? "root", flakes ? true, isLocal ? true, deployArgs }: let
24 | nodes = {
25 | server = { nodes, ... }: {
26 | imports = [
27 | ./server.nix
28 | (import ./common.nix { inherit inputs pkgs flakes; })
29 | ];
30 | virtualisation.additionalPaths = lib.optionals (!isLocal) [
31 | pkgs.hello
32 | pkgs.figlet
33 | (allDrvOutputs nodes.server.system.build.toplevel)
34 | pkgs.deploy-rs.deploy-rs
35 | ];
36 | };
37 | client = { nodes, ... }: {
38 | imports = [ (import ./common.nix { inherit inputs pkgs flakes; }) ];
39 | environment.systemPackages = [ pkgs.deploy-rs.deploy-rs ];
40 | # nix evaluation takes a lot of memory, especially in non-flake usage
41 | virtualisation.memorySize = lib.mkForce 4096;
42 | virtualisation.additionalPaths = lib.optionals isLocal [
43 | pkgs.hello
44 | pkgs.figlet
45 | (allDrvOutputs nodes.server.system.build.toplevel)
46 | ];
47 | };
48 | };
49 |
50 | flakeInputs = ''
51 | deploy-rs.url = "${../..}";
52 | deploy-rs.inputs.utils.follows = "utils";
53 | deploy-rs.inputs.flake-compat.follows = "flake-compat";
54 |
55 | nixpkgs.url = "${inputs.nixpkgs}";
56 | utils.url = "${inputs.utils}";
57 | utils.inputs.systems.follows = "systems";
58 | systems.url = "${inputs.utils.inputs.systems}";
59 | flake-compat.url = "${inputs.flake-compat}";
60 | flake-compat.flake = false;
61 |
62 | enable-flakes.url = "${builtins.toFile "use-flakes" (if flakes then "true" else "false")}";
63 | enable-flakes.flake = false;
64 | '';
65 |
66 | flake = builtins.toFile "flake.nix"
67 | (lib.replaceStrings [ "##inputs##" ] [ flakeInputs ] (builtins.readFile ./deploy-flake.nix));
68 |
69 | flakeCompat = builtins.toFile "default.nix" ''
70 | (import
71 | (
72 | let
73 | lock = builtins.fromJSON (builtins.readFile ./flake.lock);
74 | in
75 | fetchTarball {
76 | url = "https://not-used-we-fetch-by-hash";
77 | sha256 = lock.nodes.flake-compat.locked.narHash;
78 | }
79 | )
80 | { src = ./.; }
81 | ).defaultNix
82 | '';
83 |
84 | in pkgs.nixosTest {
85 | inherit nodes name;
86 |
87 | testScript = { nodes }: let
88 | serverNetworkJSON = pkgs.writeText "server-network.json"
89 | (builtins.toJSON nodes.server.system.build.networkConfig);
90 | in ''
91 | start_all()
92 |
93 | # Prepare
94 | client.succeed("mkdir tmp && cd tmp")
95 | client.succeed("cp ${flake} ./flake.nix")
96 | client.succeed("cp ${flakeCompat} ./default.nix")
97 | client.succeed("cp ${./server.nix} ./server.nix")
98 | client.succeed("cp ${./common.nix} ./common.nix")
99 | client.succeed("cp ${serverNetworkJSON} ./network.json")
100 | client.succeed("nix --extra-experimental-features flakes flake lock")
101 |
102 | # Setup SSH key
103 | client.succeed("mkdir -m 700 /root/.ssh")
104 | client.succeed('cp --no-preserve=mode ${snakeOilPrivateKey} /root/.ssh/id_ed25519')
105 | client.succeed("chmod 600 /root/.ssh/id_ed25519")
106 |
107 | # Test SSH connection
108 | server.wait_for_open_port(22)
109 | client.wait_for_unit("network.target")
110 | client.succeed(
111 | "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no server 'echo hello world' >&2",
112 | timeout=30
113 | )
114 |
115 | # Make sure the hello and figlet packages are missing
116 | server.fail("su ${user} -l -c 'hello | figlet'")
117 |
118 | # Deploy to the server
119 | client.succeed("deploy ${deployArgs}")
120 |
121 | # Make sure packages are present after deployment
122 | server.succeed("su ${user} -l -c 'hello | figlet' >&2")
123 | '';
124 | };
125 | in {
126 | # Deployment with client-side build
127 | local-build = mkTest {
128 | name = "local-build";
129 | deployArgs = "-s .#server -- --offline";
130 | };
131 | # Deployment with server-side build
132 | remote-build = mkTest {
133 | name = "remote-build";
134 | isLocal = false;
135 | deployArgs = "-s .#server --remote-build -- --offline";
136 | };
137 | non-flake-remote-build = mkTest {
138 | name = "non-flake-remote-build";
139 | isLocal = false;
140 | flakes = false;
141 | deployArgs = "-s .#server --remote-build";
142 | };
143 | # Deployment with overridden options
144 | options-overriding = mkTest {
145 | name = "options-overriding";
146 | deployArgs = lib.concatStrings [
147 | "-s .#server-override"
148 | " --hostname server --profile-user root --ssh-user root --sudo 'sudo -u'"
149 | " --ssh-opts='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null'"
150 | " --confirm-timeout 30 --activation-timeout 30"
151 | " -- --offline"
152 | ];
153 | };
154 | # User profile deployment
155 | profile = mkTest {
156 | name = "profile";
157 | user = "deploy";
158 | deployArgs = "-s .#profile -- --offline";
159 | };
160 | hyphen-ssh-opts-regression = mkTest {
161 | name = "profile";
162 | user = "deploy";
163 | deployArgs = "-s .#profile --ssh-opts '-p 22 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' -- --offline";
164 | };
165 | # Deployment using a non-flake nix
166 | non-flake-build = mkTest {
167 | name = "non-flake-build";
168 | flakes = false;
169 | deployArgs = "-s .#server";
170 | };
171 | non-flake-with-flakes = mkTest {
172 | name = "non-flake-with-flakes";
173 | flakes = true;
174 | deployArgs = "--file . --targets server";
175 | };
176 | }
177 |
--------------------------------------------------------------------------------
/flake.nix:
--------------------------------------------------------------------------------
1 | # SPDX-FileCopyrightText: 2020 Serokell
2 | # SPDX-FileCopyrightText: 2020 Andreas Fuchs
3 | #
4 | # SPDX-License-Identifier: MPL-2.0
5 |
6 | {
7 | description = "A Simple multi-profile Nix-flake deploy tool.";
8 |
9 | inputs = {
10 | nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
11 | utils.url = "github:numtide/flake-utils";
12 | flake-compat = {
13 | url = "github:edolstra/flake-compat";
14 | flake = false;
15 | };
16 | };
17 |
18 | outputs = { self, nixpkgs, utils, ... }@inputs:
19 | {
20 | overlays.default = final: prev:
21 | {
22 | deploy-rs = {
23 |
24 | deploy-rs = final.rustPlatform.buildRustPackage {
25 | pname = "deploy-rs";
26 | version = "0.1.0";
27 |
28 | src = final.lib.sourceByRegex ./. [
29 | "Cargo\.lock"
30 | "Cargo\.toml"
31 | "src"
32 | "src/bin"
33 | ".*\.rs$"
34 | ];
35 |
36 | cargoLock.lockFile = ./Cargo.lock;
37 |
38 | meta = {
39 | description = "A Simple multi-profile Nix-flake deploy tool";
40 | mainProgram = "deploy";
41 | };
42 | };
43 |
44 | lib = rec {
45 |
46 | setActivate = builtins.trace
47 | "deploy-rs#lib.setActivate is deprecated, use activate.noop, activate.nixos or activate.custom instead"
48 | activate.custom;
49 |
50 | activate = rec {
51 | custom =
52 | {
53 | __functor = customSelf: base: activate:
54 | final.buildEnv {
55 | name = ("activatable-" + base.name);
56 | paths =
57 | [
58 | base
59 | (final.writeTextFile {
60 | name = base.name + "-activate-path";
61 | text = ''
62 | #!${final.runtimeShell}
63 | set -euo pipefail
64 |
65 | if [[ "''${DRY_ACTIVATE:-}" == "1" ]]
66 | then
67 | ${customSelf.dryActivate or "echo ${final.writeScript "activate" activate}"}
68 | elif [[ "''${BOOT:-}" == "1" ]]
69 | then
70 | ${customSelf.boot or "echo ${final.writeScript "activate" activate}"}
71 | else
72 | ${activate}
73 | fi
74 | '';
75 | executable = true;
76 | destination = "/deploy-rs-activate";
77 | })
78 | (final.writeTextFile {
79 | name = base.name + "-activate-rs";
80 | text = ''
81 | #!${final.runtimeShell}
82 | exec ${final.deploy-rs.deploy-rs}/bin/activate "$@"
83 | '';
84 | executable = true;
85 | destination = "/activate-rs";
86 | })
87 | ];
88 | };
89 | };
90 |
91 | nixos = base:
92 | (custom // {
93 | dryActivate = "$PROFILE/bin/switch-to-configuration dry-activate";
94 | boot = "$PROFILE/bin/switch-to-configuration boot";
95 | })
96 | base.config.system.build.toplevel
97 | ''
98 | # work around https://github.com/NixOS/nixpkgs/issues/73404
99 | cd /tmp
100 |
101 | $PROFILE/bin/switch-to-configuration switch
102 |
103 | # https://github.com/serokell/deploy-rs/issues/31
104 | ${with base.config.boot.loader;
105 | final.lib.optionalString systemd-boot.enable
106 | "sed -i '/^default /d' ${efi.efiSysMountPoint}/loader/loader.conf"}
107 | '';
108 |
109 | home-manager = base: custom base.activationPackage "$PROFILE/activate";
110 |
111 | # Activation script for 'darwinSystem' from nix-darwin.
112 | # 'HOME=/var/root' is needed because 'sudo' on darwin doesn't change 'HOME' directory,
113 | # while 'darwin-rebuild' (which is invoked under the hood) performs some nix-channel
114 | # checks that rely on 'HOME'. As a result, if 'sshUser' is different from root,
115 | # deployment may fail without explicit 'HOME' redefinition.
116 | darwin = base: custom base.config.system.build.toplevel "HOME=/var/root $PROFILE/activate";
117 |
118 | noop = base: custom base ":";
119 | };
120 |
121 | deployChecks = deploy: builtins.mapAttrs (_: check: check deploy) {
122 | deploy-schema = deploy: final.runCommand "jsonschema-deploy-system" { } ''
123 | ${final.check-jsonschema}/bin/check-jsonschema --schemafile ${./interface.json} ${final.writeText "deploy.json" (builtins.toJSON deploy)} && touch $out
124 | '';
125 |
126 | deploy-activate = deploy:
127 | let
128 | profiles = builtins.concatLists (final.lib.mapAttrsToList (nodeName: node: final.lib.mapAttrsToList (profileName: profile: [ (toString profile.path) nodeName profileName ]) node.profiles) deploy.nodes);
129 | in
130 | final.runCommand "deploy-rs-check-activate" { } ''
131 | for x in ${builtins.concatStringsSep " " (map (p: builtins.concatStringsSep ":" p) profiles)}; do
132 | profile_path=$(echo $x | cut -f1 -d:)
133 | node_name=$(echo $x | cut -f2 -d:)
134 | profile_name=$(echo $x | cut -f3 -d:)
135 |
136 | test -f "$profile_path/deploy-rs-activate" || (echo "#$node_name.$profile_name is missing the deploy-rs-activate activation script" && exit 1);
137 |
138 | test -f "$profile_path/activate-rs" || (echo "#$node_name.$profile_name is missing the activate-rs activation script" && exit 1);
139 | done
140 |
141 | touch $out
142 | '';
143 | };
144 | };
145 | };
146 | };
147 | } //
148 | utils.lib.eachSystem (utils.lib.defaultSystems ++ ["aarch64-darwin"]) (system:
149 | let
150 | pkgs = import nixpkgs {
151 | inherit system;
152 | overlays = [ self.overlays.default ];
153 | };
154 |
155 | # make a matrix to use in GitHub pipeline
156 | mkMatrix = name: attrs: {
157 | include = map (v: { ${name} = v; }) (pkgs.lib.attrNames attrs);
158 | };
159 | in
160 | {
161 | packages.default = self.packages."${system}".deploy-rs;
162 | packages.deploy-rs = pkgs.deploy-rs.deploy-rs;
163 |
164 | apps.default = self.apps."${system}".deploy-rs;
165 | apps.deploy-rs = {
166 | type = "app";
167 | program = "${self.packages."${system}".default}/bin/deploy";
168 | };
169 |
170 | devShells.default = pkgs.mkShell {
171 | inputsFrom = [ self.packages.${system}.deploy-rs ];
172 | RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}";
173 | buildInputs = with pkgs; [
174 | nix
175 | cargo
176 | rustc
177 | rust-analyzer
178 | rustfmt
179 | clippy
180 | reuse
181 | rust.packages.stable.rustPlatform.rustLibSrc
182 | ];
183 | };
184 |
185 | checks = {
186 | deploy-rs = self.packages.${system}.default.overrideAttrs (super: { doCheck = true; });
187 | } // (pkgs.lib.optionalAttrs (pkgs.lib.elem system ["x86_64-linux"]) (import ./nix/tests {
188 | inherit inputs pkgs;
189 | }));
190 |
191 | inherit (pkgs.deploy-rs) lib;
192 |
193 | check-matrix = mkMatrix "check" self.checks.${system};
194 | });
195 | }
196 |
--------------------------------------------------------------------------------
/docs/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
102 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
7 |
8 | 
9 |
10 | ---
11 |
12 | A Simple, multi-profile Nix-flake deploy tool.
13 |
14 | Questions? Need help? Join us on Matrix: [`#deploy-rs:matrix.org`](https://matrix.to/#/#deploy-rs:matrix.org)
15 |
16 | ## Usage
17 |
18 | Basic usage: `deploy [options] `.
19 |
20 | Using this method all profiles specified in the given `` will be deployed (taking into account the [`profilesOrder`](#node)).
21 |
22 | Optionally the flake can be constrained to deploy just a single node (`my-flake#my-node`) or a profile (`my-flake#my-node.my-profile`).
23 |
24 | If your profile or node name has a . in it, simply wrap it in quotes, and the flake path in quotes (to avoid shell escaping), for example 'my-flake#"myserver.com".system'.
25 |
26 | Any "extra" arguments will be passed into the Nix calls, so for instance to deploy an impure profile, you may use `deploy . -- --impure` (note the explicit flake path is necessary for doing this).
27 |
28 | You can try out this tool easily with `nix run`:
29 | - `nix run github:serokell/deploy-rs your-flake`
30 |
31 | If you want to deploy multiple flakes or a subset of profiles with one invocation, instead of calling `deploy ` you can issue `deploy --targets [ ...]` where `` is supposed to take the same format as discussed before.
32 |
33 | Running in this mode, if any of the deploys fails, the deploy will be aborted and all successful deploys rolled back. `--rollback-succeeded false` can be used to override this behavior, otherwise the `auto-rollback` argument takes precedent.
34 |
35 | If you require a signing key to push closures to your server, specify the path to it in the `LOCAL_KEY` environment variable.
36 |
37 | Check out `deploy --help` for CLI flags! Remember to check there before making one-time changes to things like hostnames.
38 |
39 | There is also an `activate` binary though this should be ignored, it is only used internally (on the deployed system) and for testing/hacking purposes.
40 |
41 | ## Ideas
42 |
43 | `deploy-rs` is a simple Rust program that will take a Nix flake and use it to deploy any of your defined profiles to your nodes. This is _strongly_ based off of [serokell/deploy](https://github.com/serokell/deploy), designed to replace it and expand upon it.
44 |
45 | ### Multi-profile
46 |
47 | This type of design (as opposed to more traditional tools like NixOps or morph) allows for lesser-privileged deployments, and the ability to update different things independently of each other. You can deploy any type of profile to any user, not just a NixOS profile to `root`.
48 |
49 | ### Magic Rollback
50 |
51 | There is a built-in feature to prevent you making changes that might render your machine unconnectable or unusuable, which works by connecting to the machine after profile activation to confirm the machine is still available, and instructing the target node to automatically roll back if it is not confirmed. If you do not disable `magicRollback` in your configuration (see later sections) or with the CLI flag, you will be unable to make changes to the system which will affect you connecting to it (changing SSH port, changing your IP, etc).
52 |
53 | ## API
54 |
55 | ### Overall usage
56 |
57 | `deploy-rs` is designed to be used with Nix flakes. There is a Flake-less mode of operation which will automatically be used if your available Nix version does not support flakes, however you will likely want to use a flake anyway, just with `flake-compat` (see [this wiki page](https://wiki.nixos.org/wiki/Flakes)) for usage).
58 |
59 | `deploy-rs` also outputs a `lib` attribute, with tools used to make your definitions simpler and safer, including `deploy-rs.lib.${system}.activate` (see later section "Profile"), and `deploy-rs.lib.${system}.deployChecks` which will let `nix flake check` ensure your deployment is defined correctly.
60 |
61 | There are full working deploy-rs Nix expressions in the [examples folder](./examples), and there is a JSON schema [here](./interface.json) which is used internally by the `deployChecks` mentioned above to validate your expressions.
62 |
63 | A basic example of a flake that works with `deploy-rs` and deploys a simple NixOS configuration could look like this
64 |
65 | ```nix
66 | {
67 | description = "Deployment for my server cluster";
68 |
69 | # For accessing `deploy-rs`'s utility Nix functions
70 | inputs.deploy-rs.url = "github:serokell/deploy-rs";
71 |
72 | outputs = { self, nixpkgs, deploy-rs }: {
73 | nixosConfigurations.some-random-system = nixpkgs.lib.nixosSystem {
74 | system = "x86_64-linux";
75 | modules = [ ./some-random-system/configuration.nix ];
76 | };
77 |
78 | deploy.nodes.some-random-system = {
79 | hostname = "some-random-system";
80 | profiles.system = {
81 | user = "root";
82 | path = deploy-rs.lib.x86_64-linux.activate.nixos self.nixosConfigurations.some-random-system;
83 | };
84 | };
85 |
86 | # This is highly advised, and will prevent many possible mistakes
87 | checks = builtins.mapAttrs (system: deployLib: deployLib.deployChecks self.deploy) deploy-rs.lib;
88 | };
89 | }
90 | ```
91 |
92 | In the above configuration, `deploy-rs` is built from the flake, not from nixpkgs. To take advantage of the nixpkgs binary cache, the deploy-rs package can be overwritten in an overlay:
93 |
94 | ```nix
95 | {
96 | # ...
97 | outputs = { self, nixpkgs, deploy-rs }: let
98 | system = "x86_64-linux";
99 | # Unmodified nixpkgs
100 | pkgs = import nixpkgs { inherit system; };
101 | # nixpkgs with deploy-rs overlay but force the nixpkgs package
102 | deployPkgs = import nixpkgs {
103 | inherit system;
104 | overlays = [
105 | deploy-rs.overlay # or deploy-rs.overlays.default
106 | (self: super: { deploy-rs = { inherit (pkgs) deploy-rs; lib = super.deploy-rs.lib; }; })
107 | ];
108 | };
109 | in {
110 | # ...
111 | deploy.nodes.some-random-system.profiles.system = {
112 | user = "root";
113 | path = deployPkgs.deploy-rs.lib.activate.nixos self.nixosConfigurations.some-random-system;
114 | };
115 | };
116 | }
117 | ```
118 |
119 | ### Profile
120 |
121 | This is the core of how `deploy-rs` was designed, any number of these can run on a node, as any user (see further down for specifying user information). If you want to mimic the behaviour of traditional tools like NixOps or Morph, try just defining one `profile` called `system`, as root, containing a nixosSystem, and you can even similarly use [home-manager](https://github.com/nix-community/home-manager) on any non-privileged user.
122 |
123 | ```nix
124 | {
125 | # A derivation containing your required software, and a script to activate it in `${path}/deploy-rs-activate`
126 | # For ease of use, `deploy-rs` provides a function to easily add the required activation script to any derivation
127 | # Both the working directory and `$PROFILE` will point to `profilePath`
128 | path = deploy-rs.lib.x86_64-linux.activate.custom pkgs.hello "./bin/hello";
129 |
130 | # An optional path to where your profile should be installed to, this is useful if you want to use a common profile name across multiple users, but would have conflicts in your node's profile list.
131 | # This will default to `"/nix/var/nix/profiles/system` if `user` is `root` and profile name is `system`,
132 | # `/nix/var/nix/profiles/per-user/root/$PROFILE_NAME` if profile name is different.
133 | # For non-root profiles will default to /nix/var/nix/profiles/per-user/$USER/$PROFILE_NAME if `/nix/var/nix/profiles/per-user/$USER` already exists,
134 | # and `${XDG_STATE_HOME:-$HOME/.local/state}/nix/profiles/$PROFILE_NAME` otherwise.
135 | profilePath = "/home/someuser/.local/state/nix/profiles/someprofile";
136 |
137 | # ...generic options... (see lower section)
138 | }
139 | ```
140 |
141 | ### Node
142 |
143 | This defines a single node/server, and the profiles you intend it to run.
144 |
145 | ```nix
146 | {
147 | # The hostname of your server. Can be overridden at invocation time with a flag.
148 | hostname = "my.server.gov";
149 |
150 | # An optional list containing the order you want profiles to be deployed.
151 | # This will take effect whenever you run `deploy` without specifying a profile, causing it to deploy every profile automatically.
152 | # Any profiles not in this list will still be deployed (in an arbitrary order) after those which are listed
153 | profilesOrder = [ "something" "system" ];
154 |
155 | profiles = {
156 | # Definition format shown above
157 | system = {};
158 | something = {};
159 | };
160 |
161 | # ...generic options... (see lower section)
162 | }
163 | ```
164 |
165 | ### Deploy
166 |
167 | This is the top level attribute containing all of the options for this tool
168 |
169 | ```nix
170 | {
171 | nodes = {
172 | # Definition format shown above
173 | my-node = {};
174 | another-node = {};
175 | };
176 |
177 | # ...generic options... (see lower section)
178 | }
179 | ```
180 |
181 | ### Generic options
182 |
183 | This is a set of options that can be put in any of the above definitions, with the priority being `profile > node > deploy`
184 |
185 | ```nix
186 | {
187 | # This is the user that deploy-rs will use when connecting.
188 | # This will default to your own username if not specified anywhere
189 | sshUser = "admin";
190 |
191 | # This is the user that the profile will be deployed to (will use sudo if not the same as above).
192 | # If `sshUser` is specified, this will be the default (though it will _not_ default to your own username)
193 | user = "root";
194 |
195 | # Which sudo command to use. Must accept at least two arguments:
196 | # the user name to execute commands as and the rest is the command to execute
197 | # This will default to "sudo -u" if not specified anywhere.
198 | sudo = "doas -u";
199 |
200 | # Whether to enable interactive sudo (password based sudo). Useful when using non-root sshUsers.
201 | # This defaults to `false`
202 | interactiveSudo = false;
203 |
204 | # This is an optional list of arguments that will be passed to SSH.
205 | sshOpts = [ "-p" "2121" ];
206 |
207 | # Fast connection to the node. If this is true, copy the whole closure instead of letting the node substitute.
208 | # This defaults to `false`
209 | fastConnection = false;
210 |
211 | # If the previous profile should be re-activated if activation fails.
212 | # This defaults to `true`
213 | autoRollback = true;
214 |
215 | # See the earlier section about Magic Rollback for more information.
216 | # This defaults to `true`
217 | magicRollback = true;
218 |
219 | # The path which deploy-rs will use for temporary files, this is currently only used by `magicRollback` to create an inotify watcher in for confirmations
220 | # If not specified, this will default to `/tmp`
221 | # (if `magicRollback` is in use, this _must_ be writable by `user`)
222 | tempPath = "/home/someuser/.deploy-rs";
223 |
224 | # Build the derivation on the target system.
225 | # Will also fetch all external dependencies from the target system's substituters.
226 | # This default to `false`
227 | remoteBuild = true;
228 |
229 | # Timeout for profile activation.
230 | # This defaults to 240 seconds.
231 | activationTimeout = 600;
232 |
233 | # Timeout for profile activation confirmation.
234 | # This defaults to 30 seconds.
235 | confirmTimeout = 60;
236 | }
237 | ```
238 |
239 | Some of these options can be provided during `deploy` invocation to override default values or values provided in your flake, see `deploy --help`.
240 |
241 | ## About Serokell
242 |
243 | deploy-rs is maintained and funded with ❤️ by [Serokell](https://serokell.io/).
244 | The names and logo for Serokell are trademark of Serokell OÜ.
245 |
246 | We love open source software! See [our other projects](https://serokell.io/community?utm_source=github) or [hire us](https://serokell.io/hire-us?utm_source=github) to design, develop and grow your idea!
247 |
--------------------------------------------------------------------------------
/src/push.rs:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2020 Serokell
2 | //
3 | // SPDX-License-Identifier: MPL-2.0
4 |
5 | use log::{debug, info, warn};
6 | use std::collections::HashMap;
7 | use std::path::Path;
8 | use std::process::Stdio;
9 | use thiserror::Error;
10 | use tokio::process::Command;
11 |
12 | #[derive(Error, Debug)]
13 | pub enum PushProfileError {
14 | #[error("Failed to run Nix show-derivation command: {0}")]
15 | ShowDerivation(std::io::Error),
16 | #[error("Nix show-derivation command resulted in a bad exit code: {0:?}")]
17 | ShowDerivationExit(Option),
18 | #[error("Nix show-derivation command output contained an invalid UTF-8 sequence: {0}")]
19 | ShowDerivationUtf8(std::str::Utf8Error),
20 | #[error("Failed to parse the output of nix show-derivation: {0}")]
21 | ShowDerivationParse(serde_json::Error),
22 | #[error("Nix show-derivation output is empty")]
23 | ShowDerivationEmpty,
24 | #[error("Failed to run Nix build command: {0}")]
25 | Build(std::io::Error),
26 | #[error("Nix build command resulted in a bad exit code: {0:?}")]
27 | BuildExit(Option),
28 | #[error(
29 | "Activation script deploy-rs-activate does not exist in profile.\n\
30 | Did you forget to use deploy-rs#lib.<...>.activate.<...> on your profile path?"
31 | )]
32 | DeployRsActivateDoesntExist,
33 | #[error("Activation script activate-rs does not exist in profile.\n\
34 | Is there a mismatch in deploy-rs used in the flake you're deploying and deploy-rs command you're running?")]
35 | ActivateRsDoesntExist,
36 | #[error("Failed to run Nix sign command: {0}")]
37 | Sign(std::io::Error),
38 | #[error("Nix sign command resulted in a bad exit code: {0:?}")]
39 | SignExit(Option),
40 | #[error("Failed to run Nix copy command: {0}")]
41 | Copy(std::io::Error),
42 | #[error("Nix copy command resulted in a bad exit code: {0:?}")]
43 | CopyExit(Option),
44 |
45 | #[error("Failed to run Nix path-info command: {0}")]
46 | PathInfo(std::io::Error),
47 | }
48 |
49 | pub struct PushProfileData<'a> {
50 | pub supports_flakes: bool,
51 | pub check_sigs: bool,
52 | pub repo: &'a str,
53 | pub deploy_data: &'a super::DeployData<'a>,
54 | pub deploy_defs: &'a super::DeployDefs,
55 | pub keep_result: bool,
56 | pub result_path: Option<&'a str>,
57 | pub extra_build_args: &'a [String],
58 | }
59 |
60 | pub async fn build_profile_locally(data: &PushProfileData<'_>, derivation_name: &str) -> Result<(), PushProfileError> {
61 | info!(
62 | "Building profile `{}` for node `{}`",
63 | data.deploy_data.profile_name, data.deploy_data.node_name
64 | );
65 |
66 | let mut build_command = if data.supports_flakes {
67 | Command::new("nix")
68 | } else {
69 | Command::new("nix-build")
70 | };
71 |
72 | if data.supports_flakes {
73 | build_command.arg("build").arg(derivation_name)
74 | } else {
75 | build_command.arg(derivation_name)
76 | };
77 |
78 | match (data.keep_result, data.supports_flakes) {
79 | (true, _) => {
80 | let result_path = data.result_path.unwrap_or("./.deploy-gc");
81 |
82 | build_command.arg("--out-link").arg(format!(
83 | "{}/{}/{}",
84 | result_path, data.deploy_data.node_name, data.deploy_data.profile_name
85 | ))
86 | }
87 | (false, false) => build_command.arg("--no-out-link"),
88 | (false, true) => build_command.arg("--no-link"),
89 | };
90 |
91 | build_command.args(data.extra_build_args);
92 |
93 | let build_exit_status = build_command
94 | // Logging should be in stderr, this just stops the store path from printing for no reason
95 | .stdout(Stdio::null())
96 | .status()
97 | .await
98 | .map_err(PushProfileError::Build)?;
99 |
100 | match build_exit_status.code() {
101 | Some(0) => (),
102 | a => return Err(PushProfileError::BuildExit(a)),
103 | };
104 |
105 | if !Path::new(
106 | format!(
107 | "{}/deploy-rs-activate",
108 | data.deploy_data.profile.profile_settings.path
109 | )
110 | .as_str(),
111 | )
112 | .exists()
113 | {
114 | return Err(PushProfileError::DeployRsActivateDoesntExist);
115 | }
116 |
117 | if !Path::new(
118 | format!(
119 | "{}/activate-rs",
120 | data.deploy_data.profile.profile_settings.path
121 | )
122 | .as_str(),
123 | )
124 | .exists()
125 | {
126 | return Err(PushProfileError::ActivateRsDoesntExist);
127 | }
128 |
129 | if let Ok(local_key) = std::env::var("LOCAL_KEY") {
130 | info!(
131 | "Signing key present! Signing profile `{}` for node `{}`",
132 | data.deploy_data.profile_name, data.deploy_data.node_name
133 | );
134 |
135 | let sign_exit_status = Command::new("nix")
136 | .arg("sign-paths")
137 | .arg("-r")
138 | .arg("-k")
139 | .arg(local_key)
140 | .arg(&data.deploy_data.profile.profile_settings.path)
141 | .status()
142 | .await
143 | .map_err(PushProfileError::Sign)?;
144 |
145 | match sign_exit_status.code() {
146 | Some(0) => (),
147 | a => return Err(PushProfileError::SignExit(a)),
148 | };
149 | }
150 | Ok(())
151 | }
152 |
153 | pub async fn build_profile_remotely(data: &PushProfileData<'_>, derivation_name: &str) -> Result<(), PushProfileError> {
154 | info!(
155 | "Building profile `{}` for node `{}` on remote host",
156 | data.deploy_data.profile_name, data.deploy_data.node_name
157 | );
158 |
159 | // TODO: this should probably be handled more nicely during 'data' construction
160 | let hostname = match data.deploy_data.cmd_overrides.hostname {
161 | Some(ref x) => x,
162 | None => &data.deploy_data.node.node_settings.hostname,
163 | };
164 | let store_address = format!("ssh-ng://{}@{}", data.deploy_defs.ssh_user, hostname);
165 |
166 | let ssh_opts_str = data.deploy_data.merged_settings.ssh_opts.join(" ");
167 |
168 |
169 | // copy the derivation to remote host so it can be built there
170 | let copy_command_status = Command::new("nix")
171 | .arg("--experimental-features").arg("nix-command")
172 | .arg("copy")
173 | .arg("-s") // fetch dependencies from substitures, not localhost
174 | .arg("--to").arg(&store_address)
175 | .arg("--derivation").arg(derivation_name)
176 | .env("NIX_SSHOPTS", ssh_opts_str.clone())
177 | .stdout(Stdio::null())
178 | .status()
179 | .await
180 | .map_err(PushProfileError::Copy)?;
181 |
182 | match copy_command_status.code() {
183 | Some(0) => (),
184 | a => return Err(PushProfileError::CopyExit(a)),
185 | };
186 |
187 | let mut build_command = Command::new("nix");
188 | build_command
189 | .arg("--experimental-features").arg("nix-command")
190 | .arg("build").arg(derivation_name)
191 | .arg("--eval-store").arg("auto")
192 | .arg("--store").arg(&store_address)
193 | .args(data.extra_build_args)
194 | .env("NIX_SSHOPTS", ssh_opts_str.clone());
195 |
196 | debug!("build command: {:?}", build_command);
197 |
198 | let build_exit_status = build_command
199 | // Logging should be in stderr, this just stops the store path from printing for no reason
200 | .stdout(Stdio::null())
201 | .status()
202 | .await
203 | .map_err(PushProfileError::Build)?;
204 |
205 | match build_exit_status.code() {
206 | Some(0) => (),
207 | a => return Err(PushProfileError::BuildExit(a)),
208 | };
209 |
210 |
211 | Ok(())
212 | }
213 |
214 | pub async fn build_profile(data: PushProfileData<'_>) -> Result<(), PushProfileError> {
215 | debug!(
216 | "Finding the deriver of store path for {}",
217 | &data.deploy_data.profile.profile_settings.path
218 | );
219 |
220 | // `nix-store --query --deriver` doesn't work on invalid paths, so we parse output of show-derivation :(
221 | let mut show_derivation_command = Command::new("nix");
222 |
223 | show_derivation_command
224 | .arg("show-derivation")
225 | .arg(&data.deploy_data.profile.profile_settings.path);
226 |
227 | let show_derivation_output = show_derivation_command
228 | .output()
229 | .await
230 | .map_err(PushProfileError::ShowDerivation)?;
231 |
232 | match show_derivation_output.status.code() {
233 | Some(0) => (),
234 | a => return Err(PushProfileError::ShowDerivationExit(a)),
235 | };
236 |
237 | let derivation_info: HashMap<&str, serde_json::value::Value> = serde_json::from_str(
238 | std::str::from_utf8(&show_derivation_output.stdout)
239 | .map_err(PushProfileError::ShowDerivationUtf8)?,
240 | )
241 | .map_err(PushProfileError::ShowDerivationParse)?;
242 |
243 | let deriver_key = derivation_info
244 | .keys()
245 | .next()
246 | .ok_or(PushProfileError::ShowDerivationEmpty)?;
247 |
248 | // Nix 2.32+ returns relative paths (without /nix/store/ prefix) in show-derivation output
249 | // Normalize to always use full store paths
250 | let deriver = if deriver_key.starts_with("/nix/store/") {
251 | deriver_key.to_string()
252 | } else {
253 | format!("/nix/store/{}", deriver_key)
254 | };
255 |
256 | let new_deriver = if data.supports_flakes || data.deploy_data.merged_settings.remote_build.unwrap_or(false) {
257 | // Since nix 2.15.0 'nix build .drv' will build only the .drv file itself, not the
258 | // derivation outputs, '^out' is used to refer to outputs explicitly
259 | deriver.clone() + "^out"
260 | } else {
261 | deriver.clone()
262 | };
263 |
264 | let path_info_output = Command::new("nix")
265 | .arg("--experimental-features").arg("nix-command")
266 | .arg("path-info")
267 | .arg(&deriver)
268 | .output().await
269 | .map_err(PushProfileError::PathInfo)?;
270 |
271 | let deriver = if std::str::from_utf8(&path_info_output.stdout).map(|s| s.trim()) == Ok(deriver.as_str()) {
272 | // In this case we're on 2.15.0 or newer, because 'nix path-info <...>.drv'
273 | // returns the same '<...>.drv' path.
274 | // If 'nix path-info <...>.drv' returns a different path, then we're on pre 2.15.0 nix and
275 | // derivation build result is already present in the /nix/store.
276 | new_deriver
277 | } else {
278 | // If 'nix path-info <...>.drv' returns a different path, then we're on pre 2.15.0 nix and
279 | // derivation build result is already present in the /nix/store.
280 | //
281 | // Alternatively, the result of the derivation build may not be yet present
282 | // in the /nix/store. In this case, 'nix path-info' returns
283 | // 'error: path '...' is not valid'.
284 | deriver
285 | };
286 | if data.deploy_data.merged_settings.remote_build.unwrap_or(false) {
287 | if !data.supports_flakes {
288 | warn!("remote builds using non-flake nix are experimental");
289 | }
290 |
291 | build_profile_remotely(&data, &deriver).await?;
292 | } else {
293 | build_profile_locally(&data, &deriver).await?;
294 | }
295 |
296 | Ok(())
297 | }
298 |
299 | pub async fn push_profile(data: PushProfileData<'_>) -> Result<(), PushProfileError> {
300 | let ssh_opts_str = data
301 | .deploy_data
302 | .merged_settings
303 | .ssh_opts
304 | // This should provide some extra safety, but it also breaks for some reason, oh well
305 | // .iter()
306 | // .map(|x| format!("'{}'", x))
307 | // .collect::>()
308 | .join(" ");
309 |
310 | // remote building guarantees that the resulting derivation is stored on the target system
311 | // no need to copy after building
312 | if !data.deploy_data.merged_settings.remote_build.unwrap_or(false) {
313 | info!(
314 | "Copying profile `{}` to node `{}`",
315 | data.deploy_data.profile_name, data.deploy_data.node_name
316 | );
317 |
318 | let mut copy_command = Command::new("nix");
319 | copy_command.arg("copy");
320 |
321 | if data.deploy_data.merged_settings.fast_connection != Some(true) {
322 | copy_command.arg("--substitute-on-destination");
323 | }
324 |
325 | if !data.check_sigs {
326 | copy_command.arg("--no-check-sigs");
327 | }
328 |
329 | let hostname = match data.deploy_data.cmd_overrides.hostname {
330 | Some(ref x) => x,
331 | None => &data.deploy_data.node.node_settings.hostname,
332 | };
333 |
334 | let copy_exit_status = copy_command
335 | .arg("--to")
336 | .arg(format!("ssh://{}@{}", data.deploy_defs.ssh_user, hostname))
337 | .arg(&data.deploy_data.profile.profile_settings.path)
338 | .env("NIX_SSHOPTS", ssh_opts_str)
339 | .status()
340 | .await
341 | .map_err(PushProfileError::Copy)?;
342 |
343 | match copy_exit_status.code() {
344 | Some(0) => (),
345 | a => return Err(PushProfileError::CopyExit(a)),
346 | };
347 | }
348 |
349 | Ok(())
350 | }
351 |
--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2020 Serokell
2 | // SPDX-FileCopyrightText: 2020 Andreas Fuchs
3 | // SPDX-FileCopyrightText: 2021 Yannik Sander
4 | //
5 | // SPDX-License-Identifier: MPL-2.0
6 |
7 | use rnix::{types::*, SyntaxKind::*};
8 |
9 | use merge::Merge;
10 |
11 | use thiserror::Error;
12 |
13 | use flexi_logger::*;
14 |
15 | use std::path::{Path, PathBuf};
16 |
17 | pub fn make_lock_path(temp_path: &Path, closure: &str) -> PathBuf {
18 | let lock_hash =
19 | &closure["/nix/store/".len()..closure.find('-').unwrap_or_else(|| closure.len())];
20 | temp_path.join(format!("deploy-rs-canary-{}", lock_hash))
21 | }
22 |
23 | const fn make_emoji(level: log::Level) -> &'static str {
24 | match level {
25 | log::Level::Error => "❌",
26 | log::Level::Warn => "⚠️",
27 | log::Level::Info => "ℹ️",
28 | log::Level::Debug => "❓",
29 | log::Level::Trace => "🖊️",
30 | }
31 | }
32 |
33 | pub fn logger_formatter_activate(
34 | w: &mut dyn std::io::Write,
35 | _now: &mut DeferredNow,
36 | record: &Record,
37 | ) -> Result<(), std::io::Error> {
38 | let level = record.level();
39 |
40 | write!(
41 | w,
42 | "⭐ {} [activate] [{}] {}",
43 | make_emoji(level),
44 | style(level, level.to_string()),
45 | record.args()
46 | )
47 | }
48 |
49 | pub fn logger_formatter_wait(
50 | w: &mut dyn std::io::Write,
51 | _now: &mut DeferredNow,
52 | record: &Record,
53 | ) -> Result<(), std::io::Error> {
54 | let level = record.level();
55 |
56 | write!(
57 | w,
58 | "👀 {} [wait] [{}] {}",
59 | make_emoji(level),
60 | style(level, level.to_string()),
61 | record.args()
62 | )
63 | }
64 |
65 | pub fn logger_formatter_revoke(
66 | w: &mut dyn std::io::Write,
67 | _now: &mut DeferredNow,
68 | record: &Record,
69 | ) -> Result<(), std::io::Error> {
70 | let level = record.level();
71 |
72 | write!(
73 | w,
74 | "↩️ {} [revoke] [{}] {}",
75 | make_emoji(level),
76 | style(level, level.to_string()),
77 | record.args()
78 | )
79 | }
80 |
81 | pub fn logger_formatter_deploy(
82 | w: &mut dyn std::io::Write,
83 | _now: &mut DeferredNow,
84 | record: &Record,
85 | ) -> Result<(), std::io::Error> {
86 | let level = record.level();
87 |
88 | write!(
89 | w,
90 | "🚀 {} [deploy] [{}] {}",
91 | make_emoji(level),
92 | style(level, level.to_string()),
93 | record.args()
94 | )
95 | }
96 |
97 | pub enum LoggerType {
98 | Deploy,
99 | Activate,
100 | Wait,
101 | Revoke,
102 | }
103 |
104 | pub fn init_logger(
105 | debug_logs: bool,
106 | log_dir: Option<&str>,
107 | logger_type: &LoggerType,
108 | ) -> Result<(), FlexiLoggerError> {
109 | let logger_formatter = match &logger_type {
110 | LoggerType::Deploy => logger_formatter_deploy,
111 | LoggerType::Activate => logger_formatter_activate,
112 | LoggerType::Wait => logger_formatter_wait,
113 | LoggerType::Revoke => logger_formatter_revoke,
114 | };
115 |
116 | if let Some(log_dir) = log_dir {
117 | let mut logger = Logger::with_env_or_str("debug")
118 | .log_to_file()
119 | .format_for_stderr(logger_formatter)
120 | .set_palette("196;208;51;7;8".to_string())
121 | .directory(log_dir)
122 | .duplicate_to_stderr(match debug_logs {
123 | true => Duplicate::Debug,
124 | false => Duplicate::Info,
125 | })
126 | .print_message();
127 |
128 | match logger_type {
129 | LoggerType::Activate => logger = logger.discriminant("activate"),
130 | LoggerType::Wait => logger = logger.discriminant("wait"),
131 | LoggerType::Revoke => logger = logger.discriminant("revoke"),
132 | LoggerType::Deploy => (),
133 | }
134 |
135 | logger.start()?;
136 | } else {
137 | Logger::with_env_or_str(match debug_logs {
138 | true => "debug",
139 | false => "info",
140 | })
141 | .log_target(LogTarget::StdErr)
142 | .format(logger_formatter)
143 | .set_palette("196;208;51;7;8".to_string())
144 | .start()?;
145 | }
146 |
147 | Ok(())
148 | }
149 |
150 | pub mod cli;
151 | pub mod data;
152 | pub mod deploy;
153 | pub mod push;
154 |
155 | #[derive(Debug)]
156 | pub struct CmdOverrides {
157 | pub ssh_user: Option,
158 | pub profile_user: Option,
159 | pub ssh_opts: Option,
160 | pub fast_connection: Option,
161 | pub auto_rollback: Option,
162 | pub hostname: Option,
163 | pub magic_rollback: Option,
164 | pub temp_path: Option,
165 | pub confirm_timeout: Option,
166 | pub activation_timeout: Option,
167 | pub sudo: Option,
168 | pub interactive_sudo: Option,
169 | pub dry_activate: bool,
170 | pub remote_build: bool,
171 | }
172 |
173 | #[derive(PartialEq, Debug)]
174 | pub struct DeployFlake<'a> {
175 | pub repo: &'a str,
176 | pub node: Option,
177 | pub profile: Option,
178 | }
179 |
180 | #[derive(Error, Debug)]
181 | pub enum ParseFlakeError {
182 | #[error("The given path was too long, did you mean to put something in quotes?")]
183 | PathTooLong,
184 | #[error("Unrecognized node or token encountered")]
185 | Unrecognized,
186 | }
187 |
188 | fn parse_fragment(fragment: &str) -> Result<(Option, Option), ParseFlakeError> {
189 | let mut node: Option = None;
190 | let mut profile: Option = None;
191 |
192 | let ast = rnix::parse(fragment);
193 |
194 | let first_child = match ast.root().node().first_child() {
195 | Some(x) => x,
196 | None => return Ok((None, None))
197 | };
198 |
199 | let mut node_over = false;
200 |
201 | for entry in first_child.children_with_tokens() {
202 | let x: Option = match (entry.kind(), node_over) {
203 | (TOKEN_DOT, false) => {
204 | node_over = true;
205 | None
206 | }
207 | (TOKEN_DOT, true) => {
208 | return Err(ParseFlakeError::PathTooLong);
209 | }
210 | (NODE_IDENT, _) => Some(entry.into_node().unwrap().text().to_string()),
211 | (TOKEN_IDENT, _) => Some(entry.into_token().unwrap().text().to_string()),
212 | (NODE_STRING, _) => {
213 | let c = entry
214 | .into_node()
215 | .unwrap()
216 | .children_with_tokens()
217 | .nth(1)
218 | .unwrap();
219 |
220 | Some(c.into_token().unwrap().text().to_string())
221 | }
222 | _ => return Err(ParseFlakeError::Unrecognized),
223 | };
224 |
225 | if !node_over {
226 | node = x;
227 | } else {
228 | profile = x;
229 | }
230 | }
231 |
232 | Ok((node, profile))
233 | }
234 |
235 | pub fn parse_flake(flake: &str) -> Result {
236 | let flake_fragment_start = flake.find('#');
237 | let (repo, maybe_fragment) = match flake_fragment_start {
238 | Some(s) => (&flake[..s], Some(&flake[s + 1..])),
239 | None => (flake, None),
240 | };
241 |
242 | let mut node: Option = None;
243 | let mut profile: Option = None;
244 |
245 | if let Some(fragment) = maybe_fragment {
246 | (node, profile) = parse_fragment(fragment)?;
247 | }
248 |
249 | Ok(DeployFlake {
250 | repo,
251 | node,
252 | profile,
253 | })
254 | }
255 |
256 | #[test]
257 | fn test_parse_flake() {
258 | assert_eq!(
259 | parse_flake("../deploy/examples/system").unwrap(),
260 | DeployFlake {
261 | repo: "../deploy/examples/system",
262 | node: None,
263 | profile: None,
264 | }
265 | );
266 |
267 | assert_eq!(
268 | parse_flake("../deploy/examples/system#").unwrap(),
269 | DeployFlake {
270 | repo: "../deploy/examples/system",
271 | node: None,
272 | profile: None,
273 | }
274 | );
275 |
276 | assert_eq!(
277 | parse_flake("../deploy/examples/system#computer.\"something.nix\"").unwrap(),
278 | DeployFlake {
279 | repo: "../deploy/examples/system",
280 | node: Some("computer".to_string()),
281 | profile: Some("something.nix".to_string()),
282 | }
283 | );
284 |
285 | assert_eq!(
286 | parse_flake("../deploy/examples/system#\"example.com\".system").unwrap(),
287 | DeployFlake {
288 | repo: "../deploy/examples/system",
289 | node: Some("example.com".to_string()),
290 | profile: Some("system".to_string()),
291 | }
292 | );
293 |
294 | assert_eq!(
295 | parse_flake("../deploy/examples/system#example").unwrap(),
296 | DeployFlake {
297 | repo: "../deploy/examples/system",
298 | node: Some("example".to_string()),
299 | profile: None
300 | }
301 | );
302 |
303 | assert_eq!(
304 | parse_flake("../deploy/examples/system#example.system").unwrap(),
305 | DeployFlake {
306 | repo: "../deploy/examples/system",
307 | node: Some("example".to_string()),
308 | profile: Some("system".to_string())
309 | }
310 | );
311 |
312 | assert_eq!(
313 | parse_flake("../deploy/examples/system").unwrap(),
314 | DeployFlake {
315 | repo: "../deploy/examples/system",
316 | node: None,
317 | profile: None,
318 | }
319 | );
320 | }
321 |
322 | pub fn parse_file<'a>(file: &'a str, attribute: &'a str) -> Result, ParseFlakeError> {
323 | let (node, profile) = parse_fragment(attribute)?;
324 |
325 | Ok(DeployFlake {
326 | repo: &file,
327 | node,
328 | profile,
329 | })
330 | }
331 |
332 | #[derive(Debug, Clone)]
333 | pub struct DeployData<'a> {
334 | pub node_name: &'a str,
335 | pub node: &'a data::Node,
336 | pub profile_name: &'a str,
337 | pub profile: &'a data::Profile,
338 |
339 | pub cmd_overrides: &'a CmdOverrides,
340 |
341 | pub merged_settings: data::GenericSettings,
342 |
343 | pub debug_logs: bool,
344 | pub log_dir: Option<&'a str>,
345 | }
346 |
347 | #[derive(Debug)]
348 | pub struct DeployDefs {
349 | pub ssh_user: String,
350 | pub profile_user: String,
351 | pub sudo: Option,
352 | pub sudo_password: Option,
353 | }
354 | enum ProfileInfo {
355 | ProfilePath {
356 | profile_path: String,
357 | },
358 | ProfileUserAndName {
359 | profile_user: String,
360 | profile_name: String,
361 | },
362 | }
363 |
364 | #[derive(Error, Debug)]
365 | pub enum DeployDataDefsError {
366 | #[error("Neither `user` nor `sshUser` are set for profile {0} of node {1}")]
367 | NoProfileUser(String, String),
368 | }
369 |
370 | impl<'a> DeployData<'a> {
371 | pub fn defs(&'a self) -> Result {
372 | let ssh_user = match self.merged_settings.ssh_user {
373 | Some(ref u) => u.clone(),
374 | None => whoami::username(),
375 | };
376 |
377 | let profile_user = self.get_profile_user()?;
378 |
379 | let sudo: Option = match self.merged_settings.user {
380 | Some(ref user) if user != &ssh_user => Some(format!("{} {}", self.get_sudo(), user)),
381 | _ => None,
382 | };
383 |
384 | Ok(DeployDefs {
385 | ssh_user,
386 | profile_user,
387 | sudo,
388 | sudo_password: None,
389 | })
390 | }
391 |
392 | fn get_profile_user(&'a self) -> Result {
393 | let profile_user = match self.merged_settings.user {
394 | Some(ref x) => x.clone(),
395 | None => match self.merged_settings.ssh_user {
396 | Some(ref x) => x.clone(),
397 | None => {
398 | return Err(DeployDataDefsError::NoProfileUser(
399 | self.profile_name.to_owned(),
400 | self.node_name.to_owned(),
401 | ))
402 | }
403 | },
404 | };
405 | Ok(profile_user)
406 | }
407 |
408 | fn get_sudo(&'a self) -> String {
409 | match self.merged_settings.sudo {
410 | Some(ref x) => x.clone(),
411 | None => "sudo -u".to_string(),
412 | }
413 | }
414 |
415 | fn get_profile_info(&'a self) -> Result {
416 | match self.profile.profile_settings.profile_path {
417 | Some(ref profile_path) => Ok(ProfileInfo::ProfilePath { profile_path: profile_path.to_string() }),
418 | None => {
419 | let profile_user = self.get_profile_user()?;
420 | Ok(ProfileInfo::ProfileUserAndName { profile_user, profile_name: self.profile_name.to_string() })
421 | },
422 | }
423 | }
424 | }
425 |
426 | pub fn make_deploy_data<'a, 's>(
427 | top_settings: &'s data::GenericSettings,
428 | node: &'a data::Node,
429 | node_name: &'a str,
430 | profile: &'a data::Profile,
431 | profile_name: &'a str,
432 | cmd_overrides: &'a CmdOverrides,
433 | debug_logs: bool,
434 | log_dir: Option<&'a str>,
435 | ) -> DeployData<'a> {
436 | let mut merged_settings = profile.generic_settings.clone();
437 | merged_settings.merge(node.generic_settings.clone());
438 | merged_settings.merge(top_settings.clone());
439 |
440 | // build all machines remotely when the command line flag is set
441 | if cmd_overrides.remote_build {
442 | merged_settings.remote_build = Some(cmd_overrides.remote_build);
443 | }
444 | if cmd_overrides.ssh_user.is_some() {
445 | merged_settings.ssh_user = cmd_overrides.ssh_user.clone();
446 | }
447 | if cmd_overrides.profile_user.is_some() {
448 | merged_settings.user = cmd_overrides.profile_user.clone();
449 | }
450 | if let Some(ref ssh_opts) = cmd_overrides.ssh_opts {
451 | merged_settings.ssh_opts = ssh_opts.split(' ').map(|x| x.to_owned()).collect();
452 | }
453 | if let Some(fast_connection) = cmd_overrides.fast_connection {
454 | merged_settings.fast_connection = Some(fast_connection);
455 | }
456 | if let Some(auto_rollback) = cmd_overrides.auto_rollback {
457 | merged_settings.auto_rollback = Some(auto_rollback);
458 | }
459 | if let Some(magic_rollback) = cmd_overrides.magic_rollback {
460 | merged_settings.magic_rollback = Some(magic_rollback);
461 | }
462 | if let Some(confirm_timeout) = cmd_overrides.confirm_timeout {
463 | merged_settings.confirm_timeout = Some(confirm_timeout);
464 | }
465 | if let Some(activation_timeout) = cmd_overrides.activation_timeout {
466 | merged_settings.activation_timeout = Some(activation_timeout);
467 | }
468 | if let Some(interactive_sudo) = cmd_overrides.interactive_sudo {
469 | merged_settings.interactive_sudo = Some(interactive_sudo);
470 | }
471 |
472 | DeployData {
473 | node_name,
474 | node,
475 | profile_name,
476 | profile,
477 | cmd_overrides,
478 | merged_settings,
479 | debug_logs,
480 | log_dir,
481 | }
482 | }
483 |
--------------------------------------------------------------------------------
/LICENSES/MPL-2.0.txt:
--------------------------------------------------------------------------------
1 | Mozilla Public License Version 2.0
2 |
3 | 1. Definitions
4 |
5 | 1.1. "Contributor" means each individual or legal entity that creates, contributes
6 | to the creation of, or owns Covered Software.
7 |
8 | 1.2. "Contributor Version" means the combination of the Contributions of others
9 | (if any) used by a Contributor and that particular Contributor's Contribution.
10 |
11 | 1.3. "Contribution" means Covered Software of a particular Contributor.
12 |
13 | 1.4. "Covered Software" means Source Code Form to which the initial Contributor
14 | has attached the notice in Exhibit A, the Executable Form of such Source Code
15 | Form, and Modifications of such Source Code Form, in each case including portions
16 | thereof.
17 |
18 | 1.5. "Incompatible With Secondary Licenses" means
19 |
20 | (a) that the initial Contributor has attached the notice described in Exhibit
21 | B to the Covered Software; or
22 |
23 | (b) that the Covered Software was made available under the terms of version
24 | 1.1 or earlier of the License, but not also under the terms of a Secondary
25 | License.
26 |
27 | 1.6. "Executable Form" means any form of the work other than Source Code Form.
28 |
29 | 1.7. "Larger Work" means a work that combines Covered Software with other
30 | material, in a separate file or files, that is not Covered Software.
31 |
32 | 1.8. "License" means this document.
33 |
34 | 1.9. "Licensable" means having the right to grant, to the maximum extent possible,
35 | whether at the time of the initial grant or subsequently, any and all of the
36 | rights conveyed by this License.
37 |
38 | 1.10. "Modifications" means any of the following:
39 |
40 | (a) any file in Source Code Form that results from an addition to, deletion
41 | from, or modification of the contents of Covered Software; or
42 |
43 | (b) any new file in Source Code Form that contains any Covered Software.
44 |
45 | 1.11. "Patent Claims" of a Contributor means any patent claim(s), including
46 | without limitation, method, process, and apparatus claims, in any patent Licensable
47 | by such Contributor that would be infringed, but for the grant of the License,
48 | by the making, using, selling, offering for sale, having made, import, or
49 | transfer of either its Contributions or its Contributor Version.
50 |
51 | 1.12. "Secondary License" means either the GNU General Public License, Version
52 | 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General
53 | Public License, Version 3.0, or any later versions of those licenses.
54 |
55 | 1.13. "Source Code Form" means the form of the work preferred for making modifications.
56 |
57 | 1.14. "You" (or "Your") means an individual or a legal entity exercising rights
58 | under this License. For legal entities, "You" includes any entity that controls,
59 | is controlled by, or is under common control with You. For purposes of this
60 | definition, "control" means (a) the power, direct or indirect, to cause the
61 | direction or management of such entity, whether by contract or otherwise,
62 | or (b) ownership of more than fifty percent (50%) of the outstanding shares
63 | or beneficial ownership of such entity.
64 |
65 | 2. License Grants and Conditions
66 |
67 | 2.1. Grants
68 |
69 | Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive
70 | license:
71 |
72 | (a) under intellectual property rights (other than patent or trademark) Licensable
73 | by such Contributor to use, reproduce, make available, modify, display, perform,
74 | distribute, and otherwise exploit its Contributions, either on an unmodified
75 | basis, with Modifications, or as part of a Larger Work; and
76 |
77 | (b) under Patent Claims of such Contributor to make, use, sell, offer for
78 | sale, have made, import, and otherwise transfer either its Contributions or
79 | its Contributor Version.
80 |
81 | 2.2. Effective Date
82 |
83 | The licenses granted in Section 2.1 with respect to any Contribution become
84 | effective for each Contribution on the date the Contributor first distributes
85 | such Contribution.
86 |
87 | 2.3. Limitations on Grant Scope
88 |
89 | The licenses granted in this Section 2 are the only rights granted under this
90 | License. No additional rights or licenses will be implied from the distribution
91 | or licensing of Covered Software under this License. Notwithstanding Section
92 | 2.1(b) above, no patent license is granted by a Contributor:
93 |
94 | (a) for any code that a Contributor has removed from Covered Software; or
95 |
96 | (b) for infringements caused by: (i) Your and any other third party's modifications
97 | of Covered Software, or (ii) the combination of its Contributions with other
98 | software (except as part of its Contributor Version); or
99 |
100 | (c) under Patent Claims infringed by Covered Software in the absence of its
101 | Contributions.
102 |
103 | This License does not grant any rights in the trademarks, service marks, or
104 | logos of any Contributor (except as may be necessary to comply with the notice
105 | requirements in Section 3.4).
106 |
107 | 2.4. Subsequent Licenses
108 |
109 | No Contributor makes additional grants as a result of Your choice to distribute
110 | the Covered Software under a subsequent version of this License (see Section
111 | 10.2) or under the terms of a Secondary License (if permitted under the terms
112 | of Section 3.3).
113 |
114 | 2.5. Representation
115 |
116 | Each Contributor represents that the Contributor believes its Contributions
117 | are its original creation(s) or it has sufficient rights to grant the rights
118 | to its Contributions conveyed by this License.
119 |
120 | 2.6. Fair Use
121 |
122 | This License is not intended to limit any rights You have under applicable
123 | copyright doctrines of fair use, fair dealing, or other equivalents.
124 |
125 | 2.7. Conditions
126 |
127 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in
128 | Section 2.1.
129 |
130 | 3. Responsibilities
131 |
132 | 3.1. Distribution of Source Form
133 |
134 | All distribution of Covered Software in Source Code Form, including any Modifications
135 | that You create or to which You contribute, must be under the terms of this
136 | License. You must inform recipients that the Source Code Form of the Covered
137 | Software is governed by the terms of this License, and how they can obtain
138 | a copy of this License. You may not attempt to alter or restrict the recipients'
139 | rights in the Source Code Form.
140 |
141 | 3.2. Distribution of Executable Form
142 |
143 | If You distribute Covered Software in Executable Form then:
144 |
145 | (a) such Covered Software must also be made available in Source Code Form,
146 | as described in Section 3.1, and You must inform recipients of the Executable
147 | Form how they can obtain a copy of such Source Code Form by reasonable means
148 | in a timely manner, at a charge no more than the cost of distribution to the
149 | recipient; and
150 |
151 | (b) You may distribute such Executable Form under the terms of this License,
152 | or sublicense it under different terms, provided that the license for the
153 | Executable Form does not attempt to limit or alter the recipients' rights
154 | in the Source Code Form under this License.
155 |
156 | 3.3. Distribution of a Larger Work
157 |
158 | You may create and distribute a Larger Work under terms of Your choice, provided
159 | that You also comply with the requirements of this License for the Covered
160 | Software. If the Larger Work is a combination of Covered Software with a work
161 | governed by one or more Secondary Licenses, and the Covered Software is not
162 | Incompatible With Secondary Licenses, this License permits You to additionally
163 | distribute such Covered Software under the terms of such Secondary License(s),
164 | so that the recipient of the Larger Work may, at their option, further distribute
165 | the Covered Software under the terms of either this License or such Secondary
166 | License(s).
167 |
168 | 3.4. Notices
169 |
170 | You may not remove or alter the substance of any license notices (including
171 | copyright notices, patent notices, disclaimers of warranty, or limitations
172 | of liability) contained within the Source Code Form of the Covered Software,
173 | except that You may alter any license notices to the extent required to remedy
174 | known factual inaccuracies.
175 |
176 | 3.5. Application of Additional Terms
177 |
178 | You may choose to offer, and to charge a fee for, warranty, support, indemnity
179 | or liability obligations to one or more recipients of Covered Software. However,
180 | You may do so only on Your own behalf, and not on behalf of any Contributor.
181 | You must make it absolutely clear that any such warranty, support, indemnity,
182 | or liability obligation is offered by You alone, and You hereby agree to indemnify
183 | every Contributor for any liability incurred by such Contributor as a result
184 | of warranty, support, indemnity or liability terms You offer. You may include
185 | additional disclaimers of warranty and limitations of liability specific to
186 | any jurisdiction.
187 |
188 | 4. Inability to Comply Due to Statute or Regulation
189 |
190 | If it is impossible for You to comply with any of the terms of this License
191 | with respect to some or all of the Covered Software due to statute, judicial
192 | order, or regulation then You must: (a) comply with the terms of this License
193 | to the maximum extent possible; and (b) describe the limitations and the code
194 | they affect. Such description must be placed in a text file included with
195 | all distributions of the Covered Software under this License. Except to the
196 | extent prohibited by statute or regulation, such description must be sufficiently
197 | detailed for a recipient of ordinary skill to be able to understand it.
198 |
199 | 5. Termination
200 |
201 | 5.1. The rights granted under this License will terminate automatically if
202 | You fail to comply with any of its terms. However, if You become compliant,
203 | then the rights granted under this License from a particular Contributor are
204 | reinstated (a) provisionally, unless and until such Contributor explicitly
205 | and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor
206 | fails to notify You of the non-compliance by some reasonable means prior to
207 | 60 days after You have come back into compliance. Moreover, Your grants from
208 | a particular Contributor are reinstated on an ongoing basis if such Contributor
209 | notifies You of the non-compliance by some reasonable means, this is the first
210 | time You have received notice of non-compliance with this License from such
211 | Contributor, and You become compliant prior to 30 days after Your receipt
212 | of the notice.
213 |
214 | 5.2. If You initiate litigation against any entity by asserting a patent infringement
215 | claim (excluding declaratory judgment actions, counter-claims, and cross-claims)
216 | alleging that a Contributor Version directly or indirectly infringes any patent,
217 | then the rights granted to You by any and all Contributors for the Covered
218 | Software under Section 2.1 of this License shall terminate.
219 |
220 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end
221 | user license agreements (excluding distributors and resellers) which have
222 | been validly granted by You or Your distributors under this License prior
223 | to termination shall survive termination.
224 |
225 | 6. Disclaimer of Warranty
226 |
227 | Covered Software is provided under this License on an "as is" basis, without
228 | warranty of any kind, either expressed, implied, or statutory, including,
229 | without limitation, warranties that the Covered Software is free of defects,
230 | merchantable, fit for a particular purpose or non-infringing. The entire risk
231 | as to the quality and performance of the Covered Software is with You. Should
232 | any Covered Software prove defective in any respect, You (not any Contributor)
233 | assume the cost of any necessary servicing, repair, or correction. This disclaimer
234 | of warranty constitutes an essential part of this License. No use of any Covered
235 | Software is authorized under this License except under this disclaimer.
236 |
237 | 7. Limitation of Liability
238 |
239 | Under no circumstances and under no legal theory, whether tort (including
240 | negligence), contract, or otherwise, shall any Contributor, or anyone who
241 | distributes Covered Software as permitted above, be liable to You for any
242 | direct, indirect, special, incidental, or consequential damages of any character
243 | including, without limitation, damages for lost profits, loss of goodwill,
244 | work stoppage, computer failure or malfunction, or any and all other commercial
245 | damages or losses, even if such party shall have been informed of the possibility
246 | of such damages. This limitation of liability shall not apply to liability
247 | for death or personal injury resulting from such party's negligence to the
248 | extent applicable law prohibits such limitation. Some jurisdictions do not
249 | allow the exclusion or limitation of incidental or consequential damages,
250 | so this exclusion and limitation may not apply to You.
251 |
252 | 8. Litigation
253 |
254 | Any litigation relating to this License may be brought only in the courts
255 | of a jurisdiction where the defendant maintains its principal place of business
256 | and such litigation shall be governed by laws of that jurisdiction, without
257 | reference to its conflict-of-law provisions. Nothing in this Section shall
258 | prevent a party's ability to bring cross-claims or counter-claims.
259 |
260 | 9. Miscellaneous
261 |
262 | This License represents the complete agreement concerning the subject matter
263 | hereof. If any provision of this License is held to be unenforceable, such
264 | provision shall be reformed only to the extent necessary to make it enforceable.
265 | Any law or regulation which provides that the language of a contract shall
266 | be construed against the drafter shall not be used to construe this License
267 | against a Contributor.
268 |
269 | 10. Versions of the License
270 |
271 | 10.1. New Versions
272 |
273 | Mozilla Foundation is the license steward. Except as provided in Section 10.3,
274 | no one other than the license steward has the right to modify or publish new
275 | versions of this License. Each version will be given a distinguishing version
276 | number.
277 |
278 | 10.2. Effect of New Versions
279 |
280 | You may distribute the Covered Software under the terms of the version of
281 | the License under which You originally received the Covered Software, or under
282 | the terms of any subsequent version published by the license steward.
283 |
284 | 10.3. Modified Versions
285 |
286 | If you create software not governed by this License, and you want to create
287 | a new license for such software, you may create and use a modified version
288 | of this License if you rename the license and remove any references to the
289 | name of the license steward (except to note that such modified license differs
290 | from this License).
291 |
292 | 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses
293 |
294 | If You choose to distribute Source Code Form that is Incompatible With Secondary
295 | Licenses under the terms of this version of the License, the notice described
296 | in Exhibit B of this License must be attached. Exhibit A - Source Code Form
297 | License Notice
298 |
299 | This Source Code Form is subject to the terms of the Mozilla Public License,
300 | v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain
301 | one at http://mozilla.org/MPL/2.0/.
302 |
303 | If it is not possible or desirable to put the notice in a particular file,
304 | then You may include the notice in a location (such as a LICENSE file in a
305 | relevant directory) where a recipient would be likely to look for such a notice.
306 |
307 | You may add additional accurate notices of copyright ownership.
308 |
309 | Exhibit B - "Incompatible With Secondary Licenses" Notice
310 |
311 | This Source Code Form is "Incompatible With Secondary Licenses", as defined
312 | by the Mozilla Public License, v. 2.0.
313 |
--------------------------------------------------------------------------------
/src/bin/activate.rs:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2020 Serokell
2 | // SPDX-FileCopyrightText: 2020 Andreas Fuchs
3 | // SPDX-FileCopyrightText: 2021 Yannik Sander
4 | //
5 | // SPDX-License-Identifier: MPL-2.0
6 |
7 | use signal_hook::{consts::signal::SIGHUP, iterator::Signals};
8 |
9 | use clap::Parser;
10 |
11 | use tokio::fs;
12 | use tokio::process::Command;
13 | use tokio::sync::mpsc;
14 | use tokio::time::timeout;
15 |
16 | use std::time::Duration;
17 |
18 | use std::env;
19 | use std::path::{Path, PathBuf};
20 |
21 | use notify::{recommended_watcher, RecommendedWatcher, RecursiveMode, Watcher};
22 |
23 | use thiserror::Error;
24 |
25 | use log::{debug, error, info, warn};
26 |
27 | /// Remote activation utility for deploy-rs
28 | #[derive(Parser, Debug)]
29 | #[command(version = "1.0", author = "Serokell ")]
30 | struct Opts {
31 | /// Print debug logs to output
32 | #[arg(short, long)]
33 | debug_logs: bool,
34 | /// Directory to print logs to
35 | #[arg(long)]
36 | log_dir: Option,
37 |
38 | #[command(subcommand)]
39 | subcmd: SubCommand,
40 | }
41 |
42 | #[derive(Parser, Debug)]
43 | enum SubCommand {
44 | Activate(ActivateOpts),
45 | Wait(WaitOpts),
46 | Revoke(RevokeOpts),
47 | }
48 |
49 | /// Activate a profile
50 | #[derive(Parser, Debug)]
51 | #[command(group(
52 | clap::ArgGroup::new("profile")
53 | .required(true)
54 | .multiple(false)
55 | .args(&["profile_path","profile_user"])
56 | ))]
57 | struct ActivateOpts {
58 | /// The closure to activate
59 | closure: String,
60 | /// The profile path to install into
61 | #[arg(long)]
62 | profile_path: Option,
63 | /// The profile user if explicit profile path is not specified
64 | #[arg(long, requires = "profile_name")]
65 | profile_user: Option,
66 | /// The profile name
67 | #[arg(long, requires = "profile_user")]
68 | profile_name: Option,
69 |
70 | /// Maximum time to wait for confirmation after activation
71 | #[arg(long)]
72 | confirm_timeout: u16,
73 |
74 | /// Wait for confirmation after deployment and rollback if not confirmed
75 | #[arg(long)]
76 | magic_rollback: bool,
77 |
78 | /// Auto rollback if failure
79 | #[arg(long)]
80 | auto_rollback: bool,
81 |
82 | /// Show what will be activated on the machines
83 | #[arg(long)]
84 | dry_activate: bool,
85 |
86 | /// Don't activate, but update the boot loader to boot into the new profile
87 | #[arg(long)]
88 | boot: bool,
89 |
90 | /// Path for any temporary files that may be needed during activation
91 | #[arg(long)]
92 | temp_path: PathBuf,
93 | }
94 |
95 | /// Wait for profile activation
96 | #[derive(Parser, Debug)]
97 | struct WaitOpts {
98 | /// The closure to wait for
99 | closure: String,
100 |
101 | /// Path for any temporary files that may be needed during activation
102 | #[arg(long)]
103 | temp_path: PathBuf,
104 |
105 | /// Timeout to wait for activation
106 | #[arg(long)]
107 | activation_timeout: Option,
108 | }
109 |
110 | /// Revoke profile activation
111 | #[derive(Parser, Debug)]
112 | struct RevokeOpts {
113 | /// The profile path to install into
114 | #[arg(long)]
115 | profile_path: Option,
116 | /// The profile user if explicit profile path is not specified
117 | #[arg(long, requires = "profile_name")]
118 | profile_user: Option,
119 | /// The profile name
120 | #[arg(long, requires = "profile_user")]
121 | profile_name: Option,
122 | }
123 |
124 | #[derive(Error, Debug)]
125 | pub enum DeactivateError {
126 | #[error("Failed to execute the rollback command: {0}")]
127 | Rollback(std::io::Error),
128 | #[error("The rollback resulted in a bad exit code: {0:?}")]
129 | RollbackExit(Option),
130 | #[error("Failed to run command for listing generations: {0}")]
131 | ListGen(std::io::Error),
132 | #[error("Command for listing generations resulted in a bad exit code: {0:?}")]
133 | ListGenExit(Option),
134 | #[error("Error converting generation list output to utf8: {0}")]
135 | DecodeListGenUtf8(std::string::FromUtf8Error),
136 | #[error("Failed to run command for deleting generation: {0}")]
137 | DeleteGen(std::io::Error),
138 | #[error("Command for deleting generations resulted in a bad exit code: {0:?}")]
139 | DeleteGenExit(Option),
140 | #[error("Failed to run command for re-activating the last generation: {0}")]
141 | Reactivate(std::io::Error),
142 | #[error("Command for re-activating the last generation resulted in a bad exit code: {0:?}")]
143 | ReactivateExit(Option),
144 | }
145 |
146 | pub async fn deactivate(profile_path: &str) -> Result<(), DeactivateError> {
147 | warn!("De-activating due to error");
148 |
149 | let nix_env_rollback_exit_status = Command::new("nix-env")
150 | .arg("-p")
151 | .arg(&profile_path)
152 | .arg("--rollback")
153 | .status()
154 | .await
155 | .map_err(DeactivateError::Rollback)?;
156 |
157 | match nix_env_rollback_exit_status.code() {
158 | Some(0) => (),
159 | a => return Err(DeactivateError::RollbackExit(a)),
160 | };
161 |
162 | debug!("Listing generations");
163 |
164 | let nix_env_list_generations_out = Command::new("nix-env")
165 | .arg("-p")
166 | .arg(&profile_path)
167 | .arg("--list-generations")
168 | .output()
169 | .await
170 | .map_err(DeactivateError::ListGen)?;
171 |
172 | match nix_env_list_generations_out.status.code() {
173 | Some(0) => (),
174 | a => return Err(DeactivateError::ListGenExit(a)),
175 | };
176 |
177 | let generations_list = String::from_utf8(nix_env_list_generations_out.stdout)
178 | .map_err(DeactivateError::DecodeListGenUtf8)?;
179 |
180 | let last_generation_line = generations_list
181 | .lines()
182 | .last()
183 | .expect("Expected to find a generation in list");
184 |
185 | let last_generation_id = last_generation_line
186 | .split_whitespace()
187 | .next()
188 | .expect("Expected to get ID from generation entry");
189 |
190 | debug!("Removing generation entry {}", last_generation_line);
191 | warn!("Removing generation by ID {}", last_generation_id);
192 |
193 | let nix_env_delete_generation_exit_status = Command::new("nix-env")
194 | .arg("-p")
195 | .arg(&profile_path)
196 | .arg("--delete-generations")
197 | .arg(last_generation_id)
198 | .status()
199 | .await
200 | .map_err(DeactivateError::DeleteGen)?;
201 |
202 | match nix_env_delete_generation_exit_status.code() {
203 | Some(0) => (),
204 | a => return Err(DeactivateError::DeleteGenExit(a)),
205 | };
206 |
207 | info!("Attempting to re-activate the last generation");
208 |
209 | let re_activate_exit_status = Command::new(format!("{}/deploy-rs-activate", profile_path))
210 | .env("PROFILE", &profile_path)
211 | .current_dir(&profile_path)
212 | .status()
213 | .await
214 | .map_err(DeactivateError::Reactivate)?;
215 |
216 | match re_activate_exit_status.code() {
217 | Some(0) => (),
218 | a => return Err(DeactivateError::ReactivateExit(a)),
219 | };
220 |
221 | Ok(())
222 | }
223 |
224 | #[derive(Error, Debug)]
225 | pub enum ActivationConfirmationError {
226 | #[error("Failed to create activation confirmation directory: {0}")]
227 | CreateConfirmDir(std::io::Error),
228 | #[error("Failed to create activation confirmation file: {0}")]
229 | CreateConfirmFile(std::io::Error),
230 | #[error("Could not watch for activation sentinel: {0}")]
231 | Watcher(#[from] notify::Error),
232 | #[error("Error waiting for confirmation event: {0}")]
233 | WaitingError(#[from] DangerZoneError),
234 | }
235 |
236 | #[derive(Error, Debug)]
237 | pub enum DangerZoneError {
238 | #[error("Timeout elapsed for confirmation")]
239 | TimesUp,
240 | #[error("inotify stream ended without activation confirmation")]
241 | NoConfirmation,
242 | #[error("inotify encountered an error: {0}")]
243 | Watch(notify::Error),
244 | }
245 |
246 | async fn danger_zone(
247 | mut events: mpsc::Receiver>,
248 | confirm_timeout: u16,
249 | ) -> Result<(), DangerZoneError> {
250 | info!("Waiting for confirmation event...");
251 |
252 | match timeout(Duration::from_secs(confirm_timeout as u64), events.recv()).await {
253 | Ok(Some(Ok(()))) => Ok(()),
254 | Ok(Some(Err(e))) => Err(DangerZoneError::Watch(e)),
255 | Ok(None) => Err(DangerZoneError::NoConfirmation),
256 | Err(_) => Err(DangerZoneError::TimesUp),
257 | }
258 | }
259 |
260 | pub async fn activation_confirmation(
261 | temp_path: PathBuf,
262 | confirm_timeout: u16,
263 | closure: String,
264 | ) -> Result<(), ActivationConfirmationError> {
265 | let lock_path = deploy::make_lock_path(&temp_path, &closure);
266 |
267 | debug!("Ensuring parent directory exists for canary file");
268 |
269 | if let Some(parent) = lock_path.parent() {
270 | fs::create_dir_all(parent)
271 | .await
272 | .map_err(ActivationConfirmationError::CreateConfirmDir)?;
273 | }
274 |
275 | debug!("Creating canary file");
276 |
277 | fs::File::create(&lock_path)
278 | .await
279 | .map_err(ActivationConfirmationError::CreateConfirmFile)?;
280 |
281 | debug!("Creating notify watcher");
282 |
283 | let (deleted, done) = mpsc::channel(1);
284 |
285 | let mut watcher: RecommendedWatcher =
286 | recommended_watcher(move |res: Result| {
287 | let send_result = match res {
288 | Ok(e) if e.kind == notify::EventKind::Remove(notify::event::RemoveKind::File) => {
289 | debug!("Got worthy removal event, sending on channel");
290 | deleted.try_send(Ok(()))
291 | }
292 | Err(e) => {
293 | debug!("Got error waiting for removal event, sending on channel");
294 | deleted.try_send(Err(e))
295 | }
296 | Ok(_) => Ok(()), // ignore non-removal events
297 | };
298 |
299 | if let Err(e) = send_result {
300 | error!("Could not send file system event to watcher: {}", e);
301 | }
302 | })?;
303 |
304 | watcher.watch(&lock_path, RecursiveMode::NonRecursive)?;
305 |
306 | danger_zone(done, confirm_timeout)
307 | .await
308 | .map_err(|err| ActivationConfirmationError::WaitingError(err))
309 | }
310 |
311 | #[derive(Error, Debug)]
312 | pub enum WaitError {
313 | #[error("Error creating watcher for activation: {0}")]
314 | Watcher(#[from] notify::Error),
315 | #[error("Error waiting for activation: {0}")]
316 | Waiting(#[from] DangerZoneError),
317 | }
318 | pub async fn wait(temp_path: PathBuf, closure: String, activation_timeout: Option) -> Result<(), WaitError> {
319 | let lock_path = deploy::make_lock_path(&temp_path, &closure);
320 |
321 | let (created, done) = mpsc::channel(1);
322 |
323 | let mut watcher: RecommendedWatcher = {
324 | // TODO: fix wasteful clone
325 | let lock_path = lock_path.clone();
326 |
327 | recommended_watcher(move |res: Result| {
328 | let send_result = match res {
329 | Ok(e) if e.kind == notify::EventKind::Create(notify::event::CreateKind::File) => {
330 | match &e.paths[..] {
331 | [x] => match lock_path.canonicalize() {
332 | // 'lock_path' may not exist yet when some other files are created in 'temp_path'
333 | // x is already supposed to be canonical path
334 | Ok(lock_path) if x == &lock_path => created.try_send(Ok(())),
335 | _ => Ok(()),
336 | },
337 | _ => Ok(()),
338 | }
339 | }
340 | Err(e) => created.try_send(Err(e)),
341 | Ok(_) => Ok(()), // ignore non-removal events
342 | };
343 |
344 | if let Err(e) = send_result {
345 | error!("Could not send file system event to watcher: {}", e);
346 | }
347 | })?
348 | };
349 |
350 | watcher.watch(&temp_path, RecursiveMode::NonRecursive)?;
351 |
352 | // Avoid a potential race condition by checking for existence after watcher creation
353 | if fs::metadata(&lock_path).await.is_ok() {
354 | watcher.unwatch(&temp_path)?;
355 | return Ok(());
356 | }
357 |
358 | danger_zone(done, activation_timeout.unwrap_or(240)).await?;
359 |
360 | info!("Found canary file, done waiting!");
361 |
362 | Ok(())
363 | }
364 |
365 | #[derive(Error, Debug)]
366 | pub enum ActivateError {
367 | #[error("Failed to execute the command for setting profile: {0}")]
368 | SetProfile(std::io::Error),
369 | #[error("The command for setting profile resulted in a bad exit code: {0:?}")]
370 | SetProfileExit(Option),
371 |
372 | #[error("Failed to execute the activation script: {0}")]
373 | RunActivate(std::io::Error),
374 | #[error("The activation script resulted in a bad exit code: {0:?}")]
375 | RunActivateExit(Option),
376 |
377 | #[error("There was an error de-activating after an error was encountered: {0}")]
378 | Deactivate(#[from] DeactivateError),
379 |
380 | #[error("Failed to get activation confirmation: {0}")]
381 | ActivationConfirmation(#[from] ActivationConfirmationError),
382 | }
383 |
384 | pub async fn activate(
385 | profile_path: String,
386 | closure: String,
387 | auto_rollback: bool,
388 | temp_path: PathBuf,
389 | confirm_timeout: u16,
390 | magic_rollback: bool,
391 | dry_activate: bool,
392 | boot: bool,
393 | ) -> Result<(), ActivateError> {
394 | if !dry_activate {
395 | info!("Activating profile");
396 | let nix_env_set_exit_status = Command::new("nix-env")
397 | .arg("-p")
398 | .arg(&profile_path)
399 | .arg("--set")
400 | .arg(&closure)
401 | .status()
402 | .await
403 | .map_err(ActivateError::SetProfile)?;
404 | match nix_env_set_exit_status.code() {
405 | Some(0) => (),
406 | a => {
407 | if auto_rollback && !dry_activate {
408 | deactivate(&profile_path).await?;
409 | }
410 | return Err(ActivateError::SetProfileExit(a));
411 | }
412 | };
413 | }
414 |
415 | debug!("Running activation script");
416 |
417 | let activation_location = if dry_activate {
418 | &closure
419 | } else {
420 | &profile_path
421 | };
422 |
423 | let activate_status = match Command::new(format!("{}/deploy-rs-activate", activation_location))
424 | .env("PROFILE", activation_location)
425 | .env("DRY_ACTIVATE", if dry_activate { "1" } else { "0" })
426 | .env("BOOT", if boot { "1" } else { "0" })
427 | .current_dir(activation_location)
428 | .status()
429 | .await
430 | .map_err(ActivateError::RunActivate)
431 | {
432 | Ok(x) => x,
433 | Err(e) => {
434 | if auto_rollback && !dry_activate {
435 | deactivate(&profile_path).await?;
436 | }
437 | return Err(e);
438 | }
439 | };
440 |
441 | if !dry_activate {
442 | match activate_status.code() {
443 | Some(0) => (),
444 | a => {
445 | if auto_rollback {
446 | deactivate(&profile_path).await?;
447 | }
448 | return Err(ActivateError::RunActivateExit(a));
449 | }
450 | };
451 |
452 | if !dry_activate {
453 | info!("Activation succeeded!");
454 | }
455 |
456 | if magic_rollback && !boot {
457 | info!("Magic rollback is enabled, setting up confirmation hook...");
458 | if let Err(err) = activation_confirmation(temp_path, confirm_timeout, closure).await {
459 | deactivate(&profile_path).await?;
460 | return Err(ActivateError::ActivationConfirmation(err));
461 | }
462 | }
463 | }
464 |
465 | Ok(())
466 | }
467 |
468 | async fn revoke(profile_path: String) -> Result<(), DeactivateError> {
469 | deactivate(profile_path.as_str()).await?;
470 | Ok(())
471 | }
472 |
473 | #[derive(Error, Debug)]
474 | pub enum GetProfilePathError {
475 | #[error("Failed to deduce HOME directory for user {0}")]
476 | NoUserHome(String),
477 | }
478 |
479 | fn get_profile_path(
480 | profile_path: Option,
481 | profile_user: Option,
482 | profile_name: Option,
483 | ) -> Result {
484 | match (profile_path, profile_user, profile_name) {
485 | (Some(profile_path), None, None) => Ok(profile_path),
486 | (None, Some(profile_user), Some(profile_name)) => {
487 | let nix_state_dir = env::var("NIX_STATE_DIR").unwrap_or("/nix/var/nix".to_string());
488 | // As per https://nixos.org/manual/nix/stable/command-ref/files/profiles#profiles
489 | match &profile_user[..] {
490 | "root" => {
491 | match &profile_name[..] {
492 | // NixOS system profile belongs to the root user, but isn't stored in the 'per-user/root'
493 | "system" => Ok(format!("{}/profiles/system", nix_state_dir)),
494 | _ => Ok(format!(
495 | "{}/profiles/per-user/root/{}",
496 | nix_state_dir, profile_name
497 | )),
498 | }
499 | }
500 | _ => {
501 | let old_user_profiles_dir =
502 | format!("{}/profiles/per-user/{}", nix_state_dir, profile_user);
503 | // To stay backward compatible
504 | if Path::new(&old_user_profiles_dir).exists() {
505 | Ok(format!("{}/{}", old_user_profiles_dir, profile_name))
506 | } else {
507 | // https://github.com/NixOS/nix/blob/2.17.0/src/libstore/profiles.cc#L308
508 | // This is basically the equivalent of calling 'dirs::state_dir()'.
509 | // However, this function returns 'None' on macOS, while nix will actually
510 | // check env variables, so we imitate nix implementation below instead of
511 | // using 'dirs::state_dir()' directly.
512 | let state_dir = env::var("XDG_STATE_HOME").or_else(|_| {
513 | dirs::home_dir()
514 | .map(|h| {
515 | format!("{}/.local/state", h.as_path().display().to_string())
516 | })
517 | .ok_or(GetProfilePathError::NoUserHome(profile_user))
518 | })?;
519 | Ok(format!("{}/nix/profiles/{}", state_dir, profile_name))
520 | }
521 | }
522 | }
523 | }
524 | _ => panic!("impossible"),
525 | }
526 | }
527 |
528 | #[tokio::main]
529 | async fn main() -> Result<(), Box> {
530 | // Ensure that this process stays alive after the SSH connection dies
531 | let mut signals = Signals::new(&[SIGHUP])?;
532 | std::thread::spawn(move || {
533 | for _ in signals.forever() {
534 | println!("Received SIGHUP - ignoring...");
535 | }
536 | });
537 |
538 | let opts: Opts = Opts::parse();
539 |
540 | deploy::init_logger(
541 | opts.debug_logs,
542 | opts.log_dir.as_deref(),
543 | &match opts.subcmd {
544 | SubCommand::Activate(_) => deploy::LoggerType::Activate,
545 | SubCommand::Wait(_) => deploy::LoggerType::Wait,
546 | SubCommand::Revoke(_) => deploy::LoggerType::Revoke,
547 | },
548 | )?;
549 |
550 | let r = match opts.subcmd {
551 | SubCommand::Activate(activate_opts) => activate(
552 | get_profile_path(
553 | activate_opts.profile_path,
554 | activate_opts.profile_user,
555 | activate_opts.profile_name,
556 | )?,
557 | activate_opts.closure,
558 | activate_opts.auto_rollback,
559 | activate_opts.temp_path,
560 | activate_opts.confirm_timeout,
561 | activate_opts.magic_rollback,
562 | activate_opts.dry_activate,
563 | activate_opts.boot,
564 | )
565 | .await
566 | .map_err(|x| Box::new(x) as Box),
567 |
568 | SubCommand::Wait(wait_opts) => wait(wait_opts.temp_path, wait_opts.closure, wait_opts.activation_timeout)
569 | .await
570 | .map_err(|x| Box::new(x) as Box),
571 |
572 | SubCommand::Revoke(revoke_opts) => revoke(get_profile_path(
573 | revoke_opts.profile_path,
574 | revoke_opts.profile_user,
575 | revoke_opts.profile_name,
576 | )?)
577 | .await
578 | .map_err(|x| Box::new(x) as Box),
579 | };
580 |
581 | match r {
582 | Ok(()) => (),
583 | Err(err) => {
584 | error!("{}", err);
585 | std::process::exit(1)
586 | }
587 | }
588 |
589 | Ok(())
590 | }
591 |
--------------------------------------------------------------------------------
/src/deploy.rs:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2020 Serokell
2 | // SPDX-FileCopyrightText: 2020 Andreas Fuchs
3 | // SPDX-FileCopyrightText: 2021 Yannik Sander
4 | //
5 | // SPDX-License-Identifier: MPL-2.0
6 |
7 | use log::{debug, info, trace};
8 | use std::path::Path;
9 | use thiserror::Error;
10 | use tokio::{io::AsyncWriteExt, process::Command};
11 |
12 | use crate::{DeployDataDefsError, DeployDefs, ProfileInfo};
13 |
14 | struct ActivateCommandData<'a> {
15 | sudo: &'a Option,
16 | profile_info: &'a ProfileInfo,
17 | closure: &'a str,
18 | auto_rollback: bool,
19 | temp_path: &'a Path,
20 | confirm_timeout: u16,
21 | magic_rollback: bool,
22 | debug_logs: bool,
23 | log_dir: Option<&'a str>,
24 | dry_activate: bool,
25 | boot: bool,
26 | }
27 |
28 | fn build_activate_command(data: &ActivateCommandData) -> String {
29 | let mut self_activate_command = format!("{}/activate-rs", data.closure);
30 |
31 | if data.debug_logs {
32 | self_activate_command = format!("{} --debug-logs", self_activate_command);
33 | }
34 |
35 | if let Some(log_dir) = data.log_dir {
36 | self_activate_command = format!("{} --log-dir {}", self_activate_command, log_dir);
37 | }
38 |
39 | self_activate_command = format!(
40 | "{} activate '{}' {} --temp-path '{}'",
41 | self_activate_command,
42 | data.closure,
43 | match data.profile_info {
44 | ProfileInfo::ProfilePath { profile_path } =>
45 | format!("--profile-path '{}'", profile_path),
46 | ProfileInfo::ProfileUserAndName {
47 | profile_user,
48 | profile_name,
49 | } => format!(
50 | "--profile-user {} --profile-name {}",
51 | profile_user, profile_name
52 | ),
53 | },
54 | data.temp_path.display()
55 | );
56 |
57 | self_activate_command = format!(
58 | "{} --confirm-timeout {}",
59 | self_activate_command, data.confirm_timeout
60 | );
61 |
62 | if data.magic_rollback {
63 | self_activate_command = format!("{} --magic-rollback", self_activate_command);
64 | }
65 |
66 | if data.auto_rollback {
67 | self_activate_command = format!("{} --auto-rollback", self_activate_command);
68 | }
69 |
70 | if data.dry_activate {
71 | self_activate_command = format!("{} --dry-activate", self_activate_command);
72 | }
73 |
74 | if data.boot {
75 | self_activate_command = format!("{} --boot", self_activate_command);
76 | }
77 |
78 | if let Some(sudo_cmd) = &data.sudo {
79 | self_activate_command = format!("{} {}", sudo_cmd, self_activate_command);
80 | }
81 |
82 | self_activate_command
83 | }
84 |
85 | #[test]
86 | fn test_activation_command_builder() {
87 | let sudo = Some("sudo -u test".to_string());
88 | let profile_info = &ProfileInfo::ProfilePath {
89 | profile_path: "/blah/profiles/test".to_string(),
90 | };
91 | let closure = "/nix/store/blah/etc";
92 | let auto_rollback = true;
93 | let dry_activate = false;
94 | let boot = false;
95 | let temp_path = Path::new("/tmp");
96 | let confirm_timeout = 30;
97 | let magic_rollback = true;
98 | let debug_logs = true;
99 | let log_dir = Some("/tmp/something.txt");
100 |
101 | assert_eq!(
102 | build_activate_command(&ActivateCommandData {
103 | sudo: &sudo,
104 | profile_info,
105 | closure,
106 | auto_rollback,
107 | temp_path,
108 | confirm_timeout,
109 | magic_rollback,
110 | debug_logs,
111 | log_dir,
112 | dry_activate,
113 | boot,
114 | }),
115 | "sudo -u test /nix/store/blah/etc/activate-rs --debug-logs --log-dir /tmp/something.txt activate '/nix/store/blah/etc' --profile-path '/blah/profiles/test' --temp-path '/tmp' --confirm-timeout 30 --magic-rollback --auto-rollback"
116 | .to_string(),
117 | );
118 | }
119 |
120 | struct WaitCommandData<'a> {
121 | sudo: &'a Option,
122 | closure: &'a str,
123 | temp_path: &'a Path,
124 | activation_timeout: Option,
125 | debug_logs: bool,
126 | log_dir: Option<&'a str>,
127 | }
128 |
129 | fn build_wait_command(data: &WaitCommandData) -> String {
130 | let mut self_activate_command = format!("{}/activate-rs", data.closure);
131 |
132 | if data.debug_logs {
133 | self_activate_command = format!("{} --debug-logs", self_activate_command);
134 | }
135 |
136 | if let Some(log_dir) = data.log_dir {
137 | self_activate_command = format!("{} --log-dir {}", self_activate_command, log_dir);
138 | }
139 |
140 | self_activate_command = format!(
141 | "{} wait '{}' --temp-path '{}'",
142 | self_activate_command,
143 | data.closure,
144 | data.temp_path.display(),
145 | );
146 | if let Some(activation_timeout) = data.activation_timeout {
147 | self_activate_command = format!("{} --activation-timeout {}", self_activate_command, activation_timeout);
148 | }
149 |
150 | if let Some(sudo_cmd) = &data.sudo {
151 | self_activate_command = format!("{} {}", sudo_cmd, self_activate_command);
152 | }
153 |
154 | self_activate_command
155 | }
156 |
157 | #[test]
158 | fn test_wait_command_builder() {
159 | let sudo = Some("sudo -u test".to_string());
160 | let closure = "/nix/store/blah/etc";
161 | let temp_path = Path::new("/tmp");
162 | let activation_timeout = Some(600);
163 | let debug_logs = true;
164 | let log_dir = Some("/tmp/something.txt");
165 |
166 | assert_eq!(
167 | build_wait_command(&WaitCommandData {
168 | sudo: &sudo,
169 | closure,
170 | temp_path,
171 | activation_timeout,
172 | debug_logs,
173 | log_dir
174 | }),
175 | "sudo -u test /nix/store/blah/etc/activate-rs --debug-logs --log-dir /tmp/something.txt wait '/nix/store/blah/etc' --temp-path '/tmp' --activation-timeout 600"
176 | .to_string(),
177 | );
178 | }
179 |
180 | struct RevokeCommandData<'a> {
181 | sudo: &'a Option,
182 | closure: &'a str,
183 | profile_info: ProfileInfo,
184 | debug_logs: bool,
185 | log_dir: Option<&'a str>,
186 | }
187 |
188 | fn build_revoke_command(data: &RevokeCommandData) -> String {
189 | let mut self_activate_command = format!("{}/activate-rs", data.closure);
190 |
191 | if data.debug_logs {
192 | self_activate_command = format!("{} --debug-logs", self_activate_command);
193 | }
194 |
195 | if let Some(log_dir) = data.log_dir {
196 | self_activate_command = format!("{} --log-dir {}", self_activate_command, log_dir);
197 | }
198 |
199 | self_activate_command = format!(
200 | "{} revoke {}",
201 | self_activate_command,
202 | match &data.profile_info {
203 | ProfileInfo::ProfilePath { profile_path } =>
204 | format!("--profile-path '{}'", profile_path),
205 | ProfileInfo::ProfileUserAndName {
206 | profile_user,
207 | profile_name,
208 | } => format!(
209 | "--profile-user {} --profile-name {}",
210 | profile_user, profile_name
211 | ),
212 | }
213 | );
214 |
215 | if let Some(sudo_cmd) = &data.sudo {
216 | self_activate_command = format!("{} {}", sudo_cmd, self_activate_command);
217 | }
218 |
219 | self_activate_command
220 | }
221 |
222 | #[test]
223 | fn test_revoke_command_builder() {
224 | let sudo = Some("sudo -u test".to_string());
225 | let closure = "/nix/store/blah/etc";
226 | let profile_info = ProfileInfo::ProfilePath {
227 | profile_path: "/nix/var/nix/per-user/user/profile".to_string(),
228 | };
229 | let debug_logs = true;
230 | let log_dir = Some("/tmp/something.txt");
231 |
232 | assert_eq!(
233 | build_revoke_command(&RevokeCommandData {
234 | sudo: &sudo,
235 | closure,
236 | profile_info,
237 | debug_logs,
238 | log_dir
239 | }),
240 | "sudo -u test /nix/store/blah/etc/activate-rs --debug-logs --log-dir /tmp/something.txt revoke --profile-path '/nix/var/nix/per-user/user/profile'"
241 | .to_string(),
242 | );
243 | }
244 |
245 | async fn handle_sudo_stdin(ssh_activate_child: &mut tokio::process::Child, deploy_defs: &DeployDefs) -> Result<(), std::io::Error> {
246 | match ssh_activate_child.stdin.as_mut() {
247 | Some(stdin) => {
248 | let _ = stdin.write_all(format!("{}\n",deploy_defs.sudo_password.clone().unwrap_or("".to_string())).as_bytes()).await;
249 | Ok(())
250 | }
251 | None => {
252 | Err(
253 | std::io::Error::new(
254 | std::io::ErrorKind::Other,
255 | "Failed to open stdin for sudo command",
256 | )
257 | )
258 | }
259 | }
260 | }
261 |
262 | #[derive(Error, Debug)]
263 | pub enum ConfirmProfileError {
264 | #[error("Failed to run confirmation command over SSH (the server should roll back): {0}")]
265 | SSHConfirm(std::io::Error),
266 | #[error(
267 | "Confirming activation over SSH resulted in a bad exit code (the server should roll back): {0:?}"
268 | )]
269 | SSHConfirmExit(Option),
270 | }
271 |
272 | pub async fn confirm_profile(
273 | deploy_data: &super::DeployData<'_>,
274 | deploy_defs: &super::DeployDefs,
275 | temp_path: &Path,
276 | ssh_addr: &str,
277 | ) -> Result<(), ConfirmProfileError> {
278 | let mut ssh_confirm_command = Command::new("ssh");
279 | ssh_confirm_command
280 | .arg(ssh_addr)
281 | .stdin(std::process::Stdio::piped());
282 |
283 | for ssh_opt in &deploy_data.merged_settings.ssh_opts {
284 | ssh_confirm_command.arg(ssh_opt);
285 | }
286 |
287 | let lock_path = super::make_lock_path(temp_path, &deploy_data.profile.profile_settings.path);
288 |
289 | let mut confirm_command = format!("rm {}", lock_path.display());
290 | if let Some(sudo_cmd) = &deploy_defs.sudo {
291 | confirm_command = format!("{} {}", sudo_cmd, confirm_command);
292 | }
293 |
294 | debug!(
295 | "Attempting to run command to confirm deployment: {}",
296 | confirm_command
297 | );
298 |
299 | let mut ssh_confirm_child = ssh_confirm_command
300 | .arg(confirm_command)
301 | .spawn()
302 | .map_err(ConfirmProfileError::SSHConfirm)?;
303 |
304 | if deploy_data.merged_settings.interactive_sudo.unwrap_or(false) {
305 | trace!("[confirm] Piping in sudo password");
306 | handle_sudo_stdin(&mut ssh_confirm_child, deploy_defs)
307 | .await
308 | .map_err(ConfirmProfileError::SSHConfirm)?;
309 | }
310 |
311 | let ssh_confirm_exit_status = ssh_confirm_child
312 | .wait()
313 | .await
314 | .map_err(ConfirmProfileError::SSHConfirm)?;
315 |
316 | match ssh_confirm_exit_status.code() {
317 | Some(0) => (),
318 | a => return Err(ConfirmProfileError::SSHConfirmExit(a)),
319 | };
320 |
321 | info!("Deployment confirmed.");
322 |
323 | Ok(())
324 | }
325 |
326 | #[derive(Error, Debug)]
327 | pub enum DeployProfileError {
328 | #[error("Failed to spawn activation command over SSH: {0}")]
329 | SSHSpawnActivate(std::io::Error),
330 |
331 | #[error("Failed to run activation command over SSH: {0}")]
332 | SSHActivate(std::io::Error),
333 | #[error("Activating over SSH resulted in a bad exit code: {0:?}")]
334 | SSHActivateExit(Option),
335 | #[error("Activating over SSH resulted in a bad exit code: {0:?}")]
336 | SSHActivateTimeout(tokio::sync::oneshot::error::RecvError),
337 |
338 | #[error("Failed to run wait command over SSH: {0}")]
339 | SSHWait(std::io::Error),
340 | #[error("Waiting over SSH resulted in a bad exit code: {0:?}")]
341 | SSHWaitExit(Option),
342 |
343 | #[error("Failed to pipe to child stdin: {0}")]
344 | SSHActivatePipe(std::io::Error),
345 |
346 | #[error("Error confirming deployment: {0}")]
347 | Confirm(#[from] ConfirmProfileError),
348 | #[error("Deployment data invalid: {0}")]
349 | InvalidDeployDataDefs(#[from] DeployDataDefsError),
350 | }
351 |
352 | pub async fn deploy_profile(
353 | deploy_data: &super::DeployData<'_>,
354 | deploy_defs: &super::DeployDefs,
355 | dry_activate: bool,
356 | boot: bool,
357 | ) -> Result<(), DeployProfileError> {
358 | if !dry_activate {
359 | info!(
360 | "Activating profile `{}` for node `{}`",
361 | deploy_data.profile_name, deploy_data.node_name
362 | );
363 | }
364 |
365 | let temp_path: &Path = match &deploy_data.merged_settings.temp_path {
366 | Some(x) => x,
367 | None => Path::new("/tmp"),
368 | };
369 |
370 | let confirm_timeout = deploy_data.merged_settings.confirm_timeout.unwrap_or(30);
371 |
372 | let activation_timeout = deploy_data.merged_settings.activation_timeout;
373 |
374 | let magic_rollback = deploy_data.merged_settings.magic_rollback.unwrap_or(true);
375 |
376 | let auto_rollback = deploy_data.merged_settings.auto_rollback.unwrap_or(true);
377 |
378 | let self_activate_command = build_activate_command(&ActivateCommandData {
379 | sudo: &deploy_defs.sudo,
380 | profile_info: &deploy_data.get_profile_info()?,
381 | closure: &deploy_data.profile.profile_settings.path,
382 | auto_rollback,
383 | temp_path: temp_path,
384 | confirm_timeout,
385 | magic_rollback,
386 | debug_logs: deploy_data.debug_logs,
387 | log_dir: deploy_data.log_dir,
388 | dry_activate,
389 | boot,
390 | });
391 |
392 | debug!("Constructed activation command: {}", self_activate_command);
393 |
394 | let hostname = match deploy_data.cmd_overrides.hostname {
395 | Some(ref x) => x,
396 | None => &deploy_data.node.node_settings.hostname,
397 | };
398 |
399 | let ssh_addr = format!("{}@{}", deploy_defs.ssh_user, hostname);
400 |
401 | let mut ssh_activate_command = Command::new("ssh");
402 | ssh_activate_command
403 | .arg(&ssh_addr)
404 | .stdin(std::process::Stdio::piped());
405 |
406 | for ssh_opt in &deploy_data.merged_settings.ssh_opts {
407 | ssh_activate_command.arg(&ssh_opt);
408 | }
409 |
410 | if !magic_rollback || dry_activate || boot {
411 | let mut ssh_activate_child = ssh_activate_command
412 | .arg(self_activate_command)
413 | .spawn()
414 | .map_err(DeployProfileError::SSHSpawnActivate)?;
415 |
416 | if deploy_data.merged_settings.interactive_sudo.unwrap_or(false) {
417 | trace!("[activate] Piping in sudo password");
418 | handle_sudo_stdin(&mut ssh_activate_child, deploy_defs)
419 | .await
420 | .map_err(DeployProfileError::SSHActivatePipe)?;
421 | }
422 |
423 | let ssh_activate_exit_status = ssh_activate_child
424 | .wait()
425 | .await
426 | .map_err(DeployProfileError::SSHActivate)?;
427 |
428 | match ssh_activate_exit_status.code() {
429 | Some(0) => (),
430 | a => return Err(DeployProfileError::SSHActivateExit(a)),
431 | };
432 |
433 | if dry_activate {
434 | info!("Completed dry-activate!");
435 | } else if boot {
436 | info!("Success activating for next boot, done!");
437 | } else {
438 | info!("Success activating, done!");
439 | }
440 | } else {
441 | let self_wait_command = build_wait_command(&WaitCommandData {
442 | sudo: &deploy_defs.sudo,
443 | closure: &deploy_data.profile.profile_settings.path,
444 | temp_path: temp_path,
445 | activation_timeout: activation_timeout,
446 | debug_logs: deploy_data.debug_logs,
447 | log_dir: deploy_data.log_dir,
448 | });
449 |
450 | debug!("Constructed wait command: {}", self_wait_command);
451 |
452 | let mut ssh_activate_child = ssh_activate_command
453 | .arg(self_activate_command)
454 | .spawn()
455 | .map_err(DeployProfileError::SSHSpawnActivate)?;
456 |
457 | if deploy_data.merged_settings.interactive_sudo.unwrap_or(false) {
458 | trace!("[activate] Piping in sudo password");
459 | handle_sudo_stdin(&mut ssh_activate_child, deploy_defs)
460 | .await
461 | .map_err(DeployProfileError::SSHActivatePipe)?;
462 | }
463 |
464 | info!("Creating activation waiter");
465 |
466 | let mut ssh_wait_command = Command::new("ssh");
467 | ssh_wait_command
468 | .arg(&ssh_addr)
469 | .stdin(std::process::Stdio::piped());
470 |
471 | for ssh_opt in &deploy_data.merged_settings.ssh_opts {
472 | ssh_wait_command.arg(ssh_opt);
473 | }
474 |
475 | let (send_activate, recv_activate) = tokio::sync::oneshot::channel();
476 | let (send_activated, recv_activated) = tokio::sync::oneshot::channel();
477 |
478 | let thread = tokio::spawn(async move {
479 | let o = ssh_activate_child.wait_with_output().await;
480 |
481 | let maybe_err = match o {
482 | Err(x) => Some(DeployProfileError::SSHActivate(x)),
483 | Ok(ref x) => match x.status.code() {
484 | Some(0) => None,
485 | a => Some(DeployProfileError::SSHActivateExit(a)),
486 | },
487 | };
488 |
489 | if let Some(err) = maybe_err {
490 | send_activate.send(err).unwrap();
491 | }
492 |
493 | send_activated.send(()).unwrap();
494 | });
495 |
496 | let mut ssh_wait_child = ssh_wait_command
497 | .arg(self_wait_command)
498 | .spawn()
499 | .map_err(DeployProfileError::SSHWait)?;
500 |
501 | if deploy_data.merged_settings.interactive_sudo.unwrap_or(false) {
502 | trace!("[wait] Piping in sudo password");
503 | handle_sudo_stdin(&mut ssh_wait_child, deploy_defs)
504 | .await
505 | .map_err(DeployProfileError::SSHActivatePipe)?;
506 | }
507 |
508 | tokio::select! {
509 | x = ssh_wait_child.wait() => {
510 | debug!("Wait command ended");
511 | match x.map_err(DeployProfileError::SSHWait)?.code() {
512 | Some(0) => (),
513 | a => return Err(DeployProfileError::SSHWaitExit(a)),
514 | };
515 | },
516 | x = recv_activate => {
517 | debug!("Activate command exited with an error");
518 | return Err(x.unwrap());
519 | },
520 | }
521 |
522 | info!("Success activating, attempting to confirm activation");
523 |
524 | let c = confirm_profile(deploy_data, deploy_defs, temp_path, &ssh_addr).await;
525 | recv_activated.await.map_err(|x| DeployProfileError::SSHActivateTimeout(x))?;
526 | c?;
527 |
528 | thread
529 | .await
530 | .map_err(|x| DeployProfileError::SSHActivate(x.into()))?;
531 | }
532 |
533 | Ok(())
534 | }
535 |
536 | #[derive(Error, Debug)]
537 | pub enum RevokeProfileError {
538 | #[error("Failed to spawn revocation command over SSH: {0}")]
539 | SSHSpawnRevoke(std::io::Error),
540 |
541 | #[error("Error revoking deployment: {0}")]
542 | SSHRevoke(std::io::Error),
543 | #[error("Revoking over SSH resulted in a bad exit code: {0:?}")]
544 | SSHRevokeExit(Option),
545 |
546 | #[error("Deployment data invalid: {0}")]
547 | InvalidDeployDataDefs(#[from] DeployDataDefsError),
548 | }
549 | pub async fn revoke(
550 | deploy_data: &crate::DeployData<'_>,
551 | deploy_defs: &crate::DeployDefs,
552 | ) -> Result<(), RevokeProfileError> {
553 | let self_revoke_command = build_revoke_command(&RevokeCommandData {
554 | sudo: &deploy_defs.sudo,
555 | closure: &deploy_data.profile.profile_settings.path,
556 | profile_info: deploy_data.get_profile_info()?,
557 | debug_logs: deploy_data.debug_logs,
558 | log_dir: deploy_data.log_dir,
559 | });
560 |
561 | debug!("Constructed revoke command: {}", self_revoke_command);
562 |
563 | let hostname = match deploy_data.cmd_overrides.hostname {
564 | Some(ref x) => x,
565 | None => &deploy_data.node.node_settings.hostname,
566 | };
567 |
568 | let ssh_addr = format!("{}@{}", deploy_defs.ssh_user, hostname);
569 |
570 | let mut ssh_activate_command = Command::new("ssh");
571 | ssh_activate_command
572 | .arg(&ssh_addr)
573 | .stdin(std::process::Stdio::piped());
574 |
575 | for ssh_opt in &deploy_data.merged_settings.ssh_opts {
576 | ssh_activate_command.arg(&ssh_opt);
577 | }
578 |
579 | let mut ssh_revoke_child = ssh_activate_command
580 | .arg(self_revoke_command)
581 | .spawn()
582 | .map_err(RevokeProfileError::SSHSpawnRevoke)?;
583 |
584 | if deploy_data.merged_settings.interactive_sudo.unwrap_or(false) {
585 | trace!("[revoke] Piping in sudo password");
586 | handle_sudo_stdin(&mut ssh_revoke_child, deploy_defs)
587 | .await
588 | .map_err(RevokeProfileError::SSHRevoke)?;
589 | }
590 |
591 | let result = ssh_revoke_child.wait_with_output().await;
592 |
593 | match result {
594 | Err(x) => Err(RevokeProfileError::SSHRevoke(x)),
595 | Ok(ref x) => match x.status.code() {
596 | Some(0) => Ok(()),
597 | a => Err(RevokeProfileError::SSHRevokeExit(a)),
598 | },
599 | }
600 | }
601 |
--------------------------------------------------------------------------------
/src/cli.rs:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2020 Serokell
2 | // SPDX-FileCopyrightText: 2021 Yannik Sander
3 | //
4 | // SPDX-License-Identifier: MPL-2.0
5 |
6 | use std::collections::HashMap;
7 | use std::io::{stdin, stdout, Write};
8 |
9 | use clap::{ArgMatches, Parser, FromArgMatches};
10 |
11 | use crate as deploy;
12 |
13 | use self::deploy::{DeployFlake, ParseFlakeError};
14 | use futures_util::stream::{StreamExt, TryStreamExt};
15 | use log::{debug, error, info, warn};
16 | use serde::Serialize;
17 | use std::path::PathBuf;
18 | use std::process::Stdio;
19 | use thiserror::Error;
20 | use tokio::process::Command;
21 |
22 | /// Simple Rust rewrite of a simple Nix Flake deployment tool
23 | #[derive(Parser, Debug, Clone)]
24 | #[command(version = "1.0", author = "Serokell ")]
25 | pub struct Opts {
26 | /// The flake to deploy
27 | #[arg(group = "deploy")]
28 | target: Option,
29 |
30 | /// A list of flakes to deploy alternatively
31 | #[arg(long, group = "deploy", num_args = 1..)]
32 | targets: Option>,
33 | /// Treat targets as files instead of flakes
34 | #[clap(short, long)]
35 | file: Option,
36 | /// Check signatures when using `nix copy`
37 | #[arg(short, long)]
38 | checksigs: bool,
39 | /// Use the interactive prompt before deployment
40 | #[arg(short, long)]
41 | interactive: bool,
42 | /// Extra arguments to be passed to nix build
43 | #[arg(last = true)]
44 | extra_build_args: Vec,
45 |
46 | /// Print debug logs to output
47 | #[arg(short, long)]
48 | debug_logs: bool,
49 | /// Directory to print logs to (including the background activation process)
50 | #[arg(long)]
51 | log_dir: Option,
52 |
53 | /// Keep the build outputs of each built profile
54 | #[arg(short, long)]
55 | keep_result: bool,
56 | /// Location to keep outputs from built profiles in
57 | #[arg(short, long)]
58 | result_path: Option,
59 |
60 | /// Skip the automatic pre-build checks
61 | #[arg(short, long)]
62 | skip_checks: bool,
63 |
64 | /// Build on remote host
65 | #[arg(long)]
66 | remote_build: bool,
67 |
68 | /// Override the SSH user with the given value
69 | #[arg(long)]
70 | ssh_user: Option,
71 | /// Override the profile user with the given value
72 | #[arg(long)]
73 | profile_user: Option,
74 | /// Override the SSH options used
75 | #[arg(long, allow_hyphen_values = true)]
76 | ssh_opts: Option,
77 | /// Override if the connecting to the target node should be considered fast
78 | #[arg(long)]
79 | fast_connection: Option,
80 | /// Override if a rollback should be attempted if activation fails
81 | #[arg(long)]
82 | auto_rollback: Option,
83 | /// Override hostname used for the node
84 | #[arg(long)]
85 | hostname: Option,
86 | /// Make activation wait for confirmation, or roll back after a period of time
87 | #[arg(long)]
88 | magic_rollback: Option,
89 | /// How long activation should wait for confirmation (if using magic-rollback)
90 | #[arg(long)]
91 | confirm_timeout: Option,
92 | /// How long we should wait for profile activation
93 | #[arg(long)]
94 | activation_timeout: Option,
95 | /// Where to store temporary files (only used by magic-rollback)
96 | #[arg(long)]
97 | temp_path: Option,
98 | /// Show what will be activated on the machines
99 | #[arg(long)]
100 | dry_activate: bool,
101 | /// Don't activate, but update the boot loader to boot into the new profile
102 | #[arg(long)]
103 | boot: bool,
104 | /// Revoke all previously succeeded deploys when deploying multiple profiles
105 | #[arg(long)]
106 | rollback_succeeded: Option,
107 | /// Which sudo command to use. Must accept at least two arguments: user name to execute commands as and the rest is the command to execute
108 | #[arg(long)]
109 | sudo: Option,
110 | /// Prompt for sudo password during activation.
111 | #[arg(long)]
112 | interactive_sudo: Option,
113 | }
114 |
115 | /// Returns if the available Nix installation supports flakes
116 | async fn test_flake_support() -> Result {
117 | debug!("Checking for flake support");
118 |
119 | Ok(Command::new("nix")
120 | .arg("eval")
121 | .arg("--expr")
122 | .arg("builtins.getFlake")
123 | // This will error on some machines "intentionally", and we don't really need that printing
124 | .stdout(Stdio::null())
125 | .stderr(Stdio::null())
126 | .status()
127 | .await?
128 | .success())
129 | }
130 |
131 | #[derive(Error, Debug)]
132 | pub enum CheckDeploymentError {
133 | #[error("Failed to execute Nix checking command: {0}")]
134 | NixCheck(#[from] std::io::Error),
135 | #[error("Nix checking command resulted in a bad exit code: {0:?}")]
136 | NixCheckExit(Option),
137 | }
138 |
139 | async fn check_deployment(
140 | supports_flakes: bool,
141 | repo: &str,
142 | extra_build_args: &[String],
143 | ) -> Result<(), CheckDeploymentError> {
144 | info!("Running checks for flake in {}", repo);
145 |
146 | let mut check_command = match supports_flakes {
147 | true => Command::new("nix"),
148 | false => Command::new("nix-build"),
149 | };
150 |
151 | if supports_flakes {
152 | check_command.arg("flake").arg("check").arg(repo);
153 | } else {
154 | check_command.arg("-E")
155 | .arg("--no-out-link")
156 | .arg(format!("let r = import {}/.; x = (if builtins.isFunction r then (r {{}}) else r); in if x ? checks then x.checks.${{builtins.currentSystem}} else {{}}", repo));
157 | }
158 |
159 | check_command.args(extra_build_args);
160 |
161 | let check_status = check_command.status().await?;
162 |
163 | match check_status.code() {
164 | Some(0) => (),
165 | a => return Err(CheckDeploymentError::NixCheckExit(a)),
166 | };
167 |
168 | Ok(())
169 | }
170 |
171 | #[derive(Error, Debug)]
172 | pub enum GetDeploymentDataError {
173 | #[error("Failed to execute nix eval command: {0}")]
174 | NixEval(std::io::Error),
175 | #[error("Failed to read output from evaluation: {0}")]
176 | NixEvalOut(std::io::Error),
177 | #[error("Evaluation resulted in a bad exit code: {0:?}")]
178 | NixEvalExit(Option),
179 | #[error("Error converting evaluation output to utf8: {0}")]
180 | DecodeUtf8(#[from] std::string::FromUtf8Error),
181 | #[error("Error decoding the JSON from evaluation: {0}")]
182 | DecodeJson(#[from] serde_json::error::Error),
183 | #[error("Impossible happened: profile is set but node is not")]
184 | ProfileNoNode,
185 | }
186 |
187 | /// Evaluates the Nix in the given `repo` and return the processed Data from it
188 | async fn get_deployment_data(
189 | supports_flakes: bool,
190 | flakes: &[deploy::DeployFlake<'_>],
191 | extra_build_args: &[String],
192 | ) -> Result, GetDeploymentDataError> {
193 | futures_util::stream::iter(flakes).then(|flake| async move {
194 |
195 | info!("Evaluating flake in {}", flake.repo);
196 |
197 | let mut c = if supports_flakes {
198 | Command::new("nix")
199 | } else {
200 | Command::new("nix-instantiate")
201 | };
202 |
203 | if supports_flakes {
204 | c.arg("eval")
205 | .arg("--json")
206 | .arg(format!("{}#deploy", flake.repo))
207 | // We use --apply instead of --expr so that we don't have to deal with builtins.getFlake
208 | .arg("--apply");
209 | match (&flake.node, &flake.profile) {
210 | (Some(node), Some(profile)) => {
211 | // Ignore all nodes and all profiles but the one we're evaluating
212 | c.arg(format!(
213 | r#"
214 | deploy:
215 | (deploy // {{
216 | nodes = {{
217 | "{0}" = deploy.nodes."{0}" // {{
218 | profiles = {{
219 | inherit (deploy.nodes."{0}".profiles) "{1}";
220 | }};
221 | }};
222 | }};
223 | }})
224 | "#,
225 | node, profile
226 | ))
227 | }
228 | (Some(node), None) => {
229 | // Ignore all nodes but the one we're evaluating
230 | c.arg(format!(
231 | r#"
232 | deploy:
233 | (deploy // {{
234 | nodes = {{
235 | inherit (deploy.nodes) "{}";
236 | }};
237 | }})
238 | "#,
239 | node
240 | ))
241 | }
242 | (None, None) => {
243 | // We need to evaluate all profiles of all nodes anyway, so just do it strictly
244 | c.arg("deploy: deploy")
245 | }
246 | (None, Some(_)) => return Err(GetDeploymentDataError::ProfileNoNode),
247 | }
248 | } else {
249 | c
250 | .arg("--strict")
251 | .arg("--read-write-mode")
252 | .arg("--json")
253 | .arg("--eval")
254 | .arg("-E")
255 | .arg(format!("let r = import {}/.; in if builtins.isFunction r then (r {{}}).deploy else r.deploy", flake.repo))
256 | };
257 |
258 | c.args(extra_build_args);
259 |
260 | let build_child = c
261 | .stdout(Stdio::piped())
262 | .spawn()
263 | .map_err(GetDeploymentDataError::NixEval)?;
264 |
265 | let build_output = build_child
266 | .wait_with_output()
267 | .await
268 | .map_err(GetDeploymentDataError::NixEvalOut)?;
269 |
270 | match build_output.status.code() {
271 | Some(0) => (),
272 | a => return Err(GetDeploymentDataError::NixEvalExit(a)),
273 | };
274 |
275 | let data_json = String::from_utf8(build_output.stdout)?;
276 |
277 | Ok(serde_json::from_str(&data_json)?)
278 | }).try_collect().await
279 | }
280 |
281 | #[derive(Serialize)]
282 | struct PromptPart<'a> {
283 | user: &'a str,
284 | ssh_user: &'a str,
285 | path: &'a str,
286 | hostname: &'a str,
287 | ssh_opts: &'a [String],
288 | }
289 |
290 | fn print_deployment(
291 | parts: &[(
292 | &deploy::DeployFlake<'_>,
293 | deploy::DeployData,
294 | deploy::DeployDefs,
295 | )],
296 | ) -> Result<(), toml::ser::Error> {
297 | let mut part_map: HashMap> = HashMap::new();
298 |
299 | for (_, data, defs) in parts {
300 | part_map
301 | .entry(data.node_name.to_string())
302 | .or_insert_with(HashMap::new)
303 | .insert(
304 | data.profile_name.to_string(),
305 | PromptPart {
306 | user: &defs.profile_user,
307 | ssh_user: &defs.ssh_user,
308 | path: &data.profile.profile_settings.path,
309 | hostname: &data.node.node_settings.hostname,
310 | ssh_opts: &data.merged_settings.ssh_opts,
311 | },
312 | );
313 | }
314 |
315 | let toml = toml::to_string(&part_map)?;
316 |
317 | info!("The following profiles are going to be deployed:\n{}", toml);
318 |
319 | Ok(())
320 | }
321 | #[derive(Error, Debug)]
322 | pub enum PromptDeploymentError {
323 | #[error("Failed to make printable TOML of deployment: {0}")]
324 | TomlFormat(#[from] toml::ser::Error),
325 | #[error("Failed to flush stdout prior to query: {0}")]
326 | StdoutFlush(std::io::Error),
327 | #[error("Failed to read line from stdin: {0}")]
328 | StdinRead(std::io::Error),
329 | #[error("User cancelled deployment")]
330 | Cancelled,
331 | }
332 |
333 | fn prompt_deployment(
334 | parts: &[(
335 | &deploy::DeployFlake<'_>,
336 | deploy::DeployData,
337 | deploy::DeployDefs,
338 | )],
339 | ) -> Result<(), PromptDeploymentError> {
340 | print_deployment(parts)?;
341 |
342 | info!("Are you sure you want to deploy these profiles?");
343 | print!("> ");
344 |
345 | stdout()
346 | .flush()
347 | .map_err(PromptDeploymentError::StdoutFlush)?;
348 |
349 | let mut s = String::new();
350 | stdin()
351 | .read_line(&mut s)
352 | .map_err(PromptDeploymentError::StdinRead)?;
353 |
354 | if !yn::yes(&s) {
355 | if yn::is_somewhat_yes(&s) {
356 | info!("Sounds like you might want to continue, to be more clear please just say \"yes\". Do you want to deploy these profiles?");
357 | print!("> ");
358 |
359 | stdout()
360 | .flush()
361 | .map_err(PromptDeploymentError::StdoutFlush)?;
362 |
363 | let mut s = String::new();
364 | stdin()
365 | .read_line(&mut s)
366 | .map_err(PromptDeploymentError::StdinRead)?;
367 |
368 | if !yn::yes(&s) {
369 | return Err(PromptDeploymentError::Cancelled);
370 | }
371 | } else {
372 | if !yn::no(&s) {
373 | info!(
374 | "That was unclear, but sounded like a no to me. Please say \"yes\" or \"no\" to be more clear."
375 | );
376 | }
377 |
378 | return Err(PromptDeploymentError::Cancelled);
379 | }
380 | }
381 |
382 | Ok(())
383 | }
384 |
385 | #[derive(Error, Debug)]
386 | pub enum RunDeployError {
387 | #[error("Failed to deploy profile to node {0}: {1}")]
388 | DeployProfile(String, deploy::deploy::DeployProfileError),
389 | #[error("Failed to build profile on node {0}: {0}")]
390 | BuildProfile(String, deploy::push::PushProfileError),
391 | #[error("Failed to push profile to node {0}: {0}")]
392 | PushProfile(String, deploy::push::PushProfileError),
393 | #[error("No profile named `{0}` was found")]
394 | ProfileNotFound(String),
395 | #[error("No node named `{0}` was found")]
396 | NodeNotFound(String),
397 | #[error("Profile was provided without a node name")]
398 | ProfileWithoutNode,
399 | #[error("Error processing deployment definitions: {0}")]
400 | DeployDataDefs(#[from] deploy::DeployDataDefsError),
401 | #[error("Failed to make printable TOML of deployment: {0}")]
402 | TomlFormat(#[from] toml::ser::Error),
403 | #[error("{0}")]
404 | PromptDeployment(#[from] PromptDeploymentError),
405 | #[error("Failed to revoke profile for node {0}: {1}")]
406 | RevokeProfile(String, deploy::deploy::RevokeProfileError),
407 | #[error("Deployment to node {0} failed, rolled back to previous generation")]
408 | Rollback(String)
409 | }
410 |
411 | type ToDeploy<'a> = Vec<(
412 | &'a deploy::DeployFlake<'a>,
413 | &'a deploy::data::Data,
414 | (&'a str, &'a deploy::data::Node),
415 | (&'a str, &'a deploy::data::Profile),
416 | )>;
417 |
418 | async fn run_deploy(
419 | deploy_flakes: Vec>,
420 | data: Vec,
421 | supports_flakes: bool,
422 | check_sigs: bool,
423 | interactive: bool,
424 | cmd_overrides: &deploy::CmdOverrides,
425 | keep_result: bool,
426 | result_path: Option<&str>,
427 | extra_build_args: &[String],
428 | debug_logs: bool,
429 | dry_activate: bool,
430 | boot: bool,
431 | log_dir: &Option,
432 | rollback_succeeded: bool,
433 | ) -> Result<(), RunDeployError> {
434 | let to_deploy: ToDeploy = deploy_flakes
435 | .iter()
436 | .zip(&data)
437 | .map(|(deploy_flake, data)| {
438 | let to_deploys: ToDeploy = match (&deploy_flake.node, &deploy_flake.profile) {
439 | (Some(node_name), Some(profile_name)) => {
440 | let node = match data.nodes.get(node_name) {
441 | Some(x) => x,
442 | None => return Err(RunDeployError::NodeNotFound(node_name.clone())),
443 | };
444 | let profile = match node.node_settings.profiles.get(profile_name) {
445 | Some(x) => x,
446 | None => return Err(RunDeployError::ProfileNotFound(profile_name.clone())),
447 | };
448 |
449 | vec![(
450 | deploy_flake,
451 | data,
452 | (node_name.as_str(), node),
453 | (profile_name.as_str(), profile),
454 | )]
455 | }
456 | (Some(node_name), None) => {
457 | let node = match data.nodes.get(node_name) {
458 | Some(x) => x,
459 | None => return Err(RunDeployError::NodeNotFound(node_name.clone())),
460 | };
461 |
462 | let mut profiles_list: Vec<(&str, &deploy::data::Profile)> = Vec::new();
463 |
464 | for profile_name in [
465 | node.node_settings.profiles_order.iter().collect(),
466 | node.node_settings.profiles.keys().collect::>(),
467 | ]
468 | .concat()
469 | {
470 | let profile = match node.node_settings.profiles.get(profile_name) {
471 | Some(x) => x,
472 | None => {
473 | return Err(RunDeployError::ProfileNotFound(profile_name.clone()))
474 | }
475 | };
476 |
477 | if !profiles_list.iter().any(|(n, _)| n == profile_name) {
478 | profiles_list.push((profile_name, profile));
479 | }
480 | }
481 |
482 | profiles_list
483 | .into_iter()
484 | .map(|x| (deploy_flake, data, (node_name.as_str(), node), x))
485 | .collect()
486 | }
487 | (None, None) => {
488 | let mut l = Vec::new();
489 |
490 | for (node_name, node) in &data.nodes {
491 | let mut profiles_list: Vec<(&str, &deploy::data::Profile)> = Vec::new();
492 |
493 | for profile_name in [
494 | node.node_settings.profiles_order.iter().collect(),
495 | node.node_settings.profiles.keys().collect::>(),
496 | ]
497 | .concat()
498 | {
499 | let profile = match node.node_settings.profiles.get(profile_name) {
500 | Some(x) => x,
501 | None => {
502 | return Err(RunDeployError::ProfileNotFound(
503 | profile_name.clone(),
504 | ))
505 | }
506 | };
507 |
508 | if !profiles_list.iter().any(|(n, _)| n == profile_name) {
509 | profiles_list.push((profile_name, profile));
510 | }
511 | }
512 |
513 | let ll: ToDeploy = profiles_list
514 | .into_iter()
515 | .map(|x| (deploy_flake, data, (node_name.as_str(), node), x))
516 | .collect();
517 |
518 | l.extend(ll);
519 | }
520 |
521 | l
522 | }
523 | (None, Some(_)) => return Err(RunDeployError::ProfileWithoutNode),
524 | };
525 | Ok(to_deploys)
526 | })
527 | .collect::, RunDeployError>>()?
528 | .into_iter()
529 | .flatten()
530 | .collect();
531 |
532 | let mut parts: Vec<(
533 | &deploy::DeployFlake<'_>,
534 | deploy::DeployData,
535 | deploy::DeployDefs,
536 | )> = Vec::new();
537 |
538 | for (deploy_flake, data, (node_name, node), (profile_name, profile)) in to_deploy {
539 | let deploy_data = deploy::make_deploy_data(
540 | &data.generic_settings,
541 | node,
542 | node_name,
543 | profile,
544 | profile_name,
545 | cmd_overrides,
546 | debug_logs,
547 | log_dir.as_deref(),
548 | );
549 |
550 | let mut deploy_defs = deploy_data.defs()?;
551 |
552 | if deploy_data.merged_settings.interactive_sudo.unwrap_or(false) {
553 | warn!("Interactive sudo is enabled! Using a sudo password is less secure than correctly configured SSH keys.\nPlease use keys in production environments.");
554 |
555 | if deploy_data.merged_settings.sudo.is_some() {
556 | warn!("Custom sudo commands should be configured to accept password input from stdin when using the 'interactive sudo' option. Deployment may fail if the custom command ignores stdin.");
557 | } else {
558 | // this configures sudo to hide the password prompt and accept input from stdin
559 | // at the time of writing, deploy_defs.sudo defaults to 'sudo -u root' when using user=root and sshUser as non-root
560 | let original = deploy_defs.sudo.unwrap_or("sudo".to_string());
561 | deploy_defs.sudo = Some(format!("{} -S -p \"\"", original));
562 | }
563 |
564 | info!("You will now be prompted for the sudo password for {}.", node.node_settings.hostname);
565 | let sudo_password = rpassword::prompt_password(format!("(sudo for {}) Password: ", node.node_settings.hostname)).unwrap_or("".to_string());
566 |
567 | deploy_defs.sudo_password = Some(sudo_password);
568 | }
569 |
570 | parts.push((deploy_flake, deploy_data, deploy_defs));
571 | }
572 |
573 | if interactive {
574 | prompt_deployment(&parts[..])?;
575 | } else {
576 | print_deployment(&parts[..])?;
577 | }
578 |
579 | let data_iter = || {
580 | parts.iter().map(
581 | |(deploy_flake, deploy_data, deploy_defs)| deploy::push::PushProfileData {
582 | supports_flakes,
583 | check_sigs,
584 | repo: deploy_flake.repo,
585 | deploy_data,
586 | deploy_defs,
587 | keep_result,
588 | result_path,
589 | extra_build_args,
590 | },
591 | )
592 | };
593 |
594 | for data in data_iter() {
595 | let node_name: String = data.deploy_data.node_name.to_string();
596 | deploy::push::build_profile(data).await.map_err(|e| {
597 | RunDeployError::BuildProfile(node_name, e)
598 | })?;
599 | }
600 |
601 | for data in data_iter() {
602 | let node_name: String = data.deploy_data.node_name.to_string();
603 | deploy::push::push_profile(data).await.map_err(|e| {
604 | RunDeployError::PushProfile(node_name, e)
605 | })?;
606 | }
607 |
608 | let mut succeeded: Vec<(&deploy::DeployData, &deploy::DeployDefs)> = vec![];
609 |
610 | // Run all deployments
611 | // In case of an error rollback any previoulsy made deployment.
612 | // Rollbacks adhere to the global seeting to auto_rollback and secondary
613 | // the profile's configuration
614 | for (_, deploy_data, deploy_defs) in &parts {
615 | if let Err(e) = deploy::deploy::deploy_profile(deploy_data, deploy_defs, dry_activate, boot).await
616 | {
617 | error!("{}", e);
618 | if dry_activate {
619 | info!("dry run, not rolling back");
620 | }
621 | if rollback_succeeded && cmd_overrides.auto_rollback.unwrap_or(true) {
622 | info!("Revoking previous deploys");
623 | // revoking all previous deploys
624 | // (adheres to profile configuration if not set explicitely by
625 | // the command line)
626 | for (deploy_data, deploy_defs) in &succeeded {
627 | if deploy_data.merged_settings.auto_rollback.unwrap_or(true) {
628 | deploy::deploy::revoke(*deploy_data, *deploy_defs).await.map_err(|e| {
629 | RunDeployError::RevokeProfile(deploy_data.node_name.to_string(), e)
630 | })?;
631 | }
632 | }
633 | return Err(RunDeployError::Rollback(deploy_data.node_name.to_string()));
634 | }
635 | return Err(RunDeployError::DeployProfile(deploy_data.node_name.to_string(), e))
636 | }
637 | succeeded.push((deploy_data, deploy_defs))
638 | }
639 |
640 | Ok(())
641 | }
642 |
643 | #[derive(Error, Debug)]
644 | pub enum RunError {
645 | #[error("Failed to deploy profile: {0}")]
646 | DeployProfile(#[from] deploy::deploy::DeployProfileError),
647 | #[error("Failed to push profile: {0}")]
648 | PushProfile(#[from] deploy::push::PushProfileError),
649 | #[error("Failed to test for flake support: {0}")]
650 | FlakeTest(std::io::Error),
651 | #[error("Failed to check deployment: {0}")]
652 | CheckDeployment(#[from] CheckDeploymentError),
653 | #[error("Failed to evaluate deployment data: {0}")]
654 | GetDeploymentData(#[from] GetDeploymentDataError),
655 | #[error("Error parsing flake: {0}")]
656 | ParseFlake(#[from] deploy::ParseFlakeError),
657 | #[error("Error parsing arguments: {0}")]
658 | ParseArgs(#[from] clap::Error),
659 | #[error("Error initiating logger: {0}")]
660 | Logger(#[from] flexi_logger::FlexiLoggerError),
661 | #[error("{0}")]
662 | RunDeploy(#[from] RunDeployError),
663 | }
664 |
665 | pub async fn run(args: Option<&ArgMatches>) -> Result<(), RunError> {
666 | let opts = match args {
667 | Some(o) => ::from_arg_matches(o)?,
668 | None => Opts::parse(),
669 | };
670 |
671 | deploy::init_logger(
672 | opts.debug_logs,
673 | opts.log_dir.as_deref(),
674 | &deploy::LoggerType::Deploy,
675 | )?;
676 |
677 | if opts.dry_activate && opts.boot {
678 | error!("Cannot use both --dry-activate & --boot!");
679 | }
680 |
681 | let deploys = opts
682 | .clone()
683 | .targets
684 | .unwrap_or_else(|| vec![opts.clone().target.unwrap_or_else(|| ".".to_string())]);
685 |
686 | let deploy_flakes: Vec =
687 | if let Some(file) = &opts.file {
688 | deploys
689 | .iter()
690 | .map(|f| deploy::parse_file(file.as_str(), f.as_str()))
691 | .collect::, ParseFlakeError>>()?
692 | }
693 | else {
694 | deploys
695 | .iter()
696 | .map(|f| deploy::parse_flake(f.as_str()))
697 | .collect::, ParseFlakeError>>()?
698 | };
699 |
700 | let cmd_overrides = deploy::CmdOverrides {
701 | ssh_user: opts.ssh_user,
702 | profile_user: opts.profile_user,
703 | ssh_opts: opts.ssh_opts,
704 | fast_connection: opts.fast_connection,
705 | auto_rollback: opts.auto_rollback,
706 | hostname: opts.hostname,
707 | magic_rollback: opts.magic_rollback,
708 | temp_path: opts.temp_path,
709 | confirm_timeout: opts.confirm_timeout,
710 | activation_timeout: opts.activation_timeout,
711 | dry_activate: opts.dry_activate,
712 | remote_build: opts.remote_build,
713 | sudo: opts.sudo,
714 | interactive_sudo: opts.interactive_sudo
715 | };
716 |
717 | let supports_flakes = test_flake_support().await.map_err(RunError::FlakeTest)?;
718 | let do_not_want_flakes = opts.file.is_some();
719 |
720 | if !supports_flakes {
721 | warn!("A Nix version without flakes support was detected, support for this is work in progress");
722 | }
723 |
724 | if do_not_want_flakes {
725 | warn!("The --file option for deployments without flakes is experimental");
726 | }
727 |
728 | let using_flakes = supports_flakes && !do_not_want_flakes;
729 |
730 | if !opts.skip_checks {
731 | for deploy_flake in &deploy_flakes {
732 | check_deployment(using_flakes, deploy_flake.repo, &opts.extra_build_args).await?;
733 | }
734 | }
735 | let result_path = opts.result_path.as_deref();
736 | let data = get_deployment_data(using_flakes, &deploy_flakes, &opts.extra_build_args).await?;
737 | run_deploy(
738 | deploy_flakes,
739 | data,
740 | using_flakes,
741 | opts.checksigs,
742 | opts.interactive,
743 | &cmd_overrides,
744 | opts.keep_result,
745 | result_path,
746 | &opts.extra_build_args,
747 | opts.debug_logs,
748 | opts.dry_activate,
749 | opts.boot,
750 | &opts.log_dir,
751 | opts.rollback_succeeded.unwrap_or(true),
752 | )
753 | .await?;
754 |
755 | Ok(())
756 | }
757 |
--------------------------------------------------------------------------------
/Cargo.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Cargo.
2 | # It is not intended for manual editing.
3 | version = 4
4 |
5 | [[package]]
6 | name = "addr2line"
7 | version = "0.24.2"
8 | source = "registry+https://github.com/rust-lang/crates.io-index"
9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
10 | dependencies = [
11 | "gimli",
12 | ]
13 |
14 | [[package]]
15 | name = "adler2"
16 | version = "2.0.0"
17 | source = "registry+https://github.com/rust-lang/crates.io-index"
18 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
19 |
20 | [[package]]
21 | name = "aho-corasick"
22 | version = "1.1.3"
23 | source = "registry+https://github.com/rust-lang/crates.io-index"
24 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
25 | dependencies = [
26 | "memchr",
27 | ]
28 |
29 | [[package]]
30 | name = "android-tzdata"
31 | version = "0.1.1"
32 | source = "registry+https://github.com/rust-lang/crates.io-index"
33 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
34 |
35 | [[package]]
36 | name = "android_system_properties"
37 | version = "0.1.5"
38 | source = "registry+https://github.com/rust-lang/crates.io-index"
39 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
40 | dependencies = [
41 | "libc",
42 | ]
43 |
44 | [[package]]
45 | name = "anstream"
46 | version = "0.6.18"
47 | source = "registry+https://github.com/rust-lang/crates.io-index"
48 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
49 | dependencies = [
50 | "anstyle",
51 | "anstyle-parse",
52 | "anstyle-query",
53 | "anstyle-wincon",
54 | "colorchoice",
55 | "is_terminal_polyfill",
56 | "utf8parse",
57 | ]
58 |
59 | [[package]]
60 | name = "anstyle"
61 | version = "1.0.10"
62 | source = "registry+https://github.com/rust-lang/crates.io-index"
63 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
64 |
65 | [[package]]
66 | name = "anstyle-parse"
67 | version = "0.2.6"
68 | source = "registry+https://github.com/rust-lang/crates.io-index"
69 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
70 | dependencies = [
71 | "utf8parse",
72 | ]
73 |
74 | [[package]]
75 | name = "anstyle-query"
76 | version = "1.1.2"
77 | source = "registry+https://github.com/rust-lang/crates.io-index"
78 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
79 | dependencies = [
80 | "windows-sys 0.59.0",
81 | ]
82 |
83 | [[package]]
84 | name = "anstyle-wincon"
85 | version = "3.0.7"
86 | source = "registry+https://github.com/rust-lang/crates.io-index"
87 | checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
88 | dependencies = [
89 | "anstyle",
90 | "once_cell",
91 | "windows-sys 0.59.0",
92 | ]
93 |
94 | [[package]]
95 | name = "atty"
96 | version = "0.2.14"
97 | source = "registry+https://github.com/rust-lang/crates.io-index"
98 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
99 | dependencies = [
100 | "hermit-abi",
101 | "libc",
102 | "winapi",
103 | ]
104 |
105 | [[package]]
106 | name = "autocfg"
107 | version = "1.4.0"
108 | source = "registry+https://github.com/rust-lang/crates.io-index"
109 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
110 |
111 | [[package]]
112 | name = "backtrace"
113 | version = "0.3.74"
114 | source = "registry+https://github.com/rust-lang/crates.io-index"
115 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a"
116 | dependencies = [
117 | "addr2line",
118 | "cfg-if",
119 | "libc",
120 | "miniz_oxide",
121 | "object",
122 | "rustc-demangle",
123 | "windows-targets 0.52.6",
124 | ]
125 |
126 | [[package]]
127 | name = "bitflags"
128 | version = "1.3.2"
129 | source = "registry+https://github.com/rust-lang/crates.io-index"
130 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
131 |
132 | [[package]]
133 | name = "bitflags"
134 | version = "2.9.0"
135 | source = "registry+https://github.com/rust-lang/crates.io-index"
136 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
137 |
138 | [[package]]
139 | name = "bumpalo"
140 | version = "3.17.0"
141 | source = "registry+https://github.com/rust-lang/crates.io-index"
142 | checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
143 |
144 | [[package]]
145 | name = "bytes"
146 | version = "1.10.1"
147 | source = "registry+https://github.com/rust-lang/crates.io-index"
148 | checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
149 |
150 | [[package]]
151 | name = "cbitset"
152 | version = "0.2.0"
153 | source = "registry+https://github.com/rust-lang/crates.io-index"
154 | checksum = "29b6ad25ae296159fb0da12b970b2fe179b234584d7cd294c891e2bbb284466b"
155 | dependencies = [
156 | "num-traits",
157 | ]
158 |
159 | [[package]]
160 | name = "cc"
161 | version = "1.2.17"
162 | source = "registry+https://github.com/rust-lang/crates.io-index"
163 | checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a"
164 | dependencies = [
165 | "shlex",
166 | ]
167 |
168 | [[package]]
169 | name = "cfg-if"
170 | version = "1.0.0"
171 | source = "registry+https://github.com/rust-lang/crates.io-index"
172 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
173 |
174 | [[package]]
175 | name = "chrono"
176 | version = "0.4.40"
177 | source = "registry+https://github.com/rust-lang/crates.io-index"
178 | checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c"
179 | dependencies = [
180 | "android-tzdata",
181 | "iana-time-zone",
182 | "js-sys",
183 | "num-traits",
184 | "wasm-bindgen",
185 | "windows-link",
186 | ]
187 |
188 | [[package]]
189 | name = "clap"
190 | version = "4.5.34"
191 | source = "registry+https://github.com/rust-lang/crates.io-index"
192 | checksum = "e958897981290da2a852763fe9cdb89cd36977a5d729023127095fa94d95e2ff"
193 | dependencies = [
194 | "clap_builder",
195 | "clap_derive",
196 | ]
197 |
198 | [[package]]
199 | name = "clap_builder"
200 | version = "4.5.34"
201 | source = "registry+https://github.com/rust-lang/crates.io-index"
202 | checksum = "83b0f35019843db2160b5bb19ae09b4e6411ac33fc6a712003c33e03090e2489"
203 | dependencies = [
204 | "anstream",
205 | "anstyle",
206 | "clap_lex",
207 | "strsim",
208 | "terminal_size",
209 | ]
210 |
211 | [[package]]
212 | name = "clap_derive"
213 | version = "4.5.32"
214 | source = "registry+https://github.com/rust-lang/crates.io-index"
215 | checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7"
216 | dependencies = [
217 | "heck",
218 | "proc-macro2",
219 | "quote",
220 | "syn 2.0.100",
221 | ]
222 |
223 | [[package]]
224 | name = "clap_lex"
225 | version = "0.7.4"
226 | source = "registry+https://github.com/rust-lang/crates.io-index"
227 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
228 |
229 | [[package]]
230 | name = "colorchoice"
231 | version = "1.0.3"
232 | source = "registry+https://github.com/rust-lang/crates.io-index"
233 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
234 |
235 | [[package]]
236 | name = "core-foundation-sys"
237 | version = "0.8.7"
238 | source = "registry+https://github.com/rust-lang/crates.io-index"
239 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
240 |
241 | [[package]]
242 | name = "deploy-rs"
243 | version = "0.1.0"
244 | dependencies = [
245 | "clap",
246 | "dirs",
247 | "flexi_logger",
248 | "fork",
249 | "futures-util",
250 | "log",
251 | "merge",
252 | "notify",
253 | "rnix",
254 | "rpassword",
255 | "serde",
256 | "serde_json",
257 | "signal-hook",
258 | "thiserror 2.0.12",
259 | "tokio",
260 | "toml",
261 | "whoami",
262 | "yn",
263 | ]
264 |
265 | [[package]]
266 | name = "dirs"
267 | version = "6.0.0"
268 | source = "registry+https://github.com/rust-lang/crates.io-index"
269 | checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
270 | dependencies = [
271 | "dirs-sys",
272 | ]
273 |
274 | [[package]]
275 | name = "dirs-sys"
276 | version = "0.5.0"
277 | source = "registry+https://github.com/rust-lang/crates.io-index"
278 | checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
279 | dependencies = [
280 | "libc",
281 | "option-ext",
282 | "redox_users",
283 | "windows-sys 0.59.0",
284 | ]
285 |
286 | [[package]]
287 | name = "equivalent"
288 | version = "1.0.2"
289 | source = "registry+https://github.com/rust-lang/crates.io-index"
290 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
291 |
292 | [[package]]
293 | name = "errno"
294 | version = "0.3.10"
295 | source = "registry+https://github.com/rust-lang/crates.io-index"
296 | checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
297 | dependencies = [
298 | "libc",
299 | "windows-sys 0.59.0",
300 | ]
301 |
302 | [[package]]
303 | name = "filetime"
304 | version = "0.2.25"
305 | source = "registry+https://github.com/rust-lang/crates.io-index"
306 | checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
307 | dependencies = [
308 | "cfg-if",
309 | "libc",
310 | "libredox",
311 | "windows-sys 0.59.0",
312 | ]
313 |
314 | [[package]]
315 | name = "flexi_logger"
316 | version = "0.16.3"
317 | source = "registry+https://github.com/rust-lang/crates.io-index"
318 | checksum = "291b6ce7b3ed2dda82efa6aee4c6bdb55fd11bc88b06c55b01851e94b96e5322"
319 | dependencies = [
320 | "atty",
321 | "chrono",
322 | "glob",
323 | "lazy_static",
324 | "log",
325 | "regex",
326 | "thiserror 1.0.69",
327 | "yansi",
328 | ]
329 |
330 | [[package]]
331 | name = "fork"
332 | version = "0.2.0"
333 | source = "registry+https://github.com/rust-lang/crates.io-index"
334 | checksum = "05dc8b302e04a1c27f4fe694439ef0f29779ca4edc205b7b58f00db04e29656d"
335 | dependencies = [
336 | "libc",
337 | ]
338 |
339 | [[package]]
340 | name = "fsevent-sys"
341 | version = "4.1.0"
342 | source = "registry+https://github.com/rust-lang/crates.io-index"
343 | checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
344 | dependencies = [
345 | "libc",
346 | ]
347 |
348 | [[package]]
349 | name = "futures-core"
350 | version = "0.3.31"
351 | source = "registry+https://github.com/rust-lang/crates.io-index"
352 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
353 |
354 | [[package]]
355 | name = "futures-macro"
356 | version = "0.3.31"
357 | source = "registry+https://github.com/rust-lang/crates.io-index"
358 | checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
359 | dependencies = [
360 | "proc-macro2",
361 | "quote",
362 | "syn 2.0.100",
363 | ]
364 |
365 | [[package]]
366 | name = "futures-task"
367 | version = "0.3.31"
368 | source = "registry+https://github.com/rust-lang/crates.io-index"
369 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
370 |
371 | [[package]]
372 | name = "futures-util"
373 | version = "0.3.31"
374 | source = "registry+https://github.com/rust-lang/crates.io-index"
375 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
376 | dependencies = [
377 | "futures-core",
378 | "futures-macro",
379 | "futures-task",
380 | "pin-project-lite",
381 | "pin-utils",
382 | "slab",
383 | ]
384 |
385 | [[package]]
386 | name = "getrandom"
387 | version = "0.2.15"
388 | source = "registry+https://github.com/rust-lang/crates.io-index"
389 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
390 | dependencies = [
391 | "cfg-if",
392 | "libc",
393 | "wasi",
394 | ]
395 |
396 | [[package]]
397 | name = "gimli"
398 | version = "0.31.1"
399 | source = "registry+https://github.com/rust-lang/crates.io-index"
400 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
401 |
402 | [[package]]
403 | name = "glob"
404 | version = "0.3.2"
405 | source = "registry+https://github.com/rust-lang/crates.io-index"
406 | checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
407 |
408 | [[package]]
409 | name = "hashbrown"
410 | version = "0.15.2"
411 | source = "registry+https://github.com/rust-lang/crates.io-index"
412 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
413 |
414 | [[package]]
415 | name = "heck"
416 | version = "0.5.0"
417 | source = "registry+https://github.com/rust-lang/crates.io-index"
418 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
419 |
420 | [[package]]
421 | name = "hermit-abi"
422 | version = "0.1.19"
423 | source = "registry+https://github.com/rust-lang/crates.io-index"
424 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
425 | dependencies = [
426 | "libc",
427 | ]
428 |
429 | [[package]]
430 | name = "iana-time-zone"
431 | version = "0.1.62"
432 | source = "registry+https://github.com/rust-lang/crates.io-index"
433 | checksum = "b2fd658b06e56721792c5df4475705b6cda790e9298d19d2f8af083457bcd127"
434 | dependencies = [
435 | "android_system_properties",
436 | "core-foundation-sys",
437 | "iana-time-zone-haiku",
438 | "js-sys",
439 | "log",
440 | "wasm-bindgen",
441 | "windows-core",
442 | ]
443 |
444 | [[package]]
445 | name = "iana-time-zone-haiku"
446 | version = "0.1.2"
447 | source = "registry+https://github.com/rust-lang/crates.io-index"
448 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
449 | dependencies = [
450 | "cc",
451 | ]
452 |
453 | [[package]]
454 | name = "indexmap"
455 | version = "2.8.0"
456 | source = "registry+https://github.com/rust-lang/crates.io-index"
457 | checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058"
458 | dependencies = [
459 | "equivalent",
460 | "hashbrown",
461 | ]
462 |
463 | [[package]]
464 | name = "inotify"
465 | version = "0.11.0"
466 | source = "registry+https://github.com/rust-lang/crates.io-index"
467 | checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3"
468 | dependencies = [
469 | "bitflags 2.9.0",
470 | "inotify-sys",
471 | "libc",
472 | ]
473 |
474 | [[package]]
475 | name = "inotify-sys"
476 | version = "0.1.5"
477 | source = "registry+https://github.com/rust-lang/crates.io-index"
478 | checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
479 | dependencies = [
480 | "libc",
481 | ]
482 |
483 | [[package]]
484 | name = "is_terminal_polyfill"
485 | version = "1.70.1"
486 | source = "registry+https://github.com/rust-lang/crates.io-index"
487 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
488 |
489 | [[package]]
490 | name = "itoa"
491 | version = "1.0.15"
492 | source = "registry+https://github.com/rust-lang/crates.io-index"
493 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
494 |
495 | [[package]]
496 | name = "js-sys"
497 | version = "0.3.77"
498 | source = "registry+https://github.com/rust-lang/crates.io-index"
499 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
500 | dependencies = [
501 | "once_cell",
502 | "wasm-bindgen",
503 | ]
504 |
505 | [[package]]
506 | name = "kqueue"
507 | version = "1.0.8"
508 | source = "registry+https://github.com/rust-lang/crates.io-index"
509 | checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c"
510 | dependencies = [
511 | "kqueue-sys",
512 | "libc",
513 | ]
514 |
515 | [[package]]
516 | name = "kqueue-sys"
517 | version = "1.0.4"
518 | source = "registry+https://github.com/rust-lang/crates.io-index"
519 | checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b"
520 | dependencies = [
521 | "bitflags 1.3.2",
522 | "libc",
523 | ]
524 |
525 | [[package]]
526 | name = "lazy_static"
527 | version = "1.5.0"
528 | source = "registry+https://github.com/rust-lang/crates.io-index"
529 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
530 |
531 | [[package]]
532 | name = "libc"
533 | version = "0.2.171"
534 | source = "registry+https://github.com/rust-lang/crates.io-index"
535 | checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
536 |
537 | [[package]]
538 | name = "libredox"
539 | version = "0.1.3"
540 | source = "registry+https://github.com/rust-lang/crates.io-index"
541 | checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
542 | dependencies = [
543 | "bitflags 2.9.0",
544 | "libc",
545 | "redox_syscall",
546 | ]
547 |
548 | [[package]]
549 | name = "linux-raw-sys"
550 | version = "0.9.3"
551 | source = "registry+https://github.com/rust-lang/crates.io-index"
552 | checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413"
553 |
554 | [[package]]
555 | name = "log"
556 | version = "0.4.27"
557 | source = "registry+https://github.com/rust-lang/crates.io-index"
558 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
559 |
560 | [[package]]
561 | name = "memchr"
562 | version = "2.7.4"
563 | source = "registry+https://github.com/rust-lang/crates.io-index"
564 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
565 |
566 | [[package]]
567 | name = "merge"
568 | version = "0.1.0"
569 | source = "registry+https://github.com/rust-lang/crates.io-index"
570 | checksum = "10bbef93abb1da61525bbc45eeaff6473a41907d19f8f9aa5168d214e10693e9"
571 | dependencies = [
572 | "merge_derive",
573 | "num-traits",
574 | ]
575 |
576 | [[package]]
577 | name = "merge_derive"
578 | version = "0.1.0"
579 | source = "registry+https://github.com/rust-lang/crates.io-index"
580 | checksum = "209d075476da2e63b4b29e72a2ef627b840589588e71400a25e3565c4f849d07"
581 | dependencies = [
582 | "proc-macro-error",
583 | "proc-macro2",
584 | "quote",
585 | "syn 1.0.109",
586 | ]
587 |
588 | [[package]]
589 | name = "miniz_oxide"
590 | version = "0.8.5"
591 | source = "registry+https://github.com/rust-lang/crates.io-index"
592 | checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5"
593 | dependencies = [
594 | "adler2",
595 | ]
596 |
597 | [[package]]
598 | name = "mio"
599 | version = "1.0.3"
600 | source = "registry+https://github.com/rust-lang/crates.io-index"
601 | checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
602 | dependencies = [
603 | "libc",
604 | "log",
605 | "wasi",
606 | "windows-sys 0.52.0",
607 | ]
608 |
609 | [[package]]
610 | name = "notify"
611 | version = "8.0.0"
612 | source = "registry+https://github.com/rust-lang/crates.io-index"
613 | checksum = "2fee8403b3d66ac7b26aee6e40a897d85dc5ce26f44da36b8b73e987cc52e943"
614 | dependencies = [
615 | "bitflags 2.9.0",
616 | "filetime",
617 | "fsevent-sys",
618 | "inotify",
619 | "kqueue",
620 | "libc",
621 | "log",
622 | "mio",
623 | "notify-types",
624 | "walkdir",
625 | "windows-sys 0.59.0",
626 | ]
627 |
628 | [[package]]
629 | name = "notify-types"
630 | version = "2.0.0"
631 | source = "registry+https://github.com/rust-lang/crates.io-index"
632 | checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d"
633 |
634 | [[package]]
635 | name = "num-traits"
636 | version = "0.2.19"
637 | source = "registry+https://github.com/rust-lang/crates.io-index"
638 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
639 | dependencies = [
640 | "autocfg",
641 | ]
642 |
643 | [[package]]
644 | name = "object"
645 | version = "0.36.7"
646 | source = "registry+https://github.com/rust-lang/crates.io-index"
647 | checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
648 | dependencies = [
649 | "memchr",
650 | ]
651 |
652 | [[package]]
653 | name = "once_cell"
654 | version = "1.21.2"
655 | source = "registry+https://github.com/rust-lang/crates.io-index"
656 | checksum = "c2806eaa3524762875e21c3dcd057bc4b7bfa01ce4da8d46be1cd43649e1cc6b"
657 |
658 | [[package]]
659 | name = "option-ext"
660 | version = "0.2.0"
661 | source = "registry+https://github.com/rust-lang/crates.io-index"
662 | checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
663 |
664 | [[package]]
665 | name = "pin-project-lite"
666 | version = "0.2.16"
667 | source = "registry+https://github.com/rust-lang/crates.io-index"
668 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
669 |
670 | [[package]]
671 | name = "pin-utils"
672 | version = "0.1.0"
673 | source = "registry+https://github.com/rust-lang/crates.io-index"
674 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
675 |
676 | [[package]]
677 | name = "proc-macro-error"
678 | version = "1.0.4"
679 | source = "registry+https://github.com/rust-lang/crates.io-index"
680 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
681 | dependencies = [
682 | "proc-macro-error-attr",
683 | "proc-macro2",
684 | "quote",
685 | "syn 1.0.109",
686 | "version_check",
687 | ]
688 |
689 | [[package]]
690 | name = "proc-macro-error-attr"
691 | version = "1.0.4"
692 | source = "registry+https://github.com/rust-lang/crates.io-index"
693 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
694 | dependencies = [
695 | "proc-macro2",
696 | "quote",
697 | "version_check",
698 | ]
699 |
700 | [[package]]
701 | name = "proc-macro2"
702 | version = "1.0.94"
703 | source = "registry+https://github.com/rust-lang/crates.io-index"
704 | checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
705 | dependencies = [
706 | "unicode-ident",
707 | ]
708 |
709 | [[package]]
710 | name = "quote"
711 | version = "1.0.40"
712 | source = "registry+https://github.com/rust-lang/crates.io-index"
713 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
714 | dependencies = [
715 | "proc-macro2",
716 | ]
717 |
718 | [[package]]
719 | name = "redox_syscall"
720 | version = "0.5.10"
721 | source = "registry+https://github.com/rust-lang/crates.io-index"
722 | checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1"
723 | dependencies = [
724 | "bitflags 2.9.0",
725 | ]
726 |
727 | [[package]]
728 | name = "redox_users"
729 | version = "0.5.0"
730 | source = "registry+https://github.com/rust-lang/crates.io-index"
731 | checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b"
732 | dependencies = [
733 | "getrandom",
734 | "libredox",
735 | "thiserror 2.0.12",
736 | ]
737 |
738 | [[package]]
739 | name = "regex"
740 | version = "1.11.1"
741 | source = "registry+https://github.com/rust-lang/crates.io-index"
742 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
743 | dependencies = [
744 | "aho-corasick",
745 | "memchr",
746 | "regex-automata",
747 | "regex-syntax",
748 | ]
749 |
750 | [[package]]
751 | name = "regex-automata"
752 | version = "0.4.9"
753 | source = "registry+https://github.com/rust-lang/crates.io-index"
754 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
755 | dependencies = [
756 | "aho-corasick",
757 | "memchr",
758 | "regex-syntax",
759 | ]
760 |
761 | [[package]]
762 | name = "regex-syntax"
763 | version = "0.8.5"
764 | source = "registry+https://github.com/rust-lang/crates.io-index"
765 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
766 |
767 | [[package]]
768 | name = "rnix"
769 | version = "0.8.1"
770 | source = "registry+https://github.com/rust-lang/crates.io-index"
771 | checksum = "0a9b645f0edba447dbfc6473dd22999f46a1d00ab39e777a2713a1cf34a1597b"
772 | dependencies = [
773 | "cbitset",
774 | "rowan",
775 | ]
776 |
777 | [[package]]
778 | name = "rowan"
779 | version = "0.9.1"
780 | source = "registry+https://github.com/rust-lang/crates.io-index"
781 | checksum = "1ea7cadf87a9d8432e85cb4eb86bd2e765ace60c24ef86e79084dcae5d1c5a19"
782 | dependencies = [
783 | "rustc-hash",
784 | "smol_str",
785 | "text_unit",
786 | "thin-dst",
787 | ]
788 |
789 | [[package]]
790 | name = "rpassword"
791 | version = "7.3.1"
792 | source = "registry+https://github.com/rust-lang/crates.io-index"
793 | checksum = "80472be3c897911d0137b2d2b9055faf6eeac5b14e324073d83bc17b191d7e3f"
794 | dependencies = [
795 | "libc",
796 | "rtoolbox",
797 | "windows-sys 0.48.0",
798 | ]
799 |
800 | [[package]]
801 | name = "rtoolbox"
802 | version = "0.0.2"
803 | source = "registry+https://github.com/rust-lang/crates.io-index"
804 | checksum = "c247d24e63230cdb56463ae328478bd5eac8b8faa8c69461a77e8e323afac90e"
805 | dependencies = [
806 | "libc",
807 | "windows-sys 0.48.0",
808 | ]
809 |
810 | [[package]]
811 | name = "rustc-demangle"
812 | version = "0.1.24"
813 | source = "registry+https://github.com/rust-lang/crates.io-index"
814 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
815 |
816 | [[package]]
817 | name = "rustc-hash"
818 | version = "1.1.0"
819 | source = "registry+https://github.com/rust-lang/crates.io-index"
820 | checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
821 |
822 | [[package]]
823 | name = "rustix"
824 | version = "1.0.3"
825 | source = "registry+https://github.com/rust-lang/crates.io-index"
826 | checksum = "e56a18552996ac8d29ecc3b190b4fdbb2d91ca4ec396de7bbffaf43f3d637e96"
827 | dependencies = [
828 | "bitflags 2.9.0",
829 | "errno",
830 | "libc",
831 | "linux-raw-sys",
832 | "windows-sys 0.59.0",
833 | ]
834 |
835 | [[package]]
836 | name = "rustversion"
837 | version = "1.0.20"
838 | source = "registry+https://github.com/rust-lang/crates.io-index"
839 | checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
840 |
841 | [[package]]
842 | name = "ryu"
843 | version = "1.0.20"
844 | source = "registry+https://github.com/rust-lang/crates.io-index"
845 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
846 |
847 | [[package]]
848 | name = "same-file"
849 | version = "1.0.6"
850 | source = "registry+https://github.com/rust-lang/crates.io-index"
851 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
852 | dependencies = [
853 | "winapi-util",
854 | ]
855 |
856 | [[package]]
857 | name = "serde"
858 | version = "1.0.219"
859 | source = "registry+https://github.com/rust-lang/crates.io-index"
860 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
861 | dependencies = [
862 | "serde_derive",
863 | ]
864 |
865 | [[package]]
866 | name = "serde_derive"
867 | version = "1.0.219"
868 | source = "registry+https://github.com/rust-lang/crates.io-index"
869 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
870 | dependencies = [
871 | "proc-macro2",
872 | "quote",
873 | "syn 2.0.100",
874 | ]
875 |
876 | [[package]]
877 | name = "serde_json"
878 | version = "1.0.140"
879 | source = "registry+https://github.com/rust-lang/crates.io-index"
880 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
881 | dependencies = [
882 | "itoa",
883 | "memchr",
884 | "ryu",
885 | "serde",
886 | ]
887 |
888 | [[package]]
889 | name = "serde_spanned"
890 | version = "0.6.8"
891 | source = "registry+https://github.com/rust-lang/crates.io-index"
892 | checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
893 | dependencies = [
894 | "serde",
895 | ]
896 |
897 | [[package]]
898 | name = "shlex"
899 | version = "1.3.0"
900 | source = "registry+https://github.com/rust-lang/crates.io-index"
901 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
902 |
903 | [[package]]
904 | name = "signal-hook"
905 | version = "0.3.17"
906 | source = "registry+https://github.com/rust-lang/crates.io-index"
907 | checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801"
908 | dependencies = [
909 | "libc",
910 | "signal-hook-registry",
911 | ]
912 |
913 | [[package]]
914 | name = "signal-hook-registry"
915 | version = "1.4.2"
916 | source = "registry+https://github.com/rust-lang/crates.io-index"
917 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
918 | dependencies = [
919 | "libc",
920 | ]
921 |
922 | [[package]]
923 | name = "slab"
924 | version = "0.4.9"
925 | source = "registry+https://github.com/rust-lang/crates.io-index"
926 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
927 | dependencies = [
928 | "autocfg",
929 | ]
930 |
931 | [[package]]
932 | name = "smol_str"
933 | version = "0.1.16"
934 | source = "registry+https://github.com/rust-lang/crates.io-index"
935 | checksum = "2f7909a1d8bc166a862124d84fdc11bda0ea4ed3157ccca662296919c2972db1"
936 |
937 | [[package]]
938 | name = "strsim"
939 | version = "0.11.1"
940 | source = "registry+https://github.com/rust-lang/crates.io-index"
941 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
942 |
943 | [[package]]
944 | name = "syn"
945 | version = "1.0.109"
946 | source = "registry+https://github.com/rust-lang/crates.io-index"
947 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
948 | dependencies = [
949 | "proc-macro2",
950 | "quote",
951 | "unicode-ident",
952 | ]
953 |
954 | [[package]]
955 | name = "syn"
956 | version = "2.0.100"
957 | source = "registry+https://github.com/rust-lang/crates.io-index"
958 | checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
959 | dependencies = [
960 | "proc-macro2",
961 | "quote",
962 | "unicode-ident",
963 | ]
964 |
965 | [[package]]
966 | name = "terminal_size"
967 | version = "0.4.2"
968 | source = "registry+https://github.com/rust-lang/crates.io-index"
969 | checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed"
970 | dependencies = [
971 | "rustix",
972 | "windows-sys 0.59.0",
973 | ]
974 |
975 | [[package]]
976 | name = "text_unit"
977 | version = "0.1.10"
978 | source = "registry+https://github.com/rust-lang/crates.io-index"
979 | checksum = "20431e104bfecc1a40872578dbc390e10290a0e9c35fffe3ce6f73c15a9dbfc2"
980 |
981 | [[package]]
982 | name = "thin-dst"
983 | version = "1.1.0"
984 | source = "registry+https://github.com/rust-lang/crates.io-index"
985 | checksum = "db3c46be180f1af9673ebb27bc1235396f61ef6965b3fe0dbb2e624deb604f0e"
986 |
987 | [[package]]
988 | name = "thiserror"
989 | version = "1.0.69"
990 | source = "registry+https://github.com/rust-lang/crates.io-index"
991 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
992 | dependencies = [
993 | "thiserror-impl 1.0.69",
994 | ]
995 |
996 | [[package]]
997 | name = "thiserror"
998 | version = "2.0.12"
999 | source = "registry+https://github.com/rust-lang/crates.io-index"
1000 | checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
1001 | dependencies = [
1002 | "thiserror-impl 2.0.12",
1003 | ]
1004 |
1005 | [[package]]
1006 | name = "thiserror-impl"
1007 | version = "1.0.69"
1008 | source = "registry+https://github.com/rust-lang/crates.io-index"
1009 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
1010 | dependencies = [
1011 | "proc-macro2",
1012 | "quote",
1013 | "syn 2.0.100",
1014 | ]
1015 |
1016 | [[package]]
1017 | name = "thiserror-impl"
1018 | version = "2.0.12"
1019 | source = "registry+https://github.com/rust-lang/crates.io-index"
1020 | checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
1021 | dependencies = [
1022 | "proc-macro2",
1023 | "quote",
1024 | "syn 2.0.100",
1025 | ]
1026 |
1027 | [[package]]
1028 | name = "tokio"
1029 | version = "1.44.1"
1030 | source = "registry+https://github.com/rust-lang/crates.io-index"
1031 | checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a"
1032 | dependencies = [
1033 | "backtrace",
1034 | "bytes",
1035 | "libc",
1036 | "mio",
1037 | "pin-project-lite",
1038 | "signal-hook-registry",
1039 | "tokio-macros",
1040 | "windows-sys 0.52.0",
1041 | ]
1042 |
1043 | [[package]]
1044 | name = "tokio-macros"
1045 | version = "2.5.0"
1046 | source = "registry+https://github.com/rust-lang/crates.io-index"
1047 | checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
1048 | dependencies = [
1049 | "proc-macro2",
1050 | "quote",
1051 | "syn 2.0.100",
1052 | ]
1053 |
1054 | [[package]]
1055 | name = "toml"
1056 | version = "0.8.20"
1057 | source = "registry+https://github.com/rust-lang/crates.io-index"
1058 | checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148"
1059 | dependencies = [
1060 | "serde",
1061 | "serde_spanned",
1062 | "toml_datetime",
1063 | "toml_edit",
1064 | ]
1065 |
1066 | [[package]]
1067 | name = "toml_datetime"
1068 | version = "0.6.8"
1069 | source = "registry+https://github.com/rust-lang/crates.io-index"
1070 | checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
1071 | dependencies = [
1072 | "serde",
1073 | ]
1074 |
1075 | [[package]]
1076 | name = "toml_edit"
1077 | version = "0.22.24"
1078 | source = "registry+https://github.com/rust-lang/crates.io-index"
1079 | checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474"
1080 | dependencies = [
1081 | "indexmap",
1082 | "serde",
1083 | "serde_spanned",
1084 | "toml_datetime",
1085 | "winnow",
1086 | ]
1087 |
1088 | [[package]]
1089 | name = "unicode-ident"
1090 | version = "1.0.18"
1091 | source = "registry+https://github.com/rust-lang/crates.io-index"
1092 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
1093 |
1094 | [[package]]
1095 | name = "utf8parse"
1096 | version = "0.2.2"
1097 | source = "registry+https://github.com/rust-lang/crates.io-index"
1098 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
1099 |
1100 | [[package]]
1101 | name = "version_check"
1102 | version = "0.9.5"
1103 | source = "registry+https://github.com/rust-lang/crates.io-index"
1104 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
1105 |
1106 | [[package]]
1107 | name = "walkdir"
1108 | version = "2.5.0"
1109 | source = "registry+https://github.com/rust-lang/crates.io-index"
1110 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
1111 | dependencies = [
1112 | "same-file",
1113 | "winapi-util",
1114 | ]
1115 |
1116 | [[package]]
1117 | name = "wasi"
1118 | version = "0.11.0+wasi-snapshot-preview1"
1119 | source = "registry+https://github.com/rust-lang/crates.io-index"
1120 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
1121 |
1122 | [[package]]
1123 | name = "wasite"
1124 | version = "0.1.0"
1125 | source = "registry+https://github.com/rust-lang/crates.io-index"
1126 | checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
1127 |
1128 | [[package]]
1129 | name = "wasm-bindgen"
1130 | version = "0.2.100"
1131 | source = "registry+https://github.com/rust-lang/crates.io-index"
1132 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
1133 | dependencies = [
1134 | "cfg-if",
1135 | "once_cell",
1136 | "rustversion",
1137 | "wasm-bindgen-macro",
1138 | ]
1139 |
1140 | [[package]]
1141 | name = "wasm-bindgen-backend"
1142 | version = "0.2.100"
1143 | source = "registry+https://github.com/rust-lang/crates.io-index"
1144 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
1145 | dependencies = [
1146 | "bumpalo",
1147 | "log",
1148 | "proc-macro2",
1149 | "quote",
1150 | "syn 2.0.100",
1151 | "wasm-bindgen-shared",
1152 | ]
1153 |
1154 | [[package]]
1155 | name = "wasm-bindgen-macro"
1156 | version = "0.2.100"
1157 | source = "registry+https://github.com/rust-lang/crates.io-index"
1158 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
1159 | dependencies = [
1160 | "quote",
1161 | "wasm-bindgen-macro-support",
1162 | ]
1163 |
1164 | [[package]]
1165 | name = "wasm-bindgen-macro-support"
1166 | version = "0.2.100"
1167 | source = "registry+https://github.com/rust-lang/crates.io-index"
1168 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
1169 | dependencies = [
1170 | "proc-macro2",
1171 | "quote",
1172 | "syn 2.0.100",
1173 | "wasm-bindgen-backend",
1174 | "wasm-bindgen-shared",
1175 | ]
1176 |
1177 | [[package]]
1178 | name = "wasm-bindgen-shared"
1179 | version = "0.2.100"
1180 | source = "registry+https://github.com/rust-lang/crates.io-index"
1181 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
1182 | dependencies = [
1183 | "unicode-ident",
1184 | ]
1185 |
1186 | [[package]]
1187 | name = "web-sys"
1188 | version = "0.3.77"
1189 | source = "registry+https://github.com/rust-lang/crates.io-index"
1190 | checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
1191 | dependencies = [
1192 | "js-sys",
1193 | "wasm-bindgen",
1194 | ]
1195 |
1196 | [[package]]
1197 | name = "whoami"
1198 | version = "1.6.0"
1199 | source = "registry+https://github.com/rust-lang/crates.io-index"
1200 | checksum = "6994d13118ab492c3c80c1f81928718159254c53c472bf9ce36f8dae4add02a7"
1201 | dependencies = [
1202 | "redox_syscall",
1203 | "wasite",
1204 | "web-sys",
1205 | ]
1206 |
1207 | [[package]]
1208 | name = "winapi"
1209 | version = "0.3.9"
1210 | source = "registry+https://github.com/rust-lang/crates.io-index"
1211 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
1212 | dependencies = [
1213 | "winapi-i686-pc-windows-gnu",
1214 | "winapi-x86_64-pc-windows-gnu",
1215 | ]
1216 |
1217 | [[package]]
1218 | name = "winapi-i686-pc-windows-gnu"
1219 | version = "0.4.0"
1220 | source = "registry+https://github.com/rust-lang/crates.io-index"
1221 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
1222 |
1223 | [[package]]
1224 | name = "winapi-util"
1225 | version = "0.1.9"
1226 | source = "registry+https://github.com/rust-lang/crates.io-index"
1227 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
1228 | dependencies = [
1229 | "windows-sys 0.59.0",
1230 | ]
1231 |
1232 | [[package]]
1233 | name = "winapi-x86_64-pc-windows-gnu"
1234 | version = "0.4.0"
1235 | source = "registry+https://github.com/rust-lang/crates.io-index"
1236 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
1237 |
1238 | [[package]]
1239 | name = "windows-core"
1240 | version = "0.52.0"
1241 | source = "registry+https://github.com/rust-lang/crates.io-index"
1242 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
1243 | dependencies = [
1244 | "windows-targets 0.52.6",
1245 | ]
1246 |
1247 | [[package]]
1248 | name = "windows-link"
1249 | version = "0.1.1"
1250 | source = "registry+https://github.com/rust-lang/crates.io-index"
1251 | checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
1252 |
1253 | [[package]]
1254 | name = "windows-sys"
1255 | version = "0.48.0"
1256 | source = "registry+https://github.com/rust-lang/crates.io-index"
1257 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
1258 | dependencies = [
1259 | "windows-targets 0.48.5",
1260 | ]
1261 |
1262 | [[package]]
1263 | name = "windows-sys"
1264 | version = "0.52.0"
1265 | source = "registry+https://github.com/rust-lang/crates.io-index"
1266 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
1267 | dependencies = [
1268 | "windows-targets 0.52.6",
1269 | ]
1270 |
1271 | [[package]]
1272 | name = "windows-sys"
1273 | version = "0.59.0"
1274 | source = "registry+https://github.com/rust-lang/crates.io-index"
1275 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
1276 | dependencies = [
1277 | "windows-targets 0.52.6",
1278 | ]
1279 |
1280 | [[package]]
1281 | name = "windows-targets"
1282 | version = "0.48.5"
1283 | source = "registry+https://github.com/rust-lang/crates.io-index"
1284 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
1285 | dependencies = [
1286 | "windows_aarch64_gnullvm 0.48.5",
1287 | "windows_aarch64_msvc 0.48.5",
1288 | "windows_i686_gnu 0.48.5",
1289 | "windows_i686_msvc 0.48.5",
1290 | "windows_x86_64_gnu 0.48.5",
1291 | "windows_x86_64_gnullvm 0.48.5",
1292 | "windows_x86_64_msvc 0.48.5",
1293 | ]
1294 |
1295 | [[package]]
1296 | name = "windows-targets"
1297 | version = "0.52.6"
1298 | source = "registry+https://github.com/rust-lang/crates.io-index"
1299 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
1300 | dependencies = [
1301 | "windows_aarch64_gnullvm 0.52.6",
1302 | "windows_aarch64_msvc 0.52.6",
1303 | "windows_i686_gnu 0.52.6",
1304 | "windows_i686_gnullvm",
1305 | "windows_i686_msvc 0.52.6",
1306 | "windows_x86_64_gnu 0.52.6",
1307 | "windows_x86_64_gnullvm 0.52.6",
1308 | "windows_x86_64_msvc 0.52.6",
1309 | ]
1310 |
1311 | [[package]]
1312 | name = "windows_aarch64_gnullvm"
1313 | version = "0.48.5"
1314 | source = "registry+https://github.com/rust-lang/crates.io-index"
1315 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
1316 |
1317 | [[package]]
1318 | name = "windows_aarch64_gnullvm"
1319 | version = "0.52.6"
1320 | source = "registry+https://github.com/rust-lang/crates.io-index"
1321 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
1322 |
1323 | [[package]]
1324 | name = "windows_aarch64_msvc"
1325 | version = "0.48.5"
1326 | source = "registry+https://github.com/rust-lang/crates.io-index"
1327 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
1328 |
1329 | [[package]]
1330 | name = "windows_aarch64_msvc"
1331 | version = "0.52.6"
1332 | source = "registry+https://github.com/rust-lang/crates.io-index"
1333 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
1334 |
1335 | [[package]]
1336 | name = "windows_i686_gnu"
1337 | version = "0.48.5"
1338 | source = "registry+https://github.com/rust-lang/crates.io-index"
1339 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
1340 |
1341 | [[package]]
1342 | name = "windows_i686_gnu"
1343 | version = "0.52.6"
1344 | source = "registry+https://github.com/rust-lang/crates.io-index"
1345 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
1346 |
1347 | [[package]]
1348 | name = "windows_i686_gnullvm"
1349 | version = "0.52.6"
1350 | source = "registry+https://github.com/rust-lang/crates.io-index"
1351 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
1352 |
1353 | [[package]]
1354 | name = "windows_i686_msvc"
1355 | version = "0.48.5"
1356 | source = "registry+https://github.com/rust-lang/crates.io-index"
1357 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
1358 |
1359 | [[package]]
1360 | name = "windows_i686_msvc"
1361 | version = "0.52.6"
1362 | source = "registry+https://github.com/rust-lang/crates.io-index"
1363 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
1364 |
1365 | [[package]]
1366 | name = "windows_x86_64_gnu"
1367 | version = "0.48.5"
1368 | source = "registry+https://github.com/rust-lang/crates.io-index"
1369 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
1370 |
1371 | [[package]]
1372 | name = "windows_x86_64_gnu"
1373 | version = "0.52.6"
1374 | source = "registry+https://github.com/rust-lang/crates.io-index"
1375 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
1376 |
1377 | [[package]]
1378 | name = "windows_x86_64_gnullvm"
1379 | version = "0.48.5"
1380 | source = "registry+https://github.com/rust-lang/crates.io-index"
1381 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
1382 |
1383 | [[package]]
1384 | name = "windows_x86_64_gnullvm"
1385 | version = "0.52.6"
1386 | source = "registry+https://github.com/rust-lang/crates.io-index"
1387 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
1388 |
1389 | [[package]]
1390 | name = "windows_x86_64_msvc"
1391 | version = "0.48.5"
1392 | source = "registry+https://github.com/rust-lang/crates.io-index"
1393 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
1394 |
1395 | [[package]]
1396 | name = "windows_x86_64_msvc"
1397 | version = "0.52.6"
1398 | source = "registry+https://github.com/rust-lang/crates.io-index"
1399 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
1400 |
1401 | [[package]]
1402 | name = "winnow"
1403 | version = "0.7.4"
1404 | source = "registry+https://github.com/rust-lang/crates.io-index"
1405 | checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36"
1406 | dependencies = [
1407 | "memchr",
1408 | ]
1409 |
1410 | [[package]]
1411 | name = "yansi"
1412 | version = "0.5.1"
1413 | source = "registry+https://github.com/rust-lang/crates.io-index"
1414 | checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
1415 |
1416 | [[package]]
1417 | name = "yn"
1418 | version = "0.1.1"
1419 | source = "registry+https://github.com/rust-lang/crates.io-index"
1420 | checksum = "d789b24a50ca067124e6e6ad3061c48151da174043cb09285ba934425e8739ec"
1421 |
--------------------------------------------------------------------------------