master
sh 173 lines 5.56 KB
Raw
1 #!/usr/bin/env bash
2 # Helper library for the query-agent-events skill.
3 #
4 # Sources query-netdata-agents/scripts/_lib.sh and adds
5 # agentevents_* helpers. Token-safe: bearers and the cloud
6 # token never appear on the assistant-visible stdout.
7 #
8 # Usage:
9 # source "$(git rev-parse --show-toplevel)/.agents/skills/query-agent-events/scripts/_lib.sh"
10 # agentevents_load_env
11
12 set -euo pipefail
13
14 # Resolve self path (zsh + bash compatible).
15 if [ -n "${ZSH_VERSION-}" ]; then
16 eval '_agentevents_lib_self="${(%):-%x}"'
17 elif [ -n "${BASH_VERSION-}" ]; then
18 _agentevents_lib_self="${BASH_SOURCE[0]}"
19 else
20 _agentevents_lib_self="$0"
21 fi
22 _agentevents_lib_dir="$(cd "$(dirname "$_agentevents_lib_self")" && pwd)"
23
24 # Source query-netdata-agents helpers (transport + bearer mint).
25 # shellcheck disable=SC1091
26 source "$(git rev-parse --show-toplevel)/.agents/skills/query-netdata-agents/scripts/_lib.sh"
27
28 # ---------------------------------------------------------------
29 # Env loading
30
31 agentevents_load_env() {
32 agents_load_env
33
34 : "${AGENT_EVENTS_HOSTNAME:?AGENT_EVENTS_HOSTNAME is empty -- see <repo>/.agents/ENV.md to set it.}"
35 : "${AGENT_EVENTS_NODE_ID:?AGENT_EVENTS_NODE_ID is empty -- see <repo>/.agents/ENV.md to set it.}"
36 : "${AGENT_EVENTS_MACHINE_GUID:?AGENT_EVENTS_MACHINE_GUID is empty -- see <repo>/.agents/ENV.md to set it.}"
37 }
38
39 # ---------------------------------------------------------------
40 # Audit directory (gitignored under <repo>/.local/audits/)
41
42 agentevents_audit_dir() {
43 local d
44 d="$(agents_audit_dir)/../query-agent-events"
45 mkdir -p "$d"
46 (cd "$d" && pwd)
47 }
48
49 # Journal namespace for selections / __logs_sources.
50 # Hardcoded -- the ingestion server's log2journal --namespace
51 # is always 'agent-events', regardless of the host's network
52 # name (which lives in AGENT_EVENTS_HOSTNAME).
53 agentevents_namespace() {
54 printf '%s' 'agent-events'
55 }
56
57 # ---------------------------------------------------------------
58 # Function call: agentevents_query_function VIA PAYLOAD
59 #
60 # VIA is "cloud" or "agent". PAYLOAD is the systemd-journal
61 # Function POST body (JSON string).
62 # stdout: response body (JSON). No tokens leak.
63
64 agentevents_query_function() {
65 local via="$1"
66 local payload="$2"
67
68 case "$via" in
69 cloud)
70 agents_query_cloud \
71 POST \
72 "/api/v2/nodes/${AGENT_EVENTS_NODE_ID}/function?function=systemd-journal" \
73 "$payload"
74 ;;
75 agent)
76 agents_query_agent \
77 --node "${AGENT_EVENTS_NODE_ID}" \
78 --host "${AGENT_EVENTS_HOSTNAME}:19999" \
79 --machine-guid "${AGENT_EVENTS_MACHINE_GUID}" \
80 POST \
81 "/api/v3/function?function=systemd-journal" \
82 "$payload"
83 ;;
84 *)
85 echo "agentevents_query_function: unknown VIA '$via' (use cloud|agent)" >&2
86 return 2
87 ;;
88 esac
89 }
90
91 # ---------------------------------------------------------------
92 # Default version-filter computation.
93 #
94 # agentevents_compute_default_versions VIA SINCE_RELATIVE_SECONDS
95 #
96 # Queries the journal for the AE_AGENT_VERSION facet and picks:
97 # - the latest stable (matches ^v\d+\.\d+\.\d+$, sorted desc)
98 # - up to 3 latest nightlies (matches ^v\d+\.\d+\.\d+-\d+-nightly$, sorted desc by commit count)
99 #
100 # Outputs a JSON array of version strings to stdout.
101
102 agentevents_compute_default_versions() {
103 local via="${1:-cloud}"
104 local since_secs="${2:-86400}"
105
106 local namespace
107 namespace="$(agentevents_namespace)"
108
109 local payload
110 payload=$(jq -nc \
111 --arg ns "$namespace" \
112 --argjson after "-${since_secs}" \
113 '{
114 "after": $after,
115 "before": 0,
116 "last": 1,
117 "__logs_sources": $ns,
118 "facets": ["AE_AGENT_VERSION"]
119 }')
120
121 local resp
122 resp="$(agentevents_query_function "$via" "$payload")"
123
124 # Extract the AE_AGENT_VERSION facet's option values.
125 # The response shape is documented in
126 # docs/netdata-ai/skills/query-netdata-cloud/query-logs.md
127 # under "Response shape".
128 echo "$resp" | jq -c '
129 ([.facets[]? | select(.id=="AE_AGENT_VERSION") | .options[]?.id] // [])
130 as $all
131 | (
132 ($all | map(select(test("^v\\d+\\.\\d+\\.\\d+$"))) | sort | reverse | .[0:1])
133 + ($all | map(select(test("^v\\d+\\.\\d+\\.\\d+-\\d+-nightly$")))
134 | sort_by(. | capture("-(?<n>\\d+)-nightly").n | tonumber)
135 | reverse | .[0:3])
136 )'
137 }
138
139 # ---------------------------------------------------------------
140 # No-token-leak self-test.
141
142 agentevents_selftest_no_token_leak() {
143 # Drive the public wrappers with sentinel values; assert no
144 # sentinel ever appears on captured stdout.
145 local sentinel="deadbeef-1234-5678-9abc-def012345678"
146
147 # Set sentinels in the environment that COULD leak if a
148 # wrapper logged its inputs.
149 local saved_token="${NETDATA_CLOUD_TOKEN:-}"
150 local saved_node="${AGENT_EVENTS_NODE_ID:-}"
151
152 NETDATA_CLOUD_TOKEN="$sentinel"
153 AGENT_EVENTS_NODE_ID="$sentinel"
154 export NETDATA_CLOUD_TOKEN AGENT_EVENTS_NODE_ID
155
156 # Run a no-op-ish payload through the helpers; capture stdout.
157 local out
158 out="$( {
159 agentevents_query_function cloud '{"info":true}' 2>/dev/null || true
160 } )"
161
162 # Restore.
163 NETDATA_CLOUD_TOKEN="$saved_token"
164 AGENT_EVENTS_NODE_ID="$saved_node"
165
166 if printf '%s' "$out" | grep -q "$sentinel"; then
167 echo "FAIL: sentinel $sentinel appeared on captured stdout" >&2
168 return 1
169 fi
170
171 echo "PASS: agentevents_selftest_no_token_leak"
172 return 0
173 }