From 0054c6811390e7b8c9099bc85cee6727435a9981 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 30 May 2026 07:56:07 +0000 Subject: [PATCH 1/2] feat(cost): add OCI configs and cost anomalies list commands - pup cost oci-configs list: wraps ListCostOCIConfigs (SDK #1540) - pup cost anomalies list: wraps ListCostAnomalies (SDK #1588) Registers v2.list_cost_anomalies as an unstable op and adds the two API-key-only endpoints to the OAuth exclusion table. Co-Authored-By: Claude --- docs/COMMANDS.md | 2 +- src/client.rs | 16 +++++++++-- src/commands/cost.rs | 65 +++++++++++++++++++++++++++++++++++++++++++- src/main.rs | 31 +++++++++++++++++++++ 4 files changed, 109 insertions(+), 5 deletions(-) diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index 09481384..5c42e490 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -197,7 +197,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) diff --git a/src/client.rs b/src/client.rs index 42cc95d2..4ebc8fd6 100644 --- a/src/client.rs +++ b/src/client.rs @@ -414,6 +414,8 @@ static UNSTABLE_OPS: &[&str] = &[ "v2.get_investigation", "v2.list_investigations", "v2.trigger_investigation", + // Cloud Cost Management — Anomalies (1) + "v2.list_cost_anomalies", ]; // --------------------------------------------------------------------------- @@ -636,7 +638,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", @@ -698,6 +700,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 { @@ -1273,12 +1283,12 @@ mod tests { #[test] fn test_unstable_ops_count() { - assert_eq!(UNSTABLE_OPS.len(), 162); + assert_eq!(UNSTABLE_OPS.len(), 163); } #[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/cost.rs b/src/commands/cost.rs index 211c831b..d93b5c96 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,43 @@ 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/main.rs b/src/main.rs index 8fbdede0..882f0269 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7492,6 +7492,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)] @@ -7919,6 +7930,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 { @@ -13903,6 +13928,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 --- From f04988c3bde1fa454f148ff4ccb8b203ac7448da Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 30 May 2026 08:37:23 +0000 Subject: [PATCH 2/2] style(cost): apply cargo fmt Co-Authored-By: Claude --- src/commands/cost.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/commands/cost.rs b/src/commands/cost.rs index d93b5c96..a0fdbeae 100644 --- a/src/commands/cost.rs +++ b/src/commands/cost.rs @@ -225,7 +225,11 @@ mod tests { 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()); + assert!( + result.is_ok(), + "oci_configs_list failed: {:?}", + result.err() + ); cleanup_env(); }