-
Notifications
You must be signed in to change notification settings - Fork 953
feat: add --dry-run support to events helper commands #455
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| --- | ||
| "@googleworkspace/cli": patch | ||
| --- | ||
|
|
||
| feat(helpers): add --dry-run support to events helper commands | ||
|
|
||
| Add dry-run mode to `gws events +renew` and `gws events +subscribe` commands. | ||
| When --dry-run is specified, the commands will print what actions would be | ||
| taken without making any API calls. This allows agents to simulate requests | ||
| and learn without reaching the server. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -30,78 +30,109 @@ pub(super) async fn handle_renew( | |
| matches: &ArgMatches, | ||
| ) -> Result<(), GwsError> { | ||
| let config = parse_renew_args(matches)?; | ||
| let dry_run = matches.get_flag("dry-run"); | ||
|
|
||
| if dry_run { | ||
| eprintln!("🏃 DRY RUN — no changes will be made\n"); | ||
| } | ||
|
|
||
| let client = crate::client::build_client()?; | ||
| let ws_token = auth::get_token(&[WORKSPACE_EVENTS_SCOPE]) | ||
| .await | ||
| .map_err(|e| GwsError::Auth(format!("Failed to get token: {e}")))?; | ||
| let ws_token: Option<String> = if dry_run { | ||
| None | ||
| } else { | ||
| Some( | ||
| auth::get_token(&[WORKSPACE_EVENTS_SCOPE]) | ||
| .await | ||
| .map_err(|e| GwsError::Auth(format!("Failed to get token: {e}")))?, | ||
| ) | ||
| }; | ||
|
|
||
| if let Some(name) = config.name { | ||
| // Reactivate a specific subscription | ||
| let name = crate::validate::validate_resource_name(&name)?; | ||
| eprintln!("Reactivating subscription: {name}"); | ||
| let resp = client | ||
| .post(format!( | ||
| "https://workspaceevents.googleapis.com/v1/{name}:reactivate" | ||
| )) | ||
| .bearer_auth(&ws_token) | ||
| .header("Content-Type", "application/json") | ||
| .body("{}") | ||
| .send() | ||
| .await | ||
| .context("Failed to reactivate subscription")?; | ||
|
|
||
| let body: Value = resp.json().await.context("Failed to parse response")?; | ||
|
|
||
| println!( | ||
| "{}", | ||
| serde_json::to_string_pretty(&body).unwrap_or_default() | ||
| ); | ||
|
|
||
| if !dry_run { | ||
| let token = ws_token.as_ref().expect("Token should be present in non-dry-run mode"); | ||
| let resp = client | ||
| .post(format!( | ||
| "https://workspaceevents.googleapis.com/v1/{name}:reactivate" | ||
| )) | ||
| .bearer_auth(token) | ||
| .header("Content-Type", "application/json") | ||
| .body("{}") | ||
| .send() | ||
| .await | ||
| .context("Failed to reactivate subscription")?; | ||
|
|
||
| let body: Value = resp.json().await.context("Failed to parse response")?; | ||
|
|
||
| println!( | ||
| "{}", | ||
| serde_json::to_string_pretty(&body).unwrap_or_default() | ||
| ); | ||
| } | ||
|
Comment on lines
+55
to
+74
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The dry-run mode for reactivating a specific subscription is incomplete. It prints a message to stderr but doesn't produce the JSON output describing the action, which is inconsistent with the behavior for renewing multiple subscriptions. To ensure consistent behavior and fully implement the dry-run feature, you should add an if !dry_run {
let token = ws_token.as_ref().expect("Token should be present in non-dry-run mode");
let resp = client
.post(format!(
"https://workspaceevents.googleapis.com/v1/{name}:reactivate"
))
.bearer_auth(token)
.header("Content-Type", "application/json")
.body("{}")
.send()
.await
.context("Failed to reactivate subscription")?;
let body: Value = resp.json().await.context("Failed to parse response")?;
println!(
"{}",
serde_json::to_string_pretty(&body).unwrap_or_default()
);
} else {
let result = json!({
"dry_run": true,
"action": "Would reactivate subscription",
"name": name,
"note": "Run without --dry-run to actually reactivate this subscription"
});
println!(
"{}",
serde_json::to_string_pretty(&result).unwrap_or_default()
);
} |
||
| } else { | ||
| let within_secs = parse_duration(&config.within)?; | ||
|
|
||
| // List all subscriptions | ||
| let resp = client | ||
| .get("https://workspaceevents.googleapis.com/v1/subscriptions") | ||
| .bearer_auth(&ws_token) | ||
| .send() | ||
| .await | ||
| .context("Failed to list subscriptions")?; | ||
|
|
||
| let body: Value = resp.json().await.context("Failed to parse response")?; | ||
|
|
||
| let mut renewed = 0; | ||
| if let Some(subs) = body.get("subscriptions").and_then(|s| s.as_array()) { | ||
| let now = std::time::SystemTime::now() | ||
| .duration_since(std::time::UNIX_EPOCH) | ||
| .unwrap_or_default() | ||
| .as_secs(); | ||
|
|
||
| let to_renew = filter_subscriptions_to_renew(subs, now, within_secs); | ||
|
|
||
| for name in to_renew { | ||
| let name = crate::validate::validate_resource_name(&name)?; | ||
| eprintln!("Renewing {name}..."); | ||
| let _ = client | ||
| .post(format!( | ||
| "https://workspaceevents.googleapis.com/v1/{name}:reactivate" | ||
| )) | ||
| .bearer_auth(&ws_token) | ||
| .header("Content-Type", "application/json") | ||
| .body("{}") | ||
| .send() | ||
| .await; | ||
| renewed += 1; | ||
| if !dry_run { | ||
| let token = ws_token.as_ref().expect("Token should be present in non-dry-run mode"); | ||
| let resp = client | ||
| .get("https://workspaceevents.googleapis.com/v1/subscriptions") | ||
| .bearer_auth(token) | ||
| .send() | ||
| .await | ||
| .context("Failed to list subscriptions")?; | ||
|
|
||
| let body: Value = resp.json().await.context("Failed to parse response")?; | ||
|
|
||
| let mut renewed = 0; | ||
| if let Some(subs) = body.get("subscriptions").and_then(|s| s.as_array()) { | ||
| let now = std::time::SystemTime::now() | ||
| .duration_since(std::time::UNIX_EPOCH) | ||
| .unwrap_or_default() | ||
| .as_secs(); | ||
|
|
||
| let to_renew = filter_subscriptions_to_renew(subs, now, within_secs); | ||
|
|
||
| for name in to_renew { | ||
| let name = crate::validate::validate_resource_name(&name)?; | ||
| eprintln!("Renewing {name}..."); | ||
| let _ = client | ||
| .post(format!( | ||
| "https://workspaceevents.googleapis.com/v1/{name}:reactivate" | ||
| )) | ||
| .bearer_auth(token) | ||
| .header("Content-Type", "application/json") | ||
| .body("{}") | ||
| .send() | ||
| .await; | ||
| renewed += 1; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| let result = json!({ | ||
| "status": "success", | ||
| "renewed": renewed, | ||
| }); | ||
| println!( | ||
| "{}", | ||
| serde_json::to_string_pretty(&result).unwrap_or_default() | ||
| ); | ||
| let result = json!({ | ||
| "status": "success", | ||
| "renewed": renewed, | ||
| }); | ||
| println!( | ||
| "{}", | ||
| serde_json::to_string_pretty(&result).unwrap_or_default() | ||
| ); | ||
| } else { | ||
| // In dry-run mode, just print what would happen | ||
| let result = json!({ | ||
| "dry_run": true, | ||
| "action": "Would list and renew subscriptions expiring within", | ||
| "within": config.within, | ||
| "note": "Run without --dry-run to actually renew subscriptions" | ||
| }); | ||
| println!( | ||
| "{}", | ||
| serde_json::to_string_pretty(&result).unwrap_or_default() | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| Ok(()) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -106,18 +106,31 @@ pub(super) async fn handle_subscribe( | |
| matches: &ArgMatches, | ||
| ) -> Result<(), GwsError> { | ||
| let config = parse_subscribe_args(matches)?; | ||
| let dry_run = matches.get_flag("dry-run"); | ||
|
|
||
| if dry_run { | ||
| eprintln!("🏃 DRY RUN — no changes will be made\n"); | ||
| } | ||
|
Comment on lines
+109
to
+113
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The dry-run implementation is incomplete for existing subscriptions. When the A check should be added to handle this scenario. When |
||
|
|
||
| if let Some(ref dir) = config.output_dir { | ||
| std::fs::create_dir_all(dir).context("Failed to create output dir")?; | ||
| if !dry_run { | ||
| std::fs::create_dir_all(dir).context("Failed to create output dir")?; | ||
| } | ||
| } | ||
|
|
||
| let client = crate::client::build_client()?; | ||
| let pubsub_token_provider = auth::token_provider(&[PUBSUB_SCOPE]); | ||
|
|
||
| // Get Pub/Sub token | ||
| let pubsub_token = auth::get_token(&[PUBSUB_SCOPE]) | ||
| .await | ||
| .map_err(|e| GwsError::Auth(format!("Failed to get Pub/Sub token: {e}")))?; | ||
| let pubsub_token = if dry_run { | ||
| None | ||
| } else { | ||
| Some( | ||
| auth::get_token(&[PUBSUB_SCOPE]) | ||
| .await | ||
| .map_err(|e| GwsError::Auth(format!("Failed to get Pub/Sub token: {e}")))?, | ||
| ) | ||
| }; | ||
|
Comment on lines
+125
to
+133
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The This happens because there's no dry-run check for the existing subscription path. The A possible fix is to handle the dry-run case for existing subscriptions early in the function and return before if dry_run && config.subscription.is_some() {
// Print dry-run JSON for existing subscription and return Ok(())
} |
||
|
|
||
| let (pubsub_subscription, topic_name, ws_subscription_name, created_resources) = | ||
| if let Some(ref sub_name) = config.subscription { | ||
|
|
@@ -139,11 +152,35 @@ pub(super) async fn handle_subscribe( | |
| let topic = format!("projects/{project}/topics/gws-{slug}-{suffix}"); | ||
| let sub = format!("projects/{project}/subscriptions/gws-{slug}-{suffix}"); | ||
|
|
||
| // Dry-run: print what would be created and exit | ||
| if dry_run { | ||
| eprintln!("Would create Pub/Sub topic: {topic}"); | ||
| eprintln!("Would create Pub/Sub subscription: {sub}"); | ||
| eprintln!("Would create Workspace Events subscription for target: {target}"); | ||
| eprintln!("Would listen for event types: {}", config.event_types.join(", ")); | ||
|
|
||
| let result = json!({ | ||
| "dry_run": true, | ||
| "action": "Would create Workspace Events subscription", | ||
| "pubsub_topic": topic, | ||
| "pubsub_subscription": sub, | ||
| "target": target, | ||
| "event_types": config.event_types, | ||
| "note": "Run without --dry-run to actually create subscription" | ||
| }); | ||
| println!( | ||
| "{}", | ||
| serde_json::to_string_pretty(&result).unwrap_or_default() | ||
| ); | ||
| return Ok(()); | ||
| } | ||
|
|
||
| // 1. Create Pub/Sub topic | ||
| eprintln!("Creating Pub/Sub topic: {topic}"); | ||
| let token = pubsub_token.as_ref().expect("Token should be present in non-dry-run mode"); | ||
| let resp = client | ||
| .put(format!("{PUBSUB_API_BASE}/{topic}")) | ||
| .bearer_auth(&pubsub_token) | ||
| .bearer_auth(token) | ||
| .header("Content-Type", "application/json") | ||
| .body("{}") | ||
| .send() | ||
|
|
@@ -166,9 +203,10 @@ pub(super) async fn handle_subscribe( | |
| "topic": topic, | ||
| "ackDeadlineSeconds": 60, | ||
| }); | ||
| let token = pubsub_token.as_ref().expect("Token should be present in non-dry-run mode"); | ||
| let resp = client | ||
| .put(format!("{PUBSUB_API_BASE}/{sub}")) | ||
| .bearer_auth(&pubsub_token) | ||
| .bearer_auth(token) | ||
| .header("Content-Type", "application/json") | ||
| .json(&sub_body) | ||
| .send() | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inconsistent dry-run behavior. When
--dry-runis used with a specific subscription name (--name), the command doesn't produce any JSON output indicating what would have happened. This is inconsistent with the behavior when--allis used. Anelseblock should be added to provide a descriptive JSON output for the single-subscription dry-run case.