diff --git a/src/command/dev/next/mod.rs b/src/command/dev/next/mod.rs index 58c6f5962..17edffb7b 100644 --- a/src/command/dev/next/mod.rs +++ b/src/command/dev/next/mod.rs @@ -3,13 +3,16 @@ use std::io::stdin; use anyhow::anyhow; +use apollo_federation_types::config::FederationVersion::LatestFedTwo; use apollo_federation_types::config::RouterVersion; use camino::Utf8PathBuf; use futures::StreamExt; use houston::{Config, Profile}; use router::{install::InstallRouter, run::RunRouter, watchers::file::FileWatcher}; use rover_client::operations::config::who_am_i::WhoAmI; +use rover_std::{infoln, warnln}; +use self::router::config::{RouterAddress, RunRouterConfig}; use crate::{ command::Dev, composition::{ @@ -30,8 +33,6 @@ use crate::{ RoverError, RoverOutput, RoverResult, }; -use self::router::config::{RouterAddress, RunRouterConfig}; - mod router; impl Dev { @@ -65,6 +66,9 @@ impl Dev { let profile = &self.opts.plugin_opts.profile; let graph_ref = &self.opts.supergraph_opts.graph_ref; + if let Some(graph_ref) = graph_ref { + eprintln!("retrieving subgraphs remotely from {graph_ref}") + } let supergraph_config_path = &self.opts.supergraph_opts.clone().supergraph_config_path; let service = client_config.get_authenticated_client(profile)?.service()?; @@ -90,7 +94,11 @@ impl Dev { .resolve_federation_version( &client_config, make_fetch_remote_subgraph, - self.opts.supergraph_opts.federation_version.clone(), + self.opts + .supergraph_opts + .federation_version + .clone() + .or(Some(LatestFedTwo)), ) .await? .install_supergraph_binary( @@ -100,6 +108,7 @@ impl Dev { skip_update, ) .await?; + let composition_success = composition_pipeline .compose(&exec_command_impl, &read_file_impl, &write_file_impl, None) .await?; @@ -126,7 +135,12 @@ impl Dev { let composition_messages = composition_runner.run(); - let mut run_router = RunRouter::default() + eprintln!( + "composing supergraph with Federation {}", + composition_pipeline.state.supergraph_binary.version() + ); + + let run_router = RunRouter::default() .install::( router_version, client_config.clone(), @@ -138,7 +152,9 @@ impl Dev { .load_config(&read_file_impl, router_address, router_config_path) .await? .load_remote_config(service, graph_ref.clone(), Some(credential)) - .await + .await; + let router_address = run_router.state.config.address().clone(); + let mut run_router = run_router .run( FsWriteFile::default(), TokioSpawn::default(), @@ -150,6 +166,12 @@ impl Dev { .watch_for_changes(write_file_impl, composition_messages) .await; + warnln!( + "Do not run this command in production! It is intended for local development only." + ); + + infoln!("your supergraph is running! head to {router_address} to query your supergraph"); + loop { tokio::select! { _ = tokio::signal::ctrl_c() => { @@ -160,7 +182,9 @@ impl Dev { Some(router_log) = run_router.router_logs().next() => { match router_log { Ok(router_log) => { - eprintln!("{}", router_log); + if !router_log.to_string().is_empty() { + eprintln!("{}", router_log); + } } Err(err) => { tracing::error!("{:?}", err); @@ -170,7 +194,6 @@ impl Dev { else => break, } } - Ok(RoverOutput::EmptySuccess) } } diff --git a/src/command/dev/next/router/config/mod.rs b/src/command/dev/next/router/config/mod.rs index 6db3cff74..0e1351280 100644 --- a/src/command/dev/next/router/config/mod.rs +++ b/src/command/dev/next/router/config/mod.rs @@ -1,3 +1,4 @@ +use std::fmt::{Display, Formatter}; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use buildstructor::buildstructor; @@ -6,12 +7,11 @@ use http::Uri; use rover_std::{Fs, RoverStdError}; use thiserror::Error; -use crate::utils::effect::read_file::ReadFile; - use self::{ parser::{ParseRouterConfigError, RouterConfigParser}, state::{RunRouterConfigDefault, RunRouterConfigFinal, RunRouterConfigReadConfig}, }; +use crate::utils::effect::read_file::ReadFile; mod parser; pub mod remote; @@ -54,6 +54,19 @@ impl RouterAddress { } } +impl Display for RouterAddress { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let host = self + .host + .to_string() + .replace("127.0.0.1", "localhost") + .replace("0.0.0.0", "localhost") + .replace("[::]", "localhost") + .replace("[::1]", "localhost"); + write!(f, "http://{}:{}", host, self.port) + } +} + impl Default for RouterAddress { fn default() -> Self { RouterAddress { @@ -178,6 +191,10 @@ impl RunRouterConfig { &self.state.health_check_endpoint } + pub fn raw_config(&self) -> String { + self.state.raw_config.clone() + } + #[allow(unused)] pub fn router_config(&self) -> RouterConfig { RouterConfig(self.state.raw_config.to_string()) diff --git a/src/command/dev/next/router/config/parser.rs b/src/command/dev/next/router/config/parser.rs index 40da5c0b4..02b1fa7ca 100644 --- a/src/command/dev/next/router/config/parser.rs +++ b/src/command/dev/next/router/config/parser.rs @@ -84,7 +84,7 @@ impl<'a> RouterConfigParser<'a> { .and_then(|addr_and_port| Some(Uri::from_str(addr_and_port))) // See https://www.apollographql.com/docs/graphos/routing/self-hosted/health-checks for // defaults - .unwrap_or(Uri::from_str("127.0.0.1:8088")) + .unwrap_or(Uri::from_str("http://127.0.0.1:8088")) .map_err(|err| ParseRouterConfigError::ListenPath { path: "health_check.listen", source: err, @@ -161,8 +161,8 @@ health_check: ); let config_yaml = serde_yaml::from_str(&config_yaml_str)?; let router_config = RouterConfigParser { yaml: &config_yaml }; - let health_check = router_config.health_check(); - assert_that!(health_check).is_equal_to(is_health_check_enabled); + let health_check = router_config.health_check_endpoint(); + assert_that!(health_check.is_ok()).is_equal_to(is_health_check_enabled); Ok(()) } @@ -174,9 +174,8 @@ health_check: }; let config_yaml = serde_yaml::from_str(&config_yaml_str)?; let router_config = RouterConfigParser { yaml: &config_yaml }; - let health_check = router_config.health_check(); - assert_that!(health_check).is_false(); - Ok(()) + let health_check = router_config.health_check_endpoint(); + assert_that!(health_check).is_equal_to(Ok(Uri::from_str("http://127.0.0.1:8088/health")?)) } #[rstest] diff --git a/src/command/dev/next/router/run.rs b/src/command/dev/next/router/run.rs index f0ca0987b..7ff907f97 100644 --- a/src/command/dev/next/router/run.rs +++ b/src/command/dev/next/router/run.rs @@ -11,12 +11,19 @@ use rover_client::{ operations::config::who_am_i::{RegistryIdentity, WhoAmIError, WhoAmIRequest}, shared::GraphRef, }; -use rover_std::RoverStdError; +use rover_std::{infoln, RoverStdError}; use tokio::{process::Child, time::sleep}; use tokio_stream::wrappers::UnboundedReceiverStream; use tower::{Service, ServiceExt}; use tracing::info; +use super::{ + binary::{RouterLog, RunRouterBinary, RunRouterBinaryError}, + config::{remote::RemoteRouterConfig, ReadRouterConfigError, RouterAddress, RunRouterConfig}, + hot_reload::{HotReloadEvent, HotReloadWatcher, RouterUpdateEvent}, + install::{InstallRouter, InstallRouterError}, + watchers::router_config::RouterConfigWatcher, +}; use crate::{ command::dev::next::FileWatcher, composition::events::CompositionEvent, @@ -33,16 +40,8 @@ use crate::{ }, }; -use super::{ - binary::{RouterLog, RunRouterBinary, RunRouterBinaryError}, - config::{remote::RemoteRouterConfig, ReadRouterConfigError, RouterAddress, RunRouterConfig}, - hot_reload::{HotReloadEvent, HotReloadWatcher, RouterUpdateEvent}, - install::{InstallRouter, InstallRouterError}, - watchers::router_config::RouterConfigWatcher, -}; - pub struct RunRouter { - state: S, + pub(crate) state: S, } impl Default for RunRouter { @@ -86,6 +85,12 @@ impl RunRouter { .with_address(router_address) .with_config(read_file_impl, config_path.as_ref()) .await?; + if let Some(config_path) = config_path.clone() { + infoln!( + "Watching {} for changes", + config_path.as_std_path().display() + ); + } Ok(RunRouter { state: state::LoadRemoteConfig { binary: self.state.binary, @@ -158,6 +163,7 @@ impl RunRouter { .call( WriteFileRequest::builder() .path(hot_reload_config_path.clone()) + .contents(Vec::from(self.state.config.raw_config())) .build(), ) .await diff --git a/src/composition/pipeline.rs b/src/composition/pipeline.rs index f2b3a712f..a212579c0 100644 --- a/src/composition/pipeline.rs +++ b/src/composition/pipeline.rs @@ -6,18 +6,6 @@ use rover_client::shared::GraphRef; use tempfile::tempdir; use tower::MakeService; -use crate::{ - options::{LicenseAccepter, ProfileOpt}, - utils::{ - client::StudioClientConfig, - effect::{ - exec::ExecCommand, install::InstallBinary, introspect::IntrospectSubgraph, - read_file::ReadFile, read_stdin::ReadStdin, write_file::WriteFile, - }, - parsers::FileDescriptorType, - }, -}; - use super::{ runner::{CompositionRunner, Runner}, supergraph::{ @@ -32,6 +20,18 @@ use super::{ }, CompositionError, CompositionSuccess, }; +use crate::composition::pipeline::CompositionPipelineError::FederationOneWithFederationTwoSubgraphs; +use crate::{ + options::{LicenseAccepter, ProfileOpt}, + utils::{ + client::StudioClientConfig, + effect::{ + exec::ExecCommand, install::InstallBinary, introspect::IntrospectSubgraph, + read_file::ReadFile, read_stdin::ReadStdin, write_file::WriteFile, + }, + parsers::FileDescriptorType, + }, +}; #[derive(thiserror::Error, Debug)] pub enum CompositionPipelineError { @@ -52,10 +52,12 @@ pub enum CompositionPipelineError { }, #[error("Failed to install the supergraph binary.\n{}", .0)] InstallSupergraph(#[from] InstallSupergraphError), + #[error("Federation 1 version specified, but supergraph schema includes Federation 2 subgraphs: {0:?}")] + FederationOneWithFederationTwoSubgraphs(Vec), } pub struct CompositionPipeline { - state: State, + pub(crate) state: State, } impl Default for CompositionPipeline { @@ -98,10 +100,12 @@ impl CompositionPipeline { } FileDescriptorType::Stdin => None, }); + eprintln!("merging supergraph schema files"); let resolver = SupergraphConfigResolver::default() .load_remote_subgraphs(fetch_remote_subgraphs_factory, graph_ref.as_ref()) .await? .load_from_file_descriptor(read_stdin_impl, supergraph_yaml.as_ref())?; + eprintln!("supergraph config loaded successfully"); Ok(CompositionPipeline { state: state::ResolveFederationVersion { resolver, @@ -134,11 +138,27 @@ impl CompositionPipeline { &SubgraphPrompt::default(), ) .await?; - let federation_version = federation_version.unwrap_or_else(|| { + let fed_two_subgraphs = fully_resolved_supergraph_config + .subgraphs() + .iter() + .filter_map(|(name, subgraph)| { + if subgraph.is_fed_two { + Some(name.clone()) + } else { + None + } + }) + .collect::>(); + let federation_version = if let Some(fed_version) = federation_version { + if !fed_two_subgraphs.is_empty() && fed_version.is_fed_one() { + return Err(FederationOneWithFederationTwoSubgraphs(fed_two_subgraphs)); + } + fed_version + } else { fully_resolved_supergraph_config .federation_version() .clone() - }); + }; Ok(CompositionPipeline { state: state::InstallSupergraph { resolver: self.state.resolver, diff --git a/src/composition/supergraph/config/federation.rs b/src/composition/supergraph/config/federation.rs index 9a1b4324a..e4e615fdf 100644 --- a/src/composition/supergraph/config/federation.rs +++ b/src/composition/supergraph/config/federation.rs @@ -6,9 +6,8 @@ use std::marker::PhantomData; use apollo_federation_types::config::{FederationVersion, SupergraphConfig}; use derive_getters::Getters; -use crate::command::supergraph::compose::do_compose::SupergraphComposeOpts; - use super::full::FullyResolvedSubgraph; +use crate::command::supergraph::compose::do_compose::SupergraphComposeOpts; mod state { #[derive(Clone, Debug)] @@ -166,9 +165,8 @@ mod tests { use apollo_federation_types::config::{FederationVersion, SubgraphConfig, SupergraphConfig}; use speculoos::prelude::*; - use crate::composition::supergraph::config::{full::FullyResolvedSubgraph, scenario::*}; - use super::FederationVersionResolverFromSupergraphConfig; + use crate::composition::supergraph::config::{full::FullyResolvedSubgraph, scenario::*}; /// Test showing that federation version is selected from the user-specified fed version /// over local supergraph config or resolved subgraphs @@ -189,7 +187,7 @@ mod tests { let supergraph_config = SupergraphConfig::new(unresolved_subgraphs, Some(FederationVersion::LatestFedOne)); - let resolved_subgraphs = vec![( + let resolved_subgraphs = [( subgraph_name.to_string(), FullyResolvedSubgraph::builder() .schema(subgraph_scenario.sdl) @@ -222,7 +220,7 @@ mod tests { let supergraph_config = SupergraphConfig::new(unresolved_subgraphs, Some(FederationVersion::LatestFedTwo)); - let resolved_subgraphs = vec![( + let resolved_subgraphs = [( subgraph_name.to_string(), FullyResolvedSubgraph::builder() .schema(subgraph_scenario.sdl) @@ -254,7 +252,7 @@ mod tests { let federation_version_resolver = FederationVersionResolverFromSupergraphConfig::default(); let supergraph_config = SupergraphConfig::new(unresolved_subgraphs, None); - let resolved_subgraphs = vec![( + let resolved_subgraphs = [( subgraph_name.to_string(), FullyResolvedSubgraph::builder() .schema(subgraph_scenario.sdl) diff --git a/src/composition/supergraph/config/full/subgraph.rs b/src/composition/supergraph/config/full/subgraph.rs index ee949ad4c..29b83e214 100644 --- a/src/composition/supergraph/config/full/subgraph.rs +++ b/src/composition/supergraph/config/full/subgraph.rs @@ -24,7 +24,7 @@ pub struct FullyResolvedSubgraph { #[getter(skip)] routing_url: Option, schema: String, - is_fed_two: bool, + pub(crate) is_fed_two: bool, } #[buildstructor] diff --git a/src/composition/watchers/subgraphs.rs b/src/composition/watchers/subgraphs.rs index 43afe12ec..5a535bf37 100644 --- a/src/composition/watchers/subgraphs.rs +++ b/src/composition/watchers/subgraphs.rs @@ -7,6 +7,10 @@ use tap::TapFallible; use tokio::{sync::mpsc::UnboundedSender, task::AbortHandle}; use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt}; +use super::watcher::{ + subgraph::{NonRepeatingFetch, SubgraphWatcher, SubgraphWatcherKind, WatchedSdlChange}, + supergraph_config::SupergraphConfigDiff, +}; use crate::{ composition::supergraph::config::{ error::ResolveSubgraphError, full::FullyResolvedSubgraph, lazy::LazilyResolvedSubgraph, @@ -16,11 +20,6 @@ use crate::{ utils::client::StudioClientConfig, }; -use super::watcher::{ - subgraph::{NonRepeatingFetch, SubgraphWatcher, SubgraphWatcherKind, WatchedSdlChange}, - supergraph_config::SupergraphConfigDiff, -}; - #[derive(Debug)] #[cfg_attr(test, derive(derive_getters::Getters))] pub struct SubgraphWatchers { @@ -54,6 +53,7 @@ impl SubgraphWatchers { profile, client_config, introspection_polling_interval, + name.clone(), ) .tap_err(|err| tracing::warn!("Skipping subgraph {}: {:?}", name, err)) .ok() @@ -241,6 +241,7 @@ impl SubgraphHandles { profile, client_config, introspection_polling_interval, + subgraph.to_string(), ) .tap_err(|err| tracing::warn!("Cannot configure new subgraph for {subgraph}: {:?}", err)) { @@ -275,6 +276,7 @@ impl SubgraphHandles { profile, client_config, introspection_polling_interval, + subgraph.to_string(), ) .tap_err(|err| tracing::error!("Unable to get watcher: {err:?}")) { @@ -375,14 +377,13 @@ mod tests { use apollo_federation_types::config::SchemaSource; use camino::Utf8PathBuf; + use super::SubgraphWatchers; use crate::{ composition::supergraph::config::lazy::LazilyResolvedSubgraph, options::ProfileOpt, utils::client::{ClientBuilder, StudioClientConfig}, }; - use super::SubgraphWatchers; - #[test] fn test_subgraphwatchers_new() { let subgraphs = [ diff --git a/src/composition/watchers/watcher/subgraph.rs b/src/composition/watchers/watcher/subgraph.rs index d75abac56..373e55b8b 100644 --- a/src/composition/watchers/watcher/subgraph.rs +++ b/src/composition/watchers/watcher/subgraph.rs @@ -1,15 +1,15 @@ use apollo_federation_types::config::SchemaSource; use futures::{stream::BoxStream, StreamExt}; +use rover_std::infoln; use tap::TapFallible; use tokio::{sync::mpsc::UnboundedSender, task::AbortHandle}; -use crate::{ - options::ProfileOpt, subtask::SubtaskHandleUnit, utils::client::StudioClientConfig, RoverError, -}; - use super::{ file::FileWatcher, introspection::SubgraphIntrospection, remote::RemoteSchema, sdl::Sdl, }; +use crate::{ + options::ProfileOpt, subtask::SubtaskHandleUnit, utils::client::StudioClientConfig, RoverError, +}; #[derive(thiserror::Error, Debug)] #[error("Unsupported subgraph introspection source: {:?}", .0)] @@ -64,26 +64,34 @@ impl SubgraphWatcher { profile: &ProfileOpt, client_config: &StudioClientConfig, introspection_polling_interval: u64, + subgraph_name: String, ) -> Result> { + eprintln!("starting a session with the '{subgraph_name}' subgraph"); // SchemaSource comes from Apollo Federation types. Importantly, it strips comments and // directives from introspection (but not when the source is a file) match schema_source { - SchemaSource::File { file } => Ok(Self { - watcher: SubgraphWatcherKind::File(FileWatcher::new(file)), - routing_url, - }), + SchemaSource::File { file } => { + infoln!("Watching {} for changes", file.as_std_path().display()); + Ok(Self { + watcher: SubgraphWatcherKind::File(FileWatcher::new(file)), + routing_url, + }) + } SchemaSource::SubgraphIntrospection { subgraph_url, introspection_headers, - } => Ok(Self { - watcher: SubgraphWatcherKind::Introspect(SubgraphIntrospection::new( - subgraph_url.clone(), - introspection_headers.map(|header_map| header_map.into_iter().collect()), - client_config, - introspection_polling_interval, - )), - routing_url: routing_url.or_else(|| Some(subgraph_url.to_string())), - }), + } => { + eprintln!("polling {subgraph_url} every {introspection_polling_interval} seconds"); + Ok(Self { + watcher: SubgraphWatcherKind::Introspect(SubgraphIntrospection::new( + subgraph_url.clone(), + introspection_headers.map(|header_map| header_map.into_iter().collect()), + client_config, + introspection_polling_interval, + )), + routing_url: routing_url.or_else(|| Some(subgraph_url.to_string())), + }) + } SchemaSource::Subgraph { graphref, subgraph } => Ok(Self { watcher: SubgraphWatcherKind::Once(NonRepeatingFetch::RemoteSchema( RemoteSchema::new(graphref, subgraph, profile, client_config), diff --git a/src/utils/supergraph_config.rs b/src/utils/supergraph_config.rs index a8f64e4e2..243fff4a6 100644 --- a/src/utils/supergraph_config.rs +++ b/src/utils/supergraph_config.rs @@ -487,7 +487,7 @@ mod test_get_supergraph_config { let supergraph_config_path = third_level_folder.path().join("supergraph.yaml"); fs::write( supergraph_config_path.clone(), - &supergraph_config.into_bytes(), + supergraph_config.into_bytes(), ) .expect("Could not write supergraph.yaml"); @@ -568,9 +568,10 @@ fn merge_supergraph_configs( #[cfg(test)] mod test_merge_supergraph_configs { - use super::*; use rstest::{fixture, rstest}; + use super::*; + #[fixture] #[once] fn local_supergraph_config_with_latest_fed_one_version() -> SupergraphConfig { @@ -1048,6 +1049,7 @@ mod test_resolve_supergraph_yaml { use apollo_federation_types::config::{FederationVersion, SchemaSource, SubgraphConfig}; use assert_fs::TempDir; use camino::Utf8PathBuf; + use houston::Config; use httpmock::MockServer; use indoc::indoc; use rstest::{fixture, rstest}; @@ -1056,14 +1058,11 @@ mod test_resolve_supergraph_yaml { use speculoos::assert_that; use speculoos::prelude::{ResultAssertions, VecAssertions}; - use houston::Config; - + use super::*; use crate::options::ProfileOpt; use crate::utils::client::{ClientBuilder, StudioClientConfig}; use crate::utils::parsers::FileDescriptorType; - use super::*; - #[fixture] fn profile_opt() -> ProfileOpt { ProfileOpt { @@ -1185,7 +1184,7 @@ subgraphs: routing_url: https://people.example.com schema: file: ./people.graphql"#, - latest_fed2_version.to_string() + latest_fed2_version ); let tmp_home = TempDir::new().unwrap(); let mut config_path = Utf8PathBuf::try_from(tmp_home.path().to_path_buf()).unwrap(); @@ -1225,7 +1224,7 @@ subgraphs: routing_url: https://people.example.com schema: file: ../../people.graphql"#, - latest_fed2_version.to_string() + latest_fed2_version ); let tmp_home = TempDir::new().unwrap(); let tmp_dir = Utf8PathBuf::try_from(tmp_home.path().to_path_buf()).unwrap(); @@ -1279,7 +1278,7 @@ subgraphs: routing_url: https://people.example.com schema: file: ../../people.graphql"#, - latest_fed2_version.to_string() + latest_fed2_version ); let tmp_home = TempDir::new().unwrap(); let tmp_dir = Utf8PathBuf::try_from(tmp_home.path().to_path_buf()).unwrap();