Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

use official ZIO-lambda library in product-move-api #2115

Merged
merged 7 commits into from
Dec 11, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,7 @@ lazy val `product-move-api` = lambdaProject(
"com.softwaremill.sttp.tapir" %% "tapir-core" % tapirVersion,
"com.softwaremill.sttp.tapir" %% "tapir-json-zio" % tapirVersion,
"com.softwaremill.sttp.tapir" %% "tapir-aws-lambda" % tapirVersion,
"com.softwaremill.sttp.tapir" %% "tapir-aws-lambda-zio" % tapirVersion,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the new library added in softwaremill/tapir#2975

"com.softwaremill.sttp.tapir" %% "tapir-swagger-ui-bundle" % tapirVersion,
awsSecretsManager,
upickle,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ object AwsCredentialsLive {

private val ProfileName = "membership"

val layer: Layer[String, AwsCredentialsProvider] =
ZLayer.scoped(ZIO.fromAutoCloseable(ZIO.attempt(impl))).mapError(_.toString)
val layer: Layer[Throwable, AwsCredentialsProvider] =
ZLayer.scoped(ZIO.fromAutoCloseable(ZIO.attempt(impl)))
Comment on lines +16 to +17
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the official zio interpreter expects Throwable as the error, rather than Any, so I had to make everything comply. That is the majority of the changes.


private def impl: AwsCredentialsProviderChain =
AwsCredentialsProviderChain
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ object DynamoLive {
case Stage.PROD => PROD
}

val layer: ZLayer[Stage, ErrorResponse, Dynamo] =
val layer: RLayer[Stage, Dynamo] =
ZLayer.scoped {
ZIO.service[Stage].map { stage =>
val dynamoService = SupporterDataDynamoService(getStage(stage))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ object GuStageLive {
enum Stage:
case PROD, CODE

val layer: Layer[String, Stage] =
val layer: Layer[Throwable, Stage] =
ZLayer {
for {
stageString <- System.envOrElse("Stage", "CODE")
stage <- ZIO.attempt(Stage.valueOf(stageString))
} yield stage
}.mapError(_.toString)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import com.gu.productmove.endpoint.cancel.SubscriptionCancelEndpoint
import com.gu.productmove.endpoint.move.{ProductMoveEndpoint, ProductMoveEndpointTypes}
import com.gu.productmove.endpoint.updateamount.UpdateSupporterPlusAmountEndpoint
import com.gu.productmove.framework.ZIOApiGatewayRequestHandler
import com.gu.productmove.framework.ZIOApiGatewayRequestHandler.TIO
import zio.Task
import com.gu.productmove.zuora.rest.{ZuoraClient, ZuoraClientLive, ZuoraGet, ZuoraGetLive}
import com.gu.productmove.zuora.{GetSubscription, GetSubscriptionLive}
import software.amazon.awssdk.auth.credentials.*
Expand Down Expand Up @@ -65,8 +65,18 @@ object Handler extends ZIOApiGatewayRequestHandler {
)
}

@main
// this will output the HTML to the console
def testDocsHtml(): Unit = {
Handler.runTest(
"GET",
"/docs/",
None,
)
}
Copy link
Member Author

@johnduffell johnduffell Nov 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new manual test so I could reproduce the issue locally.


// this represents all the routes for the server
override val server: List[ServerEndpoint[Any, TIO]] = List(
override val server: List[ServerEndpoint[Any, Task]] = List(
AvailableProductMovesEndpoint.server,
ProductMoveEndpoint.server,
UpdateSupporterPlusAmountEndpoint.server,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,11 @@ object SQS {
}

object SQSLive {
val layer: ZLayer[AwsCredentialsProvider with Stage, ErrorResponse, SQS] =
val layer: RLayer[AwsCredentialsProvider & Stage, SQS] =
ZLayer.scoped(for {
stage <- ZIO.service[Stage]
sqsClient <- initializeSQSClient().mapError(ex =>
InternalServerError(s"Failed to initialize SQS Client with error: $ex"),
new Throwable(s"Failed to initialize SQS Client with error", ex),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we were returning InternalServerError but I don't know if it was actually getting used directly, or just converted into an internal server error later anyway

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you check? We obvs want to maintain the existing behaviour

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes that's a really good point I will check that for sure

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, existing code we end up with Any as the failure type in the ZIO, and that ends up here in the code


So no matter whether we called it InternalServerError or BadRequest etc, it it would still be an internal server error.

If/where we map the error to a ZIO.success then we could use it as a response.
I think the new (official) way of using Throwable rather than my way of using Any as the error type is better as it's pretty clear that a Throwable will be a 500 error, and if we try to use a BadRequest or something, expecting it will really be a bad request, it won't compile unless we recover it into a success.

)
emailQueueName = EmailQueueName.value
emailQueueUrlResponse <- getQueue(emailQueueName, sqsClient)
Expand Down Expand Up @@ -154,14 +154,14 @@ object SQSLive {
private def getQueue(
queueName: String,
sqsAsyncClient: SqsAsyncClient,
): ZIO[Any, ErrorResponse, GetQueueUrlResponse] =
): Task[GetQueueUrlResponse] =
val queueUrl = GetQueueUrlRequest.builder.queueName(queueName).build()

ZIO
.fromCompletableFuture(
sqsAsyncClient.getQueueUrl(queueUrl),
)
.mapError { ex => InternalServerError(s"Failed to get sqs queue name: $queueName, error: ${ex.getMessage}") }
.mapError { ex => new Throwable(s"Failed to get sqs queue name: $queueName", ex) }

private def impl(creds: AwsCredentialsProvider): SqsAsyncClient =
SqsAsyncClient.builder
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
package com.gu.productmove

import software.amazon.awssdk.services.secretsmanager.*
import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueRequest
import upickle.default.*

import scala.util.{Try, Success, Failure}
import zio.{ZIO, ULayer, ZLayer}
import com.gu.productmove.endpoint.move.ProductMoveEndpointTypes.{SecretsError, ErrorResponse}
import software.amazon.awssdk.auth.credentials.{
AwsCredentialsProviderChain,
ProfileCredentialsProvider,
EnvironmentVariableCredentialsProvider,
ProfileCredentialsProvider,
}
import software.amazon.awssdk.services.secretsmanager.*
import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueRequest
import upickle.default.*
import zio.{RIO, Task, ULayer, ZIO, ZLayer}

import scala.util.{Failure, Success, Try}

/*
In Secrets Store we have the following JSON objects:
Expand Down Expand Up @@ -52,75 +51,60 @@ case class SalesforceSSLSecrets(
)

trait Secrets {
def getInvoicingAPISecrets: ZIO[Any, ErrorResponse, InvoicingAPISecrets]
def getZuoraApiUserSecrets: ZIO[Any, ErrorResponse, ZuoraApiUserSecrets]
def getSalesforceSSLSecrets: ZIO[Any, ErrorResponse, SalesforceSSLSecrets]
def getInvoicingAPISecrets: Task[InvoicingAPISecrets]
def getZuoraApiUserSecrets: Task[ZuoraApiUserSecrets]
def getSalesforceSSLSecrets: Task[SalesforceSSLSecrets]
}

object Secrets {
def getInvoicingAPISecrets: ZIO[Secrets, ErrorResponse, InvoicingAPISecrets] =
def getInvoicingAPISecrets: RIO[Secrets, InvoicingAPISecrets] =
ZIO.environmentWithZIO[Secrets](_.get.getInvoicingAPISecrets)
def getZuoraApiUserSecrets: ZIO[Secrets, ErrorResponse, ZuoraApiUserSecrets] =
def getZuoraApiUserSecrets: RIO[Secrets, ZuoraApiUserSecrets] =
ZIO.environmentWithZIO[Secrets](_.get.getZuoraApiUserSecrets)

def getSalesforceSSLSecrets: ZIO[Secrets, ErrorResponse, SalesforceSSLSecrets] =
def getSalesforceSSLSecrets: RIO[Secrets, SalesforceSSLSecrets] =
ZIO.environmentWithZIO[Secrets](_.get.getSalesforceSSLSecrets)
}
object SecretsLive extends Secrets {

implicit val reader1: Reader[InvoicingAPISecrets] = macroRW
implicit val reader2: Reader[ZuoraApiUserSecrets] = macroRW
implicit val reader3: Reader[SalesforceSSLSecrets] = macroRW

private val ProfileName = "membership"

private lazy val secretsClient = SecretsManagerClient.builder().credentialsProvider(credentialsProvider).build()
val credentialsProvider = AwsCredentialsProviderChain
.builder()
.credentialsProviders(
ProfileCredentialsProvider.create(ProfileName),
EnvironmentVariableCredentialsProvider.create(),
)
.build()
private lazy val secretsClient = SecretsManagerClient.builder().credentialsProvider(credentialsProvider).build()
val layer: ULayer[Secrets] = ZLayer.succeed(SecretsLive)
private val ProfileName = "membership"

def getInvoicingAPISecrets: Task[InvoicingAPISecrets] = {
for {
stg <- getStage
secretId: String = s"${stg}/InvoicingApi"
secretJsonString = getJSONString(secretId)
secrets <- parseInvoicingAPISecretsJSONString(secretJsonString)
} yield secrets
}

def getJSONString(secretId: String): String = {
secretsClient.getSecretValue(GetSecretValueRequest.builder().secretId(secretId).build()).secretString()
}

def getStage: ZIO[Any, ErrorResponse, String] =
def getStage: Task[String] =
ZIO.succeed(sys.env.getOrElse("Stage", "CODE"))

def parseInvoicingAPISecretsJSONString(str: String): ZIO[Any, ErrorResponse, InvoicingAPISecrets] = {
def parseInvoicingAPISecretsJSONString(str: String): Task[InvoicingAPISecrets] = {
Try(read[InvoicingAPISecrets](str)) match {
case Success(x) => ZIO.succeed(x)
case Failure(s) => ZIO.fail(SecretsError(s"Failure while parsing json string: ${s}"))
case Failure(s) => ZIO.fail(new Throwable(s"Failure while parsing json string: ${s}"))
}
}

def parseZuoraApiUserSecretsJSONString(str: String): ZIO[Any, ErrorResponse, ZuoraApiUserSecrets] = {
Try(read[ZuoraApiUserSecrets](str)) match {
case Success(x) => ZIO.succeed(x)
case Failure(s) => ZIO.fail(SecretsError(s"Failure while parsing json string: ${s}"))
}
}

def parseSalesforceSSLSecretsJSONString(str: String): ZIO[Any, ErrorResponse, SalesforceSSLSecrets] = {
Try(read[SalesforceSSLSecrets](str)) match {
case Success(x) => ZIO.succeed(x)
case Failure(s) => ZIO.fail(SecretsError(s"Failure while parsing json string: ${s}"))
}
}

def getInvoicingAPISecrets: ZIO[Any, ErrorResponse, InvoicingAPISecrets] = {
for {
stg <- getStage
secretId: String = s"${stg}/InvoicingApi"
secretJsonString = getJSONString(secretId)
secrets <- parseInvoicingAPISecretsJSONString(secretJsonString)
} yield secrets
}

def getZuoraApiUserSecrets: ZIO[Any, ErrorResponse, ZuoraApiUserSecrets] = {
def getZuoraApiUserSecrets: Task[ZuoraApiUserSecrets] = {
for {
stg <- getStage
secretId: String = s"${stg}/Zuora/User/ZuoraApiUser"
Expand All @@ -129,7 +113,14 @@ object SecretsLive extends Secrets {
} yield secrets
}

def getSalesforceSSLSecrets: ZIO[Any, ErrorResponse, SalesforceSSLSecrets] = {
def parseZuoraApiUserSecretsJSONString(str: String): Task[ZuoraApiUserSecrets] = {
Try(read[ZuoraApiUserSecrets](str)) match {
case Success(x) => ZIO.succeed(x)
case Failure(s) => ZIO.fail(new Throwable(s"Failure while parsing json string: ${s}"))
}
}

def getSalesforceSSLSecrets: Task[SalesforceSSLSecrets] = {
for {
stg <- getStage
secretId: String = s"${stg}/Salesforce/User/SupportServiceLambdas"
Expand All @@ -138,5 +129,10 @@ object SecretsLive extends Secrets {
} yield secrets
}

val layer: ZLayer[Any, ErrorResponse, Secrets] = ZLayer.succeed(SecretsLive)
def parseSalesforceSSLSecretsJSONString(str: String): Task[SalesforceSSLSecrets] = {
Try(read[SalesforceSSLSecrets](str)) match {
case Success(x) => ZIO.succeed(x)
case Failure(s) => ZIO.fail(new Throwable(s"Failure while parsing json string: ${s}"))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import com.gu.productmove.SecretsLive
import com.gu.productmove.GuStageLive.Stage
import com.gu.productmove.endpoint.available.AvailableProductMovesEndpointTypes.*
import com.gu.productmove.endpoint.available.Currency.GBP
import com.gu.productmove.framework.ZIOApiGatewayRequestHandler.TIO
import zio.Task
import com.gu.productmove.framework.{LambdaEndpoint, ZIOApiGatewayRequestHandler}
import com.gu.productmove.zuora.GetAccount.{GetAccountResponse, PaymentMethodResponse}
import com.gu.productmove.zuora.*
Expand Down Expand Up @@ -40,7 +40,7 @@ object AvailableProductMovesEndpoint {
Unit,
OutputBody,
Any,
ZIOApiGatewayRequestHandler.TIO,
Task,
] = {
val subscriptionNameCapture: EndpointInput.PathCapture[String] =
EndpointInput.PathCapture[String](
Expand Down Expand Up @@ -74,13 +74,13 @@ object AvailableProductMovesEndpoint {
.description("""Returns an array of eligible products that the given subscription could be moved to,
|which will be empty if there aren't any for the given subscription.
|""".stripMargin)
.serverLogic[TIO] { subscriptionName =>
.serverLogic[Task] { subscriptionName =>
run(subscriptionName).tapEither(result => ZIO.log("result tapped: " + result)).map(Right.apply)
}
}

// sub to test on: "A-S00334930"
private def run(subscriptionName: String): TIO[OutputBody] =
private def run(subscriptionName: String): Task[OutputBody] =
runWithEnvironment(SubscriptionName("A-S00334930")).provide(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this shouldn't have been hard coded, it should have used the one passed from the client!

AwsS3Live.layer,
AwsCredentialsLive.layer,
Expand Down
Loading