diff --git a/.changeset/add-dry-run-to-event-helpers.md b/.changeset/add-dry-run-to-event-helpers.md new file mode 100644 index 00000000..ce995185 --- /dev/null +++ b/.changeset/add-dry-run-to-event-helpers.md @@ -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. diff --git a/src/helpers/events/renew.rs b/src/helpers/events/renew.rs index 3dd1d627..1690318d 100644 --- a/src/helpers/events/renew.rs +++ b/src/helpers/events/renew.rs @@ -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 = 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() + ); + } } 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(()) diff --git a/src/helpers/events/subscribe.rs b/src/helpers/events/subscribe.rs index edbfb4dc..7c829031 100644 --- a/src/helpers/events/subscribe.rs +++ b/src/helpers/events/subscribe.rs @@ -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"); + } 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}")))?, + ) + }; 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()