Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add 'apiclient get' for simple API retrieval #1836

Merged
merged 1 commit into from
Nov 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
zmrow marked this conversation as resolved.
Show resolved Hide resolved
```

### 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