devshell: Cache SSH connection settings
Cache all deploy node connection settings and invalidate them when HEAD or tracked working-tree contents change. Preserve SSH argument boundaries and support explicit refreshes.
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
watch_file devshell/{default,commands,install,vm-tasks}.nix
|
watch_file devshell/*
|
||||||
# --accept-flake-config trusts the flake's nixConfig (our Harmonia cache) non-interactively, so
|
# --accept-flake-config trusts the flake's nixConfig (our Harmonia cache) non-interactively, so
|
||||||
# direnv doesn't stall on the trust prompt.
|
# direnv doesn't stall on the trust prompt.
|
||||||
use flake . --accept-flake-config
|
use flake . --accept-flake-config
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
.direnv
|
.direnv
|
||||||
|
/.cache
|
||||||
result*
|
result*
|
||||||
/.vms/*
|
/.vms/*
|
||||||
!/.vms/.gitkeep
|
!/.vms/.gitkeep
|
||||||
|
|||||||
@@ -55,7 +55,10 @@ Common ones:
|
|||||||
reboot to cut over.
|
reboot to cut over.
|
||||||
- `ssh-machine <name> [cmd]` — SSH to a NixOS system or home-manager config by name. Resolves the
|
- `ssh-machine <name> [cmd]` — SSH to a NixOS system or home-manager config by name. Resolves the
|
||||||
target and ssh options (identity, port) from its deploy-rs node, so it needs `my.deploy.enable`
|
target and ssh options (identity, port) from its deploy-rs node, so it needs `my.deploy.enable`
|
||||||
(same gate as `deploy`). Boxes default to the `fish` login shell, so pipe multi-statement remote
|
(same gate as `deploy`). Connection settings for all nodes are cached together and invalidated
|
||||||
|
by tracked Git changes; use `ssh-machine --refresh` to force a refresh
|
||||||
|
(see `docs/deployment.md#ssh-machine`).
|
||||||
|
Boxes default to the `fish` login shell, so pipe multi-statement remote
|
||||||
scripts through `bash` (e.g. `ssh-machine <name> bash -s < script.sh`) rather than `&&`/`for`.
|
scripts through `bash` (e.g. `ssh-machine <name> bash -s < script.sh`) rather than `&&`/`for`.
|
||||||
If outbound SSH hangs at the publickey step (flaky `ssh-agent`), disable the agent for the call:
|
If outbound SSH hangs at the publickey step (flaky `ssh-agent`), disable the agent for the call:
|
||||||
`SSH_AUTH_SOCK= ssh-machine …` (or add `-o IdentityAgent=none` to a raw `ssh`).
|
`SSH_AUTH_SOCK= ssh-machine …` (or add `-o IdentityAgent=none` to a raw `ssh`).
|
||||||
@@ -207,6 +210,8 @@ in churn.
|
|||||||
- Comment where it genuinely aids understanding, but not for trivial/obvious code — match the file's
|
- Comment where it genuinely aids understanding, but not for trivial/obvious code — match the file's
|
||||||
existing (fairly sparse) comment density. When adding something general, comment its general
|
existing (fairly sparse) comment density. When adding something general, comment its general
|
||||||
purpose, not the specific change or one-off reason it was introduced for.
|
purpose, not the specific change or one-off reason it was introduced for.
|
||||||
|
- Separate logical steps in code with blank lines. Keep closely related statements together, and leave a
|
||||||
|
blank line before a comment that introduces the next step.
|
||||||
- Prefer `lib.my` helpers (`mkOpt'`, `mkBoolOpt'`, `mkDefault'`) and `lib.my.c` constants over
|
- Prefer `lib.my` helpers (`mkOpt'`, `mkBoolOpt'`, `mkDefault'`) and `lib.my.c` constants over
|
||||||
reimplementing.
|
reimplementing.
|
||||||
- New shared functionality → a module in `*/modules/` + entry in `_list.nix`, options under `my.*`.
|
- New shared functionality → a module in `*/modules/` + entry in `_list.nix`, options under `my.*`.
|
||||||
|
|||||||
+6
-14
@@ -2,6 +2,11 @@
|
|||||||
let
|
let
|
||||||
homeFlake = "$HOME/.config/home-manager/flake.nix";
|
homeFlake = "$HOME/.config/home-manager/flake.nix";
|
||||||
devKey = ".keys/dev.key";
|
devKey = ".keys/dev.key";
|
||||||
|
sshMachine = pkgs.writeShellApplication {
|
||||||
|
name = "ssh-machine";
|
||||||
|
runtimeInputs = with pkgs; [ coreutils git jq openssh ];
|
||||||
|
text = builtins.readFile ./ssh-machine.sh;
|
||||||
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
commands = [
|
commands = [
|
||||||
@@ -52,20 +57,7 @@ in
|
|||||||
name = "ssh-machine";
|
name = "ssh-machine";
|
||||||
category = "utilities";
|
category = "utilities";
|
||||||
help = "SSH to a machine by NixOS system or home-manager config name";
|
help = "SSH to a machine by NixOS system or home-manager config name";
|
||||||
command = ''
|
command = ''exec ${sshMachine}/bin/ssh-machine "$@"'';
|
||||||
name="$1"
|
|
||||||
shift
|
|
||||||
# Run from the project root so deploy's relative identity path (.keys/deploy.key) resolves
|
|
||||||
cd "$PRJ_ROOT"
|
|
||||||
# deploy-rs node names are the system/home name, with `@` mangled to `-at-`
|
|
||||||
node="''${name//@/-at-}"
|
|
||||||
# Single eval: resolve `user@host` plus merged (global + node) deploy-rs ssh options
|
|
||||||
info="$(nix eval --raw .#deploy --apply 'd: let n = d.nodes."'"$node"'"; in "''${n.sshUser}@''${n.hostname} ''${builtins.concatStringsSep " " (d.sshOpts ++ n.sshOpts)}"')"
|
|
||||||
target="''${info%% *}"
|
|
||||||
opts="''${info#* }"
|
|
||||||
# shellcheck disable=SC2086
|
|
||||||
exec ${pkgs.openssh}/bin/ssh $opts "$target" "$@"
|
|
||||||
'';
|
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
name = "json2nix";
|
name = "json2nix";
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
refresh=false
|
||||||
|
|
||||||
|
if [[ ${1:-} == --refresh ]]; then
|
||||||
|
refresh=true
|
||||||
|
shift
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $# == 0 && $refresh == false ]] || [[ ${1:-} == --help ]]; then
|
||||||
|
echo 'Usage: ssh-machine [--refresh] <name> [cmd ...]'
|
||||||
|
echo ' ssh-machine --refresh'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Resolve deploy's relative identity path from the checkout root.
|
||||||
|
cd "$PRJ_ROOT"
|
||||||
|
|
||||||
|
cache_dir=.cache/ssh-machine
|
||||||
|
cache=$cache_dir/connections.json
|
||||||
|
|
||||||
|
fingerprint() {
|
||||||
|
{
|
||||||
|
git rev-parse HEAD &&
|
||||||
|
git diff --no-ext-diff --no-textconv --binary HEAD -- &&
|
||||||
|
git diff --no-ext-diff --no-textconv --binary --cached --
|
||||||
|
} | sha256sum | cut -d ' ' -f 1
|
||||||
|
}
|
||||||
|
|
||||||
|
key=$(fingerprint)
|
||||||
|
|
||||||
|
if [[ $refresh == true ]] || ! jq -e --arg key "$key" \
|
||||||
|
'.fingerprint == $key and (.nodes | type == "object")' "$cache" >/dev/null 2>&1; then
|
||||||
|
mkdir -p "$cache_dir"
|
||||||
|
tmp=$(mktemp "$cache_dir/.connections.XXXXXXXX")
|
||||||
|
trap 'rm -f "$tmp"' EXIT
|
||||||
|
|
||||||
|
echo 'Refreshing ssh-machine connection cache…' >&2
|
||||||
|
|
||||||
|
# Interpolation here belongs to Nix, not the shell.
|
||||||
|
# shellcheck disable=SC2016
|
||||||
|
nix eval --json .#deploy --apply '
|
||||||
|
d: builtins.mapAttrs (_: n:
|
||||||
|
d.sshOpts ++ n.sshOpts ++ [ "${n.sshUser}@${n.hostname}" ]
|
||||||
|
) d.nodes
|
||||||
|
' > "$tmp"
|
||||||
|
|
||||||
|
if [[ $(fingerprint) != "$key" ]]; then
|
||||||
|
echo 'Checkout changed during evaluation; retry ssh-machine.' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
payload=$(jq -c --arg key "$key" '{fingerprint: $key, nodes: .}' "$tmp")
|
||||||
|
printf '%s\n' "$payload" > "$tmp"
|
||||||
|
mv -f "$tmp" "$cache"
|
||||||
|
trap - EXIT
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ $# -gt 0 ]] || exit 0
|
||||||
|
|
||||||
|
name=$1
|
||||||
|
shift
|
||||||
|
|
||||||
|
# deploy-rs mangles home configuration names containing @.
|
||||||
|
node=${name//@/-at-}
|
||||||
|
|
||||||
|
args=$(jq -ce --arg node "$node" '
|
||||||
|
.nodes | if has($node) then .[$node]
|
||||||
|
else error("Unknown deploy node: " + $node) end
|
||||||
|
' "$cache")
|
||||||
|
|
||||||
|
mapfile -d '' -t ssh_args < <(jq -j '.[] | ., "\u0000"' <<< "$args")
|
||||||
|
|
||||||
|
exec ssh "${ssh_args[@]}" "$@"
|
||||||
+10
-1
@@ -58,7 +58,16 @@ Generated profiles deploy in this order:
|
|||||||
|
|
||||||
`ssh-machine <name> [cmd]` resolves `user@host` and the merged global/node `sshOpts` from
|
`ssh-machine <name> [cmd]` resolves `user@host` and the merged global/node `sshOpts` from
|
||||||
`.#deploy`, using the same `my.deploy.enable` gate and `@` → `-at-` mangling as `deploy`, then runs
|
`.#deploy`, using the same `my.deploy.enable` gate and `@` → `-at-` mangling as `deploy`, then runs
|
||||||
`ssh`. Boxes default to `fish`, so pipe multi-statement remote scripts through `bash`, for example
|
`ssh`. Connection settings for all nodes are cached together in `.cache/ssh-machine/`;
|
||||||
|
normal connections read the cache without running Nix. A fingerprint of Git HEAD and
|
||||||
|
staged/unstaged tracked changes automatically invalidates it, including newly added files
|
||||||
|
after `git add`.
|
||||||
|
Untracked and ignored files do not affect the fingerprint. Any tracked edit, even an unrelated
|
||||||
|
one, causes the next connection to refresh all nodes. Use `ssh-machine --refresh` to force a
|
||||||
|
refresh, or `ssh-machine --refresh <name> [cmd]` to refresh and connect. Refresh failures stop
|
||||||
|
the command; the previous cache is never used as a fallback.
|
||||||
|
|
||||||
|
Boxes default to `fish`, so pipe multi-statement remote scripts through `bash`, for example
|
||||||
`ssh-machine <name> bash -s < script.sh`. If a flaky agent stalls public-key authentication, use
|
`ssh-machine <name> bash -s < script.sh`. If a flaky agent stalls public-key authentication, use
|
||||||
`SSH_AUTH_SOCK= ssh-machine …`.
|
`SSH_AUTH_SOCK= ssh-machine …`.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user