Files
contractor/crates/contractor/src/services/gitea.rs
kjuulh 8d6e706263
All checks were successful
continuous-integration/drone/push Build is passing
feat: add issues also
Signed-off-by: kjuulh <contact@kjuulh.io>
2024-04-12 23:35:01 +02:00

372 lines
11 KiB
Rust

use std::{fmt::Display, ops::Deref, pin::Pin, sync::Arc};
type DynGiteaClient = Arc<dyn traits::GiteaClient + Send + Sync + 'static>;
pub struct GiteaClient(DynGiteaClient);
impl GiteaClient {
pub fn new() -> Self {
Self(Arc::new(DefaultGiteaClient::default()))
}
}
impl Deref for GiteaClient {
type Target = DynGiteaClient;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Repository {
pub owner: String,
pub name: String,
}
impl Display for Repository {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("{}/{}", self.owner, self.name))
}
}
impl TryFrom<GiteaRepository> for Repository {
type Error = anyhow::Error;
fn try_from(value: GiteaRepository) -> Result<Self, Self::Error> {
let (owner, name) = value
.full_name
.split_once('/')
.ok_or(anyhow::anyhow!(
"name of repository is invalid, should contain a /"
))
.map_err(|e| {
tracing::warn!("failed to parse repository: {}", e);
e
})?;
Ok(Repository {
owner: owner.into(),
name: name.into(),
})
}
}
#[derive(Clone, Debug, Deserialize)]
pub struct GiteaRepository {
full_name: String,
}
pub struct DefaultGiteaClient {
url: String,
token: String,
}
impl Default for DefaultGiteaClient {
fn default() -> Self {
Self {
url: std::env::var("GITEA_URL")
.context("GITEA_URL should be set")
.map(|g| g.trim_end_matches('/').to_string())
.unwrap(),
token: std::env::var("GITEA_TOKEN")
.context("GITEA_TOKEN should be set")
.unwrap(),
}
}
}
#[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 {
pub async fn fetch_user_repos(&self) -> anyhow::Result<Vec<Repository>> {
//FIXME: We should collect the pages for these queries
let client = reqwest::Client::new();
let url = format!("{}/api/v1/user/repos", self.url);
tracing::trace!("calling url: {}", &url);
let response = client
.get(&url)
.header("Content-Type", "application/json")
.header("Authorization", format!("token {}", self.token))
.send()
.await?;
let repositories = response.json::<Vec<GiteaRepository>>().await?;
Ok(repositories
.into_iter()
.flat_map(Repository::try_from)
.collect())
}
pub async fn fetch_org_repos(&self, org: &str) -> anyhow::Result<Vec<Repository>> {
let client = reqwest::Client::new();
let url = format!("{}/api/v1/orgs/{}/repos", self.url, org);
tracing::trace!("calling url: {}", &url);
let response = client
.get(&url)
.header("Content-Type", "application/json")
.header("Authorization", format!("token {}", self.token))
.send()
.await?;
let repositories = response.json::<Vec<GiteaRepository>>().await?;
Ok(repositories
.into_iter()
.flat_map(Repository::try_from)
.collect())
}
async fn fetch_renovate(&self, repo: &Repository) -> anyhow::Result<Option<()>> {
let client = reqwest::Client::new();
let url = format!(
"{}/api/v1/repos/{}/{}/contents/renovate.json",
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?;
match response.error_for_status() {
Ok(_) => Ok(Some(())),
Err(e) => match e.status() {
Some(StatusCode::NOT_FOUND) => Ok(None),
_ => anyhow::bail!(e),
},
}
}
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_comment".into(), "issue_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_comment".into(), "issue_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 {
fn get_user_repositories<'a>(
&'a self,
user: &str,
) -> Pin<Box<dyn futures::prelude::Future<Output = anyhow::Result<Vec<Repository>>> + Send + 'a>>
{
tracing::debug!("fetching gitea repositories for user: {user}");
Box::pin(async { self.fetch_user_repos().await })
}
fn get_org_repositories<'a>(
&'a self,
org: &'a str,
) -> Pin<Box<dyn futures::prelude::Future<Output = anyhow::Result<Vec<Repository>>> + Send + 'a>>
{
tracing::debug!("fetching gitea repositories for org: {org}");
Box::pin(async move { self.fetch_org_repos(org).await })
}
fn renovate_enabled<'a>(
&'a self,
repo: &'a Repository,
) -> Pin<Box<dyn futures::prelude::Future<Output = anyhow::Result<bool>> + Send + 'a>> {
tracing::trace!("checking whether renovate is enabled for: {:?}", repo);
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;
pub mod traits;
use anyhow::Context;
pub use extensions::*;
use reqwest::StatusCode;
use serde::{Deserialize, Serialize};