This repository has no description
0

Configure Feed

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

superconfig / tools / deploy / flake-module.nix
9.1 kB 210 lines
1{ self, inputs, ... }: 2{ 3 perSystem = 4 { 5 pkgs, 6 system, 7 ... 8 }: 9 { 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 } 34 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 51 52 state=/var/lib/deploy 53 nix="/run/current-system/sw/bin/nix --extra-experimental-features 'nix-command flakes'" 54 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 64 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; }; 209 }; 210}