From 45eafd10b9eed3898bed2ba7a83a05a91319afc9 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Fri, 7 Jun 2024 19:00:03 -0700 Subject: [PATCH] finish sync backend? --- bin/cli/src/sync/user_group.rs | 3 +- bin/core/src/api/execute/sync.rs | 110 +++- bin/core/src/api/write/sync.rs | 15 + bin/core/src/helpers/sync/mod.rs | 4 +- bin/core/src/helpers/sync/resource.rs | 86 +--- bin/core/src/helpers/sync/user_groups.rs | 630 +++++++++++++++++++++++ bin/core/src/helpers/sync/variables.rs | 301 +++++++++++ client/core/rs/src/entities/sync.rs | 4 + 8 files changed, 1073 insertions(+), 80 deletions(-) create mode 100644 bin/core/src/helpers/sync/user_groups.rs create mode 100644 bin/core/src/helpers/sync/variables.rs diff --git a/bin/cli/src/sync/user_group.rs b/bin/cli/src/sync/user_group.rs index f14e29f54..a5f900b65 100644 --- a/bin/cli/src/sync/user_group.rs +++ b/bin/cli/src/sync/user_group.rs @@ -238,6 +238,7 @@ pub async fn get_updates( "adding".dimmed() )) } + println!("{}", lines.join("\n-------------------\n")); to_update.push(UpdateItem { user_group, update_users, @@ -248,7 +249,7 @@ pub async fn get_updates( for d in &to_delete { println!( - "\n{}: variable: '{}'\n-------------------", + "\n{}: user group: '{}'\n-------------------", "DELETE".red(), d.name.bold(), ); diff --git a/bin/core/src/api/execute/sync.rs b/bin/core/src/api/execute/sync.rs index 88744599b..efdd2d634 100644 --- a/bin/core/src/api/execute/sync.rs +++ b/bin/core/src/api/execute/sync.rs @@ -1,7 +1,7 @@ use anyhow::Context; use mongo_indexed::doc; use monitor_client::{ - api::execute::RunSync, + api::{execute::RunSync, write::RefreshResourceSyncPending}, entities::{ self, alerter::Alerter, @@ -15,17 +15,21 @@ use monitor_client::{ server::Server, server_template::ServerTemplate, update::{Log, Update}, - user::User, + user::{sync_user, User}, }, }; use mungos::by_id::update_one_by_id; use resolver_api::Resolve; +use serror::serialize_error_pretty; use crate::{ helpers::{ query::get_id_to_tags, - sync::resource::{ - get_updates_for_execution, AllResourcesById, ResourceSync, + sync::{ + colored, + resource::{ + get_updates_for_execution, AllResourcesById, ResourceSync, + }, }, update::update_update, }, @@ -130,7 +134,6 @@ impl Resolve for State { &id_to_tags, ) .await?; - let ( resource_syncs_to_create, resource_syncs_to_update, @@ -142,10 +145,90 @@ impl Resolve for State { &id_to_tags, ) .await?; + let ( + variables_to_create, + variables_to_update, + variables_to_delete, + ) = crate::helpers::sync::variables::get_updates_for_execution( + resources.variables, + sync.config.delete, + ) + .await?; + let ( + user_groups_to_create, + user_groups_to_update, + user_groups_to_delete, + ) = crate::helpers::sync::user_groups::get_updates_for_execution( + resources.user_groups, + sync.config.delete, + &all_resources, + ) + .await?; + + if resource_syncs_to_create.is_empty() + && resource_syncs_to_update.is_empty() + && resource_syncs_to_delete.is_empty() + && server_templates_to_create.is_empty() + && server_templates_to_update.is_empty() + && server_templates_to_delete.is_empty() + && servers_to_create.is_empty() + && servers_to_update.is_empty() + && servers_to_delete.is_empty() + && deployments_to_create.is_empty() + && deployments_to_update.is_empty() + && deployments_to_delete.is_empty() + && builds_to_create.is_empty() + && builds_to_update.is_empty() + && builds_to_delete.is_empty() + && builders_to_create.is_empty() + && builders_to_update.is_empty() + && builders_to_delete.is_empty() + && alerters_to_create.is_empty() + && alerters_to_update.is_empty() + && alerters_to_delete.is_empty() + && repos_to_create.is_empty() + && repos_to_update.is_empty() + && repos_to_delete.is_empty() + && procedures_to_create.is_empty() + && procedures_to_update.is_empty() + && procedures_to_delete.is_empty() + && user_groups_to_create.is_empty() + && user_groups_to_update.is_empty() + && user_groups_to_delete.is_empty() + && variables_to_create.is_empty() + && variables_to_update.is_empty() + && variables_to_delete.is_empty() + { + update.push_simple_log( + "No Changes", + format!("{}. exiting.", colored("nothing to do", "green")), + ); + update.finalize(); + update_update(update.clone()).await?; + return Ok(update); + } // ================= // No deps + maybe_extend( + &mut update.logs, + crate::helpers::sync::variables::run_updates( + variables_to_create, + variables_to_update, + variables_to_delete, + ) + .await, + ); + maybe_extend( + &mut update.logs, + crate::helpers::sync::user_groups::run_updates( + user_groups_to_create, + user_groups_to_update, + user_groups_to_delete, + ) + .await, + ); maybe_extend( &mut update.logs, entities::sync::ResourceSync::run_updates( @@ -256,6 +339,23 @@ impl Resolve for State { ) } + if let Err(e) = State + .resolve( + RefreshResourceSyncPending { sync: sync.id }, + sync_user().to_owned(), + ) + .await + { + warn!("failed to refresh sync {} after run | {e:#}", sync.name); + update.push_error_log( + "refresh sync", + format!( + "failed to refresh sync pending after run | {}", + serialize_error_pretty(&e) + ), + ); + } + update.finalize(); update_update(update.clone()).await?; diff --git a/bin/core/src/api/write/sync.rs b/bin/core/src/api/write/sync.rs index 3ce3123f0..5e85beced 100644 --- a/bin/core/src/api/write/sync.rs +++ b/bin/core/src/api/write/sync.rs @@ -183,6 +183,21 @@ impl Resolve for State { ) .await .context("failed to get resource sync updates")?, + variable_updates: + crate::helpers::sync::variables::get_updates_for_view( + resources.variables, + sync.config.delete, + ) + .await + .context("failed to get variable updates")?, + user_group_updates: + crate::helpers::sync::user_groups::get_updates_for_view( + resources.user_groups, + sync.config.delete, + &all_resources, + ) + .await + .context("failed to get user group updates")?, }; let pending = to_document(&pending) diff --git a/bin/core/src/helpers/sync/mod.rs b/bin/core/src/helpers/sync/mod.rs index 9d1f7f06f..bc2b7dd52 100644 --- a/bin/core/src/helpers/sync/mod.rs +++ b/bin/core/src/helpers/sync/mod.rs @@ -1,5 +1,7 @@ pub mod remote; pub mod resource; +pub mod user_groups; +pub mod variables; mod file; mod resources; @@ -12,6 +14,6 @@ fn bold(content: &str) -> String { format!("{content}") } -fn colored(content: &str, color: &str) -> String { +pub fn colored(content: &str, color: &str) -> String { format!("{content}") } diff --git a/bin/core/src/helpers/sync/resource.rs b/bin/core/src/helpers/sync/resource.rs index df9ff5a30..20de85045 100644 --- a/bin/core/src/helpers/sync/resource.rs +++ b/bin/core/src/helpers/sync/resource.rs @@ -80,9 +80,8 @@ pub trait ResourceSync: MonitorResource + Sized { { Ok(resource) => resource.id, Err(e) => { - log.push('\n'); log.push_str(&format!( - "{}: failed to create {} '{}' | {e:#}", + "\n{}: failed to create {} '{}' | {e:#}", colored("ERROR", "red"), Self::resource_type(), bold(&name) @@ -136,7 +135,6 @@ pub trait ResourceSync: MonitorResource + Sized { } if !resource.config.is_none() { - log.push('\n'); if let Err(e) = crate::resource::update::( &id, resource.config, @@ -145,14 +143,14 @@ pub trait ResourceSync: MonitorResource + Sized { .await { log.push_str(&format!( - "{}: failed to update config on {} '{}' | {e:#}", + "\n{}: failed to update config on {} '{}' | {e:#}", colored("ERROR", "red"), Self::resource_type(), bold(&name), )) } else { log.push_str(&format!( - "{}: {} {} '{}' configuration", + "\n{}: {} {} '{}' configuration", muted("INFO"), colored("updated", "blue"), Self::resource_type(), @@ -163,19 +161,18 @@ pub trait ResourceSync: MonitorResource + Sized { } for resource in to_delete { - log.push('\n'); if let Err(e) = crate::resource::delete::(&resource, sync_user()).await { log.push_str(&format!( - "{}: failed to delete {} '{}' | {e:#}", + "\n{}: failed to delete {} '{}' | {e:#}", colored("ERROR", "red"), Self::resource_type(), bold(&resource), )) } else { log.push_str(&format!( - "{}: {} {} '{}'", + "\n{}: {} {} '{}'", muted("INFO"), colored("deleted", "red"), Self::resource_type(), @@ -251,7 +248,7 @@ pub async fn get_updates_for_view( any_change = true; log.push_str(&format!( - "\n{}: {}: '{}'\n-------------------", + "\n\n{}: {}: '{}'\n-------------------", colored("UPDATE", "blue"), Resource::resource_type(), bold(&resource.name) @@ -296,7 +293,7 @@ pub async fn get_updates_for_view( None => { any_change = true; log.push_str(&format!( - "\n{}: {}: {}\n{}: {}\n{}: {:?}\n{}: {}", + "\n\n{}: {}: {}\n{}: {}\n{}: {:?}\n{}: {}", colored("CREATE", "green"), Resource::resource_type(), bold(&resource.name), @@ -314,7 +311,7 @@ pub async fn get_updates_for_view( for name in to_delete { log.push_str(&format!( - "\n{}: {}: '{}'\n-------------------", + "\n\n{}: {}: '{}'\n-------------------", colored("DELETE", "red"), Resource::resource_type(), bold(&name) @@ -379,47 +376,6 @@ pub async fn get_updates_for_execution( continue; } - // println!( - // "\n{}: {}: '{}'\n-------------------", - // "UPDATE".blue(), - // Resource::display(), - // resource.name.bold(), - // ); - // let mut lines = Vec::::new(); - // if resource.description != original.description { - // lines.push(format!( - // "{}: 'description'\n{}: {}\n{}: {}", - // "field".dimmed(), - // "from".dimmed(), - // original.description.red(), - // "to".dimmed(), - // resource.description.green() - // )) - // } - // if resource.tags != original_tags { - // let from = format!("{:?}", original_tags).red(); - // let to = format!("{:?}", resource.tags).green(); - // lines.push(format!( - // "{}: 'tags'\n{}: {from}\n{}: {to}", - // "field".dimmed(), - // "from".dimmed(), - // "to".dimmed(), - // )); - // } - // lines.extend(diff.iter_field_diffs().map( - // |FieldDiff { field, from, to }| { - // format!( - // "{}: '{field}'\n{}: {}\n{}: {}", - // "field".dimmed(), - // "from".dimmed(), - // from.red(), - // "to".dimmed(), - // to.green() - // ) - // }, - // )); - // println!("{}", lines.join("\n-------------------\n")); - // Minimizes updates through diffing. resource.config = diff.into(); @@ -433,21 +389,7 @@ pub async fn get_updates_for_execution( to_update.push(update); } - None => { - // println!( - // "\n{}: {}: {}\n{}: {}\n{}: {:?}\n{}: {}", - // "CREATE".green(), - // Resource::display(), - // resource.name.bold().green(), - // "description".dimmed(), - // resource.description, - // "tags".dimmed(), - // resource.tags, - // "config".dimmed(), - // serde_json::to_string_pretty(&resource.config)? - // ); - to_create.push(resource); - } + None => to_create.push(resource), } } @@ -461,7 +403,6 @@ pub async fn run_update_tags( log: &mut String, ) { // Update tags - log.push('\n'); if let Err(e) = State .resolve( UpdateTagsOnResource { @@ -473,14 +414,14 @@ pub async fn run_update_tags( .await { log.push_str(&format!( - "{}: failed to update tags on {} '{}' | {e:#}", + "\n{}: failed to update tags on {} '{}' | {e:#}", colored("ERROR", "red"), Resource::resource_type(), bold(name), )) } else { log.push_str(&format!( - "{}: {} {} '{}' tags", + "\n{}: {} {} '{}' tags", muted("INFO"), colored("updated", "blue"), Resource::resource_type(), @@ -495,7 +436,6 @@ pub async fn run_update_description( description: String, log: &mut String, ) { - log.push('\n'); if let Err(e) = State .resolve( UpdateDescription { @@ -507,14 +447,14 @@ pub async fn run_update_description( .await { log.push_str(&format!( - "{}: failed to update description on {} '{}' | {e:#}", + "\n{}: failed to update description on {} '{}' | {e:#}", colored("ERROR", "red"), Resource::resource_type(), bold(name), )) } else { log.push_str(&format!( - "{}: {} {} '{}' description", + "\n{}: {} {} '{}' description", muted("INFO"), colored("updated", "blue"), Resource::resource_type(), diff --git a/bin/core/src/helpers/sync/user_groups.rs b/bin/core/src/helpers/sync/user_groups.rs new file mode 100644 index 000000000..188d7df7c --- /dev/null +++ b/bin/core/src/helpers/sync/user_groups.rs @@ -0,0 +1,630 @@ +use std::{cmp::Ordering, collections::HashMap}; + +use anyhow::Context; +use monitor_client::{ + api::{ + read::ListUserTargetPermissions, + write::{ + CreateUserGroup, DeleteUserGroup, SetUsersInUserGroup, + UpdatePermissionOnTarget, + }, + }, + entities::{ + permission::UserTarget, + toml::{PermissionToml, UserGroupToml}, + update::{Log, ResourceTarget}, + user::sync_user, + }, +}; +use mungos::find::find_collect; +use resolver_api::Resolve; + +use crate::state::{db_client, State}; + +use super::{bold, colored, muted, resource::AllResourcesById}; + +pub struct UpdateItem { + user_group: UserGroupToml, + update_users: bool, + update_permissions: bool, +} + +pub struct DeleteItem { + id: String, + name: String, +} + +pub async fn get_updates_for_view( + user_groups: Vec, + delete: bool, + all_resources: &AllResourcesById, +) -> anyhow::Result> { + let map = find_collect(&db_client().await.user_groups, None, None) + .await + .context("failed to query db for UserGroups")? + .into_iter() + .map(|ug| (ug.name.clone(), ug)) + .collect::>(); + + let mut to_delete = Vec::::new(); + + if delete { + for user_group in map.values() { + if !user_groups.iter().any(|ug| ug.name == user_group.name) { + to_delete.push(user_group.name.clone()); + } + } + } + + let mut log = String::from("User Group Updates"); + + if user_groups.is_empty() { + if to_delete.is_empty() { + return Ok(None); + } + for name in &to_delete { + log.push_str(&format!( + "\n\n{}: user group: '{}'\n-------------------", + colored("DELETE", "red"), + bold(name), + )); + } + return Ok(Some(log)); + } + + let id_to_user = find_collect(&db_client().await.users, None, None) + .await + .context("failed to query db for Users")? + .into_iter() + .map(|user| (user.id.clone(), user)) + .collect::>(); + + for mut user_group in user_groups { + let original = match map.get(&user_group.name).cloned() { + Some(original) => original, + None => { + log.push_str(&format!( + "\n\n{}: user group: {}\n{}: {:?}\n{}: {:?}", + colored("CREATE", "green"), + colored(&user_group.name, "green"), + muted("users"), + user_group.users, + muted("permissions"), + user_group.permissions, + )); + continue; + } + }; + + let mut original_users = original + .users + .into_iter() + .filter_map(|user_id| { + id_to_user.get(&user_id).map(|u| u.username.clone()) + }) + .collect::>(); + + let mut original_permissions = State + .resolve( + ListUserTargetPermissions { + user_target: UserTarget::UserGroup(original.id), + }, + sync_user().to_owned(), + ) + .await + .context("failed to query for existing UserGroup permissions")? + .into_iter() + .map(|mut p| { + // replace the ids with names + match &mut p.resource_target { + ResourceTarget::System(_) => {} + ResourceTarget::Build(id) => { + *id = all_resources + .builds + .get(id) + .map(|b| b.name.clone()) + .unwrap_or_default() + } + ResourceTarget::Builder(id) => { + *id = all_resources + .builders + .get(id) + .map(|b| b.name.clone()) + .unwrap_or_default() + } + ResourceTarget::Deployment(id) => { + *id = all_resources + .deployments + .get(id) + .map(|b| b.name.clone()) + .unwrap_or_default() + } + ResourceTarget::Server(id) => { + *id = all_resources + .servers + .get(id) + .map(|b| b.name.clone()) + .unwrap_or_default() + } + ResourceTarget::Repo(id) => { + *id = all_resources + .repos + .get(id) + .map(|b| b.name.clone()) + .unwrap_or_default() + } + ResourceTarget::Alerter(id) => { + *id = all_resources + .alerters + .get(id) + .map(|b| b.name.clone()) + .unwrap_or_default() + } + ResourceTarget::Procedure(id) => { + *id = all_resources + .procedures + .get(id) + .map(|b| b.name.clone()) + .unwrap_or_default() + } + ResourceTarget::ServerTemplate(id) => { + *id = all_resources + .templates + .get(id) + .map(|b| b.name.clone()) + .unwrap_or_default() + } + ResourceTarget::ResourceSync(id) => { + *id = all_resources + .syncs + .get(id) + .map(|b| b.name.clone()) + .unwrap_or_default() + } + } + PermissionToml { + target: p.resource_target, + level: p.level, + } + }) + .collect::>(); + + original_users.sort(); + user_group.users.sort(); + + user_group.permissions.sort_by(sort_permissions); + original_permissions.sort_by(sort_permissions); + + let update_users = user_group.users != original_users; + let update_permissions = + user_group.permissions != original_permissions; + + // only push update after failed diff + if update_users || update_permissions { + log.push_str(&format!( + "\n\n{}: user group: '{}'\n-------------------", + colored("UPDATE", "blue"), + bold(&user_group.name), + )); + let mut lines = Vec::::new(); + if update_users { + let adding = user_group + .users + .iter() + .filter(|user| !original_users.contains(user)) + .map(|user| user.as_str()) + .collect::>(); + let adding = if adding.is_empty() { + String::from("None") + } else { + colored(&adding.join(", "), "green") + }; + let removing = original_users + .iter() + .filter(|user| !user_group.users.contains(user)) + .map(|user| user.as_str()) + .collect::>(); + let removing = if removing.is_empty() { + String::from("None") + } else { + colored(&removing.join(", "), "red") + }; + lines.push(format!( + "{}: 'users'\n{}: {removing}\n{}: {adding}", + muted("field"), + muted("removing"), + muted("adding"), + )) + } + if update_permissions { + let adding = user_group + .permissions + .iter() + .filter(|permission| { + !original_permissions.contains(permission) + }) + .map(|permission| format!("{permission:?}")) + .collect::>(); + let adding = if adding.is_empty() { + String::from("None") + } else { + colored(&adding.join(", "), "green") + }; + let removing = original_permissions + .iter() + .filter(|permission| { + !user_group.permissions.contains(permission) + }) + .map(|permission| format!("{permission:?}")) + .collect::>(); + let removing = if removing.is_empty() { + String::from("None") + } else { + colored(&removing.join(", "), "red") + }; + lines.push(format!( + "{}: 'permissions'\n{}: {removing}\n{}: {adding}", + muted("field"), + muted("removing"), + muted("adding"), + )) + } + log.push('\n'); + log.push_str(&lines.join("\n-------------------\n")); + } + } + + for name in &to_delete { + log.push_str(&format!( + "\n\n{}: user group: '{}'\n-------------------", + colored("DELETE", "red"), + bold(name), + )); + } + + Ok(Some(log)) +} + +pub async fn get_updates_for_execution( + user_groups: Vec, + delete: bool, + all_resources: &AllResourcesById, +) -> anyhow::Result<( + Vec, + Vec, + Vec, +)> { + let map = find_collect(&db_client().await.user_groups, None, None) + .await + .context("failed to query db for UserGroups")? + .into_iter() + .map(|ug| (ug.name.clone(), ug)) + .collect::>(); + + let mut to_create = Vec::::new(); + let mut to_update = Vec::::new(); + let mut to_delete = Vec::::new(); + + if delete { + for user_group in map.values() { + if !user_groups.iter().any(|ug| ug.name == user_group.name) { + to_delete.push(DeleteItem { + id: user_group.id.clone(), + name: user_group.name.clone(), + }); + } + } + } + + if user_groups.is_empty() { + return Ok((to_create, to_update, to_delete)); + } + + let id_to_user = find_collect(&db_client().await.users, None, None) + .await + .context("failed to query db for Users")? + .into_iter() + .map(|user| (user.id.clone(), user)) + .collect::>(); + + for mut user_group in user_groups { + let original = match map.get(&user_group.name).cloned() { + Some(original) => original, + None => { + to_create.push(user_group); + continue; + } + }; + + let mut original_users = original + .users + .into_iter() + .filter_map(|user_id| { + id_to_user.get(&user_id).map(|u| u.username.clone()) + }) + .collect::>(); + + let mut original_permissions = State + .resolve( + ListUserTargetPermissions { + user_target: UserTarget::UserGroup(original.id), + }, + sync_user().to_owned(), + ) + .await + .context("failed to query for existing UserGroup permissions")? + .into_iter() + .map(|mut p| { + // replace the ids with names + match &mut p.resource_target { + ResourceTarget::System(_) => {} + ResourceTarget::Build(id) => { + *id = all_resources + .builds + .get(id) + .map(|b| b.name.clone()) + .unwrap_or_default() + } + ResourceTarget::Builder(id) => { + *id = all_resources + .builders + .get(id) + .map(|b| b.name.clone()) + .unwrap_or_default() + } + ResourceTarget::Deployment(id) => { + *id = all_resources + .deployments + .get(id) + .map(|b| b.name.clone()) + .unwrap_or_default() + } + ResourceTarget::Server(id) => { + *id = all_resources + .servers + .get(id) + .map(|b| b.name.clone()) + .unwrap_or_default() + } + ResourceTarget::Repo(id) => { + *id = all_resources + .repos + .get(id) + .map(|b| b.name.clone()) + .unwrap_or_default() + } + ResourceTarget::Alerter(id) => { + *id = all_resources + .alerters + .get(id) + .map(|b| b.name.clone()) + .unwrap_or_default() + } + ResourceTarget::Procedure(id) => { + *id = all_resources + .procedures + .get(id) + .map(|b| b.name.clone()) + .unwrap_or_default() + } + ResourceTarget::ServerTemplate(id) => { + *id = all_resources + .templates + .get(id) + .map(|b| b.name.clone()) + .unwrap_or_default() + } + ResourceTarget::ResourceSync(id) => { + *id = all_resources + .syncs + .get(id) + .map(|b| b.name.clone()) + .unwrap_or_default() + } + } + PermissionToml { + target: p.resource_target, + level: p.level, + } + }) + .collect::>(); + + original_users.sort(); + user_group.users.sort(); + + user_group.permissions.sort_by(sort_permissions); + original_permissions.sort_by(sort_permissions); + + let update_users = user_group.users != original_users; + let update_permissions = + user_group.permissions != original_permissions; + + // only push update after failed diff + if update_users || update_permissions { + to_update.push(UpdateItem { + user_group, + update_users, + update_permissions, + }); + } + } + + Ok((to_create, to_update, to_delete)) +} + +/// order permissions in deterministic way +fn sort_permissions( + a: &PermissionToml, + b: &PermissionToml, +) -> Ordering { + let (a_t, a_id) = a.target.extract_variant_id(); + let (b_t, b_id) = b.target.extract_variant_id(); + match (a_t.cmp(&b_t), a_id.cmp(b_id)) { + (Ordering::Greater, _) => Ordering::Greater, + (Ordering::Less, _) => Ordering::Less, + (_, Ordering::Greater) => Ordering::Greater, + (_, Ordering::Less) => Ordering::Less, + _ => Ordering::Equal, + } +} + +pub async fn run_updates( + to_create: Vec, + to_update: Vec, + to_delete: Vec, +) -> Option { + if to_create.is_empty() + && to_update.is_empty() + && to_delete.is_empty() + { + return None; + } + + let mut log = String::from("running updates on UserGroups"); + + // Create the non-existant user groups + for user_group in to_create { + // Create the user group + if let Err(e) = State + .resolve( + CreateUserGroup { + name: user_group.name.clone(), + }, + sync_user().to_owned(), + ) + .await + { + log.push_str(&format!( + "\n{}: failed to create user group '{}' | {e:#}", + colored("ERROR", "red"), + bold(&user_group.name) + )); + continue; + } else { + log.push_str(&format!( + "\n{}: {} user group '{}'", + muted("INFO"), + colored("created", "green"), + bold(&user_group.name) + )) + }; + + set_users(user_group.name.clone(), user_group.users, &mut log) + .await; + run_update_permissions( + user_group.name, + user_group.permissions, + &mut log, + ) + .await; + } + + // Update the existing user groups + for UpdateItem { + user_group, + update_users, + update_permissions, + } in to_update + { + if update_users { + set_users(user_group.name.clone(), user_group.users, &mut log) + .await; + } + if update_permissions { + run_update_permissions( + user_group.name, + user_group.permissions, + &mut log, + ) + .await; + } + } + + for user_group in to_delete { + if let Err(e) = State + .resolve( + DeleteUserGroup { id: user_group.id }, + sync_user().to_owned(), + ) + .await + { + log.push_str(&format!( + "\n{}: failed to delete user group '{}' | {e:#}", + colored("ERROR", "red"), + bold(&user_group.name) + )) + } else { + log.push_str(&format!( + "\n{}: {} user group '{}'", + muted("INFO"), + colored("deleted", "red"), + bold(&user_group.name) + )) + } + } + + Some(Log::simple("Update UserGroups", log)) +} + +async fn set_users( + user_group: String, + users: Vec, + log: &mut String, +) { + if let Err(e) = State + .resolve( + SetUsersInUserGroup { + user_group: user_group.clone(), + users, + }, + sync_user().to_owned(), + ) + .await + { + log.push_str(&format!( + "\n{}: failed to set users in group {} | {e:#}", + colored("ERROR", "red"), + bold(&user_group) + )) + } else { + log.push_str(&format!( + "\n{}: {} user group '{}' users", + muted("INFO"), + colored("updated", "blue"), + bold(&user_group) + )) + } +} + +async fn run_update_permissions( + user_group: String, + permissions: Vec, + log: &mut String, +) { + for PermissionToml { target, level } in permissions { + if let Err(e) = State + .resolve( + UpdatePermissionOnTarget { + user_target: UserTarget::UserGroup(user_group.clone()), + resource_target: target.clone(), + permission: level, + }, + sync_user().to_owned(), + ) + .await + { + log.push_str(&format!( + "\n{}: failed to set permssion in group {} | target: {target:?} | {e:#}", + colored("ERROR", "red"), + bold(&user_group) + )) + } else { + log.push_str(&format!( + "\n{}: {} user group '{}' permissions", + muted("INFO"), + colored("updated", "blue"), + bold(&user_group) + )) + } + } +} diff --git a/bin/core/src/helpers/sync/variables.rs b/bin/core/src/helpers/sync/variables.rs new file mode 100644 index 000000000..4a5f7dfd3 --- /dev/null +++ b/bin/core/src/helpers/sync/variables.rs @@ -0,0 +1,301 @@ +use std::collections::HashMap; + +use anyhow::Context; +use monitor_client::{ + api::write::{ + CreateVariable, DeleteVariable, UpdateVariableDescription, + UpdateVariableValue, + }, + entities::{update::Log, user::sync_user, variable::Variable}, +}; +use mungos::find::find_collect; +use resolver_api::Resolve; + +use crate::state::{db_client, State}; + +use super::{bold, colored, muted}; + +pub struct ToUpdateItem { + pub variable: Variable, + pub update_value: bool, + pub update_description: bool, +} + +pub async fn get_updates_for_view( + variables: Vec, + delete: bool, +) -> anyhow::Result> { + let map = find_collect(&db_client().await.variables, None, None) + .await + .context("failed to query db for variables")? + .into_iter() + .map(|v| (v.name.clone(), v)) + .collect::>(); + + let mut to_delete = Vec::::new(); + + if delete { + for variable in map.values() { + if !variables.iter().any(|v| v.name == variable.name) { + to_delete.push(variable.name.clone()); + } + } + } + + let mut log = String::from("Variable Updates"); + + if variables.is_empty() { + if to_delete.is_empty() { + return Ok(None); + } + for name in &to_delete { + log.push_str(&format!( + "\n\n{}: variable: '{}'\n-------------------", + colored("DELETE", "red"), + bold(name), + )); + } + return Ok(Some(log)); + } + + for variable in variables { + match map.get(&variable.name) { + Some(original) => { + let item = ToUpdateItem { + update_value: original.value != variable.value, + update_description: original.description + != variable.description, + variable, + }; + if !item.update_value && !item.update_description { + continue; + } + log.push_str(&format!( + "\n\n{}: variable: '{}'\n-------------------", + colored("UPDATE", "blue"), + bold(&item.variable.name), + )); + + let mut lines = Vec::::new(); + + if item.update_value { + lines.push(format!( + "{}: 'value'\n{}: {}\n{}: {}", + muted("field"), + muted("from"), + colored(&original.value, "red"), + muted("to"), + colored(&item.variable.value, "green") + )) + } + + if item.update_description { + lines.push(format!( + "{}: 'description'\n{}: {}\n{}: {}", + muted("field"), + muted("from"), + colored(&original.description, "red"), + muted("to"), + colored(&item.variable.description, "green") + )) + } + + log.push('\n'); + log.push_str(&lines.join("\n-------------------\n")); + } + None => { + log.push_str(&format!( + "\n\n{}: variable: {}\n{}: {}\n{}: {}", + colored("CREATE", "green"), + colored(&variable.name, "green"), + muted("description"), + variable.description, + muted("value"), + variable.value, + )); + } + } + } + + for name in &to_delete { + log.push_str(&format!( + "\n\n{}: variable: '{}'\n-------------------", + colored("DELETE", "red"), + bold(name), + )); + } + + Ok(Some(log)) +} + +pub async fn get_updates_for_execution( + variables: Vec, + delete: bool, +) -> anyhow::Result<(Vec, Vec, Vec)> { + let map = find_collect(&db_client().await.variables, None, None) + .await + .context("failed to query db for variables")? + .into_iter() + .map(|v| (v.name.clone(), v)) + .collect::>(); + + let mut to_create = Vec::::new(); + let mut to_update = Vec::::new(); + let mut to_delete = Vec::::new(); + + if delete { + for variable in map.values() { + if !variables.iter().any(|v| v.name == variable.name) { + to_delete.push(variable.name.clone()); + } + } + } + + for variable in variables { + match map.get(&variable.name) { + Some(original) => { + let item = ToUpdateItem { + update_value: original.value != variable.value, + update_description: original.description + != variable.description, + variable, + }; + if !item.update_value && !item.update_description { + continue; + } + + to_update.push(item); + } + None => to_create.push(variable), + } + } + + Ok((to_create, to_update, to_delete)) +} + +pub async fn run_updates( + to_create: Vec, + to_update: Vec, + to_delete: Vec, +) -> Option { + if to_create.is_empty() + && to_update.is_empty() + && to_delete.is_empty() + { + return None; + } + + let mut log = String::from("running updates on Variables"); + + for variable in to_create { + if let Err(e) = State + .resolve( + CreateVariable { + name: variable.name.clone(), + value: variable.value, + description: variable.description, + }, + sync_user().to_owned(), + ) + .await + { + log.push_str(&format!( + "\n{}: failed to create variable '{}' | {e:#}", + colored("ERROR", "red"), + bold(&variable.name) + )); + } else { + log.push_str(&format!( + "\n{}: {} variable '{}'", + muted("INFO"), + colored("created", "green"), + bold(&variable.name) + )) + }; + } + + for ToUpdateItem { + variable, + update_value, + update_description, + } in to_update + { + if update_value { + if let Err(e) = State + .resolve( + UpdateVariableValue { + name: variable.name.clone(), + value: variable.value, + }, + sync_user().to_owned(), + ) + .await + { + log.push_str(&format!( + "\n{}: failed to update variable value for '{}' | {e:#}", + colored("ERROR", "red"), + bold(&variable.name) + )) + } else { + log.push_str(&format!( + "\n{}: {} variable '{}' value", + muted("INFO"), + colored("updated", "blue"), + bold(&variable.name) + )) + }; + } + if update_description { + if let Err(e) = State + .resolve( + UpdateVariableDescription { + name: variable.name.clone(), + description: variable.description, + }, + sync_user().to_owned(), + ) + .await + { + log.push_str(&format!( + "\n{}: failed to update variable description for '{}' | {e:#}", + colored("ERROR", "red"), + bold(&variable.name) + )) + } else { + log.push_str(&format!( + "\n{}: {} variable '{}' description", + muted("INFO"), + colored("updated", "blue"), + bold(&variable.name) + )) + }; + } + } + + for variable in to_delete { + if let Err(e) = State + .resolve( + DeleteVariable { + name: variable.clone(), + }, + sync_user().to_owned(), + ) + .await + { + log.push_str(&format!( + "\n{}: failed to delete variable '{}' | {e:#}", + colored("ERROR", "red"), + bold(&variable) + )) + } else { + log.push_str(&format!( + "\n{}: {} variable '{}'", + muted("INFO"), + colored("deleted", "red"), + bold(&variable) + )) + } + } + + Some(Log::simple("Update Variables", log)) +} diff --git a/client/core/rs/src/entities/sync.rs b/client/core/rs/src/entities/sync.rs index 555e47772..41659c9b7 100644 --- a/client/core/rs/src/entities/sync.rs +++ b/client/core/rs/src/entities/sync.rs @@ -90,6 +90,10 @@ pub struct PendingUpdates { pub server_template_updates: Option, /// Readable log of any pending resource sync updates pub resource_sync_updates: Option, + /// Readable log of any pending variable updates + pub variable_updates: Option, + /// Readable log of any pending user group updates + pub user_group_updates: Option, } #[typeshare(serialized_as = "Partial")]