diff --git a/Cargo.lock b/Cargo.lock index 19570a1..c9d83a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -829,8 +829,8 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "datadog-api-client" -version = "0.30.0" -source = "git+https://github.com/DataDog/datadog-api-client-rust?rev=aa0c8416e3af27038cf6a17e74ff1bf11d6bc1a6#aa0c8416e3af27038cf6a17e74ff1bf11d6bc1a6" +version = "0.31.0" +source = "git+https://github.com/DataDog/datadog-api-client-rust?rev=d4954b117c5451c0a8932dd0ad0db450fcb0989c#d4954b117c5451c0a8932dd0ad0db450fcb0989c" dependencies = [ "async-stream", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 509959d..168643c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -127,9 +127,9 @@ ssh-key = { version = "0.6", features = ["p256", "std"], optional = true } futures = { version = "0.3", optional = true } tokio-util = { version = "0.7", features = ["compat", "io"], optional = true } -# Datadog API client — pinned to 0.30.0 release tag +# Datadog API client — pinned to master HEAD 2026-05-29 # Use default-features = false; feature sets are activated per-target via features above -datadog-api-client = { git = "https://github.com/DataDog/datadog-api-client-rust", rev = "aa0c8416e3af27038cf6a17e74ff1bf11d6bc1a6", optional = true, default-features = false } +datadog-api-client = { git = "https://github.com/DataDog/datadog-api-client-rust", rev = "d4954b117c5451c0a8932dd0ad0db450fcb0989c", optional = true, default-features = false } # HTTP middleware (version-matched to DD client; compiles on all targets) reqwest-middleware = "0.5" diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index 0948138..bfd1a51 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -23,11 +23,11 @@ pup [options] # Nested commands |--------|-------------|------|--------| | acp | serve | src/commands/acp.rs | ✅ | | auth | login, logout, status, refresh | src/commands/auth.rs | ✅ | -| metrics | query, list, get, search | src/commands/metrics.rs | ✅ | +| metrics | query, list, search, timeseries, metadata, tags, submit | src/commands/metrics.rs | ✅ | | logs | search, list, aggregate | src/commands/logs.rs | ✅ | | traces | metrics (list, get, create, update, delete) | src/commands/traces.rs | ✅ | | monitors | list, get, delete, search | src/commands/monitors.rs | ✅ | -| dashboards | list, get, delete, url | src/commands/dashboards.rs | ✅ | +| dashboards | list, get, delete, url, annotations (list, get-page, create, update, delete) | src/commands/dashboards.rs, src/commands/annotations.rs | ✅ | | dbm | samples (search) | src/commands/dbm.rs | ✅ | | ddsql | table, time-series, spec, schema (tables, columns) | src/commands/ddsql.rs | ✅ | | debugger | probes (list, get, create, delete, watch) | src/commands/debugger.rs | ✅ | @@ -44,12 +44,12 @@ 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 | ✅ | | users | list, get, roles, service-accounts (create, app-keys CRUD) | src/commands/users.rs | ✅ | -| notebooks | list, get, delete | src/commands/notebooks.rs | ✅ | +| notebooks | list, get, delete, annotations (list, get-page, create, update, delete) | src/commands/notebooks.rs, src/commands/annotations.rs | ✅ | | security | rules, signals, findings, content-packs, risk-scores | src/commands/security.rs | ✅ | | organizations | get, list | src/commands/organizations.rs | ✅ | | service-catalog | list, get | src/commands/service_catalog.rs | ✅ | @@ -66,7 +66,7 @@ pup [options] # Nested commands | data-deletion | requests (list, create, cancel) | src/commands/data_deletion.rs | ✅ | | data-governance | scanner-rules (list) | src/commands/data_governance.rs | ✅ | | obs-pipelines | list, get, create, update, delete, validate | src/commands/obs_pipelines.rs | ✅ | -| llm-obs | projects (create, list), experiments (create, list, update, delete, summary, events (list, get), metric-values, dimension-values), datasets (create, list), spans (search) | src/commands/llm_obs.rs | ✅ | +| llm-obs | projects (create, list), experiments (create, list, update, delete, summary, events (list, get), metric-values, dimension-values), datasets (create, list, batch-update, clone, restore), spans (search) | src/commands/llm_obs.rs | ✅ | | reference-tables | list, get, create, batch-query | src/commands/reference_tables.rs | ✅ | | network | flows list, devices (list, get, interfaces, tags), interfaces (list, update) | src/commands/network.rs | ✅ | | cloud | aws, gcp, azure, oci | src/commands/cloud.rs | ✅ | @@ -113,6 +113,8 @@ pup logs search --query="service:api" --from="7d" --storage="flex" pup dbm samples search --query="dbm_type:activity service:orders env:prod" --from="1h" --limit=10 pup metrics search --query="avg:system.cpu.user{*}" --from="1h" pup metrics query --query="avg:system.cpu.user{*}" --from="1h" +pup metrics tags list system.cpu.user --window-seconds=3600 +pup metrics timeseries --file=request.json pup events search --query="@user.id:12345" ``` @@ -148,7 +150,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 @@ -161,6 +163,8 @@ pup infrastructure hosts list ### Security & Compliance - **security** - Security monitoring (rules, signals, findings, content-packs, risk-scores) + - `pup security findings mute --file ` — Mute or unmute up to 100 findings (stable, SDK #1519/#1660) + - `pup security rules bulk-convert --file ` — Bulk convert existing rules to Terraform ZIP archive (SDK #1675) - **static-analysis** - Code security (custom-rulesets, custom-rules) - **audit-logs** - Audit trail (list, search) - **data-governance** - Sensitive data scanning (scanner-rules list) @@ -172,7 +176,7 @@ pup infrastructure hosts list ### Development & Quality - **cicd** - CI/CD visibility (pipelines, events, tests, dora, flaky-tests) - **code-coverage** - Code coverage summaries (branch, commit) -- **error-tracking** - Error management (issues search, issues get) +- **error-tracking** - Error management (issues search, issues get); search supports `--state`, `--team`, `--assignee` filters - **scorecards** - Service quality (rules, outcomes) - **service-catalog** - Service registry (list, get) - **idp** - Service Catalog agent access (assist, find, owner, deps, register) @@ -197,7 +201,7 @@ pup infrastructure hosts list ### Cost & Usage - **usage** - Usage and billing (summary, hourly) -- **costs** - Cost management: `datadog` subgroup (projected, attribution, by-org, aws-config, azure-config, gcp-config) and `ccm` subgroup (custom-costs, tag-descriptions, tag-metadata, tags, tag-keys, budgets, commitments) +- **costs** - Cost management: `datadog` subgroup (projected, attribution, by-org, aws-config, azure-config, gcp-config), `ccm` subgroup (custom-costs, tag-descriptions, tag-metadata, tags, tag-keys, budgets, commitments), `oci-configs` subgroup (list), and `anomalies` subgroup (list) ### Configuration & Data Management - **obs-pipelines** - Observability pipelines (list, get, create, update, delete, validate) @@ -222,6 +226,14 @@ Available on all commands: ## Recent Enhancements +### v0.64.x — Error Tracking Issue Filters (SDK PRs #1568, #1480) + +- **error-tracking issues search** — new optional filter flags: + - `--state ` — filter by issue state: `OPEN`, `ACKNOWLEDGED`, `RESOLVED`, `IGNORED`, `EXCLUDED` + - `--team ` — filter by team UUID assignee + - `--assignee ` — filter by user UUID assignee + - These flags are independent of the existing `--track`/`--persona` mutual exclusion + ### v0.34.1 — ACP Server (Datadog AI Agent Integration) - ✅ **acp** (new) — Local ACP + OpenAI-compatible server that proxies to Datadog Bits AI @@ -243,7 +255,7 @@ Available on all commands: ### v0.28.0 — New Command Groups and Full Pipeline Implementation -- ✅ **llm-obs** (new) — LLM Observability: projects (create, list), experiments (create, list, update, delete, summary, events (list, get), metric-values, dimension-values), datasets (create, list), spans (search) +- ✅ **llm-obs** (new) — LLM Observability: projects (create, list), experiments (create, list, update, delete, summary, events (list, get), metric-values, dimension-values), datasets (create, list, batch-update, clone, restore), spans (search) - ✅ **reference-tables** (new) — Reference table management (list, get, create, batch-query) - ✅ **obs-pipelines** (upgraded from placeholder) — Full CRUD: list, get, create, update, delete, validate - **costs** — Added cloud cost configs: `aws-config`, `azure-config`, `gcp-config` (list, get, create, delete each) @@ -256,6 +268,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/auth/types.rs b/src/auth/types.rs index f86dfcd..168e779 100644 --- a/src/auth/types.rs +++ b/src/auth/types.rs @@ -209,6 +209,7 @@ pub fn default_scopes() -> Vec<&'static str> { "security_monitoring_filters_read", "security_monitoring_filters_write", "security_monitoring_findings_read", + "security_monitoring_findings_write", "security_monitoring_rules_read", "security_monitoring_rules_write", "security_monitoring_signals_read", diff --git a/src/client.rs b/src/client.rs index bc8f74d..4f00c3a 100644 --- a/src/client.rs +++ b/src/client.rs @@ -225,6 +225,12 @@ macro_rules! make_api_no_auth { /// All unstable operations (snake_case for the Rust DD client). static UNSTABLE_OPS: &[&str] = &[ + // Annotations (5) + "v2.create_annotation", + "v2.delete_annotation", + "v2.get_page_annotations", + "v2.list_annotations", + "v2.update_annotation", // Incidents (26) "v2.list_incidents", "v2.search_incidents", @@ -244,12 +250,6 @@ static UNSTABLE_OPS: &[&str] = &[ "v2.get_incident_postmortem_template", "v2.list_incident_postmortem_templates", "v2.update_incident_postmortem_template", - // Incident Teams (5) - "v2.create_incident_team", - "v2.delete_incident_team", - "v2.get_incident_team", - "v2.list_incident_teams", - "v2.update_incident_team", // Incident Services (5) "v2.create_incident_service", "v2.delete_incident_service", @@ -356,7 +356,7 @@ static UNSTABLE_OPS: &[&str] = &[ "v2.delete_aws_cloud_auth_persona_mapping", "v2.get_aws_cloud_auth_persona_mapping", "v2.list_aws_cloud_auth_persona_mappings", - // LLM Observability (18) + // LLM Observability (21) "v2.create_llm_obs_project", "v2.list_llm_obs_projects", "v2.create_llm_obs_experiment", @@ -365,6 +365,9 @@ static UNSTABLE_OPS: &[&str] = &[ "v2.delete_llm_obs_experiments", "v2.create_llm_obs_dataset", "v2.list_llm_obs_datasets", + "v2.batch_update_llm_obs_dataset", + "v2.clone_llm_obs_dataset", + "v2.restore_llm_obs_dataset_version", "v2.create_llm_obs_annotation_queue", "v2.list_llm_obs_annotation_queues", "v2.update_llm_obs_annotation_queue", @@ -420,6 +423,8 @@ static UNSTABLE_OPS: &[&str] = &[ "v2.get_investigation", "v2.list_investigations", "v2.trigger_investigation", + // Cloud Cost Management — Anomalies (1) + "v2.list_cost_anomalies", ]; // --------------------------------------------------------------------------- @@ -642,7 +647,7 @@ static OAUTH_EXCLUDED_ENDPOINTS: &[EndpointRequirement] = &[ path: "/api/v2/obs-pipelines/pipelines/validate", method: "POST", }, - // Cost / Billing (9) — API key only, no OAuth support + // Cost / Billing (11) — API key only, no OAuth support EndpointRequirement { path: "/api/v2/usage/projected_cost", method: "GET", @@ -704,6 +709,14 @@ static OAUTH_EXCLUDED_ENDPOINTS: &[EndpointRequirement] = &[ path: "/api/v2/cost/gcp_uc_config/", method: "DELETE", }, + EndpointRequirement { + path: "/api/v2/cost/oci_config", + method: "GET", + }, + EndpointRequirement { + path: "/api/v2/cost/anomalies", + method: "GET", + }, // Profiling (4) // No OAuth scope is declared for Continuous Profiler endpoints; force API-key auth. EndpointRequirement { @@ -1279,12 +1292,12 @@ mod tests { #[test] fn test_unstable_ops_count() { - assert_eq!(UNSTABLE_OPS.len(), 167); + assert_eq!(UNSTABLE_OPS.len(), 171); } #[test] fn test_oauth_excluded_count() { - assert_eq!(OAUTH_EXCLUDED_ENDPOINTS.len(), 52); + assert_eq!(OAUTH_EXCLUDED_ENDPOINTS.len(), 54); } #[test] diff --git a/src/commands/annotations.rs b/src/commands/annotations.rs new file mode 100644 index 0000000..7c84249 --- /dev/null +++ b/src/commands/annotations.rs @@ -0,0 +1,238 @@ +use anyhow::Result; +use datadog_api_client::datadogV2::api_annotations::{ + AnnotationsAPI, ListAnnotationsOptionalParams, +}; +use datadog_api_client::datadogV2::model::{AnnotationCreateRequest, AnnotationUpdateRequest}; + +use crate::config::Config; +use crate::formatter; +use crate::util; + +pub async fn list( + cfg: &Config, + page_id: &str, + start_time: i64, + end_time: i64, + widget_id: Option, +) -> Result<()> { + let api = crate::make_api!(AnnotationsAPI, cfg); + let params = if let Some(wid) = widget_id { + ListAnnotationsOptionalParams::default().widget_id(wid) + } else { + ListAnnotationsOptionalParams::default() + }; + let resp = api + .list_annotations(page_id.to_string(), start_time, end_time, params) + .await + .map_err(|e| anyhow::anyhow!("failed to list annotations: {e:?}"))?; + formatter::output(cfg, &resp) +} + +pub async fn get_page(cfg: &Config, page_id: &str, start_time: i64, end_time: i64) -> Result<()> { + let api = crate::make_api!(AnnotationsAPI, cfg); + let resp = api + .get_page_annotations(page_id.to_string(), start_time, end_time) + .await + .map_err(|e| anyhow::anyhow!("failed to get page annotations: {e:?}"))?; + formatter::output(cfg, &resp) +} + +pub async fn create(cfg: &Config, file: &str) -> Result<()> { + let api = crate::make_api!(AnnotationsAPI, cfg); + let body: AnnotationCreateRequest = util::read_json_file(file)?; + let resp = api + .create_annotation(body) + .await + .map_err(|e| anyhow::anyhow!("failed to create annotation: {e:?}"))?; + formatter::output(cfg, &resp) +} + +pub async fn update(cfg: &Config, annotation_id: uuid::Uuid, file: &str) -> Result<()> { + let api = crate::make_api!(AnnotationsAPI, cfg); + let body: AnnotationUpdateRequest = util::read_json_file(file)?; + let resp = api + .update_annotation(annotation_id, body) + .await + .map_err(|e| anyhow::anyhow!("failed to update annotation: {e:?}"))?; + formatter::output(cfg, &resp) +} + +pub async fn delete(cfg: &Config, annotation_id: uuid::Uuid) -> Result<()> { + let api = crate::make_api!(AnnotationsAPI, cfg); + api.delete_annotation(annotation_id) + .await + .map_err(|e| anyhow::anyhow!("failed to delete annotation: {e:?}"))?; + println!("Successfully deleted annotation {annotation_id}"); + Ok(()) +} + +#[cfg(test)] +mod tests { + use crate::test_support::*; + + #[tokio::test] + async fn test_annotations_list() { + let _lock = lock_env().await; + let mut s = mockito::Server::new_async().await; + let cfg = test_config(&s.url()); + mock_all(&mut s, r#"{"data":[]}"#).await; + let result = super::list(&cfg, "test-page", 0, 3600, None).await; + assert!( + result.is_ok(), + "annotations list failed: {:?}", + result.err() + ); + cleanup_env(); + } + + #[tokio::test] + async fn test_annotations_list_error() { + let _lock = lock_env().await; + let mut s = mockito::Server::new_async().await; + let cfg = test_config(&s.url()); + s.mock("GET", mockito::Matcher::Any) + .with_status(403) + .with_body("forbidden") + .create_async() + .await; + let result = super::list(&cfg, "test-page", 0, 3600, None).await; + assert!(result.is_err()); + cleanup_env(); + } + + #[tokio::test] + async fn test_annotations_get_page() { + let _lock = lock_env().await; + let mut s = mockito::Server::new_async().await; + let cfg = test_config(&s.url()); + mock_all( + &mut s, + r#"{"data":{"id":"p1","type":"page_annotations","attributes":{"annotations":{},"global_annotations":[],"widget_mapping":{}}}}"#, + ) + .await; + let result = super::get_page(&cfg, "test-page", 0, 3600).await; + assert!( + result.is_ok(), + "get page annotations failed: {:?}", + result.err() + ); + cleanup_env(); + } + + #[tokio::test] + async fn test_annotations_get_page_error() { + let _lock = lock_env().await; + let mut s = mockito::Server::new_async().await; + let cfg = test_config(&s.url()); + s.mock("GET", mockito::Matcher::Any) + .with_status(404) + .with_body(r#"{"errors":["not found"]}"#) + .create_async() + .await; + let result = super::get_page(&cfg, "missing-page", 0, 3600).await; + assert!(result.is_err()); + cleanup_env(); + } + + #[tokio::test] + async fn test_annotations_create() { + let _lock = lock_env().await; + let mut s = mockito::Server::new_async().await; + let cfg = test_config(&s.url()); + mock_all( + &mut s, + r#"{"data":{"id":"00000000-0000-0000-0000-000000000001","type":"annotation","attributes":{"author_id":"u1","color":"blue","created_at":0,"description":"x","end_time":0,"modified_at":0,"page_id":"p1","start_time":0,"type":"pointInTime"}}}"#, + ) + .await; + let tmp = write_temp_json( + "annotation_create.json", + r#"{"data":{"type":"annotation","attributes":{"color":"blue","description":"deploy","page_id":"p1","start_time":0,"type":"pointInTime"}}}"#, + ); + let result = super::create(&cfg, tmp.to_str().unwrap()).await; + assert!( + result.is_ok(), + "annotation create failed: {:?}", + result.err() + ); + cleanup_env(); + } + + #[tokio::test] + async fn test_annotations_create_bad_file() { + let _lock = lock_env().await; + let mut s = mockito::Server::new_async().await; + let cfg = test_config(&s.url()); + mock_all(&mut s, "{}").await; + let result = super::create(&cfg, "/nonexistent/file.json").await; + assert!(result.is_err()); + cleanup_env(); + } + + #[tokio::test] + async fn test_annotations_update() { + let _lock = lock_env().await; + let mut s = mockito::Server::new_async().await; + let cfg = test_config(&s.url()); + mock_all( + &mut s, + r#"{"data":{"id":"00000000-0000-0000-0000-000000000001","type":"annotation","attributes":{"author_id":"u1","color":"blue","created_at":0,"description":"x","end_time":0,"modified_at":0,"page_id":"p1","start_time":0,"type":"pointInTime"}}}"#, + ) + .await; + let tmp = write_temp_json( + "annotation_update.json", + r#"{"data":{"type":"annotation","attributes":{"color":"green","description":"rollback","page_id":"p1","start_time":1000,"type":"pointInTime"}}}"#, + ); + let id = uuid::Uuid::nil(); + let result = super::update(&cfg, id, tmp.to_str().unwrap()).await; + assert!( + result.is_ok(), + "annotation update failed: {:?}", + result.err() + ); + cleanup_env(); + } + + #[tokio::test] + async fn test_annotations_update_bad_file() { + let _lock = lock_env().await; + let mut s = mockito::Server::new_async().await; + let cfg = test_config(&s.url()); + mock_all(&mut s, "{}").await; + let id = uuid::Uuid::nil(); + let result = super::update(&cfg, id, "/nonexistent/file.json").await; + assert!(result.is_err()); + cleanup_env(); + } + + #[tokio::test] + async fn test_annotations_delete() { + let _lock = lock_env().await; + let mut s = mockito::Server::new_async().await; + let cfg = test_config(&s.url()); + mock_all(&mut s, "").await; + let id = uuid::Uuid::nil(); + let result = super::delete(&cfg, id).await; + assert!( + result.is_ok(), + "annotation delete failed: {:?}", + result.err() + ); + cleanup_env(); + } + + #[tokio::test] + async fn test_annotations_delete_error() { + let _lock = lock_env().await; + let mut s = mockito::Server::new_async().await; + let cfg = test_config(&s.url()); + s.mock("DELETE", mockito::Matcher::Any) + .with_status(404) + .with_body(r#"{"errors":["not found"]}"#) + .create_async() + .await; + let id = uuid::Uuid::nil(); + let result = super::delete(&cfg, id).await; + assert!(result.is_err()); + cleanup_env(); + } +} diff --git a/src/commands/cost.rs b/src/commands/cost.rs index 211c831..a0fdbea 100644 --- a/src/commands/cost.rs +++ b/src/commands/cost.rs @@ -1,5 +1,7 @@ use anyhow::Result; -use datadog_api_client::datadogV2::api_cloud_cost_management::CloudCostManagementAPI; +use datadog_api_client::datadogV2::api_cloud_cost_management::{ + CloudCostManagementAPI, ListCostAnomaliesOptionalParams, +}; use datadog_api_client::datadogV2::api_usage_metering::{ GetCostByOrgOptionalParams, GetMonthlyCostAttributionOptionalParams, GetProjectedCostOptionalParams, UsageMeteringAPI as UsageMeteringV2API, @@ -179,6 +181,28 @@ pub async fn gcp_config_delete(cfg: &Config, id: i64) -> Result<()> { Ok(()) } +// ---- Cloud Cost Management — OCI Configs ---- + +pub async fn oci_configs_list(cfg: &Config) -> Result<()> { + let api = make_ccm_api(cfg); + let resp = api + .list_cost_oci_configs() + .await + .map_err(|e| anyhow::anyhow!("failed to list OCI configs: {e:?}"))?; + formatter::output(cfg, &resp) +} + +// ---- Cloud Cost Management — Anomalies ---- + +pub async fn anomalies_list(cfg: &Config) -> Result<()> { + let api = make_ccm_api(cfg); + let resp = api + .list_cost_anomalies(ListCostAnomaliesOptionalParams::default()) + .await + .map_err(|e| anyhow::anyhow!("failed to list cost anomalies: {e:?}"))?; + formatter::output(cfg, &resp) +} + #[cfg(test)] mod tests { @@ -193,4 +217,47 @@ mod tests { let _ = super::projected(&cfg).await; cleanup_env(); } + + #[tokio::test] + async fn test_oci_configs_list() { + let _lock = lock_env().await; + let mut server = mockito::Server::new_async().await; + let cfg = test_config(&server.url()); + let _mock = mock_any(&mut server, "GET", r#"{"data":[]}"#).await; + let result = super::oci_configs_list(&cfg).await; + assert!( + result.is_ok(), + "oci_configs_list failed: {:?}", + result.err() + ); + cleanup_env(); + } + + #[tokio::test] + async fn test_oci_configs_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::oci_configs_list(&cfg).await; + assert!(result.is_err(), "expected error for 403 response"); + cleanup_env(); + } + + #[tokio::test] + async fn test_anomalies_list() { + let _lock = lock_env().await; + let mut server = mockito::Server::new_async().await; + let cfg = test_config(&server.url()); + let _mock = mock_any(&mut server, "GET", r#"{"data":null}"#).await; + let result = super::anomalies_list(&cfg).await; + assert!(result.is_ok(), "anomalies_list failed: {:?}", result.err()); + cleanup_env(); + } } diff --git a/src/commands/error_tracking.rs b/src/commands/error_tracking.rs index 00dce41..75ca9bf 100644 --- a/src/commands/error_tracking.rs +++ b/src/commands/error_tracking.rs @@ -3,7 +3,7 @@ use datadog_api_client::datadogV2::api_error_tracking::{ ErrorTrackingAPI, GetIssueOptionalParams, SearchIssuesOptionalParams, }; use datadog_api_client::datadogV2::model::{ - IssuesSearchRequest, IssuesSearchRequestData, IssuesSearchRequestDataAttributes, + IssueState, IssuesSearchRequest, IssuesSearchRequestData, IssuesSearchRequestDataAttributes, IssuesSearchRequestDataAttributesOrderBy, IssuesSearchRequestDataAttributesPersona, IssuesSearchRequestDataAttributesTrack, IssuesSearchRequestDataType, }; @@ -22,6 +22,9 @@ pub async fn issues_search( order_by: String, track: Option, persona: Option, + state: Option, + team: Option, + assignee: Option, ) -> Result<()> { let api = crate::make_api!(ErrorTrackingAPI, cfg); @@ -66,6 +69,31 @@ pub async fn issues_search( }; attrs = attrs.persona(persona_value); } + if let Some(ref s) = state { + let state_value = match s.to_uppercase().as_str() { + "OPEN" => IssueState::OPEN, + "ACKNOWLEDGED" => IssueState::ACKNOWLEDGED, + "RESOLVED" => IssueState::RESOLVED, + "IGNORED" => IssueState::IGNORED, + "EXCLUDED" => IssueState::EXCLUDED, + other => anyhow::bail!( + "invalid --state value '{}': must be OPEN, ACKNOWLEDGED, RESOLVED, IGNORED, or EXCLUDED", + other + ), + }; + attrs = attrs.states(vec![state_value]); + } + if let Some(ref t) = team { + let team_id = uuid::Uuid::parse_str(t) + .map_err(|_| anyhow::anyhow!("invalid --team value '{}': must be a valid UUID", t))?; + attrs = attrs.team_ids(vec![team_id]); + } + if let Some(ref a) = assignee { + let assignee_id = uuid::Uuid::parse_str(a).map_err(|_| { + anyhow::anyhow!("invalid --assignee value '{}': must be a valid UUID", a) + })?; + attrs = attrs.assignee_ids(vec![assignee_id]); + } let data = IssuesSearchRequestData::new(attrs, IssuesSearchRequestDataType::SEARCH_REQUEST); let body = IssuesSearchRequest::new(data); let params = SearchIssuesOptionalParams::default(); @@ -119,6 +147,9 @@ mod tests { "TOTAL_COUNT".into(), Some("trace".into()), None, + None, + None, + None, ) .await; cleanup_env(); @@ -139,6 +170,9 @@ mod tests { "TOTAL_COUNT".into(), None, Some("BROWSER".into()), + None, + None, + None, ) .await; cleanup_env(); @@ -159,6 +193,9 @@ mod tests { "TOTAL_COUNT".into(), Some("RUM".into()), None, + None, + None, + None, ) .await; cleanup_env(); @@ -176,6 +213,9 @@ mod tests { "INVALID".into(), Some("trace".into()), None, + None, + None, + None, ) .await; assert!(result.is_err()); @@ -185,6 +225,102 @@ mod tests { .contains("invalid --order-by value")); } + #[tokio::test] + async fn test_issues_search_with_state() { + let _lock = lock_env().await; + let mut s = mockito::Server::new_async().await; + let cfg = test_config(&s.url()); + mock_all(&mut s, r#"{"data": []}"#).await; + let result = super::issues_search( + &cfg, + None, + 10, + "1d".into(), + "now".into(), + "TOTAL_COUNT".into(), + Some("trace".into()), + None, + Some("OPEN".into()), + None, + None, + ) + .await; + assert!(result.is_ok()); + cleanup_env(); + } + + #[tokio::test] + async fn test_issues_search_invalid_state() { + let cfg = test_config("http://unused.local"); + let result = super::issues_search( + &cfg, + None, + 10, + "1d".into(), + "now".into(), + "TOTAL_COUNT".into(), + Some("trace".into()), + None, + Some("BADSTATE".into()), + None, + None, + ) + .await; + assert!(result.is_err()); + assert!(result + .unwrap_err() + .to_string() + .contains("invalid --state value")); + } + + #[tokio::test] + async fn test_issues_search_invalid_team_uuid() { + let cfg = test_config("http://unused.local"); + let result = super::issues_search( + &cfg, + None, + 10, + "1d".into(), + "now".into(), + "TOTAL_COUNT".into(), + Some("trace".into()), + None, + None, + Some("not-a-uuid".into()), + None, + ) + .await; + assert!(result.is_err()); + assert!(result + .unwrap_err() + .to_string() + .contains("invalid --team value")); + } + + #[tokio::test] + async fn test_issues_search_invalid_assignee_uuid() { + let cfg = test_config("http://unused.local"); + let result = super::issues_search( + &cfg, + None, + 10, + "1d".into(), + "now".into(), + "TOTAL_COUNT".into(), + Some("trace".into()), + None, + None, + None, + Some("not-a-uuid".into()), + ) + .await; + assert!(result.is_err()); + assert!(result + .unwrap_err() + .to_string() + .contains("invalid --assignee value")); + } + #[test] fn test_error_tracking_clap_mutual_exclusivity() { let result = crate::Cli::command().try_get_matches_from([ @@ -216,4 +352,22 @@ mod tests { "expected error when neither --track nor --persona is provided" ); } + + #[test] + fn test_error_tracking_clap_state_accepted() { + let result = crate::Cli::command().try_get_matches_from([ + "pup", + "error-tracking", + "issues", + "search", + "--track", + "trace", + "--state", + "OPEN", + ]); + assert!( + result.is_ok(), + "expected success when --state is provided alongside --track" + ); + } } diff --git a/src/commands/incidents.rs b/src/commands/incidents.rs index 18289bc..4d13bfe 100644 --- a/src/commands/incidents.rs +++ b/src/commands/incidents.rs @@ -2,9 +2,6 @@ use anyhow::{bail, Result}; use datadog_api_client::datadogV2::api_incident_services::{ GetIncidentServiceOptionalParams, IncidentServicesAPI, ListIncidentServicesOptionalParams, }; -use datadog_api_client::datadogV2::api_incident_teams::{ - GetIncidentTeamOptionalParams, IncidentTeamsAPI, ListIncidentTeamsOptionalParams, -}; use datadog_api_client::datadogV2::api_incidents::{ CreateGlobalIncidentHandleOptionalParams, GetIncidentOptionalParams, ImportIncidentOptionalParams, IncidentsAPI, ListGlobalIncidentHandlesOptionalParams, @@ -226,64 +223,6 @@ pub async fn postmortem_templates_delete(cfg: &Config, template_id: &str) -> Res Ok(()) } -// --------------------------------------------------------------------------- -// Incident teams -// --------------------------------------------------------------------------- - -fn make_teams_api(cfg: &Config) -> IncidentTeamsAPI { - crate::make_api!(IncidentTeamsAPI, cfg) -} - -pub async fn teams_list(cfg: &Config) -> Result<()> { - let api = make_teams_api(cfg); - let resp = api - .list_incident_teams(ListIncidentTeamsOptionalParams::default()) - .await - .map_err(|e| anyhow::anyhow!("failed to list incident teams: {:?}", e))?; - formatter::output(cfg, &resp) -} - -pub async fn teams_get(cfg: &Config, team_id: &str) -> Result<()> { - let api = make_teams_api(cfg); - let resp = api - .get_incident_team( - team_id.to_string(), - GetIncidentTeamOptionalParams::default(), - ) - .await - .map_err(|e| anyhow::anyhow!("failed to get incident team: {:?}", e))?; - formatter::output(cfg, &resp) -} - -pub async fn teams_create(cfg: &Config, file: &str) -> Result<()> { - let body = util::read_json_file(file)?; - let api = make_teams_api(cfg); - let resp = api - .create_incident_team(body) - .await - .map_err(|e| anyhow::anyhow!("failed to create incident team: {:?}", e))?; - formatter::output(cfg, &resp) -} - -pub async fn teams_update(cfg: &Config, team_id: &str, file: &str) -> Result<()> { - let body = util::read_json_file(file)?; - let api = make_teams_api(cfg); - let resp = api - .update_incident_team(team_id.to_string(), body) - .await - .map_err(|e| anyhow::anyhow!("failed to update incident team: {:?}", e))?; - formatter::output(cfg, &resp) -} - -pub async fn teams_delete(cfg: &Config, team_id: &str) -> Result<()> { - let api = make_teams_api(cfg); - api.delete_incident_team(team_id.to_string()) - .await - .map_err(|e| anyhow::anyhow!("failed to delete incident team: {:?}", e))?; - println!("Incident team {team_id} deleted."); - Ok(()) -} - // --------------------------------------------------------------------------- // Incident services // --------------------------------------------------------------------------- @@ -539,23 +478,6 @@ mod tests { cleanup_env(); } - #[tokio::test] - async fn test_incident_teams_list() { - let _lock = lock_env().await; - std::env::set_var("DD_TOKEN_STORAGE", "file"); - let mut server = mockito::Server::new_async().await; - let cfg = test_config(&server.url()); - let _mock = mock_any(&mut server, "GET", r#"{"data":[],"meta":{}}"#).await; - let result = super::teams_list(&cfg).await; - assert!( - result.is_ok(), - "incident teams list failed: {:?}", - result.err() - ); - cleanup_env(); - std::env::remove_var("DD_TOKEN_STORAGE"); - } - #[tokio::test] async fn test_incident_services_list() { let _lock = lock_env().await; diff --git a/src/commands/llm_obs.rs b/src/commands/llm_obs.rs index 6532c62..af05a95 100644 --- a/src/commands/llm_obs.rs +++ b/src/commands/llm_obs.rs @@ -4,9 +4,11 @@ use datadog_api_client::datadogV2::api_llm_observability::{ }; use datadog_api_client::datadogV2::model::{ LLMObsAnnotationQueueInteractionsRequest, LLMObsAnnotationQueueRequest, - LLMObsAnnotationQueueUpdateRequest, LLMObsCustomEvalConfigUpdateRequest, LLMObsDatasetRequest, - LLMObsDeleteAnnotationQueueInteractionsRequest, LLMObsDeleteExperimentsRequest, - LLMObsExperimentRequest, LLMObsExperimentUpdateRequest, LLMObsProjectRequest, + LLMObsAnnotationQueueUpdateRequest, LLMObsCustomEvalConfigUpdateRequest, + LLMObsDatasetBatchUpdateRequest, LLMObsDatasetCloneRequest, LLMObsDatasetRequest, + LLMObsDatasetRestoreVersionRequest, LLMObsDeleteAnnotationQueueInteractionsRequest, + LLMObsDeleteExperimentsRequest, LLMObsExperimentRequest, LLMObsExperimentUpdateRequest, + LLMObsProjectRequest, }; use crate::client; @@ -108,6 +110,51 @@ pub async fn datasets_list(cfg: &Config, project_id: &str) -> Result<()> { formatter::output(cfg, &resp) } +pub async fn datasets_batch_update( + cfg: &Config, + project_id: &str, + dataset_id: &str, + file: &str, +) -> Result<()> { + let body: LLMObsDatasetBatchUpdateRequest = util::read_json_file(file)?; + let api = make_api(cfg); + let resp = api + .batch_update_llm_obs_dataset(project_id.to_string(), dataset_id.to_string(), body) + .await + .map_err(|e| anyhow::anyhow!("failed to batch update dataset records: {e:?}"))?; + formatter::output(cfg, &resp) +} + +pub async fn datasets_clone( + cfg: &Config, + project_id: &str, + dataset_id: &str, + file: &str, +) -> Result<()> { + let body: LLMObsDatasetCloneRequest = util::read_json_file(file)?; + let api = make_api(cfg); + let resp = api + .clone_llm_obs_dataset(project_id.to_string(), dataset_id.to_string(), body) + .await + .map_err(|e| anyhow::anyhow!("failed to clone dataset: {e:?}"))?; + formatter::output(cfg, &resp) +} + +pub async fn datasets_restore( + cfg: &Config, + project_id: &str, + dataset_id: &str, + file: &str, +) -> Result<()> { + let body: LLMObsDatasetRestoreVersionRequest = util::read_json_file(file)?; + let api = make_api(cfg); + api.restore_llm_obs_dataset_version(project_id.to_string(), dataset_id.to_string(), body) + .await + .map_err(|e| anyhow::anyhow!("failed to restore dataset version: {e:?}"))?; + println!("Dataset {dataset_id} restored."); + Ok(()) +} + // ---- Experiment analytics (no typed equivalent — unstable MCP endpoints) ---- pub async fn experiments_summary(cfg: &Config, experiment_id: &str) -> Result<()> { @@ -2637,4 +2684,165 @@ mod tests { assert!(result.is_ok(), "spans_search failed: {:?}", result.err()); cleanup_env(); } + + // ---- datasets_batch_update ---- + + #[tokio::test] + async fn test_llm_obs_datasets_batch_update() { + let _lock = lock_env().await; + std::env::set_var("DD_TOKEN_STORAGE", "file"); + let mut server = mockito::Server::new_async().await; + let cfg = test_config(&server.url()); + + let tmp = write_temp_json( + "pup_test_ds_batch_update.json", + r#"{"data":{"id":"ds-1","type":"datasets","attributes":{"insert_records":[],"delete_records":[]}}}"#, + ); + let resp_body = r#"{"data":[]}"#; + let _mock = mock_any(&mut server, "POST", resp_body).await; + + let result = + super::datasets_batch_update(&cfg, "proj-1", "ds-1", tmp.to_str().unwrap()).await; + assert!( + result.is_ok(), + "datasets_batch_update failed: {:?}", + result.err() + ); + let _ = std::fs::remove_file(tmp); + cleanup_env(); + std::env::remove_var("DD_TOKEN_STORAGE"); + } + + #[tokio::test] + async fn test_llm_obs_datasets_batch_update_400() { + let _lock = lock_env().await; + std::env::set_var("DD_TOKEN_STORAGE", "file"); + let mut server = mockito::Server::new_async().await; + let cfg = test_config(&server.url()); + + let tmp = write_temp_json( + "pup_test_ds_batch_update_400.json", + r#"{"data":{"id":"ds-1","type":"datasets","attributes":{"insert_records":[],"delete_records":[]}}}"#, + ); + let _mock = server + .mock("POST", mockito::Matcher::Any) + .match_query(mockito::Matcher::Any) + .with_status(400) + .with_header("content-type", "application/json") + .with_body(r#"{"errors":["bad request"]}"#) + .create_async() + .await; + + let result = + super::datasets_batch_update(&cfg, "proj-1", "ds-1", tmp.to_str().unwrap()).await; + assert!(result.is_err(), "should fail on 400"); + let _ = std::fs::remove_file(tmp); + cleanup_env(); + std::env::remove_var("DD_TOKEN_STORAGE"); + } + + // ---- datasets_clone ---- + + #[tokio::test] + async fn test_llm_obs_datasets_clone() { + let _lock = lock_env().await; + std::env::set_var("DD_TOKEN_STORAGE", "file"); + let mut server = mockito::Server::new_async().await; + let cfg = test_config(&server.url()); + + let tmp = write_temp_json( + "pup_test_ds_clone.json", + r#"{"data":{"id":"ds-1","type":"datasets","attributes":{"name":"cloned-dataset"}}}"#, + ); + let resp_body = r#"{"data":{"id":"ds-2","type":"datasets","attributes":{"name":"cloned-dataset","description":null,"metadata":null,"created_at":"2024-01-01T00:00:00Z","updated_at":"2024-01-01T00:00:00Z","current_version":1}}}"#; + let _mock = mock_any(&mut server, "POST", resp_body).await; + + let result = super::datasets_clone(&cfg, "proj-1", "ds-1", tmp.to_str().unwrap()).await; + assert!(result.is_ok(), "datasets_clone failed: {:?}", result.err()); + let _ = std::fs::remove_file(tmp); + cleanup_env(); + std::env::remove_var("DD_TOKEN_STORAGE"); + } + + #[tokio::test] + async fn test_llm_obs_datasets_clone_404() { + let _lock = lock_env().await; + std::env::set_var("DD_TOKEN_STORAGE", "file"); + let mut server = mockito::Server::new_async().await; + let cfg = test_config(&server.url()); + + let tmp = write_temp_json( + "pup_test_ds_clone_404.json", + r#"{"data":{"id":"ds-1","type":"datasets","attributes":{"name":"cloned-dataset"}}}"#, + ); + let _mock = server + .mock("POST", mockito::Matcher::Any) + .match_query(mockito::Matcher::Any) + .with_status(404) + .with_header("content-type", "application/json") + .with_body(r#"{"errors":["not found"]}"#) + .create_async() + .await; + + let result = + super::datasets_clone(&cfg, "proj-1", "ds-missing", tmp.to_str().unwrap()).await; + assert!(result.is_err(), "should fail on 404"); + let _ = std::fs::remove_file(tmp); + cleanup_env(); + std::env::remove_var("DD_TOKEN_STORAGE"); + } + + // ---- datasets_restore ---- + + #[tokio::test] + async fn test_llm_obs_datasets_restore() { + let _lock = lock_env().await; + std::env::set_var("DD_TOKEN_STORAGE", "file"); + let mut server = mockito::Server::new_async().await; + let cfg = test_config(&server.url()); + + let tmp = write_temp_json( + "pup_test_ds_restore.json", + r#"{"data":{"id":"ds-1","type":"datasets","attributes":{"dataset_version":2}}}"#, + ); + let resp_body = r#"{"data":{"id":"ds-1","type":"datasets","attributes":{"name":"my-dataset","description":null,"metadata":null,"created_at":"2024-01-01T00:00:00Z","updated_at":"2024-01-01T00:00:00Z","current_version":2}}}"#; + let _mock = mock_any(&mut server, "POST", resp_body).await; + + let result = super::datasets_restore(&cfg, "proj-1", "ds-1", tmp.to_str().unwrap()).await; + assert!( + result.is_ok(), + "datasets_restore failed: {:?}", + result.err() + ); + let _ = std::fs::remove_file(tmp); + cleanup_env(); + std::env::remove_var("DD_TOKEN_STORAGE"); + } + + #[tokio::test] + async fn test_llm_obs_datasets_restore_400() { + let _lock = lock_env().await; + std::env::set_var("DD_TOKEN_STORAGE", "file"); + let mut server = mockito::Server::new_async().await; + let cfg = test_config(&server.url()); + + let tmp = write_temp_json( + "pup_test_ds_restore_400.json", + r#"{"data":{"id":"ds-1","type":"datasets","attributes":{"dataset_version":99}}}"#, + ); + let _mock = server + .mock("POST", mockito::Matcher::Any) + .match_query(mockito::Matcher::Any) + .with_status(400) + .with_header("content-type", "application/json") + .with_body(r#"{"errors":["invalid version"]}"#) + .create_async() + .await; + + let result = super::datasets_restore(&cfg, "proj-1", "ds-1", tmp.to_str().unwrap()).await; + assert!(result.is_err(), "should fail on 400"); + let _ = std::fs::remove_file(tmp); + cleanup_env(); + std::env::remove_var("DD_TOKEN_STORAGE"); + } } diff --git a/src/commands/metrics.rs b/src/commands/metrics.rs index d682c7f..1ba836c 100644 --- a/src/commands/metrics.rs +++ b/src/commands/metrics.rs @@ -136,20 +136,39 @@ pub async fn submit(cfg: &Config, file: &str) -> Result<()> { formatter::output(cfg, &resp) } -pub async fn tags_list(cfg: &Config, metric_name: &str) -> Result<()> { +pub async fn tags_list(cfg: &Config, metric_name: &str, window_seconds: Option) -> Result<()> { use datadog_api_client::datadogV2::api_metrics::ListTagsByMetricNameOptionalParams; let api = crate::make_api!(MetricsV2API, cfg); + let mut params = ListTagsByMetricNameOptionalParams::default(); + if let Some(w) = window_seconds { + params = params.window_seconds(w); + } let resp = api - .list_tags_by_metric_name( - metric_name.to_string(), - ListTagsByMetricNameOptionalParams::default(), - ) + .list_tags_by_metric_name(metric_name.to_string(), params) .await .map_err(|e| anyhow::anyhow!("failed to list tags for metric {metric_name}: {e:?}"))?; formatter::output(cfg, &resp) } +/// Query timeseries data using the v2 metrics API. +/// +/// The request body is provided as a JSON file matching the +/// `TimeseriesFormulaQueryRequest` schema. Use `cross_org_uuids` inside the +/// individual query objects in that file to enable cross-organisation queries +/// (SDK PR #1564). +pub async fn query_timeseries(cfg: &Config, file: &str) -> Result<()> { + use datadog_api_client::datadogV2::model::TimeseriesFormulaQueryRequest; + + let api = crate::make_api!(MetricsV2API, cfg); + let body: TimeseriesFormulaQueryRequest = util::read_json_file(file)?; + let resp = api + .query_timeseries_data(body) + .await + .map_err(|e| anyhow::anyhow!("failed to query timeseries data: {e:?}"))?; + formatter::output(cfg, &resp) +} + #[cfg(test)] mod tests { use crate::test_support::*; @@ -255,4 +274,82 @@ mod tests { ); cleanup_env(); } + + #[tokio::test] + async fn test_metrics_tags_list_no_window() { + let _lock = lock_env().await; + let mut server = mockito::Server::new_async().await; + let cfg = test_config(&server.url()); + let _mock = mock_any(&mut server, "GET", r#"{"data": null}"#).await; + + let result = super::tags_list(&cfg, "system.cpu.user", None).await; + assert!( + result.is_ok(), + "metrics tags list failed: {:?}", + result.err() + ); + cleanup_env(); + } + + #[tokio::test] + async fn test_metrics_tags_list_with_window() { + let _lock = lock_env().await; + let mut server = mockito::Server::new_async().await; + let cfg = test_config(&server.url()); + let _mock = mock_any(&mut server, "GET", r#"{"data": null}"#).await; + + let result = super::tags_list(&cfg, "system.cpu.user", Some(3600)).await; + assert!( + result.is_ok(), + "metrics tags list with window_seconds failed: {:?}", + result.err() + ); + cleanup_env(); + } + + #[tokio::test] + async fn test_metrics_query_timeseries() { + let _lock = lock_env().await; + let mut server = mockito::Server::new_async().await; + let cfg = test_config(&server.url()); + let _mock = mock_any( + &mut server, + "POST", + r#"{"data": {"type": "timeseries_response", "attributes": {"times": [], "values": [], "series": []}}}"#, + ) + .await; + + let body = r#"{ + "data": { + "attributes": { + "formulas": [{"formula": "a"}], + "from": 0, + "interval": 5000, + "queries": [{"data_source": "metrics", "query": "avg:system.cpu.user{*}", "name": "a"}], + "to": 3600000 + }, + "type": "timeseries_request" + } + }"#; + let tmp = write_temp_json("timeseries_test.json", body); + let result = super::query_timeseries(&cfg, tmp.to_str().unwrap()).await; + assert!( + result.is_ok(), + "metrics query timeseries failed: {:?}", + result.err() + ); + cleanup_env(); + } + + #[tokio::test] + async fn test_metrics_query_timeseries_bad_file() { + let _lock = lock_env().await; + let mut server = mockito::Server::new_async().await; + let cfg = test_config(&server.url()); + let _ = mock_any(&mut server, "POST", r#"{}"#).await; + + let result = super::query_timeseries(&cfg, "/nonexistent/path/request.json").await; + assert!(result.is_err(), "expected error for missing file, got ok"); + cleanup_env(); + } } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index f27fcad..ef94112 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -2,6 +2,7 @@ pub mod acp; pub mod agent; pub mod agentless_scanning; pub mod alias; +pub mod annotations; pub mod api; pub mod api_keys; pub mod apm; diff --git a/src/commands/security.rs b/src/commands/security.rs index 3f7baf6..72fa945 100644 --- a/src/commands/security.rs +++ b/src/commands/security.rs @@ -18,9 +18,10 @@ use datadog_api_client::datadogV2::api_security_monitoring::{ use datadog_api_client::datadogV2::model::{ ApplicationSecurityWafCustomRuleCreateRequest, ApplicationSecurityWafCustomRuleUpdateRequest, ApplicationSecurityWafExclusionFilterCreateRequest, - ApplicationSecurityWafExclusionFilterUpdateRequest, RestrictionPolicyUpdateRequest, - SecurityMonitoringRuleBulkExportAttributes, SecurityMonitoringRuleBulkExportData, - SecurityMonitoringRuleBulkExportDataType, SecurityMonitoringRuleBulkExportPayload, + ApplicationSecurityWafExclusionFilterUpdateRequest, MuteFindingsRequest, + RestrictionPolicyUpdateRequest, SecurityMonitoringRuleBulkExportAttributes, + SecurityMonitoringRuleBulkExportData, SecurityMonitoringRuleBulkExportDataType, + SecurityMonitoringRuleBulkExportPayload, SecurityMonitoringRuleConvertBulkPayload, SecurityMonitoringRuleConvertPayload, SecurityMonitoringRuleSort, SecurityMonitoringSignalListRequest, SecurityMonitoringSignalListRequestFilter, SecurityMonitoringSignalListRequestPage, SecurityMonitoringSignalsSort, @@ -288,6 +289,21 @@ pub async fn findings_search(cfg: &Config, query: Option, limit: i64) -> formatter::output(cfg, &resp) } +// ---- Mute Findings ---- + +/// Mute or unmute security findings (stable, SDK #1519/#1660). +/// Accepts up to 100 finding IDs per request. The `--file` must contain a +/// JSON body shaped as `MuteFindingsRequest` (see Datadog docs). +pub async fn findings_mute(cfg: &Config, file: &str) -> Result<()> { + let body: MuteFindingsRequest = util::read_json_file(file)?; + let api = crate::make_api!(SecurityMonitoringAPI, cfg); + let resp = api + .mute_security_findings(body) + .await + .map_err(|e| anyhow::anyhow!("failed to mute findings: {e:?}"))?; + formatter::output(cfg, &resp) +} + // ---- Bulk Export ---- pub async fn rules_bulk_export(cfg: &Config, rule_ids: Vec) -> Result<()> { @@ -330,6 +346,22 @@ pub async fn rules_to_terraform(cfg: &Config, file: &str) -> Result<()> { formatter::output(cfg, &resp) } +/// Bulk convert existing security monitoring rules to Terraform (SDK #1675). +/// The `--file` must contain a JSON body shaped as +/// `SecurityMonitoringRuleConvertBulkPayload`. Returns a ZIP archive written +/// to stdout (pipe to a file if you want to save it). +pub async fn rules_bulk_convert(cfg: &Config, file: &str) -> Result<()> { + let body: SecurityMonitoringRuleConvertBulkPayload = util::read_json_file(file)?; + let api = crate::make_api!(SecurityMonitoringAPI, cfg); + let bytes = api + .bulk_convert_existing_security_monitoring_rules(body) + .await + .map_err(|e| anyhow::anyhow!("failed to bulk convert security rules: {e:?}"))?; + let output = String::from_utf8_lossy(&bytes); + println!("{output}"); + Ok(()) +} + pub async fn terraform_export(cfg: &Config, resource_type: &str, resource_id: &str) -> Result<()> { let rt = parse_terraform_resource_type(resource_type)?; let api = crate::make_api!(SecurityMonitoringAPI, cfg); @@ -1099,4 +1131,104 @@ mod tests { .to_string() .contains("invalid --sort value")); } + + #[tokio::test] + async fn test_findings_mute_ok() { + let _lock = lock_env().await; + std::env::set_var("DD_TOKEN_STORAGE", "file"); + let mut server = mockito::Server::new_async().await; + let cfg = test_config(&server.url()); + let tmp = write_temp_json( + "pup_test_findings_mute.json", + r#"{"data":{"type":"mute","attributes":{"mute":{"is_muted":true,"reason":"FALSE_POSITIVE"}},"relationships":{"findings":{"data":[]}}}}"#, + ); + let _mock = mock_any( + &mut server, + "PATCH", + r#"{"data":{"id":"mute-job-1","type":"mute_findings_response"}}"#, + ) + .await; + let result = super::findings_mute(&cfg, tmp.to_str().unwrap()).await; + assert!(result.is_ok(), "findings_mute failed: {:?}", result.err()); + let _ = std::fs::remove_file(tmp); + cleanup_env(); + std::env::remove_var("DD_TOKEN_STORAGE"); + } + + #[tokio::test] + async fn test_findings_mute_error() { + let _lock = lock_env().await; + std::env::set_var("DD_TOKEN_STORAGE", "file"); + let mut server = mockito::Server::new_async().await; + let cfg = test_config(&server.url()); + let tmp = write_temp_json( + "pup_test_findings_mute_err.json", + r#"{"data":{"type":"mute","attributes":{"mute":{"is_muted":true,"reason":"FALSE_POSITIVE"}},"relationships":{"findings":{"data":[]}}}}"#, + ); + let _mock = server + .mock("PATCH", mockito::Matcher::Any) + .with_status(403) + .with_header("content-type", "application/json") + .with_body(r#"{"errors":["Forbidden"]}"#) + .create_async() + .await; + let result = super::findings_mute(&cfg, tmp.to_str().unwrap()).await; + assert!(result.is_err(), "expected error for 403 response"); + let _ = std::fs::remove_file(tmp); + cleanup_env(); + std::env::remove_var("DD_TOKEN_STORAGE"); + } + + #[tokio::test] + async fn test_rules_bulk_convert_ok() { + let _lock = lock_env().await; + std::env::set_var("DD_TOKEN_STORAGE", "file"); + let mut server = mockito::Server::new_async().await; + let cfg = test_config(&server.url()); + let tmp = write_temp_json( + "pup_test_rules_bulk_convert.json", + r#"{"data":{"type":"security_monitoring_rules_convert_bulk","attributes":{"ruleIds":["abc-123"]}}}"#, + ); + let zip_bytes: &[u8] = b"PK\x03\x04fake-zip-bytes"; + let _mock = server + .mock("POST", mockito::Matcher::Any) + .with_status(200) + .with_header("content-type", "application/zip") + .with_body(zip_bytes) + .create_async() + .await; + let result = super::rules_bulk_convert(&cfg, tmp.to_str().unwrap()).await; + assert!( + result.is_ok(), + "rules_bulk_convert failed: {:?}", + result.err() + ); + let _ = std::fs::remove_file(tmp); + cleanup_env(); + std::env::remove_var("DD_TOKEN_STORAGE"); + } + + #[tokio::test] + async fn test_rules_bulk_convert_error() { + let _lock = lock_env().await; + std::env::set_var("DD_TOKEN_STORAGE", "file"); + let mut server = mockito::Server::new_async().await; + let cfg = test_config(&server.url()); + let tmp = write_temp_json( + "pup_test_rules_bulk_convert_err.json", + r#"{"data":{"type":"security_monitoring_rules_convert_bulk","attributes":{"ruleIds":["bad"]}}}"#, + ); + let _mock = server + .mock("POST", mockito::Matcher::Any) + .with_status(400) + .with_header("content-type", "application/json") + .with_body(r#"{"errors":["Bad Request"]}"#) + .create_async() + .await; + let result = super::rules_bulk_convert(&cfg, tmp.to_str().unwrap()).await; + assert!(result.is_err(), "expected error for 400 response"); + let _ = std::fs::remove_file(tmp); + cleanup_env(); + std::env::remove_var("DD_TOKEN_STORAGE"); + } } diff --git a/src/commands/synthetics.rs b/src/commands/synthetics.rs index 1a5a1c1..08b5246 100644 --- a/src/commands/synthetics.rs +++ b/src/commands/synthetics.rs @@ -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; @@ -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,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 + } } diff --git a/src/main.rs b/src/main.rs index dae432c..0e2d7db 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3096,11 +3096,6 @@ enum IncidentActions { #[command(subcommand)] action: IncidentPostmortemActions, }, - /// Manage incident teams - Teams { - #[command(subcommand)] - action: IncidentTeamActions, - }, /// Manage incident services Services { #[command(subcommand)] @@ -3113,27 +3108,6 @@ enum IncidentActions { }, } -#[derive(Subcommand)] -enum IncidentTeamActions { - /// List incident teams - List, - /// Get incident team details - Get { team_id: String }, - /// Create an incident team from JSON - Create { - #[arg(long, help = "JSON file with team data (required)")] - file: String, - }, - /// Update an incident team - Update { - team_id: String, - #[arg(long, help = "JSON file with team data (required)")] - file: String, - }, - /// Delete an incident team - Delete { team_id: String }, -} - #[derive(Subcommand)] enum IncidentServiceActions { /// List incident services @@ -3251,6 +3225,11 @@ enum DashboardActions { #[command(subcommand)] action: WidgetActions, }, + /// Manage annotations on dashboard pages + Annotations { + #[command(subcommand)] + action: AnnotationsActions, + }, } // ---- Debugger ---- @@ -3451,6 +3430,31 @@ enum MetricActions { #[command(subcommand)] action: MetricTagActions, }, + /// Query v2 timeseries data via TimeseriesFormulaQueryRequest JSON body (SDK PR #1564) + /// + /// Provide a JSON file matching the TimeseriesFormulaQueryRequest schema. + /// To query across organisations, include `cross_org_uuids` inside each + /// query object in the file (SDK PR #1564 added this field to + /// MetricsTimeseriesQuery and MetricsScalarQuery). + /// + /// Example body (timeseries_request.json): + /// { + /// "data": { + /// "attributes": { + /// "formulas": [{"formula": "a"}], + /// "from": 1700000000000, + /// "interval": 5000, + /// "queries": [{"data_source": "metrics", "query": "avg:system.cpu.user{*}", "name": "a", + /// "cross_org_uuids": [""]}], + /// "to": 1700003600000 + /// }, + /// "type": "timeseries_request" + /// } + /// } + Timeseries { + #[arg(long, help = "JSON file with TimeseriesFormulaQueryRequest body")] + file: String, + }, } #[derive(Subcommand)] @@ -3462,6 +3466,11 @@ enum MetricTagActions { from: String, #[arg(long, default_value = "now", help = "End time")] to: String, + #[arg( + long, + help = "Lookback window in seconds (SDK PR #1593; default 3600, max 2592000)" + )] + window_seconds: Option, }, } @@ -3573,6 +3582,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)] @@ -4715,6 +4752,15 @@ enum SecurityRuleActions { #[arg(long, help = "JSON file with the rule conversion payload (required)")] file: String, }, + /// Bulk convert existing rules to Terraform (returns a ZIP archive) + #[command(name = "bulk-convert")] + BulkConvert { + #[arg( + long, + help = "JSON file with SecurityMonitoringRuleConvertBulkPayload body (required)" + )] + file: String, + }, } #[derive(Subcommand)] @@ -4785,6 +4831,11 @@ enum SecurityFindingActions { #[arg(long, default_value_t = 100)] limit: i64, }, + /// Mute or unmute security findings (up to 100 per request) + Mute { + #[arg(long, help = "JSON file with MuteFindingsRequest body (required)")] + file: String, + }, } #[derive(Subcommand)] @@ -5739,6 +5790,11 @@ enum NotebookActions { }, /// Delete a notebook Delete { notebook_id: i64 }, + /// Manage annotations on notebook pages + Annotations { + #[command(subcommand)] + action: AnnotationsActions, + }, } // ---- RUM ---- @@ -6905,6 +6961,15 @@ enum ErrorTrackingIssueActions { help = "Client persona filter: ALL, BROWSER, MOBILE, or BACKEND" )] persona: Option, + #[arg( + long, + help = "Filter by issue state: OPEN, ACKNOWLEDGED, RESOLVED, IGNORED, EXCLUDED" + )] + state: Option, + #[arg(long, help = "Filter by team UUID assignee")] + team: Option, + #[arg(long, help = "Filter by user UUID assignee")] + assignee: Option, }, /// Get issue details Get { issue_id: String }, @@ -7518,6 +7583,17 @@ enum CostActions { #[command(subcommand)] action: CostCcmActions, }, + /// Manage OCI (Oracle Cloud Infrastructure) cost configs + #[command(name = "oci-configs")] + OciConfigs { + #[command(subcommand)] + action: CostOciConfigsActions, + }, + /// Manage Cloud Cost Management anomalies + Anomalies { + #[command(subcommand)] + action: CostAnomaliesActions, + }, } #[derive(Subcommand)] @@ -7945,6 +8021,20 @@ enum CostCcmCommitmentsActions { }, } +// ---- Cost OCI Configs ---- +#[derive(Subcommand)] +enum CostOciConfigsActions { + /// List OCI cost configs + List, +} + +// ---- Cost Anomalies ---- +#[derive(Subcommand)] +enum CostAnomaliesActions { + /// List detected cost anomalies + List, +} + // ---- Misc ---- #[derive(Subcommand)] enum MiscActions { @@ -8708,6 +8798,33 @@ enum LlmObsDatasetsActions { #[arg(long, help = "Project ID (required)")] project_id: String, }, + /// Batch insert, update, and delete records in a dataset + BatchUpdate { + #[arg(long, help = "Project ID (required)")] + project_id: String, + #[arg(long, help = "Dataset ID (required)")] + dataset_id: String, + #[arg(long, help = "JSON file with batch update body (required)")] + file: String, + }, + /// Clone a dataset into a new dataset + Clone { + #[arg(long, help = "Project ID (required)")] + project_id: String, + #[arg(long, help = "Dataset ID to clone (required)")] + dataset_id: String, + #[arg(long, help = "JSON file with clone body (required)")] + file: String, + }, + /// Restore a dataset to a previous version + Restore { + #[arg(long, help = "Project ID (required)")] + project_id: String, + #[arg(long, help = "Dataset ID (required)")] + dataset_id: String, + #[arg(long, help = "JSON file with restore version body (required)")] + file: String, + }, } #[derive(Subcommand)] @@ -9174,6 +9291,75 @@ enum AliasActions { }, } +// ---- Annotations ---- +#[derive(Subcommand)] +enum AnnotationsActions { + /// List annotations for a page within a time window + List { + #[arg(long, help = "Page ID (dashboard or notebook page)")] + page_id: String, + #[arg(long, help = "Start of time window (Unix epoch seconds)")] + start: i64, + #[arg(long, help = "End of time window (Unix epoch seconds)")] + end: i64, + #[arg(long, help = "Optional widget ID to filter results")] + widget_id: Option, + }, + /// Get all annotations on a page grouped by widget + GetPage { + #[arg(long, help = "Page ID (dashboard or notebook page)")] + page_id: String, + #[arg(long, help = "Start of time window (Unix epoch seconds)")] + start: i64, + #[arg(long, help = "End of time window (Unix epoch seconds)")] + end: i64, + }, + /// Create a new annotation from a JSON file + Create { + #[arg(long, help = "JSON file with AnnotationCreateRequest body (required)")] + file: String, + }, + /// Update an existing annotation + Update { + /// Annotation UUID + annotation_id: uuid::Uuid, + #[arg(long, help = "JSON file with AnnotationUpdateRequest body (required)")] + file: String, + }, + /// Delete an annotation by ID + Delete { + /// Annotation UUID + annotation_id: uuid::Uuid, + }, +} + +/// Shared dispatch for annotation subcommands. Annotations attach to a page +/// (`dashboard:` or `notebook:`), so the same actions are exposed under +/// both `pup dashboards annotations` and `pup notebooks annotations`. +async fn run_annotations(cfg: &config::Config, action: AnnotationsActions) -> anyhow::Result<()> { + match action { + AnnotationsActions::List { + page_id, + start, + end, + widget_id, + } => commands::annotations::list(cfg, &page_id, start, end, widget_id).await, + AnnotationsActions::GetPage { + page_id, + start, + end, + } => commands::annotations::get_page(cfg, &page_id, start, end).await, + AnnotationsActions::Create { file } => commands::annotations::create(cfg, &file).await, + AnnotationsActions::Update { + annotation_id, + file, + } => commands::annotations::update(cfg, annotation_id, &file).await, + AnnotationsActions::Delete { annotation_id } => { + commands::annotations::delete(cfg, annotation_id).await + } + } +} + // ---- Skills ---- #[cfg(not(target_arch = "wasm32"))] #[derive(Subcommand)] @@ -10990,23 +11176,6 @@ async fn main_inner() -> anyhow::Result<()> { .await?; } }, - IncidentActions::Teams { action } => match action { - IncidentTeamActions::List => { - commands::incidents::teams_list(&cfg).await?; - } - IncidentTeamActions::Get { team_id } => { - commands::incidents::teams_get(&cfg, &team_id).await?; - } - IncidentTeamActions::Create { file } => { - commands::incidents::teams_create(&cfg, &file).await?; - } - IncidentTeamActions::Update { team_id, file } => { - commands::incidents::teams_update(&cfg, &team_id, &file).await?; - } - IncidentTeamActions::Delete { team_id } => { - commands::incidents::teams_delete(&cfg, &team_id).await?; - } - }, IncidentActions::Services { action } => match action { IncidentServiceActions::List => { commands::incidents::services_list(&cfg).await?; @@ -11033,6 +11202,7 @@ async fn main_inner() -> anyhow::Result<()> { Commands::Dashboards { action } => { cfg.validate_auth()?; match action { + DashboardActions::Annotations { action } => run_annotations(&cfg, action).await?, DashboardActions::List => commands::dashboards::list(&cfg).await?, DashboardActions::Get { id } => commands::dashboards::get(&cfg, &id).await?, DashboardActions::Url { @@ -11232,10 +11402,17 @@ async fn main_inner() -> anyhow::Result<()> { } }, MetricActions::Tags { action } => match action { - MetricTagActions::List { metric_name, .. } => { - commands::metrics::tags_list(&cfg, &metric_name).await?; + MetricTagActions::List { + metric_name, + window_seconds, + .. + } => { + commands::metrics::tags_list(&cfg, &metric_name, window_seconds).await?; } }, + MetricActions::Timeseries { file } => { + commands::metrics::query_timeseries(&cfg, &file).await?; + } } } // --- SLOs --- @@ -11441,6 +11618,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 --- @@ -11717,6 +11909,9 @@ async fn main_inner() -> anyhow::Result<()> { SecurityRuleActions::ToTerraform { file } => { commands::security::rules_to_terraform(&cfg, &file).await?; } + SecurityRuleActions::BulkConvert { file } => { + commands::security::rules_bulk_convert(&cfg, &file).await?; + } }, SecurityActions::Signals { action } => match action { SecuritySignalActions::List { @@ -11752,6 +11947,9 @@ async fn main_inner() -> anyhow::Result<()> { commands::security::findings_analyze(&cfg, &query, &from, &to, limit) .await?; } + SecurityFindingActions::Mute { file } => { + commands::security::findings_mute(&cfg, &file).await?; + } }, SecurityActions::ContentPacks { action } => match action { SecurityContentPackActions::List => { @@ -12430,6 +12628,7 @@ async fn main_inner() -> anyhow::Result<()> { Commands::Notebooks { action } => { cfg.validate_auth()?; match action { + NotebookActions::Annotations { action } => run_annotations(&cfg, action).await?, NotebookActions::List => commands::notebooks::list(&cfg).await?, NotebookActions::Get { notebook_id } => { commands::notebooks::get(&cfg, notebook_id).await?; @@ -13088,9 +13287,13 @@ async fn main_inner() -> anyhow::Result<()> { order_by, track, persona, + state, + team, + assignee, } => { commands::error_tracking::issues_search( - &cfg, query, limit, from, to, order_by, track, persona, + &cfg, query, limit, from, to, order_by, track, persona, state, team, + assignee, ) .await?; } @@ -13946,6 +14149,12 @@ async fn main_inner() -> anyhow::Result<()> { } }, }, + CostActions::OciConfigs { action } => match action { + CostOciConfigsActions::List => commands::cost::oci_configs_list(&cfg).await?, + }, + CostActions::Anomalies { action } => match action { + CostAnomaliesActions::List => commands::cost::anomalies_list(&cfg).await?, + }, } } // --- Misc --- @@ -14704,6 +14913,35 @@ async fn main_inner() -> anyhow::Result<()> { LlmObsDatasetsActions::List { project_id } => { commands::llm_obs::datasets_list(&cfg, &project_id).await?; } + LlmObsDatasetsActions::BatchUpdate { + project_id, + dataset_id, + file, + } => { + commands::llm_obs::datasets_batch_update( + &cfg, + &project_id, + &dataset_id, + &file, + ) + .await?; + } + LlmObsDatasetsActions::Clone { + project_id, + dataset_id, + file, + } => { + commands::llm_obs::datasets_clone(&cfg, &project_id, &dataset_id, &file) + .await?; + } + LlmObsDatasetsActions::Restore { + project_id, + dataset_id, + file, + } => { + commands::llm_obs::datasets_restore(&cfg, &project_id, &dataset_id, &file) + .await?; + } }, LlmObsActions::Spans { action } => match action { LlmObsSpansActions::Search {