use std::collections::HashMap;
use std::future::Future;
use crate::effects::TerminalEffects;
use crate::keys::RunId;
use crate::runner::StepError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TerminalStatus {
Succeeded,
Failed,
Cancelled,
}
impl TerminalStatus {
pub fn as_str(&self) -> &'static str {
match self {
TerminalStatus::Succeeded => "succeeded",
TerminalStatus::Failed => "failed",
TerminalStatus::Cancelled => "cancelled",
}
}
}
impl std::fmt::Display for TerminalStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone)]
pub struct RunOutcome {
pub run_id: RunId,
pub status: TerminalStatus,
pub result: Option<Vec<u8>>,
pub error: Option<String>,
pub headers: HashMap<String, String>,
pub final_step: u32,
}
pub trait TerminalHook: Send + Sync {
fn on_termination(
&self,
outcome: &RunOutcome,
effects: &TerminalEffects,
) -> impl Future<Output = std::result::Result<(), StepError>> + Send;
fn observes(&self, outcome: &RunOutcome) -> bool {
let _ = outcome;
true
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct NoopTerminalHook;
impl TerminalHook for NoopTerminalHook {
async fn on_termination(
&self,
_outcome: &RunOutcome,
_effects: &TerminalEffects,
) -> std::result::Result<(), StepError> {
Ok(())
}
fn observes(&self, _outcome: &RunOutcome) -> bool {
false
}
}
#[cfg(feature = "webhooks")]
mod webhook {
use super::{RunOutcome, StepError, TerminalEffects, TerminalHook, TerminalStatus};
use std::time::Duration;
use taquba_webhooks::{WebhookRequest, webhook_enqueue_request};
pub struct WebhookTerminalHook {
target_queue: String,
url_header: String,
timeout: Option<Duration>,
}
impl WebhookTerminalHook {
pub const URL_HEADER: &'static str = "callback_url";
pub fn new(target_queue: impl Into<String>) -> Self {
Self {
target_queue: target_queue.into(),
url_header: Self::URL_HEADER.to_string(),
timeout: None,
}
}
pub fn with_url_header(mut self, header: impl Into<String>) -> Self {
self.url_header = header.into();
self
}
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
}
impl TerminalHook for WebhookTerminalHook {
async fn on_termination(
&self,
outcome: &RunOutcome,
effects: &TerminalEffects,
) -> std::result::Result<(), StepError> {
let Some(url) = outcome.headers.get(&self.url_header) else {
return Ok(());
};
let mut req = WebhookRequest::new(url)
.header("Workflow-Run-Id", outcome.run_id.as_str())
.header("Workflow-Run-Status", outcome.status.as_str());
if let Some(t) = self.timeout {
req = req.timeout(t);
}
let body = match outcome.status {
TerminalStatus::Succeeded => outcome.result.clone().unwrap_or_default(),
TerminalStatus::Failed | TerminalStatus::Cancelled => {
outcome.error.clone().unwrap_or_default().into_bytes()
}
};
let request = webhook_enqueue_request(&self.target_queue, req, body);
effects
.enqueue(request)
.map_err(|e| StepError::permanent(e.to_string()))?;
Ok(())
}
fn observes(&self, outcome: &RunOutcome) -> bool {
outcome.headers.contains_key(&self.url_header)
}
}
}
#[cfg(feature = "webhooks")]
pub use webhook::WebhookTerminalHook;