Skip to content

Commit

Permalink
starting test
Browse files Browse the repository at this point in the history
  • Loading branch information
Zwiterrion committed Feb 6, 2025
1 parent 36456d4 commit c2793e3
Show file tree
Hide file tree
Showing 9 changed files with 329 additions and 23 deletions.
2 changes: 1 addition & 1 deletion otoroshi/app/api/api.scala
Original file line number Diff line number Diff line change
Expand Up @@ -988,7 +988,7 @@ class OtoroshiResources(env: Env) {
stateOne = id => env.proxyState.apiConsumerSubscription(id),
stateUpdate = seq => env.proxyState.updateApiConsumerSubscriptions(seq),
writeValidator = ApiConsumerSubscription.writeValidator,
// deleteValidator = deleteValidatorForApiConsumerSubscription,
deleteValidator = ApiConsumerSubscription.deleteValidator,
)
)
)++ env.adminExtensions.resources()
Expand Down
147 changes: 147 additions & 0 deletions otoroshi/app/next/controllers/apis.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package otoroshi.next.controllers.adminapi

import akka.NotUsed
import akka.stream.scaladsl.Source
import next.models.{Api, ApiConsumerStatus, ApiPublished, ApiRemoved, ApiState}
import otoroshi.actions.ApiAction
import otoroshi.env.Env
import otoroshi.events.{AdminApiEvent, Audit}
Expand Down Expand Up @@ -121,4 +122,150 @@ class ApisController(ApiAction: ApiAction, cc: ControllerComponents)(implicit en
}
}


def start(id: String) = {
ApiAction.async { ctx =>
ctx.canReadService(id) {
Audit.send(
AdminApiEvent(
env.snowflakeGenerator.nextIdStr(),
env.env,
Some(ctx.apiKey),
ctx.user,
"ACCESS_SERVICE_APIS",
"User started the api",
ctx.from,
ctx.ua,
Json.obj("apiId" -> id)
)
)

toggleApiRoutesStatus(id, newStatus = true)
}
}
}

def toggleApiRoutesStatus(apiId: String, newStatus: Boolean): Future[Result] = {
env.datastores.apiDataStore.findById(apiId).flatMap {
case Some(api) => env.datastores.apiDataStore.set(api.copy(
state = ApiPublished,
routes = api.routes.map(route => route.copy(enabled = newStatus))))
.flatMap(_ => Results.Ok.future)
case None => Results.NotFound.future
}
}

def stop(id: String) = {
ApiAction.async { ctx =>
ctx.canReadService(id) {
Audit.send(
AdminApiEvent(
env.snowflakeGenerator.nextIdStr(),
env.env,
Some(ctx.apiKey),
ctx.user,
"ACCESS_SERVICE_APIS",
"User stopped the api",
ctx.from,
ctx.ua,
Json.obj("apiId" -> id)
)
)

toggleApiRoutesStatus(id, newStatus = false)
}
}
}

def publishConsumer(apiId: String, consumerId: String): Action[AnyContent] = {
ApiAction.async { ctx =>
ctx.canReadService(apiId) {
Audit.send(
AdminApiEvent(
env.snowflakeGenerator.nextIdStr(),
env.env,
Some(ctx.apiKey),
ctx.user,
"ACCESS_SERVICE_API_CONSUMER",
"User published the consumer",
ctx.from,
ctx.ua,
Json.obj("apiId" -> apiId, "consumerId" -> consumerId)
)
)

updateConsumerStatus(apiId, consumerId, ApiConsumerStatus.Published)
}
}
}

def updateConsumerStatus(apiId: String, consumerId: String, status: ApiConsumerStatus): Future[Result] = {
env.datastores.apiDataStore.findById(apiId).flatMap {
case Some(api) =>
var result: Option[String] = Some("")
val newAPI = api.copy(consumers = api.consumers.map(consumer => {
if(consumer.id == consumerId) {
if (Api.updateConsumerStatus(consumer, consumer.copy(status = status))) {
consumer.copy(status = status)
} else {
result = None
consumer
}
} else {
consumer
}
}))

result match {
case None => Results.BadRequest(Json.obj("error" -> "you can't update consumer status")).future
case Some(_) => env.datastores.apiDataStore.set(newAPI)
.flatMap(_ => Results.Ok.vfuture)
}
case None => Results.NotFound.future
}
}

def deprecateConsumer(apiId: String, consumerId: String) = {
ApiAction.async { ctx =>
ctx.canReadService(apiId) {
Audit.send(
AdminApiEvent(
env.snowflakeGenerator.nextIdStr(),
env.env,
Some(ctx.apiKey),
ctx.user,
"ACCESS_SERVICE_API_CONSUMER",
"User deprecated the consumer",
ctx.from,
ctx.ua,
Json.obj("apiId" -> apiId, "consumerId" -> consumerId)
)
)

updateConsumerStatus(apiId, consumerId, ApiConsumerStatus.Deprecated)
}
}
}

