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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
26 changes: 1 addition & 25 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Restore the integration operation registrations

Removing these registrations disables all existing Databricks, Elastic Cloud, Snowflake, and Twilio account commands. Their handlers use make_api!, while the newly pinned SDK still gates these operations and returns UnstableOperationDisabledError before sending a request. Restore all 20 registrations and update the operation-count assertion to 207.

React: 👍 useful · 👎 wrong · 😕 unclear · 👀 right, but not acting on it

What each reaction means
  • 👍 Useful: correct, and you're fixing it or will.
  • 👎 Wrong: the finding is incorrect, for example the bug can't happen or the code was misread.
  • 😕 Unclear: you can't tell what it means or why it matters.
  • 👀 Right, not acting: correct, but intended, out of scope for this PR, a known risk, or too minor.

🤖 Bits Code Review · Open Bits AI session


#[cfg(test)]
Expand Down Expand Up @@ -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]
Expand Down
221 changes: 221 additions & 0 deletions src/generated/agent_health.rs
Original file line number Diff line number Diff line change
@@ -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<i64>,
/// The zero-based page number, from 0 to 1000000. Defaults to 0.
#[arg(long = "page-number")]
page_number: Option<i64>,
/// 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<String>,
/// 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<String>,
/// 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<String>,
/// 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<String>,
/// 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<String>,
},
}

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<i64>,
page_number: Option<i64>,
filter_severity: Option<String>,
filter_category: Option<String>,
filter_issue_type: Option<String>,
filter_resource_kind: Option<String>,
filter_resource_id: Option<String>,
) -> 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
Comment on lines +179 to +180

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Keep page size fixed in continuation guidance

When agent-health list --page-size=100 returns a full page, the continuation hint recommends --page-size=101, exceeding the endpoint's maximum and causing HTTP 400. Increasing page size also shifts page boundaries when --page-number is supplied. Direct users to the next page while preserving page size and filters.

Suggested change
"Results may be truncated at {l}. Use --page-size={} or narrow the filters",
l + 1
"Results may be truncated at {l}. Use --page-number to request the next page, keeping --page-size={l} and filters unchanged"

React: 👍 useful · 👎 wrong · 😕 unclear · 👀 right, but not acting on it

What each reaction means
  • 👍 Useful: correct, and you're fixing it or will.
  • 👎 Wrong: the finding is incorrect, for example the bug can't happen or the code was misread.
  • 😕 Unclear: you can't tell what it means or why it matters.
  • 👀 Right, not acting: correct, but intended, out of scope for this PR, a known risk, or too minor.

🤖 Bits Code Review · Open Bits AI session

)
})
} 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();
}
}
11 changes: 11 additions & 0 deletions src/generated/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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)]
Expand All @@ -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,
}
}
3 changes: 3 additions & 0 deletions src/generated/surface.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
agent-health
agent-health.get
agent-health.list
authn-mappings
authn-mappings.create
authn-mappings.delete
Expand Down
Loading