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: account id routing #1111

Merged
merged 23 commits into from
Nov 17, 2023
Merged

feat: account id routing #1111

merged 23 commits into from
Nov 17, 2023

Conversation

aajtodd
Copy link
Contributor

@aajtodd aajtodd commented Nov 15, 2023

Issue #

upstream: smithy-lang/smithy-kotlin#998

Description of changes

  • feat: Adds support for account ID endpoint builtin
  • feat: Adds an Arn type + parser. We discussed as a team and decided to leave this internal for now. As such I've moved it to aws-config artifact. If we ever choose to make it public and implement AWS ARN Parser #215 we should move it back to aws-core.
  • refactor: Refactored the finalizeConfig section writers. These all need to append rather than overwrite each other. I also re-ordered the integrations registration to something with more thought given to it (core integrations first, service specific ones grouped and after core).

Testing

This isn't easy to test since there are no services making use of this builtin yet. I added the parameter to the S3 endpoint rules and inspected the generated code is correct and compiles.

diff --git a/codegen/sdk/aws-models/s3.json b/codegen/sdk/aws-models/s3.json
index f119125d..16fd48ee 100644
--- a/codegen/sdk/aws-models/s3.json
+++ b/codegen/sdk/aws-models/s3.json
@@ -25,7 +25,15 @@
             {
                 "id": "Service",
                 "namespace": "*"
-            }
+            },
+          {
+            "id": "RuleSetParameter.TestCase.Unused",
+            "namespace": "*"
+          },
+          {
+            "id": "RuleSetAwsBuiltIn.AWS::Auth::AccountId",
+            "namespace":  "*"
+          }
         ]
     },
     "shapes": {
@@ -573,6 +581,12 @@
                             "documentation": "The AWS region used to dispatch the request.",
                             "type": "String"
                         },
+                      "AccountId": {
+                        "builtIn": "AWS::Auth::AccountId",
+                        "required": false,
+                        "documentation": "The AWS account ID.",
+                        "type": "String"
+                      },
                         "UseFIPS": {
                             "builtIn": "AWS::UseFIPS",
                             "required": true,

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

@aajtodd aajtodd requested a review from a team as a code owner November 15, 2023 17:24
Copy link

A new generated diff is ready to view.

Copy link

A new generated diff is ready to view.

@@ -121,6 +121,7 @@ internal const val WEB_IDENTITY_TOKEN_FILE = "web_identity_token_file"
internal const val AWS_ACCESS_KEY_ID = "aws_access_key_id"
internal const val AWS_SECRET_ACCESS_KEY = "aws_secret_access_key"
internal const val AWS_SESSION_TOKEN = "aws_session_token"
internal const val AWS_ACCOUNT_ID = "aws_account_id"
Copy link
Member

Choose a reason for hiding this comment

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

Is this standardized somewhere? I've never seen it before

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No but it's in the SEP, I too was wondering about this.

Copy link
Member

Choose a reason for hiding this comment

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

what happened here with the sorting?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I mentioned in the PR description. The order matters when we have multiple section writer bindings for the same section (though we can control this more explicitly with the order parameter in KotlinIntegration). I noticed there wasn't much thought given to the order though in terms of grouping customizations and what not so I re-arranged it.

{
"id": "08768631-1402-4492-96da-c7f246a2eec5",
"type": "feature",
"description": "Enable account ID based endpoint routing for services that use it"
Copy link
Member

Choose a reason for hiding this comment

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

nit: "use it" -> "support it"

Comment on lines +80 to +97
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Arn) return false
if (partition != other.partition) return false
if (service != other.service) return false
if (region != other.region) return false
if (accountId != other.accountId) return false
return resource == other.resource
}

