Signed-off-by: kjuulh <contact@kjuulh.io>
This commit is contained in:
parent
7f73220753
commit
fdda383f5a
@ -1,15 +1,31 @@
|
|||||||
use std::net::SocketAddr;
|
use std::{net::SocketAddr, sync::Arc};
|
||||||
|
|
||||||
use axum::{extract::MatchedPath, http::Request, routing::get, Router};
|
use anyhow::Context;
|
||||||
|
use axum::{
|
||||||
|
body::Body,
|
||||||
|
extract::{MatchedPath, State},
|
||||||
|
http::Request,
|
||||||
|
response::IntoResponse,
|
||||||
|
routing::{get, post},
|
||||||
|
Json, Router,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use tower_http::trace::TraceLayer;
|
use tower_http::trace::TraceLayer;
|
||||||
|
|
||||||
use crate::SharedState;
|
use crate::{
|
||||||
|
services::{
|
||||||
|
bot::{BotRequest, BotState},
|
||||||
|
gitea::Repository,
|
||||||
|
},
|
||||||
|
SharedState,
|
||||||
|
};
|
||||||
|
|
||||||
pub async fn serve_axum(state: &SharedState, host: &SocketAddr) -> Result<(), anyhow::Error> {
|
pub async fn serve_axum(state: &SharedState, host: &SocketAddr) -> Result<(), anyhow::Error> {
|
||||||
tracing::info!("running webhook server");
|
tracing::info!("running webhook server");
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/", get(root))
|
.route("/", get(root))
|
||||||
.with_state(state.clone())
|
.route("/webhooks/gitea", post(gitea_webhook))
|
||||||
|
.with_state(state.to_owned())
|
||||||
.layer(
|
.layer(
|
||||||
TraceLayer::new_for_http().make_span_with(|request: &Request<_>| {
|
TraceLayer::new_for_http().make_span_with(|request: &Request<_>| {
|
||||||
// Log the matched route's path (with placeholders not filled in).
|
// Log the matched route's path (with placeholders not filled in).
|
||||||
@ -38,3 +54,85 @@ pub async fn serve_axum(state: &SharedState, host: &SocketAddr) -> Result<(), an
|
|||||||
async fn root() -> &'static str {
|
async fn root() -> &'static str {
|
||||||
"Hello, contractor!"
|
"Hello, contractor!"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub struct GiteaWebhookComment {
|
||||||
|
body: String,
|
||||||
|
}
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub struct GiteaWebhookRepository {
|
||||||
|
full_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum GiteaWebhook {
|
||||||
|
Issue {
|
||||||
|
comment: GiteaWebhookComment,
|
||||||
|
repository: GiteaWebhookRepository,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum ApiError {
|
||||||
|
InternalError(anyhow::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for ApiError {
|
||||||
|
fn into_response(self) -> axum::response::Response {
|
||||||
|
match self {
|
||||||
|
ApiError::InternalError(e) => {
|
||||||
|
tracing::error!("failed with internal error: {}", e);
|
||||||
|
|
||||||
|
(axum::http::StatusCode::INTERNAL_SERVER_ERROR, e.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn gitea_webhook(
|
||||||
|
State(state): State<SharedState>,
|
||||||
|
Json(json): Json<GiteaWebhook>,
|
||||||
|
) -> Result<impl IntoResponse, ApiError> {
|
||||||
|
tracing::info!(
|
||||||
|
"called: {}",
|
||||||
|
serde_json::to_string(&json)
|
||||||
|
.context("failed to serialize webhook")
|
||||||
|
.map_err(ApiError::InternalError)?
|
||||||
|
);
|
||||||
|
|
||||||
|
let bot_req: BotRequest = json.try_into().map_err(ApiError::InternalError)?;
|
||||||
|
|
||||||
|
state
|
||||||
|
.bot()
|
||||||
|
.handle_request(bot_req)
|
||||||
|
.await
|
||||||
|
.map_err(ApiError::InternalError)?;
|
||||||
|
|
||||||
|
Ok("Hello, contractor!")
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<GiteaWebhook> for BotRequest {
|
||||||
|
type Error = anyhow::Error;
|
||||||
|
fn try_from(value: GiteaWebhook) -> Result<Self, Self::Error> {
|
||||||
|
match value {
|
||||||
|
GiteaWebhook::Issue {
|
||||||
|
comment,
|
||||||
|
repository,
|
||||||
|
} => {
|
||||||
|
let (owner, name) = repository.full_name.split_once('/').ok_or(anyhow::anyhow!(
|
||||||
|
"{} did not contain a valid owner/repository",
|
||||||
|
&repository.full_name
|
||||||
|
))?;
|
||||||
|
|
||||||
|
Ok(BotRequest {
|
||||||
|
repo: Repository {
|
||||||
|
owner: owner.into(),
|
||||||
|
name: name.into(),
|
||||||
|
},
|
||||||
|
command: comment.body,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -7,8 +7,6 @@ pub async fn serve_cron_jobs(state: &SharedState) -> Result<(), anyhow::Error> {
|
|||||||
loop {
|
loop {
|
||||||
tracing::info!("running cronjobs");
|
tracing::info!("running cronjobs");
|
||||||
|
|
||||||
todo!();
|
|
||||||
|
|
||||||
tokio::time::sleep(std::time::Duration::from_secs(10_000)).await;
|
tokio::time::sleep(std::time::Duration::from_secs(10_000)).await;
|
||||||
}
|
}
|
||||||
Ok::<(), anyhow::Error>(())
|
Ok::<(), anyhow::Error>(())
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
|
pub mod bot;
|
||||||
pub mod gitea;
|
pub mod gitea;
|
||||||
pub mod reconciler;
|
pub mod reconciler;
|
||||||
|
65
crates/contractor/src/services/bot.rs
Normal file
65
crates/contractor/src/services/bot.rs
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
use clap::{Parser, Subcommand};
|
||||||
|
|
||||||
|
use crate::SharedState;
|
||||||
|
|
||||||
|
use super::gitea::Repository;
|
||||||
|
|
||||||
|
pub struct Bot {
|
||||||
|
command_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(author, version, about, long_about = None, subcommand_required = true)]
|
||||||
|
struct BotCommand {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: Option<BotCommands>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
enum BotCommands {
|
||||||
|
Refresh {
|
||||||
|
#[arg(long)]
|
||||||
|
all: bool,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Bot {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
command_name: std::env::var("CONTRACTOR_COMMAND_NAME").unwrap_or("contractor".into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_request(&self, req: impl Into<BotRequest>) -> anyhow::Result<()> {
|
||||||
|
let req: BotRequest = req.into();
|
||||||
|
|
||||||
|
if !req.command.starts_with(&self.command_name) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let cmd = BotCommand::parse_from(req.command.split_whitespace());
|
||||||
|
|
||||||
|
match cmd.command {
|
||||||
|
Some(BotCommands::Refresh { all }) => {
|
||||||
|
tracing::info!("triggering refresh for: {}, all: {}", req.repo, all);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// TODO: Send back the help menu
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct BotRequest {
|
||||||
|
pub repo: Repository,
|
||||||
|
pub command: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait BotState {
|
||||||
|
fn bot(&self) -> Bot {
|
||||||
|
Bot::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl BotState for SharedState {}
|
@ -60,6 +60,8 @@ pub struct GiteaRepository {
|
|||||||
pub struct DefaultGiteaClient {
|
pub struct DefaultGiteaClient {
|
||||||
url: String,
|
url: String,
|
||||||
token: String,
|
token: String,
|
||||||
|
|
||||||
|
webhook_url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for DefaultGiteaClient {
|
impl Default for DefaultGiteaClient {
|
||||||
@ -72,6 +74,10 @@ impl Default for DefaultGiteaClient {
|
|||||||
token: std::env::var("GITEA_TOKEN")
|
token: std::env::var("GITEA_TOKEN")
|
||||||
.context("GITEA_TOKEN should be set")
|
.context("GITEA_TOKEN should be set")
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
|
webhook_url: std::env::var("CONTRACTOR_URL")
|
||||||
|
.context("CONTRACTOR_URL should be set")
|
||||||
|
.map(|url| format!("{}/webhooks/gitea", url.trim_end_matches('/')))
|
||||||
|
.unwrap(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -287,17 +293,7 @@ impl DefaultGiteaClient {
|
|||||||
self.url, &repo.owner, &repo.name
|
self.url, &repo.owner, &repo.name
|
||||||
);
|
);
|
||||||
|
|
||||||
let val = CreateGiteaWebhook {
|
let val = self.create_webhook();
|
||||||
active: true,
|
|
||||||
authorization_header: Some("something".into()),
|
|
||||||
branch_filter: Some("*".into()),
|
|
||||||
config: CreateGiteaWebhookConfig {
|
|
||||||
content_type: "json".into(),
|
|
||||||
url: "https://url?type=contractor".into(),
|
|
||||||
},
|
|
||||||
events: vec!["pull_request_comment".into(), "issue_comment".into()],
|
|
||||||
r#type: GiteaWebhookType::Gitea,
|
|
||||||
};
|
|
||||||
|
|
||||||
tracing::trace!(
|
tracing::trace!(
|
||||||
"calling url: {} with body {}",
|
"calling url: {} with body {}",
|
||||||
@ -325,6 +321,20 @@ impl DefaultGiteaClient {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn create_webhook(&self) -> CreateGiteaWebhook {
|
||||||
|
CreateGiteaWebhook {
|
||||||
|
active: true,
|
||||||
|
authorization_header: Some("something".into()),
|
||||||
|
branch_filter: Some("*".into()),
|
||||||
|
config: CreateGiteaWebhookConfig {
|
||||||
|
content_type: "json".into(),
|
||||||
|
url: format!("{}?type=contractor", self.webhook_url),
|
||||||
|
},
|
||||||
|
events: vec!["pull_request_comment".into(), "issue_comment".into()],
|
||||||
|
r#type: GiteaWebhookType::Gitea,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn update_webhook(&self, repo: &Repository, webhook: GiteaWebhook) -> anyhow::Result<()> {
|
async fn update_webhook(&self, repo: &Repository, webhook: GiteaWebhook) -> anyhow::Result<()> {
|
||||||
let client = reqwest::Client::new();
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
@ -333,17 +343,7 @@ impl DefaultGiteaClient {
|
|||||||
self.url, &repo.owner, &repo.name, &webhook.id,
|
self.url, &repo.owner, &repo.name, &webhook.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
let val = CreateGiteaWebhook {
|
let val = self.create_webhook();
|
||||||
active: true,
|
|
||||||
authorization_header: Some("something".into()),
|
|
||||||
branch_filter: Some("*".into()),
|
|
||||||
config: CreateGiteaWebhookConfig {
|
|
||||||
content_type: "json".into(),
|
|
||||||
url: "https://url?type=contractor".into(),
|
|
||||||
},
|
|
||||||
events: vec!["pull_request_comment".into(), "issue_comment".into()],
|
|
||||||
r#type: GiteaWebhookType::Gitea,
|
|
||||||
};
|
|
||||||
|
|
||||||
tracing::trace!(
|
tracing::trace!(
|
||||||
"calling url: {} with body {}",
|
"calling url: {} with body {}",
|
||||||
@ -464,7 +464,6 @@ pub mod traits;
|
|||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
pub use extensions::*;
|
pub use extensions::*;
|
||||||
use futures::{stream::FuturesUnordered, StreamExt, TryStreamExt};
|
use futures::{stream::FuturesUnordered, TryStreamExt};
|
||||||
use itertools::Itertools;
|
|
||||||
use reqwest::{StatusCode, Url};
|
use reqwest::{StatusCode, Url};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
Loading…
Reference in New Issue
Block a user