From b1ca65c5ff3133dfb7df74871df9790e0c21efeb Mon Sep 17 00:00:00 2001 From: Viacheslav Ivanovichev Date: Mon, 26 Aug 2024 22:47:38 +0200 Subject: [PATCH 1/3] Add end-to-end tests --- gradle/libs.versions.toml | 4 +- lambda-runtime/build.gradle.kts | 2 + .../knative/lambda/runtime/{log => }/Ext.kt | 6 +- .../lambda/runtime/LambdaEnvironment.kt | 35 +++++--- .../knative/lambda/runtime/LambdaRuntime.kt | 37 +++++--- .../lambda/runtime/api/LambdaRuntimeClient.kt | 5 +- .../lambda/runtime/log/JsonLogFormatter.kt | 1 + .../knative/lambda/runtime/log/Log.kt | 3 +- .../lambda/runtime/log/TextLogFormatter.kt | 1 + .../lambda/runtime/LambdaRunnerTest.kt | 89 +++++++++++++++++++ .../trueangle/knative/lambda/runtime/Mocks.kt | 2 +- 11 files changed, 151 insertions(+), 34 deletions(-) rename lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/{log => }/Ext.kt (64%) create mode 100644 lambda-runtime/src/nativeTest/kotlin/io/github/trueangle/knative/lambda/runtime/LambdaRunnerTest.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4c8c4f6..277a6f9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -20,10 +20,8 @@ kotlin-date-time = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version. kotlin-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutinest-test" } junit = { group = "junit", name = "junit", version.ref = "junit" } logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } -ktor-server-core = { module = "io.ktor:ktor-server-core-jvm", version.ref = "ktor" } -ktor-server-netty = { module = "io.ktor:ktor-server-netty-jvm", version.ref = "ktor" } -ktor-server-tests = { module = "io.ktor:ktor-server-tests-jvm", version.ref = "ktor" } ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } +ktor-client-mock = { module = "io.ktor:ktor-client-mock", version.ref = "ktor" } ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } ktor-client-curl = { module = "io.ktor:ktor-client-curl", version.ref = "ktor" } ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } diff --git a/lambda-runtime/build.gradle.kts b/lambda-runtime/build.gradle.kts index 3138743..160b7eb 100644 --- a/lambda-runtime/build.gradle.kts +++ b/lambda-runtime/build.gradle.kts @@ -30,6 +30,8 @@ kotlin { nativeTest.dependencies { implementation(libs.kotlin.test) + implementation(libs.kotlin.coroutines.test) + implementation(libs.ktor.client.mock) } } } diff --git a/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/log/Ext.kt b/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/Ext.kt similarity index 64% rename from lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/log/Ext.kt rename to lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/Ext.kt index 28941e7..028b83a 100644 --- a/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/log/Ext.kt +++ b/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/Ext.kt @@ -1,4 +1,4 @@ -package io.github.trueangle.knative.lambda.runtime.log +package io.github.trueangle.knative.lambda.runtime internal fun Throwable.prettyPrint(includeStackTrace: Boolean = true) = buildString { append("An exception occurred:\n") @@ -9,4 +9,6 @@ internal fun Throwable.prettyPrint(includeStackTrace: Boolean = true) = buildStr append("Stack Trace:\n") append(stackTraceToString()) } -} \ No newline at end of file +} + +internal fun unsafeLazy(initializer: () -> T): Lazy = lazy(LazyThreadSafetyMode.NONE, initializer) diff --git a/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/LambdaEnvironment.kt b/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/LambdaEnvironment.kt index 574dcee..914aa14 100644 --- a/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/LambdaEnvironment.kt +++ b/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/LambdaEnvironment.kt @@ -15,20 +15,33 @@ import platform.posix.getenv @OptIn(ExperimentalForeignApi::class) @PublishedApi internal object LambdaEnvironment { - val FUNCTION_MEMORY_SIZE = getenv(AWS_LAMBDA_FUNCTION_MEMORY_SIZE)?.toKString()?.toIntOrNull() ?: 128 - val LOG_GROUP_NAME: String = getenv(AWS_LAMBDA_LOG_GROUP_NAME)?.toKString().orEmpty() - val LOG_STREAM_NAME: String = getenv(AWS_LAMBDA_LOG_STREAM_NAME)?.toKString().orEmpty() - val LAMBDA_LOG_LEVEL: String? = getenv(AWS_LAMBDA_LOG_LEVEL)?.toKString() - val LAMBDA_LOG_FORMAT: String? = getenv(AWS_LAMBDA_LOG_FORMAT)?.toKString() - val FUNCTION_NAME: String = getenv(AWS_LAMBDA_FUNCTION_NAME)?.toKString().orEmpty() - val FUNCTION_VERSION: String = getenv(AWS_LAMBDA_FUNCTION_VERSION)?.toKString().orEmpty() - - val RUNTIME_API: String = requireNotNull(getenv(AWS_LAMBDA_RUNTIME_API)?.toKString()) { - "Can't find AWS_LAMBDA_RUNTIME_API env variable" + val FUNCTION_MEMORY_SIZE by unsafeLazy { + getenv(AWS_LAMBDA_FUNCTION_MEMORY_SIZE)?.toKString()?.toIntOrNull() ?: 128 + } + val LOG_GROUP_NAME by unsafeLazy { + getenv(AWS_LAMBDA_LOG_GROUP_NAME)?.toKString().orEmpty() + } + val LOG_STREAM_NAME by unsafeLazy { + getenv(AWS_LAMBDA_LOG_STREAM_NAME)?.toKString().orEmpty() + } + val LAMBDA_LOG_LEVEL by unsafeLazy { + getenv(AWS_LAMBDA_LOG_LEVEL)?.toKString() ?: "INFO" + } + val LAMBDA_LOG_FORMAT by unsafeLazy { + getenv(AWS_LAMBDA_LOG_FORMAT)?.toKString() ?: "TEXT" + } + val FUNCTION_NAME by unsafeLazy { + getenv(AWS_LAMBDA_FUNCTION_NAME)?.toKString().orEmpty() + } + val FUNCTION_VERSION by unsafeLazy { + getenv(AWS_LAMBDA_FUNCTION_VERSION)?.toKString().orEmpty() + } + val RUNTIME_API by unsafeLazy { + getenv(AWS_LAMBDA_RUNTIME_API)?.toKString() } } -private object ReservedRuntimeEnvironmentVariables { +internal object ReservedRuntimeEnvironmentVariables { /** * The handler location configured on the function. */ diff --git a/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/LambdaRuntime.kt b/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/LambdaRuntime.kt index 4c2db9a..8034c9c 100644 --- a/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/LambdaRuntime.kt +++ b/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/LambdaRuntime.kt @@ -9,6 +9,7 @@ import io.github.trueangle.knative.lambda.runtime.handler.LambdaStreamHandler import io.github.trueangle.knative.lambda.runtime.log.KtorLogger import io.github.trueangle.knative.lambda.runtime.log.Log import io.ktor.client.HttpClient +import io.ktor.client.engine.HttpClientEngine import io.ktor.client.engine.curl.Curl import io.ktor.client.plugins.HttpTimeout import io.ktor.client.plugins.contentnegotiation.ContentNegotiation @@ -28,7 +29,15 @@ object LambdaRuntime { @OptIn(ExperimentalSerializationApi::class) internal val json = Json { explicitNulls = false } - private val httpClient = HttpClient(Curl) { + inline fun run(crossinline initHandler: () -> LambdaHandler) = runBlocking { + val curlHttpClient = createHttpClient(Curl.create()) + val lambdaClient = LambdaClient(curlHttpClient) + + Runner(lambdaClient).run(initHandler) + } + + @PublishedApi + internal fun createHttpClient(engine: HttpClientEngine) = HttpClient(engine) { install(HttpTimeout) install(ContentNegotiation) { json(json) } install(Logging) { @@ -39,11 +48,13 @@ object LambdaRuntime { filter { !it.headers.contains("Lambda-Runtime-Function-Response-Mode", "streaming") } } } +} - @PublishedApi - internal val client = LambdaClient(httpClient) - - inline fun run(crossinline initHandler: () -> LambdaHandler) = runBlocking { +@PublishedApi +internal class Runner( + val client: LambdaClient, +) { + suspend inline fun run(crossinline initHandler: () -> LambdaHandler) { val handler = try { Log.info("Initializing Kotlin Native Lambda Runtime") @@ -91,6 +102,7 @@ object LambdaRuntime { } } catch (e: LambdaRuntimeException) { Log.error(e) + client.reportError(e) } catch (e: LambdaEnvironmentException) { when (e) { @@ -109,11 +121,8 @@ object LambdaRuntime { } } } -} -@PublishedApi -internal inline fun streamingResponse(crossinline handler: suspend (ByteWriteChannel) -> Unit) = - object : WriteChannelContent() { + inline fun streamingResponse(crossinline handler: suspend (ByteWriteChannel) -> Unit) = object : WriteChannelContent() { override suspend fun writeTo(channel: ByteWriteChannel) { try { handler(channel) @@ -128,9 +137,9 @@ internal inline fun streamingResponse(crossinline handler: suspend (ByteWriteCha "Lambda-Runtime-Function-Error-Type: Runtime.StreamError\r\nLambda-Runtime-Function-Error-Body: ${stackTraceToString().encodeBase64()}\r\n" } -@PublishedApi -internal inline fun T.bufferedResponse(context: Context, block: T.() -> R): R = try { - block() -} catch (e: Exception) { - throw e.asHandlerError(context) + inline fun T.bufferedResponse(context: Context, block: T.() -> R): R = try { + block() + } catch (e: Exception) { + throw e.asHandlerError(context) + } } \ No newline at end of file diff --git a/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/api/LambdaRuntimeClient.kt b/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/api/LambdaRuntimeClient.kt index d994b8f..89746b6 100644 --- a/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/api/LambdaRuntimeClient.kt +++ b/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/api/LambdaRuntimeClient.kt @@ -28,7 +28,10 @@ import io.ktor.http.ContentType.Application.Json as ContentTypeJson @PublishedApi internal class LambdaClient(private val httpClient: HttpClient) { - private val invokeUrl = "http://${LambdaEnvironment.RUNTIME_API}/2018-06-01/runtime" + private val baseUrl = requireNotNull(LambdaEnvironment.RUNTIME_API) { + "Can't find AWS_LAMBDA_RUNTIME_API env variable" + } + private val invokeUrl = "http://$baseUrl/2018-06-01/runtime" private val requestTimeout = 15.minutes.inWholeMilliseconds suspend fun retrieveNextEvent(bodyType: TypeInfo): Pair { diff --git a/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/log/JsonLogFormatter.kt b/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/log/JsonLogFormatter.kt index 0b395fd..744e2cc 100644 --- a/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/log/JsonLogFormatter.kt +++ b/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/log/JsonLogFormatter.kt @@ -2,6 +2,7 @@ package io.github.trueangle.knative.lambda.runtime.log import io.github.trueangle.knative.lambda.runtime.api.Context import io.github.trueangle.knative.lambda.runtime.api.dto.LogMessageDto +import io.github.trueangle.knative.lambda.runtime.prettyPrint import io.ktor.util.reflect.TypeInfo import kotlinx.datetime.Clock import kotlinx.serialization.SerializationException diff --git a/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/log/Log.kt b/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/log/Log.kt index 0372e38..d513f51 100644 --- a/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/log/Log.kt +++ b/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/log/Log.kt @@ -3,7 +3,6 @@ package io.github.trueangle.knative.lambda.runtime.log import io.github.trueangle.knative.lambda.runtime.LambdaEnvironment import io.github.trueangle.knative.lambda.runtime.LambdaRuntime import io.github.trueangle.knative.lambda.runtime.api.Context -import io.github.trueangle.knative.lambda.runtime.log.Log.write import io.ktor.util.reflect.TypeInfo import io.ktor.util.reflect.typeInfo @@ -20,7 +19,7 @@ object Log { @PublishedApi internal val currentLogLevel = LogLevel.fromEnv() private val writer = StdoutLogWriter() - private val logFormatter = if (LambdaEnvironment.LAMBDA_LOG_FORMAT == "JSON") { + private val logFormatter = if (LambdaEnvironment.LAMBDA_LOG_FORMAT.equals("JSON", ignoreCase = true)) { JsonLogFormatter(LambdaRuntime.json) } else { TextLogFormatter() diff --git a/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/log/TextLogFormatter.kt b/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/log/TextLogFormatter.kt index be23968..7232df1 100644 --- a/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/log/TextLogFormatter.kt +++ b/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/log/TextLogFormatter.kt @@ -1,5 +1,6 @@ package io.github.trueangle.knative.lambda.runtime.log +import io.github.trueangle.knative.lambda.runtime.prettyPrint import io.ktor.util.reflect.TypeInfo internal class TextLogFormatter : LogFormatter { diff --git a/lambda-runtime/src/nativeTest/kotlin/io/github/trueangle/knative/lambda/runtime/LambdaRunnerTest.kt b/lambda-runtime/src/nativeTest/kotlin/io/github/trueangle/knative/lambda/runtime/LambdaRunnerTest.kt new file mode 100644 index 0000000..c26f575 --- /dev/null +++ b/lambda-runtime/src/nativeTest/kotlin/io/github/trueangle/knative/lambda/runtime/LambdaRunnerTest.kt @@ -0,0 +1,89 @@ +package io.github.trueangle.knative.lambda.runtime + +import io.github.trueangle.knative.lambda.runtime.ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_FUNCTION_NAME +import io.github.trueangle.knative.lambda.runtime.ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_FUNCTION_VERSION +import io.github.trueangle.knative.lambda.runtime.ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_RUNTIME_API +import io.github.trueangle.knative.lambda.runtime.api.Context +import io.github.trueangle.knative.lambda.runtime.api.LambdaClient +import io.github.trueangle.knative.lambda.runtime.handler.LambdaBufferedHandler +import io.ktor.client.engine.HttpClientEngine +import io.ktor.client.engine.mock.MockEngine +import io.ktor.client.engine.mock.respond +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode +import io.ktor.http.Url +import io.ktor.http.headers +import io.ktor.http.headersOf +import io.ktor.utils.io.ByteReadChannel +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.toKString +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runTest +import platform.posix.getenv +import platform.posix.setenv +import kotlin.test.BeforeClass +import kotlin.test.BeforeTest +import kotlin.test.Test + +class LambdaRunnerTest { + + @BeforeTest + fun setup() { + mockEnvironment() + } + + @Test + fun `GIVEN string event WHEN LambdaBufferedHandler THEN success invocation`() = runTest { + val handlerResponse = "Response" + val requestId = "156cb537-e2d4-11e8-9b34-d36013741fb9" + val deadline = "1542409706888" + + val lambdaRunner = createRunner(MockEngine { request -> + when { + request.url.encodedPath.contains("invocation/next") -> respond( + content = ByteReadChannel("""Hello world"""), + status = HttpStatusCode.OK, + headers = headers { + append(HttpHeaders.ContentType, "application/json") + append("Lambda-Runtime-Aws-Request-Id", requestId) + append("Lambda-Runtime-Deadline-Ms", deadline) + append("Lambda-Runtime-Invoked-Function-Arn", "arn") + + } + ) + + else -> respond( + content = ByteReadChannel("""{"ip":"127.0.0.1"}"""), + status = HttpStatusCode.OK, + headers = headersOf(HttpHeaders.ContentType, "application/json") + ) + } + }) + + val handler = object : LambdaBufferedHandler { + override suspend fun handleRequest(input: String, context: Context): String = handlerResponse + } + + lambdaRunner.run { handler } + } + + @OptIn(ExperimentalForeignApi::class) + private fun mockEnvironment() { + if (getenv(AWS_LAMBDA_FUNCTION_NAME)?.toKString().isNullOrEmpty()) { + setenv(AWS_LAMBDA_FUNCTION_NAME, "test", 1) + } + + if (getenv(AWS_LAMBDA_FUNCTION_VERSION)?.toKString().isNullOrEmpty()) { + setenv(AWS_LAMBDA_FUNCTION_VERSION, "1", 1) + } + + if (getenv(AWS_LAMBDA_RUNTIME_API)?.toKString().isNullOrEmpty()) { + setenv(AWS_LAMBDA_RUNTIME_API, "127.0.0.1", 1) + } + } + + private fun createRunner(mockEngine: HttpClientEngine): Runner { + val lambdaClient = LambdaClient(LambdaRuntime.createHttpClient(mockEngine)) + return Runner(lambdaClient) + } +} \ No newline at end of file diff --git a/lambda-runtime/src/nativeTest/kotlin/io/github/trueangle/knative/lambda/runtime/Mocks.kt b/lambda-runtime/src/nativeTest/kotlin/io/github/trueangle/knative/lambda/runtime/Mocks.kt index a3a9ba9..7603cec 100644 --- a/lambda-runtime/src/nativeTest/kotlin/io/github/trueangle/knative/lambda/runtime/Mocks.kt +++ b/lambda-runtime/src/nativeTest/kotlin/io/github/trueangle/knative/lambda/runtime/Mocks.kt @@ -2,7 +2,7 @@ package io.github.trueangle.knative.lambda.runtime import io.github.trueangle.knative.lambda.runtime.api.Context -internal fun mockContext(awsRequestId: String) = Context( +internal fun mockContext(awsRequestId: String = "awsRequestId") = Context( awsRequestId = awsRequestId, xrayTracingId = "dummyXrayTracingId", deadlineTimeInMs = 100, From ef33a73d82e9723d030c2dda4b227920ecb9511b Mon Sep 17 00:00:00 2001 From: Viacheslav Ivanovichev Date: Tue, 27 Aug 2024 10:28:24 +0200 Subject: [PATCH 2/3] Refactor of Log --- lambda-runtime/build.gradle.kts | 5 ++ .../knative/lambda/runtime/LambdaRuntime.kt | 45 ++++++++------ .../lambda/runtime/api/dto/LogMessageDto.kt | 3 - .../knative/lambda/runtime/log/Log.kt | 59 ++++++++++--------- .../lambda/runtime/LambdaRunnerTest.kt | 30 +++++++--- .../sample/handler/ByteBodyLambdaHandler.kt | 1 + .../sample/handler/ObjectBodyLambdaHandler.kt | 3 - 7 files changed, 86 insertions(+), 60 deletions(-) diff --git a/lambda-runtime/build.gradle.kts b/lambda-runtime/build.gradle.kts index 160b7eb..cece1ca 100644 --- a/lambda-runtime/build.gradle.kts +++ b/lambda-runtime/build.gradle.kts @@ -1,3 +1,5 @@ +import dev.mokkery.MockMode + plugins { alias(libs.plugins.kotlin.multiplatform) alias(libs.plugins.kotlin.serialization) @@ -43,3 +45,6 @@ if (isTesting) allOpen { annotation("kotlin.Metadata") } +mokkery { + defaultMockMode.set(MockMode.autoUnit) +} diff --git a/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/LambdaRuntime.kt b/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/LambdaRuntime.kt index 8034c9c..687911d 100644 --- a/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/LambdaRuntime.kt +++ b/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/LambdaRuntime.kt @@ -7,7 +7,13 @@ import io.github.trueangle.knative.lambda.runtime.handler.LambdaBufferedHandler import io.github.trueangle.knative.lambda.runtime.handler.LambdaHandler import io.github.trueangle.knative.lambda.runtime.handler.LambdaStreamHandler import io.github.trueangle.knative.lambda.runtime.log.KtorLogger +import io.github.trueangle.knative.lambda.runtime.log.LambdaLogger import io.github.trueangle.knative.lambda.runtime.log.Log +import io.github.trueangle.knative.lambda.runtime.log.debug +import io.github.trueangle.knative.lambda.runtime.log.error +import io.github.trueangle.knative.lambda.runtime.log.fatal +import io.github.trueangle.knative.lambda.runtime.log.info +import io.github.trueangle.knative.lambda.runtime.log.warn import io.ktor.client.HttpClient import io.ktor.client.engine.HttpClientEngine import io.ktor.client.engine.curl.Curl @@ -33,7 +39,7 @@ object LambdaRuntime { val curlHttpClient = createHttpClient(Curl.create()) val lambdaClient = LambdaClient(curlHttpClient) - Runner(lambdaClient).run(initHandler) + Runner(client = lambdaClient, log = Log).run(false, initHandler) } @PublishedApi @@ -53,14 +59,15 @@ object LambdaRuntime { @PublishedApi internal class Runner( val client: LambdaClient, + val log: LambdaLogger ) { - suspend inline fun run(crossinline initHandler: () -> LambdaHandler) { + suspend inline fun run(singleEventMode: Boolean = false, crossinline initHandler: () -> LambdaHandler) { val handler = try { - Log.info("Initializing Kotlin Native Lambda Runtime") + log.info("Initializing Kotlin Native Lambda Runtime") initHandler() } catch (e: Exception) { - Log.fatal(e) + log.fatal(e) client.reportError(e.asInitError()) exitProcess(1) @@ -70,55 +77,59 @@ internal class Runner( val inputTypeInfo = typeInfo() val outputTypeInfo = typeInfo() - while (true) { + var shouldExit = false + while (!shouldExit) { try { - Log.info("Runtime is ready for a new event") + log.info("Runtime is ready for a new event") val (event, context) = client.retrieveNextEvent(inputTypeInfo) - with(Log) { + with(log) { setContext(context) debug(event) debug(context) + info("$handlerName invocation started") } - Log.info("$handlerName invocation started") - if (handler is LambdaStreamHandler) { val response = streamingResponse { handler.handleRequest(event, it, context) } - Log.info("$handlerName started response streaming") + log.info("$handlerName started response streaming") client.streamResponse(context, response) } else { handler as LambdaBufferedHandler val response = bufferedResponse(context) { handler.handleRequest(event, context) } - Log.info("$handlerName invocation completed") - Log.debug(response) + log.info("$handlerName invocation completed") + log.debug(response) client.sendResponse(context, response, outputTypeInfo) } } catch (e: LambdaRuntimeException) { - Log.error(e) + log.error(e) client.reportError(e) } catch (e: LambdaEnvironmentException) { when (e) { is NonRecoverableStateException -> { - Log.fatal(e) + log.fatal(e) exitProcess(1) } - else -> Log.error(e) + else -> log.error(e) } } catch (e: Throwable) { - Log.fatal(e) + log.fatal(e) exitProcess(1) } + + if (singleEventMode) { + shouldExit = singleEventMode + } } } @@ -127,7 +138,7 @@ internal class Runner( try { handler(channel) } catch (e: Exception) { - Log.warn("Exception occurred on streaming: " + e.message) + log.warn("Exception occurred on streaming: " + e.message) channel.writeStringUtf8(e.toTrailer()) } diff --git a/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/api/dto/LogMessageDto.kt b/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/api/dto/LogMessageDto.kt index ec2dcee..360a1ae 100644 --- a/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/api/dto/LogMessageDto.kt +++ b/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/api/dto/LogMessageDto.kt @@ -2,11 +2,8 @@ package io.github.trueangle.knative.lambda.runtime.api.dto import io.github.trueangle.knative.lambda.runtime.log.LogLevel import kotlinx.serialization.Contextual -import kotlinx.serialization.KSerializer import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder @Serializable internal data class LogMessageDto( diff --git a/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/log/Log.kt b/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/log/Log.kt index d513f51..9900d79 100644 --- a/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/log/Log.kt +++ b/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/log/Log.kt @@ -15,8 +15,13 @@ internal interface LogFormatter { fun onContextAvailable(context: Context) = Unit } -object Log { - @PublishedApi +interface LambdaLogger { + fun log(level: LogLevel, message: T?, typeInfo: TypeInfo) + + fun setContext(context: Context) +} + +object Log : LambdaLogger { internal val currentLogLevel = LogLevel.fromEnv() private val writer = StdoutLogWriter() private val logFormatter = if (LambdaEnvironment.LAMBDA_LOG_FORMAT.equals("JSON", ignoreCase = true)) { @@ -25,39 +30,37 @@ object Log { TextLogFormatter() } - inline fun trace(message: T?) { - write(LogLevel.TRACE, message, typeInfo()) + override fun setContext(context: Context) { + logFormatter.onContextAvailable(context) } - inline fun debug(message: T?) { - write(LogLevel.DEBUG, message, typeInfo()) + override fun log(level: LogLevel, message: T?, typeInfo: TypeInfo) { + if (level >= currentLogLevel) { + writer.write(level, logFormatter.format(level, message, typeInfo)) + } } +} - inline fun info(message: T?) { - write(LogLevel.INFO, message, typeInfo()) - } +inline fun LambdaLogger.trace(message: T?) { + log(LogLevel.TRACE, message, typeInfo()) +} - inline fun warn(message: T?) { - write(LogLevel.WARN, message, typeInfo()) - } +inline fun LambdaLogger.debug(message: T?) { + log(LogLevel.DEBUG, message, typeInfo()) +} - inline fun error(message: T?) { - write(LogLevel.ERROR, message, typeInfo()) - } +inline fun LambdaLogger.info(message: T?) { + log(LogLevel.INFO, message, typeInfo()) +} - inline fun fatal(message: T?) { - write(LogLevel.FATAL, message, typeInfo()) - } +inline fun LambdaLogger.warn(message: T?) { + log(LogLevel.WARN, message, typeInfo()) +} - @PublishedApi - internal fun setContext(context: Context) { - logFormatter.onContextAvailable(context) - } +inline fun LambdaLogger.error(message: T?) { + log(LogLevel.ERROR, message, typeInfo()) +} - @PublishedApi - internal fun write(level: LogLevel, message: Any?, typeInfo: TypeInfo) { - if (level >= currentLogLevel) { - writer.write(level, logFormatter.format(level, message, typeInfo)) - } - } +inline fun LambdaLogger.fatal(message: T?) { + log(LogLevel.FATAL, message, typeInfo()) } \ No newline at end of file diff --git a/lambda-runtime/src/nativeTest/kotlin/io/github/trueangle/knative/lambda/runtime/LambdaRunnerTest.kt b/lambda-runtime/src/nativeTest/kotlin/io/github/trueangle/knative/lambda/runtime/LambdaRunnerTest.kt index c26f575..46528f7 100644 --- a/lambda-runtime/src/nativeTest/kotlin/io/github/trueangle/knative/lambda/runtime/LambdaRunnerTest.kt +++ b/lambda-runtime/src/nativeTest/kotlin/io/github/trueangle/knative/lambda/runtime/LambdaRunnerTest.kt @@ -1,31 +1,37 @@ package io.github.trueangle.knative.lambda.runtime +import dev.mokkery.matcher.any +import dev.mokkery.mock +import dev.mokkery.verify +import dev.mokkery.verify.VerifyMode import io.github.trueangle.knative.lambda.runtime.ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_FUNCTION_NAME import io.github.trueangle.knative.lambda.runtime.ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_FUNCTION_VERSION import io.github.trueangle.knative.lambda.runtime.ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_RUNTIME_API import io.github.trueangle.knative.lambda.runtime.api.Context import io.github.trueangle.knative.lambda.runtime.api.LambdaClient import io.github.trueangle.knative.lambda.runtime.handler.LambdaBufferedHandler +import io.github.trueangle.knative.lambda.runtime.log.LambdaLogger +import io.github.trueangle.knative.lambda.runtime.log.LogLevel import io.ktor.client.engine.HttpClientEngine import io.ktor.client.engine.mock.MockEngine import io.ktor.client.engine.mock.respond +import io.ktor.client.engine.mock.respondBadRequest import io.ktor.http.HttpHeaders import io.ktor.http.HttpStatusCode -import io.ktor.http.Url import io.ktor.http.headers import io.ktor.http.headersOf import io.ktor.utils.io.ByteReadChannel import kotlinx.cinterop.ExperimentalForeignApi import kotlinx.cinterop.toKString -import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest import platform.posix.getenv import platform.posix.setenv -import kotlin.test.BeforeClass import kotlin.test.BeforeTest import kotlin.test.Test +import kotlin.test.assertFailsWith class LambdaRunnerTest { + private val log = mock() @BeforeTest fun setup() { @@ -35,28 +41,31 @@ class LambdaRunnerTest { @Test fun `GIVEN string event WHEN LambdaBufferedHandler THEN success invocation`() = runTest { val handlerResponse = "Response" - val requestId = "156cb537-e2d4-11e8-9b34-d36013741fb9" + val awsRequestId = "156cb537-e2d4-11e8-9b34-d36013741fb9" val deadline = "1542409706888" val lambdaRunner = createRunner(MockEngine { request -> + val path = request.url.encodedPath when { - request.url.encodedPath.contains("invocation/next") -> respond( + path.contains("invocation/next") -> respond( content = ByteReadChannel("""Hello world"""), status = HttpStatusCode.OK, headers = headers { append(HttpHeaders.ContentType, "application/json") - append("Lambda-Runtime-Aws-Request-Id", requestId) + append("Lambda-Runtime-Aws-Request-Id", awsRequestId) append("Lambda-Runtime-Deadline-Ms", deadline) append("Lambda-Runtime-Invoked-Function-Arn", "arn") } ) - else -> respond( + path.contains("/invocation/${awsRequestId}/response") -> respond( content = ByteReadChannel("""{"ip":"127.0.0.1"}"""), status = HttpStatusCode.OK, headers = headersOf(HttpHeaders.ContentType, "application/json") ) + + else -> respondBadRequest() } }) @@ -64,7 +73,10 @@ class LambdaRunnerTest { override suspend fun handleRequest(input: String, context: Context): String = handlerResponse } - lambdaRunner.run { handler } + lambdaRunner.run(singleEventMode = true) { handler } + + verify(VerifyMode.not) { log.log(LogLevel.ERROR, any(), any()) } + verify(VerifyMode.not) { log.log(LogLevel.FATAL, any(), any()) } } @OptIn(ExperimentalForeignApi::class) @@ -84,6 +96,6 @@ class LambdaRunnerTest { private fun createRunner(mockEngine: HttpClientEngine): Runner { val lambdaClient = LambdaClient(LambdaRuntime.createHttpClient(mockEngine)) - return Runner(lambdaClient) + return Runner(lambdaClient, log) } } \ No newline at end of file diff --git a/sample/src/nativeMain/kotlin/com/github/trueangle/knative/lambda/runtime/sample/handler/ByteBodyLambdaHandler.kt b/sample/src/nativeMain/kotlin/com/github/trueangle/knative/lambda/runtime/sample/handler/ByteBodyLambdaHandler.kt index 561f9e4..cf7e3d7 100644 --- a/sample/src/nativeMain/kotlin/com/github/trueangle/knative/lambda/runtime/sample/handler/ByteBodyLambdaHandler.kt +++ b/sample/src/nativeMain/kotlin/com/github/trueangle/knative/lambda/runtime/sample/handler/ByteBodyLambdaHandler.kt @@ -4,6 +4,7 @@ import io.github.trueangle.knative.lambda.runtime.api.Context import io.github.trueangle.knative.lambda.runtime.handler.LambdaBufferedHandler import io.github.trueangle.knative.lambda.runtime.handler.LambdaHandler import io.github.trueangle.knative.lambda.runtime.log.Log +import io.github.trueangle.knative.lambda.runtime.log.debug import io.ktor.utils.io.core.toByteArray class ByteBodyLambdaHandler : LambdaBufferedHandler { diff --git a/sample/src/nativeMain/kotlin/com/github/trueangle/knative/lambda/runtime/sample/handler/ObjectBodyLambdaHandler.kt b/sample/src/nativeMain/kotlin/com/github/trueangle/knative/lambda/runtime/sample/handler/ObjectBodyLambdaHandler.kt index 675f725..eb40894 100644 --- a/sample/src/nativeMain/kotlin/com/github/trueangle/knative/lambda/runtime/sample/handler/ObjectBodyLambdaHandler.kt +++ b/sample/src/nativeMain/kotlin/com/github/trueangle/knative/lambda/runtime/sample/handler/ObjectBodyLambdaHandler.kt @@ -1,12 +1,9 @@ package com.github.trueangle.knative.lambda.runtime.sample.handler import io.github.trueangle.knative.lambda.runtime.api.Context -import io.github.trueangle.knative.lambda.runtime.events.apigateway.APIGatewayProxy import io.github.trueangle.knative.lambda.runtime.events.apigateway.APIGatewayV2Request import io.github.trueangle.knative.lambda.runtime.events.apigateway.APIGatewayV2Response import io.github.trueangle.knative.lambda.runtime.handler.LambdaBufferedHandler -import io.github.trueangle.knative.lambda.runtime.handler.LambdaHandler -import io.github.trueangle.knative.lambda.runtime.log.Log import kotlinx.serialization.Serializable class ObjectBodyLambdaHandler : LambdaBufferedHandler { From c50c9b48be8df257000a2dd6fd69f7529b275871 Mon Sep 17 00:00:00 2001 From: Viacheslav Ivanovichev Date: Tue, 27 Aug 2024 17:03:01 +0200 Subject: [PATCH 3/3] More tests --- lambda-runtime/build.gradle.kts | 11 +- .../trueangle/knative/lambda/runtime/Ext.kt | 2 +- .../lambda/runtime/LambdaEnvironment.kt | 57 +++-- .../knative/lambda/runtime/LambdaRuntime.kt | 14 +- .../lambda/runtime/api/LambdaRuntimeClient.kt | 28 ++- .../knative/lambda/runtime/log/LogLevel.kt | 2 +- .../lambda/runtime/JsonLogFormatterTest.kt | 2 - .../lambda/runtime/LambdaRunnerTest.kt | 101 -------- .../lambda/runtime/LambdaRuntimeTest.kt | 230 ++++++++++++++++++ .../trueangle/knative/lambda/runtime/Mocks.kt | 6 +- .../resources/example-apigw-request.json | 135 ++++++++++ 11 files changed, 434 insertions(+), 154 deletions(-) delete mode 100644 lambda-runtime/src/nativeTest/kotlin/io/github/trueangle/knative/lambda/runtime/LambdaRunnerTest.kt create mode 100644 lambda-runtime/src/nativeTest/kotlin/io/github/trueangle/knative/lambda/runtime/LambdaRuntimeTest.kt create mode 100644 lambda-runtime/src/nativeTest/resources/example-apigw-request.json diff --git a/lambda-runtime/build.gradle.kts b/lambda-runtime/build.gradle.kts index cece1ca..6117a00 100644 --- a/lambda-runtime/build.gradle.kts +++ b/lambda-runtime/build.gradle.kts @@ -4,7 +4,7 @@ plugins { alias(libs.plugins.kotlin.multiplatform) alias(libs.plugins.kotlin.serialization) alias(libs.plugins.mokkery) - alias(libs.plugins.allopen) + alias(libs.plugins.kotlinx.resources) } kotlin { @@ -31,20 +31,15 @@ kotlin { } nativeTest.dependencies { + implementation(projects.lambdaEvents) implementation(libs.kotlin.test) implementation(libs.kotlin.coroutines.test) implementation(libs.ktor.client.mock) + implementation(libs.kotlinx.resources) } } } -fun isTestingTask(name: String) = name.endsWith("Test") -val isTesting = gradle.startParameter.taskNames.any(::isTestingTask) - -if (isTesting) allOpen { - annotation("kotlin.Metadata") -} - mokkery { defaultMockMode.set(MockMode.autoUnit) } diff --git a/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/Ext.kt b/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/Ext.kt index 028b83a..b872ce4 100644 --- a/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/Ext.kt +++ b/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/Ext.kt @@ -11,4 +11,4 @@ internal fun Throwable.prettyPrint(includeStackTrace: Boolean = true) = buildStr } } -internal fun unsafeLazy(initializer: () -> T): Lazy = lazy(LazyThreadSafetyMode.NONE, initializer) +internal fun unsafeLazy(initializer: () -> T): Lazy = lazy(LazyThreadSafetyMode.NONE, initializer) \ No newline at end of file diff --git a/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/LambdaEnvironment.kt b/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/LambdaEnvironment.kt index 914aa14..fb8c581 100644 --- a/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/LambdaEnvironment.kt +++ b/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/LambdaEnvironment.kt @@ -11,33 +11,40 @@ import io.github.trueangle.knative.lambda.runtime.ReservedRuntimeEnvironmentVari import kotlinx.cinterop.ExperimentalForeignApi import kotlinx.cinterop.toKString import platform.posix.getenv +import kotlin.system.exitProcess -@OptIn(ExperimentalForeignApi::class) @PublishedApi -internal object LambdaEnvironment { - val FUNCTION_MEMORY_SIZE by unsafeLazy { - getenv(AWS_LAMBDA_FUNCTION_MEMORY_SIZE)?.toKString()?.toIntOrNull() ?: 128 - } - val LOG_GROUP_NAME by unsafeLazy { - getenv(AWS_LAMBDA_LOG_GROUP_NAME)?.toKString().orEmpty() - } - val LOG_STREAM_NAME by unsafeLazy { - getenv(AWS_LAMBDA_LOG_STREAM_NAME)?.toKString().orEmpty() - } - val LAMBDA_LOG_LEVEL by unsafeLazy { - getenv(AWS_LAMBDA_LOG_LEVEL)?.toKString() ?: "INFO" - } - val LAMBDA_LOG_FORMAT by unsafeLazy { - getenv(AWS_LAMBDA_LOG_FORMAT)?.toKString() ?: "TEXT" - } - val FUNCTION_NAME by unsafeLazy { - getenv(AWS_LAMBDA_FUNCTION_NAME)?.toKString().orEmpty() - } - val FUNCTION_VERSION by unsafeLazy { - getenv(AWS_LAMBDA_FUNCTION_VERSION)?.toKString().orEmpty() - } - val RUNTIME_API by unsafeLazy { - getenv(AWS_LAMBDA_RUNTIME_API)?.toKString() +internal open class LambdaEnvironment { + // open due to Mokkery limits + open fun terminate(): Nothing = exitProcess(1) + + @OptIn(ExperimentalForeignApi::class) + @PublishedApi + internal companion object Variables { + val FUNCTION_MEMORY_SIZE by unsafeLazy { + getenv(AWS_LAMBDA_FUNCTION_MEMORY_SIZE)?.toKString()?.toIntOrNull() ?: 128 + } + val LOG_GROUP_NAME by unsafeLazy { + getenv(AWS_LAMBDA_LOG_GROUP_NAME)?.toKString().orEmpty() + } + val LOG_STREAM_NAME by unsafeLazy { + getenv(AWS_LAMBDA_LOG_STREAM_NAME)?.toKString().orEmpty() + } + val LAMBDA_LOG_LEVEL by unsafeLazy { + getenv(AWS_LAMBDA_LOG_LEVEL)?.toKString() ?: "INFO" + } + val LAMBDA_LOG_FORMAT by unsafeLazy { + getenv(AWS_LAMBDA_LOG_FORMAT)?.toKString() ?: "TEXT" + } + val FUNCTION_NAME by unsafeLazy { + getenv(AWS_LAMBDA_FUNCTION_NAME)?.toKString().orEmpty() + } + val FUNCTION_VERSION by unsafeLazy { + getenv(AWS_LAMBDA_FUNCTION_VERSION)?.toKString().orEmpty() + } + val RUNTIME_API by unsafeLazy { + getenv(AWS_LAMBDA_RUNTIME_API)?.toKString() + } } } diff --git a/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/LambdaRuntime.kt b/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/LambdaRuntime.kt index 687911d..e8541c7 100644 --- a/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/LambdaRuntime.kt +++ b/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/LambdaRuntime.kt @@ -3,6 +3,7 @@ package io.github.trueangle.knative.lambda.runtime import io.github.trueangle.knative.lambda.runtime.LambdaEnvironmentException.NonRecoverableStateException import io.github.trueangle.knative.lambda.runtime.api.Context import io.github.trueangle.knative.lambda.runtime.api.LambdaClient +import io.github.trueangle.knative.lambda.runtime.api.LambdaClientImpl import io.github.trueangle.knative.lambda.runtime.handler.LambdaBufferedHandler import io.github.trueangle.knative.lambda.runtime.handler.LambdaHandler import io.github.trueangle.knative.lambda.runtime.handler.LambdaStreamHandler @@ -29,7 +30,6 @@ import io.ktor.utils.io.writeStringUtf8 import kotlinx.coroutines.runBlocking import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json -import kotlin.system.exitProcess object LambdaRuntime { @OptIn(ExperimentalSerializationApi::class) @@ -37,7 +37,7 @@ object LambdaRuntime { inline fun run(crossinline initHandler: () -> LambdaHandler) = runBlocking { val curlHttpClient = createHttpClient(Curl.create()) - val lambdaClient = LambdaClient(curlHttpClient) + val lambdaClient = LambdaClientImpl(curlHttpClient) Runner(client = lambdaClient, log = Log).run(false, initHandler) } @@ -59,7 +59,8 @@ object LambdaRuntime { @PublishedApi internal class Runner( val client: LambdaClient, - val log: LambdaLogger + val log: LambdaLogger, + val env: LambdaEnvironment = LambdaEnvironment() ) { suspend inline fun run(singleEventMode: Boolean = false, crossinline initHandler: () -> LambdaHandler) { val handler = try { @@ -70,7 +71,8 @@ internal class Runner( log.fatal(e) client.reportError(e.asInitError()) - exitProcess(1) + + env.terminate() } val handlerName = handler::class.simpleName @@ -116,7 +118,7 @@ internal class Runner( is NonRecoverableStateException -> { log.fatal(e) - exitProcess(1) + env.terminate() } else -> log.error(e) @@ -124,7 +126,7 @@ internal class Runner( } catch (e: Throwable) { log.fatal(e) - exitProcess(1) + env.terminate() } if (singleEventMode) { diff --git a/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/api/LambdaRuntimeClient.kt b/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/api/LambdaRuntimeClient.kt index 89746b6..8a8441f 100644 --- a/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/api/LambdaRuntimeClient.kt +++ b/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/api/LambdaRuntimeClient.kt @@ -27,14 +27,22 @@ import kotlin.time.Duration.Companion.minutes import io.ktor.http.ContentType.Application.Json as ContentTypeJson @PublishedApi -internal class LambdaClient(private val httpClient: HttpClient) { +internal interface LambdaClient { + suspend fun retrieveNextEvent(bodyType: TypeInfo): Pair + suspend fun sendResponse(event: Context, body: T, bodyType: TypeInfo): HttpResponse + suspend fun streamResponse(event: Context, outgoingContent: OutgoingContent): HttpResponse + suspend fun reportError(error: LambdaRuntimeException) +} + +@PublishedApi +internal class LambdaClientImpl(private val httpClient: HttpClient): LambdaClient { private val baseUrl = requireNotNull(LambdaEnvironment.RUNTIME_API) { "Can't find AWS_LAMBDA_RUNTIME_API env variable" } private val invokeUrl = "http://$baseUrl/2018-06-01/runtime" private val requestTimeout = 15.minutes.inWholeMilliseconds - suspend fun retrieveNextEvent(bodyType: TypeInfo): Pair { + override suspend fun retrieveNextEvent(bodyType: TypeInfo): Pair { val response = httpClient.get { url("${invokeUrl}/invocation/next") @@ -50,7 +58,7 @@ internal class LambdaClient(private val httpClient: HttpClient) { return body to context } - suspend fun sendResponse(event: Context, body: T, bodyType: TypeInfo): HttpResponse { + override suspend fun sendResponse(event: Context, body: T, bodyType: TypeInfo): HttpResponse { val response = httpClient.post { url("${invokeUrl}/invocation/${event.awsRequestId}/response") contentType(ContentTypeJson) @@ -67,7 +75,7 @@ internal class LambdaClient(private val httpClient: HttpClient) { return validateResponse(response) } - suspend fun streamResponse(event: Context, outgoingContent: OutgoingContent): HttpResponse { + override suspend fun streamResponse(event: Context, outgoingContent: OutgoingContent): HttpResponse { val response = httpClient.post { url("${invokeUrl}/invocation/${event.awsRequestId}/response") @@ -92,13 +100,15 @@ internal class LambdaClient(private val httpClient: HttpClient) { return response } - suspend fun reportError(error: LambdaRuntimeException) { - val response = when (error) { + override suspend fun reportError(error: LambdaRuntimeException) { + when (error) { is LambdaRuntimeException.Init -> sendInitError(error) - is LambdaRuntimeException.Invocation -> sendInvocationError(error) - } + is LambdaRuntimeException.Invocation -> { + val response = sendInvocationError(error) - validateResponse(response) + validateResponse(response) + } + } } private suspend fun sendInvocationError( diff --git a/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/log/LogLevel.kt b/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/log/LogLevel.kt index 53850bc..c16d91b 100644 --- a/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/log/LogLevel.kt +++ b/lambda-runtime/src/commonMain/kotlin/io/github/trueangle/knative/lambda/runtime/log/LogLevel.kt @@ -29,7 +29,7 @@ enum class LogLevel { companion object { fun fromEnv(): LogLevel { - val level = LambdaEnvironment.LAMBDA_LOG_LEVEL ?: "INFO" + val level = LambdaEnvironment.LAMBDA_LOG_LEVEL return runCatching { valueOf(level) }.getOrElse { diff --git a/lambda-runtime/src/nativeTest/kotlin/io/github/trueangle/knative/lambda/runtime/JsonLogFormatterTest.kt b/lambda-runtime/src/nativeTest/kotlin/io/github/trueangle/knative/lambda/runtime/JsonLogFormatterTest.kt index f7850b6..e5c1728 100644 --- a/lambda-runtime/src/nativeTest/kotlin/io/github/trueangle/knative/lambda/runtime/JsonLogFormatterTest.kt +++ b/lambda-runtime/src/nativeTest/kotlin/io/github/trueangle/knative/lambda/runtime/JsonLogFormatterTest.kt @@ -101,7 +101,5 @@ class JsonLogFormatterTest { assertEquals(expected, actual) } - @Serializable - private data class SampleObject(val hello: String) private data class NonSerialObject(val hello: String) } \ No newline at end of file diff --git a/lambda-runtime/src/nativeTest/kotlin/io/github/trueangle/knative/lambda/runtime/LambdaRunnerTest.kt b/lambda-runtime/src/nativeTest/kotlin/io/github/trueangle/knative/lambda/runtime/LambdaRunnerTest.kt deleted file mode 100644 index 46528f7..0000000 --- a/lambda-runtime/src/nativeTest/kotlin/io/github/trueangle/knative/lambda/runtime/LambdaRunnerTest.kt +++ /dev/null @@ -1,101 +0,0 @@ -package io.github.trueangle.knative.lambda.runtime - -import dev.mokkery.matcher.any -import dev.mokkery.mock -import dev.mokkery.verify -import dev.mokkery.verify.VerifyMode -import io.github.trueangle.knative.lambda.runtime.ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_FUNCTION_NAME -import io.github.trueangle.knative.lambda.runtime.ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_FUNCTION_VERSION -import io.github.trueangle.knative.lambda.runtime.ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_RUNTIME_API -import io.github.trueangle.knative.lambda.runtime.api.Context -import io.github.trueangle.knative.lambda.runtime.api.LambdaClient -import io.github.trueangle.knative.lambda.runtime.handler.LambdaBufferedHandler -import io.github.trueangle.knative.lambda.runtime.log.LambdaLogger -import io.github.trueangle.knative.lambda.runtime.log.LogLevel -import io.ktor.client.engine.HttpClientEngine -import io.ktor.client.engine.mock.MockEngine -import io.ktor.client.engine.mock.respond -import io.ktor.client.engine.mock.respondBadRequest -import io.ktor.http.HttpHeaders -import io.ktor.http.HttpStatusCode -import io.ktor.http.headers -import io.ktor.http.headersOf -import io.ktor.utils.io.ByteReadChannel -import kotlinx.cinterop.ExperimentalForeignApi -import kotlinx.cinterop.toKString -import kotlinx.coroutines.test.runTest -import platform.posix.getenv -import platform.posix.setenv -import kotlin.test.BeforeTest -import kotlin.test.Test -import kotlin.test.assertFailsWith - -class LambdaRunnerTest { - private val log = mock() - - @BeforeTest - fun setup() { - mockEnvironment() - } - - @Test - fun `GIVEN string event WHEN LambdaBufferedHandler THEN success invocation`() = runTest { - val handlerResponse = "Response" - val awsRequestId = "156cb537-e2d4-11e8-9b34-d36013741fb9" - val deadline = "1542409706888" - - val lambdaRunner = createRunner(MockEngine { request -> - val path = request.url.encodedPath - when { - path.contains("invocation/next") -> respond( - content = ByteReadChannel("""Hello world"""), - status = HttpStatusCode.OK, - headers = headers { - append(HttpHeaders.ContentType, "application/json") - append("Lambda-Runtime-Aws-Request-Id", awsRequestId) - append("Lambda-Runtime-Deadline-Ms", deadline) - append("Lambda-Runtime-Invoked-Function-Arn", "arn") - - } - ) - - path.contains("/invocation/${awsRequestId}/response") -> respond( - content = ByteReadChannel("""{"ip":"127.0.0.1"}"""), - status = HttpStatusCode.OK, - headers = headersOf(HttpHeaders.ContentType, "application/json") - ) - - else -> respondBadRequest() - } - }) - - val handler = object : LambdaBufferedHandler { - override suspend fun handleRequest(input: String, context: Context): String = handlerResponse - } - - lambdaRunner.run(singleEventMode = true) { handler } - - verify(VerifyMode.not) { log.log(LogLevel.ERROR, any(), any()) } - verify(VerifyMode.not) { log.log(LogLevel.FATAL, any(), any()) } - } - - @OptIn(ExperimentalForeignApi::class) - private fun mockEnvironment() { - if (getenv(AWS_LAMBDA_FUNCTION_NAME)?.toKString().isNullOrEmpty()) { - setenv(AWS_LAMBDA_FUNCTION_NAME, "test", 1) - } - - if (getenv(AWS_LAMBDA_FUNCTION_VERSION)?.toKString().isNullOrEmpty()) { - setenv(AWS_LAMBDA_FUNCTION_VERSION, "1", 1) - } - - if (getenv(AWS_LAMBDA_RUNTIME_API)?.toKString().isNullOrEmpty()) { - setenv(AWS_LAMBDA_RUNTIME_API, "127.0.0.1", 1) - } - } - - private fun createRunner(mockEngine: HttpClientEngine): Runner { - val lambdaClient = LambdaClient(LambdaRuntime.createHttpClient(mockEngine)) - return Runner(lambdaClient, log) - } -} \ No newline at end of file diff --git a/lambda-runtime/src/nativeTest/kotlin/io/github/trueangle/knative/lambda/runtime/LambdaRuntimeTest.kt b/lambda-runtime/src/nativeTest/kotlin/io/github/trueangle/knative/lambda/runtime/LambdaRuntimeTest.kt new file mode 100644 index 0000000..3174fd9 --- /dev/null +++ b/lambda-runtime/src/nativeTest/kotlin/io/github/trueangle/knative/lambda/runtime/LambdaRuntimeTest.kt @@ -0,0 +1,230 @@ +package io.github.trueangle.knative.lambda.runtime + +import com.goncalossilva.resources.Resource +import dev.mokkery.answering.throws +import dev.mokkery.every +import dev.mokkery.matcher.any +import dev.mokkery.matcher.eq +import dev.mokkery.mock +import dev.mokkery.spy +import dev.mokkery.verify +import dev.mokkery.verify.VerifyMode.Companion.exactly +import dev.mokkery.verify.VerifyMode.Companion.not +import dev.mokkery.verifySuspend +import io.github.trueangle.knative.lambda.runtime.ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_FUNCTION_NAME +import io.github.trueangle.knative.lambda.runtime.ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_FUNCTION_VERSION +import io.github.trueangle.knative.lambda.runtime.ReservedRuntimeEnvironmentVariables.AWS_LAMBDA_RUNTIME_API +import io.github.trueangle.knative.lambda.runtime.api.Context +import io.github.trueangle.knative.lambda.runtime.api.LambdaClient +import io.github.trueangle.knative.lambda.runtime.api.LambdaClientImpl +import io.github.trueangle.knative.lambda.runtime.events.apigateway.APIGatewayRequest +import io.github.trueangle.knative.lambda.runtime.handler.LambdaBufferedHandler +import io.github.trueangle.knative.lambda.runtime.log.LambdaLogger +import io.github.trueangle.knative.lambda.runtime.log.LogLevel.ERROR +import io.github.trueangle.knative.lambda.runtime.log.LogLevel.FATAL +import io.ktor.client.engine.HttpClientEngine +import io.ktor.client.engine.mock.MockEngine +import io.ktor.client.engine.mock.respond +import io.ktor.client.engine.mock.respondBadRequest +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode +import io.ktor.http.headers +import io.ktor.http.headersOf +import io.ktor.util.reflect.typeInfo +import io.ktor.utils.io.ByteReadChannel +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.toKString +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.Json +import platform.posix.getenv +import platform.posix.setenv +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertFailsWith + +const val RESOURCES_PATH = "src/nativeTest/resources" + +class LambdaRuntimeTest { + private val log = mock() + private val context = Context( + awsRequestId = "156cb537-e2d4-11e8-9b34-d36013741fb9", + deadlineTimeInMs = 1542409706888L, + invokedFunctionArn = "arn", + clientContext = null, + cognitoIdentity = null, + invokedFunctionName = "invokedFunctionName", + invokedFunctionVersion = "1", + memoryLimitMb = 128, + xrayTracingId = null + ) + + @BeforeTest + fun setup() { + mockEnvironment() + } + + @Test + fun `GIVEN String event WHEN end-to-end THEN behave correctly`() = runTest { + val lambdaEvent = "Hello world" + val handlerResponse = "Response" + + val lambdaRunner = createRunner(MockEngine { request -> + val path = request.url.encodedPath + when { + path.contains("invocation/next") -> respond( + content = ByteReadChannel(lambdaEvent), + status = HttpStatusCode.OK, + headers = headers { + append(HttpHeaders.ContentType, "application/json") + append("Lambda-Runtime-Aws-Request-Id", context.awsRequestId) + append("Lambda-Runtime-Deadline-Ms", context.deadlineTimeInMs.toString()) + append("Lambda-Runtime-Invoked-Function-Arn", context.invokedFunctionArn) + + } + ) + + path.contains("/invocation/${context.awsRequestId}/response") -> respond( + content = ByteReadChannel("Ok"), + status = HttpStatusCode.Accepted, + headers = headersOf(HttpHeaders.ContentType, "application/json") + ) + + else -> respondBadRequest() + } + }) + val client = lambdaRunner.client + + val handler = object : LambdaBufferedHandler { + override suspend fun handleRequest(input: String, context: Context): String = handlerResponse + } + + lambdaRunner.run(singleEventMode = true) { handler } + + verifySuspend { handler.handleRequest(lambdaEvent, context) } + verifySuspend(exactly(1)) { client.retrieveNextEvent(typeInfo()) } + verifySuspend(exactly(1)) { client.sendResponse(context, handlerResponse, typeInfo()) } + verify(not) { log.log(ERROR, any(), any()) } + verify(not) { log.log(FATAL, any(), any()) } + } + + @Test + fun `GIVEN complex object event and response WHEN end-to-end THEN behave correctly`() = runTest { + val lambdaEventJson = Resource("$RESOURCES_PATH/example-apigw-request.json").readText() + val apiGatewayRequest = Json.decodeFromString(lambdaEventJson) + val handlerResponse = SampleObject("Hello world") + + val lambdaRunner = createRunner(MockEngine { request -> + val path = request.url.encodedPath + when { + path.contains("invocation/next") -> respond( + content = ByteReadChannel(lambdaEventJson), + status = HttpStatusCode.OK, + headers = headers { + append(HttpHeaders.ContentType, "application/json") + append("Lambda-Runtime-Aws-Request-Id", context.awsRequestId) + append("Lambda-Runtime-Deadline-Ms", context.deadlineTimeInMs.toString()) + append("Lambda-Runtime-Invoked-Function-Arn", context.invokedFunctionArn) + } + ) + + path.contains("/invocation/${context.awsRequestId}/response") -> respond( + content = ByteReadChannel("Ok"), + status = HttpStatusCode.Accepted, + headers = headersOf(HttpHeaders.ContentType, "application/json") + ) + + else -> respondBadRequest() + } + }) + val client = lambdaRunner.client + val handler = object : LambdaBufferedHandler { + override suspend fun handleRequest(input: APIGatewayRequest, context: Context) = handlerResponse + } + + lambdaRunner.run(singleEventMode = true) { handler } + + verifySuspend { handler.handleRequest(apiGatewayRequest, context) } + verifySuspend(exactly(1)) { client.retrieveNextEvent(typeInfo()) } + verifySuspend(exactly(1)) { client.sendResponse(context, handlerResponse, typeInfo()) } + verify(not) { log.log(ERROR, any(), any()) } + verify(not) { log.log(FATAL, any(), any()) } + } + + @Test + fun `GIVEN handler init error WHEN end-to-end THEN terminate immediately AND report init error`() = runTest { + val lambdaRunner = createRunner(MockEngine { request -> + val path = request.url.encodedPath + when { + path.contains("init/error") -> respond( + content = ByteReadChannel(""), + status = HttpStatusCode.Accepted, + ) + + else -> respondBadRequest() + } + }) + val client = lambdaRunner.client + + assertFailsWith { + lambdaRunner.run(singleEventMode = true) { InitErrorHandler() } + } + verifySuspend { client.reportError(any()) } + verifySuspend(not) { client.retrieveNextEvent(typeInfo()) } + verify(not) { log.log(ERROR, any(), any()) } + verify { log.log(FATAL, any(), any()) } + } + + @Test + fun `GIVEN handler init error AND report init failed WHEN end-to-end THEN terminate immediately`() = runTest { + val lambdaRunner = createRunner(MockEngine { request -> + val path = request.url.encodedPath + when { + path.contains("init/error") -> respondBadRequest() + else -> respondBadRequest() + } + }) + val client = lambdaRunner.client + + assertFailsWith { + lambdaRunner.run(singleEventMode = true) { InitErrorHandler() } + } + verifySuspend { client.reportError(any()) } + verifySuspend(not) { client.retrieveNextEvent(typeInfo()) } + verify(not) { log.log(ERROR, any(), any()) } + verify { log.log(FATAL, any(), any()) } + } + + @OptIn(ExperimentalForeignApi::class) + private fun mockEnvironment() { + if (getenv(AWS_LAMBDA_FUNCTION_NAME)?.toKString().isNullOrEmpty()) { + setenv(AWS_LAMBDA_FUNCTION_NAME, context.invokedFunctionName, 1) + } + + if (getenv(AWS_LAMBDA_FUNCTION_VERSION)?.toKString().isNullOrEmpty()) { + setenv(AWS_LAMBDA_FUNCTION_VERSION, context.invokedFunctionVersion, 1) + } + + if (getenv(AWS_LAMBDA_RUNTIME_API)?.toKString().isNullOrEmpty()) { + setenv(AWS_LAMBDA_RUNTIME_API, "127.0.0.1", 1) + } + } + + private fun createRunner(mockEngine: HttpClientEngine): Runner { + val client = LambdaClientImpl(LambdaRuntime.createHttpClient(mockEngine)) + val env = mock { + every { terminate() } throws TerminateException() + } + val lambdaClient = spy(client) + return Runner(lambdaClient, log, env) + } + + private class InitErrorHandler : LambdaBufferedHandler { + init { + throw RuntimeException() + } + + override suspend fun handleRequest(input: String, context: Context) = "" + } + + private class TerminateException : Error() +} \ No newline at end of file diff --git a/lambda-runtime/src/nativeTest/kotlin/io/github/trueangle/knative/lambda/runtime/Mocks.kt b/lambda-runtime/src/nativeTest/kotlin/io/github/trueangle/knative/lambda/runtime/Mocks.kt index 7603cec..74dc89f 100644 --- a/lambda-runtime/src/nativeTest/kotlin/io/github/trueangle/knative/lambda/runtime/Mocks.kt +++ b/lambda-runtime/src/nativeTest/kotlin/io/github/trueangle/knative/lambda/runtime/Mocks.kt @@ -1,6 +1,7 @@ package io.github.trueangle.knative.lambda.runtime import io.github.trueangle.knative.lambda.runtime.api.Context +import kotlinx.serialization.Serializable internal fun mockContext(awsRequestId: String = "awsRequestId") = Context( awsRequestId = awsRequestId, @@ -12,4 +13,7 @@ internal fun mockContext(awsRequestId: String = "awsRequestId") = Context( memoryLimitMb = 512, clientContext = null, cognitoIdentity = null -) \ No newline at end of file +) + +@Serializable +internal data class SampleObject(val hello: String) \ No newline at end of file diff --git a/lambda-runtime/src/nativeTest/resources/example-apigw-request.json b/lambda-runtime/src/nativeTest/resources/example-apigw-request.json new file mode 100644 index 0000000..d91e960 --- /dev/null +++ b/lambda-runtime/src/nativeTest/resources/example-apigw-request.json @@ -0,0 +1,135 @@ +{ + "resource": "/{proxy+}", + "path": "/hello/world", + "httpMethod": "POST", + "headers": { + "Accept": "*/*", + "Accept-Encoding": "gzip, deflate", + "cache-control": "no-cache", + "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", + "Content-Type": "application/json", + "headerName": "headerValue", + "Host": "gy415nuibc.execute-api.us-east-1.amazonaws.com", + "Postman-Token": "9f583ef0-ed83-4a38-aef3-eb9ce3f7a57f", + "User-Agent": "PostmanRuntime/2.4.5", + "Via": "1.1 d98420743a69852491bbdea73f7680bd.cloudfront.net (CloudFront)", + "X-Amz-Cf-Id": "pn-PWIJc6thYnZm5P0NMgOUglL1DYtl0gdeJky8tqsg8iS_sgsKD1A==", + "X-Forwarded-For": "54.240.196.186, 54.182.214.83", + "X-Forwarded-Port": "443", + "X-Forwarded-Proto": "https" + }, + "multiValueHeaders": { + "Accept": [ + "*/*" + ], + "Accept-Encoding": [ + "gzip, deflate" + ], + "cache-control": [ + "no-cache" + ], + "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" + ], + "Content-Type": [ + "application/json" + ], + "headerName": [ + "headerValue" + ], + "Host": [ + "gy415nuibc.execute-api.us-east-1.amazonaws.com" + ], + "Postman-Token": [ + "9f583ef0-ed83-4a38-aef3-eb9ce3f7a57f" + ], + "User-Agent": [ + "PostmanRuntime/2.4.5" + ], + "Via": [ + "1.1 d98420743a69852491bbdea73f7680bd.cloudfront.net (CloudFront)" + ], + "X-Amz-Cf-Id": [ + "pn-PWIJc6thYnZm5P0NMgOUglL1DYtl0gdeJky8tqsg8iS_sgsKD1A==" + ], + "X-Forwarded-For": [ + "54.240.196.186, 54.182.214.83" + ], + "X-Forwarded-Port": [ + "443" + ], + "X-Forwarded-Proto": [ + "https" + ] + }, + "queryStringParameters": { + "name": "me" + }, + "multiValueQueryStringParameters": { + "name": [ + "me" + ] + }, + "pathParameters": { + "proxy": "hello/world" + }, + "stageVariables": { + "stageVariableName": "stageVariableValue" + }, + "requestContext": { + "accountId": "12345678912", + "resourceId": "roq9wj", + "path": "/hello/world", + "stage": "testStage", + "domainName": "gy415nuibc.execute-api.us-east-2.amazonaws.com", + "domainPrefix": "y0ne18dixk", + "requestId": "deef4878-7910-11e6-8f14-25afc3e9ae33", + "protocol": "HTTP/1.1", + "identity": { + "cognitoIdentityPoolId": "theCognitoIdentityPoolId", + "accountId": "theAccountId", + "cognitoIdentityId": "theCognitoIdentityId", + "caller": "theCaller", + "apiKey": "theApiKey", + "apiKeyId": "theApiKeyId", + "accessKey": "ANEXAMPLEOFACCESSKEY", + "sourceIp": "192.168.196.186", + "cognitoAuthenticationType": "theCognitoAuthenticationType", + "cognitoAuthenticationProvider": "theCognitoAuthenticationProvider", + "userArn": "theUserArn", + "userAgent": "PostmanRuntime/2.4.5", + "user": "theUser" + }, + "authorizer": { + "principalId": "admin", + "clientId": 1, + "clientName": "Exata" + }, + "resourcePath": "/{proxy+}", + "httpMethod": "POST", + "requestTime": "15/May/2020:06:01:09 +0000", + "requestTimeEpoch": 1589522469693, + "apiId": "gy415nuibc" + }, + "body": "{\r\n\t\"a\": 1\r\n}" +} \ No newline at end of file