From a4734c7551694dd90a71aa5d1e4818ec9ae7b47f Mon Sep 17 00:00:00 2001 From: 0marperez <60363173+0marperez@users.noreply.github.com> Date: Mon, 16 Dec 2024 11:30:46 -0500 Subject: [PATCH 01/79] fix: create JMESPATH flattenIfPossible functions for Lists (#1169) --- .../smithy/kotlin/runtime/util/JMESPath.kt | 10 ++++ .../kotlin/runtime/util/JmesPathTest.kt | 49 +++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/JmesPathTest.kt diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/JMESPath.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/JMESPath.kt index 2a7cdd9791..9b691a79a2 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/JMESPath.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/JMESPath.kt @@ -66,6 +66,7 @@ public fun Any?.type(): String = when (this) { else -> throw Exception("Undetected type for: $this") } +// Collection `flattenIfPossible` functions @InternalApi @JvmName("noOpUnnestedCollection") public inline fun Collection.flattenIfPossible(): Collection = this @@ -73,3 +74,12 @@ public inline fun Collection.flattenIfPossible(): Collection = @InternalApi @JvmName("flattenNestedCollection") public inline fun Collection>.flattenIfPossible(): Collection = flatten() + +// List `flattenIfPossible` functions +@InternalApi +@JvmName("noOpUnnestedCollection") +public inline fun List.flattenIfPossible(): List = this + +@InternalApi +@JvmName("flattenNestedCollection") +public inline fun List>.flattenIfPossible(): List = flatten() diff --git a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/JmesPathTest.kt b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/JmesPathTest.kt new file mode 100644 index 0000000000..e867f1e8a1 --- /dev/null +++ b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/JmesPathTest.kt @@ -0,0 +1,49 @@ +package aws.smithy.kotlin.runtime.util + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class JmesPathTest { + @Test + fun flattenNestedLists() { + val nestedList = listOf( + listOf(1, 2, 3), + listOf(4, 5), + listOf(6), + ) + val flattenedList = nestedList.flattenIfPossible() + assertEquals(listOf(1, 2, 3, 4, 5, 6), flattenedList) + } + + @Test + fun flattenEmptyNestedLists() { + val nestedList = listOf( + listOf(), + listOf(), + listOf(), + ) + val flattenedList = nestedList.flattenIfPossible() + assertTrue(flattenedList.isEmpty()) + } + + @Test + fun flattenNestedEmptyAndNonEmptyNestedLists() { + val nestedList = listOf( + listOf(1, 2), + listOf(), + listOf(3, 4, 5), + ) + val flattenedList = nestedList.flattenIfPossible() + assertEquals(listOf(1, 2, 3, 4, 5), flattenedList) + } + + @Test + fun flattenList() { + val nestedList = listOf( + listOf(1, 2, 3), + ) + val flattenedList = nestedList.flattenIfPossible() + assertEquals(listOf(1, 2, 3), flattenedList) + } +} From 7911a94dd5f6ee27f2652aeb7c8031e2dce79215 Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Mon, 16 Dec 2024 16:51:49 +0000 Subject: [PATCH 02/79] chore: release 1.3.30 --- CHANGELOG.md | 2 ++ gradle.properties | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 892e2b1248..5633a30d73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [1.3.30] - 12/16/2024 + ## [1.3.29] - 12/12/2024 ## [1.3.28] - 12/03/2024 diff --git a/gradle.properties b/gradle.properties index f35cfc1d5a..377ff6666b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.3.30-SNAPSHOT +sdkVersion=1.3.30 # codegen -codegenVersion=0.33.30-SNAPSHOT \ No newline at end of file +codegenVersion=0.33.30 \ No newline at end of file From 7085c8ab0d4b34d3ddb77bbb31a2fd41c47c288c Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Mon, 16 Dec 2024 16:51:50 +0000 Subject: [PATCH 03/79] chore: bump snapshot version to 1.3.31-SNAPSHOT --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 377ff6666b..3628520e5a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.3.30 +sdkVersion=1.3.31-SNAPSHOT # codegen -codegenVersion=0.33.30 \ No newline at end of file +codegenVersion=0.33.31-SNAPSHOT \ No newline at end of file From 45750232626bbe4e279142e486be8bbbfccab33f Mon Sep 17 00:00:00 2001 From: Matas Date: Wed, 18 Dec 2024 14:20:08 -0500 Subject: [PATCH 04/79] misc: enhance support for replayable instances of `InputStream` (#1197) --- .../2de8162f-e5a0-4618-b00d-8da3cfdbc6a2.json | 8 ++++++ .../kotlin/runtime/content/ByteStreamJVM.kt | 23 +++++++++++++-- .../runtime/content/ByteStreamJVMTest.kt | 28 +++++++++++++++++++ 3 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 .changes/2de8162f-e5a0-4618-b00d-8da3cfdbc6a2.json diff --git a/.changes/2de8162f-e5a0-4618-b00d-8da3cfdbc6a2.json b/.changes/2de8162f-e5a0-4618-b00d-8da3cfdbc6a2.json new file mode 100644 index 0000000000..fb16fa5843 --- /dev/null +++ b/.changes/2de8162f-e5a0-4618-b00d-8da3cfdbc6a2.json @@ -0,0 +1,8 @@ +{ + "id": "2de8162f-e5a0-4618-b00d-8da3cfdbc6a2", + "type": "feature", + "description": "Enhance support for replayable instances of `InputStream`", + "issues": [ + "https://github.com/awslabs/aws-sdk-kotlin/issues/1473" + ] +} \ No newline at end of file diff --git a/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/content/ByteStreamJVM.kt b/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/content/ByteStreamJVM.kt index 1d6124357a..5647ac15af 100644 --- a/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/content/ByteStreamJVM.kt +++ b/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/content/ByteStreamJVM.kt @@ -114,11 +114,30 @@ public fun ByteStream.Companion.fromInputStream( * @param contentLength If specified, indicates how many bytes remain in this stream. Defaults to `null`. */ public fun InputStream.asByteStream(contentLength: Long? = null): ByteStream.SourceStream { - val source = source() + if (markSupported() && contentLength != null) { + mark(contentLength.toInt()) + } + return object : ByteStream.SourceStream() { override val contentLength: Long? = contentLength override val isOneShot: Boolean = !markSupported() - override fun readFrom(): SdkSource = source + override fun readFrom(): SdkSource { + if (markSupported() && contentLength != null) { + reset() + mark(contentLength.toInt()) + return object : SdkSource by source() { + /* + * This is a no-op close to prevent body hashing from closing the underlying InputStream, which causes + * `IOException: Stream closed` on subsequent reads. Consider making [ByteStream.ChannelStream]/[ByteStream.SourceStream] + * (or possibly even [ByteStream] itself) implement [Closeable] to better handle closing streams. + * This should allow us to clean up our usage of [ByteStream.cancel()]. + */ + override fun close() { } + } + } + + return source() + } } } diff --git a/runtime/runtime-core/jvm/test/aws/smithy/kotlin/runtime/content/ByteStreamJVMTest.kt b/runtime/runtime-core/jvm/test/aws/smithy/kotlin/runtime/content/ByteStreamJVMTest.kt index 054387b2e5..e8324fb11f 100644 --- a/runtime/runtime-core/jvm/test/aws/smithy/kotlin/runtime/content/ByteStreamJVMTest.kt +++ b/runtime/runtime-core/jvm/test/aws/smithy/kotlin/runtime/content/ByteStreamJVMTest.kt @@ -5,8 +5,11 @@ package aws.smithy.kotlin.runtime.content +import aws.smithy.kotlin.runtime.io.readToByteArray import aws.smithy.kotlin.runtime.testing.RandomTempFile import kotlinx.coroutines.test.runTest +import java.io.BufferedInputStream +import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream import java.io.InputStream import java.io.OutputStream @@ -228,6 +231,31 @@ class ByteStreamJVMTest { assertFalse(sos.closed) } + // https://github.com/awslabs/aws-sdk-kotlin/issues/1473 + @Test + fun testReplayableInputStreamAsByteStream() = runTest { + val content = "Hello, Bytes!".encodeToByteArray() + val byteArrayIns = ByteArrayInputStream(content) + val nonReplayableIns = NonReplayableInputStream(byteArrayIns) + + // buffer the non-replayable stream, making it replayable... + val bufferedIns = BufferedInputStream(nonReplayableIns) + + val byteStream = bufferedIns.asByteStream(content.size.toLong()) + + // Test that it can be read at least twice (e.g. once for hashing the body, once for transmitting the body) + assertContentEquals(content, byteStream.readFrom().use { it.readToByteArray() }) + assertContentEquals(content, byteStream.readFrom().use { it.readToByteArray() }) + } + + private class NonReplayableInputStream(val inputStream: InputStream) : InputStream() { + override fun markSupported(): Boolean = false // not replayable + + override fun read(): Int = inputStream.read() + override fun mark(readlimit: Int) = inputStream.mark(readlimit) + override fun reset() = inputStream.reset() + } + private class StatusTrackingOutputStream(val os: OutputStream) : OutputStream() { var closed: Boolean = false From b0a4bacc3626bf2c9e8cfe9607ef8d5ece03ee4e Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Wed, 18 Dec 2024 19:23:06 +0000 Subject: [PATCH 05/79] chore: release 1.3.31 --- .changes/2de8162f-e5a0-4618-b00d-8da3cfdbc6a2.json | 8 -------- CHANGELOG.md | 5 +++++ gradle.properties | 4 ++-- 3 files changed, 7 insertions(+), 10 deletions(-) delete mode 100644 .changes/2de8162f-e5a0-4618-b00d-8da3cfdbc6a2.json diff --git a/.changes/2de8162f-e5a0-4618-b00d-8da3cfdbc6a2.json b/.changes/2de8162f-e5a0-4618-b00d-8da3cfdbc6a2.json deleted file mode 100644 index fb16fa5843..0000000000 --- a/.changes/2de8162f-e5a0-4618-b00d-8da3cfdbc6a2.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "id": "2de8162f-e5a0-4618-b00d-8da3cfdbc6a2", - "type": "feature", - "description": "Enhance support for replayable instances of `InputStream`", - "issues": [ - "https://github.com/awslabs/aws-sdk-kotlin/issues/1473" - ] -} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 5633a30d73..5473a56d69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## [1.3.31] - 12/18/2024 + +### Features +* [#1473](https://github.com/awslabs/aws-sdk-kotlin/issues/1473) Enhance support for replayable instances of `InputStream` + ## [1.3.30] - 12/16/2024 ## [1.3.29] - 12/12/2024 diff --git a/gradle.properties b/gradle.properties index 3628520e5a..cf967edd0b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.3.31-SNAPSHOT +sdkVersion=1.3.31 # codegen -codegenVersion=0.33.31-SNAPSHOT \ No newline at end of file +codegenVersion=0.33.31 \ No newline at end of file From 80f453814d8b0c4e20ef7e729d47a364ca8b5753 Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Wed, 18 Dec 2024 19:23:08 +0000 Subject: [PATCH 06/79] chore: bump snapshot version to 1.3.32-SNAPSHOT --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index cf967edd0b..f902a36719 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.3.31 +sdkVersion=1.3.32-SNAPSHOT # codegen -codegenVersion=0.33.31 \ No newline at end of file +codegenVersion=0.33.32-SNAPSHOT \ No newline at end of file From e9d16a98b8030468e35a05f63c79cae2444e6759 Mon Sep 17 00:00:00 2001 From: Matas Date: Wed, 18 Dec 2024 16:28:30 -0500 Subject: [PATCH 07/79] fix: CBOR protocol test assertions / blob serialization (#1198) --- .changes/1021e75a-45f3-4f3a-820c-700d9ec6e782.json | 5 +++++ .../protocol/HttpProtocolUnitTestRequestGenerator.kt | 4 ++-- .../codegen/rendering/serde/SerializeStructGenerator.kt | 1 - .../codegen/rendering/serde/SerializeStructGeneratorTest.kt | 2 +- .../src/aws/smithy/kotlin/runtime/serde/xml/XmlSerializer.kt | 2 +- 5 files changed, 9 insertions(+), 5 deletions(-) create mode 100644 .changes/1021e75a-45f3-4f3a-820c-700d9ec6e782.json diff --git a/.changes/1021e75a-45f3-4f3a-820c-700d9ec6e782.json b/.changes/1021e75a-45f3-4f3a-820c-700d9ec6e782.json new file mode 100644 index 0000000000..0fad5e0349 --- /dev/null +++ b/.changes/1021e75a-45f3-4f3a-820c-700d9ec6e782.json @@ -0,0 +1,5 @@ +{ + "id": "1021e75a-45f3-4f3a-820c-700d9ec6e782", + "type": "bugfix", + "description": "Fix serialization of CBOR blobs" +} \ No newline at end of file diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpProtocolUnitTestRequestGenerator.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpProtocolUnitTestRequestGenerator.kt index 74bd7db782..6fcc47c3e5 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpProtocolUnitTestRequestGenerator.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpProtocolUnitTestRequestGenerator.kt @@ -117,11 +117,11 @@ open class HttpProtocolUnitTestRequestGenerator protected constructor(builder: B write("return") } write("requireNotNull(expectedBytes) { #S }", "expected application/cbor body cannot be null") - write("requireNotNull(expectedBytes) { #S }", "actual application/cbor body cannot be null") + write("requireNotNull(actualBytes) { #S }", "actual application/cbor body cannot be null") write("") write("val expectedRequest = #L(#T(expectedBytes))", inputDeserializer.name, RuntimeTypes.Serde.SerdeCbor.CborDeserializer) - write("val actualRequest = #L(#T(expectedBytes))", inputDeserializer.name, RuntimeTypes.Serde.SerdeCbor.CborDeserializer) + write("val actualRequest = #L(#T(actualBytes))", inputDeserializer.name, RuntimeTypes.Serde.SerdeCbor.CborDeserializer) write("assertEquals(expectedRequest, actualRequest)") } writer.write("") diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/serde/SerializeStructGenerator.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/serde/SerializeStructGenerator.kt index e7f2e261ef..d8810044a5 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/serde/SerializeStructGenerator.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/serde/SerializeStructGenerator.kt @@ -647,7 +647,6 @@ open class SerializeStructGenerator( val target = member.targetOrSelf(ctx.model) val encoded = when { - target.type == ShapeType.BLOB -> writer.format("#L.#T()", identifier, RuntimeTypes.Core.Text.Encoding.encodeBase64String) target.type == ShapeType.TIMESTAMP -> { writer.addImport(RuntimeTypes.Core.TimestampFormat) val tsFormat = member diff --git a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/serde/SerializeStructGeneratorTest.kt b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/serde/SerializeStructGeneratorTest.kt index 40d9c9db7d..347da007cb 100644 --- a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/serde/SerializeStructGeneratorTest.kt +++ b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/serde/SerializeStructGeneratorTest.kt @@ -1822,7 +1822,7 @@ class SerializeStructGeneratorTest { val expected = """ serializer.serializeStruct(OBJ_DESCRIPTOR) { - input.fooBlob?.let { field(FOOBLOB_DESCRIPTOR, it.encodeBase64String()) } + input.fooBlob?.let { field(FOOBLOB_DESCRIPTOR, it) } } """.trimIndent() diff --git a/runtime/serde/serde-xml/common/src/aws/smithy/kotlin/runtime/serde/xml/XmlSerializer.kt b/runtime/serde/serde-xml/common/src/aws/smithy/kotlin/runtime/serde/xml/XmlSerializer.kt index 35618cb49e..d1356f848c 100644 --- a/runtime/serde/serde-xml/common/src/aws/smithy/kotlin/runtime/serde/xml/XmlSerializer.kt +++ b/runtime/serde/serde-xml/common/src/aws/smithy/kotlin/runtime/serde/xml/XmlSerializer.kt @@ -134,7 +134,7 @@ public class XmlSerializer(private val xmlWriter: XmlStreamWriter = xmlStreamWri field(descriptor, value.format(format)) override fun field(descriptor: SdkFieldDescriptor, value: ByteArray): Unit = - field(descriptor, value) + field(descriptor, value.encodeBase64String()) override fun field(descriptor: SdkFieldDescriptor, value: Document?): Unit = throw SerializationException( "cannot serialize field ${descriptor.serialName}; Document type is not supported by xml encoding", From 48849a1208fe0bb2c42c06c49b54b9c419104378 Mon Sep 17 00:00:00 2001 From: Matas Date: Mon, 6 Jan 2025 16:34:11 -0500 Subject: [PATCH 08/79] fix: correctly serialize subset of shape's members when configured (#1199) --- .../core/QueryHttpBindingProtocolGenerator.kt | 2 +- .../codegen/aws/protocols/RpcV2CborTest.kt | 17 +++++++++++++++++ .../rendering/serde/CborSerializerGenerator.kt | 2 +- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/core/QueryHttpBindingProtocolGenerator.kt b/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/core/QueryHttpBindingProtocolGenerator.kt index 62f8f7ebe1..db97349f84 100644 --- a/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/core/QueryHttpBindingProtocolGenerator.kt +++ b/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/core/QueryHttpBindingProtocolGenerator.kt @@ -145,7 +145,7 @@ abstract class AbstractQueryFormUrlSerializerGenerator( return shape.documentSerializer(ctx.settings, symbol, members) { writer -> writer.openBlock("internal fun #identifier.name:L(serializer: #T, input: #T) {", RuntimeTypes.Serde.Serializer, symbol) .call { - renderSerializerBody(ctx, shape, shape.members().toList(), writer) + renderSerializerBody(ctx, shape, members.toList(), writer) } .closeBlock("}") } diff --git a/codegen/smithy-aws-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/RpcV2CborTest.kt b/codegen/smithy-aws-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/RpcV2CborTest.kt index 17a7c0f3d1..0114185004 100644 --- a/codegen/smithy-aws-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/RpcV2CborTest.kt +++ b/codegen/smithy-aws-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/RpcV2CborTest.kt @@ -4,6 +4,7 @@ */ package software.amazon.smithy.kotlin.codegen.aws.protocols +import io.kotest.matchers.string.shouldNotContain import software.amazon.smithy.kotlin.codegen.test.* import kotlin.test.Test @@ -145,4 +146,20 @@ class RpcV2CborTest { val serializeBody = serializer.lines(" override suspend fun serialize(context: ExecutionContext, input: PutFooStreamingRequest): HttpRequestBuilder {", "}") serializeBody.shouldContainOnlyOnceWithDiff("""builder.headers.setMissing("Content-Type", "application/vnd.amazon.eventstream")""") } + + @Test + fun testEventStreamInitialRequestDoesNotSerializeStreamMember() { + val ctx = model.newTestContext("CborExample") + + val generator = RpcV2Cbor() + generator.generateProtocolClient(ctx.generationCtx) + + ctx.generationCtx.delegator.finalize() + ctx.generationCtx.delegator.flushWriters() + + val documentSerializer = ctx.manifest.expectFileString("/src/main/kotlin/com/test/serde/PutFooStreamingRequestDocumentSerializer.kt") + + val serializeBody = documentSerializer.lines(" serializer.serializeStruct(OBJ_DESCRIPTOR) {", "}") + serializeBody.shouldNotContain("input.messages") // `messages` is the stream member and should not be serialized in the initial request + } } diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/serde/CborSerializerGenerator.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/serde/CborSerializerGenerator.kt index 1fdb7850a7..545a484a90 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/serde/CborSerializerGenerator.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/serde/CborSerializerGenerator.kt @@ -108,7 +108,7 @@ class CborSerializerGenerator( val symbol = ctx.symbolProvider.toSymbol(shape) return shape.documentSerializer(ctx.settings, symbol, members) { writer -> writer.withBlock("internal fun #identifier.name:L(serializer: #T, input: #T) {", "}", RuntimeTypes.Serde.Serializer, symbol) { - call { renderSerializerBody(ctx, shape, shape.members().toList(), writer) } + call { renderSerializerBody(ctx, shape, members.toList(), writer) } } } } From ded3a4bb03060f28f3ce085fed3a9627f7297ae5 Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Mon, 6 Jan 2025 21:45:24 +0000 Subject: [PATCH 09/79] chore: release 1.3.32 --- .changes/1021e75a-45f3-4f3a-820c-700d9ec6e782.json | 5 ----- CHANGELOG.md | 5 +++++ gradle.properties | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) delete mode 100644 .changes/1021e75a-45f3-4f3a-820c-700d9ec6e782.json diff --git a/.changes/1021e75a-45f3-4f3a-820c-700d9ec6e782.json b/.changes/1021e75a-45f3-4f3a-820c-700d9ec6e782.json deleted file mode 100644 index 0fad5e0349..0000000000 --- a/.changes/1021e75a-45f3-4f3a-820c-700d9ec6e782.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "id": "1021e75a-45f3-4f3a-820c-700d9ec6e782", - "type": "bugfix", - "description": "Fix serialization of CBOR blobs" -} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 5473a56d69..a52fe756c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## [1.3.32] - 01/06/2025 + +### Fixes +* Fix serialization of CBOR blobs + ## [1.3.31] - 12/18/2024 ### Features diff --git a/gradle.properties b/gradle.properties index f902a36719..eda623b285 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.3.32-SNAPSHOT +sdkVersion=1.3.32 # codegen -codegenVersion=0.33.32-SNAPSHOT \ No newline at end of file +codegenVersion=0.33.32 \ No newline at end of file From d97e8ba056d5f0c9869226854638dd486e06f1fa Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Mon, 6 Jan 2025 21:45:25 +0000 Subject: [PATCH 10/79] chore: bump snapshot version to 1.3.33-SNAPSHOT --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index eda623b285..b49ffff27f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.3.32 +sdkVersion=1.3.33-SNAPSHOT # codegen -codegenVersion=0.33.32 \ No newline at end of file +codegenVersion=0.33.33-SNAPSHOT \ No newline at end of file From 78f17f3dcb809e297669c9733e2f440da33985f2 Mon Sep 17 00:00:00 2001 From: Ian Botsford <83236726+ianbotsf@users.noreply.github.com> Date: Wed, 8 Jan 2025 09:23:33 -0800 Subject: [PATCH 11/79] chore: add *-main to branch workflows (#1209) --- .github/workflows/api-compat-verification.yml | 4 +++- .github/workflows/artifact-size-metrics.yml | 4 +++- .github/workflows/changelog-verification.yml | 4 +++- .github/workflows/continuous-integration.yml | 4 +++- .github/workflows/dependabot.yml | 4 +++- .github/workflows/kat-transform.yml | 4 +++- .github/workflows/lint.yml | 5 ++++- 7 files changed, 22 insertions(+), 7 deletions(-) diff --git a/.github/workflows/api-compat-verification.yml b/.github/workflows/api-compat-verification.yml index 26c7ef3901..2d4a22452a 100644 --- a/.github/workflows/api-compat-verification.yml +++ b/.github/workflows/api-compat-verification.yml @@ -3,7 +3,9 @@ name: API compatibility verification on: pull_request: types: [ opened, synchronize, reopened, labeled, unlabeled ] - branches: [ main ] + branches: + - main + - '*-main' jobs: api-compat-verification: diff --git a/.github/workflows/artifact-size-metrics.yml b/.github/workflows/artifact-size-metrics.yml index bd1c521e16..0591b2b6aa 100644 --- a/.github/workflows/artifact-size-metrics.yml +++ b/.github/workflows/artifact-size-metrics.yml @@ -2,7 +2,9 @@ name: Artifact Size Metrics on: pull_request: types: [ opened, synchronize, reopened, labeled, unlabeled ] - branches: [ main ] + branches: + - main + - '*-main' release: types: [published] diff --git a/.github/workflows/changelog-verification.yml b/.github/workflows/changelog-verification.yml index 547b155abf..ed6efef911 100644 --- a/.github/workflows/changelog-verification.yml +++ b/.github/workflows/changelog-verification.yml @@ -3,7 +3,9 @@ name: Changelog verification on: pull_request: types: [ opened, synchronize, reopened, labeled, unlabeled ] - branches: [ main ] + branches: + - main + - '*-main' jobs: changelog-verification: diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 9b0dc1c880..599293cc06 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -2,7 +2,9 @@ name: CI on: push: - branches: [ main ] + branches: + - main + - '*-main' pull_request: workflow_dispatch: diff --git a/.github/workflows/dependabot.yml b/.github/workflows/dependabot.yml index 69fb0c9885..6588de3857 100644 --- a/.github/workflows/dependabot.yml +++ b/.github/workflows/dependabot.yml @@ -2,7 +2,9 @@ name: Dependabot Dependency Submission on: push: - branches: [ main ] + branches: + - main + - '*-main' permissions: contents: write diff --git a/.github/workflows/kat-transform.yml b/.github/workflows/kat-transform.yml index 859a904bc3..938e936ef9 100644 --- a/.github/workflows/kat-transform.yml +++ b/.github/workflows/kat-transform.yml @@ -3,7 +3,9 @@ name: Kat Transform on: pull_request: types: [ opened, synchronize, reopened, labeled, unlabeled ] - branches: [ main ] + branches: + - main + - '*-main' # Allow one instance of this workflow per pull request, and cancel older runs when new changes are pushed concurrency: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 6693ed2aae..e95b98aded 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -5,8 +5,11 @@ on: branches: - '**' - '!main' + - '!*-main' pull_request: - branches: [ main ] + branches: + - main + - '*-main' workflow_dispatch: env: From d06ee0a604dd0750421c6d7dfe7c47dfbcd062bf Mon Sep 17 00:00:00 2001 From: Ian Botsford <83236726+ianbotsf@users.noreply.github.com> Date: Wed, 8 Jan 2025 09:23:43 -0800 Subject: [PATCH 12/79] chore: add .kotlin/ to .gitignore (#1208) --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 225fe6070b..99ff20f122 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ out/ # Compiled class file *.class *.klib +.kotlin/ # Log file *.log From d2e7c918e7fc9ee9892fd6cd7d852db4d5b2f2db Mon Sep 17 00:00:00 2001 From: 0marperez <60363173+0marperez@users.noreply.github.com> Date: Fri, 10 Jan 2025 09:16:58 -0500 Subject: [PATCH 13/79] chore: smithy version bump (#1213) --- .../model/error-correction-tests.smithy | 13 ++++++++++--- .../core/AwsHttpBindingProtocolGenerator.kt | 10 +++++++++- gradle/libs.versions.toml | 2 +- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/codegen/protocol-tests/model/error-correction-tests.smithy b/codegen/protocol-tests/model/error-correction-tests.smithy index 74e614bdc5..201ffc877f 100644 --- a/codegen/protocol-tests/model/error-correction-tests.smithy +++ b/codegen/protocol-tests/model/error-correction-tests.smithy @@ -37,7 +37,12 @@ operation SayHello { output: TestOutputDocument, errors: [Error] } @http(method: "POST", uri: "/") operation SayHelloXml { output: TestOutput, errors: [Error] } -structure TestOutputDocument with [TestStruct] { innerField: Nested, @required document: Document } +structure TestOutputDocument with [TestStruct] { + innerField: Nested, + // FIXME: This trait fails smithy validator + // @required + document: Document +} structure TestOutput with [TestStruct] { innerField: Nested } @mixin @@ -60,7 +65,8 @@ structure TestStruct { @required nestedListValue: NestedList - @required + // FIXME: This trait fails smithy validator + // @required nested: Nested @required @@ -91,7 +97,8 @@ union MyUnion { } structure Nested { - @required + // FIXME: This trait fails smithy validator + // @required a: String } diff --git a/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/core/AwsHttpBindingProtocolGenerator.kt b/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/core/AwsHttpBindingProtocolGenerator.kt index 864c2a6c3c..6e5834f7d1 100644 --- a/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/core/AwsHttpBindingProtocolGenerator.kt +++ b/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/core/AwsHttpBindingProtocolGenerator.kt @@ -39,7 +39,15 @@ abstract class AwsHttpBindingProtocolGenerator : HttpBindingProtocolGenerator() // The following can be used to generate only a specific test by name. // val targetedTest = TestMemberDelta(setOf("RestJsonComplexErrorWithNoMessage"), TestContainmentMode.RUN_TESTS) - val ignoredTests = TestMemberDelta(setOf()) + val ignoredTests = TestMemberDelta( + setOf( + "AwsJson10ClientErrorCorrectsWithDefaultValuesWhenServerFailsToSerializeRequiredValues", + "RestJsonNullAndEmptyHeaders", + "NullAndEmptyHeaders", + "RpcV2CborClientPopulatesDefaultsValuesWhenMissingInResponse", + "RpcV2CborClientPopulatesDefaultValuesInInput", + ), + ) val requestTestBuilder = HttpProtocolUnitTestRequestGenerator.Builder() val responseTestBuilder = HttpProtocolUnitTestResponseGenerator.Builder() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0683bfc2e1..14232f4241 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,7 +17,7 @@ crt-kotlin-version = "0.8.10" micrometer-version = "1.13.6" # codegen -smithy-version = "1.51.0" +smithy-version = "1.53.0" smithy-gradle-version = "0.9.0" # testing From 5a2df8f171250e150c685c9389f80e2a6b0778eb Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Fri, 10 Jan 2025 14:36:46 +0000 Subject: [PATCH 14/79] chore: release 1.3.33 --- CHANGELOG.md | 2 ++ gradle.properties | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a52fe756c1..2c327d4f40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [1.3.33] - 01/10/2025 + ## [1.3.32] - 01/06/2025 ### Fixes diff --git a/gradle.properties b/gradle.properties index b49ffff27f..b6869a974b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.3.33-SNAPSHOT +sdkVersion=1.3.33 # codegen -codegenVersion=0.33.33-SNAPSHOT \ No newline at end of file +codegenVersion=0.33.33 \ No newline at end of file From 003633bad25b5b3e75112da2d1de5f2eeffd1471 Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Fri, 10 Jan 2025 14:36:47 +0000 Subject: [PATCH 15/79] chore: bump snapshot version to 1.3.34-SNAPSHOT --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index b6869a974b..6c237cf8df 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.3.33 +sdkVersion=1.3.34-SNAPSHOT # codegen -codegenVersion=0.33.33 \ No newline at end of file +codegenVersion=0.33.34-SNAPSHOT \ No newline at end of file From 5f5ec8f16b4db571bb9c83f520e26f9927d8ecf2 Mon Sep 17 00:00:00 2001 From: Matas Date: Fri, 10 Jan 2025 11:13:29 -0500 Subject: [PATCH 16/79] feat: add `AuthTokenGenerator` (#1212) --- .../api/aws-signing-common.api | 10 +++ .../auth/awssigning/AuthTokenGenerator.kt | 44 ++++++++++++ .../auth/awssigning/AuthTokenGeneratorTest.kt | 71 +++++++++++++++++++ 3 files changed, 125 insertions(+) create mode 100644 runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGenerator.kt create mode 100644 runtime/auth/aws-signing-common/common/test/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGeneratorTest.kt diff --git a/runtime/auth/aws-signing-common/api/aws-signing-common.api b/runtime/auth/aws-signing-common/api/aws-signing-common.api index 0c96486679..d128b0bd6f 100644 --- a/runtime/auth/aws-signing-common/api/aws-signing-common.api +++ b/runtime/auth/aws-signing-common/api/aws-signing-common.api @@ -1,3 +1,13 @@ +public final class aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGenerator { + public fun (Ljava/lang/String;Laws/smithy/kotlin/runtime/auth/awscredentials/CredentialsProvider;Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigner;Laws/smithy/kotlin/runtime/time/Clock;)V + public synthetic fun (Ljava/lang/String;Laws/smithy/kotlin/runtime/auth/awscredentials/CredentialsProvider;Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigner;Laws/smithy/kotlin/runtime/time/Clock;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun generateAuthToken-exY8QGI (Laws/smithy/kotlin/runtime/net/url/Url;Ljava/lang/String;JLkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun getClock ()Laws/smithy/kotlin/runtime/time/Clock; + public final fun getCredentialsProvider ()Laws/smithy/kotlin/runtime/auth/awscredentials/CredentialsProvider; + public final fun getService ()Ljava/lang/String; + public final fun getSigner ()Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigner; +} + public final class aws/smithy/kotlin/runtime/auth/awssigning/AwsChunkedByteReadChannel : aws/smithy/kotlin/runtime/io/SdkByteReadChannel { public fun (Laws/smithy/kotlin/runtime/io/SdkByteReadChannel;Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigner;Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigningConfig;[BLaws/smithy/kotlin/runtime/http/DeferredHeaders;)V public synthetic fun (Laws/smithy/kotlin/runtime/io/SdkByteReadChannel;Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigner;Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigningConfig;[BLaws/smithy/kotlin/runtime/http/DeferredHeaders;ILkotlin/jvm/internal/DefaultConstructorMarker;)V diff --git a/runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGenerator.kt b/runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGenerator.kt new file mode 100644 index 0000000000..e92ac1f72c --- /dev/null +++ b/runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGenerator.kt @@ -0,0 +1,44 @@ +/* +* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +* SPDX-License-Identifier: Apache-2.0 +*/ +package aws.smithy.kotlin.runtime.auth.awssigning + +import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProvider +import aws.smithy.kotlin.runtime.auth.awssigning.AwsSigningConfig.Companion.invoke +import aws.smithy.kotlin.runtime.http.HttpMethod +import aws.smithy.kotlin.runtime.http.request.HttpRequest +import aws.smithy.kotlin.runtime.net.url.Url +import aws.smithy.kotlin.runtime.time.Clock +import kotlin.time.Duration + +/** + * Generates an authentication token, which is a SigV4-signed URL with the HTTP scheme removed. + * @param service The name of the service the token is being generated for + * @param credentialsProvider The [CredentialsProvider] which will provide credentials to use when generating the auth token + * @param signer The [AwsSigner] implementation to use when creating the authentication token + * @param clock The [Clock] implementation to use + */ +public class AuthTokenGenerator( + public val service: String, + public val credentialsProvider: CredentialsProvider, + public val signer: AwsSigner, + public val clock: Clock = Clock.System, +) { + private fun Url.trimScheme(): String = toString().removePrefix(scheme.protocolName).removePrefix("://") + + public suspend fun generateAuthToken(endpoint: Url, region: String, expiration: Duration): String { + val req = HttpRequest(HttpMethod.GET, endpoint) + + val config = AwsSigningConfig { + credentials = credentialsProvider.resolve() + this.region = region + service = this@AuthTokenGenerator.service + signingDate = clock.now() + expiresAfter = expiration + signatureType = AwsSignatureType.HTTP_REQUEST_VIA_QUERY_PARAMS + } + + return signer.sign(req, config).output.url.trimScheme() + } +} diff --git a/runtime/auth/aws-signing-common/common/test/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGeneratorTest.kt b/runtime/auth/aws-signing-common/common/test/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGeneratorTest.kt new file mode 100644 index 0000000000..87fec1a74e --- /dev/null +++ b/runtime/auth/aws-signing-common/common/test/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGeneratorTest.kt @@ -0,0 +1,71 @@ +/* +* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +* SPDX-License-Identifier: Apache-2.0 +*/ +package aws.smithy.kotlin.runtime.auth.awssigning + +import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials +import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProvider +import aws.smithy.kotlin.runtime.collections.Attributes +import aws.smithy.kotlin.runtime.http.Headers +import aws.smithy.kotlin.runtime.http.request.HttpRequest +import aws.smithy.kotlin.runtime.http.request.toBuilder +import aws.smithy.kotlin.runtime.net.Host +import aws.smithy.kotlin.runtime.net.url.Url +import aws.smithy.kotlin.runtime.time.Instant +import aws.smithy.kotlin.runtime.time.ManualClock +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertContains +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.seconds +import kotlin.time.DurationUnit + +class AuthTokenGeneratorTest { + @Test + fun testGenerateAuthToken() = runTest { + val credentials = Credentials("akid", "secret") + + val credentialsProvider = object : CredentialsProvider { + var credentialsResolved = false + override suspend fun resolve(attributes: Attributes): Credentials { + credentialsResolved = true + return credentials + } + } + + val clock = ManualClock(Instant.fromEpochSeconds(0)) + + val generator = AuthTokenGenerator("foo", credentialsProvider, TEST_SIGNER, clock = clock) + + val endpoint = Url { host = Host.parse("foo.bar.us-east-1.baz") } + val token = generator.generateAuthToken(endpoint, "us-east-1", 333.seconds) + + assertContains(token, "foo.bar.us-east-1.baz") + assertContains(token, "X-Amz-Credential=signature") // test custom signer was invoked + assertContains(token, "X-Amz-Expires=333") // expiration + assertContains(token, "X-Amz-SigningDate=0") // clock + + assertTrue(credentialsProvider.credentialsResolved) + } +} + +private val TEST_SIGNER = object : AwsSigner { + override suspend fun sign( + request: HttpRequest, + config: AwsSigningConfig, + ): AwsSigningResult { + val builder = request.toBuilder() + builder.url.parameters.decodedParameters.apply { + put("X-Amz-Credential", "signature") + put("X-Amz-Expires", (config.expiresAfter?.toLong(DurationUnit.SECONDS) ?: 900).toString()) + put("X-Amz-SigningDate", config.signingDate.epochSeconds.toString()) + } + + return AwsSigningResult(builder.build(), "signature".encodeToByteArray()) + } + + override suspend fun signChunk(chunkBody: ByteArray, prevSignature: ByteArray, config: AwsSigningConfig): AwsSigningResult = throw IllegalStateException("signChunk unexpectedly invoked") + + override suspend fun signChunkTrailer(trailingHeaders: Headers, prevSignature: ByteArray, config: AwsSigningConfig): AwsSigningResult = throw IllegalStateException("signChunkTrailer unexpectedly invoked") +} From 3fe1b5e7763bd4ed386f30d3f4bfcbb0ad4aa352 Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Fri, 10 Jan 2025 16:45:39 +0000 Subject: [PATCH 17/79] chore: release 1.3.34 --- CHANGELOG.md | 2 ++ gradle.properties | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c327d4f40..c1758b4332 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [1.3.34] - 01/10/2025 + ## [1.3.33] - 01/10/2025 ## [1.3.32] - 01/06/2025 diff --git a/gradle.properties b/gradle.properties index 6c237cf8df..193406ed69 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.3.34-SNAPSHOT +sdkVersion=1.3.34 # codegen -codegenVersion=0.33.34-SNAPSHOT \ No newline at end of file +codegenVersion=0.33.34 \ No newline at end of file From 0bba3084ebb3160abe2a21fc86f4223c7bf37c23 Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Fri, 10 Jan 2025 16:45:40 +0000 Subject: [PATCH 18/79] chore: bump snapshot version to 1.3.35-SNAPSHOT --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 193406ed69..d76819491b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.3.34 +sdkVersion=1.3.35-SNAPSHOT # codegen -codegenVersion=0.33.34 \ No newline at end of file +codegenVersion=0.33.35-SNAPSHOT \ No newline at end of file From 52e4439088b388c5f4d8c9b1e6fec6588d37a1a4 Mon Sep 17 00:00:00 2001 From: 0marperez <60363173+0marperez@users.noreply.github.com> Date: Wed, 15 Jan 2025 12:33:57 -0500 Subject: [PATCH 19/79] misc: merge v1.4 into main (#1218) --- .brazil.json | 2 +- .../0857b6f0-0444-479f-be6b-06ef71d482a0.json | 9 ++++ .../1a68d0b7-00e7-45c0-88f6-95e5c39c9c61.json | 6 +++ .../3456d00f-1b29-4d88-ab02-045db1b1ebce.json | 8 +++ .../kotlin/codegen/core/KotlinDependency.kt | 2 +- .../rendering/waiters/WaiterGenerator.kt | 15 +++--- .../waiters/ServiceWaitersGeneratorTest.kt | 6 +-- .../rendering/waiters/WaiterGeneratorTest.kt | 4 +- gradle/libs.versions.toml | 17 ++++--- .../tests/SigningSuiteTestBaseJVM.kt | 3 +- runtime/runtime-core/api/runtime-core.api | 15 +++++- .../aws/smithy/kotlin/runtime/Exceptions.kt | 49 +++++++++++++++++- .../kotlin/runtime/collections/Attributes.kt | 11 ++++ .../kotlin/runtime/retries/Exceptions.kt | 23 ++++++++- .../runtime/retries/StandardRetryStrategy.kt | 30 +++++++++-- .../retries/delay/StandardRetryTokenBucket.kt | 2 +- .../smithy/kotlin/runtime/ExceptionsTest.kt | 34 +++++++++++++ .../impl/StandardRetryIntegrationTest.kt | 50 +++++++++++++++---- .../smithy/kotlin/codegen/util/TestUtils.kt | 7 +-- 19 files changed, 250 insertions(+), 43 deletions(-) create mode 100644 .changes/0857b6f0-0444-479f-be6b-06ef71d482a0.json create mode 100644 .changes/1a68d0b7-00e7-45c0-88f6-95e5c39c9c61.json create mode 100644 .changes/3456d00f-1b29-4d88-ab02-045db1b1ebce.json diff --git a/.brazil.json b/.brazil.json index cb2e43713a..5a1d830abd 100644 --- a/.brazil.json +++ b/.brazil.json @@ -1,6 +1,6 @@ { "dependencies": { - "org.jetbrains.kotlin:kotlin-stdlib:2.0.*": "KotlinStdlib-2.x", + "org.jetbrains.kotlin:kotlin-stdlib:2.*": "KotlinStdlib-2.x", "org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.*": "KotlinxCoroutinesCoreJvm-1.x", "com.squareup.okhttp3:okhttp-coroutines:5.*": "OkHttp3Coroutines-5.x", diff --git a/.changes/0857b6f0-0444-479f-be6b-06ef71d482a0.json b/.changes/0857b6f0-0444-479f-be6b-06ef71d482a0.json new file mode 100644 index 0000000000..d89da89103 --- /dev/null +++ b/.changes/0857b6f0-0444-479f-be6b-06ef71d482a0.json @@ -0,0 +1,9 @@ +{ + "id": "0857b6f0-0444-479f-be6b-06ef71d482a0", + "type": "feature", + "description": "⚠️ **IMPORTANT**: Add `retryStrategy` configuration option for waiters", + "issues": [ + "https://github.com/awslabs/aws-sdk-kotlin/issues/1431" + ], + "requiresMinorVersionBump": true +} \ No newline at end of file diff --git a/.changes/1a68d0b7-00e7-45c0-88f6-95e5c39c9c61.json b/.changes/1a68d0b7-00e7-45c0-88f6-95e5c39c9c61.json new file mode 100644 index 0000000000..10ef502204 --- /dev/null +++ b/.changes/1a68d0b7-00e7-45c0-88f6-95e5c39c9c61.json @@ -0,0 +1,6 @@ +{ + "id": "1a68d0b7-00e7-45c0-88f6-95e5c39c9c61", + "type": "misc", + "description": "⚠️ **IMPORTANT**: Upgrade to Kotlin 2.1.0", + "requiresMinorVersionBump": true +} \ No newline at end of file diff --git a/.changes/3456d00f-1b29-4d88-ab02-045db1b1ebce.json b/.changes/3456d00f-1b29-4d88-ab02-045db1b1ebce.json new file mode 100644 index 0000000000..767e48a02e --- /dev/null +++ b/.changes/3456d00f-1b29-4d88-ab02-045db1b1ebce.json @@ -0,0 +1,8 @@ +{ + "id": "3456d00f-1b29-4d88-ab02-045db1b1ebce", + "type": "bugfix", + "description": "Include more information when retry strategy halts early due to token bucket capacity errors", + "issues": [ + "awslabs/aws-sdk-kotlin#1321" + ] +} \ No newline at end of file diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinDependency.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinDependency.kt index 17faf0ad48..b575efae60 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinDependency.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinDependency.kt @@ -37,7 +37,7 @@ private fun getDefaultRuntimeVersion(): String { // publishing info const val RUNTIME_GROUP: String = "aws.smithy.kotlin" val RUNTIME_VERSION: String = System.getProperty("smithy.kotlin.codegen.clientRuntimeVersion", getDefaultRuntimeVersion()) -val KOTLIN_COMPILER_VERSION: String = System.getProperty("smithy.kotlin.codegen.kotlinCompilerVersion", "2.0.10") +val KOTLIN_COMPILER_VERSION: String = System.getProperty("smithy.kotlin.codegen.kotlinCompilerVersion", "2.1.0") enum class SourceSet { CommonMain, diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/waiters/WaiterGenerator.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/waiters/WaiterGenerator.kt index 552a352a1a..f150cbc3d2 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/waiters/WaiterGenerator.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/waiters/WaiterGenerator.kt @@ -17,7 +17,7 @@ import java.text.DecimalFormatSymbols * Renders the top-level retry strategy for a waiter. */ private fun KotlinWriter.renderRetryStrategy(wi: WaiterInfo, asValName: String) { - withBlock("val #L = #T {", "}", asValName, RuntimeTypes.Core.Retries.StandardRetryStrategy) { + withBlock("val #L = retryStrategy ?: #T {", "}", asValName, RuntimeTypes.Core.Retries.StandardRetryStrategy) { write("maxAttempts = 20") write("tokenBucket = #T", RuntimeTypes.Core.Retries.Delay.InfiniteTokenBucket) withBlock("delayProvider {", "}") { @@ -35,18 +35,21 @@ private fun KotlinWriter.renderRetryStrategy(wi: WaiterInfo, asValName: String) internal fun KotlinWriter.renderWaiter(wi: WaiterInfo) { write("") wi.waiter.documentation.ifPresent(::dokka) - val inputParameter = if (wi.input.hasAllOptionalMembers) { - format("request: #1T = #1T { }", wi.inputSymbol) + + val requestType = if (wi.input.hasAllOptionalMembers) { + format("#1T = #1T { }", wi.inputSymbol) } else { - format("request: #T", wi.inputSymbol) + format("#T", wi.inputSymbol) } + withBlock( - "#L suspend fun #T.#L(#L): #T<#T> {", + "#L suspend fun #T.#L(request: #L, retryStrategy: #T? = null): #T<#T> {", "}", wi.ctx.settings.api.visibility, wi.serviceSymbol, wi.methodName, - inputParameter, + requestType, + RuntimeTypes.Core.Retries.RetryStrategy, RuntimeTypes.Core.Retries.Outcome, wi.outputSymbol, ) { diff --git a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/waiters/ServiceWaitersGeneratorTest.kt b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/waiters/ServiceWaitersGeneratorTest.kt index c83c29661d..49ee90f484 100644 --- a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/waiters/ServiceWaitersGeneratorTest.kt +++ b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/waiters/ServiceWaitersGeneratorTest.kt @@ -43,7 +43,7 @@ class ServiceWaitersGeneratorTest { /** * Wait until a foo exists with optional input */ - public suspend fun TestClient.waitUntilFooOptionalExists(request: DescribeFooOptionalRequest = DescribeFooOptionalRequest { }): Outcome { + public suspend fun TestClient.waitUntilFooOptionalExists(request: DescribeFooOptionalRequest = DescribeFooOptionalRequest { }, retryStrategy: RetryStrategy? = null): Outcome { """.trimIndent() val methodFooter = """ val policy = AcceptorRetryPolicy(request, acceptors) @@ -59,7 +59,7 @@ class ServiceWaitersGeneratorTest { /** * Wait until a foo exists with required input */ - public suspend fun TestClient.waitUntilFooRequiredExists(request: DescribeFooRequiredRequest): Outcome { + public suspend fun TestClient.waitUntilFooRequiredExists(request: DescribeFooRequiredRequest, retryStrategy: RetryStrategy? = null): Outcome { """.trimIndent() listOf( generateService("simple-service-with-operation-waiter.smithy"), @@ -105,7 +105,7 @@ class ServiceWaitersGeneratorTest { @Test fun testRetryStrategy() { val expected = """ - val strategy = StandardRetryStrategy { + val strategy = retryStrategy ?: StandardRetryStrategy { maxAttempts = 20 tokenBucket = InfiniteTokenBucket delayProvider { diff --git a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/waiters/WaiterGeneratorTest.kt b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/waiters/WaiterGeneratorTest.kt index c8f6ae3cfc..705b3b8b16 100644 --- a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/waiters/WaiterGeneratorTest.kt +++ b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/waiters/WaiterGeneratorTest.kt @@ -28,7 +28,7 @@ class WaiterGeneratorTest { @Test fun testDefaultDelays() { val expected = """ - val strategy = StandardRetryStrategy { + val strategy = retryStrategy ?: StandardRetryStrategy { maxAttempts = 20 tokenBucket = InfiniteTokenBucket delayProvider { @@ -45,7 +45,7 @@ class WaiterGeneratorTest { @Test fun testCustomDelays() { val expected = """ - val strategy = StandardRetryStrategy { + val strategy = retryStrategy ?: StandardRetryStrategy { maxAttempts = 20 tokenBucket = InfiniteTokenBucket delayProvider { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 14232f4241..6ef5b35d57 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,8 +1,8 @@ [versions] -kotlin-version = "2.0.21" +kotlin-version = "2.1.0" dokka-version = "1.9.10" -aws-kotlin-repo-tools-version = "0.4.16" +aws-kotlin-repo-tools-version = "0.4.17" # libs coroutines-version = "1.9.0" @@ -10,11 +10,12 @@ atomicfu-version = "0.25.0" okhttp-version = "5.0.0-alpha.14" okhttp4-version = "4.12.0" okio-version = "3.9.1" -otel-version = "1.43.0" +otel-version = "1.45.0" slf4j-version = "2.0.16" slf4j-v1x-version = "1.7.36" -crt-kotlin-version = "0.8.10" -micrometer-version = "1.13.6" +crt-kotlin-version = "0.9.0" +micrometer-version = "1.14.2" +binary-compatibility-validator-version = "0.16.3" # codegen smithy-version = "1.53.0" @@ -23,7 +24,7 @@ smithy-gradle-version = "0.9.0" # testing junit-version = "5.10.5" kotest-version = "5.9.1" -kotlin-compile-testing-version = "1.6.0" +kotlin-compile-testing-version = "0.7.0" kotlinx-benchmark-version = "0.4.12" kotlinx-serialization-version = "1.7.3" docker-java-version = "3.4.0" @@ -80,7 +81,7 @@ smithy-smoke-test-traits = { module = "software.amazon.smithy:smithy-smoke-test- junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit-version" } junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit-version" } -kotlin-compile-testing = {module = "com.github.tschuchortdev:kotlin-compile-testing", version.ref = "kotlin-compile-testing-version" } +kotlin-compile-testing = {module = "dev.zacsweers.kctfork:core", version.ref = "kotlin-compile-testing-version" } kotest-assertions-core = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest-version" } kotest-assertions-core-jvm = { module = "io.kotest:kotest-assertions-core-jvm", version.ref = "kotest-version" } kotlinx-benchmark-runtime = { module = "org.jetbrains.kotlinx:kotlinx-benchmark-runtime", version.ref = "kotlinx-benchmark-version" } @@ -104,7 +105,7 @@ dokka = { id = "org.jetbrains.dokka", version.ref = "dokka-version"} kotlin-jvm = {id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin-version" } kotlin-multiplatform = {id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin-version" } kotlinx-benchmark = { id = "org.jetbrains.kotlinx.benchmark", version.ref = "kotlinx-benchmark-version" } -kotlinx-binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version = "0.13.2" } +kotlinx-binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binary-compatibility-validator-version" } kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin-version"} aws-kotlin-repo-tools-kmp = { id = "aws.sdk.kotlin.gradle.kmp", version.ref = "aws-kotlin-repo-tools-version" } aws-kotlin-repo-tools-smithybuild = { id = "aws.sdk.kotlin.gradle.smithybuild", version.ref = "aws-kotlin-repo-tools-version" } diff --git a/runtime/auth/aws-signing-tests/jvm/src/aws/smithy/kotlin/runtime/auth/awssigning/tests/SigningSuiteTestBaseJVM.kt b/runtime/auth/aws-signing-tests/jvm/src/aws/smithy/kotlin/runtime/auth/awssigning/tests/SigningSuiteTestBaseJVM.kt index 904aef564b..b3c4202d26 100644 --- a/runtime/auth/aws-signing-tests/jvm/src/aws/smithy/kotlin/runtime/auth/awssigning/tests/SigningSuiteTestBaseJVM.kt +++ b/runtime/auth/aws-signing-tests/jvm/src/aws/smithy/kotlin/runtime/auth/awssigning/tests/SigningSuiteTestBaseJVM.kt @@ -28,6 +28,7 @@ import io.ktor.util.* import io.ktor.utils.io.* import io.ktor.utils.io.core.* import kotlinx.coroutines.runBlocking +import kotlinx.io.readByteArray import kotlinx.serialization.json.* import org.junit.jupiter.api.Assumptions.assumeTrue import org.junit.jupiter.api.Test @@ -383,7 +384,7 @@ public actual abstract class SigningSuiteTestBase : HasSigner { } if (hasBody) { - val bytes = runBlocking { chan.readRemaining().readBytes() } + val bytes = runBlocking { chan.readRemaining().readByteArray() } builder.body = HttpBody.fromBytes(bytes) } diff --git a/runtime/runtime-core/api/runtime-core.api b/runtime/runtime-core/api/runtime-core.api index 942f99e348..8c42034110 100644 --- a/runtime/runtime-core/api/runtime-core.api +++ b/runtime/runtime-core/api/runtime-core.api @@ -1,3 +1,13 @@ +public final class aws/smithy/kotlin/runtime/ClientErrorContext { + public fun (Ljava/lang/String;Ljava/lang/String;)V + public fun equals (Ljava/lang/Object;)Z + public final fun getFormatted ()Ljava/lang/String; + public final fun getKey ()Ljava/lang/String; + public final fun getValue ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public class aws/smithy/kotlin/runtime/ClientException : aws/smithy/kotlin/runtime/SdkBaseException { public fun ()V public fun (Ljava/lang/String;)V @@ -9,11 +19,13 @@ public class aws/smithy/kotlin/runtime/ErrorMetadata { public static final field Companion Laws/smithy/kotlin/runtime/ErrorMetadata$Companion; public fun ()V public final fun getAttributes ()Laws/smithy/kotlin/runtime/collections/MutableAttributes; + public final fun getClientContext ()Ljava/util/List; public final fun isRetryable ()Z public final fun isThrottling ()Z } public final class aws/smithy/kotlin/runtime/ErrorMetadata$Companion { + public final fun getClientContext ()Laws/smithy/kotlin/runtime/collections/AttributeKey; public final fun getRetryable ()Laws/smithy/kotlin/runtime/collections/AttributeKey; public final fun getThrottlingError ()Laws/smithy/kotlin/runtime/collections/AttributeKey; } @@ -62,7 +74,6 @@ public class aws/smithy/kotlin/runtime/ServiceException : aws/smithy/kotlin/runt public fun (Ljava/lang/String;)V public fun (Ljava/lang/String;Ljava/lang/Throwable;)V public fun (Ljava/lang/Throwable;)V - protected fun getDisplayMetadata ()Ljava/util/List; public fun getMessage ()Ljava/lang/String; public synthetic fun getSdkErrorMetadata ()Laws/smithy/kotlin/runtime/ErrorMetadata; public fun getSdkErrorMetadata ()Laws/smithy/kotlin/runtime/ServiceErrorMetadata; @@ -134,6 +145,7 @@ public final class aws/smithy/kotlin/runtime/collections/AttributesBuilder { } public final class aws/smithy/kotlin/runtime/collections/AttributesKt { + public static final fun appendValue (Laws/smithy/kotlin/runtime/collections/MutableAttributes;Laws/smithy/kotlin/runtime/collections/AttributeKey;Ljava/lang/Object;)V public static final fun attributesOf (Lkotlin/jvm/functions/Function1;)Laws/smithy/kotlin/runtime/collections/Attributes; public static final fun emptyAttributes ()Laws/smithy/kotlin/runtime/collections/Attributes; public static final fun get (Laws/smithy/kotlin/runtime/collections/Attributes;Laws/smithy/kotlin/runtime/collections/AttributeKey;)Ljava/lang/Object; @@ -1685,6 +1697,7 @@ public abstract class aws/smithy/kotlin/runtime/retries/RetryException : aws/smi public final fun getAttempts ()I public final fun getLastException ()Ljava/lang/Throwable; public final fun getLastResponse ()Ljava/lang/Object; + public fun toString ()Ljava/lang/String; } public final class aws/smithy/kotlin/runtime/retries/RetryFailureException : aws/smithy/kotlin/runtime/retries/RetryException { diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/Exceptions.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/Exceptions.kt index 87697ec7d7..661d855a5b 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/Exceptions.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/Exceptions.kt @@ -8,6 +8,41 @@ import aws.smithy.kotlin.runtime.collections.AttributeKey import aws.smithy.kotlin.runtime.collections.MutableAttributes import aws.smithy.kotlin.runtime.collections.mutableAttributes +/** + * Describes additional context about an error which may be useful in client-side debugging. This information will be + * included in exception messages. This contrasts with [ErrorMetadata] which is not _necessarily_ included in messages + * and not _necessarily_ client-related. + * @param key A header or key for the information + * @param value A value for the information + */ +public class ClientErrorContext(public val key: String, public val value: String) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as ClientErrorContext + + if (key != other.key) return false + if (value != other.value) return false + + return true + } + + /** + * Gets a formatted representation of this error context suitable for inclusion in a message. This format is + * generally `"$key: $value"`. + */ + public val formatted: String = "$key: $value" + + override fun hashCode(): Int { + var result = key.hashCode() + result = 31 * result + value.hashCode() + return result + } + + override fun toString(): String = "ClientErrorContext(key='$key', value='$value')" +} + /** * Additional metadata about an error */ @@ -16,6 +51,12 @@ public open class ErrorMetadata { public val attributes: MutableAttributes = mutableAttributes() public companion object { + /** + * Set if there are additional context elements about the error + */ + public val ClientContext: AttributeKey> = + AttributeKey("aws.smithy.kotlin#ClientContext") + /** * Set if an error is retryable */ @@ -32,6 +73,9 @@ public open class ErrorMetadata { public val isThrottling: Boolean get() = attributes.getOrNull(ThrottlingError) ?: false + + public val clientContext: List + get() = attributes.getOrNull(ClientContext).orEmpty() } /** @@ -156,7 +200,7 @@ public open class ServiceException : SdkBaseException { public constructor(cause: Throwable?) : super(cause) - protected open val displayMetadata: List + private val displayMetadata: List get() = buildList { val serviceProvidedMessage = super.message ?: sdkErrorMetadata.errorMessage if (serviceProvidedMessage == null) { @@ -166,7 +210,10 @@ public open class ServiceException : SdkBaseException { } else { add(serviceProvidedMessage) } + sdkErrorMetadata.requestId?.let { add("Request ID: $it") } + + sdkErrorMetadata.clientContext.mapTo(this@buildList) { it.formatted } } override val message: String diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/collections/Attributes.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/collections/Attributes.kt index 1fffcc82a2..77c2507394 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/collections/Attributes.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/collections/Attributes.kt @@ -115,6 +115,17 @@ public fun MutableAttributes.merge(other: Attributes) { } } +/** + * Appends a value to a list-typed attribute. If the attribute does not exist, it will be created. + * @param key The key for the attribute + * @param element The element to append to the existing (or new) list value of the attribute + */ +public fun MutableAttributes.appendValue(key: AttributeKey>, element: E) { + val existingList = getOrNull(key).orEmpty() + val newList = existingList + element + set(key, newList) +} + private class AttributesImpl constructor(seed: Attributes) : MutableAttributes { private val map: MutableMap, Any> = mutableMapOf() constructor() : this(emptyAttributes()) diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/retries/Exceptions.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/retries/Exceptions.kt index dbc2de0ffa..a81c830da2 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/retries/Exceptions.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/retries/Exceptions.kt @@ -22,7 +22,28 @@ public sealed class RetryException( public val attempts: Int, public val lastResponse: Any?, public val lastException: Throwable?, -) : ClientException(message, cause) +) : ClientException(message, cause) { + override fun toString(): String = buildString { + append(this@RetryException::class.simpleName) + append("(") + + append("message=") + append(message) + + append(",attempts=") + append(attempts) + + if (lastException != null) { + append(",lastException=") + append(lastException) + } else if (lastResponse != null) { + append(",lastResponse=") + append(lastResponse) + } + + append(")") + } +} /** * Indicates that retrying has failed because too many attempts have completed unsuccessfully. diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/retries/StandardRetryStrategy.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/retries/StandardRetryStrategy.kt index 95fd16faee..fb223aec68 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/retries/StandardRetryStrategy.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/retries/StandardRetryStrategy.kt @@ -5,6 +5,10 @@ package aws.smithy.kotlin.runtime.retries +import aws.smithy.kotlin.runtime.ClientErrorContext +import aws.smithy.kotlin.runtime.ErrorMetadata +import aws.smithy.kotlin.runtime.ServiceException +import aws.smithy.kotlin.runtime.collections.appendValue import aws.smithy.kotlin.runtime.retries.delay.* import aws.smithy.kotlin.runtime.retries.policy.RetryDirective import aws.smithy.kotlin.runtime.retries.policy.RetryPolicy @@ -129,17 +133,35 @@ public open class StandardRetryStrategy(override val config: Config = Config.def } } - private fun throwCapacityExceeded(cause: Throwable, attempt: Int, result: Result?): Nothing = - when (val ex = result?.exceptionOrNull()) { + private fun throwCapacityExceeded( + cause: RetryCapacityExceededException, + attempt: Int, + result: Result?, + ): Nothing { + val capacityMessage = buildString { + append("Insufficient client capacity to attempt retry, halting on attempt ") + append(attempt) + append(" of ") + append(config.maxAttempts) + } + + throw when (val retryableException = result?.exceptionOrNull()) { null -> throw TooManyAttemptsException( - cause.message!!, + capacityMessage, cause, attempt, result?.getOrNull(), result?.exceptionOrNull(), ) - else -> throw ex + + is ServiceException -> retryableException.apply { + val addCtx = ClientErrorContext("Early retry termination", capacityMessage) + sdkErrorMetadata.attributes.appendValue(ErrorMetadata.ClientContext, addCtx) + } + + else -> retryableException } + } /** * Handles the termination of the retry loop because of a non-retryable failure by throwing a diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/retries/delay/StandardRetryTokenBucket.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/retries/delay/StandardRetryTokenBucket.kt index 4bd2c23766..b2c4c70588 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/retries/delay/StandardRetryTokenBucket.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/retries/delay/StandardRetryTokenBucket.kt @@ -60,7 +60,7 @@ public class StandardRetryTokenBucket internal constructor( capacity -= size } else { if (config.useCircuitBreakerMode) { - throw RetryCapacityExceededException("Insufficient capacity to attempt another retry") + throw RetryCapacityExceededException("Insufficient capacity to attempt retry") } val extraRequiredCapacity = size - capacity diff --git a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/ExceptionsTest.kt b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/ExceptionsTest.kt index 551718c36e..0ba2d1639f 100644 --- a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/ExceptionsTest.kt +++ b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/ExceptionsTest.kt @@ -5,9 +5,14 @@ package aws.smithy.kotlin.runtime import aws.smithy.kotlin.runtime.collections.MutableAttributes +import aws.smithy.kotlin.runtime.collections.appendValue import kotlin.test.Test import kotlin.test.assertEquals +private const val CTX_KEY_1 = "Color" +private const val CTX_VALUE_1 = "blue" +private const val CTX_KEY_2 = "Shape" +private const val CTX_VALUE_2 = "square" private const val ERROR_CODE = "ErrorWithNoMessage" private const val METADATA_MESSAGE = "This is a message included in metadata but not the regular response" private const val PROTOCOL_RESPONSE_SUMMARY = "HTTP 418 I'm a teapot" @@ -104,6 +109,35 @@ class ExceptionsTest { e.message, ) } + + @Test + fun testNoMessageWithClientContext() { + val e = FooServiceException { + appendValue(ErrorMetadata.ClientContext, ClientErrorContext(CTX_KEY_1, CTX_VALUE_1)) + appendValue(ErrorMetadata.ClientContext, ClientErrorContext(CTX_KEY_2, CTX_VALUE_2)) + } + assertEquals( + buildList { + add("Error type: Unknown") + add("Protocol response: (empty response)") + add("$CTX_KEY_1: $CTX_VALUE_1") + add("$CTX_KEY_2: $CTX_VALUE_2") + }.joinToString(), + e.message, + ) + } + + @Test + fun testMessageWithClientContext() { + val e = FooServiceException(SERVICE_MESSAGE) { + appendValue(ErrorMetadata.ClientContext, ClientErrorContext(CTX_KEY_1, CTX_VALUE_1)) + appendValue(ErrorMetadata.ClientContext, ClientErrorContext(CTX_KEY_2, CTX_VALUE_2)) + } + assertEquals( + "$SERVICE_MESSAGE, $CTX_KEY_1: $CTX_VALUE_1, $CTX_KEY_2: $CTX_VALUE_2", + e.message, + ) + } } private class FooServiceException( diff --git a/runtime/runtime-core/jvm/test/aws/smithy/kotlin/runtime/retries/impl/StandardRetryIntegrationTest.kt b/runtime/runtime-core/jvm/test/aws/smithy/kotlin/runtime/retries/impl/StandardRetryIntegrationTest.kt index f2c08e47eb..2bd3bc7e6a 100644 --- a/runtime/runtime-core/jvm/test/aws/smithy/kotlin/runtime/retries/impl/StandardRetryIntegrationTest.kt +++ b/runtime/runtime-core/jvm/test/aws/smithy/kotlin/runtime/retries/impl/StandardRetryIntegrationTest.kt @@ -5,8 +5,8 @@ package aws.smithy.kotlin.runtime.retries.impl +import aws.smithy.kotlin.runtime.ServiceException import aws.smithy.kotlin.runtime.retries.StandardRetryStrategy -import aws.smithy.kotlin.runtime.retries.TooManyAttemptsException import aws.smithy.kotlin.runtime.retries.delay.StandardRetryTokenBucket import aws.smithy.kotlin.runtime.retries.getOrThrow import aws.smithy.kotlin.runtime.retries.policy.RetryDirective @@ -15,6 +15,7 @@ import aws.smithy.kotlin.runtime.retries.policy.RetryPolicy import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.currentTime import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.assertThrows import kotlin.test.* import kotlin.time.Duration.Companion.milliseconds @@ -38,7 +39,7 @@ class StandardRetryIntegrationTest { val block = object { var index = 0 - suspend fun doIt() = tc.responses[index++].response.statusCode + suspend fun doIt() = tc.responses[index++].response.getOrThrow() }::doIt val startTimeMs = currentTime @@ -47,9 +48,29 @@ class StandardRetryIntegrationTest { val finalState = tc.responses.last().expected when (finalState.outcome) { - TestOutcome.Success -> assertEquals(200, result.getOrNull()?.getOrThrow(), "Unexpected outcome for $name") - TestOutcome.MaxAttemptsExceeded -> assertIs(result.exceptionOrNull()) - TestOutcome.RetryQuotaExceeded -> assertIs(result.exceptionOrNull()) + TestOutcome.Success -> + assertEquals(Ok, result.getOrThrow().getOrThrow(), "Unexpected outcome for $name") + + TestOutcome.MaxAttemptsExceeded -> { + val e = assertThrows("Expected exception for $name") { + result.getOrThrow() + } + + assertEquals(tc.responses.last().response.statusCode, e.code, "Unexpected error code for $name") + } + + TestOutcome.RetryQuotaExceeded -> { + val e = assertThrows("Expected exception for $name") { + result.getOrThrow() + } + + assertEquals(tc.responses.last().response.statusCode, e.code, "Unexpected error code for $name") + + assertTrue("Expected retry capacity message in exception for $name") { + "Insufficient client capacity to attempt retry" in e.message + } + } + else -> fail("Unexpected outcome for $name: ${finalState.outcome}") } @@ -72,10 +93,19 @@ class StandardRetryIntegrationTest { } } -object IntegrationTestPolicy : RetryPolicy { - override fun evaluate(result: Result): RetryDirective = when (val code = result.getOrNull()!!) { - 200 -> RetryDirective.TerminateAndSucceed - 500, 502 -> RetryDirective.RetryError(RetryErrorType.ServerSide) - else -> fail("Unexpected status code: $code") +object IntegrationTestPolicy : RetryPolicy { + override fun evaluate(result: Result): RetryDirective = when { + result.isSuccess -> RetryDirective.TerminateAndSucceed + result.isFailure -> RetryDirective.RetryError(RetryErrorType.ServerSide) + else -> fail("Unexpected result condition") } } + +data object Ok + +class HttpCodeException(val code: Int) : ServiceException() + +fun Response.getOrThrow() = when (statusCode) { + 200 -> Ok + else -> throw HttpCodeException(statusCode) +} diff --git a/tests/compile/src/test/kotlin/software/amazon/smithy/kotlin/codegen/util/TestUtils.kt b/tests/compile/src/test/kotlin/software/amazon/smithy/kotlin/codegen/util/TestUtils.kt index 02bf209f3c..2e64c9d4fe 100644 --- a/tests/compile/src/test/kotlin/software/amazon/smithy/kotlin/codegen/util/TestUtils.kt +++ b/tests/compile/src/test/kotlin/software/amazon/smithy/kotlin/codegen/util/TestUtils.kt @@ -6,6 +6,7 @@ package software.amazon.smithy.kotlin.codegen.util +import com.tschuchort.compiletesting.JvmCompilationResult import com.tschuchort.compiletesting.KotlinCompilation import com.tschuchort.compiletesting.SourceFile import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi @@ -36,8 +37,8 @@ private fun String.slashEscape(char: Char) = this.replace(char.toString(), """\$ * Captures the result of a model transformation test */ data class ModelChangeTestResult( - val originalModelCompilationResult: KotlinCompilation.Result, - val updatedModelCompilationResult: KotlinCompilation.Result, + val originalModelCompilationResult: JvmCompilationResult, + val updatedModelCompilationResult: JvmCompilationResult, val compileSuccess: Boolean, val compileOutput: String, ) @@ -88,7 +89,7 @@ fun compileSdkAndTest( testSource: String? = null, outputSink: OutputStream = System.out, emitSourcesToTmp: Boolean = false, -): KotlinCompilation.Result { +): JvmCompilationResult { val sdkFileManifest = generateSdk(model) if (emitSourcesToTmp) { From e0c25d654adde3124f50733e0bc66fde2ee8d058 Mon Sep 17 00:00:00 2001 From: 0marperez <60363173+0marperez@users.noreply.github.com> Date: Wed, 15 Jan 2025 12:35:50 -0500 Subject: [PATCH 20/79] feat: support default checksums (#1191) --- .../kotlin/codegen/core/RuntimeTypes.kt | 7 +- .../HttpChecksumRequiredIntegration.kt | 66 +++++ .../protocol/HttpProtocolClientGenerator.kt | 25 -- ...tlin.codegen.integration.KotlinIntegration | 1 + .../protocol/http-client/api/http-client.api | 20 +- ...eptor.kt => CachingChecksumInterceptor.kt} | 14 +- .../interceptors/ChecksumInterceptorUtils.kt | 44 +++ .../FlexibleChecksumsRequestInterceptor.kt | 277 ++++++++---------- .../FlexibleChecksumsResponseInterceptor.kt | 94 +++--- .../HttpChecksumRequiredInterceptor.kt | 98 +++++++ .../interceptors/Md5ChecksumInterceptor.kt | 54 ---- .../http/operation/HttpOperationContext.kt | 4 +- ...t.kt => CachingChecksumInterceptorTest.kt} | 12 +- ...FlexibleChecksumsRequestInterceptorTest.kt | 115 +++++--- ...lexibleChecksumsResponseInterceptorTest.kt | 84 +++++- ...=> HttpChecksumRequiredInterceptorTest.kt} | 46 ++- runtime/protocol/http/api/http.api | 18 ++ .../smithy/kotlin/runtime/http/HttpBody.kt | 52 ++++ runtime/runtime-core/api/runtime-core.api | 15 + .../businessmetrics/BusinessMetricsUtils.kt | 8 + .../kotlin/runtime/hashing/HashFunction.kt | 55 ++++ .../kotlin/runtime/io/SdkByteReadChannel.kt | 16 + runtime/smithy-client/api/smithy-client.api | 28 ++ .../client/config/HttpChecksumConfig.kt | 63 ++++ 24 files changed, 865 insertions(+), 351 deletions(-) create mode 100644 codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/checksums/HttpChecksumRequiredIntegration.kt rename runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/{AbstractChecksumInterceptor.kt => CachingChecksumInterceptor.kt} (67%) create mode 100644 runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/ChecksumInterceptorUtils.kt create mode 100644 runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/HttpChecksumRequiredInterceptor.kt delete mode 100644 runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/Md5ChecksumInterceptor.kt rename runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/{AbstractChecksumInterceptorTest.kt => CachingChecksumInterceptorTest.kt} (88%) rename runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/{Md5ChecksumInterceptorTest.kt => HttpChecksumRequiredInterceptorTest.kt} (62%) create mode 100644 runtime/smithy-client/common/src/aws/smithy/kotlin/runtime/client/config/HttpChecksumConfig.kt diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/RuntimeTypes.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/RuntimeTypes.kt index e42612b0b5..b6341e0443 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/RuntimeTypes.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/RuntimeTypes.kt @@ -81,7 +81,7 @@ object RuntimeTypes { object Interceptors : RuntimeTypePackage(KotlinDependency.HTTP, "interceptors") { val ContinueInterceptor = symbol("ContinueInterceptor") val HttpInterceptor = symbol("HttpInterceptor") - val Md5ChecksumInterceptor = symbol("Md5ChecksumInterceptor") + val HttpChecksumRequiredInterceptor = symbol("HttpChecksumRequiredInterceptor") val FlexibleChecksumsRequestInterceptor = symbol("FlexibleChecksumsRequestInterceptor") val FlexibleChecksumsResponseInterceptor = symbol("FlexibleChecksumsResponseInterceptor") val ResponseLengthValidationInterceptor = symbol("ResponseLengthValidationInterceptor") @@ -231,6 +231,9 @@ object RuntimeTypes { object Config : RuntimeTypePackage(KotlinDependency.SMITHY_CLIENT, "config") { val RequestCompressionConfig = symbol("RequestCompressionConfig") val CompressionClientConfig = symbol("CompressionClientConfig") + val HttpChecksumConfig = symbol("HttpChecksumConfig") + val RequestHttpChecksumConfig = symbol("RequestHttpChecksumConfig") + val ResponseHttpChecksumConfig = symbol("ResponseHttpChecksumConfig") } object Endpoints : RuntimeTypePackage(KotlinDependency.SMITHY_CLIENT, "endpoints") { @@ -395,6 +398,7 @@ object RuntimeTypes { val TelemetryContextElement = symbol("TelemetryContextElement", "context") val TraceSpan = symbol("TraceSpan", "trace") val withSpan = symbol("withSpan", "trace") + val warn = symbol("warn", "logging") } object TelemetryDefaults : RuntimeTypePackage(KotlinDependency.TELEMETRY_DEFAULTS) { val Global = symbol("Global") @@ -409,6 +413,7 @@ object RuntimeTypes { val CompletableDeferred = "kotlinx.coroutines.CompletableDeferred".toSymbol() val job = "kotlinx.coroutines.job".toSymbol() + val runBlocking = "kotlinx.coroutines.runBlocking".toSymbol() object Flow { // NOTE: smithy-kotlin core has an API dependency on this already diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/checksums/HttpChecksumRequiredIntegration.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/checksums/HttpChecksumRequiredIntegration.kt new file mode 100644 index 0000000000..61432a7b12 --- /dev/null +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/checksums/HttpChecksumRequiredIntegration.kt @@ -0,0 +1,66 @@ +package software.amazon.smithy.kotlin.codegen.rendering.checksums + +import software.amazon.smithy.aws.traits.HttpChecksumTrait +import software.amazon.smithy.kotlin.codegen.KotlinSettings +import software.amazon.smithy.kotlin.codegen.core.KotlinWriter +import software.amazon.smithy.kotlin.codegen.core.RuntimeTypes +import software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration +import software.amazon.smithy.kotlin.codegen.model.hasTrait +import software.amazon.smithy.kotlin.codegen.rendering.protocol.ProtocolGenerator +import software.amazon.smithy.kotlin.codegen.rendering.protocol.ProtocolMiddleware +import software.amazon.smithy.model.Model +import software.amazon.smithy.model.shapes.OperationShape +import software.amazon.smithy.model.traits.HttpChecksumRequiredTrait + +/** + * Handles the `httpChecksumRequired` trait. + * See: https://smithy.io/2.0/spec/http-bindings.html#httpchecksumrequired-trait + */ +class HttpChecksumRequiredIntegration : KotlinIntegration { + override fun enabledForService(model: Model, settings: KotlinSettings): Boolean = + model.isTraitApplied(HttpChecksumRequiredTrait::class.java) + + override fun customizeMiddleware( + ctx: ProtocolGenerator.GenerationContext, + resolved: List, + ): List = resolved + httpChecksumRequiredDefaultAlgorithmMiddleware + httpChecksumRequiredMiddleware +} + +/** + * Adds default checksum algorithm to the execution context + */ +private val httpChecksumRequiredDefaultAlgorithmMiddleware = object : ProtocolMiddleware { + override val name: String = "httpChecksumRequiredDefaultAlgorithmMiddleware" + override val order: Byte = -2 // Before S3 Express (possibly) changes the default (-1) and before calculating checksum (0) + + override fun isEnabledFor(ctx: ProtocolGenerator.GenerationContext, op: OperationShape): Boolean = + op.hasTrait() && !op.hasTrait() + + override fun render(ctx: ProtocolGenerator.GenerationContext, op: OperationShape, writer: KotlinWriter) { + writer.write( + "op.context[#T.DefaultChecksumAlgorithm] = #S", + RuntimeTypes.HttpClient.Operation.HttpOperationContext, + "MD5", + ) + } +} + +/** + * Adds interceptor to calculate request checksums. + * The `httpChecksum` trait supersedes the `httpChecksumRequired` trait. If both are applied to an operation use `httpChecksum`. + * + * See: https://smithy.io/2.0/aws/aws-core.html#behavior-with-httpchecksumrequired + */ +private val httpChecksumRequiredMiddleware = object : ProtocolMiddleware { + override val name: String = "httpChecksumRequiredMiddleware" + + override fun isEnabledFor(ctx: ProtocolGenerator.GenerationContext, op: OperationShape): Boolean = + op.hasTrait() && !op.hasTrait() + + override fun render(ctx: ProtocolGenerator.GenerationContext, op: OperationShape, writer: KotlinWriter) { + writer.write( + "op.interceptors.add(#T())", + RuntimeTypes.HttpClient.Interceptors.HttpChecksumRequiredInterceptor, + ) + } +} diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpProtocolClientGenerator.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpProtocolClientGenerator.kt index 519957cbed..f12ab07a26 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpProtocolClientGenerator.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpProtocolClientGenerator.kt @@ -4,7 +4,6 @@ */ package software.amazon.smithy.kotlin.codegen.rendering.protocol -import software.amazon.smithy.aws.traits.HttpChecksumTrait import software.amazon.smithy.codegen.core.Symbol import software.amazon.smithy.kotlin.codegen.core.* import software.amazon.smithy.kotlin.codegen.integration.SectionId @@ -22,7 +21,6 @@ import software.amazon.smithy.model.knowledge.OperationIndex import software.amazon.smithy.model.knowledge.TopDownIndex import software.amazon.smithy.model.shapes.OperationShape import software.amazon.smithy.model.traits.EndpointTrait -import software.amazon.smithy.model.traits.HttpChecksumRequiredTrait /** * Renders an implementation of a service interface for HTTP protocol @@ -318,8 +316,6 @@ open class HttpProtocolClientGenerator( .forEach { middleware -> middleware.render(ctx, op, writer) } - - op.renderIsMd5ChecksumRequired(writer) } /** @@ -336,27 +332,6 @@ open class HttpProtocolClientGenerator( */ protected open fun renderAdditionalMethods(writer: KotlinWriter) { } - /** - * Render optionally installing Md5ChecksumMiddleware. - * The Md5 middleware will only be installed if the operation requires a checksum and the user has not opted-in to flexible checksums. - */ - private fun OperationShape.renderIsMd5ChecksumRequired(writer: KotlinWriter) { - val httpChecksumTrait = getTrait() - - // the checksum requirement can be modeled in either HttpChecksumTrait's `requestChecksumRequired` or the HttpChecksumRequired trait - if (!hasTrait() && httpChecksumTrait == null) { - return - } - - if (hasTrait() || httpChecksumTrait?.isRequestChecksumRequired == true) { - val interceptorSymbol = RuntimeTypes.HttpClient.Interceptors.Md5ChecksumInterceptor - val inputSymbol = ctx.symbolProvider.toSymbol(ctx.model.expectShape(inputShape)) - writer.withBlock("op.interceptors.add(#T<#T> {", "})", interceptorSymbol, inputSymbol) { - writer.write("op.context.getOrNull(#T.ChecksumAlgorithm) == null", RuntimeTypes.HttpClient.Operation.HttpOperationContext) - } - } - } - /** * render a utility function to populate an operation's ExecutionContext with defaults from service config, environment, etc */ diff --git a/codegen/smithy-kotlin-codegen/src/main/resources/META-INF/services/software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration b/codegen/smithy-kotlin-codegen/src/main/resources/META-INF/services/software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration index 0d02b51187..2ab7fe506a 100644 --- a/codegen/smithy-kotlin-codegen/src/main/resources/META-INF/services/software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration +++ b/codegen/smithy-kotlin-codegen/src/main/resources/META-INF/services/software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration @@ -13,3 +13,4 @@ software.amazon.smithy.kotlin.codegen.rendering.endpoints.SdkEndpointBuiltinInte software.amazon.smithy.kotlin.codegen.rendering.compression.RequestCompressionIntegration software.amazon.smithy.kotlin.codegen.rendering.auth.SigV4AsymmetricAuthSchemeIntegration software.amazon.smithy.kotlin.codegen.rendering.smoketests.SmokeTestsIntegration +software.amazon.smithy.kotlin.codegen.rendering.checksums.HttpChecksumRequiredIntegration \ No newline at end of file diff --git a/runtime/protocol/http-client/api/http-client.api b/runtime/protocol/http-client/api/http-client.api index 6e91f528f2..e8527c2c2b 100644 --- a/runtime/protocol/http-client/api/http-client.api +++ b/runtime/protocol/http-client/api/http-client.api @@ -255,7 +255,7 @@ public final class aws/smithy/kotlin/runtime/http/engine/internal/ManagedHttpCli public static final fun manage (Laws/smithy/kotlin/runtime/http/engine/HttpClientEngine;)Laws/smithy/kotlin/runtime/http/engine/HttpClientEngine; } -public abstract class aws/smithy/kotlin/runtime/http/interceptors/AbstractChecksumInterceptor : aws/smithy/kotlin/runtime/client/Interceptor { +public abstract class aws/smithy/kotlin/runtime/http/interceptors/CachingChecksumInterceptor : aws/smithy/kotlin/runtime/client/Interceptor { public fun ()V public abstract fun applyChecksum (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;Ljava/lang/String;)Laws/smithy/kotlin/runtime/http/request/HttpRequest; public abstract fun calculateChecksum (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -331,19 +331,17 @@ public final class aws/smithy/kotlin/runtime/http/interceptors/DiscoveredEndpoin public fun readBeforeTransmit (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;)V } -public final class aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsRequestInterceptor : aws/smithy/kotlin/runtime/http/interceptors/AbstractChecksumInterceptor { - public fun ()V - public fun (Lkotlin/jvm/functions/Function1;)V - public synthetic fun (Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +public final class aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsRequestInterceptor : aws/smithy/kotlin/runtime/http/interceptors/CachingChecksumInterceptor { + public fun (ZLaws/smithy/kotlin/runtime/client/config/RequestHttpChecksumConfig;Ljava/lang/String;)V public fun applyChecksum (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;Ljava/lang/String;)Laws/smithy/kotlin/runtime/http/request/HttpRequest; public fun calculateChecksum (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun modifyBeforeSigning (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public fun readAfterSerialization (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;)V } -public final class aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsResponseInterceptor : aws/smithy/kotlin/runtime/client/Interceptor { +public class aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsResponseInterceptor : aws/smithy/kotlin/runtime/client/Interceptor { public static final field Companion Laws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsResponseInterceptor$Companion; - public fun (Lkotlin/jvm/functions/Function1;)V + public fun (ZLaws/smithy/kotlin/runtime/client/config/ResponseHttpChecksumConfig;)V + public fun ignoreChecksum (Ljava/lang/String;Laws/smithy/kotlin/runtime/client/ProtocolResponseInterceptorContext;)Z public fun modifyBeforeAttemptCompletion-gIAlu-s (Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun modifyBeforeCompletion-gIAlu-s (Laws/smithy/kotlin/runtime/client/ResponseInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun modifyBeforeDeserialization (Laws/smithy/kotlin/runtime/client/ProtocolResponseInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -369,10 +367,8 @@ public final class aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksums public final fun getChecksumHeaderValidated ()Laws/smithy/kotlin/runtime/collections/AttributeKey; } -public final class aws/smithy/kotlin/runtime/http/interceptors/Md5ChecksumInterceptor : aws/smithy/kotlin/runtime/http/interceptors/AbstractChecksumInterceptor { +public final class aws/smithy/kotlin/runtime/http/interceptors/HttpChecksumRequiredInterceptor : aws/smithy/kotlin/runtime/http/interceptors/CachingChecksumInterceptor { public fun ()V - public fun (Lkotlin/jvm/functions/Function1;)V - public synthetic fun (Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun applyChecksum (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;Ljava/lang/String;)Laws/smithy/kotlin/runtime/http/request/HttpRequest; public fun calculateChecksum (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun modifyBeforeSigning (Laws/smithy/kotlin/runtime/client/ProtocolRequestInterceptorContext;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -516,9 +512,9 @@ public abstract interface class aws/smithy/kotlin/runtime/http/operation/HttpDes public final class aws/smithy/kotlin/runtime/http/operation/HttpOperationContext { public static final field INSTANCE Laws/smithy/kotlin/runtime/http/operation/HttpOperationContext; - public final fun getChecksumAlgorithm ()Laws/smithy/kotlin/runtime/collections/AttributeKey; public final fun getClockSkew ()Laws/smithy/kotlin/runtime/collections/AttributeKey; public final fun getClockSkewApproximateSigningTime ()Laws/smithy/kotlin/runtime/collections/AttributeKey; + public final fun getDefaultChecksumAlgorithm ()Laws/smithy/kotlin/runtime/collections/AttributeKey; public final fun getHostPrefix ()Laws/smithy/kotlin/runtime/collections/AttributeKey; public final fun getHttpCallList ()Laws/smithy/kotlin/runtime/collections/AttributeKey; public final fun getOperationAttributes ()Laws/smithy/kotlin/runtime/collections/AttributeKey; diff --git a/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/AbstractChecksumInterceptor.kt b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/CachingChecksumInterceptor.kt similarity index 67% rename from runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/AbstractChecksumInterceptor.kt rename to runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/CachingChecksumInterceptor.kt index 3fa8406bf5..adad89f259 100644 --- a/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/AbstractChecksumInterceptor.kt +++ b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/CachingChecksumInterceptor.kt @@ -9,13 +9,21 @@ import aws.smithy.kotlin.runtime.InternalApi import aws.smithy.kotlin.runtime.client.ProtocolRequestInterceptorContext import aws.smithy.kotlin.runtime.http.request.HttpRequest +/** + * Enables inheriting [HttpInterceptor]s to use checksums caching + */ @InternalApi -public abstract class AbstractChecksumInterceptor : HttpInterceptor { +public abstract class CachingChecksumInterceptor : HttpInterceptor { private var cachedChecksum: String? = null override suspend fun modifyBeforeSigning(context: ProtocolRequestInterceptorContext): HttpRequest { - cachedChecksum ?: calculateChecksum(context).also { cachedChecksum = it } - return cachedChecksum?.let { applyChecksum(context, it) } ?: context.protocolRequest + cachedChecksum = cachedChecksum ?: calculateChecksum(context) + + return if (cachedChecksum != null) { + applyChecksum(context, cachedChecksum!!) + } else { + context.protocolRequest + } } public abstract suspend fun calculateChecksum(context: ProtocolRequestInterceptorContext): String? diff --git a/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/ChecksumInterceptorUtils.kt b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/ChecksumInterceptorUtils.kt new file mode 100644 index 0000000000..b510ab14e5 --- /dev/null +++ b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/ChecksumInterceptorUtils.kt @@ -0,0 +1,44 @@ +package aws.smithy.kotlin.runtime.http.interceptors + +import aws.smithy.kotlin.runtime.businessmetrics.emitBusinessMetric +import aws.smithy.kotlin.runtime.client.ProtocolRequestInterceptorContext +import aws.smithy.kotlin.runtime.hashing.HashFunction +import aws.smithy.kotlin.runtime.hashing.resolveChecksumAlgorithmHeaderName +import aws.smithy.kotlin.runtime.hashing.toBusinessMetric +import aws.smithy.kotlin.runtime.http.operation.HttpOperationContext +import aws.smithy.kotlin.runtime.http.request.HttpRequest +import aws.smithy.kotlin.runtime.http.request.toBuilder +import aws.smithy.kotlin.runtime.http.toCompletingBody +import aws.smithy.kotlin.runtime.http.toHashingBody +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.job + +/** + * Configures [HttpRequest] with AWS chunked streaming to calculate checksum during transmission + * @return [HttpRequest] + */ +internal fun calculateAwsChunkedStreamingChecksum( + context: ProtocolRequestInterceptorContext, + checksumAlgorithm: HashFunction, +): HttpRequest { + val request = context.protocolRequest.toBuilder() + val deferredChecksum = CompletableDeferred(context.executionContext.coroutineContext.job) + val checksumHeader = checksumAlgorithm.resolveChecksumAlgorithmHeaderName() + + request.body = request.body + .toHashingBody(checksumAlgorithm, request.body.contentLength) + .toCompletingBody(deferredChecksum) + + request.headers.append("x-amz-trailer", checksumHeader) + request.trailingHeaders.append(checksumHeader, deferredChecksum) + + context.executionContext.emitBusinessMetric(checksumAlgorithm.toBusinessMetric()) + + return request.build() +} + +/** + * @return The default checksum algorithm name in the execution context, null if default checksums are disabled. + */ +internal val ProtocolRequestInterceptorContext.defaultChecksumAlgorithmName: String? + get() = executionContext.getOrNull(HttpOperationContext.DefaultChecksumAlgorithm) diff --git a/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsRequestInterceptor.kt b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsRequestInterceptor.kt index b29ede017f..936dead520 100644 --- a/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsRequestInterceptor.kt +++ b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsRequestInterceptor.kt @@ -5,104 +5,117 @@ package aws.smithy.kotlin.runtime.http.interceptors -import aws.smithy.kotlin.runtime.ClientException import aws.smithy.kotlin.runtime.InternalApi +import aws.smithy.kotlin.runtime.businessmetrics.emitBusinessMetric import aws.smithy.kotlin.runtime.client.ProtocolRequestInterceptorContext +import aws.smithy.kotlin.runtime.client.config.RequestHttpChecksumConfig import aws.smithy.kotlin.runtime.hashing.* import aws.smithy.kotlin.runtime.http.* -import aws.smithy.kotlin.runtime.http.operation.HttpOperationContext import aws.smithy.kotlin.runtime.http.request.HttpRequest -import aws.smithy.kotlin.runtime.http.request.header import aws.smithy.kotlin.runtime.http.request.toBuilder import aws.smithy.kotlin.runtime.io.* +import aws.smithy.kotlin.runtime.telemetry.logging.Logger import aws.smithy.kotlin.runtime.telemetry.logging.logger import aws.smithy.kotlin.runtime.text.encoding.encodeBase64String -import aws.smithy.kotlin.runtime.util.LazyAsyncValue -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.job import kotlin.coroutines.coroutineContext /** - * Mutate a request to enable flexible checksums. + * Handles request checksums for operations with the [HttpChecksumTrait] applied. * - * If the checksum will be sent as a header, calculate the checksum. + * If a user supplies a checksum via an HTTP header no calculation will be done. The exception is MD5, if a user + * supplies an MD5 checksum header it will be ignored. * - * Otherwise, if it will be sent as a trailing header, calculate the checksum as asynchronously as the body is streamed. - * In this case, a [LazyAsyncValue] will be added to the execution context which allows the trailing checksum to be sent - * after the entire body has been streamed. + * If the request configuration and model requires checksum calculation: + * - Check if the user configured a checksum algorithm for the request and attempt to use that. + * - If no checksum is configured for the request then use the default checksum algorithm to calculate a checksum. * - * @param checksumAlgorithmNameInitializer an optional function which parses the input [I] to return the checksum algorithm name. - * if not set, then the [HttpOperationContext.ChecksumAlgorithm] execution context attribute will be used. + * If the request will be streamed: + * - The checksum calculation is done during transmission using a hashing & completing body. + * - The checksum will be sent in a trailing header, once the request is consumed. + * + * If the request will not be streamed: + * - The checksum calculation is done before transmission + * - The checksum will be sent in a header + * + * Business metrics MUST be emitted for the checksum algorithm used. + * + * @param requestChecksumRequired Model sourced flag indicating if checksum calculation is mandatory. + * @param requestChecksumCalculation Configuration option that determines when checksum calculation should be done. + * @param requestChecksumAlgorithm The checksum algorithm that the user selected for the request, may be null. */ @InternalApi -public class FlexibleChecksumsRequestInterceptor( - private val checksumAlgorithmNameInitializer: ((I) -> String?)? = null, -) : AbstractChecksumInterceptor() { - private var checksumAlgorithmName: String? = null - - @Deprecated("readAfterSerialization is no longer used") - override fun readAfterSerialization(context: ProtocolRequestInterceptorContext) { } - +public class FlexibleChecksumsRequestInterceptor( + private val requestChecksumRequired: Boolean, + private val requestChecksumCalculation: RequestHttpChecksumConfig?, + private val requestChecksumAlgorithm: String?, +) : CachingChecksumInterceptor() { override suspend fun modifyBeforeSigning(context: ProtocolRequestInterceptorContext): HttpRequest { - val logger = coroutineContext.logger>() + val logger = coroutineContext.logger() - @Suppress("UNCHECKED_CAST") - val input = context.request as I - checksumAlgorithmName = checksumAlgorithmNameInitializer?.invoke(input) ?: context.executionContext.getOrNull(HttpOperationContext.ChecksumAlgorithm) + context.protocolRequest.userProvidedChecksumHeader(logger)?.let { + logger.debug { "Checksum was supplied via header: skipping checksum calculation" } - checksumAlgorithmName ?: run { - logger.debug { "no checksum algorithm specified, skipping flexible checksums processing" } + val request = context.protocolRequest.toBuilder() + request.headers.removeAllChecksumHeadersExcept(it) return context.protocolRequest } - val req = context.protocolRequest.toBuilder() - - check(context.protocolRequest.body !is HttpBody.Empty) { - "Can't calculate the checksum of an empty body" - } - - val headerName = "x-amz-checksum-$checksumAlgorithmName".lowercase() - logger.debug { "Resolved checksum header name: $headerName" } - - // remove all checksum headers except for $headerName - // this handles the case where a user inputs a precalculated checksum, but it doesn't match the input checksum algorithm - req.headers.removeAllChecksumHeadersExcept(headerName) - - val checksumAlgorithm = checksumAlgorithmName?.toHashFunction() ?: throw ClientException("Could not parse checksum algorithm $checksumAlgorithmName") - - if (!checksumAlgorithm.isSupported) { - throw ClientException("Checksum algorithm $checksumAlgorithmName is not supported for flexible checksums") - } - - if (req.body.isEligibleForAwsChunkedStreaming) { - req.header("x-amz-trailer", headerName) - - val deferredChecksum = CompletableDeferred(context.executionContext.coroutineContext.job) - - if (req.headers[headerName] != null) { - logger.debug { "User supplied a checksum, skipping asynchronous calculation" } - - val checksum = req.headers[headerName]!! - req.headers.remove(headerName) // remove the checksum header because it will be sent as a trailing header - - deferredChecksum.complete(checksum) + resolveChecksumAlgorithm( + requestChecksumRequired, + requestChecksumCalculation, + requestChecksumAlgorithm, + context, + )?.let { checksumAlgorithm -> + return if (context.protocolRequest.body.isEligibleForAwsChunkedStreaming) { + logger.debug { "Calculating checksum during transmission using: ${checksumAlgorithm::class.simpleName}" } + calculateAwsChunkedStreamingChecksum(context, checksumAlgorithm) } else { - logger.debug { "Calculating checksum asynchronously" } - req.body = req.body - .toHashingBody(checksumAlgorithm, req.body.contentLength) - .toCompletingBody(deferredChecksum) + if (context.protocolRequest.body is HttpBody.Bytes) { + // Cache checksum + super.modifyBeforeSigning(context) + } else { + val checksum = calculateFlexibleChecksumsChecksum(context) + applyFlexibleChecksumsChecksum(context, checksum) + } } - - req.trailingHeaders.append(headerName, deferredChecksum) - return req.build() - } else { - return super.modifyBeforeSigning(context) } + + logger.debug { "Checksum wasn't provided, selected, or isn't required: skipping checksum calculation" } + return context.protocolRequest } - override suspend fun calculateChecksum(context: ProtocolRequestInterceptorContext): String? { + /** + * Determines what checksum algorithm to use, null if none is required + */ + private fun resolveChecksumAlgorithm( + requestChecksumRequired: Boolean, + requestChecksumCalculation: RequestHttpChecksumConfig?, + requestChecksumAlgorithm: String?, + context: ProtocolRequestInterceptorContext, + ): HashFunction? = + requestChecksumAlgorithm + ?.toHashFunctionOrThrow() + ?.takeIf { it.isSupportedForFlexibleChecksums } + ?: context.defaultChecksumAlgorithmName + ?.toHashFunctionOrThrow() + ?.takeIf { + (requestChecksumRequired || requestChecksumCalculation == RequestHttpChecksumConfig.WHEN_SUPPORTED) && + it.isSupportedForFlexibleChecksums + } + + /** + * Calculates a checksum based on the requirements and limitations of [FlexibleChecksumsRequestInterceptor] + */ + private suspend fun calculateFlexibleChecksumsChecksum( + context: ProtocolRequestInterceptorContext, + ): String { val req = context.protocolRequest.toBuilder() - val checksumAlgorithm = checksumAlgorithmName?.toHashFunction() ?: return null + val checksumAlgorithm = resolveChecksumAlgorithm( + requestChecksumRequired, + requestChecksumCalculation, + requestChecksumAlgorithm, + context, + )!! return when { req.body.contentLength == null && !req.body.isOneShot -> { @@ -110,104 +123,66 @@ public class FlexibleChecksumsRequestInterceptor( channel.rollingHash(checksumAlgorithm).encodeBase64String() } else -> { - val bodyBytes = req.body.readAll()!! - req.body = bodyBytes.toHttpBody() + val bodyBytes = req.body.readAll() ?: byteArrayOf() + if (req.body.isOneShot) req.body = bodyBytes.toHttpBody() bodyBytes.hash(checksumAlgorithm).encodeBase64String() } } } - override fun applyChecksum( + override suspend fun calculateChecksum(context: ProtocolRequestInterceptorContext): String? = + calculateFlexibleChecksumsChecksum(context) + + /** + * Applies a checksum based on the requirements and limitations of [FlexibleChecksumsRequestInterceptor] + */ + private fun applyFlexibleChecksumsChecksum( context: ProtocolRequestInterceptorContext, checksum: String, ): HttpRequest { - val headerName = "x-amz-checksum-$checksumAlgorithmName".lowercase() - - val req = context.protocolRequest.toBuilder() - - if (!req.headers.contains(headerName)) { - req.header(headerName, checksum) - } - - return req.build() + val request = context.protocolRequest.toBuilder() + val checksumAlgorithm = resolveChecksumAlgorithm( + requestChecksumRequired, + requestChecksumCalculation, + requestChecksumAlgorithm, + context, + )!! + val checksumHeader = checksumAlgorithm.resolveChecksumAlgorithmHeaderName() + + request.headers[checksumHeader] = checksum + request.headers.removeAllChecksumHeadersExcept(checksumHeader) + context.executionContext.emitBusinessMetric(checksumAlgorithm.toBusinessMetric()) + + return request.build() } - // FIXME this duplicates the logic from aws-signing-common, but can't import from there due to circular import. - private val HttpBody.isEligibleForAwsChunkedStreaming: Boolean - get() = (this is HttpBody.SourceContent || this is HttpBody.ChannelContent) && - contentLength != null && - (isOneShot || contentLength!! > 65536 * 16) + override fun applyChecksum( + context: ProtocolRequestInterceptorContext, + checksum: String, + ): HttpRequest = applyFlexibleChecksumsChecksum(context, checksum) /** - * @return if the [HashFunction] is supported by flexible checksums + * Checks if a user provided a checksum for a request via an HTTP header. + * The header must start with "x-amz-checksum-" followed by the checksum algorithm's name. + * MD5 is not considered a supported checksum algorithm. */ - private val HashFunction.isSupported: Boolean get() = when (this) { - is Crc32, is Crc32c, is Sha256, is Sha1 -> true - else -> false - } + private fun HttpRequest.userProvidedChecksumHeader(logger: Logger) = headers + .names() + .firstOrNull { + it.startsWith("x-amz-checksum-", ignoreCase = true) && + !it.equals("x-amz-checksum-md5", ignoreCase = true).also { isMd5 -> + if (isMd5) { + logger.debug { "MD5 checksum was supplied via header, MD5 is not a supported algorithm, ignoring header" } + } + } + } /** * Removes all checksum headers except [headerName] * @param headerName the checksum header name to keep */ - private fun HeadersBuilder.removeAllChecksumHeadersExcept(headerName: String) { - names().forEach { name -> - if (name.startsWith("x-amz-checksum-") && name != headerName) { - remove(name) - } - } - } - - /** - * Convert an [HttpBody] with an underlying [HashingSource] or [HashingByteReadChannel] - * to a [CompletingSource] or [CompletingByteReadChannel], respectively. - */ - private fun HttpBody.toCompletingBody(deferred: CompletableDeferred) = when (this) { - is HttpBody.SourceContent -> CompletingSource(deferred, (readFrom() as HashingSource)).toHttpBody(contentLength) - is HttpBody.ChannelContent -> CompletingByteReadChannel(deferred, (readFrom() as HashingByteReadChannel)).toHttpBody(contentLength) - else -> throw ClientException("HttpBody type is not supported") - } - - /** - * An [SdkSource] which uses the underlying [hashingSource]'s checksum to complete a [CompletableDeferred] value. - */ - internal class CompletingSource( - private val deferred: CompletableDeferred, - private val hashingSource: HashingSource, - ) : SdkSource by hashingSource { - override fun read(sink: SdkBuffer, limit: Long): Long = hashingSource.read(sink, limit) - .also { - if (it == -1L) { - deferred.complete(hashingSource.digest().encodeBase64String()) - } - } - } - - /** - * An [SdkByteReadChannel] which uses the underlying [hashingChannel]'s checksum to complete a [CompletableDeferred] value. - */ - internal class CompletingByteReadChannel( - private val deferred: CompletableDeferred, - private val hashingChannel: HashingByteReadChannel, - ) : SdkByteReadChannel by hashingChannel { - override suspend fun read(sink: SdkBuffer, limit: Long): Long = hashingChannel.read(sink, limit) - .also { - if (it == -1L) { - deferred.complete(hashingChannel.digest().encodeBase64String()) - } - } - } - - /** - * Compute the rolling hash of an [SdkByteReadChannel] using [hashFunction], reading up-to [bufferSize] bytes into memory - * @return a ByteArray of the hash function's digest - */ - private suspend fun SdkByteReadChannel.rollingHash(hashFunction: HashFunction, bufferSize: Long = 8192): ByteArray { - val buffer = SdkBuffer() - while (!isClosedForRead) { - read(buffer, bufferSize) - hashFunction.update(buffer.readToByteArray()) - } - return hashFunction.digest() - } + private fun HeadersBuilder.removeAllChecksumHeadersExcept(headerName: String) = + names() + .filter { it.startsWith("x-amz-checksum-", ignoreCase = true) && !it.equals(headerName, ignoreCase = true) } + .forEach { remove(it) } } diff --git a/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsResponseInterceptor.kt b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsResponseInterceptor.kt index 2e43fe1469..eceb19e739 100644 --- a/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsResponseInterceptor.kt +++ b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsResponseInterceptor.kt @@ -8,10 +8,11 @@ package aws.smithy.kotlin.runtime.http.interceptors import aws.smithy.kotlin.runtime.ClientException import aws.smithy.kotlin.runtime.InternalApi import aws.smithy.kotlin.runtime.client.ProtocolResponseInterceptorContext -import aws.smithy.kotlin.runtime.client.RequestInterceptorContext +import aws.smithy.kotlin.runtime.client.config.ResponseHttpChecksumConfig import aws.smithy.kotlin.runtime.collections.AttributeKey -import aws.smithy.kotlin.runtime.hashing.toHashFunction +import aws.smithy.kotlin.runtime.hashing.toHashFunctionOrThrow import aws.smithy.kotlin.runtime.http.HttpBody +import aws.smithy.kotlin.runtime.http.readAll import aws.smithy.kotlin.runtime.http.request.HttpRequest import aws.smithy.kotlin.runtime.http.response.HttpResponse import aws.smithy.kotlin.runtime.http.response.copy @@ -31,60 +32,87 @@ internal val CHECKSUM_HEADER_VALIDATION_PRIORITY_LIST: List = listOf( ) /** - * Validate a response's checksum. + * Handles response checksums. + * + * If it's a streaming response, it wraps the response in a hashing body, calculating the checksum as the response is + * streamed to the user. The checksum is validated after the user has consumed the entire body using a checksum validating body. + * Otherwise, the checksum if calculated all at once. * - * Wraps the response in a hashing body, calculating the checksum as the response is streamed to the user. - * The checksum is validated after the user has consumed the entire body using a checksum validating body. * Users can check which checksum was validated by referencing the `ResponseChecksumValidated` execution context variable. * - * @param shouldValidateResponseChecksumInitializer A function which uses the input [I] to return whether response checksum validation should occur + * @param responseValidationRequired Model sourced flag indicating if the checksum validation is mandatory. + * @param responseChecksumValidation Configuration option that determines when checksum validation should be done. */ - @InternalApi -public class FlexibleChecksumsResponseInterceptor( - private val shouldValidateResponseChecksumInitializer: (input: I) -> Boolean, +public open class FlexibleChecksumsResponseInterceptor( + private val responseValidationRequired: Boolean, + private val responseChecksumValidation: ResponseHttpChecksumConfig?, ) : HttpInterceptor { - - private var shouldValidateResponseChecksum: Boolean = false - @InternalApi public companion object { // The name of the checksum header which was validated. If `null`, validation was not performed. public val ChecksumHeaderValidated: AttributeKey = AttributeKey("ChecksumHeaderValidated") } - override fun readBeforeSerialization(context: RequestInterceptorContext) { - @Suppress("UNCHECKED_CAST") - val input = context.request as I - shouldValidateResponseChecksum = shouldValidateResponseChecksumInitializer(input) - } - override suspend fun modifyBeforeDeserialization(context: ProtocolResponseInterceptorContext): HttpResponse { - if (!shouldValidateResponseChecksum) { - return context.protocolResponse - } + val configuredToVerifyChecksum = responseValidationRequired || responseChecksumValidation == ResponseHttpChecksumConfig.WHEN_SUPPORTED + if (!configuredToVerifyChecksum) return context.protocolResponse - val logger = coroutineContext.logger>() + val logger = coroutineContext.logger() val checksumHeader = CHECKSUM_HEADER_VALIDATION_PRIORITY_LIST .firstOrNull { context.protocolResponse.headers.contains(it) } ?: run { - logger.warn { "User requested checksum validation, but the response headers did not contain any valid checksums" } + logger.warn { "Checksum validation was requested but the response headers didn't contain a valid checksum." } return context.protocolResponse } - // let the user know which checksum will be validated - logger.debug { "Validating checksum from $checksumHeader" } - context.executionContext[ChecksumHeaderValidated] = checksumHeader + val serviceChecksumValue = context.protocolResponse.headers[checksumHeader]!! + if (ignoreChecksum(serviceChecksumValue, context)) { + return context.protocolResponse + } - val checksumAlgorithm = checksumHeader.removePrefix("x-amz-checksum-").toHashFunction() ?: throw ClientException("could not parse checksum algorithm from header $checksumHeader") + context.executionContext[ChecksumHeaderValidated] = checksumHeader - // Wrap the response body in a hashing body - return context.protocolResponse.copy( - body = context.protocolResponse.body - .toHashingBody(checksumAlgorithm, context.protocolResponse.body.contentLength) - .toChecksumValidatingBody(context.protocolResponse.headers[checksumHeader]!!), - ) + val checksumAlgorithm = checksumHeader + .removePrefix("x-amz-checksum-") + .toHashFunctionOrThrow() + + when (val bodyType = context.protocolResponse.body) { + is HttpBody.Bytes -> { + logger.debug { "Validating checksum before deserialization from $checksumHeader" } + + checksumAlgorithm.update( + context.protocolResponse.body.readAll() ?: byteArrayOf(), + ) + val sdkChecksumValue = checksumAlgorithm.digest().encodeBase64String() + + validateAndThrow( + serviceChecksumValue, + sdkChecksumValue, + ) + + return context.protocolResponse + } + is HttpBody.SourceContent, is HttpBody.ChannelContent -> { + logger.debug { "Validating checksum after deserialization from $checksumHeader" } + + return context.protocolResponse.copy( + body = context.protocolResponse.body + .toHashingBody(checksumAlgorithm, context.protocolResponse.body.contentLength) + .toChecksumValidatingBody(serviceChecksumValue), + ) + } + else -> throw IllegalStateException("HTTP body type '$bodyType' is not supported for flexible checksums.") + } } + + /** + * Additional check on the checksum itself to see if it should be validated + */ + public open fun ignoreChecksum( + checksum: String, + context: ProtocolResponseInterceptorContext, + ): Boolean = false } public class ChecksumMismatchException(message: String?) : ClientException(message) diff --git a/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/HttpChecksumRequiredInterceptor.kt b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/HttpChecksumRequiredInterceptor.kt new file mode 100644 index 0000000000..f422de7fa7 --- /dev/null +++ b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/HttpChecksumRequiredInterceptor.kt @@ -0,0 +1,98 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package aws.smithy.kotlin.runtime.http.interceptors + +import aws.smithy.kotlin.runtime.InternalApi +import aws.smithy.kotlin.runtime.client.ProtocolRequestInterceptorContext +import aws.smithy.kotlin.runtime.hashing.* +import aws.smithy.kotlin.runtime.http.* +import aws.smithy.kotlin.runtime.http.request.HttpRequest +import aws.smithy.kotlin.runtime.http.request.header +import aws.smithy.kotlin.runtime.http.request.toBuilder +import aws.smithy.kotlin.runtime.io.rollingHash +import aws.smithy.kotlin.runtime.telemetry.logging.logger +import aws.smithy.kotlin.runtime.text.encoding.encodeBase64String +import kotlin.coroutines.coroutineContext + +/** + * Handles checksum request calculation from the `httpChecksumRequired` trait. + */ +@InternalApi +public class HttpChecksumRequiredInterceptor : CachingChecksumInterceptor() { + override suspend fun modifyBeforeSigning(context: ProtocolRequestInterceptorContext): HttpRequest { + if (context.defaultChecksumAlgorithmName == null) { + // Don't calculate checksum + return context.protocolRequest + } + + val checksumAlgorithmName = context.defaultChecksumAlgorithmName!! + val checksumAlgorithm = checksumAlgorithmName.toHashFunctionOrThrow() + + return if (context.protocolRequest.body.isEligibleForAwsChunkedStreaming) { + coroutineContext.logger().debug { + "Calculating checksum during transmission using: ${checksumAlgorithm::class.simpleName}" + } + calculateAwsChunkedStreamingChecksum(context, checksumAlgorithm) + } else { + if (context.protocolRequest.body is HttpBody.Bytes) { + // Cache checksum + super.modifyBeforeSigning(context) + } else { + val checksum = calculateHttpChecksumRequiredChecksum(context) + applyHttpChecksumRequiredChecksum(context, checksum) + } + } + } + + /** + * Calculates a checksum based on the requirements and limitations of [HttpChecksumRequiredInterceptor] + */ + private suspend fun calculateHttpChecksumRequiredChecksum( + context: ProtocolRequestInterceptorContext, + ): String { + val req = context.protocolRequest.toBuilder() + val checksumAlgorithmName = context.defaultChecksumAlgorithmName!! + val checksumAlgorithm = checksumAlgorithmName.toHashFunctionOrThrow() + + return when { + req.body.contentLength == null && !req.body.isOneShot -> { + val channel = req.body.toSdkByteReadChannel()!! + channel.rollingHash(checksumAlgorithm).encodeBase64String() + } + else -> { + val bodyBytes = req.body.readAll() ?: byteArrayOf() + if (req.body.isOneShot) req.body = bodyBytes.toHttpBody() + bodyBytes.hash(checksumAlgorithm).encodeBase64String() + } + } + } + + public override suspend fun calculateChecksum( + context: ProtocolRequestInterceptorContext, + ): String? = + calculateHttpChecksumRequiredChecksum(context) + + /** + * Applies a checksum based on the requirements and limitations of [HttpChecksumRequiredInterceptor] + */ + private fun applyHttpChecksumRequiredChecksum( + context: ProtocolRequestInterceptorContext, + checksum: String, + ): HttpRequest { + val checksumAlgorithmName = context.defaultChecksumAlgorithmName!! + val checksumHeader = checksumAlgorithmName.resolveChecksumAlgorithmHeaderName() + val request = context.protocolRequest.toBuilder() + + request.header(checksumHeader, checksum) + return request.build() + } + + public override fun applyChecksum( + context: ProtocolRequestInterceptorContext, + checksum: String, + ): HttpRequest = + applyHttpChecksumRequiredChecksum(context, checksum) +} diff --git a/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/Md5ChecksumInterceptor.kt b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/Md5ChecksumInterceptor.kt deleted file mode 100644 index cbe6bcabe2..0000000000 --- a/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/interceptors/Md5ChecksumInterceptor.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package aws.smithy.kotlin.runtime.http.interceptors - -import aws.smithy.kotlin.runtime.InternalApi -import aws.smithy.kotlin.runtime.client.ProtocolRequestInterceptorContext -import aws.smithy.kotlin.runtime.hashing.md5 -import aws.smithy.kotlin.runtime.http.HttpBody -import aws.smithy.kotlin.runtime.http.request.HttpRequest -import aws.smithy.kotlin.runtime.http.request.header -import aws.smithy.kotlin.runtime.http.request.toBuilder -import aws.smithy.kotlin.runtime.text.encoding.encodeBase64String - -/** - * Set the `Content-MD5` header based on the current payload - * See: - * - https://awslabs.github.io/smithy/1.0/spec/core/behavior-traits.html#httpchecksumrequired-trait - * - https://datatracker.ietf.org/doc/html/rfc1864.html - * @param block An optional function which parses the input [I] to determine if the `Content-MD5` header should be set. - * If not provided, the default behavior will set the header. - */ -@InternalApi -public class Md5ChecksumInterceptor( - private val block: ((input: I) -> Boolean)? = null, -) : AbstractChecksumInterceptor() { - override suspend fun modifyBeforeSigning(context: ProtocolRequestInterceptorContext): HttpRequest { - @Suppress("UNCHECKED_CAST") - val input = context.request as I - - val injectMd5Header = block?.invoke(input) ?: true - if (!injectMd5Header) { - return context.protocolRequest - } - - return super.modifyBeforeSigning(context) - } - - public override suspend fun calculateChecksum(context: ProtocolRequestInterceptorContext): String? = - when (val body = context.protocolRequest.body) { - is HttpBody.Bytes -> body.bytes().md5().encodeBase64String() - else -> null - } - - public override fun applyChecksum(context: ProtocolRequestInterceptorContext, checksum: String): HttpRequest { - val req = context.protocolRequest.toBuilder() - if (!req.headers.contains("Content-MD5")) { - req.header("Content-MD5", checksum) - } - return req.build() - } -} diff --git a/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/operation/HttpOperationContext.kt b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/operation/HttpOperationContext.kt index 524614a667..56444dca98 100644 --- a/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/operation/HttpOperationContext.kt +++ b/runtime/protocol/http-client/common/src/aws/smithy/kotlin/runtime/http/operation/HttpOperationContext.kt @@ -66,9 +66,9 @@ public object HttpOperationContext { public val ClockSkewApproximateSigningTime: AttributeKey = AttributeKey("aws.smithy.kotlin#ClockSkewApproximateSigningTime") /** - * The name of the algorithm to be used for computing a checksum of the request. + * The name of the default algorithm to be used for computing a checksum of the request. */ - public val ChecksumAlgorithm: AttributeKey = AttributeKey("aws.smithy.kotlin#ChecksumAlgorithm") + public val DefaultChecksumAlgorithm: AttributeKey = AttributeKey("aws.smithy.kotlin#DefaultChecksumAlgorithm") } internal val ExecutionContext.operationMetrics: OperationMetrics diff --git a/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/AbstractChecksumInterceptorTest.kt b/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/CachingChecksumInterceptorTest.kt similarity index 88% rename from runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/AbstractChecksumInterceptorTest.kt rename to runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/CachingChecksumInterceptorTest.kt index 3de8e557c2..cee1db7766 100644 --- a/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/AbstractChecksumInterceptorTest.kt +++ b/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/CachingChecksumInterceptorTest.kt @@ -6,7 +6,7 @@ import aws.smithy.kotlin.runtime.client.ProtocolRequestInterceptorContext import aws.smithy.kotlin.runtime.collections.get import aws.smithy.kotlin.runtime.http.HttpBody import aws.smithy.kotlin.runtime.http.SdkHttpClient -import aws.smithy.kotlin.runtime.http.interceptors.AbstractChecksumInterceptor +import aws.smithy.kotlin.runtime.http.interceptors.CachingChecksumInterceptor import aws.smithy.kotlin.runtime.http.operation.HttpOperationContext import aws.smithy.kotlin.runtime.http.operation.newTestOperation import aws.smithy.kotlin.runtime.http.operation.roundTrip @@ -21,7 +21,7 @@ import kotlin.test.assertEquals private val CHECKSUM_TEST_HEADER = "x-amz-kotlin-sdk-test-checksum-header" -class AbstractChecksumInterceptorTest { +class CachingChecksumInterceptorTest { private val client = SdkHttpClient(TestEngine()) @Test @@ -33,7 +33,7 @@ class AbstractChecksumInterceptorTest { val op = newTestOperation(req, Unit) - op.interceptors.add(TestAbstractChecksumInterceptor(expectedChecksumValue)) + op.interceptors.add(TestCachingChecksumInterceptor(expectedChecksumValue)) op.roundTrip(client, Unit) val call = op.context.attributes[HttpOperationContext.HttpCallList].first() @@ -49,16 +49,16 @@ class AbstractChecksumInterceptorTest { val op = newTestOperation(req, Unit) - op.interceptors.add(TestAbstractChecksumInterceptor(expectedChecksumValue)) + op.interceptors.add(TestCachingChecksumInterceptor(expectedChecksumValue)) // the TestAbstractChecksumInterceptor will throw an exception if calculateChecksum is called more than once. op.roundTrip(client, Unit) op.roundTrip(client, Unit) } - inner class TestAbstractChecksumInterceptor( + inner class TestCachingChecksumInterceptor( private val expectedChecksum: String?, - ) : AbstractChecksumInterceptor() { + ) : CachingChecksumInterceptor() { private var alreadyCalculatedChecksum = false override suspend fun calculateChecksum(context: ProtocolRequestInterceptorContext): String? { diff --git a/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsRequestInterceptorTest.kt b/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsRequestInterceptorTest.kt index c4c85de66b..37e5054865 100644 --- a/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsRequestInterceptorTest.kt +++ b/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsRequestInterceptorTest.kt @@ -6,6 +6,7 @@ package aws.smithy.kotlin.runtime.http.interceptors import aws.smithy.kotlin.runtime.ClientException +import aws.smithy.kotlin.runtime.client.config.RequestHttpChecksumConfig import aws.smithy.kotlin.runtime.collections.get import aws.smithy.kotlin.runtime.hashing.toHashFunction import aws.smithy.kotlin.runtime.http.* @@ -41,9 +42,11 @@ class FlexibleChecksumsRequestInterceptorTest { val op = newTestOperation(req, Unit) op.interceptors.add( - FlexibleChecksumsRequestInterceptor { - checksumAlgorithmName - }, + FlexibleChecksumsRequestInterceptor( + requestChecksumAlgorithm = checksumAlgorithmName, + requestChecksumRequired = true, + requestChecksumCalculation = RequestHttpChecksumConfig.WHEN_SUPPORTED, + ), ) op.roundTrip(client, Unit) @@ -65,10 +68,13 @@ class FlexibleChecksumsRequestInterceptorTest { val op = newTestOperation(req, Unit) + op.context.attributes[HttpOperationContext.DefaultChecksumAlgorithm] = "CRC32" op.interceptors.add( - FlexibleChecksumsRequestInterceptor { - checksumAlgorithmName - }, + FlexibleChecksumsRequestInterceptor( + requestChecksumAlgorithm = checksumAlgorithmName, + requestChecksumRequired = true, + requestChecksumCalculation = RequestHttpChecksumConfig.WHEN_SUPPORTED, + ), ) op.roundTrip(client, Unit) @@ -87,13 +93,14 @@ class FlexibleChecksumsRequestInterceptorTest { val op = newTestOperation(req, Unit) - op.interceptors.add( - FlexibleChecksumsRequestInterceptor { - unsupportedChecksumAlgorithmName - }, - ) - assertFailsWith { + op.interceptors.add( + FlexibleChecksumsRequestInterceptor( + requestChecksumAlgorithm = unsupportedChecksumAlgorithmName, + requestChecksumRequired = true, + requestChecksumCalculation = RequestHttpChecksumConfig.WHEN_SUPPORTED, + ), + ) op.roundTrip(client, Unit) } } @@ -115,9 +122,11 @@ class FlexibleChecksumsRequestInterceptorTest { val op = newTestOperation(req, Unit) op.interceptors.add( - FlexibleChecksumsRequestInterceptor { - checksumAlgorithmName - }, + FlexibleChecksumsRequestInterceptor( + requestChecksumAlgorithm = checksumAlgorithmName, + requestChecksumRequired = true, + requestChecksumCalculation = RequestHttpChecksumConfig.WHEN_SUPPORTED, + ), ) op.roundTrip(client, Unit) @@ -126,23 +135,6 @@ class FlexibleChecksumsRequestInterceptorTest { assertEquals(0, call.request.headers.getNumChecksumHeaders()) } - @Test - fun itSetsChecksumHeaderViaExecutionContext() = runTest { - checksums.forEach { (checksumAlgorithmName, expectedChecksumValue) -> - val req = HttpRequestBuilder().apply { - body = HttpBody.fromBytes("bar".encodeToByteArray()) - } - - val op = newTestOperation(req, Unit) - op.context[HttpOperationContext.ChecksumAlgorithm] = checksumAlgorithmName - op.interceptors.add(FlexibleChecksumsRequestInterceptor()) - - op.roundTrip(client, Unit) - val call = op.context.attributes[HttpOperationContext.HttpCallList].first() - assertEquals(expectedChecksumValue, call.request.headers["x-amz-checksum-$checksumAlgorithmName"]) - } - } - @Test fun testCompletingSource() = runTest { val hashFunctionName = "crc32" @@ -151,7 +143,7 @@ class FlexibleChecksumsRequestInterceptorTest { val source = byteArray.source() val completableDeferred = CompletableDeferred() val hashingSource = HashingSource(hashFunctionName.toHashFunction()!!, source) - val completingSource = FlexibleChecksumsRequestInterceptor.CompletingSource(completableDeferred, hashingSource) + val completingSource = CompletingSource(completableDeferred, hashingSource) completingSource.read(SdkBuffer(), 1L) assertFalse(completableDeferred.isCompleted) // deferred value should not be completed because the source is not exhausted @@ -172,7 +164,8 @@ class FlexibleChecksumsRequestInterceptorTest { val channel = SdkByteReadChannel(byteArray) val completableDeferred = CompletableDeferred() val hashingChannel = HashingByteReadChannel(hashFunctionName.toHashFunction()!!, channel) - val completingChannel = FlexibleChecksumsRequestInterceptor.CompletingByteReadChannel(completableDeferred, hashingChannel) + val completingChannel = + CompletingByteReadChannel(completableDeferred, hashingChannel) completingChannel.read(SdkBuffer(), 1L) assertFalse(completableDeferred.isCompleted) @@ -198,9 +191,11 @@ class FlexibleChecksumsRequestInterceptorTest { val op = newTestOperation(req, Unit) op.interceptors.add( - FlexibleChecksumsRequestInterceptor { - checksumAlgorithmName - }, + FlexibleChecksumsRequestInterceptor( + requestChecksumAlgorithm = checksumAlgorithmName, + requestChecksumRequired = true, + requestChecksumCalculation = RequestHttpChecksumConfig.WHEN_SUPPORTED, + ), ) op.roundTrip(client, Unit) @@ -210,5 +205,51 @@ class FlexibleChecksumsRequestInterceptorTest { assertEquals(precalculatedChecksumValue, call.request.headers["x-amz-checksum-sha256"]) } + @Test + fun testDefaultChecksumConfiguration() = runTest { + setOf( + DefaultChecksumTest(true, RequestHttpChecksumConfig.WHEN_SUPPORTED, true), + DefaultChecksumTest(true, RequestHttpChecksumConfig.WHEN_REQUIRED, true), + DefaultChecksumTest(false, RequestHttpChecksumConfig.WHEN_SUPPORTED, true), + DefaultChecksumTest(false, RequestHttpChecksumConfig.WHEN_REQUIRED, false), + ).forEach { runDefaultChecksumTest(it) } + } + private fun Headers.getNumChecksumHeaders(): Int = entries().count { (name, _) -> name.startsWith("x-amz-checksum-") } + + private data class DefaultChecksumTest( + val requestChecksumRequired: Boolean, + val requestChecksumCalculation: RequestHttpChecksumConfig, + val defaultChecksumExpected: Boolean, + ) + + private fun runDefaultChecksumTest( + testCase: DefaultChecksumTest, + ) = runTest { + val defaultChecksumAlgorithmName = "crc32" + val expectedChecksumValue = "WdqXHQ==" + + val req = HttpRequestBuilder().apply { + body = HttpBody.fromBytes("bar".encodeToByteArray()) + } + + val op = newTestOperation(req, Unit) + + op.context.attributes[HttpOperationContext.DefaultChecksumAlgorithm] = "CRC32" + op.interceptors.add( + FlexibleChecksumsRequestInterceptor( + requestChecksumAlgorithm = null, // See if default checksum is applied + requestChecksumRequired = testCase.requestChecksumRequired, + requestChecksumCalculation = testCase.requestChecksumCalculation, + ), + ) + + op.roundTrip(client, Unit) + val call = op.context.attributes[HttpOperationContext.HttpCallList].first() + + when (testCase.defaultChecksumExpected) { + true -> assertEquals(expectedChecksumValue, call.request.headers["x-amz-checksum-$defaultChecksumAlgorithmName"]) + false -> assertFalse { call.request.headers.contains("x-amz-checksum-$defaultChecksumAlgorithmName") } + } + } } diff --git a/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsResponseInterceptorTest.kt b/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsResponseInterceptorTest.kt index 2c04ee680d..e465499ad1 100644 --- a/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsResponseInterceptorTest.kt +++ b/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsResponseInterceptorTest.kt @@ -5,6 +5,7 @@ package aws.smithy.kotlin.runtime.http.interceptors +import aws.smithy.kotlin.runtime.client.config.ResponseHttpChecksumConfig import aws.smithy.kotlin.runtime.collections.get import aws.smithy.kotlin.runtime.http.* import aws.smithy.kotlin.runtime.http.HttpCall @@ -73,9 +74,10 @@ class FlexibleChecksumsResponseInterceptorTest { val op = newTestOperation(req) op.interceptors.add( - FlexibleChecksumsResponseInterceptor { - true - }, + FlexibleChecksumsResponseInterceptor( + responseValidationRequired = true, + responseChecksumValidation = ResponseHttpChecksumConfig.WHEN_SUPPORTED, + ), ) val responseChecksumHeaderName = "x-amz-checksum-$checksumAlgorithmName" @@ -99,9 +101,10 @@ class FlexibleChecksumsResponseInterceptorTest { val op = newTestOperation(req) op.interceptors.add( - FlexibleChecksumsResponseInterceptor { - true - }, + FlexibleChecksumsResponseInterceptor( + responseValidationRequired = true, + responseChecksumValidation = ResponseHttpChecksumConfig.WHEN_SUPPORTED, + ), ) val responseChecksumHeaderName = "x-amz-checksum-$checksumAlgorithmName" @@ -126,9 +129,10 @@ class FlexibleChecksumsResponseInterceptorTest { val op = newTestOperation(req) op.interceptors.add( - FlexibleChecksumsResponseInterceptor { - true - }, + FlexibleChecksumsResponseInterceptor( + responseValidationRequired = true, + responseChecksumValidation = ResponseHttpChecksumConfig.WHEN_SUPPORTED, + ), ) val responseHeaders = Headers { @@ -150,9 +154,10 @@ class FlexibleChecksumsResponseInterceptorTest { val op = newTestOperation(req) op.interceptors.add( - FlexibleChecksumsResponseInterceptor { - true - }, + FlexibleChecksumsResponseInterceptor( + responseValidationRequired = true, + responseChecksumValidation = ResponseHttpChecksumConfig.WHEN_SUPPORTED, + ), ) val responseHeaders = Headers { @@ -170,9 +175,10 @@ class FlexibleChecksumsResponseInterceptorTest { val op = newTestOperation(req) op.interceptors.add( - FlexibleChecksumsResponseInterceptor { - false - }, + FlexibleChecksumsResponseInterceptor( + responseValidationRequired = false, + responseChecksumValidation = ResponseHttpChecksumConfig.WHEN_REQUIRED, + ), ) val responseChecksumHeaderName = "x-amz-checksum-crc32" @@ -188,4 +194,52 @@ class FlexibleChecksumsResponseInterceptorTest { assertNull(op.context.getOrNull(ChecksumHeaderValidated)) } + + @Test + fun testResponseValidationConfiguration() = runTest { + setOf( + ResponseChecksumValidationTest(true, ResponseHttpChecksumConfig.WHEN_SUPPORTED, true), + ResponseChecksumValidationTest(true, ResponseHttpChecksumConfig.WHEN_REQUIRED, true), + ResponseChecksumValidationTest(false, ResponseHttpChecksumConfig.WHEN_SUPPORTED, true), + ResponseChecksumValidationTest(false, ResponseHttpChecksumConfig.WHEN_REQUIRED, false), + ).forEach { runResponseChecksumValidationTest(it) } + } + + private data class ResponseChecksumValidationTest( + val responseValidationRequired: Boolean, + val responseChecksumValidation: ResponseHttpChecksumConfig, + val checksumValidationExpected: Boolean, + ) + + private fun runResponseChecksumValidationTest( + testCase: ResponseChecksumValidationTest, + ) = runTest { + checksums.forEach { (checksumAlgorithmName, expectedChecksum) -> + val req = HttpRequestBuilder() + val op = newTestOperation(req) + + op.interceptors.add( + FlexibleChecksumsResponseInterceptor( + responseValidationRequired = testCase.responseValidationRequired, + responseChecksumValidation = testCase.responseChecksumValidation, + ), + ) + + val responseChecksumHeaderName = "x-amz-checksum-$checksumAlgorithmName" + + val responseHeaders = Headers { + append(responseChecksumHeaderName, expectedChecksum) + } + + val client = getMockClient(response, responseHeaders) + + val output = op.roundTrip(client, TestInput("input")) + output.body.readAll() + + when (testCase.checksumValidationExpected) { + true -> assertEquals(responseChecksumHeaderName, op.context[ChecksumHeaderValidated]) + false -> assertNull(op.context.getOrNull(ChecksumHeaderValidated)) + } + } + } } diff --git a/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/Md5ChecksumInterceptorTest.kt b/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/HttpChecksumRequiredInterceptorTest.kt similarity index 62% rename from runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/Md5ChecksumInterceptorTest.kt rename to runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/HttpChecksumRequiredInterceptorTest.kt index 109d2e3e8d..df2cf49f0e 100644 --- a/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/Md5ChecksumInterceptorTest.kt +++ b/runtime/protocol/http-client/common/test/aws/smithy/kotlin/runtime/http/interceptors/HttpChecksumRequiredInterceptorTest.kt @@ -6,6 +6,7 @@ package aws.smithy.kotlin.runtime.http.interceptors import aws.smithy.kotlin.runtime.collections.get +import aws.smithy.kotlin.runtime.hashing.Crc32 import aws.smithy.kotlin.runtime.http.HttpBody import aws.smithy.kotlin.runtime.http.SdkHttpClient import aws.smithy.kotlin.runtime.http.operation.HttpOperationContext @@ -14,12 +15,13 @@ import aws.smithy.kotlin.runtime.http.operation.roundTrip import aws.smithy.kotlin.runtime.http.request.HttpRequestBuilder import aws.smithy.kotlin.runtime.httptest.TestEngine import aws.smithy.kotlin.runtime.io.SdkByteReadChannel +import aws.smithy.kotlin.runtime.text.encoding.encodeBase64String import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNull -class Md5ChecksumInterceptorTest { +class HttpChecksumRequiredInterceptorTest { private val client = SdkHttpClient(TestEngine()) @Test @@ -29,10 +31,9 @@ class Md5ChecksumInterceptorTest { } val op = newTestOperation(req, Unit) + op.context.attributes[HttpOperationContext.DefaultChecksumAlgorithm] = "MD5" op.interceptors.add( - Md5ChecksumInterceptor { - true - }, + HttpChecksumRequiredInterceptor(), ) val expected = "RG22oBSZFmabBbkzVGRi4w==" @@ -42,7 +43,30 @@ class Md5ChecksumInterceptorTest { } @Test - fun itOnlySetsHeaderForBytesContent() = runTest { + fun itSetsContentCrc32Header() = runTest { + val testBody = "bar".encodeToByteArray() + + val req = HttpRequestBuilder().apply { + body = HttpBody.fromBytes(testBody) + } + val op = newTestOperation(req, Unit) + + op.context.attributes[HttpOperationContext.DefaultChecksumAlgorithm] = "CRC32" + op.interceptors.add( + HttpChecksumRequiredInterceptor(), + ) + + val crc32 = Crc32() + crc32.update(testBody) + val expected = crc32.digest().encodeBase64String() + + op.roundTrip(client, Unit) + val call = op.context.attributes[HttpOperationContext.HttpCallList].first() + assertEquals(expected, call.request.headers["x-amz-checksum-crc32"]) + } + + @Test + fun itSetsHeaderForNonBytesContent() = runTest { val req = HttpRequestBuilder().apply { body = object : HttpBody.ChannelContent() { override fun readFrom(): SdkByteReadChannel = SdkByteReadChannel("fooey".encodeToByteArray()) @@ -50,15 +74,15 @@ class Md5ChecksumInterceptorTest { } val op = newTestOperation(req, Unit) + op.context.attributes[HttpOperationContext.DefaultChecksumAlgorithm] = "MD5" op.interceptors.add( - Md5ChecksumInterceptor { - true - }, + HttpChecksumRequiredInterceptor(), ) + val expected = "vJLiaOiNxaxdWfYAYzdzFQ==" op.roundTrip(client, Unit) val call = op.context.attributes[HttpOperationContext.HttpCallList].first() - assertNull(call.request.headers["Content-MD5"]) + assertEquals(expected, call.request.headers["Content-MD5"]) } @Test @@ -69,9 +93,7 @@ class Md5ChecksumInterceptorTest { val op = newTestOperation(req, Unit) op.interceptors.add( - Md5ChecksumInterceptor { - false // interceptor disabled - }, + HttpChecksumRequiredInterceptor(), ) op.roundTrip(client, Unit) diff --git a/runtime/protocol/http/api/http.api b/runtime/protocol/http/api/http.api index 3f4c29414f..fb338e64d9 100644 --- a/runtime/protocol/http/api/http.api +++ b/runtime/protocol/http/api/http.api @@ -1,3 +1,19 @@ +public final class aws/smithy/kotlin/runtime/http/CompletingByteReadChannel : aws/smithy/kotlin/runtime/io/SdkByteReadChannel { + public fun (Lkotlinx/coroutines/CompletableDeferred;Laws/smithy/kotlin/runtime/io/HashingByteReadChannel;)V + public fun cancel (Ljava/lang/Throwable;)Z + public fun getAvailableForRead ()I + public fun getClosedCause ()Ljava/lang/Throwable; + public fun isClosedForRead ()Z + public fun isClosedForWrite ()Z + public fun read (Laws/smithy/kotlin/runtime/io/SdkBuffer;JLkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + +public final class aws/smithy/kotlin/runtime/http/CompletingSource : aws/smithy/kotlin/runtime/io/SdkSource { + public fun (Lkotlinx/coroutines/CompletableDeferred;Laws/smithy/kotlin/runtime/io/HashingSource;)V + public fun close ()V + public fun read (Laws/smithy/kotlin/runtime/io/SdkBuffer;J)J +} + public abstract interface class aws/smithy/kotlin/runtime/http/DeferredHeaders : aws/smithy/kotlin/runtime/collections/ValuesMap { public static final field Companion Laws/smithy/kotlin/runtime/http/DeferredHeaders$Companion; } @@ -86,8 +102,10 @@ public abstract class aws/smithy/kotlin/runtime/http/HttpBody$SourceContent : aw } public final class aws/smithy/kotlin/runtime/http/HttpBodyKt { + public static final fun isEligibleForAwsChunkedStreaming (Laws/smithy/kotlin/runtime/http/HttpBody;)Z public static final fun readAll (Laws/smithy/kotlin/runtime/http/HttpBody;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun toByteStream (Laws/smithy/kotlin/runtime/http/HttpBody;)Laws/smithy/kotlin/runtime/content/ByteStream; + public static final fun toCompletingBody (Laws/smithy/kotlin/runtime/http/HttpBody;Lkotlinx/coroutines/CompletableDeferred;)Laws/smithy/kotlin/runtime/http/HttpBody; public static final fun toHashingBody (Laws/smithy/kotlin/runtime/http/HttpBody;Laws/smithy/kotlin/runtime/hashing/HashFunction;Ljava/lang/Long;)Laws/smithy/kotlin/runtime/http/HttpBody; public static final fun toHttpBody (Laws/smithy/kotlin/runtime/content/ByteStream;)Laws/smithy/kotlin/runtime/http/HttpBody; public static final fun toHttpBody (Laws/smithy/kotlin/runtime/io/SdkByteReadChannel;Ljava/lang/Long;)Laws/smithy/kotlin/runtime/http/HttpBody; diff --git a/runtime/protocol/http/common/src/aws/smithy/kotlin/runtime/http/HttpBody.kt b/runtime/protocol/http/common/src/aws/smithy/kotlin/runtime/http/HttpBody.kt index b86c5c2199..a12626a656 100644 --- a/runtime/protocol/http/common/src/aws/smithy/kotlin/runtime/http/HttpBody.kt +++ b/runtime/protocol/http/common/src/aws/smithy/kotlin/runtime/http/HttpBody.kt @@ -10,6 +10,8 @@ import aws.smithy.kotlin.runtime.content.ByteStream import aws.smithy.kotlin.runtime.hashing.HashFunction import aws.smithy.kotlin.runtime.http.content.ByteArrayContent import aws.smithy.kotlin.runtime.io.* +import aws.smithy.kotlin.runtime.text.encoding.encodeBase64String +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope /** @@ -191,6 +193,49 @@ public fun HttpBody.toHashingBody( else -> throw ClientException("HttpBody type is not supported") } +/** + * Convert an [HttpBody] with an underlying [HashingSource] or [HashingByteReadChannel] + * to a [CompletingSource] or [CompletingByteReadChannel], respectively. + */ +@InternalApi +public fun HttpBody.toCompletingBody(deferred: CompletableDeferred): HttpBody = when (this) { + is HttpBody.SourceContent -> CompletingSource(deferred, (readFrom() as HashingSource)).toHttpBody(contentLength) + is HttpBody.ChannelContent -> CompletingByteReadChannel(deferred, (readFrom() as HashingByteReadChannel)).toHttpBody(contentLength) + else -> throw ClientException("HttpBody type is not supported") +} + +/** + * An [SdkSource] which uses the underlying [hashingSource]'s checksum to complete a [CompletableDeferred] value. + */ +@InternalApi +public class CompletingSource( + private val deferred: CompletableDeferred, + private val hashingSource: HashingSource, +) : SdkSource by hashingSource { + override fun read(sink: SdkBuffer, limit: Long): Long = hashingSource.read(sink, limit) + .also { + if (it == -1L) { + deferred.complete(hashingSource.digest().encodeBase64String()) + } + } +} + +/** + * An [SdkByteReadChannel] which uses the underlying [hashingChannel]'s checksum to complete a [CompletableDeferred] value. + */ +@InternalApi +public class CompletingByteReadChannel( + private val deferred: CompletableDeferred, + private val hashingChannel: HashingByteReadChannel, +) : SdkByteReadChannel by hashingChannel { + override suspend fun read(sink: SdkBuffer, limit: Long): Long = hashingChannel.read(sink, limit) + .also { + if (it == -1L) { + deferred.complete(hashingChannel.digest().encodeBase64String()) + } + } +} + // FIXME - replace/move to reading to SdkBuffer instead /** * Consume the [HttpBody] and pull the entire contents into memory as a [ByteArray]. @@ -244,3 +289,10 @@ public fun HttpBody.toSdkByteReadChannel(scope: CoroutineScope? = null): SdkByte is HttpBody.ChannelContent -> body.readFrom() is HttpBody.SourceContent -> body.readFrom().toSdkByteReadChannel(scope) } + +// FIXME this duplicates the logic from aws-signing-common +@InternalApi +public val HttpBody.isEligibleForAwsChunkedStreaming: Boolean + get() = (this is HttpBody.SourceContent || this is HttpBody.ChannelContent) && + contentLength != null && + (isOneShot || contentLength!! > 65536 * 16) diff --git a/runtime/runtime-core/api/runtime-core.api b/runtime/runtime-core/api/runtime-core.api index 8c42034110..6d39c3b417 100644 --- a/runtime/runtime-core/api/runtime-core.api +++ b/runtime/runtime-core/api/runtime-core.api @@ -104,6 +104,14 @@ public final class aws/smithy/kotlin/runtime/businessmetrics/BusinessMetricsUtil public final class aws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric : java/lang/Enum, aws/smithy/kotlin/runtime/businessmetrics/BusinessMetric { public static final field ACCOUNT_ID_BASED_ENDPOINT Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric; + public static final field FLEXIBLE_CHECKSUMS_REQ_CRC32 Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric; + public static final field FLEXIBLE_CHECKSUMS_REQ_CRC32C Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric; + public static final field FLEXIBLE_CHECKSUMS_REQ_SHA1 Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric; + public static final field FLEXIBLE_CHECKSUMS_REQ_SHA256 Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric; + public static final field FLEXIBLE_CHECKSUMS_REQ_WHEN_REQUIRED Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric; + public static final field FLEXIBLE_CHECKSUMS_REQ_WHEN_SUPPORTED Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric; + public static final field FLEXIBLE_CHECKSUMS_RES_WHEN_REQUIRED Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric; + public static final field FLEXIBLE_CHECKSUMS_RES_WHEN_SUPPORTED Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric; public static final field GZIP_REQUEST_COMPRESSION Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric; public static final field PAGINATOR Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric; public static final field PROTOCOL_RPC_V2_CBOR Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric; @@ -699,7 +707,12 @@ public final class aws/smithy/kotlin/runtime/hashing/HashFunction$DefaultImpls { public final class aws/smithy/kotlin/runtime/hashing/HashFunctionKt { public static final fun hash ([BLaws/smithy/kotlin/runtime/hashing/HashFunction;)[B public static final fun hash ([BLkotlin/jvm/functions/Function0;)[B + public static final fun isSupportedForFlexibleChecksums (Laws/smithy/kotlin/runtime/hashing/HashFunction;)Z + public static final fun resolveChecksumAlgorithmHeaderName (Laws/smithy/kotlin/runtime/hashing/HashFunction;)Ljava/lang/String; + public static final fun resolveChecksumAlgorithmHeaderName (Ljava/lang/String;)Ljava/lang/String; + public static final fun toBusinessMetric (Laws/smithy/kotlin/runtime/hashing/HashFunction;)Laws/smithy/kotlin/runtime/businessmetrics/BusinessMetric; public static final fun toHashFunction (Ljava/lang/String;)Laws/smithy/kotlin/runtime/hashing/HashFunction; + public static final fun toHashFunctionOrThrow (Ljava/lang/String;)Laws/smithy/kotlin/runtime/hashing/HashFunction; } public final class aws/smithy/kotlin/runtime/hashing/HmacKt { @@ -958,6 +971,8 @@ public final class aws/smithy/kotlin/runtime/io/SdkByteReadChannelKt { public static final fun readFully (Laws/smithy/kotlin/runtime/io/SdkByteReadChannel;Laws/smithy/kotlin/runtime/io/SdkBuffer;JLkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun readRemaining (Laws/smithy/kotlin/runtime/io/SdkByteReadChannel;Laws/smithy/kotlin/runtime/io/SdkBuffer;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun readToBuffer (Laws/smithy/kotlin/runtime/io/SdkByteReadChannel;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static final fun rollingHash (Laws/smithy/kotlin/runtime/io/SdkByteReadChannel;Laws/smithy/kotlin/runtime/hashing/HashFunction;JLkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun rollingHash$default (Laws/smithy/kotlin/runtime/io/SdkByteReadChannel;Laws/smithy/kotlin/runtime/hashing/HashFunction;JLkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; public static final fun writeAll (Laws/smithy/kotlin/runtime/io/SdkByteWriteChannel;Laws/smithy/kotlin/runtime/io/SdkSource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/businessmetrics/BusinessMetricsUtils.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/businessmetrics/BusinessMetricsUtils.kt index fa9c41652a..0600752a6e 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/businessmetrics/BusinessMetricsUtils.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/businessmetrics/BusinessMetricsUtils.kt @@ -90,6 +90,14 @@ public enum class SmithyBusinessMetric(public override val identifier: String) : SERVICE_ENDPOINT_OVERRIDE("N"), ACCOUNT_ID_BASED_ENDPOINT("O"), SIGV4A_SIGNING("S"), + FLEXIBLE_CHECKSUMS_REQ_CRC32("U"), + FLEXIBLE_CHECKSUMS_REQ_CRC32C("V"), + FLEXIBLE_CHECKSUMS_REQ_SHA1("X"), + FLEXIBLE_CHECKSUMS_REQ_SHA256("Y"), + FLEXIBLE_CHECKSUMS_REQ_WHEN_SUPPORTED("Z"), + FLEXIBLE_CHECKSUMS_REQ_WHEN_REQUIRED("a"), + FLEXIBLE_CHECKSUMS_RES_WHEN_SUPPORTED("b"), + FLEXIBLE_CHECKSUMS_RES_WHEN_REQUIRED("c"), ; override fun toString(): String = identifier diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/hashing/HashFunction.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/hashing/HashFunction.kt index 50684d61d8..2ad5b6785f 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/hashing/HashFunction.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/hashing/HashFunction.kt @@ -4,7 +4,10 @@ */ package aws.smithy.kotlin.runtime.hashing +import aws.smithy.kotlin.runtime.ClientException import aws.smithy.kotlin.runtime.InternalApi +import aws.smithy.kotlin.runtime.businessmetrics.BusinessMetric +import aws.smithy.kotlin.runtime.businessmetrics.SmithyBusinessMetric /** * A cryptographic hash function (algorithm) @@ -70,3 +73,55 @@ public fun String.toHashFunction(): HashFunction? = when (this.lowercase()) { "md5" -> Md5() else -> null } + +/** + * @return The [HashFunction] which is represented by this string, or an exception if none match. + */ +@InternalApi +public fun String.toHashFunctionOrThrow(): HashFunction = + toHashFunction() ?: throw ClientException("Checksum algorithm is not supported: $this") + +/** + * @return If the [HashFunction] is supported by flexible checksums + */ +@InternalApi +public val HashFunction.isSupportedForFlexibleChecksums: Boolean + get() = when (this) { + is Crc32, is Crc32c, is Sha1, is Sha256 -> true + else -> false + } + +/** + * @return The checksum algorithm header used depending on the checksum algorithm name + */ +@InternalApi +public fun String.resolveChecksumAlgorithmHeaderName(): String = + this.toHashFunctionOrThrow().resolveChecksumAlgorithmHeaderName() + +/** + * @return The checksum algorithm header used depending on the checksum algorithm + */ +@InternalApi +public fun HashFunction.resolveChecksumAlgorithmHeaderName(): String { + val prefix = "x-amz-checksum-" + return when (this) { + is Crc32 -> prefix + "crc32" + is Crc32c -> prefix + "crc32c" + is Sha1 -> prefix + "sha1" + is Sha256 -> prefix + "sha256" + is Md5 -> "Content-MD5" + else -> throw ClientException("Checksum algorithm is not supported: ${this::class.simpleName}") + } +} + +/** + * Maps supported hash functions to business metrics. + */ +@InternalApi +public fun HashFunction.toBusinessMetric(): BusinessMetric = when (this) { + is Crc32 -> SmithyBusinessMetric.FLEXIBLE_CHECKSUMS_REQ_CRC32 + is Crc32c -> SmithyBusinessMetric.FLEXIBLE_CHECKSUMS_REQ_CRC32C + is Sha1 -> SmithyBusinessMetric.FLEXIBLE_CHECKSUMS_REQ_SHA1 + is Sha256 -> SmithyBusinessMetric.FLEXIBLE_CHECKSUMS_REQ_SHA256 + else -> throw IllegalStateException("Checksum was calculated using an unsupported hash function: ${this::class.simpleName}") +} diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/io/SdkByteReadChannel.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/io/SdkByteReadChannel.kt index 87d5a531da..a50259e240 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/io/SdkByteReadChannel.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/io/SdkByteReadChannel.kt @@ -4,6 +4,8 @@ */ package aws.smithy.kotlin.runtime.io +import aws.smithy.kotlin.runtime.InternalApi +import aws.smithy.kotlin.runtime.hashing.HashFunction import aws.smithy.kotlin.runtime.io.internal.SdkDispatchers import kotlinx.coroutines.withContext @@ -131,3 +133,17 @@ public suspend fun SdkByteWriteChannel.writeAll(source: SdkSource): Long = withC } totalRead } + +/** + * Compute the rolling hash of an [SdkByteReadChannel] using [hashFunction], reading up-to [bufferSize] bytes into memory + * @return a ByteArray of the hash function's digest + */ +@InternalApi +public suspend fun SdkByteReadChannel.rollingHash(hashFunction: HashFunction, bufferSize: Long = 8192): ByteArray { + val buffer = SdkBuffer() + while (!isClosedForRead) { + read(buffer, bufferSize) + hashFunction.update(buffer.readToByteArray()) + } + return hashFunction.digest() +} diff --git a/runtime/smithy-client/api/smithy-client.api b/runtime/smithy-client/api/smithy-client.api index c2a9190009..b6132b26b8 100644 --- a/runtime/smithy-client/api/smithy-client.api +++ b/runtime/smithy-client/api/smithy-client.api @@ -235,6 +235,18 @@ public final class aws/smithy/kotlin/runtime/client/config/CompressionClientConf public abstract interface annotation class aws/smithy/kotlin/runtime/client/config/CompressionClientConfigDsl : java/lang/annotation/Annotation { } +public abstract interface class aws/smithy/kotlin/runtime/client/config/HttpChecksumConfig { + public abstract fun getRequestChecksumCalculation ()Laws/smithy/kotlin/runtime/client/config/RequestHttpChecksumConfig; + public abstract fun getResponseChecksumValidation ()Laws/smithy/kotlin/runtime/client/config/ResponseHttpChecksumConfig; +} + +public abstract interface class aws/smithy/kotlin/runtime/client/config/HttpChecksumConfig$Builder { + public abstract fun getRequestChecksumCalculation ()Laws/smithy/kotlin/runtime/client/config/RequestHttpChecksumConfig; + public abstract fun getResponseChecksumValidation ()Laws/smithy/kotlin/runtime/client/config/ResponseHttpChecksumConfig; + public abstract fun setRequestChecksumCalculation (Laws/smithy/kotlin/runtime/client/config/RequestHttpChecksumConfig;)V + public abstract fun setResponseChecksumValidation (Laws/smithy/kotlin/runtime/client/config/ResponseHttpChecksumConfig;)V +} + public final class aws/smithy/kotlin/runtime/client/config/RequestCompressionConfig { public static final field Companion Laws/smithy/kotlin/runtime/client/config/RequestCompressionConfig$Companion; public fun (Laws/smithy/kotlin/runtime/client/config/RequestCompressionConfig$Builder;)V @@ -258,6 +270,22 @@ public final class aws/smithy/kotlin/runtime/client/config/RequestCompressionCon public final fun invoke (Lkotlin/jvm/functions/Function1;)Laws/smithy/kotlin/runtime/client/config/RequestCompressionConfig; } +public final class aws/smithy/kotlin/runtime/client/config/RequestHttpChecksumConfig : java/lang/Enum { + public static final field WHEN_REQUIRED Laws/smithy/kotlin/runtime/client/config/RequestHttpChecksumConfig; + public static final field WHEN_SUPPORTED Laws/smithy/kotlin/runtime/client/config/RequestHttpChecksumConfig; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Laws/smithy/kotlin/runtime/client/config/RequestHttpChecksumConfig; + public static fun values ()[Laws/smithy/kotlin/runtime/client/config/RequestHttpChecksumConfig; +} + +public final class aws/smithy/kotlin/runtime/client/config/ResponseHttpChecksumConfig : java/lang/Enum { + public static final field WHEN_REQUIRED Laws/smithy/kotlin/runtime/client/config/ResponseHttpChecksumConfig; + public static final field WHEN_SUPPORTED Laws/smithy/kotlin/runtime/client/config/ResponseHttpChecksumConfig; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Laws/smithy/kotlin/runtime/client/config/ResponseHttpChecksumConfig; + public static fun values ()[Laws/smithy/kotlin/runtime/client/config/ResponseHttpChecksumConfig; +} + public final class aws/smithy/kotlin/runtime/client/config/RetryMode : java/lang/Enum { public static final field ADAPTIVE Laws/smithy/kotlin/runtime/client/config/RetryMode; public static final field LEGACY Laws/smithy/kotlin/runtime/client/config/RetryMode; diff --git a/runtime/smithy-client/common/src/aws/smithy/kotlin/runtime/client/config/HttpChecksumConfig.kt b/runtime/smithy-client/common/src/aws/smithy/kotlin/runtime/client/config/HttpChecksumConfig.kt new file mode 100644 index 0000000000..fbe73b860e --- /dev/null +++ b/runtime/smithy-client/common/src/aws/smithy/kotlin/runtime/client/config/HttpChecksumConfig.kt @@ -0,0 +1,63 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package aws.smithy.kotlin.runtime.client.config + +/** + * Client config for HTTP checksums + */ +public interface HttpChecksumConfig { + /** + * Configures request checksum calculation + */ + public val requestChecksumCalculation: RequestHttpChecksumConfig? + + /** + * Configures response checksum validation + */ + public val responseChecksumValidation: ResponseHttpChecksumConfig? + + public interface Builder { + /** + * Configures request checksum calculation + */ + public var requestChecksumCalculation: RequestHttpChecksumConfig? + + /** + * Configures response checksum validation + */ + public var responseChecksumValidation: ResponseHttpChecksumConfig? + } +} + +/** + * Configuration options for enabling and managing HTTP request checksums + */ +public enum class RequestHttpChecksumConfig { + /** + * SDK will calculate checksums if the service marks them as required or if the service offers optional checksums. + */ + WHEN_SUPPORTED, + + /** + * SDK will only calculate checksums if the service marks them as required. + */ + WHEN_REQUIRED, +} + +/** + * Configuration options for enabling and managing HTTP response checksums + */ +public enum class ResponseHttpChecksumConfig { + /** + * SDK will validate checksums if the service marks them as required or if the service offers optional checksums. + */ + WHEN_SUPPORTED, + + /** + * SDK will only validate checksums if the service marks them as required. + */ + WHEN_REQUIRED, +} From 14e0958a95a86016d1845c89019a06bc9a0f879c Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Wed, 15 Jan 2025 17:58:12 +0000 Subject: [PATCH 21/79] chore: release 1.4.0 --- .changes/0857b6f0-0444-479f-be6b-06ef71d482a0.json | 9 --------- .changes/1a68d0b7-00e7-45c0-88f6-95e5c39c9c61.json | 6 ------ .changes/3456d00f-1b29-4d88-ab02-045db1b1ebce.json | 8 -------- CHANGELOG.md | 11 +++++++++++ gradle.properties | 4 ++-- 5 files changed, 13 insertions(+), 25 deletions(-) delete mode 100644 .changes/0857b6f0-0444-479f-be6b-06ef71d482a0.json delete mode 100644 .changes/1a68d0b7-00e7-45c0-88f6-95e5c39c9c61.json delete mode 100644 .changes/3456d00f-1b29-4d88-ab02-045db1b1ebce.json diff --git a/.changes/0857b6f0-0444-479f-be6b-06ef71d482a0.json b/.changes/0857b6f0-0444-479f-be6b-06ef71d482a0.json deleted file mode 100644 index d89da89103..0000000000 --- a/.changes/0857b6f0-0444-479f-be6b-06ef71d482a0.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "id": "0857b6f0-0444-479f-be6b-06ef71d482a0", - "type": "feature", - "description": "⚠️ **IMPORTANT**: Add `retryStrategy` configuration option for waiters", - "issues": [ - "https://github.com/awslabs/aws-sdk-kotlin/issues/1431" - ], - "requiresMinorVersionBump": true -} \ No newline at end of file diff --git a/.changes/1a68d0b7-00e7-45c0-88f6-95e5c39c9c61.json b/.changes/1a68d0b7-00e7-45c0-88f6-95e5c39c9c61.json deleted file mode 100644 index 10ef502204..0000000000 --- a/.changes/1a68d0b7-00e7-45c0-88f6-95e5c39c9c61.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "id": "1a68d0b7-00e7-45c0-88f6-95e5c39c9c61", - "type": "misc", - "description": "⚠️ **IMPORTANT**: Upgrade to Kotlin 2.1.0", - "requiresMinorVersionBump": true -} \ No newline at end of file diff --git a/.changes/3456d00f-1b29-4d88-ab02-045db1b1ebce.json b/.changes/3456d00f-1b29-4d88-ab02-045db1b1ebce.json deleted file mode 100644 index 767e48a02e..0000000000 --- a/.changes/3456d00f-1b29-4d88-ab02-045db1b1ebce.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "id": "3456d00f-1b29-4d88-ab02-045db1b1ebce", - "type": "bugfix", - "description": "Include more information when retry strategy halts early due to token bucket capacity errors", - "issues": [ - "awslabs/aws-sdk-kotlin#1321" - ] -} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index c1758b4332..c324c5e681 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## [1.4.0] - 01/15/2025 + +### Features +* [#1431](https://github.com/awslabs/aws-sdk-kotlin/issues/1431) ⚠️ **IMPORTANT**: Add `retryStrategy` configuration option for waiters + +### Fixes +* [#1321](https://github.com/awslabs/aws-sdk-kotlin/issues/1321) Include more information when retry strategy halts early due to token bucket capacity errors + +### Miscellaneous +* ⚠️ **IMPORTANT**: Upgrade to Kotlin 2.1.0 + ## [1.3.34] - 01/10/2025 ## [1.3.33] - 01/10/2025 diff --git a/gradle.properties b/gradle.properties index d76819491b..d0e2da8b41 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.3.35-SNAPSHOT +sdkVersion=1.4.0 # codegen -codegenVersion=0.33.35-SNAPSHOT \ No newline at end of file +codegenVersion=0.34.0 \ No newline at end of file From ed95d7b5014ba9311d4a3fff16671971fc026e36 Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Wed, 15 Jan 2025 17:58:13 +0000 Subject: [PATCH 22/79] chore: bump snapshot version to 1.4.1-SNAPSHOT --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index d0e2da8b41..dbe31b2197 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.4.0 +sdkVersion=1.4.1-SNAPSHOT # codegen -codegenVersion=0.34.0 \ No newline at end of file +codegenVersion=0.34.1-SNAPSHOT \ No newline at end of file From a4ace353ca9dc1517c00de345a833691c444841d Mon Sep 17 00:00:00 2001 From: Matas Date: Thu, 16 Jan 2025 11:50:34 -0500 Subject: [PATCH 23/79] fix: add 0.9.x aws-crt-kotlin transform (#1220) --- .brazil.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.brazil.json b/.brazil.json index 5a1d830abd..dbb2135d1b 100644 --- a/.brazil.json +++ b/.brazil.json @@ -8,6 +8,7 @@ "com.squareup.okio:okio-jvm:3.*": "OkioJvm-3.x", "io.opentelemetry:opentelemetry-api:1.*": "Maven-io-opentelemetry_opentelemetry-api-1.x", "org.slf4j:slf4j-api:2.*": "Maven-org-slf4j_slf4j-api-2.x", + "aws.sdk.kotlin.crt:aws-crt-kotlin:0.9.*": "AwsCrtKotlin-0.9.x", "aws.sdk.kotlin.crt:aws-crt-kotlin:0.8.*": "AwsCrtKotlin-0.8.x", "com.squareup.okhttp3:okhttp:4.*": "OkHttp3-4.x" }, From 15e5f0ce2c713f3e81f75a28e90b4175891e0575 Mon Sep 17 00:00:00 2001 From: Matas Date: Thu, 16 Jan 2025 14:57:24 -0500 Subject: [PATCH 24/79] fix: Ensure `Host` header is included when signing auth tokens (#1222) --- .github/workflows/continuous-integration.yml | 2 +- gradle/libs.versions.toml | 2 +- .../kotlin/runtime/auth/awssigning/AuthTokenGenerator.kt | 9 ++++++++- .../runtime/auth/awssigning/AuthTokenGeneratorTest.kt | 2 ++ 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 599293cc06..ef2cc9db42 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -68,7 +68,7 @@ jobs: ./gradlew test jvmTest - name: Save Test Reports if: failure() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: test-reports-${{ matrix.os }} path: '**/build/reports' diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6ef5b35d57..6d8724016e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ kotlin-version = "2.1.0" dokka-version = "1.9.10" -aws-kotlin-repo-tools-version = "0.4.17" +aws-kotlin-repo-tools-version = "0.4.18" # libs coroutines-version = "1.9.0" diff --git a/runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGenerator.kt b/runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGenerator.kt index e92ac1f72c..f80ee4fe7e 100644 --- a/runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGenerator.kt +++ b/runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGenerator.kt @@ -6,6 +6,7 @@ package aws.smithy.kotlin.runtime.auth.awssigning import aws.smithy.kotlin.runtime.auth.awscredentials.CredentialsProvider import aws.smithy.kotlin.runtime.auth.awssigning.AwsSigningConfig.Companion.invoke +import aws.smithy.kotlin.runtime.http.Headers import aws.smithy.kotlin.runtime.http.HttpMethod import aws.smithy.kotlin.runtime.http.request.HttpRequest import aws.smithy.kotlin.runtime.net.url.Url @@ -28,7 +29,13 @@ public class AuthTokenGenerator( private fun Url.trimScheme(): String = toString().removePrefix(scheme.protocolName).removePrefix("://") public suspend fun generateAuthToken(endpoint: Url, region: String, expiration: Duration): String { - val req = HttpRequest(HttpMethod.GET, endpoint) + val req = HttpRequest( + HttpMethod.GET, + endpoint, + headers = Headers { + append("Host", endpoint.hostAndPort) + }, + ) val config = AwsSigningConfig { credentials = credentialsProvider.resolve() diff --git a/runtime/auth/aws-signing-common/common/test/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGeneratorTest.kt b/runtime/auth/aws-signing-common/common/test/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGeneratorTest.kt index 87fec1a74e..75b17fbab9 100644 --- a/runtime/auth/aws-signing-common/common/test/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGeneratorTest.kt +++ b/runtime/auth/aws-signing-common/common/test/aws/smithy/kotlin/runtime/auth/awssigning/AuthTokenGeneratorTest.kt @@ -45,6 +45,7 @@ class AuthTokenGeneratorTest { assertContains(token, "X-Amz-Credential=signature") // test custom signer was invoked assertContains(token, "X-Amz-Expires=333") // expiration assertContains(token, "X-Amz-SigningDate=0") // clock + assertContains(token, "X-Amz-SignedHeaders=host") assertTrue(credentialsProvider.credentialsResolved) } @@ -60,6 +61,7 @@ private val TEST_SIGNER = object : AwsSigner { put("X-Amz-Credential", "signature") put("X-Amz-Expires", (config.expiresAfter?.toLong(DurationUnit.SECONDS) ?: 900).toString()) put("X-Amz-SigningDate", config.signingDate.epochSeconds.toString()) + put("X-Amz-SignedHeaders", request.headers.names().map { it.lowercase() }.joinToString()) } return AwsSigningResult(builder.build(), "signature".encodeToByteArray()) From 447ac10880ebe778471d9ad4bce97a424139550d Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Thu, 16 Jan 2025 19:59:39 +0000 Subject: [PATCH 25/79] chore: release 1.4.1 --- CHANGELOG.md | 2 ++ gradle.properties | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c324c5e681..a4c4a65548 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [1.4.1] - 01/16/2025 + ## [1.4.0] - 01/15/2025 ### Features diff --git a/gradle.properties b/gradle.properties index dbe31b2197..e005c08ee2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.4.1-SNAPSHOT +sdkVersion=1.4.1 # codegen -codegenVersion=0.34.1-SNAPSHOT \ No newline at end of file +codegenVersion=0.34.1 \ No newline at end of file From 0f8db4452966822949bbe6616e3642b2063cfb63 Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Thu, 16 Jan 2025 19:59:40 +0000 Subject: [PATCH 26/79] chore: bump snapshot version to 1.4.2-SNAPSHOT --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index e005c08ee2..df0e651ce1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.4.1 +sdkVersion=1.4.2-SNAPSHOT # codegen -codegenVersion=0.34.1 \ No newline at end of file +codegenVersion=0.34.2-SNAPSHOT \ No newline at end of file From 8b336937bb028bc3d0fcb1beafeabcf4c25fc51e Mon Sep 17 00:00:00 2001 From: Matas Date: Thu, 23 Jan 2025 14:03:54 -0500 Subject: [PATCH 27/79] fix: address various failing protocol tests (#1223) --- .../model/error-correction-tests.smithy | 12 ++++---- .../core/AwsHttpBindingProtocolGenerator.kt | 8 +---- .../codegen/core/KotlinSymbolProvider.kt | 30 ++++++++++++------- .../codegen/rendering/StructureGenerator.kt | 1 + .../protocol/HttpStringValuesMapSerializer.kt | 19 ++---------- .../kotlin/codegen/core/SymbolProviderTest.kt | 2 +- .../HttpBindingProtocolGeneratorTest.kt | 8 ++--- .../HttpStringValuesMapSerializerTest.kt | 16 +++++----- gradle/libs.versions.toml | 2 +- .../kotlin/runtime/text/encoding/Base64.kt | 11 +++++-- .../runtime/text/encoding/Base64Test.kt | 28 ++++++++++++----- 11 files changed, 74 insertions(+), 63 deletions(-) diff --git a/codegen/protocol-tests/model/error-correction-tests.smithy b/codegen/protocol-tests/model/error-correction-tests.smithy index 201ffc877f..4f246e1b99 100644 --- a/codegen/protocol-tests/model/error-correction-tests.smithy +++ b/codegen/protocol-tests/model/error-correction-tests.smithy @@ -39,8 +39,9 @@ operation SayHelloXml { output: TestOutput, errors: [Error] } structure TestOutputDocument with [TestStruct] { innerField: Nested, - // FIXME: This trait fails smithy validator - // @required + + // Note: This shape _should_ be @required, but causes Smithy httpResponseTests validation to fail. + // We expect `document` to be deserialized as `null` and enforce @required using a runtime check, but Smithy validator doesn't recognize / allow this. document: Document } structure TestOutput with [TestStruct] { innerField: Nested } @@ -65,8 +66,8 @@ structure TestStruct { @required nestedListValue: NestedList - // FIXME: This trait fails smithy validator - // @required + // Note: This shape _should_ be @required, but causes Smithy httpResponseTests validation to fail. + // We expect `nested` to be deserialized as `null` and enforce @required using a runtime check, but Smithy validator doesn't recognize / allow this. nested: Nested @required @@ -97,8 +98,7 @@ union MyUnion { } structure Nested { - // FIXME: This trait fails smithy validator - // @required + @required a: String } diff --git a/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/core/AwsHttpBindingProtocolGenerator.kt b/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/core/AwsHttpBindingProtocolGenerator.kt index 6e5834f7d1..bd41afcade 100644 --- a/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/core/AwsHttpBindingProtocolGenerator.kt +++ b/codegen/smithy-aws-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/core/AwsHttpBindingProtocolGenerator.kt @@ -40,13 +40,7 @@ abstract class AwsHttpBindingProtocolGenerator : HttpBindingProtocolGenerator() // val targetedTest = TestMemberDelta(setOf("RestJsonComplexErrorWithNoMessage"), TestContainmentMode.RUN_TESTS) val ignoredTests = TestMemberDelta( - setOf( - "AwsJson10ClientErrorCorrectsWithDefaultValuesWhenServerFailsToSerializeRequiredValues", - "RestJsonNullAndEmptyHeaders", - "NullAndEmptyHeaders", - "RpcV2CborClientPopulatesDefaultsValuesWhenMissingInResponse", - "RpcV2CborClientPopulatesDefaultValuesInInput", - ), + setOf(), ) val requestTestBuilder = HttpProtocolUnitTestRequestGenerator.Builder() diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinSymbolProvider.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinSymbolProvider.kt index e6dbd0dca1..f35570dfeb 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinSymbolProvider.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinSymbolProvider.kt @@ -193,7 +193,7 @@ class KotlinSymbolProvider(private val model: Model, private val settings: Kotli } else { // only use @default if type is `T` shape.getTrait()?.let { - defaultValue(it.getDefaultValue(targetShape)) + setDefaultValue(it, targetShape) } } } @@ -219,9 +219,10 @@ class KotlinSymbolProvider(private val model: Model, private val settings: Kotli } } - private fun DefaultTrait.getDefaultValue(targetShape: Shape): String? { - val node = toNode() - return when { + private fun Symbol.Builder.setDefaultValue(defaultTrait: DefaultTrait, targetShape: Shape) { + val node = defaultTrait.toNode() + + val defaultValue = when { node.toString() == "null" -> null // Check if target is an enum before treating the default like a regular number/string @@ -235,13 +236,20 @@ class KotlinSymbolProvider(private val model: Model, private val settings: Kotli "${enumSymbol.fullName}.fromValue($arg)" } - targetShape.isBlobShape && targetShape.isStreaming -> - node - .toString() - .takeUnless { it.isEmpty() } - ?.let { "ByteStream.fromString(${it.dq()})" } + targetShape.isBlobShape -> { + addReferences(RuntimeTypes.Core.Text.Encoding.decodeBase64) - targetShape.isBlobShape -> "${node.toString().dq()}.encodeToByteArray()" + if (targetShape.isStreaming) { + node.toString() + .takeUnless { it.isEmpty() } + ?.let { + addReferences(RuntimeTypes.Core.Content.ByteStream) + "ByteStream.fromString(${it.dq()}.decodeBase64())" + } + } else { + "${node.toString().dq()}.decodeBase64().encodeToByteArray()" + } + } targetShape.isDocumentShape -> getDefaultValueForDocument(node) targetShape.isTimestampShape -> getDefaultValueForTimestamp(node.asNumberNode().get()) @@ -252,6 +260,8 @@ class KotlinSymbolProvider(private val model: Model, private val settings: Kotli node.isStringNode -> node.toString().dq() else -> node.toString() } + + defaultValue(defaultValue) } private fun getDefaultValueForTimestamp(node: NumberNode): String { diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/StructureGenerator.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/StructureGenerator.kt index c34eb5ab9d..5d90f376d2 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/StructureGenerator.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/StructureGenerator.kt @@ -248,6 +248,7 @@ class StructureGenerator( } else { memberSymbol } + write("public var #L: #E", memberName, builderMemberSymbol) } write("") diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpStringValuesMapSerializer.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpStringValuesMapSerializer.kt index 932b2a796a..5dfc519962 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpStringValuesMapSerializer.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpStringValuesMapSerializer.kt @@ -157,9 +157,8 @@ class HttpStringValuesMapSerializer( val paramName = binding.locationName // addAll collection parameter 2 val param2 = if (mapFnContents.isEmpty()) "input.$memberName" else "input.$memberName.map { $mapFnContents }" - val nullCheck = if (memberSymbol.isNullable) "?" else "" writer.write( - "if (input.#L$nullCheck.isNotEmpty() == true) #L(#S, #L)", + "if (input.#L != null) #L(#S, #L)", memberName, binding.location.addAllFnName, paramName, @@ -174,8 +173,7 @@ class HttpStringValuesMapSerializer( val paramName = binding.locationName val memberSymbol = symbolProvider.toSymbol(binding.member) - // NOTE: query parameters are allowed to be empty, whereas headers should omit empty string - // values from serde + // NOTE: query parameters are allowed to be empty if ((location == HttpBinding.Location.QUERY || location == HttpBinding.Location.HEADER) && binding.member.hasTrait()) { // Call the idempotency token function if no supplied value. writer.addImport(RuntimeTypes.SmithyClient.IdempotencyTokenProviderExt) @@ -185,18 +183,7 @@ class HttpStringValuesMapSerializer( paramName, ) } else { - val nullCheck = - if (location == HttpBinding.Location.QUERY || - memberTarget.hasTrait< - @Suppress("DEPRECATION") - software.amazon.smithy.model.traits.EnumTrait, - >() - ) { - if (memberSymbol.isNullable) "input.$memberName != null" else "" - } else { - val nullCheck = if (memberSymbol.isNullable) "?" else "" - "input.$memberName$nullCheck.isNotEmpty() == true" - } + val nullCheck = if (memberSymbol.isNullable) "input.$memberName != null" else "" val cond = defaultCheck(binding.member) ?: nullCheck diff --git a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/core/SymbolProviderTest.kt b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/core/SymbolProviderTest.kt index 0cc4d2cb14..bb90fbc0fb 100644 --- a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/core/SymbolProviderTest.kt +++ b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/core/SymbolProviderTest.kt @@ -182,7 +182,7 @@ class SymbolProviderTest { "double,2.71828,2.71828", "byte,10,10.toByte()", "string,\"hello\",\"hello\"", - "blob,\"abcdefg\",\"abcdefg\".encodeToByteArray()", + "blob,\"abcdefg\",\"abcdefg\".decodeBase64().encodeToByteArray()", "boolean,true,true", "bigInteger,5,5", "bigDecimal,9.0123456789,9.0123456789", diff --git a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpBindingProtocolGeneratorTest.kt b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpBindingProtocolGeneratorTest.kt index 3c15579dab..6f8040e7dc 100644 --- a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpBindingProtocolGeneratorTest.kt +++ b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpBindingProtocolGeneratorTest.kt @@ -57,8 +57,8 @@ internal class SmokeTestOperationSerializer: HttpSerializer.NonStreaming( + "YQ" to "a", + "Yg" to "b", + "YWI" to "ab", + "YWJj" to "abc", + "SGVsbG8gd29ybGQ" to "Hello world", + ) + + inputs.forEach { (encoded, expected) -> + val actual = encoded.decodeBase64() + assertEquals(expected, actual) + } + } } From 03badf913813df5481b330e9eeff80bcea5586ec Mon Sep 17 00:00:00 2001 From: Matas Date: Thu, 23 Jan 2025 14:04:09 -0500 Subject: [PATCH 28/79] misc: re-enable `kotlinWarningsAsErrors=true` (#1224) --- .github/workflows/continuous-integration.yml | 3 +-- .../kotlin/runtime/http/engine/okhttp/MetricsInterceptor.kt | 2 +- .../smithy/kotlin/runtime/http/engine/okhttp/OkHttpEngine.kt | 2 ++ .../smithy/kotlin/runtime/http/engine/okhttp/OkHttpUtils.kt | 2 +- .../smithy/kotlin/runtime/http/engine/okhttp4/OkHttp4Engine.kt | 2 +- .../common/src/aws/smithy/kotlin/runtime/util/JMESPath.kt | 3 +-- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index ef2cc9db42..d47020ea5d 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -62,8 +62,7 @@ jobs: - name: Test shell: bash run: | - # FIXME K2. Re-enable warnings as errors after this warning is removed: https://youtrack.jetbrains.com/issue/KT-68532 - # echo "kotlinWarningsAsErrors=true" >> $GITHUB_WORKSPACE/local.properties + echo "kotlinWarningsAsErrors=true" >> $GITHUB_WORKSPACE/local.properties ./gradlew apiCheck ./gradlew test jvmTest - name: Save Test Reports diff --git a/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/MetricsInterceptor.kt b/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/MetricsInterceptor.kt index 0ea8ae5e66..8047b870c3 100644 --- a/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/MetricsInterceptor.kt +++ b/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/MetricsInterceptor.kt @@ -30,7 +30,7 @@ public object MetricsInterceptor : Interceptor { } val originalResponse = chain.proceed(request) - val response = if (originalResponse.body == null || originalResponse.body?.contentLength() == 0L) { + val response = if (originalResponse.body.contentLength() == 0L) { originalResponse } else { originalResponse.newBuilder() diff --git a/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpEngine.kt b/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpEngine.kt index 061c22a13d..a20387d0a0 100644 --- a/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpEngine.kt +++ b/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpEngine.kt @@ -15,6 +15,7 @@ import aws.smithy.kotlin.runtime.net.TlsVersion import aws.smithy.kotlin.runtime.operation.ExecutionContext import aws.smithy.kotlin.runtime.time.Instant import aws.smithy.kotlin.runtime.time.fromEpochMilliseconds +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.job import okhttp3.* import okhttp3.ConnectionPool @@ -47,6 +48,7 @@ public class OkHttpEngine( private val connectionIdleMonitor = config.connectionIdlePollingInterval?.let { ConnectionIdleMonitor(it) } private val client = config.buildClientWithConnectionListener(metrics, connectionIdleMonitor) + @OptIn(ExperimentalCoroutinesApi::class) override suspend fun roundTrip(context: ExecutionContext, request: HttpRequest): HttpCall { val callContext = callContext() diff --git a/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpUtils.kt b/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpUtils.kt index c287228fdf..181ed400c3 100644 --- a/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpUtils.kt +++ b/runtime/protocol/http-client-engines/http-client-engine-okhttp/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp/OkHttpUtils.kt @@ -104,7 +104,7 @@ public fun Headers.toOkHttpHeaders(): OkHttpHeaders = OkHttpHeaders.Builder().al @InternalApi public fun OkHttpResponse.toSdkResponse(): HttpResponse { val sdkHeaders = OkHttpHeadersAdapter(headers) - val httpBody = if (body == null || body!!.contentLength() == 0L) { + val httpBody = if (body.contentLength() == 0L) { HttpBody.Empty } else { object : HttpBody.SourceContent() { diff --git a/runtime/protocol/http-client-engines/http-client-engine-okhttp4/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp4/OkHttp4Engine.kt b/runtime/protocol/http-client-engines/http-client-engine-okhttp4/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp4/OkHttp4Engine.kt index ff59823201..026bbd942c 100644 --- a/runtime/protocol/http-client-engines/http-client-engine-okhttp4/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp4/OkHttp4Engine.kt +++ b/runtime/protocol/http-client-engines/http-client-engine-okhttp4/jvm/src/aws/smithy/kotlin/runtime/http/engine/okhttp4/OkHttp4Engine.kt @@ -99,7 +99,7 @@ private suspend fun Call.executeAsync(): Response = call: Call, response: Response, ) { - continuation.resume(response) { + continuation.resume(response) { cause, _, _ -> response.closeQuietly() } } diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/JMESPath.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/JMESPath.kt index 9b691a79a2..c8437b2b33 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/JMESPath.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/JMESPath.kt @@ -62,8 +62,7 @@ public fun Any?.type(): String = when (this) { is List<*>, is Array<*> -> "array" is Number -> "number" is Any -> "object" - null -> "null" - else -> throw Exception("Undetected type for: $this") + else -> "null" } // Collection `flattenIfPossible` functions From 9f44cdbd82adc1dd30eaf4bc4290b61eee09bbd4 Mon Sep 17 00:00:00 2001 From: Ian Botsford <83236726+ianbotsf@users.noreply.github.com> Date: Tue, 28 Jan 2025 10:33:23 -0800 Subject: [PATCH 29/79] fix: ignore hop-by-hop headers when signing requests (#1227) --- .../b0646077-397d-48a8-94f2-ea2db40b1dea.json | 5 +++++ gradle/libs.versions.toml | 2 +- .../runtime/auth/awssigning/Canonicalizer.kt | 17 ++++++++++++++--- .../auth/awssigning/DefaultCanonicalizerTest.kt | 9 +++++++-- .../runtime/http/auth/AwsHttpSignerTestBase.kt | 16 +++++++++------- 5 files changed, 36 insertions(+), 13 deletions(-) create mode 100644 .changes/b0646077-397d-48a8-94f2-ea2db40b1dea.json diff --git a/.changes/b0646077-397d-48a8-94f2-ea2db40b1dea.json b/.changes/b0646077-397d-48a8-94f2-ea2db40b1dea.json new file mode 100644 index 0000000000..96e13a4a82 --- /dev/null +++ b/.changes/b0646077-397d-48a8-94f2-ea2db40b1dea.json @@ -0,0 +1,5 @@ +{ + "id": "b0646077-397d-48a8-94f2-ea2db40b1dea", + "type": "bugfix", + "description": "Ignore hop-by-hop headers when signing requests" +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 31f5d17afa..bdc0c8e98b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,7 +13,7 @@ okio-version = "3.9.1" otel-version = "1.45.0" slf4j-version = "2.0.16" slf4j-v1x-version = "1.7.36" -crt-kotlin-version = "0.9.0" +crt-kotlin-version = "0.9.1" micrometer-version = "1.14.2" binary-compatibility-validator-version = "0.16.3" diff --git a/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/Canonicalizer.kt b/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/Canonicalizer.kt index 69d742de79..c7c47fe5a2 100644 --- a/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/Canonicalizer.kt +++ b/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/Canonicalizer.kt @@ -58,16 +58,27 @@ internal interface Canonicalizer { ): CanonicalRequest } -// Taken from https://github.com/awslabs/aws-c-auth/blob/dd505b55fd46222834f35c6e54165d8cbebbfaaa/source/aws_signing.c#L118-L156 private val skipHeaders = setOf( - "connection", "expect", // https://github.com/awslabs/aws-sdk-kotlin/issues/862 + + // Taken from https://github.com/awslabs/aws-c-auth/blob/274a1d21330731cc51bb742794adc70ada5f4380/source/aws_signing.c#L121-L164 "sec-websocket-key", "sec-websocket-protocol", "sec-websocket-version", - "upgrade", "user-agent", "x-amzn-trace-id", + + // Taken from https://datatracker.ietf.org/doc/html/rfc2616#section-13.5.1. These are "hop-by-hop" headers which may + // be modified/removed by intervening proxies or caches. These are unsafe to sign because if they change they render + // the signature invalid. + "connection", + "keep-alive", + "proxy-authenticate", + "proxy-authorization", + "te", + "trailers", + "transfer-encoding", + "upgrade", ) internal class DefaultCanonicalizer(private val sha256Supplier: HashSupplier = ::Sha256) : Canonicalizer { diff --git a/runtime/auth/aws-signing-default/common/test/aws/smithy/kotlin/runtime/auth/awssigning/DefaultCanonicalizerTest.kt b/runtime/auth/aws-signing-default/common/test/aws/smithy/kotlin/runtime/auth/awssigning/DefaultCanonicalizerTest.kt index 7fc20ddb83..2dfbdf48e1 100644 --- a/runtime/auth/aws-signing-default/common/test/aws/smithy/kotlin/runtime/auth/awssigning/DefaultCanonicalizerTest.kt +++ b/runtime/auth/aws-signing-default/common/test/aws/smithy/kotlin/runtime/auth/awssigning/DefaultCanonicalizerTest.kt @@ -6,8 +6,12 @@ package aws.smithy.kotlin.runtime.auth.awssigning import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials import aws.smithy.kotlin.runtime.auth.awssigning.tests.DEFAULT_TEST_CREDENTIALS -import aws.smithy.kotlin.runtime.http.* -import aws.smithy.kotlin.runtime.http.request.* +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.request.HttpRequest +import aws.smithy.kotlin.runtime.http.request.headers +import aws.smithy.kotlin.runtime.http.request.url import aws.smithy.kotlin.runtime.net.Host import aws.smithy.kotlin.runtime.net.url.Url import aws.smithy.kotlin.runtime.time.Instant @@ -136,6 +140,7 @@ class DefaultCanonicalizerTest { // These should not be signed set("Expect", "100-continue") set("X-Amzn-Trace-Id", "qux") + set("Transfer-Encoding", "chunked") } body = HttpBody.Empty } diff --git a/runtime/auth/http-auth-aws/common/test/aws/smithy/kotlin/runtime/http/auth/AwsHttpSignerTestBase.kt b/runtime/auth/http-auth-aws/common/test/aws/smithy/kotlin/runtime/http/auth/AwsHttpSignerTestBase.kt index 2ce308df8f..7d10b35e14 100644 --- a/runtime/auth/http-auth-aws/common/test/aws/smithy/kotlin/runtime/http/auth/AwsHttpSignerTestBase.kt +++ b/runtime/auth/http-auth-aws/common/test/aws/smithy/kotlin/runtime/http/auth/AwsHttpSignerTestBase.kt @@ -12,7 +12,9 @@ import aws.smithy.kotlin.runtime.auth.awssigning.DefaultAwsSigner import aws.smithy.kotlin.runtime.auth.awssigning.internal.AWS_CHUNKED_THRESHOLD import aws.smithy.kotlin.runtime.collections.Attributes import aws.smithy.kotlin.runtime.collections.get -import aws.smithy.kotlin.runtime.http.* +import aws.smithy.kotlin.runtime.http.HttpBody +import aws.smithy.kotlin.runtime.http.HttpMethod +import aws.smithy.kotlin.runtime.http.SdkHttpClient import aws.smithy.kotlin.runtime.http.operation.* import aws.smithy.kotlin.runtime.http.request.HttpRequest import aws.smithy.kotlin.runtime.http.request.HttpRequestBuilder @@ -149,8 +151,8 @@ public abstract class AwsHttpSignerTestBase( val op = buildOperation(streaming = true, replayable = false, requestBody = "a".repeat(AWS_CHUNKED_THRESHOLD + 1)) val expectedDate = "20201016T195600Z" val expectedSig = "AWS4-HMAC-SHA256 Credential=AKID/20201016/us-east-1/demo/aws4_request, " + - "SignedHeaders=content-encoding;host;transfer-encoding;x-amz-archive-description;x-amz-date;x-amz-decoded-content-length;x-amz-security-token, " + - "Signature=ac341b9b248a0b23d2fcd9f7e805f4eb0b8a1b789bb23a8ec6adc6c48dd084ad" + "SignedHeaders=content-encoding;host;x-amz-archive-description;x-amz-date;x-amz-decoded-content-length;x-amz-security-token, " + + "Signature=ef06c95647c4d2daa6c89ac90274f1c780777cba8eaab772df6d8009def3eb8f" val signed = getSignedRequest(op) assertEquals(expectedDate, signed.headers["X-Amz-Date"]) @@ -162,8 +164,8 @@ public abstract class AwsHttpSignerTestBase( val op = buildOperation(streaming = true, replayable = true, requestBody = "a".repeat(AWS_CHUNKED_THRESHOLD + 1)) val expectedDate = "20201016T195600Z" val expectedSig = "AWS4-HMAC-SHA256 Credential=AKID/20201016/us-east-1/demo/aws4_request, " + - "SignedHeaders=content-encoding;host;transfer-encoding;x-amz-archive-description;x-amz-date;x-amz-decoded-content-length;x-amz-security-token, " + - "Signature=ac341b9b248a0b23d2fcd9f7e805f4eb0b8a1b789bb23a8ec6adc6c48dd084ad" + "SignedHeaders=content-encoding;host;x-amz-archive-description;x-amz-date;x-amz-decoded-content-length;x-amz-security-token, " + + "Signature=ef06c95647c4d2daa6c89ac90274f1c780777cba8eaab772df6d8009def3eb8f" val signed = getSignedRequest(op) assertEquals(expectedDate, signed.headers["X-Amz-Date"]) @@ -176,8 +178,8 @@ public abstract class AwsHttpSignerTestBase( val expectedDate = "20201016T195600Z" // should have same signature as testSignAwsChunkedStreamNonReplayable(), except for the hash, since the body is different val expectedSig = "AWS4-HMAC-SHA256 Credential=AKID/20201016/us-east-1/demo/aws4_request, " + - "SignedHeaders=content-encoding;host;transfer-encoding;x-amz-archive-description;x-amz-date;x-amz-decoded-content-length;x-amz-security-token, " + - "Signature=3f0277123c9ed8a8858f793886a0ac0fcb457bc54401ffc22d470f373397cff0" + "SignedHeaders=content-encoding;host;x-amz-archive-description;x-amz-date;x-amz-decoded-content-length;x-amz-security-token, " + + "Signature=a902702b57057a864bf41cc22ee846a1b7bd047e22784367ec6a459f6791330e" val signed = getSignedRequest(op) assertEquals(expectedDate, signed.headers["X-Amz-Date"]) From 97ac44792df8016d27988bcde1d112c69b46ab29 Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Tue, 28 Jan 2025 18:35:54 +0000 Subject: [PATCH 30/79] chore: release 1.4.2 --- .changes/b0646077-397d-48a8-94f2-ea2db40b1dea.json | 5 ----- CHANGELOG.md | 5 +++++ gradle.properties | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) delete mode 100644 .changes/b0646077-397d-48a8-94f2-ea2db40b1dea.json diff --git a/.changes/b0646077-397d-48a8-94f2-ea2db40b1dea.json b/.changes/b0646077-397d-48a8-94f2-ea2db40b1dea.json deleted file mode 100644 index 96e13a4a82..0000000000 --- a/.changes/b0646077-397d-48a8-94f2-ea2db40b1dea.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "id": "b0646077-397d-48a8-94f2-ea2db40b1dea", - "type": "bugfix", - "description": "Ignore hop-by-hop headers when signing requests" -} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index a4c4a65548..2e925d0943 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## [1.4.2] - 01/28/2025 + +### Fixes +* Ignore hop-by-hop headers when signing requests + ## [1.4.1] - 01/16/2025 ## [1.4.0] - 01/15/2025 diff --git a/gradle.properties b/gradle.properties index df0e651ce1..1b11779bb6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.4.2-SNAPSHOT +sdkVersion=1.4.2 # codegen -codegenVersion=0.34.2-SNAPSHOT \ No newline at end of file +codegenVersion=0.34.2 \ No newline at end of file From b7e50e3c66a073e5696b363aae29823284beef09 Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Tue, 28 Jan 2025 18:35:55 +0000 Subject: [PATCH 31/79] chore: bump snapshot version to 1.4.3-SNAPSHOT --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 1b11779bb6..9e53dbcaf0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.4.2 +sdkVersion=1.4.3-SNAPSHOT # codegen -codegenVersion=0.34.2 \ No newline at end of file +codegenVersion=0.34.3-SNAPSHOT \ No newline at end of file From e6357f92abd84dd2276a0f9ba27204be610d0068 Mon Sep 17 00:00:00 2001 From: Xinsong Cui Date: Thu, 30 Jan 2025 16:34:10 -0500 Subject: [PATCH 32/79] misc: add telemetry configuration to DefaultAwsSigner (#1226) * add telemetry provider configuration * lint * address pr reviews * add changelog --- .../d3ce7511-6fb2-4435-8f46-db724551b384.json | 5 ++++ .../api/aws-signing-default.api | 8 ++++++ .../auth/awssigning/DefaultAwsSigner.kt | 27 ++++++++++++++++--- 3 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 .changes/d3ce7511-6fb2-4435-8f46-db724551b384.json diff --git a/.changes/d3ce7511-6fb2-4435-8f46-db724551b384.json b/.changes/d3ce7511-6fb2-4435-8f46-db724551b384.json new file mode 100644 index 0000000000..aa89d9167e --- /dev/null +++ b/.changes/d3ce7511-6fb2-4435-8f46-db724551b384.json @@ -0,0 +1,5 @@ +{ + "id": "d3ce7511-6fb2-4435-8f46-db724551b384", + "type": "misc", + "description": "Add telemetry provider configuration to `DefaultAwsSigner`" +} \ No newline at end of file diff --git a/runtime/auth/aws-signing-default/api/aws-signing-default.api b/runtime/auth/aws-signing-default/api/aws-signing-default.api index 3b9535c403..e00e6d829a 100644 --- a/runtime/auth/aws-signing-default/api/aws-signing-default.api +++ b/runtime/auth/aws-signing-default/api/aws-signing-default.api @@ -1,4 +1,12 @@ +public final class aws/smithy/kotlin/runtime/auth/awssigning/DefaultAwsSignerBuilder { + public fun ()V + public final fun build ()Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigner; + public final fun getTelemetryProvider ()Laws/smithy/kotlin/runtime/telemetry/TelemetryProvider; + public final fun setTelemetryProvider (Laws/smithy/kotlin/runtime/telemetry/TelemetryProvider;)V +} + public final class aws/smithy/kotlin/runtime/auth/awssigning/DefaultAwsSignerKt { + public static final fun DefaultAwsSigner (Lkotlin/jvm/functions/Function1;)Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigner; public static final fun getDefaultAwsSigner ()Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigner; } diff --git a/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/DefaultAwsSigner.kt b/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/DefaultAwsSigner.kt index df613d31b0..791d8fce0a 100644 --- a/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/DefaultAwsSigner.kt +++ b/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/DefaultAwsSigner.kt @@ -4,8 +4,10 @@ */ package aws.smithy.kotlin.runtime.auth.awssigning +import aws.smithy.kotlin.runtime.ExperimentalApi import aws.smithy.kotlin.runtime.http.Headers import aws.smithy.kotlin.runtime.http.request.HttpRequest +import aws.smithy.kotlin.runtime.telemetry.TelemetryProvider import aws.smithy.kotlin.runtime.telemetry.logging.logger import aws.smithy.kotlin.runtime.time.TimestampFormat import kotlin.coroutines.coroutineContext @@ -13,13 +15,30 @@ import kotlin.coroutines.coroutineContext /** The default implementation of [AwsSigner] */ public val DefaultAwsSigner: AwsSigner = DefaultAwsSignerImpl() +/** Creates a customized instance of [AwsSigner] */ +@Suppress("ktlint:standard:function-naming") +public fun DefaultAwsSigner(block: DefaultAwsSignerBuilder.() -> Unit): AwsSigner = + DefaultAwsSignerBuilder().apply(block).build() + +/** A builder class for creating instances of [AwsSigner] using the default implementation */ +public class DefaultAwsSignerBuilder { + public var telemetryProvider: TelemetryProvider? = null + + public fun build(): AwsSigner = DefaultAwsSignerImpl( + telemetryProvider = telemetryProvider, + ) +} + +@OptIn(ExperimentalApi::class) internal class DefaultAwsSignerImpl( private val canonicalizer: Canonicalizer = Canonicalizer.Default, private val signatureCalculator: SignatureCalculator = SignatureCalculator.Default, private val requestMutator: RequestMutator = RequestMutator.Default, + private val telemetryProvider: TelemetryProvider? = null, ) : AwsSigner { override suspend fun sign(request: HttpRequest, config: AwsSigningConfig): AwsSigningResult { - val logger = coroutineContext.logger() + val logger = telemetryProvider?.loggerProvider?.getOrCreateLogger("DefaultAwsSigner") + ?: coroutineContext.logger() // TODO: implement SigV4a if (config.algorithm != AwsSigningAlgorithm.SIGV4) { @@ -52,7 +71,8 @@ internal class DefaultAwsSignerImpl( prevSignature: ByteArray, config: AwsSigningConfig, ): AwsSigningResult { - val logger = coroutineContext.logger() + val logger = telemetryProvider?.loggerProvider?.getOrCreateLogger("DefaultAwsSigner") + ?: coroutineContext.logger() val stringToSign = signatureCalculator.chunkStringToSign(chunkBody, prevSignature, config) logger.trace { "Chunk string to sign:\n$stringToSign" } @@ -70,7 +90,8 @@ internal class DefaultAwsSignerImpl( prevSignature: ByteArray, config: AwsSigningConfig, ): AwsSigningResult { - val logger = coroutineContext.logger() + val logger = telemetryProvider?.loggerProvider?.getOrCreateLogger("DefaultAwsSigner") + ?: coroutineContext.logger() // FIXME - can we share canonicalization code more than we are..., also this reduce is inefficient. // canonicalize the headers From 9d6857be261d650c1ce1b8145f42881d8054747d Mon Sep 17 00:00:00 2001 From: 0marperez <60363173+0marperez@users.noreply.github.com> Date: Tue, 4 Feb 2025 15:22:57 -0500 Subject: [PATCH 33/79] misc: gradle mirror (#1204) --- .github/workflows/artifact-size-metrics.yml | 4 ++++ .github/workflows/continuous-integration.yml | 14 ++++++++++++++ .github/workflows/kat-transform.yml | 5 +++++ .github/workflows/lint.yml | 2 ++ gradle/wrapper/gradle-wrapper.properties | 2 +- 5 files changed, 26 insertions(+), 1 deletion(-) diff --git a/.github/workflows/artifact-size-metrics.yml b/.github/workflows/artifact-size-metrics.yml index 0591b2b6aa..d1216099ae 100644 --- a/.github/workflows/artifact-size-metrics.yml +++ b/.github/workflows/artifact-size-metrics.yml @@ -31,6 +31,8 @@ jobs: with: role-to-assume: ${{ secrets.CI_AWS_ROLE_ARN }} aws-region: us-west-2 + - name: Configure Gradle + uses: awslabs/aws-kotlin-repo-tools/.github/actions/configure-gradle@main - name: Generate Artifact Size Metrics run: ./gradlew artifactSizeMetrics - name: Save Artifact Size Metrics @@ -54,6 +56,8 @@ jobs: with: role-to-assume: ${{ secrets.CI_AWS_ROLE_ARN }} aws-region: us-west-2 + - name: Configure Gradle + uses: awslabs/aws-kotlin-repo-tools/.github/actions/configure-gradle@main - name: Generate Artifact Size Metrics run: ./gradlew artifactSizeMetrics - name: Analyze Artifact Size Metrics diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index d47020ea5d..5536a141bc 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -39,6 +39,8 @@ jobs: distribution: 'corretto' java-version: 17 cache: 'gradle' + - name: Configure Gradle + uses: awslabs/aws-kotlin-repo-tools/.github/actions/configure-gradle@main - name: Test shell: bash run: | @@ -59,6 +61,8 @@ jobs: distribution: 'corretto' java-version: 17 cache: 'gradle' + - name: Configure Gradle + uses: awslabs/aws-kotlin-repo-tools/.github/actions/configure-gradle@main - name: Test shell: bash run: | @@ -83,6 +87,8 @@ jobs: distribution: 'corretto' java-version: 17 cache: 'gradle' + - name: Configure Gradle + uses: awslabs/aws-kotlin-repo-tools/.github/actions/configure-gradle@main - name: Test shell: bash run: | @@ -110,6 +116,14 @@ jobs: # smithy-kotlin is checked out as a sibling dir which will automatically make it an included build path: 'aws-sdk-kotlin' repository: 'awslabs/aws-sdk-kotlin' + - name: Configure Gradle - smithy-kotlin + uses: awslabs/aws-kotlin-repo-tools/.github/actions/configure-gradle@main + with: + working-directory: ./smithy-kotlin + - name: Configure Gradle - aws-sdk-kotlin + uses: awslabs/aws-kotlin-repo-tools/.github/actions/configure-gradle@main + with: + working-directory: ./aws-sdk-kotlin - name: Configure JDK uses: actions/setup-java@v3 with: diff --git a/.github/workflows/kat-transform.yml b/.github/workflows/kat-transform.yml index 938e936ef9..bcf6c933c1 100644 --- a/.github/workflows/kat-transform.yml +++ b/.github/workflows/kat-transform.yml @@ -35,6 +35,11 @@ jobs: role-to-assume: ${{ secrets.CI_AWS_ROLE_ARN }} aws-region: us-west-2 + - name: Configure Gradle + uses: awslabs/aws-kotlin-repo-tools/.github/actions/configure-gradle@main + with: + working-directory: ./smithy-kotlin + - name: Setup kat uses: awslabs/aws-kotlin-repo-tools/.github/actions/setup-kat@main diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index e95b98aded..809b2bc451 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -21,6 +21,8 @@ jobs: steps: - name: Checkout sources uses: actions/checkout@v2 + - name: Configure Gradle + uses: awslabs/aws-kotlin-repo-tools/.github/actions/configure-gradle@main - name: Lint ${{ env.PACKAGE_NAME }} run: | ./gradlew ktlint diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 3499ded5c1..dace2bff9b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +distributionUrl=https://services.gradle.org/distributions/gradle-8.12-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 532388277d794865f2829a970cade066620ab944 Mon Sep 17 00:00:00 2001 From: Ian Botsford <83236726+ianbotsf@users.noreply.github.com> Date: Thu, 6 Feb 2025 10:26:36 -0800 Subject: [PATCH 34/79] fix: correctly check equality for CaseInsensitiveMap (#1235) --- .../4820850c-8916-47f5-a7e1-8880e6a00d22.json | 5 + runtime/runtime-core/api/runtime-core.api | 1 + .../runtime/collections/CaseInsensitiveMap.kt | 22 ++- .../CaseInsensitiveMutableStringSet.kt | 40 ++++++ .../collections/CaseInsensitiveString.kt | 11 ++ .../kotlin/runtime/collections/ValuesMap.kt | 16 +-- .../collections/CaseInsensitiveMapTest.kt | 66 +++++++++ .../CaseInsensitiveMutableStringSetTest.kt | 125 ++++++++++++++++++ .../collections/CaseInsensitiveStringTest.kt | 27 ++++ 9 files changed, 291 insertions(+), 22 deletions(-) create mode 100644 .changes/4820850c-8916-47f5-a7e1-8880e6a00d22.json create mode 100644 runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/collections/CaseInsensitiveMutableStringSet.kt create mode 100644 runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/collections/CaseInsensitiveString.kt create mode 100644 runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/collections/CaseInsensitiveMutableStringSetTest.kt create mode 100644 runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/collections/CaseInsensitiveStringTest.kt diff --git a/.changes/4820850c-8916-47f5-a7e1-8880e6a00d22.json b/.changes/4820850c-8916-47f5-a7e1-8880e6a00d22.json new file mode 100644 index 0000000000..297f7e603f --- /dev/null +++ b/.changes/4820850c-8916-47f5-a7e1-8880e6a00d22.json @@ -0,0 +1,5 @@ +{ + "id": "4820850c-8916-47f5-a7e1-8880e6a00d22", + "type": "bugfix", + "description": "Fix errors in equality checks for `CaseInsensitiveMap` which affect `Headers` and `ValuesMap` implementations" +} \ No newline at end of file diff --git a/runtime/runtime-core/api/runtime-core.api b/runtime/runtime-core/api/runtime-core.api index 6d39c3b417..0d5399d430 100644 --- a/runtime/runtime-core/api/runtime-core.api +++ b/runtime/runtime-core/api/runtime-core.api @@ -306,6 +306,7 @@ public class aws/smithy/kotlin/runtime/collections/ValuesMapImpl : aws/smithy/ko public fun getAll (Ljava/lang/String;)Ljava/util/List; public fun getCaseInsensitiveName ()Z protected final fun getValues ()Ljava/util/Map; + public fun hashCode ()I public fun isEmpty ()Z public fun names ()Ljava/util/Set; } diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/collections/CaseInsensitiveMap.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/collections/CaseInsensitiveMap.kt index 096e1b6d3d..7e6d6751f9 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/collections/CaseInsensitiveMap.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/collections/CaseInsensitiveMap.kt @@ -6,16 +6,6 @@ package aws.smithy.kotlin.runtime.collections import aws.smithy.kotlin.runtime.InternalApi -private class CaseInsensitiveString(val s: String) { - val hash: Int = s.lowercase().hashCode() - override fun hashCode(): Int = hash - override fun equals(other: Any?): Boolean = other is CaseInsensitiveString && other.s.equals(s, ignoreCase = true) - override fun toString(): String = s -} - -private fun String.toInsensitive(): CaseInsensitiveString = - CaseInsensitiveString(this) - /** * Map of case-insensitive [String] to [Value] */ @@ -30,17 +20,17 @@ internal class CaseInsensitiveMap : MutableMap { override fun containsValue(value: Value): Boolean = impl.containsValue(value) - override fun get(key: String): Value? = impl.get(key.toInsensitive()) + override fun get(key: String): Value? = impl[key.toInsensitive()] override fun isEmpty(): Boolean = impl.isEmpty() override val entries: MutableSet> get() = impl.entries.map { - Entry(it.key.s, it.value) + Entry(it.key.normalized, it.value) }.toMutableSet() override val keys: MutableSet - get() = impl.keys.map { it.s }.toMutableSet() + get() = CaseInsensitiveMutableStringSet(impl.keys) override val values: MutableCollection get() = impl.values @@ -57,6 +47,12 @@ internal class CaseInsensitiveMap : MutableMap { override fun remove(key: String): Value? = impl.remove(key.toInsensitive()) + override fun hashCode() = impl.hashCode() + + override fun equals(other: Any?) = other is CaseInsensitiveMap<*> && impl == other.impl + + override fun toString() = impl.toString() + private class Entry( override val key: Key, override var value: Value, diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/collections/CaseInsensitiveMutableStringSet.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/collections/CaseInsensitiveMutableStringSet.kt new file mode 100644 index 0000000000..63924013c9 --- /dev/null +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/collections/CaseInsensitiveMutableStringSet.kt @@ -0,0 +1,40 @@ +package aws.smithy.kotlin.runtime.collections + +internal class CaseInsensitiveMutableStringSet( + initialValues: Iterable = setOf(), +) : MutableSet { + private val delegate = initialValues.toMutableSet() + + override fun add(element: String) = delegate.add(element.toInsensitive()) + override fun clear() = delegate.clear() + override fun contains(element: String) = delegate.contains(element.toInsensitive()) + override fun containsAll(elements: Collection) = elements.all { it in this } + override fun equals(other: Any?) = other is CaseInsensitiveMutableStringSet && delegate == other.delegate + override fun hashCode() = delegate.hashCode() + override fun isEmpty() = delegate.isEmpty() + override fun remove(element: String) = delegate.remove(element.toInsensitive()) + override val size: Int get() = delegate.size + override fun toString() = delegate.toString() + + override fun addAll(elements: Collection) = + elements.fold(false) { modified, item -> add(item) || modified } + + override fun iterator() = object : MutableIterator { + val delegate = this@CaseInsensitiveMutableStringSet.delegate.iterator() + override fun hasNext() = delegate.hasNext() + override fun next() = delegate.next().normalized + override fun remove() = delegate.remove() + } + + override fun removeAll(elements: Collection) = + elements.fold(false) { modified, item -> remove(item) || modified } + + override fun retainAll(elements: Collection): Boolean { + val insensitiveElements = elements.map { it.toInsensitive() }.toSet() + val toRemove = delegate.filterNot { it in insensitiveElements } + return toRemove.fold(false) { modified, item -> delegate.remove(item) || modified } + } +} + +internal fun CaseInsensitiveMutableStringSet(initialValues: Iterable) = + CaseInsensitiveMutableStringSet(initialValues.map { it.toInsensitive() }) diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/collections/CaseInsensitiveString.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/collections/CaseInsensitiveString.kt new file mode 100644 index 0000000000..e182ca813d --- /dev/null +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/collections/CaseInsensitiveString.kt @@ -0,0 +1,11 @@ +package aws.smithy.kotlin.runtime.collections + +internal class CaseInsensitiveString(val original: String) { + val normalized = original.lowercase() + override fun hashCode() = normalized.hashCode() + override fun equals(other: Any?) = other is CaseInsensitiveString && normalized == other.normalized + override fun toString() = original +} + +internal fun String.toInsensitive(): CaseInsensitiveString = + CaseInsensitiveString(this) diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/collections/ValuesMap.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/collections/ValuesMap.kt index 763fa21ece..efa7a70af3 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/collections/ValuesMap.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/collections/ValuesMap.kt @@ -89,15 +89,13 @@ public open class ValuesMapImpl( override fun isEmpty(): Boolean = values.isEmpty() - override fun equals(other: Any?): Boolean = - other is ValuesMap<*> && - caseInsensitiveName == other.caseInsensitiveName && - names().let { names -> - if (names.size != other.names().size) { - return false - } - names.all { getAll(it) == other.getAll(it) } - } + override fun equals(other: Any?): Boolean = when (other) { + is ValuesMapImpl<*> -> caseInsensitiveName == other.caseInsensitiveName && values == other.values + is ValuesMap<*> -> caseInsensitiveName == other.caseInsensitiveName && entries() == other.entries() + else -> false + } + + override fun hashCode(): Int = values.hashCode() private fun Map>.deepCopyValues(): Map> = mapValues { (_, v) -> v.toList() } } diff --git a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/collections/CaseInsensitiveMapTest.kt b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/collections/CaseInsensitiveMapTest.kt index dcf919d52d..a36d8ebc31 100644 --- a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/collections/CaseInsensitiveMapTest.kt +++ b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/collections/CaseInsensitiveMapTest.kt @@ -6,6 +6,8 @@ package aws.smithy.kotlin.runtime.collections import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue class CaseInsensitiveMapTest { @Test @@ -18,4 +20,68 @@ class CaseInsensitiveMapTest { assertEquals("json", map["content-type"]) assertEquals("json", map["CONTENT-TYPE"]) } + + @Test + fun testContains() { + val map = CaseInsensitiveMap() + map["A"] = "apple" + map["B"] = "banana" + map["C"] = "cherry" + + assertTrue("C" in map) + assertTrue("c" in map) + assertFalse("D" in map) + } + + @Test + fun testKeysContains() { + val map = CaseInsensitiveMap() + map["A"] = "apple" + map["B"] = "banana" + map["C"] = "cherry" + val keys = map.keys + + assertTrue("C" in keys) + assertTrue("c" in keys) + assertFalse("D" in keys) + } + + @Test + fun testEquality() { + val left = CaseInsensitiveMap() + left["A"] = "apple" + left["B"] = "banana" + left["C"] = "cherry" + + val right = CaseInsensitiveMap() + right["c"] = "cherry" + right["b"] = "banana" + right["a"] = "apple" + + assertEquals(left, right) + } + + @Test + fun testEntriesEquality() { + val left = CaseInsensitiveMap() + left["A"] = "apple" + left["B"] = "banana" + left["C"] = "cherry" + + val right = CaseInsensitiveMap() + right["c"] = "cherry" + right["b"] = "banana" + right["a"] = "apple" + + assertEquals(left.entries, right.entries) + } + + @Test + fun testToString() { + val map = CaseInsensitiveMap() + map["A"] = "apple" + map["B"] = "banana" + map["C"] = "cherry" + assertEquals("{A=apple, B=banana, C=cherry}", map.toString()) + } } diff --git a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/collections/CaseInsensitiveMutableStringSetTest.kt b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/collections/CaseInsensitiveMutableStringSetTest.kt new file mode 100644 index 0000000000..258ef4c7f3 --- /dev/null +++ b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/collections/CaseInsensitiveMutableStringSetTest.kt @@ -0,0 +1,125 @@ +package aws.smithy.kotlin.runtime.collections + +import kotlin.test.* + +private val input = setOf("APPLE", "banana", "cHeRrY") +private val variations = (input + input.map { it.lowercase() } + input.map { it.uppercase() }) +private val disjoint = setOf("durIAN", "ELdeRBerRY", "FiG") +private val subset = input - "APPLE" +private val intersecting = subset + disjoint + +class CaseInsensitiveMutableStringSetTest { + private fun assertSize(size: Int, set: CaseInsensitiveMutableStringSet) { + assertEquals(size, set.size) + val emptyAsserter: (Boolean) -> Unit = if (size == 0) ::assertTrue else ::assertFalse + emptyAsserter(set.isEmpty()) + } + + @Test + fun testInitialization() { + val set = CaseInsensitiveMutableStringSet(input) + assertSize(3, set) + } + + @Test + fun testAdd() { + val set = CaseInsensitiveMutableStringSet(input) + set += "durIAN" + assertSize(4, set) + } + + @Test + fun testAddAll() { + val set = CaseInsensitiveMutableStringSet(input) + assertFalse(set.addAll(set)) + + val intersecting = input + "durian" + assertTrue(set.addAll(intersecting)) + assertSize(4, set) + } + + @Test + fun testClear() { + val set = CaseInsensitiveMutableStringSet(input) + set.clear() + assertSize(0, set) + } + + @Test + fun testContains() { + val set = CaseInsensitiveMutableStringSet(input) + variations.forEach { assertTrue("Set should contain element $it") { it in set } } + + assertFalse("durian" in set) + } + + @Test + fun testContainsAll() { + val set = CaseInsensitiveMutableStringSet(input) + assertTrue(set.containsAll(variations)) + + val intersecting = input + "durian" + assertFalse(set.containsAll(intersecting)) + } + + @Test + fun testEquality() { + val left = CaseInsensitiveMutableStringSet(input) + val right = CaseInsensitiveMutableStringSet(input) + assertEquals(left, right) + + left -= "apple" + assertNotEquals(left, right) + + right -= "ApPlE" + assertEquals(left, right) + } + + @Test + fun testIterator() { + val set = CaseInsensitiveMutableStringSet(input) + val iterator = set.iterator() + + assertTrue(iterator.hasNext()) + assertEquals("apple", iterator.next()) + + assertTrue(iterator.hasNext()) + assertEquals("banana", iterator.next()) + iterator.remove() + assertSize(2, set) + + assertTrue(iterator.hasNext()) + assertEquals("cherry", iterator.next()) + + assertFalse(iterator.hasNext()) + assertTrue(set.containsAll(input - "banana")) + } + + @Test + fun testRemove() { + val set = CaseInsensitiveMutableStringSet(input) + set -= "BANANA" + assertSize(2, set) + } + + @Test + fun testRemoveAll() { + val set = CaseInsensitiveMutableStringSet(input) + assertFalse(set.removeAll(disjoint)) + + assertTrue(set.removeAll(intersecting)) + assertSize(1, set) + assertTrue("apple" in set) + } + + @Test + fun testRetainAll() { + val set = CaseInsensitiveMutableStringSet(input) + assertFalse(set.retainAll(set)) + assertSize(3, set) + + assertTrue(set.retainAll(intersecting)) + assertSize(2, set) + assertTrue(set.containsAll(subset)) + } +} diff --git a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/collections/CaseInsensitiveStringTest.kt b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/collections/CaseInsensitiveStringTest.kt new file mode 100644 index 0000000000..f3a0c7b600 --- /dev/null +++ b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/collections/CaseInsensitiveStringTest.kt @@ -0,0 +1,27 @@ +package aws.smithy.kotlin.runtime.collections + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals + +class CaseInsensitiveStringTest { + @Test + fun testEquality() { + val left = "Banana".toInsensitive() + val right = "baNAna".toInsensitive() + assertEquals(left, right) + assertNotEquals("Banana", left) + assertNotEquals("baNAna", right) + + val nonMatching = "apple".toInsensitive() + assertNotEquals(nonMatching, left) + assertNotEquals(nonMatching, right) + } + + @Test + fun testProperties() { + val s = "BANANA".toInsensitive() + assertEquals("BANANA", s.original) + assertEquals("banana", s.normalized) + } +} From 1af0e8200c3f78949be02d1ef68dd5f120e73aa1 Mon Sep 17 00:00:00 2001 From: 0marperez <60363173+0marperez@users.noreply.github.com> Date: Fri, 7 Feb 2025 12:01:47 -0500 Subject: [PATCH 35/79] misc: gradle version bump (#1236) --- gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index dace2bff9b..b136486be8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https://services.gradle.org/distributions/gradle-8.12-bin.zip +distributionUrl=https://services.gradle.org/distributions/gradle-8.12.1-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 120768c37101a25fdfddb3504df048cb25045b8e Mon Sep 17 00:00:00 2001 From: Ian Botsford <83236726+ianbotsf@users.noreply.github.com> Date: Fri, 7 Feb 2025 16:01:31 -0800 Subject: [PATCH 36/79] fix: correct hash code calculation for case-insensitive map entries (#1238) * fix: correct hash code calculation for case-insensitive map entries * remove commented-out code --- .../c0040355-ffdc-4813-80e9-baf859ef02b9.json | 5 +++++ .../runtime/collections/CaseInsensitiveMap.kt | 2 +- .../collections/CaseInsensitiveMapTest.kt | 16 ++++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 .changes/c0040355-ffdc-4813-80e9-baf859ef02b9.json diff --git a/.changes/c0040355-ffdc-4813-80e9-baf859ef02b9.json b/.changes/c0040355-ffdc-4813-80e9-baf859ef02b9.json new file mode 100644 index 0000000000..42302b920d --- /dev/null +++ b/.changes/c0040355-ffdc-4813-80e9-baf859ef02b9.json @@ -0,0 +1,5 @@ +{ + "id": "c0040355-ffdc-4813-80e9-baf859ef02b9", + "type": "bugfix", + "description": "fix: correct hash code calculation for case-insensitive map entries" +} \ No newline at end of file diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/collections/CaseInsensitiveMap.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/collections/CaseInsensitiveMap.kt index 7e6d6751f9..e888ad72bc 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/collections/CaseInsensitiveMap.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/collections/CaseInsensitiveMap.kt @@ -63,7 +63,7 @@ internal class CaseInsensitiveMap : MutableMap { return value } - override fun hashCode(): Int = 17 * 31 + key!!.hashCode() + value!!.hashCode() + override fun hashCode(): Int = key.hashCode() xor value.hashCode() // Match JVM & K/N stdlib implementations override fun equals(other: Any?): Boolean { if (other == null || other !is Map.Entry<*, *>) return false diff --git a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/collections/CaseInsensitiveMapTest.kt b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/collections/CaseInsensitiveMapTest.kt index a36d8ebc31..96fd83612b 100644 --- a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/collections/CaseInsensitiveMapTest.kt +++ b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/collections/CaseInsensitiveMapTest.kt @@ -76,6 +76,22 @@ class CaseInsensitiveMapTest { assertEquals(left.entries, right.entries) } + @Test + fun testEntriesEqualityWithNormalMap() { + val left = CaseInsensitiveMap() + left["A"] = "apple" + left["B"] = "banana" + left["C"] = "cherry" + + val right = mutableMapOf( + "c" to "cherry", + "b" to "banana", + "a" to "apple", + ) + + assertEquals(left.entries, right.entries) + } + @Test fun testToString() { val map = CaseInsensitiveMap() From d7e36039967610e3225321f1bc7ccd47765207d6 Mon Sep 17 00:00:00 2001 From: Matas Date: Wed, 12 Feb 2025 17:41:13 -0500 Subject: [PATCH 37/79] misc: bump build plugin version (#1239) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bdc0c8e98b..bfa4c2b4f1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ kotlin-version = "2.1.0" dokka-version = "1.9.10" -aws-kotlin-repo-tools-version = "0.4.18" +aws-kotlin-repo-tools-version = "0.4.20" # libs coroutines-version = "1.9.0" From c1bcb0420a27bc7192c048983190780676c5a386 Mon Sep 17 00:00:00 2001 From: Ian Botsford <83236726+ianbotsf@users.noreply.github.com> Date: Thu, 13 Feb 2025 08:38:17 -0800 Subject: [PATCH 38/79] fix: favor endpointUrl over endpoint discovery when provided (#1240) --- .../e7d8178c-4211-443d-b4d9-44d7019aefd1.json | 8 ++++ .../discovery/EndpointDiscovererGenerator.kt | 37 +++++++++++-------- .../EndpointDiscovererGeneratorTest.kt | 30 ++++++++------- 3 files changed, 47 insertions(+), 28 deletions(-) create mode 100644 .changes/e7d8178c-4211-443d-b4d9-44d7019aefd1.json diff --git a/.changes/e7d8178c-4211-443d-b4d9-44d7019aefd1.json b/.changes/e7d8178c-4211-443d-b4d9-44d7019aefd1.json new file mode 100644 index 0000000000..51864abe9b --- /dev/null +++ b/.changes/e7d8178c-4211-443d-b4d9-44d7019aefd1.json @@ -0,0 +1,8 @@ +{ + "id": "e7d8178c-4211-443d-b4d9-44d7019aefd1", + "type": "bugfix", + "description": "Favor `endpointUrl` over endpoint discovery when provided", + "issues": [ + "https://github.com/awslabs/aws-sdk-kotlin/issues/1413" + ] +} \ No newline at end of file diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/endpoints/discovery/EndpointDiscovererGenerator.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/endpoints/discovery/EndpointDiscovererGenerator.kt index c492cd743d..73b6e434da 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/endpoints/discovery/EndpointDiscovererGenerator.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/endpoints/discovery/EndpointDiscovererGenerator.kt @@ -13,6 +13,7 @@ import software.amazon.smithy.kotlin.codegen.model.buildSymbol import software.amazon.smithy.kotlin.codegen.model.expectShape import software.amazon.smithy.kotlin.codegen.model.expectTrait import software.amazon.smithy.kotlin.codegen.rendering.endpoints.EndpointResolverAdapterGenerator +import software.amazon.smithy.kotlin.codegen.rendering.endpoints.SdkEndpointBuiltinIntegration import software.amazon.smithy.model.shapes.OperationShape import software.amazon.smithy.model.shapes.ServiceShape @@ -80,21 +81,27 @@ class EndpointDiscovererGenerator(private val ctx: CodegenContext, private val d EndpointResolverAdapterGenerator.getSymbol(ctx.settings), RuntimeTypes.HttpClient.Operation.EndpointResolver, ) { - write("val identity = request.identity") - write( - """require(identity is #T) { "Endpoint discovery requires AWS credentials" }""", - RuntimeTypes.Auth.Credentials.AwsCredentials.Credentials, - ) - write("") - write("val cacheKey = DiscoveryParams(client.config.region, identity.accessKeyId)") - write("request.context[discoveryParamsKey] = cacheKey") - write("val discoveredHost = cache.get(cacheKey) { discoverHost(client) }") - write("") - write("val originalEndpoint = delegate.resolve(request)") - withBlock("#T(", ")", RuntimeTypes.SmithyClient.Endpoints.Endpoint) { - write("originalEndpoint.uri.copy { host = discoveredHost },") - write("originalEndpoint.headers,") - write("originalEndpoint.attributes,") + // Backported from https://github.com/smithy-lang/smithy-kotlin/pull/1221; replace when merging v1.5 to main + withBlock("if (client.config.#L == null) {", "}", SdkEndpointBuiltinIntegration.EndpointUrlProp.propertyName) { + write("val identity = request.identity") + write( + """require(identity is #T) { "Endpoint discovery requires AWS credentials" }""", + RuntimeTypes.Auth.Credentials.AwsCredentials.Credentials, + ) + write("") + write("val cacheKey = DiscoveryParams(client.config.region, identity.accessKeyId)") + write("request.context[discoveryParamsKey] = cacheKey") + write("val discoveredHost = cache.get(cacheKey) { discoverHost(client) }") + write("") + write("val originalEndpoint = delegate.resolve(request)") + withBlock("#T(", ")", RuntimeTypes.SmithyClient.Endpoints.Endpoint) { + write("originalEndpoint.uri.copy { host = discoveredHost },") + write("originalEndpoint.headers,") + write("originalEndpoint.attributes,") + } + // If user manually specifies endpointUrl, skip endpoint discovery + closeAndOpenBlock("} else {") + write("delegate.resolve(request)") } } } diff --git a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/endpoints/discovery/EndpointDiscovererGeneratorTest.kt b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/endpoints/discovery/EndpointDiscovererGeneratorTest.kt index b67c51d57a..37b9e6a900 100644 --- a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/endpoints/discovery/EndpointDiscovererGeneratorTest.kt +++ b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/endpoints/discovery/EndpointDiscovererGeneratorTest.kt @@ -37,19 +37,23 @@ class EndpointDiscovererGeneratorTest { actual.shouldContainOnlyOnceWithDiff( """ internal fun asEndpointResolver(client: TestClient, delegate: EndpointResolverAdapter) = EndpointResolver { request -> - val identity = request.identity - require(identity is Credentials) { "Endpoint discovery requires AWS credentials" } - - val cacheKey = DiscoveryParams(client.config.region, identity.accessKeyId) - request.context[discoveryParamsKey] = cacheKey - val discoveredHost = cache.get(cacheKey) { discoverHost(client) } - - val originalEndpoint = delegate.resolve(request) - Endpoint( - originalEndpoint.uri.copy { host = discoveredHost }, - originalEndpoint.headers, - originalEndpoint.attributes, - ) + if (client.config.endpointUrl == null) { + val identity = request.identity + require(identity is Credentials) { "Endpoint discovery requires AWS credentials" } + + val cacheKey = DiscoveryParams(client.config.region, identity.accessKeyId) + request.context[discoveryParamsKey] = cacheKey + val discoveredHost = cache.get(cacheKey) { discoverHost(client) } + + val originalEndpoint = delegate.resolve(request) + Endpoint( + originalEndpoint.uri.copy { host = discoveredHost }, + originalEndpoint.headers, + originalEndpoint.attributes, + ) + } else { + delegate.resolve(request) + } } """.formatForTest(), ) From 4223bfa224dca5bb6274ae2da77022eed74a5b9e Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Thu, 13 Feb 2025 17:13:45 +0000 Subject: [PATCH 39/79] chore: release 1.4.3 --- .changes/4820850c-8916-47f5-a7e1-8880e6a00d22.json | 5 ----- .changes/c0040355-ffdc-4813-80e9-baf859ef02b9.json | 5 ----- .changes/d3ce7511-6fb2-4435-8f46-db724551b384.json | 5 ----- .changes/e7d8178c-4211-443d-b4d9-44d7019aefd1.json | 8 -------- CHANGELOG.md | 10 ++++++++++ gradle.properties | 4 ++-- 6 files changed, 12 insertions(+), 25 deletions(-) delete mode 100644 .changes/4820850c-8916-47f5-a7e1-8880e6a00d22.json delete mode 100644 .changes/c0040355-ffdc-4813-80e9-baf859ef02b9.json delete mode 100644 .changes/d3ce7511-6fb2-4435-8f46-db724551b384.json delete mode 100644 .changes/e7d8178c-4211-443d-b4d9-44d7019aefd1.json diff --git a/.changes/4820850c-8916-47f5-a7e1-8880e6a00d22.json b/.changes/4820850c-8916-47f5-a7e1-8880e6a00d22.json deleted file mode 100644 index 297f7e603f..0000000000 --- a/.changes/4820850c-8916-47f5-a7e1-8880e6a00d22.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "id": "4820850c-8916-47f5-a7e1-8880e6a00d22", - "type": "bugfix", - "description": "Fix errors in equality checks for `CaseInsensitiveMap` which affect `Headers` and `ValuesMap` implementations" -} \ No newline at end of file diff --git a/.changes/c0040355-ffdc-4813-80e9-baf859ef02b9.json b/.changes/c0040355-ffdc-4813-80e9-baf859ef02b9.json deleted file mode 100644 index 42302b920d..0000000000 --- a/.changes/c0040355-ffdc-4813-80e9-baf859ef02b9.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "id": "c0040355-ffdc-4813-80e9-baf859ef02b9", - "type": "bugfix", - "description": "fix: correct hash code calculation for case-insensitive map entries" -} \ No newline at end of file diff --git a/.changes/d3ce7511-6fb2-4435-8f46-db724551b384.json b/.changes/d3ce7511-6fb2-4435-8f46-db724551b384.json deleted file mode 100644 index aa89d9167e..0000000000 --- a/.changes/d3ce7511-6fb2-4435-8f46-db724551b384.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "id": "d3ce7511-6fb2-4435-8f46-db724551b384", - "type": "misc", - "description": "Add telemetry provider configuration to `DefaultAwsSigner`" -} \ No newline at end of file diff --git a/.changes/e7d8178c-4211-443d-b4d9-44d7019aefd1.json b/.changes/e7d8178c-4211-443d-b4d9-44d7019aefd1.json deleted file mode 100644 index 51864abe9b..0000000000 --- a/.changes/e7d8178c-4211-443d-b4d9-44d7019aefd1.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "id": "e7d8178c-4211-443d-b4d9-44d7019aefd1", - "type": "bugfix", - "description": "Favor `endpointUrl` over endpoint discovery when provided", - "issues": [ - "https://github.com/awslabs/aws-sdk-kotlin/issues/1413" - ] -} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e925d0943..5148b8ef03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [1.4.3] - 02/13/2025 + +### Fixes +* Fix errors in equality checks for `CaseInsensitiveMap` which affect `Headers` and `ValuesMap` implementations +* fix: correct hash code calculation for case-insensitive map entries +* [#1413](https://github.com/awslabs/aws-sdk-kotlin/issues/1413) Favor `endpointUrl` over endpoint discovery when provided + +### Miscellaneous +* Add telemetry provider configuration to `DefaultAwsSigner` + ## [1.4.2] - 01/28/2025 ### Fixes diff --git a/gradle.properties b/gradle.properties index 9e53dbcaf0..ae21839ca4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.4.3-SNAPSHOT +sdkVersion=1.4.3 # codegen -codegenVersion=0.34.3-SNAPSHOT \ No newline at end of file +codegenVersion=0.34.3 \ No newline at end of file From cea7b1ff15a744c78ab2085469772532a423b7f1 Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Thu, 13 Feb 2025 17:13:46 +0000 Subject: [PATCH 40/79] chore: bump snapshot version to 1.4.4-SNAPSHOT --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index ae21839ca4..f087c7b551 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.4.3 +sdkVersion=1.4.4-SNAPSHOT # codegen -codegenVersion=0.34.3 \ No newline at end of file +codegenVersion=0.34.4-SNAPSHOT \ No newline at end of file From ad4d2a38b85030a08efbc79aea84f3bd21596375 Mon Sep 17 00:00:00 2001 From: 0marperez <60363173+0marperez@users.noreply.github.com> Date: Fri, 14 Feb 2025 10:32:24 -0500 Subject: [PATCH 41/79] misc: test union member name same as union (#1241) --- .../smithy/kotlin/codegen/SmithySdkTest.kt | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/tests/compile/src/test/kotlin/software/amazon/smithy/kotlin/codegen/SmithySdkTest.kt b/tests/compile/src/test/kotlin/software/amazon/smithy/kotlin/codegen/SmithySdkTest.kt index 8cad9fc623..8adeb29cbf 100644 --- a/tests/compile/src/test/kotlin/software/amazon/smithy/kotlin/codegen/SmithySdkTest.kt +++ b/tests/compile/src/test/kotlin/software/amazon/smithy/kotlin/codegen/SmithySdkTest.kt @@ -224,6 +224,45 @@ class SmithySdkTest { assertEquals(KotlinCompilation.ExitCode.OK, compilationResult.exitCode, compileOutputStream.toString()) } + + // https://github.com/smithy-lang/smithy-kotlin/issues/1129 + @Test + fun `it compiles models with unions with members that have the same name as the union`() { + val model = """ + namespace aws.sdk.kotlin.test + + use aws.protocols#awsJson1_0 + use smithy.rules#operationContextParams + use smithy.rules#endpointRuleSet + use aws.api#service + + @awsJson1_0 + @service(sdkId: "UnionOperationTest") + service TestService { + operations: [UnionOperation], + version: "1" + } + + operation UnionOperation { + input: UnionOperationRequest + } + + structure UnionOperationRequest { + Union: Foo + } + + union Foo { + foo: Boolean + } + + """.asSmithy() + + val compileOutputStream = ByteArrayOutputStream() + val compilationResult = compileSdkAndTest(model = model, outputSink = compileOutputStream, emitSourcesToTmp = Debug.emitSourcesToTemp) + compileOutputStream.flush() + + assertEquals(KotlinCompilation.ExitCode.OK, compilationResult.exitCode, compileOutputStream.toString()) + } } /** From a0be9e4a870c3deffa4a5bc5e871c0c11525e55f Mon Sep 17 00:00:00 2001 From: 0marperez <60363173+0marperez@users.noreply.github.com> Date: Fri, 14 Feb 2025 14:10:10 -0500 Subject: [PATCH 42/79] fix: unions with member names matching auto-imported Kotlin symbols (#1242) --- .../codegen/core/KotlinSymbolProvider.kt | 4 +- .../kotlin/codegen/core/SymbolProviderTest.kt | 8 +-- .../smithy/kotlin/codegen/SmithySdkTest.kt | 62 +++++++++++++++++++ 3 files changed, 68 insertions(+), 6 deletions(-) diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinSymbolProvider.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinSymbolProvider.kt index f35570dfeb..cca76f0f42 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinSymbolProvider.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/KotlinSymbolProvider.kt @@ -154,7 +154,7 @@ class KotlinSymbolProvider(private val model: Model, private val settings: Kotli val fullyQualifiedValueType = "${reference.fullName}$valueSuffix" return createSymbolBuilder(shape, "List<$valueType>") .addReferences(reference) - .putProperty(SymbolProperty.FULLY_QUALIFIED_NAME_HINT, "List<$fullyQualifiedValueType>") + .putProperty(SymbolProperty.FULLY_QUALIFIED_NAME_HINT, "kotlin.collections.List<$fullyQualifiedValueType>") .putProperty(SymbolProperty.MUTABLE_COLLECTION_FUNCTION, "mutableListOf<$valueType>") .putProperty(SymbolProperty.IMMUTABLE_COLLECTION_FUNCTION, "listOf<$valueType>") .build() @@ -173,7 +173,7 @@ class KotlinSymbolProvider(private val model: Model, private val settings: Kotli return createSymbolBuilder(shape, "Map<$keyType, $valueType>") .addReferences(keyReference) .addReferences(valueReference) - .putProperty(SymbolProperty.FULLY_QUALIFIED_NAME_HINT, "Map<$fullyQualifiedKeyType, $fullyQualifiedValueType>") + .putProperty(SymbolProperty.FULLY_QUALIFIED_NAME_HINT, "kotlin.collections.Map<$fullyQualifiedKeyType, $fullyQualifiedValueType>") .putProperty(SymbolProperty.MUTABLE_COLLECTION_FUNCTION, "mutableMapOf<$keyType, $valueType>") .putProperty(SymbolProperty.IMMUTABLE_COLLECTION_FUNCTION, "mapOf<$keyType, $valueType>") .putProperty(SymbolProperty.ENTRY_EXPRESSION, "Map.Entry<$keyType, $valueType>") diff --git a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/core/SymbolProviderTest.kt b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/core/SymbolProviderTest.kt index bb90fbc0fb..df803ef25e 100644 --- a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/core/SymbolProviderTest.kt +++ b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/core/SymbolProviderTest.kt @@ -481,8 +481,8 @@ class SymbolProviderTest { assertEquals("Record", sparseListSymbol.references[0].symbol.name) // check the fully qualified name hint is set - assertEquals("List", listSymbol.fullNameHint) - assertEquals("List", sparseListSymbol.fullNameHint) + assertEquals("kotlin.collections.List", listSymbol.fullNameHint) + assertEquals("kotlin.collections.List", sparseListSymbol.fullNameHint) } @Test @@ -544,8 +544,8 @@ class SymbolProviderTest { assertTrue("com.test.model.Record" in sparseRefNames) // check the fully qualified name hint is set - assertEquals("Map", mapSymbol.fullNameHint) - assertEquals("Map", sparseMapSymbol.fullNameHint) + assertEquals("kotlin.collections.Map", mapSymbol.fullNameHint) + assertEquals("kotlin.collections.Map", sparseMapSymbol.fullNameHint) } @Test diff --git a/tests/compile/src/test/kotlin/software/amazon/smithy/kotlin/codegen/SmithySdkTest.kt b/tests/compile/src/test/kotlin/software/amazon/smithy/kotlin/codegen/SmithySdkTest.kt index 8adeb29cbf..a062d6c941 100644 --- a/tests/compile/src/test/kotlin/software/amazon/smithy/kotlin/codegen/SmithySdkTest.kt +++ b/tests/compile/src/test/kotlin/software/amazon/smithy/kotlin/codegen/SmithySdkTest.kt @@ -263,6 +263,68 @@ class SmithySdkTest { assertEquals(KotlinCompilation.ExitCode.OK, compilationResult.exitCode, compileOutputStream.toString()) } + + // https://github.com/smithy-lang/smithy-kotlin/issues/1127 + @Test + fun `it compiles models with union member names that match their types`() { + val model = """ + namespace aws.sdk.kotlin.test + + use aws.protocols#awsJson1_0 + use smithy.rules#operationContextParams + use smithy.rules#endpointRuleSet + use aws.api#service + + @awsJson1_0 + @service(sdkId: "UnionOperationTest") + service TestService { + operations: [DeleteObjects], + version: "1" + } + + operation DeleteObjects { + input: DeleteObjectsRequest + } + + structure DeleteObjectsRequest { + Delete: Foo + } + + union Foo { + list: BarList + map: IntegerMap + instant: Timestamp + byteArray: Blob + boolean: Boolean + string: String + bigInteger: BigInteger + bigDecimal: BigDecimal + double: Double + float: Float + long: Long + short: Short + int: Integer + byte: Byte + } + + list BarList { + member: Bar + } + + map IntegerMap { + key: String + value: Integer + } + + string Bar + """.asSmithy() + + val compileOutputStream = ByteArrayOutputStream() + val compilationResult = compileSdkAndTest(model = model, outputSink = compileOutputStream, emitSourcesToTmp = Debug.emitSourcesToTemp) + compileOutputStream.flush() + + assertEquals(KotlinCompilation.ExitCode.OK, compilationResult.exitCode, compileOutputStream.toString()) + } } /** From 00500f2f640ec565ad53546a3d660c2eb97b1472 Mon Sep 17 00:00:00 2001 From: Matas Date: Tue, 18 Feb 2025 10:33:39 -0500 Subject: [PATCH 43/79] fix: bump maximum event stream message length to 24MB (#1243) --- .changes/ba71e47f-d546-45f2-bf44-aedaab74a966.json | 5 +++++ .../kotlin/runtime/awsprotocol/eventstream/Message.kt | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 .changes/ba71e47f-d546-45f2-bf44-aedaab74a966.json diff --git a/.changes/ba71e47f-d546-45f2-bf44-aedaab74a966.json b/.changes/ba71e47f-d546-45f2-bf44-aedaab74a966.json new file mode 100644 index 0000000000..253c9f7e32 --- /dev/null +++ b/.changes/ba71e47f-d546-45f2-bf44-aedaab74a966.json @@ -0,0 +1,5 @@ +{ + "id": "ba71e47f-d546-45f2-bf44-aedaab74a966", + "type": "misc", + "description": "Increase maximum event stream message length to 24MB" +} \ No newline at end of file diff --git a/runtime/protocol/aws-event-stream/common/src/aws/smithy/kotlin/runtime/awsprotocol/eventstream/Message.kt b/runtime/protocol/aws-event-stream/common/src/aws/smithy/kotlin/runtime/awsprotocol/eventstream/Message.kt index 10d19fc603..6280520355 100644 --- a/runtime/protocol/aws-event-stream/common/src/aws/smithy/kotlin/runtime/awsprotocol/eventstream/Message.kt +++ b/runtime/protocol/aws-event-stream/common/src/aws/smithy/kotlin/runtime/awsprotocol/eventstream/Message.kt @@ -12,15 +12,15 @@ import aws.smithy.kotlin.runtime.text.encoding.encodeToHex internal const val MESSAGE_CRC_BYTE_LEN = 4 -// max message size is 16 MB -internal const val MAX_MESSAGE_SIZE = 16 * 1024 * 1024 +// max message size is 24 MB +internal const val MAX_MESSAGE_SIZE = 24 * 1024 * 1024 // max header size is 128 KB internal const val MAX_HEADER_SIZE = 128 * 1024 /* Message Wire Format - See also: https://docs.aws.amazon.com/transcribe/latest/dg/event-stream-med.html + See also: https://docs.aws.amazon.com/transcribe/latest/dg/streaming-setting-up.html +--------------------------------------------------------------------+ -- | Total Len (32) | | From 73292129c36dca46d50d25326eb63d9515f1758d Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Tue, 18 Feb 2025 15:52:07 +0000 Subject: [PATCH 44/79] chore: release 1.4.4 --- .changes/ba71e47f-d546-45f2-bf44-aedaab74a966.json | 5 ----- CHANGELOG.md | 5 +++++ gradle.properties | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) delete mode 100644 .changes/ba71e47f-d546-45f2-bf44-aedaab74a966.json diff --git a/.changes/ba71e47f-d546-45f2-bf44-aedaab74a966.json b/.changes/ba71e47f-d546-45f2-bf44-aedaab74a966.json deleted file mode 100644 index 253c9f7e32..0000000000 --- a/.changes/ba71e47f-d546-45f2-bf44-aedaab74a966.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "id": "ba71e47f-d546-45f2-bf44-aedaab74a966", - "type": "misc", - "description": "Increase maximum event stream message length to 24MB" -} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 5148b8ef03..8c0bee275f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## [1.4.4] - 02/18/2025 + +### Miscellaneous +* Increase maximum event stream message length to 24MB + ## [1.4.3] - 02/13/2025 ### Fixes diff --git a/gradle.properties b/gradle.properties index f087c7b551..2b8a818fdc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.4.4-SNAPSHOT +sdkVersion=1.4.4 # codegen -codegenVersion=0.34.4-SNAPSHOT \ No newline at end of file +codegenVersion=0.34.4 \ No newline at end of file From 612c39ba446e6403ea2bd9a51c4d1db111b6e26f Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Tue, 18 Feb 2025 15:52:08 +0000 Subject: [PATCH 45/79] chore: bump snapshot version to 1.4.5-SNAPSHOT --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 2b8a818fdc..f178e06d20 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.4.4 +sdkVersion=1.4.5-SNAPSHOT # codegen -codegenVersion=0.34.4 \ No newline at end of file +codegenVersion=0.34.5-SNAPSHOT \ No newline at end of file From 283ca31eb93043a967ebfc482ea76d4f94881cb5 Mon Sep 17 00:00:00 2001 From: 0marperez <60363173+0marperez@users.noreply.github.com> Date: Mon, 24 Feb 2025 13:34:11 -0500 Subject: [PATCH 46/79] misc: Remove Elastic Inference SDK ID test (#1247) --- .../src/test/resources/sdk-ids-test-output.csv | 1 - codegen/smithy-kotlin-codegen/src/test/resources/sdk-ids.csv | 1 - 2 files changed, 2 deletions(-) diff --git a/codegen/smithy-kotlin-codegen/src/test/resources/sdk-ids-test-output.csv b/codegen/smithy-kotlin-codegen/src/test/resources/sdk-ids-test-output.csv index 2482d0640e..9c16785c67 100644 --- a/codegen/smithy-kotlin-codegen/src/test/resources/sdk-ids-test-output.csv +++ b/codegen/smithy-kotlin-codegen/src/test/resources/sdk-ids-test-output.csv @@ -113,7 +113,6 @@ ECS,Ecs EFS,Efs EKS,Eks Elastic Beanstalk,ElasticBeanstalk -Elastic Inference,ElasticInference Elastic Load Balancing v2,ElasticLoadBalancingV2 Elastic Load Balancing,ElasticLoadBalancing Elastic Transcoder,ElasticTranscoder diff --git a/codegen/smithy-kotlin-codegen/src/test/resources/sdk-ids.csv b/codegen/smithy-kotlin-codegen/src/test/resources/sdk-ids.csv index 52a5c68450..369edd3e01 100644 --- a/codegen/smithy-kotlin-codegen/src/test/resources/sdk-ids.csv +++ b/codegen/smithy-kotlin-codegen/src/test/resources/sdk-ids.csv @@ -113,7 +113,6 @@ ECS EFS EKS Elastic Beanstalk -Elastic Inference Elastic Load Balancing v2 Elastic Load Balancing Elastic Transcoder From 82b9ffe794476313faaf6e4d3830c9a09455804a Mon Sep 17 00:00:00 2001 From: Matas Date: Mon, 24 Feb 2025 15:10:11 -0500 Subject: [PATCH 47/79] feat: Kotlin implementation of SigV4a signing (#1246) --- .../4b6debe1-7706-484a-8599-ef8c14cecde2.json | 5 + .../api/aws-signing-common.api | 1 + .../auth/awssigning/AwsSigningConfig.kt | 7 +- .../auth/awssigning/AwsSigningExceptions.kt | 1 + .../auth/aws-signing-default/build.gradle.kts | 1 + .../BaseSigV4SignatureCalculator.kt | 98 ++++++++++++++ .../runtime/auth/awssigning/Canonicalizer.kt | 3 +- .../auth/awssigning/DefaultAwsSigner.kt | 33 ++--- .../runtime/auth/awssigning/RequestMutator.kt | 2 +- .../awssigning/SigV4SignatureCalculator.kt | 30 +++++ .../awssigning/SigV4aSignatureCalculator.kt | 88 +++++++++++++ .../auth/awssigning/SignatureCalculator.kt | 99 +------------- .../awssigning/DefaultBasicSigningTest.kt | 7 - .../awssigning/DefaultRequestMutatorTest.kt | 38 +++++- ...est.kt => SigV4SignatureCalculatorTest.kt} | 10 +- .../SigV4aSignatureCalculatorTest.kt | 121 ++++++++++++++++++ .../awssigning/DefaultSigningSuiteTest.kt | 8 +- runtime/runtime-core/api/runtime-core.api | 4 + .../smithy/kotlin/runtime/hashing/Ecdsa.kt | 10 ++ .../smithy/kotlin/runtime/hashing/EcdsaJVM.kt | 37 ++++++ .../kotlin/runtime/hashing/EcdsaJVMTest.kt | 84 ++++++++++++ .../kotlin/runtime/hashing/EcdsaNative.kt | 13 ++ 22 files changed, 571 insertions(+), 129 deletions(-) create mode 100644 .changes/4b6debe1-7706-484a-8599-ef8c14cecde2.json create mode 100644 runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/BaseSigV4SignatureCalculator.kt create mode 100644 runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/SigV4SignatureCalculator.kt create mode 100644 runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/SigV4aSignatureCalculator.kt rename runtime/auth/aws-signing-default/common/test/aws/smithy/kotlin/runtime/auth/awssigning/{DefaultSignatureCalculatorTest.kt => SigV4SignatureCalculatorTest.kt} (92%) create mode 100644 runtime/auth/aws-signing-default/common/test/aws/smithy/kotlin/runtime/auth/awssigning/SigV4aSignatureCalculatorTest.kt create mode 100644 runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/hashing/Ecdsa.kt create mode 100644 runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/hashing/EcdsaJVM.kt create mode 100644 runtime/runtime-core/jvm/test/aws/smithy/kotlin/runtime/hashing/EcdsaJVMTest.kt create mode 100644 runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/hashing/EcdsaNative.kt diff --git a/.changes/4b6debe1-7706-484a-8599-ef8c14cecde2.json b/.changes/4b6debe1-7706-484a-8599-ef8c14cecde2.json new file mode 100644 index 0000000000..6770d0caa6 --- /dev/null +++ b/.changes/4b6debe1-7706-484a-8599-ef8c14cecde2.json @@ -0,0 +1,5 @@ +{ + "id": "4b6debe1-7706-484a-8599-ef8c14cecde2", + "type": "feature", + "description": "Add SigV4a support to the default AWS signer" +} \ No newline at end of file diff --git a/runtime/auth/aws-signing-common/api/aws-signing-common.api b/runtime/auth/aws-signing-common/api/aws-signing-common.api index d128b0bd6f..c9353509be 100644 --- a/runtime/auth/aws-signing-common/api/aws-signing-common.api +++ b/runtime/auth/aws-signing-common/api/aws-signing-common.api @@ -55,6 +55,7 @@ public final class aws/smithy/kotlin/runtime/auth/awssigning/AwsSigningAlgorithm public static final field SIGV4 Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigningAlgorithm; public static final field SIGV4_ASYMMETRIC Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigningAlgorithm; public static fun getEntries ()Lkotlin/enums/EnumEntries; + public final fun getSigningName ()Ljava/lang/String; public static fun valueOf (Ljava/lang/String;)Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigningAlgorithm; public static fun values ()[Laws/smithy/kotlin/runtime/auth/awssigning/AwsSigningAlgorithm; } diff --git a/runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AwsSigningConfig.kt b/runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AwsSigningConfig.kt index 0728e553e8..c26fcf624d 100644 --- a/runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AwsSigningConfig.kt +++ b/runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AwsSigningConfig.kt @@ -12,18 +12,19 @@ public typealias ShouldSignHeaderPredicate = (String) -> Boolean /** * Defines the AWS signature version to use + * @param signingName The name of this algorithm to use when signing requests. */ -public enum class AwsSigningAlgorithm { +public enum class AwsSigningAlgorithm(public val signingName: String) { /** * AWS Signature Version 4 * see: https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html */ - SIGV4, + SIGV4("AWS4-HMAC-SHA256"), /** * AWS Signature Version 4 Asymmetric */ - SIGV4_ASYMMETRIC, + SIGV4_ASYMMETRIC("AWS4-ECDSA-P256-SHA256"), } /** diff --git a/runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AwsSigningExceptions.kt b/runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AwsSigningExceptions.kt index 450337090b..8ff8c1e35f 100644 --- a/runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AwsSigningExceptions.kt +++ b/runtime/auth/aws-signing-common/common/src/aws/smithy/kotlin/runtime/auth/awssigning/AwsSigningExceptions.kt @@ -17,6 +17,7 @@ import aws.smithy.kotlin.runtime.InternalApi * @param cause The cause of the exception */ @InternalApi +@Deprecated("This exception is no longer thrown. It will be removed in the next minor version, v1.5.x.") public class UnsupportedSigningAlgorithmException( message: String, public val signingAlgorithm: AwsSigningAlgorithm, diff --git a/runtime/auth/aws-signing-default/build.gradle.kts b/runtime/auth/aws-signing-default/build.gradle.kts index 088c82f658..22e20d39fd 100644 --- a/runtime/auth/aws-signing-default/build.gradle.kts +++ b/runtime/auth/aws-signing-default/build.gradle.kts @@ -18,6 +18,7 @@ kotlin { dependencies { implementation(project(":runtime:auth:aws-signing-tests")) implementation(libs.kotlinx.coroutines.test) + implementation(libs.kotlinx.serialization.json) } } diff --git a/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/BaseSigV4SignatureCalculator.kt b/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/BaseSigV4SignatureCalculator.kt new file mode 100644 index 0000000000..ae90003070 --- /dev/null +++ b/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/BaseSigV4SignatureCalculator.kt @@ -0,0 +1,98 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.smithy.kotlin.runtime.auth.awssigning + +import aws.smithy.kotlin.runtime.hashing.HashSupplier +import aws.smithy.kotlin.runtime.hashing.Sha256 +import aws.smithy.kotlin.runtime.hashing.hash +import aws.smithy.kotlin.runtime.hashing.sha256 +import aws.smithy.kotlin.runtime.text.encoding.encodeToHex +import aws.smithy.kotlin.runtime.time.Instant +import aws.smithy.kotlin.runtime.time.TimestampFormat +import aws.smithy.kotlin.runtime.time.epochMilliseconds + +/** + * Common signature implementation used for SigV4 and SigV4a, primarily for forming the strings-to-sign which don't differ + * between the two signing algorithms (besides their names). + */ +internal abstract class BaseSigV4SignatureCalculator( + val algorithm: AwsSigningAlgorithm, + open val sha256Provider: HashSupplier = ::Sha256, +) : SignatureCalculator { + private val supportedAlgorithms = setOf(AwsSigningAlgorithm.SIGV4, AwsSigningAlgorithm.SIGV4_ASYMMETRIC) + + init { + check(algorithm in supportedAlgorithms) { + "This class should only be used for ${supportedAlgorithms.joinToString()}, got $algorithm" + } + } + + override fun stringToSign(canonicalRequest: String, config: AwsSigningConfig): String = buildString { + appendLine(algorithm.signingName) + appendLine(config.signingDate.format(TimestampFormat.ISO_8601_CONDENSED)) + appendLine(config.credentialScope) + append(canonicalRequest.encodeToByteArray().hash(sha256Provider).encodeToHex()) + } + + override fun chunkStringToSign(chunkBody: ByteArray, prevSignature: ByteArray, config: AwsSigningConfig): String = buildString { + appendLine("${algorithm.signingName}-PAYLOAD") + appendLine(config.signingDate.format(TimestampFormat.ISO_8601_CONDENSED)) + appendLine(config.credentialScope) + appendLine(prevSignature.decodeToString()) // Should already be a byte array of ASCII hex chars + + val nonSignatureHeadersHash = when (config.signatureType) { + AwsSignatureType.HTTP_REQUEST_EVENT -> eventStreamNonSignatureHeaders(config.signingDate) + else -> HashSpecification.EmptyBody.hash + } + + appendLine(nonSignatureHeadersHash) + append(chunkBody.hash(sha256Provider).encodeToHex()) + } + + override fun chunkTrailerStringToSign(trailingHeaders: ByteArray, prevSignature: ByteArray, config: AwsSigningConfig): String = + buildString { + appendLine("${algorithm.signingName}-TRAILER") + appendLine(config.signingDate.format(TimestampFormat.ISO_8601_CONDENSED)) + appendLine(config.credentialScope) + appendLine(prevSignature.decodeToString()) + append(trailingHeaders.hash(sha256Provider).encodeToHex()) + } +} + +private const val HEADER_TIMESTAMP_TYPE: Byte = 8 + +/** + * Return the sha256 hex representation of the encoded event stream date header + * + * ``` + * sha256Hex( Header(":date", HeaderValue::Timestamp(date)).encodeToByteArray() ) + * ``` + * + * NOTE: This duplicates parts of the event stream encoding implementation here to avoid a direct dependency. + * Should this become more involved than encoding a single date header we should reconsider this choice. + * + * see [Event Stream implementation](https://github.com/smithy-lang/smithy-kotlin/blob/612c39ba446e6403ea2bd9a51c4d1db111b6e26f/runtime/protocol/aws-event-stream/common/src/aws/smithy/kotlin/runtime/awsprotocol/eventstream/Header.kt#L52) + */ +private fun eventStreamNonSignatureHeaders(date: Instant): String { + val bytes = ByteArray(15) + // encode header name + val name = ":date".encodeToByteArray() + var offset = 0 + bytes[offset++] = name.size.toByte() + name.copyInto(bytes, destinationOffset = offset) + offset += name.size + + // encode header value + bytes[offset++] = HEADER_TIMESTAMP_TYPE + writeLongBE(bytes, offset, date.epochMilliseconds) + return bytes.sha256().encodeToHex() +} + +private fun writeLongBE(dest: ByteArray, offset: Int, x: Long) { + var idx = offset + for (i in 7 downTo 0) { + dest[idx++] = ((x ushr (i * 8)) and 0xff).toByte() + } +} diff --git a/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/Canonicalizer.kt b/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/Canonicalizer.kt index c7c47fe5a2..37d85f2704 100644 --- a/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/Canonicalizer.kt +++ b/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/Canonicalizer.kt @@ -124,12 +124,13 @@ internal class DefaultCanonicalizer(private val sha256Supplier: HashSupplier = : } param("Host", builder.url.hostAndPort, !signViaQueryParams, overwrite = false) - param("X-Amz-Algorithm", ALGORITHM_NAME, signViaQueryParams) + param("X-Amz-Algorithm", config.algorithm.signingName, signViaQueryParams) param("X-Amz-Credential", credentialValue(config), signViaQueryParams) param("X-Amz-Content-Sha256", hash, addHashHeader) param("X-Amz-Date", config.signingDate.format(TimestampFormat.ISO_8601_CONDENSED)) param("X-Amz-Expires", config.expiresAfter?.inWholeSeconds?.toString(), signViaQueryParams) param("X-Amz-Security-Token", sessionToken, !config.omitSessionToken) // Add pre-sig if omitSessionToken=false + param("X-Amz-Region-Set", config.region, config.algorithm == AwsSigningAlgorithm.SIGV4_ASYMMETRIC) val headers = builder .headers diff --git a/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/DefaultAwsSigner.kt b/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/DefaultAwsSigner.kt index 791d8fce0a..85896349b1 100644 --- a/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/DefaultAwsSigner.kt +++ b/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/DefaultAwsSigner.kt @@ -29,10 +29,15 @@ public class DefaultAwsSignerBuilder { ) } +private val AwsSigningAlgorithm.signatureCalculator + get() = when (this) { + AwsSigningAlgorithm.SIGV4 -> SignatureCalculator.SigV4 + AwsSigningAlgorithm.SIGV4_ASYMMETRIC -> SignatureCalculator.SigV4a + } + @OptIn(ExperimentalApi::class) internal class DefaultAwsSignerImpl( private val canonicalizer: Canonicalizer = Canonicalizer.Default, - private val signatureCalculator: SignatureCalculator = SignatureCalculator.Default, private val requestMutator: RequestMutator = RequestMutator.Default, private val telemetryProvider: TelemetryProvider? = null, ) : AwsSigner { @@ -40,19 +45,13 @@ internal class DefaultAwsSignerImpl( val logger = telemetryProvider?.loggerProvider?.getOrCreateLogger("DefaultAwsSigner") ?: coroutineContext.logger() - // TODO: implement SigV4a - if (config.algorithm != AwsSigningAlgorithm.SIGV4) { - throw UnsupportedSigningAlgorithmException( - "${config.algorithm} support is not yet implemented for the default signer.", - config.algorithm, - ) - } - val canonical = canonicalizer.canonicalRequest(request, config) if (config.logRequest) { logger.trace { "Canonical request:\n${canonical.requestString}" } } + val signatureCalculator = config.algorithm.signatureCalculator + val stringToSign = signatureCalculator.stringToSign(canonical.requestString, config) logger.trace { "String to sign:\n$stringToSign" } @@ -74,6 +73,8 @@ internal class DefaultAwsSignerImpl( val logger = telemetryProvider?.loggerProvider?.getOrCreateLogger("DefaultAwsSigner") ?: coroutineContext.logger() + val signatureCalculator = config.algorithm.signatureCalculator + val stringToSign = signatureCalculator.chunkStringToSign(chunkBody, prevSignature, config) logger.trace { "Chunk string to sign:\n$stringToSign" } @@ -93,6 +94,8 @@ internal class DefaultAwsSignerImpl( val logger = telemetryProvider?.loggerProvider?.getOrCreateLogger("DefaultAwsSigner") ?: coroutineContext.logger() + val signatureCalculator = config.algorithm.signatureCalculator + // FIXME - can we share canonicalization code more than we are..., also this reduce is inefficient. // canonicalize the headers val trailingHeadersBytes = trailingHeaders.entries().sortedBy { e -> e.key.lowercase() } @@ -117,16 +120,16 @@ internal class DefaultAwsSignerImpl( } } -/** The name of the SigV4 algorithm. */ -internal const val ALGORITHM_NAME = "AWS4-HMAC-SHA256" - /** - * Formats a credential scope consisting of a signing date, region, service, and a signature type + * Formats a credential scope consisting of a signing date, region (SigV4 only), service, and a signature type */ internal val AwsSigningConfig.credentialScope: String - get() { + get() = run { val signingDate = signingDate.format(TimestampFormat.ISO_8601_CONDENSED_DATE) - return "$signingDate/$region/$service/aws4_request" + return when (algorithm) { + AwsSigningAlgorithm.SIGV4 -> "$signingDate/$region/$service/aws4_request" + AwsSigningAlgorithm.SIGV4_ASYMMETRIC -> "$signingDate/$service/aws4_request" + } } /** diff --git a/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/RequestMutator.kt b/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/RequestMutator.kt index 5bfe982e54..0c1882cd54 100644 --- a/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/RequestMutator.kt +++ b/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/RequestMutator.kt @@ -43,7 +43,7 @@ internal class DefaultRequestMutator : RequestMutator { val credential = "Credential=${credentialValue(config)}" val signedHeaders = "SignedHeaders=${canonical.signedHeaders}" val signature = "Signature=$signatureHex" - canonical.request.headers["Authorization"] = "$ALGORITHM_NAME $credential, $signedHeaders, $signature" + canonical.request.headers["Authorization"] = "${config.algorithm.signingName} $credential, $signedHeaders, $signature" } AwsSignatureType.HTTP_REQUEST_VIA_QUERY_PARAMS -> diff --git a/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/SigV4SignatureCalculator.kt b/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/SigV4SignatureCalculator.kt new file mode 100644 index 0000000000..c210c964c2 --- /dev/null +++ b/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/SigV4SignatureCalculator.kt @@ -0,0 +1,30 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.smithy.kotlin.runtime.auth.awssigning + +import aws.smithy.kotlin.runtime.hashing.HashSupplier +import aws.smithy.kotlin.runtime.hashing.Sha256 +import aws.smithy.kotlin.runtime.hashing.hmac +import aws.smithy.kotlin.runtime.text.encoding.encodeToHex +import aws.smithy.kotlin.runtime.time.TimestampFormat + +/** + * [SignatureCalculator] for the SigV4 ("AWS4-HMAC-SHA256") algorithm + * @param sha256Provider the [HashSupplier] to use for computing SHA-256 hashes + */ +internal class SigV4SignatureCalculator(override val sha256Provider: HashSupplier = ::Sha256) : BaseSigV4SignatureCalculator(AwsSigningAlgorithm.SIGV4, sha256Provider) { + override fun calculate(signingKey: ByteArray, stringToSign: String): String = + hmac(signingKey, stringToSign.encodeToByteArray(), sha256Provider).encodeToHex() + + override fun signingKey(config: AwsSigningConfig): ByteArray { + fun hmac(key: ByteArray, message: String) = hmac(key, message.encodeToByteArray(), sha256Provider) + + val initialKey = ("AWS4" + config.credentials.secretAccessKey).encodeToByteArray() + val kDate = hmac(initialKey, config.signingDate.format(TimestampFormat.ISO_8601_CONDENSED_DATE)) + val kRegion = hmac(kDate, config.region) + val kService = hmac(kRegion, config.service) + return hmac(kService, "aws4_request") + } +} diff --git a/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/SigV4aSignatureCalculator.kt b/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/SigV4aSignatureCalculator.kt new file mode 100644 index 0000000000..4f4239a2ce --- /dev/null +++ b/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/SigV4aSignatureCalculator.kt @@ -0,0 +1,88 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.smithy.kotlin.runtime.auth.awssigning + +import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials +import aws.smithy.kotlin.runtime.collections.ReadThroughCache +import aws.smithy.kotlin.runtime.content.BigInteger +import aws.smithy.kotlin.runtime.hashing.HashSupplier +import aws.smithy.kotlin.runtime.hashing.Sha256 +import aws.smithy.kotlin.runtime.hashing.ecdsaSecp256r1 +import aws.smithy.kotlin.runtime.hashing.hmac +import aws.smithy.kotlin.runtime.text.encoding.decodeHexBytes +import aws.smithy.kotlin.runtime.text.encoding.encodeToHex +import aws.smithy.kotlin.runtime.time.Instant +import aws.smithy.kotlin.runtime.util.ExpiringValue +import kotlinx.coroutines.runBlocking +import kotlin.time.Duration.Companion.hours + +/** + * The maximum number of iterations to attempt private key derivation using KDF in counter mode + * Taken from CRT: https://github.com/awslabs/aws-c-auth/blob/e8360a65e0f3337d4ac827945e00c3b55a641a5f/source/key_derivation.c#L22 + */ +internal const val MAX_KDF_COUNTER_ITERATIONS = 254.toByte() + +// N value from NIST P-256 curve, minus two. +internal val N_MINUS_TWO = "FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC63254F".decodeHexBytes().toPositiveBigInteger() + +/** + * A [SignatureCalculator] for the SigV4a ("AWS4-ECDSA-P256-SHA256") algorithm. + * @param sha256Provider the [HashSupplier] to use for computing SHA-256 hashes + */ +internal class SigV4aSignatureCalculator(override val sha256Provider: HashSupplier = ::Sha256) : BaseSigV4SignatureCalculator(AwsSigningAlgorithm.SIGV4_ASYMMETRIC, sha256Provider) { + private val privateKeyCache = ReadThroughCache( + minimumSweepPeriod = 1.hours, // note: Sweeps are effectively a no-op because expiration is [Instant.MAX_VALUE] + ) + + override fun calculate(signingKey: ByteArray, stringToSign: String): String = ecdsaSecp256r1(signingKey, stringToSign.encodeToByteArray()).encodeToHex() + + /** + * Retrieve a signing key based on the signing credentials. If not cached, the key will be derived using a counter-based key derivation function (KDF) + * as specified in NIST SP 800-108. + * + * See https://github.com/awslabs/aws-c-auth/blob/e8360a65e0f3337d4ac827945e00c3b55a641a5f/source/key_derivation.c#L70 and + * https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-create-signed-request.html#derive-signing-key-sigv4a for + * more information on the derivation process. + */ + override fun signingKey(config: AwsSigningConfig): ByteArray = runBlocking { + privateKeyCache.get(config.credentials) { + var counter: Byte = 1 + var privateKey: ByteArray + + val inputKey = ("AWS4A" + config.credentials.secretAccessKey).encodeToByteArray() + + do { + val k0 = hmac(inputKey, fixedInputString(config.credentials.accessKeyId, counter), sha256Provider) + + val c = k0.toPositiveBigInteger() + privateKey = (c + BigInteger("1")).toByteArray() + + if (counter == MAX_KDF_COUNTER_ITERATIONS && c > N_MINUS_TWO) { + throw IllegalStateException("Counter exceeded maximum length") + } else { + counter++ + } + } while (c > N_MINUS_TWO) + + ExpiringValue(privateKey, Instant.MAX_VALUE) + } + } + + /** + * Forms the fixed input string used for ECDSA private key derivation + * The final output looks like: + * 0x00000001 || "AWS4-ECDSA-P256-SHA256" || 0x00 || AccessKeyId || counter || 0x00000100 + */ + private fun fixedInputString(accessKeyId: String, counter: Byte): ByteArray = + byteArrayOf(0x00, 0x00, 0x00, 0x01) + + AwsSigningAlgorithm.SIGV4_ASYMMETRIC.signingName.encodeToByteArray() + + byteArrayOf(0x00) + + accessKeyId.encodeToByteArray() + + counter + + byteArrayOf(0x00, 0x00, 0x01, 0x00) +} + +// Convert [this] [ByteArray] to a positive [BigInteger] by prepending 0x00. +private fun ByteArray.toPositiveBigInteger() = BigInteger(byteArrayOf(0x00) + this) diff --git a/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/SignatureCalculator.kt b/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/SignatureCalculator.kt index fe27320ffc..80652e6dbf 100644 --- a/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/SignatureCalculator.kt +++ b/runtime/auth/aws-signing-default/common/src/aws/smithy/kotlin/runtime/auth/awssigning/SignatureCalculator.kt @@ -4,21 +4,20 @@ */ package aws.smithy.kotlin.runtime.auth.awssigning -import aws.smithy.kotlin.runtime.hashing.* -import aws.smithy.kotlin.runtime.text.encoding.encodeToHex -import aws.smithy.kotlin.runtime.time.Instant -import aws.smithy.kotlin.runtime.time.TimestampFormat -import aws.smithy.kotlin.runtime.time.epochMilliseconds - /** * An object that can calculate signatures based on canonical requests. */ internal interface SignatureCalculator { companion object { /** - * The default implementation of [SignatureCalculator]. + * The SigV4 implementation of [SignatureCalculator]. + */ + val SigV4 = SigV4SignatureCalculator() + + /** + * The SigV4a implementation of [SignatureCalculator]. */ - val Default = DefaultSignatureCalculator() + val SigV4a = SigV4aSignatureCalculator() } /** @@ -62,87 +61,3 @@ internal interface SignatureCalculator { */ fun chunkTrailerStringToSign(trailingHeaders: ByteArray, prevSignature: ByteArray, config: AwsSigningConfig): String } - -internal class DefaultSignatureCalculator(private val sha256Provider: HashSupplier = ::Sha256) : SignatureCalculator { - override fun calculate(signingKey: ByteArray, stringToSign: String): String = - hmac(signingKey, stringToSign.encodeToByteArray(), sha256Provider).encodeToHex() - - override fun chunkStringToSign(chunkBody: ByteArray, prevSignature: ByteArray, config: AwsSigningConfig): String = - buildString { - appendLine("AWS4-HMAC-SHA256-PAYLOAD") - appendLine(config.signingDate.format(TimestampFormat.ISO_8601_CONDENSED)) - appendLine(config.credentialScope) - appendLine(prevSignature.decodeToString()) // Should already be a byte array of ASCII hex chars - - val nonSignatureHeadersHash = when (config.signatureType) { - AwsSignatureType.HTTP_REQUEST_EVENT -> eventStreamNonSignatureHeaders(config.signingDate) - else -> HashSpecification.EmptyBody.hash - } - - appendLine(nonSignatureHeadersHash) - append(chunkBody.hash(sha256Provider).encodeToHex()) - } - - override fun chunkTrailerStringToSign(trailingHeaders: ByteArray, prevSignature: ByteArray, config: AwsSigningConfig): String = - buildString { - appendLine("AWS4-HMAC-SHA256-TRAILER") - appendLine(config.signingDate.format(TimestampFormat.ISO_8601_CONDENSED)) - appendLine(config.credentialScope) - appendLine(prevSignature.decodeToString()) - append(trailingHeaders.hash(sha256Provider).encodeToHex()) - } - - override fun signingKey(config: AwsSigningConfig): ByteArray { - fun hmac(key: ByteArray, message: String) = hmac(key, message.encodeToByteArray(), sha256Provider) - - val initialKey = ("AWS4" + config.credentials.secretAccessKey).encodeToByteArray() - val kDate = hmac(initialKey, config.signingDate.format(TimestampFormat.ISO_8601_CONDENSED_DATE)) - val kRegion = hmac(kDate, config.region) - val kService = hmac(kRegion, config.service) - return hmac(kService, "aws4_request") - } - - override fun stringToSign(canonicalRequest: String, config: AwsSigningConfig): String = - buildString { - appendLine("AWS4-HMAC-SHA256") - appendLine(config.signingDate.format(TimestampFormat.ISO_8601_CONDENSED)) - appendLine(config.credentialScope) - append(canonicalRequest.encodeToByteArray().hash(sha256Provider).encodeToHex()) - } -} - -private const val HEADER_TIMESTAMP_TYPE: Byte = 8 - -/** - * Return the sha256 hex representation of the encoded event stream date header - * - * ``` - * sha256Hex( Header(":date", HeaderValue::Timestamp(date)).encodeToByteArray() ) - * ``` - * - * NOTE: This duplicates parts of the event stream encoding implementation here to avoid a direct dependency. - * Should this become more involved than encoding a single date header we should reconsider this choice. - * - * see [Event Stream implementation](https://github.com/awslabs/aws-sdk-kotlin/blob/v0.16.4-beta/aws-runtime/protocols/aws-event-stream/common/src/aws/sdk/kotlin/runtime/protocol/eventstream/Header.kt#L51) - */ -private fun eventStreamNonSignatureHeaders(date: Instant): String { - val bytes = ByteArray(15) - // encode header name - val name = ":date".encodeToByteArray() - var offset = 0 - bytes[offset++] = name.size.toByte() - name.copyInto(bytes, destinationOffset = offset) - offset += name.size - - // encode header value - bytes[offset++] = HEADER_TIMESTAMP_TYPE - writeLongBE(bytes, offset, date.epochMilliseconds) - return bytes.sha256().encodeToHex() -} - -private fun writeLongBE(dest: ByteArray, offset: Int, x: Long) { - var idx = offset - for (i in 7 downTo 0) { - dest[idx++] = ((x ushr (i * 8)) and 0xff).toByte() - } -} diff --git a/runtime/auth/aws-signing-default/common/test/aws/smithy/kotlin/runtime/auth/awssigning/DefaultBasicSigningTest.kt b/runtime/auth/aws-signing-default/common/test/aws/smithy/kotlin/runtime/auth/awssigning/DefaultBasicSigningTest.kt index 5a36a1fd46..ba766f1692 100644 --- a/runtime/auth/aws-signing-default/common/test/aws/smithy/kotlin/runtime/auth/awssigning/DefaultBasicSigningTest.kt +++ b/runtime/auth/aws-signing-default/common/test/aws/smithy/kotlin/runtime/auth/awssigning/DefaultBasicSigningTest.kt @@ -5,14 +5,7 @@ package aws.smithy.kotlin.runtime.auth.awssigning import aws.smithy.kotlin.runtime.auth.awssigning.tests.BasicSigningTestBase -import kotlinx.coroutines.test.TestResult -import kotlin.test.Ignore -import kotlin.test.Test class DefaultBasicSigningTest : BasicSigningTestBase() { override val signer: AwsSigner = DefaultAwsSigner - - @Ignore - @Test - override fun testSignRequestSigV4Asymmetric(): TestResult = TODO("Add support for SigV4a in default signer") } diff --git a/runtime/auth/aws-signing-default/common/test/aws/smithy/kotlin/runtime/auth/awssigning/DefaultRequestMutatorTest.kt b/runtime/auth/aws-signing-default/common/test/aws/smithy/kotlin/runtime/auth/awssigning/DefaultRequestMutatorTest.kt index 7eb10ab692..5cb36c9a3f 100644 --- a/runtime/auth/aws-signing-default/common/test/aws/smithy/kotlin/runtime/auth/awssigning/DefaultRequestMutatorTest.kt +++ b/runtime/auth/aws-signing-default/common/test/aws/smithy/kotlin/runtime/auth/awssigning/DefaultRequestMutatorTest.kt @@ -16,11 +16,12 @@ import kotlin.test.assertEquals class DefaultRequestMutatorTest { @Test - fun testAppendAuthHeader() { + fun testSigV4AppendAuthHeader() { val canonical = CanonicalRequest(baseRequest.toBuilder(), "", "action;host;x-amz-date", "") val signature = "0123456789abcdef" val config = AwsSigningConfig { + algorithm = AwsSigningAlgorithm.SIGV4 region = "us-west-2" service = "fooservice" signingDate = Instant.fromIso8601("20220427T012345Z") @@ -45,6 +46,41 @@ class DefaultRequestMutatorTest { assertEquals(expectedHeaders, mutated.headers.entries()) } + + @Test + fun testSigV4aAppendAuthHeader() { + val canonical = CanonicalRequest(baseRequest.toBuilder(), "", "action;host;x-amz-date", "") + val signature = "0123456789abcdef" + + val config = AwsSigningConfig { + algorithm = AwsSigningAlgorithm.SIGV4_ASYMMETRIC + region = "us-west-2" + service = "fooservice" + signingDate = Instant.fromIso8601("20220427T012345Z") + credentials = Credentials("", "secret key") + omitSessionToken = true + } + + val mutated = RequestMutator.Default.appendAuth(config, canonical, signature) + + assertEquals(baseRequest.method, mutated.method) + assertEquals(baseRequest.url.toString(), mutated.url.toString()) + assertEquals(baseRequest.body, mutated.body) + + val expectedCredentialScope = "20220427/fooservice/aws4_request" + val expectedAuthValue = buildString { + append("AWS4-ECDSA-P256-SHA256 ") + append("Credential=${config.credentials.accessKeyId}/$expectedCredentialScope, ") + append("SignedHeaders=${canonical.signedHeaders}, ") + append("Signature=$signature") + } + val expectedHeaders = Headers { + appendAll(baseRequest.headers) + append("Authorization", expectedAuthValue) + }.entries() + + assertEquals(expectedHeaders, mutated.headers.entries()) + } } private val baseRequest = HttpRequest { diff --git a/runtime/auth/aws-signing-default/common/test/aws/smithy/kotlin/runtime/auth/awssigning/DefaultSignatureCalculatorTest.kt b/runtime/auth/aws-signing-default/common/test/aws/smithy/kotlin/runtime/auth/awssigning/SigV4SignatureCalculatorTest.kt similarity index 92% rename from runtime/auth/aws-signing-default/common/test/aws/smithy/kotlin/runtime/auth/awssigning/DefaultSignatureCalculatorTest.kt rename to runtime/auth/aws-signing-default/common/test/aws/smithy/kotlin/runtime/auth/awssigning/SigV4SignatureCalculatorTest.kt index fc5c23304c..b1df30fb5b 100644 --- a/runtime/auth/aws-signing-default/common/test/aws/smithy/kotlin/runtime/auth/awssigning/DefaultSignatureCalculatorTest.kt +++ b/runtime/auth/aws-signing-default/common/test/aws/smithy/kotlin/runtime/auth/awssigning/SigV4SignatureCalculatorTest.kt @@ -14,7 +14,7 @@ import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertEquals -class DefaultSignatureCalculatorTest { +class SigV4SignatureCalculatorTest { // Test adapted from https://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html @Test fun testCalculate() { @@ -27,7 +27,7 @@ class DefaultSignatureCalculatorTest { """.trimIndent() val expected = "5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7" - val actual = SignatureCalculator.Default.calculate(signingKey, stringToSign) + val actual = SignatureCalculator.SigV4.calculate(signingKey, stringToSign) assertEquals(expected, actual) } @@ -42,7 +42,7 @@ class DefaultSignatureCalculatorTest { } val expected = "c4afb1cc5771d871763a393e44b703571b55cc28424d1a5e86da6ed3c154a4b9" - val actual = SignatureCalculator.Default.signingKey(config).encodeToHex() + val actual = SignatureCalculator.SigV4.signingKey(config).encodeToHex() assertEquals(expected, actual) } @@ -74,7 +74,7 @@ class DefaultSignatureCalculatorTest { 20150830/us-east-1/iam/aws4_request f536975d06c0309214f805bb90ccff089219ecd68b2577efef23edd43b7e1a59 """.trimIndent() - val actual = SignatureCalculator.Default.stringToSign(canonicalRequest, config) + val actual = SignatureCalculator.SigV4.stringToSign(canonicalRequest, config) assertEquals(expected, actual) } @@ -114,7 +114,7 @@ class DefaultSignatureCalculatorTest { 813ca5285c28ccee5cab8b10ebda9c908fd6d78ed9dc94cc65ea6cb67a7f13ae """.trimIndent() - val actual = SignatureCalculator.Default.chunkStringToSign(chunkBody, prevSignature, config) + val actual = SignatureCalculator.SigV4.chunkStringToSign(chunkBody, prevSignature, config) assertEquals(expected, actual) } } diff --git a/runtime/auth/aws-signing-default/common/test/aws/smithy/kotlin/runtime/auth/awssigning/SigV4aSignatureCalculatorTest.kt b/runtime/auth/aws-signing-default/common/test/aws/smithy/kotlin/runtime/auth/awssigning/SigV4aSignatureCalculatorTest.kt new file mode 100644 index 0000000000..c4c4abe953 --- /dev/null +++ b/runtime/auth/aws-signing-default/common/test/aws/smithy/kotlin/runtime/auth/awssigning/SigV4aSignatureCalculatorTest.kt @@ -0,0 +1,121 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.smithy.kotlin.runtime.auth.awssigning + +import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials +import aws.smithy.kotlin.runtime.time.Instant +import aws.smithy.kotlin.runtime.util.PlatformProvider +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonObject +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +private const val SIGV4A_RESOURCES_BASE = "../aws-signing-tests/common/resources/aws-signing-test-suite/v4a" + +/** + * Tests which are defined in resources/sigv4a. + * Copied directly from https://github.com/awslabs/aws-c-auth/tree/e8360a65e0f3337d4ac827945e00c3b55a641a5f/tests/aws-signing-test-suite/v4a. + * get-vanilla-query-order-key and get-vanilla-query-order-value were deleted since they are not complete tests. + */ +private val TESTS = listOf( + "get-header-key-duplicate", + "get-header-value-multiline", + "get-header-value-order", + "get-header-value-trim", + "get-relative-normalized", + "get-relative-relative-normalized", + "get-relative-relative-unnormalized", + "get-relative-unnormalized", + "get-slash-dot-slash-normalized", + "get-slash-dot-slash-unnormalized", + "get-slash-normalized", + "get-slash-pointless-dot-normalized", + "get-slash-pointless-dot-unnormalized", + "get-slash-unnormalized", + "get-slashes-normalized", + "get-slashes-unnormalized", + "get-space-normalized", + "get-space-unnormalized", + "get-unreserved", + "get-utf8", + "get-vanilla", + "get-vanilla-empty-query-key", + "get-vanilla-query", + "get-vanilla-query-order-encoded", + "get-vanilla-query-order-key-case", + "get-vanilla-query-unreserved", + "get-vanilla-utf8-query", + "get-vanilla-with-session-token", + "post-header-key-case", + "post-header-key-sort", + "post-header-value-case", + "post-sts-header-after", + "post-sts-header-before", + "post-vanilla", + "post-vanilla-empty-query-value", + "post-vanilla-query", + "post-x-www-form-urlencoded", + "post-x-www-form-urlencoded-parameters", +) + +// TODO Add tests against header-signature.txt when java.security implements RFC 6979 / deterministic ECDSA. https://bugs.openjdk.org/browse/JDK-8239382 +/** + * Tests for [SigV4aSignatureCalculator]. Currently only tests forming the string-to-sign. + */ +class SigV4aSignatureCalculatorTest { + @Test + fun testStringToSign() = TESTS.forEach { testId -> + runTest { + val testDir = "$SIGV4A_RESOURCES_BASE/$testId/" + assertTrue(PlatformProvider.System.fileExists(testDir), "Failed to find test directory for $testId") + + val context = Json.parseToJsonElement(testDir.fileContents("context.json")).jsonObject + val signingConfig = context.parseAwsSigningConfig() + + val expectedStringToSign = testDir.fileContents("header-string-to-sign.txt") + val canonicalRequest = testDir.fileContents("header-canonical-request.txt") + val actualStringToSign = SignatureCalculator.SigV4a.stringToSign(canonicalRequest, signingConfig) + + assertEquals(expectedStringToSign, actualStringToSign, "$testId failed") + } + } + + private fun JsonObject.parseAwsSigningConfig(): AwsSigningConfig { + fun JsonObject.getStringValue(key: String): String { + val value = checkNotNull(get(key)) { "Failed to find key $key in JSON object $this" } + return value.toString().replace("\"", "") + } + + val contextCredentials = checkNotNull(get("credentials")?.jsonObject) { "credentials unexpectedly null" } + + val credentials = Credentials( + contextCredentials.getStringValue("access_key_id"), + contextCredentials.getStringValue("secret_access_key"), + ) + + val region = getStringValue("region") + val service = getStringValue("service") + val signingDate = Instant.fromIso8601(getStringValue("timestamp")) + + return AwsSigningConfig { + algorithm = AwsSigningAlgorithm.SIGV4_ASYMMETRIC + this.credentials = credentials + this.region = region + this.service = service + this.signingDate = signingDate + } + } + + private suspend fun String.fileContents(path: String): String = checkNotNull( + PlatformProvider.System.readFileOrNull(this + path) + ?.decodeToString() + ?.replace("\r\n", "\n"), + ) { + "Unable to read contents at ${this + path}" + } +} diff --git a/runtime/auth/aws-signing-default/jvm/test/aws/smithy/kotlin/runtime/auth/awssigning/DefaultSigningSuiteTest.kt b/runtime/auth/aws-signing-default/jvm/test/aws/smithy/kotlin/runtime/auth/awssigning/DefaultSigningSuiteTest.kt index 614c2fe016..1edbca9f88 100644 --- a/runtime/auth/aws-signing-default/jvm/test/aws/smithy/kotlin/runtime/auth/awssigning/DefaultSigningSuiteTest.kt +++ b/runtime/auth/aws-signing-default/jvm/test/aws/smithy/kotlin/runtime/auth/awssigning/DefaultSigningSuiteTest.kt @@ -17,13 +17,13 @@ class DefaultSigningSuiteTest : SigningSuiteTestBase() { override val signatureProvider: SigningStateProvider = { request, config -> val canonical = Canonicalizer.Default.canonicalRequest(request, config) - val stringToSign = SignatureCalculator.Default.stringToSign(canonical.requestString, config) - val signingKey = SignatureCalculator.Default.signingKey(config) - SignatureCalculator.Default.calculate(signingKey, stringToSign) + val stringToSign = SignatureCalculator.SigV4.stringToSign(canonical.requestString, config) + val signingKey = SignatureCalculator.SigV4.signingKey(config) + SignatureCalculator.SigV4.calculate(signingKey, stringToSign) } override val stringToSignProvider: SigningStateProvider = { request, config -> val canonical = Canonicalizer.Default.canonicalRequest(request, config) - SignatureCalculator.Default.stringToSign(canonical.requestString, config) + SignatureCalculator.SigV4.stringToSign(canonical.requestString, config) } } diff --git a/runtime/runtime-core/api/runtime-core.api b/runtime/runtime-core/api/runtime-core.api index 0d5399d430..576903f1cf 100644 --- a/runtime/runtime-core/api/runtime-core.api +++ b/runtime/runtime-core/api/runtime-core.api @@ -693,6 +693,10 @@ public final class aws/smithy/kotlin/runtime/hashing/Crc32cKt { public static final fun crc32c ([B)I } +public final class aws/smithy/kotlin/runtime/hashing/EcdsaJVMKt { + public static final fun ecdsaSecp256r1 ([B[B)[B +} + public abstract interface class aws/smithy/kotlin/runtime/hashing/HashFunction { public abstract fun digest ()[B public abstract fun getBlockSizeBytes ()I diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/hashing/Ecdsa.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/hashing/Ecdsa.kt new file mode 100644 index 0000000000..ad58fc118b --- /dev/null +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/hashing/Ecdsa.kt @@ -0,0 +1,10 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.smithy.kotlin.runtime.hashing + +/** + * ECDSA on the SECP256R1 curve. + */ +public expect fun ecdsaSecp256r1(key: ByteArray, message: ByteArray): ByteArray diff --git a/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/hashing/EcdsaJVM.kt b/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/hashing/EcdsaJVM.kt new file mode 100644 index 0000000000..2f2e86b139 --- /dev/null +++ b/runtime/runtime-core/jvm/src/aws/smithy/kotlin/runtime/hashing/EcdsaJVM.kt @@ -0,0 +1,37 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.smithy.kotlin.runtime.hashing + +import aws.smithy.kotlin.runtime.content.BigInteger +import java.security.* +import java.security.interfaces.* +import java.security.spec.* + +/** + * ECDSA on the SECP256R1 curve. + */ +public actual fun ecdsaSecp256r1(key: ByteArray, message: ByteArray): ByteArray { + // Convert private key to BigInteger + val d = BigInteger(key) + + // Create key pair generator to get curve parameters + val keyGen = KeyPairGenerator.getInstance("EC").apply { + initialize(ECGenParameterSpec("secp256r1")) + } + val params = (keyGen.generateKeyPair().private as ECPrivateKey).params + + // Create private key directly from the provided key bytes + val privateKeySpec = ECPrivateKeySpec(d.toJvm(), params) + val keyFactory = KeyFactory.getInstance("EC") + val privateKey = keyFactory.generatePrivate(privateKeySpec) + + // Sign the message + return Signature.getInstance("SHA256withECDSA").apply { + initSign(privateKey) + update(message) + }.sign() +} + +private fun BigInteger.toJvm(): java.math.BigInteger = java.math.BigInteger(1, toByteArray()) diff --git a/runtime/runtime-core/jvm/test/aws/smithy/kotlin/runtime/hashing/EcdsaJVMTest.kt b/runtime/runtime-core/jvm/test/aws/smithy/kotlin/runtime/hashing/EcdsaJVMTest.kt new file mode 100644 index 0000000000..0072d112d1 --- /dev/null +++ b/runtime/runtime-core/jvm/test/aws/smithy/kotlin/runtime/hashing/EcdsaJVMTest.kt @@ -0,0 +1,84 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.smithy.kotlin.runtime.hashing + +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import java.security.* +import java.security.interfaces.* +import java.security.spec.* +import kotlin.test.Test + +class EcdsaJVMTest { + // Helper function to generate valid test key + private fun generateValidPrivateKey(): ByteArray { + val keyGen = KeyPairGenerator.getInstance("EC") + keyGen.initialize(ECGenParameterSpec("secp256r1")) + val keyPair = keyGen.generateKeyPair() + val privateKey = keyPair.private as ECPrivateKey + return privateKey.s.toByteArray() + } + + @Test + fun testValidSignature() { + val privateKey = generateValidPrivateKey() + val message = "Hello, World!".toByteArray() + + val signature = ecdsaSecp256r1(privateKey, message) + + assertTrue(signature.isNotEmpty()) + assertTrue(signature.size >= 64) // ECDSA signatures are typically 70-72 bytes in DER format + } + + @Test + fun testDifferentMessages() { + val privateKey = generateValidPrivateKey() + val message1 = "Hello, World!".toByteArray() + val message2 = "Different message".toByteArray() + + val signature1 = ecdsaSecp256r1(privateKey, message1) + val signature2 = ecdsaSecp256r1(privateKey, message2) + + assertTrue(signature1.isNotEmpty()) + assertTrue(signature2.isNotEmpty()) + assertFalse(signature1.contentEquals(signature2)) + } + + @Test + fun testEmptyMessage() { + val privateKey = generateValidPrivateKey() + val message = ByteArray(0) + + val signature = ecdsaSecp256r1(privateKey, message) + assertTrue(signature.isNotEmpty()) + } + + @Test + fun testLargeMessage() { + val privateKey = generateValidPrivateKey() + val largeMessage = ByteArray(1000000) { it.toByte() } + + val signature = ecdsaSecp256r1(privateKey, largeMessage) + assertTrue(signature.isNotEmpty()) + } + + @Test + fun testVerifySignature() { + val keyGen = KeyPairGenerator.getInstance("EC") + keyGen.initialize(ECGenParameterSpec("secp256r1")) + val keyPair = keyGen.generateKeyPair() + val privateKey = (keyPair.private as ECPrivateKey).s.toByteArray() + val publicKey = keyPair.public + + val message = "Hello, World!".toByteArray() + val signature = ecdsaSecp256r1(privateKey, message) + + val verifier = Signature.getInstance("SHA256withECDSA") + verifier.initVerify(publicKey) + verifier.update(message) + + assertTrue(verifier.verify(signature)) + } +} diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/hashing/EcdsaNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/hashing/EcdsaNative.kt new file mode 100644 index 0000000000..a2bca1c4e6 --- /dev/null +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/hashing/EcdsaNative.kt @@ -0,0 +1,13 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.smithy.kotlin.runtime.hashing + +// FIXME Implement using aws-c-cal: https://github.com/awslabs/aws-c-cal/blob/main/include/aws/cal/ecc.h +// Will need to be implemented and exposed in aws-crt-kotlin. Or maybe we can _only_ offer the CRT signer on Native? +// Will require updating DefaultAwsSigner to be expect/actual and set to CrtSigner on Native. +/** + * ECDSA on the SECP256R1 curve. + */ +public actual fun ecdsaSecp256r1(key: ByteArray, message: ByteArray): ByteArray = TODO("Not yet implemented") From 0df4e42b22d440031f07dfff6eb195e6510e1b58 Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Mon, 24 Feb 2025 20:12:31 +0000 Subject: [PATCH 48/79] chore: release 1.4.5 --- .changes/4b6debe1-7706-484a-8599-ef8c14cecde2.json | 5 ----- CHANGELOG.md | 5 +++++ gradle.properties | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) delete mode 100644 .changes/4b6debe1-7706-484a-8599-ef8c14cecde2.json diff --git a/.changes/4b6debe1-7706-484a-8599-ef8c14cecde2.json b/.changes/4b6debe1-7706-484a-8599-ef8c14cecde2.json deleted file mode 100644 index 6770d0caa6..0000000000 --- a/.changes/4b6debe1-7706-484a-8599-ef8c14cecde2.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "id": "4b6debe1-7706-484a-8599-ef8c14cecde2", - "type": "feature", - "description": "Add SigV4a support to the default AWS signer" -} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c0bee275f..7d95e5e269 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## [1.4.5] - 02/24/2025 + +### Features +* Add SigV4a support to the default AWS signer + ## [1.4.4] - 02/18/2025 ### Miscellaneous diff --git a/gradle.properties b/gradle.properties index f178e06d20..2093fcb77d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.4.5-SNAPSHOT +sdkVersion=1.4.5 # codegen -codegenVersion=0.34.5-SNAPSHOT \ No newline at end of file +codegenVersion=0.34.5 \ No newline at end of file From 7430722de7869f6b73fea99fde168beca2cc87e7 Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Mon, 24 Feb 2025 20:12:32 +0000 Subject: [PATCH 49/79] chore: bump snapshot version to 1.4.6-SNAPSHOT --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 2093fcb77d..de71e13446 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.4.5 +sdkVersion=1.4.6-SNAPSHOT # codegen -codegenVersion=0.34.5 \ No newline at end of file +codegenVersion=0.34.6-SNAPSHOT \ No newline at end of file From 80d395bb57466f1434f486ab3361e24b3c1c2d28 Mon Sep 17 00:00:00 2001 From: 0marperez <60363173+0marperez@users.noreply.github.com> Date: Tue, 25 Feb 2025 10:38:41 -0500 Subject: [PATCH 50/79] fix: account ID based endpoints (#1245) --- .../KotlinJmespathExpressionVisitor.kt | 39 ++++++++++++++----- .../endpoints/OperationContextParamsTest.kt | 29 +++++++++++++- .../waiter-tests/model/function-keys.smithy | 14 +++++++ .../test/kotlin/com/test/FunctionKeysTest.kt | 9 +++++ 4 files changed, 81 insertions(+), 10 deletions(-) diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/waiters/KotlinJmespathExpressionVisitor.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/waiters/KotlinJmespathExpressionVisitor.kt index 20b07454e4..a7b28180f2 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/waiters/KotlinJmespathExpressionVisitor.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/waiters/KotlinJmespathExpressionVisitor.kt @@ -19,16 +19,26 @@ import software.amazon.smithy.kotlin.codegen.model.isEnum import software.amazon.smithy.kotlin.codegen.model.targetOrSelf import software.amazon.smithy.kotlin.codegen.model.traits.OperationInput import software.amazon.smithy.kotlin.codegen.model.traits.OperationOutput +import software.amazon.smithy.kotlin.codegen.utils.doubleQuote import software.amazon.smithy.kotlin.codegen.utils.dq import software.amazon.smithy.kotlin.codegen.utils.getOrNull import software.amazon.smithy.kotlin.codegen.utils.toCamelCase -import software.amazon.smithy.model.shapes.ListShape -import software.amazon.smithy.model.shapes.MapShape -import software.amazon.smithy.model.shapes.MemberShape -import software.amazon.smithy.model.shapes.Shape +import software.amazon.smithy.model.shapes.* private val suffixSequence = sequenceOf("") + generateSequence(2) { it + 1 }.map(Int::toString) // "", "2", "3", etc. +/* +JMESPath has the concept of objects. +This visitor assumes JMESPath objects will be Kotlin objects/classes. +The smithy spec contains an instance where it's assumed a JMESPath object will be a Kotlin map. + +Specifically it's the keys function +Smithy spec: https://smithy.io/2.0/additional-specs/rules-engine/parameters.html#smithy-rules-operationcontextparams-trait +JMESPath spec: https://jmespath.org/specification.html#keys + +TODO: Test relevant uses of JMESPath to determine if we should support JMESPath objects as Kotlin maps throughout the entire visitor + */ + /** * An [ExpressionVisitor] used for traversing a JMESPath expression to generate code for traversing an equivalent * modeled object. This visitor is passed to [JmespathExpression.accept], at which point specific expression methods @@ -526,11 +536,22 @@ class KotlinJmespathExpressionVisitor( ".$expr" } - private fun VisitedExpression.getKeys(): String { - val keys = this.shape?.targetOrSelf(ctx.model)?.allMembers - ?.keys?.joinToString(", ", "listOf(", ")") { "\"$it\"" } - return keys ?: "listOf()" - } + /* + Smithy spec expects a map, JMESPath spec expects an object + Smithy spec: https://smithy.io/2.0/additional-specs/rules-engine/parameters.html#smithy-rules-operationcontextparams-trait + JMESPath spec: https://jmespath.org/specification.html#keys + */ + private fun VisitedExpression.getKeys(): String = + if (shape?.targetOrSelf(ctx.model)?.type == ShapeType.MAP) { + "$identifier?.keys?.map { it.toString() }?.toList()" + } else { + shape + ?.targetOrSelf(ctx.model) + ?.allMembers + ?.keys + ?.joinToString(", ", "listOf(", ")") { it.doubleQuote() } + ?: "listOf()" + } private fun VisitedExpression.getValues(): String { val values = this.shape?.targetOrSelf(ctx.model)?.allMembers?.keys diff --git a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/endpoints/OperationContextParamsTest.kt b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/endpoints/OperationContextParamsTest.kt index 1e7feeb172..6d5d713a49 100644 --- a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/endpoints/OperationContextParamsTest.kt +++ b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/endpoints/OperationContextParamsTest.kt @@ -88,7 +88,7 @@ class OperationContextParamsTest { } @Test - fun testKeysFunctionPath() { + fun testKeysFunctionPathWithStructure() { val input = """ structure TestOperationRequest { Object: Object @@ -114,4 +114,31 @@ class OperationContextParamsTest { codegen(pathResultType, path, input).shouldContainOnlyOnceWithDiff(expected) } + + @Test + fun testKeysFunctionPathWithMap() { + val input = """ + structure TestOperationRequest { + Object: StringMap + } + + map StringMap { + key: String + value: String + } + """.trimIndent() + + val path = "keys(Object)" + val pathResultType = "stringArray" + + val expected = """ + @Suppress("UNCHECKED_CAST") + val input = request.context[HttpOperationContext.OperationInput] as TestOperationRequest + val object = input.object + val keys = object?.keys?.map { it.toString() }?.toList() + builder.foo = keys + """.formatForTest(" ") + + codegen(pathResultType, path, input).shouldContainOnlyOnceWithDiff(expected) + } } diff --git a/tests/codegen/waiter-tests/model/function-keys.smithy b/tests/codegen/waiter-tests/model/function-keys.smithy index c35989f345..f625ee0791 100644 --- a/tests/codegen/waiter-tests/model/function-keys.smithy +++ b/tests/codegen/waiter-tests/model/function-keys.smithy @@ -32,6 +32,20 @@ use smithy.waiters#waitable } ] }, + KeysFunctionMapStringEquals: { + acceptors: [ + { + state: "success", + matcher: { + output: { + path: "keys(maps.strings)", + expected: "key", + comparator: "anyStringEquals" + } + } + } + ] + }, ) @readonly @http(method: "GET", uri: "/keys/{name}", code: 200) diff --git a/tests/codegen/waiter-tests/src/test/kotlin/com/test/FunctionKeysTest.kt b/tests/codegen/waiter-tests/src/test/kotlin/com/test/FunctionKeysTest.kt index 26b11a9d1b..f39a193685 100644 --- a/tests/codegen/waiter-tests/src/test/kotlin/com/test/FunctionKeysTest.kt +++ b/tests/codegen/waiter-tests/src/test/kotlin/com/test/FunctionKeysTest.kt @@ -5,10 +5,12 @@ package com.test +import com.test.model.EntityMaps import com.test.model.EntityPrimitives import com.test.model.GetFunctionKeysEqualsRequest import com.test.model.GetFunctionKeysEqualsResponse import com.test.utils.successTest +import com.test.waiters.waitUntilKeysFunctionMapStringEquals import com.test.waiters.waitUntilKeysFunctionPrimitivesIntegerEquals import com.test.waiters.waitUntilKeysFunctionPrimitivesStringEquals import kotlin.test.Test @@ -27,4 +29,11 @@ class FunctionKeysTest { WaitersTestClient::waitUntilKeysFunctionPrimitivesIntegerEquals, GetFunctionKeysEqualsResponse { primitives = EntityPrimitives { } }, ) + + @Test + fun testKeysFunctionMapStringEquals() = successTest( + GetFunctionKeysEqualsRequest { name = "test" }, + WaitersTestClient::waitUntilKeysFunctionMapStringEquals, + GetFunctionKeysEqualsResponse { maps = EntityMaps { strings = mapOf("key" to "value") } }, + ) } From 5dbc6840acfd512ceb0c26109a4b3d647b4ce976 Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Tue, 25 Feb 2025 15:55:51 +0000 Subject: [PATCH 51/79] chore: release 1.4.6 --- CHANGELOG.md | 2 ++ gradle.properties | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d95e5e269..28aa448040 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [1.4.6] - 02/25/2025 + ## [1.4.5] - 02/24/2025 ### Features diff --git a/gradle.properties b/gradle.properties index de71e13446..5a4c8a03e9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.4.6-SNAPSHOT +sdkVersion=1.4.6 # codegen -codegenVersion=0.34.6-SNAPSHOT \ No newline at end of file +codegenVersion=0.34.6 \ No newline at end of file From dc6b6466094b92ae3255908d461d9450a994855c Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Tue, 25 Feb 2025 15:55:52 +0000 Subject: [PATCH 52/79] chore: bump snapshot version to 1.4.7-SNAPSHOT --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 5a4c8a03e9..266b1c6f6c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.4.6 +sdkVersion=1.4.7-SNAPSHOT # codegen -codegenVersion=0.34.6 \ No newline at end of file +codegenVersion=0.34.7-SNAPSHOT \ No newline at end of file From 09838a7ecb582c21719cbc17a1f4f06c4e604ee1 Mon Sep 17 00:00:00 2001 From: Matas Date: Tue, 25 Feb 2025 13:10:49 -0500 Subject: [PATCH 53/79] fix: replace `Span.makeCurrent()` with `Span.asContextElement()` (#1237) --- .brazil.json | 1 + .changes/ec884c41-7bda-4033-a80b-5da20836a64b.json | 8 ++++++++ gradle/libs.versions.toml | 1 + .../observability/telemetry-api/api/telemetry-api.api | 3 +++ .../kotlin/runtime/telemetry/trace/AbstractTraceSpan.kt | 3 +++ .../runtime/telemetry/trace/CoroutineContextTraceExt.kt | 2 +- .../smithy/kotlin/runtime/telemetry/trace/TraceSpan.kt | 7 +++++++ .../telemetry-provider-otel/build.gradle.kts | 1 + .../kotlin/runtime/telemetry/otel/OtelTracerProvider.kt | 9 +++++---- 9 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 .changes/ec884c41-7bda-4033-a80b-5da20836a64b.json diff --git a/.brazil.json b/.brazil.json index dbb2135d1b..56055e11d1 100644 --- a/.brazil.json +++ b/.brazil.json @@ -7,6 +7,7 @@ "com.squareup.okhttp3:okhttp:5.*": "OkHttp3-5.x", "com.squareup.okio:okio-jvm:3.*": "OkioJvm-3.x", "io.opentelemetry:opentelemetry-api:1.*": "Maven-io-opentelemetry_opentelemetry-api-1.x", + "io.opentelemetry:opentelemetry-extension-kotlin:1.*": "Maven-io-opentelemetry_opentelemetry-extension-kotlin-1.x", "org.slf4j:slf4j-api:2.*": "Maven-org-slf4j_slf4j-api-2.x", "aws.sdk.kotlin.crt:aws-crt-kotlin:0.9.*": "AwsCrtKotlin-0.9.x", "aws.sdk.kotlin.crt:aws-crt-kotlin:0.8.*": "AwsCrtKotlin-0.8.x", diff --git a/.changes/ec884c41-7bda-4033-a80b-5da20836a64b.json b/.changes/ec884c41-7bda-4033-a80b-5da20836a64b.json new file mode 100644 index 0000000000..78aff6e839 --- /dev/null +++ b/.changes/ec884c41-7bda-4033-a80b-5da20836a64b.json @@ -0,0 +1,8 @@ +{ + "id": "ec884c41-7bda-4033-a80b-5da20836a64b", + "type": "bugfix", + "description": "Fix OpenTelemetry span concurrency by using Span.asContextElement() instead of Span.makeCurrent()", + "issues": [ + "https://github.com/smithy-lang/smithy-kotlin/issues/1211" + ] +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bfa4c2b4f1..577fc9e896 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -57,6 +57,7 @@ okhttp4 = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp4-versi okhttp-coroutines = { module = "com.squareup.okhttp3:okhttp-coroutines", version.ref = "okhttp-version" } opentelemetry-api = { module = "io.opentelemetry:opentelemetry-api", version.ref = "otel-version" } opentelemetry-sdk-testing = {module = "io.opentelemetry:opentelemetry-sdk-testing", version.ref = "otel-version" } +opentelemetry-kotlin-extension = { module = "io.opentelemetry:opentelemetry-extension-kotlin", version.ref = "otel-version" } slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j-version" } slf4j-api-v1x = { module = "org.slf4j:slf4j-api", version.ref = "slf4j-v1x-version" } slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = "slf4j-version" } diff --git a/runtime/observability/telemetry-api/api/telemetry-api.api b/runtime/observability/telemetry-api/api/telemetry-api.api index 33adef236a..cf14c4c7c0 100644 --- a/runtime/observability/telemetry-api/api/telemetry-api.api +++ b/runtime/observability/telemetry-api/api/telemetry-api.api @@ -364,6 +364,7 @@ public final class aws/smithy/kotlin/runtime/telemetry/metrics/UpDownCounter$Def public abstract class aws/smithy/kotlin/runtime/telemetry/trace/AbstractTraceSpan : aws/smithy/kotlin/runtime/telemetry/trace/TraceSpan { public fun ()V + public fun asContextElement ()Lkotlin/coroutines/CoroutineContext; public fun close ()V public fun emitEvent (Ljava/lang/String;Laws/smithy/kotlin/runtime/collections/Attributes;)V public fun getSpanContext ()Laws/smithy/kotlin/runtime/telemetry/trace/SpanContext; @@ -424,6 +425,7 @@ public final class aws/smithy/kotlin/runtime/telemetry/trace/SpanStatus : java/l public abstract interface class aws/smithy/kotlin/runtime/telemetry/trace/TraceSpan : aws/smithy/kotlin/runtime/telemetry/context/Scope { public static final field Companion Laws/smithy/kotlin/runtime/telemetry/trace/TraceSpan$Companion; + public abstract fun asContextElement ()Lkotlin/coroutines/CoroutineContext; public abstract fun close ()V public abstract fun emitEvent (Ljava/lang/String;Laws/smithy/kotlin/runtime/collections/Attributes;)V public abstract fun getSpanContext ()Laws/smithy/kotlin/runtime/telemetry/trace/SpanContext; @@ -437,6 +439,7 @@ public final class aws/smithy/kotlin/runtime/telemetry/trace/TraceSpan$Companion } public final class aws/smithy/kotlin/runtime/telemetry/trace/TraceSpan$DefaultImpls { + public static fun asContextElement (Laws/smithy/kotlin/runtime/telemetry/trace/TraceSpan;)Lkotlin/coroutines/CoroutineContext; public static synthetic fun emitEvent$default (Laws/smithy/kotlin/runtime/telemetry/trace/TraceSpan;Ljava/lang/String;Laws/smithy/kotlin/runtime/collections/Attributes;ILjava/lang/Object;)V } diff --git a/runtime/observability/telemetry-api/common/src/aws/smithy/kotlin/runtime/telemetry/trace/AbstractTraceSpan.kt b/runtime/observability/telemetry-api/common/src/aws/smithy/kotlin/runtime/telemetry/trace/AbstractTraceSpan.kt index fbc6369ec3..1892098b7b 100644 --- a/runtime/observability/telemetry-api/common/src/aws/smithy/kotlin/runtime/telemetry/trace/AbstractTraceSpan.kt +++ b/runtime/observability/telemetry-api/common/src/aws/smithy/kotlin/runtime/telemetry/trace/AbstractTraceSpan.kt @@ -6,6 +6,8 @@ package aws.smithy.kotlin.runtime.telemetry.trace import aws.smithy.kotlin.runtime.collections.AttributeKey import aws.smithy.kotlin.runtime.collections.Attributes +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext /** * An abstract implementation of a trace span. By default, this class uses no-op implementations for all members unless @@ -18,4 +20,5 @@ public abstract class AbstractTraceSpan : TraceSpan { override operator fun set(key: AttributeKey, value: T) { } override fun mergeAttributes(attributes: Attributes) { } override fun close() { } + override fun asContextElement(): CoroutineContext = EmptyCoroutineContext } diff --git a/runtime/observability/telemetry-api/common/src/aws/smithy/kotlin/runtime/telemetry/trace/CoroutineContextTraceExt.kt b/runtime/observability/telemetry-api/common/src/aws/smithy/kotlin/runtime/telemetry/trace/CoroutineContextTraceExt.kt index cb6795fe9c..73e84dd756 100644 --- a/runtime/observability/telemetry-api/common/src/aws/smithy/kotlin/runtime/telemetry/trace/CoroutineContextTraceExt.kt +++ b/runtime/observability/telemetry-api/common/src/aws/smithy/kotlin/runtime/telemetry/trace/CoroutineContextTraceExt.kt @@ -71,7 +71,7 @@ public suspend inline fun withSpan( // or else traces may be disconnected from their parent val updatedCtx = coroutineContext[TelemetryProviderContext]?.provider?.contextManager?.current() val telemetryCtxElement = (updatedCtx?.let { TelemetryContextElement(it) } ?: coroutineContext[TelemetryContextElement]) ?: EmptyCoroutineContext - withContext(context + TraceSpanContext(span) + telemetryCtxElement) { + withContext(context + TraceSpanContext(span) + telemetryCtxElement + span.asContextElement()) { block(span) } } catch (ex: Exception) { diff --git a/runtime/observability/telemetry-api/common/src/aws/smithy/kotlin/runtime/telemetry/trace/TraceSpan.kt b/runtime/observability/telemetry-api/common/src/aws/smithy/kotlin/runtime/telemetry/trace/TraceSpan.kt index d3c7972a85..78a92e6057 100644 --- a/runtime/observability/telemetry-api/common/src/aws/smithy/kotlin/runtime/telemetry/trace/TraceSpan.kt +++ b/runtime/observability/telemetry-api/common/src/aws/smithy/kotlin/runtime/telemetry/trace/TraceSpan.kt @@ -9,6 +9,8 @@ import aws.smithy.kotlin.runtime.collections.AttributeKey import aws.smithy.kotlin.runtime.collections.Attributes import aws.smithy.kotlin.runtime.collections.emptyAttributes import aws.smithy.kotlin.runtime.telemetry.context.Scope +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext /** * Represents a single operation/task within a trace. Each trace contains a root span and @@ -27,6 +29,11 @@ public interface TraceSpan : Scope { */ public val spanContext: SpanContext + /** + * A representation of this span as a [CoroutineContext] element + */ + public fun asContextElement(): CoroutineContext = EmptyCoroutineContext + /** * Set an attribute on the span * @param key the attribute key to use diff --git a/runtime/observability/telemetry-provider-otel/build.gradle.kts b/runtime/observability/telemetry-provider-otel/build.gradle.kts index 8792275766..bdee5b1d6a 100644 --- a/runtime/observability/telemetry-provider-otel/build.gradle.kts +++ b/runtime/observability/telemetry-provider-otel/build.gradle.kts @@ -18,6 +18,7 @@ kotlin { jvmMain { dependencies { api(libs.opentelemetry.api) + api(libs.opentelemetry.kotlin.extension) } } all { diff --git a/runtime/observability/telemetry-provider-otel/jvm/src/aws/smithy/kotlin/runtime/telemetry/otel/OtelTracerProvider.kt b/runtime/observability/telemetry-provider-otel/jvm/src/aws/smithy/kotlin/runtime/telemetry/otel/OtelTracerProvider.kt index cf6eb39dfd..20ef72f139 100644 --- a/runtime/observability/telemetry-provider-otel/jvm/src/aws/smithy/kotlin/runtime/telemetry/otel/OtelTracerProvider.kt +++ b/runtime/observability/telemetry-provider-otel/jvm/src/aws/smithy/kotlin/runtime/telemetry/otel/OtelTracerProvider.kt @@ -10,6 +10,8 @@ import aws.smithy.kotlin.runtime.collections.Attributes import aws.smithy.kotlin.runtime.telemetry.context.Context import aws.smithy.kotlin.runtime.telemetry.trace.* import io.opentelemetry.api.OpenTelemetry +import io.opentelemetry.extension.kotlin.asContextElement +import kotlin.coroutines.CoroutineContext import io.opentelemetry.api.trace.Span as OtelSpan import io.opentelemetry.api.trace.SpanContext as OtelSpanContext import io.opentelemetry.api.trace.SpanKind as OtelSpanKind @@ -62,11 +64,11 @@ private class OtelSpanContextImpl(private val otelSpanContext: OtelSpanContext) internal class OtelTraceSpanImpl( private val otelSpan: OtelSpan, ) : TraceSpan { - - private val spanScope = otelSpan.makeCurrent() - override val spanContext: SpanContext get() = OtelSpanContextImpl(otelSpan.spanContext) + + override fun asContextElement(): CoroutineContext = otelSpan.asContextElement() + override fun set(key: AttributeKey, value: T) { key.otelAttrKeyOrNull(value)?.let { otelKey -> otelSpan.setAttribute(otelKey, value) @@ -90,7 +92,6 @@ internal class OtelTraceSpanImpl( override fun close() { otelSpan.end() - spanScope.close() } } From d06ccf3f12894cc0dd8a97c43e5ec5af5d74dd32 Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Tue, 25 Feb 2025 18:16:01 +0000 Subject: [PATCH 54/79] chore: release 1.4.7 --- .changes/ec884c41-7bda-4033-a80b-5da20836a64b.json | 8 -------- CHANGELOG.md | 5 +++++ gradle.properties | 4 ++-- 3 files changed, 7 insertions(+), 10 deletions(-) delete mode 100644 .changes/ec884c41-7bda-4033-a80b-5da20836a64b.json diff --git a/.changes/ec884c41-7bda-4033-a80b-5da20836a64b.json b/.changes/ec884c41-7bda-4033-a80b-5da20836a64b.json deleted file mode 100644 index 78aff6e839..0000000000 --- a/.changes/ec884c41-7bda-4033-a80b-5da20836a64b.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "id": "ec884c41-7bda-4033-a80b-5da20836a64b", - "type": "bugfix", - "description": "Fix OpenTelemetry span concurrency by using Span.asContextElement() instead of Span.makeCurrent()", - "issues": [ - "https://github.com/smithy-lang/smithy-kotlin/issues/1211" - ] -} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 28aa448040..c365ff2deb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## [1.4.7] - 02/25/2025 + +### Fixes +* [#1211](https://github.com/smithy-lang/smithy-kotlin/issues/1211) Fix OpenTelemetry span concurrency by using Span.asContextElement() instead of Span.makeCurrent() + ## [1.4.6] - 02/25/2025 ## [1.4.5] - 02/24/2025 diff --git a/gradle.properties b/gradle.properties index 266b1c6f6c..a46d60cb0d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.4.7-SNAPSHOT +sdkVersion=1.4.7 # codegen -codegenVersion=0.34.7-SNAPSHOT \ No newline at end of file +codegenVersion=0.34.7 \ No newline at end of file From 03a8e6293fa468209641164639018e6600249794 Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Tue, 25 Feb 2025 18:16:02 +0000 Subject: [PATCH 55/79] chore: bump snapshot version to 1.4.8-SNAPSHOT --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index a46d60cb0d..85b12b7dc0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.4.7 +sdkVersion=1.4.8-SNAPSHOT # codegen -codegenVersion=0.34.7 \ No newline at end of file +codegenVersion=0.34.8-SNAPSHOT \ No newline at end of file From aae827c61d75de33b0dffd4dc797d6279288a96c Mon Sep 17 00:00:00 2001 From: 0marperez <60363173+0marperez@users.noreply.github.com> Date: Thu, 27 Feb 2025 11:07:35 -0500 Subject: [PATCH 56/79] fix: idempotency tokens being code-generated for nested structures (#1248) --- .../a928feea-f455-4263-8bae-cf50676a551a.json | 5 + .../codegen/aws/protocols/AwsQueryTest.kt | 105 ++++++++++++++++++ .../codegen/aws/protocols/RestJson1Test.kt | 104 +++++++++++++++++ .../codegen/aws/protocols/RestXmlTest.kt | 104 +++++++++++++++++ .../codegen/aws/protocols/RpcV2CborTest.kt | 84 +++++++++++++- .../codegen/model/knowledge/TopLevelIndex.kt | 20 ++++ .../serde/SerializeStructGenerator.kt | 14 ++- 7 files changed, 433 insertions(+), 3 deletions(-) create mode 100644 .changes/a928feea-f455-4263-8bae-cf50676a551a.json create mode 100644 codegen/smithy-aws-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/AwsQueryTest.kt create mode 100644 codegen/smithy-aws-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/RestJson1Test.kt create mode 100644 codegen/smithy-aws-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/RestXmlTest.kt create mode 100644 codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/model/knowledge/TopLevelIndex.kt diff --git a/.changes/a928feea-f455-4263-8bae-cf50676a551a.json b/.changes/a928feea-f455-4263-8bae-cf50676a551a.json new file mode 100644 index 0000000000..1a1f543dc2 --- /dev/null +++ b/.changes/a928feea-f455-4263-8bae-cf50676a551a.json @@ -0,0 +1,5 @@ +{ + "id": "a928feea-f455-4263-8bae-cf50676a551a", + "type": "bugfix", + "description": "Idempotency tokens are no longer code-generated for nested structures. See: https://smithy.io/2.0/spec/behavior-traits.html#smithy-api-idempotencytoken-trait" +} \ No newline at end of file diff --git a/codegen/smithy-aws-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/AwsQueryTest.kt b/codegen/smithy-aws-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/AwsQueryTest.kt new file mode 100644 index 0000000000..8a575c7cd4 --- /dev/null +++ b/codegen/smithy-aws-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/AwsQueryTest.kt @@ -0,0 +1,105 @@ +package software.amazon.smithy.kotlin.codegen.aws.protocols + +import software.amazon.smithy.kotlin.codegen.test.* +import kotlin.test.Test + +class AwsQueryTest { + @Test + fun testNonNestedIdempotencyToken() { + val ctx = model.newTestContext("Example") + + val generator = AwsQuery() + generator.generateProtocolClient(ctx.generationCtx) + + ctx.generationCtx.delegator.finalize() + ctx.generationCtx.delegator.flushWriters() + + val expected = """ + serializer.serializeStruct(OBJ_DESCRIPTOR) { + input.bar?.let { field(BAR_DESCRIPTOR, it) } ?: field(BAR_DESCRIPTOR, context.idempotencyTokenProvider.generateToken()) + } + """.trimIndent() + + val actual = ctx + .manifest + .expectFileString("/src/main/kotlin/com/test/serde/GetBarUnNestedOperationSerializer.kt") + .lines(" serializer.serializeStruct(OBJ_DESCRIPTOR) {", " }") + .trimIndent() + + actual.shouldContainOnlyOnceWithDiff(expected) + } + + @Test + fun testNestedIdempotencyToken() { + val ctx = model.newTestContext("Example") + + val generator = AwsQuery() + generator.generateProtocolClient(ctx.generationCtx) + + ctx.generationCtx.delegator.finalize() + ctx.generationCtx.delegator.flushWriters() + + val expected = """ + serializer.serializeStruct(OBJ_DESCRIPTOR) { + input.baz?.let { field(BAZ_DESCRIPTOR, it) } + } + """.trimIndent() + + val actual = ctx + .manifest + .expectFileString("/src/main/kotlin/com/test/serde/NestDocumentSerializer.kt") + .lines(" serializer.serializeStruct(OBJ_DESCRIPTOR) {", " }") + .trimIndent() + + actual.shouldContainOnlyOnceWithDiff(expected) + + val unexpected = """ + serializer.serializeStruct(OBJ_DESCRIPTOR) { + input.baz?.let { field(BAZ_DESCRIPTOR, it) } ?: field(BAR_DESCRIPTOR, context.idempotencyTokenProvider.generateToken()) + } + """.trimIndent() + + actual.shouldNotContainOnlyOnceWithDiff(unexpected) + } + + private val model = """ + ${"$"}version: "2" + + namespace com.test + + use aws.protocols#awsQuery + use aws.api#service + + @awsQuery + @service(sdkId: "Example") + @xmlNamespace(uri: "http://foo.com") + service Example { + version: "1.0.0", + operations: [GetBarUnNested, GetBarNested] + } + + @http(method: "POST", uri: "/get-bar-un-nested") + operation GetBarUnNested { + input: BarUnNested + } + + structure BarUnNested { + @idempotencyToken + bar: String + } + + @http(method: "POST", uri: "/get-bar-nested") + operation GetBarNested { + input: BarNested + } + + structure BarNested { + bar: Nest + } + + structure Nest { + @idempotencyToken + baz: String + } + """.toSmithyModel() +} diff --git a/codegen/smithy-aws-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/RestJson1Test.kt b/codegen/smithy-aws-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/RestJson1Test.kt new file mode 100644 index 0000000000..e1a4b22c01 --- /dev/null +++ b/codegen/smithy-aws-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/RestJson1Test.kt @@ -0,0 +1,104 @@ +package software.amazon.smithy.kotlin.codegen.aws.protocols + +import software.amazon.smithy.kotlin.codegen.test.* +import kotlin.test.Test + +class RestJson1Test { + @Test + fun testNonNestedIdempotencyToken() { + val ctx = model.newTestContext("Example") + + val generator = RestJson1() + generator.generateProtocolClient(ctx.generationCtx) + + ctx.generationCtx.delegator.finalize() + ctx.generationCtx.delegator.flushWriters() + + val expected = """ + serializer.serializeStruct(OBJ_DESCRIPTOR) { + input.bar?.let { field(BAR_DESCRIPTOR, it) } ?: field(BAR_DESCRIPTOR, context.idempotencyTokenProvider.generateToken()) + } + """.trimIndent() + + val actual = ctx + .manifest + .expectFileString("/src/main/kotlin/com/test/serde/GetBarUnNestedOperationSerializer.kt") + .lines(" serializer.serializeStruct(OBJ_DESCRIPTOR) {", " }") + .trimIndent() + + actual.shouldContainOnlyOnceWithDiff(expected) + } + + @Test + fun testNestedIdempotencyToken() { + val ctx = model.newTestContext("Example") + + val generator = RestJson1() + generator.generateProtocolClient(ctx.generationCtx) + + ctx.generationCtx.delegator.finalize() + ctx.generationCtx.delegator.flushWriters() + + val expected = """ + serializer.serializeStruct(OBJ_DESCRIPTOR) { + input.baz?.let { field(BAZ_DESCRIPTOR, it) } + } + """.trimIndent() + + val actual = ctx + .manifest + .expectFileString("/src/main/kotlin/com/test/serde/NestDocumentSerializer.kt") + .lines(" serializer.serializeStruct(OBJ_DESCRIPTOR) {", " }") + .trimIndent() + + actual.shouldContainOnlyOnceWithDiff(expected) + + val unexpected = """ + serializer.serializeStruct(OBJ_DESCRIPTOR) { + input.baz?.let { field(BAZ_DESCRIPTOR, it) } ?: field(BAR_DESCRIPTOR, context.idempotencyTokenProvider.generateToken()) + } + """.trimIndent() + + actual.shouldNotContainOnlyOnceWithDiff(unexpected) + } + + private val model = """ + ${"$"}version: "2" + + namespace com.test + + use aws.protocols#restJson1 + use aws.api#service + + @restJson1 + @service(sdkId: "Example") + service Example { + version: "1.0.0", + operations: [GetBarUnNested, GetBarNested] + } + + @http(method: "POST", uri: "/get-bar-un-nested") + operation GetBarUnNested { + input: BarUnNested + } + + structure BarUnNested { + @idempotencyToken + bar: String + } + + @http(method: "POST", uri: "/get-bar-nested") + operation GetBarNested { + input: BarNested + } + + structure BarNested { + bar: Nest + } + + structure Nest { + @idempotencyToken + baz: String + } + """.toSmithyModel() +} diff --git a/codegen/smithy-aws-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/RestXmlTest.kt b/codegen/smithy-aws-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/RestXmlTest.kt new file mode 100644 index 0000000000..4f59359bee --- /dev/null +++ b/codegen/smithy-aws-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/RestXmlTest.kt @@ -0,0 +1,104 @@ +package software.amazon.smithy.kotlin.codegen.aws.protocols + +import software.amazon.smithy.kotlin.codegen.test.* +import kotlin.test.Test + +class RestXmlTest { + @Test + fun testNonNestedIdempotencyToken() { + val ctx = model.newTestContext("Example") + + val generator = RestXml() + generator.generateProtocolClient(ctx.generationCtx) + + ctx.generationCtx.delegator.finalize() + ctx.generationCtx.delegator.flushWriters() + + val expected = """ + serializer.serializeStruct(OBJ_DESCRIPTOR) { + input.bar?.let { field(BAR_DESCRIPTOR, it) } ?: field(BAR_DESCRIPTOR, context.idempotencyTokenProvider.generateToken()) + } + """.trimIndent() + + val actual = ctx + .manifest + .expectFileString("/src/main/kotlin/com/test/serde/GetBarUnNestedOperationSerializer.kt") + .lines(" serializer.serializeStruct(OBJ_DESCRIPTOR) {", " }") + .trimIndent() + + actual.shouldContainOnlyOnceWithDiff(expected) + } + + @Test + fun testNestedIdempotencyToken() { + val ctx = model.newTestContext("Example") + + val generator = RestXml() + generator.generateProtocolClient(ctx.generationCtx) + + ctx.generationCtx.delegator.finalize() + ctx.generationCtx.delegator.flushWriters() + + val expected = """ + serializer.serializeStruct(OBJ_DESCRIPTOR) { + input.baz?.let { field(BAZ_DESCRIPTOR, it) } + } + """.trimIndent() + + val actual = ctx + .manifest + .expectFileString("/src/main/kotlin/com/test/serde/NestDocumentSerializer.kt") + .lines(" serializer.serializeStruct(OBJ_DESCRIPTOR) {", " }") + .trimIndent() + + actual.shouldContainOnlyOnceWithDiff(expected) + + val unexpected = """ + serializer.serializeStruct(OBJ_DESCRIPTOR) { + input.baz?.let { field(BAZ_DESCRIPTOR, it) } ?: field(BAR_DESCRIPTOR, context.idempotencyTokenProvider.generateToken()) + } + """.trimIndent() + + actual.shouldNotContainOnlyOnceWithDiff(unexpected) + } + + private val model = """ + ${"$"}version: "2" + + namespace com.test + + use aws.protocols#restXml + use aws.api#service + + @restXml + @service(sdkId: "Example") + service Example { + version: "1.0.0", + operations: [GetBarUnNested, GetBarNested] + } + + @http(method: "POST", uri: "/get-bar-un-nested") + operation GetBarUnNested { + input: BarUnNested + } + + structure BarUnNested { + @idempotencyToken + bar: String + } + + @http(method: "POST", uri: "/get-bar-nested") + operation GetBarNested { + input: BarNested + } + + structure BarNested { + bar: Nest + } + + structure Nest { + @idempotencyToken + baz: String + } + """.toSmithyModel() +} diff --git a/codegen/smithy-aws-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/RpcV2CborTest.kt b/codegen/smithy-aws-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/RpcV2CborTest.kt index 0114185004..a52456d61a 100644 --- a/codegen/smithy-aws-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/RpcV2CborTest.kt +++ b/codegen/smithy-aws-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/aws/protocols/RpcV2CborTest.kt @@ -21,7 +21,7 @@ class RpcV2CborTest { @service(sdkId: "CborExample") service CborExample { version: "1.0.0", - operations: [GetFoo, GetFooStreaming, PutFooStreaming] + operations: [GetFoo, GetFooStreaming, PutFooStreaming, GetBarUnNested, GetBarNested] } @http(method: "POST", uri: "/foo") @@ -74,6 +74,30 @@ class RpcV2CborTest { @error("client") @retryable(throttling: true) structure ThrottlingError {} + + @http(method: "POST", uri: "/get-bar-un-nested") + operation GetBarUnNested { + input: BarUnNested + } + + structure BarUnNested { + @idempotencyToken + bar: String + } + + @http(method: "POST", uri: "/get-bar-nested") + operation GetBarNested { + input: BarNested + } + + structure BarNested { + bar: Nest + } + + structure Nest { + @idempotencyToken + baz: String + } """.toSmithyModel() @Test @@ -162,4 +186,62 @@ class RpcV2CborTest { val serializeBody = documentSerializer.lines(" serializer.serializeStruct(OBJ_DESCRIPTOR) {", "}") serializeBody.shouldNotContain("input.messages") // `messages` is the stream member and should not be serialized in the initial request } + + @Test + fun testNonNestedIdempotencyToken() { + val ctx = model.newTestContext("CborExample") + + val generator = RpcV2Cbor() + generator.generateProtocolClient(ctx.generationCtx) + + ctx.generationCtx.delegator.finalize() + ctx.generationCtx.delegator.flushWriters() + + val expected = """ + serializer.serializeStruct(OBJ_DESCRIPTOR) { + input.bar?.let { field(BAR_DESCRIPTOR, it) } ?: field(BAR_DESCRIPTOR, context.idempotencyTokenProvider.generateToken()) + } + """.trimIndent() + + val actual = ctx + .manifest + .expectFileString("/src/main/kotlin/com/test/serde/GetBarUnNestedOperationSerializer.kt") + .lines(" serializer.serializeStruct(OBJ_DESCRIPTOR) {", " }") + .trimIndent() + + actual.shouldContainOnlyOnceWithDiff(expected) + } + + @Test + fun testNestedIdempotencyToken() { + val ctx = model.newTestContext("CborExample") + + val generator = RpcV2Cbor() + generator.generateProtocolClient(ctx.generationCtx) + + ctx.generationCtx.delegator.finalize() + ctx.generationCtx.delegator.flushWriters() + + val expected = """ + serializer.serializeStruct(OBJ_DESCRIPTOR) { + input.baz?.let { field(BAZ_DESCRIPTOR, it) } + } + """.trimIndent() + + val actual = ctx + .manifest + .expectFileString("/src/main/kotlin/com/test/serde/NestDocumentSerializer.kt") + .lines(" serializer.serializeStruct(OBJ_DESCRIPTOR) {", " }") + .trimIndent() + + actual.shouldContainOnlyOnceWithDiff(expected) + + val unexpected = """ + serializer.serializeStruct(OBJ_DESCRIPTOR) { + input.baz?.let { field(BAZ_DESCRIPTOR, it) } ?: field(BAR_DESCRIPTOR, context.idempotencyTokenProvider.generateToken()) + } + """.trimIndent() + + actual.shouldNotContainOnlyOnceWithDiff(unexpected) + } } diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/model/knowledge/TopLevelIndex.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/model/knowledge/TopLevelIndex.kt new file mode 100644 index 0000000000..df89e2d782 --- /dev/null +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/model/knowledge/TopLevelIndex.kt @@ -0,0 +1,20 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.kotlin.codegen.model.knowledge + +import software.amazon.smithy.kotlin.codegen.utils.getOrNull +import software.amazon.smithy.model.Model +import software.amazon.smithy.model.knowledge.TopDownIndex +import software.amazon.smithy.model.shapes.MemberShape +import software.amazon.smithy.model.shapes.ServiceShape + +class TopLevelIndex(model: Model, service: ServiceShape) { + private val operations = TopDownIndex(model).getContainedOperations(service) + private val inputStructs = operations.mapNotNull { it.input.getOrNull() }.map { model.expectShape(it) } + private val inputMembers = inputStructs.flatMap { it.members() }.toSet() + + fun isTopLevelInputMember(member: MemberShape): Boolean = member in inputMembers +} diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/serde/SerializeStructGenerator.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/serde/SerializeStructGenerator.kt index d8810044a5..3cfbc0dcfe 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/serde/SerializeStructGenerator.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/serde/SerializeStructGenerator.kt @@ -8,10 +8,12 @@ import software.amazon.smithy.codegen.core.CodegenException import software.amazon.smithy.kotlin.codegen.DefaultValueSerializationMode import software.amazon.smithy.kotlin.codegen.core.* import software.amazon.smithy.kotlin.codegen.model.* +import software.amazon.smithy.kotlin.codegen.model.knowledge.TopLevelIndex import software.amazon.smithy.kotlin.codegen.model.targetOrSelf import software.amazon.smithy.kotlin.codegen.rendering.protocol.ProtocolGenerator import software.amazon.smithy.model.shapes.* import software.amazon.smithy.model.traits.* +import java.util.logging.Logger /** * Generate serialization for members bound to the payload. @@ -43,6 +45,9 @@ open class SerializeStructGenerator( protected val writer: KotlinWriter, protected val defaultTimestampFormat: TimestampFormatTrait.Format, ) { + private val logger = Logger.getLogger(javaClass.name) + private val topLevelIndex = TopLevelIndex(ctx.model, ctx.service) + /** * Container for serialization information for a particular shape being serialized to */ @@ -587,7 +592,7 @@ open class SerializeStructGenerator( * @param serializerFn [SerializeFunction] the serializer responsible for returning the function to invoke */ open fun renderShapeSerializer(memberShape: MemberShape, serializerFn: SerializeFunction) { - val postfix = idempotencyTokenPostfix(memberShape) + val postfix = if (memberShape.hasTrait()) idempotencyTokenPostfix(memberShape) else "" val memberSymbol = ctx.symbolProvider.toSymbol(memberShape) val memberName = ctx.symbolProvider.toMemberName(memberShape) if (memberSymbol.isNullable) { @@ -614,10 +619,15 @@ open class SerializeStructGenerator( * @return string intended for codegen output */ private fun idempotencyTokenPostfix(memberShape: MemberShape): String = - if (memberShape.hasTrait()) { + // https://github.com/smithy-lang/smithy-kotlin/issues/1128 + if (topLevelIndex.isTopLevelInputMember(memberShape)) { writer.addImport(RuntimeTypes.SmithyClient.IdempotencyTokenProviderExt) " ?: field(${memberShape.descriptorName()}, context.idempotencyTokenProvider.generateToken())" } else { + logger.warning( + "$memberShape has the idempotency token trait but is not eligible to be used as an idempotency token. " + + "It must be a top-level structure member within the input of an operation. ", + ) "" } From c498b4282dd3666c2d9c6c0bcc0a2d82d12101c1 Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Thu, 27 Feb 2025 16:57:49 +0000 Subject: [PATCH 57/79] chore: release 1.4.8 --- .changes/a928feea-f455-4263-8bae-cf50676a551a.json | 5 ----- CHANGELOG.md | 5 +++++ gradle.properties | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) delete mode 100644 .changes/a928feea-f455-4263-8bae-cf50676a551a.json diff --git a/.changes/a928feea-f455-4263-8bae-cf50676a551a.json b/.changes/a928feea-f455-4263-8bae-cf50676a551a.json deleted file mode 100644 index 1a1f543dc2..0000000000 --- a/.changes/a928feea-f455-4263-8bae-cf50676a551a.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "id": "a928feea-f455-4263-8bae-cf50676a551a", - "type": "bugfix", - "description": "Idempotency tokens are no longer code-generated for nested structures. See: https://smithy.io/2.0/spec/behavior-traits.html#smithy-api-idempotencytoken-trait" -} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index c365ff2deb..760b2fb7c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## [1.4.8] - 02/27/2025 + +### Fixes +* Idempotency tokens are no longer code-generated for nested structures. See: https://smithy.io/2.0/spec/behavior-traits.html#smithy-api-idempotencytoken-trait + ## [1.4.7] - 02/25/2025 ### Fixes diff --git a/gradle.properties b/gradle.properties index 85b12b7dc0..91d9339d95 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.4.8-SNAPSHOT +sdkVersion=1.4.8 # codegen -codegenVersion=0.34.8-SNAPSHOT \ No newline at end of file +codegenVersion=0.34.8 \ No newline at end of file From 462967be812ecc160ee784b5c2f1cdeeddafb917 Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Thu, 27 Feb 2025 16:57:51 +0000 Subject: [PATCH 58/79] chore: bump snapshot version to 1.4.9-SNAPSHOT --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 91d9339d95..64e9a5db45 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.4.8 +sdkVersion=1.4.9-SNAPSHOT # codegen -codegenVersion=0.34.8 \ No newline at end of file +codegenVersion=0.34.9-SNAPSHOT \ No newline at end of file From 72155f765f427d9a4275b19a5f4ec4fbe1d594cf Mon Sep 17 00:00:00 2001 From: Ian Botsford <83236726+ianbotsf@users.noreply.github.com> Date: Thu, 27 Feb 2025 09:29:20 -0800 Subject: [PATCH 59/79] fix: correctly codegen paginators for types which require fully-qualified names (#1249) --- .../13a47747-197a-4b88-bc3b-393af5f5127a.json | 5 + .../codegen/rendering/PaginatorGenerator.kt | 28 ++-- .../rendering/PaginatorGeneratorTest.kt | 124 ++++++++++++++++++ 3 files changed, 144 insertions(+), 13 deletions(-) create mode 100644 .changes/13a47747-197a-4b88-bc3b-393af5f5127a.json diff --git a/.changes/13a47747-197a-4b88-bc3b-393af5f5127a.json b/.changes/13a47747-197a-4b88-bc3b-393af5f5127a.json new file mode 100644 index 0000000000..42d876eaa8 --- /dev/null +++ b/.changes/13a47747-197a-4b88-bc3b-393af5f5127a.json @@ -0,0 +1,5 @@ +{ + "id": "13a47747-197a-4b88-bc3b-393af5f5127a", + "type": "bugfix", + "description": "Correctly generate paginators for item type names which collide with other used types (e.g., an item type `com.foo.Flow` which conflicts with `kotlinx.coroutines.flow.Flow`)" +} \ No newline at end of file diff --git a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/PaginatorGenerator.kt b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/PaginatorGenerator.kt index 7ac76fee7d..1a829dbb5a 100644 --- a/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/PaginatorGenerator.kt +++ b/codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/PaginatorGenerator.kt @@ -8,12 +8,7 @@ import software.amazon.smithy.codegen.core.CodegenException import software.amazon.smithy.codegen.core.Symbol import software.amazon.smithy.codegen.core.SymbolReference import software.amazon.smithy.kotlin.codegen.KotlinSettings -import software.amazon.smithy.kotlin.codegen.core.CodegenContext -import software.amazon.smithy.kotlin.codegen.core.ExternalTypes -import software.amazon.smithy.kotlin.codegen.core.KotlinDelegator -import software.amazon.smithy.kotlin.codegen.core.KotlinWriter -import software.amazon.smithy.kotlin.codegen.core.defaultName -import software.amazon.smithy.kotlin.codegen.core.withBlock +import software.amazon.smithy.kotlin.codegen.core.* import software.amazon.smithy.kotlin.codegen.integration.KotlinIntegration import software.amazon.smithy.kotlin.codegen.lang.KotlinTypes import software.amazon.smithy.kotlin.codegen.model.* @@ -54,7 +49,7 @@ class PaginatorGenerator : KotlinIntegration { paginatedOperations.forEach { paginatedOperation -> val paginationInfo = paginatedIndex.getPaginationInfo(service, paginatedOperation).getOrNull() ?: throw CodegenException("Unexpectedly unable to get PaginationInfo from $service $paginatedOperation") - val paginationItemInfo = getItemDescriptorOrNull(paginationInfo, ctx) + val paginationItemInfo = getItemDescriptorOrNull(paginationInfo, ctx, writer) renderPaginatorForOperation(ctx, writer, paginatedOperation, paginationInfo, paginationItemInfo) } @@ -264,7 +259,11 @@ private data class ItemDescriptor( /** * Return an [ItemDescriptor] if model supplies, otherwise null */ -private fun getItemDescriptorOrNull(paginationInfo: PaginationInfo, ctx: CodegenContext): ItemDescriptor? { +private fun getItemDescriptorOrNull( + paginationInfo: PaginationInfo, + ctx: CodegenContext, + writer: KotlinWriter, +): ItemDescriptor? { val itemMemberId = paginationInfo.itemsMemberPath?.lastOrNull()?.target ?: return null val itemLiteral = paginationInfo.itemsMemberPath!!.last()!!.defaultName() @@ -273,15 +272,18 @@ private fun getItemDescriptorOrNull(paginationInfo: PaginationInfo, ctx: Codegen val isSparse = itemMember.isSparse val (collectionLiteral, targetMember) = when (itemMember) { is MapShape -> { - val symbol = ctx.symbolProvider.toSymbol(itemMember) - val entryExpression = symbol.expectProperty(SymbolProperty.ENTRY_EXPRESSION) as String - entryExpression to itemMember + val keySymbol = ctx.symbolProvider.toSymbol(itemMember.key) + val valueSymbol = ctx.symbolProvider.toSymbol(itemMember.value) + val valueSuffix = if (isSparse || valueSymbol.isNullable) "?" else "" + val elementExpression = writer.format("Map.Entry<#T, #T#L>", keySymbol, valueSymbol, valueSuffix) + elementExpression to itemMember } is CollectionShape -> { val target = ctx.model.expectShape(itemMember.member.target) val symbol = ctx.symbolProvider.toSymbol(target) - val literal = symbol.name + if (symbol.isNullable || isSparse) "?" else "" - literal to target + val suffix = if (isSparse || symbol.isNullable) "?" else "" + val elementExpression = writer.format("#T#L", symbol, suffix) + elementExpression to target } else -> error("Unexpected shape type ${itemMember.type}") } diff --git a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/PaginatorGeneratorTest.kt b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/PaginatorGeneratorTest.kt index 7b08a6d869..a3c33532d5 100644 --- a/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/PaginatorGeneratorTest.kt +++ b/codegen/smithy-kotlin-codegen/src/test/kotlin/software/amazon/smithy/kotlin/codegen/rendering/PaginatorGeneratorTest.kt @@ -263,6 +263,130 @@ class PaginatorGeneratorTest { actual.shouldContainOnlyOnceWithDiff(expectedImports) } + @Test + fun testRenderPaginatorWithItemRequiringFullName() { + val testModelWithItems = """ + namespace com.test + + use aws.protocols#restJson1 + + service FlowService { + operations: [ListFlows] + } + + @paginated( + inputToken: "Marker", + outputToken: "NextMarker", + pageSize: "MaxItems", + items: "Flows" + ) + @readonly + @http(method: "GET", uri: "/flows", code: 200) + operation ListFlows { + input: ListFlowsRequest, + output: ListFlowsResponse + } + + structure ListFlowsRequest { + @httpQuery("FlowVersion") + FlowVersion: String, + @httpQuery("Marker") + Marker: String, + @httpQuery("MasterRegion") + MasterRegion: String, + @httpQuery("MaxItems") + MaxItems: Integer + } + + structure ListFlowsResponse { + Flows: FlowList, + NextMarker: String + } + + list FlowList { + member: Flow + } + + structure Flow { + Name: String + } + """.toSmithyModel() + val testContextWithItems = testModelWithItems.newTestContext("FlowService", "com.test") + + val codegenContextWithItems = object : CodegenContext { + override val model: Model = testContextWithItems.generationCtx.model + override val symbolProvider: SymbolProvider = testContextWithItems.generationCtx.symbolProvider + override val settings: KotlinSettings = testContextWithItems.generationCtx.settings + override val protocolGenerator: ProtocolGenerator = testContextWithItems.generator + override val integrations: List = testContextWithItems.generationCtx.integrations + } + + val unit = PaginatorGenerator() + unit.writeAdditionalFiles(codegenContextWithItems, testContextWithItems.generationCtx.delegator) + + testContextWithItems.generationCtx.delegator.flushWriters() + val testManifest = testContextWithItems.generationCtx.delegator.fileManifest as MockManifest + val actual = testManifest.expectFileString("src/main/kotlin/com/test/paginators/Paginators.kt") + + val expectedCode = """ + /** + * Paginate over [ListFlowsResponse] results. + * + * When this operation is called, a [kotlinx.coroutines.Flow] is created. Flows are lazy (cold) so no service + * calls are made until the flow is collected. This also means there is no guarantee that the request is valid + * until then. Once you start collecting the flow, the SDK will lazily load response pages by making service + * calls until there are no pages left or the flow is cancelled. If there are errors in your request, you will + * see the failures only after you start collection. + * @param initialRequest A [ListFlowsRequest] to start pagination + * @return A [kotlinx.coroutines.flow.Flow] that can collect [ListFlowsResponse] + */ + public fun TestClient.listFlowsPaginated(initialRequest: ListFlowsRequest = ListFlowsRequest { }): kotlinx.coroutines.flow.Flow = + flow { + var cursor: kotlin.String? = initialRequest.marker + var hasNextPage: Boolean = true + + while (hasNextPage) { + val req = initialRequest.copy { + this.marker = cursor + } + val result = this@listFlowsPaginated.listFlows(req) + cursor = result.nextMarker + hasNextPage = cursor?.isNotEmpty() == true + emit(result) + } + } + + /** + * Paginate over [ListFlowsResponse] results. + * + * When this operation is called, a [kotlinx.coroutines.Flow] is created. Flows are lazy (cold) so no service + * calls are made until the flow is collected. This also means there is no guarantee that the request is valid + * until then. Once you start collecting the flow, the SDK will lazily load response pages by making service + * calls until there are no pages left or the flow is cancelled. If there are errors in your request, you will + * see the failures only after you start collection. + * @param block A builder block used for DSL-style invocation of the operation + * @return A [kotlinx.coroutines.flow.Flow] that can collect [ListFlowsResponse] + */ + public fun TestClient.listFlowsPaginated(block: ListFlowsRequest.Builder.() -> Unit): kotlinx.coroutines.flow.Flow = + listFlowsPaginated(ListFlowsRequest.Builder().apply(block).build()) + + /** + * This paginator transforms the flow returned by [listFlowsPaginated] + * to access the nested member [Flow] + * @return A [kotlinx.coroutines.flow.Flow] that can collect [Flow] + */ + @JvmName("listFlowsResponseFlow") + public fun kotlinx.coroutines.flow.Flow.flows(): kotlinx.coroutines.flow.Flow = + transform() { response -> + response.flows?.forEach { + emit(it) + } + } + """.trimIndent() + + actual.shouldContainOnlyOnceWithDiff(expectedCode) + } + @Test fun testRenderPaginatorWithSparseItem() { val testModelWithItems = """ From 0e4a7cb8a69d582c9ba0a9ec1421deed2fef48b3 Mon Sep 17 00:00:00 2001 From: Ian Botsford <83236726+ianbotsf@users.noreply.github.com> Date: Thu, 27 Feb 2025 11:07:38 -0800 Subject: [PATCH 60/79] chore: bump Ktor dependency (#1250) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 577fc9e896..c3e00661d1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -28,7 +28,7 @@ kotlin-compile-testing-version = "0.7.0" kotlinx-benchmark-version = "0.4.12" kotlinx-serialization-version = "1.7.3" docker-java-version = "3.4.0" -ktor-version = "3.0.0" +ktor-version = "3.1.1" kaml-version = "0.55.0" jsoup-version = "1.18.1" From aa02ac477c99309efe208997fa1a37ff506f8f56 Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Thu, 27 Feb 2025 19:23:37 +0000 Subject: [PATCH 61/79] chore: release 1.4.9 --- .changes/13a47747-197a-4b88-bc3b-393af5f5127a.json | 5 ----- CHANGELOG.md | 5 +++++ gradle.properties | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) delete mode 100644 .changes/13a47747-197a-4b88-bc3b-393af5f5127a.json diff --git a/.changes/13a47747-197a-4b88-bc3b-393af5f5127a.json b/.changes/13a47747-197a-4b88-bc3b-393af5f5127a.json deleted file mode 100644 index 42d876eaa8..0000000000 --- a/.changes/13a47747-197a-4b88-bc3b-393af5f5127a.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "id": "13a47747-197a-4b88-bc3b-393af5f5127a", - "type": "bugfix", - "description": "Correctly generate paginators for item type names which collide with other used types (e.g., an item type `com.foo.Flow` which conflicts with `kotlinx.coroutines.flow.Flow`)" -} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 760b2fb7c6..3ed6348229 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## [1.4.9] - 02/27/2025 + +### Fixes +* Correctly generate paginators for item type names which collide with other used types (e.g., an item type `com.foo.Flow` which conflicts with `kotlinx.coroutines.flow.Flow`) + ## [1.4.8] - 02/27/2025 ### Fixes diff --git a/gradle.properties b/gradle.properties index 64e9a5db45..9a662d4a5f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.4.9-SNAPSHOT +sdkVersion=1.4.9 # codegen -codegenVersion=0.34.9-SNAPSHOT \ No newline at end of file +codegenVersion=0.34.9 \ No newline at end of file From 8178808ff144a553b73752ab6cbcfa10b17a0d95 Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Thu, 27 Feb 2025 19:23:39 +0000 Subject: [PATCH 62/79] chore: bump snapshot version to 1.4.10-SNAPSHOT --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 9a662d4a5f..d59c4c4c0c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.4.9 +sdkVersion=1.4.10-SNAPSHOT # codegen -codegenVersion=0.34.9 \ No newline at end of file +codegenVersion=0.34.10-SNAPSHOT \ No newline at end of file From 4b0df676a14ba1985beee9e6807fa7f72125f9a2 Mon Sep 17 00:00:00 2001 From: Matas Date: Mon, 3 Mar 2025 11:27:48 -0500 Subject: [PATCH 63/79] misc: remove `@InternalApi` from `SdkClientOption` extension functions (#1252) --- .../src/aws/smithy/kotlin/runtime/client/SdkClientOption.kt | 5 ----- 1 file changed, 5 deletions(-) diff --git a/runtime/smithy-client/common/src/aws/smithy/kotlin/runtime/client/SdkClientOption.kt b/runtime/smithy-client/common/src/aws/smithy/kotlin/runtime/client/SdkClientOption.kt index 49045379b3..226ff8efbd 100644 --- a/runtime/smithy-client/common/src/aws/smithy/kotlin/runtime/client/SdkClientOption.kt +++ b/runtime/smithy-client/common/src/aws/smithy/kotlin/runtime/client/SdkClientOption.kt @@ -5,7 +5,6 @@ package aws.smithy.kotlin.runtime.client -import aws.smithy.kotlin.runtime.InternalApi import aws.smithy.kotlin.runtime.collections.AttributeKey import aws.smithy.kotlin.runtime.operation.ExecutionContext @@ -48,27 +47,23 @@ public object SdkClientOption { /** * Get the [IdempotencyTokenProvider] from the context. If one is not set the default will be returned. */ -@InternalApi public val ExecutionContext.idempotencyTokenProvider: IdempotencyTokenProvider get() = getOrNull(SdkClientOption.IdempotencyTokenProvider) ?: IdempotencyTokenProvider.Default /** * Get the [LogMode] from the context. If one is not set a default will be returned */ -@InternalApi public val ExecutionContext.logMode: LogMode get() = getOrNull(SdkClientOption.LogMode) ?: LogMode.Default /** * Get the name of the operation being invoked from the context. */ -@InternalApi public val ExecutionContext.operationName: String? get() = getOrNull(SdkClientOption.OperationName) /** * Get the name of the service being invoked from the context. */ -@InternalApi public val ExecutionContext.serviceName: String? get() = getOrNull(SdkClientOption.ServiceName) From 6f94c6a07a6fd17e643615e265084cad274c6b8c Mon Sep 17 00:00:00 2001 From: Ian Botsford <83236726+ianbotsf@users.noreply.github.com> Date: Mon, 3 Mar 2025 09:24:33 -0800 Subject: [PATCH 64/79] fix: correctly handle sequential calls to `SingleFlightGroup` (#1251) --- .changes/065e6349-8e2a-403b-b588-067161f260fd.json | 5 +++++ .../smithy/kotlin/runtime/util/SingleFlightGroup.kt | 2 +- .../kotlin/runtime/util/SingleFlightGroupTest.kt | 10 ++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 .changes/065e6349-8e2a-403b-b588-067161f260fd.json diff --git a/.changes/065e6349-8e2a-403b-b588-067161f260fd.json b/.changes/065e6349-8e2a-403b-b588-067161f260fd.json new file mode 100644 index 0000000000..80a6796004 --- /dev/null +++ b/.changes/065e6349-8e2a-403b-b588-067161f260fd.json @@ -0,0 +1,5 @@ +{ + "id": "065e6349-8e2a-403b-b588-067161f260fd", + "type": "bugfix", + "description": "Correctly handle sequential calls to `SingleFlightGroup`" +} \ No newline at end of file diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/SingleFlightGroup.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/SingleFlightGroup.kt index 6234d20733..5f36f3a4ae 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/SingleFlightGroup.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/util/SingleFlightGroup.kt @@ -30,7 +30,7 @@ public class SingleFlightGroup { public suspend fun singleFlight(block: suspend () -> T): T { mu.lock() val job = inFlight - if (job != null) { + if (job?.isActive == true) { waitCount++ mu.unlock() diff --git a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/SingleFlightGroupTest.kt b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/SingleFlightGroupTest.kt index cfe9d626fb..f468889d7b 100644 --- a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/SingleFlightGroupTest.kt +++ b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/util/SingleFlightGroupTest.kt @@ -109,4 +109,14 @@ class SingleFlightGroupTest { } } } + + @Test + fun testSequential() = runTest { + val group = SingleFlightGroup() + val first = group.singleFlight { "Foo" } + assertEquals("Foo", first) + + val second = group.singleFlight { "Bar" } + assertEquals("Bar", second) + } } From 667161b5856bfb3519c93d888f1c0290a7c4d07e Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Thu, 6 Mar 2025 14:38:30 +0000 Subject: [PATCH 65/79] chore: release 1.4.10 --- .changes/065e6349-8e2a-403b-b588-067161f260fd.json | 5 ----- CHANGELOG.md | 5 +++++ gradle.properties | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) delete mode 100644 .changes/065e6349-8e2a-403b-b588-067161f260fd.json diff --git a/.changes/065e6349-8e2a-403b-b588-067161f260fd.json b/.changes/065e6349-8e2a-403b-b588-067161f260fd.json deleted file mode 100644 index 80a6796004..0000000000 --- a/.changes/065e6349-8e2a-403b-b588-067161f260fd.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "id": "065e6349-8e2a-403b-b588-067161f260fd", - "type": "bugfix", - "description": "Correctly handle sequential calls to `SingleFlightGroup`" -} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ed6348229..28f8c84430 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## [1.4.10] - 03/06/2025 + +### Fixes +* Correctly handle sequential calls to `SingleFlightGroup` + ## [1.4.9] - 02/27/2025 ### Fixes diff --git a/gradle.properties b/gradle.properties index d59c4c4c0c..1f53236226 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.4.10-SNAPSHOT +sdkVersion=1.4.10 # codegen -codegenVersion=0.34.10-SNAPSHOT \ No newline at end of file +codegenVersion=0.34.10 \ No newline at end of file From f1d5f6b27b834de30e1b86ca6cbaf1c37b449a71 Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Thu, 6 Mar 2025 14:38:32 +0000 Subject: [PATCH 66/79] chore: bump snapshot version to 1.4.11-SNAPSHOT --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 1f53236226..f5f5e8b0f2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.4.10 +sdkVersion=1.4.11-SNAPSHOT # codegen -codegenVersion=0.34.10 \ No newline at end of file +codegenVersion=0.34.11-SNAPSHOT \ No newline at end of file From bf9a2007f637ecc618039e3b21e77c32ce928c74 Mon Sep 17 00:00:00 2001 From: 0marperez <60363173+0marperez@users.noreply.github.com> Date: Wed, 12 Mar 2025 14:26:54 -0700 Subject: [PATCH 67/79] feat: main to feature branches merge (#1254) --- .github/workflows/merge-main.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .github/workflows/merge-main.yml diff --git a/.github/workflows/merge-main.yml b/.github/workflows/merge-main.yml new file mode 100644 index 0000000000..560876bdf3 --- /dev/null +++ b/.github/workflows/merge-main.yml @@ -0,0 +1,14 @@ +name: Merge main +on: + schedule: + - cron: "0 7 * * 1-5" # At 07:00 UTC (00:00 PST, 03:00 EST), Monday through Friday + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Merge main + uses: awslabs/aws-kotlin-repo-tools/.github/actions/merge-main@main + with: + exempt-branches: # Add any if required \ No newline at end of file From 6e10c011d6edb1d9abe52cd9ac05114e1c2d8f20 Mon Sep 17 00:00:00 2001 From: Xinsong Cui Date: Fri, 14 Mar 2025 14:28:22 -0400 Subject: [PATCH 68/79] feat: emit accountId metrics (#1255) * add support for md5 checksum validation * add businees metrics * remove unrelated changes * remove unrelated changes * update api * lint * remove comment --- runtime/runtime-core/api/runtime-core.api | 4 ++++ .../kotlin/runtime/businessmetrics/BusinessMetricsUtils.kt | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/runtime/runtime-core/api/runtime-core.api b/runtime/runtime-core/api/runtime-core.api index 576903f1cf..3c8fbfa2bd 100644 --- a/runtime/runtime-core/api/runtime-core.api +++ b/runtime/runtime-core/api/runtime-core.api @@ -104,6 +104,9 @@ public final class aws/smithy/kotlin/runtime/businessmetrics/BusinessMetricsUtil public final class aws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric : java/lang/Enum, aws/smithy/kotlin/runtime/businessmetrics/BusinessMetric { public static final field ACCOUNT_ID_BASED_ENDPOINT Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric; + public static final field ACCOUNT_ID_MODE_DISABLED Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric; + public static final field ACCOUNT_ID_MODE_PREFERRED Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric; + public static final field ACCOUNT_ID_MODE_REQUIRED Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric; public static final field FLEXIBLE_CHECKSUMS_REQ_CRC32 Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric; public static final field FLEXIBLE_CHECKSUMS_REQ_CRC32C Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric; public static final field FLEXIBLE_CHECKSUMS_REQ_SHA1 Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric; @@ -115,6 +118,7 @@ public final class aws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetri public static final field GZIP_REQUEST_COMPRESSION Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric; public static final field PAGINATOR Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric; public static final field PROTOCOL_RPC_V2_CBOR Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric; + public static final field RESOLVED_ACCOUNT_ID Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric; public static final field RETRY_MODE_ADAPTIVE Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric; public static final field RETRY_MODE_STANDARD Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric; public static final field SERVICE_ENDPOINT_OVERRIDE Laws/smithy/kotlin/runtime/businessmetrics/SmithyBusinessMetric; diff --git a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/businessmetrics/BusinessMetricsUtils.kt b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/businessmetrics/BusinessMetricsUtils.kt index 0600752a6e..13a88aea29 100644 --- a/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/businessmetrics/BusinessMetricsUtils.kt +++ b/runtime/runtime-core/common/src/aws/smithy/kotlin/runtime/businessmetrics/BusinessMetricsUtils.kt @@ -89,7 +89,11 @@ public enum class SmithyBusinessMetric(public override val identifier: String) : PROTOCOL_RPC_V2_CBOR("M"), SERVICE_ENDPOINT_OVERRIDE("N"), ACCOUNT_ID_BASED_ENDPOINT("O"), + ACCOUNT_ID_MODE_PREFERRED("P"), + ACCOUNT_ID_MODE_DISABLED("Q"), + ACCOUNT_ID_MODE_REQUIRED("R"), SIGV4A_SIGNING("S"), + RESOLVED_ACCOUNT_ID("T"), FLEXIBLE_CHECKSUMS_REQ_CRC32("U"), FLEXIBLE_CHECKSUMS_REQ_CRC32C("V"), FLEXIBLE_CHECKSUMS_REQ_SHA1("X"), From 1caac3030bc7a87054216a639dce213bb1eec0eb Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Fri, 14 Mar 2025 19:51:24 +0000 Subject: [PATCH 69/79] chore: release 1.4.11 --- CHANGELOG.md | 2 ++ gradle.properties | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28f8c84430..cbcfddf75f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [1.4.11] - 03/14/2025 + ## [1.4.10] - 03/06/2025 ### Fixes diff --git a/gradle.properties b/gradle.properties index f5f5e8b0f2..2a633287da 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.4.11-SNAPSHOT +sdkVersion=1.4.11 # codegen -codegenVersion=0.34.11-SNAPSHOT \ No newline at end of file +codegenVersion=0.34.11 \ No newline at end of file From 0a29a00a17964b7126974744127765c52a8af2a0 Mon Sep 17 00:00:00 2001 From: aws-sdk-kotlin-ci Date: Fri, 14 Mar 2025 19:51:26 +0000 Subject: [PATCH 70/79] chore: bump snapshot version to 1.4.12-SNAPSHOT --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 2a633287da..dff2f2c264 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ kotlinx.atomicfu.enableNativeIrTransformation=false org.gradle.jvmargs=-Xmx2G -XX:MaxMetaspaceSize=1G # SDK -sdkVersion=1.4.11 +sdkVersion=1.4.12-SNAPSHOT # codegen -codegenVersion=0.34.11 \ No newline at end of file +codegenVersion=0.34.12-SNAPSHOT \ No newline at end of file From b26eeffe630453f0b57cd1bd7e6c547e9aed3053 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Mon, 17 Mar 2025 14:35:53 -0400 Subject: [PATCH 71/79] Fix micrometer-version --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8cecc7ba03..9c78ac4ba9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,7 +14,7 @@ otel-version = "1.45.0" slf4j-version = "2.0.16" slf4j-v1x-version = "1.7.36" crt-kotlin-version = "0.9.1" -micrometer-version = "1.13.6" +micrometer-version = "1.14.2" binary-compatibility-validator-version = "0.16.3" kotlin-multiplatform-bignum-version = "0.3.10" kotlinx-datetime-version = "0.6.1" From 6cbc90c3bd40481232b59bfb38b02e7aa3d3ebb6 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Tue, 18 Mar 2025 11:00:05 -0400 Subject: [PATCH 72/79] Run on macOS 15 --- .github/workflows/continuous-integration.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index b09ce9173d..64bd233abf 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -45,13 +45,13 @@ jobs: run: | ./gradlew -Paws.kotlin.native=false -Ptest.java.version=${{ matrix.java-version }} jvmTest --stacktrace - # macos-14 build and test for targets: jvm, macoArm64, iosSimulatorArm64, watchosSimulatorArm65, tvosSimulatorArm64 - # macos-13 build and test for targets: jvm, macoX64, iosX64, tvosX64, watchosX64 + # macos-15 build and test for targets: jvm, macosArm64, iosSimulatorArm64, watchosSimulatorArm65, tvosSimulatorArm64: https://github.com/actions/runner-images/blob/main/images/macos/macos-15-arm64-Readme.md + # macos-15-large build and test for targets: jvm, macosX64, iosX64, tvosX64, watchosX64: https://github.com/actions/runner-images/blob/main/images/macos/macos-15-Readme.md macos: strategy: fail-fast: false matrix: - os: [macos-14, macos-13] + os: [macos-15, macos-15-large] runs-on: ${{ matrix.os }} steps: - name: Checkout sources From 26616588e58d4e3986c74b5fe704d1c9a014de68 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Tue, 18 Mar 2025 12:30:32 -0400 Subject: [PATCH 73/79] Revert to macos14 / 13 --- .github/workflows/continuous-integration.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 64bd233abf..b09ce9173d 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -45,13 +45,13 @@ jobs: run: | ./gradlew -Paws.kotlin.native=false -Ptest.java.version=${{ matrix.java-version }} jvmTest --stacktrace - # macos-15 build and test for targets: jvm, macosArm64, iosSimulatorArm64, watchosSimulatorArm65, tvosSimulatorArm64: https://github.com/actions/runner-images/blob/main/images/macos/macos-15-arm64-Readme.md - # macos-15-large build and test for targets: jvm, macosX64, iosX64, tvosX64, watchosX64: https://github.com/actions/runner-images/blob/main/images/macos/macos-15-Readme.md + # macos-14 build and test for targets: jvm, macoArm64, iosSimulatorArm64, watchosSimulatorArm65, tvosSimulatorArm64 + # macos-13 build and test for targets: jvm, macoX64, iosX64, tvosX64, watchosX64 macos: strategy: fail-fast: false matrix: - os: [macos-15, macos-15-large] + os: [macos-14, macos-13] runs-on: ${{ matrix.os }} steps: - name: Checkout sources From 419e5665da03724ff4011d8aa4a1d0e5fc3fa8bb Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Tue, 18 Mar 2025 12:33:11 -0400 Subject: [PATCH 74/79] Disable KotlinNativeSimulatorTest tasks --- runtime/build.gradle.kts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/runtime/build.gradle.kts b/runtime/build.gradle.kts index 2ff2032916..f70686fb45 100644 --- a/runtime/build.gradle.kts +++ b/runtime/build.gradle.kts @@ -7,6 +7,7 @@ import aws.sdk.kotlin.gradle.kmp.* import aws.sdk.kotlin.gradle.util.typedProp import org.gradle.kotlin.dsl.withType import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.targets.native.tasks.KotlinNativeSimulatorTest plugins { alias(libs.plugins.dokka) @@ -115,3 +116,20 @@ subprojects { } configureIosSimulatorTasks() + +/* +FIXME iOS simulator tests fail on GitHub CI with "Bad or unknown session": +> Task :runtime:runtime-core:linkDebugTestIosSimulatorArm64 +java.lang.IllegalStateException: You have standalone simulator tests run mode disabled and tests have failed to run. + +The problem can be that you have not booted the required device or have configured the task to a different simulator. Please check the task output and its device configuration. +> Task :runtime:runtime-core:iosSimulatorArm64Test +If you are sure that your setup is correct, please file an issue: https://kotl.in/issue +An error was encountered processing the command (domain=com.apple.CoreSimulator.SimError, code=405): +Process spawn via launchd failed because device is not booted. +Underlying error (domain=com.apple.SimLaunchHostService.RequestError, code=3): + Bad or unknown session: com.apple.CoreSimulator.SimDevice.C120BDE1-C108-4759-842F-7D82B4E71E8C +*/ +tasks.withType { + enabled = false +} From b15c05ecd40961bfd18b8e591407a9fb2c4f0e18 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Tue, 18 Mar 2025 13:20:51 -0400 Subject: [PATCH 75/79] Don't run `configureIosSimulatorTasks` --- runtime/build.gradle.kts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/runtime/build.gradle.kts b/runtime/build.gradle.kts index f70686fb45..8e4cc22950 100644 --- a/runtime/build.gradle.kts +++ b/runtime/build.gradle.kts @@ -115,8 +115,6 @@ subprojects { } } -configureIosSimulatorTasks() - /* FIXME iOS simulator tests fail on GitHub CI with "Bad or unknown session": > Task :runtime:runtime-core:linkDebugTestIosSimulatorArm64 @@ -133,3 +131,4 @@ Underlying error (domain=com.apple.SimLaunchHostService.RequestError, code=3): tasks.withType { enabled = false } +//configureIosSimulatorTasks() \ No newline at end of file From c519e315db37bd41733f02d026ac8f72fb65b75e Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Tue, 18 Mar 2025 13:26:11 -0400 Subject: [PATCH 76/79] ktlint --- runtime/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/build.gradle.kts b/runtime/build.gradle.kts index 8e4cc22950..6a70c018ee 100644 --- a/runtime/build.gradle.kts +++ b/runtime/build.gradle.kts @@ -131,4 +131,4 @@ Underlying error (domain=com.apple.SimLaunchHostService.RequestError, code=3): tasks.withType { enabled = false } -//configureIosSimulatorTasks() \ No newline at end of file +// configureIosSimulatorTasks() From 6ec9b768a60e8d78fd2c75d7e8f883e19f875ecf Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Tue, 18 Mar 2025 15:01:47 -0400 Subject: [PATCH 77/79] Disable SigV4a tests on Native --- .../runtime/auth/awssigning/SigV4aSignatureCalculatorTest.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/runtime/auth/aws-signing-default/common/test/aws/smithy/kotlin/runtime/auth/awssigning/SigV4aSignatureCalculatorTest.kt b/runtime/auth/aws-signing-default/common/test/aws/smithy/kotlin/runtime/auth/awssigning/SigV4aSignatureCalculatorTest.kt index c4c4abe953..81421fcce7 100644 --- a/runtime/auth/aws-signing-default/common/test/aws/smithy/kotlin/runtime/auth/awssigning/SigV4aSignatureCalculatorTest.kt +++ b/runtime/auth/aws-signing-default/common/test/aws/smithy/kotlin/runtime/auth/awssigning/SigV4aSignatureCalculatorTest.kt @@ -4,6 +4,7 @@ */ package aws.smithy.kotlin.runtime.auth.awssigning +import aws.smithy.kotlin.runtime.IgnoreNative import aws.smithy.kotlin.runtime.auth.awscredentials.Credentials import aws.smithy.kotlin.runtime.time.Instant import aws.smithy.kotlin.runtime.util.PlatformProvider @@ -68,6 +69,7 @@ private val TESTS = listOf( * Tests for [SigV4aSignatureCalculator]. Currently only tests forming the string-to-sign. */ class SigV4aSignatureCalculatorTest { + @IgnoreNative // FIXME test resources are not loadable on iOS: https://youtrack.jetbrains.com/issue/KT-49981/ @Test fun testStringToSign() = TESTS.forEach { testId -> runTest { From 56d3b8cd032fd31972a3fe3f2cab20f68f1315b5 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Tue, 18 Mar 2025 16:36:02 -0400 Subject: [PATCH 78/79] Disable simulator tests in `subprojects` --- runtime/build.gradle.kts | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/runtime/build.gradle.kts b/runtime/build.gradle.kts index 6a70c018ee..710e7fd8da 100644 --- a/runtime/build.gradle.kts +++ b/runtime/build.gradle.kts @@ -113,22 +113,24 @@ subprojects { } } } -} -/* -FIXME iOS simulator tests fail on GitHub CI with "Bad or unknown session": -> Task :runtime:runtime-core:linkDebugTestIosSimulatorArm64 -java.lang.IllegalStateException: You have standalone simulator tests run mode disabled and tests have failed to run. - -The problem can be that you have not booted the required device or have configured the task to a different simulator. Please check the task output and its device configuration. -> Task :runtime:runtime-core:iosSimulatorArm64Test -If you are sure that your setup is correct, please file an issue: https://kotl.in/issue -An error was encountered processing the command (domain=com.apple.CoreSimulator.SimError, code=405): -Process spawn via launchd failed because device is not booted. -Underlying error (domain=com.apple.SimLaunchHostService.RequestError, code=3): - Bad or unknown session: com.apple.CoreSimulator.SimDevice.C120BDE1-C108-4759-842F-7D82B4E71E8C -*/ -tasks.withType { - enabled = false + /* + FIXME iOS simulator tests fail on GitHub CI with "Bad or unknown session": + > Task :runtime:runtime-core:linkDebugTestIosSimulatorArm64 + java.lang.IllegalStateException: You have standalone simulator tests run mode disabled and tests have failed to run. + + The problem can be that you have not booted the required device or have configured the task to a different simulator. Please check the task output and its device configuration. + > Task :runtime:runtime-core:iosSimulatorArm64Test + If you are sure that your setup is correct, please file an issue: https://kotl.in/issue + An error was encountered processing the command (domain=com.apple.CoreSimulator.SimError, code=405): + Process spawn via launchd failed because device is not booted. + Underlying error (domain=com.apple.SimLaunchHostService.RequestError, code=3): + Bad or unknown session: com.apple.CoreSimulator.SimDevice.C120BDE1-C108-4759-842F-7D82B4E71E8C + */ + tasks.withType { + enabled = false + } } + + // configureIosSimulatorTasks() From 08e18a1e450586e4fdfb4cb0fe829fec3dfbe658 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Tue, 18 Mar 2025 18:32:10 -0400 Subject: [PATCH 79/79] ktlint --- runtime/build.gradle.kts | 1 - 1 file changed, 1 deletion(-) diff --git a/runtime/build.gradle.kts b/runtime/build.gradle.kts index 710e7fd8da..ea7c0acfda 100644 --- a/runtime/build.gradle.kts +++ b/runtime/build.gradle.kts @@ -132,5 +132,4 @@ subprojects { } } - // configureIosSimulatorTasks()