From 7916056bffa084c6999443040038bc477703d136 Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Tue, 22 Oct 2024 14:21:47 +0200 Subject: [PATCH 01/46] feat(api/database/resources): add select_flavor_group_from_db Signed-off-by: Sandro-Alessio Gierens --- api/src/database/resources/flavor_group.rs | 85 ++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/api/src/database/resources/flavor_group.rs b/api/src/database/resources/flavor_group.rs index 33f123d8..f1e565a2 100644 --- a/api/src/database/resources/flavor_group.rs +++ b/api/src/database/resources/flavor_group.rs @@ -1,5 +1,6 @@ use crate::error::{NotFoundOrUnexpectedApiError, UnexpectedOnlyError}; use anyhow::Context; +use lrzcc_wire::resources::FlavorGroup; use sqlx::{Executor, FromRow, MySql, Transaction}; #[tracing::instrument( @@ -51,3 +52,87 @@ pub async fn select_flavor_group_name_from_db( "Flavor group with given ID not found".to_string(), )) } + +#[derive(Clone, Debug, PartialEq, FromRow)] +pub struct FlavorGroupDb { + pub id: u32, + pub name: String, + pub project_id: u32, +} + +#[tracing::instrument( + name = "select_maybe_flavor_group_name_from_db", + skip(transaction) +)] +pub async fn select_maybe_flavor_group_from_db( + transaction: &mut Transaction<'_, MySql>, + flavor_group_id: u64, +) -> Result, UnexpectedOnlyError> { + #[derive(FromRow)] + pub struct FlavorGroupDb { + pub id: u32, + pub name: String, + pub project_id: u32, + } + let query1 = sqlx::query!( + r#" + SELECT id, name, project_id + FROM resources_flavorgroup + WHERE id = ? + "#, + flavor_group_id + ); + let row1 = transaction + .fetch_optional(query1) + .await + .context("Failed to execute select query")?; + let flavor_group = match row1 { + Some(row) => FlavorGroupDb::from_row(&row) + .context("Failed to parse flavor group row")?, + None => return Ok(None), + }; + #[derive(FromRow)] + pub struct FlavorIdDb { + pub id: u32, + } + let query2 = sqlx::query!( + r#" + SELECT id + FROM resources_flavor + WHERE group_id = ? + "#, + flavor_group_id + ); + let flavors = transaction + .fetch_all(query2) + .await + .context("Failed to execute select query")? + .into_iter() + .map(|row| FlavorIdDb::from_row(&row)) + .collect::, _>>() + .context("Failed to parse flavor row")? + .into_iter() + .map(|row| row.id) + .collect::>(); + Ok(Some(FlavorGroup { + id: flavor_group.id, + name: flavor_group.name, + project: flavor_group.project_id, + flavors, + })) +} + +#[tracing::instrument( + name = "select_flavor_group_name_from_db", + skip(transaction) +)] +pub async fn select_flavor_group_from_db( + transaction: &mut Transaction<'_, MySql>, + flavor_group_id: u64, +) -> Result { + select_maybe_flavor_group_from_db(transaction, flavor_group_id) + .await? + .ok_or(NotFoundOrUnexpectedApiError::NotFoundError( + "Flavor group with given ID not found".to_string(), + )) +} From 20305a679ff72789c2e16eea56d5e9a6658f09af Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Tue, 22 Oct 2024 14:24:00 +0200 Subject: [PATCH 02/46] feat(api/routes/resources): add flavor_group_modify endpoint Signed-off-by: Sandro-Alessio Gierens --- api/src/routes/resources/flavor_group/mod.rs | 9 ++- .../routes/resources/flavor_group/modify.rs | 76 +++++++++++++++++++ 2 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 api/src/routes/resources/flavor_group/modify.rs diff --git a/api/src/routes/resources/flavor_group/mod.rs b/api/src/routes/resources/flavor_group/mod.rs index b73dd59b..46fae4af 100644 --- a/api/src/routes/resources/flavor_group/mod.rs +++ b/api/src/routes/resources/flavor_group/mod.rs @@ -1,6 +1,7 @@ use actix_web::web::{ delete, - // get, patch, + // get, + patch, post, scope, }; @@ -13,8 +14,8 @@ use create::flavor_group_create; // use list::flavor_group_list; // mod get; // use get::flavor_group_get; -// mod modify; -// use modify::flavor_group_modify; +mod modify; +use modify::flavor_group_modify; mod delete; use delete::flavor_group_delete; @@ -24,7 +25,7 @@ pub fn flavor_groups_scope() -> Scope { // .route("", get().to(flavor_group_list)) // .route("/{flavor_group_id}", get().to(flavor_group_get)) // TODO: what about PUT? - // .route("/{flavor_group_id}/", patch().to(flavor_group_modify)) + .route("/{flavor_group_id}/", patch().to(flavor_group_modify)) .route("/{flavor_group_id}/", delete().to(flavor_group_delete)) } diff --git a/api/src/routes/resources/flavor_group/modify.rs b/api/src/routes/resources/flavor_group/modify.rs new file mode 100644 index 00000000..ff7309b6 --- /dev/null +++ b/api/src/routes/resources/flavor_group/modify.rs @@ -0,0 +1,76 @@ +use crate::authorization::require_admin_user; +use crate::database::resources::flavor_group::select_flavor_group_from_db; +use crate::error::{NotFoundOrUnexpectedApiError, OptionApiError}; +use actix_web::web::{Data, Json, Path, ReqData}; +use actix_web::HttpResponse; +use anyhow::Context; +use lrzcc_wire::resources::{FlavorGroup, FlavorGroupModifyData}; +use lrzcc_wire::user::{Project, User}; +use sqlx::{Executor, MySql, MySqlPool, Transaction}; + +use super::FlavorGroupIdParam; + +#[tracing::instrument(name = "flavor_group_modify")] +pub async fn flavor_group_modify( + user: ReqData, + // TODO: we don't need this right? + project: ReqData, + db_pool: Data, + data: Json, + params: Path, +) -> Result { + require_admin_user(&user)?; + // TODO: do further validation + if data.id != params.flavor_group_id { + return Err(OptionApiError::ValidationError( + "ID in URL does not match ID in body".to_string(), + )); + } + let mut transaction = db_pool + .begin() + .await + .context("Failed to begin transaction")?; + let flavor_group = + update_flavor_group_in_db(&mut transaction, &data).await?; + transaction + .commit() + .await + .context("Failed to commit transaction")?; + Ok(HttpResponse::Ok() + .content_type("application/json") + .json(flavor_group)) +} + +#[tracing::instrument( + name = "update_flavor_group_in_db", + skip(data, transaction) +)] +pub async fn update_flavor_group_in_db( + transaction: &mut Transaction<'_, MySql>, + data: &FlavorGroupModifyData, +) -> Result { + let row = select_flavor_group_from_db(transaction, data.id as u64).await?; + let name = data.name.clone().unwrap_or(row.name); + let project = data.project.clone().unwrap_or(row.project); + let query = sqlx::query!( + r#" + UPDATE resources_flavorgroup + SET name = ?, project_id = ? + WHERE id = ? + "#, + name, + project, + data.id, + ); + transaction + .execute(query) + .await + .context("Failed to execute update query")?; + let project = FlavorGroup { + id: data.id, + name, + project, + flavors: row.flavors, + }; + Ok(project) +} From b119d46e50ba1c518f986aaba4463a65e9b4114a Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Tue, 22 Oct 2024 14:24:25 +0200 Subject: [PATCH 03/46] chore(changelog): update unreleased section Signed-off-by: Sandro-Alessio Gierens --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1532508..ca36e65a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,8 @@ This is the combined changelog of all contained `lrzcc` crates. - api: add budgeting::project_budget_create endpoint - wire: derive Deserialize for UserBudgetCreateData - api: add budgeting::user_budget_create endpoint +- api: add resources::select_flavor_group_from_db to database module +- api: add resources::flavor_group_modify endpoint - TODO: add remaining crud endpoints for all new modules - TODO: add tests for all new endpoints From 64aaae231ed0662c42f24e39c82f11b32e3ce2d9 Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Tue, 22 Oct 2024 14:24:48 +0200 Subject: [PATCH 04/46] chore(sqlx): update offline query data Signed-off-by: Sandro-Alessio Gierens --- ...35832bab4794df19a654f3e24e401c21d4a83.json | 12 +++++ ...b918130cdcb4aa5246c17d509bdcf2a13d311.json | 24 ++++++++++ ...4c123e0c9947910697bc8c2701d3f3dbc61d1.json | 44 +++++++++++++++++++ 3 files changed, 80 insertions(+) create mode 100644 .sqlx/query-73f67612462afeeef6d37cccf2f35832bab4794df19a654f3e24e401c21d4a83.json create mode 100644 .sqlx/query-e36e7fc4158b97cd63734738f5eb918130cdcb4aa5246c17d509bdcf2a13d311.json create mode 100644 .sqlx/query-e47d52e89a59f1616ea18056d574c123e0c9947910697bc8c2701d3f3dbc61d1.json diff --git a/.sqlx/query-73f67612462afeeef6d37cccf2f35832bab4794df19a654f3e24e401c21d4a83.json b/.sqlx/query-73f67612462afeeef6d37cccf2f35832bab4794df19a654f3e24e401c21d4a83.json new file mode 100644 index 00000000..f14c10e5 --- /dev/null +++ b/.sqlx/query-73f67612462afeeef6d37cccf2f35832bab4794df19a654f3e24e401c21d4a83.json @@ -0,0 +1,12 @@ +{ + "db_name": "MySQL", + "query": "\n UPDATE resources_flavorgroup\n SET name = ?, project_id = ?\n WHERE id = ?\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "73f67612462afeeef6d37cccf2f35832bab4794df19a654f3e24e401c21d4a83" +} diff --git a/.sqlx/query-e36e7fc4158b97cd63734738f5eb918130cdcb4aa5246c17d509bdcf2a13d311.json b/.sqlx/query-e36e7fc4158b97cd63734738f5eb918130cdcb4aa5246c17d509bdcf2a13d311.json new file mode 100644 index 00000000..3ed4690e --- /dev/null +++ b/.sqlx/query-e36e7fc4158b97cd63734738f5eb918130cdcb4aa5246c17d509bdcf2a13d311.json @@ -0,0 +1,24 @@ +{ + "db_name": "MySQL", + "query": "\n SELECT id\n FROM resources_flavor\n WHERE group_id = ?\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": { + "type": "LongLong", + "flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT", + "max_size": 20 + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "e36e7fc4158b97cd63734738f5eb918130cdcb4aa5246c17d509bdcf2a13d311" +} diff --git a/.sqlx/query-e47d52e89a59f1616ea18056d574c123e0c9947910697bc8c2701d3f3dbc61d1.json b/.sqlx/query-e47d52e89a59f1616ea18056d574c123e0c9947910697bc8c2701d3f3dbc61d1.json new file mode 100644 index 00000000..764d9ef0 --- /dev/null +++ b/.sqlx/query-e47d52e89a59f1616ea18056d574c123e0c9947910697bc8c2701d3f3dbc61d1.json @@ -0,0 +1,44 @@ +{ + "db_name": "MySQL", + "query": "\n SELECT id, name, project_id\n FROM resources_flavorgroup\n WHERE id = ?\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": { + "type": "LongLong", + "flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT", + "max_size": 20 + } + }, + { + "ordinal": 1, + "name": "name", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE", + "max_size": 256 + } + }, + { + "ordinal": 2, + "name": "project_id", + "type_info": { + "type": "Long", + "flags": "NOT_NULL | MULTIPLE_KEY | NO_DEFAULT_VALUE", + "max_size": 11 + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "e47d52e89a59f1616ea18056d574c123e0c9947910697bc8c2701d3f3dbc61d1" +} From b6ee36df2e71cb4094b6db051396dd754c10c691 Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Tue, 22 Oct 2024 14:25:38 +0200 Subject: [PATCH 05/46] style(api/routes/resources): remove unnecessary clone Signed-off-by: Sandro-Alessio Gierens --- api/src/routes/resources/flavor_group/modify.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/routes/resources/flavor_group/modify.rs b/api/src/routes/resources/flavor_group/modify.rs index ff7309b6..110183fc 100644 --- a/api/src/routes/resources/flavor_group/modify.rs +++ b/api/src/routes/resources/flavor_group/modify.rs @@ -51,7 +51,7 @@ pub async fn update_flavor_group_in_db( ) -> Result { let row = select_flavor_group_from_db(transaction, data.id as u64).await?; let name = data.name.clone().unwrap_or(row.name); - let project = data.project.clone().unwrap_or(row.project); + let project = data.project.unwrap_or(row.project); let query = sqlx::query!( r#" UPDATE resources_flavorgroup From 1447dd8083ac071eae73e52d6b0f930ad70d8a04 Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Tue, 22 Oct 2024 14:26:49 +0200 Subject: [PATCH 06/46] docs(api/routes): remove done todo comment Signed-off-by: Sandro-Alessio Gierens --- api/src/routes/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/api/src/routes/mod.rs b/api/src/routes/mod.rs index ab608980..bde64f21 100644 --- a/api/src/routes/mod.rs +++ b/api/src/routes/mod.rs @@ -18,7 +18,6 @@ pub use user::*; // TODO: modify endpoints for // - accounting::server_state -// - resources::flavor_group // - resources::flavor // - quota::flavor_quota // - pricing::flavor_price From 857203e3efc5dde5a076fba3983710e532d13fcf Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Tue, 22 Oct 2024 18:18:34 +0200 Subject: [PATCH 07/46] chore(changelog): update unreleased section Signed-off-by: Sandro-Alessio Gierens --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca36e65a..17d87d01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,11 @@ This is the combined changelog of all contained `lrzcc` crates. - api: add budgeting::user_budget_create endpoint - api: add resources::select_flavor_group_from_db to database module - api: add resources::flavor_group_modify endpoint +- deb: bump uuid from 1.10.0 to 1.11.0 +- deb: bump anyhow from 1.0.89 to 1.0.90 +- deb: bump tracing-actix-web from 0.7.13 to 0.7.14 +- deb: bump serde_json from 1.0.128 to 1.0.132 +- deb: bump serde from 1.0.210 to 1.0.211 - TODO: add remaining crud endpoints for all new modules - TODO: add tests for all new endpoints From 815d185746914a6c9d936f8e10e566c5f7bb4393 Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Wed, 23 Oct 2024 10:46:14 +0200 Subject: [PATCH 08/46] feat(api/database/resources): add select_flavor_from_db Signed-off-by: Sandro-Alessio Gierens --- api/src/database/resources/flavor.rs | 39 ++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/api/src/database/resources/flavor.rs b/api/src/database/resources/flavor.rs index 00ab2775..59f295c5 100644 --- a/api/src/database/resources/flavor.rs +++ b/api/src/database/resources/flavor.rs @@ -1,5 +1,6 @@ use crate::error::{NotFoundOrUnexpectedApiError, UnexpectedOnlyError}; use anyhow::Context; +use lrzcc_wire::resources::Flavor; use sqlx::{Executor, FromRow, MySql, Transaction}; #[tracing::instrument( @@ -48,3 +49,41 @@ pub async fn select_flavor_name_from_db( "User with given ID not found".to_string(), )) } + +#[tracing::instrument(name = "select_maybe_flavor_from_db", skip(transaction))] +pub async fn select_maybe_flavor_from_db( + transaction: &mut Transaction<'_, MySql>, + flavor_id: u64, +) -> Result, UnexpectedOnlyError> { + let query = sqlx::query!( + r#" + SELECT id, name, openstack_id, weight, group_id + FROM resources_flavor + WHERE id = ? + "#, + flavor_id + ); + let row = transaction + .fetch_optional(query) + .await + .context("Failed to execute select query")?; + // TODO: isn't there a nicer way to write this? + Ok(match row { + Some(row) => { + Some(Flavor::from_row(&row).context("Failed to parse flavor row")?) + } + None => None, + }) +} + +#[tracing::instrument(name = "select_flavor_from_db", skip(transaction))] +pub async fn select_flavor_from_db( + transaction: &mut Transaction<'_, MySql>, + flavor_id: u64, +) -> Result { + select_maybe_flavor_from_db(transaction, flavor_id) + .await? + .ok_or(NotFoundOrUnexpectedApiError::NotFoundError( + "Flavor with given ID not found".to_string(), + )) +} From c63f685e79cc4dd1698088731653fe7204fb73d6 Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Wed, 23 Oct 2024 10:47:02 +0200 Subject: [PATCH 09/46] feat(wire/resources): derive FromRow for Flavor Signed-off-by: Sandro-Alessio Gierens --- wire/src/resources/flavor.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/wire/src/resources/flavor.rs b/wire/src/resources/flavor.rs index 867346a2..2f1a1798 100644 --- a/wire/src/resources/flavor.rs +++ b/wire/src/resources/flavor.rs @@ -1,10 +1,11 @@ use crate::common::display_option; use crate::resources::FlavorGroupMinimal; use serde::{Deserialize, Serialize}; +use sqlx::FromRow; use std::fmt::Display; use tabled::Tabled; -#[derive(Clone, Debug, Deserialize, Serialize, Tabled, PartialEq)] +#[derive(Clone, Debug, Deserialize, Serialize, Tabled, PartialEq, FromRow)] pub struct Flavor { pub id: u32, pub name: String, From d6922ac7d433b7ab16a134c1251cf92b721d75ad Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Wed, 23 Oct 2024 10:47:43 +0200 Subject: [PATCH 10/46] style(api/database/resources): fix tracing name typos in flavor_group submodule Signed-off-by: Sandro-Alessio Gierens --- api/src/database/resources/flavor_group.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/api/src/database/resources/flavor_group.rs b/api/src/database/resources/flavor_group.rs index f1e565a2..c3b59359 100644 --- a/api/src/database/resources/flavor_group.rs +++ b/api/src/database/resources/flavor_group.rs @@ -61,7 +61,7 @@ pub struct FlavorGroupDb { } #[tracing::instrument( - name = "select_maybe_flavor_group_name_from_db", + name = "select_maybe_flavor_group_from_db", skip(transaction) )] pub async fn select_maybe_flavor_group_from_db( @@ -122,10 +122,7 @@ pub async fn select_maybe_flavor_group_from_db( })) } -#[tracing::instrument( - name = "select_flavor_group_name_from_db", - skip(transaction) -)] +#[tracing::instrument(name = "select_flavor_group_from_db", skip(transaction))] pub async fn select_flavor_group_from_db( transaction: &mut Transaction<'_, MySql>, flavor_group_id: u64, From 29936b246647da24d038039526cf9f4cc0ff354a Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Wed, 23 Oct 2024 11:07:05 +0200 Subject: [PATCH 11/46] fix(api/database/resources): correct query in select_maybe_flavor_from_db Signed-off-by: Sandro-Alessio Gierens --- api/src/database/resources/flavor.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/api/src/database/resources/flavor.rs b/api/src/database/resources/flavor.rs index 59f295c5..9d70ab5d 100644 --- a/api/src/database/resources/flavor.rs +++ b/api/src/database/resources/flavor.rs @@ -57,9 +57,11 @@ pub async fn select_maybe_flavor_from_db( ) -> Result, UnexpectedOnlyError> { let query = sqlx::query!( r#" - SELECT id, name, openstack_id, weight, group_id - FROM resources_flavor - WHERE id = ? + SELECT f.id, f.name, f.openstack_id, f.weight, f.group_id, g.name as group_name + FROM resources_flavor as f, resources_flavorgroup as g + WHERE + f.group_id = g.id AND + f.id = ? "#, flavor_id ); From f463c0dd8061ed47cdf70f395b9a9149c6cda1c9 Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Wed, 23 Oct 2024 11:08:53 +0200 Subject: [PATCH 12/46] feat(api/routes/resources): add flavor_modify endpoint Signed-off-by: Sandro-Alessio Gierens --- api/src/routes/mod.rs | 1 - api/src/routes/resources/flavor/mod.rs | 9 +-- api/src/routes/resources/flavor/modify.rs | 87 +++++++++++++++++++++++ 3 files changed, 92 insertions(+), 5 deletions(-) create mode 100644 api/src/routes/resources/flavor/modify.rs diff --git a/api/src/routes/mod.rs b/api/src/routes/mod.rs index bde64f21..b9f5ea15 100644 --- a/api/src/routes/mod.rs +++ b/api/src/routes/mod.rs @@ -18,7 +18,6 @@ pub use user::*; // TODO: modify endpoints for // - accounting::server_state -// - resources::flavor // - quota::flavor_quota // - pricing::flavor_price // - budgeting::project_budget diff --git a/api/src/routes/resources/flavor/mod.rs b/api/src/routes/resources/flavor/mod.rs index f12068bf..a1df85e0 100644 --- a/api/src/routes/resources/flavor/mod.rs +++ b/api/src/routes/resources/flavor/mod.rs @@ -1,6 +1,7 @@ use actix_web::web::{ delete, - // get, patch, + // get, + patch, post, scope, }; @@ -13,8 +14,8 @@ use create::flavor_create; // use list::flavor_list; // mod get; // use get::flavor_get; -// mod modify; -// use modify::flavor_modify; +mod modify; +use modify::flavor_modify; mod delete; use delete::flavor_delete; @@ -24,7 +25,7 @@ pub fn flavors_scope() -> Scope { // .route("", get().to(flavor_list)) // .route("/{flavor_id}", get().to(flavor_get)) // TODO: what about PUT? - // .route("/{flavor_id}/", patch().to(flavor_modify)) + .route("/{flavor_id}/", patch().to(flavor_modify)) .route("/{flavor_id}/", delete().to(flavor_delete)) } diff --git a/api/src/routes/resources/flavor/modify.rs b/api/src/routes/resources/flavor/modify.rs new file mode 100644 index 00000000..5d8921ff --- /dev/null +++ b/api/src/routes/resources/flavor/modify.rs @@ -0,0 +1,87 @@ +use crate::authorization::require_admin_user; +use crate::database::resources::flavor::select_flavor_from_db; +use crate::database::resources::flavor_group::select_flavor_group_name_from_db; +use crate::error::{NotFoundOrUnexpectedApiError, OptionApiError}; +use actix_web::web::{Data, Json, Path, ReqData}; +use actix_web::HttpResponse; +use anyhow::Context; +use lrzcc_wire::resources::{Flavor, FlavorModifyData}; +use lrzcc_wire::user::{Project, User}; +use sqlx::{Executor, MySql, MySqlPool, Transaction}; + +use super::FlavorIdParam; + +#[tracing::instrument(name = "flavor_modify")] +pub async fn flavor_modify( + user: ReqData, + // TODO: we don't need this right? + project: ReqData, + db_pool: Data, + data: Json, + params: Path, +) -> Result { + require_admin_user(&user)?; + // TODO: do further validation + if data.id != params.flavor_id { + return Err(OptionApiError::ValidationError( + "ID in URL does not match ID in body".to_string(), + )); + } + let mut transaction = db_pool + .begin() + .await + .context("Failed to begin transaction")?; + let flavor = update_flavor_in_db(&mut transaction, &data).await?; + transaction + .commit() + .await + .context("Failed to commit transaction")?; + Ok(HttpResponse::Ok() + .content_type("application/json") + .json(flavor)) +} + +#[tracing::instrument(name = "update_flavor_in_db", skip(data, transaction))] +pub async fn update_flavor_in_db( + transaction: &mut Transaction<'_, MySql>, + data: &FlavorModifyData, +) -> Result { + let row = select_flavor_from_db(transaction, data.id as u64).await?; + let name = data.name.clone().unwrap_or(row.name); + let openstack_id = data.openstack_id.clone().unwrap_or(row.openstack_id); + let weight = data.weight.unwrap_or(row.weight); + let group = data.group.unwrap_or(row.group); + let query = sqlx::query!( + r#" + UPDATE resources_flavor + SET name = ?, openstack_id = ?, weight = ?, group_id = ? + WHERE id = ? + "#, + name, + openstack_id, + weight, + group, + data.id, + ); + transaction + .execute(query) + .await + .context("Failed to execute update query")?; + let group_name = if let Some(group_id) = group { + Some( + select_flavor_group_name_from_db(transaction, group_id as u64) + .await?, + ) + } else { + None + }; + let project = Flavor { + id: data.id, + name, + openstack_id, + weight, + group, + group_name, + }; + Ok(project) +} From cd14f4318f2be4d083f73cea84621176217cf923 Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Wed, 23 Oct 2024 11:09:19 +0200 Subject: [PATCH 13/46] fix(wire/resources/flavor): make Flavor.group_name public Signed-off-by: Sandro-Alessio Gierens --- wire/src/resources/flavor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wire/src/resources/flavor.rs b/wire/src/resources/flavor.rs index 2f1a1798..fd986d08 100644 --- a/wire/src/resources/flavor.rs +++ b/wire/src/resources/flavor.rs @@ -13,7 +13,7 @@ pub struct Flavor { #[tabled(display_with = "display_option")] pub group: Option, #[tabled(display_with = "display_option")] - group_name: Option, + pub group_name: Option, pub weight: u32, } From a335fd94ba92dbf4bef563a6816a93d9cf12244e Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Wed, 23 Oct 2024 11:14:50 +0200 Subject: [PATCH 14/46] chore(sqlx): update offline query data Signed-off-by: Sandro-Alessio Gierens --- ...8dc0d2ff2e278ecbceb6907aefbbe4f0f7e32.json | 12 +++ ...a551dc5f0b758d4e31b6786fe8d74fe809ddf.json | 74 +++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 .sqlx/query-6cbbd76bb7fbe3d1c7200fee2588dc0d2ff2e278ecbceb6907aefbbe4f0f7e32.json create mode 100644 .sqlx/query-72fd0c10aed11a02b30b9e27637a551dc5f0b758d4e31b6786fe8d74fe809ddf.json diff --git a/.sqlx/query-6cbbd76bb7fbe3d1c7200fee2588dc0d2ff2e278ecbceb6907aefbbe4f0f7e32.json b/.sqlx/query-6cbbd76bb7fbe3d1c7200fee2588dc0d2ff2e278ecbceb6907aefbbe4f0f7e32.json new file mode 100644 index 00000000..4cec3373 --- /dev/null +++ b/.sqlx/query-6cbbd76bb7fbe3d1c7200fee2588dc0d2ff2e278ecbceb6907aefbbe4f0f7e32.json @@ -0,0 +1,12 @@ +{ + "db_name": "MySQL", + "query": "\n UPDATE resources_flavor\n SET name = ?, openstack_id = ?, weight = ?, group_id = ?\n WHERE id = ?\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 5 + }, + "nullable": [] + }, + "hash": "6cbbd76bb7fbe3d1c7200fee2588dc0d2ff2e278ecbceb6907aefbbe4f0f7e32" +} diff --git a/.sqlx/query-72fd0c10aed11a02b30b9e27637a551dc5f0b758d4e31b6786fe8d74fe809ddf.json b/.sqlx/query-72fd0c10aed11a02b30b9e27637a551dc5f0b758d4e31b6786fe8d74fe809ddf.json new file mode 100644 index 00000000..012ba737 --- /dev/null +++ b/.sqlx/query-72fd0c10aed11a02b30b9e27637a551dc5f0b758d4e31b6786fe8d74fe809ddf.json @@ -0,0 +1,74 @@ +{ + "db_name": "MySQL", + "query": "\n SELECT f.id, f.name, f.openstack_id, f.weight, f.group_id, g.name as group_name\n FROM resources_flavor as f, resources_flavorgroup as g\n WHERE\n f.group_id = g.id AND\n f.id = ?\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": { + "type": "LongLong", + "flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT", + "max_size": 20 + } + }, + { + "ordinal": 1, + "name": "name", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE", + "max_size": 256 + } + }, + { + "ordinal": 2, + "name": "openstack_id", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE", + "max_size": 1020 + } + }, + { + "ordinal": 3, + "name": "weight", + "type_info": { + "type": "Short", + "flags": "NOT_NULL | UNSIGNED | NO_DEFAULT_VALUE", + "max_size": 5 + } + }, + { + "ordinal": 4, + "name": "group_id", + "type_info": { + "type": "LongLong", + "flags": "MULTIPLE_KEY", + "max_size": 20 + } + }, + { + "ordinal": 5, + "name": "group_name", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE", + "max_size": 256 + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false, + true, + false + ] + }, + "hash": "72fd0c10aed11a02b30b9e27637a551dc5f0b758d4e31b6786fe8d74fe809ddf" +} From 62b3ffa6caf06f9f6207213f572286ea5da65df3 Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Wed, 23 Oct 2024 11:15:53 +0200 Subject: [PATCH 15/46] chore(changelog): update unreleased section Signed-off-by: Sandro-Alessio Gierens --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17d87d01..0e5bbcb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,9 @@ This is the combined changelog of all contained `lrzcc` crates. - deb: bump tracing-actix-web from 0.7.13 to 0.7.14 - deb: bump serde_json from 1.0.128 to 1.0.132 - deb: bump serde from 1.0.210 to 1.0.211 +- wire: derive FromRow for Flavor and make group_name field public +- api: add resources::select_flavor_from_db to database module +- api: add resources::flavor_modify endpoint - TODO: add remaining crud endpoints for all new modules - TODO: add tests for all new endpoints From 821aababfc9ca89a7314988eac228e0e898b1f04 Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Wed, 23 Oct 2024 16:01:54 +0200 Subject: [PATCH 16/46] feat(api/database/pricing): add flavor_price submodule with select function Signed-off-by: Sandro-Alessio Gierens --- api/src/database/mod.rs | 1 + api/src/database/pricing/flavor_price.rs | 73 ++++++++++++++++++++++++ api/src/database/pricing/mod.rs | 1 + 3 files changed, 75 insertions(+) create mode 100644 api/src/database/pricing/flavor_price.rs create mode 100644 api/src/database/pricing/mod.rs diff --git a/api/src/database/mod.rs b/api/src/database/mod.rs index 79a257a1..e9f2a821 100644 --- a/api/src/database/mod.rs +++ b/api/src/database/mod.rs @@ -1,2 +1,3 @@ +pub mod pricing; pub mod resources; pub mod user; diff --git a/api/src/database/pricing/flavor_price.rs b/api/src/database/pricing/flavor_price.rs new file mode 100644 index 00000000..4716daee --- /dev/null +++ b/api/src/database/pricing/flavor_price.rs @@ -0,0 +1,73 @@ +use crate::error::{NotFoundOrUnexpectedApiError, UnexpectedOnlyError}; +use anyhow::Context; +use chrono::{DateTime, Utc}; +use lrzcc_wire::pricing::FlavorPrice; +use sqlx::{Executor, FromRow, MySql, Transaction}; + +#[tracing::instrument( + name = "select_maybe_flavor_price_from_db", + skip(transaction) +)] +pub async fn select_maybe_flavor_price_from_db( + transaction: &mut Transaction<'_, MySql>, + flavor_price_id: u64, +) -> Result, UnexpectedOnlyError> { + #[derive(FromRow)] + pub struct Row { + pub id: u32, + pub flavor: u32, + pub flavor_name: String, + pub user_class: u32, + pub unit_price: f64, + pub start_time: DateTime, + } + let query = sqlx::query!( + r#" + SELECT + p.id, + p.flavor_id as flavor, + f.name as flavor_name, + p.user_class as user_class, + p.unit_price as unit_price, + p.start_time as start_time + FROM + pricing_flavorprice as p, + resources_flavor as f + WHERE + p.flavor_id = f.id AND + p.id = ? + "#, + flavor_price_id + ); + let row = transaction + .fetch_optional(query) + .await + .context("Failed to execute select query")?; + Ok(match row { + Some(row) => { + let row = Row::from_row(&row) + .context("Failed to parse flavor price row")?; + Some(FlavorPrice { + id: row.id, + flavor: row.flavor, + flavor_name: row.flavor_name, + user_class: row.user_class, + unit_price: row.unit_price, + start_time: row.start_time.fixed_offset(), + }) + } + None => None, + }) +} + +#[tracing::instrument(name = "select_flavor_price_from_db", skip(transaction))] +pub async fn select_flavor_price_from_db( + transaction: &mut Transaction<'_, MySql>, + flavor_price_id: u64, +) -> Result { + select_maybe_flavor_price_from_db(transaction, flavor_price_id) + .await? + .ok_or(NotFoundOrUnexpectedApiError::NotFoundError( + "Flavor price with given ID not found".to_string(), + )) +} diff --git a/api/src/database/pricing/mod.rs b/api/src/database/pricing/mod.rs new file mode 100644 index 00000000..7098bd77 --- /dev/null +++ b/api/src/database/pricing/mod.rs @@ -0,0 +1 @@ +pub mod flavor_price; From 7066e6a885789ef22a8c6b1033d28152fa367c61 Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Wed, 23 Oct 2024 16:07:35 +0200 Subject: [PATCH 17/46] feat(wire/pricing): derive Deserialize for FlavorPriceModifyData Signed-off-by: Sandro-Alessio Gierens --- wire/src/pricing.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wire/src/pricing.rs b/wire/src/pricing.rs index d05b4bbc..6f68925b 100644 --- a/wire/src/pricing.rs +++ b/wire/src/pricing.rs @@ -49,7 +49,7 @@ impl FlavorPriceCreateData { } } -#[derive(Clone, Debug, Serialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct FlavorPriceModifyData { pub id: u32, From 91e9967ad6c38e0a757efe8be68f5b80073c8f96 Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Wed, 23 Oct 2024 16:10:24 +0200 Subject: [PATCH 18/46] feat(api/pricing): add flavor_price_modify endpoint Signed-off-by: Sandro-Alessio Gierens --- api/src/routes/pricing/flavor_price/mod.rs | 9 +- api/src/routes/pricing/flavor_price/modify.rs | 82 +++++++++++++++++++ 2 files changed, 87 insertions(+), 4 deletions(-) create mode 100644 api/src/routes/pricing/flavor_price/modify.rs diff --git a/api/src/routes/pricing/flavor_price/mod.rs b/api/src/routes/pricing/flavor_price/mod.rs index 8f2db763..2c48c3c0 100644 --- a/api/src/routes/pricing/flavor_price/mod.rs +++ b/api/src/routes/pricing/flavor_price/mod.rs @@ -1,6 +1,7 @@ use actix_web::web::{ delete, - // get, patch, + // get, + patch, post, scope, }; @@ -13,8 +14,8 @@ use create::flavor_price_create; // use list::flavor_price_list; // mod get; // use get::flavor_price_get; -// mod modify; -// use modify::flavor_price_modify; +mod modify; +use modify::flavor_price_modify; mod delete; use delete::flavor_price_delete; @@ -24,7 +25,7 @@ pub fn flavor_prices_scope() -> Scope { // .route("", get().to(flavor_price_list)) // .route("/{flavor_price_id}", get().to(flavor_price_get)) // TODO: what about PUT? - // .route("/{flavor_price_id}/", patch().to(flavor_price_modify)) + .route("/{flavor_price_id}/", patch().to(flavor_price_modify)) .route("/{flavor_price_id}/", delete().to(flavor_price_delete)) } diff --git a/api/src/routes/pricing/flavor_price/modify.rs b/api/src/routes/pricing/flavor_price/modify.rs new file mode 100644 index 00000000..192f3224 --- /dev/null +++ b/api/src/routes/pricing/flavor_price/modify.rs @@ -0,0 +1,82 @@ +use crate::authorization::require_admin_user; +use crate::database::pricing::flavor_price::select_flavor_price_from_db; +use crate::error::{NotFoundOrUnexpectedApiError, OptionApiError}; +use actix_web::web::{Data, Json, Path, ReqData}; +use actix_web::HttpResponse; +use anyhow::Context; +use lrzcc_wire::pricing::{FlavorPrice, FlavorPriceModifyData}; +use lrzcc_wire::user::{Project, User}; +use sqlx::{Executor, MySql, MySqlPool, Transaction}; + +use super::FlavorPriceIdParam; + +#[tracing::instrument(name = "flavor_price_modify")] +pub async fn flavor_price_modify( + user: ReqData, + // TODO: we don't need this right? + project: ReqData, + db_pool: Data, + data: Json, + params: Path, +) -> Result { + require_admin_user(&user)?; + // TODO: do further validation + if data.id != params.flavor_price_id { + return Err(OptionApiError::ValidationError( + "ID in URL does not match ID in body".to_string(), + )); + } + let mut transaction = db_pool + .begin() + .await + .context("Failed to begin transaction")?; + let flavor_price = + update_flavor_price_in_db(&mut transaction, &data).await?; + transaction + .commit() + .await + .context("Failed to commit transaction")?; + Ok(HttpResponse::Ok() + .content_type("application/json") + .json(flavor_price)) +} + +#[tracing::instrument( + name = "update_flavor_price_in_db", + skip(data, transaction) +)] +pub async fn update_flavor_price_in_db( + transaction: &mut Transaction<'_, MySql>, + data: &FlavorPriceModifyData, +) -> Result { + let row = select_flavor_price_from_db(transaction, data.id as u64).await?; + let user_class = data.user_class.clone().unwrap_or(row.user_class); + let unit_price = data.unit_price.unwrap_or(row.unit_price); + let start_time = data.start_time.unwrap_or(row.start_time); + let flavor = data.flavor.unwrap_or(row.flavor); + let query = sqlx::query!( + r#" + UPDATE pricing_flavorprice + SET user_class = ?, unit_price = ?, start_time = ?, flavor_id = ? + WHERE id = ? + "#, + user_class, + unit_price, + start_time.to_utc(), + flavor, + data.id, + ); + transaction + .execute(query) + .await + .context("Failed to execute update query")?; + let project = FlavorPrice { + id: data.id, + user_class, + unit_price, + start_time, + flavor, + flavor_name: row.flavor_name, + }; + Ok(project) +} From 7321a2c98a73cbcff23108f0dc3b72e502460d73 Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Wed, 23 Oct 2024 16:10:51 +0200 Subject: [PATCH 19/46] docs(api/routes): remove done todo comment Signed-off-by: Sandro-Alessio Gierens --- api/src/routes/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/api/src/routes/mod.rs b/api/src/routes/mod.rs index b9f5ea15..f54b2c99 100644 --- a/api/src/routes/mod.rs +++ b/api/src/routes/mod.rs @@ -19,7 +19,6 @@ pub use user::*; // TODO: modify endpoints for // - accounting::server_state // - quota::flavor_quota -// - pricing::flavor_price // - budgeting::project_budget // - budgeting::user_budget From 24937ca2b4e0783e35e24b5f5f46d4da8ba7edad Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Wed, 23 Oct 2024 16:11:29 +0200 Subject: [PATCH 20/46] chore(changelog): update unreleased section Signed-off-by: Sandro-Alessio Gierens --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e5bbcb7..98f6d8b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,9 @@ This is the combined changelog of all contained `lrzcc` crates. - wire: derive FromRow for Flavor and make group_name field public - api: add resources::select_flavor_from_db to database module - api: add resources::flavor_modify endpoint +- api: add database::pricing::flavor_price submodule +- wire: derive Deserialize for FlavorPriceModifyData +- api: add flavor_price_modify endpoint - TODO: add remaining crud endpoints for all new modules - TODO: add tests for all new endpoints From d9ac04ef2ee9daa9839a83b9a5d1421142c6b894 Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Wed, 23 Oct 2024 16:11:48 +0200 Subject: [PATCH 21/46] chore(sqlx): update offline query data Signed-off-by: Sandro-Alessio Gierens --- ...df65cf406aec522373299b2f474ada65455ec.json | 12 +++ ...545e6c10ce00f084a13b79e3516238d13daea.json | 74 +++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 .sqlx/query-24cdcc26672d828f389071b26efdf65cf406aec522373299b2f474ada65455ec.json create mode 100644 .sqlx/query-2cc09a87b2981f353e315b8c032545e6c10ce00f084a13b79e3516238d13daea.json diff --git a/.sqlx/query-24cdcc26672d828f389071b26efdf65cf406aec522373299b2f474ada65455ec.json b/.sqlx/query-24cdcc26672d828f389071b26efdf65cf406aec522373299b2f474ada65455ec.json new file mode 100644 index 00000000..77cdb90a --- /dev/null +++ b/.sqlx/query-24cdcc26672d828f389071b26efdf65cf406aec522373299b2f474ada65455ec.json @@ -0,0 +1,12 @@ +{ + "db_name": "MySQL", + "query": "\n UPDATE pricing_flavorprice\n SET user_class = ?, unit_price = ?, start_time = ?, flavor_id = ?\n WHERE id = ?\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 5 + }, + "nullable": [] + }, + "hash": "24cdcc26672d828f389071b26efdf65cf406aec522373299b2f474ada65455ec" +} diff --git a/.sqlx/query-2cc09a87b2981f353e315b8c032545e6c10ce00f084a13b79e3516238d13daea.json b/.sqlx/query-2cc09a87b2981f353e315b8c032545e6c10ce00f084a13b79e3516238d13daea.json new file mode 100644 index 00000000..a17f2995 --- /dev/null +++ b/.sqlx/query-2cc09a87b2981f353e315b8c032545e6c10ce00f084a13b79e3516238d13daea.json @@ -0,0 +1,74 @@ +{ + "db_name": "MySQL", + "query": "\n SELECT\n p.id,\n p.flavor_id as flavor,\n f.name as flavor_name, \n p.user_class as user_class,\n p.unit_price as unit_price,\n p.start_time as start_time\n FROM\n pricing_flavorprice as p,\n resources_flavor as f\n WHERE\n p.flavor_id = f.id AND\n p.id = ?\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": { + "type": "Long", + "flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT", + "max_size": 11 + } + }, + { + "ordinal": 1, + "name": "flavor", + "type_info": { + "type": "LongLong", + "flags": "NOT_NULL | MULTIPLE_KEY | NO_DEFAULT_VALUE", + "max_size": 20 + } + }, + { + "ordinal": 2, + "name": "flavor_name", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE", + "max_size": 256 + } + }, + { + "ordinal": 3, + "name": "user_class", + "type_info": { + "type": "Short", + "flags": "NOT_NULL | MULTIPLE_KEY | UNSIGNED | NO_DEFAULT_VALUE", + "max_size": 5 + } + }, + { + "ordinal": 4, + "name": "unit_price", + "type_info": { + "type": "Double", + "flags": "NOT_NULL | NO_DEFAULT_VALUE", + "max_size": 22 + } + }, + { + "ordinal": 5, + "name": "start_time", + "type_info": { + "type": "Datetime", + "flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE", + "max_size": 26 + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false, + false, + false + ] + }, + "hash": "2cc09a87b2981f353e315b8c032545e6c10ce00f084a13b79e3516238d13daea" +} From 5a85ac4c7d63361a10d84aa8c95655670668e72e Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Wed, 23 Oct 2024 17:31:24 +0200 Subject: [PATCH 22/46] style(api/pricing): remove unnecessary clone Signed-off-by: Sandro-Alessio Gierens --- api/src/routes/pricing/flavor_price/modify.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/routes/pricing/flavor_price/modify.rs b/api/src/routes/pricing/flavor_price/modify.rs index 192f3224..c0eba2c2 100644 --- a/api/src/routes/pricing/flavor_price/modify.rs +++ b/api/src/routes/pricing/flavor_price/modify.rs @@ -50,7 +50,7 @@ pub async fn update_flavor_price_in_db( data: &FlavorPriceModifyData, ) -> Result { let row = select_flavor_price_from_db(transaction, data.id as u64).await?; - let user_class = data.user_class.clone().unwrap_or(row.user_class); + let user_class = data.user_class.unwrap_or(row.user_class); let unit_price = data.unit_price.unwrap_or(row.unit_price); let start_time = data.start_time.unwrap_or(row.start_time); let flavor = data.flavor.unwrap_or(row.flavor); From 1b02712c1a4651d987bec907788911770f36e4c9 Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Mon, 28 Oct 2024 00:00:47 +0100 Subject: [PATCH 23/46] chore(changelog): update unreleased section Signed-off-by: Sandro-Alessio Gierens --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98f6d8b0..2fe84bc8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,11 @@ This is the combined changelog of all contained `lrzcc` crates. - api: add flavor_price_modify endpoint - TODO: add remaining crud endpoints for all new modules - TODO: add tests for all new endpoints +- deb: bump config from 0.14.0 to 0.14.1 +- deb: bump anyhow from 1.0.90 to 1.0.91 +- deb: bump serde from 1.0.211 to 1.0.213 +- deb: bump thiserror from 1.0.64 to 1.0.65 +- deb: bump tokio from 1.40.0 to 1.41.0 ## [lrzcc-cli-v1.3.0] - 2024-10-08 From 2b59b174c355f468207801d7b43e335fb54116ab Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Mon, 28 Oct 2024 12:46:53 +0100 Subject: [PATCH 24/46] feat(wire): derive FromRow for budgeting::ProjectBudget and UserBudget Signed-off-by: Sandro-Alessio Gierens --- wire/src/budgeting/project_budget.rs | 3 ++- wire/src/budgeting/user_budget.rs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/wire/src/budgeting/project_budget.rs b/wire/src/budgeting/project_budget.rs index e7fca233..d41b0d86 100644 --- a/wire/src/budgeting/project_budget.rs +++ b/wire/src/budgeting/project_budget.rs @@ -1,9 +1,10 @@ use crate::common::is_false; use serde::{Deserialize, Serialize}; +use sqlx::FromRow; use std::fmt::Display; use tabled::Tabled; -#[derive(Clone, Debug, Deserialize, Serialize, Tabled, PartialEq)] +#[derive(Clone, Debug, Deserialize, Serialize, Tabled, PartialEq, FromRow)] pub struct ProjectBudget { pub id: u32, pub project: u32, diff --git a/wire/src/budgeting/user_budget.rs b/wire/src/budgeting/user_budget.rs index 48076434..fb3ab1ff 100644 --- a/wire/src/budgeting/user_budget.rs +++ b/wire/src/budgeting/user_budget.rs @@ -1,9 +1,10 @@ use crate::common::is_false; use serde::{Deserialize, Serialize}; +use sqlx::FromRow; use std::fmt::Display; use tabled::Tabled; -#[derive(Clone, Debug, Deserialize, Serialize, Tabled, PartialEq)] +#[derive(Clone, Debug, Deserialize, Serialize, Tabled, PartialEq, FromRow)] pub struct UserBudget { pub id: u32, pub user: u32, From 361ef7ac727d3db6f8edba04317657f6c667db18 Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Mon, 28 Oct 2024 12:47:37 +0100 Subject: [PATCH 25/46] feat(wire/budgeting): derive Deserialize for Project/UserBudgetModifyData Signed-off-by: Sandro-Alessio Gierens --- wire/src/budgeting/project_budget.rs | 2 +- wire/src/budgeting/user_budget.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/wire/src/budgeting/project_budget.rs b/wire/src/budgeting/project_budget.rs index d41b0d86..d8f327c6 100644 --- a/wire/src/budgeting/project_budget.rs +++ b/wire/src/budgeting/project_budget.rs @@ -38,7 +38,7 @@ impl ProjectBudgetCreateData { } } -#[derive(Clone, Debug, Serialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct ProjectBudgetModifyData { pub id: u32, diff --git a/wire/src/budgeting/user_budget.rs b/wire/src/budgeting/user_budget.rs index fb3ab1ff..fe0603c5 100644 --- a/wire/src/budgeting/user_budget.rs +++ b/wire/src/budgeting/user_budget.rs @@ -38,7 +38,7 @@ impl UserBudgetCreateData { } } -#[derive(Clone, Debug, Serialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct UserBudgetModifyData { pub id: u32, From 5fb9b13e37e9eaf8b23961954d9c0c41b543c62e Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Mon, 28 Oct 2024 12:49:19 +0100 Subject: [PATCH 26/46] feat(api/database): add budgeting submodule with select_project_budget_from_db Signed-off-by: Sandro-Alessio Gierens --- api/src/database/budgeting/mod.rs | 2 + api/src/database/budgeting/project_budget.rs | 51 ++++++++++++++++++++ api/src/database/mod.rs | 1 + 3 files changed, 54 insertions(+) create mode 100644 api/src/database/budgeting/mod.rs create mode 100644 api/src/database/budgeting/project_budget.rs diff --git a/api/src/database/budgeting/mod.rs b/api/src/database/budgeting/mod.rs new file mode 100644 index 00000000..360b51c8 --- /dev/null +++ b/api/src/database/budgeting/mod.rs @@ -0,0 +1,2 @@ +pub mod project_budget; +pub mod user_budget; diff --git a/api/src/database/budgeting/project_budget.rs b/api/src/database/budgeting/project_budget.rs new file mode 100644 index 00000000..cab738b5 --- /dev/null +++ b/api/src/database/budgeting/project_budget.rs @@ -0,0 +1,51 @@ +use crate::error::{NotFoundOrUnexpectedApiError, UnexpectedOnlyError}; +use anyhow::Context; +use lrzcc_wire::budgeting::ProjectBudget; +use sqlx::{Executor, FromRow, MySql, Transaction}; + +#[tracing::instrument( + name = "select_maybe_project_budget_from_db", + skip(transaction) +)] +pub async fn select_maybe_project_budget_from_db( + transaction: &mut Transaction<'_, MySql>, + project_budget_id: u64, +) -> Result, UnexpectedOnlyError> { + let query = sqlx::query!( + r#" + SELECT b.id, p.id as project, p.name as project_name, b.year, b.amount + FROM budgeting_projectbudget as b, user_project as p + WHERE + b.project_id = p.id AND + b.id = ? + "#, + project_budget_id + ); + let row = transaction + .fetch_optional(query) + .await + .context("Failed to execute select query")?; + // TODO: isn't there a nicer way to write this? + Ok(match row { + Some(row) => Some( + ProjectBudget::from_row(&row) + .context("Failed to parse project_budget row")?, + ), + None => None, + }) +} + +#[tracing::instrument( + name = "select_project_budget_from_db", + skip(transaction) +)] +pub async fn select_project_budget_from_db( + transaction: &mut Transaction<'_, MySql>, + project_budget_id: u64, +) -> Result { + select_maybe_project_budget_from_db(transaction, project_budget_id) + .await? + .ok_or(NotFoundOrUnexpectedApiError::NotFoundError( + "Project budget with given ID not found".to_string(), + )) +} diff --git a/api/src/database/mod.rs b/api/src/database/mod.rs index e9f2a821..4f5c7e46 100644 --- a/api/src/database/mod.rs +++ b/api/src/database/mod.rs @@ -1,3 +1,4 @@ +pub mod budgeting; pub mod pricing; pub mod resources; pub mod user; From 335e5570b777bc32c127946eb1c2403dd1b84e9b Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Mon, 28 Oct 2024 12:49:43 +0100 Subject: [PATCH 27/46] feat(api/database/budgeting): add submodule with select_user_budget_from_db Signed-off-by: Sandro-Alessio Gierens --- api/src/database/budgeting/user_budget.rs | 48 +++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 api/src/database/budgeting/user_budget.rs diff --git a/api/src/database/budgeting/user_budget.rs b/api/src/database/budgeting/user_budget.rs new file mode 100644 index 00000000..adc36f11 --- /dev/null +++ b/api/src/database/budgeting/user_budget.rs @@ -0,0 +1,48 @@ +use crate::error::{NotFoundOrUnexpectedApiError, UnexpectedOnlyError}; +use anyhow::Context; +use lrzcc_wire::budgeting::UserBudget; +use sqlx::{Executor, FromRow, MySql, Transaction}; + +#[tracing::instrument( + name = "select_maybe_user_budget_from_db", + skip(transaction) +)] +pub async fn select_maybe_user_budget_from_db( + transaction: &mut Transaction<'_, MySql>, + user_budget_id: u64, +) -> Result, UnexpectedOnlyError> { + let query = sqlx::query!( + r#" + SELECT b.id, u.id as user, u.name as username, b.year, b.amount + FROM budgeting_userbudget as b, user_user as u + WHERE + b.user_id = u.id AND + b.id = ? + "#, + user_budget_id + ); + let row = transaction + .fetch_optional(query) + .await + .context("Failed to execute select query")?; + // TODO: isn't there a nicer way to write this? + Ok(match row { + Some(row) => Some( + UserBudget::from_row(&row) + .context("Failed to parse user_budget row")?, + ), + None => None, + }) +} + +#[tracing::instrument(name = "select_user_budget_from_db", skip(transaction))] +pub async fn select_user_budget_from_db( + transaction: &mut Transaction<'_, MySql>, + user_budget_id: u64, +) -> Result { + select_maybe_user_budget_from_db(transaction, user_budget_id) + .await? + .ok_or(NotFoundOrUnexpectedApiError::NotFoundError( + "User budget with given ID not found".to_string(), + )) +} From 69ca75ff7feffdff9dcb8b72d0eb411083b676d6 Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Mon, 28 Oct 2024 12:52:38 +0100 Subject: [PATCH 28/46] feat(api/budgeting): add simplified project_budget_modify endpoint Signed-off-by: Sandro-Alessio Gierens --- .../routes/budgeting/project_budget/mod.rs | 9 ++- .../routes/budgeting/project_budget/modify.rs | 79 +++++++++++++++++++ api/src/routes/mod.rs | 6 +- 3 files changed, 88 insertions(+), 6 deletions(-) create mode 100644 api/src/routes/budgeting/project_budget/modify.rs diff --git a/api/src/routes/budgeting/project_budget/mod.rs b/api/src/routes/budgeting/project_budget/mod.rs index b88ac3f6..fe289bae 100644 --- a/api/src/routes/budgeting/project_budget/mod.rs +++ b/api/src/routes/budgeting/project_budget/mod.rs @@ -1,6 +1,7 @@ use actix_web::web::{ delete, - // get, patch, + // get, + patch, post, scope, }; @@ -13,8 +14,8 @@ use create::project_budget_create; // use list::project_budget_list; // mod get; // use get::project_budget_get; -// mod modify; -// use modify::project_budget_modify; +mod modify; +use modify::project_budget_modify; mod delete; use delete::project_budget_delete; @@ -24,7 +25,7 @@ pub fn project_budgets_scope() -> Scope { // .route("", get().to(project_budget_list)) // .route("/{project_budget_id}", get().to(project_budget_get)) // TODO: what about PUT? - // .route("/{project_budget_id}/", patch().to(project_budget_modify)) + .route("/{project_budget_id}/", patch().to(project_budget_modify)) .route("/{project_budget_id}/", delete().to(project_budget_delete)) } diff --git a/api/src/routes/budgeting/project_budget/modify.rs b/api/src/routes/budgeting/project_budget/modify.rs new file mode 100644 index 00000000..d5ff4010 --- /dev/null +++ b/api/src/routes/budgeting/project_budget/modify.rs @@ -0,0 +1,79 @@ +use crate::authorization::require_admin_user; +use crate::database::budgeting::project_budget::select_project_budget_from_db; +use crate::error::{NotFoundOrUnexpectedApiError, OptionApiError}; +use actix_web::web::{Data, Json, Path, ReqData}; +use actix_web::HttpResponse; +use anyhow::Context; +use lrzcc_wire::budgeting::{ProjectBudget, ProjectBudgetModifyData}; +use lrzcc_wire::user::{Project, User}; +use sqlx::{Executor, MySql, MySqlPool, Transaction}; + +use super::ProjectBudgetIdParam; + +#[tracing::instrument(name = "project_budget_modify")] +pub async fn project_budget_modify( + user: ReqData, + // TODO: we don't need this right? + project: ReqData, + db_pool: Data, + data: Json, + params: Path, +) -> Result { + // TODO: allow master user access + // TODO: check that cost is below + // TODO: handle force option + require_admin_user(&user)?; + // TODO: do further validation + if data.id != params.project_budget_id { + return Err(OptionApiError::ValidationError( + "ID in URL does not match ID in body".to_string(), + )); + } + let mut transaction = db_pool + .begin() + .await + .context("Failed to begin transaction")?; + let project_budget = + update_project_budget_in_db(&mut transaction, &data).await?; + transaction + .commit() + .await + .context("Failed to commit transaction")?; + Ok(HttpResponse::Ok() + .content_type("application/json") + .json(project_budget)) +} + +#[tracing::instrument( + name = "update_project_budget_in_db", + skip(data, transaction) +)] +pub async fn update_project_budget_in_db( + transaction: &mut Transaction<'_, MySql>, + data: &ProjectBudgetModifyData, +) -> Result { + let row = + select_project_budget_from_db(transaction, data.id as u64).await?; + let amount = data.amount.clone().unwrap_or(row.amount); + let query = sqlx::query!( + r#" + UPDATE budgeting_projectbudget + SET amount = ? + WHERE id = ? + "#, + amount, + data.id, + ); + transaction + .execute(query) + .await + .context("Failed to execute update query")?; + let project = ProjectBudget { + id: data.id, + amount, + project: row.project, + project_name: row.project_name, + year: row.year, + }; + Ok(project) +} diff --git a/api/src/routes/mod.rs b/api/src/routes/mod.rs index f54b2c99..a3bc1265 100644 --- a/api/src/routes/mod.rs +++ b/api/src/routes/mod.rs @@ -19,8 +19,6 @@ pub use user::*; // TODO: modify endpoints for // - accounting::server_state // - quota::flavor_quota -// - budgeting::project_budget -// - budgeting::user_budget // TODO: get endpoints for // - accounting::server_state @@ -39,3 +37,7 @@ pub use user::*; // - pricing::flavor_price // - budgeting::project_budget // - budgeting::user_budget + +// TODO: improve the following endpoints +// - budgeting::project_budget::modify +// - budgeting::user_budget::modify From 25350bf31f4990cab2f40249eb77819f30170c2d Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Mon, 28 Oct 2024 12:53:08 +0100 Subject: [PATCH 29/46] feat(api/budgeting): add simplified user_budget_modify endpoint Signed-off-by: Sandro-Alessio Gierens --- api/src/routes/budgeting/user_budget/mod.rs | 9 ++- .../routes/budgeting/user_budget/modify.rs | 77 +++++++++++++++++++ 2 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 api/src/routes/budgeting/user_budget/modify.rs diff --git a/api/src/routes/budgeting/user_budget/mod.rs b/api/src/routes/budgeting/user_budget/mod.rs index 8cce351e..643fde35 100644 --- a/api/src/routes/budgeting/user_budget/mod.rs +++ b/api/src/routes/budgeting/user_budget/mod.rs @@ -1,6 +1,7 @@ use actix_web::web::{ delete, - // get, patch, + // get, + patch, post, scope, }; @@ -13,8 +14,8 @@ use create::user_budget_create; // use list::user_budget_list; // mod get; // use get::user_budget_get; -// mod modify; -// use modify::user_budget_modify; +mod modify; +use modify::user_budget_modify; mod delete; use delete::user_budget_delete; @@ -24,7 +25,7 @@ pub fn user_budgets_scope() -> Scope { // .route("", get().to(user_budget_list)) // .route("/{user_budget_id}", get().to(user_budget_get)) // TODO: what about PUT? - // .route("/{user_budget_id}/", patch().to(user_budget_modify)) + .route("/{user_budget_id}/", patch().to(user_budget_modify)) .route("/{user_budget_id}/", delete().to(user_budget_delete)) } diff --git a/api/src/routes/budgeting/user_budget/modify.rs b/api/src/routes/budgeting/user_budget/modify.rs new file mode 100644 index 00000000..3e5cfd7d --- /dev/null +++ b/api/src/routes/budgeting/user_budget/modify.rs @@ -0,0 +1,77 @@ +use crate::authorization::require_admin_user; +use crate::database::budgeting::user_budget::select_user_budget_from_db; +use crate::error::{NotFoundOrUnexpectedApiError, OptionApiError}; +use actix_web::web::{Data, Json, Path, ReqData}; +use actix_web::HttpResponse; +use anyhow::Context; +use lrzcc_wire::budgeting::{UserBudget, UserBudgetModifyData}; +use lrzcc_wire::user::{Project, User}; +use sqlx::{Executor, MySql, MySqlPool, Transaction}; + +use super::UserBudgetIdParam; + +#[tracing::instrument(name = "user_budget_modify")] +pub async fn user_budget_modify( + user: ReqData, + // TODO: we don't need this right? + project: ReqData, + db_pool: Data, + data: Json, + params: Path, +) -> Result { + // TODO: allow master user access + // TODO: check that cost is below + // TODO: handle force option + require_admin_user(&user)?; + // TODO: do further validation + if data.id != params.user_budget_id { + return Err(OptionApiError::ValidationError( + "ID in URL does not match ID in body".to_string(), + )); + } + let mut transaction = db_pool + .begin() + .await + .context("Failed to begin transaction")?; + let user_budget = update_user_budget_in_db(&mut transaction, &data).await?; + transaction + .commit() + .await + .context("Failed to commit transaction")?; + Ok(HttpResponse::Ok() + .content_type("application/json") + .json(user_budget)) +} + +#[tracing::instrument( + name = "update_user_budget_in_db", + skip(data, transaction) +)] +pub async fn update_user_budget_in_db( + transaction: &mut Transaction<'_, MySql>, + data: &UserBudgetModifyData, +) -> Result { + let row = select_user_budget_from_db(transaction, data.id as u64).await?; + let amount = data.amount.clone().unwrap_or(row.amount); + let query = sqlx::query!( + r#" + UPDATE budgeting_userbudget + SET amount = ? + WHERE id = ? + "#, + amount, + data.id, + ); + transaction + .execute(query) + .await + .context("Failed to execute update query")?; + let project = UserBudget { + id: data.id, + amount, + user: row.user, + username: row.username, + year: row.year, + }; + Ok(project) +} From 3a420ab4a33ca611396d8b67d22fe98c5751ad04 Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Mon, 28 Oct 2024 12:53:26 +0100 Subject: [PATCH 30/46] chore(changelog): update unreleased section Signed-off-by: Sandro-Alessio Gierens --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fe84bc8..8afbd77e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,11 @@ This is the combined changelog of all contained `lrzcc` crates. - deb: bump serde from 1.0.211 to 1.0.213 - deb: bump thiserror from 1.0.64 to 1.0.65 - deb: bump tokio from 1.40.0 to 1.41.0 +- wire: derive FromRow for ProjectBudget and UserBudget +- wire: derive Deserialize for ProjectBudgetModifyData and UserBudgetModifyData +- api: add database::budgeting::project/user_budget submodule with helpers +- api: add simplified budgeting::project_budget_modify endpoint +- api: add simplified budgeting::user_budget_modify endpoint ## [lrzcc-cli-v1.3.0] - 2024-10-08 From ddeddac79950e04eb62502937e39119bcc097c95 Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Mon, 28 Oct 2024 12:53:52 +0100 Subject: [PATCH 31/46] chore(sqlx): update offline query data Signed-off-by: Sandro-Alessio Gierens --- ...e535b6ba2ca1694e4e2f706c1787a34be4eb3.json | 12 ++++ ...671a0fc5aad016e23d155377cd88e28890abc.json | 64 +++++++++++++++++++ ...bdcd332d9fdca2df5bd14dc3b264eaac40913.json | 12 ++++ ...7e40ea1b7f1c60a694e5e8c946c9ce49b99fc.json | 64 +++++++++++++++++++ 4 files changed, 152 insertions(+) create mode 100644 .sqlx/query-4c38fe78304abd8ef6950b429a4e535b6ba2ca1694e4e2f706c1787a34be4eb3.json create mode 100644 .sqlx/query-82b177cbbe313e517abae6d568c671a0fc5aad016e23d155377cd88e28890abc.json create mode 100644 .sqlx/query-88796881b4d689f1b891b549f0bbdcd332d9fdca2df5bd14dc3b264eaac40913.json create mode 100644 .sqlx/query-e43cdaac7bf0d2f82e72f1c29b47e40ea1b7f1c60a694e5e8c946c9ce49b99fc.json diff --git a/.sqlx/query-4c38fe78304abd8ef6950b429a4e535b6ba2ca1694e4e2f706c1787a34be4eb3.json b/.sqlx/query-4c38fe78304abd8ef6950b429a4e535b6ba2ca1694e4e2f706c1787a34be4eb3.json new file mode 100644 index 00000000..f005e7f1 --- /dev/null +++ b/.sqlx/query-4c38fe78304abd8ef6950b429a4e535b6ba2ca1694e4e2f706c1787a34be4eb3.json @@ -0,0 +1,12 @@ +{ + "db_name": "MySQL", + "query": "\n UPDATE budgeting_projectbudget\n SET amount = ?\n WHERE id = ?\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "4c38fe78304abd8ef6950b429a4e535b6ba2ca1694e4e2f706c1787a34be4eb3" +} diff --git a/.sqlx/query-82b177cbbe313e517abae6d568c671a0fc5aad016e23d155377cd88e28890abc.json b/.sqlx/query-82b177cbbe313e517abae6d568c671a0fc5aad016e23d155377cd88e28890abc.json new file mode 100644 index 00000000..6fd588f0 --- /dev/null +++ b/.sqlx/query-82b177cbbe313e517abae6d568c671a0fc5aad016e23d155377cd88e28890abc.json @@ -0,0 +1,64 @@ +{ + "db_name": "MySQL", + "query": "\n SELECT b.id, p.id as project, p.name as project_name, b.year, b.amount\n FROM budgeting_projectbudget as b, user_project as p\n WHERE\n b.project_id = p.id AND\n b.id = ?\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": { + "type": "Long", + "flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT", + "max_size": 11 + } + }, + { + "ordinal": 1, + "name": "project", + "type_info": { + "type": "Long", + "flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT", + "max_size": 11 + } + }, + { + "ordinal": 2, + "name": "project_name", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE", + "max_size": 1020 + } + }, + { + "ordinal": 3, + "name": "year", + "type_info": { + "type": "Short", + "flags": "NOT_NULL | MULTIPLE_KEY | UNSIGNED | NO_DEFAULT_VALUE", + "max_size": 5 + } + }, + { + "ordinal": 4, + "name": "amount", + "type_info": { + "type": "Long", + "flags": "NOT_NULL | UNSIGNED | NO_DEFAULT_VALUE", + "max_size": 10 + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false, + false + ] + }, + "hash": "82b177cbbe313e517abae6d568c671a0fc5aad016e23d155377cd88e28890abc" +} diff --git a/.sqlx/query-88796881b4d689f1b891b549f0bbdcd332d9fdca2df5bd14dc3b264eaac40913.json b/.sqlx/query-88796881b4d689f1b891b549f0bbdcd332d9fdca2df5bd14dc3b264eaac40913.json new file mode 100644 index 00000000..04dac5a2 --- /dev/null +++ b/.sqlx/query-88796881b4d689f1b891b549f0bbdcd332d9fdca2df5bd14dc3b264eaac40913.json @@ -0,0 +1,12 @@ +{ + "db_name": "MySQL", + "query": "\n UPDATE budgeting_userbudget\n SET amount = ?\n WHERE id = ?\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "88796881b4d689f1b891b549f0bbdcd332d9fdca2df5bd14dc3b264eaac40913" +} diff --git a/.sqlx/query-e43cdaac7bf0d2f82e72f1c29b47e40ea1b7f1c60a694e5e8c946c9ce49b99fc.json b/.sqlx/query-e43cdaac7bf0d2f82e72f1c29b47e40ea1b7f1c60a694e5e8c946c9ce49b99fc.json new file mode 100644 index 00000000..dbe28136 --- /dev/null +++ b/.sqlx/query-e43cdaac7bf0d2f82e72f1c29b47e40ea1b7f1c60a694e5e8c946c9ce49b99fc.json @@ -0,0 +1,64 @@ +{ + "db_name": "MySQL", + "query": "\n SELECT b.id, u.id as user, u.name as username, b.year, b.amount\n FROM budgeting_userbudget as b, user_user as u\n WHERE\n b.user_id = u.id AND\n b.id = ?\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": { + "type": "Long", + "flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT", + "max_size": 11 + } + }, + { + "ordinal": 1, + "name": "user", + "type_info": { + "type": "Long", + "flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT", + "max_size": 11 + } + }, + { + "ordinal": 2, + "name": "username", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE", + "max_size": 1020 + } + }, + { + "ordinal": 3, + "name": "year", + "type_info": { + "type": "Short", + "flags": "NOT_NULL | MULTIPLE_KEY | UNSIGNED | NO_DEFAULT_VALUE", + "max_size": 5 + } + }, + { + "ordinal": 4, + "name": "amount", + "type_info": { + "type": "Long", + "flags": "NOT_NULL | UNSIGNED | NO_DEFAULT_VALUE", + "max_size": 10 + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false, + false + ] + }, + "hash": "e43cdaac7bf0d2f82e72f1c29b47e40ea1b7f1c60a694e5e8c946c9ce49b99fc" +} From 8e038fc61e31d549c03c7180baaab7cd42a3efd7 Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Mon, 28 Oct 2024 12:54:46 +0100 Subject: [PATCH 32/46] style(api/budgeting): remove unnecessary clone Signed-off-by: Sandro-Alessio Gierens --- api/src/routes/budgeting/project_budget/modify.rs | 2 +- api/src/routes/budgeting/user_budget/modify.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/src/routes/budgeting/project_budget/modify.rs b/api/src/routes/budgeting/project_budget/modify.rs index d5ff4010..a212a834 100644 --- a/api/src/routes/budgeting/project_budget/modify.rs +++ b/api/src/routes/budgeting/project_budget/modify.rs @@ -54,7 +54,7 @@ pub async fn update_project_budget_in_db( ) -> Result { let row = select_project_budget_from_db(transaction, data.id as u64).await?; - let amount = data.amount.clone().unwrap_or(row.amount); + let amount = data.amount.unwrap_or(row.amount); let query = sqlx::query!( r#" UPDATE budgeting_projectbudget diff --git a/api/src/routes/budgeting/user_budget/modify.rs b/api/src/routes/budgeting/user_budget/modify.rs index 3e5cfd7d..928601d5 100644 --- a/api/src/routes/budgeting/user_budget/modify.rs +++ b/api/src/routes/budgeting/user_budget/modify.rs @@ -52,7 +52,7 @@ pub async fn update_user_budget_in_db( data: &UserBudgetModifyData, ) -> Result { let row = select_user_budget_from_db(transaction, data.id as u64).await?; - let amount = data.amount.clone().unwrap_or(row.amount); + let amount = data.amount.unwrap_or(row.amount); let query = sqlx::query!( r#" UPDATE budgeting_userbudget From 89fc955b13a0d4d918b31324558677f48f2edf92 Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Wed, 30 Oct 2024 10:45:06 +0100 Subject: [PATCH 33/46] feat(wire/quota): derive FromRow for FlavorQuota Signed-off-by: Sandro-Alessio Gierens --- wire/src/quota.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/wire/src/quota.rs b/wire/src/quota.rs index ee3c9143..c4eb6508 100644 --- a/wire/src/quota.rs +++ b/wire/src/quota.rs @@ -1,8 +1,9 @@ use serde::{Deserialize, Serialize}; +use sqlx::FromRow; use std::fmt::Display; use tabled::Tabled; -#[derive(Clone, Debug, Deserialize, Serialize, Tabled, PartialEq)] +#[derive(Clone, Debug, Deserialize, Serialize, Tabled, PartialEq, FromRow)] pub struct FlavorQuota { pub id: u32, pub user: u32, From 296f21dcdcba189b5c64d492b57fde1b1ef857c9 Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Wed, 30 Oct 2024 10:46:23 +0100 Subject: [PATCH 34/46] feat(api/database): add quota submodule with select_flavor_quota_from_db Signed-off-by: Sandro-Alessio Gierens --- api/src/database/mod.rs | 1 + api/src/database/quota/flavor_quota.rs | 59 ++++++++++++++++++++++++++ api/src/database/quota/mod.rs | 1 + 3 files changed, 61 insertions(+) create mode 100644 api/src/database/quota/flavor_quota.rs create mode 100644 api/src/database/quota/mod.rs diff --git a/api/src/database/mod.rs b/api/src/database/mod.rs index 4f5c7e46..3cf42a71 100644 --- a/api/src/database/mod.rs +++ b/api/src/database/mod.rs @@ -1,4 +1,5 @@ pub mod budgeting; pub mod pricing; +pub mod quota; pub mod resources; pub mod user; diff --git a/api/src/database/quota/flavor_quota.rs b/api/src/database/quota/flavor_quota.rs new file mode 100644 index 00000000..fd2d0a98 --- /dev/null +++ b/api/src/database/quota/flavor_quota.rs @@ -0,0 +1,59 @@ +use crate::error::{NotFoundOrUnexpectedApiError, UnexpectedOnlyError}; +use anyhow::Context; +use lrzcc_wire::quota::FlavorQuota; +use sqlx::{Executor, FromRow, MySql, Transaction}; + +#[tracing::instrument( + name = "select_maybe_flavor_quota_from_db", + skip(transaction) +)] +pub async fn select_maybe_flavor_quota_from_db( + transaction: &mut Transaction<'_, MySql>, + flavor_quota_id: u64, +) -> Result, UnexpectedOnlyError> { + let query = sqlx::query!( + r#" + SELECT + q.id as id, + u.id as user, + u.name as username, + q.quota as quota, + g.id as flavor_group, + g.name as flavor_group_name + FROM + quota_flavorquota as f, + quota_quota as q, + resources_flavorgroup as g, + user_user as u + WHERE + f.quota_ptr_id = q.id AND + f.flavor_group_id = g.id AND + q.user_id = u.id AND + q.id = ? + "#, + flavor_quota_id + ); + let row = transaction + .fetch_optional(query) + .await + .context("Failed to execute select query")?; + Ok(match row { + Some(row) => Some( + FlavorQuota::from_row(&row) + .context("Failed to parse flavor quota row")?, + ), + None => None, + }) +} + +#[tracing::instrument(name = "select_flavor_quota_from_db", skip(transaction))] +pub async fn select_flavor_quota_from_db( + transaction: &mut Transaction<'_, MySql>, + flavor_quota_id: u64, +) -> Result { + select_maybe_flavor_quota_from_db(transaction, flavor_quota_id) + .await? + .ok_or(NotFoundOrUnexpectedApiError::NotFoundError( + "Flavor quota with given ID not found".to_string(), + )) +} diff --git a/api/src/database/quota/mod.rs b/api/src/database/quota/mod.rs new file mode 100644 index 00000000..4d25be97 --- /dev/null +++ b/api/src/database/quota/mod.rs @@ -0,0 +1 @@ +pub mod flavor_quota; From 205dabee8fa04d04620b76b1c14ebb8c06986591 Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Wed, 30 Oct 2024 10:46:55 +0100 Subject: [PATCH 35/46] docs(api/user): add todo comment regarding wrong project name in user_modify Signed-off-by: Sandro-Alessio Gierens --- api/src/routes/user/user/modify.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/api/src/routes/user/user/modify.rs b/api/src/routes/user/user/modify.rs index 7d2f02b9..a0cb7ca3 100644 --- a/api/src/routes/user/user/modify.rs +++ b/api/src/routes/user/user/modify.rs @@ -74,6 +74,7 @@ pub async fn update_project_in_db( name, openstack_id, project: project_id, + // TODO: we need to get the new project's name project_name: row.project_name, role, is_staff, From b9cd85f8b15cb4bda0cefa810f25a0d72f7b3fcb Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Wed, 30 Oct 2024 10:48:29 +0100 Subject: [PATCH 36/46] fix(api/user): correct name of update_user_in_db Signed-off-by: Sandro-Alessio Gierens --- api/src/routes/user/user/modify.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/src/routes/user/user/modify.rs b/api/src/routes/user/user/modify.rs index a0cb7ca3..78a78ed4 100644 --- a/api/src/routes/user/user/modify.rs +++ b/api/src/routes/user/user/modify.rs @@ -29,7 +29,7 @@ pub async fn user_modify( .begin() .await .context("Failed to begin transaction")?; - let project = update_project_in_db(&mut transaction, &data).await?; + let project = update_user_in_db(&mut transaction, &data).await?; transaction .commit() .await @@ -39,8 +39,8 @@ pub async fn user_modify( .json(project)) } -#[tracing::instrument(name = "update_project_in_db", skip(data, transaction))] -pub async fn update_project_in_db( +#[tracing::instrument(name = "update_user_in_db", skip(data, transaction))] +pub async fn update_user_in_db( transaction: &mut Transaction<'_, MySql>, data: &UserModifyData, ) -> Result { From 920ccd643b06361d38b68546affa59f5e7c2b2bf Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Wed, 30 Oct 2024 10:49:01 +0100 Subject: [PATCH 37/46] fix(api/pricing): correct variable name in update_flavor_price_in_db Signed-off-by: Sandro-Alessio Gierens --- api/src/routes/pricing/flavor_price/modify.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/src/routes/pricing/flavor_price/modify.rs b/api/src/routes/pricing/flavor_price/modify.rs index c0eba2c2..69c9379c 100644 --- a/api/src/routes/pricing/flavor_price/modify.rs +++ b/api/src/routes/pricing/flavor_price/modify.rs @@ -70,7 +70,7 @@ pub async fn update_flavor_price_in_db( .execute(query) .await .context("Failed to execute update query")?; - let project = FlavorPrice { + let price = FlavorPrice { id: data.id, user_class, unit_price, @@ -78,5 +78,5 @@ pub async fn update_flavor_price_in_db( flavor, flavor_name: row.flavor_name, }; - Ok(project) + Ok(price) } From df2605db55f2c45186bf24943b9bc230d6f07b60 Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Wed, 30 Oct 2024 10:50:05 +0100 Subject: [PATCH 38/46] feat(api/routes/quota): add simplified flavor_quota_modify endpoint Signed-off-by: Sandro-Alessio Gierens --- api/src/routes/quota/flavor_quota/mod.rs | 9 +- api/src/routes/quota/flavor_quota/modify.rs | 98 +++++++++++++++++++++ 2 files changed, 103 insertions(+), 4 deletions(-) create mode 100644 api/src/routes/quota/flavor_quota/modify.rs diff --git a/api/src/routes/quota/flavor_quota/mod.rs b/api/src/routes/quota/flavor_quota/mod.rs index e49f72f4..85a4b7c1 100644 --- a/api/src/routes/quota/flavor_quota/mod.rs +++ b/api/src/routes/quota/flavor_quota/mod.rs @@ -1,6 +1,7 @@ use actix_web::web::{ delete, - // get, patch, + // get, + patch, post, scope, }; @@ -13,8 +14,8 @@ use create::flavor_quota_create; // use list::flavor_quota_list; // mod get; // use get::flavor_quota_get; -// mod modify; -// use modify::flavor_quota_modify; +mod modify; +use modify::flavor_quota_modify; mod delete; use delete::flavor_quota_delete; @@ -24,7 +25,7 @@ pub fn flavor_quotas_scope() -> Scope { // .route("", get().to(flavor_quota_list)) // .route("/{flavor_quota_id}", get().to(flavor_quota_get)) // TODO: what about PUT? - // .route("/{flavor_quota_id}/", patch().to(flavor_quota_modify)) + .route("/{flavor_quota_id}/", patch().to(flavor_quota_modify)) .route("/{flavor_quota_id}/", delete().to(flavor_quota_delete)) } diff --git a/api/src/routes/quota/flavor_quota/modify.rs b/api/src/routes/quota/flavor_quota/modify.rs new file mode 100644 index 00000000..061267fb --- /dev/null +++ b/api/src/routes/quota/flavor_quota/modify.rs @@ -0,0 +1,98 @@ +use crate::authorization::require_admin_user; +use crate::database::quota::flavor_quota::select_flavor_quota_from_db; +use crate::error::{NotFoundOrUnexpectedApiError, OptionApiError}; +use actix_web::web::{Data, Json, Path, ReqData}; +use actix_web::HttpResponse; +use anyhow::Context; +use lrzcc_wire::quota::{FlavorQuota, FlavorQuotaModifyData}; +use lrzcc_wire::user::{Project, User}; +use sqlx::{Executor, MySql, MySqlPool, Transaction}; + +use super::FlavorQuotaIdParam; + +#[tracing::instrument(name = "flavor_quota_modify")] +pub async fn flavor_quota_modify( + user: ReqData, + // TODO: we don't need this right? + project: ReqData, + db_pool: Data, + data: Json, + params: Path, +) -> Result { + require_admin_user(&user)?; + // TODO: do further validation + if data.id != params.flavor_quota_id { + return Err(OptionApiError::ValidationError( + "ID in URL does not match ID in body".to_string(), + )); + } + let mut transaction = db_pool + .begin() + .await + .context("Failed to begin transaction")?; + let flavor_quota = + update_flavor_quota_in_db(&mut transaction, &data).await?; + transaction + .commit() + .await + .context("Failed to commit transaction")?; + Ok(HttpResponse::Ok() + .content_type("application/json") + .json(flavor_quota)) +} + +#[tracing::instrument( + name = "update_flavor_quota_in_db", + skip(data, transaction) +)] +pub async fn update_flavor_quota_in_db( + transaction: &mut Transaction<'_, MySql>, + data: &FlavorQuotaModifyData, +) -> Result { + let row = select_flavor_quota_from_db(transaction, data.id as u64).await?; + let user = data.user.unwrap_or(row.user); + // TODO: what about the username + let quota = data.quota.unwrap_or(row.quota); + let flavor_group = data.flavor_group.unwrap_or(row.flavor_group); + // TODO: what about the flavor group name + let query1 = sqlx::query!( + r#" + UPDATE quota_quota + SET + user_id = ?, + quota = ? + WHERE id = ? + "#, + user, + quota, + data.id, + ); + transaction + .execute(query1) + .await + .context("Failed to execute first update query")?; + let query2 = sqlx::query!( + r#" + UPDATE quota_flavorquota + SET flavor_group_id = ? + WHERE quota_ptr_id = ? + "#, + flavor_group, + data.id, + ); + transaction + .execute(query2) + .await + .context("Failed to execute second update query")?; + let flavor_quota = FlavorQuota { + id: data.id, + user, + // TODO: we need to get the new username + username: row.username, + quota, + flavor_group, + // TODO: we need to get the new flavor group name + flavor_group_name: row.flavor_group_name, + }; + Ok(flavor_quota) +} From 29369415f6a31e444b594d83a5d0a3484c5f86f1 Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Wed, 30 Oct 2024 10:53:21 +0100 Subject: [PATCH 39/46] chore(sqlx): update offline query data Signed-off-by: Sandro-Alessio Gierens --- ...8b4b499f3ba6c9b63154bdf56a09017b91ebf.json | 12 +++ ...e893939d369e1b5d76aff33c46ef89f2b5aa1.json | 12 +++ ...57a5a7bc2203c6930fad5116c88988fecea53.json | 74 +++++++++++++++++++ 3 files changed, 98 insertions(+) create mode 100644 .sqlx/query-0cd6ae84400d5fbb5964a9cab3b8b4b499f3ba6c9b63154bdf56a09017b91ebf.json create mode 100644 .sqlx/query-1d7bce4cb1885d5c62149462f3ee893939d369e1b5d76aff33c46ef89f2b5aa1.json create mode 100644 .sqlx/query-952fb3be5f3ca26ac1a663e4bdd57a5a7bc2203c6930fad5116c88988fecea53.json diff --git a/.sqlx/query-0cd6ae84400d5fbb5964a9cab3b8b4b499f3ba6c9b63154bdf56a09017b91ebf.json b/.sqlx/query-0cd6ae84400d5fbb5964a9cab3b8b4b499f3ba6c9b63154bdf56a09017b91ebf.json new file mode 100644 index 00000000..e73047fe --- /dev/null +++ b/.sqlx/query-0cd6ae84400d5fbb5964a9cab3b8b4b499f3ba6c9b63154bdf56a09017b91ebf.json @@ -0,0 +1,12 @@ +{ + "db_name": "MySQL", + "query": "\n UPDATE quota_flavorquota\n SET flavor_group_id = ?\n WHERE quota_ptr_id = ?\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "0cd6ae84400d5fbb5964a9cab3b8b4b499f3ba6c9b63154bdf56a09017b91ebf" +} diff --git a/.sqlx/query-1d7bce4cb1885d5c62149462f3ee893939d369e1b5d76aff33c46ef89f2b5aa1.json b/.sqlx/query-1d7bce4cb1885d5c62149462f3ee893939d369e1b5d76aff33c46ef89f2b5aa1.json new file mode 100644 index 00000000..bf9ee330 --- /dev/null +++ b/.sqlx/query-1d7bce4cb1885d5c62149462f3ee893939d369e1b5d76aff33c46ef89f2b5aa1.json @@ -0,0 +1,12 @@ +{ + "db_name": "MySQL", + "query": "\n UPDATE quota_quota\n SET\n user_id = ?,\n quota = ?\n WHERE id = ?\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "1d7bce4cb1885d5c62149462f3ee893939d369e1b5d76aff33c46ef89f2b5aa1" +} diff --git a/.sqlx/query-952fb3be5f3ca26ac1a663e4bdd57a5a7bc2203c6930fad5116c88988fecea53.json b/.sqlx/query-952fb3be5f3ca26ac1a663e4bdd57a5a7bc2203c6930fad5116c88988fecea53.json new file mode 100644 index 00000000..7c838559 --- /dev/null +++ b/.sqlx/query-952fb3be5f3ca26ac1a663e4bdd57a5a7bc2203c6930fad5116c88988fecea53.json @@ -0,0 +1,74 @@ +{ + "db_name": "MySQL", + "query": "\n SELECT\n q.id as id,\n u.id as user,\n u.name as username,\n q.quota as quota,\n g.id as flavor_group,\n g.name as flavor_group_name\n FROM\n quota_flavorquota as f,\n quota_quota as q,\n resources_flavorgroup as g,\n user_user as u\n WHERE\n f.quota_ptr_id = q.id AND\n f.flavor_group_id = g.id AND\n q.user_id = u.id AND\n q.id = ?\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": { + "type": "LongLong", + "flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT", + "max_size": 20 + } + }, + { + "ordinal": 1, + "name": "user", + "type_info": { + "type": "Long", + "flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT", + "max_size": 11 + } + }, + { + "ordinal": 2, + "name": "username", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE", + "max_size": 1020 + } + }, + { + "ordinal": 3, + "name": "quota", + "type_info": { + "type": "Long", + "flags": "NOT_NULL | NO_DEFAULT_VALUE", + "max_size": 11 + } + }, + { + "ordinal": 4, + "name": "flavor_group", + "type_info": { + "type": "LongLong", + "flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT", + "max_size": 20 + } + }, + { + "ordinal": 5, + "name": "flavor_group_name", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE", + "max_size": 256 + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false, + false, + false + ] + }, + "hash": "952fb3be5f3ca26ac1a663e4bdd57a5a7bc2203c6930fad5116c88988fecea53" +} From d189c32cf0a1d0a0c2aae7e03392b86d149c7553 Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Wed, 30 Oct 2024 10:53:32 +0100 Subject: [PATCH 40/46] chore(changelog): update unreleased section Signed-off-by: Sandro-Alessio Gierens --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8afbd77e..9ced76f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,10 @@ This is the combined changelog of all contained `lrzcc` crates. - api: add database::budgeting::project/user_budget submodule with helpers - api: add simplified budgeting::project_budget_modify endpoint - api: add simplified budgeting::user_budget_modify endpoint +- wire: derive FromRow for FlavorQuota +- api: add database::quota submodule with helper functions +- api: minor naming fixes in user and pricing modules +- api: add simplified quota::flavor_quota_modify endpoint ## [lrzcc-cli-v1.3.0] - 2024-10-08 From c4941c4b57ecfa7f1d48f3ed8e0a1ddbd4a9de51 Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Wed, 30 Oct 2024 10:54:18 +0100 Subject: [PATCH 41/46] docs(api/routes): update todo list for endpoints Signed-off-by: Sandro-Alessio Gierens --- api/src/routes/mod.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/src/routes/mod.rs b/api/src/routes/mod.rs index a3bc1265..f22188fc 100644 --- a/api/src/routes/mod.rs +++ b/api/src/routes/mod.rs @@ -18,7 +18,6 @@ pub use user::*; // TODO: modify endpoints for // - accounting::server_state -// - quota::flavor_quota // TODO: get endpoints for // - accounting::server_state @@ -41,3 +40,5 @@ pub use user::*; // TODO: improve the following endpoints // - budgeting::project_budget::modify // - budgeting::user_budget::modify +// - pricing::flavor_price::modify +// - quota::flavor_quota::modify From cafd5efed85defb430f74c0481895e14aa88de3a Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Wed, 30 Oct 2024 10:56:11 +0100 Subject: [PATCH 42/46] chore(spellcheck): update dictionary Signed-off-by: Sandro-Alessio Gierens --- .spellcheck.dic | 1 + 1 file changed, 1 insertion(+) diff --git a/.spellcheck.dic b/.spellcheck.dic index 69b43148..9b20b997 100644 --- a/.spellcheck.dic +++ b/.spellcheck.dic @@ -21,6 +21,7 @@ thiserror u64 url user_class +username uuid FlavorGroupCreateData From e864255b9ff973a5dbae716e55654e499a653b65 Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Wed, 30 Oct 2024 12:05:54 +0100 Subject: [PATCH 43/46] feat(api/database): add accounting submodule with select_server_state_from_db Signed-off-by: Sandro-Alessio Gierens --- api/src/database/accounting/mod.rs | 1 + api/src/database/accounting/server_state.rs | 89 +++++++++++++++++++++ api/src/database/mod.rs | 1 + 3 files changed, 91 insertions(+) create mode 100644 api/src/database/accounting/mod.rs create mode 100644 api/src/database/accounting/server_state.rs diff --git a/api/src/database/accounting/mod.rs b/api/src/database/accounting/mod.rs new file mode 100644 index 00000000..052ad2b4 --- /dev/null +++ b/api/src/database/accounting/mod.rs @@ -0,0 +1 @@ +pub mod server_state; diff --git a/api/src/database/accounting/server_state.rs b/api/src/database/accounting/server_state.rs new file mode 100644 index 00000000..2142dc60 --- /dev/null +++ b/api/src/database/accounting/server_state.rs @@ -0,0 +1,89 @@ +use crate::error::{NotFoundOrUnexpectedApiError, UnexpectedOnlyError}; +use anyhow::Context; +use chrono::{DateTime, Utc}; +use lrzcc_wire::accounting::ServerState; +use sqlx::{Executor, FromRow, MySql, Transaction}; + +#[tracing::instrument( + name = "select_maybe_server_state_from_db", + skip(transaction) +)] +pub async fn select_maybe_server_state_from_db( + transaction: &mut Transaction<'_, MySql>, + server_state_id: u64, +) -> Result, UnexpectedOnlyError> { + #[derive(FromRow)] + pub struct Row { + pub id: u32, + pub begin: DateTime, + pub end: Option>, + pub instance_id: String, + pub instance_name: String, + pub flavor: u32, + pub flavor_name: String, + pub status: String, + pub user: u32, + pub username: String, + } + let query = sqlx::query!( + r#" + SELECT + s.id as id, + s.begin as begin, + s.end as end, + ss.instance_id as instance_id, + ss.instance_name as instance_name, + f.id as flavor, + f.name as flavor_name, + ss.status as status, + u.id as user, + u.name as username + FROM + accounting_state as s, + accounting_serverstate as ss, + resources_flavor as f, + user_user as u + WHERE + ss.flavor_id = f.id AND + ss.user_id = u.id AND + ss.state_ptr_id = s.id AND + s.id = ? + "#, + server_state_id + ); + let row = transaction + .fetch_optional(query) + .await + .context("Failed to execute select query")?; + Ok(match row { + Some(row) => { + let row = Row::from_row(&row) + .context("Failed to parse flavor price row")?; + Some(ServerState { + id: row.id, + begin: row.begin.fixed_offset(), + end: row.end.map(|end| end.fixed_offset()), + instance_id: row.instance_id, + instance_name: row.instance_name, + flavor: row.flavor, + flavor_name: row.flavor_name, + status: row.status, + user: row.user, + username: row.username, + }) + } + None => None, + }) +} + +#[tracing::instrument(name = "select_server_state_from_db", skip(transaction))] +pub async fn select_server_state_from_db( + transaction: &mut Transaction<'_, MySql>, + server_state_id: u64, +) -> Result { + select_maybe_server_state_from_db(transaction, server_state_id) + .await? + .ok_or(NotFoundOrUnexpectedApiError::NotFoundError( + "Server state with given ID not found".to_string(), + )) +} diff --git a/api/src/database/mod.rs b/api/src/database/mod.rs index 3cf42a71..b06fd839 100644 --- a/api/src/database/mod.rs +++ b/api/src/database/mod.rs @@ -1,3 +1,4 @@ +pub mod accounting; pub mod budgeting; pub mod pricing; pub mod quota; From 803ca908f4d7a7d9cad99c860a514c361c317db3 Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Wed, 30 Oct 2024 12:31:28 +0100 Subject: [PATCH 44/46] feat(api/routes/accounting): add simplified server_state_modify_endpoint Signed-off-by: Sandro-Alessio Gierens --- api/src/routes/accounting/server_state/mod.rs | 9 +- .../routes/accounting/server_state/modify.rs | 116 ++++++++++++++++++ api/src/routes/mod.rs | 4 +- 3 files changed, 122 insertions(+), 7 deletions(-) create mode 100644 api/src/routes/accounting/server_state/modify.rs diff --git a/api/src/routes/accounting/server_state/mod.rs b/api/src/routes/accounting/server_state/mod.rs index ea837dfb..4717b80b 100644 --- a/api/src/routes/accounting/server_state/mod.rs +++ b/api/src/routes/accounting/server_state/mod.rs @@ -1,6 +1,7 @@ use actix_web::web::{ delete, - // get, patch, + // get, + patch, post, scope, }; @@ -13,8 +14,8 @@ use create::server_state_create; // use list::server_state_list; // mod get; // use get::server_state_get; -// mod modify; -// use modify::server_state_modify; +mod modify; +use modify::server_state_modify; mod delete; use delete::server_state_delete; @@ -24,7 +25,7 @@ pub fn server_states_scope() -> Scope { // .route("", get().to(server_state_list)) // .route("/{server_state_id}", get().to(server_state_get)) // TODO: what about PUT? - // .route("/{server_state_id}/", patch().to(server_state_modify)) + .route("/{server_state_id}/", patch().to(server_state_modify)) .route("/{server_state_id}/", delete().to(server_state_delete)) } diff --git a/api/src/routes/accounting/server_state/modify.rs b/api/src/routes/accounting/server_state/modify.rs new file mode 100644 index 00000000..7346b4f9 --- /dev/null +++ b/api/src/routes/accounting/server_state/modify.rs @@ -0,0 +1,116 @@ +use crate::authorization::require_admin_user; +use crate::database::accounting::server_state::select_server_state_from_db; +use crate::error::{NotFoundOrUnexpectedApiError, OptionApiError}; +use actix_web::web::{Data, Json, Path, ReqData}; +use actix_web::HttpResponse; +use anyhow::Context; +use lrzcc_wire::accounting::{ServerState, ServerStateModifyData}; +use lrzcc_wire::user::{Project, User}; +use sqlx::{Executor, MySql, MySqlPool, Transaction}; + +use super::ServerStateIdParam; + +#[tracing::instrument(name = "server_state_modify")] +pub async fn server_state_modify( + user: ReqData, + // TODO: we don't need this right? + project: ReqData, + db_pool: Data, + data: Json, + params: Path, +) -> Result { + require_admin_user(&user)?; + // TODO: do further validation + if data.id != params.server_state_id { + return Err(OptionApiError::ValidationError( + "ID in URL does not match ID in body".to_string(), + )); + } + let mut transaction = db_pool + .begin() + .await + .context("Failed to begin transaction")?; + let server_state = + update_server_state_in_db(&mut transaction, &data).await?; + transaction + .commit() + .await + .context("Failed to commit transaction")?; + Ok(HttpResponse::Ok() + .content_type("application/json") + .json(server_state)) +} + +#[tracing::instrument( + name = "update_server_state_in_db", + skip(data, transaction) +)] +pub async fn update_server_state_in_db( + transaction: &mut Transaction<'_, MySql>, + data: &ServerStateModifyData, +) -> Result { + let row = select_server_state_from_db(transaction, data.id as u64).await?; + let begin = data.begin.unwrap_or(row.begin); + let mut end = data.end; + if end.is_none() { + end = row.end; + } + let instance_id = data.instance_id.clone().unwrap_or(row.instance_id); + let instance_name = data.instance_name.clone().unwrap_or(row.instance_name); + let status = data.status.clone().unwrap_or(row.status); + let user = data.user.unwrap_or(row.user); + let flavor = data.flavor.unwrap_or(row.flavor); + let query1 = sqlx::query!( + r#" + UPDATE accounting_state + SET + begin = ?, + end = ? + WHERE id = ? + "#, + begin.to_utc(), + end.map(|end| end.to_utc()), + data.id, + ); + transaction + .execute(query1) + .await + .context("Failed to execute update first query")?; + let query2 = sqlx::query!( + r#" + UPDATE accounting_serverstate + SET + instance_id = ?, + instance_name = ?, + flavor_id = ?, + status = ?, + user_id = ? + WHERE state_ptr_id = ? + "#, + instance_id, + instance_name, + flavor, + status, + user, + data.id, + ); + transaction + .execute(query2) + .await + .context("Failed to execute update second query")?; + let price = ServerState { + id: data.id, + begin, + end, + instance_id, + instance_name, + flavor, + // TODO: we need to get the new flavor's name + flavor_name: row.flavor_name, + status, + user, + // TODO: we need to get the new username + username: row.username, + }; + Ok(price) +} diff --git a/api/src/routes/mod.rs b/api/src/routes/mod.rs index f22188fc..f11ec44e 100644 --- a/api/src/routes/mod.rs +++ b/api/src/routes/mod.rs @@ -16,9 +16,6 @@ pub use quota::*; pub use resources::*; pub use user::*; -// TODO: modify endpoints for -// - accounting::server_state - // TODO: get endpoints for // - accounting::server_state // - resources::flavor_group @@ -42,3 +39,4 @@ pub use user::*; // - budgeting::user_budget::modify // - pricing::flavor_price::modify // - quota::flavor_quota::modify +// - accounting::server_state::modify From 4be789c305574602bd5d191f70916febf03f6019 Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Wed, 30 Oct 2024 12:31:54 +0100 Subject: [PATCH 45/46] chore(changelog): update unreleases section Signed-off-by: Sandro-Alessio Gierens --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ced76f8..e4f828d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,8 @@ This is the combined changelog of all contained `lrzcc` crates. - api: add database::quota submodule with helper functions - api: minor naming fixes in user and pricing modules - api: add simplified quota::flavor_quota_modify endpoint +- api: add database::accounting submodule with helper functions +- api: add simplified accounting::server_state_modify endpoint ## [lrzcc-cli-v1.3.0] - 2024-10-08 From 35a14b52e55f816decb017a98f4ca4b0d7ab6fa7 Mon Sep 17 00:00:00 2001 From: Sandro-Alessio Gierens Date: Wed, 30 Oct 2024 12:32:14 +0100 Subject: [PATCH 46/46] chore(sqlx): update offline query data Signed-off-by: Sandro-Alessio Gierens --- ...8d57373bf9af49342c114d696556d9843d652.json | 114 ++++++++++++++++++ ...d4fc1e3b1b559fdc6112d755ce1bab5049838.json | 12 ++ ...6c71b9773e5e821eb96c166b7311d6ad6abd8.json | 12 ++ 3 files changed, 138 insertions(+) create mode 100644 .sqlx/query-12dedd71e6dd2833be919bb45078d57373bf9af49342c114d696556d9843d652.json create mode 100644 .sqlx/query-2d2e83bec46d7a9f7424b738e66d4fc1e3b1b559fdc6112d755ce1bab5049838.json create mode 100644 .sqlx/query-edefd297e2a16cac1f42cf3f1176c71b9773e5e821eb96c166b7311d6ad6abd8.json diff --git a/.sqlx/query-12dedd71e6dd2833be919bb45078d57373bf9af49342c114d696556d9843d652.json b/.sqlx/query-12dedd71e6dd2833be919bb45078d57373bf9af49342c114d696556d9843d652.json new file mode 100644 index 00000000..1c43f4d1 --- /dev/null +++ b/.sqlx/query-12dedd71e6dd2833be919bb45078d57373bf9af49342c114d696556d9843d652.json @@ -0,0 +1,114 @@ +{ + "db_name": "MySQL", + "query": "\n SELECT\n s.id as id,\n s.begin as begin,\n s.end as end,\n ss.instance_id as instance_id,\n ss.instance_name as instance_name,\n f.id as flavor,\n f.name as flavor_name,\n ss.status as status,\n u.id as user,\n u.name as username\n FROM\n accounting_state as s,\n accounting_serverstate as ss,\n resources_flavor as f,\n user_user as u\n WHERE\n ss.flavor_id = f.id AND\n ss.user_id = u.id AND\n ss.state_ptr_id = s.id AND\n s.id = ?\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": { + "type": "Long", + "flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT", + "max_size": 11 + } + }, + { + "ordinal": 1, + "name": "begin", + "type_info": { + "type": "Datetime", + "flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE", + "max_size": 26 + } + }, + { + "ordinal": 2, + "name": "end", + "type_info": { + "type": "Datetime", + "flags": "BINARY", + "max_size": 26 + } + }, + { + "ordinal": 3, + "name": "instance_id", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | NO_DEFAULT_VALUE", + "max_size": 144 + } + }, + { + "ordinal": 4, + "name": "instance_name", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | NO_DEFAULT_VALUE", + "max_size": 1020 + } + }, + { + "ordinal": 5, + "name": "flavor", + "type_info": { + "type": "LongLong", + "flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT", + "max_size": 20 + } + }, + { + "ordinal": 6, + "name": "flavor_name", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE", + "max_size": 256 + } + }, + { + "ordinal": 7, + "name": "status", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | NO_DEFAULT_VALUE", + "max_size": 72 + } + }, + { + "ordinal": 8, + "name": "user", + "type_info": { + "type": "Long", + "flags": "NOT_NULL | PRIMARY_KEY | AUTO_INCREMENT", + "max_size": 11 + } + }, + { + "ordinal": 9, + "name": "username", + "type_info": { + "type": "VarString", + "flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE", + "max_size": 1020 + } + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + true, + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "12dedd71e6dd2833be919bb45078d57373bf9af49342c114d696556d9843d652" +} diff --git a/.sqlx/query-2d2e83bec46d7a9f7424b738e66d4fc1e3b1b559fdc6112d755ce1bab5049838.json b/.sqlx/query-2d2e83bec46d7a9f7424b738e66d4fc1e3b1b559fdc6112d755ce1bab5049838.json new file mode 100644 index 00000000..3d638c38 --- /dev/null +++ b/.sqlx/query-2d2e83bec46d7a9f7424b738e66d4fc1e3b1b559fdc6112d755ce1bab5049838.json @@ -0,0 +1,12 @@ +{ + "db_name": "MySQL", + "query": "\n UPDATE accounting_serverstate\n SET\n instance_id = ?,\n instance_name = ?,\n flavor_id = ?,\n status = ?,\n user_id = ?\n WHERE state_ptr_id = ?\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 6 + }, + "nullable": [] + }, + "hash": "2d2e83bec46d7a9f7424b738e66d4fc1e3b1b559fdc6112d755ce1bab5049838" +} diff --git a/.sqlx/query-edefd297e2a16cac1f42cf3f1176c71b9773e5e821eb96c166b7311d6ad6abd8.json b/.sqlx/query-edefd297e2a16cac1f42cf3f1176c71b9773e5e821eb96c166b7311d6ad6abd8.json new file mode 100644 index 00000000..82264300 --- /dev/null +++ b/.sqlx/query-edefd297e2a16cac1f42cf3f1176c71b9773e5e821eb96c166b7311d6ad6abd8.json @@ -0,0 +1,12 @@ +{ + "db_name": "MySQL", + "query": "\n UPDATE accounting_state\n SET\n begin = ?,\n end = ?\n WHERE id = ?\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "edefd297e2a16cac1f42cf3f1176c71b9773e5e821eb96c166b7311d6ad6abd8" +}