Skip to main content

apollo_router/
executable.rs

1//! Main entry point for CLI command to start server.
2
3use std::fmt::Debug;
4use std::net::SocketAddr;
5#[cfg(unix)]
6use std::os::unix::fs::MetadataExt;
7use std::path::PathBuf;
8use std::sync::atomic::AtomicBool;
9use std::sync::atomic::Ordering;
10use std::time::Duration;
11
12use anyhow::Result;
13use anyhow::anyhow;
14use clap::ArgAction;
15use clap::Args;
16use clap::Parser;
17use clap::Subcommand;
18use clap::builder::FalseyValueParser;
19use parking_lot::Mutex;
20use regex::Captures;
21use regex::Regex;
22use url::ParseError;
23use url::Url;
24
25use crate::LicenseSource;
26use crate::configuration::Discussed;
27use crate::configuration::expansion::Expansion;
28use crate::configuration::generate_config_schema;
29use crate::configuration::generate_upgrade;
30use crate::configuration::schema::Mode;
31use crate::configuration::validate_yaml_configuration;
32use crate::metrics::meter_provider_internal;
33use crate::plugin::plugins;
34use crate::plugins::telemetry::reload::otel::init_telemetry;
35use crate::registry::OciConfig;
36use crate::registry::should_use_ssl;
37use crate::registry::validate_oci_reference;
38use crate::router::ConfigurationSource;
39use crate::router::RouterHttpServer;
40use crate::router::SchemaSource;
41use crate::router::ShutdownSource;
42use crate::uplink::Endpoints;
43use crate::uplink::UplinkConfig;
44
45pub(crate) static APOLLO_ROUTER_DEV_MODE: AtomicBool = AtomicBool::new(false);
46pub(crate) static APOLLO_ROUTER_SUPERGRAPH_PATH_IS_SET: AtomicBool = AtomicBool::new(false);
47pub(crate) static APOLLO_ROUTER_SUPERGRAPH_URLS_IS_SET: AtomicBool = AtomicBool::new(false);
48pub(crate) static APOLLO_ROUTER_LICENCE_IS_SET: AtomicBool = AtomicBool::new(false);
49pub(crate) static APOLLO_ROUTER_LICENCE_PATH_IS_SET: AtomicBool = AtomicBool::new(false);
50pub(crate) static APOLLO_TELEMETRY_DISABLED: AtomicBool = AtomicBool::new(false);
51pub(crate) static APOLLO_ROUTER_LISTEN_ADDRESS: Mutex<Option<SocketAddr>> = Mutex::new(None);
52pub(crate) static APOLLO_ROUTER_GRAPH_ARTIFACT_REFERENCE: Mutex<Option<String>> = Mutex::new(None);
53pub(crate) static APOLLO_ROUTER_HOT_RELOAD_CLI: AtomicBool = AtomicBool::new(false);
54
55const INITIAL_UPLINK_POLL_INTERVAL: Duration = Duration::from_secs(10);
56const INITIAL_OCI_POLL_INTERVAL: Duration = Duration::from_secs(30);
57
58const FORBIDDEN_OTEL_VARS: [&str; 3] = [
59    "OTEL_EXPORTER_OTLP_ENDPOINT",
60    "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
61    "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT",
62];
63
64/// Subcommands
65#[derive(Subcommand, Debug)]
66enum Commands {
67    /// Configuration subcommands.
68    Config(ConfigSubcommandArgs),
69}
70
71#[derive(Args, Debug)]
72struct ConfigSubcommandArgs {
73    /// Subcommands
74    #[clap(subcommand)]
75    command: ConfigSubcommand,
76}
77
78#[derive(Subcommand, Debug)]
79enum ConfigSubcommand {
80    /// Print the json configuration schema.
81    Schema,
82
83    /// Print upgraded configuration.
84    Upgrade {
85        /// The location of the config to upgrade.
86        #[clap(value_parser, env = "APOLLO_ROUTER_CONFIG_PATH")]
87        config_path: PathBuf,
88
89        /// Print a diff.
90        #[clap(action = ArgAction::SetTrue, long)]
91        diff: bool,
92    },
93    /// Validate existing Router configuration file
94    Validate {
95        /// The location of the config to validate.
96        #[clap(value_parser, env = "APOLLO_ROUTER_CONFIG_PATH")]
97        config_path: PathBuf,
98    },
99    /// List all the available experimental configurations with related GitHub discussion
100    Experimental,
101    /// List all the available preview configurations with related GitHub discussion
102    Preview,
103}
104
105/// Options for the router
106#[derive(Parser, Debug)]
107#[clap(name = "router", about = "Apollo federation router")]
108#[command(disable_version_flag(true))]
109pub struct Opt {
110    /// Log level (off|error|warn|info|debug|trace).
111    #[clap(
112        long = "log",
113        default_value = "info",
114        alias = "log-level",
115        value_parser = add_log_filter,
116        env = "APOLLO_ROUTER_LOG"
117    )]
118    // FIXME: when upgrading to router 2.0 we should put this value in an Option
119    log_level: String,
120
121    /// Reload locally provided configuration and supergraph files automatically.  This only affects watching of local files and does not affect supergraphs and configuration provided by GraphOS through Uplink, which is always reloaded immediately.
122    #[clap(
123        alias = "hr",
124        long = "hot-reload",
125        env = "APOLLO_ROUTER_HOT_RELOAD",
126        action(ArgAction::SetTrue)
127    )]
128    hot_reload: bool,
129
130    /// Configuration location relative to the project directory.
131    #[clap(
132        short,
133        long = "config",
134        value_parser,
135        env = "APOLLO_ROUTER_CONFIG_PATH"
136    )]
137    config_path: Option<PathBuf>,
138
139    /// Enable development mode.
140    #[clap(env = "APOLLO_ROUTER_DEV", long = "dev", action(ArgAction::SetTrue))]
141    dev: bool,
142
143    /// Schema location relative to the project directory.
144    #[clap(
145        short,
146        long = "supergraph",
147        value_parser,
148        env = "APOLLO_ROUTER_SUPERGRAPH_PATH"
149    )]
150    supergraph_path: Option<PathBuf>,
151
152    /// Locations (comma separated) to fetch the supergraph from. These will be queried in order.
153    #[clap(env = "APOLLO_ROUTER_SUPERGRAPH_URLS", value_delimiter = ',')]
154    supergraph_urls: Option<Vec<Url>>,
155
156    /// Subcommands
157    #[clap(subcommand)]
158    command: Option<Commands>,
159
160    /// Your Apollo key.
161    #[clap(skip = std::env::var("APOLLO_KEY").ok())]
162    apollo_key: Option<String>,
163
164    /// Key file location relative to the current directory.
165    #[cfg(unix)]
166    #[clap(long = "apollo-key-path", env = "APOLLO_KEY_PATH")]
167    apollo_key_path: Option<PathBuf>,
168
169    /// Your Apollo graph reference.
170    #[clap(skip = std::env::var("APOLLO_GRAPH_REF").ok())]
171    apollo_graph_ref: Option<String>,
172
173    /// Your Apollo Router license.
174    #[clap(skip = std::env::var("APOLLO_ROUTER_LICENSE").ok())]
175    apollo_router_license: Option<String>,
176
177    /// License location relative to the current directory.
178    #[clap(long = "license", env = "APOLLO_ROUTER_LICENSE_PATH")]
179    apollo_router_license_path: Option<PathBuf>,
180
181    /// The endpoints (comma separated) polled to fetch the latest supergraph schema.
182    #[clap(long, env, action = ArgAction::Append)]
183    // Should be a Vec<Url> when https://github.com/clap-rs/clap/discussions/3796 is solved
184    apollo_uplink_endpoints: Option<String>,
185
186    /// An OCI reference to a graph artifact that contains the supergraph schema for the router to run.
187    #[clap(long, env = "APOLLO_GRAPH_ARTIFACT_REFERENCE", action = ArgAction::Append)]
188    graph_artifact_reference: Option<String>,
189
190    /// Disable sending anonymous usage information to Apollo.
191    #[clap(long, env = "APOLLO_TELEMETRY_DISABLED", value_parser = FalseyValueParser::new())]
192    anonymous_telemetry_disabled: bool,
193
194    /// The timeout for an http call to Apollo uplink. Defaults to 30s.
195    #[clap(long, default_value = "30s", value_parser = humantime::parse_duration, env)]
196    apollo_uplink_timeout: Duration,
197
198    /// The listen address for the router. Overrides `supergraph.listen` in router.yaml.
199    #[clap(long = "listen", env = "APOLLO_ROUTER_LISTEN_ADDRESS")]
200    listen_address: Option<SocketAddr>,
201
202    /// Display version and exit.
203    #[clap(action = ArgAction::SetTrue, long, short = 'V')]
204    pub(crate) version: bool,
205}
206
207// Add a filter to global log level settings so that the level only applies to the router.
208//
209// If you want to set a complex logging filter which isn't modified in this way, use RUST_LOG.
210fn add_log_filter(raw: &str) -> Result<String, String> {
211    match std::env::var("RUST_LOG") {
212        Ok(filter) => Ok(filter),
213        Err(_e) => {
214            // Directives are case-insensitive. Convert to lowercase before processing.
215            let lowered = raw.to_lowercase();
216            // Find "global" directives and limit them to apollo_router
217            let rgx =
218                Regex::new(r"(^|,)(off|error|warn|info|debug|trace)").expect("regex must be valid");
219            let res = rgx.replace_all(&lowered, |caps: &Captures| {
220                // The default level is info, then other ones can override the default one
221                // If the pattern matches, we must have caps 1 and 2
222                format!("{}apollo_router={}", &caps[1], &caps[2])
223            });
224            Ok(format!("info,{res}"))
225        }
226    }
227}
228
229impl Opt {
230    pub(crate) fn uplink_config(&self) -> Result<UplinkConfig, anyhow::Error> {
231        Ok(UplinkConfig {
232            apollo_key: self
233                .apollo_key
234                .clone()
235                .ok_or(Self::err_require_opt("APOLLO_KEY"))?,
236            apollo_graph_ref: self
237                .apollo_graph_ref
238                .clone()
239                .ok_or(Self::err_require_opt("APOLLO_GRAPH_REF"))?,
240            endpoints: self
241                .apollo_uplink_endpoints
242                .as_ref()
243                .map(|endpoints| Self::parse_endpoints(endpoints))
244                .transpose()?,
245            poll_interval: INITIAL_UPLINK_POLL_INTERVAL,
246            timeout: self.apollo_uplink_timeout,
247        })
248    }
249
250    pub(crate) fn oci_config(&self) -> Result<OciConfig, anyhow::Error> {
251        let graph_artifact_reference = self
252            .graph_artifact_reference
253            .clone()
254            .ok_or(Self::err_require_opt("APOLLO_GRAPH_ARTIFACT_REFERENCE"))?;
255        let (validated_reference, _) = validate_oci_reference(&graph_artifact_reference)?;
256
257        // Allow test-only override of poll interval via TEST_APOLLO_OCI_POLL_INTERVAL environment variable
258        let poll_interval = std::env::var("TEST_APOLLO_OCI_POLL_INTERVAL")
259            .ok()
260            .and_then(|s| {
261                s.parse::<u64>()
262                    .ok()
263                    .filter(|&val| (1..=60).contains(&val))
264                    .map(Duration::from_secs)
265            })
266            .unwrap_or(INITIAL_OCI_POLL_INTERVAL);
267
268        let use_ssl = should_use_ssl(&validated_reference);
269
270        Ok(OciConfig {
271            apollo_key: self
272                .apollo_key
273                .clone()
274                .ok_or(Self::err_require_opt("APOLLO_KEY"))?,
275            reference: validated_reference,
276            hot_reload: self.hot_reload,
277            poll_interval,
278            use_ssl,
279        })
280    }
281
282    fn parse_endpoints(endpoints: &str) -> std::result::Result<Endpoints, anyhow::Error> {
283        Ok(Endpoints::fallback(
284            endpoints
285                .split(',')
286                .map(|endpoint| Url::parse(endpoint.trim()))
287                .collect::<Result<Vec<Url>, ParseError>>()
288                .map_err(|err| anyhow!("invalid Apollo Uplink endpoint, {}", err))?,
289        ))
290    }
291
292    fn err_require_opt(env_var: &str) -> anyhow::Error {
293        anyhow!("Use of Apollo Graph OS requires setting the {env_var} environment variable")
294    }
295
296    fn prohibit_env_vars(env_vars: &[&'static str]) -> Result<(), anyhow::Error> {
297        reject_environment_variables(&env_variables_set(env_vars))
298    }
299}
300
301/// This is the main router entrypoint.
302///
303/// Starts a Tokio runtime and runs a Router in it based on command-line options.
304/// Returns on fatal error or after graceful shutdown has completed.
305///
306/// Refer to the examples if you would like to see how to run your own router with plugins.
307pub fn main() -> Result<()> {
308    #[cfg(feature = "dhat-heap")]
309    crate::allocator::create_heap_profiler();
310
311    #[cfg(feature = "dhat-ad-hoc")]
312    crate::allocator::create_ad_hoc_profiler();
313
314    let mut builder = tokio::runtime::Builder::new_multi_thread();
315    builder.enable_all();
316
317    // This environment variable is intentionally undocumented.
318    // See also APOLLO_ROUTER_COMPUTE_THREADS in apollo-router/src/compute_job.rs
319    if let Some(nb) = std::env::var("APOLLO_ROUTER_IO_THREADS")
320        .ok()
321        .and_then(|value| value.parse::<usize>().ok())
322    {
323        builder.worker_threads(nb);
324    }
325
326    let runtime = builder.build()?;
327    runtime.block_on(Executable::builder().start())
328}
329
330/// Entry point into creating a router executable with more customization than [`main`].
331#[non_exhaustive]
332pub struct Executable {}
333
334#[buildstructor::buildstructor]
335impl Executable {
336    /// Returns a builder that can parse command-line options and run a Router
337    /// in an existing Tokio runtime.
338    ///
339    /// Builder methods:
340    ///
341    /// * `.config(impl Into<`[`ConfigurationSource`]`>)`
342    ///   Optional.
343    ///   Specifies where to find the Router configuration.
344    ///   The default is the file specified by the `--config` or `-c` CLI option.
345    ///
346    /// * `.schema(impl Into<`[`SchemaSource`]`>)`
347    ///   Optional.
348    ///   Specifies when to find the supergraph schema.
349    ///   The default is the file specified by the `--supergraph` or `-s` CLI option.
350    ///
351    /// * `.shutdown(impl Into<`[`ShutdownSource`]`>)`
352    ///   Optional.
353    ///   Specifies when the Router should shut down gracefully.
354    ///   The default is on CTRL+C (`SIGINT`).
355    ///
356    /// * `.start()`
357    ///   Returns a future that resolves to [`anyhow::Result`]`<()>`
358    ///   on fatal error or after graceful shutdown has completed.
359    ///   Must be called (and the future awaited) in the context of an existing Tokio runtime.
360    ///
361    /// ```no_run
362    /// use apollo_router::{Executable, ShutdownSource};
363    /// # #[tokio::main]
364    /// # async fn main() -> anyhow::Result<()> {
365    /// # use futures::StreamExt;
366    /// # let schemas = futures::stream::empty().boxed();
367    /// # let configs = futures::stream::empty().boxed();
368    /// use apollo_router::{ConfigurationSource, SchemaSource};
369    /// Executable::builder()
370    ///   .shutdown(ShutdownSource::None)
371    ///   .schema(SchemaSource::Stream(schemas))
372    ///   .config(ConfigurationSource::Stream(configs))
373    ///   .start()
374    ///   .await
375    /// # }
376    /// ```
377    #[builder(entry = "builder", exit = "start", visibility = "pub")]
378    async fn start(
379        shutdown: Option<ShutdownSource>,
380        schema: Option<SchemaSource>,
381        license: Option<LicenseSource>,
382        config: Option<ConfigurationSource>,
383        cli_args: Option<Opt>,
384    ) -> Result<()> {
385        let opt = cli_args.unwrap_or_else(Opt::parse);
386
387        if opt.version {
388            println!("{}", std::env!("CARGO_PKG_VERSION"));
389            return Ok(());
390        }
391
392        *crate::services::APOLLO_KEY.lock() = opt.apollo_key.clone();
393        *crate::services::APOLLO_GRAPH_REF.lock() = opt.apollo_graph_ref.clone();
394        *APOLLO_ROUTER_LISTEN_ADDRESS.lock() = opt.listen_address;
395        *APOLLO_ROUTER_GRAPH_ARTIFACT_REFERENCE.lock() = opt.graph_artifact_reference.clone();
396        // Only set hot_reload if explicitly true
397        if opt.hot_reload {
398            APOLLO_ROUTER_HOT_RELOAD_CLI.store(true, Ordering::Relaxed);
399        }
400        APOLLO_ROUTER_DEV_MODE.store(opt.dev, Ordering::Relaxed);
401        APOLLO_ROUTER_SUPERGRAPH_PATH_IS_SET
402            .store(opt.supergraph_path.is_some(), Ordering::Relaxed);
403        APOLLO_ROUTER_SUPERGRAPH_URLS_IS_SET
404            .store(opt.supergraph_urls.is_some(), Ordering::Relaxed);
405        APOLLO_ROUTER_LICENCE_IS_SET.store(opt.apollo_router_license.is_some(), Ordering::Relaxed);
406        APOLLO_ROUTER_LICENCE_PATH_IS_SET
407            .store(opt.apollo_router_license_path.is_some(), Ordering::Relaxed);
408        APOLLO_TELEMETRY_DISABLED.store(opt.anonymous_telemetry_disabled, Ordering::Relaxed);
409
410        let apollo_telemetry_initialized = if graph_os() {
411            init_telemetry(&opt.log_level)?;
412            true
413        } else {
414            // Best effort init telemetry
415            init_telemetry(&opt.log_level).is_ok()
416        };
417
418        setup_panic_handler();
419
420        let result = match opt.command.as_ref() {
421            Some(Commands::Config(ConfigSubcommandArgs {
422                command: ConfigSubcommand::Schema,
423            })) => {
424                let schema = generate_config_schema();
425                println!("{}", serde_json::to_string_pretty(&schema)?);
426                Ok(())
427            }
428            Some(Commands::Config(ConfigSubcommandArgs {
429                command: ConfigSubcommand::Validate { config_path },
430            })) => {
431                let config_string = std::fs::read_to_string(config_path)?;
432                validate_yaml_configuration(
433                    &config_string,
434                    Expansion::default()?,
435                    Mode::NoUpgrade,
436                )?
437                .validate()?;
438
439                println!("Configuration at path {config_path:?} is valid!");
440
441                Ok(())
442            }
443            Some(Commands::Config(ConfigSubcommandArgs {
444                command: ConfigSubcommand::Upgrade { config_path, diff },
445            })) => {
446                let config_string = std::fs::read_to_string(config_path)?;
447                let output = generate_upgrade(&config_string, *diff)?;
448                println!("{output}");
449                Ok(())
450            }
451            Some(Commands::Config(ConfigSubcommandArgs {
452                command: ConfigSubcommand::Experimental,
453            })) => {
454                Discussed::new().print_experimental();
455                Ok(())
456            }
457            Some(Commands::Config(ConfigSubcommandArgs {
458                command: ConfigSubcommand::Preview,
459            })) => {
460                Discussed::new().print_preview();
461                Ok(())
462            }
463            None => Self::inner_start(shutdown, schema, config, license, opt).await,
464        };
465
466        if apollo_telemetry_initialized {
467            // We should be good to shutdown OpenTelemetry now as the router should have finished everything.
468            tokio::task::spawn_blocking(move || {
469                // Setting a new default provider causes the old one to be dropped and shut down
470                opentelemetry::global::set_tracer_provider(
471                    opentelemetry_sdk::trace::SdkTracerProvider::default(),
472                );
473                if let Err(error) = meter_provider_internal().shutdown() {
474                    tracing::error!(%error, "Failed to shut down OTel meter provider cleanly");
475                }
476            })
477            .await?;
478        }
479        result
480    }
481
482    async fn inner_start(
483        shutdown: Option<ShutdownSource>,
484        schema: Option<SchemaSource>,
485        config: Option<ConfigurationSource>,
486        license: Option<LicenseSource>,
487        mut opt: Opt,
488    ) -> Result<()> {
489        let current_directory = std::env::current_dir()?;
490        // Enable hot reload when dev mode is enabled
491        opt.hot_reload = opt.hot_reload || opt.dev;
492
493        // ROUTER-1609: prevent router from starting if OTEL environment variables are set.
494        Opt::prohibit_env_vars(&FORBIDDEN_OTEL_VARS)?;
495
496        let configuration = match (config, opt.config_path.as_ref()) {
497            (Some(_), Some(_)) => {
498                return Err(anyhow!(
499                    "--config and APOLLO_ROUTER_CONFIG_PATH cannot be used when a custom configuration source is in use"
500                ));
501            }
502            (Some(config), None) => config,
503            #[allow(clippy::blocks_in_conditions)]
504            _ => opt
505                .config_path
506                .as_ref()
507                .map(|path| {
508                    let path = if path.is_relative() {
509                        current_directory.join(path)
510                    } else {
511                        path.to_path_buf()
512                    };
513
514                    ConfigurationSource::File {
515                        path,
516                        watch: opt.hot_reload,
517                    }
518                })
519                .unwrap_or_default(),
520        };
521
522        let apollo_telemetry_msg = if opt.anonymous_telemetry_disabled {
523            "Anonymous usage data collection is disabled.".to_string()
524        } else {
525            "Anonymous usage data is gathered to inform Apollo product development.  See https://go.apollo.dev/o/privacy for details.".to_string()
526        };
527
528        let apollo_router_msg = format!(
529            "Apollo Router v{} // (c) Apollo Graph, Inc. // Licensed as ELv2 (https://go.apollo.dev/elv2)",
530            std::env!("CARGO_PKG_VERSION")
531        );
532
533        // Schema source will be in order of precedence:
534        // 1. CLI --supergraph
535        // 2. Env APOLLO_ROUTER_SUPERGRAPH_PATH
536        // 3. Env APOLLO_ROUTER_SUPERGRAPH_URLS
537        // 4. Env APOLLO_KEY and APOLLO_GRAPH_ARTIFACT_REFERENCE (CLI/env only)
538        // 5. Env APOLLO_KEY and APOLLO_GRAPH_REF (CLI/env only)
539        #[cfg(unix)]
540        let akp = &opt.apollo_key_path;
541        #[cfg(not(unix))]
542        let akp: &Option<PathBuf> = &None;
543
544        // Validate that schema sources are not conflicting
545        // Check both builder API schema and CLI supergraph_path (both map to -s/--supergraph)
546        let has_supergraph_file = schema.is_some() || opt.supergraph_path.is_some();
547        if has_supergraph_file && opt.graph_artifact_reference.is_some() {
548            return Err(anyhow!(
549                "--supergraph (-s) and --graph-artifact-reference cannot be used together. Please specify only one schema source."
550            ));
551        }
552        if opt.supergraph_urls.is_some() && opt.graph_artifact_reference.is_some() {
553            return Err(anyhow!(
554                "APOLLO_ROUTER_SUPERGRAPH_URLS and --graph-artifact-reference cannot be used together. Please specify only one schema source."
555            ));
556        }
557
558        let schema_source = match (
559            schema,
560            &opt.supergraph_path,
561            &opt.supergraph_urls,
562            &opt.apollo_key,
563            akp,
564        ) {
565            (Some(_), Some(_), _, _, _) | (Some(_), _, Some(_), _, _) => {
566                return Err(anyhow!(
567                    "--supergraph and APOLLO_ROUTER_SUPERGRAPH_PATH cannot be used when a custom schema source is in use"
568                ));
569            }
570            (Some(source), None, None, _, _) => source,
571            (_, Some(supergraph_path), _, _, _) => {
572                tracing::info!("{apollo_router_msg}");
573                tracing::info!("{apollo_telemetry_msg}");
574
575                let supergraph_path = if supergraph_path.is_relative() {
576                    current_directory.join(supergraph_path)
577                } else {
578                    supergraph_path.clone()
579                };
580                SchemaSource::File {
581                    path: supergraph_path,
582                    watch: opt.hot_reload,
583                }
584            }
585            (_, _, Some(supergraph_urls), _, _) => {
586                tracing::info!("{apollo_router_msg}");
587                tracing::info!("{apollo_telemetry_msg}");
588
589                if opt.hot_reload {
590                    tracing::warn!(
591                        "Schema hot reloading is disabled for --supergraph-urls / APOLLO_ROUTER_SUPERGRAPH_URLS."
592                    );
593                }
594
595                SchemaSource::URLs {
596                    urls: supergraph_urls.clone(),
597                }
598            }
599            (_, None, None, _, Some(apollo_key_path)) => {
600                let apollo_key_path = if apollo_key_path.is_relative() {
601                    current_directory.join(apollo_key_path)
602                } else {
603                    apollo_key_path.clone()
604                };
605
606                if !apollo_key_path.exists() {
607                    tracing::error!(
608                        "Apollo key at path '{}' does not exist.",
609                        apollo_key_path.to_string_lossy()
610                    );
611                    return Err(anyhow!(
612                        "Apollo key at path '{}' does not exist.",
613                        apollo_key_path.to_string_lossy()
614                    ));
615                } else {
616                    // On unix systems, Check that the executing user is the only user who may
617                    // read the key file.
618                    // Note: We could, in future, add support for Windows.
619                    #[cfg(unix)]
620                    {
621                        let meta = std::fs::metadata(apollo_key_path.clone())
622                            .map_err(|err| anyhow!("Failed to read Apollo key file: {}", err))?;
623                        let mode = meta.mode();
624                        // If our mode isn't "safe", fail...
625                        // safe == none of the "group" or "other" bits set.
626                        if mode & 0o077 != 0 {
627                            return Err(anyhow!(
628                                "Apollo key file permissions ({:#o}) are too permissive",
629                                mode & 0o000777
630                            ));
631                        }
632                        let euid = unsafe { libc::geteuid() };
633                        let owner = meta.uid();
634                        if euid != owner {
635                            return Err(anyhow!(
636                                "Apollo key file owner id ({owner}) does not match effective user id ({euid})"
637                            ));
638                        }
639                    }
640                    //The key file exists try and load it
641                    match std::fs::read_to_string(&apollo_key_path) {
642                        Ok(apollo_key) => {
643                            opt.apollo_key = Some(apollo_key.trim().to_string());
644                        }
645                        Err(err) => {
646                            return Err(anyhow!("Failed to read Apollo key file: {}", err));
647                        }
648                    };
649                    match opt.graph_artifact_reference {
650                        None => SchemaSource::Registry(opt.uplink_config()?),
651                        Some(_) => SchemaSource::OCI(opt.oci_config()?),
652                    }
653                }
654            }
655            (_, None, None, Some(_apollo_key), None) => {
656                tracing::info!("{apollo_router_msg}");
657                tracing::info!("{apollo_telemetry_msg}");
658                match opt.graph_artifact_reference {
659                    None => SchemaSource::Registry(opt.uplink_config()?),
660                    Some(_) => SchemaSource::OCI(opt.oci_config()?),
661                }
662            }
663            _ => {
664                return Err(anyhow!(
665                    r#"{apollo_router_msg}
666
667⚠️  The Apollo Router requires a composed supergraph schema at startup. ⚠️
668
669👉 DO ONE:
670
671  * Pass a local schema file with the '--supergraph' option:
672
673      $ ./router --supergraph <file_path>
674
675  * Fetch a registered schema from GraphOS by setting
676    these environment variables:
677
678      $ APOLLO_KEY="..." APOLLO_GRAPH_REF="..." ./router
679
680      For details, see the Apollo docs:
681      https://www.apollographql.com/docs/federation/managed-federation/setup
682
683🔬 TESTING THINGS OUT?
684
685  1. Download an example supergraph schema with Apollo-hosted subgraphs:
686
687    $ curl -L https://supergraph.demo.starstuff.dev/ > starstuff.graphql
688
689  2. Run the Apollo Router in development mode with the supergraph schema:
690
691    $ ./router --dev --supergraph starstuff.graphql
692
693    "#
694                ));
695            }
696        };
697
698        // Order of precedence for licenses:
699        // 1. explicit path from cli
700        // 2. env APOLLO_ROUTER_LICENSE
701        // 3. uplink
702
703        let license = if let Some(license) = license {
704            license
705        } else {
706            match (
707                &opt.apollo_router_license,
708                &opt.apollo_router_license_path,
709                &opt.apollo_key,
710                &opt.apollo_graph_ref,
711            ) {
712                (_, Some(license_path), _, _) => {
713                    let license_path = if license_path.is_relative() {
714                        current_directory.join(license_path)
715                    } else {
716                        license_path.clone()
717                    };
718                    LicenseSource::File {
719                        path: license_path,
720                        watch: opt.hot_reload,
721                    }
722                }
723                (Some(_license), _, _, _) => LicenseSource::Env,
724                (_, _, Some(_apollo_key), Some(_apollo_graph_ref)) => {
725                    LicenseSource::Registry(opt.uplink_config()?)
726                }
727
728                _ => LicenseSource::default(),
729            }
730        };
731
732        // If there are custom plugins then if RUST_LOG hasn't been set and APOLLO_ROUTER_LOG contains one of the defaults.
733        let user_plugins_present = plugins().filter(|p| !p.is_apollo()).count() > 0;
734        let rust_log_set = std::env::var("RUST_LOG").is_ok();
735        let apollo_router_log = std::env::var("APOLLO_ROUTER_LOG").unwrap_or_default();
736        if user_plugins_present
737            && !rust_log_set
738            && ["trace", "debug", "warn", "error", "info"].contains(&apollo_router_log.as_str())
739        {
740            tracing::info!(
741                "Custom plugins are present. To see log messages from your plugins you must configure `RUST_LOG` or `APOLLO_ROUTER_LOG` environment variables. See the Router logging documentation for more details"
742            );
743        }
744
745        let uplink_config = opt.uplink_config().ok();
746        if uplink_config
747            .clone()
748            .unwrap_or_default()
749            .endpoints
750            .unwrap_or_default()
751            .url_count()
752            == 1
753        {
754            tracing::warn!(
755                "Only a single uplink endpoint is configured. We recommend specifying at least two endpoints so that a fallback exists."
756            );
757        }
758
759        let router = RouterHttpServer::builder()
760            .is_telemetry_disabled(opt.anonymous_telemetry_disabled)
761            .configuration(configuration)
762            .and_uplink(uplink_config)
763            .schema(schema_source)
764            .license(license)
765            .shutdown(shutdown.unwrap_or(ShutdownSource::CtrlC))
766            .start();
767
768        if let Err(err) = router.await {
769            tracing::error!("{}", err);
770            return Err(err.into());
771        }
772        Ok(())
773    }
774}
775
776fn graph_os() -> bool {
777    crate::services::APOLLO_KEY.lock().is_some()
778        && crate::services::APOLLO_GRAPH_REF.lock().is_some()
779}
780
781/// Of the environment variable names provided, return a list of those which are set in the environment.
782fn env_variables_set(variables: &[&'static str]) -> Vec<&'static str> {
783    variables
784        .iter()
785        .filter(|v| !matches!(std::env::var(v), Err(std::env::VarError::NotPresent)))
786        .cloned()
787        .collect()
788}
789
790/// Return an error if the list of environment variables provided is not empty
791fn reject_environment_variables(variables: &[&str]) -> Result<(), anyhow::Error> {
792    if variables.is_empty() {
793        Ok(())
794    } else {
795        Err(anyhow!(
796            "the following environment variables must not be set: {}",
797            variables.join(", ")
798        ))
799    }
800}
801
802fn setup_panic_handler() {
803    // Redirect panics to the logs.
804    let backtrace_env = std::env::var("RUST_BACKTRACE");
805    let show_backtraces =
806        backtrace_env.as_deref() == Ok("1") || backtrace_env.as_deref() == Ok("full");
807    if show_backtraces {
808        tracing::warn!(
809            "RUST_BACKTRACE={} detected. This is useful for diagnostics but will have a performance impact and may leak sensitive information",
810            backtrace_env.as_ref().unwrap()
811        );
812    }
813    std::panic::set_hook(Box::new(move |e| {
814        if show_backtraces {
815            let backtrace = std::backtrace::Backtrace::capture();
816            tracing::error!("{}\n{}", e, backtrace)
817        } else {
818            tracing::error!("{}", e)
819        }
820
821        // Once we've panic'ed the behaviour of the router is non-deterministic
822        // We've logged out the panic details. Terminate with an error code
823        std::process::exit(1);
824    }));
825}
826
827#[cfg(test)]
828mod tests {
829    use crate::executable::add_log_filter;
830    use crate::executable::env_variables_set;
831    use crate::executable::reject_environment_variables;
832
833    #[test]
834    fn simplest_logging_modifications() {
835        for level in ["off", "error", "warn", "info", "debug", "trace"] {
836            assert_eq!(
837                add_log_filter(level).expect("conversion works"),
838                format!("info,apollo_router={level}")
839            );
840        }
841    }
842
843    // It's hard to have comprehensive tests for this kind of functionality,
844    // so this set is derived from the examples at:
845    // https://docs.rs/env_logger/latest/env_logger/#filtering-results
846    // which is a reasonably corpus of things to test.
847    #[test]
848    fn complex_logging_modifications() {
849        assert_eq!(add_log_filter("hello").unwrap(), "info,hello");
850        assert_eq!(add_log_filter("trace").unwrap(), "info,apollo_router=trace");
851        assert_eq!(add_log_filter("TRACE").unwrap(), "info,apollo_router=trace");
852        assert_eq!(add_log_filter("info").unwrap(), "info,apollo_router=info");
853        assert_eq!(add_log_filter("INFO").unwrap(), "info,apollo_router=info");
854        assert_eq!(add_log_filter("hello=debug").unwrap(), "info,hello=debug");
855        assert_eq!(add_log_filter("hello=DEBUG").unwrap(), "info,hello=debug");
856        assert_eq!(
857            add_log_filter("hello,std::option").unwrap(),
858            "info,hello,std::option"
859        );
860        assert_eq!(
861            add_log_filter("error,hello=warn").unwrap(),
862            "info,apollo_router=error,hello=warn"
863        );
864        assert_eq!(
865            add_log_filter("error,hello=off").unwrap(),
866            "info,apollo_router=error,hello=off"
867        );
868        assert_eq!(add_log_filter("off").unwrap(), "info,apollo_router=off");
869        assert_eq!(add_log_filter("OFF").unwrap(), "info,apollo_router=off");
870        assert_eq!(add_log_filter("hello/foo").unwrap(), "info,hello/foo");
871        assert_eq!(add_log_filter("hello/f.o").unwrap(), "info,hello/f.o");
872        assert_eq!(
873            add_log_filter("hello=debug/foo*foo").unwrap(),
874            "info,hello=debug/foo*foo"
875        );
876        assert_eq!(
877            add_log_filter("error,hello=warn/[0-9]scopes").unwrap(),
878            "info,apollo_router=error,hello=warn/[0-9]scopes"
879        );
880        // Add some hard ones
881        assert_eq!(
882            add_log_filter("hyper=debug,warn,regex=warn,h2=off").unwrap(),
883            "info,hyper=debug,apollo_router=warn,regex=warn,h2=off"
884        );
885        assert_eq!(
886            add_log_filter("hyper=debug,apollo_router=off,regex=info,h2=off").unwrap(),
887            "info,hyper=debug,apollo_router=off,regex=info,h2=off"
888        );
889        assert_eq!(
890            add_log_filter("apollo_router::plugins=debug").unwrap(),
891            "info,apollo_router::plugins=debug"
892        );
893    }
894
895    mod validation_tests {
896        use tokio::time::Duration;
897
898        use super::super::Executable;
899        use super::super::Opt;
900        use crate::router::SchemaSource;
901
902        #[tokio::test]
903        async fn test_conflicting_supergraph_file_and_graph_artifact_reference() {
904            // Test that --supergraph and --graph-artifact-reference cannot be used together
905            let temp_dir = tempfile::tempdir().unwrap();
906            let supergraph_path = temp_dir.path().join("supergraph.graphql");
907            std::fs::File::create(&supergraph_path).unwrap();
908
909            let schema = Some(SchemaSource::File {
910                path: supergraph_path,
911                watch: false,
912            });
913
914            let opt = Opt {
915                log_level: "error".to_string(),
916                hot_reload: false,
917                config_path: None,
918                dev: false,
919                supergraph_path: None,
920                supergraph_urls: None,
921                command: None,
922                apollo_key: Some("test-key".to_string()),
923                #[cfg(unix)]
924                apollo_key_path: None,
925                apollo_graph_ref: None,
926                apollo_router_license: None,
927                apollo_router_license_path: None,
928                apollo_uplink_endpoints: None,
929                graph_artifact_reference: Some(
930                    "registry.apollographql.com/my-graph@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef".to_string(),
931                ),
932                anonymous_telemetry_disabled: true,
933                apollo_uplink_timeout: Duration::from_secs(30),
934                listen_address: None,
935                version: false,
936            };
937
938            // Provide an explicit minimal config to avoid default Configuration parsing issues
939            use crate::router::ConfigurationSource;
940            let supergraph = crate::configuration::Supergraph::builder().build();
941            let config = ConfigurationSource::Static(Box::new(crate::Configuration {
942                supergraph,
943                health_check: Default::default(),
944                sandbox: Default::default(),
945                homepage: Default::default(),
946                server: Default::default(),
947                cors: Default::default(),
948                tls: Default::default(),
949                apq: Default::default(),
950                persisted_queries: Default::default(),
951                limits: Default::default(),
952                experimental_chaos: Default::default(),
953                batching: Default::default(),
954                experimental_type_conditioned_fetching: false,
955                experimental_hoist_orphan_errors: Default::default(),
956                plugins: Default::default(),
957                apollo_plugins: Default::default(),
958                notify: Default::default(),
959                uplink: None,
960                validated_yaml: None,
961                raw_yaml: None,
962            }));
963
964            let result = Executable::inner_start(
965                None,
966                schema,
967                Some(config),
968                Some(crate::router::LicenseSource::default()),
969                opt,
970            )
971            .await;
972
973            assert!(result.is_err(), "Should fail with conflicting options");
974            let error_msg = result.unwrap_err().to_string();
975            assert!(
976                error_msg.contains("cannot be used together"),
977                "Error should mention conflicting options, got: {}",
978                error_msg
979            );
980            assert!(
981                error_msg.contains("--supergraph")
982                    || error_msg.contains("--graph-artifact-reference"),
983                "Error should mention the conflicting options"
984            );
985        }
986
987        #[tokio::test]
988        async fn test_conflicting_supergraph_urls_and_graph_artifact_reference() {
989            // Test that APOLLO_ROUTER_SUPERGRAPH_URLS and --graph-artifact-reference cannot be used together
990            use url::Url;
991            let test_url = Url::parse("https://example.com/schema.graphql").unwrap();
992            let schema = Some(SchemaSource::URLs {
993                urls: vec![test_url],
994            });
995
996            let opt = Opt {
997                log_level: "error".to_string(),
998                hot_reload: false,
999                config_path: None,
1000                dev: false,
1001                supergraph_path: None,
1002                supergraph_urls: Some(vec![Url::parse("https://example.com/schema.graphql").unwrap()]),
1003                command: None,
1004                apollo_key: Some("test-key".to_string()),
1005                #[cfg(unix)]
1006                apollo_key_path: None,
1007                apollo_graph_ref: None,
1008                apollo_router_license: None,
1009                apollo_router_license_path: None,
1010                apollo_uplink_endpoints: None,
1011                graph_artifact_reference: Some(
1012                    "registry.apollographql.com/my-graph@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef".to_string(),
1013                ),
1014                anonymous_telemetry_disabled: true,
1015                apollo_uplink_timeout: Duration::from_secs(30),
1016                listen_address: None,
1017                version: false,
1018            };
1019
1020            // Provide an explicit minimal config to avoid default Configuration parsing issues
1021            use crate::router::ConfigurationSource;
1022            let supergraph = crate::configuration::Supergraph::builder().build();
1023            let config = ConfigurationSource::Static(Box::new(crate::Configuration {
1024                supergraph,
1025                health_check: Default::default(),
1026                sandbox: Default::default(),
1027                homepage: Default::default(),
1028                server: Default::default(),
1029                cors: Default::default(),
1030                tls: Default::default(),
1031                apq: Default::default(),
1032                persisted_queries: Default::default(),
1033                limits: Default::default(),
1034                experimental_chaos: Default::default(),
1035                batching: Default::default(),
1036                experimental_type_conditioned_fetching: false,
1037                experimental_hoist_orphan_errors: Default::default(),
1038                plugins: Default::default(),
1039                apollo_plugins: Default::default(),
1040                notify: Default::default(),
1041                uplink: None,
1042                validated_yaml: None,
1043                raw_yaml: None,
1044            }));
1045
1046            let result = Executable::inner_start(
1047                None,
1048                schema,
1049                Some(config),
1050                Some(crate::router::LicenseSource::default()),
1051                opt,
1052            )
1053            .await;
1054
1055            assert!(result.is_err(), "Should fail with conflicting options");
1056            let error_msg = result.unwrap_err().to_string();
1057            assert!(
1058                error_msg.contains("cannot be used together"),
1059                "Error should mention conflicting options, got: {}",
1060                error_msg
1061            );
1062            assert!(
1063                error_msg.contains("APOLLO_ROUTER_SUPERGRAPH_URLS")
1064                    || error_msg.contains("--graph-artifact-reference"),
1065                "Error should mention the conflicting options"
1066            );
1067        }
1068    }
1069
1070    #[test]
1071    fn it_observes_environment_variables() {
1072        const VALID_ENV_VAR: &str = "CARGO_HOME";
1073
1074        // if we're running tests, we should have a CARGO_HOME env variable present
1075        assert!(std::env::var(VALID_ENV_VAR).is_ok());
1076
1077        // make sure the env_variables_set function can see that, both alone and in a list
1078        assert!(env_variables_set(&[VALID_ENV_VAR]).contains(&VALID_ENV_VAR));
1079        assert!(
1080            env_variables_set(&[VALID_ENV_VAR, "ANOTHER_ENV_VARIABLE"]).contains(&VALID_ENV_VAR)
1081        );
1082
1083        // make sure the env_variables_set variable doesn't find not-present environment variables
1084        assert!(env_variables_set(&["AN_EXTREMELY_UNLIKELY_TO_BE_SET_VARIABLE"]).is_empty());
1085    }
1086
1087    #[test]
1088    fn it_returns_an_error_when_env_variable_provided() {
1089        assert!(reject_environment_variables(&[]).is_ok());
1090
1091        let err = reject_environment_variables(&["env1"]).unwrap_err();
1092        assert!(err.to_string().contains("env1"));
1093
1094        let err = reject_environment_variables(&["env1", "env2"]).unwrap_err();
1095        assert!(err.to_string().contains("env1"));
1096        assert!(err.to_string().contains("env2"));
1097    }
1098}