diff --git a/.github/renovate.json b/.github/renovate.json index ed525891b5..09c2a59831 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -12,7 +12,15 @@ "packageRules": [ { "matchPackagePatterns": ["actions.*"], - "dependencyDashboardApproval": true + "dependencyDashboardApproval": true, + "matchUpdateTypes": ["patch"], + "matchCurrentVersion": "!/^0/", + "automerge": true + }, + { + "matchUpdateTypes": ["patch"], + "matchCurrentVersion": "!/^0/", + "automerge": true } ] } diff --git a/.github/workflows/central-sync.yml b/.github/workflows/central-sync.yml index dd1f3514a0..23b80ba2e4 100644 --- a/.github/workflows/central-sync.yml +++ b/.github/workflows/central-sync.yml @@ -18,7 +18,7 @@ jobs: uses: actions/checkout@v4 with: ref: v${{ github.event.inputs.release_version }} - - uses: gradle/wrapper-validation-action@v2 + - uses: gradle/wrapper-validation-action@v3 - name: Set up JDK uses: actions/setup-java@v4 with: diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index cffd16db98..571e79a24a 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -45,14 +45,14 @@ jobs: fetch-depth: 0 - name: "🔧 Setup GraalVM CE" - uses: graalvm/setup-graalvm@v1.2.1 + uses: graalvm/setup-graalvm@v1.2.6 with: distribution: 'graalvm' java-version: ${{ matrix.java }} github-token: ${{ secrets.GITHUB_TOKEN }} - name: "🔧 Setup Gradle" - uses: gradle/gradle-build-action@v3.1.0 + uses: gradle/gradle-build-action@v3.5.0 - name: "❓ Optional setup step" run: | @@ -70,7 +70,7 @@ jobs: - name: "📊 Publish Test Report" if: always() - uses: mikepenz/action-junit-report@v4 + uses: mikepenz/action-junit-report@v5 with: check_name: Java CI / Test Report (${{ matrix.java }}) report_paths: '**/build/test-results/test/TEST-*.xml' @@ -78,7 +78,7 @@ jobs: - name: "📜 Upload binary compatibility check results" if: matrix.java == '17' - uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 + uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 with: name: binary-compatibility-reports path: "**/build/reports/binary-compatibility-*.html" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3a4bd1cb3c..d760186c8c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,7 @@ jobs: uses: actions/checkout@v4 with: token: ${{ secrets.GH_TOKEN }} - - uses: gradle/wrapper-validation-action@v2 + - uses: gradle/wrapper-validation-action@v3 - name: Set up JDK uses: actions/setup-java@v4 with: @@ -66,13 +66,13 @@ jobs: # Store the hash in a file, which is uploaded as a workflow artifact. sha256sum $ARTIFACTS | base64 -w0 > artifacts-sha256 - name: Upload build artifacts - uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 + uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 with: name: gradle-build-outputs path: build/repo/${{ steps.publish.outputs.group }}/*/${{ steps.publish.outputs.version }}/* retention-days: 5 - name: Upload artifacts-sha256 - uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 + uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 with: name: artifacts-sha256 path: artifacts-sha256 @@ -115,7 +115,7 @@ jobs: artifacts-sha256: ${{ steps.set-hash.outputs.artifacts-sha256 }} steps: - name: Download artifacts-sha256 - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: name: artifacts-sha256 # The SLSA provenance generator expects the hash digest of artifacts to be passed as a job @@ -134,7 +134,7 @@ jobs: actions: read # To read the workflow path. id-token: write # To sign the provenance. contents: write # To add assets to a release. - uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.10.0 + uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.0.0 with: base64-subjects: "${{ needs.provenance-subject.outputs.artifacts-sha256 }}" upload-assets: true # Upload to a new release. @@ -146,11 +146,9 @@ jobs: if: startsWith(github.ref, 'refs/tags/') steps: - name: Checkout repository - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Download artifacts - # Important: update actions/download-artifact to v4 only when generator_generic_slsa3.yml is also compatible. - # See https://github.com/slsa-framework/slsa-github-generator/issues/3068 - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: name: gradle-build-outputs path: build/repo @@ -162,6 +160,6 @@ jobs: - name: Upload assets # Upload the artifacts to the existing release. Note that the SLSA provenance will # attest to each artifact file and not the aggregated ZIP file. - uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v0.1.15 + uses: softprops/action-gh-release@7b4da11513bf3f43f9999e90eabced41ab8bb048 # v2.2.0 with: files: artifacts.zip diff --git a/aws-lambda-events-serde/src/main/java/io/micronaut/aws/lambda/events/serde/S3ObjectEntitySerde.java b/aws-lambda-events-serde/src/main/java/io/micronaut/aws/lambda/events/serde/S3ObjectEntitySerde.java index 9c98890ca7..7f3269f381 100644 --- a/aws-lambda-events-serde/src/main/java/io/micronaut/aws/lambda/events/serde/S3ObjectEntitySerde.java +++ b/aws-lambda-events-serde/src/main/java/io/micronaut/aws/lambda/events/serde/S3ObjectEntitySerde.java @@ -32,7 +32,7 @@ import java.io.IOException; /** - * This seems to be necessary because Serde was not picking the appropriate constructor {@link com.amazonaws.services.lambda.runtime.events.models.s3.S3EventNotification.S3ObjectEntity(String, Long, String, String, String)}. + * This seems to be necessary because Serde was not picking the appropriate constructor {@code S3EventNotification.S3ObjectEntity(String, Long, String, String, String)}. */ @Internal @Singleton diff --git a/aws-sdk-v2/build.gradle.kts b/aws-sdk-v2/build.gradle.kts index 7ea6a13001..aacc958d98 100644 --- a/aws-sdk-v2/build.gradle.kts +++ b/aws-sdk-v2/build.gradle.kts @@ -24,6 +24,7 @@ dependencies { compileOnly(libs.awssdk.secretsmanager) compileOnly(libs.awssdk.servicediscovery) compileOnly(libs.awssdk.cloudwatchlogs) + compileOnly(libs.awssdk.lambda) // Tests testAnnotationProcessor(mn.micronaut.inject.java) @@ -41,6 +42,7 @@ dependencies { testImplementation(libs.awssdk.sqs) testImplementation(libs.awssdk.ssm) testImplementation(libs.awssdk.rekognition) + testImplementation(libs.awssdk.lambda) testRuntimeOnly(libs.jcl.over.slf4j) testRuntimeOnly(mn.snakeyaml) diff --git a/aws-sdk-v2/src/main/java/io/micronaut/aws/sdk/v2/service/lambda/LambdaClientFactory.java b/aws-sdk-v2/src/main/java/io/micronaut/aws/sdk/v2/service/lambda/LambdaClientFactory.java new file mode 100644 index 0000000000..864ccce541 --- /dev/null +++ b/aws-sdk-v2/src/main/java/io/micronaut/aws/sdk/v2/service/lambda/LambdaClientFactory.java @@ -0,0 +1,94 @@ +/* + * Copyright 2017-2024 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.aws.sdk.v2.service.lambda; + +import io.micronaut.aws.sdk.v2.service.AWSServiceConfiguration; +import io.micronaut.aws.sdk.v2.service.AwsClientFactory; +import io.micronaut.aws.ua.UserAgentProvider; +import io.micronaut.context.annotation.Bean; +import io.micronaut.context.annotation.Factory; +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Nullable; +import jakarta.inject.Named; +import jakarta.inject.Singleton; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProviderChain; +import software.amazon.awssdk.http.SdkHttpClient; +import software.amazon.awssdk.http.async.SdkAsyncHttpClient; +import software.amazon.awssdk.regions.providers.AwsRegionProviderChain; +import software.amazon.awssdk.services.lambda.LambdaAsyncClient; +import software.amazon.awssdk.services.lambda.LambdaAsyncClientBuilder; +import software.amazon.awssdk.services.lambda.LambdaClient; +import software.amazon.awssdk.services.lambda.LambdaClientBuilder; + +/** + * Factory that creates {@link LambdaClient} and {@link LambdaAsyncClient}. + * @since 4.7.0 + */ +@Factory +class LambdaClientFactory extends AwsClientFactory { + /** + * Constructor. + * + * @param credentialsProvider The credentials provider + * @param regionProvider The region provider + * @param userAgentProvider User-Agent Provider + * @param awsServiceConfiguration AWS Service Configuration + */ + protected LambdaClientFactory(AwsCredentialsProviderChain credentialsProvider, + AwsRegionProviderChain regionProvider, + @Nullable UserAgentProvider userAgentProvider, + @Nullable @Named(LambdaClient.SERVICE_NAME) AWSServiceConfiguration awsServiceConfiguration) { + super(credentialsProvider, regionProvider, userAgentProvider, awsServiceConfiguration); + } + + @Override + protected LambdaClientBuilder createSyncBuilder() { + return LambdaClient.builder(); + } + + @Override + protected LambdaAsyncClientBuilder createAsyncBuilder() { + return LambdaAsyncClient.builder(); + } + + @Override + @Singleton + public LambdaClientBuilder syncBuilder(SdkHttpClient httpClient) { + return super.syncBuilder(httpClient); + } + + @Override + @Bean(preDestroy = "close") + @Singleton + public LambdaClient syncClient(LambdaClientBuilder builder) { + return super.syncClient(builder); + } + + @Override + @Singleton + @Requires(beans = SdkAsyncHttpClient.class) + public LambdaAsyncClientBuilder asyncBuilder(SdkAsyncHttpClient httpClient) { + return super.asyncBuilder(httpClient); + } + + @Override + @Bean(preDestroy = "close") + @Singleton + @Requires(beans = SdkAsyncHttpClient.class) + public LambdaAsyncClient asyncClient(LambdaAsyncClientBuilder builder) { + return super.asyncClient(builder); + } +} diff --git a/aws-sdk-v2/src/main/java/io/micronaut/aws/sdk/v2/service/lambda/package-info.java b/aws-sdk-v2/src/main/java/io/micronaut/aws/sdk/v2/service/lambda/package-info.java new file mode 100644 index 0000000000..5115d73a68 --- /dev/null +++ b/aws-sdk-v2/src/main/java/io/micronaut/aws/sdk/v2/service/lambda/package-info.java @@ -0,0 +1,28 @@ +/* + * Copyright 2017-2024 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Lambda client factory. + * @author Luis Duarte + * @since 4.7.0 + */ +@Requires(classes = {LambdaClient.class, LambdaAsyncClient.class}) +@Configuration +package io.micronaut.aws.sdk.v2.service.lambda; + +import io.micronaut.context.annotation.Configuration; +import io.micronaut.context.annotation.Requires; +import software.amazon.awssdk.services.lambda.LambdaAsyncClient; +import software.amazon.awssdk.services.lambda.LambdaClient; diff --git a/aws-sdk-v2/src/main/resources/META-INF/native-image/io.micronaut.aws/micronaut-aws-sdk-v2/native-image.properties b/aws-sdk-v2/src/main/resources/META-INF/native-image/io.micronaut.aws/micronaut-aws-sdk-v2/native-image.properties deleted file mode 100644 index 2e6c5ea998..0000000000 --- a/aws-sdk-v2/src/main/resources/META-INF/native-image/io.micronaut.aws/micronaut-aws-sdk-v2/native-image.properties +++ /dev/null @@ -1,17 +0,0 @@ -# -# Copyright 2017-2021 original authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -Args = --initialize-at-run-time=io.micronaut.aws.sdk.v2.service.secretsmanager.$SecretsManagerClientFactory$AsyncBuilder2$Definition,io.micronaut.aws.sdk.v2.service.servicediscovery.$ServiceDiscoveryAsyncClientFactory$AsyncBuilder2$Definition,io.micronaut.aws.sdk.v2.service.servicediscovery.$ServiceDiscoveryAsyncClientFactory$AsyncClient3$Definition,io.micronaut.aws.sdk.v2.service.servicediscovery.$ServiceDiscoveryAsyncClientFactory$Definition,io.micronaut.aws.sdk.v2.service.ses.$SesClientFactory$AsyncBuilder2$Definition,io.micronaut.aws.sdk.v2.service.servicediscovery.$ServiceDiscoveryAsyncClientFactory$SyncBuilder0$Definition,io.micronaut.aws.sdk.v2.service.servicediscovery.$ServiceDiscoveryAsyncClientFactory$SyncClient1$Definition,io.micronaut.aws.sdk.v2.service.ses.$SesClientFactory$AsyncClient3$Definition,io.micronaut.aws.sdk.v2.service.ses.$SesClientFactory$AsyncClient3$Definition,io.micronaut.aws.sdk.v2.service.ses.$SesClientFactory$SyncBuilder0$Definition,io.micronaut.aws.sdk.v2.service.ses.$SesClientFactory$SyncClient1$Definition,io.micronaut.aws.sdk.v2.service.sns.$SnsClientFactory$AsyncBuilder2$Definition,io.micronaut.aws.sdk.v2.service.sns.$SnsClientFactory$AsyncClient3$Definition,io.micronaut.aws.sdk.v2.service.sns.$SnsClientFactory$SyncClient1$Definition,io.micronaut.aws.sdk.v2.service.sqs.$SqsClientFactory$AsyncBuilder2$Definition,io.micronaut.aws.sdk.v2.service.sqs.$SqsClientFactory$SyncClient1$Definition,io.micronaut.aws.sdk.v2.service.sns.$SnsClientFactory$SyncBuilder0$Definition,io.micronaut.aws.sdk.v2.service.ssm.$SsmClientFactory$AsyncBuilder2$Definition,io.micronaut.aws.sdk.v2.service.ssm.$SsmClientFactory$AsyncClient3$Definition,io.micronaut.aws.sdk.v2.service.ssm.$SsmClientFactory$Definition,io.micronaut.aws.sdk.v2.service.sns.$SnsClientFactory$Definition,io.micronaut.aws.sdk.v2.service.dynamodb.$DynamoDbClientFactory$AsyncClient3$Definition,io.micronaut.aws.sdk.v2.service.dynamodb.$DynamoDbClientFactory$Definition,io.micronaut.aws.sdk.v2.service.secretsmanager.$SecretsManagerClientFactory$Definition,io.micronaut.aws.sdk.v2.service.secretsmanager.$SecretsManagerClientFactory$SyncBuilder0$Definition,io.micronaut.aws.sdk.v2.service.ses.$SesClientFactory$Definition,io.micronaut.aws.sdk.v2.service.sqs.$SqsClientFactory$AsyncClient3$Definition,io.micronaut.aws.sdk.v2.service.sqs.$SqsClientFactory$SyncBuilder0$Definition,io.micronaut.aws.sdk.v2.service.ssm.$SsmClientFactory$SyncBuilder0$Definition,io.micronaut.aws.sdk.v2.service.ssm.$SsmClientFactory$SyncClient1$Definition,io.micronaut.aws.sdk.v2.service.dynamodb.$DynamoDbClientFactory$AsyncBuilder2$Definition,io.micronaut.aws.sdk.v2.service.dynamodb.$DynamoDbClientFactory$SyncBuilder0$Definition,io.micronaut.aws.sdk.v2.service.dynamodb.$DynamoDbClientFactory$SyncClient1$Definition,io.micronaut.aws.sdk.v2.service.secretsmanager.$SecretsManagerClientFactory$AsyncClient3$Definition,io.micronaut.aws.sdk.v2.service.secretsmanager.$SecretsManagerClientFactory$SyncClient1$Definition,io.micronaut.aws.sdk.v2.service.sqs.$SqsClientFactory$Definition,io.micronaut.aws.sdk.v2.service.s3.$S3ClientFactory$AsyncBuilder2$Definition,io.micronaut.aws.sdk.v2.service.s3.$S3ClientFactory$AsyncClient3$Definition,io.micronaut.aws.sdk.v2.service.s3.$S3ClientFactory$Definition,io.micronaut.aws.sdk.v2.service.s3.$S3ClientFactory$SyncBuilder0$Definition,io.micronaut.aws.sdk.v2.service.s3.$S3ClientFactory$SyncClient1$Definition,io.micronaut.aws.sdk.v2.service.s3.$S3ClientFactory$AsyncBuilder2$Definition,io.micronaut.aws.sdk.v2.service.gatewaymanagement.$ApiGatewayManagementApiClientFactory$AsyncBuilder2$Definition,io.micronaut.aws.sdk.v2.service.gatewaymanagement.$ApiGatewayManagementApiClientFactory$AsyncClient3$Definition,io.micronaut.aws.sdk.v2.service.gatewaymanagement.$ApiGatewayManagementApiClientFactory$Definition,io.micronaut.aws.sdk.v2.service.gatewaymanagement.$ApiGatewayManagementApiClientFactory$SyncBuilder0$Definition,io.micronaut.aws.sdk.v2.service.gatewaymanagement.$ApiGatewayManagementApiClientFactory$SyncClient1$Definition diff --git a/aws-sdk-v2/src/test/groovy/io/micronaut/aws/sdk/v2/client/NettyClientSpec.groovy b/aws-sdk-v2/src/test/groovy/io/micronaut/aws/sdk/v2/client/NettyClientSpec.groovy index c4077d7902..06b39673e0 100644 --- a/aws-sdk-v2/src/test/groovy/io/micronaut/aws/sdk/v2/client/NettyClientSpec.groovy +++ b/aws-sdk-v2/src/test/groovy/io/micronaut/aws/sdk/v2/client/NettyClientSpec.groovy @@ -19,7 +19,7 @@ class NettyClientSpec extends ApplicationContextSpecification { then: client.configuration().maxConnections() == 123 - client.pools.proxyConfiguration == null + client.pools.proxyConfiguration } } diff --git a/aws-sdk-v2/src/test/groovy/io/micronaut/aws/sdk/v2/service/LambdaClientSpec.groovy b/aws-sdk-v2/src/test/groovy/io/micronaut/aws/sdk/v2/service/LambdaClientSpec.groovy new file mode 100644 index 0000000000..7966a9c683 --- /dev/null +++ b/aws-sdk-v2/src/test/groovy/io/micronaut/aws/sdk/v2/service/LambdaClientSpec.groovy @@ -0,0 +1,20 @@ +package io.micronaut.aws.sdk.v2.service + +import software.amazon.awssdk.services.lambda.LambdaAsyncClient +import software.amazon.awssdk.services.lambda.LambdaClient + +class LambdaClientSpec extends ServiceClientSpec { + @Override + protected String serviceName() { + return LambdaClient.SERVICE_NAME + } + + @Override + protected LambdaClient getClient() { + applicationContext.getBean(LambdaClient) + } + + protected LambdaAsyncClient getAsyncClient() { + applicationContext.getBean(LambdaAsyncClient) + } +} diff --git a/function-aws-api-proxy-test/src/main/java/io/micronaut/function/aws/proxy/test/AwsApiProxyTestServer.java b/function-aws-api-proxy-test/src/main/java/io/micronaut/function/aws/proxy/test/AwsApiProxyTestServer.java index 7861ee7c9f..ae682c222b 100644 --- a/function-aws-api-proxy-test/src/main/java/io/micronaut/function/aws/proxy/test/AwsApiProxyTestServer.java +++ b/function-aws-api-proxy-test/src/main/java/io/micronaut/function/aws/proxy/test/AwsApiProxyTestServer.java @@ -191,13 +191,7 @@ ApplicationContext getApplicationContext() { @Override public void destroy() { super.destroy(); - try { - this.lambdaHandler.close(); - } catch (IOException e) { - if (LOG.isErrorEnabled()) { - LOG.error("could not close Handler", e); - } - } + this.lambdaHandler.close(); } public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException { diff --git a/function-aws-api-proxy-test/src/main/java/io/micronaut/function/aws/proxy/test/DefaultServletToAwsProxyRequestAdapter.java b/function-aws-api-proxy-test/src/main/java/io/micronaut/function/aws/proxy/test/DefaultServletToAwsProxyRequestAdapter.java index 7776007258..b8a7744881 100644 --- a/function-aws-api-proxy-test/src/main/java/io/micronaut/function/aws/proxy/test/DefaultServletToAwsProxyRequestAdapter.java +++ b/function-aws-api-proxy-test/src/main/java/io/micronaut/function/aws/proxy/test/DefaultServletToAwsProxyRequestAdapter.java @@ -48,6 +48,7 @@ public class DefaultServletToAwsProxyRequestAdapter implements ServletToAwsProxy public APIGatewayV2HTTPEvent createAwsProxyRequest(@NonNull HttpServletRequest request) { final boolean isBase64Encoded = true; return new APIGatewayV2HTTPEvent() { + private String body; @Override public Map getHeaders() { @@ -116,18 +117,20 @@ public boolean getIsBase64Encoded() { @Override public String getBody() { - HttpMethod httpMethod = HttpMethod.parse(request.getMethod()); - if (HttpMethod.permitsRequestBody(httpMethod)) { - try (InputStream requestBody = request.getInputStream()) { - byte[] data = requestBody.readAllBytes(); - if (isBase64Encoded) { - return Base64.getEncoder().encodeToString(data); + if (body == null) { + HttpMethod httpMethod = HttpMethod.parse(request.getMethod()); + if (HttpMethod.permitsRequestBody(httpMethod)) { + try (InputStream requestBody = request.getInputStream()) { + byte[] data = requestBody.readAllBytes(); + if (isBase64Encoded) { + body = Base64.getEncoder().encodeToString(data); + } + } catch (IOException e) { + // ignore } - } catch (IOException e) { - // ignore } } - return null; + return body; } }; } diff --git a/function-aws-api-proxy/src/main/java/io/micronaut/function/aws/proxy/ApiGatewayServletRequest.java b/function-aws-api-proxy/src/main/java/io/micronaut/function/aws/proxy/ApiGatewayServletRequest.java index 4c47d9ebe5..47b6735dce 100644 --- a/function-aws-api-proxy/src/main/java/io/micronaut/function/aws/proxy/ApiGatewayServletRequest.java +++ b/function-aws-api-proxy/src/main/java/io/micronaut/function/aws/proxy/ApiGatewayServletRequest.java @@ -24,6 +24,7 @@ import io.micronaut.core.execution.ExecutionFlow; import io.micronaut.core.io.buffer.ByteBuffer; import io.micronaut.core.type.Argument; +import io.micronaut.core.util.ArrayUtils; import io.micronaut.core.util.CollectionUtils; import io.micronaut.core.util.StringUtils; import io.micronaut.core.util.SupplierUtil; @@ -34,6 +35,8 @@ import io.micronaut.http.MutableHttpHeaders; import io.micronaut.http.MutableHttpParameters; import io.micronaut.http.MutableHttpRequest; +import io.micronaut.http.ServerHttpRequest; +import io.micronaut.http.body.ByteBody; import io.micronaut.http.cookie.Cookie; import io.micronaut.http.cookie.Cookies; import io.micronaut.http.uri.UriBuilder; @@ -43,6 +46,7 @@ import io.micronaut.servlet.http.ServletHttpRequest; import io.micronaut.servlet.http.ParsedBodyHolder; import io.micronaut.servlet.http.ByteArrayByteBuffer; +import io.micronaut.servlet.http.body.AvailableByteArrayBody; import org.slf4j.Logger; import java.io.BufferedReader; @@ -71,7 +75,7 @@ */ @Internal @SuppressWarnings("java:S119") // More descriptive generics are better here -public abstract class ApiGatewayServletRequest implements MutableServletHttpRequest, ServletExchange, FullHttpRequest, ParsedBodyHolder { +public abstract class ApiGatewayServletRequest implements MutableServletHttpRequest, ServletExchange, FullHttpRequest, ParsedBodyHolder, ServerHttpRequest { private static final Set> RAW_BODY_TYPES = CollectionUtils.setOf(String.class, byte[].class, ByteBuffer.class, InputStream.class); private static final String SLASH = "/"; @@ -108,7 +112,16 @@ protected ApiGatewayServletRequest( }); } - public abstract byte[] getBodyBytes() throws IOException; + @Override + public @NonNull ByteBody byteBody() { + try { + return new AvailableByteArrayBody(getBodyBytes()); + } catch (EmptyBodyException e) { + return new AvailableByteArrayBody(ArrayUtils.EMPTY_BYTE_ARRAY); + } + } + + public abstract byte[] getBodyBytes() throws EmptyBodyException; /** * Given a path and the query params from the event, build a URI. @@ -298,12 +311,12 @@ public void setParsedBody(T body) { * @param bodySupplier HTTP Request's Body Supplier * @param base64EncodedSupplier Whether the body is Base 64 encoded * @return body bytes - * @throws IOException if the body is empty + * @throws EmptyBodyException if the body is empty */ - protected byte[] getBodyBytes(@NonNull Supplier bodySupplier, @NonNull BooleanSupplier base64EncodedSupplier) throws IOException { + protected byte[] getBodyBytes(@NonNull Supplier bodySupplier, @NonNull BooleanSupplier base64EncodedSupplier) throws EmptyBodyException { String requestBody = bodySupplier.get(); if (StringUtils.isEmpty(requestBody)) { - throw new IOException("Empty Body"); + throw new EmptyBodyException(); } return base64EncodedSupplier.getAsBoolean() ? Base64.getDecoder().decode(requestBody) : requestBody.getBytes(getCharacterEncoding()); @@ -358,4 +371,10 @@ protected MutableHttpParameters getParameters(@NonNull Supplier> singleHeaders, @NonNull Supplier>> multiValueHeaders) { return new CaseInsensitiveMutableHttpHeaders(MapCollapseUtils.collapse(multiValueHeaders.get(), singleHeaders.get()), conversionService); } + + public static final class EmptyBodyException extends IOException { + public EmptyBodyException() { + super("Empty body"); + } + } } diff --git a/function-aws-api-proxy/src/main/java/io/micronaut/function/aws/proxy/alb/ApplicationLoadBalancerServletRequest.java b/function-aws-api-proxy/src/main/java/io/micronaut/function/aws/proxy/alb/ApplicationLoadBalancerServletRequest.java index f665f9b5aa..2d16aa432f 100644 --- a/function-aws-api-proxy/src/main/java/io/micronaut/function/aws/proxy/alb/ApplicationLoadBalancerServletRequest.java +++ b/function-aws-api-proxy/src/main/java/io/micronaut/function/aws/proxy/alb/ApplicationLoadBalancerServletRequest.java @@ -28,8 +28,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; - /** * Implementation of {@link ServletHttpRequest} for Application Load Balancer events. * @@ -66,7 +64,7 @@ public ApplicationLoadBalancerServletRequest( } @Override - public byte[] getBodyBytes() throws IOException { + public byte[] getBodyBytes() throws EmptyBodyException { return getBodyBytes(requestEvent::getBody, requestEvent::getIsBase64Encoded); } diff --git a/function-aws-api-proxy/src/main/java/io/micronaut/function/aws/proxy/alb/ApplicationLoadBalancerServletResponse.java b/function-aws-api-proxy/src/main/java/io/micronaut/function/aws/proxy/alb/ApplicationLoadBalancerServletResponse.java index 2963407a4a..d1e7bbb3fc 100644 --- a/function-aws-api-proxy/src/main/java/io/micronaut/function/aws/proxy/alb/ApplicationLoadBalancerServletResponse.java +++ b/function-aws-api-proxy/src/main/java/io/micronaut/function/aws/proxy/alb/ApplicationLoadBalancerServletResponse.java @@ -18,6 +18,7 @@ import com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerResponseEvent; import io.micronaut.core.annotation.Internal; import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.util.StringUtils; import io.micronaut.function.BinaryTypeConfiguration; import io.micronaut.function.aws.proxy.AbstractServletHttpResponse; import io.micronaut.function.aws.proxy.MapCollapseUtils; @@ -49,7 +50,10 @@ public ApplicationLoadBalancerResponseEvent getNativeResponse() { nativeResponse.setBody(Base64.getMimeEncoder().encodeToString(body.toByteArray())); } else { nativeResponse.setIsBase64Encoded(false); - nativeResponse.setBody(body.toString(getCharacterEncoding())); + String bodyStr = body.toString(getCharacterEncoding()); + if (StringUtils.isNotEmpty(bodyStr)) { + nativeResponse.setBody(bodyStr); + } } return nativeResponse; } diff --git a/function-aws-api-proxy/src/main/java/io/micronaut/function/aws/proxy/payload1/ApiGatewayProxyServletRequest.java b/function-aws-api-proxy/src/main/java/io/micronaut/function/aws/proxy/payload1/ApiGatewayProxyServletRequest.java index a7e278f05e..c159ceee81 100644 --- a/function-aws-api-proxy/src/main/java/io/micronaut/function/aws/proxy/payload1/ApiGatewayProxyServletRequest.java +++ b/function-aws-api-proxy/src/main/java/io/micronaut/function/aws/proxy/payload1/ApiGatewayProxyServletRequest.java @@ -31,7 +31,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; import java.util.Base64; import java.util.List; import java.util.Map; @@ -72,10 +71,10 @@ public ApiGatewayProxyServletRequest( } @Override - public byte[] getBodyBytes() throws IOException { + public byte[] getBodyBytes() throws EmptyBodyException { String body = requestEvent.getBody(); if (StringUtils.isEmpty(body)) { - throw new IOException("Empty Body"); + throw new EmptyBodyException(); } Boolean isBase64Encoded = requestEvent.getIsBase64Encoded(); return Boolean.TRUE.equals(isBase64Encoded) ? Base64.getDecoder().decode(body) : body.getBytes(getCharacterEncoding()); diff --git a/function-aws-api-proxy/src/main/java/io/micronaut/function/aws/proxy/payload1/ApiGatewayProxyServletResponse.java b/function-aws-api-proxy/src/main/java/io/micronaut/function/aws/proxy/payload1/ApiGatewayProxyServletResponse.java index c5f991142f..07b0a1d2ef 100644 --- a/function-aws-api-proxy/src/main/java/io/micronaut/function/aws/proxy/payload1/ApiGatewayProxyServletResponse.java +++ b/function-aws-api-proxy/src/main/java/io/micronaut/function/aws/proxy/payload1/ApiGatewayProxyServletResponse.java @@ -18,6 +18,7 @@ import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; import io.micronaut.core.annotation.Internal; import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.util.StringUtils; import io.micronaut.function.BinaryTypeConfiguration; import io.micronaut.function.aws.proxy.AbstractServletHttpResponse; import io.micronaut.function.aws.proxy.MapCollapseUtils; @@ -42,7 +43,6 @@ protected ApiGatewayProxyServletResponse(ConversionService conversionService, Bi @Override public APIGatewayProxyResponseEvent getNativeResponse() { APIGatewayProxyResponseEvent apiGatewayProxyResponseEvent = new APIGatewayProxyResponseEvent() - .withBody(body.toString()) .withStatusCode(status) .withMultiValueHeaders(MapCollapseUtils.getMultiHeaders(headers)) .withHeaders(MapCollapseUtils.getSingleValueHeaders(headers)); @@ -53,8 +53,11 @@ public APIGatewayProxyResponseEvent getNativeResponse() { .withBody(Base64.getMimeEncoder().encodeToString(body.toByteArray())); } else { apiGatewayProxyResponseEvent - .withIsBase64Encoded(false) - .withBody(body.toString(getCharacterEncoding())); + .withIsBase64Encoded(false); + String bodyStr = body.toString(getCharacterEncoding()); + if (StringUtils.isNotEmpty(bodyStr)) { + apiGatewayProxyResponseEvent.withBody(bodyStr); + } } return apiGatewayProxyResponseEvent; } diff --git a/function-aws-api-proxy/src/main/java/io/micronaut/function/aws/proxy/payload2/APIGatewayV2HTTPEventServletRequest.java b/function-aws-api-proxy/src/main/java/io/micronaut/function/aws/proxy/payload2/APIGatewayV2HTTPEventServletRequest.java index 1f9c947d4f..ea5078f020 100644 --- a/function-aws-api-proxy/src/main/java/io/micronaut/function/aws/proxy/payload2/APIGatewayV2HTTPEventServletRequest.java +++ b/function-aws-api-proxy/src/main/java/io/micronaut/function/aws/proxy/payload2/APIGatewayV2HTTPEventServletRequest.java @@ -28,7 +28,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; import java.util.Collections; /** @@ -67,7 +66,7 @@ public APIGatewayV2HTTPEventServletRequest( } @Override - public byte[] getBodyBytes() throws IOException { + public byte[] getBodyBytes() throws EmptyBodyException { return getBodyBytes(requestEvent::getBody, requestEvent::getIsBase64Encoded); } diff --git a/function-aws-api-proxy/src/main/java/io/micronaut/function/aws/proxy/payload2/APIGatewayV2HTTPResponseServletResponse.java b/function-aws-api-proxy/src/main/java/io/micronaut/function/aws/proxy/payload2/APIGatewayV2HTTPResponseServletResponse.java index 94d50314a4..08370447ea 100644 --- a/function-aws-api-proxy/src/main/java/io/micronaut/function/aws/proxy/payload2/APIGatewayV2HTTPResponseServletResponse.java +++ b/function-aws-api-proxy/src/main/java/io/micronaut/function/aws/proxy/payload2/APIGatewayV2HTTPResponseServletResponse.java @@ -18,6 +18,7 @@ import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPResponse; import io.micronaut.core.annotation.Internal; import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.util.StringUtils; import io.micronaut.function.BinaryTypeConfiguration; import io.micronaut.function.aws.proxy.AbstractServletHttpResponse; import io.micronaut.function.aws.proxy.MapCollapseUtils; @@ -51,7 +52,10 @@ public APIGatewayV2HTTPResponse getNativeResponse() { .withIsBase64Encoded(true) .withBody(Base64.getMimeEncoder().encodeToString(body.toByteArray())); } else { - apiGatewayV2HTTPResponseBuilder.withBody(body.toString(getCharacterEncoding())); + String bodyStr = body.toString(getCharacterEncoding()); + if (StringUtils.isNotEmpty(bodyStr)) { + apiGatewayV2HTTPResponseBuilder.withBody(bodyStr); + } } return apiGatewayV2HTTPResponseBuilder.build(); diff --git a/function-client-aws-v2/build.gradle.kts b/function-client-aws-v2/build.gradle.kts new file mode 100644 index 0000000000..def348e4c6 --- /dev/null +++ b/function-client-aws-v2/build.gradle.kts @@ -0,0 +1,28 @@ +plugins { + id("io.micronaut.build.internal.aws-module") +} + +dependencies { + api(projects.micronautAwsSdkV2) + implementation(libs.awssdk.lambda) + implementation(mn.reactor) + api(mn.micronaut.function.client) + testAnnotationProcessor(mn.micronaut.inject.java) + testImplementation(mn.micronaut.inject.java) + testImplementation(mnSerde.micronaut.serde.api) + testImplementation(mn.micronaut.http.server.netty) + testImplementation(mn.micronaut.function.web) + testImplementation(mnGroovy.micronaut.function.groovy) + testImplementation(mnGroovy.micronaut.runtime.groovy) + testImplementation(platform(mnTestResources.boms.testcontainers)) + testImplementation(libs.testcontainers) + testImplementation(libs.testcontainers.localstack) + testImplementation(libs.testcontainers.spock) + testImplementation(libs.awssdk.iam) +} +micronautBuild { + // new module, so no binary check + binaryCompatibility { + enabled.set(false) + } +} diff --git a/function-client-aws-v2/src/main/java/io/micronaut/function/client/aws/v2/AwsInvokeRequestDefinition.java b/function-client-aws-v2/src/main/java/io/micronaut/function/client/aws/v2/AwsInvokeRequestDefinition.java new file mode 100644 index 0000000000..82b70fceb1 --- /dev/null +++ b/function-client-aws-v2/src/main/java/io/micronaut/function/client/aws/v2/AwsInvokeRequestDefinition.java @@ -0,0 +1,103 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.function.client.aws.v2; + +import io.micronaut.context.annotation.EachProperty; +import io.micronaut.context.annotation.Parameter; +import io.micronaut.function.client.FunctionDefinition; + +/** + * Builds an {@link AwsInvokeRequestDefinition} for each definition under {@code aws.lambda.functions}. + * + * @since 4.7.0 + */ +@EachProperty(AwsInvokeRequestDefinition.AWS_LAMBDA_FUNCTIONS) +public class AwsInvokeRequestDefinition implements FunctionDefinition { + /** + * Configuration prefix. + */ + public static final String AWS_LAMBDA_FUNCTIONS = "aws.lambda.functions"; + + private final String name; + + private String functionName; + + private String qualifier; + + private String clientContext; + + /** + * Constructor. + * + * @param name configured name from a property + */ + public AwsInvokeRequestDefinition(@Parameter String name) { + this.name = name; + } + + @Override + public String getName() { + return this.name; + } + + /** + * + * @return The name or ARN of the Lambda function, version, or alias. + */ + public String getFunctionName() { + return functionName; + } + + /** + * + * @param functionName The name or ARN of the Lambda function, version, or alias. + */ + public void setFunctionName(String functionName) { + this.functionName = functionName; + } + + /** + * + * @return Specify a version or alias to invoke a published version of the function. + */ + public String getQualifier() { + return qualifier; + } + + /** + * {@see software.amazon.awssdk.services.lambda.model.InvokeRequest#clientContext}. + * @return Up to 3,583 bytes of base64-encoded data about the invoking client to pass to the function in the context object. + */ + public String getClientContext() { + return clientContext; + } + + /** + * {@see software.amazon.awssdk.services.lambda.model.InvokeRequest#qualifier}. + * @param qualifier Specify a version or alias to invoke a published version of the function. + */ + public void setQualifier(String qualifier) { + this.qualifier = qualifier; + } + + /** + * + * @param clientContext Up to 3,583 bytes of base64-encoded data about the invoking client to pass to the function in the context object. + */ + public void setClientContext(String clientContext) { + this.clientContext = clientContext; + } +} diff --git a/function-client-aws-v2/src/main/java/io/micronaut/function/client/aws/v2/AwsLambdaFunctionExecutor.java b/function-client-aws-v2/src/main/java/io/micronaut/function/client/aws/v2/AwsLambdaFunctionExecutor.java new file mode 100644 index 0000000000..9c3ce5b247 --- /dev/null +++ b/function-client-aws-v2/src/main/java/io/micronaut/function/client/aws/v2/AwsLambdaFunctionExecutor.java @@ -0,0 +1,155 @@ +/* + * Copyright 2017-2020 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.function.client.aws.v2; + +import io.micronaut.context.annotation.Requires; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.async.publisher.Publishers; +import io.micronaut.core.convert.ConversionService; +import io.micronaut.core.io.buffer.ByteBufferFactory; +import io.micronaut.core.type.Argument; +import io.micronaut.function.client.FunctionDefinition; +import io.micronaut.function.client.FunctionInvoker; +import io.micronaut.function.client.FunctionInvokerChooser; +import io.micronaut.function.client.exceptions.FunctionExecutionException; +import io.micronaut.json.codec.JsonMediaTypeCodec; +import io.micronaut.scheduling.TaskExecutors; +import jakarta.inject.Named; +import jakarta.inject.Singleton; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.services.lambda.LambdaAsyncClient; +import software.amazon.awssdk.services.lambda.LambdaClient; +import software.amazon.awssdk.services.lambda.model.InvokeRequest; +import software.amazon.awssdk.services.lambda.model.InvokeResponse; + +import java.nio.ByteBuffer; +import java.util.Optional; +import java.util.concurrent.ExecutorService; + +/** + * A {@link FunctionInvoker} for invoking functions on AWS. + * + * @param input type + * @param output type + * @author graemerocher + * @since 1.0 + */ +@Requires(beans = LambdaAsyncClient.class) +@Singleton +@Internal +public class AwsLambdaFunctionExecutor implements FunctionInvoker, FunctionInvokerChooser { + + private static final int STATUS_CODE_ERROR = 300; + private final LambdaClient syncClient; + private final LambdaAsyncClient asyncClient; + private final ByteBufferFactory byteBufferFactory; + private final JsonMediaTypeCodec mediaTypeCodec; + private final ExecutorService executor; + private final ConversionService conversionService; + + /** + * Constructor. + * + * @param syncClient Lambda Sync Client + * @param asyncClient Lambda Async Client + * @param byteBufferFactory byteBufferFactory + * @param mediaTypeCodec JsonMediaTypeCodec + * @param executor blocking executor + * @param conversionService ConversionService + */ + protected AwsLambdaFunctionExecutor( + LambdaClient syncClient, + LambdaAsyncClient asyncClient, + ByteBufferFactory byteBufferFactory, + JsonMediaTypeCodec mediaTypeCodec, + @Named(TaskExecutors.BLOCKING) ExecutorService executor, + ConversionService conversionService) { + this.syncClient = syncClient; + this.asyncClient = asyncClient; + this.byteBufferFactory = byteBufferFactory; + this.mediaTypeCodec = mediaTypeCodec; + this.executor = executor; + this.conversionService = conversionService; + } + + @Override + public O invoke(FunctionDefinition definition, I input, Argument outputType) { + if (!(definition instanceof AwsInvokeRequestDefinition)) { + throw new IllegalArgumentException("Function definition must be a AWSInvokeRequestDefinition"); + } + + boolean isReactiveType = Publishers.isConvertibleToPublisher(outputType.getType()); + SdkBytes sdkBytes = encodeInput(input); + AwsInvokeRequestDefinition awsInvokeRequestDefinition = (AwsInvokeRequestDefinition) definition; + InvokeRequest invokeRequest = createInvokeRequest(awsInvokeRequestDefinition, sdkBytes); + + if (isReactiveType) { + Mono invokeFlowable = Mono.fromFuture(asyncClient.invoke(invokeRequest)) + .map(invokeResult -> + decodeResult(definition, (Argument) outputType.getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT), invokeResult)) + .onErrorResume(throwable -> Mono.error(new FunctionExecutionException("Error executing AWS Lambda [" + definition.getName() + "]: " + throwable.getMessage(), throwable))) + .subscribeOn(Schedulers.fromExecutor(executor)); + return conversionService.convert(invokeFlowable, outputType).orElseThrow(() -> new IllegalArgumentException("Unsupported Reactive type: " + outputType)); + + } else { + InvokeResponse invokeResult = syncClient.invoke(invokeRequest); + try { + return (O) decodeResult(definition, outputType, invokeResult); + } catch (Exception e) { + throw new FunctionExecutionException("Error executing AWS Lambda [" + definition.getName() + "]: " + e.getMessage(), e); + } + } + } + + private InvokeRequest createInvokeRequest(AwsInvokeRequestDefinition awsInvokeRequestDefinition, + SdkBytes sdkBytes) { + return InvokeRequest.builder() + .functionName(awsInvokeRequestDefinition.getFunctionName()) + .qualifier(awsInvokeRequestDefinition.getQualifier()) + .clientContext(awsInvokeRequestDefinition.getClientContext()) + .payload(sdkBytes) + .build(); + } + + private Object decodeResult(FunctionDefinition definition, Argument outputType, InvokeResponse invokeResult) { + Integer statusCode = invokeResult.statusCode(); + if (statusCode >= STATUS_CODE_ERROR) { + throw new FunctionExecutionException("Error executing AWS Lambda [" + definition.getName() + "]: " + invokeResult.functionError()); + } + io.micronaut.core.io.buffer.ByteBuffer byteBuffer = byteBufferFactory.copiedBuffer(invokeResult.payload().asByteArray()); + + return mediaTypeCodec.decode(outputType, byteBuffer); + } + + private SdkBytes encodeInput(I input) { + if (input != null) { + ByteBuffer nioBuffer = mediaTypeCodec.encode(input, byteBufferFactory).asNioBuffer(); + return SdkBytes.fromByteBuffer(nioBuffer); + } + return null; + } + + @SuppressWarnings("unchecked") + @Override + public Optional> choose(FunctionDefinition definition) { + if (definition instanceof AwsInvokeRequestDefinition) { + return Optional.of((FunctionInvoker) this); + } + return Optional.empty(); + } +} diff --git a/function-client-aws-v2/src/main/java/io/micronaut/function/client/aws/v2/package-info.java b/function-client-aws-v2/src/main/java/io/micronaut/function/client/aws/v2/package-info.java new file mode 100644 index 0000000000..25270c3b6e --- /dev/null +++ b/function-client-aws-v2/src/main/java/io/micronaut/function/client/aws/v2/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright 2017-2024 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * AWS Lambda Function Client related classes. + * + * @since 4.7.0 + */ +package io.micronaut.function.client.aws.v2; + diff --git a/function-client-aws-v2/src/test/groovy/io/micronaut/function/client/aws/v2/AwsInvokeRequestDefinitionSpec.groovy b/function-client-aws-v2/src/test/groovy/io/micronaut/function/client/aws/v2/AwsInvokeRequestDefinitionSpec.groovy new file mode 100644 index 0000000000..463cc03a70 --- /dev/null +++ b/function-client-aws-v2/src/test/groovy/io/micronaut/function/client/aws/v2/AwsInvokeRequestDefinitionSpec.groovy @@ -0,0 +1,32 @@ +package io.micronaut.function.client.aws.v2 + +import io.micronaut.context.annotation.Property +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import jakarta.inject.Inject +import spock.lang.Specification + +@Property(name = "aws.lambda.functions.foo.function-name", value = "x-function-name") +@Property(name = "aws.lambda.functions.foo.qualifier", value = "x-qualifier") +@Property(name = "aws.lambda.functions.foo.client-context", value = "x-client-context") +@Property(name = "aws.lambda.functions.bar.function-name", value = "z-function-name") +@Property(name = "aws.lambda.functions.bar.qualifier", value = "z-qualifier") +@Property(name = "aws.lambda.functions.bar.client-context", value = "z-client-context") +@MicronautTest +class AwsInvokeRequestDefinitionSpec extends Specification { + + @Inject + List awsInvokeRequestDefinitions + + void "test aws invoke request"() { + expect: + awsInvokeRequestDefinitions.find { it.name == 'foo' }.name == 'foo' + awsInvokeRequestDefinitions.find { it.name == 'foo' }.functionName == "x-function-name" + awsInvokeRequestDefinitions.find { it.name == 'foo' }.qualifier == "x-qualifier" + awsInvokeRequestDefinitions.find { it.name == 'foo' }.clientContext == "x-client-context" + + awsInvokeRequestDefinitions.find { it.name == 'bar' }.name == 'bar' + awsInvokeRequestDefinitions.find { it.name == 'bar' }.functionName == "z-function-name" + awsInvokeRequestDefinitions.find { it.name == 'bar' }.qualifier == "z-qualifier" + awsInvokeRequestDefinitions.find { it.name == 'bar' }.clientContext == "z-client-context" + } +} diff --git a/function-client-aws-v2/src/test/groovy/io/micronaut/function/client/aws/v2/ComplexType.java b/function-client-aws-v2/src/test/groovy/io/micronaut/function/client/aws/v2/ComplexType.java new file mode 100644 index 0000000000..27b8f5dc1f --- /dev/null +++ b/function-client-aws-v2/src/test/groovy/io/micronaut/function/client/aws/v2/ComplexType.java @@ -0,0 +1,33 @@ +package io.micronaut.function.client.aws.v2; + +import io.micronaut.serde.annotation.Serdeable; + +@Serdeable +public class ComplexType { + private int aNumber; + private String aString; + + public ComplexType() { + } + + public ComplexType(int aNumber, String aString) { + this.aNumber = aNumber; + this.aString = aString; + } + + public int getaNumber() { + return aNumber; + } + + public void setaNumber(int aNumber) { + this.aNumber = aNumber; + } + + public String getaString() { + return aString; + } + + public void setaString(String aString) { + this.aString = aString; + } +} diff --git a/function-client-aws-v2/src/test/groovy/io/micronaut/function/client/aws/v2/TestFunctionClient.java b/function-client-aws-v2/src/test/groovy/io/micronaut/function/client/aws/v2/TestFunctionClient.java new file mode 100644 index 0000000000..a2e5bde7a9 --- /dev/null +++ b/function-client-aws-v2/src/test/groovy/io/micronaut/function/client/aws/v2/TestFunctionClient.java @@ -0,0 +1,12 @@ +package io.micronaut.function.client.aws.v2; + +import io.micronaut.function.client.FunctionClient; +import io.micronaut.http.annotation.Body; +import jakarta.inject.Named; + +@FunctionClient +public interface TestFunctionClient { + + @Named("test-function") + TestFunctionClientResponse invokeFunction(@Body TestFunctionClientRequest request); +} diff --git a/function-client-aws-v2/src/test/groovy/io/micronaut/function/client/aws/v2/TestFunctionClientRequest.java b/function-client-aws-v2/src/test/groovy/io/micronaut/function/client/aws/v2/TestFunctionClientRequest.java new file mode 100644 index 0000000000..0e8fc403f4 --- /dev/null +++ b/function-client-aws-v2/src/test/groovy/io/micronaut/function/client/aws/v2/TestFunctionClientRequest.java @@ -0,0 +1,43 @@ +package io.micronaut.function.client.aws.v2; + +import io.micronaut.serde.annotation.Serdeable; + +@Serdeable +public class TestFunctionClientRequest { + private int aNumber; + private String aString; + private ComplexType aObject; + + public TestFunctionClientRequest() { + } + + public TestFunctionClientRequest(int aNumber, String aString, ComplexType aObject) { + this.aNumber = aNumber; + this.aString = aString; + this.aObject = aObject; + } + + public int getaNumber() { + return aNumber; + } + + public void setaNumber(int aNumber) { + this.aNumber = aNumber; + } + + public String getaString() { + return aString; + } + + public void setaString(String aString) { + this.aString = aString; + } + + public ComplexType getaObject() { + return aObject; + } + + public void setaObject(ComplexType aObject) { + this.aObject = aObject; + } +} diff --git a/function-client-aws-v2/src/test/groovy/io/micronaut/function/client/aws/v2/TestFunctionClientResponse.java b/function-client-aws-v2/src/test/groovy/io/micronaut/function/client/aws/v2/TestFunctionClientResponse.java new file mode 100644 index 0000000000..2319714358 --- /dev/null +++ b/function-client-aws-v2/src/test/groovy/io/micronaut/function/client/aws/v2/TestFunctionClientResponse.java @@ -0,0 +1,49 @@ +package io.micronaut.function.client.aws.v2; + +import io.micronaut.serde.annotation.Serdeable; + +import java.util.List; + +@Serdeable +public class TestFunctionClientResponse { + private int aNumber; + private String aString; + private ComplexType aObject; + private List anArray; + + public TestFunctionClientResponse() { + + } + + public int getaNumber() { + return aNumber; + } + + public void setaNumber(int aNumber) { + this.aNumber = aNumber; + } + + public String getaString() { + return aString; + } + + public void setaString(String aString) { + this.aString = aString; + } + + public ComplexType getaObject() { + return aObject; + } + + public void setaObject(ComplexType aObject) { + this.aObject = aObject; + } + + public List getAnArray() { + return anArray; + } + + public void setAnArray(List anArray) { + this.anArray = anArray; + } +} diff --git a/function-client-aws-v2/src/test/groovy/io/micronaut/function/client/aws/v2/TestFunctionReactiveClient.java b/function-client-aws-v2/src/test/groovy/io/micronaut/function/client/aws/v2/TestFunctionReactiveClient.java new file mode 100644 index 0000000000..d61afb647b --- /dev/null +++ b/function-client-aws-v2/src/test/groovy/io/micronaut/function/client/aws/v2/TestFunctionReactiveClient.java @@ -0,0 +1,12 @@ +package io.micronaut.function.client.aws.v2; + +import io.micronaut.function.client.FunctionClient; +import io.micronaut.http.annotation.Body; +import jakarta.inject.Named; +import org.reactivestreams.Publisher; + +@FunctionClient +public interface TestFunctionReactiveClient { + @Named("test-function-reactive") + Publisher invokeFunctionReactive(@Body TestFunctionClientRequest request); +} diff --git a/function-client-aws-v2/src/test/groovy/io/micronaut/function/client/aws/v2/TestFunctionSpec.groovy b/function-client-aws-v2/src/test/groovy/io/micronaut/function/client/aws/v2/TestFunctionSpec.groovy new file mode 100644 index 0000000000..03f8a4465d --- /dev/null +++ b/function-client-aws-v2/src/test/groovy/io/micronaut/function/client/aws/v2/TestFunctionSpec.groovy @@ -0,0 +1,245 @@ +package io.micronaut.function.client.aws.v2 + +import io.micronaut.core.io.ResourceLoader +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import io.micronaut.test.support.TestPropertyProvider +import jakarta.inject.Inject +import org.testcontainers.containers.localstack.LocalStackContainer +import org.testcontainers.spock.Testcontainers +import org.testcontainers.utility.DockerImageName +import reactor.core.publisher.Mono +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials +import software.amazon.awssdk.auth.credentials.AwsCredentialsProviderChain +import software.amazon.awssdk.core.SdkBytes +import software.amazon.awssdk.regions.Region +import software.amazon.awssdk.services.iam.IamClient +import software.amazon.awssdk.services.iam.model.AttachRolePolicyRequest +import software.amazon.awssdk.services.iam.model.CreatePolicyRequest +import software.amazon.awssdk.services.iam.model.CreateRoleRequest +import software.amazon.awssdk.services.iam.model.GetPolicyRequest +import software.amazon.awssdk.services.iam.model.GetRoleRequest +import software.amazon.awssdk.services.iam.model.Role +import software.amazon.awssdk.services.iam.waiters.IamWaiter +import software.amazon.awssdk.services.lambda.LambdaClient +import software.amazon.awssdk.services.lambda.model.Architecture +import software.amazon.awssdk.services.lambda.model.CreateFunctionRequest +import software.amazon.awssdk.services.lambda.model.DeleteFunctionRequest +import software.amazon.awssdk.services.lambda.model.FunctionCode +import software.amazon.awssdk.services.lambda.model.GetFunctionConfigurationRequest +import software.amazon.awssdk.services.lambda.model.Runtime +import software.amazon.awssdk.services.lambda.model.GetFunctionRequest +import software.amazon.awssdk.services.lambda.model.LambdaRequest +import spock.lang.Shared +import spock.lang.Specification + +import java.nio.file.Files +import java.nio.file.Path +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream + +import static org.testcontainers.containers.localstack.LocalStackContainer.Service.IAM +import static org.testcontainers.containers.localstack.LocalStackContainer.Service.LAMBDA + +@Testcontainers +@MicronautTest +class TestFunctionSpec extends Specification implements TestPropertyProvider { + + private static final String FUNCTION_NAME = "TEST_FUNCTION_NAME" + + @Shared + private LocalStackContainer localStackContainer = new LocalStackContainer(DockerImageName + .parse("localstack/localstack:3.4.0")) + .withServices(IAM, LAMBDA) + + @Inject + @Shared + LambdaClient lambdaClient + + @Inject + @Shared + ResourceLoader resourceLoader + + @Override + Map getProperties() { + Map.of( + "aws.access-key-id", localStackContainer.getAccessKey(), + "aws.secret-key", localStackContainer.getSecretKey(), + "aws.region", localStackContainer.getRegion(), + "aws.services.lambda.endpoint-override", localStackContainer.getEndpointOverride(LAMBDA).toString() + ) as Map + } + + @Inject + TestFunctionClient functionClient + + @Inject + TestFunctionReactiveClient testFunctionReactiveClient + + def setupSpec() { + try { + lambdaClient.getFunction(GetFunctionRequest.builder() + .functionName(FUNCTION_NAME) + .build()) + } catch(Exception e) { + // Create if not exists + byte[] bytes = lambdaBytes(resourceLoader) + LambdaRequest lambdaRequest = createFunctionRequest(bytes) + if (lambdaRequest instanceof CreateFunctionRequest) { + def waiter = lambdaClient.waiter() + + def function = lambdaClient.createFunction((CreateFunctionRequest) lambdaRequest) + waiter.waitUntilFunctionExists(GetFunctionRequest.builder() + .functionName(function.functionName()) + .build()) + GetFunctionConfigurationRequest getFunctionConfigurationRequest = + GetFunctionConfigurationRequest.builder().functionName(function.functionName()).build() + waiter.waitUntilFunctionActive(getFunctionConfigurationRequest) + } + } + } + + def "can invoke a JS Lambda function with the an @FunctionClient"() { + given: + Integer aNumber = 1 + String aString = "someString" + + when: + TestFunctionClientResponse result = functionClient + .invokeFunction(new TestFunctionClientRequest(aNumber, aString, new ComplexType(aNumber, aString))) + + then: + result.aNumber == aNumber + result.aString == aString + result.aObject + result.aObject.aNumber == aNumber + result.aObject.aString == aString + result.anArray.size() == 1 + result.anArray[0].aNumber == aNumber + result.anArray[0].aString == aString + } + + def "can invoke a JS Lambda function with the an @FunctionClient wtih reactive types"() { + given: + Integer aNumber = 1 + String aString = "someString" + when: + TestFunctionClientResponse result = Mono.from(testFunctionReactiveClient.invokeFunctionReactive(new TestFunctionClientRequest(aNumber, aString, new ComplexType(aNumber, aString)))).block() + + then: + result.aNumber == aNumber + result.aString == aString + result.aObject + result.aObject.aNumber == aNumber + result.aObject.aString == aString + result.anArray.size() == 1 + result.anArray[0].aNumber == aNumber + result.anArray[0].aString == aString + } + + private byte[] lambdaBytes(ResourceLoader resourceLoader) { + try (InputStream inputStream = resourceLoader.getResourceAsStream("classpath:lambda/index.js").orElseThrow()) { + byte[] fileBytes = inputStream.readAllBytes() + Path tempFile = Files.createTempFile(FUNCTION_NAME, ".zip"); + try (ZipOutputStream zos = new ZipOutputStream(Files.newOutputStream(tempFile))) { + ZipEntry zipEntry = new ZipEntry("index.js") + zos.putNextEntry(zipEntry) + zos.write(fileBytes) + zos.closeEntry() + } + return Files.readAllBytes(tempFile); + } + } + + private Role getLambdaRole() { + def iamClient = IamClient.builder() + .region(Region.of(localStackContainer.getRegion())) + .credentialsProvider(AwsCredentialsProviderChain.of( + () -> AwsBasicCredentials.create(localStackContainer.getAccessKey(), localStackContainer.getSecretKey()) + )) + .endpointOverride(localStackContainer.getEndpointOverride(IAM)) + .build() + def roleName = "lambda-role"; + try { + return iamClient.getRole(GetRoleRequest.builder() + .roleName(roleName) + .build()).role(); + } catch (final Exception e) { + // Create if not exists + IamWaiter iamWaiter = iamClient.waiter(); + + CreatePolicyRequest request = CreatePolicyRequest.builder() + .policyName("lambda-invoke-policy") + .policyDocument(""" + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "LambdaInvoke", + "Effect": "Allow", + "Action": [ + "lambda:InvokeFunction" + ], + "Resource": "*" + } + ] + } + """.stripIndent()) + .build(); + + def policy = iamClient.createPolicy(request) + iamWaiter.waitUntilPolicyExists(GetPolicyRequest.builder() + .policyArn(policy.policy().arn()) + .build()); + + def role = iamClient.createRole(CreateRoleRequest.builder() + .roleName(roleName) + .path("/") + .assumeRolePolicyDocument(""" + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + }, + "Action": "sts:AssumeRole" + } + ] + } + """.stripIndent()) + .build()) + + iamWaiter.waitUntilRoleExists(GetRoleRequest.builder() + .roleName(role.role().roleName()) + .build()) + + iamClient.attachRolePolicy(AttachRolePolicyRequest.builder() + .roleName(role.role().roleName()) + .policyArn(policy.policy().arn()) + .build()) + + return role.role(); + } + } + + private LambdaRequest createFunctionRequest(byte[] arr) { + def role = getLambdaRole() + CreateFunctionRequest.builder() + .functionName(FUNCTION_NAME) + .role(role.arn()) + .code(FunctionCode.builder() + .zipFile(SdkBytes.fromByteArray(arr)) + .build()) + .runtime(Runtime.NODEJS18_X) + .architectures(Architecture.X86_64) + .handler("index.handler") + .build() + } + + private LambdaRequest deleteFunctionRequest() { + DeleteFunctionRequest.builder() + .functionName(FUNCTION_NAME) + .build() + } +} diff --git a/function-client-aws-v2/src/test/resources/application-test.properties b/function-client-aws-v2/src/test/resources/application-test.properties new file mode 100644 index 0000000000..cdaf3278ec --- /dev/null +++ b/function-client-aws-v2/src/test/resources/application-test.properties @@ -0,0 +1,2 @@ +aws.lambda.functions.test-function.function-name=TEST_FUNCTION_NAME +aws.lambda.functions.test-function-reactive.function-name=TEST_FUNCTION_NAME diff --git a/function-client-aws-v2/src/test/resources/lambda/index.js b/function-client-aws-v2/src/test/resources/lambda/index.js new file mode 100644 index 0000000000..a9c8b20423 --- /dev/null +++ b/function-client-aws-v2/src/test/resources/lambda/index.js @@ -0,0 +1,17 @@ +exports.handler = async (event, context) => { + if (!event.aNumber || !event.aString || !event.aObject || !event.aObject.aNumber || !event.aObject.aString) { + throw new Error('Invalid Input'); + } + + const arr = []; + arr.push(event.aObject); + + const response = { + aNumber: event.aNumber, + aString: event.aString, + aObject: event.aObject, + anArray: arr, + }; + + return response +}; diff --git a/function-client-aws-v2/src/test/resources/logback.xml b/function-client-aws-v2/src/test/resources/logback.xml new file mode 100644 index 0000000000..80dcc40c8d --- /dev/null +++ b/function-client-aws-v2/src/test/resources/logback.xml @@ -0,0 +1,15 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + \ No newline at end of file diff --git a/function-client-aws/build.gradle.kts b/function-client-aws/build.gradle.kts index 244c91d7e2..8f332dc738 100644 --- a/function-client-aws/build.gradle.kts +++ b/function-client-aws/build.gradle.kts @@ -14,4 +14,6 @@ dependencies { testImplementation(mn.micronaut.function.web) testImplementation(mnGroovy.micronaut.function.groovy) testImplementation(mnGroovy.micronaut.runtime.groovy) + testImplementation(mnTest.micronaut.test.junit5) + testRuntimeOnly(libs.junit.jupiter.engine) } diff --git a/function-client-aws/src/test/groovy/io/micronaut/function/client/aws/AwsLambdaInvokeSpec.groovy b/function-client-aws/src/test/groovy/io/micronaut/function/client/aws/AwsLambdaInvokeSpec.groovy index f3c7112f6c..3e4ad30705 100644 --- a/function-client-aws/src/test/groovy/io/micronaut/function/client/aws/AwsLambdaInvokeSpec.groovy +++ b/function-client-aws/src/test/groovy/io/micronaut/function/client/aws/AwsLambdaInvokeSpec.groovy @@ -73,7 +73,7 @@ class AwsLambdaInvokeSpec extends Specification { void "test setup lambda config"() { given: ApplicationContext applicationContext = ApplicationContext.run( - 'aws.lambda.functions.test.functionName':'micronaut-function', + 'aws.lambda.functions.test.function-name':'micronaut-function', 'aws.lambda.functions.test.qualifier':'something', 'aws.lambda.region':'us-east-1' ) @@ -94,7 +94,7 @@ class AwsLambdaInvokeSpec extends Specification { void "test invoke function"() { given: ApplicationContext applicationContext = ApplicationContext.run( - 'aws.lambda.functions.test.functionName':'micronaut-function', + 'aws.lambda.functions.test.function-name':'micronaut-function', 'aws.lambda.region':'us-east-1' ) @@ -123,7 +123,7 @@ class AwsLambdaInvokeSpec extends Specification { void "test invoke client with @FunctionClient"() { given: ApplicationContext applicationContext = ApplicationContext.run( - 'aws.lambda.functions.test.functionName':'micronaut-function', + 'aws.lambda.functions.test.function-name':'micronaut-function', 'aws.lambda.region':'us-east-1' ) diff --git a/function-client-aws/src/test/groovy/io/micronaut/function/client/aws/LocalFunctionInvokeJavaSpec.java b/function-client-aws/src/test/groovy/io/micronaut/function/client/aws/LocalFunctionInvokeJavaSpec.java deleted file mode 100644 index 7e3dbb7bcc..0000000000 --- a/function-client-aws/src/test/groovy/io/micronaut/function/client/aws/LocalFunctionInvokeJavaSpec.java +++ /dev/null @@ -1,85 +0,0 @@ -package io.micronaut.function.client.aws; - -//tag::import[] -import io.micronaut.context.ApplicationContext; -import io.micronaut.function.client.FunctionClient; -import jakarta.inject.Named; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; -//end::rxImport[] -//end::import[] - -import io.micronaut.runtime.server.EmbeddedServer; -import org.reactivestreams.Publisher; -import reactor.core.publisher.Mono; -//tag::rxImport[] - -public class LocalFunctionInvokeJavaSpec { - - //tag::invokeLocalFunction[] - @Test - public void testInvokingALocalFunction() { - Sum sum = new Sum(); - sum.setA(5); - sum.setB(10); - - EmbeddedServer server = ApplicationContext.run(EmbeddedServer.class); - MathClient mathClient = server.getApplicationContext().getBean(MathClient.class); - - assertEquals(Long.valueOf(Integer.MAX_VALUE), mathClient.max()); - assertEquals(2, mathClient.rnd(1.6f)); - assertEquals(15, mathClient.sum(sum)); - - } - //end::invokeLocalFunction[] - - //tag::invokeRxLocalFunction[] - @Test - public void testInvokingALocalFunctionRX() { - Sum sum = new Sum(); - sum.setA(5); - sum.setB(10); - - EmbeddedServer server = ApplicationContext.run(EmbeddedServer.class); - ReactiveMathClient mathClient = server.getApplicationContext().getBean(ReactiveMathClient.class); - - assertEquals(Long.valueOf(Integer.MAX_VALUE), mathClient.max().block()); - assertEquals(2, mathClient.rnd(1.6f).block().longValue()); - assertEquals(15, mathClient.sum(sum).block().longValue()); - - } - //end::invokeRxLocalFunction[] - - //tag::beginFunctionClient[] - @FunctionClient - interface MathClient { - //end::beginFunctionClient[] - - //tag::functionMax[] - Long max(); //<1> - //end::functionMax[] - - //tag::functionRnd[] - @Named("round") - int rnd(float value); - //end::functionRnd[] - - long sum(Sum sum); - //tag::endFunctionClient[] - } - //end::endFunctionClient[] - - - //tag::rxFunctionClient[] - @FunctionClient - interface ReactiveMathClient { - Mono max(); - - @Named("round") - Mono rnd(float value); - - Mono sum(Sum sum); - } - //end::rxFunctionClient[] -} diff --git a/function-client-aws/src/test/groovy/io/micronaut/function/client/aws/LocalFunctionInvokeSpec.groovy b/function-client-aws/src/test/groovy/io/micronaut/function/client/aws/LocalFunctionInvokeSpec.groovy index e1939521de..712ffa3cee 100644 --- a/function-client-aws/src/test/groovy/io/micronaut/function/client/aws/LocalFunctionInvokeSpec.groovy +++ b/function-client-aws/src/test/groovy/io/micronaut/function/client/aws/LocalFunctionInvokeSpec.groovy @@ -26,6 +26,7 @@ import io.micronaut.runtime.server.EmbeddedServer //tag::rxImport[] import org.reactivestreams.Publisher import reactor.core.publisher.Mono +import spock.lang.Ignore //end::rxImport[] import spock.lang.Specification diff --git a/function-client-aws/src/test/java/io/micronaut/function/client/aws/LocalFunctionInvokeJavaTest.java b/function-client-aws/src/test/java/io/micronaut/function/client/aws/LocalFunctionInvokeJavaTest.java new file mode 100644 index 0000000000..f9b2c1a99b --- /dev/null +++ b/function-client-aws/src/test/java/io/micronaut/function/client/aws/LocalFunctionInvokeJavaTest.java @@ -0,0 +1,44 @@ +package io.micronaut.function.client.aws; + +import io.micronaut.context.ApplicationContext; +import static org.junit.Assert.assertEquals; +import io.micronaut.runtime.server.EmbeddedServer; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; +import spock.lang.Ignore; + +class LocalFunctionInvokeJavaTest { + + @Test + void testInvokingALocalFunction() { + Suma sum = new Suma(); + sum.setA(5); + sum.setB(10); + + EmbeddedServer server = ApplicationContext.run(EmbeddedServer.class); + MathClient mathClient = server.getApplicationContext().getBean(MathClient.class); + + assertEquals(Long.valueOf(Integer.MAX_VALUE), mathClient.max()); + assertEquals(2, mathClient.rnd(1.6f)); + assertEquals(15, mathClient.sum(sum)); + + server.close(); + } + + @Test + void testInvokingALocalFunctionRX() { + Suma sum = new Suma(); + sum.setA(5); + sum.setB(10); + + EmbeddedServer server = ApplicationContext.run(EmbeddedServer.class); + ReactiveMathClient mathClient = server.getApplicationContext().getBean(ReactiveMathClient.class); + + assertEquals(Long.valueOf(Integer.MAX_VALUE), Mono.from(mathClient.max()).block()); + assertEquals(2, Mono.from(mathClient.rnd(1.6f)).block().longValue()); + assertEquals(15, Mono.from(mathClient.sum(sum)).block().longValue()); + + server.close(); + } +} diff --git a/function-client-aws/src/test/java/io/micronaut/function/client/aws/MathClient.java b/function-client-aws/src/test/java/io/micronaut/function/client/aws/MathClient.java new file mode 100644 index 0000000000..5a29aca59c --- /dev/null +++ b/function-client-aws/src/test/java/io/micronaut/function/client/aws/MathClient.java @@ -0,0 +1,14 @@ +package io.micronaut.function.client.aws; + +import io.micronaut.function.client.FunctionClient; +import jakarta.inject.Named; + +@FunctionClient +interface MathClient { + Long max(); + + @Named("round") + int rnd(float value); + + long sum(Suma sum); +} diff --git a/function-client-aws/src/test/java/io/micronaut/function/client/aws/ReactiveMathClient.java b/function-client-aws/src/test/java/io/micronaut/function/client/aws/ReactiveMathClient.java new file mode 100644 index 0000000000..890b3260e7 --- /dev/null +++ b/function-client-aws/src/test/java/io/micronaut/function/client/aws/ReactiveMathClient.java @@ -0,0 +1,15 @@ +package io.micronaut.function.client.aws; + +import io.micronaut.function.client.FunctionClient; +import jakarta.inject.Named; +import org.reactivestreams.Publisher; + +@FunctionClient +interface ReactiveMathClient { + Publisher max(); + + @Named("round") + Publisher rnd(float value); + + Publisher sum(Suma sum); +} diff --git a/function-client-aws/src/test/java/io/micronaut/function/client/aws/Suma.java b/function-client-aws/src/test/java/io/micronaut/function/client/aws/Suma.java new file mode 100644 index 0000000000..d8cb82cac2 --- /dev/null +++ b/function-client-aws/src/test/java/io/micronaut/function/client/aws/Suma.java @@ -0,0 +1,27 @@ +package io.micronaut.function.client.aws; + +/** + * @author graemerocher + * @since 1.0 + */ +public class Suma { + + private int a; + private Integer b; + + public int getA() { + return a; + } + + public void setA(int a) { + this.a = a; + } + + public Integer getB() { + return b; + } + + public void setB(Integer b) { + this.b = b; + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b6fe5057f9..fc9f382456 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,47 +1,48 @@ [versions] -micronaut = "4.4.0" +micronaut = "4.7.2" micronaut-docs = "2.0.0" -micronaut-test = "4.2.0" -groovy = "4.0.15" +micronaut-test = "4.5.0" +groovy = "4.0.22" spock = "2.3-groovy-4.0" bouncycastle = '1.70' fileupload = '0.0.6' +jetty = '11.0.24' logback-json-classic = '0.1.5' -micronaut-discovery = "4.2.0" -micronaut-groovy = "4.2.0" -micronaut-logging = "1.2.2" -micronaut-mongodb = "5.2.0" -micronaut-reactor = "3.2.1" -micronaut-security = "4.6.9" -micronaut-serde = "2.8.2" -micronaut-servlet = "4.6.0" -micronaut-test-resources="2.4.0" -micronaut-views = "5.1.0" -micronaut-validation = "4.4.3" +micronaut-discovery = "4.5.0" +micronaut-groovy = "4.4.0" +micronaut-logging = "1.4.0" +micronaut-mongodb = "5.5.0" +micronaut-reactor = "3.6.0" +micronaut-security = "4.11.2" +micronaut-serde = "2.12.0" +micronaut-servlet = "4.12.0" +micronaut-test-resources="2.6.2" +micronaut-views = "5.5.1" +micronaut-validation = "4.8.0" managed-alexa-ask-sdk = "2.86.0" -managed-aws-java-sdk-v1 = '1.12.696' -managed-aws-java-sdk-v2 = '2.24.10' +managed-aws-java-sdk-v1 = '1.12.780' +managed-aws-java-sdk-v2 = '2.29.11' managed-aws-lambda = '1.2.3' -managed-aws-lambda-events = '3.11.4' +managed-aws-lambda-events = '3.14.0' managed-aws-lambda-java-serialization = '1.1.5' -aws-lambda-java-runtime-interface-client = '2.5.0' +aws-lambda-java-runtime-interface-client = '2.6.0' managed-aws-serverless-core = '1.9.3' micronaut-starter = "3.9.2" -slf4j = "2.0.12" +slf4j = "2.0.16" servlet-api = "2.5" javapoet = "1.13.0" # The following version should probably # be defined in Micronaut Graal but it's not shipped with a BOM yet graal = "24.0.0" -kotlin = "1.9.23" +kotlin = "1.9.25" # Micronaut -micronaut-gradle-plugin = "4.3.5" +micronaut-gradle-plugin = "4.4.4" [libraries] # Core @@ -67,6 +68,7 @@ awssdk-apache-client = { module = 'software.amazon.awssdk:apache-client' } awssdk-apigatewaymanagementapi = { module = 'software.amazon.awssdk:apigatewaymanagementapi' } awssdk-cloudwatchlogs = { module = 'software.amazon.awssdk:cloudwatchlogs'} awssdk-dynamodb = { module = 'software.amazon.awssdk:dynamodb' } +awssdk-lambda = { module = 'software.amazon.awssdk:lambda' } awssdk-netty-nio-client = { module = 'software.amazon.awssdk:netty-nio-client' } awssdk-rekognition = { module = 'software.amazon.awssdk:rekognition' } awssdk-s3 = { module = 'software.amazon.awssdk:s3' } @@ -76,6 +78,7 @@ awssdk-ses = { module = 'software.amazon.awssdk:ses' } awssdk-sns = { module = 'software.amazon.awssdk:sns' } awssdk-sqs = { module = 'software.amazon.awssdk:sqs' } awssdk-ssm = { module = 'software.amazon.awssdk:ssm' } +awssdk-iam = { module = 'software.amazon.awssdk:iam' } awssdk-url-connection-client = { module = 'software.amazon.awssdk:url-connection-client' } kotlin-stdlib-jdk8 = { module = 'org.jetbrains.kotlin:kotlin-stdlib-jdk8', version.ref = 'kotlin' } @@ -116,3 +119,8 @@ servlet-api = { module = 'javax.servlet:servlet-api', version.ref = 'servlet-api gradle-micronaut = { module = "io.micronaut.gradle:micronaut-gradle-plugin", version.ref = "micronaut-gradle-plugin" } gradle-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } + +testcontainers = { module = "org.testcontainers:testcontainers" } +testcontainers-localstack = { module = "org.testcontainers:localstack" } +testcontainers-junit = { module = "org.testcontainers:junit-jupiter" } +testcontainers-spock = { module = "org.testcontainers:spock" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e6441136f3..a4b76b9530 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b82aa23a4f..cea7a793a8 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.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 1aa94a4269..f3b75f3b0d 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -84,7 +86,7 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/gradlew.bat b/gradlew.bat index 93e3f59f13..9d21a21834 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -43,11 +45,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail diff --git a/settings.gradle.kts b/settings.gradle.kts index cbba64335e..c2d4d3d665 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -6,7 +6,7 @@ pluginManagement { } plugins { - id("io.micronaut.build.shared.settings") version "6.7.0" + id("io.micronaut.build.shared.settings") version "7.3.0" } enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") @@ -37,17 +37,21 @@ include("function-aws-api-proxy-test") include("function-aws-custom-runtime") include("function-aws-test") include("function-client-aws") +include("function-client-aws-v2") +include("test-suite-function-client-aws") include("test-suite") include("test-suite-aws-sdk-v2") include("test-suite-graal") include("test-suite-graal-logging") include("test-suite-groovy") +include("test-suite-function-client-aws-groovy") include("test-suite-http-server-tck-function-aws-api-gateway-proxy-alb") include("test-suite-http-server-tck-function-aws-api-gateway-proxy-payloadv1") include("test-suite-http-server-tck-function-aws-api-gateway-proxy-payloadv2") include("test-suite-http-server-tck-function-aws-api-proxy-test") include("test-suite-kotlin") +include("test-suite-function-client-aws-kotlin") include("test-suite-s3") configure { diff --git a/src/main/docs/guide/lambda/lambdafunctionclient.adoc b/src/main/docs/guide/lambda/lambdafunctionclient.adoc index 3f4cf86828..65b63f0af5 100644 --- a/src/main/docs/guide/lambda/lambdafunctionclient.adoc +++ b/src/main/docs/guide/lambda/lambdafunctionclient.adoc @@ -1,11 +1,19 @@ Micronaut AWS provides support for invoking AWS Lambda functions within a Micronaut application context. -To use the features described in this section, you will need to have the `micronaut-function-client-aws` dependency on your classpath. -dependency:micronaut-function-client-aws[groupId="io.micronaut.aws"] +=== AWS SDK V2 + +To use the features described in this section, you will need to have the following dependency on your classpath: + +dependency:micronaut-function-client-aws-v2[groupId="io.micronaut.aws"] + +NOTE: To invoke a function Micronaut configures a `LambdaAsyncClient` and `LambdaClient`. You can configure them by registering a https://docs.micronaut.io/latest/api/io/micronaut/context/event/BeanCreatedEventListener.html[BeanCreatedEventListener] for `software.amazon.awssdk.services.lambda.LambdaAsyncClient` or `software.amazon.awssdk.services.lambda.LambdaAsyncClientBuilder` You can define multiple named functions under the `aws.lambda.functions` configuration. -Each is configured by `AWSInvokeRequestDefinition` that allows setting any property on the underlying `com.amazonaws.services.lambda.model.InvokeRequest`. +Each is configured by `AwsInvokeRequestDefinition` that allows setting any property on the underlying `software.amazon.awssdk.services.lambda.model.InvokeRequest`. + + +=== Example For example, you invoke a function named `AwsLambdaFunctionName`, in the AWS Lambda console, with the following configuration: @@ -24,6 +32,16 @@ Alternatively, you can remove the `@Named` annotation and match the method name snippet::io.micronaut.docs.function.client.aws.methodnamed.AnalyticsClient[tags="clazz"] -To configure credentials for invoking the function you can either define a `~/.aws/credentials` file or use the application configuration file. Micronaut registers a api:configurations.aws.EnvironmentAWSCredentialsProvider[] that resolves AWS credentials from the Micronaut Environment. + +=== AWS SDK V1 + +To use AWS SDK v1 add the following dependency instead: + +dependency:micronaut-function-client-aws[groupId="io.micronaut.aws"] NOTE: To invoke a function Micronaut configures a `AWSLambdaAsyncClient` using api:function.client.aws.AWSLambdaConfiguration[] that allows configuring any of the properties of the `AWSLambdaAsyncClientBuilder` class. + +You can define multiple named functions under the `aws.lambda.functions` configuration. +Each is configured by `AWSInvokeRequestDefinition` that allows setting any property on the underlying `com.amazonaws.services.lambda.model.InvokeRequest`. + +To configure credentials for invoking the function you can either define a `~/.aws/credentials` file or use the application configuration file. Micronaut registers a api:configurations.aws.EnvironmentAWSCredentialsProvider[] that resolves AWS credentials from the Micronaut Environment. diff --git a/src/main/docs/guide/sdkv2/lambdaClient.adoc b/src/main/docs/guide/sdkv2/lambdaClient.adoc new file mode 100644 index 0000000000..495de2782e --- /dev/null +++ b/src/main/docs/guide/sdkv2/lambdaClient.adoc @@ -0,0 +1,15 @@ +To use a Lambda client, add the following dependency: + +dependency:lambda[groupId="software.amazon.awssdk"] + +Then, the following beans will be created: + +* `software.amazon.awssdk.services.lambda.LambdaClientBuilder` +* `software.amazon.awssdk.services.lambda.LambdaClient`. + +And: + +* `software.amazon.awssdk.services.lambda.LambdaAsyncClientBuilder` +* `software.amazon.awssdk.services.lambda.LambdaAsyncClient`. + +The HTTP client, credentials and region will be configured as per described in the <>. diff --git a/src/main/docs/guide/toc.yml b/src/main/docs/guide/toc.yml index 805722d2fe..667d43362f 100644 --- a/src/main/docs/guide/toc.yml +++ b/src/main/docs/guide/toc.yml @@ -28,6 +28,7 @@ sdkv2: s3: S3 dynamodb: Dynamo DB ses: SES + lambdaClient: Lambda Client sns: SNS sqs: SQS ssm: SSM diff --git a/test-suite-function-client-aws-groovy/build.gradle.kts b/test-suite-function-client-aws-groovy/build.gradle.kts new file mode 100644 index 0000000000..ef95a4b33e --- /dev/null +++ b/test-suite-function-client-aws-groovy/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + id("groovy") + id("java-library") + id("io.micronaut.build.internal.aws-tests") +} + +dependencies { + testCompileOnly(mn.micronaut.inject.groovy) + testImplementation(mnTest.micronaut.test.spock) + testImplementation(platform(mn.micronaut.core.bom)) + testImplementation(projects.micronautFunctionClientAws) +} + +tasks { + named("test", Test::class) { + useJUnitPlatform() + } +} + +java { + sourceCompatibility = JavaVersion.toVersion("17") + targetCompatibility = JavaVersion.toVersion("17") +} diff --git a/test-suite-function-client-aws-groovy/src/test/groovy/io/micronaut/docs/function/client/aws/AnalyticsClientSpec.groovy b/test-suite-function-client-aws-groovy/src/test/groovy/io/micronaut/docs/function/client/aws/AnalyticsClientSpec.groovy new file mode 100644 index 0000000000..00770b9725 --- /dev/null +++ b/test-suite-function-client-aws-groovy/src/test/groovy/io/micronaut/docs/function/client/aws/AnalyticsClientSpec.groovy @@ -0,0 +1,30 @@ +package io.micronaut.docs.function.client.aws + +import io.micronaut.context.ApplicationContext +import io.micronaut.function.client.FunctionDefinition +import io.micronaut.function.client.aws.AWSInvokeRequestDefinition +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import spock.lang.Specification +import jakarta.inject.Inject + +@MicronautTest(startApplication = false) +class AnalyticsClientSpec extends Specification { + @Inject + ApplicationContext applicationContext + + void "test setup function definitions"() { + given: + Collection definitions = applicationContext.getBeansOfType(FunctionDefinition) + + expect: + definitions.size() == 1 + definitions.first() instanceof AWSInvokeRequestDefinition + + when: + AWSInvokeRequestDefinition invokeRequestDefinition = (AWSInvokeRequestDefinition) definitions.first() + + then: + invokeRequestDefinition.name == 'analytics' + invokeRequestDefinition.invokeRequest.functionName == 'AwsLambdaFunctionName' + } +} diff --git a/test-suite-function-client-aws-groovy/src/test/groovy/io/micronaut/docs/function/client/aws/atnamed/AnalyticsClient.groovy b/test-suite-function-client-aws-groovy/src/test/groovy/io/micronaut/docs/function/client/aws/atnamed/AnalyticsClient.groovy new file mode 100644 index 0000000000..f49d482ab8 --- /dev/null +++ b/test-suite-function-client-aws-groovy/src/test/groovy/io/micronaut/docs/function/client/aws/atnamed/AnalyticsClient.groovy @@ -0,0 +1,9 @@ +package io.micronaut.docs.function.client.aws.atnamed + +import io.micronaut.function.client.FunctionClient +import jakarta.inject.Named +@FunctionClient +interface AnalyticsClient { + @Named('analytics') + String visit(String productId); +} diff --git a/test-suite-function-client-aws-groovy/src/test/groovy/io/micronaut/docs/function/client/aws/methodnamed/AnalyticsClient.groovy b/test-suite-function-client-aws-groovy/src/test/groovy/io/micronaut/docs/function/client/aws/methodnamed/AnalyticsClient.groovy new file mode 100644 index 0000000000..6dfdf74d6c --- /dev/null +++ b/test-suite-function-client-aws-groovy/src/test/groovy/io/micronaut/docs/function/client/aws/methodnamed/AnalyticsClient.groovy @@ -0,0 +1,8 @@ +package io.micronaut.docs.function.client.aws.methodnamed + +import io.micronaut.function.client.FunctionClient + +@FunctionClient +interface AnalyticsClient { + String analytics(String productId) +} diff --git a/test-suite-function-client-aws-groovy/src/test/resources/application.properties b/test-suite-function-client-aws-groovy/src/test/resources/application.properties new file mode 100644 index 0000000000..73c92aa9d4 --- /dev/null +++ b/test-suite-function-client-aws-groovy/src/test/resources/application.properties @@ -0,0 +1 @@ +aws.lambda.functions.analytics.function-name=AwsLambdaFunctionName \ No newline at end of file diff --git a/test-suite-function-client-aws-groovy/src/test/resources/logback.xml b/test-suite-function-client-aws-groovy/src/test/resources/logback.xml new file mode 100644 index 0000000000..80dcc40c8d --- /dev/null +++ b/test-suite-function-client-aws-groovy/src/test/resources/logback.xml @@ -0,0 +1,15 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + \ No newline at end of file diff --git a/test-suite-function-client-aws-kotlin/build.gradle.kts b/test-suite-function-client-aws-kotlin/build.gradle.kts new file mode 100644 index 0000000000..c423ba56a2 --- /dev/null +++ b/test-suite-function-client-aws-kotlin/build.gradle.kts @@ -0,0 +1,32 @@ +plugins { + id("org.jetbrains.kotlin.jvm") + id("org.jetbrains.kotlin.kapt") + id("io.micronaut.build.internal.aws-tests") +} + +val micronautVersion: String by project + +dependencies { + kaptTest(mn.micronaut.inject.java) + testAnnotationProcessor(platform(mn.micronaut.core.bom)) + testImplementation(libs.junit.jupiter.api) + testImplementation(mnTest.micronaut.test.junit5) + testRuntimeOnly(libs.junit.jupiter.engine) + testImplementation(projects.micronautFunctionAws) + testImplementation(libs.kotlin.stdlib.jdk8) + testImplementation(projects.micronautFunctionClientAws) + testRuntimeOnly(mn.snakeyaml) +} + +tasks { + named("test", Test::class) { + useJUnitPlatform() + } +} + +kotlin { + jvmToolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } + +} diff --git a/test-suite-function-client-aws-kotlin/src/test/kotlin/io/micronaut/docs/function/aws/AnalyticsClientTest.kt b/test-suite-function-client-aws-kotlin/src/test/kotlin/io/micronaut/docs/function/aws/AnalyticsClientTest.kt new file mode 100644 index 0000000000..1539ed0c88 --- /dev/null +++ b/test-suite-function-client-aws-kotlin/src/test/kotlin/io/micronaut/docs/function/aws/AnalyticsClientTest.kt @@ -0,0 +1,25 @@ +package io.micronaut.docs.function.aws + +import io.micronaut.context.ApplicationContext +import io.micronaut.function.client.FunctionDefinition +import io.micronaut.function.client.aws.AWSInvokeRequestDefinition +import io.micronaut.test.extensions.junit5.annotation.MicronautTest +import jakarta.inject.Inject +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +@MicronautTest(startApplication = false) +internal class AnalyticsClientTest { + @Inject + lateinit var applicationContext: ApplicationContext + @Test + fun testSetupFunctionDefinitions() { + val definitions = applicationContext.getBeansOfType(FunctionDefinition::class.java) + Assertions.assertEquals(1, definitions.size) + Assertions.assertTrue(definitions.stream().findFirst().isPresent) + Assertions.assertTrue(definitions.stream().findFirst().get() is AWSInvokeRequestDefinition) + val invokeRequestDefinition = definitions.stream().findFirst().get() as AWSInvokeRequestDefinition + Assertions.assertEquals("analytics", invokeRequestDefinition.name) + //Assertions.assertEquals("AwsLambdaFunctionName", invokeRequestDefinition.invokeRequest.functionName) + } +} \ No newline at end of file diff --git a/test-suite-function-client-aws-kotlin/src/test/kotlin/io/micronaut/docs/function/client/aws/atnamed/AnalyticsClient.kt b/test-suite-function-client-aws-kotlin/src/test/kotlin/io/micronaut/docs/function/client/aws/atnamed/AnalyticsClient.kt new file mode 100644 index 0000000000..f4ad54bfb1 --- /dev/null +++ b/test-suite-function-client-aws-kotlin/src/test/kotlin/io/micronaut/docs/function/client/aws/atnamed/AnalyticsClient.kt @@ -0,0 +1,10 @@ +package io.micronaut.docs.function.client.aws.atnamed + +import io.micronaut.function.client.FunctionClient +import jakarta.inject.Named + +@FunctionClient +internal interface AnalyticsClient { + @Named("analytics") + fun visit(productId: String): String +} diff --git a/test-suite-function-client-aws-kotlin/src/test/kotlin/io/micronaut/docs/function/client/aws/methodnamed/AnalyticsClient.kt b/test-suite-function-client-aws-kotlin/src/test/kotlin/io/micronaut/docs/function/client/aws/methodnamed/AnalyticsClient.kt new file mode 100644 index 0000000000..27efa775e2 --- /dev/null +++ b/test-suite-function-client-aws-kotlin/src/test/kotlin/io/micronaut/docs/function/client/aws/methodnamed/AnalyticsClient.kt @@ -0,0 +1,8 @@ +package io.micronaut.docs.function.client.aws.methodnamed + +import io.micronaut.function.client.FunctionClient + +@FunctionClient +internal interface AnalyticsClient { + fun analytics(productId: String): String +} diff --git a/test-suite-function-client-aws-kotlin/src/test/resources/application.properties b/test-suite-function-client-aws-kotlin/src/test/resources/application.properties new file mode 100644 index 0000000000..73c92aa9d4 --- /dev/null +++ b/test-suite-function-client-aws-kotlin/src/test/resources/application.properties @@ -0,0 +1 @@ +aws.lambda.functions.analytics.function-name=AwsLambdaFunctionName \ No newline at end of file diff --git a/test-suite-function-client-aws-kotlin/src/test/resources/logback.xml b/test-suite-function-client-aws-kotlin/src/test/resources/logback.xml new file mode 100644 index 0000000000..80dcc40c8d --- /dev/null +++ b/test-suite-function-client-aws-kotlin/src/test/resources/logback.xml @@ -0,0 +1,15 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + \ No newline at end of file diff --git a/test-suite-function-client-aws/build.gradle.kts b/test-suite-function-client-aws/build.gradle.kts new file mode 100644 index 0000000000..49070ce16b --- /dev/null +++ b/test-suite-function-client-aws/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + id("java-library") + id("io.micronaut.build.internal.aws-tests-java") + id("io.micronaut.build.internal.common") +} +dependencies { + testImplementation(projects.micronautFunctionClientAws) +} + diff --git a/test-suite-function-client-aws/src/test/java/io/micronaut/docs/function/aws/AnalyticsClientTest.java b/test-suite-function-client-aws/src/test/java/io/micronaut/docs/function/aws/AnalyticsClientTest.java new file mode 100644 index 0000000000..60be703853 --- /dev/null +++ b/test-suite-function-client-aws/src/test/java/io/micronaut/docs/function/aws/AnalyticsClientTest.java @@ -0,0 +1,32 @@ +package io.micronaut.docs.function.aws; + +import io.micronaut.context.ApplicationContext; +import io.micronaut.function.client.FunctionDefinition; +import io.micronaut.function.client.aws.AWSInvokeRequestDefinition; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Test; + +import java.util.Collection; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@MicronautTest(startApplication = false) +class AnalyticsClientTest { + @Inject + ApplicationContext applicationContext; + + @Test + void testSetupFunctionDefinitions() { + Collection definitions = applicationContext.getBeansOfType(FunctionDefinition.class); + + assertEquals(1, definitions.size()); + assertTrue(definitions.stream().findFirst().isPresent()); + assertTrue(definitions.stream().findFirst().get() instanceof AWSInvokeRequestDefinition); + + AWSInvokeRequestDefinition invokeRequestDefinition = (AWSInvokeRequestDefinition) definitions.stream().findFirst().get(); + + assertEquals("analytics", invokeRequestDefinition.getName()); + } +} diff --git a/test-suite-function-client-aws/src/test/java/io/micronaut/docs/function/client/aws/atnamed/AnalyticsClient.java b/test-suite-function-client-aws/src/test/java/io/micronaut/docs/function/client/aws/atnamed/AnalyticsClient.java new file mode 100644 index 0000000000..0547f2dc2d --- /dev/null +++ b/test-suite-function-client-aws/src/test/java/io/micronaut/docs/function/client/aws/atnamed/AnalyticsClient.java @@ -0,0 +1,10 @@ +package io.micronaut.docs.function.client.aws.atnamed; + +import io.micronaut.function.client.FunctionClient; +import jakarta.inject.Named; +@FunctionClient +public interface AnalyticsClient { + + @Named("analytics") // <1> + String visit(String productId); +} diff --git a/test-suite-function-client-aws/src/test/java/io/micronaut/docs/function/client/aws/methodnamed/AnalyticsClient.java b/test-suite-function-client-aws/src/test/java/io/micronaut/docs/function/client/aws/methodnamed/AnalyticsClient.java new file mode 100644 index 0000000000..613d1c6e8a --- /dev/null +++ b/test-suite-function-client-aws/src/test/java/io/micronaut/docs/function/client/aws/methodnamed/AnalyticsClient.java @@ -0,0 +1,8 @@ +package io.micronaut.docs.function.client.aws.methodnamed; + +import io.micronaut.function.client.FunctionClient; + +@FunctionClient +public interface AnalyticsClient { + String analytics(String productId); +} diff --git a/test-suite-function-client-aws/src/test/resources/application.yml b/test-suite-function-client-aws/src/test/resources/application.yml new file mode 100644 index 0000000000..85c4f2e020 --- /dev/null +++ b/test-suite-function-client-aws/src/test/resources/application.yml @@ -0,0 +1,8 @@ +#tag::config[] +--- +aws: + lambda: + functions: + analytics: + function-name: 'AwsLambdaFunctionName' +#end::config[] \ No newline at end of file diff --git a/test-suite-function-client-aws/src/test/resources/logback.xml b/test-suite-function-client-aws/src/test/resources/logback.xml new file mode 100644 index 0000000000..80dcc40c8d --- /dev/null +++ b/test-suite-function-client-aws/src/test/resources/logback.xml @@ -0,0 +1,15 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + \ No newline at end of file diff --git a/test-suite-groovy/build.gradle.kts b/test-suite-groovy/build.gradle.kts index 5d263f217b..5d496ab412 100644 --- a/test-suite-groovy/build.gradle.kts +++ b/test-suite-groovy/build.gradle.kts @@ -9,7 +9,7 @@ dependencies { testImplementation(mnTest.micronaut.test.spock) testImplementation(platform(mn.micronaut.core.bom)) testImplementation(projects.micronautFunctionAws) - testImplementation(projects.micronautFunctionClientAws) + testImplementation(projects.micronautFunctionClientAwsV2) testRuntimeOnly(mn.snakeyaml) } diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/function/client/aws/AnalyticsClientSpec.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/function/client/aws/AnalyticsClientSpec.groovy index 00770b9725..efedeaa7cd 100644 --- a/test-suite-groovy/src/test/groovy/io/micronaut/docs/function/client/aws/AnalyticsClientSpec.groovy +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/function/client/aws/AnalyticsClientSpec.groovy @@ -2,7 +2,7 @@ package io.micronaut.docs.function.client.aws import io.micronaut.context.ApplicationContext import io.micronaut.function.client.FunctionDefinition -import io.micronaut.function.client.aws.AWSInvokeRequestDefinition +import io.micronaut.function.client.aws.v2.AwsInvokeRequestDefinition import io.micronaut.test.extensions.spock.annotation.MicronautTest import spock.lang.Specification import jakarta.inject.Inject @@ -18,13 +18,12 @@ class AnalyticsClientSpec extends Specification { expect: definitions.size() == 1 - definitions.first() instanceof AWSInvokeRequestDefinition + definitions.first() instanceof AwsInvokeRequestDefinition when: - AWSInvokeRequestDefinition invokeRequestDefinition = (AWSInvokeRequestDefinition) definitions.first() + AwsInvokeRequestDefinition invokeRequestDefinition = (AwsInvokeRequestDefinition) definitions.first() then: invokeRequestDefinition.name == 'analytics' - invokeRequestDefinition.invokeRequest.functionName == 'AwsLambdaFunctionName' } } diff --git a/test-suite-http-server-tck-function-aws-api-gateway-proxy-alb/src/test/java/io/micronaut/http/server/tck/lambda/tests/ApplicationLoadBalancerTckTestSuite.java b/test-suite-http-server-tck-function-aws-api-gateway-proxy-alb/src/test/java/io/micronaut/http/server/tck/lambda/tests/ApplicationLoadBalancerTckTestSuite.java index 30aaa93401..b51d1e885a 100644 --- a/test-suite-http-server-tck-function-aws-api-gateway-proxy-alb/src/test/java/io/micronaut/http/server/tck/lambda/tests/ApplicationLoadBalancerTckTestSuite.java +++ b/test-suite-http-server-tck-function-aws-api-gateway-proxy-alb/src/test/java/io/micronaut/http/server/tck/lambda/tests/ApplicationLoadBalancerTckTestSuite.java @@ -11,6 +11,7 @@ "io.micronaut.http.server.tck.lambda.tests" }) @ExcludeClassNamePatterns({ + "io.micronaut.http.server.tck.tests.jsonview.JsonViewsTest", // https://github.com/micronaut-projects/micronaut-servlet/pull/826 "io.micronaut.http.server.tck.tests.hateoas.JsonErrorTest", "io.micronaut.http.server.tck.tests.hateoas.VndErrorTest", "io.micronaut.http.server.tck.tests.FilterProxyTest", // Immmutable request diff --git a/test-suite-http-server-tck-function-aws-api-gateway-proxy-payloadv1/src/test/java/io/micronaut/http/server/tck/lambda/tests/FunctionAwsApiGatewayProxyV1HttpServerTestSuite.java b/test-suite-http-server-tck-function-aws-api-gateway-proxy-payloadv1/src/test/java/io/micronaut/http/server/tck/lambda/tests/FunctionAwsApiGatewayProxyV1HttpServerTestSuite.java index 19a38ad4a0..6afd909126 100644 --- a/test-suite-http-server-tck-function-aws-api-gateway-proxy-payloadv1/src/test/java/io/micronaut/http/server/tck/lambda/tests/FunctionAwsApiGatewayProxyV1HttpServerTestSuite.java +++ b/test-suite-http-server-tck-function-aws-api-gateway-proxy-payloadv1/src/test/java/io/micronaut/http/server/tck/lambda/tests/FunctionAwsApiGatewayProxyV1HttpServerTestSuite.java @@ -1,9 +1,6 @@ package io.micronaut.http.server.tck.lambda.tests; -import org.junit.platform.suite.api.ExcludeClassNamePatterns; -import org.junit.platform.suite.api.SelectPackages; -import org.junit.platform.suite.api.Suite; -import org.junit.platform.suite.api.SuiteDisplayName; +import org.junit.platform.suite.api.*; @Suite @SelectPackages({ @@ -11,6 +8,7 @@ "io.micronaut.http.server.tck.lambda.tests" }) @ExcludeClassNamePatterns({ + "io.micronaut.http.server.tck.tests.jsonview.JsonViewsTest", // https://github.com/micronaut-projects/micronaut-servlet/pull/826 "io.micronaut.http.server.tck.tests.hateoas.JsonErrorTest", "io.micronaut.http.server.tck.tests.hateoas.VndErrorTest", "io.micronaut.http.server.tck.tests.filter.options.OptionsFilterTest", diff --git a/test-suite-http-server-tck-function-aws-api-gateway-proxy-payloadv2/src/test/java/io/micronaut/http/server/tck/lambda/tests/FunctionAwsApiGatewayProxyV2HttpServerTestSuite.java b/test-suite-http-server-tck-function-aws-api-gateway-proxy-payloadv2/src/test/java/io/micronaut/http/server/tck/lambda/tests/FunctionAwsApiGatewayProxyV2HttpServerTestSuite.java index 7241526e94..3b0b7e79ff 100644 --- a/test-suite-http-server-tck-function-aws-api-gateway-proxy-payloadv2/src/test/java/io/micronaut/http/server/tck/lambda/tests/FunctionAwsApiGatewayProxyV2HttpServerTestSuite.java +++ b/test-suite-http-server-tck-function-aws-api-gateway-proxy-payloadv2/src/test/java/io/micronaut/http/server/tck/lambda/tests/FunctionAwsApiGatewayProxyV2HttpServerTestSuite.java @@ -1,9 +1,6 @@ package io.micronaut.http.server.tck.lambda.tests; -import org.junit.platform.suite.api.ExcludeClassNamePatterns; -import org.junit.platform.suite.api.SelectPackages; -import org.junit.platform.suite.api.Suite; -import org.junit.platform.suite.api.SuiteDisplayName; +import org.junit.platform.suite.api.*; @Suite @SelectPackages({ @@ -11,6 +8,7 @@ "io.micronaut.http.server.tck.lambda.tests" }) @ExcludeClassNamePatterns({ + "io.micronaut.http.server.tck.tests.jsonview.JsonViewsTest", // https://github.com/micronaut-projects/micronaut-servlet/pull/826 "io.micronaut.http.server.tck.tests.hateoas.JsonErrorTest", "io.micronaut.http.server.tck.tests.hateoas.VndErrorTest", "io.micronaut.http.server.tck.tests.filter.options.OptionsFilterTest", diff --git a/test-suite-http-server-tck-function-aws-api-proxy-test/src/test/java/io/micronaut/http/server/tck/lambda/tests/MicronautLambdaHandlerHttpServerTestSuite.java b/test-suite-http-server-tck-function-aws-api-proxy-test/src/test/java/io/micronaut/http/server/tck/lambda/tests/MicronautLambdaHandlerHttpServerTestSuite.java index 805e205716..af790f55eb 100644 --- a/test-suite-http-server-tck-function-aws-api-proxy-test/src/test/java/io/micronaut/http/server/tck/lambda/tests/MicronautLambdaHandlerHttpServerTestSuite.java +++ b/test-suite-http-server-tck-function-aws-api-proxy-test/src/test/java/io/micronaut/http/server/tck/lambda/tests/MicronautLambdaHandlerHttpServerTestSuite.java @@ -11,6 +11,8 @@ "io.micronaut.http.server.tck.lambda.tests" }) @ExcludeClassNamePatterns({ + "io.micronaut.http.server.tck.tests.forms.FormUrlEncodedBodyInRequestFilterTest", + "io.micronaut.http.server.tck.tests.jsonview.JsonViewsTest", // https://github.com/micronaut-projects/micronaut-servlet/pull/826 "io.micronaut.http.server.tck.tests.forms.FormsSubmissionsWithListsTest", "io.micronaut.http.server.tck.tests.filter.options.OptionsFilterTest", "io.micronaut.http.server.tck.tests.LocalErrorReadingBodyTest", // Binding body different type (e.g. a String in error handler) diff --git a/test-suite-kotlin/build.gradle.kts b/test-suite-kotlin/build.gradle.kts index c423ba56a2..175c4cbe43 100644 --- a/test-suite-kotlin/build.gradle.kts +++ b/test-suite-kotlin/build.gradle.kts @@ -14,7 +14,7 @@ dependencies { testRuntimeOnly(libs.junit.jupiter.engine) testImplementation(projects.micronautFunctionAws) testImplementation(libs.kotlin.stdlib.jdk8) - testImplementation(projects.micronautFunctionClientAws) + testImplementation(projects.micronautFunctionClientAwsV2) testRuntimeOnly(mn.snakeyaml) } diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/function/aws/AnalyticsClientTest.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/function/aws/AnalyticsClientTest.kt index 1539ed0c88..5418a00b27 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/function/aws/AnalyticsClientTest.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/function/aws/AnalyticsClientTest.kt @@ -2,7 +2,7 @@ package io.micronaut.docs.function.aws import io.micronaut.context.ApplicationContext import io.micronaut.function.client.FunctionDefinition -import io.micronaut.function.client.aws.AWSInvokeRequestDefinition +import io.micronaut.function.client.aws.v2.AwsInvokeRequestDefinition import io.micronaut.test.extensions.junit5.annotation.MicronautTest import jakarta.inject.Inject import org.junit.jupiter.api.Assertions @@ -17,8 +17,8 @@ internal class AnalyticsClientTest { val definitions = applicationContext.getBeansOfType(FunctionDefinition::class.java) Assertions.assertEquals(1, definitions.size) Assertions.assertTrue(definitions.stream().findFirst().isPresent) - Assertions.assertTrue(definitions.stream().findFirst().get() is AWSInvokeRequestDefinition) - val invokeRequestDefinition = definitions.stream().findFirst().get() as AWSInvokeRequestDefinition + Assertions.assertTrue(definitions.stream().findFirst().get() is AwsInvokeRequestDefinition) + val invokeRequestDefinition = definitions.stream().findFirst().get() as AwsInvokeRequestDefinition Assertions.assertEquals("analytics", invokeRequestDefinition.name) //Assertions.assertEquals("AwsLambdaFunctionName", invokeRequestDefinition.invokeRequest.functionName) } diff --git a/test-suite/build.gradle.kts b/test-suite/build.gradle.kts index f6339f4ffa..98872291df 100644 --- a/test-suite/build.gradle.kts +++ b/test-suite/build.gradle.kts @@ -5,7 +5,7 @@ plugins { } dependencies { testImplementation(projects.micronautFunctionAws) - testImplementation(projects.micronautFunctionClientAws) + testImplementation(projects.micronautFunctionClientAwsV2) } tasks { diff --git a/test-suite/src/test/java/io/micronaut/docs/function/aws/AnalyticsClientTest.java b/test-suite/src/test/java/io/micronaut/docs/function/aws/AnalyticsClientTest.java index dec6690f8c..8f35476cd4 100644 --- a/test-suite/src/test/java/io/micronaut/docs/function/aws/AnalyticsClientTest.java +++ b/test-suite/src/test/java/io/micronaut/docs/function/aws/AnalyticsClientTest.java @@ -2,7 +2,7 @@ import io.micronaut.context.ApplicationContext; import io.micronaut.function.client.FunctionDefinition; -import io.micronaut.function.client.aws.AWSInvokeRequestDefinition; +import io.micronaut.function.client.aws.v2.AwsInvokeRequestDefinition; import io.micronaut.test.extensions.junit5.annotation.MicronautTest; import jakarta.inject.Inject; import org.junit.jupiter.api.Test; @@ -23,9 +23,9 @@ void testSetupFunctionDefinitions() { assertEquals(1, definitions.size()); assertTrue(definitions.stream().findFirst().isPresent()); - assertTrue(definitions.stream().findFirst().get() instanceof AWSInvokeRequestDefinition); + assertTrue(definitions.stream().findFirst().get() instanceof AwsInvokeRequestDefinition); - AWSInvokeRequestDefinition invokeRequestDefinition = (AWSInvokeRequestDefinition) definitions.stream().findFirst().get(); + AwsInvokeRequestDefinition invokeRequestDefinition = (AwsInvokeRequestDefinition) definitions.stream().findFirst().get(); assertEquals("analytics", invokeRequestDefinition.getName()); //assertEquals("AwsLambdaFunctionName", invokeRequestDefinition.getInvokeRequest().getFunctionName()); diff --git a/test-suite/src/test/resources/application.properties b/test-suite/src/test/resources/application.properties new file mode 100644 index 0000000000..73c92aa9d4 --- /dev/null +++ b/test-suite/src/test/resources/application.properties @@ -0,0 +1 @@ +aws.lambda.functions.analytics.function-name=AwsLambdaFunctionName \ No newline at end of file