feat(server/forgejo): load repo config

This commit is contained in:
Paul Campbell 2024-04-09 10:44:01 +01:00
parent a7a64f5a07
commit b093c002d4
9 changed files with 212 additions and 36 deletions

View file

@ -2,7 +2,7 @@ mod init;
mod server; mod server;
use clap::Parser; use clap::Parser;
use kxio::filesystem; use kxio::{filesystem, network::Network};
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[clap(version = clap::crate_version!(), author = clap::crate_authors!(), about = clap::crate_description!())] #[clap(version = clap::crate_version!(), author = clap::crate_authors!(), about = clap::crate_description!())]
@ -23,6 +23,7 @@ enum Server {
} }
fn main() { fn main() {
let fs = filesystem::FileSystem::new_real(None); let fs = filesystem::FileSystem::new_real(None);
let net = Network::new_real();
let commands = Commands::parse(); let commands = Commands::parse();
match commands.command { match commands.command {
@ -34,7 +35,7 @@ fn main() {
server::init(fs); server::init(fs);
} }
Server::Start => { Server::Start => {
server::start(fs); server::start(fs, net);
} }
}, },
} }

View file

@ -1,25 +0,0 @@
use actix::prelude::*;
use tracing::info;
use crate::server::config::RepoDetails;
pub struct RepoActor {
details: RepoDetails,
}
impl RepoActor {
pub(crate) const fn new(details: RepoDetails) -> Self {
Self { details }
}
}
impl Actor for RepoActor {
type Context = Context<Self>;
}
#[derive(Message)]
#[rtype(result = "()")]
pub struct StartRepo;
impl Handler<StartRepo> for RepoActor {
type Result = ();
fn handle(&mut self, _msg: StartRepo, _ctx: &mut Self::Context) -> Self::Result {
info!(%self.details, "Starting Repo");
}
}

View file

@ -0,0 +1,22 @@
use actix::prelude::*;
use kxio::network::Network;
use tracing::error;
use crate::server::{
config::{ForgeType, RepoDetails},
forge,
};
use super::{LoadedConfig, RepoActor};
pub async fn load(details: RepoDetails, addr: Addr<RepoActor>, net: Network) {
let config = match details.forge.forge_type {
ForgeType::ForgeJo => forge::forgejo::config::load(details, &net).await,
};
match config {
Ok(config) => addr.do_send(LoadedConfig(config)),
Err(err) => {
error!(?err, "Failed to load config");
}
}
}

View file

@ -0,0 +1,51 @@
mod config;
use actix::prelude::*;
use kxio::network::Network;
use tracing::info;
use crate::server::config::{RepoConfig, RepoDetails};
pub struct RepoActor {
details: RepoDetails,
config: Option<RepoConfig>, // INFO: if [None] then send [StartRepo] to populate it
net: Network,
}
impl RepoActor {
pub(crate) const fn new(details: RepoDetails, net: Network) -> Self {
Self {
details,
config: None,
net,
}
}
}
impl Actor for RepoActor {
type Context = Context<Self>;
}
#[derive(Message)]
#[rtype(result = "()")]
pub struct StartRepo;
impl Handler<StartRepo> for RepoActor {
type Result = ();
fn handle(&mut self, _msg: StartRepo, ctx: &mut Self::Context) -> Self::Result {
info!(%self.details, "Starting Repo");
let details = self.details.clone();
let addr = ctx.address();
let net = self.net.clone();
config::load(details, addr, net).into_actor(self).wait(ctx);
}
}
#[derive(Message)]
#[rtype(result = "()")]
struct LoadedConfig(pub RepoConfig);
impl Handler<LoadedConfig> for RepoActor {
type Result = ();
fn handle(&mut self, msg: LoadedConfig, _ctx: &mut Self::Context) -> Self::Result {
let config = msg.0;
info!(%self.details, %config, "Config loaded");
self.config.replace(config);
}
}

View file

