use std::io::Write;
use anyhow::{Context, Result};
use clap::Parser;
use crate::cli::datadog::dashboard::{render_dashboard_table, DashboardRow};
use crate::cli::datadog::format::{output_as, OutputFormat};
use crate::cli::datadog::helpers::create_client;
use crate::datadog::client::DatadogClient;
use crate::datadog::dashboards_api::DashboardsApi;
use crate::datadog::types::Dashboard;
#[derive(Parser)]
pub struct GetCommand {
pub id: String,
#[arg(short = 'o', long, value_enum, default_value_t = OutputFormat::Table)]
pub output: OutputFormat,
}
impl GetCommand {
pub async fn execute(self) -> Result<()> {
let (client, _site) = create_client()?;
run_get(&client, &self.id, &self.output).await
}
}
async fn run_get(client: &DatadogClient, id: &str, output: &OutputFormat) -> Result<()> {
let dashboard = DashboardsApi::new(client).get(id).await?;
if output_as(&dashboard, output)? {
return Ok(());
}
let stdout = std::io::stdout();
let mut handle = stdout.lock();
render_get_table(&dashboard, &mut handle)
}
pub(crate) fn render_get_table(dashboard: &Dashboard, out: &mut dyn Write) -> Result<()> {
let row = DashboardRow {
id: dashboard.id.as_str(),
title: dashboard.title.as_str(),
author: dashboard.author_label(),
url: dashboard.url_label(),
};
render_dashboard_table(std::slice::from_ref(&row), out)?;
if let Some(desc) = dashboard.description.as_deref() {
if !desc.is_empty() {
writeln!(out, "DESCRIPTION: {desc}").context("Failed to write description line")?;
}
}
Ok(())
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
struct FailAfter {
successes_remaining: usize,
sink: Vec<u8>,
}
impl FailAfter {
fn new(successes_remaining: usize) -> Self {
Self {
successes_remaining,
sink: Vec::new(),
}
}
}
impl Write for FailAfter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
if self.successes_remaining == 0 {
return Err(std::io::Error::other("test forced write failure"));
}
self.sink.extend_from_slice(buf);
if buf.contains(&b'\n') {
self.successes_remaining -= 1;
}
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
fn dashboard_json() -> serde_json::Value {
serde_json::json!({
"id": "abc-def-ghi",
"title": "Service Overview",
"description": "Top-level service health.",
"url": "/dashboard/abc-def-ghi",
"author_handle": "alice@example.com",
"layout_type": "ordered",
"widgets": []
})
}
#[test]
fn render_get_table_includes_row_and_description() {
let d: Dashboard = serde_json::from_value(dashboard_json()).unwrap();
let mut buf = Vec::new();
render_get_table(&d, &mut buf).unwrap();
let out = String::from_utf8(buf).unwrap();
assert!(out.contains("ID"));
assert!(out.contains("abc-def-ghi"));
assert!(out.contains("Service Overview"));
assert!(out.contains("alice@example.com"));
assert!(out.contains("/dashboard/abc-def-ghi"));
assert!(out.contains("DESCRIPTION: Top-level service health."));
}
#[test]
fn render_get_table_skips_description_when_missing() {
let d: Dashboard = serde_json::from_value(serde_json::json!({
"id": "x",
"title": "y"
}))
.unwrap();
let mut buf = Vec::new();
render_get_table(&d, &mut buf).unwrap();
let out = String::from_utf8(buf).unwrap();
assert!(!out.contains("DESCRIPTION"));
}
#[test]
fn render_get_table_skips_description_when_empty_string() {
let d: Dashboard = serde_json::from_value(serde_json::json!({
"id": "x",
"title": "y",
"description": ""
}))
.unwrap();
let mut buf = Vec::new();
render_get_table(&d, &mut buf).unwrap();
let out = String::from_utf8(buf).unwrap();
assert!(!out.contains("DESCRIPTION"));
}
#[test]
fn render_get_table_propagates_table_write_errors() {
let d: Dashboard = serde_json::from_value(dashboard_json()).unwrap();
let err = render_get_table(&d, &mut FailAfter::new(0)).unwrap_err();
assert!(err.to_string().contains("Failed to write"));
}
#[test]
fn render_get_table_propagates_description_line_write_errors() {
let d: Dashboard = serde_json::from_value(dashboard_json()).unwrap();
let err = render_get_table(&d, &mut FailAfter::new(3)).unwrap_err();
assert!(err.to_string().contains("Failed to write description line"));
}
#[test]
fn fail_after_flush_is_a_noop() {
let mut w = FailAfter::new(0);
w.flush().unwrap();
}
#[tokio::test]
async fn run_get_table_path_writes_to_stdout() {
let server = wiremock::MockServer::start().await;
wiremock::Mock::given(wiremock::matchers::method("GET"))
.and(wiremock::matchers::path("/api/v1/dashboard/abc-def-ghi"))
.respond_with(wiremock::ResponseTemplate::new(200).set_body_json(dashboard_json()))
.expect(1)
.mount(&server)
.await;
let client = DatadogClient::new(&server.uri(), "api", "app").unwrap();
run_get(&client, "abc-def-ghi", &OutputFormat::Table)
.await
.unwrap();
}
#[tokio::test]
async fn run_get_json_path_returns_ok() {
let server = wiremock::MockServer::start().await;
wiremock::Mock::given(wiremock::matchers::method("GET"))
.and(wiremock::matchers::path("/api/v1/dashboard/x"))
.respond_with(wiremock::ResponseTemplate::new(200).set_body_json(dashboard_json()))
.mount(&server)
.await;
let client = DatadogClient::new(&server.uri(), "api", "app").unwrap();
run_get(&client, "x", &OutputFormat::Json).await.unwrap();
}
#[tokio::test]
async fn run_get_propagates_404() {
let server = wiremock::MockServer::start().await;
wiremock::Mock::given(wiremock::matchers::method("GET"))
.and(wiremock::matchers::path("/api/v1/dashboard/missing"))
.respond_with(wiremock::ResponseTemplate::new(404).set_body_string("not found"))
.mount(&server)
.await;
let client = DatadogClient::new(&server.uri(), "api", "app").unwrap();
let err = run_get(&client, "missing", &OutputFormat::Table)
.await
.unwrap_err();
assert!(err.to_string().contains("404"));
}
#[tokio::test]
async fn get_command_execute_errors_when_credentials_missing() {
use crate::datadog::test_support::{with_empty_home, EnvGuard};
let guard = EnvGuard::take();
let _dir = with_empty_home(&guard);
let cmd = GetCommand {
id: "abc".into(),
output: OutputFormat::Table,
};
let err = cmd.execute().await.unwrap_err();
assert!(err.to_string().contains("not configured"));
}
#[tokio::test]
async fn get_command_execute_end_to_end_via_api_url_override() {
use std::fs;
use crate::datadog::auth::{DATADOG_API_KEY, DATADOG_API_URL, DATADOG_APP_KEY};
use crate::datadog::test_support::{with_empty_home, EnvGuard};
let server = wiremock::MockServer::start().await;
wiremock::Mock::given(wiremock::matchers::method("GET"))
.and(wiremock::matchers::path("/api/v1/dashboard/abc"))
.respond_with(wiremock::ResponseTemplate::new(200).set_body_json(dashboard_json()))
.expect(1)
.mount(&server)
.await;
let guard = EnvGuard::take();
let dir = with_empty_home(&guard);
let omni_dir = dir.path().join(".omni-dev");
fs::create_dir_all(&omni_dir).unwrap();
fs::write(
omni_dir.join("settings.json"),
r#"{"env":{"DATADOG_API_KEY":"api","DATADOG_APP_KEY":"app","DATADOG_SITE":"datadoghq.com"}}"#,
)
.unwrap();
std::env::set_var(DATADOG_API_KEY, "api");
std::env::set_var(DATADOG_APP_KEY, "app");
std::env::set_var(DATADOG_API_URL, server.uri());
let cmd = GetCommand {
id: "abc".into(),
output: OutputFormat::Json,
};
cmd.execute().await.unwrap();
}
}