mirror of
https://github.com/gadgethd/ukmesh.git
synced 2026-10-15 10:40:01 +00:00
* feat(ops): auto-rebaseline owner ACL inventory * docs(ops): handoff owner baseline autorebaseline * ops: add reviewed owner-authorization baseline refresh script scripts/refresh-owner-baseline.sh regenerates the owner inventory baseline with the running backend image, validates it against the live inventory (fail-closed), backs up .env before repointing, and can recreate the backend. It refuses to overwrite an existing baseline and keeps the previous file for rollback. Adds the matching operations runbook section. * test(ops): cover refresh baseline validation output --------- Co-authored-by: gadgethd <111318106+gadgethd@users.noreply.github.com>
243 lines
8.8 KiB
Bash
Executable File
243 lines
8.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# refresh-owner-baseline.sh
|
|
#
|
|
# Regenerate and validate the owner-authorization inventory baseline after an
|
|
# approved owner grant/config change.
|
|
#
|
|
# The running backend fails readiness closed when the configured baseline no
|
|
# longer matches the live database/ACL (counts, grants, generations). Use this
|
|
# script ONLY after reviewing and approving the grant change, then repoint the
|
|
# baseline and recreate the backend.
|
|
#
|
|
# Usage:
|
|
# scripts/refresh-owner-baseline.sh # generate + validate only
|
|
# scripts/refresh-owner-baseline.sh --apply # also repoint .env (backup kept)
|
|
# scripts/refresh-owner-baseline.sh --apply --recreate
|
|
# scripts/refresh-owner-baseline.sh --help
|
|
#
|
|
# Overrides:
|
|
# OWNER_BASELINE_CONTAINER backend container name (default meshcore-analytics-backend-1)
|
|
# OWNER_BASELINE_EXPORT_DIR host directory for the new baseline
|
|
# (default: OWNER_AUTH_INVENTORY_HOST_DIR from .env)
|
|
# OWNER_BASELINE_NETWORK docker network (default: first network of the container)
|
|
# OWNER_BASELINE_MOSQUITTO mosquitto config dir (default: <repo>/mosquitto)
|
|
|
|
set -euo pipefail
|
|
umask 077
|
|
|
|
mesh_script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
|
|
mesh_repo_dir="$(cd -- "$mesh_script_dir/.." && pwd -P)"
|
|
mesh_container="${OWNER_BASELINE_CONTAINER:-meshcore-analytics-backend-1}"
|
|
mesh_compose_files=(-f docker-compose.yml -f docker-compose.live.yml)
|
|
mesh_apply=false
|
|
mesh_recreate=false
|
|
|
|
usage() {
|
|
sed -n '2,25p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
|
|
}
|
|
|
|
while [ "$#" -gt 0 ]; do
|
|
case "$1" in
|
|
--apply) mesh_apply=true ;;
|
|
--recreate) mesh_recreate=true ;;
|
|
--help|-h) usage; exit 0 ;;
|
|
*) echo "unknown argument: $1" >&2; usage >&2; exit 64 ;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
for command in docker python3; do
|
|
command -v "$command" >/dev/null 2>&1 || {
|
|
echo "required command is unavailable: $command" >&2
|
|
exit 69
|
|
}
|
|
done
|
|
|
|
if ! docker inspect "$mesh_container" >/dev/null 2>&1; then
|
|
echo "backend container not found: $mesh_container" >&2
|
|
exit 66
|
|
fi
|
|
if ! docker inspect "$mesh_container" --format '{{.State.Running}}' | grep -q '^true$'; then
|
|
echo "backend container is not running: $mesh_container" >&2
|
|
exit 66
|
|
fi
|
|
|
|
mesh_env_file="$mesh_repo_dir/.env"
|
|
if [ "$mesh_apply" = true ] && [ ! -f "$mesh_env_file" ]; then
|
|
echo ".env not found in $mesh_repo_dir (required for --apply)" >&2
|
|
exit 66
|
|
fi
|
|
if [ "$mesh_recreate" = true ] \
|
|
&& { [ ! -f "$mesh_repo_dir/docker-compose.yml" ] || [ ! -f "$mesh_repo_dir/docker-compose.live.yml" ]; }; then
|
|
echo "compose files not found in $mesh_repo_dir (required for --recreate)" >&2
|
|
exit 66
|
|
fi
|
|
|
|
mesh_image="$(docker inspect "$mesh_container" --format '{{.Config.Image}}')"
|
|
mesh_network="${OWNER_BASELINE_NETWORK:-$(docker inspect "$mesh_container" \
|
|
--format '{{range $name, $_ := .NetworkSettings.Networks}}{{$name}}{{"\n"}}{{end}}' | head -n1)}"
|
|
if [ -z "$mesh_network" ]; then
|
|
echo "could not determine the backend container network" >&2
|
|
exit 66
|
|
fi
|
|
|
|
mesh_mounted_export="$(docker inspect "$mesh_container" \
|
|
--format '{{range .Mounts}}{{if eq .Destination "/owner-auth-inventory"}}{{.Source}}{{end}}{{end}}')"
|
|
mesh_mounted_mosquitto="$(docker inspect "$mesh_container" \
|
|
--format '{{range .Mounts}}{{if eq .Destination "/mosquitto/config"}}{{.Source}}{{end}}{{end}}')"
|
|
|
|
mesh_mosquitto_dir="${OWNER_BASELINE_MOSQUITTO:-${mesh_mounted_mosquitto:-$mesh_repo_dir/mosquitto}}"
|
|
if [ ! -d "$mesh_mosquitto_dir" ]; then
|
|
echo "mosquitto config directory not found: $mesh_mosquitto_dir" >&2
|
|
exit 66
|
|
fi
|
|
|
|
mesh_export_dir="${OWNER_BASELINE_EXPORT_DIR:-$mesh_mounted_export}"
|
|
if [ -z "$mesh_export_dir" ] && [ -f "$mesh_env_file" ]; then
|
|
mesh_export_dir="$(grep -m1 '^OWNER_AUTH_INVENTORY_HOST_DIR=' "$mesh_env_file" 2>/dev/null | cut -d= -f2-)"
|
|
fi
|
|
mesh_export_dir="${mesh_export_dir:-$mesh_repo_dir/owner-auth-inventory}"
|
|
if [ ! -d "$mesh_export_dir" ]; then
|
|
echo "export directory not found: $mesh_export_dir" >&2
|
|
echo "create it or set OWNER_BASELINE_EXPORT_DIR" >&2
|
|
exit 66
|
|
fi
|
|
mesh_export_dir="$(cd -- "$mesh_export_dir" && pwd -P)"
|
|
mesh_export_uid="$(stat -c '%u' "$mesh_export_dir")"
|
|
mesh_export_gid="$(stat -c '%g' "$mesh_export_dir")"
|
|
|
|
mesh_stamp="$(date -u +%Y%m%dT%H%M%SZ)"
|
|
mesh_basename="owner-grants-${mesh_stamp}-rebaseline.json"
|
|
mesh_export_path="$mesh_export_dir/$mesh_basename"
|
|
if [ -e "$mesh_export_path" ]; then
|
|
echo "refusing to overwrite existing baseline: $mesh_export_path" >&2
|
|
exit 73
|
|
fi
|
|
|
|
mesh_tmp_env="$(mktemp /tmp/owner-baseline-env.XXXXXX)"
|
|
mesh_tmp_out="$(mktemp /tmp/owner-baseline-out.XXXXXX)"
|
|
mesh_tmp_check="$(mktemp /tmp/owner-baseline-check.XXXXXX)"
|
|
mesh_mosquitto_gid="$(stat -c '%g' "$mesh_mosquitto_dir")"
|
|
cleanup() {
|
|
rm -f "$mesh_tmp_env" "$mesh_tmp_out" "$mesh_tmp_check"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
python3 - "$mesh_container" "$mesh_tmp_env" <<'PY'
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
|
|
container, destination = sys.argv[1], sys.argv[2]
|
|
raw = subprocess.check_output(["docker", "inspect", container]).decode()
|
|
envs = json.loads(raw)[0]["Config"]["Env"]
|
|
skip = {"PATH", "HOSTNAME", "HOME", "NODE_VERSION", "YARN_VERSION", "PWD", "SHLVL", "TERM", "container"}
|
|
kept = [entry for entry in envs if entry.split("=", 1)[0] not in skip]
|
|
with open(destination, "w", encoding="utf-8") as handle:
|
|
handle.write("\n".join(kept) + "\n")
|
|
PY
|
|
|
|
run_tool() {
|
|
# shellcheck disable=SC2068
|
|
docker run --rm \
|
|
--user "${mesh_export_uid}:${mesh_export_gid}" \
|
|
--group-add "$mesh_mosquitto_gid" \
|
|
--network "$mesh_network" \
|
|
--env-file "$mesh_tmp_env" \
|
|
-v "$mesh_mosquitto_dir:/mosquitto/config:ro" \
|
|
-v "$mesh_export_dir:/owner-auth-inventory:rw" \
|
|
"$mesh_image" \
|
|
node dist/tools/inventoryOwnerAuthorization.js "$@"
|
|
}
|
|
|
|
echo "Generating owner baseline from ${mesh_image} (container ${mesh_container})"
|
|
if ! run_tool "--export=/owner-auth-inventory/${mesh_basename}" >"$mesh_tmp_out" 2>&1; then
|
|
cat "$mesh_tmp_out" >&2
|
|
echo "baseline generation failed" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "Validating the generated baseline"
|
|
if ! run_tool "--baseline=/owner-auth-inventory/${mesh_basename}" >"$mesh_tmp_check" 2>&1; then
|
|
cat "$mesh_tmp_check" >&2
|
|
echo "baseline validation command failed; new file kept at $mesh_export_path" >&2
|
|
exit 1
|
|
fi
|
|
|
|
python3 - "$mesh_tmp_out" "$mesh_tmp_check" "$mesh_export_path" <<'PY'
|
|
import json
|
|
import sys
|
|
|
|
generated = json.load(open(sys.argv[1], encoding="utf-8"))
|
|
validation = json.load(open(sys.argv[2], encoding="utf-8"))
|
|
baseline_check = validation.get("baselineValidation") or {}
|
|
summary = {
|
|
"exportPath": sys.argv[3],
|
|
"generatedAt": generated.get("generatedAt"),
|
|
"counts": generated.get("counts"),
|
|
"configuredGeneration": (generated.get("configuredGeneration") or "")[:12],
|
|
"aclDesiredGeneration": ((generated.get("aclState") or {}).get("desiredGeneration") or "")[:12],
|
|
"contentSha256": (generated.get("contentSha256") or "")[:12],
|
|
"baselineOk": baseline_check.get("ok"),
|
|
"mismatches": baseline_check.get("mismatches"),
|
|
}
|
|
print(json.dumps(summary, indent=2))
|
|
if baseline_check.get("ok") is not True:
|
|
sys.exit(1)
|
|
PY
|
|
|
|
echo
|
|
echo "Baseline validated. It is NOT active until OWNER_AUTH_INVENTORY_BASELINE_PATH"
|
|
echo "points at it and the backend is recreated."
|
|
|
|
if [ "$mesh_apply" != true ]; then
|
|
echo
|
|
echo "To activate:"
|
|
echo " 1. review the summary above and confirm the grant changes are approved"
|
|
echo " 2. set OWNER_AUTH_INVENTORY_BASELINE_PATH=/owner-auth-inventory/${mesh_basename} in .env"
|
|
echo " 3. docker compose ${mesh_compose_files[*]} up -d --no-build --no-deps backend"
|
|
echo " 4. curl -fsS http://127.0.0.1:3000/readyz"
|
|
echo
|
|
echo "Or re-run with --apply (and optionally --recreate)."
|
|
exit 0
|
|
fi
|
|
|
|
mesh_env_backup="${mesh_env_file}.bak-$(date -u +%Y%m%dT%H%M%SZ)"
|
|
cp "$mesh_env_file" "$mesh_env_backup"
|
|
chmod 600 "$mesh_env_backup"
|
|
|
|
python3 - "$mesh_env_file" "/owner-auth-inventory/${mesh_basename}" <<'PY'
|
|
import re
|
|
import sys
|
|
|
|
path, new_value = sys.argv[1], sys.argv[2]
|
|
with open(path, encoding="utf-8") as handle:
|
|
content = handle.read()
|
|
updated, count = re.subn(
|
|
r"^OWNER_AUTH_INVENTORY_BASELINE_PATH=.*$",
|
|
f"OWNER_AUTH_INVENTORY_BASELINE_PATH={new_value}",
|
|
content,
|
|
count=1,
|
|
flags=re.MULTILINE,
|
|
)
|
|
if count != 1:
|
|
sys.exit("OWNER_AUTH_INVENTORY_BASELINE_PATH not found in .env")
|
|
with open(path, "w", encoding="utf-8") as handle:
|
|
handle.write(updated)
|
|
PY
|
|
chmod 600 "$mesh_env_file"
|
|
echo "Repointed OWNER_AUTH_INVENTORY_BASELINE_PATH (backup: $mesh_env_backup)"
|
|
|
|
if [ "$mesh_recreate" = true ]; then
|
|
mesh_image_env="BACKEND_IMAGE=$mesh_image"
|
|
( cd "$mesh_repo_dir" && env "$mesh_image_env" docker compose "${mesh_compose_files[@]}" \
|
|
up -d --no-build --no-deps backend )
|
|
echo "Backend recreated. Verify readiness:"
|
|
echo " curl -fsS http://127.0.0.1:3000/readyz"
|
|
else
|
|
echo "Recreate the backend to activate:"
|
|
echo " cd $mesh_repo_dir"
|
|
echo " BACKEND_IMAGE=$mesh_image docker compose ${mesh_compose_files[*]} up -d --no-build --no-deps backend"
|
|
fi
|