Skip to content

Commit 2cb886a

Browse files
committed
Wire up rover subgraph preview, swap subgraph delete to use it
Adds `rover subgraph preview`: composes a preview supergraph from hypothetical subgraph changes (add/update/remove, via --subgraph-changes) with optional contract filters, without publishing anything. Same async/--build-id/poll UX as `rover contract preview`. Also swaps `rover subgraph delete`'s pre-confirmation build-error check from a synchronous removeImplementingServiceAndTriggerComposition(dryRun: true) call to the new async compose-and-filter-preview path (delete::check). Holding that dry-run mutation open for the duration of composition blocked the server connection and risked timing out on large supergraphs -- the same reason this whole feature is async. Bundled with this PR rather than the subgraph-impl PR because it's a breaking change to the existing SubgraphDeleteInput/runner shape that only this PR's CLI change consumes; splitting it further would leave an intermediate commit that doesn't build. Also brings the command's print calls up to date with the simplified rover-print Print::print signature that landed on main after this branch was first written.
1 parent fd2afdc commit 2cb886a

9 files changed

Lines changed: 832 additions & 23 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
3232

3333
`rover auth logout` revokes the OAuth session stored by `rover auth login` for the given `--profile` (or "default") — the access token and, if one was issued, the refresh token (RFC 7009) — then removes the local credential. Revocation is best-effort: if the OAuth server can't be reached, Rover still clears the local credential and warns instead of leaving you stuck "logged in" locally. Only meaningful for profiles logged in via `rover auth login`; running it against a profile holding a Personal API Key (from `rover config auth`) errors and points you at `rover config delete` instead. Only compiled in when built with `--features oauth`, matching `rover auth login`.
3434

