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

next.plugins.chaos.scala Maven / Gradle / Ivy

package otoroshi.next.plugins

import akka.stream.Materializer
import com.github.blemale.scaffeine.Scaffeine
import otoroshi.env.Env
import otoroshi.gateway.{SnowMonkey, SnowMonkeyContext}
import otoroshi.models.SnowMonkeyConfig.logger
import otoroshi.models.{
  BadResponse,
  BadResponsesFaultConfig,
  ChaosConfig,
  LargeRequestFaultConfig,
  LargeResponseFaultConfig,
  LatencyInjectionFaultConfig,
  SnowMonkeyConfig
}
import otoroshi.next.plugins.api._
import otoroshi.utils.http.RequestImplicits.EnhancedRequestHeader
import otoroshi.utils.syntax.implicits._
import play.api.libs.json.{Format, JsArray, JsError, JsNull, JsObject, JsResult, JsSuccess, JsValue, Json, Reads}
import play.api.libs.typedmap.TypedKey
import play.api.mvc.Result

import scala.concurrent.duration.FiniteDuration
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success, Try}

case class NgBadResponse(status: Int, body: String, headers: Map[String, String]) {
  def json: JsValue            = NgBadResponse.format.writes(this)
  lazy val legacy: BadResponse = BadResponse(status, body, headers)
}

object NgBadResponse {
  def fromLegacy(settings: BadResponse): NgBadResponse = NgBadResponse(settings.status, settings.body, settings.headers)
  val format: Format[NgBadResponse]                    = new Format[NgBadResponse] {
    override def reads(json: JsValue): JsResult[NgBadResponse] = {
      Try {
        NgBadResponse(
          status = (json \ "status").asOpt[Int].orElse((json \ "status").asOpt[String].map(_.toInt)).getOrElse(500),
          body = (json \ "body").asOpt[String].getOrElse("""{"error":"..."}"""),
          headers = (json \ "headers").asOpt[Map[String, String]].getOrElse(Map.empty[String, String])
        )
      } match {
        case Failure(e) => JsError(e.getMessage)
        case Success(c) => JsSuccess(c)
      }
    }
    override def writes(o: NgBadResponse): JsValue = {
      Json.obj(
        "status"  -> o.status,
        "body"    -> o.body,
        "headers" -> o.headers
      )
    }
  }
}

