Skip to content

Commit b12d9c0

Browse files
authored
[2/2] migrate to Dropshot API manager (#38)
Part of oxidecomputer/omicron#8922. The manager can be run with `cargo xtask openapi`, e.g. `cargo xtask openapi generate`. There wasn't a test which ensured that `lldpd.json` was up-to-date, but now there is.
1 parent 515c31e commit b12d9c0

File tree

9 files changed

+468
-53
lines changed

9 files changed

+468
-53
lines changed

Cargo.lock

Lines changed: 229 additions & 33 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
members = [
44
"adm",
5+
"dropshot-apis",
56
"lldpd-api",
67
"lldpd-client",
78
"lldpd-common",
@@ -30,7 +31,9 @@ anyhow = "1.0"
3031
camino = { version = "1.1", features = ["serde1"] }
3132
chrono = "0.4"
3233
clap = { version = "4.5.45", features = ["derive"] }
33-
dropshot = "0.16.3"
34+
dropshot = "0.16.4"
35+
dropshot-api-manager = "0.2.2"
36+
dropshot-api-manager-types = "0.2.2"
3437
futures = "0.3"
3538
http = "0.2.9"
3639
omicron-zone-package = "0.11.1"

dropshot-apis/Cargo.toml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[package]
2+
name = "lldp-dropshot-apis"
3+
version = "0.1.0"
4+
edition = "2024"
5+
license = "MPL-2.0"
6+
7+
[dependencies]
8+
anyhow.workspace = true
9+
camino.workspace = true
10+
clap.workspace = true
11+
lldpd-api.workspace = true
12+
dropshot-api-manager-types.workspace = true
13+
dropshot-api-manager.workspace = true
14+
semver.workspace = true

dropshot-apis/src/main.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+
use std::process::ExitCode;
6+
7+
use anyhow::Context;
8+
use camino::Utf8PathBuf;
9+
use clap::Parser;
10+
use dropshot_api_manager::{Environment, ManagedApiConfig, ManagedApis};
11+
use dropshot_api_manager_types::{ManagedApiMetadata, Versions};
12+
use lldpd_api::*;
13+
14+
pub fn environment() -> anyhow::Result<Environment> {
15+
// The workspace root is one level up from this crate's directory.
16+
let workspace_root = Utf8PathBuf::from(env!("CARGO_MANIFEST_DIR"))
17+
.parent()
18+
.unwrap()
19+
.to_path_buf();
20+
let env = Environment::new(
21+
// This is the command used to run the OpenAPI manager.
22+
"cargo xtask openapi".to_owned(),
23+
workspace_root,
24+
// This is the location within the workspace root where the OpenAPI
25+
// documents are stored.
26+
"openapi",
27+
)?;
28+
Ok(env)
29+
}
30+
31+
/// The list of APIs managed by the OpenAPI manager.
32+
pub fn all_apis() -> anyhow::Result<ManagedApis> {
33+
let apis = vec![ManagedApiConfig {
34+
ident: "lldpd",
35+
versions: Versions::Lockstep {
36+
version: semver::Version::new(0, 0, 1),
37+
},
38+
title: "Oxide LLDP Daemon",
39+
metadata: ManagedApiMetadata {
40+
description: Some("API for managing the LLDP daemon"),
41+
contact_url: Some("https://oxide.computer"),
42+
contact_email: Some("api@oxide.computer"),
43+
..Default::default()
44+
},
45+
api_description: lldpd_api_mod::stub_api_description,
46+
extra_validation: None,
47+
}];
48+
49+
let apis = ManagedApis::new(apis).context("error creating ManagedApis")?;
50+
Ok(apis)
51+
}
52+
53+
fn main() -> anyhow::Result<ExitCode> {
54+
let app = dropshot_api_manager::App::parse();
55+
let env = environment()?;
56+
let apis = all_apis()?;
57+
58+
Ok(app.exec(&env, &apis))
59+
}
60+
61+
#[cfg(test)]
62+
mod test {
63+
use dropshot_api_manager::test_util::check_apis_up_to_date;
64+
65+
use super::*;
66+
67+
// Also recommended: a test which ensures documents are up-to-date. The
68+
// OpenAPI manager comes with a helper function for this, called
69+
// `check_apis_up_to_date`.
70+
#[test]
71+
fn test_apis_up_to_date() -> anyhow::Result<ExitCode> {
72+
let env = environment()?;
73+
let apis = all_apis()?;
74+
75+
let result = check_apis_up_to_date(&env, &apis)?;
76+
Ok(result.to_exit_code())
77+
}
78+
}

lldpd-types/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@ license = "MPL-2.0"
77
[dependencies]
88
chrono.workspace = true
99
protocol.workspace = true
10-
schemars.workspace = true
10+
schemars = { workspace = true, features = ["chrono"] }
1111
serde.workspace = true
1212
uuid.workspace = true

lldpd/src/api_server.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -652,6 +652,10 @@ pub async fn api_server_manager(
652652
}
653653
}
654654

655+
pub fn http_api() -> dropshot::ApiDescription<Arc<Global>> {
656+
lldpd_api_mod::api_description::<LldpdApiImpl>().unwrap()
657+
}
658+
655659
#[cfg(test)]
656660
mod tests {
657661
use crate::api_server::build_info;
@@ -672,7 +676,3 @@ mod tests {
672676
assert_eq!(info.git_sha, ours);
673677
}
674678
}
675-
676-
pub fn http_api() -> dropshot::ApiDescription<Arc<Global>> {
677-
lldpd_api_mod::api_description::<LldpdApiImpl>().unwrap()
678-
}

lldpd/src/main.rs

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,6 @@ pub struct SwitchInfo {
9292
enum Args {
9393
/// Run the LLDPD API server.
9494
Run(Opt),
95-
/// Generate an OpenAPI specification for the LLDPD server.
96-
Openapi,
9795
}
9896

9997
#[derive(Debug, StructOpt)]
@@ -246,23 +244,11 @@ async fn run_lldpd(opts: Opt) -> LldpdResult<()> {
246244
Ok(())
247245
}
248246

249-
fn print_openapi() -> LldpdResult<()> {
250-
lldpd_api::lldpd_api_mod::stub_api_description()
251-
.unwrap()
252-
.openapi("Oxide LLDP Daemon", "0.0.1".parse().unwrap())
253-
.description("API for managing the LLDP daemon")
254-
.contact_url("https://oxide.computer")
255-
.contact_email("api@oxide.computer")
256-
.write(&mut std::io::stdout())
257-
.map_err(|e| LldpdError::Io(e.into()))
258-
}
259-
260247
#[tokio::main(flavor = "multi_thread")]
261248
async fn main() -> LldpdResult<()> {
262249
let args = Args::from_args();
263250

264251
match args {
265-
Args::Openapi => print_openapi(),
266252
Args::Run(opt) => run_lldpd(opt).await,
267253
}
268254
}

xtask/src/external.rs

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+
//! External xtasks. (extasks?)
6+
7+
use std::ffi::OsString;
8+
use std::os::unix::process::CommandExt;
9+
use std::process::Command;
10+
11+
use anyhow::{Context, Result};
12+
use clap::Parser;
13+
14+
/// Argument parser for external xtasks.
15+
///
16+
/// In general we want all developer tasks to be discoverable simply by running
17+
/// `cargo xtask`, but some development tools end up with a particularly
18+
/// large dependency tree. It's not ideal to have to pay the cost of building
19+
/// our release engineering tooling if all the user wants to do is check for
20+
/// workspace dependency issues.
21+
///
22+
/// `External` provides a pattern for creating xtasks that live in other crates.
23+
/// An external xtask is defined on `crate::Cmds` as a tuple variant containing
24+
/// `External`, which captures all arguments and options (even `--help`) as
25+
/// a `Vec<OsString>`. The main function then calls `External::exec` with the
26+
/// appropriate bin target name and any additional Cargo arguments.
27+
#[derive(Debug, Parser)]
28+
#[clap(
29+
disable_help_flag(true),
30+
disable_help_subcommand(true),
31+
disable_version_flag(true)
32+
)]
33+
pub struct External {
34+
#[clap(trailing_var_arg(true), allow_hyphen_values(true))]
35+
args: Vec<OsString>,
36+
37+
// This stores an in-progress Command builder. `cargo_args` appends args
38+
// to it, and `exec` consumes it. Clap does not treat this as a command
39+
// (`skip`), but fills in this field by calling `new_command`.
40+
#[clap(skip = new_command())]
41+
command: Command,
42+
}
43+
44+
impl External {
45+
pub fn exec_bin(
46+
self,
47+
package: impl AsRef<str>,
48+
bin_target: impl AsRef<str>,
49+
) -> Result<()> {
50+
self.exec_common(&[
51+
"--package",
52+
package.as_ref(),
53+
"--bin",
54+
bin_target.as_ref(),
55+
])
56+
}
57+
58+
fn exec_common(mut self, args: &[&str]) -> Result<()> {
59+
let error = self.command.args(args).arg("--").args(self.args).exec();
60+
Err(error).context("failed to exec `cargo run`")
61+
}
62+
}
63+
64+
fn new_command() -> Command {
65+
let mut command = cargo_command(CargoLocation::FromEnv);
66+
command.arg("run");
67+
command
68+
}
69+
70+
/// Creates and prepares a `std::process::Command` for the `cargo` executable.
71+
pub fn cargo_command(location: CargoLocation) -> Command {
72+
let mut command = location.resolve();
73+
74+
for (key, _) in std::env::vars_os() {
75+
let Some(key) = key.to_str() else { continue };
76+
if SANITIZED_ENV_VARS.matches(key) {
77+
command.env_remove(key);
78+
}
79+
}
80+
81+
command
82+
}
83+
84+
/// How to determine the location of the `cargo` executable.
85+
#[derive(Clone, Copy, Debug)]
86+
pub enum CargoLocation {
87+
/// Use the `CARGO` environment variable, and fall back to `"cargo"` if it
88+
/// is not set.
89+
FromEnv,
90+
}
91+
92+
impl CargoLocation {
93+
fn resolve(self) -> Command {
94+
match self {
95+
CargoLocation::FromEnv => {
96+
let cargo = std::env::var_os("CARGO")
97+
.unwrap_or_else(|| OsString::from("cargo"));
98+
Command::new(&cargo)
99+
}
100+
}
101+
}
102+
}
103+
104+
#[derive(Debug)]
105+
struct SanitizedEnvVars {
106+
// At the moment we only ban some prefixes, but we may also want to ban env
107+
// vars by exact name in the future.
108+
prefixes: &'static [&'static str],
109+
}
110+
111+
impl SanitizedEnvVars {
112+
const fn new() -> Self {
113+
// Remove many of the environment variables set in
114+
// https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-build-scripts.
115+
// This is done to avoid recompilation with crates like ring between
116+
// `cargo clippy` and `cargo xtask clippy`. (This is really a bug in
117+
// both ring's build script and in Cargo.)
118+
//
119+
// The current list is informed by looking at ring's build script, so
120+
// it's not guaranteed to be exhaustive and it may need to grow over
121+
// time.
122+
let prefixes = &["CARGO_PKG_", "CARGO_MANIFEST_", "CARGO_CFG_"];
123+
Self { prefixes }
124+
}
125+
126+
fn matches(&self, key: &str) -> bool {
127+
self.prefixes.iter().any(|prefix| key.starts_with(prefix))
128+
}
129+
}
130+
131+
static SANITIZED_ENV_VARS: SanitizedEnvVars = SanitizedEnvVars::new();

xtask/src/main.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ use std::path::Path;
1212
use anyhow::{anyhow, Context, Result};
1313
use clap::{Parser, ValueEnum};
1414

15+
mod external;
16+
1517
#[cfg(target_os = "illumos")]
1618
mod illumos;
1719
#[cfg(target_os = "illumos")]
@@ -41,6 +43,8 @@ pub enum DistFormat {
4143
/// lldp xtask support
4244
#[clap(name = "xtask")]
4345
enum Xtasks {
46+
/// manage OpenAPI documents
47+
Openapi(Box<external::External>),
4448
/// build an installable dataplane controller package
4549
Dist {
4650
/// package release bits
@@ -95,6 +99,9 @@ fn collect_binaries(release: bool, dst: &str) -> Result<()> {
9599
async fn main() {
96100
let task = Xtasks::parse();
97101
if let Err(e) = match task {
102+
Xtasks::Openapi(external) => {
103+
external.exec_bin("lldp-dropshot-apis", "lldp-dropshot-apis")
104+
}
98105
Xtasks::Dist { release, format } => plat::dist(release, format).await,
99106
} {
100107
eprintln!("failed: {e}");

0 commit comments

Comments
 (0)