From c86d890c2cbbbe87fde58664c68c91b698862044 Mon Sep 17 00:00:00 2001 From: Paul Campbell Date: Thu, 18 Jul 2024 20:51:47 +0100 Subject: [PATCH] feat: enable configuration of a webhook for receiving notifications --- crates/config/src/server.rs | 106 +++++++++++++++++- crates/config/src/tests.rs | 29 ++++- crates/forge-forgejo/src/tests.rs | 2 +- crates/forge-github/src/tests/mod.rs | 4 +- crates/repo-actor/src/lib.rs | 4 +- crates/repo-actor/src/tests/given.rs | 6 +- crates/repo-actor/src/tests/mod.rs | 4 +- .../src/handlers/receive_server_config.rs | 2 +- .../handlers/receive_valid_server_config.rs | 7 +- crates/server-actor/src/lib.rs | 6 +- .../src/tests/receive_server_config.rs | 6 +- crates/server/server-default.toml | 4 + 12 files changed, 152 insertions(+), 28 deletions(-) diff --git a/crates/config/src/server.rs b/crates/config/src/server.rs index b042889b..e02ba71d 100644 --- a/crates/config/src/server.rs +++ b/crates/config/src/server.rs @@ -8,6 +8,7 @@ use std::{ }; use kxio::fs::FileSystem; +use secrecy::Secret; use tracing::info; use crate::{newtype, ForgeAlias, ForgeConfig, RepoAlias}; @@ -41,7 +42,8 @@ type Result = core::result::Result; )] pub struct ServerConfig { http: Http, - webhook: Webhook, + webhook: InboundWebhook, + notification: Notification, storage: ServerStorage, pub forge: BTreeMap, } @@ -64,7 +66,11 @@ impl ServerConfig { &self.storage } - pub const fn webhook(&self) -> &Webhook { + pub const fn notification(&self) -> &Notification { + &self.notification + } + + pub const fn inbound_webhook(&self) -> &InboundWebhook { &self.webhook } @@ -113,10 +119,10 @@ impl Http { serde::Deserialize, derive_more::Constructor, )] -pub struct Webhook { +pub struct InboundWebhook { url: String, } -impl Webhook { +impl InboundWebhook { pub fn url(&self, forge_alias: &ForgeAlias, repo_alias: &RepoAlias) -> WebhookUrl { let base_url = &self.url; WebhookUrl(format!("{base_url}/{forge_alias}/{repo_alias}")) @@ -149,3 +155,95 @@ impl ServerStorage { self.path.as_path() } } + +/// Identifier for the type of Notification +#[derive( + Clone, + Default, + Debug, + derive_more::From, + PartialEq, + Eq, + PartialOrd, + Ord, + serde::Deserialize, + Copy, +)] +pub enum NotificationType { + #[default] + None, + Webhook, +} + +/// Defines the Webhook Forges should send updates to +/// Must be an address that is accessible from the remote forge +#[derive( + Clone, + Debug, + derive_more::From, + PartialEq, + Eq, + PartialOrd, + Ord, + derive_more::AsRef, + serde::Deserialize, +)] +pub struct Notification { + r#type: NotificationType, + webhook: Option, +} +impl Notification { + pub const fn none() -> Self { + Self { + r#type: NotificationType::None, + webhook: None, + } + } + pub const fn webhook(webhook: OutboundWebhook) -> Self { + Self { + r#type: NotificationType::Webhook, + webhook: Some(webhook), + } + } + + pub const fn r#type(&self) -> NotificationType { + self.r#type + } + + pub fn webhook_url(&self) -> Option { + self.webhook.clone().map(|x| x.url) + } + + pub fn webhook_secret(&self) -> Option> { + self.webhook.clone().map(|x| x.secret).map(Secret::new) + } +} +impl Default for Notification { + fn default() -> Self { + Self::none() + } +} + +#[derive( + Clone, + Debug, + derive_more::From, + PartialEq, + Eq, + PartialOrd, + Ord, + serde::Deserialize, + derive_more::Constructor, +)] +pub struct OutboundWebhook { + url: String, + secret: String, +} +impl OutboundWebhook { + pub fn url(&self) -> &str { + self.url.as_ref() + } + pub fn secret(&self) -> Secret { + Secret::new(self.secret.clone()) + } +} diff --git a/crates/config/src/tests.rs b/crates/config/src/tests.rs index 16e337ef..5e81ee6d 100644 --- a/crates/config/src/tests.rs +++ b/crates/config/src/tests.rs @@ -7,9 +7,9 @@ use std::collections::BTreeMap; use std::path::PathBuf; use crate::server::Http; +use crate::server::InboundWebhook; use crate::server::ServerConfig; use crate::server::ServerStorage; -use crate::server::Webhook; use crate::webhook::push::Branch; mod url; @@ -468,8 +468,14 @@ mod server { let http = &server_config.http()?; let http_addr = http.ip(); let http_port = server_config.http()?.port(); - let webhook_url = server_config.webhook().base_url(); + let webhook_url = server_config.inbound_webhook().base_url(); let storage_path = server_config.storage().path(); + let notification = &server_config.notification(); + let notification_webhook_url = notification.webhook_url().unwrap_or_default(); + let notification_webhook_secret = notification + .webhook_secret() + .map(|secret| secret.expose_secret().clone()) + .unwrap_or_default(); let forge_alias = server_config .forges() .next() @@ -519,6 +525,10 @@ url = "{webhook_url}" [storage] path = {storage_path:?} +[notification] +type = "Webhook" +webhook = {{ url = "{notification_webhook_url}", secret = "{notification_webhook_secret}" }} + [forge.{forge_alias}] forge_type = "{forge_type}" hostname = "{forge_hostname}" @@ -687,6 +697,8 @@ mod push { } mod given { + use crate::server::{Notification, OutboundWebhook}; + use super::*; use rand::Rng as _; use std::{ @@ -709,7 +721,8 @@ mod given { pub fn a_server_config() -> ServerConfig { ServerConfig::new( an_http(), - a_webhook(), + an_inbound_webhook(), + a_notification_config(), a_server_storage(), some_forge_configs(), ) @@ -730,12 +743,18 @@ mod given { pub fn a_port() -> u16 { rand::thread_rng().gen() } - pub fn a_webhook() -> Webhook { - Webhook::new(a_name()) + pub fn an_inbound_webhook() -> InboundWebhook { + InboundWebhook::new(a_name()) + } + pub fn an_outbound_webhook() -> OutboundWebhook { + OutboundWebhook::new(a_name(), a_name()) } pub fn a_server_storage() -> ServerStorage { ServerStorage::new(a_name().into()) } + pub fn a_notification_config() -> Notification { + Notification::webhook(an_outbound_webhook()) + } pub fn some_forge_configs() -> BTreeMap { [(a_name(), a_forge_config())].into() } diff --git a/crates/forge-forgejo/src/tests.rs b/crates/forge-forgejo/src/tests.rs index 0baeb1ab..2feeef31 100644 --- a/crates/forge-forgejo/src/tests.rs +++ b/crates/forge-forgejo/src/tests.rs @@ -703,7 +703,7 @@ mod forgejo { forge_alias: &config::ForgeAlias, repo_alias: &config::RepoAlias, ) -> git_next_config::server::WebhookUrl { - config::server::Webhook::new(a_name()).url(forge_alias, repo_alias) + config::server::InboundWebhook::new(a_name()).url(forge_alias, repo_alias) } pub fn any_webhook_url() -> git_next_config::server::WebhookUrl { diff --git a/crates/forge-github/src/tests/mod.rs b/crates/forge-github/src/tests/mod.rs index 50798539..986211f9 100644 --- a/crates/forge-github/src/tests/mod.rs +++ b/crates/forge-github/src/tests/mod.rs @@ -510,7 +510,7 @@ mod github { use std::path::PathBuf; - use git_next_config::{server::Webhook, WebhookId}; + use git_next_config::{server::InboundWebhook, WebhookId}; use kxio::network::{MockNetwork, StatusCode}; use rand::RngCore; use serde_json::json; @@ -648,7 +648,7 @@ mod github { forge_alias: &ForgeAlias, repo_alias: &RepoAlias, ) -> git_next_config::server::WebhookUrl { - Webhook::new(a_name()).url(forge_alias, repo_alias) + InboundWebhook::new(a_name()).url(forge_alias, repo_alias) } pub fn any_webhook_url() -> git_next_config::server::WebhookUrl { given::a_webhook_url(&given::a_forge_alias(), &given::a_repo_alias()) diff --git a/crates/repo-actor/src/lib.rs b/crates/repo-actor/src/lib.rs index 24caf4f9..91b6f961 100644 --- a/crates/repo-actor/src/lib.rs +++ b/crates/repo-actor/src/lib.rs @@ -37,7 +37,7 @@ pub struct RepoActor { generation: git::Generation, message_token: messages::MessageToken, repo_details: git::RepoDetails, - webhook: config::server::Webhook, + webhook: config::server::InboundWebhook, webhook_id: Option, // INFO: if [None] then no webhook is configured webhook_auth: Option, // INFO: if [None] then no webhook is configured last_main_commit: Option, @@ -53,7 +53,7 @@ impl RepoActor { pub fn new( repo_details: git::RepoDetails, forge: Box, - webhook: config::server::Webhook, + webhook: config::server::InboundWebhook, generation: git::Generation, net: Network, repository_factory: Box, diff --git a/crates/repo-actor/src/tests/given.rs b/crates/repo-actor/src/tests/given.rs index 84f0fc38..4d56efa4 100644 --- a/crates/repo-actor/src/tests/given.rs +++ b/crates/repo-actor/src/tests/given.rs @@ -57,7 +57,7 @@ pub fn a_webhook_url( forge_alias: &ForgeAlias, repo_alias: &RepoAlias, ) -> git_next_config::server::WebhookUrl { - config::server::Webhook::new(a_name()).url(forge_alias, repo_alias) + config::server::InboundWebhook::new(a_name()).url(forge_alias, repo_alias) } pub fn a_name() -> String { @@ -179,8 +179,8 @@ pub fn a_message_token() -> MessageToken { MessageToken::default() } -pub fn a_webhook(url: &WebhookUrl) -> Webhook { - Webhook::new(url.clone().into()) +pub fn a_webhook(url: &WebhookUrl) -> InboundWebhook { + InboundWebhook::new(url.clone().into()) } pub fn a_forge() -> Box { diff --git a/crates/repo-actor/src/tests/mod.rs b/crates/repo-actor/src/tests/mod.rs index d6a003db..b92f3e1c 100644 --- a/crates/repo-actor/src/tests/mod.rs +++ b/crates/repo-actor/src/tests/mod.rs @@ -7,7 +7,7 @@ use actor::{ }; use assert2::let_assert; use config::{ - server::{Webhook, WebhookUrl}, + server::{InboundWebhook, WebhookUrl}, webhook::forge_notification::Body, BranchName, ForgeAlias, ForgeConfig, ForgeNotification, ForgeType, GitDir, RegisteredWebhook, RepoAlias, RepoBranches, RepoConfig, ServerRepoConfig, WebhookAuth, WebhookId, @@ -82,7 +82,7 @@ pub struct RepoActorView { pub generation: git::Generation, pub message_token: MessageToken, pub repo_details: git::RepoDetails, - pub webhook: config::server::Webhook, + pub webhook: config::server::InboundWebhook, pub webhook_id: Option, // INFO: if [None] then no webhook is configured pub webhook_auth: Option, // INFO: if [None] then no webhook is configured pub last_main_commit: Option, diff --git a/crates/server-actor/src/handlers/receive_server_config.rs b/crates/server-actor/src/handlers/receive_server_config.rs index 9ac9c03c..028e80a5 100644 --- a/crates/server-actor/src/handlers/receive_server_config.rs +++ b/crates/server-actor/src/handlers/receive_server_config.rs @@ -21,7 +21,7 @@ impl Handler for Server { return; }; - if msg.webhook().base_url().ends_with('/') { + if msg.inbound_webhook().base_url().ends_with('/') { tracing::error!("webhook.url must not end with a '/'"); return; } diff --git a/crates/server-actor/src/handlers/receive_valid_server_config.rs b/crates/server-actor/src/handlers/receive_valid_server_config.rs index 0fca4995..f74d6567 100644 --- a/crates/server-actor/src/handlers/receive_valid_server_config.rs +++ b/crates/server-actor/src/handlers/receive_valid_server_config.rs @@ -15,14 +15,15 @@ impl Handler for Server { socket_address, server_storage, } = msg.unwrap(); - if let Some(webhook) = self.webhook.take() { - webhook.do_send(ShutdownWebhook); + // shutdown any existing webhook actor + if let Some(webhook_actor_addr) = self.webhook.take() { + webhook_actor_addr.do_send(ShutdownWebhook); } self.generation.inc(); // Webhook Server tracing::info!("Starting Webhook Server..."); let webhook_router = WebhookRouter::default().start(); - let webhook = server_config.webhook(); + let webhook = server_config.inbound_webhook(); // Forge Actors for (forge_alias, forge_config) in server_config.forges() { let repo_actors = self diff --git a/crates/server-actor/src/lib.rs b/crates/server-actor/src/lib.rs index 17257f4d..47bae97d 100644 --- a/crates/server-actor/src/lib.rs +++ b/crates/server-actor/src/lib.rs @@ -7,7 +7,7 @@ pub mod messages; use actix::prelude::*; use git_next_config as config; -use git_next_config::server::{ServerConfig, ServerStorage, Webhook}; +use git_next_config::server::{InboundWebhook, ServerConfig, ServerStorage}; use git_next_config::{ForgeAlias, ForgeConfig, GitDir, RepoAlias, ServerRepoConfig}; use git_next_git::{Generation, RepoDetails}; use git_next_repo_actor::{messages::CloneRepo, RepoActor}; @@ -104,7 +104,7 @@ impl Server { forge_config: &ForgeConfig, forge_name: ForgeAlias, server_storage: &ServerStorage, - webhook: &Webhook, + webhook: &InboundWebhook, ) -> Vec<(ForgeAlias, RepoAlias, RepoActor)> { let span = tracing::info_span!("create_forge_repos", name = %forge_name, config = %forge_config); @@ -129,7 +129,7 @@ impl Server { forge_name: ForgeAlias, forge_config: ForgeConfig, server_storage: &ServerStorage, - webhook: &Webhook, + webhook: &InboundWebhook, ) -> impl Fn((RepoAlias, &ServerRepoConfig)) -> (ForgeAlias, RepoAlias, RepoActor) { let server_storage = server_storage.clone(); let webhook = webhook.clone(); diff --git a/crates/server-actor/src/tests/receive_server_config.rs b/crates/server-actor/src/tests/receive_server_config.rs index a5719880..de3f483b 100644 --- a/crates/server-actor/src/tests/receive_server_config.rs +++ b/crates/server-actor/src/tests/receive_server_config.rs @@ -1,7 +1,7 @@ // use crate::{tests::given, ReceiveServerConfig, Server}; use actix::prelude::*; -use git_next_config::server::{Http, ServerConfig, ServerStorage, Webhook}; +use git_next_config::server::{Http, InboundWebhook, Notification, ServerConfig, ServerStorage}; use std::{ collections::BTreeMap, sync::{Arc, RwLock}, @@ -21,7 +21,8 @@ async fn when_webhook_url_has_trailing_slash_should_not_send() { // collaborators let http = Http::new("0.0.0.0".to_string(), 80); - let webhook = Webhook::new("http://localhost/".to_string()); // With trailing slash + let webhook = InboundWebhook::new("http://localhost/".to_string()); // With trailing slash + let notifications = Notification::none(); let server_storage = ServerStorage::new((fs.base()).to_path_buf()); let repos = BTreeMap::default(); @@ -35,6 +36,7 @@ async fn when_webhook_url_has_trailing_slash_should_not_send() { .do_send(ReceiveServerConfig::new(ServerConfig::new( http, webhook, + notifications, server_storage, repos, ))); diff --git a/crates/server/server-default.toml b/crates/server/server-default.toml index 2e87e42b..eedef5e4 100644 --- a/crates/server/server-default.toml +++ b/crates/server/server-default.toml @@ -8,6 +8,10 @@ url = "https://localhost:8080" # don't include any query path or a trailing slas [storage] path = "./data" +[notifications] +type = "WebHook" +webhook = { url = "https://localhost:9090" } + [forge] [forge.default]