def closeConsumer(apiId: String, consumerId: String) = {
ApiAction.async { ctx =>
ctx.canReadService(apiId) {
Audit.send(
AdminApiEvent(
env.snowflakeGenerator.nextIdStr(),
env.env,
Some(ctx.apiKey),
ctx.user,
"ACCESS_SERVICE_API_CONSUMER",
"User deprecated the consumer",
ctx.from,
ctx.ua,
Json.obj("apiId" -> apiId, "consumerId" -> consumerId)
)
)

updateConsumerStatus(apiId, consumerId, ApiConsumerStatus.Closed)
}
}
}
}
69 changes: 53 additions & 16 deletions otoroshi/app/next/models/Api.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package next.models
import akka.util.ByteString
import diffson.PatchOps
import org.joda.time.DateTime
import otoroshi.api.WriteAction
import otoroshi.api.{DeleteAction, WriteAction}
import otoroshi.env.Env
import otoroshi.models.{EntityLocation, EntityLocationSupport, LoadBalancing, RemainingQuotas, ServiceDescriptor}
import otoroshi.next.models._
Expand Down Expand Up @@ -67,14 +67,20 @@ case object ApiRemoved extends ApiState {
// }
//}

case class ApiRoute(id: String, name: Option[String], frontend: NgFrontend, flowRef: String, backend: String)
case class ApiRoute(id: String,
enabled: Boolean,
name: Option[String],
frontend: NgFrontend,
flowRef: String,
backend: String)

object ApiRoute {
val _fmt: Format[ApiRoute] = new Format[ApiRoute] {

override def reads(json: JsValue): JsResult[ApiRoute] = Try {
ApiRoute(
id = json.select("id").asString,
enabled = json.select("enabled").asOptBoolean.getOrElse(true),
name = json.select("name").asOptString,
frontend = NgFrontend.readFrom(json \ "frontend"),
flowRef = (json \ "flow_ref").asString,
Expand All @@ -89,6 +95,7 @@ object ApiRoute {

override def writes(o: ApiRoute): JsValue = Json.obj(
"id" -> o.id,
"enabled" -> o.enabled,
"name" -> o.name,
"frontend" -> o.frontend.json,
"backend" -> o.backend,
Expand Down Expand Up @@ -297,12 +304,12 @@ object ApiBlueprint {
case class ApiConsumer(
id: String,
name: String,
description: Option[String],
description: Option[String] = None,
autoValidation: Boolean,
consumerKind: ApiConsumerKind,
settings: ApiConsumerSettings,
status: ApiConsumerStatus,
subscriptions: Seq[ApiConsumerSubscriptionRef]
subscriptions: Seq[ApiConsumerSubscriptionRef] = Seq.empty
)

object ApiConsumer {
Expand Down Expand Up @@ -446,6 +453,30 @@ case class ApiConsumerSubscription(

object ApiConsumerSubscription {

def deleteValidator(entity: ApiConsumerSubscription,
body: JsValue,
singularName: String,
id: String,
action: DeleteAction,
env: Env): Future[Either[JsValue, Unit]] = {
implicit val ec: ExecutionContext = env.otoroshiExecutionContext
implicit val e: Env = env

env.datastores.apiDataStore.findById(entity.apiRef) flatMap {
case Some(api) => env.datastores.apiDataStore.set(api.copy(consumers = api.consumers.map(consumer => {
if (consumer.id == entity.consumerRef) {
consumer.copy(subscriptions = consumer.subscriptions.filter(_.ref != id))
}
else
consumer
}))).map(_ => ().right)
case None => Json.obj(
"error" -> "api not found",
"http_status_code" -> 404
).as[JsValue].left.vfuture
}
}

def writeValidator(entity: ApiConsumerSubscription,
body: JsValue,
singularName: String,
Expand Down Expand Up @@ -713,7 +744,7 @@ case class Api(
def toRoutes(implicit env: Env): Future[Seq[NgRoute]] = {
implicit val ec = env.otoroshiExecutionContext

if (state == ApiStaging) {
if (state != ApiRemoved) {
Seq.empty.vfuture
} else {
Future.sequence(routes.map(route => apiRouteToNgRoute(route.id)))
Expand Down Expand Up @@ -760,7 +791,7 @@ case class Api(
description = description,
tags = tags,
metadata = metadata,
enabled = true,
enabled = apiRoute.enabled,
capture = capture,
debugFlow = debugFlow,
exportReporting = exportReporting,
Expand Down Expand Up @@ -801,16 +832,8 @@ object Api {

newApi.consumers.foreach(consumer => {
api.consumers.find(_.id == consumer.id).map(oldConsumer => {
// println(s"${oldConsumer.id} ${oldConsumer.status} - ${consumer.status}")
// staging -> published = ok
// published -> deprecated = ok
// deprecated -> closed = ok
// deprecated -> published = ok

if (consumer.status == ApiConsumerStatus.Published && oldConsumer.status == ApiConsumerStatus.Deprecated) {

} else if (oldConsumer.status.orderPosition > consumer.status.orderPosition) {
return Json.obj(
if (!updateConsumerStatus(oldConsumer, consumer)) {
return Json.obj(
"error" -> s"api has rejected your demand : you can't get back to a consumer status",
"http_status_code" -> 400
).leftf
Expand All @@ -823,6 +846,20 @@ object Api {
newApi.rightf
}

def updateConsumerStatus(oldConsumer: ApiConsumer, consumer: ApiConsumer): Boolean = {
// staging -> published = ok
// published -> deprecated = ok
// deprecated -> closed = ok
// deprecated -> published = ok
if (consumer.status == ApiConsumerStatus.Published && oldConsumer.status == ApiConsumerStatus.Deprecated) {
true
} else if (oldConsumer.status.orderPosition > consumer.status.orderPosition) {
false
} else {
true
}
}

def fromJsons(value: JsValue): Api =
try {
format.reads(value).get
Expand Down
7 changes: 6 additions & 1 deletion otoroshi/conf/routes
Original file line number Diff line number Diff line change
Expand Up @@ -616,7 +616,12 @@ GET /apis/cluster/node/metrics otoroshi.api.Gener
GET /apis/cluster otoroshi.controllers.adminapi.ClusterController.getClusterMembers()

GET /apis/api.otoroshi.io/v1/apis/:id/live otoroshi.next.controllers.adminapi.ApisController.liveStats(id, every: Option[Int])
GET /apis/apis.otoroshi.io/v1/apiconsumersubscriptions/:id/_validate otoroshi.next.controllers.adminapi.ApisController.liveStats(id, every: Option[Int])
GET /apis/api.otoroshi.io/v1/apis/:id/_start otoroshi.next.controllers.adminapi.ApisController.start(id)
GET /apis/api.otoroshi.io/v1/apis/:id/_stop otoroshi.next.controllers.adminapi.ApisController.stop(id)

GET /apis/api.otoroshi.io/v1/apis/:apiId/consumers/:consumerId/_publish otoroshi.next.controllers.adminapi.ApisController.publishConsumer(apiId, consumerId)
GET /apis/api.otoroshi.io/v1/apis/:apiId/consumers/:consumerId/_deprecate otoroshi.next.controllers.adminapi.ApisController.deprecateConsumer(apiId, consumerId)
GET /apis/api.otoroshi.io/v1/apis/:apiId/consumers/:consumerId/_close otoroshi.next.controllers.adminapi.ApisController.closeConsumer(apiId, consumerId)

# New admin API - generic apis
POST /apis/:group/:version/:entity/_bulk otoroshi.api.GenericApiController.bulkCreate(group, version, entity)
Expand Down
1 change: 1 addition & 0 deletions otoroshi/javascript/src/components/nginputs/inputs.js
Original file line number Diff line number Diff line change
Expand Up @@ -704,6 +704,7 @@ export class NgArrayRenderer extends Component {

render() {
const schema = this.props.schema;
console.log(schema)
const props = schema.props || {};
const readOnly = this.props.readOnly;
const ItemRenderer = schema.itemRenderer || this.props.rawSchema.itemRenderer;
Expand Down
7 changes: 7 additions & 0 deletions otoroshi/javascript/src/pages/ApiEditor/Sidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ const LINKS = (id) =>
tab: 'Consumers',
tooltip: { ...createTooltip(`Show consumers tab`) },
},
{
to: `/apis/${id}/subscriptions`,
icon: 'fa-key',
title: 'Subscriptions',
tab: 'Subscriptions',
tooltip: { ...createTooltip(`Show subscriptions tab`) },
},
{
to: `/apis/${id}/playground`,
icon: 'fa-play',
Expand Down
7 changes: 4 additions & 3 deletions otoroshi/javascript/src/pages/ApiEditor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ function Subscriptions(props) {
})
})

const deleteItem = item => client.delete(item.id)
const deleteItem = item => client.delete(item)
.then(() => window.location.reload())

const fields = []
Expand Down Expand Up @@ -195,8 +195,9 @@ function SubscriptionDesigner(props) {
}
},
token_refs: {
type: 'array',
label: 'Token refs'
array: true,
label: 'Token refs',
type: 'string'
}
}

Expand Down
10 changes: 8 additions & 2 deletions otoroshi/test/Suites.scala
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@ object OtoroshiTests {
new ApikeyGroupApiSpec(name, config),
new ApikeyServiceApiSpec(name, config),
new ApikeyApiSpec(name, config),
new Log4ShellSpec()
new Log4ShellSpec(),
new ApiEntityTestSpec(name, config)
)
Option(System.getenv("TEST_ANALYTICS")) match {
case Some("true") => suites :+ new AnalyticsSpec(name, config)
Expand Down Expand Up @@ -179,11 +180,16 @@ class AnalyticsTests
new AlertAndAnalyticsSpec("InMemory", Configurations.InMemoryConfiguration)
)

class GreenScoreTest
class GreenScoreTests
extends Suites(
new GreenScoreTestSpec("GreenScore", Configurations.InMemoryConfiguration)
)

class ApiEntityTests
extends Suites(
new ApiEntityTestSpec("ApiEntity", Configurations.InMemoryConfiguration)
)

//class ApiKeysTest
// extends Suites(
// new ApiKeysSpec("ApiKeysSpec", Configurations.InMemoryConfiguration)
Expand Down
Loading

0 comments on commit c2793e3

Please sign in to comment.