1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
//! Provides the ability to asynchronously load values from the [AWS Systems Manager Parameter Store](https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html)
//!
//! # Examples
//!
//! ```norun
//! // assuming something like this: `aws ssm put-parameter --name my.param --value "ssm value"`
//! let input = String::from("SSM template: %awsssm:my.param%");
//! let output = germinate::process(input);
//! assert_eq!(String::from("SSM template: ssm value"), output);
//! ```
use anyhow::Result;
use rusoto_core::Region;
use rusoto_ssm::{GetParameterRequest, Ssm, SsmClient};

/// This type provides functionality for loading values from [AWS Systems Manager Parameter Store](https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html)
pub struct AwsSsmLoader {
    client: rusoto_ssm::SsmClient,
}

impl AwsSsmLoader {
    /// Creates a new AwsSsmLoader with the default region
    pub fn new() -> Self {
        // TODO hard coded region - should be configurable
        let client = SsmClient::new(Region::default());
        Self::with_client(client)
    }

    /// Creates a new AwsSsmLoader with the provided SsmClient
    pub fn with_client(client: SsmClient) -> Self {
        Self { client }
    }

    /// Loads a parameter from the Parameter Store and returns it as a `String`. Provides the
    /// `decrypt` argument to control whether or not the value should be decrypted
    async fn get_parameter(&self, name: &String, decrypt: bool) -> Result<String> {
        let req = GetParameterRequest {
            name: name.clone(),
            with_decryption: Some(decrypt),
        };

        let response = match self.client.get_parameter(req).await {
            Ok(response) => response,
            Err(rusoto_core::RusotoError::Service(
                rusoto_ssm::GetParameterError::ParameterNotFound(_),
            )) => {
                return Err(anyhow::anyhow!("Parameter not found '{}'", name)
                    .context("Failed to fetch parameter from AWS SSM"))
            }
            Err(e) => return Err(anyhow::anyhow!("Failed to fetch parameter: {}", e)),
        };

        let parameter = response
            .parameter
            .ok_or(anyhow::anyhow!("Failed to get parameter"))?;

        let value = parameter
            .value
            .ok_or(anyhow::anyhow!("Parameter has no value"))?;

        Ok(value)
    }
}

#[async_trait::async_trait]
impl crate::ValueLoader for AwsSsmLoader {
    /// Loads a value from the Parameter Store and returns it as a `String`
    async fn load(&self, key: &String) -> Result<String> {
        // TODO hard coded decrypt value
        // Options:
        //   flag --awsssm-decrypt - will only work if all values are encrypted
        //   separate template strings: (ValueSource)
        //      %awsssm:my.value% - instantiate an AwsSsmLoader with decrypt set to false
        //      %awsssm_decrypt:my.value% - instantiate an AwsSsmLoader with decrypt true
        self.get_parameter(key, true).await
    }
}

#[cfg(test)]
mod test {
    use super::*;
    use crate::ValueLoader;
    use rusoto_mock::{
        MockCredentialsProvider, MockRequestDispatcher, MockResponseReader, ReadMockResponse,
    };

    #[tokio::test]
    async fn test_ssm_load_parameter() {
        let mock_client = rusoto_ssm::SsmClient::new_with(
            MockRequestDispatcher::default().with_body(&MockResponseReader::read_response(
                "testdata/awsssm",
                "get-parameter-response.json",
            )),
            MockCredentialsProvider,
            Default::default(),
        );

        let loader = AwsSsmLoader::with_client(mock_client);
        let actual = loader.load(&"test.param".into()).await.unwrap();

        assert_eq!(String::from("ssm value"), actual);
    }

    #[tokio::test]
    async fn test_ssm_load_parameter_not_found() {
        let mock_client = rusoto_ssm::SsmClient::new_with(
            MockRequestDispatcher::with_status(400).with_body(&MockResponseReader::read_response(
                "testdata/awsssm",
                "get-parameter-not-found-response.json",
            )),
            MockCredentialsProvider,
            Default::default(),
        );

        let loader = AwsSsmLoader::with_client(mock_client);
        let actual = loader.load(&"test.param".into()).await;

        assert!(actual.is_err());

        match actual {
            Err(err) => assert!(format!("{:?}", err).contains("Parameter not found")),
            _ => assert!(false),
        }
    }
}