Skip to content

Commit

Permalink
Add ability to programmatically customize profile files (#1770)
Browse files Browse the repository at this point in the history
  • Loading branch information
jdisanti authored Sep 27, 2022
1 parent 5b7aa7b commit ef85116
Show file tree
Hide file tree
Showing 13 changed files with 679 additions and 186 deletions.
28 changes: 28 additions & 0 deletions CHANGELOG.next.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,31 @@ of breaking changes and how to resolve them.
references = ["smithy-rs#1740", "smithy-rs#256"]
meta = { "breaking" = true, "tada" = false, "bug" = false, "target" = "client" }
author = "jdisanti"

[[aws-sdk-rust]]
message = """
It is now possible to programmatically customize the locations of the profile config/credentials files in `aws-config`:
```rust
use aws_config::profile::{ProfileFileCredentialsProvider, ProfileFileRegionProvider};
use aws_config::profile::profile_file::{ProfileFiles, ProfileFileKind};
let profile_files = ProfileFiles::builder()
.with_file(ProfileFileKind::Credentials, "some/path/to/credentials-file")
.build();
let credentials_provider = ProfileFileCredentialsProvider::builder()
.profile_files(profile_files.clone())
.build();
let region_provider = ProfileFileRegionProvider::builder()
.profile_files(profile_files)
.build();
let sdk_config = aws_config::from_env()
.credentials_provider(credentials_provider)
.region(region_provider)
.load()
.await;
```
"""
references = ["aws-sdk-rust#237", "smithy-rs#1770"]
meta = { "breaking" = false, "tada" = true, "bug" = false }
author = "jdisanti"
6 changes: 3 additions & 3 deletions aws/rust-runtime/aws-config/src/imds/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ use tokio::sync::OnceCell;

use crate::connector::expect_connector;
use crate::imds::client::token::TokenMiddleware;
use crate::profile::ProfileParseError;
use crate::profile::credentials::ProfileFileError;
use crate::provider_config::ProviderConfig;
use crate::{profile, PKG_VERSION};
use aws_sdk_sso::config::timeout::TimeoutConfig;
Expand Down Expand Up @@ -439,7 +439,7 @@ pub enum BuildError {
InvalidEndpointMode(InvalidEndpointMode),

/// The AWS Profile (e.g. `~/.aws/config`) was invalid
InvalidProfile(ProfileParseError),
InvalidProfile(ProfileFileError),

/// The specified endpoint was not a valid URI
InvalidEndpointUri(InvalidUri),
Expand Down Expand Up @@ -626,7 +626,7 @@ impl EndpointSource {
}
EndpointSource::Env(env, fs) => {
// load an endpoint override from the environment
let profile = profile::load(fs, env)
let profile = profile::load(fs, env, &Default::default())
.await
.map_err(BuildError::InvalidProfile)?;
let uri_override = if let Ok(uri) = env.get(env::ENDPOINT) {
Expand Down
9 changes: 8 additions & 1 deletion aws/rust-runtime/aws-config/src/profile/app_name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

//! Load an app name from an AWS profile
use super::profile_file::ProfileFiles;
use crate::provider_config::ProviderConfig;
use aws_types::app_name::AppName;
use aws_types::os_shim_internal::{Env, Fs};
Expand All @@ -14,6 +15,8 @@ use aws_types::os_shim_internal::{Env, Fs};
/// This provider will attempt to shared AWS shared configuration and then read the
/// `sdk-ua-app-id` property from the active profile.
///
#[doc = include_str!("location_of_profile_files.md")]
///
/// # Examples
///
/// **Loads "my-app" as the app name**
Expand All @@ -35,6 +38,7 @@ pub struct ProfileFileAppNameProvider {
fs: Fs,
env: Env,
profile_override: Option<String>,
profile_files: ProfileFiles,
}

impl ProfileFileAppNameProvider {
Expand All @@ -46,6 +50,7 @@ impl ProfileFileAppNameProvider {
fs: Fs::real(),
env: Env::real(),
profile_override: None,
profile_files: Default::default(),
}
}

Expand All @@ -56,7 +61,7 @@ impl ProfileFileAppNameProvider {

/// Parses the profile config and attempts to find an app name.
pub async fn app_name(&self) -> Option<AppName> {
let profile = super::parser::load(&self.fs, &self.env)
let profile = super::parser::load(&self.fs, &self.env, &self.profile_files)
.await
.map_err(|err| tracing::warn!(err = %err, "failed to parse profile"))
.ok()?;
Expand All @@ -82,6 +87,7 @@ impl ProfileFileAppNameProvider {
pub struct Builder {
config: Option<ProviderConfig>,
profile_override: Option<String>,
profile_files: Option<ProfileFiles>,
}

impl Builder {
Expand All @@ -104,6 +110,7 @@ impl Builder {
env: conf.env(),
fs: conf.fs(),
profile_override: self.profile_override,
profile_files: self.profile_files.unwrap_or_default(),
}
}
}
Expand Down
79 changes: 47 additions & 32 deletions aws/rust-runtime/aws-config/src/profile/credentials.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,21 @@
//! - `exec` which contains a chain representation of providers to implement passing bootstrapped credentials
//! through a series of providers.
use crate::profile::credentials::exec::named::NamedProviderFactory;
use crate::profile::credentials::exec::{ClientConfiguration, ProviderChain};
use crate::profile::parser::ProfileParseError;
use crate::profile::profile_file::ProfileFiles;
use crate::profile::Profile;
use crate::provider_config::ProviderConfig;
use aws_types::credentials::{self, future, CredentialsError, ProvideCredentials};
use std::borrow::Cow;
use std::collections::HashMap;
use std::error::Error;
use std::fmt::{Display, Formatter};
use std::path::PathBuf;
use std::sync::Arc;

use aws_types::credentials::{self, future, CredentialsError, ProvideCredentials};

use tracing::Instrument;

use crate::profile::credentials::exec::named::NamedProviderFactory;
use crate::profile::credentials::exec::{ClientConfiguration, ProviderChain};
use crate::profile::parser::ProfileParseError;
use crate::profile::Profile;
use crate::provider_config::ProviderConfig;

mod exec;
mod repr;

Expand Down Expand Up @@ -142,29 +141,14 @@ impl ProvideCredentials for ProfileFileCredentialsProvider {
///
/// SSO can also be used as a source profile for assume role chains.
///
/// ## Location of Profile Files
/// * The location of the config file will be loaded from the `AWS_CONFIG_FILE` environment variable
/// with a fallback to `~/.aws/config`
/// * The location of the credentials file will be loaded from the `AWS_SHARED_CREDENTIALS_FILE`
/// environment variable with a fallback to `~/.aws/credentials`
///
/// ## Home directory resolution
/// Home directory resolution is implemented to match the behavior of the CLI & Python. `~` is only
/// used for home directory resolution when it:
/// - Starts the path
/// - Is followed immediately by `/` or a platform specific separator. (On windows, `~/` and `~\` both
/// resolve to the home directory.
///
/// When determining the home directory, the following environment variables are checked:
/// - `HOME` on all platforms
/// - `USERPROFILE` on Windows
/// - The concatenation of `HOMEDRIVE` and `HOMEPATH` on Windows (`$HOMEDRIVE$HOMEPATH`)
#[doc = include_str!("location_of_profile_files.md")]
#[derive(Debug)]
pub struct ProfileFileCredentialsProvider {
factory: NamedProviderFactory,
client_config: ClientConfiguration,
provider_config: ProviderConfig,
profile_override: Option<String>,
profile_files: ProfileFiles,
}

impl ProfileFileCredentialsProvider {
Expand All @@ -178,6 +162,7 @@ impl ProfileFileCredentialsProvider {
&self.provider_config,
&self.factory,
self.profile_override.as_deref(),
&self.profile_files,
)
.await
.map_err(|err| match err {
Expand Down Expand Up @@ -225,6 +210,13 @@ impl ProfileFileCredentialsProvider {
}
}

#[doc(hidden)]
#[derive(Debug)]
pub struct CouldNotReadProfileFile {
pub(crate) path: PathBuf,
pub(crate) cause: std::io::Error,
}

/// An Error building a Credential source from an AWS Profile
#[derive(Debug)]
#[non_exhaustive]
Expand Down Expand Up @@ -283,6 +275,10 @@ pub enum ProfileFileError {
/// The name of the provider
name: String,
},

/// A custom profile file location didn't exist or could not be read
#[non_exhaustive]
CouldNotReadProfileFile(CouldNotReadProfileFile),
}

impl ProfileFileError {
Expand Down Expand Up @@ -326,6 +322,13 @@ impl Display for ProfileFileError {
"profile `{}` did not contain credential information",
profile
),
ProfileFileError::CouldNotReadProfileFile(details) => {
write!(
f,
"Failed to read custom profile file at {:?}",
details.path
)
}
}
}
}
Expand All @@ -334,16 +337,24 @@ impl Error for ProfileFileError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
ProfileFileError::CouldNotParseProfile(err) => Some(err),
ProfileFileError::CouldNotReadProfileFile(details) => Some(&details.cause),
_ => None,
}
}
}

impl From<ProfileParseError> for ProfileFileError {
fn from(err: ProfileParseError) -> Self {
ProfileFileError::CouldNotParseProfile(err)
}
}

/// Builder for [`ProfileFileCredentialsProvider`]
#[derive(Debug, Default)]
pub struct Builder {
provider_config: Option<ProviderConfig>,
profile_override: Option<String>,
profile_files: Option<ProfileFiles>,
custom_providers: HashMap<Cow<'static, str>, Arc<dyn ProvideCredentials>>,
}

Expand Down Expand Up @@ -409,6 +420,12 @@ impl Builder {
self
}

/// Set the profile file that should be used by the [`ProfileFileCredentialsProvider`]
pub fn profile_files(mut self, profile_files: ProfileFiles) -> Self {
self.profile_files = Some(profile_files);
self
}

/// Builds a [`ProfileFileCredentialsProvider`]
pub fn build(self) -> ProfileFileCredentialsProvider {
let build_span = tracing::debug_span!("build_profile_provider");
Expand Down Expand Up @@ -453,6 +470,7 @@ impl Builder {
},
provider_config: conf,
profile_override: self.profile_override,
profile_files: self.profile_files.unwrap_or_default(),
}
}
}
Expand All @@ -461,13 +479,10 @@ async fn build_provider_chain(
provider_config: &ProviderConfig,
factory: &NamedProviderFactory,
profile_override: Option<&str>,
profile_files: &ProfileFiles,
) -> Result<ProviderChain, ProfileFileError> {
let profile_set = super::parser::load(&provider_config.fs(), &provider_config.env())
.await
.map_err(|err| {
tracing::warn!(err = %err, "failed to parse profile");
ProfileFileError::CouldNotParseProfile(err)
})?;
let profile_set =
super::parser::load(&provider_config.fs(), &provider_config.env(), profile_files).await?;
let repr = repr::resolve_chain(&profile_set, profile_override)?;
tracing::info!(chain = ?repr, "constructed abstract provider from config file");
exec::ProviderChain::from_repr(provider_config, repr, factory)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
## Location of Profile Files
* The location of the config file will be loaded from the `AWS_CONFIG_FILE` environment variable
with a fallback to `~/.aws/config`
* The location of the credentials file will be loaded from the `AWS_SHARED_CREDENTIALS_FILE`
environment variable with a fallback to `~/.aws/credentials`

The location of these files can also be customized programmatically using [`ProfileFiles`](crate::profile::profile_file::ProfileFiles).

## Home directory resolution
Home directory resolution is implemented to match the behavior of the CLI & Python. `~` is only
used for home directory resolution when it:
- Starts the path
- Is followed immediately by `/` or a platform specific separator. (On windows, `~/` and `~\` both
resolve to the home directory.

When determining the home directory, the following environment variables are checked:
- `HOME` on all platforms
- `USERPROFILE` on Windows
- The concatenation of `HOMEDRIVE` and `HOMEPATH` on Windows (`$HOMEDRIVE$HOMEPATH`)
1 change: 1 addition & 0 deletions aws/rust-runtime/aws-config/src/profile/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pub use parser::{load, Profile, ProfileSet, Property};

pub mod app_name;
pub mod credentials;
pub mod profile_file;
pub mod region;
pub mod retry_config;

Expand Down
Loading

0 comments on commit ef85116

Please sign in to comment.