Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 28 additions & 6 deletions .github/workflows/check-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46.0.5
with:
files: |
powertools-*/**
- name: Setup GraalVM
uses: graalvm/setup-graalvm@7f488cf82a3629ee755e4e97342c01d6bed318fa # v1.3.5
with:
Expand All @@ -100,18 +108,32 @@ jobs:
cache: maven
- id: graalvm-native-test
name: GraalVM Native Test
if: steps.changed-files.outputs.any_changed == 'true'
env:
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

# 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 "<id>graalvm-native</id>" "$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
26 changes: 26 additions & 0 deletions GraalVM.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Response>() {
@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).
5 changes: 5 additions & 0 deletions examples/powertools-examples-cloudformation/Makefile
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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=<path to GraalVM>
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!
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading