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

next.extensions.example.scala Maven / Gradle / Ivy

package otoroshi.next.extensions

import akka.actor.ActorSystem
import akka.stream.Materializer
import akka.util.ByteString
import otoroshi.api._
import otoroshi.cluster.ClusterMode
import otoroshi.env.Env
import otoroshi.models._
import otoroshi.storage._
import otoroshi.utils.cache.types.UnboundedTrieMap
import otoroshi.utils.syntax.implicits._
import play.api.inject.ApplicationLifecycle
import play.api.libs.json._
import play.api.mvc.Results
import play.api.{Configuration, Environment, Logger}
import storage.drivers.generic.{GenericDataStores, GenericRedisLike, GenericRedisLikeBuilder}

import scala.collection.concurrent.TrieMap
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success, Try}

case class Foo(
    location: EntityLocation,
    id: String,
    name: String,
    description: String,
    tags: Seq[String],
    metadata: Map[String, String]
) extends EntityLocationSupport {
  override def internalId: String               = id
  override def json: JsValue                    = Foo.format.writes(this)
  override def theName: String                  = name
  override def theDescription: String           = description
  override def theTags: Seq[String]             = tags
  override def theMetadata: Map[String, String] = metadata
}

object Foo {
  val format = new Format[Foo] {
    override def writes(o: Foo): JsValue             = o.location.jsonWithKey ++ Json.obj(
      "id"          -> o.id,
      "name"        -> o.name,
      "description" -> o.description,
      "metadata"    -> o.metadata,
      "tags"        -> JsArray(o.tags.map(JsString.apply))
    )
    override def reads(json: JsValue): JsResult[Foo] = Try {
      Foo(
        location = otoroshi.models.EntityLocation.readFromKey(json),
        id = (json \ "id").as[String],
        name = (json \ "name").as[String],
        description = (json \ "description").as[String],
        metadata = (json \ "metadata").asOpt[Map[String, String]].getOrElse(Map.empty),
        tags = (json \ "tags").asOpt[Seq[String]].getOrElse(Seq.empty[String])
      )
    } match {
      case Failure(ex)    => JsError(ex.getMessage)
      case Success(value) => JsSuccess(value)
    }
  }
}

trait FooDataStore extends BasicStore[Foo]

class KvFooDataStore(extensionId: AdminExtensionId, redisCli: RedisLike, _env: Env)
    extends FooDataStore
    with RedisLikeStore[Foo] {
  override def fmt: Format[Foo]                        = Foo.format
  override def redisLike(implicit env: Env): RedisLike = redisCli
  override def key(id: String): String                 = s"${_env.storageRoot}:extensions:${extensionId.cleanup}:foos:$id"
  override def extractId(value: Foo): String           = value.id
}

class FooAdminExtensionDatastores(env: Env, extensionId: AdminExtensionId) {
  val fooDatastore: FooDataStore = new KvFooDataStore(extensionId, env.datastores.redis, env)
}

class FooAdminExtensionState(env: Env) {

  private val foos = new UnboundedTrieMap[String, Foo]()

  def foo(id: String): Option[Foo] = foos.get(id)
  def allFoos(): Seq[Foo]          = foos.values.toSeq

  private[extensions] def updateFoos(values: Seq[Foo]): Unit = {
    foos.addAll(values.map(v => (v.id, v))).remAll(foos.keySet.toSeq.diff(values.map(_.id)))
  }
}

class FooRedisLike(env: Env, actorSystem: ActorSystem) extends GenericRedisLike {

  val redis       = new otoroshi.storage.drivers.inmemory.SwappableInMemoryRedis(false, env, actorSystem)
  implicit val ec = actorSystem.dispatcher

  override def setCounter(key: String, value: Long): Future[Unit]               = redis.set(key, value.toString).map(_ => ())
  override def rawGet(key: String): Future[Option[Any]]                         = redis.rawGet(key)
  override def health()(implicit ec: ExecutionContext): Future[DataStoreHealth] = redis.health()
  override def stop(): Unit                                                     = redis.stop()
  override def flushall(): Future[Boolean]                                      = redis.flushall()

