blob: f61aacc6a2df318c9034cf9ae0c9bcf0def2fb32 [file] [log] [blame]
/*
* 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.database
import spray.json._
import spray.json.DefaultJsonProtocol._
import org.apache.openwhisk.common.TransactionId
import org.apache.openwhisk.core.entity.{DocId, UserLimits}
import org.apache.openwhisk.core.entity.EntityPath.PATHSEP
import org.apache.openwhisk.utils.JsHelpers
import scala.collection.immutable.Seq
import scala.concurrent.Future
import scala.concurrent.ExecutionContext
/**
* Simple abstraction allow accessing a document just by _id. This would be used
* to perform queries related to join support
*/
trait DocumentProvider {
protected[database] def get(id: DocId)(implicit transid: TransactionId): Future[Option[JsObject]]
}
trait DocumentHandler {
/**
* Returns a JsObject having computed fields. This is a substitution for fields
* computed in CouchDB views
*/
def computedFields(js: JsObject): JsObject = JsObject.empty
/**
* Returns the set of field names (including sub document field) which needs to be fetched as part of
* query made for the given view.
*/
def fieldsRequiredForView(ddoc: String, view: String): Set[String] = Set.empty
/**
* Transforms the query result instance from artifact store as per view requirements. Some view computation
* may result in performing a join operation.
*
* If the passed instance does not confirm to view conditions that transformed result would be None. This
* would be the case if view condition cannot be completed handled in query made to artifact store
*/
def transformViewResult(
ddoc: String,
view: String,
startKey: List[Any],
endKey: List[Any],
includeDocs: Boolean,
js: JsObject,
provider: DocumentProvider)(implicit transid: TransactionId, ec: ExecutionContext): Future[Seq[JsObject]]
/**
* Determines if the complete document should be fetched even if `includeDocs` is set to true. For some view computation
* complete document (including sub documents) may be needed and for them its required that complete document must be
* fetched as part of query response
*/
def shouldAlwaysIncludeDocs(ddoc: String, view: String): Boolean = false
def checkIfTableSupported(table: String): Unit = {
if (!supportedTables.contains(table)) {
throw UnsupportedView(table)
}
}
protected def supportedTables: Set[String]
}
/**
* Base class for handlers which do not perform joins for computing views
*/
abstract class SimpleHandler extends DocumentHandler {
override def transformViewResult(
ddoc: String,
view: String,
startKey: List[Any],
endKey: List[Any],
includeDocs: Boolean,
js: JsObject,
provider: DocumentProvider)(implicit transid: TransactionId, ec: ExecutionContext): Future[Seq[JsObject]] = {
//Query result from CouchDB have below object structure with actual result in `value` key
//So transform the result to confirm to that structure
val viewResult = JsObject(
"id" -> js.fields("_id"),
"key" -> createKey(ddoc, view, startKey, js),
"value" -> computeView(ddoc, view, js))
val result = if (includeDocs) JsObject(viewResult.fields + ("doc" -> js)) else viewResult
Future.successful(Seq(result))
}
/**
* Computes the view as per viewName. Its passed either the projected object or actual
* object
*/
def computeView(ddoc: String, view: String, js: JsObject): JsObject
/**
* Key is an array which matches the view query key
*/
protected def createKey(ddoc: String, view: String, startKey: List[Any], js: JsObject): JsArray
}
object ActivationHandler extends SimpleHandler {
val NS_PATH = "nspath"
private val commonFields =
Set("namespace", "name", "version", "publish", "annotations", "activationId", "start", "cause")
private val fieldsForView = commonFields ++ Seq("end", "response.statusCode")
protected val supportedTables =
Set("activations/byDate", "whisks-filters.v2.1.1/activations", "whisks.v2.1.0/activations")
override def computedFields(js: JsObject): JsObject = {
val path = js.fields.get("namespace") match {
case Some(JsString(namespace)) => JsString(namespace + PATHSEP + pathFilter(js))
case _ => JsNull
}
val deleteLogs = annotationValue(js, "kind", { v =>
v.convertTo[String] != "sequence"
}, true)
dropNull((NS_PATH, path), ("deleteLogs", JsBoolean(deleteLogs)))
}
override def fieldsRequiredForView(ddoc: String, view: String): Set[String] = view match {
case "activations" => fieldsForView
case _ => throw UnsupportedView(s"$ddoc/$view")
}
def computeView(ddoc: String, view: String, js: JsObject): JsObject = view match {
case "activations" => computeActivationView(js)
case _ => throw UnsupportedView(s"$ddoc/$view")
}
def createKey(ddoc: String, view: String, startKey: List[Any], js: JsObject): JsArray = {
startKey match {
case (ns: String) :: Nil => JsArray(Vector(JsString(ns)))
case (ns: String) :: _ :: Nil => JsArray(Vector(JsString(ns), js.fields("start")))
case _ => throw UnsupportedQueryKeys("$ddoc/$view -> ($startKey, $endKey)")
}
}
private def computeActivationView(js: JsObject): JsObject = {
val common = js.fields.filterKeys(commonFields).toMap
val (endTime, duration) = js.getFields("end", "start") match {
case Seq(JsNumber(end), JsNumber(start)) if end != 0 => (JsNumber(end), JsNumber(end - start))
case _ => (JsNull, JsNull)
}
val statusCode = JsHelpers.getFieldPath(js, "response", "statusCode").getOrElse(JsNull)
val result = common + ("end" -> endTime) + ("duration" -> duration) + ("statusCode" -> statusCode)
JsObject(result.filter(_._2 != JsNull))
}
protected[database] def pathFilter(js: JsObject): String = {
val name = js.fields("name").convertTo[String]
annotationValue(js, "path", { v =>
val p = v.convertTo[String].split(PATHSEP)
if (p.length == 3) p(1) + PATHSEP + name else name
}, name)
}
/**
* Finds and transforms annotation with matching key.
*
* @param js js object having annotations array
* @param key annotation key
* @param vtr transformer function to map annotation value
* @param default default value to use if no matching annotation found
* @return annotation value matching given key
*/
protected[database] def annotationValue[T](js: JsObject, key: String, vtr: JsValue => T, default: T): T = {
js.fields.get("annotations") match {
case Some(JsArray(e)) =>
e.view
.map(_.asJsObject.getFields("key", "value"))
.collectFirst {
case Seq(JsString(`key`), v: JsValue) => vtr(v) //match annotation with given key
}
.getOrElse(default)
case _ => default
}
}
private def dropNull(fields: JsField*) = JsObject(fields.filter(_._2 != JsNull): _*)
}
object WhisksHandler extends SimpleHandler {
val ROOT_NS = "rootns"
private val commonFields = Set("namespace", "name", "version", "publish", "annotations", "updated")
private val actionFields = commonFields ++ Set("limits", "exec.binary")
private val packageFields = commonFields ++ Set("binding")
private val packagePublicFields = commonFields
private val ruleFields = commonFields
private val triggerFields = commonFields
protected val supportedTables = Set(
"whisks.v2.1.0/actions",
"whisks.v2.1.0/packages",
"whisks.v2.1.0/packages-public",
"whisks.v2.1.0/rules",
"whisks.v2.1.0/triggers")
override def computedFields(js: JsObject): JsObject = {
js.fields.get("namespace") match {
case Some(JsString(namespace)) =>
val ns = namespace.split(PATHSEP)
val rootNS = if (ns.length > 1) ns(0) else namespace
JsObject((ROOT_NS, JsString(rootNS)))
case _ => JsObject.empty
}
}
override def fieldsRequiredForView(ddoc: String, view: String): Set[String] = view match {
case "actions" => actionFields
case "packages" => packageFields
case "packages-public" => packagePublicFields
case "rules" => ruleFields
case "triggers" => triggerFields
case _ => throw UnsupportedView(s"$ddoc/$view")
}
def computeView(ddoc: String, view: String, js: JsObject): JsObject = view match {
case "actions" => computeActionView(js)
case "packages" => computePackageView(js)
case "packages-public" => computePublicPackageView(js)
case "rules" => computeRulesView(js)
case "triggers" => computeTriggersView(js)
case _ => throw UnsupportedView(s"$ddoc/$view")
}
def createKey(ddoc: String, view: String, startKey: List[Any], js: JsObject): JsArray = {
startKey match {
case (ns: String) :: Nil => JsArray(Vector(JsString(ns)))
case (ns: String) :: _ :: Nil => JsArray(Vector(JsString(ns), js.fields("updated")))
case _ => throw UnsupportedQueryKeys("$ddoc/$view -> ($startKey, $endKey)")
}
}
def getEntityTypeForDesignDoc(ddoc: String, view: String): String = view match {
case "actions" => "action"
case "rules" => "rule"
case "triggers" => "trigger"
case "packages" | "packages-public" => "package"
case _ => throw UnsupportedView(s"$ddoc/$view")
}
private def computeTriggersView(js: JsObject): JsObject = {
JsObject(js.fields.filterKeys(commonFields).toMap)
}
private def computePublicPackageView(js: JsObject): JsObject = {
JsObject(js.fields.filterKeys(commonFields).toMap + ("binding" -> JsFalse))
}
private def computeRulesView(js: JsObject) = {
JsObject(js.fields.filterKeys(ruleFields).toMap)
}
private def computePackageView(js: JsObject): JsObject = {
val common = js.fields.filterKeys(commonFields).toMap
val binding = js.fields.get("binding") match {
case Some(x: JsObject) if x.fields.nonEmpty => x
case _ => JsFalse
}
JsObject(common + ("binding" -> binding))
}
private def computeActionView(js: JsObject): JsObject = {
val base = js.fields.filterKeys(commonFields ++ Set("limits")).toMap
val exec_binary = JsHelpers.getFieldPath(js, "exec", "binary")
JsObject(base + ("exec" -> JsObject("binary" -> exec_binary.getOrElse(JsFalse))))
}
}
object SubjectHandler extends DocumentHandler {
protected val supportedTables =
Set("subjects/identities", "subjects.v2.0.0/identities", "namespaceThrottlings/blockedNamespaces")
override def shouldAlwaysIncludeDocs(ddoc: String, view: String): Boolean = {
(ddoc, view) match {
case (s, "identities") if s.startsWith("subjects") => true
case ("namespaceThrottlings", "blockedNamespaces") => true
case _ => throw UnsupportedView(s"$ddoc/$view")
}
}
override def transformViewResult(
ddoc: String,
view: String,
startKey: List[Any],
endKey: List[Any],
includeDocs: Boolean,
js: JsObject,
provider: DocumentProvider)(implicit transid: TransactionId, ec: ExecutionContext): Future[Seq[JsObject]] = {
val result = (ddoc, view) match {
case (s, "identities") if s.startsWith("subjects") =>
require(includeDocs) //For subject/identities includeDocs is always true
computeSubjectView(startKey, js, provider)
case ("namespaceThrottlings", "blockedNamespaces") =>
Future.successful(computeBlacklistedNamespaces(js))
case _ =>
throw UnsupportedView(s"$ddoc/$view")
}
result
}
/**
* {{{
* function (doc) {
* if (doc._id.indexOf("/limits") >= 0) {
* if (doc.concurrentInvocations === 0 || doc.invocationsPerMinute === 0) {
* var namespace = doc._id.replace("/limits", "");
* emit(namespace, 1);
* }
* } else if (doc.subject && doc.namespaces && doc.blocked) {
* doc.namespaces.forEach(function(namespace) {
* emit(namespace.name, 1);
* });
* }
* }
* }}}
*/
private def computeBlacklistedNamespaces(js: JsObject): Seq[JsObject] = {
val id = js.fields("_id")
val value = JsNumber(1)
id match {
case JsString(idv) if idv.endsWith("/limits") =>
val limits = UserLimits.serdes.read(js)
if (limits.concurrentInvocations.contains(0) || limits.invocationsPerMinute.contains(0)) {
val ns = idv.substring(0, idv.indexOf("/limits"))
Seq(JsObject("id" -> id, "key" -> JsString(ns), "value" -> value))
} else Seq.empty
case _ =>
js.getFields("subject", "namespaces", "blocked") match {
case Seq(_, namespaces: JsArray, JsTrue) =>
namespaces.elements.map { ns =>
val name = ns.asJsObject.fields("name")
JsObject("id" -> id, "key" -> name, "value" -> value)
}
case _ =>
Seq.empty
}
}
}
private def computeSubjectView(startKey: List[Any], js: JsObject, provider: DocumentProvider)(
implicit transid: TransactionId,
ec: ExecutionContext) = {
val subjectOpt = findMatchingSubject(startKey, js)
val result = subjectOpt match {
case Some(subject) =>
val limitDocId = s"${subject.namespace}/limits"
val viewJS = JsObject(
"_id" -> JsString(limitDocId),
"namespace" -> JsString(subject.namespace),
"uuid" -> JsString(subject.uuid),
"key" -> JsString(subject.key))
val result =
JsObject("id" -> js.fields("_id"), "key" -> createKey(startKey), "value" -> viewJS, "doc" -> JsNull)
if (subject.matchInNamespace) {
provider
.get(DocId(limitDocId))
.map(limits => Seq(JsObject(result.fields + ("doc" -> limits.getOrElse(JsNull)))))
} else {
Future.successful(Seq(result))
}
case None =>
Future.successful(Seq.empty)
}
result
}
def findMatchingSubject(startKey: List[Any], js: JsObject): Option[SubjectView] = {
startKey match {
case (ns: String) :: Nil => findMatchingSubject(js, s => s.namespace == ns && !s.blocked)
case (uuid: String) :: (key: String) :: Nil =>
findMatchingSubject(js, s => s.uuid == uuid && s.key == key && !s.blocked)
case _ => None
}
}
private def createKey(startKey: List[Any]): JsArray = {
startKey match {
case (ns: String) :: Nil => JsArray(Vector(JsString(ns))) //namespace or subject
case (uuid: String) :: (key: String) :: Nil => JsArray(Vector(JsString(uuid), JsString(key))) // uuid, key
case _ => throw UnsupportedQueryKeys("$ddoc/$view -> ($startKey, $endKey)")
}
}
/**
* Computes the view as per logic below from (identities/subject) view
*
* {{{
* function (doc) {
* if(doc.uuid && doc.key && !doc.blocked) {
* var v = {namespace: doc.subject, uuid: doc.uuid, key: doc.key};
* emit([doc.subject], v);
* emit([doc.uuid, doc.key], v);
* }
* if(doc.namespaces && !doc.blocked) {
* doc.namespaces.forEach(function(namespace) {
* var v = {_id: namespace.name + '/limits', namespace: namespace.name, uuid: namespace.uuid, key: namespace.key};
* emit([namespace.name], v);
* emit([namespace.uuid, namespace.key], v);
* });
* }
* }
* }}}
*
* @param js subject json from db
* @param matches match predicate
*/
private def findMatchingSubject(js: JsObject, matches: SubjectView => Boolean): Option[SubjectView] = {
val blocked = js.fields.get("blocked") match {
case Some(JsTrue) => true
case _ => false
}
val r = js.getFields("subject", "uuid", "key") match {
case Seq(JsString(ns), JsString(uuid), JsString(key)) => Some(SubjectView(ns, uuid, key, blocked)).filter(matches)
case _ => None
}
r.orElse {
val namespaces = js.fields.get("namespaces") match {
case Some(JsArray(e)) =>
e.map(_.asJsObject.getFields("name", "uuid", "key") match {
case Seq(JsString(ns), JsString(uuid), JsString(key)) =>
Some(SubjectView(ns, uuid, key, blocked, matchInNamespace = true))
case _ => None
})
case _ => Seq.empty
}
namespaces.flatMap(_.filter(matches)).headOption
}
}
case class SubjectView(namespace: String,
uuid: String,
key: String,
blocked: Boolean = false,
matchInNamespace: Boolean = false)
}