Skip to main content

apollo_router/configuration/
mod.rs

1//! Logic for loading configuration in to an object model
2use 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::plugin::plugins;
54use crate::plugins::chaos;
55use crate::plugins::chaos::Config;
56use crate::plugins::healthcheck::Config as HealthCheck;
57#[cfg(test)]
58use crate::plugins::healthcheck::test_listen;
59use crate::plugins::limits;
60use crate::plugins::subscription::APOLLO_SUBSCRIPTION_PLUGIN;
61use crate::plugins::subscription::APOLLO_SUBSCRIPTION_PLUGIN_NAME;
62use crate::plugins::subscription::SubscriptionConfig;
63use crate::plugins::subscription::notification::Notify;
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
83// TODO: Talk it through with the teams
84static 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
91static TRAILING_SLASH: Lazy<Regex> =
92    Lazy::new(|| Regex::new(r"/+$").expect("this regex to check the path has no trailing slash"));
93
94/// Configuration error.
95#[derive(Debug, Error, Display)]
96#[non_exhaustive]
97pub enum ConfigurationError {
98    /// could not expand variable: {key}, {cause}
99    CannotExpandVariable { key: String, cause: String },
100    /// could not expand variable: {key}. Variables must be prefixed with one of '{supported_modes}' followed by '.' e.g. 'env.'
101    UnknownExpansionMode {
102        key: String,
103        supported_modes: String,
104    },
105    /// unknown plugin {0}
106    PluginUnknown(String),
107    /// plugin {plugin} could not be configured: {error}
108    PluginConfiguration { plugin: String, error: String },
109    /// {message}: {error}
110    InvalidConfiguration {
111        message: &'static str,
112        error: String,
113    },
114    /// could not deserialize configuration: {0}
115    DeserializeConfigError(serde_json::Error),
116
117    /// APOLLO_ROUTER_CONFIG_SUPPORTED_MODES must be of the format env,file,... Possible modes are 'env' and 'file'.
118    InvalidExpansionModeConfig,
119
120    /// could not migrate configuration: {error}.
121    MigrationFailure { error: String },
122
123    /// could not load certificate authorities: {error}
124    CertificateAuthorities { error: String },
125}
126
127impl From<proteus::Error> for ConfigurationError {
128    fn from(error: proteus::Error) -> Self {
129        Self::MigrationFailure {
130            error: error.to_string(),
131        }
132    }
133}
134
135impl From<proteus::parser::Error> for ConfigurationError {
136    fn from(error: proteus::parser::Error) -> Self {
137        Self::MigrationFailure {
138            error: error.to_string(),
139        }
140    }
141}
142
143/// The configuration for the router.
144///
145/// Can be created through `serde::Deserialize` from various formats,
146/// or inline in Rust code with `serde_json::json!` and `serde_json::from_value`.
147#[derive(Clone, Derivative, Serialize, JsonSchema)]
148#[derivative(Debug)]
149// We can't put a global #[serde(default)] here because of the Default implementation using `from_str` which use deserialize
150pub struct Configuration {
151    /// The raw configuration value.
152    #[serde(skip)]
153    pub(crate) validated_yaml: Option<Value>,
154
155    /// The full router yaml before it was parsed and env variables expanded
156    #[serde(skip)]
157    pub(crate) raw_yaml: Option<Arc<str>>,
158
159    /// Health check configuration
160    #[serde(default)]
161    pub(crate) health_check: HealthCheck,
162
163    /// Sandbox configuration
164    #[serde(default)]
165    pub(crate) sandbox: Sandbox,
166
167    /// Homepage configuration
168    #[serde(default)]
169    pub(crate) homepage: Homepage,
170
171    /// Configuration for the server
172    #[serde(default)]
173    pub(crate) server: Server,
174
175    /// Configuration for the supergraph
176    #[serde(default)]
177    pub(crate) supergraph: Supergraph,
178
179    /// Cross origin request headers.
180    #[serde(default)]
181    pub(crate) cors: Cors,
182
183    #[serde(default)]
184    pub(crate) tls: Tls,
185
186    /// Configures automatic persisted queries
187    #[serde(default)]
188    pub(crate) apq: Apq,
189
190    /// Configures managed persisted queries
191    #[serde(default)]
192    pub persisted_queries: PersistedQueries,
193
194    /// Configuration for operation limits, parser limits, HTTP limits, etc.
195    #[serde(default)]
196    pub(crate) limits: limits::Config,
197
198    /// Configuration for chaos testing, trying to reproduce bugs that require uncommon conditions.
199    /// You probably don’t want this in production!
200    #[serde(default)]
201    pub(crate) experimental_chaos: Config,
202
203    /// Plugin configuration
204    #[serde(default)]
205    pub(crate) plugins: UserPlugins,
206
207    /// Built-in plugin configuration. Built in plugins are pushed to the top level of config.
208    #[serde(default)]
209    #[serde(flatten)]
210    pub(crate) apollo_plugins: ApolloPlugins,
211
212    /// Uplink configuration.
213    #[serde(skip)]
214    pub uplink: Option<UplinkConfig>,
215
216    // FIXME(@goto-bus-stop): Sticking this on Configuration is a serious hack just to have
217    // it available everywhere, it is actually not configuration at all
218    #[serde(default, skip_serializing, skip_deserializing)]
219    pub(crate) notify: Notify<String, graphql::Response>,
220
221    /// Batching configuration.
222    #[serde(default)]
223    pub(crate) batching: Batching,
224
225    /// Type conditioned fetching configuration.
226    #[serde(default)]
227    pub(crate) experimental_type_conditioned_fetching: bool,
228
229    /// When enabled for specific subgraphs, orphan errors (those without a valid
230    /// `_entities` path) are assigned to the nearest non-array ancestor in the
231    /// response path, preventing them from being duplicated across every array
232    /// element.
233    #[serde(default)]
234    pub(crate) experimental_hoist_orphan_errors: SubgraphConfiguration<HoistOrphanErrors>,
235}
236
237impl PartialEq for Configuration {
238    fn eq(&self, other: &Self) -> bool {
239        self.validated_yaml == other.validated_yaml
240    }
241}
242
243impl<'de> serde::Deserialize<'de> for Configuration {
244    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
245    where
246        D: serde::Deserializer<'de>,
247    {
248        // This intermediate structure will allow us to deserialize a Configuration
249        // yet still exercise the Configuration validation function
250        #[derive(Deserialize, Default)]
251        #[serde(default)]
252        struct AdHocConfiguration {
253            health_check: HealthCheck,
254            sandbox: Sandbox,
255            homepage: Homepage,
256            server: Server,
257            supergraph: Supergraph,
258            cors: Cors,
259            plugins: UserPlugins,
260            #[serde(flatten)]
261            apollo_plugins: ApolloPlugins,
262            tls: Tls,
263            apq: Apq,
264            persisted_queries: PersistedQueries,
265            limits: limits::Config,
266            experimental_chaos: chaos::Config,
267            batching: Batching,
268            experimental_type_conditioned_fetching: bool,
269            experimental_hoist_orphan_errors: SubgraphConfiguration<HoistOrphanErrors>,
270        }
271        let mut ad_hoc: AdHocConfiguration = serde::Deserialize::deserialize(deserializer)?;
272
273        let notify = Configuration::notify(&ad_hoc.apollo_plugins.plugins)
274            .map_err(|e| serde::de::Error::custom(e.to_string()))?;
275
276        // Allow the limits plugin to use the configuration from the configuration struct.
277        // This means that the limits plugin will get the regular configuration via plugin init.
278        ad_hoc.apollo_plugins.plugins.insert(
279            "limits".to_string(),
280            serde_json::to_value(&ad_hoc.limits).unwrap(),
281        );
282        ad_hoc.apollo_plugins.plugins.insert(
283            "health_check".to_string(),
284            serde_json::to_value(&ad_hoc.health_check).unwrap(),
285        );
286
287        // Use a struct literal instead of a builder to ensure this is exhaustive
288        Configuration {
289            health_check: ad_hoc.health_check,
290            sandbox: ad_hoc.sandbox,
291            homepage: ad_hoc.homepage,
292            server: ad_hoc.server,
293            supergraph: ad_hoc.supergraph,
294            cors: ad_hoc.cors,
295            tls: ad_hoc.tls,
296            apq: ad_hoc.apq,
297            persisted_queries: ad_hoc.persisted_queries,
298            limits: ad_hoc.limits,
299            experimental_chaos: ad_hoc.experimental_chaos,
300            experimental_type_conditioned_fetching: ad_hoc.experimental_type_conditioned_fetching,
301            experimental_hoist_orphan_errors: ad_hoc.experimental_hoist_orphan_errors,
302            plugins: ad_hoc.plugins,
303            apollo_plugins: ad_hoc.apollo_plugins,
304            batching: ad_hoc.batching,
305
306            // serde(skip)
307            notify,
308            uplink: None,
309            validated_yaml: None,
310            raw_yaml: None,
311        }
312        .validate()
313        .map_err(|e| serde::de::Error::custom(e.to_string()))
314    }
315}
316
317pub(crate) const APOLLO_PLUGIN_PREFIX: &str = "apollo.";
318
319fn default_graphql_listen() -> ListenAddr {
320    SocketAddr::from_str("127.0.0.1:4000").unwrap().into()
321}
322
323#[cfg(test)]
324#[buildstructor::buildstructor]
325impl Configuration {
326    #[builder]
327    pub(crate) fn new(
328        supergraph: Option<Supergraph>,
329        health_check: Option<HealthCheck>,
330        sandbox: Option<Sandbox>,
331        homepage: Option<Homepage>,
332        cors: Option<Cors>,
333        plugins: Map<String, Value>,
334        apollo_plugins: Map<String, Value>,
335        tls: Option<Tls>,
336        apq: Option<Apq>,
337        persisted_query: Option<PersistedQueries>,
338        operation_limits: Option<limits::Config>,
339        chaos: Option<chaos::Config>,
340        uplink: Option<UplinkConfig>,
341        experimental_type_conditioned_fetching: Option<bool>,
342        experimental_hoist_orphan_errors: Option<SubgraphConfiguration<HoistOrphanErrors>>,
343        batching: Option<Batching>,
344        server: Option<Server>,
345    ) -> Result<Self, ConfigurationError> {
346        let notify = Self::notify(&apollo_plugins)?;
347
348        let conf = Self {
349            validated_yaml: Default::default(),
350            raw_yaml: None,
351            supergraph: supergraph.unwrap_or_default(),
352            server: server.unwrap_or_default(),
353            health_check: health_check.unwrap_or_default(),
354            sandbox: sandbox.unwrap_or_default(),
355            homepage: homepage.unwrap_or_default(),
356            cors: cors.unwrap_or_default(),
357            apq: apq.unwrap_or_default(),
358            persisted_queries: persisted_query.unwrap_or_default(),
359            limits: operation_limits.unwrap_or_default(),
360            experimental_chaos: chaos.unwrap_or_default(),
361            plugins: UserPlugins {
362                plugins: Some(plugins),
363            },
364            apollo_plugins: ApolloPlugins {
365                plugins: apollo_plugins,
366            },
367            tls: tls.unwrap_or_default(),
368            uplink,
369            batching: batching.unwrap_or_default(),
370            experimental_type_conditioned_fetching: experimental_type_conditioned_fetching
371                .unwrap_or_default(),
372            experimental_hoist_orphan_errors: experimental_hoist_orphan_errors.unwrap_or_default(),
373            notify,
374        };
375
376        conf.validate()
377    }
378}
379
380impl Configuration {
381    pub(crate) fn hash(&self) -> String {
382        let mut hasher = sha2::Sha256::new();
383        let defaulted_raw = self
384            .validated_yaml
385            .as_ref()
386            .map(|s| serde_yaml::to_string(s).expect("config was not serializable"))
387            .unwrap_or_default();
388        hasher.update(defaulted_raw);
389        let hash: String = format!("{:x}", hasher.finalize());
390        hash
391    }
392
393    fn notify(
394        apollo_plugins: &Map<String, Value>,
395    ) -> Result<Notify<String, graphql::Response>, ConfigurationError> {
396        if cfg!(test) {
397            return Ok(Notify::for_tests());
398        }
399        let notify_queue_cap = match apollo_plugins.get(APOLLO_SUBSCRIPTION_PLUGIN_NAME) {
400            Some(plugin_conf) => {
401                let conf = serde_json::from_value::<SubscriptionConfig>(plugin_conf.clone())
402                    .map_err(|err| ConfigurationError::PluginConfiguration {
403                        plugin: APOLLO_SUBSCRIPTION_PLUGIN.to_string(),
404                        error: format!("{err:?}"),
405                    })?;
406                conf.queue_capacity
407            }
408            None => None,
409        };
410        Ok(Notify::builder()
411            .and_queue_size(notify_queue_cap)
412            .ttl(Duration::from_secs(HEARTBEAT_TIMEOUT_DURATION_SECONDS))
413            .heartbeat_error_message(
414                graphql::Response::builder()
415                .errors(vec![
416                    graphql::Error::builder()
417                    .message("the connection has been closed because it hasn't heartbeat for a while")
418                    .extension_code("SUBSCRIPTION_HEARTBEAT_ERROR")
419                    .build()
420                ])
421                .build()
422            ).build())
423    }
424
425    pub(crate) fn rust_query_planner_config(
426        &self,
427    ) -> apollo_federation::query_plan::query_planner::QueryPlannerConfig {
428        use apollo_federation::query_plan::query_planner::QueryPlanIncrementalDeliveryConfig;
429        use apollo_federation::query_plan::query_planner::QueryPlannerConfig;
430        use apollo_federation::query_plan::query_planner::QueryPlannerDebugConfig;
431
432        let max_evaluated_plans = self
433            .supergraph
434            .query_planning
435            .experimental_plans_limit
436            // Fails if experimental_plans_limit is zero; use our default.
437            .and_then(NonZeroU32::new)
438            .unwrap_or(NonZeroU32::new(10_000).expect("it is not zero"));
439
440        QueryPlannerConfig {
441            subgraph_graphql_validation: false,
442            generate_query_fragments: self.supergraph.generate_query_fragments,
443            incremental_delivery: QueryPlanIncrementalDeliveryConfig {
444                enable_defer: self.supergraph.defer_support,
445            },
446            type_conditioned_fetching: self.experimental_type_conditioned_fetching,
447            debug: QueryPlannerDebugConfig {
448                max_evaluated_plans,
449                paths_limit: self.supergraph.query_planning.experimental_paths_limit,
450            },
451        }
452    }
453
454    fn apollo_plugin_enabled(&self, plugin_name: &str) -> bool {
455        self.apollo_plugins
456            .plugins
457            .get(plugin_name)
458            .and_then(|config| config.as_object().and_then(|c| c.get("enabled")))
459            .and_then(|enabled| enabled.as_bool())
460            .unwrap_or(false)
461    }
462}
463
464impl Default for Configuration {
465    fn default() -> Self {
466        // We want to trigger all defaulting logic so don't use the raw builder.
467        Configuration::from_str("").expect("default configuration must be valid")
468    }
469}
470
471#[cfg(test)]
472#[buildstructor::buildstructor]
473impl Configuration {
474    #[builder]
475    pub(crate) fn fake_new(
476        supergraph: Option<Supergraph>,
477        health_check: Option<HealthCheck>,
478        sandbox: Option<Sandbox>,
479        homepage: Option<Homepage>,
480        cors: Option<Cors>,
481        plugins: Map<String, Value>,
482        apollo_plugins: Map<String, Value>,
483        tls: Option<Tls>,
484        notify: Option<Notify<String, graphql::Response>>,
485        apq: Option<Apq>,
486        persisted_query: Option<PersistedQueries>,
487        operation_limits: Option<limits::Config>,
488        chaos: Option<chaos::Config>,
489        uplink: Option<UplinkConfig>,
490        batching: Option<Batching>,
491        experimental_type_conditioned_fetching: Option<bool>,
492        server: Option<Server>,
493    ) -> Result<Self, ConfigurationError> {
494        let configuration = Self {
495            validated_yaml: Default::default(),
496            server: server.unwrap_or_default(),
497            supergraph: supergraph.unwrap_or_else(|| Supergraph::fake_builder().build()),
498            health_check: health_check.unwrap_or_else(|| HealthCheck::builder().build()),
499            sandbox: sandbox.unwrap_or_else(|| Sandbox::fake_builder().build()),
500            homepage: homepage.unwrap_or_else(|| Homepage::fake_builder().build()),
501            cors: cors.unwrap_or_default(),
502            limits: operation_limits.unwrap_or_default(),
503            experimental_chaos: chaos.unwrap_or_default(),
504            plugins: UserPlugins {
505                plugins: Some(plugins),
506            },
507            apollo_plugins: ApolloPlugins {
508                plugins: apollo_plugins,
509            },
510            tls: tls.unwrap_or_default(),
511            notify: notify.unwrap_or_default(),
512            apq: apq.unwrap_or_default(),
513            persisted_queries: persisted_query.unwrap_or_default(),
514            uplink,
515            experimental_type_conditioned_fetching: experimental_type_conditioned_fetching
516                .unwrap_or_default(),
517            experimental_hoist_orphan_errors: Default::default(),
518            batching: batching.unwrap_or_default(),
519            raw_yaml: None,
520        };
521
522        configuration.validate()
523    }
524}
525
526impl Configuration {
527    pub(crate) fn validate(self) -> Result<Self, ConfigurationError> {
528        // Sandbox and Homepage cannot be both enabled
529        if self.sandbox.enabled && self.homepage.enabled {
530            return Err(ConfigurationError::InvalidConfiguration {
531                message: "sandbox and homepage cannot be enabled at the same time",
532                error: "disable the homepage if you want to enable sandbox".to_string(),
533            });
534        }
535        // Sandbox needs Introspection to be enabled
536        if self.sandbox.enabled && !self.supergraph.introspection {
537            return Err(ConfigurationError::InvalidConfiguration {
538                message: "sandbox requires introspection",
539                error: "sandbox needs introspection to be enabled".to_string(),
540            });
541        }
542        if !self.supergraph.path.starts_with('/') {
543            return Err(ConfigurationError::InvalidConfiguration {
544                message: "invalid 'server.graphql_path' configuration",
545                error: format!(
546                    "'{}' is invalid, it must be an absolute path and start with '/', you should try with '/{}'",
547                    self.supergraph.path, self.supergraph.path
548                ),
549            });
550        }
551        if self.supergraph.path.ends_with('*')
552            && !self.supergraph.path.ends_with("/*")
553            && !SUPERGRAPH_ENDPOINT_REGEX.is_match(&self.supergraph.path)
554        {
555            return Err(ConfigurationError::InvalidConfiguration {
556                message: "invalid 'server.graphql_path' configuration",
557                error: format!(
558                    "'{}' is invalid, you can only set a wildcard after a '/'",
559                    self.supergraph.path
560                ),
561            });
562        }
563        if self.supergraph.path.contains("/*/") {
564            return Err(ConfigurationError::InvalidConfiguration {
565                message: "invalid 'server.graphql_path' configuration",
566                error: format!(
567                    "'{}' 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'",
568                    self.supergraph.path
569                ),
570            });
571        }
572
573        // PQs.
574        if self.persisted_queries.enabled {
575            if self.persisted_queries.safelist.enabled && self.apq.enabled {
576                return Err(ConfigurationError::InvalidConfiguration {
577                    message: "apqs must be disabled to enable safelisting",
578                    error: "either set persisted_queries.safelist.enabled: false or apq.enabled: false in your router yaml configuration".into()
579                });
580            } else if !self.persisted_queries.safelist.enabled
581                && self.persisted_queries.safelist.require_id
582            {
583                return Err(ConfigurationError::InvalidConfiguration {
584                    message: "safelist must be enabled to require IDs",
585                    error: "either set persisted_queries.safelist.enabled: true or persisted_queries.safelist.require_id: false in your router yaml configuration".into()
586                });
587            }
588        } else {
589            // If the feature isn't enabled, sub-features shouldn't be.
590            if self.persisted_queries.safelist.enabled {
591                return Err(ConfigurationError::InvalidConfiguration {
592                    message: "persisted queries must be enabled to enable safelisting",
593                    error: "either set persisted_queries.safelist.enabled: false or persisted_queries.enabled: true in your router yaml configuration".into()
594                });
595            } else if self.persisted_queries.log_unknown {
596                return Err(ConfigurationError::InvalidConfiguration {
597                    message: "persisted queries must be enabled to enable logging unknown operations",
598                    error: "either set persisted_queries.log_unknown: false or persisted_queries.enabled: true in your router yaml configuration".into()
599                });
600            }
601        }
602
603        // response & entity caching
604        if self.apollo_plugin_enabled("response_cache")
605            && self.apollo_plugin_enabled("preview_entity_cache")
606        {
607            return Err(ConfigurationError::InvalidConfiguration {
608                message: "entity cache and response cache features are mutually exclusive",
609                error: "either set response_cache.enabled: false or preview_entity_cache.enabled: false in your router yaml configuration".into(),
610            });
611        }
612
613        Ok(self)
614    }
615}
616
617/// Parse configuration from a string in YAML syntax
618impl FromStr for Configuration {
619    type Err = ConfigurationError;
620
621    fn from_str(s: &str) -> Result<Self, Self::Err> {
622        schema::validate_yaml_configuration(s, Expansion::default()?, schema::Mode::Upgrade)?
623            .validate()
624    }
625}
626
627fn gen_schema(
628    plugins: BTreeMap<String, Schema>,
629    hidden_plugins: Option<BTreeMap<String, Schema>>,
630) -> Schema {
631    schemars::json_schema!({
632        "type": "object",
633        "properties": plugins,
634        "additionalProperties": false,
635        "patternProperties": hidden_plugins
636            .unwrap_or_default()
637            .into_iter()
638            // Wrap plugin name with regex start/end to enforce exact match
639            .map(|(k, v)| (format!("^{}$", k), v))
640            .collect::<BTreeMap<_, _>>()
641    })
642}
643
644/// Plugins provided by Apollo.
645///
646/// These plugins are processed prior to user plugins. Also, their configuration
647/// is "hoisted" to the top level of the config rather than being processed
648/// under "plugins" as for user plugins.
649#[derive(Clone, Debug, Default, Deserialize, Serialize)]
650#[serde(transparent)]
651pub(crate) struct ApolloPlugins {
652    pub(crate) plugins: Map<String, Value>,
653}
654
655impl JsonSchema for ApolloPlugins {
656    fn schema_name() -> std::borrow::Cow<'static, str> {
657        stringify!(Plugins).into()
658    }
659
660    fn json_schema(generator: &mut SchemaGenerator) -> Schema {
661        // This is a manual implementation of Plugins schema to allow plugins that have been registered at
662        // compile time to be picked up.
663
664        let (plugin_entries, hidden_plugin_entries): (Vec<_>, Vec<_>) = crate::plugin::plugins()
665            .sorted_by_key(|factory| factory.name.clone())
666            .filter(|factory| factory.name.starts_with(APOLLO_PLUGIN_PREFIX))
667            .partition_map(|factory| {
668                let key = factory.name[APOLLO_PLUGIN_PREFIX.len()..].to_string();
669                let schema = factory.create_schema(generator);
670                // Separate any plugins we're hiding
671                if factory.hidden_from_config_json_schema {
672                    Either::Right((key, schema))
673                } else {
674                    Either::Left((key, schema))
675                }
676            });
677        gen_schema(
678            plugin_entries.into_iter().collect(),
679            Some(hidden_plugin_entries.into_iter().collect()),
680        )
681    }
682}
683
684/// Plugins provided by a user.
685///
686/// These plugins are compiled into a router by and their configuration is performed
687/// under the "plugins" section.
688#[derive(Clone, Debug, Default, Deserialize, Serialize)]
689#[serde(transparent)]
690pub(crate) struct UserPlugins {
691    pub(crate) plugins: Option<Map<String, Value>>,
692}
693
694impl JsonSchema for UserPlugins {
695    fn schema_name() -> std::borrow::Cow<'static, str> {
696        stringify!(Plugins).into()
697    }
698
699    fn json_schema(generator: &mut SchemaGenerator) -> Schema {
700        // This is a manual implementation of Plugins schema to allow plugins that have been registered at
701        // compile time to be picked up.
702
703        let plugins = crate::plugin::plugins()
704            .sorted_by_key(|factory| factory.name.clone())
705            .filter(|factory| !factory.name.starts_with(APOLLO_PLUGIN_PREFIX))
706            .map(|factory| (factory.name.to_string(), factory.create_schema(generator)))
707            .collect();
708        gen_schema(plugins, None)
709    }
710}
711
712/// Configuration options pertaining to the supergraph server component.
713#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
714#[serde(deny_unknown_fields)]
715#[serde(default)]
716pub(crate) struct Supergraph {
717    /// The socket address and port to listen on
718    /// Defaults to 127.0.0.1:4000
719    pub(crate) listen: ListenAddr,
720
721    /// The timeout for shutting down connections during a router shutdown or a schema reload.
722    #[serde(deserialize_with = "humantime_serde::deserialize")]
723    #[schemars(with = "String", default = "default_connection_shutdown_timeout")]
724    pub(crate) connection_shutdown_timeout: Duration,
725
726    /// The HTTP path on which GraphQL requests will be served.
727    /// default: "/"
728    pub(crate) path: String,
729
730    /// Enable introspection
731    /// Default: false
732    pub(crate) introspection: bool,
733
734    /// Enable QP generation of fragments for subgraph requests
735    /// Default: true
736    pub(crate) generate_query_fragments: bool,
737
738    /// Set to false to disable defer support
739    pub(crate) defer_support: bool,
740
741    /// Query planning options
742    pub(crate) query_planning: QueryPlanning,
743
744    /// abort request handling when the client drops the connection.
745    /// Default: false.
746    /// When set to true, some parts of the request pipeline like telemetry will not work properly,
747    /// but request handling will stop immediately when the client connection is closed.
748    pub(crate) early_cancel: bool,
749
750    /// Enable errors generated during response reformatting and result coercion to be returned in
751    /// responses.
752    /// Default: false
753    /// All subgraph responses are checked and corrected to ensure alignment with the schema and
754    /// query. When enabled, misaligned values will generate errors which are included in errors
755    /// array in the response.
756    pub(crate) enable_result_coercion_errors: bool,
757
758    /// Log a message if the client closes the connection before the response is sent.
759    /// Default: false.
760    pub(crate) experimental_log_on_broken_pipe: bool,
761}
762
763const fn default_generate_query_fragments() -> bool {
764    true
765}
766
767fn default_defer_support() -> bool {
768    true
769}
770
771#[buildstructor::buildstructor]
772impl Supergraph {
773    #[builder]
774    pub(crate) fn new(
775        listen: Option<ListenAddr>,
776        path: Option<String>,
777        connection_shutdown_timeout: Option<Duration>,
778        introspection: Option<bool>,
779        defer_support: Option<bool>,
780        query_planning: Option<QueryPlanning>,
781        generate_query_fragments: Option<bool>,
782        early_cancel: Option<bool>,
783        experimental_log_on_broken_pipe: Option<bool>,
784        insert_result_coercion_errors: Option<bool>,
785    ) -> Self {
786        Self {
787            listen: listen.unwrap_or_else(default_graphql_listen),
788            path: path.unwrap_or_else(default_graphql_path),
789            connection_shutdown_timeout: connection_shutdown_timeout
790                .unwrap_or_else(default_connection_shutdown_timeout),
791            introspection: introspection.unwrap_or_else(default_graphql_introspection),
792            defer_support: defer_support.unwrap_or_else(default_defer_support),
793            query_planning: query_planning.unwrap_or_default(),
794            generate_query_fragments: generate_query_fragments
795                .unwrap_or_else(default_generate_query_fragments),
796            early_cancel: early_cancel.unwrap_or_default(),
797            experimental_log_on_broken_pipe: experimental_log_on_broken_pipe.unwrap_or_default(),
798            enable_result_coercion_errors: insert_result_coercion_errors.unwrap_or_default(),
799        }
800    }
801}
802
803#[cfg(test)]
804#[buildstructor::buildstructor]
805impl Supergraph {
806    #[builder]
807    pub(crate) fn fake_new(
808        listen: Option<ListenAddr>,
809        path: Option<String>,
810        connection_shutdown_timeout: Option<Duration>,
811        introspection: Option<bool>,
812        defer_support: Option<bool>,
813        query_planning: Option<QueryPlanning>,
814        generate_query_fragments: Option<bool>,
815        early_cancel: Option<bool>,
816        experimental_log_on_broken_pipe: Option<bool>,
817        insert_result_coercion_errors: Option<bool>,
818    ) -> Self {
819        Self {
820            listen: listen.unwrap_or_else(test_listen),
821            path: path.unwrap_or_else(default_graphql_path),
822            connection_shutdown_timeout: connection_shutdown_timeout
823                .unwrap_or_else(default_connection_shutdown_timeout),
824            introspection: introspection.unwrap_or_else(default_graphql_introspection),
825            defer_support: defer_support.unwrap_or_else(default_defer_support),
826            query_planning: query_planning.unwrap_or_default(),
827            generate_query_fragments: generate_query_fragments
828                .unwrap_or_else(default_generate_query_fragments),
829            early_cancel: early_cancel.unwrap_or_default(),
830            experimental_log_on_broken_pipe: experimental_log_on_broken_pipe.unwrap_or_default(),
831            enable_result_coercion_errors: insert_result_coercion_errors.unwrap_or_default(),
832        }
833    }
834}
835
836impl Default for Supergraph {
837    fn default() -> Self {
838        Self::builder().build()
839    }
840}
841
842impl Supergraph {
843    /// To sanitize the path for axum router
844    pub(crate) fn sanitized_path(&self) -> String {
845        let mut path = self.path.clone();
846        if self.path.ends_with("/*") {
847            // Needed for axum (check the axum docs for more information about wildcards https://docs.rs/axum/latest/axum/struct.Router.html#wildcards)
848            path = format!("{}router_extra_path", self.path);
849        } else if SUPERGRAPH_ENDPOINT_REGEX.is_match(&self.path) {
850            let new_path = SUPERGRAPH_ENDPOINT_REGEX
851                .replace(&self.path, "${first_path}${sub_path}{supergraph_route}");
852            path = new_path.to_string();
853        } else if TRAILING_SLASH.is_match(&self.path) {
854            let new_path = TRAILING_SLASH.replace(&self.path, "");
855            if new_path.is_empty() {
856                path = "/".to_string();
857            } else {
858                path = new_path.to_string();
859            }
860        }
861
862        path
863    }
864}
865
866/// Router level (APQ) configuration
867#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, Default)]
868#[serde(deny_unknown_fields)]
869pub(crate) struct Router {
870    #[serde(default)]
871    pub(crate) cache: Cache,
872}
873
874/// Automatic Persisted Queries (APQ) configuration
875#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
876#[serde(deny_unknown_fields, default)]
877pub(crate) struct Apq {
878    /// Activates Automatic Persisted Queries (enabled by default)
879    pub(crate) enabled: bool,
880
881    pub(crate) router: Router,
882
883    pub(crate) subgraph: SubgraphConfiguration<SubgraphApq>,
884}
885
886#[cfg(test)]
887#[buildstructor::buildstructor]
888impl Apq {
889    #[builder]
890    pub(crate) fn fake_new(enabled: Option<bool>) -> Self {
891        Self {
892            enabled: enabled.unwrap_or_else(default_apq),
893            ..Default::default()
894        }
895    }
896}
897
898/// Subgraph level Automatic Persisted Queries (APQ) configuration
899#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
900#[serde(deny_unknown_fields, default)]
901pub(crate) struct SubgraphApq {
902    /// Enable
903    pub(crate) enabled: bool,
904}
905
906fn default_apq() -> bool {
907    true
908}
909
910impl Default for Apq {
911    fn default() -> Self {
912        Self {
913            enabled: default_apq(),
914            router: Default::default(),
915            subgraph: Default::default(),
916        }
917    }
918}
919
920/// Query planning cache configuration
921#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, Default)]
922#[serde(deny_unknown_fields, default)]
923pub(crate) struct QueryPlanning {
924    /// Cache configuration
925    pub(crate) cache: QueryPlanCache,
926    /// Warms up the cache on reloads by running the query plan over
927    /// a list of the most used queries (from the in memory cache)
928    /// Configures the number of queries warmed up. Defaults to 1/3 of
929    /// the in memory cache
930    pub(crate) warmed_up_queries: Option<usize>,
931
932    /// Sets a limit to the number of generated query plans.
933    /// The planning process generates many different query plans as it
934    /// explores the graph, and the list can grow large. By using this
935    /// limit, we prevent that growth and still get a valid query plan,
936    /// but it may not be the optimal one.
937    ///
938    /// The default limit is set to 10000, but it may change in the future
939    pub(crate) experimental_plans_limit: Option<u32>,
940
941    /// Before creating query plans, for each path of fields in the query we compute all the
942    /// possible options to traverse that path via the subgraphs. Multiple options can arise because
943    /// fields in the path can be provided by multiple subgraphs, and abstract types (i.e. unions
944    /// and interfaces) returned by fields sometimes require the query planner to traverse through
945    /// each constituent object type. The number of options generated in this computation can grow
946    /// large if the schema or query are sufficiently complex, and that will increase the time spent
947    /// planning.
948    ///
949    /// This config allows specifying a per-path limit to the number of options considered. If any
950    /// path's options exceeds this limit, query planning will abort and the operation will fail.
951    ///
952    /// The default value is None, which specifies no limit.
953    pub(crate) experimental_paths_limit: Option<u32>,
954
955    /// If cache warm up is configured, this will allow the router to keep a query plan created with
956    /// the old schema, if it determines that the schema update does not affect the corresponding query
957    pub(crate) experimental_reuse_query_plans: bool,
958
959    /// Configures cooperative cancellation of query planning
960    ///
961    /// See [`CooperativeCancellation`] for more details.
962    pub(crate) experimental_cooperative_cancellation: CooperativeCancellation,
963}
964
965#[buildstructor::buildstructor]
966impl QueryPlanning {
967    #[builder]
968    #[allow(dead_code)]
969    pub(crate) fn new(
970        cache: Option<QueryPlanCache>,
971        warmed_up_queries: Option<usize>,
972        experimental_plans_limit: Option<u32>,
973        experimental_paths_limit: Option<u32>,
974        experimental_reuse_query_plans: Option<bool>,
975        experimental_cooperative_cancellation: Option<CooperativeCancellation>,
976    ) -> Self {
977        Self {
978            cache: cache.unwrap_or_default(),
979            warmed_up_queries,
980            experimental_plans_limit,
981            experimental_paths_limit,
982            experimental_reuse_query_plans: experimental_reuse_query_plans.unwrap_or_default(),
983            experimental_cooperative_cancellation: experimental_cooperative_cancellation
984                .unwrap_or_default(),
985        }
986    }
987}
988
989/// Cache configuration
990#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
991#[serde(deny_unknown_fields, default)]
992pub(crate) struct QueryPlanCache {
993    /// Configures the in memory cache (always active)
994    pub(crate) in_memory: InMemoryCache,
995    /// Configures and activates the Redis cache
996    pub(crate) redis: Option<QueryPlanRedisCache>,
997}
998
999#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
1000#[serde(deny_unknown_fields)]
1001/// Redis cache configuration
1002pub(crate) struct QueryPlanRedisCache {
1003    /// List of URLs to the Redis cluster
1004    pub(crate) urls: Vec<url::Url>,
1005
1006    /// Redis username if not provided in the URLs. This field takes precedence over the username in the URL
1007    pub(crate) username: Option<String>,
1008    /// Redis password if not provided in the URLs. This field takes precedence over the password in the URL
1009    pub(crate) password: Option<String>,
1010
1011    #[serde(
1012        deserialize_with = "humantime_serde::deserialize",
1013        default = "default_timeout"
1014    )]
1015    #[schemars(with = "Option<String>", default)]
1016    /// Redis request timeout (default: 500ms)
1017    pub(crate) timeout: Duration,
1018
1019    #[serde(
1020        deserialize_with = "humantime_serde::deserialize",
1021        default = "default_query_plan_cache_ttl"
1022    )]
1023    #[schemars(with = "Option<String>", default = "default_query_plan_cache_ttl")]
1024    /// TTL for entries
1025    pub(crate) ttl: Duration,
1026
1027    /// namespace used to prefix Redis keys
1028    pub(crate) namespace: Option<String>,
1029
1030    #[serde(default)]
1031    /// TLS client configuration
1032    pub(crate) tls: Option<TlsClient>,
1033
1034    #[serde(default = "default_required_to_start")]
1035    /// Prevents the router from starting if it cannot connect to Redis
1036    pub(crate) required_to_start: bool,
1037
1038    #[serde(default = "default_reset_ttl")]
1039    /// When a TTL is set on a key, reset it when reading the data from that key
1040    pub(crate) reset_ttl: bool,
1041
1042    #[serde(default = "default_query_planner_cache_pool_size")]
1043    /// The size of the Redis connection pool
1044    pub(crate) pool_size: u32,
1045}
1046
1047fn default_query_plan_cache_ttl() -> Duration {
1048    // Default TTL set to 30 days
1049    Duration::from_secs(86400 * 30)
1050}
1051
1052fn default_query_planner_cache_pool_size() -> u32 {
1053    1
1054}
1055
1056/// Cache configuration
1057#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
1058#[serde(deny_unknown_fields, default)]
1059pub(crate) struct Cache {
1060    /// Configures the in memory cache (always active)
1061    pub(crate) in_memory: InMemoryCache,
1062    /// Configures and activates the Redis cache
1063    pub(crate) redis: Option<RedisCache>,
1064}
1065
1066impl From<QueryPlanCache> for Cache {
1067    fn from(value: QueryPlanCache) -> Self {
1068        Cache {
1069            in_memory: value.in_memory,
1070            redis: value.redis.map(Into::into),
1071        }
1072    }
1073}
1074
1075#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
1076#[serde(deny_unknown_fields)]
1077/// In memory cache configuration
1078pub(crate) struct InMemoryCache {
1079    /// Number of entries in the Least Recently Used cache
1080    pub(crate) limit: NonZeroUsize,
1081}
1082
1083impl Default for InMemoryCache {
1084    fn default() -> Self {
1085        Self {
1086            limit: DEFAULT_CACHE_CAPACITY,
1087        }
1088    }
1089}
1090
1091#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
1092#[serde(deny_unknown_fields)]
1093/// Redis cache configuration
1094pub(crate) struct RedisCache {
1095    /// List of URLs to the Redis cluster
1096    pub(crate) urls: Vec<url::Url>,
1097
1098    /// Redis username if not provided in the URLs. This field takes precedence over the username in the URL
1099    pub(crate) username: Option<String>,
1100    /// Redis password if not provided in the URLs. This field takes precedence over the password in the URL
1101    pub(crate) password: Option<String>,
1102
1103    #[serde(
1104        deserialize_with = "humantime_serde::deserialize",
1105        default = "default_timeout"
1106    )]
1107    #[schemars(with = "Option<String>", default)]
1108    /// Redis request timeout (default: 500ms)
1109    pub(crate) timeout: Duration,
1110
1111    #[serde(deserialize_with = "humantime_serde::deserialize", default)]
1112    #[schemars(with = "Option<String>", default)]
1113    /// TTL for entries
1114    pub(crate) ttl: Option<Duration>,
1115
1116    /// namespace used to prefix Redis keys
1117    pub(crate) namespace: Option<String>,
1118
1119    #[serde(default)]
1120    /// TLS client configuration
1121    pub(crate) tls: Option<TlsClient>,
1122
1123    #[serde(default = "default_required_to_start")]
1124    /// Prevents the router from starting if it cannot connect to Redis
1125    pub(crate) required_to_start: bool,
1126
1127    #[serde(default = "default_reset_ttl")]
1128    /// When a TTL is set on a key, reset it when reading the data from that key
1129    pub(crate) reset_ttl: bool,
1130
1131    #[serde(default = "default_pool_size")]
1132    /// The size of the Redis connection pool
1133    pub(crate) pool_size: u32,
1134    #[serde(
1135        deserialize_with = "humantime_serde::deserialize",
1136        default = "default_metrics_interval"
1137    )]
1138    #[schemars(with = "Option<String>", default)]
1139    /// Interval for collecting Redis metrics (default: 1s)
1140    pub(crate) metrics_interval: Duration,
1141}
1142
1143fn default_timeout() -> Duration {
1144    Duration::from_millis(500)
1145}
1146
1147pub(crate) fn default_required_to_start() -> bool {
1148    false
1149}
1150
1151pub(crate) fn default_pool_size() -> u32 {
1152    1
1153}
1154
1155pub(crate) fn default_metrics_interval() -> Duration {
1156    Duration::from_secs(1)
1157}
1158
1159impl From<QueryPlanRedisCache> for RedisCache {
1160    fn from(value: QueryPlanRedisCache) -> Self {
1161        RedisCache {
1162            urls: value.urls,
1163            username: value.username,
1164            password: value.password,
1165            timeout: value.timeout,
1166            ttl: Some(value.ttl),
1167            namespace: value.namespace,
1168            tls: value.tls,
1169            required_to_start: value.required_to_start,
1170            reset_ttl: value.reset_ttl,
1171            pool_size: value.pool_size,
1172            metrics_interval: default_metrics_interval(),
1173        }
1174    }
1175}
1176
1177fn default_reset_ttl() -> bool {
1178    true
1179}
1180
1181/// TLS related configuration options.
1182#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
1183#[serde(deny_unknown_fields)]
1184#[serde(default)]
1185pub(crate) struct Tls {
1186    /// TLS server configuration
1187    ///
1188    /// This will affect the GraphQL endpoint and any other endpoint targeting the same listen address.
1189    pub(crate) supergraph: Option<Arc<TlsSupergraph>>,
1190    /// Outgoing TLS configuration to subgraphs.
1191    pub(crate) subgraph: SubgraphConfiguration<TlsClient>,
1192    /// Outgoing TLS configuration to Apollo Connectors.
1193    pub(crate) connector: ConnectorConfiguration<TlsClient>,
1194}
1195
1196/// Configuration options pertaining to the supergraph server component.
1197#[derive(Debug, Deserialize, Serialize, JsonSchema)]
1198#[serde(deny_unknown_fields)]
1199pub(crate) struct TlsSupergraph {
1200    /// server certificate in PEM format
1201    #[serde(deserialize_with = "deserialize_certificate", skip_serializing)]
1202    #[schemars(with = "String")]
1203    pub(crate) certificate: CertificateDer<'static>,
1204    /// server key in PEM format
1205    #[serde(deserialize_with = "deserialize_key", skip_serializing)]
1206    #[schemars(with = "String")]
1207    pub(crate) key: PrivateKeyDer<'static>,
1208    /// list of certificate authorities in PEM format
1209    #[serde(deserialize_with = "deserialize_certificate_chain", skip_serializing)]
1210    #[schemars(with = "String")]
1211    pub(crate) certificate_chain: Vec<CertificateDer<'static>>,
1212}
1213
1214impl TlsSupergraph {
1215    pub(crate) fn tls_config(&self) -> Result<Arc<rustls::ServerConfig>, ApolloRouterError> {
1216        let mut certificates = vec![self.certificate.clone()];
1217        certificates.extend(self.certificate_chain.iter().cloned());
1218
1219        let mut config = ServerConfig::builder()
1220            .with_no_client_auth()
1221            .with_single_cert(certificates, self.key.clone_key())
1222            .map_err(ApolloRouterError::Rustls)?;
1223        config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
1224
1225        Ok(Arc::new(config))
1226    }
1227}
1228
1229fn deserialize_certificate<'de, D>(deserializer: D) -> Result<CertificateDer<'static>, D::Error>
1230where
1231    D: Deserializer<'de>,
1232{
1233    let data = String::deserialize(deserializer)?;
1234
1235    load_certs(&data)
1236        .map_err(serde::de::Error::custom)
1237        .and_then(|mut certs| {
1238            if certs.len() > 1 {
1239                Err(serde::de::Error::custom("expected exactly one certificate"))
1240            } else {
1241                certs
1242                    .pop()
1243                    .ok_or(serde::de::Error::custom("expected exactly one certificate"))
1244            }
1245        })
1246}
1247
1248fn deserialize_certificate_chain<'de, D>(
1249    deserializer: D,
1250) -> Result<Vec<CertificateDer<'static>>, D::Error>
1251where
1252    D: Deserializer<'de>,
1253{
1254    let data = String::deserialize(deserializer)?;
1255
1256    load_certs(&data).map_err(serde::de::Error::custom)
1257}
1258
1259fn deserialize_key<'de, D>(deserializer: D) -> Result<PrivateKeyDer<'static>, D::Error>
1260where
1261    D: Deserializer<'de>,
1262{
1263    let data = String::deserialize(deserializer)?;
1264
1265    load_key(&data).map_err(serde::de::Error::custom)
1266}
1267
1268#[derive(thiserror::Error, Debug)]
1269#[error("could not load TLS certificate: {0}")]
1270struct LoadCertError(std::io::Error);
1271
1272pub(crate) fn load_certs(data: &str) -> io::Result<Vec<CertificateDer<'static>>> {
1273    rustls_pemfile::certs(&mut BufReader::new(data.as_bytes()))
1274        .collect::<Result<Vec<_>, _>>()
1275        .map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, LoadCertError(error)))
1276}
1277
1278pub(crate) fn load_key(data: &str) -> io::Result<PrivateKeyDer<'static>> {
1279    let mut reader = BufReader::new(data.as_bytes());
1280    let mut key_iterator = iter::from_fn(|| rustls_pemfile::read_one(&mut reader).transpose());
1281
1282    let private_key = match key_iterator.next() {
1283        Some(Ok(rustls_pemfile::Item::Pkcs1Key(key))) => PrivateKeyDer::from(key),
1284        Some(Ok(rustls_pemfile::Item::Pkcs8Key(key))) => PrivateKeyDer::from(key),
1285        Some(Ok(rustls_pemfile::Item::Sec1Key(key))) => PrivateKeyDer::from(key),
1286        Some(Err(e)) => {
1287            return Err(io::Error::new(
1288                io::ErrorKind::InvalidInput,
1289                format!("could not parse the key: {e}"),
1290            ));
1291        }
1292        Some(_) => {
1293            return Err(io::Error::new(
1294                io::ErrorKind::InvalidInput,
1295                "expected a private key",
1296            ));
1297        }
1298        None => {
1299            return Err(io::Error::new(
1300                io::ErrorKind::InvalidInput,
1301                "could not find a private key",
1302            ));
1303        }
1304    };
1305
1306    if key_iterator.next().is_some() {
1307        return Err(io::Error::new(
1308            io::ErrorKind::InvalidInput,
1309            "expected exactly one private key",
1310        ));
1311    }
1312    Ok(private_key)
1313}
1314
1315/// Configuration options pertaining to the subgraph server component.
1316#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
1317#[serde(deny_unknown_fields)]
1318#[serde(default)]
1319pub(crate) struct TlsClient {
1320    /// list of certificate authorities in PEM format
1321    pub(crate) certificate_authorities: Option<String>,
1322    /// client certificate authentication
1323    pub(crate) client_authentication: Option<Arc<TlsClientAuth>>,
1324}
1325
1326#[buildstructor::buildstructor]
1327impl TlsClient {
1328    #[builder]
1329    pub(crate) fn new(
1330        certificate_authorities: Option<String>,
1331        client_authentication: Option<Arc<TlsClientAuth>>,
1332    ) -> Self {
1333        Self {
1334            certificate_authorities,
1335            client_authentication,
1336        }
1337    }
1338}
1339
1340impl Default for TlsClient {
1341    fn default() -> Self {
1342        Self::builder().build()
1343    }
1344}
1345
1346/// TLS client authentication
1347#[derive(Debug, Deserialize, Serialize, JsonSchema)]
1348#[serde(deny_unknown_fields)]
1349pub(crate) struct TlsClientAuth {
1350    /// list of certificates in PEM format
1351    #[serde(deserialize_with = "deserialize_certificate_chain", skip_serializing)]
1352    #[schemars(with = "String")]
1353    pub(crate) certificate_chain: Vec<CertificateDer<'static>>,
1354    /// key in PEM format
1355    #[serde(deserialize_with = "deserialize_key", skip_serializing)]
1356    #[schemars(with = "String")]
1357    pub(crate) key: PrivateKeyDer<'static>,
1358}
1359
1360/// Configuration options pertaining to the sandbox page.
1361#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
1362#[serde(deny_unknown_fields)]
1363#[serde(default)]
1364pub(crate) struct Sandbox {
1365    /// Set to true to enable sandbox
1366    pub(crate) enabled: bool,
1367}
1368
1369fn default_sandbox() -> bool {
1370    false
1371}
1372
1373#[buildstructor::buildstructor]
1374impl Sandbox {
1375    #[builder]
1376    pub(crate) fn new(enabled: Option<bool>) -> Self {
1377        Self {
1378            enabled: enabled.unwrap_or_else(default_sandbox),
1379        }
1380    }
1381}
1382
1383#[cfg(test)]
1384#[buildstructor::buildstructor]
1385impl Sandbox {
1386    #[builder]
1387    pub(crate) fn fake_new(enabled: Option<bool>) -> Self {
1388        Self {
1389            enabled: enabled.unwrap_or_else(default_sandbox),
1390        }
1391    }
1392}
1393
1394impl Default for Sandbox {
1395    fn default() -> Self {
1396        Self::builder().build()
1397    }
1398}
1399
1400/// Configuration options pertaining to the home page.
1401#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
1402#[serde(deny_unknown_fields)]
1403#[serde(default)]
1404pub(crate) struct Homepage {
1405    /// Set to false to disable the homepage
1406    pub(crate) enabled: bool,
1407    /// Graph reference
1408    /// This will allow you to redirect from the Apollo Router landing page back to Apollo Studio Explorer
1409    pub(crate) graph_ref: Option<String>,
1410}
1411
1412fn default_homepage() -> bool {
1413    true
1414}
1415
1416#[buildstructor::buildstructor]
1417impl Homepage {
1418    #[builder]
1419    pub(crate) fn new(enabled: Option<bool>) -> Self {
1420        Self {
1421            enabled: enabled.unwrap_or_else(default_homepage),
1422            graph_ref: None,
1423        }
1424    }
1425}
1426
1427#[cfg(test)]
1428#[buildstructor::buildstructor]
1429impl Homepage {
1430    #[builder]
1431    pub(crate) fn fake_new(enabled: Option<bool>) -> Self {
1432        Self {
1433            enabled: enabled.unwrap_or_else(default_homepage),
1434            graph_ref: None,
1435        }
1436    }
1437}
1438
1439impl Default for Homepage {
1440    fn default() -> Self {
1441        Self::builder().enabled(default_homepage()).build()
1442    }
1443}
1444
1445/// Listening address.
1446#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema)]
1447#[serde(untagged)]
1448pub enum ListenAddr {
1449    /// Socket address.
1450    SocketAddr(SocketAddr),
1451    /// Unix socket.
1452    #[cfg(unix)]
1453    UnixSocket(std::path::PathBuf),
1454}
1455
1456impl ListenAddr {
1457    pub(crate) fn ip_and_port(&self) -> Option<(IpAddr, u16)> {
1458        #[cfg_attr(not(unix), allow(irrefutable_let_patterns))]
1459        if let Self::SocketAddr(addr) = self {
1460            Some((addr.ip(), addr.port()))
1461        } else {
1462            None
1463        }
1464    }
1465}
1466
1467impl From<SocketAddr> for ListenAddr {
1468    fn from(addr: SocketAddr) -> Self {
1469        Self::SocketAddr(addr)
1470    }
1471}
1472
1473#[allow(clippy::from_over_into)]
1474impl Into<serde_json::Value> for ListenAddr {
1475    fn into(self) -> serde_json::Value {
1476        match self {
1477            // It avoids to prefix with `http://` when serializing and relying on the Display impl.
1478            // Otherwise, it's converted to a `UnixSocket` in any case.
1479            Self::SocketAddr(addr) => serde_json::Value::String(addr.to_string()),
1480            #[cfg(unix)]
1481            Self::UnixSocket(path) => serde_json::Value::String(
1482                path.as_os_str()
1483                    .to_str()
1484                    .expect("unsupported non-UTF-8 path")
1485                    .to_string(),
1486            ),
1487        }
1488    }
1489}
1490
1491#[cfg(unix)]
1492impl From<tokio_util::either::Either<std::net::SocketAddr, tokio::net::unix::SocketAddr>>
1493    for ListenAddr
1494{
1495    fn from(
1496        addr: tokio_util::either::Either<std::net::SocketAddr, tokio::net::unix::SocketAddr>,
1497    ) -> Self {
1498        match addr {
1499            tokio_util::either::Either::Left(addr) => Self::SocketAddr(addr),
1500            tokio_util::either::Either::Right(addr) => Self::UnixSocket(
1501                addr.as_pathname()
1502                    .map(ToOwned::to_owned)
1503                    .unwrap_or_default(),
1504            ),
1505        }
1506    }
1507}
1508
1509impl fmt::Display for ListenAddr {
1510    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1511        match self {
1512            Self::SocketAddr(addr) => write!(f, "http://{addr}"),
1513            #[cfg(unix)]
1514            Self::UnixSocket(path) => write!(f, "{}", path.display()),
1515        }
1516    }
1517}
1518
1519fn default_graphql_path() -> String {
1520    String::from("/")
1521}
1522
1523fn default_graphql_introspection() -> bool {
1524    false
1525}
1526
1527fn default_connection_shutdown_timeout() -> Duration {
1528    Duration::from_secs(60)
1529}
1530
1531#[derive(Clone, Debug, Default, Error, Display, Serialize, Deserialize, JsonSchema)]
1532#[serde(deny_unknown_fields, rename_all = "snake_case")]
1533pub(crate) enum BatchingMode {
1534    /// batch_http_link
1535    #[default]
1536    BatchHttpLink,
1537}
1538
1539/// Configuration for Batching
1540#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
1541#[serde(deny_unknown_fields)]
1542pub(crate) struct Batching {
1543    /// Activates Batching (disabled by default)
1544    #[serde(default)]
1545    pub(crate) enabled: bool,
1546
1547    /// Batching mode
1548    pub(crate) mode: BatchingMode,
1549
1550    /// Subgraph options for batching
1551    pub(crate) subgraph: Option<SubgraphConfiguration<CommonBatchingConfig>>,
1552
1553    /// Maximum size for a batch
1554    #[serde(default)]
1555    pub(crate) maximum_size: Option<usize>,
1556}
1557
1558/// Common options for configuring subgraph batching
1559#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
1560pub(crate) struct CommonBatchingConfig {
1561    /// Whether this batching config should be enabled
1562    pub(crate) enabled: bool,
1563}
1564
1565impl Batching {
1566    // Check if we should enable batching for a particular subgraph (service_name)
1567    pub(crate) fn batch_include(&self, service_name: &str) -> bool {
1568        match &self.subgraph {
1569            Some(subgraph_batching_config) => {
1570                // Override by checking if all is enabled
1571                if subgraph_batching_config.all.enabled {
1572                    // If it is, require:
1573                    // - no subgraph entry OR
1574                    // - an enabled subgraph entry
1575                    subgraph_batching_config
1576                        .subgraphs
1577                        .get(service_name)
1578                        .is_none_or(|x| x.enabled)
1579                } else {
1580                    // If it isn't, require:
1581                    // - an enabled subgraph entry
1582                    subgraph_batching_config
1583                        .subgraphs
1584                        .get(service_name)
1585                        .is_some_and(|x| x.enabled)
1586                }
1587            }
1588            None => false,
1589        }
1590    }
1591
1592    pub(crate) fn exceeds_batch_size<T>(&self, batch: &[T]) -> bool {
1593        match self.maximum_size {
1594            Some(maximum_size) => batch.len() > maximum_size,
1595            None => false,
1596        }
1597    }
1598}
1599
1600/// Per-subgraph configuration for hoisting orphan errors.
1601///
1602/// "Orphan errors" are errors from entity fetches that lack a valid `_entities` path.
1603/// When hoisting is enabled, these errors are assigned to the nearest non-array
1604/// ancestor in the response path, preventing them from being duplicated across
1605/// every element in an array.
1606#[derive(Debug, Clone, Default, Deserialize, Serialize, JsonSchema)]
1607#[serde(deny_unknown_fields)]
1608pub(crate) struct HoistOrphanErrors {
1609    /// Enable hoisting of orphan errors for this subgraph.
1610    #[serde(default)]
1611    pub(crate) enabled: bool,
1612}