diff --git a/Cargo.lock b/Cargo.lock index c07302dd..afc4644f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1100,7 +1100,7 @@ checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "datadog-api-client" version = "0.38.0" -source = "git+https://github.com/DataDog/datadog-api-client-rust?rev=a0148e35b714615060937e8ebde2711fe9906aae#a0148e35b714615060937e8ebde2711fe9906aae" +source = "git+https://github.com/DataDog/datadog-api-client-rust?rev=b95b98d4478ee668f07a33ef52aaf5eadb858fab#b95b98d4478ee668f07a33ef52aaf5eadb858fab" dependencies = [ "async-stream", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 7f508d68..78269d4d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -136,7 +136,7 @@ zip = { version = "9", default-features = false, features = ["deflate"], optiona # Datadog API client — pinned to a git rev # 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 = "a0148e35b714615060937e8ebde2711fe9906aae", optional = true, default-features = false } +datadog-api-client = { git = "https://github.com/DataDog/datadog-api-client-rust", rev = "b95b98d4478ee668f07a33ef52aaf5eadb858fab", optional = true, default-features = false } # HTTP middleware (version-matched to DD client; compiles on all targets) reqwest-middleware = "0.5" diff --git a/src/client.rs b/src/client.rs index bded7a55..63459392 100644 --- a/src/client.rs +++ b/src/client.rs @@ -418,30 +418,6 @@ static UNSTABLE_OPS: &[&str] = &[ "v2.star_model_lab_project", "v2.unpin_model_lab_run", "v2.unstar_model_lab_project", - // Databricks Integration (5) - "v2.create_databricks_integration_account", - "v2.delete_databricks_integration_account", - "v2.get_databricks_integration_account", - "v2.list_databricks_integration_accounts", - "v2.update_databricks_integration_account", - // Elastic Cloud Integration (5) - "v2.create_elastic_cloud_integration_account", - "v2.delete_elastic_cloud_integration_account", - "v2.get_elastic_cloud_integration_account", - "v2.list_elastic_cloud_integration_accounts", - "v2.update_elastic_cloud_integration_account", - // Snowflake Integration (5) - "v2.create_snowflake_integration_account", - "v2.delete_snowflake_integration_account", - "v2.get_snowflake_integration_account", - "v2.list_snowflake_integration_accounts", - "v2.update_snowflake_integration_account", - // Twilio Integration (5) - "v2.create_twilio_integration_account", - "v2.delete_twilio_integration_account", - "v2.get_twilio_integration_account", - "v2.list_twilio_integration_accounts", - "v2.update_twilio_integration_account", ]; #[cfg(test)] @@ -563,7 +539,7 @@ mod tests { #[test] fn test_unstable_ops_count() { - assert_eq!(UNSTABLE_OPS.len(), 207); + assert_eq!(UNSTABLE_OPS.len(), 187); } #[test] diff --git a/src/generated/agent_health.rs b/src/generated/agent_health.rs new file mode 100644 index 00000000..13ecb8ab --- /dev/null +++ b/src/generated/agent_health.rs @@ -0,0 +1,221 @@ +// Code generated by openapi-transformer. DO NOT EDIT. + +use anyhow::Result; +use datadog_api_client::datadogV2::api_agent_health::{ + AgentHealthAPI, ListAgentHealthIssuesOptionalParams, +}; + +use crate::config::Config; +use crate::formatter; + +#[derive(clap::Subcommand)] +pub enum Command { + /// Get an Agent Health issue + /// + /// Get an Agent Health issue and its available remediation in your organization. + /// Only open issues are returned. The `remediation` attribute is omitted when + /// the issue has no remediation. + /// + /// Returns `404` if the issue does not exist, is not open, belongs to another + /// organization, or you do not have access to its data. This response does not + /// reveal whether the issue exists. Returns `403` if you lack the `hosts_read` permission. + Get { + /// The unique identifier of the issue. It may contain `:` and must be percent-encoded + /// by clients when included in the path. + issue_id: String, + }, + /// List Agent Health issues + /// + /// List Agent Health issues across your organization. Only open issues are returned. + /// Returns `403` if you lack the `hosts_read` permission. + /// + /// Results use a stable order: severity rank (`high`, `medium`, `low`, then other values), + /// then `first_seen_at` descending with missing timestamps last, then issue ID ascending. + /// Use `page[size]` and the zero-based `page[number]` to paginate. Invalid page values + /// return a `400` JSON:API error response. `meta.page.total_filtered_count` is the number + /// of issues matching the filters, and `meta.page.total_count` the number of open issues + /// in the organization. + /// + /// Each filter accepts up to 50 comma-separated exact values, combined with OR within + /// a filter and AND across filters. Blank entries are ignored; an all-blank filter + /// is treated as absent. + List { + /// The number of issues to return per page, from 1 to 100. Defaults to 25. + #[arg(long = "page-size")] + page_size: Option, + /// The zero-based page number, from 0 to 1000000. Defaults to 0. + #[arg(long = "page-number")] + page_number: Option, + /// Filter by a comma-separated list of exact `severity` values, up to 50 values. + /// Values are combined with OR within this filter and AND across filters. + /// Blank entries are ignored; an all-blank filter is treated as absent. + #[arg(long = "filter-severity")] + filter_severity: Option, + /// Filter by a comma-separated list of exact `category` values, up to 50 values. + /// Values are combined with OR within this filter and AND across filters. + /// Blank entries are ignored; an all-blank filter is treated as absent. + #[arg(long = "filter-category")] + filter_category: Option, + /// Filter by a comma-separated list of exact `issue_type` values, up to 50 values. + /// Values are combined with OR within this filter and AND across filters. + /// Blank entries are ignored; an all-blank filter is treated as absent. + #[arg(long = "filter-issue-type")] + filter_issue_type: Option, + /// Filter by a comma-separated list of exact `resource_kind` values, up to 50 values. + /// Values are combined with OR within this filter and AND across filters. + /// Blank entries are ignored; an all-blank filter is treated as absent. + #[arg(long = "filter-resource-kind")] + filter_resource_kind: Option, + /// Filter by a comma-separated list of exact `resource_id` values, up to 50 values. + /// Values are combined with OR within this filter and AND across filters. + /// Blank entries are ignored; an all-blank filter is treated as absent. + #[arg(long = "filter-resource-id")] + filter_resource_id: Option, + }, +} + +pub async fn run(cfg: &Config, command: Command) -> Result<()> { + match command { + Command::Get { issue_id } => get(cfg, issue_id).await, + Command::List { + page_size, + page_number, + filter_severity, + filter_category, + filter_issue_type, + filter_resource_kind, + filter_resource_id, + } => { + list( + cfg, + page_size, + page_number, + filter_severity, + filter_category, + filter_issue_type, + filter_resource_kind, + filter_resource_id, + ) + .await + } + } +} + +/// Get an Agent Health issue +/// +/// Get an Agent Health issue and its available remediation in your organization. +/// Only open issues are returned. The `remediation` attribute is omitted when +/// the issue has no remediation. +/// +/// Returns `404` if the issue does not exist, is not open, belongs to another +/// organization, or you do not have access to its data. This response does not +/// reveal whether the issue exists. Returns `403` if you lack the `hosts_read` permission. +pub async fn get(cfg: &Config, issue_id: String) -> Result<()> { + let api = crate::make_api!(AgentHealthAPI, cfg); + let resp = api + .get_agent_health_issue(issue_id) + .await + .map_err(|e| anyhow::anyhow!("failed to get_agent_health_issue: {:?}", e))?; + formatter::output(cfg, &resp) +} + +/// List Agent Health issues +/// +/// List Agent Health issues across your organization. Only open issues are returned. +/// Returns `403` if you lack the `hosts_read` permission. +/// +/// Results use a stable order: severity rank (`high`, `medium`, `low`, then other values), +/// then `first_seen_at` descending with missing timestamps last, then issue ID ascending. +/// Use `page[size]` and the zero-based `page[number]` to paginate. Invalid page values +/// return a `400` JSON:API error response. `meta.page.total_filtered_count` is the number +/// of issues matching the filters, and `meta.page.total_count` the number of open issues +/// in the organization. +/// +/// Each filter accepts up to 50 comma-separated exact values, combined with OR within +/// a filter and AND across filters. Blank entries are ignored; an all-blank filter +/// is treated as absent. +pub async fn list( + cfg: &Config, + page_size: Option, + page_number: Option, + filter_severity: Option, + filter_category: Option, + filter_issue_type: Option, + filter_resource_kind: Option, + filter_resource_id: Option, +) -> Result<()> { + let api = crate::make_api!(AgentHealthAPI, cfg); + let mut params = ListAgentHealthIssuesOptionalParams::default(); + if let Some(v) = page_size { + params = params.page_size(v); + } + if let Some(v) = page_number { + params = params.page_number(v); + } + if let Some(v) = filter_severity { + params = params.filter_severity(v); + } + if let Some(v) = filter_category { + params = params.filter_category(v); + } + if let Some(v) = filter_issue_type { + params = params.filter_issue_type(v); + } + if let Some(v) = filter_resource_kind { + params = params.filter_resource_kind(v); + } + if let Some(v) = filter_resource_id { + params = params.filter_resource_id(v); + } + let resp = api + .list_agent_health_issues(params) + .await + .map_err(|e| anyhow::anyhow!("failed to list_agent_health_issues: {:?}", e))?; + let count = resp.data.len(); + let truncated = page_size.is_some_and(|l| count >= l as usize); + let next_action = if truncated { + page_size.map(|l| { + format!( + "Results may be truncated at {l}. Use --page-size={} or narrow the filters", + l + 1 + ) + }) + } else { + None + }; + let meta = formatter::Metadata { + count: Some(count), + truncated, + command: Some("agent-health list".to_string()), + next_action, + ..Default::default() + }; + formatter::output_with_meta(cfg, &resp, &meta) +} + +#[cfg(test)] +mod tests { + use crate::test_support::*; + + #[tokio::test] + async fn test_get_ok() { + 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": {"attributes": {"category": "configuration", "description": "The Agent configuration file contains an invalid value.", "first_seen_at": "2026-11-01T10:00:00Z", "issue_name": "Invalid Config", "issue_type": "invalid_config", "remediation": {"script": {"content": "...", "filename": "fix.sh", "language": "bash", "language_version": "5", "requires_root": true}, "steps": [{"order": 1, "text": "Open datadog.yaml"}], "summary": "Fix the invalid configuration value."}, "resource_id": "my-hostname", "resource_kind": "host", "resource_type": "host", "severity": "high", "state": "open", "tags": ["env:prod"], "title": "Invalid Agent configuration", "updated_at": "2026-11-09T08:00:00Z"}, "id": "invalid-config:4828e7d6c704c4a3", "type": "agent_health_issues"}}"##).await; + let result = super::get(&cfg, "test".to_string()).await; + assert!(result.is_ok(), "get failed: {:?}", result.err()); + cleanup_env(); + } + + #[tokio::test] + async fn test_list_ok() { + 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": [{"attributes": {"category": "configuration", "description": "The Agent configuration file contains an invalid value.", "first_seen_at": "2026-11-01T10:00:00Z", "issue_name": "Invalid Config", "issue_type": "invalid_config", "resource_id": "my-hostname", "resource_kind": "host", "resource_type": "host", "severity": "high", "state": "open", "tags": ["env:prod"], "title": "Invalid Agent configuration", "updated_at": "2026-11-09T08:00:00Z"}, "id": "invalid-config:4828e7d6c704c4a3", "type": "agent_health_issues"}], "meta": {"page": {"total_count": 3, "total_filtered_count": 1}}}"##).await; + let result = super::list(&cfg, None, None, None, None, None, None, None).await; + assert!(result.is_ok(), "list failed: {:?}", result.err()); + cleanup_env(); + } +} diff --git a/src/generated/mod.rs b/src/generated/mod.rs index ba771ea5..51527236 100644 --- a/src/generated/mod.rs +++ b/src/generated/mod.rs @@ -1,5 +1,10 @@ // Code generated by openapi-transformer. DO NOT EDIT. +// pup CI denies clippy warnings, and the spec's size, not the generator, drives these two: +// operations with many params make long handler signatures and large command variants. +#![allow(clippy::too_many_arguments, clippy::large_enum_variant)] + +pub mod agent_health; pub mod authn_mappings; pub mod domain_allowlist; pub mod downtime; @@ -15,6 +20,11 @@ use crate::config::Config; /// for new commands. #[derive(clap::Subcommand)] pub enum GeneratedCommand { + /// Manage agent_health resources + AgentHealth { + #[command(subcommand)] + action: agent_health::Command, + }, /// Manage domain_allowlist resources DomainAllowlist { #[command(subcommand)] @@ -24,6 +34,7 @@ pub enum GeneratedCommand { pub async fn run(cfg: &Config, command: GeneratedCommand) -> Result<()> { match command { + GeneratedCommand::AgentHealth { action } => agent_health::run(cfg, action).await, GeneratedCommand::DomainAllowlist { action } => domain_allowlist::run(cfg, action).await, } } diff --git a/src/generated/surface.txt b/src/generated/surface.txt index 7a296027..d06dea0f 100644 --- a/src/generated/surface.txt +++ b/src/generated/surface.txt @@ -1,3 +1,6 @@ +agent-health +agent-health.get +agent-health.list authn-mappings authn-mappings.create authn-mappings.delete