use std::fmt::Debug;
use std::net::SocketAddr;
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
use std::path::PathBuf;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
use std::time::Duration;
use anyhow::Result;
use anyhow::anyhow;
use clap::ArgAction;
use clap::Args;
use clap::Parser;
use clap::Subcommand;
use clap::builder::FalseyValueParser;
use parking_lot::Mutex;
use regex::Captures;
use regex::Regex;
use url::ParseError;
use url::Url;
use crate::LicenseSource;
use crate::configuration::Discussed;
use crate::configuration::expansion::Expansion;
use crate::configuration::generate_config_schema;
use crate::configuration::generate_upgrade;
use crate::configuration::schema::Mode;
use crate::configuration::validate_yaml_configuration;
use crate::metrics::meter_provider_internal;
use crate::plugin::plugins;
use crate::plugins::telemetry::reload::otel::init_telemetry;
use crate::registry::OciConfig;
use crate::registry::should_use_ssl;
use crate::registry::validate_oci_reference;
use crate::router::ConfigurationSource;
use crate::router::RouterHttpServer;
use crate::router::SchemaSource;
use crate::router::ShutdownSource;
use crate::uplink::Endpoints;
use crate::uplink::UplinkConfig;
pub(crate) static APOLLO_ROUTER_DEV_MODE: AtomicBool = AtomicBool::new(false);
pub(crate) static APOLLO_ROUTER_SUPERGRAPH_PATH_IS_SET: AtomicBool = AtomicBool::new(false);
pub(crate) static APOLLO_ROUTER_SUPERGRAPH_URLS_IS_SET: AtomicBool = AtomicBool::new(false);
pub(crate) static APOLLO_ROUTER_LICENCE_IS_SET: AtomicBool = AtomicBool::new(false);
pub(crate) static APOLLO_ROUTER_LICENCE_PATH_IS_SET: AtomicBool = AtomicBool::new(false);
pub(crate) static APOLLO_TELEMETRY_DISABLED: AtomicBool = AtomicBool::new(false);
pub(crate) static APOLLO_ROUTER_LISTEN_ADDRESS: Mutex<Option<SocketAddr>> = Mutex::new(None);
pub(crate) static APOLLO_ROUTER_GRAPH_ARTIFACT_REFERENCE: Mutex<Option<String>> = Mutex::new(None);
pub(crate) static APOLLO_ROUTER_HOT_RELOAD_CLI: AtomicBool = AtomicBool::new(false);
const INITIAL_UPLINK_POLL_INTERVAL: Duration = Duration::from_secs(10);
const INITIAL_OCI_POLL_INTERVAL: Duration = Duration::from_secs(30);
const FORBIDDEN_OTEL_VARS: [&str; 3] = [
"OTEL_EXPORTER_OTLP_ENDPOINT",
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
"OTEL_EXPORTER_OTLP_METRICS_ENDPOINT",
];
#[derive(Subcommand, Debug)]
enum Commands {
Config(ConfigSubcommandArgs),
}
#[derive(Args, Debug)]
struct ConfigSubcommandArgs {
#[clap(subcommand)]
command: ConfigSubcommand,
}
#[derive(Subcommand, Debug)]
enum ConfigSubcommand {
Schema,
Upgrade {
#[clap(value_parser, env = "APOLLO_ROUTER_CONFIG_PATH")]
config_path: PathBuf,
#[clap(action = ArgAction::SetTrue, long)]
diff: bool,
},
Validate {
#[clap(value_parser, env = "APOLLO_ROUTER_CONFIG_PATH")]
config_path: PathBuf,
},
Experimental,
Preview,
}
#[derive(Parser, Debug)]
#[clap(name = "router", about = "Apollo federation router")]
#[command(disable_version_flag(true))]
pub struct Opt {
#[clap(
long = "log",
default_value = "info",
alias = "log-level",
value_parser = add_log_filter,
env = "APOLLO_ROUTER_LOG"
)]
log_level: String,
#[clap(
alias = "hr",
long = "hot-reload",
env = "APOLLO_ROUTER_HOT_RELOAD",
action(ArgAction::SetTrue)
)]
hot_reload: bool,
#[clap(
short,
long = "config",
value_parser,
env = "APOLLO_ROUTER_CONFIG_PATH"
)]
config_path: Option<PathBuf>,
#[clap(env = "APOLLO_ROUTER_DEV", long = "dev", action(ArgAction::SetTrue))]
dev: bool,
#[clap(
short,
long = "supergraph",
value_parser,
env = "APOLLO_ROUTER_SUPERGRAPH_PATH"
)]
supergraph_path: Option<PathBuf>,
#[clap(env = "APOLLO_ROUTER_SUPERGRAPH_URLS", value_delimiter = ',')]
supergraph_urls: Option<Vec<Url>>,
#[clap(subcommand)]
command: Option<Commands>,
#[clap(skip = std::env::var("APOLLO_KEY").ok())]
apollo_key: Option<String>,
#[cfg(unix)]
#[clap(long = "apollo-key-path", env = "APOLLO_KEY_PATH")]
apollo_key_path: Option<PathBuf>,
#[clap(skip = std::env::var("APOLLO_GRAPH_REF").ok())]
apollo_graph_ref: Option<String>,
#[clap(skip = std::env::var("APOLLO_ROUTER_LICENSE").ok())]
apollo_router_license: Option<String>,
#[clap(long = "license", env = "APOLLO_ROUTER_LICENSE_PATH")]
apollo_router_license_path: Option<PathBuf>,
#[clap(long, env, action = ArgAction::Append)]
apollo_uplink_endpoints: Option<String>,
#[clap(long, env = "APOLLO_GRAPH_ARTIFACT_REFERENCE", action = ArgAction::Append)]
graph_artifact_reference: Option<String>,
#[clap(long, env = "APOLLO_TELEMETRY_DISABLED", value_parser = FalseyValueParser::new())]
anonymous_telemetry_disabled: bool,
#[clap(long, default_value = "30s", value_parser = humantime::parse_duration, env)]
apollo_uplink_timeout: Duration,
#[clap(long = "listen", env = "APOLLO_ROUTER_LISTEN_ADDRESS")]
listen_address: Option<SocketAddr>,
#[clap(action = ArgAction::SetTrue, long, short = 'V')]
pub(crate) version: bool,
}
fn add_log_filter(raw: &str) -> Result<String, String> {
match std::env::var("RUST_LOG") {
Ok(filter) => Ok(filter),
Err(_e) => {
let lowered = raw.to_lowercase();
let rgx =
Regex::new(r"(^|,)(off|error|warn|info|debug|trace)").expect("regex must be valid");
let res = rgx.replace_all(&lowered, |caps: &Captures| {
format!("{}apollo_router={}", &caps[1], &caps[2])
});
Ok(format!("info,{res}"))
}
}
}
impl Opt {
pub(crate) fn uplink_config(&self) -> Result<UplinkConfig, anyhow::Error> {
Ok(UplinkConfig {
apollo_key: self
.apollo_key
.clone()
.ok_or(Self::err_require_opt("APOLLO_KEY"))?,
apollo_graph_ref: self
.apollo_graph_ref
.clone()
.ok_or(Self::err_require_opt("APOLLO_GRAPH_REF"))?,
endpoints: self
.apollo_uplink_endpoints
.as_ref()
.map(|endpoints| Self::parse_endpoints(endpoints))
.transpose()?,
poll_interval: INITIAL_UPLINK_POLL_INTERVAL,
timeout: self.apollo_uplink_timeout,
})
}
pub(crate) fn oci_config(&self) -> Result<OciConfig, anyhow::Error> {
let graph_artifact_reference = self
.graph_artifact_reference
.clone()
.ok_or(Self::err_require_opt("APOLLO_GRAPH_ARTIFACT_REFERENCE"))?;
let (validated_reference, _) = validate_oci_reference(&graph_artifact_reference)?;
let poll_interval = std::env::var("TEST_APOLLO_OCI_POLL_INTERVAL")
.ok()
.and_then(|s| {
s.parse::<u64>()
.ok()
.filter(|&val| (1..=60).contains(&val))
.map(Duration::from_secs)
})
.unwrap_or(INITIAL_OCI_POLL_INTERVAL);
let use_ssl = should_use_ssl(&validated_reference);
Ok(OciConfig {
apollo_key: self
.apollo_key
.clone()
.ok_or(Self::err_require_opt("APOLLO_KEY"))?,
reference: validated_reference,
hot_reload: self.hot_reload,
poll_interval,
use_ssl,
})
}
fn parse_endpoints(endpoints: &str) -> std::result::Result<Endpoints, anyhow::Error> {
Ok(Endpoints::fallback(
endpoints
.split(',')
.map(|endpoint| Url::parse(endpoint.trim()))
.collect::<Result<Vec<Url>, ParseError>>()
.map_err(|err| anyhow!("invalid Apollo Uplink endpoint, {}", err))?,
))
}
fn err_require_opt(env_var: &str) -> anyhow::Error {
anyhow!("Use of Apollo Graph OS requires setting the {env_var} environment variable")
}
fn prohibit_env_vars(env_vars: &[&'static str]) -> Result<(), anyhow::Error> {
reject_environment_variables(&env_variables_set(env_vars))
}
}
pub fn main() -> Result<()> {
#[cfg(feature = "dhat-heap")]
crate::allocator::create_heap_profiler();
#[cfg(feature = "dhat-ad-hoc")]
crate::allocator::create_ad_hoc_profiler();
let mut builder = tokio::runtime::Builder::new_multi_thread();
builder.enable_all();
if let Some(nb) = std::env::var("APOLLO_ROUTER_IO_THREADS")
.ok()
.and_then(|value| value.parse::<usize>().ok())
{
builder.worker_threads(nb);
}
let runtime = builder.build()?;
runtime.block_on(Executable::builder().start())
}
#[non_exhaustive]
pub struct Executable {}
#[buildstructor::buildstructor]
impl Executable {
#[builder(entry = "builder", exit = "start", visibility = "pub")]
async fn start(
shutdown: Option<ShutdownSource>,
schema: Option<SchemaSource>,
license: Option<LicenseSource>,
config: Option<ConfigurationSource>,
cli_args: Option<Opt>,
) -> Result<()> {
let opt = cli_args.unwrap_or_else(Opt::parse);
if opt.version {
println!("{}", std::env!("CARGO_PKG_VERSION"));
return Ok(());
}
*crate::services::APOLLO_KEY.lock() = opt.apollo_key.clone();
*crate::services::APOLLO_GRAPH_REF.lock() = opt.apollo_graph_ref.clone();
*APOLLO_ROUTER_LISTEN_ADDRESS.lock() = opt.listen_address;
*APOLLO_ROUTER_GRAPH_ARTIFACT_REFERENCE.lock() = opt.graph_artifact_reference.clone();
if opt.hot_reload {
APOLLO_ROUTER_HOT_RELOAD_CLI.store(true, Ordering::Relaxed);
}
APOLLO_ROUTER_DEV_MODE.store(opt.dev, Ordering::Relaxed);
APOLLO_ROUTER_SUPERGRAPH_PATH_IS_SET
.store(opt.supergraph_path.is_some(), Ordering::Relaxed);
APOLLO_ROUTER_SUPERGRAPH_URLS_IS_SET
.store(opt.supergraph_urls.is_some(), Ordering::Relaxed);
APOLLO_ROUTER_LICENCE_IS_SET.store(opt.apollo_router_license.is_some(), Ordering::Relaxed);
APOLLO_ROUTER_LICENCE_PATH_IS_SET
.store(opt.apollo_router_license_path.is_some(), Ordering::Relaxed);
APOLLO_TELEMETRY_DISABLED.store(opt.anonymous_telemetry_disabled, Ordering::Relaxed);
let apollo_telemetry_initialized = if graph_os() {
init_telemetry(&opt.log_level)?;
true
} else {
init_telemetry(&opt.log_level).is_ok()
};
setup_panic_handler();
let result = match opt.command.as_ref() {
Some(Commands::Config(ConfigSubcommandArgs {
command: ConfigSubcommand::Schema,
})) => {
let schema = generate_config_schema();
println!("{}", serde_json::to_string_pretty(&schema)?);
Ok(())
}
Some(Commands::Config(ConfigSubcommandArgs {
command: ConfigSubcommand::Validate { config_path },
})) => {
let config_string = std::fs::read_to_string(config_path)?;
validate_yaml_configuration(
&config_string,
Expansion::default()?,
Mode::NoUpgrade,
)?
.validate()?;
println!("Configuration at path {config_path:?} is valid!");
Ok(())
}
Some(Commands::Config(ConfigSubcommandArgs {
command: ConfigSubcommand::Upgrade { config_path, diff },
})) => {
let config_string = std::fs::read_to_string(config_path)?;
let output = generate_upgrade(&config_string, *diff)?;
println!("{output}");
Ok(())
}
Some(Commands::Config(ConfigSubcommandArgs {
command: ConfigSubcommand::Experimental,
})) => {
Discussed::new().print_experimental();
Ok(())
}
Some(Commands::Config(ConfigSubcommandArgs {
command: ConfigSubcommand::Preview,
})) => {
Discussed::new().print_preview();
Ok(())
}
None => Self::inner_start(shutdown, schema, config, license, opt).await,
};
if apollo_telemetry_initialized {
tokio::task::spawn_blocking(move || {
opentelemetry::global::set_tracer_provider(
opentelemetry_sdk::trace::SdkTracerProvider::default(),
);
if let Err(error) = meter_provider_internal().shutdown() {
tracing::error!(%error, "Failed to shut down OTel meter provider cleanly");
}
})
.await?;
}
result
}
async fn inner_start(
shutdown: Option<ShutdownSource>,
schema: Option<SchemaSource>,
config: Option<ConfigurationSource>,
license: Option<LicenseSource>,
mut opt: Opt,
) -> Result<()> {
let current_directory = std::env::current_dir()?;
opt.hot_reload = opt.hot_reload || opt.dev;
Opt::prohibit_env_vars(&FORBIDDEN_OTEL_VARS)?;
let configuration = match (config, opt.config_path.as_ref()) {
(Some(_), Some(_)) => {
return Err(anyhow!(
"--config and APOLLO_ROUTER_CONFIG_PATH cannot be used when a custom configuration source is in use"
));
}
(Some(config), None) => config,
#[allow(clippy::blocks_in_conditions)]
_ => opt
.config_path
.as_ref()
.map(|path| {
let path = if path.is_relative() {
current_directory.join(path)
} else {
path.to_path_buf()
};
ConfigurationSource::File {
path,
watch: opt.hot_reload,
}
})
.unwrap_or_default(),
};
let apollo_telemetry_msg = if opt.anonymous_telemetry_disabled {
"Anonymous usage data collection is disabled.".to_string()
} else {
"Anonymous usage data is gathered to inform Apollo product development. See https://go.apollo.dev/o/privacy for details.".to_string()
};
let apollo_router_msg = format!(
"Apollo Router v{} // (c) Apollo Graph, Inc. // Licensed as ELv2 (https://go.apollo.dev/elv2)",
std::env!("CARGO_PKG_VERSION")
);
#[cfg(unix)]
let akp = &opt.apollo_key_path;
#[cfg(not(unix))]
let akp: &Option<PathBuf> = &None;
let has_supergraph_file = schema.is_some() || opt.supergraph_path.is_some();
if has_supergraph_file && opt.graph_artifact_reference.is_some() {
return Err(anyhow!(
"--supergraph (-s) and --graph-artifact-reference cannot be used together. Please specify only one schema source."
));
}
if opt.supergraph_urls.is_some() && opt.graph_artifact_reference.is_some() {
return Err(anyhow!(
"APOLLO_ROUTER_SUPERGRAPH_URLS and --graph-artifact-reference cannot be used together. Please specify only one schema source."
));
}
let schema_source = match (
schema,
&opt.supergraph_path,
&opt.supergraph_urls,
&opt.apollo_key,
akp,
) {
(Some(_), Some(_), _, _, _) | (Some(_), _, Some(_), _, _) => {
return Err(anyhow!(
"--supergraph and APOLLO_ROUTER_SUPERGRAPH_PATH cannot be used when a custom schema source is in use"
));
}
(Some(source), None, None, _, _) => source,
(_, Some(supergraph_path), _, _, _) => {
tracing::info!("{apollo_router_msg}");
tracing::info!("{apollo_telemetry_msg}");
let supergraph_path = if supergraph_path.is_relative() {
current_directory.join(supergraph_path)
} else {
supergraph_path.clone()
};
SchemaSource::File {
path: supergraph_path,
watch: opt.hot_reload,
}
}
(_, _, Some(supergraph_urls), _, _) => {
tracing::info!("{apollo_router_msg}");
tracing::info!("{apollo_telemetry_msg}");
if opt.hot_reload {
tracing::warn!(
"Schema hot reloading is disabled for --supergraph-urls / APOLLO_ROUTER_SUPERGRAPH_URLS."
);
}
SchemaSource::URLs {
urls: supergraph_urls.clone(),
}
}
(_, None, None, _, Some(apollo_key_path)) => {
let apollo_key_path = if apollo_key_path.is_relative() {
current_directory.join(apollo_key_path)
} else {
apollo_key_path.clone()
};
if !apollo_key_path.exists() {
tracing::error!(
"Apollo key at path '{}' does not exist.",
apollo_key_path.to_string_lossy()
);
return Err(anyhow!(
"Apollo key at path '{}' does not exist.",
apollo_key_path.to_string_lossy()
));
} else {
#[cfg(unix)]
{
let meta = std::fs::metadata(apollo_key_path.clone())
.map_err(|err| anyhow!("Failed to read Apollo key file: {}", err))?;
let mode = meta.mode();
if mode & 0o077 != 0 {
return Err(anyhow!(
"Apollo key file permissions ({:#o}) are too permissive",
mode & 0o000777
));
}
let euid = unsafe { libc::geteuid() };
let owner = meta.uid();
if euid != owner {
return Err(anyhow!(
"Apollo key file owner id ({owner}) does not match effective user id ({euid})"
));
}
}
match std::fs::read_to_string(&apollo_key_path) {
Ok(apollo_key) => {
opt.apollo_key = Some(apollo_key.trim().to_string());
}
Err(err) => {
return Err(anyhow!("Failed to read Apollo key file: {}", err));
}
};
match opt.graph_artifact_reference {
None => SchemaSource::Registry(opt.uplink_config()?),
Some(_) => SchemaSource::OCI(opt.oci_config()?),
}
}
}
(_, None, None, Some(_apollo_key), None) => {
tracing::info!("{apollo_router_msg}");
tracing::info!("{apollo_telemetry_msg}");
match opt.graph_artifact_reference {
None => SchemaSource::Registry(opt.uplink_config()?),
Some(_) => SchemaSource::OCI(opt.oci_config()?),
}
}
_ => {
return Err(anyhow!(
r#"{apollo_router_msg}
⚠️ The Apollo Router requires a composed supergraph schema at startup. ⚠️
👉 DO ONE:
* Pass a local schema file with the '--supergraph' option:
$ ./router --supergraph <file_path>
* Fetch a registered schema from GraphOS by setting
these environment variables:
$ APOLLO_KEY="..." APOLLO_GRAPH_REF="..." ./router
For details, see the Apollo docs:
https://www.apollographql.com/docs/federation/managed-federation/setup
🔬 TESTING THINGS OUT?
1. Download an example supergraph schema with Apollo-hosted subgraphs:
$ curl -L https://supergraph.demo.starstuff.dev/ > starstuff.graphql
2. Run the Apollo Router in development mode with the supergraph schema:
$ ./router --dev --supergraph starstuff.graphql
"#
));
}
};
let license = if let Some(license) = license {
license
} else {
match (
&opt.apollo_router_license,
&opt.apollo_router_license_path,
&opt.apollo_key,
&opt.apollo_graph_ref,
) {
(_, Some(license_path), _, _) => {
let license_path = if license_path.is_relative() {
current_directory.join(license_path)
} else {
license_path.clone()
};
LicenseSource::File {
path: license_path,
watch: opt.hot_reload,
}
}
(Some(_license), _, _, _) => LicenseSource::Env,
(_, _, Some(_apollo_key), Some(_apollo_graph_ref)) => {
LicenseSource::Registry(opt.uplink_config()?)
}
_ => LicenseSource::default(),
}
};
let user_plugins_present = plugins().filter(|p| !p.is_apollo()).count() > 0;
let rust_log_set = std::env::var("RUST_LOG").is_ok();
let apollo_router_log = std::env::var("APOLLO_ROUTER_LOG").unwrap_or_default();
if user_plugins_present
&& !rust_log_set
&& ["trace", "debug", "warn", "error", "info"].contains(&apollo_router_log.as_str())
{
tracing::info!(
"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"
);
}
let uplink_config = opt.uplink_config().ok();
if uplink_config
.clone()
.unwrap_or_default()
.endpoints
.unwrap_or_default()
.url_count()
== 1
{
tracing::warn!(
"Only a single uplink endpoint is configured. We recommend specifying at least two endpoints so that a fallback exists."
);
}
let router = RouterHttpServer::builder()
.is_telemetry_disabled(opt.anonymous_telemetry_disabled)
.configuration(configuration)
.and_uplink(uplink_config)
.schema(schema_source)
.license(license)
.shutdown(shutdown.unwrap_or(ShutdownSource::CtrlC))
.start();
if let Err(err) = router.await {
tracing::error!("{}", err);
return Err(err.into());
}
Ok(())
}
}
fn graph_os() -> bool {
crate::services::APOLLO_KEY.lock().is_some()
&& crate::services::APOLLO_GRAPH_REF.lock().is_some()
}
fn env_variables_set(variables: &[&'static str]) -> Vec<&'static str> {
variables
.iter()
.filter(|v| !matches!(std::env::var(v), Err(std::env::VarError::NotPresent)))
.cloned()
.collect()
}
fn reject_environment_variables(variables: &[&str]) -> Result<(), anyhow::Error> {
if variables.is_empty() {
Ok(())
} else {
Err(anyhow!(
"the following environment variables must not be set: {}",
variables.join(", ")
))
}
}
fn setup_panic_handler() {
let backtrace_env = std::env::var("RUST_BACKTRACE");
let show_backtraces =
backtrace_env.as_deref() == Ok("1") || backtrace_env.as_deref() == Ok("full");
if show_backtraces {
tracing::warn!(
"RUST_BACKTRACE={} detected. This is useful for diagnostics but will have a performance impact and may leak sensitive information",
backtrace_env.as_ref().unwrap()
);
}
std::panic::set_hook(Box::new(move |e| {
if show_backtraces {
let backtrace = std::backtrace::Backtrace::capture();
tracing::error!("{}\n{}", e, backtrace)
} else {
tracing::error!("{}", e)
}
std::process::exit(1);
}));
}
#[cfg(test)]
mod tests {
use crate::executable::add_log_filter;
use crate::executable::env_variables_set;
use crate::executable::reject_environment_variables;
#[test]
fn simplest_logging_modifications() {
for level in ["off", "error", "warn", "info", "debug", "trace"] {
assert_eq!(
add_log_filter(level).expect("conversion works"),
format!("info,apollo_router={level}")
);
}
}
#[test]
fn complex_logging_modifications() {
assert_eq!(add_log_filter("hello").unwrap(), "info,hello");
assert_eq!(add_log_filter("trace").unwrap(), "info,apollo_router=trace");
assert_eq!(add_log_filter("TRACE").unwrap(), "info,apollo_router=trace");
assert_eq!(add_log_filter("info").unwrap(), "info,apollo_router=info");
assert_eq!(add_log_filter("INFO").unwrap(), "info,apollo_router=info");
assert_eq!(add_log_filter("hello=debug").unwrap(), "info,hello=debug");
assert_eq!(add_log_filter("hello=DEBUG").unwrap(), "info,hello=debug");
assert_eq!(
add_log_filter("hello,std::option").unwrap(),
"info,hello,std::option"
);
assert_eq!(
add_log_filter("error,hello=warn").unwrap(),
"info,apollo_router=error,hello=warn"
);
assert_eq!(
add_log_filter("error,hello=off").unwrap(),
"info,apollo_router=error,hello=off"
);
assert_eq!(add_log_filter("off").unwrap(), "info,apollo_router=off");
assert_eq!(add_log_filter("OFF").unwrap(), "info,apollo_router=off");
assert_eq!(add_log_filter("hello/foo").unwrap(), "info,hello/foo");
assert_eq!(add_log_filter("hello/f.o").unwrap(), "info,hello/f.o");
assert_eq!(
add_log_filter("hello=debug/foo*foo").unwrap(),
"info,hello=debug/foo*foo"
);
assert_eq!(
add_log_filter("error,hello=warn/[0-9]scopes").unwrap(),
"info,apollo_router=error,hello=warn/[0-9]scopes"
);
assert_eq!(
add_log_filter("hyper=debug,warn,regex=warn,h2=off").unwrap(),
"info,hyper=debug,apollo_router=warn,regex=warn,h2=off"
);
assert_eq!(
add_log_filter("hyper=debug,apollo_router=off,regex=info,h2=off").unwrap(),
"info,hyper=debug,apollo_router=off,regex=info,h2=off"
);
assert_eq!(
add_log_filter("apollo_router::plugins=debug").unwrap(),
"info,apollo_router::plugins=debug"
);
}
mod validation_tests {
use tokio::time::Duration;
use super::super::Executable;
use super::super::Opt;
use crate::router::SchemaSource;
#[tokio::test]
async fn test_conflicting_supergraph_file_and_graph_artifact_reference() {
let temp_dir = tempfile::tempdir().unwrap();
let supergraph_path = temp_dir.path().join("supergraph.graphql");
std::fs::File::create(&supergraph_path).unwrap();
let schema = Some(SchemaSource::File {
path: supergraph_path,
watch: false,
});
let opt = Opt {
log_level: "error".to_string(),
hot_reload: false,
config_path: None,
dev: false,
supergraph_path: None,
supergraph_urls: None,
command: None,
apollo_key: Some("test-key".to_string()),
#[cfg(unix)]
apollo_key_path: None,
apollo_graph_ref: None,
apollo_router_license: None,
apollo_router_license_path: None,
apollo_uplink_endpoints: None,
graph_artifact_reference: Some(
"registry.apollographql.com/my-graph@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef".to_string(),
),
anonymous_telemetry_disabled: true,
apollo_uplink_timeout: Duration::from_secs(30),
listen_address: None,
version: false,
};
use crate::router::ConfigurationSource;
let supergraph = crate::configuration::Supergraph::builder().build();
let config = ConfigurationSource::Static(Box::new(crate::Configuration {
supergraph,
health_check: Default::default(),
sandbox: Default::default(),
homepage: Default::default(),
server: Default::default(),
cors: Default::default(),
tls: Default::default(),
apq: Default::default(),
persisted_queries: Default::default(),
limits: Default::default(),
experimental_chaos: Default::default(),
batching: Default::default(),
experimental_type_conditioned_fetching: false,
experimental_hoist_orphan_errors: Default::default(),
plugins: Default::default(),
apollo_plugins: Default::default(),
notify: Default::default(),
uplink: None,
validated_yaml: None,
raw_yaml: None,
}));
let result = Executable::inner_start(
None,
schema,
Some(config),
Some(crate::router::LicenseSource::default()),
opt,
)
.await;
assert!(result.is_err(), "Should fail with conflicting options");
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("cannot be used together"),
"Error should mention conflicting options, got: {}",
error_msg
);
assert!(
error_msg.contains("--supergraph")
|| error_msg.contains("--graph-artifact-reference"),
"Error should mention the conflicting options"
);
}
#[tokio::test]
async fn test_conflicting_supergraph_urls_and_graph_artifact_reference() {
use url::Url;
let test_url = Url::parse("https://example.com/schema.graphql").unwrap();
let schema = Some(SchemaSource::URLs {
urls: vec![test_url],
});
let opt = Opt {
log_level: "error".to_string(),
hot_reload: false,
config_path: None,
dev: false,
supergraph_path: None,
supergraph_urls: Some(vec![Url::parse("https://example.com/schema.graphql").unwrap()]),
command: None,
apollo_key: Some("test-key".to_string()),
#[cfg(unix)]
apollo_key_path: None,
apollo_graph_ref: None,
apollo_router_license: None,
apollo_router_license_path: None,
apollo_uplink_endpoints: None,
graph_artifact_reference: Some(
"registry.apollographql.com/my-graph@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef".to_string(),
),
anonymous_telemetry_disabled: true,
apollo_uplink_timeout: Duration::from_secs(30),
listen_address: None,
version: false,
};
use crate::router::ConfigurationSource;
let supergraph = crate::configuration::Supergraph::builder().build();
let config = ConfigurationSource::Static(Box::new(crate::Configuration {
supergraph,
health_check: Default::default(),
sandbox: Default::default(),
homepage: Default::default(),
server: Default::default(),
cors: Default::default(),
tls: Default::default(),
apq: Default::default(),
persisted_queries: Default::default(),
limits: Default::default(),
experimental_chaos: Default::default(),
batching: Default::default(),
experimental_type_conditioned_fetching: false,
experimental_hoist_orphan_errors: Default::default(),
plugins: Default::default(),
apollo_plugins: Default::default(),
notify: Default::default(),
uplink: None,
validated_yaml: None,
raw_yaml: None,
}));
let result = Executable::inner_start(
None,
schema,
Some(config),
Some(crate::router::LicenseSource::default()),
opt,
)
.await;
assert!(result.is_err(), "Should fail with conflicting options");
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("cannot be used together"),
"Error should mention conflicting options, got: {}",
error_msg
);
assert!(
error_msg.contains("APOLLO_ROUTER_SUPERGRAPH_URLS")
|| error_msg.contains("--graph-artifact-reference"),
"Error should mention the conflicting options"
);
}
}
#[test]
fn it_observes_environment_variables() {
const VALID_ENV_VAR: &str = "CARGO_HOME";
assert!(std::env::var(VALID_ENV_VAR).is_ok());
assert!(env_variables_set(&[VALID_ENV_VAR]).contains(&VALID_ENV_VAR));
assert!(
env_variables_set(&[VALID_ENV_VAR, "ANOTHER_ENV_VARIABLE"]).contains(&VALID_ENV_VAR)
);
assert!(env_variables_set(&["AN_EXTREMELY_UNLIKELY_TO_BE_SET_VARIABLE"]).is_empty());
}
#[test]
fn it_returns_an_error_when_env_variable_provided() {
assert!(reject_environment_variables(&[]).is_ok());
let err = reject_environment_variables(&["env1"]).unwrap_err();
assert!(err.to_string().contains("env1"));
let err = reject_environment_variables(&["env1", "env2"]).unwrap_err();
assert!(err.to_string().contains("env1"));
assert!(err.to_string().contains("env2"));
}
}