Skip to content

Commit 599e24e

Browse files
authored
feat(rt): detect region from active AWS profile (#344)
1 parent 0505e35 commit 599e24e

File tree

10 files changed

+246
-19
lines changed

10 files changed

+246
-19
lines changed

aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/region/DefaultRegionProviderChain.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
package aws.sdk.kotlin.runtime.region
77

88
import aws.smithy.kotlin.runtime.io.Closeable
9+
import aws.smithy.kotlin.runtime.util.Platform
10+
import aws.smithy.kotlin.runtime.util.PlatformProvider
911

1012
/**
1113
* [RegionProvider] that looks for region in this order:
@@ -14,4 +16,6 @@ import aws.smithy.kotlin.runtime.io.Closeable
1416
* 3. Check the AWS config files/profile for region information
1517
* 4. If running on EC2, check the EC2 metadata service for region
1618
*/
17-
public expect class DefaultRegionProviderChain public constructor() : RegionProvider, Closeable
19+
public expect class DefaultRegionProviderChain public constructor(
20+
platformProvider: PlatformProvider = Platform
21+
) : RegionProvider, Closeable

aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/region/EnvironmentRegionProvider.kt

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,8 @@ import aws.smithy.kotlin.runtime.util.Platform
1313
* [RegionProvider] that checks `AWS_REGION` region environment variable
1414
* @param environ the environment mapping to lookup keys in (defaults to the system environment)
1515
*/
16-
public class EnvironmentRegionProvider(
17-
private val environ: EnvironmentProvider
16+
internal class EnvironmentRegionProvider(
17+
private val environ: EnvironmentProvider = Platform
1818
) : RegionProvider {
19-
public constructor() : this(Platform)
20-
2119
override suspend fun getRegion(): String? = environ.getenv(AwsSdkSetting.AwsRegion.environmentVariable)
2220
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0.
4+
*/
5+
6+
package aws.sdk.kotlin.runtime.region
7+
8+
import aws.sdk.kotlin.runtime.config.profile.loadActiveAwsProfile
9+
import aws.sdk.kotlin.runtime.config.profile.region
10+
import aws.smithy.kotlin.runtime.util.Platform
11+
import aws.smithy.kotlin.runtime.util.PlatformProvider
12+
import aws.smithy.kotlin.runtime.util.asyncLazy
13+
14+
/**
15+
* [RegionProvider] that sources region information from the active profile
16+
*/
17+
internal class ProfileRegionProvider(
18+
private val platformProvider: PlatformProvider = Platform
19+
) : RegionProvider {
20+
private val profile = asyncLazy { loadActiveAwsProfile(platformProvider) }
21+
22+
override suspend fun getRegion(): String? = profile.get().region
23+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0.
4+
*/
5+
6+
package aws.sdk.kotlin.runtime.region
7+
8+
import aws.sdk.kotlin.runtime.testing.TestPlatformProvider
9+
import aws.sdk.kotlin.runtime.testing.runSuspendTest
10+
import kotlinx.serialization.json.*
11+
import kotlin.test.Test
12+
import kotlin.test.assertEquals
13+
14+
class DefaultRegionProviderChainTest {
15+
private data class RegionProviderChainTest(
16+
val name: String,
17+
val platformProvider: TestPlatformProvider,
18+
val region: String?,
19+
val targets: List<String> = emptyList()
20+
)
21+
22+
@Test
23+
fun testSuite(): Unit = runSuspendTest {
24+
val tests = Json.parseToJsonElement(regionProviderChainTestSuite).jsonArray
25+
.map { it.jsonObject }
26+
.map {
27+
val name = it["name"]!!.jsonPrimitive.content
28+
val platform = TestPlatformProvider.fromJsonNode(it["platform"]!!.jsonObject)
29+
val region = it["region"]!!.jsonPrimitive.contentOrNull
30+
RegionProviderChainTest(name, platform, region)
31+
}
32+
33+
tests.forEach { test ->
34+
val provider = DefaultRegionProviderChain(test.platformProvider)
35+
val actual = provider.getRegion()
36+
assertEquals(test.region, actual, test.name)
37+
}
38+
}
39+
}
40+
41+
/**
42+
* Construct a [TestPlatformProvider] from a JSON node like:
43+
*
44+
* ```json
45+
* {
46+
* "env": {
47+
* "ENV_VAR": "value"
48+
* },
49+
* "props": {
50+
* "aws.property": "value"
51+
* },
52+
* "fs": {
53+
* "filename": "contents"
54+
* }
55+
* }
56+
* ```
57+
*/
58+
fun TestPlatformProvider.Companion.fromJsonNode(obj: JsonObject): TestPlatformProvider {
59+
val env = obj["env"]?.jsonObject?.mapValues { it.value.jsonPrimitive.content } ?: emptyMap()
60+
val props = obj["props"]?.jsonObject?.mapValues { it.value.jsonPrimitive.content } ?: emptyMap()
61+
val fs = obj["fs"]?.jsonObject?.mapValues { it.value.jsonPrimitive.content } ?: emptyMap()
62+
return TestPlatformProvider(env, props, fs)
63+
}
64+
65+
// language=JSON
66+
private const val regionProviderChainTestSuite = """
67+
[
68+
{
69+
"name": "no region configured",
70+
"platform": {
71+
"env": {},
72+
"props": {},
73+
"fs": {}
74+
},
75+
"region": null
76+
},
77+
{
78+
"name": "environment configured",
79+
"platform": {
80+
"env": {
81+
"AWS_REGION": "us-east-2"
82+
},
83+
"props": {},
84+
"fs": {}
85+
},
86+
"region": "us-east-2"
87+
},
88+
{
89+
"name": "jvm property is favored",
90+
"platform": {
91+
"env": {
92+
"AWS_REGION": "us-east-2"
93+
},
94+
"props": {
95+
"aws.region": "us-west-1"
96+
},
97+
"fs": {}
98+
},
99+
"region": "us-west-1"
100+
},
101+
{
102+
"name": "default profile",
103+
"platform": {
104+
"env": {
105+
"AWS_CONFIG_FILE": "config"
106+
},
107+
"props": {},
108+
"fs": {
109+
"config": "[default]\nregion = us-east-2"
110+
}
111+
},
112+
"region": "us-east-2"
113+
},
114+
{
115+
"name": "explicit profile",
116+
"platform": {
117+
"env": {
118+
"AWS_CONFIG_FILE": "config",
119+
"AWS_PROFILE": "test-profile"
120+
},
121+
"props": {},
122+
"fs": {
123+
"config": "[default]\nregion = us-east-2\n[profile test-profile]\nregion = us-west-1"
124+
}
125+
},
126+
"region": "us-west-1"
127+
}
128+
]
129+
"""
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0.
4+
*/
5+
6+
package aws.sdk.kotlin.runtime.region
7+
8+
import aws.sdk.kotlin.runtime.testing.TestPlatformProvider
9+
import aws.sdk.kotlin.runtime.testing.runSuspendTest
10+
import kotlin.test.Test
11+
import kotlin.test.assertEquals
12+
13+
class ProfileRegionProviderTest {
14+
@Test
15+
fun testSuccessDefaultProfile(): Unit = runSuspendTest {
16+
val platform = TestPlatformProvider(
17+
env = mapOf(
18+
"AWS_CONFIG_FILE" to "config"
19+
),
20+
fs = mapOf(
21+
"config" to "[default]\nregion = us-east-2"
22+
)
23+
)
24+
25+
val provider = ProfileRegionProvider(platform)
26+
assertEquals("us-east-2", provider.getRegion())
27+
}
28+
29+
@Test
30+
fun testSuccessProfileOverride(): Unit = runSuspendTest {
31+
val platform = TestPlatformProvider(
32+
env = mapOf(
33+
"AWS_CONFIG_FILE" to "config",
34+
"AWS_PROFILE" to "test-profile"
35+
),
36+
fs = mapOf(
37+
"config" to "[default]\nregion = us-east-2\n[profile test-profile]\nregion = us-west-2"
38+
)
39+
)
40+
41+
val provider = ProfileRegionProvider(platform)
42+
assertEquals("us-west-2", provider.getRegion())
43+
}
44+
45+
@Test
46+
fun testNoRegion(): Unit = runSuspendTest {
47+
val platform = TestPlatformProvider(
48+
env = mapOf(
49+
"AWS_CONFIG_FILE" to "config",
50+
),
51+
fs = mapOf(
52+
"config" to "[default]\naccess_key_id=AKID"
53+
)
54+
)
55+
56+
val provider = ProfileRegionProvider(platform)
57+
assertEquals(null, provider.getRegion())
58+
}
59+
}

aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/region/ResolveRegionTest.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,17 @@ import kotlin.test.assertEquals
1414
class ResolveRegionTest {
1515

1616
@Test
17-
fun `it resolves region for operation`() = runSuspendTest {
17+
fun testResolutionOrder() = runSuspendTest {
1818
// from context
1919
val config = object : RegionConfig {
2020
override val region: String = "us-west-2"
2121
}
2222

23+
// context has highest priority
2324
val actual = resolveRegionForOperation(ctx = ExecutionContext().apply { set(AwsClientOption.Region, "us-east-1") }, config)
2425
assertEquals("us-east-1", actual)
2526

26-
// from config
27+
// from config is next
2728
val actual2 = resolveRegionForOperation(ExecutionContext(), config)
2829
assertEquals("us-west-2", actual2)
2930
}

aws-runtime/aws-config/jvm/src/aws/sdk/kotlin/runtime/region/DefaultRegionProviderChainJVM.kt

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,19 @@
66
package aws.sdk.kotlin.runtime.region
77

88
import aws.smithy.kotlin.runtime.io.Closeable
9+
import aws.smithy.kotlin.runtime.util.PlatformProvider
910

10-
public actual class DefaultRegionProviderChain public actual constructor() :
11-
RegionProvider,
11+
public actual class DefaultRegionProviderChain public actual constructor(
12+
platformProvider: PlatformProvider
13+
) : RegionProvider,
1214
Closeable,
1315
RegionProviderChain(
14-
JvmSystemPropRegionProvider(),
15-
EnvironmentRegionProvider(),
16-
// TODO - profile
17-
ImdsRegionProvider()
16+
JvmSystemPropRegionProvider(platformProvider),
17+
EnvironmentRegionProvider(platformProvider),
18+
ProfileRegionProvider(platformProvider),
19+
ImdsRegionProvider(platformProvider = platformProvider)
1820
) {
21+
1922
override fun close() {
2023
providers.forEach { provider ->
2124
if (provider is Closeable) provider.close()

aws-runtime/aws-config/jvm/src/aws/sdk/kotlin/runtime/region/JvmSystemPropRegionProvider.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,13 @@
66
package aws.sdk.kotlin.runtime.region
77

88
import aws.sdk.kotlin.runtime.config.AwsSdkSetting
9+
import aws.smithy.kotlin.runtime.util.PropertyProvider
910

1011
/**
1112
* [RegionProvider] that checks `aws.region` system property
1213
*/
13-
internal class JvmSystemPropRegionProvider : RegionProvider {
14-
override suspend fun getRegion(): String? = System.getProperty(AwsSdkSetting.AwsRegion.jvmProperty, null)
14+
internal class JvmSystemPropRegionProvider(
15+
private val propertyProvider: PropertyProvider
16+
) : RegionProvider {
17+
override suspend fun getRegion(): String? = propertyProvider.getProperty(AwsSdkSetting.AwsRegion.jvmProperty)
1518
}

aws-runtime/aws-config/jvm/test/aws/sdk/kotlin/runtime/region/JvmSystemPropRegionProviderTest.kt

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
package aws.sdk.kotlin.runtime.region
77

8-
import aws.sdk.kotlin.runtime.config.AwsSdkSetting
8+
import aws.sdk.kotlin.runtime.testing.TestPlatformProvider
99
import aws.sdk.kotlin.runtime.testing.runSuspendTest
1010
import kotlin.test.Test
1111
import kotlin.test.assertEquals
@@ -15,11 +15,15 @@ class JvmSystemPropRegionProviderTest {
1515

1616
@Test
1717
fun testGetRegion() = runSuspendTest {
18-
val provider = JvmSystemPropRegionProvider()
18+
val provider = JvmSystemPropRegionProvider(TestPlatformProvider())
1919

2020
assertNull(provider.getRegion())
2121

22-
System.setProperty(AwsSdkSetting.AwsRegion.jvmProperty, "us-east-1")
23-
assertEquals("us-east-1", provider.getRegion())
22+
val provider2 = JvmSystemPropRegionProvider(
23+
TestPlatformProvider(
24+
props = mapOf("aws.region" to "us-east-1")
25+
)
26+
)
27+
assertEquals("us-east-1", provider2.getRegion())
2428
}
2529
}

aws-runtime/testing/common/src/aws/sdk/kotlin/runtime/testing/TestPlatformProvider.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ public class TestPlatformProvider(
2323
private val fs: Map<String, String> = emptyMap(),
2424
private val os: OperatingSystem = OperatingSystem(OsFamily.Linux, "test")
2525
) : PlatformProvider, Filesystem by Filesystem.fromMap(fs.mapValues { it.value.encodeToByteArray() }) {
26+
27+
public companion object;
28+
2629
// ensure HOME directory is set for path normalization. this is mostly for AWS config loader behavior
2730
private val env = if (env.containsKey("HOME")) env else env.toMutableMap().apply { put("HOME", "/users/test") }
2831
override val filePathSeparator: String

0 commit comments

Comments
 (0)