agent_data_plane_config_system/
compatibility.rs1use std::collections::HashSet;
8
9use agent_data_plane_config::Provenance;
10use datadog_agent_config::classifier::{ConfigClassifier, Pipeline, PipelineAffinity, Severity, SupportLevel};
11use saluki_error::{generic_error, GenericError};
12use tracing::{debug, error, trace, warn};
13
14use crate::ConfigurationSystem;
15
16impl ConfigurationSystem {
17 pub fn check_compatibility(&self, active_pipelines: &HashSet<Pipeline>) -> Result<(), GenericError> {
25 let classifier = ConfigClassifier::new();
26 let mut high_severity_incompatibilities = 0u32;
27 debug!("Analyzing configuration.");
28 for (key, value, provenance) in self.sources().flattened_keys() {
29 let Some(classification) = classifier.classify(&key, value) 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 provenance == Provenance::Default {
44 trace!(key = %key, "Configuration key is not set by any input.");
45 continue;
46 }
47
48 match classification.support_level {
49 SupportLevel::Incompatible(Severity::Low) => {
50 debug!("Low-severity incompatible key detected. Proceeding.")
51 }
52 SupportLevel::Partial => {
53 warn!(key = %key, "Partially supported configuration key. See documentation for details. Proceeding.")
54 }
55 SupportLevel::Incompatible(Severity::Medium) => {
56 warn!(key = %key, "Unsupported configuration key. Proceeding.")
57 }
58 SupportLevel::Incompatible(Severity::High) => {
59 error!(key = %key, "Unsupported configuration key with non-default value. ADP cannot run safely with \
60 this setting.");
61 high_severity_incompatibilities += 1;
62 }
63 SupportLevel::Ignored | SupportLevel::Unrecognized => {
64 trace!(key = %key, "Configuration key not-applicable. Silently ignoring.")
65 }
66 }
67 }
68
69 if high_severity_incompatibilities > 0 {
70 return Err(generic_error!(
71 "{high_severity_incompatibilities} incompatible configuration detected. ADP cannot start. Review error \
72 logs for details."
73 ));
74 }
75
76 Ok(())
77 }
78}
79
80#[cfg(test)]
81mod tests {
82 use std::collections::HashSet;
83
84 use datadog_agent_config::classifier::Pipeline;
85 use saluki_config::dynamic::{ConfigSetting, Provenance as StreamProvenance};
86 use serde_json::{json, Value};
87
88 use crate::system::translate_strict;
89 use crate::{source::SourceTree, ConfigurationSystem};
90
91 fn system_with(file: Value) -> ConfigurationSystem {
94 system_from(SourceTree::all_explicit(file))
95 }
96
97 fn system_from_settings(settings: &[(&str, Value, StreamProvenance)]) -> ConfigurationSystem {
100 let settings: Vec<_> = settings
101 .iter()
102 .map(|(key, value, provenance)| ConfigSetting::new(*key, value.clone(), *provenance))
103 .collect();
104
105 system_from(SourceTree::from_settings(&settings))
106 }
107
108 fn system_from(sources: SourceTree) -> ConfigurationSystem {
109 let config = translate_strict(&sources).expect("sources translate");
110 ConfigurationSystem::standalone(config, sources)
111 }
112
113 fn pipelines(active: &[Pipeline]) -> HashSet<Pipeline> {
114 active.iter().copied().collect()
115 }
116
117 fn otlp_tls_settings(cert_pem: &str, key_pem: &str) -> Value {
118 json!({
119 "otlp_config": {
120 "receiver": {
121 "protocols": {
122 "http": {
123 "tls": { "cert_pem": cert_pem, "key_pem": key_pem }
124 }
125 }
126 }
127 }
128 })
129 }
130
131 #[test]
132 fn high_severity_keys_fail_the_check_and_are_all_counted() {
133 let system = system_with(otlp_tls_settings("/etc/adp/cert.pem", "/etc/adp/key.pem"));
134
135 let error = system
136 .check_compatibility(&pipelines(&[Pipeline::Otlp]))
137 .expect_err("a high-severity incompatible key should fail the check");
138
139 assert!(error.to_string().contains("2 incompatible configuration detected"));
140 }
141
142 #[test]
143 fn a_high_severity_key_nobody_set_is_skipped() {
144 let system = system_from_settings(&[
147 (
148 "otlp_config.receiver.protocols.http.tls.cert_pem",
149 json!("/etc/adp/cert.pem"),
150 StreamProvenance::Default,
151 ),
152 (
153 "otlp_config.receiver.protocols.http.tls.key_pem",
154 json!("/etc/adp/key.pem"),
155 StreamProvenance::Default,
156 ),
157 ]);
158
159 system
160 .check_compatibility(&pipelines(&[Pipeline::Otlp]))
161 .expect("a key nobody set is not an incompatibility");
162 }
163
164 #[test]
165 fn a_high_severity_key_set_to_its_default_value_is_still_checked() {
166 let system = system_with(otlp_tls_settings("", ""));
169
170 let error = system
171 .check_compatibility(&pipelines(&[Pipeline::Otlp]))
172 .expect_err("an explicitly set key should fail the check");
173
174 assert!(error.to_string().contains("2 incompatible configuration detected"));
175 }
176
177 #[test]
178 fn a_high_severity_key_affecting_no_active_pipeline_is_skipped() {
179 let system = system_with(otlp_tls_settings("/etc/adp/cert.pem", "/etc/adp/key.pem"));
180
181 system
182 .check_compatibility(&pipelines(&[Pipeline::DogStatsD]))
183 .expect("an inactive pipeline's keys are not incompatibilities");
184 }
185
186 #[test]
187 fn lower_severity_keys_pass_and_cross_cutting_keys_ignore_active_pipelines() {
188 let system = system_with(json!({ "dogstatsd_queue_size": 2048, "min_tls_version": "tlsv1.3" }));
189 system
190 .check_compatibility(&pipelines(&[Pipeline::DogStatsD]))
191 .expect("only high-severity incompatibilities fail the check");
192
193 let cross_cutting = system_with(json!({ "heroku_dyno": true }));
194 cross_cutting
195 .check_compatibility(&pipelines(&[]))
196 .expect_err("a cross-cutting high-severity key fails the check with no pipeline active");
197 }
198
199 #[test]
200 fn keys_the_registry_does_not_know_are_ignored() {
201 let system = system_with(json!({ "not_a_real_agent_setting": true, "dogstatsd_port": 9125 }));
202
203 system
204 .check_compatibility(&pipelines(&[Pipeline::DogStatsD]))
205 .expect("unclassified keys are not incompatibilities");
206 }
207}