cloud_scanner_cli/
usage_location.rs

1//! The location where cloud resources are running.
2use csv::ReaderBuilder;
3use log::error;
4use once_cell::sync::Lazy;
5use rocket_okapi::okapi::schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::error::Error;
9use std::sync::Mutex;
10use thiserror::Error;
11
12// Use of static to load the region-country map once
13static REGION_COUNTRY_MAP: Lazy<Mutex<HashMap<String, String>>> = Lazy::new(|| {
14    let map = load_region_country_map().unwrap_or_default();
15    Mutex::new(map)
16});
17
18#[derive(Error, Debug)]
19pub enum RegionError {
20    #[error("Unsupported region ({0})")]
21    UnsupportedRegion(String),
22}
23
24///  The location where cloud resources are running.
25///
26/// TODO! the usage location should be abstracted and vendor specific implementation should be part of the cloud_provider model (region names are tied to a specific cloud provider)
27#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
28pub struct UsageLocation {
29    /// The AWS region (like eu-west-1)
30    pub aws_region: String,
31    /// The 3-letters ISO country code corresponding to the country of the aws_region
32    pub iso_country_code: String,
33}
34
35impl TryFrom<&str> for UsageLocation {
36    fn try_from(aws_region: &str) -> Result<Self, RegionError> {
37        let cc = get_country_from_aws_region(aws_region)?;
38        Ok(UsageLocation {
39            aws_region: String::from(aws_region),
40            iso_country_code: cc,
41        })
42    }
43    type Error = RegionError;
44}
45
46/// Load the region-country map from a CSV file
47fn load_region_country_map() -> Result<HashMap<String, String>, Box<dyn Error>> {
48    let csv_content = include_str!("../csv/cloud_providers_regions.csv");
49
50    let mut reader = ReaderBuilder::new()
51        .has_headers(true)
52        .from_reader(csv_content.as_bytes());
53
54    let mut region_country_map = HashMap::new();
55
56    for result in reader.records() {
57        let record = result?;
58        let region = &record[1]; // AWS region
59        let country_code = &record[2]; // country code
60        region_country_map.insert(region.to_string(), country_code.to_string());
61    }
62
63    Ok(region_country_map)
64}
65
66/// Converts AWS region as String into an ISO country code, returns FRA if not found
67fn get_country_from_aws_region(aws_region: &str) -> Result<String, RegionError> {
68    let map = REGION_COUNTRY_MAP.lock().unwrap();
69    match map.get(aws_region) {
70        Some(country_code) => Ok(country_code.to_string()),
71        None => {
72            error!(
73                "Unsupported region: unable to match aws region [{}] to country code",
74                aws_region
75            );
76            Err(RegionError::UnsupportedRegion(String::from(aws_region)))
77        }
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    //use super::*;
84    use super::UsageLocation;
85
86    #[test]
87    fn test_get_country_code_for_supported_aws_region() {
88        let location = UsageLocation::try_from("eu-west-1").unwrap();
89        assert_eq!("IRL", location.iso_country_code);
90
91        let location = UsageLocation::try_from("eu-west-2").unwrap();
92        assert_eq!("GBR", location.iso_country_code);
93
94        let location = UsageLocation::try_from("eu-west-3").unwrap();
95        assert_eq!("FRA", location.iso_country_code);
96    }
97
98    #[test]
99    fn test_get_country_code_of_unsupported_aws_region_returns_error() {
100        // this one is not supported
101        let res = UsageLocation::try_from("us-gov-east-1");
102        assert!(res.is_err());
103
104        // this one is not supported
105        let res = UsageLocation::try_from("whatever");
106        assert!(res.is_err());
107
108        let res = UsageLocation::try_from("");
109        assert!(res.is_err());
110    }
111}