Skip to main content

apollo_router/configuration/
mod.rs

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