All Downloads are FREE. Search and download functionalities are using the official Maven repository.

pl.touk.nussknacker.ui.api.ManagementResources.scala Maven / Gradle / Ivy

There is a newer version: 1.17.0
Show newest version
package pl.touk.nussknacker.ui.api

import akka.http.scaladsl.marshalling.Marshal
import akka.http.scaladsl.model.{HttpResponse, MessageEntity, StatusCode, StatusCodes}
import akka.http.scaladsl.server._
import akka.http.scaladsl.unmarshalling.{FromEntityUnmarshaller, Unmarshaller}
import com.typesafe.scalalogging.LazyLogging
import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport
import io.circe.generic.extras.semiauto.deriveConfiguredEncoder
import io.circe.{Decoder, Encoder, Json, parser}
import io.dropwizard.metrics5.MetricRegistry
import pl.touk.nussknacker.engine.ModelData
import pl.touk.nussknacker.engine.api.component.NodesDeploymentData
import pl.touk.nussknacker.engine.api.deployment.DeploymentUpdateStrategy.StateRestoringStrategy
import pl.touk.nussknacker.engine.api.deployment._
import pl.touk.nussknacker.engine.api.graph.ScenarioGraph
import pl.touk.nussknacker.engine.testmode.TestProcess._
import pl.touk.nussknacker.restmodel.{CustomActionRequest, CustomActionResponse}
import pl.touk.nussknacker.ui.api.description.NodesApiEndpoints.Dtos.{
  TestFromParametersRequest,
  prepareTestFromParametersDecoder
}
import pl.touk.nussknacker.ui.api.ProcessesResources.ProcessUnmarshallingError
import pl.touk.nussknacker.ui.metrics.TimeMeasuring.measureTime
import pl.touk.nussknacker.ui.process.ProcessService
import pl.touk.nussknacker.ui.process.deployment.LoggedUserConversions.LoggedUserOps
import pl.touk.nussknacker.ui.process.deployment.{
  CancelScenarioCommand,
  CommonCommandData,
  CustomActionCommand,
  DeploymentManagerDispatcher,
  DeploymentService,
  RunDeploymentCommand
}
import pl.touk.nussknacker.ui.process.processingtype.ProcessingTypeDataProvider
import pl.touk.nussknacker.ui.process.repository.{ApiCallComment, UserComment}
import pl.touk.nussknacker.ui.process.test.{RawScenarioTestData, ResultsWithCounts, ScenarioTestService}
import pl.touk.nussknacker.ui.security.api.LoggedUser

import scala.concurrent.{ExecutionContext, Future}

object ManagementResources {

  import pl.touk.nussknacker.engine.api.CirceUtil._

  import io.circe.syntax._

  implicit val resultsWithCountsEncoder: Encoder[ResultsWithCounts] = deriveConfiguredEncoder

  private implicit val testResultsEncoder: Encoder[TestResults[Json]] = new Encoder[TestResults[Json]]() {

    implicit val nodeResult: Encoder[ResultContext[Json]]                              = deriveConfiguredEncoder
    implicit val expressionInvocationResult: Encoder[ExpressionInvocationResult[Json]] = deriveConfiguredEncoder
    implicit val externalInvocationResult: Encoder[ExternalInvocationResult[Json]]     = deriveConfiguredEncoder

    // TODO: do we want more information here?
    implicit val throwableEncoder: Encoder[Throwable] = Encoder[Option[String]].contramap(th => Option(th.getMessage))
    implicit val exceptionResultEncoder: Encoder[ExceptionResult[Json]] = deriveConfiguredEncoder

    override def apply(a: TestResults[Json]): Json = a match {
      case TestResults(nodeResults, invocationResults, externalInvocationResults, exceptions) =>
        Json.obj(
          "nodeResults"       -> nodeResults.map { case (node, list) => node -> list.sortBy(_.id) }.asJson,
          "invocationResults" -> invocationResults.map { case (node, list) => node -> list.sortBy(_.contextId) }.asJson,
          "externalInvocationResults" -> externalInvocationResults.map { case (node, list) =>
            node -> list.sortBy(_.contextId)
          }.asJson,
          "exceptions" -> exceptions.sortBy(_.context.id).asJson
        )
    }

  }

}

