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::router::ConfigurationSource;
37use crate::router::RouterHttpServer;
38use crate::router::SchemaSource;
39use crate::router::ShutdownSource;
40use crate::uplink::Endpoints;
41use crate::uplink::UplinkConfig;
42
43pub(crate) static APOLLO_ROUTER_DEV_MODE: AtomicBool = AtomicBool::new(false);
44pub(crate) static APOLLO_ROUTER_SUPERGRAPH_PATH_IS_SET: AtomicBool = AtomicBool::new(false);
45pub(crate) static APOLLO_ROUTER_SUPERGRAPH_URLS_IS_SET: AtomicBool = AtomicBool::new(false);
46pub(crate) static APOLLO_ROUTER_LICENCE_IS_SET: AtomicBool = AtomicBool::new(false);
47pub(crate) static APOLLO_ROUTER_LICENCE_PATH_IS_SET: AtomicBool = AtomicBool::new(false);
48pub(crate) static APOLLO_TELEMETRY_DISABLED: AtomicBool = AtomicBool::new(false);
49pub(crate) static APOLLO_ROUTER_LISTEN_ADDRESS: Mutex<Option<SocketAddr>> = Mutex::new(None);
50
51const INITIAL_UPLINK_POLL_INTERVAL: Duration = Duration::from_secs(10);
52
53#[derive(Subcommand, Debug)]
55enum Commands {
56 Config(ConfigSubcommandArgs),
58}
59
60#[derive(Args, Debug)]
61struct ConfigSubcommandArgs {
62 #[clap(subcommand)]
64 command: ConfigSubcommand,
65}
66
67#[derive(Subcommand, Debug)]
68enum ConfigSubcommand {
69 Schema,
71
72 Upgrade {
74 #[clap(value_parser, env = "APOLLO_ROUTER_CONFIG_PATH")]
76 config_path: PathBuf,
77
78 #[clap(action = ArgAction::SetTrue, long)]
80 diff: bool,
81 },
82 Validate {
84 #[clap(value_parser, env = "APOLLO_ROUTER_CONFIG_PATH")]
86 config_path: PathBuf,
87 },
88 Experimental,
90 Preview,
92}
93
94#[derive(Parser, Debug)]
96#[clap(name = "router", about = "Apollo federation router")]
97#[command(disable_version_flag(true))]
98pub struct Opt {
99 #[clap(
101 long = "log",
102 default_value = "info",
103 alias = "log-level",
104 value_parser = add_log_filter,
105 env = "APOLLO_ROUTER_LOG"
106 )]
107 log_level: String,
109
110 #[clap(
112 alias = "hr",
113 long = "hot-reload",
114 env = "APOLLO_ROUTER_HOT_RELOAD",
115 action(ArgAction::SetTrue)
116 )]
117 hot_reload: bool,
118
119 #[clap(
121 short,
122 long = "config",
123 value_parser,
124 env = "APOLLO_ROUTER_CONFIG_PATH"
125 )]
126 config_path: Option<PathBuf>,
127
128 #[clap(env = "APOLLO_ROUTER_DEV", long = "dev", action(ArgAction::SetTrue))]
130 dev: bool,
131
132 #[clap(
134 short,
135 long = "supergraph",
136 value_parser,
137 env = "APOLLO_ROUTER_SUPERGRAPH_PATH"
138 )]
139 supergraph_path: Option<PathBuf>,
140
141 #[clap(env = "APOLLO_ROUTER_SUPERGRAPH_URLS", value_delimiter = ',')]
143 supergraph_urls: Option<Vec<Url>>,
144
145 #[clap(subcommand)]
147 command: Option<Commands>,
148
149 #[clap(skip = std::env::var("APOLLO_KEY").ok())]
151 apollo_key: Option<String>,
152
153 #[cfg(unix)]
155 #[clap(long = "apollo-key-path", env = "APOLLO_KEY_PATH")]
156 apollo_key_path: Option<PathBuf>,
157
158 #[clap(skip = std::env::var("APOLLO_GRAPH_REF").ok())]
160 apollo_graph_ref: Option<String>,
161
162 #[clap(skip = std::env::var("APOLLO_ROUTER_LICENSE").ok())]
164 apollo_router_license: Option<String>,
165
166 #[clap(long = "license", env = "APOLLO_ROUTER_LICENSE_PATH")]
168 apollo_router_license_path: Option<PathBuf>,
169
170 #[clap(long, env, action = ArgAction::Append)]
172 apollo_uplink_endpoints: Option<String>,
174
175 #[clap(long, env = "APOLLO_GRAPH_ARTIFACT_REFERENCE", action = ArgAction::Append)]
177 graph_artifact_reference: Option<String>,
178
179 #[clap(long, env = "APOLLO_TELEMETRY_DISABLED", value_parser = FalseyValueParser::new())]
181 anonymous_telemetry_disabled: bool,
182
183 #[clap(long, default_value = "30s", value_parser = humantime::parse_duration, env)]
185 apollo_uplink_timeout: Duration,
186
187 #[clap(long = "listen", env = "APOLLO_ROUTER_LISTEN_ADDRESS")]
189 listen_address: Option<SocketAddr>,
190
191 #[clap(action = ArgAction::SetTrue, long, short = 'V')]
193 pub(crate) version: bool,
194}
195
196fn add_log_filter(raw: &str) -> Result<String, String> {
200 match std::env::var("RUST_LOG") {
201 Ok(filter) => Ok(filter),
202 Err(_e) => {
203 let lowered = raw.to_lowercase();
205 let rgx =
207 Regex::new(r"(^|,)(off|error|warn|info|debug|trace)").expect("regex must be valid");
208 let res = rgx.replace_all(&lowered, |caps: &Captures| {
209 format!("{}apollo_router={}", &caps[1], &caps[2])
212 });
213 Ok(format!("info,{res}"))
214 }
215 }
216}
217
218impl Opt {
219 pub(crate) fn uplink_config(&self) -> Result<UplinkConfig, anyhow::Error> {
220 Ok(UplinkConfig {
221 apollo_key: self
222 .apollo_key
223 .clone()
224 .ok_or(Self::err_require_opt("APOLLO_KEY"))?,
225 apollo_graph_ref: self
226 .apollo_graph_ref
227 .clone()
228 .ok_or(Self::err_require_opt("APOLLO_GRAPH_REF"))?,
229 endpoints: self
230 .apollo_uplink_endpoints
231 .as_ref()
232 .map(|endpoints| Self::parse_endpoints(endpoints))
233 .transpose()?,
234 poll_interval: INITIAL_UPLINK_POLL_INTERVAL,
235 timeout: self.apollo_uplink_timeout,
236 })
237 }
238
239 pub(crate) fn oci_config(&self) -> Result<OciConfig, anyhow::Error> {
240 Ok(OciConfig {
241 apollo_key: self
242 .apollo_key
243 .clone()
244 .ok_or(Self::err_require_opt("APOLLO_KEY"))?,
245 reference: Self::validate_oci_reference(
246 &self
247 .graph_artifact_reference
248 .clone()
249 .ok_or(Self::err_require_opt("APOLLO_GRAPH_ARTIFACT_REFERENCE"))?,
250 )?,
251 })
252 }
253
254 pub fn validate_oci_reference(reference: &str) -> std::result::Result<String, anyhow::Error> {
255 let valid_regex = Regex::new(r"@sha256:[0-9a-fA-F]{64}$").unwrap();
258 if valid_regex.is_match(reference) {
259 tracing::debug!("validated OCI configuration");
260 Ok(reference.to_string())
261 } else {
262 Err(anyhow!("invalid graph artifact reference: {reference}"))
263 }
264 }
265
266 fn parse_endpoints(endpoints: &str) -> std::result::Result<Endpoints, anyhow::Error> {
267 Ok(Endpoints::fallback(
268 endpoints
269 .split(',')
270 .map(|endpoint| Url::parse(endpoint.trim()))
271 .collect::<Result<Vec<Url>, ParseError>>()
272 .map_err(|err| anyhow!("invalid Apollo Uplink endpoint, {}", err))?,
273 ))
274 }
275
276 fn err_require_opt(env_var: &str) -> anyhow::Error {
277 anyhow!("Use of Apollo Graph OS requires setting the {env_var} environment variable")
278 }
279}
280
281pub fn main() -> Result<()> {
288 #[cfg(feature = "dhat-heap")]
289 crate::allocator::create_heap_profiler();
290
291 #[cfg(feature = "dhat-ad-hoc")]
292 crate::allocator::create_ad_hoc_profiler();
293
294 let mut builder = tokio::runtime::Builder::new_multi_thread();
295 builder.enable_all();
296
297 if let Some(nb) = std::env::var("APOLLO_ROUTER_IO_THREADS")
300 .ok()
301 .and_then(|value| value.parse::<usize>().ok())
302 {
303 builder.worker_threads(nb);
304 }
305
306 let runtime = builder.build()?;
307 runtime.block_on(Executable::builder().start())
308}
309
310#[non_exhaustive]
312pub struct Executable {}
313
314#[buildstructor::buildstructor]
315impl Executable {
316 #[builder(entry = "builder", exit = "start", visibility = "pub")]
358 async fn start(
359 shutdown: Option<ShutdownSource>,
360 schema: Option<SchemaSource>,
361 license: Option<LicenseSource>,
362 config: Option<ConfigurationSource>,
363 cli_args: Option<Opt>,
364 ) -> Result<()> {
365 let opt = cli_args.unwrap_or_else(Opt::parse);
366
367 if opt.version {
368 println!("{}", std::env!("CARGO_PKG_VERSION"));
369 return Ok(());
370 }
371
372 *crate::services::APOLLO_KEY.lock() = opt.apollo_key.clone();
373 *crate::services::APOLLO_GRAPH_REF.lock() = opt.apollo_graph_ref.clone();
374 *APOLLO_ROUTER_LISTEN_ADDRESS.lock() = opt.listen_address;
375 APOLLO_ROUTER_DEV_MODE.store(opt.dev, Ordering::Relaxed);
376 APOLLO_ROUTER_SUPERGRAPH_PATH_IS_SET
377 .store(opt.supergraph_path.is_some(), Ordering::Relaxed);
378 APOLLO_ROUTER_SUPERGRAPH_URLS_IS_SET
379 .store(opt.supergraph_urls.is_some(), Ordering::Relaxed);
380 APOLLO_ROUTER_LICENCE_IS_SET.store(opt.apollo_router_license.is_some(), Ordering::Relaxed);
381 APOLLO_ROUTER_LICENCE_PATH_IS_SET
382 .store(opt.apollo_router_license_path.is_some(), Ordering::Relaxed);
383 APOLLO_TELEMETRY_DISABLED.store(opt.anonymous_telemetry_disabled, Ordering::Relaxed);
384
385 let apollo_telemetry_initialized = if graph_os() {
386 init_telemetry(&opt.log_level)?;
387 true
388 } else {
389 init_telemetry(&opt.log_level).is_ok()
391 };
392
393 setup_panic_handler();
394
395 let result = match opt.command.as_ref() {
396 Some(Commands::Config(ConfigSubcommandArgs {
397 command: ConfigSubcommand::Schema,
398 })) => {
399 let schema = generate_config_schema();
400 println!("{}", serde_json::to_string_pretty(&schema)?);
401 Ok(())
402 }
403 Some(Commands::Config(ConfigSubcommandArgs {
404 command: ConfigSubcommand::Validate { config_path },
405 })) => {
406 let config_string = std::fs::read_to_string(config_path)?;
407 validate_yaml_configuration(
408 &config_string,
409 Expansion::default()?,
410 Mode::NoUpgrade,
411 )?
412 .validate()?;
413
414 println!("Configuration at path {config_path:?} is valid!");
415
416 Ok(())
417 }
418 Some(Commands::Config(ConfigSubcommandArgs {
419 command: ConfigSubcommand::Upgrade { config_path, diff },
420 })) => {
421 let config_string = std::fs::read_to_string(config_path)?;
422 let output = generate_upgrade(&config_string, *diff)?;
423 println!("{output}");
424 Ok(())
425 }
426 Some(Commands::Config(ConfigSubcommandArgs {
427 command: ConfigSubcommand::Experimental,
428 })) => {
429 Discussed::new().print_experimental();
430 Ok(())
431 }
432 Some(Commands::Config(ConfigSubcommandArgs {
433 command: ConfigSubcommand::Preview,
434 })) => {
435 Discussed::new().print_preview();
436 Ok(())
437 }
438 None => Self::inner_start(shutdown, schema, config, license, opt).await,
439 };
440
441 if apollo_telemetry_initialized {
442 tokio::task::spawn_blocking(move || {
444 opentelemetry::global::shutdown_tracer_provider();
445 meter_provider_internal().shutdown();
446 })
447 .await?;
448 }
449 result
450 }
451
452 async fn inner_start(
453 shutdown: Option<ShutdownSource>,
454 schema: Option<SchemaSource>,
455 config: Option<ConfigurationSource>,
456 license: Option<LicenseSource>,
457 mut opt: Opt,
458 ) -> Result<()> {
459 let current_directory = std::env::current_dir()?;
460 opt.hot_reload = opt.hot_reload || opt.dev;
462
463 let configuration = match (config, opt.config_path.as_ref()) {
464 (Some(_), Some(_)) => {
465 return Err(anyhow!(
466 "--config and APOLLO_ROUTER_CONFIG_PATH cannot be used when a custom configuration source is in use"
467 ));
468 }
469 (Some(config), None) => config,
470 #[allow(clippy::blocks_in_conditions)]
471 _ => opt
472 .config_path
473 .as_ref()
474 .map(|path| {
475 let path = if path.is_relative() {
476 current_directory.join(path)
477 } else {
478 path.to_path_buf()
479 };
480
481 ConfigurationSource::File {
482 path,
483 watch: opt.hot_reload,
484 }
485 })
486 .unwrap_or_default(),
487 };
488
489 let apollo_telemetry_msg = if opt.anonymous_telemetry_disabled {
490 "Anonymous usage data collection is disabled.".to_string()
491 } else {
492 "Anonymous usage data is gathered to inform Apollo product development. See https://go.apollo.dev/o/privacy for details.".to_string()
493 };
494
495 let apollo_router_msg = format!(
496 "Apollo Router v{} // (c) Apollo Graph, Inc. // Licensed as ELv2 (https://go.apollo.dev/elv2)",
497 std::env!("CARGO_PKG_VERSION")
498 );
499
500 #[cfg(unix)]
507 let akp = &opt.apollo_key_path;
508 #[cfg(not(unix))]
509 let akp: &Option<PathBuf> = &None;
510
511 let schema_source = match (
512 schema,
513 &opt.supergraph_path,
514 &opt.supergraph_urls,
515 &opt.apollo_key,
516 akp,
517 ) {
518 (Some(_), Some(_), _, _, _) | (Some(_), _, Some(_), _, _) => {
519 return Err(anyhow!(
520 "--supergraph and APOLLO_ROUTER_SUPERGRAPH_PATH cannot be used when a custom schema source is in use"
521 ));
522 }
523 (Some(source), None, None, _, _) => source,
524 (_, Some(supergraph_path), _, _, _) => {
525 tracing::info!("{apollo_router_msg}");
526 tracing::info!("{apollo_telemetry_msg}");
527
528 let supergraph_path = if supergraph_path.is_relative() {
529 current_directory.join(supergraph_path)
530 } else {
531 supergraph_path.clone()
532 };
533 SchemaSource::File {
534 path: supergraph_path,
535 watch: opt.hot_reload,
536 }
537 }
538 (_, _, Some(supergraph_urls), _, _) => {
539 tracing::info!("{apollo_router_msg}");
540 tracing::info!("{apollo_telemetry_msg}");
541
542 if opt.hot_reload {
543 tracing::warn!(
544 "Schema hot reloading is disabled for --supergraph-urls / APOLLO_ROUTER_SUPERGRAPH_URLS."
545 );
546 }
547
548 SchemaSource::URLs {
549 urls: supergraph_urls.clone(),
550 }
551 }
552 (_, None, None, _, Some(apollo_key_path)) => {
553 let apollo_key_path = if apollo_key_path.is_relative() {
554 current_directory.join(apollo_key_path)
555 } else {
556 apollo_key_path.clone()
557 };
558
559 if !apollo_key_path.exists() {
560 tracing::error!(
561 "Apollo key at path '{}' does not exist.",
562 apollo_key_path.to_string_lossy()
563 );
564 return Err(anyhow!(
565 "Apollo key at path '{}' does not exist.",
566 apollo_key_path.to_string_lossy()
567 ));
568 } else {
569 #[cfg(unix)]
573 {
574 let meta = std::fs::metadata(apollo_key_path.clone())
575 .map_err(|err| anyhow!("Failed to read Apollo key file: {}", err))?;
576 let mode = meta.mode();
577 if mode & 0o077 != 0 {
580 return Err(anyhow!(
581 "Apollo key file permissions ({:#o}) are too permissive",
582 mode & 0o000777
583 ));
584 }
585 let euid = unsafe { libc::geteuid() };
586 let owner = meta.uid();
587 if euid != owner {
588 return Err(anyhow!(
589 "Apollo key file owner id ({owner}) does not match effective user id ({euid})"
590 ));
591 }
592 }
593 match std::fs::read_to_string(&apollo_key_path) {
595 Ok(apollo_key) => {
596 opt.apollo_key = Some(apollo_key.trim().to_string());
597 }
598 Err(err) => {
599 return Err(anyhow!("Failed to read Apollo key file: {}", err));
600 }
601 };
602 match opt.graph_artifact_reference {
603 None => SchemaSource::Registry(opt.uplink_config()?),
604 Some(_) => SchemaSource::OCI(opt.oci_config()?),
605 }
606 }
607 }
608 (_, None, None, Some(_apollo_key), None) => {
609 tracing::info!("{apollo_router_msg}");
610 tracing::info!("{apollo_telemetry_msg}");
611 match opt.graph_artifact_reference {
612 None => SchemaSource::Registry(opt.uplink_config()?),
613 Some(_) => SchemaSource::OCI(opt.oci_config()?),
614 }
615 }
616 _ => {
617 return Err(anyhow!(
618 r#"{apollo_router_msg}
619
620⚠️ The Apollo Router requires a composed supergraph schema at startup. ⚠️
621
622👉 DO ONE:
623
624 * Pass a local schema file with the '--supergraph' option:
625
626 $ ./router --supergraph <file_path>
627
628 * Fetch a registered schema from GraphOS by setting
629 these environment variables:
630
631 $ APOLLO_KEY="..." APOLLO_GRAPH_REF="..." ./router
632
633 For details, see the Apollo docs:
634 https://www.apollographql.com/docs/federation/managed-federation/setup
635
636🔬 TESTING THINGS OUT?
637
638 1. Download an example supergraph schema with Apollo-hosted subgraphs:
639
640 $ curl -L https://supergraph.demo.starstuff.dev/ > starstuff.graphql
641
642 2. Run the Apollo Router in development mode with the supergraph schema:
643
644 $ ./router --dev --supergraph starstuff.graphql
645
646 "#
647 ));
648 }
649 };
650
651 let license = if let Some(license) = license {
657 license
658 } else {
659 match (
660 &opt.apollo_router_license,
661 &opt.apollo_router_license_path,
662 &opt.apollo_key,
663 &opt.apollo_graph_ref,
664 ) {
665 (_, Some(license_path), _, _) => {
666 let license_path = if license_path.is_relative() {
667 current_directory.join(license_path)
668 } else {
669 license_path.clone()
670 };
671 LicenseSource::File {
672 path: license_path,
673 watch: opt.hot_reload,
674 }
675 }
676 (Some(_license), _, _, _) => LicenseSource::Env,
677 (_, _, Some(_apollo_key), Some(_apollo_graph_ref)) => {
678 LicenseSource::Registry(opt.uplink_config()?)
679 }
680
681 _ => LicenseSource::default(),
682 }
683 };
684
685 let user_plugins_present = plugins().filter(|p| !p.is_apollo()).count() > 0;
687 let rust_log_set = std::env::var("RUST_LOG").is_ok();
688 let apollo_router_log = std::env::var("APOLLO_ROUTER_LOG").unwrap_or_default();
689 if user_plugins_present
690 && !rust_log_set
691 && ["trace", "debug", "warn", "error", "info"].contains(&apollo_router_log.as_str())
692 {
693 tracing::info!(
694 "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"
695 );
696 }
697
698 let uplink_config = opt.uplink_config().ok();
699 if uplink_config
700 .clone()
701 .unwrap_or_default()
702 .endpoints
703 .unwrap_or_default()
704 .url_count()
705 == 1
706 {
707 tracing::warn!(
708 "Only a single uplink endpoint is configured. We recommend specifying at least two endpoints so that a fallback exists."
709 );
710 }
711
712 let router = RouterHttpServer::builder()
713 .is_telemetry_disabled(opt.anonymous_telemetry_disabled)
714 .configuration(configuration)
715 .and_uplink(uplink_config)
716 .schema(schema_source)
717 .license(license)
718 .shutdown(shutdown.unwrap_or(ShutdownSource::CtrlC))
719 .start();
720
721 if let Err(err) = router.await {
722 tracing::error!("{}", err);
723 return Err(err.into());
724 }
725 Ok(())
726 }
727}
728
729fn graph_os() -> bool {
730 crate::services::APOLLO_KEY.lock().is_some()
731 && crate::services::APOLLO_GRAPH_REF.lock().is_some()
732}
733
734fn setup_panic_handler() {
735 let backtrace_env = std::env::var("RUST_BACKTRACE");
737 let show_backtraces =
738 backtrace_env.as_deref() == Ok("1") || backtrace_env.as_deref() == Ok("full");
739 if show_backtraces {
740 tracing::warn!(
741 "RUST_BACKTRACE={} detected. This is useful for diagnostics but will have a performance impact and may leak sensitive information",
742 backtrace_env.as_ref().unwrap()
743 );
744 }
745 std::panic::set_hook(Box::new(move |e| {
746 if show_backtraces {
747 let backtrace = std::backtrace::Backtrace::capture();
748 tracing::error!("{}\n{}", e, backtrace)
749 } else {
750 tracing::error!("{}", e)
751 }
752
753 std::process::exit(1);
756 }));
757}
758
759#[cfg(test)]
760mod tests {
761 use crate::executable::add_log_filter;
762
763 #[test]
764 fn simplest_logging_modifications() {
765 for level in ["off", "error", "warn", "info", "debug", "trace"] {
766 assert_eq!(
767 add_log_filter(level).expect("conversion works"),
768 format!("info,apollo_router={level}")
769 );
770 }
771 }
772
773 #[test]
778 fn complex_logging_modifications() {
779 assert_eq!(add_log_filter("hello").unwrap(), "info,hello");
780 assert_eq!(add_log_filter("trace").unwrap(), "info,apollo_router=trace");
781 assert_eq!(add_log_filter("TRACE").unwrap(), "info,apollo_router=trace");
782 assert_eq!(add_log_filter("info").unwrap(), "info,apollo_router=info");
783 assert_eq!(add_log_filter("INFO").unwrap(), "info,apollo_router=info");
784 assert_eq!(add_log_filter("hello=debug").unwrap(), "info,hello=debug");
785 assert_eq!(add_log_filter("hello=DEBUG").unwrap(), "info,hello=debug");
786 assert_eq!(
787 add_log_filter("hello,std::option").unwrap(),
788 "info,hello,std::option"
789 );
790 assert_eq!(
791 add_log_filter("error,hello=warn").unwrap(),
792 "info,apollo_router=error,hello=warn"
793 );
794 assert_eq!(
795 add_log_filter("error,hello=off").unwrap(),
796 "info,apollo_router=error,hello=off"
797 );
798 assert_eq!(add_log_filter("off").unwrap(), "info,apollo_router=off");
799 assert_eq!(add_log_filter("OFF").unwrap(), "info,apollo_router=off");
800 assert_eq!(add_log_filter("hello/foo").unwrap(), "info,hello/foo");
801 assert_eq!(add_log_filter("hello/f.o").unwrap(), "info,hello/f.o");
802 assert_eq!(
803 add_log_filter("hello=debug/foo*foo").unwrap(),
804 "info,hello=debug/foo*foo"
805 );
806 assert_eq!(
807 add_log_filter("error,hello=warn/[0-9]scopes").unwrap(),
808 "info,apollo_router=error,hello=warn/[0-9]scopes"
809 );
810 assert_eq!(
812 add_log_filter("hyper=debug,warn,regex=warn,h2=off").unwrap(),
813 "info,hyper=debug,apollo_router=warn,regex=warn,h2=off"
814 );
815 assert_eq!(
816 add_log_filter("hyper=debug,apollo_router=off,regex=info,h2=off").unwrap(),
817 "info,hyper=debug,apollo_router=off,regex=info,h2=off"
818 );
819 assert_eq!(
820 add_log_filter("apollo_router::plugins=debug").unwrap(),
821 "info,apollo_router::plugins=debug"
822 );
823 }
824
825 #[test]
826 fn test_validate_oci_reference_valid_cases() {
827 let valid_hashes = vec![
829 "@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
830 "@sha256:ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890",
831 "@sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
832 "@sha256:0000000000000000000000000000000000000000000000000000000000000000",
833 "@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
834 ];
835
836 for hash in valid_hashes {
837 let result = super::Opt::validate_oci_reference(hash);
838 assert!(result.is_ok(), "Hash '{}' should be valid", hash);
839 assert_eq!(result.unwrap(), hash);
840 }
841 }
842
843 #[test]
844 fn test_validate_oci_reference_invalid_cases() {
845 let invalid_references = vec![
846 "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
848 "@sha1:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
850 "@sha512:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
851 "@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcde",
853 "@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1",
855 "@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdeg",
857 "@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcde!",
858 "",
860 "@sha256:",
862 "@sha256: 1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
864 "@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef ",
865 "@sha256:12345678-90abcdef-12345678-90abcdef-12345678-90abcdef-12345678-90abcdef",
867 "@sha256:12345678:90abcdef:12345678:90abcdef:12345678:90abcdef:12345678:90abcdef",
869 "@sha256",
871 "sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
873 "@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef:latest",
875 "@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef@tag",
876 ];
877
878 for reference in invalid_references {
879 let result = super::Opt::validate_oci_reference(reference);
880 assert!(
881 result.is_err(),
882 "Reference '{}' should be invalid",
883 reference
884 );
885 let error_msg = result.unwrap_err().to_string();
886 assert!(
887 error_msg.contains("invalid graph artifact reference"),
888 "Error message should contain 'invalid graph artifact reference' for '{}'",
889 reference
890 );
891 assert!(
892 error_msg.contains(reference),
893 "Error message should contain the invalid reference '{}'",
894 reference
895 );
896 }
897 }
898}