From 44a1ded1bf09a7953122e9c9b99cbb7ff965656b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 30 May 2026 08:28:35 +0000 Subject: [PATCH 1/2] feat(synthetics): add downtime list/create/delete commands Wraps the Synthetics downtime endpoints (SDK #1518): - pup synthetics downtime list [--filters] - pup synthetics downtime create --file - pup synthetics downtime delete Co-Authored-By: Claude --- docs/COMMANDS.md | 5 +- src/commands/synthetics.rs | 165 ++++++++++++++++++++++++++++++++++++- src/main.rs | 43 ++++++++++ 3 files changed, 208 insertions(+), 5 deletions(-) diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index 09481384..57c90008 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -44,7 +44,7 @@ pup [options] # Nested commands | api-keys | list, get, create, delete | src/commands/api_keys.rs | ✅ | | app-keys | list, get, create, update, delete | src/commands/app_keys.rs | ✅ | | infrastructure | hosts (list, get) | src/commands/infrastructure.rs | ✅ | -| synthetics | tests, locations, suites | src/commands/synthetics.rs | ✅ | +| synthetics | tests, locations, suites, downtime | src/commands/synthetics.rs | ✅ | | symdb | search | src/commands/symdb.rs | ✅ | | logs-restriction | list, get, create, update, delete, roles (list, add) | src/commands/logs_restriction.rs | ✅ | | processes | list | src/commands/processes.rs | ✅ | @@ -148,7 +148,7 @@ pup infrastructure hosts list - **monitors** - Monitor management (list, get, delete) - **dashboards** - Dashboard management (list, get, delete, url) - **slos** - Service Level Objectives (list, get, delete, status) -- **synthetics** - Synthetic monitoring (tests, locations, suites) +- **synthetics** - Synthetic monitoring (tests, locations, suites, downtime) - **notebooks** - Investigation notebooks (list, get, delete) - **downtime** - Monitor downtime (list, get, cancel) - **status-pages** - Status pages with components and degradations @@ -256,6 +256,7 @@ Available on all commands: - **integrations** — Added Jira integration (accounts, templates CRUD) and ServiceNow integration (instances, templates, users, assignment groups, business services) - **cloud** — Added OCI integration (tenancy configs CRUD, products) - **synthetics** — Added suites management (V2 API: search, get, create, update, delete) +- **synthetics** — Added downtime management (V2 API: list, create, delete) - **security** — Added content packs (list, activate, deactivate), bulk rule export, and entity risk scores - **incidents** — Added global settings, handles, and postmortem template management - **cases** — Added Jira/ServiceNow issue linking, case project moves, and notification rules diff --git a/src/commands/synthetics.rs b/src/commands/synthetics.rs index 1a5a1c16..77ab8a99 100644 --- a/src/commands/synthetics.rs +++ b/src/commands/synthetics.rs @@ -6,12 +6,13 @@ use datadog_api_client::datadogV2::api_synthetics::{ GetSyntheticsBrowserTestResultOptionalParams, GetSyntheticsTestResultOptionalParams, GetSyntheticsTestVersionOptionalParams, ListSyntheticsBrowserTestLatestResultsOptionalParams, ListSyntheticsTestLatestResultsOptionalParams, ListSyntheticsTestVersionsOptionalParams, - SearchSuitesOptionalParams, SyntheticsAPI as SyntheticsV2API, + ListSyntheticsDowntimesOptionalParams, SearchSuitesOptionalParams, + SyntheticsAPI as SyntheticsV2API, }; use datadog_api_client::datadogV2::model::{ DeletedSuitesRequestDelete, DeletedSuitesRequestDeleteAttributes, - DeletedSuitesRequestDeleteRequest, SuiteCreateEditRequest, SyntheticsTestResultRunType, - SyntheticsTestResultStatus, + DeletedSuitesRequestDeleteRequest, SuiteCreateEditRequest, SyntheticsDowntimeRequest, + SyntheticsTestResultRunType, SyntheticsTestResultStatus, }; use crate::config::Config; @@ -540,6 +541,47 @@ pub async fn tests_list_versions( formatter::output(cfg, &resp) } +// ---- Downtimes (V2 API) ---- + +pub async fn downtime_list( + cfg: &Config, + filter_test_ids: Option, + filter_active: Option, +) -> Result<()> { + let api = crate::make_api!(SyntheticsV2API, cfg); + let mut params = ListSyntheticsDowntimesOptionalParams::default(); + if let Some(ids) = filter_test_ids { + params = params.filter_test_ids(ids); + } + if let Some(active) = filter_active { + params = params.filter_active(active); + } + let resp = api + .list_synthetics_downtimes(params) + .await + .map_err(|e| anyhow::anyhow!("failed to list synthetics downtimes: {e:?}"))?; + formatter::output(cfg, &resp) +} + +pub async fn downtime_create(cfg: &Config, file: &str) -> Result<()> { + let api = crate::make_api!(SyntheticsV2API, cfg); + let body: SyntheticsDowntimeRequest = crate::util::read_json_file(file)?; + let resp = api + .create_synthetics_downtime(body) + .await + .map_err(|e| anyhow::anyhow!("failed to create synthetics downtime: {e:?}"))?; + formatter::output(cfg, &resp) +} + +pub async fn downtime_delete(cfg: &Config, downtime_id: &str) -> Result<()> { + let api = crate::make_api!(SyntheticsV2API, cfg); + api.delete_synthetics_downtime(downtime_id.to_string()) + .await + .map_err(|e| anyhow::anyhow!("failed to delete synthetics downtime: {e:?}"))?; + println!("Synthetics downtime {downtime_id} deleted."); + Ok(()) +} + // ---- Multistep (V2 API) ---- pub async fn multistep_get_subtests(cfg: &Config, public_id: &str) -> Result<()> { @@ -820,4 +862,121 @@ mod tests { .to_string() .contains("at least one result-id")); } + + #[tokio::test] + async fn test_synthetics_downtime_list() { + let _lock = lock_env().await; + let mut s = mockito::Server::new_async().await; + let cfg = test_config(&s.url()); + let _mock = mock_any(&mut s, "GET", r#"{"data":[]}"#).await; + let result = super::downtime_list(&cfg, None, None).await; + assert!( + result.is_ok(), + "downtime_list failed: {:?}", + result.err() + ); + cleanup_env(); + } + + #[tokio::test] + async fn test_synthetics_downtime_list_with_filters() { + let _lock = lock_env().await; + let mut s = mockito::Server::new_async().await; + let cfg = test_config(&s.url()); + let _mock = mock_any(&mut s, "GET", r#"{"data":[]}"#).await; + let result = super::downtime_list( + &cfg, + Some("abc-def-ghi".to_string()), + Some("true".to_string()), + ) + .await; + assert!( + result.is_ok(), + "downtime_list with filters failed: {:?}", + result.err() + ); + cleanup_env(); + } + + #[tokio::test] + async fn test_synthetics_downtime_list_error() { + let _lock = lock_env().await; + let mut server = mockito::Server::new_async().await; + let cfg = test_config(&server.url()); + let _mock = server + .mock("GET", mockito::Matcher::Any) + .with_status(403) + .with_header("content-type", "application/json") + .with_body(r#"{"errors":["Forbidden"]}"#) + .create_async() + .await; + let result = super::downtime_list(&cfg, None, None).await; + assert!(result.is_err(), "expected 403 error from downtime_list"); + cleanup_env(); + } + + #[tokio::test] + async fn test_synthetics_downtime_create() { + let _lock = lock_env().await; + let mut s = mockito::Server::new_async().await; + let cfg = test_config(&s.url()); + let _mock = mock_any( + &mut s, + "POST", + r#"{"data":{"id":"dt-123","type":"downtime","attributes":{"createdAt":"2024-01-01T00:00:00+00:00","createdBy":"u1","createdByName":"User One","description":"","isEnabled":true,"name":"test","tags":[],"testIds":[],"timeSlots":[],"updatedAt":"2024-01-01T00:00:00+00:00","updatedBy":"u1","updatedByName":"User One"}}}"#, + ) + .await; + let tmp = write_temp_json( + "downtime_create.json", + r#"{"data":{"type":"downtime","attributes":{"name":"test","isEnabled":true,"testIds":[],"timeSlots":[]}}}"#, + ); + let result = super::downtime_create(&cfg, tmp.to_str().unwrap()).await; + assert!( + result.is_ok(), + "downtime_create failed: {:?}", + result.err() + ); + cleanup_env(); + } + + #[tokio::test] + async fn test_synthetics_downtime_delete() { + let _lock = lock_env().await; + let mut s = mockito::Server::new_async().await; + let cfg = test_config(&s.url()); + let _mock = server_mock_delete(&mut s).await; + let result = super::downtime_delete(&cfg, "dt-abc-123").await; + assert!( + result.is_ok(), + "downtime_delete failed: {:?}", + result.err() + ); + cleanup_env(); + } + + #[tokio::test] + async fn test_synthetics_downtime_delete_error() { + let _lock = lock_env().await; + let mut server = mockito::Server::new_async().await; + let cfg = test_config(&server.url()); + let _mock = server + .mock("DELETE", mockito::Matcher::Any) + .with_status(404) + .with_header("content-type", "application/json") + .with_body(r#"{"errors":["Not Found"]}"#) + .create_async() + .await; + let result = super::downtime_delete(&cfg, "nonexistent-dt").await; + assert!(result.is_err(), "expected 404 error from downtime_delete"); + cleanup_env(); + } + + async fn server_mock_delete(s: &mut mockito::Server) -> mockito::Mock { + s.mock("DELETE", mockito::Matcher::Any) + .with_status(204) + .with_header("content-type", "application/json") + .with_body("") + .create_async() + .await + } } diff --git a/src/main.rs b/src/main.rs index 8fbdede0..31bf19c5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3547,6 +3547,34 @@ enum SyntheticsActions { #[command(subcommand)] action: SyntheticsMultistepActions, }, + /// Manage Synthetics downtimes + Downtime { + #[command(subcommand)] + action: SyntheticsDowntimeActions, + }, +} + +#[derive(Subcommand)] +enum SyntheticsDowntimeActions { + /// List all Synthetics downtimes + List { + /// Comma-separated list of Synthetics test public IDs to filter by + #[arg(long = "filter-test-ids")] + filter_test_ids: Option, + /// If set to true, return only currently active downtimes + #[arg(long = "filter-active")] + filter_active: Option, + }, + /// Create a new Synthetics downtime (body from JSON file) + Create { + #[arg(long, help = "JSON file with downtime definition (required)")] + file: String, + }, + /// Delete a Synthetics downtime by ID + Delete { + /// Downtime ID to delete + downtime_id: String, + }, } #[derive(Subcommand)] @@ -11398,6 +11426,21 @@ async fn main_inner() -> anyhow::Result<()> { .await?; } }, + SyntheticsActions::Downtime { action } => match action { + SyntheticsDowntimeActions::List { + filter_test_ids, + filter_active, + } => { + commands::synthetics::downtime_list(&cfg, filter_test_ids, filter_active) + .await?; + } + SyntheticsDowntimeActions::Create { file } => { + commands::synthetics::downtime_create(&cfg, &file).await?; + } + SyntheticsDowntimeActions::Delete { downtime_id } => { + commands::synthetics::downtime_delete(&cfg, &downtime_id).await?; + } + }, } } // --- Test Optimization --- From 3309bf8edb7cfa1fbb291c2814f2c011e50404f2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 30 May 2026 08:37:25 +0000 Subject: [PATCH 2/2] style(synthetics): apply cargo fmt Co-Authored-By: Claude --- src/commands/synthetics.rs | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/src/commands/synthetics.rs b/src/commands/synthetics.rs index 77ab8a99..08b5246e 100644 --- a/src/commands/synthetics.rs +++ b/src/commands/synthetics.rs @@ -5,8 +5,8 @@ use datadog_api_client::datadogV1::api_synthetics::{ use datadog_api_client::datadogV2::api_synthetics::{ GetSyntheticsBrowserTestResultOptionalParams, GetSyntheticsTestResultOptionalParams, GetSyntheticsTestVersionOptionalParams, ListSyntheticsBrowserTestLatestResultsOptionalParams, - ListSyntheticsTestLatestResultsOptionalParams, ListSyntheticsTestVersionsOptionalParams, - ListSyntheticsDowntimesOptionalParams, SearchSuitesOptionalParams, + ListSyntheticsDowntimesOptionalParams, ListSyntheticsTestLatestResultsOptionalParams, + ListSyntheticsTestVersionsOptionalParams, SearchSuitesOptionalParams, SyntheticsAPI as SyntheticsV2API, }; use datadog_api_client::datadogV2::model::{ @@ -870,11 +870,7 @@ mod tests { let cfg = test_config(&s.url()); let _mock = mock_any(&mut s, "GET", r#"{"data":[]}"#).await; let result = super::downtime_list(&cfg, None, None).await; - assert!( - result.is_ok(), - "downtime_list failed: {:?}", - result.err() - ); + assert!(result.is_ok(), "downtime_list failed: {:?}", result.err()); cleanup_env(); } @@ -931,11 +927,7 @@ mod tests { r#"{"data":{"type":"downtime","attributes":{"name":"test","isEnabled":true,"testIds":[],"timeSlots":[]}}}"#, ); let result = super::downtime_create(&cfg, tmp.to_str().unwrap()).await; - assert!( - result.is_ok(), - "downtime_create failed: {:?}", - result.err() - ); + assert!(result.is_ok(), "downtime_create failed: {:?}", result.err()); cleanup_env(); } @@ -946,11 +938,7 @@ mod tests { let cfg = test_config(&s.url()); let _mock = server_mock_delete(&mut s).await; let result = super::downtime_delete(&cfg, "dt-abc-123").await; - assert!( - result.is_ok(), - "downtime_delete failed: {:?}", - result.err() - ); + assert!(result.is_ok(), "downtime_delete failed: {:?}", result.err()); cleanup_env(); }