blob: 0fd81d38ef07865268b18c56ce84d44ce1244392 [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.entity
import java.time.Clock
import java.time.Instant
import java.time.temporal.ChronoUnit
import scala.util.Try
import spray.json._
import org.apache.openwhisk.core.database.DocumentUnreadable
import org.apache.openwhisk.core.database.DocumentTypeMismatchException
import org.apache.openwhisk.http.Messages
/**
* An abstract superclass that encapsulates properties common to all whisk entities (actions, rules, triggers).
* The class has a private constructor argument and abstract fields so that case classes that extend this base
* type can use the default spray JSON ser/des. An abstract entity has the following four properties.
*
* @param en the name of the entity, this is part of the primary key for the document
* @param namespace the namespace for the entity as an abstract field
* @param version the semantic version as an abstract field
* @param publish true to share the entity and false to keep it private as an abstract field
* @param annotations the set of annotations to attribute to the entity
*
* @throws IllegalArgumentException if any argument is undefined
*/
@throws[IllegalArgumentException]
abstract class WhiskEntity protected[entity] (en: EntityName, val entityType: String) extends WhiskDocument {
val namespace: EntityPath
val name = en
val version: SemVer
val publish: Boolean
val annotations: Parameters
val updated = WhiskEntity.currentMillis()
/**
* The name of the entity qualified with its namespace and version for
* creating unique keys in backend services.
*/
final def fullyQualifiedName(withVersion: Boolean) =
FullyQualifiedEntityName(namespace, en, if (withVersion) Some(version) else None)
/** The primary key for the entity in the datastore */
override final def docid = fullyQualifiedName(false).toDocId
/**
* Returns a JSON object with the fields specific to this abstract class.
*/
protected def entityDocumentRecord: JsObject =
JsObject(
"name" -> JsString(name.toString),
"updated" -> JsNumber(updated.toEpochMilli()),
"entityType" -> JsString(entityType))
override def toDocumentRecord: JsObject = {
val extraFields = entityDocumentRecord.fields
val base = super.toDocumentRecord
// In this order to make sure the subclass can rewrite using toJson.
JsObject(extraFields ++ base.fields)
}
/**
* @return the primary key (name) of the entity as a pithy description
*/
override def toString = s"${this.getClass.getSimpleName}/${fullyQualifiedName(true)}"
/**
* A JSON view of the entity, that should match the result returned in a list operation.
* This should be synchronized with the views computed in the databse.
* Strictly used in view testing to enforce alignment.
*/
def summaryAsJson: JsObject = {
import WhiskActivation.instantSerdes
JsObject(
"namespace" -> namespace.toJson,
"name" -> name.toJson,
"version" -> version.toJson,
WhiskEntity.sharedFieldName -> JsBoolean(publish),
"annotations" -> annotations.toJsArray,
"updated" -> updated.toJson)
}
}
object WhiskEntity {
val sharedFieldName = "publish"
val paramsFieldName = "parameters"
val annotationsFieldName = "annotations"
/**
* Gets fully qualified name of an activation based on its namespace and activation id.
*/
def qualifiedName(namespace: EntityPath, activationId: ActivationId) = {
s"$namespace${EntityPath.PATHSEP}$activationId"
}
/**
* Get Instant object with a millisecond precision
* timestamp of whisk entity is stored in milliseconds in the db
*/
def currentMillis() = {
Instant.now(Clock.systemUTC()).truncatedTo(ChronoUnit.MILLIS)
}
}
object WhiskDocumentReader extends DocumentReader {
override def read[A](ma: Manifest[A], value: JsValue) = {
val doc = ma.runtimeClass match {
case x if x == classOf[WhiskAction] => WhiskAction.serdes.read(value)
case x if x == classOf[WhiskActionMetaData] => WhiskActionMetaData.serdes.read(value)
case x if x == classOf[WhiskPackage] => WhiskPackage.serdes.read(value)
case x if x == classOf[WhiskActivation] => WhiskActivation.serdes.read(value)
case x if x == classOf[WhiskTrigger] => WhiskTrigger.serdes.read(value)
case x if x == classOf[WhiskRule] => WhiskRule.serdes.read(value)
case _ => throw DocumentUnreadable(Messages.corruptedEntity)
}
value.asJsObject.fields.get("entityType").foreach {
case JsString(entityType) if (doc.entityType != entityType) =>
throw DocumentTypeMismatchException(s"document type ${doc.entityType} did not match expected type $entityType.")
case _ =>
}
doc
}
}
/**
* Dispatches to appropriate serdes. This object is not itself implicit so as to
* avoid multiple implicit alternatives when working with one of the subtypes.
*/
object WhiskEntityJsonFormat extends RootJsonFormat[WhiskEntity] {
// THE ORDER MATTERS! E.g. some triggers can deserialize as packages, but not
// the other way around. Try most specific first!
private def readers: Stream[JsValue => WhiskEntity] =
Stream(
WhiskAction.serdes.read,
WhiskActivation.serdes.read,
WhiskRule.serdes.read,
WhiskTrigger.serdes.read,
WhiskPackage.serdes.read)
// Not necessarily the smartest way to go about this. In theory, whenever
// a more precise type is known, this method shouldn't be used.
override def read(js: JsValue): WhiskEntity = {
val successes: Stream[WhiskEntity] = readers.flatMap(r => Try(r(js)).toOption)
successes.headOption.getOrElse {
throw DocumentUnreadable(Messages.corruptedEntity)
}
}
override def write(we: WhiskEntity): JsValue = we match {
case a: WhiskAction => WhiskAction.serdes.write(a)
case a: WhiskActivation => WhiskActivation.serdes.write(a)
case p: WhiskPackage => WhiskPackage.serdes.write(p)
case r: WhiskRule => WhiskRule.serdes.write(r)
case t: WhiskTrigger => WhiskTrigger.serdes.write(t)
}
}
/**
* Trait for the objects we want to size. The size will be defined as ByteSize.
*/
trait ByteSizeable {
/**
* Method to calculate the size of the object.
* The size of the object is defined as the sum of sizes of all parameters, that is stored in the object.
*
* @return the size of the object as ByteSize
*/
def size: ByteSize
}
object LimitedWhiskEntityPut extends DefaultJsonProtocol {
implicit val serdes = jsonFormat3(LimitedWhiskEntityPut.apply)
}
case class SizeError(field: String, is: ByteSize, allowed: ByteSize)
case class LimitedWhiskEntityPut(exec: Option[Exec] = None,
parameters: Option[Parameters] = None,
annotations: Option[Parameters] = None) {
def isWithinSizeLimits: Option[SizeError] = {
exec.flatMap { e =>
val is = e.size
if (is <= Exec.sizeLimit) None
else
Some {
SizeError(WhiskAction.execFieldName, is, Exec.sizeLimit)
}
} orElse parameters.flatMap { p =>
val is = p.size
if (is <= Parameters.sizeLimit) None
else
Some {
SizeError(WhiskEntity.paramsFieldName, is, Parameters.sizeLimit)
}
} orElse annotations.flatMap { a =>
val is = a.size
if (is <= Parameters.sizeLimit) None
else
Some {
SizeError(WhiskEntity.annotationsFieldName, is, Parameters.sizeLimit)
}
}
}
}