From cf3bb8911a4099ad937968228036e540f46ba3cd Mon Sep 17 00:00:00 2001 From: Federico Di Pierro Date: Tue, 5 Dec 2023 09:11:19 +0100 Subject: [PATCH] new(unit_tests,userspace): properly support env var expansions in all scalar values of yaml file. Signed-off-by: Federico Di Pierro --- unit_tests/falco/test_configuration.cpp | 24 +++- userspace/falco/yaml_helper.h | 176 ++++++++++++++---------- 2 files changed, 122 insertions(+), 78 deletions(-) diff --git a/unit_tests/falco/test_configuration.cpp b/unit_tests/falco/test_configuration.cpp index 5b4cf689ff3..d8b32109dec 100644 --- a/unit_tests/falco/test_configuration.cpp +++ b/unit_tests/falco/test_configuration.cpp @@ -158,7 +158,11 @@ TEST(Configuration, configuration_environment_variables) " - ${ENV_VAR_EMBEDDED}/foo\n" "is_test: ${ENV_VAR_BOOL}\n" "num_test: ${ENV_VAR_INT}\n" - "empty_test: ${ENV_VAR_EMPTY}\n"; + "empty_test: ${ENV_VAR_EMPTY}\n" + "plugins:\n" + " - name: k8saudit\n" + " library_path: /foo/${ENV_VAR}/libk8saudit.so\n" + " open_params: ${ENV_VAR_INT}\n"; yaml_helper conf; conf.load_from_string(env_var_sample_yaml); @@ -191,9 +195,9 @@ TEST(Configuration, configuration_environment_variables) auto base_value_escaped = conf.get_scalar("base_value.escaped", default_value); ASSERT_EQ(base_value_escaped, env_var_value); // Environment variable within quotes - /* Test fetching of an undefined environment variable. Expected to return the default value.*/ + /* Test fetching of an undefined environment variable. Resolves to empty string. */ auto unknown_boolean = conf.get_scalar("base_value.subvalue.subvalue2.boolean", default_value); - ASSERT_EQ(unknown_boolean, default_value); + ASSERT_EQ(unknown_boolean, ""); /* Test fetching of environment variables from a list */ auto base_value_2_list_0 = conf.get_scalar("base_value_2.sample_list[0]", default_value); @@ -237,9 +241,17 @@ TEST(Configuration, configuration_environment_variables) auto integer = conf.get_scalar("num_test", -1); ASSERT_EQ(integer, 12); - // An env var that resolves to an empty string returns default value - auto empty_default_str = conf.get_scalar("empty_test", "test"); - ASSERT_EQ(empty_default_str, "test"); + // An env var that resolves to an empty string returns "" + auto empty_default_str = conf.get_scalar("empty_test", default_value); + ASSERT_EQ(empty_default_str, ""); + + std::list plugins; + conf.get_sequence>(plugins, std::string("plugins")); + std::vector m_plugins{ std::make_move_iterator(std::begin(plugins)), + std::make_move_iterator(std::end(plugins)) }; + ASSERT_EQ(m_plugins[0].m_name, "k8saudit"); + ASSERT_EQ(m_plugins[0].m_library_path, "/foo/" + env_var_value + "/libk8saudit.so"); + ASSERT_EQ(m_plugins[0].m_open_params, "12"); /* Clear the set environment variables after testing */ SET_ENV_VAR(env_var_name.c_str(), ""); diff --git a/userspace/falco/yaml_helper.h b/userspace/falco/yaml_helper.h index 3843b03cf7c..2297251044b 100644 --- a/userspace/falco/yaml_helper.h +++ b/userspace/falco/yaml_helper.h @@ -26,18 +26,54 @@ limitations under the License. #endif #include #include +#include #include #include #include #include #include #include +#include #include "config_falco.h" #include "event_drops.h" #include "falco_outputs.h" +class yaml_helper; + +class yaml_visitor { +private: + using Callback = std::function; + yaml_visitor(Callback cb): seen(), cb(std::move(cb)) {} + + void operator()(YAML::Node &cur) { + seen.push_back(cur); + if (cur.IsMap()) { + for (YAML::detail::iterator_value pair : cur) { + descend(pair.second); + } + } else if (cur.IsSequence()) { + for (YAML::detail::iterator_value child : cur) { + descend(child); + } + } else if (cur.IsScalar()) { + cb(cur); + } + } + + void descend(YAML::Node &target) { + if (std::find(seen.begin(), seen.end(), target) == seen.end()) { + (*this)(target); + } + } + + std::vector seen; + Callback cb; + + friend class yaml_helper; +}; + /** * @brief An helper class for reading and editing YAML documents */ @@ -50,6 +86,7 @@ class yaml_helper void load_from_string(const std::string& input) { m_root = YAML::Load(input); + pre_process_env_vars(); } /** @@ -58,6 +95,7 @@ class yaml_helper void load_from_file(const std::string& path) { m_root = YAML::LoadFile(path); + pre_process_env_vars(); } /** @@ -78,79 +116,8 @@ class yaml_helper get_node(node, key); if(node.IsDefined()) { - auto value = node.as(); - - // Helper function to convert string to the desired type T - auto convert_str_to_t = [&default_value](const std::string& str) -> T { - if (str.empty()) - { - return default_value; - } - - if constexpr (std::is_same_v) - { - return str; - } - std::stringstream ss(str); - T result; - if (ss >> std::boolalpha >> result) return result; - return default_value; - }; - - auto start_pos = value.find('$'); - while (start_pos != std::string::npos) - { - auto substr = value.substr(start_pos); - // Case 1 -> ${} - if (substr.rfind("${", 0) == 0) - { - auto end_pos = substr.find('}'); - if (end_pos != std::string::npos) - { - // Eat "${" and "}" when getting the env var name - auto env_str = substr.substr(2, end_pos - 2); - const char* env_value = std::getenv(env_str.c_str()); // Get the environment variable value - if(env_value) - { - // env variable name + "${}" - value.replace(start_pos, env_str.length() + 3, env_value); - } - else - { - value.erase(start_pos, env_str.length() + 3); - } - } - else - { - // There are no "}" chars anymore; just break leaving rest of value untouched. - break; - } - } - // Case 2 -> $${} - else if (substr.rfind("$${", 0) == 0) - { - auto end_pos = substr.find('}'); - if (end_pos != std::string::npos) - { - // Consume first "$" token - value.erase(start_pos, 1); - } - else - { - // There are no "}" chars anymore; just break leaving rest of value untouched. - break; - } - start_pos++; // consume the second '$' token - } - else - { - start_pos += substr.length(); - } - start_pos = value.find("$", start_pos); - } - return convert_str_to_t(value); + return node.as(default_value); } - return default_value; } @@ -189,6 +156,71 @@ class yaml_helper private: YAML::Node m_root; + /* + * When loading a yaml file, + * we immediately pre process all scalar values through a visitor private API, + * and resolve any "${env_var}" to its value; + * moreover, any "$${str}" is resolved to simply "${str}". + */ + void pre_process_env_vars() + { + yaml_visitor([](YAML::Node &scalar) { + auto value = scalar.as(); + auto start_pos = value.find('$'); + while (start_pos != std::string::npos) + { + auto substr = value.substr(start_pos); + // Case 1 -> ${} + if (substr.rfind("${", 0) == 0) + { + auto end_pos = substr.find('}'); + if (end_pos != std::string::npos) + { + // Eat "${" and "}" when getting the env var name + auto env_str = substr.substr(2, end_pos - 2); + const char* env_value = std::getenv(env_str.c_str()); // Get the environment variable value + if(env_value) + { + // env variable name + "${}" + value.replace(start_pos, env_str.length() + 3, env_value); + } + else + { + value.erase(start_pos, env_str.length() + 3); + } + } + else + { + // There are no "}" chars anymore; just break leaving rest of value untouched. + break; + } + } + // Case 2 -> $${} + else if (substr.rfind("$${", 0) == 0) + { + auto end_pos = substr.find('}'); + if (end_pos != std::string::npos) + { + // Consume first "$" token + value.erase(start_pos, 1); + } + else + { + // There are no "}" chars anymore; just break leaving rest of value untouched. + break; + } + start_pos++; // consume the second '$' token + } + else + { + start_pos += substr.length(); + } + start_pos = value.find("$", start_pos); + } + scalar = value; + })(m_root); + } + /** * Key is a string representing a node in the YAML document. * The provided key string can navigate the document in its