- modules/server-deploy-guard.nix: extendable aggregator registered via
services.deployGuard.checks.<name>.{description,command}. Installs
deploy-guard-check with per-check timeout, pass/block reporting, JSON
output, DEPLOY_GUARD_BYPASS / /run/deploy-guard-bypass (single-shot).
- services/jellyfin/jellyfin-deploy-guard.nix: curl+jq on /Sessions,
blocks when any session carries NowPlayingItem; soft-fails when unreachable.
- services/minecraft-deploy-guard.nix: mcstatus SLP query on 25565, blocks
when players.online > 0; soft-fails when unreachable.
- flake.nix: wrap deploy.nodes.muffin activation with activate.custom so
deploy-guard-check runs before switch-to-configuration. Auto-rollback
catches the failure. dryActivate/boot branches preserved.
- deploy.sh: SSH preflight for ./deploy.sh muffin with --force /
DEPLOY_GUARD_FORCE=1 (touches remote bypass marker). Connectivity
failure is soft; activation still enforces.
- tests/deploy-guard.nix: aggregator contract, bypass mechanics, timeout,
JSON output.
79 lines
2.4 KiB
Nix
79 lines
2.4 KiB
Nix
# Deploy guard check for Jellyfin.
|
|
#
|
|
# Contract (deploy-guard-check plug-in):
|
|
# - exit 0: Jellyfin has no active playback sessions (or is unreachable, which
|
|
# also means no users can be watching).
|
|
# - exit 1: at least one session is actively playing back media; stdout lists
|
|
# user / title / client so the operator sees who they'd disrupt.
|
|
#
|
|
# A paused session counts as "active" — the user is at the keyboard and will
|
|
# notice a restart.
|
|
{
|
|
config,
|
|
lib,
|
|
pkgs,
|
|
service_configs,
|
|
...
|
|
}:
|
|
let
|
|
apiKeyPath = config.age.secrets.jellyfin-api-key.path;
|
|
jellyfinPort = service_configs.ports.private.jellyfin.port;
|
|
|
|
check = pkgs.writeShellApplication {
|
|
name = "deploy-guard-check-jellyfin";
|
|
runtimeInputs = with pkgs; [
|
|
curl
|
|
jq
|
|
coreutils
|
|
];
|
|
text = ''
|
|
api_key_path=${lib.escapeShellArg apiKeyPath}
|
|
if [[ ! -r "$api_key_path" ]]; then
|
|
echo "jellyfin: api key not readable at $api_key_path; skipping" >&2
|
|
exit 0
|
|
fi
|
|
|
|
key=$(cat "$api_key_path")
|
|
|
|
if ! resp=$(curl -sf --max-time 5 \
|
|
-H "Authorization: MediaBrowser Token=$key" \
|
|
"http://127.0.0.1:${toString jellyfinPort}/Sessions" 2>/dev/null); then
|
|
echo "jellyfin: unreachable; assuming safe to deploy" >&2
|
|
exit 0
|
|
fi
|
|
|
|
# Parse defensively — if Jellyfin returns something we can't understand
|
|
# we prefer allowing the deploy over blocking it (the worst case is we
|
|
# restart jellyfin while nobody is watching).
|
|
if ! active=$(printf '%s' "$resp" | jq '[.[] | select(.NowPlayingItem)] | length' 2>/dev/null); then
|
|
echo "jellyfin: /Sessions response not parsable; assuming safe" >&2
|
|
exit 0
|
|
fi
|
|
|
|
if [[ "$active" -eq 0 ]]; then
|
|
exit 0
|
|
fi
|
|
|
|
echo "Jellyfin: $active active playback session(s):"
|
|
printf '%s' "$resp" | jq -r '
|
|
.[]
|
|
| select(.NowPlayingItem)
|
|
| " - \(.UserName // "?") \(if (.PlayState.IsPaused // false) then "paused" else "playing" end) \(.NowPlayingItem.Type // "item") \"\(.NowPlayingItem.Name // "?")\" on \(.Client // "?") / \(.DeviceName // "?")"
|
|
'
|
|
exit 1
|
|
'';
|
|
};
|
|
in
|
|
{
|
|
imports = [
|
|
../../modules/server-deploy-guard.nix
|
|
];
|
|
|
|
config = lib.mkIf config.services.jellyfin.enable {
|
|
services.deployGuard.checks.jellyfin = {
|
|
description = "Active Jellyfin playback sessions";
|
|
command = check;
|
|
};
|
|
};
|
|
}
|