Fix false-positive "config changed" on every alert, every run

The previous fix compared the whole live 'config' object against the
whole desired one - but Graylog's GET response fills in extra defaults
(query_parameters, filters, use_cron_scheduling, cron_expression,
cron_timezone, ...) that never appear in our alert JSON files. That made
every single alert compare as "different" on every single run, not just
the one actually edited - confirmed live: a re-run PUT-updated all 7
alerts when only alert7's query had changed.

Fixed by comparing only the keys our own files actually author (project
the live config down to just those keys before comparing), and switched
from interpolating JSON into python -c string literals to writing to temp
files and reading them - the former is fragile the moment a value contains
a quote or backslash.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
byrsapty 2026-07-23 16:34:32 +03:00
parent c1361f16c9
commit 19643a74dd

View file

@ -498,39 +498,71 @@ step_alerts() {
# Same "compare content, PUT-update if changed" fix as step_pipeline_rules()/ # Same "compare content, PUT-update if changed" fix as step_pipeline_rules()/
# step_pipelines() - an alert's title never changes when its query/threshold # step_pipelines() - an alert's title never changes when its query/threshold
# does, so a plain title-exists check would silently skip the update # does, so a plain title-exists check would silently skip the update
# forever. Confirmed live this session: alert7's query was fixed in git # forever.
# (vendor:accel-ppp -> vendor:accel-ppp AND event_type:*) but a re-run #
# kept reporting "already exists" with the old, buggy query still live. # Uses files instead of interpolating JSON into python -c strings (fragile
local existing_defs # with quotes/backslashes), and compares only the keys OUR file authors in
existing_defs="$(gcurl GET /events/definitions)" # "config" - not the whole object. Confirmed live this is necessary:
local f title id body current_config desired_config # Graylog's GET response fills in extra defaults (query_parameters, filters,
# use_cron_scheduling, cron_expression, cron_timezone, ...) that never
# appear in our alert JSON files, so a naive whole-object comparison always
# reports "changed" and PUTs every single alert on every single run, not
# just the one actually edited.
local existing_defs_file
existing_defs_file="$(mktemp)"
gcurl GET /events/definitions > "$existing_defs_file"
local f title id body_file
for f in "$SCRIPT_DIR"/alerts/alert*.json; do for f in "$SCRIPT_DIR"/alerts/alert*.json; do
[ -f "$f" ] || continue [ -f "$f" ] || continue
title="$(python3 -c "import json;print(json.load(open('$f'))['title'])")" title="$(python3 -c "import json;print(json.load(open('$f'))['title'])")"
body="$(sed \ body_file="$(mktemp)"
sed \
-e "s/__NETWORK_STREAM_ID__/$NETWORK_STREAM_ID/" \ -e "s/__NETWORK_STREAM_ID__/$NETWORK_STREAM_ID/" \
-e "s/__SERVERS_STREAM_ID__/$SERVERS_STREAM_ID/" \ -e "s/__SERVERS_STREAM_ID__/$SERVERS_STREAM_ID/" \
-e "s/__DISCORD_NOTIFICATION_ID__/$notif_id/" \ -e "s/__DISCORD_NOTIFICATION_ID__/$notif_id/" \
"$f")" "$f" > "$body_file"
id="$(echo "$existing_defs" | python3 -c "import json,sys;d=json.load(sys.stdin);print(next((e['id'] for e in d['event_definitions'] if e['title']=='$title'),''))")"
id="$(python3 -c "
import json
d = json.load(open('$existing_defs_file'))
print(next((e['id'] for e in d['event_definitions'] if e['title'] == '$title'), ''))
")"
if [ -n "$id" ]; then if [ -n "$id" ]; then
current_config="$(echo "$existing_defs" | python3 -c "import json,sys;d=json.load(sys.stdin);print(json.dumps(next(e['config'] for e in d['event_definitions'] if e['id']=='$id'),sort_keys=True))")" if python3 -c "
desired_config="$(echo "$body" | python3 -c "import json,sys;print(json.dumps(json.load(sys.stdin)['config'],sort_keys=True))")" import json, sys
if [ "$current_config" = "$desired_config" ]; then existing = json.load(open('$existing_defs_file'))
current_full = next(e['config'] for e in existing['event_definitions'] if e['id'] == '$id')
desired = json.load(open('$body_file'))['config']
filtered_current = {k: current_full.get(k) for k in desired}
sys.exit(0 if filtered_current == desired else 1)
"; then
skip "alert '$title' already up to date ($id)" skip "alert '$title' already up to date ($id)"
rm -f "$body_file"
continue continue
fi fi
gcurl PUT "/events/definitions/$id" "$(echo "$body" | python3 -c "import json,sys;d=json.load(sys.stdin);d['id']='$id';print(json.dumps(d))")" >/dev/null python3 -c "
import json
d = json.load(open('$body_file'))
d['id'] = '$id'
print(json.dumps(d))
" > "${body_file}.put"
gcurl PUT "/events/definitions/$id" "$(cat "${body_file}.put")" >/dev/null
gcurl PUT "/events/definitions/$id/schedule" "" >/dev/null gcurl PUT "/events/definitions/$id/schedule" "" >/dev/null
ok "updated alert '$title' ($id) - config changed" ok "updated alert '$title' ($id) - config changed"
rm -f "$body_file" "${body_file}.put"
continue continue
fi fi
id="$(gcurl POST /events/definitions "{\"entity\": ${body}, \"share_request\": {\"selected_grantee_capabilities\": {}}}" \
id="$(gcurl POST /events/definitions "{\"entity\": $(cat "$body_file"), \"share_request\": {\"selected_grantee_capabilities\": {}}}" \
| python3 -c "import json,sys;print(json.load(sys.stdin).get('id',''))")" | python3 -c "import json,sys;print(json.load(sys.stdin).get('id',''))")"
[ -n "$id" ] || die "alert '$title' failed to create" [ -n "$id" ] || die "alert '$title' failed to create"
gcurl PUT "/events/definitions/$id/schedule" "" >/dev/null gcurl PUT "/events/definitions/$id/schedule" "" >/dev/null
ok "created and enabled alert '$title' ($id)" ok "created and enabled alert '$title' ($id)"
rm -f "$body_file"
done done
rm -f "$existing_defs_file"
} }
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------