Skip to content

Commit

Permalink
feat(rover): compose from running services, registered graphs or subg…
Browse files Browse the repository at this point in the history
…raphs (#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]>
  • Loading branch information
lrlna authored May 11, 2021
1 parent 182240c commit 5995c16
Show file tree
Hide file tree
Showing 13 changed files with 326 additions and 132 deletions.
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,30 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
[EverlastingBugstopper]: https://github.com/EverlastingBugstopper
[pull/492]: https://github.com/apollographql/rover/pull/492

- **`rover supergraph compose` allows for registry and introspection SDL sources - [lrlna], [issue/449] [pull/519]**

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`):

```yaml
subgraphs:
films:
routing_url: https://films.example.com
schema:
file: ./films.graphql
people:
routing_url: https://example.com/people
schema:
subgraph_url: https://example.com/people
actors:
routing_url: https://localhost:4005
schema:
graphref: mygraph@current
subgraph: actors
```
[lrlna]: https://github.com/lrlna
[issue/449]: https://github.com/apollographql/rover/issues/449
[pull/519]: https://github.com/apollographql/rover/pull/519
- **`--routing-url` is now an optional argument to `rover subgraph publish` - [EverlastingBusgtopper], [issue/169] [pull/484]**

When publishing a subgraph, it is important to include a routing URL for that subgraph, so your graph router
Expand Down
7 changes: 7 additions & 0 deletions docs/source/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,3 +227,10 @@ This error occurs when working with a federated graph and its subgraphs. When gr
To resolve this error, inspect the printed errors and correct the subgraph schemas.


### E028

This error occurs when a connection could not be established with to an introspection endpoint.

To resolve this problem, make sure the endpoint URL is correct. You may wish to run the command again with `--log=debug`.


21 changes: 20 additions & 1 deletion docs/source/supergraphs.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,26 @@ subgraphs:
file: ./people.graphql
```
The YAML file must specify each subgraph's public-facing URL (`routing_url`), along with the path to its schema (`schema.file`).
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`).

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`):

```yaml
subgraphs:
films:
routing_url: https://films.example.com
schema:
file: ./films.graphql
people:
routing_url: https://example.com/people
schema:
subgraph_url: https://example.com/people
actors:
routing_url: https://localhost:4005
schema:
graphref: mygraph@current
subgraph: actors
```

### Output format

Expand Down
6 changes: 3 additions & 3 deletions installers/binstall/src/system/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ fn get_windows_path_var() -> Result<Option<String>, InstallerError> {
}
}
Err(ref e) if e.kind() == io::ErrorKind::NotFound => Ok(Some(String::new())),
Err(e) => Err(e)?,
Err(e) => Err(e.into()),
}
}

Expand Down Expand Up @@ -83,7 +83,7 @@ fn add_to_path(old_path: &str, path_str: &str) -> Option<String> {
None
} else {
let mut new_path = path_str.to_string();
new_path.push_str(";");
new_path.push(';');
new_path.push_str(&old_path);
Some(new_path)
}
Expand Down Expand Up @@ -116,7 +116,7 @@ fn apply_new_path(new_path: &str) -> Result<(), InstallerError> {
SendMessageTimeoutA(
HWND_BROADCAST,
WM_SETTINGCHANGE,
0 as WPARAM,
0_usize,
"Environment\0".as_ptr() as LPARAM,
SMTO_ABORTIFHUNG,
5000,
Expand Down
2 changes: 1 addition & 1 deletion installers/npm/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion installers/npm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,6 @@
"console.table": "^0.10.0"
},
"devDependencies": {
"prettier": "^2.2.1"
"prettier": "^2.3.0"
}
}
222 changes: 218 additions & 4 deletions src/command/supergraph/compose.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,41 @@
use crate::{anyhow, command::RoverStdout, Result};
use crate::utils::{client::StudioClientConfig, parsers::parse_graph_ref};
use crate::{anyhow, command::RoverStdout, error::RoverError, Result, Suggestion};

