| 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 | } |