You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@openwhisk.apache.org by GitBox <gi...@apache.org> on 2018/08/29 18:07:44 UTC

[GitHub] dubee closed pull request #3979: Treat blackbox action code as attachment

dubee closed pull request #3979: Treat blackbox action code as attachment
URL: https://github.com/apache/incubator-openwhisk/pull/3979
 
 
   

This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:

As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):

diff --git a/common/scala/src/main/scala/whisk/core/entity/Attachments.scala b/common/scala/src/main/scala/whisk/core/entity/Attachments.scala
index 2d00a03deb..dc2c837456 100644
--- a/common/scala/src/main/scala/whisk/core/entity/Attachments.scala
+++ b/common/scala/src/main/scala/whisk/core/entity/Attachments.scala
@@ -54,6 +54,13 @@ object Attachments {
     }
   }
 
+  implicit class OptionSizeAttachment[T <% SizeConversion](a: Option[Attachment[T]]) extends SizeConversion {
+    def sizeIn(unit: SizeUnits.Unit): ByteSize = a match {
+      case Some(Inline(v)) => (v: SizeConversion).sizeIn(unit)
+      case _               => 0.bytes
+    }
+  }
+
   object Attached {
     implicit val serdes = {
       implicit val contentTypeSerdes = new RootJsonFormat[ContentType] {
diff --git a/common/scala/src/main/scala/whisk/core/entity/Exec.scala b/common/scala/src/main/scala/whisk/core/entity/Exec.scala
index c98e57188d..8e5d8f6213 100644
--- a/common/scala/src/main/scala/whisk/core/entity/Exec.scala
+++ b/common/scala/src/main/scala/whisk/core/entity/Exec.scala
@@ -27,7 +27,6 @@ import spray.json.DefaultJsonProtocol._
 import whisk.core.entity.Attachments._
 import whisk.core.entity.ExecManifest._
 import whisk.core.entity.size.SizeInt
-import whisk.core.entity.size.SizeOptionString
 import whisk.core.entity.size.SizeString
 
 /**
@@ -115,6 +114,11 @@ sealed abstract class ExecMetaData extends ExecMetaDataBase {
   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])
@@ -142,7 +146,8 @@ 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]] {
+    extends CodeExec[Attachment[String]]
+    with AttachedCode {
   override val kind = manifest.kind
   override val image = manifest.image
   override val sentinelledLogs = manifest.sentinelledLogs.getOrElse(true)
@@ -150,12 +155,12 @@ protected[core] case class CodeExecAsAttachment(manifest: RuntimeManifest,
   override val pull = false
   override def codeAsJson = code.toJson
 
-  def inline(bytes: Array[Byte]): CodeExecAsAttachment = {
+  override def inline(bytes: Array[Byte]): CodeExecAsAttachment = {
     val encoded = new String(bytes, StandardCharsets.UTF_8)
     copy(code = Inline(encoded))
   }
 
-  def attach(attached: Attached): CodeExecAsAttachment = {
+  override def attach(attached: Attached): CodeExecAsAttachment = {
     copy(code = attached)
   }
 }
@@ -175,17 +180,27 @@ protected[core] case class CodeExecMetaDataAsAttachment(manifest: RuntimeManifes
  * @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[String],
+                                        override val code: Option[Attachment[String]],
                                         override val entryPoint: Option[String],
-                                        val native: Boolean)
-    extends CodeExec[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 lazy val binary = code map { Exec.isBinaryCode(_) } getOrElse false
   override val sentinelledLogs = native
   override val pull = !native
   override def size = super.size + image.publicImageName.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,
@@ -244,7 +259,7 @@ protected[core] object Exec extends ArgNormalizer[Exec] with DefaultJsonProtocol
       case b: BlackBoxExec =>
         val base =
           Map("kind" -> JsString(b.kind), "image" -> JsString(b.image.publicImageName), "binary" -> JsBoolean(b.binary))
-        val code = b.code.filter(_.trim.nonEmpty).map("code" -> JsString(_))
+        val code = b.code.map("code" -> attFmt[String].write(_))
         val main = b.entryPoint.map("main" -> JsString(_))
         JsObject(base ++ code ++ main)
       case _ => JsObject.empty
@@ -283,15 +298,13 @@ protected[core] object Exec extends ArgNormalizer[Exec] with DefaultJsonProtocol
               throw new DeserializationException(
                 s"'image' must be a string defined in 'exec' for '${Exec.BLACKBOX}' actions")
           }
-          val code: Option[String] = obj.fields.get("code") match {
-            case Some(JsString(i)) => if (i.trim.nonEmpty) Some(i) else None
-            case Some(_) =>
-              throw new DeserializationException(
-                s"if defined, 'code' must a string defined in 'exec' for '${Exec.BLACKBOX}' actions")
-            case None => None
+          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, code, optMainField, native)
+          BlackBoxExec(image, codeOpt, optMainField, native, binary)
 
         case _ =>
           // map "default" virtual runtime versions to the currently blessed actual runtime version
@@ -307,10 +320,6 @@ protected[core] object Exec extends ArgNormalizer[Exec] with DefaultJsonProtocol
                 throw new DeserializationException(
                   s"'code' must be a string or attachment object defined in 'exec' for '$kind' actions")
               }
-              val binary: Boolean = code match {
-                case JsString(c) => isBinaryCode(c)
-                case _           => obj.fields.get("binary").map(_.convertTo[Boolean]).getOrElse(false)
-              }
 
               val main = optMainField.orElse {
                 if (manifest.requireMain.exists(identity)) {
@@ -318,7 +327,7 @@ protected[core] object Exec extends ArgNormalizer[Exec] with DefaultJsonProtocol
                 } else None
               }
 
-              CodeExecAsAttachment(manifest, attFmt[String].read(code), main, binary)
+              CodeExecAsAttachment(manifest, attFmt[String].read(code), main, isBinary(code, obj))
             }
             .getOrElse {
               val code: String = obj.fields.get("code") match {
@@ -340,6 +349,13 @@ protected[core] object Exec extends ArgNormalizer[Exec] with DefaultJsonProtocol
       (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 {
diff --git a/common/scala/src/main/scala/whisk/core/entity/WhiskAction.scala b/common/scala/src/main/scala/whisk/core/entity/WhiskAction.scala
index fd4d5461a4..55e02a777f 100644
--- a/common/scala/src/main/scala/whisk/core/entity/WhiskAction.scala
+++ b/common/scala/src/main/scala/whisk/core/entity/WhiskAction.scala
@@ -17,9 +17,8 @@
 
 package whisk.core.entity
 
-import java.io.ByteArrayInputStream
-import java.io.ByteArrayOutputStream
-import java.nio.charset.StandardCharsets
+import java.io.{ByteArrayInputStream, ByteArrayOutputStream}
+import java.nio.charset.StandardCharsets.UTF_8
 import java.util.Base64
 
 import akka.http.scaladsl.model.ContentTypes
@@ -331,38 +330,31 @@ object WhiskAction extends DocumentFactory[WhiskAction] with WhiskEntityQueries[
     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, 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) =>
-          implicit val logger = db.logging
-          implicit val ec = db.executionContext
-
-          val (bytes, attachmentType) = if (binary) {
-            (Base64.getDecoder.decode(code), ContentTypes.`application/octet-stream`)
-          } else {
-            (code.getBytes(StandardCharsets.UTF_8), ContentTypes.`text/plain(UTF-8)`)
-          }
-          val stream = new ByteArrayInputStream(bytes)
-          val oldAttachment = old
-            .flatMap(_.exec match {
-              case CodeExecAsAttachment(_, a: Attached, _, _) => Some(a)
-              case _                                          => None
-            })
-
-          super.putAndAttach(
-            db,
-            doc,
-            (d, a) => d.copy(exec = exec.attach(a)).revision[WhiskAction](d.rev),
-            attachmentType,
-            stream,
-            oldAttachment,
-            Some { a: WhiskAction =>
-              a.copy(exec = exec.inline(code.getBytes("UTF-8")))
-            })
-
+          putWithAttachment(code, binary, exec)
+        case exec @ BlackBoxExec(_, Some(Inline(code)), _, _, binary) =>
+          putWithAttachment(code, binary, exec)
         case _ =>
           super.put(db, doc, old)
       }
@@ -384,18 +376,23 @@ object WhiskAction extends DocumentFactory[WhiskAction] with WhiskEntityQueries[
     val fa = super.getWithAttachment(db, doc, rev, fromCache, Some(attachmentHandler _))
 
     fa.flatMap { 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) =>
-          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
-          })
-
+          getWithAttachment(attached, binary, exec)
+        case exec @ BlackBoxExec(_, Some(attached: Attached), _, _, binary) =>
+          getWithAttachment(attached, binary, exec)
         case _ =>
           Future.successful(action)
       }
@@ -403,17 +400,39 @@ object WhiskAction extends DocumentFactory[WhiskAction] with WhiskEntityQueries[
   }
 
   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, _, _, _), _, _) =>
-        require(
-          attachmentName == attached.attachmentName,
-          s"Attachment name '${attached.attachmentName}' does not match the expected name '$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] = {
diff --git a/tests/src/test/scala/whisk/core/controller/test/ActionsApiTests.scala b/tests/src/test/scala/whisk/core/controller/test/ActionsApiTests.scala
index 4b1401933b..fe4d2171e2 100644
--- a/tests/src/test/scala/whisk/core/controller/test/ActionsApiTests.scala
+++ b/tests/src/test/scala/whisk/core/controller/test/ActionsApiTests.scala
@@ -36,11 +36,9 @@ import whisk.core.entitlement.Collection
 import whisk.http.ErrorResponse
 import whisk.http.Messages
 import whisk.core.database.UserContext
-import java.io.ByteArrayInputStream
-import java.util.Base64
 
 import akka.http.scaladsl.model.headers.RawHeader
-import akka.stream.scaladsl._
+import whisk.core.entity.Attachments.Inline
 
 /**
  * Tests Actions API.
@@ -591,7 +589,7 @@ class ActionsApiTests extends ControllerTestCommon with WhiskActionsApi {
           action.annotations ++ Parameters(WhiskAction.execFieldName, Exec.BLACKBOX)))
       response.exec shouldBe an[BlackBoxExec]
       val bb = response.exec.asInstanceOf[BlackBoxExec]
-      bb.code shouldBe Some("cc")
+      bb.code shouldBe Some(Inline("cc"))
       bb.binary shouldBe false
     }
   }
@@ -801,7 +799,8 @@ class ActionsApiTests extends ControllerTestCommon with WhiskActionsApi {
         annotations = Parameters("exec", "java"))
     val nodeAction = WhiskAction(namespace, aname(), jsDefault(nonInlinedCode(entityStore)), Parameters("x", "b"))
     val swiftAction = WhiskAction(namespace, aname(), swift3(nonInlinedCode(entityStore)), Parameters("x", "b"))
-    val actions = Seq((javaAction, JAVA_DEFAULT), (nodeAction, NODEJS6), (swiftAction, SWIFT3))
+    val bbAction = WhiskAction(namespace, aname(), bb("bb", nonInlinedCode(entityStore), Some("bbMain")))
+    val actions = Seq((javaAction, JAVA_DEFAULT), (nodeAction, NODEJS6), (swiftAction, SWIFT3), (bbAction, BLACKBOX))
 
     actions.foreach {
       case (action, kind) =>
@@ -959,121 +958,112 @@ class ActionsApiTests extends ControllerTestCommon with WhiskActionsApi {
 
   it should "get an action with attachment that is not cached" in {
     implicit val tid = transid()
-    val code = nonInlinedCode(entityStore)
-    val action =
-      WhiskAction(namespace, aname(), javaDefault(code, Some("hello")), annotations = Parameters("exec", "java"))
-    val content = WhiskActionPut(
-      Some(action.exec),
-      Some(action.parameters),
-      Some(ActionLimitsOption(Some(action.limits.timeout), Some(action.limits.memory), Some(action.limits.logs))))
-    val name = action.name
-    val cacheKey = s"${CacheKey(action)}".replace("(", "\\(").replace(")", "\\)")
-    val expectedGetLog = Seq(
-      s"finding document: 'id: ${action.namespace}/${action.name}",
-      s"finding attachment '[\\w-/:]+' of document 'id: ${action.namespace}/${action.name}").mkString("(?s).*")
+    val nodeAction = WhiskAction(namespace, aname(), jsDefault(nonInlinedCode(entityStore)), Parameters("x", "b"))
+    val swiftAction = WhiskAction(namespace, aname(), swift3(nonInlinedCode(entityStore)), Parameters("x", "b"))
+    val bbAction = WhiskAction(namespace, aname(), bb("bb", nonInlinedCode(entityStore), Some("bbMain")))
+    val actions = Seq((nodeAction, NODEJS6), (swiftAction, SWIFT3), (bbAction, BLACKBOX))
 
-    action.exec match {
-      case exec @ CodeExecAsAttachment(_, _, _, binary) =>
-        val bytes = if (binary) Base64.getDecoder().decode(code) else code.getBytes("UTF-8")
-        val stream = new ByteArrayInputStream(bytes)
-        val manifest = exec.manifest.attached.get
-        val src = StreamConverters.fromInputStream(() => stream)
-        putAndAttach[WhiskAction, WhiskEntity](
-          entityStore,
-          action,
-          (d, a) => d.copy(exec = exec.attach(a)).revision[WhiskAction](d.rev),
-          manifest.attachmentType,
-          src,
-          None)
+    actions.foreach {
+      case (action, kind) =>
+        val content = WhiskActionPut(
+          Some(action.exec),
+          Some(action.parameters),
+          Some(ActionLimitsOption(Some(action.limits.timeout), Some(action.limits.memory), Some(action.limits.logs))))
+        val name = action.name
+        val cacheKey = s"${CacheKey(action)}".replace("(", "\\(").replace(")", "\\)")
+        val expectedGetLog = Seq(
+          s"finding document: 'id: ${action.namespace}/${action.name}",
+          s"finding attachment '[\\w-/:]+' of document 'id: ${action.namespace}/${action.name}").mkString("(?s).*")
 
-      case _ =>
-    }
+        Put(s"$collectionPath/$name", content) ~> Route.seal(routes(creds)(transid())) ~> check {
+          status should be(OK)
+        }
 
-    // second request should fetch from cache
-    Get(s"$collectionPath/$name") ~> Route.seal(routes(creds)(transid())) ~> check {
-      status should be(OK)
-      val response = responseAs[WhiskAction]
-      response should be(
-        WhiskAction(
-          action.namespace,
-          action.name,
-          action.exec,
-          action.parameters,
-          action.limits,
-          action.version,
-          action.publish,
-          action.annotations ++ Parameters(WhiskAction.execFieldName, JAVA_DEFAULT)))
-    }
+        removeFromCache(action, WhiskAction)
 
-    stream.toString should include regex (expectedGetLog)
-    stream.reset()
+        // second request should not fetch from cache
+        Get(s"$collectionPath/$name") ~> Route.seal(routes(creds)(transid())) ~> check {
+          status should be(OK)
+          val response = responseAs[WhiskAction]
+          response should be(
+            WhiskAction(
+              action.namespace,
+              action.name,
+              action.exec,
+              action.parameters,
+              action.limits,
+              action.version,
+              action.publish,
+              action.annotations ++ Parameters(WhiskAction.execFieldName, kind)))
+        }
+
+        stream.toString should include regex (expectedGetLog)
+        stream.reset()
+    }
   }
 
   it should "update an existing action with attachment that is not cached" in {
     implicit val tid = transid()
-    val code = nonInlinedCode(entityStore)
-    val action =
-      WhiskAction(namespace, aname(), javaDefault(code, Some("hello")), annotations = Parameters("exec", "java"))
-    val content = WhiskActionPut(
-      Some(action.exec),
-      Some(action.parameters),
-      Some(ActionLimitsOption(Some(action.limits.timeout), Some(action.limits.memory), Some(action.limits.logs))))
-    val name = action.name
-    val cacheKey = s"${CacheKey(action)}".replace("(", "\\(").replace(")", "\\)")
-    val expectedPutLog =
-      Seq(s"uploading attachment '[\\w-/:]+' of document 'id: ${action.namespace}/${action.name}", s"caching $cacheKey")
-        .mkString("(?s).*")
+    val nodeAction = WhiskAction(namespace, aname(), jsDefault(nonInlinedCode(entityStore)), Parameters("x", "b"))
+    val swiftAction = WhiskAction(namespace, aname(), swift3(nonInlinedCode(entityStore)), Parameters("x", "b"))
+    val bbAction = WhiskAction(namespace, aname(), bb("bb", nonInlinedCode(entityStore), Some("bbMain")))
+    val actions = Seq((nodeAction, NODEJS6), (swiftAction, SWIFT3), (bbAction, BLACKBOX))
 
-    action.exec match {
-      case exec @ CodeExecAsAttachment(_, _, _, _) =>
-        val stream = new ByteArrayInputStream(code.getBytes)
-        val manifest = exec.manifest.attached.get
-        val src = StreamConverters.fromInputStream(() => stream)
-        putAndAttach[WhiskAction, WhiskEntity](
-          entityStore,
-          action,
-          (d, a) => d.copy(exec = exec.attach(a)).revision[WhiskAction](d.rev),
-          manifest.attachmentType,
-          src,
-          None)
+    actions.foreach {
+      case (action, kind) =>
+        val content = WhiskActionPut(
+          Some(action.exec),
+          Some(action.parameters),
+          Some(ActionLimitsOption(Some(action.limits.timeout), Some(action.limits.memory), Some(action.limits.logs))))
+        val name = action.name
+        val cacheKey = s"${CacheKey(action)}".replace("(", "\\(").replace(")", "\\)")
+        val expectedPutLog =
+          Seq(
+            s"uploading attachment '[\\w-/:]+' of document 'id: ${action.namespace}/${action.name}",
+            s"caching $cacheKey")
+            .mkString("(?s).*")
 
-      case _ =>
-    }
+        Put(s"$collectionPath/$name", content) ~> Route.seal(routes(creds)(transid())) ~> check {
+          status should be(OK)
+        }
 
-    Put(s"$collectionPath/$name?overwrite=true", content) ~> Route.seal(routes(creds)(transid())) ~> check {
-      status should be(OK)
-      val response = responseAs[WhiskAction]
-      response should be(
-        WhiskAction(
-          action.namespace,
-          action.name,
-          action.exec,
-          action.parameters,
-          action.limits,
-          action.version.upPatch,
-          action.publish,
-          action.annotations ++ Parameters(WhiskAction.execFieldName, JAVA_DEFAULT)))
-    }
-    stream.toString should include regex (expectedPutLog)
-    stream.reset()
+        removeFromCache(action, WhiskAction)
 
-    // delete should invalidate cache
-    Delete(s"$collectionPath/$name") ~> Route.seal(routes(creds)(transid())) ~> check {
-      status should be(OK)
-      val response = responseAs[WhiskAction]
-      response should be(
-        WhiskAction(
-          action.namespace,
-          action.name,
-          action.exec,
-          action.parameters,
-          action.limits,
-          action.version.upPatch,
-          action.publish,
-          action.annotations ++ Parameters(WhiskAction.execFieldName, JAVA_DEFAULT)))
+        Put(s"$collectionPath/$name?overwrite=true", content) ~> Route.seal(routes(creds)(transid())) ~> check {
+          status should be(OK)
+          val response = responseAs[WhiskAction]
+          response should be(
+            WhiskAction(
+              action.namespace,
+              action.name,
+              action.exec,
+              action.parameters,
+              action.limits,
+              action.version.upPatch,
+              action.publish,
+              action.annotations ++ Parameters(WhiskAction.execFieldName, kind)))
+        }
+        stream.toString should include regex (expectedPutLog)
+        stream.reset()
+
+        // delete should invalidate cache
+        Delete(s"$collectionPath/$name") ~> Route.seal(routes(creds)(transid())) ~> check {
+          status should be(OK)
+          val response = responseAs[WhiskAction]
+          response should be(
+            WhiskAction(
+              action.namespace,
+              action.name,
+              action.exec,
+              action.parameters,
+              action.limits,
+              action.version.upPatch,
+              action.publish,
+              action.annotations ++ Parameters(WhiskAction.execFieldName, kind)))
+        }
+        stream.toString should include(s"invalidating ${CacheKey(action)}")
+        stream.reset()
     }
-    stream.toString should include(s"invalidating ${CacheKey(action)}")
-    stream.reset()
   }
 
   it should "ensure old and new action schemas are supported" in {
diff --git a/tests/src/test/scala/whisk/core/database/test/AttachmentCompatibilityTests.scala b/tests/src/test/scala/whisk/core/database/test/AttachmentCompatibilityTests.scala
index 82b693259a..685f27d145 100644
--- a/tests/src/test/scala/whisk/core/database/test/AttachmentCompatibilityTests.scala
+++ b/tests/src/test/scala/whisk/core/database/test/AttachmentCompatibilityTests.scala
@@ -138,6 +138,21 @@ class AttachmentCompatibilityTests
     codeExec(action2).codeAsJson shouldBe JsString("while (true)")
   }
 
+  it should "read existing simple code string for blackbox action" in {
+    implicit val tid: TransactionId = transid()
+    val exec = """{
+                 |  "kind": "blackbox",
+                 |  "image": "docker-custom.com/openwhisk-runtime/magic/nodejs:0.0.1",
+                 |  "code":  "while (true)",
+                 |  "binary": false
+                 |}""".stripMargin.parseJson.asJsObject
+    val (id, action) = makeActionJson(namespace, aname(), exec)
+    val info = putDoc(id, action)
+
+    val action2 = WhiskAction.get(entityStore, info.id).futureValue
+    codeExec(action2).codeAsJson shouldBe JsString("while (true)")
+  }
+
   private def codeExec(a: WhiskAction) = a.exec.asInstanceOf[CodeExec[_]]
 
   private def makeActionJson(namespace: EntityPath, name: EntityName, exec: JsObject): (String, JsObject) = {
diff --git a/tests/src/test/scala/whisk/core/database/test/DbUtils.scala b/tests/src/test/scala/whisk/core/database/test/DbUtils.scala
index f4a1aac1d5..28a360dc2b 100644
--- a/tests/src/test/scala/whisk/core/database/test/DbUtils.scala
+++ b/tests/src/test/scala/whisk/core/database/test/DbUtils.scala
@@ -350,6 +350,11 @@ trait DbUtils extends Assertions {
   def isMemoryStore(store: ArtifactStore[_]): Boolean = store.isInstanceOf[MemoryArtifactStore[_]]
   def isCouchStore(store: ArtifactStore[_]): Boolean = store.isInstanceOf[CouchDbRestStore[_]]
 
+  protected def removeFromCache[A <: DocumentRevisionProvider](entity: WhiskEntity, factory: DocumentFactory[A])(
+    implicit ec: ExecutionContext): Unit = {
+    factory.removeId(CacheKey(entity))
+  }
+
   protected def randomBytes(size: Int): Array[Byte] = {
     val arr = new Array[Byte](size)
     Random.nextBytes(arr)
diff --git a/tests/src/test/scala/whisk/core/database/test/behavior/ArtifactStoreBehaviorBase.scala b/tests/src/test/scala/whisk/core/database/test/behavior/ArtifactStoreBehaviorBase.scala
index d4bcd5c3d0..8b6e86ee55 100644
--- a/tests/src/test/scala/whisk/core/database/test/behavior/ArtifactStoreBehaviorBase.scala
+++ b/tests/src/test/scala/whisk/core/database/test/behavior/ArtifactStoreBehaviorBase.scala
@@ -125,7 +125,7 @@ trait ArtifactStoreBehaviorBase
     WhiskNamespace(Namespace(EntityName(name), uuid), BasicAuthenticationAuthKey(uuid, Secret()))
   }
 
-  private val exec = BlackBoxExec(ExecManifest.ImageName("image"), None, None, native = false)
+  private val exec = BlackBoxExec(ExecManifest.ImageName("image"), None, None, native = false, binary = false)
 
   protected def newAction(ns: EntityPath): WhiskAction = {
     WhiskAction(ns, aname(), exec)
diff --git a/tests/src/test/scala/whisk/core/entity/test/ExecHelpers.scala b/tests/src/test/scala/whisk/core/entity/test/ExecHelpers.scala
index fa8fd2ec77..7bf608ca5e 100644
--- a/tests/src/test/scala/whisk/core/entity/test/ExecHelpers.scala
+++ b/tests/src/test/scala/whisk/core/entity/test/ExecHelpers.scala
@@ -19,7 +19,6 @@ package whisk.core.entity.test
 
 import org.scalatest.Matchers
 import org.scalatest.Suite
-
 import common.StreamLogging
 import common.WskActorSystem
 import whisk.core.WhiskConfig
@@ -27,7 +26,6 @@ import whisk.core.entity._
 import whisk.core.entity.ArgNormalizer.trim
 import whisk.core.entity.ExecManifest._
 import whisk.core.entity.size._
-
 import spray.json._
 import spray.json.DefaultJsonProtocol._
 
@@ -41,6 +39,7 @@ trait ExecHelpers extends Matchers with WskActorSystem with StreamLogging {
   protected val NODEJS6 = "nodejs:6"
   protected val SWIFT = "swift"
   protected val SWIFT3 = "swift:3.1.1"
+  protected val BLACKBOX = "blackbox"
   protected val SWIFT3_IMAGE = "action-swift-v3.1.1"
   protected val JAVA_DEFAULT = "java"
 
@@ -136,10 +135,13 @@ trait ExecHelpers extends Matchers with WskActorSystem with StreamLogging {
 
   protected def sequenceMetaData(components: Vector[FullyQualifiedEntityName]) = SequenceExecMetaData(components)
 
-  protected def bb(image: String) = BlackBoxExec(ExecManifest.ImageName(trim(image)), None, None, false)
+  protected def bb(image: String) = BlackBoxExec(ExecManifest.ImageName(trim(image)), None, None, false, false)
 
   protected def bb(image: String, code: String, main: Option[String] = None) = {
-    BlackBoxExec(ExecManifest.ImageName(trim(image)), Some(trim(code)).filter(_.nonEmpty), main, false)
+    val (codeOpt, binary) =
+      if (code.trim.nonEmpty) (Some(attFmt[String].read(code.toJson)), Exec.isBinaryCode(code))
+      else (None, false)
+    BlackBoxExec(ExecManifest.ImageName(trim(image)), codeOpt, main, false, binary)
   }
 
   protected def blackBoxMetaData(image: String, main: Option[String] = None, binary: Boolean) = {
diff --git a/tests/src/test/scala/whisk/core/entity/test/ExecTests.scala b/tests/src/test/scala/whisk/core/entity/test/ExecTests.scala
index 48a4b0d7d3..0a6397b193 100644
--- a/tests/src/test/scala/whisk/core/entity/test/ExecTests.scala
+++ b/tests/src/test/scala/whisk/core/entity/test/ExecTests.scala
@@ -17,6 +17,7 @@
 
 package whisk.core.entity.test
 
+import akka.http.scaladsl.model.ContentTypes
 import common.StreamLogging
 import spray.json._
 import org.junit.runner.RunWith
@@ -24,7 +25,8 @@ import org.scalatest.junit.JUnitRunner
 import org.scalatest.{BeforeAndAfterAll, FlatSpec, Matchers}
 import whisk.core.WhiskConfig
 import whisk.core.entity.Attachments.{Attached, Inline}
-import whisk.core.entity.{CodeExecAsAttachment, CodeExecAsString, Exec, ExecManifest, WhiskAction}
+import whisk.core.entity.ExecManifest.ImageName
+import whisk.core.entity.{BlackBoxExec, CodeExecAsAttachment, CodeExecAsString, Exec, ExecManifest, WhiskAction}
 
 import scala.collection.mutable
 
@@ -168,6 +170,131 @@ class ExecTests extends FlatSpec with Matchers with StreamLogging with BeforeAnd
     ExecManifest.initialize(config)
   }
 
+  behavior of "blackbox exec deserialization"
+
+  it should "read existing code string as attachment" in {
+    val json = """{
+                 |  "name": "action_tests_name2",
+                 |  "_id": "anon-Yzycx8QnIYDp3Tby0Fnj23KcMtH/action_tests_name2",
+                 |  "publish": false,
+                 |  "annotations": [],
+                 |  "version": "0.0.1",
+                 |  "updated": 1533623651650,
+                 |  "entityType": "action",
+                 |  "exec": {
+                 |    "kind": "blackbox",
+                 |    "image": "docker-custom.com/openwhisk-runtime/magic/nodejs:0.0.1",
+                 |    "code": "foo",
+                 |    "binary": false
+                 |  },
+                 |  "parameters": [
+                 |    {
+                 |      "key": "x",
+                 |      "value": "b"
+                 |    }
+                 |  ],
+                 |  "limits": {
+                 |    "timeout": 60000,
+                 |    "memory": 256,
+                 |    "logs": 10
+                 |  },
+                 |  "namespace": "anon-Yzycx8QnIYDp3Tby0Fnj23KcMtH"
+                 |}""".stripMargin.parseJson.asJsObject
+    val action = WhiskAction.serdes.read(json)
+    action.exec should matchPattern { case BlackBoxExec(_, Some(Inline("foo")), None, false, false) => }
+  }
+
+  it should "properly determine binary property" in {
+    val j1 = """{
+               |    "kind": "blackbox",
+               |    "image": "docker-custom.com/openwhisk-runtime/magic/nodejs:0.0.1",
+               |    "code": "SGVsbG8gT3BlbldoaXNr",
+               |    "binary": false
+               |}""".stripMargin.parseJson.asJsObject
+    Exec.serdes.read(j1) should matchPattern {
+      case BlackBoxExec(_, Some(Inline("SGVsbG8gT3BlbldoaXNr")), None, false, true) =>
+    }
+
+    val j2 = """{
+               |  "kind": "blackbox",
+               |  "image": "docker-custom.com/openwhisk-runtime/magic/nodejs:0.0.1",
+               |  "code":  "while (true)",
+               |  "binary": false
+               |}""".stripMargin.parseJson.asJsObject
+    Exec.serdes.read(j2) should matchPattern {
+      case BlackBoxExec(_, Some(Inline("while (true)")), None, false, false) =>
+    }
+
+    //Empty code should resolve as None
+    val j3 = """{
+               |  "kind": "blackbox",
+               |  "image": "docker-custom.com/openwhisk-runtime/magic/nodejs:0.0.1",
+               |  "code": " "
+               |}""".stripMargin.parseJson.asJsObject
+    Exec.serdes.read(j3) should matchPattern {
+      case BlackBoxExec(_, None, None, false, false) =>
+    }
+
+    val j4 = """{
+                 |  "kind": "blackbox",
+                 |  "image": "docker-custom.com/openwhisk-runtime/magic/nodejs:0.0.1",
+                 |  "code": {
+                 |    "attachmentName": "foo:bar",
+                 |    "attachmentType": "application/octet-stream",
+                 |    "length": 32768,
+                 |    "digest": "sha256-foo"
+                 |  },
+                 |  "binary": true,
+                 |  "main": "hello"
+                 |}""".stripMargin.parseJson.asJsObject
+    Exec.serdes.read(j4) should matchPattern {
+      case BlackBoxExec(_, Some(Attached("foo:bar", _, Some(32768), Some("sha256-foo"))), Some("hello"), false, true) =>
+    }
+  }
+
+  behavior of "blackbox exec serialization"
+
+  it should "serialize with inline attachment" in {
+    val bb = BlackBoxExec(
+      ImageName.fromString("docker-custom.com/openwhisk-runtime/magic/nodejs:0.0.1").get,
+      Some(Inline("foo")),
+      None,
+      false,
+      false)
+    val js = Exec.serdes.write(bb)
+
+    val js2 = """{
+                |  "kind": "blackbox",
+                |  "image": "docker-custom.com/openwhisk-runtime/magic/nodejs:0.0.1",
+                |  "binary": false,
+                |  "code": "foo"
+                |}""".stripMargin.parseJson.asJsObject
+    js shouldBe js2
+  }
+
+  it should "serialize with attached attachment" in {
+    val bb = BlackBoxExec(
+      ImageName.fromString("docker-custom.com/openwhisk-runtime/magic/nodejs:0.0.1").get,
+      Some(Attached("foo", ContentTypes.`application/octet-stream`, Some(42), Some("sha1-42"))),
+      None,
+      false,
+      true)
+    val js = Exec.serdes.write(bb)
+
+    val js2 = """{
+                |  "kind": "blackbox",
+                |  "image": "docker-custom.com/openwhisk-runtime/magic/nodejs:0.0.1",
+                |  "binary": true,
+                |  "code": {
+                |    "attachmentName": "foo",
+                |    "attachmentType": "application/octet-stream",
+                |    "length": 42,
+                |    "digest": "sha1-42"
+                |  }
+                |}""".stripMargin.parseJson.asJsObject
+    js shouldBe js2
+  }
+
   private class TestConfig(val props: Map[String, String], requiredProperties: Map[String, String])
       extends WhiskConfig(requiredProperties) {
     override protected def getProperties() = mutable.Map(props.toSeq: _*)


 

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
users@infra.apache.org


With regards,
Apache Git Services