diff --git a/.changes/8c2a1262-90c7-49a1-8c0a-29b269ef0f19.json b/.changes/8c2a1262-90c7-49a1-8c0a-29b269ef0f19.json new file mode 100644 index 00000000000..47ba9af227f --- /dev/null +++ b/.changes/8c2a1262-90c7-49a1-8c0a-29b269ef0f19.json @@ -0,0 +1,6 @@ +{ + "id": "8c2a1262-90c7-49a1-8c0a-29b269ef0f19", + "type": "feature", + "description": "Support static stability for IMDS credentials", + "issues": ["#707"] +} \ No newline at end of file diff --git a/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/CachedCredentialsProvider.kt b/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/CachedCredentialsProvider.kt index 4d71a4497c6..5ca690dd977 100644 --- a/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/CachedCredentialsProvider.kt +++ b/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/CachedCredentialsProvider.kt @@ -58,7 +58,8 @@ public class CachedCredentialsProvider( logger.trace { "refreshing credentials cache" } val providerCreds = source.getCredentials() if (providerCreds.expiration != null) { - ExpiringValue(providerCreds, providerCreds.expiration!!) + val expiration = minOf(providerCreds.expiration!!, (clock.now() + expireCredentialsAfter)) + ExpiringValue(providerCreds, expiration) } else { val expiration = clock.now() + expireCredentialsAfter val creds = providerCreds.copy(expiration = expiration) diff --git a/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/ImdsCredentialsProvider.kt b/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/ImdsCredentialsProvider.kt index 65886c7f5d5..38ea9b05d3f 100644 --- a/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/ImdsCredentialsProvider.kt +++ b/aws-runtime/aws-config/common/src/aws/sdk/kotlin/runtime/auth/credentials/ImdsCredentialsProvider.kt @@ -16,14 +16,21 @@ import aws.smithy.kotlin.runtime.http.HttpStatusCode import aws.smithy.kotlin.runtime.io.Closeable import aws.smithy.kotlin.runtime.logging.Logger import aws.smithy.kotlin.runtime.serde.json.JsonDeserializer +import aws.smithy.kotlin.runtime.time.Clock +import aws.smithy.kotlin.runtime.time.Instant import aws.smithy.kotlin.runtime.util.Platform import aws.smithy.kotlin.runtime.util.PlatformEnvironProvider import aws.smithy.kotlin.runtime.util.asyncLazy +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlin.time.Duration.Companion.seconds private const val CREDENTIALS_BASE_PATH: String = "/latest/meta-data/iam/security-credentials" private const val CODE_ASSUME_ROLE_UNAUTHORIZED_ACCESS: String = "AssumeRoleUnauthorizedAccess" private const val PROVIDER_NAME = "IMDSv2" +internal expect class SdkIOException : Exception // FIXME move this to the proper place when we do the larger KMP Exception refactor + /** * [CredentialsProvider] that uses EC2 instance metadata service (IMDS) to provide credentials information. * This provider requires that the EC2 instance has an [instance profile](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html#ec2-instance-profile) @@ -43,8 +50,16 @@ public class ImdsCredentialsProvider( private val profileOverride: String? = null, private val client: Lazy = lazy { ImdsClient() }, private val platformProvider: PlatformEnvironProvider = Platform, + private val clock: Clock = Clock.System, ) : CredentialsProvider, Closeable { private val logger = Logger.getLogger() + private var previousCredentials: Credentials? = null + + // the time to refresh the Credentials. If set, it will take precedence over the Credentials' expiration time + private var nextRefresh: Instant? = null + + // protects previousCredentials and nextRefresh + private val mu = Mutex() private val profile = asyncLazy { if (profileOverride != null) return@asyncLazy profileOverride @@ -56,23 +71,52 @@ public class ImdsCredentialsProvider( throw CredentialsNotLoadedException("AWS EC2 metadata is explicitly disabled; credentials not loaded") } + // if we have previously served IMDS credentials and it's not time for a refresh, just return the previous credentials + mu.withLock { + previousCredentials?.run { + nextRefresh?.takeIf { clock.now() < it }?.run { + return previousCredentials!! + } + } + } + val profileName = try { profile.get() } catch (ex: Exception) { - throw CredentialsProviderException("failed to load instance profile", ex) + return useCachedCredentials(ex) ?: throw CredentialsProviderException("failed to load instance profile", ex) + } + + val payload = try { + client.value.get("$CREDENTIALS_BASE_PATH/$profileName") + } catch (ex: Exception) { + return useCachedCredentials(ex) ?: throw CredentialsProviderException("failed to load credentials", ex) } - val payload = client.value.get("$CREDENTIALS_BASE_PATH/$profileName") val deserializer = JsonDeserializer(payload.encodeToByteArray()) return when (val resp = deserializeJsonCredentials(deserializer)) { - is JsonCredentialsResponse.SessionCredentials -> Credentials( - resp.accessKeyId, - resp.secretAccessKey, - resp.sessionToken, - resp.expiration, - PROVIDER_NAME, - ) + is JsonCredentialsResponse.SessionCredentials -> { + nextRefresh = if (resp.expiration < clock.now()) { + logger.warn { + "Attempting credential expiration extension due to a credential service availability issue. " + + "A refresh of these credentials will be attempted again in " + + "${ DEFAULT_CREDENTIALS_REFRESH_SECONDS / 60 } minutes." + } + clock.now() + DEFAULT_CREDENTIALS_REFRESH_SECONDS.seconds + } else null + + val creds = Credentials( + resp.accessKeyId, + resp.secretAccessKey, + resp.sessionToken, + resp.expiration, + PROVIDER_NAME, + ) + + creds.also { + mu.withLock { previousCredentials = it } + } + } is JsonCredentialsResponse.Error -> { when (resp.code) { CODE_ASSUME_ROLE_UNAUTHORIZED_ACCESS -> throw ProviderConfigurationException("Incorrect IMDS/IAM configuration: [${resp.code}] ${resp.message}. Hint: Does this role have a trust relationship with EC2?") @@ -98,4 +142,13 @@ public class ImdsCredentialsProvider( throw ex } } + + private suspend fun useCachedCredentials(ex: Exception): Credentials? = when { + ex is SdkIOException || ex is EC2MetadataError && ex.statusCode == HttpStatusCode.InternalServerError.value -> { + mu.withLock { + previousCredentials?.apply { nextRefresh = clock.now() + DEFAULT_CREDENTIALS_REFRESH_SECONDS.seconds } + } + } + else -> null + } } diff --git a/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/auth/credentials/CachedCredentialsProviderTest.kt b/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/auth/credentials/CachedCredentialsProviderTest.kt index 858b7c9b188..b803dcb9a4a 100644 --- a/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/auth/credentials/CachedCredentialsProviderTest.kt +++ b/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/auth/credentials/CachedCredentialsProviderTest.kt @@ -83,7 +83,7 @@ class CachedCredentialsProviderTest { @Test fun testRefreshBufferWindow() = runTest { val source = TestCredentialsProvider(expiration = testExpiration) - val provider = CachedCredentialsProvider(source, clock = testClock) + val provider = CachedCredentialsProvider(source, clock = testClock, expireCredentialsAfter = 60.minutes) val creds = provider.getCredentials() val expected = Credentials("AKID", "secret", expiration = testExpiration) assertEquals(expected, creds) diff --git a/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/auth/credentials/ImdsCredentialsProviderTest.kt b/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/auth/credentials/ImdsCredentialsProviderTest.kt index dbce9a289bd..7fe547dc8e7 100644 --- a/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/auth/credentials/ImdsCredentialsProviderTest.kt +++ b/aws-runtime/aws-config/common/test/aws/sdk/kotlin/runtime/auth/credentials/ImdsCredentialsProviderTest.kt @@ -9,21 +9,34 @@ import aws.sdk.kotlin.runtime.config.AwsSdkSetting import aws.sdk.kotlin.runtime.config.imds.* import aws.sdk.kotlin.runtime.testing.TestPlatformProvider import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials +import aws.smithy.kotlin.runtime.client.ExecutionContext import aws.smithy.kotlin.runtime.http.Headers import aws.smithy.kotlin.runtime.http.HttpBody +import aws.smithy.kotlin.runtime.http.HttpMethod import aws.smithy.kotlin.runtime.http.HttpStatusCode +import aws.smithy.kotlin.runtime.http.Protocol +import aws.smithy.kotlin.runtime.http.Url import aws.smithy.kotlin.runtime.http.content.ByteArrayContent +import aws.smithy.kotlin.runtime.http.engine.HttpClientEngineBase +import aws.smithy.kotlin.runtime.http.request.HttpRequest +import aws.smithy.kotlin.runtime.http.response.HttpCall import aws.smithy.kotlin.runtime.http.response.HttpResponse import aws.smithy.kotlin.runtime.httptest.buildTestConnection import aws.smithy.kotlin.runtime.time.Instant import aws.smithy.kotlin.runtime.time.ManualClock +import aws.smithy.kotlin.runtime.time.epochMilliseconds +import aws.smithy.kotlin.runtime.time.fromEpochMilliseconds import io.kotest.matchers.string.shouldContain +import io.mockk.coVerify +import io.mockk.spyk import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertIs +import kotlin.test.assertNotEquals +import kotlin.time.Duration.Companion.minutes @OptIn(ExperimentalCoroutinesApi::class) class ImdsCredentialsProviderTest { @@ -41,6 +54,9 @@ class ImdsCredentialsProviderTest { @Test fun testSuccess() = runTest { + val testClock = ManualClock() + val expiration = Instant.fromEpochMilliseconds(testClock.now().epochMilliseconds) + val connection = buildTestConnection { expect( tokenRequest("http://169.254.169.254", DEFAULT_TOKEN_TTL_SECONDS), @@ -61,27 +77,26 @@ class ImdsCredentialsProviderTest { "AccessKeyId" : "ASIARTEST", "SecretAccessKey" : "xjtest", "Token" : "IQote///test", - "Expiration" : "2021-09-18T03:31:56Z" + "Expiration" : "$expiration" } """, ), ) } - val testClock = ManualClock() val client = ImdsClient { engine = connection clock = testClock } - val provider = ImdsCredentialsProvider(client = lazyOf(client)) + val provider = ImdsCredentialsProvider(client = lazyOf(client), clock = testClock) val actual = provider.getCredentials() val expected = Credentials( "ASIARTEST", "xjtest", "IQote///test", - Instant.fromEpochSeconds(1631935916), + expiration, "IMDSv2", ) assertEquals(expected, actual) @@ -89,6 +104,9 @@ class ImdsCredentialsProviderTest { @Test fun testSuccessProfileOverride() = runTest { + val testClock = ManualClock() + val expiration = Instant.fromEpochMilliseconds(testClock.now().epochMilliseconds) + val connection = buildTestConnection { expect( tokenRequest("http://169.254.169.254", DEFAULT_TOKEN_TTL_SECONDS), @@ -106,27 +124,26 @@ class ImdsCredentialsProviderTest { "AccessKeyId" : "ASIARTEST", "SecretAccessKey" : "xjtest", "Token" : "IQote///test", - "Expiration" : "2021-09-18T03:31:56Z" + "Expiration" : "$expiration" } """, ), ) } - val testClock = ManualClock() val client = ImdsClient { engine = connection clock = testClock } - val provider = ImdsCredentialsProvider(profileOverride = "imds-test-role", client = lazyOf(client)) + val provider = ImdsCredentialsProvider(profileOverride = "imds-test-role", client = lazyOf(client), clock = testClock) val actual = provider.getCredentials() val expected = Credentials( "ASIARTEST", "xjtest", "IQote///test", - Instant.fromEpochSeconds(1631935916), + expiration, "IMDSv2", ) assertEquals(expected, actual) @@ -148,7 +165,7 @@ class ImdsCredentialsProviderTest { clock = testClock } - val provider = ImdsCredentialsProvider(client = lazyOf(client)) + val provider = ImdsCredentialsProvider(client = lazyOf(client), clock = testClock) val ex = assertFailsWith { provider.getCredentials() @@ -194,10 +211,394 @@ class ImdsCredentialsProviderTest { clock = testClock } - val provider = ImdsCredentialsProvider(client = lazyOf(client)) + val provider = ImdsCredentialsProvider(client = lazyOf(client), clock = testClock) assertFailsWith { provider.getCredentials() }.message.shouldContain("failed to load instance profile") } + + // SDK can send a request if expired credentials are available. + // If the credentials provider can return expired credentials, that means the SDK can use them, + // because no other checks are done before using the credentials. + @Test + fun testCanReturnExpiredCredentials() = runTest { + val connection = buildTestConnection { + expect( + tokenRequest("http://169.254.169.254", DEFAULT_TOKEN_TTL_SECONDS), + tokenResponse(DEFAULT_TOKEN_TTL_SECONDS, "TOKEN_A"), + ) + expect( + imdsRequest("http://169.254.169.254/latest/meta-data/iam/security-credentials/imds-test-role", "TOKEN_A"), + imdsResponse( + """ + { + "Code" : "Success", + "LastUpdated" : "2021-09-17T20:57:08Z", + "Type" : "AWS-HMAC", + "AccessKeyId" : "ASIARTEST", + "SecretAccessKey" : "xjtest", + "Token" : "IQote///test", + "Expiration" : "2021-09-18T03:31:56Z" + } + """, + ), + ) + } + + val testClock = ManualClock() + val client = ImdsClient { + engine = connection + clock = testClock + } + + val provider = ImdsCredentialsProvider( + profileOverride = "imds-test-role", + client = lazyOf(client), + clock = testClock, + ) + + val actual = provider.getCredentials() + + val expected = Credentials( + accessKeyId = "ASIARTEST", + secretAccessKey = "xjtest", + sessionToken = "IQote///test", + expiration = Instant.fromEpochSeconds(1631935916), + providerName = "IMDSv2", + ) + + assertEquals(expected, actual) + } + + // SDK can perform 3 successive requests with expired credentials. IMDS must only be called once. + @Test + fun testSuccessiveRequestsOnlyCallIMDSOnce() = runTest { + val connection = buildTestConnection { + expect( + tokenRequest("http://169.254.169.254", DEFAULT_TOKEN_TTL_SECONDS), + tokenResponse(DEFAULT_TOKEN_TTL_SECONDS, "TOKEN_A"), + ) + expect( + imdsRequest("http://169.254.169.254/latest/meta-data/iam/security-credentials/imds-test-role", "TOKEN_A"), + imdsResponse( + """ + { + "Code" : "Success", + "LastUpdated" : "2021-09-17T20:57:08Z", + "Type" : "AWS-HMAC", + "AccessKeyId" : "ASIARTEST", + "SecretAccessKey" : "xjtest", + "Token" : "IQote///test", + "Expiration" : "2021-09-18T03:31:56Z" + } + """, + ), + ) + } + + val testClock = ManualClock() + + val client = spyk( + ImdsClient { + engine = connection + clock = testClock + }, + ) + + val provider = ImdsCredentialsProvider( + profileOverride = "imds-test-role", + client = lazyOf(client), + clock = testClock, + ) + + // call getCredentials 3 times + repeat(3) { + provider.getCredentials() + } + + // make sure ImdsClient only gets called once + coVerify(exactly = 1) { + client.get(any()) + } + } + + @Test + fun testDontRefreshUntilNextRefreshTimeHasPassed() = runTest { + val connection = buildTestConnection { + expect( + tokenRequest("http://169.254.169.254", DEFAULT_TOKEN_TTL_SECONDS), + tokenResponse(DEFAULT_TOKEN_TTL_SECONDS, "TOKEN_A"), + ) + expect( + imdsRequest("http://169.254.169.254/latest/meta-data/iam/security-credentials/imds-test-role", "TOKEN_A"), + imdsResponse( + """ + { + "Code" : "Success", + "LastUpdated" : "2021-09-17T20:57:08Z", + "Type" : "AWS-HMAC", + "AccessKeyId" : "ASIARTEST", + "SecretAccessKey" : "xjtest", + "Token" : "IQote///test", + "Expiration" : "2021-09-18T03:31:56Z" + } + """, + ), + ) + expect( + imdsRequest("http://169.254.169.254/latest/meta-data/iam/security-credentials/imds-test-role", "TOKEN_A"), + imdsResponse( + """ + { + "Code" : "Success", + "LastUpdated" : "2021-09-17T20:57:08Z", + "Type" : "AWS-HMAC", + "AccessKeyId" : "NEWCREDENTIALS", + "SecretAccessKey" : "shhh", + "Token" : "IQote///test", + "Expiration" : "2022-10-05T03:31:56Z" + } + """, + ), + ) + } + + val testClock = ManualClock() + + val client = spyk( + ImdsClient { + engine = connection + clock = testClock + }, + ) + + val provider = ImdsCredentialsProvider( + profileOverride = "imds-test-role", + client = lazyOf(client), + clock = testClock, + ) + + val first = provider.getCredentials() + testClock.advance(20.minutes) // 20 minutes later, we should try to refresh the expired credentials + val second = provider.getCredentials() + + coVerify(exactly = 2) { + client.get(any()) + } + + // make sure we did not just serve the previous credentials + assertNotEquals(first, second) + } + + @Test + fun testUsesPreviousCredentialsOnReadTimeout() = runTest { + val testClock = ManualClock() + + // this engine throws read timeout exceptions for any requests after the initial one + // (i.e allow 1 TTL token and 1 credentials request) + val readTimeoutEngine = object : HttpClientEngineBase("readTimeout") { + var successfulCallCount = 0 + + override suspend fun roundTrip(context: ExecutionContext, request: HttpRequest): HttpCall { + if (successfulCallCount >= 2) { + throw SdkIOException() + } else { + successfulCallCount += 1 + + return when (successfulCallCount) { + 1 -> HttpCall( + tokenRequest("http://169.254.169.254", DEFAULT_TOKEN_TTL_SECONDS), + tokenResponse(DEFAULT_TOKEN_TTL_SECONDS, "TOKEN_A"), + testClock.now(), + testClock.now(), + ) + else -> HttpCall( + imdsRequest("http://169.254.169.254/latest/meta-data/iam/security-credentials/imds-test-role", "TOKEN_A"), + imdsResponse( + """ + { + "Code" : "Success", + "LastUpdated" : "2021-09-17T20:57:08Z", + "Type" : "AWS-HMAC", + "AccessKeyId" : "ASIARTEST", + "SecretAccessKey" : "xjtest", + "Token" : "IQote///test", + "Expiration" : "2021-09-18T03:31:56Z" + }""", + ), + testClock.now(), + testClock.now(), + ) + } + } + } + } + + val client = ImdsClient { + engine = readTimeoutEngine + clock = testClock + } + + val previousCredentials = Credentials( + accessKeyId = "ASIARTEST", + secretAccessKey = "xjtest", + sessionToken = "IQote///test", + expiration = Instant.fromEpochSeconds(1631935916), + providerName = "IMDSv2", + ) + + val provider = ImdsCredentialsProvider( + profileOverride = "imds-test-role", + client = lazyOf(client), + clock = testClock, + ) + + // call the engine the first time to get a proper credentials response from IMDS + val credentials = provider.getCredentials() + assertEquals(credentials, previousCredentials) + + // call it again and get a read timeout exception from the engine + val newCredentials = provider.getCredentials() + + // should cause provider to return the previously-served credentials + assertEquals(newCredentials, previousCredentials) + } + + @Test + fun testThrowsExceptionOnReadTimeoutWhenMissingPreviousCredentials() = runTest { + val readTimeoutEngine = object : HttpClientEngineBase("readTimeout") { + override suspend fun roundTrip(context: ExecutionContext, request: HttpRequest): HttpCall { + throw SdkIOException() + } + } + + val testClock = ManualClock() + + val client = ImdsClient { + engine = readTimeoutEngine + clock = testClock + } + + val provider = ImdsCredentialsProvider( + profileOverride = "imds-test-role", + client = lazyOf(client), + clock = testClock, + ) + + // a read timeout should cause an exception to be thrown, because we have no previous credentials to re-serve + assertFailsWith { + provider.getCredentials() + } + } + + @Test + fun testUsesPreviousCredentialsOnServerError() = runTest { + val testClock = ManualClock() + + // this engine returns 500 errors for any requests after the initial one (i.e allow 1 TTL token and 1 credentials request) + val internalServerErrorEngine = object : HttpClientEngineBase("internalServerError") { + var successfulCallCount = 0 + + override suspend fun roundTrip(context: ExecutionContext, request: HttpRequest): HttpCall { + if (successfulCallCount >= 2) { + return HttpCall( + HttpRequest(HttpMethod.GET, Url(Protocol.HTTP, "test", Protocol.HTTP.defaultPort, "/path/foo/bar"), Headers.Empty, HttpBody.Empty), + HttpResponse(HttpStatusCode.InternalServerError, Headers.Empty, HttpBody.Empty), + testClock.now(), + testClock.now(), + ) + } else { + successfulCallCount += 1 + + return when (successfulCallCount) { + 1 -> HttpCall( + tokenRequest("http://169.254.169.254", DEFAULT_TOKEN_TTL_SECONDS), + tokenResponse(DEFAULT_TOKEN_TTL_SECONDS, "TOKEN_A"), + testClock.now(), + testClock.now(), + ) + else -> HttpCall( + imdsRequest("http://169.254.169.254/latest/meta-data/iam/security-credentials/imds-test-role", "TOKEN_A"), + imdsResponse( + """ + { + "Code" : "Success", + "LastUpdated" : "2021-09-17T20:57:08Z", + "Type" : "AWS-HMAC", + "AccessKeyId" : "ASIARTEST", + "SecretAccessKey" : "xjtest", + "Token" : "IQote///test", + "Expiration" : "2021-09-18T03:31:56Z" + }""", + ), + testClock.now(), + testClock.now(), + ) + } + } + } + } + + val client = ImdsClient { + engine = internalServerErrorEngine + clock = testClock + } + + val previousCredentials = Credentials( + accessKeyId = "ASIARTEST", + secretAccessKey = "xjtest", + sessionToken = "IQote///test", + expiration = Instant.fromEpochSeconds(1631935916), + providerName = "IMDSv2", + ) + + val provider = ImdsCredentialsProvider( + profileOverride = "imds-test-role", + client = lazyOf(client), + clock = testClock, + ) + + // call the engine the first time to get a proper credentials response from IMDS + val credentials = provider.getCredentials() + assertEquals(previousCredentials, credentials) + + // call it again and get a 500 error from the engine + val newCredentials = provider.getCredentials() + + // should cause provider to return the previously-served credentials + assertEquals(newCredentials, previousCredentials) + } + + @Test + fun testThrowsExceptionOnServerErrorWhenMissingPreviousCredentials() = runTest { + val testClock = ManualClock() + + // this engine just returns 500 errors + val internalServerErrorEngine = object : HttpClientEngineBase("internalServerError") { + override suspend fun roundTrip(context: ExecutionContext, request: HttpRequest): HttpCall { + return HttpCall( + HttpRequest(HttpMethod.GET, Url(Protocol.HTTP, "test", Protocol.HTTP.defaultPort, "/path/foo/bar"), Headers.Empty, HttpBody.Empty), + HttpResponse(HttpStatusCode.InternalServerError, Headers.Empty, HttpBody.Empty), + testClock.now(), + testClock.now(), + ) + } + } + + val client = ImdsClient { + engine = internalServerErrorEngine + clock = testClock + } + + val provider = ImdsCredentialsProvider( + profileOverride = "imds-test-role", + client = lazyOf(client), + clock = testClock, + ) + + assertFailsWith { + provider.getCredentials() + } + } } diff --git a/aws-runtime/aws-config/jvm/src/aws/sdk/kotlin/runtime/auth/credentials/SdkIOException.kt b/aws-runtime/aws-config/jvm/src/aws/sdk/kotlin/runtime/auth/credentials/SdkIOException.kt new file mode 100644 index 00000000000..f5bd01dfe8f --- /dev/null +++ b/aws-runtime/aws-config/jvm/src/aws/sdk/kotlin/runtime/auth/credentials/SdkIOException.kt @@ -0,0 +1,6 @@ +package aws.sdk.kotlin.runtime.auth.credentials + +import java.io.IOException + +@Suppress("ACTUAL_WITHOUT_EXPECT") +internal actual typealias SdkIOException = IOException