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