use std::io::Write as _;
use anyhow::{Context, Result};
use chrono::{DateTime, Utc};
use clap::{Args, Subcommand};
use ironflow_sdk::IronflowClient;
use ironflow_sdk::types::{ApiKeyScope, CreateApiKeyRequest};
use uuid::Uuid;
use crate::commands::parse_enum;
use crate::confirm::confirm;
use crate::output;
#[derive(Debug, Args)]
pub struct ApiKeyArgs {
#[command(subcommand)]
pub command: ApiKeyCommands,
}
#[derive(Debug, Subcommand)]
pub enum ApiKeyCommands {
List,
Create {
name: String,
#[arg(long = "scope", value_name = "SCOPE", required = true, value_parser = parse_scope)]
scopes: Vec<ApiKeyScope>,
#[arg(long)]
expires_at: Option<DateTime<Utc>>,
},
Scopes,
Delete {
id: Uuid,
#[arg(long)]
yes: bool,
},
}
const ALL_SCOPES: [ApiKeyScope; 6] = [
ApiKeyScope::WorkflowsRead,
ApiKeyScope::RunsRead,
ApiKeyScope::RunsWrite,
ApiKeyScope::RunsManage,
ApiKeyScope::StatsRead,
ApiKeyScope::Admin,
];
fn parse_scope(raw: &str) -> Result<ApiKeyScope, String> {
parse_enum(raw, &ALL_SCOPES, "scope")
}
pub async fn execute(client: &IronflowClient, args: &ApiKeyArgs, json_mode: bool) -> Result<()> {
match &args.command {
ApiKeyCommands::List => {
let response = client.list_api_keys().await?;
output::print_output(json_mode, &response, || {
output::api_keys_table(&response.data)
})?;
}
ApiKeyCommands::Create {
name,
scopes,
expires_at,
} => {
let request: CreateApiKeyRequest = CreateApiKeyRequest::builder()
.name(name.clone())
.scopes(scopes.clone())
.expires_at(*expires_at)
.try_into()
.context("failed to build CreateApiKeyRequest")?;
let response = client.create_api_key(&request).await?;
if !json_mode {
let mut stderr = std::io::stderr();
writeln!(stderr, "This is the only time the key is shown.")?;
}
output::print_output(json_mode, &response, || {
output::created_api_key_table(&response.data)
})?;
}
ApiKeyCommands::Scopes => {
let response = client.available_scopes().await?;
output::print_output(json_mode, &response, || {
output::scopes_table(&response.data)
})?;
}
ApiKeyCommands::Delete { id, yes } => {
confirm(&format!("Delete API key '{id}'?"), *yes)?;
client.delete_api_key(*id).await?;
output::report_deletion(json_mode, "api-key", id.to_string())?;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_scope_accepts_every_declared_scope() {
for scope in ALL_SCOPES {
let raw = scope.to_string();
assert_eq!(parse_scope(&raw).unwrap(), scope);
}
}
#[test]
fn parse_scope_rejects_an_unknown_value_and_lists_the_valid_ones() {
let err = parse_scope("root").unwrap_err();
assert!(err.contains("unknown scope 'root'"), "{err}");
assert!(err.contains("runs_read"), "{err}");
assert!(err.contains("admin"), "{err}");
}
#[test]
fn parse_scope_is_case_sensitive() {
assert!(parse_scope("ADMIN").is_err());
}
}