From dd91aa4f691937110004a059ad4dcff04f987ee7 Mon Sep 17 00:00:00 2001 From: Paul Campbell Date: Sun, 14 Apr 2024 06:48:26 +0100 Subject: [PATCH] feat(server/webhook): implement register webhook Closes kemitix/git-next#15 --- src/server/actors/repo/branch.rs | 20 -------- src/server/actors/repo/mod.rs | 41 +++++++++++----- src/server/actors/repo/status.rs | 4 -- src/server/actors/repo/webhook.rs | 78 ++++++++++++++++++++++++++---- src/server/config.rs | 6 ++- src/server/forge/forgejo/config.rs | 3 +- src/server/mod.rs | 18 ++++--- 7 files changed, 116 insertions(+), 54 deletions(-) diff --git a/src/server/actors/repo/branch.rs b/src/server/actors/repo/branch.rs index 524c18f..2e1a43b 100644 --- a/src/server/actors/repo/branch.rs +++ b/src/server/actors/repo/branch.rs @@ -4,7 +4,6 @@ use kxio::network::{self, Network}; use tracing::{error, info, warn}; use crate::server::{ - actors::repo::ValidateRepo, config::{self, RepoConfig, RepoDetails}, forge::{self, CommitHistories}, git::Git, @@ -27,7 +26,6 @@ pub async fn validate_positions( Ok(commit_histories) => commit_histories, Err(err) => { error!(?err, "Failed to get commit histories"); - revalidate_positions(addr).await; return; } }; @@ -35,20 +33,17 @@ pub async fn validate_positions( // Validations let Some(main) = commit_histories.main.first().cloned() else { warn!("No commits on main branch '{}'", config.branches().main()); - revalidate_positions(addr).await; return; }; // verify that next is an ancestor of dev, and force update to it main if it isn't let Some(next) = commit_histories.next.first().cloned() else { warn!("No commits on next branch '{}", config.branches().next()); - revalidate_positions(addr).await; return; }; let next_is_ancestor_of_dev = commit_histories.dev.iter().any(|dev| dev == &next); if !next_is_ancestor_of_dev { info!("Next is not an ancestor of dev - resetting next to main"); reset_next_to_main(next, main, &repo_details, &config, &git); - revalidate_positions(addr).await; return; } @@ -64,12 +59,10 @@ pub async fn validate_positions( config.branches().next() ); reset_next_to_main(next, main, &repo_details, &config, &git); - revalidate_positions(addr).await; return; } let Some(next) = next_commits.first().cloned() else { warn!("No commits on next branch '{}'", config.branches().next()); - revalidate_positions(addr).await; return; }; let dev_has_next = commit_histories @@ -82,7 +75,6 @@ pub async fn validate_positions( config.branches().dev(), config.branches().next() ); - revalidate_positions(addr).await; return; // dev is not based on next } let dev_has_main = commit_histories.dev.iter().any(|commit| commit == &main); @@ -93,12 +85,10 @@ pub async fn validate_positions( config.branches().main(), config.branches().main(), ); - revalidate_positions(addr).await; return; } let Some(dev) = commit_histories.dev.first().cloned() else { warn!("No commits on dev branch '{}'", config.branches().dev()); - revalidate_positions(addr).await; return; }; addr.do_send(StartMonitoring { @@ -144,12 +134,6 @@ fn reset_next_to_main( } } -async fn revalidate_positions(addr: Addr) { - // TODO : (#43) sleep and restart while we don't have webhooks - tokio::time::sleep(std::time::Duration::from_secs(10)).await; - addr.do_send(ValidateRepo) -} - // advance next to the next commit towards the head of the dev branch #[tracing::instrument(fields(next), skip_all)] pub async fn advance_next( @@ -157,18 +141,15 @@ pub async fn advance_next( dev_commit_history: Vec, repo_details: config::RepoDetails, repo_config: config::RepoConfig, - addr: Addr, git: Git, ) { let next_commit = find_next_commit_on_dev(next, dev_commit_history); let Some(commit) = next_commit else { warn!("No commits to advance next to"); - revalidate_positions(addr).await; return; }; if let Some(problem) = validate_commit_message(commit.message()) { warn!("Can't advance next to commit '{}': {}", commit, problem); - revalidate_positions(addr).await; return; } info!("Advancing next to commit '{}'", commit); @@ -180,7 +161,6 @@ pub async fn advance_next( ) { warn!(?err, "Failed") }; - revalidate_positions(addr).await; } #[tracing::instrument] diff --git a/src/server/actors/repo/mod.rs b/src/server/actors/repo/mod.rs index 5514dfe..8c6ebaf 100644 --- a/src/server/actors/repo/mod.rs +++ b/src/server/actors/repo/mod.rs @@ -8,7 +8,7 @@ use kxio::network::Network; use tracing::{info, warn}; use crate::server::{ - config::{RepoConfig, RepoDetails}, + config::{RepoConfig, RepoDetails, Webhook}, forge, git::Git, }; @@ -17,17 +17,24 @@ use self::webhook::WebhookId; pub struct RepoActor { details: RepoDetails, - config: Option, // INFO: if [None] then send [StartRepo] to populate it + webhook: Webhook, webhook_id: Option, // INFO: if [None] then no webhook is configured + webhook_auth: Option, net: Network, git: Git, } impl RepoActor { - pub(crate) const fn new(details: RepoDetails, net: Network, git: Git) -> Self { + pub(crate) const fn new( + details: RepoDetails, + webhook: Webhook, + net: Network, + git: Git, + ) -> Self { Self { details, - config: None, + webhook, webhook_id: None, + webhook_auth: None, net, git, } @@ -72,7 +79,18 @@ impl Handler for RepoActor { fn handle(&mut self, msg: LoadedConfig, ctx: &mut Self::Context) -> Self::Result { let config = msg.0; info!(%self.details, %config, "Config loaded"); - self.config.replace(config); + self.details.config.replace(config); + if self.webhook_id.is_none() { + info!("lets register the webhook..."); + webhook::register( + self.details.clone(), + self.webhook.clone(), + ctx.address(), + self.net.clone(), + ) + .into_actor(self) + .wait(ctx); + } ctx.address().do_send(ValidateRepo); } } @@ -83,7 +101,7 @@ pub struct ValidateRepo; impl Handler for RepoActor { type Result = (); fn handle(&mut self, _msg: ValidateRepo, ctx: &mut Self::Context) -> Self::Result { - if let Some(repo_config) = self.config.clone() { + if let Some(repo_config) = self.details.config.clone() { let repo_details = self.details.clone(); let addr = ctx.address(); let net = self.net.clone(); @@ -106,7 +124,7 @@ pub struct StartMonitoring { impl Handler for RepoActor { type Result = (); fn handle(&mut self, msg: StartMonitoring, ctx: &mut Self::Context) -> Self::Result { - let Some(repo_config) = self.config.clone() else { + let Some(repo_config) = self.details.config.clone() else { warn!("No config loaded"); return; }; @@ -116,6 +134,7 @@ impl Handler for RepoActor { info!(%msg.main, %msg.next, %msg.dev, next_ahead_of_main, dev_ahead_of_next, "StartMonitoring"); let repo_details = self.details.clone(); + let webhook = self.webhook.clone(); let addr = ctx.address(); let net = self.net.clone(); let git = self.git.clone(); @@ -130,13 +149,12 @@ impl Handler for RepoActor { msg.dev_commit_history, repo_details, repo_config, - addr, git, ) .into_actor(self) .wait(ctx); } else if self.webhook_id.is_none() { - webhook::register(repo_details, addr, net) + webhook::register(repo_details, webhook, addr, net) .into_actor(self) .wait(ctx); } @@ -145,11 +163,12 @@ impl Handler for RepoActor { #[derive(Message)] #[rtype(result = "()")] -pub struct WebhookRegistered(pub WebhookId); +pub struct WebhookRegistered(pub WebhookId, pub ulid::Ulid); impl Handler for RepoActor { type Result = (); fn handle(&mut self, msg: WebhookRegistered, _ctx: &mut Self::Context) -> Self::Result { self.webhook_id.replace(msg.0); + self.webhook_auth.replace(msg.1); } } @@ -160,7 +179,7 @@ impl Handler for RepoActor { type Result = (); fn handle(&mut self, msg: AdvanceMainTo, ctx: &mut Self::Context) -> Self::Result { let repo_details = self.details.clone(); - let Some(repo_config) = self.config.clone() else { + let Some(repo_config) = self.details.config.clone() else { warn!("No config loaded"); return; }; diff --git a/src/server/actors/repo/status.rs b/src/server/actors/repo/status.rs index 4f94b50..3c7c157 100644 --- a/src/server/actors/repo/status.rs +++ b/src/server/actors/repo/status.rs @@ -3,7 +3,6 @@ use gix::trace::warn; use tracing::info; use crate::server::{ - actors::repo::ValidateRepo, config::{self, ForgeType}, forge, }; @@ -37,9 +36,6 @@ pub async fn check_next( warn!("Checks have failed"); } } - // TODO : (#43) sleep and restart while we don't have webhooks - tokio::time::sleep(std::time::Duration::from_secs(10)).await; - addr.do_send(ValidateRepo); } #[derive(Debug)] diff --git a/src/server/actors/repo/webhook.rs b/src/server/actors/repo/webhook.rs index 6c62d16..636ea7a 100644 --- a/src/server/actors/repo/webhook.rs +++ b/src/server/actors/repo/webhook.rs @@ -1,12 +1,15 @@ use actix::prelude::*; -use kxio::network; +use kxio::network::{self, json}; use tracing::{info, warn}; use std::{fmt::Display, ops::Deref}; -use crate::server::actors::{ - repo::{RepoActor, ValidateRepo}, - webhook::WebhookMessage, +use crate::server::{ + actors::{ + repo::{RepoActor, WebhookRegistered}, + webhook::WebhookMessage, + }, + config::Webhook, }; #[derive(Clone, Debug, PartialEq, Eq)] @@ -37,8 +40,10 @@ pub async fn unregister( info!(?webhook_id, "unregister webhook"); let hostname = &repo_details.forge.hostname; let path = repo_details.repo; + use secrecy::ExposeSecret; + let token = repo_details.forge.token.expose_secret(); let url = network::NetUrl::new(format!( - "https://{hostname}/api/v1/repos/{path}/hooks/{webhook_id}" + "https://{hostname}/api/v1/repos/{path}/hooks/{webhook_id}?token={token}" )); let request = network::NetRequest::new( network::RequestMethod::Delete, @@ -57,13 +62,66 @@ pub async fn unregister( } pub async fn register( - _repo_details: crate::server::config::RepoDetails, + repo_details: crate::server::config::RepoDetails, + webhook: Webhook, addr: actix::prelude::Addr, - _net: network::Network, + net: network::Network, ) { - // TODO: (#15) register webhook - on success send webhook id to RepoActor - tokio::time::sleep(std::time::Duration::from_secs(10)).await; - addr.do_send(ValidateRepo); + let Some(repo_config) = repo_details.config else { + return; + }; + info!("Registering webhook"); + let hostname = &repo_details.forge.hostname; + let path = repo_details.repo; + use secrecy::ExposeSecret; + let token = repo_details.forge.token.expose_secret(); + let url = network::NetUrl::new(format!( + "https://{hostname}/api/v1/repos/{path}/hooks?token={token}" + )); + let webhook_url = webhook.url(); + let repo_alias = repo_details.name; + let headers = network::NetRequestHeaders::new().with("Content-Type", "application/json"); + let authorisation = ulid::Ulid::new(); + let body = json!({ + "active": true, + "authorization_header": format!("Basic {}", authorisation), + "branch_filter": format!("{{{},{},{}}}", repo_config.branches().main(), repo_config.branches().next(), repo_config.branches().dev()), + "config": { + "content_type": "json", + "url": format!("{}/{}", webhook_url.as_ref(), repo_alias), + }, + "events": [ "push" ], + "type": "forgejo" + }); + let request = network::NetRequest::new( + network::RequestMethod::Post, + url, + headers, + network::RequestBody::Json(body), + network::ResponseType::Json, + None, + network::NetRequestLogging::None, + ); + let result = net.post_json::(request).await; + match result { + Ok(response) => { + if let Some(hook) = response.response_body() { + info!("Webhook registered"); + addr.do_send(WebhookRegistered(hook.id(), authorisation)); + } + } + Err(_) => warn!("Failed to register webhook"), + } +} + +#[derive(Debug, serde::Deserialize)] +struct Hook { + id: i64, +} +impl Hook { + fn id(&self) -> WebhookId { + WebhookId(format!("{}", self.id)) + } } impl Handler for RepoActor { diff --git a/src/server/config.rs b/src/server/config.rs index 822da2f..c81010c 100644 --- a/src/server/config.rs +++ b/src/server/config.rs @@ -26,10 +26,14 @@ impl ServerConfig { .iter() .map(|(name, forge)| (ForgeName(name.clone()), forge)) } + + pub const fn webhook(&self) -> &Webhook { + &self.webhook + } } /// Defines the Webhook Forges should send updates to -#[derive(Debug, PartialEq, Eq, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Deserialize)] pub struct Webhook { url: String, } diff --git a/src/server/forge/forgejo/config.rs b/src/server/forge/forgejo/config.rs index 779e57a..3d329e7 100644 --- a/src/server/forge/forgejo/config.rs +++ b/src/server/forge/forgejo/config.rs @@ -1,6 +1,5 @@ use base64::Engine; use kxio::network::{self, Network}; -use secrecy::ExposeSecret; use terrors::OneOf; use tracing::{error, info}; @@ -34,6 +33,7 @@ pub async fn load( let path = &details.repo; let filepath = ".git-next.toml"; let branch = &details.branch; + use secrecy::ExposeSecret; let token = details.forge.token.expose_secret(); let url = network::NetUrl::new(format!( "https://{hostname}/api/v1/repos/{path}/contents/{filepath}?ref={branch}&token={token}" @@ -128,6 +128,7 @@ pub async fn validate( ) -> Result> { let hostname = &details.forge.hostname; let path = &details.repo; + use secrecy::ExposeSecret; let token = details.forge.token.expose_secret(); let url = network::NetUrl::new(format!( "https://{hostname}/api/v1/repos/{path}/branches?token={token}" diff --git a/src/server/mod.rs b/src/server/mod.rs index faa19b9..3d01a4a 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -15,7 +15,7 @@ use crate::{ filesystem::FileSystem, server::{ actors::webhook, - config::{Forge, ForgeName, RepoAlias}, + config::{Forge, ForgeName, RepoAlias, Webhook}, git::Git, }, }; @@ -46,19 +46,19 @@ pub async fn start(fs: FileSystem, net: Network, git: Git) { return; }; info!("Starting Server..."); - let config = match config::ServerConfig::load(&fs) { - Ok(config) => config, + let server_config = match config::ServerConfig::load(&fs) { + Ok(server_config) => server_config, Err(err) => { error!("Failed to load config file. Error: {}", err); return; } }; - info!("Config loaded"); let webhook_router = webhook::WebhookRouter::new().start(); - config + let webhook = server_config.webhook(); + server_config .forges() - .flat_map(|(forge_name, forge)| create_forge_repos(forge, forge_name, &net, &git)) + .flat_map(|(forge_name, forge)| create_forge_repos(forge, forge_name, webhook, &net, &git)) .map(start_actor) .map(|(alias, addr)| webhook::AddWebhookRecipient(alias, addr.recipient())) .for_each(|msg| webhook_router.do_send(msg)); @@ -71,6 +71,7 @@ pub async fn start(fs: FileSystem, net: Network, git: Git) { fn create_forge_repos( forge: &Forge, forge_name: ForgeName, + webhook: &Webhook, net: &Network, git: &Git, ) -> Vec<(ForgeName, RepoAlias, RepoActor)> { @@ -80,16 +81,18 @@ fn create_forge_repos( info!("Creating Forge"); forge .repos() - .map(create_actor(forge_name, forge.clone(), net, git)) + .map(create_actor(forge_name, forge.clone(), webhook, net, git)) .collect::>() } fn create_actor( forge_name: ForgeName, forge: config::Forge, + webhook: &Webhook, net: &Network, git: &Git, ) -> impl Fn((RepoAlias, &Repo)) -> (ForgeName, RepoAlias, RepoActor) { + let webhook = webhook.clone(); let net = net.clone(); let git = git.clone(); move |(repo_name, repo)| { @@ -98,6 +101,7 @@ fn create_actor( info!("Creating Repo"); let actor = actors::repo::RepoActor::new( config::RepoDetails::new(&repo_name, repo, &forge_name, &forge), + webhook.clone(), net.clone(), git.clone(), );