Skip to content
Merged
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
10 changes: 9 additions & 1 deletion src/auth/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ pub fn read_only_scopes() -> Vec<&'static str> {
"timeseries_query",
"usage_read",
"user_access_read",
"workflows_read",
]
}

Expand Down Expand Up @@ -225,6 +226,10 @@ pub fn default_scopes() -> Vec<&'static str> {
"usage_read",
// Users
"user_access_read",
// Workflows
"workflows_read",
"workflows_run",
"workflows_write",
]
}

Expand Down Expand Up @@ -271,7 +276,6 @@ mod tests {
#[test]
fn test_default_scopes() {
let scopes = default_scopes();
assert_eq!(scopes.len(), 85);
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Dropped the assert_eq!(scopes.len(), N); assertion that used to live here.

Several PRs in flight all add or remove scopes in default_scopes(), and each one bumps that hardcoded count. The result was every concurrent PR landing turned into a trivial 3-way merge resolution against the count.

The containment asserts below still cover the scopes this PR cares about. If we ever want a non-numeric sanity check (no duplicates, alphabetical, etc.) that doesn't rot on every scope change, we can add it separately.

assert!(scopes.contains(&"dashboards_read"));
assert!(scopes.contains(&"monitors_read"));
assert!(scopes.contains(&"logs_read_data"));
Expand All @@ -290,6 +294,10 @@ mod tests {
assert!(scopes.contains(&"on_call_write"));
assert!(scopes.contains(&"aws_configuration_read"));
assert!(scopes.contains(&"gcp_configuration_read"));
// Workflows
assert!(scopes.contains(&"workflows_read"));
assert!(scopes.contains(&"workflows_run"));
assert!(scopes.contains(&"workflows_write"));
}

#[test]
Expand Down
10 changes: 5 additions & 5 deletions src/commands/workflows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ use crate::formatter::{self, Metadata};
use crate::util;

// ---------------------------------------------------------------------------
// Helper: build a WorkflowAutomationAPI (API key auth only)
// Helper: build a WorkflowAutomationAPI
// ---------------------------------------------------------------------------

fn make_api(cfg: &Config) -> WorkflowAutomationAPI {
crate::make_api_no_auth!(WorkflowAutomationAPI, cfg)
crate::make_api!(WorkflowAutomationAPI, cfg)
}

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -63,7 +63,7 @@ pub async fn delete(cfg: &Config, workflow_id: &str) -> Result<()> {
}

// ---------------------------------------------------------------------------
// Workflow execution (API trigger only — requires DD_API_KEY + DD_APP_KEY)
// Workflow execution
// ---------------------------------------------------------------------------

pub async fn run(
Expand All @@ -74,7 +74,7 @@ pub async fn run(
wait: bool,
timeout: &str,
) -> Result<()> {
let api = crate::make_api_no_auth!(WorkflowAutomationAPI, cfg);
let api = crate::make_api!(WorkflowAutomationAPI, cfg);

let input_payload: Option<BTreeMap<String, serde_json::Value>> = match (&payload, &payload_file)
{
Expand Down Expand Up @@ -132,7 +132,7 @@ pub async fn run(

tokio::time::sleep(std::time::Duration::from_secs(2)).await;

let api = crate::make_api_no_auth!(WorkflowAutomationAPI, cfg);
let api = crate::make_api!(WorkflowAutomationAPI, cfg);
let status = api
.get_workflow_instance(workflow_id.to_string(), instance_id.clone())
.await
Expand Down
147 changes: 67 additions & 80 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2708,7 +2708,7 @@ enum Commands {
/// CAPABILITIES:
/// • Get workflow details
/// • Create, update, and delete workflows
/// • Execute workflows via API trigger (requires DD_API_KEY + DD_APP_KEY)
/// • Execute workflows via API trigger
/// • List, inspect, and cancel workflow instances (executions)
///
/// EXAMPLES:
Expand All @@ -2728,8 +2728,12 @@ enum Commands {
/// pup workflows instances cancel <workflow-id> <instance-id>
///
/// AUTHENTICATION:
/// All workflow commands require DD_API_KEY + DD_APP_KEY.
/// OAuth2 bearer tokens are not supported for workflow operations at this time.
/// Workflow CRUD (`workflows get/create/update/delete`),
/// `workflows run`, and `workflows instances *` accept OAuth2
/// (`pup auth login`) or DD_API_KEY + DD_APP_KEY.
/// `workflows connections *` requires DD_API_KEY + DD_APP_KEY
/// pending server-side OAuth enablement on the action-connections
/// API.
#[command(verbatim_doc_comment)]
Workflows {
#[command(subcommand)]
Expand Down Expand Up @@ -4259,10 +4263,11 @@ enum WorkflowActions {
},
/// Delete a workflow
Delete { workflow_id: String },
/// Execute a workflow via API trigger (requires DD_API_KEY + DD_APP_KEY)
/// Execute a workflow via API trigger
///
/// The workflow must have an API trigger configured.
/// OAuth tokens are not supported — this command requires API key authentication.
/// The workflow must have an API trigger configured. Accepts OAuth2
/// (`pup auth login`, requires the `workflows_run` scope) or
/// DD_API_KEY + DD_APP_KEY.
#[command(verbatim_doc_comment)]
Run {
workflow_id: String,
Expand Down Expand Up @@ -14413,86 +14418,68 @@ async fn main_inner() -> anyhow::Result<()> {
AuthActions::Test => commands::test::run(&cfg)?,
},
// --- Workflows ---
Commands::Workflows { action } => {
cfg.validate_api_and_app_keys().map_err(|_| {
anyhow::anyhow!(
"workflow commands require DD_API_KEY and DD_APP_KEY with workflow_* scopes\n\
OAuth2 bearer tokens are not supported for workflow operations.\n\
See: https://docs.datadoghq.com/api/latest/workflow-automation"
)
})?;
match action {
WorkflowActions::Get { workflow_id } => {
commands::workflows::get(&cfg, &workflow_id).await?;
Commands::Workflows { action } => match action {
WorkflowActions::Get { workflow_id } => {
commands::workflows::get(&cfg, &workflow_id).await?;
}
WorkflowActions::Create { file } => {
commands::workflows::create(&cfg, &file).await?;
}
WorkflowActions::Update { workflow_id, file } => {
commands::workflows::update(&cfg, &workflow_id, &file).await?;
}
WorkflowActions::Delete { workflow_id } => {
commands::workflows::delete(&cfg, &workflow_id).await?;
}
WorkflowActions::Run {
workflow_id,
payload,
payload_file,
wait,
timeout,
} => {
commands::workflows::run(&cfg, &workflow_id, payload, payload_file, wait, &timeout)
.await?;
}
WorkflowActions::Instances { action } => match action {
WorkflowInstanceActions::List {
workflow_id,
limit,
page,
} => {
commands::workflows::instance_list(&cfg, &workflow_id, limit, page).await?;
}
WorkflowInstanceActions::Get {
workflow_id,
instance_id,
} => {
commands::workflows::instance_get(&cfg, &workflow_id, &instance_id).await?;
}
WorkflowActions::Create { file } => {
commands::workflows::create(&cfg, &file).await?;
WorkflowInstanceActions::Cancel {
workflow_id,
instance_id,
} => {
commands::workflows::instance_cancel(&cfg, &workflow_id, &instance_id).await?;
}
WorkflowActions::Update { workflow_id, file } => {
commands::workflows::update(&cfg, &workflow_id, &file).await?;
},
WorkflowActions::Connections { action } => match action {
WorkflowConnectionActions::Get { connection_id } => {
commands::workflows::connections_get(&cfg, &connection_id).await?;
}
WorkflowActions::Delete { workflow_id } => {
commands::workflows::delete(&cfg, &workflow_id).await?;
WorkflowConnectionActions::Create { file } => {
commands::workflows::connections_create(&cfg, &file).await?;
}
WorkflowActions::Run {
workflow_id,
payload,
payload_file,
wait,
timeout,
WorkflowConnectionActions::Update {
connection_id,
file,
} => {
commands::workflows::run(
&cfg,
&workflow_id,
payload,
payload_file,
wait,
&timeout,
)
.await?;
commands::workflows::connections_update(&cfg, &connection_id, &file).await?;
}
WorkflowActions::Instances { action } => match action {
WorkflowInstanceActions::List {
workflow_id,
limit,
page,
} => {
commands::workflows::instance_list(&cfg, &workflow_id, limit, page).await?;
}
WorkflowInstanceActions::Get {
workflow_id,
instance_id,
} => {
commands::workflows::instance_get(&cfg, &workflow_id, &instance_id).await?;
}
WorkflowInstanceActions::Cancel {
workflow_id,
instance_id,
} => {
commands::workflows::instance_cancel(&cfg, &workflow_id, &instance_id)
.await?;
}
},
WorkflowActions::Connections { action } => match action {
WorkflowConnectionActions::Get { connection_id } => {
commands::workflows::connections_get(&cfg, &connection_id).await?;
}
WorkflowConnectionActions::Create { file } => {
commands::workflows::connections_create(&cfg, &file).await?;
}
WorkflowConnectionActions::Update {
connection_id,
file,
} => {
commands::workflows::connections_update(&cfg, &connection_id, &file)
.await?;
}
WorkflowConnectionActions::Delete { connection_id } => {
commands::workflows::connections_delete(&cfg, &connection_id).await?;
}
},
}
}
WorkflowConnectionActions::Delete { connection_id } => {
commands::workflows::connections_delete(&cfg, &connection_id).await?;
}
},
},
// --- LLM Observability ---
Commands::LlmObs { action } => {
cfg.validate_auth()?;
Expand Down
Loading