diff --git a/.github/workflows/jackson3-source-build.yml b/.github/workflows/jackson3-source-build.yml new file mode 100644 index 0000000..15938a3 --- /dev/null +++ b/.github/workflows/jackson3-source-build.yml @@ -0,0 +1,143 @@ +name: Jackson 3 source build +on: + pull_request: + workflow_dispatch: + inputs: + source_refs: + description: JSON map of prerequisite repositories to full commit SHAs + type: string + default: '{}' + push: + branches: [master] +permissions: + contents: read + actions: read +env: + # Reviewed master policy and separately qualified candidate; both pins are immutable. + JACKSON3_QUALIFIER_REVISION: f0d2100f972eec765b2b276e7ace38922394f0a6 + JACKSON3_CORE_REVISION: 97b2359b92ed3e1d05cac04f2da8760aa954e49d +jobs: + test: + runs-on: ubuntu-latest + timeout-minutes: 90 + steps: + - uses: actions/checkout@v4 + with: + path: consumer + ref: ${{ github.event.pull_request.head.sha || github.sha }} + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '25' + - uses: actions/checkout@v4 + with: + repository: networknt/light-4j + ref: ${{ env.JACKSON3_QUALIFIER_REVISION }} + path: qualification-policy + - name: Select qualified core and resolve remaining prerequisites + id: prerequisites + env: + GH_TOKEN: ${{ github.token }} + SOURCE_REFS: ${{ inputs.source_refs || '{}' }} + shell: bash + run: | + jq -e --arg core "$JACKSON3_CORE_REVISION" --argjson expected '["http-client","json-overlay","light-4j","light-aws-lambda","light-rest-4j","openapi-parser","yaml-rule"]' '.["light-4j"] == $core and keys == $expected and all(.[]; type == "string" and test("^[0-9a-f]{40}$"))' consumer/ci/jackson3-source-refs.json >/dev/null + source_refs=$(jq -c --argjson overrides "$SOURCE_REFS" '. * $overrides' consumer/ci/jackson3-source-refs.json) + python3 qualification-policy/scripts/resolve_jackson3_consumer_sources.py \ + --core-sha "$JACKSON3_CORE_REVISION" \ + --repositories '{"json-overlay":null,"openapi-parser":null,"light-rest-4j":null,"light-aws-lambda":null}' --source-refs "$source_refs" \ + --report core-qualification.json --github-output "$GITHUB_OUTPUT" + - uses: actions/checkout@v4 + with: + repository: networknt/yaml-rule + ref: ${{ steps.prerequisites.outputs.yaml_rule }} + path: sources/yaml-rule + - uses: actions/checkout@v4 + with: + repository: networknt/http-client + ref: ${{ steps.prerequisites.outputs.http_client }} + path: sources/http-client + - uses: actions/checkout@v4 + with: + repository: networknt/light-4j + ref: ${{ steps.prerequisites.outputs.light_4j }} + path: sources/light-4j + - uses: actions/checkout@v4 + with: + repository: networknt/json-overlay + ref: ${{ steps.prerequisites.outputs.json_overlay }} + path: sources/json-overlay + - uses: actions/checkout@v4 + with: + repository: networknt/openapi-parser + ref: ${{ steps.prerequisites.outputs.openapi_parser }} + path: sources/openapi-parser + - uses: actions/checkout@v4 + with: + repository: networknt/light-aws-lambda + ref: ${{ steps.prerequisites.outputs.light_aws_lambda }} + path: sources/light-aws-lambda + - uses: actions/checkout@v4 + with: + repository: networknt/light-rest-4j + ref: ${{ steps.prerequisites.outputs.light_rest_4j }} + path: sources/light-rest-4j + - name: Record source commits + shell: bash + run: | + for source in consumer sources/*; do + echo "$source $(git -C "$source" rev-parse HEAD)" + done | tee source-commits.txt + for source in sources/*; do + printf '{"key":"%s","value":"%s"}\n' "${source##*/}" "$(git -C "$source" rev-parse HEAD)" + done | jq -s from_entries | tee source-refs.json + - name: Build prerequisites and run full tests + shell: bash + env: + GH_TOKEN: ${{ github.token }} + JACKSON3_REUSE_CORE_QUALIFICATION: 1 + JACKSON3_CORE_SOURCE: ${{ github.workspace }}/sources/light-4j + JACKSON3_CORE_QUALIFICATION_REPORT: ${{ github.workspace }}/core-qualification.json + JACKSON3_MAVEN_REPO: ${{ runner.temp }}/m2 + JACKSON3_YAML_RULE: ${{ github.workspace }}/sources/yaml-rule + JACKSON3_HTTP_CLIENT: ${{ github.workspace }}/sources/http-client + run: | + set -euo pipefail + bash qualification-policy/scripts/verify-jackson3-bootstrap.sh 2>&1 | tee source-build.log + mvn -f sources/yaml-rule/pom.xml "-Dmaven.repo.local=$JACKSON3_MAVEN_REPO" clean install 2>&1 | tee -a source-build.log + mvn -f sources/json-overlay/pom.xml "-Dmaven.repo.local=$JACKSON3_MAVEN_REPO" clean install 2>&1 | tee -a source-build.log + mvn -f sources/openapi-parser/pom.xml "-Dmaven.repo.local=$JACKSON3_MAVEN_REPO" clean install 2>&1 | tee -a source-build.log + mvn -f sources/light-rest-4j/pom.xml "-Dmaven.repo.local=$JACKSON3_MAVEN_REPO" clean install 2>&1 | tee -a source-build.log + mvn -f sources/light-aws-lambda/pom.xml "-Dmaven.repo.local=$JACKSON3_MAVEN_REPO" clean install 2>&1 | tee -a source-build.log + mvn -f consumer/pom.xml "-Dmaven.repo.local=$JACKSON3_MAVEN_REPO" clean install 2>&1 | tee -a source-build.log + - name: Test native packaging script + working-directory: consumer + run: python3 tests/test_native_image_script.py + - name: Build and invoke native Runtime locally + working-directory: consumer + env: + MAVEN_REPO_LOCAL: ${{ runner.temp }}/m2 + run: | + bash native-image-linux.sh + python3 tests/native_runtime_smoke.py target/custom-runtime/lambda-native + - name: Audit resolved artifacts, including releases and Jackson boundaries + env: + SOURCE_MAVEN_REPO: ${{ runner.temp }}/m2 + shell: bash + run: | + set -euo pipefail + mvn -B -Dstyle.color=never -f consumer/pom.xml "-Dmaven.repo.local=$SOURCE_MAVEN_REPO" dependency:tree 2>&1 | tee resolved-dependencies.txt + python3 sources/light-4j/scripts/jackson3_artifact_audit.py --repository "$SOURCE_MAVEN_REPO" --tree resolved-dependencies.txt --report artifact-audit.json --require-jackson3-only + - uses: actions/upload-artifact@v4 + if: always() + with: + name: source-build-reports + path: | + source-build.log + source-commits.txt + source-refs.json + resolved-dependencies.txt + artifact-audit.json + core-qualification.json + **/target/surefire-reports/** + **/target/*smoke*.log diff --git a/README.md b/README.md index e61068e..114f223 100644 --- a/README.md +++ b/README.md @@ -122,3 +122,43 @@ software.amazon.awssdk.services.lambda.model.ResourceNotFoundException: Function ### [AuditMiddleware](doc/audit.md) ### [LambdaProxyMiddleware](doc/proxy.md) + +## Jackson 3 migration + +This source line uses Jackson 3.2.3 and json-schema-validator 3.0.8. Build the Jackson 3 light-4j, http-client, REST and AWS Lambda prerequisites first; older snapshot binaries are not compatible. Jackson annotations retain their com.fasterxml.jackson.annotation package. + +The runtime mapper uses Jackson2Defaults with its existing permissive unknown-property override. Mapper instances are immutable. Java time and Optional support are built in. Classic SnakeYAML compatibility pins are retained until the stack-wide cleanup. + +Local qualification includes the full JVM suite, sanitized event/validation parity against the Jackson 2 baseline, and an offline GraalVM 25 native-image build. A native-image build does not establish deployed AWS behavior or complete reflection coverage for every middleware/configuration. The separate conformance worktree is deferred. + +Native qualification now invokes the executable against a synthetic loopback Lambda Runtime API: + +```sh +MAVEN_REPO_LOCAL=/path/to/isolated/m2 bash native-image-linux.sh +python3 tests/native_runtime_smoke.py target/custom-runtime/lambda-native +``` + +The smoke check binds an API Gateway event, runs the local health handler, and verifies success and builder-based error serialization. It uses no AWS endpoint or credentials. `-H:+AddAllCharsets` is required by SnakeYAML Engine's UTF-32 input detection. The check does not qualify every middleware chain or deployed AWS behavior. + +### Reproducible Jackson 3 source build + +`.github/workflows/jackson3-source-build.yml` builds the prerequisite source stack in an isolated Maven repository, reuses independently qualified full-core test evidence, and runs the prerequisite and full consumer suites. It pins reviewed policy `f0d2100f972eec765b2b276e7ace38922394f0a6` and candidate `97b2359b92ed3e1d05cac04f2da8760aa954e49d`; yaml-rule/http-client are selected from that verified evidence. The complete prerequisite closure is pinned in `ci/jackson3-source-refs.json`, initialized from the successful source-build manifest, with reviewed updates expressed as full SHAs; no default-branch or migration-branch lookup is used. Explicit `source_refs` replay overrides are merged into that map and remain subject to core qualification. The workflow uploads `source-refs.json`, `source-commits.txt`, `core-qualification.json` and Surefire reports. Core registration and workflow-token acceptance are merged in light-4j #2825 and #2826. + +The optional `source_refs` workflow-dispatch input replays recorded full SHAs. GitHub enables dispatch only after the workflow exists on the default branch; before merge, reproduce from the recorded source checkouts locally. A green local suite is separate from a successful remote source-build run. + +### Exact-source CI qualification + +Consumer CI requires retained successful full-reactor evidence for the actual checked-out light-4j SHA and the same yaml-rule/http-client source commits. Core PR CI explicitly checks out the PR head, not its merge ref. Replay dispatches cannot qualify consumers. The attempt-specific evidence artifact records the actual checkout, prerequisite SHAs and test totals. A later pending, failed or cancelled run does not invalidate earlier valid evidence. All API pages and retained attempts are considered; missing or expired evidence blocks reuse. `core-qualification.json` records the accepted run and artifact. + +The full dependency ledger is audited across all groups. Every NetworkNT release and snapshot must be source-installed, apart from the explicitly SHA-256-pinned Jackson 3 validator release. Known Jackson 2 yaml-rule and chaos-monkey releases are rejected even with a local-install marker. Jackson 2 dependencies in other groups are reported; ready migrations reject them except shared annotations. + + +### Updating Jackson 3 sibling source pins after merge + +The source manifest deliberately pins reviewed commits, including sibling PR sources. It does not follow branch heads. Later CI-only or documentation commits in a sibling do not require repeatedly chasing its head. + +Before merging this dependent PR, merge networknt/light-aws-lambda#168. Resolve each actual merge commit through GitHub (`gh pr view --repo networknt/ --json mergeCommit`); do not substitute the old PR head or a moving branch. For codegen, merge and requalify the Lambda native dependency after AWS first. + +Fetch each sibling repository’s master and verify the selected commit is its ancestor with `git merge-base --is-ancestor origin/master`. Replace the corresponding values in `ci/jackson3-source-refs.json` with those full 40-character merge SHAs in a reviewed dependent change. Keep the remaining closure pins and the qualified core/policy SHAs unchanged. Run fresh source-build CI on that dependent commit and review its resolved-source artifact before merging. A squash or merge commit must be qualified even when its runtime tree matches the earlier pin. + +For later sibling updates after this PR merges, use the same manifest-update PR and fresh dependent CI procedure. Never silently advance pins or infer qualification from the sibling’s own green build. diff --git a/ci/jackson3-source-refs.json b/ci/jackson3-source-refs.json new file mode 100644 index 0000000..ad31c77 --- /dev/null +++ b/ci/jackson3-source-refs.json @@ -0,0 +1,9 @@ +{ + "http-client": "5ecee29dbb08d813512f5379c2e1b902d553c5d4", + "json-overlay": "e309fd63f95317067c9b13a4bed7ac16727bfea5", + "light-4j": "97b2359b92ed3e1d05cac04f2da8760aa954e49d", + "light-aws-lambda": "dddbbc9e7f6f690f8688ceac024728b6a84f4808", + "light-rest-4j": "a31955945036d9108b7e9bb72167415cc9cfc287", + "openapi-parser": "9ae849056e49f7c796fe28bfe6093fbe4847cc5c", + "yaml-rule": "c237cc3a25720f43e57bf571e25d5fa06687aa3b" +} diff --git a/native-image-linux.sh b/native-image-linux.sh index 99eb3b5..b523a42 100755 --- a/native-image-linux.sh +++ b/native-image-linux.sh @@ -3,13 +3,21 @@ set -euo pipefail cd -- "$(dirname -- "${BASH_SOURCE[0]}")" +maven_args=() +if [[ -n ${MAVEN_REPO_LOCAL:-} ]]; then + maven_args+=("-Dmaven.repo.local=$MAVEN_REPO_LOCAL") +fi +if [[ ${MAVEN_OFFLINE:-0} == 1 ]]; then + maven_args+=(-o) +fi + # Build the shaded JAR, then obtain its name and Java level from Maven. -mvn clean install +mvn clean install "${maven_args[@]}" metadata_dir=$(mktemp -d) trap 'rm -rf -- "$metadata_dir"' EXIT -mvn -q help:evaluate -Dexpression=project.build.finalName \ +mvn -q help:evaluate "${maven_args[@]}" -Dexpression=project.build.finalName \ -Doutput="$metadata_dir/final-name" -mvn -q help:evaluate -Dexpression=maven.compiler.target \ +mvn -q help:evaluate "${maven_args[@]}" -Dexpression=maven.compiler.target \ -Doutput="$metadata_dir/java-version" jar_name=$(cat "$metadata_dir/final-name") java_version=$(cat "$metadata_dir/java-version") @@ -24,6 +32,7 @@ docker run --rm --user "$(id -u):$(id -g)" \ "$graalvm_image" \ --enable-url-protocols=http,https \ --no-fallback \ + -H:+AddAllCharsets \ --verbose \ --initialize-at-build-time=org.slf4j,ch.qos.logback \ --initialize-at-run-time=io.netty \ diff --git a/pom.xml b/pom.xml index 18fd315..1be1d97 100644 --- a/pom.xml +++ b/pom.xml @@ -82,8 +82,8 @@ ${java.version} UTF-8 2.4.2-SNAPSHOT - 2.0.8 - 2.22.3 + 3.0.8 + 3.2.3 2.0.20 1.5.0 1.4.0 @@ -114,7 +114,7 @@ import - com.fasterxml.jackson + tools.jackson jackson-bom ${version.jackson} pom @@ -356,15 +356,15 @@ ${version.lambda-awssdk} - com.fasterxml.jackson.core + tools.jackson.core jackson-databind - com.fasterxml.jackson.core + tools.jackson.core jackson-core - com.fasterxml.jackson.dataformat + tools.jackson.dataformat jackson-dataformat-yaml diff --git a/src/main/java/com/networknt/aws/lambda/Main.java b/src/main/java/com/networknt/aws/lambda/Main.java index 0a12600..276910d 100644 --- a/src/main/java/com/networknt/aws/lambda/Main.java +++ b/src/main/java/com/networknt/aws/lambda/Main.java @@ -2,16 +2,17 @@ import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.ObjectMapper; +import com.networknt.jackson.JacksonDefaults; import com.networknt.aws.lambda.app.LambdaApp; import java.io.IOException; public class Main { - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper() - .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + private static final ObjectMapper OBJECT_MAPPER = JacksonDefaults.builder() + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES).build(); public static void main(String[] args) throws IOException { diff --git a/src/main/java/com/networknt/aws/lambda/handler/middleware/limit/LimitMiddleware.java b/src/main/java/com/networknt/aws/lambda/handler/middleware/limit/LimitMiddleware.java index 953637c..d014982 100644 --- a/src/main/java/com/networknt/aws/lambda/handler/middleware/limit/LimitMiddleware.java +++ b/src/main/java/com/networknt/aws/lambda/handler/middleware/limit/LimitMiddleware.java @@ -2,7 +2,7 @@ import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectMapper; import com.networknt.aws.lambda.handler.MiddlewareHandler; import com.networknt.aws.lambda.LightLambdaExchange; import com.networknt.aws.lambda.utility.HeaderKey; diff --git a/src/main/java/com/networknt/aws/lambda/handler/middleware/specification/OpenApiMiddleware.java b/src/main/java/com/networknt/aws/lambda/handler/middleware/specification/OpenApiMiddleware.java index a9118ec..da332c4 100644 --- a/src/main/java/com/networknt/aws/lambda/handler/middleware/specification/OpenApiMiddleware.java +++ b/src/main/java/com/networknt/aws/lambda/handler/middleware/specification/OpenApiMiddleware.java @@ -1,6 +1,6 @@ package com.networknt.aws.lambda.handler.middleware.specification; -import com.fasterxml.jackson.core.JsonProcessingException; +import tools.jackson.core.JacksonException; import com.networknt.aws.lambda.handler.MiddlewareHandler; import com.networknt.aws.lambda.LightLambdaExchange; import com.networknt.config.Config; @@ -40,7 +40,7 @@ public OpenApiMiddleware() { String openapiString = Config.getInstance().getMapper().writeValueAsString(openapi); LOG.trace("OpenApiMiddleware openapiString: {}", openapiString); helper = new OpenApiHelper(openapiString); - } catch (JsonProcessingException e) { + } catch (JacksonException e) { LOG.error("merge specification failed"); throw new RuntimeException("merge specification failed"); } diff --git a/src/test/java/com/networknt/aws/lambda/Jackson3NativeCompatibilityTest.java b/src/test/java/com/networknt/aws/lambda/Jackson3NativeCompatibilityTest.java new file mode 100644 index 0000000..f0266a3 --- /dev/null +++ b/src/test/java/com/networknt/aws/lambda/Jackson3NativeCompatibilityTest.java @@ -0,0 +1,44 @@ +package com.networknt.aws.lambda; + +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; +import org.junit.jupiter.api.Test; +import tools.jackson.databind.ObjectMapper; + +import java.lang.reflect.Field; + +import static org.junit.jupiter.api.Assertions.*; + +class Jackson3NativeCompatibilityTest { + private ObjectMapper mainMapper() throws Exception { + Field field = Main.class.getDeclaredField("OBJECT_MAPPER"); + field.setAccessible(true); + return (ObjectMapper) field.get(null); + } + + @Test + void runtimeMapperKeepsPermissiveEventBinding() throws Exception { + APIGatewayProxyRequestEvent event = mainMapper().readValue( + "{\"path\":\"/v1/pets\",\"httpMethod\":\"POST\",\"futureField\":true} trailing", + APIGatewayProxyRequestEvent.class); + assertEquals("/v1/pets", event.getPath()); + assertEquals("POST", event.getHttpMethod()); + } + + @Test + void responseRetainsJackson2ShapeAndRoundTrips() throws Exception { + ObjectMapper mapper = mainMapper(); + APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent() + .withStatusCode(200).withBody("{\"ok\":true}").withIsBase64Encoded(false); + String json = mapper.writeValueAsString(response); + assertEquals("{\"statusCode\":200,\"headers\":null,\"multiValueHeaders\":null," + + "\"body\":\"{\\\"ok\\\":true}\",\"isBase64Encoded\":false}", json); + assertEquals(response, mapper.readValue(json, APIGatewayProxyResponseEvent.class)); + } + + @Test + void runtimeMapperRetainsJackson2StreamDepth() throws Exception { + String nested = "[".repeat(600) + "0" + "]".repeat(600); + assertDoesNotThrow(() -> mainMapper().readTree(nested)); + } +} diff --git a/src/test/java/com/networknt/aws/lambda/TestUtils.java b/src/test/java/com/networknt/aws/lambda/TestUtils.java index 7a05410..e36c1c9 100644 --- a/src/test/java/com/networknt/aws/lambda/TestUtils.java +++ b/src/test/java/com/networknt/aws/lambda/TestUtils.java @@ -1,9 +1,10 @@ package com.networknt.aws.lambda; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.ObjectMapper; +import com.networknt.jackson.JacksonDefaults; public class TestUtils { @@ -132,14 +133,14 @@ public class TestUtils { " }\n" + "}"; - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper() - .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + private static final ObjectMapper OBJECT_MAPPER = JacksonDefaults.builder() + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES).build(); public static APIGatewayProxyRequestEvent createTestRequestEvent() { APIGatewayProxyRequestEvent apiGatewayProxyRequestEvent; try { apiGatewayProxyRequestEvent = OBJECT_MAPPER.readValue(TEST_EVENT, APIGatewayProxyRequestEvent.class); - } catch (JsonProcessingException e) { + } catch (JacksonException e) { throw new RuntimeException(e); } diff --git a/src/test/java/com/networknt/aws/lambda/middleware/validator/ValidatorMiddlewareTest.java b/src/test/java/com/networknt/aws/lambda/middleware/validator/ValidatorMiddlewareTest.java index b8cc67e..997f102 100644 --- a/src/test/java/com/networknt/aws/lambda/middleware/validator/ValidatorMiddlewareTest.java +++ b/src/test/java/com/networknt/aws/lambda/middleware/validator/ValidatorMiddlewareTest.java @@ -3,7 +3,7 @@ import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; -import com.fasterxml.jackson.databind.JsonNode; +import tools.jackson.databind.JsonNode; import com.networknt.aws.lambda.InvocationResponse; import com.networknt.aws.lambda.LambdaContext; import com.networknt.aws.lambda.TestUtils; diff --git a/tests/native_runtime_smoke.py b/tests/native_runtime_smoke.py new file mode 100644 index 0000000..74891cf --- /dev/null +++ b/tests/native_runtime_smoke.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +"""Exercise the native Runtime against a loopback-only synthetic Lambda Runtime API.""" +import argparse +import http.server +import json +import os +from pathlib import Path +import queue +import subprocess +import tempfile +import threading + + +def invoke(binary, handler, expected_endpoint): + received = queue.Queue() + event = {"path": "/health", "httpMethod": "GET", "body": None, + "isBase64Encoded": False, "headers": {}, "multiValueHeaders": {}, + "requestContext": {"requestId": "native-smoke", "identity": {"sourceIp": "127.0.0.1"}}} + + class RuntimeApi(http.server.BaseHTTPRequestHandler): + def log_message(self, *args): + pass + + def do_GET(self): + assert self.path == "/2018-06-01/runtime/invocation/next", self.path + payload = json.dumps(event).encode() + self.send_response(200) + self.send_header("lambda-runtime-aws-request-id", "native-smoke") + self.send_header("Content-Length", str(len(payload))) + self.end_headers() + self.wfile.write(payload) + + def do_POST(self): + payload = self.rfile.read(int(self.headers["Content-Length"])) + received.put((self.path, json.loads(payload))) + self.send_response(202) + self.send_header("Content-Length", "0") + self.end_headers() + + with tempfile.TemporaryDirectory(prefix="native-runtime-smoke-") as directory: + config = Path(directory) + (config / "handler.yml").write_text("""enabled: true +handlers: + - com.networknt.aws.lambda.handler.health.HealthCheckHandler@health +chains: {} +paths: + - path: /health + method: get + exec: [health] +defaultHandlers: [health] +""") + (config / "health.yml").write_text("enabled: true\ndownstreamEnabled: false\n") + (config / "lambda-app.yml").write_text("lambdaAppId: native-smoke\nencodeBase64Request: false\nencodeBase64Response: false\n") + (config / "values.yml").write_text("{}\n") + server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), RuntimeApi) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + env = os.environ.copy() + env.update(AWS_LAMBDA_RUNTIME_API=f"127.0.0.1:{server.server_port}", _HANDLER=handler) + with (config / "runtime.log").open("w+") as log: + process = subprocess.Popen([str(binary), f"-Dlight-4j-config-dir={config}"], + env=env, stdout=log, stderr=subprocess.STDOUT) + try: + endpoint, payload = received.get(timeout=45) + assert endpoint.endswith("/native-smoke/" + expected_endpoint), endpoint + return payload + except Exception: + log.seek(0) + diagnostic = log.read() + binary.with_name("native-runtime-smoke-failure.log").write_text(diagnostic) + log.seek(0) + raise AssertionError("Native Runtime smoke failed:\n" + log.read()[-12000:]) + finally: + process.terminate() + try: + process.wait(timeout=5) + except subprocess.TimeoutExpired: + process.kill() + process.wait(timeout=5) + server.shutdown() + server.server_close() + thread.join(timeout=5) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("binary", type=Path) + args = parser.parse_args() + binary = args.binary.resolve(strict=True) + response = invoke(binary, "com.networknt.aws.lambda.app.LambdaApp", "response") + assert response["statusCode"] == 200, response + assert response["body"] == "OK", response + assert response["isBase64Encoded"] is False, response + error = invoke(binary, "native.smoke.MissingHandler", "error") + assert "native.smoke.MissingHandler" in error["message"], error + print("Native Runtime smoke passed: API Gateway binding, health response and builder error serialization") + + +if __name__ == "__main__": + main() diff --git a/tests/test_native_image_script.py b/tests/test_native_image_script.py index f6faad3..d81720a 100644 --- a/tests/test_native_image_script.py +++ b/tests/test_native_image_script.py @@ -70,6 +70,7 @@ def test_packages_pom_named_jar_with_https_and_java_25(self): self.assertIn("ghcr.io/graalvm/native-image-community:25", args) self.assertIn("/workspace/target/customer-runtime-9.9.jar", args) self.assertIn("--enable-url-protocols=http,https", args) + self.assertIn("-H:+AddAllCharsets", args) with ZipFile(self.root / "lambda-native-custom-runtime.zip") as archive: self.assertEqual(set(archive.namelist()), {"lambda-native", "bootstrap"}) for info in archive.infolist():