2
.env
2
.env
@@ -1,2 +1,2 @@
|
|||||||
RUST_LOG=notmad=debug,info
|
RUST_LOG=notmad=debug,nodrift=debug,forge=trace,info
|
||||||
FE_CONFIG_DIR=./templates/fe/configs/
|
FE_CONFIG_DIR=./templates/fe/configs/
|
||||||
|
|||||||
1305
Cargo.lock
generated
1305
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -19,3 +19,5 @@ tokio-util = "0.7.18"
|
|||||||
async-trait = "0.1.89"
|
async-trait = "0.1.89"
|
||||||
regex = "1.12.3"
|
regex = "1.12.3"
|
||||||
toml = "0.9.11"
|
toml = "0.9.11"
|
||||||
|
nodrift = "0.3.5"
|
||||||
|
octocrab = "0.49.5"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use regex::Regex;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct ForgeConfig {
|
pub struct ForgeConfig {
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub forge_type: ForgeConfigType,
|
pub forge_type: ForgeConfigType,
|
||||||
@@ -19,12 +20,14 @@ pub struct ForgeConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Deserialize)]
|
#[derive(Clone, Debug, Default, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct Policies {
|
pub struct Policies {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub squash_merge_only: PolicyOption,
|
pub squash_merge_only: PolicyOption,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Deserialize)]
|
#[derive(Clone, Debug, Default, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct PolicyOption {
|
pub struct PolicyOption {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
@@ -38,14 +41,17 @@ fn allow_all() -> Vec<ForgeRegex> {
|
|||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
pub enum ForgeSchedule {
|
pub enum ForgeSchedule {
|
||||||
Cron {},
|
#[serde(rename = "cron")]
|
||||||
Interval {},
|
Cron(String),
|
||||||
Once {},
|
#[serde(rename = "interval")]
|
||||||
|
Interval(String),
|
||||||
|
#[serde(rename = "once")]
|
||||||
|
Once(bool),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ForgeSchedule {
|
impl Default for ForgeSchedule {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::Interval {}
|
Self::Interval("60".into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,6 +60,17 @@ pub struct ForgeRegex {
|
|||||||
regex: Regex,
|
regex: Regex,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<ForgeRegex> for Regex {
|
||||||
|
fn from(value: ForgeRegex) -> Self {
|
||||||
|
value.regex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<&ForgeRegex> for Regex {
|
||||||
|
fn from(value: &ForgeRegex) -> Self {
|
||||||
|
value.clone().into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl std::ops::Deref for ForgeRegex {
|
impl std::ops::Deref for ForgeRegex {
|
||||||
type Target = Regex;
|
type Target = Regex;
|
||||||
|
|
||||||
@@ -83,8 +100,19 @@ impl<'de> Deserialize<'de> for ForgeRegex {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
#[serde(tag = "type")]
|
// #[serde(tag = "type")]
|
||||||
pub enum ForgeConfigType {
|
pub enum ForgeConfigType {
|
||||||
#[serde(rename = "github")]
|
#[serde(rename = "github")]
|
||||||
GitHub {},
|
GitHub {
|
||||||
|
credentials: GitHubCredentials,
|
||||||
|
organisation: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub enum GitHubCredentials {
|
||||||
|
#[serde(rename = "token")]
|
||||||
|
Token(String),
|
||||||
|
#[serde(rename = "token_env")]
|
||||||
|
TokenEnv(String),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
use std::path::Path;
|
use std::{path::Path, time::Duration};
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use nodrift::Drifter;
|
||||||
use notmad::{Component, MadError};
|
use notmad::{Component, MadError};
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
use crate::{State, forge_config::ForgeConfig};
|
use crate::{
|
||||||
|
State,
|
||||||
|
forge_config::{ForgeConfig, ForgeConfigType, ForgeSchedule, GitHubCredentials},
|
||||||
|
forges,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct ForgeService {
|
pub struct ForgeService {
|
||||||
name: String,
|
name: String,
|
||||||
config: ForgeConfig,
|
config: ForgeConfig,
|
||||||
@@ -18,7 +25,71 @@ impl Component for ForgeService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn run(&self, cancellation_token: CancellationToken) -> Result<(), MadError> {
|
async fn run(&self, cancellation_token: CancellationToken) -> Result<(), MadError> {
|
||||||
cancellation_token.cancelled().await;
|
let job_cancel = match &self.config.schedule {
|
||||||
|
ForgeSchedule::Cron(cron) => nodrift::schedule_drifter_cron(cron, self.clone())?,
|
||||||
|
ForgeSchedule::Interval(interval) => nodrift::schedule_drifter(
|
||||||
|
Duration::from_mins(
|
||||||
|
interval
|
||||||
|
.parse::<u64>()
|
||||||
|
.context("interval is not a minute duration")?,
|
||||||
|
),
|
||||||
|
self.clone(),
|
||||||
|
),
|
||||||
|
ForgeSchedule::Once(enabled) => {
|
||||||
|
if *enabled {
|
||||||
|
self.execute(cancellation_token.child_token()).await?;
|
||||||
|
}
|
||||||
|
cancellation_token.clone()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
tokio::select! {
|
||||||
|
_ = cancellation_token.cancelled() => { job_cancel.cancel() },
|
||||||
|
_ = job_cancel.cancelled() => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl nodrift::Drifter for ForgeService {
|
||||||
|
async fn execute(&self, _token: CancellationToken) -> anyhow::Result<()> {
|
||||||
|
tracing::info!("running schedule");
|
||||||
|
|
||||||
|
match &self.config.forge_type {
|
||||||
|
ForgeConfigType::GitHub {
|
||||||
|
credentials,
|
||||||
|
organisation,
|
||||||
|
} => {
|
||||||
|
let creds = match credentials.clone() {
|
||||||
|
GitHubCredentials::Token(token) => {
|
||||||
|
forges::github::GitHubCredentials::Token(token)
|
||||||
|
}
|
||||||
|
GitHubCredentials::TokenEnv(key) => {
|
||||||
|
let token =
|
||||||
|
std::env::var(key).context("failed to lookup github token env")?;
|
||||||
|
|
||||||
|
forges::github::GitHubCredentials::Token(token)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let client = forges::github::GitHub::new(creds);
|
||||||
|
|
||||||
|
// 1. get repositories matching allow / deny list
|
||||||
|
|
||||||
|
client
|
||||||
|
.get_repositories(
|
||||||
|
organisation,
|
||||||
|
self.config.allow.iter().map(|a| a.into()).collect(),
|
||||||
|
self.config.deny.iter().map(|a| a.into()).collect(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// 2. Get updates for each
|
||||||
|
// 3. Update repositories
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
1
crates/forge-enforce/src/forges.rs
Normal file
1
crates/forge-enforce/src/forges.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub mod github;
|
||||||
100
crates/forge-enforce/src/forges/github.rs
Normal file
100
crates/forge-enforce/src/forges/github.rs
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
use anyhow::Context;
|
||||||
|
use octocrab::{Octocrab, models::Repository};
|
||||||
|
use regex::Regex;
|
||||||
|
|
||||||
|
pub struct GitHub {
|
||||||
|
credentials: GitHubCredentials,
|
||||||
|
client: Octocrab,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum GitHubCredentials {
|
||||||
|
Token(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GitHub {
|
||||||
|
pub fn new(credentials: GitHubCredentials) -> Self {
|
||||||
|
let builder = Octocrab::builder();
|
||||||
|
|
||||||
|
let builder = match &credentials {
|
||||||
|
GitHubCredentials::Token(token) => builder.personal_token(token.clone()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let client = builder
|
||||||
|
.build()
|
||||||
|
.expect("to be able to create octocrab client");
|
||||||
|
|
||||||
|
Self {
|
||||||
|
credentials,
|
||||||
|
client,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_repositories(
|
||||||
|
&self,
|
||||||
|
organisation: &str,
|
||||||
|
allow: Vec<Regex>,
|
||||||
|
deny: Vec<Regex>,
|
||||||
|
) -> anyhow::Result<Vec<Repository>> {
|
||||||
|
tracing::debug!(organisation, "fetching github repositories");
|
||||||
|
|
||||||
|
let mut current_page = self
|
||||||
|
.client
|
||||||
|
.orgs(organisation)
|
||||||
|
.list_repos()
|
||||||
|
.per_page(100)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.context("failed to get github repositories")?;
|
||||||
|
|
||||||
|
let mut repos = current_page.take_items();
|
||||||
|
let new_repos = self
|
||||||
|
.client
|
||||||
|
.all_pages(current_page)
|
||||||
|
.await
|
||||||
|
.context("failed to get github pages")?;
|
||||||
|
repos.extend(new_repos);
|
||||||
|
|
||||||
|
let mut allowed_count = 0usize;
|
||||||
|
let mut denied_count = 0usize;
|
||||||
|
let fetched_count = repos.len();
|
||||||
|
|
||||||
|
let allowed_repos = repos
|
||||||
|
.into_iter()
|
||||||
|
.filter(|r| {
|
||||||
|
for a in &allow {
|
||||||
|
if a.is_match(&r.name) {
|
||||||
|
tracing::trace!(name = r.name, rule = a.to_string(), "allowed repository");
|
||||||
|
|
||||||
|
allowed_count += 1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
})
|
||||||
|
.filter(|r| {
|
||||||
|
for d in &deny {
|
||||||
|
if d.is_match(&r.name) {
|
||||||
|
tracing::trace!(name = r.name, rule = d.to_string(), "denied repository");
|
||||||
|
|
||||||
|
denied_count += 1;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
tracing::debug!(
|
||||||
|
organisation,
|
||||||
|
fetched_count,
|
||||||
|
allowed_count,
|
||||||
|
denied_count,
|
||||||
|
total_count = allowed_repos.len(),
|
||||||
|
"github repositories"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(allowed_repos)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
mod state;
|
mod state;
|
||||||
pub use state::*;
|
pub use state::*;
|
||||||
|
|
||||||
@@ -8,6 +10,8 @@ pub mod servehttp;
|
|||||||
mod forge_config;
|
mod forge_config;
|
||||||
mod forge_services;
|
mod forge_services;
|
||||||
|
|
||||||
|
mod forges;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> anyhow::Result<()> {
|
async fn main() -> anyhow::Result<()> {
|
||||||
dotenv::dotenv().ok();
|
dotenv::dotenv().ok();
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
type = "github"
|
# schedule.cron = "0 * * * * *"
|
||||||
deny = [".*"]
|
schedule.once = true
|
||||||
|
|
||||||
|
allow = ["^canopy-.*$"]
|
||||||
|
deny = ["^infrastructure-.*$", "^canopy-data-gateway$"]
|
||||||
|
|
||||||
|
[github]
|
||||||
|
credentials.token_env = "GITHUB_ACCESS_TOKEN"
|
||||||
|
organisation = "understory-io"
|
||||||
|
|
||||||
[policies]
|
[policies]
|
||||||
squash_merge_only.enabled = true
|
squash_merge_only.enabled = true
|
||||||
|
|||||||
Reference in New Issue
Block a user