sealed trait NgFaultConfig {
  def ratio: Double
  def json: JsValue
}
case class NgLargeRequestFaultConfig(ratio: Double, additionalRequestSize: Int)    extends NgFaultConfig {
  def json: JsValue                   = NgLargeRequestFaultConfig.format.writes(this)
  def legacy: LargeRequestFaultConfig = LargeRequestFaultConfig(ratio, additionalRequestSize)
}
object NgLargeRequestFaultConfig {
  def fromLegacy(s: LargeRequestFaultConfig): NgLargeRequestFaultConfig =
    NgLargeRequestFaultConfig(s.ratio, s.additionalRequestSize)
  val format: Format[NgLargeRequestFaultConfig]                         = new Format[NgLargeRequestFaultConfig] {
    override def reads(json: JsValue): JsResult[NgLargeRequestFaultConfig] = {
      Try {
        NgLargeRequestFaultConfig(
          ratio = (json \ "ratio").as[Double],
          additionalRequestSize = (json \ "additionalRequestSize").as[Int]
        )
      } match {
        case Failure(e) => JsError(e.getMessage)
        case Success(c) => JsSuccess(c)
      }
    }
    override def writes(o: NgLargeRequestFaultConfig): JsValue = {
      Json.obj(
        "ratio"                 -> o.ratio,
        "additionalRequestSize" -> o.additionalRequestSize
      )
    }
  }
}
case class NgLargeResponseFaultConfig(ratio: Double, additionalResponseSize: Int)  extends NgFaultConfig {
  def json: JsValue                    = NgLargeResponseFaultConfig.format.writes(this)
  def legacy: LargeResponseFaultConfig = LargeResponseFaultConfig(ratio, additionalResponseSize)
}
object NgLargeResponseFaultConfig {
  def fromLegacy(s: LargeResponseFaultConfig): NgLargeResponseFaultConfig =
    NgLargeResponseFaultConfig(s.ratio, s.additionalResponseSize)
  val format: Format[NgLargeResponseFaultConfig]                          = new Format[NgLargeResponseFaultConfig] {
    override def reads(json: JsValue): JsResult[NgLargeResponseFaultConfig] = {
      Try {
        NgLargeResponseFaultConfig(
          ratio = (json \ "ratio").as[Double],
          additionalResponseSize = (json \ "additionalResponseSize").as[Int]
        )
      } match {
        case Failure(e) => JsError(e.getMessage)
        case Success(c) => JsSuccess(c)
      }
    }
    override def writes(o: NgLargeResponseFaultConfig): JsValue = {
      Json.obj(
        "ratio"                  -> o.ratio,
        "additionalResponseSize" -> o.additionalResponseSize
      )
    }
  }
}
case class NgLatencyInjectionFaultConfig(ratio: Double, from: FiniteDuration, to: FiniteDuration)
    extends NgFaultConfig {
  def json: JsValue                       = NgLatencyInjectionFaultConfig.format.writes(this)
  def legacy: LatencyInjectionFaultConfig = LatencyInjectionFaultConfig(ratio, from, to)
}
object NgLatencyInjectionFaultConfig {
  def fromLegacy(s: LatencyInjectionFaultConfig): NgLatencyInjectionFaultConfig =
    NgLatencyInjectionFaultConfig(s.ratio, s.from, s.to)
  val format: Format[NgLatencyInjectionFaultConfig]                             = new Format[NgLatencyInjectionFaultConfig] {
    override def reads(json: JsValue): JsResult[NgLatencyInjectionFaultConfig] = {
      Try {
        NgLatencyInjectionFaultConfig(
          ratio = (json \ "ratio").as[Double],
          from = (json \ "from").as(SnowMonkeyConfig.durationFmt),
          to = (json \ "to").as(SnowMonkeyConfig.durationFmt)
        )
      } match {
        case Failure(e) => JsError(e.getMessage)
        case Success(c) => JsSuccess(c)
      }
    }
    override def writes(o: NgLatencyInjectionFaultConfig): JsValue = {
      Json.obj(
        "ratio" -> o.ratio,
        "from"  -> SnowMonkeyConfig.durationFmt.writes(o.from),
        "to"    -> SnowMonkeyConfig.durationFmt.writes(o.to)
      )
    }
  }
}
case class NgBadResponsesFaultConfig(ratio: Double, responses: Seq[NgBadResponse]) extends NgFaultConfig {
  def json: JsValue                   = NgBadResponsesFaultConfig.format.writes(this)
  def legacy: BadResponsesFaultConfig = BadResponsesFaultConfig(ratio, responses.map(_.legacy))
}
object NgBadResponsesFaultConfig {
  def fromLegacy(s: BadResponsesFaultConfig): NgBadResponsesFaultConfig =
    NgBadResponsesFaultConfig(s.ratio, s.responses.map(NgBadResponse.fromLegacy))
  val format: Format[NgBadResponsesFaultConfig]                         = new Format[NgBadResponsesFaultConfig] {
    override def reads(json: JsValue): JsResult[NgBadResponsesFaultConfig] = {
      Try {
        NgBadResponsesFaultConfig(
          ratio = (json \ "ratio").as[Double],
          responses = (json \ "responses").as(Reads.seq(NgBadResponse.format))
        )
      } match {
        case Failure(e) => JsError(e.getMessage)
        case Success(c) => JsSuccess(c)
      }
    }
    override def writes(o: NgBadResponsesFaultConfig): JsValue = {
      Json.obj(
        "ratio"     -> o.ratio,
        "responses" -> JsArray(o.responses.map(_.json))
      )
    }
  }
}

case class NgChaosConfig(
    largeRequestFaultConfig: Option[NgLargeRequestFaultConfig] = None,
    largeResponseFaultConfig: Option[NgLargeResponseFaultConfig] = None,
    latencyInjectionFaultConfig: Option[NgLatencyInjectionFaultConfig] = None,
    badResponsesFaultConfig: Option[NgBadResponsesFaultConfig] = None
) extends NgPluginConfig {
  def json: JsValue       = NgChaosConfig.format.writes(this)
  def legacy: ChaosConfig = ChaosConfig(
    enabled = true,
    largeRequestFaultConfig = largeRequestFaultConfig.map(_.legacy),
    largeResponseFaultConfig = largeResponseFaultConfig.map(_.legacy),
    latencyInjectionFaultConfig = latencyInjectionFaultConfig.map(_.legacy),
    badResponsesFaultConfig = badResponsesFaultConfig.map(_.legacy)
  )
}

