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

Imds region provider (#715)

* Add IMDS Region Provider

* Fix config-only profile parsing bug

* Add IMDS region test

* Update changelogs

* update IMDS comment
parent 0d2c1602
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -7,6 +7,7 @@ vNext (Month Day, Year)
- User agent construction is now `const fn` (#701)
- Update event stream `Receiver`s to be `Send` (#702, #aws-sdk-rust#224)
- Add `sts::AssumeRoleProvider` to `aws-config` (#703, aws-sdk-rust#3)
- Add IMDS region provider to `aws-config` (#715)
- Add query param signing to the `aws-sigv4` crate (#707)

v0.23 (September 14th, 2021)
+3 −1
Original line number Diff line number Diff line
@@ -3,10 +3,12 @@ vNext (Month Day, Year)
**New This Week**
- Add IMDS client to `aws-config`
- Add IMDS credential provider to `aws-config` (smithy-rs#709)
- Add IMDS region provider to `aws-config` (smithy-rs#715, #97)
- Update event stream `Receiver`s to be `Send` (aws-sdk-rust#224)
- Add `sts::AssumeRoleProvider` to `aws-config` (#703, aws-sdk-rust#3)
- Add `sts::AssumeRoleProvider` to `aws-config`. This enables customers to invoke STS directly, instead of using it via `~/.aws/config`. (#703, aws-sdk-rust#3)
- :bug: Fix panic when signing non-ASCII header values (smithy-rs#708, aws-sdk-rust#226)


v0.0.18-alpha (September 14th, 2021)
=======================
- :tada: Add support for `OpenSearch` service & bring in other model updates (#todo)
+5 −2
Original line number Diff line number Diff line
@@ -14,7 +14,7 @@ pub mod region {

    use crate::environment::region::EnvironmentVariableRegionProvider;
    use crate::meta::region::{ProvideRegion, RegionProviderChain};
    use crate::profile;
    use crate::{imds, profile};

    use crate::provider_config::ProviderConfig;

@@ -50,6 +50,7 @@ pub mod region {
    pub struct Builder {
        env_provider: EnvironmentVariableRegionProvider,
        profile_file: profile::region::Builder,
        imds: imds::region::Builder,
    }

    impl Builder {
@@ -61,6 +62,7 @@ pub mod region {
            self.env_provider =
                EnvironmentVariableRegionProvider::new_with_env(configuration.env());
            self.profile_file = self.profile_file.configure(configuration);
            self.imds = self.imds.configure(configuration);
            self
        }

@@ -74,7 +76,8 @@ pub mod region {
        pub fn build(self) -> DefaultRegionChain {
            DefaultRegionChain(
                RegionProviderChain::first_try(self.env_provider)
                    .or_else(self.profile_file.build()),
                    .or_else(self.profile_file.build())
                    .or_else(self.imds.build()),
            )
        }
    }
+53 −8
Original line number Diff line number Diff line
@@ -37,6 +37,7 @@ use crate::imds::client::token::TokenMiddleware;
use crate::profile::ProfileParseError;
use crate::provider_config::ProviderConfig;
use crate::{profile, PKG_VERSION};
use tokio::sync::OnceCell;

const USER_AGENT: AwsUserAgent =
    AwsUserAgent::new_from_environment(ApiMetadata::new("imds", PKG_VERSION));
@@ -113,6 +114,42 @@ pub struct Client {
    inner: smithy_client::Client<DynConnector, ImdsMiddleware>,
}

/// Client where build is sync, but usage is async
///
/// Building an imds::Client is actually an async operation, however, for credentials and region
/// providers, we want build to always be a synchronous operation. This allows building to be deferred
/// and cached until request time.
#[derive(Debug)]
pub(super) struct LazyClient {
    client: OnceCell<Result<Client, BuildError>>,
    builder: Builder,
}

impl LazyClient {
    pub fn from_ready_client(client: Client) -> Self {
        Self {
            client: OnceCell::from(Ok(client)),
            // the builder will never be used in this case
            builder: Builder::default(),
        }
    }
    pub(super) async fn client(&self) -> Result<&Client, &BuildError> {
        let builder = &self.builder;
        self.client
            // the clone will only happen once when we actually construct it for the first time,
            // after that, we will use the cache.
            .get_or_init(|| async {
                let client = builder.clone().build().await;
                if let Err(err) = &client {
                    tracing::warn!(err = % err, "failed to create IMDS client")
                }
                client
            })
            .await
            .as_ref()
    }
}

impl Client {
    /// IMDS client builder
    pub fn builder() -> Builder {
@@ -262,7 +299,7 @@ impl ParseStrictResponse for ImdsGetResponseHandler {
/// IMDS can be accessed in two ways:
/// 1. Via the IpV4 endpoint: `http://169.254.169.254`
/// 2. Via the Ipv6 endpoint: `http://[fd00:ec2::254]`
#[derive(Debug)]
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum EndpointMode {
    /// IpV4 mode: `http://169.254.169.254`
@@ -312,7 +349,7 @@ impl EndpointMode {
}

/// IMDSv2 Client Builder
#[derive(Default, Debug)]
#[derive(Default, Debug, Clone)]
pub struct Builder {
    num_retries: Option<u32>,
    endpoint: Option<EndpointSource>,
@@ -417,6 +454,13 @@ impl Builder {
        self
    }*/

    pub(super) fn build_lazy(self) -> LazyClient {
        LazyClient {
            client: OnceCell::new(),
            builder: self,
        }
    }

    /// Build an IMDSv2 Client
    pub async fn build(self) -> Result<Client, BuildError> {
        let config = self.config.unwrap_or_default();
@@ -460,7 +504,7 @@ mod profile_keys {
}

/// Endpoint Configuration Abstraction
#[derive(Debug)]
#[derive(Debug, Clone)]
enum EndpointSource {
    Explicit(Uri),
    Env(Env, Fs),
@@ -560,6 +604,7 @@ impl Error for TokenError {}

#[derive(Clone)]
struct ImdsErrorPolicy;

impl ImdsErrorPolicy {
    fn classify(response: &operation::Response) -> RetryKind {
        let status = response.http().status();
@@ -594,7 +639,7 @@ impl<T, E> ClassifyResponse<SdkSuccess<T>, SdkError<E>> for ImdsErrorPolicy {
}

#[cfg(test)]
mod test {
pub(crate) mod test {
    use std::collections::HashMap;
    use std::error::Error;
    use std::time::{Duration, UNIX_EPOCH};
@@ -614,7 +659,7 @@ mod test {
    const TOKEN_A: &str = "AQAEAFTNrA4eEGx0AQgJ1arIq_Cc-t4tWt3fB0Hd8RKhXlKc5ccvhg==";
    const TOKEN_B: &str = "alternatetoken==";

    fn token_request(base: &str, ttl: u32) -> http::Request<SdkBody> {
    pub(crate) fn token_request(base: &str, ttl: u32) -> http::Request<SdkBody> {
        http::Request::builder()
            .uri(format!("{}/latest/api/token", base))
            .header("x-aws-ec2-metadata-token-ttl-seconds", ttl)
@@ -623,7 +668,7 @@ mod test {
            .unwrap()
    }

    fn token_response(ttl: u32, token: &'static str) -> http::Response<&'static str> {
    pub(crate) fn token_response(ttl: u32, token: &'static str) -> http::Response<&'static str> {
        http::Response::builder()
            .status(200)
            .header("X-aws-ec2-metadata-token-ttl-seconds", ttl)
@@ -631,7 +676,7 @@ mod test {
            .unwrap()
    }

    fn imds_request(path: &'static str, token: &str) -> http::Request<SdkBody> {
    pub(crate) fn imds_request(path: &'static str, token: &str) -> http::Request<SdkBody> {
        http::Request::builder()
            .uri(Uri::from_static(path))
            .method("GET")
@@ -640,7 +685,7 @@ mod test {
            .unwrap()
    }

    fn imds_response(body: &'static str) -> http::Response<&'static str> {
    pub(crate) fn imds_response(body: &'static str) -> http::Response<&'static str> {
        http::Response::builder().status(200).body(body).unwrap()
    }

+18 −27
Original line number Diff line number Diff line
@@ -9,9 +9,10 @@
//! This credential provider will NOT fallback to IMDSv1. Ensure that IMDSv2 is enabled on your instances.

use crate::imds;
use crate::imds::client::ImdsError;
use crate::imds::client::{ImdsError, LazyClient};
use crate::provider_config::ProviderConfig;
use aws_types::credentials::{future, CredentialsError, ProvideCredentials};
use aws_types::os_shim_internal::Env;
use aws_types::{credentials, Credentials};
use smithy_client::SdkError;
use smithy_json::deserialize::token::skip_value;
@@ -24,18 +25,14 @@ use std::fmt::{Display, Formatter};
use std::time::SystemTime;
use tokio::sync::OnceCell;

mod env {
    pub(super) const EC2_METADATA_DISABLED: &str = "AWS_EC2_METADATA_DISABLED";
}

/// IMDSv2 Credentials Provider
///
/// **Note**: This credentials provider will NOT fallback to the IMDSv1 flow.
#[derive(Debug)]
pub struct ImdsCredentialsProvider {
    client: OnceCell<Result<imds::Client, imds::client::BuildError>>,
    client: LazyClient,
    env: Env,
    profile: OnceCell<String>,
    provider_config: ProviderConfig,
}

/// Builder for [`ImdsCredentialsProvider`]
@@ -83,16 +80,20 @@ impl Builder {
    /// Create an [`ImdsCredentialsProvider`] from this builder.
    pub fn build(self) -> ImdsCredentialsProvider {
        let provider_config = self.provider_config.unwrap_or_default();
        let client = if let Some(client) = self.imds_override {
            OnceCell::from(Ok(client))
        } else {
            OnceCell::new()
        };
        let env = provider_config.env();
        let client = self
            .imds_override
            .map(LazyClient::from_ready_client)
            .unwrap_or_else(|| {
                imds::Client::builder()
                    .configure(&provider_config)
                    .build_lazy()
            });
        let profile = OnceCell::new_with(self.profile_override);
        ImdsCredentialsProvider {
            client,
            env,
            profile,
            provider_config,
        }
    }
}
@@ -117,7 +118,7 @@ impl ImdsCredentialsProvider {
    }

    fn imds_disabled(&self) -> bool {
        match self.provider_config.env().get(env::EC2_METADATA_DISABLED) {
        match self.env.get(super::env::EC2_METADATA_DISABLED) {
            Ok(value) => value.eq_ignore_ascii_case("true"),
            _ => false,
        }
@@ -125,17 +126,7 @@ impl ImdsCredentialsProvider {

    /// Load an inner IMDS client from the OnceCell
    async fn client(&self) -> Result<&imds::Client, CredentialsError> {
        let provider_config = &self.provider_config;
        self.client
            .get_or_init(|| async {
                imds::Client::builder()
                    .configure(provider_config)
                    .build()
                    .await
            })
            .await
            .as_ref()
            .map_err(|build_error| {
        self.client.client().await.map_err(|build_error| {
            CredentialsError::InvalidConfiguration(format!("{}", build_error).into())
        })
    }
Loading