1use crate::{
4 config::{invalid, validate_name, ConnectionPolicy, Limits, RegistrationPolicy},
5 providers::LocalProvider,
6 ConnectionSpec, DiscoveryOwner, Eligibility, OwnerBuilder, RegistrationSpec,
7};
8use nadisc::{
9 managed::{ManagedProvider, RegistrationPlan, Result},
10 DnsService, Instance,
11};
12use serde::{
13 de::{Error, MapAccess, Visitor},
14 Deserialize, Serialize,
15};
16use std::{
17 collections::{BTreeMap, HashMap},
18 sync::Arc,
19};
20
21#[derive(Clone, Default, Deserialize, Serialize)]
23#[serde(default, deny_unknown_fields)]
24pub struct DiscoveryConfig {
25 pub enabled: bool,
27 pub eligibility_probe: bool,
29 #[serde(skip_serializing_if = "Option::is_none")]
31 pub default_connection: Option<String>,
32 pub limits: Limits,
34 #[serde(deserialize_with = "unique_map")]
36 pub connections: BTreeMap<String, ConnectionConfig>,
37 #[serde(deserialize_with = "unique_map")]
39 pub registrations: BTreeMap<String, RegistrationConfig>,
40}
41
42#[derive(Clone, Deserialize, Serialize)]
44#[serde(default, deny_unknown_fields)]
45pub struct ConnectionConfig {
46 pub provider: String,
48 pub startup_required: bool,
50 pub request_timeout_ms: u64,
52 pub poll_interval_ms: u64,
54 pub idle_timeout_ms: u64,
56 pub snapshot: SnapshotPolicy,
58 pub retry: RetryPolicy,
60 #[serde(deserialize_with = "optional_unique_value")]
62 pub nacos: Option<serde_json::Value>,
63 #[serde(deserialize_with = "optional_unique_value")]
65 pub consul: Option<serde_json::Value>,
66 #[serde(deserialize_with = "optional_unique_value")]
68 pub kubernetes: Option<serde_json::Value>,
69 #[serde(rename = "static")]
71 pub static_config: Option<StaticConfig>,
72 pub dns: Option<DnsConfig>,
74 pub rest: Option<RestPolicy>,
76}
77
78#[derive(Clone, Deserialize, Serialize)]
80#[serde(default, deny_unknown_fields)]
81pub struct RestPolicy {
82 pub request_timeout_ms: u64,
84 pub connect_timeout_ms: u64,
86 pub lb_strategy: String,
88 pub instance_scheme: String,
90 pub retry_get_head_on_transport_error: bool,
92 pub retry_get_head_on_retryable_status: bool,
94 pub max_attempts: usize,
96}
97impl Default for RestPolicy {
98 fn default() -> Self {
104 Self {
105 request_timeout_ms: 10000,
106 connect_timeout_ms: 2000,
107 lb_strategy: "round_robin".into(),
108 instance_scheme: "http".into(),
109 retry_get_head_on_transport_error: false,
110 retry_get_head_on_retryable_status: false,
111 max_attempts: 1,
112 }
113 }
114}
115impl RestPolicy {
116 pub fn validate(&self) -> Result<()> {
122 if self.request_timeout_ms == 0
123 || self.request_timeout_ms > 86_400_000
124 || self.connect_timeout_ms == 0
125 || self.connect_timeout_ms > self.request_timeout_ms
126 || !matches!(self.lb_strategy.as_str(), "round_robin" | "weighted")
127 || !matches!(self.instance_scheme.as_str(), "http" | "https")
128 || !(1..=16).contains(&self.max_attempts)
129 {
130 return Err(invalid("invalid per-connection REST policy"));
131 }
132 Ok(())
133 }
134}
135
136#[derive(Clone, Deserialize, Serialize)]
138#[serde(default, deny_unknown_fields)]
139pub struct SnapshotPolicy {
140 pub max_age_ms: u64,
142 pub stale_if_error_ms: u64,
144}
145impl Default for SnapshotPolicy {
146 fn default() -> Self {
152 Self {
153 max_age_ms: 15000,
154 stale_if_error_ms: 0,
155 }
156 }
157}
158
159#[derive(Clone, Deserialize, Serialize)]
161#[serde(default, deny_unknown_fields)]
162pub struct RetryPolicy {
163 pub initial_backoff_ms: u64,
165 pub max_backoff_ms: u64,
167}
168impl Default for RetryPolicy {
169 fn default() -> Self {
175 Self {
176 initial_backoff_ms: 1000,
177 max_backoff_ms: 30000,
178 }
179 }
180}
181
182impl Default for ConnectionConfig {
183 fn default() -> Self {
189 Self {
190 provider: String::new(),
191 startup_required: true,
192 request_timeout_ms: 3000,
193 poll_interval_ms: 5000,
194 idle_timeout_ms: 30000,
195 snapshot: SnapshotPolicy::default(),
196 retry: RetryPolicy::default(),
197 nacos: None,
198 consul: None,
199 kubernetes: None,
200 static_config: None,
201 dns: None,
202 rest: None,
203 }
204 }
205}
206
207#[derive(Clone, Deserialize, Serialize)]
209#[serde(deny_unknown_fields)]
210pub struct StaticConfig {
211 #[serde(deserialize_with = "unique_map")]
213 pub services: BTreeMap<String, Vec<Instance>>,
214}
215#[derive(Clone, Deserialize, Serialize)]
217#[serde(deny_unknown_fields)]
218pub struct DnsConfig {
219 #[serde(deserialize_with = "unique_map")]
221 pub services: BTreeMap<String, DnsTarget>,
222}
223#[derive(Clone, Deserialize, Serialize)]
225#[serde(deny_unknown_fields)]
226pub struct DnsTarget {
227 pub host: String,
229 pub port: u16,
231}
232
233#[derive(Clone, Deserialize, Serialize)]
235#[serde(default, deny_unknown_fields)]
236pub struct RegistrationConfig {
237 #[serde(rename = "qualifier", alias = "connection_ref")]
239 pub connection_ref: String,
240 pub service_name: String,
242 pub ip: Option<String>,
244 pub port: Option<u16>,
246 pub listener_ref: Option<String>,
248 pub eligibility_ref: Option<String>,
250 pub metadata: HashMap<String, String>,
252 pub startup_required: bool,
254 pub readiness_critical: bool,
256 pub monitor_interval_ms: u64,
258 pub observation_grace_ms: u64,
260 pub deregister_timeout_ms: u64,
262 pub nacos: Option<NacosRegistration>,
264 #[serde(deserialize_with = "optional_unique_value")]
266 pub consul: Option<serde_json::Value>,
267}
268
269impl Default for RegistrationConfig {
270 fn default() -> Self {
276 Self {
277 connection_ref: "primary".into(),
278 service_name: String::new(),
279 ip: None,
280 port: None,
281 listener_ref: None,
282 eligibility_ref: None,
283 metadata: HashMap::new(),
284 startup_required: true,
285 readiness_critical: false,
286 monitor_interval_ms: 5000,
287 observation_grace_ms: 15000,
288 deregister_timeout_ms: 3000,
289 nacos: None,
290 consul: None,
291 }
292 }
293}
294
295#[derive(Clone, Deserialize, Serialize)]
297#[serde(default, deny_unknown_fields)]
298pub struct NacosRegistration {
299 pub ephemeral: bool,
301 pub cluster_name: String,
303 pub weight: f64,
305}
306impl Default for NacosRegistration {
307 fn default() -> Self {
313 Self {
314 ephemeral: true,
315 cluster_name: "DEFAULT".into(),
316 weight: 1.0,
317 }
318 }
319}
320
321#[derive(Clone)]
323pub struct ListenerEndpoint {
324 pub ip: String,
326 pub port: u16,
328}
329
330impl DiscoveryConfig {
331 pub fn validate(&self) -> Result<()> {
337 self.limits
338 .validate()
339 .map_err(|error| error.at_config_path("discovery.limits"))?;
340 if !self.enabled
341 && (!self.connections.is_empty()
342 || !self.registrations.is_empty()
343 || self.default_connection.is_some()
344 || self.eligibility_probe)
345 {
346 return Err(invalid(
347 "disabled discovery cannot declare connections, registrations or probes",
348 ));
349 }
350 if self.connections.len() > self.limits.max_connections
351 || self.registrations.len() > self.limits.max_registrations
352 {
353 return Err(invalid("discovery configuration exceeds capacity"));
354 }
355 if let Some(name) = &self.default_connection {
356 if !self.connections.contains_key(name) {
357 return Err(invalid("unknown default connection")
358 .at_config_path("discovery.default_connection"));
359 }
360 }
361 for (name, connection) in &self.connections {
362 validate_name(name).map_err(|error| error.at_config_path("discovery.connections"))?;
363 connection
364 .validate()
365 .map_err(|error| error.at_config_path(format!("discovery.connections.{name}")))?;
366 }
367 for (name, registration) in &self.registrations {
368 validate_name(name).map_err(|error| error.at_config_path("discovery.registrations"))?;
369 let connection = self
370 .connections
371 .get(®istration.connection_ref)
372 .ok_or_else(|| {
373 invalid("unknown registration connection")
374 .at_config_path(format!("discovery.registrations.{name}.qualifier"))
375 })?;
376 if !connection.has_service(®istration.service_name) {
377 return Err(invalid("unknown registration service binding")
378 .at_config_path(format!("discovery.registrations.{name}.service_name")));
379 }
380 if registration.service_name.is_empty()
381 || registration.service_name.len() > 512
382 || registration
383 .ip
384 .as_ref()
385 .is_some_and(|ip| ip.is_empty() || ip.trim() != ip)
386 || registration.port == Some(0)
387 || registration.metadata.len() > 128
388 || registration
389 .metadata
390 .iter()
391 .any(|(key, value)| key.len() > 256 || value.len() > 4096)
392 {
393 return Err(invalid(
394 "invalid registration service, endpoint or metadata",
395 ));
396 }
397 registration.policy().validate(&connection.policy())?;
398 match connection.provider.as_str() {
399 "nacos" if registration.consul.is_none() => {}
400 "consul" if registration.nacos.is_none() && registration.consul.is_some() => {}
401 _ => {
402 return Err(invalid(
403 "registration attributes do not match a writable provider",
404 ))
405 }
406 }
407 }
408 Ok(())
409 }
410
411 pub async fn build(&self) -> Result<DiscoveryOwner> {
419 self.validate()?;
420 let mut builder = OwnerBuilder::default().limits(self.limits.clone());
421 if !self.enabled {
422 return builder.build();
423 }
424 if let Some(default) = &self.default_connection {
425 builder = builder.default_connection(default);
426 }
427 for (name, config) in &self.connections {
428 let mut policy = config.policy();
429 policy.startup_required |= self.registrations.values().any(|registration| {
430 registration.connection_ref == *name && registration.startup_required
431 });
432 builder = builder.connection(ConnectionSpec {
433 name: name.clone(),
434 provider: config.build().await.map_err(|error| {
435 error.at_config_path(format!("discovery.connections.{name}"))
436 })?,
437 policy,
438 })?;
439 }
440 builder.build()
441 }
442
443 pub fn resolve(
451 &self,
452 listeners: &BTreeMap<String, ListenerEndpoint>,
453 eligibility: &BTreeMap<String, Eligibility>,
454 ) -> Result<Vec<RegistrationSpec>> {
455 if !self.enabled {
456 return Ok(Vec::new());
457 }
458 self.validate()?;
459 let mut plans = Vec::new();
460 for (name, registration) in &self.registrations {
461 let listener = match ®istration.listener_ref {
462 Some(name) => Some(
463 listeners
464 .get(name)
465 .ok_or_else(|| invalid("unknown registration listener"))?,
466 ),
467 None if listeners.len() == 1 => listeners.values().next(),
468 None => None,
469 };
470 let inferred_listener = registration
471 .listener_ref
472 .as_ref()
473 .or_else(|| (listeners.len() == 1).then(|| listeners.keys().next().unwrap()));
474 let eligibility = match ®istration.eligibility_ref {
475 Some(name) => eligibility.get(name),
476 None if inferred_listener.is_some() => eligibility.get(inferred_listener.unwrap()),
477 None if eligibility.len() == 1 => eligibility.values().next(),
478 None => None,
479 }
480 .cloned()
481 .ok_or_else(|| invalid("registration eligibility source is absent or ambiguous"))?;
482 let ip = registration
483 .ip
484 .as_ref()
485 .or_else(|| listener.map(|listener| &listener.ip))
486 .ok_or_else(|| invalid("registration IP is absent"))?;
487 let port = registration
488 .port
489 .or_else(|| listener.map(|listener| listener.port))
490 .ok_or_else(|| invalid("registration port is absent"))?;
491 let mut instance =
492 Instance::new(ip.clone(), port).with_metadata_map(registration.metadata.clone());
493 if let Some(nacos) = ®istration.nacos {
494 instance = instance
495 .with_cluster(&nacos.cluster_name)
496 .with_ephemeral(nacos.ephemeral)
497 .with_weight(nacos.weight);
498 }
499 #[allow(unused_mut)]
500 let mut plan = RegistrationPlan {
501 id: name.clone(),
502 service: registration.service_name.clone(),
503 instance,
504 attributes: BTreeMap::new(),
505 };
506 if let Some(consul) = ®istration.consul {
507 #[cfg(feature = "consul")]
508 {
509 #[derive(Deserialize)]
510 #[serde(deny_unknown_fields)]
511 struct CheckConfig {
512 check: nadisc_consul::HealthCheck,
513 }
514 let config: CheckConfig = serde_json::from_value(consul.clone())
515 .map_err(|_| invalid("invalid Consul registration configuration"))?;
516 config.check.apply(&mut plan)?;
517 }
518 #[cfg(not(feature = "consul"))]
519 {
520 let _ = consul;
521 return Err(invalid("Consul provider feature is disabled"));
522 }
523 }
524 plans.push(RegistrationSpec {
525 connection_ref: registration.connection_ref.clone(),
526 plan,
527 policy: registration.policy(),
528 eligibility,
529 });
530 }
531 Ok(plans)
532 }
533}
534
535impl RegistrationConfig {
536 pub fn policy(&self) -> RegistrationPolicy {
542 RegistrationPolicy {
543 startup_required: self.startup_required,
544 readiness_critical: self.readiness_critical,
545 monitor_interval_ms: self.monitor_interval_ms,
546 observation_grace_ms: self.observation_grace_ms,
547 deregister_timeout_ms: self.deregister_timeout_ms,
548 }
549 }
550}
551
552impl ConnectionConfig {
553 pub fn has_service(&self, service: &str) -> bool {
560 match self.provider.as_str() {
561 "static" => self
562 .static_config
563 .as_ref()
564 .is_some_and(|config| config.services.contains_key(service)),
565 "dns" => self
566 .dns
567 .as_ref()
568 .is_some_and(|config| config.services.contains_key(service)),
569 "nacos" | "consul" | "kubernetes" => {
570 let value = match self.provider.as_str() {
571 "nacos" => &self.nacos,
572 "consul" => &self.consul,
573 _ => &self.kubernetes,
574 };
575 value
576 .as_ref()
577 .and_then(|value| value.get("services"))
578 .and_then(serde_json::Value::as_object)
579 .is_some_and(|services| services.contains_key(service))
580 }
581 _ => false,
582 }
583 }
584
585 pub fn policy(&self) -> ConnectionPolicy {
591 ConnectionPolicy {
592 startup_required: self.startup_required,
593 request_timeout_ms: self.request_timeout_ms,
594 poll_interval_ms: self.poll_interval_ms,
595 max_age_ms: self.snapshot.max_age_ms,
596 stale_if_error_ms: self.snapshot.stale_if_error_ms,
597 idle_timeout_ms: self.idle_timeout_ms,
598 initial_backoff_ms: self.retry.initial_backoff_ms,
599 max_backoff_ms: self.retry.max_backoff_ms,
600 }
601 }
602
603 pub fn validate(&self) -> Result<()> {
609 self.policy().validate()?;
610 if let Some(rest) = &self.rest {
611 rest.validate()?;
612 }
613 let blocks = [
614 self.nacos.is_some(),
615 self.consul.is_some(),
616 self.kubernetes.is_some(),
617 self.static_config.is_some(),
618 self.dns.is_some(),
619 ];
620 if blocks.into_iter().filter(|present| *present).count() != 1 {
621 return Err(invalid("one provider configuration block is required"));
622 }
623 let supported = match self.provider.as_str() {
624 "static" => self.static_config.is_some(),
625 "dns" => self.dns.is_some(),
626 "nacos" => self.nacos.is_some() && cfg!(feature = "nacos"),
627 "consul" => self.consul.is_some() && cfg!(feature = "consul"),
628 "kubernetes" => self.kubernetes.is_some() && cfg!(feature = "kubernetes"),
629 _ => false,
630 };
631 if !supported {
632 return Err(
633 invalid("provider configuration or compiled feature is unavailable")
634 .at_config_path("provider"),
635 );
636 }
637 if let Some(config) = &self.static_config {
639 LocalProvider::static_services(config.services.clone())?;
640 }
641 if let Some(config) = &self.dns {
642 LocalProvider::dns_services(
643 config
644 .services
645 .iter()
646 .map(|(name, target)| {
647 (
648 name.clone(),
649 DnsService {
650 host: target.host.clone(),
651 port: target.port,
652 },
653 )
654 })
655 .collect(),
656 )?;
657 }
658 #[cfg(feature = "nacos")]
659 if let Some(value) = &self.nacos {
660 let mut config: nanacos::managed::ManagedNacosConfig =
661 serde_json::from_value(value.clone())
662 .map_err(|_| invalid("invalid Nacos provider configuration"))?;
663 config.request_timeout_ms = self.request_timeout_ms;
664 nanacos::managed::ManagedNacos::new(config)?;
665 }
666 #[cfg(feature = "consul")]
667 if let Some(value) = &self.consul {
668 let mut config: nadisc_consul::ConsulConfig = serde_json::from_value(value.clone())
669 .map_err(|_| invalid("invalid Consul provider configuration"))?;
670 config.request_timeout_ms = self.request_timeout_ms;
671 nadisc_consul::ConsulProvider::new(config)?;
672 }
673 #[cfg(feature = "kubernetes")]
674 if let Some(value) = &self.kubernetes {
675 let mut config: nadisc_kubernetes::KubernetesConfig =
676 serde_json::from_value(value.clone())
677 .map_err(|_| invalid("invalid Kubernetes provider configuration"))?;
678 config.request_timeout_ms = self.request_timeout_ms;
679 config.validate()?;
680 }
681 Ok(())
682 }
683
684 async fn build(&self) -> Result<Arc<dyn ManagedProvider>> {
690 self.validate()?;
691 match self.provider.as_str() {
692 "static" => Ok(Arc::new(LocalProvider::static_services(
693 self.static_config.as_ref().unwrap().services.clone(),
694 )?)),
695 "dns" => Ok(Arc::new(LocalProvider::dns_services(
696 self.dns
697 .as_ref()
698 .unwrap()
699 .services
700 .iter()
701 .map(|(name, target)| {
702 (
703 name.clone(),
704 DnsService {
705 host: target.host.clone(),
706 port: target.port,
707 },
708 )
709 })
710 .collect(),
711 )?)),
712 #[cfg(feature = "nacos")]
713 "nacos" => {
714 let mut config: nanacos::managed::ManagedNacosConfig =
715 serde_json::from_value(self.nacos.clone().unwrap())
716 .map_err(|_| invalid("invalid Nacos provider configuration"))?;
717 config.request_timeout_ms = self.request_timeout_ms;
718 Ok(Arc::new(nanacos::managed::ManagedNacos::new(config)?))
719 }
720 #[cfg(feature = "consul")]
721 "consul" => {
722 let mut config: nadisc_consul::ConsulConfig =
723 serde_json::from_value(self.consul.clone().unwrap())
724 .map_err(|_| invalid("invalid Consul provider configuration"))?;
725 config.request_timeout_ms = self.request_timeout_ms;
726 Ok(Arc::new(nadisc_consul::ConsulProvider::new(config)?))
727 }
728 #[cfg(feature = "kubernetes")]
729 "kubernetes" => {
730 let mut config: nadisc_kubernetes::KubernetesConfig =
731 serde_json::from_value(self.kubernetes.clone().unwrap())
732 .map_err(|_| invalid("invalid Kubernetes provider configuration"))?;
733 config.request_timeout_ms = self.request_timeout_ms;
734 Ok(Arc::new(
735 nadisc_kubernetes::KubernetesProvider::new(config).await?,
736 ))
737 }
738 _ => Err(invalid("provider is unavailable")),
739 }
740 }
741}
742
743fn optional_unique_value<'de, D: serde::Deserializer<'de>>(
750 deserializer: D,
751) -> std::result::Result<Option<serde_json::Value>, D::Error> {
752 Option::<UniqueValue>::deserialize(deserializer).map(|value| value.map(|value| value.0))
753}
754
755struct UniqueValue(serde_json::Value);
756impl<'de> Deserialize<'de> for UniqueValue {
757 fn deserialize<D: serde::Deserializer<'de>>(
764 deserializer: D,
765 ) -> std::result::Result<Self, D::Error> {
766 struct Unique;
767 impl<'de> Visitor<'de> for Unique {
768 type Value = UniqueValue;
769 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
776 formatter.write_str("a configuration value with unique fields")
777 }
778 fn visit_map<A: MapAccess<'de>>(
785 self,
786 mut map: A,
787 ) -> std::result::Result<Self::Value, A::Error> {
788 let mut values = serde_json::Map::new();
789 while let Some((key, value)) = map.next_entry::<String, UniqueValue>()? {
790 if values.insert(key, value.0).is_some() {
791 return Err(A::Error::custom("duplicate discovery field"));
792 }
793 }
794 Ok(UniqueValue(serde_json::Value::Object(values)))
795 }
796 fn visit_seq<A: serde::de::SeqAccess<'de>>(
803 self,
804 mut seq: A,
805 ) -> std::result::Result<Self::Value, A::Error> {
806 let mut values = Vec::new();
807 while let Some(value) = seq.next_element::<UniqueValue>()? {
808 values.push(value.0);
809 }
810 Ok(UniqueValue(serde_json::Value::Array(values)))
811 }
812 fn visit_str<E: Error>(self, value: &str) -> std::result::Result<Self::Value, E> {
819 Ok(UniqueValue(value.into()))
820 }
821 fn visit_bool<E: Error>(self, value: bool) -> std::result::Result<Self::Value, E> {
828 Ok(UniqueValue(value.into()))
829 }
830 fn visit_u64<E: Error>(self, value: u64) -> std::result::Result<Self::Value, E> {
837 Ok(UniqueValue(value.into()))
838 }
839 fn visit_i64<E: Error>(self, value: i64) -> std::result::Result<Self::Value, E> {
846 Ok(UniqueValue(value.into()))
847 }
848 fn visit_f64<E: Error>(self, value: f64) -> std::result::Result<Self::Value, E> {
855 serde_json::Number::from_f64(value)
856 .map(|value| UniqueValue(value.into()))
857 .ok_or_else(|| E::custom("non-finite discovery value"))
858 }
859 fn visit_unit<E: Error>(self) -> std::result::Result<Self::Value, E> {
865 Ok(UniqueValue(serde_json::Value::Null))
866 }
867 }
868 deserializer.deserialize_any(Unique)
869 }
870}
871
872fn unique_map<'de, D, T>(deserializer: D) -> std::result::Result<BTreeMap<String, T>, D::Error>
879where
880 D: serde::Deserializer<'de>,
881 T: Deserialize<'de>,
882{
883 struct Unique<T>(std::marker::PhantomData<T>);
884 impl<'de, T: Deserialize<'de>> Visitor<'de> for Unique<T> {
885 type Value = BTreeMap<String, T>;
886 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
893 formatter.write_str("a map with unique names")
894 }
895 fn visit_map<A: MapAccess<'de>>(
902 self,
903 mut map: A,
904 ) -> std::result::Result<Self::Value, A::Error> {
905 let mut values = BTreeMap::new();
906 while let Some((key, value)) = map.next_entry::<String, T>()? {
907 if values.insert(key, value).is_some() {
908 return Err(A::Error::custom("duplicate discovery name"));
909 }
910 }
911 Ok(values)
912 }
913 }
914 deserializer.deserialize_map(Unique(std::marker::PhantomData))
915}