Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ jobs:
- name: Verify SDK Generation
run: |
cargo xtask generate
GENERATED="rfd-sdk/src/generated/ rfd-cli/src/generated/ rfd-ts/"
GENERATED="rfd-api-spec.json rfd-sdk/src/generated/ rfd-cli/src/generated/ rfd-ts/"
if ! git diff --quiet -- "$GENERATED"; then
git diff -- "$GENERATED"
echo "Found changed files after generating SDKs:"
Expand Down
25 changes: 18 additions & 7 deletions rfd-api-spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
"description": "Programmatic access to RFDs",
"contact": {
"url": "https://oxide.computer",
"email": "augustus@oxidecomputer.com"
"email": "corp-services@oxidecomputer.com"
},
"version": "0.14.0"
"version": "0.14.5"
},
"paths": {
"/.well-known/jwks.json": {
Expand Down Expand Up @@ -978,13 +978,24 @@
}
],
"responses": {
"default": {
"description": "",
"content": {
"*/*": {
"schema": {}
"307": {
"description": "redirect (temporary redirect)",
"headers": {
"location": {
"description": "HTTP \"Location\" header",
"style": "simple",
"required": true,
"schema": {
"type": "string"
}
}
}
},
"4XX": {
"$ref": "#/components/responses/Error"
},
"5XX": {
"$ref": "#/components/responses/Error"
}
}
}
Expand Down
8 changes: 8 additions & 0 deletions rfd-api/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ async fn main() -> anyhow::Result<()> {
let mut args = std::env::args();
let _ = args.next();
let config_path = args.next();

// Print the OpenAPI document to stdout and exit, without requiring the rest of the server's
// runtime configuration (database, secrets, etc). Used to keep the checked-in
// rfd-api-spec.json up to date via `cargo xtask generate`.
if config_path.as_deref() == Some("describe") {
return server::write_openapi(&mut std::io::stdout()).map_err(|err| anyhow::anyhow!(err));
}

let param_path = config_path
.as_deref()
.and_then(|path| Path::new(path).parent())
Expand Down
64 changes: 47 additions & 17 deletions rfd-api/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ use dropshot::{
use semver::Version;
use serde::Deserialize;
use slog::Drain;
use std::{collections::HashMap, error::Error, fs::File, net::SocketAddr, path::PathBuf};
use std::{
collections::HashMap, error::Error, fs::File, io::Write, net::SocketAddr, path::PathBuf,
};
use tracing_slog::TracingSlogDrain;
use v_api::{inject_endpoints, v_system_endpoints};

Expand Down Expand Up @@ -43,24 +45,16 @@ pub struct ServerConfig {
pub spec_output: Option<SpecConfig>,
}

v_system_endpoints!(RfdContext, RfdPermission);

pub fn server(
config: ServerConfig,
) -> Result<ServerBuilder<RfdContext>, Box<dyn Error + Send + Sync>> {
let config_dropshot = ConfigDropshot {
bind_address: config.server_address,
default_request_body_max_bytes: 1024 * 1024,
..Default::default()
};
// Static metadata used when generating the OpenAPI document on demand (i.e. via
// `rfd-api describe`), independent of any per-deployment `[spec]` configuration.
const SPEC_TITLE: &str = "RFD API";
const SPEC_DESCRIPTION: &str = "Programmatic access to RFDs";
const SPEC_CONTACT_URL: &str = "https://oxide.computer";
const SPEC_CONTACT_EMAIL: &str = "corp-services@oxidecomputer.com";

// Construct a shim to pipe dropshot logs into the global tracing logger
let dropshot_logger = {
let level_drain = slog::LevelFilter(TracingSlogDrain, slog::Level::Debug).fuse();
let async_drain = slog_async::Async::new(level_drain).build().fuse();
slog::Logger::root(async_drain, slog::o!())
};
v_system_endpoints!(RfdContext, RfdPermission);

fn api_description() -> ApiDescription<RfdContext> {
let mut tags = HashMap::new();
tags.insert(
"hidden".to_string(),
Expand Down Expand Up @@ -132,6 +126,42 @@ pub fn server(
api.register(github_webhook)
.expect("Failed to register endpoint");

api
}

/// Writes the current OpenAPI document to `out`, using static spec metadata. Used by the
/// `describe` CLI command to print the spec independent of any runtime configuration.
pub fn write_openapi<W: Write>(out: &mut W) -> Result<(), Box<dyn Error + Send + Sync>> {
let api = api_description();
let mut api_definition =
&mut api.openapi(SPEC_TITLE, Version::parse(env!("CARGO_PKG_VERSION"))?);
api_definition = api_definition
.description(SPEC_DESCRIPTION)
.contact_url(SPEC_CONTACT_URL)
.contact_email(SPEC_CONTACT_EMAIL);
api_definition.write(out)?;

Ok(())
}

pub fn server(
config: ServerConfig,
) -> Result<ServerBuilder<RfdContext>, Box<dyn Error + Send + Sync>> {
let config_dropshot = ConfigDropshot {
bind_address: config.server_address,
default_request_body_max_bytes: 1024 * 1024,
..Default::default()
};

// Construct a shim to pipe dropshot logs into the global tracing logger
let dropshot_logger = {
let level_drain = slog::LevelFilter(TracingSlogDrain, slog::Level::Debug).fuse();
let async_drain = slog_async::Async::new(level_drain).build().fuse();
slog::Logger::root(async_drain, slog::o!())
};

let api = api_description();

if let Some(spec) = config.spec_output {
// TODO: How do we validate that spec_output can be read or written? Currently File::create
// panics if the file path is not a valid path
Expand Down
3 changes: 2 additions & 1 deletion rfd-cli/src/generated/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2420,7 +2420,8 @@ impl<T: CliConfig> Cli<T> {
todo!()
}
Err(r) => {
todo!()
self.config.error(&r);
Err(anyhow::Error::new(r))
}
}
}
Expand Down
14 changes: 10 additions & 4 deletions rfd-sdk/src/generated/sdk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14260,7 +14260,7 @@ pub mod types {
///
/// Programmatic access to RFDs
///
/// Version: 0.14.0
/// Version: 0.14.5
pub struct Client {
pub(crate) baseurl: String,
pub(crate) client: reqwest::Client,
Expand Down Expand Up @@ -14301,7 +14301,7 @@ impl Client {

impl ClientInfo<()> for Client {
fn api_version() -> &'static str {
"0.14.0"
"0.14.5"
}

fn baseurl(&self) -> &str {
Expand Down Expand Up @@ -17622,7 +17622,7 @@ pub mod builder {
}

/// Sends a `GET` request to `/login/oauth/{provider}/code/authorize`
pub async fn send(self) -> Result<ResponseValue<ByteStream>, Error<ByteStream>> {
pub async fn send(self) -> Result<ResponseValue<ByteStream>, Error<types::Error>> {
let Self {
client,
provider,
Expand Down Expand Up @@ -17674,7 +17674,13 @@ pub mod builder {
let response = result?;
match response.status().as_u16() {
200..=299 => Ok(ResponseValue::stream(response)),
_ => Err(Error::ErrorResponse(ResponseValue::stream(response))),
400u16..=499u16 => Err(Error::ErrorResponse(
ResponseValue::from_response(response).await?,
)),
500u16..=599u16 => Err(Error::ErrorResponse(
ResponseValue::from_response(response).await?,
)),
_ => Err(Error::UnexpectedResponse(response)),
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion rfd-ts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,4 @@
}
},
"version": "0.14.5"
}
}
2 changes: 1 addition & 1 deletion rfd-ts/src/Api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -879,7 +879,7 @@ export class Api {
* Pulled from info.version in the OpenAPI schema. Sent in the
* `api-version` header on all requests.
*/
apiVersion = '0.14.0'
apiVersion = '0.14.5'

constructor({ host = '', baseParams = {}, token }: ApiConfig = {}) {
this.host = host
Expand Down
18 changes: 18 additions & 0 deletions xtask/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,24 @@ fn generate(check: bool, verbose: bool) -> Result<(), String> {
let mut spec_path = root_path.clone();
spec_path.push("rfd-api-spec.json");

// Regenerate the OpenAPI spec from the API server itself, so the SDKs below are always
// derived from the canonical description and the checked-in spec can be checked for drift.
print!("generating spec ... ");
std::io::stdout().flush().unwrap();
let output = Command::new("cargo")
.args(["run", "--quiet", "-p", "rfd-api", "--", "describe"])
.current_dir(&root_path)
.output()
.map_err(|err| format!("failed to run rfd-api describe: {}", err))?;
if !output.status.success() {
return Err(format!(
"rfd-api describe failed: {}",
String::from_utf8_lossy(&output.stderr)
));
}
fs::write(&spec_path, &output.stdout).map_err(|e| e.to_string())?;
println!("done.");

let file = File::open(spec_path).unwrap();
let spec = serde_json::from_reader(file).unwrap();
let mut generator = Generator::new(
Expand Down
Loading