use ansi_term::Colour::Red;
use camino::Utf8PathBuf;
use harmonizer::ServiceDefinition as SubgraphDefinition;
use rover_client::{
blocking::Client,
query::subgraph::{fetch, introspect},
};
use serde::Serialize;
use std::{collections::HashMap, fs};
use structopt::StructOpt;

use super::config;
use super::config::{self, SchemaSource, SupergraphConfig};

#[derive(Debug, Serialize, StructOpt)]
pub struct Compose {
/// The relative path to the supergraph configuration file.
#[structopt(long = "config")]
#[serde(skip_serializing)]
config_path: Utf8PathBuf,

/// Name of configuration profile to use
#[structopt(long = "profile", default_value = "default")]
#[serde(skip_serializing)]
profile_name: String,
}

impl Compose {
pub fn run(&self) -> Result<RoverStdout> {
pub fn run(&self, client_config: StudioClientConfig) -> Result<RoverStdout> {
let supergraph_config = config::parse_supergraph_config(&self.config_path)?;
let subgraph_definitions = supergraph_config.get_subgraph_definitions(&self.config_path)?;
let subgraph_definitions = get_subgraph_definitions(
supergraph_config,
&self.config_path,
client_config,
&self.profile_name,
)?;

match harmonizer::harmonize(subgraph_definitions) {
Ok(core_schema) => Ok(RoverStdout::CoreSchema(core_schema)),
Expand All @@ -43,3 +60,200 @@ impl Compose {
}
}
}

pub(crate) fn get_subgraph_definitions(
supergraph_config: SupergraphConfig,
config_path: &Utf8PathBuf,
client_config: StudioClientConfig,
profile_name: &str,
) -> Result<Vec<SubgraphDefinition>> {
let mut subgraphs = Vec::new();

for (subgraph_name, subgraph_data) in &supergraph_config.subgraphs {
match &subgraph_data.schema {
SchemaSource::File { file } => {
let relative_schema_path = match config_path.parent() {
Some(parent) => {
let mut schema_path = parent.to_path_buf();
schema_path.push(file);
schema_path
}
None => file.clone(),
};

let schema = fs::read_to_string(&relative_schema_path).map_err(|e| {
let err = anyhow!("Could not read \"{}\": {}", &relative_schema_path, e);
let mut err = RoverError::new(err);
err.set_suggestion(Suggestion::ValidComposeFile);
err
})?;

let url = &subgraph_data.routing_url.clone().ok_or_else(|| {
let err = anyhow!("No routing_url found for schema file.");
let mut err = RoverError::new(err);
err.set_suggestion(Suggestion::ValidComposeRoutingUrl);
err
})?;

let subgraph_definition = SubgraphDefinition::new(subgraph_name, url, &schema);
subgraphs.push(subgraph_definition);
}
SchemaSource::SubgraphIntrospection { subgraph_url } => {
// given a federated introspection URL, use subgraph introspect to
// obtain SDL and add it to subgraph_definition.
let client = Client::new(&subgraph_url.to_string());

let introspection_response = introspect::run(&client, &HashMap::new())?;
let schema = introspection_response.result;

// We don't require a routing_url for this variant of a schema,
// if none are provided, just use an empty string.
let url = &subgraph_data
.routing_url
.clone()
.unwrap_or_else(|| subgraph_url.to_string());

let subgraph_definition = SubgraphDefinition::new(subgraph_name, url, &schema);
subgraphs.push(subgraph_definition);
}
SchemaSource::Subgraph { graphref, subgraph } => {
// given a graphref and subgraph, run subgraph fetch to
// obtain SDL and add it to subgraph_definition.
let client = client_config.get_client(&profile_name)?;
let graphref = parse_graph_ref(graphref)?;
let schema = fetch::run(
fetch::fetch_subgraph_query::Variables {
graph_id: graphref.name.clone(),
variant: graphref.variant.clone(),
},
&client,
subgraph,
)?;

// We don't require a routing_url for this variant of a schema,
// if none are provided, just use an empty string.
//
// TODO: this should eventually get the url from the registry
// and use that when no routing_url is provided.
let url = &subgraph_data.routing_url.clone().unwrap_or_default();

let subgraph_definition = SubgraphDefinition::new(subgraph_name, url, &schema);
subgraphs.push(subgraph_definition);
}
}
}

Ok(subgraphs)
}

#[cfg(test)]
mod tests {
use super::*;
use assert_fs::TempDir;
use houston as houston_config;
use houston_config::Config;
use std::convert::TryFrom;

fn get_studio_config() -> StudioClientConfig {
let tmp_home = TempDir::new().unwrap();
let tmp_path = Utf8PathBuf::try_from(tmp_home.path().to_path_buf()).unwrap();
StudioClientConfig::new(None, Config::new(Some(&tmp_path), None).unwrap())
}

#[test]
fn it_errs_on_invalid_subgraph_path() {
let raw_good_yaml = r#"subgraphs:
films:
routing_url: https://films.example.com
schema:
file: ./films-do-not-exist.graphql
people:
routing_url: https://people.example.com
schema:
file: ./people-do-not-exist.graphql"#;
let tmp_home = TempDir::new().unwrap();
let mut config_path = Utf8PathBuf::try_from(tmp_home.path().to_path_buf()).unwrap();
config_path.push("config.yaml");
fs::write(&config_path, raw_good_yaml).unwrap();
let supergraph_config = config::parse_supergraph_config(&config_path).unwrap();
assert!(get_subgraph_definitions(
supergraph_config,
&config_path,
get_studio_config(),
"profile"
)
.is_err())
}

#[test]
fn it_can_get_subgraph_definitions_from_fs() {
let raw_good_yaml = r#"subgraphs:
films:
routing_url: https://films.example.com
schema:
file: ./films.graphql
people:
routing_url: https://people.example.com
schema:
file: ./people.graphql"#;
let tmp_home = TempDir::new().unwrap();
let mut config_path = Utf8PathBuf::try_from(tmp_home.path().to_path_buf()).unwrap();
config_path.push("config.yaml");
fs::write(&config_path, raw_good_yaml).unwrap();
let tmp_dir = config_path.parent().unwrap().to_path_buf();
let films_path = tmp_dir.join("films.graphql");
let people_path = tmp_dir.join("people.graphql");
fs::write(films_path, "there is something here").unwrap();
fs::write(people_path, "there is also something here").unwrap();
let supergraph_config = config::parse_supergraph_config(&config_path).unwrap();
assert!(get_subgraph_definitions(
supergraph_config,
&config_path,
get_studio_config(),
"profile"
)
.is_ok())
}

#[test]
fn it_can_compute_relative_schema_paths() {
let raw_good_yaml = r#"subgraphs:
films:
routing_url: https://films.example.com
schema:
file: ../../films.graphql
people:
routing_url: https://people.example.com
schema:
file: ../../people.graphql"#;
let tmp_home = TempDir::new().unwrap();
let tmp_dir = Utf8PathBuf::try_from(tmp_home.path().to_path_buf()).unwrap();
let mut config_path = tmp_dir.clone();
config_path.push("layer");
config_path.push("layer");
fs::create_dir_all(&config_path).unwrap();
config_path.push("config.yaml");
fs::write(&config_path, raw_good_yaml).unwrap();
let films_path = tmp_dir.join("films.graphql");
let people_path = tmp_dir.join("people.graphql");
fs::write(films_path, "there is something here").unwrap();
fs::write(people_path, "there is also something here").unwrap();
let supergraph_config = config::parse_supergraph_config(&config_path).unwrap();
let subgraph_definitions = get_subgraph_definitions(
supergraph_config,
&config_path,
get_studio_config(),
"profile",
)
.unwrap();
let film_subgraph = subgraph_definitions.get(0).unwrap();
let people_subgraph = subgraph_definitions.get(1).unwrap();

assert_eq!(film_subgraph.name, "films");
assert_eq!(film_subgraph.url, "https://films.example.com");
assert_eq!(film_subgraph.type_defs, "there is something here");
assert_eq!(people_subgraph.name, "people");
assert_eq!(people_subgraph.url, "https://people.example.com");
assert_eq!(people_subgraph.type_defs, "there is also something here");
}
}
Loading

0 comments on commit 5995c16

Please sign in to comment.