├── 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 | 5 | 10 | 11 | 12 | 13 | 14 | 15 | 17 | 19 | 20 | 22 | 23 | 24 | 25 | 26 | 37 | 48 | 59 | 71 | 76 | 77 | 82 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 7 | 8 | ![deploy-rs logo](./docs/logo.svg "deploy-rs") 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 | --------------------------------------------------------------------------------