From 7ed115cddb669b390b0e076dfcc2d21b0305bb16 Mon Sep 17 00:00:00 2001 From: mbecker20 Date: Sun, 7 Jan 2024 14:54:57 -0800 Subject: [PATCH] CRUD for procedures --- bin/core/src/api/read/mod.rs | 8 + bin/core/src/api/read/procedure.rs | 117 +++++++++++++ bin/core/src/api/write/mod.rs | 7 + bin/core/src/api/write/procedure.rs | 236 +++++++++++++++++++++++++++ bin/core/src/auth/jwt.rs | 6 +- bin/core/src/helpers/mod.rs | 8 +- bin/core/src/helpers/procedure.rs | 3 +- bin/core/src/state.rs | 2 + client/rs/src/api/execute/mod.rs | 7 + client/rs/src/api/read/mod.rs | 2 + client/rs/src/api/read/procedure.rs | 89 ++++++++++ client/rs/src/api/write/mod.rs | 5 +- client/rs/src/api/write/procedure.rs | 71 ++++++++ client/rs/src/busy.rs | 9 +- client/rs/src/entities/mod.rs | 3 + client/rs/src/entities/procedure.rs | 6 + 16 files changed, 568 insertions(+), 11 deletions(-) create mode 100644 bin/core/src/api/read/procedure.rs create mode 100644 bin/core/src/api/write/procedure.rs create mode 100644 client/rs/src/api/read/procedure.rs create mode 100644 client/rs/src/api/write/procedure.rs diff --git a/bin/core/src/api/read/mod.rs b/bin/core/src/api/read/mod.rs index eac47a27a..0588bac01 100644 --- a/bin/core/src/api/read/mod.rs +++ b/bin/core/src/api/read/mod.rs @@ -23,6 +23,7 @@ mod alerter; mod build; mod builder; mod deployment; +mod procedure; mod repo; mod search; mod server; @@ -45,6 +46,13 @@ enum ReadRequest { // ==== SEARCH ==== FindResources(FindResources), + // ==== PROCEDURE ==== + GetProceduresSummary(GetProceduresSummary), + GetProcedure(GetProcedure), + GetProcedureActionState(GetProcedureActionState), + ListProcedures(ListProcedures), + ListProceduresByIds(ListProceduresByIds), + // ==== SERVER ==== GetServersSummary(GetServersSummary), GetServer(GetServer), diff --git a/bin/core/src/api/read/procedure.rs b/bin/core/src/api/read/procedure.rs new file mode 100644 index 000000000..7edd971e1 --- /dev/null +++ b/bin/core/src/api/read/procedure.rs @@ -0,0 +1,117 @@ +use anyhow::Context; +use async_trait::async_trait; +use monitor_client::{ + api::read::{ + GetProcedure, GetProcedureActionState, + GetProcedureActionStateResponse, GetProcedureResponse, + GetProceduresSummary, GetProceduresSummaryResponse, + ListProcedures, ListProceduresByIds, ListProceduresByIdsResponse, + ListProceduresResponse, + }, + entities::{procedure::Procedure, PermissionLevel}, +}; +use mungos::mongodb::bson::doc; +use resolver_api::Resolve; + +use crate::{ + auth::RequestUser, helpers::resource::StateResource, state::State, +}; + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + GetProcedure { id }: GetProcedure, + user: RequestUser, + ) -> anyhow::Result { + self + .get_resource_check_permissions( + &id, + &user, + PermissionLevel::Read, + ) + .await + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + ListProcedures { query }: ListProcedures, + user: RequestUser, + ) -> anyhow::Result { + >::list_resources_for_user( + self, query, &user, + ) + .await + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + ListProceduresByIds { ids }: ListProceduresByIds, + user: RequestUser, + ) -> anyhow::Result { + >::list_resources_for_user( + self, + doc! { "_id": { "$in": ids } }.into(), + &user, + ) + .await + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + GetProceduresSummary {}: GetProceduresSummary, + user: RequestUser, + ) -> anyhow::Result { + let query = if user.is_admin { + None + } else { + let query = doc! { + format!("permissions.{}", user.id): { "$in": ["read", "execute", "update"] } + }; + Some(query) + }; + let total = self + .db + .procedures + .count_documents(query, None) + .await + .context("failed to count all procedure documents")?; + let res = GetProceduresSummaryResponse { + total: total as u32, + }; + Ok(res) + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + GetProcedureActionState { id }: GetProcedureActionState, + user: RequestUser, + ) -> anyhow::Result { + let _: Procedure = self + .get_resource_check_permissions( + &id, + &user, + PermissionLevel::Read, + ) + .await?; + let action_state = self + .action_states + .procedure + .get(&id) + .await + .unwrap_or_default(); + Ok(action_state) + } +} diff --git a/bin/core/src/api/write/mod.rs b/bin/core/src/api/write/mod.rs index 62cfe1b84..661c1360d 100644 --- a/bin/core/src/api/write/mod.rs +++ b/bin/core/src/api/write/mod.rs @@ -23,6 +23,7 @@ mod deployment; mod description; mod launch; mod permissions; +mod procedure; mod repo; mod secret; mod server; @@ -90,6 +91,12 @@ enum WriteRequest { DeleteAlerter(DeleteAlerter), UpdateAlerter(UpdateAlerter), + // ==== PROCEDURE ==== + CreateProcedure(CreateProcedure), + CopyProcedure(CopyProcedure), + DeleteProcedure(DeleteProcedure), + UpdateProcedure(UpdateProcedure), + // ==== TAG ==== CreateTag(CreateTag), DeleteTag(DeleteTag), diff --git a/bin/core/src/api/write/procedure.rs b/bin/core/src/api/write/procedure.rs new file mode 100644 index 000000000..889ce558e --- /dev/null +++ b/bin/core/src/api/write/procedure.rs @@ -0,0 +1,236 @@ +use anyhow::{anyhow, Context}; +use async_trait::async_trait; +use monitor_client::{ + api::write::*, + entities::{ + monitor_timestamp, procedure::Procedure, to_monitor_name, + update::Log, Operation, PermissionLevel, + }, +}; +use mungos::{ + by_id::{delete_one_by_id, update_one_by_id}, + mongodb::bson::{doc, to_document}, +}; +use resolver_api::Resolve; + +use crate::{ + auth::RequestUser, + helpers::{make_update, resource::StateResource}, + state::State, +}; + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + CreateProcedure { name, config }: CreateProcedure, + user: RequestUser, + ) -> anyhow::Result { + let name = to_monitor_name(&name); + let start_ts = monitor_timestamp(); + let procedure = Procedure { + id: Default::default(), + name, + updated_at: start_ts, + permissions: [(user.id.clone(), PermissionLevel::Update)] + .into_iter() + .collect(), + description: Default::default(), + tags: Default::default(), + info: Default::default(), + config, + }; + let procedure_id = self + .db + .procedures + .insert_one(procedure, None) + .await + .context("failed to add procedure to db")? + .inserted_id + .to_string(); + let procedure: Procedure = + self.get_resource(&procedure_id).await?; + + let mut update = + make_update(&procedure, Operation::CreateProcedure, &user); + + update.push_simple_log( + "create procedure", + format!( + "created procedure\nid: {}\nname: {}", + procedure.id, procedure.name + ), + ); + + update + .push_simple_log("config", format!("{:#?}", procedure.config)); + + update.finalize(); + + self.add_update(update).await?; + + Ok(procedure) + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + CopyProcedure { name, id }: CopyProcedure, + user: RequestUser, + ) -> anyhow::Result { + let name = to_monitor_name(&name); + let Procedure { + config, + description, + tags, + .. + } = self + .get_resource_check_permissions( + &id, + &user, + PermissionLevel::Update, + ) + .await?; + let start_ts = monitor_timestamp(); + let build = Procedure { + id: Default::default(), + name, + updated_at: start_ts, + permissions: [(user.id.clone(), PermissionLevel::Update)] + .into_iter() + .collect(), + description, + tags, + config, + info: Default::default(), + }; + let procedure_id = self + .db + .procedures + .insert_one(build, None) + .await + .context("failed to add build to db")? + .inserted_id + .to_string(); + let procedure: Procedure = + self.get_resource(&procedure_id).await?; + + let mut update = + make_update(&procedure, Operation::CreateProcedure, &user); + + update.push_simple_log( + "create procedure", + format!( + "created procedure\nid: {}\nname: {}", + procedure.id, procedure.name + ), + ); + update.push_simple_log( + "config", + serde_json::to_string_pretty(&procedure)?, + ); + + update.finalize(); + + self.add_update(update).await?; + + Ok(procedure) + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + UpdateProcedure { id, config }: UpdateProcedure, + user: RequestUser, + ) -> anyhow::Result { + let procedure: Procedure = self + .get_resource_check_permissions( + &id, + &user, + PermissionLevel::Update, + ) + .await?; + + update_one_by_id( + &self.db.procedures, + &procedure.id, + mungos::update::Update::FlattenSet( + doc! { "config": to_document(&config)? }, + ), + None, + ) + .await + .context("failed to update procedure on database")?; + + let mut update = + make_update(&procedure, Operation::UpdateProcedure, &user); + + update.push_simple_log( + "procedure update", + serde_json::to_string_pretty(&config)?, + ); + + update.finalize(); + + self.add_update(update).await?; + + let procedure: Procedure = + self.get_resource(&procedure.id).await?; + + Ok(procedure) + } +} + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + DeleteProcedure { id }: DeleteProcedure, + user: RequestUser, + ) -> anyhow::Result { + // needs to pull its id from all container procedures + if self.action_states.procedure.busy(&id).await { + return Err(anyhow!("procedure busy")); + } + + let procedure: Procedure = self + .get_resource_check_permissions( + &id, + &user, + PermissionLevel::Update, + ) + .await?; + + let mut update = + make_update(&procedure, Operation::DeleteProcedure, &user); + update.in_progress(); + update.id = self.add_update(update.clone()).await?; + + let res = delete_one_by_id(&self.db.procedures, &id, None) + .await + .context("failed to delete build from database"); + + let log = match res { + Ok(_) => Log::simple( + "delete procedure", + format!("deleted procedure {}", procedure.name), + ), + Err(e) => Log::error( + "delete procedure", + format!("failed to delete procedure\n{e:#?}"), + ), + }; + + update.logs.push(log); + update.finalize(); + self.update_update(update).await?; + + self.remove_from_recently_viewed(&procedure).await?; + + Ok(procedure) + } +} diff --git a/bin/core/src/auth/jwt.rs b/bin/core/src/auth/jwt.rs index a854cc003..160020695 100644 --- a/bin/core/src/auth/jwt.rs +++ b/bin/core/src/auth/jwt.rs @@ -31,10 +31,10 @@ pub struct InnerRequestUser { } impl InnerRequestUser { - pub fn admin() -> InnerRequestUser { + pub fn procedure() -> InnerRequestUser { InnerRequestUser { - id: String::from("admin"), - username: String::from("admin"), + id: String::from("procedure"), + username: String::from("procedure"), is_admin: true, create_build_permissions: true, create_server_permissions: true, diff --git a/bin/core/src/helpers/mod.rs b/bin/core/src/helpers/mod.rs index 9cc0d54da..1031fdf81 100644 --- a/bin/core/src/helpers/mod.rs +++ b/bin/core/src/helpers/mod.rs @@ -230,10 +230,10 @@ impl State { .update_many( doc! {}, doc! { - "$pull": { - "recently_viewed": - to_bson(&resource).context("failed to convert resource to bson")? - } + "$pull": { + "recently_viewed": + to_bson(&resource).context("failed to convert resource to bson")? + } }, None ) diff --git a/bin/core/src/helpers/procedure.rs b/bin/core/src/helpers/procedure.rs index 52ec9d54a..955eb0166 100644 --- a/bin/core/src/helpers/procedure.rs +++ b/bin/core/src/helpers/procedure.rs @@ -164,9 +164,10 @@ impl State { &self, execution: Execution, ) -> anyhow::Result<()> { - let user: Arc<_> = InnerRequestUser::admin().into(); + let user: Arc<_> = InnerRequestUser::procedure().into(); let update = match execution { + Execution::None(_) => return Ok(()), Execution::RunBuild(req) => self .resolve(req, user) .await diff --git a/bin/core/src/state.rs b/bin/core/src/state.rs index 4af8c136a..a784d59d2 100644 --- a/bin/core/src/state.rs +++ b/bin/core/src/state.rs @@ -10,6 +10,7 @@ use monitor_client::entities::{ build::BuildActionState, config::CoreConfig, deployment::{DeploymentActionState, DockerContainerState}, + procedure::ProcedureActionState, repo::RepoActionState, server::ServerActionState, update::UpdateListItem, @@ -167,5 +168,6 @@ pub struct ActionStates { pub deployment: Cache, pub server: Cache, pub repo: Cache, + pub procedure: Cache, // pub command: Cache, } diff --git a/client/rs/src/api/execute/mod.rs b/client/rs/src/api/execute/mod.rs index 081d331e6..c01c59d46 100644 --- a/client/rs/src/api/execute/mod.rs +++ b/client/rs/src/api/execute/mod.rs @@ -20,6 +20,9 @@ pub trait MonitorExecuteRequest: HasResponse {} #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(tag = "type", content = "params")] pub enum Execution { + /// For new executions upon instantiation + None(None), + // PROCEDURE RunProcedure(RunProcedure), @@ -42,3 +45,7 @@ pub enum Execution { PruneDockerImages(PruneDockerImages), PruneDockerContainers(PruneDockerContainers), } + +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct None {} diff --git a/client/rs/src/api/read/mod.rs b/client/rs/src/api/read/mod.rs index 122202750..2730cafaf 100644 --- a/client/rs/src/api/read/mod.rs +++ b/client/rs/src/api/read/mod.rs @@ -8,6 +8,7 @@ mod alerter; mod build; mod builder; mod deployment; +mod procedure; mod repo; mod search; mod server; @@ -19,6 +20,7 @@ pub use alerter::*; pub use build::*; pub use builder::*; pub use deployment::*; +pub use procedure::*; pub use repo::*; pub use search::*; pub use server::*; diff --git a/client/rs/src/api/read/procedure.rs b/client/rs/src/api/read/procedure.rs new file mode 100644 index 000000000..414753fad --- /dev/null +++ b/client/rs/src/api/read/procedure.rs @@ -0,0 +1,89 @@ +use derive_empty_traits::EmptyTraits; +use resolver_api::derive::Request; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::entities::{ + procedure::{Procedure, ProcedureActionState, ProcedureListItem}, + MongoDocument, +}; + +use super::MonitorReadRequest; + +// + +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(GetProcedureResponse)] +pub struct GetProcedure { + pub id: String, +} + +#[typeshare] +pub type GetProcedureResponse = Procedure; + +// + +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(ListProceduresResponse)] +pub struct ListProcedures { + pub query: Option, +} + +#[typeshare] +pub type ListProceduresResponse = Vec; + +// + +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(ListProceduresByIdsResponse)] +pub struct ListProceduresByIds { + pub ids: Vec, +} + +#[typeshare] +pub type ListProceduresByIdsResponse = Vec; + +// + +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(GetProceduresSummaryResponse)] +pub struct GetProceduresSummary {} + +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct GetProceduresSummaryResponse { + pub total: u32, +} + +// + +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(GetProcedureActionStateResponse)] +pub struct GetProcedureActionState { + pub id: String, +} + +#[typeshare] +pub type GetProcedureActionStateResponse = ProcedureActionState; + +// diff --git a/client/rs/src/api/write/mod.rs b/client/rs/src/api/write/mod.rs index eb468d6ad..ed886fd38 100644 --- a/client/rs/src/api/write/mod.rs +++ b/client/rs/src/api/write/mod.rs @@ -5,6 +5,7 @@ mod deployment; mod description; mod launch; mod permissions; +mod procedure; mod repo; mod secret; mod server; @@ -18,11 +19,11 @@ pub use deployment::*; pub use description::*; pub use launch::*; pub use permissions::*; +pub use procedure::*; pub use repo::*; -use resolver_api::HasResponse; pub use secret::*; pub use server::*; pub use tags::*; pub use user::*; -pub trait MonitorWriteRequest: HasResponse {} +pub trait MonitorWriteRequest: resolver_api::HasResponse {} diff --git a/client/rs/src/api/write/procedure.rs b/client/rs/src/api/write/procedure.rs new file mode 100644 index 000000000..d8d5ea8f0 --- /dev/null +++ b/client/rs/src/api/write/procedure.rs @@ -0,0 +1,71 @@ +use derive_empty_traits::EmptyTraits; +use resolver_api::derive::Request; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::entities::procedure::{Procedure, ProcedureConfig}; + +use super::MonitorWriteRequest; + +// + +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(CreateProcedureResponse)] +pub struct CreateProcedure { + pub name: String, + pub config: ProcedureConfig, +} + +#[typeshare] +pub type CreateProcedureResponse = Procedure; + +// + +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(CopyProcedureResponse)] +pub struct CopyProcedure { + pub name: String, + pub id: String, +} + +#[typeshare] +pub type CopyProcedureResponse = Procedure; + +// + +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(DeleteProcedureResponse)] +pub struct DeleteProcedure { + pub id: String, +} + +#[typeshare] +pub type DeleteProcedureResponse = Procedure; + +// + +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorWriteRequest)] +#[response(UpdateProcedureResponse)] +pub struct UpdateProcedure { + pub id: String, + pub config: ProcedureConfig, +} + +#[typeshare] +pub type UpdateProcedureResponse = Procedure; diff --git a/client/rs/src/busy.rs b/client/rs/src/busy.rs index c52314c64..0f5f0ae7f 100644 --- a/client/rs/src/busy.rs +++ b/client/rs/src/busy.rs @@ -1,6 +1,7 @@ use crate::entities::{ build::BuildActionState, deployment::DeploymentActionState, - repo::RepoActionState, server::ServerActionState, + procedure::ProcedureActionState, repo::RepoActionState, + server::ServerActionState, }; pub trait Busy { @@ -38,3 +39,9 @@ impl Busy for RepoActionState { self.cloning || self.pulling || self.updating || self.deleting } } + +impl Busy for ProcedureActionState { + fn busy(&self) -> bool { + self.running + } +} diff --git a/client/rs/src/entities/mod.rs b/client/rs/src/entities/mod.rs index 702dccfbc..07d7a354c 100644 --- a/client/rs/src/entities/mod.rs +++ b/client/rs/src/entities/mod.rs @@ -394,6 +394,9 @@ pub enum Operation { DeleteAlerter, // procedure + CreateProcedure, + UpdateProcedure, + DeleteProcedure, RunProcedure, // user diff --git a/client/rs/src/entities/procedure.rs b/client/rs/src/entities/procedure.rs index 3609d0a88..390724aed 100644 --- a/client/rs/src/entities/procedure.rs +++ b/client/rs/src/entities/procedure.rs @@ -45,3 +45,9 @@ impl From<&ProcedureConfig> for ProcedureConfigVariant { } } } + +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct ProcedureActionState { + pub running: bool +} \ No newline at end of file