new(userspace,unit_tests): port merge-strategy to be a yaml map.

Merge-strategy for included config files must now be
specified as yaml map of the form:
- path: foo
  strategy: bar

If `strategy` is omitted, or the old `string-only` form is used,
`append` strategy is enforced.

Signed-off-by: Federico Di Pierro <nierro92@gmail.com>
This commit is contained in:
Federico Di Pierro 2025-04-29 10:53:22 +02:00 committed by poiana
parent 630167d9ad
commit 08a00609a1
6 changed files with 243 additions and 41 deletions

View File

@ -144,22 +144,30 @@
#
# Like for 'rules_files', specifying a folder will load all the configs files present in it in a lexicographical order.
#
# 3 merge-strategies are available: `append`, `override` and `add-only`.
# The default merge-strategy is `append`:
# 3 merge-strategies are available:
# `append` (default):
# * existing sequence keys will be appended
# * existing scalar keys will be overridden
# * non-existing keys will be added
# To enable the `override` merge-strategy, `@` must be prepended to the entry, eg: "@/$HOME/override.yaml".
# `override`:
# * existing keys will be overridden
# * non-existing keys will be added
# Please note that since `@` is a reserved character by yaml spec, you need to quote the whole string.
# To enable the `add-only` merge-strategy, `+` must be prepended to the entry, eg: "+/$HOME/append.yaml".
# `add-only`:
# * existing keys will be ignored
# * non-existing keys will be added
#
# When a merge-strategy is enabled for a folder entry, all of the included config files will use that merge-strategy.
# Each item on the list can be either a yaml map or a simple string.
# The simple string will be interpreted as the config file path, and the `append` merge-strategy will be enforced.
# When the item is a yaml map instead, it will be of the form: ` path: foo\n strategy: X`.
# When `strategy` is omitted, once again `append` is used.
#
# When a merge-strategy is enabled for a folder entry, all the included config files will use that merge-strategy.
config_files:
- /etc/falco/config.d
# Example of config file specified as yaml map with strategy made explicit.
#- path: $HOME/falco_local_configs/
# strategy: add-only
# [Stable] `watch_config_files`
#

View File

