diff --git a/Cargo.lock b/Cargo.lock index fb60422..a92b97c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -151,9 +151,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.82" +version = "0.1.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", @@ -696,9 +696,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.17" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac" +checksum = "b0956a43b323ac1afaffc053ed5c4b7c1f1800bacd1683c353aabbb752515dd3" dependencies = [ "clap_builder", "clap_derive", @@ -706,9 +706,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.17" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73" +checksum = "4d72166dd41634086d5803a47eb71ae740e61d84709c36f3c34110173db3961b" dependencies = [ "anstream", "anstyle", @@ -718,9 +718,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.13" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -965,7 +965,7 @@ dependencies = [ [[package]] name = "dmtri" version = "0.1.0" -source = "git+https://github.com/demeter-run/specs.git#1f99ae4690ffb9f9041a57388c1cfc7f6bbf5cd5" +source = "git+https://github.com/demeter-run/specs.git#12727b71fe3af229ae79d584471b0b4083fbf108" dependencies = [ "bytes", "pbjson", @@ -1075,6 +1075,7 @@ dependencies = [ "dmtri", "dotenv", "futures", + "handlebars", "json-patch", "jsonwebtoken", "k8s-openapi", @@ -1332,6 +1333,20 @@ dependencies = [ "tracing", ] +[[package]] +name = "handlebars" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25b617d1375ef96eeb920ae717e3da34a02fc979fe632c75128350f9e1f74a" +dependencies = [ + "log", + "pest", + "pest_derive", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1652,9 +1667,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da62f120a8a37763efb0cf8fdf264b884c7b8b9ac8660b900c8661030c00e6ba" +checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b" dependencies = [ "bytes", "futures-channel", @@ -1665,7 +1680,6 @@ dependencies = [ "pin-project-lite", "socket2", "tokio", - "tower", "tower-service", "tracing", ] @@ -1920,9 +1934,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.158" +version = "0.2.159" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" +checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" [[package]] name = "libm" @@ -2517,9 +2531,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "powerfmt" @@ -2804,9 +2818,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0884ad60e090bf1345b93da0a5de8923c93884cd03f40dfcfddd3b4bee661853" +checksum = "62871f2d65009c0256aed1b9cfeeb8ac272833c404e13d53d400cd0dad7a2ac0" dependencies = [ "bitflags 2.6.0", ] @@ -3199,9 +3213,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.11.1" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" +checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6" dependencies = [ "core-foundation-sys", "libc", @@ -3743,18 +3757,18 @@ checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" [[package]] name = "thiserror" -version = "1.0.63" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.63" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" dependencies = [ "proc-macro2", "quote", @@ -3939,7 +3953,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.21", + "toml_edit 0.22.22", ] [[package]] @@ -3964,15 +3978,15 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.21" +version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b072cee73c449a636ffd6f32bd8de3a9f7119139aff882f44943ce2986dc5cf" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ "indexmap 2.5.0", "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.18", + "winnow 0.6.19", ] [[package]] @@ -4617,9 +4631,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.18" +version = "0.6.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" +checksum = "c52ac009d615e79296318c1bcce2d422aaca15ad08515e344feeda07df67a587" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index 43ab373..94ec304 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,7 @@ aws-sdk-sesv2 = { version = "1.43.0", features = ["behavior-version-latest"] } clap = { version = "4.5.17", features = ["derive", "env"] } comfy-table = "7.1.1" csv = "1.3.0" +handlebars = "6.1.0" [dev-dependencies] mockall = "0.12.1" diff --git a/bootstrap/rpc/crds.tf b/bootstrap/rpc/crds.tf index d430adb..d3116b1 100644 --- a/bootstrap/rpc/crds.tf +++ b/bootstrap/rpc/crds.tf @@ -5,7 +5,26 @@ resource "kubernetes_config_map_v1" "fabric_rpc_crds" { } data = { - "utxorpcport.json" = "${file("${path.module}/crds/utxorpcport.json")}" - "frontends.json" = "${file("${path.module}/crds/frontends.json")}" + "blockfrostport.hbs" = "${file("${path.module}/crds/blockfrostport.hbs")}" + "blockfrostport.json" = "${file("${path.module}/crds/blockfrostport.json")}" + "cardanonodeport.hbs" = "${file("${path.module}/crds/cardanonodeport.hbs")}" + "cardanonodeport.json" = "${file("${path.module}/crds/cardanonodeport.json")}" + "dbsyncport.hbs" = "${file("${path.module}/crds/dbsyncport.hbs")}" + "dbsyncport.json" = "${file("${path.module}/crds/dbsyncport.json")}" + "frontends.json" = "${file("${path.module}/crds/frontends.json")}" + "kupoport.hbs" = "${file("${path.module}/crds/kupoport.hbs")}" + "kupoport.json" = "${file("${path.module}/crds/kupoport.json")}" + "marloweport.hbs" = "${file("${path.module}/crds/marloweport.hbs")}" + "marloweport.json" = "${file("${path.module}/crds/marloweport.json")}" + "mumakport.hbs" = "${file("${path.module}/crds/mumakport.hbs")}" + "mumakport.json" = "${file("${path.module}/crds/mumakport.json")}" + "ogmiosport.hbs" = "${file("${path.module}/crds/ogmiosport.hbs")}" + "ogmiosport.json" = "${file("${path.module}/crds/ogmiosport.json")}" + "scrollsport.hbs" = "${file("${path.module}/crds/scrollsport.hbs")}" + "scrollsport.json" = "${file("${path.module}/crds/scrollsport.json")}" + "submitapiport.hbs" = "${file("${path.module}/crds/submitapiport.hbs")}" + "submitapiport.json" = "${file("${path.module}/crds/submitapiport.json")}" + "utxorpcport.hbs" = "${file("${path.module}/crds/utxorpcport.hbs")}" + "utxorpcport.json" = "${file("${path.module}/crds/utxorpcport.json")}" } } diff --git a/bootstrap/rpc/crds/blockfrostport.hbs b/bootstrap/rpc/crds/blockfrostport.hbs new file mode 100644 index 0000000..be2cbfb --- /dev/null +++ b/bootstrap/rpc/crds/blockfrostport.hbs @@ -0,0 +1,25 @@ +[ + { + "label": "Network", + "value": "{{network}}" + }, + { + "label": "Throughput Tier", + "value": "{{throughputTier}}" + }, + { + "label": "Endpoint URL", + "value": "https://blockfrost-m1.demeter.run", + "description": "Public URL to access your Blockfrost RYO API" + }, + { + "label": "Authenticated Endpoint URL", + "value": "https://{{authToken}}.blockfrost-m1.demeter.run", + "description": "Authenticated public URL to access your Blockfrost RYO API. Useful if your library does not support adding custom headers" + }, + { + "label": "Auth Token", + "value": "{{authToken}}", + "description": "Api Key to be sent in the header as dmtr-api-key" + } +] diff --git a/bootstrap/rpc/crds/cardanonodeport.hbs b/bootstrap/rpc/crds/cardanonodeport.hbs new file mode 100644 index 0000000..a12c2ee --- /dev/null +++ b/bootstrap/rpc/crds/cardanonodeport.hbs @@ -0,0 +1,15 @@ +[ + { + "label": "Network", + "value": "{{network}}" + }, + { + "label": "Throughput Tier", + "value": "{{throughputTier}}" + }, + { + "label": "Endpoint URL", + "value": "{{authToken}}.cnode-m1.demeter.run", + "description": "Public URL to access your Node. You can use the Demeter CLI to interact with your Node." + }, +] diff --git a/bootstrap/rpc/crds/dbsyncport.hbs b/bootstrap/rpc/crds/dbsyncport.hbs new file mode 100644 index 0000000..6f47200 --- /dev/null +++ b/bootstrap/rpc/crds/dbsyncport.hbs @@ -0,0 +1,40 @@ +[ + { + "label": "Network", + "value": "{{network}}" + }, + { + "label": "Throughput Tier", + "value": "{{throughputTier}}" + }, + { + "label": "Public hostname", + "value": "dbsync-v3.demeter.run", + "description": "Public hostname to use in your PostgreSQL connection" + }, + { + "label": "Public port number", + "value": "5432", + "description": "Public port number to use in your PostgreSQL connection" + }, + { + "label": "Database name", + "value": "dbsync-{{network}}", + "description": "Database name to use in your PostgreSQL connection" + }, + { + "label": "Username", + "value": "{{username}}", + "description": "Username to access the Postgres." + }, + { + "label": "Password", + "value": "{{password}}", + "description": "Password to access the Postgres." + }, + { + "label": "Connection string", + "value": "postgresql://{{username}}:{{password}}@dbsync-v3.demeter.run:5432/dbsync-{{network}}", + "description": "Connection string to use in your PostgreSQL connection" + }, +] diff --git a/bootstrap/rpc/crds/kupoport.hbs b/bootstrap/rpc/crds/kupoport.hbs new file mode 100644 index 0000000..d26f510 --- /dev/null +++ b/bootstrap/rpc/crds/kupoport.hbs @@ -0,0 +1,25 @@ +[ + { + "label": "Network", + "value": "{{network}}" + }, + { + "label": "Throughput Tier", + "value": "{{throughputTier}}" + }, + { + "label": "Endpoint URL", + "value": "https://{{network}}-v2.kupo-m1.demeter.run", + "description": "Public URL to access your Kupo API" + }, + { + "label": "Authenticated Endpoint URL", + "value": "https://{{authToken}}.{{network}}-v2.kupo-m1.demeter.run", + "description": "Authenticated public URL to access your Kupo API. Useful if your library does not support adding custom headers" + }, + { + "label": "Api Key", + "value": "{{authToken}}", + "description": "Api Key to be sent in the header as dmtr-api-key" + } +] diff --git a/bootstrap/rpc/crds/marloweport.hbs b/bootstrap/rpc/crds/marloweport.hbs new file mode 100644 index 0000000..8976591 --- /dev/null +++ b/bootstrap/rpc/crds/marloweport.hbs @@ -0,0 +1,25 @@ +[ + { + "label": "Network", + "value": "{{network}}" + }, + { + "label": "Throughput Tier", + "value": "{{throughputTier}}" + }, + { + "label": "Endpoint URL", + "value": "https://marlowe.demeter.run", + "description": "Public URL to access your Marlowe" + }, + { + "label": "Authenticated Endpoint URL", + "value": "https://{{authToken}}.marlowe.demeter.run", + "description": "Authenticated public URL to access your Marlowe. Useful if your library does not support adding custom headers" + }, + { + "label": "Api Key", + "value": "{{authToken}}", + "description": "Api Key to be sent in the header as dmtr-api-key" + } +] diff --git a/bootstrap/rpc/crds/mumakport.hbs b/bootstrap/rpc/crds/mumakport.hbs new file mode 100644 index 0000000..2145948 --- /dev/null +++ b/bootstrap/rpc/crds/mumakport.hbs @@ -0,0 +1,40 @@ +[ + { + "label": "Network", + "value": "{{network}}" + }, + { + "label": "Throughput Tier", + "value": "{{throughputTier}}" + }, + { + "label": "Public hostname", + "value": "mumak-m0.demeter.run", + "description": "Public hostname to use in your PostgreSQL connection" + }, + { + "label": "Public port number", + "value": "5432", + "description": "Public port number to use in your PostgreSQL connection" + }, + { + "label": "Database name", + "value": "cardano-{{network}}", + "description": "Database name to use in your PostgreSQL connection" + }, + { + "label": "Username", + "value": "{{username}}", + "description": "Username to access the Postgres." + }, + { + "label": "Password", + "value": "{{password}}", + "description": "Password to access the Postgres." + }, + { + "label": "Connection string", + "value": "postgresql://{{username}}:{{password}}@mumak-m0.demeter.run:5432/cardano-{{network}}", + "description": "Connection string to use in your PostgreSQL connection" + }, +] diff --git a/bootstrap/rpc/crds/ogmiosport.hbs b/bootstrap/rpc/crds/ogmiosport.hbs new file mode 100644 index 0000000..1326ee9 --- /dev/null +++ b/bootstrap/rpc/crds/ogmiosport.hbs @@ -0,0 +1,25 @@ +[ + { + "label": "Network", + "value": "{{network}}" + }, + { + "label": "Throughput Tier", + "value": "{{throughputTier}}" + }, + { + "label": "Endpoint URL", + "value": "https://{{network}}-v5.ogmios-m1.demeter.run", + "description": "Public URL to access your Ogmios API" + }, + { + "label": "Authenticated Endpoint URL", + "value": "https://{{authToken}}.{{network}}-v5.ogmios-m1.demeter.run", + "description": "Authenticated public URL to access your Ogmios API. Useful if your library does not support adding custom headers" + }, + { + "label": "Api Key", + "value": "{{authToken}}", + "description": "Api Key to be sent in the header as dmtr-api-key" + } +] diff --git a/bootstrap/rpc/crds/scrollsport.hbs b/bootstrap/rpc/crds/scrollsport.hbs new file mode 100644 index 0000000..ee67138 --- /dev/null +++ b/bootstrap/rpc/crds/scrollsport.hbs @@ -0,0 +1,25 @@ +[ + { + "label": "Network", + "value": "{{network}}" + }, + { + "label": "Throughput Tier", + "value": "{{throughputTier}}" + }, + { + "label": "Endpoint URL", + "value": "https://scrolls-m0.demeter.run", + "description": "Public URL to access Scrolls GraphQL API" + }, + { + "label": "Authenticated Endpoint URL", + "value": "https://{{authToken}}.scrolls-m0.demeter.run", + "description": "Authenticated public URL to access your Scrolls GraphQL API. Useful if your library does not support adding custom headers" + }, + { + "label": "Api Key", + "value": "{{authToken}}", + "description": "Api Key to be sent in the header as dmtr-api-key" + } +] diff --git a/bootstrap/rpc/crds/submitapiport.hbs b/bootstrap/rpc/crds/submitapiport.hbs new file mode 100644 index 0000000..eaa55e2 --- /dev/null +++ b/bootstrap/rpc/crds/submitapiport.hbs @@ -0,0 +1,25 @@ +[ + { + "label": "Network", + "value": "{{network}}" + }, + { + "label": "Throughput Tier", + "value": "{{throughputTier}}" + }, + { + "label": "Endpoint URL", + "value": "https://submitapi-m1.demeter.run", + "description": "Public URL to access your SubmitAPI API" + }, + { + "label": "Authenticated Endpoint URL", + "value": "https://{{authToken}}.submitapi-m1.demeter.run", + "description": "Authenticated public URL to access your SubmitApi API. Useful if your library does not support adding custom headers" + }, + { + "label": "Api Key", + "value": "{{authToken}}", + "description": "Api Key to be sent in the header as dmtr-api-key" + } +] diff --git a/bootstrap/rpc/crds/utxorpcport.hbs b/bootstrap/rpc/crds/utxorpcport.hbs new file mode 100644 index 0000000..c159ed3 --- /dev/null +++ b/bootstrap/rpc/crds/utxorpcport.hbs @@ -0,0 +1,20 @@ +[ + { + "label": "Network", + "value": "{{network}}" + }, + { + "label": "Throughput Tier", + "value": "{{throughputTier}}" + }, + { + "label": "GRPC Endpoint URL", + "value": "{{network}}.utxorpc-v0.demeter.run", + "description": "Public URL to access your UTxO Rpc API" + }, + { + "label": "Api Key", + "value": "{{authToken}}", + "description": "Api Key to be sent with the requests. They key in the request should be dmtr-api-key" + } +] diff --git a/src/domain/error.rs b/src/domain/error.rs index 507f5e4..26abad5 100644 --- a/src/domain/error.rs +++ b/src/domain/error.rs @@ -52,3 +52,8 @@ impl From for Error { Self::Unexpected(value.to_string()) } } +impl From for Error { + fn from(value: handlebars::RenderError) -> Self { + Self::Unexpected(value.to_string()) + } +} diff --git a/src/domain/metadata/mod.rs b/src/domain/metadata/mod.rs index 138f746..e88a865 100644 --- a/src/domain/metadata/mod.rs +++ b/src/domain/metadata/mod.rs @@ -11,6 +11,7 @@ pub mod command; pub trait MetadataDriven: Send + Sync { async fn find(&self) -> Result>; async fn find_by_kind(&self, kind: &str) -> Result>; + fn render_hbs(&self, name: &str, spec: &str) -> Result; } pub enum KnownField { diff --git a/src/domain/resource/command.rs b/src/domain/resource/command.rs index bd233e4..eb38c96 100644 --- a/src/domain/resource/command.rs +++ b/src/domain/resource/command.rs @@ -13,7 +13,7 @@ use crate::domain::{ error::Error, event::{EventDrivenBridge, ResourceCreated, ResourceDeleted}, metadata::{KnownField, MetadataDriven}, - project::{cache::ProjectDrivenCache, Project}, + project::cache::ProjectDrivenCache, resource::{ResourceStatus, ResourceUpdated}, utils::get_schema_from_crd, Result, PAGE_SIZE_DEFAULT, PAGE_SIZE_MAX, @@ -24,31 +24,44 @@ use super::{cache::ResourceDrivenCache, Resource}; pub async fn fetch( project_cache: Arc, resource_cache: Arc, + metadata: Arc, cmd: FetchCmd, ) -> Result> { assert_project_permission(project_cache.clone(), &cmd.credential, &cmd.project_id).await?; - resource_cache + let resources = resource_cache .find(&cmd.project_id, &cmd.page, &cmd.page_size) - .await + .await? + .into_iter() + .map(|mut resource| { + if let Ok(annotations) = + metadata.render_hbs(&resource.kind.to_lowercase(), &resource.spec) + { + resource.annotations = Some(annotations); + } + + resource + }) + .collect(); + + Ok(resources) } pub async fn fetch_by_id( project_cache: Arc, resource_cache: Arc, + metadata: Arc, cmd: FetchByIdCmd, ) -> Result { - assert_project_permission(project_cache.clone(), &cmd.credential, &cmd.project_id).await?; - - let Some(project) = project_cache.find_by_id(&cmd.project_id).await? else { - return Err(Error::CommandMalformed("invalid project id".into())); - }; - - let Some(resource) = resource_cache.find_by_id(&cmd.resource_id).await? else { + let Some(mut resource) = resource_cache.find_by_id(&cmd.id).await? else { return Err(Error::CommandMalformed("invalid resource id".into())); }; - assert_project_resource(&project, &resource)?; + assert_project_permission(project_cache.clone(), &cmd.credential, &resource.project_id).await?; + + if let Ok(annotations) = metadata.render_hbs(&resource.kind.to_lowercase(), &resource.spec) { + resource.annotations = Some(annotations); + } Ok(resource) } @@ -121,6 +134,7 @@ pub async fn update( }; assert_project_permission(project_cache.clone(), &cmd.credential, &resource.project_id).await?; + let Some(project) = project_cache.find_by_id(&resource.project_id).await? else { return Err(Error::CommandMalformed("invalid project id".into())); }; @@ -150,20 +164,18 @@ pub async fn delete( event: Arc, cmd: DeleteCmd, ) -> Result<()> { - assert_project_permission(project_cache.clone(), &cmd.credential, &cmd.project_id).await?; - - let Some(project) = project_cache.find_by_id(&cmd.project_id).await? else { - return Err(Error::CommandMalformed("invalid project id".into())); - }; - - let Some(resource) = resource_cache.find_by_id(&cmd.resource_id).await? else { + let Some(resource) = resource_cache.find_by_id(&cmd.id).await? else { return Err(Error::CommandMalformed("invalid resource id".into())); }; - assert_project_resource(&project, &resource)?; + assert_project_permission(project_cache.clone(), &cmd.credential, &resource.project_id).await?; + + let Some(project) = project_cache.find_by_id(&resource.project_id).await? else { + return Err(Error::CommandMalformed("invalid project id".into())); + }; let evt = ResourceDeleted { - id: cmd.resource_id, + id: cmd.id, kind: resource.kind.clone(), status: ResourceStatus::Deleted.to_string(), project_id: project.id, @@ -177,13 +189,6 @@ pub async fn delete( Ok(()) } -fn assert_project_resource(project: &Project, resource: &Resource) -> Result<()> { - if project.id != resource.project_id { - return Err(Error::CommandMalformed("invalid resource id".into())); - } - Ok(()) -} - pub fn build_key(project_id: &str, resource_id: &str) -> Result> { let argon2 = Argon2::default(); let key = format!("{project_id}{resource_id}").as_bytes().to_vec(); @@ -239,8 +244,7 @@ impl FetchCmd { #[derive(Debug, Clone)] pub struct FetchByIdCmd { pub credential: Credential, - pub project_id: String, - pub resource_id: String, + pub id: String, } pub type Spec = serde_json::value::Map; @@ -285,8 +289,7 @@ impl UpdateCmd { #[derive(Debug, Clone)] pub struct DeleteCmd { pub credential: Credential, - pub project_id: String, - pub resource_id: String, + pub id: String, } #[cfg(test)] @@ -316,8 +319,7 @@ mod tests { fn default() -> Self { Self { credential: Credential::Auth0("user id".into()), - project_id: Uuid::new_v4().to_string(), - resource_id: Uuid::new_v4().to_string(), + id: Uuid::new_v4().to_string(), } } } @@ -336,8 +338,7 @@ mod tests { fn default() -> Self { Self { credential: Credential::Auth0("user id".into()), - resource_id: Uuid::new_v4().to_string(), - project_id: Uuid::new_v4().to_string(), + id: Uuid::new_v4().to_string(), } } } @@ -354,9 +355,21 @@ mod tests { .expect_find() .return_once(|_, _, _| Ok(vec![Resource::default()])); + let mut metadata = MockMetadataDriven::new(); + metadata + .expect_render_hbs() + .return_once(|_, _| Ok("[{}]".into())); + let cmd = FetchCmd::default(); - let result = fetch(Arc::new(project_cache), Arc::new(resource_cache), cmd).await; + let result = fetch( + Arc::new(project_cache), + Arc::new(resource_cache), + Arc::new(metadata), + cmd, + ) + .await; + assert!(result.is_ok()); } #[tokio::test] @@ -368,73 +381,68 @@ mod tests { let resource_cache = MockResourceDrivenCache::new(); + let metadata = MockMetadataDriven::new(); + let cmd = FetchCmd::default(); - let result = fetch(Arc::new(project_cache), Arc::new(resource_cache), cmd).await; + let result = fetch( + Arc::new(project_cache), + Arc::new(resource_cache), + Arc::new(metadata), + cmd, + ) + .await; assert!(result.is_err()); } #[tokio::test] async fn it_should_fail_fetch_project_resources_when_secret_doesnt_have_permission() { let project_cache = MockProjectDrivenCache::new(); let resource_cache = MockResourceDrivenCache::new(); + let metadata = MockMetadataDriven::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; + let result = fetch( + Arc::new(project_cache), + Arc::new(resource_cache), + Arc::new(metadata), + cmd, + ) + .await; assert!(result.is_err()); } #[tokio::test] async fn it_should_fetch_project_resources_by_id() { - let mut project_cache = MockProjectDrivenCache::new(); - project_cache - .expect_find_user_permission() - .return_once(|_, _| Ok(Some(ProjectUser::default()))); - - let project = Project::default(); - - let project_cloned = project.clone(); - project_cache - .expect_find_by_id() - .return_once(|_| Ok(Some(project_cloned))); - let mut resource_cache = MockResourceDrivenCache::new(); - resource_cache.expect_find_by_id().return_once(|_| { - Ok(Some(Resource { - project_id: project.id, - ..Default::default() - })) - }); - - let cmd = FetchByIdCmd::default(); - - let result = fetch_by_id(Arc::new(project_cache), Arc::new(resource_cache), cmd).await; + resource_cache + .expect_find_by_id() + .return_once(|_| Ok(Some(Resource::default()))); - assert!(result.is_ok()); - } - #[tokio::test] - async fn it_should_fail_fetch_project_resources_by_id_when_resource_is_from_other_project() { let mut project_cache = MockProjectDrivenCache::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 resource_cache = MockResourceDrivenCache::new(); - resource_cache - .expect_find_by_id() - .return_once(|_| Ok(Some(Resource::default()))); + let mut metadata = MockMetadataDriven::new(); + metadata + .expect_render_hbs() + .return_once(|_, _| Ok("[{}]".into())); let cmd = FetchByIdCmd::default(); - let result = fetch_by_id(Arc::new(project_cache), Arc::new(resource_cache), cmd).await; + let result = fetch_by_id( + Arc::new(project_cache), + Arc::new(resource_cache), + Arc::new(metadata), + cmd, + ) + .await; - assert!(result.is_err()); + assert!(result.is_ok()); } #[tokio::test] @@ -563,25 +571,18 @@ mod tests { #[tokio::test] async fn it_should_delete_resource() { + let mut resource_cache = MockResourceDrivenCache::new(); + resource_cache + .expect_find_by_id() + .return_once(|_| Ok(Some(Resource::default()))); + let mut project_cache = MockProjectDrivenCache::new(); project_cache .expect_find_user_permission() .return_once(|_, _| Ok(Some(ProjectUser::default()))); - - let project = Project::default(); - - let project_cloned = project.clone(); project_cache .expect_find_by_id() - .return_once(|_| Ok(Some(project_cloned))); - - let mut resource_cache = MockResourceDrivenCache::new(); - resource_cache.expect_find_by_id().return_once(|_| { - Ok(Some(Resource { - project_id: project.id, - ..Default::default() - })) - }); + .return_once(|_| Ok(Some(Project::default()))); let mut event = MockEventDrivenBridge::new(); event.expect_dispatch().return_once(|_| Ok(())); @@ -600,12 +601,16 @@ mod tests { } #[tokio::test] async fn it_should_fail_delete_resource_when_user_doesnt_have_permission() { + let mut resource_cache = MockResourceDrivenCache::new(); + resource_cache + .expect_find_by_id() + .return_once(|_| Ok(Some(Resource::default()))); + let mut project_cache = MockProjectDrivenCache::new(); project_cache .expect_find_user_permission() .return_once(|_, _| Ok(None)); - let resource_cache = MockResourceDrivenCache::new(); let event = MockEventDrivenBridge::new(); let cmd = DeleteCmd::default(); @@ -622,43 +627,18 @@ mod tests { } #[tokio::test] async fn it_should_fail_delete_resource_when_secret_doesnt_have_permission() { - let project_cache = MockProjectDrivenCache::new(); - let resource_cache = MockResourceDrivenCache::new(); - let event = MockEventDrivenBridge::new(); - - let cmd = DeleteCmd { - credential: Credential::ApiKey(Uuid::new_v4().to_string()), - ..Default::default() - }; - - let result = delete( - Arc::new(project_cache), - Arc::new(resource_cache), - Arc::new(event), - cmd, - ) - .await; - - assert!(result.is_err()); - } - #[tokio::test] - async fn it_should_fail_delete_resource_when_resource_is_from_other_project() { - let mut project_cache = MockProjectDrivenCache::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 resource_cache = MockResourceDrivenCache::new(); resource_cache .expect_find_by_id() .return_once(|_| Ok(Some(Resource::default()))); + let project_cache = MockProjectDrivenCache::new(); let event = MockEventDrivenBridge::new(); - let cmd = DeleteCmd::default(); + let cmd = DeleteCmd { + credential: Credential::ApiKey(Uuid::new_v4().to_string()), + ..Default::default() + }; let result = delete( Arc::new(project_cache), diff --git a/src/domain/resource/mod.rs b/src/domain/resource/mod.rs index 93af025..a474ebe 100644 --- a/src/domain/resource/mod.rs +++ b/src/domain/resource/mod.rs @@ -16,6 +16,7 @@ pub struct Resource { pub project_id: String, pub kind: String, pub spec: String, + pub annotations: Option, pub status: ResourceStatus, pub created_at: DateTime, pub updated_at: DateTime, @@ -29,6 +30,7 @@ impl TryFrom for Resource { project_id: value.project_id, kind: value.kind, spec: value.spec, + annotations: None, status: value.status.parse()?, created_at: value.created_at, updated_at: value.updated_at, @@ -92,6 +94,7 @@ mod tests { kind: "CardanoNodePort".into(), spec: "{\"version\":\"stable\",\"network\":\"mainnet\",\"throughputTier\":\"1\"}" .into(), + annotations: None, status: ResourceStatus::Active, created_at: Utc::now(), updated_at: Utc::now(), diff --git a/src/driven/cache/resource.rs b/src/driven/cache/resource.rs index 18e7aa1..143ee93 100644 --- a/src/driven/cache/resource.rs +++ b/src/driven/cache/resource.rs @@ -175,6 +175,7 @@ impl FromRow<'_, SqliteRow> for Resource { project_id: row.try_get("project_id")?, kind: row.try_get("kind")?, spec: row.try_get("spec")?, + annotations: None, status: status .parse() .map_err(|err: Error| sqlx::Error::Decode(err.into()))?, diff --git a/src/driven/metadata/mod.rs b/src/driven/metadata/mod.rs index 04e90c4..e2c7c40 100644 --- a/src/driven/metadata/mod.rs +++ b/src/driven/metadata/mod.rs @@ -5,33 +5,51 @@ use k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomRe use crate::domain::{metadata::MetadataDriven, Result}; -pub struct MetadataCrd { +pub struct Metadata<'a> { crds: Vec, + hbs: handlebars::Handlebars<'a>, } -impl MetadataCrd { +impl<'a> Metadata<'a> { pub fn new(path: &Path) -> AnyhowResult { let dir = fs::read_dir(path)?; let mut crds: Vec = Vec::new(); + let mut hbs = handlebars::Handlebars::new(); for path in dir { let entry = path?; - if entry.path().is_file() - && entry.path().extension().and_then(|e| e.to_str()) == Some("json") - { + if entry.path().is_file() { let file = fs::read(entry.path())?; - let crd: CustomResourceDefinition = serde_json::from_slice(&file)?; - crds.push(crd); + + match entry.path().extension().and_then(|e| e.to_str()) { + Some("json") => { + let crd: CustomResourceDefinition = serde_json::from_slice(&file)?; + crds.push(crd); + } + Some("hbs") => { + let name = entry + .path() + .file_stem() + .unwrap() + .to_str() + .unwrap() + .to_string(); + let template = String::from_utf8(file.clone())?; + + hbs.register_template_string(&name, template)?; + } + _ => continue, + }; } } - Ok(Self { crds }) + Ok(Self { crds, hbs }) } } #[async_trait::async_trait] -impl MetadataDriven for MetadataCrd { +impl<'a> MetadataDriven for Metadata<'a> { async fn find(&self) -> Result> { Ok(self.crds.clone()) } @@ -42,4 +60,12 @@ impl MetadataDriven for MetadataCrd { .into_iter() .find(|crd| crd.spec.names.kind == kind)) } + + fn render_hbs(&self, name: &str, spec: &str) -> Result { + let data: serde_json::Value = serde_json::from_str(spec)?; + let rendered = self.hbs.render(name, &data)?; + let value: serde_json::Value = serde_json::from_str(&rendered.replace('\n', ""))?; + + Ok(value.to_string()) + } } diff --git a/src/drivers/grpc/mod.rs b/src/drivers/grpc/mod.rs index 5605ad3..27155db 100644 --- a/src/drivers/grpc/mod.rs +++ b/src/drivers/grpc/mod.rs @@ -24,7 +24,7 @@ use crate::driven::cache::resource::SqliteResourceDrivenCache; use crate::driven::cache::usage::SqliteUsageDrivenCache; use crate::driven::cache::SqliteCache; use crate::driven::kafka::KafkaProducer; -use crate::driven::metadata::MetadataCrd; +use crate::driven::metadata::Metadata; use crate::driven::ses::SESDrivenImpl; use crate::driven::stripe::StripeDrivenImpl; @@ -42,7 +42,7 @@ pub async fn server(config: GrpcConfig) -> Result<()> { let event_bridge = Arc::new(KafkaProducer::new(&config.topic, &config.kafka)?); - let metadata = Arc::new(MetadataCrd::new(&config.crds_path)?); + let metadata = Arc::new(Metadata::new(&config.crds_path)?); let auth0 = Arc::new( Auth0DrivenImpl::try_new( diff --git a/src/drivers/grpc/resource.rs b/src/drivers/grpc/resource.rs index c59061a..b2f3f81 100644 --- a/src/drivers/grpc/resource.rs +++ b/src/drivers/grpc/resource.rs @@ -47,8 +47,13 @@ impl proto::resource_service_server::ResourceService for ResourceServiceImpl { let cmd = command::FetchCmd::new(credential, req.project_id, req.page, req.page_size)?; - let resources = - command::fetch(self.project_cache.clone(), self.resource_cache.clone(), cmd).await?; + let resources = command::fetch( + self.project_cache.clone(), + self.resource_cache.clone(), + self.metadata.clone(), + cmd, + ) + .await?; let records = resources.into_iter().map(|v| v.into()).collect(); let message = proto::FetchResourcesResponse { records }; @@ -68,13 +73,16 @@ impl proto::resource_service_server::ResourceService for ResourceServiceImpl { let cmd = command::FetchByIdCmd { credential, - project_id: req.project_id, - resource_id: req.resource_id, + id: req.id, }; - let resource = - command::fetch_by_id(self.project_cache.clone(), self.resource_cache.clone(), cmd) - .await?; + let resource = command::fetch_by_id( + self.project_cache.clone(), + self.resource_cache.clone(), + self.metadata.clone(), + cmd, + ) + .await?; let records = vec![resource.into()]; let message = proto::FetchResourcesByIdResponse { records }; @@ -166,8 +174,7 @@ impl proto::resource_service_server::ResourceService for ResourceServiceImpl { let cmd = command::DeleteCmd { credential, - project_id: req.project_id, - resource_id: req.resource_id, + id: req.id, }; command::delete( @@ -188,6 +195,7 @@ impl From for proto::Resource { id: value.id, kind: value.kind, spec: value.spec, + annotations: value.annotations, status: value.status.to_string(), created_at: value.created_at.to_rfc3339(), updated_at: value.updated_at.to_rfc3339(), diff --git a/test/expect b/test/expect index 9e01f84..71ced3c 100755 --- a/test/expect +++ b/test/expect @@ -143,7 +143,7 @@ echo "Updating resource..." "$NODE_IP:30950" demeter.ops.v1alpha.ResourceService.UpdateResource sleep 1 -OUTPUT=$(./grpcurl -plaintext -H "Authorization: Bearer $TOKEN" -d '{"resource_id": "'"$RESOURCE_ID"'", "project_id": "'"$PROJECT_ID"'" }' "$NODE_IP:30950" demeter.ops.v1alpha.ResourceService.FetchResourcesById | jq ) +OUTPUT=$(./grpcurl -plaintext -H "Authorization: Bearer $TOKEN" -d '{"id": "'"$RESOURCE_ID"'"}' "$NODE_IP:30950" demeter.ops.v1alpha.ResourceService.FetchResourcesById | jq ) echo $OUTPUT NEW_TIER=$(echo $OUTPUT | jq -r '.records[0].spec' | jq -r '.throughputTier')