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)
+      }
+    }
+  }
+}