blob: 32ce60af4f015121d5d4f38f457218c8a0e494ca [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.nio.charset.StandardCharsets
import org.apache.openwhisk.core.ConfigKeys
import scala.util.matching.Regex
import spray.json._
import spray.json.DefaultJsonProtocol._
import org.apache.openwhisk.core.entity.Attachments._
import org.apache.openwhisk.core.entity.ExecManifest._
import org.apache.openwhisk.core.entity.size.SizeInt
import org.apache.openwhisk.core.entity.size._
import org.apache.openwhisk.core.entity.size.SizeString
import pureconfig.loadConfigOrThrow
import org.apache.openwhisk.http._
/**
* Exec encodes the executable details of an action. For black
* box container, an image name is required. For Javascript and Python
* actions, the code to execute is required.
* For Swift actions, the source code to execute the action is required.
* For Java actions, a base64-encoded string representing a jar file is
* required, as well as the name of the entrypoint class.
* An example exec looks like this:
* { kind : one of supported language runtimes,
* code : code to execute if kind is supported,
* image : container name when kind is "blackbox",
* binary: for some runtimes that allow binary attachments,
* main : name of the entry point function, when using a non-default value (for Java, the name of the main class)" }
*/
sealed abstract class Exec extends ByteSizeable {
override def toString: String = Exec.serdes.write(this).compactPrint
/** A type descriptor. */
val kind: String
/** When true exec may not be executed or updated. */
val deprecated: Boolean
}
sealed abstract class ExecMetaDataBase extends Exec {
override def toString: String = ExecMetaDataBase.serdes.write(this).compactPrint
}
/**
* A common super class for all action exec types that contain their executable
* code explicitly (i.e., any action other than a sequence).
*/
sealed abstract class CodeExec[+T <% SizeConversion] extends Exec {
/** An entrypoint (typically name of 'main' function). 'None' means a default value will be used. */
val entryPoint: Option[String]
/** The executable code. */
val code: T
/** Serialize code to a JSON value. */
def codeAsJson: JsValue
/** The runtime image (either built-in or a public image). */
val image: ImageName
/** Indicates if the action execution generates log markers to stdout/stderr once action activation completes. */
val sentinelledLogs: Boolean
/** Indicates if a container image is required from the registry to execute the action. */
val pull: Boolean
/**
* Indicates whether the code is stored in a text-readable or binary format.
* The binary bit may be read from the database but currently it is always computed
* when the "code" is moved to an attachment this may get changed to avoid recomputing
* the binary property.
*/
val binary: Boolean
override def size = code.sizeInBytes + entryPoint.map(_.sizeInBytes).getOrElse(0.B)
}
sealed abstract class ExecMetaData extends ExecMetaDataBase {
/** An entrypoint (typically name of 'main' function). 'None' means a default value will be used. */
val entryPoint: Option[String]
/** The runtime image (either built-in or a public image). */
val image: ImageName
/** Indicates if a container image is required from the registry to execute the action. */
val pull: Boolean
/**
* Indicates whether the code is stored in a text-readable or binary format.
* The binary bit may be read from the database but currently it is always computed
* when the "code" is moved to an attachment this may get changed to avoid recomputing
* the binary property.
*/
val binary: Boolean
override def size = 0.B
}
trait AttachedCode {
def inline(bytes: Array[Byte]): Exec
def attach(attached: Attached): Exec
}
protected[core] case class CodeExecAsString(manifest: RuntimeManifest,
override val code: String,
override val entryPoint: Option[String])
extends CodeExec[String] {
override val kind = manifest.kind
override val image = manifest.image
override val sentinelledLogs = manifest.sentinelledLogs.getOrElse(true)
override val deprecated = manifest.deprecated.getOrElse(false)
override val pull = false
override lazy val binary = Exec.isBinaryCode(code)
override def codeAsJson = JsString(code)
}
protected[core] case class CodeExecMetaDataAsString(manifest: RuntimeManifest,
override val binary: Boolean = false,
override val entryPoint: Option[String])
extends ExecMetaData {
override val kind = manifest.kind
override val image = manifest.image
override val deprecated = manifest.deprecated.getOrElse(false)
override val pull = false
}
protected[core] case class CodeExecAsAttachment(manifest: RuntimeManifest,
override val code: Attachment[String],
override val entryPoint: Option[String],
override val binary: Boolean = false)
extends CodeExec[Attachment[String]]
with AttachedCode {
override val kind = manifest.kind
override val image = manifest.image
override val sentinelledLogs = manifest.sentinelledLogs.getOrElse(true)
override val deprecated = manifest.deprecated.getOrElse(false)
override val pull = false
override def codeAsJson = code.toJson
override def inline(bytes: Array[Byte]): CodeExecAsAttachment = {
val encoded = new String(bytes, StandardCharsets.UTF_8)
copy(code = Inline(encoded))
}
override def attach(attached: Attached): CodeExecAsAttachment = {
copy(code = attached)
}
}
protected[core] case class CodeExecMetaDataAsAttachment(manifest: RuntimeManifest,
override val binary: Boolean = false,
override val entryPoint: Option[String])
extends ExecMetaData {
override val kind = manifest.kind
override val image = manifest.image
override val deprecated = manifest.deprecated.getOrElse(false)
override val pull = false
}
/**
* @param image the image name
* @param code an optional script or zip archive (as base64 encoded) string
*/
protected[core] case class BlackBoxExec(override val image: ImageName,
override val code: Option[Attachment[String]],
override val entryPoint: Option[String],
val native: Boolean,
override val binary: Boolean)
extends CodeExec[Option[Attachment[String]]]
with AttachedCode {
override val kind = Exec.BLACKBOX
override val deprecated = false
override def codeAsJson = code.toJson
override val sentinelledLogs = native
override val pull = !native
override def size = super.size + image.resolveImageName().sizeInBytes
override def inline(bytes: Array[Byte]): BlackBoxExec = {
val encoded = new String(bytes, StandardCharsets.UTF_8)
copy(code = Some(Inline(encoded)))
}
override def attach(attached: Attached): BlackBoxExec = {
copy(code = Some(attached))
}
}
protected[core] case class BlackBoxExecMetaData(override val image: ImageName,
override val entryPoint: Option[String],
val native: Boolean,
override val binary: Boolean = false)
extends ExecMetaData {
override val kind = ExecMetaDataBase.BLACKBOX
override val deprecated = false
override val pull = !native
}
protected[core] case class SequenceExec(components: Vector[FullyQualifiedEntityName]) extends Exec {
override val kind = Exec.SEQUENCE
override val deprecated = false
override def size = components.map(_.size).reduceOption(_ + _).getOrElse(0.B)
}
protected[core] case class SequenceExecMetaData(components: Vector[FullyQualifiedEntityName]) extends ExecMetaDataBase {
override val kind = ExecMetaDataBase.SEQUENCE
override val deprecated = false
override def size = components.map(_.size).reduceOption(_ + _).getOrElse(0.B)
}
object Exec extends ArgNormalizer[Exec] with DefaultJsonProtocol {
val maxSize: ByteSize = 48.MB
val sizeLimit = loadConfigOrThrow[ByteSize](ConfigKeys.execSizeLimit)
require(
sizeLimit <= maxSize,
s"Executable code size limit $sizeLimit specified by '${ConfigKeys.execSizeLimit}' should not be more than max size of $maxSize")
// The possible values of the JSON 'kind' field for certain runtimes:
// - Sequence because it is an intrinsic
// - Black Box because it is a type marker
protected[core] val SEQUENCE = "sequence"
protected[core] val BLACKBOX = "blackbox"
// This is for error cases where the action `kind` may not be known.
protected[core] val UNKNOWN = "unknown"
private def execManifests = ExecManifest.runtimesManifest
override protected[core] implicit lazy val serdes = new RootJsonFormat[Exec] {
private def attFmt[T: JsonFormat] = Attachments.serdes[T]
private lazy val runtimes: Set[String] = execManifests.knownContainerRuntimes ++ Set(SEQUENCE, BLACKBOX)
override def write(e: Exec) = e match {
case c: CodeExecAsString =>
val base = Map("kind" -> JsString(c.kind), "code" -> JsString(c.code), "binary" -> JsBoolean(c.binary))
val main = c.entryPoint.map("main" -> JsString(_))
JsObject(base ++ main)
case a: CodeExecAsAttachment =>
val base =
Map("kind" -> JsString(a.kind), "code" -> attFmt[String].write(a.code), "binary" -> JsBoolean(a.binary))
val main = a.entryPoint.map("main" -> JsString(_))
JsObject(base ++ main)
case s @ SequenceExec(comp) =>
JsObject("kind" -> JsString(s.kind), "components" -> comp.map(_.qualifiedNameWithLeadingSlash).toJson)
case b: BlackBoxExec =>
val base =
Map(
"kind" -> JsString(b.kind),
"image" -> JsString(b.image.resolveImageName()),
"binary" -> JsBoolean(b.binary))
val code = b.code.map("code" -> attFmt[String].write(_))
val main = b.entryPoint.map("main" -> JsString(_))
JsObject(base ++ code ++ main)
case _ => JsObject.empty
}
override def read(v: JsValue) = {
require(v != null)
val obj = v.asJsObject
val kind = obj.fields.get("kind") match {
case Some(JsString(k)) => k.trim.toLowerCase
case _ => throw new DeserializationException("'kind' must be a string defined in 'exec'")
}
lazy val optMainField: Option[String] = obj.fields.get("main") match {
case Some(JsString(m)) => Some(m)
case Some(_) =>
throw new DeserializationException(s"if defined, 'main' be a string in 'exec' for '$kind' actions")
case None => None
}
kind match {
case Exec.SEQUENCE =>
val comp: Vector[FullyQualifiedEntityName] = obj.fields.get("components") match {
case Some(JsArray(components)) => components map (FullyQualifiedEntityName.serdes.read(_))
case Some(_) => throw new DeserializationException(s"'components' must be an array")
case None => throw new DeserializationException(s"'components' must be defined for sequence kind")
}
SequenceExec(comp)
case Exec.BLACKBOX =>
val image: ImageName = obj.fields.get("image") match {
case Some(JsString(i)) => ImageName.fromString(i).get // throws deserialization exception on failure
case _ =>
throw new DeserializationException(
s"'image' must be a string defined in 'exec' for '${Exec.BLACKBOX}' actions")
}
val (codeOpt: Option[Attachment[String]], binary) = obj.fields.get("code") match {
case None => (None, false)
case Some(JsString(i)) if i.trim.isEmpty => (None, false)
case Some(code) => (Some(attFmt[String].read(code)), isBinary(code, obj))
}
val native = execManifests.skipDockerPull(image)
BlackBoxExec(image, codeOpt, optMainField, native, binary)
case _ =>
// map "default" virtual runtime versions to the currently blessed actual runtime version
val manifest = execManifests.resolveDefaultRuntime(kind) match {
case Some(k) => k
case None => throw new DeserializationException(Messages.invalidRuntimeError(kind, runtimes))
}
manifest.attached
.map { _ =>
// java actions once stored the attachment in "jar" instead of "code"
val code = obj.fields.get("code").orElse(obj.fields.get("jar")).getOrElse {
throw new DeserializationException(
s"'code' must be a string or attachment object defined in 'exec' for '$kind' actions")
}
val main = optMainField.orElse {
if (manifest.requireMain.exists(identity)) {
throw new DeserializationException(s"'main' must be a string defined in 'exec' for '$kind' actions")
} else None
}
CodeExecAsAttachment(manifest, attFmt[String].read(code), main, isBinary(code, obj))
}
.getOrElse {
val code: String = obj.fields.get("code") match {
case Some(JsString(c)) => c
case _ =>
throw new DeserializationException(s"'code' must be a string defined in 'exec' for '$kind' actions")
}
CodeExecAsString(manifest, code, optMainField)
}
}
}
}
val isBase64Pattern = new Regex("^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)$").pattern
def isBinaryCode(code: String): Boolean = {
if (code != null) {
val t = code.trim
(t.length > 0) && (t.length % 4 == 0) && isBase64Pattern.matcher(t).matches()
} else false
}
private def isBinary(code: JsValue, obj: JsObject): Boolean = {
code match {
case JsString(c) => isBinaryCode(c)
case _ => obj.fields.get("binary").map(_.convertTo[Boolean]).getOrElse(false)
}
}
}
protected[core] object ExecMetaDataBase extends ArgNormalizer[ExecMetaDataBase] with DefaultJsonProtocol {
// The possible values of the JSON 'kind' field for certain runtimes:
// - Sequence because it is an intrinsic
// - Black Box because it is a type marker
protected[core] val SEQUENCE = "sequence"
protected[core] val BLACKBOX = "blackbox"
private def execManifests = ExecManifest.runtimesManifest
override protected[core] implicit lazy val serdes = new RootJsonFormat[ExecMetaDataBase] {
private def attFmt[T: JsonFormat] = Attachments.serdes[T]
private lazy val runtimes: Set[String] = execManifests.knownContainerRuntimes ++ Set(SEQUENCE, BLACKBOX)
override def write(e: ExecMetaDataBase) = e match {
case c: CodeExecMetaDataAsString =>
val base = Map("kind" -> JsString(c.kind), "binary" -> JsBoolean(c.binary))
val main = c.entryPoint.map("main" -> JsString(_))
JsObject(base ++ main)
case a: CodeExecMetaDataAsAttachment =>
val base =
Map("kind" -> JsString(a.kind), "binary" -> JsBoolean(a.binary))
val main = a.entryPoint.map("main" -> JsString(_))
JsObject(base ++ main)
case s @ SequenceExecMetaData(comp) =>
JsObject("kind" -> JsString(s.kind), "components" -> comp.map(_.qualifiedNameWithLeadingSlash).toJson)
case b: BlackBoxExecMetaData =>
val base =
Map(
"kind" -> JsString(b.kind),
"image" -> JsString(b.image.resolveImageName()),
"binary" -> JsBoolean(b.binary))
val main = b.entryPoint.map("main" -> JsString(_))
JsObject(base ++ main)
}
override def read(v: JsValue) = {
require(v != null)
val obj = v.asJsObject
val kind = obj.fields.get("kind") match {
case Some(JsString(k)) => k.trim.toLowerCase
case _ => throw new DeserializationException("'kind' must be a string defined in 'exec'")
}
lazy val optMainField: Option[String] = obj.fields.get("main") match {
case Some(JsString(m)) => Some(m)
case Some(_) =>
throw new DeserializationException(s"if defined, 'main' be a string in 'exec' for '$kind' actions")
case None => None
}
lazy val binary: Boolean = obj.fields.get("binary") match {
case Some(JsBoolean(b)) => b
case _ => throw new DeserializationException("'binary' must be a boolean defined in 'exec'")
}
kind match {
case ExecMetaDataBase.SEQUENCE =>
val comp: Vector[FullyQualifiedEntityName] = obj.fields.get("components") match {
case Some(JsArray(components)) => components map (FullyQualifiedEntityName.serdes.read(_))
case Some(_) => throw new DeserializationException(s"'components' must be an array")
case None => throw new DeserializationException(s"'components' must be defined for sequence kind")
}
SequenceExecMetaData(comp)
case ExecMetaDataBase.BLACKBOX =>
val image: ImageName = obj.fields.get("image") match {
case Some(JsString(i)) => ImageName.fromString(i).get // throws deserialization exception on failure
case _ =>
throw new DeserializationException(
s"'image' must be a string defined in 'exec' for '${Exec.BLACKBOX}' actions")
}
val native = execManifests.skipDockerPull(image)
BlackBoxExecMetaData(image, optMainField, native, binary)
case _ =>
// map "default" virtual runtime versions to the currently blessed actual runtime version
val manifest = execManifests.resolveDefaultRuntime(kind) match {
case Some(k) => k
case None => throw new DeserializationException(s"kind '$kind' not in $runtimes")
}
manifest.attached
.map { a =>
val main = optMainField.orElse {
if (manifest.requireMain.exists(identity)) {
throw new DeserializationException(s"'main' must be a string defined in 'exec' for '$kind' actions")
} else None
}
CodeExecMetaDataAsAttachment(manifest, binary, main)
}
.getOrElse {
CodeExecMetaDataAsString(manifest, binary, optMainField)
}
}
}
}
}