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