This is an automated email from the ASF dual-hosted git repository.

tysonnorris pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-openwhisk.git


The following commit(s) were added to refs/heads/master by this push:
     new e66945a  Make stemcells configurable by deployment  (#3669)
e66945a is described below

commit e66945ac2b579b57d3d0698ee97a36fa9b563369
Author: rodric rabbah <[email protected]>
AuthorDate: Wed May 30 20:14:44 2018 -0400

    Make stemcells configurable by deployment  (#3669)
    
    * Augment RuntimeManifest with stem cell configuration per kind.
    
    * Change StemCell memory type from String to ByteSize.
    
    * Add test for parsing stem cell from manifest and generating an error.
    
    * Construct prewarming configuration from runtime manifest and its stem 
cell declarations.
    
    Co-authored-by: Himavanth <[email protected]>
    Co-authored-by: Rodric Rabbah <rodric.gmail.com>
    
    * Add stem cell configuration for nodejs.
    
    * Do not permit stemcell configuration with 0 or negative counts.
    Provide a factory interface to map stemcells to other types.
    Add more tests for stemcell configuration requirements and to test the 
factory.
    
    * Fix tests.
    
    * Review comments.
    
    * Do not include stem cell details when reporting the runtimes supported by 
a deployment.
---
 ansible/files/runtimes.json                        |   6 +-
 .../scala/whisk/core/entity/ExecManifest.scala     |  65 ++++++---
 .../src/main/scala/whisk/core/entity/Size.scala    |  11 ++
 .../whisk/core/containerpool/ContainerPool.scala   |  56 ++++----
 .../scala/whisk/core/invoker/InvokerReactive.scala |  18 +--
 .../containerpool/test/ContainerPoolTests.scala    |   8 +-
 .../scala/whisk/core/entity/test/ExecHelpers.scala |  15 ++-
 .../whisk/core/entity/test/ExecManifestTests.scala | 145 ++++++++++++++++++++-
 8 files changed, 254 insertions(+), 70 deletions(-)

diff --git a/ansible/files/runtimes.json b/ansible/files/runtimes.json
index 1866b69..44cb30f 100644
--- a/ansible/files/runtimes.json
+++ b/ansible/files/runtimes.json
@@ -14,7 +14,11 @@
                 "image": {
                     "name": "nodejs6action"
                 },
-                "deprecated": false
+                "deprecated": false,
+                "stemCells": [{
+                    "count": 2,
+                    "memory": "256 MB"
+                }]
             },
             {
                 "kind": "nodejs:8",
diff --git a/common/scala/src/main/scala/whisk/core/entity/ExecManifest.scala 
b/common/scala/src/main/scala/whisk/core/entity/ExecManifest.scala
index 668ef60..7436d16 100644
--- a/common/scala/src/main/scala/whisk/core/entity/ExecManifest.scala
+++ b/common/scala/src/main/scala/whisk/core/entity/ExecManifest.scala
@@ -122,6 +122,7 @@ protected[core] object ExecManifest {
    * @param requireMain true iff main entry point is not optional
    * @param sentinelledLogs true iff the runtime generates stdout/stderr log 
sentinels after an activation
    * @param image optional image name, otherwise inferred via fixed mapping 
(remove colons and append 'action')
+   * @param stemCells optional list of stemCells to be initialized by invoker 
per kind
    */
   protected[core] case class RuntimeManifest(kind: String,
                                              image: ImageName,
@@ -129,17 +130,17 @@ protected[core] object ExecManifest {
                                              default: Option[Boolean] = None,
                                              attached: Option[Attached] = None,
                                              requireMain: Option[Boolean] = 
None,
-                                             sentinelledLogs: Option[Boolean] 
= None) {
-
-    protected[entity] def toJsonSummary = {
-      JsObject(
-        "kind" -> kind.toJson,
-        "image" -> image.publicImageName.toJson,
-        "deprecated" -> deprecated.getOrElse(false).toJson,
-        "default" -> default.getOrElse(false).toJson,
-        "attached" -> attached.isDefined.toJson,
-        "requireMain" -> requireMain.getOrElse(false).toJson)
-    }
+                                             sentinelledLogs: Option[Boolean] 
= None,
+                                             stemCells: Option[List[StemCell]] 
= None)
+
+  /**
+   * A stemcell configuration read from the manifest for a container image to 
be initialized by the container pool.
+   *
+   * @param count the number of stemcell containers to create
+   * @param memory the max memory this stemcell will allocate
+   */
+  protected[entity] case class StemCell(count: Int, memory: ByteSize) {
+    require(count > 0, "count must be positive")
   }
 
   /**
@@ -240,6 +241,14 @@ protected[core] object ExecManifest {
 
     val knownContainerRuntimes: Set[String] = 
runtimes.flatMap(_.versions.map(_.kind))
 
+    val manifests: Map[String, RuntimeManifest] = {
+      runtimes.flatMap {
+        _.versions.map { m =>
+          m.kind -> m
+        }
+      }.toMap
+    }
+
     def skipDockerPull(image: ImageName): Boolean = {
       blackboxImages.contains(image) ||
       image.prefix.flatMap(p => bypassPullForLocalImages.map(_ == 
p)).getOrElse(false)
@@ -248,7 +257,16 @@ protected[core] object ExecManifest {
     def toJson: JsObject = {
       runtimes
         .map { family =>
-          family.name -> family.versions.map(_.toJsonSummary)
+          family.name -> family.versions.map {
+            case rt =>
+              JsObject(
+                "kind" -> rt.kind.toJson,
+                "image" -> rt.image.publicImageName.toJson,
+                "deprecated" -> rt.deprecated.getOrElse(false).toJson,
+                "default" -> rt.default.getOrElse(false).toJson,
+                "attached" -> rt.attached.isDefined.toJson,
+                "requireMain" -> rt.requireMain.getOrElse(false).toJson)
+          }
         }
         .toMap
         .toJson
@@ -262,12 +280,17 @@ protected[core] object ExecManifest {
       }
     }
 
-    val manifests: Map[String, RuntimeManifest] = {
-      runtimes.flatMap {
-        _.versions.map { m =>
-          m.kind -> m
+    /**
+     * Collects all runtimes for which there is a stemcell configuration 
defined
+     *
+     * @return list of runtime manifests with stemcell configurations
+     */
+    def stemcells: Map[RuntimeManifest, List[StemCell]] = {
+      manifests
+        .flatMap {
+          case (_, m) => m.stemCells.map(m -> _)
         }
-      }.toMap
+        .filter(_._2.nonEmpty)
     }
 
     private val defaultRuntimes: Map[String, String] = {
@@ -286,5 +309,11 @@ protected[core] object ExecManifest {
   }
 
   protected[entity] implicit val imageNameSerdes = jsonFormat3(ImageName.apply)
-  protected[entity] implicit val runtimeManifestSerdes = 
jsonFormat7(RuntimeManifest)
+
+  protected[entity] implicit val stemCellSerdes = {
+    import whisk.core.entity.size.serdes
+    jsonFormat2(StemCell.apply)
+  }
+
+  protected[entity] implicit val runtimeManifestSerdes = 
jsonFormat8(RuntimeManifest)
 }
diff --git a/common/scala/src/main/scala/whisk/core/entity/Size.scala 
b/common/scala/src/main/scala/whisk/core/entity/Size.scala
index 61e27b4..2d5b7d9 100644
--- a/common/scala/src/main/scala/whisk/core/entity/Size.scala
+++ b/common/scala/src/main/scala/whisk/core/entity/Size.scala
@@ -21,6 +21,8 @@ import java.nio.charset.StandardCharsets
 
 import com.typesafe.config.ConfigValue
 import pureconfig._
+import spray.json._
+import whisk.core.entity.ByteSize.formatError
 
 object SizeUnits extends Enumeration {
 
@@ -124,6 +126,15 @@ object size {
   // Creation of an intermediary Config object is necessary here, since 
"getBytes" is only part of that interface.
   implicit val pureconfigReader =
     ConfigReader[ConfigValue].map(v => 
ByteSize(v.atKey("key").getBytes("key"), SizeUnits.BYTE))
+
+  protected[entity] implicit val serdes = new RootJsonFormat[ByteSize] {
+    def write(b: ByteSize) = JsString(b.toString)
+
+    def read(value: JsValue): ByteSize = value match {
+      case JsString(s) => ByteSize.fromString(s)
+      case _           => deserializationError(formatError)
+    }
+  }
 }
 
 trait SizeConversion {
diff --git 
a/core/invoker/src/main/scala/whisk/core/containerpool/ContainerPool.scala 
b/core/invoker/src/main/scala/whisk/core/containerpool/ContainerPool.scala
index 1835569..d26ebdc 100644
--- a/core/invoker/src/main/scala/whisk/core/containerpool/ContainerPool.scala
+++ b/core/invoker/src/main/scala/whisk/core/containerpool/ContainerPool.scala
@@ -18,15 +18,9 @@
 package whisk.core.containerpool
 
 import scala.collection.immutable
-
 import whisk.common.{AkkaLogging, LoggingMarkers, TransactionId}
-
 import akka.actor.{Actor, ActorRef, ActorRefFactory, Props}
-
-import whisk.core.entity.ByteSize
-import whisk.core.entity.CodeExec
-import whisk.core.entity.EntityName
-import whisk.core.entity.ExecutableWhiskAction
+import whisk.core.entity._
 import whisk.core.entity.size._
 import whisk.core.connector.MessageFeed
 
@@ -60,7 +54,7 @@ case class WorkerData(data: ContainerData, state: WorkerState)
  */
 class ContainerPool(childFactory: ActorRefFactory => ActorRef,
                     feed: ActorRef,
-                    prewarmConfig: Option[PrewarmingConfig] = None,
+                    prewarmConfig: List[PrewarmingConfig] = List.empty,
                     poolConfig: ContainerPoolConfig)
     extends Actor {
   implicit val logging = new AkkaLogging(context.system.log)
@@ -71,7 +65,8 @@ class ContainerPool(childFactory: ActorRefFactory => ActorRef,
   val logMessageInterval = 10.seconds
 
   prewarmConfig.foreach { config =>
-    logging.info(this, s"pre-warming ${config.count} ${config.exec.kind} 
containers")(TransactionId.invokerWarmup)
+    logging.info(this, s"pre-warming ${config.count} ${config.exec.kind} 
${config.memoryLimit.toString}")(
+      TransactionId.invokerWarmup)
     (1 to config.count).foreach { _ =>
       prewarmContainer(config.exec, config.memoryLimit)
     }
@@ -204,26 +199,25 @@ class ContainerPool(childFactory: ActorRefFactory => 
ActorRef,
    * @param kind the kind you want to invoke
    * @return the container iff found
    */
-  def takePrewarmContainer(action: ExecutableWhiskAction): Option[(ActorRef, 
ContainerData)] =
-    prewarmConfig.flatMap { config =>
-      val kind = action.exec.kind
-      val memory = action.limits.memory.megabytes.MB
-      prewarmedPool
-        .find {
-          case (_, PreWarmedData(_, `kind`, `memory`)) => true
-          case _                                       => false
-        }
-        .map {
-          case (ref, data) =>
-            // Move the container to the usual pool
-            freePool = freePool + (ref -> data)
-            prewarmedPool = prewarmedPool - ref
-            // Create a new prewarm container
-            prewarmContainer(config.exec, config.memoryLimit)
-
-            (ref, data)
-        }
-    }
+  def takePrewarmContainer(action: ExecutableWhiskAction): Option[(ActorRef, 
ContainerData)] = {
+    val kind = action.exec.kind
+    val memory = action.limits.memory.megabytes.MB
+    prewarmedPool
+      .find {
+        case (_, PreWarmedData(_, `kind`, `memory`)) => true
+        case _                                       => false
+      }
+      .map {
+        case (ref, data) =>
+          // Move the container to the usual pool
+          freePool = freePool + (ref -> data)
+          prewarmedPool = prewarmedPool - ref
+          // Create a new prewarm container
+          // NOTE: prewarming ignores the action code in exec, but this is 
dangerous as the field is accessible to the factory
+          prewarmContainer(action.exec, memory)
+          (ref, data)
+      }
+  }
 
   /** Removes a container and updates state accordingly. */
   def removeContainer(toDelete: ActorRef) = {
@@ -282,9 +276,9 @@ object ContainerPool {
   def props(factory: ActorRefFactory => ActorRef,
             poolConfig: ContainerPoolConfig,
             feed: ActorRef,
-            prewarmConfig: Option[PrewarmingConfig] = None) =
+            prewarmConfig: List[PrewarmingConfig] = List.empty) =
     Props(new ContainerPool(factory, feed, prewarmConfig, poolConfig))
 }
 
-/** Contains settings needed to perform container prewarming */
+/** Contains settings needed to perform container prewarming. */
 case class PrewarmingConfig(count: Int, exec: CodeExec[_], memoryLimit: 
ByteSize)
diff --git 
a/core/invoker/src/main/scala/whisk/core/invoker/InvokerReactive.scala 
b/core/invoker/src/main/scala/whisk/core/invoker/InvokerReactive.scala
index b132dd8..20cbbd4 100644
--- a/core/invoker/src/main/scala/whisk/core/invoker/InvokerReactive.scala
+++ b/core/invoker/src/main/scala/whisk/core/invoker/InvokerReactive.scala
@@ -33,7 +33,6 @@ import whisk.core.containerpool._
 import whisk.core.containerpool.logging.LogStoreProvider
 import whisk.core.database._
 import whisk.core.entity._
-import whisk.core.entity.size._
 import whisk.http.Messages
 import whisk.spi.SpiLoader
 
@@ -173,14 +172,17 @@ class InvokerReactive(
       ContainerProxy
         .props(containerFactory.createContainer, ack, store, 
logsProvider.collectLogs, instance, poolConfig))
 
-  private val prewarmKind = "nodejs:6"
-  private val prewarmExec = ExecManifest.runtimesManifest
-    .resolveDefaultRuntime(prewarmKind)
-    .map(manifest => CodeExecAsString(manifest, "", None))
-    .get
+  val prewarmingConfigs: List[PrewarmingConfig] = {
+    ExecManifest.runtimesManifest.stemcells.flatMap {
+      case (mf, cells) =>
+        cells.map { cell =>
+          PrewarmingConfig(cell.count, new CodeExecAsString(mf, "", None), 
cell.memory)
+        }
+    }.toList
+  }
 
-  private val pool = actorSystem.actorOf(
-    ContainerPool.props(childFactory, poolConfig, activationFeed, 
Some(PrewarmingConfig(2, prewarmExec, 256.MB))))
+  private val pool =
+    actorSystem.actorOf(ContainerPool.props(childFactory, poolConfig, 
activationFeed, prewarmingConfigs))
 
   /** Is called when an ActivationMessage is read from Kafka */
   def processActivationMessage(bytes: Array[Byte]): Future[Unit] = {
diff --git 
a/tests/src/test/scala/whisk/core/containerpool/test/ContainerPoolTests.scala 
b/tests/src/test/scala/whisk/core/containerpool/test/ContainerPoolTests.scala
index b61e6f8..3fe1253 100644
--- 
a/tests/src/test/scala/whisk/core/containerpool/test/ContainerPoolTests.scala
+++ 
b/tests/src/test/scala/whisk/core/containerpool/test/ContainerPoolTests.scala
@@ -240,7 +240,7 @@ class ContainerPoolTests
 
     val pool =
       system.actorOf(
-        ContainerPool.props(factory, ContainerPoolConfig(0, 0), feed.ref, 
Some(PrewarmingConfig(1, exec, memoryLimit))))
+        ContainerPool.props(factory, ContainerPoolConfig(0, 0), feed.ref, 
List(PrewarmingConfig(1, exec, memoryLimit))))
     containers(0).expectMsg(Start(exec, memoryLimit))
   }
 
@@ -250,7 +250,7 @@ class ContainerPoolTests
 
     val pool =
       system.actorOf(
-        ContainerPool.props(factory, ContainerPoolConfig(1, 1), feed.ref, 
Some(PrewarmingConfig(1, exec, memoryLimit))))
+        ContainerPool.props(factory, ContainerPoolConfig(1, 1), feed.ref, 
List(PrewarmingConfig(1, exec, memoryLimit))))
     containers(0).expectMsg(Start(exec, memoryLimit))
     containers(0).send(pool, NeedWork(preWarmedData(exec.kind)))
     pool ! runMessage
@@ -265,7 +265,7 @@ class ContainerPoolTests
 
     val pool = system.actorOf(
       ContainerPool
-        .props(factory, ContainerPoolConfig(1, 1), feed.ref, 
Some(PrewarmingConfig(1, alternativeExec, memoryLimit))))
+        .props(factory, ContainerPoolConfig(1, 1), feed.ref, 
List(PrewarmingConfig(1, alternativeExec, memoryLimit))))
     containers(0).expectMsg(Start(alternativeExec, memoryLimit)) // container0 
was prewarmed
     containers(0).send(pool, NeedWork(preWarmedData(alternativeExec.kind)))
     pool ! runMessage
@@ -281,7 +281,7 @@ class ContainerPoolTests
     val pool =
       system.actorOf(
         ContainerPool
-          .props(factory, ContainerPoolConfig(1, 1), feed.ref, 
Some(PrewarmingConfig(1, exec, alternativeLimit))))
+          .props(factory, ContainerPoolConfig(1, 1), feed.ref, 
List(PrewarmingConfig(1, exec, alternativeLimit))))
     containers(0).expectMsg(Start(exec, alternativeLimit)) // container0 was 
prewarmed
     containers(0).send(pool, NeedWork(preWarmedData(exec.kind, 
alternativeLimit)))
     pool ! runMessage
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 688fcd5..7ff2810 100644
--- a/tests/src/test/scala/whisk/core/entity/test/ExecHelpers.scala
+++ b/tests/src/test/scala/whisk/core/entity/test/ExecHelpers.scala
@@ -26,6 +26,7 @@ import whisk.core.WhiskConfig
 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._
@@ -59,7 +60,12 @@ trait ExecHelpers extends Matchers with WskActorSystem with 
StreamLogging {
 
   protected def js6(code: String, main: Option[String] = None) = {
     CodeExecAsString(
-      RuntimeManifest(NODEJS6, imagename(NODEJS6), default = Some(true), 
deprecated = Some(false)),
+      RuntimeManifest(
+        NODEJS6,
+        imagename(NODEJS6),
+        default = Some(true),
+        deprecated = Some(false),
+        stemCells = Some(List(StemCell(2, 256.MB)))),
       trim(code),
       main.map(_.trim))
   }
@@ -70,7 +76,12 @@ trait ExecHelpers extends Matchers with WskActorSystem with 
StreamLogging {
 
   protected def js6MetaData(main: Option[String] = None, binary: Boolean) = {
     CodeExecMetaDataAsString(
-      RuntimeManifest(NODEJS6, imagename(NODEJS6), default = Some(true), 
deprecated = Some(false)),
+      RuntimeManifest(
+        NODEJS6,
+        imagename(NODEJS6),
+        default = Some(true),
+        deprecated = Some(false),
+        stemCells = Some(List(StemCell(2, 256.MB)))),
       binary,
       main.map(_.trim))
   }
diff --git 
a/tests/src/test/scala/whisk/core/entity/test/ExecManifestTests.scala 
b/tests/src/test/scala/whisk/core/entity/test/ExecManifestTests.scala
index 71f0237..690e449 100644
--- a/tests/src/test/scala/whisk/core/entity/test/ExecManifestTests.scala
+++ b/tests/src/test/scala/whisk/core/entity/test/ExecManifestTests.scala
@@ -19,12 +19,14 @@ package whisk.core.entity.test
 
 import common.{StreamLogging, WskActorSystem}
 import org.junit.runner.RunWith
-import org.scalatest.{FlatSpec, Matchers}
 import org.scalatest.junit.JUnitRunner
-import spray.json.DefaultJsonProtocol._
+import org.scalatest.{FlatSpec, Matchers}
 import spray.json._
+import spray.json.DefaultJsonProtocol._
 import whisk.core.entity.ExecManifest
 import whisk.core.entity.ExecManifest._
+import whisk.core.entity.size._
+import whisk.core.entity.ByteSize
 
 import scala.util.Success
 
@@ -63,10 +65,11 @@ class ExecManifestTests extends FlatSpec with 
WskActorSystem with StreamLogging
     val k1 = RuntimeManifest("k1", ImageName("???"))
     val k2 = RuntimeManifest("k2", ImageName("???"), default = Some(true))
     val p1 = RuntimeManifest("p1", ImageName("???"))
-    val mf = manifestFactory(JsObject("ks" -> Set(k1, k2).toJson, "p1" -> 
Set(p1).toJson))
+    val s1 = RuntimeManifest("s1", ImageName("???"), stemCells = 
Some(List(StemCell(2, 256.MB))))
+    val mf = manifestFactory(JsObject("ks" -> Set(k1, k2).toJson, "p1" -> 
Set(p1).toJson, "s1" -> Set(s1).toJson))
     val runtimes = ExecManifest.runtimes(mf, RuntimeManifestConfig()).get
 
-    Seq("k1", "k2", "p1").foreach {
+    Seq("k1", "k2", "p1", "s1").foreach {
       runtimes.knownContainerRuntimes.contains(_) shouldBe true
     }
 
@@ -75,9 +78,11 @@ class ExecManifestTests extends FlatSpec with WskActorSystem 
with StreamLogging
     runtimes.resolveDefaultRuntime("k1") shouldBe Some(k1)
     runtimes.resolveDefaultRuntime("k2") shouldBe Some(k2)
     runtimes.resolveDefaultRuntime("p1") shouldBe Some(p1)
+    runtimes.resolveDefaultRuntime("s1") shouldBe Some(s1)
 
     runtimes.resolveDefaultRuntime("ks:default") shouldBe Some(k2)
     runtimes.resolveDefaultRuntime("p1:default") shouldBe Some(p1)
+    runtimes.resolveDefaultRuntime("s1:default") shouldBe Some(s1)
   }
 
   it should "read a valid configuration without default prefix, default tag" 
in {
@@ -85,9 +90,15 @@ class ExecManifestTests extends FlatSpec with WskActorSystem 
with StreamLogging
     val i2 = RuntimeManifest("i2", ImageName("???", Some("ppp")), default = 
Some(true))
     val j1 = RuntimeManifest("j1", ImageName("???", Some("ppp"), Some("ttt")))
     val k1 = RuntimeManifest("k1", ImageName("???", None, Some("ttt")))
+    val s1 = RuntimeManifest("s1", ImageName("???"), stemCells = 
Some(List(StemCell(2, 256.MB))))
 
     val mf =
-      JsObject("runtimes" -> JsObject("is" -> Set(i1, i2).toJson, "js" -> 
Set(j1).toJson, "ks" -> Set(k1).toJson))
+      JsObject(
+        "runtimes" -> JsObject(
+          "is" -> Set(i1, i2).toJson,
+          "js" -> Set(j1).toJson,
+          "ks" -> Set(k1).toJson,
+          "ss" -> Set(s1).toJson))
     val rmc = RuntimeManifestConfig(defaultImagePrefix = Some("pre"), 
defaultImageTag = Some("test"))
     val runtimes = ExecManifest.runtimes(mf, rmc).get
 
@@ -95,6 +106,9 @@ class ExecManifestTests extends FlatSpec with WskActorSystem 
with StreamLogging
     runtimes.resolveDefaultRuntime("i2").get.image.publicImageName shouldBe 
"ppp/???:test"
     runtimes.resolveDefaultRuntime("j1").get.image.publicImageName shouldBe 
"ppp/???:ttt"
     runtimes.resolveDefaultRuntime("k1").get.image.publicImageName shouldBe 
"pre/???:ttt"
+    runtimes.resolveDefaultRuntime("s1").get.image.publicImageName shouldBe 
"pre/???:test"
+    runtimes.resolveDefaultRuntime("s1").get.stemCells.get(0).count shouldBe 2
+    runtimes.resolveDefaultRuntime("s1").get.stemCells.get(0).memory shouldBe 
256.MB
   }
 
   it should "read a valid configuration with blackbox images but without 
default prefix or tag" in {
@@ -143,7 +157,7 @@ class ExecManifestTests extends FlatSpec with 
WskActorSystem with StreamLogging
     an[IllegalArgumentException] should be thrownBy ExecManifest.runtimes(mf, 
RuntimeManifestConfig()).get
   }
 
-  it should "reject finding a default when none is specified for multiple 
versions" in {
+  it should "reject finding a default when none specified for multiple 
versions in the same family" in {
     val k1 = RuntimeManifest("k1", ImageName("???"))
     val k2 = RuntimeManifest("k2", ImageName("???"))
     val mf = manifestFactory(JsObject("ks" -> Set(k1, k2).toJson))
@@ -180,4 +194,123 @@ class ExecManifestTests extends FlatSpec with 
WskActorSystem with StreamLogging
     manifest.get.skipDockerPull(ImageName(prefix = Some("localpre"), name = 
"y")) shouldBe true
   }
 
+  it should "de/serialize stem cell configuration" in {
+    val cell = StemCell(3, 128.MB)
+    val cellAsJson = JsObject("count" -> JsNumber(3), "memory" -> 
JsString("128 MB"))
+    stemCellSerdes.write(cell) shouldBe cellAsJson
+    stemCellSerdes.read(cellAsJson) shouldBe cell
+
+    an[IllegalArgumentException] shouldBe thrownBy {
+      StemCell(-1, 128.MB)
+    }
+
+    an[IllegalArgumentException] shouldBe thrownBy {
+      StemCell(0, 128.MB)
+    }
+
+    an[IllegalArgumentException] shouldBe thrownBy {
+      val cellAsJson = JsObject("count" -> JsNumber(0), "memory" -> 
JsString("128 MB"))
+      stemCellSerdes.read(cellAsJson)
+    }
+
+    the[IllegalArgumentException] thrownBy {
+      val cellAsJson = JsObject("count" -> JsNumber(1), "memory" -> 
JsString("128"))
+      stemCellSerdes.read(cellAsJson)
+    } should have message {
+      ByteSize.formatError
+    }
+  }
+
+  it should "parse manifest from JSON string" in {
+    val json = """
+                 |{ "runtimes": {
+                 |    "nodef": [
+                 |      {
+                 |        "kind": "nodejs:6",
+                 |        "image": {
+                 |          "name": "nodejsaction"
+                 |        },
+                 |        "stemCells": [{
+                 |          "count": 1,
+                 |          "memory": "128 MB"
+                 |        }]
+                 |      }, {
+                 |        "kind": "nodejs:8",
+                 |        "default": true,
+                 |        "image": {
+                 |          "name": "nodejsaction"
+                 |        },
+                 |        "stemCells": [{
+                 |          "count": 1,
+                 |          "memory": "128 MB"
+                 |        }, {
+                 |          "count": 1,
+                 |          "memory": "256 MB"
+                 |        }]
+                 |      }
+                 |    ],
+                 |    "pythonf": [{
+                 |      "kind": "python",
+                 |      "image": {
+                 |        "name": "pythonaction"
+                 |      },
+                 |      "stemCells": [{
+                 |        "count": 2,
+                 |        "memory": "256 MB"
+                 |      }]
+                 |    }],
+                 |    "swiftf": [{
+                 |      "kind": "swift",
+                 |      "image": {
+                 |        "name": "swiftaction"
+                 |      },
+                 |      "stemCells": []
+                 |    }],
+                 |    "phpf": [{
+                 |      "kind": "php",
+                 |      "image": {
+                 |        "name": "phpaction"
+                 |      }
+                 |    }]
+                 |  }
+                 |}
+                 |""".stripMargin.parseJson.asJsObject
+
+    val js6 = RuntimeManifest("nodejs:6", ImageName("nodejsaction"), stemCells 
= Some(List(StemCell(1, 128.MB))))
+    val js8 = RuntimeManifest(
+      "nodejs:8",
+      ImageName("nodejsaction"),
+      default = Some(true),
+      stemCells = Some(List(StemCell(1, 128.MB), StemCell(1, 256.MB))))
+    val py = RuntimeManifest("python", ImageName("pythonaction"), stemCells = 
Some(List(StemCell(2, 256.MB))))
+    val sw = RuntimeManifest("swift", ImageName("swiftaction"), stemCells = 
Some(List.empty))
+    val ph = RuntimeManifest("php", ImageName("phpaction"))
+    val mf = ExecManifest.runtimes(json, RuntimeManifestConfig()).get
+
+    mf shouldBe {
+      Runtimes(
+        Set(
+          RuntimeFamily("nodef", Set(js6, js8)),
+          RuntimeFamily("pythonf", Set(py)),
+          RuntimeFamily("swiftf", Set(sw)),
+          RuntimeFamily("phpf", Set(ph))),
+        Set.empty,
+        None)
+    }
+
+    def stemCellFactory(m: RuntimeManifest, cells: List[StemCell]) = cells.map 
{ c =>
+      (m.kind, m.image, c.count, c.memory)
+    }
+
+    mf.stemcells.flatMap {
+      case (m, cells) =>
+        cells.map { c =>
+          (m.kind, m.image, c.count, c.memory)
+        }
+    }.toList should contain theSameElementsAs List(
+      (js6.kind, js6.image, 1, 128.MB),
+      (js8.kind, js8.image, 1, 128.MB),
+      (js8.kind, js8.image, 1, 256.MB),
+      (py.kind, py.image, 2, 256.MB))
+  }
 }

-- 
To stop receiving notification emails like this one, please contact
[email protected].

Reply via email to