Skip to content

Commit ce7efea

Browse files
authored
feat(rt): AWS configuration loader and parser (#315)
1 parent a39781d commit ce7efea

File tree

17 files changed

+2142
-7
lines changed

17 files changed

+2142
-7
lines changed

aws-runtime/auth/common/src/aws/sdk/kotlin/runtime/auth/signing/Presigner.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import aws.sdk.kotlin.crt.auth.signing.AwsSignedBodyHeaderType
1010
import aws.sdk.kotlin.crt.auth.signing.AwsSignedBodyValue
1111
import aws.sdk.kotlin.crt.auth.signing.AwsSigner
1212
import aws.sdk.kotlin.crt.auth.signing.AwsSigningConfig
13+
import aws.sdk.kotlin.runtime.InternalSdkApi
1314
import aws.sdk.kotlin.runtime.auth.credentials.CredentialsProvider
1415
import aws.sdk.kotlin.runtime.auth.credentials.toCrt
1516
import aws.sdk.kotlin.runtime.crt.path
@@ -24,7 +25,6 @@ import aws.smithy.kotlin.runtime.http.Protocol
2425
import aws.smithy.kotlin.runtime.http.QueryParameters
2526
import aws.smithy.kotlin.runtime.http.Url
2627
import aws.smithy.kotlin.runtime.http.request.HttpRequest
27-
import aws.smithy.kotlin.runtime.util.InternalApi
2828
import aws.sdk.kotlin.crt.http.HttpRequest as CrtHttpRequest
2929

3030
/**
@@ -80,7 +80,7 @@ public data class PresignedRequestConfig(
8080
* @param requestConfig The presign configuration to use in signing the request
8181
* @return a [HttpRequest] that can be executed by any HTTP client within the specified duration.
8282
*/
83-
@InternalApi
83+
@InternalSdkApi
8484
public suspend fun createPresignedRequest(serviceConfig: ServicePresignConfig, requestConfig: PresignedRequestConfig): HttpRequest {
8585
val crtCredentials = serviceConfig.credentialsProvider.getCredentials().toCrt()
8686
val endpoint = serviceConfig.endpointResolver.resolve(serviceConfig.serviceId, serviceConfig.region)

aws-runtime/aws-core/build.gradle.kts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,16 @@ kotlin {
1515
commonMain {
1616
dependencies {
1717
api("aws.smithy.kotlin:runtime-core:$smithyKotlinVersion")
18+
implementation("aws.smithy.kotlin:logging:$smithyKotlinVersion")
19+
}
20+
}
21+
commonTest {
22+
dependencies {
23+
val kotlinxSerializationVersion: String by project
24+
val mockkVersion: String by project
25+
implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime:$kotlinxSerializationVersion")
26+
implementation("io.mockk:mockk:$mockkVersion")
27+
implementation(project(":aws-runtime:testing"))
1828
}
1929
}
2030
}

aws-runtime/aws-core/common/src/aws/sdk/kotlin/runtime/Annotations.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ package aws.sdk.kotlin.runtime
1616
@Suppress("DEPRECATION")
1717
@RequiresOptIn(
1818
level = RequiresOptIn.Level.ERROR,
19-
message = "This API is internal to aws-client-rt and generated SDK's and should not be used. It could be removed or changed without notice."
19+
message = "This API is internal to aws-runtime and generated SDKs and should not be used. It could be removed or changed without notice."
2020
)
2121
@Experimental(level = Experimental.Level.ERROR)
2222
@Target(

aws-runtime/aws-core/common/src/aws/sdk/kotlin/runtime/AwsSdkSetting.kt

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,17 @@
55

66
package aws.sdk.kotlin.runtime
77

8+
import aws.smithy.kotlin.runtime.util.Platform
9+
810
// NOTE: The JVM property names MUST match the ones defined in the Java SDK for any setting added.
911
// see: https://github.com/aws/aws-sdk-java-v2/blob/master/core/sdk-core/src/main/java/software/amazon/awssdk/core/SdkSystemSetting.java
12+
// see: https://github.com/aws/aws-sdk-java-v2/blob/master/docs/LaunchChangelog.md#61-environment-variables-and-system-properties
1013

1114
/**
1215
* Settings to configure SDK runtime behavior
1316
*/
1417
@InternalSdkApi
15-
public sealed class AwsSdkSetting<T> (
18+
public sealed class AwsSdkSetting<T>(
1619
/**
1720
* The name of the corresponding environment variable that configures the setting
1821
*/
@@ -52,6 +55,17 @@ public sealed class AwsSdkSetting<T> (
5255
*/
5356
public object AwsRegion : AwsSdkSetting<String>("AWS_REGION", "aws.region")
5457

58+
/**
59+
* Configure the default path to the shared config file.
60+
*/
61+
public object AwsConfigFile : AwsSdkSetting<String>("AWS_CONFIG_FILE", "aws.configFile")
62+
63+
/**
64+
* Configure the default path to the shared credentials profile file.
65+
*/
66+
public object AwsSharedCredentialsFile :
67+
AwsSdkSetting<String>("AWS_SHARED_CREDENTIALS_FILE", "aws.sharedCredentialsFile")
68+
5569
/**
5670
* The execution environment of the SDK user. This is automatically set in certain environments by the underlying AWS service.
5771
* For example, AWS Lambda will automatically specify a runtime indicating that the SDK is being used within Lambda.
@@ -63,3 +77,14 @@ public sealed class AwsSdkSetting<T> (
6377
*/
6478
public object AwsProfile : AwsSdkSetting<String>("AWS_PROFILE", "aws.profile", "default")
6579
}
80+
81+
/**
82+
* Read the [AwsSdkSetting] by first checking JVM property, environment variable, and default value.
83+
* Property sources not available on a given platform will be ignored.
84+
*
85+
* @param platform A singleton that provides platform-specific settings. Exposed as a parameter for testing.
86+
* @return the value of the [AwsSdkSetting] or null if undefined.
87+
*/
88+
@InternalSdkApi
89+
public inline fun <reified T> AwsSdkSetting<T>.resolve(platform: Platform): T? =
90+
(platform.getProperty(jvmProperty) ?: platform.getenv(environmentVariable) ?: defaultValue) as T?
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package aws.sdk.kotlin.runtime.config
2+
3+
import aws.sdk.kotlin.runtime.AwsSdkSetting
4+
import aws.sdk.kotlin.runtime.InternalSdkApi
5+
import aws.sdk.kotlin.runtime.resolve
6+
import aws.smithy.kotlin.runtime.util.OsFamily
7+
import aws.smithy.kotlin.runtime.util.Platform
8+
9+
/**
10+
* Load the properties of the specified or default AWS configuration profile. This
11+
* function will return the properties of the profile specified by the local environment
12+
* or the default profile if none is defined.
13+
*
14+
* This function performs no caching. File I/O will be performed with each call.
15+
*
16+
* @param platform used for unit testing
17+
*
18+
* @return an [AwsProfile] regardless if local configuration files are available
19+
*/
20+
@InternalSdkApi
21+
public suspend fun loadActiveAwsProfile(platform: Platform): AwsProfile {
22+
// Determine active profile and location of configuration files
23+
val source = resolveConfigSource(platform)
24+
25+
// Read all profiles from local system
26+
val allProfiles = loadAwsProfiles(platform, source)
27+
28+
// Return the active profile
29+
return AwsProfile(source.profile, allProfiles[source.profile] ?: emptyMap())
30+
}
31+
32+
/**
33+
* Load all profiles specified in local configuration files.
34+
*
35+
* @param platform Platform from which to resolve configuration data
36+
* @param source Specifies the location of the configuration files
37+
*
38+
* @return A map of all profiles, which each are a map of key/value pairs.
39+
*/
40+
private suspend fun loadAwsProfiles(platform: Platform, source: AwsConfigurationSource): Map<String, Map<String, String>> {
41+
42+
// merged AWS configuration based on optional configuration and credential file contents
43+
return mergeProfiles(
44+
parse(FileType.CONFIGURATION, platform.readFileOrNull(source.configPath)?.decodeToString()),
45+
parse(FileType.CREDENTIAL, platform.readFileOrNull(source.credentialsPath)?.decodeToString()),
46+
)
47+
}
48+
49+
// Merge contents of profile maps
50+
internal fun mergeProfiles(vararg maps: ProfileMap) = buildMap<String, Map<String, String>> {
51+
maps.forEach { map ->
52+
map.entries.forEach { entry ->
53+
put(entry.key, (get(entry.key) ?: emptyMap()) + entry.value)
54+
}
55+
}
56+
}
57+
58+
// Specifies the active profile and configured (may not actually exist) locations of configuration files.
59+
internal data class AwsConfigurationSource(val profile: String, val configPath: String, val credentialsPath: String)
60+
61+
/**
62+
* Determine the source of AWS configuration
63+
*/
64+
internal fun resolveConfigSource(platform: Platform) =
65+
AwsConfigurationSource(
66+
// If the user does not specify the profile to be used, the default profile must be used by the SDK.
67+
// The default profile must be overridable using the AWS_PROFILE environment variable.
68+
AwsSdkSetting.AwsProfile.resolve(platform) ?: Literals.DEFAULT_PROFILE,
69+
normalizePath(FileType.CONFIGURATION.path(platform), platform),
70+
normalizePath(FileType.CREDENTIAL.path(platform), platform)
71+
)
72+
73+
/**
74+
* Expands paths prefixed with '~' to the home directory under which the SDK is running.
75+
*
76+
* User Home Resolution: The user's home directory must be resolved when the file location starts with ~/ or ~
77+
* followed by the operating system's default path separator by checking the following variables, in order:
78+
*
79+
* 1. (All Platforms) The HOME environment variable.
80+
* 2. (Windows Platforms) The USERPROFILE environment variable.
81+
* 3. (Windows Platforms) The HOMEDRIVE environment variable prepended to the HOMEPATH environment variable (ie. $HOMEDRIVE$HOMEPATH).
82+
* 4. (Optional) A language-specific home path resolution function or variable.
83+
*/
84+
internal fun normalizePath(path: String, platform: Platform): String {
85+
if (!path.trim().startsWith('~')) return path
86+
87+
val home = resolveHomeDir(platform) ?: error("Unable to determine user home directory")
88+
89+
return home + path.substring(1)
90+
}
91+
92+
/**
93+
* Load the user's home directory based on the priorities:
94+
*
95+
* If the implementation cannot determine the customer's platform, the USERPROFILE and HOMEDRIVE + HOMEPATH environment
96+
* variables must be checked for all platforms. If the implementation can determine the customer's platform, the
97+
* USERPROFILE and HOMEDRIVE + HOMEPATH environment variables must not be checked on non-windows platforms.
98+
*
99+
* @param
100+
* @return the absolute path of the home directory from which the SDK is running, or null if unspecified by environment.
101+
*/
102+
private fun resolveHomeDir(platform: Platform): String? =
103+
with(platform) {
104+
when (osInfo().family) {
105+
OsFamily.Unknown,
106+
OsFamily.Windows ->
107+
getenv("HOME")
108+
?: getenv("USERPROFILE")
109+
?: (getenv("HOMEDRIVE") to getenv("HOMEPATH")).concatOrNull()
110+
?: getProperty("user.home")
111+
else ->
112+
getenv("HOME")
113+
?: getProperty("user.home")
114+
}
115+
}

0 commit comments

Comments
 (0)