Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support default checksums #1191

Open
wants to merge 33 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
2a9d104
Bump smithy IDL version
0marperez Nov 11, 2024
205839c
Add requestChecksumCalculation config option
0marperez Nov 11, 2024
7233b71
Added responseChecksumValidation
0marperez Nov 11, 2024
e1dc616
Add todos for business metrics
0marperez Nov 12, 2024
e436482
Unit tests pass
0marperez Nov 25, 2024
abdba02
Merge branch 'main' of https://github.com/awslabs/smithy-kotlin into …
0marperez Nov 26, 2024
3e4c891
E2E tests pass
0marperez Nov 26, 2024
9760ee1
Self review
0marperez Nov 27, 2024
f8b39b0
Self review 2
0marperez Nov 27, 2024
f676b7b
Smithy codegen version bump
0marperez Nov 27, 2024
6e9b206
Make composite checksum check S3 specific
0marperez Dec 3, 2024
fb3a52a
Turn off all failing protocol tests
0marperez Dec 3, 2024
40bb298
PR feedback and fix breaking changes
0marperez Dec 3, 2024
828adaa
Merge branch 'main' into flexible-checksums
0marperez Dec 3, 2024
e2068e7
Trigger CI
0marperez Dec 4, 2024
08b4a37
Drop support for http body dot bytes response checksums
0marperez Dec 4, 2024
91355d1
Fix HttpChecksumRequiredTrait
0marperez Dec 4, 2024
1fcd4b2
Fix kotlin writer runtime exception
0marperez Dec 5, 2024
4edabfb
Deprecate HttpOperationContext.ChecksumAlgorithm
0marperez Dec 9, 2024
db65d74
PR feedback
0marperez Dec 11, 2024
b2e6f61
Re-add support for http body dot bytes response checksusms
0marperez Dec 13, 2024
c63cf83
Presigned URL checksums
0marperez Dec 13, 2024
b68a9f5
Merge branch 'main' of https://github.com/awslabs/smithy-kotlin into …
0marperez Dec 13, 2024
0dd7886
PR feedback checkpoint?
0marperez Dec 13, 2024
d74c949
Refactor checksum interceptors
0marperez Dec 18, 2024
1157f26
Fix composite checksums
0marperez Dec 19, 2024
d9f0659
Make it compile
0marperez Dec 19, 2024
dc3ce8b
Merge branch 'main' of https://github.com/awslabs/smithy-kotlin into …
0marperez Dec 19, 2024
3ef1c20
Use toList supported for JVM versions less than 16
0marperez Dec 19, 2024
7116221
PR feedback
0marperez Dec 23, 2024
344f118
Change JVM version
0marperez Dec 24, 2024
c689ff5
Clean up
0marperez Dec 30, 2024
9beca23
misc: revert toList/JVM compatibility changes
0marperez Jan 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions codegen/protocol-tests/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,27 @@ data class ProtocolTest(val projectionName: String, val serviceShapeId: String,
// for the configured protocols in [enabledProtocols].
val enabledProtocols = listOf(
ProtocolTest("aws-ec2-query", "aws.protocoltests.ec2#AwsEc2"),
ProtocolTest("aws-json-10", "aws.protocoltests.json10#JsonRpc10"),

// FIXME: Re-enable. This test is broken after a smithy update: https://github.com/smithy-lang/smithy/pull/2467
// ProtocolTest("aws-json-10", "aws.protocoltests.json10#JsonRpc10"),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI I know we talked about disabling some tests that are failing in Smithy 1.53.0, but these are disabling the entire test suite, which we don't want to do


ProtocolTest("aws-json-11", "aws.protocoltests.json#JsonProtocol"),
ProtocolTest("aws-restjson", "aws.protocoltests.restjson#RestJson"),
ProtocolTest("aws-restxml", "aws.protocoltests.restxml#RestXml"),

// FIXME: Re-enable. These tests are broken after a smithy update: https://github.com/smithy-lang/smithy/pull/2403
// ProtocolTest("aws-restjson", "aws.protocoltests.restjson#RestJson"),
// ProtocolTest("aws-restxml", "aws.protocoltests.restxml#RestXml"),

ProtocolTest("aws-restxml-xmlns", "aws.protocoltests.restxml.xmlns#RestXmlWithNamespace"),
ProtocolTest("aws-query", "aws.protocoltests.query#AwsQuery"),
ProtocolTest("smithy-rpcv2-cbor", "smithy.protocoltests.rpcv2Cbor#RpcV2Protocol"),

// FIXME: Re-enable. This test is broken after a smithy update: https://github.com/smithy-lang/smithy/pull/2467
// ProtocolTest("smithy-rpcv2-cbor", "smithy.protocoltests.rpcv2Cbor#RpcV2Protocol"),

// Custom hand written tests
ProtocolTest("error-correction-json", "aws.protocoltests.errorcorrection#RequiredValueJson"),
ProtocolTest("error-correction-xml", "aws.protocoltests.errorcorrection#RequiredValueXml"),
// FIXME: Re-enable. These tests were relying on a smithy bug that has since been fixed.
// https://github.com/smithy-lang/smithy/pull/2393
// ProtocolTest("error-correction-json", "aws.protocoltests.errorcorrection#RequiredValueJson"),
// ProtocolTest("error-correction-xml", "aws.protocoltests.errorcorrection#RequiredValueXml"),
)

smithyBuild {
Expand Down
10 changes: 7 additions & 3 deletions codegen/protocol-tests/model/error-correction-tests.smithy
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@ 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,
// @required
document: Document
}
structure TestOutput with [TestStruct] { innerField: Nested }

@mixin
Expand All @@ -60,7 +64,7 @@ structure TestStruct {
@required
nestedListValue: NestedList

@required
// @required
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Were these @required removed to fix some protocol tests? I'd rather leave them disabled than change our test models

Copy link
Contributor Author

@0marperez 0marperez Dec 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The required trait isn't supposed to be there, a smithy validator will throw an error if it is, even if I disable the protocol test

nested: Nested

@required
Expand Down Expand Up @@ -91,7 +95,7 @@ union MyUnion {
}

structure Nested {
@required
// @required
a: String
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -338,21 +337,14 @@ open class HttpProtocolClientGenerator(

/**
* 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.
* The Md5 middleware will only be installed if the operation requires a checksum.
*/
Copy link
Contributor

@lauzadis lauzadis Dec 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Why is this installed for every operation now without consideration of flexible checksums? If we enable CRC32 by default, doesn't this also calculate the MD5 unnecessarily?

private fun OperationShape.renderIsMd5ChecksumRequired(writer: KotlinWriter) {
val httpChecksumTrait = getTrait<HttpChecksumTrait>()

// the checksum requirement can be modeled in either HttpChecksumTrait's `requestChecksumRequired` or the HttpChecksumRequired trait
if (!hasTrait<HttpChecksumRequiredTrait>() && httpChecksumTrait == null) {
return
}

if (hasTrait<HttpChecksumRequiredTrait>() || httpChecksumTrait?.isRequestChecksumRequired == true) {
if (hasTrait<HttpChecksumRequiredTrait>()) {
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)
writer.write("true")
}
}
}
Expand Down
7 changes: 6 additions & 1 deletion runtime/protocol/http-client/api/http-client.api
Original file line number Diff line number Diff line change
Expand Up @@ -332,15 +332,20 @@ public final class aws/smithy/kotlin/runtime/http/interceptors/DiscoveredEndpoin
}

public final class aws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsRequestInterceptor : aws/smithy/kotlin/runtime/http/interceptors/AbstractChecksumInterceptor {
public fun <init> ()V
public fun <init> (Lkotlin/jvm/functions/Function1;)V
public synthetic fun <init> (Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (ZLaws/smithy/kotlin/runtime/client/config/HttpChecksumConfigOption;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 static final field Companion Laws/smithy/kotlin/runtime/http/interceptors/FlexibleChecksumsResponseInterceptor$Companion;
public fun <init> (ZLaws/smithy/kotlin/runtime/client/config/HttpChecksumConfigOption;)V
public fun <init> (Lkotlin/jvm/functions/Function1;)V
public fun <init> (ZLaws/smithy/kotlin/runtime/client/config/HttpChecksumConfigOption;Z)V
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import kotlinx.coroutines.job
import kotlin.coroutines.coroutineContext

/**
* Calculates a request's checksum.
* Handles request checksums.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clarification: "Handles request checksums for operations with the [HttpChecksumTrait] applied"

*
* 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.
Expand All @@ -36,65 +36,85 @@ import kotlin.coroutines.coroutineContext
* - If no checksum is configured for the request then use the default checksum algorithm to calculate a checksum.
*
* If the request will be streamed:
* - The checksum calculation is done asynchronously using a hashing & completing body.
* - 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 synchronously
* - 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 userSelectedChecksumAlgorithm The checksum algorithm that the user selected for the request, may be null.
* @param requestChecksumAlgorithm The checksum algorithm that the user selected for the request, may be null.
*/
@InternalApi
public class FlexibleChecksumsRequestInterceptor(
public class FlexibleChecksumsRequestInterceptor<I>(
requestChecksumRequired: Boolean,
requestChecksumCalculation: HttpChecksumConfigOption?,
userSelectedChecksumAlgorithm: String?,
requestChecksumAlgorithm: String?,
) : AbstractChecksumInterceptor() {
private val forcedToCalculateChecksum = requestChecksumRequired || requestChecksumCalculation == HttpChecksumConfigOption.WHEN_SUPPORTED
private val checksumHeader = StringBuilder("x-amz-checksum-")
private val defaultChecksumAlgorithm = lazy { Crc32() }
private val defaultChecksumAlgorithmHeaderPostfix = "crc32"

private val checksumAlgorithm = userSelectedChecksumAlgorithm?.let {
val hashFunction = userSelectedChecksumAlgorithm.toHashFunction()
// FIXME: Remove in next minor version bump
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add these to our internal ticket SDK-KT-385

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, SDK-KT-385 seems to be more for SDK changes. I can create a ticket for the smith kotlin version bump since it seems like we'll do a minor version bump for the SDK soon and one for smithy kotlin later at a different time

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure we will bump both at the same time

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, in that case I'll just fix the FIXMEs since this should go into a v1.4 branch. I have to update the PR.

@Deprecated("Old constructor is no longer used but it's kept for backwards compatibility")
public constructor() : this(
false,
HttpChecksumConfigOption.WHEN_REQUIRED,
null,
)

// FIXME: Remove in next minor version bump
@Deprecated("Old constructor is no longer used but it's kept for backwards compatibility")
public constructor(
checksumAlgorithmNameInitializer: ((I) -> String?)? = null,
) : this(
false,
HttpChecksumConfigOption.WHEN_REQUIRED,
null,
)

private val checksumHeader = buildString {
append("x-amz-checksum-")
append(requestChecksumAlgorithm?.lowercase() ?: "crc32")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: "crc32" could be lifted to a const val SDK_DEFAULT_CHECKSUM_ALGORITHM for easier understanding

}
private val checksumAlgorithm = requestChecksumAlgorithm?.let {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Missing vertical whitespace

val hashFunction = requestChecksumAlgorithm.toHashFunction()
if (hashFunction == null || !hashFunction.isSupported) {
throw ClientException("Checksum algorithm '$userSelectedChecksumAlgorithm' is not supported for flexible checksums")
throw ClientException("Checksum algorithm '$requestChecksumAlgorithm' is not supported for flexible checksums")
}
checksumHeader.append(userSelectedChecksumAlgorithm.lowercase())
hashFunction
} ?: if (forcedToCalculateChecksum) {
checksumHeader.append(defaultChecksumAlgorithmHeaderPostfix)
defaultChecksumAlgorithm.value
} ?: if (requestChecksumRequired || requestChecksumCalculation == HttpChecksumConfigOption.WHEN_SUPPORTED) {
Crc32()
} else {
null
}

// TODO: Remove in next minor version bump
@Deprecated("readAfterSerialization is no longer used but can't be removed due to backwards incompatibility")
override fun readAfterSerialization(context: ProtocolRequestInterceptorContext<Any, HttpRequest>) { }

override suspend fun modifyBeforeSigning(context: ProtocolRequestInterceptorContext<Any, HttpRequest>): HttpRequest {
val logger = coroutineContext.logger<FlexibleChecksumsRequestInterceptor>()
val logger = coroutineContext.logger<FlexibleChecksumsRequestInterceptor<I>>()

userProviderChecksumHeader(context.protocolRequest, logger)?.let {
logger.debug { "User supplied a checksum via header, skipping checksum calculation" }
context.protocolRequest.userProvidedChecksumHeader(logger)?.let {
logger.debug { "Checksum was supplied via header, skipping checksum calculation" }

val request = context.protocolRequest.toBuilder()
request.headers.removeAllChecksumHeadersExcept(it)
return request.build()
}

if (checksumAlgorithm == null) {
logger.debug { "User didn't select a checksum algorithm and checksum calculation isn't required, skipping checksum calculation" }
logger.debug { "A checksum algorithm isn't selected and checksum calculation isn't required, skipping checksum calculation" }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could also fall into this case when a user does select a checksum algorithm but they also configure WHEN_REQUIRED.

I think a better log would be:
"A checksum algorithm isn't selected or checksum calculation isn't required, skipping checksum calculation"

return context.protocolRequest
}

logger.debug { "Calculating checksum using '$checksumAlgorithm'" }

val request = context.protocolRequest.toBuilder()

if (request.body.isEligibleForAwsChunkedStreaming) {
logger.debug { "Calculating checksum during transmission using '$checksumAlgorithm'" }

val deferredChecksum = CompletableDeferred<String>(context.executionContext.coroutineContext.job)

request.body = request.body
Expand All @@ -106,26 +126,28 @@ public class FlexibleChecksumsRequestInterceptor(
deferredChecksum,
)

request.headers.append("x-amz-trailer", checksumHeader.toString())
request.trailingHeaders.append(checksumHeader.toString(), deferredChecksum)
request.headers.append("x-amz-trailer", checksumHeader)
request.trailingHeaders.append(checksumHeader, deferredChecksum)
} else {
logger.debug { "Calculating checksum before transmission using '$checksumAlgorithm'" }

checksumAlgorithm.update(
request.body.readAll() ?: byteArrayOf(),
)
request.headers[checksumHeader.toString()] = checksumAlgorithm.digest().encodeBase64String()
request.headers[checksumHeader] = checksumAlgorithm.digest().encodeBase64String()
}

context.executionContext.emitBusinessMetric(checksumAlgorithm.toBusinessMetric())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: I see here we're emitting the metric for the specific algorithm (e.g., FLEXIBLE_CHECKSUMS_REQ_CRC32C). Where are we emitting the metric for request/response enablement mode (e.g., FLEXIBLE_CHECKSUMS_REQ_WHEN_REQUIRED)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're emitting those metrics in the SDK side. Here and here.

request.headers.removeAllChecksumHeadersExcept(checksumHeader.toString())
request.headers.removeAllChecksumHeadersExcept(checksumHeader)

return request.build()
}

override suspend fun calculateChecksum(context: ProtocolRequestInterceptorContext<Any, HttpRequest>): String? {
val req = context.protocolRequest.toBuilder()

if (checksumAlgorithm == null) return null

val req = context.protocolRequest.toBuilder()

return when {
req.body.contentLength == null && !req.body.isOneShot -> {
val channel = req.body.toSdkByteReadChannel()!!
Expand All @@ -145,8 +167,8 @@ public class FlexibleChecksumsRequestInterceptor(
): HttpRequest {
val req = context.protocolRequest.toBuilder()

if (!req.headers.contains(checksumHeader.toString())) {
req.header(checksumHeader.toString(), checksum)
if (!req.headers.contains(checksumHeader)) {
req.header(checksumHeader, checksum)
}

return req.build()
Expand Down Expand Up @@ -234,15 +256,15 @@ public class FlexibleChecksumsRequestInterceptor(
/**
* 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 valid checksum algorithm.
* MD5 is not considered a supported checksum algorithm.
*/
private fun userProviderChecksumHeader(request: HttpRequest, logger: Logger): String? {
request.headers.entries().forEach { header ->
private fun HttpRequest.userProvidedChecksumHeader(logger: Logger): String? {
this.headers.entries().forEach { header ->
val headerName = header.key.lowercase()
if (headerName.startsWith("x-amz-checksum-")) {
if (headerName.endsWith("md5")) {
if (headerName == "x-amz-checksum-md5") {
logger.debug {
"User provided md5 request checksum via headers, md5 is not a valid algorithm, ignoring header"
"MD5 checksum was supplied via header, MD5 is not a supported algorithm, ignoring header"
}
} else {
return headerName
Expand Down
Loading
Loading