Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to programmatically customize profile files #1770

Merged
merged 3 commits into from
Sep 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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")]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIL. This is neat.

#[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