Unverified Commit 47507232 authored by Russell Cohen's avatar Russell Cohen Committed by GitHub
Browse files

Create `StandardProperty` in aws-config (#2162)

* Create standard_property and refactor retry_config to use standard_property

* Add docs

* backout changes to dualstack

* cleanup more dualstack stuff

* remove extra code

* more dual stack detritus

* Fix clippy

* Update changelog
parent 5392a668
Loading
Loading
Loading
Loading
+13 −7
Original line number Diff line number Diff line
@@ -136,3 +136,9 @@ message = "Fix bug where string default values were not supported for endpoint p
references = ["smithy-rs#2150"]
meta = { "breaking" = false, "tada" = false, "bug" = true, "target" = "client" }
author = "rcoh"

[[aws-sdk-rust]]
references = ["smithy-rs#2162"]
meta = { "breaking" = true, "tada" = false, "bug" = false }
message = "`aws_config::profile::retry_config` && `aws_config::environment::retry_config` have been removed. Use `aws_config::default_provider::retry_config` instead."
author = "rcoh"
+1 −1
Original line number Diff line number Diff line
@@ -496,7 +496,7 @@ retry_mode = standard
    }

    #[tokio::test]
    #[should_panic = "failed to parse max attempts set by aws profile: invalid digit found in string"]
    #[should_panic = "failed to parse max attempts. source: profile `default`, key: `max_attempts`: invalid digit found in string"]
    async fn test_invalid_profile_retry_config_panics() {
        let env = Env::from_slice(&[("AWS_CONFIG_FILE", "config")]);
        let fs = Fs::from_slice(&[(
+146 −25
Original line number Diff line number Diff line
@@ -3,11 +3,12 @@
 * SPDX-License-Identifier: Apache-2.0
 */

use crate::environment::retry_config::EnvironmentVariableRetryConfigProvider;
use crate::profile;
use crate::provider_config::ProviderConfig;
use crate::retry::error::{RetryConfigError, RetryConfigErrorKind};
use crate::standard_property::{PropertyResolutionError, StandardProperty};
use aws_smithy_types::error::display::DisplayErrorContext;
use aws_smithy_types::retry::RetryConfig;
use aws_smithy_types::retry::{RetryConfig, RetryMode};
use std::str::FromStr;

/// Default RetryConfig Provider chain
///
@@ -15,8 +16,8 @@ use aws_smithy_types::retry::RetryConfig;
/// a builder struct is returned which has a similar API.
///
/// This provider will check the following sources in order:
/// 1. [Environment variables](EnvironmentVariableRetryConfigProvider)
/// 2. [Profile file](crate::profile::retry_config::ProfileFileRetryConfigProvider)
/// 1. Environment variables: `AWS_MAX_ATTEMPTS` & `AWS_RETRY_MODE`
/// 2. Profile file: `max_attempts` and `retry_mode`
///
/// # Example
///
@@ -49,11 +50,20 @@ pub fn default_provider() -> Builder {
    Builder::default()
}

mod env {
    pub(super) const MAX_ATTEMPTS: &str = "AWS_MAX_ATTEMPTS";
    pub(super) const RETRY_MODE: &str = "AWS_RETRY_MODE";
}

mod profile_keys {
    pub(super) const MAX_ATTEMPTS: &str = "max_attempts";
    pub(super) const RETRY_MODE: &str = "retry_mode";
}

/// Builder for RetryConfig that checks the environment and aws profile for configuration
#[derive(Debug, Default)]
pub struct Builder {
    env_provider: EnvironmentVariableRetryConfigProvider,
    profile_file: profile::retry_config::Builder,
    provider_config: ProviderConfig,
}

impl Builder {
@@ -61,21 +71,19 @@ impl Builder {
    ///
    /// Exposed for overriding the environment when unit-testing providers
    pub fn configure(mut self, configuration: &ProviderConfig) -> Self {
        self.env_provider =
            EnvironmentVariableRetryConfigProvider::new_with_env(configuration.env());
        self.profile_file = self.profile_file.configure(configuration);
        self.provider_config = configuration.clone();
        self
    }

    /// Override the profile name used by this provider
    pub fn profile_name(mut self, name: &str) -> Self {
        self.profile_file = self.profile_file.profile_name(name);
        self.provider_config = self.provider_config.with_profile_name(name.to_string());
        self
    }

    /// Attempt to create a [RetryConfig](aws_smithy_types::retry::RetryConfig) from following sources in order:
    /// 1. [Environment variables](crate::environment::retry_config::EnvironmentVariableRetryConfigProvider)
    /// 2. [Profile file](crate::profile::retry_config::ProfileFileRetryConfigProvider)
    /// 1. Environment variables: `AWS_MAX_ATTEMPTS` & `AWS_RETRY_MODE`
    /// 2. Profile file: `max_attempts` and `retry_mode`
    /// 3. [RetryConfig::standard()](aws_smithy_types::retry::RetryConfig::standard)
    ///
    /// Precedence is considered on a per-field basis
@@ -85,20 +93,133 @@ impl Builder {
    /// - Panics if the `AWS_MAX_ATTEMPTS` env var or `max_attempts` profile var is set to 0
    /// - Panics if the `AWS_RETRY_MODE` env var or `retry_mode` profile var is set to "adaptive" (it's not yet supported)
    pub async fn retry_config(self) -> RetryConfig {
        match self.try_retry_config().await {
            Ok(conf) => conf,
            Err(e) => panic!("{}", DisplayErrorContext(e)),
        }
    }

    pub(crate) async fn try_retry_config(
        self,
    ) -> Result<RetryConfig, PropertyResolutionError<RetryConfigError>> {
        // Both of these can return errors due to invalid config settings and we want to surface those as early as possible
        // hence, we'll panic if any config values are invalid (missing values are OK though)
        // We match this instead of unwrapping so we can print the error with the `Display` impl instead of the `Debug` impl that unwrap uses
        let builder_from_env = match self.env_provider.retry_config_builder() {
            Ok(retry_config_builder) => retry_config_builder,
            Err(err) => panic!("{}", DisplayErrorContext(&err)),
        };
        let builder_from_profile = match self.profile_file.build().retry_config_builder().await {
            Ok(retry_config_builder) => retry_config_builder,
            Err(err) => panic!("{}", DisplayErrorContext(&err)),
        let mut retry_config = RetryConfig::standard();
        let max_attempts = StandardProperty::new()
            .env(env::MAX_ATTEMPTS)
            .profile(profile_keys::MAX_ATTEMPTS)
            .validate(&self.provider_config, validate_max_attempts);

        let retry_mode = StandardProperty::new()
            .env(env::RETRY_MODE)
            .profile(profile_keys::RETRY_MODE)
            .validate(&self.provider_config, |s| {
                RetryMode::from_str(s)
                    .map_err(|err| RetryConfigErrorKind::InvalidRetryMode { source: err }.into())
            });

        if let Some(max_attempts) = max_attempts.await? {
            retry_config = retry_config.with_max_attempts(max_attempts);
        }

        if let Some(retry_mode) = retry_mode.await? {
            retry_config = retry_config.with_retry_mode(retry_mode);
        }

        Ok(retry_config)
    }
}

fn validate_max_attempts(max_attempts: &str) -> Result<u32, RetryConfigError> {
    match max_attempts.parse::<u32>() {
        Ok(max_attempts) if max_attempts == 0 => {
            Err(RetryConfigErrorKind::MaxAttemptsMustNotBeZero.into())
        }
        Ok(max_attempts) => Ok(max_attempts),
        Err(source) => Err(RetryConfigErrorKind::FailedToParseMaxAttempts { source }.into()),
    }
}

#[cfg(test)]
mod test {
    use crate::default_provider::retry_config::env;
    use crate::provider_config::ProviderConfig;
    use crate::retry::{
        error::RetryConfigError, error::RetryConfigErrorKind, RetryConfig, RetryMode,
    };
    use crate::standard_property::PropertyResolutionError;
    use aws_types::os_shim_internal::Env;

    async fn test_provider(
        vars: &[(&str, &str)],
    ) -> Result<RetryConfig, PropertyResolutionError<RetryConfigError>> {
        super::Builder::default()
            .configure(&ProviderConfig::no_configuration().with_env(Env::from_slice(vars)))
            .try_retry_config()
            .await
    }

    #[tokio::test]
    async fn defaults() {
        let built = test_provider(&[]).await.unwrap();

        assert_eq!(built.mode(), RetryMode::Standard);
        assert_eq!(built.max_attempts(), 3);
    }

    #[tokio::test]
    async fn max_attempts_is_read_correctly() {
        assert_eq!(
            test_provider(&[(env::MAX_ATTEMPTS, "88")]).await.unwrap(),
            RetryConfig::standard().with_max_attempts(88)
        );
    }

        builder_from_env
            .take_unset_from(builder_from_profile)
            .build()
    #[tokio::test]
    async fn max_attempts_errors_when_it_cant_be_parsed_as_an_integer() {
        assert!(matches!(
            test_provider(&[(env::MAX_ATTEMPTS, "not an integer")])
                .await
                .unwrap_err()
                .err,
            RetryConfigError {
                kind: RetryConfigErrorKind::FailedToParseMaxAttempts { .. }
            }
        ));
    }

    #[tokio::test]
    async fn retry_mode_is_read_correctly() {
        assert_eq!(
            test_provider(&[(env::RETRY_MODE, "standard")])
                .await
                .unwrap(),
            RetryConfig::standard()
        );
    }

    #[tokio::test]
    async fn both_fields_can_be_set_at_once() {
        assert_eq!(
            test_provider(&[(env::RETRY_MODE, "standard"), (env::MAX_ATTEMPTS, "13")])
                .await
                .unwrap(),
            RetryConfig::standard().with_max_attempts(13)
        );
    }

    #[tokio::test]
    async fn disallow_zero_max_attempts() {
        let err = test_provider(&[(env::MAX_ATTEMPTS, "0")])
            .await
            .unwrap_err()
            .err;
        assert!(matches!(
            err,
            RetryConfigError {
                kind: RetryConfigErrorKind::MaxAttemptsMustNotBeZero { .. }
            }
        ));
    }
}
+0 −4
Original line number Diff line number Diff line
@@ -16,7 +16,3 @@ pub use credentials::EnvironmentVariableCredentialsProvider;
/// Load regions from the environment
pub mod region;
pub use region::EnvironmentVariableRegionProvider;

/// Load retry behavior configuration from the environment
pub mod retry_config;
pub use retry_config::EnvironmentVariableRetryConfigProvider;
+0 −163
Original line number Diff line number Diff line
/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

use crate::retry::{
    error::RetryConfigError, error::RetryConfigErrorKind, RetryConfigBuilder, RetryMode,
};
use aws_types::os_shim_internal::Env;
use std::str::FromStr;

const ENV_VAR_MAX_ATTEMPTS: &str = "AWS_MAX_ATTEMPTS";
const ENV_VAR_RETRY_MODE: &str = "AWS_RETRY_MODE";

/// Load a retry_config from environment variables
///
/// This provider will check the values of `AWS_RETRY_MODE` and `AWS_MAX_ATTEMPTS`
/// in order to build a retry config.
#[derive(Debug, Default)]
pub struct EnvironmentVariableRetryConfigProvider {
    env: Env,
}

impl EnvironmentVariableRetryConfigProvider {
    /// Create a new [`EnvironmentVariableRetryConfigProvider`]
    pub fn new() -> Self {
        EnvironmentVariableRetryConfigProvider { env: Env::real() }
    }

    #[doc(hidden)]
    /// Create an retry_config provider from a given `Env`
    ///
    /// This method is used for tests that need to override environment variables.
    pub fn new_with_env(env: Env) -> Self {
        EnvironmentVariableRetryConfigProvider { env }
    }

    /// Attempt to create a new `RetryConfig` from environment variables
    pub fn retry_config_builder(&self) -> Result<RetryConfigBuilder, RetryConfigError> {
        let max_attempts = match self.env.get(ENV_VAR_MAX_ATTEMPTS).ok() {
            Some(max_attempts) => match max_attempts.parse::<u32>() {
                Ok(max_attempts) if max_attempts == 0 => {
                    return Err(RetryConfigErrorKind::MaxAttemptsMustNotBeZero {
                        set_by: "environment variable".into(),
                    }
                    .into());
                }
                Ok(max_attempts) => Some(max_attempts),
                Err(source) => {
                    return Err(RetryConfigErrorKind::FailedToParseMaxAttempts {
                        set_by: "environment variable".into(),
                        source,
                    }
                    .into());
                }
            },
            None => None,
        };

        let retry_mode = match self.env.get(ENV_VAR_RETRY_MODE) {
            Ok(retry_mode) => match RetryMode::from_str(&retry_mode) {
                Ok(retry_mode) => Some(retry_mode),
                Err(retry_mode_err) => {
                    return Err(RetryConfigErrorKind::InvalidRetryMode {
                        set_by: "environment variable".into(),
                        source: retry_mode_err,
                    }
                    .into());
                }
            },
            Err(_) => None,
        };

        let mut retry_config_builder = RetryConfigBuilder::new();
        retry_config_builder
            .set_max_attempts(max_attempts)
            .set_mode(retry_mode);

        Ok(retry_config_builder)
    }
}

#[cfg(test)]
mod test {
    use super::{EnvironmentVariableRetryConfigProvider, ENV_VAR_MAX_ATTEMPTS, ENV_VAR_RETRY_MODE};
    use crate::retry::{
        error::RetryConfigError, error::RetryConfigErrorKind, RetryConfig, RetryMode,
    };
    use aws_types::os_shim_internal::Env;

    fn test_provider(vars: &[(&str, &str)]) -> EnvironmentVariableRetryConfigProvider {
        EnvironmentVariableRetryConfigProvider::new_with_env(Env::from_slice(vars))
    }

    #[test]
    fn defaults() {
        let built = test_provider(&[]).retry_config_builder().unwrap().build();

        assert_eq!(built.mode(), RetryMode::Standard);
        assert_eq!(built.max_attempts(), 3);
    }

    #[test]
    fn max_attempts_is_read_correctly() {
        assert_eq!(
            test_provider(&[(ENV_VAR_MAX_ATTEMPTS, "88")])
                .retry_config_builder()
                .unwrap()
                .build(),
            RetryConfig::standard().with_max_attempts(88)
        );
    }

    #[test]
    fn max_attempts_errors_when_it_cant_be_parsed_as_an_integer() {
        assert!(matches!(
            test_provider(&[(ENV_VAR_MAX_ATTEMPTS, "not an integer")])
                .retry_config_builder()
                .unwrap_err(),
            RetryConfigError {
                kind: RetryConfigErrorKind::FailedToParseMaxAttempts { .. }
            }
        ));
    }

    #[test]
    fn retry_mode_is_read_correctly() {
        assert_eq!(
            test_provider(&[(ENV_VAR_RETRY_MODE, "standard")])
                .retry_config_builder()
                .unwrap()
                .build(),
            RetryConfig::standard()
        );
    }

    #[test]
    fn both_fields_can_be_set_at_once() {
        assert_eq!(
            test_provider(&[
                (ENV_VAR_RETRY_MODE, "standard"),
                (ENV_VAR_MAX_ATTEMPTS, "13")
            ])
            .retry_config_builder()
            .unwrap()
            .build(),
            RetryConfig::standard().with_max_attempts(13)
        );
    }

    #[test]
    fn disallow_zero_max_attempts() {
        let err = test_provider(&[(ENV_VAR_MAX_ATTEMPTS, "0")])
            .retry_config_builder()
            .unwrap_err();
        assert!(matches!(
            err,
            RetryConfigError {
                kind: RetryConfigErrorKind::MaxAttemptsMustNotBeZero { .. }
            }
        ));
    }
}
Loading