Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions docs/COMMANDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ pup <domain> <subgroup> <action> [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 | ✅ |
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
155 changes: 151 additions & 4 deletions src/commands/synthetics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ use datadog_api_client::datadogV1::api_synthetics::{
use datadog_api_client::datadogV2::api_synthetics::{
GetSyntheticsBrowserTestResultOptionalParams, GetSyntheticsTestResultOptionalParams,
GetSyntheticsTestVersionOptionalParams, ListSyntheticsBrowserTestLatestResultsOptionalParams,
ListSyntheticsTestLatestResultsOptionalParams, ListSyntheticsTestVersionsOptionalParams,
SearchSuitesOptionalParams, SyntheticsAPI as SyntheticsV2API,
ListSyntheticsDowntimesOptionalParams, ListSyntheticsTestLatestResultsOptionalParams,
ListSyntheticsTestVersionsOptionalParams, 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;
Expand Down Expand Up @@ -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<String>,
filter_active: Option<String>,
) -> 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<()> {
Expand Down Expand Up @@ -820,4 +862,109 @@ 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
}
}
43 changes: 43 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
/// If set to true, return only currently active downtimes
#[arg(long = "filter-active")]
filter_active: Option<String>,
},
/// 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)]
Expand Down Expand Up @@ -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 ---
Expand Down
Loading