feat: add ensure webhook
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
This commit is contained in:
parent
706a62a292
commit
ff81ab252a
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -332,6 +332,7 @@ dependencies = [
|
|||||||
"regex",
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
|
@ -20,3 +20,4 @@ futures = "0.3.30"
|
|||||||
reqwest = {version = "0.12.3", default-features = false, features = ["json", "rustls-tls"]}
|
reqwest = {version = "0.12.3", default-features = false, features = ["json", "rustls-tls"]}
|
||||||
itertools = "0.12.1"
|
itertools = "0.12.1"
|
||||||
regex = "1.10.4"
|
regex = "1.10.4"
|
||||||
|
serde_json = "1.0.115"
|
||||||
|
@ -26,6 +26,9 @@ enum Commands {
|
|||||||
|
|
||||||
#[arg(long, env = "CONTRACTOR_FILTER")]
|
#[arg(long, env = "CONTRACTOR_FILTER")]
|
||||||
filter: Option<String>,
|
filter: Option<String>,
|
||||||
|
|
||||||
|
#[arg(long = "force-refresh", env = "CONTRACTOR_FORCE_REFRESH")]
|
||||||
|
force_refresh: bool,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,12 +67,20 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
result??
|
result??
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(Commands::Reconcile { user, org, filter }) => {
|
Some(Commands::Reconcile {
|
||||||
|
user,
|
||||||
|
org,
|
||||||
|
filter,
|
||||||
|
force_refresh,
|
||||||
|
}) => {
|
||||||
tracing::info!("running reconcile");
|
tracing::info!("running reconcile");
|
||||||
|
|
||||||
let state = SharedState::from(Arc::new(State::new().await?));
|
let state = SharedState::from(Arc::new(State::new().await?));
|
||||||
|
|
||||||
state.reconciler().reconcile(user, org, filter).await?;
|
state
|
||||||
|
.reconciler()
|
||||||
|
.reconcile(user, org, filter, force_refresh)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
None => {}
|
None => {}
|
||||||
}
|
}
|
||||||
|
@ -76,6 +76,42 @@ impl Default for DefaultGiteaClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
pub struct GiteaWebhook {
|
||||||
|
id: isize,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
r#type: GiteaWebhookType,
|
||||||
|
config: GiteaWebhookConfig,
|
||||||
|
}
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
pub struct GiteaWebhookConfig {
|
||||||
|
url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
|
pub enum GiteaWebhookType {
|
||||||
|
#[serde(rename = "gitea")]
|
||||||
|
Gitea,
|
||||||
|
Other(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize)]
|
||||||
|
pub struct CreateGiteaWebhook {
|
||||||
|
active: bool,
|
||||||
|
authorization_header: Option<String>,
|
||||||
|
branch_filter: Option<String>,
|
||||||
|
config: CreateGiteaWebhookConfig,
|
||||||
|
events: Vec<String>,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
r#type: GiteaWebhookType,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize)]
|
||||||
|
pub struct CreateGiteaWebhookConfig {
|
||||||
|
content_type: String,
|
||||||
|
url: String,
|
||||||
|
}
|
||||||
|
|
||||||
impl DefaultGiteaClient {
|
impl DefaultGiteaClient {
|
||||||
pub async fn fetch_user_repos(&self) -> anyhow::Result<Vec<Repository>> {
|
pub async fn fetch_user_repos(&self) -> anyhow::Result<Vec<Repository>> {
|
||||||
//FIXME: We should collect the pages for these queries
|
//FIXME: We should collect the pages for these queries
|
||||||
@ -147,6 +183,126 @@ impl DefaultGiteaClient {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_webhook(&self, repo: &Repository) -> anyhow::Result<Option<GiteaWebhook>> {
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
let url = format!(
|
||||||
|
"{}/api/v1/repos/{}/{}/hooks",
|
||||||
|
self.url, &repo.owner, &repo.name
|
||||||
|
);
|
||||||
|
|
||||||
|
tracing::trace!("calling url: {}", &url);
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.get(&url)
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("Authorization", format!("token {}", self.token))
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let webhooks = response.json::<Vec<GiteaWebhook>>().await?;
|
||||||
|
|
||||||
|
let valid_webhooks = webhooks
|
||||||
|
.into_iter()
|
||||||
|
.filter(|w| w.r#type == GiteaWebhookType::Gitea)
|
||||||
|
.filter(|w| w.config.url.contains("contractor"))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
Ok(valid_webhooks.first().map(|f| f.to_owned()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn add_webhook(&self, repo: &Repository) -> anyhow::Result<()> {
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
let url = format!(
|
||||||
|
"{}/api/v1/repos/{}/{}/hooks",
|
||||||
|
self.url, &repo.owner, &repo.name
|
||||||
|
);
|
||||||
|
|
||||||
|
let val = CreateGiteaWebhook {
|
||||||
|
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_review_comment".into()],
|
||||||
|
r#type: GiteaWebhookType::Gitea,
|
||||||
|
};
|
||||||
|
|
||||||
|
tracing::trace!(
|
||||||
|
"calling url: {} with body {}",
|
||||||
|
&url,
|
||||||
|
serde_json::to_string(&val)?
|
||||||
|
);
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.post(&url)
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.header("Authorization", format!("token {}", self.token))
|
||||||
|
.json(&val)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if let Err(e) = response.error_for_status_ref() {
|
||||||
|
if let Ok(ok) = response.text().await {
|
||||||
|
anyhow::bail!("failed to create webhook: {}, body: {}", e, ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
anyhow::bail!("failed to create webhook: {}", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_webhook(&self, repo: &Repository, webhook: GiteaWebhook) -> anyhow::Result<()> {
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
|
||||||
|
let url = format!(
|
||||||
|
"{}/api/v1/repos/{}/{}/hooks/{}",
|
||||||
|
self.url, &repo.owner, &repo.name, &webhook.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
let val = CreateGiteaWebhook {
|
||||||
|
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_review_comment".into()],
|
||||||
|
r#type: GiteaWebhookType::Gitea,
|
||||||
|
};
|
||||||
|
|
||||||
|
tracing::trace!(
|
||||||
|
"calling url: {} with body {}",
|
||||||
|
&url,
|
||||||
|
serde_json::to_string(&val)?
|
||||||
|
);
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.patch(&url)
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.header("Authorization", format!("token {}", self.token))
|
||||||
|
.json(&val)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if let Err(e) = response.error_for_status_ref() {
|
||||||
|
if let Ok(ok) = response.text().await {
|
||||||
|
anyhow::bail!("failed to create webhook: {}, body: {}", e, ok);
|
||||||
|
}
|
||||||
|
|
||||||
|
anyhow::bail!("failed to create webhook: {}", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl traits::GiteaClient for DefaultGiteaClient {
|
impl traits::GiteaClient for DefaultGiteaClient {
|
||||||
@ -178,6 +334,32 @@ impl traits::GiteaClient for DefaultGiteaClient {
|
|||||||
|
|
||||||
Box::pin(async { self.fetch_renovate(repo).await.map(|s| s.is_some()) })
|
Box::pin(async { self.fetch_renovate(repo).await.map(|s| s.is_some()) })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn ensure_webhook<'a>(
|
||||||
|
&'a self,
|
||||||
|
repo: &'a Repository,
|
||||||
|
force_refresh: bool,
|
||||||
|
) -> Pin<Box<dyn futures::prelude::Future<Output = anyhow::Result<()>> + Send + 'a>> {
|
||||||
|
tracing::trace!("ensuring webhook exists for repo: {}", repo);
|
||||||
|
|
||||||
|
Box::pin(async move {
|
||||||
|
match (self.get_webhook(repo).await?, force_refresh) {
|
||||||
|
(Some(_), false) => {
|
||||||
|
tracing::trace!("webhook already found for {} skipping...", repo);
|
||||||
|
}
|
||||||
|
(Some(webhook), true) => {
|
||||||
|
tracing::trace!("webhook already found for {} refreshing it", repo);
|
||||||
|
self.update_webhook(repo, webhook).await?;
|
||||||
|
}
|
||||||
|
(None, _) => {
|
||||||
|
tracing::trace!("webhook was not found for {} adding", repo);
|
||||||
|
self.add_webhook(repo).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mod extensions;
|
mod extensions;
|
||||||
@ -186,4 +368,4 @@ pub mod traits;
|
|||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
pub use extensions::*;
|
pub use extensions::*;
|
||||||
use reqwest::StatusCode;
|
use reqwest::StatusCode;
|
||||||
use serde::Deserialize;
|
use serde::{Deserialize, Serialize};
|
||||||
|
@ -19,4 +19,10 @@ pub trait GiteaClient {
|
|||||||
&'a self,
|
&'a self,
|
||||||
repo: &'a Repository,
|
repo: &'a Repository,
|
||||||
) -> Pin<Box<dyn Future<Output = anyhow::Result<bool>> + Send + 'a>>;
|
) -> Pin<Box<dyn Future<Output = anyhow::Result<bool>> + Send + 'a>>;
|
||||||
|
|
||||||
|
fn ensure_webhook<'a>(
|
||||||
|
&'a self,
|
||||||
|
repo: &'a Repository,
|
||||||
|
force_refresh: bool,
|
||||||
|
) -> Pin<Box<dyn Future<Output = anyhow::Result<()>> + Send + 'a>>;
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ impl Reconciler {
|
|||||||
user: Option<String>,
|
user: Option<String>,
|
||||||
orgs: Option<Vec<String>>,
|
orgs: Option<Vec<String>>,
|
||||||
filter: Option<String>,
|
filter: Option<String>,
|
||||||
|
force_refresh: bool,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let repos = self.get_repos(user, orgs).await?;
|
let repos = self.get_repos(user, orgs).await?;
|
||||||
tracing::debug!("found repositories: {}", repos.len());
|
tracing::debug!("found repositories: {}", repos.len());
|
||||||
@ -56,6 +57,9 @@ impl Reconciler {
|
|||||||
renovate_enabled.len()
|
renovate_enabled.len()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
self.ensure_webhook(&renovate_enabled, force_refresh)
|
||||||
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,6 +113,32 @@ impl Reconciler {
|
|||||||
|
|
||||||
Ok(enabled)
|
Ok(enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn ensure_webhook(
|
||||||
|
&self,
|
||||||
|
repos: &[Repository],
|
||||||
|
force_refresh: bool,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
tracing::debug!("ensuring webhooks are setup for repos");
|
||||||
|
|
||||||
|
let mut tasks = FuturesUnordered::new();
|
||||||
|
|
||||||
|
for repo in repos {
|
||||||
|
tasks.push(async move {
|
||||||
|
self.gitea_client
|
||||||
|
.ensure_webhook(repo, force_refresh)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok::<(), anyhow::Error>(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
while let Some(res) = tasks.next().await {
|
||||||
|
res?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait ReconcilerState {
|
pub trait ReconcilerState {
|
||||||
|
Loading…
Reference in New Issue
Block a user