Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat] add a publish client + some improvement around labels. #12

Merged
merged 9 commits into from
Apr 7, 2022
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,14 @@ tracing = "0.1"
url = "2"

http = { version = "0.2", optional = true }
nom = { version = "6", optional = true }
openid = { version = "0.9.1", optional = true }
opentelemetry = { version = "0.17", optional = true }
opentelemetry-http = { version = "0.6", optional = true }
reqwest = { version = "0.11", optional = true }

[features]
default = ["reqwest", "openid", "telemetry"]
default = ["reqwest", "openid", "telemetry", "nom"]
telemetry = ["opentelemetry", "opentelemetry-http", "http"]

[dev-dependencies]
Expand Down
2 changes: 2 additions & 0 deletions src/command/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
//! Command and Control API.
pub mod v1;
94 changes: 94 additions & 0 deletions src/command/v1/client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
use crate::error::ClientError;
use crate::openid::TokenProvider;
use crate::util::Client as TraitClient;
use serde::Serialize;
use std::fmt::Debug;
use tracing::instrument;
use url::Url;

/// A client for drogue cloud command and control API, backed by reqwest.
#[derive(Clone, Debug)]
pub struct Client<TP>
where
TP: TokenProvider,
{
client: reqwest::Client,
api_url: Url,
token_provider: TP,
}

type ClientResult<T> = Result<T, ClientError<reqwest::Error>>;

impl<TP> TraitClient<TP> for Client<TP>
where
TP: TokenProvider,
{
fn client(&self) -> &reqwest::Client {
&self.client
}

fn token_provider(&self) -> &TP {
&self.token_provider
}
}

impl<TP> Client<TP>
where
TP: TokenProvider,
{
/// Create a new client instance.
pub fn new(client: reqwest::Client, api_url: Url, token_provider: TP) -> Self {
Self {
client,
api_url,
token_provider,
}
}

fn url(&self, application: &str, device: &str) -> ClientResult<Url> {
let mut url = self.api_url.clone();

{
let mut path = url
.path_segments_mut()
.map_err(|_| ClientError::Request("Failed to get paths".into()))?;

path.extend(&[
"api",
"command",
"v1alpha1",
"apps",
application,
"devices",
device,
]);
}

Ok(url)
}

/// Send one way commands to devices.
///
/// The result will be true if the command was accepted.
/// False
#[instrument(skip(payload))]
pub async fn publish_command<A, D, C, P>(
&self,
application: A,
device: D,
command: C,
payload: Option<P>,
) -> ClientResult<Option<()>>
where
A: AsRef<str> + Debug,
D: AsRef<str> + Debug,
C: AsRef<str> + Debug,
P: Serialize + Send + Sync,
{
let url = self.url(application.as_ref(), device.as_ref())?;
let query = vec![("command".to_string(), command.as_ref().to_string())];

self.create_with_query_parameters(url, payload, Some(query))
.await
}
}
5 changes: 5 additions & 0 deletions src/command/v1/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#[cfg(feature = "reqwest")]
mod client;

#[cfg(feature = "reqwest")]
pub use client::*;
6 changes: 5 additions & 1 deletion src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ where

impl fmt::Display for ErrorInformation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", self.error, self.message)
if self.error.is_empty() {
write!(f, "{}", self.message)
} else {
write!(f, "{}: {}", self.error, self.message)
}
}
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! A client for the Drogue IoT Cloud APIs.

