From d885e48fadd71db437243d3f5fd34658d5e20bc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matev=C5=BE=20Jekovec?= Date: Fri, 12 Jun 2026 11:40:19 +0200 Subject: [PATCH] rofl-appd: Add upsert and delete metadata endpoint --- docs/rofl/features/appd.md | 32 ++++++++++++ rofl-appd/src/lib.rs | 7 ++- rofl-appd/src/routes/metadata.rs | 37 +++++++++++++- rofl-appd/src/services/metadata.rs | 78 +++++++++++++++++++++++++++--- 4 files changed, 143 insertions(+), 11 deletions(-) diff --git a/docs/rofl/features/appd.md b/docs/rofl/features/appd.md index 02d78b00d84..2b433f765a6 100644 --- a/docs/rofl/features/appd.md +++ b/docs/rofl/features/appd.md @@ -234,6 +234,38 @@ and will trigger a registration refresh if the metadata has changed. **Note:** Metadata is validated against runtime-configured limits for the number of pairs, key size, and value size. +#### Upsert Metadata + +Insert or update the given metadata key-value pairs. Existing app metadata not +present in the request are untouched. It will trigger a registration refresh +if the metadata has changed. + +**Endpoint:** `/rofl/v1/metadata` (`PUT`) + +**Example request:** + +```json +{ + "version": "1.0.1" +} +``` + +**Note:** The resulting metadata is validated against runtime-configured limits +for the number of pairs, key size, and value size. + +#### Delete Metadata + +Delete the given metadata keys. Keys that do not exist are ignored. It will +trigger a registration refresh if the metadata has changed. + +**Endpoint:** `/rofl/v1/metadata` (`DELETE`) + +**Example request:** + +```json +["version", "key_fingerprint"] +``` + ### Query Runs arbitrary query method defined in the [Oasis Runtime SDK] module and diff --git a/rofl-appd/src/lib.rs b/rofl-appd/src/lib.rs index 5fdefd48365..290b6386c53 100644 --- a/rofl-appd/src/lib.rs +++ b/rofl-appd/src/lib.rs @@ -43,7 +43,12 @@ where let server = server.manage(cfg.metadata).mount( "/rofl/v1/metadata", - routes![routes::metadata::set, routes::metadata::get], + routes![ + routes::metadata::set, + routes::metadata::upsert, + routes::metadata::delete, + routes::metadata::get + ], ); let server = server.mount("/rofl/v1/query", routes![routes::query::query]); diff --git a/rofl-appd/src/routes/metadata.rs b/rofl-appd/src/routes/metadata.rs index 5ee1da9b4c6..db548964f56 100644 --- a/rofl-appd/src/routes/metadata.rs +++ b/rofl-appd/src/routes/metadata.rs @@ -1,6 +1,9 @@ -use std::{collections::BTreeMap, sync::Arc}; +use std::{ + collections::{BTreeMap, BTreeSet}, + sync::Arc, +}; -use rocket::{http::Status, serde::json::Json, State}; +use rocket::{http::Status, serde::json::Json, suppress, State}; use crate::services::metadata::MetadataService; @@ -16,6 +19,36 @@ pub async fn set( .map_err(|err| (Status::BadRequest, err.to_string())) } +/// Upsert metadata endpoint. +/// +/// Inserts or updates the given key-value pairs, leaving other keys untouched. +#[rocket::put("/", data = "")] +pub async fn upsert( + body: Json>, + metadata: &State>, +) -> Result<(), (Status, String)> { + metadata + .upsert(body.into_inner()) + .await + .map_err(|err| (Status::BadRequest, err.to_string())) +} + +/// Delete metadata endpoint. +/// +/// Removes the given keys, ignoring those that do not exist. +// Rocket complains, if DELETE requires body. Suppress it. +#[suppress(dubious_payload)] +#[rocket::delete("/", data = "")] +pub async fn delete( + body: Json>, + metadata: &State>, +) -> Result<(), (Status, String)> { + metadata + .delete(body.into_inner()) + .await + .map_err(|err| (Status::BadRequest, err.to_string())) +} + /// Get metadata endpoint. #[rocket::get("/")] pub async fn get( diff --git a/rofl-appd/src/services/metadata.rs b/rofl-appd/src/services/metadata.rs index a3c97105e21..136ea5b0d01 100644 --- a/rofl-appd/src/services/metadata.rs +++ b/rofl-appd/src/services/metadata.rs @@ -1,4 +1,4 @@ -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use rocket::async_trait; use tokio::sync::RwLock; @@ -24,6 +24,18 @@ pub trait MetadataService: Send + Sync { /// refresh if the metadata has changed. async fn set(&self, metadata: BTreeMap) -> Result<(), Error>; + /// Insert or update the given metadata key-value pairs. + /// + /// Existing keys not present in `metadata` are left untouched. Will trigger a + /// registration refresh if the metadata has changed. + async fn upsert(&self, metadata: BTreeMap) -> Result<(), Error>; + + /// Delete the given metadata keys. + /// + /// Keys that do not exist are ignored. Will trigger a registration refresh if the + /// metadata has changed. + async fn delete(&self, keys: BTreeSet) -> Result<(), Error>; + /// Get all user-set metadata key-value pairs. async fn get(&self) -> Result, Error>; } @@ -67,19 +79,16 @@ impl OasisMetadataService { limits, }) } -} -#[async_trait] -impl MetadataService for OasisMetadataService { - async fn set(&self, metadata: BTreeMap) -> Result<(), Error> { - // Validate metadata against runtime limits. + /// Validate the given metadata map against the configured runtime limits. + fn validate(&self, metadata: &BTreeMap) -> Result<(), Error> { let max_user_pairs = (self.limits.max_pairs as usize).saturating_sub(RESERVED_METADATA_SLOTS); if metadata.len() > max_user_pairs { return Err(Error::InvalidArgument); } - for (key, value) in &metadata { + for (key, value) in metadata { // Account for namespace prefix when checking key size. let full_key_size = METADATA_NAMESPACE.len() + 1 + key.len(); if full_key_size > self.limits.max_key_size as usize { @@ -90,10 +99,63 @@ impl MetadataService for OasisMetadataService { } } + Ok(()) + } +} + +#[async_trait] +impl MetadataService for OasisMetadataService { + async fn set(&self, metadata: BTreeMap) -> Result<(), Error> { + self.validate(&metadata)?; + let mut map = self.metadata.write().await; + if *map == metadata { + return Ok(()); + } *map = metadata; - // Refresh registration. + self.env.refresh_registration().await?; + + Ok(()) + } + + async fn upsert(&self, metadata: BTreeMap) -> Result<(), Error> { + let mut map = self.metadata.write().await; + + let mut updated = map.clone(); + let mut changed = false; + for (key, value) in metadata { + if updated.get(&key) != Some(&value) { + changed = true; + } + updated.insert(key, value); + } + self.validate(&updated)?; + + if !changed { + return Ok(()); + } + *map = updated; + + self.env.refresh_registration().await?; + + Ok(()) + } + + async fn delete(&self, keys: BTreeSet) -> Result<(), Error> { + let mut map = self.metadata.write().await; + + let mut changed = false; + for key in &keys { + if map.remove(key).is_some() { + changed = true; + } + } + + if !changed { + return Ok(()); + } + self.env.refresh_registration().await?; Ok(())