  override def get(key: String): Future[Option[ByteString]]                                                            = redis.get(key)
  override def mget(keys: String*): Future[Seq[Option[ByteString]]]                                                    = redis.mget(keys: _*)
  override def set(key: String, value: String, exSeconds: Option[Long], pxMilliseconds: Option[Long]): Future[Boolean] =
    redis.set(key, value, exSeconds, pxMilliseconds)
  override def setBS(
      key: String,
      value: ByteString,
      exSeconds: Option[Long],
      pxMilliseconds: Option[Long]
  ): Future[Boolean]                                                                                                   = redis.setBS(key, value, exSeconds, pxMilliseconds)
  override def del(keys: String*): Future[Long]                                                                        = redis.del(keys: _*)
  override def incr(key: String): Future[Long]                                                                         = redis.incr(key)
  override def incrby(key: String, increment: Long): Future[Long]                                                      = redis.incrby(key, increment)
  override def exists(key: String): Future[Boolean]                                                                    = redis.exists(key)
  override def keys(pattern: String): Future[Seq[String]]                                                              = redis.keys(pattern)

  override def hdel(key: String, fields: String*): Future[Long]                       = redis.hdel(key, fields: _*)
  override def hgetall(key: String): Future[Map[String, ByteString]]                  = redis.hgetall(key)
  override def hset(key: String, field: String, value: String): Future[Boolean]       = redis.hset(key, field, value)
  override def hsetBS(key: String, field: String, value: ByteString): Future[Boolean] = redis.hsetBS(key, field, value)

  override def llen(key: String): Future[Long]                                       = redis.llen(key)
  override def lpush(key: String, values: String*): Future[Long]                     = redis.lpush(key, values: _*)
  override def lpushLong(key: String, values: Long*): Future[Long]                   = redis.lpushLong(key, values: _*)
  override def lpushBS(key: String, values: ByteString*): Future[Long]               = redis.lpushBS(key, values: _*)
  override def lrange(key: String, start: Long, stop: Long): Future[Seq[ByteString]] = redis.lrange(key, start, stop)
  override def ltrim(key: String, start: Long, stop: Long): Future[Boolean]          = redis.ltrim(key, start, stop)

  override def pttl(key: String): Future[Long]                           = redis.pttl(key)
  override def ttl(key: String): Future[Long]                            = redis.ttl(key)
  override def expire(key: String, seconds: Int): Future[Boolean]        = redis.expire(key, seconds)
  override def pexpire(key: String, milliseconds: Long): Future[Boolean] = redis.pexpire(key, milliseconds)

  override def sadd(key: String, members: String*): Future[Long]             = redis.sadd(key, members: _*)
  override def saddBS(key: String, members: ByteString*): Future[Long]       = redis.saddBS(key, members: _*)
  override def sismember(key: String, member: String): Future[Boolean]       = redis.sismember(key, member)
  override def sismemberBS(key: String, member: ByteString): Future[Boolean] = redis.sismemberBS(key, member)
  override def smembers(key: String): Future[Seq[ByteString]]                = redis.smembers(key)
  override def srem(key: String, members: String*): Future[Long]             = redis.srem(key, members: _*)
  override def sremBS(key: String, members: ByteString*): Future[Long]       = redis.sremBS(key, members: _*)
  override def scard(key: String): Future[Long]                              = redis.scard(key)

  override def typ(key: String): Future[String] = {
    rawGet(key) map {
      case Some(_: String)                                                     => "string"
      case Some(_: ByteString)                                                 => "string"
      case Some(_: Long)                                                       => "string"
      case Some(_: java.util.concurrent.ConcurrentHashMap[String, ByteString]) => "hash"
      case Some(_: TrieMap[String, ByteString])                                => "hash"
      case Some(_: java.util.concurrent.CopyOnWriteArrayList[ByteString])      => "list"
      case Some(_: scala.collection.mutable.MutableList[ByteString])           => "list"
      case Some(_: java.util.concurrent.CopyOnWriteArraySet[ByteString])       => "set"
      case Some(_: scala.collection.mutable.HashSet[ByteString])               => "set"
      case _                                                                   => "none"
    }
  }
}

class FooRedisLikeBuilder  extends GenericRedisLikeBuilder {
  override def build(
      configuration: Configuration,
      environment: Environment,
      lifecycle: ApplicationLifecycle,
      clusterMode: ClusterMode,
      redisStatsItems: Int,
      actorSystem: ActorSystem,
      mat: Materializer,
      logger: Logger,
      env: Env
  ): GenericRedisLike = {
    new FooRedisLike(env, actorSystem)
  }
}
class FooDataStoresBuilder extends DataStoresBuilder       {
  override def build(
      configuration: Configuration,
      environment: Environment,
      lifecycle: ApplicationLifecycle,
      clusterMode: ClusterMode,
      env: Env
  ): DataStores = {
    new GenericDataStores(
      configuration,
      environment,
      lifecycle,
      clusterMode,
      redisStatsItems = 100,
      builder = new FooRedisLikeBuilder(),
      env
    )
  }
}

