diff --git a/agent-control/src/agent_type/runtime_config/on_host/filesystem.rs b/agent-control/src/agent_type/runtime_config/on_host/filesystem.rs index d49a31e428..fac56df340 100644 --- a/agent-control/src/agent_type/runtime_config/on_host/filesystem.rs +++ b/agent-control/src/agent_type/runtime_config/on_host/filesystem.rs @@ -242,8 +242,8 @@ impl Templateable for TemplateableValue { "Could not parse templated directory items as YAML: {e}" )) })?; - // Convert the serde_yaml::Value (i.e. the file contents) to String + // Convert the serde_yaml::Value (i.e. the file contents) to String map_string_value .into_iter() .map(|(k, v)| Ok((k, output_string(v)?))) diff --git a/agent-control/src/bin/main_config_migrate.rs b/agent-control/src/bin/main_config_migrate.rs index 90d3f92fe6..adabf3029c 100644 --- a/agent-control/src/bin/main_config_migrate.rs +++ b/agent-control/src/bin/main_config_migrate.rs @@ -1,38 +1,32 @@ use newrelic_agent_control::agent_control::config_repository::store::AgentControlConfigStore; -use newrelic_agent_control::agent_control::defaults::{ - AGENT_CONTROL_DATA_DIR, AGENT_CONTROL_LOCAL_DATA_DIR, AGENT_CONTROL_LOG_DIR, SUB_AGENT_DIR, -}; use newrelic_agent_control::config_migrate::cli::Cli; use newrelic_agent_control::config_migrate::migration::agent_config_getter::AgentConfigGetter; -use newrelic_agent_control::config_migrate::migration::config::MigrationConfig; +use newrelic_agent_control::config_migrate::migration::config::{MappingType, MigrationConfig}; use newrelic_agent_control::config_migrate::migration::converter::ConfigConverter; -use newrelic_agent_control::config_migrate::migration::defaults::NEWRELIC_INFRA_AGENT_TYPE_CONFIG_MAPPING; use newrelic_agent_control::config_migrate::migration::migrator::{ConfigMigrator, MigratorError}; use newrelic_agent_control::config_migrate::migration::persister::legacy_config_renamer::LegacyConfigRenamer; use newrelic_agent_control::config_migrate::migration::persister::values_persister_file::ValuesPersisterFile; use newrelic_agent_control::instrumentation::tracing::{TracingConfig, try_init_tracing}; use newrelic_agent_control::values::file::ConfigRepositoryFile; use std::error::Error; -use std::path::PathBuf; use std::sync::Arc; use tracing::{debug, info, warn}; fn main() -> Result<(), Box> { - let tracing_config = TracingConfig::from_logging_path(PathBuf::from(AGENT_CONTROL_LOG_DIR)); + let cli = Cli::load(); + let tracing_config = TracingConfig::from_logging_path(cli.log_dir()); let _tracer = try_init_tracing(tracing_config); info!("Starting config conversion tool..."); - let config: MigrationConfig = MigrationConfig::parse(NEWRELIC_INFRA_AGENT_TYPE_CONFIG_MAPPING)?; + let config = MigrationConfig::parse(&cli.get_migration_config_str()?)?; - let cli = Cli::init_config_migrate_cli(); - let remote_dir = PathBuf::from(AGENT_CONTROL_DATA_DIR); - let vr = ConfigRepositoryFile::new(cli.local_data_dir(), remote_dir); + let vr = ConfigRepositoryFile::new(cli.local_data_dir(), cli.remote_data_dir()); let sa_local_config_loader = AgentControlConfigStore::new(Arc::new(vr)); let config_migrator = ConfigMigrator::new( ConfigConverter::default(), AgentConfigGetter::new(sa_local_config_loader), - ValuesPersisterFile::new(PathBuf::from(AGENT_CONTROL_LOCAL_DATA_DIR).join(SUB_AGENT_DIR)), + ValuesPersisterFile::new(cli.local_sub_agent_data_dir()), ); let legacy_config_renamer = LegacyConfigRenamer::default(); @@ -41,11 +35,15 @@ fn main() -> Result<(), Box> { debug!("Checking configurations for {}", cfg.agent_type_fqn); match config_migrator.migrate(&cfg) { Ok(_) => { - for (_, dir_path) in cfg.dirs_map { - legacy_config_renamer.rename_path(dir_path.path.as_path())?; - } - for (_, file_path) in cfg.files_map { - legacy_config_renamer.rename_path(file_path.as_path())?; + for (_, mapping_type) in cfg.filesystem_mappings { + match mapping_type { + MappingType::Dir(dir_path) => { + legacy_config_renamer.rename_path(dir_path.dir_path.as_path())? + } + MappingType::File(file_path) => { + legacy_config_renamer.rename_path(file_path.as_path())? + } + } } debug!("Classic config files and paths renamed"); } diff --git a/agent-control/src/config_migrate/cli.rs b/agent-control/src/config_migrate/cli.rs index 9880170de9..13d81a437a 100644 --- a/agent-control/src/config_migrate/cli.rs +++ b/agent-control/src/config_migrate/cli.rs @@ -1,29 +1,67 @@ -use crate::agent_control::defaults::AGENT_CONTROL_LOCAL_DATA_DIR; +use crate::{ + agent_control::defaults::{ + AGENT_CONTROL_DATA_DIR, AGENT_CONTROL_LOCAL_DATA_DIR, AGENT_CONTROL_LOG_DIR, SUB_AGENT_DIR, + }, + config_migrate::migration::defaults::NEWRELIC_INFRA_AGENT_TYPE_CONFIG_MAPPING, +}; use clap::Parser; -use std::path::PathBuf; +use std::{error::Error, fs, path::PathBuf}; #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] // Read from `Cargo.toml` pub struct Cli { - /// Overrides the default local configuration path `/etc/newrelic-agent-control/`. - #[cfg(debug_assertions)] + /// Local data path used by Agent Control. + #[arg(long, default_value_os_t = PathBuf::from(AGENT_CONTROL_LOCAL_DATA_DIR))] + local_dir: PathBuf, + + /// Remote data path used by Agent Control. + #[arg(long, default_value_os_t = PathBuf::from(AGENT_CONTROL_DATA_DIR))] + remote_dir: PathBuf, + + /// Logs path used by Agent Control. + #[arg(long, default_value_os_t = PathBuf::from(AGENT_CONTROL_LOG_DIR))] + logs_dir: PathBuf, + + /// Provides an external configuration mapping for the migration of agents to Agent Control. #[arg(long)] - local_dir: Option, + migration_config_file: Option, } impl Cli { /// Parses command line arguments - pub fn init_config_migrate_cli() -> Self { + pub fn load() -> Self { // Get command line args Self::parse() } pub fn local_data_dir(&self) -> PathBuf { - #[cfg(debug_assertions)] - if let Some(path) = &self.local_dir { - return path.clone(); - } + self.local_dir.to_path_buf() + } + + pub fn local_sub_agent_data_dir(&self) -> PathBuf { + self.local_dir.join(SUB_AGENT_DIR) + } + + pub fn remote_data_dir(&self) -> PathBuf { + self.remote_dir.to_path_buf() + } - PathBuf::from(AGENT_CONTROL_LOCAL_DATA_DIR) + pub fn log_dir(&self) -> PathBuf { + self.logs_dir.to_path_buf() + } + + pub fn get_migration_config_str(&self) -> Result> { + if let Some(path) = &self.migration_config_file { + fs::read_to_string(path).map_err(|e| { + format!( + "Could not read provided migration config file ({}): {}", + path.display(), + e + ) + .into() + }) + } else { + Ok(NEWRELIC_INFRA_AGENT_TYPE_CONFIG_MAPPING.to_owned()) + } } } diff --git a/agent-control/src/config_migrate/migration/agent_value_spec.rs b/agent-control/src/config_migrate/migration/agent_value_spec.rs index 19519bb1e7..183262afcf 100644 --- a/agent-control/src/config_migrate/migration/agent_value_spec.rs +++ b/agent-control/src/config_migrate/migration/agent_value_spec.rs @@ -37,7 +37,7 @@ pub fn from_fqn_and_value( fqn: AgentTypeFieldFQN, value: AgentValueSpec, ) -> HashMap { - let cloned_fqn = fqn.clone().as_string(); + let cloned_fqn = fqn.to_string(); let mut parts: Vec<&str> = cloned_fqn.rsplit(FILE_SEPARATOR).collect(); let first = parts.last().unwrap().to_string(); parts.remove(parts.len() - 1); diff --git a/agent-control/src/config_migrate/migration/config.rs b/agent-control/src/config_migrate/migration/config.rs index 436478fe6e..aa46b11b4d 100644 --- a/agent-control/src/config_migrate/migration/config.rs +++ b/agent-control/src/config_migrate/migration/config.rs @@ -1,10 +1,10 @@ -use regex::Regex; use serde::Deserialize; use serde_yaml::Error; use std::collections::HashMap; +use std::ffi::OsString; use std::fmt::{Display, Formatter}; use std::hash::{Hash, Hasher}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use thiserror::Error; use tracing::error; @@ -29,12 +29,6 @@ pub enum MigrationConfigError { #[derive(Debug, Clone, Deserialize)] pub struct AgentTypeFieldFQN(String); -impl AgentTypeFieldFQN { - pub fn as_string(&self) -> String { - self.0.clone() - } -} - impl Display for AgentTypeFieldFQN { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0.as_str()) @@ -88,38 +82,21 @@ impl Hash for AgentTypeFieldFQN { } } -pub struct FileMap { - pub file_path: FilePath, - pub agent_type_fqn: AgentTypeID, -} - -pub struct DirMap { - pub file_path: FilePath, - pub agent_type_fqn: AgentTypeID, -} - #[derive(Clone, Debug, PartialEq, Deserialize)] pub struct DirInfo { - pub path: FilePath, - pub filename_patterns: Vec, + pub dir_path: FilePath, + pub extensions: Vec, } impl DirInfo { - pub fn valid_filename(&self, filename: &str) -> bool { - for filename_pattern in &self.filename_patterns { - let re = Regex::new(filename_pattern) - .unwrap_or_else(|_| panic!("invalid filename_pattern: {filename_pattern}")); - if re.is_match(filename) { - return true; - } - } - false + pub fn valid_filename(&self, filename: impl AsRef) -> bool { + self.extensions + .iter() + .map(OsString::from) + .any(|ext| filename.as_ref().extension().is_some_and(|e| e == ext)) } } -pub type FilesMap = HashMap; -pub type DirsMap = HashMap; - #[derive(Debug, PartialEq, Clone, Deserialize)] pub struct MigrationConfig { pub configs: Vec, @@ -158,34 +135,25 @@ impl MigrationConfig { pub struct MigrationAgentConfig { #[serde(deserialize_with = "AgentTypeID::deserialize_fqn")] pub agent_type_fqn: AgentTypeID, - pub files_map: FilesMap, - pub dirs_map: DirsMap, + pub filesystem_mappings: HashMap, pub next: Option, } -impl MigrationAgentConfig { - pub(crate) fn get_agent_type_fqn(&self) -> AgentTypeID { - self.agent_type_fqn.clone() - } +#[derive(Debug, PartialEq, Clone, Deserialize)] +#[serde(untagged)] +pub enum MappingType { + File(PathBuf), + Dir(DirInfo), } -impl MigrationAgentConfig { - pub fn get_file(&self, fqn_to_check: AgentTypeFieldFQN) -> Option { - for (fqn, path) in self.files_map.iter() { - if *fqn == fqn_to_check { - return Some(path.clone()); - } - } - None +impl From for MappingType { + fn from(value: DirInfo) -> Self { + MappingType::Dir(value) } - - pub fn get_dir(&self, fqn_to_check: AgentTypeFieldFQN) -> Option { - for (fqn, dir_info) in self.dirs_map.iter() { - if *fqn == fqn_to_check { - return Some(dir_info.clone()); - } - } - None +} +impl> From

