Further scaffolding
This commit is contained in:
parent
b70f1c934d
commit
f3366b3b2b
2 changed files with 152 additions and 47 deletions
|
|
@ -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"] }
|
||||||
|
|
||||||
|
|
|
||||||
197
src/bin/main.rs
197
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::{
|
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}");
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue