#!/usr/bin/env bash # Pre-flight harness for a Canalhandia deploy. # # Every check below is READ-ONLY. It never restarts the server, never writes to # the plugins directory, and never touches the world. Run it after staging a # jar and before asking Crafty to restart: a red line here is a crash-on-boot # you get to fix while the server is still up. # # Usage: ./preflight.sh [path/to/new.jar] # (defaults to target/Canalhandia-1.0.0.jar) set -uo pipefail JAR="${1:-target/Canalhandia-1.0.0.jar}" NS=minecraft SERVER_ID=6e39a8b2-300b-42d6-8139-f397c23e461b PLUGINS="/crafty/servers/${SERVER_ID}/plugins" K="microk8s kubectl -n ${NS}" fail=0 pass() { printf ' \033[32mOK\033[0m %s\n' "$1"; } warn() { printf ' \033[33mWARN\033[0m %s\n' "$1"; } bad() { printf ' \033[31mFAIL\033[0m %s\n' "$1"; fail=$((fail + 1)); } head() { printf '\n\033[1m%s\033[0m\n' "$1"; } CRAFTY=$($K get pods -l app=crafty-controller -o jsonpath='{.items[0].metadata.name}' 2>/dev/null) if [ -z "$CRAFTY" ]; then CRAFTY=$($K get pods --no-headers 2>/dev/null | awk '/^crafty-controller/ {print $1; exit}') fi head "1. Local build" if [ ! -f "$JAR" ]; then bad "jar not found: $JAR" else pass "jar present: $JAR ($(stat -c%s "$JAR") bytes)" # A jar that does not open is a jar the server will refuse at boot. if unzip -t "$JAR" >/dev/null 2>&1; then pass "jar archive is intact" else bad "jar archive is corrupt (unzip -t failed)" fi # plugin.yml is the only file Bukkit truly requires; without it the plugin is # skipped silently and nothing in the deploy takes effect. if unzip -p "$JAR" plugin.yml >/dev/null 2>&1; then pass "plugin.yml present in jar" else bad "plugin.yml MISSING from jar — the plugin would not load at all" fi # Every command the code registers must exist in plugin.yml, or register() # logs a warning and the command silently does nothing in game. # Kept in step with Canalhandia.onEnable's register() list. A command that is # registered in code but absent here logs a warning at boot and then silently # does nothing in game, which is a hard failure to diagnose from inside. CMDS="canalhandia curiosidade adivinha enquete ranking reagir reacoes \ palpite votar legal wow top f ia iap errado nota save" missing="" for cmd in $CMDS; do unzip -p "$JAR" plugin.yml 2>/dev/null | grep -qE "^ ${cmd}:" || missing="$missing $cmd" done if [ -z "$missing" ]; then pass "all $(echo $CMDS | wc -w) commands declared in plugin.yml" else bad "commands missing from plugin.yml:$missing" fi # Permissions the new features gate on. An undeclared Bukkit permission falls # back to op-only, which would silently stop normal players writing notes. for perm in canalhandia.nota canalhandia.nota.publica; do if unzip -p "$JAR" plugin.yml 2>/dev/null | grep -q " ${perm}:"; then pass "permission ${perm} declared" else bad "permission ${perm} MISSING — would default to op-only" fi done # config.yml ships defaults; a jar without it means saveDefaultConfig() writes # nothing and every setting silently falls back to the hardcoded default. if unzip -p "$JAR" config.yml >/dev/null 2>&1; then pass "config.yml present in jar" else bad "config.yml MISSING from jar" fi fi head "2. Tests" if [ -d target/surefire-reports ]; then tests=$(grep -ho 'tests="[0-9]*"' target/surefire-reports/*.xml 2>/dev/null | grep -o '[0-9]*' | paste -sd+ | bc) bads=$(grep -ho 'failures="[0-9]*"\|errors="[0-9]*"' target/surefire-reports/*.xml 2>/dev/null | grep -o '[0-9]*' | paste -sd+ | bc) if [ "${bads:-1}" = "0" ]; then pass "${tests} tests, 0 failures/errors" else bad "${bads} test failures/errors — run mvn test" fi else warn "no surefire reports; run the test suite before deploying" fi head "3. Cluster" if [ -z "$CRAFTY" ]; then bad "crafty-controller pod not found in namespace ${NS}" else pass "crafty pod: $CRAFTY" if $K exec "$CRAFTY" -- test -d "$PLUGINS" 2>/dev/null; then pass "plugins directory reachable" else bad "cannot reach $PLUGINS" fi fi head "4. Staged jar on the server" if [ -n "$CRAFTY" ] && [ -f "$JAR" ]; then local_sum=$(sha256sum "$JAR" | cut -c1-8) remote_sum=$($K exec "$CRAFTY" -- sha256sum "$PLUGINS/Canalhandia-1.0.0.jar" 2>/dev/null | cut -c1-8) if [ -z "$remote_sum" ]; then bad "no Canalhandia jar staged on the server" elif [ "$local_sum" = "$remote_sum" ]; then pass "staged jar matches the local build (sha $local_sum)" else bad "staged jar is sha $remote_sum, local build is $local_sum — copy it again" fi # Ownership: the JVM runs as uid 1000 / gid 0. A root-owned jar is a jar the # server cannot read, and the failure looks like "plugin just did not load". owner=$($K exec "$CRAFTY" -- stat -c '%u:%g' "$PLUGINS/Canalhandia-1.0.0.jar" 2>/dev/null) if [ "$owner" = "1000:0" ]; then pass "jar ownership 1000:0" else bad "jar ownership is '$owner', expected 1000:0 — chown it" fi # A rollback target must exist before, not after, something goes wrong. if $K exec "$CRAFTY" -- sh -c "ls $PLUGINS/Canalhandia-1.0.0.jar.bak-* >/dev/null 2>&1"; then pass "rollback jar(s) present" else bad "no .bak jar to roll back to" fi fi head "5. Live config" if [ -n "$CRAFTY" ]; then cfg="$PLUGINS/Canalhandia/config.yml" if $K exec "$CRAFTY" -- test -f "$cfg" 2>/dev/null; then pass "config.yml present on the server" # Parsed HERE rather than in the pod: the crafty image has no python, and a # config.yml that does not parse is a plugin that disables itself on boot. tmp=$(mktemp) if $K exec "$CRAFTY" -- cat "$cfg" > "$tmp" 2>/dev/null && [ -s "$tmp" ]; then # Pick a parser. "no parser available" must NOT be reported as "invalid": # a harness that cries wolf is a harness people learn to ignore. if python3 -c "import yaml" 2>/dev/null; then yaml_check() { python3 -c "import yaml,sys;yaml.safe_load(open(sys.argv[1]))" "$1"; } elif command -v docker >/dev/null 2>&1; then yaml_check() { docker run --rm -v "$1":/c.yml:ro python:3.12-slim \ sh -c "pip install -q pyyaml >/dev/null 2>&1 && python3 -c \ 'import yaml;yaml.safe_load(open(\"/c.yml\"))'"; } else yaml_check() { return 2; } fi yaml_check "$tmp" 2>/dev/null case $? in 0) pass "config.yml parses as valid YAML" ;; 2) warn "no YAML parser available (pip install pyyaml) — not checked" ;; *) bad "config.yml on the server is NOT valid YAML — the plugin would fail to load" ;; esac # The keys this deploy depends on. A missing key is not fatal (Settings # has defaults) but it means the merge did not happen as intended. for key in "modulos:" "zoacao:" "personalidade:" "contexto-chat:" "estado-servidor:"; do if grep -q "$key" "$tmp"; then pass "config has ${key%:}" else warn "config has no '${key%:}' — will fall back to the built-in default" fi done else warn "could not read config.yml out of the pod" fi rm -f "$tmp" if $K exec "$CRAFTY" -- sh -c "ls $PLUGINS/Canalhandia/config.yml.bak-* >/dev/null 2>&1"; then pass "config backup(s) present" else bad "no config.yml.bak-* to roll back to" fi else bad "config.yml missing on the server" fi fi head "6. Server health right now" if [ -n "$CRAFTY" ]; then logf="/crafty/servers/${SERVER_ID}/logs/latest.log" # tr -dc keeps digits only: grep -c prints nothing on no-match under some # shells, and the stray newline made the numeric test below explode. online=$($K exec "$CRAFTY" -- sh -c "grep -c 'joined the game' $logf" 2>/dev/null | tr -dc '0-9') pass "log readable (${online:-0} join lines this session)" errs=$($K exec "$CRAFTY" -- sh -c "tail -500 $logf | grep -ci 'ERROR\]'" 2>/dev/null | tr -dc '0-9') if [ "${errs:-0}" -gt 0 ]; then warn "${errs} ERROR lines in the last 500 — read them before restarting" else pass "no ERROR lines in the last 500" fi fi printf '\n' if [ "$fail" -eq 0 ]; then printf '\033[32mPRE-FLIGHT CLEAN — safe to restart.\033[0m\n' exit 0 fi printf '\033[31m%d CHECK(S) FAILED — do NOT restart yet.\033[0m\n' "$fail" exit 1