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/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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/53] 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 df89e93e6909a740f40952e4ecfcdeb278ce248c Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Thu, 30 Jan 2025 10:23:05 -0500 Subject: [PATCH 32/53] Implement HostResolver using getaddrinfo --- .../kotlin/runtime/net/HostResolverTest.kt | 90 +++++++++++++++++++ .../kotlin/runtime/net/HostResolverNative.kt | 62 +++++++++++-- 2 files changed, 145 insertions(+), 7 deletions(-) create mode 100644 runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/net/HostResolverTest.kt diff --git a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/net/HostResolverTest.kt b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/net/HostResolverTest.kt new file mode 100644 index 0000000000..c69117685b --- /dev/null +++ b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/net/HostResolverTest.kt @@ -0,0 +1,90 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package aws.smithy.kotlin.runtime.net + +import kotlinx.coroutines.test.runTest +import kotlin.test.* + +class HostResolverTest { + @Test + fun testResolveLocalhost() = runTest { + val addresses = HostResolver.Default.resolve("localhost") + assertTrue(addresses.isNotEmpty()) + + addresses.forEach { addr -> + assertEquals("localhost", addr.hostname) + when (val ip = addr.address) { + is IpV4Addr -> { + assertEquals(4, ip.octets.size) + // localhost should resolve to 127.0.0.1 + assertContentEquals(byteArrayOf(127, 0, 0, 1), ip.octets) + } + is IpV6Addr -> { + assertEquals(16, ip.octets.size) + // ::1 in IPv6 + val expectedIpv6 = ByteArray(16) { 0 } + expectedIpv6[15] = 1 + assertContentEquals(expectedIpv6, ip.octets) + } + } + } + } + + @Test + fun testResolveIpv4Address() = runTest { + val addresses = HostResolver.Default.resolve("127.0.0.1") + assertTrue(addresses.isNotEmpty()) + + addresses.forEach { addr -> + assertTrue(addr.address is IpV4Addr) + assertContentEquals(byteArrayOf(127, 0, 0, 1), addr.address.octets) + } + } + + @Test + fun testResolveIpv6Address() = runTest { + val addresses = HostResolver.Default.resolve("::1") + assertTrue(addresses.isNotEmpty()) + + addresses.forEach { addr -> + assertTrue(addr.address is IpV6Addr) + val expectedBytes = ByteArray(16) { 0 } + expectedBytes[15] = 1 + assertContentEquals(expectedBytes, addr.address.octets) + } + } + + @Test + fun testResolveExampleDomain() = runTest { + val addresses = HostResolver.Default.resolve("example.com") + assertNotNull(addresses) + assertTrue(addresses.isNotEmpty()) + + addresses.forEach { addr -> + assertEquals("example.com", addr.hostname) + when (val ip = addr.address) { + is IpV4Addr -> assertEquals(4, ip.octets.size) + is IpV6Addr -> assertEquals(16, ip.octets.size) + } + } + } + + @Test + fun testResolveInvalidDomain() = runTest { + assertFails { + HostResolver.Default.resolve("this-domain-definitely-does-not-exist-12345.local") + } + } + + @Test + fun testNoopMethods() { + // Test no-op methods don't throw + val dummyAddr = HostAddress("test.com", IpV4Addr(ByteArray(4))) + val resolver = HostResolver.Default + resolver.reportFailure(dummyAddr) + resolver.purgeCache(null) + resolver.purgeCache(dummyAddr) + } +} \ No newline at end of file diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/net/HostResolverNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/net/HostResolverNative.kt index 4b8fd7e7ae..a5ca217175 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/net/HostResolverNative.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/net/HostResolverNative.kt @@ -4,19 +4,67 @@ */ package aws.smithy.kotlin.runtime.net -import aws.smithy.kotlin.runtime.InternalApi +import kotlinx.cinterop.* +import platform.posix.* internal actual object DefaultHostResolver : HostResolver { - actual override suspend fun resolve(hostname: String): List { - TODO("Not yet implemented") + actual override suspend fun resolve(hostname: String): List = memScoped { + val hints = alloc().apply { + ai_family = AF_UNSPEC // Allow both IPv4 and IPv6 + ai_socktype = SOCK_STREAM // TCP stream sockets + ai_flags = AI_PASSIVE // For wildcard IP address + } + + val result = allocPointerTo() + + // Perform the DNS lookup + val status = getaddrinfo(hostname, null, hints.ptr, result.ptr) + if (status != 0) { + throw RuntimeException("Failed to resolve host $hostname: ${gai_strerror(status)?.toKString()}") + } + + val addresses = mutableListOf() + var current = result.value + + while (current != null) { + val sockaddr = current.pointed.ai_addr!!.pointed + + @OptIn(UnsafeNumber::class) + when (sockaddr.sa_family.toInt()) { + AF_INET -> { + val addr = sockaddr.reinterpret() + val ipBytes = ByteArray(4) + memcpy(ipBytes.refTo(0), addr.sin_addr.ptr, 4uL) + + addresses.add(HostAddress( + hostname = hostname, + address = IpV4Addr(ipBytes) + )) + } + AF_INET6 -> { + val addr = sockaddr.reinterpret() + val ipBytes = ByteArray(16) + memcpy(ipBytes.refTo(0), addr.sin6_addr.ptr, 16.convert()) + addresses.add(HostAddress( + hostname = hostname, + address = IpV6Addr(ipBytes) + )) + } + } + current = current.pointed.ai_next + } + + // Free the getaddrinfo results + freeaddrinfo(result.value) + + addresses } actual override fun reportFailure(addr: HostAddress) { - TODO("Not yet implemented") + // No-op, same as JVM implementation } - @InternalApi actual override fun purgeCache(addr: HostAddress?) { - TODO("Not yet implemented") + // No-op, same as JVM implementation } -} +} \ No newline at end of file From 746881d93b17e2fc6d4633c431c536076edb2689 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Thu, 30 Jan 2025 10:24:28 -0500 Subject: [PATCH 33/53] ktlint --- .../kotlin/runtime/net/HostResolverTest.kt | 2 +- .../kotlin/runtime/net/HostResolverNative.kt | 26 +++++++++++-------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/net/HostResolverTest.kt b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/net/HostResolverTest.kt index c69117685b..709bdcd38c 100644 --- a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/net/HostResolverTest.kt +++ b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/net/HostResolverTest.kt @@ -87,4 +87,4 @@ class HostResolverTest { resolver.purgeCache(null) resolver.purgeCache(dummyAddr) } -} \ No newline at end of file +} diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/net/HostResolverNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/net/HostResolverNative.kt index a5ca217175..f623508e43 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/net/HostResolverNative.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/net/HostResolverNative.kt @@ -10,9 +10,9 @@ import platform.posix.* internal actual object DefaultHostResolver : HostResolver { actual override suspend fun resolve(hostname: String): List = memScoped { val hints = alloc().apply { - ai_family = AF_UNSPEC // Allow both IPv4 and IPv6 + ai_family = AF_UNSPEC // Allow both IPv4 and IPv6 ai_socktype = SOCK_STREAM // TCP stream sockets - ai_flags = AI_PASSIVE // For wildcard IP address + ai_flags = AI_PASSIVE // For wildcard IP address } val result = allocPointerTo() @@ -36,19 +36,23 @@ internal actual object DefaultHostResolver : HostResolver { val ipBytes = ByteArray(4) memcpy(ipBytes.refTo(0), addr.sin_addr.ptr, 4uL) - addresses.add(HostAddress( - hostname = hostname, - address = IpV4Addr(ipBytes) - )) + addresses.add( + HostAddress( + hostname = hostname, + address = IpV4Addr(ipBytes), + ), + ) } AF_INET6 -> { val addr = sockaddr.reinterpret() val ipBytes = ByteArray(16) memcpy(ipBytes.refTo(0), addr.sin6_addr.ptr, 16.convert()) - addresses.add(HostAddress( - hostname = hostname, - address = IpV6Addr(ipBytes) - )) + addresses.add( + HostAddress( + hostname = hostname, + address = IpV6Addr(ipBytes), + ), + ) } } current = current.pointed.ai_next @@ -67,4 +71,4 @@ internal actual object DefaultHostResolver : HostResolver { actual override fun purgeCache(addr: HostAddress?) { // No-op, same as JVM implementation } -} \ No newline at end of file +} From be248ae8c0c5f7f5c72d38bdd1abb12cc47930b0 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Thu, 30 Jan 2025 10:32:22 -0500 Subject: [PATCH 34/53] Upgrade to upload-artifact@v4 --- .github/workflows/continuous-integration.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 3187339faf..365a70b22b 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -73,7 +73,7 @@ jobs: - 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' @@ -107,7 +107,7 @@ jobs: - 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' From 5ab633db5ce1349908623bf9f08444d723c26ac8 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Thu, 30 Jan 2025 13:33:34 -0500 Subject: [PATCH 35/53] Apply iOS simulator fixes from aws-crt-kotlin --- runtime/build.gradle.kts | 44 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/runtime/build.gradle.kts b/runtime/build.gradle.kts index bea1350f51..6574598a05 100644 --- a/runtime/build.gradle.kts +++ b/runtime/build.gradle.kts @@ -5,7 +5,10 @@ import aws.sdk.kotlin.gradle.dsl.configurePublishing 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 +import org.jetbrains.kotlin.konan.target.HostManager plugins { alias(libs.plugins.dokka) @@ -67,6 +70,47 @@ subprojects { } } } + + + // disable "standalone" mode in simulator tests since it causes TLS issues. this means we need to manage the simulator + // ourselves (booting / shutting down). FIXME: https://youtrack.jetbrains.com/issue/KT-38317 + val simulatorDeviceName = project.findProperty("iosSimulatorDevice") as? String ?: "iPhone 15" + + val xcrun = "/usr/bin/xcrun" + + tasks.register("bootIosSimulatorDevice") { + isIgnoreExitValue = true + commandLine(xcrun, "simctl", "boot", simulatorDeviceName) + + doLast { + val result = executionResult.get() + val code = result.exitValue + if (code != 148 && code != 149) { // ignore "simulator already running" errors + result.assertNormalExitValue() + } + } + } + + tasks.register("shutdownIosSimulatorDevice") { + mustRunAfter(tasks.withType()) + commandLine(xcrun, "simctl", "shutdown", simulatorDeviceName) + + doLast { + executionResult.get().assertNormalExitValue() + } + } + + tasks.withType().configureEach { + if (!HostManager.hostIsMac) { + return@configureEach + } + + dependsOn("bootIosSimulatorDevice") + finalizedBy("shutdownIosSimulatorDevice") + + standalone = false + device = simulatorDeviceName + } } kotlin.sourceSets.all { From e6357f92abd84dd2276a0f9ba27204be610d0068 Mon Sep 17 00:00:00 2001 From: Xinsong Cui Date: Thu, 30 Jan 2025 16:34:10 -0500 Subject: [PATCH 36/53] 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 94e5878fdafa670e1c28fc9fa5d3f44fbba405ce Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Fri, 31 Jan 2025 11:43:38 -0500 Subject: [PATCH 37/53] Ignore code 405 on shutdown --- runtime/build.gradle.kts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/runtime/build.gradle.kts b/runtime/build.gradle.kts index 6574598a05..4aa0cf269b 100644 --- a/runtime/build.gradle.kts +++ b/runtime/build.gradle.kts @@ -71,7 +71,6 @@ subprojects { } } - // disable "standalone" mode in simulator tests since it causes TLS issues. this means we need to manage the simulator // ourselves (booting / shutting down). FIXME: https://youtrack.jetbrains.com/issue/KT-38317 val simulatorDeviceName = project.findProperty("iosSimulatorDevice") as? String ?: "iPhone 15" @@ -96,7 +95,10 @@ subprojects { commandLine(xcrun, "simctl", "shutdown", simulatorDeviceName) doLast { - executionResult.get().assertNormalExitValue() + val result = executionResult.get() + if (result.exitValue != 405) { + result.assertNormalExitValue() + } } } From 2363e269838ec7814889b669b6db5ce850c69b70 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Fri, 31 Jan 2025 13:30:12 -0500 Subject: [PATCH 38/53] Address PR feedback. Use shared configureIosSimulatorTasks --- gradle/libs.versions.toml | 2 +- runtime/build.gradle.kts | 47 +------------ .../kotlin/runtime/net/HostResolverTest.kt | 18 ++--- .../kotlin/runtime/net/HostResolverNative.kt | 70 ++++++++----------- 4 files changed, 37 insertions(+), 100 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9011904081..8fb78f3426 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.21-kn" +aws-kotlin-repo-tools-version = "0.4.22-kn" # libs coroutines-version = "1.9.0" diff --git a/runtime/build.gradle.kts b/runtime/build.gradle.kts index 4aa0cf269b..33ff1930d0 100644 --- a/runtime/build.gradle.kts +++ b/runtime/build.gradle.kts @@ -7,8 +7,6 @@ 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 -import org.jetbrains.kotlin.konan.target.HostManager plugins { alias(libs.plugins.dokka) @@ -70,49 +68,6 @@ subprojects { } } } - - // disable "standalone" mode in simulator tests since it causes TLS issues. this means we need to manage the simulator - // ourselves (booting / shutting down). FIXME: https://youtrack.jetbrains.com/issue/KT-38317 - val simulatorDeviceName = project.findProperty("iosSimulatorDevice") as? String ?: "iPhone 15" - - val xcrun = "/usr/bin/xcrun" - - tasks.register("bootIosSimulatorDevice") { - isIgnoreExitValue = true - commandLine(xcrun, "simctl", "boot", simulatorDeviceName) - - doLast { - val result = executionResult.get() - val code = result.exitValue - if (code != 148 && code != 149) { // ignore "simulator already running" errors - result.assertNormalExitValue() - } - } - } - - tasks.register("shutdownIosSimulatorDevice") { - mustRunAfter(tasks.withType()) - commandLine(xcrun, "simctl", "shutdown", simulatorDeviceName) - - doLast { - val result = executionResult.get() - if (result.exitValue != 405) { - result.assertNormalExitValue() - } - } - } - - tasks.withType().configureEach { - if (!HostManager.hostIsMac) { - return@configureEach - } - - dependsOn("bootIosSimulatorDevice") - finalizedBy("shutdownIosSimulatorDevice") - - standalone = false - device = simulatorDeviceName - } } kotlin.sourceSets.all { @@ -157,4 +112,6 @@ subprojects { } } } + + configureIosSimulatorTasks() } diff --git a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/net/HostResolverTest.kt b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/net/HostResolverTest.kt index 709bdcd38c..3bad624976 100644 --- a/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/net/HostResolverTest.kt +++ b/runtime/runtime-core/common/test/aws/smithy/kotlin/runtime/net/HostResolverTest.kt @@ -15,20 +15,12 @@ class HostResolverTest { addresses.forEach { addr -> assertEquals("localhost", addr.hostname) - when (val ip = addr.address) { - is IpV4Addr -> { - assertEquals(4, ip.octets.size) - // localhost should resolve to 127.0.0.1 - assertContentEquals(byteArrayOf(127, 0, 0, 1), ip.octets) - } - is IpV6Addr -> { - assertEquals(16, ip.octets.size) - // ::1 in IPv6 - val expectedIpv6 = ByteArray(16) { 0 } - expectedIpv6[15] = 1 - assertContentEquals(expectedIpv6, ip.octets) - } + + val localHostAddr = when (addr.address) { + is IpV4Addr -> IpV4Addr.LOCALHOST + is IpV6Addr -> IpV6Addr.LOCALHOST } + assertEquals(addr.address, localHostAddr) } } diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/net/HostResolverNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/net/HostResolverNative.kt index f623508e43..12a30e3599 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/net/HostResolverNative.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/net/HostResolverNative.kt @@ -17,51 +17,39 @@ internal actual object DefaultHostResolver : HostResolver { val result = allocPointerTo() - // Perform the DNS lookup - val status = getaddrinfo(hostname, null, hints.ptr, result.ptr) - if (status != 0) { - throw RuntimeException("Failed to resolve host $hostname: ${gai_strerror(status)?.toKString()}") + try { + // Perform the DNS lookup + val status = getaddrinfo(hostname, null, hints.ptr, result.ptr) + check (status == 0) { "Failed to resolve host $hostname: ${gai_strerror(status)?.toKString()}" } + + return generateSequence(result.value) { it.pointed.ai_next } + .map { it.pointed.ai_addr!!.pointed.toIpAddr() } + .map { HostAddress(hostname, it) } + .toList() + } finally { + freeaddrinfo(result.value) } + } - val addresses = mutableListOf() - var current = result.value - - while (current != null) { - val sockaddr = current.pointed.ai_addr!!.pointed - - @OptIn(UnsafeNumber::class) - when (sockaddr.sa_family.toInt()) { - AF_INET -> { - val addr = sockaddr.reinterpret() - val ipBytes = ByteArray(4) - memcpy(ipBytes.refTo(0), addr.sin_addr.ptr, 4uL) - - addresses.add( - HostAddress( - hostname = hostname, - address = IpV4Addr(ipBytes), - ), - ) - } - AF_INET6 -> { - val addr = sockaddr.reinterpret() - val ipBytes = ByteArray(16) - memcpy(ipBytes.refTo(0), addr.sin6_addr.ptr, 16.convert()) - addresses.add( - HostAddress( - hostname = hostname, - address = IpV6Addr(ipBytes), - ), - ) - } - } - current = current.pointed.ai_next + @OptIn(UnsafeNumber::class) + private fun sockaddr.toIpAddr(): IpAddr { + val (size, addrPtr, constructor) = when (sa_family.toInt()) { + AF_INET -> Triple( + 4, + reinterpret().sin_addr.ptr, + { bytes: ByteArray -> IpV4Addr(bytes) }, + ) + AF_INET6 -> Triple( + 16, + reinterpret().sin6_addr.ptr, + { bytes: ByteArray -> IpV6Addr(bytes) }, + ) + else -> throw IllegalArgumentException("Unsupported sockaddr family $sa_family") } - // Free the getaddrinfo results - freeaddrinfo(result.value) - - addresses + val ipBytes = ByteArray(size) + memcpy(ipBytes.refTo(0), addrPtr, size.toULong()) + return constructor(ipBytes) } actual override fun reportFailure(addr: HostAddress) { From a47da0a032f1763dd4c9348f2cbdfc44863b48c5 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Fri, 31 Jan 2025 13:34:59 -0500 Subject: [PATCH 39/53] ktlint --- .../src/aws/smithy/kotlin/runtime/net/HostResolverNative.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/net/HostResolverNative.kt b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/net/HostResolverNative.kt index 12a30e3599..9224932ef7 100644 --- a/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/net/HostResolverNative.kt +++ b/runtime/runtime-core/native/src/aws/smithy/kotlin/runtime/net/HostResolverNative.kt @@ -20,7 +20,7 @@ internal actual object DefaultHostResolver : HostResolver { try { // Perform the DNS lookup val status = getaddrinfo(hostname, null, hints.ptr, result.ptr) - check (status == 0) { "Failed to resolve host $hostname: ${gai_strerror(status)?.toKString()}" } + check(status == 0) { "Failed to resolve host $hostname: ${gai_strerror(status)?.toKString()}" } return generateSequence(result.value) { it.pointed.ai_next } .map { it.pointed.ai_addr!!.pointed.toIpAddr() } From 57c52c541a0f7065080e2f11005c39a1d9df1d56 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Fri, 31 Jan 2025 13:35:18 -0500 Subject: [PATCH 40/53] Upgrade Gradle version --- 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 3499ded5c1..9bf7bd3397 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.1-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 272b6a0724d1966bfe5b3d54b2284d0be52543a9 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Fri, 31 Jan 2025 13:40:07 -0500 Subject: [PATCH 41/53] Revert "Upgrade Gradle version" This reverts commit 57c52c541a0f7065080e2f11005c39a1d9df1d56. --- 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 9bf7bd3397..3499ded5c1 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.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 3de8e8ceebf1c6b93763030aedc0fe144f9d9aa7 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Fri, 31 Jan 2025 13:55:45 -0500 Subject: [PATCH 42/53] Revert "Revert "Upgrade Gradle version"" This reverts commit 272b6a0724d1966bfe5b3d54b2284d0be52543a9. --- 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 3499ded5c1..9bf7bd3397 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.1-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 0a5beac857f51bb95f8cb7e82bcedb81bcbc6200 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Fri, 31 Jan 2025 14:38:34 -0500 Subject: [PATCH 43/53] CI From aa069f7bdba7941cb63ab1d820fd26c8d1dcf2eb Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Sat, 1 Feb 2025 00:30:57 -0500 Subject: [PATCH 44/53] CI From 334b41c59e998de806b4593a18bcbf6be120f0ad Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Mon, 3 Feb 2025 09:17:00 -0500 Subject: [PATCH 45/53] IgnoreNative --- .../http/interceptors/FlexibleChecksumsRequestInterceptorTest.kt | 1 + 1 file changed, 1 insertion(+) 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 31ae7113cd..f95349752e 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 @@ -140,6 +140,7 @@ class FlexibleChecksumsRequestInterceptorTest { assertEquals(0, call.request.headers.getNumChecksumHeaders()) } + @IgnoreNative // FIXME Re-enable after Kotlin/Native Implementation @Test fun testCompletingSource() = runTest { val hashFunctionName = "crc32" From 5eb2d76e5f3089ff2711a486d1281cc682f617d5 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Mon, 3 Feb 2025 10:23:03 -0500 Subject: [PATCH 46/53] Revert --- .github/workflows/continuous-integration.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 372fdb8cda..3486c8e707 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -163,7 +163,8 @@ jobs: working-directory: ./smithy-kotlin shell: bash run: | - echo "kotlinWarningsAsErrors=true" >> $GITHUB_WORKSPACE/local.properties + # 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 ./gradlew -Paws.kotlin.native=false apiCheck ./gradlew -Paws.kotlin.native=false test jvmTest From d0e6c3d0d260fb4520bad50db34ef61bd57b51f4 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Mon, 3 Feb 2025 11:12:38 -0500 Subject: [PATCH 47/53] Upgrade to latest build plugin --- 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 1815c1cbd0..5dd4f5dead 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.22-kn" +aws-kotlin-repo-tools-version = "0.4.23-kn" # libs coroutines-version = "1.9.0" From 115a818a33de66d5f768954246245dfa403c7019 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Mon, 3 Feb 2025 12:01:59 -0500 Subject: [PATCH 48/53] Temporarily move iOS simulator config to smithy-kotlin to aid debugging. Ignore error code 148/149 in shutdown --- runtime/build.gradle.kts | 58 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/runtime/build.gradle.kts b/runtime/build.gradle.kts index 33ff1930d0..b729527902 100644 --- a/runtime/build.gradle.kts +++ b/runtime/build.gradle.kts @@ -6,7 +6,12 @@ import aws.sdk.kotlin.gradle.dsl.configurePublishing import aws.sdk.kotlin.gradle.kmp.* import aws.sdk.kotlin.gradle.util.typedProp import org.gradle.kotlin.dsl.withType +import org.gradle.api.Project +import org.gradle.api.tasks.Exec +import org.gradle.kotlin.dsl.withType +import org.jetbrains.kotlin.gradle.targets.native.tasks.KotlinNativeSimulatorTest import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.konan.target.HostManager plugins { alias(libs.plugins.dokka) @@ -113,5 +118,56 @@ subprojects { } } - configureIosSimulatorTasks() + smithyConfigureIosSimulatorTasks() +} + + +/** + * Disables standalone mode in simulator tests since it causes issues with TLS. + * This means we need to manage the simulator state ourselves (booting, shutting down). + * https://youtrack.jetbrains.com/issue/KT-38317 + */ +public fun Project.smithyConfigureIosSimulatorTasks() { + val simulatorDeviceName = project.findProperty("iosSimulatorDevice") as? String ?: "iPhone 15" + + val xcrun = "/usr/bin/xcrun" + + tasks.register("bootIosSimulatorDevice", Exec::class.java) { + isIgnoreExitValue = true + commandLine(xcrun, "simctl", "boot", simulatorDeviceName) + + doLast { + val result = executionResult.get() + val code = result.exitValue + if (code != 148 && code != 149) { // ignore "simulator already running" errors + result.assertNormalExitValue() + } + } + } + + tasks.register("shutdownIosSimulatorDevice", Exec::class.java) { + isIgnoreExitValue = true + mustRunAfter(tasks.withType()) + commandLine(xcrun, "simctl", "shutdown", simulatorDeviceName) + + doLast { + val result = executionResult.get() + val code = result.exitValue + if (code != 148 && code != 149) { // ignore "simulator already shutdown" errors + result.assertNormalExitValue() + } + } + } + + tasks.withType().configureEach { + if (!HostManager.hostIsMac) { + return@configureEach + } + + dependsOn("bootIosSimulatorDevice") + finalizedBy("shutdownIosSimulatorDevice") + + standalone.set(false) + device.set(simulatorDeviceName) + } } From c7487fea6c712a58e2073508f120e5648cbc120c Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Mon, 3 Feb 2025 12:12:01 -0500 Subject: [PATCH 49/53] ktlint --- runtime/build.gradle.kts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/runtime/build.gradle.kts b/runtime/build.gradle.kts index b729527902..379b37b779 100644 --- a/runtime/build.gradle.kts +++ b/runtime/build.gradle.kts @@ -5,12 +5,11 @@ import aws.sdk.kotlin.gradle.dsl.configurePublishing import aws.sdk.kotlin.gradle.kmp.* import aws.sdk.kotlin.gradle.util.typedProp -import org.gradle.kotlin.dsl.withType import org.gradle.api.Project import org.gradle.api.tasks.Exec import org.gradle.kotlin.dsl.withType -import org.jetbrains.kotlin.gradle.targets.native.tasks.KotlinNativeSimulatorTest import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.targets.native.tasks.KotlinNativeSimulatorTest import org.jetbrains.kotlin.konan.target.HostManager plugins { @@ -121,7 +120,6 @@ subprojects { smithyConfigureIosSimulatorTasks() } - /** * Disables standalone mode in simulator tests since it causes issues with TLS. * This means we need to manage the simulator state ourselves (booting, shutting down). From a17593361e39ac66adbd92cb7ee223e5bf5e948d Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Mon, 3 Feb 2025 14:18:01 -0500 Subject: [PATCH 50/53] Only run boot and shutdown tasks once --- runtime/build.gradle.kts | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/runtime/build.gradle.kts b/runtime/build.gradle.kts index 379b37b779..422cd19d5a 100644 --- a/runtime/build.gradle.kts +++ b/runtime/build.gradle.kts @@ -126,11 +126,12 @@ subprojects { * https://youtrack.jetbrains.com/issue/KT-38317 */ public fun Project.smithyConfigureIosSimulatorTasks() { - val simulatorDeviceName = project.findProperty("iosSimulatorDevice") as? String ?: "iPhone 15" + if (!HostManager.hostIsMac) return + val simulatorDeviceName = project.findProperty("iosSimulatorDevice") as? String ?: "iPhone 15" val xcrun = "/usr/bin/xcrun" - tasks.register("bootIosSimulatorDevice", Exec::class.java) { + val bootTask = rootProject.tasks.maybeCreate("bootIosSimulatorDevice", Exec::class.java).apply { isIgnoreExitValue = true commandLine(xcrun, "simctl", "boot", simulatorDeviceName) @@ -143,9 +144,8 @@ public fun Project.smithyConfigureIosSimulatorTasks() { } } - tasks.register("shutdownIosSimulatorDevice", Exec::class.java) { + val shutdownTask = rootProject.tasks.maybeCreate("shutdownIosSimulatorDevice", Exec::class.java).apply { isIgnoreExitValue = true - mustRunAfter(tasks.withType()) commandLine(xcrun, "simctl", "shutdown", simulatorDeviceName) doLast { @@ -157,15 +157,14 @@ public fun Project.smithyConfigureIosSimulatorTasks() { } } - tasks.withType().configureEach { - if (!HostManager.hostIsMac) { - return@configureEach + allprojects { + val simulatorTasks = tasks.withType() + simulatorTasks.configureEach { + dependsOn(bootTask) + standalone.set(false) + device.set(simulatorDeviceName) } - - dependsOn("bootIosSimulatorDevice") - finalizedBy("shutdownIosSimulatorDevice") - - standalone.set(false) - device.set(simulatorDeviceName) + shutdownTask.mustRunAfter(simulatorTasks) } } + From c234561ca0dd51f76675d60ff8e39e1498ac900c Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Mon, 3 Feb 2025 14:25:43 -0500 Subject: [PATCH 51/53] ktlint --- runtime/build.gradle.kts | 1 - 1 file changed, 1 deletion(-) diff --git a/runtime/build.gradle.kts b/runtime/build.gradle.kts index 422cd19d5a..6a32e48b03 100644 --- a/runtime/build.gradle.kts +++ b/runtime/build.gradle.kts @@ -167,4 +167,3 @@ public fun Project.smithyConfigureIosSimulatorTasks() { shutdownTask.mustRunAfter(simulatorTasks) } } - From c325a4c9825ef94132b7d8d4d8dd15dce471bacf Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Mon, 3 Feb 2025 20:53:33 -0500 Subject: [PATCH 52/53] Upgrade to latest build plugin --- gradle/libs.versions.toml | 2 +- runtime/build.gradle.kts | 50 +-------------------------------------- 2 files changed, 2 insertions(+), 50 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5dd4f5dead..52e3b8e587 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.23-kn" +aws-kotlin-repo-tools-version = "0.4.24-kn" # libs coroutines-version = "1.9.0" diff --git a/runtime/build.gradle.kts b/runtime/build.gradle.kts index 6a32e48b03..188c68a7c2 100644 --- a/runtime/build.gradle.kts +++ b/runtime/build.gradle.kts @@ -116,54 +116,6 @@ subprojects { } } } - - smithyConfigureIosSimulatorTasks() } -/** - * Disables standalone mode in simulator tests since it causes issues with TLS. - * This means we need to manage the simulator state ourselves (booting, shutting down). - * https://youtrack.jetbrains.com/issue/KT-38317 - */ -public fun Project.smithyConfigureIosSimulatorTasks() { - if (!HostManager.hostIsMac) return - - val simulatorDeviceName = project.findProperty("iosSimulatorDevice") as? String ?: "iPhone 15" - val xcrun = "/usr/bin/xcrun" - - val bootTask = rootProject.tasks.maybeCreate("bootIosSimulatorDevice", Exec::class.java).apply { - isIgnoreExitValue = true - commandLine(xcrun, "simctl", "boot", simulatorDeviceName) - - doLast { - val result = executionResult.get() - val code = result.exitValue - if (code != 148 && code != 149) { // ignore "simulator already running" errors - result.assertNormalExitValue() - } - } - } - - val shutdownTask = rootProject.tasks.maybeCreate("shutdownIosSimulatorDevice", Exec::class.java).apply { - isIgnoreExitValue = true - commandLine(xcrun, "simctl", "shutdown", simulatorDeviceName) - - doLast { - val result = executionResult.get() - val code = result.exitValue - if (code != 148 && code != 149) { // ignore "simulator already shutdown" errors - result.assertNormalExitValue() - } - } - } - - allprojects { - val simulatorTasks = tasks.withType() - simulatorTasks.configureEach { - dependsOn(bootTask) - standalone.set(false) - device.set(simulatorDeviceName) - } - shutdownTask.mustRunAfter(simulatorTasks) - } -} +configureIosSimulatorTasks() From 044b74c11d0c54b975cfa120c6ef4a5a39e9fdf7 Mon Sep 17 00:00:00 2001 From: Matas Lauzadis Date: Mon, 3 Feb 2025 20:58:59 -0500 Subject: [PATCH 53/53] ktlint --- runtime/build.gradle.kts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/runtime/build.gradle.kts b/runtime/build.gradle.kts index 188c68a7c2..2ff2032916 100644 --- a/runtime/build.gradle.kts +++ b/runtime/build.gradle.kts @@ -5,12 +5,8 @@ import aws.sdk.kotlin.gradle.dsl.configurePublishing import aws.sdk.kotlin.gradle.kmp.* import aws.sdk.kotlin.gradle.util.typedProp -import org.gradle.api.Project -import org.gradle.api.tasks.Exec import org.gradle.kotlin.dsl.withType import org.jetbrains.kotlin.gradle.dsl.JvmTarget -import org.jetbrains.kotlin.gradle.targets.native.tasks.KotlinNativeSimulatorTest -import org.jetbrains.kotlin.konan.target.HostManager plugins { alias(libs.plugins.dokka)