You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@openwhisk.apache.org by ra...@apache.org on 2020/05/12 02:53:12 UTC
[openwhisk] branch master updated: Add Get Namespace Limits API
(#4899)
This is an automated email from the ASF dual-hosted git repository.
rabbah pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/openwhisk.git
The following commit(s) were added to refs/heads/master by this push:
new a4122fd Add Get Namespace Limits API (#4899)
a4122fd is described below
commit a4122fdd58a2c0ce6eab09fdc3a8678a75af83b2
Author: Brendan Doyle <bj...@georgetown.edu>
AuthorDate: Mon May 11 19:53:01 2020 -0700
Add Get Namespace Limits API (#4899)
---
.../src/main/resources/apiv1swagger.json | 66 ++++++++++++
.../apache/openwhisk/core/controller/Limits.scala | 68 ++++++++++++
.../openwhisk/core/controller/RestAPIs.scala | 10 +-
.../openwhisk/core/entitlement/Collection.scala | 8 ++
docs/rest_api.md | 9 ++
.../core/controller/test/LimitsApiTests.scala | 117 +++++++++++++++++++++
6 files changed, 277 insertions(+), 1 deletion(-)
diff --git a/core/controller/src/main/resources/apiv1swagger.json b/core/controller/src/main/resources/apiv1swagger.json
index dd92815..9255234 100644
--- a/core/controller/src/main/resources/apiv1swagger.json
+++ b/core/controller/src/main/resources/apiv1swagger.json
@@ -37,6 +37,9 @@
},
{
"name": "Namespaces"
+ },
+ {
+ "name": "Limits"
}
],
"paths": {
@@ -1676,6 +1679,42 @@
}
}
}
+ },
+ "/namespaces/{namespace}/limits": {
+ "get": {
+ "tags": [
+ "Limits"
+ ],
+ "summary": "Get the limits for a namespace",
+ "description": "Get limits.",
+ "operationId": "getLimits",
+ "parameters": [
+ {
+ "name": "namespace",
+ "in": "path",
+ "description": "The entity namespace",
+ "required": true,
+ "type": "string"
+ }
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "responses": {
+ "200": {
+ "description": "Return output",
+ "schema": {
+ "$ref": "#/definitions/UserLimits"
+ }
+ },
+ "401": {
+ "$ref": "#/responses/UnauthorizedRequest"
+ },
+ "500": {
+ "$ref": "#/responses/ServerError"
+ }
+ }
+ }
}
},
"definitions": {
@@ -2613,6 +2652,33 @@
"description": "parameter bindings included in the context passed to the provider"
}
}
+ },
+ "UserLimits": {
+ "properties": {
+ "invocationsPerMinute": {
+ "type": "integer",
+ "description": "Max allowed invocations per minute for namespace"
+ },
+ "concurrentInvocations": {
+ "type": "integer",
+ "description": "Max allowed concurrent in flight invocations for namespace"
+ },
+ "firesPerMinute": {
+ "type": "integer",
+ "description": "Max allowed trigger fires per minute for namespace"
+ },
+ "allowedKinds": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "List of runtimes whitelisted to be used by namespace (all if none returned)"
+ },
+ "storeActivations": {
+ "type": "boolean",
+ "description": "Whether storing activation is turned on for namespace (default is true)"
+ }
+ }
}
},
"responses": {
diff --git a/core/controller/src/main/scala/org/apache/openwhisk/core/controller/Limits.scala b/core/controller/src/main/scala/org/apache/openwhisk/core/controller/Limits.scala
new file mode 100644
index 0000000..1212090
--- /dev/null
+++ b/core/controller/src/main/scala/org/apache/openwhisk/core/controller/Limits.scala
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.openwhisk.core.controller
+
+import akka.http.scaladsl.model.StatusCodes._
+import akka.http.scaladsl.server.{Directive1, Directives}
+import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport.sprayJsonMarshaller
+import org.apache.openwhisk.common.TransactionId
+import org.apache.openwhisk.core.WhiskConfig
+import org.apache.openwhisk.core.entitlement.{Collection, Privilege, Resource}
+import org.apache.openwhisk.core.entitlement.Privilege.READ
+import org.apache.openwhisk.core.entity.Identity
+
+trait WhiskLimitsApi extends Directives with AuthenticatedRouteProvider with AuthorizedRouteProvider {
+
+ protected val whiskConfig: WhiskConfig
+
+ protected override val collection = Collection(Collection.LIMITS)
+
+ protected val invocationsPerMinuteSystemDefault = whiskConfig.actionInvokePerMinuteLimit.toInt
+ protected val concurrentInvocationsSystemDefault = whiskConfig.actionInvokeConcurrentLimit.toInt
+ protected val firePerMinuteSystemDefault = whiskConfig.triggerFirePerMinuteLimit.toInt
+
+ override protected lazy val entityOps = get
+
+ /** JSON response formatter. */
+ import RestApiCommons.jsonDefaultResponsePrinter
+
+ /** Dispatches resource to the proper handler depending on context. */
+ protected override def dispatchOp(user: Identity, op: Privilege, resource: Resource)(
+ implicit transid: TransactionId) = {
+
+ resource.entity match {
+ case Some(_) =>
+ //TODO: Process entity level requests for an individual limit here
+ reject //should never get here
+ case None =>
+ op match {
+ case READ =>
+ val limits = user.limits.copy(
+ Some(user.limits.invocationsPerMinute.getOrElse(invocationsPerMinuteSystemDefault)),
+ Some(user.limits.concurrentInvocations.getOrElse(concurrentInvocationsSystemDefault)),
+ Some(user.limits.firesPerMinute.getOrElse(firePerMinuteSystemDefault)))
+ pathEndOrSingleSlash { complete(OK, limits) }
+ case _ => reject //should never get here
+ }
+ }
+ }
+
+ protected override def entityname(n: String): Directive1[String] = {
+ validate(false, "Inner entity level routes for limits are not yet implemented.") & extract(_ => n)
+ }
+}
diff --git a/core/controller/src/main/scala/org/apache/openwhisk/core/controller/RestAPIs.scala b/core/controller/src/main/scala/org/apache/openwhisk/core/controller/RestAPIs.scala
index cf7f81a..2dd35f0 100644
--- a/core/controller/src/main/scala/org/apache/openwhisk/core/controller/RestAPIs.scala
+++ b/core/controller/src/main/scala/org/apache/openwhisk/core/controller/RestAPIs.scala
@@ -206,7 +206,8 @@ class RestAPIVersion(config: WhiskConfig, apiPath: String, apiVersion: String)(
triggers.routes(user) ~
rules.routes(user) ~
activations.routes(user) ~
- packages.routes(user)
+ packages.routes(user) ~
+ limits.routes(user)
}
} ~
swaggerRoutes
@@ -233,10 +234,17 @@ class RestAPIVersion(config: WhiskConfig, apiPath: String, apiVersion: String)(
private val triggers = new TriggersApi(apiPath, apiVersion)
private val activations = new ActivationsApi(apiPath, apiVersion)
private val rules = new RulesApi(apiPath, apiVersion)
+ private val limits = new LimitsApi(apiPath, apiVersion)
private val web = new WebActionsApi(Seq("web"), new WebApiDirectives())
class NamespacesApi(val apiPath: String, val apiVersion: String) extends WhiskNamespacesApi
+ class LimitsApi(val apiPath: String, val apiVersion: String)(
+ implicit override val entitlementProvider: EntitlementProvider,
+ override val executionContext: ExecutionContext,
+ override val whiskConfig: WhiskConfig)
+ extends WhiskLimitsApi
+
class ActionsApi(val apiPath: String, val apiVersion: String)(
implicit override val actorSystem: ActorSystem,
override val activeAckTopicIndex: ControllerInstanceId,
diff --git a/core/controller/src/main/scala/org/apache/openwhisk/core/entitlement/Collection.scala b/core/controller/src/main/scala/org/apache/openwhisk/core/entitlement/Collection.scala
index de2e7a1..9c651cb 100644
--- a/core/controller/src/main/scala/org/apache/openwhisk/core/entitlement/Collection.scala
+++ b/core/controller/src/main/scala/org/apache/openwhisk/core/entitlement/Collection.scala
@@ -123,6 +123,7 @@ protected[core] object Collection {
protected[core] val PACKAGES = WhiskPackage.collectionName
protected[core] val ACTIVATIONS = WhiskActivation.collectionName
protected[core] val NAMESPACES = "namespaces"
+ protected[core] val LIMITS = "limits"
private val collections = scala.collection.mutable.Map[String, Collection]()
private def register(c: Collection) = collections += c.path -> c
@@ -156,5 +157,12 @@ protected[core] object Collection {
protected override val allowedEntityRights: Set[Privilege] = Set(Privilege.READ)
})
+
+ register(new Collection(LIMITS) {
+ protected[core] override def determineRight(op: HttpMethod,
+ resource: Option[String])(implicit transid: TransactionId) = {
+ if (op == GET) Privilege.READ else Privilege.REJECT
+ }
+ })
}
}
diff --git a/docs/rest_api.md b/docs/rest_api.md
index 2686bc7..f7e7f4b 100644
--- a/docs/rest_api.md
+++ b/docs/rest_api.md
@@ -33,6 +33,7 @@ These are the collection endpoints:
- `https://$APIHOST/api/v1/namespaces/{namespace}/rules`
- `https://$APIHOST/api/v1/namespaces/{namespace}/packages`
- `https://$APIHOST/api/v1/namespaces/{namespace}/activations`
+- `https://$APIHOST/api/v1/namespaces/{namespace}/limits`
The `$APIHOST` is the OpenWhisk API hostname (for example, localhost, 172.17.0.1, and so on).
For the `{namespace}`, the character `_` can be used to specify the user's *default
@@ -326,3 +327,11 @@ To get all the details of an activation including results and logs, send a HTTP
```bash
curl -u $AUTH https://$APIHOST/api/v1/namespaces/_/activations/f81dfddd7156401a8a6497f2724fec7b
```
+
+## Limits
+
+To get the limits set for a namespace (i.e. invocationsPerMinute, concurrentInvocations, firesPerMinute)
+```bash
+curl -u $AUTH https://$APIHOST/api/v1/namespaces/_/limits
+```
+Note that the default system values are returned if no specific limits are set for the user corresponding to the authenticated identity.
diff --git a/tests/src/test/scala/org/apache/openwhisk/core/controller/test/LimitsApiTests.scala b/tests/src/test/scala/org/apache/openwhisk/core/controller/test/LimitsApiTests.scala
new file mode 100644
index 0000000..b6f2eb9
--- /dev/null
+++ b/tests/src/test/scala/org/apache/openwhisk/core/controller/test/LimitsApiTests.scala
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.openwhisk.core.controller.test
+
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+import akka.http.scaladsl.model.StatusCodes.{BadRequest, MethodNotAllowed, OK}
+import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport.sprayJsonUnmarshaller
+import akka.http.scaladsl.server.Route
+import org.apache.openwhisk.core.controller.WhiskLimitsApi
+import org.apache.openwhisk.core.entity.{EntityPath, UserLimits}
+
+/**
+ * Tests Packages API.
+ *
+ * Unit tests of the controller service as a standalone component.
+ * These tests exercise a fresh instance of the service object in memory -- these
+ * tests do NOT communication with a whisk deployment.
+ *
+ * @Idioglossia
+ * "using Specification DSL to write unit tests, as in should, must, not, be"
+ * "using Specs2RouteTest DSL to chain HTTP requests for unit testing, as in ~>"
+ */
+@RunWith(classOf[JUnitRunner])
+class LimitsApiTests extends ControllerTestCommon with WhiskLimitsApi {
+
+ /** Limits API tests */
+ behavior of "Limits API"
+
+ // test namespace limit configurations
+ val testInvokesPerMinute = 100
+ val testConcurrent = 200
+ val testFiresPerMinute = 300
+ val testAllowedKinds = Set("java:8")
+ val testStoreActivations = false
+
+ val creds = WhiskAuthHelpers.newIdentity()
+ val credsWithSetLimits = WhiskAuthHelpers
+ .newIdentity()
+ .copy(
+ limits = UserLimits(
+ Some(testInvokesPerMinute),
+ Some(testConcurrent),
+ Some(testFiresPerMinute),
+ Some(testAllowedKinds),
+ Some(testStoreActivations)))
+ val namespace = EntityPath(creds.subject.asString)
+ val collectionPath = s"/${EntityPath.DEFAULT}/${collection.path}"
+
+ //// GET /limits
+ it should "list default system limits if no namespace limits are set" in {
+ implicit val tid = transid()
+ Seq("", "/").foreach { p =>
+ Get(collectionPath + p) ~> Route.seal(routes(creds)) ~> check {
+ status should be(OK)
+ responseAs[UserLimits].invocationsPerMinute shouldBe Some(whiskConfig.actionInvokePerMinuteLimit.toInt)
+ responseAs[UserLimits].concurrentInvocations shouldBe Some(whiskConfig.actionInvokeConcurrentLimit.toInt)
+ responseAs[UserLimits].firesPerMinute shouldBe Some(whiskConfig.triggerFirePerMinuteLimit.toInt)
+ responseAs[UserLimits].allowedKinds shouldBe None
+ responseAs[UserLimits].storeActivations shouldBe None
+ }
+ }
+ }
+
+ it should "list set limits if limits have been set for the namespace" in {
+ implicit val tid = transid()
+ Seq("", "/").foreach { p =>
+ Get(collectionPath + p) ~> Route.seal(routes(credsWithSetLimits)) ~> check {
+ status should be(OK)
+ responseAs[UserLimits].invocationsPerMinute shouldBe Some(testInvokesPerMinute)
+ responseAs[UserLimits].concurrentInvocations shouldBe Some(testConcurrent)
+ responseAs[UserLimits].firesPerMinute shouldBe Some(testFiresPerMinute)
+ responseAs[UserLimits].allowedKinds shouldBe Some(testAllowedKinds)
+ responseAs[UserLimits].storeActivations shouldBe Some(testStoreActivations)
+ }
+ }
+ }
+
+ it should "reject requests for unsupported methods" in {
+ implicit val tid = transid()
+ Seq(Put, Post, Delete).foreach { m =>
+ m(collectionPath) ~> Route.seal(routes(creds)) ~> check {
+ status should be(MethodNotAllowed)
+ }
+ }
+ }
+
+ it should "reject all methods for entity level request" in {
+ implicit val tid = transid()
+ Seq(Put, Post, Delete).foreach { m =>
+ m(s"$collectionPath/limitsEntity") ~> Route.seal(routes(creds)) ~> check {
+ status should be(MethodNotAllowed)
+ }
+ }
+
+ Seq(Get).foreach { m =>
+ m(s"$collectionPath/limitsEntity") ~> Route.seal(routes(creds)) ~> check {
+ status should be(BadRequest)
+ }
+ }
+ }
+}