@@ -15,3 +15,7 @@ serde.workspace = true
|
||||
uuid.workspace = true
|
||||
tower-http.workspace = true
|
||||
notmad.workspace = true
|
||||
tokio-util = "0.7.18"
|
||||
async-trait = "0.1.89"
|
||||
regex = "1.12.3"
|
||||
toml = "0.9.11"
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use axum::{Router, extract::MatchedPath, http::Request, routing::get};
|
||||
use tower_http::trace::TraceLayer;
|
||||
|
||||
use crate::State;
|
||||
use crate::{State, forge_services::load, servehttp::ServeHttp};
|
||||
|
||||
#[derive(clap::Parser)]
|
||||
pub struct ServeCommand {
|
||||
@@ -13,37 +10,20 @@ pub struct ServeCommand {
|
||||
|
||||
impl ServeCommand {
|
||||
pub async fn execute(&self, state: &State) -> anyhow::Result<()> {
|
||||
let app = Router::new()
|
||||
.route("/", get(root))
|
||||
.with_state(state.clone())
|
||||
.layer(
|
||||
TraceLayer::new_for_http().make_span_with(|request: &Request<_>| {
|
||||
// Log the matched route's path (with placeholders not filled in).
|
||||
// Use request.uri() or OriginalUri if you want the real path.
|
||||
let matched_path = request
|
||||
.extensions()
|
||||
.get::<MatchedPath>()
|
||||
.map(MatchedPath::as_str);
|
||||
let svcs = load(&state.config.config_dir, state).await?;
|
||||
|
||||
tracing::info_span!(
|
||||
"http_request",
|
||||
method = ?request.method(),
|
||||
matched_path,
|
||||
some_other_field = tracing::field::Empty,
|
||||
)
|
||||
}), // ...
|
||||
);
|
||||
let mut mad = notmad::Mad::builder();
|
||||
mad.add(ServeHttp {
|
||||
host: self.host,
|
||||
state: state.clone(),
|
||||
});
|
||||
|
||||
tracing::info!("listening on {}", self.host);
|
||||
let listener = tokio::net::TcpListener::bind(self.host).await.unwrap();
|
||||
axum::serve(listener, app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
for svc in svcs {
|
||||
mad.add(svc);
|
||||
}
|
||||
|
||||
mad.run().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn root() -> &'static str {
|
||||
"Hello, nostore!"
|
||||
}
|
||||
|
||||
90
crates/forge-enforce/src/forge_config.rs
Normal file
90
crates/forge-enforce/src/forge_config.rs
Normal file
@@ -0,0 +1,90 @@
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct ForgeConfig {
|
||||
#[serde(flatten)]
|
||||
pub forge_type: ForgeConfigType,
|
||||
|
||||
#[serde(default = "ForgeSchedule::default")]
|
||||
pub schedule: ForgeSchedule,
|
||||
|
||||
#[serde(default = "allow_all")]
|
||||
pub allow: Vec<ForgeRegex>,
|
||||
#[serde(default = "Vec::new")]
|
||||
pub deny: Vec<ForgeRegex>,
|
||||
|
||||
#[serde(default)]
|
||||
pub policies: Policies,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
pub struct Policies {
|
||||
#[serde(default)]
|
||||
pub squash_merge_only: PolicyOption,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
pub struct PolicyOption {
|
||||
#[serde(default)]
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
fn allow_all() -> Vec<ForgeRegex> {
|
||||
vec![ForgeRegex {
|
||||
regex: Regex::new(".*").unwrap(),
|
||||
}]
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub enum ForgeSchedule {
|
||||
Cron {},
|
||||
Interval {},
|
||||
Once {},
|
||||
}
|
||||
|
||||
impl Default for ForgeSchedule {
|
||||
fn default() -> Self {
|
||||
Self::Interval {}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ForgeRegex {
|
||||
regex: Regex,
|
||||
}
|
||||
|
||||
impl std::ops::Deref for ForgeRegex {
|
||||
type Target = Regex;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.regex
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ForgeRegex {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("ForgeRegex")
|
||||
.field("regex", &self.regex)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ForgeRegex {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
Regex::new(&s)
|
||||
.map(|regex| ForgeRegex { regex })
|
||||
.map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum ForgeConfigType {
|
||||
#[serde(rename = "github")]
|
||||
GitHub {},
|
||||
}
|
||||
58
crates/forge-enforce/src/forge_services.rs
Normal file
58
crates/forge-enforce/src/forge_services.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
use std::path::Path;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use notmad::{Component, MadError};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::{State, forge_config::ForgeConfig};
|
||||
|
||||
pub struct ForgeService {
|
||||
name: String,
|
||||
config: ForgeConfig,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Component for ForgeService {
|
||||
fn name(&self) -> Option<String> {
|
||||
Some(format!("forge-enforce/{}", self.name))
|
||||
}
|
||||
|
||||
async fn run(&self, cancellation_token: CancellationToken) -> Result<(), MadError> {
|
||||
cancellation_token.cancelled().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn load(path: &Path, state: &State) -> anyhow::Result<Vec<ForgeService>> {
|
||||
if !path.exists() {
|
||||
anyhow::bail!("config path does not exist: {}", path.display());
|
||||
}
|
||||
|
||||
let mut res = tokio::fs::read_dir(&path).await?;
|
||||
let mut files = Vec::new();
|
||||
while let Ok(Some(entry)) = res.next_entry().await {
|
||||
if !entry.metadata().await?.is_file() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let content = tokio::fs::read(entry.path()).await?;
|
||||
|
||||
let config: ForgeConfig = toml::from_slice(&content)?;
|
||||
|
||||
files.push((
|
||||
entry
|
||||
.file_name()
|
||||
.into_string()
|
||||
.expect("to have strings that are utf"),
|
||||
config,
|
||||
));
|
||||
}
|
||||
|
||||
let services = files
|
||||
.into_iter()
|
||||
.map(|(name, config)| ForgeService { config, name })
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(services)
|
||||
}
|
||||
@@ -3,6 +3,11 @@ pub use state::*;
|
||||
|
||||
pub mod cli;
|
||||
|
||||
pub mod servehttp;
|
||||
|
||||
mod forge_config;
|
||||
mod forge_services;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
dotenv::dotenv().ok();
|
||||
|
||||
57
crates/forge-enforce/src/servehttp.rs
Normal file
57
crates/forge-enforce/src/servehttp.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use axum::{Router, extract::MatchedPath, http::Request, routing::get};
|
||||
use notmad::{Component, MadError};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tower_http::trace::TraceLayer;
|
||||
|
||||
use crate::State;
|
||||
|
||||
pub struct ServeHttp {
|
||||
pub host: SocketAddr,
|
||||
pub state: State,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Component for ServeHttp {
|
||||
fn name(&self) -> Option<String> {
|
||||
Some("forge-enforce/http".into())
|
||||
}
|
||||
|
||||
async fn run(&self, cancellation_token: CancellationToken) -> Result<(), MadError> {
|
||||
let app = Router::new()
|
||||
.route("/", get(root))
|
||||
.with_state(self.state.clone())
|
||||
.layer(
|
||||
TraceLayer::new_for_http().make_span_with(|request: &Request<_>| {
|
||||
// Log the matched route's path (with placeholders not filled in).
|
||||
// Use request.uri() or OriginalUri if you want the real path.
|
||||
let matched_path = request
|
||||
.extensions()
|
||||
.get::<MatchedPath>()
|
||||
.map(MatchedPath::as_str);
|
||||
|
||||
tracing::info_span!(
|
||||
"http_request",
|
||||
method = ?request.method(),
|
||||
matched_path,
|
||||
some_other_field = tracing::field::Empty,
|
||||
)
|
||||
}), // ...
|
||||
);
|
||||
|
||||
tracing::info!("listening on {}", self.host);
|
||||
let listener = tokio::net::TcpListener::bind(self.host).await.unwrap();
|
||||
axum::serve(listener, app.into_make_service())
|
||||
.with_graceful_shutdown(async move { cancellation_token.cancelled().await })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn root() -> &'static str {
|
||||
"Hello, forge-enforce!"
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(clap::Parser, Clone)]
|
||||
pub struct Config {}
|
||||
pub struct Config {
|
||||
#[arg(long, env = "FE_CONFIG_DIR")]
|
||||
pub config_dir: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct State {
|
||||
|
||||
Reference in New Issue
Block a user