From f3366b3b2b4a4a6df83aa6abb7b22e1b8b469ac8 Mon Sep 17 00:00:00 2001 From: Niko Reunanen Date: Sun, 26 Jan 2025 02:32:33 +0200 Subject: [PATCH] Further scaffolding --- Cargo.toml | 2 +- src/bin/main.rs | 197 +++++++++++++++++++++++++++++++++++++----------- 2 files changed, 152 insertions(+), 47 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ab0bea1..33e8f51 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ futures-util = { version = "0.3.31", registry = "cratesio" } git2 = { version = "0.20.0" } serde = { version = "1.0.217", features = ["derive"] } 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-subscriber = { version = "0.3.19", features = ["env-filter"] } diff --git a/src/bin/main.rs b/src/bin/main.rs index 2fa07ea..6b61be0 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, process::ExitCode, sync::Arc}; +use std::{process::ExitCode, sync::Arc, time::Duration}; use axum::{ extract::State, @@ -6,12 +6,16 @@ use axum::{ routing::{get, post}, Json, Router, }; -use bollard::{image::BuildImageOptions, Docker}; +use bollard::{ + container::{CreateContainerOptions, StartContainerOptions}, + image::BuildImageOptions, + Docker, +}; use futures_util::stream::StreamExt; use git2::Repository; use serde::Deserialize; -use tokio::sync::Mutex; -use tower_http::trace::TraceLayer; +use tokio::{signal, sync::Mutex}; +use tower_http::{timeout::TimeoutLayer, trace::TraceLayer}; use tracing::level_filters::LevelFilter; struct AppState { @@ -20,80 +24,175 @@ struct AppState { } #[derive(Deserialize)] -struct CdBuild { +struct CdBuildGit { url: String, path: String, - serve: bool, + branch: String, + remote: String, +} + +#[derive(Deserialize)] +struct CdBuildDocker { + image: String, + tag: String, name: String, } +#[derive(Deserialize)] +struct CdBuild { + docker: CdBuildDocker, + git: CdBuildGit, + serve: bool, +} + async fn build( State(state): State>>, Json(cd): Json, ) -> (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) => { - let repo = Repository::open(&cd.path).unwrap(); - repo.find_remote("origin") - .unwrap() - .fetch(&["main"], None, None) - .unwrap(); + tracing::info!("loading {}", &cd.git.path); + let repo = match Repository::open(&cd.git.path) { + Ok(repo) => repo, + Err(error) => { + 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) => { - 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) => { tracing::error!("{error}"); + return (StatusCode::INTERNAL_SERVER_ERROR, "tokio::fs::try_exists"); } } let build_image_options = BuildImageOptions { dockerfile: "Dockerfile", - t: "bollard-build-example", - extrahosts: Some("myhost:127.0.0.1"), - remote: "", - q: false, - nocache: false, - cachefrom: vec![], - pull: true, + t: &format!("{}:{}", &cd.docker.image, &cd.docker.tag), rm: true, - forcerm: true, - 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, + ..BuildImageOptions::default() }; + // Build image from the repository { let mtx = state.lock().await; + let mut image_build_stream = mtx.docker.build_image(build_image_options, None, None); while let Some(msg) = image_build_stream.next().await { - tracing::info!("{msg:?}"); + tracing::info!("docker build image: {msg:?}"); } } if cd.serve { - let mut mtx = state.lock().await; - mtx.name = Some(cd.name); + return serve_container(cd.docker, state).await; } (StatusCode::OK, "ok") } +async fn stop(State(state): State>>) -> (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>>, + Json(cd): Json, +) -> (StatusCode, &'static str) { + serve_container(cd, state).await +} + +async fn serve_container( + cd: CdBuildDocker, + state: Arc>, +) -> (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::>) + .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] async fn main() -> ExitCode { let docker = match Docker::connect_with_socket_defaults() { @@ -136,12 +235,18 @@ async fn main() -> ExitCode { let app = Router::new() .route("/api/v1/build", post(build)) - .route("/api/v1/serve", get("todo")) - .route("/api/v1/stop", get("todo")) - .layer(TraceLayer::new_for_http()) + .route("/api/v1/serve", get(serve)) + .route("/api/v1/stop", get(stop)) + .layer(( + TraceLayer::new_for_http(), + TimeoutLayer::new(Duration::from_secs(10)), + )) .with_state(shared_state); - match axum::serve(listener, app).await { + match axum::serve(listener, app) + .with_graceful_shutdown(shutdown_signal()) + .await + { Ok(()) => ExitCode::SUCCESS, Err(error) => { tracing::error!("axum::serve: {error}");