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 ;
85use crate :: rando;
9- use futures:: future:: { join, ready, TryFutureExt } ;
6+ use futures:: future:: { join, ready} ;
107use futures:: stream:: { self , StreamExt } ;
118use reqwest:: Url ;
129use serde:: de:: { Deserialize , IntoDeserializer } ;
13- use snafu:: { futures :: try_future :: TryFutureExt as SnafuTryFutureExt , OptionExt , ResultExt } ;
10+ use snafu:: { OptionExt , ResultExt } ;
1411use std:: convert:: TryFrom ;
1512use 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}
274259pub use error:: Error ;
275260pub 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