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