This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

tools.deploy: build and switch on the target in systemd units

deploy now always takes a machine and a target host. It extracts the
bulk key once, runs clan vars generate, copies the flake source to
/var/lib/deploy/incoming over ssh, builds it there in the transient unit
deploy-build (progress via nom), shows the source diff against the
running system's source and the closure diff, asks, uploads secrets and
switches in deploy-switch. Both units survive the ssh connection dying;
rerunning deploy attaches to a running one.

+203 -46
-24
tools/deploy/deploy.usage.kdl
··· 1 - bin "deploy" 2 - about "Deploy machines via clan, with passage sync and bulk key management" 3 - 4 - arg "<machine>" help="Machine(s) to deploy" var=#true 5 - 6 - flag "--tags" help="Tags to query machines for" { 7 - arg "<tags>" 8 - var=#true 9 - } 10 - flag "--target-host" help="Address of the machine to update (user@host:1234)" { 11 - arg "<target_host>" 12 - } 13 - flag "--build-host" help="Machine to build the configuration on" { 14 - arg "<build_host>" 15 - } 16 - flag "--host-key-check" help="SSH host key check mode" { 17 - arg "<mode>" 18 - choices "strict" "ask" "accept-new" "tofu" "none" 19 - } 20 - flag "--upload-inputs" help="Upload all flake inputs from local machine" 21 - flag "--debug" help="Enable debug logging" 22 - flag "--flake" help="Path to the flake" { 23 - arg "<path>" 24 - }
+195 -22
tools/deploy/flake-module.nix
··· 7 7 ... 8 8 }: 9 9 { 10 - packages.deploy = pkgs.writeShellApplication { 11 - name = "deploy"; 12 - runtimeInputs = [ 13 - self.packages.${system}.pass 14 - inputs.clan-core.packages.${system}.clan-cli 15 - ]; 16 - text = '' 17 - set -x 18 - DEPLOY_TMPDIR=$(mktemp -d) 19 - chmod 700 "$DEPLOY_TMPDIR" 20 - trap 'rm -rf "$DEPLOY_TMPDIR"' EXIT 10 + # deploy [--flake PATH] <machine> <[user@]host> 11 + # 12 + # Generates vars, copies the flake source to /var/lib/deploy/incoming 13 + # on the target, builds it there in the systemd unit deploy-build 14 + # (progress via nom), shows the source diff against the running system 15 + # (/var/lib/deploy/src) and the closure diff, asks, then uploads 16 + # secrets and switches in the unit deploy-switch. Both units survive 17 + # the ssh connection dying; rerunning deploy attaches to a running one. 18 + packages.deploy = 19 + (pkgs.writeShellApplication { 20 + name = "deploy"; 21 + runtimeInputs = [ 22 + self.packages.${system}.pass 23 + inputs.clan-core.packages.${system}.clan-cli 24 + pkgs.gnutar 25 + pkgs.jq 26 + pkgs.less 27 + pkgs.nix-output-monitor 28 + ]; 29 + text = '' 30 + usage() { 31 + echo "usage: deploy [--flake PATH] <machine> <[user@]host>" >&2 32 + exit 1 33 + } 21 34 22 - # Set bulk key path so the pass wrapper can lazily extract it 23 - # on the first actual decryption (if any secrets need uploading). 24 - export PASS_BULK_KEY_FILE="$DEPLOY_TMPDIR/bulk-key" 35 + flake=. 36 + while [ $# -gt 0 ]; do 37 + case $1 in 38 + --flake) 39 + [ $# -ge 2 ] || usage 40 + flake=$2 41 + shift 2 42 + ;; 43 + -*) usage ;; 44 + *) break ;; 45 + esac 46 + done 47 + [ $# -eq 2 ] || usage 48 + machine=$1 49 + target=$2 50 + [[ $target == *@* ]] || target=root@$target 25 51 26 - # Sync password store before deploy 27 - pass git pull --rebase 52 + state=/var/lib/deploy 53 + nix="/run/current-system/sw/bin/nix --extra-experimental-features 'nix-command flakes'" 28 54 29 - # Deploy 30 - clan machines update --debug "$@" 55 + tmp=$(mktemp -d) 56 + chmod 700 "$tmp" 57 + # One ssh connection for the whole run, so the key signs only once. 58 + ssh_opts=(-o ControlMaster=auto -o "ControlPath=$tmp/ssh" -o ControlPersist=yes) 59 + cleanup() { 60 + ssh "''${ssh_opts[@]}" -O exit "$target" 2>/dev/null || true 61 + rm -rf "$tmp" 62 + } 63 + trap cleanup EXIT 31 64 32 - # Push any secret changes after deploy 33 - pass git push 34 - ''; 35 - }; 65 + remote() { 66 + # shellcheck disable=SC2029 # the command line is meant for the target 67 + ssh "''${ssh_opts[@]}" "$target" "$@" 68 + } 69 + step() { 70 + printf '\n\033[1m== %s\033[0m\n' "$*" >&2 71 + } 72 + page() { 73 + if [ -t 1 ]; then ''${PAGER:-less -RFX}; else cat; fi 74 + } 75 + 76 + # True while the unit's ExecStartPre or ExecStart is running. 77 + # Finished units stay around (RemainAfterExit) until the next start, 78 + # so their result can still be read after reconnecting. 79 + in_progress() { 80 + case $(remote systemctl show -p ActiveState -p SubState "$1") in 81 + *ActiveState=activating* | *SubState=running*) return 0 ;; 82 + esac 83 + return 1 84 + } 85 + 86 + # start <unit> <systemd-run args...>: the arguments are quoted, so 87 + # they reach systemd-run on the target unchanged. Units start with 88 + # an empty environment; go through the daemon like a login shell 89 + # (the store is read-only inside containers). 90 + start() { 91 + local unit=$1 92 + shift 93 + remote "systemctl stop $unit 2>/dev/null; systemctl reset-failed $unit 2>/dev/null;" \ 94 + "systemd-run --quiet --unit=$unit -p RemainAfterExit=yes" \ 95 + "-p StandardOutput=truncate:$state/$unit.log" \ 96 + "--setenv=NIX_REMOTE=daemon --setenv=PATH=/run/wrappers/bin:/run/current-system/sw/bin" \ 97 + "$(printf ' %q' "$@")" 98 + } 99 + 100 + # follow <unit>: stream its log until it finishes, fail if it did. 101 + follow() { 102 + ssh "''${ssh_opts[@]}" "$target" bash -s -- "$1" "$state" <<'EOF' 103 + unit=$1 log=$2/$1.log 104 + tail -n +1 -F "$log" 2>/dev/null & 105 + tailpid=$! 106 + while case $(systemctl show -p ActiveState -p SubState "$unit") in 107 + *ActiveState=activating* | *SubState=running*) true ;; 108 + *) false ;; 109 + esac; do 110 + sleep 1 111 + done 112 + sleep 1 113 + kill "$tailpid" 114 + [ "$(systemctl show -p Result --value "$unit")" = success ] && exit 0 115 + echo "deploy: $unit failed (exit status $(systemctl show -p ExecMainStatus --value "$unit")), log: $log" >&2 116 + exit 1 117 + EOF 118 + } 119 + 120 + step "connecting to $target" 121 + # shellcheck disable=SC2016 # expanded on the target 122 + remote 'test -e /etc/NIXOS && command -v systemd-run >/dev/null' || 123 + { echo "deploy: $target is not a NixOS system with systemd" >&2; exit 1; } 124 + if in_progress deploy-switch; then 125 + step "a switch is still running on $target, attaching" 126 + follow deploy-switch 127 + exit 128 + fi 129 + 130 + # Decrypt the bulk key once (one Secure Enclave unlock); every 131 + # later pass call from clan then uses it without prompting. 132 + PASS_BULK_KEY_FILE="" pass show bulk-operations/age-key >"$tmp/bulk-key" 133 + if [ ! -s "$tmp/bulk-key" ]; then 134 + echo "deploy: could not extract the bulk key" >&2 135 + exit 1 136 + fi 137 + export PASS_BULK_KEY_FILE="$tmp/bulk-key" 138 + 139 + if in_progress deploy-build; then 140 + step "a build is still running on $target, attaching" 141 + else 142 + step "generating vars for $machine" 143 + pass git pull --rebase 144 + clan vars generate --flake "$flake" "$machine" 145 + pass git push 146 + 147 + step "copying source to $target:$state/incoming" 148 + # The flake source exactly as nix sees it: tracked files only. 149 + src=$(nix flake metadata --json "$flake" | jq -r .path) 150 + tar -C "$src" --owner=0 --group=0 --numeric-owner -czf - . | 151 + remote "rm -rf $state/incoming.new && mkdir -p $state/incoming.new &&" \ 152 + "tar -C $state/incoming.new -xzf - &&" \ 153 + "rm -rf $state/incoming && mv $state/incoming.new $state/incoming" 154 + 155 + step "source changes" 156 + # shellcheck disable=SC2016 # expanded on the target 157 + remote "if [ -d $state/src ]; then diff -ruN --color=always $state/src $state/incoming" \ 158 + '|| [ $? -eq 1 ]; else echo "no source recorded for the running system"; fi' | page 159 + 160 + step "building $machine on $target" 161 + start deploy-build \ 162 + /run/current-system/sw/bin/nix --extra-experimental-features "nix-command flakes" \ 163 + --log-format internal-json -v build --out-link "$state/result" \ 164 + "$state/incoming#nixosConfigurations.$machine.config.system.build.toplevel" 165 + fi 166 + follow deploy-build | nom --json 167 + system=$(remote readlink -f "$state/result") 168 + 169 + step "closure changes" 170 + if [ "$(remote readlink -f /run/current-system)" = "$system" ]; then 171 + echo "$system is already running" 172 + else 173 + remote "$nix store diff-closures /run/current-system $system" 174 + fi 175 + 176 + read -r -p "switch $target to $system? [y/N] " answer </dev/tty 177 + if [[ $answer != [yY]* ]]; then 178 + echo "aborted, $target is unchanged" >&2 179 + exit 1 180 + fi 181 + 182 + step "uploading secrets" 183 + secret_location=$(remote "$nix eval --raw $state/incoming#nixosConfigurations.$machine.config.clan.core.vars.password-store.secretLocation") 184 + clan vars upload --flake "$flake" --directory "$tmp/vars" "$machine" 185 + if [ -s "$tmp/vars/.pass_info" ] && 186 + [ "$(remote cat "$secret_location/.pass_info" 2>/dev/null)" = "$(cat "$tmp/vars/.pass_info")" ]; then 187 + echo "secrets in $secret_location unchanged" 188 + else 189 + # Same layout and permissions as `clan vars upload`; swapped in 190 + # with a rename so the directory is never empty. 191 + find "$tmp/vars" -type d -exec chmod 700 {} + 192 + find "$tmp/vars" -type f -exec chmod 400 {} + 193 + # shellcheck disable=SC2016 # expanded on the target 194 + tar -C "$tmp/vars" --owner=0 --group=0 --numeric-owner -czf - . | 195 + remote "d=$secret_location;" \ 196 + 'rm -rf "$d.new" "$d.old" && install -d -m 700 "$d.new" && tar -C "$d.new" -xzf - &&' \ 197 + '{ [ ! -e "$d" ] || mv "$d" "$d.old"; } && mv "$d.new" "$d" && rm -rf "$d.old"' 198 + fi 199 + 200 + step "switching $target" 201 + start deploy-switch \ 202 + -p "ExecStartPre=/run/current-system/sw/bin/nix-env -p /nix/var/nix/profiles/system --set $system" \ 203 + -p "ExecStartPre=/bin/sh -c 'rm -rf $state/src && cp -a $state/incoming $state/src'" \ 204 + "$system/bin/switch-to-configuration" switch 205 + follow deploy-switch 206 + ''; 207 + }).overrideAttrs 208 + { passthru.usage = builtins.readFile ./usage.kdl; }; 36 209 }; 37 210 }
+8
tools/deploy/usage.kdl
··· 1 + bin "deploy" 2 + about "Build a machine on its target host in a systemd unit, review source and closure diffs, then switch" 3 + 4 + flag "--flake" help="Path to the flake (default: .)" { 5 + arg "<path>" 6 + } 7 + arg "<machine>" help="Machine configuration to deploy" 8 + arg "<target_host>" help="SSH destination to deploy to, [user@]host (user defaults to root)"