From 79a2532f334c207840b95e9e3a172325b1e495fe Mon Sep 17 00:00:00 2001 From: Brian Holt Date: Thu, 16 Nov 2023 18:30:59 -0600 Subject: [PATCH] convert remaining AWS interactions to use smithy4s --- .../com/dwolla/aws/ArbitraryInstances.scala | 12 ++ .../aws/autoscaling/ArbitraryInstances.scala | 83 +++++++---- .../aws/autoscaling/TestAutoScalingAlg.scala | 4 +- .../cloudformation/ArbitraryInstances.scala | 7 + .../TestCloudFormationAlg.scala | 1 + .../dwolla/aws/ec2/ArbitraryInstances.scala | 23 ++-- .../scala/com/dwolla/aws/ec2/TestEc2Alg.scala | 3 +- .../dwolla/aws/ecs/ArbitraryInstances.scala | 47 +++++-- .../scala/com/dwolla/aws/ecs/FakeECS.scala | 64 --------- .../scala/com/dwolla/aws/ecs/TestEcsAlg.scala | 4 +- .../dwolla/aws/sns/ArbitraryInstances.scala | 18 ++- build.sbt | 43 ++++-- core-tests/src/test/resources/log4j2.xml | 15 ++ .../autoscaling/AutoScalingAlgImplSpec.scala | 130 +++++++++--------- .../LifecycleHookHandlerSpec.scala | 14 +- .../CloudFormationAlgSpec.scala | 85 +++++------- .../dwolla/aws/cloudformation/TestApp.scala | 17 ++- .../scala/com/dwolla/aws/ec2/Ec2AlgSpec.scala | 59 ++++---- .../scala/com/dwolla/aws/ecs/EcsAlgSpec.scala | 24 ++-- .../scala/com/dwolla/aws/sns/SnsAlgSpec.scala | 48 +++---- .../aws/autoscaling/AutoScalingAlg.scala | 57 ++++---- .../autoscaling/LifecycleHookHandler.scala | 5 +- .../com/dwolla/aws/autoscaling/model.scala | 22 +-- .../cloudformation/CloudFormationAlg.scala | 32 +---- .../scala/com/dwolla/aws/ec2/Ec2Alg.scala | 40 +++--- .../scala/com/dwolla/aws/ecs/EcsAlg.scala | 8 +- .../main/scala/com/dwolla/aws/ecs/model.scala | 11 +- .../src/main/scala/com/dwolla/aws/model.scala | 5 + .../sns/ParseLifecycleHookNotification.scala | 6 +- .../scala/com/dwolla/aws/sns/SnsAlg.scala | 27 ++-- .../main/scala/com/dwolla/aws/sns/model.scala | 10 -- .../ecs/draining/TerminationEventBridge.scala | 6 +- .../draining/TerminationEventHandler.scala | 8 +- .../draining/TerminationEventBridgeSpec.scala | 25 ++-- .../ScaleOutPendingEventBridge.scala | 8 +- .../ScaleOutPendingEventHandler.scala | 18 +-- .../ScaleOutPendingEventBridgeSpec.scala | 23 ++-- ....amazon.smithy.build.ProjectionTransformer | 5 + .../AutoscalingPreprocessor.scala | 14 ++ .../CloudformationPreprocessor.scala | 11 ++ .../scala/preprocessors/Ec2Preprocessor.scala | 11 ++ .../scala/preprocessors/EcsPreprocessor.scala | 16 +++ .../OperationFilteringPreprocessor.scala | 41 ++++++ .../scala/preprocessors/SnsPreprocessor.scala | 11 ++ 44 files changed, 622 insertions(+), 499 deletions(-) delete mode 100644 aws-testkit/src/main/scala/com/dwolla/aws/ecs/FakeECS.scala create mode 100644 core-tests/src/test/resources/log4j2.xml delete mode 100644 core/src/main/scala/com/dwolla/aws/sns/model.scala create mode 100644 smithy4s-preprocessors/src/main/resources/META-INF/services/software.amazon.smithy.build.ProjectionTransformer create mode 100644 smithy4s-preprocessors/src/main/scala/preprocessors/AutoscalingPreprocessor.scala create mode 100644 smithy4s-preprocessors/src/main/scala/preprocessors/CloudformationPreprocessor.scala create mode 100644 smithy4s-preprocessors/src/main/scala/preprocessors/Ec2Preprocessor.scala create mode 100644 smithy4s-preprocessors/src/main/scala/preprocessors/EcsPreprocessor.scala create mode 100644 smithy4s-preprocessors/src/main/scala/preprocessors/OperationFilteringPreprocessor.scala create mode 100644 smithy4s-preprocessors/src/main/scala/preprocessors/SnsPreprocessor.scala diff --git a/aws-testkit/src/main/scala/com/dwolla/aws/ArbitraryInstances.scala b/aws-testkit/src/main/scala/com/dwolla/aws/ArbitraryInstances.scala index 370e821..28a1dd5 100644 --- a/aws-testkit/src/main/scala/com/dwolla/aws/ArbitraryInstances.scala +++ b/aws-testkit/src/main/scala/com/dwolla/aws/ArbitraryInstances.scala @@ -1,6 +1,7 @@ package com.dwolla.aws import org.scalacheck.* +import smithy4s.Timestamp given Arbitrary[AccountId] = Arbitrary(Gen.stringOfN(12, Gen.numChar).map(AccountId(_))) @@ -11,3 +12,14 @@ val genTag: Gen[Tag] = value <- Gen.asciiPrintableStr.map(TagValue(_)) } yield Tag(key, value) given Arbitrary[Tag] = Arbitrary(genTag) + +/** + * Timestamp doesn't allow the full range from Instant, so we can't just + * use an Arbitrary[Instant] and map it to Timestamp + */ +val genTimestamp: Gen[Timestamp] = + for { + epoch <- Gen.chooseNum(-62167219200L, 253402300799L) + nanos <- Gen.chooseNum(0, 999999999) + } yield Timestamp(epoch, nanos) +given Arbitrary[Timestamp] = Arbitrary(genTimestamp) diff --git a/aws-testkit/src/main/scala/com/dwolla/aws/autoscaling/ArbitraryInstances.scala b/aws-testkit/src/main/scala/com/dwolla/aws/autoscaling/ArbitraryInstances.scala index eb4f31d..d14d867 100644 --- a/aws-testkit/src/main/scala/com/dwolla/aws/autoscaling/ArbitraryInstances.scala +++ b/aws-testkit/src/main/scala/com/dwolla/aws/autoscaling/ArbitraryInstances.scala @@ -1,22 +1,29 @@ package com.dwolla.aws.autoscaling import cats.syntax.all.* -import java.time.* -import com.dwolla.aws.AccountId -import com.dwolla.aws.ec2.Ec2InstanceId +import com.amazonaws.autoscaling.{LifecycleState as _, *} +import com.amazonaws.ec2.InstanceId +import com.dwolla.aws.autoscaling.LifecycleState.* +import com.dwolla.aws.ec2.given +import com.dwolla.aws.{AccountId, given} import com.fortysevendeg.scalacheck.datetime.jdk8.ArbitraryJdk8.* -import org.scalacheck.Arbitrary.{arbUuid, arbitrary} import org.scalacheck.* -import LifecycleState.* -import software.amazon.awssdk.services.autoscaling.model.{DescribeAutoScalingInstancesResponse, AutoScalingInstanceDetails} -import com.dwolla.aws.given -import com.dwolla.aws.ec2.given +import org.scalacheck.Arbitrary.{arbChar, arbUuid, arbitrary} + +import java.time.* + +given Arbitrary[ResourceName] = Arbitrary(Gen.asciiStr.map(ResourceName.apply)) +given Arbitrary[AsciiStringMaxLen255] = Arbitrary { + Gen.chooseNum(0, 255) + .flatMap(Gen.stringOfN(_, Gen.asciiChar)) + .map(AsciiStringMaxLen255.apply) +} // TODO genAutoScalingGroupName could be more realistic -val genAutoScalingGroupName: Gen[AutoScalingGroupName] = Gen.asciiStr.map(AutoScalingGroupName(_)) +val genAutoScalingGroupName: Gen[AutoScalingGroupName] = arbitrary[ResourceName].map(AutoScalingGroupName(_)) given Arbitrary[AutoScalingGroupName] = Arbitrary(genAutoScalingGroupName) -val genLifecycleHookName: Gen[LifecycleHookName] = Gen.asciiStr.map(LifecycleHookName(_)) +val genLifecycleHookName: Gen[LifecycleHookName] = arbitrary[AsciiStringMaxLen255].map(LifecycleHookName(_)) given Arbitrary[LifecycleHookName] = Arbitrary(genLifecycleHookName) val genLifecycleTransition: Gen[LifecycleTransition] = Gen.const("autoscaling:EC2_INSTANCE_TERMINATING").map(LifecycleTransition(_)) @@ -31,9 +38,9 @@ val genLifecycleHookNotification: Gen[LifecycleHookNotification] = accountId <- arbitrary[AccountId] groupName <- arbitrary[AutoScalingGroupName] hookName <- arbitrary[LifecycleHookName] - ec2InstanceId <- arbitrary[Ec2InstanceId] + InstanceId <- arbitrary[InstanceId] lifecycleTransition <- arbitrary[LifecycleTransition] - } yield LifecycleHookNotification(service, time, requestId, lifecycleActionToken, accountId, groupName, hookName, ec2InstanceId, lifecycleTransition, None) + } yield LifecycleHookNotification(service, time, requestId, lifecycleActionToken, accountId, groupName, hookName, InstanceId, lifecycleTransition, None) given Arbitrary[LifecycleHookNotification] = Arbitrary(genLifecycleHookNotification) val genLifecycleState: Gen[LifecycleState] = @@ -46,7 +53,19 @@ val genLifecycleState: Gen[LifecycleState] = ) given Arbitrary[LifecycleState] = Arbitrary(genLifecycleState) -def genAutoScalingInstanceDetails(maybeId: Option[Ec2InstanceId] = None, +given Arbitrary[XmlStringMaxLen255] = Arbitrary { + Gen.chooseNum(0, 255) + .flatMap(Gen.stringOfN(_, arbitrary[Char])) + .map(XmlStringMaxLen255.apply) +} +given Arbitrary[XmlStringMaxLen32] = Arbitrary { + Gen.chooseNum(0, 32) + .flatMap(Gen.stringOfN(_, arbitrary[Char])) + .map(XmlStringMaxLen32.apply) +} +given Arbitrary[InstanceProtected] = Arbitrary(arbitrary[Boolean].map(InstanceProtected.apply)) + +def genAutoScalingInstanceDetails(maybeId: Option[InstanceId] = None, maybeAutoScalingGroupName: Option[AutoScalingGroupName] = None, maybeLifecycleState: Option[LifecycleState] = None, ): Gen[AutoScalingInstanceDetails] = @@ -54,27 +73,37 @@ def genAutoScalingInstanceDetails(maybeId: Option[Ec2InstanceId] = None, id <- maybeId.orGen asgName <- maybeAutoScalingGroupName.orGen lifecycleState <- maybeLifecycleState.orGen + availabilityZone <- arbitrary[XmlStringMaxLen255] + healthStatus <- arbitrary[XmlStringMaxLen32] + protectedFromScaleIn <- arbitrary[InstanceProtected] + instanceType <- Gen.option(arbitrary[XmlStringMaxLen255]) + launchConfigurationName = None + launchTemplate = None + weightedCapacity = None } yield - AutoScalingInstanceDetails - .builder() - .instanceId(id.value) - .autoScalingGroupName(asgName.value) - .lifecycleState(lifecycleState.awsName) - .build() + AutoScalingInstanceDetails( + instanceId = XmlStringMaxLen19(id.value), + autoScalingGroupName = XmlStringMaxLen255(asgName.value.value), + lifecycleState = XmlStringMaxLen32(lifecycleState.awsName), + availabilityZone = availabilityZone, + healthStatus = healthStatus, + protectedFromScaleIn = protectedFromScaleIn, + instanceType = instanceType, + launchConfigurationName = launchConfigurationName, + launchTemplate = launchTemplate, + weightedCapacity = weightedCapacity, + ) -val genLifecycleHookNotificationWithRelatedDescribeAutoScalingInstancesResponse: Gen[(LifecycleHookNotification, DescribeAutoScalingInstancesResponse)] = +val genLifecycleHookNotificationWithRelatedAutoScalingInstancesType: Gen[(LifecycleHookNotification, AutoScalingInstancesType)] = for { notification <- genLifecycleHookNotification groupName <- arbitrary[AutoScalingGroupName] autoScalingDetailsFromHook <- genAutoScalingInstanceDetails(notification.EC2InstanceId.some, groupName.some) otherAutoScalingDetails <- Gen.listOf(genAutoScalingInstanceDetails(maybeAutoScalingGroupName = groupName.some)) - } yield { - notification -> DescribeAutoScalingInstancesResponse.builder() - .autoScalingInstances(otherAutoScalingDetails.appended(autoScalingDetailsFromHook) *) - .build() - } -given Arbitrary[(LifecycleHookNotification, DescribeAutoScalingInstancesResponse)] = - Arbitrary(genLifecycleHookNotificationWithRelatedDescribeAutoScalingInstancesResponse) + details = otherAutoScalingDetails.appended(autoScalingDetailsFromHook) + } yield notification -> AutoScalingInstancesType(details.some) +given Arbitrary[(LifecycleHookNotification, AutoScalingInstancesType)] = + Arbitrary(genLifecycleHookNotificationWithRelatedAutoScalingInstancesType) extension [A](maybeA: Option[A]) { def orGen(using Arbitrary[A]): Gen[A] = diff --git a/aws-testkit/src/main/scala/com/dwolla/aws/autoscaling/TestAutoScalingAlg.scala b/aws-testkit/src/main/scala/com/dwolla/aws/autoscaling/TestAutoScalingAlg.scala index 6c433c1..7e8e8b9 100644 --- a/aws-testkit/src/main/scala/com/dwolla/aws/autoscaling/TestAutoScalingAlg.scala +++ b/aws-testkit/src/main/scala/com/dwolla/aws/autoscaling/TestAutoScalingAlg.scala @@ -1,9 +1,9 @@ package com.dwolla.aws.autoscaling import cats.effect.* -import com.dwolla.aws.sns.SnsTopicArn +import com.amazonaws.sns.TopicARN abstract class TestAutoScalingAlg extends AutoScalingAlg[IO] { - override def pauseAndRecurse(topic: SnsTopicArn, lifecycleHookNotification: LifecycleHookNotification, onlyIfInState: LifecycleState): IO[Unit] = IO.raiseError(new NotImplementedError) + override def pauseAndRecurse(topic: TopicARN, lifecycleHookNotification: LifecycleHookNotification, onlyIfInState: LifecycleState): IO[Unit] = IO.raiseError(new NotImplementedError) override def continueAutoScaling(l: LifecycleHookNotification): IO[Unit] = IO.raiseError(new NotImplementedError) } diff --git a/aws-testkit/src/main/scala/com/dwolla/aws/cloudformation/ArbitraryInstances.scala b/aws-testkit/src/main/scala/com/dwolla/aws/cloudformation/ArbitraryInstances.scala index 24b5e45..3bdead6 100644 --- a/aws-testkit/src/main/scala/com/dwolla/aws/cloudformation/ArbitraryInstances.scala +++ b/aws-testkit/src/main/scala/com/dwolla/aws/cloudformation/ArbitraryInstances.scala @@ -1,5 +1,6 @@ package com.dwolla.aws.cloudformation +import com.amazonaws.cloudformation.* import org.scalacheck.{Arbitrary, Gen} val genStackArn: Gen[StackArn] = @@ -16,3 +17,9 @@ given Arbitrary[LogicalResourceId] = Arbitrary(genLogicalResourceId) val genPhysicalResourceId: Gen[PhysicalResourceId] = Gen.asciiPrintableStr.map(PhysicalResourceId(_)) given Arbitrary[PhysicalResourceId] = Arbitrary(genPhysicalResourceId) + +val genResourceType: Gen[ResourceType] = Gen.asciiPrintableStr.map(ResourceType.apply) +given Arbitrary[ResourceType] = Arbitrary(genResourceType) + +val genResourceStatus: Gen[ResourceStatus] = Gen.oneOf(ResourceStatus.values) +given Arbitrary[ResourceStatus] = Arbitrary(genResourceStatus) diff --git a/aws-testkit/src/main/scala/com/dwolla/aws/cloudformation/TestCloudFormationAlg.scala b/aws-testkit/src/main/scala/com/dwolla/aws/cloudformation/TestCloudFormationAlg.scala index b848da2..7cae7d3 100644 --- a/aws-testkit/src/main/scala/com/dwolla/aws/cloudformation/TestCloudFormationAlg.scala +++ b/aws-testkit/src/main/scala/com/dwolla/aws/cloudformation/TestCloudFormationAlg.scala @@ -1,6 +1,7 @@ package com.dwolla.aws.cloudformation import cats.effect.* +import com.amazonaws.cloudformation.* abstract class TestCloudFormationAlg extends CloudFormationAlg[IO] { override def physicalResourceIdFor(stack: StackArn, logicalResourceId: LogicalResourceId): IO[Option[PhysicalResourceId]] = IO.raiseError(new NotImplementedError) diff --git a/aws-testkit/src/main/scala/com/dwolla/aws/ec2/ArbitraryInstances.scala b/aws-testkit/src/main/scala/com/dwolla/aws/ec2/ArbitraryInstances.scala index d07d2da..dd10db4 100644 --- a/aws-testkit/src/main/scala/com/dwolla/aws/ec2/ArbitraryInstances.scala +++ b/aws-testkit/src/main/scala/com/dwolla/aws/ec2/ArbitraryInstances.scala @@ -1,26 +1,21 @@ package com.dwolla.aws.ec2 +import com.amazonaws.ec2.{Instance, InstanceId, Tag as AwsTag} import org.scalacheck.* import org.scalacheck.Arbitrary.arbitrary -import software.amazon.awssdk.services.ec2.model.{Instance, Tag as AwsTag} -given Arbitrary[Ec2InstanceId] = Arbitrary(Gen.listOfN(17, Gen.hexChar).map(_.mkString("i-", "", "")).map(Ec2InstanceId(_))) +given Arbitrary[InstanceId] = + Arbitrary(Gen.listOfN(17, Gen.hexChar).map(_.mkString("i-", "", "")).map(InstanceId(_))) val genAwsTag: Gen[AwsTag] = for { - key <- Gen.identifier - value <- arbitrary[String] - } yield AwsTag.builder().key(key).value(value).build() + key <- Gen.option(Gen.identifier) + value <- Gen.option(arbitrary[String]) + } yield AwsTag(key, value) val genInstance: Gen[Instance] = for { - id <- arbitrary[Ec2InstanceId] - tags <- Gen.nonEmptyListOf(genAwsTag) - } yield { - Instance - .builder() - .instanceId(id.value) - .tags(tags *) - .build() - } + id <- Gen.option(arbitrary[InstanceId]) + tags <- Gen.option(Gen.nonEmptyListOf(genAwsTag)) + } yield Instance(instanceId = id.map(_.value), tags = tags) given Arbitrary[Instance] = Arbitrary(genInstance) diff --git a/aws-testkit/src/main/scala/com/dwolla/aws/ec2/TestEc2Alg.scala b/aws-testkit/src/main/scala/com/dwolla/aws/ec2/TestEc2Alg.scala index b508123..68bc05d 100644 --- a/aws-testkit/src/main/scala/com/dwolla/aws/ec2/TestEc2Alg.scala +++ b/aws-testkit/src/main/scala/com/dwolla/aws/ec2/TestEc2Alg.scala @@ -1,8 +1,9 @@ package com.dwolla.aws.ec2 import cats.effect.* +import com.amazonaws.ec2.InstanceId import com.dwolla.aws.Tag abstract class TestEc2Alg extends Ec2Alg[IO] { - override def getTagsForInstance(id: Ec2InstanceId): IO[List[Tag]] = IO.raiseError(new NotImplementedError) + override def getTagsForInstance(id: InstanceId): IO[List[Tag]] = IO.raiseError(new NotImplementedError) } diff --git a/aws-testkit/src/main/scala/com/dwolla/aws/ecs/ArbitraryInstances.scala b/aws-testkit/src/main/scala/com/dwolla/aws/ecs/ArbitraryInstances.scala index a782019..2922645 100644 --- a/aws-testkit/src/main/scala/com/dwolla/aws/ecs/ArbitraryInstances.scala +++ b/aws-testkit/src/main/scala/com/dwolla/aws/ecs/ArbitraryInstances.scala @@ -1,21 +1,23 @@ package com.dwolla.aws.ecs import cats.syntax.all.* -import com.dwolla.aws.{AccountId, given} -import com.dwolla.aws.ec2.{Ec2InstanceId, given} +import com.amazonaws.ec2.InstanceId import com.dwolla.RandomChunks +import com.dwolla.aws.ec2.given import com.dwolla.aws.ecs.TaskStatus.stoppedTaskStatuses +import com.dwolla.aws.{AccountId, given} import fs2.{Chunk, Pure, Stream} import monix.newtypes.NewtypeWrapped import org.scalacheck.* import org.scalacheck.Arbitrary.arbitrary +import smithy4s.aws.AwsRegion +import smithy4s.aws.kernel.AwsRegion.{AF_SOUTH_1, AP_EAST_1, AP_NORTHEAST_1, AP_NORTHEAST_2, AP_NORTHEAST_3, AP_SOUTHEAST_1, AP_SOUTHEAST_2, AP_SOUTH_1, CA_CENTRAL_1, CN_NORTHWEST_1, CN_NORTH_1, EU_CENTRAL_1, EU_NORTH_1, EU_SOUTH_1, EU_WEST_1, EU_WEST_2, EU_WEST_3, GovCloud, ME_SOUTH_1, SA_EAST_1, US_EAST_1, US_EAST_2, US_GOV_EAST_1, US_ISOB_EAST_1, US_ISO_EAST_1, US_ISO_WEST_1, US_WEST_1, US_WEST_2} import java.util.UUID -import scala.jdk.CollectionConverters.* case class TaskArnAndStatus(arn: TaskArn, status: TaskStatus) case class ContainerInstanceWithTaskPages(containerInstanceId: ContainerInstanceId, - ec2InstanceId: Ec2InstanceId, + ec2InstanceId: InstanceId, tasks: List[Chunk[TaskArnAndStatus]], status: ContainerInstanceStatus, ) { @@ -43,8 +45,37 @@ object ClusterWithInstances extends NewtypeWrapped[(Cluster, List[Chunk[Containe type TasksForContainerInstance = TasksForContainerInstance.Type object TasksForContainerInstance extends NewtypeWrapped[List[Chunk[TaskArnAndStatus]]] -def genRegion: Gen[Region] = Gen.oneOf(software.amazon.awssdk.regions.Region.regions().asScala).map(x => Region(x.id())) -given Arbitrary[Region] = Arbitrary(genRegion) +def genRegion: Gen[AwsRegion] = Gen.oneOf( + AF_SOUTH_1, + AP_EAST_1, + AP_NORTHEAST_1, + AP_NORTHEAST_2, + AP_NORTHEAST_3, + AP_SOUTH_1, + AP_SOUTHEAST_1, + AP_SOUTHEAST_2, + CA_CENTRAL_1, + CN_NORTH_1, + CN_NORTHWEST_1, + EU_CENTRAL_1, + EU_NORTH_1, + EU_SOUTH_1, + EU_WEST_1, + EU_WEST_2, + EU_WEST_3, + GovCloud, + ME_SOUTH_1, + SA_EAST_1, + US_EAST_1, + US_EAST_2, + US_GOV_EAST_1, + US_ISO_EAST_1, + US_ISO_WEST_1, + US_ISOB_EAST_1, + US_WEST_1, + US_WEST_2, +) +given Arbitrary[AwsRegion] = Arbitrary(genRegion) def genContainerInstanceStatus: Gen[ContainerInstanceStatus] = Gen.oneOf(ContainerInstanceStatus.Active, ContainerInstanceStatus.Draining, ContainerInstanceStatus.Inactive) given Arbitrary[ContainerInstanceStatus] = Arbitrary(genContainerInstanceStatus) @@ -52,7 +83,7 @@ given Arbitrary[ContainerInstanceStatus] = Arbitrary(genContainerInstanceStatus) def genContainerInstanceWithTaskPages: Gen[ContainerInstanceWithTaskPages] = for { cId <- arbitrary[ContainerInstanceId] - ec2Id <- arbitrary[Ec2InstanceId] + ec2Id <- arbitrary[InstanceId] taskCount <- arbitrary[TasksForContainerInstance] status <- arbitrary[ContainerInstanceStatus] } yield ContainerInstanceWithTaskPages(cId, ec2Id, taskCount.value, status) @@ -115,7 +146,7 @@ given Arbitrary[ContainerInstanceId] = Arbitrary(genContainerInstanceId) def genCluster: Gen[Cluster] = for { - region <- arbitrary[Region] + region <- arbitrary[AwsRegion] accountId <- arbitrary[AccountId] clusterName <- arbitrary[ClusterName] } yield Cluster(region, accountId, clusterName) diff --git a/aws-testkit/src/main/scala/com/dwolla/aws/ecs/FakeECS.scala b/aws-testkit/src/main/scala/com/dwolla/aws/ecs/FakeECS.scala deleted file mode 100644 index 7b7ee14..0000000 --- a/aws-testkit/src/main/scala/com/dwolla/aws/ecs/FakeECS.scala +++ /dev/null @@ -1,64 +0,0 @@ -package com.dwolla.aws.ecs - -import cats.* -import cats.syntax.all.* -import com.amazonaws.ecs.* -import smithy4s.Timestamp - -abstract class FakeECS[F[_] : ApplicativeThrow] extends ECS[F] { - override def createCapacityProvider(name: String, autoScalingGroupProvider: AutoScalingGroupProvider, tags: Option[List[Tag]]): F[CreateCapacityProviderResponse] = (new NotImplementedError).raiseError - override def createCluster(clusterName: Option[String], tags: Option[List[Tag]], settings: Option[List[ClusterSetting]], configuration: Option[ClusterConfiguration], capacityProviders: Option[List[String]], defaultCapacityProviderStrategy: Option[List[CapacityProviderStrategyItem]], serviceConnectDefaults: Option[ClusterServiceConnectDefaultsRequest]): F[CreateClusterResponse] = (new NotImplementedError).raiseError - override def createService(serviceName: String, enableECSManagedTags: Boolean, enableExecuteCommand: Boolean, cluster: Option[String], taskDefinition: Option[String], loadBalancers: Option[List[LoadBalancer]], serviceRegistries: Option[List[ServiceRegistry]], desiredCount: Option[BoxedInteger], clientToken: Option[String], launchType: Option[LaunchType], capacityProviderStrategy: Option[List[CapacityProviderStrategyItem]], platformVersion: Option[String], role: Option[String], deploymentConfiguration: Option[DeploymentConfiguration], placementConstraints: Option[List[PlacementConstraint]], placementStrategy: Option[List[PlacementStrategy]], networkConfiguration: Option[NetworkConfiguration], healthCheckGracePeriodSeconds: Option[BoxedInteger], schedulingStrategy: Option[SchedulingStrategy], deploymentController: Option[DeploymentController], tags: Option[List[Tag]], propagateTags: Option[PropagateTags], serviceConnectConfiguration: Option[ServiceConnectConfiguration]): F[CreateServiceResponse] = (new NotImplementedError).raiseError - override def createTaskSet(service: String, cluster: String, taskDefinition: String, externalId: Option[String], networkConfiguration: Option[NetworkConfiguration], loadBalancers: Option[List[LoadBalancer]], serviceRegistries: Option[List[ServiceRegistry]], launchType: Option[LaunchType], capacityProviderStrategy: Option[List[CapacityProviderStrategyItem]], platformVersion: Option[String], scale: Option[Scale], clientToken: Option[String], tags: Option[List[Tag]]): F[CreateTaskSetResponse] = (new NotImplementedError).raiseError - override def deleteAccountSetting(name: SettingName, principalArn: Option[String]): F[DeleteAccountSettingResponse] = (new NotImplementedError).raiseError - override def deleteAttributes(attributes: List[Attribute], cluster: Option[String]): F[DeleteAttributesResponse] = (new NotImplementedError).raiseError - override def deleteCapacityProvider(capacityProvider: String): F[DeleteCapacityProviderResponse] = (new NotImplementedError).raiseError - override def deleteCluster(cluster: String): F[DeleteClusterResponse] = (new NotImplementedError).raiseError - override def deleteService(service: String, cluster: Option[String], force: Option[BoxedBoolean]): F[DeleteServiceResponse] = (new NotImplementedError).raiseError - override def deleteTaskSet(cluster: String, service: String, taskSet: String, force: Option[BoxedBoolean]): F[DeleteTaskSetResponse] = (new NotImplementedError).raiseError - override def deregisterContainerInstance(containerInstance: String, cluster: Option[String], force: Option[BoxedBoolean]): F[DeregisterContainerInstanceResponse] = (new NotImplementedError).raiseError - override def deregisterTaskDefinition(taskDefinition: String): F[DeregisterTaskDefinitionResponse] = (new NotImplementedError).raiseError - override def describeCapacityProviders(capacityProviders: Option[List[String]], include: Option[List[CapacityProviderField]], maxResults: Option[BoxedInteger], nextToken: Option[String]): F[DescribeCapacityProvidersResponse] = (new NotImplementedError).raiseError - override def describeClusters(clusters: Option[List[String]], include: Option[List[ClusterField]]): F[DescribeClustersResponse] = (new NotImplementedError).raiseError - override def describeContainerInstances(containerInstances: List[String], cluster: Option[String], include: Option[List[ContainerInstanceField]]): F[DescribeContainerInstancesResponse] = (new NotImplementedError).raiseError - override def describeServices(services: List[String], cluster: Option[String], include: Option[List[ServiceField]]): F[DescribeServicesResponse] = (new NotImplementedError).raiseError - override def describeTaskDefinition(taskDefinition: String, include: Option[List[TaskDefinitionField]]): F[DescribeTaskDefinitionResponse] = (new NotImplementedError).raiseError - override def describeTasks(tasks: List[String], cluster: Option[String], include: Option[List[TaskField]]): F[DescribeTasksResponse] = (new NotImplementedError).raiseError - override def describeTaskSets(cluster: String, service: String, taskSets: Option[List[String]], include: Option[List[TaskSetField]]): F[DescribeTaskSetsResponse] = (new NotImplementedError).raiseError - override def discoverPollEndpoint(containerInstance: Option[String], cluster: Option[String]): F[DiscoverPollEndpointResponse] = (new NotImplementedError).raiseError - override def executeCommand(command: String, task: String, interactive: Boolean, cluster: Option[String], container: Option[String]): F[ExecuteCommandResponse] = (new NotImplementedError).raiseError - override def getTaskProtection(cluster: String, tasks: Option[List[String]]): F[GetTaskProtectionResponse] = (new NotImplementedError).raiseError - override def listAccountSettings(effectiveSettings: Boolean, maxResults: Int, name: Option[SettingName], value: Option[String], principalArn: Option[String], nextToken: Option[String]): F[ListAccountSettingsResponse] = (new NotImplementedError).raiseError - override def listAttributes(targetType: TargetType, cluster: Option[String], attributeName: Option[String], attributeValue: Option[String], nextToken: Option[String], maxResults: Option[BoxedInteger]): F[ListAttributesResponse] = (new NotImplementedError).raiseError - override def listClusters(nextToken: Option[String], maxResults: Option[BoxedInteger]): F[ListClustersResponse] = (new NotImplementedError).raiseError - override def listContainerInstances(cluster: Option[String], filter: Option[String], nextToken: Option[String], maxResults: Option[BoxedInteger], status: Option[ContainerInstanceStatus]): F[ListContainerInstancesResponse] = (new NotImplementedError).raiseError - override def listServices(cluster: Option[String], nextToken: Option[String], maxResults: Option[BoxedInteger], launchType: Option[LaunchType], schedulingStrategy: Option[SchedulingStrategy]): F[ListServicesResponse] = (new NotImplementedError).raiseError - override def listServicesByNamespace(namespace: String, nextToken: Option[String], maxResults: Option[BoxedInteger]): F[ListServicesByNamespaceResponse] = (new NotImplementedError).raiseError - override def listTagsForResource(resourceArn: String): F[ListTagsForResourceResponse] = (new NotImplementedError).raiseError - override def listTaskDefinitionFamilies(familyPrefix: Option[String], status: Option[TaskDefinitionFamilyStatus], nextToken: Option[String], maxResults: Option[BoxedInteger]): F[ListTaskDefinitionFamiliesResponse] = (new NotImplementedError).raiseError - override def listTaskDefinitions(familyPrefix: Option[String], status: Option[TaskDefinitionStatus], sort: Option[SortOrder], nextToken: Option[String], maxResults: Option[BoxedInteger]): F[ListTaskDefinitionsResponse] = (new NotImplementedError).raiseError - override def listTasks(cluster: Option[String], containerInstance: Option[String], family: Option[String], nextToken: Option[String], maxResults: Option[BoxedInteger], startedBy: Option[String], serviceName: Option[String], desiredStatus: Option[DesiredStatus], launchType: Option[LaunchType]): F[ListTasksResponse] = (new NotImplementedError).raiseError - override def putAccountSetting(name: SettingName, value: String, principalArn: Option[String]): F[PutAccountSettingResponse] = (new NotImplementedError).raiseError - override def putAccountSettingDefault(name: SettingName, value: String): F[PutAccountSettingDefaultResponse] = (new NotImplementedError).raiseError - override def putAttributes(attributes: List[Attribute], cluster: Option[String]): F[PutAttributesResponse] = (new NotImplementedError).raiseError - override def putClusterCapacityProviders(cluster: String, capacityProviders: List[String], defaultCapacityProviderStrategy: List[CapacityProviderStrategyItem]): F[PutClusterCapacityProvidersResponse] = (new NotImplementedError).raiseError - override def registerContainerInstance(cluster: Option[String], instanceIdentityDocument: Option[String], instanceIdentityDocumentSignature: Option[String], totalResources: Option[List[Resource]], versionInfo: Option[VersionInfo], containerInstanceArn: Option[String], attributes: Option[List[Attribute]], platformDevices: Option[List[PlatformDevice]], tags: Option[List[Tag]]): F[RegisterContainerInstanceResponse] = (new NotImplementedError).raiseError - override def registerTaskDefinition(family: String, containerDefinitions: List[ContainerDefinition], taskRoleArn: Option[String], executionRoleArn: Option[String], networkMode: Option[NetworkMode], volumes: Option[List[Volume]], placementConstraints: Option[List[TaskDefinitionPlacementConstraint]], requiresCompatibilities: Option[List[Compatibility]], cpu: Option[String], memory: Option[String], tags: Option[List[Tag]], pidMode: Option[PidMode], ipcMode: Option[IpcMode], proxyConfiguration: Option[ProxyConfiguration], inferenceAccelerators: Option[List[InferenceAccelerator]], ephemeralStorage: Option[EphemeralStorage], runtimePlatform: Option[RuntimePlatform]): F[RegisterTaskDefinitionResponse] = (new NotImplementedError).raiseError - override def runTask(taskDefinition: String, enableECSManagedTags: Boolean, enableExecuteCommand: Boolean, capacityProviderStrategy: Option[List[CapacityProviderStrategyItem]], cluster: Option[String], count: Option[BoxedInteger], group: Option[String], launchType: Option[LaunchType], networkConfiguration: Option[NetworkConfiguration], overrides: Option[TaskOverride], placementConstraints: Option[List[PlacementConstraint]], placementStrategy: Option[List[PlacementStrategy]], platformVersion: Option[String], propagateTags: Option[PropagateTags], referenceId: Option[String], startedBy: Option[String], tags: Option[List[Tag]]): F[RunTaskResponse] = (new NotImplementedError).raiseError - override def startTask(containerInstances: List[String], taskDefinition: String, enableECSManagedTags: Boolean, enableExecuteCommand: Boolean, cluster: Option[String], group: Option[String], networkConfiguration: Option[NetworkConfiguration], overrides: Option[TaskOverride], propagateTags: Option[PropagateTags], referenceId: Option[String], startedBy: Option[String], tags: Option[List[Tag]]): F[StartTaskResponse] = (new NotImplementedError).raiseError - override def stopTask(task: String, cluster: Option[String], reason: Option[String]): F[StopTaskResponse] = (new NotImplementedError).raiseError - override def submitAttachmentStateChanges(attachments: List[AttachmentStateChange], cluster: Option[String]): F[SubmitAttachmentStateChangesResponse] = (new NotImplementedError).raiseError - override def submitContainerStateChange(cluster: Option[String], task: Option[String], containerName: Option[String], runtimeId: Option[String], status: Option[String], exitCode: Option[BoxedInteger], reason: Option[String], networkBindings: Option[List[NetworkBinding]]): F[SubmitContainerStateChangeResponse] = (new NotImplementedError).raiseError - override def submitTaskStateChange(cluster: Option[String], task: Option[String], status: Option[String], reason: Option[String], containers: Option[List[ContainerStateChange]], attachments: Option[List[AttachmentStateChange]], managedAgents: Option[List[ManagedAgentStateChange]], pullStartedAt: Option[Timestamp], pullStoppedAt: Option[Timestamp], executionStoppedAt: Option[Timestamp]): F[SubmitTaskStateChangeResponse] = (new NotImplementedError).raiseError - override def tagResource(resourceArn: String, tags: List[Tag]): F[TagResourceResponse] = (new NotImplementedError).raiseError - override def untagResource(resourceArn: String, tagKeys: List[TagKey]): F[UntagResourceResponse] = (new NotImplementedError).raiseError - override def updateCapacityProvider(name: String, autoScalingGroupProvider: AutoScalingGroupProviderUpdate): F[UpdateCapacityProviderResponse] = (new NotImplementedError).raiseError - override def updateCluster(cluster: String, settings: Option[List[ClusterSetting]], configuration: Option[ClusterConfiguration], serviceConnectDefaults: Option[ClusterServiceConnectDefaultsRequest]): F[UpdateClusterResponse] = (new NotImplementedError).raiseError - override def updateClusterSettings(cluster: String, settings: List[ClusterSetting]): F[UpdateClusterSettingsResponse] = (new NotImplementedError).raiseError - override def updateContainerAgent(containerInstance: String, cluster: Option[String]): F[UpdateContainerAgentResponse] = (new NotImplementedError).raiseError - override def updateContainerInstancesState(containerInstances: List[String], status: ContainerInstanceStatus, cluster: Option[String]): F[UpdateContainerInstancesStateResponse] = (new NotImplementedError).raiseError - override def updateService(service: String, forceNewDeployment: Boolean, cluster: Option[String], desiredCount: Option[BoxedInteger], taskDefinition: Option[String], capacityProviderStrategy: Option[List[CapacityProviderStrategyItem]], deploymentConfiguration: Option[DeploymentConfiguration], networkConfiguration: Option[NetworkConfiguration], placementConstraints: Option[List[PlacementConstraint]], placementStrategy: Option[List[PlacementStrategy]], platformVersion: Option[String], healthCheckGracePeriodSeconds: Option[BoxedInteger], enableExecuteCommand: Option[BoxedBoolean], enableECSManagedTags: Option[BoxedBoolean], loadBalancers: Option[List[LoadBalancer]], propagateTags: Option[PropagateTags], serviceRegistries: Option[List[ServiceRegistry]], serviceConnectConfiguration: Option[ServiceConnectConfiguration]): F[UpdateServiceResponse] = (new NotImplementedError).raiseError - override def updateServicePrimaryTaskSet(cluster: String, service: String, primaryTaskSet: String): F[UpdateServicePrimaryTaskSetResponse] = (new NotImplementedError).raiseError - override def updateTaskProtection(cluster: String, tasks: List[String], protectionEnabled: Boolean, expiresInMinutes: Option[BoxedInteger]): F[UpdateTaskProtectionResponse] = (new NotImplementedError).raiseError - override def updateTaskSet(cluster: String, service: String, taskSet: String, scale: Scale): F[UpdateTaskSetResponse] = (new NotImplementedError).raiseError -} diff --git a/aws-testkit/src/main/scala/com/dwolla/aws/ecs/TestEcsAlg.scala b/aws-testkit/src/main/scala/com/dwolla/aws/ecs/TestEcsAlg.scala index d687ad3..aaa7f20 100644 --- a/aws-testkit/src/main/scala/com/dwolla/aws/ecs/TestEcsAlg.scala +++ b/aws-testkit/src/main/scala/com/dwolla/aws/ecs/TestEcsAlg.scala @@ -1,13 +1,13 @@ package com.dwolla.aws.ecs import cats.effect.* -import com.dwolla.aws.ec2.* +import com.amazonaws.ec2.InstanceId import com.dwolla.aws.ecs.* abstract class TestEcsAlg extends EcsAlg[IO, List] { override def listClusterArns: List[ClusterArn] = ??? override def listContainerInstances(cluster: ClusterArn): List[ContainerInstance] = ??? - override def findEc2Instance(ec2InstanceId: Ec2InstanceId): IO[Option[(ClusterArn, ContainerInstance)]] = IO.raiseError(new NotImplementedError) + override def findEc2Instance(ec2InstanceId: InstanceId): IO[Option[(ClusterArn, ContainerInstance)]] = IO.raiseError(new NotImplementedError) override def isTaskDefinitionRunningOnInstance(cluster: ClusterArn, ci: ContainerInstance, taskDefinition: TaskDefinitionArn): IO[Boolean] = IO.raiseError(new NotImplementedError) override def drainInstanceImpl(cluster: ClusterArn, ci: ContainerInstance): IO[Unit] = IO.raiseError(new NotImplementedError) } diff --git a/aws-testkit/src/main/scala/com/dwolla/aws/sns/ArbitraryInstances.scala b/aws-testkit/src/main/scala/com/dwolla/aws/sns/ArbitraryInstances.scala index 0f791d8..6241172 100644 --- a/aws-testkit/src/main/scala/com/dwolla/aws/sns/ArbitraryInstances.scala +++ b/aws-testkit/src/main/scala/com/dwolla/aws/sns/ArbitraryInstances.scala @@ -1,13 +1,21 @@ package com.dwolla.aws.sns +import com.amazonaws.sns.* +import com.dwolla.aws.ecs.given import com.dwolla.aws.{AccountId, given} -import com.dwolla.aws.ecs.{Region, given} -import org.scalacheck.{Arbitrary, Gen} import org.scalacheck.Arbitrary.arbitrary +import org.scalacheck.{Arbitrary, Gen} +import smithy4s.aws.AwsRegion -given Arbitrary[SnsTopicArn] = +given Arbitrary[TopicARN] = Arbitrary(for { - region <- arbitrary[Region] + region <- arbitrary[AwsRegion] accountId <- arbitrary[AccountId] topicName <- Gen.alphaNumStr - } yield SnsTopicArn(s"arn:aws:sns:$region:$accountId:$topicName")) + } yield TopicARN(s"arn:aws:sns:$region:$accountId:$topicName")) + +given Arbitrary[Message] = + Arbitrary(arbitrary[String].map(Message.apply)) + +given Arbitrary[MessageId] = + Arbitrary(Gen.identifier.map(MessageId.apply)) diff --git a/build.sbt b/build.sbt index 08803db..1a684ef 100644 --- a/build.sbt +++ b/build.sbt @@ -13,6 +13,19 @@ ThisBuild / mergifyStewardConfig ~= { _.map(_.copy( topLevelDirectory := None ThisBuild / scalacOptions += "-source:future" +lazy val `smithy4s-preprocessors` = project + .in(file("smithy4s-preprocessors")) + .settings( + scalaVersion := "2.12.13", // 2.12 to match what SBT uses + scalacOptions -= "-source:future", + libraryDependencies ++= { + Seq( + "org.typelevel" %% "cats-core" % "2.10.0", + "software.amazon.smithy" % "smithy-build" % smithy4s.codegen.BuildInfo.smithyVersion, + ) + }, + ) + lazy val `smithy4s-generated` = project .in(file("smithy4s")) .settings( @@ -20,10 +33,24 @@ lazy val `smithy4s-generated` = project Seq( "com.disneystreaming.smithy4s" %% "smithy4s-http4s" % smithy4sVersion.value, "com.disneystreaming.smithy4s" %% "smithy4s-aws-http4s" % smithy4sVersion.value, - "com.disneystreaming.smithy" % "aws-ecs-spec" % "2023.02.10", ) }, - scalacOptions ~= (_.filterNot(s => s.startsWith("-Ywarn") || s.startsWith("-Xlint") || s.startsWith("-W") || s.equals("-Xfatal-warnings"))), + smithy4sAwsSpecs ++= Seq( + AWS.autoScaling, + AWS.cloudformation, + AWS.ec2, + AWS.ecs, + AWS.sns, + ), + scalacOptions += "-Wconf:any:s", + Compile / smithy4sModelTransformers ++= List( + "AutoscalingPreprocessor", + "CloudformationPreprocessor", + "Ec2Preprocessor", + "EcsPreprocessor", + "SnsPreprocessor", + ), + Compile / smithy4sAllDependenciesAsJars += (`smithy4s-preprocessors` / Compile / packageBin).value ) .enablePlugins( Smithy4sCodegenPlugin, @@ -40,12 +67,6 @@ lazy val `autoscaling-ecs-core`: Project = project "io.circe" %% "circe-parser" % "0.14.6", "io.monix" %% "newtypes-core" % "0.2.3", "io.monix" %% "newtypes-circe-v0-14" % "0.2.3", - - // TODO when smithy4s is updated, hopefully these Java SDK artifacts can be replaced with smithy4s equivalents - "software.amazon.awssdk" % "autoscaling" % "2.20.162", - "software.amazon.awssdk" % "sns" % "2.20.162", - "software.amazon.awssdk" % "ec2" % "2.20.162", - "software.amazon.awssdk" % "cloudformation" % "2.20.162", ) } ) @@ -58,6 +79,7 @@ lazy val `core-tests` = project .settings( libraryDependencies ++= { Seq( + "org.http4s" %% "http4s-ember-client" % "0.23.24" % Test, "org.typelevel" %% "cats-effect-testkit" % "3.5.2" % Test, "org.typelevel" %% "munit-cats-effect" % "2.0.0-M4" % Test, "org.scalameta" %% "munit-scalacheck" % "1.0.0-M10" % Test, @@ -68,8 +90,11 @@ lazy val `core-tests` = project "io.circe" %% "circe-testing" % "0.14.6" % Test, "com.47deg" %% "scalacheck-toolbox-datetime" % "0.7.0" % Test exclude("joda-time", "joda-time"), "org.scala-lang.modules" %% "scala-java8-compat" % "1.0.2" % Test, + "com.amazonaws" % "aws-lambda-java-log4j2" % "1.6.0" % Test, + "org.apache.logging.log4j" % "log4j-slf4j-impl" % "2.21.1" % Test, ) - } + }, + scalacOptions += "-language:adhocExtensions", // TODO this might be a bug in smithy4s ) .dependsOn( `autoscaling-ecs-core`, diff --git a/core-tests/src/test/resources/log4j2.xml b/core-tests/src/test/resources/log4j2.xml new file mode 100644 index 0000000..d6d4349 --- /dev/null +++ b/core-tests/src/test/resources/log4j2.xml @@ -0,0 +1,15 @@ + + + + + + %d{yyyy-MM-dd HH:mm:ss} %X{AWSRequestId} %-5p %c{1}:%L - %m + + + + + + + + + diff --git a/core-tests/src/test/scala/com/dwolla/aws/autoscaling/AutoScalingAlgImplSpec.scala b/core-tests/src/test/scala/com/dwolla/aws/autoscaling/AutoScalingAlgImplSpec.scala index 0e575e7..89964c1 100644 --- a/core-tests/src/test/scala/com/dwolla/aws/autoscaling/AutoScalingAlgImplSpec.scala +++ b/core-tests/src/test/scala/com/dwolla/aws/autoscaling/AutoScalingAlgImplSpec.scala @@ -1,23 +1,21 @@ package com.dwolla.aws.autoscaling import cats.effect.* -import cats.effect.std.Dispatcher import cats.effect.testkit.TestControl +import cats.syntax.all.* +import com.amazonaws.autoscaling.{LifecycleState as _, *} +import com.amazonaws.ec2.InstanceId +import com.amazonaws.sns.{Message, TopicARN} import com.dwolla.aws.autoscaling.LifecycleState.* import com.dwolla.aws.autoscaling.given -import com.dwolla.aws.ec2.Ec2InstanceId -import com.dwolla.aws.sns.{SnsAlg, SnsTopicArn, given} +import com.dwolla.aws.sns.{SnsAlg, given} import io.circe.syntax.* import munit.{CatsEffectSuite, ScalaCheckEffectSuite} import org.scalacheck.effect.PropF.forAllF import org.typelevel.log4cats.LoggerFactory import org.typelevel.log4cats.noop.NoOpFactory -import software.amazon.awssdk.services.autoscaling.AutoScalingAsyncClient -import software.amazon.awssdk.services.autoscaling.model.{LifecycleState as _, *} -import java.util.concurrent.CompletableFuture import scala.concurrent.duration.* -import scala.jdk.CollectionConverters.* class AutoScalingAlgImplSpec extends CatsEffectSuite @@ -27,73 +25,70 @@ class AutoScalingAlgImplSpec test("AutoScalingAlgImpl should make an async completeLifecycleAction request to continueAutoScaling") { forAllF { (arbLifecycleHookNotification: LifecycleHookNotification) => - Dispatcher.sequential[IO].use { dispatcher => - for { - deferredCompleteLifecycleActionRequest <- Deferred[IO, CompleteLifecycleActionRequest] - autoScalingClient = new AutoScalingAsyncClient { - override def serviceName(): String = "FakeAutoScalingAsyncClient" - override def close(): Unit = () - - override def completeLifecycleAction(completeLifecycleActionRequest: CompleteLifecycleActionRequest): CompletableFuture[CompleteLifecycleActionResponse] = - dispatcher.unsafeToCompletableFuture { - deferredCompleteLifecycleActionRequest.complete(completeLifecycleActionRequest) - .as(CompleteLifecycleActionResponse.builder().build()) - } - } + for { + deferredCompleteLifecycleActionRequest <- Deferred[IO, (AsciiStringMaxLen255, ResourceName, LifecycleActionResult, Option[XmlStringMaxLen19])] + autoScalingClient = new AutoScaling.Default[IO](new NotImplementedError().raiseError) { + override def completeLifecycleAction(lifecycleHookName: AsciiStringMaxLen255, + autoScalingGroupName: ResourceName, + lifecycleActionResult: LifecycleActionResult, + lifecycleActionToken: Option[LifecycleActionToken], + instanceId: Option[XmlStringMaxLen19]): IO[CompleteLifecycleActionAnswer] = + deferredCompleteLifecycleActionRequest.complete((lifecycleHookName, autoScalingGroupName, lifecycleActionResult, instanceId)) + .as(CompleteLifecycleActionAnswer()) + } - sns = new SnsAlg[IO] { - override def publish(topic: SnsTopicArn, message: String): IO[Unit] = - IO.raiseError(new RuntimeException("SnsAsyncClient.publish should not have been called")) - } + sns = new SnsAlg[IO] { + override def publish(topic: TopicARN, message: Message): IO[Unit] = + IO.raiseError(new RuntimeException("SnsAsyncClient.publish should not have been called")) + } - _ <- new AutoScalingAlgImpl(autoScalingClient, sns).continueAutoScaling(arbLifecycleHookNotification) + _ <- new AutoScalingAlgImpl(autoScalingClient, sns).continueAutoScaling(arbLifecycleHookNotification) - passedReq <- deferredCompleteLifecycleActionRequest.get - } yield { - assertEquals(LifecycleHookName(passedReq.lifecycleHookName()), arbLifecycleHookNotification.lifecycleHookName) - assertEquals(AutoScalingGroupName(passedReq.autoScalingGroupName()), arbLifecycleHookNotification.autoScalingGroupName) - assertEquals(passedReq.lifecycleActionResult(), "CONTINUE") - assertEquals(passedReq.instanceId(), arbLifecycleHookNotification.EC2InstanceId.value) - } + (hook, group, result, instance) <- deferredCompleteLifecycleActionRequest.get + } yield { + assertEquals(LifecycleHookName(hook), arbLifecycleHookNotification.lifecycleHookName) + assertEquals(AutoScalingGroupName(group), arbLifecycleHookNotification.autoScalingGroupName) + assertEquals(result, LifecycleActionResult("CONTINUE")) + assertEquals(instance.map(i => InstanceId(i.value)), arbLifecycleHookNotification.EC2InstanceId.some) } } } test("AutoScalingAlgImpl should pause 5 seconds and then send a message to restart the lambda, but only if the Lifecycle Action is still active") { - forAllF { (arbSnsTopicArn: SnsTopicArn, - notifAndResp: (LifecycleHookNotification, DescribeAutoScalingInstancesResponse), + forAllF { (arbSnsTopicArn: TopicARN, + notifAndResp: (LifecycleHookNotification, AutoScalingInstancesType), guardState: LifecycleState, ) => - val (arbLifecycleHookNotification, arbDescribeAutoScalingInstancesResponse) = notifAndResp + val (arbLifecycleHookNotification, arbAutoScalingInstancesType) = notifAndResp for { - capturedPublishRequests <- Ref[IO].of(Set.empty[(SnsTopicArn, String)]) + capturedPublishRequests <- Ref[IO].of(Set.empty[(TopicARN, Message)]) control <- TestControl.execute { - Dispatcher.sequential[IO](await = true).use { dispatcher => - val autoScalingClient = new AutoScalingAsyncClient { - override def serviceName(): String = "FakeAutoScalingAsyncClient" - override def close(): Unit = () - - override def describeAutoScalingInstances(req: DescribeAutoScalingInstancesRequest): CompletableFuture[DescribeAutoScalingInstancesResponse] = - dispatcher.unsafeToCompletableFuture(IO { - if (req.instanceIds().contains(arbLifecycleHookNotification.EC2InstanceId.value)) - arbDescribeAutoScalingInstancesResponse - else - DescribeAutoScalingInstancesResponse.builder().build() - }) - - override def completeLifecycleAction(completeLifecycleActionRequest: CompleteLifecycleActionRequest): CompletableFuture[CompleteLifecycleActionResponse] = { - CompletableFuture.failedFuture(new RuntimeException("AutoScalingAsyncClient.completeLifecycleAction should not have been called")) + val autoScalingClient = new AutoScaling.Default[IO](new NotImplementedError().raiseError) { + override def describeAutoScalingInstances(instanceIds: Option[List[XmlStringMaxLen19]], + maxRecords: Option[MaxRecords], + nextToken: Option[XmlString]): IO[AutoScalingInstancesType] = + IO.pure { + if (instanceIds.exists(_.contains(arbLifecycleHookNotification.EC2InstanceId.value))) + arbAutoScalingInstancesType + else + AutoScalingInstancesType() } - } - val sns = new SnsAlg[IO] { - override def publish(topic: SnsTopicArn, message: String): IO[Unit] = - capturedPublishRequests.update(_ + (topic -> message)).void - } + override def completeLifecycleAction(lifecycleHookName: AsciiStringMaxLen255, + autoScalingGroupName: ResourceName, + lifecycleActionResult: LifecycleActionResult, + lifecycleActionToken: Option[LifecycleActionToken], + instanceId: Option[XmlStringMaxLen19]): IO[CompleteLifecycleActionAnswer] = + IO.raiseError(new RuntimeException("AutoScalingAsyncClient.completeLifecycleAction should not have been called")) + } - AutoScalingAlg[IO](autoScalingClient, sns) - .pauseAndRecurse(arbSnsTopicArn, arbLifecycleHookNotification, guardState) + val sns = new SnsAlg[IO] { + override def publish(topic: TopicARN, message: Message): IO[Unit] = + capturedPublishRequests.update(_ + (topic -> message)).void } + + AutoScalingAlg[IO](autoScalingClient, sns) + .pauseAndRecurse(arbSnsTopicArn, arbLifecycleHookNotification, guardState) } _ <- control.tick _ <- control.tickFor(4.seconds) @@ -111,17 +106,18 @@ class AutoScalingAlgImplSpec assert(firstShouldBeEmpty.isEmpty) val arbLifecycleState: Option[LifecycleState] = - arbDescribeAutoScalingInstancesResponse - .autoScalingInstances() - .asScala - .collectFirst { - case instance if Ec2InstanceId(instance.instanceId()) == arbLifecycleHookNotification.EC2InstanceId => - LifecycleState.fromString(instance.lifecycleState()) + arbAutoScalingInstancesType + .autoScalingInstances + .flatMap { + _.collectFirstSome { + case instance if InstanceId(instance.instanceId.value) == arbLifecycleHookNotification.EC2InstanceId => + LifecycleState.fromString(instance.lifecycleState.value) + case _ => None + } } - .flatten - + if (arbLifecycleState.contains(guardState)) { - assert(finalCapturedPublishRequests.contains(arbSnsTopicArn -> arbLifecycleHookNotification.asJson.noSpaces)) + assert(finalCapturedPublishRequests.contains(arbSnsTopicArn -> Message(arbLifecycleHookNotification.asJson.noSpaces))) } else { assert(finalCapturedPublishRequests.isEmpty, s"Input Lifecycle State was $arbLifecycleState, not $guardState, so we should have stopped without publishing any messages") } diff --git a/core-tests/src/test/scala/com/dwolla/aws/autoscaling/LifecycleHookHandlerSpec.scala b/core-tests/src/test/scala/com/dwolla/aws/autoscaling/LifecycleHookHandlerSpec.scala index b0dc879..2a7f76f 100644 --- a/core-tests/src/test/scala/com/dwolla/aws/autoscaling/LifecycleHookHandlerSpec.scala +++ b/core-tests/src/test/scala/com/dwolla/aws/autoscaling/LifecycleHookHandlerSpec.scala @@ -1,4 +1,5 @@ -package com.dwolla.aws.autoscaling +package com.dwolla.aws +package autoscaling import _root_.io.circe.* import _root_.io.circe.literal.* @@ -7,13 +8,14 @@ import cats.effect.* import cats.syntax.all.* import com.dwolla.aws import com.dwolla.aws.autoscaling.given -import com.dwolla.aws.sns.{SnsTopicArn, given} +import com.dwolla.aws.sns.given import feral.lambda.events.SnsEvent import feral.lambda.{Context, ContextInstances, LambdaEnv} import munit.{CatsEffectSuite, ScalaCheckEffectSuite} import org.scalacheck.effect.PropF.forAllF import org.typelevel.log4cats.LoggerFactory import org.typelevel.log4cats.noop.NoOpFactory +import com.amazonaws.sns.TopicARN class LifecycleHookHandlerSpec extends CatsEffectSuite @@ -21,7 +23,7 @@ class LifecycleHookHandlerSpec with ContextInstances { given LoggerFactory[IO] = NoOpFactory[IO] - private def snsMessage[T: Encoder](topic: SnsTopicArn, detail: T, maybeSubject: Option[String]): Json = + private def snsMessage[T: Encoder](topic: TopicARN, detail: T, maybeSubject: Option[String]): Json = json"""{ "Records": [ { @@ -66,14 +68,14 @@ class LifecycleHookHandlerSpec }""" test("LifecycleHookHandler should handle a message") { - forAllF { (arbSnsTopicArn: SnsTopicArn, + forAllF { (arbSnsTopicArn: TopicARN, arbContext: Context[IO], arbLifecycleHookNotification: LifecycleHookNotification, arbSubject: Option[String], ) => for { deferredLifecycleHookNotification <- Deferred[IO, LifecycleHookNotification] - deferredSnsTopicArn <- Deferred[IO, SnsTopicArn] + deferredSnsTopicArn <- Deferred[IO, TopicARN] eventHandler = LifecycleHookHandler { case (arn, notif) => deferredLifecycleHookNotification.complete(notif) >> @@ -94,7 +96,7 @@ class LifecycleHookHandlerSpec } test("LifecycleHookHandler should handle a test notification message") { - forAllF { (arbSnsTopicArn: SnsTopicArn, + forAllF { (arbSnsTopicArn: TopicARN, arbContext: Context[IO], arbSubject: Option[String], ) => diff --git a/core-tests/src/test/scala/com/dwolla/aws/cloudformation/CloudFormationAlgSpec.scala b/core-tests/src/test/scala/com/dwolla/aws/cloudformation/CloudFormationAlgSpec.scala index bf7e1b0..aee8460 100644 --- a/core-tests/src/test/scala/com/dwolla/aws/cloudformation/CloudFormationAlgSpec.scala +++ b/core-tests/src/test/scala/com/dwolla/aws/cloudformation/CloudFormationAlgSpec.scala @@ -1,19 +1,17 @@ package com.dwolla.aws.cloudformation import cats.effect.* -import cats.effect.std.Dispatcher -import com.dwolla.aws +import cats.syntax.all.* +import com.amazonaws.cloudformation.* import com.dwolla.aws.cloudformation.given +import com.dwolla.aws.given import munit.{CatsEffectSuite, ScalaCheckEffectSuite} import org.scalacheck.Arbitrary import org.scalacheck.effect.PropF.forAllF import org.typelevel.log4cats.LoggerFactory import org.typelevel.log4cats.noop.NoOpFactory -import software.amazon.awssdk.services.cloudformation.CloudFormationAsyncClient -import software.amazon.awssdk.services.cloudformation.model.{CloudFormationException, DescribeStackResourceRequest, DescribeStackResourceResponse, StackResourceDetail} - -import java.util.concurrent.CompletableFuture -import scala.jdk.CollectionConverters.* +import smithy4s.Timestamp +import smithy4s.http.UnknownErrorResponse class CloudFormationAlgSpec extends CatsEffectSuite @@ -22,53 +20,42 @@ class CloudFormationAlgSpec given LoggerFactory[IO] = NoOpFactory[IO] test("CloudFormationAlg should return the physical resource ID for the given parameters, if it can") { - forAllF { (stack: StackArn, - logicalResourceId: LogicalResourceId, + forAllF { (arbStack: StackArn, + arbLogicalResourceId: LogicalResourceId, maybePhysicalResourceId: Option[PhysicalResourceId], + resourceType: ResourceType, + timestamp: Timestamp, + resourceStatus: ResourceStatus, ) => - Dispatcher.sequential[IO].use { dispatcher => - val cfn = new CloudFormationAsyncClient { - override def serviceName(): String = "FakeCloudFormationAsyncClient" - override def close(): Unit = () - - override def describeStackResource(req: DescribeStackResourceRequest): CompletableFuture[DescribeStackResourceResponse] = - dispatcher.unsafeToCompletableFuture { - if (req.stackName() == stack.value && req.logicalResourceId() == logicalResourceId.value) { - maybePhysicalResourceId - .fold { - IO.raiseError[DescribeStackResourceResponse] { - CloudFormationException.builder() - .message(s"Resource ${logicalResourceId.value} does not exist for stack ${stack.value}") - .build() - } - } { physicalResourceId => - IO.pure { - DescribeStackResourceResponse.builder() - .stackResourceDetail { - StackResourceDetail.builder() - .stackId(stack.value) - .logicalResourceId(logicalResourceId.value) - .physicalResourceId(physicalResourceId.value) - .build() - } - .build() - } - } - } else { - IO.raiseError[DescribeStackResourceResponse] { - CloudFormationException.builder() - .message(s"Stack '${stack.value}' does not exist") - .build() - } - } + val cfn = new CloudFormation.Default[IO](new NotImplementedError().raiseError) { + override def describeStackResources(stackName: Option[StackName], + logicalResourceId: Option[LogicalResourceId], + physicalResourceId: Option[PhysicalResourceId]): IO[DescribeStackResourcesOutput] = { + if (stackName.exists(_.value == arbStack.value) && logicalResourceId.exists(_.value == arbLogicalResourceId.value)) { + maybePhysicalResourceId + .fold { + DescribeStackResourcesOutput(None) + } { physicalResourceId => + DescribeStackResourcesOutput(StackResource( + logicalResourceId = arbLogicalResourceId, + resourceType = resourceType, + timestamp = timestamp, + resourceStatus = resourceStatus, + stackId = StackId(arbStack.value).some, + physicalResourceId = physicalResourceId.some, + ).pure[List].some) + }.pure[IO] + } else + IO.raiseError[DescribeStackResourcesOutput] { + UnknownErrorResponse(400, Map.empty, s"Stack with id ${arbStack.value} does not exist") } } + } - for { - output <- CloudFormationAlg[IO](cfn).physicalResourceIdFor(stack, logicalResourceId) - } yield { - assertEquals(output, maybePhysicalResourceId) - } + for { + output <- CloudFormationAlg[IO](cfn).physicalResourceIdFor(arbStack, arbLogicalResourceId) + } yield { + assertEquals(output, maybePhysicalResourceId) } } } diff --git a/core-tests/src/test/scala/com/dwolla/aws/cloudformation/TestApp.scala b/core-tests/src/test/scala/com/dwolla/aws/cloudformation/TestApp.scala index 351c778..994a523 100644 --- a/core-tests/src/test/scala/com/dwolla/aws/cloudformation/TestApp.scala +++ b/core-tests/src/test/scala/com/dwolla/aws/cloudformation/TestApp.scala @@ -2,18 +2,23 @@ package com.dwolla.aws.cloudformation import cats.effect.* import cats.syntax.all.* +import com.amazonaws.cloudformation.{CloudFormation, LogicalResourceId} +import org.http4s.ember.client.EmberClientBuilder import org.typelevel.log4cats.LoggerFactory -import org.typelevel.log4cats.noop.NoOpFactory -import software.amazon.awssdk.services.cloudformation.CloudFormationAsyncClient +import org.typelevel.log4cats.slf4j.Slf4jFactory +import smithy4s.aws.{AwsClient, AwsEnvironment, AwsRegion} object TestApp extends ResourceApp.Simple { - given LoggerFactory[IO] = NoOpFactory[IO] + given LoggerFactory[IO] = Slf4jFactory.create[IO] - private def stackArn = StackArn(???) - private val logicalResourceId = LogicalResourceId(???) + private def stackArn = StackArn("does-not-exist") + private val logicalResourceId = LogicalResourceId("bar") override def run: Resource[IO, Unit] = - Resource.fromAutoCloseable(IO(CloudFormationAsyncClient.builder().build())) + EmberClientBuilder.default[IO] + .build + .flatMap(AwsEnvironment.default(_, AwsRegion.US_WEST_2)) + .flatMap(AwsClient(CloudFormation, _)) .map(CloudFormationAlg[IO](_)) .evalMap { alg => alg.physicalResourceIdFor(stackArn, logicalResourceId) diff --git a/core-tests/src/test/scala/com/dwolla/aws/ec2/Ec2AlgSpec.scala b/core-tests/src/test/scala/com/dwolla/aws/ec2/Ec2AlgSpec.scala index 4fe1c36..22188ff 100644 --- a/core-tests/src/test/scala/com/dwolla/aws/ec2/Ec2AlgSpec.scala +++ b/core-tests/src/test/scala/com/dwolla/aws/ec2/Ec2AlgSpec.scala @@ -1,18 +1,15 @@ package com.dwolla.aws.ec2 import cats.effect.* -import cats.effect.std.Dispatcher +import cats.syntax.all.* +import com.amazonaws.ec2.{EC2, Instance, DescribeInstancesResult, Filter, InstanceId, Reservation} import com.dwolla.aws.* import munit.{CatsEffectSuite, ScalaCheckEffectSuite} import org.scalacheck.effect.PropF.forAllF import org.typelevel.log4cats.LoggerFactory import org.typelevel.log4cats.noop.NoOpFactory -import software.amazon.awssdk.services.ec2.Ec2AsyncClient -import software.amazon.awssdk.services.ec2.model.{DescribeInstancesRequest, DescribeInstancesResponse, Instance, Reservation} import com.dwolla.aws -import java.util.concurrent.CompletableFuture -import scala.jdk.CollectionConverters.* import com.dwolla.aws.ec2.given class Ec2AlgSpec @@ -23,34 +20,38 @@ class Ec2AlgSpec test("Ec2Alg should retrieve the tags for an instance") { forAllF { (instance: Instance) => - Dispatcher.sequential[IO].use { dispatcher => - val fakeClient = new Ec2AsyncClient { - override def serviceName(): String = "FakeEc2AsyncClient" - override def close(): Unit = () - - override def describeInstances(req: DescribeInstancesRequest): CompletableFuture[DescribeInstancesResponse] = - dispatcher.unsafeToCompletableFuture { + instance + .instanceId + .traverse { instanceId => + val fakeClient = new EC2.Default[IO](new NotImplementedError().raiseError) { + override def describeInstances(dryRun: Boolean, + maxResults: Int, + filters: Option[List[Filter]], + instanceIds: Option[List[InstanceId]], + nextToken: Option[String]): IO[DescribeInstancesResult] = IO.pure { - if (req.instanceIds().contains(instance.instanceId())) { - DescribeInstancesResponse - .builder() - .reservations(Reservation.builder().instances(instance).build()) - .build() - } else { - DescribeInstancesResponse - .builder() - .build() - } + if (instanceIds.exists(_.contains(instanceId))) + DescribeInstancesResult(Reservation(instances = instance.pure[List].some).pure[List].some) + else + DescribeInstancesResult() } - } - } + } - for { - output <- Ec2Alg[IO](fakeClient).getTagsForInstance(Ec2InstanceId(instance.instanceId())) - } yield { - assertEquals(output, instance.tags().asScala.toList.map { tag => Tag(TagName(tag.key), TagValue(tag.value))}) + for { + output <- Ec2Alg[IO](fakeClient).getTagsForInstance(InstanceId(instanceId)) + } yield { + assertEquals(output, + instance + .tags + .toList + .flatten + .map { tag => + (tag.key.map(TagName.apply), tag.value.map(TagValue.apply)).mapN(Tag.apply) + } + .flattenOption) + } } - } + .map(_.getOrElse(())) } } } diff --git a/core-tests/src/test/scala/com/dwolla/aws/ecs/EcsAlgSpec.scala b/core-tests/src/test/scala/com/dwolla/aws/ecs/EcsAlgSpec.scala index 9353098..293a57b 100644 --- a/core-tests/src/test/scala/com/dwolla/aws/ecs/EcsAlgSpec.scala +++ b/core-tests/src/test/scala/com/dwolla/aws/ecs/EcsAlgSpec.scala @@ -24,7 +24,7 @@ class EcsAlgSpec given [F[_] : Applicative]: LoggerFactory[F] = NoOpFactory[F] - def fakeECS[F[_] : MonadThrow](arbCluster: ArbitraryCluster): ECS[F] = new FakeECS[F] { + def fakeECS(arbCluster: ArbitraryCluster): ECS[IO] = new ECS.Default[IO](new NotImplementedError().raiseError) { private lazy val listClustersResponses: Map[NextPageToken, ListClustersResponse] = ArbitraryPagination.paginateWith[Chunk, ArbitraryCluster, ClusterWithInstances, ClusterArn](arbCluster) { case ClusterWithInstances((c, _)) => c.clusterArn @@ -111,7 +111,7 @@ class EcsAlgSpec } override def listClusters(nextToken: Option[String], - maxResults: Option[BoxedInteger]): F[ListClustersResponse] = + maxResults: Option[BoxedInteger]): IO[ListClustersResponse] = rejectParameters("listCluster")(maxResults.as("maxResults")) .as(listClustersResponses(NextPageToken(nextToken))) @@ -119,7 +119,7 @@ class EcsAlgSpec filter: Option[String], nextToken: Option[String], maxResults: Option[BoxedInteger], - status: Option[com.amazonaws.ecs.ContainerInstanceStatus]): F[ListContainerInstancesResponse] = + status: Option[com.amazonaws.ecs.ContainerInstanceStatus]): IO[ListContainerInstancesResponse] = rejectParameters("listContainerInstances")( filter.as("filter"), maxResults.as("maxResults"), @@ -133,20 +133,20 @@ class EcsAlgSpec override def describeContainerInstances(containerInstances: List[String], cluster: Option[String], - include: Option[List[ContainerInstanceField]]): F[DescribeContainerInstancesResponse] = { + include: Option[List[ContainerInstanceField]]): IO[DescribeContainerInstancesResponse] = { val requestedCluster = ClusterArn(cluster.getOrElse("default")) val allInstances = clusterMap.getOrElse(requestedCluster, List.empty) val cis: List[ContainerInstance] = allInstances.filter(i => containerInstances.contains(i.containerInstanceId.value)) - DescribeContainerInstancesResponse(containerInstances = cis.map(ciToCi).some).pure[F] + DescribeContainerInstancesResponse(containerInstances = cis.map(ciToCi).some).pure[IO] } private def rejectParameters(method: String) - (options: Option[String]*): F[Unit] = + (options: Option[String]*): IO[Unit] = options .traverse(_.toInvalidNel(())) .leftMap(s => new RuntimeException(s"$method called with unimplemented parameters ${s.mkString_(", ")}")) - .liftTo[F] + .liftTo[IO] .void override def listTasks(cluster: Option[String], @@ -157,7 +157,7 @@ class EcsAlgSpec startedBy: Option[String], serviceName: Option[String], desiredStatus: Option[DesiredStatus], - launchType: Option[LaunchType]): F[ListTasksResponse] = + launchType: Option[LaunchType]): IO[ListTasksResponse] = rejectParameters("listTasks")( family.as("family"), maxResults.as("maxResults"), @@ -173,7 +173,7 @@ class EcsAlgSpec override def describeTasks(tasks: List[String], cluster: Option[String], - include: Option[List[TaskField]]): F[DescribeTasksResponse] = + include: Option[List[TaskField]]): IO[DescribeTasksResponse] = rejectParameters("describeTasks")(include.as("maxResults")) .as { val tasksSet = tasks.toSet @@ -253,7 +253,7 @@ class EcsAlgSpec deferredContainerInstances <- Deferred[IO, List[ContainerInstanceId]] deferredStatus <- Deferred[IO, com.amazonaws.ecs.ContainerInstanceStatus] deferredCluster <- Deferred[IO, Option[ClusterArn]] - fakeEcsClient: ECS[IO] = new FakeECS[IO] { + fakeEcsClient: ECS[IO] = new ECS.Default[IO](new NotImplementedError().raiseError) { override def updateContainerInstancesState(containerInstances: List[String], status: com.amazonaws.ecs.ContainerInstanceStatus, cluster: Option[String]): IO[UpdateContainerInstancesStateResponse] = @@ -278,7 +278,7 @@ class EcsAlgSpec forAllF { (cluster: ClusterArn, ci: ContainerInstance) => val activeContainerInstance = ci.copy(status = ContainerInstanceStatus.Draining) - EcsAlg[IO](new FakeECS[IO] {}).drainInstance(cluster, activeContainerInstance) + EcsAlg[IO](new ECS.Default[IO](new NotImplementedError().raiseError)).drainInstance(cluster, activeContainerInstance) } } @@ -301,7 +301,7 @@ class EcsAlgSpec val allTasks: Map[TaskArn, (TaskStatus, TaskDefinitionArn)] = allTaskPages.flatten.map { case (a, s, tda) => a -> (s, tda) }.toMap - val alg = EcsAlg(new FakeECS[IO] { + val alg = EcsAlg(new ECS.Default[IO](new NotImplementedError().raiseError) { override def listTasks(cluster: Option[String], containerInstance: Option[String], family: Option[String], diff --git a/core-tests/src/test/scala/com/dwolla/aws/sns/SnsAlgSpec.scala b/core-tests/src/test/scala/com/dwolla/aws/sns/SnsAlgSpec.scala index 24fe155..8da5744 100644 --- a/core-tests/src/test/scala/com/dwolla/aws/sns/SnsAlgSpec.scala +++ b/core-tests/src/test/scala/com/dwolla/aws/sns/SnsAlgSpec.scala @@ -1,18 +1,14 @@ package com.dwolla.aws.sns import cats.effect.* -import cats.effect.std.Dispatcher +import cats.syntax.all.* +import com.amazonaws.sns.* import com.dwolla.aws import com.dwolla.aws.sns.given import munit.{CatsEffectSuite, ScalaCheckEffectSuite} import org.scalacheck.effect.PropF.forAllF import org.typelevel.log4cats.* import org.typelevel.log4cats.noop.NoOpFactory -import software.amazon.awssdk.services.sns.SnsAsyncClient -import software.amazon.awssdk.services.sns.model.{PublishRequest, PublishResponse} - -import java.util.UUID -import java.util.concurrent.CompletableFuture class SnsAlgSpec extends CatsEffectSuite @@ -21,28 +17,28 @@ class SnsAlgSpec given LoggerFactory[IO] = NoOpFactory[IO] test("SnsAlg should publish a message using the underlying client") { - forAllF { (topic: SnsTopicArn, message: String, messageId: UUID) => - Dispatcher.sequential[IO].use { dispatcher => - for { - deferredTopicAndMessage <- Deferred[IO, (SnsTopicArn, String)] - fakeClient = new SnsAsyncClient { - override def serviceName(): String = "FakeSnsAsyncClient" - override def close(): Unit = () - - override def publish(publishRequest: PublishRequest): CompletableFuture[PublishResponse] = - dispatcher.unsafeToCompletableFuture { - deferredTopicAndMessage.complete(SnsTopicArn(publishRequest.topicArn()) -> publishRequest.message()) - .as(PublishResponse.builder().messageId(messageId.toString).build()) - } - } + forAllF { (topic: TopicARN, message: Message, messageId: MessageId) => + for { + deferredTopicAndMessage <- Deferred[IO, (Option[TopicARN], Message)] + fakeClient = new SNS.Default[IO](new NotImplementedError().raiseError) { + override def publish(message: Message, + topicArn: Option[TopicARN], + targetArn: Option[String], + phoneNumber: Option[String], + subject: Option[Subject], + messageStructure: Option[MessageStructure], + messageAttributes: Option[Map[String, MessageAttributeValue]], + messageDeduplicationId: Option[String], + messageGroupId: Option[String]): IO[PublishResponse] = + deferredTopicAndMessage.complete(topicArn -> message).as(PublishResponse()) + } - _ <- SnsAlg[IO](fakeClient).publish(topic, message) + _ <- SnsAlg[IO](fakeClient).publish(topic, message) - (capturedTopic, capturedMessage) <- deferredTopicAndMessage.get - } yield { - assertEquals(capturedTopic, topic) - assertEquals(capturedMessage, message) - } + (capturedTopic, capturedMessage) <- deferredTopicAndMessage.get + } yield { + assertEquals(capturedTopic, topic.some) + assertEquals(capturedMessage, message) } } } diff --git a/core/src/main/scala/com/dwolla/aws/autoscaling/AutoScalingAlg.scala b/core/src/main/scala/com/dwolla/aws/autoscaling/AutoScalingAlg.scala index 64591cf..c934347 100644 --- a/core/src/main/scala/com/dwolla/aws/autoscaling/AutoScalingAlg.scala +++ b/core/src/main/scala/com/dwolla/aws/autoscaling/AutoScalingAlg.scala @@ -3,19 +3,18 @@ package com.dwolla.aws.autoscaling import cats.effect.* import cats.effect.syntax.all.* import cats.syntax.all.* +import com.amazonaws.autoscaling.{AutoScaling, LifecycleActionResult, XmlStringMaxLen19} +import com.amazonaws.ec2.InstanceId +import com.amazonaws.sns.* import com.dwolla.aws.autoscaling.LifecycleState.* import com.dwolla.aws.sns.* -import com.dwolla.aws.ec2.* import io.circe.syntax.* import org.typelevel.log4cats.{Logger, LoggerFactory} -import software.amazon.awssdk.services.autoscaling.* -import software.amazon.awssdk.services.autoscaling.model.{CompleteLifecycleActionRequest, DescribeAutoScalingInstancesRequest} import scala.concurrent.duration.* -import scala.jdk.CollectionConverters.* trait AutoScalingAlg[F[_]] { - def pauseAndRecurse(topic: SnsTopicArn, + def pauseAndRecurse(topic: TopicARN, lifecycleHookNotification: LifecycleHookNotification, onlyIfInState: LifecycleState, ): F[Unit] @@ -23,31 +22,35 @@ trait AutoScalingAlg[F[_]] { } object AutoScalingAlg { - def apply[F[_] : Async : LoggerFactory](autoScalingClient: AutoScalingAsyncClient, + def apply[F[_] : Async : LoggerFactory](autoScalingClient: AutoScaling[F], sns: SnsAlg[F]): AutoScalingAlg[F] = new AutoScalingAlgImpl(autoScalingClient, sns) } -class AutoScalingAlgImpl[F[_] : Async : LoggerFactory](autoScalingClient: AutoScalingAsyncClient, +extension (i: InstanceId) { + def forAutoScaling: XmlStringMaxLen19 = XmlStringMaxLen19(i.value) +} + +class AutoScalingAlgImpl[F[_] : Async : LoggerFactory](autoScalingClient: AutoScaling[F], sns: SnsAlg[F]) extends AutoScalingAlg[F] { - private def getInstanceLifecycleState(ec2Instance: Ec2InstanceId): F[Option[LifecycleState]] = + private def getInstanceLifecycleState(ec2Instance: InstanceId): F[Option[LifecycleState]] = LoggerFactory[F].create.flatMap { case given Logger[F] => for { _ <- Logger[F].info(s"checking lifecycle state for instance $ec2Instance") - req = DescribeAutoScalingInstancesRequest.builder().instanceIds(ec2Instance.value).build() - resp <- Async[F].fromCompletableFuture(Sync[F].delay(autoScalingClient.describeAutoScalingInstances(req))) + resp <- autoScalingClient.describeAutoScalingInstances(ec2Instance.forAutoScaling.pure[List].some) } yield resp - .autoScalingInstances() - .asScala - .collectFirst { - case instance if Ec2InstanceId(instance.instanceId()) == ec2Instance => - LifecycleState.fromString(instance.lifecycleState()) + .autoScalingInstances + .flatMap { + _.collectFirstSome { + case instance if InstanceId(instance.instanceId.value) == ec2Instance => + LifecycleState.fromString(instance.lifecycleState.value) + case _ => None + } } - .flatten } - override def pauseAndRecurse(t: SnsTopicArn, + override def pauseAndRecurse(t: TopicARN, l: LifecycleHookNotification, onlyIfInState: LifecycleState, ): F[Unit] = { @@ -58,22 +61,20 @@ class AutoScalingAlgImpl[F[_] : Async : LoggerFactory](autoScalingClient: AutoSc getInstanceLifecycleState(l.EC2InstanceId) .map(_.contains(onlyIfInState)) .ifM( - sns.publish(t, l.asJson.noSpaces).delayBy(sleepDuration), + sns.publish(t, Message(l.asJson.noSpaces)).delayBy(sleepDuration), Logger[F].info("Instance not in PendingWait status; refusing to republish the SNS message") ) } } - override def continueAutoScaling(l: LifecycleHookNotification): F[Unit] = { - val req = CompleteLifecycleActionRequest.builder() - .lifecycleHookName(l.lifecycleHookName.value) - .autoScalingGroupName(l.autoScalingGroupName.value) - .lifecycleActionResult("CONTINUE") - .instanceId(l.EC2InstanceId.value) - .build() - + override def continueAutoScaling(l: LifecycleHookNotification): F[Unit] = LoggerFactory[F].create.flatMap(_.info(s"continuing auto scaling for ${l.autoScalingGroupName}")) >> - Async[F].fromCompletableFuture(Sync[F].delay(autoScalingClient.completeLifecycleAction(req))).void - } + autoScalingClient.completeLifecycleAction( + lifecycleHookName = l.lifecycleHookName.value, + autoScalingGroupName = l.autoScalingGroupName.value, + lifecycleActionResult = LifecycleActionResult("CONTINUE"), + instanceId = l.EC2InstanceId.forAutoScaling.some, + ) + .void } diff --git a/core/src/main/scala/com/dwolla/aws/autoscaling/LifecycleHookHandler.scala b/core/src/main/scala/com/dwolla/aws/autoscaling/LifecycleHookHandler.scala index da5fb85..8213aab 100644 --- a/core/src/main/scala/com/dwolla/aws/autoscaling/LifecycleHookHandler.scala +++ b/core/src/main/scala/com/dwolla/aws/autoscaling/LifecycleHookHandler.scala @@ -2,14 +2,15 @@ package com.dwolla.aws.autoscaling import cats.* import cats.syntax.all.* +import com.amazonaws.sns.TopicARN import com.dwolla.aws.sns.* -import feral.lambda.{INothing, LambdaEnv} import feral.lambda.events.SnsEvent +import feral.lambda.{INothing, LambdaEnv} import fs2.Stream import org.typelevel.log4cats.LoggerFactory object LifecycleHookHandler { - def apply[F[_] : MonadThrow : LoggerFactory](eventBridge: (SnsTopicArn, LifecycleHookNotification) => F[Unit]) + def apply[F[_] : MonadThrow : LoggerFactory](eventBridge: (TopicARN, LifecycleHookNotification) => F[Unit]) (using fs2.Compiler[F, F]): LambdaEnv[F, SnsEvent] => F[Option[INothing]] = env => Stream.eval(env.event) .map(_.records) diff --git a/core/src/main/scala/com/dwolla/aws/autoscaling/model.scala b/core/src/main/scala/com/dwolla/aws/autoscaling/model.scala index 3be6602..0918622 100644 --- a/core/src/main/scala/com/dwolla/aws/autoscaling/model.scala +++ b/core/src/main/scala/com/dwolla/aws/autoscaling/model.scala @@ -1,23 +1,23 @@ -package com.dwolla.aws.autoscaling +package com.dwolla.aws +package autoscaling import cats.FlatMap import cats.syntax.all.* +import com.amazonaws.autoscaling.* +import com.amazonaws.ec2.* import com.dwolla.aws.AccountId -import com.dwolla.aws.ec2.Ec2InstanceId import io.circe.* import monix.newtypes.NewtypeWrapped import monix.newtypes.integrations.* +import smithy4s.{Bijection, Newtype} import java.time.Instant type AutoScalingGroupName = AutoScalingGroupName.Type -object AutoScalingGroupName extends NewtypeWrapped[String] +object AutoScalingGroupName extends NewtypeWrapped[ResourceName] type LifecycleHookName = LifecycleHookName.Type -object LifecycleHookName extends NewtypeWrapped[String] - -type LifecycleTransition = LifecycleTransition.Type -object LifecycleTransition extends NewtypeWrapped[String] +object LifecycleHookName extends NewtypeWrapped[AsciiStringMaxLen255] sealed trait AutoScalingSnsMessage case class LifecycleHookNotification(service: String, @@ -27,7 +27,7 @@ case class LifecycleHookNotification(service: String, accountId: AccountId, autoScalingGroupName: AutoScalingGroupName, lifecycleHookName: LifecycleHookName, - EC2InstanceId: Ec2InstanceId, + EC2InstanceId: InstanceId, lifecycleTransition: LifecycleTransition, notificationMetadata: Option[String], ) extends AutoScalingSnsMessage @@ -41,6 +41,12 @@ case class TestNotification(accountId: AccountId, ) extends AutoScalingSnsMessage object LifecycleHookNotification extends DerivedCirceCodec { + def foo[B <: Newtype[A]#Type, A: Encoder](using Bijection[A, B]): Encoder[B] = Encoder[A].contramap(summon[Bijection[A, B]].from) + foo[ResourceName, String] + + summon[Encoder[ResourceName]] + summon[Encoder[AutoScalingGroupName]] + given Encoder[LifecycleHookNotification] = Encoder.forProduct10( "Service", diff --git a/core/src/main/scala/com/dwolla/aws/cloudformation/CloudFormationAlg.scala b/core/src/main/scala/com/dwolla/aws/cloudformation/CloudFormationAlg.scala index afd20cc..ef940a2 100644 --- a/core/src/main/scala/com/dwolla/aws/cloudformation/CloudFormationAlg.scala +++ b/core/src/main/scala/com/dwolla/aws/cloudformation/CloudFormationAlg.scala @@ -2,47 +2,27 @@ package com.dwolla.aws.cloudformation import cats.effect.* import cats.syntax.all.* +import com.amazonaws.cloudformation.* import monix.newtypes.NewtypeWrapped import org.typelevel.log4cats.* -import software.amazon.awssdk.services.cloudformation.CloudFormationAsyncClient -import software.amazon.awssdk.services.cloudformation.model.DescribeStackResourceRequest -import software.amazon.awssdk.services.cloudformation.model.CloudFormationException +import smithy4s.http.UnknownErrorResponse type StackArn = StackArn.Type object StackArn extends NewtypeWrapped[String] -type LogicalResourceId = LogicalResourceId.Type -object LogicalResourceId extends NewtypeWrapped[String] - -type PhysicalResourceId = PhysicalResourceId.Type -object PhysicalResourceId extends NewtypeWrapped[String] - trait CloudFormationAlg[F[_]] { def physicalResourceIdFor(stack: StackArn, logicalResourceId: LogicalResourceId): F[Option[PhysicalResourceId]] } object CloudFormationAlg { - def apply[F[_] : Async : LoggerFactory](client: CloudFormationAsyncClient): CloudFormationAlg[F] = new CloudFormationAlg[F] { + def apply[F[_] : Async : LoggerFactory](client: CloudFormation[F]): CloudFormationAlg[F] = new CloudFormationAlg[F] { override def physicalResourceIdFor(stack: StackArn, logicalResourceId: LogicalResourceId): F[Option[PhysicalResourceId]] = LoggerFactory[F].create.flatMap { case given Logger[F] => - val req = DescribeStackResourceRequest.builder() - .stackName(stack.value) - .logicalResourceId(logicalResourceId.value) - .build() - Logger[F].info(s"retrieving $logicalResourceId from $stack") >> - Async[F] - .fromCompletableFuture { - Sync[F].delay(client.describeStackResource(req)) - } - .map(_.stackResourceDetail().physicalResourceId()) - .map(PhysicalResourceId(_).some) + client.describeStackResources(StackName(stack.value).some, logicalResourceId.some) + .map(_.stackResources.flatMap(_.collectFirstSome(_.physicalResourceId))) .recoverWith { - case ex: CloudFormationException if ex.getMessage.startsWith(s"Resource $logicalResourceId does not exist for stack") => - Logger[F] - .trace(ex)(s"Could not find $logicalResourceId in $stack") - .as(None) - case ex: CloudFormationException if ex.getMessage.startsWith(s"Stack '$stack' does not exist") => + case ex: UnknownErrorResponse if ex.getMessage.contains(s"Stack with id ${stack.value} does not exist") => Logger[F] .trace(ex)(s"Could not find stack $stack") .as(None) diff --git a/core/src/main/scala/com/dwolla/aws/ec2/Ec2Alg.scala b/core/src/main/scala/com/dwolla/aws/ec2/Ec2Alg.scala index 019f7ac..1800bef 100644 --- a/core/src/main/scala/com/dwolla/aws/ec2/Ec2Alg.scala +++ b/core/src/main/scala/com/dwolla/aws/ec2/Ec2Alg.scala @@ -3,43 +3,35 @@ package com.dwolla.aws.ec2 import cats.effect.* import cats.syntax.all.* import com.dwolla.aws.* -import monix.newtypes.NewtypeWrapped import org.typelevel.log4cats.* -import software.amazon.awssdk.services.ec2.* -import software.amazon.awssdk.services.ec2.model.DescribeInstancesRequest - -import scala.jdk.CollectionConverters.* - -type Ec2InstanceId = Ec2InstanceId.Type -object Ec2InstanceId extends NewtypeWrapped[String] +import com.amazonaws.ec2.{Tag as _, *} trait Ec2Alg[F[_]] { - def getTagsForInstance(id: Ec2InstanceId): F[List[Tag]] + def getTagsForInstance(id: InstanceId): F[List[Tag]] } object Ec2Alg { - def apply[F[_]: Async : LoggerFactory](ec2Client: Ec2AsyncClient): Ec2Alg[F] = new Ec2Alg[F] { - override def getTagsForInstance(id: Ec2InstanceId): F[List[Tag]] = { - val req = DescribeInstancesRequest.builder().instanceIds(id.value).build() - + def apply[F[_]: Async : LoggerFactory](ec2Client: EC2[F]): Ec2Alg[F] = new Ec2Alg[F] { + override def getTagsForInstance(id: InstanceId): F[List[Tag]] = LoggerFactory[F].create.flatMap { case given Logger[F] => for { _ <- Logger[F].info(s"describing instance ${id.value}") - resp <- Async[F].fromCompletableFuture( - Sync[F].delay(ec2Client.describeInstances(req)) - ) + resp: DescribeInstancesResult <- ec2Client.describeInstances(instanceIds = id.pure[List].some) } yield { - resp - .reservations() - .asScala - .flatMap(_.instances().asScala) - .flatMap(_.tags().asScala) + resp.reservations + .collapse + .flatMap(_.instances.collapse) + .flatMap(_.tags.collapse) .map { t => - Tag(TagName(t.key()), TagValue(t.value())) + (t.key.map(TagName(_)), t.value.map(TagValue(_))).mapN(Tag.apply) } - .toList + .flattenOption } } - } } } + +extension [A] (maybeList: Option[List[A]]) { + def collapse: List[A] = + maybeList.getOrElse(List.empty) +} diff --git a/core/src/main/scala/com/dwolla/aws/ecs/EcsAlg.scala b/core/src/main/scala/com/dwolla/aws/ecs/EcsAlg.scala index 6f21ae8..dbb9b95 100644 --- a/core/src/main/scala/com/dwolla/aws/ecs/EcsAlg.scala +++ b/core/src/main/scala/com/dwolla/aws/ecs/EcsAlg.scala @@ -2,8 +2,8 @@ package com.dwolla.aws.ecs import cats.* import cats.syntax.all.* +import com.amazonaws.ec2.InstanceId import com.amazonaws.ecs.ECS -import com.dwolla.aws.ec2.* import com.dwolla.aws.ecs.* import com.dwolla.aws.ecs.TaskStatus.{Running, stoppedTaskStatuses} import com.dwolla.fs2utils.Pagination @@ -14,7 +14,7 @@ import org.typelevel.log4cats.{Logger, LoggerFactory} abstract class EcsAlg[F[_] : Applicative, G[_]] { def listClusterArns: G[ClusterArn] def listContainerInstances(cluster: ClusterArn): G[ContainerInstance] - def findEc2Instance(ec2InstanceId: Ec2InstanceId): F[Option[(ClusterArn, ContainerInstance)]] + def findEc2Instance(ec2InstanceId: InstanceId): F[Option[(ClusterArn, ContainerInstance)]] def drainInstance(cluster: ClusterArn, ci: ContainerInstance): F[Unit] = drainInstanceImpl(cluster, ci).unlessA(ci.status == ContainerInstanceStatus.Draining) @@ -49,7 +49,7 @@ object EcsAlg { .through(chunkedEcsRequest(ecs.describeContainerInstances(_, cluster = cluster.value.some))(_.containerInstances)) .map { ci => (ci.containerInstanceArn.map(ContainerInstanceId(_)), - ci.ec2InstanceId.map(Ec2InstanceId(_)), + ci.ec2InstanceId.map(InstanceId(_)), ci.status.flatMap(ContainerInstanceStatus.fromStatus), ) .tupled @@ -78,7 +78,7 @@ object EcsAlg { .map(extract(_).toChunk) .unchunks - override def findEc2Instance(ec2InstanceId: Ec2InstanceId): F[Option[(ClusterArn, ContainerInstance)]] = + override def findEc2Instance(ec2InstanceId: InstanceId): F[Option[(ClusterArn, ContainerInstance)]] = LoggerFactory[F].create.flatMap { case given Logger[F] => listClusterArns // TODO listContainerInstances could use a CQL expression to narrow the search diff --git a/core/src/main/scala/com/dwolla/aws/ecs/model.scala b/core/src/main/scala/com/dwolla/aws/ecs/model.scala index fbd137c..89a1970 100644 --- a/core/src/main/scala/com/dwolla/aws/ecs/model.scala +++ b/core/src/main/scala/com/dwolla/aws/ecs/model.scala @@ -1,10 +1,11 @@ package com.dwolla.aws.ecs import cats.Order -import monix.newtypes.* import cats.syntax.all.* +import com.amazonaws.ec2.InstanceId import com.dwolla.aws.AccountId -import com.dwolla.aws.ec2.Ec2InstanceId +import monix.newtypes.* +import smithy4s.aws.AwsRegion type ContainerInstanceId = ContainerInstanceId.Type object ContainerInstanceId extends NewtypeWrapped[String] @@ -18,15 +19,13 @@ type TaskCount = TaskCount.Type object TaskCount extends NewtypeWrapped[Long] { given Order[TaskCount] = Order[Long].contramap(_.value) } -type Region = Region.Type -object Region extends NewtypeWrapped[String] -case class Cluster(region: Region, accountId: AccountId, name: ClusterName) { +case class Cluster(region: AwsRegion, accountId: AccountId, name: ClusterName) { val clusterArn: ClusterArn = ClusterArn(s"arn:aws:ecs:$region:$accountId:cluster/$name") } case class ContainerInstance(containerInstanceId: ContainerInstanceId, - ec2InstanceId: Ec2InstanceId, + ec2InstanceId: InstanceId, countOfTasksNotStopped: TaskCount, status: ContainerInstanceStatus, ) diff --git a/core/src/main/scala/com/dwolla/aws/model.scala b/core/src/main/scala/com/dwolla/aws/model.scala index 3dd2f77..a895b66 100644 --- a/core/src/main/scala/com/dwolla/aws/model.scala +++ b/core/src/main/scala/com/dwolla/aws/model.scala @@ -1,6 +1,8 @@ package com.dwolla.aws import monix.newtypes.NewtypeWrapped +import io.circe.{Decoder, Encoder} +import smithy4s.{Bijection, Newtype} type AccountId = AccountId.Type object AccountId extends NewtypeWrapped[String] @@ -12,3 +14,6 @@ object TagValue extends NewtypeWrapped[String] case class Tag(name: TagName, value: TagValue) + +given[B <: Newtype[A]#Type, A: Encoder](using Bijection[A, B]): Encoder[B] = Encoder[A].contramap(summon[Bijection[A, B]].from) +given[B <: Newtype[A]#Type, A: Decoder](using Bijection[A, B]): Decoder[B] = Decoder[A].map(summon[Bijection[A, B]].to) diff --git a/core/src/main/scala/com/dwolla/aws/sns/ParseLifecycleHookNotification.scala b/core/src/main/scala/com/dwolla/aws/sns/ParseLifecycleHookNotification.scala index 22bb04a..b18bfbb 100644 --- a/core/src/main/scala/com/dwolla/aws/sns/ParseLifecycleHookNotification.scala +++ b/core/src/main/scala/com/dwolla/aws/sns/ParseLifecycleHookNotification.scala @@ -2,14 +2,14 @@ package com.dwolla.aws.sns import cats.* import cats.syntax.all.* +import com.amazonaws.sns.TopicARN import com.dwolla.aws.autoscaling.* -import com.dwolla.aws.sns.* import feral.lambda.events.SnsMessage import io.circe.* import org.typelevel.log4cats.LoggerFactory object ParseLifecycleHookNotification { - def apply[F[_] : MonadThrow : LoggerFactory]: SnsMessage => F[Option[(SnsTopicArn, LifecycleHookNotification)]] = s => + def apply[F[_] : MonadThrow : LoggerFactory]: SnsMessage => F[Option[(TopicARN, LifecycleHookNotification)]] = s => for { json <- io.circe.parser.parse(s.message).liftTo[F] autoScalingMessage <- json.as[AutoScalingSnsMessage].liftTo[F] @@ -19,5 +19,5 @@ object ParseLifecycleHookNotification { case TestNotification(_, requestId, _, _, _, _, _) => LoggerFactory[F].create.flatMap(_.info(s"ignoring TestNotification message $requestId")).as(None) } - } yield notification.tupleLeft(SnsTopicArn(s.topicArn)) + } yield notification.tupleLeft(TopicARN(s.topicArn)) } diff --git a/core/src/main/scala/com/dwolla/aws/sns/SnsAlg.scala b/core/src/main/scala/com/dwolla/aws/sns/SnsAlg.scala index 0fcd618..be9fb76 100644 --- a/core/src/main/scala/com/dwolla/aws/sns/SnsAlg.scala +++ b/core/src/main/scala/com/dwolla/aws/sns/SnsAlg.scala @@ -1,31 +1,20 @@ package com.dwolla.aws.sns -import cats.effect.* +import cats.* import cats.syntax.all.* -import software.amazon.awssdk.services.sns.SnsAsyncClient import org.typelevel.log4cats.* -import software.amazon.awssdk.services.sns.model.PublishRequest +import com.amazonaws.sns.* trait SnsAlg[F[_]] { - def publish(topic: SnsTopicArn, message: String): F[Unit] + def publish(topic: TopicARN, message: Message): F[Unit] } object SnsAlg { - def apply[F[_] : Async : LoggerFactory](client: SnsAsyncClient): SnsAlg[F] = new SnsAlg[F] { - override def publish(topic: SnsTopicArn, message: String): F[Unit] = - LoggerFactory[F].create.flatMap { case given Logger[F] => - val req = PublishRequest.builder() - .topicArn(topic.value) - .message(message) - .build() - - Logger[F].trace(s"Publishing message to $topic") >> - Async[F] - .fromCompletableFuture { - Sync[F].delay(client.publish(req)) - } - .void - } + def apply[F[_] : Apply : LoggerFactory](client: SNS[F]): SnsAlg[F] = new SnsAlg[F] { + private given Logger[F] = LoggerFactory[F].getLogger + override def publish(topic: TopicARN, message: Message): F[Unit] = + Logger[F].trace(s"Publishing message to $topic") *> + client.publish(message, topic.some).void } } diff --git a/core/src/main/scala/com/dwolla/aws/sns/model.scala b/core/src/main/scala/com/dwolla/aws/sns/model.scala deleted file mode 100644 index 8062849..0000000 --- a/core/src/main/scala/com/dwolla/aws/sns/model.scala +++ /dev/null @@ -1,10 +0,0 @@ -package com.dwolla.aws.sns - -import io.circe.* -import monix.newtypes.* - -type SnsTopicArn = SnsTopicArn.Type -object SnsTopicArn extends NewtypeWrapped[String] { - given Encoder[SnsTopicArn] = Encoder[String].contramap(_.value) - given Decoder[SnsTopicArn] = Decoder[String].map(SnsTopicArn(_)) -} diff --git a/draining/src/main/scala/com/dwolla/autoscaling/ecs/draining/TerminationEventBridge.scala b/draining/src/main/scala/com/dwolla/autoscaling/ecs/draining/TerminationEventBridge.scala index 6984dd3..466c8ec 100644 --- a/draining/src/main/scala/com/dwolla/autoscaling/ecs/draining/TerminationEventBridge.scala +++ b/draining/src/main/scala/com/dwolla/autoscaling/ecs/draining/TerminationEventBridge.scala @@ -2,13 +2,13 @@ package com.dwolla.autoscaling.ecs.draining import cats.* import cats.syntax.all.* +import com.amazonaws.sns.TopicARN import com.dwolla.aws.autoscaling.* import com.dwolla.aws.autoscaling.LifecycleState.TerminatingWait import com.dwolla.aws.ecs.* -import com.dwolla.aws.sns.SnsTopicArn class TerminationEventBridge[F[_] : Monad, G[_]](ECS: EcsAlg[F, G], AutoScaling: AutoScalingAlg[F]) { - def apply(topic: SnsTopicArn, lifecycleHook: LifecycleHookNotification): F[Unit] = + def apply(topic: TopicARN, lifecycleHook: LifecycleHookNotification): F[Unit] = for { maybeInstance <- ECS.findEc2Instance(lifecycleHook.EC2InstanceId) tasksRemaining <- maybeInstance match { @@ -21,6 +21,6 @@ class TerminationEventBridge[F[_] : Monad, G[_]](ECS: EcsAlg[F, G], AutoScaling: } object TerminationEventBridge { - def apply[F[_] : Monad, G[_]](ecsAlg: EcsAlg[F, G], autoScalingAlg: AutoScalingAlg[F]): (SnsTopicArn, LifecycleHookNotification) => F[Unit] = + def apply[F[_] : Monad, G[_]](ecsAlg: EcsAlg[F, G], autoScalingAlg: AutoScalingAlg[F]): (TopicARN, LifecycleHookNotification) => F[Unit] = new TerminationEventBridge(ecsAlg, autoScalingAlg).apply } diff --git a/draining/src/main/scala/com/dwolla/autoscaling/ecs/draining/TerminationEventHandler.scala b/draining/src/main/scala/com/dwolla/autoscaling/ecs/draining/TerminationEventHandler.scala index 6a1c254..556b756 100644 --- a/draining/src/main/scala/com/dwolla/autoscaling/ecs/draining/TerminationEventHandler.scala +++ b/draining/src/main/scala/com/dwolla/autoscaling/ecs/draining/TerminationEventHandler.scala @@ -2,7 +2,9 @@ package com.dwolla.autoscaling.ecs.draining import cats.* import cats.effect.* +import com.amazonaws.autoscaling.AutoScaling import com.amazonaws.ecs.ECS +import com.amazonaws.sns.SNS import com.dwolla.aws.autoscaling.{AutoScalingAlg, LifecycleHookHandler} import com.dwolla.aws.ecs.EcsAlg import com.dwolla.aws.sns.SnsAlg @@ -13,8 +15,6 @@ import org.typelevel.log4cats.LoggerFactory import org.typelevel.log4cats.slf4j.Slf4jFactory import smithy4s.aws.* import smithy4s.aws.kernel.AwsRegion -import software.amazon.awssdk.services.autoscaling.AutoScalingAsyncClient -import software.amazon.awssdk.services.sns.SnsAsyncClient class TerminationEventHandler extends IOLambda[SnsEvent, INothing] { override def handler: Resource[IO, LambdaEnv[IO, SnsEvent] => IO[Option[INothing]]] = @@ -23,8 +23,8 @@ class TerminationEventHandler extends IOLambda[SnsEvent, INothing] { given LoggerFactory[IO] = Slf4jFactory.create[IO] awsEnv <- AwsEnvironment.default(client, AwsRegion.US_WEST_2) ecs <- AwsClient(ECS, awsEnv).map(EcsAlg(_)) - autoscalingClient <- Resource.fromAutoCloseable(IO(AutoScalingAsyncClient.builder().build())) - sns <- Resource.fromAutoCloseable(IO(SnsAsyncClient.builder().build())).map(SnsAlg[IO](_)) + autoscalingClient <- AwsClient(AutoScaling, awsEnv) + sns <- AwsClient(SNS, awsEnv).map(SnsAlg[IO](_)) autoscaling = AutoScalingAlg[IO](autoscalingClient, sns) bridgeFunction = TerminationEventBridge(ecs, autoscaling) } yield LifecycleHookHandler[IO](bridgeFunction) diff --git a/draining/src/test/scala/com/dwolla/autoscaling/ecs/draining/TerminationEventBridgeSpec.scala b/draining/src/test/scala/com/dwolla/autoscaling/ecs/draining/TerminationEventBridgeSpec.scala index dfacec2..f584c59 100644 --- a/draining/src/test/scala/com/dwolla/autoscaling/ecs/draining/TerminationEventBridgeSpec.scala +++ b/draining/src/test/scala/com/dwolla/autoscaling/ecs/draining/TerminationEventBridgeSpec.scala @@ -1,11 +1,12 @@ package com.dwolla.autoscaling.ecs.draining import cats.effect.* +import com.amazonaws.ec2.InstanceId +import com.amazonaws.sns.TopicARN import com.dwolla.aws.autoscaling.LifecycleState.TerminatingWait import com.dwolla.aws.autoscaling.{*, given} -import com.dwolla.aws.ec2.Ec2InstanceId import com.dwolla.aws.ecs.{*, given} -import com.dwolla.aws.sns.{SnsTopicArn, given} +import com.dwolla.aws.sns.given import munit.{CatsEffectSuite, ScalaCheckEffectSuite} import org.scalacheck.effect.PropF.forAllF @@ -14,24 +15,24 @@ class TerminationEventBridgeSpec with ScalaCheckEffectSuite { test("TerminationEventBridge should mark a non-draining instance as draining and pause and recurse") { - forAllF { (arbSnsTopicArn: SnsTopicArn, + forAllF { (arbSnsTopicArn: TopicARN, arbLifecycleHookNotification: LifecycleHookNotification, arbClusterArn: ClusterArn, arbConInstId: ContainerInstanceId) => for { deferredDrainInstanceArgs <- Deferred[IO, (ClusterArn, ContainerInstance)] - deferredPauseAndRecurse <- Deferred[IO, (SnsTopicArn, LifecycleHookNotification, LifecycleState)] + deferredPauseAndRecurse <- Deferred[IO, (TopicARN, LifecycleHookNotification, LifecycleState)] expectedContainerInstance = ContainerInstance(arbConInstId, arbLifecycleHookNotification.EC2InstanceId, 1.asInstanceOf[TaskCount], ContainerInstanceStatus.Active) ecsAlg = new TestEcsAlg { - override def findEc2Instance(ec2InstanceId: Ec2InstanceId) = + override def findEc2Instance(ec2InstanceId: InstanceId) = IO.pure(Option((arbClusterArn, expectedContainerInstance))) override def drainInstanceImpl(cluster: ClusterArn, ci: ContainerInstance): IO[Unit] = deferredDrainInstanceArgs.complete((cluster, ci)).void } autoScalingAlg = new TestAutoScalingAlg { - override def pauseAndRecurse(topic: SnsTopicArn, + override def pauseAndRecurse(topic: TopicARN, lifecycleHookNotification: LifecycleHookNotification, onlyIfInState: LifecycleState, ): IO[Unit] = @@ -53,21 +54,21 @@ class TerminationEventBridgeSpec } test("TerminationEventBridge should pause and recurse if a draining instance still has tasks") { - forAllF { (arbSnsTopicArn: SnsTopicArn, + forAllF { (arbSnsTopicArn: TopicARN, arbLifecycleHookNotification: LifecycleHookNotification, arbClusterArn: ClusterArn, arbConInstId: ContainerInstanceId) => for { - deferredPauseAndRecurse <- Deferred[IO, (SnsTopicArn, LifecycleHookNotification, LifecycleState)] + deferredPauseAndRecurse <- Deferred[IO, (TopicARN, LifecycleHookNotification, LifecycleState)] expectedContainerInstance = ContainerInstance(arbConInstId, arbLifecycleHookNotification.EC2InstanceId, 1.asInstanceOf[TaskCount], ContainerInstanceStatus.Draining) ecsAlg = new TestEcsAlg { - override def findEc2Instance(ec2InstanceId: Ec2InstanceId) = + override def findEc2Instance(ec2InstanceId: InstanceId) = IO.pure(Option((arbClusterArn, expectedContainerInstance))) } autoScalingAlg = new TestAutoScalingAlg { - override def pauseAndRecurse(topic: SnsTopicArn, + override def pauseAndRecurse(topic: TopicARN, lifecycleHookNotification: LifecycleHookNotification, onlyIfInState: LifecycleState, ): IO[Unit] = @@ -86,7 +87,7 @@ class TerminationEventBridgeSpec } test("TerminationEventBridge should continue autoscaling if instance has no running tasks") { - forAllF { (arbSnsTopicArn: SnsTopicArn, + forAllF { (arbSnsTopicArn: TopicARN, arbLifecycleHookNotification: LifecycleHookNotification, arbClusterArn: ClusterArn, arbConInstId: ContainerInstanceId) => @@ -95,7 +96,7 @@ class TerminationEventBridgeSpec expectedContainerInstance = ContainerInstance(arbConInstId, arbLifecycleHookNotification.EC2InstanceId, 0.asInstanceOf[TaskCount], ContainerInstanceStatus.Draining) ecsAlg = new TestEcsAlg { - override def findEc2Instance(ec2InstanceId: Ec2InstanceId) = + override def findEc2Instance(ec2InstanceId: InstanceId) = IO.pure(Option((arbClusterArn, expectedContainerInstance))) } diff --git a/registrator-health-check/src/main/scala/com/dwolla/autoscaling/ecs/registrator/ScaleOutPendingEventBridge.scala b/registrator-health-check/src/main/scala/com/dwolla/autoscaling/ecs/registrator/ScaleOutPendingEventBridge.scala index 70c4fa0..dd73cba 100644 --- a/registrator-health-check/src/main/scala/com/dwolla/autoscaling/ecs/registrator/ScaleOutPendingEventBridge.scala +++ b/registrator-health-check/src/main/scala/com/dwolla/autoscaling/ecs/registrator/ScaleOutPendingEventBridge.scala @@ -2,15 +2,15 @@ package com.dwolla.autoscaling.ecs.registrator import cats.* import cats.syntax.all.* +import com.amazonaws.cloudformation.LogicalResourceId +import com.amazonaws.sns.TopicARN import com.dwolla.aws.* import com.dwolla.aws.autoscaling.* import com.dwolla.aws.autoscaling.AdvanceLifecycleHook.* import com.dwolla.aws.autoscaling.LifecycleState.* import com.dwolla.aws.cloudformation.* import com.dwolla.aws.ec2.Ec2Alg -import com.dwolla.aws.ecs.EcsAlg import com.dwolla.aws.ecs.* -import com.dwolla.aws.sns.SnsTopicArn import mouse.all.* class ScaleOutPendingEventBridge[F[_] : Monad, G[_]](ECS: EcsAlg[F, G], @@ -18,7 +18,7 @@ class ScaleOutPendingEventBridge[F[_] : Monad, G[_]](ECS: EcsAlg[F, G], EC2: Ec2Alg[F], Cfn: CloudFormationAlg[F], ) { - def apply(topic: SnsTopicArn, lifecycleHook: LifecycleHookNotification): F[Unit] = + def apply(topic: TopicARN, lifecycleHook: LifecycleHookNotification): F[Unit] = ECS.findEc2Instance(lifecycleHook.EC2InstanceId) .liftOptionT .mproduct { case (cluster, ContainerInstance(containerInstanceId, ec2InstanceId, _, _)) => @@ -49,7 +49,7 @@ object ScaleOutPendingEventBridge { autoScalingAlg: AutoScalingAlg[F], ec2Alg: Ec2Alg[F], cloudFormationAlg: CloudFormationAlg[F], - ): (SnsTopicArn, LifecycleHookNotification) => F[Unit] = + ): (TopicARN, LifecycleHookNotification) => F[Unit] = new ScaleOutPendingEventBridge(ecsAlg, autoScalingAlg, ec2Alg, cloudFormationAlg).apply } diff --git a/registrator-health-check/src/main/scala/com/dwolla/autoscaling/ecs/registrator/ScaleOutPendingEventHandler.scala b/registrator-health-check/src/main/scala/com/dwolla/autoscaling/ecs/registrator/ScaleOutPendingEventHandler.scala index f60865a..64956a0 100644 --- a/registrator-health-check/src/main/scala/com/dwolla/autoscaling/ecs/registrator/ScaleOutPendingEventHandler.scala +++ b/registrator-health-check/src/main/scala/com/dwolla/autoscaling/ecs/registrator/ScaleOutPendingEventHandler.scala @@ -2,10 +2,14 @@ package com.dwolla.autoscaling.ecs.registrator import cats.* import cats.effect.* +import com.amazonaws.autoscaling.AutoScaling +import com.amazonaws.cloudformation.CloudFormation +import com.amazonaws.ec2.EC2 import com.amazonaws.ecs.ECS +import com.amazonaws.sns.SNS import com.dwolla.aws.autoscaling.{AutoScalingAlg, LifecycleHookHandler} -import com.dwolla.aws.ec2.Ec2Alg import com.dwolla.aws.cloudformation.CloudFormationAlg +import com.dwolla.aws.ec2.Ec2Alg import com.dwolla.aws.ecs.EcsAlg import com.dwolla.aws.sns.SnsAlg import feral.lambda.events.SnsEvent @@ -15,10 +19,6 @@ import org.typelevel.log4cats.LoggerFactory import org.typelevel.log4cats.slf4j.Slf4jFactory import smithy4s.aws.* import smithy4s.aws.kernel.AwsRegion -import software.amazon.awssdk.services.autoscaling.AutoScalingAsyncClient -import software.amazon.awssdk.services.cloudformation.CloudFormationAsyncClient -import software.amazon.awssdk.services.ec2.Ec2AsyncClient -import software.amazon.awssdk.services.sns.SnsAsyncClient class ScaleOutPendingEventHandler extends IOLambda[SnsEvent, INothing] { override def handler: Resource[IO, LambdaEnv[IO, SnsEvent] => IO[Option[INothing]]] = @@ -27,10 +27,10 @@ class ScaleOutPendingEventHandler extends IOLambda[SnsEvent, INothing] { given LoggerFactory[IO] = Slf4jFactory.create[IO] awsEnv <- AwsEnvironment.default(client, AwsRegion.US_WEST_2) ecs <- AwsClient(ECS, awsEnv).map(EcsAlg(_)) - autoscalingClient <- Resource.fromAutoCloseable(IO(AutoScalingAsyncClient.builder().build())) - sns <- Resource.fromAutoCloseable(IO(SnsAsyncClient.builder().build())).map(SnsAlg[IO](_)) - ec2Client <- Resource.fromAutoCloseable(IO(Ec2AsyncClient.builder().build())).map(Ec2Alg[IO](_)) - cloudformationClient <- Resource.fromAutoCloseable(IO(CloudFormationAsyncClient.builder().build())).map(CloudFormationAlg[IO](_)) + autoscalingClient <- AwsClient(AutoScaling, awsEnv) + sns <- AwsClient(SNS, awsEnv).map(SnsAlg[IO](_)) + ec2Client <- AwsClient(EC2, awsEnv).map(Ec2Alg[IO](_)) + cloudformationClient <- AwsClient(CloudFormation, awsEnv).map(CloudFormationAlg[IO](_)) autoscaling = AutoScalingAlg[IO](autoscalingClient, sns) bridgeFunction = ScaleOutPendingEventBridge(ecs, autoscaling, ec2Client, cloudformationClient) } yield LifecycleHookHandler[IO](bridgeFunction) diff --git a/registrator-health-check/src/test/scala/com/dwolla/autoscaling/ecs/registrator/ScaleOutPendingEventBridgeSpec.scala b/registrator-health-check/src/test/scala/com/dwolla/autoscaling/ecs/registrator/ScaleOutPendingEventBridgeSpec.scala index 48bd040..1a9a418 100644 --- a/registrator-health-check/src/test/scala/com/dwolla/autoscaling/ecs/registrator/ScaleOutPendingEventBridgeSpec.scala +++ b/registrator-health-check/src/test/scala/com/dwolla/autoscaling/ecs/registrator/ScaleOutPendingEventBridgeSpec.scala @@ -3,13 +3,16 @@ package com.dwolla.autoscaling.ecs.registrator import cats.* import cats.effect.* import cats.syntax.all.* +import com.amazonaws.cloudformation.* +import com.amazonaws.ec2.InstanceId +import com.amazonaws.sns.TopicARN import com.dwolla.aws.autoscaling.AdvanceLifecycleHook.* import com.dwolla.aws.autoscaling.LifecycleState.PendingWait import com.dwolla.aws.autoscaling.{*, given} import com.dwolla.aws.cloudformation.{*, given} import com.dwolla.aws.ec2.* import com.dwolla.aws.ecs.{*, given} -import com.dwolla.aws.sns.{SnsTopicArn, given} +import com.dwolla.aws.sns.given import com.dwolla.aws.{*, given} import munit.{CatsEffectSuite, ScalaCheckEffectSuite} import org.scalacheck.effect.PropF.forAllF @@ -19,7 +22,7 @@ class ScaleOutPendingEventBridgeSpec with ScalaCheckEffectSuite { test("ScaleOutPendingEventBridge continues autoscaling when Registrator is running on instance") { - forAllF { (arbTopic: SnsTopicArn, + forAllF { (arbTopic: TopicARN, arbLifecycleHook: LifecycleHookNotification, arbCluster: ClusterArn, arbContainerInstance: ContainerInstance, @@ -29,9 +32,9 @@ class ScaleOutPendingEventBridgeSpec arbRegistratorStatus: Boolean, ) => for { - deferredResult <- Deferred[IO, (AdvanceLifecycleHook, Option[SnsTopicArn], LifecycleHookNotification, Option[LifecycleState])] + deferredResult <- Deferred[IO, (AdvanceLifecycleHook, Option[TopicARN], LifecycleHookNotification, Option[LifecycleState])] ecs = new TestEcsAlg { - override def findEc2Instance(ec2InstanceId: Ec2InstanceId): IO[Option[(ClusterArn, ContainerInstance)]] = + override def findEc2Instance(ec2InstanceId: InstanceId): IO[Option[(ClusterArn, ContainerInstance)]] = Option.when(ec2InstanceId == arbLifecycleHook.EC2InstanceId) { (arbCluster, arbContainerInstance) }.pure[IO] @@ -68,7 +71,7 @@ class ScaleOutPendingEventBridgeSpec } test("ScaleOutPendingEventBridge pauses and recurses if EC2 instance isn't found in ECS cluster") { - forAllF { (arbTopic: SnsTopicArn, + forAllF { (arbTopic: TopicARN, arbLifecycleHook: LifecycleHookNotification, arbCluster: ClusterArn, arbContainerInstance: ContainerInstance, @@ -77,9 +80,9 @@ class ScaleOutPendingEventBridgeSpec arbTags: List[Tag], ) => for { - deferredResult <- Deferred[IO, (AdvanceLifecycleHook, Option[SnsTopicArn], LifecycleHookNotification, Option[LifecycleState])] + deferredResult <- Deferred[IO, (AdvanceLifecycleHook, Option[TopicARN], LifecycleHookNotification, Option[LifecycleState])] ecs = new TestEcsAlg { - override def findEc2Instance(ec2InstanceId: Ec2InstanceId): IO[Option[(ClusterArn, ContainerInstance)]] = + override def findEc2Instance(ec2InstanceId: InstanceId): IO[Option[(ClusterArn, ContainerInstance)]] = none[(ClusterArn, ContainerInstance)].pure[IO] } autoscaling = new FakeAutoScalingAlgThatCapturesMethodParameters(deferredResult) @@ -103,7 +106,7 @@ class FakeEc2AlgThatReturnsArbitraryTagsWithCloudFormationStackIdTag(arbLifecycl arbStackArn: StackArn, arbTags: List[Tag], ) extends TestEc2Alg { - override def getTagsForInstance(id: Ec2InstanceId): IO[List[Tag]] = + override def getTagsForInstance(id: InstanceId): IO[List[Tag]] = IO.pure { if (id == arbLifecycleHook.EC2InstanceId) arbTags ++ List( Tag(TagName("aws:cloudformation:stack-id"), TagValue(arbStackArn.value)), @@ -112,8 +115,8 @@ class FakeEc2AlgThatReturnsArbitraryTagsWithCloudFormationStackIdTag(arbLifecycl } } -class FakeAutoScalingAlgThatCapturesMethodParameters(deferredResult: Deferred[IO, (AdvanceLifecycleHook, Option[SnsTopicArn], LifecycleHookNotification, Option[LifecycleState])]) extends TestAutoScalingAlg { - override def pauseAndRecurse(topic: SnsTopicArn, +class FakeAutoScalingAlgThatCapturesMethodParameters(deferredResult: Deferred[IO, (AdvanceLifecycleHook, Option[TopicARN], LifecycleHookNotification, Option[LifecycleState])]) extends TestAutoScalingAlg { + override def pauseAndRecurse(topic: TopicARN, lifecycleHookNotification: LifecycleHookNotification, onlyIfInState: LifecycleState, ): IO[Unit] = diff --git a/smithy4s-preprocessors/src/main/resources/META-INF/services/software.amazon.smithy.build.ProjectionTransformer b/smithy4s-preprocessors/src/main/resources/META-INF/services/software.amazon.smithy.build.ProjectionTransformer new file mode 100644 index 0000000..a229039 --- /dev/null +++ b/smithy4s-preprocessors/src/main/resources/META-INF/services/software.amazon.smithy.build.ProjectionTransformer @@ -0,0 +1,5 @@ +preprocessors.AutoscalingPreprocessor +preprocessors.CloudformationPreprocessor +preprocessors.Ec2Preprocessor +preprocessors.EcsPreprocessor +preprocessors.SnsPreprocessor diff --git a/smithy4s-preprocessors/src/main/scala/preprocessors/AutoscalingPreprocessor.scala b/smithy4s-preprocessors/src/main/scala/preprocessors/AutoscalingPreprocessor.scala new file mode 100644 index 0000000..1308e69 --- /dev/null +++ b/smithy4s-preprocessors/src/main/scala/preprocessors/AutoscalingPreprocessor.scala @@ -0,0 +1,14 @@ +package preprocessors + +final class AutoscalingPreprocessor extends OperationFilteringPreprocessor { + override lazy val shapesToKeep: Set[String] = Set( + "CompleteLifecycleAction", + "DescribeAutoScalingInstances", + "LifecycleTransition", + "LifecycleState", + ) + + override lazy val namespace: Set[String] = Set("com.amazonaws.autoscaling") + + override def getName: String = "AutoscalingPreprocessor" +} diff --git a/smithy4s-preprocessors/src/main/scala/preprocessors/CloudformationPreprocessor.scala b/smithy4s-preprocessors/src/main/scala/preprocessors/CloudformationPreprocessor.scala new file mode 100644 index 0000000..af9a8ef --- /dev/null +++ b/smithy4s-preprocessors/src/main/scala/preprocessors/CloudformationPreprocessor.scala @@ -0,0 +1,11 @@ +package preprocessors + +final class CloudformationPreprocessor extends OperationFilteringPreprocessor { + override lazy val shapesToKeep: Set[String] = Set( + "DescribeStackResources", + ) + + override lazy val namespace: Set[String] = Set("com.amazonaws.cloudformation") + + override def getName: String = "CloudformationPreprocessor" +} diff --git a/smithy4s-preprocessors/src/main/scala/preprocessors/Ec2Preprocessor.scala b/smithy4s-preprocessors/src/main/scala/preprocessors/Ec2Preprocessor.scala new file mode 100644 index 0000000..7fa92c5 --- /dev/null +++ b/smithy4s-preprocessors/src/main/scala/preprocessors/Ec2Preprocessor.scala @@ -0,0 +1,11 @@ +package preprocessors + +final class Ec2Preprocessor extends OperationFilteringPreprocessor { + override lazy val shapesToKeep: Set[String] = Set( + "DescribeInstances", + ) + + override lazy val namespace: Set[String] = Set("com.amazonaws.ec2") + + override def getName: String = "Ec2Preprocessor" +} diff --git a/smithy4s-preprocessors/src/main/scala/preprocessors/EcsPreprocessor.scala b/smithy4s-preprocessors/src/main/scala/preprocessors/EcsPreprocessor.scala new file mode 100644 index 0000000..fa26a8f --- /dev/null +++ b/smithy4s-preprocessors/src/main/scala/preprocessors/EcsPreprocessor.scala @@ -0,0 +1,16 @@ +package preprocessors + +final class EcsPreprocessor extends OperationFilteringPreprocessor { + override lazy val shapesToKeep: Set[String] = Set( + "DescribeContainerInstances", + "DescribeTasks", + "ListClusters", + "ListContainerInstances", + "ListTasks", + "UpdateContainerInstancesState", + ) + + override lazy val namespace: Set[String] = Set("com.amazonaws.ecs") + + override def getName: String = "EcsPreprocessor" +} diff --git a/smithy4s-preprocessors/src/main/scala/preprocessors/OperationFilteringPreprocessor.scala b/smithy4s-preprocessors/src/main/scala/preprocessors/OperationFilteringPreprocessor.scala new file mode 100644 index 0000000..b9cbc2c --- /dev/null +++ b/smithy4s-preprocessors/src/main/scala/preprocessors/OperationFilteringPreprocessor.scala @@ -0,0 +1,41 @@ +package preprocessors + +import software.amazon.smithy.build._ +import software.amazon.smithy.model._ +import software.amazon.smithy.model.neighbor.Walker +import software.amazon.smithy.model.shapes._ + +import scala.collection.JavaConverters._ + +abstract class OperationFilteringPreprocessor extends ProjectionTransformer { + def shapesToKeep: Set[String] + def namespace: Set[String] + + override def transform(ctx: TransformContext): Model = { + val incomingModel = ctx.getModel + + val allShapesInNamespace = incomingModel.shapes() + .toList + .asScala + .toSet + .filter(shape => namespace.contains(shape.getId.getNamespace)) + + val shapesNotExplicitlyKept: Set[Shape] = allShapesInNamespace + .filterNot(shape => shapesToKeep.contains(shape.getId.getName)) + + val remainingShapes = allShapesInNamespace -- shapesNotExplicitlyKept + val shapeWalker = new Walker(incomingModel) + + val referencedShapeIds: Set[ShapeId] = remainingShapes.foldLeft(Set.empty[ShapeId]) { + _ ++ shapeWalker.walkShapeIds(_).asScala + } + + if (referencedShapeIds.nonEmpty) { + ctx.getTransformer.filterShapes(incomingModel, { (t: Shape) => + !namespace.contains(t.getId.getNamespace) || referencedShapeIds.contains(t.toShapeId) || t.isServiceShape + }) + } else { + incomingModel + } + } +} diff --git a/smithy4s-preprocessors/src/main/scala/preprocessors/SnsPreprocessor.scala b/smithy4s-preprocessors/src/main/scala/preprocessors/SnsPreprocessor.scala new file mode 100644 index 0000000..ee24d2f --- /dev/null +++ b/smithy4s-preprocessors/src/main/scala/preprocessors/SnsPreprocessor.scala @@ -0,0 +1,11 @@ +package preprocessors + +final class SnsPreprocessor extends OperationFilteringPreprocessor { + override lazy val shapesToKeep: Set[String] = Set( + "Publish" + ) + + override lazy val namespace: Set[String] = Set("com.amazonaws.sns") + + override def getName: String = "SnsPreprocessor" +}