Unverified Commit 308634c4 authored by Zelda Hessler's avatar Zelda Hessler Committed by GitHub
Browse files

Support for accessing arbitrary profile config data (#3429)



## Motivation and Context
<!--- Why is this change required? What problem does it solve? -->
<!--- If it fixes an open issue, please link to the issue here -->
This is required to enable an upcoming service-specific env config
feature

## Description
<!--- Describe your changes in detail -->
This PR adds support for accessing profile config data defined in any
sort of section. It also supports sub-properties.

## Testing
<!--- Please describe in detail how you tested your changes -->
<!--- Include details of your testing environment, and the tests you ran
to -->
<!--- see how your change affects other areas of the code, etc. -->
I wrote tests

## Checklist
No changelog entry because this feature isn't publicly accessible yet.

----

_By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice._

---------

Co-authored-by: default avatarJohn DiSanti <jdisanti@amazon.com>
parent a884a669
Loading
Loading
Loading
Loading
+3 −3
Original line number Diff line number Diff line
@@ -173,8 +173,8 @@ mod test {
        let expected_retry_config = RetryConfig::standard();

        assert_eq!(actual_retry_config, expected_retry_config);
        // This is redundant but it's really important to make sure that
        // we're setting these exact values by default so we check twice
        // This is redundant, but it's really important to make sure that
        // we're setting these exact values by default, so we check twice
        assert_eq!(actual_retry_config.max_attempts(), 3);
        assert_eq!(actual_retry_config.mode(), RetryMode::Standard);
    }
@@ -255,7 +255,7 @@ retry_mode = standard
    }

    #[tokio::test]
    #[should_panic = "failed to parse max attempts. source: profile `default`, key: `max_attempts`: invalid digit found in string"]
    #[should_panic = "failed to parse max attempts. source: global 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(&[(
+1 −0
Original line number Diff line number Diff line
@@ -15,6 +15,7 @@ mod parser;
// to Rust 1.60
#[doc(inline)]
pub use parser::ProfileParseError;
pub(crate) use parser::PropertiesKey;
#[doc(inline)]
pub use parser::{load, Profile, ProfileFileLoadError, ProfileSet, Property};

+23 −221
Original line number Diff line number Diff line
@@ -3,21 +3,26 @@
 * SPDX-License-Identifier: Apache-2.0
 */

use crate::profile::parser::parse::{parse_profile_file, to_ascii_lowercase};
use crate::profile::parser::source::Source;
use crate::profile::profile_file::ProfileFiles;
use self::parse::parse_profile_file;
use self::section::{Section, SsoSession};
use self::source::Source;
use super::profile_file::ProfileFiles;
use crate::profile::parser::section::Properties;
use aws_types::os_shim_internal::{Env, Fs};
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;

pub use self::error::ProfileFileLoadError;
pub use self::parse::ProfileParseError;
pub use self::section::Profile;
pub use self::section::Property;

pub(crate) use self::section::PropertiesKey;

mod error;
mod normalize;
mod parse;
mod section;
mod source;

/// Read & parse AWS config files
@@ -75,6 +80,7 @@ pub struct ProfileSet {
    profiles: HashMap<String, Profile>,
    selected_profile: Cow<'static, str>,
    sso_sessions: HashMap<String, SsoSession>,
    other_sections: Properties,
}

impl ProfileSet {
@@ -153,6 +159,12 @@ impl ProfileSet {
        self.sso_sessions.get(name)
    }

    /// Returns a struct allowing access to other sections in the profile config
    #[allow(dead_code)] // Leaving this hidden for now.
    pub(crate) fn other_sections(&self) -> &Properties {
        &self.other_sections
    }

    fn parse(source: Source) -> Result<Self, ProfileParseError> {
        let mut base = ProfileSet::empty();
        base.selected_profile = source.profile;
@@ -168,225 +180,15 @@ impl ProfileSet {
            profiles: Default::default(),
            selected_profile: "default".into(),
            sso_sessions: Default::default(),
            other_sections: Default::default(),
        }
    }
}

/// Represents a top-level section (e.g., `[profile name]`) in a config file.
pub(crate) trait Section {
    /// The name of this section
    fn name(&self) -> &str;

    /// Returns all the properties in this section
    fn properties(&self) -> &HashMap<String, Property>;

    /// Returns a reference to the property named `name`
    fn get(&self, name: &str) -> Option<&str>;

    /// True if there are no properties in this section.
    fn is_empty(&self) -> bool;

    /// Insert a property into a section
    fn insert(&mut self, name: String, value: Property);
}

#[derive(Debug, Clone, Eq, PartialEq)]
struct SectionInner {
    name: String,
    properties: HashMap<String, Property>,
}

impl Section for SectionInner {
    fn name(&self) -> &str {
        &self.name
    }

    fn properties(&self) -> &HashMap<String, Property> {
        &self.properties
    }

    fn get(&self, name: &str) -> Option<&str> {
        self.properties
            .get(to_ascii_lowercase(name).as_ref())
            .map(|prop| prop.value())
    }

    fn is_empty(&self) -> bool {
        self.properties.is_empty()
    }

    fn insert(&mut self, name: String, value: Property) {
        self.properties
            .insert(to_ascii_lowercase(&name).into(), value);
    }
}

/// An individual configuration profile
///
/// An AWS config may be composed of a multiple named profiles within a [`ProfileSet`].
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Profile(SectionInner);

