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(())