Files
nixos/services/minecraft-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

68 lines
1.9 KiB
Nix

# Deploy guard check for the Minecraft server.
#
# Queries the standard Server List Ping (SLP) handshake on the game port —
# no RCON, no query, no extra config. SLP is always enabled and returns the
# live player count plus (usually) a short name sample.
#
# Contract (deploy-guard-check plug-in):
# - exit 0: no players online, or the server isn't reachable at all (down ⇒
# no users to disrupt).
# - exit 1: at least one player is connected; stdout lists the names that
# made it into the SLP sample.
{
config,
lib,
pkgs,
service_configs,
...
}:
let
minecraftPort = service_configs.ports.public.minecraft.port;
check =
pkgs.writers.writePython3Bin "deploy-guard-check-minecraft"
{
libraries = [ pkgs.python3Packages.mcstatus ];
flakeIgnore = [
"E501"
"E402"
];
}
''
import sys
try:
from mcstatus import JavaServer
except ImportError as e:
print(f"minecraft: mcstatus unavailable ({e}); assuming safe", file=sys.stderr)
sys.exit(0)
try:
status = JavaServer.lookup("127.0.0.1:${toString minecraftPort}", timeout=5).status()
except Exception as e:
print(f"minecraft: unreachable ({e}); assuming safe to deploy", file=sys.stderr)
sys.exit(0)
online = status.players.online
if online <= 0:
sys.exit(0)
sample = getattr(status.players, "sample", None) or []
names = ", ".join(p.name for p in sample) or "<names not reported>"
print(f"Minecraft: {online} player(s) online: {names}")
sys.exit(1)
'';
in
{
imports = [
../modules/server-deploy-guard.nix
];
config = lib.mkIf config.services.minecraft-servers.enable {
services.deployGuard.checks.minecraft = {
description = "Players connected to the Minecraft server";
command = check;
};
};
}