master
c 363 lines 15.2 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 #include "rrdhost-labels.h"
4 #include "rrdhost.h"
5 #include "streaming/stream.h"
6
7 void rrdhost_set_is_parent_label(void) {
8 if (!localhost || !localhost->rrdlabels)
9 return;
10
11 uint32_t count = stream_receivers_currently_connected();
12
13 if (count == 0 || count == 1) {
14 RRDLABELS *labels = localhost->rrdlabels;
15 if(rrdlabels_add_changed(labels, "_is_parent", (count) ? "true" : "false", RRDLABEL_SRC_AUTO))
16 rrdhost_flag_set(localhost, RRDHOST_FLAG_PENDING_LABEL_RECHECK);
17
18 // queue a node info
19 aclk_queue_node_info(localhost, false);
20 }
21 }
22
23 // expand ${VAR} and ${VAR:-default} patterns in src, writing result to dst
24 static void env_expand_labels_value(const char *src, char *dst, size_t dst_size) {
25 if(!src || !dst || dst_size < 1) return;
26
27 const char *s = src;
28 char *d = dst;
29 char *end = dst + dst_size - 1;
30
31 while(*s && d < end) {
32 if(s[0] == '$' && s[1] == '{') {
33 const char *closing = strchr(s + 2, '}');
34 if(!closing) {
35 // no closing brace — copy rest literally
36 while(*s && d < end)
37 *d++ = *s++;
38 break;
39 }
40
41 size_t content_len = closing - (s + 2);
42 char *var_name = mallocz(content_len + 1);
43 memcpy(var_name, s + 2, content_len);
44 var_name[content_len] = '\0';
45
46 // check for :- default separator
47 char *default_val = NULL;
48 char *sep = strstr(var_name, ":-");
49 if(sep) {
50 *sep = '\0';
51 default_val = sep + 2;
52 }
53
54 const char *env_val = getenv(var_name);
55 const char *resolved;
56
57 if(env_val && *env_val)
58 resolved = env_val;
59 else if(default_val)
60 resolved = default_val;
61 else {
62 nd_log(NDLS_DAEMON, NDLP_WARNING,
63 "RRDLABEL: environment variable '%s' is not set and no default provided", var_name);
64 resolved = "";
65 }
66
67 size_t rlen = strlen(resolved);
68 size_t available = end - d;
69 size_t to_copy = rlen < available ? rlen : available;
70 memcpy(d, resolved, to_copy);
71 d += to_copy;
72
73 freez(var_name);
74 s = closing + 1;
75 }
76 else {
77 *d++ = *s++;
78 }
79 }
80
81 *d = '\0';
82 }
83
84 // check if value contains any ${...} pattern worth expanding
85 static bool value_has_env_variables(const char *value) {
86 const char *p = value;
87 while((p = strchr(p, '$')) != NULL) {
88 if(p[1] == '{') return true;
89 p++;
90 }
91 return false;
92 }
93
94 static bool config_label_cb(void *data __maybe_unused, const char *name, const char *value) {
95 if(value_has_env_variables(value)) {
96 char expanded[RRDLABELS_MAX_VALUE_LENGTH + 1];
97 env_expand_labels_value(value, expanded, sizeof(expanded));
98 rrdlabels_add(localhost->rrdlabels, name, expanded, RRDLABEL_SRC_CONFIG);
99 }
100 else
101 rrdlabels_add(localhost->rrdlabels, name, value, RRDLABEL_SRC_CONFIG);
102
103 return true;
104 }
105
106 static void rrdhost_load_config_labels(void) {
107 int status = inicfg_load(&netdata_config, NULL, 1, CONFIG_SECTION_HOST_LABEL);
108 if(!status) {
109 char *filename = CONFIG_DIR "/" CONFIG_FILENAME;
110 nd_log(NDLS_DAEMON, NDLP_WARNING,
111 "RRDLABEL: Cannot reload the configuration file '%s', using labels in memory",
112 filename);
113 }
114
115 inicfg_foreach_value_in_section(&netdata_config, CONFIG_SECTION_HOST_LABEL, config_label_cb, NULL);
116 }
117
118 // Returns true if the kubernetes labels script ran to a clean exit. A false
119 // return tells the caller the refresh was best-effort: callers must preserve
120 // the previously-loaded k8s labels (see reload_host_labels()) so a transient
121 // script failure does not silently delete them.
122 static bool rrdhost_load_kubernetes_labels(void) {
123 char label_script[sizeof(char) * (strlen(netdata_configured_primary_plugins_dir) + strlen("get-kubernetes-labels.sh") + 2)];
124 sprintf(label_script, "%s/%s", netdata_configured_primary_plugins_dir, "get-kubernetes-labels.sh");
125
126 if (unlikely(access(label_script, R_OK) != 0)) {
127 nd_log(NDLS_DAEMON, NDLP_ERR,
128 "Kubernetes pod label fetching script %s not found.",
129 label_script);
130
131 return false;
132 }
133
134 POPEN_INSTANCE *instance = spawn_popen_run(label_script);
135 if(!instance) return false;
136
137 char buffer[1000 + 1];
138 while (fgets(buffer, 1000, spawn_popen_stdout(instance)) != NULL)
139 rrdlabels_add_pair(localhost->rrdlabels, buffer, RRDLABEL_SRC_AUTO|RRDLABEL_SRC_K8S);
140
141 // Non-zero exit code means that all the script output is error messages. We've shown already any message that didn't include a ':'
142 // Here we'll inform with an ERROR that the script failed, show whatever (if anything) was added to the list of labels, free the memory and set the return to null
143 int rc = spawn_popen_wait(instance);
144 if(rc) {
145 nd_log(NDLS_DAEMON, NDLP_ERR,
146 "%s exited abnormally. Failed to get kubernetes labels.",
147 label_script);
148 return false;
149 }
150
151 return true;
152 }
153
154 static void rrdhost_load_auto_labels(void) {
155 RRDLABELS *labels = localhost->rrdlabels;
156
157 rrdhost_system_info_to_rrdlabels(localhost->system_info, labels);
158 add_aclk_host_labels();
159
160 // The source should be CONF, but when it is set, these labels are exported by default ('send configured labels' in exporting.conf).
161 // Their export seems to break exporting to Graphite, see https://github.com/netdata/netdata/issues/14084.
162
163 int is_ephemeral = inicfg_get_boolean(&netdata_config, CONFIG_SECTION_GLOBAL, "is ephemeral node", CONFIG_BOOLEAN_NO);
164 rrdlabels_add(labels, HOST_LABEL_IS_EPHEMERAL, is_ephemeral ? "true" : "false", RRDLABEL_SRC_CONFIG);
165
166 int has_unstable_connection = inicfg_get_boolean(&netdata_config, CONFIG_SECTION_GLOBAL, "has unstable connection", CONFIG_BOOLEAN_NO);
167 rrdlabels_add(labels, "_has_unstable_connection", has_unstable_connection ? "true" : "false", RRDLABEL_SRC_AUTO);
168
169 rrdlabels_add(labels, "_is_parent", (stream_receivers_currently_connected() > 0) ? "true" : "false", RRDLABEL_SRC_AUTO);
170
171 rrdlabels_add(labels, "_hostname", string2str(localhost->hostname), RRDLABEL_SRC_AUTO);
172 rrdlabels_add(labels, "_os", string2str(localhost->os), RRDLABEL_SRC_AUTO);
173
174 if (localhost->stream.snd.destination)
175 rrdlabels_add(labels, "_streams_to", string2str(localhost->stream.snd.destination), RRDLABEL_SRC_AUTO);
176
177 {
178 RRDHOST_TZ host_tz = rrdhost_tz_get(localhost);
179 rrdlabels_add(labels, "_timezone", host_tz.timezone, RRDLABEL_SRC_AUTO);
180 rrdlabels_add(labels, "_abbrev_timezone", host_tz.abbrev_timezone, RRDLABEL_SRC_AUTO);
181 rrdhost_tz_free(&host_tz);
182 }
183 }
184
185 void reload_host_labels(void) {
186 if(!localhost->rrdlabels)
187 localhost->rrdlabels = rrdlabels_create();
188
189 rrdlabels_unmark_all(localhost->rrdlabels);
190
191 // priority is important here
192 rrdhost_load_config_labels();
193 bool k8s_loaded = rrdhost_load_kubernetes_labels();
194 rrdhost_load_auto_labels();
195
196 // If the kubernetes loader did not run cleanly (script missing, spawn
197 // failure, or non-zero exit), the previously-loaded k8s labels were not
198 // refreshed and must survive the prune below -- otherwise a transient
199 // script failure would silently delete them. Re-mark them so the next
200 // step keeps them.
201 if (!k8s_loaded)
202 rrdlabels_mark_source_as_old(localhost->rrdlabels, RRDLABEL_SRC_K8S);
203
204 // drop entries that the loaders did not re-add (e.g. a label removed from
205 // netdata.conf, or no longer returned by get-kubernetes-labels.sh).
206 // RRDLABEL_FLAG_DONT_DELETE entries are preserved.
207 rrdlabels_remove_all_unmarked(localhost->rrdlabels);
208
209 rrdhost_flag_set(localhost,
210 RRDHOST_FLAG_METADATA_LABELS | RRDHOST_FLAG_METADATA_UPDATE | RRDHOST_FLAG_PENDING_LABEL_RECHECK);
211
212 stream_send_host_labels(localhost);
213 }
214
215 // ----------------------------------------------------------------------------
216 // unit tests
217
218 static int env_expand_unittest_check(const char *src, const char *expected, const char *test_name) {
219 char buf[RRDLABELS_MAX_VALUE_LENGTH + 1];
220 env_expand_labels_value(src, buf, sizeof(buf));
221
222 int err = strcmp(buf, expected) != 0;
223 fprintf(stderr, " env_expand(%s): %s, expected '%s', got '%s'\n",
224 test_name, err ? "FAILED" : "OK", expected, buf);
225 return err;
226 }
227
228 int rrdhost_labels_unittest(void) {
229 fprintf(stderr, "\n%s() tests\n", __FUNCTION__);
230 int errors = 0;
231
232 // --- set up test env vars ---
233 setenv("ND_TEST_VAR", "hello", 1);
234 setenv("ND_TEST_DC", "us-east", 1);
235 setenv("ND_TEST_RACK", "rack42", 1);
236 setenv("ND_TEST_EMPTY", "", 1);
237 unsetenv("ND_TEST_UNSET");
238
239 // no variables — pass through unchanged
240 errors += env_expand_unittest_check("plain value", "plain value", "plain text");
241 errors += env_expand_unittest_check("", "", "empty string");
242
243 // basic variable expansion
244 errors += env_expand_unittest_check("${ND_TEST_VAR}", "hello", "${VAR} set");
245 errors += env_expand_unittest_check("prefix-${ND_TEST_VAR}", "prefix-hello", "prefix + ${VAR}");
246 errors += env_expand_unittest_check("${ND_TEST_VAR}-suffix", "hello-suffix", "${VAR} + suffix");
247 errors += env_expand_unittest_check("pre-${ND_TEST_VAR}-post", "pre-hello-post", "prefix + ${VAR} + suffix");
248
249 // multiple variables
250 errors += env_expand_unittest_check("${ND_TEST_DC}-${ND_TEST_RACK}", "us-east-rack42", "two vars adjacent");
251 errors += env_expand_unittest_check("${ND_TEST_DC}/${ND_TEST_RACK}/${ND_TEST_VAR}", "us-east/rack42/hello", "three vars");
252 errors += env_expand_unittest_check("dc=${ND_TEST_DC} rack=${ND_TEST_RACK}", "dc=us-east rack=rack42", "vars with literal labels");
253
254 // default values — variable is set (default ignored)
255 errors += env_expand_unittest_check("${ND_TEST_VAR:-fallback}", "hello", "default ignored when var set");
256 errors += env_expand_unittest_check("${ND_TEST_DC:-other}", "us-east", "default ignored when var set (2)");
257
258 // default values — variable is unset
259 errors += env_expand_unittest_check("${ND_TEST_UNSET:-fallback}", "fallback", "default used when var unset");
260 errors += env_expand_unittest_check("pre-${ND_TEST_UNSET:-fallback}-post", "pre-fallback-post", "default with surrounding text");
261
262 // default values — variable is empty (treated same as unset)
263 errors += env_expand_unittest_check("${ND_TEST_EMPTY:-fallback}", "fallback", "default used when var empty");
264
265 // unset variable, no default — empty string
266 errors += env_expand_unittest_check("${ND_TEST_UNSET}", "", "unset var no default = empty");
267 errors += env_expand_unittest_check("pre-${ND_TEST_UNSET}-post", "pre--post", "unset var no default with text");
268
269 // empty default — should resolve to empty string
270 errors += env_expand_unittest_check("${ND_TEST_UNSET:-}", "", "empty default");
271 errors += env_expand_unittest_check("pre-${ND_TEST_UNSET:-}-post", "pre--post", "empty default with text");
272
273 // malformed syntax — no closing brace, copy literally
274 errors += env_expand_unittest_check("${ND_TEST_UNCLOSED", "${ND_TEST_UNCLOSED", "no closing brace");
275 errors += env_expand_unittest_check("pre-${ND_TEST_UNCLOSED", "pre-${ND_TEST_UNCLOSED", "no closing brace with prefix");
276
277 // dollar sign not followed by brace — literal
278 errors += env_expand_unittest_check("$notavar", "$notavar", "$ without {");
279 errors += env_expand_unittest_check("price is $5", "price is $5", "$ with digit");
280 errors += env_expand_unittest_check("$$", "$$", "double dollar");
281 errors += env_expand_unittest_check("$", "$", "lone dollar at end");
282
283 // empty variable name: ${} — getenv("") returns NULL, no default → empty
284 errors += env_expand_unittest_check("${}", "", "empty var name");
285 errors += env_expand_unittest_check("${:-fallback}", "fallback", "empty var name with default");
286
287 // default containing :- (only first :- is the separator)
288 errors += env_expand_unittest_check("${ND_TEST_UNSET:-a:-b}", "a:-b", "default containing :-");
289
290 // no recursive expansion — env value containing ${...} is NOT re-expanded
291 setenv("ND_TEST_NESTED", "${ND_TEST_VAR}", 1);
292 errors += env_expand_unittest_check("${ND_TEST_NESTED}", "${ND_TEST_VAR}", "no recursive expansion");
293
294 // default containing ${...} is NOT re-expanded
295 errors += env_expand_unittest_check("${ND_TEST_UNSET:-${ND_TEST_VAR}}", "${ND_TEST_VAR}", "no expansion in default");
296
297 // buffer overflow protection — expand into small buffer
298 {
299 char tiny[8];
300 env_expand_labels_value("${ND_TEST_DC}", tiny, sizeof(tiny));
301 // "us-east" is 7 chars, buffer is 8 (7+null) — should fit exactly
302 int err = strcmp(tiny, "us-east") != 0;
303 fprintf(stderr, " env_expand(small buffer exact fit): %s, expected 'us-east', got '%s'\n",
304 err ? "FAILED" : "OK", tiny);
305 errors += err;
306 }
307 {
308 char tiny[5];
309 env_expand_labels_value("${ND_TEST_DC}", tiny, sizeof(tiny));
310 // "us-east" is 7 chars, buffer is 5 (4+null) — should truncate to "us-e"
311 int err = strcmp(tiny, "us-e") != 0;
312 fprintf(stderr, " env_expand(small buffer truncation): %s, expected 'us-e', got '%s'\n",
313 err ? "FAILED" : "OK", tiny);
314 errors += err;
315 }
316 {
317 char tiny[5];
318 env_expand_labels_value("abcdefghij", tiny, sizeof(tiny));
319 // plain text truncation — should truncate to "abcd"
320 int err = strcmp(tiny, "abcd") != 0;
321 fprintf(stderr, " env_expand(plain text truncation): %s, expected 'abcd', got '%s'\n",
322 err ? "FAILED" : "OK", tiny);
323 errors += err;
324 }
325
326 // value_has_env_variables() tests
327 {
328 struct {
329 const char *input;
330 bool expected;
331 } detect_tests[] = {
332 { "plain", false },
333 { "", false },
334 { "$notvar", false },
335 { "$", false },
336 { "${VAR}", true },
337 { "pre${VAR}post", true },
338 { "${A}${B}", true },
339 { "$${}", true }, // second $ starts ${
340 { "${", true }, // has ${ even without closing }
341 { NULL, false }
342 };
343 for(int i = 0; detect_tests[i].input; i++) {
344 bool result = value_has_env_variables(detect_tests[i].input);
345 int err = result != detect_tests[i].expected;
346 fprintf(stderr, " value_has_env_variables('%s'): %s, expected %s, got %s\n",
347 detect_tests[i].input, err ? "FAILED" : "OK",
348 detect_tests[i].expected ? "true" : "false",
349 result ? "true" : "false");
350 errors += err;
351 }
352 }
353
354 // --- cleanup test env vars ---
355 unsetenv("ND_TEST_VAR");
356 unsetenv("ND_TEST_DC");
357 unsetenv("ND_TEST_RACK");
358 unsetenv("ND_TEST_EMPTY");
359 unsetenv("ND_TEST_NESTED");
360
361 fprintf(stderr, "%s: %d errors\n", __FUNCTION__, errors);
362 return errors;
363 }