images.rs
1 use docker_api::{ 2 models::ImageBuildChunk, 3 opts::{ContainerCreateOpts, PullOpts}, 4 Container, Docker, 5 }; 6 use rocket::{futures::StreamExt, serde::json::Json, State}; 7 use serde::{Deserialize, Serialize}; 8 use sqlx::Pool; 9 use uuid::Uuid; 10 11 use crate::{ 12 api::INTERNAL_SERVER_ERROR, 13 config::Config, 14 db::DB, 15 services::get_images_service::{self, Image}, 16 }; 17 18 use super::ErrorResponse; 19 20 #[derive(Serialize, Deserialize, Debug)] 21 pub struct ImagesResponse { 22 repositories: Vec<Image>, 23 } 24 25 #[derive(Responder, Debug)] 26 pub enum GetImagesResponse { 27 #[response(status = 200)] 28 Success(Json<ImagesResponse>), 29 #[response(status = 500)] 30 Failure(Json<ErrorResponse>), 31 } 32 33 #[get("/images")] 34 pub async fn get_images(db_pool: &State<Pool<DB>>) -> GetImagesResponse { 35 match get_images_service::get_all_images(db_pool).await { 36 Ok(repositories) => GetImagesResponse::Success(Json(ImagesResponse { repositories })), 37 Err(e) => { 38 error!("Failed to get images, err: {e:?}"); 39 GetImagesResponse::Failure(Json(ErrorResponse { 40 error: INTERNAL_SERVER_ERROR.to_string(), 41 })) 42 } 43 } 44 } 45 46 #[derive(Serialize, Deserialize)] 47 pub struct RunImageRequest { 48 name: String, 49 tag: String, 50 } 51 52 #[derive(Serialize, Deserialize)] 53 pub struct RunImageResponseData { 54 name: String, 55 } 56 57 #[derive(Responder)] 58 pub enum RunImageResponse { 59 #[response(status = 200)] 60 Sucess(Json<RunImageResponseData>), 61 #[response(status = 500)] 62 Failure(String), 63 } 64 65 #[post("/images", data = "<body>")] 66 pub async fn run_image( 67 body: Json<RunImageRequest>, 68 docker: &State<Docker>, 69 config: &State<Config>, 70 ) -> RunImageResponse { 71 let images = docker.images(); 72 73 let image_name = format!("{}/{}:{}", config.registry_url, body.name, body.tag); 74 75 let mut stream = images.pull( 76 &PullOpts::builder() 77 .image(&image_name) 78 .tag(body.tag.clone()) 79 .build(), 80 ); 81 82 while let Some(pull_result) = stream.next().await { 83 match pull_result { 84 Ok(ImageBuildChunk::Digest { aux }) => info!("Image pull DIGEST aux: {aux:?}"), 85 Ok(ImageBuildChunk::Update { stream }) => { 86 info!("Image pull UPDATE stream: {stream}") 87 } 88 Ok(ImageBuildChunk::PullStatus { 89 status, 90 id: _, 91 progress: _, 92 progress_detail: _, 93 }) => { 94 info!("Image pull PULL STATUS status: {status}") 95 } 96 Ok(ImageBuildChunk::Error { 97 error, 98 error_detail, 99 }) => error!("Image pull ERROR '{error}', details: '{error_detail:?}'"), 100 Err(e) => { 101 error!("Err: {e:?}"); 102 return RunImageResponse::Failure(String::from( 103 "Failed to pull image, maybe it doesn't exist?", 104 )); 105 } 106 } 107 } 108 109 // We should now have the image locally 110 let containers = docker.containers(); 111 112 let id = Uuid::new_v4(); // Random ID to keep it unique 113 let container_name = format!("GAME_CONTAINER_{}_{}__{id}", body.name, body.tag); 114 let container = match containers 115 .create( 116 &ContainerCreateOpts::builder() 117 .image(&image_name) 118 .name(container_name.clone()) 119 .build(), 120 ) 121 .await 122 { 123 Ok(c) => c, 124 Err(e) => { 125 error!("Failed to create container, err: {e:?}"); 126 return RunImageResponse::Failure(String::from( 127 "Failed to create container from image :(", 128 )); 129 } 130 }; 131 132 if let Err(e) = container.start().await { 133 error!("Failed to start container, err: {e:?}"); 134 } 135 136 RunImageResponse::Sucess(Json(RunImageResponseData { 137 name: container.id().to_string(), 138 })) 139 } 140 141 #[derive(Serialize, Deserialize)] 142 pub enum ContainerStatus { 143 Running, 144 Dead, 145 } 146 147 #[derive(Serialize, Deserialize)] 148 pub struct GetContainerResponseData { 149 status: ContainerStatus, 150 } 151 152 #[derive(Responder)] 153 pub enum GetContainerResponse { 154 #[response(status = 200)] 155 Success(Json<GetContainerResponseData>), 156 #[response(status = 404)] 157 NotFound(()), 158 #[response(status = 500)] 159 Failure(String), 160 } 161 162 #[get("/images/status/<id>")] 163 pub async fn get_container_status(id: String, docker: &State<Docker>) -> GetContainerResponse { 164 let containers = docker.containers(); 165 let container = containers.get(id); 166 167 let container_status = match get_status(container).await { 168 Some(s) => s, 169 None => { 170 error!("Failed to get container state"); 171 return GetContainerResponse::Failure(String::from("Failed to get container state")); 172 } 173 }; 174 175 GetContainerResponse::Success(Json(GetContainerResponseData { 176 status: container_status, 177 })) 178 } 179 180 async fn get_status(container: Container) -> Option<ContainerStatus> { 181 let state = match container.inspect().await { 182 Ok(o) => o, 183 Err(e) => { 184 error!("Failed to inspect container, err: {e:?}"); 185 return None; 186 } 187 } 188 .state?; 189 190 let running = state.running?; 191 let dead = state.dead?; 192 let oom_killed = state.oom_killed?; 193 if !running || dead || oom_killed { 194 if state.exit_code.is_some() && state.error.is_some() { 195 error!( 196 "Container seems to have gone awry (exited with code {:?}), error: {:?}", 197 state.exit_code, state.error 198 ); 199 } 200 201 return Some(ContainerStatus::Dead); 202 } 203 204 Some(ContainerStatus::Running) 205 }