Pull image from public registry

This commit is contained in:
Niko Reunanen 2025-02-11 18:38:33 +02:00
parent f3366b3b2b
commit 35b3f4920b
Signed by: nreunane
GPG key ID: D192625387DB0F16

View file

@ -1,25 +1,24 @@
use std::{process::ExitCode, sync::Arc, time::Duration}; use std::{process::ExitCode, sync::Arc, time::Duration};
use axum::{ use axum::{
extract::State, extract::State, http::StatusCode, response::{IntoResponse, Response}, routing::{get, post}, Json, Router
http::StatusCode,
routing::{get, post},
Json, Router,
}; };
use bollard::{ use bollard::{
container::{CreateContainerOptions, StartContainerOptions}, container::{CreateContainerOptions, StartContainerOptions},
image::BuildImageOptions, image::{BuildImageOptions, CreateImageOptions},
Docker, Docker,
}; };
use futures_util::stream::StreamExt; use futures_util::stream::StreamExt;
use git2::Repository; use git2::Repository;
use serde::Deserialize; use serde::{Deserialize, Serialize};
use tokio::{signal, sync::Mutex}; use tokio::{signal, sync::Mutex};
use tower_http::{timeout::TimeoutLayer, trace::TraceLayer}; use tower_http::{timeout::TimeoutLayer, trace::TraceLayer};
use tracing::level_filters::LevelFilter; use tracing::level_filters::LevelFilter;
struct AppState { struct AppState {
docker: Docker, docker: Docker,
pub(crate) image: Option<String>,
pub(crate) tag: Option<String>,
pub(crate) name: Option<String>, pub(crate) name: Option<String>,
} }
@ -45,6 +44,51 @@ struct CdBuild {
serve: bool, serve: bool,
} }
#[derive(Serialize)]
struct MsgStatus {
image: Option<String>,
tag: Option<String>,
name: Option<String>,
}
#[derive(Deserialize)]
struct Selector {
image: String,
tag: String,
}
async fn pull(
State(state): State<Arc<Mutex<AppState>>>,
Json(cd): Json<Selector>,
) -> Response {
let mtx = state.lock().await;
let options = Some(CreateImageOptions{
from_image: format!("{}:{}", cd.image, cd.tag),
..Default::default()
});
let mut image_create_stream = mtx.docker.create_image(options, None, None);
while let Some(msg) = image_create_stream.next().await {
if let Ok(msg) = msg {
tracing::info!("docker create image: {} {}", msg.status.unwrap_or_default(), msg.progress.unwrap_or_default());
}
}
StatusCode::OK.into_response()
}
async fn status(
State(state): State<Arc<Mutex<AppState>>>,
) -> Json<MsgStatus> {
let mtx = state.lock().await;
Json(MsgStatus {
name: mtx.name.clone(),
image: mtx.image.clone(),
tag: mtx.tag.clone(),
})
}
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>,
@ -105,25 +149,25 @@ async fn build(
(StatusCode::OK, "ok") (StatusCode::OK, "ok")
} }
async fn stop(State(state): State<Arc<Mutex<AppState>>>) -> (StatusCode, &'static str) { async fn stop(State(state): State<Arc<Mutex<AppState>>>) -> (StatusCode, String) {
let mut mtx = state.lock().await; let mut mtx = state.lock().await;
if let Some(name) = &mtx.name { if let Some(name) = mtx.name.clone() {
tracing::info!("stopping container {name}"); tracing::info!("stopping container {name}");
match mtx.docker.stop_container(name, None).await { match mtx.docker.stop_container(&name, None).await {
Ok(()) => { Ok(()) => {
mtx.name = None; mtx.name = None;
tracing::info!("container stopped"); tracing::info!("container {name} stopped");
(StatusCode::OK, "container stopped") (StatusCode::OK, format!("container {name} stopped"))
} }
Err(error) => { Err(error) => {
tracing::error!("stop_container: {error}"); tracing::error!("stop_container: {error}");
(StatusCode::INTERNAL_SERVER_ERROR, "failed to stop") (StatusCode::INTERNAL_SERVER_ERROR, "failed to stop".into())
} }
} }
} else { } else {
tracing::warn!("stop: container is not running"); tracing::warn!("no running container available");
(StatusCode::OK, "stop: container is not running") (StatusCode::OK, "no running container available".into())
} }
} }
@ -146,14 +190,10 @@ async fn serve_container(
}); });
let config = bollard::container::Config { let config = bollard::container::Config {
image: Some(cd.tag), image: Some(format!("{}:{}", cd.image, cd.tag)),
..bollard::container::Config::default() ..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 { if let Err(error) = mtx.docker.create_container(options, config).await {
tracing::error!("create_container: {error}"); tracing::error!("create_container: {error}");
return (StatusCode::INTERNAL_SERVER_ERROR, "create_container"); return (StatusCode::INTERNAL_SERVER_ERROR, "create_container");
@ -169,6 +209,8 @@ async fn serve_container(
} }
mtx.name = Some(cd.name); mtx.name = Some(cd.name);
mtx.tag = Some(cd.tag);
mtx.image = Some(cd.image);
(StatusCode::OK, "ok") (StatusCode::OK, "ok")
} }
@ -195,6 +237,14 @@ async fn shutdown_signal() {
#[tokio::main] #[tokio::main]
async fn main() -> ExitCode { async fn main() -> ExitCode {
if let Err(error) = tracing_subscriber::fmt()
.with_max_level(LevelFilter::INFO)
.try_init()
{
eprintln!("failed to register a global tracing logger: {error}");
return ExitCode::FAILURE;
}
let docker = match Docker::connect_with_socket_defaults() { let docker = match Docker::connect_with_socket_defaults() {
Ok(docker) => docker, Ok(docker) => docker,
Err(error) => { Err(error) => {
@ -204,7 +254,9 @@ async fn main() -> ExitCode {
}; };
match docker.version().await { match docker.version().await {
Ok(version) => println!("docker version: {version:?}"), Ok(version) => {
tracing::info!("connected to docker version {}", version.version.unwrap_or_default());
},
Err(error) => { Err(error) => {
eprintln!("failed to read docker version: {error}"); eprintln!("failed to read docker version: {error}");
return ExitCode::FAILURE; return ExitCode::FAILURE;
@ -213,14 +265,6 @@ async fn main() -> ExitCode {
let bind = std::env::var("BIND").unwrap_or("0.0.0.0:8080".into()); let bind = std::env::var("BIND").unwrap_or("0.0.0.0:8080".into());
if let Err(error) = tracing_subscriber::fmt()
.with_max_level(LevelFilter::DEBUG)
.try_init()
{
eprintln!("failed to register a global tracing logger: {error}");
return ExitCode::FAILURE;
}
tracing::info!("launching microdeploy on {bind}"); tracing::info!("launching microdeploy on {bind}");
let listener = match tokio::net::TcpListener::bind(&bind).await { let listener = match tokio::net::TcpListener::bind(&bind).await {
@ -231,19 +275,21 @@ async fn main() -> ExitCode {
} }
}; };
let shared_state = Arc::new(Mutex::new(AppState { docker, name: None })); let shared_state = Arc::new(Mutex::new(AppState {docker, name: None, image: None, tag: None }));
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(serve)) .route("/api/v1/serve", post(serve))
.route("/api/v1/stop", get(stop)) .route("/api/v1/stop", get(stop))
.route("/api/v1/status", get(status))
.route("/api/v1/pull", post(pull))
.layer(( .layer((
TraceLayer::new_for_http(), TraceLayer::new_for_http(),
TimeoutLayer::new(Duration::from_secs(10)), TimeoutLayer::new(Duration::from_secs(10)),
)) ))
.with_state(shared_state); .with_state(shared_state);
match axum::serve(listener, app) let exit_code = match axum::serve(listener, app)
.with_graceful_shutdown(shutdown_signal()) .with_graceful_shutdown(shutdown_signal())
.await .await
{ {
@ -252,5 +298,9 @@ async fn main() -> ExitCode {
tracing::error!("axum::serve: {error}"); tracing::error!("axum::serve: {error}");
ExitCode::FAILURE ExitCode::FAILURE
} }
} };
tracing::info!("shutting down");
exit_code
} }