diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index ab40d21d7..d9fdc8b1d 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -2,5 +2,7 @@ *Description of changes:* +*Target (OCI, Managed Runtime, both):* + By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. diff --git a/.github/actions/configure-maven-mirror/action.yml b/.github/actions/configure-maven-mirror/action.yml new file mode 100644 index 000000000..ce309a8f7 --- /dev/null +++ b/.github/actions/configure-maven-mirror/action.yml @@ -0,0 +1,42 @@ +name: Configure Maven CodeArtifact mirror +description: Configure Maven to resolve dependencies through the release CodeArtifact repository. + +runs: + using: composite + steps: + - shell: bash + run: | + CA_DOMAIN=aws-lambda + CA_REPO=maven-central-store + + # Uses the ambient region and caller account. + TOKEN=$(aws codeartifact get-authorization-token \ + --domain "$CA_DOMAIN" --query authorizationToken --output text) + echo "::add-mask::$TOKEN" + + CA_URL=$(aws codeartifact get-repository-endpoint \ + --domain "$CA_DOMAIN" --repository "$CA_REPO" --format maven \ + --query repositoryEndpoint --output text) + + # * routes all resolution through the mirror; + # deployment uses distributionManagement and is unaffected. + mkdir -p "$HOME/.m2" + cat > "$HOME/.m2/settings.xml" < + + + codeartifact-mirror + aws + ${TOKEN} + + + + + codeartifact-mirror + release CodeArtifact Maven Central proxy + ${CA_URL} + * + + + + EOF diff --git a/.github/actions/configure-release-aws-credentials/action.yml b/.github/actions/configure-release-aws-credentials/action.yml new file mode 100644 index 000000000..6a34f6480 --- /dev/null +++ b/.github/actions/configure-release-aws-credentials/action.yml @@ -0,0 +1,28 @@ +name: "Configure AWS credentials for release (OIDC)" +description: > + Assumes the release OIDC role via aws-actions/configure-aws-credentials so the + job can read the signing key and Sonatype token from Secrets Manager. Pinning + of the underlying action lives here so it is updated in one place. + +inputs: + aws-region: + description: "AWS region to operate in." + required: true + role-to-assume: + description: "ARN of the OIDC role to assume." + required: true + role-session-name: + description: "Session name for the assumed role (helps distinguish callers in CloudTrail)." + required: true + +runs: + using: composite + steps: + - uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4 + with: + aws-region: ${{ inputs.aws-region }} + role-to-assume: ${{ inputs.role-to-assume }} + role-session-name: ${{ inputs.role-session-name }} + # Kept short: the job only needs the role briefly to read two secrets. + # 900s is STS's minimum for assume-role; anything lower is rejected. + role-duration-seconds: 900 diff --git a/.github/actions/resolve-release-version/action.yml b/.github/actions/resolve-release-version/action.yml new file mode 100644 index 000000000..06d3f4f1e --- /dev/null +++ b/.github/actions/resolve-release-version/action.yml @@ -0,0 +1,54 @@ +name: "Resolve and validate release version" +description: > + Reads the module POM version (the source of truth), verifies it is a + -SNAPSHOT, and derives the effective release version (the optional override, + or the POM version with -SNAPSHOT stripped). Exports CURRENT_VERSION and + EFFECTIVE_RELEASE_VERSION to the job environment for subsequent steps. + +inputs: + module: + description: "Module directory containing the pom.xml to release." + required: true + release-version-override: + description: "Optional release version; defaults to the POM version without -SNAPSHOT." + required: false + default: "" + validate-module-dir: + description: "Fail if the module directory or its pom.xml is missing (use for the choice-driven workflow)." + required: false + default: "false" + +runs: + using: composite + steps: + - name: Resolve and validate release version + shell: bash + env: + MODULE: ${{ inputs.module }} + RELEASE_VERSION_OVERRIDE: ${{ inputs.release-version-override }} + VALIDATE_MODULE_DIR: ${{ inputs.validate-module-dir }} + run: | + if [[ "$VALIDATE_MODULE_DIR" == "true" ]]; then + if [[ ! -d "$MODULE" ]]; then + echo "::error::Module directory '$MODULE' does not exist" + exit 1 + fi + if [[ ! -f "$MODULE/pom.xml" ]]; then + echo "::error::No pom.xml found in '$MODULE'" + exit 1 + fi + fi + + # The POM version is the source of truth and must be a SNAPSHOT. + CURRENT_VERSION=$(mvn -q -DforceStdout help:evaluate -Dexpression=project.version --file "$MODULE/pom.xml") + CURRENT_VERSION="${CURRENT_VERSION//[$'\r\n']/}" + if [[ "$CURRENT_VERSION" != *-SNAPSHOT ]]; then + echo "::error::POM version '$CURRENT_VERSION' is not a SNAPSHOT" + exit 1 + fi + + # Optional override; default strips -SNAPSHOT. + EFFECTIVE_RELEASE_VERSION="${RELEASE_VERSION_OVERRIDE:-${CURRENT_VERSION%-SNAPSHOT}}" + + echo "CURRENT_VERSION=$CURRENT_VERSION" >> "$GITHUB_ENV" + echo "EFFECTIVE_RELEASE_VERSION=$EFFECTIVE_RELEASE_VERSION" >> "$GITHUB_ENV" diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..88f18ea29 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: "maven" + directory: "/aws-lambda-java-runtime-interface" + schedule: + interval: "weekly" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" \ No newline at end of file diff --git a/.github/test-matrix.json b/.github/test-matrix.json new file mode 100644 index 000000000..7e6539cb6 --- /dev/null +++ b/.github/test-matrix.json @@ -0,0 +1,16 @@ +{ + "arch": [ + { + "runner": "ubuntu-latest", + "label": "x64", + "sam_arch": "x86_64", + "java_suffix": "X64" + }, + { + "runner": "ubuntu-24.04-arm", + "label": "arm64", + "sam_arch": "arm64", + "java_suffix": "ARM64" + } + ] +} diff --git a/.github/workflows/aws-lambda-java-core.yml b/.github/workflows/aws-lambda-java-core.yml new file mode 100644 index 000000000..3e4364672 --- /dev/null +++ b/.github/workflows/aws-lambda-java-core.yml @@ -0,0 +1,36 @@ +# This workflow will be triggered if there will be changes to aws-lambda-java-core +# package and it builds the package. + +name: Java CI aws-lambda-java-core + +on: + workflow_dispatch: + push: + branches: [ main ] + paths: + - 'aws-lambda-java-core/**' + pull_request: + branches: [ '*' ] + paths: + - 'aws-lambda-java-core/**' + - '.github/workflows/aws-lambda-java-core.yml' + +permissions: + contents: read + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + - name: Set up JDK 1.8 + uses: actions/setup-java@v5 + with: + java-version: 8 + distribution: corretto + cache: maven + + - name: Install core with Maven + run: mvn -B install --file aws-lambda-java-core/pom.xml diff --git a/.github/workflows/aws-lambda-java-events-sdk-transformer.yml b/.github/workflows/aws-lambda-java-events-sdk-transformer.yml new file mode 100644 index 000000000..144d52f86 --- /dev/null +++ b/.github/workflows/aws-lambda-java-events-sdk-transformer.yml @@ -0,0 +1,43 @@ +# This workflow will be triggered if there will be changes to +# aws-lambda-java-events-sdk-transformer package or its dependency (events), +# and it builds the package. + +name: Java CI aws-lambda-java-events-sdk-transformer + +on: + workflow_dispatch: + push: + branches: [ main ] + paths: + - 'aws-lambda-java-events-sdk-transformer/**' + - 'aws-lambda-java-events/**' + pull_request: + branches: [ '*' ] + paths: + - 'aws-lambda-java-events-sdk-transformer/**' + - 'aws-lambda-java-events/**' + - '.github/workflows/aws-lambda-java-events-sdk-transformer.yml' + +permissions: + contents: read + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + - name: Set up JDK 1.8 + uses: actions/setup-java@v5 + with: + java-version: 8 + distribution: corretto + cache: maven + + # Install dependency + - name: Install events with Maven + run: mvn -B install --file aws-lambda-java-events/pom.xml + # Package target module + - name: Package events-sdk-transformer with Maven + run: mvn -B package --file aws-lambda-java-events-sdk-transformer/pom.xml diff --git a/.github/workflows/aws-lambda-java-events.yml b/.github/workflows/aws-lambda-java-events.yml new file mode 100644 index 000000000..18be63cf9 --- /dev/null +++ b/.github/workflows/aws-lambda-java-events.yml @@ -0,0 +1,36 @@ +# This workflow will be triggered if there will be changes to aws-lambda-java-events +# package and it builds the package. + +name: Java CI aws-lambda-java-events + +on: + workflow_dispatch: + push: + branches: [ main ] + paths: + - 'aws-lambda-java-events/**' + pull_request: + branches: [ '*' ] + paths: + - 'aws-lambda-java-events/**' + - '.github/workflows/aws-lambda-java-events.yml' + +permissions: + contents: read + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + - name: Set up JDK 1.8 + uses: actions/setup-java@v5 + with: + java-version: 8 + distribution: corretto + cache: maven + + - name: Install events with Maven + run: mvn -B install --file aws-lambda-java-events/pom.xml diff --git a/.github/workflows/aws-lambda-java-log4j2.yml b/.github/workflows/aws-lambda-java-log4j2.yml new file mode 100644 index 000000000..945a1cb30 --- /dev/null +++ b/.github/workflows/aws-lambda-java-log4j2.yml @@ -0,0 +1,42 @@ +# This workflow will be triggered if there will be changes to +# aws-lambda-java-log4j2 package or its dependency (core), and it builds the package. + +name: Java CI aws-lambda-java-log4j2 + +on: + push: + workflow_dispatch: + branches: [ main ] + paths: + - 'aws-lambda-java-log4j2/**' + - 'aws-lambda-java-core/**' + pull_request: + branches: [ '*' ] + paths: + - 'aws-lambda-java-log4j2/**' + - 'aws-lambda-java-core/**' + - '.github/workflows/aws-lambda-java-log4j2.yml' + +permissions: + contents: read + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + - name: Set up JDK 1.8 + uses: actions/setup-java@v5 + with: + java-version: 8 + distribution: corretto + cache: maven + + # Install dependency + - name: Install core with Maven + run: mvn -B install --file aws-lambda-java-core/pom.xml + # Package target module + - name: Package log4j2 with Maven + run: mvn -B package --file aws-lambda-java-log4j2/pom.xml diff --git a/.github/workflows/aws-lambda-java-profiler.yml b/.github/workflows/aws-lambda-java-profiler.yml new file mode 100644 index 000000000..a098bfd14 --- /dev/null +++ b/.github/workflows/aws-lambda-java-profiler.yml @@ -0,0 +1,79 @@ +name: Run integration tests for aws-lambda-java-profiler + +on: + pull_request: + branches: [ '*' ] + paths: + - 'experimental/aws-lambda-java-profiler/**' + - '.github/workflows/aws-lambda-java-profiler.yml' + push: + branches: ['*'] + paths: + - 'experimental/aws-lambda-java-profiler/**' + - '.github/workflows/aws-lambda-java-profiler.yml' + +jobs: + + build: + runs-on: ubuntu-latest + + permissions: + id-token: write + contents: read + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Set up JDK + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 + with: + java-version: 21 + distribution: corretto + cache: maven + + - name: Issue AWS credentials + uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4 + with: + aws-region: ${{ secrets.AWS_REGION_PROFILER_EXTENSION_INTEGRATION_TEST }} + role-to-assume: ${{ secrets.AWS_ROLE_PROFILER_EXTENSION_INTEGRATION_TEST }} + role-session-name: GitHubActionsRunIntegrationTests + role-duration-seconds: 900 + + - name: Build layer + working-directory: ./experimental/aws-lambda-java-profiler/extension + run: ./build_layer.sh + + - name: Publish layer + working-directory: ./experimental/aws-lambda-java-profiler + run: ./integration_tests/publish_layer.sh + + - name: Create the bucket layer + working-directory: ./experimental/aws-lambda-java-profiler + run: ./integration_tests/create_bucket.sh + + - name: Create Java function + working-directory: ./experimental/aws-lambda-java-profiler + run: ./integration_tests/create_function.sh + + - name: Invoke Java function + working-directory: ./experimental/aws-lambda-java-profiler + run: ./integration_tests/invoke_function.sh + + - name: Invoke Java Custom Options function + working-directory: ./experimental/aws-lambda-java-profiler + run: ./integration_tests/invoke_function_custom_options.sh + + - name: Download from s3 + working-directory: ./experimental/aws-lambda-java-profiler + run: ./integration_tests/download_from_s3.sh + + - name: Upload profiles + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: profiles + path: /tmp/s3-artifacts + + - name: cleanup + if: always() + working-directory: ./experimental/aws-lambda-java-profiler + run: ./integration_tests/cleanup.sh \ No newline at end of file diff --git a/.github/workflows/aws-lambda-java-serialization.yml b/.github/workflows/aws-lambda-java-serialization.yml new file mode 100644 index 000000000..f52c96fed --- /dev/null +++ b/.github/workflows/aws-lambda-java-serialization.yml @@ -0,0 +1,47 @@ +# This workflow will be triggered if there will be changes to aws-lambda-java-serialization +# package or its dependency (events), and it builds the package. + +name: Java CI aws-lambda-java-serialization + +on: + workflow_dispatch: + push: + branches: [ main ] + paths: + - 'aws-lambda-java-serialization/**' + - 'aws-lambda-java-events/**' + pull_request: + branches: [ '*' ] + paths: + - 'aws-lambda-java-serialization/**' + - 'aws-lambda-java-events/**' + - '.github/workflows/aws-lambda-java-serialization.yml' + +permissions: + contents: read + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + - name: Set up JDK 1.8 + uses: actions/setup-java@v5 + with: + java-version: 8 + distribution: corretto + cache: maven + + # Install dependency + - name: Install events with Maven + run: mvn -B install --file aws-lambda-java-events/pom.xml + + # Package and install target module + - name: Package serialization with Maven + run: mvn -B install --file aws-lambda-java-serialization/pom.xml + + # Run tests + - name: Run tests from aws-lambda-java-tests + run: mvn test --file aws-lambda-java-tests/pom.xml diff --git a/.github/workflows/aws-lambda-java-tests.yml b/.github/workflows/aws-lambda-java-tests.yml new file mode 100644 index 000000000..324c44514 --- /dev/null +++ b/.github/workflows/aws-lambda-java-tests.yml @@ -0,0 +1,47 @@ +# This workflow will be triggered if there will be changes to aws-lambda-java-tests +# package or its dependencies (events, serialization), and it builds the package. + +name: Java CI aws-lambda-java-tests + +on: + workflow_dispatch: + push: + branches: [ main ] + paths: + - 'aws-lambda-java-tests/**' + - 'aws-lambda-java-events/**' + - 'aws-lambda-java-serialization/**' + pull_request: + branches: [ '*' ] + paths: + - 'aws-lambda-java-tests/**' + - 'aws-lambda-java-events/**' + - 'aws-lambda-java-serialization/**' + - '.github/workflows/aws-lambda-java-tests.yml' + +permissions: + contents: read + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + - name: Set up JDK 1.8 + uses: actions/setup-java@v5 + with: + java-version: 8 + distribution: corretto + cache: maven + + # Install dependencies + - name: Install events with Maven + run: mvn -B install --file aws-lambda-java-events/pom.xml + - name: Install serialization with Maven + run: mvn -B install --file aws-lambda-java-serialization/pom.xml + + # Package target module + - name: Package tests with Maven + run: mvn -B package --file aws-lambda-java-tests/pom.xml diff --git a/.github/workflows/build-integration-test.yml b/.github/workflows/build-integration-test.yml new file mode 100644 index 000000000..2a6bb30c5 --- /dev/null +++ b/.github/workflows/build-integration-test.yml @@ -0,0 +1,86 @@ +# this workflow verifies that the integration test Lambda function builds successfully. +# it does NOT deploy or run the tests (that requires AWS credentials and is done in +# run-integration-test.yml). + +name: Build integration tests + +on: + push: + branches: [ main ] + paths: + - 'aws-lambda-java-log4j2/**' + - 'aws-lambda-java-core/**' + - 'lambda-integration-tests/**' + pull_request: + branches: [ '*' ] + paths: + - 'aws-lambda-java-log4j2/**' + - 'aws-lambda-java-core/**' + - 'lambda-integration-tests/**' + - '.github/workflows/build-integration-test.yml' + +permissions: + contents: read + +jobs: + load-matrix: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set.outputs.matrix }} + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + + - name: Load test matrix + id: set + run: | + MATRIX=$(jq -c '.' .github/test-matrix.json) + echo "matrix=${MATRIX}" >> "$GITHUB_OUTPUT" + + build-arch: + needs: load-matrix + runs-on: ${{ matrix.arch.runner }} + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.load-matrix.outputs.matrix) }} + name: "build (${{ matrix.arch.label }})" + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + + - name: Set up JDK + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 + with: + java-version: | + 8 + 21 + distribution: corretto + cache: maven + + - name: Install core with Maven + run: | + export JAVA_HOME=$JAVA_HOME_8_${{ matrix.arch.java_suffix }} + mvn -B install --file aws-lambda-java-core/pom.xml + + - name: Install log4j2 with Maven + run: | + export JAVA_HOME=$JAVA_HOME_8_${{ matrix.arch.java_suffix }} + mvn -B install --file aws-lambda-java-log4j2/pom.xml + + # build the integration test function + # this verifies that the function compiles and packages correctly. + # the tests will run in run-integration-test.yml which deploys to AWS. + - name: Package integration test function + run: | + export JAVA_HOME=$JAVA_HOME_21_${{ matrix.arch.java_suffix }} + mvn -B package --file lambda-integration-tests/log4j2-test-function/pom.xml + + build: + needs: build-arch + if: always() + runs-on: ubuntu-latest + steps: + - name: Check build results + run: | + if [ "${{ needs.build-arch.result }}" != "success" ]; then + echo "Build failed on one or more architectures" + exit 1 + fi diff --git a/.github/workflows/maven-build-all.yml b/.github/workflows/maven-build-all.yml deleted file mode 100644 index 67c4a6112..000000000 --- a/.github/workflows/maven-build-all.yml +++ /dev/null @@ -1,33 +0,0 @@ -# This workflow will build all Java packages in this project with Maven (Java 8) - -name: Java CI with Maven - -on: - push: - branches: [ master ] - pull_request: - branches: [ '*' ] - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 1.8 - uses: actions/setup-java@v1 - with: - java-version: 1.8 - - # Install base modules - - name: Build core with Maven - run: mvn -B install --file aws-lambda-java-core/pom.xml - - name: Build events with Maven - run: mvn -B install --file aws-lambda-java-events/pom.xml - - # Package modules that depend on base modules - - name: Build events-sdk-transformer with Maven - run: mvn -B package --file aws-lambda-java-events-sdk-transformer/pom.xml - - name: Build log4j2 with Maven - run: mvn -B package --file aws-lambda-java-log4j2/pom.xml diff --git a/.github/workflows/release-runtime-interface-client.yml b/.github/workflows/release-runtime-interface-client.yml new file mode 100644 index 000000000..e41dcfc61 --- /dev/null +++ b/.github/workflows/release-runtime-interface-client.yml @@ -0,0 +1,361 @@ +name: Release RIC to Maven Central + +# RIC ships a native JNI lib for 4 targets + a main JAR (5 artifacts). Each +# native lib is built on its own architecture (x86_64 and aarch64 CodeBuild +# runners) instead of emulating with QEMU. A build matrix produces the +# classifier JARs, then one job assembles and publishes them. + +on: + workflow_dispatch: + inputs: + releaseVersion: + description: 'Release version override (optional; defaults to the POM version without -SNAPSHOT)' + required: false + type: string + developmentVersion: + description: 'Next development version override (optional, must end with -SNAPSHOT)' + required: false + type: string + skip_publish: + description: 'Skip publish (dry-run validation)' + required: false + type: boolean + default: false + +permissions: + contents: write # push release commit and tag + id-token: write # assume the OIDC role for secret retrieval + +# Share the repo-wide "release" group with release.yml so RIC and the pure-Java +# modules can never publish concurrently. Never cancel in-flight: it could leave +# a half-published state. +concurrency: + group: release + cancel-in-progress: false + +env: + MODULE: aws-lambda-java-runtime-interface-client + RELEASE_VERSION_INPUT: ${{ github.event.inputs.releaseVersion }} + DEVELOPMENT_VERSION_INPUT: ${{ github.event.inputs.developmentVersion }} + MAVEN_ARGS: "-B --no-transfer-progress" + AWS_REGION: ${{ vars.AWS_REGION_MAVEN_RELEASE }} + OIDC_ROLE_ARN: ${{ secrets.AWS_ROLE_MAVEN_RELEASE }} + # ECR pull-through cache used for the native JNI base images. ECR_REGISTRY is + # the login target; BASE_REGISTRY (with the /ecr-public prefix) is passed to + # the Dockerfiles as a build-arg. + ECR_REGISTRY: ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ vars.AWS_REGION_MAVEN_RELEASE }}.amazonaws.com + BASE_REGISTRY: ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ vars.AWS_REGION_MAVEN_RELEASE }}.amazonaws.com/ecr-public + +jobs: + # Build each architecture's native libs (glibc + musl) on a native runner. + build-natives: + strategy: + fail-fast: true + matrix: + include: + - arch: x86_64 + runner: codebuild-aws-lambda-java-libs-test-trigger-x86-${{ github.run_id }}-${{ github.run_attempt }} + profiles: linux-x86_64 linux_musl-x86_64 + - arch: aarch64 + runner: codebuild-aws-lambda-java-libs-test-trigger-arm64-${{ github.run_id }}-${{ github.run_attempt }} + profiles: linux-aarch64 linux_musl-aarch64 + runs-on: ${{ matrix.runner }} + timeout-minutes: 45 + steps: + # Manual (workflow_dispatch) releases must only run from main, never from + # an arbitrary branch that could carry unreviewed release logic. Guarding + # the first job blocks the whole pipeline (release needs build-natives). + - name: Verify release branch + run: | + if [[ "$GITHUB_REF_NAME" != "main" ]]; then + echo "::error::Releases must run from the main branch, got '$GITHUB_REF_NAME'" + exit 1 + fi + + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + # Use the CodeBuild image's preinstalled Corretto 8. The image ships it at + # $JAVA_8_HOME but defaults JAVA_HOME to Java 25, so point JAVA_HOME/PATH at + # 8. Avoids actions/setup-java, which fetches from corretto.github.io + + # corretto.aws, both blocked by the runner egress lock. $JAVA_8_HOME + # resolves per-arch (x86_64/aarch64). + - name: Use the runner image's preinstalled Corretto 8 + run: | + echo "JAVA_HOME=$JAVA_8_HOME" >> "$GITHUB_ENV" + echo "$JAVA_8_HOME/bin" >> "$GITHUB_PATH" + "$JAVA_8_HOME/bin/java" -version + mkdir -p "$HOME/.m2" + cat > "$HOME/.m2/toolchains.xml" < + + + jdk + 8 + $JAVA_8_HOME + + + EOF + + # Route all mvn resolution through the CodeArtifact mirror. Must precede + # resolve-release-version, which invokes `mvn help:evaluate`. Ambient + # CodeBuild runner-role creds supply the token; no OIDC step in this job. + - name: Configure Maven CodeArtifact mirror + uses: ./.github/actions/configure-maven-mirror + + - name: Resolve and validate release version + uses: ./.github/actions/resolve-release-version + with: + module: ${{ env.MODULE }} + release-version-override: ${{ env.RELEASE_VERSION_INPUT }} + + # The native JNI build shells out to `docker build` against the ECR + # pull-through cache (see src/main/jni/Dockerfile.*). Authenticate first so + # the base-image pulls don't hit public.ecr.aws. Uses ambient runner creds. + - name: Log in to Amazon ECR (pull-through cache) + run: | + aws ecr get-login-password --region "$AWS_REGION" \ + | docker login --username AWS --password-stdin "$ECR_REGISTRY" + + # -DskipTests: only installed so the module compiles, not released here. + - name: Install intra-repo dependencies + run: | + for dep in aws-lambda-java-core aws-lambda-java-serialization; do + mvn install -DskipTests --file "$dep/pom.xml" + done + + # Build at the release version (matches the JAR names the release job + # attaches). + - name: Build native classifier JARs (${{ matrix.arch }}) + env: + IS_JAVA_8: true + run: | + mvn versions:set -DnewVersion="$EFFECTIVE_RELEASE_VERSION" -DgenerateBackupPoms=false --file "$MODULE/pom.xml" + for profile in ${{ matrix.profiles }}; do + echo "::group::Building $profile" + mvn package -P "$profile" -DmultiArch=false -DskipTests --file "$MODULE/pom.xml" + echo "::endgroup::" + done + + # JARs to attach + .so files to assemble the fat main JAR. + - name: Upload native artifacts + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: ric-natives-${{ matrix.arch }} + if-no-files-found: error + path: | + ${{ env.MODULE }}/target/*-linux*.jar + ${{ env.MODULE }}/target/classes/jni/*.so + + # Remove the user settings holding the CodeArtifact mirror token once the + # build is done. Ephemeral runner, so defence-in-depth, not load-bearing. + - name: Scrub Maven settings + if: always() + run: rm -f "$HOME/.m2/settings.xml" + + # Assemble all native builds and publish. + release: + needs: build-natives + runs-on: codebuild-aws-lambda-java-libs-test-trigger-x86-${{ github.run_id }}-${{ github.run_attempt }} + environment: Release + timeout-minutes: 30 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 0 # full history for tagging/pushing + + # Use the CodeBuild image's preinstalled Corretto 8. The image ships it at + # $JAVA_8_HOME but defaults JAVA_HOME to Java 25, so point JAVA_HOME/PATH at + # 8. Avoids actions/setup-java, which fetches from corretto.github.io + + # corretto.aws, both blocked by the runner egress lock. $JAVA_8_HOME + # resolves per-arch (x86_64/aarch64). + - name: Use the runner image's preinstalled Corretto 8 + run: | + echo "JAVA_HOME=$JAVA_8_HOME" >> "$GITHUB_ENV" + echo "$JAVA_8_HOME/bin" >> "$GITHUB_PATH" + "$JAVA_8_HOME/bin/java" -version + mkdir -p "$HOME/.m2" + cat > "$HOME/.m2/toolchains.xml" < + + + jdk + 8 + $JAVA_8_HOME + + + EOF + + # Route all mvn resolution through the CodeArtifact mirror. Must precede + # resolve-release-version (which invokes `mvn help:evaluate`) and the OIDC + # step (which would shadow the runner-role creds this needs). Runs on every + # path, since dependency resolution happens on dry-runs too. + - name: Configure Maven CodeArtifact mirror + uses: ./.github/actions/configure-maven-mirror + + - name: Resolve and validate release version + uses: ./.github/actions/resolve-release-version + with: + module: ${{ env.MODULE }} + release-version-override: ${{ env.RELEASE_VERSION_INPUT }} + + # The native JNI build shells out to `docker build` against the ECR + # pull-through cache (see src/main/jni/Dockerfile.*). Authenticate first so + # the base-image pulls don't hit public.ecr.aws. Uses ambient runner creds. + - name: Log in to Amazon ECR (pull-through cache) + run: | + aws ecr get-login-password --region "$AWS_REGION" \ + | docker login --username AWS --password-stdin "$ECR_REGISTRY" + + - name: Resolve next development version and tag + run: | + # Next development version: use the override, or bump the patch. + if [[ -n "$DEVELOPMENT_VERSION_INPUT" ]]; then + if [[ "$DEVELOPMENT_VERSION_INPUT" != *-SNAPSHOT ]]; then + echo "::error::developmentVersion '$DEVELOPMENT_VERSION_INPUT' must end with -SNAPSHOT" + exit 1 + fi + NEXT_DEV_VERSION="$DEVELOPMENT_VERSION_INPUT" + else + IFS='.' read -r MA MI PA <<< "$EFFECTIVE_RELEASE_VERSION" + NEXT_DEV_VERSION="${MA}.${MI}.$((PA + 1))-SNAPSHOT" + fi + + echo "NEXT_DEV_VERSION=$NEXT_DEV_VERSION" >> "$GITHUB_ENV" + echo "TAG_NAME=${MODULE}-${EFFECTIVE_RELEASE_VERSION}" >> "$GITHUB_ENV" + echo "::notice::Releasing $MODULE $EFFECTIVE_RELEASE_VERSION (next dev $NEXT_DEV_VERSION)" + + - name: Configure git user + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + # -DskipTests: only installed so the module compiles, not released here. + - name: Install intra-repo dependencies + run: | + for dep in aws-lambda-java-core aws-lambda-java-serialization; do + mvn install -DskipTests --file "$dep/pom.xml" + done + + - name: Set release version + run: mvn versions:set -DnewVersion="$EFFECTIVE_RELEASE_VERSION" -DgenerateBackupPoms=false --file "$MODULE/pom.xml" + + # Test gate before publish. + - name: Run tests + env: + IS_JAVA_8: true + run: mvn test -DargLineForReflectionTestOnly="" --file "$MODULE/pom.xml" + + # JARs to attach + .so files for the fat main JAR. + - name: Download native artifacts + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + pattern: ric-natives-* + path: ric-natives + + - name: Stage native artifacts + run: | + mkdir -p "$MODULE/target/classes/jni" + find ric-natives -name '*.jar' -exec cp {} "$MODULE/target/" \; + find ric-natives -name '*.so' -exec cp {} "$MODULE/target/classes/jni/" \; + echo "Staged native artifacts:" + ls -1 "$MODULE/target/"*-linux*.jar "$MODULE/target/classes/jni/"*.so + + - name: Configure AWS credentials (OIDC) + if: ${{ github.event.inputs.skip_publish != 'true' }} + uses: ./.github/actions/configure-release-aws-credentials + with: + aws-region: ${{ env.AWS_REGION }} + role-to-assume: ${{ env.OIDC_ROLE_ARN }} + role-session-name: GitHubActionsRicMavenCentralRelease + + # Fetch signing material and publish in a single step so the GPG passphrase + # and Sonatype token stay in this shell and never cross a $GITHUB_ENV + # boundary, where a later (possibly compromised) step could read them. + # -DmultiArch=false builds only the host .so; the aarch_64 .so is already + # staged, so the main JAR still bundles all four. build-helper attaches + # the staged classifier JARs. Gate already ran, so -DskipTests. + - name: Publish to Maven Central + if: ${{ github.event.inputs.skip_publish != 'true' }} + env: + IS_JAVA_8: true + run: | + # Scrub the settings.xml (contains the Sonatype token) and the keyring + # on exit, so no sensitive file is left on the runner even on failure. + MAVEN_SETTINGS="$RUNNER_TEMP/settings.xml" + export GNUPGHOME=$(mktemp -d) + trap 'rm -rf "$MAVEN_SETTINGS" "$GNUPGHOME"' EXIT + + # --- Signing key + Sonatype token (shared secrets from LambdaMavenDeploy) --- + GPG_JSON=$(aws secretsmanager get-secret-value --secret-id lambda-runtimes/java/gpg-signing-key --query SecretString --output text) + CREDS_JSON=$(aws secretsmanager get-secret-value --secret-id lambda-runtimes/java/maven-sonatype-creds --query SecretString --output text) + GPG_PRIVATE_KEY=$(jq -r '.private' <<< "$GPG_JSON") + GPG_PASSPHRASE=$(jq -r '.passphrase' <<< "$GPG_JSON") + SONATYPE_USERNAME=$(jq -r '."maven-central-login"' <<< "$CREDS_JSON") + SONATYPE_PASSWORD=$(jq -r '."maven-central-password"' <<< "$CREDS_JSON") + echo "::add-mask::$GPG_PASSPHRASE" + echo "::add-mask::$SONATYPE_USERNAME" + echo "::add-mask::$SONATYPE_PASSWORD" + + # Import the key with loopback pinentry so Maven can sign non-interactively. + chmod 700 "$GNUPGHOME" + echo "allow-loopback-pinentry" > "$GNUPGHOME/gpg-agent.conf" + echo "pinentry-mode loopback" > "$GNUPGHOME/gpg.conf" + gpgconf --kill gpg-agent || true + gpg --batch --import <<< "$GPG_PRIVATE_KEY" + GPG_KEYNAME=$(gpg --list-secret-keys --with-colons | awk -F: '/^sec:/ {print $5; exit}') + + # Global settings holding only the Sonatype "central" server for upload. + # Passed to Maven as -gs (global) so it MERGES with the CodeArtifact + # mirror in ~/.m2/settings.xml (user) that the mirror step wrote: deps + # resolve through the mirror, upload goes to central, and the mirror + # token stays in that user file instead of being re-passed here. + { + echo '' + echo "central" + echo "${SONATYPE_USERNAME}" + echo "${SONATYPE_PASSWORD}" + echo '' + } > "$MAVEN_SETTINGS" + + # --- Publish --- (-gs: merge Sonatype creds with the ~/.m2 mirror) + mvn deploy -Prelease -DskipTests -DmultiArch=false \ + -gs "$MAVEN_SETTINGS" \ + -Dgpg.keyname="$GPG_KEYNAME" -Dgpg.passphrase="$GPG_PASSPHRASE" \ + --file "$MODULE/pom.xml" + + - name: Tag and push (only after publish succeeds) + if: ${{ github.event.inputs.skip_publish != 'true' }} + run: | + git commit -am "chore(ric): release ${EFFECTIVE_RELEASE_VERSION}" + git tag "$TAG_NAME" + mvn versions:set -DnewVersion="$NEXT_DEV_VERSION" -DgenerateBackupPoms=false --file "$MODULE/pom.xml" + git commit -am "chore(ric): prepare next development ${NEXT_DEV_VERSION}" + git push --atomic origin "HEAD:${GITHUB_REF_NAME}" "refs/tags/${TAG_NAME}" + + # Dry-run: validate assembly, no publish/push. + - name: Dry-run assemble (no publish) + if: ${{ github.event.inputs.skip_publish == 'true' }} + env: + IS_JAVA_8: true + run: mvn package -DskipTests -DmultiArch=false --file "$MODULE/pom.xml" + + # Nothing was pushed, so this only cleans the runner. + - name: Roll back local tag on failure + if: ${{ failure() && github.event.inputs.skip_publish != 'true' }} + run: | + git tag -d "$TAG_NAME" 2>/dev/null || true + echo "::warning::Release failed. The remote was not modified; safe to retry." + + - name: Summary + if: ${{ github.event.inputs.skip_publish != 'true' }} + run: | + echo "## Release Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Module | \`$MODULE\` |" >> $GITHUB_STEP_SUMMARY + echo "| Version | \`$EFFECTIVE_RELEASE_VERSION\` |" >> $GITHUB_STEP_SUMMARY + echo "| Tag | \`$TAG_NAME\` |" >> $GITHUB_STEP_SUMMARY + echo "| Artifacts | main JAR + linux/linux_musl x x86_64/aarch_64 classifier JARs |" >> $GITHUB_STEP_SUMMARY + echo "| Built natively | x86_64 and aarch_64 on CodeBuild runners (no QEMU) |" >> $GITHUB_STEP_SUMMARY + echo "| Maven Central | [com.amazonaws:$MODULE:$EFFECTIVE_RELEASE_VERSION](https://central.sonatype.com/artifact/com.amazonaws/$MODULE/$EFFECTIVE_RELEASE_VERSION) |" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..7ff834e56 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,297 @@ +name: Release to Maven Central + +# Builds, tests, and publishes a module to Maven Central in one environment. + +on: + workflow_dispatch: + inputs: + module: + description: 'Module to release (directory name, e.g. aws-lambda-java-log4j2)' + required: true + type: choice + # aws-lambda-java-runtime-interface-client is intentionally excluded: it + # ships a cross-compiled JNI native library and has its own dedicated + # pipeline, .github/workflows/release-runtime-interface-client.yml. + options: + - aws-lambda-java-core + - aws-lambda-java-events + - aws-lambda-java-events-sdk-transformer + - aws-lambda-java-log4j2 + - aws-lambda-java-serialization + - aws-lambda-java-tests + releaseVersion: + description: 'Release version override (optional; defaults to the POM version without -SNAPSHOT)' + required: false + type: string + developmentVersion: + description: 'Next development version override (optional, must end with -SNAPSHOT)' + required: false + type: string + skip_publish: + description: 'Skip publish (dry-run validation)' + required: false + type: boolean + default: false + +permissions: + contents: write + id-token: write + +# Serialize all releases repo-wide to avoid concurrent pushes racing on the +# default branch. Never cancel in-flight: it could leave a half-published state. +concurrency: + group: release + cancel-in-progress: false + +env: + MODULE: ${{ github.event.inputs.module }} + RELEASE_VERSION_INPUT: ${{ github.event.inputs.releaseVersion }} + DEVELOPMENT_VERSION_INPUT: ${{ github.event.inputs.developmentVersion }} + # Batch mode + no transfer-progress spam for every Maven call (Maven 3.9+). + MAVEN_ARGS: "-B --no-transfer-progress" + AWS_REGION: ${{ vars.AWS_REGION_MAVEN_RELEASE }} + OIDC_ROLE_ARN: ${{ secrets.AWS_ROLE_MAVEN_RELEASE }} + +jobs: + # Pre-publish gate for log4j2: deploy a real Lambda, invoke it, + # and assert the log line reaches CloudWatch. Binds the end-to-end validation + # to the publish event itself. Skipped for every other module, which are + # covered by their own tests (or the cross-module gate below). + integration-test: + if: ${{ github.event.inputs.module == 'aws-lambda-java-log4j2' }} + uses: ./.github/workflows/run-integration-test.yml + secrets: inherit + + release: + needs: [integration-test] + # Publish when the gate passed, or when it was skipped for a non-log4j2 + # module. A failed or cancelled gate blocks the release. + if: ${{ always() && (needs.integration-test.result == 'success' || needs.integration-test.result == 'skipped') }} + runs-on: codebuild-aws-lambda-java-libs-test-trigger-x86-${{ github.run_id }}-${{ github.run_attempt }} + environment: Release + timeout-minutes: 30 + + steps: + # Manual (workflow_dispatch) releases must only run from main, never from + # an arbitrary branch that could carry unreviewed release logic. + - name: Verify release branch + run: | + if [[ "$GITHUB_REF_NAME" != "main" ]]; then + echo "::error::Releases must run from the main branch, got '$GITHUB_REF_NAME'" + exit 1 + fi + + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + fetch-depth: 0 # full history for tagging/pushing + + # Use the CodeBuild image's preinstalled Corretto 8. The image ships it at + # $JAVA_8_HOME but defaults JAVA_HOME to Java 25, so point JAVA_HOME/PATH at + # 8. Avoids actions/setup-java, which fetches from corretto.github.io + + # corretto.aws, both blocked by the runner egress lock. $JAVA_8_HOME + # resolves per-arch (x86_64/aarch64). + - name: Use the runner image's preinstalled Corretto 8 + run: | + echo "JAVA_HOME=$JAVA_8_HOME" >> "$GITHUB_ENV" + echo "$JAVA_8_HOME/bin" >> "$GITHUB_PATH" + "$JAVA_8_HOME/bin/java" -version + mkdir -p "$HOME/.m2" + cat > "$HOME/.m2/toolchains.xml" < + + + jdk + 8 + $JAVA_8_HOME + + + EOF + + # Route all mvn resolution through the CodeArtifact mirror. Runs before the + # OIDC step (which would shadow the runner-role creds this needs) and on + # every path, since dependency resolution happens on dry-runs too. + - name: Configure Maven CodeArtifact mirror + uses: ./.github/actions/configure-maven-mirror + + - name: Resolve and validate release version + uses: ./.github/actions/resolve-release-version + with: + module: ${{ env.MODULE }} + release-version-override: ${{ env.RELEASE_VERSION_INPUT }} + validate-module-dir: "true" + + - name: Validate development version override + run: | + if [[ -n "$DEVELOPMENT_VERSION_INPUT" && "$DEVELOPMENT_VERSION_INPUT" != *-SNAPSHOT ]]; then + echo "::error::developmentVersion '$DEVELOPMENT_VERSION_INPUT' must end with -SNAPSHOT" + exit 1 + fi + echo "::notice::Releasing $MODULE $EFFECTIVE_RELEASE_VERSION (POM currently $CURRENT_VERSION)" + + - name: Configure git user + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Install intra-repo dependencies + run: | + # Installed so the target compiles. -DskipTests: not released here, + # only the target module gets the full verify gate below. + declare -A DEPS + DEPS[aws-lambda-java-core]="" + DEPS[aws-lambda-java-events]="" + DEPS[aws-lambda-java-serialization]="" + DEPS[aws-lambda-java-log4j2]="aws-lambda-java-core" + DEPS[aws-lambda-java-events-sdk-transformer]="aws-lambda-java-events" + DEPS[aws-lambda-java-tests]="aws-lambda-java-core aws-lambda-java-serialization aws-lambda-java-events" + + DEP_LIST="${DEPS[$MODULE]}" + if [[ -n "$DEP_LIST" ]]; then + for dep in $DEP_LIST; do + echo "::group::Installing dependency: $dep" + mvn install -DskipTests --file "$dep/pom.xml" + echo "::endgroup::" + done + else + echo "::notice::No intra-repo dependencies for $MODULE" + fi + + - name: Run tests + run: mvn verify --file "$MODULE/pom.xml" + + # Cross-module gate: serialization has no tests in its own build, so the + # `mvn verify` above exercises nothing. Its behavioral coverage lives in + # aws-lambda-java-tests, which depends on serialization via a version + # property. Install the just-built serialization and run that suite + # against it, so we never publish serialization the suite hasn't exercised. + - name: Run cross-module test gate + run: | + case "$MODULE" in + aws-lambda-java-serialization) + MOD_VER=$(mvn -q -DforceStdout help:evaluate -Dexpression=project.version --file "$MODULE/pom.xml") + MOD_VER="${MOD_VER//[$'\r\n']/}" + echo "::group::Installing $MODULE $MOD_VER for the gate" + mvn install -DskipTests --file "$MODULE/pom.xml" + echo "::endgroup::" + echo "::notice::Gating $MODULE on aws-lambda-java-tests (aws-lambda-java-serialization.version=$MOD_VER)" + mvn verify -Daws-lambda-java-serialization.version="$MOD_VER" --file aws-lambda-java-tests/pom.xml + ;; + *) + echo "::notice::No cross-module test gate for $MODULE" + ;; + esac + + - name: Configure AWS credentials (OIDC) + if: ${{ github.event.inputs.skip_publish != 'true' }} + uses: ./.github/actions/configure-release-aws-credentials + with: + aws-region: ${{ env.AWS_REGION }} + role-to-assume: ${{ env.OIDC_ROLE_ARN }} + role-session-name: GitHubActionsMavenCentralRelease + + # Fetch signing material and publish in a single step so the GPG passphrase + # and Sonatype token stay in this shell and never cross a $GITHUB_ENV + # boundary, where a later (possibly compromised) step could read them. + # prepare/perform aren't atomic: prepare locally, publish, push only after. + - name: Release (prepare locally, publish, then push) + if: ${{ github.event.inputs.skip_publish != 'true' }} + run: | + # Scrub the settings.xml (contains the Sonatype token) and the keyring + # on exit, so no sensitive file is left on the runner even on failure. + MAVEN_SETTINGS="$RUNNER_TEMP/settings.xml" + export GNUPGHOME=$(mktemp -d) + trap 'rm -rf "$MAVEN_SETTINGS" "$GNUPGHOME"' EXIT + + # --- Signing key + Sonatype token (shared secrets from LambdaMavenDeploy) --- + GPG_JSON=$(aws secretsmanager get-secret-value --secret-id lambda-runtimes/java/gpg-signing-key --query SecretString --output text) + CREDS_JSON=$(aws secretsmanager get-secret-value --secret-id lambda-runtimes/java/maven-sonatype-creds --query SecretString --output text) + GPG_PRIVATE_KEY=$(jq -r '.private' <<< "$GPG_JSON") + GPG_PASSPHRASE=$(jq -r '.passphrase' <<< "$GPG_JSON") + SONATYPE_USERNAME=$(jq -r '."maven-central-login"' <<< "$CREDS_JSON") + SONATYPE_PASSWORD=$(jq -r '."maven-central-password"' <<< "$CREDS_JSON") + echo "::add-mask::$GPG_PASSPHRASE" + echo "::add-mask::$SONATYPE_USERNAME" + echo "::add-mask::$SONATYPE_PASSWORD" + + # Import the key with loopback pinentry so Maven can sign non-interactively. + chmod 700 "$GNUPGHOME" + echo "allow-loopback-pinentry" > "$GNUPGHOME/gpg-agent.conf" + echo "pinentry-mode loopback" > "$GNUPGHOME/gpg.conf" + gpgconf --kill gpg-agent || true + gpg --batch --import <<< "$GPG_PRIVATE_KEY" + GPG_KEYNAME=$(gpg --list-secret-keys --with-colons | awk -F: '/^sec:/ {print $5; exit}') + + # Global settings holding only the Sonatype "central" server for upload. + # Passed to Maven as -gs (global) so it MERGES with the CodeArtifact + # mirror in ~/.m2/settings.xml (user) that the mirror step wrote: deps + # resolve through the mirror, upload goes to central, and the mirror + # token stays in that user file instead of being re-passed here. + { + echo '' + echo "central" + echo "${SONATYPE_USERNAME}" + echo "${SONATYPE_PASSWORD}" + echo '' + } > "$MAVEN_SETTINGS" + + # --- Release: build args as an array so each value is a single, + # properly quoted argument (no word-splitting of untrusted input). --- + RELEASE_ARGS=(-DreleaseVersion="$EFFECTIVE_RELEASE_VERSION") + if [[ -n "$DEVELOPMENT_VERSION_INPUT" ]]; then + RELEASE_ARGS+=(-DdevelopmentVersion="$DEVELOPMENT_VERSION_INPUT") + fi + + # Prepare locally (no push): release commits + tag. + mvn release:prepare -DpushChanges=false "${RELEASE_ARGS[@]}" --file "$MODULE/pom.xml" + + # perform forks a fresh build. Pass the Sonatype creds as GLOBAL + # settings (-gs) so the fork still auto-reads ~/.m2/settings.xml (the + # mirror) and merges the two. + mvn release:perform -DlocalCheckout=true \ + -Darguments="-gs $MAVEN_SETTINGS -Prelease -Dgpg.keyname=$GPG_KEYNAME -Dgpg.passphrase=$GPG_PASSPHRASE" \ + --file "$MODULE/pom.xml" + + # Push commits + tag atomically, only after publish succeeded. + git push --atomic origin \ + "HEAD:${GITHUB_REF_NAME}" \ + "refs/tags/${MODULE}-${EFFECTIVE_RELEASE_VERSION}" + + - name: Dry-run release (prepare only, no publish) + if: ${{ github.event.inputs.skip_publish == 'true' }} + run: | + RELEASE_ARGS=(-DreleaseVersion="$EFFECTIVE_RELEASE_VERSION") + if [[ -n "$DEVELOPMENT_VERSION_INPUT" ]]; then + RELEASE_ARGS+=(-DdevelopmentVersion="$DEVELOPMENT_VERSION_INPUT") + fi + mvn release:prepare -DdryRun=true "${RELEASE_ARGS[@]}" --file "$MODULE/pom.xml" + mvn release:clean --file "$MODULE/pom.xml" || true + + # Nothing was pushed, so this only cleans the runner for a retry. + - name: Roll back release on failure + if: ${{ failure() && github.event.inputs.skip_publish != 'true' }} + run: | + mvn release:rollback --file "$MODULE/pom.xml" || true + mvn release:clean --file "$MODULE/pom.xml" || true + git tag -d "${MODULE}-${EFFECTIVE_RELEASE_VERSION}" 2>/dev/null || true + echo "::warning::Release failed before publish completed. The remote was not modified; the runner state has been rolled back. Safe to retry." + + - name: Summary + if: ${{ github.event.inputs.skip_publish != 'true' }} + run: | + TAG_NAME="${MODULE}-${EFFECTIVE_RELEASE_VERSION}" + echo "## Release Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Module | \`$MODULE\` |" >> $GITHUB_STEP_SUMMARY + echo "| Version | \`$EFFECTIVE_RELEASE_VERSION\` |" >> $GITHUB_STEP_SUMMARY + echo "| Tag | \`$TAG_NAME\` |" >> $GITHUB_STEP_SUMMARY + echo "| Maven Central | [com.amazonaws:$MODULE:$EFFECTIVE_RELEASE_VERSION](https://central.sonatype.com/artifact/com.amazonaws/$MODULE/$EFFECTIVE_RELEASE_VERSION) |" >> $GITHUB_STEP_SUMMARY + + # Symmetry with the publish step's in-shell scrub: remove the user settings + # holding the CodeArtifact mirror token. Last step, after the mvn-using + # rollback path, so nothing still needs it. The runner is ephemeral, so + # this is defence-in-depth, not load-bearing. + - name: Scrub Maven settings + if: always() + run: rm -f "$HOME/.m2/settings.xml" diff --git a/.github/workflows/repo-sync.yml b/.github/workflows/repo-sync.yml new file mode 100644 index 000000000..4934754d8 --- /dev/null +++ b/.github/workflows/repo-sync.yml @@ -0,0 +1,39 @@ +name: Repo Sync + +on: + schedule: + - cron: "0 8 * * 1-5" # At 08:00 on every day-of-week from Monday through Friday + pull_request: + branches: [ '*' ] + paths: + - '.github/workflows/repo-sync.yml' + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + repo-sync: + name: Repo Sync + runs-on: ubuntu-latest + env: + IS_CONFIGURED: ${{ secrets.SOURCE_REPO != '' }} + steps: + - uses: actions/checkout@v6 + if: ${{ env.IS_CONFIGURED == 'true' }} + - uses: repo-sync/github-sync@3832fe8e2be32372e1b3970bbae8e7079edeec88 # v2.3.0 + name: Sync repo to branch + if: ${{ env.IS_CONFIGURED == 'true' }} + with: + source_repo: ${{ secrets.SOURCE_REPO }} + source_branch: main + destination_branch: ${{ secrets.INTERMEDIATE_BRANCH }} + github_token: ${{ secrets.GITHUB_TOKEN }} + - uses: repo-sync/pull-request@7e79a9f5dc3ad0ce53138f01df2fad14a04831c5 # v2.12.1 + name: Create pull request + if: ${{ env.IS_CONFIGURED == 'true' }} + with: + source_branch: ${{ secrets.INTERMEDIATE_BRANCH }} + destination_branch: main + github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/run-integration-test.yml b/.github/workflows/run-integration-test.yml new file mode 100644 index 000000000..35456a7c5 --- /dev/null +++ b/.github/workflows/run-integration-test.yml @@ -0,0 +1,139 @@ +# this workflow deploys a Lambda function that uses aws-lambda-java-log4j2, +# invokes it, and verifies that logs arrive in CloudWatch. + +name: Run integration tests + +permissions: + id-token: write + contents: read + +on: + workflow_dispatch: + workflow_call: + push: + branches: [ main ] + paths: + - 'aws-lambda-java-log4j2/**' + - 'aws-lambda-java-core/**' + - 'lambda-integration-tests/**' + +jobs: + load-matrix: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set.outputs.matrix }} + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + + - name: Load test matrix + id: set + run: | + MATRIX=$(jq -c '.' .github/test-matrix.json) + echo "matrix=${MATRIX}" >> "$GITHUB_OUTPUT" + + run-integration-tests: + needs: load-matrix + # Only run on the main repo, not forks + if: ${{ github.repository_owner == 'aws' }} + runs-on: ${{ matrix.arch.runner }} + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.load-matrix.outputs.matrix) }} + name: "integration-test (${{ matrix.arch.label }})" + concurrency: + group: integration-test-${{ matrix.arch.label }} + cancel-in-progress: false + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + + - name: Set up JDK + uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 + with: + java-version: | + 8 + 21 + distribution: corretto + cache: maven + + - name: Install SAM CLI + uses: aws-actions/setup-sam@f84ec7d548307efafe33230528756de3c5841a17 # v2 + with: + use-installer: true + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6.0.0 + with: + role-to-assume: ${{ secrets.AWS_ROLE_LOG4J2_INTEG_TEST }} + role-session-name: GitHubActionsLog4j2IntegTest + aws-region: ${{ secrets.AWS_REGION_LOG4J2_INTEG_TEST }} + + - name: Install core with Maven + run: | + export JAVA_HOME=$JAVA_HOME_8_${{ matrix.arch.java_suffix }} + mvn -B install --file aws-lambda-java-core/pom.xml + + - name: Install log4j2 with Maven + run: | + export JAVA_HOME=$JAVA_HOME_8_${{ matrix.arch.java_suffix }} + mvn -B install --file aws-lambda-java-log4j2/pom.xml + + - name: Build SAM stack + run: | + export JAVA_HOME=$JAVA_HOME_21_${{ matrix.arch.java_suffix }} + cd lambda-integration-tests && sam build + + - name: Validate SAM stack + run: cd lambda-integration-tests && sam validate --lint + + - name: Deploy stack + id: deploy_stack + env: + AWS_REGION: ${{ secrets.AWS_REGION_LOG4J2_INTEG_TEST }} + run: | + cd lambda-integration-tests + stackName="aws-lambda-java-log4j2-integ-test-${{ matrix.arch.label }}-$GITHUB_RUN_ID" + echo "STACK_NAME=$stackName" >> "$GITHUB_OUTPUT" + echo "Stack name = $stackName" + sam deploy \ + --stack-name "${stackName}" \ + --parameter-overrides "ParameterKey=LambdaRole,ParameterValue=${{ secrets.AWS_LAMBDA_ROLE_LOG4J2_INTEG_TEST }} ParameterKey=Architecture,ParameterValue=${{ matrix.arch.sam_arch }}" \ + --no-confirm-changeset \ + --no-progressbar \ + --s3-bucket "${{ secrets.S3_BUCKET_LOG4J2_INTEG_TEST }}" \ + --capabilities CAPABILITY_IAM \ + 2>&1 | tee /tmp/sam-deploy.log | tail -n 20 + + # Verify stack is in a healthy state + STACK_STATUS=$(aws cloudformation describe-stacks \ + --stack-name "${stackName}" \ + --region "${AWS_REGION}" \ + --query 'Stacks[0].StackStatus' \ + --output text 2>&1) + echo "Stack status: $STACK_STATUS" + if [ "$STACK_STATUS" != "CREATE_COMPLETE" ] && [ "$STACK_STATUS" != "UPDATE_COMPLETE" ]; then + echo "FAIL: Stack is not in a healthy state (status: $STACK_STATUS)" + aws cloudformation describe-stack-events \ + --stack-name "${stackName}" \ + --region "${AWS_REGION}" \ + --query 'StackEvents[?ResourceStatus==`CREATE_FAILED` || ResourceStatus==`UPDATE_FAILED`].[LogicalResourceId,ResourceStatusReason]' \ + --output table 2>&1 || true + exit 1 + fi + + LOG4J2_TEST_FUNCTION=$(sam list stack-outputs --stack-name "${stackName}" --output json | jq -r '.[] | select(.OutputKey=="Log4j2TestFunction") | .OutputValue') + echo "LOG4J2_TEST_FUNCTION=$LOG4J2_TEST_FUNCTION" >> "$GITHUB_OUTPUT" + echo "Function name: $LOG4J2_TEST_FUNCTION" + + - name: Run integration test + env: + LOG4J2_TEST_FUNCTION: ${{ steps.deploy_stack.outputs.LOG4J2_TEST_FUNCTION }} + AWS_REGION: ${{ secrets.AWS_REGION_LOG4J2_INTEG_TEST }} + run: ./lambda-integration-tests/run-tests.sh + + - name: Cleanup + if: always() && steps.deploy_stack.outputs.STACK_NAME + env: + AWS_REGION: ${{ secrets.AWS_REGION_LOG4J2_INTEG_TEST }} + STACK_NAME: ${{ steps.deploy_stack.outputs.STACK_NAME }} + run: | + sam delete --stack-name "${STACK_NAME}" --no-prompts --region "${AWS_REGION}" diff --git a/.github/workflows/runtime-interface-client_merge_to_main.yml b/.github/workflows/runtime-interface-client_merge_to_main.yml new file mode 100644 index 000000000..d0d479111 --- /dev/null +++ b/.github/workflows/runtime-interface-client_merge_to_main.yml @@ -0,0 +1,96 @@ +# This workflow will be triggered on merge to the main branch if +# aws-lambda-java-runtime-interface-client package was changed +# +# It will publish artifacts to CodeArtifact repository, specified by properties defined in GitHub repo secrets: +# CODE_ARTIFACT_REPO_ACCOUNT, AWS_REGION, CODE_ARTIFACT_REPO_NAME, CODE_ARTIFACT_DOMAIN +# and will assume role specified by AWS_ROLE +# +# Prerequisite setup: +# https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services + +name: Publish artifact for aws-lambda-java-runtime-interface-client + +on: + workflow_dispatch: + push: + branches: [ main ] + paths: + - 'aws-lambda-java-runtime-interface-client/**' + - '.github/workflows/runtime-interface-client_*.yml' + +jobs: + + publish: + runs-on: ubuntu-latest + + permissions: + id-token: write + contents: read + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Set up JDK 1.8 + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 + with: + java-version: 8 + distribution: corretto + cache: maven + + - name: Set up QEMU + uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 + with: + install: true + + - name: Available buildx platforms + run: echo ${{ steps.buildx.outputs.platforms }} + + - name: Build and install serialization dependency locally + working-directory: ./aws-lambda-java-serialization + run: mvn clean install + + - name: Test Runtime Interface Client xplatform build - Run 'build' target + working-directory: ./aws-lambda-java-runtime-interface-client + run: make build + env: + IS_JAVA_8: true + + - name: Issue AWS credentials + if: env.ENABLE_SNAPSHOT != null + env: + ENABLE_SNAPSHOT: ${{ secrets.ENABLE_SNAPSHOT }} + uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4 + with: + aws-region: ${{ secrets.AWS_REGION }} + role-to-assume: ${{ secrets.AWS_ROLE }} + role-session-name: GitHubActionsPublishPackage + role-duration-seconds: 900 + + - name: Prepare codeartifact properties + if: env.ENABLE_SNAPSHOT != null + env: + ENABLE_SNAPSHOT: ${{ secrets.ENABLE_SNAPSHOT }} + working-directory: ./aws-lambda-java-runtime-interface-client/ric-dev-environment + run: | + cat < codeartifact-properties.mk + CODE_ARTIFACT_REPO_ACCOUNT=${{ secrets.AWS_ACCOUNT }} + CODE_ARTIFACT_REPO_REGION=${{ env.AWS_REGION }} + CODE_ARTIFACT_REPO_NAME=${{ secrets.CODE_ARTIFACT_REPO_NAME }} + CODE_ARTIFACT_DOMAIN=${{ secrets.AWS_CODEARTIFACT_DOMAIN }} + EOF + + - name: Publish + if: env.ENABLE_SNAPSHOT != null + working-directory: ./aws-lambda-java-runtime-interface-client + env: + ENABLE_SNAPSHOT: ${{ secrets.ENABLE_SNAPSHOT }} + run: make publish + + - name: Upload coverage to Codecov + if: env.CODECOV_TOKEN != null + uses: codecov/codecov-action@75cd11691c0faa626561e295848008c8a7dddffe # v5 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/runtime-interface-client_pr.yml b/.github/workflows/runtime-interface-client_pr.yml new file mode 100644 index 000000000..bc9e3f3eb --- /dev/null +++ b/.github/workflows/runtime-interface-client_pr.yml @@ -0,0 +1,186 @@ +# This workflow will be triggered if there will be changes to +# aws-lambda-java-runtime-interface-client package or its dependencies (core, serialization), +# and it builds the package. + +name: PR to runtime-interface-client + +on: + workflow_dispatch: + pull_request: + branches: [ '*' ] + paths: + - 'aws-lambda-java-runtime-interface-client/**' + - 'aws-lambda-java-core/**' + - 'aws-lambda-java-serialization/**' + - '.github/workflows/runtime-interface-client_*.yml' + +permissions: + contents: read + +jobs: + + smoke-test-arch: + strategy: + fail-fast: true + matrix: + include: + - arch: x86_64 + runner: ubuntu-latest + - arch: aarch64 + runner: ubuntu-24.04-arm + runs-on: ${{ matrix.runner }} + name: "smoke-test (${{ matrix.arch }})" + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Set up JDK 1.8 + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 + with: + java-version: 8 + distribution: corretto + cache: maven + + - name: Build and install core dependency locally + working-directory: ./aws-lambda-java-core + run: mvn clean install + + - name: Build and install serialization dependency locally + working-directory: ./aws-lambda-java-serialization + run: mvn clean install + + - name: Runtime Interface Client smoke tests - Run 'pr-${{ matrix.arch }}' target + working-directory: ./aws-lambda-java-runtime-interface-client + run: make pr-${{ matrix.arch }} + env: + IS_JAVA_8: true + + build-arch: + strategy: + fail-fast: true + matrix: + include: + - arch: x86_64 + runner: ubuntu-latest + - arch: aarch64 + runner: ubuntu-24.04-arm + runs-on: ${{ matrix.runner }} + name: "build (${{ matrix.arch }})" + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Set up JDK 1.8 + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 + with: + java-version: 8 + distribution: corretto + cache: maven + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 + with: + install: true + + - name: Build and install core dependency locally + working-directory: ./aws-lambda-java-core + run: mvn clean install + + - name: Build and install serialization dependency locally + working-directory: ./aws-lambda-java-serialization + run: mvn clean install + + - name: Test Runtime Interface Client build - Run 'build-${{ matrix.arch }}' target + working-directory: ./aws-lambda-java-runtime-interface-client + run: make build-${{ matrix.arch }} + env: + IS_JAVA_8: true + + - name: Save the built jar + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: aws-lambda-java-runtime-interface-client-${{ matrix.arch }} + path: ./aws-lambda-java-runtime-interface-client/target/aws-lambda-java-runtime-interface-client-*.jar + + - name: Upload coverage to Codecov + if: env.CODECOV_TOKEN != null + uses: codecov/codecov-action@75cd11691c0faa626561e295848008c8a7dddffe # v5 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + integration-test-matrix: + runs-on: ${{ matrix.arch.runner }} + strategy: + # Run every OS/arch combination to completion so one failure doesn't mask the others. + fail-fast: false + matrix: + buildspec: + - buildspec.os.alpine.yml + - buildspec.os.amazoncorretto.yml + - buildspec.os.amazonlinux.1.yml + - buildspec.os.amazonlinux.2.yml + - buildspec.os.debian.yml + - buildspec.os.ubuntu.yml + arch: + - label: x64 + runner: ubuntu-latest + platform: linux/amd64 + - label: arm64 + runner: ubuntu-24.04-arm + platform: linux/arm64/v8 + exclude: + # Amazon Linux 1 was never published for ARM64 (x86_64 only), so + # public.ecr.aws/amazonlinux/amazonlinux:1 has no arm64 manifest. + - buildspec: buildspec.os.amazonlinux.1.yml + arch: + label: arm64 + runner: ubuntu-24.04-arm + platform: linux/arm64/v8 + name: "integration-test (${{ matrix.buildspec }} / ${{ matrix.arch.label }})" + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 + with: + install: true + + - name: Run OS integration test - 'test-integ' target + working-directory: ./aws-lambda-java-runtime-interface-client + run: make test-integ BUILDSPEC=test/integration/codebuild/${{ matrix.buildspec }} + env: + PLATFORM_FILTER: ${{ matrix.arch.platform }} + + integration-test: + needs: integration-test-matrix + if: always() + runs-on: ubuntu-latest + steps: + - name: Check integration-test results + run: | + if [ "${{ needs.integration-test-matrix.result }}" != "success" ]; then + echo "Integration tests failed on one or more OS/arch combinations" + exit 1 + fi + + smoke-test: + needs: smoke-test-arch + if: always() + runs-on: ubuntu-latest + steps: + - name: Check smoke-test results + run: | + if [ "${{ needs.smoke-test-arch.result }}" != "success" ]; then + echo "Smoke tests failed on one or more architectures" + exit 1 + fi + + build: + needs: build-arch + if: always() + runs-on: ubuntu-latest + steps: + - name: Check build results + run: | + if [ "${{ needs.build-arch.result }}" != "success" ]; then + echo "Build failed on one or more architectures" + exit 1 + fi diff --git a/.github/workflows/samples.yml b/.github/workflows/samples.yml new file mode 100644 index 000000000..68e25827d --- /dev/null +++ b/.github/workflows/samples.yml @@ -0,0 +1,86 @@ +# This workflow will be triggered if there will be changes to samples +# or their dependencies (events, serialization, tests). + +name: Java CI samples + +on: + workflow_dispatch: + push: + branches: [ main ] + paths: + - 'samples/**' + - 'aws-lambda-java-events/**' + - 'aws-lambda-java-serialization/**' + - 'aws-lambda-java-tests/**' + pull_request: + branches: [ '*' ] + paths: + - 'samples/**' + - 'aws-lambda-java-events/**' + - 'aws-lambda-java-serialization/**' + - 'aws-lambda-java-tests/**' + - '.github/workflows/samples.yml' + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Set up JDK 1.8 + uses: actions/setup-java@v5 + with: + java-version: 8 + distribution: corretto + cache: maven + + # Install dependencies + - name: Install events with Maven + run: mvn -B install --file aws-lambda-java-events/pom.xml + - name: Install serialization with Maven + run: mvn -B install --file aws-lambda-java-serialization/pom.xml + - name: Install tests with Maven + run: mvn -B install --file aws-lambda-java-tests/pom.xml + + # Install samples + - name: Install Kinesis Firehose Sample with Maven + run: mvn -B install --file samples/kinesis-firehose-event-handler/pom.xml + + custom-serialization: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + # Set up both Java 8 and 21 + - name: Set up Java 8 and 21 + uses: actions/setup-java@v5 + with: + java-version: | + 8 + 21 + distribution: corretto + cache: maven + + # Install events module using Java 8 + - name: Install events with Maven + run: | + export JAVA_HOME=$JAVA_HOME_8_X64 + mvn -B clean install \ + -Dmaven.compiler.source=1.8 \ + -Dmaven.compiler.target=1.8 \ + --file aws-lambda-java-events/pom.xml + + # Build custom-serialization samples + - name: install sam + uses: aws-actions/setup-sam@d78e1a4a9656d3b223e59b80676a797f20093133 # v2 + - name: test fastJson + run: cd samples/custom-serialization/fastJson && sam build && sam local invoke -e events/event.json | grep 200 + - name: test gson + run: cd samples/custom-serialization/gson && sam build && sam local invoke -e events/event.json | grep 200 + - name: test jackson-jr + run: cd samples/custom-serialization/jackson-jr && sam build && sam local invoke -e events/event.json | grep 200 + - name: test moshi + run: cd samples/custom-serialization/moshi && sam build && sam local invoke -e events/event.json | grep 200 + - name: test request-stream-handler + run: cd samples/custom-serialization/request-stream-handler && sam build && sam local invoke -e events/event.json | grep 200 diff --git a/.gitignore b/.gitignore index e7133935b..5a277e5d6 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,29 @@ hs_err_pid* # Maven build target/ +dependency-reduced-pom.xml # IDEA internal *.iml .idea +.gradle +.settings +.classpath +.project + +# OSX +.DS_Store + +# snapshot process +aws-lambda-java-runtime-interface-client/pom.xml.versionsBackup + +# profiler +experimental/aws-lambda-java-profiler/integration_tests/helloworld/build +experimental/aws-lambda-java-profiler/extension/build/ +experimental/aws-lambda-java-profiler/integration_tests/helloworld/bin +!experimental/aws-lambda-java-profiler/extension/gradle/wrapper/*.jar +/scratch/ +.vscode +.kiro +build +mise.toml diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a1241783c..7e8164689 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -36,6 +36,25 @@ To send us a pull request, please: 5. Send us a pull request, answering any default questions in the pull request interface. 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. + +## Build Prerequisites + +This project uses the Maven Toolchains Plugin to pin compilation to JDK 8. If you don't have a `~/.m2/toolchains.xml` configured, builds will fail with: + +``` +No toolchain found for type jdk [ version='[1.8,9)' ] +``` + +To fix this, copy the example file to your Maven config directory and update the path: + +```bash +cp toolchains.xml.example ~/.m2/toolchains.xml +``` + +Then edit `~/.m2/toolchains.xml` and set `` to your local JDK 8 installation path. + +Note: if you use `actions/setup-java` in CI (as our GitHub Actions workflows do), this file is generated automatically. + GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). diff --git a/README.md b/README.md index af925de69..580e14e41 100644 --- a/README.md +++ b/README.md @@ -1,97 +1,190 @@ # AWS Lambda Java Support Libraries -Interface definitions for Java code running on the AWS Lambda platform. +Key libraries for running Java on the AWS Lambda platform. For issues and questions, you can start with our [FAQ](https://aws.amazon.com/lambda/faqs/) - and the [AWS forums](https://forums.aws.amazon.com/forum.jspa?forumID=186) +and the AWS questions and answer site [re:Post](https://repost.aws/tags/TA5uNafDy2TpGNjidWLMSxDw/aws-lambda) -To get started writing AWS Lambda functions in Java, check out the [official documentation](http://docs.aws.amazon.com/lambda/latest/dg/java-gs.html). +To get started writing Lambda functions in Java, check out the official [developer guide](https://docs.aws.amazon.com/lambda/latest/dg/lambda-java.html). -# Disclaimer of use +For information on how to optimize your functions watch the re:Invent talk [Optimize your Java application on AWS Lambda](https://www.youtube.com/watch?v=sVJOJUD0fhQ). -Each of the supplied packages should be used without modification. Removing -dependencies, adding conflicting dependencies, or selectively including classes -from the packages can result in unexpected behavior. +## Core Java Lambda interfaces - aws-lambda-java-core + +[![Maven](https://img.shields.io/maven-central/v/com.amazonaws/aws-lambda-java-core.svg?label=Maven)](https://central.sonatype.com/artifact/com.amazonaws/aws-lambda-java-core) + +This package defines the Lambda [Context](http://docs.aws.amazon.com/lambda/latest/dg/java-context-object.html) object +as well as [interfaces](http://docs.aws.amazon.com/lambda/latest/dg/java-handler-using-predefined-interfaces.html) that Lambda accepts. + +- [Release Notes](aws-lambda-java-core/RELEASE.CHANGELOG.md) + +Example request handler + +```java +public class Handler implements RequestHandler, String>{ + @Override + public String handleRequest(Map event, Context context) { -# Release Notes + } +} +``` -Check out the per-module release notes: -- [aws-lambda-java-core](aws-lambda-java-core/RELEASE.CHANGELOG.md) -- [aws-lambda-java-events](aws-lambda-java-events/RELEASE.CHANGELOG.md) -- [aws-lambda-java-events-sdk-transformer](aws-lambda-java-events-sdk-transformer/RELEASE.CHANGELOG.md) -- [aws-lambda-java-log4j2](aws-lambda-java-log4j2/RELEASE.CHANGELOG.md) +Example request stream handler -# Where to get packages -___ +```java +public class HandlerStream implements RequestStreamHandler { + @Override + public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) throws IOException { -[Maven](https://maven.apache.org) + } +} +``` ```xml - com.amazonaws - aws-lambda-java-core - 1.2.1 + com.amazonaws + aws-lambda-java-core + 1.3.0 +``` + +## Java objects of Lambda event sources - aws-lambda-java-events + +[![Maven](https://img.shields.io/maven-central/v/com.amazonaws/aws-lambda-java-events.svg?label=Maven)](https://central.sonatype.com/artifact/com.amazonaws/aws-lambda-java-events) + +This package defines [event sources](http://docs.aws.amazon.com/lambda/latest/dg/intro-invocation-modes.html) that Lambda natively accepts. +See the [documentation](aws-lambda-java-events/README.md) for a list of currently supported event sources. +Using this library you can have Java objects which represent event sources. + +For example an SQS event: + +```java +import com.amazonaws.services.lambda.runtime.events.SQSEvent; + +public class SqsHandler implements RequestHandler { + + @Override + public String handleRequest(SQSEvent event, Context context) { + + } +} +``` + +- [Release Notes](aws-lambda-java-events/RELEASE.CHANGELOG.md) + +```xml - com.amazonaws - aws-lambda-java-events - 3.1.0 + com.amazonaws + aws-lambda-java-events + 3.16.0 +``` + +## Java Lambda JUnit Support - aws-lambda-java-tests + +[![Maven](https://img.shields.io/maven-central/v/com.amazonaws/aws-lambda-java-tests.svg?label=Maven)](https://central.sonatype.com/artifact/com.amazonaws/aws-lambda-java-tests) + +This package provides utils to ease Lambda Java testing. It uses the same Lambda serialisation logic and `aws-lambda-java-events` to inject events in your JUnit tests. + +- [Release Notes](aws-lambda-java-tests/RELEASE.CHANGELOG.md) + +```java +@ParameterizedTest +@Event(value = "sqs/sqs_event.json", type = SQSEvent.class) +public void testInjectSQSEvent(SQSEvent event) { + ... +} +``` + +```xml com.amazonaws - aws-lambda-java-events-sdk-transformer - 2.0.0 + aws-lambda-java-tests + 1.1.1 + test +``` + +## aws-lambda-java-events-sdk-transformer + +[![Maven](https://img.shields.io/maven-central/v/com.amazonaws/aws-lambda-java-events-sdk-transformer.svg?label=Maven)](https://central.sonatype.com/artifact/com.amazonaws/aws-lambda-java-events-sdk-transformer) + +This package provides helper classes/methods to use alongside `aws-lambda-java-events` in order to transform +Lambda input event model objects into SDK-compatible output model objects. +See the [documentation](aws-lambda-java-events-sdk-transformer/README.md) for more information. + +- [Release Notes](aws-lambda-java-events-sdk-transformer/RELEASE.CHANGELOG.md) + +```xml - com.amazonaws - aws-lambda-java-log4j2 - 1.2.0 + com.amazonaws + aws-lambda-java-events-sdk-transformer + 3.1.0 ``` -[Gradle](https://gradle.org) +## Java Lambda Log4J2 support - aws-lambda-java-log4j2 -```groovy -'com.amazonaws:aws-lambda-java-core:1.2.1' -'com.amazonaws:aws-lambda-java-events:3.1.0' -'com.amazonaws:aws-lambda-java-events-sdk-transformer:2.0.0' -'com.amazonaws:aws-lambda-java-log4j2:1.2.0' -``` +[![Maven](https://img.shields.io/maven-central/v/com.amazonaws/aws-lambda-java-log4j2.svg?label=Maven)](https://central.sonatype.com/artifact/com.amazonaws/aws-lambda-java-log4j2) + +This package defines the Lambda adapter to use with Log4J version 2. +See the [README](aws-lambda-java-log4j2/README.md) or the [official documentation](http://docs.aws.amazon.com/lambda/latest/dg/java-logging.html#java-wt-logging-using-log4j) for information on how to use the adapter. -[Leiningen](http://leiningen.org) and [Boot](http://boot-clj.com) +- [Release Notes](aws-lambda-java-log4j2/RELEASE.CHANGELOG.md) -```clojure -[com.amazonaws/aws-lambda-java-core "1.2.1"] -[com.amazonaws/aws-lambda-java-events "3.1.0"] -[com.amazonaws/aws-lambda-java-events-sdk-transformer "2.0.0"] -[com.amazonaws/aws-lambda-java-log4j2 "1.2.0"] +```xml + + com.amazonaws + aws-lambda-java-log4j2 + 1.6.0 + ``` -[sbt](http://www.scala-sbt.org) +## Lambda Profiler Extension for Java - aws-lambda-java-profiler -```scala -"com.amazonaws" % "aws-lambda-java-core" % "1.2.1" -"com.amazonaws" % "aws-lambda-java-events" % "3.1.0" -"com.amazonaws" % "aws-lambda-java-events-sdk-transformer" % "2.0.0" -"com.amazonaws" % "aws-lambda-java-log4j2" % "1.2.0" -``` +

+ A flame graph of a Java Lambda function +

-# Using aws-lambda-java-core +This project allows you to profile your Java functions invoke by invoke, with high fidelity, and no code changes. It +uses the [async-profiler](https://github.com/async-profiler/async-profiler) project to produce profiling data and +automatically uploads the data as flame graphs to S3. -This package defines the Lambda [Context](http://docs.aws.amazon.com/lambda/latest/dg/java-context-object.html) object - as well as [interfaces](http://docs.aws.amazon.com/lambda/latest/dg/java-handler-using-predefined-interfaces.html) that Lambda accepts. +Follow our [Quick Start](experimental/aws-lambda-java-profiler#quick-start) to profile your functions. -# Using aws-lambda-java-events +## Java implementation of the Runtime Interface Client API - aws-lambda-java-runtime-interface-client +[![Maven](https://img.shields.io/maven-central/v/com.amazonaws/aws-lambda-java-runtime-interface-client.svg?label=Maven)](https://central.sonatype.com/artifact/com.amazonaws/aws-lambda-java-runtime-interface-client) -This package defines [event sources](http://docs.aws.amazon.com/lambda/latest/dg/intro-invocation-modes.html) that AWS Lambda natively accepts. -See the [documentation](aws-lambda-java-events/README.md) for more information. +This package defines the Lambda Java Runtime Interface Client package, a Lambda Runtime component that starts the runtime and interacts with the Runtime API - i.e., it calls the API for invocation events, starts the function code, calls the API to return the response. +The purpose of this package is to allow developers to deploy their applications in Lambda under the form of Container Images. See the [README](aws-lambda-java-runtime-interface-client/README.md) for information on how to use the library. -# Using aws-lambda-java-events-sdk-transformer +- [Release Notes](aws-lambda-java-runtime-interface-client/RELEASE.CHANGELOG.md) -This package provides helper classes/methods to use alongside `aws-lambda-java-events` in order to transform - Lambda input event model objects into SDK-compatible output model objects. -See the [documentation](aws-lambda-java-events-sdk-transformer/README.md) for more information. +```xml + + com.amazonaws + aws-lambda-java-runtime-interface-client + 2.10.1 + +``` -# Using aws-lambda-java-log4j2 +## Java Lambda provided serialization support - aws-lambda-java-serialization -This package defines the Lambda adapter to use with log4j version 2. -See the [README](aws-lambda-java-log4j2/README.md) or the [official documentation](http://docs.aws.amazon.com/lambda/latest/dg/java-logging.html#java-wt-logging-using-log4j) for information on how to use the adapter. +[![Maven](https://img.shields.io/maven-central/v/com.amazonaws/aws-lambda-java-serialization.svg?label=Maven)](https://central.sonatype.com/artifact/com.amazonaws/aws-lambda-java-serialization) + +This package defines the Lambda serialization logic using in the `aws-lambda-java-runtime-client` library. It has no current standalone usage. + +- [Release Notes](aws-lambda-java-serialization/RELEASE.CHANGELOG.md) + +```xml + + com.amazonaws + aws-lambda-java-serialization + 1.1.5 + +``` + +## Disclaimer of use + +Each of the supplied packages should be used without modification. Removing +dependencies, adding conflicting dependencies, or selectively including classes +from the packages can result in unexpected behavior. diff --git a/aws-lambda-java-core/RELEASE.CHANGELOG.md b/aws-lambda-java-core/RELEASE.CHANGELOG.md index 0309da578..aebc8ecd9 100644 --- a/aws-lambda-java-core/RELEASE.CHANGELOG.md +++ b/aws-lambda-java-core/RELEASE.CHANGELOG.md @@ -1,18 +1,35 @@ -### Apr 28, 2020 +### September 3, 2025 +`1.4.0` +- Getter support for x-ray trace ID through the Context object + +### May 26, 2025 +`1.3.0` +- Adding support for multi tenancy ([#545](https://github.com/aws/aws-lambda-java-libs/pull/545)) + +### August 17, 2023 +`1.2.3`: +- Extended logger interface with level-aware logging backend functions + +### November 09, 2022 +`1.2.2`: +- Added new `CustomPojoSerializer` interface +- Removed unnecessary usage of public on interface methods (aws#172) + +### April 28, 2020 `1.2.1`: - Added missing XML namespace declarations to `pom.xml` file ([#97](https://github.com/aws/aws-lambda-java-libs/issues/97)) - Updated `nexusUrl` in `pom.xml` file ([#108](https://github.com/aws/aws-lambda-java-libs/issues/108)) -### Nov 21, 2017 +### November 21, 2017 `1.2.0`: - Added method to log byte array to `LambdaLogger` -### Oct 07, 2015 +### October 07, 2015 `1.1.0`: - Added `LambdaRuntime` and `LambdaRuntimeInternal` - Added `getInstallationId()` to `Client` - Added `getFunctionVersion()` and `getInvokedFunctionArn()` to `Context` -### Jun 15, 2015 +### June 15, 2015 `1.0.0`: - Initial support for java in AWS Lambda diff --git a/aws-lambda-java-core/pom.xml b/aws-lambda-java-core/pom.xml index 52d2976f7..f45b32fb6 100644 --- a/aws-lambda-java-core/pom.xml +++ b/aws-lambda-java-core/pom.xml @@ -5,7 +5,7 @@ com.amazonaws aws-lambda-java-core - 1.2.1 + 1.4.0-SNAPSHOT jar AWS Lambda Java Core Library @@ -22,6 +22,9 @@ https://github.com/aws/aws-lambda-java-libs.git + scm:git:https://github.com/aws/aws-lambda-java-libs.git + scm:git:https://github.com/aws/aws-lambda-java-libs.git + HEAD @@ -36,12 +39,42 @@ 1.8 - - - sonatype-nexus-staging - https://oss.sonatype.org/service/local/staging/deploy/maven2/ - - + + + + org.apache.maven.plugins + maven-release-plugin + 3.1.1 + + aws-lambda-java-core-@{project.version} + true + release + deploy + + + + org.apache.maven.plugins + maven-toolchains-plugin + 3.2.0 + + + + + [1.8,9) + + + + + + + toolchain + + + + + + @@ -115,14 +148,13 @@ - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.3 + org.sonatype.central + central-publishing-maven-plugin + 0.8.0 true - sonatype-nexus-staging - https://aws.oss.sonatype.org/ - false + central + false diff --git a/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/Client.java b/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/Client.java index 6acdd38cc..be8856871 100644 --- a/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/Client.java +++ b/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/Client.java @@ -11,28 +11,28 @@ public interface Client { /** * Gets the application's installation id */ - public String getInstallationId(); + String getInstallationId(); /** * Gets the application's title * */ - public String getAppTitle(); + String getAppTitle(); /** * Gets the application's version * */ - public String getAppVersionName(); + String getAppVersionName(); /** * Gets the application's version code * */ - public String getAppVersionCode(); + String getAppVersionCode(); /** * Gets the application's package name */ - public String getAppPackageName(); + String getAppPackageName(); } diff --git a/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/ClientContext.java b/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/ClientContext.java index 2815396d0..71f442e49 100644 --- a/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/ClientContext.java +++ b/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/ClientContext.java @@ -14,7 +14,7 @@ public interface ClientContext { * Gets the client information provided by the AWS Mobile SDK * */ - public Client getClient(); + Client getClient(); /** * Gets custom values set by the client application @@ -22,12 +22,12 @@ public interface ClientContext { * This map is mutable (and not thread-safe if mutated) *

*/ - public Map getCustom(); + Map getCustom(); /** * Gets environment information provided by mobile SDK, immutable. * */ - public Map getEnvironment(); + Map getEnvironment(); } diff --git a/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/CognitoIdentity.java b/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/CognitoIdentity.java index 25f3b3dd3..a65887632 100644 --- a/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/CognitoIdentity.java +++ b/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/CognitoIdentity.java @@ -11,11 +11,11 @@ public interface CognitoIdentity { * Gets the Amazon Cognito identity ID * */ - public String getIdentityId(); + String getIdentityId(); /** * Gets the Amazon Cognito identity pool ID * */ - public String getIdentityPoolId(); + String getIdentityPoolId(); } diff --git a/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/Context.java b/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/Context.java index a0850e78c..ed9311a11 100644 --- a/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/Context.java +++ b/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/Context.java @@ -100,4 +100,23 @@ public interface Context { */ LambdaLogger getLogger(); + /** + * + * Returns the tenant ID associated with the request. + * + * @return null by default + */ + default String getTenantId() { + return null; + } + + /** + * + * Returns the X-Ray trace ID associated with the request. + * + * @return null by default + */ + default String getXrayTraceId() { + return null; + } } diff --git a/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/CustomPojoSerializer.java b/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/CustomPojoSerializer.java new file mode 100644 index 000000000..0d7cc27d4 --- /dev/null +++ b/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/CustomPojoSerializer.java @@ -0,0 +1,38 @@ +/* Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ + +package com.amazonaws.services.lambda.runtime; + +import java.io.InputStream; +import java.io.OutputStream; + +import java.lang.reflect.Type; + +/** + * Interface required to implement a custom plain old java objects serializer + */ +public interface CustomPojoSerializer { + + /** + * Deserializes from input stream to plain old java object + * @param input input stream + * @param type plain old java object type + * @return deserialized plain old java object of type T + */ + T fromJson(InputStream input, Type type); + + /** + * Deserializes from String to plain old java object + * @param input input string + * @param type plain old java object type + * @return deserialized plain old java object of type T + */ + T fromJson(String input, Type type); + + /** + * Serializes plain old java object to output stream + * @param value instance of type T to be serialized + * @param output OutputStream to serialize plain old java object to + * @param type plain old java object type + */ + void toJson(T value, OutputStream output, Type type); +} diff --git a/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/LambdaLogger.java b/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/LambdaLogger.java index 8ff064589..e068abe8a 100644 --- a/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/LambdaLogger.java +++ b/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/LambdaLogger.java @@ -2,6 +2,8 @@ package com.amazonaws.services.lambda.runtime; +import com.amazonaws.services.lambda.runtime.logging.LogLevel; + /** * A low level Lambda runtime logger * @@ -10,7 +12,7 @@ public interface LambdaLogger { /** * Logs a string to AWS CloudWatch Logs - * + * *

* Logging will not be done: *

    @@ -22,15 +24,37 @@ public interface LambdaLogger { * *
*

- * + * * @param message A string containing the event to log. */ - public void log(String message); + void log(String message); /** * Logs a byte array to AWS CloudWatch Logs * @param message byte array containing logs */ - public void log(byte[] message); + void log(byte[] message); + + /** + * LogLevel aware logging backend function. + * + * @param message in String format + * @param logLevel + */ + default void log(String message, LogLevel logLevel) { + log(message); + } + + /** + * LogLevel aware logging backend function. + * + * @param message in byte[] format + * @param logLevel + */ + default void log(byte[] message, LogLevel logLevel) { + log(message); + } + + } diff --git a/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/RequestHandler.java b/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/RequestHandler.java index 406f2be3b..834683f26 100644 --- a/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/RequestHandler.java +++ b/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/RequestHandler.java @@ -17,5 +17,5 @@ public interface RequestHandler { * @param context The Lambda execution environment context object. * @return The Lambda Function output */ - public O handleRequest(I input, Context context); + O handleRequest(I input, Context context); } diff --git a/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/RequestStreamHandler.java b/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/RequestStreamHandler.java index d8ccf5a6b..3a34adc9b 100644 --- a/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/RequestStreamHandler.java +++ b/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/RequestStreamHandler.java @@ -18,5 +18,5 @@ public interface RequestStreamHandler { * @param context The Lambda execution environment context object. * @throws IOException */ - public void handleRequest(InputStream input, OutputStream output, Context context) throws IOException; + void handleRequest(InputStream input, OutputStream output, Context context) throws IOException; } diff --git a/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/logging/LogFormat.java b/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/logging/LogFormat.java new file mode 100644 index 000000000..0d65860d7 --- /dev/null +++ b/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/logging/LogFormat.java @@ -0,0 +1,17 @@ +/* Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ + +package com.amazonaws.services.lambda.runtime.logging; + + +public enum LogFormat { + JSON, + TEXT; + + public static LogFormat fromString(String logFormat) { + try { + return LogFormat.valueOf(logFormat.toUpperCase()); + } catch (Exception e) { + throw new IllegalArgumentException("Invalid log format: '" + logFormat + "' expected one of [JSON, TEXT]"); + } + } +} diff --git a/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/logging/LogLevel.java b/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/logging/LogLevel.java new file mode 100644 index 000000000..4f48a3e41 --- /dev/null +++ b/aws-lambda-java-core/src/main/java/com/amazonaws/services/lambda/runtime/logging/LogLevel.java @@ -0,0 +1,25 @@ +/* Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ + +package com.amazonaws.services.lambda.runtime.logging; + + +public enum LogLevel { + // UNDEFINED log level is used when the legacy LambdaLogger::log(String) function is called + // where the loglevel is not defined. In this case we're not filtering the message in the runtime + UNDEFINED, + TRACE, + DEBUG, + INFO, + WARN, + ERROR, + FATAL; + + public static LogLevel fromString(String logLevel) { + try { + return LogLevel.valueOf(logLevel.toUpperCase()); + } catch (Exception e) { + throw new IllegalArgumentException( + "Invalid log level: '" + logLevel + "' expected one of [TRACE, DEBUG, INFO, WARN, ERROR, FATAL]"); + } + } +} diff --git a/aws-lambda-java-events-sdk-transformer/README.md b/aws-lambda-java-events-sdk-transformer/README.md index f2fc32d2f..02f2dc11f 100644 --- a/aws-lambda-java-events-sdk-transformer/README.md +++ b/aws-lambda-java-events-sdk-transformer/README.md @@ -4,7 +4,7 @@ Provides helper classes/methods to use alongside `aws-lambda-java-events` in order to transform Lambda input event model objects into SDK-compatible output model objects - (eg. DynamodbEvent to a List of records writable back to DynamoDB through the AWS DynamoDB SDK v2). + (eg. DynamodbEvent to a List of records writable back to DynamoDB through the AWS DynamoDB SDK for Java v1 or v2). ### Getting started @@ -16,12 +16,12 @@ Add the following Apache Maven dependencies to your `pom.xml` file: com.amazonaws aws-lambda-java-events-sdk-transformer - 2.0.1 + 3.1.0 com.amazonaws aws-lambda-java-events - 3.1.0 + 3.11.2 ``` @@ -33,7 +33,19 @@ To use this library as a transformer to the AWS DynamoDB Java SDK v2, also add t software.amazon.awssdk dynamodb - 2.13.18 + 2.15.40 + + +``` + +To use this library as a transformer to the AWS DynamoDB Java SDK v1, add the following dependency to your `pom.xml` file instead: + +```xml + + + com.amazonaws + aws-java-sdk-dynamodb + 1.11.914 ``` @@ -41,75 +53,162 @@ To use this library as a transformer to the AWS DynamoDB Java SDK v2, also add t ### Example Usage +#### SDK v2 + To convert a full `DynamodbEvent` object to an SDK v2 compatible `List`: + ```java -import com.amazonaws.services.lambda.runtime.events.transformers.DynamodbEventTransformer; +import com.amazonaws.services.lambda.runtime.events.transformers.v2.DynamodbEventTransformer; public class DDBEventProcessor implements RequestHandler { - public String handleRequest(DynamodbEvent ddbEvent, Context context) { - // Process input event - List convertedRecords = DynamodbEventTransformer.toRecordsV2(ddbEvent); - // Modify records as needed and write back to DynamoDB using the DynamoDB AWS SDK for Java 2.0 - } + public String handleRequest(DynamodbEvent ddbEvent, Context context) { + // Process input event + List convertedRecords = DynamodbEventTransformer.toRecordsV2(ddbEvent); + // Modify records as needed and write back to DynamoDB using the DynamoDB AWS SDK for Java 2.0 + } } ``` To convert a single `DynamodbEvent.DynamodbStreamRecord` object to an SDK v2 compatible `Record`: + ```java -import com.amazonaws.services.lambda.runtime.events.transformers.dynamodb.DynamodbRecordTransformer; +import com.amazonaws.services.lambda.runtime.events.transformers.v2.dynamodb.DynamodbRecordTransformer; public class MyClass { - public void myMethod(DynamodbEvent.DynamodbStreamRecord record) { - // ... - Record convertedRecord = DynamodbRecordTransformer.toRecordV2(record); - // ... - } + public void myMethod(DynamodbEvent.DynamodbStreamRecord record) { + // ... + Record convertedRecord = DynamodbRecordTransformer.toRecordV2(record); + // ... + } } ``` To convert a `StreamRecord` object originating from a `DynamodbEvent` to an SDK v2 compatible `StreamRecord`: + ```java -import com.amazonaws.services.lambda.runtime.events.transformers.dynamodb.DynamodbStreamRecordTransformer; +import com.amazonaws.services.lambda.runtime.events.transformers.v2.dynamodb.DynamodbStreamRecordTransformer; public class MyClass { - public void myMethod(StreamRecord streamRecord) { - // ... - software.amazon.awssdk.services.dynamodb.model.StreamRecord convertedStreamRecord = - DynamodbStreamRecordTransformer.toStreamRecordV2(streamRecord); - // ... - } + public void myMethod(StreamRecord streamRecord) { + // ... + software.amazon.awssdk.services.dynamodb.model.StreamRecord convertedStreamRecord = + DynamodbStreamRecordTransformer.toStreamRecordV2(streamRecord); + // ... + } } ``` To convert an `AttributeValue` object originating from a `DynamodbEvent` to an SDK v2 compatible `AttributeValue`: + ```java -import com.amazonaws.services.lambda.runtime.events.transformers.dynamodb.DynamodbAttributeValueTransformer; +import com.amazonaws.services.lambda.runtime.events.transformers.v2.dynamodb.DynamodbAttributeValueTransformer; public class MyClass { - public void myMethod(AttributeValue attributeValue) { - // ... - software.amazon.awssdk.services.dynamodb.model.AttributeValue convertedAttributeValue = - DynamodbAttributeValueTransformer.toAttributeValueV2(attributeValue); - // ... - } + public void myMethod(AttributeValue attributeValue) { + // ... + software.amazon.awssdk.services.dynamodb.model.AttributeValue convertedAttributeValue = + DynamodbAttributeValueTransformer.toAttributeValueV2(attributeValue); + // ... + } } ``` To convert an `Identity` object originating from a `DynamodbEvent` to an SDK v2 compatible `Identity`: + +```java +import com.amazonaws.services.lambda.runtime.events.transformers.v2.dynamodb.DynamodbIdentityTransformer; + +public class MyClass { + + public void myMethod(Identity identity) { + // ... + software.amazon.awssdk.services.dynamodb.model.Identity convertedIdentity = + DynamodbIdentityTransformer.toIdentityV2(identity); + // ... + } +} +``` + +#### SDK v1 + +To convert a full `DynamodbEvent` object to an SDK v1 compatible `List`: + +```java +import com.amazonaws.services.lambda.runtime.events.transformers.v1.DynamodbEventTransformer; + +public class DDBEventProcessor implements RequestHandler { + + public String handleRequest(DynamodbEvent ddbEvent, Context context) { + // Process input event + List convertedRecords = DynamodbEventTransformer.toRecordsV1(ddbEvent); + // Modify records as needed and write back to DynamoDB using the DynamoDB AWS SDK for Java 2.0 + } +} +``` + +To convert a single `DynamodbEvent.DynamodbStreamRecord` object to an SDK v1 compatible `Record`: + +```java +import com.amazonaws.services.lambda.runtime.events.transformers.v1.dynamodb.DynamodbRecordTransformer; + +public class MyClass { + + public void myMethod(DynamodbEvent.DynamodbStreamRecord record) { + // ... + Record convertedRecord = DynamodbRecordTransformer.toRecordV1(record); + // ... + } +} +``` + +To convert a `StreamRecord` object originating from a `DynamodbEvent` to an SDK v1 compatible `StreamRecord`: + +```java +import com.amazonaws.services.lambda.runtime.events.transformers.v1.dynamodb.DynamodbStreamRecordTransformer; + +public class MyClass { + + public void myMethod(StreamRecord streamRecord) { + // ... + com.amazonaws.services.dynamodbv2.model.StreamRecord convertedStreamRecord = + DynamodbStreamRecordTransformer.toStreamRecordV1(streamRecord); + // ... + } +} +``` + +To convert an `AttributeValue` object originating from a `DynamodbEvent` to an SDK v1 compatible `AttributeValue`: + +```java +import com.amazonaws.services.lambda.runtime.events.transformers.v1.dynamodb.DynamodbAttributeValueTransformer; + +public class MyClass { + + public void myMethod(AttributeValue attributeValue) { + // ... + com.amazonaws.services.dynamodbv2.model.AttributeValue convertedAttributeValue = + DynamodbAttributeValueTransformer.toAttributeValueV1(attributeValue); + // ... + } +} +``` + +To convert an `Identity` object originating from a `DynamodbEvent` to an SDK v1 compatible `Identity`: + ```java -import com.amazonaws.services.lambda.runtime.events.transformers.dynamodb.DynamodbIdentityTransformer; +import com.amazonaws.services.lambda.runtime.events.transformers.v1.dynamodb.DynamodbIdentityTransformer; public class MyClass { - public void myMethod(Identity identity) { - // ... - software.amazon.awssdk.services.dynamodb.model.Identity convertedIdentity = - DynamodbIdentityTransformer.toIdentityV2(identity); - // ... - } + public void myMethod(Identity identity) { + // ... + com.amazonaws.services.dynamodbv2.model.Identity convertedIdentity = + DynamodbIdentityTransformer.toIdentityV1(identity); + // ... + } } ``` \ No newline at end of file diff --git a/aws-lambda-java-events-sdk-transformer/RELEASE.CHANGELOG.md b/aws-lambda-java-events-sdk-transformer/RELEASE.CHANGELOG.md index 47eb27580..791348208 100644 --- a/aws-lambda-java-events-sdk-transformer/RELEASE.CHANGELOG.md +++ b/aws-lambda-java-events-sdk-transformer/RELEASE.CHANGELOG.md @@ -1,3 +1,71 @@ +### February 03, 2022 +`3.1.0`: +- Make DynamodbAttributeValueTransformer v1 and v2 return empty list instead of null for empty list attribute ([#309](https://github.com/aws/aws-lambda-java-libs/pull/309)) + +### November 24, 2021 +`3.0.7`: +- Bumped `aws-lambda-java-events` to version `3.11.0` + +### September 02, 2021 +`3.0.6`: +- Fixed NPE when UserIdentity, OldImage, or NewImage is null ([#264](https://github.com/aws/aws-lambda-java-libs/pull/264)) + +### August 26, 2021 +`3.0.5`: +- Bumped `aws-lambda-java-events` to version `3.10.0` + +### June 2, 2021 +`3.0.4`: +- Bumped `aws-lambda-java-events` to version `3.9.0` + +### March 24, 2021 +`3.0.3`: +- Bumped `aws-lambda-java-events` to version `3.8.0` + +### December 16, 2020 +`3.0.2`: +- Bumped `aws-lambda-java-events` to version `3.7.0` + +### December 10, 2020 +`3.0.1`: +- Change visibility scope of `Map toAttributeValueMapVx(Map)` to `public` + +### December 09, 2020 +`3.0.0`: +- Added AWS SDK V1 transformers for `DynamodbEvent` in `aws-lambda-java-events` versions `3.0.0` and up +- Moved existing SDK v2 transformers into `v2` package (from `com.amazonaws.services.lambda.runtime.events.transformers` to `com.amazonaws.services.lambda.runtime.events.transformers.v2`) +- Bumped `software.amazon.awssdk:dynamodb` to version `2.15.40` + +### November 06, 2020 +`2.0.8`: +- Bumped `aws-lambda-java-events` to version `3.6.0` +- Bumped `junit-jupiter-engine` to version `5.7.0` + +### October 28, 2020 +`2.0.7`: +- Bumped `aws-lambda-java-events` to version `3.5.0` + +### October 07, 2020 +`2.0.6`: +- Fixed NPE when UserIdentity is null ([#169](https://github.com/aws/aws-lambda-java-libs/pull/169)) +- Bumped `aws-lambda-java-events` to version `3.4.0` + +### September 23, 2020 +`2.0.5`: +- Bumped `aws-lambda-java-events` to version `3.3.1` + +### September 14, 2020 +`2.0.4`: +- Bumped `aws-lambda-java-events` to version `3.3.0` + +### August 11, 2020 +`2.0.3`: +- Bumped `aws-lambda-java-events` to version `3.2.0` + +### July 31, 2020 +`2.0.2`: +- Bumped `aws-lambda-java-events` to version `3.1.1` + ### June 15, 2020 `2.0.1`: - Fixed NPE when mapping insert/delete events ([#143](https://github.com/aws/aws-lambda-java-libs/pull/143)) @@ -7,6 +75,6 @@ - Updated AWS SDK V2 transformers for `DynamodbEvent` to work with `aws-lambda-java-events` versions `3.0.0` and up - Bumped `software.amazon.awssdk:dynamodb` to version `2.13.18` -### Apr 29, 2020 +### April 29, 2020 `1.0.0`: - Added AWS SDK V2 transformers for `DynamodbEvent` in `aws-lambda-java-events` versions up to and including `2.x` diff --git a/aws-lambda-java-events-sdk-transformer/pom.xml b/aws-lambda-java-events-sdk-transformer/pom.xml index a8c2a1779..f66020068 100644 --- a/aws-lambda-java-events-sdk-transformer/pom.xml +++ b/aws-lambda-java-events-sdk-transformer/pom.xml @@ -5,14 +5,14 @@ com.amazonaws aws-lambda-java-events-sdk-transformer - 2.0.1 + 3.1.1-SNAPSHOT jar AWS Lambda Java Events SDK Transformer Library Provides helper classes/methods to use alongside aws-lambda-java-events in order to transform Lambda input event model objects into SDK-compatible output model objects (eg. DynamodbEvent to a List of records writable back to DynamoDB - through the AWS DynamoDB SDK v2) + through the AWS DynamoDB SDK for Java v1 or v2) https://aws.amazon.com/lambda/ @@ -24,6 +24,9 @@ https://github.com/aws/aws-lambda-java-libs.git + scm:git:https://github.com/aws/aws-lambda-java-libs.git + scm:git:https://github.com/aws/aws-lambda-java-libs.git + HEAD @@ -36,6 +39,10 @@ 1.8 1.8 + 1.11.914 + 2.15.40 + 5.12.2 + 3.5.4 @@ -49,33 +56,74 @@ software.amazon.awssdk dynamodb - 2.13.18 + ${sdk.v2.version} + provided + + + com.amazonaws + aws-java-sdk-dynamodb + ${sdk.v1.version} provided com.amazonaws aws-lambda-java-events - 3.1.0 + 3.16.1 provided org.junit.jupiter junit-jupiter-engine - 5.6.2 + ${junit-jupiter.version} test + + org.apache.maven.plugins + maven-release-plugin + 3.1.1 + + aws-lambda-java-events-sdk-transformer-@{project.version} + true + release + deploy + + + + org.apache.maven.plugins + maven-toolchains-plugin + 3.2.0 + + + + + [1.8,9) + + + + + + + toolchain + + + + maven-surefire-plugin - 2.22.2 + ${maven-surefire-plugin.version} + + true + maven-failsafe-plugin - 2.22.2 + ${maven-surefire-plugin.version} @@ -152,18 +200,17 @@ - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.3 + org.sonatype.central + central-publishing-maven-plugin + 0.8.0 true - sonatype-nexus-staging - https://aws.oss.sonatype.org/ - false + central + false
- + \ No newline at end of file diff --git a/aws-lambda-java-events-sdk-transformer/src/main/java/com/amazonaws/services/lambda/runtime/events/transformers/v1/DynamodbEventTransformer.java b/aws-lambda-java-events-sdk-transformer/src/main/java/com/amazonaws/services/lambda/runtime/events/transformers/v1/DynamodbEventTransformer.java new file mode 100644 index 000000000..c9686d103 --- /dev/null +++ b/aws-lambda-java-events-sdk-transformer/src/main/java/com/amazonaws/services/lambda/runtime/events/transformers/v1/DynamodbEventTransformer.java @@ -0,0 +1,21 @@ +package com.amazonaws.services.lambda.runtime.events.transformers.v1; + +import com.amazonaws.services.dynamodbv2.model.Record; +import com.amazonaws.services.lambda.runtime.events.DynamodbEvent; +import com.amazonaws.services.lambda.runtime.events.transformers.v1.dynamodb.DynamodbRecordTransformer; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +public class DynamodbEventTransformer { + + public static List toRecordsV1(final DynamodbEvent dynamodbEvent) { + return dynamodbEvent + .getRecords() + .stream() + .filter(record -> !Objects.isNull(record)) + .map(DynamodbRecordTransformer::toRecordV1) + .collect(Collectors.toList()); + } +} diff --git a/aws-lambda-java-events-sdk-transformer/src/main/java/com/amazonaws/services/lambda/runtime/events/transformers/v1/dynamodb/DynamodbAttributeValueTransformer.java b/aws-lambda-java-events-sdk-transformer/src/main/java/com/amazonaws/services/lambda/runtime/events/transformers/v1/dynamodb/DynamodbAttributeValueTransformer.java new file mode 100644 index 000000000..61f311f5b --- /dev/null +++ b/aws-lambda-java-events-sdk-transformer/src/main/java/com/amazonaws/services/lambda/runtime/events/transformers/v1/dynamodb/DynamodbAttributeValueTransformer.java @@ -0,0 +1,74 @@ +package com.amazonaws.services.lambda.runtime.events.transformers.v1.dynamodb; + +import com.amazonaws.services.dynamodbv2.model.AttributeValue; + +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +public class DynamodbAttributeValueTransformer { + + public static AttributeValue toAttributeValueV1(final com.amazonaws.services.lambda.runtime.events.models.dynamodb.AttributeValue value) { + if (Objects.nonNull(value.getS())) { + return new AttributeValue() + .withS(value.getS()); + + } else if (Objects.nonNull(value.getSS())) { + return new AttributeValue() + .withSS(value.getSS().isEmpty() ? null : value.getSS()); + + } else if (Objects.nonNull(value.getN())) { + return new AttributeValue() + .withN(value.getN()); + + } else if (Objects.nonNull(value.getNS())) { + return new AttributeValue() + .withNS(value.getNS().isEmpty() ? null : value.getNS()); + + } else if (Objects.nonNull(value.getB())) { + return new AttributeValue() + .withB(value.getB()); + + } else if (Objects.nonNull(value.getBS())) { + return new AttributeValue() + .withBS(value.getBS().isEmpty() ? null : value.getBS()); + + } else if (Objects.nonNull(value.getBOOL())) { + return new AttributeValue() + .withBOOL(value.getBOOL()); + + } else if (Objects.nonNull(value.getL())) { + return new AttributeValue() + .withL(value.getL().isEmpty() + ? Collections.emptyList() + : value.getL().stream() + .map(DynamodbAttributeValueTransformer::toAttributeValueV1) + .collect(Collectors.toList())); + + } else if (Objects.nonNull(value.getM())) { + return new AttributeValue() + .withM(toAttributeValueMapV1(value.getM())); + + } else if (Objects.nonNull(value.getNULL())) { + return new AttributeValue() + .withNULL(value.getNULL()); + + } else { + throw new IllegalArgumentException( + String.format("Unsupported attributeValue type: %s", value)); + } + } + + public static Map toAttributeValueMapV1( + final Map attributeValueMap + ) { + return attributeValueMap + .entrySet() + .stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + entry -> toAttributeValueV1(entry.getValue()) + )); + } +} diff --git a/aws-lambda-java-events-sdk-transformer/src/main/java/com/amazonaws/services/lambda/runtime/events/transformers/v1/dynamodb/DynamodbIdentityTransformer.java b/aws-lambda-java-events-sdk-transformer/src/main/java/com/amazonaws/services/lambda/runtime/events/transformers/v1/dynamodb/DynamodbIdentityTransformer.java new file mode 100644 index 000000000..32f7aaff9 --- /dev/null +++ b/aws-lambda-java-events-sdk-transformer/src/main/java/com/amazonaws/services/lambda/runtime/events/transformers/v1/dynamodb/DynamodbIdentityTransformer.java @@ -0,0 +1,12 @@ +package com.amazonaws.services.lambda.runtime.events.transformers.v1.dynamodb; + +import com.amazonaws.services.dynamodbv2.model.Identity; + +public class DynamodbIdentityTransformer { + + public static Identity toIdentityV1(final com.amazonaws.services.lambda.runtime.events.models.dynamodb.Identity identity) { + return new Identity() + .withPrincipalId(identity.getPrincipalId()) + .withType(identity.getType()); + } +} diff --git a/aws-lambda-java-events-sdk-transformer/src/main/java/com/amazonaws/services/lambda/runtime/events/transformers/v1/dynamodb/DynamodbRecordTransformer.java b/aws-lambda-java-events-sdk-transformer/src/main/java/com/amazonaws/services/lambda/runtime/events/transformers/v1/dynamodb/DynamodbRecordTransformer.java new file mode 100644 index 000000000..7527cd91b --- /dev/null +++ b/aws-lambda-java-events-sdk-transformer/src/main/java/com/amazonaws/services/lambda/runtime/events/transformers/v1/dynamodb/DynamodbRecordTransformer.java @@ -0,0 +1,24 @@ +package com.amazonaws.services.lambda.runtime.events.transformers.v1.dynamodb; + +import com.amazonaws.services.dynamodbv2.model.Record; +import com.amazonaws.services.lambda.runtime.events.DynamodbEvent; + +public class DynamodbRecordTransformer { + + public static Record toRecordV1(final DynamodbEvent.DynamodbStreamRecord record) { + return new Record() + .withAwsRegion(record.getAwsRegion()) + .withDynamodb( + DynamodbStreamRecordTransformer.toStreamRecordV1(record.getDynamodb()) + ) + .withEventID(record.getEventID()) + .withEventName(record.getEventName()) + .withEventSource(record.getEventSource()) + .withEventVersion(record.getEventVersion()) + .withUserIdentity( + record.getUserIdentity() != null + ? DynamodbIdentityTransformer.toIdentityV1(record.getUserIdentity()) + : null + ); + } +} diff --git a/aws-lambda-java-events-sdk-transformer/src/main/java/com/amazonaws/services/lambda/runtime/events/transformers/v1/dynamodb/DynamodbStreamRecordTransformer.java b/aws-lambda-java-events-sdk-transformer/src/main/java/com/amazonaws/services/lambda/runtime/events/transformers/v1/dynamodb/DynamodbStreamRecordTransformer.java new file mode 100644 index 000000000..f7ca2c6d3 --- /dev/null +++ b/aws-lambda-java-events-sdk-transformer/src/main/java/com/amazonaws/services/lambda/runtime/events/transformers/v1/dynamodb/DynamodbStreamRecordTransformer.java @@ -0,0 +1,29 @@ +package com.amazonaws.services.lambda.runtime.events.transformers.v1.dynamodb; + +import com.amazonaws.services.dynamodbv2.model.StreamRecord; + +public class DynamodbStreamRecordTransformer { + + public static StreamRecord toStreamRecordV1(final com.amazonaws.services.lambda.runtime.events.models.dynamodb.StreamRecord streamRecord) { + return new StreamRecord() + .withApproximateCreationDateTime( + streamRecord.getApproximateCreationDateTime() + ) + .withKeys( + DynamodbAttributeValueTransformer.toAttributeValueMapV1(streamRecord.getKeys()) + ) + .withNewImage( + streamRecord.getNewImage() != null + ? DynamodbAttributeValueTransformer.toAttributeValueMapV1(streamRecord.getNewImage()) + : null + ) + .withOldImage( + streamRecord.getOldImage() != null + ? DynamodbAttributeValueTransformer.toAttributeValueMapV1(streamRecord.getOldImage()) + : null + ) + .withSequenceNumber(streamRecord.getSequenceNumber()) + .withSizeBytes(streamRecord.getSizeBytes()) + .withStreamViewType(streamRecord.getStreamViewType()); + } +} diff --git a/aws-lambda-java-events-sdk-transformer/src/main/java/com/amazonaws/services/lambda/runtime/events/transformers/DynamodbEventTransformer.java b/aws-lambda-java-events-sdk-transformer/src/main/java/com/amazonaws/services/lambda/runtime/events/transformers/v2/DynamodbEventTransformer.java similarity index 94% rename from aws-lambda-java-events-sdk-transformer/src/main/java/com/amazonaws/services/lambda/runtime/events/transformers/DynamodbEventTransformer.java rename to aws-lambda-java-events-sdk-transformer/src/main/java/com/amazonaws/services/lambda/runtime/events/transformers/v2/DynamodbEventTransformer.java index c2c81ec54..43e57564c 100644 --- a/aws-lambda-java-events-sdk-transformer/src/main/java/com/amazonaws/services/lambda/runtime/events/transformers/DynamodbEventTransformer.java +++ b/aws-lambda-java-events-sdk-transformer/src/main/java/com/amazonaws/services/lambda/runtime/events/transformers/v2/DynamodbEventTransformer.java @@ -1,7 +1,7 @@ -package com.amazonaws.services.lambda.runtime.events.transformers; +package com.amazonaws.services.lambda.runtime.events.transformers.v2; import com.amazonaws.services.lambda.runtime.events.DynamodbEvent; -import com.amazonaws.services.lambda.runtime.events.transformers.dynamodb.DynamodbRecordTransformer; +import com.amazonaws.services.lambda.runtime.events.transformers.v2.dynamodb.DynamodbRecordTransformer; import software.amazon.awssdk.services.dynamodb.model.Record; import java.util.List; diff --git a/aws-lambda-java-events-sdk-transformer/src/main/java/com/amazonaws/services/lambda/runtime/events/transformers/dynamodb/DynamodbAttributeValueTransformer.java b/aws-lambda-java-events-sdk-transformer/src/main/java/com/amazonaws/services/lambda/runtime/events/transformers/v2/dynamodb/DynamodbAttributeValueTransformer.java similarity index 75% rename from aws-lambda-java-events-sdk-transformer/src/main/java/com/amazonaws/services/lambda/runtime/events/transformers/dynamodb/DynamodbAttributeValueTransformer.java rename to aws-lambda-java-events-sdk-transformer/src/main/java/com/amazonaws/services/lambda/runtime/events/transformers/v2/dynamodb/DynamodbAttributeValueTransformer.java index be6307277..ee810c501 100644 --- a/aws-lambda-java-events-sdk-transformer/src/main/java/com/amazonaws/services/lambda/runtime/events/transformers/dynamodb/DynamodbAttributeValueTransformer.java +++ b/aws-lambda-java-events-sdk-transformer/src/main/java/com/amazonaws/services/lambda/runtime/events/transformers/v2/dynamodb/DynamodbAttributeValueTransformer.java @@ -1,8 +1,9 @@ -package com.amazonaws.services.lambda.runtime.events.transformers.dynamodb; +package com.amazonaws.services.lambda.runtime.events.transformers.v2.dynamodb; import software.amazon.awssdk.core.SdkBytes; import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import java.util.Collections; import java.util.Map; import java.util.Objects; import java.util.stream.Collectors; @@ -17,7 +18,7 @@ public static AttributeValue toAttributeValueV2(final com.amazonaws.services.lam } else if (Objects.nonNull(value.getSS())) { return AttributeValue.builder() - .ss(value.getSS()) + .ss(value.getSS().isEmpty() ? null : value.getSS()) .build(); } else if (Objects.nonNull(value.getN())) { @@ -27,7 +28,7 @@ public static AttributeValue toAttributeValueV2(final com.amazonaws.services.lam } else if (Objects.nonNull(value.getNS())) { return AttributeValue.builder() - .ns(value.getNS()) + .ns(value.getNS().isEmpty() ? null : value.getNS()) .build(); } else if (Objects.nonNull(value.getB())) { @@ -37,9 +38,11 @@ public static AttributeValue toAttributeValueV2(final com.amazonaws.services.lam } else if (Objects.nonNull(value.getBS())) { return AttributeValue.builder() - .bs(value.getBS().stream() - .map(SdkBytes::fromByteBuffer) - .collect(Collectors.toList())) + .bs(value.getBS().isEmpty() + ? null + : value.getBS().stream() + .map(SdkBytes::fromByteBuffer) + .collect(Collectors.toList())) .build(); } else if (Objects.nonNull(value.getBOOL())) { @@ -49,9 +52,11 @@ public static AttributeValue toAttributeValueV2(final com.amazonaws.services.lam } else if (Objects.nonNull(value.getL())) { return AttributeValue.builder() - .l(value.getL().stream() - .map(DynamodbAttributeValueTransformer::toAttributeValueV2) - .collect(Collectors.toList())) + .l(value.getL().isEmpty() + ? Collections.emptyList() + : value.getL().stream() + .map(DynamodbAttributeValueTransformer::toAttributeValueV2) + .collect(Collectors.toList())) .build(); } else if (Objects.nonNull(value.getM())) { @@ -70,7 +75,7 @@ public static AttributeValue toAttributeValueV2(final com.amazonaws.services.lam } } - static Map toAttributeValueMapV2( + public static Map toAttributeValueMapV2( final Map attributeValueMap ) { return attributeValueMap diff --git a/aws-lambda-java-events-sdk-transformer/src/main/java/com/amazonaws/services/lambda/runtime/events/transformers/dynamodb/DynamodbIdentityTransformer.java b/aws-lambda-java-events-sdk-transformer/src/main/java/com/amazonaws/services/lambda/runtime/events/transformers/v2/dynamodb/DynamodbIdentityTransformer.java similarity index 96% rename from aws-lambda-java-events-sdk-transformer/src/main/java/com/amazonaws/services/lambda/runtime/events/transformers/dynamodb/DynamodbIdentityTransformer.java rename to aws-lambda-java-events-sdk-transformer/src/main/java/com/amazonaws/services/lambda/runtime/events/transformers/v2/dynamodb/DynamodbIdentityTransformer.java index 1699286fc..34c5fe69f 100644 --- a/aws-lambda-java-events-sdk-transformer/src/main/java/com/amazonaws/services/lambda/runtime/events/transformers/dynamodb/DynamodbIdentityTransformer.java +++ b/aws-lambda-java-events-sdk-transformer/src/main/java/com/amazonaws/services/lambda/runtime/events/transformers/v2/dynamodb/DynamodbIdentityTransformer.java @@ -1,4 +1,4 @@ -package com.amazonaws.services.lambda.runtime.events.transformers.dynamodb; +package com.amazonaws.services.lambda.runtime.events.transformers.v2.dynamodb; import software.amazon.awssdk.services.dynamodb.model.Identity; diff --git a/aws-lambda-java-events-sdk-transformer/src/main/java/com/amazonaws/services/lambda/runtime/events/transformers/dynamodb/DynamodbRecordTransformer.java b/aws-lambda-java-events-sdk-transformer/src/main/java/com/amazonaws/services/lambda/runtime/events/transformers/v2/dynamodb/DynamodbRecordTransformer.java similarity index 79% rename from aws-lambda-java-events-sdk-transformer/src/main/java/com/amazonaws/services/lambda/runtime/events/transformers/dynamodb/DynamodbRecordTransformer.java rename to aws-lambda-java-events-sdk-transformer/src/main/java/com/amazonaws/services/lambda/runtime/events/transformers/v2/dynamodb/DynamodbRecordTransformer.java index d9547a76e..0d035d192 100644 --- a/aws-lambda-java-events-sdk-transformer/src/main/java/com/amazonaws/services/lambda/runtime/events/transformers/dynamodb/DynamodbRecordTransformer.java +++ b/aws-lambda-java-events-sdk-transformer/src/main/java/com/amazonaws/services/lambda/runtime/events/transformers/v2/dynamodb/DynamodbRecordTransformer.java @@ -1,4 +1,4 @@ -package com.amazonaws.services.lambda.runtime.events.transformers.dynamodb; +package com.amazonaws.services.lambda.runtime.events.transformers.v2.dynamodb; import com.amazonaws.services.lambda.runtime.events.DynamodbEvent; import software.amazon.awssdk.services.dynamodb.model.Record; @@ -16,7 +16,9 @@ public static Record toRecordV2(final DynamodbEvent.DynamodbStreamRecord record) .eventSource(record.getEventSource()) .eventVersion(record.getEventVersion()) .userIdentity( - DynamodbIdentityTransformer.toIdentityV2(record.getUserIdentity()) + record.getUserIdentity() != null + ? DynamodbIdentityTransformer.toIdentityV2(record.getUserIdentity()) + : null ) .build(); } diff --git a/aws-lambda-java-events-sdk-transformer/src/main/java/com/amazonaws/services/lambda/runtime/events/transformers/dynamodb/DynamodbStreamRecordTransformer.java b/aws-lambda-java-events-sdk-transformer/src/main/java/com/amazonaws/services/lambda/runtime/events/transformers/v2/dynamodb/DynamodbStreamRecordTransformer.java similarity index 98% rename from aws-lambda-java-events-sdk-transformer/src/main/java/com/amazonaws/services/lambda/runtime/events/transformers/dynamodb/DynamodbStreamRecordTransformer.java rename to aws-lambda-java-events-sdk-transformer/src/main/java/com/amazonaws/services/lambda/runtime/events/transformers/v2/dynamodb/DynamodbStreamRecordTransformer.java index 659854873..6cb1102dd 100644 --- a/aws-lambda-java-events-sdk-transformer/src/main/java/com/amazonaws/services/lambda/runtime/events/transformers/dynamodb/DynamodbStreamRecordTransformer.java +++ b/aws-lambda-java-events-sdk-transformer/src/main/java/com/amazonaws/services/lambda/runtime/events/transformers/v2/dynamodb/DynamodbStreamRecordTransformer.java @@ -1,4 +1,4 @@ -package com.amazonaws.services.lambda.runtime.events.transformers.dynamodb; +package com.amazonaws.services.lambda.runtime.events.transformers.v2.dynamodb; import software.amazon.awssdk.services.dynamodb.model.StreamRecord; diff --git a/aws-lambda-java-events-sdk-transformer/src/test/java/com/amazonaws/services/lambda/runtime/events/transformers/v1/DynamodbEventTransformerTest.java b/aws-lambda-java-events-sdk-transformer/src/test/java/com/amazonaws/services/lambda/runtime/events/transformers/v1/DynamodbEventTransformerTest.java new file mode 100644 index 000000000..234b97aea --- /dev/null +++ b/aws-lambda-java-events-sdk-transformer/src/test/java/com/amazonaws/services/lambda/runtime/events/transformers/v1/DynamodbEventTransformerTest.java @@ -0,0 +1,43 @@ +package com.amazonaws.services.lambda.runtime.events.transformers.v1; + +import com.amazonaws.services.dynamodbv2.model.Record; +import com.amazonaws.services.lambda.runtime.events.DynamodbEvent; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static com.amazonaws.services.lambda.runtime.events.transformers.v1.dynamodb.DynamodbRecordTransformerTest.record_event; +import static com.amazonaws.services.lambda.runtime.events.transformers.v1.dynamodb.DynamodbRecordTransformerTest.record_v1; + +public class DynamodbEventTransformerTest { + + private final DynamodbEvent dynamodbEvent; + + { + record_event.setEventSourceARN("arn:aws:dynamodb:us-west-2:account-id:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899"); + dynamodbEvent = new DynamodbEvent(); + dynamodbEvent.setRecords(Collections.singletonList(record_event)); + } + + private final List expectedRecordsV2 = Collections.singletonList(record_v1); + + @Test + public void testDynamodbEventToRecordsV2() { + List convertedRecords = DynamodbEventTransformer.toRecordsV1(dynamodbEvent); + Assertions.assertEquals(expectedRecordsV2, convertedRecords); + } + + @Test + public void testDynamodbEventToRecordsV2_FiltersNullRecords() { + DynamodbEvent event = dynamodbEvent.clone(); + event.setRecords(Arrays.asList(record_event, null)); + Assertions.assertEquals(2, event.getRecords().size()); + + List convertedRecords = DynamodbEventTransformer.toRecordsV1(event); + Assertions.assertEquals(expectedRecordsV2, convertedRecords); + Assertions.assertEquals(1, convertedRecords.size()); + } +} \ No newline at end of file diff --git a/aws-lambda-java-events-sdk-transformer/src/test/java/com/amazonaws/services/lambda/runtime/events/transformers/v1/dynamodb/DynamodbAttributeValueTransformerTest.java b/aws-lambda-java-events-sdk-transformer/src/test/java/com/amazonaws/services/lambda/runtime/events/transformers/v1/dynamodb/DynamodbAttributeValueTransformerTest.java new file mode 100644 index 000000000..14534fae5 --- /dev/null +++ b/aws-lambda-java-events-sdk-transformer/src/test/java/com/amazonaws/services/lambda/runtime/events/transformers/v1/dynamodb/DynamodbAttributeValueTransformerTest.java @@ -0,0 +1,316 @@ +package com.amazonaws.services.lambda.runtime.events.transformers.v1.dynamodb; + +import com.amazonaws.services.lambda.runtime.events.models.dynamodb.AttributeValue; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + +class DynamodbAttributeValueTransformerTest { + + private static final String valueN = "101"; + private static final List valueNS = Arrays.asList("1", "2", "3"); + private static final String valueS = "SVal"; + private static final List valueSS = Arrays.asList("first", "second", "third"); + private static final ByteBuffer valueB = ByteBuffer.wrap("BVal".getBytes()); + private static final List valueBS = Arrays.asList( + ByteBuffer.wrap("first".getBytes()), + ByteBuffer.wrap("second".getBytes()), + ByteBuffer.wrap("third".getBytes())); + private static final boolean valueBOOL = true; + private static final boolean valueNUL = true; + + private static final String keyM1 = "NestedMapKey1"; + private static final String keyM2 = "NestedMapKey2"; + + //region AttributeValue_event + public static final AttributeValue attributeValueN_event = new AttributeValue().withN(valueN); + public static final AttributeValue attributeValueNS_event = new AttributeValue().withNS(valueNS); + public static final AttributeValue attributeValueS_event = new AttributeValue().withS(valueS); + public static final AttributeValue attributeValueSS_event = new AttributeValue().withSS(valueSS); + public static final AttributeValue attributeValueB_event = new AttributeValue().withB(valueB); + public static final AttributeValue attributeValueBS_event = new AttributeValue().withBS(valueBS); + public static final AttributeValue attributeValueBOOL_event = new AttributeValue().withBOOL(valueBOOL); + public static final AttributeValue attributeValueNUL_event = new AttributeValue().withNULL(valueNUL); + public static final AttributeValue attributeValueM_event = new AttributeValue().withM(new HashMap() {{ + put(keyM1, attributeValueN_event); + put(keyM2, attributeValueS_event); + }}); + public static final AttributeValue attributeValueL_event = new AttributeValue().withL(Arrays.asList( + attributeValueN_event, + attributeValueNS_event, + attributeValueS_event, + attributeValueSS_event, + attributeValueB_event, + attributeValueBS_event, + attributeValueBOOL_event, + attributeValueNUL_event, + attributeValueM_event, + new AttributeValue().withL(Arrays.asList( + attributeValueN_event, + attributeValueNS_event, + attributeValueS_event, + attributeValueSS_event, + attributeValueB_event, + attributeValueBS_event, + attributeValueBOOL_event, + attributeValueNUL_event, + attributeValueM_event + )) + )); + //endregion + + //region AttributeValue_v1 + public static final com.amazonaws.services.dynamodbv2.model.AttributeValue attributeValueN_v1 = + new com.amazonaws.services.dynamodbv2.model.AttributeValue().withN(valueN); + public static final com.amazonaws.services.dynamodbv2.model.AttributeValue attributeValueNS_v1 = + new com.amazonaws.services.dynamodbv2.model.AttributeValue().withNS(valueNS); + public static final com.amazonaws.services.dynamodbv2.model.AttributeValue attributeValueS_v1 = + new com.amazonaws.services.dynamodbv2.model.AttributeValue().withS(valueS); + public static final com.amazonaws.services.dynamodbv2.model.AttributeValue attributeValueSS_v1 = + new com.amazonaws.services.dynamodbv2.model.AttributeValue().withSS(valueSS); + public static final com.amazonaws.services.dynamodbv2.model.AttributeValue attributeValueB_v1 = + new com.amazonaws.services.dynamodbv2.model.AttributeValue().withB(valueB); + public static final com.amazonaws.services.dynamodbv2.model.AttributeValue attributeValueBS_v1 = + new com.amazonaws.services.dynamodbv2.model.AttributeValue().withBS(valueBS); + public static final com.amazonaws.services.dynamodbv2.model.AttributeValue attributeValueBOOL_v1 = + new com.amazonaws.services.dynamodbv2.model.AttributeValue().withBOOL(valueBOOL); + public static final com.amazonaws.services.dynamodbv2.model.AttributeValue attributeValueNUL_v1 = + new com.amazonaws.services.dynamodbv2.model.AttributeValue().withNULL(valueNUL); + public static final com.amazonaws.services.dynamodbv2.model.AttributeValue attributeValueM_v1 = + new com.amazonaws.services.dynamodbv2.model.AttributeValue().withM(new HashMap() {{ + put(keyM1, attributeValueN_v1); + put(keyM2, attributeValueS_v1); + }}); + public static final com.amazonaws.services.dynamodbv2.model.AttributeValue attributeValueL_v1 = + new com.amazonaws.services.dynamodbv2.model.AttributeValue().withL(Arrays.asList( + attributeValueN_v1, + attributeValueNS_v1, + attributeValueS_v1, + attributeValueSS_v1, + attributeValueB_v1, + attributeValueBS_v1, + attributeValueBOOL_v1, + attributeValueNUL_v1, + attributeValueM_v1, + new com.amazonaws.services.dynamodbv2.model.AttributeValue().withL(Arrays.asList( + attributeValueN_v1, + attributeValueNS_v1, + attributeValueS_v1, + attributeValueSS_v1, + attributeValueB_v1, + attributeValueBS_v1, + attributeValueBOOL_v1, + attributeValueNUL_v1, + attributeValueM_v1 + )) + )); + //endregion + + @Test + public void testToAttributeValueV1_N() { + com.amazonaws.services.dynamodbv2.model.AttributeValue convertedAttributeValueN = + DynamodbAttributeValueTransformer.toAttributeValueV1(attributeValueN_event); + Assertions.assertEquals(attributeValueN_v1, convertedAttributeValueN); + } + + @Test + public void testToAttributeValueV1_NS() { + com.amazonaws.services.dynamodbv2.model.AttributeValue convertedAttributeValueNS = + DynamodbAttributeValueTransformer.toAttributeValueV1(attributeValueNS_event); + Assertions.assertEquals(attributeValueNS_v1, convertedAttributeValueNS); + } + + @Test + public void testToAttributeValueV1_S() { + com.amazonaws.services.dynamodbv2.model.AttributeValue convertedAttributeValueS = + DynamodbAttributeValueTransformer.toAttributeValueV1(attributeValueS_event); + Assertions.assertEquals(attributeValueS_v1, convertedAttributeValueS); + } + + @Test + public void testToAttributeValueV1_SS() { + com.amazonaws.services.dynamodbv2.model.AttributeValue convertedAttributeValueSS = + DynamodbAttributeValueTransformer.toAttributeValueV1(attributeValueSS_event); + Assertions.assertEquals(attributeValueSS_v1, convertedAttributeValueSS); + } + + @Test + public void testToAttributeValueV1_B() { + com.amazonaws.services.dynamodbv2.model.AttributeValue convertedAttributeValueB = + DynamodbAttributeValueTransformer.toAttributeValueV1(attributeValueB_event); + Assertions.assertEquals(attributeValueB_v1, convertedAttributeValueB); + } + + @Test + public void testToAttributeValueV1_BS() { + com.amazonaws.services.dynamodbv2.model.AttributeValue convertedAttributeValueBS = + DynamodbAttributeValueTransformer.toAttributeValueV1(attributeValueBS_event); + Assertions.assertEquals(attributeValueBS_v1, convertedAttributeValueBS); + } + + @Test + public void testToAttributeValueV1_BOOL() { + com.amazonaws.services.dynamodbv2.model.AttributeValue convertedAttributeValueBOOL = + DynamodbAttributeValueTransformer.toAttributeValueV1(attributeValueBOOL_event); + Assertions.assertEquals(attributeValueBOOL_v1, convertedAttributeValueBOOL); + } + + @Test + public void testToAttributeValueV1_NUL() { + com.amazonaws.services.dynamodbv2.model.AttributeValue convertedAttributeValueNUL = + DynamodbAttributeValueTransformer.toAttributeValueV1(attributeValueNUL_event); + Assertions.assertEquals(attributeValueNUL_v1, convertedAttributeValueNUL); + } + + @Test + public void testToAttributeValueV1_M() { + com.amazonaws.services.dynamodbv2.model.AttributeValue convertedAttributeValueM = + DynamodbAttributeValueTransformer.toAttributeValueV1(attributeValueM_event); + Assertions.assertEquals(attributeValueM_v1, convertedAttributeValueM); + } + + @Test + public void testToAttributeValueV1_L() { + com.amazonaws.services.dynamodbv2.model.AttributeValue convertedAttributeValueL = + DynamodbAttributeValueTransformer.toAttributeValueV1(attributeValueL_event); + Assertions.assertEquals(attributeValueL_v1, convertedAttributeValueL); + Assertions.assertEquals("ArrayList", convertedAttributeValueL.getL().getClass().getSimpleName(), "List is mutable"); + } + + @Test + public void testToAttributeValueV1_IllegalArgumentWhenNull() { + Assertions.assertThrows(IllegalArgumentException.class, () -> + DynamodbAttributeValueTransformer.toAttributeValueV1(new AttributeValue()) + ); + } + + @Test + public void testToAttributeValueV1_IllegalArgumentWhenNull_N() { + Assertions.assertThrows(IllegalArgumentException.class, () -> + DynamodbAttributeValueTransformer.toAttributeValueV1(new AttributeValue().withN(null)) + ); + } + + @Test + public void testToAttributeValueV1_IllegalArgumentWhenNull_S() { + Assertions.assertThrows(IllegalArgumentException.class, () -> + DynamodbAttributeValueTransformer.toAttributeValueV1(new AttributeValue().withS(null)) + ); + } + + @Test + public void testToAttributeValueV1_IllegalArgumentWhenNull_B() { + Assertions.assertThrows(IllegalArgumentException.class, () -> + DynamodbAttributeValueTransformer.toAttributeValueV1(new AttributeValue().withB(null)) + ); + } + + @Test + public void testToAttributeValueV1_IllegalArgumentWhenNull_BOOL() { + Assertions.assertThrows(IllegalArgumentException.class, () -> + DynamodbAttributeValueTransformer.toAttributeValueV1(new AttributeValue().withBOOL(null)) + ); + } + + @Test + public void testToAttributeValueV1_IllegalArgumentWhenNull_NUL() { + Assertions.assertThrows(IllegalArgumentException.class, () -> + DynamodbAttributeValueTransformer.toAttributeValueV1(new AttributeValue().withNULL(null)) + ); + } + + @Test + public void testToAttributeValueV1_IllegalArgumentWhenNull_M() { + Assertions.assertThrows(IllegalArgumentException.class, () -> + DynamodbAttributeValueTransformer.toAttributeValueV1(new AttributeValue().withM(null)) + ); + } + + @Test + public void testToAttributeValueV1_DoesNotThrowWhenEmpty_NS() { + Assertions.assertDoesNotThrow(() -> + DynamodbAttributeValueTransformer.toAttributeValueV1(new AttributeValue().withNS()) + ); + Assertions.assertDoesNotThrow(() -> + DynamodbAttributeValueTransformer.toAttributeValueV1(new AttributeValue().withNS(Collections.emptyList())) + ); + } + + @Test + public void testToAttributeValueV1_DoesNotThrowWhenEmpty_SS() { + Assertions.assertDoesNotThrow(() -> + DynamodbAttributeValueTransformer.toAttributeValueV1(new AttributeValue().withSS()) + ); + Assertions.assertDoesNotThrow(() -> + DynamodbAttributeValueTransformer.toAttributeValueV1(new AttributeValue().withSS(Collections.emptyList())) + ); + } + + @Test + public void testToAttributeValueV1_DoesNotThrowWhenEmpty_BS() { + Assertions.assertDoesNotThrow(() -> + DynamodbAttributeValueTransformer.toAttributeValueV1(new AttributeValue().withBS()) + ); + Assertions.assertDoesNotThrow(() -> + DynamodbAttributeValueTransformer.toAttributeValueV1(new AttributeValue().withBS(Collections.emptyList())) + ); + } + + @Test + public void testToAttributeValueV1_DoesNotThrowWhenEmpty_L() { + Assertions.assertDoesNotThrow(() -> { + com.amazonaws.services.dynamodbv2.model.AttributeValue attributeValue = DynamodbAttributeValueTransformer.toAttributeValueV1(new AttributeValue().withL()); + Assertions.assertEquals("ArrayList", attributeValue.getL().getClass().getSimpleName(), "List is mutable"); + }); + Assertions.assertDoesNotThrow(() -> { + com.amazonaws.services.dynamodbv2.model.AttributeValue attributeValue = DynamodbAttributeValueTransformer.toAttributeValueV1(new AttributeValue().withL(Collections.emptyList())); + Assertions.assertEquals("ArrayList", attributeValue.getL().getClass().getSimpleName(), "List is mutable"); + }); + } + + @Test + public void testToAttributeValueV1_EmptyV1ObjectWhenEmpty_NS() { + com.amazonaws.services.dynamodbv2.model.AttributeValue expectedAttributeValue_v1 = + new com.amazonaws.services.dynamodbv2.model.AttributeValue(); + Assertions.assertEquals(expectedAttributeValue_v1, + DynamodbAttributeValueTransformer.toAttributeValueV1(new AttributeValue().withNS())); + Assertions.assertEquals(expectedAttributeValue_v1, + DynamodbAttributeValueTransformer.toAttributeValueV1(new AttributeValue().withNS(Collections.emptyList()))); + } + + @Test + public void testToAttributeValueV1_EmptyV1ObjectWhenEmpty_SS() { + com.amazonaws.services.dynamodbv2.model.AttributeValue expectedAttributeValue_v1 = + new com.amazonaws.services.dynamodbv2.model.AttributeValue(); + Assertions.assertEquals(expectedAttributeValue_v1, + DynamodbAttributeValueTransformer.toAttributeValueV1(new AttributeValue().withSS())); + Assertions.assertEquals(expectedAttributeValue_v1, + DynamodbAttributeValueTransformer.toAttributeValueV1(new AttributeValue().withSS(Collections.emptyList()))); + } + + @Test + public void testToAttributeValueV1_EmptyV1ObjectWhenEmpty_BS() { + com.amazonaws.services.dynamodbv2.model.AttributeValue expectedAttributeValue_v1 = + new com.amazonaws.services.dynamodbv2.model.AttributeValue(); + Assertions.assertEquals(expectedAttributeValue_v1, + DynamodbAttributeValueTransformer.toAttributeValueV1(new AttributeValue().withBS())); + Assertions.assertEquals(expectedAttributeValue_v1, + DynamodbAttributeValueTransformer.toAttributeValueV1(new AttributeValue().withBS(Collections.emptyList()))); + } + + @Test + public void testToAttributeValueV1_EmptyV1ObjectWhenEmpty_L() { + com.amazonaws.services.dynamodbv2.model.AttributeValue expectedAttributeValue_v1 = + new com.amazonaws.services.dynamodbv2.model.AttributeValue().withL(Collections.emptyList()); + Assertions.assertEquals(expectedAttributeValue_v1, + DynamodbAttributeValueTransformer.toAttributeValueV1(new AttributeValue().withL())); + Assertions.assertEquals(expectedAttributeValue_v1, + DynamodbAttributeValueTransformer.toAttributeValueV1(new AttributeValue().withL(Collections.emptyList()))); + } + +} \ No newline at end of file diff --git a/aws-lambda-java-events-sdk-transformer/src/test/java/com/amazonaws/services/lambda/runtime/events/transformers/v1/dynamodb/DynamodbIdentityTransformerTest.java b/aws-lambda-java-events-sdk-transformer/src/test/java/com/amazonaws/services/lambda/runtime/events/transformers/v1/dynamodb/DynamodbIdentityTransformerTest.java new file mode 100644 index 000000000..5da2f319b --- /dev/null +++ b/aws-lambda-java-events-sdk-transformer/src/test/java/com/amazonaws/services/lambda/runtime/events/transformers/v1/dynamodb/DynamodbIdentityTransformerTest.java @@ -0,0 +1,30 @@ +package com.amazonaws.services.lambda.runtime.events.transformers.v1.dynamodb; + +import com.amazonaws.services.dynamodbv2.model.Identity; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class DynamodbIdentityTransformerTest { + + private static final String principalId = "1234567890"; + private static final String identityType = "type"; + + //region Identity_event + public static final com.amazonaws.services.lambda.runtime.events.models.dynamodb.Identity identity_event = new com.amazonaws.services.lambda.runtime.events.models.dynamodb.Identity() + .withPrincipalId(principalId) + .withType(identityType); + //endregion + + //region Identity_v1 + public static final Identity identity_v1 = new Identity() + .withPrincipalId(principalId) + .withType(identityType); + //endregion + + @Test + public void testToIdentityV1() { + Identity convertedIdentity = DynamodbIdentityTransformer.toIdentityV1(identity_event); + Assertions.assertEquals(identity_v1, convertedIdentity); + } + +} \ No newline at end of file diff --git a/aws-lambda-java-events-sdk-transformer/src/test/java/com/amazonaws/services/lambda/runtime/events/transformers/v1/dynamodb/DynamodbRecordTransformerTest.java b/aws-lambda-java-events-sdk-transformer/src/test/java/com/amazonaws/services/lambda/runtime/events/transformers/v1/dynamodb/DynamodbRecordTransformerTest.java new file mode 100644 index 000000000..4504da1db --- /dev/null +++ b/aws-lambda-java-events-sdk-transformer/src/test/java/com/amazonaws/services/lambda/runtime/events/transformers/v1/dynamodb/DynamodbRecordTransformerTest.java @@ -0,0 +1,63 @@ +package com.amazonaws.services.lambda.runtime.events.transformers.v1.dynamodb; + +import com.amazonaws.services.dynamodbv2.model.OperationType; +import com.amazonaws.services.dynamodbv2.model.Record; +import com.amazonaws.services.lambda.runtime.events.DynamodbEvent; +import com.amazonaws.services.lambda.runtime.events.transformers.v1.DynamodbEventTransformer; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import static com.amazonaws.services.lambda.runtime.events.transformers.v1.dynamodb.DynamodbIdentityTransformerTest.identity_event; +import static com.amazonaws.services.lambda.runtime.events.transformers.v1.dynamodb.DynamodbIdentityTransformerTest.identity_v1; +import static com.amazonaws.services.lambda.runtime.events.transformers.v1.dynamodb.DynamodbStreamRecordTransformerTest.streamRecord_event; +import static com.amazonaws.services.lambda.runtime.events.transformers.v1.dynamodb.DynamodbStreamRecordTransformerTest.streamRecord_v1; + +public class DynamodbRecordTransformerTest { + + private static final String eventId = "2"; + private static final String eventName = OperationType.MODIFY.toString(); + private static final String eventVersion = "1.0"; + private static final String eventSource = "aws:dynamodb"; + private static final String awsRegion = "us-west-2"; + + //region Record_event + public static final DynamodbEvent.DynamodbStreamRecord record_event = (DynamodbEvent.DynamodbStreamRecord) + new DynamodbEvent.DynamodbStreamRecord() + .withEventID(eventId) + .withEventName(eventName) + .withEventVersion(eventVersion) + .withEventSource(eventSource) + .withAwsRegion(awsRegion) + .withDynamodb(streamRecord_event) + .withUserIdentity(identity_event); + //endregion + + //region Record_v1 + public static final Record record_v1 = + new Record() + .withEventID(eventId) + .withEventName(eventName) + .withEventVersion(eventVersion) + .withEventSource(eventSource) + .withAwsRegion(awsRegion) + .withDynamodb(streamRecord_v1) + .withUserIdentity(identity_v1); + //endregion + + @Test + public void testToRecordV1() { + Record convertedRecord = DynamodbRecordTransformer.toRecordV1(record_event); + Assertions.assertEquals(record_v1, convertedRecord); + } + + @Test + public void testToRecordV1WhenUserIdentityIsNull() { + DynamodbEvent.DynamodbStreamRecord record = record_event.clone(); + record.setUserIdentity(null); + + Assertions.assertDoesNotThrow(() -> { + com.amazonaws.services.lambda.runtime.events.transformers.v1.dynamodb.DynamodbRecordTransformer.toRecordV1(record); + }); + } + +} \ No newline at end of file diff --git a/aws-lambda-java-events-sdk-transformer/src/test/java/com/amazonaws/services/lambda/runtime/events/transformers/v1/dynamodb/DynamodbStreamRecordTransformerTest.java b/aws-lambda-java-events-sdk-transformer/src/test/java/com/amazonaws/services/lambda/runtime/events/transformers/v1/dynamodb/DynamodbStreamRecordTransformerTest.java new file mode 100644 index 000000000..594d2a1dc --- /dev/null +++ b/aws-lambda-java-events-sdk-transformer/src/test/java/com/amazonaws/services/lambda/runtime/events/transformers/v1/dynamodb/DynamodbStreamRecordTransformerTest.java @@ -0,0 +1,150 @@ +package com.amazonaws.services.lambda.runtime.events.transformers.v1.dynamodb; + +import com.amazonaws.services.dynamodbv2.model.StreamRecord; +import com.amazonaws.services.dynamodbv2.model.StreamViewType; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.Date; +import java.util.HashMap; + +import static com.amazonaws.services.lambda.runtime.events.transformers.v1.dynamodb.DynamodbAttributeValueTransformerTest.attributeValueBOOL_event; +import static com.amazonaws.services.lambda.runtime.events.transformers.v1.dynamodb.DynamodbAttributeValueTransformerTest.attributeValueBOOL_v1; +import static com.amazonaws.services.lambda.runtime.events.transformers.v1.dynamodb.DynamodbAttributeValueTransformerTest.attributeValueBS_event; +import static com.amazonaws.services.lambda.runtime.events.transformers.v1.dynamodb.DynamodbAttributeValueTransformerTest.attributeValueBS_v1; +import static com.amazonaws.services.lambda.runtime.events.transformers.v1.dynamodb.DynamodbAttributeValueTransformerTest.attributeValueB_event; +import static com.amazonaws.services.lambda.runtime.events.transformers.v1.dynamodb.DynamodbAttributeValueTransformerTest.attributeValueB_v1; +import static com.amazonaws.services.lambda.runtime.events.transformers.v1.dynamodb.DynamodbAttributeValueTransformerTest.attributeValueL_event; +import static com.amazonaws.services.lambda.runtime.events.transformers.v1.dynamodb.DynamodbAttributeValueTransformerTest.attributeValueL_v1; +import static com.amazonaws.services.lambda.runtime.events.transformers.v1.dynamodb.DynamodbAttributeValueTransformerTest.attributeValueM_event; +import static com.amazonaws.services.lambda.runtime.events.transformers.v1.dynamodb.DynamodbAttributeValueTransformerTest.attributeValueM_v1; +import static com.amazonaws.services.lambda.runtime.events.transformers.v1.dynamodb.DynamodbAttributeValueTransformerTest.attributeValueNS_event; +import static com.amazonaws.services.lambda.runtime.events.transformers.v1.dynamodb.DynamodbAttributeValueTransformerTest.attributeValueNS_v1; +import static com.amazonaws.services.lambda.runtime.events.transformers.v1.dynamodb.DynamodbAttributeValueTransformerTest.attributeValueNUL_event; +import static com.amazonaws.services.lambda.runtime.events.transformers.v1.dynamodb.DynamodbAttributeValueTransformerTest.attributeValueNUL_v1; +import static com.amazonaws.services.lambda.runtime.events.transformers.v1.dynamodb.DynamodbAttributeValueTransformerTest.attributeValueN_event; +import static com.amazonaws.services.lambda.runtime.events.transformers.v1.dynamodb.DynamodbAttributeValueTransformerTest.attributeValueN_v1; +import static com.amazonaws.services.lambda.runtime.events.transformers.v1.dynamodb.DynamodbAttributeValueTransformerTest.attributeValueSS_event; +import static com.amazonaws.services.lambda.runtime.events.transformers.v1.dynamodb.DynamodbAttributeValueTransformerTest.attributeValueSS_v1; +import static com.amazonaws.services.lambda.runtime.events.transformers.v1.dynamodb.DynamodbAttributeValueTransformerTest.attributeValueS_event; +import static com.amazonaws.services.lambda.runtime.events.transformers.v1.dynamodb.DynamodbAttributeValueTransformerTest.attributeValueS_v1; + +class DynamodbStreamRecordTransformerTest { + + private static final String keyNK = "Id"; + private static final String keyNSK = "KeyNS"; + + private static final String keySK = "SKey"; + private static final String keySSK = "KeySS"; + + private static final String keyBK = "BKey"; + private static final String keyBSK = "KeyBS"; + + private static final String keyBOOLK = "IsBool"; + private static final String keyNULK = "nil"; + + private static final String keyMK = "MapKey"; + + private static final String keyLK = "LongNum"; + + private static final String oldImageSK = "Message"; + private static final String newImageSK = "Message"; + private static final String streamViewType = StreamViewType.NEW_AND_OLD_IMAGES.toString(); + private static final String sequenceNumber = "222"; + private static final Long sizeBytes = 59L; + private static final Date approximateCreationDateTime = new Date(); + + //region StreamRecord_event + public static final com.amazonaws.services.lambda.runtime.events.models.dynamodb.StreamRecord streamRecord_event = new com.amazonaws.services.lambda.runtime.events.models.dynamodb.StreamRecord() + .withKeys(new HashMap() { + { + put(keyNK, attributeValueN_event); + put(keyNSK, attributeValueNS_event); + put(keySK, attributeValueS_event); + put(keySSK, attributeValueSS_event); + put(keyBK, attributeValueB_event); + put(keyBSK, attributeValueBS_event); + put(keyBOOLK, attributeValueBOOL_event); + put(keyNULK, attributeValueNUL_event); + put(keyMK, attributeValueM_event); + put(keyLK, attributeValueL_event); + } + }) + .withOldImage(new HashMap() { + { + put(oldImageSK, attributeValueS_event); + put(keyNK, attributeValueN_event); + } + }) + .withNewImage(new HashMap() { + { + put(newImageSK, attributeValueS_event); + put(keyNK, attributeValueN_event); + } + }) + .withStreamViewType(com.amazonaws.services.lambda.runtime.events.models.dynamodb.StreamViewType.fromValue(streamViewType)) + .withSequenceNumber(sequenceNumber) + .withSizeBytes(sizeBytes) + .withApproximateCreationDateTime(approximateCreationDateTime); + //endregion + + //region StreamRecord_v1 + public static final StreamRecord streamRecord_v1 = new StreamRecord() + .withApproximateCreationDateTime(approximateCreationDateTime) + .withKeys(new HashMap() { + { + put(keyNK, attributeValueN_v1); + put(keyNSK, attributeValueNS_v1); + put(keySK, attributeValueS_v1); + put(keySSK, attributeValueSS_v1); + put(keyBK, attributeValueB_v1); + put(keyBSK, attributeValueBS_v1); + put(keyBOOLK, attributeValueBOOL_v1); + put(keyNULK, attributeValueNUL_v1); + put(keyMK, attributeValueM_v1); + put(keyLK, attributeValueL_v1); + } + }) + .withOldImage(new HashMap() { + { + put(oldImageSK, attributeValueS_v1); + put(keyNK, attributeValueN_v1); + } + }) + .withNewImage(new HashMap() { + { + put(newImageSK, attributeValueS_v1); + put(keyNK, attributeValueN_v1); + } + }) + .withSequenceNumber(sequenceNumber) + .withSizeBytes(sizeBytes) + .withStreamViewType(streamViewType); + //endregion + + @Test + public void testToStreamRecordV1() { + StreamRecord convertedStreamRecord = DynamodbStreamRecordTransformer.toStreamRecordV1(streamRecord_event); + Assertions.assertEquals(streamRecord_v1, convertedStreamRecord); + } + + @Test + public void testToStreamRecordV1WhenOldImageIsNull() { + com.amazonaws.services.lambda.runtime.events.models.dynamodb.StreamRecord streamRecord = streamRecord_event.clone(); + streamRecord.setOldImage(null); + + Assertions.assertDoesNotThrow(() -> { + com.amazonaws.services.lambda.runtime.events.transformers.v1.dynamodb.DynamodbStreamRecordTransformer.toStreamRecordV1(streamRecord); + }); + } + + @Test + public void testToStreamRecordV1WhenNewImageIsNull() { + com.amazonaws.services.lambda.runtime.events.models.dynamodb.StreamRecord streamRecord = streamRecord_event.clone(); + streamRecord.setNewImage(null); + + Assertions.assertDoesNotThrow(() -> { + com.amazonaws.services.lambda.runtime.events.transformers.v1.dynamodb.DynamodbStreamRecordTransformer.toStreamRecordV1(streamRecord); + }); + } +} \ No newline at end of file diff --git a/aws-lambda-java-events-sdk-transformer/src/test/java/com/amazonaws/services/lambda/runtime/events/transformers/DynamodbEventTransformerTest.java b/aws-lambda-java-events-sdk-transformer/src/test/java/com/amazonaws/services/lambda/runtime/events/transformers/v2/DynamodbEventTransformerTest.java similarity index 92% rename from aws-lambda-java-events-sdk-transformer/src/test/java/com/amazonaws/services/lambda/runtime/events/transformers/DynamodbEventTransformerTest.java rename to aws-lambda-java-events-sdk-transformer/src/test/java/com/amazonaws/services/lambda/runtime/events/transformers/v2/DynamodbEventTransformerTest.java index 6fedb3fe8..e9e385480 100644 --- a/aws-lambda-java-events-sdk-transformer/src/test/java/com/amazonaws/services/lambda/runtime/events/transformers/DynamodbEventTransformerTest.java +++ b/aws-lambda-java-events-sdk-transformer/src/test/java/com/amazonaws/services/lambda/runtime/events/transformers/v2/DynamodbEventTransformerTest.java @@ -1,4 +1,4 @@ -package com.amazonaws.services.lambda.runtime.events.transformers; +package com.amazonaws.services.lambda.runtime.events.transformers.v2; import com.amazonaws.services.lambda.runtime.events.DynamodbEvent; import org.junit.jupiter.api.Assertions; @@ -9,12 +9,13 @@ import java.util.Collections; import java.util.List; -import static com.amazonaws.services.lambda.runtime.events.transformers.dynamodb.DynamodbRecordTransformerTest.record_event; -import static com.amazonaws.services.lambda.runtime.events.transformers.dynamodb.DynamodbRecordTransformerTest.record_v2; +import static com.amazonaws.services.lambda.runtime.events.transformers.v2.dynamodb.DynamodbRecordTransformerTest.record_event; +import static com.amazonaws.services.lambda.runtime.events.transformers.v2.dynamodb.DynamodbRecordTransformerTest.record_v2; public class DynamodbEventTransformerTest { private final DynamodbEvent dynamodbEvent; + { record_event.setEventSourceARN("arn:aws:dynamodb:us-west-2:account-id:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899"); dynamodbEvent = new DynamodbEvent(); diff --git a/aws-lambda-java-events-sdk-transformer/src/test/java/com/amazonaws/services/lambda/runtime/events/transformers/dynamodb/DynamodbAttributeValueTransformerTest.java b/aws-lambda-java-events-sdk-transformer/src/test/java/com/amazonaws/services/lambda/runtime/events/transformers/v2/dynamodb/DynamodbAttributeValueTransformerTest.java similarity index 94% rename from aws-lambda-java-events-sdk-transformer/src/test/java/com/amazonaws/services/lambda/runtime/events/transformers/dynamodb/DynamodbAttributeValueTransformerTest.java rename to aws-lambda-java-events-sdk-transformer/src/test/java/com/amazonaws/services/lambda/runtime/events/transformers/v2/dynamodb/DynamodbAttributeValueTransformerTest.java index e9b3f7b50..1c7f05f7d 100644 --- a/aws-lambda-java-events-sdk-transformer/src/test/java/com/amazonaws/services/lambda/runtime/events/transformers/dynamodb/DynamodbAttributeValueTransformerTest.java +++ b/aws-lambda-java-events-sdk-transformer/src/test/java/com/amazonaws/services/lambda/runtime/events/transformers/v2/dynamodb/DynamodbAttributeValueTransformerTest.java @@ -1,4 +1,4 @@ -package com.amazonaws.services.lambda.runtime.events.transformers.dynamodb; +package com.amazonaws.services.lambda.runtime.events.transformers.v2.dynamodb; import com.amazonaws.services.lambda.runtime.events.models.dynamodb.AttributeValue; import org.junit.jupiter.api.Assertions; @@ -183,6 +183,7 @@ public void testToAttributeValueV2_L() { software.amazon.awssdk.services.dynamodb.model.AttributeValue convertedAttributeValueL = DynamodbAttributeValueTransformer.toAttributeValueV2(attributeValueL_event); Assertions.assertEquals(attributeValueL_v2, convertedAttributeValueL); + Assertions.assertEquals("UnmodifiableRandomAccessList", convertedAttributeValueL.l().getClass().getSimpleName(), "List is immutable"); } @Test @@ -266,12 +267,14 @@ public void testToAttributeValueV2_DoesNotThrowWhenEmpty_BS() { @Test public void testToAttributeValueV2_DoesNotThrowWhenEmpty_L() { - Assertions.assertDoesNotThrow(() -> - DynamodbAttributeValueTransformer.toAttributeValueV2(new AttributeValue().withL()) - ); - Assertions.assertDoesNotThrow(() -> - DynamodbAttributeValueTransformer.toAttributeValueV2(new AttributeValue().withL(Collections.emptyList())) - ); + Assertions.assertDoesNotThrow(() -> { + software.amazon.awssdk.services.dynamodb.model.AttributeValue attributeValue = DynamodbAttributeValueTransformer.toAttributeValueV2(new AttributeValue().withL()); + Assertions.assertEquals("UnmodifiableRandomAccessList", attributeValue.l().getClass().getSimpleName(), "List is immutable"); + }); + Assertions.assertDoesNotThrow(() -> { + software.amazon.awssdk.services.dynamodb.model.AttributeValue attributeValue = DynamodbAttributeValueTransformer.toAttributeValueV2(new AttributeValue().withL(Collections.emptyList())); + Assertions.assertEquals("UnmodifiableRandomAccessList", attributeValue.l().getClass().getSimpleName(), "List is immutable"); + }); } @Test @@ -307,7 +310,7 @@ public void testToAttributeValueV2_EmptyV2ObjectWhenEmpty_BS() { @Test public void testToAttributeValueV2_EmptyV2ObjectWhenEmpty_L() { software.amazon.awssdk.services.dynamodb.model.AttributeValue expectedAttributeValue_v2 = - software.amazon.awssdk.services.dynamodb.model.AttributeValue.builder().build(); + software.amazon.awssdk.services.dynamodb.model.AttributeValue.builder().l(Collections.emptyList()).build(); Assertions.assertEquals(expectedAttributeValue_v2, DynamodbAttributeValueTransformer.toAttributeValueV2(new AttributeValue().withL())); Assertions.assertEquals(expectedAttributeValue_v2, diff --git a/aws-lambda-java-events-sdk-transformer/src/test/java/com/amazonaws/services/lambda/runtime/events/transformers/dynamodb/DynamodbIdentityTransformerTest.java b/aws-lambda-java-events-sdk-transformer/src/test/java/com/amazonaws/services/lambda/runtime/events/transformers/v2/dynamodb/DynamodbIdentityTransformerTest.java similarity index 98% rename from aws-lambda-java-events-sdk-transformer/src/test/java/com/amazonaws/services/lambda/runtime/events/transformers/dynamodb/DynamodbIdentityTransformerTest.java rename to aws-lambda-java-events-sdk-transformer/src/test/java/com/amazonaws/services/lambda/runtime/events/transformers/v2/dynamodb/DynamodbIdentityTransformerTest.java index f3781445a..f4ec64be8 100644 --- a/aws-lambda-java-events-sdk-transformer/src/test/java/com/amazonaws/services/lambda/runtime/events/transformers/dynamodb/DynamodbIdentityTransformerTest.java +++ b/aws-lambda-java-events-sdk-transformer/src/test/java/com/amazonaws/services/lambda/runtime/events/transformers/v2/dynamodb/DynamodbIdentityTransformerTest.java @@ -1,4 +1,4 @@ -package com.amazonaws.services.lambda.runtime.events.transformers.dynamodb; +package com.amazonaws.services.lambda.runtime.events.transformers.v2.dynamodb; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; diff --git a/aws-lambda-java-events-sdk-transformer/src/test/java/com/amazonaws/services/lambda/runtime/events/transformers/dynamodb/DynamodbRecordTransformerTest.java b/aws-lambda-java-events-sdk-transformer/src/test/java/com/amazonaws/services/lambda/runtime/events/transformers/v2/dynamodb/DynamodbRecordTransformerTest.java similarity index 63% rename from aws-lambda-java-events-sdk-transformer/src/test/java/com/amazonaws/services/lambda/runtime/events/transformers/dynamodb/DynamodbRecordTransformerTest.java rename to aws-lambda-java-events-sdk-transformer/src/test/java/com/amazonaws/services/lambda/runtime/events/transformers/v2/dynamodb/DynamodbRecordTransformerTest.java index 69645844b..cd8bbdc88 100644 --- a/aws-lambda-java-events-sdk-transformer/src/test/java/com/amazonaws/services/lambda/runtime/events/transformers/dynamodb/DynamodbRecordTransformerTest.java +++ b/aws-lambda-java-events-sdk-transformer/src/test/java/com/amazonaws/services/lambda/runtime/events/transformers/v2/dynamodb/DynamodbRecordTransformerTest.java @@ -1,4 +1,4 @@ -package com.amazonaws.services.lambda.runtime.events.transformers.dynamodb; +package com.amazonaws.services.lambda.runtime.events.transformers.v2.dynamodb; import com.amazonaws.services.lambda.runtime.events.DynamodbEvent; import org.junit.jupiter.api.Assertions; @@ -6,10 +6,10 @@ import software.amazon.awssdk.services.dynamodb.model.OperationType; import software.amazon.awssdk.services.dynamodb.model.Record; -import static com.amazonaws.services.lambda.runtime.events.transformers.dynamodb.DynamodbIdentityTransformerTest.identity_event; -import static com.amazonaws.services.lambda.runtime.events.transformers.dynamodb.DynamodbIdentityTransformerTest.identity_v2; -import static com.amazonaws.services.lambda.runtime.events.transformers.dynamodb.DynamodbStreamRecordTransformerTest.streamRecord_event; -import static com.amazonaws.services.lambda.runtime.events.transformers.dynamodb.DynamodbStreamRecordTransformerTest.streamRecord_v2; +import static com.amazonaws.services.lambda.runtime.events.transformers.v2.dynamodb.DynamodbIdentityTransformerTest.identity_event; +import static com.amazonaws.services.lambda.runtime.events.transformers.v2.dynamodb.DynamodbIdentityTransformerTest.identity_v2; +import static com.amazonaws.services.lambda.runtime.events.transformers.v2.dynamodb.DynamodbStreamRecordTransformerTest.streamRecord_event; +import static com.amazonaws.services.lambda.runtime.events.transformers.v2.dynamodb.DynamodbStreamRecordTransformerTest.streamRecord_v2; public class DynamodbRecordTransformerTest { @@ -21,14 +21,14 @@ public class DynamodbRecordTransformerTest { //region Record_event public static final DynamodbEvent.DynamodbStreamRecord record_event = (DynamodbEvent.DynamodbStreamRecord) - new DynamodbEvent.DynamodbStreamRecord() - .withEventID(eventId) - .withEventName(eventName) - .withEventVersion(eventVersion) - .withEventSource(eventSource) - .withAwsRegion(awsRegion) - .withDynamodb(streamRecord_event) - .withUserIdentity(identity_event); + new DynamodbEvent.DynamodbStreamRecord() + .withEventID(eventId) + .withEventName(eventName) + .withEventVersion(eventVersion) + .withEventSource(eventSource) + .withAwsRegion(awsRegion) + .withDynamodb(streamRecord_event) + .withUserIdentity(identity_event); //endregion //region Record_v2 @@ -50,4 +50,14 @@ public void testToRecordV2() { Assertions.assertEquals(record_v2, convertedRecord); } + @Test + public void testToRecordV2WhenUserIdentityIsNull() { + DynamodbEvent.DynamodbStreamRecord record = record_event.clone(); + record.setUserIdentity(null); + + Assertions.assertDoesNotThrow(() -> { + DynamodbRecordTransformer.toRecordV2(record); + }); + } + } \ No newline at end of file diff --git a/aws-lambda-java-events-sdk-transformer/src/test/java/com/amazonaws/services/lambda/runtime/events/transformers/dynamodb/DynamodbStreamRecordTransformerTest.java b/aws-lambda-java-events-sdk-transformer/src/test/java/com/amazonaws/services/lambda/runtime/events/transformers/v2/dynamodb/DynamodbStreamRecordTransformerTest.java similarity index 65% rename from aws-lambda-java-events-sdk-transformer/src/test/java/com/amazonaws/services/lambda/runtime/events/transformers/dynamodb/DynamodbStreamRecordTransformerTest.java rename to aws-lambda-java-events-sdk-transformer/src/test/java/com/amazonaws/services/lambda/runtime/events/transformers/v2/dynamodb/DynamodbStreamRecordTransformerTest.java index 5cd3b01b5..d663d1dbf 100644 --- a/aws-lambda-java-events-sdk-transformer/src/test/java/com/amazonaws/services/lambda/runtime/events/transformers/dynamodb/DynamodbStreamRecordTransformerTest.java +++ b/aws-lambda-java-events-sdk-transformer/src/test/java/com/amazonaws/services/lambda/runtime/events/transformers/v2/dynamodb/DynamodbStreamRecordTransformerTest.java @@ -1,4 +1,4 @@ -package com.amazonaws.services.lambda.runtime.events.transformers.dynamodb; +package com.amazonaws.services.lambda.runtime.events.transformers.v2.dynamodb; import com.amazonaws.services.lambda.runtime.events.models.dynamodb.AttributeValue; import com.amazonaws.services.lambda.runtime.events.models.dynamodb.StreamViewType; @@ -9,7 +9,26 @@ import java.util.Date; -import static com.amazonaws.services.lambda.runtime.events.transformers.dynamodb.DynamodbAttributeValueTransformerTest.*; +import static com.amazonaws.services.lambda.runtime.events.transformers.v2.dynamodb.DynamodbAttributeValueTransformerTest.attributeValueBOOL_event; +import static com.amazonaws.services.lambda.runtime.events.transformers.v2.dynamodb.DynamodbAttributeValueTransformerTest.attributeValueBOOL_v2; +import static com.amazonaws.services.lambda.runtime.events.transformers.v2.dynamodb.DynamodbAttributeValueTransformerTest.attributeValueBS_event; +import static com.amazonaws.services.lambda.runtime.events.transformers.v2.dynamodb.DynamodbAttributeValueTransformerTest.attributeValueBS_v2; +import static com.amazonaws.services.lambda.runtime.events.transformers.v2.dynamodb.DynamodbAttributeValueTransformerTest.attributeValueB_event; +import static com.amazonaws.services.lambda.runtime.events.transformers.v2.dynamodb.DynamodbAttributeValueTransformerTest.attributeValueB_v2; +import static com.amazonaws.services.lambda.runtime.events.transformers.v2.dynamodb.DynamodbAttributeValueTransformerTest.attributeValueL_event; +import static com.amazonaws.services.lambda.runtime.events.transformers.v2.dynamodb.DynamodbAttributeValueTransformerTest.attributeValueL_v2; +import static com.amazonaws.services.lambda.runtime.events.transformers.v2.dynamodb.DynamodbAttributeValueTransformerTest.attributeValueM_event; +import static com.amazonaws.services.lambda.runtime.events.transformers.v2.dynamodb.DynamodbAttributeValueTransformerTest.attributeValueM_v2; +import static com.amazonaws.services.lambda.runtime.events.transformers.v2.dynamodb.DynamodbAttributeValueTransformerTest.attributeValueNS_event; +import static com.amazonaws.services.lambda.runtime.events.transformers.v2.dynamodb.DynamodbAttributeValueTransformerTest.attributeValueNS_v2; +import static com.amazonaws.services.lambda.runtime.events.transformers.v2.dynamodb.DynamodbAttributeValueTransformerTest.attributeValueNUL_event; +import static com.amazonaws.services.lambda.runtime.events.transformers.v2.dynamodb.DynamodbAttributeValueTransformerTest.attributeValueNUL_v2; +import static com.amazonaws.services.lambda.runtime.events.transformers.v2.dynamodb.DynamodbAttributeValueTransformerTest.attributeValueN_event; +import static com.amazonaws.services.lambda.runtime.events.transformers.v2.dynamodb.DynamodbAttributeValueTransformerTest.attributeValueN_v2; +import static com.amazonaws.services.lambda.runtime.events.transformers.v2.dynamodb.DynamodbAttributeValueTransformerTest.attributeValueSS_event; +import static com.amazonaws.services.lambda.runtime.events.transformers.v2.dynamodb.DynamodbAttributeValueTransformerTest.attributeValueSS_v2; +import static com.amazonaws.services.lambda.runtime.events.transformers.v2.dynamodb.DynamodbAttributeValueTransformerTest.attributeValueS_event; +import static com.amazonaws.services.lambda.runtime.events.transformers.v2.dynamodb.DynamodbAttributeValueTransformerTest.attributeValueS_v2; class DynamodbStreamRecordTransformerTest { @@ -39,7 +58,7 @@ class DynamodbStreamRecordTransformerTest { //region StreamRecord_event public static final com.amazonaws.services.lambda.runtime.events.models.dynamodb.StreamRecord streamRecord_event = new com.amazonaws.services.lambda.runtime.events.models.dynamodb.StreamRecord() - .withKeys(ImmutableMap. builder() + .withKeys(ImmutableMap.builder() .put(keyNK, attributeValueN_event) .put(keyNSK, attributeValueNS_event) .put(keySK, attributeValueS_event) @@ -69,7 +88,7 @@ class DynamodbStreamRecordTransformerTest { //region StreamRecord_v2 public static final StreamRecord streamRecord_v2 = StreamRecord.builder() .approximateCreationDateTime(approximateCreationDateTime.toInstant()) - .keys(ImmutableMap. builder() + .keys(ImmutableMap.builder() .put(keyNK, attributeValueN_v2) .put(keyNSK, attributeValueNS_v2) .put(keySK, attributeValueS_v2) diff --git a/aws-lambda-java-events/README.md b/aws-lambda-java-events/README.md index bd7e4c84a..43c25d76a 100644 --- a/aws-lambda-java-events/README.md +++ b/aws-lambda-java-events/README.md @@ -1,21 +1,43 @@ -# AWS Lambda Java Events v3.0 +# AWS Lambda Java Events v3 ### Event Models Supported +* `ActiveMQEvent` +* `APIGatewayCustomAuthorizerEvent` * `APIGatewayProxyRequestEvent` * `APIGatewayProxyResponseEvent` +* `APIGatewayV2CustomAuthorizerEvent` * `APIGatewayV2HTTPEvent` * `APIGatewayV2HTTPResponse` * `APIGatewayV2WebSocketEvent` * `APIGatewayV2WebSocketResponse` * `ApplicationLoadBalancerRequestEvent` * `ApplicationLoadBalancerResponseEvent` +* `AppSyncLambdaAuthorizerEvent` +* `AppSyncLambdaAuthorizerResponse` +* `CloudFormationCustomResourceEvent` * `CloudFrontEvent` +* `CloudWatchCompositeAlarmEvent` * `CloudWatchLogsEvent` +* `CloudWatchMetricAlarmEvent` * `CodeCommitEvent` * `CognitoEvent` +* `CognitoUserPoolCreateAuthChallengeEvent` +* `CognitoUserPoolCustomMessageEvent` +* `CognitoUserPoolDefineAuthChallengeEvent` +* `CognitoUserPoolEvent` +* `CognitoUserPoolMigrateUserEvent` +* `CognitoUserPoolPostAuthenticationEvent` +* `CognitoUserPoolPostConfirmationEvent` +* `CognitoUserPoolPreAuthenticationEvent` +* `CognitoUserPoolPreSignUpEvent` +* `CognitoUserPoolPreTokenGenerationEvent` +* `CognitoUserPoolPreTokenGenerationEventV2` +* `CognitoUserPoolVerifyAuthChallengeResponseEvent` * `ConfigEvent` +* `ConnectEvent` * `DynamodbEvent` * `IoTButtonEvent` +* `KafkaEvent` * `KinesisAnalyticsFirehoseInputPreprocessingEvent` * `KinesisAnalyticsInputPreprocessingResponse` * `KinesisAnalyticsOutputDeliveryEvent` @@ -23,18 +45,23 @@ * `KinesisAnalyticsStreamsInputPreprocessingEvent` * `KinesisEvent` * `KinesisFirehoseEvent` +* `LambdaDestinationEvent` * `LexEvent` +* `MSKFirehoseEvent` +* `MSKFirehoseResponse` +* `RabbitMQEvent` +* `S3BatchEvent` +* `S3BatchResponse` * `S3Event` * `ScheduledEvent` +* `SecretsManagerRotationEvent` +* `SimpleIAMPolicyResponse` * `SNSEvent` +* `SQSBatchResponse` * `SQSEvent` -*As of version `3.0.0`, users are no longer required to pull in SDK dependencies in order to use this library.* - -### Getting Started - -[Maven](https://maven.apache.org) +### Usage ```xml @@ -42,34 +69,13 @@ com.amazonaws aws-lambda-java-core - 1.2.1 + 1.2.3 com.amazonaws aws-lambda-java-events - 3.1.0 + 3.16.0 ... ``` - -[Gradle](https://gradle.org) - -```groovy -'com.amazonaws:aws-lambda-java-core:1.2.1' -'com.amazonaws:aws-lambda-java-events:3.1.0' -``` - -[Leiningen](http://leiningen.org) and [Boot](http://boot-clj.com) - -```clojure -[com.amazonaws/aws-lambda-java-core "1.2.1"] -[com.amazonaws/aws-lambda-java-events "3.1.0"] -``` - -[sbt](http://www.scala-sbt.org) - -```scala -"com.amazonaws" % "aws-lambda-java-core" % "1.2.1" -"com.amazonaws" % "aws-lambda-java-events" % "3.1.0" -``` diff --git a/aws-lambda-java-events/RELEASE.CHANGELOG.md b/aws-lambda-java-events/RELEASE.CHANGELOG.md index 708702b36..a4bcd10a0 100644 --- a/aws-lambda-java-events/RELEASE.CHANGELOG.md +++ b/aws-lambda-java-events/RELEASE.CHANGELOG.md @@ -1,3 +1,150 @@ +### June 17, 2025 +`3.16.0`: +- Add Schema metadata related attributes in KafkaEvent ([#548](https://github.com/aws/aws-lambda-java-libs/pull/548)) + +### January 31, 2025 +`3.15.0`: +- Fix `CognitoUserPoolPreTokenGenerationEventV2` model ([#519](https://github.com/aws/aws-lambda-java-libs/pull/519)) +- Add RotationToken to SecretsManagerRotationEvent ([#520](https://github.com/aws/aws-lambda-java-libs/pull/520)) + + +### September 13, 2024 +`3.14.0`: +- Fix name of s3Bucket field of Task class in S3BatchEventV2 ([#506](https://github.com/aws/aws-lambda-java-libs/pull/506)) + +### July 29, 2024 +`3.13.0`: +- Add S3BatchEventV2 ([#496](https://github.com/aws/aws-lambda-java-libs/pull/496)) + +### July 11, 2024 +`3.12.0`: +- Added the object representations of the CloudWatch alarms([#493](https://github.com/aws/aws-lambda-java-libs/pull/493)) +- Added event class MskFirehoseEvent.java for Firehose Lambda transformation when MSK is the source([#490](https://github.com/aws/aws-lambda-java-libs/pull/490)) + +### June 11, 2024 +`3.11.6`: +- Add the V2 version of the pre token generation event([#465](https://github.com/aws/aws-lambda-java-libs/pull/465)) + +### April 12, 2024 +`3.11.5`: +- Add requestHeaders field for Appsync lambda authorizer event([#473](https://github.com/aws/aws-lambda-java-libs/pull/473)) + +### December 1, 2023 +`3.11.4`: +- Improve `toString` in Cognito events by calling `super` +- Added missing `version` field to ScheduledEvent from CloudWatch + +### September 1, 2023 +`3.11.3`: +- Update challengeAnswer field format in CognitoUserPoolEvent + +### May 18, 2023 +`3.11.2`: +- Add missing fields to API Gateway request context + +### March 10, 2023 +`3.11.1`: +- Extended ActiveMQEvent with custom properties ([#408](https://github.com/aws/aws-lambda-java-libs/pull/408)) +- Updated dependencies([#410](https://github.com/aws/aws-lambda-java-libs/pull/410)): + - `joda-time` from 2.6 to 2.10.8 + - `jackson-databind` from 2.13.4.1 to 2.14.2 + - `junit-jupiter-engine` from 5.7.0 to 5.9.2 + - `json-unit-assertj` from 2.22.0 to 2.36.1 + +### November 24, 2021 +`3.11.0`: +- Added support for SQSaaES Partial Batch Feature ([#279](https://github.com/aws/aws-lambda-java-libs/pull/279)) + - `SQSBatchResponse` + +### August 26, 2021 +`3.10.0`: +- Added headers in `KafkaEventRecord` ([#260](https://github.com/aws/aws-lambda-java-libs/pull/260)) +- Added support for AppSync Lambda Authorizer ([#263](https://github.com/aws/aws-lambda-java-libs/pull/263)) + - `AppSyncLambdaAuthorizerEvent` + - `AppSyncLambdaAuthorizerResponse` +- Added support for RabbitMQ Event ([#256](https://github.com/aws/aws-lambda-java-libs/pull/256)) + - `RabbitMQEvent` +- Added missing `version` field to `APIGatewayProxyRequestEvent` ([#258](https://github.com/aws/aws-lambda-java-libs/pull/258)) + +### June 2, 2021 +`3.9.0`: +- Added support for Cognito User Pool events ([#175](https://github.com/aws/aws-lambda-java-libs/pull/175)) + - `CognitoUserPoolCreateAuthChallengeEvent` + - `CognitoUserPoolCustomMessageEvent` + - `CognitoUserPoolDefineAuthChallengeEvent` + - `CognitoUserPoolEvent` + - `CognitoUserPoolMigrateUserEvent` + - `CognitoUserPoolPostAuthenticationEvent` + - `CognitoUserPoolPostConfirmationEvent` + - `CognitoUserPoolPreAuthenticationEvent` + - `CognitoUserPoolPreSignUpEvent` + - `CognitoUserPoolPreTokenGenerationEvent` + - `CognitoUserPoolVerifyAuthChallengeResponseEvent` +- Added support for IAM Policy Responses for API Gateway REST APIs ([#213](https://github.com/aws/aws-lambda-java-libs/pull/213)) + - `IamPolicyResponseV1` +- Added default IntelliJ equals, hashCode and toString methods to `APIGatewayV2WebSocketEvent` ([#248](https://github.com/aws/aws-lambda-java-libs/pull/248)) +- Fixed toString method in `KinesisEvent` ([#245](https://github.com/aws/aws-lambda-java-libs/pull/245)) +- Changed `body` field to lowercase in `APIGatewayV2HTTPEvent` ([#236](https://github.com/aws/aws-lambda-java-libs/pull/236)) +- Added `principalOrgId` field to `APIGatewayProxyRequestEvent` ([#247](https://github.com/aws/aws-lambda-java-libs/pull/247)) + +### March 24, 2021 +`3.8.0`: +- Added support for S3 Object Lambda event ([#229](https://github.com/aws/aws-lambda-java-libs/pull/229)) + - `S3ObjectLambdaEvent` +- Added support for IAM Policy response ([#213](https://github.com/aws/aws-lambda-java-libs/pull/213)) + - `IamPolicyResponse` +- Added `bootstrapServers` to `KafkaEvent` ([#216](https://github.com/aws/aws-lambda-java-libs/pull/216)) +- Added `requestId` to `APIGatewayV2HTTPEvent` ([#230](https://github.com/aws/aws-lambda-java-libs/pull/230)) +- Added `multiValueHeaders` to `APIGatewayProxyResponseEvent` ([#228](https://github.com/aws/aws-lambda-java-libs/pull/228)) +- Bumped `jackson-databind` from `2.10.4` to `2.10.5.1` ([#231](https://github.com/aws/aws-lambda-java-libs/pull/231)) + +### December 16, 2020 +`3.7.0`: +- Added support for StreamAnalytics and CustomCheckpointing ([#202](https://github.com/aws/aws-lambda-java-libs/pull/202)) + - `DynamodbTimeWindowEvent` + - `KinesisTimeWindowEvent` + - `StreamsEventResponse` + - `TimeWindowEventResponse` + +### November 06, 2020 +`3.6.0`: +- Added support for Amazon `ActiveMQ` event: ([#185](https://github.com/aws/aws-lambda-java-libs/pull/185)) +- Bumped `junit-jupiter-engine` to version `5.7.0` +- Bumped `lombok` to version `1.18.16` + +### October 28, 2020 +`3.5.0`: +- Added support for S3 Batch events: ([#179](https://github.com/aws/aws-lambda-java-libs/pull/179)) + - `S3BatchEvent` + - `S3BatchResponse` + +### October 07, 2020 +`3.4.0`: +- Added Lambda and IAM authorizers to `APIGatewayV2HTTPEvent` request context ([#167](https://github.com/aws/aws-lambda-java-libs/pull/167)) +- Added support for API Gateway custom authorizer: ([#166](https://github.com/aws/aws-lambda-java-libs/pull/166)) + - `APIGatewayCustomAuthorizerEvent` + - `APIGatewayV2CustomAuthorizerEvent` + - `SimpleIAMPolicyResponse` + +### September 23, 2020 +`3.3.1`: +- Added `multiValueQueryStringParameters` to `ApplicationLoadBalancerRequestEvent` ([#163](https://github.com/aws/aws-lambda-java-libs/pull/163)) + +### September 14, 2020 +`3.3.0`: +- Added support for Secrets Manager Rotation Event ([#130](https://github.com/aws/aws-lambda-java-libs/pull/130)) +- Added support for CloudFormation Custom Resource Event ([#138](https://github.com/aws/aws-lambda-java-libs/pull/138)) +- Added support for Lambda Destination Event ([#139](https://github.com/aws/aws-lambda-java-libs/pull/139)) +- Added support for Amazon Connect Event ([#140](https://github.com/aws/aws-lambda-java-libs/pull/140)) + +### August 11, 2020 +`3.2.0`: +- Added support for Kafka Events ([#154](https://github.com/aws/aws-lambda-java-libs/pull/154)) + +### July 31, 2020 +`3.1.1`: +- Fixed Base64 encoding for ALB and API Gateway HTTP events ([#150](https://github.com/aws/aws-lambda-java-libs/pull/131)) + ### May 20, 2020 `3.1.0`: - Added support for Application Load Balancer Target Events ([#131](https://github.com/aws/aws-lambda-java-libs/pull/131)) @@ -29,49 +176,49 @@ `2.2.9`: - Added field `operationName` to `APIGatewayProxyRequestEvent` ([#126](https://github.com/aws/aws-lambda-java-libs/pull/126)) -### Apr 28, 2020 +### April 28, 2020 `2.2.8`: - Added missing XML namespace declarations to `pom.xml` file ([#97](https://github.com/aws/aws-lambda-java-libs/issues/97)) - Updated `nexusUrl` in `pom.xml` file ([#108](https://github.com/aws/aws-lambda-java-libs/issues/108)) -### Aug 13, 2019 +### August 13, 2019 `2.2.7`: - Added support for APIGatewayV2 (Web Sockets) ([#92](https://github.com/aws/aws-lambda-java-libs/issues/92)) - `APIGatewayV2ProxyRequestEvent` - `APIGatewayV2ProxyResponseEvent` - Fixed typo in `CognitoEvent` javadoc ([#87](https://github.com/aws/aws-lambda-java-libs/issues/87)) -### Mar 11, 2019 +### March 11, 2019 `2.2.6`: - Added field `customData` to `CommitEvent.Record` ([#79](https://github.com/aws/aws-lambda-java-libs/issues/79)) - Added field `isBase64Encoded` to `APIGatewayProxyResponseEvent` ([#48](https://github.com/aws/aws-lambda-java-libs/issues/48)) - Added field `authorizer` to `APIGatewayProxyRequestEvent` ([#77](https://github.com/aws/aws-lambda-java-libs/issues/77)) -### Jan 03, 2019 +### January 03, 2019 `2.2.5`: - Fixed "Paramters" typo in `APIGatewayProxyRequestEvent` and `ConfigEvent` ([#65](https://github.com/aws/aws-lambda-java-libs/issues/65)) -### Nov 14, 2018 +### November 14, 2018 `2.2.4`: - Added default constructor for `S3Event` for easier deserialization -### Nov 05, 2018 +### November 05, 2018 `2.2.3`: - Added support for Multi-Value Headers and Query String Parameters to `APIGatewayProxyRequestEvent` ([#60](https://github.com/aws/aws-lambda-java-libs/issues/60)) -### Jul 02, 2018 +### July 02, 2018 `2.2.2`: - Made `SQSEvent.SQSMessage` default constructor public ([#51](https://github.com/aws/aws-lambda-java-libs/issues/51)) -### Jun 29, 2018 +### June 29, 2018 `2.2.1`: - Made `SQSEvent.SQSMessage` public ([#51](https://github.com/aws/aws-lambda-java-libs/issues/51)) -### Jun 28, 2018 +### June 28, 2018 `2.2.0`: - Added `SQSEvent` -### Mar 09, 2018 +### March 09, 2018 `2.1.0`: - Added Kinesis Analytics events - `KinesisAnalyticsFirehoseInputPreprocessingEvent` @@ -80,16 +227,16 @@ - `KinesisAnalyticsOutputDeliveryResponse` - `KinesisAnalyticsStreamsInputPreprocessingEvent` -### Nov 21, 2017 +### November 21, 2017 `2.0.2`: - Added missing fields to `APIGatewayProxyRequestEvent` ([#46](https://github.com/aws/aws-lambda-java-libs/issues/46)) -### Oct 07, 2017 +### October 07, 2017 `2.0.1`: - Updated KinesisFirehose event schema. - `approximateArrivalTimestamp` is now represented as a millisecond epoch instead of an `org.joda.time.DateTime` object. -### Sep 20, 2017 +### September 20, 2017 `2.0`: - Added the following events: - `APIGatewayProxyRequestEvent` @@ -114,15 +261,15 @@ `1.2.1`: - Bumped AWS SDK versions to `1.10.77` -### Apr 22, 2016 +### April 22, 2016 `1.2.0`: - Added `ConfigEvent` -### Aug 21, 2015 +### August 21, 2015 `1.1.0`: - Added `DynamodbEvent` -### Jun 15, 2015 +### June 15, 2015 `1.0.0`: - Initial support for java in AWS Lambda, includes the following events: - `CognitoEvent` diff --git a/aws-lambda-java-events/pom.xml b/aws-lambda-java-events/pom.xml index 92c42242b..0b69b03e6 100644 --- a/aws-lambda-java-events/pom.xml +++ b/aws-lambda-java-events/pom.xml @@ -1,153 +1,234 @@ - 4.0.0 + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/maven-v4_0_0.xsd"> + 4.0.0 - com.amazonaws - aws-lambda-java-events - 3.1.0 - jar + com.amazonaws + aws-lambda-java-events + 3.16.1-SNAPSHOT + jar - AWS Lambda Java Events Library - - Event interface definitions AWS services supported by AWS Lambda. - - https://aws.amazon.com/lambda/ - - - Apache License, Version 2.0 - https://aws.amazon.com/apache2.0 - repo - - - - https://github.com/aws/aws-lambda-java-libs.git - - - - AWS Lambda team - Amazon Web Services - https://aws.amazon.com/ - - + AWS Lambda Java Events Library + + Event interface definitions AWS services supported by AWS Lambda. + + https://aws.amazon.com/lambda/ + + + Apache License, Version 2.0 + https://aws.amazon.com/apache2.0 + repo + + + + https://github.com/aws/aws-lambda-java-libs.git + scm:git:https://github.com/aws/aws-lambda-java-libs.git + scm:git:https://github.com/aws/aws-lambda-java-libs.git + HEAD + + + + AWS Lambda team + Amazon Web Services + https://aws.amazon.com/ + + - - 1.8 - 1.8 - + + 1.8 + 1.8 + 1.18.22 + UTF-8 + UTF-8 + 2.20.1 + 2.40.1 + 5.12.2 + - - - sonatype-nexus-staging - https://oss.sonatype.org/service/local/staging/deploy/maven2/ - - + + + + org.apache.maven.plugins + maven-toolchains-plugin + 3.2.0 + + + + + [1.8,9) + + + + + + + toolchain + + + + + + org.apache.maven.plugins + maven-release-plugin + 3.1.1 + + aws-lambda-java-events-@{project.version} + true + release + deploy + + + + - - - joda-time - joda-time - 2.6 - + + + sonatype-nexus-staging + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + - - org.junit.jupiter - junit-jupiter-engine - 5.5.2 - test - - - org.projectlombok - lombok - 1.18.12 - provided - - + + + joda-time + joda-time + 2.10.8 + - - - dev - - - - org.apache.maven.plugins - maven-javadoc-plugin - 2.9.1 - - -Xdoclint:none - - - - attach-javadocs - - jar - - - - - - - - - release - - - - org.apache.maven.plugins - maven-source-plugin - 2.2.1 - - - attach-sources - - jar-no-fork - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - 2.9.1 - - -Xdoclint:none - - - - attach-javadocs - - jar - - - - - - org.apache.maven.plugins - maven-gpg-plugin - 1.5 - - - sign-artifacts - verify - - sign - - - - - - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.3 - true - - sonatype-nexus-staging - https://aws.oss.sonatype.org/ - false - - - - - - - + + org.junit.jupiter + junit-jupiter-engine + ${junit-jupiter.version} + test + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + test + + + net.javacrumbs.json-unit + json-unit-assertj + ${json.unit} + test + + + + org.projectlombok + lombok + ${lombok.version} + provided + + + + + + dev + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.9.1 + + -Xdoclint:none + + + + attach-javadocs + + jar + + + + + + + + + release + + + + org.apache.maven.plugins + maven-source-plugin + 3.2.1 + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.9.1 + + -Xdoclint:none + + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.5 + + + sign-artifacts + verify + + sign + + + + + + org.sonatype.central + central-publishing-maven-plugin + 0.8.0 + true + + central + false + + + + org.apache.maven.plugins + maven-resources-plugin + 3.3.1 + + UTF-8 + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + + + org.projectlombok + lombok + ${lombok.version} + + + UTF-8 + + + + + + + \ No newline at end of file diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/APIGatewayCustomAuthorizerEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/APIGatewayCustomAuthorizerEvent.java new file mode 100644 index 000000000..728833195 --- /dev/null +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/APIGatewayCustomAuthorizerEvent.java @@ -0,0 +1,59 @@ +package com.amazonaws.services.lambda.runtime.events; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Map; + +/** + * The API Gateway customer authorizer event object as described - https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-lambda-authorizer.html + * + */ + +@Data +@Builder(setterPrefix = "with") +@NoArgsConstructor +@AllArgsConstructor +public class APIGatewayCustomAuthorizerEvent { + + private String version; + private String type; + private String methodArn; + private String identitySource; + private String authorizationToken; + private String resource; + private String path; + private String httpMethod; + private Map headers; + private Map queryStringParameters; + private Map pathParameters; + private Map stageVariables; + private RequestContext requestContext; + + @Data + @Builder(setterPrefix = "with") + @NoArgsConstructor + @AllArgsConstructor + public static class RequestContext { + private String path; + private String accountId; + private String resourceId; + private String stage; + private String requestId; + private Identity identity; + private String resourcePath; + private String httpMethod; + private String apiId; + } + + @Data + @Builder(setterPrefix = "with") + @NoArgsConstructor + @AllArgsConstructor + public static class Identity { + private String apiKey; + private String sourceIp; + } +} \ No newline at end of file diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/APIGatewayProxyRequestEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/APIGatewayProxyRequestEvent.java index f9c118cae..8ff8ccb8b 100644 --- a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/APIGatewayProxyRequestEvent.java +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/APIGatewayProxyRequestEvent.java @@ -11,6 +11,8 @@ public class APIGatewayProxyRequestEvent implements Serializable, Cloneable { private static final long serialVersionUID = 4189228800688527467L; + private String version; + private String resource; private String path; @@ -64,6 +66,18 @@ public static class ProxyRequestContext implements Serializable, Cloneable { private Map authorizer; + private String extendedRequestId; + + private String requestTime; + + private Long requestTimeEpoch; + + private String domainName; + + private String domainPrefix; + + private String protocol; + /** * default constructor */ @@ -303,6 +317,145 @@ public ProxyRequestContext withOperationName(String operationName) { return this; } + /** + * @return The API Gateway Extended Request Id + */ + public String getExtendedRequestId() { + return extendedRequestId; + } + + /** + * @param extendedRequestId The API Gateway Extended Request Id + */ + public void setExtendedRequestId(String extendedRequestId) { + this.extendedRequestId = extendedRequestId; + } + + /** + * @param extendedRequestId The API Gateway Extended Request Id + * @return ProxyRequestContext object + */ + public ProxyRequestContext withExtendedRequestId(String extendedRequestId) { + this.setExtendedRequestId(extendedRequestId); + return this; + } + + /** + * @return The CLF-formatted request time (dd/MMM/yyyy:HH:mm:ss +-hhmm). + */ + public String getRequestTime() { + return requestTime; + } + + /** + * @param requestTime The CLF-formatted request time (dd/MMM/yyyy:HH:mm:ss +-hhmm). + */ + public void setRequestTime(String requestTime) { + this.requestTime = requestTime; + } + + /** + * @param requestTime The CLF-formatted request time (dd/MMM/yyyy:HH:mm:ss +-hhmm). + * @return ProxyRequestContext object + */ + public ProxyRequestContext withRequestTime(String requestTime) { + this.setRequestTime(requestTime); + return this; + } + + /** + * @return The Epoch-formatted request time (in millis) + */ + public Long getRequestTimeEpoch() { + return requestTimeEpoch; + } + + /** + * @param requestTimeEpoch The Epoch-formatted request time (in millis) + */ + public void setRequestTimeEpoch(Long requestTimeEpoch) { + this.requestTimeEpoch = requestTimeEpoch; + } + + /** + * @param requestTimeEpoch The Epoch-formatted request time (in millis) + * @return ProxyRequestContext object + */ + public ProxyRequestContext withRequestTimeEpoch(Long requestTimeEpoch) { + this.setRequestTimeEpoch(requestTimeEpoch); + return this; + } + + /** + * @return The full domain name used to invoke the API. This should be the same as the incoming Host header. + */ + public String getDomainName() { + return domainName; + } + + /** + * @param domainName The full domain name used to invoke the API. + * This should be the same as the incoming Host header. + */ + public void setDomainName(String domainName) { + this.domainName = domainName; + } + + /** + * @param domainName The full domain name used to invoke the API. + * This should be the same as the incoming Host header. + * @return ProxyRequestContext object + */ + public ProxyRequestContext withDomainName(String domainName) { + this.setDomainName(domainName); + return this; + } + + /** + * @return The first label of the domainName. This is often used as a caller/customer identifier. + */ + public String getDomainPrefix() { + return domainPrefix; + } + + /** + * @param domainPrefix The first label of the domainName. This is often used as a caller/customer identifier. + */ + public void setDomainPrefix(String domainPrefix) { + this.domainPrefix = domainPrefix; + } + + /** + * @param domainPrefix The first label of the domainName. This is often used as a caller/customer identifier. + * @return + */ + public ProxyRequestContext withDomainPrefix(String domainPrefix) { + this.setDomainPrefix(domainPrefix); + return this; + } + /** + * @return The request protocol, for example, HTTP/1.1. + */ + public String getProtocol() { + return protocol; + } + + /** + * @param protocol The request protocol, for example, HTTP/1.1. + */ + public void setProtocol(String protocol) { + this.protocol = protocol; + } + + /** + * @param protocol The request protocol, for example, HTTP/1.1. + * @return ProxyRequestContext object + */ + public ProxyRequestContext withProtocol(String protocol) { + this.setProtocol(protocol); + return this; + } + /** * Returns a string representation of this object; useful for testing and debugging. * @@ -336,6 +489,18 @@ public String toString() { sb.append("authorizer: ").append(getAuthorizer().toString()); if (getOperationName() != null) sb.append("operationName: ").append(getOperationName().toString()); + if (getExtendedRequestId() != null) + sb.append("extendedRequestId: ").append(getExtendedRequestId()).append(","); + if (getRequestTime() != null) + sb.append("requestTime: ").append(getRequestTime()).append(","); + if (getProtocol() != null) + sb.append("protocol: ").append(getProtocol()).append(","); + if (getRequestTimeEpoch() != null) + sb.append("requestTimeEpoch: ").append(getRequestTimeEpoch()).append(","); + if (getDomainPrefix() != null) + sb.append("domainPrefix: ").append(getDomainPrefix()).append(","); + if (getDomainName() != null) + sb.append("domainName: ").append(getDomainName()); sb.append("}"); return sb.toString(); } @@ -394,6 +559,30 @@ public boolean equals(Object obj) { return false; if (other.getOperationName() != null && !other.getOperationName().equals(this.getOperationName())) return false; + if (other.getExtendedRequestId() == null ^ this.getExtendedRequestId() == null) + return false; + if (other.getExtendedRequestId() != null && other.getExtendedRequestId().equals(this.getExtendedRequestId()) == false) + return false; + if (other.getRequestTime() == null ^ this.getRequestTime() == null) + return false; + if (other.getRequestTime() != null && other.getRequestTime().equals(this.getRequestTime()) == false) + return false; + if (other.getRequestTimeEpoch() == null ^ this.getRequestTimeEpoch() == null) + return false; + if (other.getRequestTimeEpoch() != null && other.getRequestTimeEpoch().equals(this.getRequestTimeEpoch()) == false) + return false; + if (other.getDomainName() == null ^ this.getDomainName() == null) + return false; + if (other.getDomainName() != null && other.getDomainName().equals(this.getDomainName()) == false) + return false; + if (other.getDomainPrefix() == null ^ this.getDomainPrefix() == null) + return false; + if (other.getDomainPrefix() != null && other.getDomainPrefix().equals(this.getDomainPrefix()) == false) + return false; + if (other.getProtocol() == null ^ this.getProtocol() == null) + return false; + if (other.getProtocol() != null && other.getProtocol().equals(this.getProtocol()) == false) + return false; return true; } @@ -413,6 +602,12 @@ public int hashCode() { hashCode = prime * hashCode + ((getPath() == null) ? 0 : getPath().hashCode()); hashCode = prime * hashCode + ((getAuthorizer() == null) ? 0 : getAuthorizer().hashCode()); hashCode = prime * hashCode + ((getOperationName() == null) ? 0: getOperationName().hashCode()); + hashCode = prime * hashCode + ((getExtendedRequestId() == null) ? 0 : getExtendedRequestId().hashCode()); + hashCode = prime * hashCode + ((getRequestTime() == null) ? 0 : getRequestTime().hashCode()); + hashCode = prime * hashCode + ((getRequestTimeEpoch() == null) ? 0 : getRequestTimeEpoch().hashCode()); + hashCode = prime * hashCode + ((getDomainName() == null) ? 0 : getDomainName().hashCode()); + hashCode = prime * hashCode + ((getDomainPrefix() == null) ? 0 : getDomainPrefix().hashCode()); + hashCode = prime * hashCode + ((getProtocol() == null) ? 0 : getProtocol().hashCode()); return hashCode; } @@ -440,6 +635,8 @@ public static class RequestIdentity implements Serializable, Cloneable { private String apiKey; + private String principalOrgId; + private String sourceIp; private String cognitoAuthenticationType; @@ -574,6 +771,29 @@ public RequestIdentity withApiKey(String apiKey) { return this; } + /** + * @return the principal org Id + */ + public String getPrincipalOrgId() { + return principalOrgId; + } + + /** + * @param principalOrgId the principal org Id + */ + public void setPrincipalOrgId(String principalOrgId) { + this.principalOrgId = principalOrgId; + } + + /** + * @param principalOrgId the principal org Id + * @return RequestIdentity object + */ + public RequestIdentity withPrincipalOrgId(String principalOrgId) { + this.setPrincipalOrgId(principalOrgId); + return this; + } + /** * @return source ip address */ @@ -756,6 +976,8 @@ public String toString() { sb.append("caller: ").append(getCaller()).append(","); if (getApiKey() != null) sb.append("apiKey: ").append(getApiKey()).append(","); + if (getPrincipalOrgId() != null) + sb.append("principalOrgId: ").append(getPrincipalOrgId()).append(","); if (getSourceIp() != null) sb.append("sourceIp: ").append(getSourceIp()).append(","); if (getCognitoAuthenticationType() != null) @@ -804,6 +1026,10 @@ public boolean equals(Object obj) { return false; if (other.getApiKey() != null && other.getApiKey().equals(this.getApiKey()) == false) return false; + if (other.getPrincipalOrgId() == null ^ this.getPrincipalOrgId() == null) + return false; + if (other.getPrincipalOrgId() != null && other.getPrincipalOrgId().equals(this.getPrincipalOrgId()) == false) + return false; if (other.getSourceIp() == null ^ this.getSourceIp() == null) return false; if (other.getSourceIp() != null && other.getSourceIp().equals(this.getSourceIp()) == false) @@ -846,6 +1072,7 @@ public int hashCode() { hashCode = prime * hashCode + ((getCognitoIdentityId() == null) ? 0 : getCognitoIdentityId().hashCode()); hashCode = prime * hashCode + ((getCaller() == null) ? 0 : getCaller().hashCode()); hashCode = prime * hashCode + ((getApiKey() == null) ? 0 : getApiKey().hashCode()); + hashCode = prime * hashCode + ((getPrincipalOrgId() == null) ? 0 : getPrincipalOrgId().hashCode()); hashCode = prime * hashCode + ((getSourceIp() == null) ? 0 : getSourceIp().hashCode()); hashCode = prime * hashCode + ((getCognitoAuthenticationType() == null) ? 0 : getCognitoAuthenticationType().hashCode()); hashCode = prime * hashCode + ((getCognitoAuthenticationProvider() == null) ? 0 : getCognitoAuthenticationProvider().hashCode()); @@ -871,6 +1098,29 @@ public RequestIdentity clone() { */ public APIGatewayProxyRequestEvent() {} + /** + * @return The payload format version + */ + public String getVersion() { + return version; + } + + /** + * @param version The payload format version + */ + public void setVersion(String version) { + this.version = version; + } + + /** + * @param version The payload format version + * @return + */ + public APIGatewayProxyRequestEvent withVersion(String version) { + this.setVersion(version); + return this; + } + /** * @return The resource path defined in API Gateway */ @@ -1174,6 +1424,8 @@ public APIGatewayProxyRequestEvent withIsBase64Encoded(Boolean isBase64Encoded) public String toString() { StringBuilder sb = new StringBuilder(); sb.append("{"); + if (getVersion() != null) + sb.append("version: ").append(getVersion()).append(","); if (getResource() != null) sb.append("resource: ").append(getResource()).append(","); if (getPath() != null) @@ -1212,6 +1464,10 @@ public boolean equals(Object obj) { if (obj instanceof APIGatewayProxyRequestEvent == false) return false; APIGatewayProxyRequestEvent other = (APIGatewayProxyRequestEvent) obj; + if (other.getVersion() == null ^ this.getVersion() == null) + return false; + if (other.getVersion() != null && other.getVersion().equals(this.getVersion()) == false) + return false; if (other.getResource() == null ^ this.getResource() == null) return false; if (other.getResource() != null && other.getResource().equals(this.getResource()) == false) @@ -1268,6 +1524,7 @@ public int hashCode() { final int prime = 31; int hashCode = 1; + hashCode = prime * hashCode + ((getVersion() == null) ? 0 : getVersion().hashCode()); hashCode = prime * hashCode + ((getResource() == null) ? 0 : getResource().hashCode()); hashCode = prime * hashCode + ((getPath() == null) ? 0 : getPath().hashCode()); hashCode = prime * hashCode + ((getHttpMethod() == null) ? 0 : getHttpMethod().hashCode()); diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/APIGatewayProxyResponseEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/APIGatewayProxyResponseEvent.java index da6220bdd..fb1f87c35 100644 --- a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/APIGatewayProxyResponseEvent.java +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/APIGatewayProxyResponseEvent.java @@ -1,6 +1,7 @@ package com.amazonaws.services.lambda.runtime.events; import java.io.Serializable; +import java.util.List; import java.util.Map; /** @@ -13,6 +14,8 @@ public class APIGatewayProxyResponseEvent implements Serializable, Cloneable { private Integer statusCode; private Map headers; + + private Map> multiValueHeaders; private String body; @@ -69,6 +72,30 @@ public APIGatewayProxyResponseEvent withHeaders(Map headers) { return this; } + /** + * @return the Http multi value headers to return in the response + */ + public Map> getMultiValueHeaders() { + return multiValueHeaders; + } + + /** + * @param multiValueHeaders the Http multi value headers to return in the response + */ + public void setMultiValueHeaders(Map> multiValueHeaders) { + this.multiValueHeaders = multiValueHeaders; + } + + /** + * + * @param multiValueHeaders the Http multi value headers to return in the response + * @return APIGatewayProxyResponseEvent + */ + public APIGatewayProxyResponseEvent withMultiValueHeaders(Map> multiValueHeaders) { + this.setMultiValueHeaders(multiValueHeaders); + return this; + } + /** * @return The response body */ @@ -130,6 +157,8 @@ public String toString() { sb.append("statusCode: ").append(getStatusCode()).append(","); if (getHeaders() != null) sb.append("headers: ").append(getHeaders().toString()).append(","); + if (getMultiValueHeaders() != null) + sb.append("multiValueHeaders: ").append(getMultiValueHeaders().toString()).append(","); if (getBody() != null) sb.append("body: ").append(getBody()); sb.append("}"); @@ -154,6 +183,10 @@ public boolean equals(Object obj) { return false; if (other.getHeaders() != null && other.getHeaders().equals(this.getHeaders()) == false) return false; + if (other.getMultiValueHeaders() == null ^ this.getMultiValueHeaders() == null) + return false; + if (other.getMultiValueHeaders() != null && other.getMultiValueHeaders().equals(this.getMultiValueHeaders()) == false) + return false; if (other.getBody() == null ^ this.getBody() == null) return false; if (other.getBody() != null && other.getBody().equals(this.getBody()) == false) @@ -168,6 +201,7 @@ public int hashCode() { hashCode = prime * hashCode + ((getStatusCode() == null) ? 0 : getStatusCode().hashCode()); hashCode = prime * hashCode + ((getHeaders() == null) ? 0 : getHeaders().hashCode()); + hashCode = prime * hashCode + ((getMultiValueHeaders() == null) ? 0 : getMultiValueHeaders().hashCode()); hashCode = prime * hashCode + ((getBody() == null) ? 0 : getBody().hashCode()); return hashCode; } diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/APIGatewayV2CustomAuthorizerEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/APIGatewayV2CustomAuthorizerEvent.java new file mode 100644 index 000000000..6abfe0513 --- /dev/null +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/APIGatewayV2CustomAuthorizerEvent.java @@ -0,0 +1,80 @@ +package com.amazonaws.services.lambda.runtime.events; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.joda.time.DateTime; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; + +import java.time.Instant; +import java.util.List; +import java.util.Map; + +/** + * The V2 API Gateway customer authorizer event object as described - https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-lambda-authorizer.html + * + */ + +@Data +@Builder(setterPrefix = "with") +@NoArgsConstructor +@AllArgsConstructor +public class APIGatewayV2CustomAuthorizerEvent { + + private String version; + private String type; + private String routeArn; + private List identitySource; + private String routeKey; + private String rawPath; + private String rawQueryString; + private List cookies; + private Map headers; + private Map queryStringParameters; + private RequestContext requestContext; + private Map pathParameters; + private Map stageVariables; + + @Data + @Builder(setterPrefix = "with") + @NoArgsConstructor + @AllArgsConstructor + public static class RequestContext { + + private static DateTimeFormatter fmt = DateTimeFormat.forPattern("dd/MMM/yyyy:HH:mm:ss Z"); + + private String accountId; + private String apiId; + private String domainName; + private String domainPrefix; + private Http http; + private String requestId; + private String routeKey; + private String stage; + private String time; + private long timeEpoch; + + public Instant getTimeEpoch() { + return Instant.ofEpochMilli(timeEpoch); + } + + public DateTime getTime() { + return fmt.parseDateTime(time); + } + } + + @AllArgsConstructor + @Builder(setterPrefix = "with") + @Data + @NoArgsConstructor + public static class Http { + + private String method; + private String path; + private String protocol; + private String sourceIp; + private String userAgent; + } +} \ No newline at end of file diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/APIGatewayV2HTTPEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/APIGatewayV2HTTPEvent.java index a45cbf81d..3393822ec 100644 --- a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/APIGatewayV2HTTPEvent.java +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/APIGatewayV2HTTPEvent.java @@ -25,6 +25,9 @@ @Builder(setterPrefix = "with") @Data @NoArgsConstructor +/** + * API Gateway v2 event: https://docs.aws.amazon.com/lambda/latest/dg/services-apigateway.html + */ public class APIGatewayV2HTTPEvent { private String version; private String routeKey; @@ -35,7 +38,7 @@ public class APIGatewayV2HTTPEvent { private Map queryStringParameters; private Map pathParameters; private Map stageVariables; - private String Body; + private String body; private boolean isBase64Encoded; private RequestContext requestContext; @@ -54,6 +57,7 @@ public static class RequestContext { private long timeEpoch; private Http http; private Authorizer authorizer; + private String requestId; @AllArgsConstructor @Builder(setterPrefix = "with") @@ -61,6 +65,8 @@ public static class RequestContext { @NoArgsConstructor public static class Authorizer { private JWT jwt; + private Map lambda; + private IAM iam; @AllArgsConstructor @Builder(setterPrefix = "with") @@ -83,5 +89,29 @@ public static class Http { private String sourceIp; private String userAgent; } + + @AllArgsConstructor + @Builder(setterPrefix = "with") + @Data + @NoArgsConstructor + public static class IAM { + private String accessKey; + private String accountId; + private String callerId; + private CognitoIdentity cognitoIdentity; + private String principalOrgId; + private String userArn; + private String userId; + } + + @AllArgsConstructor + @Builder(setterPrefix = "with") + @Data + @NoArgsConstructor + public static class CognitoIdentity { + private List amr; + private String identityId; + private String identityPoolId; + } } } diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/APIGatewayV2WebSocketEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/APIGatewayV2WebSocketEvent.java index 43a9966b8..cb6ffa991 100644 --- a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/APIGatewayV2WebSocketEvent.java +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/APIGatewayV2WebSocketEvent.java @@ -663,64 +663,65 @@ public void setIsBase64Encoded(boolean isBase64Encoded) { } @Override - public int hashCode() { - int hash = 7; - - hash = 43 * hash + Objects.hashCode(this.requestContext); - hash = 43 * hash + Objects.hashCode(this.body); - hash = 43 * hash + (this.isBase64Encoded ? 1 : 0); - - return hash; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - - if (obj == null) { + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + APIGatewayV2WebSocketEvent that = (APIGatewayV2WebSocketEvent) o; + + if (isBase64Encoded != that.isBase64Encoded) return false; + if (resource != null ? !resource.equals(that.resource) : that.resource != null) return false; + if (path != null ? !path.equals(that.path) : that.path != null) return false; + if (httpMethod != null ? !httpMethod.equals(that.httpMethod) : that.httpMethod != null) return false; + if (headers != null ? !headers.equals(that.headers) : that.headers != null) return false; + if (multiValueHeaders != null ? !multiValueHeaders.equals(that.multiValueHeaders) : that.multiValueHeaders != null) return false; - } - - if (getClass() != obj.getClass()) { + if (queryStringParameters != null ? !queryStringParameters.equals(that.queryStringParameters) : that.queryStringParameters != null) return false; - } - - final APIGatewayV2WebSocketEvent other = (APIGatewayV2WebSocketEvent) obj; - - if (this.isBase64Encoded != other.isBase64Encoded) { + if (multiValueQueryStringParameters != null ? !multiValueQueryStringParameters.equals(that.multiValueQueryStringParameters) : that.multiValueQueryStringParameters != null) return false; - } - - if (!Objects.equals(this.body, other.body)) { + if (pathParameters != null ? !pathParameters.equals(that.pathParameters) : that.pathParameters != null) return false; - } - - if (!Objects.equals(this.requestContext, other.requestContext)) { + if (stageVariables != null ? !stageVariables.equals(that.stageVariables) : that.stageVariables != null) return false; - } - return true; + if (requestContext != null ? !requestContext.equals(that.requestContext) : that.requestContext != null) + return false; + return body != null ? body.equals(that.body) : that.body == null; } @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("{"); - - if (requestContext != null) { - sb.append("requestContext: ").append(requestContext).append(","); - } - - if (body != null) { - sb.append("body: ").append(body).append(","); - } - - sb.append("isBase64Encoded: ").append(isBase64Encoded).append(","); - - sb.append("}"); + public int hashCode() { + int result = resource != null ? resource.hashCode() : 0; + result = 31 * result + (path != null ? path.hashCode() : 0); + result = 31 * result + (httpMethod != null ? httpMethod.hashCode() : 0); + result = 31 * result + (headers != null ? headers.hashCode() : 0); + result = 31 * result + (multiValueHeaders != null ? multiValueHeaders.hashCode() : 0); + result = 31 * result + (queryStringParameters != null ? queryStringParameters.hashCode() : 0); + result = 31 * result + (multiValueQueryStringParameters != null ? multiValueQueryStringParameters.hashCode() : 0); + result = 31 * result + (pathParameters != null ? pathParameters.hashCode() : 0); + result = 31 * result + (stageVariables != null ? stageVariables.hashCode() : 0); + result = 31 * result + (requestContext != null ? requestContext.hashCode() : 0); + result = 31 * result + (body != null ? body.hashCode() : 0); + result = 31 * result + (isBase64Encoded ? 1 : 0); + return result; + } + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("APIGatewayV2WebSocketEvent{"); + sb.append("resource='").append(resource).append('\''); + sb.append(", path='").append(path).append('\''); + sb.append(", httpMethod='").append(httpMethod).append('\''); + sb.append(", headers=").append(headers); + sb.append(", multiValueHeaders=").append(multiValueHeaders); + sb.append(", queryStringParameters=").append(queryStringParameters); + sb.append(", multiValueQueryStringParameters=").append(multiValueQueryStringParameters); + sb.append(", pathParameters=").append(pathParameters); + sb.append(", stageVariables=").append(stageVariables); + sb.append(", requestContext=").append(requestContext); + sb.append(", body='").append(body).append('\''); + sb.append(", isBase64Encoded=").append(isBase64Encoded); + sb.append('}'); return sb.toString(); } - } diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/ActiveMQEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/ActiveMQEvent.java new file mode 100644 index 000000000..e896a223e --- /dev/null +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/ActiveMQEvent.java @@ -0,0 +1,67 @@ +/* + * Copyright 2015-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with + * the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.lambda.runtime.events; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import java.util.List; +import java.util.Map; + +/** +* Represents an Active MQ event sent to Lambda +* Onboarding Amazon MQ as event source to Lambda +*/ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder(setterPrefix = "with") +public class ActiveMQEvent { + private String eventSource; + private String eventSourceArn; + private List messages; + + @Data + @NoArgsConstructor + @AllArgsConstructor + @Builder(setterPrefix = "with") + public static class ActiveMQMessage { + private String messageID; + private String messageType; + private long timestamp; + private int deliveryMode; + private String correlationID; + private String replyTo; + private Destination destination; + private boolean redelivered; + private String type; + private long expiration; + private int priority; + /** Message data sent to Active MQ broker encooded in Base 64 **/ + private String data; + private long brokerInTime; + private long brokerOutTime; + private Map properties; + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + @Builder(setterPrefix = "with") + public static class Destination { + /** Queue Name **/ + private String physicalName; + } +} diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/AppSyncLambdaAuthorizerEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/AppSyncLambdaAuthorizerEvent.java new file mode 100644 index 000000000..0bb6c8b06 --- /dev/null +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/AppSyncLambdaAuthorizerEvent.java @@ -0,0 +1,49 @@ +/* + * Copyright 2015-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with + * the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.lambda.runtime.events; + +import java.util.Map; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Class that represents the input to an AppSync Lambda authorizer invocation. + */ +@Data +@Builder(setterPrefix = "with") +@NoArgsConstructor +@AllArgsConstructor +public class AppSyncLambdaAuthorizerEvent { + + private RequestContext requestContext; + private String authorizationToken; + private Map requestHeaders; + + @Data + @Builder(setterPrefix = "with") + @NoArgsConstructor + @AllArgsConstructor + public static class RequestContext { + + private String apiId; + private String accountId; + private String requestId; + private String queryDocument; + private String operationName; + private Map variables; + } + +} diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/AppSyncLambdaAuthorizerResponse.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/AppSyncLambdaAuthorizerResponse.java new file mode 100644 index 000000000..4eec5aa3d --- /dev/null +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/AppSyncLambdaAuthorizerResponse.java @@ -0,0 +1,36 @@ +/* + * Copyright 2015-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with + * the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.lambda.runtime.events; + +import java.util.Map; +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Class that represents the output from an AppSync Lambda authorizer invocation. + */ +@Data +@Builder(setterPrefix = "with") +@NoArgsConstructor +@AllArgsConstructor +public class AppSyncLambdaAuthorizerResponse { + + private boolean isAuthorized; + private Map resolverContext; + private List deniedFields; + private int ttlOverride; +} \ No newline at end of file diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/ApplicationLoadBalancerRequestEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/ApplicationLoadBalancerRequestEvent.java index 29d01f787..e7b33117e 100644 --- a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/ApplicationLoadBalancerRequestEvent.java +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/ApplicationLoadBalancerRequestEvent.java @@ -39,6 +39,7 @@ public static class RequestContext implements Serializable, Cloneable { private String httpMethod; private String path; private Map queryStringParameters; + private Map> multiValueQueryStringParameters; private Map headers; private Map> multiValueHeaders; private String body; diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CloudFormationCustomResourceEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CloudFormationCustomResourceEvent.java new file mode 100644 index 000000000..37c00a903 --- /dev/null +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CloudFormationCustomResourceEvent.java @@ -0,0 +1,50 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with + * the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.lambda.runtime.events; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.util.Map; + +/** + * Class to represent the custom resource request event from CloudFormation. + * + * CloudFormation invokes your Lambda function asynchronously with this event and includes a callback URL. The function + * is responsible for returning a response to the callback URL that indicates success or failure. + * + * @see Using AWS Lambda with AWS CloudFormation + * + * @author msailes + */ + +@Data +@Builder(setterPrefix = "with") +@NoArgsConstructor +@AllArgsConstructor +public class CloudFormationCustomResourceEvent implements Serializable, Cloneable { + + private String requestType; + private String serviceToken; + private String responseUrl; + private String stackId; + private String requestId; + private String logicalResourceId; + private String physicalResourceId; + private String resourceType; + private Map resourceProperties; + private Map oldResourceProperties; +} diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CloudWatchCompositeAlarmEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CloudWatchCompositeAlarmEvent.java new file mode 100644 index 000000000..d4090b55b --- /dev/null +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CloudWatchCompositeAlarmEvent.java @@ -0,0 +1,70 @@ +package com.amazonaws.services.lambda.runtime.events; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Represents an CloudWatch Composite Alarm event. This event occurs when a composite alarm is triggered. + * + * @see Using Amazon CloudWatch alarms + */ +@Data +@Builder(setterPrefix = "with") +@NoArgsConstructor +@AllArgsConstructor +public class CloudWatchCompositeAlarmEvent { + private String source; + private String alarmArn; + private String accountId; + private String time; + private String region; + private AlarmData alarmData; + + @Data + @Builder(setterPrefix = "with") + @NoArgsConstructor + @AllArgsConstructor + public static class AlarmData { + private String alarmName; + private State state; + private PreviousState previousState; + private Configuration configuration; + } + + @Data + @Builder(setterPrefix = "with") + @NoArgsConstructor + @AllArgsConstructor + public static class State { + private String value; + private String reason; + private String reasonData; + private String timestamp; + } + + @Data + @Builder(setterPrefix = "with") + @NoArgsConstructor + @AllArgsConstructor + public static class PreviousState { + private String value; + private String reason; + private String reasonData; + private String timestamp; + private String actionsSuppressedBy; + private String actionsSuppressedReason; + } + + @Data + @Builder(setterPrefix = "with") + @NoArgsConstructor + @AllArgsConstructor + public static class Configuration { + private String alarmRule; + private String actionsSuppressor; + private Integer actionsSuppressorWaitPeriod; + private Integer actionsSuppressorExtensionPeriod; + } +} diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CloudWatchMetricAlarmEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CloudWatchMetricAlarmEvent.java new file mode 100644 index 000000000..2b5f503c3 --- /dev/null +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CloudWatchMetricAlarmEvent.java @@ -0,0 +1,99 @@ +package com.amazonaws.services.lambda.runtime.events; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +/** + * Represents an CloudWatch Metric Alarm event. This event occurs when a metric alarm is triggered. + * + * @see Using Amazon CloudWatch alarms + */ +@Data +@Builder(setterPrefix = "with") +@NoArgsConstructor +@AllArgsConstructor +public class CloudWatchMetricAlarmEvent { + private String source; + private String alarmArn; + private String accountId; + private String time; + private String region; + private AlarmData alarmData; + + @Data + @Builder(setterPrefix = "with") + @NoArgsConstructor + @AllArgsConstructor + public static class AlarmData { + private String alarmName; + private State state; + private PreviousState previousState; + private Configuration configuration; + } + + @Data + @Builder(setterPrefix = "with") + @NoArgsConstructor + @AllArgsConstructor + public static class State { + private String value; + private String reason; + private String timestamp; + } + + @Data + @Builder(setterPrefix = "with") + @NoArgsConstructor + @AllArgsConstructor + public static class PreviousState { + private String value; + private String reason; + private String reasonData; + private String timestamp; + } + + @Data + @Builder(setterPrefix = "with") + @NoArgsConstructor + @AllArgsConstructor + public static class Configuration { + private String description; + private List metrics; + } + + @Data + @Builder(setterPrefix = "with") + @NoArgsConstructor + @AllArgsConstructor + public static class Metric { + private String id; + private MetricStat metricStat; + private Boolean returnData; + } + + @Data + @Builder(setterPrefix = "with") + @NoArgsConstructor + @AllArgsConstructor + public static class MetricStat { + private MetricDetail metric; + private Integer period; + private String stat; + private String unit; + } + + @Data + @Builder(setterPrefix = "with") + @NoArgsConstructor + @AllArgsConstructor + public static class MetricDetail { + private String namespace; + private String name; + private Map dimensions; + } +} diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolCreateAuthChallengeEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolCreateAuthChallengeEvent.java new file mode 100644 index 000000000..6074ca9b5 --- /dev/null +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolCreateAuthChallengeEvent.java @@ -0,0 +1,124 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with + * the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.lambda.runtime.events; + +import lombok.*; + +import java.util.Map; + +/** + * Represent the class for the Cognito User Pool Create Auth Challenge Lambda Trigger + * + * See Create Auth Challenge Lambda Trigger + * + * @author jvdl + */ +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@ToString(callSuper = true) +public class CognitoUserPoolCreateAuthChallengeEvent extends CognitoUserPoolEvent { + + /** + * The request from the Amazon Cognito service. + */ + private Request request; + + /** + * The response from your Lambda trigger. + */ + private Response response; + + @Builder(setterPrefix = "with") + public CognitoUserPoolCreateAuthChallengeEvent( + String version, + String triggerSource, + String region, + String userPoolId, + String userName, + CallerContext callerContext, + Request request, + Response response) { + super(version, triggerSource, region, userPoolId, userName, callerContext); + this.request = request; + this.response = response; + } + + @Data + @EqualsAndHashCode(callSuper = true) + @NoArgsConstructor + @ToString(callSuper = true) + public static class Request extends CognitoUserPoolEvent.Request { + /** + * One or more key-value pairs that you can provide as custom input to the Lambda function that you specify for the create auth challenge trigger. + */ + private Map clientMetadata; + /** + * The name of the new challenge. + */ + private String challengeName; + private ChallengeResult[] session; + /** + * This boolean is populated when PreventUserExistenceErrors is set to ENABLED for your User Pool client. + */ + private boolean userNotFound; + + @Builder(setterPrefix = "with") + public Request(Map userAttributes, Map clientMetadata, String challengeName, ChallengeResult[] session, boolean userNotFound) { + super(userAttributes); + this.clientMetadata = clientMetadata; + this.session = session; + this.userNotFound = userNotFound; + this.challengeName = challengeName; + } + } + + @AllArgsConstructor + @Builder(setterPrefix = "with") + @Data + @NoArgsConstructor + public static class ChallengeResult { + /** + * The challenge type. One of: "CUSTOM_CHALLENGE", "PASSWORD_VERIFIER", "SMS_MFA", "DEVICE_SRP_AUTH", "DEVICE_PASSWORD_VERIFIER", or "ADMIN_NO_SRP_AUTH". + */ + private String challengeName; + /** + * Set to true if the user successfully completed the challenge, or false otherwise. + */ + private boolean challengeResult; + /** + * Your name for the custom challenge. Used only if challengeName is CUSTOM_CHALLENGE. + */ + private String challengeMetadata; + } + + @AllArgsConstructor + @Builder(setterPrefix = "with") + @Data + @NoArgsConstructor + public static class Response { + /** + * One or more key-value pairs for the client app to use in the challenge to be presented to the user. + * Contains the question that is presented to the user. + */ + private Map publicChallengeParameters; + /** + * Contains the valid answers for the question in publicChallengeParameters + */ + private Map privateChallengeParameters; + /** + * Your name for the custom challenge, if this is a custom challenge. + */ + private String challengeMetadata; + } +} diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolCustomMessageEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolCustomMessageEvent.java new file mode 100644 index 000000000..403f85393 --- /dev/null +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolCustomMessageEvent.java @@ -0,0 +1,101 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with + * the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.lambda.runtime.events; + +import lombok.*; + +import java.util.Map; + +/** + * Represent the class for the Cognito User Pool Custom Message Lambda Trigger + * + * See Custom Message Lambda Trigger + * + * @author jvdl + */ +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@ToString(callSuper = true) +public class CognitoUserPoolCustomMessageEvent extends CognitoUserPoolEvent { + /** + * The request from the Amazon Cognito service. + */ + private Request request; + + /** + * The response from your Lambda trigger. + */ + private Response response; + + @Builder(setterPrefix = "with") + public CognitoUserPoolCustomMessageEvent( + String version, + String triggerSource, + String region, + String userPoolId, + String userName, + CallerContext callerContext, + Request request, + Response response) { + super(version, triggerSource, region, userPoolId, userName, callerContext); + this.request = request; + this.response = response; + } + + @Data + @EqualsAndHashCode(callSuper = true) + @NoArgsConstructor + @ToString(callSuper = true) + public static class Request extends CognitoUserPoolEvent.Request { + /** + * One or more key-value pairs that you can provide as custom input to the Lambda function that you specify for the custom message trigger. + */ + private Map clientMetadata; + /** + * A string for you to use as the placeholder for the verification code in the custom message. + */ + private String codeParameter; + /** + * The username parameter. It is a required request parameter for the admin create user flow. + */ + private String usernameParameter; + + @Builder(setterPrefix = "with") + public Request(Map userAttributes, Map clientMetadata, String codeParameter, String usernameParameter) { + super(userAttributes); + this.clientMetadata = clientMetadata; + this.codeParameter = codeParameter; + this.usernameParameter = usernameParameter; + } + } + + @Data + @AllArgsConstructor + @Builder(setterPrefix = "with") + @NoArgsConstructor + public static class Response { + /** + * The custom SMS message to be sent to your users. Must include the codeParameter value received in the request. + */ + private String smsMessage; + /** + * The custom email message to be sent to your users. Must include the codeParameter value received in the request. + */ + private String emailMessage; + /** + * The subject line for the custom message. + */ + private String emailSubject; + } +} diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolDefineAuthChallengeEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolDefineAuthChallengeEvent.java new file mode 100644 index 000000000..8577c9f7a --- /dev/null +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolDefineAuthChallengeEvent.java @@ -0,0 +1,123 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with + * the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.lambda.runtime.events; + +import lombok.*; + +import java.util.Map; + +/** + * Represent the class for the Cognito User Pool Define Auth Challenge Lambda Trigger + * + * See Define Auth Challenge Lambda Trigger + * + * @author jvdl + */ +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@ToString(callSuper = true) +public class CognitoUserPoolDefineAuthChallengeEvent extends CognitoUserPoolEvent { + + /** + * The request from the Amazon Cognito service. + */ + private Request request; + + /** + * The response from your Lambda trigger. + */ + private Response response; + + @Builder(setterPrefix = "with") + public CognitoUserPoolDefineAuthChallengeEvent( + String version, + String triggerSource, + String region, + String userPoolId, + String userName, + CallerContext callerContext, + Request request, + Response response) { + super(version, triggerSource, region, userPoolId, userName, callerContext); + this.request = request; + this.response = response; + } + + @Data + @EqualsAndHashCode(callSuper = true) + @NoArgsConstructor + @ToString(callSuper = true) + public static class Request extends CognitoUserPoolEvent.Request { + /** + * One or more key-value pairs that you can provide as custom input to the Lambda function that you specify for the define auth challenge trigger. + */ + private Map clientMetadata; + + private ChallengeResult[] session; + + /** + * A Boolean that is populated when PreventUserExistenceErrors is set to ENABLED for your user pool client. + * A value of true means that the user id (user name, email address, etc.) did not match any existing users. + */ + private boolean userNotFound; + + @Builder(setterPrefix = "with") + public Request(Map userAttributes, Map clientMetadata, ChallengeResult[] session, boolean userNotFound) { + super(userAttributes); + this.clientMetadata = clientMetadata; + this.session = session; + this.userNotFound = userNotFound; + } + } + + @Data + @AllArgsConstructor + @Builder(setterPrefix = "with") + @NoArgsConstructor + public static class ChallengeResult { + /** + * The challenge type. One of: CUSTOM_CHALLENGE, SRP_A, PASSWORD_VERIFIER, SMS_MFA, DEVICE_SRP_AUTH, DEVICE_PASSWORD_VERIFIER, or ADMIN_NO_SRP_AUTH. + */ + private String challengeName; + /** + * Set to true if the user successfully completed the challenge, or false otherwise. + */ + private boolean challengeResult; + /** + * Your name for the custom challenge. Used only if challengeName is CUSTOM_CHALLENGE. + */ + private String challengeMetadata; + } + + @Data + @AllArgsConstructor + @Builder(setterPrefix = "with") + @NoArgsConstructor + public static class Response { + /** + * Name of the next challenge, if you want to present a new challenge to your user. + */ + private String challengeName; + + /** + * Set to true if you determine that the user has been sufficiently authenticated by completing the challenges, or false otherwise. + */ + private boolean issueTokens; + + /** + * Set to true if you want to terminate the current authentication process, or false otherwise. + */ + private boolean failAuthentication; + } +} diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolEvent.java new file mode 100644 index 000000000..17c4b409d --- /dev/null +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolEvent.java @@ -0,0 +1,89 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with + * the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.lambda.runtime.events; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Map; + +/** + * Represent the base class for all Cognito User Pool Events + * + * See Customizing User Pool Workflows with Lambda Triggers + * + * @author jvdl + */ +@AllArgsConstructor +@Data +@NoArgsConstructor +public abstract class CognitoUserPoolEvent { + + /** + * The version number of your Lambda function. + */ + private String version; + + /** + * The name of the event that triggered the Lambda function. + */ + private String triggerSource; + + /** + * The AWS Region. + */ + private String region; + + /** + * The user pool ID for the user pool. + */ + private String userPoolId; + + /** + * The username of the current user. + */ + private String userName; + + /** + * The caller context. + */ + private CallerContext callerContext; + + @AllArgsConstructor + @Data + @NoArgsConstructor + public static abstract class Request { + /** + * One or more pairs of user attribute names and values. + */ + private Map userAttributes; + } + + @AllArgsConstructor + @Builder(setterPrefix = "with") + @Data + @NoArgsConstructor + public static class CallerContext { + /** + * The AWS SDK version number. + */ + private String awsSdkVersion; + + /** + * The ID of the client associated with the user pool. + */ + private String clientId; + } +} diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolMigrateUserEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolMigrateUserEvent.java new file mode 100644 index 000000000..381010a76 --- /dev/null +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolMigrateUserEvent.java @@ -0,0 +1,121 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with + * the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.lambda.runtime.events; + +import lombok.*; + +import java.util.Map; + +/** + * Represent the class for the Cognito User Pool Migrate User Lambda Trigger + * + * See Migrate User Lambda Trigger + * + * @author jvdl + */ +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@ToString(callSuper = true) +public class CognitoUserPoolMigrateUserEvent extends CognitoUserPoolEvent { + /** + * The request from the Amazon Cognito service. + */ + private Request request; + + /** + * The response from your Lambda trigger. + */ + private Response response; + + @Builder(setterPrefix = "with") + public CognitoUserPoolMigrateUserEvent( + String version, + String triggerSource, + String region, + String userPoolId, + String userName, + CallerContext callerContext, + Request request, + Response response) { + super(version, triggerSource, region, userPoolId, userName, callerContext); + this.request = request; + this.response = response; + } + + @Data + @EqualsAndHashCode(callSuper = true) + @NoArgsConstructor + @ToString(callSuper = true) + public static class Request extends CognitoUserPoolEvent.Request { + /** + * The username entered by the user. + */ + private String userName; + /** + * The password entered by the user for sign-in. It is not set in the forgot-password flow. + */ + private String password; + /** + * One or more key-value pairs containing the validation data in the user's sign-in request. + */ + private Map validationData; + /** + * One or more key-value pairs that you can provide as custom input to the Lambda function that you specify for the migrate user trigger. + */ + private Map clientMetadata; + + @Builder(setterPrefix = "with") + public Request(Map userAttributes, Map validationData, Map clientMetadata, String userName, String password) { + super(userAttributes); + this.validationData = validationData; + this.clientMetadata = clientMetadata; + this.userName = userName; + this.password = password; + } + } + + @AllArgsConstructor + @Builder(setterPrefix = "with") + @Data + @NoArgsConstructor + public static class Response { + + /** + * It must contain one or more name-value pairs representing user attributes to be stored in the user profile in your user pool. + */ + private Map userAttributes; + + /** + * During sign-in, this attribute can be set to CONFIRMED, or not set, to auto-confirm your users and allow them to sign-in with their previous passwords. + */ + private String finalUserStatus; + + /** + * This attribute can be set to "SUPPRESS" to suppress the welcome message usually sent by Amazon Cognito to new users. + * If this attribute is not returned, the welcome message will be sent. + */ + private String messageAction; + + /** + * This attribute can be set to "EMAIL" to send the welcome message by email, or "SMS" to send the welcome message by SMS. + * If this attribute is not returned, the welcome message will be sent by SMS. + */ + private String[] desiredDeliveryMediums; + + /** + * If this parameter is set to "true" and the phone number or email address specified in the UserAttributes parameter already exists as an alias with a different user, the API call will migrate the alias from the previous user to the newly created user. + */ + private boolean forceAliasCreation; + } +} diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolPostAuthenticationEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolPostAuthenticationEvent.java new file mode 100644 index 000000000..de1af6565 --- /dev/null +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolPostAuthenticationEvent.java @@ -0,0 +1,76 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with + * the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.lambda.runtime.events; + +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.util.Map; + +/** + * Represent the class for the Cognito User Pool Post Authentication Lambda Trigger + * + * See Post Authentication Lambda Trigger + * + * @author jvdl + */ +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@ToString(callSuper = true) +public class CognitoUserPoolPostAuthenticationEvent extends CognitoUserPoolEvent { + + /** + * The request from the Amazon Cognito service. + */ + private Request request; + + @Builder(setterPrefix = "with") + public CognitoUserPoolPostAuthenticationEvent( + String version, + String triggerSource, + String region, + String userPoolId, + String userName, + CallerContext callerContext, + Request request) { + super(version, triggerSource, region, userPoolId, userName, callerContext); + this.request = request; + } + + @Data + @EqualsAndHashCode(callSuper = true) + @NoArgsConstructor + @ToString(callSuper = true) + public static class Request extends CognitoUserPoolEvent.Request { + /** + * One or more key-value pairs that you can provide as custom input to the Lambda function that you specify for the post authentication trigger. + */ + private Map clientMetadata; + + /** + * This flag indicates if the user has signed in on a new device. + */ + private boolean newDeviceUsed; + + @Builder(setterPrefix = "with") + public Request(Map userAttributes, Map clientMetadata, boolean newDeviceUsed) { + super(userAttributes); + this.clientMetadata = clientMetadata; + this.newDeviceUsed = newDeviceUsed; + } + } +} diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolPostConfirmationEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolPostConfirmationEvent.java new file mode 100644 index 000000000..4a835489d --- /dev/null +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolPostConfirmationEvent.java @@ -0,0 +1,70 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with + * the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.lambda.runtime.events; + +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.util.Map; + +/** + * Represent the class for the Cognito User Pool Post Confirmation Lambda Trigger + * + * See Post Confirmation Lambda Trigger + * + * @author jvdl + */ +@EqualsAndHashCode(callSuper = true) +@Data +@NoArgsConstructor +@ToString(callSuper = true) +public class CognitoUserPoolPostConfirmationEvent extends CognitoUserPoolEvent { + + /** + * The request from the Amazon Cognito service. + */ + private Request request; + + @Builder(setterPrefix = "with") + public CognitoUserPoolPostConfirmationEvent( + String version, + String triggerSource, + String region, + String userPoolId, + String userName, + CallerContext callerContext, + Request request) { + super(version, triggerSource, region, userPoolId, userName, callerContext); + this.request = request; + } + + @Data + @EqualsAndHashCode(callSuper = true) + @NoArgsConstructor + @ToString(callSuper = true) + public static class Request extends CognitoUserPoolEvent.Request { + /** + * One or more key-value pairs that you can provide as custom input to the Lambda function that you specify for the post confirmation trigger. + */ + private Map clientMetadata; + + @Builder(setterPrefix = "with") + public Request(Map userAttributes, Map clientMetadata) { + super(userAttributes); + this.clientMetadata = clientMetadata; + } + } +} diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolPreAuthenticationEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolPreAuthenticationEvent.java new file mode 100644 index 000000000..110160415 --- /dev/null +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolPreAuthenticationEvent.java @@ -0,0 +1,77 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with + * the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.lambda.runtime.events; + +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.util.Map; + +/** + * Represent the class for the Cognito User Pool Pre Authentication Lambda Trigger + * + * See Pre Authentication Lambda Trigger + * + * @author jvdl + */ +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@ToString(callSuper = true) +public class CognitoUserPoolPreAuthenticationEvent extends CognitoUserPoolEvent { + + /** + * The request from the Amazon Cognito service. + */ + private Request request; + + @Builder(setterPrefix = "with") + public CognitoUserPoolPreAuthenticationEvent( + String version, + String triggerSource, + String region, + String userPoolId, + String userName, + CallerContext callerContext, + Request request) { + super(version, triggerSource, region, userPoolId, userName, callerContext); + this.request = request; + } + + @Data + @EqualsAndHashCode(callSuper = true) + @NoArgsConstructor + @ToString(callSuper = true) + public static class Request extends CognitoUserPoolEvent.Request { + /** + * One or more name-value pairs containing the validation data in the request to register a user. + * The validation data is set and then passed from the client in the request to register a user. + */ + private Map validationData; + + /** + * This boolean is populated when PreventUserExistenceErrors is set to ENABLED for your User Pool client. + */ + private boolean userNotFound; + + @Builder(setterPrefix = "with") + public Request(Map userAttributes, Map validationData, boolean userNotFound) { + super(userAttributes); + this.validationData = validationData; + this.userNotFound = userNotFound; + } + } +} diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolPreSignUpEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolPreSignUpEvent.java new file mode 100644 index 000000000..da7a848e5 --- /dev/null +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolPreSignUpEvent.java @@ -0,0 +1,104 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with + * the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.lambda.runtime.events; + +import lombok.*; + +import java.util.Map; + +/** + * Represent the class for the Cognito User Pool Pre Sign-up Lambda Trigger + * + * See Pre Sign-up Lambda Trigger + * + * @author jvdl + */ +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@ToString(callSuper = true) +public class CognitoUserPoolPreSignUpEvent extends CognitoUserPoolEvent { + + /** + * The request from the Amazon Cognito service. + */ + private Request request; + + /** + * The response from your Lambda trigger. + */ + private Response response; + + @Builder(setterPrefix = "with") + public CognitoUserPoolPreSignUpEvent( + String version, + String triggerSource, + String region, + String userPoolId, + String userName, + CallerContext callerContext, + Request request, + Response response) { + super(version, triggerSource, region, userPoolId, userName, callerContext); + this.request = request; + this.response = response; + } + + @Data + @EqualsAndHashCode(callSuper = true) + @NoArgsConstructor + @ToString(callSuper = true) + public static class Request extends CognitoUserPoolEvent.Request { + /** + * One or more name-value pairs containing the validation data in the request to register a user. + * The validation data is set and then passed from the client in the request to register a user. + */ + private Map validationData; + + /** + * One or more key-value pairs that you can provide as custom input + * to the Lambda function that you specify for the pre sign-up trigger. + */ + private Map clientMetadata; + + @Builder(setterPrefix = "with") + public Request(Map userAttributes, Map validationData, Map clientMetadata) { + super(userAttributes); + this.validationData = validationData; + this.clientMetadata = clientMetadata; + } + } + + @AllArgsConstructor + @Builder(setterPrefix = "with") + @Data + @NoArgsConstructor + public static class Response { + /** + * Set to true to auto-confirm the user, or false otherwise. + */ + private boolean autoConfirmUser; + + /** + * Set to true to set as verified the phone number of a user who is signing up, or false otherwise. + * If autoVerifyPhone is set to true, the phone_number attribute must have a valid, non-null value. + */ + private boolean autoVerifyPhone; + + /** + * Set to true to set as verified the email of a user who is signing up, or false otherwise. + * If autoVerifyEmail is set to true, the email attribute must have a valid, non-null value. + */ + private boolean autoVerifyEmail; + } +} diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolPreTokenGenerationEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolPreTokenGenerationEvent.java new file mode 100644 index 000000000..e49ce3c40 --- /dev/null +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolPreTokenGenerationEvent.java @@ -0,0 +1,125 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with + * the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.lambda.runtime.events; + +import lombok.*; + +import java.util.Map; + +/** + * Represent the class for the Cognito User Pool Pre Token Generation Lambda Trigger + * + * See Pre Token Generation Lambda Trigger + * + * @author jvdl + */ +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@ToString(callSuper = true) +public class CognitoUserPoolPreTokenGenerationEvent extends CognitoUserPoolEvent { + /** + * The request from the Amazon Cognito service. + */ + private Request request; + + /** + * The response from your Lambda trigger. + */ + private Response response; + + @Builder(setterPrefix = "with") + public CognitoUserPoolPreTokenGenerationEvent( + String version, + String triggerSource, + String region, + String userPoolId, + String userName, + CallerContext callerContext, + Request request, + Response response) { + super(version, triggerSource, region, userPoolId, userName, callerContext); + this.request = request; + this.response = response; + } + + @Data + @EqualsAndHashCode(callSuper = true) + @NoArgsConstructor + @ToString(callSuper = true) + public static class Request extends CognitoUserPoolEvent.Request { + /** + * One or more key-value pairs that you can provide as custom input to the Lambda function that you specify for the pre token generation trigger. + */ + private Map clientMetadata; + + /** + * The input object containing the current group configuration. + */ + private GroupConfiguration groupConfiguration; + + @Builder(setterPrefix = "with") + public Request(Map userAttributes, Map clientMetadata, GroupConfiguration groupConfiguration) { + super(userAttributes); + this.clientMetadata = clientMetadata; + this.groupConfiguration = groupConfiguration; + } + } + + @Data + @AllArgsConstructor + @Builder(setterPrefix = "with") + @NoArgsConstructor + public static class GroupConfiguration { + /** + * A list of the group names that are associated with the user that the identity token is issued for. + */ + private String[] groupsToOverride; + /** + * A list of the current IAM roles associated with these groups. + */ + private String[] iamRolesToOverride; + /** + * Indicates the preferred IAM role. + */ + private String preferredRole; + } + + @Data + @AllArgsConstructor + @Builder(setterPrefix = "with") + @NoArgsConstructor + public static class Response { + private ClaimsOverrideDetails claimsOverrideDetails; + } + + @Data + @AllArgsConstructor + @Builder(setterPrefix = "with") + @NoArgsConstructor + public static class ClaimsOverrideDetails { + /** + * A map of one or more key-value pairs of claims to add or override. + * For group related claims, use groupOverrideDetails instead. + */ + private Map claimsToAddOrOverride; + /** + * A list that contains claims to be suppressed from the identity token. + */ + private String[] claimsToSuppress; + /** + * The output object containing the current group configuration. + */ + private GroupConfiguration groupOverrideDetails; + } +} diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolPreTokenGenerationEventV2.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolPreTokenGenerationEventV2.java new file mode 100644 index 000000000..9faeb9704 --- /dev/null +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolPreTokenGenerationEventV2.java @@ -0,0 +1,134 @@ +/* Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ + +package com.amazonaws.services.lambda.runtime.events; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.util.Map; + +/** + * Represent the class for the Cognito User Pool Pre Token Generation Lambda Trigger V2 + *

+ * See Pre Token Generation Lambda Trigger + */ +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@ToString(callSuper = true) +public class CognitoUserPoolPreTokenGenerationEventV2 extends CognitoUserPoolEvent { + /** + * The request from the Amazon Cognito service. + */ + private Request request; + + /** + * The response from your Lambda trigger. + */ + private Response response; + + @Builder(setterPrefix = "with") + public CognitoUserPoolPreTokenGenerationEventV2( + String version, + String triggerSource, + String region, + String userPoolId, + String userName, + CallerContext callerContext, + Request request, + Response response) { + super(version, triggerSource, region, userPoolId, userName, callerContext); + this.request = request; + this.response = response; + } + + @Data + @EqualsAndHashCode(callSuper = true) + @NoArgsConstructor + @ToString(callSuper = true) + public static class Request extends CognitoUserPoolEvent.Request { + + private String[] scopes; + private GroupConfiguration groupConfiguration; + private Map clientMetadata; + + @Builder(setterPrefix = "with") + public Request(Map userAttributes, String[] scopes, GroupConfiguration groupConfiguration, Map clientMetadata) { + super(userAttributes); + this.scopes = scopes; + this.groupConfiguration = groupConfiguration; + this.clientMetadata = clientMetadata; + } + } + + @Data + @AllArgsConstructor + @Builder(setterPrefix = "with") + @NoArgsConstructor + public static class GroupConfiguration { + /** + * A list of the group names that are associated with the user that the identity token is issued for. + */ + private String[] groupsToOverride; + /** + * A list of the current IAM roles associated with these groups. + */ + private String[] iamRolesToOverride; + /** + * Indicates the preferred IAM role. + */ + private String preferredRole; + } + + @Data + @AllArgsConstructor + @Builder(setterPrefix = "with") + @NoArgsConstructor + public static class Response { + private ClaimsAndScopeOverrideDetails claimsAndScopeOverrideDetails; + } + + @Data + @AllArgsConstructor + @Builder(setterPrefix = "with") + @NoArgsConstructor + public static class ClaimsAndScopeOverrideDetails { + private IdTokenGeneration idTokenGeneration; + private AccessTokenGeneration accessTokenGeneration; + private GroupOverrideDetails groupOverrideDetails; + } + + @Data + @AllArgsConstructor + @Builder(setterPrefix = "with") + @NoArgsConstructor + public static class IdTokenGeneration { + private Map claimsToAddOrOverride; + private String[] claimsToSuppress; + } + + @Data + @AllArgsConstructor + @Builder(setterPrefix = "with") + @NoArgsConstructor + public static class AccessTokenGeneration { + private Map claimsToAddOrOverride; + private String[] claimsToSuppress; + private String[] scopesToAdd; + private String[] scopesToSuppress; + } + + @Data + @AllArgsConstructor + @Builder(setterPrefix = "with") + @NoArgsConstructor + public static class GroupOverrideDetails { + private String[] groupsToOverride; + private String[] iamRolesToOverride; + private String preferredRole; + } +} \ No newline at end of file diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolVerifyAuthChallengeResponseEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolVerifyAuthChallengeResponseEvent.java new file mode 100644 index 000000000..982ff72fd --- /dev/null +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/CognitoUserPoolVerifyAuthChallengeResponseEvent.java @@ -0,0 +1,102 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with + * the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.lambda.runtime.events; + +import lombok.*; + +import java.util.Map; + +/** + * Represent the class for the Cognito User Pool Verify Auth Challenge Response Lambda Trigger + * + * See Verify Auth Challenge Response Lambda Trigger + * + * @author jvdl + */ +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@ToString(callSuper = true) +public class CognitoUserPoolVerifyAuthChallengeResponseEvent extends CognitoUserPoolEvent { + /** + * The request from the Amazon Cognito service. + */ + private Request request; + + /** + * The response from your Lambda trigger. + */ + private Response response; + + @Builder(setterPrefix = "with") + public CognitoUserPoolVerifyAuthChallengeResponseEvent( + String version, + String triggerSource, + String region, + String userPoolId, + String userName, + CallerContext callerContext, + Request request, + Response response) { + super(version, triggerSource, region, userPoolId, userName, callerContext); + this.request = request; + this.response = response; + } + + @Data + @EqualsAndHashCode(callSuper = true) + @NoArgsConstructor + @ToString(callSuper = true) + public static class Request extends CognitoUserPoolEvent.Request { + /** + * One or more key-value pairs that you can provide as custom input to the Lambda function that you specify for the verify auth challenge trigger. + */ + private Map clientMetadata; + /** + * This parameter comes from the Create Auth Challenge trigger, and is compared against a user's challengeAnswer to determine whether the user passed the challenge. + */ + private Map privateChallengeParameters; + /** + * The answer from the user's response to the challenge. + */ + private String challengeAnswer; + /** + * This boolean is populated when PreventUserExistenceErrors is set to ENABLED for your User Pool client + */ + private boolean userNotFound; + + @Builder(setterPrefix = "with") + public Request(Map userAttributes, + Map clientMetadata, + String challengeAnswer, + Map privateChallengeParameters, + boolean userNotFound) { + super(userAttributes); + this.clientMetadata = clientMetadata; + this.userNotFound = userNotFound; + this.challengeAnswer = challengeAnswer; + this.privateChallengeParameters = privateChallengeParameters; + } + } + + @AllArgsConstructor + @Builder(setterPrefix = "with") + @Data + @NoArgsConstructor + public static class Response { + /** + * Set to true if the user has successfully completed the challenge, or false otherwise. + */ + private boolean answerCorrect; + } +} diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/ConnectEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/ConnectEvent.java new file mode 100644 index 000000000..e94875614 --- /dev/null +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/ConnectEvent.java @@ -0,0 +1,92 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with + * the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.lambda.runtime.events; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.util.Map; + +/** + * Class to represent an Amazon Connect contact flow event. + * + * @see parameters; + } + + @Data + @Builder(setterPrefix = "with") + @NoArgsConstructor + @AllArgsConstructor + public static class ContactData implements Serializable, Cloneable { + private Map attributes; + private String channel; + private String contactId; + private CustomerEndpoint customerEndpoint; + private String initialContactId; + private String initiationMethod; + private String instanceArn; + private String previousContactId; + private Queue queue; + private SystemEndpoint systemEndpoint; + } + + @Data + @Builder(setterPrefix = "with") + @NoArgsConstructor + @AllArgsConstructor + public static class CustomerEndpoint implements Serializable, Cloneable { + private String address; + private String type; + } + + @Data + @Builder(setterPrefix = "with") + @NoArgsConstructor + @AllArgsConstructor + public static class SystemEndpoint implements Serializable, Cloneable { + private String address; + private String type; + } + @Data + @Builder(setterPrefix = "with") + @NoArgsConstructor + @AllArgsConstructor + public static class Queue implements Serializable, Cloneable { + private String name; + private String ARN; + } + +} diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/DynamodbTimeWindowEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/DynamodbTimeWindowEvent.java new file mode 100644 index 000000000..64ed9fb29 --- /dev/null +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/DynamodbTimeWindowEvent.java @@ -0,0 +1,83 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with + * the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.lambda.runtime.events; + +import com.amazonaws.services.lambda.runtime.events.models.TimeWindow; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; + +/** + * Represents an Amazon Dynamodb event when using time windows. + */ +@Data +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class DynamodbTimeWindowEvent extends DynamodbEvent implements Serializable, Cloneable { + + private static final long serialVersionUID = -5449871161108629510L; + + /** + * Time window for the records in the event. + */ + private TimeWindow window; + + /** + * State being built up to this invoke in the time window. + */ + private Map state; + + /** + * Shard id of the records + */ + private String shardId; + + /** + * Dynamodb stream arn. + */ + private String eventSourceArn; + + /** + * Set to true for the last invoke of the time window. Subsequent invoke will start a new time window along with a fresh state. + */ + private Boolean isFinalInvokeForWindow; + + /** + * Set to true if window is terminated prematurely. Subsequent invoke will continue the same window with a fresh state. + */ + private Boolean isWindowTerminatedEarly; + + @Builder(setterPrefix = "with") + public DynamodbTimeWindowEvent( + final List records, + final TimeWindow window, + final Map state, + final String shardId, + final String eventSourceArn, + final Boolean isFinalInvokeForWindow, + final Boolean isWindowTerminatedEarly) { + this.setRecords(records); + this.window = window; + this.state = state; + this.shardId = shardId; + this.eventSourceArn = eventSourceArn; + this.isFinalInvokeForWindow = isFinalInvokeForWindow; + this.isWindowTerminatedEarly = isWindowTerminatedEarly; + } +} diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/IamPolicyResponse.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/IamPolicyResponse.java new file mode 100644 index 000000000..e8d3b13d9 --- /dev/null +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/IamPolicyResponse.java @@ -0,0 +1,92 @@ +package com.amazonaws.services.lambda.runtime.events; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * The IAM Policy Response required for API Gateway HTTP APIs + * + * https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-lambda-authorizer.html + * + */ + +@Data +@Builder(setterPrefix = "with") +@NoArgsConstructor +@AllArgsConstructor +public class IamPolicyResponse implements Serializable, Cloneable { + + public static final String EXECUTE_API_INVOKE = "execute-api:Invoke"; + public static final String VERSION_2012_10_17 = "2012-10-17"; + public static final String ALLOW = "Allow"; + public static final String DENY = "Deny"; + + private String principalId; + private PolicyDocument policyDocument; + private Map context; + + public Map getPolicyDocument() { + Map serializablePolicy = new HashMap<>(); + serializablePolicy.put("Version", policyDocument.getVersion()); + + int numberOfStatements = policyDocument.getStatement().size(); + Map[] serializableStatementArray = new Map[numberOfStatements]; + for (int i = 0; i < numberOfStatements; i++) { + Statement statement = policyDocument.getStatement().get(i); + Map serializableStatement = new HashMap<>(); + serializableStatement.put("Effect", statement.getEffect()); + serializableStatement.put("Action", statement.getAction()); + serializableStatement.put("Resource", statement.getResource().toArray(new String[0])); + serializableStatement.put("Condition", statement.getCondition()); + serializableStatementArray[i] = serializableStatement; + } + serializablePolicy.put("Statement", serializableStatementArray); + return serializablePolicy; + } + + public static Statement allowStatement(String resource) { + return Statement.builder() + .withEffect(ALLOW) + .withResource(Collections.singletonList(resource)) + .withAction(EXECUTE_API_INVOKE) + .build(); + } + + public static Statement denyStatement(String resource) { + return Statement.builder() + .withEffect(DENY) + .withResource(Collections.singletonList(resource)) + .withAction(EXECUTE_API_INVOKE) + .build(); + } + + @Data + @Builder(setterPrefix = "with") + @NoArgsConstructor + @AllArgsConstructor + public static class PolicyDocument implements Serializable, Cloneable { + + private String version; + private List statement; + } + + @Data + @Builder(setterPrefix = "with") + @NoArgsConstructor + @AllArgsConstructor + public static class Statement implements Serializable, Cloneable { + + private String action; + private String effect; + private List resource; + private Map> condition; + } +} \ No newline at end of file diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/IamPolicyResponseV1.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/IamPolicyResponseV1.java new file mode 100644 index 000000000..a4316536f --- /dev/null +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/IamPolicyResponseV1.java @@ -0,0 +1,93 @@ +package com.amazonaws.services.lambda.runtime.events; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * The IAM Policy Response required for API Gateway REST APIs + * + * https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-lambda-authorizer-output.html + * + */ + +@Data +@Builder(setterPrefix = "with") +@NoArgsConstructor +@AllArgsConstructor +public class IamPolicyResponseV1 implements Serializable, Cloneable { + + public static final String EXECUTE_API_INVOKE = "execute-api:Invoke"; + public static final String VERSION_2012_10_17 = "2012-10-17"; + public static final String ALLOW = "Allow"; + public static final String DENY = "Deny"; + + private String principalId; + private PolicyDocument policyDocument; + private Map context; + private String usageIdentifierKey; + + public Map getPolicyDocument() { + Map serializablePolicy = new HashMap<>(); + serializablePolicy.put("Version", policyDocument.getVersion()); + + int numberOfStatements = policyDocument.getStatement().size(); + Map[] serializableStatementArray = new Map[numberOfStatements]; + for (int i = 0; i < numberOfStatements; i++) { + Statement statement = policyDocument.getStatement().get(i); + Map serializableStatement = new HashMap<>(); + serializableStatement.put("Effect", statement.getEffect()); + serializableStatement.put("Action", statement.getAction()); + serializableStatement.put("Resource", statement.getResource().toArray(new String[0])); + serializableStatement.put("Condition", statement.getCondition()); + serializableStatementArray[i] = serializableStatement; + } + serializablePolicy.put("Statement", serializableStatementArray); + return serializablePolicy; + } + + public static Statement allowStatement(String resource) { + return Statement.builder() + .withEffect(ALLOW) + .withResource(Collections.singletonList(resource)) + .withAction(EXECUTE_API_INVOKE) + .build(); + } + + public static Statement denyStatement(String resource) { + return Statement.builder() + .withEffect(DENY) + .withResource(Collections.singletonList(resource)) + .withAction(EXECUTE_API_INVOKE) + .build(); + } + + @Data + @Builder(setterPrefix = "with") + @NoArgsConstructor + @AllArgsConstructor + public static class PolicyDocument implements Serializable, Cloneable { + + private String version; + private List statement; + } + + @Data + @Builder(setterPrefix = "with") + @NoArgsConstructor + @AllArgsConstructor + public static class Statement implements Serializable, Cloneable { + + private String action; + private String effect; + private List resource; + private Map> condition; + } +} \ No newline at end of file diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/KafkaEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/KafkaEvent.java new file mode 100644 index 000000000..aa6c00de3 --- /dev/null +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/KafkaEvent.java @@ -0,0 +1,73 @@ +/* + * Copyright 2015-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with + * the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.lambda.runtime.events; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import java.util.List; +import java.util.Map; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder(setterPrefix = "with") +/** Represents a Kafka Event. **/ +public class KafkaEvent { + private Map> records; + private String eventSource; + private String eventSourceArn; + private String bootstrapServers; + + @Data + @NoArgsConstructor + @AllArgsConstructor + @Builder(setterPrefix = "with") + public static class KafkaEventRecord { + private String topic; + private int partition; + private long offset; + private long timestamp; + private String timestampType; + private String key; + private String value; + private List> headers; + private SchemaMetadata keySchemaMetadata; + private SchemaMetadata valueSchemaMetadata; + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + @Builder(setterPrefix = "with") + public static class TopicPartition { + private String topic; + private int partition; + + @Override + public String toString() { + //Kafka also uses '-' for toString() + return topic + "-" + partition; + } + } + + @Data + @AllArgsConstructor + @NoArgsConstructor + @Builder(setterPrefix = "with") + public static class SchemaMetadata { + private String schemaId; + private String dataFormat; + } +} diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/KinesisEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/KinesisEvent.java index d3c44e5b8..cce378035 100644 --- a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/KinesisEvent.java +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/KinesisEvent.java @@ -316,7 +316,7 @@ public String toString() { if (getEventName() != null) sb.append("eventName: ").append(getEventName()).append(","); if (getEventVersion() != null) - sb.append("eventSourceARN: ").append(getEventSourceARN()).append(","); + sb.append("eventVersion: ").append(getEventVersion()).append(","); if (getEventSourceARN() != null) sb.append("eventSourceARN: ").append(getEventSourceARN()).append(","); if (getAwsRegion() != null) diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/KinesisTimeWindowEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/KinesisTimeWindowEvent.java new file mode 100644 index 000000000..f5e982a1b --- /dev/null +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/KinesisTimeWindowEvent.java @@ -0,0 +1,83 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with + * the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.lambda.runtime.events; + +import com.amazonaws.services.lambda.runtime.events.models.TimeWindow; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; + +/** + * Represents an Amazon Kinesis event when using time windows. + */ +@Data +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class KinesisTimeWindowEvent extends KinesisEvent implements Serializable, Cloneable { + + private static final long serialVersionUID = 8926430039233062266L; + + /** + * Time window for the records in the event. + */ + private TimeWindow window; + + /** + * State being built up to this invoke in the time window. + */ + private Map state; + + /** + * Shard id of the records + */ + private String shardId; + + /** + * Kinesis stream or consumer arn. + */ + private String eventSourceArn; + + /** + * Set to true for the last invoke of the time window. Subsequent invoke will start a new time window along with a fresh state. + */ + private Boolean isFinalInvokeForWindow; + + /** + * Set to true if window is terminated prematurely. Subsequent invoke will continue the same window with a fresh state. + */ + private Boolean isWindowTerminatedEarly; + + @Builder(setterPrefix = "with") + public KinesisTimeWindowEvent( + final List records, + final TimeWindow window, + final Map state, + final String shardId, + final String eventSourceArn, + final Boolean isFinalInvokeForWindow, + final Boolean isWindowTerminatedEarly) { + this.setRecords(records); + this.window = window; + this.state = state; + this.shardId = shardId; + this.eventSourceArn = eventSourceArn; + this.isFinalInvokeForWindow = isFinalInvokeForWindow; + this.isWindowTerminatedEarly = isWindowTerminatedEarly; + } +} diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/LambdaDestinationEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/LambdaDestinationEvent.java new file mode 100644 index 000000000..eaa4611fc --- /dev/null +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/LambdaDestinationEvent.java @@ -0,0 +1,55 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with + * the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.lambda.runtime.events; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.joda.time.DateTime; + +import java.io.Serializable; +import java.util.Map; + +/** + * Class to represent an invocation record for a Lambda event. + * + * @see requestPayload; + private Object responseContext; + private Object responsePayload; + + @Data + @Builder(setterPrefix = "with") + @NoArgsConstructor + @AllArgsConstructor + public static class RequestContext implements Serializable, Cloneable { + private String requestId; + private String functionArn; + private String condition; + private int approximateInvokeCount; + } +} diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/MSKFirehoseEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/MSKFirehoseEvent.java new file mode 100644 index 000000000..1af40ce43 --- /dev/null +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/MSKFirehoseEvent.java @@ -0,0 +1,51 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.events; + +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Map; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder(setterPrefix = "with") +@NoArgsConstructor +@AllArgsConstructor + +public class MSKFirehoseEvent { + + private String invocationId; + + private String deliveryStreamArn; + + private String sourceMSKArn; + + private String region; + + private List records; + + @Data + @Builder(setterPrefix = "with") + @NoArgsConstructor + @AllArgsConstructor + public static class Record { + + private ByteBuffer kafkaRecordValue; + + private String recordId; + + private Long approximateArrivalEpoch; + + private Long approximateArrivalTimestamp; + + private Map mskRecordMetadata; + + } +} diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/MSKFirehoseResponse.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/MSKFirehoseResponse.java new file mode 100644 index 000000000..18b5aa13f --- /dev/null +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/MSKFirehoseResponse.java @@ -0,0 +1,61 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.events; + +import java.nio.ByteBuffer; +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Response model for Amazon Data Firehose Lambda transformation with MSK as a source. + * [+] Amazon Data Firehose Data Transformation - Data Transformation and Status Model - ... + * OK : Indicates that processing of this item succeeded. + * ProcessingFailed : Indicate that the processing of this item failed. + * Dropped : Indicates that this item should be silently dropped + */ + +@Data +@Builder(setterPrefix = "with") +@NoArgsConstructor +@AllArgsConstructor + +public class MSKFirehoseResponse { + + public enum Result { + + /** + * Indicates that processing of this item succeeded. + */ + Ok, + + /** + * Indicate that the processing of this item failed + */ + ProcessingFailed, + + /** + * Indicates that this item should be silently dropped + */ + Dropped + } + public List records; + + @Data + @NoArgsConstructor + @Builder(setterPrefix = "with") + @AllArgsConstructor + + public static class Record { + public String recordId; + public Result result; + public ByteBuffer kafkaRecordValue; + + } +} diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/RabbitMQEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/RabbitMQEvent.java new file mode 100644 index 000000000..24fe946c5 --- /dev/null +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/RabbitMQEvent.java @@ -0,0 +1,57 @@ +package com.amazonaws.services.lambda.runtime.events; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +/** + * Represents a Rabbit MQ event sent to Lambda + * Onboarding Amazon MQ as event source to Lambda + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder(setterPrefix = "with") +public class RabbitMQEvent { + + private String eventSource; + private String eventSourceArn; + private Map> rmqMessagesByQueue; + + @Data + @NoArgsConstructor + @AllArgsConstructor + @Builder(setterPrefix = "with") + public static class RabbitMessage { + private BasicProperties basicProperties; + private boolean redelivered; + private String data; + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + @Builder(setterPrefix = "with") + public static class BasicProperties { + + private String contentType; + private String contentEncoding; + private Map headers; + private int deliveryMode; + private int priority; + private String correlationId; + private String replyTo; + private int expiration; + private String messageId; + private String timestamp; + private String type; + private String userId; + private String appId; + private String clusterId; + private int bodySize; + } +} diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/S3BatchEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/S3BatchEvent.java new file mode 100644 index 000000000..a3e8d682b --- /dev/null +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/S3BatchEvent.java @@ -0,0 +1,48 @@ +package com.amazonaws.services.lambda.runtime.events; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * Event to represent the payload which is sent to Lambda by S3 Batch to perform a custom + * action. + * + * https://docs.aws.amazon.com/AmazonS3/latest/dev/batch-ops-invoke-lambda.html + */ + +@Data +@Builder(setterPrefix = "with") +@NoArgsConstructor +@AllArgsConstructor +public class S3BatchEvent { + + private String invocationSchemaVersion; + private String invocationId; + private Job job; + private List tasks; + + @Data + @Builder(setterPrefix = "with") + @NoArgsConstructor + @AllArgsConstructor + public static class Job { + + private String id; + } + + @Data + @Builder(setterPrefix = "with") + @NoArgsConstructor + @AllArgsConstructor + public static class Task { + + private String taskId; + private String s3Key; + private String s3VersionId; + private String s3BucketArn; + } +} \ No newline at end of file diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/S3BatchEventV2.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/S3BatchEventV2.java new file mode 100644 index 000000000..e9beb1f41 --- /dev/null +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/S3BatchEventV2.java @@ -0,0 +1,50 @@ +package com.amazonaws.services.lambda.runtime.events; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +/** + * Event to represent the payload which is sent to Lambda by S3 Batch to perform a custom + * action when using invocation schema version 2.0. + * + * https://docs.aws.amazon.com/AmazonS3/latest/dev/batch-ops-invoke-lambda.html + */ + +@Data +@Builder(setterPrefix = "with") +@NoArgsConstructor +@AllArgsConstructor +public class S3BatchEventV2 { + + private String invocationSchemaVersion; + private String invocationId; + private Job job; + private List tasks; + + @Data + @Builder(setterPrefix = "with") + @NoArgsConstructor + @AllArgsConstructor + public static class Job { + + private String id; + private Map userArguments; + } + + @Data + @Builder(setterPrefix = "with") + @NoArgsConstructor + @AllArgsConstructor + public static class Task { + + private String taskId; + private String s3Key; + private String s3VersionId; + private String s3Bucket; + } +} diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/S3BatchResponse.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/S3BatchResponse.java new file mode 100644 index 000000000..d584a31dd --- /dev/null +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/S3BatchResponse.java @@ -0,0 +1,71 @@ +package com.amazonaws.services.lambda.runtime.events; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * Event to represent the response which should be returned as part of a S3 Batch custom + * action. + * + * https://docs.aws.amazon.com/AmazonS3/latest/dev/batch-ops-invoke-lambda.html + */ + +@Data +@Builder(setterPrefix = "with") +@NoArgsConstructor +@AllArgsConstructor +public class S3BatchResponse { + + private String invocationSchemaVersion; + private ResultCode treatMissingKeysAs; + private String invocationId; + private List results; + + @Data + @Builder(setterPrefix = "with") + @NoArgsConstructor + @AllArgsConstructor + public static class Result { + + private String taskId; + private ResultCode resultCode; + private String resultString; + } + + public enum ResultCode { + + /** + * The task completed normally. If you requested a job completion report, + * the task's result string is included in the report. + */ + Succeeded, + /** + * The task suffered a temporary failure and will be redriven before the job + * completes. The result string is ignored. If this is the final redrive, + * the error message is included in the final report. + */ + TemporaryFailure, + /** + * The task suffered a permanent failure. If you requested a job-completion + * report, the task is marked as Failed and includes the error message + * string. Result strings from failed tasks are ignored. + */ + PermanentFailure + } + + public static S3BatchResponseBuilder fromS3BatchEvent(S3BatchEvent s3BatchEvent) { + return S3BatchResponse.builder() + .withInvocationId(s3BatchEvent.getInvocationId()) + .withInvocationSchemaVersion(s3BatchEvent.getInvocationSchemaVersion()); + } + + public static S3BatchResponseBuilder fromS3BatchEvent(S3BatchEventV2 s3BatchEvent) { + return S3BatchResponse.builder() + .withInvocationId(s3BatchEvent.getInvocationId()) + .withInvocationSchemaVersion(s3BatchEvent.getInvocationSchemaVersion()); + } +} diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/S3ObjectLambdaEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/S3ObjectLambdaEvent.java new file mode 100644 index 000000000..35c836381 --- /dev/null +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/S3ObjectLambdaEvent.java @@ -0,0 +1,136 @@ +package com.amazonaws.services.lambda.runtime.events; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Map; + +/** + * Event to allow transformations to occur before an S3 object is returned to the calling service. + * + * Documentation + * + * Writing and debugging Lambda functions for S3 Object Lambda Access Points + * + * Example: + * + *

+ * import com.amazonaws.services.lambda.runtime.Context;
+ * import com.amazonaws.services.lambda.runtime.events.S3ObjectLambdaEvent;
+ * import org.apache.http.client.fluent.Request;
+ * import software.amazon.awssdk.services.s3.S3Client;
+ * import software.amazon.awssdk.services.s3.model.WriteGetObjectResponseRequest;
+ *
+ * import java.io.IOException;
+ *
+ * import static software.amazon.awssdk.core.sync.RequestBody.fromString;
+ *
+ * public class S3ObjectRequestHandler {
+ *
+ *      private static final S3Client s3Client = S3Client.create();
+ *
+ *      public void handleRequest(S3ObjectLambdaEvent event, Context context) throws IOException {
+ *          String s3Body = Request.Get(event.inputS3Url()).execute().returnContent().asString();
+ *
+ *          String responseBody = s3Body.toUpperCase();
+ *
+ *          WriteGetObjectResponseRequest request = WriteGetObjectResponseRequest.builder()
+ *              .requestRoute(event.outputRoute())
+ *              .requestToken(event.outputToken())
+ *              .build();
+ *          s3Client.writeGetObjectResponse(request, fromString(responseBody));
+ *      }
+ * }
+ * 
+ * 
+ */ + +@Data +@Builder(setterPrefix = "with") +@NoArgsConstructor +@AllArgsConstructor +public class S3ObjectLambdaEvent { + + private String xAmzRequestId; + private GetObjectContext getObjectContext; + private Configuration configuration; + private UserRequest userRequest; + private UserIdentity userIdentity; + private String protocolVersion; + + /** + * A pre-signed URL that can be used to fetch the original object from Amazon S3. + * + * The URL is signed using the original caller's identity, and their permissions + * will apply when the URL is used. If there are signed headers in the URL, the + * Lambda function must include these in the call to Amazon S3, except for the Host. + * + * @return A pre-signed URL that can be used to fetch the original object from Amazon S3. + */ + public String inputS3Url() { + return getGetObjectContext().getInputS3Url(); + } + + /** + * A routing token that is added to the S3 Object Lambda URL when the Lambda function + * calls the S3 API WriteGetObjectResponse. + * + * @return the outputRoute + */ + public String outputRoute() { + return getGetObjectContext().getOutputRoute(); + } + + /** + * An opaque token used by S3 Object Lambda to match the WriteGetObjectResponse call + * with the original caller. + * + * @return the outputToken + */ + public String outputToken() { + return getGetObjectContext().getOutputToken(); + } + + @Data + @Builder(setterPrefix = "with") + @NoArgsConstructor + @AllArgsConstructor + public static class GetObjectContext { + private String inputS3Url; + private String outputRoute; + private String outputToken; + } + + @Data + @Builder(setterPrefix = "with") + @NoArgsConstructor + @AllArgsConstructor + public static class Configuration { + private String accessPointArn; + private String supportingAccessPointArn; + private String payload; + } + + @Data + @Builder(setterPrefix = "with") + @NoArgsConstructor + @AllArgsConstructor + public static class UserRequest { + private String url; + private Map headers; + } + + @Data + @Builder(setterPrefix = "with") + @NoArgsConstructor + @AllArgsConstructor + public static class UserIdentity { + private String type; + private String principalId; + private String arn; + private String accountId; + private String accessKeyId; + } +} \ No newline at end of file diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/SQSBatchResponse.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/SQSBatchResponse.java new file mode 100644 index 000000000..c1f728f1b --- /dev/null +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/SQSBatchResponse.java @@ -0,0 +1,53 @@ +/* + * Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with + * the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.lambda.runtime.events; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.util.List; + +/** + * Function response type to report batch item failures for {@link SQSEvent}. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder(setterPrefix = "with") +public class SQSBatchResponse implements Serializable { + + private static final long serialVersionUID = 5075170615239078773L; + + /** + * A list of messageIds that failed processing. These messageIds will be retried. + */ + private List batchItemFailures; + + @Data + @NoArgsConstructor + @AllArgsConstructor + @Builder(setterPrefix = "with") + public static class BatchItemFailure implements Serializable { + + private static final long serialVersionUID = 40727862494377907L; + + /** + * MessageId that failed processing + */ + String itemIdentifier; + } +} diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/ScheduledEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/ScheduledEvent.java index 5908c39c3..405ede583 100644 --- a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/ScheduledEvent.java +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/ScheduledEvent.java @@ -26,6 +26,8 @@ public class ScheduledEvent implements Serializable, Cloneable { private static final long serialVersionUID = -5810383198587331146L; + private String version; + private String account; private String region; @@ -47,6 +49,29 @@ public class ScheduledEvent implements Serializable, Cloneable { */ public ScheduledEvent() {} + /** + * @return the version number + */ + public String getVersion() { + return version; + } + + /** + * @param version the version number + */ + public void setVersion(String version) { + this.version = version; + } + + /** + * @param version version number + * @return ScheduledEvent + */ + public ScheduledEvent withVersion(String version) { + setVersion(version); + return this; + } + /** * @return the account id */ @@ -69,7 +94,7 @@ public ScheduledEvent withAccount(String account) { setAccount(account); return this; } - + /** * @return the aws region */ @@ -92,7 +117,7 @@ public ScheduledEvent withRegion(String region) { setRegion(region); return this; } - + /** * @return The details of the events (usually left blank) */ @@ -115,7 +140,7 @@ public ScheduledEvent withDetail(Map detail) { setDetail(detail); return this; } - + /** * @return The details type - see cloud watch events for more info */ @@ -138,19 +163,19 @@ public ScheduledEvent withDetailType(String detailType) { setDetailType(detailType); return this; } - + /** - * @return the soruce of the event + * @return the source of the event */ public String getSource() { return source; } /** - * @param soruce the soruce of the event + * @param source the source of the event */ - public void setSource(String soruce) { - this.source = soruce; + public void setSource(String source) { + this.source = source; } /** @@ -161,7 +186,7 @@ public ScheduledEvent withSource(String source) { setSource(source); return this; } - + /** * @return the timestamp for when the event is scheduled */ @@ -184,7 +209,7 @@ public ScheduledEvent withTime(DateTime time) { setTime(time); return this; } - + /** * @return the id of the event */ @@ -207,7 +232,7 @@ public ScheduledEvent withId(String id) { setId(id); return this; } - + /** * @return the resources used by event */ @@ -242,6 +267,8 @@ public ScheduledEvent withResources(List resources) { public String toString() { StringBuilder sb = new StringBuilder(); sb.append("{"); + if (getVersion() != null) + sb.append("version: ").append(getVersion()).append(","); if (getAccount() != null) sb.append("account: ").append(getAccount()).append(","); if (getRegion() != null) @@ -272,6 +299,10 @@ public boolean equals(Object obj) { if (obj instanceof ScheduledEvent == false) return false; ScheduledEvent other = (ScheduledEvent) obj; + if (other.getVersion() == null ^ this.getVersion() == null) + return false; + if (other.getVersion() != null && other.getVersion().equals(this.getVersion()) == false) + return false; if (other.getAccount() == null ^ this.getAccount() == null) return false; if (other.getAccount() != null && other.getAccount().equals(this.getAccount()) == false) @@ -312,6 +343,7 @@ public int hashCode() { final int prime = 31; int hashCode = 1; + hashCode = prime * hashCode + ((getVersion() == null) ? 0 : getVersion().hashCode()); hashCode = prime * hashCode + ((getAccount() == null) ? 0 : getAccount().hashCode()); hashCode = prime * hashCode + ((getRegion() == null) ? 0 : getRegion().hashCode()); hashCode = prime * hashCode + ((getDetail() == null) ? 0 : getDetail().hashCode()); @@ -331,5 +363,5 @@ public ScheduledEvent clone() { throw new IllegalStateException("Got a CloneNotSupportedException from Object.clone()", e); } } - + } diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/SecretsManagerRotationEvent.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/SecretsManagerRotationEvent.java new file mode 100644 index 000000000..3e8df5bce --- /dev/null +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/SecretsManagerRotationEvent.java @@ -0,0 +1,40 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with + * the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.lambda.runtime.events; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Class to represent the events which are sent during a Secrets Manager rotation process. + * + * @see Rotating secrets lambda function overview + * + * @author msailes + */ + +@Data +@Builder(setterPrefix = "with") +@NoArgsConstructor +@AllArgsConstructor +public class SecretsManagerRotationEvent { + + private String step; + private String secretId; + private String clientRequestToken; + private String rotationToken; + +} diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/SimpleIAMPolicyResponse.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/SimpleIAMPolicyResponse.java new file mode 100644 index 000000000..030a1468d --- /dev/null +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/SimpleIAMPolicyResponse.java @@ -0,0 +1,23 @@ +package com.amazonaws.services.lambda.runtime.events; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Map; + +/** + * The simplified IAM Policy response object as described in https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-lambda-authorizer.html + * + */ + +@Data +@Builder(setterPrefix = "with") +@NoArgsConstructor +@AllArgsConstructor +public class SimpleIAMPolicyResponse { + + private boolean isAuthorized; + private Map context; +} \ No newline at end of file diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/StreamsEventResponse.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/StreamsEventResponse.java new file mode 100644 index 000000000..9d2990f9d --- /dev/null +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/StreamsEventResponse.java @@ -0,0 +1,52 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with + * the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.lambda.runtime.events; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.util.List; + +/** + * Function response type to report batch item failures for {@link KinesisEvent} and {@link DynamodbEvent}. + * https://docs.aws.amazon.com/lambda/latest/dg/with-kinesis.html#services-kinesis-batchfailurereporting + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder(setterPrefix = "with") +public class StreamsEventResponse implements Serializable { + private static final long serialVersionUID = 3232053116472095907L; + + /** + * A list of records which failed processing. Returning the first record which failed would retry all remaining records from the batch. + */ + private List batchItemFailures; + + @Data + @NoArgsConstructor + @AllArgsConstructor + @Builder(setterPrefix = "with") + public static class BatchItemFailure implements Serializable { + private static final long serialVersionUID = 1473983466096085881L; + + /** + * Sequence number of the record which failed processing. + */ + String itemIdentifier; + } +} diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/TimeWindowEventResponse.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/TimeWindowEventResponse.java new file mode 100644 index 000000000..8d1440757 --- /dev/null +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/TimeWindowEventResponse.java @@ -0,0 +1,58 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with + * the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.lambda.runtime.events; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; + +/** + * Response type to return a new state for the time window and to report batch item failures. This should be used along with {@link KinesisTimeWindowEvent} or {@link DynamodbTimeWindowEvent}. + * https://docs.aws.amazon.com/lambda/latest/dg/with-kinesis.html#services-kinesis-windows + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder(setterPrefix = "with") +public class TimeWindowEventResponse implements Serializable { + private static final long serialVersionUID = 2259096191791166028L; + + /** + * New state after processing a batch of records. + */ + private Map state; + + /** + * A list of records which failed processing. Returning the first record which failed would retry all remaining records from the batch. + */ + private List batchItemFailures; + + @Data + @NoArgsConstructor + @AllArgsConstructor + @Builder(setterPrefix = "with") + public static class BatchItemFailure implements Serializable { + private static final long serialVersionUID = 5224634072234167773L; + + /** + * Sequence number of the record which failed processing. + */ + String itemIdentifier; + } +} diff --git a/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/models/TimeWindow.java b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/models/TimeWindow.java new file mode 100644 index 000000000..77d0452fa --- /dev/null +++ b/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/models/TimeWindow.java @@ -0,0 +1,39 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with + * the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.lambda.runtime.events.models; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Represents a time window. + */ +@Data +@Builder(setterPrefix = "with") +@NoArgsConstructor +@AllArgsConstructor +public class TimeWindow { + + /** + * Window start instant represented as ISO-8601 string. + */ + private String start; + + /** + * Window end instant represented as ISO-8601 string. + */ + private String end; +} diff --git a/aws-lambda-java-events/src/main/java/lombok.config b/aws-lambda-java-events/src/main/java/lombok.config new file mode 100644 index 000000000..531bad714 --- /dev/null +++ b/aws-lambda-java-events/src/main/java/lombok.config @@ -0,0 +1 @@ +lombok.getter.noIsPrefix = true diff --git a/aws-lambda-java-events/src/test/java/com/amazonaws/services/lambda/runtime/events/APIGatewayV2CustomAuthorizerEventTest.java b/aws-lambda-java-events/src/test/java/com/amazonaws/services/lambda/runtime/events/APIGatewayV2CustomAuthorizerEventTest.java new file mode 100644 index 000000000..8f1662cdf --- /dev/null +++ b/aws-lambda-java-events/src/test/java/com/amazonaws/services/lambda/runtime/events/APIGatewayV2CustomAuthorizerEventTest.java @@ -0,0 +1,36 @@ +package com.amazonaws.services.lambda.runtime.events; + +import org.junit.jupiter.api.Test; + +import java.time.Instant; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class APIGatewayV2CustomAuthorizerEventTest { + + private static final long TIME_EPOCH = 1601306426515L; + private static final String TIME = "28/Sep/2020:15:14:43 +0000"; + + @Test + public void testEpochLongAsAnInstant() { + APIGatewayV2CustomAuthorizerEvent customAuthorizerEvent = APIGatewayV2CustomAuthorizerEvent.builder() + .withRequestContext(APIGatewayV2CustomAuthorizerEvent.RequestContext.builder() + .withTimeEpoch(TIME_EPOCH) + .build()) + .build(); + + assertEquals(Instant.ofEpochMilli(1601306426515L), customAuthorizerEvent.getRequestContext().getTimeEpoch()); + } + + @Test + public void testTimeStringAsDateTime() { + APIGatewayV2CustomAuthorizerEvent customAuthorizerEvent = APIGatewayV2CustomAuthorizerEvent.builder() + .withRequestContext(APIGatewayV2CustomAuthorizerEvent.RequestContext.builder() + .withTime(TIME) + .build()) + .build(); + + assertNotNull(customAuthorizerEvent.getRequestContext().getTime()); + } +} \ No newline at end of file diff --git a/aws-lambda-java-events/src/test/java/com/amazonaws/services/lambda/runtime/events/IamPolicyResponseTest.java b/aws-lambda-java-events/src/test/java/com/amazonaws/services/lambda/runtime/events/IamPolicyResponseTest.java new file mode 100644 index 000000000..4dbbb108d --- /dev/null +++ b/aws-lambda-java-events/src/test/java/com/amazonaws/services/lambda/runtime/events/IamPolicyResponseTest.java @@ -0,0 +1,91 @@ +package com.amazonaws.services.lambda.runtime.events; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; + +import static com.amazonaws.services.lambda.runtime.events.IamPolicyResponse.ALLOW; +import static com.amazonaws.services.lambda.runtime.events.IamPolicyResponse.EXECUTE_API_INVOKE; +import static com.amazonaws.services.lambda.runtime.events.IamPolicyResponse.VERSION_2012_10_17; +import static com.amazonaws.services.lambda.runtime.events.IamPolicyResponse.allowStatement; +import static com.amazonaws.services.lambda.runtime.events.IamPolicyResponse.denyStatement; +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; + +public class IamPolicyResponseTest { + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + @Test + public void testAllowStatement() throws JsonProcessingException { + IamPolicyResponse iamPolicyResponse = IamPolicyResponse.builder() + .withPrincipalId("me") + .withPolicyDocument(IamPolicyResponse.PolicyDocument.builder() + .withVersion(VERSION_2012_10_17) + .withStatement(singletonList(allowStatement("arn:aws:execute-api:eu-west-1:123456789012:1234abc/$deafult/*/*"))) + .build()) + .build(); + + String json = OBJECT_MAPPER.writeValueAsString(iamPolicyResponse); + + assertThatJson(json).isEqualTo(readResource("iamPolicyResponses/allow.json")); + } + + @Test + public void testDenyStatement() throws JsonProcessingException { + IamPolicyResponse iamPolicyResponse = IamPolicyResponse.builder() + .withPrincipalId("me") + .withPolicyDocument(IamPolicyResponse.PolicyDocument.builder() + .withVersion(VERSION_2012_10_17) + .withStatement(singletonList(denyStatement("arn:aws:execute-api:eu-west-1:123456789012:1234abc/$deafult/*/*"))) + .build()) + .build(); + + String json = OBJECT_MAPPER.writeValueAsString(iamPolicyResponse); + + assertThatJson(json).isEqualTo(readResource("iamPolicyResponses/deny.json")); + } + + @Test + public void testStatementWithCondition() throws JsonProcessingException { + Map> conditions = new HashMap<>(); + conditions.put("DateGreaterThan", singletonMap("aws:TokenIssueTime", "2020-01-01T00:00:01Z")); + + IamPolicyResponse iamPolicyResponse = IamPolicyResponse.builder() + .withPrincipalId("me") + .withPolicyDocument(IamPolicyResponse.PolicyDocument.builder() + .withVersion(VERSION_2012_10_17) + .withStatement(singletonList(IamPolicyResponse.Statement.builder() + .withAction(EXECUTE_API_INVOKE) + .withEffect(ALLOW) + .withResource(singletonList("arn:aws:execute-api:eu-west-1:123456789012:1234abc/$deafult/*/*")) + .withCondition(conditions) + .build())) + .build()) + .build(); + + String json = OBJECT_MAPPER.writeValueAsString(iamPolicyResponse); + + assertThatJson(json).isEqualTo(readResource("iamPolicyResponses/allow-with-condition.json")); + } + + private String readResource(String name) { + Path filePath = Paths.get("src", "test", "resources", name); + byte[] bytes = new byte[0]; + try { + bytes = Files.readAllBytes(filePath); + } catch (IOException e) { + e.printStackTrace(); + } + return new String(bytes, StandardCharsets.UTF_8); + } +} \ No newline at end of file diff --git a/aws-lambda-java-events/src/test/java/com/amazonaws/services/lambda/runtime/events/IamPolicyResponseV1Test.java b/aws-lambda-java-events/src/test/java/com/amazonaws/services/lambda/runtime/events/IamPolicyResponseV1Test.java new file mode 100644 index 000000000..9b966179f --- /dev/null +++ b/aws-lambda-java-events/src/test/java/com/amazonaws/services/lambda/runtime/events/IamPolicyResponseV1Test.java @@ -0,0 +1,94 @@ +package com.amazonaws.services.lambda.runtime.events; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; + +import static com.amazonaws.services.lambda.runtime.events.IamPolicyResponseV1.ALLOW; +import static com.amazonaws.services.lambda.runtime.events.IamPolicyResponseV1.EXECUTE_API_INVOKE; +import static com.amazonaws.services.lambda.runtime.events.IamPolicyResponseV1.VERSION_2012_10_17; +import static com.amazonaws.services.lambda.runtime.events.IamPolicyResponseV1.allowStatement; +import static com.amazonaws.services.lambda.runtime.events.IamPolicyResponseV1.denyStatement; +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; + +public class IamPolicyResponseV1Test { + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + @Test + public void testAllowStatement() throws JsonProcessingException { + IamPolicyResponseV1 iamPolicyResponse = IamPolicyResponseV1.builder() + .withPrincipalId("me") + .withPolicyDocument(IamPolicyResponseV1.PolicyDocument.builder() + .withVersion(VERSION_2012_10_17) + .withStatement(singletonList(allowStatement("arn:aws:execute-api:eu-west-1:123456789012:1234abc/$deafult/*/*"))) + .build()) + .withUsageIdentifierKey("123ABC") + .build(); + + String json = OBJECT_MAPPER.writeValueAsString(iamPolicyResponse); + + assertThatJson(json).isEqualTo(readResource("iamPolicyV1Responses/allow.json")); + } + + @Test + public void testDenyStatement() throws JsonProcessingException { + IamPolicyResponseV1 iamPolicyResponse = IamPolicyResponseV1.builder() + .withPrincipalId("me") + .withPolicyDocument(IamPolicyResponseV1.PolicyDocument.builder() + .withVersion(VERSION_2012_10_17) + .withStatement(singletonList(denyStatement("arn:aws:execute-api:eu-west-1:123456789012:1234abc/$deafult/*/*"))) + .build()) + .withUsageIdentifierKey("123ABC") + .build(); + + String json = OBJECT_MAPPER.writeValueAsString(iamPolicyResponse); + + assertThatJson(json).isEqualTo(readResource("iamPolicyV1Responses/deny.json")); + } + + @Test + public void testStatementWithCondition() throws JsonProcessingException { + Map> conditions = new HashMap<>(); + conditions.put("DateGreaterThan", singletonMap("aws:TokenIssueTime", "2020-01-01T00:00:01Z")); + + IamPolicyResponseV1 iamPolicyResponse = IamPolicyResponseV1.builder() + .withPrincipalId("me") + .withPolicyDocument(IamPolicyResponseV1.PolicyDocument.builder() + .withVersion(VERSION_2012_10_17) + .withStatement(singletonList(IamPolicyResponseV1.Statement.builder() + .withAction(EXECUTE_API_INVOKE) + .withEffect(ALLOW) + .withResource(singletonList("arn:aws:execute-api:eu-west-1:123456789012:1234abc/$deafult/*/*")) + .withCondition(conditions) + .build())) + .build()) + .withUsageIdentifierKey("123ABC") + .build(); + + String json = OBJECT_MAPPER.writeValueAsString(iamPolicyResponse); + + assertThatJson(json).isEqualTo(readResource("iamPolicyV1Responses/allow-with-condition.json")); + } + + private String readResource(String name) { + Path filePath = Paths.get("src", "test", "resources", name); + byte[] bytes = new byte[0]; + try { + bytes = Files.readAllBytes(filePath); + } catch (IOException e) { + e.printStackTrace(); + } + return new String(bytes, StandardCharsets.UTF_8); + } +} \ No newline at end of file diff --git a/aws-lambda-java-events/src/test/resources/iamPolicyResponses/allow-with-condition.json b/aws-lambda-java-events/src/test/resources/iamPolicyResponses/allow-with-condition.json new file mode 100644 index 000000000..0541e4109 --- /dev/null +++ b/aws-lambda-java-events/src/test/resources/iamPolicyResponses/allow-with-condition.json @@ -0,0 +1,13 @@ +{ + "principalId": "me", + "policyDocument": { + "Version": "2012-10-17", + "Statement": [{ + "Action": "execute-api:Invoke", + "Resource": ["arn:aws:execute-api:eu-west-1:123456789012:1234abc/$deafult/*/*"], + "Effect": "Allow", + "Condition": {"DateGreaterThan": {"aws:TokenIssueTime": "2020-01-01T00:00:01Z"}} + }] + }, + "context":null +} \ No newline at end of file diff --git a/aws-lambda-java-events/src/test/resources/iamPolicyResponses/allow.json b/aws-lambda-java-events/src/test/resources/iamPolicyResponses/allow.json new file mode 100644 index 000000000..7636502b3 --- /dev/null +++ b/aws-lambda-java-events/src/test/resources/iamPolicyResponses/allow.json @@ -0,0 +1,13 @@ +{ + "principalId": "me", + "policyDocument": { + "Version": "2012-10-17", + "Statement": [{ + "Action": "execute-api:Invoke", + "Resource": ["arn:aws:execute-api:eu-west-1:123456789012:1234abc/$deafult/*/*"], + "Effect": "Allow", + "Condition": null + }] + }, + "context":null +} \ No newline at end of file diff --git a/aws-lambda-java-events/src/test/resources/iamPolicyResponses/deny.json b/aws-lambda-java-events/src/test/resources/iamPolicyResponses/deny.json new file mode 100644 index 000000000..c5e360d39 --- /dev/null +++ b/aws-lambda-java-events/src/test/resources/iamPolicyResponses/deny.json @@ -0,0 +1,13 @@ +{ + "principalId": "me", + "policyDocument": { + "Version": "2012-10-17", + "Statement": [{ + "Action": "execute-api:Invoke", + "Resource": ["arn:aws:execute-api:eu-west-1:123456789012:1234abc/$deafult/*/*"], + "Effect": "Deny", + "Condition": null + }] + }, + "context":null +} \ No newline at end of file diff --git a/aws-lambda-java-events/src/test/resources/iamPolicyV1Responses/allow-with-condition.json b/aws-lambda-java-events/src/test/resources/iamPolicyV1Responses/allow-with-condition.json new file mode 100644 index 000000000..afebc54e9 --- /dev/null +++ b/aws-lambda-java-events/src/test/resources/iamPolicyV1Responses/allow-with-condition.json @@ -0,0 +1,14 @@ +{ + "principalId": "me", + "policyDocument": { + "Version": "2012-10-17", + "Statement": [{ + "Action": "execute-api:Invoke", + "Resource": ["arn:aws:execute-api:eu-west-1:123456789012:1234abc/$deafult/*/*"], + "Effect": "Allow", + "Condition": {"DateGreaterThan": {"aws:TokenIssueTime": "2020-01-01T00:00:01Z"}} + }] + }, + "context":null, + "usageIdentifierKey": "123ABC" +} \ No newline at end of file diff --git a/aws-lambda-java-events/src/test/resources/iamPolicyV1Responses/allow.json b/aws-lambda-java-events/src/test/resources/iamPolicyV1Responses/allow.json new file mode 100644 index 000000000..518f5baa3 --- /dev/null +++ b/aws-lambda-java-events/src/test/resources/iamPolicyV1Responses/allow.json @@ -0,0 +1,14 @@ +{ + "principalId": "me", + "policyDocument": { + "Version": "2012-10-17", + "Statement": [{ + "Action": "execute-api:Invoke", + "Resource": ["arn:aws:execute-api:eu-west-1:123456789012:1234abc/$deafult/*/*"], + "Effect": "Allow", + "Condition": null + }] + }, + "context":null, + "usageIdentifierKey": "123ABC" +} \ No newline at end of file diff --git a/aws-lambda-java-events/src/test/resources/iamPolicyV1Responses/deny.json b/aws-lambda-java-events/src/test/resources/iamPolicyV1Responses/deny.json new file mode 100644 index 000000000..d06e170c2 --- /dev/null +++ b/aws-lambda-java-events/src/test/resources/iamPolicyV1Responses/deny.json @@ -0,0 +1,14 @@ +{ + "principalId": "me", + "policyDocument": { + "Version": "2012-10-17", + "Statement": [{ + "Action": "execute-api:Invoke", + "Resource": ["arn:aws:execute-api:eu-west-1:123456789012:1234abc/$deafult/*/*"], + "Effect": "Deny", + "Condition": null + }] + }, + "context":null, + "usageIdentifierKey": "123ABC" +} \ No newline at end of file diff --git a/aws-lambda-java-log4j2/README.md b/aws-lambda-java-log4j2/README.md index 40ca3c3aa..b906ee8f6 100644 --- a/aws-lambda-java-log4j2/README.md +++ b/aws-lambda-java-log4j2/README.md @@ -1,5 +1,7 @@ # Using log4j2 with AWS Lambda +**IMPORTANT: The v1.6.3 release contained a regression (see [#612](https://github.com/aws/aws-lambda-java-libs/issues/612)) resulting in missing logs. Please upgrade to v1.6.4 or later. We apologize for the inconvenience.** + ### 1. Pull in log4j2 dependencies Example for Maven pom.xml @@ -10,17 +12,22 @@ Example for Maven pom.xml com.amazonaws aws-lambda-java-log4j2 - 1.2.0 + 1.6.5 org.apache.logging.log4j log4j-core - 2.13.2 + 2.25.5 org.apache.logging.log4j log4j-api - 2.13.2 + 2.25.5 + + + org.apache.logging.log4j + log4j-layout-template-json + 2.25.5 .... @@ -34,7 +41,7 @@ If using maven shade plugin, set the plugin configuration as follows org.apache.maven.plugins maven-shade-plugin - 2.4.3 + 3.6.1 package @@ -65,10 +72,10 @@ If using maven shade plugin, set the plugin configuration as follows If you are using the [John Rengelman](https://github.com/johnrengelman/shadow) Gradle shadow plugin, then the plugin configuration is as follows: ```groovy - + dependencies{ ... - implementation group: 'com.amazonaws', name: 'aws-lambda-java-log4j2', version: '1.1.0' + implementation group: 'com.amazonaws', name: 'aws-lambda-java-log4j2', version: '1.6.5' implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: log4jVersion implementation group: 'org.apache.logging.log4j', name: 'log4j-api', version: log4jVersion } @@ -83,8 +90,8 @@ shadowJar { build.dependsOn(shadowJar) ``` - -If you are using the `sam build` and `sam deploy` commands to deploy your lambda function, then you don't + +If you are using the `sam build` and `sam deploy` commands to deploy your lambda function, then you don't need to use the shadow jar plugin. The `sam` cli-tool merges itself the `Log4j2Plugins.dat` files. @@ -94,22 +101,29 @@ Add the following file `/src/main/resources/log4j2.xml` ```xml - + - - + + + %d{yyyy-MM-dd HH:mm:ss} %X{AWSRequestId} %-5p %c{1}:%L - %m%n - + + + + + - + ``` +If the `AWS_LAMBDA_LOG_FORMAT` is set to `JSON`, the `LambdaJSONFormat` formatter will be applied, otherwise the `LambdaTextFormat`. + ### 3. Example code ```java @@ -117,6 +131,8 @@ package example; import com.amazonaws.services.lambda.runtime.Context; +import static org.apache.logging.log4j.CloseableThreadContext.put; +import org.apache.logging.log4j.CloseableThreadContext.Instance; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -136,6 +152,12 @@ public class Hello { logger.error("log data from log4j err. \n this is a continuation of log4j.err"); + // When logging in JSON, you can also add custom fields + // In java11+ you can use the `try (var ctx = put("name", name)) {}` structure + Instance ctx = put("name", name); + logger.info("log line with input name"); + ctx.close(); + // Return will include the log stream name so you can look // up the log later. return String.format("Hello %s. log stream = %s", name, context.getLogStreamName()); diff --git a/aws-lambda-java-log4j2/RELEASE.CHANGELOG.md b/aws-lambda-java-log4j2/RELEASE.CHANGELOG.md index 09dbc918a..37e7a8760 100644 --- a/aws-lambda-java-log4j2/RELEASE.CHANGELOG.md +++ b/aws-lambda-java-log4j2/RELEASE.CHANGELOG.md @@ -1,17 +1,52 @@ +### May 19, 2026 +`1.6.5`: +- Updated `log4j-core` and `log4j-api` dependencies to `2.25.5` + +`1.6.4`: +- Fix regression in `1.6.3` + +### May 13, 2026 +`1.6.3`: +- Updated `log4j-core` and `log4j-api` dependencies to `2.25.4` + +### February 2026 +`1.6.2`: +- Updated `log4j-core` and `log4j-api` dependencies to `2.25.3` + +### October 24, 2023 +`1.6.0`: +- Log level and log format support + +### January 04, 2022 +`1.5.1`: +- Updated `log4j-core` and `log4j-api` dependencies to `2.17.1` + +### December 18, 2021 +`1.5.0`: +- Updated `log4j-core` and `log4j-api` dependencies to `2.17.0` + +### December 15, 2021 +`1.4.0`: +- Updated `log4j-core` and `log4j-api` dependencies to `2.16.0` + +### December 10, 2021 +`1.3.0`: +- Updated `log4j-core` and `log4j-api` dependencies to `2.15.0` + ### May 05, 2020 `1.2.0`: - Updated `log4j-core` and `log4j-api` dependencies to `2.13.2` -### Apr 28, 2020 +### April 28, 2020 `1.1.1`: - Added missing XML namespace declarations to `pom.xml` file ([#97](https://github.com/aws/aws-lambda-java-libs/issues/97)) - Updated `nexusUrl` in `pom.xml` file ([#108](https://github.com/aws/aws-lambda-java-libs/issues/108)) - Updated `aws-lambda-java-core` to `1.2.1` -### Nov 21, 2017 +### November 21, 2017 `1.1.0`: - Changed `LambdaAppender.append()` to make use of `LambdaLogger` from `com.amazonaws:aws-lambda-java-core:1.2.0` -### Jun 29, 2017 +### June 29, 2017 `1.0.0`: -- Initial release of AWS Lambda Log4j2 support \ No newline at end of file +- Initial release of AWS Lambda Log4j2 support diff --git a/aws-lambda-java-log4j2/pom.xml b/aws-lambda-java-log4j2/pom.xml index c060f8e30..d136612f7 100644 --- a/aws-lambda-java-log4j2/pom.xml +++ b/aws-lambda-java-log4j2/pom.xml @@ -5,7 +5,7 @@ com.amazonaws aws-lambda-java-log4j2 - 1.2.0 + 1.6.4-SNAPSHOT jar AWS Lambda Java Log4j 2.x Libraries @@ -22,6 +22,9 @@ https://github.com/aws/aws-lambda-java-libs.git + scm:git:https://github.com/aws/aws-lambda-java-libs.git + scm:git:https://github.com/aws/aws-lambda-java-libs.git + HEAD @@ -34,7 +37,8 @@ 1.8 1.8 - 2.13.2 + 2.25.5 + 5.12.2 @@ -48,7 +52,7 @@ com.amazonaws aws-lambda-java-core - 1.2.1 + 1.2.3 org.apache.logging.log4j @@ -60,8 +64,61 @@ log4j-api ${log4j.version} + + org.apache.logging.log4j + log4j-layout-template-json + ${log4j.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit-jupiter.version} + test + + + + + org.apache.maven.plugins + maven-release-plugin + 3.1.1 + + aws-lambda-java-log4j2-@{project.version} + true + release + deploy + + + + org.apache.maven.plugins + maven-toolchains-plugin + 3.2.0 + + + + + [1.8,9) + + + + + + + toolchain + + + + + + maven-surefire-plugin + 3.5.2 + + + + dev @@ -134,14 +191,13 @@ - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.3 + org.sonatype.central + central-publishing-maven-plugin + 0.8.0 true - sonatype-nexus-staging - https://aws.oss.sonatype.org/ - false + central + false diff --git a/aws-lambda-java-log4j2/src/main/java/com/amazonaws/services/lambda/runtime/log4j2/LambdaAppender.java b/aws-lambda-java-log4j2/src/main/java/com/amazonaws/services/lambda/runtime/log4j2/LambdaAppender.java index 5c1dd3158..a511c8dea 100755 --- a/aws-lambda-java-log4j2/src/main/java/com/amazonaws/services/lambda/runtime/log4j2/LambdaAppender.java +++ b/aws-lambda-java-log4j2/src/main/java/com/amazonaws/services/lambda/runtime/log4j2/LambdaAppender.java @@ -2,16 +2,24 @@ import com.amazonaws.services.lambda.runtime.LambdaRuntime; import com.amazonaws.services.lambda.runtime.LambdaRuntimeInternal; + import com.amazonaws.services.lambda.runtime.LambdaLogger; +import com.amazonaws.services.lambda.runtime.logging.LogFormat; +import com.amazonaws.services.lambda.runtime.logging.LogLevel; +import org.apache.logging.log4j.Level; import org.apache.logging.log4j.core.Filter; import org.apache.logging.log4j.core.Layout; import org.apache.logging.log4j.core.LogEvent; import org.apache.logging.log4j.core.appender.AbstractAppender; import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.config.plugins.PluginAttribute; import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory; +import org.apache.logging.log4j.core.config.plugins.PluginElement; import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; /** * Class to append log4j2 logs from AWS Lambda function to CloudWatch @@ -20,6 +28,9 @@ @Plugin(name = LambdaAppender.PLUGIN_NAME, category = LambdaAppender.PLUGIN_CATEGORY, elementType = LambdaAppender.PLUGIN_TYPE, printObject = true) public class LambdaAppender extends AbstractAppender { + static { + LambdaRuntimeInternal.setUseLog4jAppender(true); + } public static final String PLUGIN_NAME = "Lambda"; public static final String PLUGIN_CATEGORY = "Core"; @@ -27,6 +38,17 @@ public class LambdaAppender extends AbstractAppender { private LambdaLogger logger = LambdaRuntime.getLogger(); + private static LogFormat logFormat = LogFormat.TEXT; + + private static final Map logLevelMapper = new HashMap() {{ + put(Level.TRACE, LogLevel.TRACE); + put(Level.DEBUG, LogLevel.DEBUG); + put(Level.INFO, LogLevel.INFO); + put(Level.WARN, LogLevel.WARN); + put(Level.ERROR, LogLevel.ERROR); + put(Level.FATAL, LogLevel.FATAL); + }}; + /** * Builder class that follows log4j2 plugin convention * @param Generic Builder class @@ -34,13 +56,25 @@ public class LambdaAppender extends AbstractAppender { public static class Builder> extends AbstractAppender.Builder implements org.apache.logging.log4j.core.util.Builder { + @PluginAttribute(value = "format", defaultString = "TEXT") + LogFormat logFormat; + @PluginElement("LambdaTextFormat") + private LambdaTextFormat lambdaTextFormat; + @PluginElement("LambdaJsonFormat") + private LambdaJsonFormat lambdaJsonFormat; + /** * creates a new LambdaAppender * @return a new LambdaAppender */ public LambdaAppender build() { - return new LambdaAppender(super.getName(), super.getFilter(), super.getOrCreateLayout(), - super.isIgnoreExceptions()); + Layout layout; + if (logFormat == LogFormat.TEXT) { + layout = lambdaTextFormat != null ? lambdaTextFormat.getLayout() : super.getOrCreateLayout(); + } else { + layout = lambdaJsonFormat != null ? lambdaJsonFormat.getLayout() : super.getOrCreateLayout(); + } + return new LambdaAppender(super.getName(), super.getFilter(), layout, super.isIgnoreExceptions()); } } @@ -63,7 +97,15 @@ public static > B newBuilder() { */ private LambdaAppender(String name, Filter filter, Layout layout, boolean ignoreExceptions) { super(name, filter, layout, ignoreExceptions); - LambdaRuntimeInternal.setUseLog4jAppender(true); + } + + /** + * Converts log4j Level into Lambda LogLevel + * @param level the log4j log level + * @return Lambda log leve + */ + private LogLevel toLambdaLogLevel(Level level) { + return logLevelMapper.getOrDefault(level, LogLevel.UNDEFINED); } /** @@ -71,6 +113,6 @@ private LambdaAppender(String name, Filter filter, Layout layout) { + return new LambdaJsonFormat(layout); + } + + private LambdaJsonFormat(Layout layout) { + this.layout = layout; + } + + public Layout getLayout() { + return layout; + } +} diff --git a/aws-lambda-java-log4j2/src/main/java/com/amazonaws/services/lambda/runtime/log4j2/LambdaTextFormat.java b/aws-lambda-java-log4j2/src/main/java/com/amazonaws/services/lambda/runtime/log4j2/LambdaTextFormat.java new file mode 100644 index 000000000..0bd0304a0 --- /dev/null +++ b/aws-lambda-java-log4j2/src/main/java/com/amazonaws/services/lambda/runtime/log4j2/LambdaTextFormat.java @@ -0,0 +1,29 @@ +/* Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ + +package com.amazonaws.services.lambda.runtime.log4j2; + +import org.apache.logging.log4j.core.Layout; +import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.config.plugins.PluginElement; +import org.apache.logging.log4j.core.config.plugins.PluginFactory; + +import java.io.Serializable; + +@Plugin(name = "LambdaTextFormat", category = "core", printObject = true) +public class LambdaTextFormat { + + private Layout layout; + + @PluginFactory + public static LambdaTextFormat createNode(@PluginElement("Layout") Layout layout) { + return new LambdaTextFormat(layout); + } + + private LambdaTextFormat(Layout layout) { + this.layout = layout; + } + + public Layout getLayout() { + return layout; + } +} diff --git a/aws-lambda-java-log4j2/src/main/resources/LambdaLayout.json b/aws-lambda-java-log4j2/src/main/resources/LambdaLayout.json new file mode 100644 index 000000000..975f4b529 --- /dev/null +++ b/aws-lambda-java-log4j2/src/main/resources/LambdaLayout.json @@ -0,0 +1,39 @@ +{ + "timestamp": { + "$resolver": "timestamp", + "pattern": { + "format": "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", + "timeZone": "UTC" + } + }, + "level": { + "$resolver": "level", + "field": "name" + }, + "message": { + "$resolver": "message" + }, + "logger": { + "$resolver": "logger", + "field": "name" + }, + + "errorType": { + "$resolver": "exception", + "field": "className" + }, + "errorMessage": { + "$resolver": "exception", + "field": "message" + }, + "stackTrace": { + "$resolver": "exception", + "field": "stackTrace" + }, + + "labels": { + "$resolver": "mdc", + "flatten": true, + "stringified": true + } +} \ No newline at end of file diff --git a/aws-lambda-java-log4j2/src/test/java/com/amazonaws/services/lambda/runtime/log4j2/LambdaAppenderPluginTest.java b/aws-lambda-java-log4j2/src/test/java/com/amazonaws/services/lambda/runtime/log4j2/LambdaAppenderPluginTest.java new file mode 100644 index 000000000..0bcd057a6 --- /dev/null +++ b/aws-lambda-java-log4j2/src/test/java/com/amazonaws/services/lambda/runtime/log4j2/LambdaAppenderPluginTest.java @@ -0,0 +1,86 @@ +/* Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ + +package com.amazonaws.services.lambda.runtime.log4j2; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class LambdaAppenderPluginTest { + + private final PrintStream originalOut = System.out; + private ByteArrayOutputStream captured; + + @BeforeEach + void redirectStdout() throws UnsupportedEncodingException { + captured = new ByteArrayOutputStream(); + System.setOut(new PrintStream(captured, true, StandardCharsets.UTF_8.name())); + } + + @AfterEach + void restoreStdout() { + System.setOut(originalOut); + } + + @Test + void lambdaAppenderEmitsLogsAtVariousLevels() throws UnsupportedEncodingException { + Logger logger = LogManager.getLogger(LambdaAppenderPluginTest.class); + + logger.debug("debug-msg"); + logger.info("info-msg"); + logger.warn("warn-msg"); + logger.error("error-msg"); + + String output = captured.toString(StandardCharsets.UTF_8.name()); + + // The PatternLayout in src/test/resources/log4j2.xml is "%-5p %c{1} - %m%n", + // so each event should appear as " LambdaAppenderPluginTest - ". + assertTrue(output.contains("DEBUG LambdaAppenderPluginTest - debug-msg"), + "expected DEBUG line in output but got:\n" + output); + assertTrue(output.contains("INFO LambdaAppenderPluginTest - info-msg"), + "expected INFO line in output but got:\n" + output); + assertTrue(output.contains("WARN LambdaAppenderPluginTest - warn-msg"), + "expected WARN line in output but got:\n" + output); + assertTrue(output.contains("ERROR LambdaAppenderPluginTest - error-msg"), + "expected ERROR line in output but got:\n" + output); + + // Sanity check: log4j should not have fallen back to its default + // ConsoleAppender / status logger error message. + assertFalse(output.contains("ERROR StatusLogger"), + "log4j status logger reported an error, output was:\n" + output); + } + + @Test + void lambdaAppenderEmitsJsonForJsonFormatLogger() throws UnsupportedEncodingException { + // The "json-test" logger is configured in src/test/resources/log4j2.xml + // with additivity=false to a second LambdaAppender using format="JSON" + // and JsonTemplateLayout backed by LambdaLayout.json. + Logger logger = LogManager.getLogger("json-test"); + + logger.info("json-info-msg"); + logger.error("json-error-msg"); + + String output = captured.toString(StandardCharsets.UTF_8.name()); + + assertTrue(output.contains("json-info-msg"), + "expected json-info-msg in output but got:\n" + output); + assertTrue(output.contains("json-error-msg"), + "expected json-error-msg in output but got:\n" + output); + + // Output should look like JSON, not the text PatternLayout from the + // root logger — so it must contain JSON field punctuation around the + // message rather than the "INFO json-test - ..." text pattern. + assertTrue(output.contains("\"message\":\"json-info-msg\""), + "expected JSON-encoded message field but got:\n" + output); + } +} diff --git a/aws-lambda-java-log4j2/src/test/resources/log4j2.xml b/aws-lambda-java-log4j2/src/test/resources/log4j2.xml new file mode 100644 index 000000000..7b43094e2 --- /dev/null +++ b/aws-lambda-java-log4j2/src/test/resources/log4j2.xml @@ -0,0 +1,25 @@ + + + + + + + %-5p %c{1} - %m%n + + + + + + + + + + + + + + + + + + diff --git a/aws-lambda-java-runtime-interface-client/.gitignore b/aws-lambda-java-runtime-interface-client/.gitignore new file mode 100644 index 000000000..f6064106d --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/.gitignore @@ -0,0 +1,5 @@ +compile-flags.txt +ric-dev-environment/codeartifact-properties.mk + +# aws-lambda-cpp prebuilt lib + headers, fetched and staged at build time +src/main/jni/deps/aws-lambda-cpp/ diff --git a/aws-lambda-java-runtime-interface-client/Dockerfile.rie b/aws-lambda-java-runtime-interface-client/Dockerfile.rie new file mode 100644 index 000000000..66a01c834 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/Dockerfile.rie @@ -0,0 +1,8 @@ +FROM public.ecr.aws/lambda/java:21 + +COPY target/aws-lambda-java-runtime-interface-client-*.jar ${LAMBDA_TASK_ROOT}/ +COPY target/aws-lambda-java-core-*.jar ${LAMBDA_TASK_ROOT}/ +COPY target/aws-lambda-java-serialization-*.jar ${LAMBDA_TASK_ROOT}/ +COPY test-handlers/EchoHandler.class ${LAMBDA_TASK_ROOT}/ + +CMD ["EchoHandler::handleRequest"] \ No newline at end of file diff --git a/aws-lambda-java-runtime-interface-client/Makefile b/aws-lambda-java-runtime-interface-client/Makefile new file mode 100644 index 000000000..770671c07 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/Makefile @@ -0,0 +1,110 @@ +x86_64_ALIAS := amd64 +aarch64_ALIAS := arm64 +ARCHITECTURE := $(shell arch) +ARCHITECTURE_ALIAS := $($(shell echo "$(ARCHITECTURE)_ALIAS")) +ARCHITECTURE_ALIAS := $(or $(ARCHITECTURE_ALIAS),amd64) # on any other archs defaulting to amd64 + +# Java 8 does not support passing some args (such add --add-opens) so we need to clear them +ifeq ($(IS_JAVA_8),true) + EXTRA_LOAD_ARG := -DargLineForReflectionTestOnly="" +else + EXTRA_LOAD_ARG := +endif + +# This optional module exports MAVEN_REPO_URL, MAVEN_REPO_USERNAME and MAVEN_REPO_PASSWORD environment variables +# making it possible to publish resulting artifacts to a codeartifact maven repository +-include ric-dev-environment/codeartifact-repo.mk + +.PHONY: target +target: + $(info ${HELP_MESSAGE}) + @exit 0 + +.PHONY: test +test: + mvn test $(EXTRA_LOAD_ARG) + +.PHONY: setup-codebuild-agent +setup-codebuild-agent: + test/integration/codebuild-local/docker-retry.sh docker build --load -t codebuild-agent \ + --build-arg ARCHITECTURE=$(ARCHITECTURE_ALIAS) \ + -f test/integration/codebuild-local/Dockerfile.agent \ + test/integration/codebuild-local + +# Smoke tests are split per-architecture so CI can run each set on a native +# runner. Running the linux/arm64/v8 combos under QEMU on an x86_64 host makes +# `mvn install` recompile curl for aarch64 emulated, which takes ~30 minutes. +.PHONY: test-smoke +test-smoke: test-smoke-x86_64 test-smoke-aarch64 + +.PHONY: test-smoke-x86_64 +test-smoke-x86_64: setup-codebuild-agent + CODEBUILD_IMAGE_TAG=codebuild-agent test/integration/codebuild-local/test_one.sh test/integration/codebuild/buildspec.os.alpine.yml alpine 3.15 corretto11 linux/amd64 + CODEBUILD_IMAGE_TAG=codebuild-agent test/integration/codebuild-local/test_one.sh test/integration/codebuild/buildspec.os.amazoncorretto.yml amazoncorretto amazoncorretto 11 linux/amd64 + +.PHONY: test-smoke-aarch64 +test-smoke-aarch64: setup-codebuild-agent + CODEBUILD_IMAGE_TAG=codebuild-agent test/integration/codebuild-local/test_one.sh test/integration/codebuild/buildspec.os.alpine.yml alpine 3.15 corretto11 linux/arm64/v8 + CODEBUILD_IMAGE_TAG=codebuild-agent test/integration/codebuild-local/test_one.sh test/integration/codebuild/buildspec.os.amazoncorretto.yml amazoncorretto amazoncorretto 11 linux/arm64/v8 + +# BUILDSPEC can point to the buildspec directory (default, runs every OS) or to a +# single buildspec file, which is how CI parallelizes the run across OSes. +BUILDSPEC ?= test/integration/codebuild + +.PHONY: test-integ +test-integ: setup-codebuild-agent + CODEBUILD_IMAGE_TAG=codebuild-agent test/integration/codebuild-local/test_all.sh $(BUILDSPEC) + +# Command to run everytime you make changes to verify everything works +.PHONY: dev +dev: test + +# Verifications to run before sending a pull request +.PHONY: pr +pr: test test-smoke + +# Per-architecture PR checks so CI can run each on a native runner (no QEMU). +.PHONY: pr-x86_64 +pr-x86_64: test test-smoke-x86_64 + +.PHONY: pr-aarch64 +pr-aarch64: test test-smoke-aarch64 + +.PHONY: build +build: build-x86_64 build-aarch64 + +.PHONY: build-x86_64 +build-x86_64: + mvn clean install -DmultiArch=false $(EXTRA_LOAD_ARG) + mvn install -P linux-x86_64 $(EXTRA_LOAD_ARG) + mvn install -P linux_musl-x86_64 $(EXTRA_LOAD_ARG) + +.PHONY: build-aarch64 +build-aarch64: + mvn clean install -DmultiArch=false $(EXTRA_LOAD_ARG) + mvn install -P linux-aarch64 $(EXTRA_LOAD_ARG) + mvn install -P linux_musl-aarch64 $(EXTRA_LOAD_ARG) + +.PHONY: publish +publish: + ./ric-dev-environment/publish_snapshot.sh + +.PHONY: publish +test-publish: + ./ric-dev-environment/test-platform-specific-jar-snapshot.sh + +.PHONY: test-rie +test-rie: + ./scripts/test-rie.sh "EchoHandler::handleRequest" + +define HELP_MESSAGE + +Usage: $ make [TARGETS] + +TARGETS + build Builds the package. + dev Run all development tests after a change. + pr Perform all checks before submitting a Pull Request. + test Run the Unit tests. + test-rie Build and test RIC locally with Lambda Runtime Interface Emulator. (Requires building the project first) +endef diff --git a/aws-lambda-java-runtime-interface-client/README.md b/aws-lambda-java-runtime-interface-client/README.md new file mode 100644 index 000000000..a49bf87b4 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/README.md @@ -0,0 +1,231 @@ +## AWS Lambda Java Runtime Interface Client + +We have open-sourced a set of software packages, Runtime Interface Clients (RIC), that implement the Lambda + [Runtime API](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html), allowing you to seamlessly extend your preferred + base images to be Lambda compatible. +The Lambda Runtime Interface Client is a lightweight interface that allows your runtime to receive requests from and send requests to the Lambda service. + +You can include this package in your preferred base image to make that base image Lambda compatible. + +## Usage + +### Creating a Docker Image for Lambda with the Runtime Interface Client + +Choose a preferred base image. The Runtime Interface Client is tested on Amazon Linux, Alpine, Ubuntu, and Debian. The requirements are that the image is: + +* built for x86_64 and ARM64 +* contains Java >= 8 +* contains glibc >= 2.17 or musl + +### Example + +The Runtime Interface Client library can be installed into the image separate from the function code, but the simplest approach to keeping the Dockerfile simple is to include the library as a part of the function's dependencies! + +Dockerfile +```dockerfile +# we'll use Amazon Linux 2 + Corretto 11 as our base +FROM public.ecr.aws/amazoncorretto/amazoncorretto:11 as base + +# configure the build environment +FROM base as build +RUN yum install -y maven +WORKDIR /src + +# cache and copy dependencies +ADD pom.xml . +RUN mvn dependency:go-offline dependency:copy-dependencies + +# compile the function +ADD . . +RUN mvn package + +# copy the function artifact and dependencies onto a clean base +FROM base +WORKDIR /function + +COPY --from=build /src/target/dependency/*.jar ./ +COPY --from=build /src/target/*.jar ./ + +# configure the runtime startup as main +ENTRYPOINT [ "/usr/bin/java", "-cp", "./*", "com.amazonaws.services.lambda.runtime.api.client.AWSLambda" ] +# pass the name of the function handler as an argument to the runtime +CMD [ "example.App::sayHello" ] +``` +pom.xml +```xml + + 4.0.0 + example + hello-lambda + jar + 1.0-SNAPSHOT + hello-lambda + http://maven.apache.org + + 1.8 + 1.8 + + + + com.amazonaws + aws-lambda-java-runtime-interface-client + 2.11.0 + + + + + + org.apache.maven.plugins + maven-dependency-plugin + 3.1.2 + + + copy-dependencies + package + + copy-dependencies + + + + + + + +``` +src/main/java/example/App.java +```java +package example; + +public class App { + public static String sayHello() { + return "Hello λ!"; + } +} +``` + +### Local Testing + +To make it easy to locally test Lambda functions packaged as container images we open-sourced a lightweight web-server, Lambda Runtime Interface Emulator (RIE), which allows your function packaged as a container image to accept HTTP requests. You can install the [AWS Lambda Runtime Interface Emulator](https://github.com/aws/aws-lambda-runtime-interface-emulator) on your local machine to test your function. Then when you run the image function, you set the entrypoint to be the emulator. + +*To install the emulator and test your Lambda function* + +1) Run the following command to download the RIE from GitHub and install it on your local machine. + +```shell script +mkdir -p ~/.aws-lambda-rie && \ + curl -Lo ~/.aws-lambda-rie/aws-lambda-rie https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie && \ + chmod +x ~/.aws-lambda-rie/aws-lambda-rie +``` +2) Run your Lambda image function using the docker run command. + +```shell script +docker run -d -v ~/.aws-lambda-rie:/aws-lambda -p 9000:8080 \ + --entrypoint /aws-lambda/aws-lambda-rie \ + myfunction:latest \ + /usr/bin/java -cp './*' com.amazonaws.services.lambda.runtime.api.client.AWSLambda example.App::sayHello +``` + +This runs the image as a container and starts up an endpoint locally at `http://localhost:9000/2015-03-31/functions/function/invocations`. + +3) Post an event to the following endpoint using a curl command: + +```shell script +curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{}' +``` + +This command invokes the function running in the container image and returns a response. + +*Alternately, you can also include RIE as a part of your base image. See the AWS documentation on how to [Build RIE into your base image](https://docs.aws.amazon.com/lambda/latest/dg/images-test.html#images-test-alternative).* + +### Automated Local Testing + +For developers working on this runtime interface client, we provide an automated testing script that handles RIE setup, dependency management, and Docker orchestration. + +*Prerequisites:* +- Build the project first: `mvn clean install` +- Docker must be installed and running + +*To run automated tests:* + +```shell script +make test-rie +``` + +This single command will: +- Automatically download required dependencies (aws-lambda-java-core, aws-lambda-java-serialization) +- Build a Docker image with RIE pre-installed +- Compile and run a test Lambda function (EchoHandler) +- Execute the function and validate the response +- Clean up containers automatically + +The test uses a simple EchoHandler that returns the input event, making it easy to verify the runtime interface client is working correctly. + +## Test Coverage + +This project uses JaCoCo for code coverage analysis. To exclude classes from JaCoCo coverage, add them to the `jacoco-maven-plugin` configuration: + +```xml + + org.jacoco + jacoco-maven-plugin + + + **/*Exception.class + **/dto/*.class + **/YourClassName.class + + + +``` + +This project excludes by default: exceptions, interfaces, DTOs, constants, and runtime-only classes. + +### Troubleshooting + +While running integration tests, you might encounter the Docker Hub rate limit error with the following body: +``` +You have reached your pull rate limit. You may increase the limit by authenticating and upgrading: https://www.docker.com/increase-rate-limits +``` +To fix the above issue, consider authenticating to a Docker Hub account by setting the Docker Hub credentials as below CodeBuild environment variables. +```shell script +DOCKERHUB_USERNAME= +DOCKERHUB_PASSWORD= +``` +Recommended way is to set the Docker Hub credentials in CodeBuild job by retrieving them from AWS Secrets Manager. + +## Configuration +The `aws-lambda-java-runtime-interface-client` JAR is a large uber jar, which contains compiled C libraries +for x86_64 and aarch_64 for glibc and musl LIBC implementations. If the size is an issue, you can pick a smaller +platform-specific JAR by setting the ``. +``` + + + com.amazonaws + aws-lambda-java-runtime-interface-client + 2.11.0 + linux-x86_64 + +``` + +Available platform classifiers: `linux-x86_64`, `linux-aarch_64`, `linux_musl-aarch_64`, `linux_musl-x86_64` + +The Lambda runtime interface client tries to load compatible library during execution, by unpacking it to a temporary +location `/tmp/.libaws-lambda-jni.so`. +If this behaviour is not desirable, it is possible to extract the `.so` files during build time and specify the location via +`com.amazonaws.services.lambda.runtime.api.client.runtimeapi.NativeClient.JNI` system property, like +``` +ENTRYPOINT [ "/usr/bin/java", +"-Dcom.amazonaws.services.lambda.runtime.api.client.runtimeapi.NativeClient.JNI=/function/libaws-lambda-jni.linux_x86_64.so" +"-cp", "./*", +"com.amazonaws.services.lambda.runtime.api.client.AWSLambda" ] +``` + +## Security + +If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. + +## License + +This project is licensed under the Apache-2.0 License. + diff --git a/aws-lambda-java-runtime-interface-client/RELEASE.CHANGELOG.md b/aws-lambda-java-runtime-interface-client/RELEASE.CHANGELOG.md new file mode 100644 index 000000000..97d177034 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/RELEASE.CHANGELOG.md @@ -0,0 +1,131 @@ +### July 17, 2026 +`2.12.0` +- Add `Lambda-Runtime-Invocation-Id` header support for cross-wiring protection. The RIC now echoes the invocation ID received from RAPID on `/next` back on `/response` and `/error`, enabling RAPID to detect and reject stale responses from timed-out invocations. + +### May 13, 2026 +`2.11.0` +- Update aws-lambda-java-serialization dependency to 1.4.1 + +### March 19, 2026 +`2.10.1` +- Revert aws-lambda-java-serialization dependency to 1.2.0 + +### March 12, 2026 +`2.10.0` +- Update aws-lambda-java-serialization dependency to 1.3.0 + +### March 12, 2026 +`2.9.0` +- Update aws-lambda-java-serialization dependency to 1.2.0 + +### September 22, 2025 +`2.8.7` +- Remove Minimum and Maximum Limits of AWS_LAMBDA_MAX_CONCURRENCY. + +### September 22, 2025 +`2.8.6` +- Set Multiconcurrent Trace ID using utils-lite. + +### September 17, 2025 +`2.8.5` +- Log errorType and errorMessage from RAPID in C++ Client. +- Performance Upgrade for Multiconcurrency Mode. + +### September 9, 2025 +`2.8.4` +- Make Trace ID Accessible through Context Object. + +### July 19, 2025 +`2.8.3` +- Ensure EventHandlerLoader Thread Safety. + +### June 26, 2025 +`2.8.2` +- Allow AWS_LAMBDA_MAX_CONCURRENCY to be One. Crash the RIC if it is set to an un-parsable string to an integer or an out of bounds value. + +### June 26, 2025 +`2.8.1` +- Refactoring + +### June 26, 2025 +`2.8.0` +- Refactoring + +### May 21, 2025 +`2.7.0` +- Adding support for multi tenancy ([#540](https://github.com/aws/aws-lambda-java-libs/pull/540)) + +### August 7, 2024 +`2.6.0` +- Runtime API client improvements: use Lambda-Runtime-Function-Error-Type for reporting errors in format "Runtime." + +### June 28, 2024 +`2.5.1` +- Runtime API client improvements: fix a DNS cache issue +- Runtime API client improvements: fix circular exception references causing stackOverflow + +### March 20, 2024 +`2.5.0` +- Runtime API client improvements ([#471](https://github.com/aws/aws-lambda-java-libs/pull/471)) + +### February 27, 2024 +`2.4.2` +- Exceptions caught by the runtime are logged as ERROR in JSON mode + +### September 4, 2023 +`2.4.1` +- Null pointer bugfix ([#439](https://github.com/aws/aws-lambda-java-libs/pull/439)) + +### August 29, 2023 +`2.4.0` +- Logging improvements ([#436](https://github.com/aws/aws-lambda-java-libs/pull/436)) + +### July 17, 2023 +`2.3.3` +- Build platform specific JAR files +- NativeClient optimisations + +### April 14, 2023 +`2.3.2` +- Add curl patch + +### March 16, 2023 +`2.3.1` +- ignore module-info for CDS preparation purposes +- clear thread interrupted flag instead of exiting Lambda Runtime + +### March 14, 2023 +`2.3.0` +- added CRaC context implementation +- added runtime hooks execution logic +- updated serialisation dependency +- reduced Reflection API usage + +### February 3, 2023 +`2.2.0` +- Added timestamps to TLV +- Removed legacy `init` method support +- libcurl updated to version 7.86 +- Support sockets as transport for framed telemetry +- Updated aws-lambda-java-core to 1.2.2 + +### April 11, 2022 +`2.1.1` +- fix: Re-build of the x86_64/aarch64 artifacts + +### January 20, 2022 +`2.1.0` +- fix: Added support for ARM64 architecture + +### Sept 29, 2021 +`2.0.0` +- Added support for ARM64 architecture + +### June 02, 2021 +`1.1.0`: +- Added reserved environment variables constants ([#238](https://github.com/aws/aws-lambda-java-libs/pull/238)) +- Updated libcurl dependency to `7.77.0` ([#249](https://github.com/aws/aws-lambda-java-libs/pull/249)) + +### December 01, 2020 +`1.0.0`: +- Initial release of AWS Lambda Java Runtime Interface Client diff --git a/aws-lambda-java-runtime-interface-client/build-tools/checkstyle.xml b/aws-lambda-java-runtime-interface-client/build-tools/checkstyle.xml new file mode 100644 index 000000000..263834dc4 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/build-tools/checkstyle.xml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/aws-lambda-java-runtime-interface-client/pom.xml b/aws-lambda-java-runtime-interface-client/pom.xml new file mode 100644 index 000000000..6db41aa36 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/pom.xml @@ -0,0 +1,505 @@ + + 4.0.0 + com.amazonaws + aws-lambda-java-runtime-interface-client + 2.12.0-SNAPSHOT + jar + + AWS Lambda Java Runtime Interface Client + + The AWS Lambda Java Runtime Interface Client implements the Lambda programming model for Java + + https://aws.amazon.com/lambda/ + + + Apache License, Version 2.0 + https://aws.amazon.com/apache2.0 + repo + + + + https://github.com/aws/aws-lambda-java-libs.git + scm:git:https://github.com/aws/aws-lambda-java-libs.git + scm:git:https://github.com/aws/aws-lambda-java-libs.git + HEAD + + + + AWS Lambda team + Amazon Web Services + https://aws.amazon.com/ + + + + + + UTF-8 + UTF-8 + 0.8.12 + 2.4 + 3.1.1 + 5.12.2 + 3.4.0 + 3.5.4 + + true + + + + + --add-opens java.base/java.net=ALL-UNNAMED + + + + + com.amazonaws + aws-lambda-java-core + 1.4.0 + + + com.amazonaws + aws-lambda-java-serialization + 1.4.1 + + + software.amazon.awssdk + utils-lite + 2.34.0 + + + org.junit.jupiter + junit-jupiter-engine + ${junit-jupiter.version} + test + + + org.junit.jupiter + junit-jupiter + ${junit-jupiter.version} + test + + + org.mockito + mockito-core + 4.11.0 + test + + + org.mockito + mockito-junit-jupiter + 4.11.0 + test + + + com.squareup.okhttp3 + mockwebserver + 4.12.0 + test + + + + + + + com.allogy.maven.wagon + maven-s3-wagon + 1.2.0 + + + + + org.apache.maven.plugins + maven-release-plugin + 3.1.1 + + aws-lambda-java-runtime-interface-client-@{project.version} + true + release + deploy + + + + org.apache.maven.plugins + maven-toolchains-plugin + 3.2.0 + + + + + [1.8,9) + + + + + + + toolchain + + + + + + maven-install-plugin + org.apache.maven.plugins + ${maven-install-plugin.version} + + + maven-deploy-plugin + org.apache.maven.plugins + ${maven-deploy-plugin.version} + + + maven-surefire-plugin + ${maven-surefire-plugin.version} + + ${argLineForReflectionTestOnly} ${argLine} + + + + org.junit.jupiter + junit-jupiter-engine + ${junit-jupiter.version} + + + org.junit.platform + junit-platform-launcher + 1.12.2 + + + + + maven-failsafe-plugin + 2.22.2 + + + org.apache.maven.plugins + maven-antrun-plugin + 1.7 + + + build-jni-lib-for-tests + generate-test-sources + + run + + + + + + + + + + + + + + build-jni-lib + prepare-package + + run + + + + + + + + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 1.8 + 1.8 + UTF-8 + + + + org.apache.maven.plugins + maven-jar-plugin + 3.3.0 + + + + com.amazonaws.services.lambda.runtime.api.client.AWSLambda + true + true + + + ${ric.classifier} + + com/ + jni/*${ric.classifier}.so + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco.maven.plugin.version} + + + + **/*Exception.class + + **/Resource.class + + **/dto/*.class + + **/ReservedRuntimeEnvironmentVariables.class + **/RapidErrorType.class + + **/FrameType.class + **/StructuredLogMessage.class + + **/AWSLambda.class + + + + + default-prepare-agent + + prepare-agent + + + + default-report + test + + report + + + + default-check + test + + check + + + + + BUNDLE + + + LINE + COVEREDRATIO + 0.5 + + + + + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + ${maven-checkstyle-plugin.version} + + build-tools/checkstyle.xml + true + true + true + + + + validate + validate + + check + + + + + + + + + + + dev + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.9.1 + + -Xdoclint:none + + + + attach-javadocs + + jar + + + + + + + + + release + + + + org.apache.maven.plugins + maven-source-plugin + 2.2.1 + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.9.1 + + -Xdoclint:none + + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.5 + + + sign-artifacts + verify + + sign + + + + + + org.sonatype.central + central-publishing-maven-plugin + 0.8.0 + true + + central + false + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.4.0 + + + attach-platform-artifacts + package + + attach-artifact + + + + + ${project.build.directory}/${project.build.finalName}-linux-x86_64.jar + jar + linux-x86_64 + + + ${project.build.directory}/${project.build.finalName}-linux-aarch_64.jar + jar + linux-aarch_64 + + + ${project.build.directory}/${project.build.finalName}-linux_musl-x86_64.jar + jar + linux_musl-x86_64 + + + ${project.build.directory}/${project.build.finalName}-linux_musl-aarch_64.jar + jar + linux_musl-aarch_64 + + + + + + + + + + + ci-repo + + + ci-repo + ${env.MAVEN_REPO_URL} + + + + + linux-x86_64 + + linux + x86_64 + linux-x86_64 + + + + linux_musl-x86_64 + + linux_musl + x86_64 + linux_musl-x86_64 + + + + linux-aarch64 + + linux + aarch_64 + linux-aarch_64 + + + + linux_musl-aarch64 + + linux_musl + aarch_64 + linux_musl-aarch_64 + + + + diff --git a/aws-lambda-java-runtime-interface-client/ric-dev-environment/codeartifact-repo.mk b/aws-lambda-java-runtime-interface-client/ric-dev-environment/codeartifact-repo.mk new file mode 100644 index 000000000..022c49e79 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/ric-dev-environment/codeartifact-repo.mk @@ -0,0 +1,27 @@ + +ifneq ("$(wildcard ric-dev-environment/codeartifact-properties.mk)","") + + include ric-dev-environment/codeartifact-properties.mk + $(info Found codeartifact-properties.mk module) + + export MAVEN_REPO_URL:=$(shell aws codeartifact get-repository-endpoint \ + --domain ${CODE_ARTIFACT_DOMAIN} \ + --repository ${CODE_ARTIFACT_REPO_NAME} \ + --format maven \ + --output text \ + --region ${CODE_ARTIFACT_REPO_REGION}) + + export MAVEN_REPO_PASSWORD:=$(shell aws codeartifact get-authorization-token \ + --domain ${CODE_ARTIFACT_DOMAIN} \ + --domain-owner ${CODE_ARTIFACT_REPO_ACCOUNT} \ + --query authorizationToken \ + --output text \ + --region ${CODE_ARTIFACT_REPO_REGION}) + + export MAVEN_REPO_USERNAME:=aws + + $(info MAVEN_REPO_URL: $(MAVEN_REPO_URL)) + # $(info MAVEN_REPO_PASSWORD: $(MAVEN_REPO_PASSWORD)) + $(info MAVEN_REPO_USERNAME: $(MAVEN_REPO_USERNAME)) + $(info CODE_ARTIFACT_REPO_NAME: $(CODE_ARTIFACT_REPO_NAME)) +endif diff --git a/aws-lambda-java-runtime-interface-client/ric-dev-environment/publish_snapshot.sh b/aws-lambda-java-runtime-interface-client/ric-dev-environment/publish_snapshot.sh new file mode 100755 index 000000000..9d2f9837f --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/ric-dev-environment/publish_snapshot.sh @@ -0,0 +1,54 @@ +#!/bin/bash -x + +set -e + +projectVersion=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) +if [[ -z ${ENABLE_SNAPSHOT} ]]; then + echo "Skipping SNAPSHOT deployment, as ENABLE_SNAPSHOT environment variable is not defined" + exit +fi + +echo "Deploying SNAPSHOT artifact" +if [[ ${projectVersion} != *"SNAPSHOT"* ]]; then + snapshotProjectVersion="${projectVersion}-SNAPSHOT" + echo "projectVersion: ${projectVersion}" + echo "snapshotProjectVersion: ${snapshotProjectVersion}" + mvn versions:set "-DnewVersion=${snapshotProjectVersion}" +else + echo "Already -SNAPSHOT version" +fi + +# get the updated project version +snapshotProjectVersion=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) +echo "Updated project version is ${snapshotProjectVersion}" + +CLASSIFIERS_ARRAY=("linux-x86_64" "linux_musl-x86_64" "linux-aarch_64" "linux_musl-aarch_64") + +for str in "${CLASSIFIERS_ARRAY[@]}"; do + FILES="${FILES}target/aws-lambda-java-runtime-interface-client-$projectVersion-$str.jar," + CLASSIFIERS="${CLASSIFIERS}${str}," + TYPES="${TYPES}jar," +done + +# remove the last "," +FILES=${FILES%?} +CLASSIFIERS=${CLASSIFIERS%?} +TYPES=${TYPES%?} + +mvn -B -X -P ci-repo \ + deploy:deploy-file \ + -DgroupId=com.amazonaws \ + -DartifactId=aws-lambda-java-runtime-interface-client \ + -Dpackaging=jar \ + -Dversion=$snapshotProjectVersion \ + -Dfile=./target/aws-lambda-java-runtime-interface-client-$projectVersion.jar \ + -Dfiles=$FILES \ + -Dclassifiers=$CLASSIFIERS \ + -Dtypes=$TYPES \ + -DpomFile=pom.xml \ + -DrepositoryId=ci-repo -Durl=$MAVEN_REPO_URL \ + --settings ric-dev-environment/settings.xml + +if [ -f pom.xml.versionsBackup ]; then + mv pom.xml.versionsBackup pom.xml +fi diff --git a/aws-lambda-java-runtime-interface-client/ric-dev-environment/settings.xml b/aws-lambda-java-runtime-interface-client/ric-dev-environment/settings.xml new file mode 100644 index 000000000..d6f38929b --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/ric-dev-environment/settings.xml @@ -0,0 +1,20 @@ + + + + dev-ci + + + ci-repo + ${env.MAVEN_REPO_URL} + + + + + + + ci-repo + ${env.MAVEN_REPO_USERNAME} + ${env.MAVEN_REPO_PASSWORD} + + + diff --git a/aws-lambda-java-runtime-interface-client/ric-dev-environment/test-platform-specific-jar-snapshot.sh b/aws-lambda-java-runtime-interface-client/ric-dev-environment/test-platform-specific-jar-snapshot.sh new file mode 100755 index 000000000..c9eced5cb --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/ric-dev-environment/test-platform-specific-jar-snapshot.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +set -e + +projectVersion=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) + + +# test uber jar +mvn -B -X -P ci-repo \ + dependency:get \ + -DremoteRepositories=ci-repo::::$MAVEN_REPO_URL \ + -Dartifact=com.amazonaws:aws-lambda-java-runtime-interface-client:${projectVersion}-SNAPSHOT \ + -Dtransitive=false \ + --settings ric-dev-environment/settings.xml + + +PLATFORM_ARRAY=("linux-x86_64" "linux_musl-x86_64" "linux-aarch_64" "linux_musl-aarch_64") + +for classifier in "${PLATFORM_ARRAY[@]}"; do + # Test platform specific jar + mvn -B -P ci-repo \ + dependency:get \ + -DremoteRepositories=ci-repo::::$MAVEN_REPO_URL \ + -Dartifact=com.amazonaws:aws-lambda-java-runtime-interface-client:${projectVersion}-SNAPSHOT:jar:${classifier} \ + -Dtransitive=false \ + --settings ric-dev-environment/settings.xml +done \ No newline at end of file diff --git a/aws-lambda-java-runtime-interface-client/scripts/test-rie.sh b/aws-lambda-java-runtime-interface-client/scripts/test-rie.sh new file mode 100755 index 000000000..b69c967a1 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/scripts/test-rie.sh @@ -0,0 +1,46 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" +SERIALIZATION_ROOT="$(dirname "$PROJECT_ROOT")/aws-lambda-java-serialization" + +if ! ls "$PROJECT_ROOT"/target/aws-lambda-java-runtime-interface-client-*.jar >/dev/null 2>&1; then + echo "RIC jar not found. Please build the project first with 'mvn package'." + exit 1 +fi + +IMAGE_TAG="java-ric-rie-test" + +HANDLER="${1:-EchoHandler::handleRequest}" + +echo "Starting RIE test setup for Java..." + +# Build local dependencies if not present +CORE_ROOT="$(dirname "$PROJECT_ROOT")/aws-lambda-java-core" +if ! ls "$PROJECT_ROOT"/target/aws-lambda-java-core-*.jar >/dev/null 2>&1; then + echo "Building local aws-lambda-java-core..." + (cd "$CORE_ROOT" && mvn package -DskipTests) + cp "$CORE_ROOT"/target/aws-lambda-java-core-*.jar "$PROJECT_ROOT/target/" +fi + +if ! ls "$PROJECT_ROOT"/target/aws-lambda-java-serialization-*.jar >/dev/null 2>&1; then + echo "Building local aws-lambda-java-serialization..." + (cd "$SERIALIZATION_ROOT" && mvn package -DskipTests) + cp "$SERIALIZATION_ROOT"/target/aws-lambda-java-serialization-*.jar "$PROJECT_ROOT/target/" +fi + +echo "Compiling EchoHandler..." +javac -source 21 -target 21 -cp "$(ls "$PROJECT_ROOT"/target/aws-lambda-java-runtime-interface-client-*.jar):$(ls "$PROJECT_ROOT"/target/aws-lambda-java-core-*.jar):$(ls "$PROJECT_ROOT"/target/aws-lambda-java-serialization-*.jar)" \ + -d "$PROJECT_ROOT/test-handlers/" "$PROJECT_ROOT/test-handlers/EchoHandler.java" + +echo "Building test Docker image..." +docker build -t "$IMAGE_TAG" -f "$PROJECT_ROOT/Dockerfile.rie" "$PROJECT_ROOT" + +echo "Starting test container on port 9000..." +echo "" +echo "In another terminal, invoke with:" +echo "curl -s -X POST -H 'Content-Type: application/json' \"http://localhost:9000/2015-03-31/functions/function/invocations\" -d '{\"message\":\"test\"}'" +echo "" + +exec docker run -it -p 9000:8080 -e _HANDLER="$HANDLER" "$IMAGE_TAG" \ No newline at end of file diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/crac/CheckpointException.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/crac/CheckpointException.java new file mode 100644 index 000000000..f802ad5f7 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/crac/CheckpointException.java @@ -0,0 +1,12 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.crac; + +public class CheckpointException extends Exception { + private static final long serialVersionUID = -4956873658083157585L; + public CheckpointException() { + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/crac/Context.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/crac/Context.java new file mode 100644 index 000000000..d62ef0143 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/crac/Context.java @@ -0,0 +1,22 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.crac; + +public abstract class Context implements Resource { + + protected Context() { + } + + @Override + public abstract void beforeCheckpoint(Context context) + throws CheckpointException; + + @Override + public abstract void afterRestore(Context context) + throws RestoreException; + + public abstract void register(R resource); +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/crac/ContextImpl.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/crac/ContextImpl.java new file mode 100644 index 000000000..04b1436a8 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/crac/ContextImpl.java @@ -0,0 +1,96 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.crac; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.WeakHashMap; +import java.util.stream.Collectors; + + +/** + * Spec reference: https://crac.github.io/openjdk-builds/javadoc/api/java.base/jdk/crac/package-summary.html + */ + +public class ContextImpl extends Context { + + private volatile long order = -1L; + private final WeakHashMap checkpointQueue = new WeakHashMap<>(); + + @Override + public synchronized void beforeCheckpoint(Context context) throws CheckpointException { + executeBeforeCheckpointHooks(); + DNSManager.clearCache(); + System.gc(); + } + + @Override + public synchronized void afterRestore(Context context) throws RestoreException { + + List exceptionsThrown = new ArrayList<>(); + for (Resource resource : getCheckpointQueueForwardOrderOfRegistration()) { + try { + resource.afterRestore(this); + } catch (RestoreException e) { + Collections.addAll(exceptionsThrown, e.getSuppressed()); + } catch (Exception e) { + exceptionsThrown.add(e); + } + } + + if (!exceptionsThrown.isEmpty()) { + RestoreException restoreException = new RestoreException(); + for (Throwable t : exceptionsThrown) { + restoreException.addSuppressed(t); + } + throw restoreException; + } + } + + @Override + public synchronized void register(Resource resource) { + checkpointQueue.put(resource, ++order); + } + + private List getCheckpointQueueReverseOrderOfRegistration() { + return checkpointQueue.entrySet(). + stream(). + sorted((r1, r2) -> (int) (r2.getValue() - r1.getValue())). + map(Map.Entry::getKey). + collect(Collectors.toList()); + } + + private List getCheckpointQueueForwardOrderOfRegistration() { + return checkpointQueue.entrySet(). + stream(). + sorted((r1, r2) -> (int) (r1.getValue() - r2.getValue())). + map(Map.Entry::getKey). + collect(Collectors.toList()); + } + + private void executeBeforeCheckpointHooks() throws CheckpointException { + List exceptionsThrown = new ArrayList<>(); + for (Resource resource : getCheckpointQueueReverseOrderOfRegistration()) { + try { + resource.beforeCheckpoint(this); + } catch (CheckpointException e) { + Collections.addAll(exceptionsThrown, e.getSuppressed()); + } catch (Exception e) { + exceptionsThrown.add(e); + } + } + + if (!exceptionsThrown.isEmpty()) { + CheckpointException checkpointException = new CheckpointException(); + for (Throwable t : exceptionsThrown) { + checkpointException.addSuppressed(t); + } + throw checkpointException; + } + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/crac/Core.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/crac/Core.java new file mode 100644 index 000000000..7e0b24a2d --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/crac/Core.java @@ -0,0 +1,29 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.crac; + +/** + * Provides the global context for registering resources. + */ +public final class Core { + + private static Context globalContext = new ContextImpl(); + + private Core() { + } + + public static Context getGlobalContext() { + return globalContext; + } + + public static void checkpointRestore() { + throw new UnsupportedOperationException(); + } + + static void resetGlobalContext() { + globalContext = new ContextImpl(); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/crac/DNSManager.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/crac/DNSManager.java new file mode 100644 index 000000000..6c485ec80 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/crac/DNSManager.java @@ -0,0 +1,10 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.crac; + +class DNSManager { + static native void clearCache(); +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/crac/Resource.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/crac/Resource.java new file mode 100644 index 000000000..7ef933202 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/crac/Resource.java @@ -0,0 +1,12 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.crac; + +public interface Resource { + void afterRestore(Context context) throws Exception; + + void beforeCheckpoint(Context context) throws Exception; +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/crac/RestoreException.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/crac/RestoreException.java new file mode 100644 index 000000000..cef38e00f --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/crac/RestoreException.java @@ -0,0 +1,13 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.crac; + +public class RestoreException extends Exception { + private static final long serialVersionUID = -823900409868237860L; + + public RestoreException() { + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/AWSLambda.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/AWSLambda.java new file mode 100644 index 000000000..b9aa0fd11 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/AWSLambda.java @@ -0,0 +1,376 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client; + +import com.amazonaws.services.lambda.crac.Core; +import com.amazonaws.services.lambda.runtime.LambdaLogger; +import com.amazonaws.services.lambda.runtime.api.client.LambdaRequestHandler.UserFaultHandler; +import com.amazonaws.services.lambda.runtime.api.client.logging.FramedTelemetryLogSink; +import com.amazonaws.services.lambda.runtime.api.client.logging.LambdaContextLogger; +import com.amazonaws.services.lambda.runtime.api.client.logging.LogSink; +import com.amazonaws.services.lambda.runtime.api.client.logging.StdOutLogSink; +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.LambdaError; +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.LambdaRuntimeApiClient; +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.LambdaRuntimeApiClientImpl; +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.LambdaRuntimeClientMaxRetriesExceededException; +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.RapidErrorType; +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.converters.LambdaErrorConverter; +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.converters.XRayErrorCauseConverter; +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.InvocationRequest; +import com.amazonaws.services.lambda.runtime.api.client.util.ConcurrencyConfig; +import com.amazonaws.services.lambda.runtime.api.client.util.LambdaOutputStream; +import com.amazonaws.services.lambda.runtime.api.client.util.UnsafeUtil; +import com.amazonaws.services.lambda.runtime.logging.LogFormat; +import com.amazonaws.services.lambda.runtime.logging.LogLevel; +import com.amazonaws.services.lambda.runtime.serialization.util.ReflectUtil; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.IOError; +import java.io.IOException; +import java.io.PrintStream; +import java.lang.reflect.Constructor; +import java.net.URLClassLoader; +import java.security.Security; +import java.util.Properties; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import software.amazon.awssdk.utilslite.SdkInternalThreadLocal; + +/** + * The entrypoint of this class is {@link AWSLambda#startRuntime}. It performs two main tasks: + * + *

+ * 1. loads the user's handler. + *
+ * 2. enters the Lambda runtime loop which handles function invocations as defined in the Lambda Custom Runtime API. + * + *

+ * Once initialized, {@link AWSLambda#startRuntime} will halt only if an irrecoverable error occurs. + */ +public class AWSLambda { + + private static URLClassLoader customerClassLoader; + + private static final String TRUST_STORE_PROPERTY = "javax.net.ssl.trustStore"; + + private static final String JAVA_SECURITY_PROPERTIES = "java.security.properties"; + + private static final String NETWORKADDRESS_CACHE_NEGATIVE_TTL_ENV_VAR = "AWS_LAMBDA_JAVA_NETWORKADDRESS_CACHE_NEGATIVE_TTL"; + + private static final String NETWORKADDRESS_CACHE_NEGATIVE_TTL_PROPERTY = "networkaddress.cache.negative.ttl"; + + private static final String DEFAULT_NEGATIVE_CACHE_TTL = "1"; + + // System property for Lambda tracing, see aws-xray-sdk-java/LambdaSegmentContext + // https://github.com/aws/aws-xray-sdk-java/blob/2f467e50db61abb2ed2bd630efc21bddeabd64d9/aws-xray-recorder-sdk-core/src/main/java/com/amazonaws/xray/contexts/LambdaSegmentContext.java#L39-L40 + private static final String LAMBDA_TRACE_HEADER_PROP = "com.amazonaws.xray.traceHeader"; + + private static final String INIT_TYPE_SNAP_START = "snap-start"; + + private static final String AWS_LAMBDA_INITIALIZATION_TYPE = System.getenv(ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_INITIALIZATION_TYPE); + + private static final String CONCURRENT_TRACE_ID_KEY = "AWS_LAMBDA_X_TRACE_ID"; + + static { + // Override the disabledAlgorithms setting to match configuration for openjdk8-u181. + // This is to keep DES ciphers around while we deploying security updates. + Security.setProperty( + "jdk.tls.disabledAlgorithms", + "SSLv3, RC4, MD5withRSA, DH keySize < 1024, EC keySize < 224, DES40_CBC, RC4_40, 3DES_EDE_CBC" + ); + // Override the location of the trusted certificate authorities to be provided by the system. + // The ca-certificates package provides /etc/pki/java/cacerts which becomes the symlink destination + // of $java_home/lib/security/cacerts when java is installed in the chroot. Given that java is provided + // in /var/lang as opposed to installed in the chroot, this brings it closer. + if (System.getProperty(TRUST_STORE_PROPERTY) == null) { + final File systemCacerts = new File("/etc/pki/java/cacerts"); + if (systemCacerts.exists() && systemCacerts.isFile()) { + System.setProperty(TRUST_STORE_PROPERTY, systemCacerts.getPath()); + } + } + + if (isNegativeCacheOverridable()) { + String ttlFromEnv = System.getenv(NETWORKADDRESS_CACHE_NEGATIVE_TTL_ENV_VAR); + String negativeCacheTtl = ttlFromEnv == null ? DEFAULT_NEGATIVE_CACHE_TTL : ttlFromEnv; + Security.setProperty(NETWORKADDRESS_CACHE_NEGATIVE_TTL_PROPERTY, negativeCacheTtl); + } + } + + private static boolean isNegativeCacheOverridable() { + String securityPropertiesPath = System.getProperty(JAVA_SECURITY_PROPERTIES); + if (securityPropertiesPath == null) { + return true; + } + try (FileInputStream inputStream = new FileInputStream(securityPropertiesPath)) { + Properties secProps = new Properties(); + secProps.load(inputStream); + return !secProps.containsKey(NETWORKADDRESS_CACHE_NEGATIVE_TTL_PROPERTY); + } catch (IOException e) { + return true; + } + } + + private static LambdaRequestHandler findRequestHandler(final String handlerString, ClassLoader customerClassLoader) { + final HandlerInfo handlerInfo; + try { + handlerInfo = HandlerInfo.fromString(handlerString, customerClassLoader); + } catch (HandlerInfo.InvalidHandlerException e) { + UserFault userFault = UserFault.makeUserFault("Invalid handler: `" + handlerString + "'"); + return new UserFaultHandler(userFault); + } catch (ClassNotFoundException e) { + return LambdaRequestHandler.classNotFound(e, HandlerInfo.className(handlerString)); + } catch (NoClassDefFoundError e) { + return LambdaRequestHandler.initErrorHandler(e, HandlerInfo.className(handlerString)); + } catch (Throwable t) { + throw UserFault.makeInitErrorUserFault(t, HandlerInfo.className(handlerString)); + } + + final LambdaRequestHandler requestHandler = EventHandlerLoader.loadEventHandler(handlerInfo); + // if loading the handler failed and the failure is fatal (for e.g. the constructor threw an exception) + // we want to report this as an init error rather than deferring to the first invoke. + if (requestHandler instanceof UserFaultHandler) { + UserFault userFault = ((UserFaultHandler) requestHandler).fault; + if (userFault.fatal) { + throw userFault; + } + } + return requestHandler; + } + + private static LambdaRequestHandler getLambdaRequestHandlerObject(String handler, LambdaContextLogger lambdaLogger, LambdaRuntimeApiClient runtimeClient) throws ClassNotFoundException, IOException { + UnsafeUtil.disableIllegalAccessWarning(); + + System.setOut(new PrintStream(new LambdaOutputStream(System.out), false, "UTF-8")); + System.setErr(new PrintStream(new LambdaOutputStream(System.err), false, "UTF-8")); + setupRuntimeLogger(lambdaLogger); + + String taskRoot = System.getProperty("user.dir"); + String libRoot = "/opt/java"; + // Make system classloader the customer classloader's parent to ensure any aws-lambda-java-core classes + // are loaded from the system classloader. + customerClassLoader = new CustomerClassLoader(taskRoot, libRoot, ClassLoader.getSystemClassLoader()); + Thread.currentThread().setContextClassLoader(customerClassLoader); + + // Load the user's handler + LambdaRequestHandler requestHandler = null; + try { + requestHandler = findRequestHandler(handler, customerClassLoader); + } catch (UserFault userFault) { + lambdaLogger.log(userFault.reportableError(), lambdaLogger.getLogFormat() == LogFormat.JSON ? LogLevel.ERROR : LogLevel.UNDEFINED); + LambdaError error = new LambdaError( + LambdaErrorConverter.fromUserFault(userFault), + RapidErrorType.BadFunctionCode); + runtimeClient.reportInitError(error); + System.exit(1); + } + + if (INIT_TYPE_SNAP_START.equals(AWS_LAMBDA_INITIALIZATION_TYPE)) { + onInitComplete(lambdaLogger, runtimeClient); + } + + return requestHandler; + } + + private static void setupRuntimeLogger(LambdaLogger lambdaLogger) + throws ClassNotFoundException { + ReflectUtil.setStaticField( + Class.forName("com.amazonaws.services.lambda.runtime.LambdaRuntime"), + "logger", + true, + lambdaLogger + ); + } + + /** + * convert an integer into a FileDescriptor object using reflection to access private members. + */ + private static FileDescriptor intToFd(int fd) throws RuntimeException { + try { + Class clazz = FileDescriptor.class; + Constructor c = clazz.getDeclaredConstructor(Integer.TYPE); + c.setAccessible(true); + return c.newInstance(fd); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static LogSink createLogSink() { + final String fdStr = System.getenv("_LAMBDA_TELEMETRY_LOG_FD"); + if (fdStr == null) { + return new StdOutLogSink(); + } + + try { + int fdInt = Integer.parseInt(fdStr); + FileDescriptor fd = intToFd(fdInt); + return new FramedTelemetryLogSink(fd); + } catch (Exception e) { + return new StdOutLogSink(); + } + } + + public static void main(String[] args) throws Throwable { + try (LambdaContextLogger lambdaLogger = initLogger()) { + LambdaRuntimeApiClient runtimeClient = new LambdaRuntimeApiClientImpl(LambdaEnvironment.RUNTIME_API); + LambdaRequestHandler lambdaRequestHandler = getLambdaRequestHandlerObject(args[0], lambdaLogger, runtimeClient); + ConcurrencyConfig concurrencyConfig = new ConcurrencyConfig(lambdaLogger); + startRuntimeLoops(lambdaRequestHandler, lambdaLogger, concurrencyConfig, runtimeClient); + } catch (IOException | ClassNotFoundException t) { + throw new Error(t); + } + } + + private static LambdaContextLogger initLogger() { + LogSink logSink = createLogSink(); + LambdaContextLogger logger = new LambdaContextLogger( + logSink, + LogLevel.fromString(LambdaEnvironment.LAMBDA_LOG_LEVEL), + LogFormat.fromString(LambdaEnvironment.LAMBDA_LOG_FORMAT)); + + return logger; + } + + private static void startRuntimeLoopWithExecutor(LambdaRequestHandler lambdaRequestHandler, LambdaContextLogger lambdaLogger, ExecutorService executorService, LambdaRuntimeApiClient runtimeClient) { + executorService.submit(() -> { + try { + startRuntimeLoop(lambdaRequestHandler, lambdaLogger, runtimeClient, false); + } catch (Exception e) { + lambdaLogger.log(String.format("Runtime Loop on Thread ID: %s Failed.\n%s", Thread.currentThread().getName(), UserFault.trace(e)), lambdaLogger.getLogFormat() == LogFormat.JSON ? LogLevel.ERROR : LogLevel.UNDEFINED); + } + }); + } + + protected static void startRuntimeLoops(LambdaRequestHandler lambdaRequestHandler, LambdaContextLogger lambdaLogger, ConcurrencyConfig concurrencyConfig, LambdaRuntimeApiClient runtimeClient) throws Exception { + if (concurrencyConfig.isMultiConcurrent()) { + lambdaLogger.log(concurrencyConfig.getConcurrencyConfigMessage(), lambdaLogger.getLogFormat() == LogFormat.JSON ? LogLevel.INFO : LogLevel.UNDEFINED); + ExecutorService platformThreadExecutor = Executors.newFixedThreadPool(concurrencyConfig.getNumberOfPlatformThreads()); + try { + for (int i = 0; i < concurrencyConfig.getNumberOfPlatformThreads(); i++) { + startRuntimeLoopWithExecutor(lambdaRequestHandler, lambdaLogger, platformThreadExecutor, runtimeClient); + } + } finally { + platformThreadExecutor.shutdown(); + try { + platformThreadExecutor.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } else { + startRuntimeLoop(lambdaRequestHandler, lambdaLogger, runtimeClient, true); + } + } + + private static LambdaError createLambdaErrorFromThrowableOrUserFault(Throwable t) { + if (t instanceof UserFault) { + return new LambdaError( + LambdaErrorConverter.fromUserFault((UserFault) t), + RapidErrorType.BadFunctionCode); + } else { + return new LambdaError( + LambdaErrorConverter.fromThrowable(t), + XRayErrorCauseConverter.fromThrowable(t), + RapidErrorType.UserException); + } + } + + private static void setEnvVarForXrayTraceId(InvocationRequest request) { + if (request.getXrayTraceId() != null) { + System.setProperty(LAMBDA_TRACE_HEADER_PROP, request.getXrayTraceId()); + } else { + System.clearProperty(LAMBDA_TRACE_HEADER_PROP); + } + } + + private static void reportNonLoopTerminatingException(LambdaContextLogger lambdaLogger, Throwable t) { + lambdaLogger.log( + String.format( + "Runtime Loop on Thread ID: %s Faced and Exception. This exception will not stop the runtime loop.\nException:\n%s", + Thread.currentThread().getName(), UserFault.trace(t)), + lambdaLogger.getLogFormat() == LogFormat.JSON ? LogLevel.ERROR : LogLevel.UNDEFINED); + } + + /* + * In multiconcurrent mode (exitLoopOnErrors = false), The Runtime Loop will not exit unless LambdaRuntimeClientMaxRetriesExceededException is thrown when calling nextInvocationWithExponentialBackoff. + * In normal/sequential mode (exitLoopOnErrors = true), The Runtime Loop will exit if nextInvocation call fails, when UserFault is fatal, or an Error of type VirtualMachineError or IOError is thrown. + */ + private static void startRuntimeLoop(LambdaRequestHandler lambdaRequestHandler, LambdaContextLogger lambdaLogger, LambdaRuntimeApiClient runtimeClient, boolean exitLoopOnErrors) throws Exception { + boolean shouldExit = false; + while (!shouldExit) { + try { + UserFault userFault = null; + InvocationRequest request = exitLoopOnErrors ? runtimeClient.nextInvocation() : runtimeClient.nextInvocationWithExponentialBackoff(lambdaLogger); + if (exitLoopOnErrors) { + setEnvVarForXrayTraceId(request); + } else { + SdkInternalThreadLocal.put(CONCURRENT_TRACE_ID_KEY, request.getXrayTraceId()); + } + + try { + ByteArrayOutputStream payload = lambdaRequestHandler.call(request); + runtimeClient.reportInvocationSuccess(request.getId(), payload.toByteArray(), request.getInvocationId()); + // clear interrupted flag in case if it was set by user's code + Thread.interrupted(); + } catch (Throwable t) { + UserFault.filterStackTrace(t); + userFault = UserFault.makeUserFault(t); + shouldExit = exitLoopOnErrors && (t instanceof VirtualMachineError || t instanceof IOError || userFault.fatal); + LambdaError error = createLambdaErrorFromThrowableOrUserFault(t); + runtimeClient.reportInvocationError(request.getId(), error, request.getInvocationId()); + } finally { + if (userFault != null) { + lambdaLogger.log(userFault.reportableError(), lambdaLogger.getLogFormat() == LogFormat.JSON ? LogLevel.ERROR : LogLevel.UNDEFINED); + } + + SdkInternalThreadLocal.remove(CONCURRENT_TRACE_ID_KEY); + } + } catch (Throwable t) { + if (exitLoopOnErrors || t instanceof LambdaRuntimeClientMaxRetriesExceededException) { + throw t; + } + + reportNonLoopTerminatingException(lambdaLogger, t); + } + } + } + + private static void onInitComplete(final LambdaContextLogger lambdaLogger, LambdaRuntimeApiClient runtimeClient) throws IOException { + try { + Core.getGlobalContext().beforeCheckpoint(null); + runtimeClient.restoreNext(); + } catch (Exception e1) { + logExceptionCloudWatch(lambdaLogger, e1); + runtimeClient.reportInitError(new LambdaError( + LambdaErrorConverter.fromThrowable(e1), + RapidErrorType.BeforeCheckpointError)); + System.exit(64); + } + + try { + Core.getGlobalContext().afterRestore(null); + } catch (Exception restoreExc) { + logExceptionCloudWatch(lambdaLogger, restoreExc); + runtimeClient.reportRestoreError(new LambdaError( + LambdaErrorConverter.fromThrowable(restoreExc), + RapidErrorType.AfterRestoreError)); + System.exit(64); + } + } + + private static void logExceptionCloudWatch(LambdaContextLogger lambdaLogger, Exception exc) { + UserFault.filterStackTrace(exc); + UserFault userFault = UserFault.makeUserFault(exc, true); + lambdaLogger.log(userFault.reportableError(), lambdaLogger.getLogFormat() == LogFormat.JSON ? LogLevel.ERROR : LogLevel.UNDEFINED); + } + + protected static URLClassLoader getCustomerClassLoader() { + return customerClassLoader; + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/ClasspathLoader.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/ClasspathLoader.java new file mode 100644 index 000000000..4204f3010 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/ClasspathLoader.java @@ -0,0 +1,95 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +/** + * This class loads all of the classes that are in jars on the classpath. + *

+ * It is used to generate a class list and Application CDS archive that includes all the possible classes that could be + * loaded by the runtime. This simplifies the process of generating the Application CDS archive. + */ +public class ClasspathLoader { + + private static final Set BLOCKLIST = new HashSet<>(); + private static final ClassLoader SYSTEM_CLASS_LOADER = ClassLoader.getSystemClassLoader(); + private static final int CLASS_SUFFIX_LEN = ".class".length(); + + static { + // Ignore module info class for serialization lib + BLOCKLIST.add("META-INF.versions.9.module-info"); + } + + private static String pathToClassName(final String path) { + return path.substring(0, path.length() - CLASS_SUFFIX_LEN).replaceAll("/|\\\\", "\\."); + } + + private static void loadClass(String name) { + try { + Class.forName(name, true, SYSTEM_CLASS_LOADER); + System.out.println("Loaded " + name); + } catch (ClassNotFoundException e) { + System.err.println("[WARN] Failed to load " + name + ": " + e.getMessage()); + } + } + + private static void loadClassesInJar(File file) throws IOException { + JarFile jar = new JarFile(file); + Enumeration en = jar.entries(); + while (en.hasMoreElements()) { + JarEntry entry = en.nextElement(); + + if (!entry.getName().endsWith(".class")) { + continue; + } + + String name = pathToClassName(entry.getName()); + + if (BLOCKLIST.contains(name)) { + continue; + } + + loadClass(name); + } + } + + private static void loadClassesInClasspathEntry(String entry) throws IOException { + File file = new File(entry); + + if (!file.exists()) { + throw new FileNotFoundException("Classpath entry does not exist: " + file.getPath()); + } + + if (file.isDirectory() || !file.getPath().endsWith(".jar")) { + System.err.println("[WARN] Only jar classpath entries are supported. Skipping " + file.getPath()); + return; + } + + loadClassesInJar(file); + } + + private static void loadAllClasses() throws IOException { + final String classPath = System.getProperty("java.class.path"); + if (classPath == null) { + return; + } + for (String classPathEntry : classPath.split(File.pathSeparator)) { + loadClassesInClasspathEntry(classPathEntry); + } + } + + public static void main(String[] args) throws IOException { + loadAllClasses(); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/CustomerClassLoader.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/CustomerClassLoader.java new file mode 100644 index 000000000..b8aabbf37 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/CustomerClassLoader.java @@ -0,0 +1,70 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client; + +import java.io.File; +import java.io.FilenameFilter; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; + +class CustomerClassLoader extends URLClassLoader { + /** + * This Comparator is used to ensure that jars added to this classloader are added in a deterministic order which + * does not depend on the underlying filesystem. + */ + private final static Comparator LEXICAL_SORT_ORDER = Comparator.comparing(String::toString); + private final static FilenameFilter JAR_FILE_NAME_FILTER = new FilenameFilter() { + + @Override + public boolean accept(File dir, String name) { + int offset = name.length() - 4; + // must be at least A.jar + if (offset <= 0) { + return false; + } else { + return name.startsWith(".jar", offset); + } + } + }; + + CustomerClassLoader(String taskRoot, String optRoot, ClassLoader parent) throws IOException { + super(getUrls(taskRoot, optRoot), parent); + } + + private static URL[] getUrls(String taskRoot, String optRoot) throws MalformedURLException { + File taskDir = new File(taskRoot + "/"); + List res = new ArrayList<>(); + res.add(newURL(taskDir, "")); + appendJars(new File(taskRoot + "/lib"), res); + appendJars(new File(optRoot + "/lib"), res); + return res.toArray(new URL[res.size()]); + } + + private static void appendJars(File dir, List result) throws MalformedURLException { + if (!dir.isDirectory()) { + return; + } + String[] names = dir.list(CustomerClassLoader.JAR_FILE_NAME_FILTER); + if (names == null) { + return; + } + Arrays.sort(names, CustomerClassLoader.LEXICAL_SORT_ORDER); + + for (String path : names) { + result.add(newURL(dir, path)); + } + } + + private static URL newURL(File parent, String path) throws MalformedURLException { + return new URL("file", null, -1, parent.getPath() + "/" + path); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/EventHandlerLoader.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/EventHandlerLoader.java new file mode 100644 index 000000000..f679c217c --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/EventHandlerLoader.java @@ -0,0 +1,921 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client; + +import com.amazonaws.services.lambda.runtime.ClientContext; +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.LambdaLogger; +import com.amazonaws.services.lambda.runtime.LambdaRuntimeInternal; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.RequestStreamHandler; +import com.amazonaws.services.lambda.runtime.api.client.LambdaRequestHandler.UserFaultHandler; +import com.amazonaws.services.lambda.runtime.api.client.api.LambdaClientContext; +import com.amazonaws.services.lambda.runtime.api.client.api.LambdaCognitoIdentity; +import com.amazonaws.services.lambda.runtime.api.client.api.LambdaContext; +import com.amazonaws.services.lambda.runtime.api.client.logging.LambdaContextLogger; +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.InvocationRequest; +import com.amazonaws.services.lambda.runtime.api.client.util.UnsafeUtil; +import com.amazonaws.services.lambda.runtime.serialization.PojoSerializer; +import com.amazonaws.services.lambda.runtime.serialization.events.LambdaEventSerializers; +import com.amazonaws.services.lambda.runtime.serialization.factories.GsonFactory; +import com.amazonaws.services.lambda.runtime.serialization.factories.JacksonFactory; +import com.amazonaws.services.lambda.runtime.serialization.util.Functions; +import com.amazonaws.services.lambda.runtime.serialization.util.ReflectUtil; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.util.Arrays; +import java.util.Comparator; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import static com.amazonaws.services.lambda.runtime.api.client.UserFault.filterStackTrace; +import static com.amazonaws.services.lambda.runtime.api.client.UserFault.makeUserFault; +import static com.amazonaws.services.lambda.runtime.api.client.UserFault.trace; + +public final class EventHandlerLoader { + private static final byte[] _JsonNull = new byte[]{'n', 'u', 'l', 'l'}; + + private enum Platform { + ANDROID, + IOS, + UNKNOWN + } + + private static volatile ThreadLocal> contextSerializer = new ThreadLocal<>(); + private static volatile ThreadLocal> cognitoSerializer = new ThreadLocal<>(); + + private static final ThreadLocal>>> typeCache = ThreadLocal.withInitial(() -> new EnumMap<>(Platform.class)); + + private static final Comparator methodPriority = new Comparator() { + public int compare(Method lhs, Method rhs) { + + //1. Non bridge methods are preferred over bridge methods. + if (!lhs.isBridge() && rhs.isBridge()) { + return -1; + } else if (!rhs.isBridge() && lhs.isBridge()) { + return 1; + } + + //2. We prefer longer signatures to shorter signatures. Except we count a method whose last argument is + //Context as having 1 more argument than it really does. This is a stupid thing to do, but we + //need to keep it for back compat reasons. + Class[] lParams = lhs.getParameterTypes(); + Class[] rParams = rhs.getParameterTypes(); + + int lParamCompareLength = lParams.length; + int rParamCompareLength = rParams.length; + + if (lastParameterIsContext(lParams)) { + ++lParamCompareLength; + } + + if (lastParameterIsContext(rParams)) { + ++rParamCompareLength; + } + + return -Integer.compare(lParamCompareLength, rParamCompareLength); + } + }; + + private EventHandlerLoader() { + } + + /** + * returns the appropriate serializer for the class based on platform and whether the class is a supported event + * + * @param platform enum platform + * @param type Type of object used + * @return PojoSerializer + * @see Platform for which platforms are used + * @see LambdaEventSerializers for how mixins and modules are added to the serializer + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + private static PojoSerializer getSerializer(Platform platform, Type type) { + PojoSerializer customSerializer = PojoSerializerLoader.getCustomerSerializer(type); + if (customSerializer != null) { + return customSerializer; + } + + // if serializing a Class that is a Lambda supported event, use Jackson with customizations + if (type instanceof Class) { + Class clazz = ((Class) type); + if (LambdaEventSerializers.isLambdaSupportedEvent(clazz.getName())) { + return LambdaEventSerializers.serializerFor(clazz, AWSLambda.getCustomerClassLoader()); + } + } + // else platform dependent (Android uses GSON but all other platforms use Jackson) + if (Objects.requireNonNull(platform) == Platform.ANDROID) { + return GsonFactory.getInstance().getSerializer(type); + } + return JacksonFactory.getInstance().getSerializer(type); + } + + private static PojoSerializer getSerializerCached(Platform platform, Type type) { + EnumMap>> threadTypeCache = typeCache.get(); + Map> cache = threadTypeCache.get(platform); + if (cache == null) { + cache = new HashMap<>(); + threadTypeCache.put(platform, cache); + } + + PojoSerializer serializer = cache.get(type); + if (serializer == null) { + serializer = getSerializer(platform, type); + cache.put(type, serializer); + } + + return serializer; + } + + private static PojoSerializer getContextSerializer() { + if (contextSerializer.get() == null) { + contextSerializer.set(GsonFactory.getInstance().getSerializer(LambdaClientContext.class)); + } + return contextSerializer.get(); + } + + private static PojoSerializer getCognitoSerializer() { + if (cognitoSerializer.get() == null) { + cognitoSerializer.set(GsonFactory.getInstance().getSerializer(LambdaCognitoIdentity.class)); + } + return cognitoSerializer.get(); + } + + + private static Platform getPlatform(Context context) { + ClientContext cc = context.getClientContext(); + if (cc == null) { + return Platform.UNKNOWN; + } + + Map env = cc.getEnvironment(); + if (env == null) { + return Platform.UNKNOWN; + } + + String platform = env.get("platform"); + if (platform == null) { + return Platform.UNKNOWN; + } + + if ("Android".equalsIgnoreCase(platform)) { + return Platform.ANDROID; + } else if ("iPhoneOS".equalsIgnoreCase(platform)) { + return Platform.IOS; + } else { + return Platform.UNKNOWN; + } + } + + private static boolean isVoid(Type type) { + return Void.TYPE.equals(type) || (type instanceof Class) && Void.class.isAssignableFrom((Class) type); + } + + private static Constructor getConstructor(Class clazz) throws Exception { + final Constructor constructor; + try { + constructor = clazz.getConstructor(); + } catch (NoSuchMethodException e) { + if (clazz.getEnclosingClass() != null && !Modifier.isStatic(clazz.getModifiers())) { + throw new Exception("Class " + + clazz.getName() + + " cannot be instantiated because it is a non-static inner class"); + } else { + throw new Exception("Class " + clazz.getName() + " has no public zero-argument constructor", e); + } + } + return constructor; + } + + private static T newInstance(Constructor constructor) { + try { + return constructor.newInstance(); + } catch (UserFault e) { + throw e; + } catch (InvocationTargetException e) { + throw makeUserFault(e.getCause() == null ? e : e.getCause(), true); + } catch (InstantiationException e) { + throw UnsafeUtil.throwException(e.getCause() == null ? e : e.getCause()); + } catch (IllegalAccessException e) { + throw UnsafeUtil.throwException(e); + } + } + + /** + * perform a breadth-first search for the first parameterized type for iface + * + * @return null of no type found. Otherwise the type found. + */ + private static Type[] findInterfaceParameters(Class clazz, Class iface) { + LinkedList clazzes = new LinkedList<>(); + clazzes.addFirst(new ClassContext(clazz, (Type[]) null)); + while (!clazzes.isEmpty()) { + final ClassContext curContext = clazzes.removeLast(); + Type[] interfaces = curContext.clazz.getGenericInterfaces(); + + for (Type type : interfaces) { + if (type instanceof ParameterizedType) { + ParameterizedType candidate = (ParameterizedType) type; + Type rawType = candidate.getRawType(); + if (!(rawType instanceof Class)) { + //should be impossible + System.err.println("raw type is not a class: " + rawType); + continue; + } + Class rawClass = (Class) rawType; + if (iface.isAssignableFrom(rawClass)) { + return new ClassContext(candidate, curContext).actualTypeArguments; + } else { + clazzes.addFirst(new ClassContext(candidate, curContext)); + } + } else if (type instanceof Class) { + clazzes.addFirst(new ClassContext((Class) type, curContext)); + } else { + //should never happen? + System.err.println("Unexpected type class " + type.getClass().getName()); + } + } + + final Type superClass = curContext.clazz.getGenericSuperclass(); + if (superClass instanceof ParameterizedType) { + clazzes.addFirst(new ClassContext((ParameterizedType) superClass, curContext)); + } else if (superClass != null) { + clazzes.addFirst(new ClassContext((Class) superClass, curContext)); + } + } + return null; + } + + + @SuppressWarnings({"rawtypes"}) + private static LambdaRequestHandler wrapRequestHandlerClass(final Class clazz) { + Type[] ptypes = findInterfaceParameters(clazz, RequestHandler.class); + if (ptypes == null) { + return new UserFaultHandler(makeUserFault("Class " + + clazz.getName() + + " does not implement RequestHandler with concrete type parameters")); + } + if (ptypes.length != 2) { + return new UserFaultHandler(makeUserFault( + "Invalid class signature for RequestHandler. Expected two generic types, got " + ptypes.length)); + } + + for (Type t : ptypes) { + if (t instanceof TypeVariable) { + Type[] bounds = ((TypeVariable) t).getBounds(); + boolean foundBound = false; + if (bounds != null) { + for (Type bound : bounds) { + if (!Object.class.equals(bound)) { + foundBound = true; + break; + } + } + } + + if (!foundBound) { + return new UserFaultHandler(makeUserFault("Class " + clazz.getName() + + " does not implement RequestHandler with concrete type parameters: parameter " + + t + " has no upper bound.")); + } + } + } + + final Type pType = ptypes[0]; + final Type rType = ptypes[1]; + + final Constructor constructor; + try { + constructor = getConstructor(clazz); + return wrapPojoHandler(newInstance(constructor), pType, rType); + } catch (UserFault f) { + return new UserFaultHandler(f); + } catch (Throwable e) { + return new UserFaultHandler(makeUserFault(e)); + } + } + + private static LambdaRequestHandler wrapRequestStreamHandlerClass(final Class clazz) { + final Constructor constructor; + try { + constructor = getConstructor(clazz); + return wrapRequestStreamHandler(newInstance(constructor)); + } catch (UserFault f) { + return new UserFaultHandler(f); + } catch (Throwable e) { + return new UserFaultHandler(makeUserFault(e)); + } + } + + private static LambdaRequestHandler loadStreamingRequestHandler(Class clazz) { + if (RequestStreamHandler.class.isAssignableFrom(clazz)) { + return wrapRequestStreamHandlerClass(clazz.asSubclass(RequestStreamHandler.class)); + } else if (RequestHandler.class.isAssignableFrom(clazz)) { + return wrapRequestHandlerClass(clazz.asSubclass(RequestHandler.class)); + } else { + return new UserFaultHandler(makeUserFault("Class does not implement an appropriate handler interface: " + + clazz.getName())); + } + } + + public static LambdaRequestHandler loadEventHandler(HandlerInfo handlerInfo) { + if (handlerInfo.methodName == null) { + return loadStreamingRequestHandler(handlerInfo.clazz); + } else { + return loadEventPojoHandler(handlerInfo); + } + } + + private static Optional getOneLengthHandler( + Class clazz, + Method m, + Type pType, + Type rType + ) { + if (InputStream.class.equals(pType)) { + return Optional.of(StreamMethodRequestHandler.makeRequestHandler(clazz, m, true, false, false)); + } else if (OutputStream.class.equals(pType)) { + return Optional.of(StreamMethodRequestHandler.makeRequestHandler(clazz, m, false, true, false)); + } else if (isContext(pType)) { + return Optional.of(PojoMethodRequestHandler.makeRequestHandler(clazz, m, null, rType, true)); + } else { + return Optional.of(PojoMethodRequestHandler.makeRequestHandler(clazz, m, pType, rType, false)); + } + } + + private static Optional getTwoLengthHandler( + Class clazz, + Method m, + Type pType1, + Type pType2, + Type rType + ) { + if (OutputStream.class.equals(pType1)) { + if (isContext(pType2)) { + return Optional.of(StreamMethodRequestHandler.makeRequestHandler(clazz, m, false, true, true)); + } else { + System.err.println( + "Ignoring two-argument overload because first argument type is OutputStream and second argument type is not Context"); + return Optional.empty(); + } + } else if (isContext(pType1)) { + System.err.println("Ignoring two-argument overload because first argument type is Context"); + return Optional.empty(); + } else if (InputStream.class.equals(pType1)) { + if (OutputStream.class.equals(pType2)) { + return Optional.of(StreamMethodRequestHandler.makeRequestHandler(clazz, m, true, true, false)); + } else if (isContext(pType2)) { + return Optional.of(StreamMethodRequestHandler.makeRequestHandler(clazz, m, true, false, true)); + } else { + System.err.println("Ignoring two-argument overload because second parameter type, " + + ReflectUtil.getRawClass(pType2).getName() + + ", is not OutputStream."); + return Optional.empty(); + } + } else if (isContext(pType2)) { + return Optional.of(PojoMethodRequestHandler.makeRequestHandler(clazz, m, pType1, rType, true)); + } else { + System.err.println("Ignoring two-argument overload because second parameter type is not Context"); + return Optional.empty(); + } + } + + private static Optional getThreeLengthHandler( + Class clazz, + Method m, + Type pType1, + Type pType2, + Type pType3, + Type rType + ) { + if (InputStream.class.equals(pType1) && OutputStream.class.equals(pType2) && isContext(pType3)) { + return Optional.of(StreamMethodRequestHandler.makeRequestHandler(clazz, m, true, true, true)); + } else { + System.err.println( + "Ignoring three-argument overload because argument signature is not (InputStream, OutputStream, Context"); + return Optional.empty(); + } + } + + private static Optional getHandlerFromOverload(Class clazz, Method m) { + final Type rType = m.getGenericReturnType(); + final Type[] pTypes = m.getGenericParameterTypes(); + + if (pTypes.length == 0) { + return Optional.of(PojoMethodRequestHandler.makeRequestHandler(clazz, m, null, rType, false)); + } else if (pTypes.length == 1) { + return getOneLengthHandler(clazz, m, pTypes[0], rType); + } else if (pTypes.length == 2) { + return getTwoLengthHandler(clazz, m, pTypes[0], pTypes[1], rType); + } else if (pTypes.length == 3) { + return getThreeLengthHandler(clazz, m, pTypes[0], pTypes[1], pTypes[2], rType); + } else { + System.err.println("Ignoring an overload of method " + + m.getName() + + " because it has too many parameters: Expected at most 3, got " + + pTypes.length); + return Optional.empty(); + } + } + + private static boolean isContext(Type t) { + return Context.class.equals(t); + } + + /** + * Returns true if the last type in params is a lambda context object interface (Context). + */ + private static boolean lastParameterIsContext(Class[] params) { + return params.length != 0 && isContext(params[params.length - 1]); + } + + /** + * Implement a comparator for Methods. We sort overloaded handler methods using this comparator, and then pick the + * lowest sorted method. + */ + + private static LambdaRequestHandler loadEventPojoHandler(HandlerInfo handlerInfo) { + Method[] methods; + try { + methods = handlerInfo.clazz.getMethods(); + } catch (NoClassDefFoundError e) { + return new LambdaRequestHandler.UserFaultHandler(new UserFault( + "Error loading method " + handlerInfo.methodName + " on class " + handlerInfo.clazz.getName(), + e.getClass().getName(), + trace(e) + )); + } + if (methods.length == 0) { + final String msg = "Class " + + handlerInfo.getClass().getName() + + " has no public method named " + + handlerInfo.methodName; + return new UserFaultHandler(makeUserFault(msg)); + } + + /* + * We support the following signatures + * Anything (InputStream, OutputStream, Context) + * Anything (InputStream, OutputStream) + * Anything (OutputStream, Context) + * Anything (InputStream, Context) + * Anything (InputStream) + * Anything (OutputStream) + * Anything (Context) + * Anything (AlmostAnything, Context) + * Anything (AlmostAnything) + * Anything () + * + * where AlmostAnything is any type except InputStream, OutputStream, Context + * Anything represents any type (primitive, void, or Object) + * + * prefer methods with longer signatures, add extra weight to those ending with a Context object + * + */ + + int slide = 0; + + for (int i = 0; i < methods.length; i++) { + Method m = methods[i]; + methods[i - slide] = m; + if (!m.getName().equals(handlerInfo.methodName)) { + slide++; + continue; + } + } + + final int end = methods.length - slide; + Arrays.sort(methods, 0, end, methodPriority); + + for (int i = 0; i < end; i++) { + Method m = methods[i]; + Optional result = getHandlerFromOverload(handlerInfo.clazz, m); + if (result.isPresent()) { + return result.get(); + } else { + continue; + } + } + + return new UserFaultHandler(makeUserFault("No public method named " + + handlerInfo.methodName + + " with appropriate method signature found on class " + + handlerInfo.clazz.getName())); + } + + @SuppressWarnings({"rawtypes"}) + private static LambdaRequestHandler wrapPojoHandler(RequestHandler instance, Type pType, Type rType) { + return wrapRequestStreamHandler(new PojoHandlerAsStreamHandler(instance, Optional.ofNullable(pType), + isVoid(rType) ? Optional.empty() : Optional.of(rType) + )); + } + + private static LambdaRequestHandler wrapRequestStreamHandler(final RequestStreamHandler handler) { + return new LambdaRequestHandler() { + private final ThreadLocal outputBuffers = ThreadLocal.withInitial(() -> new ByteArrayOutputStream(1024)); + private ThreadLocal> log4jContextPutMethod = new ThreadLocal<>(); + + private void safeAddRequestIdToLog4j(String log4jContextClassName, InvocationRequest request, Class contextMapValueClass) { + try { + Class log4jContextClass = ReflectUtil.loadClass(AWSLambda.getCustomerClassLoader(), log4jContextClassName); + log4jContextPutMethod.set(ReflectUtil.loadStaticV2(log4jContextClass, "put", false, String.class, contextMapValueClass)); + log4jContextPutMethod.get().call("AWSRequestId", request.getId()); + } catch (Exception e) { + // nothing to do here + } + } + + /** + * Passes the LambdaContext to the logger so that the JSON formatter can include the requestId. + * + * We do casting here because both the LambdaRuntime and the LambdaLogger is in the core package, + * and the setLambdaContext(context) is a method we don't want to publish for customers. That method is + * only implemented on the internal LambdaContextLogger, so we check and cast to be able to call it. + * @param context the LambdaContext + */ + private void safeAddContextToLambdaLogger(LambdaContext context) { + LambdaLogger logger = com.amazonaws.services.lambda.runtime.LambdaRuntime.getLogger(); + if (logger instanceof LambdaContextLogger) { + LambdaContextLogger contextLogger = (LambdaContextLogger) logger; + contextLogger.setLambdaContext(context); + } + } + + public ByteArrayOutputStream call(InvocationRequest request) throws Error, Exception { + ByteArrayOutputStream output = outputBuffers.get(); + output.reset(); + + LambdaCognitoIdentity cognitoIdentity = null; + if (request.getCognitoIdentity() != null && !request.getCognitoIdentity().isEmpty()) { + cognitoIdentity = getCognitoSerializer().fromJson(request.getCognitoIdentity()); + } + + LambdaClientContext clientContext = null; + if (request.getClientContext() != null && !request.getClientContext().isEmpty()) { + //Use GSON here because it handles immutable types without requiring annotations + clientContext = getContextSerializer().fromJson(request.getClientContext()); + } + + LambdaContext context = new LambdaContext( + LambdaEnvironment.MEMORY_LIMIT, + request.getDeadlineTimeInMs(), + request.getId(), + LambdaEnvironment.LOG_GROUP_NAME, + LambdaEnvironment.LOG_STREAM_NAME, + LambdaEnvironment.FUNCTION_NAME, + cognitoIdentity, + LambdaEnvironment.FUNCTION_VERSION, + request.getInvokedFunctionArn(), + request.getTenantId(), + request.getXrayTraceId(), + clientContext + ); + + safeAddContextToLambdaLogger(context); + + if (LambdaRuntimeInternal.getUseLog4jAppender()) { + safeAddRequestIdToLog4j("org.apache.log4j.MDC", request, Object.class); + safeAddRequestIdToLog4j("org.apache.logging.log4j.ThreadContext", request, String.class); + // if put method not assigned in either call to safeAddRequestIdtoLog4j then log4jContextPutMethod = null + if (log4jContextPutMethod.get() == null) { + System.err.println("Customer using log4j appender but unable to load either " + + "org.apache.log4j.MDC or org.apache.logging.log4j.ThreadContext. " + + "Customer cannot see RequestId in log4j log lines."); + } + } + + ByteArrayInputStream bais = new ByteArrayInputStream(request.getContent()); + handler.handleRequest(bais, output, context); + return output; + } + }; + } + + /** + * Wraps a RequestHandler as a lower level stream handler using supplied types. + * Optional types mean that the input and/or output should be ignored respectiveley + */ + @SuppressWarnings("rawtypes") + private static final class PojoHandlerAsStreamHandler implements RequestStreamHandler { + + public RequestHandler innerHandler; + public final Optional inputType; + public final Optional outputType; + + public PojoHandlerAsStreamHandler( + RequestHandler innerHandler, + Optional inputType, + Optional outputType + ) { + this.innerHandler = innerHandler; + this.inputType = inputType; + this.outputType = outputType; + + + if (inputType.isPresent()) { + getSerializerCached(Platform.UNKNOWN, inputType.get()); + } + + if (outputType.isPresent()) { + getSerializerCached(Platform.UNKNOWN, outputType.get()); + } + } + + @SuppressWarnings("unchecked") + @Override + public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) + throws IOException { + final Object input; + final Platform platform = getPlatform(context); + try { + if (inputType.isPresent()) { + input = getSerializerCached(platform, inputType.get()).fromJson(inputStream); + } else { + input = null; + } + } catch (Throwable t) { + throw new RuntimeException("An error occurred during JSON parsing", filterStackTrace(t)); + } + + final Object output; + try { + output = innerHandler.handleRequest(input, context); + } catch (Throwable t) { + throw UnsafeUtil.throwException(filterStackTrace(t)); + } + + try { + if (outputType.isPresent()) { + PojoSerializer serializer = getSerializerCached(platform, outputType.get()); + serializer.toJson(output, outputStream); + } else { + outputStream.write(_JsonNull); + } + } catch (Throwable t) { + throw new RuntimeException("An error occurred during JSON serialization of response", t); + } + } + } + + /** + * Wraps a java.lang.reflect.Method as a POJO RequestHandler + */ + private static final class PojoMethodRequestHandler implements RequestHandler { + public final Method m; + public final Type pType; + public final Object instance; + public final boolean needsContext; + public final int argSize; + + public PojoMethodRequestHandler(Method m, Type pType, Type rType, Object instance, boolean needsContext) { + this.m = m; + this.pType = pType; + this.instance = instance; + this.needsContext = needsContext; + this.argSize = (needsContext ? 1 : 0) + (pType != null ? 1 : 0); + } + + public static PojoMethodRequestHandler fromMethod( + Class clazz, + Method m, + Type pType, + Type rType, + boolean needsContext + ) throws Exception { + final Object instance; + if (Modifier.isStatic(m.getModifiers())) { + instance = null; + } else { + instance = newInstance(getConstructor(clazz)); + } + + return new PojoMethodRequestHandler(m, pType, rType, instance, needsContext); + } + + public static LambdaRequestHandler makeRequestHandler( + Class clazz, + Method m, + Type pType, + Type rType, + boolean needsContext + ) { + try { + return wrapPojoHandler(fromMethod(clazz, m, pType, rType, needsContext), pType, rType); + } catch (UserFault f) { + return new UserFaultHandler(f); + } catch (Throwable t) { + return new UserFaultHandler(makeUserFault(t)); + } + } + + @Override + public Object handleRequest(Object input, Context context) { + final Object[] args = new Object[argSize]; + int idx = 0; + + if (pType != null) { + args[idx++] = input; + } + + if (this.needsContext) { + args[idx++] = context; + } + + try { + return m.invoke(this.instance, args); + } catch (InvocationTargetException e) { + if (e.getCause() != null) { + throw UnsafeUtil.throwException(filterStackTrace(e.getCause())); + } else { + throw UnsafeUtil.throwException(filterStackTrace(e)); + } + } catch (Throwable t) { + throw UnsafeUtil.throwException(filterStackTrace(t)); + } + } + } + + /** + * Wraps a java.lang.reflect.Method object as a RequestStreamHandler + */ + private static final class StreamMethodRequestHandler implements RequestStreamHandler { + public final Method m; + public final Object instance; + public final boolean needsInput; + public final boolean needsOutput; + public final boolean needsContext; + public final int argSize; + + public StreamMethodRequestHandler( + Method m, + Object instance, + boolean needsInput, + boolean needsOutput, + boolean needsContext + ) { + this.m = m; + this.instance = instance; + this.needsInput = needsInput; + this.needsOutput = needsOutput; + this.needsContext = needsContext; + this.argSize = (needsInput ? 1 : 0) + (needsOutput ? 1 : 0) + (needsContext ? 1 : 0); + } + + public static StreamMethodRequestHandler fromMethod( + Class clazz, + Method m, + boolean needsInput, + boolean needsOutput, + boolean needsContext + ) throws Exception { + if (!isVoid(m.getReturnType())) { + System.err.println("Will ignore return type " + m.getReturnType() + " on byte stream handler"); + } + final Object instance = Modifier.isStatic(m.getModifiers()) + ? null + : newInstance(getConstructor(clazz)); + + return new StreamMethodRequestHandler(m, instance, needsInput, needsOutput, needsContext); + } + + public static LambdaRequestHandler makeRequestHandler( + Class clazz, + Method m, + boolean needsInput, + boolean needsOutput, + boolean needsContext + ) { + try { + return wrapRequestStreamHandler(fromMethod(clazz, m, needsInput, needsOutput, needsContext)); + } catch (UserFault f) { + return new UserFaultHandler(f); + } catch (Throwable t) { + return new UserFaultHandler(makeUserFault(t)); + } + } + + @Override + public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) + throws IOException { + final Object[] args = new Object[argSize]; + int idx = 0; + + if (needsInput) { + args[idx++] = inputStream; + } else { + inputStream.close(); + } + + if (needsOutput) { + args[idx++] = outputStream; + } + + if (needsContext) { + args[idx++] = context; + } + + try { + m.invoke(this.instance, args); + if (!needsOutput) { + outputStream.write(_JsonNull); + } + } catch (InvocationTargetException e) { + if (e.getCause() != null) { + throw UnsafeUtil.throwException(filterStackTrace(e.getCause())); + } else { + throw UnsafeUtil.throwException(filterStackTrace(e)); + } + } catch (Throwable t) { + throw UnsafeUtil.throwException(filterStackTrace(t)); + } + } + } + + private static final class ClassContext { + public final Class clazz; + public final Type[] actualTypeArguments; + + @SuppressWarnings({"rawtypes"}) + private TypeVariable[] typeParameters; + + public ClassContext(Class clazz, Type[] actualTypeArguments) { + this.clazz = clazz; + this.actualTypeArguments = actualTypeArguments; + } + + @SuppressWarnings({"rawtypes"}) + public ClassContext(Class clazz, ClassContext curContext) { + this.typeParameters = clazz.getTypeParameters(); + if (typeParameters.length == 0 || curContext.actualTypeArguments == null) { + this.clazz = clazz; + this.actualTypeArguments = null; + } else { + Type[] types = new Type[typeParameters.length]; + for (int i = 0; i < types.length; i++) { + types[i] = curContext.resolveTypeVariable(typeParameters[i]); + } + + this.clazz = clazz; + this.actualTypeArguments = types; + } + } + + @SuppressWarnings({"rawtypes"}) + public ClassContext(ParameterizedType type, ClassContext curContext) { + Type[] types = type.getActualTypeArguments(); + for (int i = 0; i < types.length; i++) { + Type t = types[i]; + if (t instanceof TypeVariable) { + types[i] = curContext.resolveTypeVariable((TypeVariable) t); + } + } + + Type t = type.getRawType(); + if (t instanceof Class) { + this.clazz = (Class) t; + } else if (t instanceof TypeVariable) { + this.clazz = (Class) ((TypeVariable) t).getGenericDeclaration(); + } else { + throw new RuntimeException("Type " + t + " is of unexpected type " + t.getClass()); + } + this.actualTypeArguments = types; + } + + @SuppressWarnings({"rawtypes"}) + public Type resolveTypeVariable(TypeVariable t) { + TypeVariable[] variables = getTypeParameters(); + for (int i = 0; i < variables.length; i++) { + if (t.getName().equals(variables[i].getName())) { + return actualTypeArguments == null ? variables[i] : actualTypeArguments[i]; + } + } + + return t; + } + + @SuppressWarnings({"rawtypes"}) + private TypeVariable[] getTypeParameters() { + if (typeParameters == null) { + typeParameters = clazz.getTypeParameters(); + } + return typeParameters; + } + } + +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/HandlerInfo.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/HandlerInfo.java new file mode 100644 index 000000000..54e2f6710 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/HandlerInfo.java @@ -0,0 +1,45 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client; + +public final class HandlerInfo { + + public final Class clazz; + public final String methodName; + + + public HandlerInfo(Class clazz, String methodName) { + this.clazz = clazz; + this.methodName = methodName; + } + + public static HandlerInfo fromString(String handler, ClassLoader cl) throws ClassNotFoundException, NoClassDefFoundError, InvalidHandlerException { + final int colonLoc = handler.lastIndexOf("::"); + final String className; + final String methodName; + if (colonLoc < 0) { + className = handler; + methodName = null; + } else { + className = handler.substring(0, colonLoc); + methodName = handler.substring(colonLoc + 2); + } + + if (className.isEmpty() || (methodName != null && methodName.isEmpty())) { + throw new InvalidHandlerException(); + } + return new HandlerInfo(Class.forName(className, true, cl), methodName); + } + + public static String className(String handler) { + final int colonLoc = handler.lastIndexOf("::"); + return (colonLoc < 0) ? handler : handler.substring(0, colonLoc); + } + + public static class InvalidHandlerException extends RuntimeException { + public static final long serialVersionUID = -1; + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/LambdaEnvironment.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/LambdaEnvironment.java new file mode 100644 index 000000000..77838f72a --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/LambdaEnvironment.java @@ -0,0 +1,29 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client; + +import com.amazonaws.services.lambda.runtime.api.client.util.EnvReader; +import static com.amazonaws.services.lambda.runtime.api.client.ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_FUNCTION_MEMORY_SIZE; +import static com.amazonaws.services.lambda.runtime.api.client.ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_FUNCTION_NAME; +import static com.amazonaws.services.lambda.runtime.api.client.ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_FUNCTION_VERSION; +import static com.amazonaws.services.lambda.runtime.api.client.ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_LOG_FORMAT; +import static com.amazonaws.services.lambda.runtime.api.client.ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_LOG_GROUP_NAME; +import static com.amazonaws.services.lambda.runtime.api.client.ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_LOG_LEVEL; +import static com.amazonaws.services.lambda.runtime.api.client.ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_LOG_STREAM_NAME; +import static com.amazonaws.services.lambda.runtime.api.client.ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_RUNTIME_API; +import static java.lang.Integer.parseInt; + +public class LambdaEnvironment { + public static final EnvReader ENV_READER = new EnvReader(); + public static final int MEMORY_LIMIT = parseInt(ENV_READER.getEnvOrDefault(AWS_LAMBDA_FUNCTION_MEMORY_SIZE, "128")); + public static final String LOG_GROUP_NAME = ENV_READER.getEnv(AWS_LAMBDA_LOG_GROUP_NAME); + public static final String LOG_STREAM_NAME = ENV_READER.getEnv(AWS_LAMBDA_LOG_STREAM_NAME); + public static final String LAMBDA_LOG_LEVEL = ENV_READER.getEnvOrDefault(AWS_LAMBDA_LOG_LEVEL, "UNDEFINED"); + public static final String LAMBDA_LOG_FORMAT = ENV_READER.getEnvOrDefault(AWS_LAMBDA_LOG_FORMAT, "TEXT"); + public static final String FUNCTION_NAME = ENV_READER.getEnv(AWS_LAMBDA_FUNCTION_NAME); + public static final String FUNCTION_VERSION = ENV_READER.getEnv(AWS_LAMBDA_FUNCTION_VERSION); + public static final String RUNTIME_API = ENV_READER.getEnv(AWS_LAMBDA_RUNTIME_API); +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/LambdaRequestHandler.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/LambdaRequestHandler.java new file mode 100644 index 000000000..ce9254ef8 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/LambdaRequestHandler.java @@ -0,0 +1,33 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client; + +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.InvocationRequest; +import java.io.ByteArrayOutputStream; + +public interface LambdaRequestHandler { + ByteArrayOutputStream call(InvocationRequest request) throws Error, Exception; + + static LambdaRequestHandler initErrorHandler(final Throwable e, String className) { + return new UserFaultHandler(UserFault.makeInitErrorUserFault(e, className)); + } + + static LambdaRequestHandler classNotFound(final Throwable e, String className) { + return new UserFaultHandler(UserFault.makeClassNotFoundUserFault(e, className)); + } + + class UserFaultHandler implements LambdaRequestHandler { + public final UserFault fault; + + public UserFaultHandler(UserFault fault) { + this.fault = fault; + } + + public ByteArrayOutputStream call(InvocationRequest request) { + throw fault; + } + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/PojoSerializerLoader.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/PojoSerializerLoader.java new file mode 100644 index 000000000..da37f7ca7 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/PojoSerializerLoader.java @@ -0,0 +1,77 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client; + +import com.amazonaws.services.lambda.runtime.CustomPojoSerializer; +import com.amazonaws.services.lambda.runtime.serialization.PojoSerializer; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Type; +import java.util.Iterator; +import java.util.ServiceConfigurationError; +import java.util.ServiceLoader; + +public class PojoSerializerLoader { + // The serializer obtained from the provider will always be the same so we can cache it as a filed. + private static CustomPojoSerializer customPojoSerializer; + // If Input and Output type are different, the runtime will try to search for a serializer twice due to + // the getSerializerCached method. Save the initialization state in order to search for the provider only once. + private static boolean initialized = false; + + private static CustomPojoSerializer loadSerializer() + throws ServiceConfigurationError, TooManyServiceProvidersFoundException { + + if (customPojoSerializer != null) { + return customPojoSerializer; + } + + ServiceLoader loader = ServiceLoader.load(CustomPojoSerializer.class, AWSLambda.getCustomerClassLoader()); + Iterator serializers = loader.iterator(); + + if (!serializers.hasNext()) { + initialized = true; + return null; + } + + customPojoSerializer = serializers.next(); + + if (serializers.hasNext()) { + throw new TooManyServiceProvidersFoundException( + "Too many serializers provided inside the META-INF/services folder, only one is allowed" + ); + } + + initialized = true; + return customPojoSerializer; + } + + public static PojoSerializer getCustomerSerializer(Type type) { + if (!initialized) { + customPojoSerializer = loadSerializer(); + } + + if (customPojoSerializer == null) { + return null; + } + + return new PojoSerializer() { + @Override + public Object fromJson(InputStream input) { + return customPojoSerializer.fromJson(input, type); + } + + @Override + public Object fromJson(String input) { + return customPojoSerializer.fromJson(input, type); + } + + @Override + public void toJson(Object value, OutputStream output) { + customPojoSerializer.toJson(value, output, type); + } + }; + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/ReservedRuntimeEnvironmentVariables.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/ReservedRuntimeEnvironmentVariables.java new file mode 100644 index 000000000..9fdec6b9f --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/ReservedRuntimeEnvironmentVariables.java @@ -0,0 +1,115 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client; + +/** + * Lambda runtimes set several environment variables during initialization. + * Most of the environment variables provide information about the function or runtime. + * The keys for these environment variables are reserved and cannot be set in your function configuration. + * + * @see Using AWS Lambda Environment Variables + *

+ * NOTICE: This class is forked from io.micronaut.function.aws.runtime.ReservedRuntimeEnvironments found at https://github.com/micronaut-projects/micronaut-aws + */ +public interface ReservedRuntimeEnvironmentVariables { + + /** + * The handler location configured on the function. + */ + String HANDLER = "_HANDLER"; + + /** + * The AWS Region where the Lambda function is executed. + */ + String AWS_REGION = "AWS_REGION"; + + /** + * The runtime identifier, prefixed by AWS_Lambda_—for example, AWS_Lambda_java8. + */ + String AWS_EXECUTION_ENV = "AWS_EXECUTION_ENV"; + + /** + * The name of the function. + */ + String AWS_LAMBDA_FUNCTION_NAME = "AWS_LAMBDA_FUNCTION_NAME"; + + /** + * The amount of memory available to the function in MB. + */ + String AWS_LAMBDA_FUNCTION_MEMORY_SIZE = "AWS_LAMBDA_FUNCTION_MEMORY_SIZE"; + + /** + * The version of the function being executed. + */ + String AWS_LAMBDA_FUNCTION_VERSION = "AWS_LAMBDA_FUNCTION_VERSION"; + + /** + * The name of the Amazon CloudWatch Logs group for the function. + */ + String AWS_LAMBDA_LOG_GROUP_NAME = "AWS_LAMBDA_LOG_GROUP_NAME"; + + /** + * The name of the Amazon CloudWatch stream for the function. + */ + String AWS_LAMBDA_LOG_STREAM_NAME = "AWS_LAMBDA_LOG_STREAM_NAME"; + + /** + * The logging level set for the function. + */ + String AWS_LAMBDA_LOG_LEVEL = "AWS_LAMBDA_LOG_LEVEL"; + + /** + * The logging format set for the function. + */ + String AWS_LAMBDA_LOG_FORMAT = "AWS_LAMBDA_LOG_FORMAT"; + + /** + * Access key id obtained from the function's execution role. + */ + String AWS_ACCESS_KEY_ID = "AWS_ACCESS_KEY_ID"; + + /** + * secret access key obtained from the function's execution role. + */ + String AWS_SECRET_ACCESS_KEY = "AWS_SECRET_ACCESS_KEY"; + + /** + * The access keys obtained from the function's execution role. + */ + String AWS_SESSION_TOKEN = "AWS_SESSION_TOKEN"; + + /** + * (Custom runtime) The host and port of the runtime API. + */ + String AWS_LAMBDA_RUNTIME_API = "AWS_LAMBDA_RUNTIME_API"; + + + /** + * Initialization type + */ + String AWS_LAMBDA_INITIALIZATION_TYPE = "AWS_LAMBDA_INITIALIZATION_TYPE"; + + /** + * The path to your Lambda function code. + */ + String LAMBDA_TASK_ROOT = "LAMBDA_TASK_ROOT"; + + /** + * The path to runtime libraries. + */ + String LAMBDA_RUNTIME_DIR = "LAMBDA_RUNTIME_DIR"; + + /** + * The environment's time zone (UTC). The execution environment uses NTP to synchronize the system clock. + */ + String TZ = "TZ"; + + /* + * If set to a string parsable as an integer > 0, It enables multiconcurrency mode. + * Otherwise, if it is set to an invalid value, it will crash the whole RIC process. + */ + String AWS_LAMBDA_MAX_CONCURRENCY = "AWS_LAMBDA_MAX_CONCURRENCY"; +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/TooManyServiceProvidersFoundException.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/TooManyServiceProvidersFoundException.java new file mode 100644 index 000000000..07fac7170 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/TooManyServiceProvidersFoundException.java @@ -0,0 +1,23 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client; + +public class TooManyServiceProvidersFoundException extends RuntimeException { + public TooManyServiceProvidersFoundException() { + } + + public TooManyServiceProvidersFoundException(String errorMessage) { + super(errorMessage); + } + + public TooManyServiceProvidersFoundException(Throwable cause) { + super(cause); + } + + public TooManyServiceProvidersFoundException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/UserFault.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/UserFault.java new file mode 100644 index 000000000..7d8a50347 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/UserFault.java @@ -0,0 +1,136 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.HashSet; +import java.util.Set; + +public final class UserFault extends RuntimeException { + private static final long serialVersionUID = -479308856905162038L; + + private static final String packagePrefix = AWSLambda.class.getPackage().getName(); + public final String msg; + public final String exception; + public final String trace; + public final Boolean fatal; + + public UserFault(String msg, String exception, String trace) { + this.msg = msg; + this.exception = exception; + this.trace = trace; + this.fatal = false; + } + + public UserFault(String msg, String exception, String trace, Boolean fatal) { + this.msg = msg; + this.exception = exception; + this.trace = trace; + this.fatal = fatal; + } + + /** + * Convenience function to report a fault given an exception. The constructed fault is marked non-fatal. + * No more user code should run after a fault. + */ + public static UserFault makeUserFault(Throwable t) { + return t instanceof UserFault ? (UserFault) t : makeUserFault(t, false); + } + + public static UserFault makeUserFault(Throwable t, boolean fatal) { + final String msg = t.getLocalizedMessage() == null ? t.getClass().getName() : t.getLocalizedMessage(); + return new UserFault(msg, t.getClass().getName(), trace(t), fatal); + } + + /** + * Convenience function to report a fault given a message. + * No more user code should run after a fault. + */ + public static UserFault makeUserFault(String msg) { + return new UserFault(msg, null, null); + } + + /** + * Convert a throwable's stack trace to a String + */ + public static String trace(Throwable t) { + filterStackTrace(t); + StringWriter sw = new StringWriter(); + t.printStackTrace(new PrintWriter(sw)); + return sw.toString(); + } + + /** + * remove our runtime code from the stack trace recursively. Returns + * the same object for convenience. + */ + public static T filterStackTrace(T t) { + return filterStackTrace(t, new HashSet<>(), new HashSet<>()); + } + + private static T filterStackTrace(T t, Set visited, Set visitedSuppressed) { + if (visited.contains(t)) { + return t; + } + + visited.add(t); + + StackTraceElement[] trace = t.getStackTrace(); + for (int i = 0; i < trace.length; i++) { + if (trace[i].getClassName().startsWith(packagePrefix)) { + StackTraceElement[] newTrace = new StackTraceElement[i]; + System.arraycopy(trace, 0, newTrace, 0, i); + t.setStackTrace(newTrace); + break; + } + } + + + Throwable cause = t.getCause(); + if (cause != null) { + filterStackTrace(cause, visited, visitedSuppressed); + } + + Throwable[] suppressedExceptions = t.getSuppressed(); + for (Throwable suppressed: suppressedExceptions) { + if (!visitedSuppressed.contains(suppressed)) { + visitedSuppressed.add(suppressed); + filterStackTrace(suppressed, visited, visitedSuppressed); + } + } + + return t; + } + + static UserFault makeInitErrorUserFault(Throwable e, String className) { + return new UserFault( + "Error loading class " + className + (e.getMessage() == null ? "" : ": " + e.getMessage()), + e.getClass().getName(), + trace(e), + true + ); + } + + static UserFault makeClassNotFoundUserFault(Throwable e, String className) { + return new UserFault( + "Class not found: " + className, + e.getClass().getName(), + trace(e), + false + ); + } + + public String reportableError() { + if (this.exception != null || this.trace != null) { + return String.format("%s: %s\n%s\n", + this.msg, + this.exception == null ? "" : this.exception, + this.trace == null ? "" : this.trace); + } + return String.format("%s\n", this.msg); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaClientContext.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaClientContext.java new file mode 100644 index 000000000..3baa5347b --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaClientContext.java @@ -0,0 +1,29 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client.api; + +import com.amazonaws.services.lambda.runtime.Client; +import com.amazonaws.services.lambda.runtime.ClientContext; +import java.util.Map; + +public class LambdaClientContext implements ClientContext { + + private LambdaClientContextClient client; + private Map custom; + private Map env; + + public Client getClient() { + return client; + } + + public Map getCustom() { + return custom; + } + + public Map getEnvironment() { + return env; + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaClientContextClient.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaClientContextClient.java new file mode 100644 index 000000000..b76a25f5e --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaClientContextClient.java @@ -0,0 +1,41 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client.api; + +import com.amazonaws.services.lambda.runtime.Client; + +public class LambdaClientContextClient implements Client { + + private String installation_id; + + private String app_title; + + private String app_version_name; + + private String app_version_code; + + private String app_package_name; + + public String getInstallationId() { + return installation_id; + } + + public String getAppTitle() { + return app_title; + } + + public String getAppVersionName() { + return app_version_name; + } + + public String getAppVersionCode() { + return app_version_code; + } + + public String getAppPackageName() { + return app_package_name; + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaCognitoIdentity.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaCognitoIdentity.java new file mode 100644 index 000000000..89e60d348 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaCognitoIdentity.java @@ -0,0 +1,27 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client.api; + +import com.amazonaws.services.lambda.runtime.CognitoIdentity; + +public class LambdaCognitoIdentity implements CognitoIdentity { + + private final String cognitoIdentityId; + private final String cognitoIdentityPoolId; + + public LambdaCognitoIdentity(String identityid, String poolid) { + this.cognitoIdentityId = identityid; + this.cognitoIdentityPoolId = poolid; + } + + public String getIdentityId() { + return this.cognitoIdentityId; + } + + public String getIdentityPoolId() { + return this.cognitoIdentityPoolId; + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaContext.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaContext.java new file mode 100644 index 000000000..20b77262d --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaContext.java @@ -0,0 +1,111 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client.api; + +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; + +public class LambdaContext implements Context { + + private int memoryLimit; + private final String awsRequestId; + private final String logGroupName; + private final String logStreamName; + private final String functionName; + private final String functionVersion; + private final String invokedFunctionArn; + private final long deadlineTimeInMs; + private final CognitoIdentity cognitoIdentity; + private final ClientContext clientContext; + private final String tenantId; + private final String xrayTraceId; + private final LambdaLogger logger; + + public LambdaContext( + int memoryLimit, + long deadlineTimeInMs, + String requestId, + String logGroupName, + String logStreamName, + String functionName, + CognitoIdentity identity, + String functionVersion, + String invokedFunctionArn, + String tenantId, + String xrayTraceId, + ClientContext clientContext + ) { + this.memoryLimit = memoryLimit; + this.deadlineTimeInMs = deadlineTimeInMs; + this.awsRequestId = requestId; + this.logGroupName = logGroupName; + this.logStreamName = logStreamName; + this.functionName = functionName; + this.cognitoIdentity = identity; + this.clientContext = clientContext; + this.functionVersion = functionVersion; + this.invokedFunctionArn = invokedFunctionArn; + this.tenantId = tenantId; + this.xrayTraceId = xrayTraceId; + this.logger = com.amazonaws.services.lambda.runtime.LambdaRuntime.getLogger(); + } + + public int getMemoryLimitInMB() { + return memoryLimit; + } + + public String getAwsRequestId() { + return awsRequestId; + } + + public String getLogGroupName() { + return logGroupName; + } + + public String getLogStreamName() { + return logStreamName; + } + + public String getFunctionName() { + return functionName; + } + + public String getFunctionVersion() { + return functionVersion; + } + + public String getInvokedFunctionArn() { + return invokedFunctionArn; + } + + public CognitoIdentity getIdentity() { + return cognitoIdentity; + } + + public ClientContext getClientContext() { + return clientContext; + } + + public int getRemainingTimeInMillis() { + long now = System.currentTimeMillis(); + int delta = (int) (this.deadlineTimeInMs - now); + return delta > 0 ? delta : 0; + } + + public String getTenantId() { + return tenantId; + } + + public String getXrayTraceId() { + return xrayTraceId; + } + + public LambdaLogger getLogger() { + return logger; + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/AbstractLambdaLogger.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/AbstractLambdaLogger.java new file mode 100644 index 000000000..f987b0bdb --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/AbstractLambdaLogger.java @@ -0,0 +1,74 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client.logging; + +import com.amazonaws.services.lambda.runtime.LambdaLogger; +import com.amazonaws.services.lambda.runtime.api.client.api.LambdaContext; +import com.amazonaws.services.lambda.runtime.logging.LogFormat; +import com.amazonaws.services.lambda.runtime.logging.LogLevel; +import static java.nio.charset.StandardCharsets.UTF_8; + +/** + * Provides default implementation of the convenience logger functions. + * When extending AbstractLambdaLogger, only one function has to be overridden: + * void logMessage(byte[] message, LogLevel logLevel); + */ +public abstract class AbstractLambdaLogger implements LambdaLogger { + protected final LogFormat logFormat; + private final LogFiltering logFiltering; + private final LogFormatter logFormatter; + + public AbstractLambdaLogger(LogLevel logLevel, LogFormat logFormat) { + this.logFiltering = new LogFiltering(logLevel); + + this.logFormat = logFormat; + if (logFormat == LogFormat.JSON) { + logFormatter = new JsonLogFormatter(); + } else { + logFormatter = new TextLogFormatter(); + } + } + + protected abstract void logMessage(byte[] message, LogLevel logLevel); + + protected void logMessage(String message, LogLevel logLevel) { + byte[] messageBytes = message == null ? null : message.getBytes(UTF_8); + logMessage(messageBytes, logLevel); + } + + @Override + public void log(String message, LogLevel logLevel) { + if (logFiltering.isEnabled(logLevel)) { + this.logMessage(logFormatter.format(message, logLevel), logLevel); + } + } + + @Override + public void log(byte[] message, LogLevel logLevel) { + if (logFiltering.isEnabled(logLevel)) { + // there is no formatting for byte[] messages + this.logMessage(message, logLevel); + } + } + + @Override + public void log(String message) { + this.log(message, LogLevel.UNDEFINED); + } + + @Override + public void log(byte[] message) { + this.log(message, LogLevel.UNDEFINED); + } + + public void setLambdaContext(LambdaContext lambdaContext) { + this.logFormatter.setLambdaContext(lambdaContext); + } + + public LogFormat getLogFormat() { + return logFormat; + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/FrameType.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/FrameType.java new file mode 100644 index 000000000..f3891ce20 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/FrameType.java @@ -0,0 +1,49 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client.logging; + +import com.amazonaws.services.lambda.runtime.logging.LogFormat; +import com.amazonaws.services.lambda.runtime.logging.LogLevel; + +/** + * The first 4 bytes of the framing protocol is the Frame Type, that's made of a magic number (3 bytes) and 1 byte of flags. + * +-----------------------+ + * | Frame Type - 4 bytes | + * +-----------------------+ + * | a5 5a 00 | flgs | + * + - - - - - + - - - - - + + * \ bit | + * | view| + * +---------+ + + * | | + * v byte 3 v F - free + * +-+-+-+-+-+-+-+-+ J - { JsonLog = 0, PlainTextLog = 1 } + * |F|F|F|L|l|l|T|J| T - { NoTimeStamp = 0, TimeStampPresent = 1 } + * +-+-+-+-+-+-+-+-+ Lll -> Log Level in 3-bit binary (L-> most significant bit) + */ +public class FrameType { + private static final int LOG_MAGIC = 0xa55a0000; + private static final int OFFSET_LOG_FORMAT = 0; + private static final int OFFSET_TIMESTAMP_PRESENT = 1; + private static final int OFFSET_LOG_LEVEL = 2; + + private final int val; + + FrameType(int val) { + this.val = val; + } + + public static int getValue(LogLevel logLevel, LogFormat logFormat) { + return LOG_MAGIC + | (logLevel.ordinal() << OFFSET_LOG_LEVEL) + | (1 << OFFSET_TIMESTAMP_PRESENT) + | (logFormat.ordinal() << OFFSET_LOG_FORMAT); + } + + public int getValue() { + return this.val; + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/FramedTelemetryLogSink.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/FramedTelemetryLogSink.java new file mode 100644 index 000000000..e297d1908 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/FramedTelemetryLogSink.java @@ -0,0 +1,88 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client.logging; + +import com.amazonaws.services.lambda.runtime.logging.LogFormat; +import com.amazonaws.services.lambda.runtime.logging.LogLevel; +import java.io.FileDescriptor; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.time.Instant; + + +/** + * FramedTelemetryLogSink implements the logging contract between runtimes and the platform. It implements a simple + * framing protocol so message boundaries can be determined. Each frame can be visualized as follows: + * + *

+ * {@code
+ * +----------------------+------------------------+---------------------+-----------------------+
+ * | Frame Type - 4 bytes | Length (len) - 4 bytes | Timestamp - 8 bytes | Message - 'len' bytes |
+ * +----------------------+------------------------+---------------------+-----------------------+
+ * }
+ * 
+ *

+ * The first 4 bytes indicate the type of the frame - log frames have a type defined as the hex value 0xa55a0001. The + * second 4 bytes should indicate the message's length. The next 8 bytes contain UNIX timestamp of the message in + * microsecond accuracy. The next 'len' bytes contain the message. The byte order is big-endian. + */ +public class FramedTelemetryLogSink implements LogSink { + + private static final int HEADER_LENGTH = 16; + + private final FileOutputStream logOutputStream; + private final ByteBuffer headerBuf; + + public FramedTelemetryLogSink(FileDescriptor fd) throws IOException { + this.logOutputStream = new FileOutputStream(fd); + this.headerBuf = ByteBuffer.allocate(HEADER_LENGTH).order(ByteOrder.BIG_ENDIAN); + } + + @Override + public synchronized void log(LogLevel logLevel, LogFormat logFormat, byte[] message) { + try { + writeFrame(logLevel, logFormat, message); + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Override + public void log(byte[] message) { + log(LogLevel.UNDEFINED, LogFormat.TEXT, message); + } + + private void writeFrame(LogLevel logLevel, LogFormat logFormat, byte[] message) throws IOException { + updateHeader(logLevel, logFormat, message.length); + this.logOutputStream.write(this.headerBuf.array()); + this.logOutputStream.write(message); + } + + private long timestamp() { + Instant instant = Instant.now(); + // microsecond precision + return instant.getEpochSecond() * 1_000_000 + instant.getNano() / 1000; + } + + /** + * Updates the header ByteBuffer with the provided length. The header comprises the frame type and message length. + */ + private void updateHeader(LogLevel logLevel, LogFormat logFormat, int length) { + this.headerBuf.clear(); + this.headerBuf.putInt(FrameType.getValue(logLevel, logFormat)); + this.headerBuf.putInt(length); + this.headerBuf.putLong(timestamp()); + this.headerBuf.flip(); + } + + @Override + public void close() throws IOException { + this.logOutputStream.close(); + } + +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/JsonLogFormatter.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/JsonLogFormatter.java new file mode 100644 index 000000000..f1051a216 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/JsonLogFormatter.java @@ -0,0 +1,64 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client.logging; + +import com.amazonaws.services.lambda.runtime.api.client.api.LambdaContext; +import com.amazonaws.services.lambda.runtime.logging.LogLevel; +import com.amazonaws.services.lambda.runtime.serialization.PojoSerializer; +import com.amazonaws.services.lambda.runtime.serialization.factories.GsonFactory; +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; + +public class JsonLogFormatter implements LogFormatter { + private static final DateTimeFormatter dateFormatter = + DateTimeFormatter. + ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"). + withZone(ZoneId.of("UTC")); + private final PojoSerializer serializer = GsonFactory.getInstance().getSerializer(StructuredLogMessage.class); + + private ThreadLocal lambdaContext = new ThreadLocal<>(); + + @Override + public String format(String message, LogLevel logLevel) { + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + StructuredLogMessage msg = createLogMessage(message, logLevel); + serializer.toJson(msg, stream); + stream.write('\n'); + return new String(stream.toByteArray(), StandardCharsets.UTF_8); + } + + private StructuredLogMessage createLogMessage(String message, LogLevel logLevel) { + StructuredLogMessage msg = new StructuredLogMessage(); + msg.timestamp = dateFormatter.format(LocalDateTime.now()); + msg.message = message; + msg.level = logLevel; + + LambdaContext lambdaContextForCurrentThread = lambdaContext.get(); + if (lambdaContextForCurrentThread != null) { + msg.AWSRequestId = lambdaContextForCurrentThread.getAwsRequestId(); + msg.tenantId = lambdaContextForCurrentThread.getTenantId(); + } + + return msg; + } + + + /** + * Function to set the context for every invocation. + * This way the logger will be able to attach additional information to the log packet. + */ + @Override + public void setLambdaContext(LambdaContext context) { + if (context == null) { + lambdaContext.remove(); + } else { + lambdaContext.set(context); + } + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/LambdaContextLogger.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/LambdaContextLogger.java new file mode 100644 index 000000000..dd3569126 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/LambdaContextLogger.java @@ -0,0 +1,40 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client.logging; + +import com.amazonaws.services.lambda.runtime.logging.LogFormat; +import com.amazonaws.services.lambda.runtime.logging.LogLevel; +import java.io.Closeable; +import java.io.IOException; +import static java.nio.charset.StandardCharsets.UTF_8; + +public class LambdaContextLogger extends AbstractLambdaLogger implements Closeable { + // If a null string is passed in, replace it with "null", + // replicating the behavior of System.out.println(null); + private static final byte[] NULL_BYTES_VALUE = "null".getBytes(UTF_8); + + private final transient LogSink sink; + + public LambdaContextLogger(LogSink sink, LogLevel logLevel, LogFormat logFormat) { + super(logLevel, logFormat); + this.sink = sink; + } + + @Override + protected void logMessage(byte[] message, LogLevel logLevel) { + if (message == null) { + sink.log(logLevel, this.logFormat, NULL_BYTES_VALUE); + } else { + sink.log(logLevel, this.logFormat, message); + } + } + + @Override + public void close() throws IOException { + sink.close(); + + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/LogFiltering.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/LogFiltering.java new file mode 100644 index 000000000..a9bdec86c --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/LogFiltering.java @@ -0,0 +1,20 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client.logging; + +import com.amazonaws.services.lambda.runtime.logging.LogLevel; + +public class LogFiltering { + private final LogLevel minimumLogLevel; + + public LogFiltering(LogLevel minimumLogLevel) { + this.minimumLogLevel = minimumLogLevel; + } + + boolean isEnabled(LogLevel logLevel) { + return (logLevel == LogLevel.UNDEFINED || logLevel.ordinal() >= minimumLogLevel.ordinal()); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/LogFormatter.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/LogFormatter.java new file mode 100644 index 000000000..283b52289 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/LogFormatter.java @@ -0,0 +1,16 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client.logging; + +import com.amazonaws.services.lambda.runtime.api.client.api.LambdaContext; +import com.amazonaws.services.lambda.runtime.logging.LogLevel; + +public interface LogFormatter { + String format(String message, LogLevel logLevel); + + default void setLambdaContext(LambdaContext context) { + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/LogSink.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/LogSink.java new file mode 100644 index 000000000..769adb77d --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/LogSink.java @@ -0,0 +1,18 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client.logging; + +import com.amazonaws.services.lambda.runtime.logging.LogFormat; +import com.amazonaws.services.lambda.runtime.logging.LogLevel; +import java.io.Closeable; + +public interface LogSink extends Closeable { + + void log(byte[] message); + + void log(LogLevel logLevel, LogFormat logFormat, byte[] message); + +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/StdOutLogSink.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/StdOutLogSink.java new file mode 100644 index 000000000..90e7d39c2 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/StdOutLogSink.java @@ -0,0 +1,29 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client.logging; + +import com.amazonaws.services.lambda.runtime.logging.LogFormat; +import com.amazonaws.services.lambda.runtime.logging.LogLevel; +import java.io.IOException; + +public class StdOutLogSink implements LogSink { + @Override + public void log(byte[] message) { + log(LogLevel.UNDEFINED, LogFormat.TEXT, message); + } + + public synchronized void log(LogLevel logLevel, LogFormat logFormat, byte[] message) { + try { + System.out.write(message); + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Override + public void close() { + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/StructuredLogMessage.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/StructuredLogMessage.java new file mode 100644 index 000000000..0ae19961f --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/StructuredLogMessage.java @@ -0,0 +1,16 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client.logging; + +import com.amazonaws.services.lambda.runtime.logging.LogLevel; + +class StructuredLogMessage { + public String timestamp; + public String message; + public LogLevel level; + public String AWSRequestId; + public String tenantId; +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/TextLogFormatter.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/TextLogFormatter.java new file mode 100644 index 000000000..5424bd4bd --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/logging/TextLogFormatter.java @@ -0,0 +1,32 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client.logging; + +import com.amazonaws.services.lambda.runtime.logging.LogLevel; +import java.util.HashMap; +import java.util.Map; + +public class TextLogFormatter implements LogFormatter { + private static final Map logLevelMapper = new HashMap() { + { + for (LogLevel logLevel: LogLevel.values()) { + put(logLevel, "[" + logLevel.toString() + "] "); + } + } + }; + + @Override + public String format(String message, LogLevel logLevel) { + if (logLevel == LogLevel.UNDEFINED) { + return message; + } + + return new StringBuilder(). + append(logLevelMapper. + get(logLevel)).append(message). + toString(); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/DtoSerializers.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/DtoSerializers.java new file mode 100644 index 000000000..9f0045e0d --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/DtoSerializers.java @@ -0,0 +1,42 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +package com.amazonaws.services.lambda.runtime.api.client.runtimeapi; + +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.ErrorRequest; +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.XRayErrorCause; +import com.amazonaws.services.lambda.runtime.serialization.PojoSerializer; +import com.amazonaws.services.lambda.runtime.serialization.factories.GsonFactory; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +public class DtoSerializers { + + public static byte[] serialize(ErrorRequest error) { + return serialize(error, SingletonHelper.LAMBDA_ERROR_SERIALIZER); + } + + public static byte[] serialize(XRayErrorCause xRayErrorCause) { + return serialize(xRayErrorCause, SingletonHelper.X_RAY_ERROR_CAUSE_SERIALIZER); + } + + private static byte[] serialize(T pojo, PojoSerializer serializer) { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + serializer.toJson(pojo, outputStream); + return outputStream.toByteArray(); + } catch (IOException e) { + return null; + } + } + + /** + * Implementation of + * Initialization-on-demand holder idiom + * This way the serializers will be loaded lazily + */ + private static class SingletonHelper { + private static final PojoSerializer LAMBDA_ERROR_SERIALIZER = GsonFactory.getInstance().getSerializer(ErrorRequest.class); + private static final PojoSerializer X_RAY_ERROR_CAUSE_SERIALIZER = GsonFactory.getInstance().getSerializer(XRayErrorCause.class); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/JniHelper.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/JniHelper.java new file mode 100644 index 000000000..349b4ab07 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/JniHelper.java @@ -0,0 +1,66 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +package com.amazonaws.services.lambda.runtime.api.client.runtimeapi; + +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.List; + +public class JniHelper { + + private static final String NATIVE_LIB_PATH = "/tmp/.libaws-lambda-jni.so"; + private static final String NATIVE_CLIENT_JNI_PROPERTY = "com.amazonaws.services.lambda.runtime.api.client.runtimeapi.NativeClient.JNI"; + + /** + * Unpacks JNI library from the JAR to a temporary location and tries to load it using System.load() + * Implementation based on AWS CRT + * (ref. ...) + * + * @param libsToTry - array of native libraries to try + */ + public static void load() { + String jniLib = System.getProperty(NATIVE_CLIENT_JNI_PROPERTY); + if (jniLib != null) { + System.load(jniLib); + } else { + String[] libsToTry = new String[]{ + "libaws-lambda-jni.linux-x86_64.so", + "libaws-lambda-jni.linux-aarch_64.so", + "libaws-lambda-jni.linux_musl-x86_64.so", + "libaws-lambda-jni.linux_musl-aarch_64.so" + }; + unpackAndLoad(libsToTry, NativeClient.class); + } + } + + private static void unpackAndLoad(String[] libsToTry, Class clazz) { + List errorMessages = new ArrayList<>(); + for (String libToTry : libsToTry) { + try (InputStream inputStream = clazz.getResourceAsStream( + Paths.get("/jni", libToTry).toString())) { + if (inputStream == null) { + throw new FileNotFoundException("Specified file not in the JAR: " + libToTry); + } + Files.copy(inputStream, Paths.get(NATIVE_LIB_PATH), StandardCopyOption.REPLACE_EXISTING); + System.load(NATIVE_LIB_PATH); + return; + } catch (UnsatisfiedLinkError | Exception e) { + errorMessages.add(e.getMessage()); + } + } + + for (int i = 0; i < libsToTry.length; ++i) { + System.err.println("Failed to load the native runtime interface client library " + + libsToTry[i] + + ". Exception: " + + errorMessages.get(i)); + } + System.exit(-1); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaError.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaError.java new file mode 100644 index 000000000..cb59a8c00 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaError.java @@ -0,0 +1,27 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +package com.amazonaws.services.lambda.runtime.api.client.runtimeapi; + +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.ErrorRequest; +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.XRayErrorCause; + +public class LambdaError { + + public final ErrorRequest errorRequest; + + public final XRayErrorCause xRayErrorCause; + + public final RapidErrorType errorType; + + public LambdaError(ErrorRequest errorRequest, XRayErrorCause xRayErrorCause, RapidErrorType errorType) { + this.errorRequest = errorRequest; + this.xRayErrorCause = xRayErrorCause; + this.errorType = errorType; + } + + public LambdaError(ErrorRequest errorRequest, RapidErrorType errorType) { + this(errorRequest, null, errorType); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaRuntimeApiClient.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaRuntimeApiClient.java new file mode 100644 index 000000000..042bd2579 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaRuntimeApiClient.java @@ -0,0 +1,59 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +package com.amazonaws.services.lambda.runtime.api.client.runtimeapi; + +import com.amazonaws.services.lambda.runtime.api.client.logging.LambdaContextLogger; +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.InvocationRequest; +import java.io.IOException; + +/** + * Java interface for + * Lambda Runtime API + */ +public interface LambdaRuntimeApiClient { + + /** + * Report Init error + * @param error error to report + */ + void reportInitError(LambdaError error) throws IOException; + + /** + * Get next invocation + */ + InvocationRequest nextInvocation() throws IOException; + + /** + * Get next invocation with exponential backoff + */ + InvocationRequest nextInvocationWithExponentialBackoff(LambdaContextLogger lambdaLogger) throws Exception; + + /** + * Report invocation success + * @param requestId request id + * @param response byte array representing response + * @param invocationId invocation id for cross-wiring protection (may be null) + */ + void reportInvocationSuccess(String requestId, byte[] response, String invocationId) throws IOException; + + /** + * Report invocation error + * @param requestId request id + * @param error error to report + * @param invocationId invocation id for cross-wiring protection (may be null) + */ + void reportInvocationError(String requestId, LambdaError error, String invocationId) throws IOException; + + /** + * SnapStart endpoint to report that beforeCheckoint hooks were executed + */ + void restoreNext() throws IOException; + + /** + * SnapStart endpoint to report errors during afterRestore hooks execution + * @param error error to report + */ + void reportRestoreError(LambdaError error) throws IOException; +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaRuntimeApiClientImpl.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaRuntimeApiClientImpl.java new file mode 100644 index 000000000..fce12eade --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaRuntimeApiClientImpl.java @@ -0,0 +1,241 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +package com.amazonaws.services.lambda.runtime.api.client.runtimeapi; + +import com.amazonaws.services.lambda.runtime.api.client.UserFault; +import com.amazonaws.services.lambda.runtime.api.client.logging.LambdaContextLogger; +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.InvocationRequest; +import com.amazonaws.services.lambda.runtime.logging.LogFormat; +import com.amazonaws.services.lambda.runtime.logging.LogLevel; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.function.Supplier; +import static java.net.HttpURLConnection.HTTP_ACCEPTED; +import static java.net.HttpURLConnection.HTTP_OK; +import static java.nio.charset.StandardCharsets.UTF_8; + +public class LambdaRuntimeApiClientImpl implements LambdaRuntimeApiClient { + + static final String USER_AGENT = String.format( + "aws-lambda-java/%s-%s", + System.getProperty("java.vendor.version"), + LambdaRuntimeApiClientImpl.class.getPackage().getImplementationVersion()); + + private static final String DEFAULT_CONTENT_TYPE = "application/json"; + private static final String XRAY_ERROR_CAUSE_HEADER = "Lambda-Runtime-Function-XRay-Error-Cause"; + private static final String ERROR_TYPE_HEADER = "Lambda-Runtime-Function-Error-Type"; + private static final String INVOCATION_ID_HEADER = "Lambda-Runtime-Invocation-Id"; + // 1MiB + private static final int XRAY_ERROR_CAUSE_MAX_HEADER_SIZE = 1024 * 1024; + + // ~32 Seconds Max Backoff. + private static final long MAX_BACKOFF_PERIOD_MS = 1024 * 32; + private static final long INITIAL_BACKOFF_PERIOD_MS = 100; + private static final int MAX_NUMBER_OF_RETRIALS = 5; + + private final String baseUrl; + private final String invocationEndpoint; + + public LambdaRuntimeApiClientImpl(String hostnameAndPort) { + Objects.requireNonNull(hostnameAndPort, "hostnameAndPort cannot be null"); + this.baseUrl = "http://" + hostnameAndPort; + this.invocationEndpoint = this.baseUrl + "/2018-06-01/runtime/invocation/"; + NativeClient.init(hostnameAndPort); + } + + @Override + public void reportInitError(LambdaError error) throws IOException { + String endpoint = this.baseUrl + "/2018-06-01/runtime/init/error"; + reportLambdaError(endpoint, error, XRAY_ERROR_CAUSE_MAX_HEADER_SIZE, null); + } + + @Override + public InvocationRequest nextInvocation() { + return NativeClient.next(); + } + + /* + * Retry immediately then retry with exponential backoff. + */ + public static T getSupplierResultWithExponentialBackoff(LambdaContextLogger lambdaLogger, long initialDelayMS, long maxBackoffPeriodMS, int maxNumOfAttempts, Supplier supplier, Function exceptionMessageComposer, Exception maxRetriesException) throws Exception { + long delayMS = initialDelayMS; + for (int attempts = 0; attempts < maxNumOfAttempts; attempts++) { + boolean isFirstAttempt = attempts == 0; + boolean isLastAttempt = (attempts + 1) == maxNumOfAttempts; + + // Try and log whichever exceptions happened + try { + return supplier.get(); + } catch (Exception e) { + String logMessage = exceptionMessageComposer.apply(e); + if (!isLastAttempt) { + logMessage += String.format("\nRetrying%s", isFirstAttempt ? "." : String.format(" in %d ms.", delayMS)); + } + + lambdaLogger.log(logMessage, lambdaLogger.getLogFormat() == LogFormat.JSON ? LogLevel.ERROR : LogLevel.UNDEFINED); + } + + // throw if ran out of attempts. + if (isLastAttempt) { + throw maxRetriesException; + } + + // update the delay duration. + if (!isFirstAttempt) { + try { + Thread.sleep(delayMS); + delayMS = Math.min(delayMS * 2, maxBackoffPeriodMS); + } catch (InterruptedException e) { + Thread.interrupted(); + } + } + } + + // Should Not be reached. + throw new IllegalStateException(); + } + + @Override + public InvocationRequest nextInvocationWithExponentialBackoff(LambdaContextLogger lambdaLogger) throws Exception { + Supplier nextInvocationSupplier = () -> nextInvocation(); + Function exceptionMessageComposer = (e) -> { + return String.format("Runtime Loop on Thread ID: %s Failed to fetch next invocation.\n%s", Thread.currentThread().getName(), UserFault.trace(e)); + }; + + return getSupplierResultWithExponentialBackoff( + lambdaLogger, + INITIAL_BACKOFF_PERIOD_MS, + MAX_BACKOFF_PERIOD_MS, + MAX_NUMBER_OF_RETRIALS, + nextInvocationSupplier, + exceptionMessageComposer, + new LambdaRuntimeClientMaxRetriesExceededException("Get Next Invocation") + ); + } + + @Override + public void reportInvocationSuccess(String requestId, byte[] response, String invocationId) { + byte[] invocationIdBytes = invocationId != null ? invocationId.getBytes(UTF_8) : null; + NativeClient.postInvocationResponse(requestId.getBytes(UTF_8), response, invocationIdBytes); + } + + @Override + public void reportInvocationError(String requestId, LambdaError error, String invocationId) throws IOException { + String endpoint = invocationEndpoint + requestId + "/error"; + reportLambdaError(endpoint, error, XRAY_ERROR_CAUSE_MAX_HEADER_SIZE, invocationId); + } + + @Override + public void restoreNext() throws IOException { + String endpoint = this.baseUrl + "/2018-06-01/runtime/restore/next"; + int responseCode = doGet(endpoint); + if (responseCode != HTTP_OK) { + throw new LambdaRuntimeClientException(endpoint, responseCode); + } + } + + @Override + public void reportRestoreError(LambdaError error) throws IOException { + String endpoint = this.baseUrl + "/2018-06-01/runtime/restore/error"; + reportLambdaError(endpoint, error, XRAY_ERROR_CAUSE_MAX_HEADER_SIZE, null); + } + + void reportLambdaError(String endpoint, LambdaError error, int maxXrayHeaderSize, String invocationId) throws IOException { + Map headers = new HashMap<>(); + headers.put(ERROR_TYPE_HEADER, error.errorType.getRapidError()); + + if (invocationId != null) { + headers.put(INVOCATION_ID_HEADER, invocationId); + } + + if (error.xRayErrorCause != null) { + byte[] xRayErrorCauseJson = DtoSerializers.serialize(error.xRayErrorCause); + if (xRayErrorCauseJson != null && xRayErrorCauseJson.length < maxXrayHeaderSize) { + headers.put(XRAY_ERROR_CAUSE_HEADER, new String(xRayErrorCauseJson)); + } + } + + byte[] payload = DtoSerializers.serialize(error.errorRequest); + int responseCode = doPost(endpoint, headers, payload); + if (responseCode != HTTP_ACCEPTED) { + throw new LambdaRuntimeClientException(endpoint, responseCode); + } + } + + private int doPost(String endpoint, + Map headers, + byte[] payload) throws IOException { + URL url = createUrl(endpoint); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Content-Type", DEFAULT_CONTENT_TYPE); + conn.setRequestProperty("User-Agent", USER_AGENT); + + for (Map.Entry header : headers.entrySet()) { + conn.setRequestProperty(header.getKey(), header.getValue()); + } + + conn.setFixedLengthStreamingMode(payload.length); + conn.setDoOutput(true); + + try (OutputStream outputStream = conn.getOutputStream()) { + outputStream.write(payload); + } + + // get response code before closing the stream + int responseCode = conn.getResponseCode(); + // don't need to read the response, close stream to ensure connection re-use + closeInputStreamQuietly(conn); + + return responseCode; + } + + private int doGet(String endpoint) throws IOException { + URL url = createUrl(endpoint); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + conn.setRequestProperty("User-Agent", USER_AGENT); + + int responseCode = conn.getResponseCode(); + closeInputStreamQuietly(conn); + + return responseCode; + } + + private URL createUrl(String endpoint) { + try { + return new URL(endpoint); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + } + + private void closeInputStreamQuietly(HttpURLConnection conn) { + + InputStream inputStream; + try { + inputStream = conn.getInputStream(); + } catch (IOException e) { + return; + } + + if (inputStream == null) { + return; + } + try { + inputStream.close(); + } catch (IOException e) { + // ignore + } + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaRuntimeClientException.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaRuntimeClientException.java new file mode 100644 index 000000000..d9f0341ae --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaRuntimeClientException.java @@ -0,0 +1,11 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +package com.amazonaws.services.lambda.runtime.api.client.runtimeapi; + +public class LambdaRuntimeClientException extends RuntimeException { + public LambdaRuntimeClientException(String message, int responseCode) { + super(message + " Response code: '" + responseCode + "'."); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaRuntimeClientMaxRetriesExceededException.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaRuntimeClientMaxRetriesExceededException.java new file mode 100644 index 000000000..467afa25c --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaRuntimeClientMaxRetriesExceededException.java @@ -0,0 +1,15 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +package com.amazonaws.services.lambda.runtime.api.client.runtimeapi; + +public class LambdaRuntimeClientMaxRetriesExceededException extends LambdaRuntimeClientException { + // 429 is possible; however, that is more appropriate when a server is responding to a spamming client that it wants to rate limit. + // In Our case, however, the RIC is a client that is not able to get a response from an upstream server, so 500 is more appropriate. + public LambdaRuntimeClientMaxRetriesExceededException(String operationName) { + super("Maximum Number of retries have been exceed" + (operationName.equals(null) + ? String.format(" for the %s operation.", operationName) + : "."), 500); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/NativeClient.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/NativeClient.java new file mode 100644 index 000000000..5c690814b --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/NativeClient.java @@ -0,0 +1,26 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +package com.amazonaws.services.lambda.runtime.api.client.runtimeapi; + +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.InvocationRequest; +import static com.amazonaws.services.lambda.runtime.api.client.runtimeapi.LambdaRuntimeApiClientImpl.USER_AGENT; + +/** + * This module defines the native Runtime Interface Client which is responsible for HTTP + * interactions with the Runtime API. + */ +class NativeClient { + static void init(String awsLambdaRuntimeApi) { + JniHelper.load(); + initializeClient(USER_AGENT.getBytes(), awsLambdaRuntimeApi.getBytes()); + } + + static native void initializeClient(byte[] userAgent, byte[] awsLambdaRuntimeApi); + + static native InvocationRequest next(); + + static native void postInvocationResponse(byte[] requestId, byte[] response, byte[] invocationId); + +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/RapidErrorType.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/RapidErrorType.java new file mode 100644 index 000000000..b471ce3f5 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/RapidErrorType.java @@ -0,0 +1,16 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +package com.amazonaws.services.lambda.runtime.api.client.runtimeapi; + +public enum RapidErrorType { + BadFunctionCode, + UserException, + BeforeCheckpointError, + AfterRestoreError; + + public String getRapidError() { + return "Runtime." + this; + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/converters/LambdaErrorConverter.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/converters/LambdaErrorConverter.java new file mode 100644 index 000000000..a2520bf74 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/converters/LambdaErrorConverter.java @@ -0,0 +1,32 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +package com.amazonaws.services.lambda.runtime.api.client.runtimeapi.converters; + +import com.amazonaws.services.lambda.runtime.api.client.UserFault; +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.ErrorRequest; + +public class LambdaErrorConverter { + private LambdaErrorConverter() { + } + + public static ErrorRequest fromUserFault(UserFault userFault) { + // Not setting stacktrace for compatibility with legacy/native runtime + return new ErrorRequest(userFault.msg, userFault.exception, null); + } + + public static ErrorRequest fromThrowable(Throwable throwable) { + String errorMessage = throwable.getLocalizedMessage() == null + ? throwable.getClass().getName() + : throwable.getLocalizedMessage(); + String errorType = throwable.getClass().getName(); + + StackTraceElement[] trace = throwable.getStackTrace(); + String[] stackTrace = new String[trace.length]; + for (int i = 0; i < trace.length; i++) { + stackTrace[i] = trace[i].toString(); + } + return new ErrorRequest(errorMessage, errorType, stackTrace); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/converters/XRayErrorCauseConverter.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/converters/XRayErrorCauseConverter.java new file mode 100644 index 000000000..7065bc764 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/converters/XRayErrorCauseConverter.java @@ -0,0 +1,58 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +package com.amazonaws.services.lambda.runtime.api.client.runtimeapi.converters; + +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.StackElement; +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.XRayErrorCause; +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.XRayException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class XRayErrorCauseConverter { + private XRayErrorCauseConverter() { + } + + public static XRayErrorCause fromThrowable(Throwable throwable) { + String workingDirectory = System.getProperty("user.dir"); + XRayException xRayException = getXRayExceptionFromThrowable(throwable); + Collection exceptions = Collections.singletonList(xRayException); + Collection paths = Arrays.stream(throwable.getStackTrace()). + map(XRayErrorCauseConverter::determineFileName). + collect(Collectors.toSet()); + + return new XRayErrorCause(workingDirectory, exceptions, paths); + } + + static XRayException getXRayExceptionFromThrowable(Throwable throwable) { + String message = throwable.getMessage(); + String type = throwable.getClass().getName(); + List stack = Arrays.stream(throwable.getStackTrace()). + map(XRayErrorCauseConverter::convertStackTraceElement). + collect(Collectors.toList()); + return new XRayException(message, type, stack); + } + + static String determineFileName(StackTraceElement e) { + String fileName = null; + if (e.getFileName() != null) { + fileName = e.getFileName(); + } + if (fileName == null) { + String className = e.getClassName(); + fileName = className.substring(className.lastIndexOf('.') + 1) + ".java"; + } + return fileName; + } + + static StackElement convertStackTraceElement(StackTraceElement e) { + return new StackElement( + e.getMethodName(), + determineFileName(e), + e.getLineNumber()); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/dto/ErrorRequest.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/dto/ErrorRequest.java new file mode 100644 index 000000000..d5886a67d --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/dto/ErrorRequest.java @@ -0,0 +1,21 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +package com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto; + +public class ErrorRequest { + public String errorMessage; + public String errorType; + public String[] stackTrace; + + @SuppressWarnings("unused") + public ErrorRequest() { + } + + public ErrorRequest(String errorMessage, String errorType, String[] stackTrace) { + this.errorMessage = errorMessage; + this.errorType = errorType; + this.stackTrace = stackTrace; + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/dto/InvocationRequest.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/dto/InvocationRequest.java new file mode 100644 index 000000000..a589cb024 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/dto/InvocationRequest.java @@ -0,0 +1,130 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +package com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto; + +/** + * An invocation request represents the response of the runtime API's next invocation API. + */ +public class InvocationRequest { + + /** + * The Lambda request ID associated with the request. + */ + private String id; + + /** + * The X-Ray tracing ID. + */ + private String xrayTraceId; + + /** + * The ARN of the Lambda function being invoked. + */ + private String invokedFunctionArn; + + /** + * Function execution deadline counted in milliseconds since the Unix epoch. + */ + private long deadlineTimeInMs; + + /** + * The client context header. This field is populated when the function is invoked from a mobile client. + */ + private String clientContext; + + /** + * The Cognito Identity context for the invocation. This field is populated when the function is invoked with AWS + * credentials obtained from Cognito Identity. + */ + private String cognitoIdentity; + + /** + * The tenant ID associated with the request. + */ + private String tenantId; + + /** + * The invocation ID for cross-wiring protection. + */ + private String invocationId; + + private byte[] content; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getXrayTraceId() { + return xrayTraceId; + } + + public void setXrayTraceId(String xrayTraceId) { + this.xrayTraceId = xrayTraceId; + } + + public String getInvokedFunctionArn() { + return invokedFunctionArn; + } + + @SuppressWarnings("unused") + public void setInvokedFunctionArn(String invokedFunctionArn) { + this.invokedFunctionArn = invokedFunctionArn; + } + + public long getDeadlineTimeInMs() { + return deadlineTimeInMs; + } + + @SuppressWarnings("unused") + public void setDeadlineTimeInMs(long deadlineTimeInMs) { + this.deadlineTimeInMs = deadlineTimeInMs; + } + + public String getClientContext() { + return clientContext; + } + + @SuppressWarnings("unused") + public void setClientContext(String clientContext) { + this.clientContext = clientContext; + } + + public String getCognitoIdentity() { + return cognitoIdentity; + } + + @SuppressWarnings("unused") + public void setCognitoIdentity(String cognitoIdentity) { + this.cognitoIdentity = cognitoIdentity; + } + + public String getTenantId() { + return tenantId; + } + + public void setTenantId(String tenantId) { + this.tenantId = tenantId; + } + + public String getInvocationId() { + return invocationId; + } + + public void setInvocationId(String invocationId) { + this.invocationId = invocationId; + } + + public byte[] getContent() { + return content; + } + + public void setContent(byte[] content) { + this.content = content; + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/dto/StackElement.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/dto/StackElement.java new file mode 100644 index 000000000..679f8bf9f --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/dto/StackElement.java @@ -0,0 +1,21 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +package com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto; + +public class StackElement { + public String label; + public String path; + public int line; + + @SuppressWarnings("unused") + public StackElement() { + } + + public StackElement(String label, String path, int line) { + this.label = label; + this.path = path; + this.line = line; + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/dto/XRayErrorCause.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/dto/XRayErrorCause.java new file mode 100644 index 000000000..cc5bee8a7 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/dto/XRayErrorCause.java @@ -0,0 +1,24 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +package com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto; + +import java.util.Collection; + +public class XRayErrorCause { + public String working_directory; + public Collection exceptions; + public Collection paths; + + @SuppressWarnings("unused") + public XRayErrorCause() { + + } + + public XRayErrorCause(String working_directory, Collection exceptions, Collection paths) { + this.working_directory = working_directory; + this.exceptions = exceptions; + this.paths = paths; + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/dto/XRayException.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/dto/XRayException.java new file mode 100644 index 000000000..2b17fd5f2 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/dto/XRayException.java @@ -0,0 +1,23 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +package com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto; + +import java.util.List; + +public class XRayException { + public String message; + public String type; + public List stack; + + @SuppressWarnings("unused") + public XRayException() { + } + + public XRayException(String message, String type, List stack) { + this.message = message; + this.type = type; + this.stack = stack; + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/util/ConcurrencyConfig.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/util/ConcurrencyConfig.java new file mode 100644 index 000000000..a768e240e --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/util/ConcurrencyConfig.java @@ -0,0 +1,50 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client.util; + +import com.amazonaws.services.lambda.runtime.api.client.ReservedRuntimeEnvironmentVariables; +import com.amazonaws.services.lambda.runtime.api.client.UserFault; +import com.amazonaws.services.lambda.runtime.api.client.logging.LambdaContextLogger; +import com.amazonaws.services.lambda.runtime.logging.LogFormat; +import com.amazonaws.services.lambda.runtime.logging.LogLevel; + +public class ConcurrencyConfig { + private final int numberOfPlatformThreads; + private final String INVALID_CONFIG_MESSAGE_PREFIX = String.format("User configured %s is invalid.", ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_MAX_CONCURRENCY); + + public ConcurrencyConfig(LambdaContextLogger logger) { + this(logger, new EnvReader()); + } + + public ConcurrencyConfig(LambdaContextLogger logger, EnvReader envReader) { + int readNumOfPlatformThreads = 0; + try { + String readLambdaMaxConcurrencyEnvVar = envReader.getEnv(ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_MAX_CONCURRENCY); + + if (readLambdaMaxConcurrencyEnvVar != null) { + readNumOfPlatformThreads = Integer.parseInt(readLambdaMaxConcurrencyEnvVar); + } + } catch (Exception e) { + String message = String.format("%s\n%s", INVALID_CONFIG_MESSAGE_PREFIX, UserFault.trace(e)); + logger.log(message, logger.getLogFormat() == LogFormat.JSON ? LogLevel.ERROR : LogLevel.UNDEFINED); + throw e; + } + + this.numberOfPlatformThreads = readNumOfPlatformThreads; + } + + public String getConcurrencyConfigMessage() { + return String.format("Starting %d concurrent function handler threads.", this.numberOfPlatformThreads); + } + + public boolean isMultiConcurrent() { + return this.numberOfPlatformThreads >= 1; + } + + public int getNumberOfPlatformThreads() { + return numberOfPlatformThreads; + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/util/EnvReader.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/util/EnvReader.java new file mode 100644 index 000000000..840bd440c --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/util/EnvReader.java @@ -0,0 +1,25 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client.util; + +import java.util.Map; + +public class EnvReader { + + public Map getEnv() { + return System.getenv(); + } + + public String getEnv(String envVariableName) { + return System.getenv(envVariableName); + } + + public String getEnvOrDefault(String envVariableName, String defaultVal) { + String val = getEnv(envVariableName); + return val == null ? defaultVal : val; + } + +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/util/LambdaOutputStream.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/util/LambdaOutputStream.java new file mode 100644 index 000000000..22d01b0aa --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/util/LambdaOutputStream.java @@ -0,0 +1,33 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client.util; + +import java.io.IOException; +import java.io.OutputStream; + +public class LambdaOutputStream extends OutputStream { + private final OutputStream inner; + + public LambdaOutputStream(OutputStream inner) { + this.inner = inner; + } + + @Override + public void write(int b) throws IOException { + write(new byte[]{(byte) b}); + } + + @Override + public void write(byte[] bytes) throws IOException { + write(bytes, 0, bytes.length); + + } + + @Override + public void write(byte[] bytes, int offset, int length) throws IOException { + inner.write(bytes, offset, length); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/util/UnsafeUtil.java b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/util/UnsafeUtil.java new file mode 100644 index 000000000..f11d4357c --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/java/com/amazonaws/services/lambda/runtime/api/client/util/UnsafeUtil.java @@ -0,0 +1,42 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client.util; + +import java.lang.reflect.Field; +import sun.misc.Unsafe; + +/** + * Utilities for easy access to sun.misc.Unsafe + */ +public final class UnsafeUtil { + public static final Unsafe TheUnsafe; + + static { + try { + final Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); + theUnsafe.setAccessible(true); + TheUnsafe = (Unsafe) theUnsafe.get(null); + } catch (Exception e) { + throw new Error("failed to load Unsafe", e); + } + } + + private UnsafeUtil() { + } + + public static void disableIllegalAccessWarning() { + try { + Class illegalAccessLoggerClass = Class.forName("jdk.internal.module.IllegalAccessLogger"); + Field loggerField = illegalAccessLoggerClass.getDeclaredField("logger"); + TheUnsafe.putObjectVolatile(illegalAccessLoggerClass, TheUnsafe.staticFieldOffset(loggerField), null); + } catch (Throwable t) { /* ignore */ } + } + + public static RuntimeException throwException(Throwable t) { + TheUnsafe.throwException(t); + throw new Error("should never get here"); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/jni/Dockerfile.glibc b/aws-lambda-java-runtime-interface-client/src/main/jni/Dockerfile.glibc new file mode 100644 index 000000000..ab6f83b69 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/jni/Dockerfile.glibc @@ -0,0 +1,69 @@ +ARG BASE_REGISTRY=public.ecr.aws +FROM ${BASE_REGISTRY}/amazonlinux/amazonlinux:2 + +ARG CURL_VERSION +ARG AWS_REGION + +RUN if [ -n "${AWS_REGION}" ]; then echo "${AWS_REGION}" > /etc/yum/vars/awsregion; fi + +RUN yum install -y \ + tar \ + gzip \ + make \ + patch \ + gcc \ + gcc-c++ \ + java-11-amazon-corretto + +# Install curl dependency +COPY ./deps/curl-$CURL_VERSION.tar.gz /src/deps/ +COPY ./deps/curl_001_disable_wakeup.patch /src/deps/ + +RUN tar xzf /src/deps/curl-$CURL_VERSION.tar.gz -C /src/deps + +WORKDIR /src/deps/curl-$CURL_VERSION +RUN patch lib/multihandle.h ../curl_001_disable_wakeup.patch +RUN ./configure \ + --prefix $(pwd)/../artifacts \ + --disable-shared \ + --without-ssl \ + --with-pic \ + --without-zlib && \ + make && \ + make install + +# Install prebuilt aws-lambda-cpp dependency. The static library and headers +# were fetched and GPG-verified on the host by build-jni-lib.sh; here we only +# COPY them into the artifacts tree the native client links against (the build +# container never reaches the network). +COPY ./deps/aws-lambda-cpp/include /src/deps/artifacts/include +COPY ./deps/aws-lambda-cpp/lib/libaws-lambda-runtime.a /src/deps/artifacts/lib/ + +# Build native client +ADD *.cpp *.h /src/ +WORKDIR /src +ENV JAVA_HOME=/usr/lib/jvm/java-11-amazon-corretto +RUN /usr/bin/c++ -c \ + -std=gnu++11 \ + -fPIC \ + -I${JAVA_HOME}/include \ + -I${JAVA_HOME}/include/linux \ + -I ./deps/artifacts/include \ + com_amazonaws_services_lambda_runtime_api_client_runtimeapi_NativeClient.cpp -o com_amazonaws_services_lambda_runtime_api_client_runtimeapi_NativeClient.o && \ + /usr/bin/c++ -c \ + -std=gnu++11 \ + -fPIC \ + -I${JAVA_HOME}/include \ + -I${JAVA_HOME}/include/linux \ + -I ./deps/artifacts/include \ + com_amazonaws_services_lambda_crac_DNSManager.cpp -o com_amazonaws_services_lambda_crac_DNSManager.o && \ + /usr/bin/c++ -shared \ + -std=gnu++11 \ + -o aws-lambda-runtime-interface-client.so com_amazonaws_services_lambda_runtime_api_client_runtimeapi_NativeClient.o com_amazonaws_services_lambda_crac_DNSManager.o \ + -L ./deps/artifacts/lib64/ \ + -L ./deps/artifacts/lib/ \ + -laws-lambda-runtime \ + -lcurl \ + -static-libstdc++ \ + -lrt \ + -O2 diff --git a/aws-lambda-java-runtime-interface-client/src/main/jni/Dockerfile.musl b/aws-lambda-java-runtime-interface-client/src/main/jni/Dockerfile.musl new file mode 100644 index 000000000..fa5b98173 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/jni/Dockerfile.musl @@ -0,0 +1,63 @@ +ARG BASE_REGISTRY=public.ecr.aws +FROM ${BASE_REGISTRY}/docker/library/alpine:3 + +ARG CURL_VERSION + +RUN apk update && \ + apk add \ + openjdk11 \ + file \ + g++ \ + gcc \ + make \ + patch \ + perl + +# Install curl dependency +COPY ./deps/curl-$CURL_VERSION.tar.gz /src/deps/ +COPY ./deps/curl_001_disable_wakeup.patch /src/deps/ + +RUN tar xzf /src/deps/curl-$CURL_VERSION.tar.gz -C /src/deps + +WORKDIR /src/deps/curl-$CURL_VERSION +RUN patch lib/multihandle.h ../curl_001_disable_wakeup.patch +RUN ./configure \ + --prefix $(pwd)/../artifacts \ + --disable-shared \ + --without-ssl \ + --with-pic \ + --without-zlib && \ + make && \ + make install + +COPY ./deps/aws-lambda-cpp/include /src/deps/artifacts/include +COPY ./deps/aws-lambda-cpp/lib/libaws-lambda-runtime.a /src/deps/artifacts/lib/ + +# Build native client +ADD *.cpp *.h /src/ +WORKDIR /src + +ENV JAVA_HOME=/usr/lib/jvm/java-11-openjdk +RUN /usr/bin/c++ -c \ + -std=gnu++11 \ + -fPIC \ + -I${JAVA_HOME}/include \ + -I${JAVA_HOME}/include/linux \ + -I ./deps/artifacts/include \ + com_amazonaws_services_lambda_runtime_api_client_runtimeapi_NativeClient.cpp -o com_amazonaws_services_lambda_runtime_api_client_runtimeapi_NativeClient.o && \ + /usr/bin/c++ -c \ + -std=gnu++11 \ + -fPIC \ + -I${JAVA_HOME}/include \ + -I${JAVA_HOME}/include/linux \ + -I ./deps/artifacts/include \ + com_amazonaws_services_lambda_crac_DNSManager.cpp -o com_amazonaws_services_lambda_crac_DNSManager.o && \ + /usr/bin/c++ -shared \ + -std=gnu++11 \ + -o aws-lambda-runtime-interface-client.so com_amazonaws_services_lambda_runtime_api_client_runtimeapi_NativeClient.o com_amazonaws_services_lambda_crac_DNSManager.o \ + -L ./deps/artifacts/lib/ \ + -laws-lambda-runtime \ + -lcurl \ + -static-libstdc++ \ + -static-libgcc \ + -O2 diff --git a/aws-lambda-java-runtime-interface-client/src/main/jni/build-jni-lib.sh b/aws-lambda-java-runtime-interface-client/src/main/jni/build-jni-lib.sh new file mode 100755 index 000000000..28263531b --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/jni/build-jni-lib.sh @@ -0,0 +1,197 @@ +#!/bin/bash -x +# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +set -euo pipefail + +SRC_DIR=$(dirname "$0") +DST_DIR=${1} +MULTI_ARCH=${2} +BUILD_OS=${3} +BUILD_ARCH=${4} +CURL_VERSION=7.83.1 + +BASE_REGISTRY="${BASE_REGISTRY:-public.ecr.aws}" +AWS_REGION="${AWS_REGION:-${AWS_DEFAULT_REGION:-}}" + +# aws-lambda-cpp is consumed as the prebuilt static library published on the +# upstream GitHub release rather than being compiled from a vendored source +# tree. We fetch and GPG-verify it +ALC_VERSION="1.0.1" +ALC_TAG="v${ALC_VERSION}" +ALC_REPO_URL="https://github.com/awslabs/aws-lambda-cpp" +ALC_RELEASE_URL="${ALC_REPO_URL}/releases/download/${ALC_TAG}" +ALC_SIGNING_KEY_URL="https://raw.githubusercontent.com/awslabs/aws-lambda-cpp/${ALC_TAG}/signing-public-key.asc" +ALC_STAGE_DIR="${SRC_DIR}/deps/aws-lambda-cpp" + +function fetch_aws_lambda_cpp() { + arch=$1 + + release_arch="${arch/aarch_64/aarch64}" + + if [ -f "${ALC_STAGE_DIR}/.staged-arch" ] && \ + [ "$(cat "${ALC_STAGE_DIR}/.staged-arch")" == "${release_arch}" ]; then + echo "aws-lambda-cpp ${ALC_VERSION} (${release_arch}) already staged, skipping fetch" + return + fi + + echo "Fetching prebuilt aws-lambda-cpp ${ALC_VERSION} for ${release_arch}" + rm -rf "${ALC_STAGE_DIR}" + mkdir -p "${ALC_STAGE_DIR}/lib" "${ALC_STAGE_DIR}/include" + + local workdir + workdir=$(mktemp -d) + local lib_asset="libaws-lambda-runtime-${release_arch}.a" + + curl -fsSL -o "${workdir}/${lib_asset}" "${ALC_RELEASE_URL}/${lib_asset}" + curl -fsSL -o "${workdir}/${lib_asset}.asc" "${ALC_RELEASE_URL}/${lib_asset}.asc" + curl -fsSL -o "${workdir}/SHA256SUMS" "${ALC_RELEASE_URL}/SHA256SUMS" + curl -fsSL -o "${workdir}/SHA256SUMS.asc" "${ALC_RELEASE_URL}/SHA256SUMS.asc" + curl -fsSL -o "${workdir}/signing-key.asc" "${ALC_SIGNING_KEY_URL}" + + local gnupghome + gnupghome=$(mktemp -d) + gpg --homedir "${gnupghome}" --batch --quiet --import "${workdir}/signing-key.asc" + gpg --homedir "${gnupghome}" --batch --verify "${workdir}/${lib_asset}.asc" "${workdir}/${lib_asset}" + gpg --homedir "${gnupghome}" --batch --verify "${workdir}/SHA256SUMS.asc" "${workdir}/SHA256SUMS" + rm -rf "${gnupghome}" + + # Cross-check the checksum too (defence in depth; SHA256SUMS is itself signed). + ( cd "${workdir}" && grep "${lib_asset}\$" SHA256SUMS | sha256sum -c - ) + + cp "${workdir}/${lib_asset}" "${ALC_STAGE_DIR}/lib/libaws-lambda-runtime.a" + + # Headers aren't a release asset, so take them from the source at the same + # tag. They are declarations only -- every symbol lives in the prebuilt lib. + curl -fsSL -o "${workdir}/src.tar.gz" "${ALC_REPO_URL}/archive/refs/tags/${ALC_TAG}.tar.gz" + tar -xzf "${workdir}/src.tar.gz" -C "${workdir}" "aws-lambda-cpp-${ALC_VERSION}/include" + cp -R "${workdir}/aws-lambda-cpp-${ALC_VERSION}/include/." "${ALC_STAGE_DIR}/include/" + + echo "${release_arch}" > "${ALC_STAGE_DIR}/.staged-arch" + rm -rf "${workdir}" +} + +function get_docker_platform() { + arch=$1 + + if [ "${arch}" == "x86_64" ]; then + echo "linux/amd64" + elif [ "${arch}" == "aarch_64" ]; then + echo "linux/arm64/v8" + else + echo "UNKNOWN_DOCKER_PLATFORM" + fi +} + +function get_target_os() { + libc_impl=$1 + + if [ "${libc_impl}" == "glibc" ]; then + echo "linux" + elif [ "${libc_impl}" == "musl" ]; then + echo "linux_musl" + else + echo "UNKNOWN_OS" + fi +} + +function build_for_libc_arch() { + libc_impl=$1 + arch=$2 + artifact=$3 + + fetch_aws_lambda_cpp "${arch}" + + docker_platform=$(get_docker_platform ${arch}) + + echo "Compiling the native library with libc implementation \`${libc_impl}\` on architecture \`${arch}\` using Docker platform \`${docker_platform}\`" + + if [[ "${MULTI_ARCH}" == "true" ]]; then + docker build --platform="${docker_platform}" -f "${SRC_DIR}/Dockerfile.${libc_impl}" \ + --build-arg CURL_VERSION=${CURL_VERSION} --build-arg BASE_REGISTRY=${BASE_REGISTRY} --build-arg AWS_REGION=${AWS_REGION} "${SRC_DIR}" -o - \ + | tar -xOf - src/aws-lambda-runtime-interface-client.so > "${artifact}" + else + echo "multi-arch not requested, assuming this is a workaround to goofyness when docker buildx is enabled on Linux CI environments." + echo "enabling docker buildx often updates the docker api version, so assuming that docker cli is also too old to use --output type=tar, so doing alternative build-tag-run approach" + image_name="lambda-java-jni-lib-${libc_impl}-${arch}" + + # GitHub actions is using dockerx build under the hood. We need to pass --load option to be able to run the image + # This args is NOT part of the classic docker build command, so we need to check against a GitHub Action env var not to make local build crash. + if [[ "${GITHUB_RUN_ID:+isset}" == "isset" ]]; then + EXTRA_LOAD_ARG="--load" + else + EXTRA_LOAD_ARG="" + fi + + docker build --platform="${docker_platform}" \ + -t "${image_name}" \ + -f "${SRC_DIR}/Dockerfile.${libc_impl}" \ + --build-arg CURL_VERSION=${CURL_VERSION} --build-arg BASE_REGISTRY=${BASE_REGISTRY} --build-arg AWS_REGION=${AWS_REGION} "${SRC_DIR}" ${EXTRA_LOAD_ARG} + + echo "Docker image has been successfully built" + + docker run --rm --entrypoint /bin/cat "${image_name}" \ + /src/aws-lambda-runtime-interface-client.so > "${artifact}" + fi + + [ -f "${artifact}" ] + + # file -b ${artifact} produces lines like this: + # x86_64: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, BuildID[sha1]=582888b42da34895828e1281cbbae15d279175b7, not stripped + # aarch_64: ELF 64-bit LSB shared object, ARM aarch64, version 1 (GNU/Linux), dynamically linked, BuildID[sha1]=fa54218974fb2c17772b6acf22467a2c67a87011, not stripped + # we need to ensure it has the expected architecture in it + # + # cut -d "," -f2 will extract second field (' x86-64' or ' ARM aarch64') + # tr -d '-' removes '-', so we'll have (' x8664' or ' ARM aarch64') + # grep -q is for quiet mode, no output + # ${arch//_} removes '_' chars from the `aarch` variable, (aarch_64 => aarch64, x86_64 => x8664) + if ! file -b "${artifact}" | cut -d "," -f2 | tr -d '-' | grep -q "${arch//_}"; then + echo "${artifact} did not appear to be the correct architecture, check that Docker buildx is enabled" + exit 1 + fi +} + +function get_target_artifact() { + target_os=$1 + target_arch=$2 + + target_file="${DST_DIR}/classes/jni/libaws-lambda-jni.${target_os}-${target_arch}.so" + target_dir=$(dirname "$target_file") + mkdir -p "$target_dir" + echo "$target_file" +} + + + +if [ -n "$BUILD_OS" ] && [ -n "$BUILD_ARCH" ]; then + # build for the specified arch and libc implementation + libc_impl="glibc" + if [ "$BUILD_OS" == "linux_musl" ]; then + libc_impl="musl" + fi + target_artifact=$(get_target_artifact "$BUILD_OS" "$BUILD_ARCH") + build_for_libc_arch "$libc_impl" "$BUILD_ARCH" "$target_artifact" +else + # build for all architectures and libc implementations + declare -a ARCHITECTURES=("x86_64" "aarch_64") + declare -a LIBC_IMPLS=("glibc" "musl") + + host_arch="$(arch)" + case "${host_arch}" in + aarch64|arm64) host_arch="aarch_64" ;; + esac + + for arch in "${ARCHITECTURES[@]}"; do + + if [[ "${MULTI_ARCH}" != "true" ]] && [[ "${host_arch}" != "${arch}" ]]; then + echo "multi arch build not requested and host arch is ${host_arch}, so skipping ${arch}..." + continue + fi + + for libc_impl in "${LIBC_IMPLS[@]}"; do + target_os=$(get_target_os $libc_impl) + target_artifact=$(get_target_artifact "$target_os" "$arch") + build_for_libc_arch "$libc_impl" "$arch" "$target_artifact" + done + + done +fi diff --git a/aws-lambda-java-runtime-interface-client/src/main/jni/com_amazonaws_services_lambda_crac_DNSManager.cpp b/aws-lambda-java-runtime-interface-client/src/main/jni/com_amazonaws_services_lambda_crac_DNSManager.cpp new file mode 100644 index 000000000..ccf5481b9 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/jni/com_amazonaws_services_lambda_crac_DNSManager.cpp @@ -0,0 +1,27 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +#include +#include "macro.h" +#include "com_amazonaws_services_lambda_crac_DNSManager.h" + +JNIEXPORT void JNICALL Java_com_amazonaws_services_lambda_crac_DNSManager_clearCache + (JNIEnv *env, jclass thisClass) { + jclass iNetAddressClass; + jclass concurrentMap; + jfieldID cacheFieldID; + jobject cacheObj; + jmethodID clearMethodID; + CHECK_EXCEPTION(env, iNetAddressClass = env->FindClass("java/net/InetAddress")); + CHECK_EXCEPTION(env, concurrentMap = env->FindClass("java/util/concurrent/ConcurrentMap")); + CHECK_EXCEPTION(env, cacheFieldID = env->GetStaticFieldID(iNetAddressClass, "cache", "Ljava/util/concurrent/ConcurrentMap;")); + CHECK_EXCEPTION(env, cacheObj = (jobject) env->GetStaticObjectField(iNetAddressClass, cacheFieldID)); + CHECK_EXCEPTION(env, clearMethodID = env->GetMethodID(concurrentMap, "clear", "()V")); + CHECK_EXCEPTION(env, env->CallVoidMethod(cacheObj, clearMethodID)); + return; + + ERROR: + // we need to fail silently here + env->ExceptionClear(); +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/jni/com_amazonaws_services_lambda_crac_DNSManager.h b/aws-lambda-java-runtime-interface-client/src/main/jni/com_amazonaws_services_lambda_crac_DNSManager.h new file mode 100644 index 000000000..f26639ba9 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/jni/com_amazonaws_services_lambda_crac_DNSManager.h @@ -0,0 +1,19 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +#include + +#ifndef _Included_com_amazonaws_services_lambda_crac_DNSManager +#define _Included_com_amazonaws_services_lambda_crac_DNSManager +#ifdef __cplusplus +extern "C" { +#endif + +JNIEXPORT void JNICALL Java_com_amazonaws_services_lambda_crac_DNSManager_clearCache + (JNIEnv *, jclass); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/aws-lambda-java-runtime-interface-client/src/main/jni/com_amazonaws_services_lambda_runtime_api_client_runtimeapi_NativeClient.cpp b/aws-lambda-java-runtime-interface-client/src/main/jni/com_amazonaws_services_lambda_runtime_api_client_runtimeapi_NativeClient.cpp new file mode 100644 index 000000000..fb6cd3ca3 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/jni/com_amazonaws_services_lambda_runtime_api_client_runtimeapi_NativeClient.cpp @@ -0,0 +1,157 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +#include +#include "macro.h" +#include "com_amazonaws_services_lambda_runtime_api_client_runtimeapi_NativeClient.h" +#include "aws/lambda-runtime/runtime.h" +#include "aws/lambda-runtime/version.h" + +static aws::lambda_runtime::runtime * CLIENT = nullptr; + +static jint JNI_VERSION = JNI_VERSION_1_8; + +static jclass invocationRequestClass; +static jfieldID invokedFunctionArnField; +static jfieldID deadlineTimeInMsField; +static jfieldID idField; +static jfieldID contentField; +static jfieldID clientContextField; +static jfieldID cognitoIdentityField; +static jfieldID xrayTraceIdField; +static jfieldID tenantIdField; +static jfieldID invocationIdField; + + +jint JNI_OnLoad(JavaVM* vm, void* reserved) { + + JNIEnv* env; + if (vm->GetEnv(reinterpret_cast(&env), JNI_VERSION) != JNI_OK) { + return JNI_ERR; + } + + jclass tempInvocationRequestClassRef; + tempInvocationRequestClassRef = env->FindClass("com/amazonaws/services/lambda/runtime/api/client/runtimeapi/dto/InvocationRequest"); + invocationRequestClass = (jclass) env->NewGlobalRef(tempInvocationRequestClassRef); + env->DeleteLocalRef(tempInvocationRequestClassRef); + + idField = env->GetFieldID(invocationRequestClass , "id", "Ljava/lang/String;"); + invokedFunctionArnField = env->GetFieldID(invocationRequestClass , "invokedFunctionArn", "Ljava/lang/String;"); + deadlineTimeInMsField = env->GetFieldID(invocationRequestClass , "deadlineTimeInMs", "J"); + contentField = env->GetFieldID(invocationRequestClass , "content", "[B"); + xrayTraceIdField = env->GetFieldID(invocationRequestClass , "xrayTraceId", "Ljava/lang/String;"); + clientContextField = env->GetFieldID(invocationRequestClass , "clientContext", "Ljava/lang/String;"); + cognitoIdentityField = env->GetFieldID(invocationRequestClass , "cognitoIdentity", "Ljava/lang/String;"); + tenantIdField = env->GetFieldID(invocationRequestClass, "tenantId", "Ljava/lang/String;"); + invocationIdField = env->GetFieldID(invocationRequestClass, "invocationId", "Ljava/lang/String;"); + + return JNI_VERSION; +} + +void JNI_OnUnload(JavaVM *vm, void *reserved) { + JNIEnv* env; + vm->GetEnv(reinterpret_cast(&env), JNI_VERSION); + + env->DeleteGlobalRef(invocationRequestClass); +} + +static void throwLambdaRuntimeClientException(JNIEnv *env, std::string message, aws::http::response_code responseCode){ + jclass lambdaRuntimeExceptionClass = env->FindClass("com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaRuntimeClientException"); + jstring jMessage = env->NewStringUTF(message.c_str()); + jmethodID exInit = env->GetMethodID(lambdaRuntimeExceptionClass, "", "(Ljava/lang/String;I)V"); + jthrowable lambdaRuntimeException = (jthrowable) env->NewObject(lambdaRuntimeExceptionClass, exInit, jMessage, static_cast(responseCode)); + env->Throw(lambdaRuntimeException); +} + +static std::string toNativeString(JNIEnv *env, jbyteArray jArray) { + int length = env->GetArrayLength(jArray); + jbyte* bytes = env->GetByteArrayElements(jArray, NULL); + std::string nativeString = std::string((char *)bytes, length); + env->ReleaseByteArrayElements(jArray, bytes, JNI_ABORT); + env->DeleteLocalRef(jArray); + return nativeString; +} + +JNIEXPORT void JNICALL Java_com_amazonaws_services_lambda_runtime_api_client_runtimeapi_NativeClient_initializeClient(JNIEnv *env, jobject thisObject, jbyteArray userAgent, jbyteArray awsLambdaRuntimeApi) { + std::string user_agent = toNativeString(env, userAgent); + std::string endpoint = toNativeString(env, awsLambdaRuntimeApi); + CLIENT = new aws::lambda_runtime::runtime(endpoint, user_agent); +} + +JNIEXPORT jobject JNICALL Java_com_amazonaws_services_lambda_runtime_api_client_runtimeapi_NativeClient_next + (JNIEnv *env, jobject thisObject){ + auto outcome = CLIENT->get_next(); + if (!outcome.is_success()) { + std::string errorMessage("Failed to get next."); + throwLambdaRuntimeClientException(env, errorMessage, outcome.get_failure()); + return NULL; + } + + jobject invocationRequest; + jbyteArray jArray; + const jbyte* bytes; + auto response = outcome.get_result(); + + CHECK_EXCEPTION(env, invocationRequest = env->AllocObject(invocationRequestClass)); + CHECK_EXCEPTION(env, env->SetObjectField(invocationRequest, idField, env->NewStringUTF(response.request_id.c_str()))); + CHECK_EXCEPTION(env, env->SetObjectField(invocationRequest, invokedFunctionArnField, env->NewStringUTF(response.function_arn.c_str()))); + CHECK_EXCEPTION(env, env->SetLongField(invocationRequest, deadlineTimeInMsField, std::chrono::duration_cast(response.deadline.time_since_epoch()).count())); + + if(response.xray_trace_id != ""){ + CHECK_EXCEPTION(env, env->SetObjectField(invocationRequest, xrayTraceIdField, env->NewStringUTF(response.xray_trace_id.c_str()))); + } + + if(response.client_context != ""){ + CHECK_EXCEPTION(env, env->SetObjectField(invocationRequest, clientContextField, env->NewStringUTF(response.client_context.c_str()))); + } + + if(response.cognito_identity != ""){ + CHECK_EXCEPTION(env, env->SetObjectField(invocationRequest, cognitoIdentityField, env->NewStringUTF(response.cognito_identity.c_str()))); + } + + if(response.tenant_id != ""){ + CHECK_EXCEPTION(env, env->SetObjectField(invocationRequest, tenantIdField, env->NewStringUTF(response.tenant_id.c_str()))); + } + + if(response.invocation_id != ""){ + CHECK_EXCEPTION(env, env->SetObjectField(invocationRequest, invocationIdField, env->NewStringUTF(response.invocation_id.c_str()))); + } + + bytes = reinterpret_cast(response.payload.c_str()); + CHECK_EXCEPTION(env, jArray = env->NewByteArray(response.payload.length())); + CHECK_EXCEPTION(env, env->SetByteArrayRegion(jArray, 0, response.payload.length(), bytes)); + CHECK_EXCEPTION(env, env->SetObjectField(invocationRequest, contentField, jArray)); + + return invocationRequest; + + ERROR: + return NULL; +} + +JNIEXPORT void JNICALL Java_com_amazonaws_services_lambda_runtime_api_client_runtimeapi_NativeClient_postInvocationResponse + (JNIEnv *env, jobject thisObject, jbyteArray jrequestId, jbyteArray jresponseArray, jbyteArray jinvocationId) { + std::string payload = toNativeString(env, jresponseArray); + if ((env)->ExceptionOccurred()){ + return; + } + std::string requestId = toNativeString(env, jrequestId); + if ((env)->ExceptionOccurred()){ + return; + } + + std::string invocationId; + if (jinvocationId != nullptr) { + invocationId = toNativeString(env, jinvocationId); + if ((env)->ExceptionOccurred()){ + return; + } + } + + auto response = aws::lambda_runtime::invocation_response::success(payload, "application/json"); + auto outcome = CLIENT->post_success(requestId, response, invocationId); + if (!outcome.is_success()) { + std::string errorMessage("Failed to post invocation response."); + throwLambdaRuntimeClientException(env, errorMessage, outcome.get_failure()); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/main/jni/com_amazonaws_services_lambda_runtime_api_client_runtimeapi_NativeClient.h b/aws-lambda-java-runtime-interface-client/src/main/jni/com_amazonaws_services_lambda_runtime_api_client_runtimeapi_NativeClient.h new file mode 100644 index 000000000..0f1aaa2ca --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/jni/com_amazonaws_services_lambda_runtime_api_client_runtimeapi_NativeClient.h @@ -0,0 +1,25 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +#include + +#ifndef _Included_com_amazonaws_services_lambda_runtime_api_client_runtimeapi_NativeClient +#define _Included_com_amazonaws_services_lambda_runtime_api_client_runtimeapi_NativeClient +#ifdef __cplusplus +extern "C" { +#endif + +JNIEXPORT void JNICALL Java_com_amazonaws_services_lambda_runtime_api_client_runtimeapi_NativeClient_initializeClient + (JNIEnv *, jobject, jbyteArray, jbyteArray); + +JNIEXPORT jobject JNICALL Java_com_amazonaws_services_lambda_runtime_api_client_runtimeapi_NativeClient_next + (JNIEnv *, jobject); + +JNIEXPORT void JNICALL Java_com_amazonaws_services_lambda_runtime_api_client_runtimeapi_NativeClient_postInvocationResponse + (JNIEnv *, jobject, jbyteArray, jbyteArray, jbyteArray); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/aws-lambda-java-runtime-interface-client/src/main/jni/deps/curl-7.83.1.tar.gz b/aws-lambda-java-runtime-interface-client/src/main/jni/deps/curl-7.83.1.tar.gz new file mode 100644 index 000000000..b71926a37 Binary files /dev/null and b/aws-lambda-java-runtime-interface-client/src/main/jni/deps/curl-7.83.1.tar.gz differ diff --git a/aws-lambda-java-runtime-interface-client/src/main/jni/deps/curl_001_disable_wakeup.patch b/aws-lambda-java-runtime-interface-client/src/main/jni/deps/curl_001_disable_wakeup.patch new file mode 100644 index 000000000..1bb067054 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/jni/deps/curl_001_disable_wakeup.patch @@ -0,0 +1,14 @@ +diff --git a/multihandle.h b/multihandle.h +index a26fb619a..18080f1c3 100644 +--- a/multihandle.h ++++ b/multihandle.h +@@ -70,10 +70,6 @@ typedef enum { + + #define CURLPIPE_ANY (CURLPIPE_MULTIPLEX) + +-#if !defined(CURL_DISABLE_SOCKETPAIR) +-#define ENABLE_WAKEUP +-#endif +- + /* value for MAXIMUM CONCURRENT STREAMS upper limit */ + #define INITIAL_MAX_CONCURRENT_STREAMS ((1U << 31) - 1) \ No newline at end of file diff --git a/aws-lambda-java-runtime-interface-client/src/main/jni/macro.h b/aws-lambda-java-runtime-interface-client/src/main/jni/macro.h new file mode 100644 index 000000000..df5759afe --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/main/jni/macro.h @@ -0,0 +1,14 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +#ifndef _Included_macros +#define _Included_macros + +#define CHECK_EXCEPTION(env, expr) \ + expr; \ + if ((env)->ExceptionOccurred()) \ + goto ERROR; + +#endif diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/crac/ContextImplTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/crac/ContextImplTest.java new file mode 100644 index 000000000..7a7653dc2 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/crac/ContextImplTest.java @@ -0,0 +1,314 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.crac; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; +import org.mockito.ArgumentMatchers; +import org.mockito.InOrder; +import org.mockito.Mockito; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.Mockito.doThrow; + +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.JniHelper; + +@DisabledOnOs(OS.MAC) +public class ContextImplTest { + + private Resource throwsWithSuppressedException, noop, noop2, throwsException, throwCustomException; + + @BeforeAll + public static void jniLoad() { + JniHelper.load(); + } + + @BeforeEach + public void setup() throws Exception { + + throwsWithSuppressedException = Mockito.mock(Resource.class); + CheckpointException checkpointException = new CheckpointException(); + checkpointException.addSuppressed(new NumberFormatException()); + + RestoreException restoreException = new RestoreException(); + restoreException.addSuppressed(new NumberFormatException()); + + doThrow(checkpointException).when(throwsWithSuppressedException).beforeCheckpoint(ArgumentMatchers.any()); + doThrow(restoreException).when(throwsWithSuppressedException).afterRestore(ArgumentMatchers.any()); + + noop = Mockito.mock(Resource.class); + Mockito.doNothing().when(noop).beforeCheckpoint(ArgumentMatchers.any()); + Mockito.doNothing().when(noop).afterRestore(ArgumentMatchers.any()); + + noop2 = Mockito.mock(Resource.class); + Mockito.doNothing().when(noop2).beforeCheckpoint(ArgumentMatchers.any()); + Mockito.doNothing().when(noop2).afterRestore(ArgumentMatchers.any()); + + throwsException = Mockito.mock(Resource.class); + doThrow(CheckpointException.class).when(throwsException).beforeCheckpoint(ArgumentMatchers.any()); + doThrow(RestoreException.class).when(throwsException).afterRestore(ArgumentMatchers.any()); + + throwCustomException = Mockito.mock(Resource.class); + doThrow(IndexOutOfBoundsException.class).when(throwCustomException).beforeCheckpoint(ArgumentMatchers.any()); + doThrow(UnsupportedOperationException.class).when(throwCustomException).afterRestore(ArgumentMatchers.any()); + + Core.resetGlobalContext(); + } + + static class StatefulResource implements Resource { + + int state = 0; + + @Override + public void afterRestore(Context context) { + state += 1; + } + + @Override + public void beforeCheckpoint(Context context) { + state += 2; + } + + int getValue() { + return state; + } + } + + static int GLOBAL_STATE; + + static class ChangeGlobalStateResource implements Resource { + + ChangeGlobalStateResource() { + GLOBAL_STATE = 0; + } + + @Override + public void afterRestore(Context context) { + GLOBAL_STATE += 1; + } + + @Override + public void beforeCheckpoint(Context context) { + GLOBAL_STATE += 2; + } + } + + /** + * Happy path test with real / not mocked resource + */ + @Test + public void verifyHooksWereExecuted() throws CheckpointException, RestoreException { + StatefulResource resource = new StatefulResource(); + Core.getGlobalContext().register(resource); + + Core.getGlobalContext().beforeCheckpoint(null); + Core.getGlobalContext().afterRestore(null); + + assertEquals(3, resource.getValue()); + } + + /** + * This test is to validate GC intervention + */ + @Test + public void verifyHooksWereExecutedWithGC() throws CheckpointException, RestoreException { + StatefulResource resource = new StatefulResource(); + Core.getGlobalContext().register(resource); + gcAndSleep(); + + Core.getGlobalContext().beforeCheckpoint(null); + Core.getGlobalContext().afterRestore(null); + + assertEquals(3, resource.getValue()); + } + + @Test + public void verifyHooksAreNotExecutedForGarbageCollectedResources() throws CheckpointException, RestoreException { + Core.getGlobalContext().register(new ChangeGlobalStateResource()); + gcAndSleep(); + + Core.getGlobalContext().beforeCheckpoint(null); + Core.getGlobalContext().afterRestore(null); + + + assertEquals(0, GLOBAL_STATE); + } + + private static void gcAndSleep() { + for (int i = 0; i < 10; i++) { + System.gc(); + } + + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + System.out.println("thread was interrupted"); + throw new RuntimeException(e); + } + } + + @Test + public void Should_NotifyResourcesInReverseOrderOfRegistration_When_CheckpointNotification() throws Exception { + // Given + InOrder checkpointNotificationOrder = Mockito.inOrder(noop, noop2); + Core.getGlobalContext().register(noop); + Core.getGlobalContext().register(noop2); + + // When + Core.getGlobalContext().beforeCheckpoint(null); + + // Then + checkpointNotificationOrder.verify(noop2).beforeCheckpoint(ArgumentMatchers.any()); + checkpointNotificationOrder.verify(noop).beforeCheckpoint(ArgumentMatchers.any()); + } + + @Test + public void Should_NotifyResourcesInOrderOfRegistration_When_RestoreNotification() throws Exception { + // Given + InOrder restoreNotificationOrder = Mockito.inOrder(noop, noop2); + Core.getGlobalContext().register(noop); + Core.getGlobalContext().register(noop2); + + // When + Core.getGlobalContext().afterRestore(null); + + // Then + restoreNotificationOrder.verify(noop).afterRestore(ArgumentMatchers.any()); + restoreNotificationOrder.verify(noop2).afterRestore(ArgumentMatchers.any()); + } + + @Test + public void Should_ResourcesAreAlwaysNotified_When_AnyNotificationThrowsException() throws Exception { + + // Given + Core.getGlobalContext().register(throwsWithSuppressedException); + Core.getGlobalContext().register(noop); + Core.getGlobalContext().register(noop2); + Core.getGlobalContext().register(throwsException); + Core.getGlobalContext().register(throwCustomException); + + // When + try { + Core.getGlobalContext().beforeCheckpoint(null); + } catch (Exception ignored) { + } + + try { + Core.getGlobalContext().afterRestore(null); + } catch (Exception ignored) { + } + + // Then + Mockito.verify(throwsWithSuppressedException, Mockito.times(1)).beforeCheckpoint(ArgumentMatchers.any()); + Mockito.verify(noop, Mockito.times(1)).beforeCheckpoint(ArgumentMatchers.any()); + Mockito.verify(noop2, Mockito.times(1)).beforeCheckpoint(ArgumentMatchers.any()); + Mockito.verify(throwsException, Mockito.times(1)).beforeCheckpoint(ArgumentMatchers.any()); + Mockito.verify(throwCustomException, Mockito.times(1)).beforeCheckpoint(ArgumentMatchers.any()); + + Mockito.verify(throwsWithSuppressedException, Mockito.times(1)).afterRestore(ArgumentMatchers.any()); + Mockito.verify(noop, Mockito.times(1)).afterRestore(ArgumentMatchers.any()); + Mockito.verify(noop2, Mockito.times(1)).afterRestore(ArgumentMatchers.any()); + Mockito.verify(throwsException, Mockito.times(1)).afterRestore(ArgumentMatchers.any()); + Mockito.verify(throwCustomException, Mockito.times(1)).afterRestore(ArgumentMatchers.any()); + } + + @Test + public void Should_CatchAndSuppressAnyExceptionsAsCheckpointException_When_CheckpointNotification() { + // Given + Core.getGlobalContext().register(throwsWithSuppressedException); + Core.getGlobalContext().register(throwCustomException); + + // When + try { + Core.getGlobalContext().beforeCheckpoint(null); + } catch (CheckpointException e1) { + // Then + assertEquals(2, e1.getSuppressed().length); + } catch (Throwable e2) { + fail("All exceptions thrown during checkpoint notification should be reported as CheckpointException"); + } + } + + @Test + public void Should_CatchAndSuppressAnyExceptionsAsRestoreException_When_RestoreNotification() { + // Given + Core.getGlobalContext().register(throwsWithSuppressedException); + Core.getGlobalContext().register(throwCustomException); + + // When + try { + Core.getGlobalContext().afterRestore(null); + } catch (RestoreException e1) { + // Then + assertEquals(2, e1.getSuppressed().length); + } catch (Exception e2) { + fail("All exceptions thrown during restore notification should be reported as RestoreException"); + } + } + + @Test + public void Should_SuppressOriginalCheckpointExceptionUnderAnotherCheckpointException_When_ResourceIsAContext() throws Exception { + // Given + Context c0 = Mockito.mock(Context.class); + doThrow(CheckpointException.class).when(c0).beforeCheckpoint(ArgumentMatchers.any()); + + Core.getGlobalContext().register(c0); + + // When + try { + Core.getGlobalContext().beforeCheckpoint(null); + } catch (CheckpointException e1) { + // Then + assertEquals(1, e1.getSuppressed().length); + assertTrue(e1.getSuppressed()[0] instanceof CheckpointException, + "When the Resource is a Context and it throws CheckpointException it should be suppressed under another CheckpointException"); + + } catch (Exception e2) { + fail("All exceptions thrown during checkpoint notification should be reported as CheckpointException"); + } + } + + @Test + public void Should_SuppressOriginalRestoreExceptionUnderAnotherRestoreException_When_ResourceIsAContext() throws Exception { + // Given + Context c0 = Mockito.mock(Context.class); + doThrow(RestoreException.class).when(c0).afterRestore(ArgumentMatchers.any()); + + Core.getGlobalContext().register(c0); + + // When + try { + Core.getGlobalContext().afterRestore(null); + } catch (RestoreException e1) { + // Then + assertEquals(1, e1.getSuppressed().length); + assertTrue(e1.getSuppressed()[0] instanceof RestoreException, + "When the Resource is a Context and it throws RestoreException it should be suppressed under another RestoreException"); + } catch (Exception e2) { + fail("All exceptions thrown during restore notification should be reported as RestoreException"); + } + } + + @Test + public void Should_NotifyOnlyOnce_When_ResourceRegistersMultipleTimes() throws Exception { + // Given + Core.getGlobalContext().register(noop); + Core.getGlobalContext().register(noop); + + // When + Core.getGlobalContext().beforeCheckpoint(null); + Core.getGlobalContext().afterRestore(null); + + // Then + Mockito.verify(noop, Mockito.times(1)).beforeCheckpoint(ArgumentMatchers.any()); + Mockito.verify(noop, Mockito.times(1)).afterRestore(ArgumentMatchers.any()); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/crac/DNSCacheManagerTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/crac/DNSCacheManagerTest.java new file mode 100644 index 000000000..721b27059 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/crac/DNSCacheManagerTest.java @@ -0,0 +1,124 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.crac; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; + +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.JniHelper; + +import java.util.Map; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.lang.reflect.Field; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +@DisabledOnOs(OS.MAC) +public class DNSCacheManagerTest { + + static String CACHE_FIELD_NAME = "cache"; + + // this should have no effect, as the DNS cache is cleared explicitly in these tests + static { + java.security.Security.setProperty("networkaddress.cache.ttl" , "10000"); + java.security.Security.setProperty("networkaddress.cache.negative.ttl" , "10000"); + } + + @BeforeAll + public static void jniLoad() { + JniHelper.load(); + } + + @BeforeEach + public void setup() { + Core.resetGlobalContext(); + DNSManager.clearCache(); + } + + static class StatefulResource implements Resource { + + int state = 0; + + @Override + public void afterRestore(Context context) { + state += 1; + } + + @Override + public void beforeCheckpoint(Context context) { + state += 2; + } + + int getValue() { + return state; + } + } + + @Test + public void positiveDnsCacheShouldBeEmpty() throws CheckpointException, RestoreException, UnknownHostException, ReflectiveOperationException { + int baselineDNSEntryCount = getDNSEntryCount(); + + StatefulResource resource = new StatefulResource(); + Core.getGlobalContext().register(resource); + + String[] hosts = {"github.com", "amazonaws.com"}; + for(String singleHost : hosts) { + InetAddress address = InetAddress.getByName(singleHost); + } + // n hosts -> n DNS entries + assertEquals(hosts.length, getDNSEntryCount() - baselineDNSEntryCount); + + // this should call the native static method clearDNSCache + Core.getGlobalContext().beforeCheckpoint(null); + + // cache should be cleared + assertEquals(0, getDNSEntryCount()); + } + + @Test + public void negativeDnsCacheShouldBeEmpty() throws CheckpointException, RestoreException, UnknownHostException, ReflectiveOperationException { + int baselineDNSEntryCount = getDNSEntryCount(); + + StatefulResource resource = new StatefulResource(); + Core.getGlobalContext().register(resource); + + String invalidHost = "not.a.valid.host"; + try { + InetAddress address = InetAddress.getByName(invalidHost); + fail(); + } catch(UnknownHostException uhe) { + // this is actually fine + } + + // 1 host -> 1 DNS entry + assertEquals(1, getDNSEntryCount() - baselineDNSEntryCount); + + // this should the native static method clearDNSCache + Core.getGlobalContext().beforeCheckpoint(null); + + // cache should be cleared + assertEquals(0, getDNSEntryCount()); + } + + // helper functions to access the cache via reflection (see maven-surefire-plugin command args) + protected static Map getDNSCache() throws ReflectiveOperationException { + Class klass = InetAddress.class; + Field acf = klass.getDeclaredField(CACHE_FIELD_NAME); + acf.setAccessible(true); + Object addressCache = acf.get(null); + return (Map) acf.get(addressCache); + } + + protected static int getDNSEntryCount() throws ReflectiveOperationException { + Map cache = getDNSCache(); + return cache.size(); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/AWSLambdaTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/AWSLambdaTest.java new file mode 100644 index 000000000..100465531 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/AWSLambdaTest.java @@ -0,0 +1,623 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +import java.io.ByteArrayOutputStream; +import java.io.IOError; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.LambdaRuntimeApiClientImpl; +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.LambdaRuntimeClientMaxRetriesExceededException; +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.InvocationRequest; +import com.amazonaws.services.lambda.runtime.api.client.util.ConcurrencyConfig; +import com.amazonaws.services.lambda.runtime.api.client.util.EnvReader; +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.api.client.logging.LambdaContextLogger; +import com.amazonaws.services.lambda.runtime.logging.LogFormat; +import com.amazonaws.services.lambda.runtime.logging.LogLevel; +import software.amazon.awssdk.utilslite.SdkInternalThreadLocal; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +class AWSLambdaTest { + + private static final String CONCURRENT_TRACE_ID_KEY = "AWS_LAMBDA_X_TRACE_ID"; + + private static class SampleHandler implements RequestHandler, String> { + public static final String ADD_ENTRY_TO_MAP_ID_OP_MODE = "ADD_ENTRY_TO_MAP_ID"; + public static final String FAIL_IMMEDIATELY_OP_MODE = "FAIL_IMMEDIATELY"; + + public static final int nOfIterations = 10; + public static final int perIterationDelayMS = 10; + public static Map hashMap = new ConcurrentHashMap(); + public static AtomicInteger globalCounter = new AtomicInteger(); + + public static void resetStaticFields() { + hashMap.clear(); + globalCounter = new AtomicInteger(); + } + + private static void addEntryToMapImplementation(String name) { + int i = 0; + while (i++ < nOfIterations) { + hashMap.put(name, hashMap.getOrDefault(name, 0) + 1); + globalCounter.incrementAndGet(); + try { + Thread.sleep(perIterationDelayMS); + } catch (InterruptedException e) { + } + } + } + + @Override + public String handleRequest(Map event, Context context) { + // Thread.currentThread().getId() instead of Thread.currentThread().getName() when upgrading JAVA + String name = "Thread " + Thread.currentThread().getName(); + String opMode = event.get("id"); + + switch (opMode) { + case ADD_ENTRY_TO_MAP_ID_OP_MODE: + addEntryToMapImplementation(name); + break; + case FAIL_IMMEDIATELY_OP_MODE: + String[] sArr = {}; + return sArr[1]; + default: + break; + } + + return name; + } + } + + // Handler for testing SdkInternalThreadLocal trace ID functionality in concurrent scenarios + private static class SdkInternalThreadLocalTraceIdHandler implements RequestHandler, String> { + public static final String CAPTURE_TRACE_ID_OP_MODE = "CAPTURE_TRACE_ID"; + public static final int nOfIterations = 5; + public static final int perIterationDelayMS = 20; + public static CountDownLatch cdl = new CountDownLatch(1); + public static CountDownLatch readyLatch = null; + + public static Map capturedTraceIds = new ConcurrentHashMap<>(); + + public static void resetStaticFields() { + capturedTraceIds.clear(); + cdl = new CountDownLatch(1); + readyLatch = null; + } + + @Override + public String handleRequest(Map event, Context context) { + readyLatch.countDown(); + try { + cdl.await(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + String threadName = Thread.currentThread().getName(); + String opMode = event.get("id"); + + if (CAPTURE_TRACE_ID_OP_MODE.equals(opMode)) { + // Capture the SdkInternalThreadLocal trace ID for this thread + String traceId = SdkInternalThreadLocal.get(CONCURRENT_TRACE_ID_KEY); + if (traceId != null) { + capturedTraceIds.put(threadName, traceId); + } + + // Simulate some work with delays to ensure concurrent execution + for (int i = 0; i < nOfIterations; i++) { + try { + Thread.sleep(perIterationDelayMS); + // Re-check SdkInternalThreadLocal during processing to ensure it's consistent + String currentTraceId = SdkInternalThreadLocal.get(CONCURRENT_TRACE_ID_KEY); + if (currentTraceId != null && !currentTraceId.equals(traceId)) { + throw new RuntimeException("SdkInternalThreadLocal trace ID changed during processing!"); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + } + + return threadName; + } + } + + @Mock + private LambdaRuntimeApiClientImpl runtimeClient; + + @Mock + private LambdaContextLogger lambdaLogger; + + @Mock + private EnvReader envReader; + + @Mock + private ConcurrencyConfig concurrencyConfig; + + private LambdaRequestHandler lambdaRequestHandler = new LambdaRequestHandler() { + private SampleHandler sHandler = new SampleHandler(); + + @Override + public ByteArrayOutputStream call(InvocationRequest request) throws Error, Exception { + HashMap eventMap = new HashMap(); + eventMap.put("id", request.getId()); + String outStr = sHandler.handleRequest(eventMap, null); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + output.write(outStr.getBytes()); + return output; + } + }; + + private LambdaRequestHandler SdkInternalThreadLocalRequestHandler = new LambdaRequestHandler() { + private SdkInternalThreadLocalTraceIdHandler SdkInternalThreadLocalHandler = new SdkInternalThreadLocalTraceIdHandler(); + + @Override + public ByteArrayOutputStream call(InvocationRequest request) throws Error, Exception { + HashMap eventMap = new HashMap<>(); + eventMap.put("id", request.getId()); + String outStr = SdkInternalThreadLocalHandler.handleRequest(eventMap, null); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + output.write(outStr.getBytes()); + return output; + } + }; + + private static InvocationRequest getFakeInvocationRequest(String id) { + InvocationRequest request = new InvocationRequest(); + request.setId(id); + request.setDeadlineTimeInMs(Long.MAX_VALUE); + request.setContent("".getBytes()); + return request; + } + + private static InvocationRequest getFakeInvocationRequest(String id, String traceId) { + InvocationRequest request = getFakeInvocationRequest(id); + request.setXrayTraceId(traceId); + return request; + } + + private static final LambdaRuntimeClientMaxRetriesExceededException fakelambdaRuntimeClientMaxRetriesExceededException = new LambdaRuntimeClientMaxRetriesExceededException("Fake max retries happened"); + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + SampleHandler.resetStaticFields(); + } + + /* + * com.amazonaws.services.lambda.runtime.api.client.util.SampleHandler contains static fields. These fields are expected to be shared if initialization is behaving as expected. + * After execution of the Runtime loops, we should see that the SampleHandler.globalCounter has been acted on by all the threads. + * The concurrent hashmap in SampleHandler.hashMap should also have all the correct count of Threads that ran. + * IMPORTANT: This test fails through only timeout. + */ + @Test + @Timeout(value = 1, unit = TimeUnit.MINUTES) + void testConcurrentRunWithPlatformThreads() throws Throwable { + when(concurrencyConfig.isMultiConcurrent()).thenReturn(true); + when(concurrencyConfig.getNumberOfPlatformThreads()).thenReturn(4); + + InvocationRequest successfullInvocationRequest = getFakeInvocationRequest(SampleHandler.ADD_ENTRY_TO_MAP_ID_OP_MODE); + + when(runtimeClient.nextInvocationWithExponentialBackoff(lambdaLogger)) + .thenReturn(successfullInvocationRequest) + .thenReturn(successfullInvocationRequest) + .thenReturn(successfullInvocationRequest) + .thenReturn(successfullInvocationRequest) + .thenReturn(successfullInvocationRequest) + .thenReturn(successfullInvocationRequest) + .thenReturn(successfullInvocationRequest) + .thenThrow(fakelambdaRuntimeClientMaxRetriesExceededException) + .thenThrow(fakelambdaRuntimeClientMaxRetriesExceededException) + .thenThrow(fakelambdaRuntimeClientMaxRetriesExceededException) + .thenThrow(fakelambdaRuntimeClientMaxRetriesExceededException); + + AWSLambda.startRuntimeLoops(lambdaRequestHandler, lambdaLogger, concurrencyConfig, runtimeClient); + + // Success Reports Must Equal number of tasks that ran successfully. + verify(runtimeClient, times(7)).reportInvocationSuccess(eq(SampleHandler.ADD_ENTRY_TO_MAP_ID_OP_MODE), any(), any()); + // Hashmap keys should equal the number of threads (runtime loops). + assertEquals(4, SampleHandler.hashMap.size()); + // Hashmap total count should equal all tasks that ran * number of iterations per task + assertEquals(7 * SampleHandler.nOfIterations, SampleHandler.globalCounter.get()); + } + + @Test + @Timeout(value = 1, unit = TimeUnit.MINUTES) + void testConcurrentRunWithPlatformThreadsWithFailures() throws Throwable { + when(lambdaLogger.getLogFormat()).thenReturn(LogFormat.JSON); + when(concurrencyConfig.isMultiConcurrent()).thenReturn(true); + when(concurrencyConfig.getNumberOfPlatformThreads()).thenReturn(4); + + InvocationRequest successfullInvocationRequest = getFakeInvocationRequest(SampleHandler.ADD_ENTRY_TO_MAP_ID_OP_MODE); + InvocationRequest failImmediatelyRequest = getFakeInvocationRequest(SampleHandler.FAIL_IMMEDIATELY_OP_MODE); + InvocationRequest userFaultRequest = mock(InvocationRequest.class); + final String UserFaultID = "Injected Fault Request ID"; + when(userFaultRequest.getId()).thenThrow(UserFault.makeUserFault(new Exception("OH NO"), true)).thenReturn(UserFaultID); + + when(runtimeClient.nextInvocationWithExponentialBackoff(lambdaLogger)) + .thenReturn(failImmediatelyRequest) + .thenReturn(userFaultRequest) + .thenReturn(successfullInvocationRequest) + .thenReturn(successfullInvocationRequest) + .thenThrow(fakelambdaRuntimeClientMaxRetriesExceededException) + .thenThrow(fakelambdaRuntimeClientMaxRetriesExceededException) + .thenThrow(fakelambdaRuntimeClientMaxRetriesExceededException) + .thenThrow(fakelambdaRuntimeClientMaxRetriesExceededException); + + AWSLambda.startRuntimeLoops(lambdaRequestHandler, lambdaLogger, concurrencyConfig, runtimeClient); + + // One for each of failImmediatelyRequest and userFaultRequest in finally block + // Four for crashing the Four runtime loops in the outermost catch of the runtime loop after the Null responses. + // 2 + 4 = 6 + verify(lambdaLogger, times(6)).log(anyString(), eq(LogLevel.ERROR)); + + // Failed invokes should be reported. + verify(runtimeClient).reportInvocationError(eq(SampleHandler.FAIL_IMMEDIATELY_OP_MODE), any(), any()); + verify(runtimeClient).reportInvocationError(eq(UserFaultID), any(), any()); + + // Success Reports Must Equal number of tasks that ran successfully. + verify(runtimeClient, times(2)).reportInvocationSuccess(eq(SampleHandler.ADD_ENTRY_TO_MAP_ID_OP_MODE), any(), any()); + + // Hashmap keys should equal the minumum between(number of threads (runtime loops) AND number of tasks that ran successfully). + assertEquals(2, SampleHandler.hashMap.size()); + + // Hashmap total count should equal all tasks that ran * number of iterations per task + assertEquals(2 * SampleHandler.nOfIterations, SampleHandler.globalCounter.get()); + } + + @Test + @Timeout(value = 1, unit = TimeUnit.MINUTES) + void testConcurrentModeLoopDoesNotExitExceptForLambdaRuntimeClientMaxRetriesExceededException() throws Throwable { + when(lambdaLogger.getLogFormat()).thenReturn(LogFormat.JSON); + when(concurrencyConfig.isMultiConcurrent()).thenReturn(true); + when(concurrencyConfig.getNumberOfPlatformThreads()).thenReturn(1); + + InvocationRequest successfullInvocationRequest = getFakeInvocationRequest(SampleHandler.ADD_ENTRY_TO_MAP_ID_OP_MODE); + InvocationRequest failImmediatelyRequest = getFakeInvocationRequest(SampleHandler.FAIL_IMMEDIATELY_OP_MODE); + + InvocationRequest userFaultRequest = mock(InvocationRequest.class); // unrecoverable in sequential but recoverable in multiconcurrent mode. + final String UserFaultID = "Injected Fault Request ID"; + when(userFaultRequest.getId()).thenThrow(UserFault.makeUserFault(new Exception("OH NO"), true)).thenReturn(UserFaultID); + + InvocationRequest virtualMachineErrorRequest = mock(InvocationRequest.class); // unrecoverable in sequential but recoverable in multiconcurrent mode. + final String IOErrorID = "ioerr1"; + when(virtualMachineErrorRequest.getId()).thenThrow(UserFault.makeUserFault(new IOError(new Throwable()), true)).thenReturn(IOErrorID); + + when(runtimeClient.nextInvocationWithExponentialBackoff(lambdaLogger)) + .thenReturn(failImmediatelyRequest) + .thenReturn(userFaultRequest) + .thenReturn(virtualMachineErrorRequest) + .thenReturn(successfullInvocationRequest) + .thenReturn(successfullInvocationRequest) + .thenThrow(fakelambdaRuntimeClientMaxRetriesExceededException) + .thenReturn(successfullInvocationRequest); + + AWSLambda.startRuntimeLoops(lambdaRequestHandler, lambdaLogger, concurrencyConfig, runtimeClient); + + // One for each of failImmediatelyRequest, userFaultRequest, and virtualMachineErrorRequest + One for the runtime loop thread crashing. + verify(lambdaLogger, times(4)).log(anyString(), eq(LogLevel.ERROR)); + + // Failed invokes should be reported. + verify(runtimeClient).reportInvocationError(eq(SampleHandler.FAIL_IMMEDIATELY_OP_MODE), any(), any()); + verify(runtimeClient).reportInvocationError(eq(UserFaultID), any(), any()); + verify(runtimeClient).reportInvocationError(eq(IOErrorID), any(), any()); + + // Success Reports Must Equal number of tasks that ran successfully. + verify(runtimeClient, times(2)).reportInvocationSuccess(eq(SampleHandler.ADD_ENTRY_TO_MAP_ID_OP_MODE), any(), any()); + + // Hashmap keys should equal the minumum between(number of threads (runtime loops) AND number of tasks that ran successfully). + assertEquals(1, SampleHandler.hashMap.size()); + + // Hashmap total count should equal all tasks that ran * number of iterations per task + assertEquals(2 * SampleHandler.nOfIterations, SampleHandler.globalCounter.get()); + } + + /* + * + * SdkInternalThreadLocal XRAY TRACE ID TESTS + * + */ + + @Test + @Timeout(value = 1, unit = TimeUnit.MINUTES) + void testSdkInternalThreadLocalTraceIdIsInheritable() throws Throwable { + ExecutorService parentExecutorPool = Executors.newFixedThreadPool(1000); + CountDownLatch cdl = new CountDownLatch(1000); + CountDownLatch childCdl = new CountDownLatch(1000); + AtomicReference error = new AtomicReference<>(); + + for (int i = 0; i < 1000; i++) { + final int threadIndex = i; + parentExecutorPool.submit(() -> { + try { + String traceValue = "Val from parent thread" + threadIndex; + SdkInternalThreadLocal.put(CONCURRENT_TRACE_ID_KEY, traceValue); + + cdl.countDown(); + cdl.await(); + + assertEquals(SdkInternalThreadLocal.get(CONCURRENT_TRACE_ID_KEY), traceValue); + + ExecutorService internalExecutorPool = Executors.newFixedThreadPool(2); + internalExecutorPool.submit(() -> { + try { + assertEquals(SdkInternalThreadLocal.get(CONCURRENT_TRACE_ID_KEY), traceValue); + } catch (Throwable t) { + error.set(t); + } finally { + childCdl.countDown(); + } + }); + } catch (Throwable t) { + error.set(t); + childCdl.countDown(); + } + }); + } + + childCdl.await(); + if (error.get() != null) { + throw error.get(); + } + assertEquals(SdkInternalThreadLocal.get(CONCURRENT_TRACE_ID_KEY), null); + } + + @Test + @Timeout(value = 1, unit = TimeUnit.MINUTES) + void testSdkInternalThreadLocalTraceIdIsCleared() throws Throwable { + when(concurrencyConfig.isMultiConcurrent()).thenReturn(true); + when(concurrencyConfig.getNumberOfPlatformThreads()).thenReturn(1); + + InvocationRequest requestWithTrace = getFakeInvocationRequest("req_with_traceID", "test-trace-123"); + InvocationRequest requestWithNoTrace = getFakeInvocationRequest("req_without_traceID"); + + when(runtimeClient.nextInvocationWithExponentialBackoff(any())) + .thenReturn(requestWithTrace) + .thenReturn(requestWithNoTrace) + .thenThrow(fakelambdaRuntimeClientMaxRetriesExceededException); + + AtomicReference error = new AtomicReference<>(); + LambdaRequestHandler traceCheckingHandler = new LambdaRequestHandler() { + @Override + public ByteArrayOutputStream call(InvocationRequest request) throws Error, Exception { + try { + if (request.getId().equals("req_without_traceID")) { + assertEquals(null, SdkInternalThreadLocal.get(CONCURRENT_TRACE_ID_KEY)); + } + else { + assertEquals("test-trace-123", SdkInternalThreadLocal.get(CONCURRENT_TRACE_ID_KEY)); + } + } catch (Throwable t) { + error.set(t); + } + + return new ByteArrayOutputStream(); + } + }; + + AWSLambda.startRuntimeLoops(traceCheckingHandler, lambdaLogger, concurrencyConfig, runtimeClient); + + if (error.get() != null) { + throw error.get(); + } + } + + @Test + @Timeout(value = 1, unit = TimeUnit.MINUTES) + void testSdkInternalThreadLocalTraceIdInConcurrentMode() throws Throwable { + SdkInternalThreadLocalTraceIdHandler.resetStaticFields(); + + // Create invocation requests with different trace IDs + int numOfThreads = 1000; + HashSet traceIds = new HashSet<>(); + ArrayList requests = new ArrayList<>(); + for (int i = 0; i < numOfThreads - 1; i++) { + String randTId = java.util.UUID.randomUUID().toString(); + traceIds.add(randTId); + requests.add(getFakeInvocationRequest(SdkInternalThreadLocalTraceIdHandler.CAPTURE_TRACE_ID_OP_MODE, randTId)); + } + + // Test Nulls as well. + requests.add(getFakeInvocationRequest(SdkInternalThreadLocalTraceIdHandler.CAPTURE_TRACE_ID_OP_MODE, null)); + + when(concurrencyConfig.isMultiConcurrent()).thenReturn(true); + when(concurrencyConfig.getNumberOfPlatformThreads()).thenReturn(numOfThreads); + AtomicInteger iAtomic = new AtomicInteger(); + when(runtimeClient.nextInvocationWithExponentialBackoff(lambdaLogger)) + .thenAnswer((o) -> { + if (iAtomic.get() < numOfThreads) { + return requests.get(iAtomic.getAndIncrement()); + } else { + throw fakelambdaRuntimeClientMaxRetriesExceededException; + } + }); + + Thread thread = new Thread(() -> { try { + AWSLambda.startRuntimeLoops(SdkInternalThreadLocalRequestHandler, lambdaLogger, concurrencyConfig, runtimeClient); + } catch (Exception e) { + } }); + + SdkInternalThreadLocalTraceIdHandler.readyLatch = new CountDownLatch(numOfThreads); + thread.start(); + SdkInternalThreadLocalTraceIdHandler.readyLatch.await(); + SdkInternalThreadLocalTraceIdHandler.cdl.countDown(); + thread.join(); + + for (String traceId : SdkInternalThreadLocalTraceIdHandler.capturedTraceIds.values()) { + traceIds.remove(traceId); + } + + assertTrue(traceIds.isEmpty()); + } + + /* + * + * NON-CONCURRENT-MODE TESTS + * + */ + + @Test + @Timeout(value = 1, unit = TimeUnit.MINUTES) + void testSequentialWithFatalUserFaultErrorStopsLoop() throws Throwable { + when(lambdaLogger.getLogFormat()).thenReturn(LogFormat.JSON); + when(concurrencyConfig.isMultiConcurrent()).thenReturn(false); + + InvocationRequest successfullInvocationRequest = getFakeInvocationRequest(SampleHandler.ADD_ENTRY_TO_MAP_ID_OP_MODE); + InvocationRequest failImmediatelyRequest = getFakeInvocationRequest(SampleHandler.FAIL_IMMEDIATELY_OP_MODE); // recoverable error in all modes. + + InvocationRequest userFaultRequest = mock(InvocationRequest.class); // unrecoverable in sequential but recoverable in multiconcurrent mode. + final String UserFaultID = "Injected Fault Request ID"; + when(userFaultRequest.getId()).thenThrow(UserFault.makeUserFault(new Exception("OH NO"), true)).thenReturn(UserFaultID); + + InvocationRequest virtualMachineErrorRequest = mock(InvocationRequest.class); // unrecoverable in sequential but recoverable in multiconcurrent mode. + final String IOErrorID = "ioerr1"; + when(virtualMachineErrorRequest.getId()).thenThrow(UserFault.makeUserFault(new IOError(new Throwable()), true)).thenReturn(IOErrorID); + + when(runtimeClient.nextInvocation()) + .thenReturn(successfullInvocationRequest) + .thenReturn(successfullInvocationRequest) + .thenReturn(failImmediatelyRequest) + .thenReturn(userFaultRequest) + // these two should not be even feltched since userFaultRequest is not recoverable. + .thenReturn(successfullInvocationRequest) + .thenReturn(virtualMachineErrorRequest); + + AWSLambda.startRuntimeLoops(lambdaRequestHandler, lambdaLogger, concurrencyConfig, runtimeClient); + + // One for failImmediatelyRequest and userFaultRequest in finally block. + verify(lambdaLogger, times(2)).log(anyString(), eq(LogLevel.ERROR)); + + // Failed invokes should be reported. + verify(runtimeClient).reportInvocationError(eq(SampleHandler.FAIL_IMMEDIATELY_OP_MODE), any(), any()); + verify(runtimeClient).reportInvocationError(eq(UserFaultID), any(), any()); + + // Success Reports Must Equal number of tasks that ran successfully. And only 2 Error reports for failImmediatelyRequest and userFaultRequest. + verify(runtimeClient, times(2)).reportInvocationSuccess(eq(SampleHandler.ADD_ENTRY_TO_MAP_ID_OP_MODE), any(), any()); + verify(runtimeClient, times(2)).reportInvocationError(any(), any(), any()); + + // Hashmap keys should equal one as it is not multithreaded. + assertEquals(1, SampleHandler.hashMap.size()); + + // Hashmap total count should equal all tasks that ran * number of iterations per task + assertEquals(2 * SampleHandler.nOfIterations, SampleHandler.globalCounter.get()); + } + + @Test + @Timeout(value = 1, unit = TimeUnit.MINUTES) + void testSequentialWithVirtualMachineErrorStopsLoop() throws Throwable { + when(lambdaLogger.getLogFormat()).thenReturn(LogFormat.JSON); + when(concurrencyConfig.isMultiConcurrent()).thenReturn(false); + + InvocationRequest successfullInvocationRequest = getFakeInvocationRequest(SampleHandler.ADD_ENTRY_TO_MAP_ID_OP_MODE); + InvocationRequest failImmediatelyRequest = getFakeInvocationRequest(SampleHandler.FAIL_IMMEDIATELY_OP_MODE); // recoverable error in all modes. + + InvocationRequest userFaultRequest = mock(InvocationRequest.class); // unrecoverable in sequential but recoverable in multiconcurrent mode. + final String UserFaultID = "Injected Fault Request ID"; + when(userFaultRequest.getId()).thenThrow(UserFault.makeUserFault(new Exception("OH NO"), true)).thenReturn(UserFaultID); + + InvocationRequest virtualMachineErrorRequest = mock(InvocationRequest.class); // unrecoverable in sequential but recoverable in multiconcurrent mode. + final String IOErrorID = "ioerr1"; + when(virtualMachineErrorRequest.getId()).thenThrow(UserFault.makeUserFault(new IOError(new Throwable()), true)).thenReturn(IOErrorID); + + when(runtimeClient.nextInvocation()) + .thenReturn(successfullInvocationRequest) + .thenReturn(successfullInvocationRequest) + .thenReturn(failImmediatelyRequest) + .thenReturn(virtualMachineErrorRequest) + // these two should not be even feltched since userFaultRequest is not recoverable. + .thenReturn(successfullInvocationRequest) + .thenReturn(userFaultRequest); + + AWSLambda.startRuntimeLoops(lambdaRequestHandler, lambdaLogger, concurrencyConfig, runtimeClient); + + // One for failImmediatelyRequest and userFaultRequest in finally block. + verify(lambdaLogger, times(2)).log(anyString(), eq(LogLevel.ERROR)); + + // Failed invokes should be reported. + verify(runtimeClient).reportInvocationError(eq(SampleHandler.FAIL_IMMEDIATELY_OP_MODE), any(), any()); + verify(runtimeClient).reportInvocationError(eq(IOErrorID), any(), any()); + + // Success Reports Must Equal number of tasks that ran successfully. And only 2 Error reports for failImmediatelyRequest and virtualMachineErrorRequest. + verify(runtimeClient, times(2)).reportInvocationSuccess(eq(SampleHandler.ADD_ENTRY_TO_MAP_ID_OP_MODE), any(), any()); + verify(runtimeClient, times(2)).reportInvocationError(any(), any(), any()); + + // Hashmap keys should equal one as it is not multithreaded. + assertEquals(1, SampleHandler.hashMap.size()); + + // Hashmap total count should equal all tasks that ran * number of iterations per task + assertEquals(2 * SampleHandler.nOfIterations, SampleHandler.globalCounter.get()); + } + + @Test + @Timeout(value = 1, unit = TimeUnit.MINUTES) + void testInvocationIdIsPassedToReportSuccess() throws Throwable { + when(concurrencyConfig.isMultiConcurrent()).thenReturn(false); + + InvocationRequest requestWithInvId = getFakeInvocationRequest(SampleHandler.ADD_ENTRY_TO_MAP_ID_OP_MODE); + requestWithInvId.setInvocationId("test-inv-uuid-1234"); + + // Fatal error to stop the loop after one successful invocation + InvocationRequest fatalRequest = mock(InvocationRequest.class); + when(fatalRequest.getId()).thenThrow(UserFault.makeUserFault(new IOError(new Throwable()), true)).thenReturn("fatal"); + + when(runtimeClient.nextInvocation()) + .thenReturn(requestWithInvId) + .thenReturn(fatalRequest); + + AWSLambda.startRuntimeLoops(lambdaRequestHandler, lambdaLogger, concurrencyConfig, runtimeClient); + + verify(runtimeClient).reportInvocationSuccess( + eq(SampleHandler.ADD_ENTRY_TO_MAP_ID_OP_MODE), any(), eq("test-inv-uuid-1234")); + } + + @Test + @Timeout(value = 1, unit = TimeUnit.MINUTES) + void testInvocationIdIsPassedToReportError() throws Throwable { + when(lambdaLogger.getLogFormat()).thenReturn(LogFormat.JSON); + when(concurrencyConfig.isMultiConcurrent()).thenReturn(false); + + InvocationRequest requestWithInvId = getFakeInvocationRequest(SampleHandler.FAIL_IMMEDIATELY_OP_MODE); + requestWithInvId.setInvocationId("test-inv-uuid-5678"); + + // Fatal error to stop the loop after one error invocation + InvocationRequest fatalRequest = mock(InvocationRequest.class); + when(fatalRequest.getId()).thenThrow(UserFault.makeUserFault(new IOError(new Throwable()), true)).thenReturn("fatal"); + + when(runtimeClient.nextInvocation()) + .thenReturn(requestWithInvId) + .thenReturn(fatalRequest); + + AWSLambda.startRuntimeLoops(lambdaRequestHandler, lambdaLogger, concurrencyConfig, runtimeClient); + + verify(runtimeClient).reportInvocationError( + eq(SampleHandler.FAIL_IMMEDIATELY_OP_MODE), any(), eq("test-inv-uuid-5678")); + } +} \ No newline at end of file diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/ClasspathLoaderTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/ClasspathLoaderTest.java new file mode 100644 index 000000000..38147d219 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/ClasspathLoaderTest.java @@ -0,0 +1,153 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collections; +import java.util.Enumeration; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.JarOutputStream; + +import static org.junit.jupiter.api.Assertions.*; + +class ClasspathLoaderTest { + + @Test + void testLoadAllClassesWithNoClasspath() throws IOException { + String originalClasspath = System.getProperty("java.class.path"); + try { + System.clearProperty("java.class.path"); + ClasspathLoader.main(new String[]{}); + } finally { + if (originalClasspath != null) { + System.setProperty("java.class.path", originalClasspath); + } + } + } + + @Test + void testLoadAllClassesWithEmptyClasspath() { + String originalClasspath = System.getProperty("java.class.path"); + try { + System.setProperty("java.class.path", ""); + assertThrows(FileNotFoundException.class, () -> + ClasspathLoader.main(new String[]{})); + } finally { + if (originalClasspath != null) { + System.setProperty("java.class.path", originalClasspath); + } + } + } + + @Test + void testLoadAllClassesWithInvalidPath() { + String originalClasspath = System.getProperty("java.class.path"); + try { + System.setProperty("java.class.path", "nonexistent/path"); + assertThrows(FileNotFoundException.class, () -> + ClasspathLoader.main(new String[]{})); + } finally { + if (originalClasspath != null) { + System.setProperty("java.class.path", originalClasspath); + } + } + } + + @Test + void testLoadAllClassesWithValidJar(@TempDir Path tempDir) throws IOException { + File jarFile = createSimpleJar(tempDir, "test.jar", "TestClass"); + String originalClasspath = System.getProperty("java.class.path"); + try { + System.setProperty("java.class.path", jarFile.getAbsolutePath()); + ClasspathLoader.main(new String[]{}); + } finally { + if (originalClasspath != null) { + System.setProperty("java.class.path", originalClasspath); + } + } + } + + @Test + void testLoadAllClassesWithDirectory(@TempDir Path tempDir) throws IOException { + String originalClasspath = System.getProperty("java.class.path"); + try { + System.setProperty("java.class.path", tempDir.toString()); + ClasspathLoader.main(new String[]{}); + } finally { + if (originalClasspath != null) { + System.setProperty("java.class.path", originalClasspath); + } + } + } + + @Test + void testLoadAllClassesWithMultipleEntries(@TempDir Path tempDir) throws IOException { + File jarFile1 = createSimpleJar(tempDir, "test1.jar", "TestClass1"); + File jarFile2 = createSimpleJar(tempDir, "test2.jar", "TestClass2"); + + String originalClasspath = System.getProperty("java.class.path"); + try { + String newClasspath = jarFile1.getAbsolutePath() + + File.pathSeparator + + jarFile2.getAbsolutePath(); + System.setProperty("java.class.path", newClasspath); + ClasspathLoader.main(new String[]{}); + } finally { + if (originalClasspath != null) { + System.setProperty("java.class.path", originalClasspath); + } + } + } + + @Test + void testLoadAllClassesWithBlocklistedClass(@TempDir Path tempDir) throws IOException { + File jarFile = tempDir.resolve("blocklist-test.jar").toFile(); + + try (JarOutputStream jos = new JarOutputStream(new FileOutputStream(jarFile))) { + JarEntry blockedEntry = new JarEntry("META-INF/versions/9/module-info.class"); + jos.putNextEntry(blockedEntry); + jos.write("dummy content".getBytes()); + jos.closeEntry(); + + JarEntry normalEntry = new JarEntry("com/test/Normal.class"); + jos.putNextEntry(normalEntry); + jos.write("dummy content".getBytes()); + jos.closeEntry(); + } + + String originalClasspath = System.getProperty("java.class.path"); + try { + System.setProperty("java.class.path", jarFile.getAbsolutePath()); + ClasspathLoader.main(new String[]{}); + // The test passes if no exception is thrown and the blocklisted class is skipped + } finally { + if (originalClasspath != null) { + System.setProperty("java.class.path", originalClasspath); + } + } + } + + private File createSimpleJar(Path tempDir, String jarName, String className) throws IOException { + File jarFile = tempDir.resolve(jarName).toFile(); + + try (JarOutputStream jos = new JarOutputStream(new FileOutputStream(jarFile))) { + // Add a simple non-class file to make it a valid jar + JarEntry entry = new JarEntry("com/test/" + className + ".txt"); + jos.putNextEntry(entry); + jos.write("test content".getBytes()); + jos.closeEntry(); + } + + return jarFile; + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/CustomerClassLoaderTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/CustomerClassLoaderTest.java new file mode 100644 index 000000000..71fb013f3 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/CustomerClassLoaderTest.java @@ -0,0 +1,146 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; + +import java.io.IOException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.condition.OS.MAC; + +public class CustomerClassLoaderTest { + + final static String[] EXAMPLE_FUNCTION = new String[]{ + "var/runtime/lib/LambdaJavaRTEntry-1.0.jar", + "user/path/Hello.class", + "user/path/example/Hello.class", + "user/path/hidden.jar", + "user/path/lib/b.jar", + "user/path/lib/z.jar", + "user/path/lib/A.jar", + "user/path/lib/4.jar", + "user/path/lib/λ.jar", + "user/path/lib/a.jar", + "user/path/lib/hidden/c.jar" + }; + + final static String[] EXAMPLE_FUNCTION_AND_LAYERS = new String[]{ + "var/runtime/lib/LambdaJavaRTEntry-1.0.jar", + "user/path/Hello.class", + "user/path/example/Hello.class", + "user/path/hidden.jar", + "user/path/lib/a.jar", + "user/path/lib/b.jar", + "user/path/lib/hidden/c.jar", + "opt/hidden.jar", + "opt/java/hidden.jar", + "opt/java/lib/b.jar", + "opt/java/lib/c.jar", + "opt/java/lib/a-2.0.jar", + "opt/java/lib/a-1.0.jar", + "opt/java/lib/a-1.12.jar", + "opt/java/lib/a-1.21.jar", + "user/path/lib/hidden/d.jar" + }; + + /** + * Generate a fake file-system with the provided list of paths + */ + private Path fakeFileSystem(String[] paths) throws IOException { + Path dir = Files.createTempDirectory("rtentry"); + + for (String path : paths) { + Path stub = dir.resolve(path); + Files.createDirectories(stub.getParent()); + Files.write(stub, "fake-data".getBytes()); + } + + return dir; + } + + /** + * Strip the base URL from list + */ + private List strip(String base, URL[] urls) { + return Arrays.stream(urls) + .map(URL::toExternalForm) + .filter(s -> s.startsWith(base)) + .map(s -> s.substring(base.length())) + .collect(Collectors.toList()); + } + + @Test + @DisabledOnOs(MAC) // test fails on systems with case-insensitive volumes + public void customerClassLoaderFunction() throws IOException { + try { + Path rootDir = fakeFileSystem(EXAMPLE_FUNCTION); + + URLClassLoader customerClassLoader = new CustomerClassLoader( + rootDir.resolve("user/path").toString(), + rootDir.resolve("opt/java").toString(), + ClassLoader.getSystemClassLoader()); + + List res = strip("file:" + rootDir.toString(), customerClassLoader.getURLs()); + + Assertions.assertEquals(Arrays.asList( + "/user/path/", + "/user/path/lib/4.jar", + "/user/path/lib/A.jar", + "/user/path/lib/a.jar", + "/user/path/lib/b.jar", + "/user/path/lib/z.jar", + "/user/path/lib/λ.jar"), + res); + } catch(Throwable t) { + // this system property is the name of the charset used when encoding/decoding file paths + // exception is expected if it is not set to a UTF variant or not set at all + String systemEncoding = System.getProperty("sun.jnu.encoding"); + + if (systemEncoding != null && !systemEncoding.toLowerCase().contains("utf")){ + Assertions.assertTrue(t.getMessage().contains("Malformed input or input contains unmappable characters")); + } + else { + throw t; + } + } + } + + @Test + @DisabledOnOs(MAC) // test fails on systems with case-insensitive volumes + public void customerClassLoaderLayer() throws IOException { + + Path rootDir = fakeFileSystem(EXAMPLE_FUNCTION_AND_LAYERS); + + URLClassLoader customerClassLoader = new CustomerClassLoader( + rootDir.resolve("user/path").toString(), + rootDir.resolve("opt/java").toString(), + ClassLoader.getSystemClassLoader()); + + List res = strip("file:" + rootDir.toString(), customerClassLoader.getURLs()); + + // Layer order is fixed (unicode value) + Assertions.assertEquals(Arrays.asList( + "/user/path/", + "/user/path/lib/a.jar", + "/user/path/lib/b.jar", + "/opt/java/lib/a-1.0.jar", + "/opt/java/lib/a-1.12.jar", + "/opt/java/lib/a-1.21.jar", + "/opt/java/lib/a-2.0.jar", + "/opt/java/lib/b.jar", + "/opt/java/lib/c.jar" + ), res); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/EventHandlerLoaderTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/EventHandlerLoaderTest.java new file mode 100644 index 000000000..aae2f1afe --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/EventHandlerLoaderTest.java @@ -0,0 +1,152 @@ +package com.amazonaws.services.lambda.runtime.api.client; + +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.InvocationRequest; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class EventHandlerLoaderTest { + + @Test + void RequestHandlerTest() throws Exception { + String handler = "test.lambda.handlers.RequestHandlerImpl"; + LambdaRequestHandler lambdaRequestHandler = getLambdaRequestHandler(handler); + assertSuccessfulInvocation(lambdaRequestHandler); + } + + @Test + void RequestStreamHandlerTest() throws Exception { + String handler = "test.lambda.handlers.RequestStreamHandlerImpl"; + LambdaRequestHandler lambdaRequestHandler = getLambdaRequestHandler(handler); + assertSuccessfulInvocation(lambdaRequestHandler); + } + + @Test + void PojoHandlerTest_noParams() throws Exception { + String handler = "test.lambda.handlers.POJOHanlderImpl::noParamsHandler"; + LambdaRequestHandler lambdaRequestHandler = getLambdaRequestHandler(handler); + assertSuccessfulInvocation(lambdaRequestHandler); + } + + @Test + void PojoHandlerTest_oneParamEvent() throws Exception { + String handler = "test.lambda.handlers.POJOHanlderImpl::oneParamHandler_event"; + LambdaRequestHandler lambdaRequestHandler = getLambdaRequestHandler(handler); + assertSuccessfulInvocation(lambdaRequestHandler); + } + + @Test + void PojoHandlerTest_oneParamContext() throws Exception { + String handler = "test.lambda.handlers.POJOHanlderImpl::oneParamHandler_context"; + LambdaRequestHandler lambdaRequestHandler = getLambdaRequestHandler(handler); + assertSuccessfulInvocation(lambdaRequestHandler); + } + + @Test + void PojoHandlerTest_twoParams() throws Exception { + String handler = "test.lambda.handlers.POJOHanlderImpl::twoParamsHandler"; + LambdaRequestHandler lambdaRequestHandler = getLambdaRequestHandler(handler); + assertSuccessfulInvocation(lambdaRequestHandler); + } + + private LambdaRequestHandler getLambdaRequestHandler(String handler) throws ClassNotFoundException { + ClassLoader cl = this.getClass().getClassLoader(); + HandlerInfo handlerInfo = HandlerInfo.fromString(handler, cl); + return EventHandlerLoader.loadEventHandler(handlerInfo); + } + + private static void assertSuccessfulInvocation(LambdaRequestHandler lambdaRequestHandler) throws Exception { + InvocationRequest invocationRequest = getTestInvocationRequest(); + + ByteArrayOutputStream resultBytes = lambdaRequestHandler.call(invocationRequest); + String result = resultBytes.toString(); + + assertEquals("\"success\"", result); + } + + private static InvocationRequest getTestInvocationRequest() { + InvocationRequest invocationRequest = new InvocationRequest(); + invocationRequest.setContent("\"Hello\"".getBytes()); + invocationRequest.setId("id"); + invocationRequest.setXrayTraceId("traceId"); + return invocationRequest; + } + + // Multithreaded test methods + + @Test + void RequestHandlerTest_Multithreaded() throws Exception { + testHandlerConcurrency("test.lambda.handlers.RequestHandlerImpl"); + } + + @Test + void RequestStreamHandlerTest_Multithreaded() throws Exception { + testHandlerConcurrency("test.lambda.handlers.RequestStreamHandlerImpl"); + } + + @Test + void PojoHandlerTest_noParams_Multithreaded() throws Exception { + testHandlerConcurrency("test.lambda.handlers.POJOHanlderImpl::noParamsHandler"); + } + + @Test + void PojoHandlerTest_oneParamEvent_Multithreaded() throws Exception { + testHandlerConcurrency("test.lambda.handlers.POJOHanlderImpl::oneParamHandler_event"); + } + + @Test + void PojoHandlerTest_oneParamContext_Multithreaded() throws Exception { + testHandlerConcurrency("test.lambda.handlers.POJOHanlderImpl::oneParamHandler_context"); + } + + @Test + void PojoHandlerTest_twoParams_Multithreaded() throws Exception { + testHandlerConcurrency("test.lambda.handlers.POJOHanlderImpl::twoParamsHandler"); + } + + private void testHandlerConcurrency(String handlerName) throws Exception { + // Create one handler instance + LambdaRequestHandler handler = getLambdaRequestHandler(handlerName); + + int threadCount = 10; + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + List> futures = new ArrayList<>(); + CountDownLatch startLatch = new CountDownLatch(1); + + try { + for (int i = 0; i < threadCount; i++) { + futures.add(executor.submit(() -> { + try { + InvocationRequest request = getTestInvocationRequest(); + startLatch.await(); + ByteArrayOutputStream result = handler.call(request); + return result.toString(); + } catch (Exception e) { + throw new RuntimeException(e); + } + })); + } + + // Release all threads simultaneously and Verify all invocations return the expected result + startLatch.countDown(); + + for (Future future : futures) { + String result = future.get(5, TimeUnit.SECONDS); + assertEquals("\"success\"", result); + } + } finally { + executor.shutdown(); + assertTrue(executor.awaitTermination(10, TimeUnit.SECONDS)); + } + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/HandlerInfoTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/HandlerInfoTest.java new file mode 100644 index 000000000..e134ddc8c --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/HandlerInfoTest.java @@ -0,0 +1,132 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +class HandlerInfoTest { + + @Test + void testConstructor() { + Class testClass = String.class; + String methodName = "testMethod"; + + HandlerInfo info = new HandlerInfo(testClass, methodName); + + assertNotNull(info); + assertEquals(testClass, info.clazz); + assertEquals(methodName, info.methodName); + } + + @Test + void testFromStringWithoutMethod() throws Exception { + String handler = "java.lang.String"; + HandlerInfo info = HandlerInfo.fromString(handler, ClassLoader.getSystemClassLoader()); + + assertEquals(String.class, info.clazz); + assertNull(info.methodName); + } + + @Test + void testFromStringWithMethod() throws Exception { + String handler = "java.lang.String::length"; + HandlerInfo info = HandlerInfo.fromString(handler, ClassLoader.getSystemClassLoader()); + + assertEquals(String.class, info.clazz); + assertEquals("length", info.methodName); + } + + @Test + void testFromStringWithEmptyClass() { + String handler = "::method"; + + assertThrows(HandlerInfo.InvalidHandlerException.class, () -> + HandlerInfo.fromString(handler, ClassLoader.getSystemClassLoader()) + ); + } + + @Test + void testFromStringWithEmptyMethod() { + String handler = "java.lang.String::"; + + assertThrows(HandlerInfo.InvalidHandlerException.class, () -> + HandlerInfo.fromString(handler, ClassLoader.getSystemClassLoader()) + ); + } + + @Test + void testFromStringWithNonexistentClass() { + String handler = "com.nonexistent.TestClass::method"; + + assertThrows(ClassNotFoundException.class, () -> + HandlerInfo.fromString(handler, ClassLoader.getSystemClassLoader()) + ); + } + + @Test + void testFromStringWithNullHandler() { + assertThrows(NullPointerException.class, () -> + HandlerInfo.fromString(null, ClassLoader.getSystemClassLoader()) + ); + } + + @Test + void testClassNameWithoutMethod() { + String handler = "java.lang.String"; + String className = HandlerInfo.className(handler); + + assertEquals("java.lang.String", className); + } + + @Test + void testClassNameWithMethod() { + String handler = "java.lang.String::length"; + String className = HandlerInfo.className(handler); + + assertEquals("java.lang.String", className); + } + + @Test + void testClassNameWithEmptyString() { + String handler = ""; + String className = HandlerInfo.className(handler); + + assertEquals("", className); + } + + @Test + void testClassNameWithOnlyDelimiter() { + String handler = "::"; + String className = HandlerInfo.className(handler); + + assertEquals("", className); + } + + @Test + void testInvalidHandlerExceptionSerialVersionUID() { + assertEquals(-1L, HandlerInfo.InvalidHandlerException.serialVersionUID); + } + + @Test + void testFromStringWithInnerClass() throws Exception { + // Create a custom class loader that can load our test class + ClassLoader cl = new ClassLoader() { + @Override + public Class loadClass(String name) throws ClassNotFoundException { + if (name.equals("com.test.OuterClass$InnerClass")) { + throw new ClassNotFoundException("Test class not found"); + } + return super.loadClass(name); + } + }; + + String handler = "com.test.OuterClass$InnerClass::method"; + assertThrows(ClassNotFoundException.class, () -> + HandlerInfo.fromString(handler, cl) + ); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/LambdaRequestHandler.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/LambdaRequestHandler.java new file mode 100644 index 000000000..d86b73857 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/LambdaRequestHandler.java @@ -0,0 +1,142 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client; + +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.InvocationRequest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeEach; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class LambdaRequestHandlerTest { + + private InvocationRequest mockRequest; + + @BeforeEach + void setUp() { + mockRequest = mock(InvocationRequest.class); + } + + @Test + void testInitErrorHandler() { + String className = "com.example.TestClass"; + Exception testException = new RuntimeException("initialization error"); + + LambdaRequestHandler handler = LambdaRequestHandler.initErrorHandler(testException, className); + + assertNotNull(handler); + assertTrue(handler instanceof LambdaRequestHandler.UserFaultHandler); + + LambdaRequestHandler.UserFaultHandler userFaultHandler = (LambdaRequestHandler.UserFaultHandler) handler; + UserFault fault = userFaultHandler.fault; + + assertNotNull(fault); + assertEquals("Error loading class " + className + ": initialization error", fault.msg); + assertEquals("java.lang.RuntimeException", fault.exception); + assertTrue(fault.fatal); + } + + @Test + void testClassNotFound() { + String className = "com.example.MissingClass"; + Exception testException = new ClassNotFoundException("class not found"); + + LambdaRequestHandler handler = LambdaRequestHandler.classNotFound(testException, className); + + assertNotNull(handler); + assertTrue(handler instanceof LambdaRequestHandler.UserFaultHandler); + + LambdaRequestHandler.UserFaultHandler userFaultHandler = (LambdaRequestHandler.UserFaultHandler) handler; + UserFault fault = userFaultHandler.fault; + + assertNotNull(fault); + assertEquals("Class not found: " + className, fault.msg); + assertEquals("java.lang.ClassNotFoundException", fault.exception); + assertFalse(fault.fatal); + } + + @Test + void testUserFaultHandlerConstructor() { + UserFault testFault = new UserFault("test message", "TestException", "test trace"); + LambdaRequestHandler.UserFaultHandler handler = new LambdaRequestHandler.UserFaultHandler(testFault); + + assertNotNull(handler); + assertSame(testFault, handler.fault); + } + + @Test + void testUserFaultHandlerCallThrowsFault() { + UserFault testFault = new UserFault("test message", "TestException", "test trace"); + LambdaRequestHandler.UserFaultHandler handler = new LambdaRequestHandler.UserFaultHandler(testFault); + + UserFault thrownFault = assertThrows(UserFault.class, () -> handler.call(mockRequest)); + assertSame(testFault, thrownFault); + } + + @Test + void testInitErrorHandlerWithNullMessage() { + String className = "com.example.TestClass"; + Exception testException = new RuntimeException(); + + LambdaRequestHandler handler = LambdaRequestHandler.initErrorHandler(testException, className); + + assertNotNull(handler); + assertTrue(handler instanceof LambdaRequestHandler.UserFaultHandler); + + LambdaRequestHandler.UserFaultHandler userFaultHandler = (LambdaRequestHandler.UserFaultHandler) handler; + UserFault fault = userFaultHandler.fault; + + assertNotNull(fault); + assertEquals("Error loading class " + className, fault.msg); + assertEquals("java.lang.RuntimeException", fault.exception); + assertTrue(fault.fatal); + } + + @Test + void testInitErrorHandlerWithNullClassName() { + Exception testException = new RuntimeException("test error"); + + LambdaRequestHandler handler = LambdaRequestHandler.initErrorHandler(testException, null); + + assertNotNull(handler); + assertTrue(handler instanceof LambdaRequestHandler.UserFaultHandler); + + LambdaRequestHandler.UserFaultHandler userFaultHandler = (LambdaRequestHandler.UserFaultHandler) handler; + UserFault fault = userFaultHandler.fault; + + assertNotNull(fault); + assertEquals("Error loading class null: test error", fault.msg); + assertEquals("java.lang.RuntimeException", fault.exception); + assertTrue(fault.fatal); + } + + @Test + void testClassNotFoundWithNullClassName() { + Exception testException = new ClassNotFoundException("test error"); + + LambdaRequestHandler handler = LambdaRequestHandler.classNotFound(testException, null); + + assertNotNull(handler); + assertTrue(handler instanceof LambdaRequestHandler.UserFaultHandler); + + LambdaRequestHandler.UserFaultHandler userFaultHandler = (LambdaRequestHandler.UserFaultHandler) handler; + UserFault fault = userFaultHandler.fault; + + assertNotNull(fault); + assertEquals("Class not found: null", fault.msg); + assertEquals("java.lang.ClassNotFoundException", fault.exception); + assertFalse(fault.fatal); + } + + @Test + void testUserFaultHandlerCallWithNullRequest() { + UserFault testFault = new UserFault("test message", "TestException", "test trace"); + LambdaRequestHandler.UserFaultHandler handler = new LambdaRequestHandler.UserFaultHandler(testFault); + + UserFault thrownFault = assertThrows(UserFault.class, () -> handler.call(null)); + assertSame(testFault, thrownFault); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/PojoSerializerLoaderTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/PojoSerializerLoaderTest.java new file mode 100644 index 000000000..4ebcf5d7e --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/PojoSerializerLoaderTest.java @@ -0,0 +1,153 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client; + +import com.amazonaws.services.lambda.runtime.CustomPojoSerializer; +import com.amazonaws.services.lambda.runtime.serialization.PojoSerializer; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Field; +import java.lang.reflect.Type; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class PojoSerializerLoaderTest { + + @Mock + private CustomPojoSerializer mockSerializer; + + @AfterEach + @BeforeEach + void setUp() throws Exception { + resetStaticFields(); + } + + private void resetStaticFields() throws Exception { + Field serializerField = PojoSerializerLoader.class.getDeclaredField("customPojoSerializer"); + serializerField.setAccessible(true); + serializerField.set(null, null); + + Field initializedField = PojoSerializerLoader.class.getDeclaredField("initialized"); + initializedField.setAccessible(true); + initializedField.set(null, false); + } + + + private void setMockSerializer(CustomPojoSerializer serializer) throws Exception { + Field serializerField = PojoSerializerLoader.class.getDeclaredField("customPojoSerializer"); + serializerField.setAccessible(true); + serializerField.set(null, serializer); + } + + @Test + void testGetCustomerSerializerNoSerializerAvailable() throws Exception { + PojoSerializer serializer = PojoSerializerLoader.getCustomerSerializer(String.class); + assertNull(serializer); + Field initializedField = PojoSerializerLoader.class.getDeclaredField("initialized"); + initializedField.setAccessible(true); + assert((Boolean) initializedField.get(null)); + } + + @Test + void testGetCustomerSerializerWithValidSerializer() throws Exception { + setMockSerializer(mockSerializer); + String testInput = "test input"; + String testOutput = "test output"; + Type testType = String.class; + when(mockSerializer.fromJson(any(InputStream.class), eq(testType))).thenReturn(testOutput); + when(mockSerializer.fromJson(eq(testInput), eq(testType))).thenReturn(testOutput); + + PojoSerializer serializer = PojoSerializerLoader.getCustomerSerializer(testType); + assertNotNull(serializer); + + ByteArrayInputStream inputStream = new ByteArrayInputStream(testInput.getBytes()); + Object result1 = serializer.fromJson(inputStream); + assertEquals(testOutput, result1); + + Object result2 = serializer.fromJson(testInput); + assertEquals(testOutput, result2); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + serializer.toJson(testInput, outputStream); + verify(mockSerializer).toJson(eq(testInput), any(OutputStream.class), eq(testType)); + } + + @Test + void testGetCustomerSerializerCachingBehavior() throws Exception { + setMockSerializer(mockSerializer); + + Type testType = String.class; + PojoSerializer serializer1 = PojoSerializerLoader.getCustomerSerializer(testType); + PojoSerializer serializer2 = PojoSerializerLoader.getCustomerSerializer(testType); + + assertNotNull(serializer1); + assertNotNull(serializer2); + + String testInput = "test"; + serializer1.fromJson(testInput); + serializer2.fromJson(testInput); + + verify(mockSerializer, times(2)).fromJson(eq(testInput), eq(testType)); + } + + @Test + void testGetCustomerSerializerDifferentTypes() throws Exception { + setMockSerializer(mockSerializer); + + PojoSerializer stringSerializer = PojoSerializerLoader.getCustomerSerializer(String.class); + PojoSerializer integerSerializer = PojoSerializerLoader.getCustomerSerializer(Integer.class); + + assertNotNull(stringSerializer); + assertNotNull(integerSerializer); + + String testString = "test"; + Integer testInt = 123; + + stringSerializer.fromJson(testString); + integerSerializer.fromJson(testInt.toString()); + + verify(mockSerializer).fromJson(eq(testString), eq(String.class)); + verify(mockSerializer).fromJson(eq(testInt.toString()), eq(Integer.class)); + } + + @Test + void testGetCustomerSerializerNullType() throws Exception { + setMockSerializer(mockSerializer); + + PojoSerializer serializer = PojoSerializerLoader.getCustomerSerializer(null); + assertNotNull(serializer); + + String testInput = "test"; + serializer.fromJson(testInput); + verify(mockSerializer).fromJson(eq(testInput), eq(null)); + } + + @Test + void testGetCustomerSerializerExceptionHandling() throws Exception { + setMockSerializer(mockSerializer); + + doThrow(new RuntimeException("Test exception")) + .when(mockSerializer) + .fromJson(any(String.class), any(Type.class)); + + PojoSerializer serializer = PojoSerializerLoader.getCustomerSerializer(String.class); + assertNotNull(serializer); + assertThrows(RuntimeException.class, () -> serializer.fromJson("test")); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/TooManyServiceProvidersFoundExceptionTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/TooManyServiceProvidersFoundExceptionTest.java new file mode 100644 index 000000000..38d33f63b --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/TooManyServiceProvidersFoundExceptionTest.java @@ -0,0 +1,59 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client; + +import org.junit.jupiter.api.Test; + +import com.amazonaws.services.lambda.runtime.api.client.TooManyServiceProvidersFoundException; + +import static org.junit.jupiter.api.Assertions.*; + +class TooManyServiceProvidersFoundExceptionTest { + + @Test + void testDefaultConstructor() { + TooManyServiceProvidersFoundException exception = new TooManyServiceProvidersFoundException(); + + assertNotNull(exception); + assertNull(exception.getMessage()); + assertNull(exception.getCause()); + } + + @Test + void testMessageConstructor() { + String errorMessage = "Too many service providers found"; + TooManyServiceProvidersFoundException exception = + new TooManyServiceProvidersFoundException(errorMessage); + + assertNotNull(exception); + assertEquals(errorMessage, exception.getMessage()); + assertNull(exception.getCause()); + } + + @Test + void testCauseConstructor() { + Throwable cause = new IllegalStateException("Original error"); + TooManyServiceProvidersFoundException exception = + new TooManyServiceProvidersFoundException(cause); + + assertNotNull(exception); + assertEquals(cause.toString(), exception.getMessage()); + assertSame(cause, exception.getCause()); + } + + @Test + void testMessageAndCauseConstructor() { + String errorMessage = "Too many service providers found"; + Throwable cause = new IllegalStateException("Original error"); + TooManyServiceProvidersFoundException exception = + new TooManyServiceProvidersFoundException(errorMessage, cause); + + assertNotNull(exception); + assertEquals(errorMessage, exception.getMessage()); + assertSame(cause, exception.getCause()); + } + +} diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/UserFaultTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/UserFaultTest.java new file mode 100644 index 000000000..479162adf --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/UserFaultTest.java @@ -0,0 +1,165 @@ +/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ + +package com.amazonaws.services.lambda.runtime.api.client; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; +import static testpkg.StackTraceHelper.callThenThrowRuntimeException; +import static testpkg.StackTraceHelper.throwCheckpointExceptionWithTwoSuppressedExceptions; +import static testpkg.StackTraceHelper.throwRuntimeException; + +public class UserFaultTest { + + @Test + public void testReportableErrorNoTraces() { + UserFault userFault = UserFault.makeUserFault(new RuntimeException("woops")); + + String actual = userFault.reportableError(); + String expected = "woops: java.lang.RuntimeException\n" + + "java.lang.RuntimeException: woops\n\n"; + + assertEquals(expected, actual); + } + + @Test + public void testReportableErrorSingleTrace() { + try { + throwRuntimeException("woops"); + } catch (RuntimeException e) { + UserFault userFault = UserFault.makeUserFault(e); + + String actual = userFault.reportableError(); + String expected = "woops: java.lang.RuntimeException\n" + + "java.lang.RuntimeException: woops\n" + + "\tat testpkg.StackTraceHelper.throwRuntimeException\\(StackTraceHelper.java:\\d+\\)\n\n"; + + assertTrue(actual.matches(expected), String.format("'%s' did not match '%s'", actual, expected)); + return; + } + + fail("Exception should have been thrown and caught"); + } + + @Test + public void testReportableErrorMultipleTraces() { + try { + callThenThrowRuntimeException("woops"); + } catch (RuntimeException e) { + UserFault userFault = UserFault.makeUserFault(e); + + String actual = userFault.reportableError(); + String expected = "woops: java.lang.RuntimeException\n" + + "java.lang.RuntimeException: woops\n" + + "\tat testpkg.StackTraceHelper.throwRuntimeException\\(StackTraceHelper.java:\\d+\\)\n" + + "\tat testpkg.StackTraceHelper.callThenThrowRuntimeException\\(StackTraceHelper.java:\\d+\\)\n\n"; + + assertTrue(actual.matches(expected), String.format("'%s' did not match '%s'", actual, expected)); + return; + } + + fail("Exception should have been thrown and caught"); + } + + @Test + public void testReportableErrorOnlyMessage() { + String msg = "No public method named handleRequest with appropriate method signature found on class example.Function"; + UserFault userFault = UserFault.makeUserFault(msg); + + String expected = msg + '\n'; + String actual = userFault.reportableError(); + assertEquals(expected, actual); + } + + @Test + public void testSuppressedExceptionsAreIncluded() { + try{ + throwCheckpointExceptionWithTwoSuppressedExceptions("error 1", "error 2"); + } catch(Exception e1) { + UserFault userFault = UserFault.makeUserFault(e1); + String reportableUserFault = userFault.reportableError(); + + assertTrue(reportableUserFault.contains("com.amazonaws.services.lambda.crac.CheckpointException"), "CheckpointException missing in reported UserFault"); + assertTrue(reportableUserFault.contains("Suppressed: java.lang.RuntimeException: error 1"), "Suppressed error 1 missing in reported UserFault"); + assertTrue(reportableUserFault.contains("Suppressed: java.lang.RuntimeException: error 2"), "Suppressed error 2 missing in reported UserFault"); + } + } + + @Test + public void testCircularExceptionReference() { + RuntimeException e1 = new RuntimeException(); + RuntimeException e2 = new RuntimeException(e1); + e1.initCause(e2); + + try { + throw e2; + } catch (Exception e) { + String stackTrace = UserFault.trace(e); + String expectedStackTrace = "java.lang.RuntimeException: java.lang.RuntimeException\n" + + "Caused by: java.lang.RuntimeException\n" + + "Caused by: [CIRCULAR REFERENCE: java.lang.RuntimeException: java.lang.RuntimeException]\n"; + + assertEquals(expectedStackTrace, stackTrace); + } + } + + @Test + public void testCircularSuppressedExceptionReference() { + RuntimeException e1 = new RuntimeException("Primary Exception"); + RuntimeException e2 = new RuntimeException(e1); + RuntimeException e3 = new RuntimeException("Exception with suppressed"); + + e1.addSuppressed(e2); + e3.addSuppressed(e2); + + try { + throw e3; + } catch (Exception e) { + String stackTrace = UserFault.trace(e); + String expectedStackTrace = "java.lang.RuntimeException: Exception with suppressed\n" + + "\tSuppressed: java.lang.RuntimeException: java.lang.RuntimeException: Primary Exception\n" + + "\tCaused by: java.lang.RuntimeException: Primary Exception\n" + + "\t\tSuppressed: [CIRCULAR REFERENCE: java.lang.RuntimeException: java.lang.RuntimeException: Primary Exception]\n"; + + assertEquals(expectedStackTrace, stackTrace); + } + } + + private Exception createExceptionWithStackTrace() { + try { + throw new RuntimeException("Test exception"); + } catch (RuntimeException e) { + return e; + } + } + + @Test + void testMakeInitErrorUserFault() { + String className = "com.example.TestClass"; + Exception testException = createExceptionWithStackTrace(); + + UserFault initFault = UserFault.makeInitErrorUserFault(testException, className); + UserFault notFoundFault = UserFault.makeClassNotFoundUserFault(testException, className); + + assertNotNull(initFault.trace); + assertNotNull(notFoundFault.trace); + + assertFalse(initFault.trace.contains("com.amazonaws.services.lambda.runtime")); + assertFalse(notFoundFault.trace.contains("com.amazonaws.services.lambda.runtime")); + } + + @Test + void testMakeClassNotFoundUserFault() { + String className = "com.example.MissingClass"; + Exception testException = new ClassNotFoundException("Class not found in classpath"); + + UserFault fault = UserFault.makeClassNotFoundUserFault(testException, className); + + assertNotNull(fault); + assertEquals("Class not found: com.example.MissingClass", fault.msg); + assertEquals("java.lang.ClassNotFoundException", fault.exception); + assertNotNull(fault.trace); + assertFalse(fault.fatal); + assertTrue(fault.trace.contains("ClassNotFoundException")); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/XRayErrorCauseTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/XRayErrorCauseTest.java new file mode 100644 index 000000000..8de6963a8 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/XRayErrorCauseTest.java @@ -0,0 +1,87 @@ +/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ + +package com.amazonaws.services.lambda.runtime.api.client; + +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.converters.XRayErrorCauseConverter; +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.XRayErrorCause; +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.XRayException; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static testpkg.StackTraceHelper.callThenThrowRuntimeException; + +public class XRayErrorCauseTest { + + private static final String TEST_WORKING_DIR = "/tmp"; + private static final String ORIGINAL_WORKING_DIR = System.getProperty("user.dir"); + + @BeforeEach + public void before() { + System.setProperty("user.dir", TEST_WORKING_DIR); + } + + @AfterEach + public void after() { + System.setProperty("user.dir", ORIGINAL_WORKING_DIR); + } + + @Test + public void xrayErrorCauseTest() { + try { + callThenThrowRuntimeException("woops"); + } catch (Throwable t) { + UserFault.filterStackTrace(t); + assertXrayErrorCause(t); + } + } + + @Test + public void xrayErrorCauseTestNoFileName() { + try { + callThenThrowRuntimeException("woops"); + } catch (Throwable t) { + UserFault.filterStackTrace(t); + clearStackTraceElementsFilename(t); + assertXrayErrorCause(t); + } + } + + private void assertXrayErrorCause(Throwable t) { + XRayErrorCause xRayErrorCause = XRayErrorCauseConverter.fromThrowable(t); + + assertEquals(TEST_WORKING_DIR, xRayErrorCause.working_directory); + + assertEquals(1, xRayErrorCause.paths.size()); + assertTrue(xRayErrorCause.paths.contains("StackTraceHelper.java")); + + assertEquals(1, xRayErrorCause.exceptions.size()); + XRayException xRayException = xRayErrorCause.exceptions.iterator().next(); + assertEquals("woops", xRayException.message); + assertEquals("java.lang.RuntimeException", xRayException.type); + + assertEquals("throwRuntimeException", xRayException.stack.get(0).label); + assertEquals("StackTraceHelper.java", xRayException.stack.get(0).path); + assertTrue(xRayException.stack.get(0).line > 0); + + assertEquals("callThenThrowRuntimeException", xRayException.stack.get(1).label); + assertEquals("StackTraceHelper.java", xRayException.stack.get(1).path); + assertTrue(xRayException.stack.get(0).line > 0); + } + + private void clearStackTraceElementsFilename(Throwable t) { + StackTraceElement[] stackTrace = t.getStackTrace(); + StackTraceElement[] updatedStackTrace = new StackTraceElement[stackTrace.length]; + + for(int i = 0; i < updatedStackTrace.length; i++) { + StackTraceElement curr = stackTrace[i]; + updatedStackTrace[i] = new StackTraceElement(curr.getClassName(), curr.getMethodName(), null, curr.getLineNumber()); + } + + t.setStackTrace(updatedStackTrace); + } + + +} diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaContextTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaContextTest.java new file mode 100644 index 000000000..f7da76198 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/api/LambdaContextTest.java @@ -0,0 +1,61 @@ +/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ + +package com.amazonaws.services.lambda.runtime.api.client.api; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class LambdaContextTest { + + private static final String REQUEST_ID = "request-id"; + private static final String LOG_GROUP_NAME = "log-group-name"; + private static final String LOG_STREAM_NAME = "log-stream-name"; + private static final String FUNCTION_NAME = "function-name"; + private static final LambdaCognitoIdentity IDENTITY = new LambdaCognitoIdentity("identity-id", "pool-id"); + private static final String FUNCTION_VERSION = "function-version"; + private static final String INVOKED_FUNCTION_ARN = "invoked-function-arn"; + private static final LambdaClientContext CLIENT_CONTEXT = new LambdaClientContext(); + public static final int MEMORY_LIMIT = 128; + public static final String TENANT_ID = "tenant-id"; + public static final String X_RAY_TRACE_ID = "x-ray-trace-id"; + + @Test + public void getRemainingTimeInMillis() { + long now = System.currentTimeMillis(); + LambdaContext ctx = createContextWithDeadline(now + 1000); + + int actual = ctx.getRemainingTimeInMillis(); + + assertTrue(actual > 0); + assertTrue(actual <= 1000); + } + + @Test + public void getRemainingTimeInMillis_Sleep() throws InterruptedException { + long now = System.currentTimeMillis(); + LambdaContext ctx = createContextWithDeadline(now + 1000); + + int before = ctx.getRemainingTimeInMillis(); + Thread.sleep(100); + int after = ctx.getRemainingTimeInMillis(); + + assertTrue((before - after) >= 100); + } + + @Test + public void getRemainingTimeInMillis_Deadline() throws InterruptedException { + long now = System.currentTimeMillis(); + LambdaContext ctx = createContextWithDeadline(now + 100); + + Thread.sleep(110); + + assertEquals(0, ctx.getRemainingTimeInMillis()); + } + + private LambdaContext createContextWithDeadline(long deadlineTimeInMs) { + return new LambdaContext(MEMORY_LIMIT, deadlineTimeInMs, REQUEST_ID, LOG_GROUP_NAME, LOG_STREAM_NAME, + FUNCTION_NAME, IDENTITY, FUNCTION_VERSION, INVOKED_FUNCTION_ARN, TENANT_ID, X_RAY_TRACE_ID, CLIENT_CONTEXT); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/AbstractLambdaLoggerTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/AbstractLambdaLoggerTest.java new file mode 100644 index 000000000..3a5ee8d5f --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/AbstractLambdaLoggerTest.java @@ -0,0 +1,187 @@ +package com.amazonaws.services.lambda.runtime.api.client.logging; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.amazonaws.services.lambda.runtime.logging.LogFormat; +import org.junit.jupiter.api.Test; + +import java.nio.charset.StandardCharsets; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import com.amazonaws.lambda.thirdparty.org.json.JSONObject; +import com.amazonaws.services.lambda.runtime.LambdaLogger; +import com.amazonaws.services.lambda.runtime.api.client.api.LambdaContext; +import com.amazonaws.services.lambda.runtime.logging.LogLevel; + + +public class AbstractLambdaLoggerTest { + class TestSink implements LogSink { + private List messages = new LinkedList<>(); + + public TestSink() { + } + + @Override + public synchronized void log(byte[] message) { + messages.add(message); + } + + @Override + public synchronized void log(LogLevel logLevel, LogFormat logFormat, byte[] message) { + messages.add(message); + } + + @Override + public void close() { + } + + List getMessages() { + return messages; + } + } + + private void logMessages(LambdaLogger logger) { + logger.log("trace", LogLevel.TRACE); + logger.log("debug", LogLevel.DEBUG); + logger.log("info", LogLevel.INFO); + logger.log("warn", LogLevel.WARN); + logger.log("error", LogLevel.ERROR); + logger.log("fatal", LogLevel.FATAL); + } + + @Test + public void testLoggingNullValuesWithoutLogLevelInText() { + TestSink sink = new TestSink(); + LambdaLogger logger = new LambdaContextLogger(sink, LogLevel.INFO, LogFormat.TEXT); + + String isNullString = null; + byte[] isNullBytes = null; + + logger.log(isNullString); + logger.log(isNullBytes); + + assertEquals("null", new String(sink.getMessages().get(0))); + assertEquals("null", new String(sink.getMessages().get(1))); + } + + /* + * Makes Sure Logging Contexts are thread local. + * We start `setLambdaContext` operations using the **single** shared `logger` object on a fixed thread pool, differentiating them with thread IDs. + * We then start concurrent `log` operations which are scheduled using that fixed pool. + * It is then verified that a given log operation, which logs the thread ID it is running on, used a context that had the same thread ID. + */ + @Test + public void testMultiConcurrentLoggingWithoutLogLevelInJSON() { + TestSink sink = new TestSink(); + LambdaContextLogger logger = new LambdaContextLogger(sink, LogLevel.INFO, LogFormat.JSON); + + String someMessagePrefix = "Some Message from "; + String reqIDPrefix = "Thread ID as request# "; + + final int nThreads = 5; + ExecutorService es = Executors.newFixedThreadPool(nThreads); + for (int i = 0; i < nThreads; i++) { + es.submit(() -> logger.setLambdaContext(new LambdaContext(Integer.MAX_VALUE, Long.MAX_VALUE, reqIDPrefix + Thread.currentThread().getName(), "", "", "", null, "", "", "", null, null))); + } + + final int nMessages = 100_000; + for (int i = 0; i < nMessages; i++) { + es.submit(() -> logger.log(someMessagePrefix + Thread.currentThread().getName())); + } + + es.shutdown(); + while (!es.isTerminated()) { + ; + } + + assertEquals(nMessages, sink.getMessages().size()); + for (byte[] message : sink.getMessages()) { + JSONObject parsedLog = new JSONObject(new String(message, StandardCharsets.UTF_8)); + String parsedMessage = parsedLog.getString("message"); + String parsedReqID = parsedLog.getString("AWSRequestId"); + assertEquals(parsedMessage.substring(someMessagePrefix.length()), parsedReqID.substring(reqIDPrefix.length())); + } + } + + @Test + public void testLoggingNullValuesWithoutLogLevelInJSON() { + TestSink sink = new TestSink(); + LambdaLogger logger = new LambdaContextLogger(sink, LogLevel.INFO, LogFormat.JSON); + + String isNullString = null; + byte[] isNullBytes = null; + + logger.log(isNullString); + logger.log(isNullBytes); + + assertEquals(2, sink.getMessages().size()); + } + + @Test + public void testLoggingNullValuesWithLogLevelInText() { + TestSink sink = new TestSink(); + LambdaLogger logger = new LambdaContextLogger(sink, LogLevel.INFO, LogFormat.TEXT); + + String isNullString = null; + byte[] isNullBytes = null; + + logger.log(isNullString, LogLevel.ERROR); + logger.log(isNullBytes, LogLevel.ERROR); + + assertEquals("[ERROR] null", new String(sink.getMessages().get(0))); + assertEquals("null", new String(sink.getMessages().get(1))); + } + + @Test + public void testLoggingNullValuesWithLogLevelInJSON() { + TestSink sink = new TestSink(); + LambdaLogger logger = new LambdaContextLogger(sink, LogLevel.INFO, LogFormat.JSON); + + String isNullString = null; + byte[] isNullBytes = null; + + logger.log(isNullString, LogLevel.ERROR); + logger.log(isNullBytes, LogLevel.ERROR); + + assertEquals(2, sink.getMessages().size()); + } + @Test + public void testWithoutFiltering() { + TestSink sink = new TestSink(); + LambdaLogger logger = new LambdaContextLogger(sink, LogLevel.UNDEFINED, LogFormat.TEXT); + logMessages(logger); + + assertEquals(6, sink.getMessages().size()); + } + + @Test + public void testWithFiltering() { + TestSink sink = new TestSink(); + LambdaLogger logger = new LambdaContextLogger(sink, LogLevel.WARN, LogFormat.TEXT); + logMessages(logger); + + assertEquals(3, sink.getMessages().size()); + } + + @Test + public void testUndefinedLogLevelWithFiltering() { + TestSink sink = new TestSink(); + LambdaLogger logger = new LambdaContextLogger(sink, LogLevel.WARN, LogFormat.TEXT); + logger.log("undefined"); + + assertEquals(1, sink.getMessages().size()); + } + + @Test + public void testFormattingLogMessages() { + TestSink sink = new TestSink(); + LambdaLogger logger = new LambdaContextLogger(sink, LogLevel.INFO, LogFormat.TEXT); + logger.log("test message", LogLevel.INFO); + + assertEquals(1, sink.getMessages().size()); + assertEquals("[INFO] test message", new String(sink.getMessages().get(0))); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/FrameTypeTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/FrameTypeTest.java new file mode 100644 index 000000000..65078790c --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/FrameTypeTest.java @@ -0,0 +1,39 @@ +package com.amazonaws.services.lambda.runtime.api.client.logging; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +import com.amazonaws.services.lambda.runtime.logging.LogLevel; +import com.amazonaws.services.lambda.runtime.logging.LogFormat; + +public class FrameTypeTest { + + @Test + public void logFrames() { + assertHexEquals( + 0xa55a0003, + FrameType.getValue(LogLevel.UNDEFINED, LogFormat.TEXT) + ); + + assertHexEquals( + 0xa55a001b, + FrameType.getValue(LogLevel.FATAL, LogFormat.TEXT) + ); + } + + + /** + * Helper function to make it easier to debug failing test. + * + * @param expected Expected value as int + * @param actual Actual value as int + */ + private void assertHexEquals(int expected, int actual) { + assertEquals( + Integer.toHexString(expected), + Integer.toHexString(actual) + ); + } + +} diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/FramedTelemetryLogSinkTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/FramedTelemetryLogSinkTest.java new file mode 100644 index 000000000..e3e68a693 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/FramedTelemetryLogSinkTest.java @@ -0,0 +1,170 @@ +/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ + +package com.amazonaws.services.lambda.runtime.api.client.logging; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ReadableByteChannel; +import java.nio.file.Path; +import java.time.Instant; +import java.util.Arrays; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.amazonaws.services.lambda.runtime.logging.LogLevel; +import com.amazonaws.services.lambda.runtime.logging.LogFormat; + +public class FramedTelemetryLogSinkTest { + + private static final int DEFAULT_BUFFER_SIZE = 256; + private static final byte ZERO_BYTE = (byte) 0; + + private long timestamp() { + Instant instant = Instant.now(); + return instant.getEpochSecond() * 1_000_000 + instant.getNano() / 1000; + } + + @TempDir + public Path tmpFolder; + + @Test + public void logSingleFrame() throws IOException { + byte[] message = "{\"message\": \"hello world\nsomething on a new line!\"}".getBytes(); + LogLevel logLevel = LogLevel.ERROR; + LogFormat logFormat = LogFormat.JSON; + + File tmpFile = tmpFolder.resolve("pipe").toFile(); + FileOutputStream fos = new FileOutputStream(tmpFile); + FileDescriptor fd = fos.getFD(); + long before = timestamp(); + try (FramedTelemetryLogSink logSink = new FramedTelemetryLogSink(fd)) { + logSink.log(logLevel, logFormat, message); + } + long after = timestamp(); + + ByteBuffer buf = ByteBuffer.allocate(DEFAULT_BUFFER_SIZE); + ReadableByteChannel readChannel = new FileInputStream(tmpFile).getChannel(); + readChannel.read(buf); + + // reset the position to the start + buf.position(0); + + // first 4 bytes indicate the type + int type = buf.getInt(); + assertEquals(FrameType.getValue(logLevel, logFormat), type); + + // next 4 bytes indicate the length of the message + int len = buf.getInt(); + assertEquals(message.length, len); + + // next 8 bytes should indicate the timestamp + long timestamp = buf.getLong(); + assertTrue(before <= timestamp); + assertTrue(timestamp <= after); + + // use `len` to allocate a byte array to read the logged message into + byte[] actual = new byte[len]; + buf.get(actual); + assertArrayEquals(message, actual); + + // rest of buffer should be empty + while (buf.hasRemaining()) + assertEquals(ZERO_BYTE, buf.get()); + } + + @Test + public void logMultipleFrames() throws IOException { + byte[] firstMessage = "hello world\nsomething on a new line!".getBytes(); + byte[] secondMessage = "hello again\nhere's another message\n".getBytes(); + LogLevel logLevel = LogLevel.ERROR; + LogFormat logFormat = LogFormat.TEXT; + + File tmpFile = tmpFolder.resolve("pipe").toFile(); + FileOutputStream fos = new FileOutputStream(tmpFile); + FileDescriptor fd = fos.getFD(); + long before = timestamp(); + try (FramedTelemetryLogSink logSink = new FramedTelemetryLogSink(fd)) { + logSink.log(logLevel, logFormat, firstMessage); + logSink.log(logLevel, logFormat, secondMessage); + } + long after = timestamp(); + + ByteBuffer buf = ByteBuffer.allocate(DEFAULT_BUFFER_SIZE); + ReadableByteChannel readChannel = new FileInputStream(tmpFile).getChannel(); + readChannel.read(buf); + + // reset the position to the start + buf.position(0); + + for (byte[] message : Arrays.asList(firstMessage, secondMessage)) { + // first 4 bytes indicate the type + int type = buf.getInt(); + assertEquals(FrameType.getValue(logLevel, logFormat), type); + + // next 4 bytes indicate the length of the message + int len = buf.getInt(); + assertEquals(message.length, len); + + // next 8 bytes should indicate the timestamp + long timestamp = buf.getLong(); + assertTrue(before <= timestamp); + assertTrue(timestamp <= after); + + // use `len` to allocate a byte array to read the logged message into + byte[] actual = new byte[len]; + buf.get(actual); + assertArrayEquals(message, actual); + } + + // rest of buffer should be empty + while (buf.hasRemaining()) + assertEquals(ZERO_BYTE, buf.get()); + } + + /** + * The implementation of FramedTelemetryLogSink was based on java.nio.channels.WritableByteChannel which would + * throw ClosedByInterruptException if Thread.currentThread.interrupt() was called. The implementation was changed + * and this test ensures that logging works even if the current thread was interrupted. + *

+ * https://t.corp.amazon.com/0304370986/ + */ + @Test + public void interruptedThread() throws IOException { + try { + byte[] message = "hello world\nsomething on a new line!\n".getBytes(); + File tmpFile = tmpFolder.resolve("pipe").toFile(); + FileOutputStream fos = new FileOutputStream(tmpFile); + FileDescriptor fd = fos.getFD(); + try (FramedTelemetryLogSink logSink = new FramedTelemetryLogSink(fd)) { + Thread.currentThread().interrupt(); + + logSink.log(LogLevel.ERROR, LogFormat.TEXT, message); + } + + byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; + FileInputStream logInputStream = new FileInputStream(tmpFile); + int readBytes = logInputStream.read(buffer); + + int headerSizeBytes = 16; // message type (4 bytes) + len (4 bytes) + timestamp (8 bytes) + int expectedBytes = headerSizeBytes + message.length; + + assertEquals(expectedBytes, readBytes); + + for (int i = 0; i < message.length; i++) { + assertEquals(message[i], buffer[i + headerSizeBytes]); + } + } finally { + // clear interrupted status of the current thread + assertTrue(Thread.interrupted()); + } + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/JsonLogFormatterTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/JsonLogFormatterTest.java new file mode 100644 index 000000000..91ce9d2a3 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/JsonLogFormatterTest.java @@ -0,0 +1,79 @@ +package com.amazonaws.services.lambda.runtime.api.client.logging; + +import com.amazonaws.services.lambda.runtime.api.client.api.LambdaContext; +import com.amazonaws.services.lambda.runtime.serialization.PojoSerializer; +import com.amazonaws.services.lambda.runtime.serialization.factories.GsonFactory; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.amazonaws.services.lambda.runtime.logging.LogLevel; + +public class JsonLogFormatterTest { + + @Test + void testFormattingWithoutLambdaContext() { + assertFormatsString("test log", LogLevel.WARN, null); + } + + @Test + void testFormattingWithLambdaContext() { + LambdaContext context = new LambdaContext( + 0, + 0, + "request-id", + null, + null, + "function-name", + null, + null, + "function-arn", + null, + null, + null + ); + assertFormatsString("test log", LogLevel.WARN, context); + } + + @Test + void testFormattingWithTenantIdInLambdaContext() { + LambdaContext context = new LambdaContext( + 0, + 0, + "request-id", + null, + null, + "function-name", + null, + null, + "function-arn", + "tenant-id", + "xray-trace-id", + null + ); + assertFormatsString("test log", LogLevel.WARN, context); + } + + void assertFormatsString(String message, LogLevel logLevel, LambdaContext context) { + JsonLogFormatter logFormatter = new JsonLogFormatter(); + if (context != null) { + logFormatter.setLambdaContext(context); + } + String output = logFormatter.format(message, logLevel); + + PojoSerializer serializer = GsonFactory.getInstance().getSerializer(StructuredLogMessage.class); + assert_expected_log_message(serializer.fromJson(output), message, logLevel, context); + } + + void assert_expected_log_message(StructuredLogMessage result, String message, LogLevel logLevel, LambdaContext context) { + assertEquals(message, result.message); + assertEquals(logLevel, result.level); + assertNotNull(result.timestamp); + + if (context != null) { + assertEquals(context.getAwsRequestId(), result.AWSRequestId); + assertEquals(context.getTenantId(), result.tenantId); + } + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/StdOutLogSinkTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/StdOutLogSinkTest.java new file mode 100644 index 000000000..b1bbefc4c --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/StdOutLogSinkTest.java @@ -0,0 +1,70 @@ +/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ + +package com.amazonaws.services.lambda.runtime.api.client.logging; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.amazonaws.services.lambda.runtime.logging.LogFormat; +import com.amazonaws.services.lambda.runtime.logging.LogLevel; + +public class StdOutLogSinkTest { + + private final PrintStream originalOutPrintStream = System.out; + private final ByteArrayOutputStream bos = new ByteArrayOutputStream(); + private final PrintStream capturedOutPrintStream = new PrintStream(bos); + + @BeforeEach + public void setup() { + bos.reset(); + } + + @Test + public void testSingleLog() { + System.setOut(capturedOutPrintStream); + try { + try (StdOutLogSink logSink = new StdOutLogSink()) { + logSink.log("hello\nworld".getBytes()); + } + } finally { + System.setOut(originalOutPrintStream); + } + + assertEquals("hello\nworld", bos.toString()); + } + + @Test + public void testSingleLogWithLogLevel() { + System.setOut(capturedOutPrintStream); + try { + try (StdOutLogSink logSink = new StdOutLogSink()) { + logSink.log(LogLevel.ERROR, LogFormat.TEXT, "hello\nworld".getBytes()); + } + } finally { + System.setOut(originalOutPrintStream); + } + + assertEquals("hello\nworld", bos.toString()); + } + + @Test + public void testContextLoggerWithStdoutLogSink_logBytes() { + System.setOut(capturedOutPrintStream); + try { + try (StdOutLogSink logSink = new StdOutLogSink()) { + logSink.log("hello\nworld".getBytes()); + logSink.log("hello again".getBytes()); + } + } finally { + System.setOut(originalOutPrintStream); + } + + assertEquals("hello\nworldhello again", bos.toString()); + } + +} diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/TextLogFormatterTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/TextLogFormatterTest.java new file mode 100644 index 000000000..598074a3b --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/logging/TextLogFormatterTest.java @@ -0,0 +1,25 @@ +package com.amazonaws.services.lambda.runtime.api.client.logging; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.amazonaws.services.lambda.runtime.logging.LogLevel; + +class TextLogFormatterTest { + @Test + void testFormattingStringWithLogLevel() { + assertFormatsString("test log", LogLevel.WARN, "[WARN] test log"); + } + + @Test + void testFormattingStringWithoutLogLevel() { + assertFormatsString("test log", LogLevel.UNDEFINED, "test log"); + } + + void assertFormatsString(String input, LogLevel logLevel, String expected) { + LogFormatter logFormatter = new TextLogFormatter(); + String output = logFormatter.format(input, logLevel); + assertEquals(expected, output); + } +} \ No newline at end of file diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaRuntimeApiClientImplTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaRuntimeApiClientImplTest.java new file mode 100644 index 000000000..9d5929263 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/LambdaRuntimeApiClientImplTest.java @@ -0,0 +1,539 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client.runtimeapi; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.amazonaws.services.lambda.runtime.api.client.logging.LambdaContextLogger; +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.ErrorRequest; +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.InvocationRequest; +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.StackElement; +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.XRayErrorCause; +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.XRayException; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; +import java.util.function.Supplier; + +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import okhttp3.HttpUrl; +import static java.net.HttpURLConnection.HTTP_ACCEPTED; +import static java.net.HttpURLConnection.HTTP_OK; +import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR; +import okhttp3.mockwebserver.MockWebServer; + +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.RecordedRequest; + +@DisabledOnOs(OS.MAC) +public class LambdaRuntimeApiClientImplTest { + + @SuppressWarnings("rawtypes") + private final Supplier mockSupplier = mock(Supplier.class); + @SuppressWarnings("rawtypes") + private final Function mockExceptionMessageComposer = mock(Function.class); + private final LambdaContextLogger mockLambdaContextLogger = mock(LambdaContextLogger.class); + private final LambdaRuntimeClientMaxRetriesExceededException retriesExceededException = new LambdaRuntimeClientMaxRetriesExceededException("Testing Invocations"); + final String fakeExceptionMessage = "Something bad"; + + MockWebServer mockWebServer; + LambdaRuntimeApiClientImpl lambdaRuntimeApiClientImpl; + + String[] errorStackStrace = { "item0", "item1", "item2" }; + ErrorRequest errorRequest = new ErrorRequest("testErrorMessage", "testErrorType", errorStackStrace); + + String requestId = "1234"; + + @BeforeEach + void setUp() { + mockWebServer = new MockWebServer(); + String hostnamePort = getHostnamePort(); + lambdaRuntimeApiClientImpl = new LambdaRuntimeApiClientImpl(hostnamePort); + } + + @SuppressWarnings("unchecked") + @Test + public void testgetSupplierResultWithExponentialBackoffAllFailing() throws Exception { + + when(mockSupplier.get()).thenThrow(new RuntimeException(new Exception(fakeExceptionMessage))); + when(mockExceptionMessageComposer.apply(any())).thenReturn(fakeExceptionMessage); + + try { + LambdaRuntimeApiClientImpl.getSupplierResultWithExponentialBackoff(mockLambdaContextLogger, 5, 200, 5, mockSupplier, mockExceptionMessageComposer, retriesExceededException); + } catch (LambdaRuntimeClientMaxRetriesExceededException e) { } + + verify(mockSupplier, times(5)).get(); + verify(mockLambdaContextLogger).log(eq(fakeExceptionMessage + "\nRetrying."), any()); + verify(mockLambdaContextLogger).log(eq(fakeExceptionMessage + "\nRetrying in 5 ms."), any()); + verify(mockLambdaContextLogger).log(eq(fakeExceptionMessage + "\nRetrying in 10 ms."), any()); + verify(mockLambdaContextLogger).log(eq(fakeExceptionMessage + "\nRetrying in 20 ms."), any()); + verify(mockLambdaContextLogger).log(eq(fakeExceptionMessage), any()); + verify(mockLambdaContextLogger, times(5)).log(anyString(), any()); + } + + @SuppressWarnings("unchecked") + @Test + public void testgetSupplierResultWithExponentialBackoffTwoFailingThenSuccess() throws Exception { + InvocationRequest fakeRequest = new InvocationRequest(); + + when(mockExceptionMessageComposer.apply(any())).thenReturn(fakeExceptionMessage); + + when(mockSupplier.get()) + .thenThrow(new RuntimeException(new Exception(fakeExceptionMessage))) + .thenThrow(new RuntimeException(new Exception(fakeExceptionMessage))) + .thenReturn(fakeRequest); + + InvocationRequest invocationRequest = (InvocationRequest) LambdaRuntimeApiClientImpl.getSupplierResultWithExponentialBackoff(mockLambdaContextLogger, 5, 200, 5, mockSupplier, mockExceptionMessageComposer, retriesExceededException); + + assertEquals(fakeRequest, invocationRequest); + verify(mockSupplier, times(3)).get(); + verify(mockLambdaContextLogger).log(eq(fakeExceptionMessage + "\nRetrying."), any()); + verify(mockLambdaContextLogger).log(eq(fakeExceptionMessage + "\nRetrying in 5 ms."), any()); + verify(mockLambdaContextLogger, times(2)).log(anyString(), any()); + } + + @SuppressWarnings("unchecked") + @Test + public void testgetSupplierResultWithExponentialBackoffDoesntGoAboveMax() throws Exception { + + when(mockSupplier.get()).thenThrow(new RuntimeException(new Exception(fakeExceptionMessage))); + + when(mockExceptionMessageComposer.apply(any())).thenReturn(fakeExceptionMessage); + + try { + LambdaRuntimeApiClientImpl.getSupplierResultWithExponentialBackoff(mockLambdaContextLogger, 100, 200, 5, mockSupplier, mockExceptionMessageComposer, retriesExceededException); + } catch (LambdaRuntimeClientMaxRetriesExceededException e) { } + + verify(mockSupplier, times(5)).get(); + verify(mockLambdaContextLogger).log(eq(fakeExceptionMessage + "\nRetrying."), any()); + verify(mockLambdaContextLogger).log(eq(fakeExceptionMessage + "\nRetrying in 100 ms."), any()); + verify(mockLambdaContextLogger, times(2)).log(eq(fakeExceptionMessage + "\nRetrying in 200 ms."), any()); + verify(mockLambdaContextLogger).log(eq(fakeExceptionMessage), any()); + verify(mockLambdaContextLogger, times(5)).log(anyString(), any()); + } + + @Test + public void reportInitErrorTest() { + try { + RapidErrorType rapidErrorType = RapidErrorType.AfterRestoreError; + + MockResponse mockResponse = new MockResponse(); + mockResponse.setResponseCode(HTTP_ACCEPTED); + mockWebServer.enqueue(mockResponse); + + LambdaError lambdaError = new LambdaError(errorRequest, rapidErrorType); + lambdaRuntimeApiClientImpl.reportInitError(lambdaError); + RecordedRequest recordedRequest = mockWebServer.takeRequest(); + HttpUrl actualUrl = recordedRequest.getRequestUrl(); + String expectedUrl = "http://" + getHostnamePort() + "/2018-06-01/runtime/init/error"; + assertEquals(expectedUrl, actualUrl.toString()); + + String userAgentHeader = recordedRequest.getHeader("User-Agent"); + assertTrue(userAgentHeader.startsWith("aws-lambda-java/")); + + String lambdaRuntimeErrorTypeHeader = recordedRequest.getHeader("Lambda-Runtime-Function-Error-Type"); + assertEquals("Runtime.AfterRestoreError", lambdaRuntimeErrorTypeHeader); + + String actualBody = recordedRequest.getBody().readUtf8(); + assertEquals("{\"errorMessage\":\"testErrorMessage\",\"errorType\":\"testErrorType\",\"stackTrace\":[\"item0\",\"item1\",\"item2\"]}", actualBody); + } catch(Exception e) { + fail(); + } + } + + @Test + public void reportInitErrorTestWrongStatusCode() { + int errorStatusCode = HTTP_INTERNAL_ERROR; + try { + RapidErrorType rapidErrorType = RapidErrorType.AfterRestoreError; + + MockResponse mockResponse = new MockResponse(); + mockResponse.setResponseCode(errorStatusCode); + mockWebServer.enqueue(mockResponse); + + LambdaError lambdaError = new LambdaError(errorRequest, rapidErrorType); + lambdaRuntimeApiClientImpl.reportInitError(lambdaError); + fail(); + } catch(LambdaRuntimeClientException e) { + String expectedUrl = "http://" + getHostnamePort() + "/2018-06-01/runtime/init/error"; + String expectedMessage = expectedUrl + " Response code: '" + errorStatusCode + "'."; + assertEquals(expectedMessage, e.getLocalizedMessage()); + } catch(Exception e) { + fail(); + } + } + + @Test + public void reportRestoreErrorTest() { + try { + RapidErrorType rapidErrorType = RapidErrorType.AfterRestoreError; + + MockResponse mockResponse = new MockResponse(); + mockResponse.setResponseCode(HTTP_ACCEPTED); + mockWebServer.enqueue(mockResponse); + + LambdaError lambdaError = new LambdaError(errorRequest, rapidErrorType); + lambdaRuntimeApiClientImpl.reportRestoreError(lambdaError); + RecordedRequest recordedRequest = mockWebServer.takeRequest(); + HttpUrl actualUrl = recordedRequest.getRequestUrl(); + String expectedUrl = "http://" + getHostnamePort() + "/2018-06-01/runtime/restore/error"; + assertEquals(expectedUrl, actualUrl.toString()); + + String userAgentHeader = recordedRequest.getHeader("User-Agent"); + assertTrue(userAgentHeader.startsWith("aws-lambda-java/")); + + String lambdaRuntimeErrorTypeHeader = recordedRequest.getHeader("Lambda-Runtime-Function-Error-Type"); + assertEquals("Runtime.AfterRestoreError", lambdaRuntimeErrorTypeHeader); + + String actualBody = recordedRequest.getBody().readUtf8(); + assertEquals("{\"errorMessage\":\"testErrorMessage\",\"errorType\":\"testErrorType\",\"stackTrace\":[\"item0\",\"item1\",\"item2\"]}", actualBody); + } catch(Exception e) { + fail(); + } + } + + @Test + public void reportRestoreErrorTestWrongStatusCode() { + int errorStatusCode = HTTP_INTERNAL_ERROR; + try { + RapidErrorType rapidErrorType = RapidErrorType.AfterRestoreError; + + MockResponse mockResponse = new MockResponse(); + mockResponse.setResponseCode(errorStatusCode); + mockWebServer.enqueue(mockResponse); + + LambdaError lambdaError = new LambdaError(errorRequest, rapidErrorType); + lambdaRuntimeApiClientImpl.reportRestoreError(lambdaError); + fail(); + } catch(LambdaRuntimeClientException e) { + String expectedUrl = "http://" + getHostnamePort() + "/2018-06-01/runtime/restore/error"; + String expectedMessage = expectedUrl + " Response code: '" + errorStatusCode + "'."; + assertEquals(expectedMessage, e.getLocalizedMessage()); + } catch(Exception e) { + fail(); + } + } + + @Test + public void reportInvocationErrorTest() { + try { + RapidErrorType rapidErrorType = RapidErrorType.AfterRestoreError; + + MockResponse mockResponse = new MockResponse(); + mockResponse.setResponseCode(HTTP_ACCEPTED); + mockWebServer.enqueue(mockResponse); + + LambdaError lambdaError = new LambdaError(errorRequest, rapidErrorType); + lambdaRuntimeApiClientImpl.reportInvocationError(requestId, lambdaError, null); + RecordedRequest recordedRequest = mockWebServer.takeRequest(); + HttpUrl actualUrl = recordedRequest.getRequestUrl(); + String expectedUrl = "http://" + getHostnamePort() + "/2018-06-01/runtime/invocation/1234/error"; + assertEquals(expectedUrl, actualUrl.toString()); + + String userAgentHeader = recordedRequest.getHeader("User-Agent"); + assertTrue(userAgentHeader.startsWith("aws-lambda-java/")); + + String lambdaRuntimeErrorTypeHeader = recordedRequest.getHeader("Lambda-Runtime-Function-Error-Type"); + assertEquals("Runtime.AfterRestoreError", lambdaRuntimeErrorTypeHeader); + + String actualBody = recordedRequest.getBody().readUtf8(); + assertEquals("{\"errorMessage\":\"testErrorMessage\",\"errorType\":\"testErrorType\",\"stackTrace\":[\"item0\",\"item1\",\"item2\"]}", actualBody); + } catch(Exception e) { + fail(); + } + } + + @Test + public void reportInvocationErrorTestWrongStatusCode() { + int errorStatusCode = HTTP_INTERNAL_ERROR; + try { + RapidErrorType rapidErrorType = RapidErrorType.AfterRestoreError; + + MockResponse mockResponse = new MockResponse(); + mockResponse.setResponseCode(errorStatusCode); + mockWebServer.enqueue(mockResponse); + + LambdaError lambdaError = new LambdaError(errorRequest, rapidErrorType); + lambdaRuntimeApiClientImpl.reportInvocationError(requestId, lambdaError, null); + fail(); + } catch(LambdaRuntimeClientException e) { + String expectedUrl = "http://" + getHostnamePort() + "/2018-06-01/runtime/invocation/1234/error"; + String expectedMessage = expectedUrl + " Response code: '" + errorStatusCode + "'."; + assertEquals(expectedMessage, e.getLocalizedMessage()); + } catch(Exception e) { + fail(); + } + } + + @Test + public void reportLambdaErrorWithXRayTest() { + try { + RapidErrorType rapidErrorType = RapidErrorType.AfterRestoreError; + + MockResponse mockResponse = new MockResponse(); + mockResponse.setResponseCode(HTTP_ACCEPTED); + mockWebServer.enqueue(mockResponse); + + String workingDirectory = "my-test-directory"; + List paths = new ArrayList(); + paths.add("path-0"); + paths.add("path-1"); + paths.add("path-2"); + + List stackElements0 = new ArrayList<>(); + stackElements0.add(new StackElement("label0", "path0", 0)); + stackElements0.add(new StackElement("label1", "path1", 1)); + stackElements0.add(new StackElement("label2", "path2", 2)); + XRayException xRayException0 = new XRayException("my-test-message0", "my-test-type0", stackElements0); + + List stackElements1 = new ArrayList<>(); + stackElements1.add(new StackElement("label10", "path10", 0)); + stackElements1.add(new StackElement("label11", "path11", 11)); + stackElements1.add(new StackElement("label12", "path12", 12)); + XRayException xRayException1 = new XRayException("my-test-message1", "my-test-type0", stackElements1); + + List exceptions = new ArrayList<>(); + exceptions.add(xRayException0); + exceptions.add(xRayException1); + + XRayErrorCause xRayErrorCause = new XRayErrorCause(workingDirectory, exceptions, paths); + LambdaError lambdaError = new LambdaError(errorRequest, xRayErrorCause, rapidErrorType); + lambdaRuntimeApiClientImpl.reportInvocationError(requestId, lambdaError, null); + RecordedRequest recordedRequest = mockWebServer.takeRequest(); + + String xrayErrorCauseHeader = recordedRequest.getHeader("Lambda-Runtime-Function-XRay-Error-Cause"); + assertEquals("{\"working_directory\":\"my-test-directory\",\"exceptions\":[{\"message\":\"my-test-message0\",\"type\":\"my-test-type0\",\"stack\":[{\"label\":\"label0\"," + + "\"path\":\"path0\",\"line\":0},{\"label\":\"label1\",\"path\":\"path1\",\"line\":1},{\"label\":\"label2\",\"path\":\"path2\",\"line\":2}]},{\"message\":\"my-test-message1\"," + + "\"type\":\"my-test-type0\",\"stack\":[{\"label\":\"label10\",\"path\":\"path10\",\"line\":0},{\"label\":\"label11\",\"path\":\"path11\",\"line\":11},{\"label\":\"label12\"," + + "\"path\":\"path12\",\"line\":12}]}],\"paths\":[\"path-0\",\"path-1\",\"path-2\"]}", xrayErrorCauseHeader); + } catch(Exception e) { + fail(); + } + } + + @Test + public void reportInvocationSuccessTest() { + try { + MockResponse mockResponse = new MockResponse(); + mockResponse.setResponseCode(HTTP_ACCEPTED); + mockWebServer.enqueue(mockResponse); + + String response = "{\"msg\":\"test\"}"; + lambdaRuntimeApiClientImpl.reportInvocationSuccess(requestId, response.getBytes(), null); + RecordedRequest recordedRequest = mockWebServer.takeRequest(); + HttpUrl actualUrl = recordedRequest.getRequestUrl(); + String expectedUrl = "http://" + getHostnamePort() + "/2018-06-01/runtime/invocation/1234/response"; + assertEquals(expectedUrl, actualUrl.toString()); + + String actualBody = recordedRequest.getBody().readUtf8(); + assertEquals("{\"msg\":\"test\"}", actualBody); + } catch(Exception e) { + e.printStackTrace(); + fail(); + } + } + + @Test + public void restoreNextTest() { + try { + MockResponse mockResponse = new MockResponse(); + mockResponse.setResponseCode(HTTP_OK); + mockWebServer.enqueue(mockResponse); + + lambdaRuntimeApiClientImpl.restoreNext(); + RecordedRequest recordedRequest = mockWebServer.takeRequest(); + HttpUrl actualUrl = recordedRequest.getRequestUrl(); + String expectedUrl = "http://" + getHostnamePort() + "/2018-06-01/runtime/restore/next"; + assertEquals(expectedUrl, actualUrl.toString()); + + String actualBody = recordedRequest.getBody().readUtf8(); + assertEquals("", actualBody); + } catch(Exception e) { + e.printStackTrace(); + fail(); + } + } + + @Test + public void restoreNextWrongStatusCodeTest() { + int errorStatusCode = HTTP_INTERNAL_ERROR; + try { + MockResponse mockResponse = new MockResponse(); + mockResponse.setResponseCode(errorStatusCode); + mockWebServer.enqueue(mockResponse); + + lambdaRuntimeApiClientImpl.restoreNext(); + fail(); + } catch(LambdaRuntimeClientException e) { + String expectedUrl = "http://" + getHostnamePort() + "/2018-06-01/runtime/restore/next"; + String expectedMessage = expectedUrl + " Response code: '" + errorStatusCode + "'."; + assertEquals(expectedMessage, e.getLocalizedMessage()); + } catch(Exception e) { + fail(); + } + } + + @Test + public void nextWithoutTenantIdHeaderTest() { + try { + MockResponse mockResponse = buildMockResponseForNextInvocation(); + mockWebServer.enqueue(mockResponse); + + InvocationRequest invocationRequest = lambdaRuntimeApiClientImpl.nextInvocation(); + verifyNextInvocationRequest(); + assertNull(invocationRequest.getTenantId()); + } catch(Exception e) { + fail(); + } + } + + @Test + public void nextWithTenantIdHeaderTest() { + try { + MockResponse mockResponse = buildMockResponseForNextInvocation(); + String expectedTenantId = "my-tenant-id"; + mockResponse.setHeader("lambda-runtime-aws-tenant-id", expectedTenantId); + mockWebServer.enqueue(mockResponse); + + InvocationRequest invocationRequest = lambdaRuntimeApiClientImpl.nextInvocation(); + verifyNextInvocationRequest(); + assertEquals(expectedTenantId, invocationRequest.getTenantId()); + + } catch(Exception e) { + fail(); + } + } + + @Test + public void nextWithEmptyTenantIdHeaderTest() { + try { + MockResponse mockResponse = buildMockResponseForNextInvocation(); + mockResponse.setHeader("lambda-runtime-aws-tenant-id", ""); + mockWebServer.enqueue(mockResponse); + + InvocationRequest invocationRequest = lambdaRuntimeApiClientImpl.nextInvocation(); + verifyNextInvocationRequest(); + assertNull(invocationRequest.getTenantId()); + } catch(Exception e) { + fail(); + } + } + + @Test + public void nextWithNullTenantIdHeaderTest() { + try { + MockResponse mockResponse = buildMockResponseForNextInvocation(); + assertThrows(NullPointerException.class, () -> { + mockResponse.setHeader("lambda-runtime-aws-tenant-id", null); + }); + } catch(Exception e) { + fail(); + } + } + + @Test + public void createUrlMalformedTest() { + RapidErrorType rapidErrorType = RapidErrorType.AfterRestoreError; + LambdaError lambdaError = new LambdaError(errorRequest, rapidErrorType); + RuntimeException thrown = assertThrows(RuntimeException.class, ()->{ + lambdaRuntimeApiClientImpl.reportLambdaError("invalidurl", lambdaError, 100, null); + }); + assertTrue(thrown.getLocalizedMessage().contains("java.net.MalformedURLException")); + } + + @Test + public void lambdaReportErrorXRayHeaderTooLongTest() { + try { + RapidErrorType rapidErrorType = RapidErrorType.AfterRestoreError; + + MockResponse mockResponse = new MockResponse(); + mockResponse.setResponseCode(HTTP_ACCEPTED); + mockWebServer.enqueue(mockResponse); + + String workingDirectory = "my-test-directory"; + List paths = new ArrayList(); + paths.add("path-0"); + + List stackElements = new ArrayList<>(); + stackElements.add(new StackElement("label0", "path0", 0)); + XRayException xRayException = new XRayException("my-test-message0", "my-test-type0", stackElements); + + List exceptions = new ArrayList<>(); + exceptions.add(xRayException); + + XRayErrorCause xRayErrorCause = new XRayErrorCause(workingDirectory, exceptions, paths); + LambdaError lambdaError = new LambdaError(errorRequest, xRayErrorCause, rapidErrorType); + lambdaRuntimeApiClientImpl.reportLambdaError("http://" + getHostnamePort(), lambdaError, 10, null); + RecordedRequest recordedRequest = mockWebServer.takeRequest(); + + String xrayErrorCauseHeader = recordedRequest.getHeader("Lambda-Runtime-Function-XRay-Error-Cause"); + assertNull(xrayErrorCauseHeader); + } catch(Exception e) { + fail(); + } + } + + private MockResponse buildMockResponseForNextInvocation() { + MockResponse mockResponse = new MockResponse(); + mockResponse.setResponseCode(HTTP_ACCEPTED); + mockResponse.setHeader("lambda-runtime-aws-request-id", "1234567890"); + mockResponse.setHeader("Content-Type", "application/json"); + return mockResponse; + } + + private void verifyNextInvocationRequest() throws Exception { + RecordedRequest recordedRequest = mockWebServer.takeRequest(); + HttpUrl actualUrl = recordedRequest.getRequestUrl(); + String expectedUrl = "http://" + getHostnamePort() + "/2018-06-01/runtime/invocation/next"; + assertEquals(expectedUrl, actualUrl.toString()); + + String actualBody = recordedRequest.getBody().readUtf8(); + assertEquals("", actualBody); + } + + @Test + public void reportInvocationErrorWithInvocationIdTest() { + try { + RapidErrorType rapidErrorType = RapidErrorType.AfterRestoreError; + + MockResponse mockResponse = new MockResponse(); + mockResponse.setResponseCode(HTTP_ACCEPTED); + mockWebServer.enqueue(mockResponse); + + String invocationId = "test-invocation-uuid-1234"; + LambdaError lambdaError = new LambdaError(errorRequest, rapidErrorType); + lambdaRuntimeApiClientImpl.reportInvocationError(requestId, lambdaError, invocationId); + RecordedRequest recordedRequest = mockWebServer.takeRequest(); + + String invocationIdHeader = recordedRequest.getHeader("Lambda-Runtime-Invocation-Id"); + assertEquals(invocationId, invocationIdHeader); + } catch(Exception e) { + e.printStackTrace(); + fail(); + } + } + + private String getHostnamePort() { + return mockWebServer.getHostName() + ":" + mockWebServer.getPort(); + } +} \ No newline at end of file diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/converters/LambdaErrorConverterTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/converters/LambdaErrorConverterTest.java new file mode 100644 index 000000000..f94bc0c5f --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/runtimeapi/converters/LambdaErrorConverterTest.java @@ -0,0 +1,112 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +package com.amazonaws.services.lambda.runtime.api.client.runtimeapi.converters; + +import com.amazonaws.services.lambda.runtime.api.client.UserFault; +import com.amazonaws.services.lambda.runtime.api.client.runtimeapi.dto.ErrorRequest; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +class LambdaErrorConverterTest { + + @Test + void testFromUserFaultWithMessageAndException() { + UserFault userFault = new UserFault("Test error message", "TestException", "Test stack trace"); + ErrorRequest errorRequest = LambdaErrorConverter.fromUserFault(userFault); + + assertNotNull(errorRequest); + assertEquals("Test error message", errorRequest.errorMessage); + assertEquals("TestException", errorRequest.errorType); + assertNull(errorRequest.stackTrace); + } + + @Test + void testFromUserFaultWithNullValues() { + UserFault userFault = new UserFault(null, null, null); + ErrorRequest errorRequest = LambdaErrorConverter.fromUserFault(userFault); + + assertNotNull(errorRequest); + assertNull(errorRequest.errorMessage); + assertNull(errorRequest.errorType); + assertNull(errorRequest.stackTrace); + } + + @Test + void testFromUserFaultWithFatalError() { + UserFault userFault = new UserFault("Fatal error", "FatalException", "Test stack trace", true); + ErrorRequest errorRequest = LambdaErrorConverter.fromUserFault(userFault); + + assertNotNull(errorRequest); + assertEquals("Fatal error", errorRequest.errorMessage); + assertEquals("FatalException", errorRequest.errorType); + assertNull(errorRequest.stackTrace); + } + + @Test + void testFromUserFaultCreatedFromException() { + Exception exception = new RuntimeException("Test exception message"); + UserFault userFault = UserFault.makeUserFault(exception); + ErrorRequest errorRequest = LambdaErrorConverter.fromUserFault(userFault); + + assertNotNull(errorRequest); + assertEquals("Test exception message", errorRequest.errorMessage); + assertEquals("java.lang.RuntimeException", errorRequest.errorType); + assertNull(errorRequest.stackTrace); + } + + @Test + void testFromUserFaultCreatedFromMessage() { + UserFault userFault = UserFault.makeUserFault("Simple message"); + ErrorRequest errorRequest = LambdaErrorConverter.fromUserFault(userFault); + + assertNotNull(errorRequest); + assertEquals("Simple message", errorRequest.errorMessage); + assertNull(errorRequest.errorType); + assertNull(errorRequest.stackTrace); + } + + @Test + void testFromThrowableWithMessage() { + Exception exception = new RuntimeException("Test exception message"); + ErrorRequest errorRequest = LambdaErrorConverter.fromThrowable(exception); + + assertNotNull(errorRequest); + assertEquals("Test exception message", errorRequest.errorMessage); + assertEquals("java.lang.RuntimeException", errorRequest.errorType); + assertNotNull(errorRequest.stackTrace); + assertTrue(errorRequest.stackTrace.length > 0); + } + + @Test + void testFromThrowableWithNullMessage() { + Exception exception = new RuntimeException(); + ErrorRequest errorRequest = LambdaErrorConverter.fromThrowable(exception); + + assertNotNull(errorRequest); + assertEquals("java.lang.RuntimeException", errorRequest.errorMessage); + assertEquals("java.lang.RuntimeException", errorRequest.errorType); + assertNotNull(errorRequest.stackTrace); + assertTrue(errorRequest.stackTrace.length > 0); + } + + @Test + void testFromThrowableStackTraceContent() { + Exception exception = new RuntimeException("Test message"); + ErrorRequest errorRequest = LambdaErrorConverter.fromThrowable(exception); + + String[] stackTrace = errorRequest.stackTrace; + assertNotNull(stackTrace); + assertTrue(stackTrace.length > 0); + + boolean foundTestClass = false; + for (String traceLine : stackTrace) { + if (traceLine.contains(LambdaErrorConverterTest.class.getSimpleName())) { + foundTestClass = true; + break; + } + } + assertTrue(foundTestClass); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/util/ConcurrencyConfigTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/util/ConcurrencyConfigTest.java new file mode 100644 index 000000000..b1284e90c --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/util/ConcurrencyConfigTest.java @@ -0,0 +1,90 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client.util; + +import com.amazonaws.services.lambda.runtime.api.client.ReservedRuntimeEnvironmentVariables; +import com.amazonaws.services.lambda.runtime.api.client.logging.LambdaContextLogger; +import com.amazonaws.services.lambda.runtime.logging.LogFormat; +import com.amazonaws.services.lambda.runtime.logging.LogLevel; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.Assert.assertThrows; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.contains; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class ConcurrencyConfigTest { + @Mock + private LambdaContextLogger lambdaLogger; + + @Mock + private EnvReader envReader; + + private static final String exitingRuntimeString = String.format("User configured %s is invalid.", ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_MAX_CONCURRENCY); + + @Test + void testDefaultConfiguration() { + when(envReader.getEnv(ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_MAX_CONCURRENCY)).thenReturn(null); + + ConcurrencyConfig config = new ConcurrencyConfig(lambdaLogger, envReader); + verifyNoInteractions(lambdaLogger); + assertEquals(0, config.getNumberOfPlatformThreads()); + assertEquals(false, config.isMultiConcurrent()); + } + + @Test + void testMinValidPlatformThreadsConfig() { + when(envReader.getEnv(ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_MAX_CONCURRENCY)).thenReturn("1"); + + ConcurrencyConfig config = new ConcurrencyConfig(lambdaLogger, envReader); + verifyNoInteractions(lambdaLogger); + assertEquals(1, config.getNumberOfPlatformThreads()); + assertEquals(true, config.isMultiConcurrent()); + } + + @Test + void testValidPlatformThreadsConfig() { + when(envReader.getEnv(ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_MAX_CONCURRENCY)).thenReturn("4"); + + ConcurrencyConfig config = new ConcurrencyConfig(lambdaLogger, envReader); + verifyNoInteractions(lambdaLogger); + assertEquals(4, config.getNumberOfPlatformThreads()); + assertEquals(true, config.isMultiConcurrent()); + } + + @Test + void testInvalidPlatformThreadsConfig() { + when(lambdaLogger.getLogFormat()).thenReturn(LogFormat.JSON); + when(envReader.getEnv(ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_MAX_CONCURRENCY)).thenReturn("invalid"); + + assertThrows(NumberFormatException.class, () -> new ConcurrencyConfig(lambdaLogger, envReader)); + verify(lambdaLogger).log(contains(exitingRuntimeString), eq(LogLevel.ERROR)); + } + + @Test + void testGetConcurrencyConfigMessage() { + when(envReader.getEnv(ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_MAX_CONCURRENCY)).thenReturn("4"); + + ConcurrencyConfig config = new ConcurrencyConfig(lambdaLogger, envReader); + String expectedMessage = "Starting 4 concurrent function handler threads."; + verifyNoInteractions(lambdaLogger); + assertEquals(expectedMessage, config.getConcurrencyConfigMessage()); + assertEquals(true, config.isMultiConcurrent()); + } + + @Test + void testGetConcurrencyConfigWithNoConcurrency() { + ConcurrencyConfig config = new ConcurrencyConfig(lambdaLogger, envReader); + verifyNoInteractions(lambdaLogger); + assertEquals(0, config.getNumberOfPlatformThreads()); + assertEquals(false, config.isMultiConcurrent()); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/util/LambdaOutputStreamTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/util/LambdaOutputStreamTest.java new file mode 100644 index 000000000..30146ea84 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/util/LambdaOutputStreamTest.java @@ -0,0 +1,81 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client.util; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.io.IOException; +import java.io.OutputStream; + +import static org.mockito.Mockito.*; +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(MockitoExtension.class) +public class LambdaOutputStreamTest { + + @Mock + private OutputStream mockInnerStream; + + private LambdaOutputStream lambdaOutputStream; + + @BeforeEach + void setUp() { + lambdaOutputStream = new LambdaOutputStream(mockInnerStream); + } + + @Test + void writeSingleByte() throws IOException { + int testByte = 65; // 'A' + lambdaOutputStream.write(testByte); + verify(mockInnerStream).write(new byte[]{(byte) testByte}, 0, 1); + } + + @Test + void writeByteArray() throws IOException { + byte[] testBytes = "test".getBytes(); + lambdaOutputStream.write(testBytes); + verify(mockInnerStream).write(testBytes, 0, testBytes.length); + } + + @Test + void writeOffsetLength() throws IOException { + byte[] testBytes = "test".getBytes(); + int offset = 1; + int length = 2; + lambdaOutputStream.write(testBytes, offset, length); + verify(mockInnerStream).write(testBytes, offset, length); + } + + @Test + void throwWriteSingleByte() throws IOException { + doThrow(new IOException("Test exception")) + .when(mockInnerStream) + .write(any(byte[].class), anyInt(), anyInt()); + assertThrows(IOException.class, () -> lambdaOutputStream.write(65)); + } + + @Test + void throwWriteByteArray() throws IOException { + byte[] testBytes = "test".getBytes(); + doThrow(new IOException("Test exception")) + .when(mockInnerStream) + .write(any(byte[].class), anyInt(), anyInt()); + assertThrows(IOException.class, () -> lambdaOutputStream.write(testBytes)); + } + + @Test + void throwWriteOffsetLength() throws IOException { + byte[] testBytes = "test".getBytes(); + doThrow(new IOException("Test exception")) + .when(mockInnerStream) + .write(any(byte[].class), anyInt(), anyInt()); + assertThrows(IOException.class, () -> lambdaOutputStream.write(testBytes, 1, 2)); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/util/UnsafeUtilTest.java b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/util/UnsafeUtilTest.java new file mode 100644 index 000000000..b1f0592f0 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/com/amazonaws/services/lambda/runtime/api/client/util/UnsafeUtilTest.java @@ -0,0 +1,56 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.amazonaws.services.lambda.runtime.api.client.util; + +import org.junit.jupiter.api.Test; +import java.lang.reflect.Field; +import static org.junit.jupiter.api.Assertions.*; + +public class UnsafeUtilTest { + + @Test + void testTheUnsafeIsInitialized() { + assertNotNull(UnsafeUtil.TheUnsafe); + } + + @Test + void testThrowException() { + Exception testException = new Exception("Test exception"); + + try { + UnsafeUtil.throwException(testException); + fail("Should have thrown an exception"); + } catch (Throwable e) { + assertEquals("Test exception", e.getMessage()); + assertSame(testException, e); + } + } + + @Test + void testDisableIllegalAccessWarning() { + assertDoesNotThrow(() -> UnsafeUtil.disableIllegalAccessWarning()); + try { + Class illegalAccessLoggerClass = Class.forName("jdk.internal.module.IllegalAccessLogger"); + Field loggerField = illegalAccessLoggerClass.getDeclaredField("logger"); + loggerField.setAccessible(true); + Object loggerValue = loggerField.get(null); + assertNull(loggerValue); + } catch (ClassNotFoundException e) { + assertTrue(true); + } catch (NoSuchFieldException e) { + assertTrue(true); + } catch (Exception e) { + fail("Unexpected exception: " + e.getMessage()); + } + } + + @Test + void testPrivateConstructor() { + assertThrows(IllegalAccessException.class, () -> { + UnsafeUtil.class.getDeclaredConstructor().newInstance(); + }); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/test/lambda/handlers/POJOHanlderImpl.java b/aws-lambda-java-runtime-interface-client/src/test/java/test/lambda/handlers/POJOHanlderImpl.java new file mode 100644 index 000000000..ca1a6bd4f --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/test/lambda/handlers/POJOHanlderImpl.java @@ -0,0 +1,26 @@ +package test.lambda.handlers; + +import com.amazonaws.services.lambda.runtime.Context; + +@SuppressWarnings("unused") +public class POJOHanlderImpl { + @SuppressWarnings("unused") + public String noParamsHandler() { + return "success"; + } + + @SuppressWarnings("unused") + public String oneParamHandler_event(String event) { + return "success"; + } + + @SuppressWarnings("unused") + public String oneParamHandler_context(Context context) { + return "success"; + } + + @SuppressWarnings("unused") + public String twoParamsHandler(String event, Context context) { + return "success"; + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/test/lambda/handlers/RequestHandlerImpl.java b/aws-lambda-java-runtime-interface-client/src/test/java/test/lambda/handlers/RequestHandlerImpl.java new file mode 100644 index 000000000..47fbade4d --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/test/lambda/handlers/RequestHandlerImpl.java @@ -0,0 +1,12 @@ +package test.lambda.handlers; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; + + +public class RequestHandlerImpl implements RequestHandler { + @Override + public String handleRequest(String event, Context context) { + return "success"; + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/test/lambda/handlers/RequestStreamHandlerImpl.java b/aws-lambda-java-runtime-interface-client/src/test/java/test/lambda/handlers/RequestStreamHandlerImpl.java new file mode 100644 index 000000000..2bf2212c1 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/test/lambda/handlers/RequestStreamHandlerImpl.java @@ -0,0 +1,16 @@ +package test.lambda.handlers; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestStreamHandler; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +@SuppressWarnings("unused") +public class RequestStreamHandlerImpl implements RequestStreamHandler { + @Override + public void handleRequest(InputStream input, OutputStream output, Context context) throws IOException { + output.write("\"success\"".getBytes()); + } +} diff --git a/aws-lambda-java-runtime-interface-client/src/test/java/testpkg/StackTraceHelper.java b/aws-lambda-java-runtime-interface-client/src/test/java/testpkg/StackTraceHelper.java new file mode 100644 index 000000000..c7d8cb834 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/src/test/java/testpkg/StackTraceHelper.java @@ -0,0 +1,33 @@ +/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ + +package testpkg; + +import com.amazonaws.services.lambda.crac.CheckpointException; + +/** + * A helper class for throwing exception which is not in the com.amazonaws.services.lambda.runtime.api.client package + * to avoid the stack traces from being filtered out. + * + */ +public class StackTraceHelper { + /** + * Throws a RuntimeException directly with msg as the message. + */ + public static void throwRuntimeException(String msg){ + throw new RuntimeException(msg); + } + + /** + * Calls another method which throws a RuntimeException with msg as the message. + */ + public static void callThenThrowRuntimeException(String msg){ + throwRuntimeException(msg); + } + + public static void throwCheckpointExceptionWithTwoSuppressedExceptions(String msg1, String msg2) throws CheckpointException { + CheckpointException e1 = new CheckpointException(); + e1.addSuppressed(new RuntimeException(msg1)); + e1.addSuppressed(new RuntimeException(msg2)); + throw e1; + } +} diff --git a/aws-lambda-java-runtime-interface-client/test-handlers/EchoHandler.java b/aws-lambda-java-runtime-interface-client/test-handlers/EchoHandler.java new file mode 100644 index 000000000..cb324e7f7 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/test-handlers/EchoHandler.java @@ -0,0 +1,20 @@ +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import java.util.Map; +import java.util.HashMap; + +public class EchoHandler implements RequestHandler, Map> { + + @Override + public Map handleRequest(Map event, Context context) { + context.getLogger().log("Processing event: " + event); + + Map response = new HashMap<>(event); + response.put("timestamp", System.currentTimeMillis()); + response.put("requestId", context.getAwsRequestId()); + response.put("functionName", context.getFunctionName()); + response.put("remainingTimeInMillis", context.getRemainingTimeInMillis()); + + return response; + } +} \ No newline at end of file diff --git a/aws-lambda-java-runtime-interface-client/test/integration/.gitignore b/aws-lambda-java-runtime-interface-client/test/integration/.gitignore new file mode 100644 index 000000000..2c52883fe --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/test/integration/.gitignore @@ -0,0 +1,2 @@ +generated.docker-compose.*.yml +.idea \ No newline at end of file diff --git a/aws-lambda-java-runtime-interface-client/test/integration/codebuild-local/Dockerfile.agent b/aws-lambda-java-runtime-interface-client/test/integration/codebuild-local/Dockerfile.agent new file mode 100644 index 000000000..b077df89d --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/test/integration/codebuild-local/Dockerfile.agent @@ -0,0 +1,31 @@ +# syntax=docker/dockerfile:1.4 +FROM public.ecr.aws/amazoncorretto/amazoncorretto:8 + +# Install docker and buildx extension. +# gnupg2 provides gpg/gpgv/gpg-agent so build-jni-lib.sh can GPG-verify the prebuilt assets. +RUN yum install -y --allowerasing docker tar gzip unzip file findutils gnupg2 + +COPY --from=docker/buildx-bin:latest /buildx /usr/libexec/docker/cli-plugins/docker-buildx + +# Install maven from apache.org, as version in the yum repository doesn't support latest maven plugins +ENV PATH="$PATH:/apache-maven/bin" +RUN mkdir /apache-maven && \ + curl https://archive.apache.org/dist/maven/maven-3/3.8.7/binaries/apache-maven-3.8.7-bin.tar.gz | \ + tar -xz -C /apache-maven --strip-components 1 + +# Declare JDK 8 in toolchains.xml so maven-toolchains-plugin can resolve it +RUN mkdir -p /root/.m2 && \ + cat > /root/.m2/toolchains.xml < + + + jdk + + 8 + + + ${JAVA_HOME} + + + +EOF diff --git a/aws-lambda-java-runtime-interface-client/test/integration/codebuild-local/codebuild_build.sh b/aws-lambda-java-runtime-interface-client/test/integration/codebuild-local/codebuild_build.sh new file mode 100755 index 000000000..2a6ffa972 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/test/integration/codebuild-local/codebuild_build.sh @@ -0,0 +1,206 @@ +#!/bin/bash +# This file is copied from https://github.com/aws/aws-codebuild-docker-images/blob/282c6634e8c83c2a9841719b09aabfced3461981/local_builds/codebuild_build.sh + +function allOSRealPath() { + if isOSWindows + then + path="" + case $1 in + .* ) path="$PWD/${1#./}" ;; + /* ) path="$1" ;; + * ) path="/$1" ;; + esac + + echo "/$path" | sed -e 's/\\/\//g' -e 's/://' -e 's/./\U&/3' + else + case $1 in + /* ) echo "$1"; exit;; + * ) echo "$PWD/${1#./}"; exit;; + esac + fi +} + +function isOSWindows() { + if [ $OSTYPE == "msys" ] + then + return 0 + else + return 1 + fi +} + +function usage { + echo "usage: codebuild_build.sh [-i image_name] [-a artifact_output_directory] [options]" + echo "Required:" + echo " -i Used to specify the customer build container image." + echo " -a Used to specify an artifact output directory." + echo "Options:" + echo " -l IMAGE Used to override the default local agent image." + echo " -r Used to specify a report output directory." + echo " -s Used to specify source information. Defaults to the current working directory for primary source." + echo " * First (-s) is for primary source" + echo " * Use additional (-s) in : format for secondary source" + echo " * For sourceIdentifier, use a value that is fewer than 128 characters and contains only alphanumeric characters and underscores" + echo " -c Use the AWS configuration and credentials from your local host. This includes ~/.aws and any AWS_* environment variables." + echo " -p Used to specify the AWS CLI Profile." + echo " -b FILE Used to specify a buildspec override file. Defaults to buildspec.yml in the source directory." + echo " -m Used to mount the source directory to the customer build container directly." + echo " -d Used to run the build container in docker privileged mode." + echo " -e FILE Used to specify a file containing environment variables." + echo " (-e) File format expectations:" + echo " * Each line is in VAR=VAL format" + echo " * Lines beginning with # are processed as comments and ignored" + echo " * Blank lines are ignored" + echo " * File can be of type .env or .txt" + echo " * There is no special handling of quotation marks, meaning they will be part of the VAL" + exit 1 +} + +image_flag=false +artifact_flag=false +awsconfig_flag=false +mount_src_dir_flag=false +docker_privileged_mode_flag=false + +while getopts "cmdi:a:r:s:b:e:l:p:h" opt; do + case $opt in + i ) image_flag=true; image_name=$OPTARG;; + a ) artifact_flag=true; artifact_dir=$OPTARG;; + r ) report_dir=$OPTARG;; + b ) buildspec=$OPTARG;; + c ) awsconfig_flag=true;; + m ) mount_src_dir_flag=true;; + d ) docker_privileged_mode_flag=true;; + s ) source_dirs+=("$OPTARG");; + e ) environment_variable_file=$OPTARG;; + l ) local_agent_image=$OPTARG;; + p ) aws_profile=$OPTARG;; + h ) usage; exit;; + \? ) echo "Unknown option: -$OPTARG" >&2; exit 1;; + : ) echo "Missing option argument for -$OPTARG" >&2; exit 1;; + * ) echo "Invalid option: -$OPTARG" >&2; exit 1;; + esac +done + +if ! $image_flag +then + echo "The image name flag (-i) must be included for a build to run" >&2 +fi + +if ! $artifact_flag +then + echo "The artifact directory (-a) must be included for a build to run" >&2 +fi + +if ! $image_flag || ! $artifact_flag +then + exit 1 +fi + +docker_command="docker run " +if isOSWindows +then + docker_command+="-v //var/run/docker.sock:/var/run/docker.sock -e " +else + docker_command+="-v /var/run/docker.sock:/var/run/docker.sock -e " +fi + +docker_command+="\"IMAGE_NAME=$image_name\" -e \ + \"ARTIFACTS=$(allOSRealPath "$artifact_dir")\"" + +if [ -n "$report_dir" ] +then + docker_command+=" -e \"REPORTS=$(allOSRealPath "$report_dir")\"" +fi + +if [ -z "$source_dirs" ] +then + docker_command+=" -e \"SOURCE=$(allOSRealPath "$PWD")\"" +else + for index in "${!source_dirs[@]}"; do + if [ $index -eq 0 ] + then + docker_command+=" -e \"SOURCE=$(allOSRealPath "${source_dirs[$index]}")\"" + else + identifier=${source_dirs[$index]%%:*} + src_dir=$(allOSRealPath "${source_dirs[$index]#*:}") + + docker_command+=" -e \"SECONDARY_SOURCE_$index=$identifier:$src_dir\"" + fi + done +fi + +if [ -n "$buildspec" ] +then + docker_command+=" -e \"BUILDSPEC=$(allOSRealPath "$buildspec")\"" +fi + +if [ -n "$environment_variable_file" ] +then + environment_variable_file_path=$(allOSRealPath "$environment_variable_file") + environment_variable_file_dir=$(dirname "$environment_variable_file_path") + environment_variable_file_basename=$(basename "$environment_variable_file") + docker_command+=" -v \"$environment_variable_file_dir:/LocalBuild/envFile/\" -e \"ENV_VAR_FILE=$environment_variable_file_basename\"" +fi + +if [ -n "$local_agent_image" ] +then + docker_command+=" -e \"LOCAL_AGENT_IMAGE_NAME=$local_agent_image\"" +fi + +if $awsconfig_flag +then + if [ -d "$HOME/.aws" ] + then + configuration_file_path=$(allOSRealPath "$HOME/.aws") + docker_command+=" -e \"AWS_CONFIGURATION=$configuration_file_path\"" + else + docker_command+=" -e \"AWS_CONFIGURATION=NONE\"" + fi + + if [ -n "$aws_profile" ] + then + docker_command+=" -e \"AWS_PROFILE=$aws_profile\"" + fi + + docker_command+="$(env | grep ^AWS_ | while read -r line; do echo " -e \"$line\""; done )" +fi + +if $mount_src_dir_flag +then + docker_command+=" -e \"MOUNT_SOURCE_DIRECTORY=TRUE\"" +fi + +if $docker_privileged_mode_flag +then + docker_command+=" -e \"DOCKER_PRIVILEGED_MODE=TRUE\"" +fi + +if isOSWindows +then + docker_command+=" -e \"INITIATOR=$USERNAME\"" +else + docker_command+=" -e \"INITIATOR=$USER\"" +fi + +if [ -n "$local_agent_image" ] +then + docker_command+=" $local_agent_image" +else + docker_command+=" public.ecr.aws/codebuild/local-builds:latest" +fi + +# Note we do not expose the AWS_SECRET_ACCESS_KEY or the AWS_SESSION_TOKEN +exposed_command=$docker_command +secure_variables=( "AWS_SECRET_ACCESS_KEY=" "AWS_SESSION_TOKEN=") +for variable in "${secure_variables[@]}" +do + exposed_command="$(echo $exposed_command | sed "s/\($variable\)[^ ]*/\1********\"/")" +done + +echo "Build Command:" +echo "" +echo $exposed_command +echo "" + +eval $docker_command diff --git a/aws-lambda-java-runtime-interface-client/test/integration/codebuild-local/docker-retry.sh b/aws-lambda-java-runtime-interface-client/test/integration/codebuild-local/docker-retry.sh new file mode 100755 index 000000000..5f03cad59 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/test/integration/codebuild-local/docker-retry.sh @@ -0,0 +1,35 @@ +#!/bin/bash +# Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +set -uo pipefail + +MAX_ATTEMPTS="${RETRY_MAX_ATTEMPTS:-5}" +BASE_DELAY="${RETRY_BASE_DELAY:-5}" +MAX_DELAY="${RETRY_MAX_DELAY:-60}" + +if (( $# == 0 )); then + >&2 echo "usage: docker-retry.sh [args...]" + exit 2 +fi + +attempt=1 +while true; do + "$@" && exit 0 + status=$? + + if (( attempt >= MAX_ATTEMPTS )); then + >&2 echo "docker-retry: '$*' failed after ${attempt} attempt(s) (exit ${status}); giving up." + exit "$status" + fi + + # Exponential backoff: BASE_DELAY * 2^(attempt-1), capped at MAX_DELAY. + backoff=$(( BASE_DELAY * (2 ** (attempt - 1)) )) + (( backoff > MAX_DELAY )) && backoff=$MAX_DELAY + # Full jitter: wait a random duration in [0, backoff] so concurrent jobs + # spread out instead of retrying at the same moment. + delay=$(( RANDOM % (backoff + 1) )) + + >&2 echo "docker-retry: '$*' failed (exit ${status}); attempt ${attempt}/${MAX_ATTEMPTS}, retrying in ${delay}s." + sleep "$delay" + attempt=$(( attempt + 1 )) +done diff --git a/aws-lambda-java-runtime-interface-client/test/integration/codebuild-local/test_all.sh b/aws-lambda-java-runtime-interface-client/test/integration/codebuild-local/test_all.sh new file mode 100755 index 000000000..cedda98bf --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/test/integration/codebuild-local/test_all.sh @@ -0,0 +1,88 @@ +#!/bin/bash +# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +set -euo pipefail + +CODEBUILD_IMAGE_TAG="${CODEBUILD_IMAGE_TAG:-al2/x86_64/standard/3.0}" +DRYRUN="${DRYRUN-0}" +# When set, only the matching platform from each buildspec is run. Used by CI to +# run each architecture on its own native runner instead of emulating via QEMU. +PLATFORM_FILTER="${PLATFORM_FILTER-}" + +function usage { + echo "usage: test_all.sh buildspec_yml_dir_or_file" + echo "Runs all buildspec build-matrix combinations via test_one.sh." + echo "Required:" + echo " buildspec_yml_dir_or_file A directory of buildspec templates (runs every *.yml)," + echo " or a single buildspec .yml file." +} + +do_one_yaml() { + local -r YML="$1" + + OS_DISTRIBUTION=$(grep -oE 'OS_DISTRIBUTION:\s*(\S+)' "$YML" | cut -d' ' -f2) + DISTRO_VERSIONS=$(sed '1,/DISTRO_VERSION/d;/RUNTIME_VERSION/,$d' "$YML" | tr -d '\-" ') + RUNTIME_VERSIONS=$(sed '1,/RUNTIME_VERSION/d;/PLATFORM/,$d' "$YML" | sed '/#.*$/d' | tr -d '\-" ') + PLATFORMS=$(sed '1,/PLATFORM/d;/phases/,$d' "$YML" | tr -d '\-" ') + + if [ -n "$PLATFORM_FILTER" ]; then + PLATFORMS="$PLATFORM_FILTER" + fi + + for DISTRO_VERSION in $DISTRO_VERSIONS; do + for RUNTIME_VERSION in $RUNTIME_VERSIONS; do + for PLATFORM in $PLATFORMS; do + if (( DRYRUN == 1 )); then + echo DRYRUN test_one_combination "$YML" "$OS_DISTRIBUTION" "$DISTRO_VERSION" "$RUNTIME_VERSION" "$PLATFORM" + else + test_one_combination "$YML" "$OS_DISTRIBUTION" "$DISTRO_VERSION" "$RUNTIME_VERSION" "$PLATFORM" + fi + done + done + done +} + +test_one_combination() { + local -r YML="$1" + local -r OS_DISTRIBUTION="$2" + local -r DISTRO_VERSION="$3" + local -r RUNTIME_VERSION="$4" + local -r PLATFORM="$5" + local -r PLATFORM_SANITIZED=$(echo "$PLATFORM" | tr "/" ".") + + echo Testing: + echo " BUILDSPEC" "$YML" + echo " with" "$OS_DISTRIBUTION"-"$DISTRO_VERSION" "$RUNTIME_VERSION" "$PLATFORM" + + "$(dirname "$0")"/test_one.sh "$YML" "$OS_DISTRIBUTION" "$DISTRO_VERSION" "$RUNTIME_VERSION" "$PLATFORM" \ + > >(sed "s/^/$OS_DISTRIBUTION$DISTRO_VERSION-$RUNTIME_VERSION-$PLATFORM_SANITIZED: /") 2> >(sed "s/^/$OS_DISTRIBUTION-$DISTRO_VERSION:$RUNTIME_VERSION:$PLATFORM_SANITIZED: /" >&2) +} + +main() { + if (( $# != 1 && $# != 2)); then + >&2 echo "Invalid number of parameters." + usage + exit 1 + fi + BUILDSPEC_YML_PATH="$1" + + # Allow passing a single buildspec file so CI can parallelize per OS. + if [ -f "$BUILDSPEC_YML_PATH" ]; then + do_one_yaml "$BUILDSPEC_YML_PATH" + return + fi + + HAS_YML=0 + for f in "$BUILDSPEC_YML_PATH"/*.yml ; do + [ -f "$f" ] || continue; + do_one_yaml "$f" + HAS_YML=1 + done + + if (( HAS_YML == 0 )); then + >&2 echo At least one buildspec is required. + exit 2 + fi +} + +main "$@" diff --git a/aws-lambda-java-runtime-interface-client/test/integration/codebuild-local/test_one.sh b/aws-lambda-java-runtime-interface-client/test/integration/codebuild-local/test_one.sh new file mode 100755 index 000000000..2d20f3162 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/test/integration/codebuild-local/test_one.sh @@ -0,0 +1,77 @@ +#!/bin/bash +# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +set -euo pipefail + +CODEBUILD_IMAGE_TAG="${CODEBUILD_IMAGE_TAG:-al2/x86_64/standard/3.0}" + +function usage { + >&2 echo "usage: test_one.sh buildspec_yml os_distribution distro_version runtime_version [env]" + >&2 echo "Runs one buildspec version combination from a build-matrix buildspec." + >&2 echo "Required:" + >&2 echo " buildspec_yml Used to specify the CodeBuild buildspec template file." + >&2 echo " os_distribution Used to specify the OS distribution to build." + >&2 echo " distro_version Used to specify the distro version of ." + >&2 echo " runtime_version Used to specify the runtime version to test on the selected ." + >&2 echo " platform Used to specify the architecture platform to test on the selected ." + >&2 echo "Optional:" + >&2 echo " env Additional environment variables file." +} + +# codebuild/local-builds images are not multi-architectural +function get_local_agent_image() { + if [[ "$(arch)" == "aarch64" ]]; then + echo "public.ecr.aws/codebuild/local-builds:aarch64" + else + echo "public.ecr.aws/codebuild/local-builds:latest" + fi +} + +main() { + if (( $# != 5 && $# != 6)); then + >&2 echo "Invalid number of parameters." + usage + exit 1 + fi + + BUILDSPEC_YML="$1" + OS_DISTRIBUTION="$2" + DISTRO_VERSION="$3" + RUNTIME_VERSION="$4" + PLATFORM="$5" + PLATFORM_SANITIZED=$(echo "$PLATFORM" | tr "/" ".") + EXTRA_ENV="${6-}" + + CODEBUILD_TEMP_DIR=$(mktemp -d codebuild."$OS_DISTRIBUTION"-"$DISTRO_VERSION"-"$RUNTIME_VERSION"-"$PLATFORM_SANITIZED".XXXXXXXXXX) + trap 'rm -rf $CODEBUILD_TEMP_DIR' EXIT + + # Create an env file for codebuild_build. + ENVFILE="$CODEBUILD_TEMP_DIR/.env" + if [ -f "$EXTRA_ENV" ]; then + cat "$EXTRA_ENV" > "$ENVFILE" + fi + { + echo "" + echo "OS_DISTRIBUTION=$OS_DISTRIBUTION" + echo "DISTRO_VERSION=$DISTRO_VERSION" + echo "RUNTIME_VERSION=$RUNTIME_VERSION" + echo "PLATFORM=$PLATFORM" + } >> "$ENVFILE" + + ARTIFACTS_DIR="$CODEBUILD_TEMP_DIR/artifacts" + mkdir -p "$ARTIFACTS_DIR" + + LOCAL_AGENT_IMAGE="$(get_local_agent_image)" + "$(dirname "$0")"/docker-retry.sh docker pull "$LOCAL_AGENT_IMAGE" || true + + # Run CodeBuild local agent. + "$(dirname "$0")"/codebuild_build.sh \ + -i "$CODEBUILD_IMAGE_TAG" \ + -a "$ARTIFACTS_DIR" \ + -e "$ENVFILE" \ + -b "$BUILDSPEC_YML" \ + -s "$(dirname $PWD)" \ + -l "$LOCAL_AGENT_IMAGE" +} + +main "$@" diff --git a/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.alpine.yml b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.alpine.yml new file mode 100644 index 000000000..65a9da50a --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.alpine.yml @@ -0,0 +1,86 @@ +version: 0.2 + +env: + variables: + OS_DISTRIBUTION: alpine + JAVA_BINARY_LOCATION: "/usr/bin/java" + DOCKER_CLI_EXPERIMENTAL: "enabled" + DOCKER_CLI_PLUGIN_DIR: "/root/.docker/cli-plugins" +batch: + build-matrix: + static: + ignore-failure: false + env: + privileged-mode: true + dynamic: + env: + variables: + DISTRO_VERSION: + - "3.13" + - "3.14" + - "3.15" + RUNTIME_VERSION: + - "corretto11" + PLATFORM: + - "linux/amd64" + - "linux/arm64/v8" +phases: + install: + commands: + - > + if [[ -z "${DOCKERHUB_USERNAME}" && -z "${DOCKERHUB_PASSWORD}" ]]; + then + echo "DockerHub credentials not set as CodeBuild environment variables. Continuing without docker login." + else + echo "Performing DockerHub login . . ." + docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_PASSWORD + fi + - aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/configure_multi_arch_env.sh + pre_build: + commands: + # Log some environment variables for troubleshooting + - (mvn -v) + # Install events (dependency of serialization) + - (cd aws-lambda-java-events && mvn install) + # Install serialization (dependency of RIC) + - (cd aws-lambda-java-core && mvn install) + - (cd aws-lambda-java-serialization && mvn install) + - (cd aws-lambda-java-runtime-interface-client && mvn install -DmultiArch=false -DargLineForReflectionTestOnly="") + - (cd aws-lambda-java-runtime-interface-client/test/integration/test-handler && mvn install) + - export IMAGE_TAG="java-${OS_DISTRIBUTION}-${DISTRO_VERSION}:${RUNTIME_VERSION}" + - echo "Extracting and including Runtime Interface Emulator" + - SCRATCH_DIR=".scratch" + - mkdir "${SCRATCH_DIR}" + - > + if [[ "$PLATFORM" == "linux/amd64" ]]; then + RIE="aws-lambda-rie" + elif [[ "$PLATFORM" == "linux/arm64/v8" ]]; then + RIE="aws-lambda-rie-arm64" + else + echo "Platform $PLATFORM is not currently supported." + exit 1 + fi + - tar -xvf aws-lambda-java-runtime-interface-client/test/integration/resources/${RIE}.tar.gz --directory "${SCRATCH_DIR}" + - > + cp "aws-lambda-java-runtime-interface-client/test/integration/docker/Dockerfile.function.${OS_DISTRIBUTION}" \ + "${SCRATCH_DIR}/Dockerfile.function.${OS_DISTRIBUTION}.tmp" + - > + echo "RUN apk add curl" >> \ + "${SCRATCH_DIR}/Dockerfile.function.${OS_DISTRIBUTION}.tmp" + - > + echo "COPY ${SCRATCH_DIR}/${RIE} /usr/bin/${RIE}" >> \ + "${SCRATCH_DIR}/Dockerfile.function.${OS_DISTRIBUTION}.tmp" + - echo "Building image ${IMAGE_TAG}" + - > + aws-lambda-java-runtime-interface-client/test/integration/codebuild-local/docker-retry.sh docker build . \ + -f "${SCRATCH_DIR}/Dockerfile.function.${OS_DISTRIBUTION}.tmp" \ + -t "${IMAGE_TAG}" \ + --platform="${PLATFORM}" \ + --build-arg RUNTIME_VERSION="${RUNTIME_VERSION}" \ + --build-arg DISTRO_VERSION="${DISTRO_VERSION}" + build: + commands: + - aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/run_invocation_test.sh + finally: + - aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/fetch_test_container_logs.sh + - aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/clean_up.sh diff --git a/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.amazoncorretto.yml b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.amazoncorretto.yml new file mode 100644 index 000000000..c578235fe --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.amazoncorretto.yml @@ -0,0 +1,82 @@ +version: 0.2 + +env: + variables: + OS_DISTRIBUTION: amazoncorretto + JAVA_BINARY_LOCATION: "/usr/bin/java" + DOCKER_CLI_EXPERIMENTAL: "enabled" + DOCKER_CLI_PLUGIN_DIR: "/root/.docker/cli-plugins" +batch: + build-matrix: + static: + ignore-failure: false + env: + privileged-mode: true + dynamic: + env: + variables: + DISTRO_VERSION: + - "amazoncorretto" + RUNTIME_VERSION: + - "8" + - "11" + PLATFORM: + - "linux/amd64" + - "linux/arm64/v8" +phases: + install: + commands: + - > + if [[ -z "${DOCKERHUB_USERNAME}" && -z "${DOCKERHUB_PASSWORD}" ]]; + then + echo "DockerHub credentials not set as CodeBuild environment variables. Continuing without docker login." + else + echo "Performing DockerHub login . . ." + docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_PASSWORD + fi + - aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/configure_multi_arch_env.sh + pre_build: + commands: + # Log some environment variables for troubleshooting + - (mvn -v) + # Install events (dependency of serialization) + - (cd aws-lambda-java-events && mvn install) + # Install serialization (dependency of RIC) + - (cd aws-lambda-java-core && mvn install) + - (cd aws-lambda-java-serialization && mvn install) + - (cd aws-lambda-java-runtime-interface-client && mvn install -DmultiArch=false -DargLineForReflectionTestOnly="") + - (cd aws-lambda-java-runtime-interface-client/test/integration/test-handler && mvn install) + - export IMAGE_TAG="java-${OS_DISTRIBUTION}-${DISTRO_VERSION}:${RUNTIME_VERSION}" + - echo "Extracting and including Runtime Interface Emulator" + - SCRATCH_DIR=".scratch" + - mkdir "${SCRATCH_DIR}" + - > + if [[ "$PLATFORM" == "linux/amd64" ]]; then + RIE="aws-lambda-rie" + elif [[ "$PLATFORM" == "linux/arm64/v8" ]]; then + RIE="aws-lambda-rie-arm64" + else + echo "Platform $PLATFORM is not currently supported." + exit 1 + fi + - tar -xvf aws-lambda-java-runtime-interface-client/test/integration/resources/${RIE}.tar.gz --directory "${SCRATCH_DIR}" + - > + cp "aws-lambda-java-runtime-interface-client/test/integration/docker/Dockerfile.function.${OS_DISTRIBUTION}" \ + "${SCRATCH_DIR}/Dockerfile.function.${OS_DISTRIBUTION}.tmp" + - > + echo "COPY ${SCRATCH_DIR}/${RIE} /usr/bin/${RIE}" >> \ + "${SCRATCH_DIR}/Dockerfile.function.${OS_DISTRIBUTION}.tmp" + - echo "Building image ${IMAGE_TAG}" + - > + aws-lambda-java-runtime-interface-client/test/integration/codebuild-local/docker-retry.sh docker build . \ + -f "${SCRATCH_DIR}/Dockerfile.function.${OS_DISTRIBUTION}.tmp" \ + -t "${IMAGE_TAG}" \ + --platform="${PLATFORM}" \ + --build-arg RUNTIME_VERSION="${RUNTIME_VERSION}" \ + --build-arg DISTRO_VERSION="${DISTRO_VERSION}" + build: + commands: + - aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/run_invocation_test.sh + finally: + - aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/fetch_test_container_logs.sh + - aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/clean_up.sh diff --git a/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.amazonlinux.1.yml b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.amazonlinux.1.yml new file mode 100644 index 000000000..0398ca642 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.amazonlinux.1.yml @@ -0,0 +1,68 @@ +version: 0.2 + +env: + variables: + OS_DISTRIBUTION: amazonlinux + JAVA_BINARY_LOCATION: "/usr/bin/java" +batch: + build-matrix: + static: + ignore-failure: false + env: + privileged-mode: true + dynamic: + env: + variables: + DISTRO_VERSION: + - "1" + RUNTIME_VERSION: + - "openjdk8" + PLATFORM: + - "linux/amd64" +phases: + install: + commands: + - > + if [[ -z "${DOCKERHUB_USERNAME}" && -z "${DOCKERHUB_PASSWORD}" ]]; + then + echo "DockerHub credentials not set as CodeBuild environment variables. Continuing without docker login." + else + echo "Performing DockerHub login . . ." + docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_PASSWORD + fi + pre_build: + commands: + # Log some environment variables for troubleshooting + - (mvn -v) + # Install events (dependency of serialization) + - (cd aws-lambda-java-events && mvn install) + # Install serialization (dependency of RIC) + - (cd aws-lambda-java-core && mvn install) + - (cd aws-lambda-java-serialization && mvn install) + - (cd aws-lambda-java-runtime-interface-client && mvn install -DmultiArch=false -DargLineForReflectionTestOnly="") + - (cd aws-lambda-java-runtime-interface-client/test/integration/test-handler && mvn install) + - export IMAGE_TAG="java-${OS_DISTRIBUTION}-${DISTRO_VERSION}:${RUNTIME_VERSION}" + - echo "Extracting and including Runtime Interface Emulator" + - SCRATCH_DIR=".scratch" + - mkdir "${SCRATCH_DIR}" + - RIE="aws-lambda-rie" + - tar -xvf aws-lambda-java-runtime-interface-client/test/integration/resources/${RIE}.tar.gz --directory "${SCRATCH_DIR}" + - > + cp "aws-lambda-java-runtime-interface-client/test/integration/docker/Dockerfile.function.${OS_DISTRIBUTION}" \ + "${SCRATCH_DIR}/Dockerfile.function.${OS_DISTRIBUTION}.tmp" + - > + echo "COPY ${SCRATCH_DIR}/${RIE} /usr/bin/${RIE}" >> \ + "${SCRATCH_DIR}/Dockerfile.function.${OS_DISTRIBUTION}.tmp" + - echo "Building image ${IMAGE_TAG}" + - > + aws-lambda-java-runtime-interface-client/test/integration/codebuild-local/docker-retry.sh docker build . \ + -f "${SCRATCH_DIR}/Dockerfile.function.${OS_DISTRIBUTION}.tmp" \ + -t "${IMAGE_TAG}" \ + --build-arg RUNTIME_VERSION="${RUNTIME_VERSION}" \ + --build-arg DISTRO_VERSION="${DISTRO_VERSION}" + build: + commands: + - aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/run_invocation_test.sh + finally: + - aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/fetch_test_container_logs.sh + - aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/clean_up.sh diff --git a/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.amazonlinux.2.yml b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.amazonlinux.2.yml new file mode 100644 index 000000000..a229defbc --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.amazonlinux.2.yml @@ -0,0 +1,81 @@ +version: 0.2 + +env: + variables: + OS_DISTRIBUTION: amazonlinux + JAVA_BINARY_LOCATION: "/usr/bin/java" + DOCKER_CLI_EXPERIMENTAL: "enabled" + DOCKER_CLI_PLUGIN_DIR: "/root/.docker/cli-plugins" +batch: + build-matrix: + static: + ignore-failure: false + env: + privileged-mode: true + dynamic: + env: + variables: + DISTRO_VERSION: + - "2" + RUNTIME_VERSION: + - "openjdk8" + PLATFORM: + - "linux/amd64" + - "linux/arm64/v8" +phases: + install: + commands: + - > + if [[ -z "${DOCKERHUB_USERNAME}" && -z "${DOCKERHUB_PASSWORD}" ]]; + then + echo "DockerHub credentials not set as CodeBuild environment variables. Continuing without docker login." + else + echo "Performing DockerHub login . . ." + docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_PASSWORD + fi + - aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/configure_multi_arch_env.sh + pre_build: + commands: + # Log some environment variables for troubleshooting + - (mvn -v) + # Install events (dependency of serialization) + - (cd aws-lambda-java-events && mvn install) + # Install serialization (dependency of RIC) + - (cd aws-lambda-java-core && mvn install) + - (cd aws-lambda-java-serialization && mvn install) + - (cd aws-lambda-java-runtime-interface-client && mvn install -DmultiArch=false -DargLineForReflectionTestOnly="") + - (cd aws-lambda-java-runtime-interface-client/test/integration/test-handler && mvn install) + - export IMAGE_TAG="java-${OS_DISTRIBUTION}-${DISTRO_VERSION}:${RUNTIME_VERSION}" + - echo "Extracting and including Runtime Interface Emulator" + - SCRATCH_DIR=".scratch" + - mkdir "${SCRATCH_DIR}" + - > + if [[ "$PLATFORM" == "linux/amd64" ]]; then + RIE="aws-lambda-rie" + elif [[ "$PLATFORM" == "linux/arm64/v8" ]]; then + RIE="aws-lambda-rie-arm64" + else + echo "Platform $PLATFORM is not currently supported." + exit 1 + fi + - tar -xvf aws-lambda-java-runtime-interface-client/test/integration/resources/${RIE}.tar.gz --directory "${SCRATCH_DIR}" + - > + cp "aws-lambda-java-runtime-interface-client/test/integration/docker/Dockerfile.function.${OS_DISTRIBUTION}" \ + "${SCRATCH_DIR}/Dockerfile.function.${OS_DISTRIBUTION}.tmp" + - > + echo "COPY ${SCRATCH_DIR}/${RIE} /usr/bin/${RIE}" >> \ + "${SCRATCH_DIR}/Dockerfile.function.${OS_DISTRIBUTION}.tmp" + - echo "Building image ${IMAGE_TAG}" + - > + aws-lambda-java-runtime-interface-client/test/integration/codebuild-local/docker-retry.sh docker build . \ + -f "${SCRATCH_DIR}/Dockerfile.function.${OS_DISTRIBUTION}.tmp" \ + -t "${IMAGE_TAG}" \ + --platform="${PLATFORM}" \ + --build-arg RUNTIME_VERSION="${RUNTIME_VERSION}" \ + --build-arg DISTRO_VERSION="${DISTRO_VERSION}" + build: + commands: + - aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/run_invocation_test.sh + finally: + - aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/fetch_test_container_logs.sh + - aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/clean_up.sh diff --git a/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.debian.yml b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.debian.yml new file mode 100644 index 000000000..02f3915c3 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.debian.yml @@ -0,0 +1,85 @@ +version: 0.2 + +env: + variables: + OS_DISTRIBUTION: debian + JAVA_BINARY_LOCATION: "/usr/lib/jvm/java-11-amazon-corretto/bin/java" + DOCKER_CLI_EXPERIMENTAL: "enabled" + DOCKER_CLI_PLUGIN_DIR: "/root/.docker/cli-plugins" +batch: + build-matrix: + static: + ignore-failure: false + env: + privileged-mode: true + dynamic: + env: + variables: + DISTRO_VERSION: + - "bookworm" + - "trixie" + RUNTIME_VERSION: + - "corretto11" + PLATFORM: + - "linux/amd64" + - "linux/arm64/v8" +phases: + install: + commands: + - > + if [[ -z "${DOCKERHUB_USERNAME}" && -z "${DOCKERHUB_PASSWORD}" ]]; + then + echo "DockerHub credentials not set as CodeBuild environment variables. Continuing without docker login." + else + echo "Performing DockerHub login . . ." + docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_PASSWORD + fi + - aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/configure_multi_arch_env.sh + pre_build: + commands: + # Log some environment variables for troubleshooting + - (mvn -v) + # Install events (dependency of serialization) + - (cd aws-lambda-java-events && mvn install) + # Install serialization (dependency of RIC) + - (cd aws-lambda-java-core && mvn install) + - (cd aws-lambda-java-serialization && mvn install) + - (cd aws-lambda-java-runtime-interface-client && mvn install -DmultiArch=false -DargLineForReflectionTestOnly="") + - (cd aws-lambda-java-runtime-interface-client/test/integration/test-handler && mvn install) + - export IMAGE_TAG="java-${OS_DISTRIBUTION}-${DISTRO_VERSION}:${RUNTIME_VERSION}" + - echo "Extracting and including Runtime Interface Emulator" + - SCRATCH_DIR=".scratch" + - mkdir "${SCRATCH_DIR}" + - > + if [[ "$PLATFORM" == "linux/amd64" ]]; then + RIE="aws-lambda-rie" + elif [[ "$PLATFORM" == "linux/arm64/v8" ]]; then + RIE="aws-lambda-rie-arm64" + else + echo "Platform $PLATFORM is not currently supported." + exit 1 + fi + - tar -xvf aws-lambda-java-runtime-interface-client/test/integration/resources/${RIE}.tar.gz --directory "${SCRATCH_DIR}" + - > + cp "aws-lambda-java-runtime-interface-client/test/integration/docker/Dockerfile.function.${OS_DISTRIBUTION}" \ + "${SCRATCH_DIR}/Dockerfile.function.${OS_DISTRIBUTION}.tmp" + - > + echo "COPY ${SCRATCH_DIR}/${RIE} /usr/bin/${RIE}" >> \ + "${SCRATCH_DIR}/Dockerfile.function.${OS_DISTRIBUTION}.tmp" + - > + echo "RUN apt-get update && apt-get install -y curl" >> \ + "${SCRATCH_DIR}/Dockerfile.function.${OS_DISTRIBUTION}.tmp" + - echo "Building image ${IMAGE_TAG}" + - > + aws-lambda-java-runtime-interface-client/test/integration/codebuild-local/docker-retry.sh docker build . \ + -f "${SCRATCH_DIR}/Dockerfile.function.${OS_DISTRIBUTION}.tmp" \ + -t "${IMAGE_TAG}" \ + --platform="${PLATFORM}" \ + --build-arg RUNTIME_VERSION="${RUNTIME_VERSION}" \ + --build-arg DISTRO_VERSION="${DISTRO_VERSION}" + build: + commands: + - aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/run_invocation_test.sh + finally: + - aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/fetch_test_container_logs.sh + - aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/clean_up.sh diff --git a/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.ubuntu.yml b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.ubuntu.yml new file mode 100644 index 000000000..cb63814d0 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/buildspec.os.ubuntu.yml @@ -0,0 +1,85 @@ +version: 0.2 + +env: + variables: + OS_DISTRIBUTION: ubuntu + JAVA_BINARY_LOCATION: "/usr/lib/jvm/java-11-amazon-corretto/bin/java" + DOCKER_CLI_EXPERIMENTAL: "enabled" + DOCKER_CLI_PLUGIN_DIR: "/root/.docker/cli-plugins" +batch: + build-matrix: + static: + ignore-failure: false + env: + privileged-mode: true + dynamic: + env: + variables: + DISTRO_VERSION: + - "22.04" + - "24.04" + RUNTIME_VERSION: + - "corretto11" + PLATFORM: + - "linux/amd64" + - "linux/arm64/v8" +phases: + install: + commands: + - > + if [[ -z "${DOCKERHUB_USERNAME}" && -z "${DOCKERHUB_PASSWORD}" ]]; + then + echo "DockerHub credentials not set as CodeBuild environment variables. Continuing without docker login." + else + echo "Performing DockerHub login . . ." + docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_PASSWORD + fi + - aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/configure_multi_arch_env.sh + pre_build: + commands: + # Log some environment variables for troubleshooting + - (mvn -v) + # Install events (dependency of serialization) + - (cd aws-lambda-java-events && mvn install) + # Install serialization (dependency of RIC) + - (cd aws-lambda-java-core && mvn install) + - (cd aws-lambda-java-serialization && mvn install) + - (cd aws-lambda-java-runtime-interface-client && mvn install -DmultiArch=false -DargLineForReflectionTestOnly="") + - (cd aws-lambda-java-runtime-interface-client/test/integration/test-handler && mvn install) + - export IMAGE_TAG="java-${OS_DISTRIBUTION}-${DISTRO_VERSION}:${RUNTIME_VERSION}" + - echo "Extracting and including Runtime Interface Emulator" + - SCRATCH_DIR=".scratch" + - mkdir "${SCRATCH_DIR}" + - > + if [[ "$PLATFORM" == "linux/amd64" ]]; then + RIE="aws-lambda-rie" + elif [[ "$PLATFORM" == "linux/arm64/v8" ]]; then + RIE="aws-lambda-rie-arm64" + else + echo "Platform $PLATFORM is not currently supported." + exit 1 + fi + - tar -xvf aws-lambda-java-runtime-interface-client/test/integration/resources/${RIE}.tar.gz --directory "${SCRATCH_DIR}" + - > + cp "aws-lambda-java-runtime-interface-client/test/integration/docker/Dockerfile.function.${OS_DISTRIBUTION}" \ + "${SCRATCH_DIR}/Dockerfile.function.${OS_DISTRIBUTION}.tmp" + - > + echo "COPY ${SCRATCH_DIR}/${RIE} /usr/bin/${RIE}" >> \ + "${SCRATCH_DIR}/Dockerfile.function.${OS_DISTRIBUTION}.tmp" + - > + echo "RUN apt-get update && apt-get install -y curl" >> \ + "${SCRATCH_DIR}/Dockerfile.function.${OS_DISTRIBUTION}.tmp" + - echo "Building image ${IMAGE_TAG}" + - > + aws-lambda-java-runtime-interface-client/test/integration/codebuild-local/docker-retry.sh docker build . \ + -f "${SCRATCH_DIR}/Dockerfile.function.${OS_DISTRIBUTION}.tmp" \ + -t "${IMAGE_TAG}" \ + --platform="${PLATFORM}" \ + --build-arg RUNTIME_VERSION="${RUNTIME_VERSION}" \ + --build-arg DISTRO_VERSION="${DISTRO_VERSION}" + build: + commands: + - aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/run_invocation_test.sh + finally: + - aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/fetch_test_container_logs.sh + - aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/clean_up.sh diff --git a/aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/clean_up.sh b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/clean_up.sh new file mode 100755 index 000000000..236fa7b26 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/clean_up.sh @@ -0,0 +1,11 @@ +#!/bin/bash +# Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +set -euo pipefail + +echo "Cleaning up..." +docker stop "${OS_DISTRIBUTION}-app" || true +docker rm --force "${OS_DISTRIBUTION}-app" || true +docker stop "${OS_DISTRIBUTION}-tester" || true +docker rm --force "${OS_DISTRIBUTION}-tester" || true +docker network rm "${OS_DISTRIBUTION}-network" || true diff --git a/aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/configure_multi_arch_env.sh b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/configure_multi_arch_env.sh new file mode 100755 index 000000000..7e1ed3b1e --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/configure_multi_arch_env.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +set -euo pipefail + +echo "Setting up multi-arch build environment" +ARCHITECTURE=$(arch) +if [[ "$ARCHITECTURE" == "x86_64" ]]; then + TARGET_EMULATOR="arm64" +elif [[ "$ARCHITECTURE" == "aarch64" ]]; then + TARGET_EMULATOR="amd64" +else + echo "Architecture $ARCHITECTURE is not currently supported." + exit 1 +fi + +echo "Installing ${TARGET_EMULATOR} emulator" +"$(dirname "$0")"/../../codebuild-local/docker-retry.sh docker pull public.ecr.aws/eks-distro-build-tooling/binfmt-misc:qemu-v6.1.0 +docker run --rm --privileged public.ecr.aws/eks-distro-build-tooling/binfmt-misc:qemu-v6.1.0 --install ${TARGET_EMULATOR} +echo "Setting docker build command to default to buildx" +echo "Docker buildx version: $(docker buildx version)" +docker buildx install diff --git a/aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/fetch_test_container_logs.sh b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/fetch_test_container_logs.sh new file mode 100755 index 000000000..4bc809dc1 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/fetch_test_container_logs.sh @@ -0,0 +1,15 @@ +#!/bin/bash +# Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +set -euo pipefail + +echo "---------Container Logs: ${OS_DISTRIBUTION}-app----------" +echo +docker logs "${OS_DISTRIBUTION}-app" || true +echo +echo "---------------------------------------------------" +echo "--------Container Logs: ${OS_DISTRIBUTION}-tester--------" +echo +docker logs "${OS_DISTRIBUTION}-tester" || true +echo +echo "---------------------------------------------------" diff --git a/aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/run_invocation_test.sh b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/run_invocation_test.sh new file mode 100755 index 000000000..f5ebf0687 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/test/integration/codebuild/scripts/run_invocation_test.sh @@ -0,0 +1,43 @@ +#!/bin/bash +# Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +set -euxo pipefail + +echo "Running Image ${IMAGE_TAG}" +docker network create "${OS_DISTRIBUTION}-network" + +function_jar="./HelloWorld-1.0.jar" +function_handler="helloworld.App" +docker run \ + --detach \ + --name "${OS_DISTRIBUTION}-app" \ + --network "${OS_DISTRIBUTION}-network" \ + --entrypoint="" \ + --platform="${PLATFORM}" \ + "${IMAGE_TAG}" \ + sh -c "/usr/bin/${RIE} ${JAVA_BINARY_LOCATION} -jar ${function_jar} ${function_handler}" +sleep 2 + +# running on arm64 hosts with x86_64 being emulated takes significantly more time than any other combination +if [[ "$(arch)" == "aarch64" ]] && [[ "${PLATFORM}" == "linux/amd64" ]]; then + declare -i time_out=150 +else + declare -i time_out=10 +fi + +docker run \ + --name "${OS_DISTRIBUTION}-tester" \ + --env "TARGET=${OS_DISTRIBUTION}-app" \ + --env "MAX_TIME=${time_out}" \ + --network "${OS_DISTRIBUTION}-network" \ + --entrypoint="" \ + --platform="${PLATFORM}" \ + "${IMAGE_TAG}" \ + sh -c 'curl -X POST "http://${TARGET}:8080/2015-03-31/functions/function/invocations" -d "{}" --max-time ${MAX_TIME}' +actual="$(docker logs --tail 1 "${OS_DISTRIBUTION}-tester" | xargs)" +expected='success' +echo "Response: ${actual}" +if [[ "${actual}" != "${expected}" ]]; then + echo "fail! runtime: ${RUNTIME} - expected output ${expected} - got ${actual}" + exit 1 +fi diff --git a/aws-lambda-java-runtime-interface-client/test/integration/docker/Dockerfile.function.alpine b/aws-lambda-java-runtime-interface-client/test/integration/docker/Dockerfile.function.alpine new file mode 100644 index 000000000..bfd288901 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/test/integration/docker/Dockerfile.function.alpine @@ -0,0 +1,12 @@ +ARG DISTRO_VERSION + +FROM public.ecr.aws/docker/library/alpine:${DISTRO_VERSION} + +RUN apk update && \ + apk add openjdk8 + +ADD aws-lambda-java-runtime-interface-client/test/integration/test-handler/target/HelloWorld-1.0.jar . + +ENTRYPOINT ["java", "-jar", "./HelloWorld-1.0.jar"] + +CMD ["helloworld.App"] diff --git a/aws-lambda-java-runtime-interface-client/test/integration/docker/Dockerfile.function.amazoncorretto b/aws-lambda-java-runtime-interface-client/test/integration/docker/Dockerfile.function.amazoncorretto new file mode 100644 index 000000000..a6958cb05 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/test/integration/docker/Dockerfile.function.amazoncorretto @@ -0,0 +1,9 @@ +ARG RUNTIME_VERSION + +FROM public.ecr.aws/amazoncorretto/amazoncorretto:${RUNTIME_VERSION} + +ADD aws-lambda-java-runtime-interface-client/test/integration/test-handler/target/HelloWorld-1.0.jar . + +ENTRYPOINT ["java", "-jar", "./HelloWorld-1.0.jar"] + +CMD ["helloworld.App"] diff --git a/aws-lambda-java-runtime-interface-client/test/integration/docker/Dockerfile.function.amazonlinux b/aws-lambda-java-runtime-interface-client/test/integration/docker/Dockerfile.function.amazonlinux new file mode 100644 index 000000000..b3d152fc1 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/test/integration/docker/Dockerfile.function.amazonlinux @@ -0,0 +1,11 @@ +ARG DISTRO_VERSION + +FROM public.ecr.aws/amazonlinux/amazonlinux:${DISTRO_VERSION} + +RUN yum install -y java-1.8.0-openjdk + +ADD aws-lambda-java-runtime-interface-client/test/integration/test-handler/target/HelloWorld-1.0.jar . + +ENTRYPOINT ["java", "-jar", "./HelloWorld-1.0.jar"] + +CMD ["helloworld.App"] diff --git a/aws-lambda-java-runtime-interface-client/test/integration/docker/Dockerfile.function.debian b/aws-lambda-java-runtime-interface-client/test/integration/docker/Dockerfile.function.debian new file mode 100644 index 000000000..019b46a62 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/test/integration/docker/Dockerfile.function.debian @@ -0,0 +1,22 @@ +ARG DISTRO_VERSION + +FROM public.ecr.aws/debian/debian:${DISTRO_VERSION} as build-image + +RUN apt-get update && \ + apt-get install -y wget gnupg ca-certificates && \ + wget -O- https://apt.corretto.aws/corretto.key | gpg --dearmor -o /usr/share/keyrings/corretto-keyring.gpg && \ + echo "deb [signed-by=/usr/share/keyrings/corretto-keyring.gpg] https://apt.corretto.aws stable main" > /etc/apt/sources.list.d/corretto.list && \ + apt-get update && \ + apt-get install -y java-11-amazon-corretto-jdk + +FROM public.ecr.aws/debian/debian:${DISTRO_VERSION} + +COPY --from=build-image /usr/lib/jvm /usr/lib/jvm + +ADD aws-lambda-java-runtime-interface-client/test/integration/test-handler/target/HelloWorld-1.0.jar . + +ENV PATH=/usr/lib/jvm/java-11-amazon-corretto/bin/:$PATH + +ENTRYPOINT ["java", "-jar", "./HelloWorld-1.0.jar"] + +CMD ["helloworld.App"] diff --git a/aws-lambda-java-runtime-interface-client/test/integration/docker/Dockerfile.function.ubuntu b/aws-lambda-java-runtime-interface-client/test/integration/docker/Dockerfile.function.ubuntu new file mode 100644 index 000000000..09107c469 --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/test/integration/docker/Dockerfile.function.ubuntu @@ -0,0 +1,23 @@ +ARG DISTRO_VERSION + +FROM public.ecr.aws/ubuntu/ubuntu:${DISTRO_VERSION} as build-image + +RUN apt-get update && \ + apt-get install -y apt-transport-https ca-certificates && \ + apt-get install -y wget gnupg software-properties-common && \ + wget -O- https://apt.corretto.aws/corretto.key | apt-key add - && \ + add-apt-repository 'deb https://apt.corretto.aws stable main' && \ + apt-get update && \ + apt-get install -y java-11-amazon-corretto-jdk + +FROM public.ecr.aws/ubuntu/ubuntu:${DISTRO_VERSION} + +COPY --from=build-image /usr/lib/jvm /usr/lib/jvm + +ADD aws-lambda-java-runtime-interface-client/test/integration/test-handler/target/HelloWorld-1.0.jar . + +ENV PATH=/usr/lib/jvm/java-11-amazon-corretto/bin/:$PATH + +ENTRYPOINT ["java", "-jar", "./HelloWorld-1.0.jar"] + +CMD ["helloworld.App"] diff --git a/aws-lambda-java-runtime-interface-client/test/integration/resources/aws-lambda-rie-arm64.tar.gz b/aws-lambda-java-runtime-interface-client/test/integration/resources/aws-lambda-rie-arm64.tar.gz new file mode 100644 index 000000000..f62577fcf Binary files /dev/null and b/aws-lambda-java-runtime-interface-client/test/integration/resources/aws-lambda-rie-arm64.tar.gz differ diff --git a/aws-lambda-java-runtime-interface-client/test/integration/resources/aws-lambda-rie.tar.gz b/aws-lambda-java-runtime-interface-client/test/integration/resources/aws-lambda-rie.tar.gz new file mode 100644 index 000000000..feda16d9a Binary files /dev/null and b/aws-lambda-java-runtime-interface-client/test/integration/resources/aws-lambda-rie.tar.gz differ diff --git a/aws-lambda-java-runtime-interface-client/test/integration/test-handler/pom.xml b/aws-lambda-java-runtime-interface-client/test/integration/test-handler/pom.xml new file mode 100644 index 000000000..64893528b --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/test/integration/test-handler/pom.xml @@ -0,0 +1,52 @@ + + 4.0.0 + helloworld + HelloWorld + 1.0 + jar + A sample Hello World. + + 1.8 + 1.8 + + + + + com.amazonaws + aws-lambda-java-runtime-interface-client + 2.10.1 + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.1.1 + + + + + package + + shade + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + com.amazonaws.services.lambda.runtime.api.client.AWSLambda + + + + + + + diff --git a/aws-lambda-java-runtime-interface-client/test/integration/test-handler/src/main/java/helloworld/App.java b/aws-lambda-java-runtime-interface-client/test/integration/test-handler/src/main/java/helloworld/App.java new file mode 100644 index 000000000..a2d0c6a6c --- /dev/null +++ b/aws-lambda-java-runtime-interface-client/test/integration/test-handler/src/main/java/helloworld/App.java @@ -0,0 +1,18 @@ +package helloworld; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; + +import java.util.Map; + +/** + * Handler for requests to Lambda function. + */ +public class App implements RequestHandler, String>{ + @Override + public String handleRequest(Map event, Context context) + { + String response = new String("success"); + return response; + } +} diff --git a/aws-lambda-java-serialization/RELEASE.CHANGELOG.md b/aws-lambda-java-serialization/RELEASE.CHANGELOG.md new file mode 100644 index 000000000..2aaa8732a --- /dev/null +++ b/aws-lambda-java-serialization/RELEASE.CHANGELOG.md @@ -0,0 +1,62 @@ +### Aug 20, 2026 +`1.4.2`: +- Update `jackson-databind` dependency from 2.18.6 to 2.18.9 + +### May 20, 2026 +`1.4.1`: +- Fix build issue + +### March 26, 2026 +`1.4.0`: +- Update `jackson-databind` dependency from 2.15.4 to 2.18.6 +- Replace deprecated `PropertyNamingStrategy.PascalCaseStrategy` with `PropertyNamingStrategies.UpperCamelCaseStrategy` +- The regression reported in 1.3.1 was a false positive caused by a CI workflow bug (`mvn package install` running the shade plugin twice, corrupting the artifact). Fixed by using `mvn install` instead. + +### March 19, 2026 +`1.3.1`: +- Revert `jackson-databind` dependency from 2.18.6 to 2.15.4 +- Revert `PropertyNamingStrategies.UpperCamelCaseStrategy` to `PropertyNamingStrategy.PascalCaseStrategy` +- Note: reverted due to a suspected regression in Joda DateTime deserialization; later confirmed to be a false positive (see 1.4.0) + +### March 11, 2026 +`1.3.0`: +- Update `jackson-databind` dependency from 2.15.4 to 2.18.6 +- Replace deprecated `PropertyNamingStrategy.PascalCaseStrategy` with `PropertyNamingStrategies.UpperCamelCaseStrategy` + +### December 16, 2025 +`1.2.0`: +- Update `jackson-databind` dependency from 2.14.2 to 2.15.4 + +### December 1, 2023 +`1.1.5`: +- Add support for DynamodbEvent.DynamodbStreamRecord serialization + +### October 19, 2023 +`1.1.4`: +- Update org.json version to 20231013 +- Rollback relocation changes(1.1.3 version) + +### September 21, 2023 +`1.1.3`: +- Add support for event v4 lib + +### February 22, 2023 +`1.1.1`: +- Register `JodaModule` to JacksonFactory + +### February 17, 2023 +`1.1.0`: +- Update `jackson-databind` dependency from 2.13.4.1 to 2.14.2 +- Register `JavaTimeModule` and `Jdk8Module` modules to `jackson-databind` + +### February 09, 2023 +`1.0.2`: +- Updated `gson` dependency from 2.8.9 to 2.10.1 + +### November 21, 2022 +`1.0.1`: +- Updated `jackson-databind` dependency from 2.12.6.1 to 2.13.4.1 + +### December 01, 2020 +`1.0.0`: +- Initial release of AWS Lambda Java Serialization \ No newline at end of file diff --git a/aws-lambda-java-serialization/pom.xml b/aws-lambda-java-serialization/pom.xml new file mode 100644 index 000000000..fbdb93d9a --- /dev/null +++ b/aws-lambda-java-serialization/pom.xml @@ -0,0 +1,305 @@ + + 4.0.0 + + com.amazonaws + aws-lambda-java-serialization + 1.4.1-SNAPSHOT + jar + + AWS Lambda Java Runtime Serialization + Serialization logic for the AWS Lambda Java Runtime + https://aws.amazon.com/lambda/ + + + Apache License, Version 2.0 + https://aws.amazon.com/apache2.0 + repo + + + + https://github.com/aws/aws-lambda-java-libs.git + scm:git:https://github.com/aws/aws-lambda-java-libs.git + scm:git:https://github.com/aws/aws-lambda-java-libs.git + HEAD + + + + AWS Lambda team + Amazon Web Services + https://aws.amazon.com/ + + + + + 1.8 + 1.8 + com.amazonaws.lambda.thirdparty + 2.18.9 + 2.10.1 + 20231013 + 7.3.2 + + + + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + com.fasterxml.jackson.datatype + jackson-datatype-joda + ${jackson.version} + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + ${jackson.version} + + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + ${jackson.version} + + + com.google.code.gson + gson + ${gson.version} + + + org.json + json + ${json.version} + + + + + + checkDependencies + + + checkDependencies + + + + + + org.owasp + dependency-check-maven + ${owasp.version} + + + validate + + check + + + + + + + + + dev + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.9.1 + + -Xdoclint:none + + + + attach-javadocs + + jar + + + + + + + + + release + + + + org.apache.maven.plugins + maven-source-plugin + 2.2.1 + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.9.1 + + -Xdoclint:none + + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.5 + + + sign-artifacts + verify + + sign + + + + + + org.sonatype.central + central-publishing-maven-plugin + 0.8.0 + true + + central + false + + + + + + + + + + + com.allogy.maven.wagon + maven-s3-wagon + 1.2.0 + + + + + org.apache.maven.plugins + maven-release-plugin + 3.1.1 + + aws-lambda-java-serialization-@{project.version} + true + release + deploy + + + + org.apache.maven.plugins + maven-toolchains-plugin + 3.2.0 + + + + + [1.8,9) + + + + + + + toolchain + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.1 + + + package + + shade + + + + + + + + com.fasterxml.jackson + ${relocation.prefix}.com.fasterxml.jackson + + + com.google.gson + ${relocation.prefix}.com.google.gson + + + org.json + ${relocation.prefix}.org.json + + + org.joda.time + ${relocation.prefix}.org.joda.time + + + + com.amazonaws.lambda.unshade.thirdparty.org.joda.time + org.joda.time + + + + + *:* + + META-INF/maven/** + + + + + + + + + org.apache.maven.plugins + maven-antrun-plugin + 1.7 + + + verify-relocation + package + + + + + + + + + + + run + + + + + + + diff --git a/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/PojoSerializer.java b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/PojoSerializer.java new file mode 100644 index 000000000..75e029892 --- /dev/null +++ b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/PojoSerializer.java @@ -0,0 +1,12 @@ +/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ + +package com.amazonaws.services.lambda.runtime.serialization; + +import java.io.InputStream; +import java.io.OutputStream; + +public interface PojoSerializer { + T fromJson(InputStream input); + T fromJson(String input); + void toJson(T value, OutputStream output); +} diff --git a/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/LambdaEventSerializers.java b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/LambdaEventSerializers.java new file mode 100644 index 000000000..533bdcd49 --- /dev/null +++ b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/LambdaEventSerializers.java @@ -0,0 +1,333 @@ +/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ + +package com.amazonaws.services.lambda.runtime.serialization.events; + +import com.amazonaws.services.lambda.runtime.serialization.events.mixins.CloudFormationCustomResourceEventMixin; +import com.amazonaws.services.lambda.runtime.serialization.events.mixins.CloudFrontEventMixin; +import com.amazonaws.services.lambda.runtime.serialization.events.mixins.CloudWatchLogsEventMixin; +import com.amazonaws.services.lambda.runtime.serialization.events.mixins.CodeCommitEventMixin; +import com.amazonaws.services.lambda.runtime.serialization.events.mixins.ConnectEventMixin; +import com.amazonaws.services.lambda.runtime.serialization.events.mixins.DynamodbEventMixin; +import com.amazonaws.services.lambda.runtime.serialization.events.mixins.DynamodbTimeWindowEventMixin; +import com.amazonaws.services.lambda.runtime.serialization.events.mixins.KinesisEventMixin; +import com.amazonaws.services.lambda.runtime.serialization.events.mixins.KinesisTimeWindowEventMixin; +import com.amazonaws.services.lambda.runtime.serialization.events.mixins.SNSEventMixin; +import com.amazonaws.services.lambda.runtime.serialization.events.mixins.SQSEventMixin; +import com.amazonaws.services.lambda.runtime.serialization.events.mixins.ScheduledEventMixin; +import com.amazonaws.services.lambda.runtime.serialization.events.mixins.SecretsManagerRotationEventMixin; +import com.amazonaws.services.lambda.runtime.serialization.factories.JacksonFactory; +import com.amazonaws.services.lambda.runtime.serialization.PojoSerializer; +import com.amazonaws.services.lambda.runtime.serialization.util.ReflectUtil; +import com.amazonaws.services.lambda.runtime.serialization.util.SerializeUtil; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.amazonaws.services.lambda.runtime.serialization.events.modules.DateModule; +import com.amazonaws.services.lambda.runtime.serialization.events.modules.DateTimeModule; +import com.amazonaws.services.lambda.runtime.serialization.events.serializers.OrgJsonSerializer; +import com.amazonaws.services.lambda.runtime.serialization.events.serializers.S3EventSerializer; + +import java.util.AbstractMap.SimpleEntry; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * This class provides serializers for Lambda supported events. + * + * HOW TO ADD SUPPORT FOR A NEW EVENT MODEL: + * + * Option 1 (Preferred): + * 1. Add Class name to SUPPORTED_EVENTS + * 2. Add Mixin Class to + * com.amazonaws.services.lambda.runtime.serialization.events.mixins package (if + * needed) + * 3. Add entries to MIXIN_MAP for event class and sub classes (if needed) + * 4. Add entries to NESTED_CLASS_MAP for event class and sub classes (if + * needed) + * 5. Add entry to NAMING_STRATEGY_MAP (if needed i.e. Could be used in place of + * a mixin) + * + * Option 2 (longer - for event models that do not work with Jackson or GSON): + * 1. Add Class name to SUPPORTED_EVENTS + * 2. Add serializer (using org.json) to + * com.amazonaws.services.lambda.runtime.serialization.events.serializers + * 3. Add class name and serializer to SERIALIZER_MAP + */ +public class LambdaEventSerializers { + + /** + * list of supported events + */ + private static final List SUPPORTED_EVENTS = Stream.of( + "com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent", + "com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent", + "com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent", + "com.amazonaws.services.lambda.runtime.events.CloudFrontEvent", + "com.amazonaws.services.lambda.runtime.events.CloudWatchLogsEvent", + "com.amazonaws.services.lambda.runtime.events.CodeCommitEvent", + "com.amazonaws.services.lambda.runtime.events.CognitoEvent", + "com.amazonaws.services.lambda.runtime.events.ConfigEvent", + "com.amazonaws.services.lambda.runtime.events.ConnectEvent", + "com.amazonaws.services.lambda.runtime.events.DynamodbEvent", + "com.amazonaws.services.lambda.runtime.events.DynamodbTimeWindowEvent", + "com.amazonaws.services.lambda.runtime.events.IoTButtonEvent", + "com.amazonaws.services.lambda.runtime.events.KinesisEvent", + "com.amazonaws.services.lambda.runtime.events.KinesisTimeWindowEvent", + "com.amazonaws.services.lambda.runtime.events.KinesisFirehoseEvent", + "com.amazonaws.services.lambda.runtime.events.LambdaDestinationEvent", + "com.amazonaws.services.lambda.runtime.events.LexEvent", + "com.amazonaws.services.lambda.runtime.events.ScheduledEvent", + "com.amazonaws.services.lambda.runtime.events.SecretsManagerRotationEvent", + "com.amazonaws.services.s3.event.S3EventNotification", + "com.amazonaws.services.lambda.runtime.events.models.s3.S3EventNotification", + "com.amazonaws.services.lambda.runtime.events.S3Event", + "com.amazonaws.services.lambda.runtime.events.SNSEvent", + "com.amazonaws.services.lambda.runtime.events.SQSEvent") + .collect(Collectors.toList()); + + /** + * list of events incompatible with Jackson, with serializers explicitly defined + * Classes are incompatible with Jackson for any of the following reasons: + * 1. different constructor/setter types from getter types + * 2. various bugs within Jackson + */ + private static final Map SERIALIZER_MAP = Stream.of( + new SimpleEntry<>("com.amazonaws.services.s3.event.S3EventNotification", new S3EventSerializer<>()), + new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.models.s3.S3EventNotification", new S3EventSerializer<>()), + new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.S3Event", new S3EventSerializer<>())) + .collect(Collectors.toMap(SimpleEntry::getKey, SimpleEntry::getValue)); + + /** + * Maps supported event classes to mixin classes with Jackson annotations. + * Jackson annotations are not loaded through the ClassLoader so if a Java field is serialized or deserialized from a + * json field that does not match the Jave field name, then a Mixin is required. + */ + @SuppressWarnings("rawtypes") + private static final Map MIXIN_MAP = Stream.of( + new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent", + CloudFormationCustomResourceEventMixin.class), + new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.CloudFrontEvent", + CloudFrontEventMixin.class), + new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.CloudWatchLogsEvent", + CloudWatchLogsEventMixin.class), + new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.CodeCommitEvent", + CodeCommitEventMixin.class), + new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.CodeCommitEvent$Record", + CodeCommitEventMixin.RecordMixin.class), + new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.ConnectEvent", + ConnectEventMixin.class), + new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.ConnectEvent$Details", + ConnectEventMixin.DetailsMixin.class), + new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.ConnectEvent$ContactData", + ConnectEventMixin.ContactDataMixin.class), + new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.ConnectEvent$CustomerEndpoint", + ConnectEventMixin.CustomerEndpointMixin.class), + new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.ConnectEvent$Queue", ConnectEventMixin.QueueMixin.class), + new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.ConnectEvent$SystemEndpoint", + ConnectEventMixin.SystemEndpointMixin.class), + new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.DynamodbEvent", + DynamodbEventMixin.class), + new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.DynamodbEvent$DynamodbStreamRecord", + DynamodbEventMixin.DynamodbStreamRecordMixin.class), + new SimpleEntry<>("com.amazonaws.services.dynamodbv2.model.StreamRecord", + DynamodbEventMixin.StreamRecordMixin.class), + new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.models.dynamodb.StreamRecord", + DynamodbEventMixin.StreamRecordMixin.class), + new SimpleEntry<>("com.amazonaws.services.dynamodbv2.model.AttributeValue", + DynamodbEventMixin.AttributeValueMixin.class), + new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.models.dynamodb.AttributeValue", + DynamodbEventMixin.AttributeValueMixin.class), + new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.DynamodbTimeWindowEvent", + DynamodbTimeWindowEventMixin.class), + new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.KinesisEvent", + KinesisEventMixin.class), + new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.KinesisEvent$Record", + KinesisEventMixin.RecordMixin.class), + new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.KinesisTimeWindowEvent", + KinesisTimeWindowEventMixin.class), + new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.ScheduledEvent", + ScheduledEventMixin.class), + new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.SecretsManagerRotationEvent", + SecretsManagerRotationEventMixin.class), + new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.SNSEvent", + SNSEventMixin.class), + new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.SNSEvent$SNSRecord", + SNSEventMixin.SNSRecordMixin.class), + new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.SQSEvent", + SQSEventMixin.class), + new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.SQSEvent$SQSMessage", + SQSEventMixin.SQSMessageMixin.class)) + .collect(Collectors.toMap(SimpleEntry::getKey, SimpleEntry::getValue)); + + /** + * If mixins are required for inner classes of an event, then those nested classes must be specified here. + */ + @SuppressWarnings("rawtypes") + private static final Map> NESTED_CLASS_MAP = Stream.of( + new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.CodeCommitEvent", + Arrays.asList( + new NestedClass("com.amazonaws.services.lambda.runtime.events.CodeCommitEvent$Record"))), + new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.CognitoEvent", + Arrays.asList( + new NestedClass("com.amazonaws.services.lambda.runtime.events.CognitoEvent$DatasetRecord"))), + new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.ConnectEvent", + Arrays.asList( + new NestedClass("com.amazonaws.services.lambda.runtime.events.ConnectEvent$Details"), + new NestedClass("com.amazonaws.services.lambda.runtime.events.ConnectEvent$ContactData"), + new NestedClass("com.amazonaws.services.lambda.runtime.events.ConnectEvent$CustomerEndpoint"), + new NestedClass("com.amazonaws.services.lambda.runtime.events.ConnectEvent$Queue"), + new NestedClass("com.amazonaws.services.lambda.runtime.events.ConnectEvent$SystemEndpoint"))), + new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.DynamodbEvent", + Arrays.asList( + new AlternateNestedClass( + "com.amazonaws.services.lambda.runtime.events.models.dynamodb.AttributeValue", + "com.amazonaws.services.dynamodbv2.model.AttributeValue"), + new AlternateNestedClass( + "com.amazonaws.services.lambda.runtime.events.models.dynamodb.StreamRecord", + "com.amazonaws.services.dynamodbv2.model.StreamRecord"), + new NestedClass("com.amazonaws.services.lambda.runtime.events.DynamodbEvent$DynamodbStreamRecord"))), + new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.DynamodbEvent$DynamodbStreamRecord", + Arrays.asList( + new AlternateNestedClass( + "com.amazonaws.services.lambda.runtime.events.models.dynamodb.AttributeValue", + "com.amazonaws.services.dynamodbv2.model.AttributeValue"), + new AlternateNestedClass( + "com.amazonaws.services.lambda.runtime.events.models.dynamodb.StreamRecord", + "com.amazonaws.services.dynamodbv2.model.StreamRecord"))), + new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.DynamodbTimeWindowEvent", + Arrays.asList( + new AlternateNestedClass( + "com.amazonaws.services.lambda.runtime.events.models.dynamodb.AttributeValue", + "com.amazonaws.services.dynamodbv2.model.AttributeValue"), + new AlternateNestedClass( + "com.amazonaws.services.lambda.runtime.events.models.dynamodb.StreamRecord", + "com.amazonaws.services.dynamodbv2.model.StreamRecord"), + new NestedClass("com.amazonaws.services.lambda.runtime.events.DynamodbEvent$DynamodbStreamRecord"))), + new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.KinesisEvent", + Arrays.asList( + new NestedClass("com.amazonaws.services.lambda.runtime.events.KinesisEvent$Record"))), + new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.SNSEvent", + Arrays.asList( + new NestedClass("com.amazonaws.services.lambda.runtime.events.SNSEvent$SNSRecord"))), + new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.SQSEvent", + Arrays.asList( + new NestedClass("com.amazonaws.services.lambda.runtime.events.SQSEvent$SQSMessage")))) + .collect(Collectors.toMap(SimpleEntry::getKey, SimpleEntry::getValue)); + + /** + * If event requires a naming strategy. For example, when someone names the getter method getSNS and the setter + * method setSns, for some magical reasons, using both mixins and a naming strategy works + */ + private static final Map NAMING_STRATEGY_MAP = Stream.of( + new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.SNSEvent", + new PropertyNamingStrategies.UpperCamelCaseStrategy()), + new SimpleEntry<>("com.amazonaws.services.lambda.runtime.events.ConnectEvent$Queue", + new PropertyNamingStrategies.UpperCamelCaseStrategy()) + ) + .collect(Collectors.toMap(SimpleEntry::getKey, SimpleEntry::getValue)); + + /** + * Returns whether the class name is a Lambda supported event model. + * + * @param className class name as string + * @return whether the event model is supported + */ + public static boolean isLambdaSupportedEvent(String className) { + return SUPPORTED_EVENTS.contains(className); + } + + /** + * Return a serializer for the event class + * + * @return a specific PojoSerializer or modified JacksonFactory instance with + * mixins and modules added in + */ + @SuppressWarnings({ "unchecked" }) + public static PojoSerializer serializerFor(Class eventClass, ClassLoader classLoader) { + // if serializer specifically defined for event then use that + if (SERIALIZER_MAP.containsKey(eventClass.getName())) { + return SERIALIZER_MAP.get(eventClass.getName()).withClass(eventClass) + .withClassLoader(classLoader); + } + // else use a Jackson ObjectMapper instance + JacksonFactory factory = JacksonFactory.getInstance(); + // if mixins required for class, then apply + if (MIXIN_MAP.containsKey(eventClass.getName())) { + factory = factory.withMixin(eventClass, MIXIN_MAP.get(eventClass.getName())); + } + // if event model has nested classes then load those classes and check if mixins + // apply + if (NESTED_CLASS_MAP.containsKey(eventClass.getName())) { + List nestedClasses = NESTED_CLASS_MAP.get(eventClass.getName()); + for (NestedClass nestedClass : nestedClasses) { + // if mixin exists for nested class then apply + if (MIXIN_MAP.containsKey(nestedClass.className)) { + factory = tryLoadingNestedClass(classLoader, factory, nestedClass); + } + } + } + // load DateModules + factory.getMapper().registerModules(new DateModule(), new DateTimeModule(classLoader)); + // load naming strategy if needed + if (NAMING_STRATEGY_MAP.containsKey(eventClass.getName())) { + factory = factory.withNamingStrategy(NAMING_STRATEGY_MAP.get(eventClass.getName())); + } + return factory.getSerializer(eventClass); + } + + /** + * Tries to load a nested class with its defined mixin from {@link #MIXIN_MAP} + * into the {@link JacksonFactory} object. + * Will allow initial failure for {@link AlternateNestedClass} objects and try + * again with their alternate class name + * + * @return a modified JacksonFactory instance with mixins added in + */ + private static JacksonFactory tryLoadingNestedClass(ClassLoader classLoader, JacksonFactory factory, + NestedClass nestedClass) { + Class eventClazz; + Class mixinClazz; + try { + eventClazz = SerializeUtil.loadCustomerClass(nestedClass.getClassName(), classLoader); + mixinClazz = MIXIN_MAP.get(nestedClass.getClassName()); + } catch (ReflectUtil.ReflectException e) { + if (nestedClass instanceof AlternateNestedClass) { + AlternateNestedClass alternateNestedClass = (AlternateNestedClass) nestedClass; + eventClazz = SerializeUtil.loadCustomerClass( + alternateNestedClass.getAlternateClassName(), classLoader); + mixinClazz = MIXIN_MAP.get(alternateNestedClass.getAlternateClassName()); + } else { + throw e; + } + } + + return factory.withMixin(eventClazz, mixinClazz); + } + + private static class NestedClass { + private final String className; + + protected NestedClass(String className) { + this.className = className; + } + + protected String getClassName() { + return className; + } + } + + private static class AlternateNestedClass extends NestedClass { + private final String alternateClassName; + + private AlternateNestedClass(String className, String alternateClassName) { + super(className); + this.alternateClassName = alternateClassName; + } + + private String getAlternateClassName() { + return alternateClassName; + } + } +} diff --git a/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/CloudFormationCustomResourceEventMixin.java b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/CloudFormationCustomResourceEventMixin.java new file mode 100644 index 000000000..299763add --- /dev/null +++ b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/CloudFormationCustomResourceEventMixin.java @@ -0,0 +1,51 @@ +/* Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ + +package com.amazonaws.services.lambda.runtime.serialization.events.mixins; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Map; + +public abstract class CloudFormationCustomResourceEventMixin { + + // needed because jackson expects "requestType" instead of "RequestType" + @JsonProperty("RequestType") abstract String getRequestType(); + @JsonProperty("RequestType") abstract void setRequestType(String requestType); + + // needed because jackson expects "serviceToken" instead of "ServiceToken" + @JsonProperty("ServiceToken") abstract String getServiceToken(); + @JsonProperty("ServiceToken") abstract void setServiceToken(String serviceToken); + + // needed because jackson expects "physicalResourceId" instead of "PhysicalResourceId" + @JsonProperty("PhysicalResourceId") abstract String getPhysicalResourceId(); + @JsonProperty("PhysicalResourceId") abstract void setPhysicalResourceId(String physicalResourceId); + + // needed because jackson expects "responseUrl" instead of "ResponseURL" + @JsonProperty("ResponseURL") abstract String getResponseUrl(); + @JsonProperty("ResponseURL") abstract void setResponseUrl(String responseUrl); + + // needed because jackson expects "stackId" instead of "StackId" + @JsonProperty("StackId") abstract String getStackId(); + @JsonProperty("StackId") abstract void setStackId(String stackId); + + // needed because jackson expects "requestId" instead of "RequestId" + @JsonProperty("RequestId") abstract String getRequestId(); + @JsonProperty("RequestId") abstract void setRequestId(String requestId); + + // needed because jackson expects "logicalResourceId" instead of "LogicalResourceId" + @JsonProperty("LogicalResourceId") abstract String getLogicalResourceId(); + @JsonProperty("LogicalResourceId") abstract void setLogicalResourceId(String logicalResourceId); + + // needed because jackson expects "resourceType" instead of "ResourceType" + @JsonProperty("ResourceType") abstract String getResourceType(); + @JsonProperty("ResourceType") abstract void setResourceType(String resourceType); + + // needed because jackson expects "resourceProperties" instead of "ResourceProperties" + @JsonProperty("ResourceProperties") abstract Map getResourceProperties(); + @JsonProperty("ResourceProperties") abstract void setResourceProperties(Map resourceProperties); + + // needed because jackson expects "oldResourceProperties" instead of "OldResourceProperties" + @JsonProperty("OldResourceProperties") abstract Map getOldResourceProperties(); + @JsonProperty("OldResourceProperties") abstract void setOldResourceProperties(Map oldResourceProperties); + +} diff --git a/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/CloudFrontEventMixin.java b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/CloudFrontEventMixin.java new file mode 100644 index 000000000..3b8f4a98c --- /dev/null +++ b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/CloudFrontEventMixin.java @@ -0,0 +1,18 @@ +/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ + +package com.amazonaws.services.lambda.runtime.serialization.events.mixins; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +/** + * Mixin for CloudFrontEvent + */ +public abstract class CloudFrontEventMixin { + + // needed because jackson expects "records" instead of "Records" + @JsonProperty("Records") abstract List getRecords(); + @JsonProperty("Records") abstract void setRecords(List records); + +} diff --git a/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/CloudWatchLogsEventMixin.java b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/CloudWatchLogsEventMixin.java new file mode 100644 index 000000000..a87c2c041 --- /dev/null +++ b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/CloudWatchLogsEventMixin.java @@ -0,0 +1,16 @@ +/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ + +package com.amazonaws.services.lambda.runtime.serialization.events.mixins; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Interface with Jackson annotations for CloudWatchLogsEvent + */ +public abstract class CloudWatchLogsEventMixin { + + // needed because jackson expects "awsLogs" instead of "awslogs" + @JsonProperty("awslogs") abstract Object getAwsLogs(); + @JsonProperty("awslogs") abstract void setAwsLogs(Object awsLogs); + +} diff --git a/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/CodeCommitEventMixin.java b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/CodeCommitEventMixin.java new file mode 100644 index 000000000..c9e11d1a8 --- /dev/null +++ b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/CodeCommitEventMixin.java @@ -0,0 +1,32 @@ +/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ + +package com.amazonaws.services.lambda.runtime.serialization.events.mixins; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +/** + * interface with Jackson annotations for CodeCommitEvent + */ +public abstract class CodeCommitEventMixin { + + // needed because Jackson expects "records" instead of "Records" + @JsonProperty("Records") abstract List getRecords(); + @JsonProperty("Records") abstract void setRecords(List records); + + public abstract class RecordMixin { + + // needed because Jackson expects "codeCommit" instead of "codeCommit" + @JsonProperty("codecommit") abstract Object getCodeCommit(); + @JsonProperty("codecommit") abstract void setCodeCommit(Object codeCommit); + // needed because Jackson expects "eventSourceArn" instead of "eventSourceARN" + @JsonProperty("eventSourceARN") abstract String getEventSourceArn(); + @JsonProperty("eventSourceARN") abstract void setEventSourceArn(String eventSourceArn); + // needed because Jackson expects "userIdentityArn" instead of "UserIdentityArn" + @JsonProperty("userIdentityARN") abstract String getUserIdentityArn(); + @JsonProperty("userIdentityARN") abstract void setUserIdentityArn(String userIdentityArn); + + } + +} diff --git a/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/ConnectEventMixin.java b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/ConnectEventMixin.java new file mode 100644 index 000000000..1645fdaee --- /dev/null +++ b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/ConnectEventMixin.java @@ -0,0 +1,103 @@ +/* Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ + +package com.amazonaws.services.lambda.runtime.serialization.events.mixins; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Map; + +/** + * Jackson annotations for ConnectEvent + */ +public abstract class ConnectEventMixin { + + // needed because Jackson expects "details" instead of "Details" + @JsonProperty("Details") abstract Map getDetails(); + @JsonProperty("Details") abstract void setDetails(Map details); + + // needed because Jackson expects "name" instead of "Name" + @JsonProperty("Name") abstract String getName(); + @JsonProperty("Name") abstract void setName(String name); + + public abstract class DetailsMixin { + + // needed because Jackson expects "contactData" instead of "ContactData" + @JsonProperty("ContactData") abstract Map getContactData(); + @JsonProperty("ContactData") abstract void setContactData(Map contactData); + + // needed because Jackson expects "parameters" instead of "Parameters" + @JsonProperty("Parameters") abstract Map getParameters(); + @JsonProperty("Parameters") abstract void setParameters(Map parameters); + } + + public abstract class ContactDataMixin { + + // needed because Jackson expects "attributes" instead of "Attributes" + @JsonProperty("Attributes") abstract Map getAttributes(); + @JsonProperty("Attributes") abstract void setAttributes(Map attributes); + + // needed because Jackson expects "channel" instead of "Channel" + @JsonProperty("Channel") abstract String getChannel(); + @JsonProperty("Channel") abstract void setChannel(String channel); + + // needed because Jackson expects "contactId" instead of "ContactId" + @JsonProperty("ContactId") abstract String getContactId(); + @JsonProperty("ContactId") abstract void setContactId(String contactId); + + // needed because Jackson expects "customerEndpoint" instead of "CustomerEndpoint" + @JsonProperty("CustomerEndpoint") abstract Map getCustomerEndpoint(); + @JsonProperty("CustomerEndpoint") abstract void setCustomerEndpoint(Map systemEndpoint); + + // needed because Jackson expects "initialContactId" instead of "InitialContactId" + @JsonProperty("InitialContactId") abstract String getInitialContactId(); + @JsonProperty("InitialContactId") abstract void setInitialContactId(String initialContactId); + + // needed because Jackson expects "initiationMethod" instead of "InitiationMethod" + @JsonProperty("InitiationMethod") abstract String getInitiationMethod(); + @JsonProperty("InitiationMethod") abstract void setInitiationMethod(String initiationMethod); + + // needed because Jackson expects "instanceARN" instead of "InstanceARN" + @JsonProperty("InstanceARN") abstract String getInstanceArn(); + @JsonProperty("InstanceARN") abstract void setInstanceArn(String instanceArn); + + // needed because Jackson expects "previousContactId" instead of "PreviousContactId" + @JsonProperty("PreviousContactId") abstract String getPreviousContactId(); + @JsonProperty("PreviousContactId") abstract void setPreviousContactId(String previousContactId); + + // needed because Jackson expects "queue" instead of "Queue" + @JsonProperty("Queue") abstract Map getQueue(); + @JsonProperty("Queue") abstract void setQueue(Map queue); + + // needed because Jackson expects "systemEndpoint" instead of "SystemEndpoint" + @JsonProperty("SystemEndpoint") abstract Map getSystemEndpoint(); + @JsonProperty("SystemEndpoint") abstract void setSystemEndpoint(Map systemEndpoint); + + } + + public abstract class CustomerEndpointMixin { + + // needed because Jackson expects "address" instead of "Address" + @JsonProperty("Address") abstract String getAddress(); + @JsonProperty("Address") abstract void setAddress(String previousContactId); + + // needed because Jackson expects "type" instead of "Type" + @JsonProperty("Type") abstract String getType(); + @JsonProperty("Type") abstract void setType(String type); + } + + public abstract class SystemEndpointMixin { + + // needed because Jackson expects "address" instead of "Address" + @JsonProperty("Address") abstract String getAddress(); + @JsonProperty("Address") abstract void setAddress(String previousContactId); + + // needed because Jackson expects "type" instead of "Type" + @JsonProperty("Type") abstract String getType(); + @JsonProperty("Type") abstract void setType(String type); + } + + public abstract class QueueMixin { + @JsonProperty("Name") abstract String getName(); + @JsonProperty("Name") abstract void setName(String name); + } +} diff --git a/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/DynamodbEventMixin.java b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/DynamodbEventMixin.java new file mode 100644 index 000000000..b692be40c --- /dev/null +++ b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/DynamodbEventMixin.java @@ -0,0 +1,88 @@ +/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ + +package com.amazonaws.services.lambda.runtime.serialization.events.mixins; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.nio.ByteBuffer; +import java.util.Date; +import java.util.List; +import java.util.Map; + +public abstract class DynamodbEventMixin { + + // needed because jackson expects "records" instead of "Records" + @JsonProperty("Records") abstract List getRecords(); + @JsonProperty("Records") abstract void setRecords(List records); + + public abstract class DynamodbStreamRecordMixin { + + // needed because Jackson cannot distinguish between Enum eventName from String eventName + @JsonProperty("eventName") abstract String getEventName(); + @JsonProperty("eventName") abstract void setEventName(String eventName); + // needed because Jackson expects "eventSourceArn" instead of "eventSourceARN" + @JsonProperty("eventSourceARN") abstract String getEventSourceArn(); + @JsonProperty("eventSourceARN") abstract void setEventSourceArn(String eventSourceArn); + } + + public abstract class StreamRecordMixin { + + // needed because Jackson expects "keys" instead of "Keys" + @JsonProperty("Keys") abstract Map getKeys(); + @JsonProperty("Keys") abstract void setKeys(Map keys); + // needed because Jackson expects "sizeBytes" instead of "SizeBytes" + @JsonProperty("SizeBytes") abstract Long getSizeBytes(); + @JsonProperty("SizeBytes") abstract void setSizeBytes(Long sizeBytes); + // needed because Jackson expects "sequenceNumber" instead of "SequenceNumber" + @JsonProperty("SequenceNumber") abstract String getSequenceNumber(); + @JsonProperty("SequenceNumber") abstract void setSequenceNumber(String sequenceNumber); + // needed because Jackson expects "streamViewType" instead of "StreamViewType" + @JsonProperty("StreamViewType") abstract String getStreamViewType(); + @JsonProperty("StreamViewType") abstract void setStreamViewType(String streamViewType); + // needed because Jackson expects "newImage" instead of "NewImage" + @JsonProperty("NewImage") abstract Map getNewImage(); + @JsonProperty("NewImage") abstract void setNewImage(Map newImage); + // needed because Jackson expects "oldImage" instead of "OldImage" + @JsonProperty("OldImage") abstract Map getOldImage(); + @JsonProperty("OldImage") abstract void setOldImage(Map oldImage); + // needed because Jackson expects "approximateCreationDateTime" instead of "ApproximateCreationDateTime" + @JsonProperty("ApproximateCreationDateTime") abstract Date getApproximateCreationDateTime(); + @JsonProperty("ApproximateCreationDateTime") abstract void setApproximateCreationDateTime(Date approximateCreationDateTime); + + } + + public abstract class AttributeValueMixin { + + // needed because Jackson expects "s" instead of "S" + @JsonProperty("S") abstract String getS(); + @JsonProperty("S") abstract void setS(String s); + // needed because Jackson expects "n" instead of "N" + @JsonProperty("N") abstract String getN(); + @JsonProperty("N") abstract void setN(String n); + // needed because Jackson expects "b" instead of "B" + @JsonProperty("B") abstract ByteBuffer getB(); + @JsonProperty("B") abstract void setB(ByteBuffer b); + // needed because Jackson expects "null" instead of "NULL" + @JsonProperty("NULL") abstract Boolean isNULL(); + @JsonProperty("NULL") abstract void setNULL(Boolean nU); + // needed because Jackson expects "bool" instead of "BOOL" + @JsonProperty("BOOL") abstract Boolean getBOOL(); + @JsonProperty("BOOL") abstract void setBOOL(Boolean bO); + // needed because Jackson expects "ss" instead of "SS" + @JsonProperty("SS") abstract List getSS(); + @JsonProperty("SS") abstract void setSS(List sS); + // needed because Jackson expects "ns" instead of "NS" + @JsonProperty("NS") abstract List getNS(); + @JsonProperty("NS") abstract void setNS(List nS); + // needed because Jackson expects "bs" instead of "BS" + @JsonProperty("BS") abstract List getBS(); + @JsonProperty("BS") abstract void setBS(List bS); + // needed because Jackson expects "m" instead of "M" + @JsonProperty("M") abstract Map getM(); + @JsonProperty("M") abstract void setM(Map val); + // needed because Jackson expects "l" instead of "L" + @JsonProperty("L") abstract List getL(); + @JsonProperty("L") abstract void setL(List val); + + } +} diff --git a/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/DynamodbTimeWindowEventMixin.java b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/DynamodbTimeWindowEventMixin.java new file mode 100644 index 000000000..93d87a32f --- /dev/null +++ b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/DynamodbTimeWindowEventMixin.java @@ -0,0 +1,14 @@ +/* + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + */ + +package com.amazonaws.services.lambda.runtime.serialization.events.mixins; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public abstract class DynamodbTimeWindowEventMixin extends DynamodbEventMixin { + + // needed because Jackson expects "eventSourceArn" instead of "eventSourceARN" + @JsonProperty("eventSourceARN") abstract String getEventSourceArn(); + @JsonProperty("eventSourceARN") abstract void setEventSourceArn(String eventSourceArn); +} diff --git a/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/KinesisEventMixin.java b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/KinesisEventMixin.java new file mode 100644 index 000000000..0da9331b6 --- /dev/null +++ b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/KinesisEventMixin.java @@ -0,0 +1,23 @@ +/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ + +package com.amazonaws.services.lambda.runtime.serialization.events.mixins; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +public abstract class KinesisEventMixin { + + // needed because Jackson expects "records" instead of "Records" + @JsonProperty("Records") abstract List getRecords(); + @JsonProperty("Records") abstract void setRecords(List records); + + public abstract class RecordMixin { + + // needed because Jackson cannot distinguish between Enum encryptionType and String encryptionType + @JsonProperty("encryptionType") abstract String getEncryptionType(); + @JsonProperty("encryptionType") abstract void setEncryptionType(String encryptionType); + + } + +} diff --git a/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/KinesisTimeWindowEventMixin.java b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/KinesisTimeWindowEventMixin.java new file mode 100644 index 000000000..374a23bc1 --- /dev/null +++ b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/KinesisTimeWindowEventMixin.java @@ -0,0 +1,14 @@ +/* + * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. + */ + +package com.amazonaws.services.lambda.runtime.serialization.events.mixins; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public abstract class KinesisTimeWindowEventMixin extends KinesisEventMixin { + + // needed because Jackson expects "eventSourceArn" instead of "eventSourceARN" + @JsonProperty("eventSourceARN") abstract String getEventSourceArn(); + @JsonProperty("eventSourceARN") abstract void setEventSourceArn(String eventSourceArn); +} diff --git a/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/SNSEventMixin.java b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/SNSEventMixin.java new file mode 100644 index 000000000..5f1821bb1 --- /dev/null +++ b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/SNSEventMixin.java @@ -0,0 +1,23 @@ +/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ + +package com.amazonaws.services.lambda.runtime.serialization.events.mixins; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +public abstract class SNSEventMixin { + + // needed because Jackson expects "records" instead of "Records" + @JsonProperty("Records") abstract List getRecords(); + @JsonProperty("Records") abstract void setRecords(List records); + + public abstract class SNSRecordMixin { + + // needed because Jackson expects "getSns" instead of "getSNS" + @JsonProperty("Sns") abstract Object getSNS(); + @JsonProperty("Sns") abstract void setSns(Object sns); + + } + +} diff --git a/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/SQSEventMixin.java b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/SQSEventMixin.java new file mode 100644 index 000000000..fad6c9c92 --- /dev/null +++ b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/SQSEventMixin.java @@ -0,0 +1,22 @@ +/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ + +package com.amazonaws.services.lambda.runtime.serialization.events.mixins; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +public abstract class SQSEventMixin { + + // Needed because Jackson expects "records" instead of "Records" + @JsonProperty("Records") abstract List getRecords(); + @JsonProperty("Records") abstract void setRecords(List records); + + public abstract class SQSMessageMixin { + + // needed because Jackson expects "eventSourceArn" instead of "eventSourceARN" + @JsonProperty("eventSourceARN") abstract String getEventSourceArn(); + @JsonProperty("eventSourceARN") abstract void setEventSourceArn(String eventSourceArn); + } + +} diff --git a/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/ScheduledEventMixin.java b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/ScheduledEventMixin.java new file mode 100644 index 000000000..3bf378e7a --- /dev/null +++ b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/ScheduledEventMixin.java @@ -0,0 +1,16 @@ +/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ + +package com.amazonaws.services.lambda.runtime.serialization.events.mixins; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Jackson annotations for ScheduledEvent + */ +public abstract class ScheduledEventMixin { + + // needed because Jackson expects "detailType" instead of "detail-type" + @JsonProperty("detail-type") abstract String getDetailType(); + @JsonProperty("detail-type") abstract void setDetailType(String detailType); + +} diff --git a/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/SecretsManagerRotationEventMixin.java b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/SecretsManagerRotationEventMixin.java new file mode 100644 index 000000000..1b862e8cb --- /dev/null +++ b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/mixins/SecretsManagerRotationEventMixin.java @@ -0,0 +1,28 @@ +/* Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ + +package com.amazonaws.services.lambda.runtime.serialization.events.mixins; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Jackson annotations for SecretsManagerRotationEvent + */ + +public abstract class SecretsManagerRotationEventMixin { + + // needed because Jackson expects "step" instead of "Step" + @JsonProperty("Step") abstract String getStep(); + @JsonProperty("Step") abstract void setStep(String step); + + // needed because Jackson expects "secretId" instead of "SecretId" + @JsonProperty("SecretId") abstract String getSecretId(); + @JsonProperty("SecretId") abstract void setSecretId(String secretId); + + // needed because Jackson expects "clientRequestToken" instead of "ClientRequestToken" + @JsonProperty("ClientRequestToken") abstract String getClientRequestToken(); + @JsonProperty("ClientRequestToken") abstract void setClientRequestToken(String clientRequestToken); + + // needed because Jackson expects "rotationToken" instead of "RotationToken" + @JsonProperty("RotationToken") abstract String getRotationToken(); + @JsonProperty("RotationToken") abstract void setRotationToken(String rotationToken); +} diff --git a/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/modules/DateModule.java b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/modules/DateModule.java new file mode 100644 index 000000000..acc8bde2a --- /dev/null +++ b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/modules/DateModule.java @@ -0,0 +1,70 @@ +/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ + +package com.amazonaws.services.lambda.runtime.serialization.events.modules; + +import java.io.IOException; +import java.util.Date; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.json.PackageVersion; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.module.SimpleModule; + +/** + * The AWS API represents a date as a double (fractional seconds since epoch). + * Java's Date uses a long (milliseconds since epoch). This module translates + * between the two formats. + * + *

+ * Round-trip caveats: The serializer always writes via + * {@link JsonGenerator#writeNumber(double)}, so integer epochs + * (e.g. {@code 1428537600}) round-trip as decimal ({@code 1.4285376E9}). + * Sub-millisecond precision is lost because {@link java.util.Date} + * has milliseconds precision. + *

+ * + * This class is copied from LambdaEventBridgeservice + * com.amazon.aws.lambda.stream.ddb.DateModule + */ +public class DateModule extends SimpleModule { + private static final long serialVersionUID = 1L; + + public static final class Serializer extends JsonSerializer { + @Override + public void serialize(Date date, JsonGenerator generator, SerializerProvider serializers) throws IOException { + if (date != null) { + generator.writeNumber(millisToSeconds(date.getTime())); + } + } + } + + public static final class Deserializer extends JsonDeserializer { + @Override + public Date deserialize(JsonParser parser, DeserializationContext context) throws IOException { + double dateSeconds = parser.getValueAsDouble(); + if (dateSeconds == 0.0) { + return null; + } else { + return new Date((long) secondsToMillis(dateSeconds)); + } + } + } + + private static double millisToSeconds(double millis) { + return millis / 1000.0; + } + + private static double secondsToMillis(double seconds) { + return seconds * 1000.0; + } + + public DateModule() { + super(PackageVersion.VERSION); + addSerializer(Date.class, new Serializer()); + addDeserializer(Date.class, new Deserializer()); + } +} diff --git a/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/modules/DateTimeModule.java b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/modules/DateTimeModule.java new file mode 100644 index 000000000..a02857e00 --- /dev/null +++ b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/modules/DateTimeModule.java @@ -0,0 +1,78 @@ +/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ + +package com.amazonaws.services.lambda.runtime.serialization.events.modules; + +import com.amazonaws.services.lambda.runtime.serialization.util.SerializeUtil; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.datatype.joda.JodaModule; + +import java.io.IOException; + +/** + * Class that is used to load customer DateTime class + */ +public class DateTimeModule extends JodaModule { + + /** + * creates a DateTimeModule using customer class loader to pull org.joda.time.DateTime + */ + public DateTimeModule(ClassLoader classLoader) { + // Workaround not to let maven shade plugin relocating string literals https://issues.apache.org/jira/browse/MSHADE-156 + Class dateTimeClass = SerializeUtil.loadCustomerClass("com.amazonaws.lambda.unshade.thirdparty.org.joda.time.DateTime", classLoader); + this.addSerializer(dateTimeClass, getSerializer(dateTimeClass, classLoader)); + this.addDeserializer(dateTimeClass, getDeserializer(dateTimeClass)); + } + + /** + * @param refers to type org.joda.time.DateTime + * @param dateTimeClass org.joda.time.DateTime class of the customer + * @param classLoader classLoader that's used to load any DateTime classes + * @return JsonSerializer with generic DateTime + */ + private JsonSerializer getSerializer(Class dateTimeClass, ClassLoader classLoader) { + return new JsonSerializer() { + + /** + * @param dateTime customer DateTime class + * @param jsonGenerator json generator + * @param serializerProvider serializer provider + * @throws IOException when unable to write + * @throws JsonProcessingException when unable to parse + */ + @Override + public void serialize(T dateTime, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) + throws IOException, JsonProcessingException { + jsonGenerator.writeString(SerializeUtil.serializeDateTime(dateTime, classLoader)); + } + }; + } + + /** + * @param dateTimeClass org.joda.time.DateTime class of the customer + * @param refers to type org.joda.time.DateTime + * @return JsonDeserializer with generic DateTime + */ + private JsonDeserializer getDeserializer(Class dateTimeClass) { + return new JsonDeserializer() { + + /** + * @param jsonParser json parser + * @param deserializationContext deserialization context + * @return DateTime instance + * @throws IOException error when reading + * @throws JsonProcessingException error when processing json + */ + @Override + public T deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) + throws IOException, JsonProcessingException { + return SerializeUtil.deserializeDateTime(dateTimeClass, jsonParser.getValueAsString()); + } + }; + } +} diff --git a/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/serializers/OrgJsonSerializer.java b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/serializers/OrgJsonSerializer.java new file mode 100644 index 000000000..e558dc5d3 --- /dev/null +++ b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/serializers/OrgJsonSerializer.java @@ -0,0 +1,47 @@ +/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ + +package com.amazonaws.services.lambda.runtime.serialization.events.serializers; + +import com.amazonaws.services.lambda.runtime.serialization.PojoSerializer; + +import java.io.InputStream; +import java.io.OutputStream; + +/** + * Interface for event serializers that use org json + */ +public interface OrgJsonSerializer extends PojoSerializer { + + /** + * @param eventClass event class object + * @return OrgJsonSerializer with event type + */ + OrgJsonSerializer withClass(Class eventClass); + + /** + * @param classLoader to use if the implementation needs to load any classes + * @return OrgJsonSerializer with the supplied classLoader + */ + OrgJsonSerializer withClassLoader(ClassLoader classLoader); + + /** + * defined in PojoSerializer + * @param input input stream + * @return deserialized object of type T + */ + T fromJson(InputStream input); + + /** + * defined in PojoSerializer + * @param input String input + * @return deserialized object of type T + */ + T fromJson(String input); + + /** + * defined in PojoSerializer + * @param value instance of type T to be serialized + * @param output OutputStream to serialize object to + */ + void toJson(T value, OutputStream output); +} diff --git a/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/serializers/S3EventSerializer.java b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/serializers/S3EventSerializer.java new file mode 100644 index 000000000..c833abcc1 --- /dev/null +++ b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/events/serializers/S3EventSerializer.java @@ -0,0 +1,555 @@ +/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ + +package com.amazonaws.services.lambda.runtime.serialization.events.serializers; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.UncheckedIOException; +import java.io.Writer; +import java.util.ArrayList; +import java.util.List; + +import com.amazonaws.services.lambda.runtime.serialization.util.Functions; +import com.amazonaws.services.lambda.runtime.serialization.util.ReflectUtil; +import com.amazonaws.services.lambda.runtime.serialization.util.SerializeUtil; +import org.json.JSONArray; +import org.json.JSONObject; + +/** + * Serializer for S3 event + * NOTE: Because the s3 event class provided by the SDK does not play well with Jackson through a class laoder, + * this class uses the low level org json library to serialize and deserialize the event. If new events are added + * that do not work well with Jackson or GSON, this is the fallback method that will always work but is more verbose. + */ +public class S3EventSerializer implements OrgJsonSerializer { + + /** + * As part of https://github.com/aws/aws-lambda-java-libs/issues/74 the `com.amazonaws.services.s3.event.S3EventNotification` + * class used by the aws-lambda-java-events library was adapted from the AWSS3JavaClient library into + * `com.amazonaws.services.lambda.runtime.events.models.s3.S3EventNotification`, hence the need to support both classes + * in the runtime + * @see com.amazonaws.services.lambda.runtime.events.S3Event; + * @see com.amazonaws.services.lambda.runtime.events.models.s3.S3EventNotification; + * @see com.amazonaws.services.s3.event.S3EventNotification; + */ + private static final String S3_EVENT_NOTIFICATION_CLASS_V3 = "com.amazonaws.services.lambda.runtime.events.models.s3.S3EventNotification"; + private static final String S3_EVENT_NOTIFICATION_CLASS_V2 = "com.amazonaws.services.s3.event.S3EventNotification"; + + /** + * S3 event class + * @see com.amazonaws.services.lambda.runtime.events.S3Event; + * @see com.amazonaws.services.lambda.runtime.events.models.s3.S3EventNotification; + * @see com.amazonaws.services.s3.event.S3EventNotification; + */ + private Class eventClass; + + /** + * ClassLoader to be used when loading S3 event classes + */ + private ClassLoader classLoader; + + /** + * Construct s3Event Serialize from specific s3 event class from user + * @param eventClass s3 event class + * @see com.amazonaws.services.lambda.runtime.events.S3Event; + * @see com.amazonaws.services.lambda.runtime.events.models.s3.S3EventNotification; + * @see com.amazonaws.services.s3.event.S3EventNotification; + */ + @Override + public S3EventSerializer withClass(Class eventClass) { + this.eventClass = eventClass; + return this; + } + + /** + * Sets the ClassLoader that will be used to load S3 event classes + * @param classLoader - ClassLoader that S3 event classes will be loaded from + */ + @Override + public S3EventSerializer withClassLoader(ClassLoader classLoader) { + this.classLoader = classLoader; + return this; + } + + + /** + * deserialize an instance of an s3 event from an input stream + * @param input InputStream reading from + * @return S3Event Object + */ + public T fromJson(InputStream input) { + return fromJson(SerializeUtil.convertStreamToString(input)); + } + + /** + * deserialize an instance of an s3 event from a string + * @param input String with JSON + * @return s3Event object + */ + public T fromJson(String input) { + JSONObject jsonObject = new JSONObject((input)); + return deserializeEvent(jsonObject); + } + + /** + * serialize an S3 event object to the output stream + * @param value S3 event object + * @param output OutputStream serializing to + */ + public void toJson(T value, OutputStream output) { + JSONObject jsonObject; + try { + // Try to load newer version of S3EventNotification from aws-lambda-java-events v3+ + Class eventNotificationRecordClass = SerializeUtil.loadCustomerClass( + S3_EVENT_NOTIFICATION_CLASS_V3 + "$S3EventNotificationRecord", classLoader); + jsonObject = serializeEvent(eventNotificationRecordClass, value, S3_EVENT_NOTIFICATION_CLASS_V3); + + } catch (Exception ex) { + // Fallback to aws-lambda-java-events pre-v3 (relies on aws-s3-sdk) + Class eventNotificationRecordClass = SerializeUtil.loadCustomerClass( + S3_EVENT_NOTIFICATION_CLASS_V2 + "$S3EventNotificationRecord", classLoader); + jsonObject = serializeEvent(eventNotificationRecordClass, value, S3_EVENT_NOTIFICATION_CLASS_V2); + } + + // Writer in try block so that writer gets flushed and closed + try (Writer writer = new OutputStreamWriter(output)) { + writer.write(jsonObject.toString()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + /** + * serialize an s3 event + * @param eventNotificationRecordClass class holding the s3 event notification record + * @param value s3 event object + * @param baseClassName base class name + * @return JSONObject that contains s3 event + */ + @SuppressWarnings({"unchecked"}) + private JSONObject serializeEvent(Class eventNotificationRecordClass, T value, String baseClassName) { + JSONObject jsonObject = new JSONObject(); + Functions.R0 getRecordsMethod = ReflectUtil.bindInstanceR0(value, "getRecords", true, List.class); + jsonObject.put("Records", serializeEventNotificationRecordList(getRecordsMethod.call(), eventNotificationRecordClass, baseClassName)); + return jsonObject; + } + + /** + * deserialize an s3 event + * @param jsonObject JSONObject with s3 data + * @return S3 Event Object + */ + @SuppressWarnings({"unchecked"}) + private T deserializeEvent(JSONObject jsonObject) { + Functions.R1 constructor = ReflectUtil.loadConstructor1(eventClass, true, List.class); + JSONArray records = jsonObject.optJSONArray("Records"); + try { + // Try to load newer version of S3EventNotification from aws-lambda-java-events v3+ + Class recordClass = SerializeUtil.loadCustomerClass( + S3_EVENT_NOTIFICATION_CLASS_V3 + "$S3EventNotificationRecord", classLoader); + return (T) constructor.call(deserializeEventNotificationRecordList(records, recordClass, + S3_EVENT_NOTIFICATION_CLASS_V3)); + + } catch (Exception ex) { + // Fallback to aws-lambda-java-events pre-v3 (relies on aws-s3-sdk) + Class eventNotificationRecordClass = SerializeUtil.loadCustomerClass( + S3_EVENT_NOTIFICATION_CLASS_V2 + "$S3EventNotificationRecord", classLoader); + return (T) constructor.call(deserializeEventNotificationRecordList(records, eventNotificationRecordClass, + S3_EVENT_NOTIFICATION_CLASS_V2)); + } + } + + /** + * serialize an s3 event notification record list + * @param eventNotificationRecords List of event notification records + * @param EventNotificationRecord + * @return JSONArray with s3 event records + */ + @SuppressWarnings({"unchecked"}) + private JSONArray serializeEventNotificationRecordList(List eventNotificationRecords, + Class eventNotificationRecordClass, + String baseClassName) { + JSONArray jsonRecords = new JSONArray(); + for (Object eventNotificationRecord: eventNotificationRecords) { + jsonRecords.put(serializeEventNotificationRecord((A) eventNotificationRecord, baseClassName)); + } + return jsonRecords; + } + + /** + * deserialize an s3 event notification record + * @param jsonRecords JSONArray of event notification records + * @param eventNotificiationRecordClass Event notification record class + * @param Event notification record type + * @return List of event notification records + */ + @SuppressWarnings({"unchecked"}) + private List deserializeEventNotificationRecordList(JSONArray jsonRecords, + Class eventNotificiationRecordClass, + String baseClassName) { + if (jsonRecords == null) { + jsonRecords = new JSONArray(); + } + Class s3EntityClass = SerializeUtil.loadCustomerClass(baseClassName + "$S3Entity", classLoader); + Class s3BucketClass = SerializeUtil.loadCustomerClass(baseClassName + "$S3BucketEntity", classLoader); + Class s3ObjectClass = SerializeUtil.loadCustomerClass(baseClassName + "$S3ObjectEntity", classLoader); + Class requestParametersClass = SerializeUtil.loadCustomerClass(baseClassName + "$RequestParametersEntity", classLoader); + Class responseElementsClass = SerializeUtil.loadCustomerClass(baseClassName + "$ResponseElementsEntity", classLoader); + Class userIdentityClass = SerializeUtil.loadCustomerClass(baseClassName + "$UserIdentityEntity", classLoader); + + List records = new ArrayList<>(); + for (int i=0; i < jsonRecords.length(); i++) { + records.add((A) deserializeEventNotificationRecord( + jsonRecords.getJSONObject(i), eventNotificiationRecordClass, s3EntityClass, s3BucketClass, + s3ObjectClass, requestParametersClass, responseElementsClass, userIdentityClass)); + } + return records; + } + + /** + * serialize an s3 event notification record + * @param eventNotificationRecord Event notification record + * @param Event notification record type + * @return JSONObject + */ + private JSONObject serializeEventNotificationRecord(A eventNotificationRecord, String baseClassName) { + // reflect load all the classes we need + Class s3EntityClass = SerializeUtil.loadCustomerClass(baseClassName + "$S3Entity", classLoader); + Class requestParametersClass = SerializeUtil.loadCustomerClass(baseClassName + "$RequestParametersEntity", classLoader); + Class responseElementsClass = SerializeUtil.loadCustomerClass(baseClassName + "$ResponseElementsEntity", classLoader); + Class userIdentityClass = SerializeUtil.loadCustomerClass(baseClassName + "$UserIdentityEntity", classLoader); + // Workaround not to let maven shade plugin relocating string literals https://issues.apache.org/jira/browse/MSHADE-156 + Class dateTimeClass = SerializeUtil.loadCustomerClass("com.amazonaws.lambda.unshade.thirdparty.org.joda.time.DateTime", classLoader); + // serialize object + JSONObject jsonObject = new JSONObject(); + Functions.R0 getAwsRegionMethod = + ReflectUtil.bindInstanceR0(eventNotificationRecord, "getAwsRegion", true, String.class); + jsonObject.put("awsRegion", getAwsRegionMethod.call()); + Functions.R0 getEventNameMethod = + ReflectUtil.bindInstanceR0(eventNotificationRecord, "getEventName", true, String.class); + jsonObject.put("eventName", getEventNameMethod.call()); + Functions.R0 getEventSourceMethod = + ReflectUtil.bindInstanceR0(eventNotificationRecord, "getEventSource", true, String.class); + jsonObject.put("eventSource", getEventSourceMethod.call()); + Functions.R0 getEventTimeMethod = + ReflectUtil.bindInstanceR0(eventNotificationRecord, "getEventTime", true, dateTimeClass); + jsonObject.put("eventTime", SerializeUtil.serializeDateTime(getEventTimeMethod.call(), classLoader)); + Functions.R0 getEventVersionMethod = + ReflectUtil.bindInstanceR0(eventNotificationRecord, "getEventVersion", true, String.class); + jsonObject.put("eventVersion", getEventVersionMethod.call()); + Functions.R0 getRequestParametersMethod = + ReflectUtil.bindInstanceR0(eventNotificationRecord, "getRequestParameters", true, requestParametersClass); + jsonObject.put("requestParameters", serializeRequestParameters(getRequestParametersMethod.call())); + Functions.R0 getResponseElementsMethod = + ReflectUtil.bindInstanceR0(eventNotificationRecord, "getResponseElements", true, responseElementsClass); + jsonObject.put("responseElements", serializeResponseElements(getResponseElementsMethod.call())); + Functions.R0 getS3EntityMethod = + ReflectUtil.bindInstanceR0(eventNotificationRecord, "getS3", true, s3EntityClass); + jsonObject.put("s3", serializeS3Entity(getS3EntityMethod.call(), baseClassName)); + Functions.R0 getUserIdentityMethod = + ReflectUtil.bindInstanceR0(eventNotificationRecord, "getUserIdentity", true, userIdentityClass); + jsonObject.put("userIdentity", serializeUserIdentity(getUserIdentityMethod.call())); + return jsonObject; + } + + /** + * deserialize an event notification record + * NOTE: Yes there are a lot of generics. They are needed for the compiler to correctly associate instance types + * with class types + * @param jsonObject JSONObject to deserialize from + * @param eventNotificationRecordClass event notification record class + * @param s3EntityClass s3 entity class + * @param s3BucketClass s3 bucket class + * @param s3ObjectClass s3 object class + * @param requestParametersClass request parameters class + * @param responseElementsClass response elements class + * @param userIdentityClass user identity class + * @param event notification record type + * @param s3 entity type + * @param s3 bucket type + * @param s3 object type + * @param request parameters type + * @param response elements type + * @param user identity class + * @return event notification record object + */ + private A deserializeEventNotificationRecord(JSONObject jsonObject, + Class eventNotificationRecordClass, + Class s3EntityClass, + Class s3BucketClass, + Class s3ObjectClass, + Class requestParametersClass, + Class responseElementsClass, + Class userIdentityClass) { + if (jsonObject == null) { + jsonObject = new JSONObject(); + } + String awsRegion = jsonObject.optString("awsRegion"); + String eventName = jsonObject.optString("eventName"); + String eventSource = jsonObject.optString("eventSource"); + String eventTime = jsonObject.optString("eventTime"); + String eventVersion = jsonObject.optString("eventVersion"); + E requestParameters = deserializeRequestParameters(jsonObject.optJSONObject("requestParameters"), requestParametersClass); + F responseElements = deserializeResponseElements(jsonObject.optJSONObject("responseElements"), responseElementsClass); + B s3 = deserializeS3Entity(jsonObject.optJSONObject("s3"), s3EntityClass, s3BucketClass, s3ObjectClass, userIdentityClass); + G userIdentity = deserializeUserIdentity(jsonObject.optJSONObject("userIdentity"), userIdentityClass); + Functions.R9 constructor = + ReflectUtil.loadConstuctor9(eventNotificationRecordClass, true, String.class, String.class, + String.class, String.class, String.class, requestParametersClass, responseElementsClass, + s3EntityClass, userIdentityClass); + return constructor.call(awsRegion, eventName, eventSource, eventTime, eventVersion, requestParameters, + responseElements, s3, userIdentity); + } + + /** + * serialize an s3 entity + * @param s3Entity S3 entity object + * @param S3 entity type + * @return JSONObject with serialized s3 entity + */ + private JSONObject serializeS3Entity(A s3Entity, String baseClassName) { + Class s3BucketClass = SerializeUtil.loadCustomerClass(baseClassName + "$S3BucketEntity", classLoader); + Class s3ObjectClass = SerializeUtil.loadCustomerClass(baseClassName + "$S3ObjectEntity", classLoader); + JSONObject jsonObject = new JSONObject(); + Functions.R0 getConfigurationIdMethod = + ReflectUtil.bindInstanceR0(s3Entity, "getConfigurationId", true, String.class); + jsonObject.put("configurationId", getConfigurationIdMethod.call()); + Functions.R0 getBucketMethod = + ReflectUtil.bindInstanceR0(s3Entity, "getBucket", true, s3BucketClass); + jsonObject.put("bucket", serializeS3Bucket(getBucketMethod.call(), baseClassName)); + Functions.R0 getObjectMethod = + ReflectUtil.bindInstanceR0(s3Entity, "getObject", true, s3ObjectClass); + jsonObject.put("object", serializeS3Object(getObjectMethod.call())); + Functions.R0 getSchemaVersionMethod = + ReflectUtil.bindInstanceR0(s3Entity, "getS3SchemaVersion", true, String.class); + jsonObject.put("s3SchemaVersion", getSchemaVersionMethod.call()); + return jsonObject; + } + + /** + * deserialize an S3 entity object + * @param jsonObject json object to deserialize from + * @param s3EntityClass s3 entity class + * @param s3BucketClass s3 bucket class + * @param s3ObjectClass s3 object class + * @param userIdentityClass s3 user identity class + * @param s3 entity type + * @param s3 bucket type + * @param s3 object type + * @param s3 user identity type + * @return s3 entity object + */ + private A deserializeS3Entity(JSONObject jsonObject, Class s3EntityClass, Class s3BucketClass, + Class s3ObjectClass, Class userIdentityClass) { + if (jsonObject == null) { + jsonObject = new JSONObject(); + } + String configurationId = jsonObject.optString("configurationId"); + B bucket = deserializeS3Bucket(jsonObject.optJSONObject("bucket"), s3BucketClass, userIdentityClass); + C object = deserializeS3Object(jsonObject.optJSONObject("object"), s3ObjectClass); + String schemaVersion = jsonObject.optString("s3SchemaVersion"); + Functions.R4 constructor = + ReflectUtil.loadConstuctor4(s3EntityClass, true, String.class, s3BucketClass, s3ObjectClass, String.class); + return constructor.call(configurationId, bucket, object, schemaVersion); + } + + /** + * serialize an s3 bucket object + * @param s3Bucket S3 bucket object + * @param S3 bucket type + * @return JSONObject + */ + private JSONObject serializeS3Bucket(A s3Bucket, String baseClassName) { + Class userIdentityClass = SerializeUtil.loadCustomerClass(baseClassName + "$UserIdentityEntity", classLoader); + JSONObject jsonObject = new JSONObject(); + Functions.R0 getNameMethod = + ReflectUtil.bindInstanceR0(s3Bucket, "getName", true, String.class); + jsonObject.put("name", getNameMethod.call()); + Functions.R0 getOwnerIdentityMethod = + ReflectUtil.bindInstanceR0(s3Bucket, "getOwnerIdentity", true, userIdentityClass); + jsonObject.put("ownerIdentity", serializeUserIdentity(getOwnerIdentityMethod.call())); + Functions.R0 getArnMethod = ReflectUtil.bindInstanceR0(s3Bucket, "getArn", true, String.class); + jsonObject.put("arn", getArnMethod.call()); + return jsonObject; + } + + /** + * deserialize an s3 bucket object + * @param jsonObject JSONObject to deserialize from + * @param s3BucketClass S3Bucket class + * @param userIdentityClass user identity class + * @param s3 bucket type + * @param user identity type + * @return s3 bucket object + */ + private A deserializeS3Bucket(JSONObject jsonObject, Class s3BucketClass, Class userIdentityClass) { + if (jsonObject == null) { + jsonObject = new JSONObject(); + } + String name = jsonObject.optString("name"); + B ownerIdentity = deserializeUserIdentity(jsonObject.optJSONObject("ownerIdentity"), userIdentityClass); + String arn = jsonObject.optString("arn"); + Functions.R3 constructor = + ReflectUtil.loadConstuctor3(s3BucketClass, true, String.class, userIdentityClass, String.class); + return constructor.call(name, ownerIdentity, arn); + } + + /** + * serialize an s3 object + * @param s3Object s3Object object + * @param s3 object type + * @return s3Object object + */ + private JSONObject serializeS3Object(A s3Object) { + JSONObject jsonObject = new JSONObject(); + Functions.R0 getKeyMethod = + ReflectUtil.bindInstanceR0(s3Object, "getKey", true, String.class); + jsonObject.put("key", getKeyMethod.call()); + Functions.R0 getSizeMethod = + ReflectUtil.bindInstanceR0(s3Object, "getSizeAsLong", true, Long.class); + jsonObject.put("size", getSizeMethod.call().longValue()); + Functions.R0 getETagMethod = + ReflectUtil.bindInstanceR0(s3Object, "geteTag", true, String.class); + jsonObject.put("eTag", getETagMethod.call()); + Functions.R0 getVersionIdMethod = + ReflectUtil.bindInstanceR0(s3Object, "getVersionId", true, String.class); + jsonObject.put("versionId", getVersionIdMethod.call()); + // legacy s3 event models do not have these methods + try { + Functions.R0 getUrlEncodedKeyMethod = + ReflectUtil.bindInstanceR0(s3Object, "getUrlDecodedKey", true, String.class); + jsonObject.put("urlDecodedKey", getUrlEncodedKeyMethod.call()); + Functions.R0 getSequencerMethod = + ReflectUtil.bindInstanceR0(s3Object, "getSequencer", true, String.class); + jsonObject.put("sequencer", getSequencerMethod.call()); + } catch (Exception ignored) {} + return jsonObject; + } + + /** + * deserialize an s3Object + * @param jsonObject json object to deserialize from + * @param s3ObjectClass class of s3Object + * @param s3Object type + * @return s3Object object + */ + private A deserializeS3Object(JSONObject jsonObject, Class s3ObjectClass) { + if (jsonObject == null) { + jsonObject = new JSONObject(); + } + String key = jsonObject.optString("key"); + Long size = jsonObject.optLong("size"); + String eTag = jsonObject.optString("eTag"); + String versionId = jsonObject.optString("versionId"); + String sequencer = jsonObject.optString("sequencer"); + // legacy s3 event uses constructor in catch statement + try { + Functions.R5 constructor = + ReflectUtil.loadConstuctor5(s3ObjectClass, true, String.class, Long.class, String.class, String.class, String.class); + return constructor.call(key, size, eTag, versionId, sequencer); + } catch (Exception e) { + Functions.R4 constructor = + ReflectUtil.loadConstuctor4(s3ObjectClass, true, String.class, Long.class, String.class, String.class); + return constructor.call(key, size, eTag, versionId); + } + } + + /** + * serialize an s3 user identity + * @param userIdentity user identity object + * @param user identity type + * @return JSONObject with serialized user identity + */ + private JSONObject serializeUserIdentity(A userIdentity) { + JSONObject jsonObject = new JSONObject(); + Functions.R0 getPrincipalIdMethod = + ReflectUtil.bindInstanceR0(userIdentity, "getPrincipalId", true, String.class); + jsonObject.put("principalId", getPrincipalIdMethod.call()); + return jsonObject; + } + + /** + * deserialize a user identity + * @param jsonObject JSONObject to deserialize from + * @param userIdentityClass User Identity Class + * @param User Identity Type + * @return User Identity Object + */ + private A deserializeUserIdentity(JSONObject jsonObject, Class userIdentityClass) { + if (jsonObject == null) { + jsonObject = new JSONObject(); + } + String principalId = jsonObject.optString("principalId"); + Functions.R1 constructor = + ReflectUtil.loadConstructor1(userIdentityClass, true, String.class); + return constructor.call(principalId); + } + + /** + * serialize request parameters + * @param requestParameters request parameters object + * @param request parameters type + * @return JSONObject with serialized request parameters + */ + private JSONObject serializeRequestParameters(A requestParameters) { + JSONObject jsonObject = new JSONObject(); + Functions.R0 getSourceIpMethod = + ReflectUtil.bindInstanceR0(requestParameters, "getSourceIPAddress", true, String.class); + jsonObject.put("sourceIPAddress", getSourceIpMethod.call()); + return jsonObject; + } + + /** + * deserialize request parameters + * @param jsonObject JSONObject to deserialize from + * @param requestParametersClass RequestParameters class + * @param RequestParameters type + * @return RequestParameters object + */ + private A deserializeRequestParameters(JSONObject jsonObject, Class requestParametersClass) { + if (jsonObject == null) { + jsonObject = new JSONObject(); + } + String sourceIpAddress = jsonObject.optString("sourceIPAddress"); + Functions.R1 constructor = ReflectUtil.loadConstructor1(requestParametersClass, true, String.class); + return constructor.call(sourceIpAddress); + } + + /** + * serialize response elements object + * @param responseElements response elements object + * @param response elements type + * @return JSONObject with serialized responseElements + */ + private JSONObject serializeResponseElements(A responseElements) { + JSONObject jsonObject = new JSONObject(); + Functions.R0 getXAmzId2Method = + ReflectUtil.bindInstanceR0(responseElements, "getxAmzId2", true, String.class); + jsonObject.put("x-amz-id-2", getXAmzId2Method.call()); + Functions.R0 getXAmzRequestId = + ReflectUtil.bindInstanceR0(responseElements, "getxAmzRequestId", true, String.class); + jsonObject.put("x-amz-request-id", getXAmzRequestId.call()); + return jsonObject; + } + + /** + * deserialize response elements + * @param jsonObject JSONObject deserializing from + * @param responseElementsClass response elements class + * @param response elements type + * @return Response elements object + */ + private A deserializeResponseElements(JSONObject jsonObject, Class responseElementsClass) { + if (jsonObject == null) { + jsonObject = new JSONObject(); + } + String xAmzId2 = jsonObject.optString("x-amz-id-2"); + String xAmzRequestId = jsonObject.optString("x-amz-request-id"); + Functions.R2 constructor = + ReflectUtil.loadConstructor2(responseElementsClass, true, String.class, String.class); + return constructor.call(xAmzId2, xAmzRequestId); + } + +} diff --git a/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/factories/GsonFactory.java b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/factories/GsonFactory.java new file mode 100644 index 000000000..a2adeb2e4 --- /dev/null +++ b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/factories/GsonFactory.java @@ -0,0 +1,125 @@ +/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ + +package com.amazonaws.services.lambda.runtime.serialization.factories; + +import java.io.OutputStream; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.StringReader; +import java.io.OutputStreamWriter; +import java.io.IOException; +import java.io.EOFException; +import java.io.UncheckedIOException; +import java.lang.reflect.Type; + +import com.amazonaws.services.lambda.runtime.serialization.PojoSerializer; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.TypeAdapter; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonWriter; +import com.google.gson.stream.JsonReader; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +public class GsonFactory implements PojoSerializerFactory { + private static final Charset utf8 = StandardCharsets.UTF_8; + private static final Gson gson = new GsonBuilder() + .disableHtmlEscaping() + .serializeSpecialFloatingPointValues() + .create(); + + private static final GsonFactory instance = new GsonFactory(); + + public static GsonFactory getInstance() { + return instance; + } + + private GsonFactory() { + } + + private static class InternalSerializer implements PojoSerializer { + private final TypeAdapter adapter; + + public InternalSerializer(TypeAdapter adapter) { + this.adapter = adapter.nullSafe(); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + public static InternalSerializer create(TypeToken token) { + if(Void.TYPE.equals(token.getRawType())) { + return new InternalSerializer(gson.getAdapter(Object.class)); + } else { + return new InternalSerializer(gson.getAdapter(token)); + } + } + + public static InternalSerializer create(Class clazz) { + return create(TypeToken.get(clazz)); + } + + @SuppressWarnings("unchecked") + public static InternalSerializer create(Type type) { + return create((TypeToken)TypeToken.get(type)); + } + + private T fromJson(JsonReader reader) { + reader.setLenient(true); + try { + try { + reader.peek(); + } catch(EOFException e) { + return null; + } + return adapter.read(reader); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + + @Override + public T fromJson(InputStream input) { + try(JsonReader reader = new JsonReader(new InputStreamReader(input, utf8))) { + return fromJson(reader); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public T fromJson(String input) { + try(JsonReader reader = new JsonReader(new StringReader(input))) { + return fromJson(reader); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public void toJson(T value, OutputStream output) { + try { + try (JsonWriter writer = new JsonWriter(new OutputStreamWriter((output), utf8))) { + writer.setLenient(true); + writer.setSerializeNulls(false); + writer.setHtmlSafe(false); + adapter.write(writer, value); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + } + + + @Override + public PojoSerializer getSerializer(Class clazz) { + return InternalSerializer.create(clazz); + } + + @Override + public PojoSerializer getSerializer(Type type) { + return InternalSerializer.create(type); + } +} diff --git a/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/factories/JacksonFactory.java b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/factories/JacksonFactory.java new file mode 100644 index 000000000..660ca8f58 --- /dev/null +++ b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/factories/JacksonFactory.java @@ -0,0 +1,230 @@ +/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ + +package com.amazonaws.services.lambda.runtime.serialization.factories; + +import com.amazonaws.services.lambda.runtime.serialization.PojoSerializer; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.json.JsonReadFeature; +import com.fasterxml.jackson.core.json.JsonWriteFeature; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.databind.DeserializationConfig; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import com.fasterxml.jackson.databind.ObjectWriter; +import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.fasterxml.jackson.databind.SerializationConfig; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Type; + +public class JacksonFactory implements PojoSerializerFactory { + + private static final ObjectMapper globalMapper = createObjectMapper(); + + private static final JacksonFactory instance = new JacksonFactory(globalMapper); + + public static JacksonFactory getInstance() { + return instance; + } + + private final ObjectMapper mapper; + + private JacksonFactory(ObjectMapper mapper) { + this.mapper = mapper; + } + + public ObjectMapper getMapper() { + return mapper; + } + + + private static ObjectMapper createObjectMapper() { + ObjectMapper mapper = JsonMapper.builder(createJsonFactory()) + .enable(MapperFeature.ALLOW_FINAL_FIELDS_AS_MUTATORS) // this is default as of 2.2.0 + .enable(MapperFeature.AUTO_DETECT_FIELDS) // this is default as of 2.0.0 + .enable(MapperFeature.AUTO_DETECT_GETTERS) // this is default as of 2.0.0 + .enable(MapperFeature.AUTO_DETECT_IS_GETTERS) // this is default as of 2.0.0 + .enable(MapperFeature.AUTO_DETECT_SETTERS) // this is default as of 2.0.0 + .enable(MapperFeature.CAN_OVERRIDE_ACCESS_MODIFIERS) // this is default as of 2.0.0 + .enable(MapperFeature.USE_STD_BEAN_NAMING) + .enable(MapperFeature.USE_ANNOTATIONS) // this is default as of 2.0.0 + .disable(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) // this is default as of 2.5.0 + .disable(MapperFeature.AUTO_DETECT_CREATORS) + .disable(MapperFeature.INFER_PROPERTY_MUTATORS) + .disable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY) // this is default as of 2.0.0 + .disable(MapperFeature.USE_GETTERS_AS_SETTERS) + .disable(MapperFeature.USE_WRAPPER_NAME_AS_PROPERTY_NAME) // this is default as of 2.1.0 + .disable(MapperFeature.USE_STATIC_TYPING) // this is default as of 2.0.0 + .disable(MapperFeature.REQUIRE_SETTERS_FOR_GETTERS) // this is default as of 2.0.0 + .build(); + + SerializationConfig scfg = mapper.getSerializationConfig(); + scfg = scfg.withFeatures( + SerializationFeature.FAIL_ON_SELF_REFERENCES, // this is default as of 2.4.0 + SerializationFeature.FAIL_ON_UNWRAPPED_TYPE_IDENTIFIERS, // this is default as of 2.4.0 + SerializationFeature.WRAP_EXCEPTIONS // this is default as of 2.0.0 + ); + scfg = scfg.withoutFeatures( + SerializationFeature.CLOSE_CLOSEABLE, // this is default as of 2.0.0 + SerializationFeature.EAGER_SERIALIZER_FETCH, + SerializationFeature.FAIL_ON_EMPTY_BEANS, + SerializationFeature.FLUSH_AFTER_WRITE_VALUE, + SerializationFeature.INDENT_OUTPUT, // this is default as of 2.5.0 + SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, // this is default as of 2.0.0 + SerializationFeature.USE_EQUALITY_FOR_OBJECT_ID, // this is default as of 2.3.0 + SerializationFeature.WRITE_CHAR_ARRAYS_AS_JSON_ARRAYS, // this is default as of 2.0.0 + SerializationFeature.WRAP_ROOT_VALUE // this is default as of 2.2.0 + ); + mapper.setConfig(scfg); + + DeserializationConfig dcfg = mapper.getDeserializationConfig(); + dcfg = dcfg.withFeatures( + DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT, + DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, + DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, + DeserializationFeature.FAIL_ON_INVALID_SUBTYPE, // this is default as of 2.2.0 + DeserializationFeature.FAIL_ON_UNRESOLVED_OBJECT_IDS, // this is default as of 2.5.0 + DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, + DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS, + DeserializationFeature.WRAP_EXCEPTIONS // this is default as of 2.0.0 + ); + dcfg = dcfg.withoutFeatures( + DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES, // this is default as of 2.3.0 + DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES, // this is default as of 2.0.0 + DeserializationFeature.FAIL_ON_NUMBERS_FOR_ENUMS, // this is default as of 2.0.0 + DeserializationFeature.FAIL_ON_READING_DUP_TREE_KEY, // this is default as of 2.3.0 + DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES + ); + mapper.setConfig(dcfg); + mapper.setSerializationInclusion(Include.NON_NULL); + + mapper.registerModule(new JavaTimeModule()); + mapper.registerModule(new Jdk8Module()); + + return mapper; + } + + private static JsonFactory createJsonFactory() { + JsonFactory factory = JsonFactory.builder() + //Json Read enabled + .enable(JsonReadFeature.ALLOW_NON_NUMERIC_NUMBERS) + .enable(JsonReadFeature.ALLOW_LEADING_ZEROS_FOR_NUMBERS) + .enable(JsonReadFeature.ALLOW_SINGLE_QUOTES) + .enable(JsonReadFeature.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER) + .enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS) + .enable(JsonReadFeature.ALLOW_UNQUOTED_FIELD_NAMES) + + //Json Read disabled + .disable(JsonReadFeature.ALLOW_JAVA_COMMENTS) // this is default as of 2.10.0 + .disable(JsonReadFeature.ALLOW_YAML_COMMENTS) // this is default as of 2.10.0 + + //Json Write enabled + .enable(JsonWriteFeature.QUOTE_FIELD_NAMES) // this is default as of 2.10.0 + .enable(JsonWriteFeature.WRITE_NAN_AS_STRINGS) // this is default as of 2.10.0 + + //Json Write disabled + .disable(JsonWriteFeature.ESCAPE_NON_ASCII) // this is default as of 2.10.0 + .disable(JsonWriteFeature.WRITE_NUMBERS_AS_STRINGS) // this is default as of 2.10.0 + .build(); + + //Json Parser disabled + factory.disable(JsonParser.Feature.AUTO_CLOSE_SOURCE); + factory.disable(JsonParser.Feature.STRICT_DUPLICATE_DETECTION); + + //Json Generator enabled + factory.enable(JsonGenerator.Feature.IGNORE_UNKNOWN); + + //Json Generator disabled + factory.disable(JsonGenerator.Feature.AUTO_CLOSE_JSON_CONTENT); + factory.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); + factory.disable(JsonGenerator.Feature.FLUSH_PASSED_TO_STREAM); + factory.disable(JsonGenerator.Feature.STRICT_DUPLICATE_DETECTION); // this is default as of 2.3.0 + factory.disable(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN); // this is default as of 2.3.0 + + return factory; + } + + private static class InternalSerializer implements PojoSerializer { + private final ObjectReader reader; + private final ObjectWriter writer; + public InternalSerializer(ObjectReader reader, ObjectWriter writer) { + this.reader = reader; + this.writer = writer; + } + + @Override + public T fromJson(InputStream input) { + try { + return reader.readValue(input); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public T fromJson(String input) { + try { + return reader.readValue(input); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public void toJson(T value, OutputStream output) { + try { + writer.writeValue(output, value); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + } + + private static final class TypeSerializer extends InternalSerializer { + public TypeSerializer(ObjectMapper mapper, JavaType type) { + super(mapper.readerFor(type), mapper.writerFor(type)); + } + + public TypeSerializer(ObjectMapper mapper, Type type) { + this(mapper, mapper.constructType(type)); + } + } + + private static final class ClassSerializer extends InternalSerializer { + public ClassSerializer(ObjectMapper mapper, Class clazz) { + super(mapper.readerFor(clazz), mapper.writerFor(clazz)); + } + } + + public PojoSerializer getSerializer(Class clazz) { + return new ClassSerializer(this.mapper, clazz); + } + public PojoSerializer getSerializer(Type type) { + return new TypeSerializer(this.mapper, type); + } + + public JacksonFactory withNamingStrategy(PropertyNamingStrategy strategy) { + return new JacksonFactory(this.mapper.copy().setPropertyNamingStrategy(strategy)); + } + + public JacksonFactory withMixin(Class clazz, Class mixin) { + return new JacksonFactory(this.mapper.copy().addMixIn(clazz, mixin)); + } + +} diff --git a/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/factories/PojoSerializerFactory.java b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/factories/PojoSerializerFactory.java new file mode 100644 index 000000000..e731c708d --- /dev/null +++ b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/factories/PojoSerializerFactory.java @@ -0,0 +1,12 @@ +/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ + +package com.amazonaws.services.lambda.runtime.serialization.factories; + +import com.amazonaws.services.lambda.runtime.serialization.PojoSerializer; + +import java.lang.reflect.Type; + +public interface PojoSerializerFactory { + PojoSerializer getSerializer(Class clazz); + PojoSerializer getSerializer(Type type); +} diff --git a/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/util/Functions.java b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/util/Functions.java new file mode 100644 index 000000000..46847aeb6 --- /dev/null +++ b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/util/Functions.java @@ -0,0 +1,54 @@ +/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ + +package com.amazonaws.services.lambda.runtime.serialization.util; + +/** + * Interfaces for reflective function calls + * R functions return a type R with n number of arguments + * V functions are void + * A generics represent arguments for a function handle + */ +public final class Functions { + + private Functions() {} + + public interface R0 { + public R call(); + } + + public interface R1 { + public R call(A1 arg1); + } + + public interface R2 { + public R call(A1 arg1, A2 arg2); + } + + public interface R3 { + public R call(A1 arg1, A2 arg2, A3 arg3); + } + + public interface R4 { + public R call(A1 arg1, A2 arg2, A3 arg3, A4 arg4); + } + + public interface R5 { + public R call(A1 arg1, A2 arg2, A3 arg3, A4 arg4, A5 arg5); + } + + public interface R9 { + public R call(A1 arg1, A2 arg2, A3 arg3, A4 arg4, A5 arg5, A6 arg6, A7 arg7, A8 arg8, A9 arg9); + } + + public interface V0 { + public void call(); + } + + public interface V1 { + public void call(A1 arg1); + } + + public interface V2 { + public void call(A1 arg1, A2 arg2); + } +} diff --git a/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/util/LambdaByteArrayOutputStream.java b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/util/LambdaByteArrayOutputStream.java new file mode 100644 index 000000000..5e24e25e6 --- /dev/null +++ b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/util/LambdaByteArrayOutputStream.java @@ -0,0 +1,54 @@ +/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ + +package com.amazonaws.services.lambda.runtime.serialization.util; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * java.io.ByteArrayOutputStream that gives raw access to underlying byte array + */ +final class LambdaByteArrayOutputStream extends ByteArrayOutputStream { + + public LambdaByteArrayOutputStream(int size) { + super(size); + } + + public byte[] getRawBuf() { + return super.buf; + } + + public int getValidByteCount() { + return super.count; + } + + public void readAll(InputStream input) throws IOException { + while(true) { + int numToRead = Math.max(input.available(), 1024); + ensureSpaceAvailable(numToRead); + int rc = input.read(this.buf, this.count, numToRead); + if(rc < 0) { + break; + } else { + this.count += rc; + } + } + } + + private void ensureSpaceAvailable(int space) { + if(space <= 0) { + return; + } + int remaining = count - buf.length; + if(remaining < space) { + int newSize = buf.length * 2; + if(newSize < buf.length) { + newSize = Integer.MAX_VALUE; + } + byte[] newBuf = new byte[newSize]; + System.arraycopy(this.buf, 0, newBuf, 0, count); + this.buf = newBuf; + } + } +} diff --git a/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/util/ReflectUtil.java b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/util/ReflectUtil.java new file mode 100644 index 000000000..695a6fa25 --- /dev/null +++ b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/util/ReflectUtil.java @@ -0,0 +1,663 @@ +/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ + +package com.amazonaws.services.lambda.runtime.serialization.util; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Constructor; +import java.lang.reflect.Modifier; +import java.lang.reflect.Array; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.TypeVariable; +import java.lang.reflect.Type; + +import com.amazonaws.services.lambda.runtime.serialization.util.Functions.R0; +import com.amazonaws.services.lambda.runtime.serialization.util.Functions.R1; +import com.amazonaws.services.lambda.runtime.serialization.util.Functions.R2; +import com.amazonaws.services.lambda.runtime.serialization.util.Functions.R3; +import com.amazonaws.services.lambda.runtime.serialization.util.Functions.R4; +import com.amazonaws.services.lambda.runtime.serialization.util.Functions.R5; +import com.amazonaws.services.lambda.runtime.serialization.util.Functions.R9; +import com.amazonaws.services.lambda.runtime.serialization.util.Functions.V1; +import com.amazonaws.services.lambda.runtime.serialization.util.Functions.V2; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; + +/** + * Class with reflection utilities + */ +public final class ReflectUtil { + + private ReflectUtil() { + } + + /** + * Copy a class from one class loader to another. Was previously used in AwsJackson class to do some crazy + * passing of classes from class loader to class loader. When that class was removed due to excessive complexity, + * this method was retained for potential future use. + * @param clazz class to copy + * @param cl class loader to copy class to + * @return Class inside new classloader + * @throws UncheckedIOException if class cannot be copied + * @throws ReflectException if class cannot be read after copying + */ + @SuppressWarnings({"unchecked"}) + public static Class copyClass(Class clazz, ClassLoader cl) { + // if class exists in target class loader then just load that class and return + try { + return cl.loadClass(clazz.getName()); + } catch (ClassNotFoundException e) {} + // copy class to target class loader + LambdaByteArrayOutputStream stream; + // 1 kb + final int chunkSize = 1024; + final String resourceName = clazz.getName().replace('.', '/') + ".class"; + try(InputStream input = clazz.getClassLoader().getResourceAsStream(resourceName)) { + int initial = Math.max(chunkSize, input.available()); + stream = new LambdaByteArrayOutputStream(initial); + stream.readAll(input); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + // load class from target class loader + try { + Functions.R5, ClassLoader, String, byte[], Integer, Integer> defineClassMethod = + ReflectUtil.loadInstanceR4(ClassLoader.class, "defineClass", true, + (Class>)(Class)Class.class, String.class, byte[].class, int.class, int.class); + Class result = defineClassMethod.call(cl, clazz.getName(), stream.getRawBuf(), 0, stream.getValidByteCount()); + V2> resolveClass = + ReflectUtil.loadInstanceV1(ClassLoader.class, "resolveClass", true, Class.class); + resolveClass.call(cl, result); + return result; + } catch (ClassFormatError | SecurityException e) { + throw new ReflectException(e); + } + } + + public static Class loadClass(ClassLoader cl, String name) { + try { + return Class.forName(name, true, cl); + } catch(ClassNotFoundException | LinkageError e) { + throw new ReflectException(e); + } + } + + public static class ReflectException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public ReflectException() { + super(); + // TODO Auto-generated constructor stub + } + + public ReflectException(String message, Throwable cause, + boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + + public ReflectException(String message, Throwable cause) { + super(message, cause); + } + + public ReflectException(String message) { + super(message); + } + + public ReflectException(Throwable cause) { + super(cause); + } + + } + + private static T newInstance(Constructor constructor, Object... params) { + try { + return constructor.newInstance(params); + } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + throw new ReflectException(e); + } + } + + + public static Class getRawClass(Type type) { + if (type instanceof Class) { + return (Class) type; + } else if (type instanceof ParameterizedType) { + return getRawClass(((ParameterizedType)type).getRawType()); + } else if (type instanceof GenericArrayType) { + Class componentRaw = getRawClass(((GenericArrayType)type).getGenericComponentType()); + return Array.newInstance(componentRaw, 0).getClass(); + } else if (type instanceof TypeVariable) { + throw new ReflectException("type variables not supported"); + } else { + throw new ReflectException("unsupport type: " + type.getClass().getName()); + } + } + + public static R1 makeCaster(Type type) { + return makeCaster(getRawClass(type)); + } + + private static R1 boxCaster(final Class clazz) { + return new R1() { + public T call(Object o) { + return clazz.cast(o); + } + }; + } + + @SuppressWarnings("unchecked") + public static R1 makeCaster(Class clazz) { + if(long.class.equals(clazz)) { + return (R1)boxCaster(Long.class); + } else if (double.class.equals(clazz)) { + return (R1)boxCaster(Double.class); + } else if (float.class.equals(clazz)) { + return (R1)boxCaster(Float.class); + } else if (int.class.equals(clazz)) { + return (R1)boxCaster(Integer.class); + } else if (short.class.equals(clazz)) { + return (R1)boxCaster(Short.class); + } else if (char.class.equals(clazz)) { + return (R1)boxCaster(Character.class); + } else if (byte.class.equals(clazz)) { + return (R1)boxCaster(Byte.class); + } else if (boolean.class.equals(clazz)) { + return (R1)boxCaster(Boolean.class); + } else { + return boxCaster(clazz); + } + } + + private static T invoke(Method method, Object instance, Class rType, Object... params) { + final R1 caster = makeCaster(rType); + try { + Object result = method.invoke(instance, params); + if(rType.equals(Void.TYPE)) { + return null; + } else { + return caster.call(result); + } + } catch(InvocationTargetException | ExceptionInInitializerError | IllegalAccessException e) { + throw new ReflectException(e); + } + } + + private static Method lookupMethod(Class clazz, String name, Class... pTypes) { + try { + try { + return clazz.getDeclaredMethod(name, pTypes); + } catch (NoSuchMethodException e) { + return clazz.getMethod(name, pTypes); + } + } catch (NoSuchMethodException | SecurityException e) { + throw new ReflectException(e); + } + } + + private static Method getDeclaredMethod(Class clazz, String name, boolean isStatic, + boolean setAccessible, Class rType, + Class... pTypes) { + final Method method = lookupMethod(clazz, name, pTypes); + + if (!rType.equals(Void.TYPE) && !rType.isAssignableFrom(method.getReturnType())) { + throw new ReflectException("Class=" + clazz.getName() + " method=" + + name + " type " + method.getReturnType().getName() + " not assignment-compatible with " + + rType.getName()); + } + + int mods = method.getModifiers(); + if (Modifier.isStatic(mods) != isStatic) { + throw new ReflectException("Class=" + clazz.getName() + " method=" + + name + " expected isStatic=" + isStatic); + } + + if (setAccessible) { + method.setAccessible(true); + } + return method; + } + + private static Constructor getDeclaredConstructor(Class clazz, boolean setAccessible, Class... pTypes) { + final Constructor constructor; + try { + constructor = clazz.getDeclaredConstructor(pTypes); + } catch (NoSuchMethodException | SecurityException e) { + throw new ReflectException(e); + } + + if (setAccessible) { + constructor.setAccessible(true); + } + return constructor; + } + + /** + * load instance method that takes no parameters and returns type R + * @param clazz Class of instance + * @param name name of method + * @param setAccessible whether method is accessible (public vs private) + * @param rType class of return type + * @param instance class type + * @param return type + * @return function handle + */ + public static R1 loadInstanceR0(Class clazz, String name, + boolean setAccessible, final Class rType) { + final Method method = getDeclaredMethod(clazz, name, false, setAccessible, rType); + return new R1() { + public R call(C instance) { + return invoke(method, instance, rType); + } + }; + } + + /** + * load instance method that takes 4 parameters and return type R + * @param clazz class of instance + * @param name name of method + * @param setAccessible whether method is accessible (public vs private) + * @param rType class of return type + * @param a1Type argument 1 class + * @param a2Type argument 2 class + * @param a3Type argument 3 class + * @param a4Type argument 4 class + * @param argument 1 type + * @param argument 2 type + * @param argument 3 type + * @param argument 4 type + * @param instance class type + * @param return type + * @return function handle + */ + public static R5 loadInstanceR4(Class clazz, + String name, + boolean setAccessible, + final Class rType, + Class a1Type, + Class a2Type, + Class a3Type, + Class a4Type) { + final Method method = getDeclaredMethod(clazz, name, false, setAccessible, rType, a1Type, a2Type, a3Type, a4Type); + return new R5() { + public R call(C instance, A1 a1, A2 a2, A3 a3, A4 a4) { + return invoke(method, instance, rType, a1, a2, a3, a4); + } + }; + } + + /** + * load an instance method that take 1 parameter and does not return anything + * @param clazz class of instance + * @param name name of method + * @param setAccessible whether method is accessible (public vs private) + * @param a1Type argument 1 class + * @param instance class type + * @param argument 1 type + * @return function handle + */ + public static V2 loadInstanceV1(final Class clazz, String name, boolean setAccessible, + final Class a1Type) { + final Method method = getDeclaredMethod(clazz, name, false, setAccessible, Void.TYPE, a1Type); + return new V2() { + public void call(C instance, A1 a1) { + invoke(method, instance, Void.TYPE, a1); + } + }; + } + + /** + * load an instance method that takes no parameters and return type R + * @param instance instance to load method from + * @param name method name + * @param setAccessible whether method is accessible (public vs private) + * @param rType class of return type + * @param instance type + * @param return type + * @return function handle + */ + public static R0 bindInstanceR0(final C instance, String name, boolean setAccessible, + final Class rType) { + final Method method = getDeclaredMethod(instance.getClass(), name, false, setAccessible, rType); + return new R0() { + public R call() { + return invoke(method, instance, rType); + } + }; + } + + /** + * load an instance method that takes 1 parameter and returns type R + * @param instance instance to load method from + * @param name method name + * @param setAccessible whether method is accessible (public vs private) + * @param rType class of return type + * @param a1Type class of argument 1 + * @param instance type + * @param argument 1 type + * @param return type + * @return function handle + */ + public static R1 bindInstanceR1(final C instance, String name, boolean setAccessible, + final Class rType, Class a1Type) { + final Method method = getDeclaredMethod(instance.getClass(), name, false, setAccessible, rType, a1Type); + return new R1() { + public R call(A1 a1) { + return invoke(method, instance, rType, a1); + } + }; + } + + /** + * * load an instance method that takes 1 parameter and returns nothing + * @param instance instance to load method from + * @param name method name + * @param setAccessible whether method is accessible (public vs private) + * @param a1Type class of argument 1 + * @param instance type + * @param argument 1 type + * @return function handle + */ + public static V1 bindInstanceV1(final C instance, String name, boolean setAccessible, + final Class a1Type) { + final Method method = getDeclaredMethod(instance.getClass(), name, false, setAccessible, Void.TYPE, a1Type); + return new V1() { + public void call(A1 a1) { + invoke(method, instance, Void.TYPE, a1); + } + }; + } + + /** + * load an instance method that takes 2 parameter and returns nothing + * @param instance instance to load method from + * @param name method name + * @param setAccessible whether method is accessible (public vs private) + * @param a1Type class of argument 1 + * @param a2Type class of argument 2 + * @param instance type + * @param argument 1 type + * @param argument 2 type + * @return function handle + */ + public static V2 bindInstanceV2(final C instance, String name, boolean setAccessible, + final Class a1Type, final Class a2Type) { + final Method method = getDeclaredMethod(instance.getClass(), name, false, setAccessible, Void.TYPE, a1Type, a2Type); + return new V2() { + public void call(A1 a1, A2 a2) { + invoke(method, instance, Void.TYPE, a1, a2); + } + }; + } + + /** + * load static method that takes no parameters and returns type R + * @param clazz class to load static method from + * @param name method name + * @param setAccessible whether method is accessible (public vs private) + * @param rType class of return type + * @param return type + * @return function handle + */ + public static R0 loadStaticR0(Class clazz, String name, boolean setAccessible, + final Class rType) { + final Method method = getDeclaredMethod(clazz, name, true, setAccessible, rType); + return new R0() { + public R call() { + return invoke(method, null, rType); + } + }; + } + + /** + * load static method that takes one parameter and returns type R + * @param clazz class to load static method from + * @param name method name + * @param setAccessible whether method is accessible (public vs private) + * @param rType class of return type + * @param a1Type argument 1 class + * @param return type + * @param argument 1 type + * @return function handle + */ + public static R1 loadStaticR1(Class clazz, String name, boolean setAccessible, + final Class rType, Class a1Type) { + final Method method = getDeclaredMethod(clazz, name, true, setAccessible, rType, a1Type); + return new R1() { + public R call(A1 a1) { + return invoke(method, null, rType, a1); + } + }; + } + + /** + * load static method that takes two parameters and return nothing + * @param clazz class to load static method from + * @param name method name + * @param setAccessible whether method is accessible (public vs private) + * @param a1Type argument 1 class + * @param a2Type argument 2 class + * @param argument 1 type + * @param argument 2 type + * @return function handle + */ + public static V2 loadStaticV2(Class clazz, String name, boolean setAccessible, + final Class a1Type, final Class a2Type) { + final Method method = getDeclaredMethod(clazz, name, true, setAccessible, Void.TYPE, a1Type, a2Type); + return new V2() { + public void call(A1 a1, A2 a2) { + invoke(method, null, Void.TYPE, a1, a2); + } + }; + } + + /** + * load default constructor + * @param clazz Class to load constructor for + * @param setAccessible whether method is accessible (public vs private) + * @param Class type + * @return function handle + */ + public static R0 loadConstructor0(final Class clazz, boolean setAccessible) { + final Constructor constructor = getDeclaredConstructor(clazz, setAccessible); + return new R0() { + public C call() { + return newInstance(constructor); + } + }; + } + + /** + * load constructor that takes 1 parameter + * @param clazz Class to load constructor for + * @param setAccessible whether method is accessible (public vs private) + * @param a1Type argument 1 class + * @param Class type + * @param argument 1 type + * @return function handle + */ + public static R1 loadConstructor1(final Class clazz, boolean setAccessible, + Class a1Type) { + final Constructor constructor = getDeclaredConstructor(clazz, setAccessible, a1Type); + return new R1() { + public C call(A1 a1) { + return newInstance(constructor, a1); + } + }; + } + + /** + * load constructor that takes 2 parameters + * @param clazz Class to load constructor for + * @param setAccessible whether method is accessible (public vs private) + * @param a1Type argument 1 class + * @param a2Type argument 2 class + * @param Class type + * @param argument 1 type + * @param argument 2 type + * @return function handle + */ + public static R2 loadConstructor2(final Class clazz, boolean setAccessible, + Class a1Type, Class a2Type) { + final Constructor constructor = getDeclaredConstructor(clazz, setAccessible, a1Type, a2Type); + return new R2() { + public C call(A1 a1, A2 a2) { + return newInstance(constructor, a1, a2); + } + }; + } + + /** + * load constuctor that takes 3 parameters + * @param clazz class to load constructor for + * @param setAccessible whether method is accessible (public vs private) + * @param a1Type argument 1 class + * @param a2Type argument 2 class + * @param a3Type argument 3 class + * @param Class type + * @param argument 1 type + * @param argument 2 type + * @param argument 3 type + * @return function handle + */ + public static R3 loadConstuctor3(final Class clazz, boolean setAccessible, + Class a1Type, Class a2Type, + Class a3Type) { + final Constructor constructor = getDeclaredConstructor(clazz, setAccessible, a1Type, a2Type, a3Type); + return new R3() { + public C call(A1 a1, A2 a2, A3 a3) { + return newInstance(constructor, a1, a2, a3); + } + }; + } + + /** + * loads constructor that takes 4 parameters + * @param clazz class to load constructor for + * @param setAccessible whether method is accessible (public vs private) + * @param a1Type argument 1 class + * @param a2Type argument 2 class + * @param a3Type argument 3 class + * @param a4Type argument 4 class + * @param Class type + * @param argument 1 type + * @param argument 2 type + * @param argument 3 type + * @param argument 4 type + * @return function handle + */ + public static R4 loadConstuctor4(final Class clazz, + boolean setAccessible, + Class a1Type, + Class a2Type, + Class a3Type, + Class a4Type) { + final Constructor constructor = getDeclaredConstructor(clazz, setAccessible, a1Type, a2Type, a3Type, a4Type); + return new R4() { + public C call(A1 a1, A2 a2, A3 a3, A4 a4) { + return newInstance(constructor, a1, a2, a3, a4); + } + }; + } + + /** + * loads constructor that takes 5 paramters + * @param clazz class to load constructor for + * @param setAccessible whether method is accessible (public vs private) + * @param a1Type argument 1 class + * @param a2Type argument 2 class + * @param a3Type argument 3 class + * @param a4Type argument 4 class + * @param a5Type argument 5 class + * @param Class type + * @param argument 1 type + * @param argument 2 type + * @param argument 3 type + * @param argument 4 type + * @param argument 5 type + * @return function handle + */ + public static R5 loadConstuctor5(final Class clazz, + boolean setAccessible, + Class a1Type, + Class a2Type, + Class a3Type, + Class a4Type, + Class a5Type) { + final Constructor constructor = getDeclaredConstructor(clazz, setAccessible, a1Type, a2Type, a3Type, a4Type, a5Type); + return new R5() { + public C call(A1 a1, A2 a2, A3 a3, A4 a4, A5 a5) { + return newInstance(constructor, a1, a2, a3, a4, a5); + } + }; + } + + /** + * load constuctor that takes 9 parameters + * @param a1Type argument 1 class + * @param a2Type argument 2 class + * @param a3Type argument 3 class + * @param a4Type argument 4 class + * @param a5Type argument 5 class + * @param a6Type argument 6 class + * @param a7Type argument 7 class + * @param a8Type argument 8 class + * @param a9type argument 9 class + * @param Class type + * @param argument 1 type + * @param argument 2 type + * @param argument 3 type + * @param argument 4 type + * @param argument 5 type + * @param argument 6 type + * @param argument 7 type + * @param argument 8 type + * @param argument 9 type + * @return function handle + */ + public static R9 loadConstuctor9( + final Class clazz, + boolean setAccessible, + Class a1Type, + Class a2Type, + Class a3Type, + Class a4Type, + Class a5Type, + Class a6Type, + Class a7Type, + Class a8Type, + Class a9type) { + final Constructor constructor= + getDeclaredConstructor(clazz, setAccessible, a1Type, a2Type, a3Type, a4Type, a5Type, a6Type, a7Type, a8Type, a9type); + return new R9(){ + public C call(A1 a1,A2 a2,A3 a3,A4 a4,A5 a5,A6 a6,A7 a7,A8 a8,A9 a9){ + return newInstance(constructor,a1,a2,a3,a4,a5,a6,a7,a8,a9); + } + }; + } + + public static T getStaticField(Class clazz, String name, Class type) { + R1 caster = makeCaster(type); + try { + return caster.call(clazz.getField(name).get(null)); + } catch (NoSuchFieldException | SecurityException | IllegalAccessException e) { + throw new ReflectException(e); + } + } + + public static void setStaticField(Class clazz, String name, boolean setAccessible, final Object value) { + try { + Field field = clazz.getDeclaredField(name); + if (setAccessible) { + field.setAccessible(true); + } + field.set(null, value); + } catch (NoSuchFieldException | SecurityException | IllegalAccessException e) { + throw new ReflectException(e); + } + } +} diff --git a/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/util/SerializeUtil.java b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/util/SerializeUtil.java new file mode 100644 index 000000000..f6acb528f --- /dev/null +++ b/aws-lambda-java-serialization/src/main/java/com/amazonaws/services/lambda/runtime/serialization/util/SerializeUtil.java @@ -0,0 +1,104 @@ +/* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ + +package com.amazonaws.services.lambda.runtime.serialization.util; + +import java.io.InputStream; +import java.util.HashMap; +import java.util.Scanner; + +/** + * Class with Utilities for serializing and deserializing customer classes + */ +public class SerializeUtil { + + /** + * cached of classes being loaded for faster reflect loading + */ + private static final HashMap cachedClasses = new HashMap<>(); + + /** + * converts an input stream to a string + * @param inputStream InputStream object + * @return String with stream contents + */ + public static String convertStreamToString(InputStream inputStream) { + Scanner s = new Scanner(inputStream).useDelimiter("\\A"); + return s.hasNext() ? s.next() : ""; + } + + /** + * load a customer class + * @param className name of class to load + * @return Class object + */ + public static Class loadCustomerClass(String className, ClassLoader customerClassLoader) { + Class cachedClass = cachedClasses.get(className); + if (cachedClass == null) { + cachedClass = ReflectUtil.loadClass(customerClassLoader, className); + cachedClasses.put(className, cachedClass); + } + return cachedClass; + } + + /** + * deserialize a joda datetime object + * Underneath the reflection, this method does the following: + * + * DateTime.parse(jsonParser.getValueAsString()); + * + * @param dateTimeClass DateTime class + * @param dateTimeString string to deserialize from + * @param DateTime type + * @return DateTime instance + */ + public static T deserializeDateTime(Class dateTimeClass, String dateTimeString) { + Functions.R1 parseMethod = + ReflectUtil.loadStaticR1(dateTimeClass, "parse", true, dateTimeClass, String.class); + return parseMethod.call(dateTimeString); + } + + /** + * serialize a DateTime object + * Underneath the reflection, this method does the following: + * + * DateTimeFormatter formatter = ISODateTimeFormat.dateTime(); + * jsonGenerator.writeString(formatter.print(customerDateTime) + * + * @param dateTime DateTime object to serialize + * @param DateTime type + * @param classLoader ClassLoader used to load DateTime classes + * @return timestamp as formatted string + */ + @SuppressWarnings({"unchecked"}) + public static String serializeDateTime(T dateTime, ClassLoader classLoader) { + // Workaround not to let maven shade plugin relocating string literals https://issues.apache.org/jira/browse/MSHADE-156 + Class dateTimeFormatterClass = loadCustomerClass("com.amazonaws.lambda.unshade.thirdparty.org.joda.time.format.DateTimeFormatter", classLoader); + Class dateTimeFormatClass = loadCustomerClass("com.amazonaws.lambda.unshade.thirdparty.org.joda.time.format.ISODateTimeFormat", classLoader); + Class readableInstantInterface = loadCustomerClass("com.amazonaws.lambda.unshade.thirdparty.org.joda.time.ReadableInstant", classLoader); + return serializeDateTimeHelper(dateTime, dateTimeFormatterClass, dateTimeFormatClass, readableInstantInterface); + } + + /** + * Helper method to serialize DateTime objects (We need some way to define generics to get code to compile) + * @param dateTime DAteTime object + * @param dateTimeFormatterClass DAteTime formatter class + * @param dateTimeFormatClass DateTime ISO format class + * @param readableInstantInterface DateTime readable instant interface (Needed because reflection is type specific) + * @param DAteTime type + * @param DateTimeFormatter type + * @param DateTimeFormat type + * @param ReadableInstant type + * @return String with serialized date time + */ + private static String serializeDateTimeHelper(S dateTime, Class dateTimeFormatterClass, + Class dateTimeFormatClass, + Class readableInstantInterface) { + Functions.R0 dateTimeFormatterConstructor = + ReflectUtil.loadStaticR0(dateTimeFormatClass, "dateTime", true, dateTimeFormatterClass); + T dateTimeFormatter = dateTimeFormatterConstructor.call(); + Functions.R1 printMethod = + ReflectUtil.bindInstanceR1(dateTimeFormatter, "print", true, String.class, readableInstantInterface); + return printMethod.call(dateTime); + } + +} diff --git a/aws-lambda-java-serialization/verify-relocation.sh b/aws-lambda-java-serialization/verify-relocation.sh new file mode 100755 index 000000000..d44cae7f4 --- /dev/null +++ b/aws-lambda-java-serialization/verify-relocation.sh @@ -0,0 +1,35 @@ +#!/bin/bash +# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +# This script runs after the aws-lambda-java-serialization package phase. It verifies that no unexpected transitive +# dependencies were missed from the relocation of third party classes. + +set -euo pipefail + +ARTIFACT_PATH=${1} +RELOCATION_PREFIX=${2} +SERIALIZATION_MODULE_PATTERN=${3} + +echo 'Validating that serialization module classes were not relocated' +OUTPUT=$(zipinfo ${ARTIFACT_PATH} | grep '.class' | grep ${RELOCATION_PREFIX//.//} | grep ${SERIALIZATION_MODULE_PATTERN//.//} || true) +if [[ ! -z "$OUTPUT" ]]; then + echo "Serialization module classes were unexpectedly relocated" + echo ${OUTPUT} + exit 1 +fi + +echo 'Validating that everything other than serialization module classes were relocated' +OUTPUT=$(zipinfo ${ARTIFACT_PATH} | grep '.class' | grep -v ${SERIALIZATION_MODULE_PATTERN//.//} | grep -v 'META-INF' | grep -v ${RELOCATION_PREFIX//.//} || true) +if [[ ! -z "$OUTPUT" ]]; then + echo "Some classes were not relocated" + echo ${OUTPUT} + exit 1 +fi + +echo 'Validating that META-INF/services were relocated' +OUTPUT=$(zipinfo ${ARTIFACT_PATH} | grep 'META-INF/services/.\+' | grep -v ${RELOCATION_PREFIX} || true) +if [[ ! -z "$OUTPUT" ]]; then + echo "Some meta-inf services were not relocated" + echo ${OUTPUT} + exit 1 +fi diff --git a/aws-lambda-java-tests/README.md b/aws-lambda-java-tests/README.md new file mode 100644 index 000000000..e0cd85213 --- /dev/null +++ b/aws-lambda-java-tests/README.md @@ -0,0 +1,198 @@ + +# Tests utility + +The `aws-lambda-java-tests` module provides opinionated tools to ease Java Lambda testing. This is a test dependency. + +**Key features** + +* Load events from json files and get them deserialized into Java Events. +* Inject Events directly in JUnit 5 tests, using the `@ParameterizedTest` annotation. + + +## Background + +When using Java for a Lambda function, you must implement the RequestHandler interface and provide input and output types: + +```java +public interface RequestHandler { + public O handleRequest(I input, Context context); +} +``` + +The input is automatically deserialized by the Lambda Java Runtime from a json event into the type you define, +and the output is serialized into JSON from the output type. More info in the [docs](https://docs.aws.amazon.com/lambda/latest/dg/java-handler.html). + +When you want to test your Lambda function and your handleRequest method, you cannot simply use JSON events files +as some of the event fields may not be deserialized correctly. + +For example, an SQS JSON event contains a list of "Records", while the [`SQSEvent`](https://github.com/aws/aws-lambda-java-libs/blob/master/aws-lambda-java-events/src/main/java/com/amazonaws/services/lambda/runtime/events/SQSEvent.java) use "records" with a lowercase. +You can choose to modify the JSON input but it can be tedious and you generally want to keep the JSON event as you get it +in the doc, the Lambda console or in your logs. + +Now you can use the [aws-lambda-java-serialization](https://github.com/aws/aws-lambda-java-libs/tree/master/aws-lambda-java-serialization) module to deserialize events. And this test library is using this module as a dependency to ease tests of lambda function handlers. + +## Installation + +To install this utility, add the following dependency to your project. Note that it's a test dependency. + +```xml + + com.amazonaws + aws-lambda-java-tests + 1.1.1 + test + +``` + +Also have surefire in your plugins: + +```xml + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.2 + + + +``` + +## Usage + +### Events injection + +A set of annotations can be used to inject Events and/or to validate handler responses against those Events. +**All those annotations must be used in conjunction with the [`@ParameterizedTest`](https://junit.org/junit5/docs/current/api/org.junit.jupiter.params/org/junit/jupiter/params/ParameterizedTest.html) annotation from Junit 5.** + +`ParameterizedTest` enables to inject arguments into a unit test, so you can run the same test one or more time with different parameters. +See [the doc](https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests) for more details on this. + +**Event** + +The `@Event` annotation permits to inject one Event into a Junit test. + +Example: + +```java +// the json file must be in the classpath (most often in src/test/resources) +@ParameterizedTest +@Event(value = "sqs/sqs_event.json", type = SQSEvent.class) +public void testInjectSQSEvent(SQSEvent event) { + // test your handleRequest method with this event as parameter +} +``` + +**Events** + +The `@Events` annotation permits to inject multiple Events into a Junit test + +Examples: + +```java +@ParameterizedTest +@Events( + events = { + @Event("sqs/sqs_event.json"), + @Event("sqs/sqs_event2.json"), + }, + type = SQSEvent.class +) +public void testInjectEvents(SQSEvent event) { + // test your handleRequest method with all the JSON events available in the sqs folder +} + +// OR simpler + +// sqs folder must be in the classpath (most often in src/test/resources) +@ParameterizedTest +@Events(folder = "sqs", type = SQSEvent.class) +public void testInjectEventsFromFolder(SQSEvent event) { + // test your handleRequest method with all the JSON events available in the sqs folder +} +``` + +**HandlerParams** + +The `@HandlerParams` is the most advanced one as it permits to provide both input and output as arguments to your tests. +Thus you can validate your `handlerRequest` method by providing the output and asserting on the expected output. + +```java + +// Single event +@ParameterizedTest +@HandlerParams( + event = @Event(value = "apigw/events/apigw_event.json", type = APIGatewayProxyRequestEvent.class), + response = @Response(value = "apigw/responses/apigw_response.json", type = APIGatewayProxyResponseEvent.class)) +public void testSingleEventResponse(APIGatewayProxyRequestEvent event, APIGatewayProxyResponseEvent response) { +} + +// Multiple events in folder +@ParameterizedTest +@HandlerParams( + events = @Events(folder = "apigw/events/", type = APIGatewayProxyRequestEvent.class), + responses = @Responses(folder = "apigw/responses/", type = APIGatewayProxyResponseEvent.class)) +public void testMultipleEventsResponsesInFolder(APIGatewayProxyRequestEvent event, APIGatewayProxyResponseEvent response) { +} + +// Multiple events +@HandlerParams( + events = @Events( + events = { + @Event("apigw/events/apigw_event.json"), + @Event("apigw/events/apigw_event2.json"), + }, + type = APIGatewayProxyRequestEvent.class + ), + responses = @Responses( + responses = { + @Response("apigw/responses/apigw_response.json"), + @Response("apigw/responses/apigw_response2.json") + }, + type = APIGatewayProxyResponseEvent.class + ) +) +public void testMultipleEventsResponses(APIGatewayProxyRequestEvent event, APIGatewayProxyResponseEvent response) { +} +``` + +If you cannot use those annotations (for example if you use TestNG), or if you want to load the events on your own, you can directly use the `EventLoader`, which is the underlying class that load the json events. + +### EventLoader + +`EventLoader` enables to load any Event from a JSON file and deserialize it into a Java Object. +Either one from the [aws-lambda-java-events](https://github.com/aws/aws-lambda-java-libs/tree/master/aws-lambda-java-events) library +or your own Event. + +EventLoader provides a load method for most of the pre-defined events: + +```java +APIGatewayV2HTTPEvent httpEvent = + EventLoader.loadApiGatewayHttpEvent("apigw_http_event.json"); + +APIGatewayProxyRequestEvent restEvent = + EventLoader.loadApiGatewayRestEvent("apigw_rest_event.json"); + +DynamodbEvent ddbEvent = EventLoader.loadDynamoDbEvent("ddb_event.json"); + +KinesisEvent kinesisEvent = + EventLoader.loadKinesisEvent("kinesis_event.json"); + +ScheduledEvent eventBridgeEvent = + EventLoader.loadScheduledEvent("eb_event.json"); + +S3Event s3Event = EventLoader.loadS3Event("s3_event.json"); + +SNSEvent snsEvent = EventLoader.loadSNSEvent("sns_event.json"); + +SQSEvent sqsEvent = EventLoader.loadSQSEvent("sqs_event.json"); + +// ... and many others +``` + +Or you can load what you prefer with the generic method: + +```java +MyEvent myEvent = EventLoader.loadEvent("my_event.json", MyEvent.class); +``` + diff --git a/aws-lambda-java-tests/RELEASE.CHANGELOG.md b/aws-lambda-java-tests/RELEASE.CHANGELOG.md new file mode 100644 index 000000000..0b4bd2510 --- /dev/null +++ b/aws-lambda-java-tests/RELEASE.CHANGELOG.md @@ -0,0 +1,29 @@ +### March 27, 2026 +`1.1.3`: +- Add serialization round-trip tests covering 66 event classes +- Bumped `aws-lambda-java-serialization` to version `1.4.0` (Jackson `2.15.x` → `2.18.6`) +- Bumped `aws-lambda-java-events` to version `3.16.1` + +### August 26, 2021 +`1.1.1`: +- Bumped `aws-lambda-java-events` to version `3.11.0` + +### August 26, 2021 +`1.1.0`: +- Added test for `RabbitMQEvent` ([#256](https://github.com/aws/aws-lambda-java-libs/pull/256)) +- Added test for `KafkaEventRecord` headers ([#260](https://github.com/aws/aws-lambda-java-libs/pull/260)) +- Bumped `aws-lambda-java-events` to version `3.10.0` + +### March 24, 2021 +`1.0.2`: +- Bumped `aws-lambda-java-events` to version `3.9.0` + +### March 24, 2021 +`1.0.1`: +- Added sorting to the event/response files to guarantee order ([#218](https://github.com/aws/aws-lambda-java-libs/pull/218)) +- Added `bootstrapServers` to Kafka Event tests ([#216](https://github.com/aws/aws-lambda-java-libs/pull/216)) +- Bumped `aws-lambda-java-events` to version `3.8.0` + +### December 10, 2020 +`1.0.0`: +- Initial release of AWS Lambda Java Tests diff --git a/aws-lambda-java-tests/pom.xml b/aws-lambda-java-tests/pom.xml new file mode 100644 index 000000000..bb0c7ab74 --- /dev/null +++ b/aws-lambda-java-tests/pom.xml @@ -0,0 +1,304 @@ + + + 4.0.0 + + com.amazonaws + aws-lambda-java-tests + 1.1.3-SNAPSHOT + jar + + AWS Lambda Java Tests + Testing module for the AWS Lambda Java Runtime + https://aws.amazon.com/lambda/ + + + Apache License, Version 2.0 + https://aws.amazon.com/apache2.0 + repo + + + + https://github.com/aws/aws-lambda-java-libs.git + scm:git:https://github.com/aws/aws-lambda-java-libs.git + scm:git:https://github.com/aws/aws-lambda-java-libs.git + HEAD + + + + AWS Lambda team + Amazon Web Services + https://aws.amazon.com/ + + + + + 1.8 + 1.8 + UTF-8 + + 5.9.2 + 0.8.7 + 1.4.1 + 3.16.1 + 3.18.0 + 3.27.7 + + + + + com.amazonaws + aws-lambda-java-serialization + ${aws-lambda-java-serialization.version} + + + com.amazonaws + aws-lambda-java-events + ${aws-lambda-java-events.version} + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + + + org.junit.jupiter + junit-jupiter-params + ${junit.version} + + + org.apache.commons + commons-lang3 + ${commons-lang3.version} + + + + org.assertj + assertj-core + ${assertj-core.version} + test + + + + + + checkDependencies + + + checkDependencies + + + + + + org.owasp + dependency-check-maven + 5.3.2 + + + validate + + check + + + + + + + + + dev + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.2.0 + + none + false + 8 + false + + + + attach-javadocs + + jar + + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco.maven.plugin.version} + + + default-prepare-agent + + prepare-agent + + + + default-report + test + + report + + + + default-check + test + + check + + + + + PACKAGE + + + LINE + COVEREDRATIO + 0 + + + + + + + + + + + + + release + + + + org.apache.maven.plugins + maven-source-plugin + 2.2.1 + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.2.0 + + none + false + 8 + false + + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.5 + + + sign-artifacts + verify + + sign + + + + + + org.sonatype.central + central-publishing-maven-plugin + 0.8.0 + true + + central + false + + + + + + + + + + + org.apache.maven.plugins + maven-release-plugin + 3.1.1 + + aws-lambda-java-tests-@{project.version} + true + release + deploy + + + + org.apache.maven.plugins + maven-toolchains-plugin + 3.2.0 + + + + + [1.8,9) + + + + + + + toolchain + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + ${maven.compiler.source} + ${maven.compiler.target} + false + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.2 + + true + + + + + \ No newline at end of file diff --git a/aws-lambda-java-tests/src/main/java/com/amazonaws/services/lambda/runtime/tests/EventArgumentsProvider.java b/aws-lambda-java-tests/src/main/java/com/amazonaws/services/lambda/runtime/tests/EventArgumentsProvider.java new file mode 100644 index 000000000..0dec6e6b7 --- /dev/null +++ b/aws-lambda-java-tests/src/main/java/com/amazonaws/services/lambda/runtime/tests/EventArgumentsProvider.java @@ -0,0 +1,29 @@ +/* Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ +package com.amazonaws.services.lambda.runtime.tests; + +import com.amazonaws.services.lambda.runtime.tests.annotations.Event; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.support.AnnotationConsumer; + +import java.util.stream.Stream; + +/** + * Used to process @{@link Event} com.amazonaws.services.lambda.runtime.tests.annotations + */ +public class EventArgumentsProvider implements ArgumentsProvider, AnnotationConsumer { + + private Event event; + + @Override + public Stream provideArguments(ExtensionContext extensionContext) { + Object o = EventLoader.loadEvent(event.value(), event.type()); + return Stream.of(Arguments.of(o)); + } + + @Override + public void accept(Event event) { + this.event = event; + } +} diff --git a/aws-lambda-java-tests/src/main/java/com/amazonaws/services/lambda/runtime/tests/EventLoader.java b/aws-lambda-java-tests/src/main/java/com/amazonaws/services/lambda/runtime/tests/EventLoader.java new file mode 100644 index 000000000..0c5d66206 --- /dev/null +++ b/aws-lambda-java-tests/src/main/java/com/amazonaws/services/lambda/runtime/tests/EventLoader.java @@ -0,0 +1,165 @@ +/* Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ +package com.amazonaws.services.lambda.runtime.tests; + +import com.amazonaws.services.lambda.runtime.serialization.PojoSerializer; +import com.amazonaws.services.lambda.runtime.serialization.events.LambdaEventSerializers; + +import java.io.*; + +import com.amazonaws.services.lambda.runtime.events.*; + +/** + * Load events from json files and serialize them in Events + */ +public class EventLoader { + + public static ActiveMQEvent loadActiveMQEvent(String filename) { + return loadEvent(filename, ActiveMQEvent.class); + } + + public static APIGatewayV2HTTPEvent loadApiGatewayHttpEvent(String filename) { + return loadEvent(filename, APIGatewayV2HTTPEvent.class); + } + + public static APIGatewayProxyRequestEvent loadApiGatewayRestEvent(String filename) { + return loadEvent(filename, APIGatewayProxyRequestEvent.class); + } + + public static APIGatewayCustomAuthorizerEvent loadAPIGatewayCustomAuthorizerEvent(String filename) { + return loadEvent(filename, APIGatewayCustomAuthorizerEvent.class); + } + + public static APIGatewayV2CustomAuthorizerEvent loadAPIGatewayV2CustomAuthorizerEvent(String filename) { + return loadEvent(filename, APIGatewayV2CustomAuthorizerEvent.class); + } + + public static ApplicationLoadBalancerRequestEvent loadApplicationLoadBalancerRequestEvent(String filename) { + return loadEvent(filename, ApplicationLoadBalancerRequestEvent.class); + } + + public static CloudFormationCustomResourceEvent loadCloudFormationCustomResourceEvent(String filename) { + return loadEvent(filename, CloudFormationCustomResourceEvent.class); + } + + public static CloudFrontEvent loadCloudFrontEvent(String filename) { + return loadEvent(filename, CloudFrontEvent.class); + } + + public static CloudWatchCompositeAlarmEvent loadCloudWatchCompositeAlarmEvent(String filename) { + return loadEvent(filename, CloudWatchCompositeAlarmEvent.class); + } + + public static CloudWatchLogsEvent loadCloudWatchLogsEvent(String filename) { + return loadEvent(filename, CloudWatchLogsEvent.class); + } + + public static CloudWatchMetricAlarmEvent loadCloudWatchMetricAlarmEvent(String filename) { + return loadEvent(filename, CloudWatchMetricAlarmEvent.class); + } + + public static CodeCommitEvent loadCodeCommitEvent(String filename) { + return loadEvent(filename, CodeCommitEvent.class); + } + + public static ConfigEvent loadConfigEvent(String filename) { + return loadEvent(filename, ConfigEvent.class); + } + + public static ConnectEvent loadConnectEvent(String filename) { + return loadEvent(filename, ConnectEvent.class); + } + + public static DynamodbEvent loadDynamoDbEvent(String filename) { + return loadEvent(filename, DynamodbEvent.class); + } + + public static DynamodbEvent.DynamodbStreamRecord loadDynamoDbStreamRecord(String filename) { + return loadEvent(filename, DynamodbEvent.DynamodbStreamRecord.class); + } + + public static KafkaEvent loadKafkaEvent(String filename) { + return loadEvent(filename, KafkaEvent.class); + } + + public static KinesisEvent loadKinesisEvent(String filename) { + return loadEvent(filename, KinesisEvent.class); + } + + public static KinesisFirehoseEvent loadKinesisFirehoseEvent(String filename) { + return loadEvent(filename, KinesisFirehoseEvent.class); + } + + public static LambdaDestinationEvent loadLambdaDestinationEvent(String filename) { + return loadEvent(filename, LambdaDestinationEvent.class); + } + + public static LexEvent loadLexEvent(String filename) { + return loadEvent(filename, LexEvent.class); + } + + public static MSKFirehoseEvent loadMSKFirehoseEvent(String filename) { + return loadEvent(filename, MSKFirehoseEvent.class); + } + + public static S3Event loadS3Event(String filename) { + return loadEvent(filename, S3Event.class); + } + + public static S3BatchEventV2 loadS3BatchEventV2(String filename) { + return loadEvent(filename, S3BatchEventV2.class); + } + + public static SecretsManagerRotationEvent loadSecretsManagerRotationEvent(String filename) { + return loadEvent(filename, SecretsManagerRotationEvent.class); + } + + public static ScheduledEvent loadScheduledEvent(String filename) { + return loadEvent(filename, ScheduledEvent.class); + } + + public static SNSEvent loadSNSEvent(String filename) { + return loadEvent(filename, SNSEvent.class); + } + + public static SQSEvent loadSQSEvent(String filename) { + return loadEvent(filename, SQSEvent.class); + } + + public static RabbitMQEvent loadRabbitMQEvent(String filename) { + return loadEvent(filename, RabbitMQEvent.class); + } + + public static CognitoUserPoolPreTokenGenerationEventV2 loadCognitoUserPoolPreTokenGenerationEventV2(String filename) { + return loadEvent(filename, CognitoUserPoolPreTokenGenerationEventV2.class); + } + + public static T loadEvent(String filename, Class targetClass) { + + if (!filename.endsWith("json")) { + throw new IllegalArgumentException("File " + filename + " must have json extension"); + } + + PojoSerializer serializer = LambdaEventSerializers.serializerFor(targetClass, ClassLoader.getSystemClassLoader()); + + InputStream stream = serializer.getClass().getResourceAsStream(filename); + if (stream == null) { + stream = serializer.getClass().getClassLoader().getResourceAsStream(filename); + } + if (stream == null) { + try { + stream = new FileInputStream(new File(filename)); + } catch (FileNotFoundException e) { + throw new EventLoadingException("Cannot load " + filename, e); + } + } + try { + return serializer.fromJson(stream); + } finally { + try { + stream.close(); + } catch (IOException ioe) { + ioe.printStackTrace(); + } + } + } +} diff --git a/aws-lambda-java-tests/src/main/java/com/amazonaws/services/lambda/runtime/tests/EventLoadingException.java b/aws-lambda-java-tests/src/main/java/com/amazonaws/services/lambda/runtime/tests/EventLoadingException.java new file mode 100644 index 000000000..c7b8c34db --- /dev/null +++ b/aws-lambda-java-tests/src/main/java/com/amazonaws/services/lambda/runtime/tests/EventLoadingException.java @@ -0,0 +1,18 @@ +/* Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ +package com.amazonaws.services.lambda.runtime.tests; + +public class EventLoadingException extends RuntimeException { + + private static final long serialVersionUID = 5766526909472206270L; + + public EventLoadingException() { + } + + public EventLoadingException(String message) { + super(message); + } + + public EventLoadingException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/aws-lambda-java-tests/src/main/java/com/amazonaws/services/lambda/runtime/tests/EventsArgumentsProvider.java b/aws-lambda-java-tests/src/main/java/com/amazonaws/services/lambda/runtime/tests/EventsArgumentsProvider.java new file mode 100644 index 000000000..8db5d21d4 --- /dev/null +++ b/aws-lambda-java-tests/src/main/java/com/amazonaws/services/lambda/runtime/tests/EventsArgumentsProvider.java @@ -0,0 +1,52 @@ +/* Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ +package com.amazonaws.services.lambda.runtime.tests; + +import com.amazonaws.services.lambda.runtime.tests.annotations.Events; +import org.apache.commons.lang3.ArrayUtils; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.support.AnnotationConsumer; + +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.stream.Stream; + +/** + * Used to process @{@link Events} com.amazonaws.services.lambda.runtime.tests.annotations + */ +public class EventsArgumentsProvider implements ArgumentsProvider, AnnotationConsumer { + + private Events events; + + @Override + public void accept(Events events) { + this.events = events; + } + + @Override + public Stream provideArguments(ExtensionContext context) throws Exception { + if (ArrayUtils.isNotEmpty(events.events())) { + return Arrays.stream(events.events()) + .map(event -> { + Class clazz = event.type() == Void.class ? events.type() : event.type(); + return Arguments.of(EventLoader.loadEvent(event.value(), clazz)); + }); + } else { + URL folderUrl = getClass().getResource(events.folder()); + if (folderUrl == null) { + folderUrl = getClass().getClassLoader().getResource(events.folder()); + } + if (folderUrl == null) { + throw new IllegalArgumentException("Path " + events.folder() + " cannot be found"); + } + Stream files = Files.list(Paths.get(folderUrl.toURI())).sorted(); + return files + .filter(Files::isRegularFile) + .map(path -> Arguments.of(EventLoader.loadEvent(path.toString(), events.type()))); + } + } +} diff --git a/aws-lambda-java-tests/src/main/java/com/amazonaws/services/lambda/runtime/tests/HandlerParamsArgumentsProvider.java b/aws-lambda-java-tests/src/main/java/com/amazonaws/services/lambda/runtime/tests/HandlerParamsArgumentsProvider.java new file mode 100644 index 000000000..a4c974f4b --- /dev/null +++ b/aws-lambda-java-tests/src/main/java/com/amazonaws/services/lambda/runtime/tests/HandlerParamsArgumentsProvider.java @@ -0,0 +1,130 @@ +/* Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ +package com.amazonaws.services.lambda.runtime.tests; + +import com.amazonaws.services.lambda.runtime.tests.annotations.*; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.support.AnnotationConsumer; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Used to process @{@link HandlerParams} com.amazonaws.services.lambda.runtime.tests.annotations + */ +public class HandlerParamsArgumentsProvider implements ArgumentsProvider, AnnotationConsumer { + + private Event event; + private Response response; + + private Events events; + private Responses responses; + + @Override + public void accept(HandlerParams handlerParams) { + this.event = handlerParams.event(); + this.response = handlerParams.response(); + this.events = handlerParams.events(); + this.responses = handlerParams.responses(); + } + + @Override + public Stream provideArguments(ExtensionContext context) throws Exception { + if ((!event.value().isEmpty() && response.value().isEmpty()) || + (event.value().isEmpty() && !response.value().isEmpty())) { + throw new IllegalStateException("You must use either Event & Response (singular) or Events & Responses (plural) annotations together, you cannot mix them"); + } + if (((ArrayUtils.isEmpty(events.events()) && StringUtils.isEmpty(events.folder())) + && (StringUtils.isNotEmpty(responses.folder()) || ArrayUtils.isNotEmpty(responses.responses()))) + || + ((ArrayUtils.isEmpty(responses.responses()) && StringUtils.isEmpty(responses.folder())) + && (StringUtils.isNotEmpty(events.folder()) || ArrayUtils.isNotEmpty(events.events())))) { + throw new IllegalStateException("You must use either Event & Response (singular) or Events & Responses (plural) annotations together, you cannot mix them"); + } + + // deal with one element + if (!event.value().isEmpty() && !response.value().isEmpty()) { + return Stream.of( + Arguments.of( + EventLoader.loadEvent(event.value(), event.type()), + EventLoader.loadEvent(response.value(), response.type()) + ) + ); + } + + // deal with many elements + List eventList = getEvents(); + List responseList = getResponses(); + if (eventList == null || eventList.size() == 0 || responseList == null || responseList.size() == 0 || eventList.size() != responseList.size()) { + throw new IllegalStateException("At least one event and one response should be provided, and you should have the exact same number of events and responses."); + } + + Stream.Builder streamBuilder = Stream.builder(); + for (int i = 0; i < eventList.size(); i++) { + streamBuilder.add(Arguments.of(eventList.get(i), responseList.get(i))); + } + return streamBuilder.build(); + } + + private List getResponses() throws IOException, URISyntaxException { + List responseList; + if (ArrayUtils.isNotEmpty(responses.responses())) { + responseList = Arrays.stream(responses.responses()).map( + response -> { + Class clazz = response.type() == Void.class ? responses.type() : response.type(); + return EventLoader.loadEvent(response.value(), clazz); + } + ).collect(Collectors.toList()); + } else { + Stream files = listFiles(responses.folder()); + + responseList = files + .filter(Files::isRegularFile) + .map(path -> EventLoader.loadEvent(path.toString(), responses.type())) + .collect(Collectors.toList()); + } + return responseList; + } + + private List getEvents() throws IOException, URISyntaxException { + List eventList; + if (ArrayUtils.isNotEmpty(events.events())) { + eventList = Arrays.stream(events.events()).map( + event -> { + Class clazz = event.type() == Void.class ? events.type() : event.type(); + return EventLoader.loadEvent(event.value(), clazz); + } + ).collect(Collectors.toList()); + } else { + Stream files = listFiles(events.folder()); + + eventList = files + .filter(Files::isRegularFile) + .map(path -> EventLoader.loadEvent(path.toString(), events.type())) + .collect(Collectors.toList()); + } + return eventList; + } + + private Stream listFiles(String folder) throws IOException, URISyntaxException { + URL folderUrl = getClass().getResource(folder); + if (folderUrl == null) { + folderUrl = getClass().getClassLoader().getResource(folder); + } + if (folderUrl == null) { + throw new IllegalArgumentException("Path " + folder + " cannot be found"); + } + return Files.list(Paths.get(folderUrl.toURI())).sorted(); + } +} diff --git a/aws-lambda-java-tests/src/main/java/com/amazonaws/services/lambda/runtime/tests/JsonNodeUtils.java b/aws-lambda-java-tests/src/main/java/com/amazonaws/services/lambda/runtime/tests/JsonNodeUtils.java new file mode 100644 index 000000000..f9f4e1eb6 --- /dev/null +++ b/aws-lambda-java-tests/src/main/java/com/amazonaws/services/lambda/runtime/tests/JsonNodeUtils.java @@ -0,0 +1,110 @@ +package com.amazonaws.services.lambda.runtime.tests; + +import com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.JsonNode; +import com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.node.ObjectNode; +import java.util.Iterator; +import java.util.List; +import java.util.TreeSet; +import java.util.regex.Pattern; + +import org.joda.time.DateTime; + +/** + * Utility methods for working with shaded Jackson {@link JsonNode} trees. + * + *

+ * Package-private — not part of the public API. + *

+ */ +class JsonNodeUtils { + + private static final Pattern ISO_DATE_REGEX = Pattern.compile("\\d{4}-\\d{2}-\\d{2}T.+"); + + private JsonNodeUtils() { + } + + /** + * Recursively removes all fields whose value is {@code null} from the + * tree. This mirrors the serializer's {@code Include.NON_NULL} behaviour + * so that explicit nulls in the fixture don't cause false-positive diffs. + */ + static JsonNode stripNulls(JsonNode node) { + if (node.isObject()) { + ObjectNode obj = (ObjectNode) node; + Iterator fieldNames = obj.fieldNames(); + while (fieldNames.hasNext()) { + String field = fieldNames.next(); + if (obj.get(field).isNull()) { + fieldNames.remove(); + } else { + stripNulls(obj.get(field)); + } + } + } else if (node.isArray()) { + for (JsonNode element : node) { + stripNulls(element); + } + } + return node; + } + + /** + * Recursively walks both trees and collects human-readable diff lines. + */ + static void diffNodes(String path, JsonNode expected, JsonNode actual, List diffs) { + if (expected.equals(actual)) + return; + + // Compares two datetime strings by parsed instant, because DateTimeModule + // normalizes the format on serialization (e.g. "+0000" → "Z", "Z" → ".000Z") + if (areSameDateTime(expected.textValue(), actual.textValue())) { + return; + } + + if (expected.isObject() && actual.isObject()) { + TreeSet allKeys = new TreeSet<>(); + expected.fieldNames().forEachRemaining(allKeys::add); + actual.fieldNames().forEachRemaining(allKeys::add); + for (String key : allKeys) { + diffChild(path + "." + key, expected.get(key), actual.get(key), diffs); + } + } else if (expected.isArray() && actual.isArray()) { + for (int i = 0; i < Math.max(expected.size(), actual.size()); i++) { + diffChild(path + "[" + i + "]", expected.get(i), actual.get(i), diffs); + } + } else { + diffs.add("CHANGED " + path + " : " + summarize(expected) + " -> " + summarize(actual)); + } + } + + /** + * Compares two strings by parsed instant when both look like ISO-8601 dates, + * because DateTimeModule normalizes format on serialization + * (e.g. "+0000" → "Z", "Z" → ".000Z"). + */ + private static boolean areSameDateTime(String expected, String actual) { + if (expected == null || actual == null + || !ISO_DATE_REGEX.matcher(expected).matches() + || !ISO_DATE_REGEX.matcher(actual).matches()) { + return false; + } + return DateTime.parse(expected).equals(DateTime.parse(actual)); + } + + private static void diffChild(String path, JsonNode expected, JsonNode actual, List diffs) { + if (expected == null) + diffs.add("ADDED " + path + " = " + summarize(actual)); + else if (actual == null) + diffs.add("MISSING " + path + " (was " + summarize(expected) + ")"); + else + diffNodes(path, expected, actual, diffs); + } + + private static String summarize(JsonNode node) { + if (node == null) { + return ""; + } + String text = node.toString(); + return text.length() > 80 ? text.substring(0, 77) + "..." : text; + } +} diff --git a/aws-lambda-java-tests/src/main/java/com/amazonaws/services/lambda/runtime/tests/LambdaEventAssert.java b/aws-lambda-java-tests/src/main/java/com/amazonaws/services/lambda/runtime/tests/LambdaEventAssert.java new file mode 100644 index 000000000..f8d7e106d --- /dev/null +++ b/aws-lambda-java-tests/src/main/java/com/amazonaws/services/lambda/runtime/tests/LambdaEventAssert.java @@ -0,0 +1,142 @@ +package com.amazonaws.services.lambda.runtime.tests; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import com.amazonaws.services.lambda.runtime.serialization.PojoSerializer; +import com.amazonaws.services.lambda.runtime.serialization.events.LambdaEventSerializers; +import com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.JsonNode; +import com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.ObjectMapper; + +import java.util.ArrayList; +import java.util.List; + +/** + * Framework-agnostic assertion utilities for verifying Lambda event + * serialization. + * + *

+ * When opentest4j is on the classpath (e.g. JUnit 5.x / JUnit Platform), + * assertion failures are reported as + * {@code org.opentest4j.AssertionFailedError} + * which enables rich diff support in IDEs. Otherwise, falls back to plain + * {@link AssertionError}. + *

+ * + */ +public class LambdaEventAssert { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + /** + * Round-trip using the registered {@link LambdaEventSerializers} path + * (Jackson + mixins + DateModule + DateTimeModule + naming strategies). + * + *

+ * The check performs two consecutive round-trips + * (JSON → POJO → JSON → POJO → JSON) and compares the + * original JSON tree against the final output tree. A single structural + * comparison catches both: + *

+ *
    + *
  • Fields silently dropped during deserialization
  • + *
  • Non-idempotent serialization (output changes across round-trips)
  • + *
+ * + * @param fileName classpath resource name (must end with {@code .json}) + * @param targetClass the event class to deserialize into + * @throws AssertionError if the original and final JSON trees differ + */ + public static void assertSerializationRoundTrip(String fileName, Class targetClass) { + PojoSerializer serializer = LambdaEventSerializers.serializerFor(targetClass, + ClassLoader.getSystemClassLoader()); + + if (!fileName.endsWith(".json")) { + throw new IllegalArgumentException("File " + fileName + " must have json extension"); + } + + byte[] originalBytes; + try (InputStream stream = Thread.currentThread().getContextClassLoader().getResourceAsStream(fileName)) { + if (stream == null) { + throw new IllegalArgumentException("Could not load resource '" + fileName + "' from classpath"); + } + originalBytes = toBytes(stream); + } catch (IOException e) { + throw new UncheckedIOException("Failed to read resource " + fileName, e); + } + + // Two round-trips: original → POJO → JSON → POJO → JSON + // We are doing 2 passes so we can check instability problems + // like UnstablePojo in LambdaEventAssertTest + ByteArrayOutputStream firstOutput = roundTrip(new ByteArrayInputStream(originalBytes), serializer); + ByteArrayOutputStream secondOutput = roundTrip( + new ByteArrayInputStream(firstOutput.toByteArray()), serializer); + + // Compare original tree against final tree. + // Strip explicit nulls from the original because the serializer is + // configured with Include.NON_NULL — null fields are intentionally + // omitted and that is not a data-loss bug. + try { + JsonNode originalTree = JsonNodeUtils.stripNulls(MAPPER.readTree(originalBytes)); + JsonNode finalTree = MAPPER.readTree(secondOutput.toByteArray()); + + if (!originalTree.equals(finalTree)) { + List diffs = new ArrayList<>(); + JsonNodeUtils.diffNodes("", originalTree, finalTree, diffs); + + if (!diffs.isEmpty()) { + StringBuilder msg = new StringBuilder(); + msg.append("Serialization round-trip failure for ") + .append(targetClass.getSimpleName()) + .append(" (").append(diffs.size()).append(" difference(s)):\n"); + for (String diff : diffs) { + msg.append(" ").append(diff).append('\n'); + } + + String expected = MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(originalTree); + String actual = MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(finalTree); + throw buildAssertionError(msg.toString(), expected, actual); + } + } + } catch (IOException e) { + throw new UncheckedIOException("Failed to parse JSON for tree comparison", e); + } + } + + private static ByteArrayOutputStream roundTrip(InputStream stream, PojoSerializer serializer) { + T event = serializer.fromJson(stream); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + serializer.toJson(event, outputStream); + return outputStream; + } + + private static byte[] toBytes(InputStream stream) throws IOException { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + byte[] chunk = new byte[4096]; + int n; + while ((n = stream.read(chunk)) != -1) { + buffer.write(chunk, 0, n); + } + return buffer.toByteArray(); + } + + /** + * Tries to create an opentest4j AssertionFailedError for rich IDE diff + * support. Falls back to plain AssertionError if opentest4j is not on + * the classpath. + */ + private static AssertionError buildAssertionError(String message, String expected, String actual) { + try { + // opentest4j is provided by JUnit Platform (5.x) and enables + // IDE diff viewers to show expected vs actual side-by-side. + Class cls = Class.forName("org.opentest4j.AssertionFailedError"); + return (AssertionError) cls + .getConstructor(String.class, Object.class, Object.class) + .newInstance(message, expected, actual); + } catch (ReflectiveOperationException e) { + return new AssertionError(message + "\nExpected:\n" + expected + "\nActual:\n" + actual); + } + } +} diff --git a/aws-lambda-java-tests/src/main/java/com/amazonaws/services/lambda/runtime/tests/annotations/Event.java b/aws-lambda-java-tests/src/main/java/com/amazonaws/services/lambda/runtime/tests/annotations/Event.java new file mode 100644 index 000000000..d50112902 --- /dev/null +++ b/aws-lambda-java-tests/src/main/java/com/amazonaws/services/lambda/runtime/tests/annotations/Event.java @@ -0,0 +1,39 @@ +/* Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ +package com.amazonaws.services.lambda.runtime.tests.annotations; + +import com.amazonaws.services.lambda.runtime.tests.EventArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; + +import java.lang.annotation.*; + +/** + * This annotation must be used in conjunction with {@link org.junit.jupiter.params.ParameterizedTest}.
+ * It enables to inject an event (loaded from a json file) of the desired type into the current test.
+ * Example:
+ *
+ *     @ParameterizedTest
+ *     @Event(value = "sqs_event.json", type = SQSEvent.class)
+ *     public void testInjectEvent(SQSEvent event) {
+ *         assertThat(event).isNotNull();
+ *         assertThat(event.getRecords()).hasSize(1);
+ *     }
+ * 
+ */ +@Documented +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@ArgumentsSource(EventArgumentsProvider.class) +public @interface Event { + + /** + * Path and file name of the json event + * @return the file name (including the path) + */ + String value(); + + /** + * Type of the event (for example, one of the aws-lambda-java-events), or your own type + * @return the type of the event + */ + Class type() default Void.class; +} diff --git a/aws-lambda-java-tests/src/main/java/com/amazonaws/services/lambda/runtime/tests/annotations/Events.java b/aws-lambda-java-tests/src/main/java/com/amazonaws/services/lambda/runtime/tests/annotations/Events.java new file mode 100644 index 000000000..240caab4f --- /dev/null +++ b/aws-lambda-java-tests/src/main/java/com/amazonaws/services/lambda/runtime/tests/annotations/Events.java @@ -0,0 +1,81 @@ +/* Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ +package com.amazonaws.services.lambda.runtime.tests.annotations; + +import com.amazonaws.services.lambda.runtime.tests.EventsArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; + +import java.lang.annotation.*; + +/** + * This annotation must be used in conjunction with {@link org.junit.jupiter.params.ParameterizedTest}.
+ * It enables to inject multiple events (loaded from json files) of the desired type into the current test.
+ *

+ * Several notations are possible according to what you want to do: + *

    + *
  • + * Using the folder parameter is the more straightforward, and it will use all files in the folder
    + *
    + *     @ParameterizedTest
    + *     @Events(folder = "sqs", type = SQSEvent.class)
    + *     public void testInjectEventsFromFolder(SQSEvent event) {
    + *         assertThat(event).isNotNull();
    + *         assertThat(event.getRecords()).hasSize(1);
    + *     }
    + * 
    + *
  • + *
  • + * Or you can list all the {@link Event}s
    + *
    + * @ParameterizedTest
    + *     @Events(
    + *             events = {
    + *                     @Event("sqs/sqs_event.json"),
    + *                     @Event("sqs/sqs_event2.json"),
    + *             },
    + *             type = SQSEvent.class
    + *     )
    + *     public void testInjectEvents(SQSEvent event) {
    + *         assertThat(event).isNotNull();
    + *         assertThat(event.getRecords()).hasSize(1);
    + *     }
    + *
    + *     @ParameterizedTest
    + *     @Events(
    + *             events = {
    + *                     @Event(value = "sqs/sqs_event.json", type = SQSEvent.class),
    + *                     @Event(value = "sqs/sqs_event2.json", type = SQSEvent.class),
    + *             }
    + *     )
    + *     public void testInjectEvents2(SQSEvent event) {
    + *         assertThat(event).isNotNull();
    + *         assertThat(event.getRecords()).hasSize(1);
    + *     }
    + * 
    + *
  • + *
+ *

+ */ +@Documented +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@ArgumentsSource(EventsArgumentsProvider.class) +public @interface Events { + + /** + * Folder where to find json files containing events + * @return the folder name + */ + String folder() default ""; + + /** + * Type of the events (for example, one of the aws-lambda-java-events), or your own type + * @return the type of the events + */ + Class type() default Void.class; + + /** + * Mutually exclusive with folder + * @return the array of events + */ + Event[] events() default {}; +} diff --git a/aws-lambda-java-tests/src/main/java/com/amazonaws/services/lambda/runtime/tests/annotations/HandlerParams.java b/aws-lambda-java-tests/src/main/java/com/amazonaws/services/lambda/runtime/tests/annotations/HandlerParams.java new file mode 100644 index 000000000..3e656cc9c --- /dev/null +++ b/aws-lambda-java-tests/src/main/java/com/amazonaws/services/lambda/runtime/tests/annotations/HandlerParams.java @@ -0,0 +1,63 @@ +/* Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ +package com.amazonaws.services.lambda.runtime.tests.annotations; + +import com.amazonaws.services.lambda.runtime.tests.HandlerParamsArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; + +import java.lang.annotation.*; + +/** + * This annotation must be used in conjunction with {@link org.junit.jupiter.params.ParameterizedTest}.
+ * It enables to inject Events and Responses into the current test.
+ * Either use the {@link #event()} and {@link #response()} for a single event/response + * or {@link #events()} and {@link #responses()} for multiple ones.
+ * + * Example:
+ *
+ * @ParameterizedTest
+ * @HandlerParams(
+ *         event = @Event(value = "apigw/events/apigw_event.json", type = APIGatewayProxyRequestEvent.class),
+ *         response = @Response(value = "apigw/responses/apigw_response.json", type = APIGatewayProxyResponseEvent.class))
+ * public void testSingleEventResponse(APIGatewayProxyRequestEvent event, APIGatewayProxyResponseEvent response) {
+ * }
+ *
+ * @ParameterizedTest
+ * @HandlerParams(
+ *         events = @Events(folder = "apigw/events/", type = APIGatewayProxyRequestEvent.class),
+ *         responses = @Responses(folder = "apigw/responses/", type = APIGatewayProxyResponseEvent.class))
+ * public void testMultipleEventsResponsesInFolder(APIGatewayProxyRequestEvent event, APIGatewayProxyResponseEvent response) {
+ * }
+ *
+ * @ParameterizedTest
+ * @HandlerParams(
+ *         events = @Events(
+ *                 events = {
+ *                         @Event("apigw/events/apigw_event.json"),
+ *                         @Event("apigw/events/apigw_event2.json"),
+ *                 },
+ *                 type = APIGatewayProxyRequestEvent.class
+ *         ),
+ *         responses = @Responses(
+ *                 responses = {
+ *                         @Response("apigw/responses/apigw_response.json"),
+ *                         @Response("apigw/responses/apigw_response2.json")
+ *                 },
+ *                 type = APIGatewayProxyResponseEvent.class
+ *         )
+ * )
+ * public void testMultipleEventsResponses(APIGatewayProxyRequestEvent event, APIGatewayProxyResponseEvent response) {
+ * }
+ * 
+ */ +@Documented +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@ArgumentsSource(HandlerParamsArgumentsProvider.class) +public @interface HandlerParams { + + Event event() default @Event(""); + Response response() default @Response(""); + + Events events() default @Events; + Responses responses() default @Responses; +} diff --git a/aws-lambda-java-tests/src/main/java/com/amazonaws/services/lambda/runtime/tests/annotations/Response.java b/aws-lambda-java-tests/src/main/java/com/amazonaws/services/lambda/runtime/tests/annotations/Response.java new file mode 100644 index 000000000..d53ef128a --- /dev/null +++ b/aws-lambda-java-tests/src/main/java/com/amazonaws/services/lambda/runtime/tests/annotations/Response.java @@ -0,0 +1,25 @@ +/* Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ +package com.amazonaws.services.lambda.runtime.tests.annotations; + +import java.lang.annotation.*; + +/** + * This annotation must be used in conjunction with {@link HandlerParams}. + */ +@Documented +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Response { + + /** + * Path and file name of the json response + * @return the file name (including the path) + */ + String value(); + + /** + * Type of the response + * @return the type of the response + */ + Class type() default Void.class; +} diff --git a/aws-lambda-java-tests/src/main/java/com/amazonaws/services/lambda/runtime/tests/annotations/Responses.java b/aws-lambda-java-tests/src/main/java/com/amazonaws/services/lambda/runtime/tests/annotations/Responses.java new file mode 100644 index 000000000..8788baa7e --- /dev/null +++ b/aws-lambda-java-tests/src/main/java/com/amazonaws/services/lambda/runtime/tests/annotations/Responses.java @@ -0,0 +1,31 @@ +/* Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ +package com.amazonaws.services.lambda.runtime.tests.annotations; + +import java.lang.annotation.*; + +/** + * This annotation must be used in conjunction with {@link HandlerParams}. + */ +@Documented +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Responses { + + /** + * Folder where to find json files containing responses + * @return the folder name + */ + String folder() default ""; + + /** + * Type of the responses + * @return the type of the responses + */ + Class type() default Void.class; + + /** + * Mutually exclusive with folder + * @return the array of responses + */ + Response[] responses() default {}; +} diff --git a/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/EventLoaderTest.java b/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/EventLoaderTest.java new file mode 100644 index 000000000..43030bbca --- /dev/null +++ b/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/EventLoaderTest.java @@ -0,0 +1,562 @@ +/* Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ +package com.amazonaws.services.lambda.runtime.tests; + +import com.amazonaws.services.lambda.runtime.events.APIGatewayCustomAuthorizerEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayV2CustomAuthorizerEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent; +import com.amazonaws.services.lambda.runtime.events.ActiveMQEvent; +import com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerRequestEvent; +import com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent; +import com.amazonaws.services.lambda.runtime.events.CloudFrontEvent; +import com.amazonaws.services.lambda.runtime.events.CloudWatchCompositeAlarmEvent; +import com.amazonaws.services.lambda.runtime.events.CloudWatchCompositeAlarmEvent.AlarmData; +import com.amazonaws.services.lambda.runtime.events.CloudWatchCompositeAlarmEvent.Configuration; +import com.amazonaws.services.lambda.runtime.events.CloudWatchCompositeAlarmEvent.PreviousState; +import com.amazonaws.services.lambda.runtime.events.CloudWatchCompositeAlarmEvent.State; +import com.amazonaws.services.lambda.runtime.events.CloudWatchLogsEvent; +import com.amazonaws.services.lambda.runtime.events.CloudWatchMetricAlarmEvent; +import com.amazonaws.services.lambda.runtime.events.CodeCommitEvent; +import com.amazonaws.services.lambda.runtime.events.CognitoUserPoolPreTokenGenerationEventV2; +import com.amazonaws.services.lambda.runtime.events.ConfigEvent; +import com.amazonaws.services.lambda.runtime.events.ConnectEvent; +import com.amazonaws.services.lambda.runtime.events.DynamodbEvent; +import com.amazonaws.services.lambda.runtime.events.KafkaEvent; +import com.amazonaws.services.lambda.runtime.events.KinesisEvent; +import com.amazonaws.services.lambda.runtime.events.KinesisFirehoseEvent; +import com.amazonaws.services.lambda.runtime.events.LambdaDestinationEvent; +import com.amazonaws.services.lambda.runtime.events.LexEvent; +import com.amazonaws.services.lambda.runtime.events.MSKFirehoseEvent; +import com.amazonaws.services.lambda.runtime.events.RabbitMQEvent; +import com.amazonaws.services.lambda.runtime.events.S3Event; +import com.amazonaws.services.lambda.runtime.events.SNSEvent; +import com.amazonaws.services.lambda.runtime.events.SQSEvent; +import com.amazonaws.services.lambda.runtime.events.ScheduledEvent; +import com.amazonaws.services.lambda.runtime.events.SecretsManagerRotationEvent; +import com.amazonaws.services.lambda.runtime.events.models.dynamodb.AttributeValue; +import com.amazonaws.services.lambda.runtime.events.models.dynamodb.Record; +import com.amazonaws.services.lambda.runtime.events.models.dynamodb.StreamRecord; +import org.joda.time.DateTime; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.Date; +import java.util.List; +import java.util.Map; + +import static java.time.Instant.ofEpochSecond; +import static org.assertj.core.api.Assertions.*; + +public class EventLoaderTest { + + @Test + public void testLoadApiGatewayRestEvent() { + APIGatewayProxyRequestEvent event = EventLoader.loadApiGatewayRestEvent("apigw_rest_event.json"); + + assertThat(event).isNotNull(); + assertThat(event.getBody()).isEqualTo("Hello from Lambda!"); + assertThat(event.getHeaders()).hasSize(2); + } + + @Test + public void testLoadApiGatewayHttpEvent() { + APIGatewayV2HTTPEvent event = EventLoader.loadApiGatewayHttpEvent("apigw_http_event.json"); + + assertThat(event).isNotNull(); + assertThat(event.getBody()).isEqualTo("Hello from Lambda!!"); + } + + @Test + public void testLoadAPIGatewayCustomAuthorizerEvent() { + APIGatewayCustomAuthorizerEvent event = EventLoader.loadAPIGatewayCustomAuthorizerEvent("apigw_auth.json"); + + assertThat(event).isNotNull(); + assertThat(event.getRequestContext().getHttpMethod()).isEqualTo("GET"); + assertThat(event.getHeaders()).hasSize(8); + } + + @Test + public void testLoadAPIGatewayV2CustomAuthorizerEvent() { + APIGatewayV2CustomAuthorizerEvent event = EventLoader.loadAPIGatewayV2CustomAuthorizerEvent("apigw_auth_v2.json"); + + assertThat(event).isNotNull(); + assertThat(event.getRequestContext().getHttp().getMethod()).isEqualTo("POST"); + // getTime() converts the raw string "12/Mar/2020:19:03:58 +0000" into a DateTime object; + // Jackson then serializes it as ISO-8601 "2020-03-12T19:03:58.000Z" + assertThat(event.getRequestContext().getTime().toInstant().getMillis()) + .isEqualTo(DateTime.parse("2020-03-12T19:03:58.000Z").toInstant().getMillis()); + // getTimeEpoch() converts the raw long into an Instant; + // Jackson then serializes it as a decimal seconds value + assertThat(event.getRequestContext().getTimeEpoch()).isEqualTo(Instant.ofEpochMilli(1583348638390L)); + } + + @Test + public void testLoadApplicationLoadBalancerRequestEvent() { + ApplicationLoadBalancerRequestEvent event = EventLoader.loadApplicationLoadBalancerRequestEvent("elb_event.json"); + + assertThat(event).isNotNull(); + assertThat(event.getBody()).isEqualTo("Hello from ELB"); + } + + @Test + public void testLoadConfigEvent() { + ConfigEvent event = EventLoader.loadConfigEvent("config_event.json"); + + assertThat(event).isNotNull(); + assertThat(event.getConfigRuleArn()).isEqualTo("arn:aws:config:eu-central-1:123456789012:config-rule/config-rule-0123456"); + assertThat(event.getConfigRuleName()).isEqualTo("change-triggered-config-rule"); + } + + @Test + public void testLoadKafkaEvent() { + KafkaEvent event = EventLoader.loadKafkaEvent("kafka_event.json"); + + assertThat(event).isNotNull(); + assertThat(event.getEventSourceArn()).isEqualTo("arn:aws:kafka:us-east-1:123456789012:cluster/vpc-3432434/4834-3547-3455-9872-7929"); + assertThat(event.getBootstrapServers()).isEqualTo("b-2.demo-cluster-1.a1bcde.c1.kafka.us-east-1.amazonaws.com:9092,b-1.demo-cluster-1.a1bcde.c1.kafka.us-east-1.amazonaws.com:9092"); + + KafkaEvent.KafkaEventRecord record = event.getRecords().get("mytopic-01").get(0); + assertThat(record.getValue()).decodedAsBase64().asString().isEqualTo("Hello from Kafka !!"); + + String headerValue = new String(record.getHeaders().get(0).get("headerKey")); + assertThat(headerValue).isEqualTo("headerValue"); + } + + @Test + public void testLoadLambdaDestinationEvent() { + LambdaDestinationEvent event = EventLoader.loadLambdaDestinationEvent("lambda_destination_event.json"); + + assertThat(event).isNotNull(); + assertThat(event.getTimestamp()).isEqualTo(DateTime.parse("2019-11-24T21:52:47.333Z")); + assertThat(event.getRequestContext().getFunctionArn()).isEqualTo("arn:aws:lambda:sa-east-1:123456678912:function:event-destinations:$LATEST"); + assertThat(event.getRequestPayload().get("Success")).isEqualTo(false); + } + + @Test + public void testLoadLexEvent() { + LexEvent event = EventLoader.loadLexEvent("lex_event.json"); + + assertThat(event).isNotNull(); + assertThat(event.getInvocationSource()).isEqualTo("DialogCodeHook"); + assertThat(event.getSessionAttributes()).hasSize(1); + assertThat(event.getCurrentIntent().getName()).isEqualTo("BookHotel"); + assertThat(event.getCurrentIntent().getSlots()).hasSize(4); + assertThat(event.getBot().getName()).isEqualTo("BookTrip"); + // Jackson leniently coerces the JSON number for "Nights" into a String + // because slots is typed as Map + assertThat(event.getCurrentIntent().getSlots().get("Nights")).isInstanceOf(String.class); + } + + @Test + public void testLoadKinesisFirehoseEvent() { + KinesisFirehoseEvent event = EventLoader.loadKinesisFirehoseEvent("firehose_event.json"); + + assertThat(event).isNotNull(); + assertThat(event.getDeliveryStreamArn()).isEqualTo("arn:aws:kinesis:EXAMPLE"); + assertThat(event.getRecords()).hasSize(1); + assertThat(event.getRecords().get(0).getData().array()).asString().isEqualTo("Hello, this is a test 123."); + } + + @Test + public void testLoadMSKFirehoseEvent() { + MSKFirehoseEvent event = EventLoader.loadMSKFirehoseEvent("msk_firehose_event.json"); + + assertThat(event).isNotNull(); + assertThat(event.getSourceMSKArn()).isEqualTo("arn:aws:kafka:EXAMPLE"); + assertThat(event.getDeliveryStreamArn()).isEqualTo("arn:aws:firehose:EXAMPLE"); + assertThat(event.getRecords()).hasSize(1); + assertThat(event.getRecords().get(0).getKafkaRecordValue().array()).asString().isEqualTo("{\"Name\":\"Hello World\"}"); + assertThat(event.getRecords().get(0).getApproximateArrivalTimestamp()).asString().isEqualTo("1716369573887"); + assertThat(event.getRecords().get(0).getMskRecordMetadata()).asString().isEqualTo("{offset=0, partitionId=1, approximateArrivalTimestamp=1716369573887}"); + // Jackson leniently coerces the JSON number in mskRecordMetadata into a String + // because the map is typed as Map + Map metadata = event.getRecords().get(0).getMskRecordMetadata(); + assertThat(metadata.get("approximateArrivalTimestamp")).isInstanceOf(String.class); + } + + @Test + public void testLoadS3Event() { + S3Event event = EventLoader.loadS3Event("s3_event.json"); + assertThat(event).isNotNull(); + assertThat(event.getRecords()).hasSize(1); + } + + @Test + public void testLoadSQSEvent() { + SQSEvent event = EventLoader.loadSQSEvent("sqs/sqs_event_nobody.json"); + assertThat(event).isNotNull(); + assertThat(event.getRecords()).hasSize(1); + assertThat(event.getRecords().get(0).getEventSourceArn()).isEqualTo("arn:aws:sqs:eu-central-1:123456789012:TestLambda"); + } + + @Test + public void testLoadSNSEvent() { + SNSEvent event = EventLoader.loadSNSEvent("sns_event.json"); + assertThat(event).isNotNull(); + assertThat(event.getRecords()).hasSize(1); + + SNSEvent.SNSRecord record = event.getRecords().get(0); + assertThat(record.getEventSource()).isEqualTo("aws:sns"); + assertThat(record.getEventVersion()).isEqualTo("1.0"); + assertThat(record.getEventSubscriptionArn()).isEqualTo("arn:aws:sns:eu-central-1:123456789012:TopicSendToMe:e3ddc7d5-2f86-40b8-a13d-3362f94fd8dd"); + + SNSEvent.SNS sns = record.getSNS(); + assertThat(sns) + .returns("Test sns message", from(SNSEvent.SNS::getSubject)) + .returns("{\n \"id\": 42,\n \"name\": \"Bob\"\n}", from(SNSEvent.SNS::getMessage)) + .returns("arn:aws:sns:eu-central-1:123456789012:TopicSendToMe", from(SNSEvent.SNS::getTopicArn)) + .returns("dc918f50-80c6-56a2-ba33-d8a9bbf013ab", from(SNSEvent.SNS::getMessageId)) + .returns(DateTime.parse("2020-10-08T16:06:14.656Z"), from(SNSEvent.SNS::getTimestamp)) + .returns("https://sns.eu-central-1.amazonaws.com/?Action=Unsubscribe", SNSEvent.SNS::getUnsubscribeUrl); + assertThat(sns.getMessageAttributes()).containsKey("name"); + assertThat(sns.getMessageAttributes().get("name").getValue()).isEqualTo("Bob"); + assertThat(sns.getMessageAttributes().get("name").getType()).isEqualTo("String"); + } + + @Test + public void testLoadDynamoEvent() { + DynamodbEvent event = EventLoader.loadDynamoDbEvent("ddb/dynamo_event.json"); + assertThat(event).isNotNull(); + assertThat(event.getRecords()).hasSize(3); + assertDynamoDbStreamRecord(event.getRecords().get(1)); + } + + @Test + public void testLoadDynamoDbStreamRecord() { + assertDynamoDbStreamRecord(EventLoader.loadDynamoDbStreamRecord("ddb/dynamo_ddb_stream_record.json")); + } + + private static void assertDynamoDbStreamRecord(final DynamodbEvent.DynamodbStreamRecord record) { + assertThat(record) + .isNotNull() + .returns("arn:aws:dynamodb:eu-central-1:123456789012:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899", from(DynamodbEvent.DynamodbStreamRecord::getEventSourceARN)) + .returns("MODIFY", from(Record::getEventName)); + + StreamRecord streamRecord = record.getDynamodb(); + assertThat(streamRecord) + .returns("4421584500000000017450439092", StreamRecord::getSequenceNumber) + .returns(59L, StreamRecord::getSizeBytes) + .returns("NEW_AND_OLD_IMAGES", StreamRecord::getStreamViewType) + .returns(Date.from(ofEpochSecond(1635734407).plusNanos(123456789)), StreamRecord::getApproximateCreationDateTime); + + assertThat(streamRecord.getKeys()) + .isNotNull() + .contains(entry("Id", new AttributeValue().withN("101"))); + assertThat(streamRecord.getNewImage()) + .isNotNull() + .containsAnyOf( + entry("Message", new AttributeValue("This item has changed")), + entry("Id", new AttributeValue().withN("101"))); + assertThat(streamRecord.getOldImage()) + .isNotNull() + .containsAnyOf( + entry("Message", new AttributeValue("New item!")), + entry("Id", new AttributeValue().withN("101"))); + } + + @Test + public void testLoadKinesisEvent() { + KinesisEvent event = EventLoader.loadKinesisEvent("kinesis_event.json"); + assertThat(event).isNotNull(); + assertThat(event.getRecords()).hasSize(1); + + KinesisEvent.Record record = event.getRecords().get(0).getKinesis(); + assertThat(record.getEncryptionType()).isEqualTo("NONE"); + assertThat(record.getApproximateArrivalTimestamp()).isEqualTo(Date.from(ofEpochSecond(1428537600))); + assertThat(new String(record.getData().array())).isEqualTo("Hello, this is a test 123."); + } + + @Test + public void testLoadActiveMQEvent() { + ActiveMQEvent event = EventLoader.loadActiveMQEvent("mq_event.json"); + assertThat(event).isNotNull(); + assertThat(event.getMessages()).hasSize(2); + + assertThat(event.getMessages().get(0).getMessageID()).isEqualTo("ID:b-9bcfa592-423a-4942-879d-eb284b418fc8-1.mq.us-west-2.amazonaws.com-37557-1234520418293-4:1:1:1:1"); + assertThat(event.getMessages().get(1).getMessageID()).isEqualTo("ID:b-8bcfa572-428a-4642-879d-eb284b418fc8-1.mq.us-west-2.amazonaws.com-37557-1234520418293-4:1:1:1:1"); + } + + @Test + public void testLoadActiveMQEventWithProperties() { + ActiveMQEvent event = EventLoader.loadActiveMQEvent("mq_event.json"); + assertThat(event).isNotNull(); + assertThat(event.getMessages()).hasSize(2); + assertThat(event.getMessages().get(0).getProperties().get("testKey")).isEqualTo("testValue"); + assertThat(event.getMessages().get(1).getProperties().get("testKey")).isEqualTo("testValue"); + } + + @Test + public void testLoadCodeCommitEvent() { + CodeCommitEvent event = EventLoader.loadCodeCommitEvent("codecommit_event.json"); + assertThat(event).isNotNull(); + assertThat(event.getRecords()).hasSize(1); + + CodeCommitEvent.Record record = event.getRecords().get(0); + assertThat(record.getEventSourceArn()).isEqualTo("arn:aws:codecommit:eu-central-1:123456789012:my-repo"); + assertThat(record.getUserIdentityArn()).isEqualTo("arn:aws:iam::123456789012:root"); + assertThat(record.getEventTime()).isEqualTo(DateTime.parse("2016-01-01T23:59:59.000+0000")); + + assertThat(record.getCodeCommit().getReferences()).hasSize(1); + CodeCommitEvent.Reference reference = record.getCodeCommit().getReferences().get(0); + assertThat(reference.getCommit()).isEqualTo("5c4ef1049f1d27deadbeeff313e0730018be182b"); + assertThat(reference.getRef()).isEqualTo("refs/heads/master"); + } + + @Test + public void testLoadCloudWatchLogsEvent() { + CloudWatchLogsEvent cloudWatchLogsEvent = EventLoader.loadCloudWatchLogsEvent("cloudwatchlogs_event.json"); + assertThat(cloudWatchLogsEvent).isNotNull(); + assertThat(cloudWatchLogsEvent.getAwsLogs().getData()).isEqualTo("H4sIAAAAAAAAAHWPwQqCQBCGX0Xm7EFtK+smZBEUgXoLCdMhFtKV3akI8d0bLYmibvPPN3wz00CJxmQnTO41whwWQRIctmEcB6sQbFC3CjW3XW8kxpOpP+OC22d1Wml1qZkQGtoMsScxaczKN3plG8zlaHIta5KqWsozoTYw3/djzwhpLwivWFGHGpAFe7DL68JlBUk+l7KSN7tCOEJ4M3/qOI49vMHj+zCKdlFqLaU2ZHV2a4Ct/an0/ivdX8oYc1UVX860fQDQiMdxRQEAAA=="); + } + + @Test + public void testLoadCloudFrontEvent() { + CloudFrontEvent event = EventLoader.loadCloudFrontEvent("cloudfront_event.json"); + assertThat(event).isNotNull(); + assertThat(event.getRecords()).hasSize(1); + assertThat(event.getRecords().get(0).getCf().getConfig().getDistributionId()).isEqualTo("EXAMPLE"); + } + + @Test + public void testLoadScheduledEvent() { + ScheduledEvent event = EventLoader.loadScheduledEvent("cloudwatch_event.json"); + assertThat(event).isNotNull(); + assertThat(event.getDetailType()).isEqualTo("Scheduled Event"); + assertThat(event.getTime()).isEqualTo(DateTime.parse("2020-09-30T15:58:34Z")); + } + + @Test + public void testLoadConnectEvent() { + ConnectEvent event = EventLoader.loadConnectEvent("connect_event.json"); + assertThat(event).isNotNull(); + + ConnectEvent.ContactData contactData = event.getDetails().getContactData(); + assertThat(contactData) + .returns("VOICE", from(ConnectEvent.ContactData::getChannel)) + .returns("5ca32fbd-8f92-46af-92a5-6b0f970f0efe", from(ConnectEvent.ContactData::getContactId)) + .returns("6ca32fbd-8f92-46af-92a5-6b0f970f0efe", from(ConnectEvent.ContactData::getInitialContactId)) + .returns("API", from(ConnectEvent.ContactData::getInitiationMethod)) + .returns("arn:aws:connect:eu-central-1:123456789012:instance/9308c2a1-9bc6-4cea-8290-6c0b4a6d38fa", from(ConnectEvent.ContactData::getInstanceArn)) + .returns("4ca32fbd-8f92-46af-92a5-6b0f970f0efe", from(ConnectEvent.ContactData::getPreviousContactId)); + + assertThat(contactData.getCustomerEndpoint()) + .returns("+11234567890",from(ConnectEvent.CustomerEndpoint::getAddress)) + .returns("TELEPHONE_NUMBER",from(ConnectEvent.CustomerEndpoint::getType)); + + assertThat(contactData.getSystemEndpoint()) + .returns("+21234567890",from(ConnectEvent.SystemEndpoint::getAddress)) + .returns("TELEPHONE_NUMBER",from(ConnectEvent.SystemEndpoint::getType)); + + assertThat(contactData.getQueue()) + .isNotNull() + .returns("SampleQueue", from(ConnectEvent.Queue::getName)) + .returns("arn:aws:connect:eu-central-1:123456789012:instance/9308c2a1-9bc6-4cea-8290-6c0b4a6d38fa", + from(ConnectEvent.Queue::getARN) + ); + + } + + @Test + public void testLoadCloudFormationCustomResourceEvent() { + CloudFormationCustomResourceEvent event = EventLoader.loadCloudFormationCustomResourceEvent("cloudformation_event.json"); + assertThat(event).isNotNull(); + assertThat(event) + .returns("Update", from(CloudFormationCustomResourceEvent::getRequestType)) + .returns("http://pre-signed-S3-url-for-response", from(CloudFormationCustomResourceEvent::getResponseUrl)) + .returns("arn:aws:cloudformation:eu-central-1:123456789012:stack/MyStack/guid", from(CloudFormationCustomResourceEvent::getStackId)) + .returns("unique id for this create request", from(CloudFormationCustomResourceEvent::getRequestId)) + .returns("Custom::TestResource", from(CloudFormationCustomResourceEvent::getResourceType)) + .returns("MyTestResource", from(CloudFormationCustomResourceEvent::getLogicalResourceId)) + .returns("MyTestResourceId", from(CloudFormationCustomResourceEvent::getPhysicalResourceId)) + .returns("abcd", from(CloudFormationCustomResourceEvent::getServiceToken)); + + Map resourceProperties = event.getResourceProperties(); + assertThat(resourceProperties).hasSize(2); + assertThat(resourceProperties.get("StackName")).isEqualTo("MyStack"); + assertThat(resourceProperties.get("List")).isInstanceOf(List.class); + assertThat(resourceProperties.get("List")).asList().hasSize(3); + + Map oldResourceProperties = event.getOldResourceProperties(); + assertThat(oldResourceProperties).hasSize(2); + assertThat(oldResourceProperties.get("StackName")).isEqualTo("MyStack"); + assertThat(oldResourceProperties.get("List")).isInstanceOf(List.class); + assertThat(oldResourceProperties.get("List")).asList().hasSize(1); + } + + @Test + public void testLoadSecretsManagerRotationEvent() { + SecretsManagerRotationEvent event = EventLoader.loadSecretsManagerRotationEvent("secrets_rotation_event.json"); + assertThat(event).isNotNull(); + assertThat(event) + .returns("123e4567-e89b-12d3-a456-426614174000", from(SecretsManagerRotationEvent::getClientRequestToken)) + .returns("arn:aws:secretsmanager:eu-central-1:123456789012:secret:/powertools/secretparam-xBPaJ5", from(SecretsManagerRotationEvent::getSecretId)) + .returns("CreateSecret", from(SecretsManagerRotationEvent::getStep)) + .returns("8a4cc1ac-82ea-47c7-bd9f-aeb370b1b6a6", from(SecretsManagerRotationEvent::getRotationToken)); +; + } + + @Test + public void testLoadRabbitMQEvent() { + RabbitMQEvent event = EventLoader.loadRabbitMQEvent("rabbitmq_event.json"); + assertThat(event).isNotNull(); + assertThat(event) + .returns("aws:rmq", from(RabbitMQEvent::getEventSource)) + .returns("arn:aws:mq:us-west-2:112556298976:broker:test:b-9bcfa592-423a-4942-879d-eb284b418fc8", from(RabbitMQEvent::getEventSourceArn)); + + Map> messagesByQueue = event.getRmqMessagesByQueue(); + assertThat(messagesByQueue).isNotEmpty(); + List messages = messagesByQueue.get("test::/"); + assertThat(messages).hasSize(1); + RabbitMQEvent.RabbitMessage firstMessage = messages.get(0); + assertThat(firstMessage) + .returns(false, RabbitMQEvent.RabbitMessage::getRedelivered) + .returns("eyJ0aW1lb3V0IjowLCJkYXRhIjoiQ1pybWYwR3c4T3Y0YnFMUXhENEUifQ==", RabbitMQEvent.RabbitMessage::getData); + + RabbitMQEvent.BasicProperties basicProperties = firstMessage.getBasicProperties(); + assertThat(basicProperties) + .returns("text/plain", from(RabbitMQEvent.BasicProperties::getContentType)) + .returns(1, from(RabbitMQEvent.BasicProperties::getDeliveryMode)) + .returns(34, from(RabbitMQEvent.BasicProperties::getPriority)) + .returns(60000, from(RabbitMQEvent.BasicProperties::getExpiration)) + .returns("AIDACKCEVSQ6C2EXAMPLE", from(RabbitMQEvent.BasicProperties::getUserId)) + .returns(80, from(RabbitMQEvent.BasicProperties::getBodySize)) + .returns("Jan 1, 1970, 12:33:41 AM", from(RabbitMQEvent.BasicProperties::getTimestamp)); + // Jackson leniently coerces the JSON string "60000" for expiration into int + // because the model field is typed as int + + Map headers = basicProperties.getHeaders(); + assertThat(headers).hasSize(3); + Map> header1 = (Map>) headers.get("header1"); + assertThat(header1.get("bytes")).contains(118, 97, 108, 117, 101, 49); + assertThat((Integer) headers.get("numberInHeader")).isEqualTo(10); + } + + @Test + public void testLoadCognitoUserPoolPreTokenGenerationEventV2() { + CognitoUserPoolPreTokenGenerationEventV2 event = EventLoader.loadCognitoUserPoolPreTokenGenerationEventV2("cognito_user_pool_pre_token_generation_event_v2.json"); + assertThat(event).isNotNull(); + assertThat(event) + .returns("2", from(CognitoUserPoolPreTokenGenerationEventV2::getVersion)) + .returns("us-east-1", from(CognitoUserPoolPreTokenGenerationEventV2::getRegion)) + .returns("TokenGeneration_Authentication", from(CognitoUserPoolPreTokenGenerationEventV2::getTriggerSource)); + + CognitoUserPoolPreTokenGenerationEventV2.Request request = event.getRequest(); + String[] requestScopes = request.getScopes(); + assertThat("aws.cognito.signin.user.admin").isEqualTo(requestScopes[0]); + + CognitoUserPoolPreTokenGenerationEventV2.Response response = event.getResponse(); + String[] groupsToOverride = response.getClaimsAndScopeOverrideDetails().getGroupOverrideDetails().getGroupsToOverride(); + String[] iamRolesToOverride = response.getClaimsAndScopeOverrideDetails().getGroupOverrideDetails().getIamRolesToOverride(); + String preferredRole = response.getClaimsAndScopeOverrideDetails().getGroupOverrideDetails().getPreferredRole(); + + assertThat("group-99").isEqualTo(groupsToOverride[0]); + assertThat("group-98").isEqualTo(groupsToOverride[1]); + assertThat("arn:aws:iam::123456789012:role/sns_caller99").isEqualTo(iamRolesToOverride[0]); + assertThat("arn:aws:iam::123456789012:role/sns_caller98").isEqualTo(iamRolesToOverride[1]); + assertThat("arn:aws:iam::123456789012:role/sns_caller_99").isEqualTo(preferredRole); + } + + @Test + public void testCloudWatchCompositeAlarmEvent() { + CloudWatchCompositeAlarmEvent event = EventLoader.loadCloudWatchCompositeAlarmEvent("cloudwatch_composite_alarm.json"); + assertThat(event).isNotNull(); + assertThat(event) + .returns("aws.cloudwatch", from(CloudWatchCompositeAlarmEvent::getSource)) + .returns("arn:aws:cloudwatch:us-east-1:111122223333:alarm:SuppressionDemo.Main", from(CloudWatchCompositeAlarmEvent::getAlarmArn)) + .returns("111122223333", from(CloudWatchCompositeAlarmEvent::getAccountId)) + .returns("2023-08-04T12:56:46.138+0000", from(CloudWatchCompositeAlarmEvent::getTime)) + .returns("us-east-1", from(CloudWatchCompositeAlarmEvent::getRegion)); + + AlarmData alarmData = event.getAlarmData(); + assertThat(alarmData).isNotNull(); + assertThat(alarmData) + .returns("CompositeDemo.Main", from(AlarmData::getAlarmName)); + + State state = alarmData.getState(); + assertThat(state).isNotNull(); + assertThat(state) + .returns("ALARM", from(State::getValue)) + .returns("arn:aws:cloudwatch:us-east-1:111122223333:alarm:CompositeDemo.FirstChild transitioned to ALARM at Friday 04 August, 2023 12:54:46 UTC", from(State::getReason)) + .returns("{\"triggeringAlarms\":[{\"arn\":\"arn:aws:cloudwatch:us-east-1:111122223333:alarm:CompositeDemo.FirstChild\",\"state\":{\"value\":\"ALARM\",\"timestamp\":\"2023-08-04T12:54:46.138+0000\"}}]}", from(State::getReasonData)) + .returns("2023-08-04T12:56:46.138+0000", from(State::getTimestamp)); + + PreviousState previousState = alarmData.getPreviousState(); + assertThat(previousState).isNotNull(); + assertThat(previousState) + .returns("ALARM", from(PreviousState::getValue)) + .returns("arn:aws:cloudwatch:us-east-1:111122223333:alarm:CompositeDemo.FirstChild transitioned to ALARM at Friday 04 August, 2023 12:54:46 UTC", from(PreviousState::getReason)) + .returns("{\"triggeringAlarms\":[{\"arn\":\"arn:aws:cloudwatch:us-east-1:111122223333:alarm:CompositeDemo.FirstChild\",\"state\":{\"value\":\"ALARM\",\"timestamp\":\"2023-08-04T12:54:46.138+0000\"}}]}", from(PreviousState::getReasonData)) + .returns("2023-08-04T12:54:46.138+0000", from(PreviousState::getTimestamp)) + .returns("WaitPeriod", from(PreviousState::getActionsSuppressedBy)) + .returns("Actions suppressed by WaitPeriod", from(PreviousState::getActionsSuppressedReason)); + + Configuration configuration = alarmData.getConfiguration(); + assertThat(configuration).isNotNull(); + assertThat(configuration) + .returns("ALARM(CompositeDemo.FirstChild) OR ALARM(CompositeDemo.SecondChild)", from(Configuration::getAlarmRule)) + .returns("CompositeDemo.ActionsSuppressor", from(Configuration::getActionsSuppressor)) + .returns(120, from(Configuration::getActionsSuppressorWaitPeriod)) + .returns(180, from(Configuration::getActionsSuppressorExtensionPeriod)); + } + + @Test + public void testCloudWatchMetricAlarmEvent() { + CloudWatchMetricAlarmEvent event = EventLoader.loadCloudWatchMetricAlarmEvent("cloudwatch_metric_alarm.json"); + assertThat(event).isNotNull(); + assertThat(event) + .returns("aws.cloudwatch", from(CloudWatchMetricAlarmEvent::getSource)) + .returns("arn:aws:cloudwatch:us-east-1:444455556666:alarm:lambda-demo-metric-alarm", from(CloudWatchMetricAlarmEvent::getAlarmArn)) + .returns("444455556666", from(CloudWatchMetricAlarmEvent::getAccountId)) + .returns("2023-08-04T12:36:15.490+0000", from(CloudWatchMetricAlarmEvent::getTime)) + .returns("us-east-1", from(CloudWatchMetricAlarmEvent::getRegion)); + + CloudWatchMetricAlarmEvent.AlarmData alarmData = event.getAlarmData(); + assertThat(alarmData).isNotNull(); + assertThat(alarmData) + .returns("lambda-demo-metric-alarm", from(CloudWatchMetricAlarmEvent.AlarmData::getAlarmName)); + + CloudWatchMetricAlarmEvent.State state = alarmData.getState(); + assertThat(state).isNotNull(); + assertThat(state) + .returns("ALARM", from(CloudWatchMetricAlarmEvent.State::getValue)) + .returns("test", from(CloudWatchMetricAlarmEvent.State::getReason)) + .returns("2023-08-04T12:36:15.490+0000", from(CloudWatchMetricAlarmEvent.State::getTimestamp)); + + CloudWatchMetricAlarmEvent.PreviousState previousState = alarmData.getPreviousState(); + assertThat(previousState).isNotNull(); + assertThat(previousState) + .returns("INSUFFICIENT_DATA", from(CloudWatchMetricAlarmEvent.PreviousState::getValue)) + .returns("Insufficient Data: 5 datapoints were unknown.", from(CloudWatchMetricAlarmEvent.PreviousState::getReason)) + .returns("{\"version\":\"1.0\",\"queryDate\":\"2023-08-04T12:31:29.591+0000\",\"statistic\":\"Average\",\"period\":60,\"recentDatapoints\":[],\"threshold\":5.0,\"evaluatedDatapoints\":[{\"timestamp\":\"2023-08-04T12:30:00.000+0000\"},{\"timestamp\":\"2023-08-04T12:29:00.000+0000\"},{\"timestamp\":\"2023-08-04T12:28:00.000+0000\"},{\"timestamp\":\"2023-08-04T12:27:00.000+0000\"},{\"timestamp\":\"2023-08-04T12:26:00.000+0000\"}]}", from(CloudWatchMetricAlarmEvent.PreviousState::getReasonData)) + .returns("2023-08-04T12:31:29.595+0000", from(CloudWatchMetricAlarmEvent.PreviousState::getTimestamp)); + + CloudWatchMetricAlarmEvent.Configuration configuration = alarmData.getConfiguration(); + assertThat(configuration).isNotNull(); + assertThat(configuration) + .returns("Metric Alarm to test Lambda actions", from(CloudWatchMetricAlarmEvent.Configuration::getDescription)); + + List metrics = configuration.getMetrics(); + assertThat(metrics).hasSize(1); + CloudWatchMetricAlarmEvent.Metric metric = metrics.get(0); + assertThat(metric) + .returns("1234e046-06f0-a3da-9534-EXAMPLEe4c", from(CloudWatchMetricAlarmEvent.Metric::getId)); + + CloudWatchMetricAlarmEvent.MetricStat metricStat = metric.getMetricStat(); + assertThat(metricStat).isNotNull(); + assertThat(metricStat) + .returns(60, from(CloudWatchMetricAlarmEvent.MetricStat::getPeriod)) + .returns("Average", from(CloudWatchMetricAlarmEvent.MetricStat::getStat)) + .returns("Percent", from(CloudWatchMetricAlarmEvent.MetricStat::getUnit)); + + CloudWatchMetricAlarmEvent.MetricDetail metricDetail = metricStat.getMetric(); + assertThat(metricDetail).isNotNull(); + assertThat(metricDetail) + .returns("AWS/Logs", from(CloudWatchMetricAlarmEvent.MetricDetail::getNamespace)) + .returns("CallCount", from(CloudWatchMetricAlarmEvent.MetricDetail::getName)); + + Map dimensions = metricDetail.getDimensions(); + assertThat(dimensions).isNotEmpty().hasSize(1); + assertThat(dimensions) + .contains(entry("InstanceId", "i-12345678")); + } +} diff --git a/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/EventTest.java b/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/EventTest.java new file mode 100644 index 000000000..af94bcc36 --- /dev/null +++ b/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/EventTest.java @@ -0,0 +1,18 @@ +/* Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ +package com.amazonaws.services.lambda.runtime.tests; + +import com.amazonaws.services.lambda.runtime.events.SQSEvent; +import com.amazonaws.services.lambda.runtime.tests.annotations.Event; +import org.junit.jupiter.params.ParameterizedTest; + +import static org.assertj.core.api.Assertions.assertThat; + +public class EventTest { + + @ParameterizedTest + @Event(value = "sqs/sqs_event_nobody.json", type = SQSEvent.class) + public void testInjectEvent(SQSEvent event) { + assertThat(event).isNotNull(); + assertThat(event.getRecords()).hasSize(1); + } +} diff --git a/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/EventsTest.java b/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/EventsTest.java new file mode 100644 index 000000000..0861878e7 --- /dev/null +++ b/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/EventsTest.java @@ -0,0 +1,45 @@ +/* Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ +package com.amazonaws.services.lambda.runtime.tests; + +import com.amazonaws.services.lambda.runtime.events.SQSEvent; +import com.amazonaws.services.lambda.runtime.tests.annotations.Event; +import com.amazonaws.services.lambda.runtime.tests.annotations.Events; +import org.junit.jupiter.params.ParameterizedTest; + +import static org.assertj.core.api.Assertions.assertThat; + + +public class EventsTest { + + @ParameterizedTest + @Events( + events = { + @Event("sqs/sqs_event_nobody.json"), + @Event("sqs/sqs_event_product.json"), + }, + type = SQSEvent.class + ) + public void testInjectEvents(SQSEvent event) { + assertThat(event).isNotNull(); + assertThat(event.getRecords()).hasSize(1); + } + + @ParameterizedTest + @Events( + events = { + @Event(value = "sqs/sqs_event_nobody.json", type = SQSEvent.class), + @Event(value = "sqs/sqs_event_product.json", type = SQSEvent.class), + } + ) + public void testInjectEvents2(SQSEvent event) { + assertThat(event).isNotNull(); + assertThat(event.getRecords()).hasSize(1); + } + + @ParameterizedTest + @Events(folder = "sqs", type = SQSEvent.class) + public void testInjectEventsFromFolder(SQSEvent event) { + assertThat(event).isNotNull(); + assertThat(event.getRecords()).hasSize(1); + } +} diff --git a/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/HandlerParamsTest.java b/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/HandlerParamsTest.java new file mode 100644 index 000000000..a7c3c6e01 --- /dev/null +++ b/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/HandlerParamsTest.java @@ -0,0 +1,71 @@ +/* Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ +package com.amazonaws.services.lambda.runtime.tests; + +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; +import com.amazonaws.services.lambda.runtime.tests.annotations.*; +import org.junit.jupiter.params.ParameterizedTest; + +import static org.assertj.core.api.Assertions.assertThat; + +public class HandlerParamsTest { + + @ParameterizedTest + @HandlerParams( + event = @Event(value = "apigw/events/apigw_event.json", type = APIGatewayProxyRequestEvent.class), + response = @Response(value = "apigw/responses/apigw_response.json", type = APIGatewayProxyResponseEvent.class)) + public void testSimpleEventResponse(APIGatewayProxyRequestEvent event, APIGatewayProxyResponseEvent response) { + assertThat(event).isNotNull(); + assertThat(event.getBody()).contains("Lambda rocks"); + assertThat(event.getHeaders()).hasSize(18); + assertThat(event.getHttpMethod()).isEqualTo("POST"); + + assertThat(response).isNotNull(); + assertThat(response.getStatusCode()).isEqualTo(200); + assertThat(response.getBody()).contains("Lambda rocks"); + } + + @ParameterizedTest + @HandlerParams( + events = @Events(folder = "apigw/events/", type = APIGatewayProxyRequestEvent.class), + responses = @Responses(folder = "apigw/responses/", type = APIGatewayProxyResponseEvent.class)) + public void testMultipleEventsResponsesinFolder(APIGatewayProxyRequestEvent event, APIGatewayProxyResponseEvent response) { + assertThat(event).isNotNull(); + assertThat(event.getHeaders()).hasSize(18); + assertThat(event.getHttpMethod()).isEqualTo("POST"); + + assertThat(response).isNotNull(); + assertThat(response.getStatusCode()).isEqualTo(200); + + assertThat(response.getBody()).isEqualTo(event.getBody()); + } + + @ParameterizedTest + @HandlerParams( + events = @Events( + events = { + @Event("apigw/events/apigw_event.json"), + @Event("apigw/events/apigw_event_nobody.json"), + }, + type = APIGatewayProxyRequestEvent.class + ), + responses = @Responses( + responses = { + @Response("apigw/responses/apigw_response.json"), + @Response("apigw/responses/apigw_response2.json") + }, + type = APIGatewayProxyResponseEvent.class + ) + ) + public void testMultipleEventsResponses(APIGatewayProxyRequestEvent event, APIGatewayProxyResponseEvent response) { + assertThat(event).isNotNull(); + assertThat(event.getHeaders()).hasSize(18); + assertThat(event.getHttpMethod()).isEqualTo("POST"); + + assertThat(response).isNotNull(); + assertThat(response.getStatusCode()).isEqualTo(200); + + assertThat(response.getBody()).isEqualTo(event.getBody()); + } + +} diff --git a/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/JsonNodeUtilsTest.java b/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/JsonNodeUtilsTest.java new file mode 100644 index 000000000..ec5798dca --- /dev/null +++ b/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/JsonNodeUtilsTest.java @@ -0,0 +1,155 @@ +/* Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ +package com.amazonaws.services.lambda.runtime.tests; + +import com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.JsonNode; +import com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class JsonNodeUtilsTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + // --- stripNulls --- + + @Test + void stripNulls_removesTopLevelNulls() throws Exception { + JsonNode node = MAPPER.readTree("{\"a\":1,\"b\":null,\"c\":\"hello\"}"); + JsonNode result = JsonNodeUtils.stripNulls(node); + assertEquals(MAPPER.readTree("{\"a\":1,\"c\":\"hello\"}"), result); + } + + @Test + void stripNulls_removesNestedNulls() throws Exception { + JsonNode node = MAPPER.readTree("{\"outer\":{\"keep\":true,\"drop\":null}}"); + JsonNode result = JsonNodeUtils.stripNulls(node); + assertEquals(MAPPER.readTree("{\"outer\":{\"keep\":true}}"), result); + } + + @Test + void stripNulls_leavesArrayElementsIntact() throws Exception { + // Nulls inside arrays are kept (they're positional) + JsonNode node = MAPPER.readTree("{\"arr\":[1,null,3]}"); + JsonNode result = JsonNodeUtils.stripNulls(node); + assertEquals(MAPPER.readTree("{\"arr\":[1,null,3]}"), result); + } + + @Test + void stripNulls_removesNullsInsideArrayObjects() throws Exception { + JsonNode node = MAPPER.readTree("[{\"a\":1,\"b\":null},{\"c\":null}]"); + JsonNode result = JsonNodeUtils.stripNulls(node); + assertEquals(MAPPER.readTree("[{\"a\":1},{}]"), result); + } + + @Test + void stripNulls_noOpOnCleanTree() throws Exception { + JsonNode node = MAPPER.readTree("{\"a\":1,\"b\":\"two\"}"); + JsonNode result = JsonNodeUtils.stripNulls(node); + assertEquals(MAPPER.readTree("{\"a\":1,\"b\":\"two\"}"), result); + } + + // --- diffNodes --- + + @Test + void diffNodes_identicalTrees_noDiffs() throws Exception { + JsonNode a = MAPPER.readTree("{\"x\":1,\"y\":\"hello\"}"); + List diffs = new ArrayList<>(); + JsonNodeUtils.diffNodes("", a, a.deepCopy(), diffs); + assertTrue(diffs.isEmpty()); + } + + @Test + void diffNodes_changedValue() throws Exception { + JsonNode expected = MAPPER.readTree("{\"x\":1}"); + JsonNode actual = MAPPER.readTree("{\"x\":2}"); + List diffs = new ArrayList<>(); + JsonNodeUtils.diffNodes("", expected, actual, diffs); + assertEquals(1, diffs.size()); + assertTrue(diffs.get(0).startsWith("CHANGED .x")); + } + + @Test + void diffNodes_missingField() throws Exception { + JsonNode expected = MAPPER.readTree("{\"a\":1,\"b\":2}"); + JsonNode actual = MAPPER.readTree("{\"a\":1}"); + List diffs = new ArrayList<>(); + JsonNodeUtils.diffNodes("", expected, actual, diffs); + assertEquals(1, diffs.size()); + assertTrue(diffs.get(0).contains("MISSING") && diffs.get(0).contains(".b"), "got: " + diffs.get(0)); + } + + @Test + void diffNodes_addedField() throws Exception { + JsonNode expected = MAPPER.readTree("{\"a\":1}"); + JsonNode actual = MAPPER.readTree("{\"a\":1,\"b\":2}"); + List diffs = new ArrayList<>(); + JsonNodeUtils.diffNodes("", expected, actual, diffs); + assertEquals(1, diffs.size(), "diffs: " + diffs); + assertTrue(diffs.get(0).contains("ADDED") && diffs.get(0).contains(".b"), "got: " + diffs.get(0)); + } + + @Test + void diffNodes_nestedObjectDiff() throws Exception { + JsonNode expected = MAPPER.readTree("{\"outer\":{\"inner\":\"old\"}}"); + JsonNode actual = MAPPER.readTree("{\"outer\":{\"inner\":\"new\"}}"); + List diffs = new ArrayList<>(); + JsonNodeUtils.diffNodes("", expected, actual, diffs); + assertEquals(1, diffs.size()); + assertTrue(diffs.get(0).contains(".outer.inner")); + } + + @Test + void diffNodes_arrayElementDiff() throws Exception { + JsonNode expected = MAPPER.readTree("{\"arr\":[1,2,3]}"); + JsonNode actual = MAPPER.readTree("{\"arr\":[1,99,3]}"); + List diffs = new ArrayList<>(); + JsonNodeUtils.diffNodes("", expected, actual, diffs); + assertEquals(1, diffs.size()); + assertTrue(diffs.get(0).contains("[1]")); + } + + @Test + void diffNodes_arrayLengthMismatch() throws Exception { + JsonNode expected = MAPPER.readTree("{\"arr\":[1,2]}"); + JsonNode actual = MAPPER.readTree("{\"arr\":[1,2,3]}"); + List diffs = new ArrayList<>(); + JsonNodeUtils.diffNodes("", expected, actual, diffs); + assertEquals(1, diffs.size()); + assertTrue(diffs.get(0).contains("ADDED"), "got: " + diffs.get(0)); + } + + // --- areSameDateTime (tested indirectly via diffNodes) --- + + @Test + void diffNodes_equivalentDateTimes_noDiff() throws Exception { + // "+0000" vs "Z" — same instant, different format + JsonNode expected = MAPPER.readTree("{\"t\":\"2020-03-12T19:03:58.000+0000\"}"); + JsonNode actual = MAPPER.readTree("{\"t\":\"2020-03-12T19:03:58.000Z\"}"); + List diffs = new ArrayList<>(); + JsonNodeUtils.diffNodes("", expected, actual, diffs); + assertTrue(diffs.isEmpty(), "Expected no diffs for equivalent datetimes, got: " + diffs); + } + + @Test + void diffNodes_differentDateTimes_hasDiff() throws Exception { + JsonNode expected = MAPPER.readTree("{\"t\":\"2020-03-12T19:03:58.000Z\"}"); + JsonNode actual = MAPPER.readTree("{\"t\":\"2021-01-01T00:00:00.000Z\"}"); + List diffs = new ArrayList<>(); + JsonNodeUtils.diffNodes("", expected, actual, diffs); + assertEquals(1, diffs.size()); + } + + @Test + void diffNodes_nonDateStrings_notTreatedAsDates() throws Exception { + JsonNode expected = MAPPER.readTree("{\"s\":\"hello\"}"); + JsonNode actual = MAPPER.readTree("{\"s\":\"world\"}"); + List diffs = new ArrayList<>(); + JsonNodeUtils.diffNodes("", expected, actual, diffs); + assertEquals(1, diffs.size()); + assertTrue(diffs.get(0).startsWith("CHANGED .s")); + } +} diff --git a/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/LambdaEventAssertTest.java b/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/LambdaEventAssertTest.java new file mode 100644 index 000000000..63067f8b2 --- /dev/null +++ b/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/LambdaEventAssertTest.java @@ -0,0 +1,71 @@ +package com.amazonaws.services.lambda.runtime.tests; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class LambdaEventAssertTest { + /** + * Demonstrates the completeness check: the fixture has a field + * ({@code unknownField}) that {@code PartialPojo} does not capture, + * so it gets silently dropped during deserialization. + */ + @Test + void shouldFailWhenFieldIsDropped() { + AssertionError error = assertThrows(AssertionError.class, + () -> LambdaEventAssert.assertSerializationRoundTrip( + "partial_pojo.json", PartialPojo.class)); + + assertTrue(error.getMessage().contains("PartialPojo"), + "Error message should name the failing class"); + } + + /** + * Demonstrates the stability check: the getter mutates state on each + * call, so the first and second round-trips produce different JSON. + */ + @Test + void shouldFailWhenSerializationIsUnstable() { + AssertionError error = assertThrows(AssertionError.class, + () -> LambdaEventAssert.assertSerializationRoundTrip( + "unstable_pojo.json", UnstablePojo.class)); + + assertTrue(error.getMessage().contains("UnstablePojo"), + "Error message should name the failing class"); + } + + /** POJO that only captures {@code name}, silently dropping any other fields. */ + public static class PartialPojo { + private String name; + + public PartialPojo() { + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + /** + * POJO with a getter that appends a suffix, making serialization + * non-idempotent. + */ + public static class UnstablePojo { + private String name; + + public UnstablePojo() { + } + + public String getName() { + return name == null ? null : name + "_x"; + } + + public void setName(String name) { + this.name = name; + } + } +} diff --git a/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/ResponseEventSerializationRoundTripTest.java b/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/ResponseEventSerializationRoundTripTest.java new file mode 100644 index 000000000..e700b0d01 --- /dev/null +++ b/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/ResponseEventSerializationRoundTripTest.java @@ -0,0 +1,59 @@ +/* Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ +package com.amazonaws.services.lambda.runtime.tests; + +import com.amazonaws.services.lambda.runtime.events.*; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +/** + * Verifies serialization round-trip fidelity for Lambda response event types. + * + *

Response events are POJOs that Lambda functions return to the RIC, which + * serializes them to JSON via {@code EventHandlerLoader.getSerializer()}. + * None of these types are registered in {@code SUPPORTED_EVENTS}, so they + * go through the bare Jackson path ({@code JacksonFactory.getInstance() + * .getSerializer(type)}).

+ * + *

Although the RIC only calls {@code toJson()} on response types (never + * {@code fromJson()}), the round-trip test is a stricter check: if a response + * type survives JSON → POJO → JSON → POJO → JSON, it + * certainly survives the production POJO → JSON path.

+ * + * @see SerializationRoundTripTest for registered input events + * @see UnregisteredEventSerializationRoundTripTest for unregistered input events + */ +@SuppressWarnings("deprecation") // APIGatewayV2ProxyResponseEvent is deprecated +public class ResponseEventSerializationRoundTripTest { + + @ParameterizedTest(name = "{0}") + @MethodSource("passingCases") + void roundTrip(String displayName, String fixture, Class eventClass) { + LambdaEventAssert.assertSerializationRoundTrip(fixture, eventClass); + } + + private static Stream passingCases() { + return Stream.of( + // API Gateway responses + args(APIGatewayProxyResponseEvent.class, "response/apigw_proxy_response.json"), + args(APIGatewayV2HTTPResponse.class, "response/apigw_v2_http_response.json"), + args(APIGatewayV2WebSocketResponse.class, "response/apigw_v2_websocket_response.json"), + args(APIGatewayV2ProxyResponseEvent.class, "response/apigw_v2_websocket_response.json"), + // ALB response + args(ApplicationLoadBalancerResponseEvent.class, "response/alb_response.json"), + // S3 Batch response + args(S3BatchResponse.class, "response/s3_batch_response.json"), + // SQS Batch response + args(SQSBatchResponse.class, "response/sqs_batch_response.json"), + // Simple IAM Policy response (HTTP API) + args(SimpleIAMPolicyResponse.class, "response/simple_iam_policy_response.json"), + // MSK Firehose response + args(MSKFirehoseResponse.class, "response/msk_firehose_response.json")); + } + + private static Arguments args(Class clazz, String fixture) { + return Arguments.of(clazz.getSimpleName(), fixture, clazz); + } +} diff --git a/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/S3BatchEventV2Test.java b/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/S3BatchEventV2Test.java new file mode 100644 index 000000000..562af4355 --- /dev/null +++ b/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/S3BatchEventV2Test.java @@ -0,0 +1,21 @@ +package com.amazonaws.services.lambda.runtime.tests; + +import com.amazonaws.services.lambda.runtime.events.S3BatchEventV2; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.*; + +public class S3BatchEventV2Test { + + @Test + public void testS3BatchEventV2() { + S3BatchEventV2 event = EventLoader.loadS3BatchEventV2("s3_batch_event_v2.json"); + assertThat(event).isNotNull(); + assertThat(event.getInvocationId()).isEqualTo("Jr3s8KZqYWRmaiBhc2RmdW9hZHNmZGpmaGFzbGtkaGZzatx7Ruy"); + assertThat(event.getJob()).isNotNull(); + assertThat(event.getJob().getUserArguments().get("MyDestinationBucket")).isEqualTo("destination-directory-bucket-name"); + assertThat(event.getTasks()).hasSize(1); + assertThat(event.getTasks().get(0).getS3Key()).isEqualTo("s3objectkey"); + assertThat(event.getTasks().get(0).getS3Bucket()).isEqualTo("source-directory-bucket-name"); + } +} diff --git a/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/SerializationRoundTripTest.java b/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/SerializationRoundTripTest.java new file mode 100644 index 000000000..3eab8fec0 --- /dev/null +++ b/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/SerializationRoundTripTest.java @@ -0,0 +1,108 @@ +/* Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ +package com.amazonaws.services.lambda.runtime.tests; + +import com.amazonaws.services.lambda.runtime.events.*; +import com.amazonaws.services.lambda.runtime.events.models.s3.S3EventNotification; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Verifies serialization round-trip fidelity for events that are registered in + * {@code LambdaEventSerializers.SUPPORTED_EVENTS}. + * + *

Registered events go through the full customized serialization path in the + * Runtime Interface Client (RIC): {@code EventHandlerLoader.getSerializer()} + * detects them via {@code isLambdaSupportedEvent()} and delegates to + * {@code LambdaEventSerializers.serializerFor()}, which applies Jackson mixins, + * {@code DateModule}/{@code DateTimeModule}, and naming strategies.

+ * + *

Each case feeds a JSON fixture through + * {@link LambdaEventAssert#assertSerializationRoundTrip} which performs two + * consecutive round-trips and compares the original JSON tree against the + * final output.

+ * + * @see UnregisteredEventSerializationRoundTripTest for events not in SUPPORTED_EVENTS + */ +public class SerializationRoundTripTest { + + @ParameterizedTest(name = "{0}") + @MethodSource("passingCases") + void roundTrip(String displayName, String fixture, Class eventClass) { + LambdaEventAssert.assertSerializationRoundTrip(fixture, eventClass); + } + + @ParameterizedTest(name = "{0} (known failure)") + @MethodSource("knownFailureCases") + void roundTripKnownFailures(String displayName, String fixture, Class eventClass) { + assertThrows(Throwable.class, + () -> LambdaEventAssert.assertSerializationRoundTrip(fixture, eventClass), + displayName + " was expected to fail but passed — move it to passingCases()"); + } + + private static Stream passingCases() { + return Stream.of( + args(CloudFormationCustomResourceEvent.class, "cloudformation_event.json"), + args(CloudWatchLogsEvent.class, "cloudwatchlogs_event.json"), + args(CodeCommitEvent.class, "codecommit_event.json"), + args(ConfigEvent.class, "config_event.json"), + args(DynamodbEvent.class, "ddb/dynamo_event_roundtrip.json"), + args(KinesisEvent.class, "kinesis/kinesis_event_roundtrip.json"), + args(KinesisFirehoseEvent.class, "firehose_event.json"), + args(LambdaDestinationEvent.class, "lambda_destination_event.json"), + args(ScheduledEvent.class, "cloudwatch_event.json"), + args(SecretsManagerRotationEvent.class, "secrets_rotation_event.json"), + args(SNSEvent.class, "sns_event.json"), + args(LexEvent.class, "lex_event_roundtrip.json"), + args(ConnectEvent.class, "connect_event.json"), + args(SQSEvent.class, "sqs/sqs_event_nobody.json"), + args(APIGatewayProxyRequestEvent.class, "apigw_rest_event.json"), + args(CloudFrontEvent.class, "cloudfront_event.json"), + args(S3Event.class, "s3_event.json"), + args(S3EventNotification.class, "s3_event.json"), + args(APIGatewayV2HTTPEvent.class, "apigw_http_event.json"), + args(APIGatewayCustomAuthorizerEvent.class, "apigw_auth.json"), + args(ApplicationLoadBalancerRequestEvent.class, "elb_event.json"), + args(CloudWatchCompositeAlarmEvent.class, "cloudwatch_composite_alarm.json"), + args(CloudWatchMetricAlarmEvent.class, "cloudwatch_metric_alarm.json"), + args(CognitoUserPoolPreTokenGenerationEventV2.class, "cognito_user_pool_pre_token_generation_event_v2.json"), + args(KafkaEvent.class, "kafka_event_roundtrip.json"), + args(MSKFirehoseEvent.class, "msk_firehose_event_roundtrip.json"), + args(RabbitMQEvent.class, "rabbitmq_event_roundtrip.json"), + args(S3BatchEventV2.class, "s3_batch_event_v2.json"), + args(IoTButtonEvent.class, "iot_button_event.json"), + args(CognitoEvent.class, "cognito_sync_event.json"), + args(DynamodbTimeWindowEvent.class, "ddb/dynamo_time_window_event.json"), + args(KinesisTimeWindowEvent.class, "kinesis/kinesis_time_window_event.json")); + } + + private static Stream knownFailureCases() { + return Stream.of( + // APIGatewayV2CustomAuthorizerEvent has two serialization issues: + // 1. getTime() parses the raw string "12/Mar/2020:19:03:58 +0000" into a + // DateTime via dd/MMM/yyyy formatter. Jackson serializes as ISO-8601, but + // the formatter cannot parse ISO-8601 back on the second round-trip. + // The time field is effectively mandatory (getTime() throws NPE if null), + // and the date format change is inherent to how the serialization works. + // 2. getTimeEpoch() converts long to Instant; Jackson serializes as decimal + // seconds (e.g. 1583348638.390000000) instead of the original long. + // Both transformations are lossy; coercion captured in EventLoaderTest. + args(APIGatewayV2CustomAuthorizerEvent.class, "apigw_auth_v2.json"), + // ActiveMQEvent has one serialization issue: + // Destination.physicalName (camelCase) vs JSON "physicalname" (lowercase) — + // ACCEPT_CASE_INSENSITIVE_PROPERTIES is disabled in JacksonFactory so the + // field is silently dropped during deserialization. + // Fix: create an ActiveMQEventMixin with a DestinationMixin that maps + // @JsonProperty("physicalname") to getPhysicalName()/setPhysicalName(), + // then register it in LambdaEventSerializers MIXIN_MAP and NESTED_CLASS_MAP. + args(ActiveMQEvent.class, "mq_event.json")); + } + + private static Arguments args(Class clazz, String fixture) { + return Arguments.of(clazz.getSimpleName(), fixture, clazz); + } +} diff --git a/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/UnregisteredEventSerializationRoundTripTest.java b/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/UnregisteredEventSerializationRoundTripTest.java new file mode 100644 index 000000000..12ad818e4 --- /dev/null +++ b/aws-lambda-java-tests/src/test/java/com/amazonaws/services/lambda/runtime/tests/UnregisteredEventSerializationRoundTripTest.java @@ -0,0 +1,90 @@ +/* Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. */ +package com.amazonaws.services.lambda.runtime.tests; + +import com.amazonaws.services.lambda.runtime.events.*; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Verifies serialization round-trip fidelity for events that are NOT registered + * in {@code LambdaEventSerializers.SUPPORTED_EVENTS}. + * + *

In the Runtime Interface Client (RIC), when a handler's event type is not + * in {@code SUPPORTED_EVENTS}, {@code EventHandlerLoader.getSerializer()} falls + * through to {@code JacksonFactory.getInstance().getSerializer(type)} — a bare + * {@code PojoSerializer} backed by Jackson without any mixins or naming + * strategies. However, {@code LambdaEventSerializers.serializerFor()} (used by + * this test) unconditionally registers {@code DateModule} and + * {@code DateTimeModule}, so Joda/java.time types are still handled. For most + * unregistered events this makes no practical difference because they don't + * contain Joda DateTime fields.

+ * + * @see SerializationRoundTripTest for events registered in SUPPORTED_EVENTS + */ +@SuppressWarnings("deprecation") // APIGatewayV2ProxyRequestEvent is deprecated +public class UnregisteredEventSerializationRoundTripTest { + + @ParameterizedTest(name = "{0}") + @MethodSource("passingCases") + void roundTrip(String displayName, String fixture, Class eventClass) { + LambdaEventAssert.assertSerializationRoundTrip(fixture, eventClass); + } + + @ParameterizedTest(name = "{0} (known failure)") + @MethodSource("knownFailureCases") + void roundTripKnownFailures(String displayName, String fixture, Class eventClass) { + assertThrows(Throwable.class, + () -> LambdaEventAssert.assertSerializationRoundTrip(fixture, eventClass), + displayName + " was expected to fail but passed — move it to passingCases()"); + } + + private static Stream passingCases() { + return Stream.of( + // S3 Batch + args(S3BatchEvent.class, "s3_batch_event.json"), + // AppSync + args(AppSyncLambdaAuthorizerEvent.class, "appsync_authorizer_event.json"), + args(AppSyncLambdaAuthorizerResponse.class, "appsync_authorizer_response.json"), + // TimeWindow response + args(TimeWindowEventResponse.class, "time_window_event_response.json"), + // Cognito UserPool triggers + args(CognitoUserPoolPreSignUpEvent.class, "cognito/cognito_userpool_presignup.json"), + args(CognitoUserPoolPostConfirmationEvent.class, "cognito/cognito_userpool_postconfirmation.json"), + args(CognitoUserPoolPreAuthenticationEvent.class, "cognito/cognito_userpool_preauthentication.json"), + args(CognitoUserPoolPostAuthenticationEvent.class, "cognito/cognito_userpool_postauthentication.json"), + args(CognitoUserPoolDefineAuthChallengeEvent.class, "cognito/cognito_userpool_define_auth_challenge.json"), + args(CognitoUserPoolCreateAuthChallengeEvent.class, "cognito/cognito_userpool_create_auth_challenge.json"), + args(CognitoUserPoolVerifyAuthChallengeResponseEvent.class, "cognito/cognito_userpool_verify_auth_challenge.json"), + args(CognitoUserPoolMigrateUserEvent.class, "cognito/cognito_userpool_migrate_user.json"), + args(CognitoUserPoolCustomMessageEvent.class, "cognito/cognito_userpool_custom_message.json"), + args(CognitoUserPoolPreTokenGenerationEvent.class, "cognito/cognito_userpool_pre_token_generation.json"), + // Kinesis Analytics + args(KinesisAnalyticsFirehoseInputPreprocessingEvent.class, "kinesis/kinesis_analytics_firehose_input_preprocessing.json"), + args(KinesisAnalyticsStreamsInputPreprocessingEvent.class, "kinesis/kinesis_analytics_streams_input_preprocessing.json"), + args(KinesisAnalyticsInputPreprocessingResponse.class, "kinesis/kinesis_analytics_input_preprocessing_response.json"), + args(KinesisAnalyticsOutputDeliveryEvent.class, "kinesis/kinesis_analytics_output_delivery.json"), + args(KinesisAnalyticsOutputDeliveryResponse.class, "kinesis/kinesis_analytics_output_delivery_response.json"), + // API Gateway V2 WebSocket + args(APIGatewayV2WebSocketEvent.class, "apigw_websocket_event.json"), + args(APIGatewayV2ProxyRequestEvent.class, "apigw_websocket_event.json")); + } + + private static Stream knownFailureCases() { + return Stream.of( + // S3ObjectLambdaEvent: Lombok generates getXAmzRequestId() for field + // "xAmzRequestId". With USE_STD_BEAN_NAMING, Jackson derives the property + // name as "XAmzRequestId" (capital X), so the original "xAmzRequestId" key + // is silently dropped during deserialization. + // Fix: add @JsonProperty("xAmzRequestId") on the field or getter. + args(S3ObjectLambdaEvent.class, "s3_object_lambda_event.json")); + } + + private static Arguments args(Class clazz, String fixture) { + return Arguments.of(clazz.getSimpleName(), fixture, clazz); + } +} diff --git a/aws-lambda-java-tests/src/test/resources/apigw/events/apigw_event.json b/aws-lambda-java-tests/src/test/resources/apigw/events/apigw_event.json new file mode 100644 index 000000000..5a575592b --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/apigw/events/apigw_event.json @@ -0,0 +1,62 @@ +{ + "body": "{\"message\": \"Lambda rocks\"}", + "resource": "/{proxy+}", + "path": "/path/to/resource", + "httpMethod": "POST", + "isBase64Encoded": false, + "queryStringParameters": { + "foo": "bar" + }, + "pathParameters": { + "proxy": "/path/to/resource" + }, + "stageVariables": { + "baz": "qux" + }, + "headers": { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate, sdch", + "Accept-Language": "en-US,en;q=0.8", + "Cache-Control": "max-age=0", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Custom User Agent String", + "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", + "X-Forwarded-For": "127.0.0.1, 127.0.0.2", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "requestContext": { + "accountId": "123456789012", + "resourceId": "123456", + "stage": "prod", + "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", + "requestTime": "09/Apr/2015:12:34:56 +0000", + "requestTimeEpoch": 1428582896000, + "identity": { + "cognitoIdentityPoolId": null, + "accountId": null, + "cognitoIdentityId": null, + "caller": null, + "accessKey": null, + "sourceIp": "127.0.0.1", + "cognitoAuthenticationType": null, + "cognitoAuthenticationProvider": null, + "userArn": null, + "userAgent": "Custom User Agent String", + "user": null + }, + "path": "/prod/path/to/resource", + "resourcePath": "/{proxy+}", + "httpMethod": "POST", + "apiId": "1234567890", + "protocol": "HTTP/1.1" + } +} diff --git a/aws-lambda-java-tests/src/test/resources/apigw/events/apigw_event_nobody.json b/aws-lambda-java-tests/src/test/resources/apigw/events/apigw_event_nobody.json new file mode 100644 index 000000000..e56774416 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/apigw/events/apigw_event_nobody.json @@ -0,0 +1,62 @@ +{ + "body": "", + "resource": "/{proxy+}", + "path": "/path/to/resource", + "httpMethod": "POST", + "isBase64Encoded": false, + "queryStringParameters": { + "foo": "bar" + }, + "pathParameters": { + "proxy": "/path/to/resource" + }, + "stageVariables": { + "baz": "qux" + }, + "headers": { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Encoding": "gzip, deflate, sdch", + "Accept-Language": "en-US,en;q=0.8", + "Cache-Control": "max-age=0", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Desktop-Viewer": "true", + "CloudFront-Is-Mobile-Viewer": "false", + "CloudFront-Is-SmartTV-Viewer": "false", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Viewer-Country": "US", + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Custom User Agent String", + "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", + "X-Forwarded-For": "127.0.0.1, 127.0.0.2", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "requestContext": { + "accountId": "123456789012", + "resourceId": "123456", + "stage": "prod", + "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", + "requestTime": "09/Apr/2015:12:34:56 +0000", + "requestTimeEpoch": 1428582896000, + "identity": { + "cognitoIdentityPoolId": null, + "accountId": null, + "cognitoIdentityId": null, + "caller": null, + "accessKey": null, + "sourceIp": "127.0.0.1", + "cognitoAuthenticationType": null, + "cognitoAuthenticationProvider": null, + "userArn": null, + "userAgent": "Custom User Agent String", + "user": null + }, + "path": "/prod/path/to/resource", + "resourcePath": "/{proxy+}", + "httpMethod": "POST", + "apiId": "1234567890", + "protocol": "HTTP/1.1" + } +} diff --git a/aws-lambda-java-tests/src/test/resources/apigw/responses/apigw_response.json b/aws-lambda-java-tests/src/test/resources/apigw/responses/apigw_response.json new file mode 100644 index 000000000..691ca3a9c --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/apigw/responses/apigw_response.json @@ -0,0 +1,16 @@ +{ + "body": "{\"message\": \"Lambda rocks\"}", + "statusCode": 200, + "headers": { + "Cache-Control": "max-age=0", + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Custom User Agent String", + "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", + "X-Forwarded-For": "127.0.0.1, 127.0.0.2", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "isBase64Encoded": false +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/apigw/responses/apigw_response2.json b/aws-lambda-java-tests/src/test/resources/apigw/responses/apigw_response2.json new file mode 100644 index 000000000..365d9ca58 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/apigw/responses/apigw_response2.json @@ -0,0 +1,16 @@ +{ + "body": "", + "statusCode": 200, + "headers": { + "Cache-Control": "max-age=0", + "Host": "1234567890.execute-api.us-east-1.amazonaws.com", + "Upgrade-Insecure-Requests": "1", + "User-Agent": "Custom User Agent String", + "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", + "X-Forwarded-For": "127.0.0.1, 127.0.0.2", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "isBase64Encoded": false +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/apigw_auth.json b/aws-lambda-java-tests/src/test/resources/apigw_auth.json new file mode 100644 index 000000000..eb73956ee --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/apigw_auth.json @@ -0,0 +1,41 @@ +{ + "version": "1.0", + "type": "REQUEST", + "methodArn": "arn:aws:execute-api:us-east-1:123456789012:abcdef123/test/GET/request", + "identitySource": "user1,123", + "authorizationToken": "user1,123", + "resource": "/request", + "path": "/request", + "httpMethod": "GET", + "headers": { + "X-AMZ-Date": "20170718T062915Z", + "Accept": "*/*", + "HeaderAuth1": "headerValue1", + "CloudFront-Viewer-Country": "US", + "CloudFront-Forwarded-Proto": "https", + "CloudFront-Is-Tablet-Viewer": "false", + "CloudFront-Is-Mobile-Viewer": "false", + "User-Agent": "..." + }, + "queryStringParameters": { + "QueryString1": "queryValue1" + }, + "pathParameters": {}, + "stageVariables": { + "StageVar1": "stageValue1" + }, + "requestContext": { + "path": "/request", + "accountId": "123456789012", + "resourceId": "05c7jb", + "stage": "test", + "requestId": "...", + "identity": { + "apiKey": "...", + "sourceIp": "..." + }, + "resourcePath": "/request", + "httpMethod": "GET", + "apiId": "abcdef123" + } +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/apigw_auth_v2.json b/aws-lambda-java-tests/src/test/resources/apigw_auth_v2.json new file mode 100644 index 000000000..a603763ec --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/apigw_auth_v2.json @@ -0,0 +1,35 @@ +{ + "version": "2.0", + "type": "REQUEST", + "routeArn": "arn:aws:execute-api:us-east-1:123456789012:abcdef123/test/GET/request", + "identitySource": ["user1", "123"], + "routeKey": "$default", + "rawPath": "/my/path", + "rawQueryString": "parameter1=value1¶meter1=value2¶meter2=value", + "cookies": [ "cookie1", "cookie2" ], + "headers": { + "Header1": "value1", + "Header2": "value2" + }, + "queryStringParameters": { "parameter1": "value1,value2", "parameter2": "value" }, + "requestContext": { + "accountId": "123456789012", + "apiId": "api-id", + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "http": { + "method": "POST", + "path": "/my/path", + "protocol": "HTTP/1.1", + "sourceIp": "IP", + "userAgent": "agent" + }, + "requestId": "id", + "routeKey": "$default", + "stage": "$default", + "time": "12/Mar/2020:19:03:58 +0000", + "timeEpoch": 1583348638390 + }, + "pathParameters": {"parameter1": "value1"}, + "stageVariables": {"stageVariable1": "value1", "stageVariable2": "value2"} +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/apigw_http_event.json b/aws-lambda-java-tests/src/test/resources/apigw_http_event.json new file mode 100644 index 000000000..91954656c --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/apigw_http_event.json @@ -0,0 +1,57 @@ +{ + "version": "2.0", + "routeKey": "$default", + "rawPath": "/my/path", + "rawQueryString": "parameter1=value1¶meter1=value2¶meter2=value", + "cookies": [ + "cookie1", + "cookie2" + ], + "headers": { + "Header1": "value1", + "Header2": "value1,value2" + }, + "queryStringParameters": { + "parameter1": "value1,value2", + "parameter2": "value" + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "api-id", + "authorizer": { + "jwt": { + "claims": { + "claim1": "value1", + "claim2": "value2" + }, + "scopes": [ + "scope1", + "scope2" + ] + } + }, + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "http": { + "method": "POST", + "path": "/my/path", + "protocol": "HTTP/1.1", + "sourceIp": "IP", + "userAgent": "agent" + }, + "requestId": "id", + "routeKey": "$default", + "stage": "$default", + "time": "12/Mar/2020:19:03:58 +0000", + "timeEpoch": 1583348638390 + }, + "body": "Hello from Lambda!!", + "pathParameters": { + "parameter1": "value1" + }, + "isBase64Encoded": false, + "stageVariables": { + "stageVariable1": "value1", + "stageVariable2": "value2" + } +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/apigw_rest_event.json b/aws-lambda-java-tests/src/test/resources/apigw_rest_event.json new file mode 100644 index 000000000..a139ccbe8 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/apigw_rest_event.json @@ -0,0 +1,73 @@ +{ + "version": "1.0", + "resource": "/my/path", + "path": "/my/path", + "httpMethod": "GET", + "headers": { + "Header1": "value1", + "Header2": "value2" + }, + "multiValueHeaders": { + "Header1": [ + "value1" + ], + "Header2": [ + "value1", + "value2" + ], + "Header3": [ + "value1,value2" + ] + }, + "queryStringParameters": { + "parameter1": "value1", + "parameter2": "value2" + }, + "multiValueQueryStringParameters": { + "parameter1": [ + "value1" + ], + "parameter2": [ + "value1", + "value2" + ] + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "id", + "authorizer": { + "claims": null, + "scopes": null + }, + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "extendedRequestId": "request-id", + "httpMethod": "GET", + "identity": { + "accessKey": null, + "accountId": null, + "caller": null, + "cognitoAuthenticationProvider": null, + "cognitoAuthenticationType": null, + "cognitoIdentityId": null, + "cognitoIdentityPoolId": null, + "principalOrgId": null, + "sourceIp": "IP", + "user": null, + "userAgent": "user-agent", + "userArn": null + }, + "path": "/my/path", + "protocol": "HTTP/1.1", + "requestId": "id=", + "requestTime": "04/Mar/2020:19:15:17 +0000", + "requestTimeEpoch": 1583349317135, + "resourceId": null, + "resourcePath": "/my/path", + "stage": "$default" + }, + "pathParameters": null, + "stageVariables": null, + "body": "Hello from Lambda!", + "isBase64Encoded": false +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/apigw_websocket_event.json b/aws-lambda-java-tests/src/test/resources/apigw_websocket_event.json new file mode 100644 index 000000000..a47fc3a94 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/apigw_websocket_event.json @@ -0,0 +1,88 @@ +{ + "resource": "/", + "path": "/", + "httpMethod": "GET", + "headers": { + "Host": "abcdef1234.execute-api.us-east-1.amazonaws.com", + "Sec-WebSocket-Extensions": "permessage-deflate", + "Sec-WebSocket-Key": "dGhlIHNhbXBsZSBub25jZQ==", + "Sec-WebSocket-Version": "13", + "X-Forwarded-For": "192.0.2.1", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "multiValueHeaders": { + "Host": [ + "abcdef1234.execute-api.us-east-1.amazonaws.com" + ], + "Sec-WebSocket-Extensions": [ + "permessage-deflate" + ], + "Sec-WebSocket-Key": [ + "dGhlIHNhbXBsZSBub25jZQ==" + ], + "Sec-WebSocket-Version": [ + "13" + ], + "X-Forwarded-For": [ + "192.0.2.1" + ], + "X-Forwarded-Port": [ + "443" + ], + "X-Forwarded-Proto": [ + "https" + ] + }, + "queryStringParameters": { + "param1": "value1" + }, + "multiValueQueryStringParameters": { + "param1": [ + "value1" + ] + }, + "pathParameters": { + "proxy": "path/to/resource" + }, + "stageVariables": { + "stageVar1": "value1" + }, + "requestContext": { + "accountId": "123456789012", + "resourceId": "abcdef", + "stage": "prod", + "requestId": "abc-def-ghi", + "identity": { + "cognitoIdentityPoolId": "us-east-1:id-pool", + "accountId": "123456789012", + "cognitoIdentityId": "us-east-1:identity-id", + "caller": "caller-id", + "apiKey": "api-key-id", + "sourceIp": "192.0.2.1", + "cognitoAuthenticationType": "authenticated", + "cognitoAuthenticationProvider": "provider", + "userArn": "arn:aws:iam::123456789012:user/testuser", + "userAgent": "Mozilla/5.0", + "user": "testuser", + "accessKey": "AKIAIOSFODNN7EXAMPLE" + }, + "resourcePath": "/", + "httpMethod": "GET", + "apiId": "abcdef1234", + "connectedAt": 1583348638390, + "connectionId": "abc123=", + "domainName": "abcdef1234.execute-api.us-east-1.amazonaws.com", + "eventType": "CONNECT", + "extendedRequestId": "abc123=", + "integrationLatency": "100", + "messageDirection": "IN", + "messageId": "msg-001", + "requestTime": "09/Apr/2020:18:03:58 +0000", + "requestTimeEpoch": 1583348638390, + "routeKey": "$connect", + "status": "200" + }, + "body": "request body", + "isBase64Encoded": false +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/appsync_authorizer_event.json b/aws-lambda-java-tests/src/test/resources/appsync_authorizer_event.json new file mode 100644 index 000000000..494a500f3 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/appsync_authorizer_event.json @@ -0,0 +1,15 @@ +{ + "authorizationToken": "BE9DC5E3-D410-4733-AF76-70178092E681", + "requestContext": { + "apiId": "giy7kumfmvcqvbedntjwjvagii", + "accountId": "254688921111", + "requestId": "b80ed838-14c6-4500-b4c3-b694c7bef086", + "queryDocument": "mutation MyNewTask($desc: String!) {\n createTask(description: $desc) {\n id\n }\n}\n", + "operationName": "MyNewTask", + "variables": {} + }, + "requestHeaders": { + "host": "giy7kumfmvcqvbedntjwjvagii.appsync-api.us-east-1.amazonaws.com", + "content-type": "application/json" + } +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/appsync_authorizer_response.json b/aws-lambda-java-tests/src/test/resources/appsync_authorizer_response.json new file mode 100644 index 000000000..1216ad51d --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/appsync_authorizer_response.json @@ -0,0 +1,11 @@ +{ + "isAuthorized": true, + "resolverContext": { + "name": "Foo Man", + "balance": "100" + }, + "deniedFields": [ + "Mutation.createEvent" + ], + "ttlOverride": 15 +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/cloudformation_event.json b/aws-lambda-java-tests/src/test/resources/cloudformation_event.json new file mode 100644 index 000000000..92b95b713 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/cloudformation_event.json @@ -0,0 +1,24 @@ +{ + "RequestType": "Update", + "ResponseURL": "http://pre-signed-S3-url-for-response", + "StackId": "arn:aws:cloudformation:eu-central-1:123456789012:stack/MyStack/guid", + "RequestId": "unique id for this create request", + "ResourceType": "Custom::TestResource", + "LogicalResourceId": "MyTestResource", + "PhysicalResourceId": "MyTestResourceId", + "ServiceToken": "abcd", + "ResourceProperties": { + "StackName": "MyStack", + "List": [ + "1", + "2", + "3" + ] + }, + "OldResourceProperties": { + "StackName": "MyStack", + "List": [ + "1" + ] + } +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/cloudfront_event.json b/aws-lambda-java-tests/src/test/resources/cloudfront_event.json new file mode 100644 index 000000000..bf4625d06 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/cloudfront_event.json @@ -0,0 +1,36 @@ +{ + "Records": [ + { + "cf": { + "config": { + "distributionId": "EXAMPLE" + }, + "request": { + "uri": "/test", + "method": "GET", + "clientIp": "2001:cdba::3257:9652", + "headers": { + "host": [ + { + "key": "Host", + "value": "d123.cf.net" + } + ], + "user-agent": [ + { + "key": "User-Agent", + "value": "Test Agent" + } + ], + "user-name": [ + { + "key": "User-Name", + "value": "aws-cloudfront" + } + ] + } + } + } + } + ] +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/cloudwatch_composite_alarm.json b/aws-lambda-java-tests/src/test/resources/cloudwatch_composite_alarm.json new file mode 100644 index 000000000..353d470ae --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/cloudwatch_composite_alarm.json @@ -0,0 +1,30 @@ +{ + "source": "aws.cloudwatch", + "alarmArn": "arn:aws:cloudwatch:us-east-1:111122223333:alarm:SuppressionDemo.Main", + "accountId": "111122223333", + "time": "2023-08-04T12:56:46.138+0000", + "region": "us-east-1", + "alarmData": { + "alarmName": "CompositeDemo.Main", + "state": { + "value": "ALARM", + "reason": "arn:aws:cloudwatch:us-east-1:111122223333:alarm:CompositeDemo.FirstChild transitioned to ALARM at Friday 04 August, 2023 12:54:46 UTC", + "reasonData": "{\"triggeringAlarms\":[{\"arn\":\"arn:aws:cloudwatch:us-east-1:111122223333:alarm:CompositeDemo.FirstChild\",\"state\":{\"value\":\"ALARM\",\"timestamp\":\"2023-08-04T12:54:46.138+0000\"}}]}", + "timestamp": "2023-08-04T12:56:46.138+0000" + }, + "previousState": { + "value": "ALARM", + "reason": "arn:aws:cloudwatch:us-east-1:111122223333:alarm:CompositeDemo.FirstChild transitioned to ALARM at Friday 04 August, 2023 12:54:46 UTC", + "reasonData": "{\"triggeringAlarms\":[{\"arn\":\"arn:aws:cloudwatch:us-east-1:111122223333:alarm:CompositeDemo.FirstChild\",\"state\":{\"value\":\"ALARM\",\"timestamp\":\"2023-08-04T12:54:46.138+0000\"}}]}", + "timestamp": "2023-08-04T12:54:46.138+0000", + "actionsSuppressedBy": "WaitPeriod", + "actionsSuppressedReason": "Actions suppressed by WaitPeriod" + }, + "configuration": { + "alarmRule": "ALARM(CompositeDemo.FirstChild) OR ALARM(CompositeDemo.SecondChild)", + "actionsSuppressor": "CompositeDemo.ActionsSuppressor", + "actionsSuppressorWaitPeriod": 120, + "actionsSuppressorExtensionPeriod": 180 + } + } +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/cloudwatch_event.json b/aws-lambda-java-tests/src/test/resources/cloudwatch_event.json new file mode 100644 index 000000000..0573f3fe9 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/cloudwatch_event.json @@ -0,0 +1,15 @@ +{ + "version": "0", + "id": "fae0433b-7a0e-e383-7849-7e10153eaa47", + "detail-type": "Scheduled Event", + "source": "aws.events", + "account": "123456789012", + "time": "2020-09-30T15:58:34Z", + "region": "eu-central-1", + "resources": [ + "arn:aws:events:eu-central-1:123456789012:rule/demoschedule" + ], + "detail": { + + } +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/cloudwatch_metric_alarm.json b/aws-lambda-java-tests/src/test/resources/cloudwatch_metric_alarm.json new file mode 100644 index 000000000..61b4187b5 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/cloudwatch_metric_alarm.json @@ -0,0 +1,42 @@ +{ + "source": "aws.cloudwatch", + "alarmArn": "arn:aws:cloudwatch:us-east-1:444455556666:alarm:lambda-demo-metric-alarm", + "accountId": "444455556666", + "time": "2023-08-04T12:36:15.490+0000", + "region": "us-east-1", + "alarmData": { + "alarmName": "lambda-demo-metric-alarm", + "state": { + "value": "ALARM", + "reason": "test", + "timestamp": "2023-08-04T12:36:15.490+0000" + }, + "previousState": { + "value": "INSUFFICIENT_DATA", + "reason": "Insufficient Data: 5 datapoints were unknown.", + "reasonData": "{\"version\":\"1.0\",\"queryDate\":\"2023-08-04T12:31:29.591+0000\",\"statistic\":\"Average\",\"period\":60,\"recentDatapoints\":[],\"threshold\":5.0,\"evaluatedDatapoints\":[{\"timestamp\":\"2023-08-04T12:30:00.000+0000\"},{\"timestamp\":\"2023-08-04T12:29:00.000+0000\"},{\"timestamp\":\"2023-08-04T12:28:00.000+0000\"},{\"timestamp\":\"2023-08-04T12:27:00.000+0000\"},{\"timestamp\":\"2023-08-04T12:26:00.000+0000\"}]}", + "timestamp": "2023-08-04T12:31:29.595+0000" + }, + "configuration": { + "description": "Metric Alarm to test Lambda actions", + "metrics": [ + { + "id": "1234e046-06f0-a3da-9534-EXAMPLEe4c", + "metricStat": { + "metric": { + "namespace": "AWS/Logs", + "name": "CallCount", + "dimensions": { + "InstanceId": "i-12345678" + } + }, + "period": 60, + "stat": "Average", + "unit": "Percent" + }, + "returnData": true + } + ] + } + } +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/cloudwatchlogs_event.json b/aws-lambda-java-tests/src/test/resources/cloudwatchlogs_event.json new file mode 100644 index 000000000..2b455b9bc --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/cloudwatchlogs_event.json @@ -0,0 +1,5 @@ +{ + "awslogs": { + "data": "H4sIAAAAAAAAAHWPwQqCQBCGX0Xm7EFtK+smZBEUgXoLCdMhFtKV3akI8d0bLYmibvPPN3wz00CJxmQnTO41whwWQRIctmEcB6sQbFC3CjW3XW8kxpOpP+OC22d1Wml1qZkQGtoMsScxaczKN3plG8zlaHIta5KqWsozoTYw3/djzwhpLwivWFGHGpAFe7DL68JlBUk+l7KSN7tCOEJ4M3/qOI49vMHj+zCKdlFqLaU2ZHV2a4Ct/an0/ivdX8oYc1UVX860fQDQiMdxRQEAAA==" + } +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/codecommit_event.json b/aws-lambda-java-tests/src/test/resources/codecommit_event.json new file mode 100644 index 000000000..227cac73b --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/codecommit_event.json @@ -0,0 +1,27 @@ +{ + "Records": [ + { + "awsRegion": "eu-central-1", + "codecommit": { + "references": [ + { + "commit": "5c4ef1049f1d27deadbeeff313e0730018be182b", + "ref": "refs/heads/master" + } + ] + }, + "customData": "this is custom data", + "eventId": "5a824061-17ca-46a9-bbf9-114edeadbeef", + "eventName": "TriggerEventTest", + "eventPartNumber": 1, + "eventSource": "aws:codecommit", + "eventSourceARN": "arn:aws:codecommit:eu-central-1:123456789012:my-repo", + "eventTime": "2016-01-01T23:59:59.000+0000", + "eventTotalParts": 1, + "eventTriggerConfigId": "5a824061-17ca-46a9-bbf9-114edeadbeef", + "eventTriggerName": "my-trigger", + "eventVersion": "1.0", + "userIdentityARN": "arn:aws:iam::123456789012:root" + } + ] +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_create_auth_challenge.json b/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_create_auth_challenge.json new file mode 100644 index 000000000..495b41475 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_create_auth_challenge.json @@ -0,0 +1,37 @@ +{ + "version": "1", + "triggerSource": "CreateAuthChallenge_Authentication", + "region": "us-east-1", + "userPoolId": "us-east-1_uPoolId", + "userName": "testuser", + "callerContext": { + "awsSdkVersion": "2.0.0", + "clientId": "abcdefg1234567" + }, + "request": { + "userAttributes": { + "email": "user@example.com" + }, + "clientMetadata": { + "meta1": "value1" + }, + "challengeName": "CUSTOM_CHALLENGE", + "session": [ + { + "challengeName": "PASSWORD_VERIFIER", + "challengeResult": true, + "challengeMetadata": "metadata1" + } + ], + "userNotFound": false + }, + "response": { + "publicChallengeParameters": { + "captchaUrl": "url/123.jpg" + }, + "privateChallengeParameters": { + "answer": "5" + }, + "challengeMetadata": "CAPTCHA_CHALLENGE" + } +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_custom_message.json b/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_custom_message.json new file mode 100644 index 000000000..aa7a53a83 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_custom_message.json @@ -0,0 +1,28 @@ +{ + "version": "1", + "triggerSource": "CustomMessage_SignUp", + "region": "us-east-1", + "userPoolId": "us-east-1_uPoolId", + "userName": "testuser", + "callerContext": { + "awsSdkVersion": "2.0.0", + "clientId": "abcdefg1234567" + }, + "request": { + "userAttributes": { + "email": "user@example.com", + "phone_number_verified": "true", + "email_verified": "true" + }, + "clientMetadata": { + "meta1": "value1" + }, + "codeParameter": "####", + "usernameParameter": "testuser" + }, + "response": { + "smsMessage": "Your code is ####", + "emailMessage": "Your code is ####", + "emailSubject": "Welcome" + } +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_define_auth_challenge.json b/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_define_auth_challenge.json new file mode 100644 index 000000000..e320b71d1 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_define_auth_challenge.json @@ -0,0 +1,32 @@ +{ + "version": "1", + "triggerSource": "DefineAuthChallenge_Authentication", + "region": "us-east-1", + "userPoolId": "us-east-1_uPoolId", + "userName": "testuser", + "callerContext": { + "awsSdkVersion": "2.0.0", + "clientId": "abcdefg1234567" + }, + "request": { + "userAttributes": { + "email": "user@example.com" + }, + "clientMetadata": { + "meta1": "value1" + }, + "session": [ + { + "challengeName": "PASSWORD_VERIFIER", + "challengeResult": true, + "challengeMetadata": "metadata1" + } + ], + "userNotFound": false + }, + "response": { + "challengeName": "CUSTOM_CHALLENGE", + "issueTokens": false, + "failAuthentication": false + } +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_migrate_user.json b/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_migrate_user.json new file mode 100644 index 000000000..2897ae063 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_migrate_user.json @@ -0,0 +1,35 @@ +{ + "version": "1", + "triggerSource": "UserMigration_Authentication", + "region": "us-east-1", + "userPoolId": "us-east-1_uPoolId", + "userName": "testuser", + "callerContext": { + "awsSdkVersion": "2.0.0", + "clientId": "abcdefg1234567" + }, + "request": { + "userAttributes": { + "email": "user@example.com" + }, + "validationData": { + "key1": "val1" + }, + "clientMetadata": { + "meta1": "value1" + }, + "userName": "testuser", + "password": "test-password" + }, + "response": { + "userAttributes": { + "email": "user@example.com" + }, + "finalUserStatus": "CONFIRMED", + "messageAction": "SUPPRESS", + "desiredDeliveryMediums": [ + "EMAIL" + ], + "forceAliasCreation": false + } +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_postauthentication.json b/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_postauthentication.json new file mode 100644 index 000000000..f41084d54 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_postauthentication.json @@ -0,0 +1,20 @@ +{ + "version": "1", + "triggerSource": "PostAuthentication_Authentication", + "region": "us-east-1", + "userPoolId": "us-east-1_uPoolId", + "userName": "testuser", + "callerContext": { + "awsSdkVersion": "2.0.0", + "clientId": "abcdefg1234567" + }, + "request": { + "userAttributes": { + "email": "user@example.com" + }, + "clientMetadata": { + "meta1": "value1" + }, + "newDeviceUsed": false + } +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_postconfirmation.json b/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_postconfirmation.json new file mode 100644 index 000000000..ecf63c7d3 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_postconfirmation.json @@ -0,0 +1,20 @@ +{ + "version": "1", + "triggerSource": "PostConfirmation_ConfirmSignUp", + "region": "us-east-1", + "userPoolId": "us-east-1_uPoolId", + "userName": "testuser", + "callerContext": { + "awsSdkVersion": "2.0.0", + "clientId": "abcdefg1234567" + }, + "request": { + "userAttributes": { + "email": "user@example.com", + "email_verified": "true" + }, + "clientMetadata": { + "meta1": "value1" + } + } +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_pre_token_generation.json b/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_pre_token_generation.json new file mode 100644 index 000000000..f81ffb902 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_pre_token_generation.json @@ -0,0 +1,48 @@ +{ + "version": "1", + "triggerSource": "TokenGeneration_HostedAuth", + "region": "us-east-1", + "userPoolId": "us-east-1_uPoolId", + "userName": "testuser", + "callerContext": { + "awsSdkVersion": "2.0.0", + "clientId": "abcdefg1234567" + }, + "request": { + "userAttributes": { + "email": "user@example.com" + }, + "clientMetadata": { + "meta1": "value1" + }, + "groupConfiguration": { + "groupsToOverride": [ + "group1", + "group2" + ], + "iamRolesToOverride": [ + "arn:aws:iam::123456789012:role/role1" + ], + "preferredRole": "arn:aws:iam::123456789012:role/role1" + } + }, + "response": { + "claimsOverrideDetails": { + "claimsToAddOrOverride": { + "custom:myattr": "myvalue" + }, + "claimsToSuppress": [ + "email" + ], + "groupOverrideDetails": { + "groupsToOverride": [ + "group1" + ], + "iamRolesToOverride": [ + "arn:aws:iam::123456789012:role/role1" + ], + "preferredRole": "arn:aws:iam::123456789012:role/role1" + } + } + } +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_preauthentication.json b/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_preauthentication.json new file mode 100644 index 000000000..1402c4684 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_preauthentication.json @@ -0,0 +1,20 @@ +{ + "version": "1", + "triggerSource": "PreAuthentication_Authentication", + "region": "us-east-1", + "userPoolId": "us-east-1_uPoolId", + "userName": "testuser", + "callerContext": { + "awsSdkVersion": "2.0.0", + "clientId": "abcdefg1234567" + }, + "request": { + "userAttributes": { + "email": "user@example.com" + }, + "validationData": { + "key1": "val1" + }, + "userNotFound": false + } +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_presignup.json b/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_presignup.json new file mode 100644 index 000000000..0d1f0936a --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_presignup.json @@ -0,0 +1,27 @@ +{ + "version": "1", + "triggerSource": "PreSignUp_SignUp", + "region": "us-east-1", + "userPoolId": "us-east-1_uPoolId", + "userName": "testuser", + "callerContext": { + "awsSdkVersion": "2.0.0", + "clientId": "abcdefg1234567" + }, + "request": { + "userAttributes": { + "email": "user@example.com" + }, + "validationData": { + "key1": "val1" + }, + "clientMetadata": { + "meta1": "value1" + } + }, + "response": { + "autoConfirmUser": false, + "autoVerifyPhone": false, + "autoVerifyEmail": false + } +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_verify_auth_challenge.json b/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_verify_auth_challenge.json new file mode 100644 index 000000000..ef14c4ddf --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/cognito/cognito_userpool_verify_auth_challenge.json @@ -0,0 +1,27 @@ +{ + "version": "1", + "triggerSource": "VerifyAuthChallengeResponse_Authentication", + "region": "us-east-1", + "userPoolId": "us-east-1_uPoolId", + "userName": "testuser", + "callerContext": { + "awsSdkVersion": "2.0.0", + "clientId": "abcdefg1234567" + }, + "request": { + "userAttributes": { + "email": "user@example.com" + }, + "clientMetadata": { + "meta1": "value1" + }, + "privateChallengeParameters": { + "answer": "5" + }, + "challengeAnswer": "5", + "userNotFound": false + }, + "response": { + "answerCorrect": true + } +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/cognito_sync_event.json b/aws-lambda-java-tests/src/test/resources/cognito_sync_event.json new file mode 100644 index 000000000..6edf1c246 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/cognito_sync_event.json @@ -0,0 +1,20 @@ +{ + "version": 2, + "eventType": "SyncTrigger", + "region": "us-east-1", + "identityPoolId": "us-east-1:example-identity-pool-id", + "identityId": "us-east-1:example-identity-id", + "datasetName": "SampleDataset", + "datasetRecords": { + "SampleKey1": { + "oldValue": "oldValue1", + "newValue": "newValue1", + "op": "replace" + }, + "SampleKey2": { + "oldValue": "oldValue2", + "newValue": "newValue2", + "op": "replace" + } + } +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/cognito_user_pool_pre_token_generation_event_v2.json b/aws-lambda-java-tests/src/test/resources/cognito_user_pool_pre_token_generation_event_v2.json new file mode 100644 index 000000000..eb46b8cb3 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/cognito_user_pool_pre_token_generation_event_v2.json @@ -0,0 +1,39 @@ +{ + "version": "2", + "triggerSource": "TokenGeneration_Authentication", + "region": "us-east-1", + "userPoolId": "us-east-1_EXAMPLE", + "userName": "JaneDoe", + "callerContext": { + "awsSdkVersion": "aws-sdk-unknown-unknown", + "clientId": "1example23456789" + }, + "request": { + "userAttributes": { + "sub": "a1b2c3d4-5678-90ab-cdef-EXAMPLE11111", + "cognito:user_status": "CONFIRMED", + "email_verified": "true", + "phone_number_verified": "true", + "phone_number": "+12065551212", + "family_name": "Zoe", + "email": "Jane.Doe@example.com" + }, + "groupConfiguration": { + "groupsToOverride": ["group-1", "group-2", "group-3"], + "iamRolesToOverride": ["arn:aws:iam::123456789012:role/sns_caller1", "arn:aws:iam::123456789012:role/sns_caller2", "arn:aws:iam::123456789012:role/sns_caller3"], + "preferredRole": "arn:aws:iam::123456789012:role/sns_caller" + }, + "scopes": [ + "aws.cognito.signin.user.admin", "openid", "email", "phone" + ] + }, + "response": { + "claimsAndScopeOverrideDetails": { + "groupOverrideDetails": { + "groupsToOverride": ["group-99", "group-98"], + "iamRolesToOverride": ["arn:aws:iam::123456789012:role/sns_caller99", "arn:aws:iam::123456789012:role/sns_caller98"], + "preferredRole": "arn:aws:iam::123456789012:role/sns_caller_99" + } + } + } +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/config_event.json b/aws-lambda-java-tests/src/test/resources/config_event.json new file mode 100644 index 000000000..cf7956873 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/config_event.json @@ -0,0 +1,12 @@ +{ + "invokingEvent": "{\"configurationItem\":{\"configurationItemCaptureTime\":\"2016-10-06T16:46:16.261Z\",\"awsAccountId\":\"123456789012\",\"configurationItemStatus\":\"OK\",\"resourceId\":\"i-00000000\",\"resourceName\":\"foo\",\"configurationStateMd5Hash\":\"8f1ee69b297895a0f8bc5753eca68e96\",\"resourceCreationTime\":\"2016-10-06T16:46:10.489Z\",\"configurationStateId\":0,\"configurationItemVersion\":\"1.2\",\"ARN\":\"arn:aws:ec2:eu-central-1:123456789012:instance/i-00000000\",\"awsRegion\":\"eu-central-1\",\"availabilityZone\":\"eu-central-1\",\"resourceType\":\"AWS::EC2::Instance\",\"tags\":{\"\":\"\"},\"relationships\":[{\"resourceId\":\"eipalloc-00000000\",\"resourceType\":\"AWS::EC2::EIP\",\"name\":\"Is attached to ElasticIp\"}],\"configuration\":{\"\":\"\"}},\"messageType\":\"ConfigurationItemChangeNotification\"}", + "ruleParameters": "{\"\":\"\"}", + "resultToken": "myResultToken", + "eventLeftScope": false, + "executionRoleArn": "arn:aws:iam::123456789012:role/config-role", + "configRuleArn": "arn:aws:config:eu-central-1:123456789012:config-rule/config-rule-0123456", + "configRuleName": "change-triggered-config-rule", + "configRuleId": "config-rule-0123456", + "accountId": "123456789012", + "version": "1.0" +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/connect_event.json b/aws-lambda-java-tests/src/test/resources/connect_event.json new file mode 100644 index 000000000..4ce17a657 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/connect_event.json @@ -0,0 +1,27 @@ +{ + "Name": "ContactFlowEvent", + "Details": { + "ContactData": { + "Attributes": {}, + "Channel": "VOICE", + "ContactId": "5ca32fbd-8f92-46af-92a5-6b0f970f0efe", + "CustomerEndpoint": { + "Address": "+11234567890", + "Type": "TELEPHONE_NUMBER" + }, + "InitialContactId": "6ca32fbd-8f92-46af-92a5-6b0f970f0efe", + "InitiationMethod": "API", + "InstanceARN": "arn:aws:connect:eu-central-1:123456789012:instance/9308c2a1-9bc6-4cea-8290-6c0b4a6d38fa", + "PreviousContactId": "4ca32fbd-8f92-46af-92a5-6b0f970f0efe", + "Queue": { + "Name": "SampleQueue", + "ARN": "arn:aws:connect:eu-central-1:123456789012:instance/9308c2a1-9bc6-4cea-8290-6c0b4a6d38fa" + }, + "SystemEndpoint": { + "Address": "+21234567890", + "Type": "TELEPHONE_NUMBER" + } + }, + "Parameters": {} + } +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/ddb/dynamo_ddb_stream_record.json b/aws-lambda-java-tests/src/test/resources/ddb/dynamo_ddb_stream_record.json new file mode 100644 index 000000000..f5df23ff5 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/ddb/dynamo_ddb_stream_record.json @@ -0,0 +1,35 @@ +{ + "eventID": "c81e728d9d4c2f636f067f89cc14862c", + "eventName": "MODIFY", + "eventVersion": "1.1", + "eventSource": "aws:dynamodb", + "awsRegion": "eu-central-1", + "dynamodb": { + "Keys": { + "Id": { + "N": "101" + } + }, + "NewImage": { + "Message": { + "S": "This item has changed" + }, + "Id": { + "N": "101" + } + }, + "OldImage": { + "Message": { + "S": "New item!" + }, + "Id": { + "N": "101" + } + }, + "ApproximateCreationDateTime": 1.635734407123456789E9, + "SequenceNumber": "4421584500000000017450439092", + "SizeBytes": 59, + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "eventSourceARN": "arn:aws:dynamodb:eu-central-1:123456789012:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899" +} diff --git a/aws-lambda-java-tests/src/test/resources/ddb/dynamo_event.json b/aws-lambda-java-tests/src/test/resources/ddb/dynamo_event.json new file mode 100644 index 000000000..2e43ba497 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/ddb/dynamo_event.json @@ -0,0 +1,97 @@ +{ + "Records": [ + { + "eventID": "c4ca4238a0b923820dcc509a6f75849b", + "eventName": "INSERT", + "eventVersion": "1.1", + "eventSource": "aws:dynamodb", + "awsRegion": "eu-central-1", + "dynamodb": { + "Keys": { + "Id": { + "N": "101" + } + }, + "NewImage": { + "Message": { + "S": "New item!" + }, + "Id": { + "N": "101" + } + }, + "ApproximateCreationDateTime": 1428537600, + "SequenceNumber": "4421584500000000017450439091", + "SizeBytes": 26, + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "eventSourceARN": "arn:aws:dynamodb:eu-central-1:123456789012:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899", + "userIdentity": { + "principalId": "dynamodb.amazonaws.com", + "type": "Service" + } + }, + { + "eventID": "c81e728d9d4c2f636f067f89cc14862c", + "eventName": "MODIFY", + "eventVersion": "1.1", + "eventSource": "aws:dynamodb", + "awsRegion": "eu-central-1", + "dynamodb": { + "Keys": { + "Id": { + "N": "101" + } + }, + "NewImage": { + "Message": { + "S": "This item has changed" + }, + "Id": { + "N": "101" + } + }, + "OldImage": { + "Message": { + "S": "New item!" + }, + "Id": { + "N": "101" + } + }, + "ApproximateCreationDateTime": 1.635734407123456789E9, + "SequenceNumber": "4421584500000000017450439092", + "SizeBytes": 59, + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "eventSourceARN": "arn:aws:dynamodb:eu-central-1:123456789012:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899" + }, + { + "eventID": "eccbc87e4b5ce2fe28308fd9f2a7baf3", + "eventName": "REMOVE", + "eventVersion": "1.1", + "eventSource": "aws:dynamodb", + "awsRegion": "eu-central-1", + "dynamodb": { + "Keys": { + "Id": { + "N": "101" + } + }, + "OldImage": { + "Message": { + "S": "This item has changed" + }, + "Id": { + "N": "101" + } + }, + "ApproximateCreationDateTime": 1428537600, + "SequenceNumber": "4421584500000000017450439093", + "SizeBytes": 38, + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "eventSourceARN": "arn:aws:dynamodb:eu-central-1:123456789012:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899" + } + ] +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/ddb/dynamo_event_roundtrip.json b/aws-lambda-java-tests/src/test/resources/ddb/dynamo_event_roundtrip.json new file mode 100644 index 000000000..10d963c3c --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/ddb/dynamo_event_roundtrip.json @@ -0,0 +1,97 @@ +{ + "Records": [ + { + "eventID": "c4ca4238a0b923820dcc509a6f75849b", + "eventName": "INSERT", + "eventVersion": "1.1", + "eventSource": "aws:dynamodb", + "awsRegion": "eu-central-1", + "dynamodb": { + "Keys": { + "Id": { + "N": "101" + } + }, + "NewImage": { + "Message": { + "S": "New item!" + }, + "Id": { + "N": "101" + } + }, + "ApproximateCreationDateTime": 1.4285376E9, + "SequenceNumber": "4421584500000000017450439091", + "SizeBytes": 26, + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "eventSourceARN": "arn:aws:dynamodb:eu-central-1:123456789012:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899", + "userIdentity": { + "principalId": "dynamodb.amazonaws.com", + "type": "Service" + } + }, + { + "eventID": "c81e728d9d4c2f636f067f89cc14862c", + "eventName": "MODIFY", + "eventVersion": "1.1", + "eventSource": "aws:dynamodb", + "awsRegion": "eu-central-1", + "dynamodb": { + "Keys": { + "Id": { + "N": "101" + } + }, + "NewImage": { + "Message": { + "S": "This item has changed" + }, + "Id": { + "N": "101" + } + }, + "OldImage": { + "Message": { + "S": "New item!" + }, + "Id": { + "N": "101" + } + }, + "ApproximateCreationDateTime": 1.635734407123E9, + "SequenceNumber": "4421584500000000017450439092", + "SizeBytes": 59, + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "eventSourceARN": "arn:aws:dynamodb:eu-central-1:123456789012:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899" + }, + { + "eventID": "eccbc87e4b5ce2fe28308fd9f2a7baf3", + "eventName": "REMOVE", + "eventVersion": "1.1", + "eventSource": "aws:dynamodb", + "awsRegion": "eu-central-1", + "dynamodb": { + "Keys": { + "Id": { + "N": "101" + } + }, + "OldImage": { + "Message": { + "S": "This item has changed" + }, + "Id": { + "N": "101" + } + }, + "ApproximateCreationDateTime": 1.4285376E9, + "SequenceNumber": "4421584500000000017450439093", + "SizeBytes": 38, + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "eventSourceARN": "arn:aws:dynamodb:eu-central-1:123456789012:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899" + } + ] +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/ddb/dynamo_time_window_event.json b/aws-lambda-java-tests/src/test/resources/ddb/dynamo_time_window_event.json new file mode 100644 index 000000000..d931acb80 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/ddb/dynamo_time_window_event.json @@ -0,0 +1,75 @@ +{ + "Records": [ + { + "eventID": "1", + "eventName": "INSERT", + "eventVersion": "1.0", + "eventSource": "aws:dynamodb", + "awsRegion": "us-east-1", + "dynamodb": { + "Keys": { + "Id": { + "N": "101" + } + }, + "NewImage": { + "Message": { + "S": "New item!" + }, + "Id": { + "N": "101" + } + }, + "SequenceNumber": "111", + "SizeBytes": 26, + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "eventSourceARN": "arn:aws:dynamodb:us-east-1:123456789012:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899" + }, + { + "eventID": "2", + "eventName": "MODIFY", + "eventVersion": "1.0", + "eventSource": "aws:dynamodb", + "awsRegion": "us-east-1", + "dynamodb": { + "Keys": { + "Id": { + "N": "101" + } + }, + "NewImage": { + "Message": { + "S": "This item has changed" + }, + "Id": { + "N": "101" + } + }, + "OldImage": { + "Message": { + "S": "New item!" + }, + "Id": { + "N": "101" + } + }, + "SequenceNumber": "222", + "SizeBytes": 59, + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "eventSourceARN": "arn:aws:dynamodb:us-east-1:123456789012:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899" + } + ], + "window": { + "start": "2020-07-30T17:00:00Z", + "end": "2020-07-30T17:05:00Z" + }, + "state": { + "1": "state1" + }, + "shardId": "shard123456789", + "eventSourceARN": "arn:aws:dynamodb:us-east-1:123456789012:table/ExampleTableWithStream/stream/2015-06-27T00:48:05.899", + "isFinalInvokeForWindow": false, + "isWindowTerminatedEarly": false +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/elb_event.json b/aws-lambda-java-tests/src/test/resources/elb_event.json new file mode 100644 index 000000000..23f599f4d --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/elb_event.json @@ -0,0 +1,26 @@ +{ + "requestContext": { + "elb": { + "targetGroupArn": "arn:aws:elasticloadbalancing:us-east-1:XXXXXXXXXXX:targetgroup/sample/6d0ecf831eec9f09" + } + }, + "httpMethod": "GET", + "path": "/", + "queryStringParameters": {}, + "headers": { + "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "accept-encoding": "gzip", + "accept-language": "en-US,en;q=0.5", + "connection": "keep-alive", + "cookie": "name=value", + "host": "lambda-YYYYYYYY.elb.amazonaws.com", + "upgrade-insecure-requests": "1", + "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:60.0) Gecko/20100101 Firefox/60.0", + "x-amzn-trace-id": "Root=1-5bdb40ca-556d8b0c50dc66f0511bf520", + "x-forwarded-for": "192.0.2.1", + "x-forwarded-port": "80", + "x-forwarded-proto": "http" + }, + "body": "Hello from ELB", + "isBase64Encoded": false +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/firehose_event.json b/aws-lambda-java-tests/src/test/resources/firehose_event.json new file mode 100644 index 000000000..df2358393 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/firehose_event.json @@ -0,0 +1,12 @@ +{ + "invocationId": "invocationIdExample", + "deliveryStreamArn": "arn:aws:kinesis:EXAMPLE", + "region": "eu-central-1", + "records": [ + { + "recordId": "49546986683135544286507457936321625675700192471156785154", + "approximateArrivalTimestamp": 1495072949453, + "data": "SGVsbG8sIHRoaXMgaXMgYSB0ZXN0IDEyMy4=" + } + ] +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/iot_button_event.json b/aws-lambda-java-tests/src/test/resources/iot_button_event.json new file mode 100644 index 000000000..8dc82826b --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/iot_button_event.json @@ -0,0 +1,5 @@ +{ + "serialNumber": "G030JF055364XVRB", + "clickType": "SINGLE", + "batteryVoltage": "2000mV" +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/kafka_event.json b/aws-lambda-java-tests/src/test/resources/kafka_event.json new file mode 100644 index 000000000..a7be5e8ff --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/kafka_event.json @@ -0,0 +1,34 @@ +{ + "eventSource": "aws:kafka", + "eventSourceArn": "arn:aws:kafka:us-east-1:123456789012:cluster/vpc-3432434/4834-3547-3455-9872-7929", + "bootstrapServers": "b-2.demo-cluster-1.a1bcde.c1.kafka.us-east-1.amazonaws.com:9092,b-1.demo-cluster-1.a1bcde.c1.kafka.us-east-1.amazonaws.com:9092", + "records": { + "mytopic-01": [ + { + "topic": "mytopic", + "partition": 0, + "offset": 15, + "timestamp": 1596480920837, + "timestampType": "CREATE_TIME", + "value": "SGVsbG8gZnJvbSBLYWZrYSAhIQ==", + "headers": [ + { + "headerKey": [ + 104, + 101, + 97, + 100, + 101, + 114, + 86, + 97, + 108, + 117, + 101 + ] + } + ] + } + ] + } +} diff --git a/aws-lambda-java-tests/src/test/resources/kafka_event_roundtrip.json b/aws-lambda-java-tests/src/test/resources/kafka_event_roundtrip.json new file mode 100644 index 000000000..d9f682e5f --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/kafka_event_roundtrip.json @@ -0,0 +1,22 @@ +{ + "eventSource": "aws:kafka", + "eventSourceArn": "arn:aws:kafka:us-east-1:123456789012:cluster/vpc-3432434/4834-3547-3455-9872-7929", + "bootstrapServers": "b-2.demo-cluster-1.a1bcde.c1.kafka.us-east-1.amazonaws.com:9092,b-1.demo-cluster-1.a1bcde.c1.kafka.us-east-1.amazonaws.com:9092", + "records": { + "mytopic-01": [ + { + "topic": "mytopic", + "partition": 0, + "offset": 15, + "timestamp": 1596480920837, + "timestampType": "CREATE_TIME", + "value": "SGVsbG8gZnJvbSBLYWZrYSAhIQ==", + "headers": [ + { + "headerKey": "aGVhZGVyVmFsdWU=" + } + ] + } + ] + } +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/kinesis/kinesis_analytics_firehose_input_preprocessing.json b/aws-lambda-java-tests/src/test/resources/kinesis/kinesis_analytics_firehose_input_preprocessing.json new file mode 100644 index 000000000..8c6cfe514 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/kinesis/kinesis_analytics_firehose_input_preprocessing.json @@ -0,0 +1,14 @@ +{ + "invocationId": "invocationIdExample", + "applicationArn": "arn:aws:kinesisanalytics:us-east-1:123456789012:application/my-app", + "streamArn": "arn:aws:firehose:us-east-1:123456789012:deliverystream/my-stream", + "records": [ + { + "recordId": "49546986683135544286507457936321625675700192471156785154", + "kinesisFirehoseRecordMetadata": { + "approximateArrivalTimestamp": 1583348638390 + }, + "data": "SGVsbG8gV29ybGQ=" + } + ] +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/kinesis/kinesis_analytics_input_preprocessing_response.json b/aws-lambda-java-tests/src/test/resources/kinesis/kinesis_analytics_input_preprocessing_response.json new file mode 100644 index 000000000..f5be190ec --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/kinesis/kinesis_analytics_input_preprocessing_response.json @@ -0,0 +1,9 @@ +{ + "records": [ + { + "recordId": "49546986683135544286507457936321625675700192471156785154", + "result": "Ok", + "data": "SGVsbG8gV29ybGQ=" + } + ] +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/kinesis/kinesis_analytics_output_delivery.json b/aws-lambda-java-tests/src/test/resources/kinesis/kinesis_analytics_output_delivery.json new file mode 100644 index 000000000..573b6baba --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/kinesis/kinesis_analytics_output_delivery.json @@ -0,0 +1,13 @@ +{ + "invocationId": "invocationIdExample", + "applicationArn": "arn:aws:kinesisanalytics:us-east-1:123456789012:application/my-app", + "records": [ + { + "recordId": "49546986683135544286507457936321625675700192471156785154", + "lambdaDeliveryRecordMetadata": { + "retryHint": 0 + }, + "data": "SGVsbG8gV29ybGQ=" + } + ] +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/kinesis/kinesis_analytics_output_delivery_response.json b/aws-lambda-java-tests/src/test/resources/kinesis/kinesis_analytics_output_delivery_response.json new file mode 100644 index 000000000..56ddaa194 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/kinesis/kinesis_analytics_output_delivery_response.json @@ -0,0 +1,8 @@ +{ + "records": [ + { + "recordId": "49546986683135544286507457936321625675700192471156785154", + "result": "Ok" + } + ] +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/kinesis/kinesis_analytics_streams_input_preprocessing.json b/aws-lambda-java-tests/src/test/resources/kinesis/kinesis_analytics_streams_input_preprocessing.json new file mode 100644 index 000000000..4ae8f3705 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/kinesis/kinesis_analytics_streams_input_preprocessing.json @@ -0,0 +1,17 @@ +{ + "invocationId": "invocationIdExample", + "applicationArn": "arn:aws:kinesisanalytics:us-east-1:123456789012:application/my-app", + "streamArn": "arn:aws:kinesis:us-east-1:123456789012:stream/my-stream", + "records": [ + { + "recordId": "49546986683135544286507457936321625675700192471156785154", + "kinesisStreamRecordMetadata": { + "sequenceNumber": "49546986683135544286507457936321625675700192471156785154", + "partitionKey": "partKey", + "shardId": "shardId-000000000000", + "approximateArrivalTimestamp": 1583348638390 + }, + "data": "SGVsbG8gV29ybGQ=" + } + ] +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/kinesis/kinesis_event_roundtrip.json b/aws-lambda-java-tests/src/test/resources/kinesis/kinesis_event_roundtrip.json new file mode 100644 index 000000000..e2081ef2b --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/kinesis/kinesis_event_roundtrip.json @@ -0,0 +1,21 @@ +{ + "Records": [ + { + "kinesis": { + "partitionKey": "partitionKey-03", + "kinesisSchemaVersion": "1.0", + "data": "SGVsbG8sIHRoaXMgaXMgYSB0ZXN0IDEyMy4=", + "sequenceNumber": "49545115243490985018280067714973144582180062593244200961", + "approximateArrivalTimestamp": 1.4285376E9, + "encryptionType": "NONE" + }, + "eventSource": "aws:kinesis", + "eventID": "shardId-000000000000:49545115243490985018280067714973144582180062593244200961", + "invokeIdentityArn": "arn:aws:iam::EXAMPLE", + "eventVersion": "1.0", + "eventName": "aws:kinesis:record", + "eventSourceARN": "arn:aws:kinesis:EXAMPLE", + "awsRegion": "eu-central-1" + } + ] +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/kinesis/kinesis_time_window_event.json b/aws-lambda-java-tests/src/test/resources/kinesis/kinesis_time_window_event.json new file mode 100644 index 000000000..2d6283c58 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/kinesis/kinesis_time_window_event.json @@ -0,0 +1,32 @@ +{ + "Records": [ + { + "kinesis": { + "kinesisSchemaVersion": "1.0", + "partitionKey": "1", + "sequenceNumber": "49590338271490256608559692538361571095921575989136588898", + "data": "SGVsbG8sIHRoaXMgaXMgYSB0ZXN0Lg==", + "approximateArrivalTimestamp": 1.607497475E9 + }, + "eventSource": "aws:kinesis", + "eventVersion": "1.0", + "eventID": "shardId-000000000006:49590338271490256608559692538361571095921575989136588898", + "eventName": "aws:kinesis:record", + "invokeIdentityArn": "arn:aws:iam::123456789012:role/lambda-kinesis-role", + "awsRegion": "us-east-1", + "eventSourceARN": "arn:aws:kinesis:us-east-1:123456789012:stream/lambda-stream" + } + ], + "window": { + "start": "2020-12-09T07:04:00Z", + "end": "2020-12-09T07:06:00Z" + }, + "state": { + "1": "282", + "2": "715" + }, + "shardId": "shardId-000000000006", + "eventSourceARN": "arn:aws:kinesis:us-east-1:123456789012:stream/lambda-stream", + "isFinalInvokeForWindow": false, + "isWindowTerminatedEarly": false +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/kinesis_event.json b/aws-lambda-java-tests/src/test/resources/kinesis_event.json new file mode 100644 index 000000000..5e083f496 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/kinesis_event.json @@ -0,0 +1,21 @@ +{ + "Records": [ + { + "kinesis": { + "partitionKey": "partitionKey-03", + "kinesisSchemaVersion": "1.0", + "data": "SGVsbG8sIHRoaXMgaXMgYSB0ZXN0IDEyMy4=", + "sequenceNumber": "49545115243490985018280067714973144582180062593244200961", + "approximateArrivalTimestamp": 1428537600, + "encryptionType": "NONE" + }, + "eventSource": "aws:kinesis", + "eventID": "shardId-000000000000:49545115243490985018280067714973144582180062593244200961", + "invokeIdentityArn": "arn:aws:iam::EXAMPLE", + "eventVersion": "1.0", + "eventName": "aws:kinesis:record", + "eventSourceARN": "arn:aws:kinesis:EXAMPLE", + "awsRegion": "eu-central-1" + } + ] +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/lambda_destination_event.json b/aws-lambda-java-tests/src/test/resources/lambda_destination_event.json new file mode 100644 index 000000000..30dea813c --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/lambda_destination_event.json @@ -0,0 +1,23 @@ +{ + "version": "1.0", + "timestamp": "2019-11-24T21:52:47.333Z", + "requestContext": { + "requestId": "8ea123e4-1db7-4aca-ad10-d9ca1234c1fd", + "functionArn": "arn:aws:lambda:sa-east-1:123456678912:function:event-destinations:$LATEST", + "condition": "RetriesExhausted", + "approximateInvokeCount": 3 + }, + "requestPayload": { + "Success": false + }, + "responseContext": { + "statusCode": 200, + "executedVersion": "$LATEST", + "functionError": "Handled" + }, + "responsePayload": { + "errorMessage": "Failure from event, Success = false, I am failing!", + "errorType": "Error", + "stackTrace": [ "exports.handler (/var/task/index.js:18:18)" ] + } +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/lex_event.json b/aws-lambda-java-tests/src/test/resources/lex_event.json new file mode 100644 index 000000000..880baa156 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/lex_event.json @@ -0,0 +1,24 @@ +{ + "messageVersion": "1.0", + "invocationSource": "DialogCodeHook", + "userId": "John", + "sessionAttributes": { + "key": "value" + }, + "bot": { + "name": "BookTrip", + "alias": "$LATEST", + "version": "$LATEST" + }, + "outputDialogMode": "Text", + "currentIntent": { + "name": "BookHotel", + "slots": { + "Location": "Chicago", + "CheckInDate": "2030-11-08", + "Nights": 4, + "RoomType": "queen" + }, + "confirmationStatus": "None" + } +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/lex_event_roundtrip.json b/aws-lambda-java-tests/src/test/resources/lex_event_roundtrip.json new file mode 100644 index 000000000..be065eb3f --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/lex_event_roundtrip.json @@ -0,0 +1,24 @@ +{ + "messageVersion": "1.0", + "invocationSource": "DialogCodeHook", + "userId": "John", + "sessionAttributes": { + "key": "value" + }, + "bot": { + "name": "BookTrip", + "alias": "$LATEST", + "version": "$LATEST" + }, + "outputDialogMode": "Text", + "currentIntent": { + "name": "BookHotel", + "slots": { + "Location": "Chicago", + "CheckInDate": "2030-11-08", + "Nights": "4", + "RoomType": "queen" + }, + "confirmationStatus": "None" + } +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/mq_event.json b/aws-lambda-java-tests/src/test/resources/mq_event.json new file mode 100644 index 000000000..8b12af72e --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/mq_event.json @@ -0,0 +1,42 @@ +{ + "eventSource": "aws:amq", + "eventSourceArn": "arn:aws:mq:us-west-2:112556298976:broker:test:b-9bcfa592-423a-4942-879d-eb284b418fc8", + "messages": [ + { + "messageID": "ID:b-9bcfa592-423a-4942-879d-eb284b418fc8-1.mq.us-west-2.amazonaws.com-37557-1234520418293-4:1:1:1:1", + "messageType": "jms/text-message", + "timestamp": 1598827811958, + "deliveryMode": 0, + "redelivered": false, + "expiration": 0, + "priority": 0, + "data": "QUJDOkFBQUE=", + "brokerInTime": 1598827811958, + "brokerOutTime": 1598827811959, + "destination": { + "physicalname": "testQueue" + }, + "properties": { + "testKey": "testValue" + } + }, + { + "messageID": "ID:b-8bcfa572-428a-4642-879d-eb284b418fc8-1.mq.us-west-2.amazonaws.com-37557-1234520418293-4:1:1:1:1", + "messageType": "jms/bytes-message", + "timestamp": 1598827811958, + "deliveryMode": 0, + "redelivered": false, + "expiration": 0, + "priority": 0, + "data": "3DTOOW7crj51prgVLQaGQ82S48k=", + "brokerInTime": 1598827811958, + "brokerOutTime": 1598827811959, + "destination": { + "physicalname": "testQueue" + }, + "properties": { + "testKey": "testValue" + } + } + ] +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/msk_firehose_event.json b/aws-lambda-java-tests/src/test/resources/msk_firehose_event.json new file mode 100644 index 000000000..140908250 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/msk_firehose_event.json @@ -0,0 +1,18 @@ +{ + "invocationId": "12345621-4787-0000-a418-36e56Example", + "sourceMSKArn": "arn:aws:kafka:EXAMPLE", + "deliveryStreamArn": "arn:aws:firehose:EXAMPLE", + "region": "us-east-1", + "records": [ + { + "recordId": "00000000000000000000000000000000000000000000000000000000000000", + "approximateArrivalTimestamp": 1716369573887, + "mskRecordMetadata": { + "offset": "0", + "partitionId": "1", + "approximateArrivalTimestamp": 1716369573887 + }, + "kafkaRecordValue": "eyJOYW1lIjoiSGVsbG8gV29ybGQifQ==" + } + ] +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/msk_firehose_event_roundtrip.json b/aws-lambda-java-tests/src/test/resources/msk_firehose_event_roundtrip.json new file mode 100644 index 000000000..81b0a9c81 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/msk_firehose_event_roundtrip.json @@ -0,0 +1,18 @@ +{ + "invocationId": "12345621-4787-0000-a418-36e56Example", + "sourceMSKArn": "arn:aws:kafka:EXAMPLE", + "deliveryStreamArn": "arn:aws:firehose:EXAMPLE", + "region": "us-east-1", + "records": [ + { + "recordId": "00000000000000000000000000000000000000000000000000000000000000", + "approximateArrivalTimestamp": 1716369573887, + "mskRecordMetadata": { + "offset": "0", + "partitionId": "1", + "approximateArrivalTimestamp": "1716369573887" + }, + "kafkaRecordValue": "eyJOYW1lIjoiSGVsbG8gV29ybGQifQ==" + } + ] +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/partial_pojo.json b/aws-lambda-java-tests/src/test/resources/partial_pojo.json new file mode 100644 index 000000000..398218039 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/partial_pojo.json @@ -0,0 +1,4 @@ +{ + "name": "test", + "unknownField": "this will be dropped" +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/rabbitmq_event.json b/aws-lambda-java-tests/src/test/resources/rabbitmq_event.json new file mode 100644 index 000000000..1f57c53a3 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/rabbitmq_event.json @@ -0,0 +1,51 @@ +{ + "eventSource": "aws:rmq", + "eventSourceArn": "arn:aws:mq:us-west-2:112556298976:broker:test:b-9bcfa592-423a-4942-879d-eb284b418fc8", + "rmqMessagesByQueue": { + "test::/": [ + { + "basicProperties": { + "contentType": "text/plain", + "contentEncoding": null, + "headers": { + "header1": { + "bytes": [ + 118, + 97, + 108, + 117, + 101, + 49 + ] + }, + "header2": { + "bytes": [ + 118, + 97, + 108, + 117, + 101, + 50 + ] + }, + "numberInHeader": 10 + }, + "deliveryMode": 1, + "priority": 34, + "correlationId": null, + "replyTo": null, + "expiration": "60000", + "messageId": null, + "timestamp": "Jan 1, 1970, 12:33:41 AM", + "type": null, + "userId": "AIDACKCEVSQ6C2EXAMPLE", + "appId": null, + "clusterId": null, + "bodySize": 80 + }, + "redelivered": false, + "data": "eyJ0aW1lb3V0IjowLCJkYXRhIjoiQ1pybWYwR3c4T3Y0YnFMUXhENEUifQ==" + } + ] + } +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/rabbitmq_event_roundtrip.json b/aws-lambda-java-tests/src/test/resources/rabbitmq_event_roundtrip.json new file mode 100644 index 000000000..44edf2f0a --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/rabbitmq_event_roundtrip.json @@ -0,0 +1,51 @@ +{ + "eventSource": "aws:rmq", + "eventSourceArn": "arn:aws:mq:us-west-2:112556298976:broker:test:b-9bcfa592-423a-4942-879d-eb284b418fc8", + "rmqMessagesByQueue": { + "test::/": [ + { + "basicProperties": { + "contentType": "text/plain", + "contentEncoding": null, + "headers": { + "header1": { + "bytes": [ + 118, + 97, + 108, + 117, + 101, + 49 + ] + }, + "header2": { + "bytes": [ + 118, + 97, + 108, + 117, + 101, + 50 + ] + }, + "numberInHeader": 10 + }, + "deliveryMode": 1, + "priority": 34, + "correlationId": null, + "replyTo": null, + "expiration": 60000, + "messageId": null, + "timestamp": "Jan 1, 1970, 12:33:41 AM", + "type": null, + "userId": "AIDACKCEVSQ6C2EXAMPLE", + "appId": null, + "clusterId": null, + "bodySize": 80 + }, + "redelivered": false, + "data": "eyJ0aW1lb3V0IjowLCJkYXRhIjoiQ1pybWYwR3c4T3Y0YnFMUXhENEUifQ==" + } + ] + } +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/response/alb_response.json b/aws-lambda-java-tests/src/test/resources/response/alb_response.json new file mode 100644 index 000000000..355eb193a --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/response/alb_response.json @@ -0,0 +1,15 @@ +{ + "statusCode": 200, + "statusDescription": "200 OK", + "headers": { + "Content-Type": "text/html" + }, + "multiValueHeaders": { + "Set-Cookie": [ + "cookie1=value1", + "cookie2=value2" + ] + }, + "body": "Hello", + "isBase64Encoded": false +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/response/apigw_proxy_response.json b/aws-lambda-java-tests/src/test/resources/response/apigw_proxy_response.json new file mode 100644 index 000000000..640ccdc5c --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/response/apigw_proxy_response.json @@ -0,0 +1,15 @@ +{ + "statusCode": 200, + "headers": { + "Content-Type": "application/json", + "X-Custom-Header": "custom-value" + }, + "multiValueHeaders": { + "Set-Cookie": [ + "cookie1=value1", + "cookie2=value2" + ] + }, + "body": "{\"message\":\"Hello from Lambda\"}", + "isBase64Encoded": false +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/response/apigw_v2_http_response.json b/aws-lambda-java-tests/src/test/resources/response/apigw_v2_http_response.json new file mode 100644 index 000000000..c39236650 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/response/apigw_v2_http_response.json @@ -0,0 +1,16 @@ +{ + "statusCode": 200, + "headers": { + "Content-Type": "application/json" + }, + "multiValueHeaders": { + "Set-Cookie": [ + "cookie1=value1" + ] + }, + "cookies": [ + "session=abc123; Secure; HttpOnly" + ], + "body": "{\"message\":\"OK\"}", + "isBase64Encoded": false +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/response/apigw_v2_websocket_response.json b/aws-lambda-java-tests/src/test/resources/response/apigw_v2_websocket_response.json new file mode 100644 index 000000000..08392e890 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/response/apigw_v2_websocket_response.json @@ -0,0 +1,14 @@ +{ + "statusCode": 200, + "headers": { + "Content-Type": "application/json" + }, + "multiValueHeaders": { + "X-Custom": [ + "val1", + "val2" + ] + }, + "body": "{\"action\":\"sendmessage\",\"data\":\"hello\"}", + "isBase64Encoded": false +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/response/msk_firehose_response.json b/aws-lambda-java-tests/src/test/resources/response/msk_firehose_response.json new file mode 100644 index 000000000..9ac497624 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/response/msk_firehose_response.json @@ -0,0 +1,9 @@ +{ + "records": [ + { + "recordId": "record-1", + "result": "Ok", + "kafkaRecordValue": "dHJhbnNmb3JtZWQgZGF0YQ==" + } + ] +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/response/s3_batch_response.json b/aws-lambda-java-tests/src/test/resources/response/s3_batch_response.json new file mode 100644 index 000000000..e63439d84 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/response/s3_batch_response.json @@ -0,0 +1,12 @@ +{ + "invocationSchemaVersion": "1.0", + "treatMissingKeysAs": "PermanentFailure", + "invocationId": "YXNkbGZqYWRmaiBhc2RmdW9hZHNmZGpmaGFzbGtkaGZza2RmaAo", + "results": [ + { + "taskId": "dGFza2lkZ29lc2hlcmUK", + "resultCode": "Succeeded", + "resultString": "Successfully processed" + } + ] +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/response/simple_iam_policy_response.json b/aws-lambda-java-tests/src/test/resources/response/simple_iam_policy_response.json new file mode 100644 index 000000000..5f23b6405 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/response/simple_iam_policy_response.json @@ -0,0 +1,7 @@ +{ + "isAuthorized": true, + "context": { + "userId": "user-123", + "scope": "read:all" + } +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/response/sqs_batch_response.json b/aws-lambda-java-tests/src/test/resources/response/sqs_batch_response.json new file mode 100644 index 000000000..5ef2de697 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/response/sqs_batch_response.json @@ -0,0 +1,7 @@ +{ + "batchItemFailures": [ + { + "itemIdentifier": "059f36b4-87a3-44ab-83d2-661975830a7d" + } + ] +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/s3_batch_event.json b/aws-lambda-java-tests/src/test/resources/s3_batch_event.json new file mode 100644 index 000000000..a70af0fdd --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/s3_batch_event.json @@ -0,0 +1,15 @@ +{ + "invocationSchemaVersion": "1.0", + "invocationId": "YXNkbGZqYWRmaiBhc2RmdW9hZHNmZGpmaGFzbGtkaGZza2RmaAo", + "job": { + "id": "f3cc4f60-61f6-4a2b-8a21-d07600c373ce" + }, + "tasks": [ + { + "taskId": "dGFza2lkZ29lc2hlcmUK", + "s3Key": "customerImage1.jpg", + "s3VersionId": "1", + "s3BucketArn": "arn:aws:s3:::amzn-s3-demo-bucket" + } + ] +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/s3_batch_event_v2.json b/aws-lambda-java-tests/src/test/resources/s3_batch_event_v2.json new file mode 100644 index 000000000..4cdbacaaa --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/s3_batch_event_v2.json @@ -0,0 +1,21 @@ +{ + "invocationSchemaVersion": "2.0", + "invocationId": "Jr3s8KZqYWRmaiBhc2RmdW9hZHNmZGpmaGFzbGtkaGZzatx7Ruy", + "job": { + "id": "ry77cd60-61f6-4a2b-8a21-d07600c874gf", + "userArguments": { + "MyDestinationBucket": "destination-directory-bucket-name", + "MyDestinationBucketRegion": "us-east-1", + "MyDestinationPrefix": "copied/", + "MyDestinationObjectKeySuffix": "_new_suffix" + } + }, + "tasks": [ + { + "taskId": "y5R3a2lkZ29lc2hlurcS", + "s3Key": "s3objectkey", + "s3VersionId": null, + "s3Bucket": "source-directory-bucket-name" + } + ] +} diff --git a/aws-lambda-java-tests/src/test/resources/s3_event.json b/aws-lambda-java-tests/src/test/resources/s3_event.json new file mode 100644 index 000000000..73f59d072 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/s3_event.json @@ -0,0 +1,40 @@ +{ + "Records": [ + { + "eventVersion": "2.0", + "eventSource": "aws:s3", + "awsRegion": "eu-central-1", + "eventTime": "1970-01-01T00:00:00.000Z", + "eventName": "ObjectCreated:Put", + "userIdentity": { + "principalId": "EXAMPLE" + }, + "requestParameters": { + "sourceIPAddress": "127.0.0.1" + }, + "responseElements": { + "x-amz-request-id": "EXAMPLE123456789", + "x-amz-id-2": "EXAMPLE123/5678abcdefghijklambdaisawesome/mnopqrstuvwxyzABCDEFGH" + }, + "s3": { + "s3SchemaVersion": "1.0", + "configurationId": "testConfigRule", + "bucket": { + "name": "example-bucket", + "ownerIdentity": { + "principalId": "EXAMPLE" + }, + "arn": "arn:aws:s3:::example-bucket" + }, + "object": { + "key": "test/key", + "urlDecodedKey": "test/key", + "size": 1024, + "eTag": "0123456789abcdef0123456789abcdef", + "versionId": "", + "sequencer": "0A1B2C3D4E5F678901" + } + } + } + ] +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/s3_object_lambda_event.json b/aws-lambda-java-tests/src/test/resources/s3_object_lambda_event.json new file mode 100644 index 000000000..db996e71c --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/s3_object_lambda_event.json @@ -0,0 +1,29 @@ +{ + "xAmzRequestId": "requestId", + "getObjectContext": { + "inputS3Url": "https://my-s3-ap-111122223333.s3-accesspoint.us-east-1.amazonaws.com/example?X-Amz-Security-Token=snip", + "outputRoute": "io-use1-001", + "outputToken": "OutputToken" + }, + "configuration": { + "accessPointArn": "arn:aws:s3-object-lambda:us-east-1:111122223333:accesspoint/example-object-lambda-ap", + "supportingAccessPointArn": "arn:aws:s3:us-east-1:111122223333:accesspoint/example-ap", + "payload": "{}" + }, + "userRequest": { + "url": "https://object-lambda-111122223333.s3-object-lambda.us-east-1.amazonaws.com/example", + "headers": { + "Host": "object-lambda-111122223333.s3-object-lambda.us-east-1.amazonaws.com", + "Accept-Encoding": "identity", + "X-Amz-Content-SHA256": "e3b0c44298fc1example" + } + }, + "userIdentity": { + "type": "AssumedRole", + "principalId": "principalId", + "arn": "arn:aws:sts::111122223333:assumed-role/Admin/example", + "accountId": "111122223333", + "accessKeyId": "accessKeyId" + }, + "protocolVersion": "1.00" +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/secrets_rotation_event.json b/aws-lambda-java-tests/src/test/resources/secrets_rotation_event.json new file mode 100644 index 000000000..e8d80b573 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/secrets_rotation_event.json @@ -0,0 +1,6 @@ +{ + "Step" : "CreateSecret", + "SecretId" : "arn:aws:secretsmanager:eu-central-1:123456789012:secret:/powertools/secretparam-xBPaJ5", + "ClientRequestToken" : "123e4567-e89b-12d3-a456-426614174000", + "RotationToken": "8a4cc1ac-82ea-47c7-bd9f-aeb370b1b6a6" +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/sns_event.json b/aws-lambda-java-tests/src/test/resources/sns_event.json new file mode 100644 index 000000000..25e123c48 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/sns_event.json @@ -0,0 +1,27 @@ +{ + "Records": [ + { + "EventSource": "aws:sns", + "EventVersion": "1.0", + "EventSubscriptionArn": "arn:aws:sns:eu-central-1:123456789012:TopicSendToMe:e3ddc7d5-2f86-40b8-a13d-3362f94fd8dd", + "Sns": { + "Type": "Notification", + "MessageId": "dc918f50-80c6-56a2-ba33-d8a9bbf013ab", + "TopicArn": "arn:aws:sns:eu-central-1:123456789012:TopicSendToMe", + "Subject": "Test sns message", + "Message": "{\n \"id\": 42,\n \"name\": \"Bob\"\n}", + "Timestamp": "2020-10-08T16:06:14.656Z", + "SignatureVersion": "1", + "Signature": "UWnPpkqPAphyr+6PXzUF9++4zJcw==", + "SigningCertUrl": "https://sns.eu-central-1.amazonaws.com/SimpleNotificationService-a86cb10b4e1f29c941702d737128f7b6.pem", + "UnsubscribeUrl": "https://sns.eu-central-1.amazonaws.com/?Action=Unsubscribe", + "MessageAttributes": { + "name": { + "Type": "String", + "Value": "Bob" + } + } + } + } + ] +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/sqs/sqs_event_nobody.json b/aws-lambda-java-tests/src/test/resources/sqs/sqs_event_nobody.json new file mode 100644 index 000000000..bff825bd1 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/sqs/sqs_event_nobody.json @@ -0,0 +1,22 @@ +{ + "Records": [ + { + "messageId": "d9144555-9a4f-4ec3-99a0-fc4e625a8db2", + "receiptHandle": "7kam5bfzbDsjtcjElvhSbxeLJbeey3A==", + "body": "", + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1601975709495", + "SenderId": "AROAIFU457DVZ5L2J53F2", + "ApproximateFirstReceiveTimestamp": "1601975709499" + }, + "messageAttributes": { + + }, + "md5OfBody": "0f96e88a291edb4429f2f7b9fdc3df96", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:eu-central-1:123456789012:TestLambda", + "awsRegion": "eu-central-1" + } + ] +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/sqs/sqs_event_product.json b/aws-lambda-java-tests/src/test/resources/sqs/sqs_event_product.json new file mode 100644 index 000000000..d97bf2c9f --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/sqs/sqs_event_product.json @@ -0,0 +1,22 @@ +{ + "Records": [ + { + "messageId": "d9144555-9a4f-4ec3-99a0-34ce359b4b54", + "receiptHandle": "13e7f7851d2eaa5c01f208ebadbf1e72==", + "body": "{\n \"id\": 1234,\n \"name\": \"Product1234\",\n \"price\": 450\n}", + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1601975706495", + "SenderId": "AROAIFU437PVZ5L2J53F5", + "ApproximateFirstReceiveTimestamp": "1601975706499" + }, + "messageAttributes": { + + }, + "md5OfBody": "13e7f7851d2eaa5c01f208ebadbf1e72", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:eu-central-1:123456789012:TestLambda", + "awsRegion": "eu-central-1" + } + ] +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/time_window_event_response.json b/aws-lambda-java-tests/src/test/resources/time_window_event_response.json new file mode 100644 index 000000000..3c77b3784 --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/time_window_event_response.json @@ -0,0 +1,10 @@ +{ + "state": { + "totalAmount": "500" + }, + "batchItemFailures": [ + { + "itemIdentifier": "49590338271490256608559692538361571095921575989136588898" + } + ] +} \ No newline at end of file diff --git a/aws-lambda-java-tests/src/test/resources/unstable_pojo.json b/aws-lambda-java-tests/src/test/resources/unstable_pojo.json new file mode 100644 index 000000000..19db9a1cc --- /dev/null +++ b/aws-lambda-java-tests/src/test/resources/unstable_pojo.json @@ -0,0 +1,3 @@ +{ + "name": "test" +} \ No newline at end of file diff --git a/experimental/aws-lambda-java-profiler/.gitignore b/experimental/aws-lambda-java-profiler/.gitignore new file mode 100644 index 000000000..4c3fb86d5 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/.gitignore @@ -0,0 +1,3 @@ +*.zip +/.idea/ +/target/ diff --git a/experimental/aws-lambda-java-profiler/.mvn/wrapper/maven-wrapper.properties b/experimental/aws-lambda-java-profiler/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 000000000..48a56c99a --- /dev/null +++ b/experimental/aws-lambda-java-profiler/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip diff --git a/experimental/aws-lambda-java-profiler/README.md b/experimental/aws-lambda-java-profiler/README.md new file mode 100644 index 000000000..c15c22791 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/README.md @@ -0,0 +1,133 @@ +

+ AWS Lambda service icon +

+ +

AWS Lambda Profiler Extension for Java

+ +The Lambda profiler extension allows you to profile your Java functions invoke by invoke, with high fidelity, and no +code changes. It uses the [async-profiler](https://github.com/async-profiler/async-profiler) project to produce +profiling data and automatically uploads the data as HTML flame graphs to S3. + +

+ A flame graph of a Java Lambda function +

+ +## Current status +**This is an alpha release and not yet ready for production use.** We're especially interested in early feedback on usability, features, performance, and compatibility. Please send feedback by opening a [GitHub issue](https://github.com/aws/aws-lambda-java-libs/issues/new). + +The profiler has been tested with Lambda managed runtimes for Java 17 and Java 21. + +## How to use the Lambda Profiler + +To use the profiler you need to + +1. Build the extension in this repo +2. Deploy it as a Lambda Layer and attach the layer to your function +3. Create an S3 bucket for the results, or reuse an existing one +4. Give your function permission to write to the bucket +5. Configure the required environment variables. + +The above assumes you're using the ZIP deployment method with managed runtimes. If you deploy your functions as container images instead, you will need to include the profiler in your Dockerfile at `/opt/extensions/` rather than using a Lambda layer. + +### Quick Start + +The following [Quick Start](#quick-start) gives AWS CLI commands you can run to get started (MacOS/Linux). There are also [examples](examples) using infrastructure as code for you to refer to. + +1. Clone the repo + + ```bash + git clone https://github.com/aws/aws-lambda-java-libs + ``` + +2. Build the extension + + ```bash + cd aws-lambda-java-libs/experimental/aws-lambda-java-profiler/extension + ./build_layer.sh + ``` + +3. Run the `update-function.sh` script which will create a new S3 bucket, Lambda layer and all the configuration required. + + ```bash + cd .. + ./update-function.sh YOUR_FUNCTION_NAME + ``` + +4. Invoke your function and review the flame graph in S3 using your browser. + +### Configuration + +#### Required Environment Variables + +| Name | Value | +|-----------------------------------------|-----------------------------------------------------------------------------------------------| +| AWS_LAMBDA_PROFILER_RESULTS_BUCKET_NAME | Your unique bucket name | +| JAVA_TOOL_OPTIONS | -XX:+UnlockDiagnosticVMOptions -XX:+DebugNonSafepoints -javaagent:/opt/profiler-extension.jar | + +#### Optional Environment Variables + +| Name | Default Value | Options | +|------------------------------------------|-----------------------------------------------------------|--------------------------------| +| AWS_LAMBDA_PROFILER_START_COMMAND | start,event=wall,interval=1us | | +| AWS_LAMBDA_PROFILER_STOP_COMMAND | stop,file=%s,include=*AWSLambda.main,include=start_thread | file=%s is required | +| AWS_LAMBDA_PROFILER_DEBUG | false | true - to enable debug logging | +| AWS_LAMBDA_PROFILER_COMMUNICATION_PORT | 1234 | a valid port number | + +### How does it work? + +In `/src` is the code for a Java agent. It's entry point `AgentEntry.premain()` is executed as the runtime starts up. +The environment variable `JAVA_TOOL_OPTIONS` is used to specify which `.jar` file the agent is in. The `MANIFEST.MF` file is used to specify the pre-main class. + +When the agent is constructed, it starts the profiler and registers itself as a Lambda extension for `INVOKE` request. + +A new thread is created to handle calling `/next` and uploading the results of the profiler to S3. The bucket to upload +the result to is configurable using an environment variable. + +### Custom Parameters for the Profiler + +Users can configure the profiler output by setting environment variables. + +``` +# Example: Output as JFR format instead of HTML +AWS_LAMBDA_PROFILER_START_COMMAND="start,event=wall,interval=1us,file=/tmp/profile.jfr" +AWS_LAMBDA_PROFILER_STOP_COMMAND="stop,file=%s" +``` + +Defaults are the following: + +``` +AWS_LAMBDA_PROFILER_START_COMMAND="start,event=wall,interval=1us" +AWS_LAMBDA_PROFILER_STOP_COMMAND="stop,file=%s,include=*AWSLambda.main,include=start_thread" +``` + +See [async-profiler's ProfilerOptions](https://github.com/async-profiler/async-profiler/blob/master/docs/ProfilerOptions.md) for all available profiler parameters. + +### Troubleshooting + +- Ensure the Lambda function execution role has the necessary permissions to write to the S3 bucket. +- Verify that the environment variables are set correctly in your Lambda function configuration. +- Check CloudWatch logs for any error messages from the extension. +- The profiler extension uses dependencies such as `com.amazonaws:aws-lambda-java-core`, `com.amazonaws:aws-lambda-java-events` and `software.amazon.awssdk:s3`. If you're using the same dependencies in your Lambda function, make sure that the versions match those used by the extension as mismatched versions can lead to compatibility issues. + +## Contributing + +Contributions to improve the Java profiler extension are welcome. Please see [CONTRIBUTING.md](../../CONTRIBUTING.md) for more information on how to report bugs or submit pull requests. + +Issues or contributions to the [async-profiler](https://github.com/async-profiler/async-profiler) itself should be submitted to that project. + +### Security + +If you discover a potential security issue in this project we ask that you notify AWS Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public GitHub issue. + +### Code of conduct + +This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). See [CODE_OF_CONDUCT.md](doc/CODE_OF_CONDUCT.md) for more details. + +## License + +This project is licensed under the [Apache 2.0](../../LICENSE) License. It uses the following projects: + +- [async-profiler](https://github.com/async-profiler/async-profiler) (Apache 2.0 license) +- [AWS SDK for Java 2.0](https://github.com/aws/aws-sdk-java-v2) (Apache 2.0 license) +- Other libraries in this repository (Apache 2.0 license) + diff --git a/experimental/aws-lambda-java-profiler/RELEASE.CHANGELOG.md b/experimental/aws-lambda-java-profiler/RELEASE.CHANGELOG.md new file mode 100644 index 000000000..f2f14ae48 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/RELEASE.CHANGELOG.md @@ -0,0 +1,7 @@ +### March 31, 2025 +`0.1.1` [link to tag](https://github.com/aws/aws-lambda-java-libs/releases/tag/profiler-extension-0.1.1) +- fix: use PROFILER_STOP_COMMAND in Shutdown hooks ([#537](https://github.com/aws/aws-lambda-java-libs/pull/537)) + +### March 18, 2025 +`0.1.0` [link to tag](https://github.com/aws/aws-lambda-java-libs/releases/tag/profiler-extension-0.1.0) +- Initial release \ No newline at end of file diff --git a/experimental/aws-lambda-java-profiler/docs/Arch_AWS-Lambda_64.svg b/experimental/aws-lambda-java-profiler/docs/Arch_AWS-Lambda_64.svg new file mode 100644 index 000000000..496ef0e72 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/docs/Arch_AWS-Lambda_64.svg @@ -0,0 +1,18 @@ + + + + Icon-Architecture/64/Arch_AWS-Lambda_64 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/experimental/aws-lambda-java-profiler/docs/example-cold-start-flame-graph-small.png b/experimental/aws-lambda-java-profiler/docs/example-cold-start-flame-graph-small.png new file mode 100644 index 000000000..81ae8cba3 Binary files /dev/null and b/experimental/aws-lambda-java-profiler/docs/example-cold-start-flame-graph-small.png differ diff --git a/experimental/aws-lambda-java-profiler/docs/example-cold-start-flame-graph.png b/experimental/aws-lambda-java-profiler/docs/example-cold-start-flame-graph.png new file mode 100644 index 000000000..26d11c310 Binary files /dev/null and b/experimental/aws-lambda-java-profiler/docs/example-cold-start-flame-graph.png differ diff --git a/experimental/aws-lambda-java-profiler/examples/cdk/.gitignore b/experimental/aws-lambda-java-profiler/examples/cdk/.gitignore new file mode 100644 index 000000000..1db21f162 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/examples/cdk/.gitignore @@ -0,0 +1,13 @@ +.classpath.txt +target +.classpath +.project +.idea +.settings +.vscode +*.iml + +# CDK asset staging directory +.cdk.staging +cdk.out + diff --git a/experimental/aws-lambda-java-profiler/examples/cdk/README.md b/experimental/aws-lambda-java-profiler/examples/cdk/README.md new file mode 100644 index 000000000..516ef71a2 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/examples/cdk/README.md @@ -0,0 +1,18 @@ +# Welcome to your CDK Java project! + +This is a blank project for CDK development with Java. + +The `cdk.json` file tells the CDK Toolkit how to execute your app. + +It is a [Maven](https://maven.apache.org/) based project, so you can open this project with any Maven compatible Java IDE to build and run tests. + +## Useful commands + + * `mvn package` compile and run tests + * `cdk ls` list all stacks in the app + * `cdk synth` emits the synthesized CloudFormation template + * `cdk deploy` deploy this stack to your default AWS account/region + * `cdk diff` compare deployed stack with current state + * `cdk docs` open CDK documentation + +Enjoy! diff --git a/experimental/aws-lambda-java-profiler/examples/cdk/cdk.json b/experimental/aws-lambda-java-profiler/examples/cdk/cdk.json new file mode 100644 index 000000000..e94ff8512 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/examples/cdk/cdk.json @@ -0,0 +1,68 @@ +{ + "app": "mvn -e -q compile exec:java", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "target", + "pom.xml", + "src/test" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": [ + "aws", + "aws-cn" + ], + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, + "@aws-cdk/aws-route53-patters:useCertificate": true, + "@aws-cdk/customresources:installLatestAwsSdkDefault": false, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, + "@aws-cdk/aws-redshift:columnId": true, + "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, + "@aws-cdk/aws-kms:aliasNameRef": true, + "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, + "@aws-cdk/aws-efs:denyAnonymousAccess": true, + "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, + "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, + "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, + "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, + "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, + "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, + "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, + "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, + "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, + "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, + "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, + "@aws-cdk/aws-eks:nodegroupNameAttribute": true, + "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, + "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, + "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false, + "@aws-cdk/aws-s3:keepNotificationInImportedBucket": false + } +} diff --git a/experimental/aws-lambda-java-profiler/examples/cdk/pom.xml b/experimental/aws-lambda-java-profiler/examples/cdk/pom.xml new file mode 100644 index 000000000..4b46f4e2b --- /dev/null +++ b/experimental/aws-lambda-java-profiler/examples/cdk/pom.xml @@ -0,0 +1,59 @@ + + + 4.0.0 + + com.myorg + example-cdk-profiler-layer + 0.1 + + + UTF-8 + 2.155.0 + [10.0.0,11.0.0) + 5.12.2 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 17 + + + + + org.codehaus.mojo + exec-maven-plugin + 3.1.0 + + com.myorg.InfraApp + + + + + + + + software.amazon.awscdk + aws-cdk-lib + ${cdk.version} + + + + software.constructs + constructs + ${constructs.version} + + + + org.junit.jupiter + junit-jupiter + ${junit.version} + test + + + diff --git a/experimental/aws-lambda-java-profiler/examples/cdk/src/main/java/com/myorg/InfraApp.java b/experimental/aws-lambda-java-profiler/examples/cdk/src/main/java/com/myorg/InfraApp.java new file mode 100644 index 000000000..1232c1b8b --- /dev/null +++ b/experimental/aws-lambda-java-profiler/examples/cdk/src/main/java/com/myorg/InfraApp.java @@ -0,0 +1,42 @@ +package com.myorg; + +import software.amazon.awscdk.App; +import software.amazon.awscdk.Environment; +import software.amazon.awscdk.StackProps; + +import java.util.Arrays; + +public class InfraApp { + public static void main(final String[] args) { + App app = new App(); + + new InfraStack(app, "InfraStack", StackProps.builder() + // If you don't specify 'env', this stack will be environment-agnostic. + // Account/Region-dependent features and context lookups will not work, + // but a single synthesized template can be deployed anywhere. + + // Uncomment the next block to specialize this stack for the AWS Account + // and Region that are implied by the current CLI configuration. + /* + .env(Environment.builder() + .account(System.getenv("CDK_DEFAULT_ACCOUNT")) + .region(System.getenv("CDK_DEFAULT_REGION")) + .build()) + */ + + // Uncomment the next block if you know exactly what Account and Region you + // want to deploy the stack to. + /* + .env(Environment.builder() + .account("123456789012") + .region("us-east-1") + .build()) + */ + + // For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html + .build()); + + app.synth(); + } +} + diff --git a/experimental/aws-lambda-java-profiler/examples/cdk/src/main/java/com/myorg/InfraStack.java b/experimental/aws-lambda-java-profiler/examples/cdk/src/main/java/com/myorg/InfraStack.java new file mode 100644 index 000000000..79773e39e --- /dev/null +++ b/experimental/aws-lambda-java-profiler/examples/cdk/src/main/java/com/myorg/InfraStack.java @@ -0,0 +1,53 @@ +package com.myorg; + +import software.amazon.awscdk.Duration; +import software.amazon.awscdk.services.lambda.Code; +import software.amazon.awscdk.services.lambda.Function; +import software.amazon.awscdk.services.lambda.LayerVersion; +import software.amazon.awscdk.services.s3.Bucket; +import software.constructs.Construct; +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static software.amazon.awscdk.services.lambda.Architecture.*; +import static software.amazon.awscdk.services.lambda.Runtime.*; + +public class InfraStack extends Stack { + public InfraStack(final Construct scope, final String id) { + this(scope, id, null); + } + + public InfraStack(final Construct scope, final String id, final StackProps props) { + super(scope, id, props); + + var resultsBucketName = UUID.randomUUID().toString(); + var resultsBucket = Bucket.Builder.create(this, "profiler-results-bucket") + .bucketName(resultsBucketName) + .build(); + + var layerVersion = LayerVersion.Builder.create(this, "async-profiler-layer") + .compatibleArchitectures(List.of(ARM_64, X86_64)) + .compatibleRuntimes(List.of(JAVA_11, JAVA_17, JAVA_21)) + .code(Code.fromAsset("../../target/extension.zip")) + .build(); + + var environmentVariables = Map.of("JAVA_TOOL_OPTIONS", "-XX:+UnlockDiagnosticVMOptions -XX:+DebugNonSafepoints -javaagent:/opt/profiler.jar", + "AWS_LAMBDA_PROFILER_RESULTS_BUCKET_NAME", resultsBucketName); + + var function = Function.Builder.create(this, "example-profiler-function") + .runtime(JAVA_21) + .handler("helloworld.App") + .code(Code.fromAsset("../function/profiling-example/target/Helloworld-1.0.jar")) + .memorySize(2048) + .layers(List.of(layerVersion)) + .environment(environmentVariables) + .timeout(Duration.seconds(30)) + .build(); + + resultsBucket.grantPut(function); + } +} diff --git a/experimental/aws-lambda-java-profiler/examples/function/profiling-example/pom.xml b/experimental/aws-lambda-java-profiler/examples/function/profiling-example/pom.xml new file mode 100644 index 000000000..ac1001009 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/examples/function/profiling-example/pom.xml @@ -0,0 +1,63 @@ + + 4.0.0 + helloworld + HelloWorld + 1.0 + jar + A sample Hello World created for SAM CLI. + + 21 + 21 + + + + + com.amazonaws + aws-lambda-java-core + 1.2.2 + + + com.amazonaws + aws-lambda-java-events + 3.11.0 + + + com.hkupty.penna + penna-core + 0.8.0 + + + org.slf4j + slf4j-api + 2.0.13 + + + + junit + junit + 4.13.2 + test + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.1 + + + + + package + + shade + + + + + + + diff --git a/experimental/aws-lambda-java-profiler/examples/function/profiling-example/src/main/java/helloworld/App.java b/experimental/aws-lambda-java-profiler/examples/function/profiling-example/src/main/java/helloworld/App.java new file mode 100644 index 000000000..c58f55a1f --- /dev/null +++ b/experimental/aws-lambda-java-profiler/examples/function/profiling-example/src/main/java/helloworld/App.java @@ -0,0 +1,53 @@ +package helloworld; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Handler for requests to Lambda function. + */ +public class App implements RequestHandler { + + private static Logger logger = LoggerFactory.getLogger(App.class); + + public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { + Map headers = new HashMap<>(); + headers.put("Content-Type", "application/json"); + headers.put("X-Custom-Header", "application/json"); + + APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent() + .withHeaders(headers); + try { + final String pageContents = this.getPageContents("https://checkip.amazonaws.com"); + String output = String.format("{ \"message\": \"hello world\", \"location\": \"%s\" }", pageContents); + logger.info(output); + + return response + .withStatusCode(200) + .withBody(output); + } catch (IOException e) { + return response + .withBody("{}") + .withStatusCode(500); + } + } + + private String getPageContents(String address) throws IOException{ + URL url = new URL(address); + try(BufferedReader br = new BufferedReader(new InputStreamReader(url.openStream()))) { + return br.lines().collect(Collectors.joining(System.lineSeparator())); + } + } +} diff --git a/experimental/aws-lambda-java-profiler/examples/function/profiling-example/src/test/java/helloworld/AppTest.java b/experimental/aws-lambda-java-profiler/examples/function/profiling-example/src/test/java/helloworld/AppTest.java new file mode 100644 index 000000000..240323bb7 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/examples/function/profiling-example/src/test/java/helloworld/AppTest.java @@ -0,0 +1,22 @@ +package helloworld; + +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import org.junit.Test; + +public class AppTest { + @Test + public void successfulResponse() { + App app = new App(); + APIGatewayProxyResponseEvent result = app.handleRequest(null, null); + assertEquals(200, result.getStatusCode().intValue()); + assertEquals("application/json", result.getHeaders().get("Content-Type")); + String content = result.getBody(); + assertNotNull(content); + assertTrue(content.contains("\"message\"")); + assertTrue(content.contains("\"hello world\"")); + assertTrue(content.contains("\"location\"")); + } +} diff --git a/experimental/aws-lambda-java-profiler/extension/build.gradle b/experimental/aws-lambda-java-profiler/extension/build.gradle new file mode 100644 index 000000000..387bb3528 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/extension/build.gradle @@ -0,0 +1,34 @@ +plugins { + id 'java' + id "com.gradleup.shadow" version "8.3.3" +} + +repositories { + mavenCentral() +} + +sourceCompatibility = 11 +targetCompatibility = 11 + +dependencies { + implementation 'com.amazonaws:aws-lambda-java-core:1.2.3' + implementation 'com.amazonaws:aws-lambda-java-events:3.11.5' + implementation("tools.profiler:async-profiler:3.0") + implementation("software.amazon.awssdk:s3:2.31.2") { + exclude group: 'software.amazon.awssdk', module: 'netty-nio-client' + } +} + +jar { + manifest { + attributes 'Main-Class': 'com.amazonaws.services.lambda.extension.ExtensionMain' + attributes 'Premain-Class': 'com.amazonaws.services.lambda.extension.PreMain' + attributes 'Can-Redefine-Class': true + } +} + +shadowJar { + archiveFileName = "profiler-extension.jar" +} + +build.dependsOn jar diff --git a/experimental/aws-lambda-java-profiler/extension/build_layer.sh b/experimental/aws-lambda-java-profiler/extension/build_layer.sh new file mode 100755 index 000000000..cfb381cff --- /dev/null +++ b/experimental/aws-lambda-java-profiler/extension/build_layer.sh @@ -0,0 +1,13 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 + +./gradlew :shadowJar + +chmod +x extensions/profiler-extension +archive="extension.zip" +if [ -f "$archive" ] ; then + rm "$archive" +fi + +zip "$archive" -j build/libs/profiler-extension.jar +zip "$archive" extensions/* \ No newline at end of file diff --git a/experimental/aws-lambda-java-profiler/extension/extensions/profiler-extension b/experimental/aws-lambda-java-profiler/extension/extensions/profiler-extension new file mode 100755 index 000000000..ef9a5e47c --- /dev/null +++ b/experimental/aws-lambda-java-profiler/extension/extensions/profiler-extension @@ -0,0 +1,6 @@ +#!/bin/bash +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 + +set -euo pipefail +exec -- java -jar /opt/profiler-extension.jar \ No newline at end of file diff --git a/experimental/aws-lambda-java-profiler/extension/gradle/wrapper/gradle-wrapper.jar b/experimental/aws-lambda-java-profiler/extension/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..a4b76b953 Binary files /dev/null and b/experimental/aws-lambda-java-profiler/extension/gradle/wrapper/gradle-wrapper.jar differ diff --git a/experimental/aws-lambda-java-profiler/extension/gradle/wrapper/gradle-wrapper.properties b/experimental/aws-lambda-java-profiler/extension/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..df97d72b8 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/extension/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/experimental/aws-lambda-java-profiler/extension/gradlew b/experimental/aws-lambda-java-profiler/extension/gradlew new file mode 100755 index 000000000..f5feea6d6 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/extension/gradlew @@ -0,0 +1,252 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/experimental/aws-lambda-java-profiler/extension/gradlew.bat b/experimental/aws-lambda-java-profiler/extension/gradlew.bat new file mode 100644 index 000000000..9b42019c7 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/extension/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/Constants.java b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/Constants.java new file mode 100644 index 000000000..f9ca3010c --- /dev/null +++ b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/Constants.java @@ -0,0 +1,29 @@ +package com.amazonaws.services.lambda.extension; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class Constants { + + private static final String DEFAULT_AWS_LAMBDA_PROFILER_START_COMMAND = + "start,event=wall,interval=1us"; + private static final String DEFAULT_AWS_LAMBDA_PROFILER_STOP_COMMAND = + "stop,file=%s,include=*AWSLambda.main,include=start_thread"; + public static final String PROFILER_START_COMMAND = + System.getenv().getOrDefault( + "AWS_LAMBDA_PROFILER_START_COMMAND", + DEFAULT_AWS_LAMBDA_PROFILER_START_COMMAND + ); + public static final String PROFILER_STOP_COMMAND = + System.getenv().getOrDefault( + "AWS_LAMBDA_PROFILER_STOP_COMMAND", + DEFAULT_AWS_LAMBDA_PROFILER_STOP_COMMAND + ); + + public static String getFilePathFromEnv(){ + Pattern pattern = Pattern.compile("file=([^,]+)"); + Matcher matcher = pattern.matcher(PROFILER_START_COMMAND); + + return matcher.find() ? matcher.group(1) : "/tmp/profiling-data-%s.html"; + } +} diff --git a/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/ExtensionClient.java b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/ExtensionClient.java new file mode 100644 index 000000000..60c13a811 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/ExtensionClient.java @@ -0,0 +1,73 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 +package com.amazonaws.services.lambda.extension; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.Optional; + +/** + * Utility class that takes care of registration of extension, fetching the next event, initializing + * and exiting with error + */ +public class ExtensionClient { + private static final String EXTENSION_NAME = "profiler-extension"; + private static final String BASEURL = String + .format("http://%s/2020-01-01/extension", System.getenv("AWS_LAMBDA_RUNTIME_API")); + private static final String BODY = "{" + + " \"events\": [" + + " \"INVOKE\"," + + " \"SHUTDOWN\"" + + " ]" + + " }"; + private static final String LAMBDA_EXTENSION_IDENTIFIER = "Lambda-Extension-Identifier"; + private static final HttpClient client = HttpClient.newBuilder().build(); + + public static String registerExtension() { + final String registerUrl = String.format("%s/register", BASEURL); + HttpRequest request = HttpRequest.newBuilder() + .POST(HttpRequest.BodyPublishers.ofString(BODY)) + .header("Content-Type", "application/json") + .header("Lambda-Extension-Name", EXTENSION_NAME) + .uri(URI.create(registerUrl)) + .build(); + try { + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + // Get extension ID from the response headers + Optional lambdaExtensionHeader = response.headers().firstValue("lambda-extension-identifier"); + if (lambdaExtensionHeader.isPresent()) { + return lambdaExtensionHeader.get(); + } + } + catch (Exception e) { + Logger.error("could not register the extension"); + e.printStackTrace(); + } + throw new RuntimeException("Error while registering extension"); + } + + public static String getNext(final String extensionId) { + try { + final String nextEventUrl = String.format("%s/event/next", BASEURL); + HttpRequest request = HttpRequest.newBuilder() + .GET() + .header(LAMBDA_EXTENSION_IDENTIFIER, extensionId) + .uri(URI.create(nextEventUrl)) + .build(); + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() == 200) { + return response.body(); + } else { + Logger.error("invalid status code returned while processing event = " + response.statusCode()); + } + } + catch (Exception e) { + Logger.error("could not get /next event"); + e.printStackTrace(); + } + + return null; + } +} diff --git a/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/ExtensionMain.java b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/ExtensionMain.java new file mode 100644 index 000000000..18115a9fd --- /dev/null +++ b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/ExtensionMain.java @@ -0,0 +1,136 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 +package com.amazonaws.services.lambda.extension; + +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.URI; +import java.util.UUID; + +public class ExtensionMain { + + private static final HttpClient client = HttpClient.newBuilder().build(); + private static String previousFileSuffix = null; + private static boolean coldstart = true; + private static final String REQUEST_ID = "requestId"; + private static final String EVENT_TYPE = "eventType"; + private static final String INTERNAL_COMMUNICATION_PORT = System.getenv().getOrDefault("AWS_LAMBDA_PROFILER_COMMUNICATION_PORT", "1234"); + public static final String HEADER_NAME = "X-FileName"; + + private static S3Manager s3Manager; + + public static void main(String[] args) { + final String extension = ExtensionClient.registerExtension(); + Logger.debug("Extension registration complete, extensionID: " + extension); + s3Manager = new S3Manager(); + while (true) { + try { + String response = ExtensionClient.getNext(extension); + if (response != null && !response.isEmpty()) { + final String eventType = extractInfo(EVENT_TYPE, response); + Logger.debug("eventType = " + eventType); + if (eventType != null) { + switch (eventType) { + case "INVOKE": + handleInvoke(response); + break; + case "SHUTDOWN": + handleShutDown(); + break; + default: + Logger.error("invalid event type received " + eventType); + } + } + } + } catch (Exception e) { + Logger.error("error while processing extension -" + e.getMessage()); + e.printStackTrace(); + } + } + } + + private static void handleShutDown() { + Logger.debug("handling SHUTDOWN event, flushing the last profile"); + try { + // no need to stop the profiler as it has been stopped by the shutdown hook + s3Manager.upload(previousFileSuffix, true); + } catch (Exception e) { + Logger.error("could not upload the file"); + throw e; + } + System.exit(0); + } + + public static void handleInvoke(String payload) { + final String requestId = extractInfo(REQUEST_ID, payload); + final String randomSuffix = UUID.randomUUID().toString().substring(0,5); + Logger.debug("handling INVOKE event, requestID = " + requestId); + if (!coldstart) { + try { + stopProfiler(previousFileSuffix); + s3Manager.upload(previousFileSuffix, false); + startProfiler(); + } catch (Exception e) { + Logger.error("could not start the profiler"); + throw e; + } + } + coldstart = false; + previousFileSuffix = extractInfo(REQUEST_ID, payload) + "-" + randomSuffix; + } + + private static String extractInfo(String info, String jsonString) { + String prefix = "\"" + info + "\":\""; + String suffix = "\""; + + int startIndex = jsonString.indexOf(prefix); + if (startIndex == -1) { + return null; // requestId not found + } + + startIndex += prefix.length(); + int endIndex = jsonString.indexOf(suffix, startIndex); + + if (endIndex == -1) { + return null; // Malformed JSON + } + + return jsonString.substring(startIndex, endIndex); + } + + private static void startProfiler() { + try { + String url = String.format("http://localhost:%s/profiler/start", INTERNAL_COMMUNICATION_PORT); + HttpRequest request = HttpRequest.newBuilder() + .GET() + .uri(URI.create(url)) + .build(); + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() == 200) { + Logger.debug("profiler successfully started"); + } + } catch(Exception e) { + Logger.error("could not start the profiler"); + e.printStackTrace(); + } + } + + private static void stopProfiler(String fileNameSuffix) { + try { + String url = String.format("http://localhost:%s/profiler/stop", INTERNAL_COMMUNICATION_PORT); + HttpRequest request = HttpRequest.newBuilder() + .GET() + .setHeader(HEADER_NAME, fileNameSuffix) + .uri(URI.create(url)) + .build(); + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() == 200) { + Logger.debug("profiler successfully stopped"); + } + } catch(Exception e) { + Logger.error("could not stop the profiler"); + e.printStackTrace(); + } + } +} diff --git a/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/Logger.java b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/Logger.java new file mode 100644 index 000000000..e064da101 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/Logger.java @@ -0,0 +1,25 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 +package com.amazonaws.services.lambda.extension; + +public class Logger { + + private static final boolean IS_DEBUG_ENABLED = initializeDebugFlag(); + private static final String PREFIX = "[PROFILER] "; + + private static boolean initializeDebugFlag() { + String envValue = System.getenv("AWS_LAMBDA_PROFILER_DEBUG"); + return "true".equalsIgnoreCase(envValue) || "1".equals(envValue); + } + + public static void debug(String message) { + if(IS_DEBUG_ENABLED) { + System.out.println(PREFIX + message); + } + } + + public static void error(String message) { + System.out.println(PREFIX + message); + } + +} \ No newline at end of file diff --git a/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/PreMain.java b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/PreMain.java new file mode 100644 index 000000000..2a84eb641 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/PreMain.java @@ -0,0 +1,131 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 +package com.amazonaws.services.lambda.extension; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.lang.instrument.Instrumentation; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import one.profiler.AsyncProfiler; + +import static com.amazonaws.services.lambda.extension.Constants.PROFILER_START_COMMAND; +import static com.amazonaws.services.lambda.extension.Constants.PROFILER_STOP_COMMAND; + +public class PreMain { + + + private static final String INTERNAL_COMMUNICATION_PORT = + System.getenv().getOrDefault( + "AWS_LAMBDA_PROFILER_COMMUNICATION_PORT", + "1234" + ); + + + private String filepath; + + public static void premain(String agentArgs, Instrumentation inst) { + Logger.debug("premain is starting"); + if (!createFileIfNotExist("/tmp/aws-lambda-java-profiler")) { + Logger.debug("starting the profiler for coldstart"); + startProfiler(); + registerShutdownHook(); + try { + Integer port = Integer.parseInt(INTERNAL_COMMUNICATION_PORT); + Logger.debug("using profile communication port = " + port); + HttpServer server = HttpServer.create( + new InetSocketAddress(port), + 0 + ); + server.createContext("/profiler/start", new StartProfiler()); + server.createContext("/profiler/stop", new StopProfiler()); + server.setExecutor(null); // Use the default executor + server.start(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + private static boolean createFileIfNotExist(String filePath) { + File file = new File(filePath); + try { + return file.createNewFile(); + } catch (IOException e) { + System.out.println(e); + return false; + } + } + + public static class StopProfiler implements HttpHandler { + + @Override + public void handle(HttpExchange exchange) throws IOException { + Logger.debug("hit /profiler/stop"); + final String fileName = exchange + .getRequestHeaders() + .getFirst(ExtensionMain.HEADER_NAME); + stopProfiler(fileName); + String response = "ok"; + exchange.sendResponseHeaders(200, response.length()); + try (OutputStream os = exchange.getResponseBody()) { + os.write(response.getBytes(StandardCharsets.UTF_8)); + } + } + } + + public static class StartProfiler implements HttpHandler { + + @Override + public void handle(HttpExchange exchange) throws IOException { + Logger.debug("hit /profiler/start"); + startProfiler(); + String response = "ok"; + exchange.sendResponseHeaders(200, response.length()); + try (OutputStream os = exchange.getResponseBody()) { + os.write(response.getBytes(StandardCharsets.UTF_8)); + } + } + } + + public static void stopProfiler(String fileNameSuffix) { + try { + final String fileName = String.format( + Constants.getFilePathFromEnv(), + fileNameSuffix + ); + Logger.debug( + "stopping the profiler with filename = " + fileName + ); + AsyncProfiler.getInstance().execute( + String.format(PROFILER_STOP_COMMAND, fileName) + ); + } catch (Exception e) { + Logger.error("could not stop the profiler"); + e.printStackTrace(); + } + } + + public static void startProfiler() { + try { + Logger.debug( + "starting the profiler with command = " + PROFILER_START_COMMAND + ); + AsyncProfiler.getInstance().execute(PROFILER_START_COMMAND); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static void registerShutdownHook() { + Logger.debug("registering shutdown hook wit command = " + PROFILER_STOP_COMMAND); + Thread shutdownHook = new Thread( + new ShutdownHook(PROFILER_STOP_COMMAND) + ); + Runtime.getRuntime().addShutdownHook(shutdownHook); + } +} diff --git a/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/S3Manager.java b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/S3Manager.java new file mode 100644 index 000000000..0e31a2421 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/S3Manager.java @@ -0,0 +1,66 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 +package com.amazonaws.services.lambda.extension; + +import java.io.File; +import java.time.format.DateTimeFormatter; +import java.time.LocalDate; + +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.awssdk.services.s3.model.PutObjectResponse; + +public class S3Manager { + + private static final String RESULTS_BUCKET = "AWS_LAMBDA_PROFILER_RESULTS_BUCKET_NAME"; + private static final String FUNCTION_NAME = System.getenv().getOrDefault("AWS_LAMBDA_FUNCTION_NAME", "function"); + private S3Client s3Client; + private String bucketName; + + public S3Manager() { + final String bucketName = System.getenv(RESULTS_BUCKET); + Logger.debug("creating S3Manager with bucketName = " + bucketName); + if (null == bucketName || bucketName.isEmpty()) { + throw new IllegalArgumentException("please set the bucket name using AWS_LAMBDA_PROFILER_RESULTS_BUCKET_NAME environment variable"); + } + this.s3Client = S3Client.builder().build(); + this.bucketName = bucketName; + Logger.debug("S3Manager successfully created"); + } + + public void upload(String fileName, boolean isShutDownEvent) { + try { + final String suffix = isShutDownEvent ? "shutdown" : fileName; + final String key = buildKey(FUNCTION_NAME, fileName); + Logger.debug("uploading profile to key = " + key); + PutObjectRequest putObjectRequest = PutObjectRequest.builder() + .bucket(bucketName) + .key(key) + .build(); + File file = new File(String.format(Constants.getFilePathFromEnv(), suffix)); + if (file.exists()) { + Logger.debug("file size is " + file.length()); + RequestBody requestBody = RequestBody.fromFile(file); + PutObjectResponse response = s3Client.putObject(putObjectRequest, requestBody); + Logger.debug("profile uploaded successfully. ETag: " + response.eTag()); + if(file.delete()) { + Logger.debug("file deleted"); + } + } else { + throw new IllegalArgumentException("could not find the profile to upload"); + } + } catch (Exception e) { + Logger.error("could not upload the profile"); + e.printStackTrace(); + } + } + + private String buildKey(String functionName, String fileName) { + final LocalDate currentDate = LocalDate.now(); + final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd"); + final String formattedDate = currentDate.format(formatter); + return String.format("%s/%s/%s", formattedDate, functionName, fileName); + } + +} \ No newline at end of file diff --git a/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/ShutdownHook.java b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/ShutdownHook.java new file mode 100644 index 000000000..a36584bc1 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/extension/src/main/java/com/amazonaws/services/lambda/extension/ShutdownHook.java @@ -0,0 +1,26 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 +package com.amazonaws.services.lambda.extension; + +import one.profiler.AsyncProfiler; + +public class ShutdownHook implements Runnable { + + private String stopCommand; + + public ShutdownHook(String stopCommand) { + this.stopCommand = stopCommand; + } + + @Override + public void run() { + Logger.debug("running ShutdownHook"); + try { + final String fileName = "/tmp/profiling-data-shutdown.html"; + Logger.debug("stopping the profiler"); + AsyncProfiler.getInstance().execute(String.format(this.stopCommand, fileName)); + } catch (Exception e) { + Logger.error("could not stop the profiler"); + } + } +} \ No newline at end of file diff --git a/experimental/aws-lambda-java-profiler/integration_tests/cleanup.sh b/experimental/aws-lambda-java-profiler/integration_tests/cleanup.sh new file mode 100755 index 000000000..d58142a04 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/integration_tests/cleanup.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +# Set variables +LAYER_ARN=$(cat /tmp/layer_arn) +FUNCTION_NAME="aws-lambda-java-profiler-function-${GITHUB_RUN_ID}" +ROLE_NAME="aws-lambda-java-profiler-role-${GITHUB_RUN_ID}" + +# Function to check if a command was successful +check_success() { + if [ $? -eq 0 ]; then + echo "Success: $1" + else + echo "Error: Failed to $1" + exit 1 + fi +} + +# Delete Lambda Layer +echo "Deleting Lambda Layer..." +aws lambda delete-layer-version --layer-name $(echo $LAYER_ARN | cut -d: -f7) --version-number $(echo $LAYER_ARN | cut -d: -f8) +check_success "delete Lambda Layer" + +# Delete Lambda Function +echo "Deleting Lambda Function..." +aws lambda delete-function --function-name $FUNCTION_NAME +check_success "delete Lambda Function" + +# Delete IAM Role +echo "Deleting IAM Role..." +# First, detach all policies from the role +for policy in $(aws iam list-attached-role-policies --role-name $ROLE_NAME --query 'AttachedPolicies[*].PolicyArn' --output text); do + aws iam detach-role-policy --role-name $ROLE_NAME --policy-arn $policy + check_success "detach policy $policy from role $ROLE_NAME" +done + +# Remove s3 inline policy +aws iam delete-role-policy --role-name $ROLE_NAME --policy-name "s3PutObject" +check_success "deleted inline policy" + + +# Then delete the role +aws iam delete-role --role-name $ROLE_NAME +check_success "delete IAM Role" + +echo "All deletions completed successfully." \ No newline at end of file diff --git a/experimental/aws-lambda-java-profiler/integration_tests/create_bucket.sh b/experimental/aws-lambda-java-profiler/integration_tests/create_bucket.sh new file mode 100755 index 000000000..0ba50b732 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/integration_tests/create_bucket.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +PROFILER_RESULTS_BUCKET_NAME="aws-lambda-java-profiler-bucket-${GITHUB_RUN_ID}" + +# Create the S3 bucket +aws s3 mb s3://"$PROFILER_RESULTS_BUCKET_NAME" + +# Check if the bucket was created successfully +if [ $? -eq 0 ]; then + echo "Bucket '$PROFILER_RESULTS_BUCKET_NAME' created successfully." +else + echo "Error: Failed to create bucket '$PROFILER_RESULTS_BUCKET_NAME'." + exit 1 +fi \ No newline at end of file diff --git a/experimental/aws-lambda-java-profiler/integration_tests/create_function.sh b/experimental/aws-lambda-java-profiler/integration_tests/create_function.sh new file mode 100755 index 000000000..12ba1cb2b --- /dev/null +++ b/experimental/aws-lambda-java-profiler/integration_tests/create_function.sh @@ -0,0 +1,85 @@ +#!/bin/bash + +# Set variables +FUNCTION_NAME="aws-lambda-java-profiler-function-${GITHUB_RUN_ID}" +FUNCTION_NAME_CUSTOM_PROFILER_OPTIONS="aws-lambda-java-profiler-function-custom-${GITHUB_RUN_ID}" +ROLE_NAME="aws-lambda-java-profiler-role-${GITHUB_RUN_ID}" +HANDLER="helloworld.Handler::handleRequest" +RUNTIME="java21" +LAYER_ARN=$(cat /tmp/layer_arn) + +JAVA_TOOL_OPTIONS="-XX:+UnlockDiagnosticVMOptions -XX:+DebugNonSafepoints -javaagent:/opt/profiler-extension.jar" +AWS_LAMBDA_PROFILER_RESULTS_BUCKET_NAME="aws-lambda-java-profiler-bucket-${GITHUB_RUN_ID}" +AWS_LAMBDA_PROFILER_START_COMMAND="start,event=wall,interval=1us,file=/tmp/profile.jfr" +AWS_LAMBDA_PROFILER_STOP_COMMAND="stop,file=%s" + +# Compile the Hello World project +cd integration_tests/helloworld +gradle :buildZip +cd ../.. + +# Create IAM role for Lambda +ROLE_ARN=$(aws iam create-role \ + --role-name $ROLE_NAME \ + --assume-role-policy-document '{"Version": "2012-10-17","Statement": [{ "Effect": "Allow", "Principal": {"Service": "lambda.amazonaws.com"}, "Action": "sts:AssumeRole"}]}' \ + --query 'Role.Arn' \ + --output text) + +# Attach basic Lambda execution policy to the role +aws iam attach-role-policy \ + --role-name $ROLE_NAME \ + --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole + +# Attach s3:PutObject policy to the role so we can upload profiles +POLICY_DOCUMENT=$(cat < $new_filename" + else + echo "No change: $filename" + fi + fi +done + +echo "All files processed." \ No newline at end of file diff --git a/experimental/aws-lambda-java-profiler/integration_tests/helloworld/build.gradle b/experimental/aws-lambda-java-profiler/integration_tests/helloworld/build.gradle new file mode 100644 index 000000000..79ffa030a --- /dev/null +++ b/experimental/aws-lambda-java-profiler/integration_tests/helloworld/build.gradle @@ -0,0 +1,32 @@ +plugins { + id 'java' +} + +repositories { + mavenCentral() +} + +java { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 +} + +dependencies { + implementation ( + 'com.amazonaws:aws-lambda-java-core:1.2.3', + 'com.amazonaws:aws-lambda-java-events:3.11.0', + 'org.slf4j:slf4j-api:2.0.13' + ) +} + +task buildZip(type: Zip) { + archiveBaseName = "code" + from compileJava + from processResources + into('lib') { + from configurations.runtimeClasspath + } +} + + +build.dependsOn buildZip \ No newline at end of file diff --git a/experimental/aws-lambda-java-profiler/integration_tests/helloworld/src/main/java/helloworld/Handler.java b/experimental/aws-lambda-java-profiler/integration_tests/helloworld/src/main/java/helloworld/Handler.java new file mode 100644 index 000000000..a29cae18e --- /dev/null +++ b/experimental/aws-lambda-java-profiler/integration_tests/helloworld/src/main/java/helloworld/Handler.java @@ -0,0 +1,53 @@ +package helloworld; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +import java.util.stream.Collectors; +import java.util.ArrayList; +import java.util.List; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; +import com.amazonaws.services.lambda.runtime.Context; + +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Handler implements RequestHandler { + + public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { + long start = System.currentTimeMillis(); + List result = slowRecursiveFunction(0, 5); + long end = System.currentTimeMillis(); + long duration = end - start; + + System.out.println("Function execution time: " + duration + " ms"); + System.out.println("Result size: " + result.size()); + System.out.println("First few elements: " + result.subList(0, Math.min(10, result.size()))); + + return new APIGatewayProxyResponseEvent() + .withStatusCode(200) + .withBody("ok"); + + } + + private static List slowRecursiveFunction(int n, int depth) { + List result = new ArrayList<>(); + if (depth == 0) { + return result; + } + long startTime = System.currentTimeMillis(); + while (System.currentTimeMillis() - startTime < 100) { + // nothing to do here + } + result.add(n); + result.addAll(slowRecursiveFunction(n + 2, depth - 1)); + return result; + } +} diff --git a/experimental/aws-lambda-java-profiler/integration_tests/helloworld/src/main/resources/wrapper.sh b/experimental/aws-lambda-java-profiler/integration_tests/helloworld/src/main/resources/wrapper.sh new file mode 100644 index 000000000..b54b77673 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/integration_tests/helloworld/src/main/resources/wrapper.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +# the path to the interpreter and all of the originally intended arguments +args=("$@") + +# the extra options to pass to the interpreter +echo "${args[@]}" + +# start the runtime with the extra options +exec "${args[@]}" \ No newline at end of file diff --git a/experimental/aws-lambda-java-profiler/integration_tests/invoke_function.sh b/experimental/aws-lambda-java-profiler/integration_tests/invoke_function.sh new file mode 100755 index 000000000..39b0dd885 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/integration_tests/invoke_function.sh @@ -0,0 +1,74 @@ +#!/bin/bash + +# Set variables +FUNCTION_NAME="aws-lambda-java-profiler-function-${GITHUB_RUN_ID}" +PAYLOAD='{"key": "value"}' + +echo "Invoking Lambda function: $FUNCTION_NAME" + +# Invoke the Lambda function synchronously and capture the response +RESPONSE=$(aws lambda invoke \ + --function-name "$FUNCTION_NAME" \ + --payload "$PAYLOAD" \ + --cli-binary-format raw-in-base64-out \ + --log-type Tail \ + output.json) + +# Extract the status code and log result from the response +STATUS_CODE=$(echo "$RESPONSE" | jq -r '.StatusCode') +LOG_RESULT=$(echo "$RESPONSE" | jq -r '.LogResult') + +echo "Function invocation completed with status code: $STATUS_CODE" + +# Decode and display the logs +if [ -n "$LOG_RESULT" ]; then + echo "Function logs:" + echo "$LOG_RESULT" | base64 --decode +else + echo "No logs available." +fi + +# Display the function output +echo "Function output:" +cat output.json + +echo "$LOG_RESULT" | base64 --decode | grep "starting the profiler for coldstart" || { echo "ERROR: Profiler did not start for coldstart"; exit 1; } +echo "$LOG_RESULT" | base64 --decode | grep -v "uploading" || { echo "ERROR: Unexpected upload detected on cold start"; exit 1; } + +# Clean up the output file +rm output.json + + +# Invoke it a second time for warm start +echo "Invoking Lambda function: $FUNCTION_NAME" + +# Invoke the Lambda function synchronously and capture the response +RESPONSE=$(aws lambda invoke \ + --function-name "$FUNCTION_NAME" \ + --payload "$PAYLOAD" \ + --cli-binary-format raw-in-base64-out \ + --log-type Tail \ + output.json) + +# Extract the status code and log result from the response +STATUS_CODE=$(echo "$RESPONSE" | jq -r '.StatusCode') +LOG_RESULT=$(echo "$RESPONSE" | jq -r '.LogResult') + +echo "Function invocation completed with status code: $STATUS_CODE" + +# Decode and display the logs +if [ -n "$LOG_RESULT" ]; then + echo "Function logs:" + echo "$LOG_RESULT" | base64 --decode +else + echo "No logs available." +fi + +# Display the function output +echo "Function output:" +cat output.json + +echo "$LOG_RESULT" | base64 --decode | grep "uploading" || { echo "ERROR: Upload not detected on warm start"; exit 1; } + +# Clean up the output file +rm output.json diff --git a/experimental/aws-lambda-java-profiler/integration_tests/invoke_function_custom_options.sh b/experimental/aws-lambda-java-profiler/integration_tests/invoke_function_custom_options.sh new file mode 100755 index 000000000..6cf927ae0 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/integration_tests/invoke_function_custom_options.sh @@ -0,0 +1,86 @@ +#!/bin/bash + +# Set variables +FUNCTION_NAME_CUSTOM_PROFILER_OPTIONS="aws-lambda-java-profiler-function-custom-${GITHUB_RUN_ID}" +PAYLOAD='{"key": "value"}' + +# Expected profiler commands (should match create_function.sh) +EXPECTED_START_COMMAND="start,event=wall,interval=1us,file=/tmp/profile.jfr" +EXPECTED_STOP_COMMAND="stop,file=%s" + +echo "Invoking Lambda function with custom profiler options: $FUNCTION_NAME_CUSTOM_PROFILER_OPTIONS" + +# Invoke the Lambda function synchronously and capture the response +RESPONSE=$(aws lambda invoke \ + --function-name "$FUNCTION_NAME_CUSTOM_PROFILER_OPTIONS" \ + --payload "$PAYLOAD" \ + --cli-binary-format raw-in-base64-out \ + --log-type Tail \ + output.json) + +# Extract the status code and log result from the response +STATUS_CODE=$(echo "$RESPONSE" | jq -r '.StatusCode') +LOG_RESULT=$(echo "$RESPONSE" | jq -r '.LogResult') + +echo "Function invocation completed with status code: $STATUS_CODE" + +# Decode and display the logs +if [ -n "$LOG_RESULT" ]; then + echo "Function logs:" + echo "$LOG_RESULT" | base64 --decode +else + echo "No logs available." +fi + +# Display the function output +echo "Function output:" +cat output.json + +# Verify profiler started +echo "$LOG_RESULT" | base64 --decode | grep "starting the profiler for coldstart" || { echo "ERROR: Profiler did not start for coldstart"; exit 1; } + +# Verify custom start command is being used +echo "$LOG_RESULT" | base64 --decode | grep "$EXPECTED_START_COMMAND" || { echo "ERROR: Expected start command not found: $EXPECTED_START_COMMAND"; exit 1; } +echo "$LOG_RESULT" | base64 --decode | grep "$EXPECTED_STOP_COMMAND" || { echo "ERROR: Expected stop command not found: $EXPECTED_STOP_COMMAND"; exit 1; } + +# Verify no upload on cold start +echo "$LOG_RESULT" | base64 --decode | grep -v "uploading" || { echo "ERROR: Unexpected upload detected on cold start"; exit 1; } + +# Clean up the output file +rm output.json + + +# Invoke it a second time for warm start +echo "Invoking Lambda function (warm start): $FUNCTION_NAME_CUSTOM_PROFILER_OPTIONS" + +# Invoke the Lambda function synchronously and capture the response +RESPONSE=$(aws lambda invoke \ + --function-name "$FUNCTION_NAME_CUSTOM_PROFILER_OPTIONS" \ + --payload "$PAYLOAD" \ + --cli-binary-format raw-in-base64-out \ + --log-type Tail \ + output.json) + +# Extract the status code and log result from the response +STATUS_CODE=$(echo "$RESPONSE" | jq -r '.StatusCode') +LOG_RESULT=$(echo "$RESPONSE" | jq -r '.LogResult') + +echo "Function invocation completed with status code: $STATUS_CODE" + +# Decode and display the logs +if [ -n "$LOG_RESULT" ]; then + echo "Function logs:" + echo "$LOG_RESULT" | base64 --decode +else + echo "No logs available." +fi + +# Display the function output +echo "Function output:" +cat output.json + +# Verify upload happens on warm start +echo "$LOG_RESULT" | base64 --decode | grep "uploading" || { echo "ERROR: Upload not detected on warm start"; exit 1; } + +# Clean up the output file +rm output.json diff --git a/experimental/aws-lambda-java-profiler/integration_tests/publish_layer.sh b/experimental/aws-lambda-java-profiler/integration_tests/publish_layer.sh new file mode 100755 index 000000000..879944e8e --- /dev/null +++ b/experimental/aws-lambda-java-profiler/integration_tests/publish_layer.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +# Set variables +LAYER_NAME="aws-lambda-java-profiler-test" +DESCRIPTION="AWS Lambda Java Profiler Test Layer" +ZIP_FILE="./extension/extension.zip" +RUNTIME="java21" +ARCHITECTURE="x86_64" + +# Check if AWS CLI is installed +if ! command -v aws &> /dev/null; then + echo "AWS CLI is not installed. Please install it first." + exit 1 +fi + +# Check if the ZIP file exists +if [ ! -f "$ZIP_FILE" ]; then + echo "ZIP file $ZIP_FILE not found. Please make sure it exists." + exit 1 +fi + +# Publish the layer +echo "Publishing layer $LAYER_NAME..." +RESPONSE=$(aws lambda publish-layer-version \ + --layer-name "$LAYER_NAME" \ + --description "$DESCRIPTION" \ + --zip-file "fileb://$ZIP_FILE" \ + --compatible-runtimes "$RUNTIME" \ + --compatible-architectures "$ARCHITECTURE") + +# Check if the layer was published successfully +if [ $? -eq 0 ]; then + LAYER_VERSION=$(echo $RESPONSE | jq -r '.Version') + LAYER_ARN=$(echo $RESPONSE | jq -r '.LayerVersionArn') + echo "Layer published successfully!" + echo "Layer Version: $LAYER_VERSION" + echo "Layer ARN: $LAYER_ARN" + echo $LAYER_ARN > /tmp/layer_arn +else + echo "Failed to publish layer. Please check your AWS credentials and permissions." + exit 1 +fi \ No newline at end of file diff --git a/experimental/aws-lambda-java-profiler/update-function.sh b/experimental/aws-lambda-java-profiler/update-function.sh new file mode 100755 index 000000000..e849246a6 --- /dev/null +++ b/experimental/aws-lambda-java-profiler/update-function.sh @@ -0,0 +1,93 @@ +#!/bin/bash + +# Check if a function name was provided +if [ $# -eq 0 ]; then + echo "Please provide a function name as an argument." + echo "Usage: $0 " + exit 1 +fi + +FUNCTION_NAME="$1" + +# Generate a random lowercase S3 bucket name +RANDOM_SUFFIX=$(uuidgen | tr '[:upper:]' '[:lower:]' | cut -d'-' -f1) +BUCKET_NAME="my-bucket-${RANDOM_SUFFIX}" +echo "Generated bucket name: $BUCKET_NAME" + +# Create the S3 bucket with the random name +aws s3 mb "s3://$BUCKET_NAME" + +# Create a Lambda layer +aws lambda publish-layer-version \ + --layer-name profiler-layer \ + --description "Profiler Layer" \ + --license-info "MIT" \ + --zip-file fileb://extension/extension.zip \ + --compatible-runtimes java11 java17 java21 \ + --compatible-architectures "arm64" "x86_64" + +# Assign the layer to the function +aws lambda update-function-configuration \ + --function-name "$FUNCTION_NAME" \ + --layers $(aws lambda list-layer-versions --layer-name profiler-layer --query 'LayerVersions[0].LayerVersionArn' --output text) + +# Wait for the function to be updated +aws lambda wait function-updated \ + --function-name "$FUNCTION_NAME" + +# Get existing environment variables (handle null case) +EXISTING_VARS=$(aws lambda get-function-configuration --function-name "$FUNCTION_NAME" --query "Environment.Variables" --output json 2>/dev/null) +if [[ -z "$EXISTING_VARS" || "$EXISTING_VARS" == "null" ]]; then + EXISTING_VARS="{}" +fi + +# Define new environment variables in JSON format +NEW_VARS=$(jq -n --arg bucket "$BUCKET_NAME" \ + --arg java_opts "-XX:+UnlockDiagnosticVMOptions -XX:+DebugNonSafepoints -javaagent:/opt/profiler-extension.jar" \ + '{AWS_LAMBDA_PROFILER_RESULTS_BUCKET_NAME: $bucket, JAVA_TOOL_OPTIONS: $java_opts}') + +# Merge existing and new variables (compact JSON output) +UPDATED_VARS=$(echo "$EXISTING_VARS" | jq -c --argjson new_vars "$NEW_VARS" '. + $new_vars') + +# Convert JSON to "Key=Value" format for AWS CLI +ENV_VARS_FORMATTED=$(echo "$UPDATED_VARS" | jq -r 'to_entries | map("\(.key)=\(.value)") | join(",")') + +# Update Lambda function with correct format +aws lambda update-function-configuration \ + --function-name "$FUNCTION_NAME" \ + --environment "Variables={$ENV_VARS_FORMATTED}" + +# Update the function's permissions to write to the S3 bucket +# Get the function's execution role +ROLE_NAME=$(aws lambda get-function --function-name "$FUNCTION_NAME" --query 'Configuration.Role' --output text | awk -F'/' '{print $NF}') + +# Create a policy document +cat << EOF > s3-write-policy.json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "s3:PutObject" + ], + "Resource": [ + "arn:aws:s3:::$BUCKET_NAME", + "arn:aws:s3:::$BUCKET_NAME/*" + ] + } + ] +} +EOF + +# Attach the policy to the role +aws iam put-role-policy \ + --role-name "$ROLE_NAME" \ + --policy-name S3WriteAccess \ + --policy-document file://s3-write-policy.json + +echo "Setup completed for function $FUNCTION_NAME with S3 bucket $BUCKET_NAME" +echo "S3 write permissions added to the function's execution role" + +# Clean up temporary files +rm s3-write-policy.json diff --git a/git-config/hooks/pre-commit b/git-config/hooks/pre-commit new file mode 100755 index 000000000..a3e90e581 --- /dev/null +++ b/git-config/hooks/pre-commit @@ -0,0 +1,41 @@ +#!/bin/bash -e +# +# based on : https://hdpe.me/post/maven-checkstyle-pre-commit-hook/ +# +# DESCRIPTION +# Considering only the staged .java files, it walks the filesystem tree +# to look for any modules (it considers a module if it has a pom.xml file). +# For each module found it runs Checkstyle on it. + +function get_module() { + local path=$1; + while true; do + path=$(dirname $path); + if [ -f "$path/pom.xml" ]; then + echo "$path"; + return; + elif [[ "./" =~ "$path" ]]; then + return; + fi + done +} + +modules=(); + +for file in $(git diff --name-only --cached \*.java); do + module=$(get_module "$file"); + if [ "" != "$module" ] \ + && [[ ! " ${modules[@]} " =~ " $module " ]]; then + modules+=("$module"); + fi +done; + +if [ ${#modules[@]} -eq 0 ]; then + exit; +fi + +for dir in ${modules[@]}; do + cd ${dir}; + mvn checkstyle:check -Dcheckstyle.config.location=google_checks.xml -Dcheckstyle.consoleOutput=true; + cd ..; +done; diff --git a/lambda-integration-tests/log4j2-test-function/pom.xml b/lambda-integration-tests/log4j2-test-function/pom.xml new file mode 100644 index 000000000..295ddebef --- /dev/null +++ b/lambda-integration-tests/log4j2-test-function/pom.xml @@ -0,0 +1,77 @@ + + 4.0.0 + + com.amazonaws + log4j2-integration-test-function + 1.0.0 + jar + + Log4j2 Integration Test Function + + Lambda function used to verify that aws-lambda-java-log4j2 correctly emits logs to CloudWatch. + + + + 21 + 21 + UTF-8 + 2.25.5 + + + + + com.amazonaws + aws-lambda-java-core + 1.4.0 + + + com.amazonaws + aws-lambda-java-log4j2 + 1.6.4 + + + org.apache.logging.log4j + log4j-core + ${log4j.version} + + + org.apache.logging.log4j + log4j-api + ${log4j.version} + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.1 + + + package + + shade + + + + + + + + + + + + com.github.edwgiz + maven-shade-plugin.log4j2-cachefile-transformer + 2.8.1 + + + + + + diff --git a/lambda-integration-tests/log4j2-test-function/src/main/java/integ/Log4j2TestHandler.java b/lambda-integration-tests/log4j2-test-function/src/main/java/integ/Log4j2TestHandler.java new file mode 100644 index 000000000..d81a3fa27 --- /dev/null +++ b/lambda-integration-tests/log4j2-test-function/src/main/java/integ/Log4j2TestHandler.java @@ -0,0 +1,30 @@ +package integ; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.Map; + +/** + * integration test handler that logs a marker string using Log4j2 with the LambdaAppender. + * the test verifies that the marker appears in CloudWatch Logs, confirming end-to-end + * log delivery through the aws-lambda-java-log4j2 library. + */ +public class Log4j2TestHandler implements RequestHandler, String> { + + private static final Logger logger = LogManager.getLogger(Log4j2TestHandler.class); + + @Override + public String handleRequest(Map event, Context context) { + String marker = event.getOrDefault("marker", "NO_MARKER_PROVIDED"); + + logger.info("INTEG_TEST_MARKER: {}", marker); + logger.debug("Debug level message with marker: {}", marker); + logger.warn("Warning level message with marker: {}", marker); + logger.error("Error level message with marker: {}", marker); + + return "OK:" + marker; + } +} diff --git a/lambda-integration-tests/log4j2-test-function/src/main/resources/log4j2.xml b/lambda-integration-tests/log4j2-test-function/src/main/resources/log4j2.xml new file mode 100644 index 000000000..1cbc36bd0 --- /dev/null +++ b/lambda-integration-tests/log4j2-test-function/src/main/resources/log4j2.xml @@ -0,0 +1,17 @@ + + + + + + + %d{yyyy-MM-dd HH:mm:ss} %X{AWSRequestId} %-5p %c{1}:%L - %m%n + + + + + + + + + + diff --git a/lambda-integration-tests/run-tests.sh b/lambda-integration-tests/run-tests.sh new file mode 100755 index 000000000..844cc5655 --- /dev/null +++ b/lambda-integration-tests/run-tests.sh @@ -0,0 +1,103 @@ +# integration test script for aws-lambda-java-log4j2. +# invokes the deployed lambda function and verifies logs appear in CloudWatch. + +set -euo pipefail + +FUNCTION_NAME="${LOG4J2_TEST_FUNCTION:?LOG4J2_TEST_FUNCTION env var is required}" +REGION="${AWS_REGION:?AWS_REGION env var is required}" +MARKER="integ-test-$(date +%s)-${RANDOM}" + +echo "=== Log4j2 Integration Test ===" +echo "Function: ${FUNCTION_NAME}" +echo "Region: ${REGION}" +echo "Marker: ${MARKER}" +echo "" + +# invoke the lambda function +echo ">>> Invoking Lambda function..." +INVOKE_OUTPUT=$(aws lambda invoke \ + --function-name "${FUNCTION_NAME}" \ + --region "${REGION}" \ + --payload "{\"marker\": \"${MARKER}\"}" \ + --cli-binary-format raw-in-base64-out \ + --output json \ + /tmp/integ-test-response.json) || { + echo "FAIL: aws lambda invoke command failed with exit code $?" + echo "Output: ${INVOKE_OUTPUT:-}" + exit 1 +} + +echo "Invoke output: ${INVOKE_OUTPUT}" +RESPONSE=$(cat /tmp/integ-test-response.json) +echo "Response payload: ${RESPONSE}" + +# check for lambda execution errors +FUNCTION_ERROR=$(echo "${INVOKE_OUTPUT}" | jq -r '.FunctionError // empty') +if [ -n "${FUNCTION_ERROR}" ]; then + echo "FAIL: Lambda function returned an execution error (FunctionError: ${FUNCTION_ERROR})" + echo "Error response: ${RESPONSE}" + exit 1 +fi + +# verify the function executed successfully +if echo "${RESPONSE}" | grep -q "OK:${MARKER}"; then + echo ">>> Function invocation successful." +else + echo "FAIL: Unexpected response from Lambda function." + echo "Expected response containing: OK:${MARKER}" + echo "Got: ${RESPONSE}" + exit 1 +fi + +# query CloudWatch logs for the marker +LOG_GROUP="/aws/lambda/${FUNCTION_NAME}" +echo "" +echo ">>> Querying CloudWatch Logs group: ${LOG_GROUP}" + +MAX_ATTEMPTS=5 +WAIT_SECONDS=10 +FOUND=false + +for attempt in $(seq 1 $MAX_ATTEMPTS); do + echo ">>> Attempt ${attempt}/${MAX_ATTEMPTS}: waiting ${WAIT_SECONDS}s for log propagation..." + sleep "${WAIT_SECONDS}" + + LOGS_OUTPUT=$(aws logs filter-log-events \ + --log-group-name "${LOG_GROUP}" \ + --region "${REGION}" \ + --filter-pattern "\"INTEG_TEST_MARKER\" \"${MARKER}\"" \ + --start-time $(($(date +%s) * 1000 - 120000)) \ + --output json 2>&1) + + if echo "${LOGS_OUTPUT}" | grep -q "INTEG_TEST_MARKER: ${MARKER}"; then + FOUND=true + break + fi + + echo " Marker not found yet." + WAIT_SECONDS=$((WAIT_SECONDS * 2)) +done + +# verify the marker was found +if [ "${FOUND}" = true ]; then + echo "" + echo "=== PASS: Log4j2 integration test succeeded ===" + echo "The marker '${MARKER}' was found in CloudWatch Logs (attempt ${attempt})." + echo "This confirms that the LambdaAppender plugin was discovered by Log4j2" + echo "and logs are being delivered to CloudWatch correctly." +else + echo "" + echo "=== FAIL: Log4j2 integration test failed ===" + echo "The marker '${MARKER}' was NOT found in CloudWatch Logs after ${MAX_ATTEMPTS} attempts." + echo "This indicates that the LambdaAppender was not discovered by Log4j2," + echo "likely due to a missing Log4j2Plugins.dat in the packaged JAR." + echo "" + echo "Dumping all recent log events for debugging:" + aws logs filter-log-events \ + --log-group-name "${LOG_GROUP}" \ + --region "${REGION}" \ + --start-time $(($(date +%s) * 1000 - 120000)) \ + --limit 50 \ + --output text 2>&1 || true + exit 1 +fi diff --git a/lambda-integration-tests/samconfig.toml b/lambda-integration-tests/samconfig.toml new file mode 100644 index 000000000..f1f665e48 --- /dev/null +++ b/lambda-integration-tests/samconfig.toml @@ -0,0 +1,23 @@ +version = 0.1 + +[default] +[default.build.parameters] +cached = true +parallel = true +build_in_source = true + +[default.validate.parameters] +lint = true + +[default.deploy.parameters] +capabilities = "CAPABILITY_IAM" +confirm_changeset = true + +[default.sync.parameters] +watch = true + +[default.local_start_api.parameters] +warm_containers = "EAGER" + +[default.local_start_lambda.parameters] +warm_containers = "EAGER" diff --git a/lambda-integration-tests/template.yaml b/lambda-integration-tests/template.yaml new file mode 100644 index 000000000..a12e5f656 --- /dev/null +++ b/lambda-integration-tests/template.yaml @@ -0,0 +1,43 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: aws-lambda-java-log4j2 integration tests + +Parameters: + LambdaRole: + Type: String + Architecture: + Type: String + Default: x86_64 + AllowedValues: + - x86_64 + - arm64 + +Globals: + Function: + Timeout: 30 + MemorySize: 512 + +Resources: + Log4j2TestFunction: + Type: AWS::Serverless::Function + Metadata: + BuildMethod: java21 + Properties: + FunctionName: !Sub "${AWS::StackName}-fn" + CodeUri: log4j2-test-function/ + Handler: integ.Log4j2TestHandler::handleRequest + Runtime: java21 + Architectures: + - !Ref Architecture + Role: !Ref LambdaRole + Environment: + Variables: + AWS_LAMBDA_LOG_FORMAT: TEXT + +Outputs: + Log4j2TestFunction: + Description: "Log4j2 integration test function name" + Value: !Ref Log4j2TestFunction + Log4j2TestFunctionArn: + Description: "Log4j2 integration test function ARN" + Value: !GetAtt Log4j2TestFunction.Arn diff --git a/samples/custom-serialization/.gitignore b/samples/custom-serialization/.gitignore new file mode 100644 index 000000000..2b448259f --- /dev/null +++ b/samples/custom-serialization/.gitignore @@ -0,0 +1,7 @@ +**/target/ +**/HelloWorld.iml +**/samconfig.toml +**/dependency-reduced-pom.xml +**/.aws-sam +**/.gradle +**/bin diff --git a/samples/custom-serialization/README.md b/samples/custom-serialization/README.md new file mode 100644 index 000000000..d9e751471 --- /dev/null +++ b/samples/custom-serialization/README.md @@ -0,0 +1,5 @@ +The Lambda Java managed runtimes support custom serialization for JSON events. +https://docs.aws.amazon.com/lambda/latest/dg/java-custom-serialization.html + +## Sample projects +In this repository you will find a number of sample projects from AWS to help you get started with the custom serialization feature. diff --git a/samples/custom-serialization/fastJson/HelloWorldFunction/pom.xml b/samples/custom-serialization/fastJson/HelloWorldFunction/pom.xml new file mode 100644 index 000000000..2a963ca21 --- /dev/null +++ b/samples/custom-serialization/fastJson/HelloWorldFunction/pom.xml @@ -0,0 +1,52 @@ + + 4.0.0 + helloworld + HelloWorld + 1.0 + jar + A sample Hello World created for SAM CLI. + + 21 + 21 + + + + + com.amazonaws + aws-lambda-java-core + 1.2.3 + + + com.amazonaws + aws-lambda-java-events + 3.16.0 + + + + com.alibaba.fastjson2 + fastjson2 + 2.0.33 + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.1 + + + + + package + + shade + + + + + + + diff --git a/samples/custom-serialization/fastJson/HelloWorldFunction/src/main/java/com/example/vehicles/serialization/FastJsonSerializer.java b/samples/custom-serialization/fastJson/HelloWorldFunction/src/main/java/com/example/vehicles/serialization/FastJsonSerializer.java new file mode 100644 index 000000000..44709e768 --- /dev/null +++ b/samples/custom-serialization/fastJson/HelloWorldFunction/src/main/java/com/example/vehicles/serialization/FastJsonSerializer.java @@ -0,0 +1,50 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.example.vehicles.serialization; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONException; +import com.amazonaws.services.lambda.runtime.CustomPojoSerializer; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Type; + +public class FastJsonSerializer implements CustomPojoSerializer { + /** + * ServiceLoader class requires that the single exposed provider type has a default constructor + * to easily instantiate the service providers that it finds + */ + public FastJsonSerializer() { + } + + @Override + public T fromJson(InputStream input, Type type) { + try { + return JSON.parseObject(input, type); + } catch (JSONException e) { + throw (e); + } + } + + @Override + public T fromJson(String input, Type type) { + try { + return JSON.parseObject(input, type); + } catch (JSONException e) { + throw (e); + } + } + + @Override + public void toJson(T value, OutputStream output, Type type) { + try { + JSON.writeTo(output, value); + } catch (JSONException e) { + throw (e); + } + } + +} diff --git a/samples/custom-serialization/fastJson/HelloWorldFunction/src/main/java/helloworld/App.java b/samples/custom-serialization/fastJson/HelloWorldFunction/src/main/java/helloworld/App.java new file mode 100644 index 000000000..02ba6048f --- /dev/null +++ b/samples/custom-serialization/fastJson/HelloWorldFunction/src/main/java/helloworld/App.java @@ -0,0 +1,23 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package helloworld; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; + +/** + * Handler for requests to Lambda function. + */ +public class App implements RequestHandler { + + public APIGatewayProxyResponseEvent handleRequest(Vehicle vehicle, Context context) { + System.out.println("input: " + vehicle); + + return new APIGatewayProxyResponseEvent().withStatusCode(200); + } + +} diff --git a/samples/custom-serialization/fastJson/HelloWorldFunction/src/main/java/helloworld/Vehicle.java b/samples/custom-serialization/fastJson/HelloWorldFunction/src/main/java/helloworld/Vehicle.java new file mode 100644 index 000000000..2d34ee6eb --- /dev/null +++ b/samples/custom-serialization/fastJson/HelloWorldFunction/src/main/java/helloworld/Vehicle.java @@ -0,0 +1,49 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package helloworld; + +import com.alibaba.fastjson2.annotation.JSONField; + +public class Vehicle { + + @JSONField(name = "vehicle-type") + private String vehicleType; + + @JSONField(name = "vehicleID") + private String vehicleId; + + public Vehicle() { + } + + public Vehicle(String vehicleType, String vehicleId) { + this.vehicleType = vehicleType; + this.vehicleId = vehicleId; + } + + public String getVehicleType() { + return vehicleType; + } + + public void setVehicleType(String vehicleType) { + this.vehicleType = vehicleType; + } + + public String getVehicleId() { + return vehicleId; + } + + public void setVehicleId(String vehicleId) { + this.vehicleId = vehicleId; + } + + @Override + public String toString() { + return "Vehicle{" + + "vehicleType='" + vehicleType + '\'' + + ", vehicleId='" + vehicleId + '\'' + + '}'; + } +} diff --git a/samples/custom-serialization/fastJson/HelloWorldFunction/src/main/resources/META-INF/services/com.amazonaws.services.lambda.runtime.CustomPojoSerializer b/samples/custom-serialization/fastJson/HelloWorldFunction/src/main/resources/META-INF/services/com.amazonaws.services.lambda.runtime.CustomPojoSerializer new file mode 100644 index 000000000..58c85a7a4 --- /dev/null +++ b/samples/custom-serialization/fastJson/HelloWorldFunction/src/main/resources/META-INF/services/com.amazonaws.services.lambda.runtime.CustomPojoSerializer @@ -0,0 +1 @@ +com.example.vehicles.serialization.FastJsonSerializer \ No newline at end of file diff --git a/samples/custom-serialization/fastJson/README.md b/samples/custom-serialization/fastJson/README.md new file mode 100644 index 000000000..3f6a2f3a2 --- /dev/null +++ b/samples/custom-serialization/fastJson/README.md @@ -0,0 +1,7 @@ +Build and test commands + +```bash +sam build +sam local invoke -e events/event.json +``` + diff --git a/samples/custom-serialization/fastJson/events/event.json b/samples/custom-serialization/fastJson/events/event.json new file mode 100644 index 000000000..5d882dba3 --- /dev/null +++ b/samples/custom-serialization/fastJson/events/event.json @@ -0,0 +1,4 @@ +{ + "vehicle-type": "car", + "vehicleID": 123 +} \ No newline at end of file diff --git a/samples/custom-serialization/fastJson/template.yaml b/samples/custom-serialization/fastJson/template.yaml new file mode 100644 index 000000000..016239cf5 --- /dev/null +++ b/samples/custom-serialization/fastJson/template.yaml @@ -0,0 +1,43 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: > + fastJson + + Sample SAM Template for fastJson + +# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst +Globals: + Function: + Timeout: 20 + MemorySize: 512 + +Resources: + HelloWorldFunction: + Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction + Properties: + CodeUri: HelloWorldFunction + Handler: helloworld.App::handleRequest + Runtime: java21 + Architectures: + - x86_64 + MemorySize: 512 + Events: + HelloWorld: + Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api + Properties: + Path: /hello + Method: get + +Outputs: + # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function + # Find out more about other implicit resources you can reference within SAM + # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api + HelloWorldApi: + Description: "API Gateway endpoint URL for Prod stage for Hello World function" + Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/" + HelloWorldFunction: + Description: "Hello World Lambda Function ARN" + Value: !GetAtt HelloWorldFunction.Arn + HelloWorldFunctionIamRole: + Description: "Implicit IAM Role created for Hello World function" + Value: !GetAtt HelloWorldFunctionRole.Arn diff --git a/samples/custom-serialization/gson/HelloWorldFunction/pom.xml b/samples/custom-serialization/gson/HelloWorldFunction/pom.xml new file mode 100644 index 000000000..47d04926a --- /dev/null +++ b/samples/custom-serialization/gson/HelloWorldFunction/pom.xml @@ -0,0 +1,51 @@ + + 4.0.0 + helloworld + HelloWorld + 1.0 + jar + A sample Hello World created for SAM CLI. + + 21 + 21 + + + + + com.amazonaws + aws-lambda-java-core + 1.2.3 + + + com.amazonaws + aws-lambda-java-events + 3.16.0 + + + com.google.code.gson + gson + 2.11.0 + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.1 + + + + + package + + shade + + + + + + + diff --git a/samples/custom-serialization/gson/HelloWorldFunction/src/main/java/com/example/vehicles/serialization/GsonSerializer.java b/samples/custom-serialization/gson/HelloWorldFunction/src/main/java/com/example/vehicles/serialization/GsonSerializer.java new file mode 100644 index 000000000..5d2597657 --- /dev/null +++ b/samples/custom-serialization/gson/HelloWorldFunction/src/main/java/com/example/vehicles/serialization/GsonSerializer.java @@ -0,0 +1,60 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.example.vehicles.serialization; + +import com.amazonaws.services.lambda.runtime.CustomPojoSerializer; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.stream.JsonReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.StringReader; +import java.io.UncheckedIOException; +import java.lang.reflect.Type; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +public class GsonSerializer implements CustomPojoSerializer { + private static final Charset utf8 = StandardCharsets.UTF_8; + private static Gson gson; + + public GsonSerializer() { + gson = new GsonBuilder() + .disableHtmlEscaping() + .serializeSpecialFloatingPointValues() + .create(); + } + + @Override + public T fromJson(InputStream input, Type type) { + try (JsonReader reader = new JsonReader(new InputStreamReader(input, utf8))) { + return gson.fromJson(reader, type); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public T fromJson(String input, Type type) { + try (JsonReader reader = new JsonReader(new StringReader(input))) { + return gson.fromJson(reader, type); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public void toJson(T value, OutputStream output, Type type) { + try (PrintWriter writer = new PrintWriter(new BufferedWriter(new OutputStreamWriter(output, utf8)))) { + writer.write(gson.toJson(value)); + } + } +} diff --git a/samples/custom-serialization/gson/HelloWorldFunction/src/main/java/helloworld/App.java b/samples/custom-serialization/gson/HelloWorldFunction/src/main/java/helloworld/App.java new file mode 100644 index 000000000..02ba6048f --- /dev/null +++ b/samples/custom-serialization/gson/HelloWorldFunction/src/main/java/helloworld/App.java @@ -0,0 +1,23 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package helloworld; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; + +/** + * Handler for requests to Lambda function. + */ +public class App implements RequestHandler { + + public APIGatewayProxyResponseEvent handleRequest(Vehicle vehicle, Context context) { + System.out.println("input: " + vehicle); + + return new APIGatewayProxyResponseEvent().withStatusCode(200); + } + +} diff --git a/samples/custom-serialization/gson/HelloWorldFunction/src/main/java/helloworld/Vehicle.java b/samples/custom-serialization/gson/HelloWorldFunction/src/main/java/helloworld/Vehicle.java new file mode 100644 index 000000000..ffce611b2 --- /dev/null +++ b/samples/custom-serialization/gson/HelloWorldFunction/src/main/java/helloworld/Vehicle.java @@ -0,0 +1,49 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package helloworld; + +import com.google.gson.annotations.SerializedName; + +public class Vehicle { + + @SerializedName("vehicle-type") + private String vehicleType; + + @SerializedName("vehicleID") + private String vehicleId; + + public Vehicle() { + } + + public Vehicle(String vehicleType, String vehicleId) { + this.vehicleType = vehicleType; + this.vehicleId = vehicleId; + } + + public String getVehicleType() { + return vehicleType; + } + + public void setVehicleType(String vehicleType) { + this.vehicleType = vehicleType; + } + + public String getVehicleId() { + return vehicleId; + } + + public void setVehicleId(String vehicleId) { + this.vehicleId = vehicleId; + } + + @Override + public String toString() { + return "Vehicle{" + + "vehicleType='" + vehicleType + '\'' + + ", vehicleId='" + vehicleId + '\'' + + '}'; + } +} diff --git a/samples/custom-serialization/gson/HelloWorldFunction/src/main/resources/META-INF/services/com.amazonaws.services.lambda.runtime.CustomPojoSerializer b/samples/custom-serialization/gson/HelloWorldFunction/src/main/resources/META-INF/services/com.amazonaws.services.lambda.runtime.CustomPojoSerializer new file mode 100644 index 000000000..0a4e281c0 --- /dev/null +++ b/samples/custom-serialization/gson/HelloWorldFunction/src/main/resources/META-INF/services/com.amazonaws.services.lambda.runtime.CustomPojoSerializer @@ -0,0 +1 @@ +com.example.vehicles.serialization.GsonSerializer \ No newline at end of file diff --git a/samples/custom-serialization/gson/README.md b/samples/custom-serialization/gson/README.md new file mode 100644 index 000000000..924c0cfd8 --- /dev/null +++ b/samples/custom-serialization/gson/README.md @@ -0,0 +1,6 @@ +Build and test commands + +```bash +sam build +sam local invoke -e events/event.json +``` \ No newline at end of file diff --git a/samples/custom-serialization/gson/events/event.json b/samples/custom-serialization/gson/events/event.json new file mode 100644 index 000000000..5d882dba3 --- /dev/null +++ b/samples/custom-serialization/gson/events/event.json @@ -0,0 +1,4 @@ +{ + "vehicle-type": "car", + "vehicleID": 123 +} \ No newline at end of file diff --git a/samples/custom-serialization/gson/template.yaml b/samples/custom-serialization/gson/template.yaml new file mode 100644 index 000000000..baf3b075e --- /dev/null +++ b/samples/custom-serialization/gson/template.yaml @@ -0,0 +1,43 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: > + gson + + Sample SAM Template for gson + +# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst +Globals: + Function: + Timeout: 20 + MemorySize: 512 + +Resources: + HelloWorldFunction: + Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction + Properties: + CodeUri: HelloWorldFunction + Handler: helloworld.App::handleRequest + Runtime: java21 + Architectures: + - x86_64 + MemorySize: 512 + Events: + HelloWorld: + Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api + Properties: + Path: /hello + Method: get + +Outputs: + # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function + # Find out more about other implicit resources you can reference within SAM + # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api + HelloWorldApi: + Description: "API Gateway endpoint URL for Prod stage for Hello World function" + Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/" + HelloWorldFunction: + Description: "Hello World Lambda Function ARN" + Value: !GetAtt HelloWorldFunction.Arn + HelloWorldFunctionIamRole: + Description: "Implicit IAM Role created for Hello World function" + Value: !GetAtt HelloWorldFunctionRole.Arn diff --git a/samples/custom-serialization/jackson-jr/HelloWorldFunction/build.gradle b/samples/custom-serialization/jackson-jr/HelloWorldFunction/build.gradle new file mode 100644 index 000000000..480abfded --- /dev/null +++ b/samples/custom-serialization/jackson-jr/HelloWorldFunction/build.gradle @@ -0,0 +1,20 @@ +plugins { + id 'java' +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'com.amazonaws:aws-lambda-java-core:1.2.3' + implementation 'com.amazonaws:aws-lambda-java-events:3.14.0' + implementation 'com.fasterxml.jackson.jr:jackson-jr-objects:2.15.2' + implementation 'com.fasterxml.jackson.jr:jackson-jr-annotation-support:2.15.2' + implementation 'com.fasterxml.jackson.core:jackson-annotations:2.15.2' +} + +java { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 +} diff --git a/samples/custom-serialization/jackson-jr/HelloWorldFunction/src/main/java/com/example/vehicles/serialization/JacksonJRSerializer.java b/samples/custom-serialization/jackson-jr/HelloWorldFunction/src/main/java/com/example/vehicles/serialization/JacksonJRSerializer.java new file mode 100644 index 000000000..1ae1661b1 --- /dev/null +++ b/samples/custom-serialization/jackson-jr/HelloWorldFunction/src/main/java/com/example/vehicles/serialization/JacksonJRSerializer.java @@ -0,0 +1,88 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.example.vehicles.serialization; + +import com.amazonaws.services.lambda.runtime.CustomPojoSerializer; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.json.JsonWriteFeature; +import com.fasterxml.jackson.jr.annotationsupport.JacksonAnnotationExtension; +import com.fasterxml.jackson.jr.ob.JSON; +import com.fasterxml.jackson.jr.ob.JSON.Feature; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UncheckedIOException; +import java.lang.reflect.Type; + +public class JacksonJRSerializer implements CustomPojoSerializer { + + private static final JSON globalJson = createJson(); + + private static final JacksonJRSerializer instance = new JacksonJRSerializer(globalJson); + + private final JSON json; + + private JacksonJRSerializer(JSON json) { + this.json = json; + } + + /** + * ServiceLoader class requires that the single exposed provider type has a default constructor + * to easily instantiate the service providers that it finds + */ + public JacksonJRSerializer() { + this.json = globalJson; + } + + public static JacksonJRSerializer getInstance() { + return instance; + } + + public JSON getJson() { + return json; + } + + private static JSON createJson() { + JSON json = JSON.builder(createJsonFactory()) + .register(JacksonAnnotationExtension.std) + .build(); + + json.with(Feature.FLUSH_AFTER_WRITE_VALUE, false); + + return json; + } + + private static JsonFactory createJsonFactory() { + return JsonFactory.builder().build(); + } + + @Override + public T fromJson(InputStream input, Type type) { + try { + return json.beanFrom((Class) type, input); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public T fromJson(String input, Type type) { + try { + return json.beanFrom((Class) type, input); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public void toJson(T value, OutputStream output, Type type) { + try { + json.write(value, output); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } +} diff --git a/samples/custom-serialization/jackson-jr/HelloWorldFunction/src/main/java/helloworld/App.java b/samples/custom-serialization/jackson-jr/HelloWorldFunction/src/main/java/helloworld/App.java new file mode 100644 index 000000000..02ba6048f --- /dev/null +++ b/samples/custom-serialization/jackson-jr/HelloWorldFunction/src/main/java/helloworld/App.java @@ -0,0 +1,23 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package helloworld; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; + +/** + * Handler for requests to Lambda function. + */ +public class App implements RequestHandler { + + public APIGatewayProxyResponseEvent handleRequest(Vehicle vehicle, Context context) { + System.out.println("input: " + vehicle); + + return new APIGatewayProxyResponseEvent().withStatusCode(200); + } + +} diff --git a/samples/custom-serialization/jackson-jr/HelloWorldFunction/src/main/java/helloworld/Vehicle.java b/samples/custom-serialization/jackson-jr/HelloWorldFunction/src/main/java/helloworld/Vehicle.java new file mode 100644 index 000000000..f32c503b3 --- /dev/null +++ b/samples/custom-serialization/jackson-jr/HelloWorldFunction/src/main/java/helloworld/Vehicle.java @@ -0,0 +1,49 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package helloworld; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class Vehicle { + + @JsonProperty("vehicle-type") + private String vehicleType; + + @JsonProperty("vehicleID") + private String vehicleId; + + public Vehicle() { + } + + public Vehicle(String vehicleType, String vehicleId) { + this.vehicleType = vehicleType; + this.vehicleId = vehicleId; + } + + public String getVehicleType() { + return vehicleType; + } + + public void setVehicleType(String vehicleType) { + this.vehicleType = vehicleType; + } + + public String getVehicleId() { + return vehicleId; + } + + public void setVehicleId(String vehicleId) { + this.vehicleId = vehicleId; + } + + @Override + public String toString() { + return "Vehicle{" + + "vehicleType='" + vehicleType + '\'' + + ", vehicleId='" + vehicleId + '\'' + + '}'; + } +} diff --git a/samples/custom-serialization/jackson-jr/HelloWorldFunction/src/main/resources/META-INF/services/com.amazonaws.services.lambda.runtime.CustomPojoSerializer b/samples/custom-serialization/jackson-jr/HelloWorldFunction/src/main/resources/META-INF/services/com.amazonaws.services.lambda.runtime.CustomPojoSerializer new file mode 100644 index 000000000..a54949b07 --- /dev/null +++ b/samples/custom-serialization/jackson-jr/HelloWorldFunction/src/main/resources/META-INF/services/com.amazonaws.services.lambda.runtime.CustomPojoSerializer @@ -0,0 +1 @@ +com.example.vehicles.serialization.JacksonJRSerializer \ No newline at end of file diff --git a/samples/custom-serialization/jackson-jr/README.md b/samples/custom-serialization/jackson-jr/README.md new file mode 100644 index 000000000..3f6a2f3a2 --- /dev/null +++ b/samples/custom-serialization/jackson-jr/README.md @@ -0,0 +1,7 @@ +Build and test commands + +```bash +sam build +sam local invoke -e events/event.json +``` + diff --git a/samples/custom-serialization/jackson-jr/events/event.json b/samples/custom-serialization/jackson-jr/events/event.json new file mode 100644 index 000000000..5d882dba3 --- /dev/null +++ b/samples/custom-serialization/jackson-jr/events/event.json @@ -0,0 +1,4 @@ +{ + "vehicle-type": "car", + "vehicleID": 123 +} \ No newline at end of file diff --git a/samples/custom-serialization/jackson-jr/template.yaml b/samples/custom-serialization/jackson-jr/template.yaml new file mode 100644 index 000000000..e3cf91dfc --- /dev/null +++ b/samples/custom-serialization/jackson-jr/template.yaml @@ -0,0 +1,43 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: > + jackson-jr + + Sample SAM Template for jackson-jr + +# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst +Globals: + Function: + Timeout: 20 + MemorySize: 512 + +Resources: + HelloWorldFunction: + Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction + Properties: + CodeUri: HelloWorldFunction + Handler: helloworld.App::handleRequest + Runtime: java21 + Architectures: + - x86_64 + MemorySize: 512 + Events: + HelloWorld: + Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api + Properties: + Path: /hello + Method: get + +Outputs: + # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function + # Find out more about other implicit resources you can reference within SAM + # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api + HelloWorldApi: + Description: "API Gateway endpoint URL for Prod stage for Hello World function" + Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/" + HelloWorldFunction: + Description: "Hello World Lambda Function ARN" + Value: !GetAtt HelloWorldFunction.Arn + HelloWorldFunctionIamRole: + Description: "Implicit IAM Role created for Hello World function" + Value: !GetAtt HelloWorldFunctionRole.Arn diff --git a/samples/custom-serialization/moshi/HelloWorldFunction/pom.xml b/samples/custom-serialization/moshi/HelloWorldFunction/pom.xml new file mode 100644 index 000000000..60277f10b --- /dev/null +++ b/samples/custom-serialization/moshi/HelloWorldFunction/pom.xml @@ -0,0 +1,52 @@ + + 4.0.0 + helloworld + HelloWorld + 1.0 + jar + A sample Hello World created for SAM CLI. + + 21 + 21 + + + + + com.amazonaws + aws-lambda-java-core + 1.2.3 + + + com.amazonaws + aws-lambda-java-events + 3.16.0 + + + + com.squareup.moshi + moshi + 1.15.1 + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.1 + + + + + package + + shade + + + + + + + diff --git a/samples/custom-serialization/moshi/HelloWorldFunction/src/main/java/com/example/vehicles/serialization/MoshiSerializer.java b/samples/custom-serialization/moshi/HelloWorldFunction/src/main/java/com/example/vehicles/serialization/MoshiSerializer.java new file mode 100644 index 000000000..1254b1eec --- /dev/null +++ b/samples/custom-serialization/moshi/HelloWorldFunction/src/main/java/com/example/vehicles/serialization/MoshiSerializer.java @@ -0,0 +1,74 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package com.example.vehicles.serialization; + +import com.amazonaws.services.lambda.runtime.CustomPojoSerializer; +import com.squareup.moshi.JsonAdapter; +import com.squareup.moshi.Moshi; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UncheckedIOException; +import java.lang.reflect.Type; +import okio.BufferedSink; +import okio.Okio; + +public class MoshiSerializer implements CustomPojoSerializer { + + private static final Moshi globalMoshi = createMoshi(); + + private final Moshi moshi; + + /** + * ServiceLoader class requires that the single exposed provider type has a + * default constructor + * to easily instantiate the service providers that it finds + */ + public MoshiSerializer() { + this.moshi = globalMoshi; + } + + private static Moshi createMoshi() { + return new Moshi.Builder().build(); + } + + @Override + public T fromJson(InputStream input, Type type) { + JsonAdapter jsonAdapter = moshi.adapter(type); + try { + return jsonAdapter.fromJson(Okio.buffer(Okio.source(input))); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public T fromJson(String input, Type type) { + JsonAdapter jsonAdapter = moshi.adapter(type); + try { + return jsonAdapter.fromJson(input); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public void toJson(T value, OutputStream output, Type type) { + JsonAdapter jsonAdapter = moshi.adapter(type); + BufferedSink out = Okio.buffer(Okio.sink(output)); + try { + jsonAdapter.toJson(out, value); + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + out.flush(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } +} diff --git a/samples/custom-serialization/moshi/HelloWorldFunction/src/main/java/helloworld/App.java b/samples/custom-serialization/moshi/HelloWorldFunction/src/main/java/helloworld/App.java new file mode 100644 index 000000000..02ba6048f --- /dev/null +++ b/samples/custom-serialization/moshi/HelloWorldFunction/src/main/java/helloworld/App.java @@ -0,0 +1,23 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package helloworld; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; + +/** + * Handler for requests to Lambda function. + */ +public class App implements RequestHandler { + + public APIGatewayProxyResponseEvent handleRequest(Vehicle vehicle, Context context) { + System.out.println("input: " + vehicle); + + return new APIGatewayProxyResponseEvent().withStatusCode(200); + } + +} diff --git a/samples/custom-serialization/moshi/HelloWorldFunction/src/main/java/helloworld/Vehicle.java b/samples/custom-serialization/moshi/HelloWorldFunction/src/main/java/helloworld/Vehicle.java new file mode 100644 index 000000000..0087ee2cf --- /dev/null +++ b/samples/custom-serialization/moshi/HelloWorldFunction/src/main/java/helloworld/Vehicle.java @@ -0,0 +1,49 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package helloworld; + +import com.squareup.moshi.Json; + +public class Vehicle { + + @Json(name = "vehicle-type") + private String vehicleType; + + @Json(name = "vehicleID") + private String vehicleId; + + public Vehicle() { + } + + public Vehicle(String vehicleType, String vehicleId) { + this.vehicleType = vehicleType; + this.vehicleId = vehicleId; + } + + public String getVehicleType() { + return vehicleType; + } + + public void setVehicleType(String vehicleType) { + this.vehicleType = vehicleType; + } + + public String getVehicleId() { + return vehicleId; + } + + public void setVehicleId(String vehicleId) { + this.vehicleId = vehicleId; + } + + @Override + public String toString() { + return "Vehicle{" + + "vehicleType='" + vehicleType + '\'' + + ", vehicleId='" + vehicleId + '\'' + + '}'; + } +} diff --git a/samples/custom-serialization/moshi/HelloWorldFunction/src/main/resources/META-INF/services/com.amazonaws.services.lambda.runtime.CustomPojoSerializer b/samples/custom-serialization/moshi/HelloWorldFunction/src/main/resources/META-INF/services/com.amazonaws.services.lambda.runtime.CustomPojoSerializer new file mode 100644 index 000000000..8f07647e8 --- /dev/null +++ b/samples/custom-serialization/moshi/HelloWorldFunction/src/main/resources/META-INF/services/com.amazonaws.services.lambda.runtime.CustomPojoSerializer @@ -0,0 +1 @@ +com.example.vehicles.serialization.MoshiSerializer \ No newline at end of file diff --git a/samples/custom-serialization/moshi/README.md b/samples/custom-serialization/moshi/README.md new file mode 100644 index 000000000..3f6a2f3a2 --- /dev/null +++ b/samples/custom-serialization/moshi/README.md @@ -0,0 +1,7 @@ +Build and test commands + +```bash +sam build +sam local invoke -e events/event.json +``` + diff --git a/samples/custom-serialization/moshi/events/event.json b/samples/custom-serialization/moshi/events/event.json new file mode 100644 index 000000000..5d882dba3 --- /dev/null +++ b/samples/custom-serialization/moshi/events/event.json @@ -0,0 +1,4 @@ +{ + "vehicle-type": "car", + "vehicleID": 123 +} \ No newline at end of file diff --git a/samples/custom-serialization/moshi/template.yaml b/samples/custom-serialization/moshi/template.yaml new file mode 100644 index 000000000..8d2b95365 --- /dev/null +++ b/samples/custom-serialization/moshi/template.yaml @@ -0,0 +1,43 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: > + moshi + + Sample SAM Template for moshi + +# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst +Globals: + Function: + Timeout: 20 + MemorySize: 512 + +Resources: + HelloWorldFunction: + Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction + Properties: + CodeUri: HelloWorldFunction + Handler: helloworld.App::handleRequest + Runtime: java21 + Architectures: + - x86_64 + MemorySize: 512 + Events: + HelloWorld: + Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api + Properties: + Path: /hello + Method: get + +Outputs: + # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function + # Find out more about other implicit resources you can reference within SAM + # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api + HelloWorldApi: + Description: "API Gateway endpoint URL for Prod stage for Hello World function" + Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/" + HelloWorldFunction: + Description: "Hello World Lambda Function ARN" + Value: !GetAtt HelloWorldFunction.Arn + HelloWorldFunctionIamRole: + Description: "Implicit IAM Role created for Hello World function" + Value: !GetAtt HelloWorldFunctionRole.Arn diff --git a/samples/custom-serialization/request-stream-handler/HelloWorldFunction/pom.xml b/samples/custom-serialization/request-stream-handler/HelloWorldFunction/pom.xml new file mode 100644 index 000000000..15e16439d --- /dev/null +++ b/samples/custom-serialization/request-stream-handler/HelloWorldFunction/pom.xml @@ -0,0 +1,51 @@ + + 4.0.0 + helloworld + HelloWorld + 1.0 + jar + A sample Hello World created for SAM CLI. + + 17 + 17 + + + + + com.amazonaws + aws-lambda-java-core + 1.2.3 + + + com.amazonaws + aws-lambda-java-events + 3.16.0 + + + com.google.code.gson + gson + 2.10.1 + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.1 + + + + + package + + shade + + + + + + + diff --git a/samples/custom-serialization/request-stream-handler/HelloWorldFunction/src/main/java/helloworld/App.java b/samples/custom-serialization/request-stream-handler/HelloWorldFunction/src/main/java/helloworld/App.java new file mode 100644 index 000000000..645fe8f5e --- /dev/null +++ b/samples/custom-serialization/request-stream-handler/HelloWorldFunction/src/main/java/helloworld/App.java @@ -0,0 +1,46 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package helloworld; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestStreamHandler; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonSyntaxException; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +/** + * Handler for requests to Lambda function. + */ + +public class App implements RequestStreamHandler { + private static final Charset usAscii = StandardCharsets.US_ASCII; + private final Gson gson = new GsonBuilder().setPrettyPrinting().create(); + + @Override + public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) throws IOException { + try ( + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, usAscii)); + PrintWriter writer = new PrintWriter(new BufferedWriter(new OutputStreamWriter(outputStream, usAscii))) + ) { + Vehicle vehicle = gson.fromJson(reader, Vehicle.class); System.out.println("input: " + vehicle); + APIGatewayProxyResponseEvent responseEvent = new APIGatewayProxyResponseEvent().withStatusCode(200); + writer.write(gson.toJson(responseEvent)); + } catch (IllegalStateException | JsonSyntaxException exception) { + exception.printStackTrace(); + } + } +} diff --git a/samples/custom-serialization/request-stream-handler/HelloWorldFunction/src/main/java/helloworld/Vehicle.java b/samples/custom-serialization/request-stream-handler/HelloWorldFunction/src/main/java/helloworld/Vehicle.java new file mode 100644 index 000000000..ffce611b2 --- /dev/null +++ b/samples/custom-serialization/request-stream-handler/HelloWorldFunction/src/main/java/helloworld/Vehicle.java @@ -0,0 +1,49 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package helloworld; + +import com.google.gson.annotations.SerializedName; + +public class Vehicle { + + @SerializedName("vehicle-type") + private String vehicleType; + + @SerializedName("vehicleID") + private String vehicleId; + + public Vehicle() { + } + + public Vehicle(String vehicleType, String vehicleId) { + this.vehicleType = vehicleType; + this.vehicleId = vehicleId; + } + + public String getVehicleType() { + return vehicleType; + } + + public void setVehicleType(String vehicleType) { + this.vehicleType = vehicleType; + } + + public String getVehicleId() { + return vehicleId; + } + + public void setVehicleId(String vehicleId) { + this.vehicleId = vehicleId; + } + + @Override + public String toString() { + return "Vehicle{" + + "vehicleType='" + vehicleType + '\'' + + ", vehicleId='" + vehicleId + '\'' + + '}'; + } +} diff --git a/samples/custom-serialization/request-stream-handler/README.md b/samples/custom-serialization/request-stream-handler/README.md new file mode 100644 index 000000000..924c0cfd8 --- /dev/null +++ b/samples/custom-serialization/request-stream-handler/README.md @@ -0,0 +1,6 @@ +Build and test commands + +```bash +sam build +sam local invoke -e events/event.json +``` \ No newline at end of file diff --git a/samples/custom-serialization/request-stream-handler/events/event.json b/samples/custom-serialization/request-stream-handler/events/event.json new file mode 100644 index 000000000..5d882dba3 --- /dev/null +++ b/samples/custom-serialization/request-stream-handler/events/event.json @@ -0,0 +1,4 @@ +{ + "vehicle-type": "car", + "vehicleID": 123 +} \ No newline at end of file diff --git a/samples/custom-serialization/request-stream-handler/template.yaml b/samples/custom-serialization/request-stream-handler/template.yaml new file mode 100644 index 000000000..b1ba37890 --- /dev/null +++ b/samples/custom-serialization/request-stream-handler/template.yaml @@ -0,0 +1,43 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: > + request-stream-handler + + Sample SAM Template for request-stream-handler + +# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst +Globals: + Function: + Timeout: 20 + MemorySize: 512 + +Resources: + HelloWorldFunction: + Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction + Properties: + CodeUri: HelloWorldFunction + Handler: helloworld.App::handleRequest + Runtime: java21 + Architectures: + - x86_64 + MemorySize: 512 + Events: + HelloWorld: + Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api + Properties: + Path: /hello + Method: get + +Outputs: + # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function + # Find out more about other implicit resources you can reference within SAM + # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api + HelloWorldApi: + Description: "API Gateway endpoint URL for Prod stage for Hello World function" + Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/" + HelloWorldFunction: + Description: "Hello World Lambda Function ARN" + Value: !GetAtt HelloWorldFunction.Arn + HelloWorldFunctionIamRole: + Description: "Implicit IAM Role created for Hello World function" + Value: !GetAtt HelloWorldFunctionRole.Arn diff --git a/samples/kinesis-firehose-event-handler/pom.xml b/samples/kinesis-firehose-event-handler/pom.xml new file mode 100644 index 000000000..0db8ed83a --- /dev/null +++ b/samples/kinesis-firehose-event-handler/pom.xml @@ -0,0 +1,81 @@ + + 4.0.0 + + com.amazonaws + aws-lambda-java-events-examples + 1.0.0 + jar + + AWS Lambda Java Events Samples - KinesisFirehose + + AWS Lambda Java Function Examples + + https://aws.amazon.com/lambda/ + + + Apache License, Version 2.0 + https://aws.amazon.com/apache2.0 + repo + + + + https://github.com/aws/aws-lambda-java-libs.git + + + + AWS Lambda team + Amazon Web Services + https://aws.amazon.com/ + + + + + 1.8 + 1.8 + UTF-8 + 5.12.2 + 3.5.4 + + + + + + com.amazonaws + aws-lambda-java-core + 1.2.3 + + + com.amazonaws + aws-lambda-java-events + 3.16.0 + + + + org.junit.jupiter + junit-jupiter + ${junit-jupiter.version} + test + + + com.amazonaws + aws-lambda-java-tests + 1.1.1 + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + true + + + + + diff --git a/samples/kinesis-firehose-event-handler/src/main/java/example/KinesisFirehoseEventHandler.java b/samples/kinesis-firehose-event-handler/src/main/java/example/KinesisFirehoseEventHandler.java new file mode 100644 index 000000000..1d343a1f5 --- /dev/null +++ b/samples/kinesis-firehose-event-handler/src/main/java/example/KinesisFirehoseEventHandler.java @@ -0,0 +1,36 @@ +package example; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.KinesisAnalyticsInputPreprocessingResponse; +import com.amazonaws.services.lambda.runtime.events.KinesisFirehoseEvent; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +import static com.amazonaws.services.lambda.runtime.events.KinesisAnalyticsInputPreprocessingResponse.Result.Ok; +import static java.nio.charset.StandardCharsets.UTF_8; + +/** + * A sample KinesisFirehoseEvent handler + * + * For more information see the developer guide - https://docs.aws.amazon.com/firehose/latest/dev/data-transformation.html + */ +public class KinesisFirehoseEventHandler implements RequestHandler { + + @Override + public KinesisAnalyticsInputPreprocessingResponse handleRequest(KinesisFirehoseEvent kinesisFirehoseEvent, Context context) { + List records = new ArrayList<>(); + + for (KinesisFirehoseEvent.Record record : kinesisFirehoseEvent.getRecords()) { + String recordData = new String(record.getData().array()); + // Your business logic + String reversedString = new StringBuilder(recordData).reverse().toString(); + + records.add(new KinesisAnalyticsInputPreprocessingResponse.Record(record.getRecordId(), Ok, ByteBuffer.wrap(reversedString.getBytes(UTF_8)))); + } + + return new KinesisAnalyticsInputPreprocessingResponse(records); + } +} diff --git a/samples/kinesis-firehose-event-handler/src/test/java/example/KinesisFirehoseEventHandlerTest.java b/samples/kinesis-firehose-event-handler/src/test/java/example/KinesisFirehoseEventHandlerTest.java new file mode 100644 index 000000000..78edbcb97 --- /dev/null +++ b/samples/kinesis-firehose-event-handler/src/test/java/example/KinesisFirehoseEventHandlerTest.java @@ -0,0 +1,27 @@ +package example; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.events.KinesisAnalyticsInputPreprocessingResponse; +import com.amazonaws.services.lambda.runtime.events.KinesisFirehoseEvent; +import com.amazonaws.services.lambda.runtime.tests.annotations.Event; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; + +import static java.nio.charset.StandardCharsets.UTF_8; + +public class KinesisFirehoseEventHandlerTest { + + private Context context; // intentionally null as it's not used in the test + + @ParameterizedTest + @Event(value = "event.json", type = KinesisFirehoseEvent.class) + public void testEventHandler(KinesisFirehoseEvent event) { + KinesisFirehoseEventHandler kinesisFirehoseEventHandler = new KinesisFirehoseEventHandler(); + KinesisAnalyticsInputPreprocessingResponse response = kinesisFirehoseEventHandler.handleRequest(event, context); + + String expectedString = "\n!dlroW olleH"; + KinesisAnalyticsInputPreprocessingResponse.Record firstRecord = response.getRecords().get(0); + Assertions.assertEquals(expectedString, UTF_8.decode(firstRecord.getData()).toString()); + Assertions.assertEquals(KinesisAnalyticsInputPreprocessingResponse.Result.Ok, firstRecord.getResult()); + } +} \ No newline at end of file diff --git a/samples/kinesis-firehose-event-handler/src/test/resources/event.json b/samples/kinesis-firehose-event-handler/src/test/resources/event.json new file mode 100644 index 000000000..08dd9025e --- /dev/null +++ b/samples/kinesis-firehose-event-handler/src/test/resources/event.json @@ -0,0 +1,19 @@ +{ + "invocationId": "invoked123", + "deliveryStreamArn": "aws:lambda:events", + "region": "us-west-2", + "records": [ + { + "data": "SGVsbG8gV29ybGQhCg==", + "recordId": "record2", + "approximateArrivalTimestamp": 1510772160000, + "kinesisRecordMetadata": { + "shardId": "shardId-000000000000", + "partitionKey": "4d1ad2b9-24f8-4b9d-a088-76e9947c317a", + "approximateArrivalTimestamp": "2012-04-23T18:25:43.511Z", + "sequenceNumber": "49546986683135544286507457936321625675700192471156785154", + "subsequenceNumber": "" + } + } + ] +} \ No newline at end of file diff --git a/samples/msk-firehose-event-handler/src/main/java/example/MSKFirehoseEventHandler.java b/samples/msk-firehose-event-handler/src/main/java/example/MSKFirehoseEventHandler.java new file mode 100644 index 000000000..f5e513496 --- /dev/null +++ b/samples/msk-firehose-event-handler/src/main/java/example/MSKFirehoseEventHandler.java @@ -0,0 +1,39 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package example; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.MSKFirehoseResponse; +import com.amazonaws.services.lambda.runtime.events.MSKFirehoseEvent; +import org.json.JSONObject; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +/** + * A sample MSKFirehoseEvent handler + * For more information see the developer guide - ... + */ +public class MSKFirehoseEventHandler implements RequestHandler { + + @Override + public MSKFirehoseResponse handleRequest(MSKFirehoseEvent MSKFirehoseEvent, Context context) { + List records = new ArrayList<>(); + + for (MSKFirehoseEvent.Record record : MSKFirehoseEvent.getRecords()) { + String recordData = new String(record.getKafkaRecordValue().array()); + // Your business logic + JSONObject jsonObject = new JSONObject(recordData); + records.add(new MSKFirehoseResponse.Record(record.getRecordId(), MSKFirehoseResponse.Result.Ok, encode(jsonObject.toString()))); + } + return new MSKFirehoseResponse(records); + } + private ByteBuffer encode(String content) { + return ByteBuffer.wrap(content.getBytes()); + } +} diff --git a/samples/msk-firehose-event-handler/src/test/java/example/MSKFirehoseEventHandlerTest.java b/samples/msk-firehose-event-handler/src/test/java/example/MSKFirehoseEventHandlerTest.java new file mode 100644 index 000000000..77223e516 --- /dev/null +++ b/samples/msk-firehose-event-handler/src/test/java/example/MSKFirehoseEventHandlerTest.java @@ -0,0 +1,32 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package example; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.tests.annotations.Event; +import com.amazonaws.services.lambda.runtime.events.MSKFirehoseEvent; +import com.amazonaws.services.lambda.runtime.events.MSKFirehoseResponse; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; + +import static java.nio.charset.StandardCharsets.UTF_8; + +public class MSKFirehoseEventHandlerTest { + + private Context context; // intentionally null as it's not used in the test + + @ParameterizedTest + @Event(value = "event.json", type = MSKFirehoseEvent.class) + public void testEventHandler(MSKFirehoseEvent event) { + MSKFirehoseEventHandler Sample = new MSKFirehoseEventHandler(); + MSKFirehoseResponse response = Sample.handleRequest(event, context); + + String expectedString = "{\"Name\":\"Hello World\"}"; + MSKFirehoseResponse.Record firstRecord = response.getRecords().get(0); + Assertions.assertEquals(expectedString, UTF_8.decode(firstRecord.getKafkaRecordValue()).toString()); + Assertions.assertEquals(MSKFirehoseResponse.Result.Ok, firstRecord.getResult()); + } +} diff --git a/samples/msk-firehose-event-handler/src/test/resources/event.json b/samples/msk-firehose-event-handler/src/test/resources/event.json new file mode 100644 index 000000000..91c4b4203 --- /dev/null +++ b/samples/msk-firehose-event-handler/src/test/resources/event.json @@ -0,0 +1,18 @@ +{ + "invocationId": "12345621-4787-0000-a418-36e56Example", + "sourceMSKArn": "", + "deliveryStreamArn": "", + "region": "us-east-1", + "records": [ + { + "recordId": "00000000000000000000000000000000000000000000000000000000000000", + "approximateArrivalTimestamp": 1716369573887, + "mskRecordMetadata": { + "offset": "0", + "partitionId": "1", + "approximateArrivalTimestamp": 1716369573887 + }, + "kafkaRecordValue": "eyJOYW1lIjoiSGVsbG8gV29ybGQifQ==" + } + ] +} diff --git a/toolchains.xml.example b/toolchains.xml.example new file mode 100644 index 000000000..b57490761 --- /dev/null +++ b/toolchains.xml.example @@ -0,0 +1,21 @@ + + + + + jdk + + 8 + + + /path/to/your/jdk8 + + +