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                reload: Default::default(),
943                supergraph,
944                health_check: Default::default(),
945                sandbox: Default::default(),
946                homepage: Default::default(),
947                server: Default::default(),
948                cors: Default::default(),
949                tls: Default::default(),
950                apq: Default::default(),
951                persisted_queries: Default::default(),
952                limits: Default::default(),
953                experimental_chaos: Default::default(),
954                batching: Default::default(),
955                experimental_type_conditioned_fetching: false,
956                experimental_hoist_orphan_errors: Default::default(),
957                plugins: Default::default(),
958                apollo_plugins: Default::default(),
959                notify: Default::default(),
960                uplink: None,
961                validated_yaml: None,
962                raw_yaml: None,
963            }));
964
965            let result = Executable::inner_start(
966                None,
967                schema,
968                Some(config),
969                Some(crate::router::LicenseSource::default()),
970                opt,
971            )
972            .await;
973
974            assert!(result.is_err(), "Should fail with conflicting options");
975            let error_msg = result.unwrap_err().to_string();
976            assert!(
977                error_msg.contains("cannot be used together"),
978                "Error should mention conflicting options, got: {}",
979                error_msg
980            );
981            assert!(
982                error_msg.contains("--supergraph")
983                    || error_msg.contains("--graph-artifact-reference"),
984                "Error should mention the conflicting options"
985            );
986        }
987
988        #[tokio::test]
989        async fn test_conflicting_supergraph_urls_and_graph_artifact_reference() {
990            // Test that APOLLO_ROUTER_SUPERGRAPH_URLS and --graph-artifact-reference cannot be used together
991            use url::Url;
992            let test_url = Url::parse("https://example.com/schema.graphql").unwrap();
993            let schema = Some(SchemaSource::URLs {
994                urls: vec![test_url],
995            });
996
997            let opt = Opt {
998                log_level: "error".to_string(),
999                hot_reload: false,
1000                config_path: None,
1001                dev: false,
1002                supergraph_path: None,
1003                supergraph_urls: Some(vec![Url::parse("https://example.com/schema.graphql").unwrap()]),
1004                command: None,
1005                apollo_key: Some("test-key".to_string()),
1006                #[cfg(unix)]
1007                apollo_key_path: None,
1008                apollo_graph_ref: None,
1009                apollo_router_license: None,
1010                apollo_router_license_path: None,
1011                apollo_uplink_endpoints: None,
1012                graph_artifact_reference: Some(
1013                    "registry.apollographql.com/my-graph@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef".to_string(),
1014                ),
1015                anonymous_telemetry_disabled: true,
1016                apollo_uplink_timeout: Duration::from_secs(30),
1017                listen_address: None,
1018                version: false,
1019            };
1020
1021            // Provide an explicit minimal config to avoid default Configuration parsing issues
1022            use crate::router::ConfigurationSource;
1023            let supergraph = crate::configuration::Supergraph::builder().build();
1024            let config = ConfigurationSource::Static(Box::new(crate::Configuration {
1025                reload: Default::default(),
1026                supergraph,
1027                health_check: Default::default(),
1028                sandbox: Default::default(),
1029                homepage: Default::default(),
1030                server: Default::default(),
1031                cors: Default::default(),
1032                tls: Default::default(),
1033                apq: Default::default(),
1034                persisted_queries: Default::default(),
1035                limits: Default::default(),
1036                experimental_chaos: Default::default(),
1037                batching: Default::default(),
1038                experimental_type_conditioned_fetching: false,
1039                experimental_hoist_orphan_errors: Default::default(),
1040                plugins: Default::default(),
1041                apollo_plugins: Default::default(),
1042                notify: Default::default(),
1043                uplink: None,
1044                validated_yaml: None,
1045                raw_yaml: None,
1046            }));
1047
1048            let result = Executable::inner_start(
1049                None,
1050                schema,
1051                Some(config),
1052                Some(crate::router::LicenseSource::default()),
1053                opt,
1054            )
1055            .await;
1056
1057            assert!(result.is_err(), "Should fail with conflicting options");
1058            let error_msg = result.unwrap_err().to_string();
1059            assert!(
1060                error_msg.contains("cannot be used together"),
1061                "Error should mention conflicting options, got: {}",
1062                error_msg
1063            );
1064            assert!(
1065                error_msg.contains("APOLLO_ROUTER_SUPERGRAPH_URLS")
1066                    || error_msg.contains("--graph-artifact-reference"),
1067                "Error should mention the conflicting options"
1068            );
1069        }
1070    }
1071
1072    #[test]
1073    fn it_observes_environment_variables() {
1074        const VALID_ENV_VAR: &str = "CARGO_HOME";
1075
1076        // if we're running tests, we should have a CARGO_HOME env variable present
1077        assert!(std::env::var(VALID_ENV_VAR).is_ok());
1078
1079        // make sure the env_variables_set function can see that, both alone and in a list
1080        assert!(env_variables_set(&[VALID_ENV_VAR]).contains(&VALID_ENV_VAR));
1081        assert!(
1082            env_variables_set(&[VALID_ENV_VAR, "ANOTHER_ENV_VARIABLE"]).contains(&VALID_ENV_VAR)
1083        );
1084
1085        // make sure the env_variables_set variable doesn't find not-present environment variables
1086        assert!(env_variables_set(&["AN_EXTREMELY_UNLIKELY_TO_BE_SET_VARIABLE"]).is_empty());
1087    }
1088
1089    #[test]
1090    fn it_returns_an_error_when_env_variable_provided() {
1091        assert!(reject_environment_variables(&[]).is_ok());
1092
1093        let err = reject_environment_variables(&["env1"]).unwrap_err();
1094        assert!(err.to_string().contains("env1"));
1095
1096        let err = reject_environment_variables(&["env1", "env2"]).unwrap_err();
1097        assert!(err.to_string().contains("env1"));
1098        assert!(err.to_string().contains("env2"));
1099    }
1100}