1use 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#[derive(Subcommand, Debug)]
66enum Commands {
67 Config(ConfigSubcommandArgs),
69}
70
71#[derive(Args, Debug)]
72struct ConfigSubcommandArgs {
73 #[clap(subcommand)]
75 command: ConfigSubcommand,
76}
77
78#[derive(Subcommand, Debug)]
79enum ConfigSubcommand {
80 Schema,
82
83 Upgrade {
85 #[clap(value_parser, env = "APOLLO_ROUTER_CONFIG_PATH")]
87 config_path: PathBuf,
88
89 #[clap(action = ArgAction::SetTrue, long)]
91 diff: bool,
92 },
93 Validate {
95 #[clap(value_parser, env = "APOLLO_ROUTER_CONFIG_PATH")]
97 config_path: PathBuf,
98 },
99 Experimental,
101 Preview,
103}
104
105#[derive(Parser, Debug)]
107#[clap(name = "router", about = "Apollo federation router")]
108#[command(disable_version_flag(true))]
109pub struct Opt {
110 #[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 log_level: String,
120
121 #[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 #[clap(
132 short,
133 long = "config",
134 value_parser,
135 env = "APOLLO_ROUTER_CONFIG_PATH"
136 )]
137 config_path: Option<PathBuf>,
138
139 #[clap(env = "APOLLO_ROUTER_DEV", long = "dev", action(ArgAction::SetTrue))]
141 dev: bool,
142
143 #[clap(
145 short,
146 long = "supergraph",
147 value_parser,
148 env = "APOLLO_ROUTER_SUPERGRAPH_PATH"
149 )]
150 supergraph_path: Option<PathBuf>,
151
152 #[clap(env = "APOLLO_ROUTER_SUPERGRAPH_URLS", value_delimiter = ',')]
154 supergraph_urls: Option<Vec<Url>>,
155
156 #[clap(subcommand)]
158 command: Option<Commands>,
159
160 #[clap(skip = std::env::var("APOLLO_KEY").ok())]
162 apollo_key: Option<String>,
163
164 #[cfg(unix)]
166 #[clap(long = "apollo-key-path", env = "APOLLO_KEY_PATH")]
167 apollo_key_path: Option<PathBuf>,
168
169 #[clap(skip = std::env::var("APOLLO_GRAPH_REF").ok())]
171 apollo_graph_ref: Option<String>,
172
173 #[clap(skip = std::env::var("APOLLO_ROUTER_LICENSE").ok())]
175 apollo_router_license: Option<String>,
176
177 #[clap(long = "license", env = "APOLLO_ROUTER_LICENSE_PATH")]
179 apollo_router_license_path: Option<PathBuf>,
180
181 #[clap(long, env, action = ArgAction::Append)]
183 apollo_uplink_endpoints: Option<String>,
185
186 #[clap(long, env = "APOLLO_GRAPH_ARTIFACT_REFERENCE", action = ArgAction::Append)]
188 graph_artifact_reference: Option<String>,
189
190 #[clap(long, env = "APOLLO_TELEMETRY_DISABLED", value_parser = FalseyValueParser::new())]
192 anonymous_telemetry_disabled: bool,
193
194 #[clap(long, default_value = "30s", value_parser = humantime::parse_duration, env)]
196 apollo_uplink_timeout: Duration,
197
198 #[clap(long = "listen", env = "APOLLO_ROUTER_LISTEN_ADDRESS")]
200 listen_address: Option<SocketAddr>,
201
202 #[clap(action = ArgAction::SetTrue, long, short = 'V')]
204 pub(crate) version: bool,
205}
206
207fn add_log_filter(raw: &str) -> Result<String, String> {
211 match std::env::var("RUST_LOG") {
212 Ok(filter) => Ok(filter),
213 Err(_e) => {
214 let lowered = raw.to_lowercase();
216 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 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 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
301pub 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 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#[non_exhaustive]
332pub struct Executable {}
333
334#[buildstructor::buildstructor]
335impl Executable {
336 #[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 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 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 tokio::task::spawn_blocking(move || {
469 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 opt.hot_reload = opt.hot_reload || opt.dev;
492
493 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 #[cfg(unix)]
540 let akp = &opt.apollo_key_path;
541 #[cfg(not(unix))]
542 let akp: &Option<PathBuf> = &None;
543
544 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 #[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 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 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 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 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
781fn 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
790fn 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 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 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 #[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 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 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 use crate::router::ConfigurationSource;
940 let supergraph = crate::configuration::Supergraph::builder().build();
941 let config = ConfigurationSource::Static(Box::new(crate::Configuration {
942 supergraph,
943 health_check: Default::default(),
944 sandbox: Default::default(),
945 homepage: Default::default(),
946 server: Default::default(),
947 cors: Default::default(),
948 tls: Default::default(),
949 apq: Default::default(),
950 persisted_queries: Default::default(),
951 limits: Default::default(),
952 experimental_chaos: Default::default(),
953 batching: Default::default(),
954 experimental_type_conditioned_fetching: false,
955 experimental_hoist_orphan_errors: Default::default(),
956 plugins: Default::default(),
957 apollo_plugins: Default::default(),
958 notify: Default::default(),
959 uplink: None,
960 validated_yaml: None,
961 raw_yaml: None,
962 }));
963
964 let result = Executable::inner_start(
965 None,
966 schema,
967 Some(config),
968 Some(crate::router::LicenseSource::default()),
969 opt,
970 )
971 .await;
972
973 assert!(result.is_err(), "Should fail with conflicting options");
974 let error_msg = result.unwrap_err().to_string();
975 assert!(
976 error_msg.contains("cannot be used together"),
977 "Error should mention conflicting options, got: {}",
978 error_msg
979 );
980 assert!(
981 error_msg.contains("--supergraph")
982 || error_msg.contains("--graph-artifact-reference"),
983 "Error should mention the conflicting options"
984 );
985 }
986
987 #[tokio::test]
988 async fn test_conflicting_supergraph_urls_and_graph_artifact_reference() {
989 use url::Url;
991 let test_url = Url::parse("https://example.com/schema.graphql").unwrap();
992 let schema = Some(SchemaSource::URLs {
993 urls: vec![test_url],
994 });
995
996 let opt = Opt {
997 log_level: "error".to_string(),
998 hot_reload: false,
999 config_path: None,
1000 dev: false,
1001 supergraph_path: None,
1002 supergraph_urls: Some(vec![Url::parse("https://example.com/schema.graphql").unwrap()]),
1003 command: None,
1004 apollo_key: Some("test-key".to_string()),
1005 #[cfg(unix)]
1006 apollo_key_path: None,
1007 apollo_graph_ref: None,
1008 apollo_router_license: None,
1009 apollo_router_license_path: None,
1010 apollo_uplink_endpoints: None,
1011 graph_artifact_reference: Some(
1012 "registry.apollographql.com/my-graph@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef".to_string(),
1013 ),
1014 anonymous_telemetry_disabled: true,
1015 apollo_uplink_timeout: Duration::from_secs(30),
1016 listen_address: None,
1017 version: false,
1018 };
1019
1020 use crate::router::ConfigurationSource;
1022 let supergraph = crate::configuration::Supergraph::builder().build();
1023 let config = ConfigurationSource::Static(Box::new(crate::Configuration {
1024 supergraph,
1025 health_check: Default::default(),
1026 sandbox: Default::default(),
1027 homepage: Default::default(),
1028 server: Default::default(),
1029 cors: Default::default(),
1030 tls: Default::default(),
1031 apq: Default::default(),
1032 persisted_queries: Default::default(),
1033 limits: Default::default(),
1034 experimental_chaos: Default::default(),
1035 batching: Default::default(),
1036 experimental_type_conditioned_fetching: false,
1037 experimental_hoist_orphan_errors: Default::default(),
1038 plugins: Default::default(),
1039 apollo_plugins: Default::default(),
1040 notify: Default::default(),
1041 uplink: None,
1042 validated_yaml: None,
1043 raw_yaml: None,
1044 }));
1045
1046 let result = Executable::inner_start(
1047 None,
1048 schema,
1049 Some(config),
1050 Some(crate::router::LicenseSource::default()),
1051 opt,
1052 )
1053 .await;
1054
1055 assert!(result.is_err(), "Should fail with conflicting options");
1056 let error_msg = result.unwrap_err().to_string();
1057 assert!(
1058 error_msg.contains("cannot be used together"),
1059 "Error should mention conflicting options, got: {}",
1060 error_msg
1061 );
1062 assert!(
1063 error_msg.contains("APOLLO_ROUTER_SUPERGRAPH_URLS")
1064 || error_msg.contains("--graph-artifact-reference"),
1065 "Error should mention the conflicting options"
1066 );
1067 }
1068 }
1069
1070 #[test]
1071 fn it_observes_environment_variables() {
1072 const VALID_ENV_VAR: &str = "CARGO_HOME";
1073
1074 assert!(std::env::var(VALID_ENV_VAR).is_ok());
1076
1077 assert!(env_variables_set(&[VALID_ENV_VAR]).contains(&VALID_ENV_VAR));
1079 assert!(
1080 env_variables_set(&[VALID_ENV_VAR, "ANOTHER_ENV_VARIABLE"]).contains(&VALID_ENV_VAR)
1081 );
1082
1083 assert!(env_variables_set(&["AN_EXTREMELY_UNLIKELY_TO_BE_SET_VARIABLE"]).is_empty());
1085 }
1086
1087 #[test]
1088 fn it_returns_an_error_when_env_variable_provided() {
1089 assert!(reject_environment_variables(&[]).is_ok());
1090
1091 let err = reject_environment_variables(&["env1"]).unwrap_err();
1092 assert!(err.to_string().contains("env1"));
1093
1094 let err = reject_environment_variables(&["env1", "env2"]).unwrap_err();
1095 assert!(err.to_string().contains("env1"));
1096 assert!(err.to_string().contains("env2"));
1097 }
1098}