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: endpoints2: aws middleware and integration changes #751

Merged
merged 4 commits into from
Nov 18, 2022

Conversation

lucix-aws
Copy link
Contributor

Description of changes

  • Implement aws extensions for smithy-kotlin generated endpoint provider middleware
  • Add new client configs to S3 and S3Control for endpoint builtins
  • Remove old partitions.json-based resolver generator

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

@lucix-aws lucix-aws requested a review from a team as a code owner November 12, 2022 03:21
*/
@InternalSdkApi
public fun Endpoint.authScheme(): AuthScheme.SigV4? =
attributes.getOrNull(AuthSchemesAttributeKey)?.find { it is AuthScheme.SigV4 } as AuthScheme.SigV4?
Copy link
Contributor

Choose a reason for hiding this comment

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

correctness: as? AuthScheme.SigV4

/**
* Sugar extension to pull an auth scheme out of the attribute set.
*
* FUTURE: Right now we only support sigv4. The auth scheme property can include multiple schemes, for now we only pull
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this return a list of auth schemes then?

Endpoint.authSchemes(): List<AuthScheme> = 
    attributes.getOrNull(AuthSchemesAttributeKey) ?: emptyList()

Then have a separate extensions like Endpoint.hasSigV4 or Endpoint.supportsSigv4?

e.g.

@InternalSdkApi
public fun Endpoint.supportsSigv4(): Boolean = 
    authSchemes().find { it is AuthScheme.SigV4} != null

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So, I don't think it's necessary in the current context of us only supporting sigv4. When we have sigv4a as a first-class citizen the logic to retrieve this (it's basically used for presigning and for setting request context values for request signing) would have to evolve and at that point it would need extensions like this.

""".trimIndent(),
)

val EndpointUrlProp = ClientConfigProperty {
Copy link
Contributor

Choose a reason for hiding this comment

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

question/concern: Wouldn't this just be done by overriding the endpoint provider? This seems like two different ways to do the same thing to me but maybe I'm missing something

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is part of the endpoints SEP, it basically allows you to sideload an endpoint that you want to use in the ruleset and have it validated / transformed if necessary.

That said there's nothing preventing someone from writing their own endpoint provider that just blindly resolves to a static endpoint.

@@ -51,6 +48,31 @@ class AwsServiceConfigIntegration : KotlinIntegration {
AwsRuntimeTypes.Config.Credentials.DefaultChainCredentialsProvider,
)
}

val UseFipsProp: ClientConfigProperty = ClientConfigProperty.Boolean(
Copy link
Contributor

Choose a reason for hiding this comment

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

question: It is correct to add fips/dual-stack to every generated AWS client? Or should this be predicated on their use as builtins in rules?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is part of every aws client, yes.

@@ -382,12 +403,9 @@ class PresignerGenerator : KotlinIntegration {
symbol = RuntimeTypes.Auth.Signing.AwsSigningCommon.SigningEndpointProvider
name = "endpointProvider"
documentation =
"Provides the endpoint (hostname) and signing context to make requests to. When not provided a default resolver is configured automatically. This is an advanced client option."
"Provides the endpoint (hostname) and signing context to make requests to. You MUST pass a provider will resolve to an endpoint for the desired request."
Copy link
Contributor

Choose a reason for hiding this comment

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

fix: doc comment You MUST pass a provider that will resolve an endpoint ...

) {
builtinParams.forEach {
when (it.builtIn.get()) {
"AWS::Region" -> renderBasicConfigBinding(writer, it, "region")
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Can we use the config property names here rather than hard coding the strings? Or use shared constants?

writer.write("#L = config.endpointUrl?.toString()", EndpointParametersGenerator.getParamKotlinName(it))

// as a newer SDK we do NOT support these values, they are always false
"AWS::S3::UseGlobalEndpoint", "AWS::STS::UseGlobalEndpoint" ->
Copy link
Contributor

Choose a reason for hiding this comment

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

Question/Correctness: Should we even render this for the Kotlin SDK? This seems like a legacy concern that we could filter or make internal?

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 have to force them false here if we definitely don't want to support this route in the SDK, since the ruleset could default them to true.

Copy link
Contributor

Choose a reason for hiding this comment

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

What I meant was should we even be surfacing this at the config level? I guess it's fine, it's just legacy cruft that doesn't really apply to us though.

@@ -59,4 +57,10 @@ internal fun <T : HttpMessageTestCase> HttpProtocolUnitTestGenerator<T>.renderCo

// specify a default region
writer.write("region = \"us-east-1\"")
writer.write(
"endpointResolver = #T { #T(#S) }",
Copy link
Contributor

Choose a reason for hiding this comment

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

fix: Isn't this the wrong config property name? (FWIW I still sort of think we should name the generated type/field EndpointResolver but I believe it's endpointProvider in generated config right now)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It was incorrectly different based on whether or not the service had a ruleset, that's getting fixed in this next iteration (it'll all be the modern endpointProvider).

*/
fun renderBindAwsBuiltins(ctx: ProtocolGenerator.GenerationContext, writer: KotlinWriter, builtinParams: List<Parameter>) {
writer.withBlock(
"public fun #T.Builder.bindAwsBuiltins(config: #T.Config) {",
Copy link
Contributor

Choose a reason for hiding this comment

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

fix: internal

import software.amazon.smithy.model.shapes.OperationShape
import software.amazon.smithy.rulesengine.language.EndpointRuleSet
import software.amazon.smithy.rulesengine.traits.EndpointRuleSetTrait

/**
* HTTP client interceptor that resolves service endpoints for a single service
*/
class ResolveAwsEndpointMiddleware(private val ctx: ProtocolGenerator.GenerationContext) : ProtocolMiddleware {
Copy link
Contributor

Choose a reason for hiding this comment

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

comment: It would be really nice if we didn't even need this custom middleware anymore and could just use the one from smithy-kotlin.

Copy link
Contributor

Choose a reason for hiding this comment

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

In fact I don't think we do need this.

We seem to have punted on any kind of SPI or hook to customize bindings (which is fine) but we never really needed one anyway.

We have extremely fine grained ways to customize how parts of the code are generated via SectionWriter which integrations can register.

An example of this in current use is the service client declares a section (in this case with default code being generated in others it's empty and serves as a hook to append, example). Example of that section being overridden in AWS SDK.


We don't have to go this route but looking at EndpointParameterBindingGenerator it actually seems like a reasonable use case to declare sections for rendering the various binding types (which will guarantee the order is correct). Integrations can wholesale override or append. Food for thought.

"smithy.api#documentation": "<p>The end time of the range for the request, expressed as the number of milliseconds\n after Jan 1, 1970 00:00:00 UTC. Events with a timestamp later than this time are not\n exported.</p>",
"smithy.api#documentation": "<p>The end time of the range for the request, expressed as the number of milliseconds\n after Jan 1, 1970 00:00:00 UTC. Events with a timestamp later than this time are not\n exported.</p>\n <p>You must specify a time that is not earlier than when this log group was created.</p>",
Copy link
Contributor

Choose a reason for hiding this comment

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

Question: Why are there service model changes in this PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was just an incidental model update to make sure I wasn't ever breaking anything provider-wise with the newest version of the rules I could get. We can respect the in-main versions when we merge, or take the update, I don't have a real preference.

@@ -382,12 +403,9 @@ class PresignerGenerator : KotlinIntegration {
symbol = RuntimeTypes.Auth.Signing.AwsSigningCommon.SigningEndpointProvider
name = "endpointProvider"
documentation =
"Provides the endpoint (hostname) and signing context to make requests to. When not provided a default resolver is configured automatically. This is an advanced client option."
"Provides the endpoint (hostname) and signing context to make requests to. You MUST pass a provider will resolve to an endpoint for the desired request."
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: "You MUST pass a provider that will resolve..."?

Also, we don't usually scream "MUST" in our KDocs. That's appropriate for specifications but documentation would generally say something like "this is a required parameter".

Comment on lines 30 to 50
override fun render(ctx: ProtocolGenerator.GenerationContext, op: OperationShape, writer: KotlinWriter) {
val resolverFeatureSymbol = buildSymbol {
name = "ResolveAwsEndpoint"
namespace(AwsKotlinDependency.AWS_HTTP, subpackage = "middleware")
if (!ctx.service.hasTrait<EndpointRuleSetTrait>()) {
writeDefault(writer)
return
}

val rules = EndpointRuleSet.fromNode(ctx.service.expectTrait<EndpointRuleSetTrait>().ruleSet)
val middlewareSymbol = ResolveEndpointMiddlewareGenerator.getSymbol(ctx.settings)
val (inputSymbol, outputSymbol) = OperationIndex.of(ctx.model).getOperationInputOutputSymbols(op, ctx.symbolProvider)

writer.withBlock(
"op.install(#T<#T, #T>(config.endpointProvider) { input ->",
"})",
middlewareSymbol,
inputSymbol,
outputSymbol,
) {
write("#T(config)", bindAwsBuiltinsSymbol(ctx.settings))
EndpointParameterBindingGenerator(ctx.model, ctx.service, writer, op, rules, "input.").render()
}
writer.addImport(resolverFeatureSymbol)
writer.write("op.install(#T(ServiceId, config.endpointResolver))", resolverFeatureSymbol)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Question: Why should this be AWS-specific going forward? The Smithy spec now includes endpoint rules and in theory any custom service could use them, right?

Copy link
Contributor Author

@lucix-aws lucix-aws Nov 15, 2022

Choose a reason for hiding this comment

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

It isn't, there's some minor code duplication to generate the default provider / tests in both this and the base ProtocolGenerator - that's a duplication we chose to take on in the provider PR based on how we expose the hook to generate endpoints code on ProtocolGenerator.

I think I misread your question. This isn't AWS-specific in the sense that there IS a base smithy version (in the other PR), this is just the AWS version that adds the builtin bindings.

Copy link
Contributor

@aajtodd aajtodd left a comment

Choose a reason for hiding this comment

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

Couple questions (and a few unanswered from previous review). Overall looks really good, think we're close

Comment on lines 45 to 46
public val Endpoint.authScheme: AuthScheme.SigV4? get() =
attributes.getOrNull(AuthSchemesAttributeKey)?.find { it is AuthScheme.SigV4 } as? AuthScheme.SigV4
Copy link
Contributor

Choose a reason for hiding this comment

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

Style: I suggest moving the get() = to the next line with the getter body (ref: Kotlin coding conventions).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Surprised that ktlintFormat didn't catch this one tbh

import software.amazon.smithy.model.shapes.OperationShape
import software.amazon.smithy.rulesengine.language.EndpointRuleSet
import software.amazon.smithy.rulesengine.traits.EndpointRuleSetTrait

/**
* HTTP client interceptor that resolves service endpoints for a single service
*/
class ResolveAwsEndpointMiddleware(private val ctx: ProtocolGenerator.GenerationContext) : ProtocolMiddleware {
Copy link
Contributor

Choose a reason for hiding this comment

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

In fact I don't think we do need this.

We seem to have punted on any kind of SPI or hook to customize bindings (which is fine) but we never really needed one anyway.

We have extremely fine grained ways to customize how parts of the code are generated via SectionWriter which integrations can register.

An example of this in current use is the service client declares a section (in this case with default code being generated in others it's empty and serves as a hook to append, example). Example of that section being overridden in AWS SDK.


We don't have to go this route but looking at EndpointParameterBindingGenerator it actually seems like a reasonable use case to declare sections for rendering the various binding types (which will guarantee the order is correct). Integrations can wholesale override or append. Food for thought.

@sonarqubecloud
Copy link

Kudos, SonarCloud Quality Gate passed!    Quality Gate passed

Bug A 0 Bugs
Vulnerability A 0 Vulnerabilities
Security Hotspot A 0 Security Hotspots
Code Smell A 1 Code Smell

No Coverage information No Coverage information
0.0% 0.0% Duplication

@lucix-aws lucix-aws merged commit 96a835d into feat-endpoints2 Nov 18, 2022
@aajtodd aajtodd deleted the feat-endpoints2-middleware branch November 29, 2022 14:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants