libdd_library_config/
lib.rs

1// Copyright 2021-Present Datadog, Inc. https://www.datadoghq.com/
2// SPDX-License-Identifier: Apache-2.0
3pub mod tracer_metadata;
4
5use std::borrow::Cow;
6use std::cell::OnceCell;
7use std::collections::HashMap;
8use std::ops::Deref;
9use std::path::Path;
10use std::{env, fs, io, mem};
11
12/// This struct holds maps used to match and template configurations.
13///
14/// They are computed lazily so that if the templating feature is not necessary, we don't
15/// have to create the maps.
16///
17/// These maps come from one of three origins:
18///  * tags: This one is fairly simple, the format is tag_key: tag_value
19///  * envs: Splits env variables with format KEY=VALUE
20///  * args: Splits args with format key=value. If the arg doesn't contain an '=', skip it
21struct MatchMaps<'a> {
22    tags: &'a HashMap<String, String>,
23    env_map: OnceCell<HashMap<&'a str, &'a str>>,
24    args_map: OnceCell<HashMap<&'a str, &'a str>>,
25}
26
27impl<'a> MatchMaps<'a> {
28    fn env(&self, process_info: &'a ProcessInfo) -> &HashMap<&'a str, &'a str> {
29        self.env_map.get_or_init(|| {
30            let mut map = HashMap::new();
31            for e in &process_info.envp {
32                let Ok(s) = std::str::from_utf8(e.deref()) else {
33                    continue;
34                };
35                let (k, v) = match s.split_once('=') {
36                    Some((k, v)) => (k, v),
37                    None => (s, ""),
38                };
39                map.insert(k, v);
40            }
41            map
42        })
43    }
44
45    fn args(&self, process_info: &'a ProcessInfo) -> &HashMap<&str, &str> {
46        self.args_map.get_or_init(|| {
47            let mut map = HashMap::new();
48            let mut args = process_info.args.iter().peekable();
49            loop {
50                let Some(arg) = args.next() else {
51                    break;
52                };
53                let Ok(arg) = std::str::from_utf8(arg.deref()) else {
54                    continue;
55                };
56                // Split args between key and value on '='
57                if let Some((k, v)) = arg.split_once('=') {
58                    map.insert(k, v);
59                    continue;
60                }
61            }
62            map
63        })
64    }
65}
66
67struct Matcher<'a> {
68    process_info: &'a ProcessInfo,
69    match_maps: MatchMaps<'a>,
70}
71
72impl<'a> Matcher<'a> {
73    fn new(process_info: &'a ProcessInfo, tags: &'a HashMap<String, String>) -> Self {
74        Self {
75            process_info,
76            match_maps: MatchMaps {
77                tags,
78                env_map: OnceCell::new(),
79                args_map: OnceCell::new(),
80            },
81        }
82    }
83
84    /// Returns the first set of configurations that match the current process
85    fn find_stable_config<'b>(&'a self, cfg: &'b StableConfig) -> Option<&'b ConfigMap> {
86        for rule in &cfg.rules {
87            if rule.selectors.iter().all(|s| self.selector_match(s)) {
88                return Some(&rule.configuration);
89            }
90        }
91        None
92    }
93
94    /// Returns true if the selector matches the process
95    ///
96    /// Any element in the "matches" section of the selector must match, they are ORed,
97    /// as selectors are ANDed.
98    fn selector_match(&'a self, selector: &Selector) -> bool {
99        match selector.origin {
100            Origin::Language => string_selector(selector, self.process_info.language.deref()),
101            Origin::ProcessArguments => match &selector.key {
102                Some(key) => {
103                    let arg_map = self.match_maps.args(self.process_info);
104                    map_operator_match(selector, arg_map, key)
105                }
106                None => string_list_selector(selector, &self.process_info.args),
107            },
108            Origin::EnvironmentVariables => match &selector.key {
109                Some(key) => {
110                    let env_map = self.match_maps.env(self.process_info);
111                    map_operator_match(selector, env_map, key)
112                }
113                None => string_list_selector(selector, &self.process_info.envp),
114            },
115            Origin::Tags => match &selector.key {
116                Some(key) => map_operator_match(selector, self.match_maps.tags, key),
117                None => false,
118            },
119        }
120    }
121
122    /// Templates a config string.
123    ///
124    /// variables are enclosed in double curly brackets "{{" and "}}"
125    ///
126    /// For instance:
127    ///
128    /// with the following varriable definition, var = "abc" var2 = "def", this transforms \
129    /// "foo_{{ var }}_bar_{{ var2 }}" -> "foo_abc_bar_def"
130    fn template_config(&'a self, config_val: &str) -> anyhow::Result<String> {
131        let mut rest = config_val;
132        let mut templated = String::with_capacity(config_val.len());
133        loop {
134            let Some((head, after_bracket)) = rest.split_once("{{") else {
135                templated.push_str(rest);
136                return Ok(templated);
137            };
138            templated.push_str(head);
139            let Some((template_var, tail)) = after_bracket.split_once("}}") else {
140                anyhow::bail!("unterminated template in config")
141            };
142            let (template_var, index) = parse_template_var(template_var.trim());
143            let val = match template_var {
144                "language" => String::from_utf8_lossy(self.process_info.language.deref()),
145                "environment_variables" => {
146                    template_map_key(index, self.match_maps.env(self.process_info))
147                }
148                "process_arguments" => {
149                    template_map_key(index, self.match_maps.args(self.process_info))
150                }
151                "tags" => template_map_key(index, self.match_maps.tags),
152                _ => std::borrow::Cow::Borrowed("UNDEFINED"),
153            };
154            templated.push_str(&val);
155            rest = tail;
156        }
157    }
158}
159
160fn map_operator_match(selector: &Selector, map: &impl Get, key: &str) -> bool {
161    let Some(val) = map.get(key) else {
162        return false;
163    };
164    string_selector(selector, val.as_bytes())
165}
166
167fn parse_template_var(template_var: &str) -> (&str, Option<&str>) {
168    match template_var.trim().split_once('[') {
169        Some((template_var, idx)) => {
170            let Some((index, _)) = idx.split_once(']') else {
171                return (template_var, None);
172            };
173            (template_var, Some(index.trim()))
174        }
175        None => (template_var, None),
176    }
177}
178
179fn template_map_key<'a>(key: Option<&str>, map: &'a impl Get) -> Cow<'a, str> {
180    let Some(key) = key else {
181        return Cow::Borrowed("UNDEFINED");
182    };
183    Cow::Borrowed(map.get(key).unwrap_or("UNDEFINED"))
184}
185
186#[repr(C)]
187pub struct ProcessInfo {
188    pub args: Vec<Vec<u8>>,
189    pub envp: Vec<Vec<u8>>,
190    pub language: Vec<u8>,
191}
192
193fn process_envp() -> Vec<Vec<u8>> {
194    #[allow(clippy::unnecessary_filter_map)]
195    env::vars_os()
196        .filter_map(|(k, v)| {
197            #[cfg(not(unix))]
198            {
199                let mut env = Vec::new();
200                env.extend(k.to_str()?.as_bytes());
201                env.push(b'=');
202                env.extend(v.to_str()?.as_bytes());
203                Some(env)
204            }
205            #[cfg(unix)]
206            {
207                use std::os::unix::ffi::OsStrExt;
208                let mut env = Vec::new();
209                env.extend(k.as_bytes());
210                env.push(b'=');
211                env.extend(v.as_bytes());
212                Some(env)
213            }
214        })
215        .collect()
216}
217
218fn process_args() -> Vec<Vec<u8>> {
219    #[allow(clippy::unnecessary_filter_map)]
220    env::args_os()
221        .filter_map(|a| {
222            #[cfg(not(unix))]
223            {
224                Some(a.into_string().ok()?.into_bytes())
225            }
226            #[cfg(unix)]
227            {
228                use std::os::unix::ffi::OsStringExt;
229                Some(a.into_vec())
230            }
231        })
232        .collect()
233}
234
235impl ProcessInfo {
236    pub fn detect_global(language: String) -> Self {
237        let envp = process_envp();
238        let args = process_args();
239        Self {
240            args,
241            envp,
242            language: language.into_bytes(),
243        }
244    }
245}
246
247/// A (key, value) struct
248///
249/// This type has a custom serde Deserialize implementation from maps:
250/// * It skips invalid/unknown keys in the map
251/// * Since the storage is a Boxed slice and not a Hashmap, it doesn't over-allocate
252#[derive(Debug, Default, PartialEq, Eq)]
253struct ConfigMap(Box<[(String, String)]>);
254
255impl<'de> serde::Deserialize<'de> for ConfigMap {
256    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
257    where
258        D: serde::Deserializer<'de>,
259    {
260        struct ConfigMapVisitor;
261        impl<'de> serde::de::Visitor<'de> for ConfigMapVisitor {
262            type Value = ConfigMap;
263
264            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
265                formatter.write_str("struct ConfigMap(HashMap<String, String>)")
266            }
267
268            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
269            where
270                A: serde::de::MapAccess<'de>,
271            {
272                let mut configs = Vec::new();
273                configs.reserve_exact(map.size_hint().unwrap_or(0));
274                loop {
275                    let k = match map.next_key::<String>() {
276                        Ok(Some(k)) => k,
277                        Ok(None) => break,
278                        Err(_) => {
279                            map.next_value::<serde::de::IgnoredAny>()?;
280                            continue;
281                        }
282                    };
283                    let v = map.next_value::<String>()?;
284                    configs.push((k, v));
285                }
286                Ok(ConfigMap(configs.into_boxed_slice()))
287            }
288        }
289        deserializer.deserialize_map(ConfigMapVisitor)
290    }
291}
292
293#[repr(C)]
294#[derive(Clone, Copy, serde::Deserialize, Debug, PartialEq, Eq, Hash)]
295#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
296#[allow(clippy::enum_variant_names)]
297pub enum LibraryConfigSource {
298    // Order matters, as it is used to determine the priority of the source.
299    //  The higher the value, the higher the priority.
300    LocalStableConfig = 0,
301    FleetStableConfig = 1,
302}
303
304impl LibraryConfigSource {
305    pub fn to_str(&self) -> &'static str {
306        use LibraryConfigSource::*;
307        match self {
308            LocalStableConfig => "local_stable_config",
309            FleetStableConfig => "fleet_stable_config",
310        }
311    }
312}
313
314#[derive(serde::Deserialize, Debug, PartialEq, Eq)]
315#[serde(rename_all = "snake_case")]
316enum Origin {
317    ProcessArguments,
318    EnvironmentVariables,
319    Language,
320    Tags,
321}
322
323#[derive(serde::Deserialize, Debug, PartialEq, Eq)]
324#[serde(rename_all = "snake_case")]
325#[serde(tag = "operator")]
326enum Operator {
327    Exists,
328    Equals { matches: Vec<String> },
329    PrefixMatches { matches: Vec<String> },
330    SuffixMatches { matches: Vec<String> },
331    // todo
332    // WildcardMatches,
333}
334
335#[derive(serde::Deserialize, Debug, PartialEq, Eq)]
336struct Selector {
337    origin: Origin,
338    #[serde(default)]
339    key: Option<String>,
340    #[serde(flatten)]
341    operator: Operator,
342}
343
344#[derive(serde::Deserialize, Debug, PartialEq, Eq)]
345struct Rule {
346    selectors: Vec<Selector>,
347    configuration: ConfigMap,
348}
349
350#[derive(serde::Deserialize, Default, Debug, PartialEq, Eq)]
351struct StableConfig {
352    // Phase 1
353    #[serde(default)]
354    config_id: Option<String>,
355    #[serde(default)]
356    apm_configuration_default: ConfigMap,
357
358    // Phase 2
359    #[serde(default)]
360    tags: HashMap<String, String>,
361    #[serde(default)]
362    rules: Vec<Rule>,
363}
364
365fn string_list_selector<B: Deref<Target = [u8]>>(selector: &Selector, l: &[B]) -> bool {
366    l.iter().any(|v| string_selector(selector, v.deref()))
367}
368
369fn string_selector(selector: &Selector, value: &[u8]) -> bool {
370    let matches = match &selector.operator {
371        Operator::Exists => return true,
372        Operator::Equals { matches } => matches,
373        Operator::PrefixMatches { matches } => matches,
374        Operator::SuffixMatches { matches } => matches,
375    };
376    matches
377        .iter()
378        .any(|m| string_operator_match(&selector.operator, m.as_bytes(), value))
379}
380
381fn string_operator_match(op: &Operator, matches: &[u8], value: &[u8]) -> bool {
382    match op {
383        Operator::Equals { .. } => matches == value,
384        Operator::PrefixMatches { .. } => value.starts_with(matches),
385        Operator::SuffixMatches { .. } => value.ends_with(matches),
386        Operator::Exists => true,
387        // Operator::WildcardMatches => todo!("Wildcard matches is not implemented"),
388    }
389}
390
391#[derive(Debug, PartialEq, Eq)]
392/// LibraryConfig represent a configuration item and is part of the public API
393/// of this module
394pub struct LibraryConfig {
395    pub name: String,
396    pub value: String,
397    pub source: LibraryConfigSource,
398    pub config_id: Option<String>,
399}
400
401#[derive(Debug)]
402/// This struct is used to hold configuration item data in a Hashmap, while the name of
403/// the configuration is the key used for deduplication
404struct LibraryConfigVal {
405    value: String,
406    source: LibraryConfigSource,
407    config_id: Option<String>,
408}
409
410#[derive(Debug)]
411pub struct Configurator {
412    debug_logs: bool,
413}
414
415pub enum Target {
416    Linux,
417    Macos,
418    Windows,
419}
420
421impl Target {
422    #[cfg(any(target_os = "linux", target_os = "macos", windows))]
423    const fn current() -> Self {
424        #[cfg(target_os = "linux")]
425        {
426            Self::Linux
427        }
428        #[cfg(target_os = "macos")]
429        {
430            Self::Macos
431        }
432        #[cfg(windows)]
433        {
434            Self::Windows
435        }
436    }
437}
438
439#[derive(Debug)]
440pub enum LoggedResult<T, E> {
441    Ok(T, Vec<String>),
442    Err(E),
443}
444
445impl<T, E> LoggedResult<T, E> {
446    pub fn data(self) -> Result<T, E> {
447        match self {
448            LoggedResult::Ok(value, _) => Ok(value),
449            LoggedResult::Err(err) => Err(err),
450        }
451    }
452
453    pub fn logs(&self) -> &[String] {
454        match self {
455            LoggedResult::Ok(_, logs) => logs,
456            LoggedResult::Err(_) => &[],
457        }
458    }
459
460    pub fn into_logs(self) -> Vec<String> {
461        match self {
462            LoggedResult::Ok(_, logs) => logs,
463            LoggedResult::Err(_) => Vec::new(),
464        }
465    }
466
467    pub fn logs_as_string(&self) -> String {
468        self.logs().join("\n")
469    }
470}
471
472impl Configurator {
473    #[cfg(any(target_os = "linux", target_os = "macos", windows))]
474    pub const FLEET_STABLE_CONFIGURATION_PATH: &'static str =
475        Self::fleet_stable_configuration_path(Target::current());
476
477    #[cfg(any(target_os = "linux", target_os = "macos", windows))]
478    pub const LOCAL_STABLE_CONFIGURATION_PATH: &'static str =
479        Self::local_stable_configuration_path(Target::current());
480
481    pub const fn local_stable_configuration_path(target: Target) -> &'static str {
482        match target {
483            Target::Linux => "/etc/datadog-agent/application_monitoring.yaml",
484            Target::Macos => "/opt/datadog-agent/etc/application_monitoring.yaml",
485            Target::Windows => "C:\\ProgramData\\Datadog\\application_monitoring.yaml",
486        }
487    }
488
489    pub const fn fleet_stable_configuration_path(target: Target) -> &'static str {
490        match target {
491            Target::Linux => "/etc/datadog-agent/managed/datadog-agent/stable/application_monitoring.yaml",
492            Target::Macos => "/opt/datadog-agent/etc/stable/application_monitoring.yaml",
493            Target::Windows => "C:\\ProgramData\\Datadog\\managed\\datadog-agent\\stable\\application_monitoring.yaml",
494        }
495    }
496
497    pub fn new(debug_logs: bool) -> Self {
498        Self { debug_logs }
499    }
500
501    fn parse_stable_config_slice(&self, buf: &[u8]) -> LoggedResult<StableConfig, anyhow::Error> {
502        let stable_config = if buf.is_empty() {
503            StableConfig::default()
504        } else {
505            match serde_yaml::from_slice(buf) {
506                Ok(config) => config,
507                Err(e) => return LoggedResult::Err(e.into()),
508            }
509        };
510
511        let messages = if self.debug_logs {
512            vec![format!(
513                "Read the following static config: {stable_config:?}"
514            )]
515        } else {
516            Vec::new()
517        };
518
519        LoggedResult::Ok(stable_config, messages)
520    }
521
522    fn parse_stable_config_file<F: io::Read>(
523        &self,
524        mut f: F,
525    ) -> LoggedResult<StableConfig, anyhow::Error> {
526        let mut buffer = Vec::new();
527        match f.read_to_end(&mut buffer) {
528            Ok(_) => {}
529            Err(e) => return LoggedResult::Err(e.into()),
530        }
531        self.parse_stable_config_slice(utils::trim_bytes(&buffer))
532    }
533
534    pub fn get_config_from_file(
535        &self,
536        path_local: &Path,
537        path_managed: &Path,
538        process_info: &ProcessInfo,
539    ) -> LoggedResult<Vec<LibraryConfig>, anyhow::Error> {
540        let mut debug_messages = Vec::new();
541        if self.debug_logs {
542            debug_messages.push("Reading stable configuration from files:".to_string());
543            debug_messages.push(format!("\tlocal: {path_local:?}"));
544            debug_messages.push(format!("\tfleet: {path_managed:?}"));
545        }
546        let local_config = match fs::File::open(path_local) {
547            Ok(file) => match self.parse_stable_config_file(file) {
548                LoggedResult::Ok(config, logs) => {
549                    debug_messages.extend(logs);
550                    config
551                }
552                LoggedResult::Err(e) => return LoggedResult::Err(e),
553            },
554            Err(e) if e.kind() == io::ErrorKind::NotFound => StableConfig::default(),
555            Err(e) => {
556                return LoggedResult::Err(
557                    anyhow::Error::from(e).context("failed to open config file"),
558                )
559            }
560        };
561        let fleet_config = match fs::File::open(path_managed) {
562            Ok(file) => match self.parse_stable_config_file(file) {
563                LoggedResult::Ok(config, logs) => {
564                    debug_messages.extend(logs);
565                    config
566                }
567                LoggedResult::Err(e) => return LoggedResult::Err(e),
568            },
569            Err(e) if e.kind() == io::ErrorKind::NotFound => StableConfig::default(),
570            Err(e) => {
571                return LoggedResult::Err(
572                    anyhow::Error::from(e).context("failed to open config file"),
573                )
574            }
575        };
576
577        match self.get_config(local_config, fleet_config, process_info) {
578            LoggedResult::Ok(configs, msgs) => {
579                debug_messages.extend(msgs);
580                LoggedResult::Ok(configs, debug_messages)
581            }
582            LoggedResult::Err(e) => LoggedResult::Err(e),
583        }
584    }
585
586    pub fn get_config_from_bytes(
587        &self,
588        s_local: &[u8],
589        s_managed: &[u8],
590        process_info: ProcessInfo,
591    ) -> anyhow::Result<Vec<LibraryConfig>> {
592        let local_config = match self.parse_stable_config_slice(s_local) {
593            LoggedResult::Ok(config, _) => config,
594            LoggedResult::Err(e) => return Err(e),
595        };
596        let fleet_config = match self.parse_stable_config_slice(s_managed) {
597            LoggedResult::Ok(config, _) => config,
598            LoggedResult::Err(e) => return Err(e),
599        };
600
601        match self.get_config(local_config, fleet_config, &process_info) {
602            LoggedResult::Ok(configs, _) => Ok(configs),
603            LoggedResult::Err(e) => Err(e),
604        }
605    }
606
607    fn get_config(
608        &self,
609        local_config: StableConfig,
610        fleet_config: StableConfig,
611        process_info: &ProcessInfo,
612    ) -> LoggedResult<Vec<LibraryConfig>, anyhow::Error> {
613        let mut debug_messages = Vec::new();
614        if self.debug_logs {
615            debug_messages.push("\tProcess args:".to_string());
616
617            for arg in &process_info.args {
618                let arg_str = String::from_utf8_lossy(arg);
619                debug_messages.push(format!("\t\t{:?}", arg_str.as_ref()));
620            }
621
622            debug_messages.push(format!(
623                "\tProcess language: {:?}",
624                String::from_utf8_lossy(&process_info.language).as_ref()
625            ));
626        }
627
628        let mut cfg = HashMap::new();
629        // First get local configuration
630        match self.get_single_source_config(
631            local_config,
632            LibraryConfigSource::LocalStableConfig,
633            process_info,
634            &mut cfg,
635        ) {
636            LoggedResult::Ok(_, msgs) => debug_messages.extend(msgs),
637            LoggedResult::Err(e) => return LoggedResult::Err(e),
638        }
639
640        if self.debug_logs {
641            debug_messages.push("Called library_config_common_component:".to_string());
642            debug_messages.push(format!(
643                "\tsource: {:?}",
644                LibraryConfigSource::LocalStableConfig
645            ));
646            debug_messages.push(format!("\tconfigurator: {self:?}"));
647        }
648
649        // Merge with fleet config override
650        match self.get_single_source_config(
651            fleet_config,
652            LibraryConfigSource::FleetStableConfig,
653            process_info,
654            &mut cfg,
655        ) {
656            LoggedResult::Ok(_, msgs) => debug_messages.extend(msgs),
657            LoggedResult::Err(e) => return LoggedResult::Err(e),
658        }
659
660        if self.debug_logs {
661            debug_messages.push("Called library_config_common_component:".to_string());
662            debug_messages.push(format!(
663                "\tsource: {:?}",
664                LibraryConfigSource::FleetStableConfig
665            ));
666            debug_messages.push(format!("\tconfigurator: {self:?}"));
667        }
668
669        let configs = cfg
670            .into_iter()
671            .map(|(k, v)| LibraryConfig {
672                name: k,
673                value: v.value,
674                source: v.source,
675                config_id: v.config_id,
676            })
677            .collect();
678
679        LoggedResult::Ok(configs, debug_messages)
680    }
681
682    /// Get config from a stable config file and associate them with the file origin
683    ///
684    /// This is done in two steps:
685    ///     * First take the global host config
686    ///     * Merge the global config with the process specific config
687    fn get_single_source_config(
688        &self,
689        mut stable_config: StableConfig,
690        source: LibraryConfigSource,
691        process_info: &ProcessInfo,
692        cfg: &mut HashMap<String, LibraryConfigVal>,
693    ) -> LoggedResult<(), anyhow::Error> {
694        // Phase 1: take host default config
695        cfg.extend(
696            mem::take(&mut stable_config.apm_configuration_default)
697                .0
698                // TODO(paullgdc): use Box<[I]>::into_iter when we can use rust 1.80
699                .into_vec()
700                .into_iter()
701                .map(|(k, v)| {
702                    (
703                        k,
704                        LibraryConfigVal {
705                            value: v,
706                            source,
707                            config_id: stable_config.config_id.clone(),
708                        },
709                    )
710                }),
711        );
712
713        // Phase 2: process specific config
714        self.get_single_source_process_config(stable_config, source, process_info, cfg)
715    }
716
717    /// Get config from a stable config using process matching rules
718    fn get_single_source_process_config(
719        &self,
720        stable_config: StableConfig,
721        source: LibraryConfigSource,
722        process_info: &ProcessInfo,
723        library_config: &mut HashMap<String, LibraryConfigVal>,
724    ) -> LoggedResult<(), anyhow::Error> {
725        let matcher = Matcher::new(process_info, &stable_config.tags);
726        let Some(configs) = matcher.find_stable_config(&stable_config) else {
727            let messages = if self.debug_logs {
728                vec![format!("No selector matched for source {source:?}")]
729            } else {
730                Vec::new()
731            };
732            return LoggedResult::Ok((), messages);
733        };
734
735        for (name, config_val) in configs.0.iter() {
736            let value = match matcher.template_config(config_val) {
737                Ok(v) => v,
738                Err(e) => return LoggedResult::Err(e),
739            };
740            library_config.insert(
741                name.clone(),
742                LibraryConfigVal {
743                    value,
744                    source,
745                    config_id: stable_config.config_id.clone(),
746                },
747            );
748        }
749
750        let messages = if self.debug_logs {
751            vec![format!("Will apply the following configuration:\n\tsource {source:?}\n\t{library_config:?}")]
752        } else {
753            Vec::new()
754        };
755
756        LoggedResult::Ok((), messages)
757    }
758}
759
760use utils::Get;
761mod utils {
762    use std::collections::HashMap;
763
764    /// Removes leading and trailing ascci whitespaces from a byte slice
765    pub(crate) fn trim_bytes(mut b: &[u8]) -> &[u8] {
766        while b.first().map(u8::is_ascii_whitespace).unwrap_or(false) {
767            b = &b[1..];
768        }
769        while b.last().map(u8::is_ascii_whitespace).unwrap_or(false) {
770            b = &b[..b.len() - 1];
771        }
772        b
773    }
774
775    /// Helper trait so we don't have to duplicate code for
776    /// HashMap<&str, &str> and HashMap<String, String>
777    pub(crate) trait Get {
778        fn get(&self, k: &str) -> Option<&str>;
779    }
780
781    impl Get for HashMap<&str, &str> {
782        fn get(&self, k: &str) -> Option<&str> {
783            self.get(k).copied()
784        }
785    }
786
787    impl Get for HashMap<String, String> {
788        fn get(&self, k: &str) -> Option<&str> {
789            self.get(k).map(|v| v.as_str())
790        }
791    }
792}
793
794#[cfg(test)]
795mod tests {
796    use std::{collections::HashMap, io::Write};
797
798    use super::{Configurator, LoggedResult, ProcessInfo};
799    use crate::{
800        ConfigMap, LibraryConfig, LibraryConfigSource, Matcher, Operator, Origin, Rule, Selector,
801        StableConfig,
802    };
803
804    fn test_config(local_cfg: &[u8], fleet_cfg: &[u8], expected: Vec<LibraryConfig>) {
805        let process_info: ProcessInfo = ProcessInfo {
806            args: vec![
807                b"-Djava_config_key=my_config".to_vec(),
808                b"-jar".to_vec(),
809                b"HelloWorld.jar".to_vec(),
810            ],
811            envp: vec![b"ENV=VAR".to_vec()],
812            language: b"java".to_vec(),
813        };
814        let configurator = Configurator::new(true);
815        let mut actual = configurator
816            .get_config_from_bytes(local_cfg, fleet_cfg, process_info)
817            .unwrap();
818
819        // Sort by name for determinism
820        actual.sort_by_key(|c| c.name.clone());
821        assert_eq!(actual, expected);
822    }
823
824    #[test]
825    fn test_empty_configs() {
826        test_config(b"", b"", vec![]);
827    }
828
829    #[test]
830    fn test_missing_files() {
831        let configurator = Configurator::new(true);
832        let result = configurator.get_config_from_file(
833            "/file/is/missing".as_ref(),
834            "/file/is/missing_too".as_ref(),
835            &ProcessInfo {
836                args: vec![b"-jar HelloWorld.jar".to_vec()],
837                envp: vec![b"ENV=VAR".to_vec()],
838                language: b"java".to_vec(),
839            },
840        );
841        match result {
842            LoggedResult::Ok(configs, logs) => {
843                assert_eq!(configs, vec![]);
844                assert_eq!(
845                    logs,
846                    vec![
847                        "Reading stable configuration from files:",
848                        "\tlocal: \"/file/is/missing\"",
849                        "\tfleet: \"/file/is/missing_too\"",
850                        "\tProcess args:",
851                        "\t\t\"-jar HelloWorld.jar\"",
852                        "\tProcess language: \"java\"",
853                        "No selector matched for source LocalStableConfig",
854                        "Called library_config_common_component:",
855                        "\tsource: LocalStableConfig",
856                        "\tconfigurator: Configurator { debug_logs: true }",
857                        "No selector matched for source FleetStableConfig",
858                        "Called library_config_common_component:",
859                        "\tsource: FleetStableConfig",
860                        "\tconfigurator: Configurator { debug_logs: true }"
861                    ]
862                );
863            }
864            LoggedResult::Err(_) => panic!("Expected success"),
865        }
866    }
867
868    #[test]
869    fn test_local_host_global_config() {
870        use LibraryConfigSource::*;
871        test_config(
872            b"
873apm_configuration_default:
874  DD_APM_TRACING_ENABLED: true
875  DD_RUNTIME_METRICS_ENABLED: true
876  DD_LOGS_INJECTION: true
877  DD_PROFILING_ENABLED: true
878  DD_DATA_STREAMS_ENABLED: true
879  DD_APPSEC_ENABLED: true
880  DD_IAST_ENABLED: true
881  DD_DYNAMIC_INSTRUMENTATION_ENABLED: true
882  DD_DATA_JOBS_ENABLED: true
883  DD_APPSEC_SCA_ENABLED: true
884    ",
885            b"",
886            vec![
887                LibraryConfig {
888                    name: "DD_APM_TRACING_ENABLED".to_owned(),
889                    value: "true".to_owned(),
890                    source: LocalStableConfig,
891                    config_id: None,
892                },
893                LibraryConfig {
894                    name: "DD_APPSEC_ENABLED".to_owned(),
895                    value: "true".to_owned(),
896                    source: LocalStableConfig,
897                    config_id: None,
898                },
899                LibraryConfig {
900                    name: "DD_APPSEC_SCA_ENABLED".to_owned(),
901                    value: "true".to_owned(),
902                    source: LocalStableConfig,
903                    config_id: None,
904                },
905                LibraryConfig {
906                    name: "DD_DATA_JOBS_ENABLED".to_owned(),
907                    value: "true".to_owned(),
908                    source: LocalStableConfig,
909                    config_id: None,
910                },
911                LibraryConfig {
912                    name: "DD_DATA_STREAMS_ENABLED".to_owned(),
913                    value: "true".to_owned(),
914                    source: LocalStableConfig,
915                    config_id: None,
916                },
917                LibraryConfig {
918                    name: "DD_DYNAMIC_INSTRUMENTATION_ENABLED".to_owned(),
919                    value: "true".to_owned(),
920                    source: LocalStableConfig,
921                    config_id: None,
922                },
923                LibraryConfig {
924                    name: "DD_IAST_ENABLED".to_owned(),
925                    value: "true".to_owned(),
926                    source: LocalStableConfig,
927                    config_id: None,
928                },
929                LibraryConfig {
930                    name: "DD_LOGS_INJECTION".to_owned(),
931                    value: "true".to_owned(),
932                    source: LocalStableConfig,
933                    config_id: None,
934                },
935                LibraryConfig {
936                    name: "DD_PROFILING_ENABLED".to_owned(),
937                    value: "true".to_owned(),
938                    source: LocalStableConfig,
939                    config_id: None,
940                },
941                LibraryConfig {
942                    name: "DD_RUNTIME_METRICS_ENABLED".to_owned(),
943                    value: "true".to_owned(),
944                    source: LocalStableConfig,
945                    config_id: None,
946                },
947            ],
948        );
949    }
950
951    #[test]
952    fn test_fleet_host_global_config() {
953        use LibraryConfigSource::*;
954        test_config(
955            b"",
956            b"
957config_id: abc
958apm_configuration_default:
959  DD_APM_TRACING_ENABLED: true
960  DD_RUNTIME_METRICS_ENABLED: true
961  DD_LOGS_INJECTION: true
962  DD_PROFILING_ENABLED: true
963  DD_DATA_STREAMS_ENABLED: true
964  DD_APPSEC_ENABLED: true
965  DD_IAST_ENABLED: true
966  DD_DYNAMIC_INSTRUMENTATION_ENABLED: true
967  FOO_BAR: quoicoubeh
968  DD_DATA_JOBS_ENABLED: true
969  DD_APPSEC_SCA_ENABLED: true
970wtf:
971- 1
972    ",
973            vec![
974                LibraryConfig {
975                    name: "DD_APM_TRACING_ENABLED".to_owned(),
976                    value: "true".to_owned(),
977                    source: FleetStableConfig,
978                    config_id: Some("abc".to_owned()),
979                },
980                LibraryConfig {
981                    name: "DD_APPSEC_ENABLED".to_owned(),
982                    value: "true".to_owned(),
983                    source: FleetStableConfig,
984                    config_id: Some("abc".to_owned()),
985                },
986                LibraryConfig {
987                    name: "DD_APPSEC_SCA_ENABLED".to_owned(),
988                    value: "true".to_owned(),
989                    source: FleetStableConfig,
990                    config_id: Some("abc".to_owned()),
991                },
992                LibraryConfig {
993                    name: "DD_DATA_JOBS_ENABLED".to_owned(),
994                    value: "true".to_owned(),
995                    source: FleetStableConfig,
996                    config_id: Some("abc".to_owned()),
997                },
998                LibraryConfig {
999                    name: "DD_DATA_STREAMS_ENABLED".to_owned(),
1000                    value: "true".to_owned(),
1001                    source: FleetStableConfig,
1002                    config_id: Some("abc".to_owned()),
1003                },
1004                LibraryConfig {
1005                    name: "DD_DYNAMIC_INSTRUMENTATION_ENABLED".to_owned(),
1006                    value: "true".to_owned(),
1007                    source: FleetStableConfig,
1008                    config_id: Some("abc".to_owned()),
1009                },
1010                LibraryConfig {
1011                    name: "DD_IAST_ENABLED".to_owned(),
1012                    value: "true".to_owned(),
1013                    source: FleetStableConfig,
1014                    config_id: Some("abc".to_owned()),
1015                },
1016                LibraryConfig {
1017                    name: "DD_LOGS_INJECTION".to_owned(),
1018                    value: "true".to_owned(),
1019                    source: FleetStableConfig,
1020                    config_id: Some("abc".to_owned()),
1021                },
1022                LibraryConfig {
1023                    name: "DD_PROFILING_ENABLED".to_owned(),
1024                    value: "true".to_owned(),
1025                    source: FleetStableConfig,
1026                    config_id: Some("abc".to_owned()),
1027                },
1028                LibraryConfig {
1029                    name: "DD_RUNTIME_METRICS_ENABLED".to_owned(),
1030                    value: "true".to_owned(),
1031                    source: FleetStableConfig,
1032                    config_id: Some("abc".to_owned()),
1033                },
1034                LibraryConfig {
1035                    name: "FOO_BAR".to_owned(),
1036                    value: "quoicoubeh".to_owned(),
1037                    source: FleetStableConfig,
1038                    config_id: Some("abc".to_owned()),
1039                },
1040            ],
1041        );
1042    }
1043
1044    #[test]
1045    fn test_merge_local_fleet() {
1046        use LibraryConfigSource::*;
1047
1048        test_config(
1049            b"
1050apm_configuration_default:
1051  DD_APM_TRACING_ENABLED: true
1052  DD_RUNTIME_METRICS_ENABLED: true
1053  DD_PROFILING_ENABLED: true
1054        ",
1055            b"
1056config_id: abc
1057apm_configuration_default:
1058  DD_APM_TRACING_ENABLED: true
1059  DD_LOGS_INJECTION: true
1060  DD_PROFILING_ENABLED: false
1061",
1062            vec![
1063                LibraryConfig {
1064                    name: "DD_APM_TRACING_ENABLED".to_owned(),
1065                    value: "true".to_owned(),
1066                    source: FleetStableConfig,
1067                    config_id: Some("abc".to_owned()),
1068                },
1069                LibraryConfig {
1070                    name: "DD_LOGS_INJECTION".to_owned(),
1071                    value: "true".to_owned(),
1072                    source: FleetStableConfig,
1073                    config_id: Some("abc".to_owned()),
1074                },
1075                LibraryConfig {
1076                    name: "DD_PROFILING_ENABLED".to_owned(),
1077                    value: "false".to_owned(),
1078                    source: FleetStableConfig,
1079                    config_id: Some("abc".to_owned()),
1080                },
1081                LibraryConfig {
1082                    name: "DD_RUNTIME_METRICS_ENABLED".to_owned(),
1083                    value: "true".to_owned(),
1084                    source: LocalStableConfig,
1085                    config_id: None,
1086                },
1087            ],
1088        );
1089    }
1090
1091    #[test]
1092    fn test_process_config() {
1093        test_config(
1094    b"
1095config_id: abc
1096tags:
1097  cluster_name: my_cluster 
1098rules:
1099- selectors:
1100  - origin: language
1101    matches: [\"java\"]
1102    operator: equals
1103  - origin: process_arguments
1104    key: \"-Djava_config_key\"
1105    operator: exists
1106  - origin: process_arguments
1107    matches: [\"HelloWorld.jar\"]
1108    operator: equals
1109  configuration:
1110    DD_SERVICE: my_service_{{ tags[cluster_name] }}_{{ process_arguments[-Djava_config_key] }}_{{ language }}
1111    ",
1112    b"", 
1113    vec![LibraryConfig {
1114            name: "DD_SERVICE".to_string(),
1115            value: "my_service_my_cluster_my_config_java".to_string(),
1116            source: LibraryConfigSource::LocalStableConfig,
1117            config_id: Some("abc".to_string()),
1118        }],
1119        );
1120    }
1121
1122    #[test]
1123    fn test_parse_static_config() {
1124        let mut tmp = tempfile::NamedTempFile::new().unwrap();
1125        tmp.reopen()
1126            .unwrap()
1127            .write_all(
1128                b"
1129rules:
1130- selectors:
1131  - origin: language
1132    matches: [\"java\"]
1133    operator: equals
1134  configuration:
1135    DD_PROFILING_ENABLED: true
1136    DD_SERVICE: my-service
1137    # extra keys should be skipped without errors
1138    FOOBAR: maybe??
1139",
1140            )
1141            .unwrap();
1142        let configurator = Configurator::new(true);
1143        let cfg = configurator.parse_stable_config_file(tmp.as_file_mut());
1144        let config = match cfg {
1145            LoggedResult::Ok(config, _) => config,
1146            LoggedResult::Err(_) => panic!("Expected success"),
1147        };
1148        assert_eq!(
1149            config,
1150            StableConfig {
1151                config_id: None,
1152                apm_configuration_default: ConfigMap::default(),
1153                tags: HashMap::default(),
1154                rules: vec![Rule {
1155                    selectors: vec![Selector {
1156                        origin: Origin::Language,
1157                        operator: Operator::Equals {
1158                            matches: vec!["java".to_owned()]
1159                        },
1160                        key: None,
1161                    }],
1162                    configuration: ConfigMap(
1163                        vec![
1164                            ("DD_PROFILING_ENABLED".to_owned(), "true".to_owned()),
1165                            ("DD_SERVICE".to_owned(), "my-service".to_owned()),
1166                            ("FOOBAR".to_owned(), "maybe??".to_owned()),
1167                        ]
1168                        .into_boxed_slice()
1169                    ),
1170                }]
1171            }
1172        )
1173    }
1174
1175    #[test]
1176    fn test_selector_match() {
1177        let process_info = ProcessInfo {
1178            args: vec![b"-jar HelloWorld.jar".to_vec()],
1179            envp: vec![b"ENV=VAR".to_vec()],
1180            language: b"java".to_vec(),
1181        };
1182        let tags = HashMap::new();
1183        let matcher = Matcher::new(&process_info, &tags);
1184
1185        let test_cases = &[
1186            (
1187                Selector {
1188                    key: None,
1189                    origin: Origin::Language,
1190                    operator: Operator::Equals {
1191                        matches: vec!["java".to_owned()],
1192                    },
1193                },
1194                true,
1195            ),
1196            (
1197                Selector {
1198                    key: None,
1199                    origin: Origin::ProcessArguments,
1200                    operator: Operator::Equals {
1201                        matches: vec!["-jar HelloWorld.jar".to_owned()],
1202                    },
1203                },
1204                true,
1205            ),
1206            (
1207                Selector {
1208                    key: None,
1209                    origin: Origin::EnvironmentVariables,
1210                    operator: Operator::Equals {
1211                        matches: vec!["ENV=VAR".to_owned()],
1212                    },
1213                },
1214                true,
1215            ),
1216            (
1217                Selector {
1218                    key: None,
1219                    origin: Origin::Language,
1220                    operator: Operator::Equals {
1221                        matches: vec!["python".to_owned()],
1222                    },
1223                },
1224                false,
1225            ),
1226        ];
1227        for (i, (selector, matches)) in test_cases.iter().enumerate() {
1228            assert_eq!(matcher.selector_match(selector), *matches, "case {i}");
1229        }
1230    }
1231
1232    #[test]
1233    fn test_fleet_over_local() {
1234        let process_info: ProcessInfo = ProcessInfo {
1235            args: vec![
1236                b"-Djava_config_key=my_config".to_vec(),
1237                b"-jar".to_vec(),
1238                b"HelloWorld.jar".to_vec(),
1239            ],
1240            envp: vec![b"ENV=VAR".to_vec()],
1241            language: b"java".to_vec(),
1242        };
1243        let configurator = Configurator::new(true);
1244        let config = configurator
1245            .get_config_from_bytes(
1246                b"
1247config_id: abc
1248tags:
1249  cluster_name: my_cluster 
1250rules:
1251- selectors:
1252  - origin: language
1253    matches: [\"java\"]
1254    operator: equals
1255  configuration:
1256    DD_SERVICE: local
1257",
1258                b"
1259config_id: def
1260rules:
1261- selectors:
1262  - origin: language
1263    matches: [\"java\"]
1264    operator: equals
1265  configuration:
1266    DD_SERVICE: managed",
1267                process_info,
1268            )
1269            .unwrap();
1270        assert_eq!(
1271            config,
1272            vec![LibraryConfig {
1273                name: "DD_SERVICE".to_string(),
1274                value: "managed".to_string(),
1275                source: LibraryConfigSource::FleetStableConfig,
1276                config_id: Some("def".to_string()),
1277            }]
1278        );
1279    }
1280}