Skip to main content

aws_lite_rs/api/
config.rs

1//! AWS Config API client.
2//!
3//! Thin wrapper over generated ops. All URL construction and HTTP methods
4//! are in `ops::config::ConfigOps`. This layer adds:
5//! - Ergonomic method signatures
6//! - Pagination stream helpers
7
8use crate::{
9    AwsHttpClient, Result,
10    ops::config::ConfigOps,
11    types::config::{
12        ConfigurationRecorder, ConfigurationRecorderStatus,
13        DescribeConfigurationRecorderStatusRequest, DescribeConfigurationRecorderStatusResponse,
14        DescribeConfigurationRecordersRequest, DescribeConfigurationRecordersResponse,
15        SelectResourceConfigRequest, SelectResourceConfigResponse,
16    },
17};
18
19/// Client for the AWS Config API
20pub struct ConfigClient<'a> {
21    ops: ConfigOps<'a>,
22}
23
24impl<'a> ConfigClient<'a> {
25    /// Create a new AWS Config API client
26    pub(crate) fn new(client: &'a AwsHttpClient) -> Self {
27        Self {
28            ops: ConfigOps::new(client),
29        }
30    }
31
32    /// Accepts a SQL SELECT command and returns matching resource configurations.
33    pub async fn select_resource_config(
34        &self,
35        body: &SelectResourceConfigRequest,
36    ) -> Result<SelectResourceConfigResponse> {
37        self.ops.select_resource_config(body).await
38    }
39
40    // ── Configuration Recorders ────────────────────────────────────────
41
42    /// Return all configuration recorders in the current account/region.
43    ///
44    /// Optionally filter by recorder name(s). Passing an empty slice returns
45    /// all recorders (typically just one named `"default"`).
46    ///
47    /// CIS 4.3: at least one active configuration recorder must exist in each
48    /// region. Call `describe_configuration_recorder_status` to confirm it is
49    /// actively recording.
50    pub async fn describe_configuration_recorders(
51        &self,
52        recorder_names: &[&str],
53    ) -> Result<DescribeConfigurationRecordersResponse> {
54        let body = DescribeConfigurationRecordersRequest {
55            configuration_recorder_names: recorder_names.iter().map(|s| s.to_string()).collect(),
56            ..Default::default()
57        };
58        self.ops.describe_configuration_recorders(&body).await
59    }
60
61    /// Return all configuration recorders as a flat `Vec`.
62    pub async fn list_configuration_recorders(&self) -> Result<Vec<ConfigurationRecorder>> {
63        let resp = self.describe_configuration_recorders(&[]).await?;
64        Ok(resp.configuration_recorders)
65    }
66
67    /// Return the recording status for all (or named) configuration recorders.
68    ///
69    /// CIS 4.3: verify `recording == true` and `last_status` is not `Failure`
70    /// to confirm Config is actively recording in the region.
71    pub async fn describe_configuration_recorder_status(
72        &self,
73        recorder_names: &[&str],
74    ) -> Result<DescribeConfigurationRecorderStatusResponse> {
75        let body = DescribeConfigurationRecorderStatusRequest {
76            configuration_recorder_names: recorder_names.iter().map(|s| s.to_string()).collect(),
77            ..Default::default()
78        };
79        self.ops.describe_configuration_recorder_status(&body).await
80    }
81
82    /// Return the recording status for all recorders as a flat `Vec`.
83    pub async fn list_configuration_recorder_statuses(
84        &self,
85    ) -> Result<Vec<ConfigurationRecorderStatus>> {
86        let resp = self.describe_configuration_recorder_status(&[]).await?;
87        Ok(resp.configuration_recorders_status)
88    }
89
90    /// Stream all results for a SQL SELECT query, automatically handling pagination.
91    ///
92    /// Each item is a raw JSON string representing a resource configuration.
93    pub fn select_resource_config_stream(
94        &self,
95        expression: &str,
96    ) -> impl futures_core::Stream<Item = Result<String>> + '_ {
97        let expression = expression.to_string();
98        async_stream::try_stream! {
99            let mut next_token: Option<String> = None;
100            loop {
101                let request = SelectResourceConfigRequest {
102                    expression: expression.clone(),
103                    next_token: next_token.clone(),
104                    ..Default::default()
105                };
106                let response = self.ops.select_resource_config(&request).await?;
107                for result in response.results {
108                    yield result;
109                }
110                match response.next_token {
111                    Some(token) if !token.is_empty() => next_token = Some(token),
112                    _ => break,
113                }
114            }
115        }
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122    use crate::mock_client::MockClient;
123    use crate::test_support::config_mock_helpers::ConfigMockHelpers;
124    use tokio_stream::StreamExt;
125
126    #[tokio::test]
127    async fn select_resource_config_stream_paginates() {
128        let mut mock = MockClient::new();
129
130        mock.expect_post("/")
131            .returning_json_sequence(vec![
132                serde_json::json!({
133                    "Results": ["{\"resourceId\":\"vol-1\",\"resourceType\":\"AWS::EC2::Volume\"}"],
134                    "NextToken": "token-page2"
135                }),
136                serde_json::json!({
137                    "Results": ["{\"resourceId\":\"vol-2\",\"resourceType\":\"AWS::EC2::Volume\"}"]
138                }),
139            ])
140            .times(2);
141
142        let client = AwsHttpClient::from_mock(mock);
143        let config = client.config();
144
145        let results: Vec<String> = config
146            .select_resource_config_stream(
147                "SELECT resourceId WHERE resourceType = 'AWS::EC2::Volume'",
148            )
149            .map(|r| r.unwrap())
150            .collect()
151            .await;
152
153        assert_eq!(results.len(), 2);
154        assert!(results[0].contains("vol-1"));
155        assert!(results[1].contains("vol-2"));
156    }
157
158    #[tokio::test]
159    async fn select_resource_config_stream_single_page() {
160        let mut mock = MockClient::new();
161
162        mock.expect_post("/").returning_json(serde_json::json!({
163            "Results": [
164                "{\"resourceId\":\"vol-1\"}",
165                "{\"resourceId\":\"vol-2\"}"
166            ]
167        }));
168
169        let client = AwsHttpClient::from_mock(mock);
170        let config = client.config();
171
172        let results: Vec<String> = config
173            .select_resource_config_stream("SELECT resourceId")
174            .map(|r| r.unwrap())
175            .collect()
176            .await;
177
178        assert_eq!(results.len(), 2);
179    }
180
181    // ── Configuration Recorders ────────────────────────────────────────
182
183    #[tokio::test]
184    async fn describe_configuration_recorders_returns_recorders() {
185        let mut mock = MockClient::new();
186        mock.expect_describe_configuration_recorders()
187            .returning_json(serde_json::json!({
188                "ConfigurationRecorders": [
189                    {
190                        "name": "default",
191                        "roleARN": "arn:aws:iam::123456789012:role/aws-service-role/config.amazonaws.com/AWSServiceRoleForConfig",
192                        "recordingGroup": {
193                            "allSupported": true,
194                            "includeGlobalResourceTypes": true
195                        }
196                    }
197                ]
198            }));
199
200        let client = crate::AwsHttpClient::from_mock(mock);
201        let resp = client
202            .config()
203            .describe_configuration_recorders(&[])
204            .await
205            .unwrap();
206        assert_eq!(resp.configuration_recorders.len(), 1);
207        let r = &resp.configuration_recorders[0];
208        assert_eq!(r.name.as_deref(), Some("default"));
209        assert!(
210            r.role_arn
211                .as_deref()
212                .unwrap_or("")
213                .contains("AWSServiceRoleForConfig")
214        );
215        let group = r
216            .recording_group
217            .as_ref()
218            .expect("recording_group should be set");
219        assert_eq!(group.all_supported, Some(true));
220    }
221
222    #[tokio::test]
223    async fn describe_configuration_recorders_handles_empty() {
224        let mut mock = MockClient::new();
225        mock.expect_describe_configuration_recorders()
226            .returning_json(serde_json::json!({"ConfigurationRecorders": []}));
227
228        let client = crate::AwsHttpClient::from_mock(mock);
229        let resp = client
230            .config()
231            .describe_configuration_recorders(&[])
232            .await
233            .unwrap();
234        assert!(resp.configuration_recorders.is_empty());
235    }
236
237    #[tokio::test]
238    async fn describe_configuration_recorder_status_returns_status() {
239        let mut mock = MockClient::new();
240        mock.expect_describe_configuration_recorder_status()
241            .returning_json(serde_json::json!({
242                "ConfigurationRecordersStatus": [
243                    {
244                        "name": "default",
245                        "recording": true,
246                        "lastStatus": "Success"
247                    }
248                ]
249            }));
250
251        let client = crate::AwsHttpClient::from_mock(mock);
252        let resp = client
253            .config()
254            .describe_configuration_recorder_status(&[])
255            .await
256            .unwrap();
257        assert_eq!(resp.configuration_recorders_status.len(), 1);
258        let s = &resp.configuration_recorders_status[0];
259        assert_eq!(s.name.as_deref(), Some("default"));
260        assert_eq!(s.recording, Some(true));
261        assert_eq!(
262            s.last_status,
263            Some(crate::types::config::RecorderStatus::Success)
264        );
265    }
266
267    #[tokio::test]
268    async fn describe_configuration_recorder_status_handles_empty() {
269        let mut mock = MockClient::new();
270        mock.expect_describe_configuration_recorder_status()
271            .returning_json(serde_json::json!({"ConfigurationRecordersStatus": []}));
272
273        let client = crate::AwsHttpClient::from_mock(mock);
274        let resp = client
275            .config()
276            .describe_configuration_recorder_status(&[])
277            .await
278            .unwrap();
279        assert!(resp.configuration_recorders_status.is_empty());
280    }
281
282    #[tokio::test]
283    async fn select_resource_config_stream_empty() {
284        let mut mock = MockClient::new();
285
286        mock.expect_post("/").returning_json(serde_json::json!({
287            "Results": []
288        }));
289
290        let client = AwsHttpClient::from_mock(mock);
291        let config = client.config();
292
293        let results: Vec<String> = config
294            .select_resource_config_stream(
295                "SELECT resourceId WHERE resourceType = 'AWS::Fake::Thing'",
296            )
297            .map(|r| r.unwrap())
298            .collect()
299            .await;
300
301        assert_eq!(results.len(), 0);
302    }
303}