1use std::collections::BTreeMap;
6use std::hash::Hasher;
7
8use async_trait::async_trait;
9use datadog_protos::agent::{
10 AdvancedAdIdentifier as ProtoAdvancedAdIdentifier, Config as ProtoConfig, ConfigEventType,
11 KubeNamespacedName as ProtoKubeNamespacedName,
12};
13use fnv::FnvHasher;
14use saluki_error::GenericError;
15use stringtheory::MetaString;
16use tokio::sync::mpsc::Receiver;
17use twox_hash::XxHash64;
18
19pub mod providers;
20
21#[derive(Debug, Clone, PartialEq, Eq)]
23pub enum EventType {
24 Schedule,
26 Unschedule,
28}
29
30impl From<ConfigEventType> for EventType {
31 fn from(event_type: ConfigEventType) -> Self {
32 match event_type {
33 ConfigEventType::Schedule => EventType::Schedule,
34 ConfigEventType::Unschedule => EventType::Unschedule,
35 }
36 }
37}
38
39impl From<i32> for EventType {
40 fn from(value: i32) -> Self {
41 if value == ConfigEventType::Unschedule as i32 {
42 EventType::Unschedule
43 } else {
44 EventType::Schedule
46 }
47 }
48}
49
50#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct KubeNamespacedName {
53 pub name: MetaString,
55 pub namespace: MetaString,
57}
58
59impl From<ProtoKubeNamespacedName> for KubeNamespacedName {
60 fn from(value: ProtoKubeNamespacedName) -> Self {
61 Self {
62 name: value.name.into(),
63 namespace: value.namespace.into(),
64 }
65 }
66}
67
68#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct KubeEndpointsIdentifier {
71 pub kube_namespaced_name: KubeNamespacedName,
73 pub resolve: MetaString,
75}
76
77#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct AdvancedADIdentifier {
80 pub kube_service: Option<KubeNamespacedName>,
82 pub kube_endpoints: Option<KubeEndpointsIdentifier>,
84}
85
86impl From<ProtoAdvancedAdIdentifier> for AdvancedADIdentifier {
87 fn from(value: ProtoAdvancedAdIdentifier) -> Self {
88 Self {
89 kube_service: value.kube_service.map(Into::into),
90 kube_endpoints: value.kube_endpoints.and_then(|endpoints| {
91 endpoints
92 .kube_namespaced_name
93 .map(|namespaced_name| KubeEndpointsIdentifier {
94 kube_namespaced_name: namespaced_name.into(),
95 resolve: endpoints.resolve.into(),
96 })
97 }),
98 }
99 }
100}
101
102pub trait RawData {
104 fn get_value(&self) -> &BTreeMap<MetaString, serde_json::Value>;
106
107 fn get(&self, key: &str) -> Option<&serde_json::Value> {
109 self.get_value().get(key)
110 }
111
112 fn to_bytes(&self) -> Result<Vec<u8>, GenericError> {
114 let mut buffer = Vec::new();
115 serde_saphyr::to_io_writer(&mut buffer, &self.get_value())?;
116 Ok(buffer)
117 }
118}
119
120#[derive(Debug, Default, Clone, PartialEq, Eq)]
122pub struct Data {
123 value: BTreeMap<MetaString, serde_json::Value>,
124}
125
126impl RawData for Data {
127 fn get_value(&self) -> &BTreeMap<MetaString, serde_json::Value> {
128 &self.value
129 }
130}
131
132#[derive(Debug, Default, Clone, PartialEq, Eq)]
134pub struct Instance {
135 id: String,
137 value: BTreeMap<MetaString, serde_json::Value>,
139}
140
141impl Instance {
142 pub fn id(&self) -> &String {
144 &self.id
145 }
146}
147
148impl RawData for Instance {
149 fn get_value(&self) -> &BTreeMap<MetaString, serde_json::Value> {
150 &self.value
151 }
152}
153
154#[derive(Debug, Clone)]
156pub struct Config {
157 pub name: MetaString,
159 pub init_config: Data,
161 pub instances: Vec<Data>,
163 pub metric_config: Data,
165 pub logs_config: Data,
167 pub ad_identifiers: Vec<MetaString>,
169 pub advanced_ad_identifiers: Vec<AdvancedADIdentifier>,
171 pub provider: MetaString,
173 pub service_id: MetaString,
175 pub tagger_entity: MetaString,
177 pub cluster_check: bool,
179 pub node_name: MetaString,
181 pub source: MetaString,
183 pub ignore_autodiscovery_tags: bool,
185 pub metrics_excluded: bool,
187 pub logs_excluded: bool,
189}
190
191#[derive(Debug, Clone, PartialEq, Eq)]
193pub struct CheckConfig {
194 pub name: MetaString,
196 pub init_config: Data,
198 pub instances: Vec<Instance>,
200 pub metric_config: Data,
202 pub logs_config: Data,
204 pub ad_identifiers: Vec<MetaString>,
206 pub advanced_ad_identifiers: Vec<AdvancedADIdentifier>,
208 pub provider: MetaString,
210 pub service_id: MetaString,
212 pub tagger_entity: MetaString,
214 pub cluster_check: bool,
216 pub node_name: MetaString,
218 pub source: MetaString,
220 pub ignore_autodiscovery_tags: bool,
222 pub metrics_excluded: bool,
224 pub logs_excluded: bool,
226}
227
228impl Config {
229 pub fn digest(&self) -> u64 {
231 let mut h = XxHash64::with_seed(0);
232
233 h.write(self.name.as_bytes());
234 for i in &self.instances {
235 h.write(&i.to_bytes().unwrap_or_default());
236 }
237 h.write(&self.init_config.to_bytes().unwrap_or_default());
238 for i in &self.ad_identifiers {
239 h.write(i.as_bytes());
240 }
241 h.write(self.node_name.as_bytes());
242 h.write(&self.logs_config.to_bytes().unwrap_or_default());
243 h.write(self.service_id.as_bytes());
244 h.write(if self.ignore_autodiscovery_tags {
245 b"true"
246 } else {
247 b"false"
248 });
249
250 h.finish()
251 }
252}
253
254impl From<ProtoConfig> for Config {
255 fn from(proto: ProtoConfig) -> Self {
256 let advanced_ad_identifiers = proto.advanced_ad_identifiers.into_iter().map(Into::into).collect();
258
259 let init_config = bytes_to_data(proto.init_config).unwrap_or_default();
260 let instances = proto
261 .instances
262 .into_iter()
263 .map(|instance| bytes_to_data(instance).unwrap_or_default())
264 .collect();
265
266 Self {
267 name: proto.name.into(),
268 init_config,
269 instances,
270 metric_config: bytes_to_data(proto.metric_config).unwrap_or_default(),
271 logs_config: bytes_to_data(proto.logs_config).unwrap_or_default(),
272 ad_identifiers: proto.ad_identifiers.into_iter().map(MetaString::from).collect(),
273 advanced_ad_identifiers,
274 provider: proto.provider.into(),
275 service_id: proto.service_id.into(),
276 tagger_entity: proto.tagger_entity.into(),
277 cluster_check: proto.cluster_check,
278 node_name: proto.node_name.into(),
279 source: proto.source.into(),
280 ignore_autodiscovery_tags: proto.ignore_autodiscovery_tags,
281 metrics_excluded: proto.metrics_excluded,
282 logs_excluded: proto.logs_excluded,
283 }
284 }
285}
286
287pub(crate) fn yaml_from_str<T>(input: &str) -> Result<T, serde_saphyr::Error>
298where
299 T: serde::de::DeserializeOwned,
300{
301 let mut budget = serde_saphyr::Budget::default();
302 budget.max_nodes = usize::MAX;
303 budget.max_events = usize::MAX;
304
305 let options = serde_saphyr::options! {
306 budget: Some(budget),
307 emit_comments: false,
308 legacy_octal_numbers: true,
309 reject_non_finite_typeless_float: false,
310 with_snippet: false,
311 };
312 serde_saphyr::from_str_with_options(input, options)
313}
314
315fn bytes_to_data(bytes: Vec<u8>) -> Result<Data, GenericError> {
316 let parse_bytes = String::from_utf8(bytes)?;
317
318 let map: BTreeMap<String, serde_json::Value> = yaml_from_str(&parse_bytes)?;
319
320 let mut result = BTreeMap::<MetaString, serde_json::Value>::new();
321
322 for (key, value) in map {
323 result.insert(key.into(), value);
324 }
325
326 Ok(Data { value: result })
327}
328
329fn instance_id(name: &str, instance: &Data, digest: u64, init_config: &Data) -> String {
330 let mut h2 = FnvHasher::default();
331 h2.write_u64(digest);
332 h2.write(&instance.to_bytes().unwrap_or_default());
333 h2.write(&init_config.to_bytes().unwrap_or_default());
334
335 let instance_name = instance_name(instance);
336 let hash2 = h2.finish();
337
338 if !instance_name.is_empty() {
339 format!("{}:{}:{:X}", name, instance_name, hash2)
340 } else {
341 format!("{}:{:X}", name, hash2)
342 }
343}
344
345fn instance_name(instance: &Data) -> String {
346 if let Some(name) = instance.get("name") {
347 if let Some(value) = name.as_str() {
348 return value.to_string();
349 }
350 }
351 if let Some(namespace) = instance.get("namespace") {
352 if let Some(value) = namespace.as_str() {
353 return value.to_string();
354 }
355 }
356 "".to_string()
357}
358
359impl From<Config> for CheckConfig {
360 fn from(config: Config) -> Self {
361 let digest = config.digest();
362 let Config {
363 name,
364 init_config,
365 instances,
366 metric_config,
367 logs_config,
368 ad_identifiers,
369 advanced_ad_identifiers,
370 provider,
371 service_id,
372 tagger_entity,
373 cluster_check,
374 node_name,
375 source,
376 ignore_autodiscovery_tags,
377 metrics_excluded,
378 logs_excluded,
379 } = config;
380 let instances = instances
381 .into_iter()
382 .map(|instance_data| Instance {
383 id: instance_id(&name, &instance_data, digest, &init_config),
384 value: instance_data.value,
385 })
386 .collect();
387
388 Self {
389 name,
390 init_config,
391 instances,
392 metric_config,
393 logs_config,
394 ad_identifiers,
395 advanced_ad_identifiers,
396 provider,
397 service_id,
398 tagger_entity,
399 cluster_check,
400 node_name,
401 source,
402 ignore_autodiscovery_tags,
403 metrics_excluded,
404 logs_excluded,
405 }
406 }
407}
408
409impl From<ProtoConfig> for AutodiscoveryEvent {
410 fn from(proto: ProtoConfig) -> AutodiscoveryEvent {
411 let event_type = EventType::from(proto.event_type);
412
413 let config = Config::from(proto);
414
415 if !config.instances.is_empty() && !config.cluster_check {
416 let check_config = CheckConfig::from(config);
417
418 if event_type == EventType::Schedule {
419 return AutodiscoveryEvent::CheckSchedule { config: check_config };
420 } else {
421 return AutodiscoveryEvent::CheckUnscheduled { config: check_config };
422 }
423 }
424
425 if event_type == EventType::Schedule {
426 AutodiscoveryEvent::Schedule { config }
427 } else {
428 AutodiscoveryEvent::Unscheduled { config }
429 }
430 }
431}
432
433#[derive(Debug, Clone)]
435#[allow(clippy::large_enum_variant)]
436pub enum AutodiscoveryEvent {
437 CheckSchedule {
439 config: CheckConfig,
441 },
442 CheckUnscheduled {
444 config: CheckConfig,
446 },
447 Schedule {
449 config: Config,
451 },
452 Unscheduled {
454 config: Config,
456 },
457}
458
459#[async_trait]
463pub trait AutodiscoveryProvider {
464 async fn subscribe(&self) -> Option<Receiver<AutodiscoveryEvent>>;
468}
469
470#[async_trait]
471impl<T> AutodiscoveryProvider for Option<T>
472where
473 T: AutodiscoveryProvider + Sync,
474{
475 async fn subscribe(&self) -> Option<Receiver<AutodiscoveryEvent>> {
476 match self.as_ref() {
477 Some(provider) => provider.subscribe().await,
478 None => None,
479 }
480 }
481}
482
483#[cfg(test)]
484mod tests {
485 use datadog_protos::agent::{
486 AdvancedAdIdentifier, KubeEndpointsIdentifier as ProtoKubeEndpointsIdentifier, KubeNamespacedName,
487 };
488
489 use super::*;
490
491 fn base_proto_config() -> ProtoConfig {
494 ProtoConfig {
495 name: "test-config".to_string(),
496 event_type: ConfigEventType::Schedule as i32,
497 init_config: b"key: value".to_vec(),
498 instances: vec![],
499 provider: "test-provider".to_string(),
500 ad_identifiers: vec!["id1".to_string(), "id2".to_string()],
501 cluster_check: false,
502 metric_config: b"metric_key: metric_value".to_vec(),
503 logs_config: b"log_key: log_value".to_vec(),
504 advanced_ad_identifiers: vec![],
505 service_id: "service-id".to_string(),
506 tagger_entity: "tagger-entity".to_string(),
507 node_name: "node-name".to_string(),
508 source: "source".to_string(),
509 ignore_autodiscovery_tags: false,
510 metrics_excluded: false,
511 logs_excluded: false,
512 }
513 }
514
515 #[test]
516 fn test_event_type_from_config_event_type() {
517 assert_eq!(EventType::from(ConfigEventType::Schedule), EventType::Schedule);
518 assert_eq!(EventType::from(ConfigEventType::Unschedule), EventType::Unschedule);
519 }
520
521 #[test]
522 fn test_event_type_from_i32() {
523 assert_eq!(EventType::from(0), EventType::Schedule); assert_eq!(EventType::from(1), EventType::Unschedule); assert_eq!(EventType::from(2), EventType::Schedule);
529 assert_eq!(EventType::from(-1), EventType::Schedule);
530 }
531
532 #[test]
533 fn test_check_config_instance_id() {
534 let proto_config = ProtoConfig {
535 name: "test-check".to_string(),
536 instances: vec![b"name: test".to_vec(), b"another_key: another_value".to_vec()],
537 ..base_proto_config()
538 };
539
540 let config = Config::from(proto_config);
541
542 let check_config = CheckConfig::from(config);
543
544 let id1 = &check_config.instances[0].id;
545 let id2 = &check_config.instances[1].id;
546
547 assert_ne!(id1, id2);
548
549 assert_eq!(id1, "test-check:test:369F074E36651C8");
550 assert_eq!(id2, "test-check:8C83712B7A572843");
551 }
552
553 #[test]
554 fn test_config_from_proto_config() {
555 let mut proto_config = ProtoConfig {
557 instances: vec![
558 b"instance_key: instance_value".to_vec(),
559 b"another_key: another_value".to_vec(),
560 ],
561 cluster_check: true,
562 ..base_proto_config()
563 };
564
565 let kube_svc = KubeNamespacedName {
566 name: "nginx".to_string(),
567 namespace: "default".to_string(),
568 };
569
570 let adv_id = AdvancedAdIdentifier {
571 kube_service: Some(kube_svc),
572 kube_endpoints: None,
573 };
574
575 proto_config.advanced_ad_identifiers = vec![adv_id];
576
577 let config = Config::from(proto_config);
578
579 assert_eq!(config.name, "test-config");
580 assert_eq!(config.provider, "test-provider");
581 assert_eq!(
582 config.ad_identifiers,
583 vec![MetaString::from_static("id1"), MetaString::from_static("id2")]
584 );
585 assert!(config.cluster_check);
586 assert_eq!(config.service_id, "service-id");
587 assert_eq!(config.tagger_entity, "tagger-entity");
588 assert_eq!(config.node_name, "node-name");
589 assert_eq!(config.source, "source");
590 assert!(!config.ignore_autodiscovery_tags);
591 assert!(!config.metrics_excluded);
592 assert!(!config.logs_excluded);
593 assert_eq!(
594 config.init_config.get("key"),
595 Some(&serde_json::Value::String("value".to_string()))
596 );
597 assert_eq!(config.instances.len(), 2);
598 assert_eq!(
599 config.instances[0].get("instance_key"),
600 Some(&serde_json::Value::String("instance_value".to_string()))
601 );
602 assert_eq!(
603 config.instances[1].get("another_key"),
604 Some(&serde_json::Value::String("another_value".to_string()))
605 );
606 assert_eq!(
607 config.metric_config.get("metric_key"),
608 Some(&serde_json::Value::String("metric_value".to_string()))
609 );
610 assert_eq!(
611 config.logs_config.get("log_key"),
612 Some(&serde_json::Value::String("log_value".to_string()))
613 );
614
615 assert_eq!(config.advanced_ad_identifiers.len(), 1);
616 let adv_id = &config.advanced_ad_identifiers[0];
617 assert!(adv_id.kube_endpoints.is_none());
618 assert!(adv_id.kube_service.is_some());
619 let svc = adv_id.kube_service.as_ref().unwrap();
620 assert_eq!(svc.name, "nginx");
621 assert_eq!(svc.namespace, "default");
622 }
623
624 #[test]
625 fn test_advanced_ad_identifier_from_proto_retains_endpoints_resolve() {
626 fn proto_id(resolve: &str) -> AdvancedAdIdentifier {
627 AdvancedAdIdentifier {
628 kube_service: None,
629 kube_endpoints: Some(ProtoKubeEndpointsIdentifier {
630 kube_namespaced_name: Some(KubeNamespacedName {
631 name: "nginx".to_string(),
632 namespace: "default".to_string(),
633 }),
634 resolve: resolve.to_string(),
635 }),
636 }
637 }
638
639 let adv_id = AdvancedADIdentifier::from(proto_id("ip"));
640
641 assert!(adv_id.kube_service.is_none());
642 let endpoints = adv_id.kube_endpoints.as_ref().expect("endpoints should be present");
643 assert_eq!(endpoints.kube_namespaced_name.name, "nginx");
644 assert_eq!(endpoints.kube_namespaced_name.namespace, "default");
645 assert_eq!(endpoints.resolve, "ip");
646
647 assert_ne!(adv_id, AdvancedADIdentifier::from(proto_id("auto")));
649 }
650
651 #[test]
652 fn test_advanced_ad_identifier_from_proto_drops_endpoints_without_namespaced_name() {
653 let proto_id = AdvancedAdIdentifier {
654 kube_service: None,
655 kube_endpoints: Some(ProtoKubeEndpointsIdentifier {
656 kube_namespaced_name: None,
657 resolve: "ip".to_string(),
658 }),
659 };
660
661 assert!(AdvancedADIdentifier::from(proto_id).kube_endpoints.is_none());
662 }
663
664 #[test]
665 fn test_autodiscovery_event_from_proto_config() {
666 let mut proto_config = ProtoConfig {
668 init_config: b"init-data".to_vec(),
669 cluster_check: true,
670 metric_config: vec![],
671 logs_config: vec![],
672 ..base_proto_config()
673 };
674
675 let kube_svc = KubeNamespacedName {
676 name: "nginx".to_string(),
677 namespace: "default".to_string(),
678 };
679
680 let adv_id = AdvancedAdIdentifier {
681 kube_service: Some(kube_svc),
682 kube_endpoints: None,
683 };
684
685 proto_config.advanced_ad_identifiers = vec![adv_id];
686
687 let event = AutodiscoveryEvent::from(proto_config.clone());
688
689 match event {
690 AutodiscoveryEvent::Schedule { config: _config } => {}
691 _ => panic!("Expected an Schedule event"),
692 }
693
694 proto_config.event_type = ConfigEventType::Unschedule as i32;
695
696 let event = AutodiscoveryEvent::from(proto_config.clone());
697
698 match event {
699 AutodiscoveryEvent::Unscheduled { config } => {
700 assert_eq!(config.name, "test-config");
701 }
702 _ => panic!("Expected an Unscheduled event"),
703 }
704
705 proto_config.instances = vec![b"instance1".to_vec(), b"instance2".to_vec()];
706 proto_config.cluster_check = false;
707 proto_config.event_type = ConfigEventType::Schedule as i32;
708
709 let event = AutodiscoveryEvent::from(proto_config.clone());
710
711 match event {
712 AutodiscoveryEvent::CheckSchedule { config: _config } => {}
713 _ => panic!("Expected an CheckSchedule event"),
714 }
715
716 proto_config.event_type = ConfigEventType::Unschedule as i32;
717
718 let event = AutodiscoveryEvent::from(proto_config);
719
720 match event {
721 AutodiscoveryEvent::CheckUnscheduled { config: _config } => {}
722 _ => panic!("Expected an CheckUnscheduled event"),
723 }
724 }
725
726 #[test]
727 fn test_yaml_errors_do_not_include_source_lines() {
728 let input = "password: hunter2hunter2\nhost: [unclosed\n";
729
730 let error = yaml_from_str::<BTreeMap<String, serde_json::Value>>(input)
731 .unwrap_err()
732 .to_string();
733
734 assert!(!error.contains("hunter2hunter2"), "error leaks source: {error}");
735 }
736
737 #[test]
738 fn test_yaml_parses_long_runs_of_comments() {
739 let mut input = String::from("username: # optional\n");
740 for i in 0..100 {
741 input.push_str(&format!("# documentation line {i}\n"));
742 }
743 input.push_str("host: localhost\n");
744
745 let data = bytes_to_data(input.into_bytes()).unwrap();
746
747 assert_eq!(data.get("username"), Some(&serde_json::Value::Null));
748 assert_eq!(
749 data.get("host"),
750 Some(&serde_json::Value::String("localhost".to_string()))
751 );
752 }
753}