Skip to content

Commit 5995c16

Browse files
authored
feat(rover): compose from running services, registered graphs or subgraphs (#519)
This commit introduces multiple ways to run composition locally. You may compose from various subgraphs that could obtain SDL from using Apollo Registry refs (`subgraph`, `graphref`), local file references (`file`) and subgraph introspection (`subgraph_url`). For example: ```yaml subgraphs: films: routing_url: https://films.example.com schema: file: ./films.graphql people: schema: subgraph_url: https://example.com/people actors: routing_url: https://localhost:4005 schema: graphref: mygraph@current subgraph: actors ``` Co-authored-by: Jesse Rosenberger <[email protected]>
1 parent 182240c commit 5995c16

File tree

13 files changed

+326
-132
lines changed

13 files changed

+326
-132
lines changed

CHANGELOG.md

+24
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,30 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
4646
[EverlastingBugstopper]: https://github.com/EverlastingBugstopper
4747
[pull/492]: https://github.com/apollographql/rover/pull/492
4848

49+
- **`rover supergraph compose` allows for registry and introspection SDL sources - [lrlna], [issue/449] [pull/519]**
50+
51+
Pulls subgraphs from various sources specified in the YAML config file. A valid config can now specify schema using Apollo Registry refs (`subgraph`, `graphref`), local file references (`file`) and subgraph introspection (`subgraph_url`):
52+
53+
```yaml
54+
subgraphs:
55+
films:
56+
routing_url: https://films.example.com
57+
schema:
58+
file: ./films.graphql
59+
people:
60+
routing_url: https://example.com/people
61+
schema:
62+
subgraph_url: https://example.com/people
63+
actors:
64+
routing_url: https://localhost:4005
65+
schema:
66+
graphref: mygraph@current
67+
subgraph: actors
68+
```
69+
[lrlna]: https://github.com/lrlna
70+
[issue/449]: https://github.com/apollographql/rover/issues/449
71+
[pull/519]: https://github.com/apollographql/rover/pull/519
72+
4973
- **`--routing-url` is now an optional argument to `rover subgraph publish` - [EverlastingBusgtopper], [issue/169] [pull/484]**
5074

5175
When publishing a subgraph, it is important to include a routing URL for that subgraph, so your graph router

docs/source/errors.md

+7
Original file line numberDiff line numberDiff line change
@@ -227,3 +227,10 @@ This error occurs when working with a federated graph and its subgraphs. When gr
227227
To resolve this error, inspect the printed errors and correct the subgraph schemas.
228228

229229

230+
### E028
231+
232+
This error occurs when a connection could not be established with to an introspection endpoint.
233+
234+
To resolve this problem, make sure the endpoint URL is correct. You may wish to run the command again with `--log=debug`.
235+
236+

docs/source/supergraphs.md

+20-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,26 @@ subgraphs:
4343
file: ./people.graphql
4444
```
4545
46-
The YAML file must specify each subgraph's public-facing URL (`routing_url`), along with the path to its schema (`schema.file`).
46+
In the above example, The YAML file specifies each subgraph's public-facing URL (`routing_url`), along with the path to its schema (`schema.file`).
47+
48+
It's also possible to pull subgraphs from various sources and specify them in the YAML file. For example, here is a configuration that specifies schema using Apollo Registry refs (`subgraph`, `graphref`) and subgraph introspection (`subgraph_url`):
49+
50+
```yaml
51+
subgraphs:
52+
films:
53+
routing_url: https://films.example.com
54+
schema:
55+
file: ./films.graphql
56+
people:
57+
routing_url: https://example.com/people
58+
schema:
59+
subgraph_url: https://example.com/people
60+
actors:
61+
routing_url: https://localhost:4005
62+
schema:
63+
graphref: mygraph@current
64+
subgraph: actors
65+
```
4766

4867
### Output format
4968

installers/binstall/src/system/windows.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ fn get_windows_path_var() -> Result<Option<String>, InstallerError> {
4444
}
4545
}
4646
Err(ref e) if e.kind() == io::ErrorKind::NotFound => Ok(Some(String::new())),
47-
Err(e) => Err(e)?,
47+
Err(e) => Err(e.into()),
4848
}
4949
}
5050

@@ -83,7 +83,7 @@ fn add_to_path(old_path: &str, path_str: &str) -> Option<String> {
8383
None
8484
} else {
8585
let mut new_path = path_str.to_string();
86-
new_path.push_str(";");
86+
new_path.push(';');
8787
new_path.push_str(&old_path);
8888
Some(new_path)
8989
}
@@ -116,7 +116,7 @@ fn apply_new_path(new_path: &str) -> Result<(), InstallerError> {
116116
SendMessageTimeoutA(
117117
HWND_BROADCAST,
118118
WM_SETTINGCHANGE,
119-
0 as WPARAM,
119+
0_usize,
120120
"Environment\0".as_ptr() as LPARAM,
121121
SMTO_ABORTIFHUNG,
122122
5000,

installers/npm/package-lock.json

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

installers/npm/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,6 @@
3333
"console.table": "^0.10.0"
3434
},
3535
"devDependencies": {
36-
"prettier": "^2.2.1"
36+
"prettier": "^2.3.0"
3737
}
3838
}

src/command/supergraph/compose.rs

+218-4
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,41 @@
1-
use crate::{anyhow, command::RoverStdout, Result};
1+
use crate::utils::{client::StudioClientConfig, parsers::parse_graph_ref};
2+
use crate::{anyhow, command::RoverStdout, error::RoverError, Result, Suggestion};
23

34
use ansi_term::Colour::Red;
45
use camino::Utf8PathBuf;
6+
use harmonizer::ServiceDefinition as SubgraphDefinition;
7+
use rover_client::{
8+
blocking::Client,
9+
query::subgraph::{fetch, introspect},
10+
};
511
use serde::Serialize;
12+
use std::{collections::HashMap, fs};
613
use structopt::StructOpt;
714

8-
use super::config;
15+
use super::config::{self, SchemaSource, SupergraphConfig};
916

1017
#[derive(Debug, Serialize, StructOpt)]
1118
pub struct Compose {
1219
/// The relative path to the supergraph configuration file.
1320
#[structopt(long = "config")]
1421
#[serde(skip_serializing)]
1522
config_path: Utf8PathBuf,
23+
24+
/// Name of configuration profile to use
25+
#[structopt(long = "profile", default_value = "default")]
26+
#[serde(skip_serializing)]
27+
profile_name: String,
1628
}
1729

1830
impl Compose {
19-
pub fn run(&self) -> Result<RoverStdout> {
31+
pub fn run(&self, client_config: StudioClientConfig) -> Result<RoverStdout> {
2032
let supergraph_config = config::parse_supergraph_config(&self.config_path)?;
21-
let subgraph_definitions = supergraph_config.get_subgraph_definitions(&self.config_path)?;
33+
let subgraph_definitions = get_subgraph_definitions(
34+
supergraph_config,
35+
&self.config_path,
36+
client_config,
37+
&self.profile_name,
38+
)?;
2239

2340
match harmonizer::harmonize(subgraph_definitions) {
2441
Ok(core_schema) => Ok(RoverStdout::CoreSchema(core_schema)),
@@ -43,3 +60,200 @@ impl Compose {
4360
}
4461
}
4562
}
63+
64+
pub(crate) fn get_subgraph_definitions(
65+
supergraph_config: SupergraphConfig,
66+
config_path: &Utf8PathBuf,
67+
client_config: StudioClientConfig,
68+
profile_name: &str,
69+
) -> Result<Vec<SubgraphDefinition>> {
70+
let mut subgraphs = Vec::new();
71+
72+
for (subgraph_name, subgraph_data) in &supergraph_config.subgraphs {
73+
match &subgraph_data.schema {
74+
SchemaSource::File { file } => {
75+
let relative_schema_path = match config_path.parent() {
76+
Some(parent) => {
77+
let mut schema_path = parent.to_path_buf();
78+
schema_path.push(file);
79+
schema_path
80+
}
81+
None => file.clone(),
82+
};
83+
84+
let schema = fs::read_to_string(&relative_schema_path).map_err(|e| {
85+
let err = anyhow!("Could not read \"{}\": {}", &relative_schema_path, e);
86+
let mut err = RoverError::new(err);
87+
err.set_suggestion(Suggestion::ValidComposeFile);
88+
err
89+
})?;
90+
91+
let url = &subgraph_data.routing_url.clone().ok_or_else(|| {
92+
let err = anyhow!("No routing_url found for schema file.");
93+
let mut err = RoverError::new(err);
94+
err.set_suggestion(Suggestion::ValidComposeRoutingUrl);
95+
err
96+
})?;
97+
98+
let subgraph_definition = SubgraphDefinition::new(subgraph_name, url, &schema);
99+
subgraphs.push(subgraph_definition);
100+
}
101+
SchemaSource::SubgraphIntrospection { subgraph_url } => {
102+
// given a federated introspection URL, use subgraph introspect to
103+
// obtain SDL and add it to subgraph_definition.
104+
let client = Client::new(&subgraph_url.to_string());
105+
106+
let introspection_response = introspect::run(&client, &HashMap::new())?;
107+
let schema = introspection_response.result;
108+
109+
// We don't require a routing_url for this variant of a schema,
110+
// if none are provided, just use an empty string.
111+
let url = &subgraph_data
112+
.routing_url
113+
.clone()
114+
.unwrap_or_else(|| subgraph_url.to_string());
115+
116+
let subgraph_definition = SubgraphDefinition::new(subgraph_name, url, &schema);
117+
subgraphs.push(subgraph_definition);
118+
}
119+
SchemaSource::Subgraph { graphref, subgraph } => {
120+
// given a graphref and subgraph, run subgraph fetch to
121+
// obtain SDL and add it to subgraph_definition.
122+
let client = client_config.get_client(&profile_name)?;
123+
let graphref = parse_graph_ref(graphref)?;
124+
let schema = fetch::run(
125+
fetch::fetch_subgraph_query::Variables {
126+
graph_id: graphref.name.clone(),
127+
variant: graphref.variant.clone(),
128+
},
129+
&client,
130+
subgraph,
131+
)?;
132+
133+
// We don't require a routing_url for this variant of a schema,
134+
// if none are provided, just use an empty string.
135+
//
136+
// TODO: this should eventually get the url from the registry
137+
// and use that when no routing_url is provided.
138+
let url = &subgraph_data.routing_url.clone().unwrap_or_default();
139+
140+
let subgraph_definition = SubgraphDefinition::new(subgraph_name, url, &schema);
141+
subgraphs.push(subgraph_definition);
142+
}
143+
}
144+
}
145+
146+
Ok(subgraphs)
147+
}
148+
149+
#[cfg(test)]
150+
mod tests {
151+
use super::*;
152+
use assert_fs::TempDir;
153+
use houston as houston_config;
154+
use houston_config::Config;
155+
use std::convert::TryFrom;
156+
157+
fn get_studio_config() -> StudioClientConfig {
158+
let tmp_home = TempDir::new().unwrap();
159+
let tmp_path = Utf8PathBuf::try_from(tmp_home.path().to_path_buf()).unwrap();
160+
StudioClientConfig::new(None, Config::new(Some(&tmp_path), None).unwrap())
161+
}
162+
163+
#[test]
164+
fn it_errs_on_invalid_subgraph_path() {
165+
let raw_good_yaml = r#"subgraphs:
166+
films:
167+
routing_url: https://films.example.com
168+
schema:
169+
file: ./films-do-not-exist.graphql
170+
people:
171+
routing_url: https://people.example.com
172+
schema:
173+
file: ./people-do-not-exist.graphql"#;
174+
let tmp_home = TempDir::new().unwrap();
175+
let mut config_path = Utf8PathBuf::try_from(tmp_home.path().to_path_buf()).unwrap();
176+
config_path.push("config.yaml");
177+
fs::write(&config_path, raw_good_yaml).unwrap();
178+
let supergraph_config = config::parse_supergraph_config(&config_path).unwrap();
179+
assert!(get_subgraph_definitions(
180+
supergraph_config,
181+
&config_path,
182+
get_studio_config(),
183+
"profile"
184+
)
185+
.is_err())
186+
}
187+
188+
#[test]
189+
fn it_can_get_subgraph_definitions_from_fs() {
190+
let raw_good_yaml = r#"subgraphs:
191+
films:
192+
routing_url: https://films.example.com
193+
schema:
194+
file: ./films.graphql
195+
people:
196+
routing_url: https://people.example.com
197+
schema:
198+
file: ./people.graphql"#;
199+
let tmp_home = TempDir::new().unwrap();
200+
let mut config_path = Utf8PathBuf::try_from(tmp_home.path().to_path_buf()).unwrap();
201+
config_path.push("config.yaml");
202+
fs::write(&config_path, raw_good_yaml).unwrap();
203+
let tmp_dir = config_path.parent().unwrap().to_path_buf();
204+
let films_path = tmp_dir.join("films.graphql");
205+
let people_path = tmp_dir.join("people.graphql");
206+
fs::write(films_path, "there is something here").unwrap();
207+
fs::write(people_path, "there is also something here").unwrap();
208+
let supergraph_config = config::parse_supergraph_config(&config_path).unwrap();
209+
assert!(get_subgraph_definitions(
210+
supergraph_config,
211+
&config_path,
212+
get_studio_config(),
213+
"profile"
214+
)
215+
.is_ok())
216+
}
217+
218+
#[test]
219+
fn it_can_compute_relative_schema_paths() {
220+
let raw_good_yaml = r#"subgraphs:
221+
films:
222+
routing_url: https://films.example.com
223+
schema:
224+
file: ../../films.graphql
225+
people:
226+
routing_url: https://people.example.com
227+
schema:
228+
file: ../../people.graphql"#;
229+
let tmp_home = TempDir::new().unwrap();
230+
let tmp_dir = Utf8PathBuf::try_from(tmp_home.path().to_path_buf()).unwrap();
231+
let mut config_path = tmp_dir.clone();
232+
config_path.push("layer");
233+
config_path.push("layer");
234+
fs::create_dir_all(&config_path).unwrap();
235+
config_path.push("config.yaml");
236+
fs::write(&config_path, raw_good_yaml).unwrap();
237+
let films_path = tmp_dir.join("films.graphql");
238+
let people_path = tmp_dir.join("people.graphql");
239+
fs::write(films_path, "there is something here").unwrap();
240+
fs::write(people_path, "there is also something here").unwrap();
241+
let supergraph_config = config::parse_supergraph_config(&config_path).unwrap();
242+
let subgraph_definitions = get_subgraph_definitions(
243+
supergraph_config,
244+
&config_path,
245+
get_studio_config(),
246+
"profile",
247+
)
248+
.unwrap();
249+
let film_subgraph = subgraph_definitions.get(0).unwrap();
250+
let people_subgraph = subgraph_definitions.get(1).unwrap();
251+
252+
assert_eq!(film_subgraph.name, "films");
253+
assert_eq!(film_subgraph.url, "https://films.example.com");
254+
assert_eq!(film_subgraph.type_defs, "there is something here");
255+
assert_eq!(people_subgraph.name, "people");
256+
assert_eq!(people_subgraph.url, "https://people.example.com");
257+
assert_eq!(people_subgraph.type_defs, "there is also something here");
258+
}
259+
}

0 commit comments

Comments
 (0)