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