impl Profile {
    /// Create a new profile
    pub fn new(name: impl Into<String>, properties: HashMap<String, Property>) -> Self {
        Self(SectionInner {
            name: name.into(),
            properties,
        })
    }

    /// The name of this profile
    pub fn name(&self) -> &str {
        self.0.name()
    }

    /// Returns a reference to the property named `name`
    pub fn get(&self, name: &str) -> Option<&str> {
        self.0.get(name)
    }
}

impl Section for Profile {
    fn name(&self) -> &str {
        self.0.name()
    }

    fn properties(&self) -> &HashMap<String, Property> {
        self.0.properties()
    }

    fn get(&self, name: &str) -> Option<&str> {
        self.0.get(name)
    }

    fn is_empty(&self) -> bool {
        self.0.is_empty()
    }

    fn insert(&mut self, name: String, value: Property) {
        self.0.insert(name, value)
    }
}

/// A `[sso-session name]` section in the config.
#[derive(Debug, Clone, Eq, PartialEq)]
pub(crate) struct SsoSession(SectionInner);

impl SsoSession {
    /// Create a new SSO session section.
    pub(crate) fn new(name: impl Into<String>, properties: HashMap<String, Property>) -> Self {
        Self(SectionInner {
            name: name.into(),
            properties,
        })
    }

    /// Returns a reference to the property named `name`
    pub(crate) fn get(&self, name: &str) -> Option<&str> {
        self.0.get(name)
    }
}

impl Section for SsoSession {
    fn name(&self) -> &str {
        self.0.name()
    }

    fn properties(&self) -> &HashMap<String, Property> {
        self.0.properties()
    }

    fn get(&self, name: &str) -> Option<&str> {
        self.0.get(name)
    }

    fn is_empty(&self) -> bool {
        self.0.is_empty()
    }

    fn insert(&mut self, name: String, value: Property) {
        self.0.insert(name, value)
    }
}

/// Key-Value property pair
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Property {
    key: String,
    value: String,
}

impl Property {
    /// Value of this property
    pub fn value(&self) -> &str {
        &self.value
    }

    /// Name of this property
    pub fn key(&self) -> &str {
        &self.key
    }

    /// Creates a new property
    pub fn new(key: String, value: String) -> Self {
        Property { key, value }
    }
}

/// Failed to read or parse the profile file(s)
#[derive(Debug, Clone)]
pub enum ProfileFileLoadError {
    /// The profile could not be parsed
    #[non_exhaustive]
    ParseError(ProfileParseError),

    /// Attempt to read the AWS config file (`~/.aws/config` by default) failed with a filesystem error.
    #[non_exhaustive]
    CouldNotReadFile(CouldNotReadProfileFile),
}

impl Display for ProfileFileLoadError {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            ProfileFileLoadError::ParseError(_err) => {
                write!(f, "could not parse profile file")
            }
            ProfileFileLoadError::CouldNotReadFile(err) => {
                write!(f, "could not read file `{}`", err.path.display())
            }
        }
    }
}

impl Error for ProfileFileLoadError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            ProfileFileLoadError::ParseError(err) => Some(err),
            ProfileFileLoadError::CouldNotReadFile(details) => Some(&details.cause),
        }
    }
}

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

/// An error encountered while reading the AWS config file
#[derive(Debug, Clone)]
pub struct CouldNotReadProfileFile {
    pub(crate) path: PathBuf,
    pub(crate) cause: Arc<std::io::Error>,
}

#[cfg(test)]
mod test {
    use crate::profile::parser::{
        source::{File, Source},
        Section,
    };
    use super::section::Section;
    use super::source::{File, Source};
    use crate::profile::profile_file::ProfileFileKind;
    use crate::profile::ProfileSet;
    use arbitrary::{Arbitrary, Unstructured};
@@ -492,7 +294,7 @@ mod test {
                    section
                        .properties()
                        .values()
                        .map(|prop| (prop.key.clone(), prop.value.clone()))
                        .map(|prop| (prop.key().to_owned(), prop.value().to_owned()))
                        .collect(),
                )
            })
+57 −0
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::profile::ProfileParseError;
use std::error::Error;
use std::fmt::{Display, Formatter};
use std::path::PathBuf;
use std::sync::Arc;

/// Failed to read or parse the profile file(s)
#[derive(Debug, Clone)]
pub enum ProfileFileLoadError {
    /// The profile could not be parsed
    #[non_exhaustive]
    ParseError(ProfileParseError),

    /// Attempt to read the AWS config file (`~/.aws/config` by default) failed with a filesystem error.
    #[non_exhaustive]
    CouldNotReadFile(CouldNotReadProfileFile),
}

impl Display for ProfileFileLoadError {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            ProfileFileLoadError::ParseError(_err) => {
                write!(f, "could not parse profile file")
            }
            ProfileFileLoadError::CouldNotReadFile(err) => {
                write!(f, "could not read file `{}`", err.path.display())
            }
        }
    }
}

impl Error for ProfileFileLoadError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            ProfileFileLoadError::ParseError(err) => Some(err),
            ProfileFileLoadError::CouldNotReadFile(details) => Some(&details.cause),
        }
    }
}

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

/// An error encountered while reading the AWS config file
#[derive(Debug, Clone)]
pub struct CouldNotReadProfileFile {
    pub(crate) path: PathBuf,
    pub(crate) cause: Arc<std::io::Error>,
}
+169 −114

File changed.

Preview size limit exceeded, changes collapsed.

Loading