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/05/31 00:14:46 UTC

[GitHub] tysonnorris closed pull request #3669: Make stemcells configurable by deployment

tysonnorris closed pull request #3669: Make stemcells configurable by deployment 
URL: https://github.com/apache/incubator-openwhisk/pull/3669
 
 
   

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/ansible/files/runtimes.json b/ansible/files/runtimes.json
index 1866b69195..44cb30f2c1 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 668ef609f6..7436d167f0 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 61e27b4faf..2d5b7d9a20 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 183556942e..d26ebdc2f9 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 b132dd815f..20cbbd46cd 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 b61e6f8db0..3fe12538fe 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 688fcd5b7e..7ff281078e 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 71f02379e9..690e449051 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))
+  }
 }


 

----------------------------------------------------------------
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