app/controllers/RecipeController.scala (360 lines of code) (raw):

package controllers import com.gu.googleauth.AuthAction import data._ import models._ import org.quartz.CronExpression import play.api.data.{Form, Mapping} import play.api.data.Forms._ import play.api.i18n.I18nSupport import play.api.mvc._ import prism.RecipeUsage import schedule.BakeScheduler import services.{Loggable, PrismData} import scala.util.Try class RecipeController( val authAction: AuthAction[AnyContent], bakeScheduler: BakeScheduler, prismAgents: PrismData, components: ControllerComponents, debugAvailable: Boolean )(implicit dynamo: Dynamo) extends AbstractController(components) with I18nSupport with Loggable { import RecipeController._ def listRecipes = authAction { val recipes: Iterable[Recipe] = Recipes.list() val usages: Map[Recipe, RecipeUsage] = RecipeUsage.getUsagesMap(recipes)(prismAgents, dynamo) val (usedRecipes, unusedRecipes) = recipes.partition(r => RecipeUsage.hasUsage(r, usages)) Ok(views.html.recipes(usedRecipes, unusedRecipes, usages)) } def showRecipe(id: RecipeId) = authAction { implicit request => Recipes.findById(id).fold[Result](NotFound) { recipe => val bakes = Bakes.list(recipe.id) val recentBakes = bakes.take(20) val recentCopies = prismAgents.copiedImages(recentBakes.flatMap(_.amiId).toSet) Ok( views.html.showRecipe( recipe, recentBakes, recentCopies, prismAgents.accounts, RecipeUsage(bakes)(prismAgents), Roles.list, debugAvailable, Forms.cloneRecipe ) ) } } def editRecipe(id: RecipeId) = authAction { implicit request => Recipes.findById(id).fold[Result](NotFound) { recipe => val form = Forms.editRecipe.fill( ( recipe.description, recipe.baseImage.id, recipe.diskSize, recipe.bakeSchedule, recipe.encryptFor ) ) Ok( views.html .editRecipe(recipe, form, BaseImages.list().toSeq, Roles.listIds) ) } } def updateRecipe(id: RecipeId) = authAction(parse.formUrlEncoded) { implicit request => Recipes.findById(id).fold[Result](NotFound) { recipe => Forms.editRecipe .bindFromRequest() .fold( { formWithErrors => BadRequest( views.html.editRecipe( recipe, formWithErrors, BaseImages.list().toSeq, Roles.listIds ) ) }, { case ( description, baseImageId, diskSize, bakeSchedule, encryptFor ) => BaseImages.findById(baseImageId) match { case Some(baseImage) => log.info( s"Updating recipe ${id} - requested by ${request.user.email}" ) val customisedRoles = controllers.ControllerHelpers .parseEnabledRoles(request.body) customisedRoles.fold( error => BadRequest(s"Problem parsing roles: $error"), roles => { val updatedRecipe = Recipes.update( recipe, description, baseImage, diskSize, roles, modifiedBy = request.user.fullName, bakeSchedule, encryptFor ) updatedRecipe.fold( e => InternalServerError(e.toString), { r => bakeScheduler.reschedule(r) Redirect(routes.RecipeController.showRecipe(id)) .flashing("info" -> "Successfully updated recipe") } ) } ) case None => val formWithError = Forms.editRecipe .fill( ( description, baseImageId, diskSize, bakeSchedule, encryptFor ) ) .withError("baseImageId", "Unknown base image") BadRequest( views.html.editRecipe( recipe, formWithError, BaseImages.list().toSeq, Roles.listIds ) ) } } ) } } def newRecipe = authAction { implicit request => Ok( views.html .newRecipe(Forms.createRecipe, BaseImages.list().toSeq, Roles.listIds) ) } def createRecipe = authAction(parse.formUrlEncoded) { implicit request => Forms.createRecipe .bindFromRequest() .fold( { formWithErrors => BadRequest( views.html .newRecipe(formWithErrors, BaseImages.list().toSeq, Roles.listIds) ) }, { case ( id, description, baseImageId, diskSize, bakeSchedule, encryptedCopies ) => log.info( s"Creating recipe ${id} - requested by ${request.user.email}" ) Recipes.findById(id) match { case Some(existingRecipe) => val formWithError = Forms.createRecipe .fill( ( id, description, baseImageId, diskSize, bakeSchedule, encryptedCopies ) ) .withError("id", "This recipe ID is already in use") Conflict(views.html.newBaseImage(formWithError, Roles.listIds)) case None => BaseImages.findById(baseImageId) match { case Some(baseImage) => val customisedRoles = controllers.ControllerHelpers .parseEnabledRoles(request.body) customisedRoles.fold( error => BadRequest(s"Problem parsing roles: $error"), roles => { val recipe = Recipes.create( id, description, baseImage, diskSize, roles, createdBy = request.user.fullName, bakeSchedule, encryptedCopies ) // TODO: FIX THIS bakeScheduler.reschedule(recipe) Redirect(routes.RecipeController.showRecipe(id)) .flashing("info" -> "Successfully created recipe") } ) case None => val formWithError = Forms.createRecipe .fill( ( id, description, baseImageId, diskSize, bakeSchedule, encryptedCopies ) ) .withError("baseImageId", "Unknown base image") BadRequest( views.html.newRecipe( formWithError, BaseImages.list().toSeq, Roles.listIds ) ) } } } ) } def showUsages(id: RecipeId) = authAction { implicit request => Recipes.findById(id).fold[Result](NotFound) { recipe => val bakes = Bakes.list(recipe.id) val recipeUsage: RecipeUsage = RecipeUsage(bakes)(prismAgents) Ok( views.html.showUsage( recipe, recipeUsage.bakeUsage, prismAgents.accounts, prismAgents.baseUrl ) ) } } def cloneRecipe(id: RecipeId) = authAction { implicit request => Forms.cloneRecipe .bindFromRequest() .fold( { form => Redirect(routes.RecipeController.showRecipe(id)).flashing( "info" -> s"Failed to clone recipe: ${form.errors.head.message}" ) }, { newId => Recipes .findById(newId) .fold[Result] { Recipes.findById(id).fold[Result](NotFound) { recipe => Recipes.create( id = newId, description = recipe.description, baseImage = recipe.baseImage, diskSize = recipe.diskSize, roles = recipe.roles, createdBy = request.user.fullName, bakeSchedule = recipe.bakeSchedule, encryptedCopies = recipe.encryptFor ) Redirect(routes.RecipeController.showRecipe(newId)) .flashing("info" -> "Successfully cloned recipe") } }(_ => Conflict(s"$newId already exists")) } ) } def deleteConfirm(id: RecipeId) = authAction { implicit request => Recipes.findById(id).fold[Result](NotFound) { recipe => val bakes = Bakes.list(recipe.id).toSeq val recipeUsage: RecipeUsage = RecipeUsage(bakes)(prismAgents) Ok(views.html.confirmRecipeDelete(recipe, bakes, recipeUsage.bakeUsage)) } } def deleteRecipe(id: RecipeId) = authAction { implicit request => Recipes.findById(id).fold[Result](NotFound) { recipe => val bakes = Bakes.list(recipe.id) val recipeUsage: RecipeUsage = RecipeUsage(bakes)(prismAgents) if (recipeUsage.bakeUsage.nonEmpty) { Conflict( s"Can't delete recipe $id as it is still used by ${recipeUsage.bakeUsage.size} resources." ) } else { log.info( s"Deleting recipe ${id} - requested by ${request.user.email}" ) // stop any scheduled build bakeScheduler.reschedule(recipe.copy(bakeSchedule = None)) // delete the AMIgo data bakes.foreach { bake => Bakes.markToDelete(bake.bakeId) } Recipes.delete(recipe) // redirect back to the index page Redirect(routes.RecipeController.listRecipes()) } } } } object RecipeController { object Forms { private val baseImageIdMapping: Mapping[BaseImageId] = nonEmptyText(maxLength = 50) .transform[BaseImageId](BaseImageId.apply, _.value) private val validQuartzCronExpression = (text: String) => Try(new CronExpression(text)).isSuccess private val bakeScheduleMapping = optional( text(maxLength = 50) .verifying("Invalid Quartz cron expression", validQuartzCronExpression) .transform[BakeSchedule](BakeSchedule.apply, _.quartzCronExpression) ) private val accountNumbersMapping = text() .verifying(_.forall(c => c.isDigit || c.isWhitespace || c == ',')) .transform[List[AccountNumber]]( _.split(',').toList .map(_.trim) .filter(_.nonEmpty) .map(AccountNumber.apply), _.map(_.accountNumber).mkString(",") ) val editRecipe = Form( tuple( "description" -> optional(text(maxLength = 10000)), "baseImageId" -> baseImageIdMapping, "diskSize" -> optional(number), "bakeSchedule" -> bakeScheduleMapping, "encryptFor" -> accountNumbersMapping ) ) val createRecipe = Form( tuple( "id" -> text(minLength = 3, maxLength = 50) .transform[RecipeId](RecipeId.apply, _.value), "description" -> optional(text(maxLength = 10000)), "baseImageId" -> baseImageIdMapping, "diskSize" -> optional(number), "bakeSchedule" -> bakeScheduleMapping, "encryptFor" -> accountNumbersMapping ) ) val cloneRecipe = Form( "newId" -> text(minLength = 3, maxLength = 50) .transform[RecipeId](RecipeId.apply, _.value) ) } }