object NgChaosConfig {
  def fromLegacy(s: ChaosConfig): NgChaosConfig = NgChaosConfig(
    largeRequestFaultConfig = s.largeRequestFaultConfig.map(NgLargeRequestFaultConfig.fromLegacy),
    largeResponseFaultConfig = s.largeResponseFaultConfig.map(NgLargeResponseFaultConfig.fromLegacy),
    latencyInjectionFaultConfig = s.latencyInjectionFaultConfig.map(NgLatencyInjectionFaultConfig.fromLegacy),
    badResponsesFaultConfig = s.badResponsesFaultConfig.map(NgBadResponsesFaultConfig.fromLegacy)
  )
  val format: Format[NgChaosConfig]             = new Format[NgChaosConfig] {
    override def reads(json: JsValue): JsResult[NgChaosConfig] = {
      Try {
        NgChaosConfig(
          largeRequestFaultConfig =
            (json \ "large_request_fault").asOpt[NgLargeRequestFaultConfig](NgLargeRequestFaultConfig.format),
          largeResponseFaultConfig =
            (json \ "large_response_fault").asOpt[NgLargeResponseFaultConfig](NgLargeResponseFaultConfig.format),
          latencyInjectionFaultConfig = (json \ "latency_injection_fault")
            .asOpt[NgLatencyInjectionFaultConfig](NgLatencyInjectionFaultConfig.format),
          badResponsesFaultConfig =
            (json \ "bad_responses_fault").asOpt[NgBadResponsesFaultConfig](NgBadResponsesFaultConfig.format)
        )
      } match {
        case Failure(e) => JsError(e.getMessage)
        case Success(c) => JsSuccess(c)
      }
    }
    override def writes(o: NgChaosConfig): JsValue = {
      Json.obj(
        "large_request_fault"     -> o.largeRequestFaultConfig.map(_.json).getOrElse(JsNull).as[JsValue],
        "large_response_fault"    -> o.largeResponseFaultConfig.map(_.json).getOrElse(JsNull).as[JsValue],
        "latency_injection_fault" -> o.latencyInjectionFaultConfig.map(_.json).getOrElse(JsNull).as[JsValue],
        "bad_responses_fault"     -> o.badResponsesFaultConfig.map(_.json).getOrElse(JsNull).as[JsValue]
      )
    }
  }
}

object SnowMonkeyChaos {
  val ContextKey = TypedKey[SnowMonkeyContext]("otoroshi.next.plugins.SnowMonkeyContext")
}

class SnowMonkeyChaos extends NgRequestTransformer {

  private val snowMonkeyRef                     = Scaffeine().maximumSize(1).build[String, SnowMonkey]()
  private val configReads: Reads[NgChaosConfig] = NgChaosConfig.format

  override def steps: Seq[NgStep]                = Seq(NgStep.TransformRequest, NgStep.TransformResponse)
  override def categories: Seq[NgPluginCategory] = Seq(NgPluginCategory.TrafficControl, NgPluginCategory.Classic)
  override def visibility: NgPluginVisibility    = NgPluginVisibility.NgUserLand

  override def multiInstance: Boolean                      = false
  override def core: Boolean                               = true
  override def usesCallbacks: Boolean                      = false
  override def transformsRequest: Boolean                  = true
  override def transformsResponse: Boolean                 = true
  override def transformsError: Boolean                    = false
  override def isTransformRequestAsync: Boolean            = true
  override def isTransformResponseAsync: Boolean           = false
  override def name: String                                = "Snow Monkey Chaos"
  override def description: Option[String]                 = "This plugin introduce some chaos into you life".some
  override def defaultConfigObject: Option[NgPluginConfig] = NgChaosConfig().some

  override def transformRequest(
      ctx: NgTransformerRequestContext
  )(implicit env: Env, ec: ExecutionContext, mat: Materializer): Future[Either[Result, NgPluginHttpRequest]] = {
    // val config = ctx.cachedConfig(internalName)(configReads).getOrElse(ChaosConfig(enabled = true))
    val snowMonkey   = snowMonkeyRef.get("singleton", _ => new SnowMonkey()(env))
    val globalConfig = env.datastores.globalConfigDataStore.latest()
    val reqNumber    = ctx.attrs.get(otoroshi.plugins.Keys.RequestNumberKey).get
    snowMonkey.introduceChaosGen[NgPluginHttpRequest](
      reqNumber,
      globalConfig,
      ctx.route.serviceDescriptor,
      ctx.request.theHasBody
    ) { snowMonkeyCtx =>
      ctx.attrs.put(SnowMonkeyChaos.ContextKey -> snowMonkeyCtx)
      ctx.otoroshiRequest
        .copy(
          headers = ctx.otoroshiRequest.headers + ("Content-Length" -> (ctx.otoroshiRequest
            .header("Content-Length")
            .getOrElse("0")
            .toInt + snowMonkeyCtx.trailingRequestBodySize).toString),
          body = ctx.otoroshiRequest.body.concat(snowMonkeyCtx.trailingRequestBodyStream)
        )
        .right
        .vfuture
    }
  }

  override def transformResponseSync(
      ctx: NgTransformerResponseContext
  )(implicit env: Env, ec: ExecutionContext, mat: Materializer): Either[Result, NgPluginHttpResponse] = {
    // val config = ctx.cachedConfig(internalName)(configReads).getOrElse(ChaosConfig(enabled = true))
    ctx.attrs.get(SnowMonkeyChaos.ContextKey) match {
      case None                => ctx.otoroshiResponse.right
      case Some(snowMonkeyCtx) => {
        ctx.otoroshiResponse
          .copy(
            headers = ctx.otoroshiResponse.headers + ("Content-Length" -> (ctx.otoroshiResponse
              .header("Content-Length")
              .getOrElse("0")
              .toInt + snowMonkeyCtx.trailingResponseBodySize).toString),
            body = ctx.otoroshiResponse.body.concat(snowMonkeyCtx.trailingResponseBodyStream)
          )
          .right
      }
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy