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