Skip to content

Commit 3fc635c

Browse files
author
lif
committed
Generate an API client with Progenitor
- Validate/generate an OpenAPI schema in a test, as done in omicron - Provide an (optional, for now) codegen'd client from the OpenAPI
1 parent c11699a commit 3fc635c

File tree

13 files changed

+1231
-210
lines changed

13 files changed

+1231
-210
lines changed

bin/propolis-cli/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ anyhow = "1.0"
1111
clap = { version = "3.2", features = ["derive"] }
1212
futures = "0.3"
1313
libc = "0.2"
14-
propolis-client = { path = "../../lib/propolis-client" }
14+
propolis-client = { path = "../../lib/propolis-client", features = ["generated"] }
1515
slog = "2.7"
1616
slog-async = "2.7"
1717
slog-term = "2.8"

bin/propolis-cli/src/main.rs

+11-4
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use std::{
1010
use anyhow::{anyhow, Context};
1111
use clap::{Parser, Subcommand};
1212
use futures::{future, SinkExt, StreamExt};
13-
use propolis_client::{
13+
use propolis_client::handmade::{
1414
api::{
1515
DiskRequest, InstanceEnsureRequest, InstanceMigrateInitiateRequest,
1616
InstanceProperties, InstanceStateRequested, MigrationState,
@@ -19,7 +19,9 @@ use propolis_client::{
1919
};
2020
use slog::{o, Drain, Level, Logger};
2121
use tokio::io::{AsyncReadExt, AsyncWriteExt};
22+
use tokio_tungstenite::tungstenite::protocol::Role;
2223
use tokio_tungstenite::tungstenite::Message;
24+
use tokio_tungstenite::WebSocketStream;
2325
use uuid::Uuid;
2426

2527
#[derive(Debug, Parser)]
@@ -317,10 +319,15 @@ async fn test_stdin_to_websockets_task() {
317319
}
318320

319321
async fn serial(addr: SocketAddr) -> anyhow::Result<()> {
320-
let path = format!("ws://{}/instance/serial", addr);
321-
let (mut ws, _) = tokio_tungstenite::connect_async(path)
322+
let upgraded = propolis_client::Client::new(&format!("http://{}", addr))
323+
.instance_serial()
324+
.send()
322325
.await
323-
.with_context(|| anyhow!("failed to create serial websocket stream"))?;
326+
.expect("Failed to upgrade connection")
327+
//.map_err(|_| anyhow!("Failed to upgrade connection"))?
328+
.into_inner();
329+
let mut ws =
330+
WebSocketStream::from_raw_socket(upgraded, Role::Client, None).await;
324331

325332
let _raw_guard = RawTermiosGuard::stdio_guard()
326333
.with_context(|| anyhow!("failed to set raw mode"))?;

bin/propolis-server/Cargo.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ serde_derive = "1.0"
4545
serde_json = "1.0"
4646
slog = "2.7"
4747
propolis = { path = "../../lib/propolis", features = ["crucible-full", "oximeter"], default-features = false }
48-
propolis-client = { path = "../../lib/propolis-client" }
48+
propolis-client = { path = "../../lib/propolis-client", features = ["generated"] }
4949
propolis-server-config = { path = "../../crates/propolis-server-config" }
5050
rfb = { git = "https://github.com/oxidecomputer/rfb", branch = "main" }
5151
uuid = "1.0.0"
@@ -60,6 +60,7 @@ hex = "0.4.3"
6060
reqwest = { version = "0.11", default-features = false, features = ["rustls-tls"] }
6161
ring = "0.16"
6262
slog = { version = "2.5", features = [ "max_level_trace", "release_max_level_debug" ] }
63+
expectorate = "1.0.5"
6364

6465
[features]
6566
default = ["dtrace-probes"]

bin/propolis-server/src/lib/migrate/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use bit_field::BitField;
44
use dropshot::{HttpError, RequestContext};
55
use hyper::{header, Body, Method, Response, StatusCode};
66
use propolis::migrate::MigrateStateError;
7-
use propolis_client::api::{self, MigrationState};
7+
use propolis_client::handmade::api::{self, MigrationState};
88
use serde::{Deserialize, Serialize};
99
use slog::{error, info, o};
1010
use thiserror::Error;

bin/propolis-server/src/lib/server.rs

+35-19
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@ use dropshot::{
1515
};
1616
use hyper::{Body, Response};
1717
use oximeter::types::ProducerRegistry;
18-
use propolis_client::instance_spec;
19-
use propolis_client::{api, instance_spec::InstanceSpec};
18+
use propolis_client::{
19+
handmade::api,
20+
instance_spec::{self, InstanceSpec},
21+
};
2022
use propolis_server_config::Config as VmTomlConfig;
2123
use rfb::server::VncServer;
2224
use slog::{error, o, Logger};
@@ -111,9 +113,9 @@ impl VmControllerState {
111113
/// `VmControllerState::Destroyed`.
112114
pub fn take_controller(&mut self) -> Option<Arc<VmController>> {
113115
if let VmControllerState::Created(vm) = self {
114-
let last_instance = propolis_client::api::Instance {
116+
let last_instance = api::Instance {
115117
properties: vm.properties().clone(),
116-
state: propolis_client::api::InstanceState::Destroyed,
118+
state: api::InstanceState::Destroyed,
117119
disks: vec![],
118120
nics: vec![],
119121
};
@@ -244,7 +246,7 @@ enum SpecCreationError {
244246
/// Creates an instance spec from an ensure request. (Both types are foreign to
245247
/// this crate, so implementing TryFrom for them is not allowed.)
246248
fn instance_spec_from_request(
247-
request: &propolis_client::api::InstanceEnsureRequest,
249+
request: &api::InstanceEnsureRequest,
248250
toml_config: &VmTomlConfig,
249251
) -> Result<(InstanceSpec, BTreeMap<String, Vec<u8>>), SpecCreationError> {
250252
let mut in_memory_disk_contents: BTreeMap<String, Vec<u8>> =
@@ -313,11 +315,8 @@ async fn register_oximeter(
313315
}]
314316
async fn instance_ensure(
315317
rqctx: Arc<RequestContext<DropshotEndpointContext>>,
316-
request: TypedBody<propolis_client::api::InstanceEnsureRequest>,
317-
) -> Result<
318-
HttpResponseCreated<propolis_client::api::InstanceEnsureResponse>,
319-
HttpError,
320-
> {
318+
request: TypedBody<api::InstanceEnsureRequest>,
319+
) -> Result<HttpResponseCreated<api::InstanceEnsureResponse>, HttpError> {
321320
let server_context = rqctx.context();
322321
let request = request.into_inner();
323322

@@ -448,9 +447,7 @@ async fn instance_ensure(
448447
None
449448
};
450449

451-
Ok(HttpResponseCreated(propolis_client::api::InstanceEnsureResponse {
452-
migrate,
453-
}))
450+
Ok(HttpResponseCreated(api::InstanceEnsureResponse { migrate }))
454451
}
455452

456453
#[endpoint {
@@ -459,8 +456,7 @@ async fn instance_ensure(
459456
}]
460457
async fn instance_get(
461458
rqctx: Arc<RequestContext<DropshotEndpointContext>>,
462-
) -> Result<HttpResponseOk<propolis_client::api::InstanceGetResponse>, HttpError>
463-
{
459+
) -> Result<HttpResponseOk<api::InstanceGetResponse>, HttpError> {
464460
let ctx = rqctx.context();
465461
let instance_info = match &*ctx.services.vm.lock().await {
466462
VmControllerState::NotCreated => {
@@ -469,7 +465,7 @@ async fn instance_get(
469465
));
470466
}
471467
VmControllerState::Created(vm) => {
472-
propolis_client::api::Instance {
468+
api::Instance {
473469
properties: vm.properties().clone(),
474470
state: vm.external_instance_state(),
475471
disks: vec![],
@@ -488,9 +484,7 @@ async fn instance_get(
488484
}
489485
};
490486

491-
Ok(HttpResponseOk(propolis_client::api::InstanceGetResponse {
492-
instance: instance_info,
493-
}))
487+
Ok(HttpResponseOk(api::InstanceGetResponse { instance: instance_info }))
494488
}
495489

496490
#[endpoint {
@@ -701,3 +695,25 @@ pub fn api() -> ApiDescription<DropshotEndpointContext> {
701695

702696
api
703697
}
698+
699+
#[cfg(test)]
700+
mod tests {
701+
#[test]
702+
fn test_propolis_server_openapi() {
703+
let mut buf: Vec<u8> = vec![];
704+
super::api()
705+
.openapi("Oxide Propolis Server API", "0.0.1")
706+
.description(
707+
"API for interacting with the Propolis hypervisor frontend.",
708+
)
709+
.contact_url("https://oxide.computer")
710+
.contact_email("api@oxide.computer")
711+
.write(&mut buf)
712+
.unwrap();
713+
let output = String::from_utf8(buf).unwrap();
714+
expectorate::assert_contents(
715+
"../../openapi/propolis-server.json",
716+
&output,
717+
);
718+
}
719+
}

bin/propolis-server/src/lib/spec.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::collections::BTreeSet;
44
use std::convert::TryInto;
55
use std::str::FromStr;
66

7-
use propolis_client::api::{
7+
use propolis_client::handmade::api::{
88
self, DiskRequest, InstanceProperties, NetworkInterfaceRequest,
99
};
1010
use propolis_client::instance_spec::*;
@@ -552,7 +552,7 @@ impl SpecBuilder {
552552
mod test {
553553
use std::{collections::BTreeMap, path::PathBuf};
554554

555-
use propolis_client::api::Slot;
555+
use propolis_client::handmade::api::Slot;
556556
use uuid::Uuid;
557557

558558
use crate::config::{self, Config};

bin/propolis-server/src/lib/vm.rs

+3-2
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,13 @@ use propolis::{
3535
hw::{qemu::ramfb::RamFb, uart::LpcUart},
3636
Instance,
3737
};
38-
use propolis_client::{
38+
use propolis_client::handmade::{
3939
api::InstanceProperties, api::InstanceState as ApiInstanceState,
4040
api::InstanceStateMonitorResponse as ApiMonitoredState,
4141
api::InstanceStateRequested as ApiInstanceStateRequested,
42-
api::MigrationState as ApiMigrationState, instance_spec::InstanceSpec,
42+
api::MigrationState as ApiMigrationState,
4343
};
44+
use propolis_client::instance_spec::InstanceSpec;
4445
use slog::{error, info, Logger};
4546
use thiserror::Error;
4647
use tokio::task::JoinHandle as TaskJoinHandle;

lib/propolis-client/Cargo.toml

+9-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ edition = "2018"
88

99
[dependencies]
1010
propolis_types = { path = "../../crates/propolis-types" }
11-
reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls"] }
11+
reqwest = { git = "https://github.com/seanmonstar/reqwest", rev = "6ceb23958c8b6d7f1d8ee093f0ad73184d133d40" }
12+
#reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls"] }
1213
ring = "0.16"
1314
schemars = { version = "0.8.10", features = [ "uuid1" ] }
1415
serde = "1.0"
@@ -17,3 +18,10 @@ slog = { version = "2.5", features = [ "max_level_trace", "release_max_level_deb
1718
thiserror = "1.0"
1819
uuid = { version = "1.0.0", features = [ "serde", "v4" ] }
1920
crucible-client-types = { git = "https://github.com/oxidecomputer/crucible", rev = "bacffd142fc38a01fe255407b0c8d5d0aacfe778" }
21+
progenitor = { git = "https://github.com/oxidecomputer/progenitor", branch = "main", optional = true }
22+
tokio = { version = "1.0", features = [ "net" ], optional = true }
23+
24+
[features]
25+
default = []
26+
generated = ["progenitor", "tokio"]
27+
generated-migration = ["generated"]

lib/propolis-client/src/generated.rs

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// Copyright 2022 Oxide Computer Company
2+
//! Experimental progenitor-generated propolis-server API client.
3+
4+
progenitor::generate_api!(
5+
spec = "../../openapi/propolis-server.json",
6+
interface = Builder,
7+
tags = Separate,
8+
);
File renamed without changes.

0 commit comments

Comments
 (0)