blob: a51e55278896f7527419a7d93b79c856fabe0ca6 [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.io.{ByteArrayInputStream, ByteArrayOutputStream}
import java.nio.charset.StandardCharsets.UTF_8
import java.time.Instant
import java.util.Base64
import akka.http.scaladsl.model.ContentTypes
import scala.concurrent.ExecutionContext
import scala.concurrent.Future
import scala.util.{Failure, Success, Try}
import spray.json._
import spray.json.DefaultJsonProtocol._
import org.apache.openwhisk.common.TransactionId
import org.apache.openwhisk.core.database.ArtifactStore
import org.apache.openwhisk.core.database.DocumentFactory
import org.apache.openwhisk.core.database.CacheChangeNotification
import org.apache.openwhisk.core.entity.Attachments._
import org.apache.openwhisk.core.entity.types.EntityStore
/**
* ActionLimitsOption mirrors ActionLimits but makes both the timeout and memory
* limit optional so that it is convenient to override just one limit at a time.
*/
case class ActionLimitsOption(timeout: Option[TimeLimit],
memory: Option[MemoryLimit],
logs: Option[LogLimit],
concurrency: Option[ConcurrencyLimit])
/**
* WhiskActionPut is a restricted WhiskAction view that eschews properties
* that are auto-assigned or derived from URI: namespace and name. It
* also replaces limits with an optional counterpart for convenience of
* overriding only one value at a time.
*/
case class WhiskActionPut(exec: Option[Exec] = None,
parameters: Option[Parameters] = None,
limits: Option[ActionLimitsOption] = None,
version: Option[SemVer] = None,
publish: Option[Boolean] = None,
annotations: Option[Parameters] = None,
delAnnotations: Option[Array[String]] = None) {
protected[core] def replace(exec: Exec) = {
WhiskActionPut(Some(exec), parameters, limits, version, publish, annotations)
}
/**
* Resolves sequence components if they contain default namespace.
*/
protected[core] def resolve(userNamespace: Namespace): WhiskActionPut = {
exec map {
case SequenceExec(components) =>
val newExec = SequenceExec(components map { c =>
FullyQualifiedEntityName(c.path.resolveNamespace(userNamespace), c.name)
})
WhiskActionPut(Some(newExec), parameters, limits, version, publish, annotations)
case _ => this
} getOrElse this
}
}
abstract class WhiskActionLike(override val name: EntityName) extends WhiskEntity(name, "action") {
def exec: Exec
def parameters: Parameters
def limits: ActionLimits
/** @return true iff action has appropriate annotation. */
def hasFinalParamsAnnotation = {
annotations.getAs[Boolean](Annotations.FinalParamsAnnotationName) getOrElse false
}
/** @return a Set of immutable parameternames */
def immutableParameters =
if (hasFinalParamsAnnotation) {
parameters.definedParameters
} else Set.empty[String]
def toJson =
JsObject(
"namespace" -> namespace.toJson,
"name" -> name.toJson,
"exec" -> exec.toJson,
"parameters" -> parameters.toJson,
"limits" -> limits.toJson,
"version" -> version.toJson,
"publish" -> publish.toJson,
"annotations" -> annotations.toJson)
}
abstract class WhiskActionLikeMetaData(override val name: EntityName) extends WhiskActionLike(name) {
override def exec: ExecMetaDataBase
}
/**
* A WhiskAction provides an abstraction of the meta-data
* for a whisk action.
*
* The WhiskAction object is used as a helper to adapt objects between
* the schema used by the database and the WhiskAction abstraction.
*
* @param namespace the namespace for the action
* @param name the name of the action
* @param exec the action executable details
* @param parameters the set of parameters to bind to the action environment
* @param limits the limits to impose on the action
* @param version the semantic version
* @param publish true to share the action or false otherwise
* @param annotations the set of annotations to attribute to the action
* @param updated the timestamp when the action is updated
* @throws IllegalArgumentException if any argument is undefined
*/
@throws[IllegalArgumentException]
case class WhiskAction(namespace: EntityPath,
override val name: EntityName,
exec: Exec,
parameters: Parameters = Parameters(),
limits: ActionLimits = ActionLimits(),
version: SemVer = SemVer(),
publish: Boolean = false,
annotations: Parameters = Parameters(),
override val updated: Instant = WhiskEntity.currentMillis())
extends WhiskActionLike(name) {
require(exec != null, "exec undefined")
require(limits != null, "limits undefined")
/**
* Merges parameters (usually from package) with existing action parameters.
* Existing parameters supersede those in p.
*/
def inherit(p: Parameters): WhiskAction = copy(parameters = p ++ parameters).revision[WhiskAction](rev)
/**
* Resolves sequence components if they contain default namespace.
*/
protected[core] def resolve(userNamespace: Namespace): WhiskAction = {
resolve(userNamespace.name)
}
/**
* Resolves sequence components if they contain default namespace.
*/
protected[core] def resolve(userNamespace: EntityName): WhiskAction = {
exec match {
case SequenceExec(components) =>
val newExec = SequenceExec(components map { c =>
FullyQualifiedEntityName(c.path.resolveNamespace(userNamespace), c.name)
})
copy(exec = newExec).revision[WhiskAction](rev)
case _ => this
}
}
def toExecutableWhiskAction: Option[ExecutableWhiskAction] = exec match {
case codeExec: CodeExec[_] =>
Some(
ExecutableWhiskAction(namespace, name, codeExec, parameters, limits, version, publish, annotations)
.revision[ExecutableWhiskAction](rev))
case _ => None
}
/**
* This the action summary as computed by the database view.
* Strictly used in view testing to enforce alignment.
*/
override def summaryAsJson: JsObject = {
val binary = exec match {
case c: CodeExec[_] => c.binary
case _ => false
}
JsObject(
super.summaryAsJson.fields +
("limits" -> limits.toJson) +
("exec" -> JsObject("binary" -> JsBoolean(binary))))
}
}
@throws[IllegalArgumentException]
case class WhiskActionMetaData(namespace: EntityPath,
override val name: EntityName,
exec: ExecMetaDataBase,
parameters: Parameters = Parameters(),
limits: ActionLimits = ActionLimits(),
version: SemVer = SemVer(),
publish: Boolean = false,
annotations: Parameters = Parameters(),
override val updated: Instant = WhiskEntity.currentMillis(),
binding: Option[EntityPath] = None)
extends WhiskActionLikeMetaData(name) {
require(exec != null, "exec undefined")
require(limits != null, "limits undefined")
/**
* Merges parameters (usually from package) with existing action parameters.
* Existing parameters supersede those in p.
*/
def inherit(p: Parameters, binding: Option[EntityPath] = None) =
copy(parameters = p ++ parameters, binding = binding).revision[WhiskActionMetaData](rev)
/**
* Resolves sequence components if they contain default namespace.
*/
protected[core] def resolve(userNamespace: Namespace): WhiskActionMetaData = {
exec match {
case SequenceExecMetaData(components) =>
val newExec = SequenceExecMetaData(components map { c =>
FullyQualifiedEntityName(c.path.resolveNamespace(userNamespace.name), c.name)
})
copy(exec = newExec).revision[WhiskActionMetaData](rev)
case _ => this
}
}
def toExecutableWhiskAction = exec match {
case execMetaData: ExecMetaData =>
Some(
ExecutableWhiskActionMetaData(
namespace,
name,
execMetaData,
parameters,
limits,
version,
publish,
annotations,
binding)
.revision[ExecutableWhiskActionMetaData](rev))
case _ =>
None
}
}
/**
* Variant of WhiskAction which only includes information necessary to be
* executed by an Invoker.
*
* exec is typed to CodeExec to guarantee executability by an Invoker.
*
* Note: Two actions are equal regardless of their DocRevision if there is one.
* The invoker uses action equality when matching actions to warm containers.
* That means creating an action, invoking it, then deleting/recreating/reinvoking
* it will reuse the previous container. The delete/recreate restores the SemVer to 0.0.1.
*
* @param namespace the namespace for the action
* @param name the name of the action
* @param exec the action executable details
* @param parameters the set of parameters to bind to the action environment
* @param limits the limits to impose on the action
* @param version the semantic version
* @param publish true to share the action or false otherwise
* @param annotations the set of annotations to attribute to the action
* @param binding the path of the package binding if any
* @throws IllegalArgumentException if any argument is undefined
*/
@throws[IllegalArgumentException]
case class ExecutableWhiskAction(namespace: EntityPath,
override val name: EntityName,
exec: CodeExec[_],
parameters: Parameters = Parameters(),
limits: ActionLimits = ActionLimits(),
version: SemVer = SemVer(),
publish: Boolean = false,
annotations: Parameters = Parameters(),
binding: Option[EntityPath] = None)
extends WhiskActionLike(name) {
require(exec != null, "exec undefined")
require(limits != null, "limits undefined")
/**
* Gets initializer for action. This typically includes the code to execute,
* or a zip file containing the executable artifacts.
*
* @param env optional map of properties to be exported to the environment
*/
def containerInitializer(env: Map[String, JsValue] = Map.empty): JsObject = {
val code = Option(exec.codeAsJson).filter(_ != JsNull).map("code" -> _)
val envargs = if (env.nonEmpty) {
val stringifiedEnvVars = env.map {
case (k, v: JsString) => (k, v)
case (k, JsNull) => (k, JsString.empty)
case (k, JsBoolean(v)) => (k, JsString(v.toString))
case (k, JsNumber(v)) => (k, JsString(v.toString))
case (k, v) => (k, JsString(v.compactPrint))
}
Some("env" -> JsObject(stringifiedEnvVars))
} else None
val base =
Map("name" -> name.toJson, "binary" -> exec.binary.toJson, "main" -> exec.entryPoint.getOrElse("main").toJson)
JsObject(base ++ envargs ++ code)
}
def toWhiskAction =
WhiskAction(namespace, name, exec, parameters, limits, version, publish, annotations)
.revision[WhiskAction](rev)
}
@throws[IllegalArgumentException]
case class ExecutableWhiskActionMetaData(namespace: EntityPath,
override val name: EntityName,
exec: ExecMetaData,
parameters: Parameters = Parameters(),
limits: ActionLimits = ActionLimits(),
version: SemVer = SemVer(),
publish: Boolean = false,
annotations: Parameters = Parameters(),
binding: Option[EntityPath] = None)
extends WhiskActionLikeMetaData(name) {
require(exec != null, "exec undefined")
require(limits != null, "limits undefined")
def toWhiskAction =
WhiskActionMetaData(namespace, name, exec, parameters, limits, version, publish, annotations, updated)
.revision[WhiskActionMetaData](rev)
/**
* Some fully qualified name only if there's a binding, else None.
*/
def bindingFullyQualifiedName: Option[FullyQualifiedEntityName] =
binding.map(ns => FullyQualifiedEntityName(ns, name, None))
}
object WhiskAction extends DocumentFactory[WhiskAction] with WhiskEntityQueries[WhiskAction] with DefaultJsonProtocol {
import WhiskActivation.instantSerdes
val execFieldName = "exec"
val requireWhiskAuthHeader = "x-require-whisk-auth"
override val collectionName = "actions"
override val cacheEnabled = true
override implicit val serdes = jsonFormat(
WhiskAction.apply,
"namespace",
"name",
"exec",
"parameters",
"limits",
"version",
"publish",
"annotations",
"updated")
// overridden to store attached code
override def put[A >: WhiskAction](db: ArtifactStore[A], doc: WhiskAction, old: Option[WhiskAction])(
implicit transid: TransactionId,
notifier: Option[CacheChangeNotification]): Future[DocInfo] = {
def putWithAttachment(code: String, binary: Boolean, exec: AttachedCode) = {
implicit val logger = db.logging
implicit val ec = db.executionContext
val oldAttachment = old.flatMap(getAttachment)
val (bytes, attachmentType) = if (binary) {
(Base64.getDecoder.decode(code), ContentTypes.`application/octet-stream`)
} else {
(code.getBytes(UTF_8), ContentTypes.`text/plain(UTF-8)`)
}
val stream = new ByteArrayInputStream(bytes)
super.putAndAttach(
db,
doc
.copy(parameters = doc.parameters.lock(ParameterEncryption.singleton.default))
.revision[WhiskAction](doc.rev),
attachmentUpdater,
attachmentType,
stream,
oldAttachment,
Some { a: WhiskAction =>
a.copy(exec = exec.inline(code.getBytes(UTF_8)))
})
}
Try {
require(db != null, "db undefined")
require(doc != null, "doc undefined")
} map { _ =>
doc.exec match {
case exec @ CodeExecAsAttachment(_, Inline(code), _, binary) =>
putWithAttachment(code, binary, exec)
case exec @ BlackBoxExec(_, Some(Inline(code)), _, _, binary) =>
putWithAttachment(code, binary, exec)
case _ =>
super.put(
db,
doc
.copy(parameters = doc.parameters.lock(ParameterEncryption.singleton.default))
.revision[WhiskAction](doc.rev),
old)
}
} match {
case Success(f) => f
case Failure(f) => Future.failed(f)
}
}
// overridden to retrieve attached code
override def get[A >: WhiskAction](
db: ArtifactStore[A],
doc: DocId,
rev: DocRevision = DocRevision.empty,
fromCache: Boolean)(implicit transid: TransactionId, mw: Manifest[WhiskAction]): Future[WhiskAction] = {
implicit val ec = db.executionContext
val inlineActionCode: WhiskAction => Future[WhiskAction] = { action =>
def getWithAttachment(attached: Attached, binary: Boolean, exec: AttachedCode) = {
val boas = new ByteArrayOutputStream()
val wrapped = if (binary) Base64.getEncoder().wrap(boas) else boas
getAttachment[A](db, action, attached, wrapped, Some { a: WhiskAction =>
wrapped.close()
val newAction = a.copy(exec = exec.inline(boas.toByteArray))
newAction.revision(a.rev)
newAction
})
}
action.exec match {
case exec @ CodeExecAsAttachment(_, attached: Attached, _, binary) =>
getWithAttachment(attached, binary, exec)
case exec @ BlackBoxExec(_, Some(attached: Attached), _, _, binary) =>
getWithAttachment(attached, binary, exec)
case _ =>
Future.successful(action)
}
}
super.getWithAttachment(db, doc, rev, fromCache, attachmentHandler, inlineActionCode)
}
def attachmentHandler(action: WhiskAction, attached: Attached): WhiskAction = {
def checkName(name: String) = {
require(
name == attached.attachmentName,
s"Attachment name '${attached.attachmentName}' does not match the expected name '$name'")
}
val eu = action.exec match {
case exec @ CodeExecAsAttachment(_, Attached(attachmentName, _, _, _), _, _) =>
checkName(attachmentName)
exec.attach(attached)
case exec @ BlackBoxExec(_, Some(Attached(attachmentName, _, _, _)), _, _, _) =>
checkName(attachmentName)
exec.attach(attached)
case exec => exec
}
action.copy(exec = eu).revision[WhiskAction](action.rev)
}
def attachmentUpdater(action: WhiskAction, updatedAttachment: Attached): WhiskAction = {
action.exec match {
case exec: AttachedCode =>
action.copy(exec = exec.attach(updatedAttachment)).revision[WhiskAction](action.rev)
case _ => action
}
}
def getAttachment(action: WhiskAction): Option[Attached] = {
action.exec match {
case CodeExecAsAttachment(_, a: Attached, _, _) => Some(a)
case BlackBoxExec(_, Some(a: Attached), _, _, _) => Some(a)
case _ => None
}
}
override def del[Wsuper >: WhiskAction](db: ArtifactStore[Wsuper], doc: DocInfo)(
implicit transid: TransactionId,
notifier: Option[CacheChangeNotification]): Future[Boolean] = {
Try {
require(db != null, "db undefined")
require(doc != null, "doc undefined")
}.map { _ =>
val fa = super.del(db, doc)
implicit val ec = db.executionContext
fa.flatMap { _ =>
super.deleteAttachments(db, doc)
}
} match {
case Success(f) => f
case Failure(f) => Future.failed(f)
}
}
/**
* Resolves an action name if it is contained in a package.
* Look up the package to determine if it is a binding or the actual package.
* If it's a binding, rewrite the fully qualified name of the action using the actual package path name.
* If it's the actual package, use its name directly as the package path name.
*/
def resolveAction(db: EntityStore, fullyQualifiedActionName: FullyQualifiedEntityName)(
implicit ec: ExecutionContext,
transid: TransactionId): Future[FullyQualifiedEntityName] = {
// first check that there is a package to be resolved
val entityPath = fullyQualifiedActionName.path
if (entityPath.defaultPackage) {
// this is the default package, nothing to resolve
Future.successful(fullyQualifiedActionName)
} else {
// there is a package to be resolved
val pkgDocId = fullyQualifiedActionName.path.toDocId
val actionName = fullyQualifiedActionName.name
WhiskPackage.resolveBinding(db, pkgDocId) map {
_.fullyQualifiedName(withVersion = false).add(actionName)
}
}
}
/**
* Resolves an action name if it is contained in a package.
* Look up the package to determine if it is a binding or the actual package.
* If it's a binding, rewrite the fully qualified name of the action using the actual package path name.
* If it's the actual package, use its name directly as the package path name.
* While traversing the package bindings, merge the parameters.
*/
def resolveActionAndMergeParameters(entityStore: EntityStore, fullyQualifiedName: FullyQualifiedEntityName)(
implicit ec: ExecutionContext,
transid: TransactionId): Future[WhiskAction] = {
// first check that there is a package to be resolved
val entityPath = fullyQualifiedName.path
if (entityPath.defaultPackage) {
// this is the default package, nothing to resolve
WhiskAction.get(entityStore, fullyQualifiedName.toDocId)
} else {
// there is a package to be resolved
val pkgDocid = fullyQualifiedName.path.toDocId
val actionName = fullyQualifiedName.name
val wp = WhiskPackage.resolveBinding(entityStore, pkgDocid, mergeParameters = true)
wp flatMap { resolvedPkg =>
// fully resolved name for the action
val fqnAction = resolvedPkg.fullyQualifiedName(withVersion = false).add(actionName)
// get the whisk action associate with it and inherit the parameters from the package/binding
WhiskAction.get(entityStore, fqnAction.toDocId) map {
_.inherit(resolvedPkg.parameters)
}
}
}
}
}
object WhiskActionMetaData
extends DocumentFactory[WhiskActionMetaData]
with WhiskEntityQueries[WhiskActionMetaData]
with DefaultJsonProtocol {
import WhiskActivation.instantSerdes
override val collectionName = "actions"
override val cacheEnabled = true
override implicit val serdes = jsonFormat(
WhiskActionMetaData.apply,
"namespace",
"name",
"exec",
"parameters",
"limits",
"version",
"publish",
"annotations",
"updated",
"binding")
/**
* Resolves an action name if it is contained in a package.
* Look up the package to determine if it is a binding or the actual package.
* If it's a binding, rewrite the fully qualified name of the action using the actual package path name.
* If it's the actual package, use its name directly as the package path name.
*/
def resolveAction(db: EntityStore, fullyQualifiedActionName: FullyQualifiedEntityName)(
implicit ec: ExecutionContext,
transid: TransactionId): Future[FullyQualifiedEntityName] = {
// first check that there is a package to be resolved
val entityPath = fullyQualifiedActionName.path
if (entityPath.defaultPackage) {
// this is the default package, nothing to resolve
Future.successful(fullyQualifiedActionName)
} else {
// there is a package to be resolved
val pkgDocId = fullyQualifiedActionName.path.toDocId
val actionName = fullyQualifiedActionName.name
WhiskPackage.resolveBinding(db, pkgDocId) map {
_.fullyQualifiedName(withVersion = false).add(actionName)
}
}
}
/**
* Resolves an action name if it is contained in a package.
* Look up the package to determine if it is a binding or the actual package.
* If it's a binding, rewrite the fully qualified name of the action using the actual package path name.
* If it's the actual package, use its name directly as the package path name.
* While traversing the package bindings, merge the parameters.
*/
def resolveActionAndMergeParameters(entityStore: EntityStore, fullyQualifiedName: FullyQualifiedEntityName)(
implicit ec: ExecutionContext,
transid: TransactionId): Future[WhiskActionMetaData] = {
// first check that there is a package to be resolved
val entityPath = fullyQualifiedName.path
if (entityPath.defaultPackage) {
// this is the default package, nothing to resolve
WhiskActionMetaData.get(entityStore, fullyQualifiedName.toDocId)
} else {
// there is a package to be resolved
val pkgDocid = fullyQualifiedName.path.toDocId
val actionName = fullyQualifiedName.name
val wp = WhiskPackage.resolveBinding(entityStore, pkgDocid, mergeParameters = true)
wp flatMap { resolvedPkg =>
// fully resolved name for the action
val fqnAction = resolvedPkg.fullyQualifiedName(withVersion = false).add(actionName)
// get the whisk action associate with it and inherit the parameters from the package/binding
WhiskActionMetaData.get(entityStore, fqnAction.toDocId) map {
_.inherit(
resolvedPkg.parameters,
if (fullyQualifiedName.path.equals(resolvedPkg.fullPath)) None
else Some(fullyQualifiedName.path))
}
}
}
}
}
object ActionLimitsOption extends DefaultJsonProtocol {
implicit val serdes = jsonFormat4(ActionLimitsOption.apply)
}
object WhiskActionPut extends DefaultJsonProtocol {
implicit val serdes = jsonFormat7(WhiskActionPut.apply)
}