pub mod admin;
pub mod command;
pub mod core;
pub mod error;
pub mod meta;
Expand Down
64 changes: 14 additions & 50 deletions src/registry/v1/client.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use super::data::*;
use crate::core::WithTracing;
use crate::openid::{TokenInjector, TokenProvider};
use crate::openid::TokenProvider;
use crate::registry::v1::labels::LabelSelector;
use crate::util::Client as ClientTrait;
use crate::{error::ClientError, Translator};
use futures::{stream, StreamExt, TryStreamExt};
Expand Down Expand Up @@ -84,33 +84,15 @@ where
/// If the user does not have access to the API, the server side may return "not found"
/// as a response instead of "forbidden".
#[instrument]
pub async fn list_apps<L>(&self, labels: Option<L>) -> ClientResult<Option<Vec<Application>>>
where
L: IntoIterator + Debug,
L::Item: AsRef<str>,
{
let mut req = self.client().get(self.url(None, None)?);

// todo it would be cool to have a programmatic way to construct labels selectors
// using drogue-cloud-service-api::labels::LabelSelector
// Also, allocating strings from the `&str` we have is terrible,
// but using only `as_ref()` from the iter was dropping the reference after the loop.
if let Some(labels) = labels {
let label_string = labels
.into_iter()
.map(|item| item.as_ref().to_string())
.collect::<Vec<String>>()
.join(",");

req = req.query(&[("labels", label_string)]);
}
pub async fn list_apps(
&self,
labels: Option<LabelSelector>,
) -> ClientResult<Option<Vec<Application>>> {
let url = self.url(None, None)?;

let req = req
.propagate_current_context()
.inject_token(self.token_provider())
.await?;
let labels = labels.map(|l| l.to_query_parameters());

Self::read_response(req.send().await?).await
self.read_with_query_parameters(url, labels).await
}

/// Get an application by name.
Expand Down Expand Up @@ -210,37 +192,19 @@ where
/// If the user does not have access to the API, the server side may return "not found"
/// as a response instead of "forbidden".
#[instrument]
pub async fn list_devices<A, L>(
pub async fn list_devices<A>(
&self,
application: A,
labels: Option<L>,
labels: Option<LabelSelector>,
) -> ClientResult<Option<Vec<Device>>>
where
A: AsRef<str> + Debug,
L: IntoIterator + Debug,
L::Item: AsRef<str>,
{
let mut req = self
.client()
.get(self.url(Some(application.as_ref()), Some(""))?);

// todo refactor this duplicated code
if let Some(labels) = labels {
let label_string = labels
.into_iter()
.map(|item| item.as_ref().to_string())
.collect::<Vec<String>>()
.join(",");

req = req.query(&[("labels", label_string)]);
}
let url = self.url(Some(application.as_ref()), Some(""))?;

let req = req
.propagate_current_context()
.inject_token(self.token_provider())
.await?;
let labels = labels.map(|l| l.to_query_parameters());

Self::read_response(req.send().await?).await
self.read_with_query_parameters(url, labels).await
}

/// Update (overwrite) an application.
Expand Down
32 changes: 32 additions & 0 deletions src/registry/v1/data/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,35 @@ impl AsMut<dyn CommonMetadataMut> for Application {
}
}

impl Application {
/// Create an minimal application object from the an application name
pub fn new<A>(name: A) -> Self
where
A: AsRef<str>,
{
Application {
metadata: NonScopedMetadata {
name: name.as_ref().into(),
..Default::default()
},
..Default::default()
}
}

/// Insert a trust anchor entry to an application
/// If there are no trust anchors already existing an array is created
/// if there is an error deserializing the existing data an error is returned
pub fn add_trust_anchor(
&mut self,
anchor: ApplicationSpecTrustAnchorEntry,
) -> Result<(), serde_json::Error> {
self.update_section::<ApplicationSpecTrustAnchors, _>(|mut credentials| {
credentials.anchors.push(anchor);
credentials
})
}
}

/// The application's trust-anchors.
#[derive(Clone, Debug, Default, Deserialize, Serialize, Eq, PartialEq)]
pub struct ApplicationSpecTrustAnchors {
Expand Down Expand Up @@ -139,3 +168,6 @@ impl Default for Authentication {
Self::None
}
}

#[cfg(test)]
mod test;
62 changes: 62 additions & 0 deletions src/registry/v1/data/app/test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
use super::*;
use serde_json::json;

#[derive(Debug)]
struct ApplicationTestWrapper(Application);

/// We simply don't look at the creation_timestamp and deletion_timestamp of the application
/// because they cannot be created at the exact same time during tests.
impl PartialEq for ApplicationTestWrapper {
fn eq(&self, other: &Self) -> bool {
self.0.spec == other.0.spec
&& self.0.status == other.0.status
&& self.0.metadata.name == other.0.metadata.name
&& self.0.metadata.uid == other.0.metadata.uid
&& self.0.metadata.generation == other.0.metadata.generation
&& self.0.metadata.resource_version == other.0.metadata.resource_version
&& self.0.metadata.finalizers == other.0.metadata.finalizers
&& self.0.metadata.labels == other.0.metadata.labels
&& self.0.metadata.annotations == other.0.metadata.annotations
}
}

#[test]
fn create_empty_application() {
let json_app: Application = serde_json::from_value(json!({
"metadata": {
"name": "foo",
},
"spec": {}
}))
.unwrap();

let app = Application::new("foo");

assert_eq!(
ApplicationTestWrapper { 0: app },
ApplicationTestWrapper { 0: json_app }
);
}

#[test]
fn create_add_cert() {
const CERT: &str =
"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tDQpNSUlCb2pDQ0FVaWdBxVUsBxTlAraRS0tLS0tDQo=";

let mut app = Application::new("foo");

let anchors = app.section::<ApplicationSpecTrustAnchors>();
assert!(anchors.is_none());

let anchor = ApplicationSpecTrustAnchorEntry {
certificate: CERT.into(),
};
app.add_trust_anchor(anchor.clone()).unwrap();

let anchors = app.section::<ApplicationSpecTrustAnchors>();
assert!(anchors.is_some());

let anchor_extracted = anchors.unwrap().unwrap();
assert!(!anchor_extracted.anchors.is_empty());
assert_eq!(anchor_extracted.anchors[0], anchor);
}
Loading