Skip to content

Commit 4c97027

Browse files
author
Anish Cheraku
committed
apiclient: add support for ssm:// URIs
1 parent ed78c98 commit 4c97027

File tree

6 files changed

+627
-153
lines changed

6 files changed

+627
-153
lines changed

sources/Cargo.lock

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

sources/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ aws-sdk-ec2 = "1"
116116
aws-sdk-eks = "1"
117117
aws-sdk-s3 = "1"
118118
aws-sdk-secretsmanager = "1"
119+
aws-sdk-ssm = "1"
119120
aws-smithy-runtime = "1"
120121
aws-smithy-runtime-api = "1"
121122
aws-smithy-types = "1"

sources/api/apiclient/Cargo.toml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,16 @@ fips = ["tls", "aws-lc-rs/fips", "rustls/fips"]
1616

1717
[dependencies]
1818
async-trait.workspace = true
19-
aws-smithy-types.workspace = true
2019
aws-config.workspace = true
2120
aws-lc-rs = { workspace = true, optional = true, features = ["bindgen"] }
2221
aws-sdk-s3.workspace = true
2322
aws-sdk-secretsmanager.workspace = true
23+
aws-sdk-ssm.workspace = true
2424
base64.workspace = true
2525
constants.workspace = true
2626
datastore.workspace = true
2727
futures.workspace = true
28+
futures-util.workspace = true
2829
futures-channel.workspace = true
2930
http.workspace = true
3031
httparse.workspace = true
@@ -36,19 +37,22 @@ log.workspace = true
3637
models.workspace = true
3738
nix.workspace = true
3839
rand = { workspace = true, features = ["default"] }
39-
reqwest.workspace = true
40+
reqwest.workspace = true
4041
retry-read.workspace = true
4142
rustls = { workspace = true, optional = true }
4243
serde = { workspace = true, features = ["derive"] }
4344
serde_json.workspace = true
4445
signal-hook.workspace = true
4546
simplelog.workspace = true
4647
snafu = { workspace = true, features = ["futures"] }
48+
test-case.workspace = true
4749
tokio = { workspace = true, features = ["fs", "io-std", "io-util", "macros", "rt-multi-thread", "time"] }
4850
tokio-tungstenite = { workspace = true, features = ["connect"] }
4951
toml.workspace = true
5052
unindent.workspace = true
5153
url.workspace = true
54+
aws-smithy-runtime-api.workspace = true
55+
tempfile.workspace = true
5256

5357
[build-dependencies]
5458
generate-readme.workspace = true

sources/api/apiclient/src/apply.rs

Lines changed: 75 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,15 @@
11
//! This module allows application of settings from URIs or stdin. The inputs are expected to be
22
//! TOML settings files, in the same format as user data, or the JSON equivalent. The inputs are
33
//! pulled and applied to the API server in a single transaction.
4-
//! use aws_smithy_runtime_api::client::result::SdkError;
5-
use aws_sdk_secretsmanager::operation::get_secret_value::GetSecretValueError;
6-
use aws_sdk_secretsmanager::config::http::HttpResponse as SdkHttpResponse;
7-
use crate::uri_resolver::{StdinUri, FileUri, HttpUri, S3Uri, UriResolver, SecretsManagerUri};
4+
use crate::apply::error::ResolverFailureSnafu;
85
use crate::rando;
9-
use futures::future::{join, ready, TryFutureExt};
6+
use futures::future::{join, ready};
107
use futures::stream::{self, StreamExt};
118
use reqwest::Url;
129
use serde::de::{Deserialize, IntoDeserializer};
13-
use snafu::{futures::try_future::TryFutureExt as SnafuTryFutureExt, OptionExt, ResultExt};
10+
use snafu::{OptionExt, ResultExt};
1411
use std::convert::TryFrom;
1512
use std::path::Path;
16-
use tokio::io::AsyncReadExt;
17-
1813

1914
/// Reads settings in TOML or JSON format from files at the requested URIs (or from stdin, if given
2015
/// "-"), then commits them in a single transaction and applies them to the system.
@@ -71,44 +66,65 @@ where
7166
Ok(())
7267
}
7368

