From ba073bf8b2d38b34c2f4d0ebcc7fe5a0ba3b36f4 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Tue, 7 May 2024 03:40:30 -0700 Subject: [PATCH] minimize update diffs --- bin/core/src/helpers/resource.rs | 737 ++++++++++++++++++ bin/core/src/resource/alerter.rs | 17 +- bin/core/src/resource/build.rs | 2 +- bin/core/src/resource/builder.rs | 14 +- bin/core/src/resource/deployment.rs | 2 +- bin/core/src/resource/mod.rs | 38 +- bin/core/src/resource/procedure.rs | 4 +- bin/core/src/resource/repo.rs | 2 +- bin/core/src/resource/server.rs | 2 +- bin/core/src/resource/server_template.rs | 21 +- client/core/rs/src/entities/alerter.rs | 10 +- client/core/rs/src/entities/builder.rs | 10 +- client/core/rs/src/entities/mod.rs | 5 + .../core/rs/src/entities/server_template.rs | 6 +- 14 files changed, 833 insertions(+), 37 deletions(-) create mode 100644 bin/core/src/helpers/resource.rs diff --git a/bin/core/src/helpers/resource.rs b/bin/core/src/helpers/resource.rs new file mode 100644 index 000000000..4f5a12df7 --- /dev/null +++ b/bin/core/src/helpers/resource.rs @@ -0,0 +1,737 @@ +use std::{collections::HashSet, str::FromStr}; + +use anyhow::{anyhow, Context}; +use futures::future::join_all; +use monitor_client::{ + api::write::CreateTag, + entities::{ + alerter::{ + Alerter, AlerterConfig, AlerterConfigVariant, AlerterInfo, + AlerterListItem, AlerterListItemInfo, AlerterQuerySpecifics, + }, + build::{ + Build, BuildConfig, BuildInfo, BuildListItem, + BuildListItemInfo, BuildQuerySpecifics, + }, + builder::{ + Builder, BuilderConfig, BuilderConfigVariant, BuilderListItem, + BuilderListItemInfo, BuilderQuerySpecifics, + }, + deployment::{ + Deployment, DeploymentConfig, DeploymentImage, + DeploymentListItem, DeploymentListItemInfo, + DeploymentQuerySpecifics, + }, + permission::PermissionLevel, + procedure::{ + Procedure, ProcedureConfig, ProcedureListItem, + ProcedureListItemInfo, ProcedureQuerySpecifics, + }, + repo::{ + Repo, RepoConfig, RepoInfo, RepoListItem, RepoListItemInfo, + RepoQuerySpecifics, + }, + resource::{AddFilters, Resource, ResourceQuery}, + server::{ + Server, ServerConfig, ServerListItem, ServerListItemInfo, + ServerQuerySpecifics, + }, + server_template::{ + ServerTemplate, ServerTemplateConfig, + ServerTemplateConfigVariant, ServerTemplateListItem, + ServerTemplateListItemInfo, ServerTemplateQuerySpecifics, + }, + update::{ResourceTarget, ResourceTargetVariant}, + user::User, + }, +}; +use mungos::{ + find::find_collect, + mongodb::{ + bson::{doc, oid::ObjectId, Document}, + Collection, + }, +}; +use resolver_api::Resolve; +use serde::{de::DeserializeOwned, Serialize}; + +use crate::{ + helpers::query::user_target_query, + state::State, + state::{db_client, deployment_status_cache, server_status_cache}, +}; + +use super::query::get_tag; + +pub trait StateResource { + type ListItem: Serialize + Send; + type Config: Send + + Sync + + Unpin + + Serialize + + DeserializeOwned + + 'static; + type Info: Send + + Sync + + Unpin + + Default + + Serialize + + DeserializeOwned + + 'static; + type QuerySpecifics: AddFilters + Default + std::fmt::Debug; + + fn name() -> &'static str; + + fn resource_target_variant() -> ResourceTargetVariant; + + async fn coll( + ) -> &'static Collection>; + + async fn to_list_item( + resource: Resource, + ) -> anyhow::Result; + + async fn get_resource( + id_or_name: &str, + ) -> anyhow::Result> { + let filter = match ObjectId::from_str(id_or_name) { + Ok(id) => doc! { "_id": id }, + Err(_) => doc! { "name": id_or_name }, + }; + Self::coll() + .await + .find_one(filter, None) + .await + .context("failed to query db for resource")? + .with_context(|| { + format!( + "did not find any {} matching {id_or_name}", + Self::name() + ) + }) + } + + async fn get_resource_check_permissions( + id_or_name: &str, + user: &User, + permission_level: PermissionLevel, + ) -> anyhow::Result> { + let resource = Self::get_resource(id_or_name).await?; + if user.admin { + return Ok(resource); + } + let permissions = + Self::get_user_permission_on_resource(&user.id, &resource.id) + .await?; + if permissions >= permission_level { + Ok(resource) + } else { + Err(anyhow!( + "user does not have required permissions on this {}", + Self::name() + )) + } + } + + async fn get_user_permission_on_resource( + user_id: &str, + resource_id: &str, + ) -> anyhow::Result { + get_user_permission_on_resource( + user_id, + Self::resource_target_variant(), + resource_id, + ) + .await + } + + async fn get_resource_ids_for_non_admin( + user_id: &str, + ) -> anyhow::Result> { + get_resource_ids_for_non_admin( + user_id, + Self::resource_target_variant(), + ) + .await + } + + async fn list_resource_list_items_for_user( + mut query: ResourceQuery, + user: &User, + ) -> anyhow::Result> { + validate_resource_query_tags(&mut query).await; + let mut filters = Document::new(); + query.add_filters(&mut filters); + Self::query_resource_list_items_for_user(filters, user).await + } + + async fn query_resource_list_items_for_user( + filters: Document, + user: &User, + ) -> anyhow::Result> { + let list = Self::query_resources_for_user(filters, user) + .await? + .into_iter() + .map(|resource| Self::to_list_item(resource)); + + let list = join_all(list) + .await + .into_iter() + .collect::>>() + .context(format!( + "failed to convert {} list item", + Self::name() + ))?; + + Ok(list) + } + + async fn list_resources_for_user( + mut query: ResourceQuery, + user: &User, + ) -> anyhow::Result>> { + validate_resource_query_tags(&mut query).await; + let mut filters = Document::new(); + query.add_filters(&mut filters); + Self::query_resources_for_user(filters, user).await + } + + async fn query_resources_for_user( + mut filters: Document, + user: &User, + ) -> anyhow::Result>> { + if !user.admin { + let ids = Self::get_resource_ids_for_non_admin(&user.id) + .await? + .into_iter() + .flat_map(|id| ObjectId::from_str(&id)) + .collect::>(); + filters.insert("_id", doc! { "$in": ids }); + } + find_collect(Self::coll().await, filters, None) + .await + .with_context(|| { + format!("failed to pull {}s from mongo", Self::name()) + }) + } + + async fn update_description( + id_or_name: &str, + description: &str, + user: &User, + ) -> anyhow::Result<()> { + Self::get_resource_check_permissions( + id_or_name, + user, + PermissionLevel::Write, + ) + .await?; + let filter = match ObjectId::from_str(id_or_name) { + Ok(id) => doc! { "_id": id }, + Err(_) => doc! { "name": id_or_name }, + }; + Self::coll() + .await + .update_one( + filter, + doc! { "$set": { "description": description } }, + None, + ) + .await?; + Ok(()) + } + + async fn update_tags_on_resource( + id_or_name: &str, + tags: Vec, + user: User, + ) -> anyhow::Result<()> { + let futures = tags.iter().map(|tag| async { + match get_tag(tag).await { + Ok(tag) => Ok(tag.id), + Err(_) => State + .resolve( + CreateTag { + name: tag.to_string(), + }, + user.clone(), + ) + .await + .map(|tag| tag.id), + } + }); + let tags = join_all(futures) + .await + .into_iter() + .flatten() + .collect::>(); + Self::coll() + .await + .update_one( + id_or_name_filter(id_or_name), + doc! { "$set": { "tags": tags } }, + None, + ) + .await?; + Ok(()) + } + + async fn remove_tag_from_resources( + tag_id: &str, + ) -> anyhow::Result<()> { + Self::coll() + .await + .update_many( + doc! {}, + doc! { "$pull": { "tags": tag_id } }, + None, + ) + .await + .context("failed to remove tag from resources")?; + Ok(()) + } +} + +fn id_or_name_filter(id_or_name: &str) -> Document { + match ObjectId::from_str(id_or_name) { + Ok(id) => doc! { "_id": id }, + Err(_) => doc! { "name": id_or_name }, + } +} + +impl StateResource for Server { + type ListItem = ServerListItem; + type Config = ServerConfig; + type Info = (); + type QuerySpecifics = ServerQuerySpecifics; + + fn name() -> &'static str { + "server" + } + + fn resource_target_variant() -> ResourceTargetVariant { + ResourceTargetVariant::Server + } + + async fn coll() -> &'static Collection { + &db_client().await.servers + } + + async fn to_list_item( + server: Server, + ) -> anyhow::Result { + let status = server_status_cache().get(&server.id).await; + Ok(ServerListItem { + name: server.name, + created_at: ObjectId::from_str(&server.id)? + .timestamp() + .timestamp_millis(), + id: server.id, + tags: server.tags, + resource_type: ResourceTargetVariant::Server, + info: ServerListItemInfo { + status: status.map(|s| s.status).unwrap_or_default(), + region: server.config.region, + send_unreachable_alerts: server + .config + .send_unreachable_alerts, + send_cpu_alerts: server.config.send_cpu_alerts, + send_mem_alerts: server.config.send_mem_alerts, + send_disk_alerts: server.config.send_disk_alerts, + }, + }) + } +} + +impl StateResource for Deployment { + type ListItem = DeploymentListItem; + type Config = DeploymentConfig; + type Info = (); + type QuerySpecifics = DeploymentQuerySpecifics; + + fn name() -> &'static str { + "deployment" + } + + fn resource_target_variant() -> ResourceTargetVariant { + ResourceTargetVariant::Deployment + } + + async fn coll() -> &'static Collection { + &db_client().await.deployments + } + + async fn to_list_item( + deployment: Deployment, + ) -> anyhow::Result { + let status = deployment_status_cache().get(&deployment.id).await; + let (image, build_id) = match deployment.config.image { + DeploymentImage::Build { build_id, version } => { + let build = Build::get_resource(&build_id).await?; + let version = if version.is_none() { + build.config.version.to_string() + } else { + version.to_string() + }; + (format!("{}:{version}", build.name), Some(build_id)) + } + DeploymentImage::Image { image } => (image, None), + }; + Ok(DeploymentListItem { + name: deployment.name, + created_at: ObjectId::from_str(&deployment.id)? + .timestamp() + .timestamp_millis(), + id: deployment.id, + tags: deployment.tags, + resource_type: ResourceTargetVariant::Deployment, + info: DeploymentListItemInfo { + state: status + .as_ref() + .map(|s| s.curr.state) + .unwrap_or_default(), + status: status.as_ref().and_then(|s| { + s.curr.container.as_ref().and_then(|c| c.status.to_owned()) + }), + image, + server_id: deployment.config.server_id, + build_id, + }, + }) + } +} + +impl StateResource for Build { + type ListItem = BuildListItem; + type Config = BuildConfig; + type Info = BuildInfo; + type QuerySpecifics = BuildQuerySpecifics; + + fn name() -> &'static str { + "build" + } + + fn resource_target_variant() -> ResourceTargetVariant { + ResourceTargetVariant::Build + } + + async fn coll() -> &'static Collection { + &db_client().await.builds + } + + async fn to_list_item( + build: Build, + ) -> anyhow::Result { + Ok(BuildListItem { + name: build.name, + created_at: ObjectId::from_str(&build.id)? + .timestamp() + .timestamp_millis(), + id: build.id, + tags: build.tags, + resource_type: ResourceTargetVariant::Build, + info: BuildListItemInfo { + last_built_at: build.info.last_built_at, + version: build.config.version, + repo: build.config.repo, + branch: build.config.branch, + }, + }) + } +} + +impl StateResource for Repo { + type ListItem = RepoListItem; + type Config = RepoConfig; + type Info = RepoInfo; + type QuerySpecifics = RepoQuerySpecifics; + + fn name() -> &'static str { + "repo" + } + + fn resource_target_variant() -> ResourceTargetVariant { + ResourceTargetVariant::Repo + } + + async fn coll() -> &'static Collection { + &db_client().await.repos + } + + async fn to_list_item(repo: Repo) -> anyhow::Result { + Ok(RepoListItem { + name: repo.name, + created_at: ObjectId::from_str(&repo.id)? + .timestamp() + .timestamp_millis(), + id: repo.id, + tags: repo.tags, + resource_type: ResourceTargetVariant::Repo, + info: RepoListItemInfo { + last_pulled_at: repo.info.last_pulled_at, + repo: repo.config.repo, + branch: repo.config.branch, + }, + }) + } +} + +impl StateResource for Builder { + type ListItem = BuilderListItem; + type Config = BuilderConfig; + type Info = (); + type QuerySpecifics = BuilderQuerySpecifics; + + fn name() -> &'static str { + "builder" + } + + fn resource_target_variant() -> ResourceTargetVariant { + ResourceTargetVariant::Builder + } + + async fn coll() -> &'static Collection { + &db_client().await.builders + } + + async fn to_list_item( + builder: Builder, + ) -> anyhow::Result { + let (builder_type, instance_type) = match builder.config { + BuilderConfig::Server(config) => ( + BuilderConfigVariant::Server.to_string(), + Some(config.server_id), + ), + BuilderConfig::Aws(config) => ( + BuilderConfigVariant::Aws.to_string(), + Some(config.instance_type), + ), + }; + + Ok(BuilderListItem { + name: builder.name, + created_at: ObjectId::from_str(&builder.id)? + .timestamp() + .timestamp_millis(), + id: builder.id, + tags: builder.tags, + resource_type: ResourceTargetVariant::Builder, + info: BuilderListItemInfo { + builder_type, + instance_type, + }, + }) + } +} + +impl StateResource for Alerter { + type ListItem = AlerterListItem; + type Config = AlerterConfig; + type Info = AlerterInfo; + type QuerySpecifics = AlerterQuerySpecifics; + + fn name() -> &'static str { + "alerter" + } + + fn resource_target_variant() -> ResourceTargetVariant { + ResourceTargetVariant::Alerter + } + + async fn coll() -> &'static Collection { + &db_client().await.alerters + } + + async fn to_list_item( + alerter: Alerter, + ) -> anyhow::Result { + let (alerter_type, enabled) = match alerter.config { + AlerterConfig::Custom(config) => { + (AlerterConfigVariant::Custom.to_string(), config.enabled) + } + AlerterConfig::Slack(config) => { + (AlerterConfigVariant::Slack.to_string(), config.enabled) + } + }; + Ok(AlerterListItem { + name: alerter.name, + created_at: ObjectId::from_str(&alerter.id)? + .timestamp() + .timestamp_millis(), + id: alerter.id, + tags: alerter.tags, + resource_type: ResourceTargetVariant::Alerter, + info: AlerterListItemInfo { + alerter_type: alerter_type.to_string(), + is_default: alerter.info.is_default, + enabled, + }, + }) + } +} + +impl StateResource for Procedure { + type ListItem = ProcedureListItem; + type Config = ProcedureConfig; + type Info = (); + type QuerySpecifics = ProcedureQuerySpecifics; + + fn name() -> &'static str { + "procedure" + } + + fn resource_target_variant() -> ResourceTargetVariant { + ResourceTargetVariant::Procedure + } + + async fn coll() -> &'static Collection { + &db_client().await.procedures + } + + async fn to_list_item( + procedure: Procedure, + ) -> anyhow::Result { + Ok(ProcedureListItem { + name: procedure.name, + created_at: ObjectId::from_str(&procedure.id)? + .timestamp() + .timestamp_millis(), + id: procedure.id, + tags: procedure.tags, + resource_type: ResourceTargetVariant::Procedure, + info: ProcedureListItemInfo { + procedure_type: procedure.config.procedure_type, + }, + }) + } +} + +impl StateResource for ServerTemplate { + type ListItem = ServerTemplateListItem; + type Config = ServerTemplateConfig; + type Info = (); + type QuerySpecifics = ServerTemplateQuerySpecifics; + + fn name() -> &'static str { + "server_template" + } + + fn resource_target_variant() -> ResourceTargetVariant { + ResourceTargetVariant::Alerter + } + + async fn coll() -> &'static Collection { + &db_client().await.server_templates + } + + async fn to_list_item( + server_template: ServerTemplate, + ) -> anyhow::Result { + let (template_type, instance_type) = match server_template.config + { + ServerTemplateConfig::Aws(config) => ( + ServerTemplateConfigVariant::Aws.to_string(), + Some(config.instance_type), + ), + }; + Ok(ServerTemplateListItem { + name: server_template.name, + created_at: ObjectId::from_str(&server_template.id)? + .timestamp() + .timestamp_millis(), + id: server_template.id, + tags: server_template.tags, + resource_type: ResourceTargetVariant::ServerTemplate, + info: ServerTemplateListItemInfo { + provider: template_type.to_string(), + instance_type, + }, + }) + } +} + +#[instrument(level = "debug")] +pub async fn get_user_permission_on_resource( + user_id: &str, + resource_variant: ResourceTargetVariant, + resource_id: &str, +) -> anyhow::Result { + let permission = find_collect( + &db_client().await.permissions, + doc! { + "$or": user_target_query(user_id).await?, + "resource_target.type": resource_variant.as_ref(), + "resource_target.id": resource_id + }, + None, + ) + .await + .context("failed to query db for permissions")? + .into_iter() + // get the max permission user has between personal / any user groups + .fold(PermissionLevel::None, |level, permission| { + if permission.level > level { + permission.level + } else { + level + } + }); + Ok(permission) +} + +#[instrument] +pub async fn delete_all_permissions_on_resource(target: T) +where + T: Into + std::fmt::Debug, +{ + let target: ResourceTarget = target.into(); + let (variant, id) = target.extract_variant_id(); + if let Err(e) = db_client() + .await + .permissions + .delete_many( + doc! { + "resource_target.type": variant.as_ref(), + "resource_target.id": &id + }, + None, + ) + .await + { + warn!("failed to delete_many permissions matching target {target:?} | {e:#}"); + } +} + +#[instrument(level = "debug")] +pub async fn get_resource_ids_for_non_admin( + user_id: &str, + resource_type: ResourceTargetVariant, +) -> anyhow::Result> { + let permissions = find_collect( + &db_client().await.permissions, + doc! { + "$or": user_target_query(user_id).await?, + "resource_target.type": resource_type.as_ref(), + "level": { "$in": ["Read", "Execute", "Write"] } + }, + None, + ) + .await + .context("failed to query permissions on db")? + .into_iter() + .map(|p| p.resource_target.extract_variant_id().1.to_string()) + // collect into hashset first to remove any duplicates + .collect::>(); + Ok(permissions.into_iter().collect()) +} + +#[instrument(level = "debug")] +pub async fn validate_resource_query_tags< + T: Default + std::fmt::Debug, +>( + query: &mut ResourceQuery, +) { + let futures = query.tags.iter().map(|tag| get_tag(tag)); + let res = join_all(futures).await; + query.tags = res.into_iter().flatten().map(|tag| tag.id).collect(); +} diff --git a/bin/core/src/resource/alerter.rs b/bin/core/src/resource/alerter.rs index 684fa9b89..f8c033781 100644 --- a/bin/core/src/resource/alerter.rs +++ b/bin/core/src/resource/alerter.rs @@ -9,9 +9,12 @@ use monitor_client::entities::{ resource::Resource, update::{ResourceTargetVariant, Update}, user::User, - Operation, + MergePartial, Operation, +}; +use mungos::mongodb::{ + bson::{oid::ObjectId, to_document, Document}, + Collection, }; -use mungos::mongodb::{bson::oid::ObjectId, Collection}; use crate::state::db_client; @@ -103,13 +106,21 @@ impl super::MonitorResource for Alerter { } async fn validate_update_config( - _original: Resource, + _id: &str, _config: &mut Self::PartialConfig, _user: &User, ) -> anyhow::Result<()> { Ok(()) } + fn update_document( + original: Resource, + config: Self::PartialConfig, + ) -> Result { + let config = original.config.merge_partial(config); + to_document(&config) + } + async fn post_update( _updated: &Self, _update: &mut Update, diff --git a/bin/core/src/resource/build.rs b/bin/core/src/resource/build.rs index a685c3f86..341b73a24 100644 --- a/bin/core/src/resource/build.rs +++ b/bin/core/src/resource/build.rs @@ -96,7 +96,7 @@ impl super::MonitorResource for Build { } async fn validate_update_config( - _original: Resource, + _id: &str, config: &mut Self::PartialConfig, user: &User, ) -> anyhow::Result<()> { diff --git a/bin/core/src/resource/builder.rs b/bin/core/src/resource/builder.rs index b2dc3f4bb..2d04fd636 100644 --- a/bin/core/src/resource/builder.rs +++ b/bin/core/src/resource/builder.rs @@ -12,10 +12,10 @@ use monitor_client::entities::{ server::Server, update::{ResourceTargetVariant, Update}, user::User, - Operation, + MergePartial, Operation, }; use mungos::mongodb::{ - bson::{doc, oid::ObjectId}, + bson::{doc, oid::ObjectId, to_document, Document}, Collection, }; @@ -101,13 +101,21 @@ impl super::MonitorResource for Builder { } async fn validate_update_config( - _original: Resource, + _id: &str, config: &mut Self::PartialConfig, user: &User, ) -> anyhow::Result<()> { validate_config(config, user).await } + fn update_document( + original: Resource, + config: Self::PartialConfig, + ) -> Result { + let config = original.config.merge_partial(config); + to_document(&config) + } + async fn post_update( _updated: &Self, _update: &mut Update, diff --git a/bin/core/src/resource/deployment.rs b/bin/core/src/resource/deployment.rs index a69591853..b79ff97c4 100644 --- a/bin/core/src/resource/deployment.rs +++ b/bin/core/src/resource/deployment.rs @@ -124,7 +124,7 @@ impl super::MonitorResource for Deployment { } async fn validate_update_config( - _original: Resource, + _id: &str, config: &mut Self::PartialConfig, user: &User, ) -> anyhow::Result<()> { diff --git a/bin/core/src/resource/mod.rs b/bin/core/src/resource/mod.rs index 44e518af2..186519012 100644 --- a/bin/core/src/resource/mod.rs +++ b/bin/core/src/resource/mod.rs @@ -22,6 +22,7 @@ use mungos::{ Collection, }, }; +use partial_derive2::{MaybeNone, PartialDiff}; use resolver_api::Resolve; use serde::{de::DeserializeOwned, Serialize}; use serror::serialize_error_pretty; @@ -55,8 +56,9 @@ pub trait MonitorResource { + Unpin + Serialize + DeserializeOwned + + PartialDiff + 'static; - type PartialConfig: Serialize + Into; + type PartialConfig: Into + Serialize + MaybeNone; type Info: Send + Sync + Unpin @@ -107,11 +109,19 @@ pub trait MonitorResource { fn update_operation() -> Operation; async fn validate_update_config( - current: Resource, + id: &str, config: &mut Self::PartialConfig, user: &User, ) -> anyhow::Result<()>; + /// Should be overridden for enum configs, eg Alerter, Builder, ... + fn update_document( + _original: Resource, + config: Self::PartialConfig, + ) -> Result { + to_document(&config) + } + /// Run any required task after resource updated in database but /// before the request resolves. async fn post_update( @@ -339,11 +349,23 @@ pub async fn update( return Err(anyhow!("{} busy", T::resource_type())); } + T::validate_update_config(&resource.id, &mut config, user).await?; + + // This minimizes the update against the existing config + let config = resource.config.partial_diff(config); + + if config.is_none() { + return Err(anyhow!( + "Partial update has no changes to database state" + )); + } + + let config_for_log = serde_json::to_string_pretty(&config) + .context("failed to serialize config to json")?; + let id = resource.id.clone(); - T::validate_update_config(resource, &mut config, user).await?; - - let config_doc = to_document(&config) + let config_doc = T::update_document(resource, config) .context("failed to serialize config to bson document")?; update_one_by_id( @@ -361,11 +383,7 @@ pub async fn update( user, ); - update.push_simple_log( - "update config", - serde_json::to_string_pretty(&config) - .context("failed to serialize config to json")?, - ); + update.push_simple_log("update config", config_for_log); let updated = get::(id_or_name).await?; diff --git a/bin/core/src/resource/procedure.rs b/bin/core/src/resource/procedure.rs index 06ec710e6..08721ea6b 100644 --- a/bin/core/src/resource/procedure.rs +++ b/bin/core/src/resource/procedure.rs @@ -97,11 +97,11 @@ impl super::MonitorResource for Procedure { } async fn validate_update_config( - original: Resource, + id: &str, config: &mut Self::PartialConfig, user: &User, ) -> anyhow::Result<()> { - validate_config(config, user, Some(&original.id)).await + validate_config(config, user, Some(id)).await } async fn post_update( diff --git a/bin/core/src/resource/repo.rs b/bin/core/src/resource/repo.rs index 966663411..f9bd638c7 100644 --- a/bin/core/src/resource/repo.rs +++ b/bin/core/src/resource/repo.rs @@ -99,7 +99,7 @@ impl super::MonitorResource for Repo { } async fn validate_update_config( - _original: Resource, + _id: &str, config: &mut Self::PartialConfig, user: &User, ) -> anyhow::Result<()> { diff --git a/bin/core/src/resource/server.rs b/bin/core/src/resource/server.rs index c0939baef..5de3cd452 100644 --- a/bin/core/src/resource/server.rs +++ b/bin/core/src/resource/server.rs @@ -104,7 +104,7 @@ impl super::MonitorResource for Server { } async fn validate_update_config( - _original: Server, + _id: &str, _config: &mut Self::PartialConfig, _user: &User, ) -> anyhow::Result<()> { diff --git a/bin/core/src/resource/server_template.rs b/bin/core/src/resource/server_template.rs index d12210092..95a8693e7 100644 --- a/bin/core/src/resource/server_template.rs +++ b/bin/core/src/resource/server_template.rs @@ -10,9 +10,12 @@ use monitor_client::entities::{ }, update::{ResourceTargetVariant, Update}, user::User, - Operation, + MergePartial, Operation, +}; +use mungos::mongodb::{ + bson::{oid::ObjectId, to_document, Document}, + Collection, }; -use mungos::mongodb::{bson::oid::ObjectId, Collection}; use crate::state::db_client; @@ -73,7 +76,7 @@ impl super::MonitorResource for ServerTemplate { async fn validate_create_config( _config: &mut Self::PartialConfig, - _user: &User, + _user: &User, ) -> anyhow::Result<()> { Ok(()) } @@ -92,13 +95,21 @@ impl super::MonitorResource for ServerTemplate { } async fn validate_update_config( - _original: Resource, + _id: &str, _config: &mut Self::PartialConfig, - _user: &User, + _user: &User, ) -> anyhow::Result<()> { Ok(()) } + fn update_document( + original: Resource, + config: Self::PartialConfig, + ) -> Result { + let config = original.config.merge_partial(config); + to_document(&config) + } + async fn post_update( _updated: &Self, _update: &mut Update, diff --git a/client/core/rs/src/entities/alerter.rs b/client/core/rs/src/entities/alerter.rs index dc5cb1544..1d41857e1 100644 --- a/client/core/rs/src/entities/alerter.rs +++ b/client/core/rs/src/entities/alerter.rs @@ -7,8 +7,9 @@ use serde::{Deserialize, Serialize}; use strum::{AsRefStr, Display, EnumString}; use typeshare::typeshare; -use super::resource::{ - AddFilters, Resource, ResourceListItem, ResourceQuery, +use super::{ + resource::{AddFilters, Resource, ResourceListItem, ResourceQuery}, + MergePartial, }; #[typeshare] @@ -131,8 +132,9 @@ impl From for PartialAlerterConfig { } } -impl AlerterConfig { - pub fn merge_partial( +impl MergePartial for AlerterConfig { + type Partial = PartialAlerterConfig; + fn merge_partial( self, partial: PartialAlerterConfig, ) -> AlerterConfig { diff --git a/client/core/rs/src/entities/builder.rs b/client/core/rs/src/entities/builder.rs index 0cf040d82..1391cbffc 100644 --- a/client/core/rs/src/entities/builder.rs +++ b/client/core/rs/src/entities/builder.rs @@ -5,8 +5,9 @@ use serde::{Deserialize, Serialize}; use strum::{Display, EnumString}; use typeshare::typeshare; -use super::resource::{ - AddFilters, Resource, ResourceListItem, ResourceQuery, +use super::{ + resource::{AddFilters, Resource, ResourceListItem, ResourceQuery}, + MergePartial, }; #[typeshare] @@ -120,8 +121,9 @@ impl From for PartialBuilderConfig { } } -impl BuilderConfig { - pub fn merge_partial( +impl MergePartial for BuilderConfig { + type Partial = PartialBuilderConfig; + fn merge_partial( self, partial: PartialBuilderConfig, ) -> BuilderConfig { diff --git a/client/core/rs/src/entities/mod.rs b/client/core/rs/src/entities/mod.rs index 9e049e7d8..0f89ab5a8 100644 --- a/client/core/rs/src/entities/mod.rs +++ b/client/core/rs/src/entities/mod.rs @@ -43,6 +43,11 @@ pub type _Serror = Serror; )] pub struct NoData {} +pub trait MergePartial: Sized { + type Partial; + fn merge_partial(self, partial: Self::Partial) -> Self; +} + pub fn all_logs_success(logs: &[update::Log]) -> bool { for log in logs { if !log.success { diff --git a/client/core/rs/src/entities/server_template.rs b/client/core/rs/src/entities/server_template.rs index 5bf0996e5..8b0fc6649 100644 --- a/client/core/rs/src/entities/server_template.rs +++ b/client/core/rs/src/entities/server_template.rs @@ -10,6 +10,7 @@ use typeshare::typeshare; use super::{ builder::AwsBuilderConfig, resource::{AddFilters, Resource, ResourceListItem, ResourceQuery}, + MergePartial, }; #[typeshare] @@ -112,8 +113,9 @@ impl From for PartialServerTemplateConfig { } } -impl ServerTemplateConfig { - pub fn merge_partial( +impl MergePartial for ServerTemplateConfig { + type Partial = PartialServerTemplateConfig; + fn merge_partial( self, partial: PartialServerTemplateConfig, ) -> ServerTemplateConfig {