Files
nixos/services/jellyfin/jellyfin-deploy-guard.nix
Simon Gardling aef99e7365
Some checks failed
Build and Deploy / mreow (push) Successful in 51s
Build and Deploy / yarn (push) Successful in 47s
Build and Deploy / muffin (push) Failing after 1m9s
deploy-guard: block activation while users are online
- 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.
2026-04-22 00:36:21 -04:00

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;
};
};
}