From e08ac646fce2b14eaeb999cd759fb7c0a9713847 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Wed, 27 Aug 2025 11:01:14 +0000 Subject: [PATCH 01/17] Add GraalVM profile to powertools-cloudformation. Make unit tests compatible with subclass mock maker and native mode. --- GraalVM.md | 26 +++ powertools-cloudformation/pom.xml | 87 +++++++++ .../CloudFormationResponse.java | 121 +++++++----- .../AbstractCustomResourceHandlerTest.java | 180 +++--------------- .../CloudFormationIntegrationTest.java | 153 ++++----------- .../CloudFormationResponseTest.java | 88 ++++----- .../ExceptionThrowingHandler.java | 31 +++ .../ExpectedStatusResourceHandler.java | 55 ++++++ .../ExplicitSuccessResponseHandler.java | 31 +++ .../FailToSendResponseHandler.java | 40 ++++ .../cloudformation/FailedResponseHandler.java | 31 +++ .../cloudformation/FailedSendHandler.java | 25 +++ .../NoOpCustomResourceHandler.java | 33 ++++ .../NullCustomResourceHandler.java | 47 +++++ .../cloudformation/ResponseTest.java | 8 +- .../SuccessResponseHandler.java | 31 +++ .../cloudformation/SuccessfulSendHandler.java | 25 +++ .../NoPhysicalResourceIdSetHandler.java | 1 + .../PhysicalResourceIdSetHandler.java | 1 + .../RuntimeExceptionThrownHandler.java | 1 + .../test/resources/simplelogger.properties | 7 + 21 files changed, 660 insertions(+), 362 deletions(-) create mode 100644 powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/ExceptionThrowingHandler.java create mode 100644 powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/ExpectedStatusResourceHandler.java create mode 100644 powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/ExplicitSuccessResponseHandler.java create mode 100644 powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/FailToSendResponseHandler.java create mode 100644 powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/FailedResponseHandler.java create mode 100644 powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/FailedSendHandler.java create mode 100644 powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/NoOpCustomResourceHandler.java create mode 100644 powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/NullCustomResourceHandler.java create mode 100644 powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/SuccessResponseHandler.java create mode 100644 powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/SuccessfulSendHandler.java create mode 100644 powertools-cloudformation/src/test/resources/simplelogger.properties diff --git a/GraalVM.md b/GraalVM.md index 56c72d96f..bbddb5e3b 100644 --- a/GraalVM.md +++ b/GraalVM.md @@ -56,5 +56,31 @@ java.lang.InternalError: com.oracle.svm.core.jdk.UnsupportedFeatureError: Defini ``` - This has been [fixed](https://github.com/apache/logging-log4j2/discussions/2364#discussioncomment-8950077) in Log4j 2.24.x. PT has been updated to use this version of Log4j +3. **Test Class Organization** + - **Issue**: Anonymous inner classes and lambda expressions in Mockito matchers cause `NoSuchMethodError` in GraalVM native tests + - **Solution**: + - Extract static inner test classes to separate concrete classes in the same package as the class under test + - Replace lambda expressions in `ArgumentMatcher` with concrete implementations + - Use `mockito-subclass` dependency in GraalVM profiles + - **Example**: Replace `argThat(resp -> resp.getStatus() != expectedStatus)` with: + ```java + argThat(new ArgumentMatcher() { + @Override + public boolean matches(Response resp) { + return resp != null && resp.getStatus() != expectedStatus; + } + }) + ``` + +4. **Package Visibility Issues** + - **Issue**: Test handler classes cannot access package-private methods when placed in subpackages + - **Solution**: Place test handler classes in the same package as the class under test, not in subpackages like `handlers/` + - **Example**: Use `software.amazon.lambda.powertools.cloudformation` instead of `software.amazon.lambda.powertools.cloudformation.handlers` + +5. **Test Stubs Best Practice** + - **Best Practice**: Avoid mocking where possible and use concrete test stubs provided by `powertools-common` package + - **Solution**: Use `TestLambdaContext` and other test stubs from `powertools-common` test-jar instead of Mockito mocks + - **Implementation**: Add `powertools-common` test-jar dependency and replace `mock(Context.class)` with `new TestLambdaContext()` + ## Reference Implementation Working example is available in the [examples](examples/powertools-examples-core-utilities/sam-graalvm). diff --git a/powertools-cloudformation/pom.xml b/powertools-cloudformation/pom.xml index 355e8a3ed..862f5832e 100644 --- a/powertools-cloudformation/pom.xml +++ b/powertools-cloudformation/pom.xml @@ -95,5 +95,92 @@ wiremock test + + software.amazon.lambda + powertools-common + ${project.version} + test-jar + test + + + + + generate-graalvm-files + + + org.mockito + mockito-subclass + test + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + -Dorg.graalvm.nativeimage.imagecode=agent + -agentlib:native-image-agent=config-output-dir=src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-cloudformation,experimental-class-define-support + --add-opens java.base/java.util=ALL-UNNAMED + --add-opens java.base/java.lang=ALL-UNNAMED + + + + + + + + graalvm-native + + + org.mockito + mockito-subclass + test + + + + + + org.graalvm.buildtools + native-maven-plugin + 0.11.0 + true + + + test-native + + test + + test + + + + powertools-cloudformation + + --add-opens java.base/java.util=ALL-UNNAMED + --add-opens java.base/java.lang=ALL-UNNAMED + --enable-url-protocols=http + --no-fallback + --verbose + --native-image-info + -H:+UnlockExperimentalVMOptions + -H:+ReportExceptionStackTraces + + + + + + + + + + + + + src/main/resources + + + diff --git a/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponse.java b/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponse.java index 404137802..a52bded38 100644 --- a/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponse.java +++ b/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponse.java @@ -14,12 +14,6 @@ package software.amazon.lambda.powertools.cloudformation; -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.PropertyNamingStrategies; -import com.fasterxml.jackson.databind.node.ObjectNode; import java.io.IOException; import java.net.URI; import java.util.Collections; @@ -27,8 +21,17 @@ import java.util.List; import java.util.Map; import java.util.Objects; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.node.ObjectNode; + import software.amazon.awssdk.http.Header; import software.amazon.awssdk.http.HttpExecuteRequest; import software.amazon.awssdk.http.HttpExecuteResponse; @@ -39,20 +42,24 @@ import software.amazon.awssdk.utils.StringUtils; /** - * Client for sending responses to AWS CloudFormation custom resources by way of a response URL, which is an Amazon S3 + * Client for sending responses to AWS CloudFormation custom resources by way of + * a response URL, which is an Amazon S3 * pre-signed URL. *

- * See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cfn-lambda-function-code-cfnresponsemodule.html + * See + * https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cfn-lambda-function-code-cfnresponsemodule.html *

- * This class is thread-safe provided the SdkHttpClient instance used is also thread-safe. + * This class is thread-safe provided the SdkHttpClient instance used is also + * thread-safe. */ -class CloudFormationResponse { +public class CloudFormationResponse { private static final Logger LOG = LoggerFactory.getLogger(CloudFormationResponse.class); private final SdkHttpClient client; /** - * Creates a new CloudFormationResponse that uses the provided HTTP client and default JSON serialization format. + * Creates a new CloudFormationResponse that uses the provided HTTP client and + * default JSON serialization format. * * @param client HTTP client to use for sending requests; cannot be null */ @@ -70,36 +77,46 @@ SdkHttpClient getClient() { } /** - * Forwards a response containing a custom payload to the target resource specified by the event. The payload is + * Forwards a response containing a custom payload to the target resource + * specified by the event. The payload is * formed from the event and context data. Status is assumed to be SUCCESS. * * @param event custom CF resource event. Cannot be null. - * @param context used to specify when the function and any callbacks have completed execution, or to - * access information from within the Lambda execution environment. Cannot be null. + * @param context used to specify when the function and any callbacks have + * completed execution, or to + * access information from within the Lambda execution + * environment. Cannot be null. * @return the response object * @throws IOException when unable to send the request - * @throws CustomResourceResponseException when unable to synthesize or serialize the response payload + * @throws CustomResourceResponseException when unable to synthesize or + * serialize the response payload */ public HttpExecuteResponse send(CloudFormationCustomResourceEvent event, - Context context) throws IOException, CustomResourceResponseException { + Context context) throws IOException, CustomResourceResponseException { return send(event, context, null); } /** - * Forwards a response containing a custom payload to the target resource specified by the event. The payload is + * Forwards a response containing a custom payload to the target resource + * specified by the event. The payload is * formed from the event, context, and response data. * * @param event custom CF resource event. Cannot be null. - * @param context used to specify when the function and any callbacks have completed execution, or to - * access information from within the Lambda execution environment. Cannot be null. - * @param responseData response to send, e.g. a list of name-value pairs. If null, an empty success is assumed. + * @param context used to specify when the function and any callbacks have + * completed execution, or to + * access information from within the Lambda execution + * environment. Cannot be null. + * @param responseData response to send, e.g. a list of name-value pairs. If + * null, an empty success is assumed. * @return the response object - * @throws IOException when unable to generate or send the request - * @throws CustomResourceResponseException when unable to serialize the response payload + * @throws IOException when unable to generate or send the + * request + * @throws CustomResourceResponseException when unable to serialize the response + * payload */ public HttpExecuteResponse send(CloudFormationCustomResourceEvent event, - Context context, - Response responseData) throws IOException, CustomResourceResponseException { + Context context, + Response responseData) throws IOException, CustomResourceResponseException { // no need to explicitly close in-memory stream StringInputStream stream = responseBodyStream(event, context, responseData); URI uri = URI.create(event.getResponseUrl()); @@ -129,20 +146,23 @@ protected Map> headers(int contentLength) { } /** - * Returns the response body as an input stream, for supplying with the HTTP request to the custom resource. + * Returns the response body as an input stream, for supplying with the HTTP + * request to the custom resource. *

- * If PhysicalResourceId is null at this point it will be replaced with the Lambda LogStreamName. + * If PhysicalResourceId is null at this point it will be replaced with the + * Lambda LogStreamName. * - * @throws CustomResourceResponseException if unable to generate the response stream + * @throws CustomResourceResponseException if unable to generate the response + * stream */ StringInputStream responseBodyStream(CloudFormationCustomResourceEvent event, - Context context, - Response resp) throws CustomResourceResponseException { + Context context, + Response resp) throws CustomResourceResponseException { try { String reason = "See the details in CloudWatch Log Stream: " + context.getLogStreamName(); if (resp == null) { - String physicalResourceId = event.getPhysicalResourceId() != null ? event.getPhysicalResourceId() : - context.getLogStreamName(); + String physicalResourceId = event.getPhysicalResourceId() != null ? event.getPhysicalResourceId() + : context.getLogStreamName(); ResponseBody body = new ResponseBody(event, Response.Status.SUCCESS, physicalResourceId, false, reason); LOG.debug("ResponseBody: {}", body); @@ -152,12 +172,12 @@ StringInputStream responseBodyStream(CloudFormationCustomResourceEvent event, if (!StringUtils.isBlank(resp.getReason())) { reason = resp.getReason(); } - String physicalResourceId = resp.getPhysicalResourceId() != null ? resp.getPhysicalResourceId() : - event.getPhysicalResourceId() != null ? event.getPhysicalResourceId() : - context.getLogStreamName(); + String physicalResourceId = resp.getPhysicalResourceId() != null ? resp.getPhysicalResourceId() + : event.getPhysicalResourceId() != null ? event.getPhysicalResourceId() + : context.getLogStreamName(); - ResponseBody body = - new ResponseBody(event, resp.getStatus(), physicalResourceId, resp.isNoEcho(), reason); + ResponseBody body = new ResponseBody(event, resp.getStatus(), physicalResourceId, resp.isNoEcho(), + reason); LOG.debug("ResponseBody: {}", body); ObjectNode node = body.toObjectNode(resp.getJsonNode()); return new StringInputStream(node.toString()); @@ -169,10 +189,14 @@ StringInputStream responseBodyStream(CloudFormationCustomResourceEvent event, } /** - * Internal representation of the payload to be sent to the event target URL. Retains all properties of the payload - * except for "Data". This is done so that the serialization of the non-"Data" properties and the serialization of - * the value of "Data" can be handled by separate ObjectMappers, if need be. The former properties are dictated by - * the custom resource but the latter is dictated by the implementor of the custom resource handler. + * Internal representation of the payload to be sent to the event target URL. + * Retains all properties of the payload + * except for "Data". This is done so that the serialization of the non-"Data" + * properties and the serialization of + * the value of "Data" can be handled by separate ObjectMappers, if need be. The + * former properties are dictated by + * the custom resource but the latter is dictated by the implementor of the + * custom resource handler. */ @SuppressWarnings("unused") static class ResponseBody { @@ -189,10 +213,10 @@ static class ResponseBody { private final boolean noEcho; ResponseBody(CloudFormationCustomResourceEvent event, - Response.Status responseStatus, - String physicalResourceId, - boolean noEcho, - String reason) { + Response.Status responseStatus, + String physicalResourceId, + boolean noEcho, + String reason) { Objects.requireNonNull(event, "CloudFormationCustomResourceEvent cannot be null"); this.physicalResourceId = physicalResourceId; @@ -233,10 +257,13 @@ public boolean isNoEcho() { } /** - * Returns this ResponseBody as an ObjectNode with the provided JsonNode as the value of its "Data" property. + * Returns this ResponseBody as an ObjectNode with the provided JsonNode as the + * value of its "Data" property. * - * @param dataNode the value of the "Data" property for the returned node; may be null - * @return an ObjectNode representation of this ResponseBody and the provided dataNode + * @param dataNode the value of the "Data" property for the returned node; may + * be null + * @return an ObjectNode representation of this ResponseBody and the provided + * dataNode */ ObjectNode toObjectNode(JsonNode dataNode) { ObjectNode node = MAPPER.valueToTree(this); diff --git a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/AbstractCustomResourceHandlerTest.java b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/AbstractCustomResourceHandlerTest.java index 1e399ef6f..9d0669d43 100644 --- a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/AbstractCustomResourceHandlerTest.java +++ b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/AbstractCustomResourceHandlerTest.java @@ -16,7 +16,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.mock; @@ -25,14 +24,17 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent; import java.io.IOException; + import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent; + import software.amazon.awssdk.http.SdkHttpClient; -import software.amazon.lambda.powertools.cloudformation.Response.Status; +import software.amazon.lambda.powertools.common.stubs.TestLambdaContext; public class AbstractCustomResourceHandlerTest { @@ -68,11 +70,11 @@ void defaultAndCustomSdkHttpClients() { } @ParameterizedTest - @CsvSource(value = {"Create,1,0,0", "Update,0,1,0", "Delete,0,0,1"}, delimiter = ',') + @CsvSource(value = { "Create,1,0,0", "Update,0,1,0", "Delete,0,0,1" }, delimiter = ',') void eventsDelegateToCorrectHandlerMethod(String eventType, int createCount, int updateCount, int deleteCount) { AbstractCustomResourceHandler handler = spy(new NoOpCustomResourceHandler()); - Context context = mock(Context.class); + Context context = new TestLambdaContext(); handler.handleRequest(eventOfType(eventType), context); verify(handler, times(createCount)).create(any(), eq(context)); @@ -84,7 +86,7 @@ void eventsDelegateToCorrectHandlerMethod(String eventType, int createCount, int void eventOfUnknownRequestTypeSendEmptySuccess() { AbstractCustomResourceHandler handler = spy(new NoOpCustomResourceHandler()); - Context context = mock(Context.class); + Context context = new TestLambdaContext(); CloudFormationCustomResourceEvent event = eventOfType("UNKNOWN"); handler.handleRequest(event, context); @@ -96,16 +98,9 @@ void eventOfUnknownRequestTypeSendEmptySuccess() { @Test void defaultStatusResponseSendsSuccess() { - ExpectedStatusResourceHandler handler = spy(new ExpectedStatusResourceHandler(Status.SUCCESS) { - @Override - protected Response create(CloudFormationCustomResourceEvent event, Context context) { - return Response.builder() - .value("whatever") - .build(); - } - }); - - Context context = mock(Context.class); + SuccessResponseHandler handler = spy(new SuccessResponseHandler()); + + Context context = new TestLambdaContext(); CloudFormationCustomResourceEvent event = eventOfType("Create"); Response response = handler.handleRequest(event, context); @@ -116,17 +111,9 @@ protected Response create(CloudFormationCustomResourceEvent event, Context conte @Test void explicitResponseWithStatusSuccessSendsSuccess() { - ExpectedStatusResourceHandler handler = spy(new ExpectedStatusResourceHandler(Status.SUCCESS) { - @Override - protected Response create(CloudFormationCustomResourceEvent event, Context context) { - return Response.builder() - .value("whatever") - .status(Status.SUCCESS) - .build(); - } - }); - - Context context = mock(Context.class); + ExplicitSuccessResponseHandler handler = spy(new ExplicitSuccessResponseHandler()); + + Context context = new TestLambdaContext(); CloudFormationCustomResourceEvent event = eventOfType("Create"); Response response = handler.handleRequest(event, context); @@ -137,17 +124,9 @@ protected Response create(CloudFormationCustomResourceEvent event, Context conte @Test void explicitResponseWithStatusFailedSendsFailure() { - ExpectedStatusResourceHandler handler = spy(new ExpectedStatusResourceHandler(Status.FAILED) { - @Override - protected Response create(CloudFormationCustomResourceEvent event, Context context) { - return Response.builder() - .value("whatever") - .status(Status.FAILED) - .build(); - } - }); - - Context context = mock(Context.class); + FailedResponseHandler handler = spy(new FailedResponseHandler()); + + Context context = new TestLambdaContext(); CloudFormationCustomResourceEvent event = eventOfType("Create"); Response response = handler.handleRequest(event, context); @@ -158,14 +137,9 @@ protected Response create(CloudFormationCustomResourceEvent event, Context conte @Test void exceptionWhenGeneratingResponseSendsFailure() { - ExpectedStatusResourceHandler handler = spy(new ExpectedStatusResourceHandler(Status.FAILED) { - @Override - protected Response create(CloudFormationCustomResourceEvent event, Context context) { - throw new RuntimeException("This exception is intentional for testing"); - } - }); - - Context context = mock(Context.class); + ExceptionThrowingHandler handler = spy(new ExceptionThrowingHandler()); + + Context context = new TestLambdaContext(); CloudFormationCustomResourceEvent event = eventOfType("Create"); Response response = handler.handleRequest(event, context); @@ -178,14 +152,9 @@ protected Response create(CloudFormationCustomResourceEvent event, Context conte @Test void exceptionWhenSendingResponseInvokesOnSendFailure() { // a custom handler that builds response successfully but fails to send it - FailToSendResponseHandler handler = spy(new FailToSendResponseHandler() { - @Override - protected Response create(CloudFormationCustomResourceEvent event, Context context) { - return Response.builder().value("Failure happens on send").build(); - } - }); - - Context context = mock(Context.class); + SuccessfulSendHandler handler = spy(new SuccessfulSendHandler()); + + Context context = new TestLambdaContext(); CloudFormationCustomResourceEvent event = eventOfType("Create"); Response response = handler.handleRequest(event, context); @@ -197,15 +166,11 @@ protected Response create(CloudFormationCustomResourceEvent event, Context conte @Test void bothResponseGenerationAndSendFail() { - // a custom handler that fails to build response _and_ fails to send a FAILED response - FailToSendResponseHandler handler = spy(new FailToSendResponseHandler() { - @Override - protected Response create(CloudFormationCustomResourceEvent event, Context context) { - throw new RuntimeException("This exception is intentional for testing"); - } - }); - - Context context = mock(Context.class); + // a custom handler that fails to build response _and_ fails to send a FAILED + // response + FailedSendHandler handler = spy(new FailedSendHandler()); + + Context context = new TestLambdaContext(); CloudFormationCustomResourceEvent event = eventOfType("Create"); Response response = handler.handleRequest(event, context); @@ -214,91 +179,4 @@ protected Response create(CloudFormationCustomResourceEvent event, Context conte .onSendFailure(eq(event), eq(context), isNull(), any(IOException.class)); } - /** - * Bare-bones implementation that returns null for abstract methods. - */ - static class NullCustomResourceHandler extends AbstractCustomResourceHandler { - NullCustomResourceHandler() { - } - - NullCustomResourceHandler(SdkHttpClient client) { - super(client); - } - - @Override - protected Response create(CloudFormationCustomResourceEvent event, Context context) { - return null; - } - - @Override - protected Response update(CloudFormationCustomResourceEvent event, Context context) { - return null; - } - - @Override - protected Response delete(CloudFormationCustomResourceEvent event, Context context) { - return null; - } - } - - /** - * Uses a mocked CloudFormationResponse to avoid sending actual HTTP requests. - */ - static class NoOpCustomResourceHandler extends NullCustomResourceHandler { - - NoOpCustomResourceHandler() { - super(mock(SdkHttpClient.class)); - } - - @Override - protected CloudFormationResponse buildResponseClient() { - return mock(CloudFormationResponse.class); - } - } - - /** - * Creates a handler that will expect the Response to be sent with an expected status. Will throw an AssertionError - * if the method is sent with an unexpected status. - */ - static class ExpectedStatusResourceHandler extends NoOpCustomResourceHandler { - private final Status expectedStatus; - - ExpectedStatusResourceHandler(Status expectedStatus) { - this.expectedStatus = expectedStatus; - } - - @Override - protected CloudFormationResponse buildResponseClient() { - // create a CloudFormationResponse that fails if invoked with unexpected status - CloudFormationResponse cfnResponse = mock(CloudFormationResponse.class); - try { - when(cfnResponse.send(any(), any(), argThat(resp -> resp.getStatus() != expectedStatus))) - .thenThrow(new AssertionError("Expected response's status to be " + expectedStatus)); - } catch (IOException | CustomResourceResponseException e) { - // this should never happen - throw new RuntimeException("Unexpected mocking exception", e); - } - return cfnResponse; - } - } - - /** - * Always fails to send the response - */ - static class FailToSendResponseHandler extends NoOpCustomResourceHandler { - @Override - protected CloudFormationResponse buildResponseClient() { - CloudFormationResponse cfnResponse = mock(CloudFormationResponse.class); - try { - when(cfnResponse.send(any(), any())) - .thenThrow(new IOException("Intentional send failure")); - when(cfnResponse.send(any(), any(), any())) - .thenThrow(new IOException("Intentional send failure")); - } catch (IOException | CustomResourceResponseException e) { - // this should never happen - throw new RuntimeException("Unexpected mocking exception", e); - } - return cfnResponse; - } - } } diff --git a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationIntegrationTest.java b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationIntegrationTest.java index ce45d3afc..913740dfa 100644 --- a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationIntegrationTest.java +++ b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationIntegrationTest.java @@ -23,62 +23,42 @@ import static com.github.tomakehurst.wiremock.client.WireMock.verify; import static org.assertj.core.api.Assertions.assertThat; -import com.amazonaws.services.lambda.runtime.ClientContext; -import com.amazonaws.services.lambda.runtime.CognitoIdentity; -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.LambdaLogger; -import com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent; -import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; -import com.github.tomakehurst.wiremock.junit5.WireMockTest; import java.util.UUID; + import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; + +import com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent; +import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; +import com.github.tomakehurst.wiremock.junit5.WireMockTest; + import software.amazon.lambda.powertools.cloudformation.handlers.NoPhysicalResourceIdSetHandler; import software.amazon.lambda.powertools.cloudformation.handlers.PhysicalResourceIdSetHandler; import software.amazon.lambda.powertools.cloudformation.handlers.RuntimeExceptionThrownHandler; +import software.amazon.lambda.powertools.common.stubs.TestLambdaContext; @WireMockTest public class CloudFormationIntegrationTest { public static final String PHYSICAL_RESOURCE_ID = UUID.randomUUID().toString(); - public static final String LOG_STREAM_NAME = "FakeLogStreamName"; - - private static CloudFormationCustomResourceEvent updateEventWithPhysicalResourceId(int httpPort, - String physicalResourceId) { - CloudFormationCustomResourceEvent.CloudFormationCustomResourceEventBuilder builder = baseEvent(httpPort); - - builder.withPhysicalResourceId(physicalResourceId); - builder.withRequestType("Update"); - - return builder.build(); - } - - private static CloudFormationCustomResourceEvent deleteEventWithPhysicalResourceId(int httpPort, - String physicalResourceId) { - CloudFormationCustomResourceEvent.CloudFormationCustomResourceEventBuilder builder = baseEvent(httpPort); - - builder.withPhysicalResourceId(physicalResourceId); - builder.withRequestType("Delete"); - - return builder.build(); - } + public static final String LOG_STREAM_NAME = "test-log-stream"; private static CloudFormationCustomResourceEvent.CloudFormationCustomResourceEventBuilder baseEvent(int httpPort) { - CloudFormationCustomResourceEvent.CloudFormationCustomResourceEventBuilder builder = - CloudFormationCustomResourceEvent.builder() - .withResponseUrl("http://localhost:" + httpPort + "/") - .withStackId("123") - .withRequestId("234") - .withLogicalResourceId("345"); + CloudFormationCustomResourceEvent.CloudFormationCustomResourceEventBuilder builder = CloudFormationCustomResourceEvent + .builder() + .withResponseUrl("http://localhost:" + httpPort + "/") + .withStackId("123") + .withRequestId("234") + .withLogicalResourceId("345"); return builder; } @ParameterizedTest - @ValueSource(strings = {"Update", "Delete"}) + @ValueSource(strings = { "Update", "Delete" }) void physicalResourceIdTakenFromRequestForUpdateOrDeleteWhenUserSpecifiesNull(String requestType, - WireMockRuntimeInfo wmRuntimeInfo) { + WireMockRuntimeInfo wmRuntimeInfo) { stubFor(put("/").willReturn(ok())); NoPhysicalResourceIdSetHandler handler = new NoPhysicalResourceIdSetHandler(); @@ -89,18 +69,17 @@ void physicalResourceIdTakenFromRequestForUpdateOrDeleteWhenUserSpecifiesNull(St .withRequestType(requestType) .build(); - handler.handleRequest(event, new FakeContext()); + handler.handleRequest(event, new TestLambdaContext()); verify(putRequestedFor(urlPathMatching("/")) .withRequestBody(matchingJsonPath("[?(@.Status == 'SUCCESS')]")) - .withRequestBody(matchingJsonPath("[?(@.PhysicalResourceId == '" + PHYSICAL_RESOURCE_ID + "')]")) - ); + .withRequestBody(matchingJsonPath("[?(@.PhysicalResourceId == '" + PHYSICAL_RESOURCE_ID + "')]"))); } @ParameterizedTest - @ValueSource(strings = {"Update", "Delete"}) + @ValueSource(strings = { "Update", "Delete" }) void physicalResourceIdDoesNotChangeWhenRuntimeExceptionThrownWhenUpdatingOrDeleting(String requestType, - WireMockRuntimeInfo wmRuntimeInfo) { + WireMockRuntimeInfo wmRuntimeInfo) { stubFor(put("/").willReturn(ok())); RuntimeExceptionThrownHandler handler = new RuntimeExceptionThrownHandler(); @@ -111,12 +90,11 @@ void physicalResourceIdDoesNotChangeWhenRuntimeExceptionThrownWhenUpdatingOrDele .withRequestType(requestType) .build(); - handler.handleRequest(event, new FakeContext()); + handler.handleRequest(event, new TestLambdaContext()); verify(putRequestedFor(urlPathMatching("/")) .withRequestBody(matchingJsonPath("[?(@.Status == 'FAILED')]")) - .withRequestBody(matchingJsonPath("[?(@.PhysicalResourceId == '" + PHYSICAL_RESOURCE_ID + "')]")) - ); + .withRequestBody(matchingJsonPath("[?(@.PhysicalResourceId == '" + PHYSICAL_RESOURCE_ID + "')]"))); } @Test @@ -127,16 +105,15 @@ void runtimeExceptionThrownOnCreateSendsLogStreamNameAsPhysicalResourceId(WireMo CloudFormationCustomResourceEvent createEvent = baseEvent(wmRuntimeInfo.getHttpPort()) .withRequestType("Create") .build(); - handler.handleRequest(createEvent, new FakeContext()); + handler.handleRequest(createEvent, new TestLambdaContext()); verify(putRequestedFor(urlPathMatching("/")) .withRequestBody(matchingJsonPath("[?(@.Status == 'FAILED')]")) - .withRequestBody(matchingJsonPath("[?(@.PhysicalResourceId == '" + LOG_STREAM_NAME + "')]")) - ); + .withRequestBody(matchingJsonPath("[?(@.PhysicalResourceId == '" + LOG_STREAM_NAME + "')]"))); } @ParameterizedTest - @ValueSource(strings = {"Update", "Delete"}) + @ValueSource(strings = { "Update", "Delete" }) void physicalResourceIdSetFromRequestOnUpdateOrDeleteWhenCustomerDoesntProvideAPhysicalResourceId( String requestType, WireMockRuntimeInfo wmRuntimeInfo) { stubFor(put("/").willReturn(ok())); @@ -149,13 +126,12 @@ void physicalResourceIdSetFromRequestOnUpdateOrDeleteWhenCustomerDoesntProvideAP .withRequestType(requestType) .build(); - Response response = handler.handleRequest(event, new FakeContext()); + Response response = handler.handleRequest(event, new TestLambdaContext()); assertThat(response).isNotNull(); verify(putRequestedFor(urlPathMatching("/")) .withRequestBody(matchingJsonPath("[?(@.Status == 'SUCCESS')]")) - .withRequestBody(matchingJsonPath("[?(@.PhysicalResourceId == '" + PHYSICAL_RESOURCE_ID + "')]")) - ); + .withRequestBody(matchingJsonPath("[?(@.PhysicalResourceId == '" + PHYSICAL_RESOURCE_ID + "')]"))); } @Test @@ -166,17 +142,16 @@ void createNewResourceBecausePhysicalResourceIdNotSetByCustomerOnCreate(WireMock CloudFormationCustomResourceEvent createEvent = baseEvent(wmRuntimeInfo.getHttpPort()) .withRequestType("Create") .build(); - Response response = handler.handleRequest(createEvent, new FakeContext()); + Response response = handler.handleRequest(createEvent, new TestLambdaContext()); assertThat(response).isNotNull(); verify(putRequestedFor(urlPathMatching("/")) .withRequestBody(matchingJsonPath("[?(@.Status == 'SUCCESS')]")) - .withRequestBody(matchingJsonPath("[?(@.PhysicalResourceId == '" + LOG_STREAM_NAME + "')]")) - ); + .withRequestBody(matchingJsonPath("[?(@.PhysicalResourceId == '" + LOG_STREAM_NAME + "')]"))); } @ParameterizedTest - @ValueSource(strings = {"Create", "Update", "Delete"}) + @ValueSource(strings = { "Create", "Update", "Delete" }) void physicalResourceIdReturnedFromSuccessToCloudformation(String requestType, WireMockRuntimeInfo wmRuntimeInfo) { String physicalResourceId = UUID.randomUUID().toString(); @@ -185,17 +160,16 @@ void physicalResourceIdReturnedFromSuccessToCloudformation(String requestType, W CloudFormationCustomResourceEvent createEvent = baseEvent(wmRuntimeInfo.getHttpPort()) .withRequestType(requestType) .build(); - Response response = handler.handleRequest(createEvent, new FakeContext()); + Response response = handler.handleRequest(createEvent, new TestLambdaContext()); assertThat(response).isNotNull(); verify(putRequestedFor(urlPathMatching("/")) .withRequestBody(matchingJsonPath("[?(@.Status == 'SUCCESS')]")) - .withRequestBody(matchingJsonPath("[?(@.PhysicalResourceId == '" + physicalResourceId + "')]")) - ); + .withRequestBody(matchingJsonPath("[?(@.PhysicalResourceId == '" + physicalResourceId + "')]"))); } @ParameterizedTest - @ValueSource(strings = {"Create", "Update", "Delete"}) + @ValueSource(strings = { "Create", "Update", "Delete" }) void physicalResourceIdReturnedFromFailedToCloudformation(String requestType, WireMockRuntimeInfo wmRuntimeInfo) { String physicalResourceId = UUID.randomUUID().toString(); @@ -204,69 +178,12 @@ void physicalResourceIdReturnedFromFailedToCloudformation(String requestType, Wi CloudFormationCustomResourceEvent createEvent = baseEvent(wmRuntimeInfo.getHttpPort()) .withRequestType(requestType) .build(); - Response response = handler.handleRequest(createEvent, new FakeContext()); + Response response = handler.handleRequest(createEvent, new TestLambdaContext()); assertThat(response).isNotNull(); verify(putRequestedFor(urlPathMatching("/")) .withRequestBody(matchingJsonPath("[?(@.Status == 'FAILED')]")) - .withRequestBody(matchingJsonPath("[?(@.PhysicalResourceId == '" + physicalResourceId + "')]")) - ); + .withRequestBody(matchingJsonPath("[?(@.PhysicalResourceId == '" + physicalResourceId + "')]"))); } - private static class FakeContext implements Context { - @Override - public String getAwsRequestId() { - return null; - } - - @Override - public String getLogGroupName() { - return null; - } - - @Override - public String getLogStreamName() { - return LOG_STREAM_NAME; - } - - @Override - public String getFunctionName() { - return null; - } - - @Override - public String getFunctionVersion() { - return null; - } - - @Override - public String getInvokedFunctionArn() { - return null; - } - - @Override - public CognitoIdentity getIdentity() { - return null; - } - - @Override - public ClientContext getClientContext() { - return null; - } - - @Override - public int getRemainingTimeInMillis() { - return 0; - } - - @Override - public int getMemoryLimitInMB() { - return 0; - } - - @Override - public LambdaLogger getLogger() { - return null; - } - } } diff --git a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponseTest.java b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponseTest.java index 9da18790c..0cc65f884 100644 --- a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponseTest.java +++ b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponseTest.java @@ -20,15 +20,18 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent; -import com.fasterxml.jackson.databind.node.ObjectNode; import java.io.IOException; import java.io.InputStream; import java.util.LinkedHashMap; import java.util.Map; import java.util.Optional; + import org.junit.jupiter.api.Test; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent; +import com.fasterxml.jackson.databind.node.ObjectNode; + import software.amazon.awssdk.http.AbortableInputStream; import software.amazon.awssdk.http.ExecutableHttpRequest; import software.amazon.awssdk.http.HttpExecuteRequest; @@ -37,11 +40,13 @@ import software.amazon.awssdk.utils.IoUtils; import software.amazon.awssdk.utils.StringInputStream; import software.amazon.lambda.powertools.cloudformation.CloudFormationResponse.ResponseBody; +import software.amazon.lambda.powertools.common.stubs.TestLambdaContext; class CloudFormationResponseTest { /** - * Creates a mock CloudFormationCustomResourceEvent with a non-null response URL. + * Creates a mock CloudFormationCustomResourceEvent with a non-null response + * URL. */ static CloudFormationCustomResourceEvent mockCloudFormationCustomResourceEvent() { CloudFormationCustomResourceEvent event = mock(CloudFormationCustomResourceEvent.class); @@ -50,15 +55,15 @@ static CloudFormationCustomResourceEvent mockCloudFormationCustomResourceEvent() } /** - * Creates a CloudFormationResponse that does not make actual HTTP requests. The HTTP response body is the request + * Creates a CloudFormationResponse that does not make actual HTTP requests. The + * HTTP response body is the request * body. */ static CloudFormationResponse testableCloudFormationResponse() { SdkHttpClient client = mock(SdkHttpClient.class); ExecutableHttpRequest executableRequest = mock(ExecutableHttpRequest.class); - when(client.prepareRequest(any(HttpExecuteRequest.class))).thenAnswer(args -> - { + when(client.prepareRequest(any(HttpExecuteRequest.class))).thenAnswer(args -> { HttpExecuteRequest request = args.getArgument(0, HttpExecuteRequest.class); assertThat(request.contentStreamProvider()).isPresent(); @@ -89,7 +94,7 @@ void eventRequiredToSend() { SdkHttpClient client = mock(SdkHttpClient.class); CloudFormationResponse response = new CloudFormationResponse(client); - Context context = mock(Context.class); + Context context = new TestLambdaContext(); assertThatThrownBy(() -> response.send(null, context)) .isInstanceOf(CustomResourceResponseException.class); } @@ -99,7 +104,7 @@ void contextRequiredToSend() { SdkHttpClient client = mock(SdkHttpClient.class); CloudFormationResponse response = new CloudFormationResponse(client); - Context context = mock(Context.class); + Context context = new TestLambdaContext(); assertThatThrownBy(() -> response.send(null, context)) .isInstanceOf(CustomResourceResponseException.class); } @@ -110,8 +115,9 @@ void eventResponseUrlRequiredToSend() { CloudFormationResponse response = new CloudFormationResponse(client); CloudFormationCustomResourceEvent event = mock(CloudFormationCustomResourceEvent.class); - Context context = mock(Context.class); - // not a CustomResourceResponseException since the URL is not part of the response but + Context context = new TestLambdaContext(); + // not a CustomResourceResponseException since the URL is not part of the + // response but // rather the location the response is sent to assertThatThrownBy(() -> response.send(event, context)) .isInstanceOf(RuntimeException.class); @@ -122,8 +128,7 @@ void customPhysicalResponseId() { CloudFormationCustomResourceEvent event = mockCloudFormationCustomResourceEvent(); when(event.getPhysicalResourceId()).thenReturn("This-Is-Ignored"); - Context context = mock(Context.class); - when(context.getLogStreamName()).thenReturn("My-Log-Stream-Name"); + Context context = new TestLambdaContext(); String customPhysicalResourceId = "Custom-Physical-Resource-ID"; ResponseBody body = new ResponseBody( @@ -135,7 +140,7 @@ void customPhysicalResponseId() { @Test void responseBodyWithNullDataNode() { CloudFormationCustomResourceEvent event = mockCloudFormationCustomResourceEvent(); - Context context = mock(Context.class); + Context context = new TestLambdaContext(); ResponseBody responseBody = new ResponseBody(event, Response.Status.FAILED, null, true, "See the details in CloudWatch Log Stream: " + context.getLogStreamName()); @@ -143,7 +148,7 @@ void responseBodyWithNullDataNode() { String expectedJson = "{" + "\"Status\":\"FAILED\"," + - "\"Reason\":\"See the details in CloudWatch Log Stream: null\"," + + "\"Reason\":\"See the details in CloudWatch Log Stream: test-log-stream\"," + "\"PhysicalResourceId\":null," + "\"StackId\":null," + "\"RequestId\":null," + @@ -157,7 +162,7 @@ void responseBodyWithNullDataNode() { @Test void responseBodyWithNonNullDataNode() { CloudFormationCustomResourceEvent event = mockCloudFormationCustomResourceEvent(); - Context context = mock(Context.class); + Context context = new TestLambdaContext(); ObjectNode dataNode = ResponseBody.MAPPER.createObjectNode(); dataNode.put("foo", "bar"); dataNode.put("baz", 10); @@ -168,7 +173,7 @@ void responseBodyWithNonNullDataNode() { String expectedJson = "{" + "\"Status\":\"FAILED\"," + - "\"Reason\":\"See the details in CloudWatch Log Stream: null\"," + + "\"Reason\":\"See the details in CloudWatch Log Stream: test-log-stream\"," + "\"PhysicalResourceId\":null," + "\"StackId\":null," + "\"RequestId\":null," + @@ -182,7 +187,7 @@ void responseBodyWithNonNullDataNode() { @Test void defaultStatusIsSuccess() { CloudFormationCustomResourceEvent event = mockCloudFormationCustomResourceEvent(); - Context context = mock(Context.class); + Context context = new TestLambdaContext(); ResponseBody body = new ResponseBody( event, null, null, false, "See the details in CloudWatch Log Stream: " + context.getLogStreamName()); @@ -192,7 +197,7 @@ void defaultStatusIsSuccess() { @Test void customStatus() { CloudFormationCustomResourceEvent event = mockCloudFormationCustomResourceEvent(); - Context context = mock(Context.class); + Context context = new TestLambdaContext(); ResponseBody body = new ResponseBody( event, Response.Status.FAILED, null, false, @@ -203,21 +208,18 @@ void customStatus() { @Test void reasonIncludesLogStreamName() { CloudFormationCustomResourceEvent event = mockCloudFormationCustomResourceEvent(); - - String logStreamName = "My-Log-Stream-Name"; - Context context = mock(Context.class); - when(context.getLogStreamName()).thenReturn(logStreamName); + Context context = new TestLambdaContext(); ResponseBody body = new ResponseBody( event, Response.Status.SUCCESS, null, false, "See the details in CloudWatch Log Stream: " + context.getLogStreamName()); - assertThat(body.getReason()).contains(logStreamName); + assertThat(body.getReason()).contains(context.getLogStreamName()); } @Test void sendWithNoResponseData() throws Exception { CloudFormationCustomResourceEvent event = mockCloudFormationCustomResourceEvent(); - Context context = mock(Context.class); + Context context = new TestLambdaContext(); CloudFormationResponse cfnResponse = testableCloudFormationResponse(); HttpExecuteResponse response = cfnResponse.send(event, context); @@ -225,8 +227,8 @@ void sendWithNoResponseData() throws Exception { String actualJson = responseAsString(response); String expectedJson = "{" + "\"Status\":\"SUCCESS\"," + - "\"Reason\":\"See the details in CloudWatch Log Stream: null\"," + - "\"PhysicalResourceId\":null," + + "\"Reason\":\"See the details in CloudWatch Log Stream: test-log-stream\"," + + "\"PhysicalResourceId\":\"test-log-stream\"," + "\"StackId\":null," + "\"RequestId\":null," + "\"LogicalResourceId\":null," + @@ -239,7 +241,7 @@ void sendWithNoResponseData() throws Exception { @Test void sendWithNonNullResponseData() throws Exception { CloudFormationCustomResourceEvent event = mockCloudFormationCustomResourceEvent(); - Context context = mock(Context.class); + Context context = new TestLambdaContext(); CloudFormationResponse cfnResponse = testableCloudFormationResponse(); Map responseData = new LinkedHashMap<>(); @@ -251,8 +253,8 @@ void sendWithNonNullResponseData() throws Exception { String actualJson = responseAsString(response); String expectedJson = "{" + "\"Status\":\"SUCCESS\"," + - "\"Reason\":\"See the details in CloudWatch Log Stream: null\"," + - "\"PhysicalResourceId\":null," + + "\"Reason\":\"See the details in CloudWatch Log Stream: test-log-stream\"," + + "\"PhysicalResourceId\":\"test-log-stream\"," + "\"StackId\":null," + "\"RequestId\":null," + "\"LogicalResourceId\":null," + @@ -265,15 +267,15 @@ void sendWithNonNullResponseData() throws Exception { @Test void responseBodyStreamNullResponseDefaultsToSuccessStatus() throws Exception { CloudFormationCustomResourceEvent event = mockCloudFormationCustomResourceEvent(); - Context context = mock(Context.class); + Context context = new TestLambdaContext(); CloudFormationResponse cfnResponse = testableCloudFormationResponse(); StringInputStream stream = cfnResponse.responseBodyStream(event, context, null); String expectedJson = "{" + "\"Status\":\"SUCCESS\"," + - "\"Reason\":\"See the details in CloudWatch Log Stream: null\"," + - "\"PhysicalResourceId\":null," + + "\"Reason\":\"See the details in CloudWatch Log Stream: test-log-stream\"," + + "\"PhysicalResourceId\":\"test-log-stream\"," + "\"StackId\":null," + "\"RequestId\":null," + "\"LogicalResourceId\":null," + @@ -286,15 +288,15 @@ void responseBodyStreamNullResponseDefaultsToSuccessStatus() throws Exception { @Test void responseBodyStreamSuccessResponse() throws Exception { CloudFormationCustomResourceEvent event = mockCloudFormationCustomResourceEvent(); - Context context = mock(Context.class); + Context context = new TestLambdaContext(); CloudFormationResponse cfnResponse = testableCloudFormationResponse(); StringInputStream stream = cfnResponse.responseBodyStream(event, context, Response.success(null)); String expectedJson = "{" + "\"Status\":\"SUCCESS\"," + - "\"Reason\":\"See the details in CloudWatch Log Stream: null\"," + - "\"PhysicalResourceId\":null," + + "\"Reason\":\"See the details in CloudWatch Log Stream: test-log-stream\"," + + "\"PhysicalResourceId\":\"test-log-stream\"," + "\"StackId\":null," + "\"RequestId\":null," + "\"LogicalResourceId\":null," + @@ -307,15 +309,15 @@ void responseBodyStreamSuccessResponse() throws Exception { @Test void responseBodyStreamFailedResponse() throws Exception { CloudFormationCustomResourceEvent event = mockCloudFormationCustomResourceEvent(); - Context context = mock(Context.class); + Context context = new TestLambdaContext(); CloudFormationResponse cfnResponse = testableCloudFormationResponse(); StringInputStream stream = cfnResponse.responseBodyStream(event, context, Response.failed(null)); String expectedJson = "{" + "\"Status\":\"FAILED\"," + - "\"Reason\":\"See the details in CloudWatch Log Stream: null\"," + - "\"PhysicalResourceId\":null," + + "\"Reason\":\"See the details in CloudWatch Log Stream: test-log-stream\"," + + "\"PhysicalResourceId\":\"test-log-stream\"," + "\"StackId\":null," + "\"RequestId\":null," + "\"LogicalResourceId\":null," + @@ -328,17 +330,17 @@ void responseBodyStreamFailedResponse() throws Exception { @Test void responseBodyStreamFailedResponseWithReason() throws Exception { CloudFormationCustomResourceEvent event = mockCloudFormationCustomResourceEvent(); - Context context = mock(Context.class); + Context context = new TestLambdaContext(); CloudFormationResponse cfnResponse = testableCloudFormationResponse(); String failureReason = "Failed test reason"; - Response failedResponseWithReason = Response.builder(). - status(Response.Status.FAILED).reason(failureReason).build(); + Response failedResponseWithReason = Response.builder().status(Response.Status.FAILED).reason(failureReason) + .build(); StringInputStream stream = cfnResponse.responseBodyStream(event, context, failedResponseWithReason); String expectedJson = "{" + "\"Status\":\"FAILED\"," + "\"Reason\":\"" + failureReason + "\"," + - "\"PhysicalResourceId\":null," + + "\"PhysicalResourceId\":\"test-log-stream\"," + "\"StackId\":null," + "\"RequestId\":null," + "\"LogicalResourceId\":null," + diff --git a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/ExceptionThrowingHandler.java b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/ExceptionThrowingHandler.java new file mode 100644 index 000000000..dd2d1c853 --- /dev/null +++ b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/ExceptionThrowingHandler.java @@ -0,0 +1,31 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * http://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 software.amazon.lambda.powertools.cloudformation; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent; + +import software.amazon.lambda.powertools.cloudformation.Response.Status; + +public class ExceptionThrowingHandler extends ExpectedStatusResourceHandler { + public ExceptionThrowingHandler() { + super(Status.FAILED); + } + + @Override + protected Response create(CloudFormationCustomResourceEvent event, Context context) { + throw new RuntimeException("This exception is intentional for testing"); + } +} diff --git a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/ExpectedStatusResourceHandler.java b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/ExpectedStatusResourceHandler.java new file mode 100644 index 000000000..428c02f88 --- /dev/null +++ b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/ExpectedStatusResourceHandler.java @@ -0,0 +1,55 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * http://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 software.amazon.lambda.powertools.cloudformation; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; + +import org.mockito.ArgumentMatcher; + +import software.amazon.lambda.powertools.cloudformation.Response.Status; + +/** + * Creates a handler that will expect the Response to be sent with an expected + * status. Will throw an AssertionError + * if the method is sent with an unexpected status. + */ +public class ExpectedStatusResourceHandler extends NoOpCustomResourceHandler { + private final Status expectedStatus; + + public ExpectedStatusResourceHandler(Status expectedStatus) { + this.expectedStatus = expectedStatus; + } + + CloudFormationResponse buildResponseClient() { + // create a CloudFormationResponse that fails if invoked with unexpected status + CloudFormationResponse cfnResponse = mock(CloudFormationResponse.class); + try { + when(cfnResponse.send(any(), any(), org.mockito.ArgumentMatchers.argThat(new ArgumentMatcher() { + @Override + public boolean matches(Response resp) { + return resp != null && resp.getStatus() != expectedStatus; + } + }))).thenThrow(new AssertionError("Expected response's status to be " + expectedStatus)); + } catch (IOException | CustomResourceResponseException e) { + // this should never happen + throw new RuntimeException("Unexpected mocking exception", e); + } + return cfnResponse; + } +} diff --git a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/ExplicitSuccessResponseHandler.java b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/ExplicitSuccessResponseHandler.java new file mode 100644 index 000000000..2b11f8020 --- /dev/null +++ b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/ExplicitSuccessResponseHandler.java @@ -0,0 +1,31 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * http://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 software.amazon.lambda.powertools.cloudformation; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent; + +import software.amazon.lambda.powertools.cloudformation.Response.Status; + +public class ExplicitSuccessResponseHandler extends ExpectedStatusResourceHandler { + public ExplicitSuccessResponseHandler() { + super(Status.SUCCESS); + } + + @Override + protected Response create(CloudFormationCustomResourceEvent event, Context context) { + return Response.builder().value("whatever").status(Status.SUCCESS).build(); + } +} diff --git a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/FailToSendResponseHandler.java b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/FailToSendResponseHandler.java new file mode 100644 index 000000000..1934fb0d7 --- /dev/null +++ b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/FailToSendResponseHandler.java @@ -0,0 +1,40 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * http://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 software.amazon.lambda.powertools.cloudformation; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; + +/** + * Always fails to send the response + */ +public class FailToSendResponseHandler extends NoOpCustomResourceHandler { + CloudFormationResponse buildResponseClient() { + CloudFormationResponse cfnResponse = mock(CloudFormationResponse.class); + try { + when(cfnResponse.send(any(), any())) + .thenThrow(new IOException("Intentional send failure")); + when(cfnResponse.send(any(), any(), any())) + .thenThrow(new IOException("Intentional send failure")); + } catch (IOException | CustomResourceResponseException e) { + // this should never happen + throw new RuntimeException("Unexpected mocking exception", e); + } + return cfnResponse; + } +} diff --git a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/FailedResponseHandler.java b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/FailedResponseHandler.java new file mode 100644 index 000000000..787713535 --- /dev/null +++ b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/FailedResponseHandler.java @@ -0,0 +1,31 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * http://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 software.amazon.lambda.powertools.cloudformation; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent; + +import software.amazon.lambda.powertools.cloudformation.Response.Status; + +public class FailedResponseHandler extends ExpectedStatusResourceHandler { + public FailedResponseHandler() { + super(Status.FAILED); + } + + @Override + protected Response create(CloudFormationCustomResourceEvent event, Context context) { + return Response.builder().value("whatever").status(Status.FAILED).build(); + } +} diff --git a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/FailedSendHandler.java b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/FailedSendHandler.java new file mode 100644 index 000000000..fe88d8895 --- /dev/null +++ b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/FailedSendHandler.java @@ -0,0 +1,25 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * http://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 software.amazon.lambda.powertools.cloudformation; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent; + +public class FailedSendHandler extends FailToSendResponseHandler { + @Override + protected Response create(CloudFormationCustomResourceEvent event, Context context) { + throw new RuntimeException("This exception is intentional for testing"); + } +} diff --git a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/NoOpCustomResourceHandler.java b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/NoOpCustomResourceHandler.java new file mode 100644 index 000000000..005df8374 --- /dev/null +++ b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/NoOpCustomResourceHandler.java @@ -0,0 +1,33 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * http://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 software.amazon.lambda.powertools.cloudformation; + +import static org.mockito.Mockito.mock; + +import software.amazon.awssdk.http.SdkHttpClient; + +/** + * Uses a mocked CloudFormationResponse to avoid sending actual HTTP requests. + */ +public class NoOpCustomResourceHandler extends NullCustomResourceHandler { + + public NoOpCustomResourceHandler() { + super(mock(SdkHttpClient.class)); + } + + CloudFormationResponse buildResponseClient() { + return mock(CloudFormationResponse.class); + } +} diff --git a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/NullCustomResourceHandler.java b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/NullCustomResourceHandler.java new file mode 100644 index 000000000..a44e2d57e --- /dev/null +++ b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/NullCustomResourceHandler.java @@ -0,0 +1,47 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * http://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 software.amazon.lambda.powertools.cloudformation; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent; + +import software.amazon.awssdk.http.SdkHttpClient; + +/** + * Bare-bones implementation that returns null for abstract methods. + */ +public class NullCustomResourceHandler extends AbstractCustomResourceHandler { + public NullCustomResourceHandler() { + } + + public NullCustomResourceHandler(SdkHttpClient client) { + super(client); + } + + @Override + protected Response create(CloudFormationCustomResourceEvent event, Context context) { + return null; + } + + @Override + protected Response update(CloudFormationCustomResourceEvent event, Context context) { + return null; + } + + @Override + protected Response delete(CloudFormationCustomResourceEvent event, Context context) { + return null; + } +} diff --git a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/ResponseTest.java b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/ResponseTest.java index 3e2930541..726bcbeee 100644 --- a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/ResponseTest.java +++ b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/ResponseTest.java @@ -16,12 +16,14 @@ import static org.assertj.core.api.Assertions.assertThat; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.PropertyNamingStrategies; import java.util.HashMap; import java.util.Map; + import org.junit.jupiter.api.Test; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; + class ResponseTest { @Test @@ -81,7 +83,7 @@ void explicitReasonWithDefaultValues() { assertThat(response.toString()).contains("Status = SUCCESS"); assertThat(response.toString()).contains("PhysicalResourceId = null"); assertThat(response.toString()).contains("NoEcho = false"); - assertThat(response.toString()).contains("Reason = "+reason); + assertThat(response.toString()).contains("Reason = " + reason); } @Test diff --git a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/SuccessResponseHandler.java b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/SuccessResponseHandler.java new file mode 100644 index 000000000..18538bc9d --- /dev/null +++ b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/SuccessResponseHandler.java @@ -0,0 +1,31 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * http://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 software.amazon.lambda.powertools.cloudformation; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent; + +import software.amazon.lambda.powertools.cloudformation.Response.Status; + +public class SuccessResponseHandler extends ExpectedStatusResourceHandler { + public SuccessResponseHandler() { + super(Status.SUCCESS); + } + + @Override + protected Response create(CloudFormationCustomResourceEvent event, Context context) { + return Response.builder().value("whatever").build(); + } +} diff --git a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/SuccessfulSendHandler.java b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/SuccessfulSendHandler.java new file mode 100644 index 000000000..074d1499e --- /dev/null +++ b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/SuccessfulSendHandler.java @@ -0,0 +1,25 @@ +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. + * 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 + * http://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 software.amazon.lambda.powertools.cloudformation; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent; + +public class SuccessfulSendHandler extends FailToSendResponseHandler { + @Override + protected Response create(CloudFormationCustomResourceEvent event, Context context) { + return Response.builder().value("Failure happens on send").build(); + } +} diff --git a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/handlers/NoPhysicalResourceIdSetHandler.java b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/handlers/NoPhysicalResourceIdSetHandler.java index e55abca03..4fb14110c 100644 --- a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/handlers/NoPhysicalResourceIdSetHandler.java +++ b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/handlers/NoPhysicalResourceIdSetHandler.java @@ -16,6 +16,7 @@ import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent; + import software.amazon.lambda.powertools.cloudformation.AbstractCustomResourceHandler; import software.amazon.lambda.powertools.cloudformation.Response; diff --git a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/handlers/PhysicalResourceIdSetHandler.java b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/handlers/PhysicalResourceIdSetHandler.java index c6bd56b76..a01319342 100644 --- a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/handlers/PhysicalResourceIdSetHandler.java +++ b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/handlers/PhysicalResourceIdSetHandler.java @@ -16,6 +16,7 @@ import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent; + import software.amazon.lambda.powertools.cloudformation.AbstractCustomResourceHandler; import software.amazon.lambda.powertools.cloudformation.Response; diff --git a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/handlers/RuntimeExceptionThrownHandler.java b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/handlers/RuntimeExceptionThrownHandler.java index d5a11e895..10e3801c2 100644 --- a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/handlers/RuntimeExceptionThrownHandler.java +++ b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/handlers/RuntimeExceptionThrownHandler.java @@ -16,6 +16,7 @@ import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent; + import software.amazon.lambda.powertools.cloudformation.AbstractCustomResourceHandler; import software.amazon.lambda.powertools.cloudformation.Response; diff --git a/powertools-cloudformation/src/test/resources/simplelogger.properties b/powertools-cloudformation/src/test/resources/simplelogger.properties new file mode 100644 index 000000000..e8ba3a5fa --- /dev/null +++ b/powertools-cloudformation/src/test/resources/simplelogger.properties @@ -0,0 +1,7 @@ +org.slf4j.simpleLogger.logFile=target/cloudformation-test.log +org.slf4j.simpleLogger.defaultLogLevel=warn +org.slf4j.simpleLogger.showDateTime=true +org.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd HH:mm:ss:SSS +org.slf4j.simpleLogger.showThreadName=false +org.slf4j.simpleLogger.showLogName=true +org.slf4j.simpleLogger.showShortLogName=false From 0c310619cbec92e186e45a25d926ae2c78ce1d94 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Wed, 27 Aug 2025 11:09:18 +0000 Subject: [PATCH 02/17] Restore CloudFormationResponse.java from incorrect formatting changes. --- .../CloudFormationResponse.java | 121 +++++++----------- 1 file changed, 47 insertions(+), 74 deletions(-) diff --git a/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponse.java b/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponse.java index a52bded38..404137802 100644 --- a/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponse.java +++ b/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponse.java @@ -14,6 +14,12 @@ package software.amazon.lambda.powertools.cloudformation; +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.node.ObjectNode; import java.io.IOException; import java.net.URI; import java.util.Collections; @@ -21,17 +27,8 @@ import java.util.List; import java.util.Map; import java.util.Objects; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - -import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.PropertyNamingStrategies; -import com.fasterxml.jackson.databind.node.ObjectNode; - import software.amazon.awssdk.http.Header; import software.amazon.awssdk.http.HttpExecuteRequest; import software.amazon.awssdk.http.HttpExecuteResponse; @@ -42,24 +39,20 @@ import software.amazon.awssdk.utils.StringUtils; /** - * Client for sending responses to AWS CloudFormation custom resources by way of - * a response URL, which is an Amazon S3 + * Client for sending responses to AWS CloudFormation custom resources by way of a response URL, which is an Amazon S3 * pre-signed URL. *

- * See - * https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cfn-lambda-function-code-cfnresponsemodule.html + * See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cfn-lambda-function-code-cfnresponsemodule.html *

- * This class is thread-safe provided the SdkHttpClient instance used is also - * thread-safe. + * This class is thread-safe provided the SdkHttpClient instance used is also thread-safe. */ -public class CloudFormationResponse { +class CloudFormationResponse { private static final Logger LOG = LoggerFactory.getLogger(CloudFormationResponse.class); private final SdkHttpClient client; /** - * Creates a new CloudFormationResponse that uses the provided HTTP client and - * default JSON serialization format. + * Creates a new CloudFormationResponse that uses the provided HTTP client and default JSON serialization format. * * @param client HTTP client to use for sending requests; cannot be null */ @@ -77,46 +70,36 @@ SdkHttpClient getClient() { } /** - * Forwards a response containing a custom payload to the target resource - * specified by the event. The payload is + * Forwards a response containing a custom payload to the target resource specified by the event. The payload is * formed from the event and context data. Status is assumed to be SUCCESS. * * @param event custom CF resource event. Cannot be null. - * @param context used to specify when the function and any callbacks have - * completed execution, or to - * access information from within the Lambda execution - * environment. Cannot be null. + * @param context used to specify when the function and any callbacks have completed execution, or to + * access information from within the Lambda execution environment. Cannot be null. * @return the response object * @throws IOException when unable to send the request - * @throws CustomResourceResponseException when unable to synthesize or - * serialize the response payload + * @throws CustomResourceResponseException when unable to synthesize or serialize the response payload */ public HttpExecuteResponse send(CloudFormationCustomResourceEvent event, - Context context) throws IOException, CustomResourceResponseException { + Context context) throws IOException, CustomResourceResponseException { return send(event, context, null); } /** - * Forwards a response containing a custom payload to the target resource - * specified by the event. The payload is + * Forwards a response containing a custom payload to the target resource specified by the event. The payload is * formed from the event, context, and response data. * * @param event custom CF resource event. Cannot be null. - * @param context used to specify when the function and any callbacks have - * completed execution, or to - * access information from within the Lambda execution - * environment. Cannot be null. - * @param responseData response to send, e.g. a list of name-value pairs. If - * null, an empty success is assumed. + * @param context used to specify when the function and any callbacks have completed execution, or to + * access information from within the Lambda execution environment. Cannot be null. + * @param responseData response to send, e.g. a list of name-value pairs. If null, an empty success is assumed. * @return the response object - * @throws IOException when unable to generate or send the - * request - * @throws CustomResourceResponseException when unable to serialize the response - * payload + * @throws IOException when unable to generate or send the request + * @throws CustomResourceResponseException when unable to serialize the response payload */ public HttpExecuteResponse send(CloudFormationCustomResourceEvent event, - Context context, - Response responseData) throws IOException, CustomResourceResponseException { + Context context, + Response responseData) throws IOException, CustomResourceResponseException { // no need to explicitly close in-memory stream StringInputStream stream = responseBodyStream(event, context, responseData); URI uri = URI.create(event.getResponseUrl()); @@ -146,23 +129,20 @@ protected Map> headers(int contentLength) { } /** - * Returns the response body as an input stream, for supplying with the HTTP - * request to the custom resource. + * Returns the response body as an input stream, for supplying with the HTTP request to the custom resource. *

- * If PhysicalResourceId is null at this point it will be replaced with the - * Lambda LogStreamName. + * If PhysicalResourceId is null at this point it will be replaced with the Lambda LogStreamName. * - * @throws CustomResourceResponseException if unable to generate the response - * stream + * @throws CustomResourceResponseException if unable to generate the response stream */ StringInputStream responseBodyStream(CloudFormationCustomResourceEvent event, - Context context, - Response resp) throws CustomResourceResponseException { + Context context, + Response resp) throws CustomResourceResponseException { try { String reason = "See the details in CloudWatch Log Stream: " + context.getLogStreamName(); if (resp == null) { - String physicalResourceId = event.getPhysicalResourceId() != null ? event.getPhysicalResourceId() - : context.getLogStreamName(); + String physicalResourceId = event.getPhysicalResourceId() != null ? event.getPhysicalResourceId() : + context.getLogStreamName(); ResponseBody body = new ResponseBody(event, Response.Status.SUCCESS, physicalResourceId, false, reason); LOG.debug("ResponseBody: {}", body); @@ -172,12 +152,12 @@ StringInputStream responseBodyStream(CloudFormationCustomResourceEvent event, if (!StringUtils.isBlank(resp.getReason())) { reason = resp.getReason(); } - String physicalResourceId = resp.getPhysicalResourceId() != null ? resp.getPhysicalResourceId() - : event.getPhysicalResourceId() != null ? event.getPhysicalResourceId() - : context.getLogStreamName(); + String physicalResourceId = resp.getPhysicalResourceId() != null ? resp.getPhysicalResourceId() : + event.getPhysicalResourceId() != null ? event.getPhysicalResourceId() : + context.getLogStreamName(); - ResponseBody body = new ResponseBody(event, resp.getStatus(), physicalResourceId, resp.isNoEcho(), - reason); + ResponseBody body = + new ResponseBody(event, resp.getStatus(), physicalResourceId, resp.isNoEcho(), reason); LOG.debug("ResponseBody: {}", body); ObjectNode node = body.toObjectNode(resp.getJsonNode()); return new StringInputStream(node.toString()); @@ -189,14 +169,10 @@ StringInputStream responseBodyStream(CloudFormationCustomResourceEvent event, } /** - * Internal representation of the payload to be sent to the event target URL. - * Retains all properties of the payload - * except for "Data". This is done so that the serialization of the non-"Data" - * properties and the serialization of - * the value of "Data" can be handled by separate ObjectMappers, if need be. The - * former properties are dictated by - * the custom resource but the latter is dictated by the implementor of the - * custom resource handler. + * Internal representation of the payload to be sent to the event target URL. Retains all properties of the payload + * except for "Data". This is done so that the serialization of the non-"Data" properties and the serialization of + * the value of "Data" can be handled by separate ObjectMappers, if need be. The former properties are dictated by + * the custom resource but the latter is dictated by the implementor of the custom resource handler. */ @SuppressWarnings("unused") static class ResponseBody { @@ -213,10 +189,10 @@ static class ResponseBody { private final boolean noEcho; ResponseBody(CloudFormationCustomResourceEvent event, - Response.Status responseStatus, - String physicalResourceId, - boolean noEcho, - String reason) { + Response.Status responseStatus, + String physicalResourceId, + boolean noEcho, + String reason) { Objects.requireNonNull(event, "CloudFormationCustomResourceEvent cannot be null"); this.physicalResourceId = physicalResourceId; @@ -257,13 +233,10 @@ public boolean isNoEcho() { } /** - * Returns this ResponseBody as an ObjectNode with the provided JsonNode as the - * value of its "Data" property. + * Returns this ResponseBody as an ObjectNode with the provided JsonNode as the value of its "Data" property. * - * @param dataNode the value of the "Data" property for the returned node; may - * be null - * @return an ObjectNode representation of this ResponseBody and the provided - * dataNode + * @param dataNode the value of the "Data" property for the returned node; may be null + * @return an ObjectNode representation of this ResponseBody and the provided dataNode */ ObjectNode toObjectNode(JsonNode dataNode) { ObjectNode node = MAPPER.valueToTree(this); From e2caae0f309bf62e1eed7d82970f065d0c49f0b7 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Wed, 27 Aug 2025 11:13:12 +0000 Subject: [PATCH 03/17] Add missing @Override. --- .../powertools/cloudformation/ExpectedStatusResourceHandler.java | 1 + .../powertools/cloudformation/FailToSendResponseHandler.java | 1 + .../powertools/cloudformation/NoOpCustomResourceHandler.java | 1 + 3 files changed, 3 insertions(+) diff --git a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/ExpectedStatusResourceHandler.java b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/ExpectedStatusResourceHandler.java index 428c02f88..7992c712c 100644 --- a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/ExpectedStatusResourceHandler.java +++ b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/ExpectedStatusResourceHandler.java @@ -36,6 +36,7 @@ public ExpectedStatusResourceHandler(Status expectedStatus) { this.expectedStatus = expectedStatus; } + @Override CloudFormationResponse buildResponseClient() { // create a CloudFormationResponse that fails if invoked with unexpected status CloudFormationResponse cfnResponse = mock(CloudFormationResponse.class); diff --git a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/FailToSendResponseHandler.java b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/FailToSendResponseHandler.java index 1934fb0d7..218994c56 100644 --- a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/FailToSendResponseHandler.java +++ b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/FailToSendResponseHandler.java @@ -24,6 +24,7 @@ * Always fails to send the response */ public class FailToSendResponseHandler extends NoOpCustomResourceHandler { + @Override CloudFormationResponse buildResponseClient() { CloudFormationResponse cfnResponse = mock(CloudFormationResponse.class); try { diff --git a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/NoOpCustomResourceHandler.java b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/NoOpCustomResourceHandler.java index 005df8374..0271c36f5 100644 --- a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/NoOpCustomResourceHandler.java +++ b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/NoOpCustomResourceHandler.java @@ -27,6 +27,7 @@ public NoOpCustomResourceHandler() { super(mock(SdkHttpClient.class)); } + @Override CloudFormationResponse buildResponseClient() { return mock(CloudFormationResponse.class); } From 39782fcbbe5b58b726fb6b40a3ae41263bff4f5d Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Wed, 27 Aug 2025 11:14:02 +0000 Subject: [PATCH 04/17] Fix Sonar finding. --- .../cloudformation/CloudFormationIntegrationTest.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationIntegrationTest.java b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationIntegrationTest.java index 913740dfa..316913bf2 100644 --- a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationIntegrationTest.java +++ b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationIntegrationTest.java @@ -45,14 +45,12 @@ public class CloudFormationIntegrationTest { public static final String LOG_STREAM_NAME = "test-log-stream"; private static CloudFormationCustomResourceEvent.CloudFormationCustomResourceEventBuilder baseEvent(int httpPort) { - CloudFormationCustomResourceEvent.CloudFormationCustomResourceEventBuilder builder = CloudFormationCustomResourceEvent + return CloudFormationCustomResourceEvent .builder() .withResponseUrl("http://localhost:" + httpPort + "/") .withStackId("123") .withRequestId("234") .withLogicalResourceId("345"); - - return builder; } @ParameterizedTest From 0ac151c365a053b9e86ec34d2890862028b15af3 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Wed, 27 Aug 2025 11:36:46 +0000 Subject: [PATCH 05/17] Make CloudFormationResponse public as it is needed for subclass mockmaker. --- .../powertools/cloudformation/CloudFormationResponse.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponse.java b/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponse.java index 404137802..cf6fad827 100644 --- a/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponse.java +++ b/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponse.java @@ -46,7 +46,7 @@ *

* This class is thread-safe provided the SdkHttpClient instance used is also thread-safe. */ -class CloudFormationResponse { +public class CloudFormationResponse { private static final Logger LOG = LoggerFactory.getLogger(CloudFormationResponse.class); private final SdkHttpClient client; From 2dbaab73fc295059d46df3654b6cc6eae7819786 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Wed, 27 Aug 2025 12:53:01 +0000 Subject: [PATCH 06/17] Add reflective metadata for powertools-cloudformation. Add sam-graalvm infra example. --- .../Makefile | 5 + .../infra/sam-graalvm/Dockerfile | 14 + .../infra/sam-graalvm/README.md | 52 +++ .../infra/sam-graalvm/template.yaml | 49 ++ .../pom.xml | 184 +++++--- .../src/main/config/bootstrap | 4 + .../aws-lambda-java-core/reflect-config.json | 13 + .../reflect-config.json | 35 ++ .../jni-config.json | 11 + .../native-image.properties | 1 + .../reflect-config.json | 34 ++ .../resource-config.json | 19 + .../reflect-config.json | 25 + .../helloworld/native-image.properties | 1 + .../helloworld/reflect-config.json | 11 + .../helloworld/resource-config.json | 7 + .../s3/reflect-config.json | 27 ++ .../reflect-config.json | 432 ++++++++++++++++++ .../resource-config.json | 41 ++ 19 files changed, 889 insertions(+), 76 deletions(-) create mode 100644 examples/powertools-examples-cloudformation/Makefile create mode 100644 examples/powertools-examples-cloudformation/infra/sam-graalvm/Dockerfile create mode 100644 examples/powertools-examples-cloudformation/infra/sam-graalvm/README.md create mode 100644 examples/powertools-examples-cloudformation/infra/sam-graalvm/template.yaml create mode 100755 examples/powertools-examples-cloudformation/src/main/config/bootstrap create mode 100644 examples/powertools-examples-cloudformation/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-core/reflect-config.json create mode 100644 examples/powertools-examples-cloudformation/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-events/reflect-config.json create mode 100644 examples/powertools-examples-cloudformation/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/jni-config.json create mode 100644 examples/powertools-examples-cloudformation/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/native-image.properties create mode 100644 examples/powertools-examples-cloudformation/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/reflect-config.json create mode 100644 examples/powertools-examples-cloudformation/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/resource-config.json create mode 100644 examples/powertools-examples-cloudformation/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-serialization/reflect-config.json create mode 100644 examples/powertools-examples-cloudformation/src/main/resources/META-INF/native-image/helloworld/native-image.properties create mode 100644 examples/powertools-examples-cloudformation/src/main/resources/META-INF/native-image/helloworld/reflect-config.json create mode 100644 examples/powertools-examples-cloudformation/src/main/resources/META-INF/native-image/helloworld/resource-config.json create mode 100644 examples/powertools-examples-cloudformation/src/main/resources/META-INF/native-image/software.amazon.awssdk/s3/reflect-config.json create mode 100644 powertools-cloudformation/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-cloudformation/reflect-config.json create mode 100644 powertools-cloudformation/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-cloudformation/resource-config.json diff --git a/examples/powertools-examples-cloudformation/Makefile b/examples/powertools-examples-cloudformation/Makefile new file mode 100644 index 000000000..b916d823c --- /dev/null +++ b/examples/powertools-examples-cloudformation/Makefile @@ -0,0 +1,5 @@ +build-HelloWorldFunction: + chmod +x target/hello-world + cp target/hello-world $(ARTIFACTS_DIR) # (ARTIFACTS_DIR --> https://github.com/aws/aws-lambda-builders/blob/develop/aws_lambda_builders/workflows/custom_make/DESIGN.md#implementation) + chmod +x src/main/config/bootstrap + cp src/main/config/bootstrap $(ARTIFACTS_DIR) diff --git a/examples/powertools-examples-cloudformation/infra/sam-graalvm/Dockerfile b/examples/powertools-examples-cloudformation/infra/sam-graalvm/Dockerfile new file mode 100644 index 000000000..dac9390e5 --- /dev/null +++ b/examples/powertools-examples-cloudformation/infra/sam-graalvm/Dockerfile @@ -0,0 +1,14 @@ +# Use the official AWS SAM base image for Java 21 +FROM public.ecr.aws/sam/build-java21@sha256:a5554d68374e19450c6c88448516ac95a9acedc779f318040f5c230134b4e461 + +# Install GraalVM dependencies +RUN curl -4 -L curl https://download.oracle.com/graalvm/21/latest/graalvm-jdk-21_linux-x64_bin.tar.gz | tar -xvz +RUN mv graalvm-jdk-21.* /usr/lib/graalvm + +# Make native image and mvn available on CLI +RUN ln -s /usr/lib/graalvm/bin/native-image /usr/bin/native-image +RUN ln -s /usr/lib/maven/bin/mvn /usr/bin/mvn + +# Set GraalVM as default +ENV JAVA_HOME=/usr/lib/graalvm +ENV PATH=/usr/lib/graalvm/bin:$PATH diff --git a/examples/powertools-examples-cloudformation/infra/sam-graalvm/README.md b/examples/powertools-examples-cloudformation/infra/sam-graalvm/README.md new file mode 100644 index 000000000..e8deeb8fd --- /dev/null +++ b/examples/powertools-examples-cloudformation/infra/sam-graalvm/README.md @@ -0,0 +1,52 @@ +# Powertools for AWS Lambda (Java) - CloudFormation Custom Resource Example with SAM on GraalVM + +This project contains an example of a Lambda function using the CloudFormation module of Powertools for AWS Lambda (Java). For more information on this module, please refer to the [documentation](https://docs.powertools.aws.dev/lambda-java/utilities/custom_resources/). + +In this example you pass in a bucket name as a parameter and upon CloudFormation events a call is made to a lambda. That lambda attempts to create the bucket on CREATE events, create a new bucket if the name changes with an UPDATE event and delete the bucket upon DELETE events. + +Have a look at [App.java](../../src/main/java/helloworld/App.java) for the full details. + +## Build the sample application + +> [!NOTE] +> Building AWS Lambda packages on macOS (ARM64/Intel) for deployment on AWS Lambda (Linux x86_64 or ARM64) will result in incompatible binary dependencies that cause import errors at runtime. + +Choose the appropriate build method based on your operating system: + +### Build locally using Docker + +Recommended for macOS and Windows users: Cross-compile using Docker to match target platform of Lambda: + +```shell +docker build --platform linux/amd64 . -t powertools-examples-cloudformation-sam-graalvm +docker run --platform linux/amd64 -it -v `pwd`/../..:`pwd`/../.. -w `pwd`/../.. -v ~/.m2:/root/.m2 powertools-examples-cloudformation-sam-graalvm mvn clean -Pnative-image package -DskipTests +sam build --use-container --build-image powertools-examples-cloudformation-sam-graalvm +``` + +**Note**: The Docker run command mounts your local Maven cache (`~/.m2`) and builds the native binary with SNAPSHOT support, then SAM packages the pre-built binary. + +### Build on native OS + +For Linux users with GraalVM installed: + +```shell +export JAVA_HOME= +cd ../.. +mvn clean -Pnative-image package -DskipTests +cd infra/sam-graalvm +sam build +``` + +## Deploy the sample application + +```shell +sam deploy --guided --parameter-overrides BucketNameParam=my-unique-bucket-2.3.0718 +``` + +This sample is based on Serverless Application Model (SAM). To deploy it, check out the instructions for getting started with SAM in [the examples directory](../../../README.md) + +## Test the application + +The CloudFormation custom resource will be triggered automatically during stack deployment. You can monitor the Lambda function execution in CloudWatch Logs to see the custom resource handling CREATE, UPDATE, and DELETE events for the S3 bucket. + +Check out [App.java](../../src/main/java/helloworld/App.java) to see how it works! \ No newline at end of file diff --git a/examples/powertools-examples-cloudformation/infra/sam-graalvm/template.yaml b/examples/powertools-examples-cloudformation/infra/sam-graalvm/template.yaml new file mode 100644 index 000000000..4249aaed1 --- /dev/null +++ b/examples/powertools-examples-cloudformation/infra/sam-graalvm/template.yaml @@ -0,0 +1,49 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: > + powertools-examples-cloudformation-graalvm + + Sample SAM Template for powertools-examples-cloudformation with GraalVM native image + +Globals: + Function: + Timeout: 20 + +Parameters: + BucketNameParam: + Type: String + +Resources: + HelloWorldCustomResource: + Type: AWS::CloudFormation::CustomResource + Properties: + ServiceToken: !GetAtt HelloWorldFunction.Arn + BucketName: !Ref BucketNameParam + + HelloWorldFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: ../../ + Handler: helloworld.App::handleRequest + Runtime: provided.al2023 + Architectures: + - x86_64 + MemorySize: 512 + Policies: + - Statement: + - Sid: bucketaccess1 + Effect: Allow + Action: + - s3:GetLifecycleConfiguration + - s3:PutLifecycleConfiguration + - s3:CreateBucket + - s3:ListBucket + - s3:DeleteBucket + Resource: '*' + Metadata: + BuildMethod: makefile + +Outputs: + HelloWorldFunction: + Description: "Hello World Lambda Function ARN" + Value: !GetAtt HelloWorldFunction.Arn \ No newline at end of file diff --git a/examples/powertools-examples-cloudformation/pom.xml b/examples/powertools-examples-cloudformation/pom.xml index cfb757440..c3c1eabaf 100644 --- a/examples/powertools-examples-cloudformation/pom.xml +++ b/examples/powertools-examples-cloudformation/pom.xml @@ -37,9 +37,9 @@ ${lambda.core.version} - com.amazonaws - aws-lambda-java-events - ${lambda.events.version} + com.amazonaws + aws-lambda-java-events + ${lambda.events.version} software.amazon.lambda @@ -73,82 +73,114 @@ software.amazon.awssdk apache-client - - - commons-logging - commons-logging - - + + + com.amazonaws + aws-lambda-java-runtime-interface-client + 2.8.3 - - - dev.aspectj - aspectj-maven-plugin - 1.14.1 - - ${maven.compiler.source} - ${maven.compiler.target} - ${maven.compiler.target} - - - software.amazon.lambda - powertools-logging - - - - - - - compile - - - - - - org.aspectj - aspectjtools - ${aspectj.version} - - - - - org.apache.maven.plugins - maven-shade-plugin - 3.6.0 - - - package - - shade - - - false - - - - - - - - - org.apache.logging.log4j - log4j-transform-maven-shade-plugin-extensions - 0.2.0 - - - - - - org.apache.maven.plugins - maven-deploy-plugin - 3.1.4 - - true - - - + + + dev.aspectj + aspectj-maven-plugin + 1.14.1 + + ${maven.compiler.source} + ${maven.compiler.target} + ${maven.compiler.target} + + + software.amazon.lambda + powertools-logging + + + + + + + compile + + + + + + org.aspectj + aspectjtools + ${aspectj.version} + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.0 + + + package + + shade + + + false + + + + + + + + + org.apache.logging.log4j + log4j-transform-maven-shade-plugin-extensions + 0.2.0 + + + + + + org.apache.maven.plugins + maven-deploy-plugin + 3.1.4 + + true + + + + + + native-image + + + + org.graalvm.buildtools + native-maven-plugin + 0.11.0 + true + + + build-native + + build + + package + + + + hello-world + com.amazonaws.services.lambda.runtime.api.client.AWSLambda + + --enable-url-protocols=http + --add-opens java.base/java.util=ALL-UNNAMED + + + + + + + diff --git a/examples/powertools-examples-cloudformation/src/main/config/bootstrap b/examples/powertools-examples-cloudformation/src/main/config/bootstrap new file mode 100755 index 000000000..8e7928cd3 --- /dev/null +++ b/examples/powertools-examples-cloudformation/src/main/config/bootstrap @@ -0,0 +1,4 @@ +#!/bin/bash +set -e + +./hello-world $_HANDLER \ No newline at end of file diff --git a/examples/powertools-examples-cloudformation/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-core/reflect-config.json b/examples/powertools-examples-cloudformation/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-core/reflect-config.json new file mode 100644 index 000000000..2780aca09 --- /dev/null +++ b/examples/powertools-examples-cloudformation/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-core/reflect-config.json @@ -0,0 +1,13 @@ +[ + { + "name":"com.amazonaws.services.lambda.runtime.LambdaRuntime", + "methods":[{"name":"","parameterTypes":[] }], + "fields":[{"name":"logger"}], + "allPublicMethods":true + }, + { + "name":"com.amazonaws.services.lambda.runtime.LambdaRuntimeInternal", + "methods":[{"name":"","parameterTypes":[] }], + "allPublicMethods":true + } +] \ No newline at end of file diff --git a/examples/powertools-examples-cloudformation/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-events/reflect-config.json b/examples/powertools-examples-cloudformation/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-events/reflect-config.json new file mode 100644 index 000000000..ddda5d5f1 --- /dev/null +++ b/examples/powertools-examples-cloudformation/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-events/reflect-config.json @@ -0,0 +1,35 @@ +[ + { + "name": "com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent", + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true + }, + { + "name": "com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent$ProxyRequestContext", + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true + }, + { + "name": "com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent$RequestIdentity", + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true + }, + { + "name": "com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent", + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true + }, + { + "name": "com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent", + "allDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredClasses": true, + "allPublicClasses": true + } +] \ No newline at end of file diff --git a/examples/powertools-examples-cloudformation/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/jni-config.json b/examples/powertools-examples-cloudformation/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/jni-config.json new file mode 100644 index 000000000..91be72f7a --- /dev/null +++ b/examples/powertools-examples-cloudformation/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/jni-config.json @@ -0,0 +1,11 @@ +[ + { + "name":"com.amazonaws.services.lambda.runtime.api.client.runtimeapi.LambdaRuntimeClientException", + "methods":[{"name":"","parameterTypes":["java.lang.String","int"] }] + }, + { + "name":"com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.InvocationRequest", + "fields":[{"name":"id"}, {"name":"invokedFunctionArn"}, {"name":"deadlineTimeInMs"}, {"name":"xrayTraceId"}, {"name":"clientContext"}, {"name":"cognitoIdentity"}, {"name": "tenantId"}, {"name":"content"}], + "allPublicMethods":true + } +] \ No newline at end of file diff --git a/examples/powertools-examples-cloudformation/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/native-image.properties b/examples/powertools-examples-cloudformation/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/native-image.properties new file mode 100644 index 000000000..20f8b7801 --- /dev/null +++ b/examples/powertools-examples-cloudformation/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/native-image.properties @@ -0,0 +1 @@ +Args = --initialize-at-build-time=jdk.xml.internal.SecuritySupport \ No newline at end of file diff --git a/examples/powertools-examples-cloudformation/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/reflect-config.json b/examples/powertools-examples-cloudformation/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/reflect-config.json new file mode 100644 index 000000000..10152cc64 --- /dev/null +++ b/examples/powertools-examples-cloudformation/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/reflect-config.json @@ -0,0 +1,34 @@ +[ + { + "name":"com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.deser.Deserializers[]" + }, + { + "name":"com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.ext.Java7SupportImpl", + "methods":[{"name":"","parameterTypes":[] }] + }, + { + "name":"com.amazonaws.services.lambda.runtime.LambdaRuntime", + "fields":[{"name":"logger"}] + }, + { + "name":"java.lang.Void", + "methods":[{"name":"","parameterTypes":[] }] + }, + { + "name":"java.util.Collections$UnmodifiableMap", + "fields":[{"name":"m"}] + }, + { + "name":"jdk.internal.module.IllegalAccessLogger", + "fields":[{"name":"logger"}] + }, + { + "name":"sun.misc.Unsafe", + "fields":[{"name":"theUnsafe"}] + }, + { + "name":"com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.InvocationRequest", + "fields":[{"name":"id"}, {"name":"invokedFunctionArn"}, {"name":"deadlineTimeInMs"}, {"name":"xrayTraceId"}, {"name":"clientContext"}, {"name":"cognitoIdentity"}, {"name": "tenantId"}, {"name":"content"}], + "allPublicMethods":true + } +] \ No newline at end of file diff --git a/examples/powertools-examples-cloudformation/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/resource-config.json b/examples/powertools-examples-cloudformation/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/resource-config.json new file mode 100644 index 000000000..1062b4249 --- /dev/null +++ b/examples/powertools-examples-cloudformation/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-runtime-interface-client/resource-config.json @@ -0,0 +1,19 @@ +{ + "resources": { + "includes": [ + { + "pattern": "\\Qjni/libaws-lambda-jni.linux-aarch_64.so\\E" + }, + { + "pattern": "\\Qjni/libaws-lambda-jni.linux-x86_64.so\\E" + }, + { + "pattern": "\\Qjni/libaws-lambda-jni.linux_musl-aarch_64.so\\E" + }, + { + "pattern": "\\Qjni/libaws-lambda-jni.linux_musl-x86_64.so\\E" + } + ] + }, + "bundles": [] +} \ No newline at end of file diff --git a/examples/powertools-examples-cloudformation/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-serialization/reflect-config.json b/examples/powertools-examples-cloudformation/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-serialization/reflect-config.json new file mode 100644 index 000000000..9890688f9 --- /dev/null +++ b/examples/powertools-examples-cloudformation/src/main/resources/META-INF/native-image/com.amazonaws/aws-lambda-java-serialization/reflect-config.json @@ -0,0 +1,25 @@ +[ + { + "name": "com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.deser.Deserializers[]" + }, + { + "name": "com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.ext.Java7HandlersImpl", + "methods": [{ "name": "", "parameterTypes": [] }] + }, + { + "name": "com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.ext.Java7SupportImpl", + "methods": [{ "name": "", "parameterTypes": [] }] + }, + { + "name": "com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.ser.Serializers[]" + }, + { + "name": "org.joda.time.DateTime", + "allDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredClasses": true, + "allPublicClasses": true + } +] diff --git a/examples/powertools-examples-cloudformation/src/main/resources/META-INF/native-image/helloworld/native-image.properties b/examples/powertools-examples-cloudformation/src/main/resources/META-INF/native-image/helloworld/native-image.properties new file mode 100644 index 000000000..db5ebaa55 --- /dev/null +++ b/examples/powertools-examples-cloudformation/src/main/resources/META-INF/native-image/helloworld/native-image.properties @@ -0,0 +1 @@ +Args = --enable-url-protocols=http,https \ No newline at end of file diff --git a/examples/powertools-examples-cloudformation/src/main/resources/META-INF/native-image/helloworld/reflect-config.json b/examples/powertools-examples-cloudformation/src/main/resources/META-INF/native-image/helloworld/reflect-config.json new file mode 100644 index 000000000..06ea9ce2f --- /dev/null +++ b/examples/powertools-examples-cloudformation/src/main/resources/META-INF/native-image/helloworld/reflect-config.json @@ -0,0 +1,11 @@ +[ + { + "name": "helloworld.App", + "allDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredClasses": true, + "allPublicClasses": true + } +] diff --git a/examples/powertools-examples-cloudformation/src/main/resources/META-INF/native-image/helloworld/resource-config.json b/examples/powertools-examples-cloudformation/src/main/resources/META-INF/native-image/helloworld/resource-config.json new file mode 100644 index 000000000..be6aac3f6 --- /dev/null +++ b/examples/powertools-examples-cloudformation/src/main/resources/META-INF/native-image/helloworld/resource-config.json @@ -0,0 +1,7 @@ +{ + "resources":{ + "includes":[{ + "pattern":"\\Qlog4j2.xml\\E" + }]}, + "bundles":[] +} diff --git a/examples/powertools-examples-cloudformation/src/main/resources/META-INF/native-image/software.amazon.awssdk/s3/reflect-config.json b/examples/powertools-examples-cloudformation/src/main/resources/META-INF/native-image/software.amazon.awssdk/s3/reflect-config.json new file mode 100644 index 000000000..d685b7e20 --- /dev/null +++ b/examples/powertools-examples-cloudformation/src/main/resources/META-INF/native-image/software.amazon.awssdk/s3/reflect-config.json @@ -0,0 +1,27 @@ +[ + { + "name": "software.amazon.awssdk.services.s3.model.CreateBucketRequest", + "allPublicMethods": true, + "allPublicConstructors": true + }, + { + "name": "software.amazon.awssdk.services.s3.model.DeleteBucketRequest", + "allPublicMethods": true, + "allPublicConstructors": true + }, + { + "name": "software.amazon.awssdk.services.s3.model.HeadBucketRequest", + "allPublicMethods": true, + "allPublicConstructors": true + }, + { + "name": "software.amazon.awssdk.services.s3.model.HeadBucketResponse", + "allPublicMethods": true, + "allPublicConstructors": true + }, + { + "name": "software.amazon.awssdk.services.s3.model.NoSuchBucketException", + "allPublicMethods": true, + "allPublicConstructors": true + } +] \ No newline at end of file diff --git a/powertools-cloudformation/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-cloudformation/reflect-config.json b/powertools-cloudformation/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-cloudformation/reflect-config.json new file mode 100644 index 000000000..218382888 --- /dev/null +++ b/powertools-cloudformation/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-cloudformation/reflect-config.json @@ -0,0 +1,432 @@ +[ +{ + "name":"[Lcom.fasterxml.jackson.databind.deser.BeanDeserializerModifier;" +}, +{ + "name":"[Lcom.fasterxml.jackson.databind.deser.Deserializers;" +}, +{ + "name":"[Lcom.fasterxml.jackson.databind.deser.KeyDeserializers;" +}, +{ + "name":"[Lcom.fasterxml.jackson.databind.deser.ValueInstantiators;" +}, +{ + "name":"[Lcom.fasterxml.jackson.databind.ser.BeanSerializerModifier;" +}, +{ + "name":"[Lcom.fasterxml.jackson.databind.ser.Serializers;" +}, +{ + "name":"com.amazonaws.services.lambda.runtime.RequestHandler", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"canEqual","parameterTypes":["java.lang.Object"] }, {"name":"getLogicalResourceId","parameterTypes":[] }, {"name":"getOldResourceProperties","parameterTypes":[] }, {"name":"getPhysicalResourceId","parameterTypes":[] }, {"name":"getRequestId","parameterTypes":[] }, {"name":"getRequestType","parameterTypes":[] }, {"name":"getResourceProperties","parameterTypes":[] }, {"name":"getResourceType","parameterTypes":[] }, {"name":"getResponseUrl","parameterTypes":[] }, {"name":"getServiceToken","parameterTypes":[] }, {"name":"getStackId","parameterTypes":[] }, {"name":"setLogicalResourceId","parameterTypes":["java.lang.String"] }, {"name":"setOldResourceProperties","parameterTypes":["java.util.Map"] }, {"name":"setPhysicalResourceId","parameterTypes":["java.lang.String"] }, {"name":"setRequestId","parameterTypes":["java.lang.String"] }, {"name":"setRequestType","parameterTypes":["java.lang.String"] }, {"name":"setResourceProperties","parameterTypes":["java.util.Map"] }, {"name":"setResourceType","parameterTypes":["java.lang.String"] }, {"name":"setResponseUrl","parameterTypes":["java.lang.String"] }, {"name":"setServiceToken","parameterTypes":["java.lang.String"] }, {"name":"setStackId","parameterTypes":["java.lang.String"] }, {"name":"toString","parameterTypes":[] }] +}, +{ + "name":"com.fasterxml.jackson.databind.ext.Java7SupportImpl", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.jayway.jsonpath.spi.cache.CacheProvider", + "fields":[{"name":"cache"}] +}, +{ + "name":"com.sun.crypto.provider.AESCipher$General", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.crypto.provider.ARCFOURCipher", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.crypto.provider.ChaCha20Cipher$ChaCha20Poly1305", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.crypto.provider.DESCipher", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.crypto.provider.DESedeCipher", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.crypto.provider.GaloisCounterMode$AESGCM", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"com.sun.tools.attach.VirtualMachine" +}, +{ + "name":"java.io.FileNotFoundException", + "methods":[{"name":"","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"java.io.Serializable", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"java.lang.AutoCloseable", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"java.lang.Boolean", + "methods":[{"name":"valueOf","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"java.lang.Byte", + "methods":[{"name":"valueOf","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"java.lang.Class", + "methods":[{"name":"getAnnotatedInterfaces","parameterTypes":[] }, {"name":"getAnnotatedSuperclass","parameterTypes":[] }, {"name":"getDeclaredMethod","parameterTypes":["java.lang.String","java.lang.Class[]"] }, {"name":"getMethod","parameterTypes":["java.lang.String","java.lang.Class[]"] }, {"name":"getModule","parameterTypes":[] }, {"name":"getNestHost","parameterTypes":[] }, {"name":"getNestMembers","parameterTypes":[] }, {"name":"getPackageName","parameterTypes":[] }, {"name":"getPermittedSubclasses","parameterTypes":[] }, {"name":"getRecordComponents","parameterTypes":[] }, {"name":"isNestmateOf","parameterTypes":["java.lang.Class"] }, {"name":"isRecord","parameterTypes":[] }, {"name":"isSealed","parameterTypes":[] }] +}, +{ + "name":"java.lang.ClassLoader", + "methods":[{"name":"getDefinedPackage","parameterTypes":["java.lang.String"] }, {"name":"getUnnamedModule","parameterTypes":[] }, {"name":"registerAsParallelCapable","parameterTypes":[] }] +}, +{ + "name":"java.lang.Cloneable", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"java.lang.Double", + "methods":[{"name":"valueOf","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"java.lang.Float", + "methods":[{"name":"valueOf","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"java.lang.Integer", + "methods":[{"name":"valueOf","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"java.lang.Long", + "methods":[{"name":"valueOf","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"java.lang.Module", + "methods":[{"name":"addExports","parameterTypes":["java.lang.String","java.lang.Module"] }, {"name":"addOpens","parameterTypes":["java.lang.String","java.lang.Module"] }, {"name":"addReads","parameterTypes":["java.lang.Module"] }, {"name":"canRead","parameterTypes":["java.lang.Module"] }, {"name":"getClassLoader","parameterTypes":[] }, {"name":"getName","parameterTypes":[] }, {"name":"getPackages","parameterTypes":[] }, {"name":"getResourceAsStream","parameterTypes":["java.lang.String"] }, {"name":"isExported","parameterTypes":["java.lang.String"] }, {"name":"isExported","parameterTypes":["java.lang.String","java.lang.Module"] }, {"name":"isNamed","parameterTypes":[] }, {"name":"isOpen","parameterTypes":["java.lang.String","java.lang.Module"] }] +}, +{ + "name":"java.lang.Object", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"","parameterTypes":[] }, {"name":"clone","parameterTypes":[] }, {"name":"getClass","parameterTypes":[] }, {"name":"toString","parameterTypes":[] }] +}, +{ + "name":"java.lang.ProcessHandle", + "methods":[{"name":"current","parameterTypes":[] }, {"name":"pid","parameterTypes":[] }] +}, +{ + "name":"java.lang.Runtime", + "methods":[{"name":"version","parameterTypes":[] }] +}, +{ + "name":"java.lang.Runtime$Version", + "methods":[{"name":"feature","parameterTypes":[] }] +}, +{ + "name":"java.lang.Short", + "methods":[{"name":"valueOf","parameterTypes":["java.lang.String"] }] +}, +{ + "name":"java.lang.StackWalker" +}, +{ + "name":"java.lang.System", + "methods":[{"name":"getSecurityManager","parameterTypes":[] }] +}, +{ + "name":"java.lang.Thread", + "fields":[{"name":"threadLocalRandomProbe"}], + "methods":[{"name":"isVirtual","parameterTypes":[] }] +}, +{ + "name":"java.lang.annotation.Retention", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"java.lang.annotation.Target", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"java.lang.invoke.MethodHandle", + "methods":[{"name":"bindTo","parameterTypes":["java.lang.Object"] }, {"name":"invokeWithArguments","parameterTypes":["java.lang.Object[]"] }] +}, +{ + "name":"java.lang.invoke.MethodHandles", + "methods":[{"name":"lookup","parameterTypes":[] }, {"name":"privateLookupIn","parameterTypes":["java.lang.Class","java.lang.invoke.MethodHandles$Lookup"] }] +}, +{ + "name":"java.lang.invoke.MethodHandles$Lookup", + "methods":[{"name":"findVirtual","parameterTypes":["java.lang.Class","java.lang.String","java.lang.invoke.MethodType"] }] +}, +{ + "name":"java.lang.invoke.MethodType", + "methods":[{"name":"methodType","parameterTypes":["java.lang.Class","java.lang.Class[]"] }] +}, +{ + "name":"java.lang.management.ManagementFactory", + "methods":[{"name":"getRuntimeMXBean","parameterTypes":[] }] +}, +{ + "name":"java.lang.management.RuntimeMXBean", + "methods":[{"name":"getInputArguments","parameterTypes":[] }, {"name":"getUptime","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.AccessibleObject", + "methods":[{"name":"setAccessible","parameterTypes":["boolean"] }] +}, +{ + "name":"java.lang.reflect.AnnotatedArrayType", + "methods":[{"name":"getAnnotatedGenericComponentType","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.AnnotatedParameterizedType", + "methods":[{"name":"getAnnotatedActualTypeArguments","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.AnnotatedType", + "methods":[{"name":"getType","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.Executable", + "methods":[{"name":"getAnnotatedExceptionTypes","parameterTypes":[] }, {"name":"getAnnotatedParameterTypes","parameterTypes":[] }, {"name":"getAnnotatedReceiverType","parameterTypes":[] }, {"name":"getParameterCount","parameterTypes":[] }, {"name":"getParameters","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.Method", + "methods":[{"name":"getAnnotatedReturnType","parameterTypes":[] }] +}, +{ + "name":"java.lang.reflect.Parameter", + "methods":[{"name":"getModifiers","parameterTypes":[] }, {"name":"getName","parameterTypes":[] }, {"name":"isNamePresent","parameterTypes":[] }] +}, +{ + "name":"java.security.AccessController", + "methods":[{"name":"doPrivileged","parameterTypes":["java.security.PrivilegedAction"] }, {"name":"doPrivileged","parameterTypes":["java.security.PrivilegedExceptionAction"] }] +}, +{ + "name":"java.security.AlgorithmParametersSpi" +}, +{ + "name":"java.security.KeyStoreSpi" +}, +{ + "name":"java.security.SecureRandomParameters" +}, +{ + "name":"java.util.HashSet", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"java.util.concurrent.Callable", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true +}, +{ + "name":"java.util.concurrent.Executors", + "methods":[{"name":"newVirtualThreadPerTaskExecutor","parameterTypes":[] }] +}, +{ + "name":"java.util.concurrent.ForkJoinTask", + "fields":[{"name":"aux"}, {"name":"status"}] +}, +{ + "name":"java.util.concurrent.atomic.AtomicBoolean", + "fields":[{"name":"value"}] +}, +{ + "name":"java.util.concurrent.atomic.AtomicReference", + "fields":[{"name":"value"}] +}, +{ + "name":"java.util.concurrent.atomic.Striped64", + "fields":[{"name":"base"}, {"name":"cellsBusy"}] +}, +{ + "name":"java.util.function.Consumer", + "queryAllPublicMethods":true +}, +{ + "name":"javax.security.auth.x500.X500Principal", + "fields":[{"name":"thisX500Name"}], + "methods":[{"name":"","parameterTypes":["sun.security.x509.X500Name"] }] +}, +{ + "name":"jdk.internal.misc.Unsafe" +}, +{ + "name":"kotlin.Metadata" +}, +{ + "name":"kotlin.jvm.JvmInline" +}, +{ + "name":"org.apiguardian.api.API", + "queryAllPublicMethods":true +}, +{ + "name":"org.eclipse.jetty.http.pathmap.PathSpecSet", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.eclipse.jetty.servlets.CrossOriginFilter", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.eclipse.jetty.util.AsciiLowerCaseSet", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.eclipse.jetty.util.TypeUtil", + "methods":[{"name":"getClassLoaderLocation","parameterTypes":["java.lang.Class"] }, {"name":"getCodeSourceLocation","parameterTypes":["java.lang.Class"] }, {"name":"getModuleLocation","parameterTypes":["java.lang.Class"] }, {"name":"getSystemClassLoaderLocation","parameterTypes":["java.lang.Class"] }] +}, +{ + "name":"software.amazon.awssdk.http.Abortable", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"abort","parameterTypes":[] }] +}, +{ + "name":"software.amazon.awssdk.http.ExecutableHttpRequest", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"call","parameterTypes":[] }] +}, +{ + "name":"software.amazon.awssdk.http.HttpExecuteResponse", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"httpResponse","parameterTypes":[] }, {"name":"responseBody","parameterTypes":[] }] +}, +{ + "name":"software.amazon.awssdk.http.SdkHttpClient", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"clientName","parameterTypes":[] }, {"name":"prepareRequest","parameterTypes":["software.amazon.awssdk.http.HttpExecuteRequest"] }] +}, +{ + "name":"software.amazon.awssdk.utils.SdkAutoCloseable", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"close","parameterTypes":[] }] +}, +{ + "name":"software.amazon.lambda.powertools.cloudformation.AbstractCustomResourceHandler", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"onSendFailure","parameterTypes":["com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent","com.amazonaws.services.lambda.runtime.Context","software.amazon.lambda.powertools.cloudformation.Response","java.lang.Exception"] }] +}, +{ + "name":"software.amazon.lambda.powertools.cloudformation.CloudFormationResponse", + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"headers","parameterTypes":["int"] }, {"name":"send","parameterTypes":["com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent","com.amazonaws.services.lambda.runtime.Context"] }, {"name":"send","parameterTypes":["com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent","com.amazonaws.services.lambda.runtime.Context","software.amazon.lambda.powertools.cloudformation.Response"] }] +}, +{ + "name":"software.amazon.lambda.powertools.cloudformation.CloudFormationResponse$ResponseBody", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true, + "queryAllDeclaredConstructors":true, + "methods":[{"name":"getLogicalResourceId","parameterTypes":[] }, {"name":"getPhysicalResourceId","parameterTypes":[] }, {"name":"getReason","parameterTypes":[] }, {"name":"getRequestId","parameterTypes":[] }, {"name":"getStackId","parameterTypes":[] }, {"name":"getStatus","parameterTypes":[] }, {"name":"isNoEcho","parameterTypes":[] }] +}, +{ + "name":"sun.misc.SharedSecrets" +}, +{ + "name":"sun.reflect.ReflectionFactory", + "methods":[{"name":"getReflectionFactory","parameterTypes":[] }, {"name":"newConstructorForSerialization","parameterTypes":["java.lang.Class","java.lang.reflect.Constructor"] }] +}, +{ + "name":"sun.security.pkcs12.PKCS12KeyStore", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.pkcs12.PKCS12KeyStore$DualFormatPKCS12", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.provider.NativePRNG", + "methods":[{"name":"","parameterTypes":[] }, {"name":"","parameterTypes":["java.security.SecureRandomParameters"] }] +}, +{ + "name":"sun.security.provider.SHA", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.provider.X509Factory", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.rsa.RSAKeyFactory$Legacy", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.ssl.SSLContextImpl$TLSContext", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.ssl.TrustManagerFactoryImpl$PKIXFactory", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"sun.security.x509.AuthorityInfoAccessExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.AuthorityKeyIdentifierExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.BasicConstraintsExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.CRLDistributionPointsExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.CertificatePoliciesExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.ExtendedKeyUsageExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.KeyUsageExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.NetscapeCertTypeExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.PrivateKeyUsageExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +}, +{ + "name":"sun.security.x509.SubjectKeyIdentifierExtension", + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] +} +] diff --git a/powertools-cloudformation/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-cloudformation/resource-config.json b/powertools-cloudformation/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-cloudformation/resource-config.json new file mode 100644 index 000000000..f3b58337b --- /dev/null +++ b/powertools-cloudformation/src/main/resources/META-INF/native-image/software.amazon.lambda/powertools-cloudformation/resource-config.json @@ -0,0 +1,41 @@ +{ + "resources":{ + "includes":[{ + "pattern":"\\QMETA-INF/services/java.lang.System$LoggerFinder\\E" + }, { + "pattern":"\\QMETA-INF/services/java.net.spi.InetAddressResolverProvider\\E" + }, { + "pattern":"\\QMETA-INF/services/java.net.spi.URLStreamHandlerProvider\\E" + }, { + "pattern":"\\QMETA-INF/services/java.nio.channels.spi.SelectorProvider\\E" + }, { + "pattern":"\\QMETA-INF/services/java.util.spi.ResourceBundleControlProvider\\E" + }, { + "pattern":"\\QMETA-INF/services/org.slf4j.spi.SLF4JServiceProvider\\E" + }, { + "pattern":"\\Qassets/swagger-ui/index.html\\E" + }, { + "pattern":"\\Qassets\\E" + }, { + "pattern":"\\Qhelpers.nashorn.js\\E" + }, { + "pattern":"\\Qkeystore\\E" + }, { + "pattern":"\\Qorg/apache/hc/client5/version.properties\\E" + }, { + "pattern":"\\Qorg/eclipse/jetty/http/encoding.properties\\E" + }, { + "pattern":"\\Qorg/eclipse/jetty/http/mime.properties\\E" + }, { + "pattern":"\\Qorg/eclipse/jetty/version/build.properties\\E" + }, { + "pattern":"\\Qorg/publicsuffix/list/effective_tld_names.dat\\E" + }]}, + "bundles":[{ + "name":"jakarta.servlet.LocalStrings", + "locales":[""] + }, { + "name":"jakarta.servlet.http.LocalStrings", + "locales":[""] + }] +} From c276e6f75dd5e20278cfa56ff7dc1b2b9c337ce4 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Wed, 27 Aug 2025 13:22:13 +0000 Subject: [PATCH 07/17] Test conditional graalvm builds based on PR changes. --- .github/workflows/check-build.yml | 34 +++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/.github/workflows/check-build.yml b/.github/workflows/check-build.yml index 98e1eae6e..1360fc0f9 100644 --- a/.github/workflows/check-build.yml +++ b/.github/workflows/check-build.yml @@ -92,6 +92,14 @@ jobs: - id: checkout name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 + - name: Get changed files + id: changed-files + uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4 + with: + files: | + powertools-*/** - name: Setup GraalVM uses: graalvm/setup-graalvm@7f488cf82a3629ee755e4e97342c01d6bed318fa # v1.3.5 with: @@ -100,18 +108,32 @@ jobs: cache: maven - id: graalvm-native-test name: GraalVM Native Test + if: steps.changed-files.outputs.files_changed == 'true' + env: + CHANGED_FILES: ${{ steps.changed-files.outputs.changed_files }} run: | # Build the entire project first to ensure test-jar dependencies are available mvn -B -q install -DskipTests - # Find modules with graalvm-native profile and run tests recursively. - # This will make sure to discover new GraalVM supported modules automatically in the future. + echo "Changes detected in powertools modules: $CHANGED_FILES" + + # Find modules with graalvm-native profile and run tests find . -name "pom.xml" -path "./powertools-*" | while read module; do if grep -q "graalvm-native" "$module"; then module_dir=$(dirname "$module") - echo "Regenerating GraalVM metadata for $module_dir" - mvn -B -q -f "$module" -Pgenerate-graalvm-files clean test - echo "Running GraalVM native tests for $module_dir" - mvn -B -q -f "$module" -Pgraalvm-native test + module_name=$(basename "$module_dir") + + # Check if this specific module or common dependencies changed + if echo "$CHANGED_FILES" | grep -q "$module_name/" || \ + echo "$CHANGED_FILES" | grep -q "pom.xml" || \ + echo "$CHANGED_FILES" | grep -q "powertools-common/"; then + echo "Changes detected in $module_name - running GraalVM tests" + echo "Regenerating GraalVM metadata for $module_dir" + mvn -B -q -f "$module" -Pgenerate-graalvm-files clean test + echo "Running GraalVM native tests for $module_dir" + mvn -B -q -f "$module" -Pgraalvm-native test + else + echo "No changes detected in $module_name - skipping GraalVM tests" + fi fi done From fc0af96c2eabd13d5b5902f0c140508bd0c146c9 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Wed, 27 Aug 2025 13:27:59 +0000 Subject: [PATCH 08/17] Use correct changed-files GH action. --- .github/workflows/check-build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/check-build.yml b/.github/workflows/check-build.yml index 1360fc0f9..a7f6ea07f 100644 --- a/.github/workflows/check-build.yml +++ b/.github/workflows/check-build.yml @@ -96,7 +96,7 @@ jobs: fetch-depth: 0 - name: Get changed files id: changed-files - uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4 + uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46.0.5 with: files: | powertools-*/** @@ -108,9 +108,9 @@ jobs: cache: maven - id: graalvm-native-test name: GraalVM Native Test - if: steps.changed-files.outputs.files_changed == 'true' + if: steps.changed-files.outputs.any_changed == 'true' env: - CHANGED_FILES: ${{ steps.changed-files.outputs.changed_files }} + CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }} run: | # Build the entire project first to ensure test-jar dependencies are available mvn -B -q install -DskipTests From f53801fbd6f081fca56d0b361faa8b8132b26567 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Wed, 27 Aug 2025 13:32:04 +0000 Subject: [PATCH 09/17] Attempt to fix bug in detecting module changes. --- .github/workflows/check-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check-build.yml b/.github/workflows/check-build.yml index a7f6ea07f..0185b6054 100644 --- a/.github/workflows/check-build.yml +++ b/.github/workflows/check-build.yml @@ -124,7 +124,7 @@ jobs: module_name=$(basename "$module_dir") # Check if this specific module or common dependencies changed - if echo "$CHANGED_FILES" | grep -q "$module_name/" || \ + if echo "$CHANGED_FILES" | grep -q "^$module_name/" || \ echo "$CHANGED_FILES" | grep -q "pom.xml" || \ echo "$CHANGED_FILES" | grep -q "powertools-common/"; then echo "Changes detected in $module_name - running GraalVM tests" From f62a46ea3f68a301b4132c06b9488c81b7ad1991 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Wed, 27 Aug 2025 13:35:31 +0000 Subject: [PATCH 10/17] Use space separator to detect changed files. --- .github/workflows/check-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check-build.yml b/.github/workflows/check-build.yml index 0185b6054..bbc44e24e 100644 --- a/.github/workflows/check-build.yml +++ b/.github/workflows/check-build.yml @@ -124,7 +124,7 @@ jobs: module_name=$(basename "$module_dir") # Check if this specific module or common dependencies changed - if echo "$CHANGED_FILES" | grep -q "^$module_name/" || \ + if echo "$CHANGED_FILES" | grep -q "\b$module_name/" || \ echo "$CHANGED_FILES" | grep -q "pom.xml" || \ echo "$CHANGED_FILES" | grep -q "powertools-common/"; then echo "Changes detected in $module_name - running GraalVM tests" From 3a69e9e0ffabb9483ea216cc945dab5e5d71d409 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Wed, 27 Aug 2025 13:39:41 +0000 Subject: [PATCH 11/17] New attempt to try to fix module detection. --- .github/workflows/check-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check-build.yml b/.github/workflows/check-build.yml index bbc44e24e..278ff1a0f 100644 --- a/.github/workflows/check-build.yml +++ b/.github/workflows/check-build.yml @@ -124,7 +124,7 @@ jobs: module_name=$(basename "$module_dir") # Check if this specific module or common dependencies changed - if echo "$CHANGED_FILES" | grep -q "\b$module_name/" || \ + if echo " $CHANGED_FILES " | grep -q " $module_name/" || \ echo "$CHANGED_FILES" | grep -q "pom.xml" || \ echo "$CHANGED_FILES" | grep -q "powertools-common/"; then echo "Changes detected in $module_name - running GraalVM tests" From de951dece230f9c39bfef96d65a853b90f65094e Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Wed, 27 Aug 2025 13:47:27 +0000 Subject: [PATCH 12/17] Attempt to fix bug in detecting module changes. --- .github/workflows/check-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check-build.yml b/.github/workflows/check-build.yml index 278ff1a0f..4349ecf6b 100644 --- a/.github/workflows/check-build.yml +++ b/.github/workflows/check-build.yml @@ -124,7 +124,7 @@ jobs: module_name=$(basename "$module_dir") # Check if this specific module or common dependencies changed - if echo " $CHANGED_FILES " | grep -q " $module_name/" || \ + if echo "$CHANGED_FILES" | grep -q " $module_name/" || \ echo "$CHANGED_FILES" | grep -q "pom.xml" || \ echo "$CHANGED_FILES" | grep -q "powertools-common/"; then echo "Changes detected in $module_name - running GraalVM tests" From 9443963eab3cd58b1427e503b9ca2aeb34ffebec Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Wed, 27 Aug 2025 13:52:04 +0000 Subject: [PATCH 13/17] Add debug output to understand why changed module detection not working. --- .github/workflows/check-build.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/check-build.yml b/.github/workflows/check-build.yml index 4349ecf6b..93e7b8c14 100644 --- a/.github/workflows/check-build.yml +++ b/.github/workflows/check-build.yml @@ -123,8 +123,14 @@ jobs: module_dir=$(dirname "$module") module_name=$(basename "$module_dir") + # Get the relative path from project root + module_path=${module_dir#./} + + echo "DEBUG: module_dir=$module_dir, module_name=$module_name, module_path=$module_path" + echo "DEBUG: Checking for ' $module_path/' in ' $CHANGED_FILES '" + # Check if this specific module or common dependencies changed - if echo "$CHANGED_FILES" | grep -q " $module_name/" || \ + if echo " $CHANGED_FILES " | grep -q " $module_path/" || \ echo "$CHANGED_FILES" | grep -q "pom.xml" || \ echo "$CHANGED_FILES" | grep -q "powertools-common/"; then echo "Changes detected in $module_name - running GraalVM tests" From 1f2c8dd985907461eb2b82ca8380a5dc21454f16 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Wed, 27 Aug 2025 13:58:11 +0000 Subject: [PATCH 14/17] Add more debug statemetns. --- .github/workflows/check-build.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/check-build.yml b/.github/workflows/check-build.yml index 93e7b8c14..2e821a13a 100644 --- a/.github/workflows/check-build.yml +++ b/.github/workflows/check-build.yml @@ -126,11 +126,14 @@ jobs: # Get the relative path from project root module_path=${module_dir#./} - echo "DEBUG: module_dir=$module_dir, module_name=$module_name, module_path=$module_path" - echo "DEBUG: Checking for ' $module_path/' in ' $CHANGED_FILES '" + # Debug: show what we're actually checking + echo "DEBUG: Looking for changes in module: $module_path" + echo "DEBUG: Changed files contain pom.xml: $(echo "$CHANGED_FILES" | grep -q "pom.xml" && echo "YES" || echo "NO")" + echo "DEBUG: Changed files contain powertools-common/: $(echo "$CHANGED_FILES" | grep -q "powertools-common/" && echo "YES" || echo "NO")" + echo "DEBUG: Changed files contain $module_path/: $(echo "$CHANGED_FILES" | grep -q "$module_path/" && echo "YES" || echo "NO")" # Check if this specific module or common dependencies changed - if echo " $CHANGED_FILES " | grep -q " $module_path/" || \ + if echo "$CHANGED_FILES" | grep -q "$module_path/" || \ echo "$CHANGED_FILES" | grep -q "pom.xml" || \ echo "$CHANGED_FILES" | grep -q "powertools-common/"; then echo "Changes detected in $module_name - running GraalVM tests" From 29e606389f12dd3c8b193d7ae838aac5a120977f Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Wed, 27 Aug 2025 14:01:55 +0000 Subject: [PATCH 15/17] Fix issue with pom.xml root path detection. --- .github/workflows/check-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check-build.yml b/.github/workflows/check-build.yml index 2e821a13a..095ab1d52 100644 --- a/.github/workflows/check-build.yml +++ b/.github/workflows/check-build.yml @@ -134,7 +134,7 @@ jobs: # Check if this specific module or common dependencies changed if echo "$CHANGED_FILES" | grep -q "$module_path/" || \ - echo "$CHANGED_FILES" | grep -q "pom.xml" || \ + echo " $CHANGED_FILES " | grep -q " pom.xml " || \ echo "$CHANGED_FILES" | grep -q "powertools-common/"; then echo "Changes detected in $module_name - running GraalVM tests" echo "Regenerating GraalVM metadata for $module_dir" From 4304ae2fcfe0825189110da08acdb5f6247a8556 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Wed, 27 Aug 2025 14:05:23 +0000 Subject: [PATCH 16/17] Remove debug logs again. --- .github/workflows/check-build.yml | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/.github/workflows/check-build.yml b/.github/workflows/check-build.yml index 095ab1d52..cb47fad16 100644 --- a/.github/workflows/check-build.yml +++ b/.github/workflows/check-build.yml @@ -123,17 +123,8 @@ jobs: module_dir=$(dirname "$module") module_name=$(basename "$module_dir") - # Get the relative path from project root - module_path=${module_dir#./} - - # Debug: show what we're actually checking - echo "DEBUG: Looking for changes in module: $module_path" - echo "DEBUG: Changed files contain pom.xml: $(echo "$CHANGED_FILES" | grep -q "pom.xml" && echo "YES" || echo "NO")" - echo "DEBUG: Changed files contain powertools-common/: $(echo "$CHANGED_FILES" | grep -q "powertools-common/" && echo "YES" || echo "NO")" - echo "DEBUG: Changed files contain $module_path/: $(echo "$CHANGED_FILES" | grep -q "$module_path/" && echo "YES" || echo "NO")" - # Check if this specific module or common dependencies changed - if echo "$CHANGED_FILES" | grep -q "$module_path/" || \ + if echo "$CHANGED_FILES" | grep -q "$module_name/" || \ echo " $CHANGED_FILES " | grep -q " pom.xml " || \ echo "$CHANGED_FILES" | grep -q "powertools-common/"; then echo "Changes detected in $module_name - running GraalVM tests" From cd70d38b094b44835840a735ab5ef8f8b3c6e1f9 Mon Sep 17 00:00:00 2001 From: Philipp Page Date: Thu, 28 Aug 2025 11:41:17 +0000 Subject: [PATCH 17/17] Use github ::group::. --- .github/workflows/check-build.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/check-build.yml b/.github/workflows/check-build.yml index d1538dfdf..3a34188f9 100644 --- a/.github/workflows/check-build.yml +++ b/.github/workflows/check-build.yml @@ -113,7 +113,9 @@ jobs: CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }} run: | # Build the entire project first to ensure test-jar dependencies are available + echo "::group::Building project dependencies" mvn -B -q install -DskipTests + echo "::endgroup::" echo "Changes detected in powertools modules: $CHANGED_FILES" @@ -127,11 +129,13 @@ jobs: if echo "$CHANGED_FILES" | grep -q "$module_name/" || \ echo " $CHANGED_FILES " | grep -q " pom.xml " || \ echo "$CHANGED_FILES" | grep -q "powertools-common/"; then + echo "::group::Building $module_name with GraalVM" echo "Changes detected in $module_name - running GraalVM tests" echo "Regenerating GraalVM metadata for $module_dir" mvn -B -q -f "$module" -Pgenerate-graalvm-files clean test echo "Running GraalVM native tests for $module_dir" mvn -B -q -f "$module" -Pgraalvm-native test + echo "::endgroup::" else echo "No changes detected in $module_name - skipping GraalVM tests" fi