Skip to content

Commit 0b1c7c5

Browse files
authored
client: prepare instance spec types for use in sled-agent API (#896)
The next step toward implementing virtual platforms in Omicron is to move instance spec construction from sled-agent up to Nexus. This requires instance spec types to appear in sled-agent's API. To allow this: - Direct the generated client to add a `schemars::JsonSchema` derive to all generated types. - Add `rename = snake_case` attributes to instance spec enum types that sled-agent expects to reuse. Omicron requires all of its API specifications to pass the OpenAPI linting rules defined in the openapi-lint crate (https://github.com/oxidecomputer/openapi-lint); among these is a requirement that enum variants all have snake case names.
1 parent 0c439e8 commit 0b1c7c5

File tree

14 files changed

+224
-77
lines changed

14 files changed

+224
-77
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,6 @@ serde_arrays = "0.1"
151151
serde_derive = "1.0"
152152
serde_json = "1.0"
153153
serde_test = "1.0.138"
154-
serde_with = "3.11.0"
155154
slog = "2.7"
156155
slog-async = "2.8"
157156
slog-bunyan = "2.4.0"

bin/mock-server/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ dropshot = { workspace = true }
2727
futures.workspace = true
2828
hyper.workspace = true
2929
serde.workspace = true
30+
propolis_api_types.workspace = true
3031
propolis_types.workspace = true
3132
semver.workspace = true
3233
serde_json.workspace = true

bin/mock-server/src/lib/api_types.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ use serde::{Deserialize, Serialize};
88
progenitor::generate_api!(
99
spec = "../../openapi/propolis-server.json",
1010
derives = [schemars::JsonSchema],
11+
replace = {
12+
SpecKey = propolis_api_types::instance_spec::SpecKey,
13+
},
1114
patch = {
1215
InstanceMetadata = { derives = [Clone, Eq, PartialEq] },
1316
InstanceProperties = { derives = [ Clone, Eq, PartialEq ] },

bin/propolis-cli/src/main.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ use propolis_client::types::{
2222
GuestHypervisorInterface, HyperVFeatureFlag, I440Fx, InstanceEnsureRequest,
2323
InstanceInitializationMethod, InstanceMetadata, InstanceSpecGetResponse,
2424
InstanceSpecV0, NvmeDisk, QemuPvpanic, ReplacementComponent, SerialPort,
25-
SerialPortNumber, VirtioDisk,
25+
SerialPortNumber, SpecKey, VirtioDisk,
2626
};
27-
use propolis_client::{PciPath, SpecKey};
27+
use propolis_client::PciPath;
2828
use propolis_config_toml::spec::SpecConfig;
2929
use serde::{Deserialize, Serialize};
3030
use slog::{o, Drain, Level, Logger};

crates/propolis-api-types/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ crucible-client-types.workspace = true
1212
propolis_types.workspace = true
1313
schemars.workspace = true
1414
serde.workspace = true
15-
serde_with.workspace = true
1615
thiserror.workspace = true
1716
uuid.workspace = true
1817

crates/propolis-api-types/src/instance_spec/components/board.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,14 +114,19 @@ pub struct CpuidEntry {
114114
Eq,
115115
PartialEq,
116116
)]
117-
#[serde(deny_unknown_fields)]
117+
#[serde(deny_unknown_fields, rename_all = "snake_case")]
118118
pub enum HyperVFeatureFlag {
119119
ReferenceTsc,
120120
}
121121

122122
/// A hypervisor interface to expose to the guest.
123123
#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema, Default)]
124-
#[serde(deny_unknown_fields, tag = "type", content = "value")]
124+
#[serde(
125+
deny_unknown_fields,
126+
rename_all = "snake_case",
127+
tag = "type",
128+
content = "value"
129+
)]
125130
pub enum GuestHypervisorInterface {
126131
/// Expose a bhyve-like interface ("bhyve bhyve " as the hypervisor ID in
127132
/// leaf 0x4000_0000 and no additional leaves or features).

crates/propolis-api-types/src/instance_spec/mod.rs

Lines changed: 69 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,6 @@
155155

156156
use schemars::JsonSchema;
157157
use serde::{Deserialize, Serialize};
158-
use serde_with::{DeserializeFromStr, SerializeDisplay};
159158

160159
pub use propolis_types::{CpuidIdent, CpuidValues, CpuidVendor, PciPath};
161160
use uuid::Uuid;
@@ -187,21 +186,20 @@ pub mod v0;
187186
// names if they have no UUIDs available or the most obvious UUID is in use
188187
// elsewhere. The key type's From impls will try to parse strings into UUIDs
189188
// before storing keys as strings.
190-
//
191-
// This type derives `SerializeDisplay` and `DeserializeFromStr` so that it can
192-
// be used as a map key when serializing to JSON, which requires strings (and
193-
// not objects) as keys.
194189
#[derive(
195-
Clone,
196-
Debug,
197-
SerializeDisplay,
198-
DeserializeFromStr,
199-
Eq,
200-
PartialEq,
201-
Ord,
202-
PartialOrd,
203-
JsonSchema,
190+
Clone, Debug, Serialize, Deserialize, Eq, PartialEq, Ord, PartialOrd,
204191
)]
192+
// Direct serde to use an untagged enum representation for this type. Since both
193+
// Uuid and String serialize to strings, this allows other types that contain a
194+
// Map<K = SpecKey> to derive Serialize and successfully serialize to JSON.
195+
// (This doesn't work with a tagged representation because JSON doesn't allow
196+
// maps to be used as map keys.)
197+
//
198+
// Note that this makes the order of variants matter: serde will pick the first
199+
// variant into which it can successfully deserialize an untagged enum value,
200+
// and the point is to use the UUID representation for any value that can be
201+
// interpreted as a UUID.
202+
#[serde(untagged)]
205203
pub enum SpecKey {
206204
Uuid(Uuid),
207205
Name(String),
@@ -217,10 +215,7 @@ impl std::fmt::Display for SpecKey {
217215
}
218216

219217
impl std::str::FromStr for SpecKey {
220-
// This conversion is infallible, but the error type needs to implement
221-
// `Display` for `SpecKey` to derive `DeserializeFromStr`.
222-
type Err = &'static str;
223-
218+
type Err = core::convert::Infallible;
224219
fn from_str(s: &str) -> Result<Self, Self::Err> {
225220
Ok(match Uuid::parse_str(s) {
226221
Ok(uuid) => Self::Uuid(uuid),
@@ -244,6 +239,62 @@ impl From<Uuid> for SpecKey {
244239
}
245240
}
246241

242+
// Manually implement JsonSchema to help Progenitor generate the expected enum
243+
// type for spec keys.
244+
impl JsonSchema for SpecKey {
245+
fn schema_name() -> String {
246+
"SpecKey".to_owned()
247+
}
248+
249+
fn json_schema(
250+
generator: &mut schemars::gen::SchemaGenerator,
251+
) -> schemars::schema::Schema {
252+
use schemars::schema::*;
253+
fn label_schema(label: &str, schema: Schema) -> Schema {
254+
SchemaObject {
255+
metadata: Some(
256+
Metadata {
257+
title: Some(label.to_string()),
258+
..Default::default()
259+
}
260+
.into(),
261+
),
262+
subschemas: Some(
263+
SubschemaValidation {
264+
all_of: Some(vec![schema]),
265+
..Default::default()
266+
}
267+
.into(),
268+
),
269+
..Default::default()
270+
}
271+
.into()
272+
}
273+
274+
SchemaObject {
275+
metadata: Some(
276+
Metadata {
277+
description: Some(
278+
"A key identifying a component in an instance spec."
279+
.to_string(),
280+
),
281+
..Default::default()
282+
}
283+
.into(),
284+
),
285+
subschemas: Some(Box::new(SubschemaValidation {
286+
one_of: Some(vec![
287+
label_schema("uuid", generator.subschema_for::<Uuid>()),
288+
label_schema("name", generator.subschema_for::<String>()),
289+
]),
290+
..Default::default()
291+
})),
292+
..Default::default()
293+
}
294+
.into()
295+
}
296+
}
297+
247298
/// A versioned instance spec.
248299
#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
249300
#[serde(deny_unknown_fields, tag = "version", content = "spec")]

crates/propolis-api-types/src/instance_spec/v0.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@ use schemars::JsonSchema;
99
use serde::{Deserialize, Serialize};
1010

1111
#[derive(Clone, Deserialize, Serialize, Debug, JsonSchema)]
12-
#[serde(deny_unknown_fields, tag = "type", content = "component")]
12+
#[serde(
13+
deny_unknown_fields,
14+
tag = "type",
15+
content = "component",
16+
rename_all = "snake_case"
17+
)]
1318
pub enum ComponentV0 {
1419
VirtioDisk(components::devices::VirtioDisk),
1520
NvmeDisk(components::devices::NvmeDisk),

crates/propolis-config-toml/src/spec.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ use propolis_client::{
1414
types::{
1515
ComponentV0, DlpiNetworkBackend, FileStorageBackend,
1616
MigrationFailureInjector, NvmeDisk, P9fs, PciPciBridge, SoftNpuP9,
17-
SoftNpuPciPort, SoftNpuPort, VirtioDisk, VirtioNetworkBackend,
17+
SoftNpuPciPort, SoftNpuPort, SpecKey, VirtioDisk, VirtioNetworkBackend,
1818
VirtioNic,
1919
},
20-
PciPath, SpecKey,
20+
PciPath,
2121
};
2222
use thiserror::Error;
2323

@@ -115,7 +115,7 @@ impl TryFrom<&super::Config> for SpecConfig {
115115
};
116116

117117
for (device_name, device) in config.devices.iter() {
118-
let device_id = SpecKey::from(device_name.clone());
118+
let device_id = SpecKey::Name(device_name.clone());
119119
let driver = device.driver.as_str();
120120
if device_name == MIGRATION_FAILURE_DEVICE_NAME {
121121
const FAIL_EXPORTS: &str = "fail_exports";

0 commit comments

Comments
 (0)