agent_data_plane_config_system/
compatibility.rs1use 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 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 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}