Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support parsing of system environment variables in yaml #2562

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
78 changes: 75 additions & 3 deletions unit_tests/falco/test_configuration.cpp
Expand Up @@ -88,16 +88,88 @@ TEST(Configuration, modify_yaml_fields)
{
std::string key = "base_value.subvalue.subvalue2.boolean";
yaml_helper conf;

/* Get original value */
conf.load_from_string(sample_yaml);
ASSERT_EQ(conf.get_scalar<bool>(key, false), true);

/* Modify the original value */
conf.set_scalar<bool>(key, false);
ASSERT_EQ(conf.get_scalar<bool>(key, true), false);

/* Modify it again */
conf.set_scalar<bool>(key, true);
ASSERT_EQ(conf.get_scalar<bool>(key, false), true);
}

TEST(Configuration, configuration_environment_variables)
{
// Set an environment variable for testing purposes
std::string env_var_value = "envVarValue";
std::string env_var_name = "ENV_VAR";
std::string default_value = "default";
setenv(env_var_name.c_str(), env_var_value.c_str(), 1);
yaml_helper conf;

std::string sample_yaml =
"base_value:\n"
" id: $ENV_VAR\n"
" name: '${ENV_VAR}'\n"
" string: my_string\n"
" invalid: $${ENV_VAR}\n"
" invalid_env: $$ENV_VAR\n"
" escaped: \"${ENV_VAR}\"\n"
" subvalue:\n"
" subvalue2:\n"
" boolean: ${UNSED_XX_X_X_VAR}\n"
"base_value_2:\n"
" sample_list:\n"
" - ${ENV_VAR}\n"
" - ' ${ENV_VAR}'\n"
" - $UNSED_XX_X_X_VAR\n";
conf.load_from_string(sample_yaml);

/* Check if the base values are defined */
ASSERT_TRUE(conf.is_defined("base_value"));
ASSERT_TRUE(conf.is_defined("base_value_2"));
ASSERT_FALSE(conf.is_defined("unknown_base_value"));

/* Test fetching of a regular string without any environment variable */
std::string base_value_string = conf.get_scalar<std::string>("base_value.string", default_value);
ASSERT_EQ(base_value_string, "my_string");

/* Test fetching of escaped environment variable format. Should return the string as-is after stripping the leading `$` */
std::string base_value_invalid = conf.get_scalar<std::string>("base_value.invalid", default_value);
ASSERT_EQ(base_value_invalid, "${ENV_VAR}");

/* Test fetching of invalid escaped environment variable format. Should return the string as-is */
std::string base_value_invalid_env = conf.get_scalar<std::string>("base_value.invalid_env", default_value);
ASSERT_EQ(base_value_invalid_env, "$$ENV_VAR");

/* Test fetching of strings that contain environment variables */
std::string base_value_id = conf.get_scalar<std::string>("base_value.id", default_value);
ASSERT_EQ(base_value_id, "$ENV_VAR"); // Does not follow the `${VAR}` format, so it should be treated as a regular string

std::string base_value_name = conf.get_scalar<std::string>("base_value.name", default_value);
ASSERT_EQ(base_value_name, env_var_value); // Proper environment variable format

std::string 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.*/
std::string unknown_boolean = conf.get_scalar<std::string>("base_value.subvalue.subvalue2.boolean", default_value);
ASSERT_EQ(unknown_boolean, default_value);

/* Test fetching of environment variables from a list */
std::string base_value_2_list_0 = conf.get_scalar<std::string>("base_value_2.sample_list[0]", default_value);
ASSERT_EQ(base_value_2_list_0, env_var_value); // Proper environment variable format

std::string base_value_2_list_1 = conf.get_scalar<std::string>("base_value_2.sample_list[1]", default_value);
ASSERT_EQ(base_value_2_list_1, " ${ENV_VAR}"); // Environment variable preceded by a space, hence treated as a regular string

std::string base_value_2_list_2 = conf.get_scalar<std::string>("base_value_2.sample_list[2]", default_value);
ASSERT_EQ(base_value_2_list_2, "$UNSED_XX_X_X_VAR"); // Does not follow the `${VAR}` format, so should be treated as a regular string

/* Clear the set environment variable after testing */
unsetenv(env_var_name.c_str());
}
47 changes: 41 additions & 6 deletions userspace/falco/yaml_helper.h
Expand Up @@ -72,6 +72,41 @@ class yaml_helper
get_node(node, key);
if(node.IsDefined())
{
std::string 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 {
std::stringstream ss(str);
T result;
if (ss >> result) return result;
return default_value;
};

// If the value starts with `$$`, check for a subsequent `{...}`
if (value.size() >= 3 && value[0] == '$' && value[1] == '$')
{
// If after stripping the first `$`, the string format is like `${VAR}`, treat it as a plain string and don't resolve.
if (value[2] == '{' && value[value.size() - 1] == '}')
{
value = value.substr(1);
return convert_str_to_t(value);
}
else return convert_str_to_t(value);
}

// Check if the value is an environment variable reference
if(value.size() >= 2 && value[0] == '$' && value[1] == '{' && value[value.size() - 1] == '}')
{
// Format: ${ENV_VAR_NAME}
std::string env_var = value.substr(2, value.size() - 3);

const char* env_value = std::getenv(env_var.c_str()); // Get the environment variable value
if(env_value) return convert_str_to_t(env_value);

return default_value;
}

// If it's not an environment variable reference, return the value as is
return node.as<T>();
}

Expand Down Expand Up @@ -118,10 +153,10 @@ class yaml_helper
* The provided key string can navigate the document in its
* nested nodes, with arbitrary depth. The key string follows
* this regular language:
*
*
* Key := NodeKey ('.' NodeKey)*
* NodeKey := (any)+ ('[' (integer)+ ']')*
*
*
* Some examples of accepted key strings:
* - NodeName
* - ListValue[3].subvalue
Expand All @@ -146,7 +181,7 @@ class yaml_helper
if (i > 0 && nodeKey.empty() && key[i - 1] != '.')
{
throw std::runtime_error(
"Parsing error: expected '.' character at pos "
"Parsing error: expected '.' character at pos "
+ std::to_string(i - 1));
}
nodeKey += c;
Expand All @@ -157,7 +192,7 @@ class yaml_helper
if (nodeKey.empty())
{
throw std::runtime_error(
"Parsing error: unexpected character at pos "
"Parsing error: unexpected character at pos "
+ std::to_string(i));
}
ret.reset(ret[nodeKey]);
Expand All @@ -181,7 +216,7 @@ class yaml_helper
throw std::runtime_error("Config error at key \"" + key + "\": " + std::string(e.what()));
}
}

template<typename T>
void get_sequence_from_node(T& ret, const YAML::Node& node) const
{
Expand Down Expand Up @@ -250,7 +285,7 @@ namespace YAML {
default:
break;
}

return true;
}
};
Expand Down