object FooDataStoresBuilder {
  def apply(): DataStoresBuilder = new FooDataStoresBuilder()
}

class FooAdminExtension(val env: Env) extends AdminExtension {

  private lazy val datastores = new FooAdminExtensionDatastores(env, id)
  private lazy val states     = new FooAdminExtensionState(env)

  override def id: AdminExtensionId = AdminExtensionId("otoroshi.extensions.Foo")

  override def name: String = "Foo"

  override def description: Option[String] = "Foo".some

  override def enabled: Boolean = env.isDev || configuration.getOptional[Boolean]("enabled").getOrElse(false)

  override def start(): Unit = {
    "start example extension".debugPrintln
  }

  override def stop(): Unit = {
    "stop example extension".debugPrintln
  }

  override def datastoreBuilders(): Map[String, DataStoresBuilder] = Map(
    "foo" -> FooDataStoresBuilder()
  )

  override def syncStates(): Future[Unit] = {
    implicit val ec = env.otoroshiExecutionContext
    implicit val ev = env
    for {
      foos <- datastores.fooDatastore.findAll()
    } yield {
      states.updateFoos(foos)
      ()
    }
  }

  override def frontendExtensions(): Seq[AdminExtensionFrontendExtension] = {
    Seq(
      AdminExtensionFrontendExtension("/__otoroshi_assets/javascripts/extensions/foos.js")
    )
  }

  override def wellKnownRoutes(): Seq[AdminExtensionWellKnownRoute] = Seq(
    AdminExtensionWellKnownRoute(
      "GET",
      "/.well-known/otoroshi/extensions/foo/bars/:id",
      false,
      (ctx, request, body) => {
        Results.Ok(Json.obj("id" -> ctx.named("id").map(JsString.apply).getOrElse(JsNull).asValue)).vfuture
      }
    ),
    AdminExtensionWellKnownRoute(
      "GET",
      "/.well-known/otoroshi/extensions/foo/check",
      false,
      (ctx, request, body) => {
        Results.Ok(Json.obj("check" -> true)).vfuture
      }
    )
  )

  override def adminApiRoutes(): Seq[AdminExtensionAdminApiRoute] = Seq(
    AdminExtensionAdminApiRoute(
      "GET",
      "/api/extensions/foo/foos/:id",
      false,
      (ctx, request, apk, _) => {
        Results.Ok(Json.obj("foo_id" -> ctx.named("id").map(JsString.apply).getOrElse(JsNull).asValue)).vfuture
      }
    )
  )

  override def entities(): Seq[AdminExtensionEntity[EntityLocationSupport]] = {
    Seq(
      AdminExtensionEntity(
        Resource(
          "Foo",
          "foos",
          "foo",
          "foo.extensions.otoroshi.io",
          ResourceVersion("v1", true, false, true),
          GenericResourceAccessApiWithState[Foo](
            Foo.format,
            classOf[Foo],
            id => datastores.fooDatastore.key(id),
            c => datastores.fooDatastore.extractId(c),
            json => json.select("id").asString,
            () => "id",
            stateAll = () => states.allFoos(),
            stateOne = id => states.foo(id),
            stateUpdate = values => states.updateFoos(values)
          )
        )
      )
    )
  }
}

/*

case class FooPluginConfig(filter: String) extends NgPluginConfig {
  def json: JsValue = Json.obj("filter" -> filter)
}

class FooPlugin extends NgAccessValidator {

  override def core: Boolean = false
  override def name: String = "Foo"
  override def description: Option[String] = "foo foo".some
  override def visibility: NgPluginVisibility = NgPluginVisibility.NgUserLand
  override def categories: Seq[NgPluginCategory] = Seq(NgPluginCategory.AccessControl)
  override def steps: Seq[NgStep] = Seq(NgStep.ValidateAccess)
  override def isAccessAsync: Boolean = true
  override def defaultConfigObject: Option[NgPluginConfig] = Some(FooPluginConfig("--"))
  override def multiInstance: Boolean = true

  override def accessSync(ctx: NgAccessContext)(implicit env: Env, ec: ExecutionContext): NgAccess = NgAccess.NgAllowed
}

 */




© 2015 - 2025 Weber Informatics LLC | Privacy Policy