cargo_lambda_remote/
lib.rs

1use aws_config::{
2    BehaviorVersion,
3    meta::region::RegionProviderChain,
4    profile::{ProfileFileCredentialsProvider, ProfileFileRegionProvider},
5    provider_config::ProviderConfig,
6    retry::RetryConfig,
7};
8use aws_types::{SdkConfig, region::Region};
9use clap::Args;
10use serde::{Deserialize, Serialize, ser::SerializeStruct};
11pub mod tls;
12
13const DEFAULT_REGION: &str = "us-east-1";
14
15#[derive(Args, Clone, Debug, Default, Deserialize, Serialize)]
16pub struct RemoteConfig {
17    /// AWS configuration profile to use for authorization
18    #[arg(short, long)]
19    #[serde(default)]
20    pub profile: Option<String>,
21
22    /// AWS region to deploy, if there is no default
23    #[arg(short, long)]
24    #[serde(default)]
25    pub region: Option<String>,
26
27    /// AWS Lambda alias to associate the function to
28    #[arg(short, long, alias = "qualifier")]
29    #[serde(default)]
30    pub alias: Option<String>,
31
32    /// Number of attempts to try failed operations
33    #[arg(long, default_value = "1")]
34    #[serde(default)]
35    pub retry_attempts: Option<u32>,
36
37    /// Custom endpoint URL to target
38    #[arg(long)]
39    #[serde(default)]
40    pub endpoint_url: Option<String>,
41}
42
43impl RemoteConfig {
44    fn retry_policy(&self) -> RetryConfig {
45        let attempts = self.retry_attempts.unwrap_or(1);
46        RetryConfig::standard().with_max_attempts(attempts)
47    }
48
49    pub async fn sdk_config(&self, retry: Option<RetryConfig>) -> SdkConfig {
50        let explicit_region = self.region.clone().map(Region::new);
51
52        let region_provider = RegionProviderChain::first_try(explicit_region.clone())
53            .or_default_provider()
54            .or_else(Region::new(DEFAULT_REGION));
55
56        let retry = retry.unwrap_or_else(|| self.retry_policy());
57        let mut config_loader = if let Some(ref endpoint_url) = self.endpoint_url {
58            aws_config::defaults(BehaviorVersion::latest())
59                .endpoint_url(endpoint_url)
60                .region(region_provider)
61                .retry_config(retry)
62        } else {
63            aws_config::defaults(BehaviorVersion::latest())
64                .region(region_provider)
65                .retry_config(retry)
66        };
67
68        if let Some(profile) = &self.profile {
69            let profile_region = ProfileFileRegionProvider::builder()
70                .profile_name(profile)
71                .build();
72
73            let region_provider =
74                RegionProviderChain::first_try(explicit_region).or_else(profile_region);
75            let region = region_provider.region().await;
76
77            let conf = ProviderConfig::default().with_region(region);
78
79            let creds_provider = ProfileFileCredentialsProvider::builder()
80                .profile_name(profile)
81                .configure(&conf)
82                .build();
83
84            config_loader = config_loader
85                .region(region_provider)
86                .credentials_provider(creds_provider);
87        }
88
89        config_loader.load().await
90    }
91
92    pub fn count_fields(&self) -> usize {
93        self.profile.is_some() as usize
94            + self.region.is_some() as usize
95            + self.alias.is_some() as usize
96            + self.retry_attempts.is_some() as usize
97            + self.endpoint_url.is_some() as usize
98    }
99
100    pub fn serialize_fields<S>(
101        &self,
102        state: &mut <S as serde::Serializer>::SerializeStruct,
103    ) -> Result<(), S::Error>
104    where
105        S: serde::Serializer,
106    {
107        if let Some(ref profile) = self.profile {
108            state.serialize_field("profile", profile)?;
109        }
110        if let Some(ref region) = self.region {
111            state.serialize_field("region", region)?;
112        }
113        if let Some(ref alias) = self.alias {
114            state.serialize_field("alias", alias)?;
115        }
116        if let Some(ref retry_attempts) = self.retry_attempts {
117            state.serialize_field("retry_attempts", retry_attempts)?;
118        }
119        if let Some(ref endpoint_url) = self.endpoint_url {
120            state.serialize_field("endpoint_url", endpoint_url)?;
121        }
122
123        Ok(())
124    }
125}
126
127pub mod aws_sdk_config {
128    pub use aws_types::SdkConfig;
129}
130pub use aws_sdk_lambda;
131
132#[cfg(test)]
133mod tests {
134    use aws_sdk_lambda::config::{ProvideCredentials, Region};
135
136    use crate::RemoteConfig;
137
138    fn setup() {
139        let manifest_dir = env!("CARGO_MANIFEST_DIR");
140        unsafe {
141            std::env::set_var(
142                "AWS_CONFIG_FILE",
143                format!("{manifest_dir}/test-data/aws_config"),
144            );
145            std::env::set_var(
146                "AWS_SHARED_CREDENTIALS_FILE",
147                format!("{manifest_dir}/test-data/aws_credentials"),
148            );
149        }
150    }
151
152    /// Specify a profile which does not exist
153    /// Expectations:
154    /// - Region is undefined
155    /// - Credentials are undefined
156    #[tokio::test]
157    async fn undefined_profile() {
158        setup();
159
160        let args = RemoteConfig {
161            profile: Some("durian".to_owned()),
162            region: None,
163            alias: None,
164            retry_attempts: Some(1),
165            endpoint_url: None,
166        };
167
168        let config = args.sdk_config(None).await;
169        let creds = config
170            .credentials_provider()
171            .unwrap()
172            .provide_credentials()
173            .await;
174
175        assert_eq!(config.region(), None);
176        assert!(creds.is_err());
177    }
178
179    /// Specify a profile which exists in the credentials file but not in the config file
180    /// Expectations:
181    /// - Region is undefined
182    /// - Credentials are used from the profile
183    #[tokio::test]
184    async fn undefined_profile_with_creds() {
185        setup();
186
187        let args = RemoteConfig {
188            profile: Some("cherry".to_owned()),
189            region: None,
190            alias: None,
191            retry_attempts: Some(1),
192            endpoint_url: None,
193        };
194
195        let config = args.sdk_config(None).await;
196        let creds = config
197            .credentials_provider()
198            .unwrap()
199            .provide_credentials()
200            .await
201            .unwrap();
202
203        assert_eq!(config.region(), None);
204        assert_eq!(creds.access_key_id(), "CCCCCCCCCCCCCCCCCCCC");
205    }
206
207    /// Specify a profile which has a region associated to it
208    /// Expectations:
209    /// - Region is used from the profile
210    /// - Credentials are used from the profile
211    #[tokio::test]
212    async fn profile_with_region() {
213        setup();
214
215        let args = RemoteConfig {
216            profile: Some("apple".to_owned()),
217            region: None,
218            alias: None,
219            retry_attempts: Some(1),
220            endpoint_url: None,
221        };
222
223        let config = args.sdk_config(None).await;
224        let creds = config
225            .credentials_provider()
226            .unwrap()
227            .provide_credentials()
228            .await
229            .unwrap();
230
231        assert_eq!(config.region(), Some(&Region::from_static("ca-central-1")));
232        assert_eq!(creds.access_key_id(), "AAAAAAAAAAAAAAAAAAAA");
233    }
234
235    /// Specify a profile which does not have a region associated to it
236    /// Expectations:
237    /// - Region is undefined
238    /// - Credentials are used from the profile
239    #[tokio::test]
240    async fn profile_without_region() {
241        setup();
242
243        let args = RemoteConfig {
244            profile: Some("banana".to_owned()),
245            region: None,
246            alias: None,
247            retry_attempts: Some(1),
248            endpoint_url: None,
249        };
250
251        let config = args.sdk_config(None).await;
252        let creds = config
253            .credentials_provider()
254            .unwrap()
255            .provide_credentials()
256            .await
257            .unwrap();
258
259        assert_eq!(config.region(), None);
260        assert_eq!(creds.access_key_id(), "BBBBBBBBBBBBBBBBBBBB");
261    }
262
263    /// Use the default profile which has a region associated to it
264    /// Expectations:
265    /// - Region is used from the profile
266    /// - Credentials are used from the profile
267    #[tokio::test]
268    async fn default_profile() {
269        setup();
270
271        let args = RemoteConfig {
272            profile: None,
273            region: None,
274            alias: None,
275            retry_attempts: Some(1),
276            endpoint_url: None,
277        };
278
279        let config = args.sdk_config(None).await;
280        let creds = config
281            .credentials_provider()
282            .unwrap()
283            .provide_credentials()
284            .await
285            .unwrap();
286
287        assert_eq!(config.region(), Some(&Region::from_static("af-south-1")));
288        assert_eq!(creds.access_key_id(), "DDDDDDDDDDDDDDDDDDDD");
289    }
290}