agent_data_plane_config_system/
compatibility.rs

1//! Compatibility checks against the raw Datadog configuration.
2//!
3//! The typed model excludes unsupported keys, so classification uses the by-key view.
4
5use std::collections::HashSet;
6
7use datadog_agent_config::classifier::{ConfigClassifier, Pipeline, PipelineAffinity, Severity, SupportLevel};
8use saluki_error::{generic_error, ErrorContext as _, GenericError};
9use tracing::{debug, error, trace, warn};
10
11use crate::ConfigurationSystem;
12
13impl ConfigurationSystem {
14    /// Checks non-default settings that affect active pipelines and logs their severity.
15    ///
16    /// # Errors
17    ///
18    /// Returns an error if flattening fails or high-severity incompatibilities exist. All keys are
19    /// checked before returning, so the error includes the total count.
20    pub fn check_compatibility(&self, active_pipelines: &HashSet<Pipeline>) -> Result<(), GenericError> {
21        let classifier = ConfigClassifier::new();
22        let mut high_severity_incompatibilities = 0u32;
23        debug!("Analyzing configuration.");
24        for (key, val) in self
25            .raw_map()
26            .flattened_keys()
27            .error_context("Unable to flatten configuration into a list of dot-separated keys.")?
28        {
29            let Some(classification) = classifier.classify(&key, &val) else {
30                continue;
31            };
32
33            let pipeline_is_active = match &classification.pipeline_affinity {
34                PipelineAffinity::Pipelines(affected) => affected.iter().any(|p| active_pipelines.contains(p)),
35                PipelineAffinity::CrossCutting => true,
36            };
37            if !pipeline_is_active {
38                continue;
39            }
40
41            // The Agent includes schema defaults even when the operator did not set them.
42            if classification.is_default {
43                trace!(key = %key, "Configuration key has a default value.");
44                continue;
45            }
46
47            match classification.support_level {
48                SupportLevel::Incompatible(Severity::Low) => {
49                    debug!("Low-severity incompatible key detected. Proceeding.")
50                }
51                SupportLevel::Partial => {
52                    warn!(key = %key, "Partially supported configuration key. See documentation for details. Proceeding.")
53                }
54                SupportLevel::Incompatible(Severity::Medium) => {
55                    warn!(key = %key, "Unsupported configuration key. Proceeding.")
56                }
57                SupportLevel::Incompatible(Severity::High) => {
58                    error!(key = %key, "Unsupported configuration key with non-default value. ADP cannot run safely with \
59                    this setting.");
60                    high_severity_incompatibilities += 1;
61                }
62                SupportLevel::Ignored | SupportLevel::Unrecognized => {
63                    trace!(key = %key, "Configuration key not-applicable. Silently ignoring.")
64                }
65            }
66        }
67
68        if high_severity_incompatibilities > 0 {
69            return Err(generic_error!(
70                "{high_severity_incompatibilities} incompatible configuration detected. ADP cannot start. Review error \
71                logs for details."
72            ));
73        }
74
75        Ok(())
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use std::collections::HashSet;
82
83    use datadog_agent_config::classifier::Pipeline;
84    use saluki_config::ConfigurationLoader;
85    use serde_json::{json, Value};
86
87    use crate::system::translate_strict;
88    use crate::{source::SourceTree, ConfigurationSystem};
89
90    async fn system_with(file: Value) -> ConfigurationSystem {
91        let (raw_map, _) = ConfigurationLoader::for_tests(Some(file), None, false).await;
92        let base = SourceTree::all_explicit(raw_map.as_typed::<Value>().expect("base extracts"));
93        let config = translate_strict(&base).expect("sources translate");
94        ConfigurationSystem::standalone(raw_map, config)
95    }
96
97    fn pipelines(active: &[Pipeline]) -> HashSet<Pipeline> {
98        active.iter().copied().collect()
99    }
100
101    fn otlp_tls_settings(cert_pem: &str, key_pem: &str) -> Value {
102        json!({
103            "otlp_config": {
104                "receiver": {
105                    "protocols": {
106                        "http": {
107                            "tls": { "cert_pem": cert_pem, "key_pem": key_pem }
108                        }
109                    }
110                }
111            }
112        })
113    }
114
115    #[tokio::test]
116    async fn high_severity_keys_fail_the_check_and_are_all_counted() {
117        let system = system_with(otlp_tls_settings("/etc/adp/cert.pem", "/etc/adp/key.pem")).await;
118
119        let error = system
120            .check_compatibility(&pipelines(&[Pipeline::Otlp]))
121            .expect_err("a high-severity incompatible key should fail the check");
122
123        assert!(error.to_string().contains("2 incompatible configuration detected"));
124    }
125
126    #[tokio::test]
127    async fn a_high_severity_key_holding_its_default_is_skipped() {
128        let system = system_with(otlp_tls_settings("", "")).await;
129
130        system
131            .check_compatibility(&pipelines(&[Pipeline::Otlp]))
132            .expect("default-valued keys are not incompatibilities");
133    }
134
135    #[tokio::test]
136    async fn a_high_severity_key_affecting_no_active_pipeline_is_skipped() {
137        let system = system_with(otlp_tls_settings("/etc/adp/cert.pem", "/etc/adp/key.pem")).await;
138
139        system
140            .check_compatibility(&pipelines(&[Pipeline::DogStatsD]))
141            .expect("an inactive pipeline's keys are not incompatibilities");
142    }
143
144    #[tokio::test]
145    async fn lower_severity_keys_pass_and_cross_cutting_keys_ignore_active_pipelines() {
146        let system = system_with(json!({ "dogstatsd_queue_size": 2048, "min_tls_version": "tlsv1.3" })).await;
147        system
148            .check_compatibility(&pipelines(&[Pipeline::DogStatsD]))
149            .expect("only high-severity incompatibilities fail the check");
150
151        let cross_cutting = system_with(json!({ "heroku_dyno": true })).await;
152        cross_cutting
153            .check_compatibility(&pipelines(&[]))
154            .expect_err("a cross-cutting high-severity key fails the check with no pipeline active");
155    }
156
157    #[tokio::test]
158    async fn keys_the_registry_does_not_know_are_ignored() {
159        let system = system_with(json!({ "not_a_real_agent_setting": true, "dogstatsd_port": 9125 })).await;
160
161        system
162            .check_compatibility(&pipelines(&[Pipeline::DogStatsD]))
163            .expect("unclassified keys are not incompatibilities");
164    }
165}