diff --git a/Cargo.lock b/Cargo.lock index ffd5936..b58fa2d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -476,7 +476,7 @@ dependencies = [ [[package]] name = "dmtri" version = "0.1.0" -source = "git+https://github.com/demeter-run/specs.git#04f4a89bb382c11504d5c230f2375de94c319dbd" +source = "git+https://github.com/demeter-run/specs.git#51812d214be74b463366a4034d154ea0976d4d9f" dependencies = [ "bytes", "pbjson", diff --git a/src/domain/auth.rs b/src/domain/auth/mod.rs similarity index 100% rename from src/domain/auth.rs rename to src/domain/auth/mod.rs diff --git a/src/domain/event.rs b/src/domain/event/mod.rs similarity index 59% rename from src/domain/event.rs rename to src/domain/event/mod.rs index ba406bb..3029744 100644 --- a/src/domain/event.rs +++ b/src/domain/event/mod.rs @@ -78,3 +78,54 @@ impl Event { pub trait EventDrivenBridge: Send + Sync { async fn dispatch(&self, event: Event) -> Result<()>; } + +#[cfg(test)] +mod tests { + use uuid::Uuid; + + use crate::domain::{ + project::ProjectStatus, + tests::{PHC, SECRET}, + }; + + use super::*; + + impl Default for ProjectCreated { + fn default() -> Self { + Self { + id: Uuid::new_v4().to_string(), + name: "New Project".into(), + namespace: "sonic-vegas".into(), + owner: "user id".into(), + status: ProjectStatus::Active.to_string(), + created_at: Utc::now(), + updated_at: Utc::now(), + } + } + } + impl Default for ProjectSecretCreated { + fn default() -> Self { + Self { + id: Uuid::new_v4().to_string(), + project_id: Uuid::new_v4().to_string(), + name: "Key 1".into(), + phc: PHC.into(), + secret: SECRET.as_bytes().to_vec(), + created_at: Utc::now(), + } + } + } + impl Default for ResourceCreated { + fn default() -> Self { + Self { + id: Uuid::new_v4().to_string(), + project_id: Uuid::new_v4().to_string(), + project_namespace: "prj-test".into(), + kind: "CardanoNode".into(), + data: "{\"spec\":{\"operatorVersion\":\"1\",\"kupoVersion\":\"v1\",\"network\":\"mainnet\",\"pruneUtxo\":false,\"throughputTier\":\"0\"}}".into(), + created_at: Utc::now(), + updated_at: Utc::now(), + } + } + } +} diff --git a/src/domain/mod.rs b/src/domain/mod.rs index 20c42cc..75b2d5a 100644 --- a/src/domain/mod.rs +++ b/src/domain/mod.rs @@ -5,3 +5,12 @@ pub mod resource; pub const PAGE_SIZE_DEFAULT: u32 = 12; pub const PAGE_SIZE_MAX: u32 = 120; + +#[cfg(test)] +mod tests { + pub const KEY: &str = "dmtr_apikey1g9gyswtcf3zxwd26v4x5jj3jw5wx3sn2"; + pub const PHC: &str = "$argon2id$v=19$m=19456,t=2,p=1$xVIt6Wr/bm1FewVhTr6zgA$nTO6EgGeOYZe7thACrHmFUWND40U4GEQCXKyvqzvRvs"; + pub const SECRET: &str = "fabric@txpipe"; + pub const INVALID_KEY: &str = "dmtr_apikey1xe6xzcjxv9nhycnz2ffnq6m02y7nat9e"; + pub const INVALID_HRP_KEY: &str = "dmtr_test18pp5vkjzfuuyzwpeg9gk2a2zvsylc5wg"; +} diff --git a/src/domain/project/cache.rs b/src/domain/project/cache.rs new file mode 100644 index 0000000..b7bdfab --- /dev/null +++ b/src/domain/project/cache.rs @@ -0,0 +1,77 @@ +use std::sync::Arc; + +use anyhow::Result; + +use crate::domain::event::{ProjectCreated, ProjectSecretCreated}; + +use super::{Project, ProjectSecret, ProjectUser}; + +#[async_trait::async_trait] +pub trait ProjectDrivenCache: Send + Sync { + async fn find(&self, user_id: &str, page: &u32, page_size: &u32) -> Result>; + async fn find_by_namespace(&self, namespace: &str) -> Result>; + async fn find_by_id(&self, id: &str) -> Result>; + async fn create(&self, project: &Project) -> Result<()>; + async fn create_secret(&self, secret: &ProjectSecret) -> Result<()>; + async fn find_secret_by_project_id(&self, project_id: &str) -> Result>; + async fn find_user_permission( + &self, + user_id: &str, + project_id: &str, + ) -> Result>; +} + +pub async fn create(cache: Arc, evt: ProjectCreated) -> Result<()> { + cache.create(&evt.try_into()?).await +} + +pub async fn create_secret( + cache: Arc, + evt: ProjectSecretCreated, +) -> Result<()> { + cache.create_secret(&evt.into()).await +} + +#[cfg(test)] +mod tests { + use mockall::mock; + + use super::*; + + mock! { + pub FakeProjectDrivenCache { } + + #[async_trait::async_trait] + impl ProjectDrivenCache for FakeProjectDrivenCache { + async fn find(&self, user_id: &str, page: &u32, page_size: &u32) -> Result>; + async fn find_by_namespace(&self, namespace: &str) -> Result>; + async fn find_by_id(&self, id: &str) -> Result>; + async fn create(&self, project: &Project) -> Result<()>; + async fn create_secret(&self, secret: &ProjectSecret) -> Result<()>; + async fn find_secret_by_project_id(&self, project_id: &str) -> Result>; + async fn find_user_permission(&self,user_id: &str, project_id: &str) -> Result>; + } + } + + #[tokio::test] + async fn it_should_create_project_cache() { + let mut cache = MockFakeProjectDrivenCache::new(); + cache.expect_create().return_once(|_| Ok(())); + + let evt = ProjectCreated::default(); + + let result = create(Arc::new(cache), evt).await; + assert!(result.is_ok()); + } + + #[tokio::test] + async fn it_should_create_project_secret_cache() { + let mut cache = MockFakeProjectDrivenCache::new(); + cache.expect_create_secret().return_once(|_| Ok(())); + + let evt = ProjectSecretCreated::default(); + + let result = create_secret(Arc::new(cache), evt).await; + assert!(result.is_ok()); + } +} diff --git a/src/domain/project/cluster.rs b/src/domain/project/cluster.rs new file mode 100644 index 0000000..3169813 --- /dev/null +++ b/src/domain/project/cluster.rs @@ -0,0 +1,81 @@ +use std::sync::Arc; + +use anyhow::{bail, Result}; +use k8s_openapi::api::core::v1::Namespace; +use kube::{api::ObjectMeta, ResourceExt}; +use tracing::info; + +use crate::domain::event::ProjectCreated; + +#[async_trait::async_trait] +pub trait ProjectDrivenCluster: Send + Sync { + async fn create(&self, namespace: &Namespace) -> Result<()>; + async fn find_by_name(&self, name: &str) -> Result>; +} + +pub async fn apply_manifest( + cluster: Arc, + evt: ProjectCreated, +) -> Result<()> { + if cluster.find_by_name(&evt.namespace).await?.is_some() { + bail!("namespace alread exist") + } + + let namespace = Namespace { + metadata: ObjectMeta { + name: Some(evt.namespace), + ..Default::default() + }, + ..Default::default() + }; + + cluster.create(&namespace).await?; + + //TODO: create event to update cache + info!(namespace = namespace.name_any(), "new namespace created"); + + Ok(()) +} + +#[cfg(test)] +mod tests { + use k8s_openapi::api::core::v1::Namespace; + use mockall::mock; + + use super::*; + + mock! { + pub FakeProjectDrivenCluster { } + + #[async_trait::async_trait] + impl ProjectDrivenCluster for FakeProjectDrivenCluster { + async fn create(&self, namespace: &Namespace) -> Result<()>; + async fn find_by_name(&self, name: &str) -> Result>; + } + } + + #[tokio::test] + async fn it_should_apply_manifest() { + let mut cluster = MockFakeProjectDrivenCluster::new(); + cluster.expect_create().return_once(|_| Ok(())); + cluster.expect_find_by_name().return_once(|_| Ok(None)); + + let project = ProjectCreated::default(); + + let result = apply_manifest(Arc::new(cluster), project).await; + assert!(result.is_ok()); + } + #[tokio::test] + async fn it_should_fail_apply_manifest_when_resource_exists() { + let mut cluster = MockFakeProjectDrivenCluster::new(); + cluster.expect_create().return_once(|_| Ok(())); + cluster + .expect_find_by_name() + .return_once(|_| Ok(Some(Namespace::default()))); + + let project = ProjectCreated::default(); + + let result = apply_manifest(Arc::new(cluster), project).await; + assert!(result.is_err()); + } +} diff --git a/src/domain/project.rs b/src/domain/project/command.rs similarity index 52% rename from src/domain/project.rs rename to src/domain/project/command.rs index f33ee63..5bdd4ab 100644 --- a/src/domain/project.rs +++ b/src/domain/project/command.rs @@ -1,31 +1,36 @@ +use std::sync::Arc; + use anyhow::{bail, ensure, Error, Result}; use argon2::{password_hash::SaltString, Argon2, PasswordHash, PasswordHasher, PasswordVerifier}; use bech32::{Bech32m, Hrp}; -use chrono::{DateTime, Utc}; -use k8s_openapi::api::core::v1::Namespace; -use kube::{api::ObjectMeta, ResourceExt}; +use chrono::Utc; use rand::{ distributions::{Alphanumeric, DistString}, rngs::OsRng, Rng, }; -use rdkafka::message::ToBytes; -use std::{fmt::Display, str::FromStr, sync::Arc}; use tracing::{error, info}; use uuid::Uuid; -use crate::domain::PAGE_SIZE_MAX; - -use super::{ +use crate::domain::{ auth::{Credential, UserId}, event::{EventDrivenBridge, ProjectCreated, ProjectSecretCreated}, - PAGE_SIZE_DEFAULT, + project::ProjectStatus, + PAGE_SIZE_DEFAULT, PAGE_SIZE_MAX, }; +use super::{cache::ProjectDrivenCache, Project, ProjectSecret}; + +pub async fn fetch(cache: Arc, cmd: FetchCmd) -> Result> { + let user_id = assert_credential(&cmd.credential)?; + + cache.find(&user_id, &cmd.page, &cmd.page_size).await +} + pub async fn create( cache: Arc, event: Arc, - cmd: CreateProjectCmd, + cmd: CreateCmd, ) -> Result<()> { let user_id = assert_credential(&cmd.credential)?; @@ -49,49 +54,10 @@ pub async fn create( Ok(()) } -pub async fn create_cache(cache: Arc, evt: ProjectCreated) -> Result<()> { - cache.create(&evt.try_into()?).await?; - - Ok(()) -} - -pub async fn find_cache( - cache: Arc, - cmd: FindProjectCmd, -) -> Result> { - let user_id = assert_credential(&cmd.credential)?; - - cache.find(&user_id, &cmd.page, &cmd.page_size).await -} - -pub async fn apply_manifest( - cluster: Arc, - evt: ProjectCreated, -) -> Result<()> { - if cluster.find_by_name(&evt.namespace).await?.is_some() { - bail!("namespace alread exist") - } - - let namespace = Namespace { - metadata: ObjectMeta { - name: Some(evt.namespace), - ..Default::default() - }, - ..Default::default() - }; - - cluster.create(&namespace).await?; - - //TODO: create event to update cache - info!(namespace = namespace.name_any(), "new namespace created"); - - Ok(()) -} - pub async fn create_secret( cache: Arc, event: Arc, - cmd: CreateProjectSecretCmd, + cmd: CreateSecretCmd, ) -> Result { assert_credential(&cmd.credential)?; assert_permission(cache.clone(), &cmd.credential, &cmd.project_id).await?; @@ -102,10 +68,10 @@ pub async fn create_secret( let key = Alphanumeric.sample_string(&mut rand::thread_rng(), 16); let salt_string = SaltString::generate(&mut OsRng); - let secret = cmd.secret.to_bytes(); + let secret = cmd.secret.into_bytes(); let argon2 = match Argon2::new_with_secret( - secret, + &secret, Default::default(), Default::default(), Default::default(), @@ -117,12 +83,14 @@ pub async fn create_secret( } }; + let key_bytes = key.into_bytes(); + let password_hash = argon2 - .hash_password(key.to_bytes(), salt_string.as_salt()) + .hash_password(&key_bytes, salt_string.as_salt()) .map_err(|err| Error::msg(err.to_string()))?; let hrp = Hrp::parse("dmtr_apikey")?; - let key = bech32::encode::(hrp, key.to_bytes())?; + let key = bech32::encode::(hrp, &key_bytes)?; let evt = ProjectSecretCreated { id: cmd.id, @@ -138,20 +106,12 @@ pub async fn create_secret( Ok(key) } -pub async fn create_secret_cache( - cache: Arc, - evt: ProjectSecretCreated, -) -> Result<()> { - cache.create_secret(&evt.into()).await?; - Ok(()) -} pub async fn verify_secret( cache: Arc, - project_id: &str, - key: &str, -) -> Result { - let (hrp, key) = bech32::decode(key).map_err(|error| { + cmd: VerifySecretCmd, +) -> Result { + let (hrp, key) = bech32::decode(&cmd.key).map_err(|error| { error!(?error, "invalid bech32"); Error::msg("invalid bech32") })?; @@ -161,7 +121,7 @@ pub async fn verify_secret( bail!("invalid project secret") } - let secrets = cache.find_secret_by_project_id(project_id).await?; + let secrets = cache.find_secret_by_project_id(&cmd.project_id).await?; let secret = secrets.into_iter().find(|project_secret| { let argon2 = Argon2::new_with_secret( @@ -174,7 +134,7 @@ pub async fn verify_secret( let Ok(password_hash) = PasswordHash::new(&project_secret.phc) else { error!( - project_id, + project_id = cmd.project_id, secret_id = project_secret.id, "error to decode phc" ); @@ -213,12 +173,12 @@ async fn assert_permission( } #[derive(Debug, Clone)] -pub struct FindProjectCmd { +pub struct FetchCmd { pub credential: Credential, pub page: u32, pub page_size: u32, } -impl FindProjectCmd { +impl FetchCmd { pub fn new(credential: Credential, page: Option, page_size: Option) -> Result { let page = page.unwrap_or(1); let page_size = page_size.unwrap_or(PAGE_SIZE_DEFAULT); @@ -235,15 +195,14 @@ impl FindProjectCmd { }) } } - #[derive(Debug, Clone)] -pub struct CreateProjectCmd { +pub struct CreateCmd { pub credential: Credential, pub id: String, pub name: String, pub namespace: String, } -impl CreateProjectCmd { +impl CreateCmd { pub fn new(credential: Credential, name: String) -> Self { let id = Uuid::new_v4().to_string(); let namespace: String = rand::thread_rng() @@ -262,66 +221,15 @@ impl CreateProjectCmd { } } -#[derive(Debug)] -pub enum ProjectStatus { - Active, - Deleted, -} -impl FromStr for ProjectStatus { - type Err = Error; - - fn from_str(s: &str) -> std::result::Result { - match s { - "active" => Ok(ProjectStatus::Active), - "deleted" => Ok(ProjectStatus::Deleted), - _ => bail!("project status not supported"), - } - } -} -impl Display for ProjectStatus { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - ProjectStatus::Active => write!(f, "active"), - ProjectStatus::Deleted => write!(f, "deleted"), - } - } -} - -#[derive(Debug)] -pub struct ProjectCache { - pub id: String, - pub name: String, - pub namespace: String, - pub owner: String, - pub status: ProjectStatus, - pub created_at: DateTime, - pub updated_at: DateTime, -} -impl TryFrom for ProjectCache { - type Error = Error; - - fn try_from(value: ProjectCreated) -> std::result::Result { - Ok(Self { - id: value.id, - namespace: value.namespace, - name: value.name, - owner: value.owner, - status: value.status.parse()?, - created_at: value.created_at, - updated_at: value.updated_at, - }) - } -} - #[derive(Debug, Clone)] -pub struct CreateProjectSecretCmd { +pub struct CreateSecretCmd { pub credential: Credential, pub secret: String, pub id: String, pub project_id: String, pub name: String, } -impl CreateProjectSecretCmd { +impl CreateSecretCmd { pub fn new(credential: Credential, secret: String, project_id: String, name: String) -> Self { let id = Uuid::new_v4().to_string(); @@ -334,78 +242,36 @@ impl CreateProjectSecretCmd { } } } - -#[derive(Debug)] -pub struct ProjectSecretCache { - pub id: String, - pub project_id: String, - pub name: String, - pub phc: String, - pub secret: Vec, - pub created_at: DateTime, -} -impl From for ProjectSecretCache { - fn from(value: ProjectSecretCreated) -> Self { - Self { - id: value.id, - project_id: value.project_id, - name: value.name, - phc: value.phc, - secret: value.secret, - created_at: value.created_at, - } - } -} - -#[allow(dead_code)] -pub struct ProjectUserCache { - pub user_id: String, +#[derive(Debug, Clone)] +pub struct VerifySecretCmd { pub project_id: String, - pub created_at: DateTime, -} - -#[async_trait::async_trait] -pub trait ProjectDrivenCache: Send + Sync { - async fn find(&self, user_id: &str, page: &u32, page_size: &u32) -> Result>; - async fn find_by_namespace(&self, namespace: &str) -> Result>; - async fn find_by_id(&self, id: &str) -> Result>; - async fn create(&self, project: &ProjectCache) -> Result<()>; - async fn create_secret(&self, secret: &ProjectSecretCache) -> Result<()>; - async fn find_secret_by_project_id(&self, project_id: &str) -> Result>; - async fn find_user_permission( - &self, - user_id: &str, - project_id: &str, - ) -> Result>; -} - -#[async_trait::async_trait] -pub trait ProjectDrivenCluster: Send + Sync { - async fn create(&self, namespace: &Namespace) -> Result<()>; - async fn find_by_name(&self, name: &str) -> Result>; + pub key: String, } #[cfg(test)] mod tests { - use k8s_openapi::api::core::v1::Namespace; use mockall::mock; use uuid::Uuid; use super::*; - use crate::domain::event::Event; + use crate::domain::{ + event::Event, + project::ProjectUser, + tests::{INVALID_HRP_KEY, INVALID_KEY, KEY, SECRET}, + }; mock! { pub FakeProjectDrivenCache { } #[async_trait::async_trait] impl ProjectDrivenCache for FakeProjectDrivenCache { - async fn find(&self, user_id: &str, page: &u32, page_size: &u32) -> Result>; - async fn find_by_namespace(&self, namespace: &str) -> Result>; - async fn find_by_id(&self, id: &str) -> Result>; - async fn create(&self, project: &ProjectCache) -> Result<()>; - async fn create_secret(&self, secret: &ProjectSecretCache) -> Result<()>; - async fn find_secret_by_project_id(&self, project_id: &str) -> Result>; - async fn find_user_permission(&self,user_id: &str, project_id: &str) -> Result>; + async fn find(&self, user_id: &str, page: &u32, page_size: &u32) -> Result>; + async fn find_by_namespace(&self, namespace: &str) -> Result>; + async fn find_by_id(&self, id: &str) -> Result>; + async fn create(&self, project: &Project) -> Result<()>; + async fn create_secret(&self, secret: &ProjectSecret) -> Result<()>; + async fn find_secret_by_project_id(&self, project_id: &str) -> Result>; + async fn find_user_permission(&self,user_id: &str, project_id: &str) -> Result>; } } @@ -418,60 +284,26 @@ mod tests { } } - mock! { - pub FakeProjectDrivenCluster { } - - #[async_trait::async_trait] - impl ProjectDrivenCluster for FakeProjectDrivenCluster { - async fn create(&self, namespace: &Namespace) -> Result<()>; - async fn find_by_name(&self, name: &str) -> Result>; - } - } - - impl Default for CreateProjectCmd { + impl Default for FetchCmd { fn default() -> Self { Self { credential: Credential::Auth0("user id".into()), - id: Uuid::new_v4().to_string(), - name: "New Project".into(), - namespace: "sonic-vegas".into(), - } - } - } - impl Default for ProjectCache { - fn default() -> Self { - Self { - id: Uuid::new_v4().to_string(), - name: "New Project".into(), - namespace: "sonic-vegas".into(), - owner: "user id".into(), - status: ProjectStatus::Active, - created_at: Utc::now(), - updated_at: Utc::now(), + page: 1, + page_size: 12, } } } - impl Default for ProjectCreated { + impl Default for CreateCmd { fn default() -> Self { Self { + credential: Credential::Auth0("user id".into()), id: Uuid::new_v4().to_string(), name: "New Project".into(), namespace: "sonic-vegas".into(), - owner: "user id".into(), - status: ProjectStatus::Active.to_string(), - created_at: Utc::now(), - updated_at: Utc::now(), } } } - - const KEY: &str = "dmtr_apikey1g9gyswtcf3zxwd26v4x5jj3jw5wx3sn2"; - const PHC: &str = "$argon2id$v=19$m=19456,t=2,p=1$xVIt6Wr/bm1FewVhTr6zgA$nTO6EgGeOYZe7thACrHmFUWND40U4GEQCXKyvqzvRvs"; - const SECRET: &str = "fabric@txpipe"; - const INVALID_KEY: &str = "dmtr_apikey1xe6xzcjxv9nhycnz2ffnq6m02y7nat9e"; - const INVALID_HRP_KEY: &str = "dmtr_test18pp5vkjzfuuyzwpeg9gk2a2zvsylc5wg"; - - impl Default for CreateProjectSecretCmd { + impl Default for CreateSecretCmd { fn default() -> Self { Self { credential: Credential::Auth0("user id".into()), @@ -482,48 +314,26 @@ mod tests { } } } - impl Default for FindProjectCmd { - fn default() -> Self { - Self { - credential: Credential::Auth0("user id".into()), - page: 1, - page_size: 12, - } - } - } - impl Default for ProjectSecretCache { - fn default() -> Self { - Self { - id: Uuid::new_v4().to_string(), - project_id: Uuid::new_v4().to_string(), - name: "Key 1".into(), - phc: PHC.into(), - secret: SECRET.to_bytes().to_vec(), - created_at: Utc::now(), - } - } - } - impl Default for ProjectSecretCreated { + impl Default for VerifySecretCmd { fn default() -> Self { Self { - id: Uuid::new_v4().to_string(), - project_id: Uuid::new_v4().to_string(), - name: "Key 1".into(), - phc: PHC.into(), - secret: SECRET.to_bytes().to_vec(), - created_at: Utc::now(), + project_id: Default::default(), + key: KEY.into(), } } } - impl Default for ProjectUserCache { - fn default() -> Self { - Self { - user_id: Uuid::new_v4().to_string(), - project_id: Uuid::new_v4().to_string(), - created_at: Utc::now(), - } - } + #[tokio::test] + async fn it_should_fetch_user_projects() { + let mut cache = MockFakeProjectDrivenCache::new(); + cache + .expect_find() + .return_once(|_, _, _| Ok(vec![Project::default()])); + + let cmd = FetchCmd::default(); + + let result = fetch(Arc::new(cache), cmd).await; + assert!(result.is_ok()); } #[tokio::test] @@ -534,7 +344,7 @@ mod tests { let mut event = MockFakeEventDrivenBridge::new(); event.expect_dispatch().return_once(|_| Ok(())); - let cmd = CreateProjectCmd::default(); + let cmd = CreateCmd::default(); let result = create(Arc::new(cache), Arc::new(event), cmd).await; assert!(result.is_ok()); @@ -544,11 +354,11 @@ mod tests { let mut cache = MockFakeProjectDrivenCache::new(); cache .expect_find_by_namespace() - .return_once(|_| Ok(Some(ProjectCache::default()))); + .return_once(|_| Ok(Some(Project::default()))); let event = MockFakeEventDrivenBridge::new(); - let cmd = CreateProjectCmd::default(); + let cmd = CreateCmd::default(); let result = create(Arc::new(cache), Arc::new(event), cmd).await; assert!(result.is_err()); @@ -558,7 +368,7 @@ mod tests { let cache = MockFakeProjectDrivenCache::new(); let event = MockFakeEventDrivenBridge::new(); - let cmd = CreateProjectCmd { + let cmd = CreateCmd { credential: Credential::ApiKey("xxxx".into()), ..Default::default() }; @@ -567,44 +377,20 @@ mod tests { assert!(result.is_err()); } - #[tokio::test] - async fn it_should_create_project_cache() { - let mut cache = MockFakeProjectDrivenCache::new(); - cache.expect_create().return_once(|_| Ok(())); - - let evt = ProjectCreated::default(); - - let result = create_cache(Arc::new(cache), evt).await; - assert!(result.is_ok()); - } - - #[tokio::test] - async fn it_should_find_user_projects_cache() { - let mut cache = MockFakeProjectDrivenCache::new(); - cache - .expect_find() - .return_once(|_, _, _| Ok(vec![ProjectCache::default()])); - - let evt = FindProjectCmd::default(); - - let result = find_cache(Arc::new(cache), evt).await; - assert!(result.is_ok()); - } - #[tokio::test] async fn it_should_create_project_secret() { let mut cache = MockFakeProjectDrivenCache::new(); cache .expect_find_user_permission() - .return_once(|_, _| Ok(Some(ProjectUserCache::default()))); + .return_once(|_, _| Ok(Some(ProjectUser::default()))); cache .expect_find_by_id() - .return_once(|_| Ok(Some(ProjectCache::default()))); + .return_once(|_| Ok(Some(Project::default()))); let mut event = MockFakeEventDrivenBridge::new(); event.expect_dispatch().return_once(|_| Ok(())); - let cmd = CreateProjectSecretCmd::default(); + let cmd = CreateSecretCmd::default(); let result = create_secret(Arc::new(cache), Arc::new(event), cmd).await; assert!(result.is_ok()); @@ -614,12 +400,12 @@ mod tests { let mut cache = MockFakeProjectDrivenCache::new(); cache .expect_find_user_permission() - .return_once(|_, _| Ok(Some(ProjectUserCache::default()))); + .return_once(|_, _| Ok(Some(ProjectUser::default()))); cache.expect_find_by_id().return_once(|_| Ok(None)); let event = MockFakeEventDrivenBridge::new(); - let cmd = CreateProjectSecretCmd::default(); + let cmd = CreateSecretCmd::default(); let result = create_secret(Arc::new(cache), Arc::new(event), cmd).await; assert!(result.is_err()); @@ -629,7 +415,7 @@ mod tests { let cache = MockFakeProjectDrivenCache::new(); let event = MockFakeEventDrivenBridge::new(); - let cmd = CreateProjectSecretCmd { + let cmd = CreateSecretCmd { credential: Credential::ApiKey("xxxx".into()), ..Default::default() }; @@ -646,31 +432,22 @@ mod tests { let event = MockFakeEventDrivenBridge::new(); - let cmd = CreateProjectSecretCmd::default(); + let cmd = CreateSecretCmd::default(); let result = create_secret(Arc::new(cache), Arc::new(event), cmd).await; assert!(result.is_err()); } - #[tokio::test] - async fn it_should_create_project_secret_cache() { - let mut cache = MockFakeProjectDrivenCache::new(); - cache.expect_create_secret().return_once(|_| Ok(())); - - let evt = ProjectSecretCreated::default(); - - let result = create_secret_cache(Arc::new(cache), evt).await; - assert!(result.is_ok()); - } - #[tokio::test] async fn it_should_verify_secret() { let mut cache = MockFakeProjectDrivenCache::new(); cache .expect_find_secret_by_project_id() - .return_once(|_| Ok(vec![ProjectSecretCache::default()])); + .return_once(|_| Ok(vec![ProjectSecret::default()])); - let result = verify_secret(Arc::new(cache), Default::default(), KEY).await; + let cmd = VerifySecretCmd::default(); + + let result = verify_secret(Arc::new(cache), cmd).await; assert!(result.is_ok()); } #[tokio::test] @@ -678,23 +455,38 @@ mod tests { let mut cache = MockFakeProjectDrivenCache::new(); cache .expect_find_secret_by_project_id() - .return_once(|_| Ok(vec![ProjectSecretCache::default()])); + .return_once(|_| Ok(vec![ProjectSecret::default()])); - let result = verify_secret(Arc::new(cache), Default::default(), INVALID_KEY).await; + let cmd = VerifySecretCmd { + key: INVALID_KEY.into(), + ..Default::default() + }; + + let result = verify_secret(Arc::new(cache), cmd).await; assert!(result.is_err()); } #[tokio::test] async fn it_should_fail_verify_secret_when_invalid_bech32() { let cache = MockFakeProjectDrivenCache::new(); - let result = verify_secret(Arc::new(cache), Default::default(), "invalid bech32").await; + let cmd = VerifySecretCmd { + key: "invalid bech32".into(), + ..Default::default() + }; + + let result = verify_secret(Arc::new(cache), cmd).await; assert!(result.is_err()); } #[tokio::test] async fn it_should_fail_verify_secret_when_invalid_bech32_hrp() { let cache = MockFakeProjectDrivenCache::new(); - let result = verify_secret(Arc::new(cache), Default::default(), INVALID_HRP_KEY).await; + let cmd = VerifySecretCmd { + key: INVALID_HRP_KEY.into(), + ..Default::default() + }; + + let result = verify_secret(Arc::new(cache), cmd).await; assert!(result.is_err()); } #[tokio::test] @@ -704,32 +496,9 @@ mod tests { .expect_find_secret_by_project_id() .return_once(|_| Ok(vec![])); - let result = verify_secret(Arc::new(cache), Default::default(), KEY).await; - assert!(result.is_err()); - } - - #[tokio::test] - async fn it_should_apply_manifest() { - let mut cluster = MockFakeProjectDrivenCluster::new(); - cluster.expect_create().return_once(|_| Ok(())); - cluster.expect_find_by_name().return_once(|_| Ok(None)); - - let project = ProjectCreated::default(); - - let result = apply_manifest(Arc::new(cluster), project).await; - assert!(result.is_ok()); - } - #[tokio::test] - async fn it_should_fail_apply_manifest_when_resource_exists() { - let mut cluster = MockFakeProjectDrivenCluster::new(); - cluster.expect_create().return_once(|_| Ok(())); - cluster - .expect_find_by_name() - .return_once(|_| Ok(Some(Namespace::default()))); - - let project = ProjectCreated::default(); + let cmd = VerifySecretCmd::default(); - let result = apply_manifest(Arc::new(cluster), project).await; + let result = verify_secret(Arc::new(cache), cmd).await; assert!(result.is_err()); } } diff --git a/src/domain/project/mod.rs b/src/domain/project/mod.rs new file mode 100644 index 0000000..31c73d3 --- /dev/null +++ b/src/domain/project/mod.rs @@ -0,0 +1,134 @@ +use std::{fmt::Display, str::FromStr}; + +use anyhow::{bail, Error}; +use chrono::{DateTime, Utc}; + +use super::event::{ProjectCreated, ProjectSecretCreated}; + +pub mod cache; +pub mod cluster; +pub mod command; + +#[derive(Debug)] +pub struct Project { + pub id: String, + pub name: String, + pub namespace: String, + pub owner: String, + pub status: ProjectStatus, + pub created_at: DateTime, + pub updated_at: DateTime, +} +impl TryFrom for Project { + type Error = Error; + + fn try_from(value: ProjectCreated) -> std::result::Result { + Ok(Self { + id: value.id, + namespace: value.namespace, + name: value.name, + owner: value.owner, + status: value.status.parse()?, + created_at: value.created_at, + updated_at: value.updated_at, + }) + } +} + +#[derive(Debug)] +pub enum ProjectStatus { + Active, + Deleted, +} +impl FromStr for ProjectStatus { + type Err = Error; + + fn from_str(s: &str) -> std::result::Result { + match s { + "active" => Ok(ProjectStatus::Active), + "deleted" => Ok(ProjectStatus::Deleted), + _ => bail!("project status not supported"), + } + } +} +impl Display for ProjectStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ProjectStatus::Active => write!(f, "active"), + ProjectStatus::Deleted => write!(f, "deleted"), + } + } +} + +#[derive(Debug)] +pub struct ProjectSecret { + pub id: String, + pub project_id: String, + pub name: String, + pub phc: String, + pub secret: Vec, + pub created_at: DateTime, +} +impl From for ProjectSecret { + fn from(value: ProjectSecretCreated) -> Self { + Self { + id: value.id, + project_id: value.project_id, + name: value.name, + phc: value.phc, + secret: value.secret, + created_at: value.created_at, + } + } +} + +#[allow(dead_code)] +pub struct ProjectUser { + pub user_id: String, + pub project_id: String, + pub created_at: DateTime, +} + +#[cfg(test)] +mod tests { + use uuid::Uuid; + + use crate::domain::tests::{PHC, SECRET}; + + use super::*; + + impl Default for Project { + fn default() -> Self { + Self { + id: Uuid::new_v4().to_string(), + name: "New Project".into(), + namespace: "sonic-vegas".into(), + owner: "user id".into(), + status: ProjectStatus::Active, + created_at: Utc::now(), + updated_at: Utc::now(), + } + } + } + impl Default for ProjectSecret { + fn default() -> Self { + Self { + id: Uuid::new_v4().to_string(), + project_id: Uuid::new_v4().to_string(), + name: "Key 1".into(), + phc: PHC.into(), + secret: SECRET.as_bytes().to_vec(), + created_at: Utc::now(), + } + } + } + impl Default for ProjectUser { + fn default() -> Self { + Self { + user_id: Uuid::new_v4().to_string(), + project_id: Uuid::new_v4().to_string(), + created_at: Utc::now(), + } + } + } +} diff --git a/src/domain/resource.rs b/src/domain/resource.rs deleted file mode 100644 index 24b5115..0000000 --- a/src/domain/resource.rs +++ /dev/null @@ -1,433 +0,0 @@ -use anyhow::{bail, ensure, Result}; -use chrono::{DateTime, Utc}; -use kube::{ - api::{ApiResource, DynamicObject, ObjectMeta}, - ResourceExt, -}; -use std::sync::Arc; -use tracing::info; -use uuid::Uuid; - -use crate::domain::{PAGE_SIZE_DEFAULT, PAGE_SIZE_MAX}; - -use super::{ - auth::Credential, - event::{EventDrivenBridge, ResourceCreated}, - project::ProjectDrivenCache, -}; - -pub async fn create( - cache: Arc, - event: Arc, - cmd: CreateResourceCmd, -) -> Result<()> { - assert_permission(cache.clone(), &cmd.credential, &cmd.project_id).await?; - - let Some(project) = cache.find_by_id(&cmd.project_id).await? else { - bail!("project doesnt exist") - }; - - let evt = ResourceCreated { - id: cmd.id, - project_id: project.id, - project_namespace: project.namespace, - kind: cmd.kind.clone(), - data: cmd.data, - created_at: Utc::now(), - updated_at: Utc::now(), - }; - - event.dispatch(evt.into()).await?; - info!(resource = cmd.kind, "new resource created"); - - Ok(()) -} - -pub async fn create_cache(cache: Arc, evt: ResourceCreated) -> Result<()> { - cache.create(&evt.into()).await?; - - Ok(()) -} - -pub async fn find_cache( - project_cache: Arc, - resource_cache: Arc, - cmd: FindResourcdCmd, -) -> Result> { - assert_permission(project_cache.clone(), &cmd.credential, &cmd.project_id).await?; - - resource_cache - .find(&cmd.project_id, &cmd.page, &cmd.page_size) - .await -} - -pub async fn apply_manifest( - cluster: Arc, - evt: ResourceCreated, -) -> Result<()> { - let api = ApiResource { - kind: evt.kind.clone(), - group: "demeter.run".into(), - version: "v1alpha1".into(), - plural: format!("{}s", evt.kind.clone().to_lowercase()), - api_version: "demeter.run/v1alpha1".into(), - }; - - let mut obj = DynamicObject::new(&evt.id, &api); - obj.metadata = ObjectMeta { - name: Some(evt.id), - namespace: Some(evt.project_namespace), - ..Default::default() - }; - obj.data = serde_json::from_str(&evt.data)?; - - cluster.create(&obj).await?; - - //TODO: create event to update cache - info!(resource = obj.name_any(), "new resource created"); - - Ok(()) -} - -async fn assert_permission( - cache: Arc, - credential: &Credential, - project_id: &str, -) -> Result<()> { - match credential { - Credential::Auth0(user_id) => { - let result = cache.find_user_permission(user_id, project_id).await?; - ensure!(result.is_some(), "user doesnt have permission"); - Ok(()) - } - Credential::ApiKey(secret_project_id) => { - ensure!( - project_id == secret_project_id, - "secret doesnt have permission" - ); - - Ok(()) - } - } -} - -#[derive(Debug, Clone)] -pub struct CreateResourceCmd { - pub credential: Credential, - pub id: String, - pub project_id: String, - pub kind: String, - pub data: String, -} -impl CreateResourceCmd { - pub fn new(credential: Credential, project_id: String, kind: String, data: String) -> Self { - let id = Uuid::new_v4().to_string(); - - Self { - credential, - id, - project_id, - kind, - data, - } - } -} - -#[derive(Debug, Clone)] -pub struct FindResourcdCmd { - pub credential: Credential, - pub project_id: String, - pub page: u32, - pub page_size: u32, -} -impl FindResourcdCmd { - pub fn new( - credential: Credential, - project_id: String, - page: Option, - page_size: Option, - ) -> Result { - let page = page.unwrap_or(1); - let page_size = page_size.unwrap_or(PAGE_SIZE_DEFAULT); - - ensure!( - page_size <= PAGE_SIZE_MAX, - "page_size exceeded the limit of {PAGE_SIZE_MAX}" - ); - - Ok(Self { - credential, - project_id, - page, - page_size, - }) - } -} - -pub struct ResourceCache { - pub id: String, - pub project_id: String, - pub kind: String, - pub data: String, - pub created_at: DateTime, - pub updated_at: DateTime, -} -impl From for ResourceCache { - fn from(value: ResourceCreated) -> Self { - Self { - id: value.id, - project_id: value.project_id, - kind: value.kind, - data: value.data, - created_at: value.created_at, - updated_at: value.updated_at, - } - } -} - -#[async_trait::async_trait] -pub trait ResourceDrivenCache: Send + Sync { - async fn find( - &self, - project_id: &str, - page: &u32, - page_size: &u32, - ) -> Result>; - async fn create(&self, resource: &ResourceCache) -> Result<()>; -} - -#[async_trait::async_trait] -pub trait ResourceDrivenCluster: Send + Sync { - async fn create(&self, obj: &DynamicObject) -> Result<()>; -} - -#[cfg(test)] -mod tests { - use mockall::mock; - use uuid::Uuid; - - use crate::domain::event::Event; - use crate::domain::project::{ProjectCache, ProjectSecretCache, ProjectUserCache}; - - use super::*; - - mock! { - pub FakeProjectDrivenCache { } - - #[async_trait::async_trait] - impl ProjectDrivenCache for FakeProjectDrivenCache { - async fn find(&self, user_id: &str, page: &u32, page_size: &u32) -> Result>; - async fn find_by_namespace(&self, namespace: &str) -> Result>; - async fn find_by_id(&self, id: &str) -> Result>; - async fn create(&self, project: &ProjectCache) -> Result<()>; - async fn create_secret(&self, secret: &ProjectSecretCache) -> Result<()>; - async fn find_secret_by_project_id(&self, project_id: &str) -> Result>; - async fn find_user_permission(&self,user_id: &str, project_id: &str) -> Result>; - } - } - - mock! { - pub FakeResourceDrivenCache { } - - #[async_trait::async_trait] - impl ResourceDrivenCache for FakeResourceDrivenCache { - async fn find(&self,project_id: &str,page: &u32,page_size: &u32) -> Result>; - async fn create(&self, resource: &ResourceCache) -> Result<()>; - } - } - - mock! { - pub FakeEventDrivenBridge { } - - #[async_trait::async_trait] - impl EventDrivenBridge for FakeEventDrivenBridge { - async fn dispatch(&self, event: Event) -> Result<()>; - } - } - - mock! { - pub FakeResourceDrivenCluster { } - - #[async_trait::async_trait] - impl ResourceDrivenCluster for FakeResourceDrivenCluster { - async fn create(&self, obj: &DynamicObject) -> Result<()>; - } - } - - impl Default for CreateResourceCmd { - fn default() -> Self { - Self { - credential: Credential::Auth0("user id".into()), - id: Uuid::new_v4().to_string(), - project_id: Uuid::new_v4().to_string(), - kind: "CardanoNode".into(), - data: "{\"spec\":{\"operatorVersion\":\"1\",\"kupoVersion\":\"v1\",\"network\":\"mainnet\",\"pruneUtxo\":false,\"throughputTier\":\"0\"}}".into(), - } - } - } - impl Default for FindResourcdCmd { - fn default() -> Self { - Self { - credential: Credential::Auth0("user id".into()), - project_id: Uuid::new_v4().to_string(), - page: 1, - page_size: 12, - } - } - } - impl Default for ResourceCreated { - fn default() -> Self { - Self { - id: Uuid::new_v4().to_string(), - project_id: Uuid::new_v4().to_string(), - project_namespace: "prj-test".into(), - kind: "CardanoNode".into(), - data: "{\"spec\":{\"operatorVersion\":\"1\",\"kupoVersion\":\"v1\",\"network\":\"mainnet\",\"pruneUtxo\":false,\"throughputTier\":\"0\"}}".into(), - created_at: Utc::now(), - updated_at: Utc::now(), - } - } - } - impl Default for ResourceCache { - fn default() -> Self { - Self { - id: Uuid::new_v4().to_string(), - project_id: Uuid::new_v4().to_string(), - kind: "CardanoNode".into(), - data: "{\"spec\":{\"operatorVersion\":\"1\",\"kupoVersion\":\"v1\",\"network\":\"mainnet\",\"pruneUtxo\":false,\"throughputTier\":\"0\"}}".into(), - created_at: Utc::now(), - updated_at: Utc::now(), - } - } - } - - #[tokio::test] - async fn it_should_create_resource() { - let mut cache = MockFakeProjectDrivenCache::new(); - cache - .expect_find_user_permission() - .return_once(|_, _| Ok(Some(ProjectUserCache::default()))); - cache - .expect_find_by_id() - .return_once(|_| Ok(Some(ProjectCache::default()))); - - let mut event = MockFakeEventDrivenBridge::new(); - event.expect_dispatch().return_once(|_| Ok(())); - - let cmd = CreateResourceCmd::default(); - - let result = create(Arc::new(cache), Arc::new(event), cmd).await; - - assert!(result.is_ok()); - } - #[tokio::test] - async fn it_should_fail_create_resource_when_project_doesnt_exist() { - let mut cache = MockFakeProjectDrivenCache::new(); - cache - .expect_find_user_permission() - .return_once(|_, _| Ok(Some(ProjectUserCache::default()))); - cache.expect_find_by_id().return_once(|_| Ok(None)); - - let event = MockFakeEventDrivenBridge::new(); - - let cmd = CreateResourceCmd::default(); - - let result = create(Arc::new(cache), Arc::new(event), cmd).await; - assert!(result.is_err()); - } - #[tokio::test] - async fn it_should_fail_create_resource_when_user_doesnt_have_permission() { - let mut cache = MockFakeProjectDrivenCache::new(); - cache - .expect_find_user_permission() - .return_once(|_, _| Ok(None)); - - let event = MockFakeEventDrivenBridge::new(); - - let cmd = CreateResourceCmd::default(); - - let result = create(Arc::new(cache), Arc::new(event), cmd).await; - assert!(result.is_err()); - } - #[tokio::test] - async fn it_should_fail_create_resource_when_secret_doesnt_have_permission() { - let cache = MockFakeProjectDrivenCache::new(); - - let event = MockFakeEventDrivenBridge::new(); - - let cmd = CreateResourceCmd { - credential: Credential::ApiKey(Uuid::new_v4().to_string()), - ..Default::default() - }; - - let result = create(Arc::new(cache), Arc::new(event), cmd).await; - assert!(result.is_err()); - } - - #[tokio::test] - async fn it_should_find_project_resources_cache() { - let mut project_cache = MockFakeProjectDrivenCache::new(); - project_cache - .expect_find_user_permission() - .return_once(|_, _| Ok(Some(ProjectUserCache::default()))); - - let mut resource_cache = MockFakeResourceDrivenCache::new(); - resource_cache - .expect_find() - .return_once(|_, _, _| Ok(vec![ResourceCache::default()])); - - let evt = FindResourcdCmd::default(); - - let result = find_cache(Arc::new(project_cache), Arc::new(resource_cache), evt).await; - assert!(result.is_ok()); - } - #[tokio::test] - async fn it_should_fail_find_project_resources_when_user_doesnt_have_permission() { - let mut project_cache = MockFakeProjectDrivenCache::new(); - project_cache - .expect_find_user_permission() - .return_once(|_, _| Ok(None)); - - let resource_cache = MockFakeResourceDrivenCache::new(); - - let cmd = FindResourcdCmd::default(); - - let result = find_cache(Arc::new(project_cache), Arc::new(resource_cache), cmd).await; - assert!(result.is_err()); - } - #[tokio::test] - async fn it_should_fail_find_project_resources_when_secret_doesnt_have_permission() { - let project_cache = MockFakeProjectDrivenCache::new(); - let resource_cache = MockFakeResourceDrivenCache::new(); - - let cmd = FindResourcdCmd { - credential: Credential::ApiKey(Uuid::new_v4().to_string()), - ..Default::default() - }; - - let result = find_cache(Arc::new(project_cache), Arc::new(resource_cache), cmd).await; - assert!(result.is_err()); - } - - #[tokio::test] - async fn it_should_create_resource_cache() { - let mut cache = MockFakeResourceDrivenCache::new(); - cache.expect_create().return_once(|_| Ok(())); - - let evt = ResourceCreated::default(); - - let result = create_cache(Arc::new(cache), evt).await; - assert!(result.is_ok()); - } - - #[tokio::test] - async fn it_should_apply_manifest() { - let mut cluster = MockFakeResourceDrivenCluster::new(); - cluster.expect_create().return_once(|_| Ok(())); - - let evt = ResourceCreated::default(); - - let result = apply_manifest(Arc::new(cluster), evt).await; - assert!(result.is_ok()); - } -} diff --git a/src/domain/resource/cache.rs b/src/domain/resource/cache.rs new file mode 100644 index 0000000..8ed39df --- /dev/null +++ b/src/domain/resource/cache.rs @@ -0,0 +1,45 @@ +use std::sync::Arc; + +use crate::domain::event::ResourceCreated; + +use super::Resource; + +use anyhow::Result; + +#[async_trait::async_trait] +pub trait ResourceDrivenCache: Send + Sync { + async fn find(&self, project_id: &str, page: &u32, page_size: &u32) -> Result>; + async fn create(&self, resource: &Resource) -> Result<()>; +} + +pub async fn create(cache: Arc, evt: ResourceCreated) -> Result<()> { + cache.create(&evt.into()).await +} + +#[cfg(test)] +mod tests { + use mockall::mock; + + use super::*; + + mock! { + pub FakeResourceDrivenCache { } + + #[async_trait::async_trait] + impl ResourceDrivenCache for FakeResourceDrivenCache { + async fn find(&self,project_id: &str,page: &u32,page_size: &u32) -> Result>; + async fn create(&self, resource: &Resource) -> Result<()>; + } + } + + #[tokio::test] + async fn it_should_create_resource_cache() { + let mut cache = MockFakeResourceDrivenCache::new(); + cache.expect_create().return_once(|_| Ok(())); + + let evt = ResourceCreated::default(); + + let result = create(Arc::new(cache), evt).await; + assert!(result.is_ok()); + } +} diff --git a/src/domain/resource/cluster.rs b/src/domain/resource/cluster.rs new file mode 100644 index 0000000..9f1519b --- /dev/null +++ b/src/domain/resource/cluster.rs @@ -0,0 +1,70 @@ +use std::sync::Arc; + +use anyhow::Result; +use kube::{ + api::{ApiResource, DynamicObject, ObjectMeta}, + ResourceExt, +}; +use tracing::info; + +use crate::domain::event::ResourceCreated; + +#[async_trait::async_trait] +pub trait ResourceDrivenCluster: Send + Sync { + async fn create(&self, obj: &DynamicObject) -> Result<()>; +} + +pub async fn apply_manifest( + cluster: Arc, + evt: ResourceCreated, +) -> Result<()> { + let api = ApiResource { + kind: evt.kind.clone(), + group: "demeter.run".into(), + version: "v1alpha1".into(), + plural: format!("{}s", evt.kind.clone().to_lowercase()), + api_version: "demeter.run/v1alpha1".into(), + }; + + let mut obj = DynamicObject::new(&evt.id, &api); + obj.metadata = ObjectMeta { + name: Some(evt.id), + namespace: Some(evt.project_namespace), + ..Default::default() + }; + obj.data = serde_json::from_str(&evt.data)?; + + cluster.create(&obj).await?; + + //TODO: create event to update cache + info!(resource = obj.name_any(), "new resource created"); + + Ok(()) +} + +#[cfg(test)] +mod tests { + use mockall::mock; + + use super::*; + + mock! { + pub FakeResourceDrivenCluster { } + + #[async_trait::async_trait] + impl ResourceDrivenCluster for FakeResourceDrivenCluster { + async fn create(&self, obj: &DynamicObject) -> Result<()>; + } + } + + #[tokio::test] + async fn it_should_apply_manifest() { + let mut cluster = MockFakeResourceDrivenCluster::new(); + cluster.expect_create().return_once(|_| Ok(())); + + let evt = ResourceCreated::default(); + + let result = apply_manifest(Arc::new(cluster), evt).await; + assert!(result.is_ok()); + } +} diff --git a/src/domain/resource/command.rs b/src/domain/resource/command.rs new file mode 100644 index 0000000..b0e29d9 --- /dev/null +++ b/src/domain/resource/command.rs @@ -0,0 +1,305 @@ +use std::sync::Arc; + +use anyhow::{bail, ensure, Result}; +use chrono::Utc; +use tracing::info; +use uuid::Uuid; + +use crate::domain::{ + auth::Credential, + event::{EventDrivenBridge, ResourceCreated}, + project::cache::ProjectDrivenCache, + PAGE_SIZE_DEFAULT, PAGE_SIZE_MAX, +}; + +use super::{cache::ResourceDrivenCache, Resource}; + +pub async fn fetch( + project_cache: Arc, + resource_cache: Arc, + cmd: FetchCmd, +) -> Result> { + assert_permission(project_cache.clone(), &cmd.credential, &cmd.project_id).await?; + + resource_cache + .find(&cmd.project_id, &cmd.page, &cmd.page_size) + .await +} + +pub async fn create( + project_cache: Arc, + event: Arc, + cmd: CreateCmd, +) -> Result<()> { + assert_permission(project_cache.clone(), &cmd.credential, &cmd.project_id).await?; + + let Some(project) = project_cache.find_by_id(&cmd.project_id).await? else { + bail!("project doesnt exist") + }; + + let evt = ResourceCreated { + id: cmd.id, + project_id: project.id, + project_namespace: project.namespace, + kind: cmd.kind.clone(), + data: cmd.data, + created_at: Utc::now(), + updated_at: Utc::now(), + }; + + event.dispatch(evt.into()).await?; + info!(resource = cmd.kind, "new resource created"); + + Ok(()) +} + +async fn assert_permission( + project_cache: Arc, + credential: &Credential, + project_id: &str, +) -> Result<()> { + match credential { + Credential::Auth0(user_id) => { + let result = project_cache + .find_user_permission(user_id, project_id) + .await?; + ensure!(result.is_some(), "user doesnt have permission"); + Ok(()) + } + Credential::ApiKey(secret_project_id) => { + ensure!( + project_id == secret_project_id, + "secret doesnt have permission" + ); + + Ok(()) + } + } +} + +#[derive(Debug, Clone)] +pub struct CreateCmd { + pub credential: Credential, + pub id: String, + pub project_id: String, + pub kind: String, + pub data: String, +} +impl CreateCmd { + pub fn new(credential: Credential, project_id: String, kind: String, data: String) -> Self { + let id = Uuid::new_v4().to_string(); + + Self { + credential, + id, + project_id, + kind, + data, + } + } +} + +#[derive(Debug, Clone)] +pub struct FetchCmd { + pub credential: Credential, + pub project_id: String, + pub page: u32, + pub page_size: u32, +} +impl FetchCmd { + pub fn new( + credential: Credential, + project_id: String, + page: Option, + page_size: Option, + ) -> Result { + let page = page.unwrap_or(1); + let page_size = page_size.unwrap_or(PAGE_SIZE_DEFAULT); + + ensure!( + page_size <= PAGE_SIZE_MAX, + "page_size exceeded the limit of {PAGE_SIZE_MAX}" + ); + + Ok(Self { + credential, + project_id, + page, + page_size, + }) + } +} + +#[cfg(test)] +mod tests { + use mockall::mock; + use uuid::Uuid; + + use crate::domain::event::Event; + use crate::domain::project::{Project, ProjectSecret, ProjectUser}; + + use super::*; + + mock! { + pub FakeProjectDrivenCache { } + + #[async_trait::async_trait] + impl ProjectDrivenCache for FakeProjectDrivenCache { + async fn find(&self, user_id: &str, page: &u32, page_size: &u32) -> Result>; + async fn find_by_namespace(&self, namespace: &str) -> Result>; + async fn find_by_id(&self, id: &str) -> Result>; + async fn create(&self, project: &Project) -> Result<()>; + async fn create_secret(&self, secret: &ProjectSecret) -> Result<()>; + async fn find_secret_by_project_id(&self, project_id: &str) -> Result>; + async fn find_user_permission(&self,user_id: &str, project_id: &str) -> Result>; + } + } + + mock! { + pub FakeResourceDrivenCache { } + + #[async_trait::async_trait] + impl ResourceDrivenCache for FakeResourceDrivenCache { + async fn find(&self,project_id: &str,page: &u32,page_size: &u32) -> Result>; + async fn create(&self, resource: &Resource) -> Result<()>; + } + } + + mock! { + pub FakeEventDrivenBridge { } + + #[async_trait::async_trait] + impl EventDrivenBridge for FakeEventDrivenBridge { + async fn dispatch(&self, event: Event) -> Result<()>; + } + } + + impl Default for CreateCmd { + fn default() -> Self { + Self { + credential: Credential::Auth0("user id".into()), + id: Uuid::new_v4().to_string(), + project_id: Uuid::new_v4().to_string(), + kind: "CardanoNode".into(), + data: "{\"spec\":{\"operatorVersion\":\"1\",\"kupoVersion\":\"v1\",\"network\":\"mainnet\",\"pruneUtxo\":false,\"throughputTier\":\"0\"}}".into(), + } + } + } + impl Default for FetchCmd { + fn default() -> Self { + Self { + credential: Credential::Auth0("user id".into()), + project_id: Uuid::new_v4().to_string(), + page: 1, + page_size: 12, + } + } + } + #[tokio::test] + async fn it_should_fetch_project_resources() { + let mut project_cache = MockFakeProjectDrivenCache::new(); + project_cache + .expect_find_user_permission() + .return_once(|_, _| Ok(Some(ProjectUser::default()))); + + let mut resource_cache = MockFakeResourceDrivenCache::new(); + resource_cache + .expect_find() + .return_once(|_, _, _| Ok(vec![Resource::default()])); + + let cmd = FetchCmd::default(); + + let result = fetch(Arc::new(project_cache), Arc::new(resource_cache), cmd).await; + assert!(result.is_ok()); + } + #[tokio::test] + async fn it_should_fail_fetch_project_resources_when_user_doesnt_have_permission() { + let mut project_cache = MockFakeProjectDrivenCache::new(); + project_cache + .expect_find_user_permission() + .return_once(|_, _| Ok(None)); + + let resource_cache = MockFakeResourceDrivenCache::new(); + + let cmd = FetchCmd::default(); + + let result = fetch(Arc::new(project_cache), Arc::new(resource_cache), cmd).await; + assert!(result.is_err()); + } + #[tokio::test] + async fn it_should_fail_fetch_project_resources_when_secret_doesnt_have_permission() { + let project_cache = MockFakeProjectDrivenCache::new(); + let resource_cache = MockFakeResourceDrivenCache::new(); + + let cmd = FetchCmd { + credential: Credential::ApiKey(Uuid::new_v4().to_string()), + ..Default::default() + }; + + let result = fetch(Arc::new(project_cache), Arc::new(resource_cache), cmd).await; + assert!(result.is_err()); + } + + #[tokio::test] + async fn it_should_create_resource() { + let mut project_cache = MockFakeProjectDrivenCache::new(); + project_cache + .expect_find_user_permission() + .return_once(|_, _| Ok(Some(ProjectUser::default()))); + project_cache + .expect_find_by_id() + .return_once(|_| Ok(Some(Project::default()))); + + let mut event = MockFakeEventDrivenBridge::new(); + event.expect_dispatch().return_once(|_| Ok(())); + + let cmd = CreateCmd::default(); + + let result = create(Arc::new(project_cache), Arc::new(event), cmd).await; + + assert!(result.is_ok()); + } + #[tokio::test] + async fn it_should_fail_create_resource_when_project_doesnt_exist() { + let mut project_cache = MockFakeProjectDrivenCache::new(); + project_cache + .expect_find_user_permission() + .return_once(|_, _| Ok(Some(ProjectUser::default()))); + project_cache.expect_find_by_id().return_once(|_| Ok(None)); + + let event = MockFakeEventDrivenBridge::new(); + + let cmd = CreateCmd::default(); + + let result = create(Arc::new(project_cache), Arc::new(event), cmd).await; + assert!(result.is_err()); + } + #[tokio::test] + async fn it_should_fail_create_resource_when_user_doesnt_have_permission() { + let mut project_cache = MockFakeProjectDrivenCache::new(); + project_cache + .expect_find_user_permission() + .return_once(|_, _| Ok(None)); + + let event = MockFakeEventDrivenBridge::new(); + + let cmd = CreateCmd::default(); + + let result = create(Arc::new(project_cache), Arc::new(event), cmd).await; + assert!(result.is_err()); + } + #[tokio::test] + async fn it_should_fail_create_resource_when_secret_doesnt_have_permission() { + let project_cache = MockFakeProjectDrivenCache::new(); + + let event = MockFakeEventDrivenBridge::new(); + + let cmd = CreateCmd { + credential: Credential::ApiKey(Uuid::new_v4().to_string()), + ..Default::default() + }; + + let result = create(Arc::new(project_cache), Arc::new(event), cmd).await; + assert!(result.is_err()); + } +} diff --git a/src/domain/resource/mod.rs b/src/domain/resource/mod.rs new file mode 100644 index 0000000..036a816 --- /dev/null +++ b/src/domain/resource/mod.rs @@ -0,0 +1,48 @@ +use chrono::{DateTime, Utc}; + +use super::event::ResourceCreated; + +pub mod cache; +pub mod cluster; +pub mod command; + +pub struct Resource { + pub id: String, + pub project_id: String, + pub kind: String, + pub data: String, + pub created_at: DateTime, + pub updated_at: DateTime, +} +impl From for Resource { + fn from(value: ResourceCreated) -> Self { + Self { + id: value.id, + project_id: value.project_id, + kind: value.kind, + data: value.data, + created_at: value.created_at, + updated_at: value.updated_at, + } + } +} + +#[cfg(test)] +mod tests { + use uuid::Uuid; + + use super::*; + + impl Default for Resource { + fn default() -> Self { + Self { + id: Uuid::new_v4().to_string(), + project_id: Uuid::new_v4().to_string(), + kind: "CardanoNode".into(), + data: "{\"spec\":{\"operatorVersion\":\"1\",\"kupoVersion\":\"v1\",\"network\":\"mainnet\",\"pruneUtxo\":false,\"throughputTier\":\"0\"}}".into(), + created_at: Utc::now(), + updated_at: Utc::now(), + } + } + } +} diff --git a/src/driven/cache/project.rs b/src/driven/cache/project.rs index 56e144a..6863762 100644 --- a/src/driven/cache/project.rs +++ b/src/driven/cache/project.rs @@ -2,9 +2,7 @@ use anyhow::{Error, Result}; use sqlx::{sqlite::SqliteRow, FromRow, Row}; use std::sync::Arc; -use crate::domain::project::{ - ProjectCache, ProjectDrivenCache, ProjectSecretCache, ProjectUserCache, -}; +use crate::domain::project::{cache::ProjectDrivenCache, Project, ProjectSecret, ProjectUser}; use super::SqliteCache; @@ -18,10 +16,10 @@ impl SqliteProjectDrivenCache { } #[async_trait::async_trait] impl ProjectDrivenCache for SqliteProjectDrivenCache { - async fn find(&self, user_id: &str, page: &u32, page_size: &u32) -> Result> { + async fn find(&self, user_id: &str, page: &u32, page_size: &u32) -> Result> { let offset = page_size * (page - 1); - let projects = sqlx::query_as::<_, ProjectCache>( + let projects = sqlx::query_as::<_, Project>( r#" SELECT p.id as id, @@ -46,8 +44,8 @@ impl ProjectDrivenCache for SqliteProjectDrivenCache { Ok(projects) } - async fn find_by_namespace(&self, namespace: &str) -> Result> { - let project = sqlx::query_as::<_, ProjectCache>( + async fn find_by_namespace(&self, namespace: &str) -> Result> { + let project = sqlx::query_as::<_, Project>( r#" SELECT id, namespace, name, owner, status, created_at, updated_at FROM project WHERE namespace = $1; @@ -59,8 +57,8 @@ impl ProjectDrivenCache for SqliteProjectDrivenCache { Ok(project) } - async fn find_by_id(&self, id: &str) -> Result> { - let project = sqlx::query_as::<_, ProjectCache>( + async fn find_by_id(&self, id: &str) -> Result> { + let project = sqlx::query_as::<_, Project>( r#" SELECT id, namespace, name, owner, status, created_at, updated_at FROM project WHERE id = $1; @@ -73,7 +71,7 @@ impl ProjectDrivenCache for SqliteProjectDrivenCache { Ok(project) } - async fn create(&self, project: &ProjectCache) -> Result<()> { + async fn create(&self, project: &Project) -> Result<()> { let mut tx = self.sqlite.db.begin().await?; let status = project.status.to_string(); @@ -110,7 +108,7 @@ impl ProjectDrivenCache for SqliteProjectDrivenCache { Ok(()) } - async fn create_secret(&self, secret: &ProjectSecretCache) -> Result<()> { + async fn create_secret(&self, secret: &ProjectSecret) -> Result<()> { sqlx::query!( r#" INSERT INTO project_secret (id, project_id, name, phc, secret, created_at) @@ -128,8 +126,8 @@ impl ProjectDrivenCache for SqliteProjectDrivenCache { Ok(()) } - async fn find_secret_by_project_id(&self, project_id: &str) -> Result> { - let secrets = sqlx::query_as::<_, ProjectSecretCache>( + async fn find_secret_by_project_id(&self, project_id: &str) -> Result> { + let secrets = sqlx::query_as::<_, ProjectSecret>( r#" SELECT id, project_id, name, phc, secret, created_at FROM project_secret WHERE project_id = $1; @@ -145,8 +143,8 @@ impl ProjectDrivenCache for SqliteProjectDrivenCache { &self, user_id: &str, project_id: &str, - ) -> Result> { - let project_user = sqlx::query_as::<_, ProjectUserCache>( + ) -> Result> { + let project_user = sqlx::query_as::<_, ProjectUser>( r#" SELECT user_id, project_id, created_at FROM project_user WHERE user_id = $1 and project_id = $2; @@ -161,7 +159,7 @@ impl ProjectDrivenCache for SqliteProjectDrivenCache { } } -impl FromRow<'_, SqliteRow> for ProjectCache { +impl FromRow<'_, SqliteRow> for Project { fn from_row(row: &SqliteRow) -> sqlx::Result { let status: &str = row.try_get("status")?; @@ -179,7 +177,7 @@ impl FromRow<'_, SqliteRow> for ProjectCache { } } -impl FromRow<'_, SqliteRow> for ProjectSecretCache { +impl FromRow<'_, SqliteRow> for ProjectSecret { fn from_row(row: &SqliteRow) -> sqlx::Result { Ok(Self { id: row.try_get("id")?, @@ -192,7 +190,7 @@ impl FromRow<'_, SqliteRow> for ProjectSecretCache { } } -impl FromRow<'_, SqliteRow> for ProjectUserCache { +impl FromRow<'_, SqliteRow> for ProjectUser { fn from_row(row: &SqliteRow) -> sqlx::Result { Ok(Self { user_id: row.try_get("user_id")?, @@ -214,7 +212,7 @@ mod tests { #[tokio::test] async fn it_should_create_project() { let cache = get_cache().await; - let project = ProjectCache::default(); + let project = Project::default(); let result = cache.create(&project).await; assert!(result.is_ok()); @@ -223,7 +221,7 @@ mod tests { #[tokio::test] async fn it_should_find_user_projects() { let cache = get_cache().await; - let project = ProjectCache::default(); + let project = Project::default(); cache.create(&project).await.unwrap(); let result = cache.find(&project.owner, &1, &12).await; @@ -234,7 +232,7 @@ mod tests { #[tokio::test] async fn it_should_return_none_find_user_projects_invalid_page() { let cache = get_cache().await; - let project = ProjectCache::default(); + let project = Project::default(); cache.create(&project).await.unwrap(); let result = cache.find(&project.owner, &2, &12).await; @@ -254,7 +252,7 @@ mod tests { #[tokio::test] async fn it_should_find_project_by_id() { let cache = get_cache().await; - let project = ProjectCache::default(); + let project = Project::default(); cache.create(&project).await.unwrap(); let result = cache.find_by_id(&project.id).await; @@ -265,7 +263,7 @@ mod tests { #[tokio::test] async fn it_should_return_none_find_project_by_id() { let cache = get_cache().await; - let project = ProjectCache::default(); + let project = Project::default(); let result = cache.find_by_id(&project.id).await; @@ -276,7 +274,7 @@ mod tests { #[tokio::test] async fn it_should_find_project_by_namespace() { let cache = get_cache().await; - let project = ProjectCache::default(); + let project = Project::default(); cache.create(&project).await.unwrap(); let result = cache.find_by_namespace(&project.namespace).await; @@ -287,7 +285,7 @@ mod tests { #[tokio::test] async fn it_should_return_none_find_project_by_namespace() { let cache = get_cache().await; - let project = ProjectCache::default(); + let project = Project::default(); let result = cache.find_by_namespace(&project.namespace).await; @@ -299,10 +297,10 @@ mod tests { async fn it_should_create_project_secret() { let cache = get_cache().await; - let project = ProjectCache::default(); + let project = Project::default(); cache.create(&project).await.unwrap(); - let secret = ProjectSecretCache { + let secret = ProjectSecret { project_id: project.id, ..Default::default() }; @@ -315,10 +313,10 @@ mod tests { async fn it_should_find_secret_by_project_id() { let cache = get_cache().await; - let project = ProjectCache::default(); + let project = Project::default(); cache.create(&project).await.unwrap(); - let secret = ProjectSecretCache { + let secret = ProjectSecret { project_id: project.id.clone(), ..Default::default() }; @@ -334,7 +332,7 @@ mod tests { async fn it_should_find_user_permission() { let cache = get_cache().await; - let project = ProjectCache::default(); + let project = Project::default(); cache.create(&project).await.unwrap(); let result = cache @@ -348,7 +346,7 @@ mod tests { async fn it_should_return_none_find_user_permission() { let cache = get_cache().await; - let project = ProjectCache::default(); + let project = Project::default(); let result = cache .find_user_permission(&project.owner, &project.id) diff --git a/src/driven/cache/resource.rs b/src/driven/cache/resource.rs index b6962c4..34065bf 100644 --- a/src/driven/cache/resource.rs +++ b/src/driven/cache/resource.rs @@ -2,7 +2,7 @@ use anyhow::Result; use sqlx::{sqlite::SqliteRow, FromRow, Row}; use std::sync::Arc; -use crate::domain::resource::{ResourceCache, ResourceDrivenCache}; +use crate::domain::resource::{cache::ResourceDrivenCache, Resource}; use super::SqliteCache; @@ -16,7 +16,7 @@ impl SqliteResourceDrivenCache { } #[async_trait::async_trait] impl ResourceDrivenCache for SqliteResourceDrivenCache { - async fn create(&self, resource: &ResourceCache) -> Result<()> { + async fn create(&self, resource: &Resource) -> Result<()> { sqlx::query!( r#" INSERT INTO resource (id, project_id, kind, data, created_at, updated_at) @@ -34,15 +34,10 @@ impl ResourceDrivenCache for SqliteResourceDrivenCache { Ok(()) } - async fn find( - &self, - project_id: &str, - page: &u32, - page_size: &u32, - ) -> Result> { + async fn find(&self, project_id: &str, page: &u32, page_size: &u32) -> Result> { let offset = page_size * (page - 1); - let resources = sqlx::query_as::<_, ResourceCache>( + let resources = sqlx::query_as::<_, Resource>( r#" SELECT r.id, @@ -67,7 +62,7 @@ impl ResourceDrivenCache for SqliteResourceDrivenCache { } } -impl FromRow<'_, SqliteRow> for ResourceCache { +impl FromRow<'_, SqliteRow> for Resource { fn from_row(row: &SqliteRow) -> sqlx::Result { Ok(Self { id: row.try_get("id")?, @@ -83,17 +78,17 @@ impl FromRow<'_, SqliteRow> for ResourceCache { #[cfg(test)] mod tests { use crate::{ - domain::project::{ProjectCache, ProjectDrivenCache}, + domain::project::{cache::ProjectDrivenCache, Project}, driven::cache::project::SqliteProjectDrivenCache, }; use super::*; - async fn mock_project(sqlite_cache: Arc) -> ProjectCache { + async fn mock_project(sqlite_cache: Arc) -> Project { let cache: Box = Box::new(SqliteProjectDrivenCache::new(sqlite_cache)); - let project = ProjectCache::default(); + let project = Project::default(); cache.create(&project).await.unwrap(); project @@ -106,7 +101,7 @@ mod tests { let project = mock_project(sqlite_cache.clone()).await; - let resource = ResourceCache { + let resource = Resource { project_id: project.id, ..Default::default() }; @@ -124,7 +119,7 @@ mod tests { let project = mock_project(sqlite_cache.clone()).await; - let resource = ResourceCache { + let resource = Resource { project_id: project.id.clone(), ..Default::default() }; @@ -142,7 +137,7 @@ mod tests { let project = mock_project(sqlite_cache.clone()).await; - let resource = ResourceCache { + let resource = Resource { project_id: project.id.clone(), ..Default::default() }; diff --git a/src/driven/k8s/mod.rs b/src/driven/k8s/mod.rs index 64ade45..4b4c272 100644 --- a/src/driven/k8s/mod.rs +++ b/src/driven/k8s/mod.rs @@ -5,7 +5,9 @@ use kube::{ discovery, Api, Client, ResourceExt, }; -use crate::domain::{project::ProjectDrivenCluster, resource::ResourceDrivenCluster}; +use crate::domain::{ + project::cluster::ProjectDrivenCluster, resource::cluster::ResourceDrivenCluster, +}; pub struct K8sCluster { client: Client, diff --git a/src/drivers/cache/mod.rs b/src/drivers/cache/mod.rs index b050a87..193ce14 100644 --- a/src/drivers/cache/mod.rs +++ b/src/drivers/cache/mod.rs @@ -35,13 +35,13 @@ pub async fn subscribe(config: CacheConfig) -> Result<()> { Ok(event) => { match event { Event::ProjectCreated(evt) => { - project::create_cache(project_cache.clone(), evt).await?; - } - Event::ResourceCreated(evt) => { - resource::create_cache(resource_cache.clone(), evt).await? + project::cache::create(project_cache.clone(), evt).await?; } Event::ProjectSecretCreated(evt) => { - project::create_secret_cache(project_cache.clone(), evt).await?; + project::cache::create_secret(project_cache.clone(), evt).await?; + } + Event::ResourceCreated(evt) => { + resource::cache::create(resource_cache.clone(), evt).await? } }; consumer.commit_message(&message, CommitMode::Async)?; diff --git a/src/drivers/grpc/middlewares/auth.rs b/src/drivers/grpc/middlewares/auth.rs index 86e4f43..ac7f5f8 100644 --- a/src/drivers/grpc/middlewares/auth.rs +++ b/src/drivers/grpc/middlewares/auth.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use crate::{ domain::{ auth::Credential, - project::{self, ProjectDrivenCache}, + project::{self, cache::ProjectDrivenCache}, }, driven::auth::Auth0Provider, }; @@ -51,7 +51,12 @@ impl tonic::service::Interceptor for AuthenticatorImpl { }; return tokio::task::block_in_place(|| { return tokio::runtime::Runtime::new().unwrap().block_on(async { - match project::verify_secret(self.cache.clone(), &project_id, &token).await { + let cmd = project::command::VerifySecretCmd { + key: token, + project_id, + }; + + match project::command::verify_secret(self.cache.clone(), cmd).await { Ok(secret) => { let credential = Credential::ApiKey(secret.project_id); request.extensions_mut().insert(credential); diff --git a/src/drivers/grpc/project.rs b/src/drivers/grpc/project.rs index 40b8bed..a24becd 100644 --- a/src/drivers/grpc/project.rs +++ b/src/drivers/grpc/project.rs @@ -5,10 +5,7 @@ use tonic::{async_trait, Status}; use crate::domain::{ auth::Credential, event::EventDrivenBridge, - project::{ - self, CreateProjectCmd, CreateProjectSecretCmd, FindProjectCmd, ProjectCache, - ProjectDrivenCache, - }, + project::{self, cache::ProjectDrivenCache, Project}, }; pub struct ProjectServiceImpl { @@ -33,10 +30,10 @@ impl ProjectServiceImpl { #[async_trait] impl proto::project_service_server::ProjectService for ProjectServiceImpl { - async fn create_project( + async fn fetch_projects( &self, - request: tonic::Request, - ) -> Result, tonic::Status> { + request: tonic::Request, + ) -> Result, tonic::Status> { let credential = match request.extensions().get::() { Some(credential) => credential.clone(), None => return Err(Status::permission_denied("invalid credential")), @@ -44,26 +41,22 @@ impl proto::project_service_server::ProjectService for ProjectServiceImpl { let req = request.into_inner(); - let cmd = CreateProjectCmd::new(credential, req.name); + let cmd = project::command::FetchCmd::new(credential, req.page, req.page_size) + .map_err(|err| Status::failed_precondition(err.to_string()))?; - let result = project::create(self.cache.clone(), self.event.clone(), cmd.clone()).await; - if let Err(err) = result { - return Err(Status::failed_precondition(err.to_string())); - } + let projects = project::command::fetch(self.cache.clone(), cmd.clone()) + .await + .map_err(|err| Status::failed_precondition(err.to_string()))?; - let message = proto::CreateProjectResponse { - id: cmd.id, - name: cmd.name, - namespace: cmd.namespace, - }; + let records = projects.into_iter().map(|v| v.into()).collect(); + let message = proto::FetchProjectsResponse { records }; Ok(tonic::Response::new(message)) } - - async fn create_project_secret( + async fn create_project( &self, - request: tonic::Request, - ) -> Result, tonic::Status> { + request: tonic::Request, + ) -> Result, tonic::Status> { let credential = match request.extensions().get::() { Some(credential) => credential.clone(), None => return Err(Status::permission_denied("invalid credential")), @@ -71,29 +64,25 @@ impl proto::project_service_server::ProjectService for ProjectServiceImpl { let req = request.into_inner(); - let cmd = - CreateProjectSecretCmd::new(credential, self.secret.clone(), req.project_id, req.name); + let cmd = project::command::CreateCmd::new(credential, req.name); - let result = - project::create_secret(self.cache.clone(), self.event.clone(), cmd.clone()).await; - if let Err(err) = result { - return Err(Status::failed_precondition(err.to_string())); - } + project::command::create(self.cache.clone(), self.event.clone(), cmd.clone()) + .await + .map_err(|err| Status::failed_precondition(err.to_string()))?; - let key = result.unwrap(); - let message = proto::CreateProjectSecretResponse { + let message = proto::CreateProjectResponse { id: cmd.id, name: cmd.name, - key, + namespace: cmd.namespace, }; Ok(tonic::Response::new(message)) } - async fn find_projects( + async fn create_project_secret( &self, - request: tonic::Request, - ) -> Result, tonic::Status> { + request: tonic::Request, + ) -> Result, tonic::Status> { let credential = match request.extensions().get::() { Some(credential) => credential.clone(), None => return Err(Status::permission_denied("invalid credential")), @@ -101,22 +90,30 @@ impl proto::project_service_server::ProjectService for ProjectServiceImpl { let req = request.into_inner(); - let cmd = FindProjectCmd::new(credential, req.page, req.page_size) - .map_err(|err| Status::failed_precondition(err.to_string()))?; + let cmd = project::command::CreateSecretCmd::new( + credential, + self.secret.clone(), + req.project_id, + req.name, + ); - let projects = project::find_cache(self.cache.clone(), cmd.clone()) - .await - .map_err(|err| Status::failed_precondition(err.to_string()))?; + let key = + project::command::create_secret(self.cache.clone(), self.event.clone(), cmd.clone()) + .await + .map_err(|err| Status::failed_precondition(err.to_string()))?; - let records = projects.into_iter().map(|v| v.into()).collect(); - let message = proto::FindProjectsResponse { records }; + let message = proto::CreateProjectSecretResponse { + id: cmd.id, + name: cmd.name, + key, + }; Ok(tonic::Response::new(message)) } } -impl From for proto::Project { - fn from(value: ProjectCache) -> Self { +impl From for proto::Project { + fn from(value: Project) -> Self { Self { id: value.id, name: value.name, diff --git a/src/drivers/grpc/resource.rs b/src/drivers/grpc/resource.rs index ce03b67..dba92da 100644 --- a/src/drivers/grpc/resource.rs +++ b/src/drivers/grpc/resource.rs @@ -5,8 +5,8 @@ use tonic::{async_trait, Status}; use crate::domain::{ auth::Credential, event::EventDrivenBridge, - project::ProjectDrivenCache, - resource::{self, CreateResourceCmd, FindResourcdCmd, ResourceCache, ResourceDrivenCache}, + project::cache::ProjectDrivenCache, + resource::{cache::ResourceDrivenCache, command, Resource}, }; pub struct ResourceServiceImpl { @@ -30,10 +30,10 @@ impl ResourceServiceImpl { #[async_trait] impl proto::resource_service_server::ResourceService for ResourceServiceImpl { - async fn create_resource( + async fn fetch_resources( &self, - request: tonic::Request, - ) -> Result, tonic::Status> { + request: tonic::Request, + ) -> Result, tonic::Status> { let credential = match request.extensions().get::() { Some(credential) => credential.clone(), None => return Err(Status::permission_denied("invalid credential")), @@ -41,24 +41,23 @@ impl proto::resource_service_server::ResourceService for ResourceServiceImpl { let req = request.into_inner(); - let cmd = CreateResourceCmd::new(credential, req.project_id, req.kind, req.data); - let result = - resource::create(self.project_cache.clone(), self.event.clone(), cmd.clone()).await; + let cmd = command::FetchCmd::new(credential, req.project_id, req.page, req.page_size) + .map_err(|err| Status::failed_precondition(err.to_string()))?; - if let Err(err) = result { - return Err(Status::failed_precondition(err.to_string())); - } + let resources = + command::fetch(self.project_cache.clone(), self.resource_cache.clone(), cmd) + .await + .map_err(|err| Status::failed_precondition(err.to_string()))?; + + let records = resources.into_iter().map(|v| v.into()).collect(); + let message = proto::FetchResourcesResponse { records }; - let message = proto::CreateResourceResponse { - id: cmd.id, - kind: cmd.kind, - }; Ok(tonic::Response::new(message)) } - async fn find_resources( + async fn create_resource( &self, - request: tonic::Request, - ) -> Result, tonic::Status> { + request: tonic::Request, + ) -> Result, tonic::Status> { let credential = match request.extensions().get::() { Some(credential) => credential.clone(), None => return Err(Status::permission_denied("invalid credential")), @@ -66,23 +65,23 @@ impl proto::resource_service_server::ResourceService for ResourceServiceImpl { let req = request.into_inner(); - let cmd = FindResourcdCmd::new(credential, req.project_id, req.page, req.page_size) - .map_err(|err| Status::failed_precondition(err.to_string()))?; + let cmd = command::CreateCmd::new(credential, req.project_id, req.kind, req.data); - let resources = - resource::find_cache(self.project_cache.clone(), self.resource_cache.clone(), cmd) - .await - .map_err(|err| Status::failed_precondition(err.to_string()))?; + command::create(self.project_cache.clone(), self.event.clone(), cmd.clone()) + .await + .map_err(|err| Status::failed_precondition(err.to_string()))?; - let records = resources.into_iter().map(|v| v.into()).collect(); - let message = proto::FindResourcesResponse { records }; + let message = proto::CreateResourceResponse { + id: cmd.id, + kind: cmd.kind, + }; Ok(tonic::Response::new(message)) } } -impl From for proto::Resource { - fn from(value: ResourceCache) -> Self { +impl From for proto::Resource { + fn from(value: Resource) -> Self { Self { id: value.id, kind: value.kind, diff --git a/src/drivers/monitor/mod.rs b/src/drivers/monitor/mod.rs index 08d8234..a39274a 100644 --- a/src/drivers/monitor/mod.rs +++ b/src/drivers/monitor/mod.rs @@ -29,10 +29,10 @@ pub async fn subscribe(config: MonitorConfig) -> Result<()> { Ok(event) => { match event { Event::ProjectCreated(evt) => { - project::apply_manifest(cluster.clone(), evt).await?; + project::cluster::apply_manifest(cluster.clone(), evt).await?; } Event::ResourceCreated(evt) => { - resource::apply_manifest(cluster.clone(), evt).await? + resource::cluster::apply_manifest(cluster.clone(), evt).await? } _ => { info!(event = event.key(), "bypass event")