@ -35,12 +35,22 @@ impl RepoConfig {
toml::from_str(toml).map_err(OneOf::new) toml::from_str(toml).map_err(OneOf::new)
} }
} }
impl Display for RepoConfig {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self.branches)
}
}
#[derive(Debug, PartialEq, Eq, Deserialize)] #[derive(Debug, PartialEq, Eq, Deserialize)]
pub struct RepoBranches { pub struct RepoBranches {
main: String, main: String,
next: String, next: String,
dev: String, dev: String,
} }
impl Display for RepoBranches {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
pub struct Forge { pub struct Forge {

View file

@ -0,0 +1,112 @@
use base64::Engine;
use kxio::network::{self, Network};
use terrors::OneOf;
use tracing::{error, info};
use crate::server::config::{RepoConfig, RepoDetails};
pub async fn load(
details: RepoDetails,
net: &Network,
) -> Result<
RepoConfig,
OneOf<(
RepoConfigFileNotFound,
RepoConfigIsNotFile,
RepoConfigDecodeError,
RepoConfigParseError,
RepoConfigUnknownError,
)>,
> {
let hostname = details.forge.hostname;
let path = details.repo;
let filepath = ".git-next.toml";
let branch = details.branch;
let token = details.forge.token;
let url = network::NetUrl::new(format!(
"https://{hostname}/api/v1/repos/{path}/contents/{filepath}?ref={branch}&token={token}"
));
info!(%url, "Loading config");
let request = network::NetRequest::new(
network::RequestMethod::Get,
url,
network::NetRequestHeaders::new(),
network::RequestBody::None,
network::ResponseType::Json,
None,
network::NetRequestLogging::Both,
);
let result = net.get::<ForgeContentsResponse>(request).await;
let response = result.map_err(|e| match e {
network::NetworkError::RequestFailed(_, status_code, _)
| network::NetworkError::RequestError(_, status_code, _) => match status_code {
network::StatusCode::NOT_FOUND => OneOf::new(RepoConfigFileNotFound),
_ => OneOf::new(RepoConfigUnknownError(status_code)),
},
_ => OneOf::new(RepoConfigUnknownError(
network::StatusCode::INTERNAL_SERVER_ERROR,
)),
})?;
let status = response.status_code();
match response.response_body() {
Some(body) => {
// we need to decode (see encoding field) the value of 'content' from the response
match body.content_type {
ForgeContentsType::File => decode_config(body).map_err(OneOf::broaden),
_ => Err(OneOf::new(RepoConfigIsNotFile)),
}
}
None => {
error!(%status, "Failed to fetch repo config file");
Err(OneOf::new(RepoConfigUnknownError(status)))
}
}
}
fn decode_config(
body: ForgeContentsResponse,
) -> Result<RepoConfig, OneOf<(RepoConfigDecodeError, RepoConfigParseError)>> {
match body.encoding.as_str() {
"base64" => {
let decoded = base64::engine::general_purpose::STANDARD
.decode(body.content)
.map_err(|_| OneOf::new(RepoConfigDecodeError))?;
let decoded =
String::from_utf8(decoded).map_err(|_| OneOf::new(RepoConfigDecodeError))?;
let config = toml::from_str(&decoded).map_err(|_| OneOf::new(RepoConfigParseError))?;
Ok(config)
}
_ => Err(OneOf::new(RepoConfigDecodeError)),
}
}
#[derive(Debug)]
pub struct RepoConfigFileNotFound;
#[derive(Debug)]
pub struct RepoConfigIsNotFile;
#[derive(Debug)]
pub struct RepoConfigDecodeError;
#[derive(Debug)]
pub struct RepoConfigParseError;
#[derive(Debug)]
pub struct RepoConfigUnknownError(pub network::StatusCode);
#[derive(Clone, Debug, serde::Deserialize)]
struct ForgeContentsResponse {
#[serde(rename = "type")]
pub content_type: ForgeContentsType,
pub content: String,
pub encoding: String,
}
#[derive(Clone, Debug, serde::Deserialize)]
enum ForgeContentsType {
#[serde(rename = "file")]
File,
#[serde(rename = "dir")]
Dir,
#[serde(rename = "symlink")]
Symlink,
#[serde(rename = "submodule")]
Submodule,
}

View file

@ -0,0 +1 @@
pub mod config;

1
src/server/forge/mod.rs Normal file
View file

@ -0,0 +1 @@
pub mod forgejo;

View file

@ -1,7 +1,9 @@
mod actors; mod actors;
mod config; mod config;
pub mod forge;
use actix::prelude::*; use actix::prelude::*;
use kxio::network::Network;
use std::path::PathBuf; use std::path::PathBuf;
@ -32,7 +34,7 @@ pub fn init(fs: FileSystem) {
} }
} }
pub fn start(fs: FileSystem) { pub fn start(fs: FileSystem, net: Network) {
let Ok(_) = init_logging() else { let Ok(_) = init_logging() else {
eprintln!("Failed to initialize logging."); eprintln!("Failed to initialize logging.");
return; return;
@ -49,7 +51,7 @@ pub fn start(fs: FileSystem) {
info!("Config loaded"); info!("Config loaded");
let actors = config let actors = config
.forges() .forges()
.flat_map(|(forge_name, forge)| create_forge_repos(forge, forge_name)) .flat_map(|(forge_name, forge)| create_forge_repos(forge, forge_name, &net))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
actix::System::new().block_on(async { actix::System::new().block_on(async {
@ -63,6 +65,7 @@ pub fn start(fs: FileSystem) {
fn create_forge_repos( fn create_forge_repos(
forge: &Forge, forge: &Forge,
forge_name: ForgeName, forge_name: ForgeName,
net: &Network,
) -> Vec<(ForgeName, RepoName, RepoActor)> { ) -> Vec<(ForgeName, RepoName, RepoActor)> {
let forge = forge.clone(); let forge = forge.clone();
let span = tracing::info_span!("Forge", %forge_name, %forge); let span = tracing::info_span!("Forge", %forge_name, %forge);
@ -70,24 +73,24 @@ fn create_forge_repos(
info!("Creating Forge"); info!("Creating Forge");
forge forge
.repos() .repos()
.map(create_actor(forge_name, forge.clone())) .map(create_actor(forge_name, forge.clone(), net))
.collect::<Vec<_>>() .collect::<Vec<_>>()
} }
fn create_actor( fn create_actor(
forge_name: ForgeName, forge_name: ForgeName,
forge: config::Forge, forge: config::Forge,
net: &Network,
) -> impl Fn((RepoName, &Repo)) -> (ForgeName, RepoName, RepoActor) { ) -> impl Fn((RepoName, &Repo)) -> (ForgeName, RepoName, RepoActor) {
let net = net.clone();
move |(repo_name, repo)| { move |(repo_name, repo)| {
let span = tracing::info_span!("Repo", %repo_name, %repo); let span = tracing::info_span!("Repo", %repo_name, %repo);
let _guard = span.enter(); let _guard = span.enter();
info!("Creating Repo"); info!("Creating Repo");
let actor = actors::repo::RepoActor::new(config::RepoDetails::new( let actor = actors::repo::RepoActor::new(
&repo_name, config::RepoDetails::new(&repo_name, repo, &forge_name, &forge),
repo, net.clone(),
&forge_name, );
&forge,
));
info!("Created Repo"); info!("Created Repo");
(forge_name.clone(), repo_name, actor) (forge_name.clone(), repo_name, actor)
} }