override fun hashCode(): Int {
var result = partition.hashCode()
result = 31 * result + service.hashCode()
result = 31 * result + (region?.hashCode() ?: 0)
result = 31 * result + (accountId?.hashCode() ?: 0)
result = 31 * result + resource.hashCode()
return result
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: If this is an internal class, it could be a data class to avoid writing/maintaining these.

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 could be, I didn't refactor this when moving from public to internal though and didn't see a reason to.

Comment on lines +110 to +116
@PublishedApi
internal fun build(): Arn {
require(!partition.isNullOrBlank()) { "ARN partition must not be null or blank" }
require(!service.isNullOrBlank()) { "ARN service must not be null or blank" }
requireNotNull(resource) { "ARN resource must not be null" }
return Arn(this)
}
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 validate some of the fields in the builder and the other fields in the Arn class itself?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

because it has a constructor that can be used independent of the builder. The builder itself has nullable fields but the constructor doesn't. We only validate the things the builder needs to be responsible for here and elsewhere.

Comment on lines +110 to +111
@PublishedApi
internal fun build(): Arn {
Copy link
Contributor

Choose a reason for hiding this comment

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

Question: Does @PublishedApi have any effect on members of inner classes of internal classes?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

N, this was originally all public so I left it as is.

return result
}

public class Builder {
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 public?

Copy link
Contributor Author

@aajtodd aajtodd Nov 15, 2023

Choose a reason for hiding this comment

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

It was originally public. My understanding is that any client who sees the declaring class sees its public members. Because the outer class visibility is internal though this is only visible wherever the outer class is. The binary diff tooling seems to agree.

Comment on lines 129 to 133
public fun fromValue(value: String): ArnResourceTypeSeparator = when (value) {
"/" -> SLASH
":" -> COLON
else -> error("unknown ARN resource type separator `$value`, expected one of ${values().map { it.separator }}")
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Style:

public fun fromValue(value: String): ArnResourceTypeSeparator =
    checkNotNull(entries.find { it.separator = value }) {
        "unknown ARN resource type separator `$value`, expected one of ${entries.map { it.separator }}"
    }

Comment on lines 12 to 16
/**
* Endpoint parameters are populated on a best effort basis. This is the default when not
* explicitly set.
*/
PREFERRED,
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Defaults are at the usage site (e.g., AccountIdEndpointModeProp) not at the declaration site. The sentence about default probably belongs in other KDocs.

Comment on lines 76 to 81
tests.forEach { test ->
val parsed = Arn.parse(test.first)
assertEquals(test.second, parsed)
// test round trip
assertEquals(test.first, parsed.toString())
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Style: Consider using destructuring to make the test loop clearer:

tests.forEach { (originalString, expected) ->
    val parsed = Arn.parse(originalString)
    assertEquals(expected, parsed)
    // test round trip
    assertEquals(originalString, parsed.toString())
}

Comment on lines 46 to 65
private fun ecsResponse(accountId: String? = null): HttpResponse {
val kvp = buildMap {
put("Code", "Success")
put("LastUpdated", "2021-09-17T20to57to08Z")
put("Type", "AWS-HMAC")
put("AccessKeyId", "AKID")
put("SecretAccessKey", "test-secret")
put("Token", "test-token")
put("Expiration", expectedExpiration.format(TimestampFormat.ISO_8601))
if (accountId != null) {
put("AccountId", accountId)
}
}
""".encodeToByteArray()

val payload = kvp.entries.joinToString(prefix = "{", postfix = "}", separator = ",") {
"\"${it.key}\":\"${it.value}\""
}.encodeToByteArray()

return HttpResponse(HttpStatusCode.OK, Headers.Empty, HttpBody.fromBytes(payload))
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggestion: Manually constructing a JSON string of this complexity is brittle and prone to break. Consider using kotlinx.serialization for clarity and extra safety:

val payload = buildJsonObject {
    put("Code", "Success")
    put("LastUpdated", "2021-09-17T20to57to08Z")
    put("Type", "AWS-HMAC")
    put("AccessKeyId", "AKID")
    put("SecretAccessKey", "test-secret")
    put("Token", "test-token")
    put("Expiration", expectedExpiration.format(TimestampFormat.ISO_8601))
    if (accountId != null) {
        put("AccountId", accountId)
    }
}.toString().encodeToByteArray()

private fun ecsResponse(accountId: String? = null): HttpResponse {
val kvp = buildMap {
put("Code", "Success")
put("LastUpdated", "2021-09-17T20to57to08Z")
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 is "to" used instead of ":" in this timestamp?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good question


override fun enabledForService(model: Model, settings: KotlinSettings): Boolean {
val rules = model.expectShape<ServiceShape>(settings.service).getEndpointRules()
return rules?.parameters?.find { it.isBuiltIn && it.builtIn.get() == "AWS::Auth::AccountId" } != null
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Feels like "AWS::Auth::AccountId" should be a constant somewhere.

Copy link

A new generated diff is ready to view.

Copy link

A new generated diff is ready to view.

Copy link

Kudos, SonarCloud Quality Gate passed!    Quality Gate passed

Bug A 0 Bugs
Vulnerability A 0 Vulnerabilities
Security Hotspot E 1 Security Hotspot
Code Smell A 6 Code Smells

No Coverage information No Coverage information
2.0% 2.0% Duplication

@aajtodd aajtodd merged commit 3270089 into main Nov 17, 2023
15 of 17 checks passed
@aajtodd aajtodd deleted the accountid-builtin branch November 17, 2023 19:07
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