Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add handling of s3 events to kotless provider #114

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import org.jetbrains.kotlin.gradle.dsl.KotlinJvmCompile

group = "io.kotless"
version = "0.2.0"
version = "0.2.1"

plugins {
id("io.gitlab.arturbosch.detekt") version ("1.15.0") apply true
Expand Down
2 changes: 1 addition & 1 deletion buildSrc/src/main/kotlin/io/kotless/buildsrc/Versions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ object Versions {
//Note, that it also should be changed in dependencies of buildSrc and in plugins blocks
//Due to limitations of Gradle DSL
const val kotlin = "1.5.31"
const val serialization = "1.0.1"
const val serialization = "1.3.2"

const val aws = "1.11.788"
const val lambdaJavaCore = "1.2.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import java.util.*
* @param body payload of response
*/
@Serializable
data class HttpResponse(val statusCode: Int, val headers: HashMap<String, String> = HashMap(), val body: String?, val isBase64Encoded: Boolean) {
data class HttpResponse(val statusCode: Int, val headers: HashMap<String, String> = HashMap(), val body: String?, val isBase64Encoded: Boolean): Response(statusCode) {

constructor(statusCode: Int, headers: HashMap<String, String>, body: String) : this(statusCode, headers, body, false)
constructor(statusCode: Int, headers: HashMap<String, String>, body: ByteArray) : this(statusCode, headers, Base64.getEncoder().encodeToString(body), true)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.kotless.dsl.model

import kotlinx.serialization.Serializable

@Serializable
open class Response(val status: Int) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package io.kotless.dsl.model.events

import kotlinx.serialization.*
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.json.*
import org.slf4j.LoggerFactory

abstract class AwsEventGenerator {
abstract fun mayDeserialize(jsonObject: String): Boolean

abstract val serializer: KSerializer<out AwsEvent>
}

@Serializable(with = AwsEvent.AwsEventSerializer::class)
abstract class AwsEvent {

abstract fun records(): List<AwsEventInformation>

companion object {
private val logger = LoggerFactory.getLogger(AwsEventGenerator::class.java)

fun isEventRequest(jsonRequest: String): Boolean {
return eventKSerializers.any { it.mayDeserialize(jsonRequest) }
}

val eventKSerializers = mutableListOf<AwsEventGenerator>()
}

@Serializer(forClass = AwsEvent::class)
class AwsEventSerializer : DeserializationStrategy<AwsEvent> {
override val descriptor: SerialDescriptor =
buildClassSerialDescriptor("io.kotless.dsl.model.events.AwsEventInformation")

private fun selectDeserializer(element: JsonElement): List<DeserializationStrategy<out AwsEvent>> {
return eventKSerializers.filter { it.mayDeserialize(element.jsonObject.toString()) }.map { it.serializer }
}

override fun deserialize(decoder: Decoder): AwsEvent {
val input = decoder as? JsonDecoder
val tree = input?.decodeJsonElement() ?: error("")
val serializers = selectDeserializer(tree)
serializers.forEach { serializer ->
@Suppress("UNCHECKED_CAST")
val actualSerializer = serializer as KSerializer<AwsEvent>
try {
return input.json.decodeFromJsonElement(actualSerializer, tree)
} catch (e: SerializationException) {
logger.warn("Error while deserialization", e)
}
}
error("Failed to define serializer for event: $tree")
}
}
}


abstract class AwsEventInformation {
abstract val parameters: Map<String, String>

abstract val eventSource: String

abstract val path: String
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package io.kotless.dsl.model.events

import io.kotless.CloudwatchEventType
import io.kotless.InternalAPI
import kotlinx.serialization.*
import kotlinx.serialization.encoding.Decoder
import org.slf4j.LoggerFactory


class CloudwatchEventInformationGenerator : AwsEventGenerator() {
override fun mayDeserialize(jsonObject: String): Boolean {
return jsonObject.contains(CloudwatchEventInformation.eventSource)
}

override val serializer: KSerializer<out AwsEvent> = CloudwatchEvent.serializer()
}


@Serializable(with = CloudwatchEvent.CloudwatchEventSerializer::class)
class CloudwatchEvent(private val records: List<CloudwatchEventInformation>) : AwsEvent() {
override fun records(): List<AwsEventInformation> = records

@Serializer(forClass = CloudwatchEvent::class)
object CloudwatchEventSerializer {
override fun deserialize(decoder: Decoder): CloudwatchEvent {
val cloudwatchEventInformation = decoder.decodeSerializableValue(CloudwatchEventInformation.serializer())
return CloudwatchEvent(listOf(cloudwatchEventInformation))
}
}
}

@Serializable
data class CloudwatchEventInformation(
val id: String,
@SerialName("detail-type") val detailType: String,
val source: String,
val account: String,
val time: String,
val region: String,
val resources: List<String>,
val detail: Detail? = null,
val version: String? = null
) : AwsEventInformation() {

override val parameters: Map<String, String> = mapOf(
"id" to id,
"detailType" to detailType,
"source" to source,
"account" to account,
"time" to time,
"region" to region
)

override val path: String
get() {
val resource = resources.first().dropWhile { it != '/' }.drop(1)
return if (resource.contains(CloudwatchEventType.General.prefix)) {
resource.substring(resource.lastIndexOf(CloudwatchEventType.General.prefix))
} else {
CloudwatchEventType.Autowarm.prefix
}
}

override val eventSource: String = CloudwatchEventInformation.eventSource

companion object {
const val eventSource: String = "aws.events"
}


@Serializable
data class Detail(val instanceGroupId: String? = null)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package io.kotless.dsl.model.events

import kotlinx.serialization.*

class S3EventInformationGenerator : AwsEventGenerator() {
override fun mayDeserialize(jsonObject: String): Boolean {
return jsonObject.contains(S3EventInformation.eventSource)
ValchukDmitry marked this conversation as resolved.
Show resolved Hide resolved
}

override val serializer: KSerializer<out AwsEvent> = S3Event.serializer()
}

@Serializable
class S3Event(@SerialName("Records") val records: List<S3EventInformation>) : AwsEvent() {
override fun records(): List<AwsEventInformation> = records
}

@Serializable
data class S3EventInformation(
val eventTime: String,
val eventName: String,
val awsRegion: String,
val s3: S3Event,
) : AwsEventInformation() {
override val parameters: Map<String, String> = mapOf(
"bucket_name" to s3.bucket.name,
"bucket_arn" to s3.bucket.arn,
"object_name" to s3.s3Object.key,
"object_eTag" to s3.s3Object.eTag,
"object_size" to s3.s3Object.size.toString()
).filter { it.value != null }.mapValues { it.value!! }

override val path: String = "${s3.bucket.name}:${eventName}"
override val eventSource: String = S3EventInformation.eventSource

companion object {
const val eventSource: String = "aws:s3"
}


@Serializable
data class S3Event(val bucket: Bucket, @SerialName("object") val s3Object: S3Object) {
@Serializable
data class Bucket(val name: String, val arn: String)

@Serializable
data class S3Object(val key: String, val size: Long? = null, val eTag: String? = null, val versionId: String? = null)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package io.kotless.dsl.model.events

import kotlinx.serialization.*

class SQSEventInformationGenerator : AwsEventGenerator() {
override fun mayDeserialize(jsonObject: String): Boolean {
return jsonObject.contains(SQSEventInformation.eventSource)
}

override val serializer: KSerializer<out AwsEvent> = SQSEvent.serializer()
}


@Serializable
class SQSEvent(@SerialName("Records") val records: List<SQSEventInformation>): AwsEvent() {
override fun records(): List<AwsEventInformation> = records
}


@Serializable
data class SQSEventInformation(
val messageId: String,
val receiptHandle: String,
val body: String,
val attributes: SQSEventAttributes,
val md5OfBody: String,
val awsRegion: String,
val eventSourceARN: String,
// TODO: check this field one more time
val messageAttributes: Map<String, String>
) : AwsEventInformation() {
override val parameters: Map<String, String> = mapOf(
"body" to body,
"md5OfBody" to md5OfBody
)

override val path: String = eventSourceARN
override val eventSource: String = SQSEventInformation.eventSource

companion object {
val eventSource: String = "aws:sqs"
}

@Serializable
data class SQSEventAttributes(
@SerialName("ApproximateReceiveCount") val approximateReceiveCount: String,
@SerialName("SentTimestamp") val sentTimestamp: String,
@SerialName("SequenceNumber") val sequenceNumber: String? = null,
@SerialName("MessageGroupId") val messageGroupId: String? = null,
@SerialName("SenderId") val senderId: String,
@SerialName("MessageDeduplicationId") val messageDeduplicationId: String? = null,
@SerialName("ApproximateFirstReceiveTimestamp") val approximateFirstReceiveTimestamp: String
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ open class Parser(private val processors: Set<Processor<*>>) {
data class Result(val routes: Routes, val resources: Resources, val events: Events) {
data class Routes(val dynamics: Set<Application.API.DynamicRoute>, val statics: Set<Application.API.StaticRoute>)
data class Resources(val dynamics: TypedStorage<Lambda>, val statics: TypedStorage<StaticResource>)
data class Events(val scheduled: Set<Application.Events.Scheduled>)
data class Events(val events: Set<Application.Events.Event>)
}

fun parse(sources: Set<File>, resources: Set<File>, jar: File, config: KotlessConfig, lambda: Lambda.Config, libs: Set<File>): Result {
Expand All @@ -48,7 +48,7 @@ open class Parser(private val processors: Set<Processor<*>>) {
return Result(
Result.Routes(context.routes.dynamics, context.routes.statics),
Result.Resources(context.resources.dynamics, context.resources.statics),
Result.Events(context.events.scheduled)
Result.Events(context.events.events)
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,17 @@ class ProcessorContext(val jar: File, val config: KotlessConfig, val lambda: Lam

val routes = Routes()

class Events(private val myScheduled: MutableSet<Application.Events.Scheduled> = HashSet()) {
val scheduled: Set<Application.Events.Scheduled>
get() = myScheduled.toSet()
class Events(private val myEvents: MutableSet<Application.Events.Event> = HashSet()) {

val events: Set<Application.Events.Event>
get() = myEvents

fun register(scheduled: Application.Events.Scheduled) {
myScheduled.add(scheduled)
myEvents.add(scheduled)
}

fun register(event: Application.Events.Event) {
myEvents.add(event)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import com.amazonaws.services.lambda.runtime.Context
import com.amazonaws.services.lambda.runtime.RequestStreamHandler
import io.kotless.InternalAPI
import io.kotless.dsl.app.events.EventsDispatcher
import io.kotless.dsl.app.events.processors.CustomAwsEventGeneratorAnnotationProcessor
import io.kotless.dsl.app.http.RouteKey
import io.kotless.dsl.app.http.RoutesDispatcher
import io.kotless.dsl.cloud.aws.CloudWatch
import io.kotless.dsl.cloud.aws.model.AwsHttpRequest
import io.kotless.dsl.lang.http.serverError
import io.kotless.dsl.model.HttpResponse
import io.kotless.dsl.model.events.*
import io.kotless.dsl.utils.JSON
import kotlinx.serialization.KSerializer
import org.slf4j.LoggerFactory
import java.io.InputStream
import java.io.OutputStream
Expand All @@ -31,6 +33,17 @@ class HandlerAWS : RequestStreamHandler {
private val logger = LoggerFactory.getLogger(HandlerAWS::class.java)
}

fun registerAwsEvent(generator: AwsEventGenerator) {
AwsEvent.eventKSerializers.add(generator)
}

init {
registerAwsEvent(S3EventInformationGenerator())
registerAwsEvent(SQSEventInformationGenerator())
registerAwsEvent(CloudwatchEventInformationGenerator())
CustomAwsEventGeneratorAnnotationProcessor.process()
}

override fun handleRequest(input: InputStream, output: OutputStream, @Suppress("UNUSED_PARAMETER") any: Context?) {
val response = try {
val jsonRequest = input.bufferedReader().use { it.readText() }
Expand All @@ -40,15 +53,21 @@ class HandlerAWS : RequestStreamHandler {

Application.init()

if (jsonRequest.contains("Scheduled Event")) {
val event = JSON.parse(CloudWatch.serializer(), jsonRequest)
if (event.`detail-type` == "Scheduled Event" && event.source == "aws.events") {
logger.debug("Request is Scheduled Event")
EventsDispatcher.process(event)
return
}
if (AwsEvent.isEventRequest(jsonRequest)) {
val event = JSON.parse(AwsEvent.serializer(), jsonRequest)
EventsDispatcher.process(event)
return
}

// if (jsonRequest.contains("Scheduled Event")) {
// val event = JSON.parse(CloudWatch.serializer(), jsonRequest)
// if (event.`detail-type` == "Scheduled Event" && event.source == "aws.events") {
// logger.debug("Request is Scheduled Event")
// EventsDispatcher.process(event)
// return
// }
// }

logger.debug("Request is HTTP Event")

val request = JSON.parse(AwsHttpRequest.serializer(), jsonRequest)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package io.kotless.dsl.app.events

class AwsEventKey(val key: String) {
fun cover(other: AwsEventKey): Boolean {
val thisParts = key.split(":")
val otherParts = other.key.split(":")

if (thisParts.size != otherParts.size) return false

return thisParts.zip(otherParts).all { (part, other) ->
part == "*" || part == other
}
}
}
Loading