Skip to content

Commit 64439ea

Browse files
committed
refactor(policy): extract middleware serialization
Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
1 parent d2a9791 commit 64439ea

10 files changed

Lines changed: 337 additions & 428 deletions

File tree

architecture/sandbox.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,9 @@ over the common middleware gRPC contract. The gateway validates external
7474
service capabilities and policy-owned config before delivery. Supervisors keep
7575
the last-known-good service registry when a live config reload fails. Built-in
7676
middleware identifiers and pure config validation live in `openshell-core` so
77-
policy admission does not depend on the supervisor runtime implementation.
77+
policy admission does not depend on the supervisor runtime implementation. The
78+
policy and runtime also share the core JSON/protobuf adapter for middleware
79+
configuration, keeping serialization consistent across that boundary.
7880

7981
`https://inference.local` is special. It bypasses OPA network policy and is
8082
handled by the inference interception path:

crates/openshell-cli/src/run.rs

Lines changed: 3 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1648,39 +1648,9 @@ fn parse_driver_config_json(value: &str) -> Result<prost_types::Struct> {
16481648
));
16491649
};
16501650

1651-
Ok(prost_types::Struct {
1652-
fields: fields
1653-
.into_iter()
1654-
.map(|(key, value)| json_to_protobuf_value(value).map(|value| (key, value)))
1655-
.collect::<Result<_>>()?,
1656-
})
1657-
}
1658-
1659-
fn json_to_protobuf_value(value: serde_json::Value) -> Result<prost_types::Value> {
1660-
use prost_types::{ListValue, Struct, Value, value::Kind};
1661-
1662-
let kind = match value {
1663-
serde_json::Value::Null => Kind::NullValue(0),
1664-
serde_json::Value::Bool(value) => Kind::BoolValue(value),
1665-
serde_json::Value::Number(value) => Kind::NumberValue(value.as_f64().ok_or_else(|| {
1666-
miette!("--driver-config-json contains a number that cannot be represented")
1667-
})?),
1668-
serde_json::Value::String(value) => Kind::StringValue(value),
1669-
serde_json::Value::Array(values) => Kind::ListValue(ListValue {
1670-
values: values
1671-
.into_iter()
1672-
.map(json_to_protobuf_value)
1673-
.collect::<Result<_>>()?,
1674-
}),
1675-
serde_json::Value::Object(fields) => Kind::StructValue(Struct {
1676-
fields: fields
1677-
.into_iter()
1678-
.map(|(key, value)| json_to_protobuf_value(value).map(|value| (key, value)))
1679-
.collect::<Result<_>>()?,
1680-
}),
1681-
};
1682-
1683-
Ok(Value { kind: Some(kind) })
1651+
openshell_core::proto_struct::json_object_to_struct(fields)
1652+
.into_diagnostic()
1653+
.wrap_err("--driver-config-json contains a value that cannot be represented")
16841654
}
16851655

