Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
Zwiterrion committed Jan 24, 2025
1 parent b85fea9 commit 44e12b2
Show file tree
Hide file tree
Showing 8 changed files with 198 additions and 51 deletions.
95 changes: 65 additions & 30 deletions otoroshi/app/next/models/Api.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ import otoroshi.storage.{BasicStore, RedisLike, RedisLikeStore}
import otoroshi.utils.syntax.implicits.{BetterJsReadable, BetterJsValue, BetterSyntax}
import play.api.libs.json.{Format, JsArray, JsBoolean, JsError, JsNull, JsNumber, JsObject, JsResult, JsString, JsSuccess, JsValue, Json}

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

sealed trait ApiState {
def name: String
}

case object ApiStarted extends ApiState {
def name: String = "started"
case object ApiStaging extends ApiState {
def name: String = "staging"
}
case object ApiPublished extends ApiState {
def name: String = "published"
Expand Down Expand Up @@ -454,20 +454,35 @@ object ApiConsumerSubscription {
implicit val ec = env.otoroshiExecutionContext
implicit val e = env

def onError(error: String) = Json.obj(
def onError(error: String): Either[JsValue, ApiConsumerSubscription] = Json.obj(
"error" -> s"api consumer has rejected your demand : $error",
"http_status_code" -> 400
).leftf
).left

def addSubscriptionToConsumer(api: Api): Future[Boolean] = {
env.datastores.apiDataStore.set(api.copy(consumers = api.consumers.map(consumer => {
if (consumer.id == entity.consumerRef) {
consumer.copy(subscriptions = consumer.subscriptions :+ ApiConsumerSubscriptionRef(entity.id))
} else {
consumer
}
})))
}

// println(s"write validation foo: ${singularName} - ${id} - ${action} - ${body.prettify}")

env.datastores.apiDataStore.findById(entity.apiRef) flatMap {
env.datastores.apiDataStore.findById(entity.apiRef) flatMap {
case Some(api) => api.consumers.find(_.id == entity.consumerRef) match {
case Some(consumer) if consumer.status == ApiConsumerStatus.Published => entity.rightf
case _ => onError("wrong status")
case None => onError("consumer not found").vfuture
case Some(consumer) if consumer.status == ApiConsumerStatus.Published =>
addSubscriptionToConsumer(api).map {
case true => entity.right
case false => onError("failed to add subscription to api")
}
case _ => onError("wrong status").vfuture
}
case None => onError("consumer not found")
}
case _ => onError("api not found").vfuture
}
}

val format: Format[ApiConsumerSubscription] = new Format[ApiConsumerSubscription] {
Expand Down Expand Up @@ -708,7 +723,18 @@ case class Api(

override def theMetadata: Map[String, String] = metadata

def toRoutes: Seq[NgRoute] = ???
def toRoutes(implicit env: Env): Future[Seq[NgRoute]] = {
implicit val ec = env.otoroshiExecutionContext

if (state == ApiStaging) {
Seq.empty.vfuture
} else {
Future.sequence(routes.map(route => apiRouteToNgRoute(route.id))
.map(_.collect {
case Some(route) => route
})).map(_.filter(_.enabled))
}
}

def apiRouteToNgRoute(routeId: String)(implicit env: Env): Future[Option[NgRoute]] = {
implicit val ec = env.otoroshiExecutionContext
Expand Down Expand Up @@ -742,27 +768,30 @@ case class Api(

object Api {
def writeValidator(newApi: Api,
body: JsValue,
singularName: String,
id: Option[String],
_body: JsValue,
_singularName: String,
_id: Option[String],
action: WriteAction,
env: Env): Future[Either[JsValue, Api]] = {
implicit val ec = env.otoroshiExecutionContext
implicit val e = env

def onError(error: String) = Json.obj(
"error" -> s"api has rejected your demand : $error",
"http_status_code" -> 400
).leftf

// println(s"write validation foo: ${singularName} - ${id} - ${action} - ${body.prettify}")
implicit val ec: ExecutionContext = env.otoroshiExecutionContext
implicit val e: Env = env

if(action == WriteAction.Update) env.datastores.apiDataStore.findById(newApi.id)
if(action == WriteAction.Update) {
env.datastores.apiDataStore.findById(newApi.id)
.map(_.get)
.map(api => {

// API needs to be published to have consumers
if (newApi.consumers.nonEmpty && newApi.state == ApiStaging) {
return Json.obj(
"error" -> s"api is not accessible by consumers. Publish your API to continue",
"http_status_code" -> 400
).leftf
}

newApi.consumers.foreach(consumer => {
api.consumers.find(_.id == consumer.id).map(oldConsumer => {
// println(s"${oldConsumer.id} ${oldConsumer.status} - ${consumer.status}")
// println(s"${oldConsumer.id} ${oldConsumer.status} - ${consumer.status}")
// staging -> published = ok
// published -> deprecated = ok
// deprecated -> closed = ok
Expand All @@ -771,11 +800,16 @@ object Api {
if (consumer.status == ApiConsumerStatus.Published && oldConsumer.status == ApiConsumerStatus.Deprecated) {

} else if (oldConsumer.status.orderPosition > consumer.status.orderPosition) {
return onError("you can't get back to a consumer status")
return Json.obj(
"error" -> s"api has rejected your demand : you can't get back to a consumer status",
"http_status_code" -> 400
).leftf
}
})
})
})
})
}

newApi.rightf
}

Expand Down Expand Up @@ -819,11 +853,12 @@ object Api {
capture = (json \ "capture").asOpt[Boolean].getOrElse(false),
exportReporting = (json \ "export_reporting").asOpt[Boolean].getOrElse(false),
state = (json \ "state").asOptString.map {
case "started" => ApiStarted
case "staging" => ApiStaging
case "published" => ApiPublished
case "deprecated" => ApiDeprecated
case "removed" => ApiRemoved
}.getOrElse(ApiStarted),
case _ => ApiStaging
}.getOrElse(ApiStaging),
blueprint = (json \ " blueprint").asOptString.map {
case "REST" => ApiBlueprint.REST
case "GraphQL" => ApiBlueprint.GraphQL
Expand Down Expand Up @@ -880,7 +915,7 @@ trait ApiDataStore extends BasicStore[Api] {
debugFlow = false,
capture = false,
exportReporting = false,
state = ApiStarted,
state = ApiStaging,
blueprint = ApiBlueprint.REST,
routes = Seq.empty,
backends = Seq.empty,
Expand Down
3 changes: 2 additions & 1 deletion otoroshi/app/next/proxy/state.scala
Original file line number Diff line number Diff line change
Expand Up @@ -632,11 +632,12 @@ class NgProxyState(env: Env) {
)
})
} else Seq.empty[NgRoute].vfuture
apisRoutes <- Future.sequence(apis.map(_.toRoutes)).map(_.flatten)
_ <- env.adminExtensions.syncStates()
} yield {
env.proxyState.updateGlobalConfig(gc)
env.proxyState.updateRawRoutes(routes)
env.proxyState.updateRoutes(newRoutes ++ croutes)
env.proxyState.updateRoutes(newRoutes ++ croutes ++ apisRoutes)
env.proxyState.updateBackends(backends)
env.proxyState.updateApikeys(apikeys)
env.proxyState.updateCertificates(certs)
Expand Down
3 changes: 1 addition & 2 deletions otoroshi/app/storage/stores/KvGlobalConfigDataStore.scala
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,7 @@ class KvGlobalConfigDataStore(redisCli: RedisLike, _env: Env)
override def withinThrottlingQuota()(implicit ec: ExecutionContext, env: Env): Future[Boolean] = {
val config = latest()
//singleton().map { config =>
redisCli.get(throttlingKey()).map { bs =>
throttlingQuotasCache.set(bs.map(_.utf8String.toLong).getOrElse(0L))
redisCli.get(throttlingKey()).map { bs => throttlingQuotasCache.set(bs.map(_.utf8String.toLong).getOrElse(0L))
// throttlingQuotasCache.get() <= (config.throttlingQuota * 10L)
throttlingQuotasCache.get() <= config.throttlingQuota
}
Expand Down
2 changes: 1 addition & 1 deletion otoroshi/javascript/src/forms/ng_plugins/NgBackend.js
Original file line number Diff line number Diff line change
Expand Up @@ -537,7 +537,7 @@ export default {
},
},
flow: (item) => {
if (item.type === 'WeightedBestResponseTime') {
if (item?.type === 'WeightedBestResponseTime') {
return ['type', 'ratio'];
} else {
return ['type'];
Expand Down
18 changes: 8 additions & 10 deletions otoroshi/javascript/src/pages/ApiEditor/ApiStats.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,7 @@ class Metric extends Component {
width: props.width || 300,
}}
>
<div className="metric-text">
<span className="metric-text-value">{props.value}</span>
<span className="metric-text-title">{props.legend}</span>
</div>
<span className="metric-text-title">{props.legend}</span>
<div className="metric-box">
<Sparklines data={this.state.values} limit={this.state.values.length} height={65}>
<SparklinesLine
Expand All @@ -64,6 +61,7 @@ class Metric extends Component {
<SparklinesSpots />
</Sparklines>
</div>
<span className="metric-text-value">{props.value}<span>{props.unit}</span></span>
</div>
);
}
Expand Down Expand Up @@ -149,21 +147,21 @@ export class ApiStats extends Component {
}
return <>
<div className="row-metrics">
<Metric time={Date.now()} value={this.state.rate} legend="requests per second" />
<Metric time={Date.now()} value={this.state.duration} legend="ms. per request" />
<Metric time={Date.now()} value={this.state.overhead} legend="ms. overhead per request" />
<Metric time={Date.now()} value={this.state.rate} legend="Requests" unit="/sec" />
<Metric time={Date.now()} value={this.state.duration} legend="Per request" unit="ms" />
<Metric time={Date.now()} value={this.state.overhead} legend="Overhead per request" unit="ms" />
</div>
<div className="row-metrics">
{/* <Metric time={Date.now()} value={this.state.dataInRate} />
<Metric time={Date.now()} value={this.state.dataOutRate} /> */}

</div>
<div className="row-metrics">
<Metric time={Date.now()} value={this.state.requests} legend="requests served" />
<Metric time={Date.now()} value={this.state.requests} legend="Requests served" />
<Metric
time={Date.now()}
value={this.state.concurrentHandledRequests}
legend="concurrent requests"
legend="Concurrent requests"
/>
{/* <Metric time={Date.now()} value={this.state.dataIn} />
<Metric time={Date.now()} value={this.state.dataOut} /> */}
Expand Down
94 changes: 88 additions & 6 deletions otoroshi/javascript/src/pages/ApiEditor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,19 @@ function RouteDesigner(props) {
const [schema, setSchema] = useState()
const [route, setRoute] = useState({})

const [backends, setBackends] = useState([])

const backendsQuery = useQuery(['getBackends'],
() => nextClient.forEntityNext(nextClient.ENTITIES.BACKENDS).findAll(),
{
enabled: backends.length <= 0,
onSuccess: setBackends
})

const rawAPI = useQuery(["getAPI", params.apiId],
() => nextClient.forEntityNext(nextClient.ENTITIES.APIS).findById(params.apiId), {
retry: 0,
enabled: backendsQuery.data !== undefined,
onSuccess: data => {
setRoute(data.routes.find(route => route.id === params.routeId))
setSchema({
Expand Down Expand Up @@ -109,10 +119,41 @@ function RouteDesigner(props) {
},
},
backend: {
type: 'form',
type: 'select',
label: 'Backend',
schema: NgBackend.schema,
flow: NgBackend.flow
props: {
options: backends,
optionsTransformer: {
value: 'id',
label: 'name'
}
}
// return <BackendSelector
// enabled
// backends={[...data.backends, ...backends]}
// setUsingExistingBackend={e => {
// props.rootOnChange({
// ...props.rootValue,
// usingExistingBackend: e
// })
// }}
// onChange={backend_ref => {
// props.rootOnChange({
// ...props.rootValue,
// usingExistingBackend: true,
// backend: backend_ref
// })
// }}
// usingExistingBackend={props.rootValue.usingExistingBackend !== undefined ?
// props.rootValue.usingExistingBackend : (typeof props.rootValue.backend === 'string')
// }
// route={props.rootValue}
// />
// }
// type: 'form',
// label: 'Backend',
// schema: NgBackend.schema,
// flow: NgBackend.flow
}
})
}
Expand Down Expand Up @@ -1433,7 +1474,7 @@ function Dashboard(props) {
{/* {!api.health && <p className="alert alert-info" role="alert">API Health will appear here</p>} */}
{api.routes.map(route => {
return <div key={route.id}>
<h3 className="m-0">{route.name}</h3>
<h3 className="m-0 mb-1">{route.name}</h3>
<ApiStats url={`/bo/api/proxy/api/live/${route.id}?every=2000`} />
</div>
})}
Expand Down Expand Up @@ -1620,10 +1661,51 @@ function ContainerBlock({ children, full, highlighted }) {
}

function APIHeader({ api }) {
const updateAPI = newAPI => {
return nextClient
.forEntityNext(nextClient.ENTITIES.APIS)
.update(newAPI)
}

return <>
<div className='d-flex align-items-center gap-3'>
<h2 className='m-0'>{api.name}</h2>
<APIState value={api.state} />
{api.state === API_STATE.STAGING && <Button
type='primaryColor'
onClick={() => {
updateAPI({
...api,
state: API_STATE.PUBLISHED
})
.then(() => window.location.reload())
}}
className='btn-sm ms-auto'
text="Start you API" />}
{(api.state === API_STATE.PUBLISHED || api.state === API_STATE.DEPRECATED) &&
<Button
type='quiet'
onClick={() => {
updateAPI({
...api,
state: api.state === API_STATE.PUBLISHED ? API_STATE.DEPRECATED : API_STATE.PUBLISHED
})
.then(() => window.location.reload())
}}
className='btn-sm ms-auto'
text={api.state === API_STATE.PUBLISHED ? "Deprecate your API" : "Publish your API"} />}
{/* {(api.state === API_STATE.PUBLISHED || api.state === API_STATE.DEPRECATED) &&
<Button
type='danger'
onClick={() => {
updateAPI({
...api,
state: API_STATE.DEPRECATED
})
.then(() => window.location.reload())
}}
className='btn-sm ms-auto'
text="Close your API" />} */}
</div>
<div className='d-flex align-items-center gap-1 mb-3'>
<p className='m-0 me-2'>{api.description}</p>
Expand All @@ -1635,10 +1717,10 @@ function APIHeader({ api }) {
}

function APIState({ value }) {
if (value === API_STATE.STARTED)
if (value === API_STATE.STAGING)
return <span className='badge api-status-started'>
<i className='fas fa-rocket me-2' />
Started
Staging
</span>

if (value === API_STATE.DEPRECATED)
Expand Down
Loading

0 comments on commit 44e12b2

Please sign in to comment.