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
13pub const 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_iam;
131pub use aws_sdk_lambda;
132
133#[cfg(test)]
134mod tests {
135    use aws_sdk_lambda::config::{ProvideCredentials, Region};
136
137    use crate::RemoteConfig;
138
139    fn setup() {
140        let manifest_dir = env!("CARGO_MANIFEST_DIR");
141        unsafe {
142            std::env::set_var(
143                "AWS_CONFIG_FILE",
144                format!("{manifest_dir}/test-data/aws_config"),
145            );
146            std::env::set_var(
147                "AWS_SHARED_CREDENTIALS_FILE",
148                format!("{manifest_dir}/test-data/aws_credentials"),
149            );
150        }
151    }
152
153    /// Specify a profile which does not exist
154    /// Expectations:
155    /// - Region is undefined
156    /// - Credentials are undefined
157    #[tokio::test]
158    async fn undefined_profile() {
159        setup();
160
161        let args = RemoteConfig {
162            profile: Some("durian".to_owned()),
163            region: None,
164            alias: None,
165            retry_attempts: Some(1),
166            endpoint_url: None,
167        };
168
169        let config = args.sdk_config(None).await;
170        let creds = config
171            .credentials_provider()
172            .unwrap()
173            .provide_credentials()
174            .await;
175
176        assert_eq!(config.region(), None);
177        assert!(creds.is_err());
178    }
179
180    /// Specify a profile which exists in the credentials file but not in the config file
181    /// Expectations:
182    /// - Region is undefined
183    /// - Credentials are used from the profile
184    #[tokio::test]
185    async fn undefined_profile_with_creds() {
186        setup();
187
188        let args = RemoteConfig {
189            profile: Some("cherry".to_owned()),
190            region: None,
191            alias: None,
192            retry_attempts: Some(1),
193            endpoint_url: None,
194        };
195
196        let config = args.sdk_config(None).await;
197        let creds = config
198            .credentials_provider()
199            .unwrap()
200            .provide_credentials()
201            .await
202            .unwrap();
203
204        assert_eq!(config.region(), None);
205        assert_eq!(creds.access_key_id(), "CCCCCCCCCCCCCCCCCCCC");
206    }
207
208    /// Specify a profile which has a region associated to it
209    /// Expectations:
210    /// - Region is used from the profile
211    /// - Credentials are used from the profile
212    #[tokio::test]
213    async fn profile_with_region() {
214        setup();
215
216        let args = RemoteConfig {
217            profile: Some("apple".to_owned()),
218            region: None,
219            alias: None,
220            retry_attempts: Some(1),
221            endpoint_url: None,
222        };
223
224        let config = args.sdk_config(None).await;
225        let creds = config
226            .credentials_provider()
227            .unwrap()
228            .provide_credentials()
229            .await
230            .unwrap();
231
232        assert_eq!(config.region(), Some(&Region::from_static("ca-central-1")));
233        assert_eq!(creds.access_key_id(), "AAAAAAAAAAAAAAAAAAAA");
234    }
235
236    /// Specify a profile which does not have a region associated to it
237    /// Expectations:
238    /// - Region is undefined
239    /// - Credentials are used from the profile
240    #[tokio::test]
241    async fn profile_without_region() {
242        setup();
243
244        let args = RemoteConfig {
245            profile: Some("banana".to_owned()),
246            region: None,
247            alias: None,
248            retry_attempts: Some(1),
249            endpoint_url: None,
250        };
251
252        let config = args.sdk_config(None).await;
253        let creds = config
254            .credentials_provider()
255            .unwrap()
256            .provide_credentials()
257            .await
258            .unwrap();
259
260        assert_eq!(config.region(), None);
261        assert_eq!(creds.access_key_id(), "BBBBBBBBBBBBBBBBBBBB");
262    }
263
264    /// Use the default profile which has a region associated to it
265    /// Expectations:
266    /// - Region is used from the profile
267    /// - Credentials are used from the profile
268    #[tokio::test]
269    async fn default_profile() {
270        setup();
271
272        let args = RemoteConfig {
273            profile: None,
274            region: None,
275            alias: None,
276            retry_attempts: Some(1),
277            endpoint_url: None,
278        };
279
280        let config = args.sdk_config(None).await;
281        let creds = config
282            .credentials_provider()
283            .unwrap()
284            .provide_credentials()
285            .await
286            .unwrap();
287
288        assert_eq!(config.region(), Some(&Region::from_static("af-south-1")));
289        assert_eq!(creds.access_key_id(), "DDDDDDDDDDDDDDDDDDDD");
290    }
291}