69+
/// Holds the raw input string and the URL (if it parses).
70+
pub struct SettingsInput {
71+
pub input: String,
72+
pub parsed_url: Option<Url>,
73+
}
74+
impl SettingsInput {
75+
pub(crate) fn new(input: impl Into<String>) -> Self {
76+
let input = input.into();
77+
let parsed_url = Url::parse(&input).ok();
78+
SettingsInput { input, parsed_url }
79+
}
80+
}
81+
7482
/// Retrieves the given source location and returns the result in a String.
75-
pub async fn get(input: &str) -> Result<String> {
76-
let resolver = select_resolver(input)?;
77-
resolver.resolve().await
83+
async fn get<S>(input_source: S) -> Result<String>
84+
where
85+
S: AsRef<str>,
86+
{
87+
let settings = SettingsInput::new(input_source.as_ref());
88+
let resolver = select_resolver(&settings)?;
89+
resolver.resolve().await.context(ResolverFailureSnafu)
7890
}
7991

80-
/// Choose which UriResolver applies to `input` (stdin, file://, http(s):// or s3://).
81-
fn select_resolver(input: &str) -> Result<Box<dyn UriResolver>> {
82-
// 1) "-" → stdin
83-
if let Ok(r) = StdinUri::try_from(input) {
92+
/// Choose which UriResolver applies to `input` (stdin, file://, http(s)://, s3://, secretsmanager://, and ssm://).
93+
fn select_resolver(input: &SettingsInput) -> Result<Box<dyn crate::uri_resolver::UriResolver>> {
94+
use crate::uri_resolver;
95+
96+
// stdin ("-")
97+
if let Ok(r) = uri_resolver::StdinUri::try_from(input) {
8498
return Ok(Box::new(r));
8599
}
86100

87-
// 6) secretsmanager://
88-
if let Ok(r) = SecretsManagerUri::try_from(input) {
101+
// file://
102+
if let Ok(r) = uri_resolver::FileUri::try_from(input) {
89103
return Ok(Box::new(r));
90104
}
91105

92-
// 2) parse as a URL
93-
let url = Url::parse(input).context(error::UriSnafu { input_source: input.to_string() })?;
106+
// http(s)://
107+
if let Ok(r) = uri_resolver::HttpUri::try_from(input) {
108+
return Ok(Box::new(r));
109+
}
94110

95-
// 3) file://
96-
if let Ok(r) = FileUri::try_from(&url) {
111+
// s3://
112+
if let Ok(r) = uri_resolver::S3Uri::try_from(input) {
97113
return Ok(Box::new(r));
98114
}
99115

100-
// 4) http(s)://
101-
if let Ok(r) = HttpUri::try_from(url.clone()) {
116+
// secretsmanager://
117+
if let Ok(r) = uri_resolver::SecretsManagerUri::try_from(input) {
102118
return Ok(Box::new(r));
103119
}
104120

105-
// 5) s3://
106-
if let Ok(r) = S3Uri::try_from(url.clone()) {
121+
// ssm://
122+
if let Ok(r) = uri_resolver::SsmUri::try_from(input) {
107123
return Ok(Box::new(r));
108124
}
109125

110126
error::NoResolverSnafu {
111-
input_source: input.to_string(),
127+
input_source: input.input.clone(),
112128
}
113129
.fail()
114130
}
@@ -146,13 +162,11 @@ fn format_change(input: &str, input_source: &str) -> Result<String> {
146162
serde_json::to_string(&json_inner).context(error::JsonSerializeSnafu { input_source })
147163
}
148164

149-
pub(crate) mod error {
150-
use aws_sdk_secretsmanager::operation::get_secret_value::GetSecretValueError;
165+
mod error {
151166
use snafu::Snafu;
152167

153168
#[derive(Debug, Snafu)]
154-
#[snafu(visibility(pub(crate)))]
155-
169+
#[snafu(visibility(pub(super)))]
156170
pub enum Error {
157171
#[snafu(display("Failed to commit combined settings to '{}': {}", uri, source))]
158172
CommitApply {
@@ -161,15 +175,6 @@ pub(crate) mod error {
161175
source: Box<crate::Error>,
162176
},
163177

164-
#[snafu(display("Failed to read given file '{}': {}", input_source, source))]
165-
FileRead {
166-
input_source: String,
167-
source: std::io::Error,
168-
},
169-
170-
#[snafu(display("Given invalid file URI '{}'", input_source))]
171-
FileUri { input_source: String },
172-
173178
#[snafu(display("No URI resolver found for '{}'", input_source))]
174179
NoResolver { input_source: String },
175180

@@ -227,49 +232,52 @@ pub(crate) mod error {
227232
source: reqwest::Error,
228233
},
229234

230-
#[snafu(display("Invalid S3 URI '{}': missing bucket name", input_source))]
231-
S3UriMissingBucket { input_source: String },
232-
233-
#[snafu(display("Given invalid file URI '{}'", input_source))]
234-
InvalidFileUri { input_source: String },
235-
236-
#[snafu(display("Given HTTP(S) URI '{}'", input_source))]
237-
InvalidHTTPUri { input_source: String },
238-
239-
#[snafu(display("Failed to read standard input: {}", source))]
240-
StdinRead { source: std::io::Error },
241-
242-
#[snafu(display("Invalid S3 URI scheme for '{}', expected s3://", input_source))]
243-
S3UriScheme { input_source: String },
244-
245-
#[snafu(display("Invalid Secrets Manager URI scheme for '{}', expected secretsmanager://", input_source))]
246-
SecretsManagerUri { input_source: String },
247-
248-
#[snafu(display("Failed to fetch secret '{}' from Secrets Manager: {}", secret_id, source))]
249-
SecretsManagerGet {
250-
secret_id: String,
251-
source: aws_sdk_secretsmanager::error::SdkError<GetSecretValueError>,
252-
},
253-
254-
#[snafu(display("Secrets Manager secret '{}' did not return a string value", secret_id))]
255-
SecretsManagerStringMissing { secret_id: String },
256-
257235
#[snafu(display(
258236
"Failed to translate TOML from '{}' to JSON for API: {}",
259237
input_source,
260238
source
261239
))]
262240
TomlToJson {
263241
input_source: String,
264-
source: toml::de::Error,
242+
#[snafu(source(from(toml::de::Error, Box::new)))]
243+
source: Box<toml::de::Error>,
265244
},
266245

267246
#[snafu(display("Given invalid URI '{}': {}", input_source, source))]
268247
Uri {
269248
input_source: String,
270249
source: url::ParseError,
271250
},
251+
252+
#[snafu(display("Resolver failed: {}", source))]
253+
ResolverFailure {
254+
#[snafu(source(from(crate::uri_resolver::ResolverError, Box::new)))]
255+
source: Box<crate::uri_resolver::ResolverError>,
256+
},
272257
}
273258
}
274259
pub use error::Error;
275260
pub type Result<T> = std::result::Result<T, error::Error>;
261+
262+
#[cfg(test)]
263+
mod resolver_selection_tests {
264+
use super::select_resolver;
265+
use crate::apply::SettingsInput;
266+
use std::any::{Any, TypeId};
267+
use test_case::test_case;
268+
269+
#[test_case("-", TypeId::of::<crate::uri_resolver::StdinUri>(); "stdin")]
270+
#[test_case("file:///tmp/folder", TypeId::of::<crate::uri_resolver::FileUri>(); "file")]
271+
#[test_case("http://amazon.com", TypeId::of::<crate::uri_resolver::HttpUri>(); "http")]
272+
#[test_case("https://amazon.com", TypeId::of::<crate::uri_resolver::HttpUri>(); "https")]
273+
#[test_case("s3://mybucket/path", TypeId::of::<crate::uri_resolver::S3Uri>(); "s3")]
274+
#[test_case("secretsmanager://sec", TypeId::of::<crate::uri_resolver::SecretsManagerUri>(); "secrets")]
275+
#[test_case("ssm://param", TypeId::of::<crate::uri_resolver::SsmUri>(); "ssm")]
276+
277+
fn resolver_selection(input: &str, expected: std::any::TypeId) {
278+
let settings = SettingsInput::new(input);
279+
let resolver = select_resolver(&settings).expect("should have a resolver for this scheme");
280+
let any = resolver.as_ref() as &dyn Any;
281+
assert_eq!(any.type_id(), expected);
282+
}
283+
}

0 commit comments

Comments
 (0)