From 420c96662e96144af307e64a4314797f51706561 Mon Sep 17 00:00:00 2001 From: Piyush-t24 Date: Thu, 12 Mar 2026 13:36:25 +0530 Subject: [PATCH] Add CloudWatchLogsEvent and SecretsManagerRotationEvent (part of #48) --- .../lambda/events/CloudWatchLogsEvent.scala | 141 ++++++++++++++++++ .../events/SecretsManagerRotationEvent.scala | 75 ++++++++++ .../events/CloudWatchLogsEventSuite.scala | 81 ++++++++++ .../SecretsManagerRotationEventSuite.scala | 103 +++++++++++++ 4 files changed, 400 insertions(+) create mode 100644 lambda/shared/src/main/scala/feral/lambda/events/CloudWatchLogsEvent.scala create mode 100644 lambda/shared/src/main/scala/feral/lambda/events/SecretsManagerRotationEvent.scala create mode 100644 lambda/shared/src/test/scala/feral/lambda/events/CloudWatchLogsEventSuite.scala create mode 100644 lambda/shared/src/test/scala/feral/lambda/events/SecretsManagerRotationEventSuite.scala diff --git a/lambda/shared/src/main/scala/feral/lambda/events/CloudWatchLogsEvent.scala b/lambda/shared/src/main/scala/feral/lambda/events/CloudWatchLogsEvent.scala new file mode 100644 index 00000000..9bc720ad --- /dev/null +++ b/lambda/shared/src/main/scala/feral/lambda/events/CloudWatchLogsEvent.scala @@ -0,0 +1,141 @@ +/* + * Copyright 2021 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package feral.lambda.events + +import io.circe.Decoder + +// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/aws-lambda/trigger/cloudwatch-logs.d.ts +// https://docs.aws.amazon.com/lambda/latest/dg/eventsources.html#eventsources-cloudwatch-logs + +sealed abstract class CloudWatchLogsEvent { + def awslogs: CloudWatchLogsEventData +} + +object CloudWatchLogsEvent { + + def apply(awslogs: CloudWatchLogsEventData): CloudWatchLogsEvent = + new Impl(awslogs) + + implicit val decoder: Decoder[CloudWatchLogsEvent] = + Decoder.forProduct1("awslogs")(CloudWatchLogsEvent.apply) + + private final case class Impl(awslogs: CloudWatchLogsEventData) extends CloudWatchLogsEvent { + override def productPrefix = "CloudWatchLogsEvent" + } +} + +sealed abstract class CloudWatchLogsEventData { + def data: String +} + +object CloudWatchLogsEventData { + + def apply(data: String): CloudWatchLogsEventData = + new Impl(data) + + private[events] implicit val decoder: Decoder[CloudWatchLogsEventData] = + Decoder.forProduct1("data")(CloudWatchLogsEventData.apply) + + private final case class Impl(data: String) extends CloudWatchLogsEventData { + override def productPrefix = "CloudWatchLogsEventData" + } +} + +/** + * Decoded payload after base64-decode and gzip-decompress of `CloudWatchLogsEventData.data`. + */ +sealed abstract class CloudWatchLogsDecodedData { + def owner: String + def logGroup: String + def logStream: String + def subscriptionFilters: List[String] + def messageType: String + def logEvents: List[CloudWatchLogsLogEvent] +} + +object CloudWatchLogsDecodedData { + + def apply( + owner: String, + logGroup: String, + logStream: String, + subscriptionFilters: List[String], + messageType: String, + logEvents: List[CloudWatchLogsLogEvent] + ): CloudWatchLogsDecodedData = + new Impl( + owner, + logGroup, + logStream, + subscriptionFilters, + messageType, + logEvents + ) + + implicit val decoder: Decoder[CloudWatchLogsDecodedData] = + Decoder.forProduct6( + "owner", + "logGroup", + "logStream", + "subscriptionFilters", + "messageType", + "logEvents" + )(CloudWatchLogsDecodedData.apply) + + private final case class Impl( + owner: String, + logGroup: String, + logStream: String, + subscriptionFilters: List[String], + messageType: String, + logEvents: List[CloudWatchLogsLogEvent] + ) extends CloudWatchLogsDecodedData { + override def productPrefix = "CloudWatchLogsDecodedData" + } +} + +sealed abstract class CloudWatchLogsLogEvent { + def id: String + def timestamp: Long + def message: String + def extractedFields: Option[Map[String, String]] +} + +object CloudWatchLogsLogEvent { + + def apply( + id: String, + timestamp: Long, + message: String, + extractedFields: Option[Map[String, String]] + ): CloudWatchLogsLogEvent = + new Impl(id, timestamp, message, extractedFields) + + private[events] implicit val decoder: Decoder[CloudWatchLogsLogEvent] = + Decoder.forProduct4("id", "timestamp", "message", "extractedFields")( + CloudWatchLogsLogEvent.apply + ) + + private final case class Impl( + id: String, + timestamp: Long, + message: String, + extractedFields: Option[Map[String, String]] + ) extends CloudWatchLogsLogEvent { + override def productPrefix = "CloudWatchLogsLogEvent" + } +} diff --git a/lambda/shared/src/main/scala/feral/lambda/events/SecretsManagerRotationEvent.scala b/lambda/shared/src/main/scala/feral/lambda/events/SecretsManagerRotationEvent.scala new file mode 100644 index 00000000..9dcb91f2 --- /dev/null +++ b/lambda/shared/src/main/scala/feral/lambda/events/SecretsManagerRotationEvent.scala @@ -0,0 +1,75 @@ +/* + * Copyright 2021 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package feral.lambda.events + +import io.circe.Decoder + +import scala.util.Try + +// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/aws-lambda/trigger/secretsmanager.d.ts +// https://docs.aws.amazon.com/secretsmanager/latest/userguide/rotating-secrets-lambda-function-overview.html + +sealed abstract class SecretsManagerRotationEventStep + +object SecretsManagerRotationEventStep { + + case object CreateSecret extends SecretsManagerRotationEventStep + case object SetSecret extends SecretsManagerRotationEventStep + case object TestSecret extends SecretsManagerRotationEventStep + case object FinishSecret extends SecretsManagerRotationEventStep + + private[events] implicit val decoder: Decoder[SecretsManagerRotationEventStep] = + Decoder.decodeString.emapTry { + case "createSecret" => Try(CreateSecret) + case "setSecret" => Try(SetSecret) + case "testSecret" => Try(TestSecret) + case "finishSecret" => Try(FinishSecret) + case s => scala.util.Failure(new IllegalArgumentException(s"Unknown step: $s")) + } +} + +sealed abstract class SecretsManagerRotationEvent { + def step: SecretsManagerRotationEventStep + def secretId: String + def clientRequestToken: String +} + +object SecretsManagerRotationEvent { + + def apply( + step: SecretsManagerRotationEventStep, + secretId: String, + clientRequestToken: String + ): SecretsManagerRotationEvent = + new Impl(step, secretId, clientRequestToken) + + private[events] implicit val decoder: Decoder[SecretsManagerRotationEvent] = + Decoder.instance(c => + for { + step <- c.get[SecretsManagerRotationEventStep]("Step") + secretId <- c.get[String]("SecretId") + clientRequestToken <- c.get[String]("ClientRequestToken") + } yield SecretsManagerRotationEvent(step, secretId, clientRequestToken)) + + private final case class Impl( + step: SecretsManagerRotationEventStep, + secretId: String, + clientRequestToken: String + ) extends SecretsManagerRotationEvent { + override def productPrefix = "SecretsManagerRotationEvent" + } +} diff --git a/lambda/shared/src/test/scala/feral/lambda/events/CloudWatchLogsEventSuite.scala b/lambda/shared/src/test/scala/feral/lambda/events/CloudWatchLogsEventSuite.scala new file mode 100644 index 00000000..432ea478 --- /dev/null +++ b/lambda/shared/src/test/scala/feral/lambda/events/CloudWatchLogsEventSuite.scala @@ -0,0 +1,81 @@ +/* + * Copyright 2021 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package feral.lambda.events + +import io.circe.literal._ +import munit.FunSuite + +class CloudWatchLogsEventSuite extends FunSuite { + + test("decoder") { + assertEquals(event.as[CloudWatchLogsEvent].toTry.get, expected) + } + + test("decoded data decoder") { + assertEquals(decodedDataJson.as[CloudWatchLogsDecodedData].toTry.get, expectedDecodedData) + } + + def event = json""" + { + "awslogs": { + "data": "H4sIAAAAAAAA/6tWKkktLlGyUlAqSS0u0QHQKhZnJ2cm5+cXZOYnlQAAAP//" + } + } + """ + + def expected = + CloudWatchLogsEvent( + awslogs = CloudWatchLogsEventData( + data = "H4sIAAAAAAAA/6tWKkktLlGyUlAqSS0u0QHQKhZnJ2cm5+cXZOYnlQAAAP//" + ) + ) + + def decodedDataJson = json""" + { + "owner": "123456789012", + "logGroup": "/aws/lambda/my-function", + "logStream": "2024/01/15/[$$LATEST]abc123", + "subscriptionFilters": ["filter-1"], + "messageType": "DATA_MESSAGE", + "logEvents": [ + { + "id": "event-id-1", + "timestamp": 1705312800000, + "message": "Log line one", + "extractedFields": { "field1": "value1" } + } + ] + } + """ + + def expectedDecodedData = + CloudWatchLogsDecodedData( + owner = "123456789012", + logGroup = "/aws/lambda/my-function", + logStream = "2024/01/15/[$LATEST]abc123", + subscriptionFilters = List("filter-1"), + messageType = "DATA_MESSAGE", + logEvents = List( + CloudWatchLogsLogEvent( + id = "event-id-1", + timestamp = 1705312800000L, + message = "Log line one", + extractedFields = Some(Map("field1" -> "value1")) + ) + ) + ) +} diff --git a/lambda/shared/src/test/scala/feral/lambda/events/SecretsManagerRotationEventSuite.scala b/lambda/shared/src/test/scala/feral/lambda/events/SecretsManagerRotationEventSuite.scala new file mode 100644 index 00000000..97dd4a45 --- /dev/null +++ b/lambda/shared/src/test/scala/feral/lambda/events/SecretsManagerRotationEventSuite.scala @@ -0,0 +1,103 @@ +/* + * Copyright 2021 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package feral.lambda.events + +import io.circe.literal._ +import munit.FunSuite + +class SecretsManagerRotationEventSuite extends FunSuite { + + test("decoder createSecret") { + assertEquals( + createSecretEvent.as[SecretsManagerRotationEvent].toTry.get, + expectedCreateSecret) + } + + test("decoder setSecret") { + assertEquals(setSecretEvent.as[SecretsManagerRotationEvent].toTry.get, expectedSetSecret) + } + + test("decoder testSecret") { + assertEquals(testSecretEvent.as[SecretsManagerRotationEvent].toTry.get, expectedTestSecret) + } + + test("decoder finishSecret") { + assertEquals( + finishSecretEvent.as[SecretsManagerRotationEvent].toTry.get, + expectedFinishSecret) + } + + def createSecretEvent = json""" + { + "Step": "createSecret", + "SecretId": "arn:aws:secretsmanager:us-east-1:123456789012:secret:my-secret", + "ClientRequestToken": "token-123" + } + """ + + def expectedCreateSecret = + SecretsManagerRotationEvent( + step = SecretsManagerRotationEventStep.CreateSecret, + secretId = "arn:aws:secretsmanager:us-east-1:123456789012:secret:my-secret", + clientRequestToken = "token-123" + ) + + def setSecretEvent = json""" + { + "Step": "setSecret", + "SecretId": "arn:aws:secretsmanager:us-east-1:123456789012:secret:my-secret", + "ClientRequestToken": "token-456" + } + """ + + def expectedSetSecret = + SecretsManagerRotationEvent( + step = SecretsManagerRotationEventStep.SetSecret, + secretId = "arn:aws:secretsmanager:us-east-1:123456789012:secret:my-secret", + clientRequestToken = "token-456" + ) + + def testSecretEvent = json""" + { + "Step": "testSecret", + "SecretId": "my-secret-id", + "ClientRequestToken": "token-789" + } + """ + + def expectedTestSecret = + SecretsManagerRotationEvent( + step = SecretsManagerRotationEventStep.TestSecret, + secretId = "my-secret-id", + clientRequestToken = "token-789" + ) + + def finishSecretEvent = json""" + { + "Step": "finishSecret", + "SecretId": "my-secret-id", + "ClientRequestToken": "token-finish" + } + """ + + def expectedFinishSecret = + SecretsManagerRotationEvent( + step = SecretsManagerRotationEventStep.FinishSecret, + secretId = "my-secret-id", + clientRequestToken = "token-finish" + ) +}