diff --git a/crates/rover-client/src/operations/cloud/config/mod.rs b/crates/rover-client/src/operations/cloud/config/mod.rs index a79378c74..f56f25bb9 100644 --- a/crates/rover-client/src/operations/cloud/config/mod.rs +++ b/crates/rover-client/src/operations/cloud/config/mod.rs @@ -1,6 +1,6 @@ pub mod fetch; pub mod types; pub mod update; +pub mod validate; -pub use fetch::run; pub use types::*; diff --git a/crates/rover-client/src/operations/cloud/config/types.rs b/crates/rover-client/src/operations/cloud/config/types.rs index 1912066c5..973cb6d2e 100644 --- a/crates/rover-client/src/operations/cloud/config/types.rs +++ b/crates/rover-client/src/operations/cloud/config/types.rs @@ -1,9 +1,13 @@ use crate::operations::cloud::config::fetch::cloud_config_fetch_query; use crate::operations::cloud::config::update::cloud_config_update_query; +use crate::operations::cloud::config::validate::cloud_config_validate_query::{ + self, RouterConfigInput, +}; use crate::shared::GraphRef; type FetchQueryVariables = cloud_config_fetch_query::Variables; type UpdateQueryVariables = cloud_config_update_query::Variables; +type ValidateQueryVariables = cloud_config_validate_query::Variables; #[derive(Debug, Clone, Eq, PartialEq)] pub struct CloudConfigFetchInput { @@ -40,3 +44,28 @@ impl From for UpdateQueryVariables { } } } + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct CloudConfigValidateInput { + pub graph_ref: GraphRef, + pub config: String, +} + +impl From for ValidateQueryVariables { + fn from(input: CloudConfigValidateInput) -> Self { + Self { + ref_: input.graph_ref.to_string(), + config: RouterConfigInput { + gcus: None, + graph_composition_id: None, + router_config: Some(input.config), + router_version: None, + }, + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct CloudConfigValidateResponse { + pub msg: String, +} diff --git a/crates/rover-client/src/operations/cloud/config/update_query.graphql b/crates/rover-client/src/operations/cloud/config/update_query.graphql index 26093f955..84b33c816 100644 --- a/crates/rover-client/src/operations/cloud/config/update_query.graphql +++ b/crates/rover-client/src/operations/cloud/config/update_query.graphql @@ -3,7 +3,7 @@ mutation CloudConfigUpdateQuery($graph_id: ID!, $variant: String!, $config: Stri variant(name: $variant) { upsertRouterConfig(configuration: $config) { __typename - ... on RouterUpsertFailure { + ... on RouterUpsertFailure { message } ... on GraphVariant { diff --git a/crates/rover-client/src/operations/cloud/config/validate.rs b/crates/rover-client/src/operations/cloud/config/validate.rs new file mode 100644 index 000000000..ebf6a3f52 --- /dev/null +++ b/crates/rover-client/src/operations/cloud/config/validate.rs @@ -0,0 +1,150 @@ +use super::types::{CloudConfigValidateInput, CloudConfigValidateResponse}; + +use graphql_client::*; + +use crate::blocking::StudioClient; +use crate::operations::cloud::config::validate::cloud_config_validate_query::{ + CloudConfigValidateQueryVariant::GraphVariant, + CloudConfigValidateQueryVariantOnGraphVariantValidateRouter::{ + CloudValidationSuccess, InternalServerError, InvalidInputErrors, + }, +}; +use crate::shared::GraphRef; +use crate::RoverClientError; + +#[derive(GraphQLQuery, Debug)] +// 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/operations/cloud/config/validate_query.graphql", + schema_path = ".schema/schema.graphql", + response_derives = "Eq, PartialEq, Debug, Serialize, Deserialize", + deprecated = "warn" +)] +pub struct CloudConfigValidateQuery; + +pub async fn run( + input: CloudConfigValidateInput, + client: &StudioClient, +) -> Result { + let graph_ref = input.graph_ref.clone(); + let data = client + .post::(input.into()) + .await?; + build_response(graph_ref, data) +} + +fn build_response( + graph_ref: GraphRef, + data: cloud_config_validate_query::ResponseData, +) -> Result { + let graph_variant = match data.variant { + Some(GraphVariant(gv)) => gv, + _ => { + return Err(RoverClientError::GraphNotFound { + graph_ref: graph_ref.clone(), + }) + } + }; + + match graph_variant.validate_router { + CloudValidationSuccess(res) => Ok(CloudConfigValidateResponse { msg: res.message }), + InvalidInputErrors(e) => Err(RoverClientError::InvalidRouterConfig { msg: e.message }), + InternalServerError(e) => Err(RoverClientError::ClientError { msg: e.message }), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::shared::GraphRef; + use pretty_assertions::assert_eq; + use serde_json::json; + + fn mock_graph_ref() -> GraphRef { + GraphRef { + name: "mygraph".to_string(), + variant: "current".to_string(), + } + } + + #[test] + fn test_build_response_success() { + let json_response = json!({ + "variant": { + "__typename": "GraphVariant", + "validateRouter": { + "__typename": "CloudValidationSuccess", + "message": "No errors!" + } + } + }); + let data = serde_json::from_value(json_response).unwrap(); + let output = build_response(mock_graph_ref(), data); + + let expected = CloudConfigValidateResponse { + msg: "No errors!".to_string(), + }; + assert!(output.is_ok()); + assert_eq!(output.unwrap(), expected); + } + + #[test] + fn test_build_response_errs_with_no_variant() { + let json_response = json!({ + "variant": null + }); + let data = serde_json::from_value(json_response).unwrap(); + let output = build_response(mock_graph_ref(), data); + + match output.err() { + Some(RoverClientError::GraphNotFound { .. }) => {} + _ => panic!("expected graph not found error"), + } + } + + #[test] + fn test_build_response_errs_invalid_input() { + let json_response = json!({ + "variant": { + "__typename": "GraphVariant", + "validateRouter": { + "__typename": "InvalidInputErrors", + "errors": [], + "message": "Invalid config" + } + } + }); + let data = serde_json::from_value(json_response).unwrap(); + let output = build_response(mock_graph_ref(), data); + + match output.err() { + Some(RoverClientError::InvalidRouterConfig { msg }) => { + assert_eq!("Invalid config".to_string(), msg) + } + _ => panic!("expected invalid router config error"), + } + } + + #[test] + fn test_build_response_errs_internal_server_error() { + let json_response = json!({ + "variant": { + "__typename": "GraphVariant", + "validateRouter": { + "__typename": "InternalServerError", + "message": "Client error" + } + } + }); + let data = serde_json::from_value(json_response).unwrap(); + let output = build_response(mock_graph_ref(), data); + + match output.err() { + Some(RoverClientError::ClientError { msg }) => { + assert_eq!("Client error".to_string(), msg) + } + _ => panic!("expected client error"), + } + } +} diff --git a/crates/rover-client/src/operations/cloud/config/validate_query.graphql b/crates/rover-client/src/operations/cloud/config/validate_query.graphql new file mode 100644 index 000000000..0ec968a7f --- /dev/null +++ b/crates/rover-client/src/operations/cloud/config/validate_query.graphql @@ -0,0 +1,24 @@ +query CloudConfigValidateQuery($config: RouterConfigInput!, $ref: ID!) { + variant(ref: $ref) { + __typename + ... on GraphVariant { + validateRouter(config: $config) { + __typename + ... on CloudValidationSuccess { + message + } + ... on InvalidInputErrors { + errors { + argument + location + reason + } + message + } + ... on InternalServerError { + message + } + } + } + } +} diff --git a/src/command/cloud/config.rs b/src/command/cloud/config.rs index 12691c4e3..72e46d954 100644 --- a/src/command/cloud/config.rs +++ b/src/command/cloud/config.rs @@ -1,5 +1,4 @@ use clap::Parser; -use rover_client::operations::cloud::config::CloudConfigUpdateInput; use serde::Serialize; use crate::options::{FileOpt, GraphRefOpt, ProfileOpt}; @@ -7,7 +6,11 @@ use crate::utils::client::StudioClientConfig; use crate::{RoverOutput, RoverResult}; use rover_client::blocking::StudioClient; -use rover_client::operations::cloud::config::{fetch, types::CloudConfigFetchInput, update}; +use rover_client::operations::cloud::config::{ + fetch, + types::{CloudConfigFetchInput, CloudConfigUpdateInput, CloudConfigValidateInput}, + update, validate, +}; #[derive(Debug, Serialize, Parser)] pub struct Config { @@ -17,11 +20,14 @@ pub struct Config { #[derive(Debug, Serialize, Parser)] pub enum Command { - /// Get current config for a given graph ref + /// Get current cloud router config for a given graph ref Fetch(Fetch), - /// Update current config for a given graph ref + /// Update current cloud router config for a given graph ref Update(Update), + + /// Validate a cloud router config for a given graph ref + Validate(Update), } #[derive(Debug, Serialize, Parser)] @@ -57,6 +63,10 @@ impl Config { let client = client_config.get_authenticated_client(&args.profile)?; self.update(client, &args.graph, &args.file).await } + Command::Validate(args) => { + let client = client_config.get_authenticated_client(&args.profile)?; + self.validate(client, &args.graph, &args.file).await + } } } @@ -65,7 +75,7 @@ impl Config { client: StudioClient, graph: &GraphRefOpt, ) -> RoverResult { - eprintln!("Fetching cloud config for: {}", graph.graph_ref); + eprintln!("Fetching cloud router config for: {}", graph.graph_ref); let cloud_config = fetch::run( CloudConfigFetchInput { @@ -76,7 +86,6 @@ impl Config { .await?; Ok(RoverOutput::CloudConfigFetchResponse { - graph_ref: cloud_config.graph_ref, config: cloud_config.config, }) } @@ -87,7 +96,7 @@ impl Config { graph: &GraphRefOpt, file: &FileOpt, ) -> RoverResult { - println!("Updating cloud config for: {}", graph.graph_ref); + eprintln!("Updating cloud router config for: {}", graph.graph_ref); let config = file.read_file_descriptor("Cloud Router config", &mut std::io::stdin())?; @@ -102,4 +111,28 @@ impl Config { Ok(RoverOutput::EmptySuccess) } + + pub async fn validate( + &self, + client: StudioClient, + graph: &GraphRefOpt, + file: &FileOpt, + ) -> RoverResult { + eprintln!("Validating cloud router config for: {}", graph.graph_ref); + + let config = file.read_file_descriptor("Cloud Router config", &mut std::io::stdin())?; + + let validation = validate::run( + CloudConfigValidateInput { + graph_ref: graph.graph_ref.clone(), + config, + }, + &client, + ) + .await?; + + Ok(RoverOutput::MessageResponse { + msg: validation.msg, + }) + } } diff --git a/src/command/output.rs b/src/command/output.rs index 13d19bc61..87e53b0ee 100644 --- a/src/command/output.rs +++ b/src/command/output.rs @@ -98,9 +98,11 @@ pub enum RoverOutput { }, EmptySuccess, CloudConfigFetchResponse { - graph_ref: GraphRef, config: String, }, + MessageResponse { + msg: String, + }, } impl RoverOutput { @@ -447,10 +449,8 @@ impl RoverOutput { Some(jwt.to_string()) } RoverOutput::EmptySuccess => None, - RoverOutput::CloudConfigFetchResponse { - graph_ref: _, - config, - } => Some(config.to_string()), + RoverOutput::CloudConfigFetchResponse { config } => Some(config.to_string()), + RoverOutput::MessageResponse { msg } => Some(msg.into()), }) } @@ -569,12 +569,12 @@ impl RoverOutput { RoverOutput::LicenseResponse { jwt, .. } => { json!({"jwt": jwt }) } - RoverOutput::CloudConfigFetchResponse { - graph_ref: _, - config, - } => { + RoverOutput::CloudConfigFetchResponse { config } => { json!({ "config": config }) } + RoverOutput::MessageResponse { msg } => { + json!({ "message": msg }) + } } }