Skip to content

Commit a4122fd

Browse files
author
Brendan Doyle
authored
Add Get Namespace Limits API (#4899)
1 parent 1c6852f commit a4122fd

File tree

6 files changed

+277
-1
lines changed

6 files changed

+277
-1
lines changed

core/controller/src/main/resources/apiv1swagger.json

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@
3737
},
3838
{
3939
"name": "Namespaces"
40+
},
41+
{
42+
"name": "Limits"
4043
}
4144
],
4245
"paths": {
@@ -1676,6 +1679,42 @@
16761679
}
16771680
}
16781681
}
1682+
},
1683+
"/namespaces/{namespace}/limits": {
1684+
"get": {
1685+
"tags": [
1686+
"Limits"
1687+
],
1688+
"summary": "Get the limits for a namespace",
1689+
"description": "Get limits.",
1690+
"operationId": "getLimits",
1691+
"parameters": [
1692+
{
1693+
"name": "namespace",
1694+
"in": "path",
1695+
"description": "The entity namespace",
1696+
"required": true,
1697+
"type": "string"
1698+
}
1699+
],
1700+
"produces": [
1701+
"application/json"
1702+
],
1703+
"responses": {
1704+
"200": {
1705+
"description": "Return output",
1706+
"schema": {
1707+
"$ref": "#/definitions/UserLimits"
1708+
}
1709+
},
1710+
"401": {
1711+
"$ref": "#/responses/UnauthorizedRequest"
1712+
},
1713+
"500": {
1714+
"$ref": "#/responses/ServerError"
1715+
}
1716+
}
1717+
}
16791718
}
16801719
},
16811720
"definitions": {
@@ -2613,6 +2652,33 @@
26132652
"description": "parameter bindings included in the context passed to the provider"
26142653
}
26152654
}
2655+
},
2656+
"UserLimits": {
2657+
"properties": {
2658+
"invocationsPerMinute": {
2659+
"type": "integer",
2660+
"description": "Max allowed invocations per minute for namespace"
2661+
},
2662+
"concurrentInvocations": {
2663+
"type": "integer",
2664+
"description": "Max allowed concurrent in flight invocations for namespace"
2665+
},
2666+
"firesPerMinute": {
2667+
"type": "integer",
2668+
"description": "Max allowed trigger fires per minute for namespace"
2669+
},
2670+
"allowedKinds": {
2671+
"type": "array",
2672+
"items": {
2673+
"type": "string"
2674+
},
2675+
"description": "List of runtimes whitelisted to be used by namespace (all if none returned)"
2676+
},
2677+
"storeActivations": {
2678+
"type": "boolean",
2679+
"description": "Whether storing activation is turned on for namespace (default is true)"
2680+
}
2681+
}
26162682
}
26172683
},
26182684
"responses": {
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.openwhisk.core.controller
19+
20+
import akka.http.scaladsl.model.StatusCodes._
21+
import akka.http.scaladsl.server.{Directive1, Directives}
22+
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport.sprayJsonMarshaller
23+
import org.apache.openwhisk.common.TransactionId
24+
import org.apache.openwhisk.core.WhiskConfig
25+
import org.apache.openwhisk.core.entitlement.{Collection, Privilege, Resource}
26+
import org.apache.openwhisk.core.entitlement.Privilege.READ
27+
import org.apache.openwhisk.core.entity.Identity
28+
29+
trait WhiskLimitsApi extends Directives with AuthenticatedRouteProvider with AuthorizedRouteProvider {
30+
31+
protected val whiskConfig: WhiskConfig
32+
33+
protected override val collection = Collection(Collection.LIMITS)
34+
35+
protected val invocationsPerMinuteSystemDefault = whiskConfig.actionInvokePerMinuteLimit.toInt
36+
protected val concurrentInvocationsSystemDefault = whiskConfig.actionInvokeConcurrentLimit.toInt
37+
protected val firePerMinuteSystemDefault = whiskConfig.triggerFirePerMinuteLimit.toInt
38+
39+
override protected lazy val entityOps = get
40+
41+
/** JSON response formatter. */
42+
import RestApiCommons.jsonDefaultResponsePrinter
43+
44+
/** Dispatches resource to the proper handler depending on context. */
45+
protected override def dispatchOp(user: Identity, op: Privilege, resource: Resource)(
46+
implicit transid: TransactionId) = {
47+
48+
resource.entity match {
49+
case Some(_) =>
50+
//TODO: Process entity level requests for an individual limit here
51+
reject //should never get here
52+
case None =>
53+
op match {
54+
case READ =>
55+
val limits = user.limits.copy(
56+
Some(user.limits.invocationsPerMinute.getOrElse(invocationsPerMinuteSystemDefault)),
57+
Some(user.limits.concurrentInvocations.getOrElse(concurrentInvocationsSystemDefault)),
58+
Some(user.limits.firesPerMinute.getOrElse(firePerMinuteSystemDefault)))
59+
pathEndOrSingleSlash { complete(OK, limits) }
60+
case _ => reject //should never get here
61+
}
62+
}
63+
}
64+
65+
protected override def entityname(n: String): Directive1[String] = {
66+
validate(false, "Inner entity level routes for limits are not yet implemented.") & extract(_ => n)
67+
}
68+
}

core/controller/src/main/scala/org/apache/openwhisk/core/controller/RestAPIs.scala

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,8 @@ class RestAPIVersion(config: WhiskConfig, apiPath: String, apiVersion: String)(
206206
triggers.routes(user) ~
207207
rules.routes(user) ~
208208
activations.routes(user) ~
209-
packages.routes(user)
209+
packages.routes(user) ~
210+
limits.routes(user)
210211
}
211212
} ~
212213
swaggerRoutes
@@ -233,10 +234,17 @@ class RestAPIVersion(config: WhiskConfig, apiPath: String, apiVersion: String)(
233234
private val triggers = new TriggersApi(apiPath, apiVersion)
234235
private val activations = new ActivationsApi(apiPath, apiVersion)
235236
private val rules = new RulesApi(apiPath, apiVersion)
237+
private val limits = new LimitsApi(apiPath, apiVersion)
236238
private val web = new WebActionsApi(Seq("web"), new WebApiDirectives())
237239

238240
class NamespacesApi(val apiPath: String, val apiVersion: String) extends WhiskNamespacesApi
239241

242+
class LimitsApi(val apiPath: String, val apiVersion: String)(
243+
implicit override val entitlementProvider: EntitlementProvider,
244+
override val executionContext: ExecutionContext,
245+
override val whiskConfig: WhiskConfig)
246+
extends WhiskLimitsApi
247+
240248
class ActionsApi(val apiPath: String, val apiVersion: String)(
241249
implicit override val actorSystem: ActorSystem,
242250
override val activeAckTopicIndex: ControllerInstanceId,

core/controller/src/main/scala/org/apache/openwhisk/core/entitlement/Collection.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ protected[core] object Collection {
123123
protected[core] val PACKAGES = WhiskPackage.collectionName
124124
protected[core] val ACTIVATIONS = WhiskActivation.collectionName
125125
protected[core] val NAMESPACES = "namespaces"
126+
protected[core] val LIMITS = "limits"
126127

127128
private val collections = scala.collection.mutable.Map[String, Collection]()
128129
private def register(c: Collection) = collections += c.path -> c
@@ -156,5 +157,12 @@ protected[core] object Collection {
156157

157158
protected override val allowedEntityRights: Set[Privilege] = Set(Privilege.READ)
158159
})
160+
161+
register(new Collection(LIMITS) {
162+
protected[core] override def determineRight(op: HttpMethod,
163+
resource: Option[String])(implicit transid: TransactionId) = {
164+
if (op == GET) Privilege.READ else Privilege.REJECT
165+
}
166+
})
159167
}
160168
}

