Skip to content

Commit

Permalink
Generate an API client with Progenitor
Browse files Browse the repository at this point in the history
- Validate/generate an OpenAPI schema in a test, as done in omicron
- Provide an (optional, for now) codegen'd client from the OpenAPI
  • Loading branch information
lif committed Sep 22, 2022
1 parent 2ff9199 commit aed73b1
Show file tree
Hide file tree
Showing 14 changed files with 1,234 additions and 212 deletions.
2 changes: 1 addition & 1 deletion bin/propolis-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ anyhow = "1.0"
clap = { version = "3.2", features = ["derive"] }
futures = "0.3"
libc = "0.2"
propolis-client = { path = "../../lib/propolis-client" }
propolis-client = { path = "../../lib/propolis-client", features = ["generated"] }
slog = "2.7"
slog-async = "2.7"
slog-term = "2.8"
Expand Down
15 changes: 11 additions & 4 deletions bin/propolis-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use std::{
use anyhow::{anyhow, Context};
use clap::{Parser, Subcommand};
use futures::{future, SinkExt, StreamExt};
use propolis_client::{
use propolis_client::handmade::{
api::{
DiskRequest, InstanceEnsureRequest, InstanceMigrateInitiateRequest,
InstanceProperties, InstanceStateRequested, MigrationState,
Expand All @@ -19,7 +19,9 @@ use propolis_client::{
};
use slog::{o, Drain, Level, Logger};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio_tungstenite::tungstenite::protocol::Role;
use tokio_tungstenite::tungstenite::Message;
use tokio_tungstenite::WebSocketStream;
use uuid::Uuid;

#[derive(Debug, Parser)]
Expand Down Expand Up @@ -317,10 +319,15 @@ async fn test_stdin_to_websockets_task() {
}

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

let _raw_guard = RawTermiosGuard::stdio_guard()
.with_context(|| anyhow!("failed to set raw mode"))?;
Expand Down
5 changes: 3 additions & 2 deletions bin/propolis-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ serde_derive = "1.0"
serde_json = "1.0"
slog = "2.7"
propolis = { path = "../../lib/propolis", features = ["crucible-full", "oximeter"], default-features = false }
propolis-client = { path = "../../lib/propolis-client" }
propolis-client = { path = "../../lib/propolis-client", features = ["generated"] }
propolis-server-config = { path = "../../crates/propolis-server-config" }
rfb = { git = "https://github.com/oxidecomputer/rfb", branch = "main" }
uuid = "1.0.0"
Expand All @@ -57,9 +57,10 @@ features = [ "chrono", "uuid1" ]

[dev-dependencies]
hex = "0.4.3"
reqwest = { version = "0.11", default-features = false, features = ["rustls-tls"] }
reqwest = { version = "0.11.12", default-features = false, features = ["rustls-tls"] }
ring = "0.16"
slog = { version = "2.5", features = [ "max_level_trace", "release_max_level_debug" ] }
expectorate = "1.0.5"

[features]
default = ["dtrace-probes"]
Expand Down
2 changes: 1 addition & 1 deletion bin/propolis-server/src/lib/migrate/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use bit_field::BitField;
use dropshot::{HttpError, RequestContext};
use hyper::{header, Body, Method, Response, StatusCode};
use propolis::migrate::MigrateStateError;
use propolis_client::api::{self, MigrationState};
use propolis_client::handmade::api::{self, MigrationState};
use serde::{Deserialize, Serialize};
use slog::{error, info, o};
use thiserror::Error;
Expand Down
54 changes: 35 additions & 19 deletions bin/propolis-server/src/lib/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ use dropshot::{
};
use hyper::{Body, Response};
use oximeter::types::ProducerRegistry;
use propolis_client::instance_spec;
use propolis_client::{api, instance_spec::InstanceSpec};
use propolis_client::{
handmade::api,
instance_spec::{self, InstanceSpec},
};
use propolis_server_config::Config as VmTomlConfig;
use rfb::server::VncServer;
use slog::{error, o, Logger};
Expand Down Expand Up @@ -111,9 +113,9 @@ impl VmControllerState {
/// `VmControllerState::Destroyed`.
pub fn take_controller(&mut self) -> Option<Arc<VmController>> {
if let VmControllerState::Created(vm) = self {
let last_instance = propolis_client::api::Instance {
let last_instance = api::Instance {
properties: vm.properties().clone(),
state: propolis_client::api::InstanceState::Destroyed,
state: api::InstanceState::Destroyed,
disks: vec![],
nics: vec![],
};
Expand Down Expand Up @@ -244,7 +246,7 @@ enum SpecCreationError {
/// Creates an instance spec from an ensure request. (Both types are foreign to
/// this crate, so implementing TryFrom for them is not allowed.)
fn instance_spec_from_request(
request: &propolis_client::api::InstanceEnsureRequest,
request: &api::InstanceEnsureRequest,
toml_config: &VmTomlConfig,
) -> Result<(InstanceSpec, BTreeMap<String, Vec<u8>>), SpecCreationError> {
let mut in_memory_disk_contents: BTreeMap<String, Vec<u8>> =
Expand Down Expand Up @@ -313,11 +315,8 @@ async fn register_oximeter(
}]
async fn instance_ensure(
rqctx: Arc<RequestContext<DropshotEndpointContext>>,
request: TypedBody<propolis_client::api::InstanceEnsureRequest>,
) -> Result<
HttpResponseCreated<propolis_client::api::InstanceEnsureResponse>,
HttpError,
> {
request: TypedBody<api::InstanceEnsureRequest>,
) -> Result<HttpResponseCreated<api::InstanceEnsureResponse>, HttpError> {
let server_context = rqctx.context();
let request = request.into_inner();

Expand Down Expand Up @@ -448,9 +447,7 @@ async fn instance_ensure(
None
};

Ok(HttpResponseCreated(propolis_client::api::InstanceEnsureResponse {
migrate,
}))
Ok(HttpResponseCreated(api::InstanceEnsureResponse { migrate }))
}

#[endpoint {
Expand All @@ -459,8 +456,7 @@ async fn instance_ensure(
}]
async fn instance_get(
rqctx: Arc<RequestContext<DropshotEndpointContext>>,
) -> Result<HttpResponseOk<propolis_client::api::InstanceGetResponse>, HttpError>
{
) -> Result<HttpResponseOk<api::InstanceGetResponse>, HttpError> {
let ctx = rqctx.context();
let instance_info = match &*ctx.services.vm.lock().await {
VmControllerState::NotCreated => {
Expand All @@ -469,7 +465,7 @@ async fn instance_get(
));
}
VmControllerState::Created(vm) => {
propolis_client::api::Instance {
api::Instance {
properties: vm.properties().clone(),
state: vm.external_instance_state(),
disks: vec![],
Expand All @@ -488,9 +484,7 @@ async fn instance_get(
}
};

Ok(HttpResponseOk(propolis_client::api::InstanceGetResponse {
instance: instance_info,
}))
Ok(HttpResponseOk(api::InstanceGetResponse { instance: instance_info }))
}

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

api
}

#[cfg(test)]
mod tests {
#[test]
fn test_propolis_server_openapi() {
let mut buf: Vec<u8> = vec![];
super::api()
.openapi("Oxide Propolis Server API", "0.0.1")
.description(
"API for interacting with the Propolis hypervisor frontend.",
)
.contact_url("https://oxide.computer")
.contact_email("api@oxide.computer")
.write(&mut buf)
.unwrap();
let output = String::from_utf8(buf).unwrap();
expectorate::assert_contents(
"../../openapi/propolis-server.json",
&output,
);
}
}
4 changes: 2 additions & 2 deletions bin/propolis-server/src/lib/spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::collections::BTreeSet;
use std::convert::TryInto;
use std::str::FromStr;

use propolis_client::api::{
use propolis_client::handmade::api::{
self, DiskRequest, InstanceProperties, NetworkInterfaceRequest,
};
use propolis_client::instance_spec::*;
Expand Down Expand Up @@ -552,7 +552,7 @@ impl SpecBuilder {
mod test {
use std::{collections::BTreeMap, path::PathBuf};

use propolis_client::api::Slot;
use propolis_client::handmade::api::Slot;
use uuid::Uuid;

use crate::config::{self, Config};
Expand Down
5 changes: 3 additions & 2 deletions bin/propolis-server/src/lib/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,13 @@ use propolis::{
hw::{qemu::ramfb::RamFb, uart::LpcUart},
Instance,
};
use propolis_client::{
use propolis_client::handmade::{
api::InstanceProperties, api::InstanceState as ApiInstanceState,
api::InstanceStateMonitorResponse as ApiMonitoredState,
api::InstanceStateRequested as ApiInstanceStateRequested,
api::MigrationState as ApiMigrationState, instance_spec::InstanceSpec,
api::MigrationState as ApiMigrationState,
};
use propolis_client::instance_spec::InstanceSpec;
use slog::{error, info, Logger};
use thiserror::Error;
use tokio::task::JoinHandle as TaskJoinHandle;
Expand Down
11 changes: 10 additions & 1 deletion lib/propolis-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ edition = "2018"

[dependencies]
propolis_types = { path = "../../crates/propolis-types" }
reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls"] }
reqwest = { version = "0.11.12", default-features = false, features = ["json", "rustls-tls"] }
base64 = "0.13"
rand = "0.8"
ring = "0.16"
schemars = { version = "0.8.10", features = [ "uuid1" ] }
serde = "1.0"
Expand All @@ -17,3 +19,10 @@ slog = { version = "2.5", features = [ "max_level_trace", "release_max_level_deb
thiserror = "1.0"
uuid = { version = "1.0.0", features = [ "serde", "v4" ] }
crucible-client-types = { git = "https://github.com/oxidecomputer/crucible", rev = "bacffd142fc38a01fe255407b0c8d5d0aacfe778" }
progenitor = { git = "https://github.com/oxidecomputer/progenitor", branch = "main", optional = true }
tokio = { version = "1.0", features = [ "net" ], optional = true }

[features]
default = []
generated = ["progenitor", "tokio"]
generated-migration = ["generated"]
8 changes: 8 additions & 0 deletions lib/propolis-client/src/generated.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Copyright 2022 Oxide Computer Company
//! Experimental progenitor-generated propolis-server API client.

progenitor::generate_api!(
spec = "../../openapi/propolis-server.json",
interface = Builder,
tags = Separate,
);
File renamed without changes.

0 comments on commit aed73b1

Please sign in to comment.