16861656
fn validate_cpu_quantity(value: &str) -> Result<String> {

crates/openshell-core/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,10 @@ Built-in supervisor middleware identifiers and pure configuration validation
5757
live in `openshell_core::middleware`. Policy admission and the supervisor
5858
runtime consume the same contract without introducing a dependency from the
5959
policy crate to the supervisor implementation.
60+
61+
## Protobuf Struct Conversion
62+
63+
Use `openshell_core::proto_struct` when crossing between `serde_json` values and
64+
`prost_types::{Struct, Value}`. Both conversion directions live in this module;
65+
JSON-to-protobuf conversion is fallible so callers cannot silently replace an
66+
unrepresentable number.

crates/openshell-core/src/proto_struct.rs

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,57 @@
11
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
22
// SPDX-License-Identifier: Apache-2.0
33

4-
//! Helpers for decoding `google.protobuf.Struct` values.
4+
//! Helpers for converting `google.protobuf.Struct` values to and from JSON.
55
66
use serde::{Deserialize, Deserializer, de::Error as _};
77

8+
/// Errors converting JSON values into protobuf well-known types.
9+
#[derive(Debug, thiserror::Error)]
10+
pub enum ProtoStructError {
11+
/// A JSON number cannot be represented by protobuf's double value.
12+
#[error("JSON number {0} cannot be represented as a protobuf double")]
13+
UnrepresentableNumber(serde_json::Number),
14+
}
15+
16+
/// Convert a JSON object into a protobuf Struct.
17+
pub fn json_object_to_struct(
18+
object: serde_json::Map<String, serde_json::Value>,
19+
) -> Result<prost_types::Struct, ProtoStructError> {
20+
Ok(prost_types::Struct {
21+
fields: object
22+
.into_iter()
23+
.map(|(key, value)| json_value_to_proto(value).map(|value| (key, value)))
24+
.collect::<Result<_, _>>()?,
25+
})
26+
}
27+
28+
/// Convert a JSON value into a protobuf Value.
29+
pub fn json_value_to_proto(
30+
value: serde_json::Value,
31+
) -> Result<prost_types::Value, ProtoStructError> {
32+
use prost_types::{ListValue, Value, value::Kind};
33+
34+
let kind = match value {
35+
serde_json::Value::Null => Kind::NullValue(0),
36+
serde_json::Value::Bool(value) => Kind::BoolValue(value),
37+
serde_json::Value::Number(value) => Kind::NumberValue(
38+
value
39+
.as_f64()
40+
.ok_or_else(|| ProtoStructError::UnrepresentableNumber(value.clone()))?,
41+
),
42+
serde_json::Value::String(value) => Kind::StringValue(value),
43+
serde_json::Value::Array(values) => Kind::ListValue(ListValue {
44+
values: values
45+
.into_iter()
46+
.map(json_value_to_proto)
47+
.collect::<Result<_, _>>()?,
48+
}),
49+
serde_json::Value::Object(object) => Kind::StructValue(json_object_to_struct(object)?),
50+
};
51+
52+
Ok(Value { kind: Some(kind) })
53+
}
54+
855
/// Convert a protobuf Struct into a JSON object for typed serde decoding.
956
#[must_use]
1057
pub fn struct_to_json_object(
@@ -72,6 +119,24 @@ where
72119
mod tests {
73120
use super::*;
74121

122+
#[test]
123+
fn json_and_proto_values_round_trip() {
124+
let json = serde_json::json!({
125+
"null": null,
126+
"bool": true,
127+
"number": 42.5,
128+
"string": "value",
129+
"list": [1.0, {"nested": "value"}],
130+
});
131+
let serde_json::Value::Object(object) = json.clone() else {
132+
unreachable!();
133+
};
134+
135+
let proto = json_object_to_struct(object).unwrap();
136+
137+
assert_eq!(struct_to_json_value(&proto), json);
138+
}
139+
75140
#[derive(Debug, Default, Deserialize)]
76141
#[serde(default)]
77142
struct TestConfig {

crates/openshell-driver-docker/src/tests.rs

Lines changed: 5 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -118,40 +118,11 @@ fn runtime_config() -> DockerDriverRuntimeConfig {
118118
}
119119

120120
fn json_struct(value: serde_json::Value) -> prost_types::Struct {
121-
match json_value(value).kind {
122-
Some(prost_types::value::Kind::StructValue(value)) => value,
123-
_ => panic!("expected JSON object"),
124-
}
125-
}
126-
127-
fn json_value(value: serde_json::Value) -> prost_types::Value {
128-
match value {
129-
serde_json::Value::Null => prost_types::Value { kind: None },
130-
serde_json::Value::Bool(value) => prost_types::Value {
131-
kind: Some(prost_types::value::Kind::BoolValue(value)),
132-
},
133-
serde_json::Value::Number(value) => prost_types::Value {
134-
kind: value.as_f64().map(prost_types::value::Kind::NumberValue),
135-
},
136-
serde_json::Value::String(value) => prost_types::Value {
137-
kind: Some(prost_types::value::Kind::StringValue(value)),
138-
},
139-
serde_json::Value::Array(values) => prost_types::Value {
140-
kind: Some(prost_types::value::Kind::ListValue(
141-
prost_types::ListValue {
142-
values: values.into_iter().map(json_value).collect(),
143-
},
144-
)),
145-
},
146-
serde_json::Value::Object(values) => prost_types::Value {
147-
kind: Some(prost_types::value::Kind::StructValue(prost_types::Struct {
148-
fields: values
149-
.into_iter()
150-
.map(|(key, value)| (key, json_value(value)))
151-
.collect(),
152-
})),
153-
},
154-
}
121+
let serde_json::Value::Object(object) = value else {
122+
panic!("expected JSON object");
123+
};
124+
openshell_core::proto_struct::json_object_to_struct(object)
125+
.expect("test JSON must convert to a protobuf Struct")
155126
}
156127

157128
fn inspected_volume(driver: &str, options: HashMap<String, String>) -> bollard::models::Volume {

crates/openshell-driver-kubernetes/src/driver.rs

Lines changed: 5 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2200,38 +2200,11 @@ mod tests {
22002200
std::sync::LazyLock::new(|| std::sync::Mutex::new(()));
22012201

22022202
fn json_struct(value: serde_json::Value) -> Struct {
2203-
match json_value(value).kind {
2204-
Some(Kind::StructValue(value)) => value,
2205-
_ => panic!("expected JSON object"),
2206-
}
2207-
}
2208-
2209-
fn json_value(value: serde_json::Value) -> Value {
2210-
match value {
2211-
serde_json::Value::Null => Value { kind: None },
2212-
serde_json::Value::Bool(value) => Value {
2213-
kind: Some(Kind::BoolValue(value)),
2214-
},
2215-
serde_json::Value::Number(value) => Value {
2216-
kind: value.as_f64().map(Kind::NumberValue),
2217-
},
2218-
serde_json::Value::String(value) => Value {
2219-
kind: Some(Kind::StringValue(value)),
2220-
},
2221-
serde_json::Value::Array(values) => Value {
2222-
kind: Some(Kind::ListValue(prost_types::ListValue {
2223-
values: values.into_iter().map(json_value).collect(),
2224-
})),
2225-
},
2226-
serde_json::Value::Object(values) => Value {
2227-
kind: Some(Kind::StructValue(Struct {
2228-
fields: values
2229-
.into_iter()
2230-
.map(|(key, value)| (key, json_value(value)))
2231-
.collect(),
2232-
})),
2233-
},
2234-
}
2203+
let serde_json::Value::Object(object) = value else {
2204+
panic!("expected JSON object");
2205+
};
2206+
openshell_core::proto_struct::json_object_to_struct(object)
2207+
.expect("test JSON must convert to a protobuf Struct")
22352208
}
22362209

22372210
fn kube_api_error(code: u16, message: &str) -> KubeError {

crates/openshell-driver-podman/src/container.rs

Lines changed: 5 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1128,40 +1128,11 @@ mod tests {
11281128
std::sync::LazyLock::new(|| std::sync::Mutex::new(()));
11291129

11301130
fn json_struct(value: Value) -> prost_types::Struct {
1131-
match json_value(value).kind {
1132-
Some(prost_types::value::Kind::StructValue(value)) => value,
1133-
_ => panic!("expected JSON object"),
1134-
}
1135-
}
1136-
1137-
fn json_value(value: Value) -> prost_types::Value {
1138-
match value {
1139-
Value::Null => prost_types::Value { kind: None },
1140-
Value::Bool(value) => prost_types::Value {
1141-
kind: Some(prost_types::value::Kind::BoolValue(value)),
1142-
},
1143-
Value::Number(value) => prost_types::Value {
1144-
kind: value.as_f64().map(prost_types::value::Kind::NumberValue),
1145-
},
1146-
Value::String(value) => prost_types::Value {
1147-
kind: Some(prost_types::value::Kind::StringValue(value)),
1148-
},
1149-
Value::Array(values) => prost_types::Value {
1150-
kind: Some(prost_types::value::Kind::ListValue(
1151-
prost_types::ListValue {
1152-
values: values.into_iter().map(json_value).collect(),
1153-
},
1154-
)),
1155-
},
1156-
Value::Object(values) => prost_types::Value {
1157-
kind: Some(prost_types::value::Kind::StructValue(prost_types::Struct {
1158-
fields: values
1159-
.into_iter()
1160-
.map(|(key, value)| (key, json_value(value)))
1161-
.collect(),
1162-
})),
1163-
},
1164-
}
1131+
let Value::Object(object) = value else {
1132+
panic!("expected JSON object");
1133+
};
1134+
proto_struct::json_object_to_struct(object)
1135+
.expect("test JSON must convert to a protobuf Struct")
11651136
}
11661137

11671138
fn gpu_resources(count: Option<u32>) -> ResourceRequirements {

crates/openshell-driver-podman/src/driver.rs

Lines changed: 5 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1274,41 +1274,12 @@ mod tests {
12741274
PodmanComputeDriver::for_tests(config)
12751275
}
12761276

1277-
fn json_value(value: serde_json::Value) -> prost_types::Value {
1278-
match value {
1279-
serde_json::Value::Null => prost_types::Value { kind: None },
1280-
serde_json::Value::Bool(value) => prost_types::Value {
1281-
kind: Some(prost_types::value::Kind::BoolValue(value)),
1282-
},
1283-
serde_json::Value::Number(value) => prost_types::Value {
1284-
kind: value.as_f64().map(prost_types::value::Kind::NumberValue),
1285-
},
1286-
serde_json::Value::String(value) => prost_types::Value {
1287-
kind: Some(prost_types::value::Kind::StringValue(value)),
1288-
},
1289-
serde_json::Value::Array(values) => prost_types::Value {
1290-
kind: Some(prost_types::value::Kind::ListValue(
1291-
prost_types::ListValue {
1292-
values: values.into_iter().map(json_value).collect(),
1293-
},
1294-
)),
1295-
},
1296-
serde_json::Value::Object(values) => prost_types::Value {
1297-
kind: Some(prost_types::value::Kind::StructValue(prost_types::Struct {
1298-
fields: values
1299-
.into_iter()
1300-
.map(|(key, value)| (key, json_value(value)))
1301-
.collect(),
1302-
})),
1303-
},
1304-
}
1305-
}
1306-
13071277
fn json_struct(value: serde_json::Value) -> prost_types::Struct {
1308-
match json_value(value).kind {
1309-
Some(prost_types::value::Kind::StructValue(value)) => value,
1310-
_ => panic!("expected JSON object"),
1311-
}
1278+
let serde_json::Value::Object(object) = value else {
1279+
panic!("expected JSON object");
1280+
};
1281+
openshell_core::proto_struct::json_object_to_struct(object)
1282+
.expect("test JSON must convert to a protobuf Struct")
13121283
}
13131284

13141285
fn sandbox_with_volume_mount(volume: &str) -> DriverSandbox {

0 commit comments

Comments
 (0)