class ManagementResources(
    val processAuthorizer: AuthorizeProcess,
    protected val processService: ProcessService,
    deploymentService: DeploymentService,
    dispatcher: DeploymentManagerDispatcher,
    metricRegistry: MetricRegistry,
    scenarioTestServices: ProcessingTypeDataProvider[ScenarioTestService, _],
    typeToConfig: ProcessingTypeDataProvider[ModelData, _]
)(implicit val ec: ExecutionContext)
    extends Directives
    with LazyLogging
    with RouteWithUser
    with FailFastCirceSupport
    with AuthorizeProcessDirectives
    with ProcessDirectives {

  import ManagementResources._

  // TODO: in the future we could use https://github.com/akka/akka-http/pull/1828 when we can bump version to 10.1.x
  private implicit final val plainBytes: FromEntityUnmarshaller[Array[Byte]] = Unmarshaller.byteArrayUnmarshaller
  private implicit final val plainString: FromEntityUnmarshaller[String]     = Unmarshaller.stringUnmarshaller

  def securedRoute(implicit user: LoggedUser): Route = {
    pathPrefix("adminProcessManagement") {
      path("snapshot" / ProcessNameSegment) { processName =>
        (post & processId(processName) & parameters(Symbol("savepointDir").?)) { (processId, savepointDir) =>
          canDeploy(processId) {
            complete {
              convertSavepointResultToResponse(
                dispatcher
                  .deploymentManagerUnsafe(processId)(ec, user)
                  .flatMap(_.processCommand(DMMakeScenarioSavepointCommand(processId.name, savepointDir)))
              )
            }
          }
        }
      } ~
        path("stop" / ProcessNameSegment) { processName =>
          (post & processId(processName) & parameters(Symbol("savepointDir").?)) { (processId, savepointDir) =>
            canDeploy(processId) {
              complete {
                convertSavepointResultToResponse(
                  dispatcher
                    .deploymentManagerUnsafe(processId)(ec, user)
                    .flatMap(_.processCommand(DMStopScenarioCommand(processId.name, savepointDir, user.toManagerUser)))
                )
              }
            }
          }
        } ~
        path("deploy" / ProcessNameSegment) { processName =>
          (post & processId(processName) & entity(as[Option[String]]) & parameters(Symbol("savepointPath"))) {
            (processIdWithName, comment, savepointPath) =>
              canDeploy(processIdWithName) {
                complete {
                  deploymentService
                    .processCommand(
                      RunDeploymentCommand(
                        // adminProcessManagement endpoint is not used by the designer client. It is a part of API for tooling purpose
                        commonData = CommonCommandData(processIdWithName, comment.map(ApiCallComment(_)), user),
                        nodesDeploymentData = NodesDeploymentData.empty,
                        stateRestoringStrategy = StateRestoringStrategy.RestoreStateFromCustomSavepoint(savepointPath)
                      )
                    )
                    .map(_ => ())
                }
              }
          }
        }
    }
  } ~
    pathPrefix("processManagement") {

      path("deploy" / ProcessNameSegment) { processName =>
        (post & processId(processName) & entity(as[Option[String]])) { (processIdWithName, comment) =>
          canDeploy(processIdWithName) {
            complete {
              measureTime("deployment", metricRegistry) {
                deploymentService
                  .processCommand(
                    RunDeploymentCommand(
                      commonData = CommonCommandData(processIdWithName, comment.map(UserComment), user),
                      nodesDeploymentData = NodesDeploymentData.empty,
                      stateRestoringStrategy = StateRestoringStrategy.RestoreStateFromReplacedJobSavepoint
                    )
                  )
                  .map(_ => ())
              }
            }
          }
        }
      } ~
        path("cancel" / ProcessNameSegment) { processName =>
          (post & processId(processName) & entity(as[Option[String]])) { (processIdWithName, comment) =>
            canDeploy(processIdWithName) {
              complete {
                measureTime("cancel", metricRegistry) {
                  deploymentService.processCommand(
                    CancelScenarioCommand(commonData =
                      CommonCommandData(processIdWithName, comment.map(UserComment), user)
                    )
                  )
                }
              }
            }
          }
        } ~
        // TODO: maybe Write permission is enough here?
        path("test" / ProcessNameSegment) { processName =>
          (post & processDetailsForName(processName)) { details =>
            canDeploy(details.idWithNameUnsafe) {
              formFields(Symbol("testData"), Symbol("scenarioGraph")) { (testDataContent, scenarioGraphJson) =>
                complete {
                  measureTime("test", metricRegistry) {
                    parser.parse(scenarioGraphJson).flatMap(Decoder[ScenarioGraph].decodeJson) match {
                      case Right(scenarioGraph) =>
                        scenarioTestServices
                          .forProcessingTypeUnsafe(details.processingType)
                          .performTest(
                            details.idWithNameUnsafe,
                            scenarioGraph,
                            details.isFragment,
                            RawScenarioTestData(testDataContent)
                          )
                          .flatMap(mapResultsToHttpResponse)
                      case Left(error) =>
                        Future.failed(ProcessUnmarshallingError(error.toString))
                    }
                  }
                }
              }
            }
          }
        } ~
        path("generateAndTest" / ProcessNameSegment / IntNumber) { (processName, testSampleSize) =>
          {
            (post & entity(as[ScenarioGraph])) { scenarioGraph =>
              {
                processDetailsForName(processName)(user) { details =>
                  canDeploy(details.idWithNameUnsafe) {
                    complete {
                      measureTime("generateAndTest", metricRegistry) {
                        val scenarioTestService = scenarioTestServices.forProcessingTypeUnsafe(details.processingType)
                        scenarioTestService.generateData(
                          scenarioGraph,
                          processName,
                          details.isFragment,
                          testSampleSize
                        ) match {
                          case Left(error) => Future.failed(ProcessUnmarshallingError(error))
                          case Right(rawScenarioTestData) =>
                            scenarioTestService
                              .performTest(
                                details.idWithNameUnsafe,
                                scenarioGraph,
                                details.isFragment,
                                rawScenarioTestData
                              )
                              .flatMap(mapResultsToHttpResponse)
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        } ~
        path("testWithParameters" / ProcessNameSegment) { processName =>
          {
            (post & processDetailsForName(processName)) { process =>
              val modelData = typeToConfig.forProcessingTypeUnsafe(process.processingType)
              implicit val requestDecoder: Decoder[TestFromParametersRequest] =
                prepareTestFromParametersDecoder(modelData)
              (post & entity(as[TestFromParametersRequest])) { testParametersRequest =>
                {
                  canDeploy(process.idWithNameUnsafe) {
                    complete {
                      scenarioTestServices
                        .forProcessingTypeUnsafe(process.processingType)
                        .performTest(
                          process.idWithNameUnsafe,
                          testParametersRequest.scenarioGraph,
                          process.isFragment,
                          testParametersRequest.sourceParameters
                        )
                        .flatMap(mapResultsToHttpResponse)
                    }
                  }
                }
              }
            }
          }
        } ~
        path("customAction" / ProcessNameSegment) { processName =>
          (post & processId(processName) & entity(as[CustomActionRequest])) { (processIdWithName, req) =>
            complete {
              deploymentService
                .processCommand(
                  CustomActionCommand(
                    commonData = CommonCommandData(processIdWithName, req.comment.map(UserComment), user),
                    actionName = req.actionName,
                    params = req.params
                  )
                )
                .flatMap(actionResult =>
                  toHttpResponse(CustomActionResponse(isSuccess = true, actionResult.msg))(StatusCodes.OK)
                )
            }
          }
        }
    }

  private def mapResultsToHttpResponse: ResultsWithCounts => Future[HttpResponse] = { results =>
    Marshal(results).to[MessageEntity].map(en => HttpResponse(entity = en))
  }

  private def toHttpResponse[A: Encoder](a: A)(code: StatusCode): Future[HttpResponse] =
    Marshal(a).to[MessageEntity].map(en => HttpResponse(entity = en, status = code))

  private def convertSavepointResultToResponse(future: Future[SavepointResult]) = {
    future
      .map { case SavepointResult(path) => HttpResponse(entity = path, status = StatusCodes.OK) }
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy