agent-ctl: Allow API specification in JSON format

Update the `agent-ctl` tool to allow API fields to be specified in JSON
format, either directly on the command-line, or via a file URI.

This feature is made possible by enabling `serde` support in the agent
`protocols` crate. Careful use of the `serde` macros allows the
`agent-ctl` tool to accept _partially_ specified API objects in JSON
format; fields that are not specified are set to the default value for
their respective types.

`build.rs` changes based on work by Fupan.

Fixes: #2978.

Contributions-by: Fupan Li <lifupan@gmail.com>
Contributions-by: Bin Liu <bin@hyper.sh>
Signed-off-by: James O. D. Hunt <james.o.hunt@intel.com>
This commit is contained in:
James O. D. Hunt 2021-10-26 14:02:07 +01:00
parent 18c47fe8f3
commit 8ab90e1068
10 changed files with 729 additions and 351 deletions

49
src/agent/Cargo.lock generated
View File

@ -4,9 +4,9 @@ version = 3
[[package]]
name = "addr2line"
version = "0.15.1"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03345e98af8f3d786b6d9f656ccfa6ac316d954e92bc4841f0bba20789d5fb5a"
checksum = "3e61f2b7f93d2c7d2b08263acaa4a363b3e276806c68af6134c44f523bf1aacd"
dependencies = [
"gimli",
]
@ -83,9 +83,9 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
[[package]]
name = "backtrace"
version = "0.3.59"
version = "0.3.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4717cfcbfaa661a0fd48f8453951837ae7e8f81e481fbb136e3202d72805a744"
checksum = "e7a905d892734eea339e896738c14b9afce22b5318f64b951e70bf3844419b01"
dependencies = [
"addr2line",
"cc",
@ -414,15 +414,15 @@ dependencies = [
[[package]]
name = "gimli"
version = "0.24.0"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e4075386626662786ddb0ec9081e7c7eeb1ba31951f447ca780ef9f5d568189"
checksum = "f0a01e0497841a3b2db4f8afa483cce65f7e96a3498bd6c541734792aeac8fe7"
[[package]]
name = "heck"
version = "0.3.2"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87cbf45460356b7deeb5e3415b5563308c0a9b057c85e12b06ad551f98d0a6ac"
checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
dependencies = [
"unicode-segmentation",
]
@ -871,9 +871,12 @@ dependencies = [
[[package]]
name = "object"
version = "0.24.0"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a5b3dd1c072ee7963717671d1ca129f1048fda25edea6b752bfc71ac8854170"
checksum = "c55827317fb4c08822499848a14237d2874d6f139828893017237e7ab93eb386"
dependencies = [
"memchr",
]
[[package]]
name = "oci"
@ -1138,6 +1141,10 @@ name = "protobuf"
version = "2.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e86d370532557ae7573551a1ec8235a0f8d6cb276c7c9e6aa490b511c447485"
dependencies = [
"serde",
"serde_derive",
]
[[package]]
name = "protobuf-codegen"
@ -1164,6 +1171,8 @@ version = "0.1.0"
dependencies = [
"async-trait",
"protobuf",
"serde",
"serde_json",
"ttrpc",
"ttrpc-codegen",
]
@ -1304,9 +1313,9 @@ dependencies = [
[[package]]
name = "rustc-demangle"
version = "0.1.19"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "410f7acf3cb3a44527c5d9546bad4bf4e6c460915d5f9f2fc524498bfe8f70ce"
checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342"
[[package]]
name = "rustjail"
@ -1364,18 +1373,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "serde"
version = "1.0.129"
version = "1.0.130"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1f72836d2aa753853178eda473a3b9d8e4eefdaf20523b919677e6de489f8f1"
checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.129"
version = "1.0.130"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e57ae87ad533d9a56427558b516d0adac283614e347abf85b0dc0cbbf0a249f3"
checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b"
dependencies = [
"proc-macro2 1.0.26",
"quote 1.0.9",
@ -1384,9 +1393,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.64"
version = "1.0.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79"
checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8"
dependencies = [
"itoa",
"ryu",
@ -1804,9 +1813,9 @@ dependencies = [
[[package]]
name = "unicode-segmentation"
version = "1.7.1"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796"
checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
[[package]]
name = "unicode-xid"

View File

@ -4,10 +4,16 @@ version = "0.1.0"
authors = ["The Kata Containers community <kata-dev@lists.katacontainers.io>"]
edition = "2018"
[features]
default = []
with-serde = [ "serde", "serde_json" ]
[dependencies]
ttrpc = { version = "0.5.0", features = ["async"] }
async-trait = "0.1.42"
protobuf = "=2.14.0"
protobuf = { version = "=2.14.0", features = ["with-serde"] }
serde = { version = "1.0.130", features = ["derive"], optional = true }
serde_json = { version = "1.0.68", optional = true }
[build-dependencies]
ttrpc-codegen = "0.2.0"

View File

@ -3,29 +3,148 @@
// SPDX-License-Identifier: Apache-2.0
//
use std::fs;
use ttrpc_codegen::{Codegen, Customize};
use std::fs::File;
use std::io::{BufRead, BufReader, Read, Write};
use std::path::Path;
use std::process::exit;
use ttrpc_codegen::{Codegen, Customize, ProtobufCustomize};
fn replace_text_in_file(file_name: &str, from: &str, to: &str) -> Result<(), std::io::Error> {
let mut src = File::open(file_name)?;
let mut contents = String::new();
src.read_to_string(&mut contents).unwrap();
drop(src);
let new_contents = contents.replace(from, to);
let mut dst = File::create(&file_name)?;
dst.write_all(new_contents.as_bytes())?;
Ok(())
}
fn use_serde(protos: &[&str], out_dir: &Path) -> Result<(), std::io::Error> {
protos
.iter()
.try_for_each(|f: &&str| -> Result<(), std::io::Error> {
let out_file = Path::new(f)
.file_name()
.and_then(|s| s.to_str())
.ok_or(format!("failed to get proto file name for {:?}", f))
.map(|s| {
let t = s.replace(".proto", ".rs");
out_dir.join(t)
})
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?
.to_str()
.ok_or(format!("cannot convert {:?} path to string", f))
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?
.to_string();
replace_text_in_file(
&out_file,
"derive(Serialize, Deserialize)",
"derive(serde::Serialize, serde::Deserialize)",
)
})
}
fn handle_file(autogen_comment: &str, rust_filename: &str) -> Result<(), std::io::Error> {
let mut new_contents = Vec::new();
let file = File::open(rust_filename)?;
let reader = BufReader::new(file);
// Guard the code since it is only needed for the agent-ctl tool,
// not the agent itself.
let serde_default_code = r#"#[cfg_attr(feature = "with-serde", serde(default))]"#;
for line in reader.lines() {
let line = line?;
new_contents.push(line.clone());
let pattern = "//! Generated file from";
if line.starts_with(&pattern) {
new_contents.push(autogen_comment.into());
}
let struct_pattern = "pub struct ";
// Although we've requested serde support via `Customize`, to
// allow the `kata-agent-ctl` tool to partially deserialise structures
// specified in JSON, we need this bit of additional magic.
if line.starts_with(&struct_pattern) {
new_contents.insert(new_contents.len() - 1, serde_default_code.trim().into());
}
}
let data = new_contents.join("\n");
let mut dst = File::create(&rust_filename)?;
dst.write_all(data.as_bytes())?;
Ok(())
}
fn real_main() -> Result<(), std::io::Error> {
let autogen_comment = format!("\n//! Generated by {:?} ({:?})", file!(), module_path!());
fn main() {
let protos = vec![
"protos/types.proto",
"protos/agent.proto",
"protos/health.proto",
"protos/google/protobuf/empty.proto",
"protos/health.proto",
"protos/oci.proto",
"protos/types.proto",
];
// Tell Cargo that if the .proto files changed, to rerun this build script.
protos
.iter()
.for_each(|p| println!("cargo:rerun-if-changed={}", &p));
let ttrpc_options = Customize {
async_server: true,
..Default::default()
};
let protobuf_options = ProtobufCustomize {
serde_derive: Some(true),
..Default::default()
};
let out_dir = Path::new("src");
Codegen::new()
.out_dir("src")
.out_dir(out_dir)
.inputs(&protos)
.include("protos")
.customize(ttrpc_options)
.rust_protobuf()
.customize(Customize {
async_server: true,
..Default::default()
})
.run()
.expect("Gen codes failed.");
.rust_protobuf_customize(protobuf_options)
.run()?;
for file in protos.iter() {
let proto_filename = Path::new(file).file_name().unwrap();
let generated_file = proto_filename
.to_str()
.ok_or("failed")
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?
.replace(".proto", ".rs");
let out_file = out_dir.join(generated_file);
let out_file_str = out_file
.to_str()
.ok_or("failed")
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
handle_file(&autogen_comment, out_file_str)?;
}
// There is a message named 'Box' in oci.proto
// so there is a struct named 'Box', we should replace Box<Self> to ::std::boxed::Box<Self>
@ -34,11 +153,16 @@ fn main() {
"src/oci.rs",
"self: Box<Self>",
"self: ::std::boxed::Box<Self>",
)
.unwrap();
)?;
use_serde(&protos, out_dir)?;
Ok(())
}
fn replace_text_in_file(file_name: &str, from: &str, to: &str) -> Result<(), std::io::Error> {
let new_contents = fs::read_to_string(file_name)?.replace(from, to);
fs::write(&file_name, new_contents.as_bytes())
fn main() {
if let Err(e) = real_main() {
eprintln!("ERROR: {}", e);
exit(1);
}
}

View File

@ -818,6 +818,10 @@ name = "protobuf"
version = "2.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e86d370532557ae7573551a1ec8235a0f8d6cb276c7c9e6aa490b511c447485"
dependencies = [
"serde",
"serde_derive",
]
[[package]]
name = "protobuf-codegen"
@ -844,6 +848,8 @@ version = "0.1.0"
dependencies = [
"async-trait",
"protobuf",
"serde",
"serde_json",
"ttrpc",
"ttrpc-codegen",
]
@ -1050,18 +1056,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "serde"
version = "1.0.126"
version = "1.0.130"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03"
checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.126"
version = "1.0.130"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43"
checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b"
dependencies = [
"proc-macro2 1.0.26",
"quote 1.0.9",
@ -1070,9 +1076,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.64"
version = "1.0.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79"
checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8"
dependencies = [
"itoa",
"ryu",

View File

@ -10,7 +10,7 @@ authors = ["The Kata Containers community <kata-dev@lists.katacontainers.io>"]
edition = "2018"
[dependencies]
protocols = { path = "../../src/agent/protocols" }
protocols = { path = "../../src/agent/protocols", features = ["with-serde"] }
rustjail = { path = "../../src/agent/rustjail" }
oci = { path = "../../src/agent/oci" }
@ -20,6 +20,8 @@ anyhow = "1.0.31"
hex = "0.4.2"
byteorder = "1.3.4"
# Note: this crate sets the slog 'max_*' features which allows the log level
# to be modified at runtime.
logging = { path = "../../pkg/logging" }
slog = "2.5.2"
slog-scope = "4.3.0"
@ -35,5 +37,5 @@ ttrpc = { version = "0.5.0" }
humantime = "2.0.0"
# For Options (state passing)
serde = { version = "1.0.110", features = ["derive"] }
serde_json = "1.0.53"
serde = { version = "1.0.130", features = ["derive"] }
serde_json = "1.0.68"

View File

@ -60,6 +60,49 @@ $ mkdir -p "$rootfs_dir" && (cd "$bundle_dir" && runc spec)
$ sudo docker export $(sudo docker create "$image") | tar -C "$rootfs_dir" -xvf -
```
### Specify API commands to run
The tool allows one or more API commands to be specified using the `-c` or
`--cmd` command-line options. At their simplest, these are just the name of
the API commands, which will make the API command using default values
(generally blank or empty) where possible. However, some API calls require
some basic value to be specified such as a sandbox ID or container ID. For
these calls, the tool will generate a value by default unless told not to.
If the user wishes to, they may specify these values as part of the command
using `name=value` syntax.
In addition to this, it is possible to specify either a complete or partial
set of values for the API call using JSON syntax, either directly on the
command-line or via a file URI.
The table below summarises the possible ways of specifying an API call to
make.
| CLI values | API Query |
|-|-|
| `-c 'SomeAPIName' -n` | Calls the API using the default values for all request options |
| `-c 'SomeAPIName'` | Calls the API specifying some values automatically if possible |
| `-c 'SomeAPIName foo=bar baz="hello world" x=3 y="a cat"'` | Calls the API specifying various values in name/value form |
| `-c 'SomeAPIName json://{}' -n` | Calls the API specifying empty values via an empty JSON document |
| `-c 'SomeAPIName json://{"foo": true, "bar": "hello world"}' -n` | Calls the API specifying _some_ values in JSON syntax |
| `-c 'SomeAPIName file:///foo.json' -n` | Calls the API passing the JSON values from the specified file |
#### JSON Example
An example showing how to specify the messages fields for an API call
(`GetGuestDetails`):
```sh
$ cargo run -- -l debug connect --server-address "unix://@/tmp/foo.socket" --bundle-dir "$bundle_dir" -c Check -c 'GetGuestDetails json://{"mem_block_size": true, "mem_hotplug_probe": true}'
```
> **Note:**
>
> For details of the names of the APIs to call and the available fields
> in each API, see the [Code Summary](#code-summary) section.
### Connect to a real Kata Container
The method used to connect to Kata Containers agent depends on the configured

File diff suppressed because it is too large Load Diff

View File

@ -96,6 +96,15 @@ fn make_examples_text(program_name: &str) -> String {
$ {program} connect --server-address "{vsock_server_address}" --repeat -1 --cmd GetGuestDetails
- Query guest details, asking for full details by specifying the API request object in JSON format:
$ {program} connect --server-address "{vsock_server_address}" -c 'GetGuestDetails json://{{"mem_block_size": true, "mem_hotplug_probe": true}}'
- Query guest details, asking for extra detail by partially specifying the API request object in JSON format from a file:
$ echo '{{"mem_block_size": true}}' > /tmp/api.json
$ {program} connect --server-address "{vsock_server_address}" -c 'GetGuestDetails file:///tmp/api.json'
- Send a 'SIGUSR1' signal to a container process:
$ {program} connect --server-address "{vsock_server_address}" --cmd 'SignalProcess signal=usr1 sid={sandbox_id} cid={container_id}'
@ -169,6 +178,7 @@ fn connect(name: &str, global_args: clap::ArgMatches) -> Result<()> {
let bundle_dir = args.value_of("bundle-dir").unwrap_or("").to_string();
let hybrid_vsock = args.is_present("hybrid-vsock");
let no_auto_values = args.is_present("no-auto-values");
let cfg = Config {
server_address,
@ -178,6 +188,7 @@ fn connect(name: &str, global_args: clap::ArgMatches) -> Result<()> {
timeout_nano,
hybrid_vsock_port,
hybrid_vsock,
no_auto_values,
};
let result = rpc::run(&logger, &cfg, commands);
@ -255,6 +266,12 @@ fn real_main() -> Result<()> {
.long("interactive")
.help("Allow interactive client"),
)
.arg(
Arg::with_name("no-auto-values")
.short("n")
.long("no-auto-values")
.help("Disable automatic generation of values for sandbox ID, container ID, etc"),
)
.arg(
Arg::with_name("server-address")
.long("server-address")

View File

@ -18,4 +18,5 @@ pub struct Config {
pub interactive: bool,
pub hybrid_vsock: bool,
pub ignore_errors: bool,
pub no_auto_values: bool,
}

View File

@ -23,8 +23,10 @@ use protocols::oci::{
User as ttrpcUser,
};
use rand::Rng;
use serde::de::DeserializeOwned;
use slog::{debug, warn};
use std::collections::HashMap;
use std::fs::File;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
@ -86,7 +88,7 @@ lazy_static! {
}
pub fn signame_to_signum(name: &str) -> Result<u8> {
if name == "" {
if name.is_empty() {
return Err(anyhow!("invalid signal"));
}
@ -121,7 +123,7 @@ pub fn signame_to_signum(name: &str) -> Result<u8> {
// Convert a human time fornat (like "2s") into the equivalent number
// of nano seconds.
pub fn human_time_to_ns(human_time: &str) -> Result<i64> {
if human_time == "" || human_time == "0" {
if human_time.is_empty() || human_time.eq("0") {
return Ok(0);
}
@ -143,17 +145,17 @@ pub fn human_time_to_ns(human_time: &str) -> Result<i64> {
// - All other options values default to an empty string.
// - All options are saved in the global hash before being returned for future
// use.
pub fn get_option(name: &str, options: &mut Options, args: &str) -> String {
pub fn get_option(name: &str, options: &mut Options, args: &str) -> Result<String> {
let words: Vec<&str> = args.split_whitespace().collect();
for word in words {
let fields: Vec<String> = word.split("=").map(|s| s.to_string()).collect();
let fields: Vec<String> = word.split('=').map(|s| s.to_string()).collect();
if fields.len() < 2 {
continue;
}
if fields[0] == "" {
if fields[0].is_empty() {
continue;
}
@ -162,17 +164,10 @@ pub fn get_option(name: &str, options: &mut Options, args: &str) -> String {
let mut value = fields[1..].join("=");
// Expand "spec=file:///some/where/config.json"
if key == "spec" && value.starts_with(FILE_URI) {
let spec_file = match uri_to_filename(&value) {
Ok(file) => file,
Err(e) => {
warn!(sl!(), "failed to handle spec file URI: {:}", e);
if key.eq("spec") && value.starts_with(FILE_URI) {
let (_, spec_file) = split_uri(&value)?;
"".to_string()
}
};
if spec_file != "" {
if !spec_file.is_empty() {
value = match spec_file_to_string(spec_file) {
Ok(s) => s,
Err(e) => {
@ -197,7 +192,7 @@ pub fn get_option(name: &str, options: &mut Options, args: &str) -> String {
if let Some(value) = options.get(name) {
debug!(sl!(), "using option {:?}={:?} ({})", name, value, msg);
return value.to_string();
return Ok(value.into());
}
msg = "generated";
@ -210,14 +205,14 @@ pub fn get_option(name: &str, options: &mut Options, args: &str) -> String {
// Default to CID
"exec_id" => {
msg = "derived";
//derived = true;
match options.get("cid") {
Some(value) => value.to_string(),
None => "".to_string(),
Some(value) => value,
None => "",
}
.into()
}
_ => "".to_string(),
_ => "".into(),
};
debug!(sl!(), "using option {:?}={:?} ({})", name, value, msg);
@ -225,7 +220,7 @@ pub fn get_option(name: &str, options: &mut Options, args: &str) -> String {
// Store auto-generated value
options.insert(name.to_string(), value.to_string());
value
Ok(value)
}
pub fn generate_random_hex_string(len: u32) -> String {
@ -252,7 +247,7 @@ pub fn random_container_id() -> String {
}
fn config_file_from_bundle_dir(bundle_dir: &str) -> Result<String> {
if bundle_dir == "" {
if bundle_dir.is_empty() {
return Err(anyhow!("missing bundle directory"));
}
@ -739,18 +734,20 @@ fn oci_to_ttrpc(bundle_dir: &str, cid: &str, oci: &ociSpec) -> Result<ttrpcSpec>
Ok(ttrpc_spec)
}
fn uri_to_filename(uri: &str) -> Result<String> {
if !uri.starts_with(FILE_URI) {
return Err(anyhow!(format!("invalid URI: {:?}", uri)));
}
// Split a URI and return a tuple comprising the scheme and the data.
//
// Note that we have to use our own parsing since "json://" is not
// an official schema ;(
fn split_uri(uri: &str) -> Result<(String, String)> {
const URI_DELIMITER: &str = "://";
let fields: Vec<&str> = uri.split(FILE_URI).collect();
let fields: Vec<&str> = uri.split(URI_DELIMITER).collect();
if fields.len() != 2 {
return Err(anyhow!(format!("invalid URI: {:?}", uri)));
return Err(anyhow!("invalid URI: {:?}", uri));
}
Ok(fields[1].to_string())
Ok((fields[0].into(), fields[1].into()))
}
pub fn spec_file_to_string(spec_file: String) -> Result<String> {
@ -766,9 +763,9 @@ pub fn get_oci_spec_json(cfg: &Config) -> Result<String> {
}
pub fn get_ttrpc_spec(options: &mut Options, cid: &str) -> Result<ttrpcSpec> {
let bundle_dir = get_option("bundle-dir", options, "");
let bundle_dir = get_option("bundle-dir", options, "")?;
let json_spec = get_option("spec", options, "");
let json_spec = get_option("spec", options, "")?;
assert_ne!(json_spec, "");
let oci_spec: ociSpec = serde_json::from_str(&json_spec).map_err(|e| anyhow!(e))?;
@ -789,3 +786,67 @@ pub fn str_to_bytes(s: &str) -> Result<Vec<u8>> {
Ok(s.as_bytes().to_vec())
}
}
// Returns a request object of the type requested.
//
// Call as:
//
// ```rust
// let req1: SomeType = make_request(args)?;
// let req2: AnotherType = make_request(args)?;
// ```
//
// The args string can take a number of forms:
//
// - A file URI:
//
// The string is expected to start with 'file://' with the full path to
// a local file containing a complete or partial JSON document.
//
// Example: 'file:///some/where/foo.json'
//
// - A JSON URI:
//
// This invented 'json://{ ...}' URI allows either a complete JSON document
// or a partial JSON fragment to be specified. The JSON takes the form of
// the JSON serialised protocol buffers files that specify the Kata Agent
// API.
//
// - If the complete document for the specified type is provided, the values
// specified are deserialised into the returned
// type.
//
// - If a partial document is provided, the values specified are
// deserialised into the returned type and all remaining elements take their
// default values.
//
// - If no values are specified, all returned type will be created as
// if TypeName::default() had been specified instead.
//
// Example 1 (Complete and valid empty JSON document): 'json://{}'
// Example 2 (Valid partial JSON document): 'json://{"foo": true, "bar": "hello"}'
// Example 3 (GetGuestDetails API example):
//
// let args = r#"json://{"mem_block_size": true, "mem_hotplug_probe": true}"#;
//
// let req: GetGuestDetailsRequest = make_request(args)?;
//
pub fn make_request<T: Default + DeserializeOwned>(args: &str) -> Result<T> {
if args.is_empty() {
return Ok(Default::default());
}
let (scheme, data) = split_uri(args)?;
match scheme.as_str() {
"json" => Ok(serde_json::from_str(&data)?),
"file" => {
let file = File::open(data)?;
Ok(serde_json::from_reader(file)?)
}
// Don't error since the args may contain key=value pairs which
// are not handled by this functionz.
_ => Ok(Default::default()),
}
}