for MappingType { + fn from(value: P) -> Self { + MappingType::File(value.into()) } } @@ -193,7 +161,9 @@ impl MigrationAgentConfig { mod tests { use crate::agent_type::agent_type_id::AgentTypeID; - use crate::config_migrate::migration::config::{DirInfo, FilePath, MigrationConfig}; + use crate::config_migrate::migration::config::{ + DirInfo, FilePath, MappingType, MigrationConfig, + }; use crate::config_migrate::migration::defaults::NEWRELIC_INFRA_AGENT_TYPE_CONFIG_MAPPING; #[test] @@ -202,62 +172,60 @@ mod tests { configs: - agent_type_fqn: newrelic/com.newrelic.infrastructure:0.0.2 - files_map: + filesystem_mappings: config_agent: /etc/newrelic-infra.yml - dirs_map: config_ohis: - path: /etc/newrelic-infra/integrations.d - filename_patterns: - - ".*\\.ya?ml$" + dir_path: /etc/newrelic-infra/integrations.d + extensions: + - "yaml" + - "yml" logging: - path: /etc/newrelic-infra/logging.d - filename_patterns: - - ".*\\.ya?ml$" + dir_path: /etc/newrelic-infra/logging.d + extensions: + - "yaml" + - "yml" - agent_type_fqn: newrelic/com.newrelic.another:1.0.0 - files_map: + filesystem_mappings: config_another: /etc/another.yml - dirs_map: - agent_type_fqn: newrelic/com.newrelic.infrastructure:1.0.1 - files_map: + filesystem_mappings: config_agent: /etc/newrelic-infra.yml - dirs_map: config_integrations: - path: /etc/newrelic-infra/integrations.d - filename_patterns: - - ".*\\.ya?ml$" - + dir_path: /etc/newrelic-infra/integrations.d + extensions: + - "yaml" + - "yml" config_logging: - path: /etc/newrelic-infra/logging.d - filename_patterns: - - ".*\\.ya?ml$" + dir_path: /etc/newrelic-infra/logging.d + extensions: + - "yaml" + - "yml" - agent_type_fqn: francisco-partners/com.newrelic.another:0.0.2 - files_map: + filesystem_mappings: config_another: /etc/another.yml - dirs_map: - agent_type_fqn: newrelic/com.newrelic.infrastructure:0.1.2 - files_map: + filesystem_mappings: config_agent: /etc/newrelic-infra.yml - dirs_map: config_integrations: - path: /etc/newrelic-infra/integrations.d - filename_patterns: - - ".*\\.ya?ml$" - + dir_path: /etc/newrelic-infra/integrations.d + extensions: + - "yaml" + - "yml" config_logging: - path: /etc/newrelic-infra/logging.d - filename_patterns: - - ".*\\.ya?ml$" + dir_path: /etc/newrelic-infra/logging.d + extensions: + - "yaml" + - "yml" - agent_type_fqn: newrelic/com.newrelic.another:0.0.1 - files_map: + filesystem_mappings: config_another: /etc/another.yml - dirs_map: "#; let expected_fqns_in_order = [ @@ -311,8 +279,12 @@ configs: [] #[test] fn test_dir_info() { let dir_info = DirInfo { - filename_patterns: vec![String::from(".*\\.ya?ml$"), String::from(".*\\.otro$")], - path: FilePath::from("some/path"), + extensions: vec![ + String::from("yaml"), + String::from("yml"), + String::from("otro"), + ], + dir_path: FilePath::from("some/path"), }; assert!(dir_info.valid_filename("something.yaml")); @@ -328,13 +300,21 @@ configs: [] let migration_config: MigrationConfig = MigrationConfig::parse(NEWRELIC_INFRA_AGENT_TYPE_CONFIG_MAPPING).unwrap(); - for config in migration_config.configs { - for dir_map in config.dirs_map { - assert!(dir_map.1.valid_filename("something.yaml")); - assert!(dir_map.1.valid_filename("something.yml")); - assert!(!dir_map.1.valid_filename("something.yml.sample")); - assert!(!dir_map.1.valid_filename("something.yaml.sample")); - assert!(!dir_map.1.valid_filename("something.yoml")); + for config in migration_config.configs.into_iter() { + let dir_mappings = + config + .filesystem_mappings + .into_iter() + .filter_map(|(_, v)| match v { + MappingType::Dir(dir) => Some(dir), + _ => None, + }); + for dir_map in dir_mappings { + assert!(dir_map.valid_filename("something.yaml")); + assert!(dir_map.valid_filename("something.yml")); + assert!(!dir_map.valid_filename("something.yml.sample")); + assert!(!dir_map.valid_filename("something.yaml.sample")); + assert!(!dir_map.valid_filename("something.yoml")); } } } diff --git a/agent-control/src/config_migrate/migration/converter.rs b/agent-control/src/config_migrate/migration/converter.rs index 82475bc808..ee5e347703 100644 --- a/agent-control/src/config_migrate/migration/converter.rs +++ b/agent-control/src/config_migrate/migration/converter.rs @@ -1,23 +1,18 @@ -use crate::agent_control::run::Environment; -use crate::agent_type::agent_type_registry::{AgentRegistry, AgentRepositoryError}; -use crate::agent_type::embedded_registry::EmbeddedRegistry; -use crate::agent_type::variable::constraints::VariableConstraints; -use crate::agent_type::variable::variable_type::VariableType; -use crate::config_migrate::migration::agent_value_spec::AgentValueSpec::AgentValueSpecEnd; -use crate::config_migrate::migration::agent_value_spec::{ - AgentValueError, AgentValueSpec, from_fqn_and_value, merge_agent_values, +use crate::agent_type::agent_type_registry::AgentRepositoryError; +use crate::config_migrate::migration::config::MappingType; +use crate::config_migrate::migration::{ + agent_value_spec::AgentValueError, + config::{DirInfo, MigrationAgentConfig}, }; -use crate::config_migrate::migration::config::{ - AgentTypeFieldFQN, DirInfo, FilePath, MigrationAgentConfig, -}; -use crate::config_migrate::migration::config::{FILE_SEPARATOR, FILE_SEPARATOR_REPLACE}; -use crate::config_migrate::migration::converter::ConversionError::RequiredFileMappingNotFoundError; -use crate::sub_agent::effective_agents_assembler::{AgentTypeDefinitionError, build_agent_type}; +use crate::sub_agent::effective_agents_assembler::AgentTypeDefinitionError; use fs::LocalFile; use fs::file_reader::{FileReader, FileReaderError}; +use regex::Regex; use std::collections::HashMap; +use std::path::Path; +use std::sync::OnceLock; use thiserror::Error; -use tracing::{debug, error}; +use tracing::error; #[derive(Error, Debug)] pub enum ConversionError { @@ -29,103 +24,574 @@ pub enum ConversionError { AgentValueError(#[from] AgentValueError), #[error("{0}")] AgentTypeDefinitionError(#[from] AgentTypeDefinitionError), - #[error("cannot find required file map")] - RequiredFileMappingNotFoundError, + #[error("cannot find required file map: {0}")] + RequiredFileMappingNotFoundError(String), + #[error("cannot find required dir map: {0}")] + RequiredDirMappingNotFoundError(String), + #[error("deserializing YAML: {0}")] + InvalidYamlConfiguration(#[from] serde_yaml::Error), } -pub struct ConfigConverter { - agent_registry: R, +pub struct ConfigConverter { file_reader: F, } -impl Default for ConfigConverter { +impl Default for ConfigConverter { fn default() -> Self { ConfigConverter { - agent_registry: EmbeddedRegistry::default(), file_reader: LocalFile, } } } #[cfg_attr(test, mockall::automock)] -impl ConfigConverter { +impl ConfigConverter { pub fn convert( &self, migration_agent_config: &MigrationAgentConfig, - ) -> Result, ConversionError> { - let agent_type_definition = self - .agent_registry - .get(&migration_agent_config.get_agent_type_fqn().to_string())?; - - let agent_type = build_agent_type( - agent_type_definition, - &Environment::OnHost, - &VariableConstraints::default(), - )?; - let mut agent_values_specs: Vec> = Vec::new(); - for (normalized_fqn, spec) in agent_type.variables.flatten().iter() { - let agent_type_fqn: AgentTypeFieldFQN = normalized_fqn.into(); - match spec.kind() { - VariableType::File(_) => { - // look for file mapping, if not found and required throw an error - let file_map = migration_agent_config.get_file(agent_type_fqn.clone()); - if spec.is_required() && file_map.is_none() { - return Err(RequiredFileMappingNotFoundError); + ) -> Result, ConversionError> { + // Parse first file mappings (supposedly only a single infra-agent config) + // then directory mappings (integrations and logs). + // Both file and directory mappings are key-value structures. I assume + // the keys are the intended variable names for the agent type, and the values + // the places where the contents of these variables will be read from, namely + // the files and directory paths respectively. They will be parsed to YAML or + // key-value mappings (file as string, YAML) as appropriate. + + let file_reader = &self.file_reader; + + migration_agent_config + .filesystem_mappings + .iter() + .map(|(k, v)| match v { + MappingType::File(path) => Ok(( + k.to_string(), + retrieve_file_mapping_value(file_reader, path)?, + )), + MappingType::Dir(dir_info) => Ok(( + k.to_string(), + retrieve_dir_mapping_values(file_reader, dir_info)?, + )), + }) + .collect::, ConversionError>>() + } +} + +fn retrieve_file_mapping_value( + file_reader: &F, + file_path: &Path, +) -> Result { + let yaml_value = file_reader.read(file_path)?; + let parsed_yaml: serde_yaml::Value = serde_yaml::from_str(&yaml_value)?; + Ok(parsed_yaml) +} + +fn retrieve_dir_mapping_values( + file_reader: &F, + dir_info: &DirInfo, +) -> Result { + let valid_extension_files = file_reader + .dir_entries(&dir_info.dir_path)? + .into_iter() + .filter(|p| dir_info.valid_filename(p)); + + let mut read_files = valid_extension_files.map(|filepath| { + file_reader.read(&filepath).map(|content| { + // If I am here means read was successful (it was a file), so I can unwrap `file_name`. + let filename = filepath.file_name().unwrap().to_string_lossy().to_string(); + (filename, content) + }) + }); + + let read_files = read_files.try_fold(HashMap::new(), |mut acc, read_file| { + let (filepath, content) = read_file?; + let parsed = serde_yaml::from_str::(&process_config_input(content))?; + acc.insert(filepath, parsed); + Ok::<_, ConversionError>(acc) + })?; + + Ok(serde_yaml::to_value(read_files)?) +} + +/// Handles the usage of environment variables in the YAML config files via the special +/// `{{VAR_NAME}}` syntax, by replacing them with a YAML-compatible syntax `'{{VAR_NAME}}'`. +/// (just adding quotes to make it a string). If this pattern is not quoted, the resulting YAML +/// would evaluate to a nested mapping with a single key-null pair, which is not what we want. +/// +/// This uses a regex-based approach. +fn process_config_input(input: String) -> String { + env_var_syntax_regex() + .replace_all(&input, "${pre}'{{${2}}}'${post}") + .to_string() +} + +/// Regex to match {{VAR_NAME}} if not already inside quotes. Used for pre-processing YAML configs +/// coming from the infrastructure-agent which may include this syntax for env var interpolation. +/// +/// The Regex is compiled just once and reused. +fn env_var_syntax_regex() -> &'static Regex { + static RE_ONCE: OnceLock = OnceLock::new(); + RE_ONCE.get_or_init(|| { + Regex::new(r#"(?P

[^'"]|^)\{\{([A-Za-z0-9_]+)\}\}(?P[^'"]|$)"#).unwrap()
+    })
+}
+
+#[cfg(test)]
+mod tests {
+    use std::path::{Path, PathBuf};
+
+    use fs::mock::MockLocalFile;
+    use mockall::{PredicateBooleanExt, predicate};
+    use rstest::rstest;
+
+    use crate::config_migrate::migration::config::DirInfo;
+
+    use super::*;
+
+    const INTEGRATION_1_CONFIG: &str = r#"
+integrations:
+  - name: nri-docker
+    when:
+      feature: docker_enabled
+      file_exists: /var/run/docker.sock
+    interval: 15s
+"#;
+
+    const INTEGRATION_2_CONFIG: &str = r#"
+integrations:
+  - name: nri-docker
+    when:
+      feature: docker_enabled
+      env_exists:
+        FARGATE: "true"
+    interval: 15s
+"#;
+
+    const LOGS_1_CONFIG: &str = r#"
+logs:
+  - name: basic-file
+    file: /var/log/logFile.log
+  - name: file-with-spaces-in-path
+    file: /var/log/folder with spaces/logFile.log
+  - name: file-with-attributes
+    file: /var/log/logFile.log
+    attributes:
+      application: tomcat
+      department: sales
+      maintainer: example@mailprovider.com
+"#;
+
+    const LOGS_2_CONFIG: &str = r#"
+logs:
+  - name: log-files-in-folder
+    file: /var/log/logF*.log
+  - name: log-file-with-long-lines
+    file: /var/log/logFile.log
+    max_line_kb: 256
+  - name: only-records-with-warn-and-error
+    file: /var/log/logFile.log
+    pattern: WARN|ERROR
+"#;
+
+    #[rstest]
+    #[case::no_templates("license_key: {{MY_ENV_VAR}}", "license_key: '{{MY_ENV_VAR}}'")]
+    #[case::multiple_templates(
+        "license_key: {{MY_ENV_VAR}} other {{ANOTHER_ENV}}",
+        "license_key: '{{MY_ENV_VAR}}' other '{{ANOTHER_ENV}}'"
+    )]
+    #[case::no_templates_at_all(
+        "license_key: my_real_license_key",
+        "license_key: my_real_license_key"
+    )]
+    #[case::multiline_yaml_syntax(
+        "license_key: {{MY_ENV_VAR}}\nother_key: value",
+        "license_key: '{{MY_ENV_VAR}}'\nother_key: value"
+    )]
+    #[case::already_quoted("license_key: '{{MY_ENV_VAR}}'", "license_key: '{{MY_ENV_VAR}}'")]
+    #[case::double_quoted(r#"license_key: "{{MY_ENV_VAR}}""#, "license_key: \"{{MY_ENV_VAR}}\"")]
+    fn env_var_interpolation(#[case] input: &str, #[case] output: &str) {
+        let result = process_config_input(input.to_string());
+        assert_eq!(result, output);
+    }
+
+    #[test]
+    fn from_migration_config_to_conversion() {
+        // Sample config
+        let migration_agent_config = MigrationAgentConfig {
+            agent_type_fqn: "newrelic/com.newrelic.infrastructure:0.1.0"
+                .try_into()
+                .unwrap(),
+            filesystem_mappings: HashMap::from([
+                ("config_agent".into(), "/etc/newrelic-infra.yml".into()),
+                (
+                    "config_integrations".into(),
+                    DirInfo {
+                        dir_path: "/etc/newrelic-infra/integrations.d".into(),
+                        extensions: vec!["yml".to_string(), "yaml".to_string()],
                     }
-                    agent_values_specs
-                        .push(self.file_to_agent_value_spec(agent_type_fqn, file_map.unwrap())?)
-                }
-                VariableType::MapStringFile(_) => {
-                    // look for file mapping, if not found and required throw an error
-                    let dir_info = migration_agent_config.get_dir(agent_type_fqn.clone());
-                    if spec.is_required() && dir_info.is_none() {
-                        return Err(RequiredFileMappingNotFoundError);
+                    .into(),
+                ),
+                (
+                    "config_logging".into(),
+                    DirInfo {
+                        dir_path: "/etc/newrelic-infra/logging.d".into(),
+                        extensions: vec!["yml".to_string(), "yaml".to_string()],
                     }
-                    agent_values_specs
-                        .push(self.dir_to_agent_value_spec(agent_type_fqn, dir_info.unwrap())?)
-                }
-                _ => {
-                    debug!("skipping variable {}", agent_type_fqn.as_string())
-                }
-            }
-        }
+                    .into(),
+                ),
+            ]),
+            next: None,
+        };
+
+        let config_agent = "license_key: TESTING_CONVERSION";
+
+        let mut file_reader = MockLocalFile::new();
+
+        // Capture in a sequence the three reads. First the config, then the integrations dir, then the logging dir.
+        file_reader
+            .expect_read()
+            .with(predicate::eq(Path::new("/etc/newrelic-infra.yml")))
+            .times(1)
+            .return_once({
+                let config_agent = config_agent.to_string();
+                move |_| Ok(config_agent)
+            });
+
+        file_reader
+            .expect_dir_entries()
+            .with(predicate::eq(Path::new(
+                "/etc/newrelic-infra/integrations.d",
+            )))
+            .times(1)
+            .return_once(|_| {
+                Ok(vec![
+                    PathBuf::from("integration1.yml"),
+                    PathBuf::from("integration2.yaml"),
+                ])
+            });
+
+        // Reading the two files "recovered" above for integrations.d
+        file_reader
+            .expect_read()
+            .with(predicate::eq(Path::new("integration1.yml")))
+            .times(1)
+            .return_once(|_| Ok(String::from(INTEGRATION_1_CONFIG)));
+
+        file_reader
+            .expect_read()
+            .with(predicate::eq(Path::new("integration2.yaml")))
+            .times(1)
+            .return_once(|_| Ok(String::from(INTEGRATION_2_CONFIG)));
+
+        file_reader
+            .expect_dir_entries()
+            .with(predicate::eq(Path::new("/etc/newrelic-infra/logging.d")))
+            .times(1)
+            .return_once(|_| {
+                Ok(vec![
+                    PathBuf::from("logging1.yaml"),
+                    PathBuf::from("logging2.yml"),
+                ])
+            });
+
+        // Reading the two files "recovered" above for logging.d
+        file_reader
+            .expect_read()
+            .with(predicate::eq(Path::new("logging1.yaml")))
+            .times(1)
+            .return_once(|_| Ok(String::from(LOGS_1_CONFIG)));
+
+        file_reader
+            .expect_read()
+            .with(predicate::eq(Path::new("logging2.yml")))
+            .times(1)
+            .return_once(|_| Ok(String::from(LOGS_2_CONFIG)));
+
+        let config_converter = ConfigConverter { file_reader };
 
