Skip to content

Commit

Permalink
Merge pull request #1836 from tjkirch/apiclient-get
Browse files Browse the repository at this point in the history
Add 'apiclient get' for simple API retrieval
  • Loading branch information
tjkirch committed Nov 30, 2021
2 parents cb745ae + 2e49159 commit 47640f7
Show file tree
Hide file tree
Showing 8 changed files with 261 additions and 22 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ Here we'll describe the settings you can configure on your Bottlerocket instance

You can see the current settings with an API request:
```
apiclient -u /settings
apiclient get settings
```

This will return all of the current settings in JSON format.
Expand Down
15 changes: 7 additions & 8 deletions sources/api/apiclient/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,17 @@ It can be pointed to another socket using `--socket-path`, for example for local
The most important use is probably checking your current settings:

```
apiclient -u /settings
apiclient get settings
```

You can also request the values of specific settings using `keys`:
`get` will request all settings whose names start with the given prefix, so you can drill down into specific areas of interest:
```
apiclient -u /settings?keys=settings.motd,settings.kernel.lockdown
apiclient get settings.host-containers.admin
```

Or, request all settings whose names start with a given `prefix`.
(Note: here, the prefix should not start with "settings." since it's assumed.)
Or, request some specific settings:
```
apiclient -u /settings?prefix=host-containers.admin
apiclient get settings.motd settings.kernel.lockdown
```

### Set mode
Expand Down Expand Up @@ -193,8 +192,8 @@ For example, if you want the name "FOO", you can `PATCH` to `/settings?tx=FOO` a
## apiclient library

The apiclient library provides high-level methods to interact with the Bottlerocket API. See
the documentation for submodules [`apply`], [`exec`], [`reboot`], [`set`], and [`update`] for
high-level helpers.
the documentation for submodules [`apply`], [`exec`], [`get`], [`reboot`], [`set`], and
[`update`] for high-level helpers.

For more control, and to handle APIs without high-level wrappers, there are also 'raw' methods
to query an HTTP API over a Unix-domain socket.
Expand Down
11 changes: 5 additions & 6 deletions sources/api/apiclient/README.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,17 @@ It can be pointed to another socket using `--socket-path`, for example for local
The most important use is probably checking your current settings:

```
apiclient -u /settings
apiclient get settings
```

You can also request the values of specific settings using `keys`:
`get` will request all settings whose names start with the given prefix, so you can drill down into specific areas of interest:
```
apiclient -u /settings?keys=settings.motd,settings.kernel.lockdown
apiclient get settings.host-containers.admin
```

Or, request all settings whose names start with a given `prefix`.
(Note: here, the prefix should not start with "settings." since it's assumed.)
Or, request some specific settings:
```
apiclient -u /settings?prefix=host-containers.admin
apiclient get settings.motd settings.kernel.lockdown
```

### Set mode
Expand Down
72 changes: 72 additions & 0 deletions sources/api/apiclient/src/get.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
use snafu::{OptionExt, ResultExt};
use std::path::Path;

mod merge_json;
use merge_json::merge_json;

/// Fetches the given prefixes from the API and merges them into a single Value. (It's not
/// expected that given prefixes would overlap, but if they do, later ones take precedence.)
pub async fn get_prefixes<P>(socket_path: P, prefixes: Vec<String>) -> Result<serde_json::Value>
where
P: AsRef<Path>,
{
let mut results: Vec<serde_json::Value> = Vec::with_capacity(prefixes.len());

// Fetch all given prefixes into separate Values.
for prefix in prefixes {
let uri = format!("/?prefix={}", prefix);
let method = "GET";
let (_status, body) = crate::raw_request(&socket_path, &uri, method, None)
.await
.context(error::Request { uri, method })?;
let value = serde_json::from_str(&body).context(error::ResponseJson { body })?;
results.push(value);
}

// Merge results together.
results
.into_iter()
.reduce(|mut merge_into, merge_from| {
merge_json(&mut merge_into, merge_from);
merge_into
})
.context(error::NoPrefixes)
}

/// Fetches the given URI from the API and returns the result as an untyped Value.
pub async fn get_uri<P>(socket_path: P, uri: String) -> Result<serde_json::Value>
where
P: AsRef<Path>,
{
let method = "GET";
let (_status, body) = crate::raw_request(&socket_path, &uri, method, None)
.await
.context(error::Request { uri, method })?;
serde_json::from_str(&body).context(error::ResponseJson { body })
}

mod error {
use snafu::Snafu;

#[derive(Debug, Snafu)]
#[snafu(visibility = "pub(super)")]
pub enum Error {
#[snafu(display("Must give prefixes to query"))]
NoPrefixes,

#[snafu(display("Failed {} request to '{}': {}", method, uri, source))]
Request {
method: String,
uri: String,
source: crate::Error,
},

#[snafu(display("Response contained invalid JSON '{}' - {}", body, source))]
ResponseJson {
body: String,
source: serde_json::Error,
},
}
}
pub use error::Error;
pub type Result<T> = std::result::Result<T, error::Error>;
96 changes: 96 additions & 0 deletions sources/api/apiclient/src/get/merge_json.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
use serde_json::{map::Entry, Value};

/// This modifies the first given JSON Value by inserting any values from the second Value.
///
/// This is done recursively. Any time a scalar or array is seen, the left side is set to match
/// the right side. Any time an object is seen, we iterate through the keys of the objects; if the
/// left side does not have the key from the right side, it's inserted, otherwise we recursively
/// merge the values in each object for that key.
// Logic and tests taken from storewolf::merge-toml, modified for serde_json.
pub(super) fn merge_json(merge_into: &mut Value, merge_from: Value) {
match (merge_into, merge_from) {
// If we see objects, we recursively merge each key.
(Value::Object(merge_into), Value::Object(merge_from)) => {
for (merge_from_key, merge_from_val) in merge_from.into_iter() {
// Check if the left has the same key as the right.
match merge_into.entry(merge_from_key) {
// If not, we can just insert the value.
Entry::Vacant(entry) => {
entry.insert(merge_from_val);
}
// If so, we need to recursively merge; we don't want to replace an entire
// table, for example, because the left may have some distinct inner keys.
Entry::Occupied(ref mut entry) => {
merge_json(entry.get_mut(), merge_from_val);
}
}
}
}

// If we see a scalar, we replace the left with the right. We treat arrays like scalars so
// behavior is clear - no question about whether we're appending right onto left, etc.
(merge_into, merge_from) => {
*merge_into = merge_from;
}
}
}

#[cfg(test)]
mod test {
use super::merge_json;
use serde_json::json;

#[test]
fn recursion() {
let mut left = json! {{
"top1": "left top1",
"top2": "left top2",
"settings": {
"inner": {
"inner_setting1": "left inner_setting1",
"inner_setting2": "left inner_setting2"
}
}
}};
let right = json! {{
"top1": "right top1",
"settings": {
"setting": "right setting",
"inner": {
"inner_setting1": "right inner_setting1",
"inner_setting3": "right inner_setting3"
}
}
}};
let expected = json! {{
// "top1" is being overwritten from right.
"top1": "right top1",
// "top2" is only in the left and remains.
"top2": "left top2",
"settings": {
// "setting" is only in the right side.
"setting": "right setting",
// "inner" tests that recursion works.
"inner": {
// inner_setting1 is replaced.
"inner_setting1": "right inner_setting1",
// 2 is untouched.
"inner_setting2": "left inner_setting2",
// 3 is new.
"inner_setting3": "right inner_setting3"
}
}
}};
merge_json(&mut left, right);
assert_eq!(left, expected);
}

#[test]
fn array() {
let mut left = json!({"a": [1, 2, 3]});
let right = json!({"a": [4, 5]});
let expected = json!({"a": [4, 5]});
merge_json(&mut left, right);
assert_eq!(left, expected);
}
}
5 changes: 3 additions & 2 deletions sources/api/apiclient/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#![deny(rust_2018_idioms)]

//! The apiclient library provides high-level methods to interact with the Bottlerocket API. See
//! the documentation for submodules [`apply`], [`exec`], [`reboot`], [`set`], and [`update`] for
//! high-level helpers.
//! the documentation for submodules [`apply`], [`exec`], [`get`], [`reboot`], [`set`], and
//! [`update`] for high-level helpers.
//!
//! For more control, and to handle APIs without high-level wrappers, there are also 'raw' methods
//! to query an HTTP API over a Unix-domain socket.
Expand All @@ -23,6 +23,7 @@ use std::path::Path;

pub mod apply;
pub mod exec;
pub mod get;
pub mod reboot;
pub mod set;
pub mod update;
Expand Down
Loading

0 comments on commit 47640f7

Please sign in to comment.