Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions docs/rofl/features/appd.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion rofl-appd/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand Down
37 changes: 35 additions & 2 deletions rofl-appd/src/routes/metadata.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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 = "<body>")]
pub async fn upsert(
body: Json<BTreeMap<String, String>>,
metadata: &State<Arc<dyn MetadataService>>,
) -> 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 = "<body>")]
pub async fn delete(
body: Json<BTreeSet<String>>,
metadata: &State<Arc<dyn MetadataService>>,
) -> 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(
Expand Down
78 changes: 70 additions & 8 deletions rofl-appd/src/services/metadata.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::collections::BTreeMap;
use std::collections::{BTreeMap, BTreeSet};

use rocket::async_trait;
use tokio::sync::RwLock;
Expand All @@ -24,6 +24,18 @@ pub trait MetadataService: Send + Sync {
/// refresh if the metadata has changed.
async fn set(&self, metadata: BTreeMap<String, String>) -> 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<String, String>) -> 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<String>) -> Result<(), Error>;

/// Get all user-set metadata key-value pairs.
async fn get(&self) -> Result<BTreeMap<String, String>, Error>;
}
Expand Down Expand Up @@ -67,19 +79,16 @@ impl<A: App> OasisMetadataService<A> {
limits,
})
}
}

#[async_trait]
impl<A: App> MetadataService for OasisMetadataService<A> {
async fn set(&self, metadata: BTreeMap<String, String>) -> Result<(), Error> {
// Validate metadata against runtime limits.
/// Validate the given metadata map against the configured runtime limits.
fn validate(&self, metadata: &BTreeMap<String, String>) -> 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 {
Expand All @@ -90,10 +99,63 @@ impl<A: App> MetadataService for OasisMetadataService<A> {
}
}

Ok(())
}
}

#[async_trait]
impl<A: App> MetadataService for OasisMetadataService<A> {
async fn set(&self, metadata: BTreeMap<String, String>) -> 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<String, String>) -> 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<String>) -> 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(())
Expand Down
Loading