-        Ok(merge_agent_values(agent_values_specs)?)
+        let result = config_converter.convert(&migration_agent_config);
+        assert!(result.is_ok());
+
+        let result = result.unwrap();
+        assert_eq!(3, result.len());
+        assert!(result.contains_key("config_agent"));
+        assert!(result.contains_key("config_integrations"));
+        assert!(result.contains_key("config_logging"));
+
+        let expected_config_agent =
+            serde_yaml::from_str::(config_agent).unwrap();
+        assert_eq!(&expected_config_agent, result.get("config_agent").unwrap());
+
+        let mut expected_integrations_mapping = serde_yaml::Mapping::new();
+        expected_integrations_mapping.insert(
+            serde_yaml::Value::String("integration1.yml".into()),
+            serde_yaml::from_str::(INTEGRATION_1_CONFIG).unwrap(),
+        );
+        expected_integrations_mapping.insert(
+            serde_yaml::Value::String("integration2.yaml".into()),
+            serde_yaml::from_str::(INTEGRATION_2_CONFIG).unwrap(),
+        );
+        let expected_integrations = serde_yaml::Value::Mapping(expected_integrations_mapping);
+        assert_eq!(
+            &expected_integrations,
+            result.get("config_integrations").unwrap()
+        );
+
+        let mut expected_logs_mapping = serde_yaml::Mapping::new();
+        expected_logs_mapping.insert(
+            serde_yaml::Value::String("logging1.yaml".into()),
+            serde_yaml::from_str::(LOGS_1_CONFIG).unwrap(),
+        );
+        expected_logs_mapping.insert(
+            serde_yaml::Value::String("logging2.yml".into()),
+            serde_yaml::from_str::(LOGS_2_CONFIG).unwrap(),
+        );
+        let expected_logs = serde_yaml::Value::Mapping(expected_logs_mapping);
+        assert_eq!(&expected_logs, result.get("config_logging").unwrap());
     }
 
