use std::path::Path;
pub(super) fn send_webhook(url: &str, payload: &str) {
match std::process::Command::new("curl")
.args([
"-sf",
"-X",
"POST",
"-H",
"Content-Type: application/json",
"-d",
payload,
url,
])
.output()
{
Ok(o) if !o.status.success() => {
eprintln!(
"warn: webhook to {} failed (exit {})",
url,
o.status.code().unwrap_or(-1)
);
}
Err(e) => eprintln!("warn: webhook to {url} error: {e}"),
_ => {}
}
}
pub(super) fn send_webhook_with_header(url: &str, header: &str, payload: &str) {
match std::process::Command::new("curl")
.args([
"-sf",
"-X",
"POST",
"-H",
"Content-Type: application/json",
"-H",
header,
"-d",
payload,
url,
])
.output()
{
Ok(o) if !o.status.success() => {
eprintln!(
"warn: webhook to {} failed (exit {})",
url,
o.status.code().unwrap_or(-1)
);
}
Err(e) => eprintln!("warn: webhook to {url} error: {e}"),
_ => {}
}
}
pub(super) fn event_json(status: &str, config: &Path) -> String {
super::apply_gates::format_event_json(status, &config.display().to_string())
}
pub(super) fn publish_stdin(cmd: &str, args: &[&str], message: &str) {
let child = std::process::Command::new(cmd)
.args(args)
.stdin(std::process::Stdio::piped())
.spawn();
if let Ok(mut c) = child {
if let Some(ref mut stdin) = c.stdin {
use std::io::Write;
let _ = stdin.write_all(message.as_bytes());
}
let _ = c.wait();
}
}
pub(crate) struct NotifyOpts<'a> {
pub slack: Option<&'a str>,
pub email: Option<&'a str>,
pub webhook: Option<&'a str>,
pub teams: Option<&'a str>,
pub discord: Option<&'a str>,
pub opsgenie: Option<&'a str>,
pub datadog: Option<&'a str>,
pub newrelic: Option<&'a str>,
pub grafana: Option<&'a str>,
pub victorops: Option<&'a str>,
pub msteams_adaptive: Option<&'a str>,
pub incident: Option<&'a str>,
pub sns: Option<&'a str>,
pub pubsub: Option<&'a str>,
pub eventbridge: Option<&'a str>,
pub kafka: Option<&'a str>,
pub azure_servicebus: Option<&'a str>,
pub gcp_pubsub_v2: Option<&'a str>,
pub rabbitmq: Option<&'a str>,
pub nats: Option<&'a str>,
pub mqtt: Option<&'a str>,
pub redis: Option<&'a str>,
pub amqp: Option<&'a str>,
pub stomp: Option<&'a str>,
pub zeromq: Option<&'a str>,
pub grpc: Option<&'a str>,
pub sqs: Option<&'a str>,
pub mattermost: Option<&'a str>,
pub ntfy: Option<&'a str>,
pub pagerduty: Option<&'a str>,
pub discord_webhook: Option<&'a str>,
pub teams_webhook: Option<&'a str>,
pub slack_blocks: Option<&'a str>,
pub custom_template: Option<&'a str>,
pub custom_webhook: Option<&'a str>,
pub custom_headers: Option<&'a str>,
pub custom_json: Option<&'a str>,
pub custom_filter: Option<&'a str>,
pub custom_retry: Option<&'a str>,
pub custom_transform: Option<&'a str>,
pub custom_batch: Option<&'a str>,
pub custom_deduplicate: Option<&'a str>,
pub custom_throttle: Option<&'a str>,
pub custom_aggregate: Option<&'a str>,
pub custom_priority: Option<&'a str>,
pub custom_routing: Option<&'a str>,
pub custom_dedup_window: Option<&'a str>,
pub custom_rate_limit: Option<&'a str>,
pub custom_backoff: Option<&'a str>,
pub custom_circuit_breaker: Option<&'a str>,
pub custom_dead_letter: Option<&'a str>,
pub custom_escalation: Option<&'a str>,
pub custom_correlation: Option<&'a str>,
pub custom_sampling: Option<&'a str>,
pub custom_digest: Option<&'a str>,
pub custom_severity_filter: Option<&'a str>,
}
pub(crate) fn send_apply_notifications(
opts: &NotifyOpts<'_>,
result: &Result<(), String>,
config_path: &Path,
) {
let status = super::apply_gates::notify_status(result);
let msg = event_json(status, config_path);
send_webhook_notifications(opts, status, config_path);
send_monitoring_notifications(opts, status, config_path);
send_incident_notifications(opts, result, config_path);
send_email_notification(opts.email, status, config_path);
send_cloud_notifications(opts, &msg);
send_broker_notifications(opts, &msg);
}
pub(super) fn send_webhook_notifications(opts: &NotifyOpts<'_>, status: &str, config: &Path) {
if let Some(url) = opts.slack {
send_webhook(
url,
&format!(
r#"{{"text":"forjar apply {}: {}"}}"#,
status,
config.display()
),
);
}
if let Some(url) = opts.webhook {
send_webhook(
url,
&format!(
r#"{{"event":"apply_complete","status":"{}","config":"{}"}}"#,
status,
config.display()
),
);
}
if let Some(url) = opts.teams {
send_webhook(
url,
&format!(
r#"{{"@type":"MessageCard","summary":"forjar apply {}","text":"Apply {} for {}"}}"#,
status,
status,
config.display()
),
);
}
if let Some(url) = opts.discord {
send_webhook(
url,
&format!(
r#"{{"content":"forjar apply {}: {}"}}"#,
status,
config.display()
),
);
}
if let Some(url) = opts.msteams_adaptive {
send_webhook(
url,
&format!(
r#"{{"type":"message","attachments":[{{"contentType":"application/vnd.microsoft.card.adaptive","content":{{"type":"AdaptiveCard","body":[{{"type":"TextBlock","text":"Forjar Apply: {}","weight":"bolder"}},{{"type":"TextBlock","text":"Config: {}"}}],"$schema":"http://adaptivecards.io/schemas/adaptive-card.json","version":"1.4"}}}}]}}"#,
status,
config.display()
),
);
}
if let Some(url) = opts.mattermost {
send_webhook(
url,
&format!(
r#"{{"text":"forjar apply {}: {}"}}"#,
status,
config.display()
),
);
}
if let Some(topic) = opts.ntfy {
let url = format!("https://ntfy.sh/{topic}");
let msg = format!("forjar apply {}: {}", status, config.display());
if let Err(e) = std::process::Command::new("curl")
.args(["-sf", "-d", &msg, &url])
.output()
{
eprintln!("warning: ntfy notification error: {e}");
}
}
if let Some(url) = opts.grafana {
let ts = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
send_webhook(
url,
&format!(
r#"{{"text":"forjar apply {status}","tags":["forjar","deploy"],"time":{ts}}}"#
),
);
}
}
pub(super) fn send_monitoring_notifications(opts: &NotifyOpts<'_>, status: &str, config: &Path) {
if let Some(key) = opts.opsgenie {
send_webhook_with_header(
"https://api.opsgenie.com/v2/alerts",
&format!("Authorization: GenieKey {key}"),
&format!(
r#"{{"message":"forjar apply {}","description":"Apply {} for {}","priority":"P3"}}"#,
status,
status,
config.display()
),
);
}
if let Some(key) = opts.datadog {
send_webhook_with_header(
"https://api.datadoghq.com/api/v1/events",
&format!("DD-API-KEY: {key}"),
&format!(
r#"{{"title":"forjar apply {}","text":"Apply {} for {}","alert_type":"info"}}"#,
status,
status,
config.display()
),
);
}
if let Some(key) = opts.newrelic {
send_webhook_with_header(
"https://insights-collector.newrelic.com/v1/accounts/events",
&format!("Api-Key: {key}"),
&format!(
r#"{{"eventType":"ForjarApply","status":"{}","config":"{}"}}"#,
status,
config.display()
),
);
}
}
pub(super) fn send_incident_notifications(
opts: &NotifyOpts<'_>,
result: &Result<(), String>,
config: &Path,
) {
if let Some(key) = opts.victorops {
let (vo_status, verb) = super::apply_gates::victorops_status(result);
send_webhook(
&format!(
"https://alert.victorops.com/integrations/generic/20131114/alert/{key}/forjar"
),
&format!(
r#"{{"message_type":"{}","entity_display_name":"forjar apply","state_message":"Apply {} for {}"}}"#,
vo_status,
verb,
config.display()
),
);
}
if let Some(key) = opts.incident {
if result.is_err() {
send_webhook(
"https://events.pagerduty.com/v2/enqueue",
&format!(
r#"{{"routing_key":"{}","event_action":"trigger","payload":{{"summary":"Forjar apply failed: {}","source":"forjar","severity":"critical","component":"infrastructure","group":"apply"}}}}"#,
key,
config.display()
),
);
}
}
send_pagerduty_notification(opts.pagerduty, result, config);
send_discord_webhook_notification(opts.discord_webhook, result, config);
send_teams_webhook_notification(opts.teams_webhook, result, config);
send_slack_blocks_notification(opts.slack_blocks, result, config);
send_custom_template_notification(opts.custom_template, result, config);
send_custom_webhook_notification(opts.custom_webhook, result, config);
send_custom_headers_notification(opts.custom_headers, result, config);
send_custom_json_notification(opts.custom_json, result, config);
send_custom_filter_notification(opts.custom_filter, result, config);
send_custom_retry_notification(opts.custom_retry, result, config);
super::dispatch_notify_custom::send_custom_transform_notification(
opts.custom_transform,
result,
config,
);
super::dispatch_notify_custom::send_custom_batch_notification(
opts.custom_batch,
result,
config,
);
super::dispatch_notify_custom::send_custom_deduplicate_notification(
opts.custom_deduplicate,
result,
config,
);
super::dispatch_notify_custom::send_custom_throttle_notification(
opts.custom_throttle,
result,
config,
);
super::dispatch_notify_custom::send_custom_aggregate_notification(
opts.custom_aggregate,
result,
config,
);
super::dispatch_notify_custom::send_custom_priority_notification(
opts.custom_priority,
result,
config,
);
super::dispatch_notify_custom::send_custom_routing_notification(
opts.custom_routing,
result,
config,
);
super::dispatch_notify_custom::send_custom_dedup_window_notification(
opts.custom_dedup_window,
result,
config,
);
super::dispatch_notify_custom::send_custom_rate_limit_notification(
opts.custom_rate_limit,
result,
config,
);
super::dispatch_notify_custom::send_custom_backoff_notification(
opts.custom_backoff,
result,
config,
);
super::dispatch_notify_custom::send_custom_circuit_breaker_notification(
opts.custom_circuit_breaker,
result,
config,
);
super::dispatch_notify_custom::send_custom_dead_letter_notification(
opts.custom_dead_letter,
result,
config,
);
super::dispatch_notify_custom::send_custom_escalation_notification(
opts.custom_escalation,
result,
config,
);
super::dispatch_notify_custom::send_custom_correlation_notification(
opts.custom_correlation,
result,
config,
);
super::dispatch_notify_custom::send_custom_sampling_notification(
opts.custom_sampling,
result,
config,
);
super::dispatch_notify_custom::send_custom_digest_notification(
opts.custom_digest,
result,
config,
);
super::dispatch_notify_custom::send_custom_severity_filter_notification(
opts.custom_severity_filter,
result,
config,
);
}
pub(super) use super::dispatch_notify_b::*;