35+
- **Add `rover subgraph preview` - @sirdodger**
36+
37+
`rover subgraph preview` composes a preview supergraph from hypothetical subgraph changes described by a `--subgraph-changes` YAML file (add/update a subgraph's schema or routing URL, or mark one `remove: true`), with optional include/exclude/hide-unreachable-types contract filters, without publishing anything. It runs asynchronously on the server; by default Rover polls until the build completes (or `APOLLO_CHECKS_TIMEOUT_SECONDS` elapses), or pass `--async` to just start the build and check on it later with `--build-id`. Exits non-zero if composition or filtering fails.
38+
39+
`rover subgraph delete`'s pre-confirmation build-error check now runs through this same async preview path instead of a synchronous dry-run mutation, avoiding a long-held server connection (and its timeout risk) while previewing the deletion of a subgraph from a large supergraph.
40+
3541
## 🐛 Fixes
3642

3743
- **Surface the underlying GraphQL errors when a response has no `data` field - @sirdodger**

crates/rover-client/src/operations/subgraph/delete/delete_mutation.graphql

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,11 @@ mutation SubgraphDeleteMutation(
22
$graph_id: ID!
33
$variant: String!
44
$subgraph: String!
5-
$dry_run: Boolean!
65
) {
76
graph(id: $graph_id) {
87
removeImplementingServiceAndTriggerComposition(
98
graphVariant: $variant
109
name: $subgraph
11-
dryRun: $dry_run
1210
) {
1311
errors {
1412
message
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
mod runner;
22
mod types;
33

4-
pub use runner::run;
4+
pub use runner::{check, run};
55
pub use types::{SubgraphDeleteInput, SubgraphDeleteResponse};

crates/rover-client/src/operations/subgraph/delete/runner.rs

Lines changed: 173 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,15 @@ use apollo_federation_types::rover::{BuildError, BuildErrors};
22
use graphql_client::*;
33
use rover_studio::types::GraphRef;
44

5-
use crate::{blocking::StudioClient, operations::subgraph::delete::types::*, RoverClientError};
5+
use crate::{
6+
blocking::StudioClient,
7+
operations::subgraph::{
8+
delete::types::*,
9+
preview::{self, ComposeAndFilterPreviewInput, SubgraphChange},
10+
},
11+
shared::AsyncBuildStatus,
12+
RoverClientError,
13+
};
614

715
#[derive(GraphQLQuery)]
816
// The paths are relative to the directory where your `Cargo.toml` is located.
@@ -30,6 +38,37 @@ pub async fn run(
3038
Ok(build_response(data))
3139
}
3240

41+
/// Preview the composition impact of deleting a subgraph, via the same async
42+
/// `composeAndFilterPreviewAsync` build `rover subgraph preview` uses.
43+
pub async fn check(
44+
input: SubgraphDeleteInput,
45+
client: &StudioClient,
46+
checks_timeout_seconds: u64,
47+
) -> Result<SubgraphDeleteResponse, RoverClientError> {
48+
let preview_response = preview::run(
49+
ComposeAndFilterPreviewInput {
50+
graph_ref: input.graph_ref,
51+
filter_config: None,
52+
subgraph_changes: vec![SubgraphChange {
53+
name: input.subgraph,
54+
info: None,
55+
}],
56+
},
57+
client,
58+
checks_timeout_seconds,
59+
)
60+
.await?;
61+
62+
Ok(SubgraphDeleteResponse {
63+
supergraph_was_updated: preview_response.status == AsyncBuildStatus::Success,
64+
build_errors: preview_response
65+
.errors
66+
.into_iter()
67+
.map(|message| BuildError::composition_error(None, Some(message), None, None))
68+
.collect(),
69+
})
70+
}
71+
3372
fn get_delete_data_from_response(
3473
response_data: subgraph_delete_mutation::ResponseData,
3574
graph_ref: GraphRef,
@@ -60,10 +99,143 @@ fn build_response(response: MutationComposition) -> SubgraphDeleteResponse {
6099

61100
#[cfg(test)]
62101
mod tests {
102+
use std::time::Duration;
103+
104+
use houston::{Credential, CredentialOrigin};
105+
use httpmock::prelude::*;
106+
use reqwest::Client as ReqwestClient;
63107
use serde_json::json;
64108

65109
use super::*;
66110

111+
fn test_client(server_url: &str) -> StudioClient {
112+
StudioClient::new(
113+
Credential {
114+
api_key: "test".to_string(),
115+
origin: CredentialOrigin::EnvVar,
116+
expires_at: None,
117+
},
118+
server_url,
119+
"test-version",
120+
false,
121+
ReqwestClient::new(),
122+
Duration::from_secs(1),
123+
)
124+
}
125+
126+
fn test_input() -> SubgraphDeleteInput {
127+
SubgraphDeleteInput {
128+
graph_ref: "test-graph@test-variant".parse().unwrap(),
129+
subgraph: "accounts".to_string(),
130+
}
131+
}
132+
133+
#[tokio::test]
134+
async fn check_reports_success_when_composition_succeeds() {
135+
let server = MockServer::start_async().await;
136+
server.mock(|when, then| {
137+
when.method(POST)
138+
.body_includes("ComposeAndFilterPreviewAsyncMutation");
139+
then.status(200).json_body(json!({
140+
"data": { "graph": { "variant": {
141+
"composeAndFilterPreviewAsync": { "buildID": "build-123" }
142+
} } }
143+
}));
144+
});
145+
server.mock(|when, then| {
146+
when.method(POST)
147+
.body_includes("ComposeAndFilterPreviewStatusQuery");
148+
then.status(200).json_body(json!({
149+
"data": { "graph": { "variant": {
150+
"composeAndFilterPreviewStatus": { "__typename": "ComposeAndFilterPreviewSuccess" }
151+
} } }
152+
}));
153+
});
154+
server.mock(|when, then| {
155+
when.method(POST)
156+
.body_includes("ComposeAndFilterPreviewResultQuery");
157+
then.status(200).json_body(json!({
158+
"data": { "graph": { "variant": {
159+
"composeAndFilterPreviewStatus": {
160+
"__typename": "ComposeAndFilterPreviewSuccess",
161+
"composeResults": {
162+
"apiSchemaDocument": "type Query { hi: String }",
163+
"supergraphSchemaDocument": "supergraph"
164+
},
165+
"filterResults": null
166+
}
167+
} } }
168+
}));
169+
});
170+
171+
let response = check(test_input(), &test_client(&server.url("/")), 30)
172+
.await
173+
.unwrap();
174+
175+
assert_eq!(
176+
response,
177+
SubgraphDeleteResponse {
178+
supergraph_was_updated: true,
179+
build_errors: BuildErrors::new(),
180+
}
181+
);
182+
}
183+
184+
#[tokio::test]
185+
async fn check_reports_build_errors_when_composition_fails() {
186+
let server = MockServer::start_async().await;
187+
server.mock(|when, then| {
188+
when.method(POST)
189+
.body_includes("ComposeAndFilterPreviewAsyncMutation");
190+
then.status(200).json_body(json!({
191+
"data": { "graph": { "variant": {
192+
"composeAndFilterPreviewAsync": { "buildID": "build-123" }
193+
} } }
194+
}));
195+
});
196+
server.mock(|when, then| {
197+
when.method(POST)
198+
.body_includes("ComposeAndFilterPreviewStatusQuery");
199+
then.status(200).json_body(json!({
200+
"data": { "graph": { "variant": {
201+
"composeAndFilterPreviewStatus": { "__typename": "ComposeAndFilterPreviewComposeFailure" }
202+
} } }
203+
}));
204+
});
205+
server.mock(|when, then| {
206+
when.method(POST)
207+
.body_includes("ComposeAndFilterPreviewResultQuery");
208+
then.status(200).json_body(json!({
209+
"data": { "graph": { "variant": {
210+
"composeAndFilterPreviewStatus": {
211+
"__typename": "ComposeAndFilterPreviewComposeFailure",
212+
"composeErrors": [
213+
{ "message": "accounts is required by products", "code": "REQUIRED_SUBGRAPH", "failedStep": "VALIDATE" }
214+
]
215+
}
216+
} } }
217+
}));
218+
});
219+
220+
let response = check(test_input(), &test_client(&server.url("/")), 30)
221+
.await
222+
.unwrap();
223+
224+
assert_eq!(
225+
response,
226+
SubgraphDeleteResponse {
227+
supergraph_was_updated: false,
228+
build_errors: vec![BuildError::composition_error(
229+
None,
230+
Some("accounts is required by products".to_string()),
231+
None,
232+
None
233+
)]
234+
.into(),
235+
}
236+
);
237+
}
238+
67239
#[test]
68240
fn get_delete_data_from_response_works() {
69241
let json_response = json!({

crates/rover-client/src/operations/subgraph/delete/types.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ pub(crate) type MutationCompositionErrors = subgraph_delete_mutation::SubgraphDe
1414
pub struct SubgraphDeleteInput {
1515
pub graph_ref: GraphRef,
1616
pub subgraph: String,
17-
pub dry_run: bool,
1817
}
1918

2019
/// this struct contains all the info needed to print the result of the delete.
@@ -36,7 +35,6 @@ impl From<SubgraphDeleteInput> for MutationVariables {
3635
graph_id,
3736
variant,
3837
subgraph: input.subgraph,
39-
dry_run: input.dry_run,
4038
}
4139
}
4240
}

src/command/subgraph/delete.rs

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,11 @@ pub struct Delete {
2828
}
2929

3030
impl Delete {
31-
pub async fn run(&self, client_config: StudioClientConfig) -> RoverResult<RoverOutput> {
31+
pub async fn run(
32+
&self,
33+
client_config: StudioClientConfig,
34+
checks_timeout_seconds: u64,
35+
) -> RoverResult<RoverOutput> {
3236
let client = client_config.get_authenticated_client(&self.profile)?;
3337
eprintln!(
3438
"Checking for build errors resulting from deleting subgraph {} from {} using credentials from the {} profile.",
@@ -40,23 +44,21 @@ impl Delete {
4044
// this is probably the normal path -- preview a subgraph delete
4145
// and make the user confirm it manually.
4246
if !self.confirm {
43-
let dry_run = true;
44-
// run delete with dryRun, so we can preview build errors
45-
let delete_dry_run_response = delete::run(
47+
let delete_check_response = delete::check(
4648
SubgraphDeleteInput {
4749
graph_ref: self.graph.graph_ref.clone(),
4850
subgraph: self.subgraph.subgraph_name.clone(),
49-
dry_run,
5051
},
5152
&client,
53+
checks_timeout_seconds,
5254
)
5355
.await?;
5456

5557
RoverOutput::SubgraphDeleteResponse {
5658
graph_ref: self.graph.graph_ref.clone(),
5759
subgraph: self.subgraph.subgraph_name.clone(),
58-
dry_run,
59-
delete_response: delete_dry_run_response,
60+
dry_run: true,
61+
delete_response: delete_check_response,
6062
}
6163
.get_stdout()?;
6264

@@ -67,13 +69,10 @@ impl Delete {
6769
}
6870
}
6971

70-
let dry_run = false;
71-
7272
let delete_response = delete::run(
7373
SubgraphDeleteInput {
7474
graph_ref: self.graph.graph_ref.clone(),
7575
subgraph: self.subgraph.subgraph_name.clone(),
76-
dry_run,
7776
},
7877
&client,
7978
)
@@ -82,7 +81,7 @@ impl Delete {
8281
Ok(RoverOutput::SubgraphDeleteResponse {
8382
graph_ref: self.graph.graph_ref.clone(),
8483
subgraph: self.subgraph.subgraph_name.clone(),
85-
dry_run,
84+
dry_run: false,
8685
delete_response,
8786
})
8887
}

src/command/subgraph/mod.rs

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ mod fetch;
44
pub mod introspect;
55
mod lint;
66
mod list;
7+
mod preview;
78
mod publish;
89

910
use clap::Parser;
@@ -20,17 +21,20 @@ pub struct Subgraph {
2021

2122
#[derive(Debug, Serialize, Parser)]
2223
pub enum Command {
23-
/// Check for build errors and breaking changes caused by an updated subgraph schema
24-
/// against the federated graph in the Apollo graph registry
24+
/// Check for build errors and breaking changes caused by an updated
25+
/// subgraph schema against the federated graph in the Apollo graph
26+
/// registry
2527
Check(check::Check),
2628

27-
/// Delete a subgraph from the Apollo registry and trigger composition in the graph router
29+
/// Delete a subgraph from the Apollo registry and trigger composition in
30+
/// the graph router
2831
Delete(delete::Delete),
2932

3033
/// Fetch a subgraph schema from the Apollo graph registry
3134
Fetch(fetch::Fetch),
3235

33-
/// Introspect a running subgraph endpoint to retrieve its schema definition (SDL)
36+
/// Introspect a running subgraph endpoint to retrieve its schema
37+
/// definition (SDL)
3438
Introspect(introspect::Introspect),
3539

3640
/// Lint a subgraph schema
@@ -39,7 +43,12 @@ pub enum Command {
3943
/// List all subgraphs for a federated graph
4044
List(list::List),
4145

42-
/// Publish an updated subgraph schema to the Apollo graph registry and trigger composition in the graph router
46+
/// Preview the supergraph schema (and optionally a contract filter)
47+
/// produced by hypothetical subgraph changes (without publishing them)
48+
Preview(preview::Preview),
49+
50+
/// Publish an updated subgraph schema to the Apollo graph registry and
51+
/// trigger composition in the graph router
4352
Publish(publish::Publish),
4453
}
4554

@@ -57,7 +66,7 @@ impl Subgraph {
5766
.run(client_config, git_context, checks_timeout_seconds)
5867
.await
5968
}
60-
Command::Delete(command) => command.run(client_config).await,
69+
Command::Delete(command) => command.run(client_config, checks_timeout_seconds).await,
6170
Command::Introspect(command) => {
6271
command
6372
.run(
@@ -70,6 +79,15 @@ impl Subgraph {
7079
Command::Fetch(command) => command.run(client_config).await,
7180
Command::Lint(command) => command.run(client_config).await,
7281
Command::List(command) => command.run(client_config).await,
82+
Command::Preview(command) => {
83+
command
84+
.run(
85+
client_config,
86+
checks_timeout_seconds,
87+
&rover_print::print::stderr::default(),
88+
)
89+
.await
90+
}
7391
Command::Publish(command) => {
7492
command
7593
.run(client_config, git_context, checks_timeout_seconds)

0 commit comments

Comments
 (0)