-    fn file_to_agent_value_spec(
-        &self,
-        agent_type_field_fqn: AgentTypeFieldFQN,
-        file_path: FilePath,
-    ) -> Result, ConversionError> {
-        let contents = self.file_reader.read(file_path.as_path())?;
-        Ok(from_fqn_and_value(
-            agent_type_field_fqn.clone(),
-            AgentValueSpecEnd(contents),
-        ))
+    #[test]
+    fn empty_integrations_dir_entry() {
+        // Sample config
+        let migration_agent_config = MigrationAgentConfig {
+            agent_type_fqn: "newrelic/com.newrelic.infrastructure:0.1.0"
+                .try_into()
+                .unwrap(),
+            filesystem_mappings: HashMap::from([
+                ("config_agent".into(), "/etc/newrelic-infra.yml".into()),
+                (
+                    "config_integrations".into(),
+                    DirInfo {
+                        dir_path: "/etc/newrelic-infra/integrations.d".into(),
+                        extensions: vec!["yml".to_string(), "yaml".to_string()],
+                    }
+                    .into(),
+                ),
+                (
+                    "config_logging".into(),
+                    DirInfo {
+                        dir_path: "/etc/newrelic-infra/logging.d".into(),
+                        extensions: vec!["yml".to_string(), "yaml".to_string()],
+                    }
+                    .into(),
+                ),
+            ]),
+            next: None,
+        };
+
+        let config_agent = "license_key: TESTING_CONVERSION";
+
+        let mut file_reader = MockLocalFile::new();
+
+        file_reader
+            .expect_read()
+            .with(predicate::eq(Path::new("/etc/newrelic-infra.yml")))
+            .times(1)
+            .return_once({
+                let config_agent = config_agent.to_string();
+                move |_| Ok(config_agent)
+            });
+
+        // Let's suppose the integrations.d directory is empty, so no files
+        file_reader
+            .expect_dir_entries()
+            .with(
+                predicate::eq(Path::new("/etc/newrelic-infra/logging.d")).or(predicate::eq(
+                    Path::new("/etc/newrelic-infra/integrations.d"),
+                )),
+            )
+            .times(2)
+            .returning(|dir| {
+                let output = if dir == Path::new("/etc/newrelic-infra/logging.d") {
+                    vec![
+                        PathBuf::from("logging1.yaml"),
+                        PathBuf::from("logging2.yml"),
+                    ]
+                } else {
+                    vec![]
+                };
+                Ok(output)
+            });
+
+        // Reading the two files "recovered" above for logging.d
+        file_reader
+            .expect_read()
+            .with(predicate::eq(Path::new("logging1.yaml")))
+            .times(1)
+            .return_once(|_| Ok(String::from(LOGS_1_CONFIG)));
+
+        file_reader
+            .expect_read()
+            .with(predicate::eq(Path::new("logging2.yml")))
+            .times(1)
+            .return_once(|_| Ok(String::from(LOGS_2_CONFIG)));
+
+        let config_converter = ConfigConverter { file_reader };
+
+        let result = config_converter.convert(&migration_agent_config);
+        assert!(result.is_ok());
+
+        let result = result.unwrap();
+        assert_eq!(3, result.len());
+        assert!(result.contains_key("config_agent"));
+        assert!(result.contains_key("config_integrations"));
+        assert!(result.contains_key("config_logging"));
+
+        let expected_config_agent =
+            serde_yaml::from_str::(config_agent).unwrap();
+        assert_eq!(&expected_config_agent, result.get("config_agent").unwrap());
+
+        // Read integrations object should be present but empty array
+        let expected_integrations = serde_yaml::Value::Mapping(serde_yaml::Mapping::new());
+        assert_eq!(
+            &expected_integrations,
+            result.get("config_integrations").unwrap()
+        );
+        let mut expected_logs_mapping = serde_yaml::Mapping::new();
+        expected_logs_mapping.insert(
+            serde_yaml::Value::String("logging1.yaml".into()),
+            serde_yaml::from_str::(LOGS_1_CONFIG).unwrap(),
+        );
+        expected_logs_mapping.insert(
+            serde_yaml::Value::String("logging2.yml".into()),
+            serde_yaml::from_str::(LOGS_2_CONFIG).unwrap(),
+        );
+        let expected_logs = serde_yaml::Value::Mapping(expected_logs_mapping);
+        assert_eq!(&expected_logs, result.get("config_logging").unwrap());
     }
 
-    fn dir_to_agent_value_spec(
-        &self,
-        agent_type_field_fqn: AgentTypeFieldFQN,
-        dir_info: DirInfo,
-    ) -> Result, ConversionError> {
-        let files_paths = self.file_reader.dir_entries(dir_info.path.as_path())?;
-        let mut res: Vec> = Vec::new();
-        // refactor file_path to path
-        for path in files_paths {
-            let filename = path.file_name().unwrap().to_str().unwrap().to_string();
-            //filter by filename
-            if !dir_info.valid_filename(filename.as_str()) {
-                continue;
-            }
-
-            // replace the file separator to not be treated as a leaf
-            let escaped_filename = filename.replace(FILE_SEPARATOR, FILE_SEPARATOR_REPLACE);
-            let full_agent_type_field_fqn: AgentTypeFieldFQN =
-                format!("{agent_type_field_fqn}.{escaped_filename}").into();
-            res.push(self.file_to_agent_value_spec(full_agent_type_field_fqn, path)?);
-        }
-        Ok(merge_agent_values(res)?)
+    #[test]
+    fn no_infra_agent_config_should_fail() {
+        // Sample config
+        let migration_agent_config = MigrationAgentConfig {
+            agent_type_fqn: "newrelic/com.newrelic.infrastructure:0.1.0"
+                .try_into()
+                .unwrap(),
+            filesystem_mappings: HashMap::from([
+                ("config_agent".into(), "/etc/newrelic-infra.yml".into()),
+                (
+                    "config_integrations".into(),
+                    DirInfo {
+                        dir_path: "/etc/newrelic-infra/integrations.d".into(),
+                        extensions: vec!["yml".to_string(), "yaml".to_string()],
+                    }
+                    .into(),
+                ),
+                (
+                    "config_logging".into(),
+                    DirInfo {
+                        dir_path: "/etc/newrelic-infra/logging.d".into(),
+                        extensions: vec!["yml".to_string(), "yaml".to_string()],
+                    }
+                    .into(),
+                ),
+            ]),
+            next: None,
+        };
+
+        let mut file_reader = MockLocalFile::new();
+
+        file_reader
+            .expect_dir_entries()
+            .with(predicate::always())
+            // We don't care about the dir entries for this test
+            .returning(|_| Ok(vec![]));
+        file_reader
+            .expect_read()
+            .with(predicate::always())
+            .return_once(move |p| {
+                if p == Path::new("/etc/newrelic-infra.yml") {
+                    Err(FileReaderError::FileNotFound(String::from(
+                        "file not found: `/etc/newrelic-infra.yml`",
+                    )))
+                } else {
+                    // Default string because we don't care about other reads
+                    Ok(String::new())
+                }
+            });
+
+        let config_converter = ConfigConverter { file_reader };
+
+        let result = config_converter.convert(&migration_agent_config);
+        assert!(result.is_err());
+    }
+
+    #[test]
+    fn empty_integrations_and_logs_should_succeed() {
+        // Sample config
+        let migration_agent_config = MigrationAgentConfig {
+            agent_type_fqn: "newrelic/com.newrelic.infrastructure:0.1.0"
+                .try_into()
+                .unwrap(),
+            filesystem_mappings: HashMap::from([
+                ("config_agent".into(), "/etc/newrelic-infra.yml".into()),
+                (
+                    "config_integrations".into(),
+                    DirInfo {
+                        dir_path: "/etc/newrelic-infra/integrations.d".into(),
+                        extensions: vec!["yml".to_string(), "yaml".to_string()],
+                    }
+                    .into(),
+                ),
+                (
+                    "config_logging".into(),
+                    DirInfo {
+                        dir_path: "/etc/newrelic-infra/logging.d".into(),
+                        extensions: vec!["yml".to_string(), "yaml".to_string()],
+                    }
+                    .into(),
+                ),
+            ]),
+            next: None,
+        };
+
+        let config_agent = "license_key: TESTING_CONVERSION";
+
+        let mut file_reader = MockLocalFile::new();
+
+        file_reader
+            .expect_read()
+            .with(predicate::eq(Path::new("/etc/newrelic-infra.yml")))
+            .times(1)
+            .return_once({
+                let config_agent = config_agent.to_string();
+                move |_| Ok(config_agent)
+            });
+
+        // Let's suppose both integrations.d and logging.d directories are empty, so no files
+        file_reader
+            .expect_dir_entries()
+            .with(
+                predicate::eq(Path::new("/etc/newrelic-infra/logging.d")).or(predicate::eq(
+                    Path::new("/etc/newrelic-infra/integrations.d"),
+                )),
+            )
+            .times(2)
+            .returning(|_| Ok(vec![]));
+
+        let config_converter = ConfigConverter { file_reader };
+
+        let result = config_converter.convert(&migration_agent_config);
+        assert!(result.is_ok());
+
+        let result = result.unwrap();
+        assert_eq!(3, result.len());
+        assert!(result.contains_key("config_agent"));
+        assert!(result.contains_key("config_integrations"));
+        assert!(result.contains_key("config_logging"));
+
+        let expected_config_agent =
+            serde_yaml::from_str::(config_agent).unwrap();
+        assert_eq!(&expected_config_agent, result.get("config_agent").unwrap());
+
+        // Read integrations object should be present but empty array
+        let expected_integrations = serde_yaml::Value::Mapping(serde_yaml::Mapping::new());
+        assert_eq!(
+            &expected_integrations,
+            result.get("config_integrations").unwrap()
+        );
+
+        let expected_logs = serde_yaml::Value::Mapping(serde_yaml::Mapping::new());
+        assert_eq!(&expected_logs, result.get("config_logging").unwrap());
     }
 }
diff --git a/agent-control/src/config_migrate/migration/defaults.rs b/agent-control/src/config_migrate/migration/defaults.rs
index 6e3a861bf6..f187c7b0b3 100644
--- a/agent-control/src/config_migrate/migration/defaults.rs
+++ b/agent-control/src/config_migrate/migration/defaults.rs
@@ -4,15 +4,16 @@ pub const NEWRELIC_INFRA_AGENT_TYPE_CONFIG_MAPPING: &str = r#"
 configs:
   -
     agent_type_fqn: newrelic/com.newrelic.infrastructure:0.1.0
-    files_map:
+    filesystem_mappings:
       config_agent: /etc/newrelic-infra.yml
-    dirs_map:
       config_integrations:
-        path: /etc/newrelic-infra/integrations.d
-        filename_patterns:
-          - ".*\\.ya?ml$"
+        dir_path: /etc/newrelic-infra/integrations.d
+        extensions:
+          - "yml"
+          - "yaml"
       config_logging:
-        path: /etc/newrelic-infra/logging.d
-        filename_patterns:
-          - ".*\\.ya?ml$"
+        dir_path: /etc/newrelic-infra/logging.d
+        extensions:
+          - "yml"
+          - "yaml"
 "#;
diff --git a/agent-control/src/config_migrate/migration/migrator.rs b/agent-control/src/config_migrate/migration/migrator.rs
index d7d4af3951..9a1b254dd3 100644
--- a/agent-control/src/config_migrate/migration/migrator.rs
+++ b/agent-control/src/config_migrate/migration/migrator.rs
@@ -1,8 +1,6 @@
 use crate::agent_control::config::AgentControlConfigError;
 use crate::agent_control::config_repository::repository::AgentControlDynamicConfigRepository;
 use crate::agent_control::config_repository::store::AgentControlConfigStore;
-use crate::agent_type::agent_type_registry::AgentRegistry;
-use crate::agent_type::embedded_registry::EmbeddedRegistry;
 #[cfg_attr(test, mockall_double::double)]
 use crate::config_migrate::migration::agent_config_getter::AgentConfigGetter;
 use crate::config_migrate::migration::config::MigrationAgentConfig;
@@ -38,26 +36,24 @@ pub enum MigratorError {
 }
 
 pub struct ConfigMigrator<
-    R: AgentRegistry,
     SL: AgentControlDynamicConfigRepository + 'static,
     C: DirectoryManager,
     F: FileReader,
 > {
-    config_converter: ConfigConverter,
+    config_converter: ConfigConverter,
     agent_config_getter: AgentConfigGetter,
     values_persister: ValuesPersisterFile,
 }
 
 impl
     ConfigMigrator<
-        EmbeddedRegistry,
         AgentControlConfigStore>,
         DirectoryManagerFs,
         LocalFile,
     >
 {
     pub fn new(
-        config_converter: ConfigConverter,
+        config_converter: ConfigConverter,
         agent_config_getter: AgentConfigGetter<
             AgentControlConfigStore>,
         >,
@@ -109,7 +105,6 @@ mod tests {
     use crate::agent_control::config::{AgentControlDynamicConfig, SubAgentConfig};
     use crate::agent_type::agent_type_id::AgentTypeID;
     use crate::config_migrate::migration::agent_config_getter::MockAgentConfigGetter;
-    use crate::config_migrate::migration::agent_value_spec::AgentValueSpec::AgentValueSpecEnd;
     use crate::config_migrate::migration::config::MigrationAgentConfig;
     use crate::config_migrate::migration::converter::MockConfigConverter;
     use crate::config_migrate::migration::migrator::ConfigMigrator;
@@ -150,8 +145,10 @@ mod tests {
                 })
             });
 
-        let agent_variables =
-            HashMap::from([("cfg".to_string(), AgentValueSpecEnd("value".to_string()))]);
+        let agent_variables = HashMap::from([(
+            "cfg".to_string(),
+            serde_yaml::Value::String("value".to_string()),
+        )]);
 
         let mut config_converter = MockConfigConverter::default();
         config_converter
@@ -176,8 +173,7 @@ mod tests {
         let agent_config_mapping = MigrationAgentConfig {
             agent_type_fqn: AgentTypeID::try_from("newrelic/com.newrelic.infrastructure:0.0.1")
                 .unwrap(),
-            files_map: Default::default(),
-            dirs_map: Default::default(),
+            filesystem_mappings: Default::default(),
             next: None,
         };
         let migration = migrator.migrate(&agent_config_mapping);
diff --git a/fs/src/file_reader.rs b/fs/src/file_reader.rs
index 47c625a671..aa1a34079f 100644
--- a/fs/src/file_reader.rs
+++ b/fs/src/file_reader.rs
@@ -15,11 +15,13 @@ pub enum FileReaderError {
 }
 
 pub trait FileReader {
-    /// Read the contents of file_path and return them as string
+    /// Read the contents of file_path and return them as string.
+    ///
     /// If the file is not present it will return a FileReaderError
     fn read(&self, file_path: &Path) -> Result;
 
     /// Return the entries inside a given Path.
+    ///
     /// If the path does not exist it will return a FileReaderError
     fn dir_entries(&self, dir_path: &Path) -> Result, FileReaderError>;
 }
diff --git a/test/onhost-e2e/ansible/remote_config.yaml b/test/onhost-e2e/ansible/remote_config.yaml
index 135238742d..edf3adcc21 100644
--- a/test/onhost-e2e/ansible/remote_config.yaml
+++ b/test/onhost-e2e/ansible/remote_config.yaml
@@ -96,7 +96,7 @@
           copy:
             dest: /etc/newrelic-agent-control/fleet/agents.d/nr-infra/values/values.yaml
             content: |
-              config_agent: |+
+              config_agent:
                 status_server_enabled: true
                 status_server_port: 18003
                 license_key: {{'{{'}}NEW_RELIC_LICENSE_KEY{{'}}'}}