Skip to content

Commit

Permalink
new(unit_tests,userspace): properly support env var expansions in all…
Browse files Browse the repository at this point in the history
… scalar values of yaml file.

Signed-off-by: Federico Di Pierro <nierro92@gmail.com>
  • Loading branch information
FedeDP committed Dec 5, 2023
1 parent 0977f98 commit cf3bb89
Show file tree
Hide file tree
Showing 2 changed files with 122 additions and 78 deletions.
24 changes: 18 additions & 6 deletions unit_tests/falco/test_configuration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -191,9 +195,9 @@ TEST(Configuration, configuration_environment_variables)
auto base_value_escaped = conf.get_scalar<std::string>("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<std::string>("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<std::string>("base_value_2.sample_list[0]", default_value);
Expand Down Expand Up @@ -237,9 +241,17 @@ TEST(Configuration, configuration_environment_variables)
auto integer = conf.get_scalar<int32_t>("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<std::string>("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<std::string>("empty_test", default_value);
ASSERT_EQ(empty_default_str, "");

std::list<falco_configuration::plugin_config> plugins;
conf.get_sequence<std::list<falco_configuration::plugin_config>>(plugins, std::string("plugins"));
std::vector<falco_configuration::plugin_config> 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(), "");
Expand Down
176 changes: 104 additions & 72 deletions userspace/falco/yaml_helper.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,54 @@ limitations under the License.
#endif
#include <yaml-cpp/yaml.h>
#include <string>
#include <utility>
#include <vector>
#include <list>
#include <set>
#include <iostream>
#include <fstream>
#include <type_traits>
#include <regex>

#include "config_falco.h"

#include "event_drops.h"
#include "falco_outputs.h"

class yaml_helper;

class yaml_visitor {
private:
using Callback = std::function<void(YAML::Node&)>;
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<YAML::Node> seen;
Callback cb;

friend class yaml_helper;
};

/**
* @brief An helper class for reading and editing YAML documents
*/
Expand All @@ -50,6 +86,7 @@ class yaml_helper
void load_from_string(const std::string& input)
{
m_root = YAML::Load(input);
pre_process_env_vars();
}

/**
Expand All @@ -58,6 +95,7 @@ class yaml_helper
void load_from_file(const std::string& path)
{
m_root = YAML::LoadFile(path);
pre_process_env_vars();
}

/**
Expand All @@ -78,79 +116,8 @@ class yaml_helper
get_node(node, key);
if(node.IsDefined())
{
auto value = node.as<std::string>();

// 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<T, std::string>)
{
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<T>(default_value);
}

return default_value;
}

Expand Down Expand Up @@ -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<std::string>();
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
Expand Down

0 comments on commit cf3bb89

Please sign in to comment.