Further scaffolding

This commit is contained in:
Niko Reunanen 2025-01-26 02:32:33 +02:00
parent b70f1c934d
commit f3366b3b2b
Signed by: nreunane
GPG key ID: D192625387DB0F16
2 changed files with 152 additions and 47 deletions

View file

@ -10,7 +10,7 @@ futures-util = { version = "0.3.31", registry = "cratesio" }
git2 = { version = "0.20.0" } git2 = { version = "0.20.0" }
serde = { version = "1.0.217", features = ["derive"] } serde = { version = "1.0.217", features = ["derive"] }
tokio = { version = "1.43.0", features = ["full"] } tokio = { version = "1.43.0", features = ["full"] }
tower-http = { version = "0.6.2", features = ["trace"] } tower-http = { version = "0.6.2", features = ["timeout", "trace"] }
tracing = { version = "0.1.41" } tracing = { version = "0.1.41" }
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }

View file

@ -1,4 +1,4 @@
use std::{collections::HashMap, process::ExitCode, sync::Arc}; use std::{process::ExitCode, sync::Arc, time::Duration};
use axum::{ use axum::{
extract::State, extract::State,
@ -6,12 +6,16 @@ use axum::{
routing::{get, post}, routing::{get, post},
Json, Router, Json, Router,
}; };
use bollard::{image::BuildImageOptions, Docker}; use bollard::{
container::{CreateContainerOptions, StartContainerOptions},
image::BuildImageOptions,
Docker,
};
use futures_util::stream::StreamExt; use futures_util::stream::StreamExt;
use git2::Repository; use git2::Repository;
use serde::Deserialize; use serde::Deserialize;
use tokio::sync::Mutex; use tokio::{signal, sync::Mutex};
use tower_http::trace::TraceLayer; use tower_http::{timeout::TimeoutLayer, trace::TraceLayer};
use tracing::level_filters::LevelFilter; use tracing::level_filters::LevelFilter;
struct AppState { struct AppState {
@ -20,80 +24,175 @@ struct AppState {
} }
#[derive(Deserialize)] #[derive(Deserialize)]
struct CdBuild { struct CdBuildGit {
url: String, url: String,
path: String, path: String,
serve: bool, branch: String,
remote: String,
}
#[derive(Deserialize)]
struct CdBuildDocker {
image: String,
tag: String,
name: String, name: String,
} }
#[derive(Deserialize)]
struct CdBuild {
docker: CdBuildDocker,
git: CdBuildGit,
serve: bool,
}
async fn build( async fn build(
State(state): State<Arc<Mutex<AppState>>>, State(state): State<Arc<Mutex<AppState>>>,
Json(cd): Json<CdBuild>, Json(cd): Json<CdBuild>,
) -> (StatusCode, &'static str) { ) -> (StatusCode, &'static str) {
match tokio::fs::try_exists(&cd.path).await { // Clone or fetch the latest version of build repository
match tokio::fs::try_exists(&cd.git.path).await {
Ok(true) => { Ok(true) => {
let repo = Repository::open(&cd.path).unwrap(); tracing::info!("loading {}", &cd.git.path);
repo.find_remote("origin") let repo = match Repository::open(&cd.git.path) {
.unwrap() Ok(repo) => repo,
.fetch(&["main"], None, None) Err(error) => {
.unwrap(); tracing::error!("Repository::open: {error}");
return (StatusCode::INTERNAL_SERVER_ERROR, "Repository::open");
}
};
let Ok(mut remote) = repo.find_remote(&cd.git.remote) else {
tracing::error!("remote not found");
return (StatusCode::BAD_REQUEST, "remote not found");
};
if remote.fetch(&[cd.git.branch], None, None).is_err() {
tracing::error!("remote.fetch error");
return (StatusCode::BAD_REQUEST, "remote.fetch error");
}
} }
Ok(false) => { Ok(false) => {
Repository::clone(&cd.url, &cd.path).unwrap(); tracing::info!("cloning {} to {}", &cd.git.url, &cd.git.path);
Repository::clone(&cd.git.url, &cd.git.path).unwrap();
} }
Err(error) => { Err(error) => {
tracing::error!("{error}"); tracing::error!("{error}");
return (StatusCode::INTERNAL_SERVER_ERROR, "tokio::fs::try_exists");
} }
} }
let build_image_options = BuildImageOptions { let build_image_options = BuildImageOptions {
dockerfile: "Dockerfile", dockerfile: "Dockerfile",
t: "bollard-build-example", t: &format!("{}:{}", &cd.docker.image, &cd.docker.tag),
extrahosts: Some("myhost:127.0.0.1"),
remote: "",
q: false,
nocache: false,
cachefrom: vec![],
pull: true,
rm: true, rm: true,
forcerm: true, ..BuildImageOptions::default()
memory: Some(120_000_000),
memswap: Some(500_000),
cpushares: Some(2),
cpusetcpus: "0-3",
cpuperiod: Some(2000),
cpuquota: Some(1000),
buildargs: HashMap::new(),
shmsize: Some(1_000_000),
squash: false,
labels: HashMap::new(),
networkmode: "host",
platform: "linux/x86_64",
target: "",
#[cfg(feature = "buildkit")]
session: None,
#[cfg(feature = "buildkit")]
outputs: None,
version: bollard::image::BuilderVersion::BuilderV1,
}; };
// Build image from the repository
{ {
let mtx = state.lock().await; let mtx = state.lock().await;
let mut image_build_stream = mtx.docker.build_image(build_image_options, None, None); let mut image_build_stream = mtx.docker.build_image(build_image_options, None, None);
while let Some(msg) = image_build_stream.next().await { while let Some(msg) = image_build_stream.next().await {
tracing::info!("{msg:?}"); tracing::info!("docker build image: {msg:?}");
} }
} }
if cd.serve { if cd.serve {
let mut mtx = state.lock().await; return serve_container(cd.docker, state).await;
mtx.name = Some(cd.name);
} }
(StatusCode::OK, "ok") (StatusCode::OK, "ok")
} }
async fn stop(State(state): State<Arc<Mutex<AppState>>>) -> (StatusCode, &'static str) {
let mut mtx = state.lock().await;
if let Some(name) = &mtx.name {
tracing::info!("stopping container {name}");
match mtx.docker.stop_container(name, None).await {
Ok(()) => {
mtx.name = None;
tracing::info!("container stopped");
(StatusCode::OK, "container stopped")
}
Err(error) => {
tracing::error!("stop_container: {error}");
(StatusCode::INTERNAL_SERVER_ERROR, "failed to stop")
}
}
} else {
tracing::warn!("stop: container is not running");
(StatusCode::OK, "stop: container is not running")
}
}
async fn serve(
State(state): State<Arc<Mutex<AppState>>>,
Json(cd): Json<CdBuildDocker>,
) -> (StatusCode, &'static str) {
serve_container(cd, state).await
}
async fn serve_container(
cd: CdBuildDocker,
state: Arc<Mutex<AppState>>,
) -> (StatusCode, &'static str) {
let mut mtx = state.lock().await;
let options = Some(CreateContainerOptions {
name: cd.name.clone(),
platform: Some("linux/amd64".into()),
});
let config = bollard::container::Config {
image: Some(cd.tag),
..bollard::container::Config::default()
};
tracing::info!("create_container");
tracing::info!("{options:?}");
tracing::info!("{config:?}");
if let Err(error) = mtx.docker.create_container(options, config).await {
tracing::error!("create_container: {error}");
return (StatusCode::INTERNAL_SERVER_ERROR, "create_container");
}
if let Err(error) = mtx
.docker
.start_container(&cd.name, None::<StartContainerOptions<&str>>)
.await
{
tracing::error!("start_container: {error}");
return (StatusCode::INTERNAL_SERVER_ERROR, "start_container");
}
mtx.name = Some(cd.name);
(StatusCode::OK, "ok")
}
async fn shutdown_signal() {
let ctrl_c = async {
signal::ctrl_c()
.await
.expect("failed to install ctrl-c handler");
};
let terminate = async {
signal::unix::signal(signal::unix::SignalKind::terminate())
.expect("failed to install signal handler")
.recv()
.await;
};
tokio::select! {
() = ctrl_c => {},
() = terminate => {},
}
}
#[tokio::main] #[tokio::main]
async fn main() -> ExitCode { async fn main() -> ExitCode {
let docker = match Docker::connect_with_socket_defaults() { let docker = match Docker::connect_with_socket_defaults() {
@ -136,12 +235,18 @@ async fn main() -> ExitCode {
let app = Router::new() let app = Router::new()
.route("/api/v1/build", post(build)) .route("/api/v1/build", post(build))
.route("/api/v1/serve", get("todo")) .route("/api/v1/serve", get(serve))
.route("/api/v1/stop", get("todo")) .route("/api/v1/stop", get(stop))
.layer(TraceLayer::new_for_http()) .layer((
TraceLayer::new_for_http(),
TimeoutLayer::new(Duration::from_secs(10)),
))
.with_state(shared_state); .with_state(shared_state);
match axum::serve(listener, app).await { match axum::serve(listener, app)
.with_graceful_shutdown(shutdown_signal())
.await
{
Ok(()) => ExitCode::SUCCESS, Ok(()) => ExitCode::SUCCESS,
Err(error) => { Err(error) => {
tracing::error!("axum::serve: {error}"); tracing::error!("axum::serve: {error}");