docs/rest_api.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ These are the collection endpoints:
3333
- `https://$APIHOST/api/v1/namespaces/{namespace}/rules`
3434
- `https://$APIHOST/api/v1/namespaces/{namespace}/packages`
3535
- `https://$APIHOST/api/v1/namespaces/{namespace}/activations`
36+
- `https://$APIHOST/api/v1/namespaces/{namespace}/limits`
3637

3738
The `$APIHOST` is the OpenWhisk API hostname (for example, localhost, 172.17.0.1, and so on).
3839
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
326327
```bash
327328
curl -u $AUTH https://$APIHOST/api/v1/namespaces/_/activations/f81dfddd7156401a8a6497f2724fec7b
328329
```
330+
331+
## Limits
332+
333+
To get the limits set for a namespace (i.e. invocationsPerMinute, concurrentInvocations, firesPerMinute)
334+
```bash
335+
curl -u $AUTH https://$APIHOST/api/v1/namespaces/_/limits
336+
```
337+
Note that the default system values are returned if no specific limits are set for the user corresponding to the authenticated identity.
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.openwhisk.core.controller.test
19+
20+
import org.junit.runner.RunWith
21+
import org.scalatest.junit.JUnitRunner
22+
import akka.http.scaladsl.model.StatusCodes.{BadRequest, MethodNotAllowed, OK}
23+
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport.sprayJsonUnmarshaller
24+
import akka.http.scaladsl.server.Route
25+
import org.apache.openwhisk.core.controller.WhiskLimitsApi
26+
import org.apache.openwhisk.core.entity.{EntityPath, UserLimits}
27+
28+
/**
29+
* Tests Packages API.
30+
*
31+
* Unit tests of the controller service as a standalone component.
32+
* These tests exercise a fresh instance of the service object in memory -- these
33+
* tests do NOT communication with a whisk deployment.
34+
*
35+
* @Idioglossia
36+
* "using Specification DSL to write unit tests, as in should, must, not, be"
37+
* "using Specs2RouteTest DSL to chain HTTP requests for unit testing, as in ~>"
38+
*/
39+
@RunWith(classOf[JUnitRunner])
40+
class LimitsApiTests extends ControllerTestCommon with WhiskLimitsApi {
41+
42+
/** Limits API tests */
43+
behavior of "Limits API"
44+
45+
// test namespace limit configurations
46+
val testInvokesPerMinute = 100
47+
val testConcurrent = 200
48+
val testFiresPerMinute = 300
49+
val testAllowedKinds = Set("java:8")
50+
val testStoreActivations = false
51+
52+
val creds = WhiskAuthHelpers.newIdentity()
53+
val credsWithSetLimits = WhiskAuthHelpers
54+
.newIdentity()
55+
.copy(
56+
limits = UserLimits(
57+
Some(testInvokesPerMinute),
58+
Some(testConcurrent),
59+
Some(testFiresPerMinute),
60+
Some(testAllowedKinds),
61+
Some(testStoreActivations)))
62+
val namespace = EntityPath(creds.subject.asString)
63+
val collectionPath = s"/${EntityPath.DEFAULT}/${collection.path}"
64+
65+
//// GET /limits
66+
it should "list default system limits if no namespace limits are set" in {
67+
implicit val tid = transid()
68+
Seq("", "/").foreach { p =>
69+
Get(collectionPath + p) ~> Route.seal(routes(creds)) ~> check {
70+
status should be(OK)
71+
responseAs[UserLimits].invocationsPerMinute shouldBe Some(whiskConfig.actionInvokePerMinuteLimit.toInt)
72+
responseAs[UserLimits].concurrentInvocations shouldBe Some(whiskConfig.actionInvokeConcurrentLimit.toInt)
73+
responseAs[UserLimits].firesPerMinute shouldBe Some(whiskConfig.triggerFirePerMinuteLimit.toInt)
74+
responseAs[UserLimits].allowedKinds shouldBe None
75+
responseAs[UserLimits].storeActivations shouldBe None
76+
}
77+
}
78+
}
79+
80+
it should "list set limits if limits have been set for the namespace" in {
81+
implicit val tid = transid()
82+
Seq("", "/").foreach { p =>
83+
Get(collectionPath + p) ~> Route.seal(routes(credsWithSetLimits)) ~> check {
84+
status should be(OK)
85+
responseAs[UserLimits].invocationsPerMinute shouldBe Some(testInvokesPerMinute)
86+
responseAs[UserLimits].concurrentInvocations shouldBe Some(testConcurrent)
87+
responseAs[UserLimits].firesPerMinute shouldBe Some(testFiresPerMinute)
88+
responseAs[UserLimits].allowedKinds shouldBe Some(testAllowedKinds)
89+
responseAs[UserLimits].storeActivations shouldBe Some(testStoreActivations)
90+
}
91+
}
92+
}
93+
94+
it should "reject requests for unsupported methods" in {
95+
implicit val tid = transid()
96+
Seq(Put, Post, Delete).foreach { m =>
97+
m(collectionPath) ~> Route.seal(routes(creds)) ~> check {
98+
status should be(MethodNotAllowed)
99+
}
100+
}
101+
}
102+
103+
it should "reject all methods for entity level request" in {
104+
implicit val tid = transid()
105+
Seq(Put, Post, Delete).foreach { m =>
106+
m(s"$collectionPath/limitsEntity") ~> Route.seal(routes(creds)) ~> check {
107+
status should be(MethodNotAllowed)
108+
}
109+
}
110+
111+
Seq(Get).foreach { m =>
112+
m(s"$collectionPath/limitsEntity") ~> Route.seal(routes(creds)) ~> check {
113+
status should be(BadRequest)
114+
}
115+
}
116+
}
117+
}

0 commit comments

Comments
 (0)