scripts/deploy: port from bash to ysh

This commit is contained in:
2025-05-15 06:47:01 +00:00
parent 471341447e
commit 56b8d82c42

View File

@@ -1,9 +1,9 @@
#!/usr/bin/env nix-shell #!/usr/bin/env nix-shell
#!nix-shell -i bash -p nettools #!nix-shell -i ysh -p nettools -p oils-for-unix
SELF=$(hostname) var SELF = $(hostname)
usage() { proc usage {
echo "deploy: deploy a nix config to a remote machine, possibly activating it" echo "deploy: deploy a nix config to a remote machine, possibly activating it"
echo "" echo ""
echo "usage: deploy [options] [host] [host2 ...]" echo "usage: deploy [options] [host] [host2 ...]"
@@ -13,7 +13,9 @@ usage() {
echo "- --switch (default)" echo "- --switch (default)"
echo "- --test: switch to the new configuration, but do not make it bootable" echo "- --test: switch to the new configuration, but do not make it bootable"
echo "- --dry-run: show what would be done without actually doing it" echo "- --dry-run: show what would be done without actually doing it"
echo "- --pre: alias for --action copy --variant all all" echo "- --pre: alias for --action copy ..."
echo ' if otherwise unspecified, implies `--variant all`'
echo " if no host is specified, copies to all known hosts"
echo "- --reboot: reboot the target machine after deploying (if deployed with no errors)" echo "- --reboot: reboot the target machine after deploying (if deployed with no errors)"
echo "- --reboot-force: reboot the target machine after deploying (even on error)" echo "- --reboot-force: reboot the target machine after deploying (even on error)"
echo "- --variant light|min|''|all (default: '')" echo "- --variant light|min|''|all (default: '')"
@@ -29,192 +31,195 @@ usage() {
exit 1 exit 1
} }
info() { proc info (...stmts) {
echo "[deploy]" "$@" echo "[deploy]" @stmts
} }
action=switch var action = "switch"
hosts=() var hosts = []
ip= var ip = null
defaultHost="$SELF" var defaultHost = SELF
variants=() var variants = []
defaultVariant= var defaultVariant = ""
# by default, don't ship builds to servo. i'd guess this can be overriden by passing --builders servo # by default, don't ship builds to servo. i'd guess this can be overriden by passing --builders servo
nixArgs=(--builders "") var nixArgs = ["--builders", ""]
doReboot= var doReboot = false
doRebootForce= var doRebootForce = false
dryRun= var dryRun = false
wireguard=opportunistic var wireguard = "opportunistic"
storePath= var storePath = null
addHost() { proc addHost (; host) {
if [ "$1" = all ]; then if (host === "all" ) {
# order matters: # order matters:
hosts+=(moby lappy desko servo) call hosts->extend(["moby", "lappy", "desko", "servo"])
else } else {
hosts+=("$1") call hosts->append(host)
fi }
} }
addVariant() { proc addVariant (; v) {
if [ "$1" = all ]; then if (v === "all") {
variants+=("-min" "-light" "" "-next-min" "-next-light" "-next") call variants->extend(["-min", "-light", "", "-next-min", "-next-light", "-next"])
elif [ -n "$1" ]; then } elif (v === "") {
variants+=("-$1")
else
# "full" variant # "full" variant
variants+=("") call variants->append("")
fi } else {
call variants->append("-$v")
}
} }
parseArgs() { proc parseArgs(;cur=null, next=null, ...rest) {
while [ "$#" -ne 0 ]; do if (cur === null) {
local arg=$1 # end
shift :
case "$arg" in } else {
(--build|--copy|--switch|--test) case (cur) {
action=${arg/--/} --build | --copy | --switch | --test {
;; setglobal action = cur
(--deriv) parseArgs (next, ...rest)
storePath="$1" }
shift --deriv {
;; setglobal storePath = next
(--dry-run) parseArgs (...rest)
dryRun=1 }
;; --dry-run {
(--help) setglobal dryRun = true
parseArgs (next, ...rest)
}
--help {
usage usage
;; parseArgs (next, ...rest)
(--ip) }
ip="$1" --ip {
shift setglobal ip = next
;; parseArgs (...rest)
(--pre) }
action=copy --pre {
defaultVariant=all setglobal action = "copy"
defaultHost=all setglobal defaultVariant = "all"
;; setglobal defaultHost = "all"
(--reboot) parseArgs (next, ...rest)
doReboot=1 }
;; --reboot {
(--reboot-force|--force-reboot) setglobal doReboot = true
doReboot=1 parseArgs (next, ...rest)
doRebootForce=1 }
;; --reboot-force | --force-reboot {
(--variant) setglobal doReboot = true
addVariant "$1" setglobal doRebootForce = true
shift parseArgs (next, ...rest)
;; }
(--wireguard) --variant {
wireguard="$1" addVariant (next)
shift parseArgs (...rest)
;; }
(all|crappy|desko|lappy|moby|servo) --wireguard {
addHost "$arg" setglobal wireguard = next
;; parseArgs (...rest)
(*) }
nixArgs+=("$arg") all | crappy | desko | lappy | moby | servo {
;; addHost (cur)
esac parseArgs (next, ...rest)
done }
(else) {
call nixArgs->append(cur)
parseArgs (next, ...rest)
}
}
}
if [ "${#hosts[@]}" -eq 0 ] && [ -n "$defaultHost" ]; then if (hosts === [] and defaultHost) {
addHost "$defaultHost" addHost (defaultHost)
fi }
if [ "${#variants[@]}" -eq 0 ]; then if (variants === []) {
addVariant "$defaultVariant" addVariant (defaultVariant)
fi }
} }
destructive() { proc destructive (...cmd) {
if [ -z "$dryRun" ]; then if (dryRun) {
if [ -n "$ECHO_CMD" ]; then echo "dry-run:" @cmd
echo "$@" } else {
fi if (get(ENV, "ECHO_CMD")) {
"$@" echo @cmd
else }
echo "dry-run: $@" @cmd
fi }
} }
# return "$1" or "$1-hn", based on if wireguard was requested or not # return "$1" or "$1-hn", based on if wireguard was requested or not
resolveHost() { func resolveHost (host) {
if [ -n "$ip" ]; then if (ip) {
echo "$ip" return (ip)
else } else {
case "$wireguard-$1" in case ("$wireguard-$host") {
(opportunistic-moby) opportunistic-moby {
echo "$1-hn" return ("$host-hn")
;; }
(opportunistic-*) opportunistic-* {
echo "$1" return (host)
;; }
(never-*) never-* {
echo "$1" return (host)
;; }
(always-*) always-* {
echo "$1-hn" return ("$host-hn")
;; }
(*) (else) {
echo "$1-hn" return ("$host-hn")
;; }
esac }
fi }
} }
# return the number of seconds to allot to `nix copy` when copying the given variant. # return the number of seconds to allot to `nix copy` when copying the given variant.
# this is done to avoid stalled copies from blocking the entire process, while hopefully not breaking the copies that are actually important # this is done to avoid stalled copies from blocking the entire process, while hopefully not breaking the copies that are actually important
timeoutFor() { func timeoutFor (variant) {
case $1 in case (variant) {
(-min|-light|-next) -min | -light | -next {
echo 3600 return (3600)
;; }
(-next-min|-next-light) -next-min | -next-light {
echo 1800 return (1800)
;; }
(*) (else) {
# this catches the normal variant # this catches the normal variant
echo 14400 return (14400)
;; }
esac }
} }
runOnTarget() { proc runOnTarget (host, ...cmd) {
local host="$1" # run the command on the machine we're deploying to.
shift
# run the command ($@) on the machine we're deploying to.
# if that's a remote machine, then do it via ssh, else local shell. # if that's a remote machine, then do it via ssh, else local shell.
if [ -n "$host" ] && [ "$host" != "$SELF" ]; then if (host !== "" and host !== SELF) {
info "running on remote ($host):" "$@" info "running on remote ($host):" @cmd
ssh "$host" "$@" ssh "$host" @cmd
else } else {
info "running locally ($SELF):" "$@" info "running locally ($SELF):" @cmd
"$@" @cmd
fi }
} }
# deployOneHost $host $variant proc deployOneHost (; host, variant) {
deployOneHost() { var timeout = timeoutFor(variant)
local host="$1"
local variant="$2"
local timeout=$(timeoutFor "$variant")
# storePath is allowed to be either a realized derivation, # storePath is allowed to be either a realized derivation,
# or the path to a .drv file itself # or the path to a .drv file itself
local myStorePath="$storePath" var myStorePath = storePath
if [ -z "$myStorePath" ]; then if (not myStorePath) {
# `nix-build -A foo` evals and then realizes foo, but it never unloads the memory used to eval foo. # `nix-build -A foo` evals and then realizes foo, but it never unloads the memory used to eval foo.
# my exprs are heavyweight, we need that memory for building, so do the evals separately from the realizations: # my exprs are heavyweight, we need that memory for building, so do the evals separately from the realizations:
info "evaluating $host$variant..." info "evaluating $host$variant..."
myStorePath=$(nix eval --raw -f . "hosts.$host$variant.toplevel.drvPath") setvar myStorePath = $(nix eval --raw -f . "hosts.$host$variant.toplevel.drvPath")
fi }
if [[ "$myStorePath" == *.drv ]]; then if ("$myStorePath" => endsWith(".drv")) {
info "building $host$variant ($drvPath)" info "building $host$variant ($myStorePath)"
myStorePath=$(destructive nix-store --realize "$myStorePath" "${nixArgs[@]}") setvar myStorePath = $(destructive nix-store --realize "$myStorePath" @nixArgs)
if [ -z "$myStorePath" ]; then if (myStorePath === "") {
return 1 false
fi }
info "built $host$variant -> $myStorePath" info "built $host$variant -> $myStorePath"
fi }
# mimic `nixos-rebuild --target-host`, in effect: # mimic `nixos-rebuild --target-host`, in effect:
# - nix-copy-closure ... # - nix-copy-closure ...
@@ -225,76 +230,88 @@ deployOneHost() {
# - more introspectability and debuggability # - more introspectability and debuggability
# - sandbox friendliness (especially: `git` doesn't have to be run as root) # - sandbox friendliness (especially: `git` doesn't have to be run as root)
local netHost=$(resolveHost "$host") var netHost = resolveHost(host)
case "$action" in case (action) {
(copy|switch|test) copy | switch | test {
if [ -n "$host" ] && [ "$host" != "$SELF" ]; then if (host !== "" and host !== SELF) {
if [ -e /run/secrets/nix_signing_key ]; then if test -e /run/secrets/nix_signing_key {
info "signing store paths ..." info "signing store paths ..."
destructive sudo nix store sign -r -k /run/secrets/nix_signing_key "$myStorePath" destructive sudo nix store sign -r -k /run/secrets/nix_signing_key "$myStorePath"
else } else {
info "not signing store paths: /run/secrets/nix_signing_key does not exist" info "not signing store paths: /run/secrets/nix_signing_key does not exist"
fi }
# add more `-v` for more verbosity (up to 5). # add more `-v` for more verbosity (up to 5).
# builders-use-substitutes false: optimizes so that the remote machine doesn't try to get paths from its substituters. # builders-use-substitutes false: optimizes so that the remote machine doesn't try to get paths from its substituters.
# we already have all paths here, and the remote substitution is slow to check and SERIOUSLY flaky on moby in particular. # we already have all paths here, and the remote substitution is slow to check and SERIOUSLY flaky on moby in particular.
ECHO_CMD=1 destructive timeout "$timeout" nix copy -vv --option builders-use-substitutes false --to "ssh-ng://$netHost" "$myStorePath" || return 1 ECHO_CMD=1 destructive timeout "$timeout" nix copy -vv --option builders-use-substitutes false --to "ssh-ng://$netHost" "$myStorePath"
fi }
;; }
esac }
case (action) {
case "$action" in switch | test {
(switch|test)
info "activating profile... " info "activating profile... "
destructive runOnTarget "$netHost" sudo nix-env -p /nix/var/nix/profiles/system --set "$myStorePath" || return 1 destructive runOnTarget "$netHost" sudo nix-env -p /nix/var/nix/profiles/system --set "$myStorePath"
destructive runOnTarget "$netHost" sudo "$myStorePath/bin/switch-to-configuration" "$action" try {
local rc=$? destructive runOnTarget "$netHost" sudo "$myStorePath/bin/switch-to-configuration" "$action"
if [[ -n "$doReboot" && ("$rc" -eq 0 || -n "$doRebootForce") ]]; then }
var fail = failed
if (doReboot and (not fail or doRebootForce)) {
info "rebooting $host" info "rebooting $host"
destructive runOnTarget "$netHost" sane-reboot "$host" || return 1 destructive runOnTarget "$netHost" sane-reboot "$host" || return 1
fi }
return $rc if (fail) {
;; false
esac }
}
}
} }
failedDeploys=() var failedDeploys = []
deployHosts() { proc deployHosts(; ...hosts) {
local hosts=("$@") for v in (variants) {
for v in "${variants[@]}"; do for h in (hosts) {
for h in "${hosts[@]}"; do try {
deployOneHost "$h" "$v" || \ deployOneHost (h, v)
failedDeploys+=("$h$v") }
done if failed {
done call failedDeploys->append("$h$v")
}
}
}
} }
parseArgs "$@" proc main (...args) {
parseArgs (...args)
# i care e.g. that full moby is deployed before crappy: # i care e.g. that full moby is deployed before crappy:
earlyHosts=() var earlyHosts = []
lateHosts=() var lateHosts = []
for host in "${hosts[@]}"; do for host in (hosts) {
case $host in case (host) {
(crappy) crappy {
lateHosts+=("$host") call lateHosts->append(host)
;; }
(*) (else) {
earlyHosts+=("$host") call earlyHosts->append(host)
;; }
esac }
done }
deployHosts "${earlyHosts[@]}" deployHosts (...earlyHosts)
deployHosts "${lateHosts[@]}" deployHosts (...lateHosts)
if (failedDeploys !== []) {
echo "FAILED DEPLOYMENT:"
for d in (failedDeploys) {
echo "- $d"
}
exit 1
}
if [ "${#failedDeploys[@]}" -ne 0 ]; then
echo "FAILED DEPLOYMENT:"
for d in "${failedDeploys[@]}"; do
echo "- $d"
done
exit 1
else
echo "SUCCESS" echo "SUCCESS"
fi }
if is-main {
main @ARGV
}