1use std::collections::BTreeMap;
3use std::fmt;
4use std::hash::Hash;
5use std::io;
6use std::io::BufReader;
7use std::iter;
8use std::net::IpAddr;
9use std::net::SocketAddr;
10use std::num::NonZeroU32;
11use std::num::NonZeroUsize;
12use std::str::FromStr;
13use std::sync::Arc;
14use std::time::Duration;
15
16use connector::ConnectorConfiguration;
17use derivative::Derivative;
18use displaydoc::Display;
19use itertools::Either;
20use itertools::Itertools;
21use once_cell::sync::Lazy;
22pub(crate) use persisted_queries::PersistedQueries;
23pub(crate) use persisted_queries::PersistedQueriesPrewarmQueryPlanCache;
24#[cfg(test)]
25pub(crate) use persisted_queries::PersistedQueriesSafelist;
26use regex::Regex;
27use rustls::ServerConfig;
28use rustls::pki_types::CertificateDer;
29use rustls::pki_types::PrivateKeyDer;
30use schemars::JsonSchema;
31use schemars::Schema;
32use schemars::SchemaGenerator;
33use serde::Deserialize;
34use serde::Deserializer;
35use serde::Serialize;
36use serde_json::Map;
37use serde_json::Value;
38use sha2::Digest;
39use thiserror::Error;
40
41use self::cors::Cors;
42use self::expansion::Expansion;
43pub(crate) use self::experimental::Discussed;
44pub(crate) use self::schema::generate_config_schema;
45pub(crate) use self::schema::generate_upgrade;
46pub(crate) use self::schema::validate_yaml_configuration;
47use self::server::Server;
48use self::subgraph::SubgraphConfiguration;
49use crate::ApolloRouterError;
50use crate::cache::DEFAULT_CACHE_CAPACITY;
51use crate::configuration::cooperative_cancellation::CooperativeCancellation;
52use crate::graphql;
53use crate::notification::Notify;
54use crate::plugin::plugins;
55use crate::plugins::chaos;
56use crate::plugins::chaos::Config;
57use crate::plugins::healthcheck::Config as HealthCheck;
58#[cfg(test)]
59use crate::plugins::healthcheck::test_listen;
60use crate::plugins::limits;
61use crate::plugins::subscription::APOLLO_SUBSCRIPTION_PLUGIN;
62use crate::plugins::subscription::APOLLO_SUBSCRIPTION_PLUGIN_NAME;
63use crate::plugins::subscription::SubscriptionConfig;
64use crate::uplink::UplinkConfig;
65
66pub(crate) mod connector;
67pub(crate) mod cooperative_cancellation;
68pub(crate) mod cors;
69pub(crate) mod expansion;
70mod experimental;
71pub(crate) mod metrics;
72pub(crate) mod mode;
73mod persisted_queries;
74pub(crate) mod schema;
75pub(crate) mod server;
76pub(crate) mod shared;
77pub(crate) mod subgraph;
78#[cfg(test)]
79mod tests;
80mod upgrade;
81mod yaml;
82
83static HEARTBEAT_TIMEOUT_DURATION_SECONDS: u64 = 15;
85
86static SUPERGRAPH_ENDPOINT_REGEX: Lazy<Regex> = Lazy::new(|| {
87 Regex::new(r"(?P<first_path>.*/)(?P<sub_path>.+)\*$")
88 .expect("this regex to check the path is valid")
89});
90
91#[derive(Debug, Error, Display)]
93#[non_exhaustive]
94pub enum ConfigurationError {
95 CannotExpandVariable { key: String, cause: String },
97 UnknownExpansionMode {
99 key: String,
100 supported_modes: String,
101 },
102 PluginUnknown(String),
104 PluginConfiguration { plugin: String, error: String },
106 InvalidConfiguration {
108 message: &'static str,
109 error: String,
110 },
111 DeserializeConfigError(serde_json::Error),
113
114 InvalidExpansionModeConfig,
116
117 MigrationFailure { error: String },
119
120 CertificateAuthorities { error: String },
122}
123
124impl From<proteus::Error> for ConfigurationError {
125 fn from(error: proteus::Error) -> Self {
126 Self::MigrationFailure {
127 error: error.to_string(),
128 }
129 }
130}
131
132impl From<proteus::parser::Error> for ConfigurationError {
133 fn from(error: proteus::parser::Error) -> Self {
134 Self::MigrationFailure {
135 error: error.to_string(),
136 }
137 }
138}
139
140#[derive(Clone, Derivative, Serialize, JsonSchema)]
145#[derivative(Debug)]
146pub struct Configuration {
148 #[serde(skip)]
150 pub(crate) validated_yaml: Option<Value>,
151
152 #[serde(default)]
154 pub(crate) health_check: HealthCheck,
155
156 #[serde(default)]
158 pub(crate) sandbox: Sandbox,
159
160 #[serde(default)]
162 pub(crate) homepage: Homepage,
163
164 #[serde(default)]
166 pub(crate) server: Server,
167
168 #[serde(default)]
170 pub(crate) supergraph: Supergraph,
171
172 #[serde(default)]
174 pub(crate) cors: Cors,
175
176 #[serde(default)]
177 pub(crate) tls: Tls,
178
179 #[serde(default)]
181 pub(crate) apq: Apq,
182
183 #[serde(default)]
185 pub persisted_queries: PersistedQueries,
186
187 #[serde(default)]
189 pub(crate) limits: limits::Config,
190
191 #[serde(default)]
194 pub(crate) experimental_chaos: Config,
195
196 #[serde(default)]
198 pub(crate) plugins: UserPlugins,
199
200 #[serde(default)]
202 #[serde(flatten)]
203 pub(crate) apollo_plugins: ApolloPlugins,
204
205 #[serde(skip)]
207 pub uplink: Option<UplinkConfig>,
208
209 #[serde(default, skip_serializing, skip_deserializing)]
210 pub(crate) notify: Notify<String, graphql::Response>,
211
212 #[serde(default)]
214 pub(crate) batching: Batching,
215
216 #[serde(default)]
218 pub(crate) experimental_type_conditioned_fetching: bool,
219}
220
221impl PartialEq for Configuration {
222 fn eq(&self, other: &Self) -> bool {
223 self.validated_yaml == other.validated_yaml
224 }
225}
226
227impl<'de> serde::Deserialize<'de> for Configuration {
228 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
229 where
230 D: serde::Deserializer<'de>,
231 {
232 #[derive(Deserialize, Default)]
235 #[serde(default)]
236 struct AdHocConfiguration {
237 health_check: HealthCheck,
238 sandbox: Sandbox,
239 homepage: Homepage,
240 server: Server,
241 supergraph: Supergraph,
242 cors: Cors,
243 plugins: UserPlugins,
244 #[serde(flatten)]
245 apollo_plugins: ApolloPlugins,
246 tls: Tls,
247 apq: Apq,
248 persisted_queries: PersistedQueries,
249 limits: limits::Config,
250 experimental_chaos: chaos::Config,
251 batching: Batching,
252 experimental_type_conditioned_fetching: bool,
253 }
254 let mut ad_hoc: AdHocConfiguration = serde::Deserialize::deserialize(deserializer)?;
255
256 let notify = Configuration::notify(&ad_hoc.apollo_plugins.plugins)
257 .map_err(|e| serde::de::Error::custom(e.to_string()))?;
258
259 ad_hoc.apollo_plugins.plugins.insert(
262 "limits".to_string(),
263 serde_json::to_value(&ad_hoc.limits).unwrap(),
264 );
265 ad_hoc.apollo_plugins.plugins.insert(
266 "health_check".to_string(),
267 serde_json::to_value(&ad_hoc.health_check).unwrap(),
268 );
269
270 Configuration {
272 health_check: ad_hoc.health_check,
273 sandbox: ad_hoc.sandbox,
274 homepage: ad_hoc.homepage,
275 server: ad_hoc.server,
276 supergraph: ad_hoc.supergraph,
277 cors: ad_hoc.cors,
278 tls: ad_hoc.tls,
279 apq: ad_hoc.apq,
280 persisted_queries: ad_hoc.persisted_queries,
281 limits: ad_hoc.limits,
282 experimental_chaos: ad_hoc.experimental_chaos,
283 experimental_type_conditioned_fetching: ad_hoc.experimental_type_conditioned_fetching,
284 plugins: ad_hoc.plugins,
285 apollo_plugins: ad_hoc.apollo_plugins,
286 batching: ad_hoc.batching,
287
288 notify,
290 uplink: None,
291 validated_yaml: None,
292 }
293 .validate()
294 .map_err(|e| serde::de::Error::custom(e.to_string()))
295 }
296}
297
298pub(crate) const APOLLO_PLUGIN_PREFIX: &str = "apollo.";
299
300fn default_graphql_listen() -> ListenAddr {
301 SocketAddr::from_str("127.0.0.1:4000").unwrap().into()
302}
303
304#[cfg(test)]
305#[buildstructor::buildstructor]
306impl Configuration {
307 #[builder]
308 pub(crate) fn new(
309 supergraph: Option<Supergraph>,
310 health_check: Option<HealthCheck>,
311 sandbox: Option<Sandbox>,
312 homepage: Option<Homepage>,
313 cors: Option<Cors>,
314 plugins: Map<String, Value>,
315 apollo_plugins: Map<String, Value>,
316 tls: Option<Tls>,
317 apq: Option<Apq>,
318 persisted_query: Option<PersistedQueries>,
319 operation_limits: Option<limits::Config>,
320 chaos: Option<chaos::Config>,
321 uplink: Option<UplinkConfig>,
322 experimental_type_conditioned_fetching: Option<bool>,
323 batching: Option<Batching>,
324 server: Option<Server>,
325 ) -> Result<Self, ConfigurationError> {
326 let notify = Self::notify(&apollo_plugins)?;
327
328 let conf = Self {
329 validated_yaml: Default::default(),
330 supergraph: supergraph.unwrap_or_default(),
331 server: server.unwrap_or_default(),
332 health_check: health_check.unwrap_or_default(),
333 sandbox: sandbox.unwrap_or_default(),
334 homepage: homepage.unwrap_or_default(),
335 cors: cors.unwrap_or_default(),
336 apq: apq.unwrap_or_default(),
337 persisted_queries: persisted_query.unwrap_or_default(),
338 limits: operation_limits.unwrap_or_default(),
339 experimental_chaos: chaos.unwrap_or_default(),
340 plugins: UserPlugins {
341 plugins: Some(plugins),
342 },
343 apollo_plugins: ApolloPlugins {
344 plugins: apollo_plugins,
345 },
346 tls: tls.unwrap_or_default(),
347 uplink,
348 batching: batching.unwrap_or_default(),
349 experimental_type_conditioned_fetching: experimental_type_conditioned_fetching
350 .unwrap_or_default(),
351 notify,
352 };
353
354 conf.validate()
355 }
356}
357
358impl Configuration {
359 pub(crate) fn hash(&self) -> String {
360 let mut hasher = sha2::Sha256::new();
361 let defaulted_raw = self
362 .validated_yaml
363 .as_ref()
364 .map(|s| serde_yaml::to_string(s).expect("config was not serializable"))
365 .unwrap_or_default();
366 hasher.update(defaulted_raw);
367 let hash: String = format!("{:x}", hasher.finalize());
368 hash
369 }
370
371 fn notify(
372 apollo_plugins: &Map<String, Value>,
373 ) -> Result<Notify<String, graphql::Response>, ConfigurationError> {
374 if cfg!(test) {
375 return Ok(Notify::for_tests());
376 }
377 let notify_queue_cap = match apollo_plugins.get(APOLLO_SUBSCRIPTION_PLUGIN_NAME) {
378 Some(plugin_conf) => {
379 let conf = serde_json::from_value::<SubscriptionConfig>(plugin_conf.clone())
380 .map_err(|err| ConfigurationError::PluginConfiguration {
381 plugin: APOLLO_SUBSCRIPTION_PLUGIN.to_string(),
382 error: format!("{err:?}"),
383 })?;
384 conf.queue_capacity
385 }
386 None => None,
387 };
388 Ok(Notify::builder()
389 .and_queue_size(notify_queue_cap)
390 .ttl(Duration::from_secs(HEARTBEAT_TIMEOUT_DURATION_SECONDS))
391 .heartbeat_error_message(
392 graphql::Response::builder()
393 .errors(vec![
394 graphql::Error::builder()
395 .message("the connection has been closed because it hasn't heartbeat for a while")
396 .extension_code("SUBSCRIPTION_HEARTBEAT_ERROR")
397 .build()
398 ])
399 .build()
400 ).build())
401 }
402
403 pub(crate) fn rust_query_planner_config(
404 &self,
405 ) -> apollo_federation::query_plan::query_planner::QueryPlannerConfig {
406 use apollo_federation::query_plan::query_planner::QueryPlanIncrementalDeliveryConfig;
407 use apollo_federation::query_plan::query_planner::QueryPlannerConfig;
408 use apollo_federation::query_plan::query_planner::QueryPlannerDebugConfig;
409
410 let max_evaluated_plans = self
411 .supergraph
412 .query_planning
413 .experimental_plans_limit
414 .and_then(NonZeroU32::new)
416 .unwrap_or(NonZeroU32::new(10_000).expect("it is not zero"));
417
418 QueryPlannerConfig {
419 subgraph_graphql_validation: false,
420 generate_query_fragments: self.supergraph.generate_query_fragments,
421 incremental_delivery: QueryPlanIncrementalDeliveryConfig {
422 enable_defer: self.supergraph.defer_support,
423 },
424 type_conditioned_fetching: self.experimental_type_conditioned_fetching,
425 debug: QueryPlannerDebugConfig {
426 max_evaluated_plans,
427 paths_limit: self.supergraph.query_planning.experimental_paths_limit,
428 },
429 }
430 }
431}
432
433impl Default for Configuration {
434 fn default() -> Self {
435 Configuration::from_str("").expect("default configuration must be valid")
437 }
438}
439
440#[cfg(test)]
441#[buildstructor::buildstructor]
442impl Configuration {
443 #[builder]
444 pub(crate) fn fake_new(
445 supergraph: Option<Supergraph>,
446 health_check: Option<HealthCheck>,
447 sandbox: Option<Sandbox>,
448 homepage: Option<Homepage>,
449 cors: Option<Cors>,
450 plugins: Map<String, Value>,
451 apollo_plugins: Map<String, Value>,
452 tls: Option<Tls>,
453 notify: Option<Notify<String, graphql::Response>>,
454 apq: Option<Apq>,
455 persisted_query: Option<PersistedQueries>,
456 operation_limits: Option<limits::Config>,
457 chaos: Option<chaos::Config>,
458 uplink: Option<UplinkConfig>,
459 batching: Option<Batching>,
460 experimental_type_conditioned_fetching: Option<bool>,
461 server: Option<Server>,
462 ) -> Result<Self, ConfigurationError> {
463 let configuration = Self {
464 validated_yaml: Default::default(),
465 server: server.unwrap_or_default(),
466 supergraph: supergraph.unwrap_or_else(|| Supergraph::fake_builder().build()),
467 health_check: health_check.unwrap_or_else(|| HealthCheck::builder().build()),
468 sandbox: sandbox.unwrap_or_else(|| Sandbox::fake_builder().build()),
469 homepage: homepage.unwrap_or_else(|| Homepage::fake_builder().build()),
470 cors: cors.unwrap_or_default(),
471 limits: operation_limits.unwrap_or_default(),
472 experimental_chaos: chaos.unwrap_or_default(),
473 plugins: UserPlugins {
474 plugins: Some(plugins),
475 },
476 apollo_plugins: ApolloPlugins {
477 plugins: apollo_plugins,
478 },
479 tls: tls.unwrap_or_default(),
480 notify: notify.unwrap_or_default(),
481 apq: apq.unwrap_or_default(),
482 persisted_queries: persisted_query.unwrap_or_default(),
483 uplink,
484 experimental_type_conditioned_fetching: experimental_type_conditioned_fetching
485 .unwrap_or_default(),
486 batching: batching.unwrap_or_default(),
487 };
488
489 configuration.validate()
490 }
491}
492
493impl Configuration {
494 pub(crate) fn validate(self) -> Result<Self, ConfigurationError> {
495 if self.sandbox.enabled && self.homepage.enabled {
497 return Err(ConfigurationError::InvalidConfiguration {
498 message: "sandbox and homepage cannot be enabled at the same time",
499 error: "disable the homepage if you want to enable sandbox".to_string(),
500 });
501 }
502 if self.sandbox.enabled && !self.supergraph.introspection {
504 return Err(ConfigurationError::InvalidConfiguration {
505 message: "sandbox requires introspection",
506 error: "sandbox needs introspection to be enabled".to_string(),
507 });
508 }
509 if !self.supergraph.path.starts_with('/') {
510 return Err(ConfigurationError::InvalidConfiguration {
511 message: "invalid 'server.graphql_path' configuration",
512 error: format!(
513 "'{}' is invalid, it must be an absolute path and start with '/', you should try with '/{}'",
514 self.supergraph.path, self.supergraph.path
515 ),
516 });
517 }
518 if self.supergraph.path.ends_with('*')
519 && !self.supergraph.path.ends_with("/*")
520 && !SUPERGRAPH_ENDPOINT_REGEX.is_match(&self.supergraph.path)
521 {
522 return Err(ConfigurationError::InvalidConfiguration {
523 message: "invalid 'server.graphql_path' configuration",
524 error: format!(
525 "'{}' is invalid, you can only set a wildcard after a '/'",
526 self.supergraph.path
527 ),
528 });
529 }
530 if self.supergraph.path.contains("/*/") {
531 return Err(ConfigurationError::InvalidConfiguration {
532 message: "invalid 'server.graphql_path' configuration",
533 error: format!(
534 "'{}' is invalid, if you need to set a path like '/*/graphql' then specify it as a path parameter with a name, for example '/:my_project_key/graphql'",
535 self.supergraph.path
536 ),
537 });
538 }
539
540 if self.persisted_queries.enabled {
542 if self.persisted_queries.safelist.enabled && self.apq.enabled {
543 return Err(ConfigurationError::InvalidConfiguration {
544 message: "apqs must be disabled to enable safelisting",
545 error: "either set persisted_queries.safelist.enabled: false or apq.enabled: false in your router yaml configuration".into()
546 });
547 } else if !self.persisted_queries.safelist.enabled
548 && self.persisted_queries.safelist.require_id
549 {
550 return Err(ConfigurationError::InvalidConfiguration {
551 message: "safelist must be enabled to require IDs",
552 error: "either set persisted_queries.safelist.enabled: true or persisted_queries.safelist.require_id: false in your router yaml configuration".into()
553 });
554 }
555 } else {
556 if self.persisted_queries.safelist.enabled {
558 return Err(ConfigurationError::InvalidConfiguration {
559 message: "persisted queries must be enabled to enable safelisting",
560 error: "either set persisted_queries.safelist.enabled: false or persisted_queries.enabled: true in your router yaml configuration".into()
561 });
562 } else if self.persisted_queries.log_unknown {
563 return Err(ConfigurationError::InvalidConfiguration {
564 message: "persisted queries must be enabled to enable logging unknown operations",
565 error: "either set persisted_queries.log_unknown: false or persisted_queries.enabled: true in your router yaml configuration".into()
566 });
567 }
568 }
569
570 Ok(self)
571 }
572}
573
574impl FromStr for Configuration {
576 type Err = ConfigurationError;
577
578 fn from_str(s: &str) -> Result<Self, Self::Err> {
579 schema::validate_yaml_configuration(s, Expansion::default()?, schema::Mode::Upgrade)?
580 .validate()
581 }
582}
583
584fn gen_schema(
585 plugins: BTreeMap<String, Schema>,
586 hidden_plugins: Option<BTreeMap<String, Schema>>,
587) -> Schema {
588 schemars::json_schema!({
589 "type": "object",
590 "properties": plugins,
591 "additionalProperties": false,
592 "patternProperties": hidden_plugins
593 .unwrap_or_default()
594 .into_iter()
595 .map(|(k, v)| (format!("^{}$", k), v))
597 .collect::<BTreeMap<_, _>>()
598 })
599}
600
601#[derive(Clone, Debug, Default, Deserialize, Serialize)]
607#[serde(transparent)]
608pub(crate) struct ApolloPlugins {
609 pub(crate) plugins: Map<String, Value>,
610}
611
612impl JsonSchema for ApolloPlugins {
613 fn schema_name() -> std::borrow::Cow<'static, str> {
614 stringify!(Plugins).into()
615 }
616
617 fn json_schema(generator: &mut SchemaGenerator) -> Schema {
618 let (plugin_entries, hidden_plugin_entries): (Vec<_>, Vec<_>) = crate::plugin::plugins()
622 .sorted_by_key(|factory| factory.name.clone())
623 .filter(|factory| factory.name.starts_with(APOLLO_PLUGIN_PREFIX))
624 .partition_map(|factory| {
625 let key = factory.name[APOLLO_PLUGIN_PREFIX.len()..].to_string();
626 let schema = factory.create_schema(generator);
627 if factory.hidden_from_config_json_schema {
629 Either::Right((key, schema))
630 } else {
631 Either::Left((key, schema))
632 }
633 });
634 gen_schema(
635 plugin_entries.into_iter().collect(),
636 Some(hidden_plugin_entries.into_iter().collect()),
637 )
638 }
639}
640
641#[derive(Clone, Debug, Default, Deserialize, Serialize)]
646#[serde(transparent)]
647pub(crate) struct UserPlugins {
648 pub(crate) plugins: Option<Map<String, Value>>,
649}
650
651impl JsonSchema for UserPlugins {
652 fn schema_name() -> std::borrow::Cow<'static, str> {
653 stringify!(Plugins).into()
654 }
655
656 fn json_schema(generator: &mut SchemaGenerator) -> Schema {
657 let plugins = crate::plugin::plugins()
661 .sorted_by_key(|factory| factory.name.clone())
662 .filter(|factory| !factory.name.starts_with(APOLLO_PLUGIN_PREFIX))
663 .map(|factory| (factory.name.to_string(), factory.create_schema(generator)))
664 .collect();
665 gen_schema(plugins, None)
666 }
667}
668
669#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
671#[serde(deny_unknown_fields)]
672#[serde(default)]
673pub(crate) struct Supergraph {
674 pub(crate) listen: ListenAddr,
677
678 #[serde(deserialize_with = "humantime_serde::deserialize")]
680 #[schemars(with = "String", default = "default_connection_shutdown_timeout")]
681 pub(crate) connection_shutdown_timeout: Duration,
682
683 pub(crate) path: String,
686
687 pub(crate) introspection: bool,
690
691 pub(crate) generate_query_fragments: bool,
694
695 pub(crate) defer_support: bool,
697
698 pub(crate) query_planning: QueryPlanning,
700
701 pub(crate) early_cancel: bool,
706
707 pub(crate) experimental_log_on_broken_pipe: bool,
710}
711
712const fn default_generate_query_fragments() -> bool {
713 true
714}
715
716#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
717#[serde(rename_all = "snake_case")]
718pub(crate) enum Auto {
719 Auto,
720}
721
722fn default_defer_support() -> bool {
723 true
724}
725
726#[buildstructor::buildstructor]
727impl Supergraph {
728 #[builder]
729 pub(crate) fn new(
730 listen: Option<ListenAddr>,
731 path: Option<String>,
732 connection_shutdown_timeout: Option<Duration>,
733 introspection: Option<bool>,
734 defer_support: Option<bool>,
735 query_planning: Option<QueryPlanning>,
736 generate_query_fragments: Option<bool>,
737 early_cancel: Option<bool>,
738 experimental_log_on_broken_pipe: Option<bool>,
739 ) -> Self {
740 Self {
741 listen: listen.unwrap_or_else(default_graphql_listen),
742 path: path.unwrap_or_else(default_graphql_path),
743 connection_shutdown_timeout: connection_shutdown_timeout
744 .unwrap_or_else(default_connection_shutdown_timeout),
745 introspection: introspection.unwrap_or_else(default_graphql_introspection),
746 defer_support: defer_support.unwrap_or_else(default_defer_support),
747 query_planning: query_planning.unwrap_or_default(),
748 generate_query_fragments: generate_query_fragments
749 .unwrap_or_else(default_generate_query_fragments),
750 early_cancel: early_cancel.unwrap_or_default(),
751 experimental_log_on_broken_pipe: experimental_log_on_broken_pipe.unwrap_or_default(),
752 }
753 }
754}
755
756#[cfg(test)]
757#[buildstructor::buildstructor]
758impl Supergraph {
759 #[builder]
760 pub(crate) fn fake_new(
761 listen: Option<ListenAddr>,
762 path: Option<String>,
763 connection_shutdown_timeout: Option<Duration>,
764 introspection: Option<bool>,
765 defer_support: Option<bool>,
766 query_planning: Option<QueryPlanning>,
767 generate_query_fragments: Option<bool>,
768 early_cancel: Option<bool>,
769 experimental_log_on_broken_pipe: Option<bool>,
770 ) -> Self {
771 Self {
772 listen: listen.unwrap_or_else(test_listen),
773 path: path.unwrap_or_else(default_graphql_path),
774 connection_shutdown_timeout: connection_shutdown_timeout
775 .unwrap_or_else(default_connection_shutdown_timeout),
776 introspection: introspection.unwrap_or_else(default_graphql_introspection),
777 defer_support: defer_support.unwrap_or_else(default_defer_support),
778 query_planning: query_planning.unwrap_or_default(),
779 generate_query_fragments: generate_query_fragments
780 .unwrap_or_else(default_generate_query_fragments),
781 early_cancel: early_cancel.unwrap_or_default(),
782 experimental_log_on_broken_pipe: experimental_log_on_broken_pipe.unwrap_or_default(),
783 }
784 }
785}
786
787impl Default for Supergraph {
788 fn default() -> Self {
789 Self::builder().build()
790 }
791}
792
793impl Supergraph {
794 pub(crate) fn sanitized_path(&self) -> String {
796 let mut path = self.path.clone();
797 if self.path.ends_with("/*") {
798 path = format!("{}router_extra_path", self.path);
800 } else if SUPERGRAPH_ENDPOINT_REGEX.is_match(&self.path) {
801 let new_path = SUPERGRAPH_ENDPOINT_REGEX
802 .replace(&self.path, "${first_path}${sub_path}{supergraph_route}");
803 path = new_path.to_string();
804 }
805
806 path
807 }
808}
809
810#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, Default)]
812#[serde(deny_unknown_fields)]
813pub(crate) struct Router {
814 #[serde(default)]
815 pub(crate) cache: Cache,
816}
817
818#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
820#[serde(deny_unknown_fields, default)]
821pub(crate) struct Apq {
822 pub(crate) enabled: bool,
824
825 pub(crate) router: Router,
826
827 pub(crate) subgraph: SubgraphConfiguration<SubgraphApq>,
828}
829
830#[cfg(test)]
831#[buildstructor::buildstructor]
832impl Apq {
833 #[builder]
834 pub(crate) fn fake_new(enabled: Option<bool>) -> Self {
835 Self {
836 enabled: enabled.unwrap_or_else(default_apq),
837 ..Default::default()
838 }
839 }
840}
841
842#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
844#[serde(deny_unknown_fields, default)]
845pub(crate) struct SubgraphApq {
846 pub(crate) enabled: bool,
848}
849
850fn default_apq() -> bool {
851 true
852}
853
854impl Default for Apq {
855 fn default() -> Self {
856 Self {
857 enabled: default_apq(),
858 router: Default::default(),
859 subgraph: Default::default(),
860 }
861 }
862}
863
864#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, Default)]
866#[serde(deny_unknown_fields, default)]
867pub(crate) struct QueryPlanning {
868 pub(crate) cache: QueryPlanCache,
870 pub(crate) warmed_up_queries: Option<usize>,
875
876 pub(crate) experimental_plans_limit: Option<u32>,
884
885 pub(crate) experimental_paths_limit: Option<u32>,
898
899 pub(crate) experimental_reuse_query_plans: bool,
902
903 pub(crate) experimental_cooperative_cancellation: CooperativeCancellation,
907}
908
909#[buildstructor::buildstructor]
910impl QueryPlanning {
911 #[builder]
912 #[allow(dead_code)]
913 pub(crate) fn new(
914 cache: Option<QueryPlanCache>,
915 warmed_up_queries: Option<usize>,
916 experimental_plans_limit: Option<u32>,
917 experimental_paths_limit: Option<u32>,
918 experimental_reuse_query_plans: Option<bool>,
919 experimental_cooperative_cancellation: Option<CooperativeCancellation>,
920 ) -> Self {
921 Self {
922 cache: cache.unwrap_or_default(),
923 warmed_up_queries,
924 experimental_plans_limit,
925 experimental_paths_limit,
926 experimental_reuse_query_plans: experimental_reuse_query_plans.unwrap_or_default(),
927 experimental_cooperative_cancellation: experimental_cooperative_cancellation
928 .unwrap_or_default(),
929 }
930 }
931}
932
933#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
935#[serde(deny_unknown_fields, default)]
936pub(crate) struct QueryPlanCache {
937 pub(crate) in_memory: InMemoryCache,
939 pub(crate) redis: Option<QueryPlanRedisCache>,
941}
942
943#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
944#[serde(deny_unknown_fields)]
945pub(crate) struct QueryPlanRedisCache {
947 pub(crate) urls: Vec<url::Url>,
949
950 pub(crate) username: Option<String>,
952 pub(crate) password: Option<String>,
954
955 #[serde(deserialize_with = "humantime_serde::deserialize", default)]
956 #[schemars(with = "Option<String>", default)]
957 pub(crate) timeout: Option<Duration>,
959
960 #[serde(
961 deserialize_with = "humantime_serde::deserialize",
962 default = "default_query_plan_cache_ttl"
963 )]
964 #[schemars(with = "Option<String>", default = "default_query_plan_cache_ttl")]
965 pub(crate) ttl: Duration,
967
968 pub(crate) namespace: Option<String>,
970
971 #[serde(default)]
972 pub(crate) tls: Option<TlsClient>,
974
975 #[serde(default = "default_required_to_start")]
976 pub(crate) required_to_start: bool,
978
979 #[serde(default = "default_reset_ttl")]
980 pub(crate) reset_ttl: bool,
982
983 #[serde(default = "default_query_planner_cache_pool_size")]
984 pub(crate) pool_size: u32,
986}
987
988fn default_query_plan_cache_ttl() -> Duration {
989 Duration::from_secs(86400 * 30)
991}
992
993fn default_query_planner_cache_pool_size() -> u32 {
994 1
995}
996
997#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
999#[serde(deny_unknown_fields, default)]
1000pub(crate) struct Cache {
1001 pub(crate) in_memory: InMemoryCache,
1003 pub(crate) redis: Option<RedisCache>,
1005}
1006
1007impl From<QueryPlanCache> for Cache {
1008 fn from(value: QueryPlanCache) -> Self {
1009 Cache {
1010 in_memory: value.in_memory,
1011 redis: value.redis.map(Into::into),
1012 }
1013 }
1014}
1015
1016#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
1017#[serde(deny_unknown_fields)]
1018pub(crate) struct InMemoryCache {
1020 pub(crate) limit: NonZeroUsize,
1022}
1023
1024impl Default for InMemoryCache {
1025 fn default() -> Self {
1026 Self {
1027 limit: DEFAULT_CACHE_CAPACITY,
1028 }
1029 }
1030}
1031
1032#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
1033#[serde(deny_unknown_fields)]
1034pub(crate) struct RedisCache {
1036 pub(crate) urls: Vec<url::Url>,
1038
1039 pub(crate) username: Option<String>,
1041 pub(crate) password: Option<String>,
1043
1044 #[serde(deserialize_with = "humantime_serde::deserialize", default)]
1045 #[schemars(with = "Option<String>", default)]
1046 pub(crate) timeout: Option<Duration>,
1048
1049 #[serde(deserialize_with = "humantime_serde::deserialize", default)]
1050 #[schemars(with = "Option<String>", default)]
1051 pub(crate) ttl: Option<Duration>,
1053
1054 pub(crate) namespace: Option<String>,
1056
1057 #[serde(default)]
1058 pub(crate) tls: Option<TlsClient>,
1060
1061 #[serde(default = "default_required_to_start")]
1062 pub(crate) required_to_start: bool,
1064
1065 #[serde(default = "default_reset_ttl")]
1066 pub(crate) reset_ttl: bool,
1068
1069 #[serde(default = "default_pool_size")]
1070 pub(crate) pool_size: u32,
1072 #[serde(
1073 deserialize_with = "humantime_serde::deserialize",
1074 default = "default_metrics_interval"
1075 )]
1076 #[schemars(with = "Option<String>", default)]
1077 pub(crate) metrics_interval: Duration,
1079}
1080
1081fn default_required_to_start() -> bool {
1082 false
1083}
1084
1085fn default_pool_size() -> u32 {
1086 1
1087}
1088
1089pub(crate) fn default_metrics_interval() -> Duration {
1090 Duration::from_secs(1)
1091}
1092
1093impl From<QueryPlanRedisCache> for RedisCache {
1094 fn from(value: QueryPlanRedisCache) -> Self {
1095 RedisCache {
1096 urls: value.urls,
1097 username: value.username,
1098 password: value.password,
1099 timeout: value.timeout,
1100 ttl: Some(value.ttl),
1101 namespace: value.namespace,
1102 tls: value.tls,
1103 required_to_start: value.required_to_start,
1104 reset_ttl: value.reset_ttl,
1105 pool_size: value.pool_size,
1106 metrics_interval: default_metrics_interval(),
1107 }
1108 }
1109}
1110
1111fn default_reset_ttl() -> bool {
1112 true
1113}
1114
1115#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
1117#[serde(deny_unknown_fields)]
1118#[serde(default)]
1119pub(crate) struct Tls {
1120 pub(crate) supergraph: Option<Arc<TlsSupergraph>>,
1124 pub(crate) subgraph: SubgraphConfiguration<TlsClient>,
1126 pub(crate) connector: ConnectorConfiguration<TlsClient>,
1128}
1129
1130#[derive(Debug, Deserialize, Serialize, JsonSchema)]
1132#[serde(deny_unknown_fields)]
1133pub(crate) struct TlsSupergraph {
1134 #[serde(deserialize_with = "deserialize_certificate", skip_serializing)]
1136 #[schemars(with = "String")]
1137 pub(crate) certificate: CertificateDer<'static>,
1138 #[serde(deserialize_with = "deserialize_key", skip_serializing)]
1140 #[schemars(with = "String")]
1141 pub(crate) key: PrivateKeyDer<'static>,
1142 #[serde(deserialize_with = "deserialize_certificate_chain", skip_serializing)]
1144 #[schemars(with = "String")]
1145 pub(crate) certificate_chain: Vec<CertificateDer<'static>>,
1146}
1147
1148impl TlsSupergraph {
1149 pub(crate) fn tls_config(&self) -> Result<Arc<rustls::ServerConfig>, ApolloRouterError> {
1150 let mut certificates = vec![self.certificate.clone()];
1151 certificates.extend(self.certificate_chain.iter().cloned());
1152
1153 let mut config = ServerConfig::builder()
1154 .with_no_client_auth()
1155 .with_single_cert(certificates, self.key.clone_key())
1156 .map_err(ApolloRouterError::Rustls)?;
1157 config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
1158
1159 Ok(Arc::new(config))
1160 }
1161}
1162
1163fn deserialize_certificate<'de, D>(deserializer: D) -> Result<CertificateDer<'static>, D::Error>
1164where
1165 D: Deserializer<'de>,
1166{
1167 let data = String::deserialize(deserializer)?;
1168
1169 load_certs(&data)
1170 .map_err(serde::de::Error::custom)
1171 .and_then(|mut certs| {
1172 if certs.len() > 1 {
1173 Err(serde::de::Error::custom("expected exactly one certificate"))
1174 } else {
1175 certs
1176 .pop()
1177 .ok_or(serde::de::Error::custom("expected exactly one certificate"))
1178 }
1179 })
1180}
1181
1182fn deserialize_certificate_chain<'de, D>(
1183 deserializer: D,
1184) -> Result<Vec<CertificateDer<'static>>, D::Error>
1185where
1186 D: Deserializer<'de>,
1187{
1188 let data = String::deserialize(deserializer)?;
1189
1190 load_certs(&data).map_err(serde::de::Error::custom)
1191}
1192
1193fn deserialize_key<'de, D>(deserializer: D) -> Result<PrivateKeyDer<'static>, D::Error>
1194where
1195 D: Deserializer<'de>,
1196{
1197 let data = String::deserialize(deserializer)?;
1198
1199 load_key(&data).map_err(serde::de::Error::custom)
1200}
1201
1202#[derive(thiserror::Error, Debug)]
1203#[error("could not load TLS certificate: {0}")]
1204struct LoadCertError(std::io::Error);
1205
1206pub(crate) fn load_certs(data: &str) -> io::Result<Vec<CertificateDer<'static>>> {
1207 rustls_pemfile::certs(&mut BufReader::new(data.as_bytes()))
1208 .collect::<Result<Vec<_>, _>>()
1209 .map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, LoadCertError(error)))
1210}
1211
1212pub(crate) fn load_key(data: &str) -> io::Result<PrivateKeyDer<'static>> {
1213 let mut reader = BufReader::new(data.as_bytes());
1214 let mut key_iterator = iter::from_fn(|| rustls_pemfile::read_one(&mut reader).transpose());
1215
1216 let private_key = match key_iterator.next() {
1217 Some(Ok(rustls_pemfile::Item::Pkcs1Key(key))) => PrivateKeyDer::from(key),
1218 Some(Ok(rustls_pemfile::Item::Pkcs8Key(key))) => PrivateKeyDer::from(key),
1219 Some(Ok(rustls_pemfile::Item::Sec1Key(key))) => PrivateKeyDer::from(key),
1220 Some(Err(e)) => {
1221 return Err(io::Error::new(
1222 io::ErrorKind::InvalidInput,
1223 format!("could not parse the key: {e}"),
1224 ));
1225 }
1226 Some(_) => {
1227 return Err(io::Error::new(
1228 io::ErrorKind::InvalidInput,
1229 "expected a private key",
1230 ));
1231 }
1232 None => {
1233 return Err(io::Error::new(
1234 io::ErrorKind::InvalidInput,
1235 "could not find a private key",
1236 ));
1237 }
1238 };
1239
1240 if key_iterator.next().is_some() {
1241 return Err(io::Error::new(
1242 io::ErrorKind::InvalidInput,
1243 "expected exactly one private key",
1244 ));
1245 }
1246 Ok(private_key)
1247}
1248
1249#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
1251#[serde(deny_unknown_fields)]
1252#[serde(default)]
1253pub(crate) struct TlsClient {
1254 pub(crate) certificate_authorities: Option<String>,
1256 pub(crate) client_authentication: Option<Arc<TlsClientAuth>>,
1258}
1259
1260#[buildstructor::buildstructor]
1261impl TlsClient {
1262 #[builder]
1263 pub(crate) fn new(
1264 certificate_authorities: Option<String>,
1265 client_authentication: Option<Arc<TlsClientAuth>>,
1266 ) -> Self {
1267 Self {
1268 certificate_authorities,
1269 client_authentication,
1270 }
1271 }
1272}
1273
1274impl Default for TlsClient {
1275 fn default() -> Self {
1276 Self::builder().build()
1277 }
1278}
1279
1280#[derive(Debug, Deserialize, Serialize, JsonSchema)]
1282#[serde(deny_unknown_fields)]
1283pub(crate) struct TlsClientAuth {
1284 #[serde(deserialize_with = "deserialize_certificate_chain", skip_serializing)]
1286 #[schemars(with = "String")]
1287 pub(crate) certificate_chain: Vec<CertificateDer<'static>>,
1288 #[serde(deserialize_with = "deserialize_key", skip_serializing)]
1290 #[schemars(with = "String")]
1291 pub(crate) key: PrivateKeyDer<'static>,
1292}
1293
1294#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
1296#[serde(deny_unknown_fields)]
1297#[serde(default)]
1298pub(crate) struct Sandbox {
1299 pub(crate) enabled: bool,
1301}
1302
1303fn default_sandbox() -> bool {
1304 false
1305}
1306
1307#[buildstructor::buildstructor]
1308impl Sandbox {
1309 #[builder]
1310 pub(crate) fn new(enabled: Option<bool>) -> Self {
1311 Self {
1312 enabled: enabled.unwrap_or_else(default_sandbox),
1313 }
1314 }
1315}
1316
1317#[cfg(test)]
1318#[buildstructor::buildstructor]
1319impl Sandbox {
1320 #[builder]
1321 pub(crate) fn fake_new(enabled: Option<bool>) -> Self {
1322 Self {
1323 enabled: enabled.unwrap_or_else(default_sandbox),
1324 }
1325 }
1326}
1327
1328impl Default for Sandbox {
1329 fn default() -> Self {
1330 Self::builder().build()
1331 }
1332}
1333
1334#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
1336#[serde(deny_unknown_fields)]
1337#[serde(default)]
1338pub(crate) struct Homepage {
1339 pub(crate) enabled: bool,
1341 pub(crate) graph_ref: Option<String>,
1344}
1345
1346fn default_homepage() -> bool {
1347 true
1348}
1349
1350#[buildstructor::buildstructor]
1351impl Homepage {
1352 #[builder]
1353 pub(crate) fn new(enabled: Option<bool>) -> Self {
1354 Self {
1355 enabled: enabled.unwrap_or_else(default_homepage),
1356 graph_ref: None,
1357 }
1358 }
1359}
1360
1361#[cfg(test)]
1362#[buildstructor::buildstructor]
1363impl Homepage {
1364 #[builder]
1365 pub(crate) fn fake_new(enabled: Option<bool>) -> Self {
1366 Self {
1367 enabled: enabled.unwrap_or_else(default_homepage),
1368 graph_ref: None,
1369 }
1370 }
1371}
1372
1373impl Default for Homepage {
1374 fn default() -> Self {
1375 Self::builder().enabled(default_homepage()).build()
1376 }
1377}
1378
1379#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema)]
1381#[serde(untagged)]
1382pub enum ListenAddr {
1383 SocketAddr(SocketAddr),
1385 #[cfg(unix)]
1387 UnixSocket(std::path::PathBuf),
1388}
1389
1390impl ListenAddr {
1391 pub(crate) fn ip_and_port(&self) -> Option<(IpAddr, u16)> {
1392 #[cfg_attr(not(unix), allow(irrefutable_let_patterns))]
1393 if let Self::SocketAddr(addr) = self {
1394 Some((addr.ip(), addr.port()))
1395 } else {
1396 None
1397 }
1398 }
1399}
1400
1401impl From<SocketAddr> for ListenAddr {
1402 fn from(addr: SocketAddr) -> Self {
1403 Self::SocketAddr(addr)
1404 }
1405}
1406
1407#[allow(clippy::from_over_into)]
1408impl Into<serde_json::Value> for ListenAddr {
1409 fn into(self) -> serde_json::Value {
1410 match self {
1411 Self::SocketAddr(addr) => serde_json::Value::String(addr.to_string()),
1414 #[cfg(unix)]
1415 Self::UnixSocket(path) => serde_json::Value::String(
1416 path.as_os_str()
1417 .to_str()
1418 .expect("unsupported non-UTF-8 path")
1419 .to_string(),
1420 ),
1421 }
1422 }
1423}
1424
1425#[cfg(unix)]
1426impl From<tokio_util::either::Either<std::net::SocketAddr, tokio::net::unix::SocketAddr>>
1427 for ListenAddr
1428{
1429 fn from(
1430 addr: tokio_util::either::Either<std::net::SocketAddr, tokio::net::unix::SocketAddr>,
1431 ) -> Self {
1432 match addr {
1433 tokio_util::either::Either::Left(addr) => Self::SocketAddr(addr),
1434 tokio_util::either::Either::Right(addr) => Self::UnixSocket(
1435 addr.as_pathname()
1436 .map(ToOwned::to_owned)
1437 .unwrap_or_default(),
1438 ),
1439 }
1440 }
1441}
1442
1443impl fmt::Display for ListenAddr {
1444 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1445 match self {
1446 Self::SocketAddr(addr) => write!(f, "http://{addr}"),
1447 #[cfg(unix)]
1448 Self::UnixSocket(path) => write!(f, "{}", path.display()),
1449 }
1450 }
1451}
1452
1453fn default_graphql_path() -> String {
1454 String::from("/")
1455}
1456
1457fn default_graphql_introspection() -> bool {
1458 false
1459}
1460
1461fn default_connection_shutdown_timeout() -> Duration {
1462 Duration::from_secs(60)
1463}
1464
1465#[derive(Clone, Debug, Default, Error, Display, Serialize, Deserialize, JsonSchema)]
1466#[serde(deny_unknown_fields, rename_all = "snake_case")]
1467pub(crate) enum BatchingMode {
1468 #[default]
1470 BatchHttpLink,
1471}
1472
1473#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
1475#[serde(deny_unknown_fields)]
1476pub(crate) struct Batching {
1477 #[serde(default)]
1479 pub(crate) enabled: bool,
1480
1481 pub(crate) mode: BatchingMode,
1483
1484 pub(crate) subgraph: Option<SubgraphConfiguration<CommonBatchingConfig>>,
1486
1487 #[serde(default)]
1489 pub(crate) maximum_size: Option<usize>,
1490}
1491
1492#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
1494pub(crate) struct CommonBatchingConfig {
1495 pub(crate) enabled: bool,
1497}
1498
1499impl Batching {
1500 pub(crate) fn batch_include(&self, service_name: &str) -> bool {
1502 match &self.subgraph {
1503 Some(subgraph_batching_config) => {
1504 if subgraph_batching_config.all.enabled {
1506 subgraph_batching_config
1510 .subgraphs
1511 .get(service_name)
1512 .is_none_or(|x| x.enabled)
1513 } else {
1514 subgraph_batching_config
1517 .subgraphs
1518 .get(service_name)
1519 .is_some_and(|x| x.enabled)
1520 }
1521 }
1522 None => false,
1523 }
1524 }
1525
1526 pub(crate) fn exceeds_batch_size<T>(&self, batch: &[T]) -> bool {
1527 match self.maximum_size {
1528 Some(maximum_size) => batch.len() > maximum_size,
1529 None => false,
1530 }
1531 }
1532}