@ -245,8 +245,7 @@ TEST(Configuration, configuration_config_files_override) {
std::filesystem::remove("conf_3.yaml");
}
TEST(Configuration, configuration_config_files_sequence) {
/* Test that included config files are able to override configs from main file */
TEST(Configuration, configuration_config_files_sequence_strategy_default) {
const std::string main_conf_yaml = yaml_helper::configs_key +
":\n"
" - conf_2.yaml\n" // default merge-strategy: append
@ -306,11 +305,72 @@ TEST(Configuration, configuration_config_files_sequence) {
std::filesystem::remove("conf_3.yaml");
}
TEST(Configuration, configuration_config_files_sequence_override) {
/* Test that included config files are able to override configs from main file */
TEST(Configuration, configuration_config_files_sequence_strategy_append) {
const std::string main_conf_yaml = yaml_helper::configs_key +
":\n"
" - '@conf_2.yaml'\n" // merge-strategy: override
" - path: conf_2.yaml\n"
" strategy: append\n"
" - conf_3.yaml\n"
"foo: [ bar ]\n"
"base_value:\n"
" id: 1\n"
" name: foo\n";
const std::string conf_yaml_2 =
"foo: [ bar2 ]\n" // append to foo sequence
"base_value_2:\n"
" id: 2\n";
const std::string conf_yaml_3 =
"base_value:\n" // override base_value
" id: 3\n";
std::ofstream outfile("main.yaml");
outfile << main_conf_yaml;
outfile.close();
outfile.open("conf_2.yaml");
outfile << conf_yaml_2;
outfile.close();
outfile.open("conf_3.yaml");
outfile << conf_yaml_3;
outfile.close();
std::vector<std::string> cmdline_config_options;
falco_configuration falco_config;
config_loaded_res res;
ASSERT_NO_THROW(res = falco_config.init_from_file("main.yaml", cmdline_config_options));
// main + conf_2 + conf_3
ASSERT_EQ(res.size(), 3);
ASSERT_TRUE(falco_config.m_config.is_defined("foo"));
std::vector<std::string> foos;
auto expected_foos = std::vector<std::string>{"bar", "bar2"};
ASSERT_NO_THROW(falco_config.m_config.get_sequence<std::vector<std::string>>(foos, "foo"));
ASSERT_EQ(foos.size(), 2); // 2 elements in `foo` sequence because we appended to it
for(size_t i = 0; i < foos.size(); ++i) {
EXPECT_EQ(foos[i], expected_foos[i])
<< "Vectors foo's and expected_foo's differ at index " << i;
}
ASSERT_TRUE(falco_config.m_config.is_defined("base_value.id"));
ASSERT_EQ(falco_config.m_config.get_scalar<int>("base_value.id", 0), 3); // overridden!
ASSERT_FALSE(falco_config.m_config.is_defined(
"base_value.name")); // no more present since entire `base_value` block was overridden
ASSERT_TRUE(falco_config.m_config.is_defined("base_value_2.id"));
ASSERT_EQ(falco_config.m_config.get_scalar<int>("base_value_2.id", 0), 2);
ASSERT_FALSE(falco_config.m_config.is_defined("base_value_3.id")); // not defined
std::filesystem::remove("main.yaml");
std::filesystem::remove("conf_2.yaml");
std::filesystem::remove("conf_3.yaml");
}
TEST(Configuration, configuration_config_files_sequence_strategy_override) {
const std::string main_conf_yaml = yaml_helper::configs_key +
":\n"
" - path: conf_2.yaml\n"
" strategy: override\n"
" - conf_3.yaml\n"
"foo: [ bar ]\n"
"base_value:\n"
@ -367,11 +427,12 @@ TEST(Configuration, configuration_config_files_sequence_override) {
std::filesystem::remove("conf_3.yaml");
}
TEST(Configuration, configuration_config_files_sequence_addonly) {
TEST(Configuration, configuration_config_files_sequence_strategy_addonly) {
/* Test that included config files are able to override configs from main file */
const std::string main_conf_yaml = yaml_helper::configs_key +
":\n"
" - +conf_2.yaml\n" // merge-strategy: add-only
" - path: conf_2.yaml\n"
" strategy: add-only\n"
" - conf_3.yaml\n"
"foo: [ bar ]\n"
"base_value:\n"
@ -429,6 +490,71 @@ TEST(Configuration, configuration_config_files_sequence_addonly) {
std::filesystem::remove("conf_3.yaml");
}
TEST(Configuration, configuration_config_files_sequence_wrong_strategy) {
const std::string main_conf_yaml = yaml_helper::configs_key +
":\n"
" - path: conf_2.yaml\n"
" strategy: wrong\n"
" - conf_3.yaml\n"
"foo: [ bar ]\n"
"base_value:\n"
" id: 1\n"
" name: foo\n";
const std::string conf_yaml_2 =
"foo: [ bar2 ]\n" // append to foo sequence
"base_value_2:\n"
" id: 2\n";
const std::string conf_yaml_3 =
"base_value:\n" // override base_value
" id: 3\n";
std::ofstream outfile("main.yaml");
outfile << main_conf_yaml;
outfile.close();
outfile.open("conf_2.yaml");
outfile << conf_yaml_2;
outfile.close();
outfile.open("conf_3.yaml");
outfile << conf_yaml_3;
outfile.close();
std::vector<std::string> cmdline_config_options;
falco_configuration falco_config;
config_loaded_res res;
ASSERT_NO_THROW(res = falco_config.init_from_file("main.yaml", cmdline_config_options));
// main
ASSERT_EQ(res.size(), 3);
auto validation = res["main.yaml"];
// Since we are using a wrong strategy, the validation should fail
// but the enforced strategy should be "append"
ASSERT_NE(validation, yaml_helper::validation_ok);
ASSERT_TRUE(falco_config.m_config.is_defined("foo"));
std::vector<std::string> foos;
auto expected_foos = std::vector<std::string>{"bar", "bar2"};
ASSERT_NO_THROW(falco_config.m_config.get_sequence<std::vector<std::string>>(foos, "foo"));
ASSERT_EQ(foos.size(), 2); // 2 elements in `foo` sequence because we appended to it
for(size_t i = 0; i < foos.size(); ++i) {
EXPECT_EQ(foos[i], expected_foos[i])
<< "Vectors foo's and expected_foo's differ at index " << i;
}
ASSERT_TRUE(falco_config.m_config.is_defined("base_value.id"));
ASSERT_EQ(falco_config.m_config.get_scalar<int>("base_value.id", 0), 3); // overridden!
ASSERT_FALSE(falco_config.m_config.is_defined(
"base_value.name")); // no more present since entire `base_value` block was overridden
ASSERT_TRUE(falco_config.m_config.is_defined("base_value_2.id"));
ASSERT_EQ(falco_config.m_config.get_scalar<int>("base_value_2.id", 0), 2);
ASSERT_FALSE(falco_config.m_config.is_defined("base_value_3.id")); // not defined
std::filesystem::remove("main.yaml");
std::filesystem::remove("conf_2.yaml");
std::filesystem::remove("conf_3.yaml");
}
TEST(Configuration, configuration_config_files_unexistent) {
/* Test that including an unexistent file just skips it */
const std::string main_conf_yaml = yaml_helper::configs_key +

View File

@ -85,25 +85,31 @@ public:
inline static const std::string validation_failed = "failed";
inline static const std::string validation_none = "none";
enum include_files_strategy {
enum config_files_strategy {
STRATEGY_APPEND, // append to existing sequence keys, override scalar keys and add new ones
STRATEGY_OVERRIDE, // override existing keys (sequences too) and add new ones
STRATEGY_ADDONLY // only add new keys and ignore existing ones
};
static inline enum include_files_strategy get_include_file_strategy(
std::string& include_file_name) {
if(include_file_name.length() > 0) {
if(include_file_name[0] == '+') {
include_file_name.erase(0, 1);
return STRATEGY_ADDONLY;
}
if(include_file_name[0] == '@') {
include_file_name.erase(0, 1);
return STRATEGY_OVERRIDE;
}
static enum config_files_strategy strategy_from_string(const std::string& strategy) {
if(strategy == "override") {
return yaml_helper::STRATEGY_OVERRIDE;
}
if(strategy == "add-only") {
return yaml_helper::STRATEGY_ADDONLY;
}
return yaml_helper::STRATEGY_APPEND;
}
static std::string strategy_to_string(const enum config_files_strategy strategy) {
switch(strategy) {
case yaml_helper::STRATEGY_OVERRIDE:
return "override";
case yaml_helper::STRATEGY_ADDONLY:
return "add-only";
default:
return "append";
}
return STRATEGY_APPEND;
}
/**
@ -158,7 +164,7 @@ public:
}
void include_config_file(const std::string& include_file_path,
enum include_files_strategy strategy = STRATEGY_APPEND,
enum config_files_strategy strategy = STRATEGY_APPEND,
const nlohmann::json& schema = {},
std::vector<std::string>* schema_warnings = nullptr) {
auto loaded_nodes = load_from_file_int(include_file_path, schema, schema_warnings);

View File

@ -38,7 +38,30 @@ const char config_schema_string[] = LONG_STRING_CONST(
"config_files": {
"type": "array",
"items": {
"type": "string"
"oneOf": [
{
"type": "string"
},
{
"type": "object",
"properties": {
"path": {
"type": "string"
},
"strategy": {
"type": "string",
"enum": [
"append",
"override",
"add-only"
]
}
},
"required": [
"path"
]
}
]
}
},
"watch_config_files": {

View File

@ -162,13 +162,12 @@ void falco_configuration::merge_config_files(const std::string &config_name,
m_loaded_configs_filenames.push_back(config_name);
const auto ppath = std::filesystem::path(config_name);
// Parse files to be included
std::vector<std::string> include_files;
m_config.get_sequence<std::vector<std::string>>(include_files, yaml_helper::configs_key);
for(auto include_file : include_files) {
// This can modify include_file by dropping the '+/@' prefixes.
const auto merge_strategy = yaml_helper::get_include_file_strategy(include_file);
auto include_file_path = std::filesystem::path(include_file);
std::list<falco_configuration::config_files_config> include_files;
m_config.get_sequence<std::list<falco_configuration::config_files_config>>(
include_files,
yaml_helper::configs_key);
for(const auto &include_file : include_files) {
auto include_file_path = std::filesystem::path(include_file.m_path);
if(include_file_path == ppath) {
throw std::logic_error("Config error: '" + yaml_helper::configs_key +
"' directive tried to recursively include main config file: " +
@ -179,15 +178,15 @@ void falco_configuration::merge_config_files(const std::string &config_name,
continue;
}
if(std::filesystem::is_regular_file(include_file_path)) {
m_loaded_configs_filenames.push_back(include_file);
m_config.include_config_file(include_file,
merge_strategy,
m_loaded_configs_filenames.push_back(include_file.m_path);
m_config.include_config_file(include_file.m_path,
include_file.m_strategy,
m_config_schema,
&validation_status);
// Only report top most schema validation status
res[include_file] = validation_status[0];
res[include_file.m_path] = validation_status[0];
} else if(std::filesystem::is_directory(include_file_path)) {
m_loaded_configs_folders.push_back(include_file);
m_loaded_configs_folders.push_back(include_file.m_path);
std::vector<std::string> v;
const auto it_options = std::filesystem::directory_options::follow_directory_symlink |
std::filesystem::directory_options::skip_permission_denied;
@ -200,7 +199,7 @@ void falco_configuration::merge_config_files(const std::string &config_name,
std::sort(v.begin(), v.end());
for(const auto &f : v) {
m_config.include_config_file(f,
merge_strategy,
include_file.m_strategy,
m_config_schema,
&validation_status);
// Only report top most schema validation status

View File

@ -54,6 +54,11 @@ public:
std::string m_open_params;
};
struct config_files_config {
std::string m_path;
yaml_helper::config_files_strategy m_strategy;
};
struct kmod_config {
int16_t m_buf_size_preset;
bool m_drop_failed_exit;
@ -425,4 +430,39 @@ struct convert<falco_configuration::plugin_config> {
return true;
}
};
template<>
struct convert<falco_configuration::config_files_config> {
static Node encode(const falco_configuration::config_files_config& rhs) {
Node node;
node["path"] = rhs.m_path;
node["strategy"] = yaml_helper::strategy_to_string(rhs.m_strategy);
return node;
}
static bool decode(const Node& node, falco_configuration::config_files_config& rhs) {
if(!node.IsMap()) {
// Single string mode defaults to append strategy
rhs.m_path = node.as<std::string>();
rhs.m_strategy = yaml_helper::STRATEGY_APPEND;
return true;
}
// Path is required
if(!node["path"]) {
return false;
}
rhs.m_path = node["path"].as<std::string>();
// Strategy is not required
if(!node["strategy"]) {
rhs.m_strategy = yaml_helper::STRATEGY_APPEND;
} else {
std::string strategy = node["strategy"].as<std::string>();
rhs.m_strategy = yaml_helper::strategy_from_string(strategy);
}
return true;
}
};
} // namespace YAML