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