/ backend / src / api / images.rs
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  }