Skip to content

Commit 8f54e11

Browse files
authored
Create Propolis Client Crate (#10)
1 parent 9b0bd7e commit 8f54e11

File tree

5 files changed

+167
-195
lines changed

5 files changed

+167
-195
lines changed

propolis-client/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ panic = "abort"
1212
panic = "abort"
1313

1414
[dependencies]
15+
reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls"] }
16+
ring = "0.16"
1517
schemars = { version = "0.8", features = [ "uuid" ] }
1618
serde = "1.0"
19+
serde_json = "1.0"
20+
slog = { version = "2.5", features = [ "max_level_trace", "release_max_level_debug" ] }
21+
structopt = "0.3"
22+
thiserror = "1.0"
1723
uuid = { version = "0.8", features = [ "serde", "v4" ] }

propolis-client/src/lib.rs

Lines changed: 148 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,153 @@
11
//! Interface for making API requests to propolis.
22
//! This should be replaced with a client generated from the OpenAPI spec.
33
4+
use reqwest::Body;
5+
use reqwest::IntoUrl;
6+
use serde::de::DeserializeOwned;
7+
use slog::{info, o, Logger};
8+
use std::net::SocketAddr;
9+
use thiserror::Error;
10+
use uuid::Uuid;
11+
412
pub mod api;
513

6-
// TODO: Implement the client.
14+
/// Errors which may be returend from the Propolis Client.
15+
#[derive(Debug, Error)]
16+
pub enum Error {
17+
#[error("Request failed: {0}")]
18+
Reqwest(#[from] reqwest::Error),
19+
20+
#[error("Bad Status: {0}")]
21+
Status(u16),
22+
}
23+
24+
/// Client-side connection to propolis.
25+
pub struct Client {
26+
client: reqwest::Client,
27+
log: Logger,
28+
address: SocketAddr,
29+
}
30+
31+
// Sends "request", awaits "response", and returns an error on any
32+
// non-success status code.
33+
//
34+
// TODO: Do we want to handle re-directs?
35+
async fn send_and_check_ok(
36+
request: reqwest::RequestBuilder,
37+
) -> Result<reqwest::Response, Error> {
38+
let response = request.send().await.map_err(Error::from)?;
39+
40+
if !response.status().is_success() {
41+
return Err(Error::Status(response.status().as_u16()));
42+
}
43+
44+
Ok(response)
45+
}
46+
47+
// Sends a "request", awaits "response", and parses the body
48+
// into a deserializable type.
49+
async fn send_and_parse_response<T: DeserializeOwned>(
50+
request: reqwest::RequestBuilder,
51+
) -> Result<T, Error> {
52+
send_and_check_ok(request).await?.json().await.map_err(|e| e.into())
53+
}
54+
55+
impl Client {
56+
pub fn new(address: SocketAddr, log: Logger) -> Client {
57+
Client {
58+
client: reqwest::Client::new(),
59+
log: log.new(o!("propolis_client address" => address.to_string())),
60+
address,
61+
}
62+
}
63+
64+
async fn get<T: DeserializeOwned, U: IntoUrl + std::fmt::Display>(
65+
&self,
66+
path: U,
67+
body: Option<Body>,
68+
) -> Result<T, Error> {
69+
info!(self.log, "GET request to {}", path);
70+
let mut request = self.client.get(path);
71+
if let Some(body) = body {
72+
request = request.body(body);
73+
}
74+
75+
send_and_parse_response(request).await
76+
}
77+
78+
async fn put<T: DeserializeOwned, U: IntoUrl + std::fmt::Display>(
79+
&self,
80+
path: U,
81+
body: Option<Body>,
82+
) -> Result<T, Error> {
83+
info!(self.log, "PUT request to {}", path);
84+
let mut request = self.client.put(path);
85+
if let Some(body) = body {
86+
request = request.body(body);
87+
}
88+
89+
send_and_parse_response(request).await
90+
}
91+
92+
async fn put_no_response<U: IntoUrl + std::fmt::Display>(
93+
&self,
94+
path: U,
95+
body: Option<Body>,
96+
) -> Result<(), Error> {
97+
info!(self.log, "PUT request to {}", path);
98+
let mut request = self.client.put(path);
99+
if let Some(body) = body {
100+
request = request.body(body);
101+
}
102+
103+
send_and_check_ok(request).await?;
104+
Ok(())
105+
}
106+
107+
/// Ensures that an instance with the specified properties exists.
108+
pub async fn instance_ensure(
109+
&self,
110+
request: &api::InstanceEnsureRequest,
111+
) -> Result<api::InstanceEnsureResponse, Error> {
112+
let path = format!(
113+
"http://{}/instances/{}",
114+
self.address, request.properties.id
115+
);
116+
let body = Body::from(serde_json::to_string(&request).unwrap());
117+
self.put(path, Some(body)).await
118+
}
119+
120+
/// Returns information about an instance, by UUID.
121+
pub async fn instance_get(
122+
&self,
123+
id: Uuid,
124+
) -> Result<api::InstanceGetResponse, Error> {
125+
let path = format!("http://{}/instances/{}", self.address, id);
126+
self.get(path, None).await
127+
}
128+
129+
/// Puts an instance into a new state.
130+
pub async fn instance_state_put(
131+
&self,
132+
id: Uuid,
133+
state: api::InstanceStateRequested,
134+
) -> Result<(), Error> {
135+
let path = format!("http://{}/instances/{}/state", self.address, id);
136+
let body = Body::from(serde_json::to_string(&state).unwrap());
137+
self.put_no_response(path, Some(body)).await
138+
}
139+
140+
/// Inputs bytes to the serial console, and returns any new output.
141+
///
142+
/// TODO: This method is NOT idempotent, uses short-polling, and should be
143+
/// replaced by a websocket-based interface (or equivalent).
144+
pub async fn instance_serial(
145+
&self,
146+
id: Uuid,
147+
input: api::InstanceSerialRequest,
148+
) -> Result<api::InstanceSerialResponse, Error> {
149+
let path = format!("http://{}/instances/{}/serial", self.address, id);
150+
let body = Body::from(serde_json::to_string(&input).unwrap());
151+
self.put(path, Some(body)).await
152+
}
153+
}

propolis-server/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,5 @@ serde_derive = "1.0"
2727
schemars = { version = "0.8", features = [ "uuid" ] }
2828
structopt = { version = "0.3", default-features = false }
2929
propolis = { path = "../propolis" }
30+
propolis-client = { path = "../propolis-client" }
3031
uuid = "0.8"

propolis-server/src/api.rs

Lines changed: 0 additions & 189 deletions
This file was deleted.

0 commit comments

Comments
 (0)