From 9a27a304a124e6635cebb22ea3b5a7f16e5fddf6 Mon Sep 17 00:00:00 2001 From: Avery Harnish Date: Mon, 14 Dec 2020 10:36:48 -0600 Subject: [PATCH] feat(rover): subgraph checks --- .../src/query/subgraph/check.graphql | 36 ++++++ .../rover-client/src/query/subgraph/check.rs | 79 +++++++++++++ crates/rover-client/src/query/subgraph/mod.rs | 2 + src/command/subgraph/check.rs | 109 ++++++++++++++++++ src/command/subgraph/mod.rs | 6 + 5 files changed, 232 insertions(+) create mode 100644 crates/rover-client/src/query/subgraph/check.graphql create mode 100644 crates/rover-client/src/query/subgraph/check.rs create mode 100644 src/command/subgraph/check.rs diff --git a/crates/rover-client/src/query/subgraph/check.graphql b/crates/rover-client/src/query/subgraph/check.graphql new file mode 100644 index 0000000000..ce16dd8dde --- /dev/null +++ b/crates/rover-client/src/query/subgraph/check.graphql @@ -0,0 +1,36 @@ + mutation CheckPartialSchemaQuery ( + $graph_id: ID! + $variant: String! + $implementingServiceName: String! + $partialSchema: PartialSchemaInput! + ) { + service(id: $graph_id) { + checkPartialSchema( + graphVariant: $variant + implementingServiceName: $implementingServiceName + partialSchema: $partialSchema + ) { + compositionValidationResult { + compositionValidationDetails { + schemaHash + } + graphCompositionID + errors { + message + } + } + checkSchemaResult { + diffToPrevious { + severity + numberOfCheckedOperations + changes { + severity + code + description + } + } + targetUrl + } + } + } + } \ No newline at end of file diff --git a/crates/rover-client/src/query/subgraph/check.rs b/crates/rover-client/src/query/subgraph/check.rs new file mode 100644 index 0000000000..be87563295 --- /dev/null +++ b/crates/rover-client/src/query/subgraph/check.rs @@ -0,0 +1,79 @@ +use crate::blocking::StudioClient; +use crate::RoverClientError; +use graphql_client::*; + +use reqwest::Url; + +#[derive(GraphQLQuery)] +// The paths are relative to the directory where your `Cargo.toml` is located. +// Both json and the GraphQL schema language are supported as sources for the schema +#[graphql( + query_path = "src/query/subgraph/check.graphql", + schema_path = ".schema/schema.graphql", + response_derives = "PartialEq, Debug, Serialize, Deserialize", + deprecated = "warn" +)] +/// This struct is used to generate the module containing `Variables` and +/// `ResponseData` structs. +/// Snake case of this name is the mod name. i.e. check_partial_schema_query +pub struct CheckPartialSchemaQuery; + +/// The main function to be used from this module. +/// This function takes a proposed schema and validates it against a pushed +/// schema. +pub fn run( + variables: check_partial_schema_query::Variables, + client: &StudioClient, +) -> Result { + let data = client.post::(variables)?; + get_check_response_from_data(data) +} + +#[derive(Debug)] +pub struct CheckResponse { + pub target_url: Option, + pub number_of_checked_operations: i64, + pub change_severity: check_partial_schema_query::ChangeSeverity, + pub changes: Vec +} + +fn get_check_response_from_data( + data: check_partial_schema_query::ResponseData, +) -> Result { + let service = data.service.ok_or(RoverClientError::NoService)?; + + // TODO: fix this error + let check_schema_result = service + .check_partial_schema + .check_schema_result + .ok_or(RoverClientError::NoData)?; + let target_url = get_url(check_schema_result.target_url); + + let diff_to_previous = check_schema_result.diff_to_previous; + + let number_of_checked_operations = diff_to_previous.number_of_checked_operations.unwrap_or(0); + + let change_severity = diff_to_previous.severity; + let changes = diff_to_previous.changes; + + Ok(CheckResponse { + target_url, + number_of_checked_operations, + change_severity, + changes, + }) +} + +fn get_url(url: Option) -> Option { + match url { + Some(url) => { + let url = Url::parse(&url); + match url { + Ok(url) => Some(url), + // if the API returns an invalid URL, don't put it in the response + Err(_) => None, + } + } + None => None, + } +} diff --git a/crates/rover-client/src/query/subgraph/mod.rs b/crates/rover-client/src/query/subgraph/mod.rs index 2ed4f587d6..32119da5be 100644 --- a/crates/rover-client/src/query/subgraph/mod.rs +++ b/crates/rover-client/src/query/subgraph/mod.rs @@ -4,3 +4,5 @@ pub mod push; /// "subgraph delete" command execution pub mod delete; +/// "subgraph check" command execution +pub mod check; diff --git a/src/command/subgraph/check.rs b/src/command/subgraph/check.rs new file mode 100644 index 0000000000..dca2964729 --- /dev/null +++ b/src/command/subgraph/check.rs @@ -0,0 +1,109 @@ +use std::path::PathBuf; + +use anyhow::{Context, Result}; +use prettytable::{cell, row, Table}; +use serde::Serialize; +use structopt::StructOpt; + +use rover_client::query::subgraph::check; + +use crate::client::get_studio_client; +use crate::command::RoverStdout; +use crate::utils::parsers::{parse_graph_ref, GraphRef}; + +#[derive(Debug, Serialize, StructOpt)] +pub struct Check { + /// @ of graph in Apollo Studio to validate. + /// @ may be left off, defaulting to @current + #[structopt(name = "GRAPH_REF", parse(try_from_str = parse_graph_ref))] + #[serde(skip_serializing)] + graph: GraphRef, + + /// Name of the implementing service to validate + #[structopt(long = "service", required = true)] + #[serde(skip_serializing)] + service_name: String, + + /// Name of configuration profile to use + #[structopt(long = "profile", default_value = "default")] + #[serde(skip_serializing)] + profile_name: String, + + /// Path of .graphql/.gql schema file to push + #[structopt(long = "schema", short = "s")] + #[serde(skip_serializing)] + schema_path: PathBuf, +} + +impl Check { + pub fn run(&self) -> Result { + let client = + get_studio_client(&self.profile_name).context("Failed to get studio client")?; + let sdl = std::fs::read_to_string(&self.schema_path) + .with_context(|| format!("Could not read file `{}`", &self.schema_path.display()))?; + let partial_schema = check::check_partial_schema_query::PartialSchemaInput { + sdl: Some(sdl), + hash: None, + }; + let res = check::run( + check::check_partial_schema_query::Variables { + graph_id: self.graph.name.clone(), + variant: self.graph.variant.clone(), + partial_schema, + implementing_service_name: self.service_name.clone(), + }, + &client, + ) + .context("Failed to validate schema")?; + + tracing::info!( + "Validated schema against metrics from variant {} on subgraph {}", + &self.graph.variant, + &self.graph.name + ); + tracing::info!( + "Compared {} schema changes against {} operations", + res.changes.len(), + res.number_of_checked_operations + ); + + if let Some(url) = res.target_url { + tracing::info!("View full details here"); + tracing::info!("{}", url.to_string()); + } + + let num_failures = print_changes(&res.changes); + + match num_failures { + 0 => Ok(RoverStdout::None), + 1 => Err(anyhow::anyhow!("Encountered 1 failure.")), + _ => Err(anyhow::anyhow!("Encountered {} failures.", num_failures)), + } + } +} + +fn print_changes( + checks: &[check::check_partial_schema_query::CheckPartialSchemaQueryServiceCheckPartialSchemaCheckSchemaResultDiffToPreviousChanges], +) -> u64 { + let mut num_failures = 0; + + if checks.is_empty() { + let mut table = Table::new(); + table.add_row(row!["Change", "Code", "Description"]); + for check in checks { + let change = match check.severity { + check::check_partial_schema_query::ChangeSeverity::NOTICE => "PASS", + check::check_partial_schema_query::ChangeSeverity::FAILURE => { + num_failures += 1; + "FAIL" + } + _ => unreachable!("Unknown change severity"), + }; + table.add_row(row![change, check.code, check.description]); + } + + eprintln!("{}", table); + } + + num_failures +} diff --git a/src/command/subgraph/mod.rs b/src/command/subgraph/mod.rs index 7c992878ec..be292ff355 100644 --- a/src/command/subgraph/mod.rs +++ b/src/command/subgraph/mod.rs @@ -1,3 +1,4 @@ +mod check; mod delete; mod push; @@ -17,8 +18,12 @@ pub struct Subgraph { pub enum Command { /// Push an implementing service schema from a local file Push(push::Push), + /// Delete an implementing service and trigger composition Delete(delete::Delete), + + /// Validate changes to an implementing service + Check(check::Check), } impl Subgraph { @@ -26,6 +31,7 @@ impl Subgraph { match &self.command { Command::Push(command) => command.run(), Command::Delete(command) => command.run(), + Command::Check(command) => command.run(), } } }