From bb76713ed0a3bf2a002b710ee65f2182390f6890 Mon Sep 17 00:00:00 2001 From: Leo Vernisse Date: Thu, 24 Oct 2024 10:49:49 +0200 Subject: [PATCH] refactor/nanocld: cleaner image service with file separation (#1110) --- bin/nanocld/src/services/vm_image.rs | 343 ------------------ bin/nanocld/src/services/vm_image/clone.rs | 36 ++ bin/nanocld/src/services/vm_image/count.rs | 33 ++ .../src/services/vm_image/create_snapshot.rs | 37 ++ bin/nanocld/src/services/vm_image/delete.rs | 27 ++ bin/nanocld/src/services/vm_image/import.rs | 68 ++++ bin/nanocld/src/services/vm_image/inspect.rs | 30 ++ bin/nanocld/src/services/vm_image/list.rs | 32 ++ bin/nanocld/src/services/vm_image/mod.rs | 120 ++++++ bin/nanocld/src/services/vm_image/resize.rs | 31 ++ 10 files changed, 414 insertions(+), 343 deletions(-) delete mode 100644 bin/nanocld/src/services/vm_image.rs create mode 100644 bin/nanocld/src/services/vm_image/clone.rs create mode 100644 bin/nanocld/src/services/vm_image/count.rs create mode 100644 bin/nanocld/src/services/vm_image/create_snapshot.rs create mode 100644 bin/nanocld/src/services/vm_image/delete.rs create mode 100644 bin/nanocld/src/services/vm_image/import.rs create mode 100644 bin/nanocld/src/services/vm_image/inspect.rs create mode 100644 bin/nanocld/src/services/vm_image/list.rs create mode 100644 bin/nanocld/src/services/vm_image/mod.rs create mode 100644 bin/nanocld/src/services/vm_image/resize.rs diff --git a/bin/nanocld/src/services/vm_image.rs b/bin/nanocld/src/services/vm_image.rs deleted file mode 100644 index 88fe4a1fc..000000000 --- a/bin/nanocld/src/services/vm_image.rs +++ /dev/null @@ -1,343 +0,0 @@ -use std::io::Write; - -use futures::StreamExt; -use ntex::web; - -use nanocl_error::http::{HttpError, HttpResult}; - -use nanocl_stubs::{ - generic::{GenericCount, GenericListQuery}, - vm_image::VmImageResizePayload, -}; - -use crate::{ - models::{SystemState, VmImageDb}, - repositories::generic::*, - utils, -}; - -/// List virtual machine images -#[cfg_attr(feature = "dev", utoipa::path( - get, - tag = "VmImages", - path = "/vms/images", - params( - ("filter" = Option, Query, description = "Generic filter", example = "{ \"filter\": { \"where\": { \"name\": { \"eq\": \"my-image\" } } } }"), - ), - responses( - (status = 200, description = "List of vm images", body = [VmImage]), - ), -))] -#[web::get("/vms/images")] -pub async fn list_vm_images( - state: web::types::State, - qs: web::types::Query, -) -> HttpResult { - let filter = utils::query_string::parse_qs_filter(&qs)?; - let images = VmImageDb::read_by(&filter, &state.inner.pool).await?; - Ok(web::HttpResponse::Ok().json(&images)) -} - -/// Import a virtual machine image from a file -#[cfg_attr(feature = "dev", utoipa::path( - post, - tag = "VmImages", - request_body = String, - path = "/vms/images/{name}/import", - params( - ("name" = String, Path, description = "The name of the vm image"), - ), - responses( - (status = 200, description = "Image have been imported"), - ), -))] -#[web::post("/vms/images/{name}/import")] -pub async fn import_vm_image( - state: web::types::State, - path: web::types::Path<(String, String)>, - mut payload: web::types::Payload, -) -> HttpResult { - let name = path.1.to_owned(); - utils::key::validate_name(&name)?; - if VmImageDb::read_by_pk(&name, &state.inner.pool) - .await - .is_ok() - { - return Err(HttpError::conflict(format!("Vm image {name} already used"))); - } - let state_dir = state.inner.config.state_dir.clone(); - let vm_images_dir = format!("{state_dir}/vms/images"); - let filepath = format!("{vm_images_dir}/{name}.img"); - let fp = filepath.clone(); - let mut f = web::block(move || std::fs::File::create(fp)) - .await - .map_err(|err| { - HttpError::internal_server_error(format!( - "Unable to create vm image {name}: {err}" - )) - })?; - while let Some(bytes) = payload.next().await { - let bytes = bytes.map_err(|err| { - HttpError::internal_server_error(format!( - "Unable to create vm image {name}: {err}" - )) - })?; - f = web::block(move || f.write_all(&bytes).map(|_| f)) - .await - .map_err(|err| { - HttpError::internal_server_error(format!( - "Unable to create vm image {name}: {err}" - )) - })?; - } - utils::vm_image::create(&name, &filepath, &state).await?; - Ok(web::HttpResponse::Ok().into()) -} - -/// Create a snapshot of a virtual machine image -#[cfg_attr(feature = "dev", utoipa::path( - post, - tag = "VmImages", - request_body = String, - path = "/vms/images/{name}/snapshot/{snapshot_name}", - params( - ("name" = String, Path, description = "The name of the vm image"), - ("snap" = String, Path, description = "The name of the snapshot"), - ), - responses( - (status = 200, description = "The snapshot have been created", body = VmImage), - ), -))] -#[web::post("/vms/images/{name}/snapshot/{snapshot_name}")] -pub async fn snapshot_vm_image( - state: web::types::State, - path: web::types::Path<(String, String, String)>, -) -> HttpResult { - let name = path.1.to_owned(); - let snapshot_name = path.2.to_owned(); - utils::key::validate_name(&snapshot_name)?; - let image = VmImageDb::read_by_pk(&name, &state.inner.pool).await?; - let vm_image = - utils::vm_image::create_snap(&snapshot_name, 50, &image, &state).await?; - Ok(web::HttpResponse::Ok().json(&vm_image)) -} - -/// Clone a virtual machine image -#[cfg_attr(feature = "dev", utoipa::path( - post, - tag = "VmImages", - request_body = String, - path = "/vms/images/{name}/clone/{clone_name}", - params( - ("name" = String, Path, description = "The name of the vm image"), - ("clone_name" = String, Path, description = "The name of the clone"), - ), - responses( - (status = 200, description = "The snapshot have been created", body = VmImage), - ), -))] -#[web::post("/vms/images/{name}/clone/{clone_name}")] -pub async fn clone_vm_image( - state: web::types::State, - path: web::types::Path<(String, String, String)>, -) -> HttpResult { - let name = path.1.to_owned(); - let clone_name = path.2.to_owned(); - utils::key::validate_name(&clone_name)?; - let image = VmImageDb::read_by_pk(&name, &state.inner.pool).await?; - let rx = utils::vm_image::clone(&clone_name, &image, &state).await?; - Ok(web::HttpResponse::Ok().streaming(rx)) -} - -/// Resize a virtual machine image -#[cfg_attr(feature = "dev", utoipa::path( - post, - tag = "VmImages", - request_body = VmImageResizePayload, - path = "/vms/images/{name}/resize", - params( - ("name" = String, Path, description = "The name of the vm image"), - ), - responses( - (status = 200, description = "The snapshot have been created", body = VmImage), - ), -))] -#[web::post("/vms/images/{name}/resize")] -pub async fn resize_vm_image( - state: web::types::State, - path: web::types::Path<(String, String)>, - web::types::Json(payload): web::types::Json, -) -> HttpResult { - let name = path.1.to_owned(); - let rx = - utils::vm_image::resize_by_name(&name, &payload, &state.inner.pool).await?; - Ok(web::HttpResponse::Ok().json(&rx)) -} - -/// Get detailed information about a virtual machine image -#[cfg_attr(feature = "dev", utoipa::path( - get, - tag = "VmImages", - path = "/vms/images/{name}", - params( - ("name" = String, Path, description = "The name of the vm image"), - ), - responses( - (status = 200, description = "Detailed information about the vm image", body = VmImage), - ), -))] -#[web::get("/vms/images/{name}/inspect")] -pub async fn inspect_vm_image( - state: web::types::State, - path: web::types::Path<(String, String)>, -) -> HttpResult { - let name = path.1.to_owned(); - let item = VmImageDb::read_by_pk(&name, &state.inner.pool).await?; - Ok(web::HttpResponse::Ok().json(&item)) -} - -/// Delete a virtual machine image -#[cfg_attr(feature = "dev", utoipa::path( - delete, - tag = "VmImages", - path = "/vms/images/{name}", - params( - ("name" = String, Path, description = "The name of the vm image"), - ), - responses( - (status = 200, description = "Image have been deleted"), - ), -))] -#[web::delete("/vms/images/{name}")] -pub async fn delete_vm_image( - state: web::types::State, - path: web::types::Path<(String, String)>, -) -> HttpResult { - let pk = path.1.to_owned(); - utils::vm_image::delete_by_pk(&pk, &state).await?; - Ok(web::HttpResponse::Ok().into()) -} - -/// Count vm images -#[cfg_attr(feature = "dev", utoipa::path( - get, - tag = "VmImages", - path = "/vms/images/count", - params( - ("filter" = Option, Query, description = "Generic filter", example = "{ \"filter\": { \"where\": { \"name\": { \"eq\": \"global\" } } } }"), - ), - responses( - (status = 200, description = "Count result", body = GenericCount), - ), -))] -#[web::get("/vms/images/count")] -pub async fn count_vm_image( - state: web::types::State, - qs: web::types::Query, -) -> HttpResult { - let filter: nanocl_stubs::generic::GenericFilter = - utils::query_string::parse_qs_filter(&qs)?; - let count = VmImageDb::count_by(&filter, &state.inner.pool).await?; - Ok(web::HttpResponse::Ok().json(&GenericCount { count })) -} - -pub fn ntex_config(config: &mut web::ServiceConfig) { - config.service(import_vm_image); - config.service(list_vm_images); - config.service(delete_vm_image); - config.service(snapshot_vm_image); - config.service(clone_vm_image); - config.service(count_vm_image); - config.service(resize_vm_image); - config.service(inspect_vm_image); -} - -#[cfg(test)] -pub mod tests { - use futures_util::StreamExt; - use ntex::http::StatusCode; - use tokio_util::codec; - - use nanocl_error::io::{FromIo, IoError, IoResult}; - use nanocl_stubs::vm_image::VmImage; - - use crate::utils::tests::*; - - async fn import_image(name: &str, path: &str) -> IoResult<()> { - let system = gen_default_test_system().await; - let client = system.client; - let file = tokio::fs::File::open(path).await?; - let err_msg = format!("Unable to import image {name}:{path}"); - let stream = - codec::FramedRead::new(file, codec::BytesCodec::new()).map(move |r| { - let r = r?; - let bytes = ntex::util::Bytes::from_iter(r.freeze().to_vec()); - Ok::(bytes) - }); - let mut res = client - .post(&format!("/vms/images/{name}/import")) - .send_stream(stream) - .await - .map_err(|err| err.map_err_context(|| &err_msg))?; - let status = res.status(); - if status != StatusCode::OK { - let error = res - .json::() - .await - .map_err(|err| err.map_err_context(|| &err_msg))?; - println!("{:?}", error); - } - test_status_code!(res.status(), StatusCode::OK, &err_msg); - Ok(()) - } - - async fn inspect_image(name: &str) -> IoResult { - let system = gen_default_test_system().await; - let client = system.client; - let err_msg = format!("Unable to inspect image {name}"); - let mut res = client - .get(&format!("/vms/images/{name}/inspect")) - .send() - .await - .map_err(|err| err.map_err_context(|| &err_msg))?; - if res.status() != StatusCode::OK { - return Err(IoError::not_found("vm_image", name)); - } - test_status_code!(res.status(), StatusCode::OK, &err_msg); - let data = res - .json::() - .await - .map_err(|err| err.map_err_context(|| &err_msg))?; - Ok(data) - } - - pub async fn ensure_test_image() { - let name = "ubuntu-22-test"; - let path = "../../tests/ubuntu-22.04-minimal-cloudimg-amd64.img"; - if inspect_image(name).await.is_ok() { - return; - } - import_image(name, path).await.unwrap(); - } - - #[ntex::test] - async fn basic() { - let system = gen_default_test_system().await; - let client = system.client; - let name = "ubuntu-22-test-basic"; - let path = "../../tests/ubuntu-22.04-minimal-cloudimg-amd64.img"; - import_image(name, path).await.unwrap(); - let image = inspect_image(name).await.unwrap(); - assert_eq!(image.name, name); - let mut res = client.get("/vms/images").send().await.unwrap(); - test_status_code!(res.status(), StatusCode::OK, "Unable to list images"); - let images = res.json::>().await.unwrap(); - assert!(images.iter().any(|i| i.name == name)); - let res = client - .delete(&format!("/vms/images/{name}")) - .send() - .await - .unwrap(); - test_status_code!(res.status(), StatusCode::OK, "Unable to delete image"); - } -} diff --git a/bin/nanocld/src/services/vm_image/clone.rs b/bin/nanocld/src/services/vm_image/clone.rs new file mode 100644 index 000000000..b6209337c --- /dev/null +++ b/bin/nanocld/src/services/vm_image/clone.rs @@ -0,0 +1,36 @@ +use ntex::web; + +use nanocl_error::http::HttpResult; + +use crate::{ + models::{SystemState, VmImageDb}, + repositories::generic::*, + utils, +}; + +/// Clone a virtual machine image +#[cfg_attr(feature = "dev", utoipa::path( + post, + tag = "VmImages", + request_body = String, + path = "/vms/images/{name}/clone/{clone_name}", + params( + ("name" = String, Path, description = "The name of the vm image"), + ("clone_name" = String, Path, description = "The name of the clone"), + ), + responses( + (status = 200, description = "The snapshot have been created", body = VmImage), + ), +))] +#[web::post("/vms/images/{name}/clone/{clone_name}")] +pub async fn clone_vm_image( + state: web::types::State, + path: web::types::Path<(String, String, String)>, +) -> HttpResult { + let name = path.1.to_owned(); + let clone_name = path.2.to_owned(); + utils::key::validate_name(&clone_name)?; + let image = VmImageDb::read_by_pk(&name, &state.inner.pool).await?; + let rx = utils::vm_image::clone(&clone_name, &image, &state).await?; + Ok(web::HttpResponse::Ok().streaming(rx)) +} diff --git a/bin/nanocld/src/services/vm_image/count.rs b/bin/nanocld/src/services/vm_image/count.rs new file mode 100644 index 000000000..eb4980be7 --- /dev/null +++ b/bin/nanocld/src/services/vm_image/count.rs @@ -0,0 +1,33 @@ +use ntex::web; + +use nanocl_error::http::HttpResult; +use nanocl_stubs::generic::{GenericCount, GenericListQuery}; + +use crate::{ + models::{SystemState, VmImageDb}, + repositories::generic::*, + utils, +}; + +/// Count vm images +#[cfg_attr(feature = "dev", utoipa::path( + get, + tag = "VmImages", + path = "/vms/images/count", + params( + ("filter" = Option, Query, description = "Generic filter", example = "{ \"filter\": { \"where\": { \"name\": { \"eq\": \"global\" } } } }"), + ), + responses( + (status = 200, description = "Count result", body = GenericCount), + ), +))] +#[web::get("/vms/images/count")] +pub async fn count_vm_image( + state: web::types::State, + qs: web::types::Query, +) -> HttpResult { + let filter: nanocl_stubs::generic::GenericFilter = + utils::query_string::parse_qs_filter(&qs)?; + let count = VmImageDb::count_by(&filter, &state.inner.pool).await?; + Ok(web::HttpResponse::Ok().json(&GenericCount { count })) +} diff --git a/bin/nanocld/src/services/vm_image/create_snapshot.rs b/bin/nanocld/src/services/vm_image/create_snapshot.rs new file mode 100644 index 000000000..20b0da23a --- /dev/null +++ b/bin/nanocld/src/services/vm_image/create_snapshot.rs @@ -0,0 +1,37 @@ +use ntex::web; + +use nanocl_error::http::HttpResult; + +use crate::{ + models::{SystemState, VmImageDb}, + repositories::generic::*, + utils, +}; + +/// Create a snapshot of a virtual machine image +#[cfg_attr(feature = "dev", utoipa::path( + post, + tag = "VmImages", + request_body = String, + path = "/vms/images/{name}/snapshot/{snapshot_name}", + params( + ("name" = String, Path, description = "The name of the vm image"), + ("snap" = String, Path, description = "The name of the snapshot"), + ), + responses( + (status = 200, description = "The snapshot have been created", body = VmImage), + ), +))] +#[web::post("/vms/images/{name}/snapshot/{snapshot_name}")] +pub async fn snapshot_vm_image( + state: web::types::State, + path: web::types::Path<(String, String, String)>, +) -> HttpResult { + let name = path.1.to_owned(); + let snapshot_name = path.2.to_owned(); + utils::key::validate_name(&snapshot_name)?; + let image = VmImageDb::read_by_pk(&name, &state.inner.pool).await?; + let vm_image = + utils::vm_image::create_snap(&snapshot_name, 50, &image, &state).await?; + Ok(web::HttpResponse::Ok().json(&vm_image)) +} diff --git a/bin/nanocld/src/services/vm_image/delete.rs b/bin/nanocld/src/services/vm_image/delete.rs new file mode 100644 index 000000000..0b956652d --- /dev/null +++ b/bin/nanocld/src/services/vm_image/delete.rs @@ -0,0 +1,27 @@ +use ntex::web; + +use nanocl_error::http::HttpResult; + +use crate::{models::SystemState, utils}; + +/// Delete a virtual machine image +#[cfg_attr(feature = "dev", utoipa::path( + delete, + tag = "VmImages", + path = "/vms/images/{name}", + params( + ("name" = String, Path, description = "The name of the vm image"), + ), + responses( + (status = 200, description = "Image have been deleted"), + ), +))] +#[web::delete("/vms/images/{name}")] +pub async fn delete_vm_image( + state: web::types::State, + path: web::types::Path<(String, String)>, +) -> HttpResult { + let pk = path.1.to_owned(); + utils::vm_image::delete_by_pk(&pk, &state).await?; + Ok(web::HttpResponse::Ok().into()) +} diff --git a/bin/nanocld/src/services/vm_image/import.rs b/bin/nanocld/src/services/vm_image/import.rs new file mode 100644 index 000000000..cddf4107e --- /dev/null +++ b/bin/nanocld/src/services/vm_image/import.rs @@ -0,0 +1,68 @@ +use std::io::Write; + +use futures::StreamExt; +use ntex::web; + +use nanocl_error::http::{HttpError, HttpResult}; + +use crate::{ + models::{SystemState, VmImageDb}, + repositories::generic::*, + utils, +}; + +/// Import a virtual machine image from a file +#[cfg_attr(feature = "dev", utoipa::path( + post, + tag = "VmImages", + request_body = String, + path = "/vms/images/{name}/import", + params( + ("name" = String, Path, description = "The name of the vm image"), + ), + responses( + (status = 200, description = "Image have been imported"), + ), +))] +#[web::post("/vms/images/{name}/import")] +pub async fn import_vm_image( + state: web::types::State, + path: web::types::Path<(String, String)>, + mut payload: web::types::Payload, +) -> HttpResult { + let name = path.1.to_owned(); + utils::key::validate_name(&name)?; + if VmImageDb::read_by_pk(&name, &state.inner.pool) + .await + .is_ok() + { + return Err(HttpError::conflict(format!("Vm image {name} already used"))); + } + let state_dir = state.inner.config.state_dir.clone(); + let vm_images_dir = format!("{state_dir}/vms/images"); + let filepath = format!("{vm_images_dir}/{name}.img"); + let fp = filepath.clone(); + let mut f = web::block(move || std::fs::File::create(fp)) + .await + .map_err(|err| { + HttpError::internal_server_error(format!( + "Unable to create vm image {name}: {err}" + )) + })?; + while let Some(bytes) = payload.next().await { + let bytes = bytes.map_err(|err| { + HttpError::internal_server_error(format!( + "Unable to create vm image {name}: {err}" + )) + })?; + f = web::block(move || f.write_all(&bytes).map(|_| f)) + .await + .map_err(|err| { + HttpError::internal_server_error(format!( + "Unable to create vm image {name}: {err}" + )) + })?; + } + utils::vm_image::create(&name, &filepath, &state).await?; + Ok(web::HttpResponse::Ok().into()) +} diff --git a/bin/nanocld/src/services/vm_image/inspect.rs b/bin/nanocld/src/services/vm_image/inspect.rs new file mode 100644 index 000000000..24901c1ae --- /dev/null +++ b/bin/nanocld/src/services/vm_image/inspect.rs @@ -0,0 +1,30 @@ +use ntex::web; + +use nanocl_error::http::HttpResult; + +use crate::{ + models::{SystemState, VmImageDb}, + repositories::generic::*, +}; + +/// Get detailed information about a virtual machine image +#[cfg_attr(feature = "dev", utoipa::path( + get, + tag = "VmImages", + path = "/vms/images/{name}/inspect", + params( + ("name" = String, Path, description = "The name of the vm image"), + ), + responses( + (status = 200, description = "Detailed information about the vm image", body = VmImage), + ), +))] +#[web::get("/vms/images/{name}/inspect")] +pub async fn inspect_vm_image( + state: web::types::State, + path: web::types::Path<(String, String)>, +) -> HttpResult { + let name = path.1.to_owned(); + let item = VmImageDb::read_by_pk(&name, &state.inner.pool).await?; + Ok(web::HttpResponse::Ok().json(&item)) +} diff --git a/bin/nanocld/src/services/vm_image/list.rs b/bin/nanocld/src/services/vm_image/list.rs new file mode 100644 index 000000000..852006c8f --- /dev/null +++ b/bin/nanocld/src/services/vm_image/list.rs @@ -0,0 +1,32 @@ +use ntex::web; + +use nanocl_error::http::HttpResult; +use nanocl_stubs::generic::GenericListQuery; + +use crate::{ + models::{SystemState, VmImageDb}, + repositories::generic::*, + utils, +}; + +/// List virtual machine images with optional filter +#[cfg_attr(feature = "dev", utoipa::path( + get, + tag = "VmImages", + path = "/vms/images", + params( + ("filter" = Option, Query, description = "Generic filter", example = "{ \"filter\": { \"where\": { \"name\": { \"eq\": \"my-image\" } } } }"), + ), + responses( + (status = 200, description = "List of vm images", body = [VmImage]), + ), +))] +#[web::get("/vms/images")] +pub async fn list_vm_images( + state: web::types::State, + qs: web::types::Query, +) -> HttpResult { + let filter = utils::query_string::parse_qs_filter(&qs)?; + let images = VmImageDb::read_by(&filter, &state.inner.pool).await?; + Ok(web::HttpResponse::Ok().json(&images)) +} diff --git a/bin/nanocld/src/services/vm_image/mod.rs b/bin/nanocld/src/services/vm_image/mod.rs new file mode 100644 index 000000000..b5b29a532 --- /dev/null +++ b/bin/nanocld/src/services/vm_image/mod.rs @@ -0,0 +1,120 @@ +use ntex::web; + +pub mod clone; +pub mod count; +pub mod create_snapshot; +pub mod delete; +pub mod import; +pub mod inspect; +pub mod list; +pub mod resize; + +pub use clone::*; +pub use count::*; +pub use create_snapshot::*; +pub use delete::*; +pub use import::*; +pub use inspect::*; +pub use list::*; +pub use resize::*; + +pub fn ntex_config(config: &mut web::ServiceConfig) { + config.service(import_vm_image); + config.service(list_vm_images); + config.service(delete_vm_image); + config.service(snapshot_vm_image); + config.service(clone_vm_image); + config.service(count_vm_image); + config.service(resize_vm_image); + config.service(inspect_vm_image); +} + +#[cfg(test)] +pub mod tests { + use futures_util::StreamExt; + use ntex::http::StatusCode; + use tokio_util::codec; + + use nanocl_error::io::{FromIo, IoError, IoResult}; + use nanocl_stubs::vm_image::VmImage; + + use crate::utils::tests::*; + + async fn import_image(name: &str, path: &str) -> IoResult<()> { + let system = gen_default_test_system().await; + let client = system.client; + let file = tokio::fs::File::open(path).await?; + let err_msg = format!("Unable to import image {name}:{path}"); + let stream = + codec::FramedRead::new(file, codec::BytesCodec::new()).map(move |r| { + let r = r?; + let bytes = ntex::util::Bytes::from_iter(r.freeze().to_vec()); + Ok::(bytes) + }); + let mut res = client + .post(&format!("/vms/images/{name}/import")) + .send_stream(stream) + .await + .map_err(|err| err.map_err_context(|| &err_msg))?; + let status = res.status(); + if status != StatusCode::OK { + let error = res + .json::() + .await + .map_err(|err| err.map_err_context(|| &err_msg))?; + println!("{:?}", error); + } + test_status_code!(res.status(), StatusCode::OK, &err_msg); + Ok(()) + } + + async fn inspect_image(name: &str) -> IoResult { + let system = gen_default_test_system().await; + let client = system.client; + let err_msg = format!("Unable to inspect image {name}"); + let mut res = client + .get(&format!("/vms/images/{name}/inspect")) + .send() + .await + .map_err(|err| err.map_err_context(|| &err_msg))?; + if res.status() != StatusCode::OK { + return Err(IoError::not_found("vm_image", name)); + } + test_status_code!(res.status(), StatusCode::OK, &err_msg); + let data = res + .json::() + .await + .map_err(|err| err.map_err_context(|| &err_msg))?; + Ok(data) + } + + pub async fn ensure_test_image() { + let name = "ubuntu-22-test"; + let path = "../../tests/ubuntu-22.04-minimal-cloudimg-amd64.img"; + if inspect_image(name).await.is_ok() { + return; + } + import_image(name, path).await.unwrap(); + } + + #[ntex::test] + async fn basic() { + let system = gen_default_test_system().await; + let client = system.client; + let name = "ubuntu-22-test-basic"; + let path = "../../tests/ubuntu-22.04-minimal-cloudimg-amd64.img"; + import_image(name, path).await.unwrap(); + let image = inspect_image(name).await.unwrap(); + assert_eq!(image.name, name); + let mut res = client.get("/vms/images").send().await.unwrap(); + test_status_code!(res.status(), StatusCode::OK, "Unable to list images"); + let images = res.json::>().await.unwrap(); + assert!(images.iter().any(|i| i.name == name)); + let res = client + .delete(&format!("/vms/images/{name}")) + .send() + .await + .unwrap(); + test_status_code!(res.status(), StatusCode::OK, "Unable to delete image"); + } +} diff --git a/bin/nanocld/src/services/vm_image/resize.rs b/bin/nanocld/src/services/vm_image/resize.rs new file mode 100644 index 000000000..409df719a --- /dev/null +++ b/bin/nanocld/src/services/vm_image/resize.rs @@ -0,0 +1,31 @@ +use ntex::web; + +use nanocl_error::http::HttpResult; +use nanocl_stubs::vm_image::VmImageResizePayload; + +use crate::{models::SystemState, utils}; + +/// Resize a virtual machine image +#[cfg_attr(feature = "dev", utoipa::path( + post, + tag = "VmImages", + request_body = VmImageResizePayload, + path = "/vms/images/{name}/resize", + params( + ("name" = String, Path, description = "The name of the vm image"), + ), + responses( + (status = 200, description = "The snapshot have been created", body = VmImage), + ), +))] +#[web::post("/vms/images/{name}/resize")] +pub async fn resize_vm_image( + state: web::types::State, + path: web::types::Path<(String, String)>, + web::types::Json(payload): web::types::Json, +) -> HttpResult { + let name = path.1.to_owned(); + let rx = + utils::vm_image::resize_by_name(&name, &payload, &state.inner.pool).await?; + Ok(web::HttpResponse::Ok().json(&rx)) +}