Skip to content

Commit e873a9d

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 a372de0 commit e873a9d

File tree

14 files changed

+1233
-212
lines changed

14 files changed

+1233
-212
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

+10-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,14 @@ 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+
.map_err(|e| anyhow!("Failed to upgrade connection: {}", e))?
327+
.into_inner();
328+
let mut ws =
329+
WebSocketStream::from_raw_socket(upgraded, Role::Client, None).await;
324330

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

bin/propolis-server/Cargo.toml

+3-2
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" }
5151
uuid = "1.0.0"
@@ -57,9 +57,10 @@ features = [ "chrono", "uuid1" ]
5757

5858
[dev-dependencies]
5959
hex = "0.4.3"
60-
reqwest = { version = "0.11", default-features = false, features = ["rustls-tls"] }
60+
reqwest = { version = "0.11.12", 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

@@ -477,9 +476,7 @@ async fn instance_ensure(
477476
None
478477
};
479478

480-
Ok(HttpResponseCreated(propolis_client::api::InstanceEnsureResponse {
481-
migrate,
482-
}))
479+
Ok(HttpResponseCreated(api::InstanceEnsureResponse { migrate }))
483480
}
484481

485482
#[endpoint {
@@ -488,8 +485,7 @@ async fn instance_ensure(
488485
}]
489486
async fn instance_get(
490487
rqctx: Arc<RequestContext<DropshotEndpointContext>>,
491-
) -> Result<HttpResponseOk<propolis_client::api::InstanceGetResponse>, HttpError>
492-
{
488+
) -> Result<HttpResponseOk<api::InstanceGetResponse>, HttpError> {
493489
let ctx = rqctx.context();
494490
let instance_info = match &*ctx.services.vm.lock().await {
495491
VmControllerState::NotCreated => {
@@ -498,7 +494,7 @@ async fn instance_get(
498494
));
499495
}
500496
VmControllerState::Created(vm) => {
501-
propolis_client::api::Instance {
497+
api::Instance {
502498
properties: vm.properties().clone(),
503499
state: vm.external_instance_state(),
504500
disks: vec![],
@@ -517,9 +513,7 @@ async fn instance_get(
517513
}
518514
};
519515

520-
Ok(HttpResponseOk(propolis_client::api::InstanceGetResponse {
521-
instance: instance_info,
522-
}))
516+
Ok(HttpResponseOk(api::InstanceGetResponse { instance: instance_info }))
523517
}
524518

525519
#[endpoint {
@@ -730,3 +724,25 @@ pub fn api() -> ApiDescription<DropshotEndpointContext> {
730724

731725
api
732726
}
727+
728+
#[cfg(test)]
729+
mod tests {
730+
#[test]
731+
fn test_propolis_server_openapi() {
732+
let mut buf: Vec<u8> = vec![];
733+
super::api()
734+
.openapi("Oxide Propolis Server API", "0.0.1")
735+
.description(
736+
"API for interacting with the Propolis hypervisor frontend.",
737+
)
738+
.contact_url("https://oxide.computer")
739+
.contact_email("api@oxide.computer")
740+
.write(&mut buf)
741+
.unwrap();
742+
let output = String::from_utf8(buf).unwrap();
743+
expectorate::assert_contents(
744+
"../../openapi/propolis-server.json",
745+
&output,
746+
);
747+
}
748+
}

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::*;
@@ -567,7 +567,7 @@ impl SpecBuilder {
567567
mod test {
568568
use std::{collections::BTreeMap, path::PathBuf};
569569

570-
use propolis_client::api::Slot;
570+
use propolis_client::handmade::api::Slot;
571571
use uuid::Uuid;
572572

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

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

+3-2
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,13 @@ use propolis::{
3636
hw::{ps2::ctrl::PS2Ctrl, qemu::ramfb::RamFb, uart::LpcUart},
3737
Instance,
3838
};
39-
use propolis_client::{
39+
use propolis_client::handmade::{
4040
api::InstanceProperties, api::InstanceState as ApiInstanceState,
4141
api::InstanceStateMonitorResponse as ApiMonitoredState,
4242
api::InstanceStateRequested as ApiInstanceStateRequested,
43-
api::MigrationState as ApiMigrationState, instance_spec::InstanceSpec,
43+
api::MigrationState as ApiMigrationState,
4444
};
45+
use propolis_client::instance_spec::InstanceSpec;
4546
use slog::{error, info, Logger};
4647
use thiserror::Error;
4748
use tokio::task::JoinHandle as TaskJoinHandle;

lib/propolis-client/Cargo.toml

+10-1
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,24 @@ 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 = { version = "0.11.12", default-features = false, features = ["json", "rustls-tls"] }
12+
base64 = "0.13"
13+
rand = "0.8"
1214
ring = "0.16"
1315
schemars = { version = "0.8.10", features = [ "uuid1" ] }
1416
serde = "1.0"
1517
serde_json = "1.0"
1618
slog = { version = "2.5", features = [ "max_level_trace", "release_max_level_debug" ] }
1719
thiserror = "1.0"
1820
uuid = { version = "1.0.0", features = [ "serde", "v4" ] }
21+
progenitor = { git = "https://github.com/oxidecomputer/progenitor", branch = "main", optional = true }
22+
tokio = { version = "1.0", features = [ "net" ], optional = true }
1923

2024
[dependencies.crucible-client-types]
2125
git = "https://github.com/oxidecomputer/crucible"
2226
rev = "144d8dafa41715e00b08a5929cc62140ff0eb561"
27+
28+
[features]
29+
default = []
30+
generated = ["progenitor", "tokio"]
31+
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)