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