You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@openwhisk.apache.org by du...@apache.org on 2018/02/06 15:42:17 UTC

[incubator-openwhisk] branch master updated: reduce rule activation records (#3187)

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

dubeejw 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 9ededdf  reduce rule activation records (#3187)
9ededdf is described below

commit 9ededdf82210682726f8800b2c02e68e3234efdf
Author: Mark Deuser <md...@us.ibm.com>
AuthorDate: Tue Feb 6 10:42:12 2018 -0500

    reduce rule activation records (#3187)
    
    * reduce rule activation records
    
    * include action invocation async response in trigger activation logs
    
    * adjust rule tests to have active rules
    
    * trigger activation log format is array of stringified JSON objects
    
    * Add trigger tests to validate trigger activation logs
---
 .../scala/whisk/core/controller/Triggers.scala     | 241 +++++++++++++++------
 .../test/scala/system/basic/WskBasicTests.scala    | 165 ++++++++++++--
 .../src/test/scala/system/basic/WskRuleTests.scala |  36 ++-
 .../scala/whisk/core/cli/test/Swift311Tests.scala  |  11 +
 .../core/controller/test/TriggersApiTests.scala    |  13 +-
 5 files changed, 350 insertions(+), 116 deletions(-)

diff --git a/core/controller/src/main/scala/whisk/core/controller/Triggers.scala b/core/controller/src/main/scala/whisk/core/controller/Triggers.scala
index 044d5a2..f50fdb1 100644
--- a/core/controller/src/main/scala/whisk/core/controller/Triggers.scala
+++ b/core/controller/src/main/scala/whisk/core/controller/Triggers.scala
@@ -19,17 +19,22 @@ package whisk.core.controller
 
 import java.time.{Clock, Instant}
 
+import scala.collection.immutable.Map
+import scala.concurrent.Future
+import scala.util.{Failure, Success}
+
 import akka.actor.ActorSystem
 import akka.http.scaladsl.Http
 import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._
 import akka.http.scaladsl.model.HttpMethods.POST
-import akka.http.scaladsl.model.StatusCodes._
-import akka.http.scaladsl.model.{HttpEntity, HttpRequest, MediaTypes, Uri}
+import akka.http.scaladsl.model.StatusCodes.{Accepted, BadRequest, InternalServerError, OK}
 import akka.http.scaladsl.model.Uri.Path
 import akka.http.scaladsl.model.headers.{Authorization, BasicHttpCredentials}
+import akka.http.scaladsl.model._
 import akka.http.scaladsl.server.{RequestContext, RouteResult}
 import akka.http.scaladsl.unmarshalling.{Unmarshal, Unmarshaller}
 import akka.stream.ActorMaterializer
+import spray.json.DefaultJsonProtocol._
 import spray.json._
 import whisk.common.TransactionId
 import whisk.core.controller.RestApiCommons.ListLimit
@@ -37,8 +42,7 @@ import whisk.core.database.CacheChangeNotification
 import whisk.core.entitlement.Collection
 import whisk.core.entity._
 import whisk.core.entity.types.{ActivationStore, EntityStore}
-
-import scala.concurrent.Future
+import whisk.http.ErrorResponse
 
 /** A trait implementing the triggers API. */
 trait WhiskTriggersApi extends WhiskCollectionAPI {
@@ -59,10 +63,9 @@ trait WhiskTriggersApi extends WhiskCollectionAPI {
   protected val activationStore: ActivationStore
 
   /** JSON response formatter. */
-  import RestApiCommons.jsonDefaultResponsePrinter
-
   /** Path to Triggers REST API. */
   protected val triggersPath = "triggers"
+  protected val url = Uri(s"http://localhost:${whiskConfig.servicePort}")
 
   protected implicit val materializer: ActorMaterializer
 
@@ -107,10 +110,8 @@ trait WhiskTriggersApi extends WhiskCollectionAPI {
     entity(as[Option[JsObject]]) { payload =>
       getEntity(WhiskTrigger, entityStore, entityName.toDocId, Some {
         trigger: WhiskTrigger =>
-          val args = trigger.parameters.merge(payload)
           val triggerActivationId = activationIdFactory.make()
           logging.info(this, s"[POST] trigger activation id: ${triggerActivationId}")
-
           val triggerActivation = WhiskActivation(
             namespace = user.namespace.toPath, // all activations should end up in the one space regardless trigger.namespace,
             entityName.name,
@@ -122,70 +123,42 @@ trait WhiskTriggersApi extends WhiskCollectionAPI {
             version = trigger.version,
             duration = None)
 
-          logging.debug(this, s"[POST] trigger activated, writing activation record to datastore: $triggerActivationId")
-          WhiskActivation.put(activationStore, triggerActivation) recover {
-            case t =>
-              logging.error(this, s"[POST] storing trigger activation $triggerActivationId failed: ${t.getMessage}")
-          }
-
-          val url = Uri(s"http://localhost:${whiskConfig.servicePort}")
-
-          trigger.rules.map {
-            _.filter {
-              case (ruleName, rule) => rule.status == Status.ACTIVE
-            } foreach {
-              case (ruleName, rule) =>
-                val ruleActivationId = activationIdFactory.make()
-                val ruleActivation = WhiskActivation(
-                  namespace = user.namespace.toPath, // all activations should end up in the one space regardless trigger.namespace,
-                  ruleName.name,
-                  user.subject,
-                  ruleActivationId,
-                  Instant.now(Clock.systemUTC()),
-                  Instant.EPOCH,
-                  cause = Some(triggerActivationId),
-                  response = ActivationResponse.success(),
-                  version = trigger.version,
-                  duration = None)
-                WhiskActivation.put(activationStore, ruleActivation) recover {
-                  case t =>
-                    logging.error(this, s"[POST] storing rule activation $ruleActivationId failed: ${t.getMessage}")
-                }
-
-                val actionNamespace = rule.action.path.root.asString
-                val actionPath = {
-                  rule.action.path.relativePath.map { pkg =>
-                    (Path.SingleSlash + pkg.namespace) / rule.action.name.asString
-                  } getOrElse {
-                    Path.SingleSlash + rule.action.name.asString
+          // List of active rules associated with the trigger
+          val activeRules: Map[FullyQualifiedEntityName, ReducedRule] =
+            trigger.rules.map(_.filter(_._2.status == Status.ACTIVE)).getOrElse(Map.empty)
+
+          if (activeRules.nonEmpty) {
+            val args: JsObject = trigger.parameters.merge(payload).getOrElse(JsObject())
+            val actionLogList: Iterable[Future[JsObject]] = activateRules(user, args, activeRules)
+
+            // For each of the action activation results, generate a log message to attach to the trigger activation
+            Future
+              .sequence(actionLogList)
+              .map(_.map(_.compactPrint))
+              .onComplete {
+                case Success(triggerLogs) =>
+                  val triggerActivationDoc = triggerActivation.withLogs(ActivationLogs(triggerLogs.toVector))
+                  logging
+                    .debug(
+                      this,
+                      s"[POST] trigger activated, writing activation record to datastore: $triggerActivationId")
+                  WhiskActivation.put(activationStore, triggerActivationDoc) recover {
+                    case t =>
+                      logging
+                        .error(this, s"[POST] storing trigger activation $triggerActivationId failed: ${t.getMessage}")
                   }
-                }.toString
-
-                val actionUrl = Path("/api/v1") / "namespaces" / actionNamespace / "actions"
-                val request = HttpRequest(
-                  method = POST,
-                  uri = url.withPath(actionUrl + actionPath),
-                  headers =
-                    List(Authorization(BasicHttpCredentials(user.authkey.uuid.asString, user.authkey.key.asString))),
-                  entity = HttpEntity(MediaTypes.`application/json`, args.getOrElse(JsObject()).compactPrint))
-
-                Http().singleRequest(request).map {
-                  response =>
-                    response.status match {
-                      case OK | Accepted =>
-                        Unmarshal(response.entity).to[JsObject].map { a =>
-                          logging.info(this, s"${rule.action} activated ${a.fields("activationId")}")
-                        }
-                      case NotFound =>
-                        response.discardEntityBytes()
-                        logging.debug(this, s"${rule.action} failed, action not found")
-                      case _ =>
-                        Unmarshal(response.entity).to[String].map { error =>
-                          logging.warn(this, s"${rule.action} failed due to $error")
-                        }
-                    }
-                }
-            }
+                case Failure(e) =>
+                  logging.error(this, s"Failed to write action activation results to trigger activation: $e")
+                  logging
+                    .info(
+                      this,
+                      s"[POST] trigger activated, writing activation record to datastore: $triggerActivationId")
+                  WhiskActivation.put(activationStore, triggerActivation) recover {
+                    case t =>
+                      logging
+                        .error(this, s"[POST] storing trigger activation $triggerActivationId failed: ${t.getMessage}")
+                  }
+              }
           }
 
           complete(Accepted, triggerActivationId.toJsObject)
@@ -326,6 +299,132 @@ trait WhiskTriggersApi extends WhiskCollectionAPI {
     complete(OK, trigger.withoutRules)
   }
 
+  /**
+   * Iterates through each active rule and invoke each mapped action.
+   */
+  private def activateRules(user: Identity,
+                            args: JsObject,
+                            rulesToActivate: Map[FullyQualifiedEntityName, ReducedRule])(
+    implicit transid: TransactionId): Iterable[Future[JsObject]] = {
+    rulesToActivate.map {
+      case (ruleName, rule) =>
+        // Invoke the action. Retain action results for inclusion in the trigger activation record
+        val actionActivationResult: Future[JsObject] = postActivation(user, rule, args)
+          .flatMap { response =>
+            response.status match {
+              case OK | Accepted =>
+                Unmarshal(response.entity).to[JsObject].map { activationResponse =>
+                  val activationId: JsValue = activationResponse.fields("activationId")
+                  logging.debug(this, s"trigger-fired action '${rule.action}' invoked with activation $activationId")
+                  ruleResult(ActivationResponse.Success, ruleName, rule.action, Some(activationId))
+                }
+
+              // all proper controller responses are JSON objects that deserialize to an ErrorResponse instance
+              case code if (response.entity.contentType == ContentTypes.`application/json`) =>
+                Unmarshal(response.entity).to[ErrorResponse].map { e =>
+                  val statusCode =
+                    if (code != InternalServerError) {
+                      logging
+                        .debug(
+                          this,
+                          s"trigger-fired action '${rule.action}' failed to invoke with ${e.error}, ${e.code}")
+                      ActivationResponse.ApplicationError
+                    } else {
+                      logging
+                        .error(
+                          this,
+                          s"trigger-fired action '${rule.action}' failed to invoke with ${e.error}, ${e.code}")
+                      ActivationResponse.WhiskError
+                    }
+                  ruleResult(statusCode, ruleName, rule.action, errorMsg = Some(e.error))
+                }
+
+              case code =>
+                logging.error(this, s"trigger-fired action '${rule.action}' failed to invoke with status code $code")
+                Unmarshal(response.entity).to[String].map { error =>
+                  ruleResult(ActivationResponse.WhiskError, ruleName, rule.action, errorMsg = Some(error))
+                }
+            }
+          }
+          .recover {
+            case t =>
+              logging.error(this, s"trigger-fired action '${rule.action}' failed to invoke with $t")
+              ruleResult(
+                ActivationResponse.WhiskError,
+                ruleName,
+                rule.action,
+                errorMsg = Some(InternalServerError.defaultMessage))
+          }
+
+        actionActivationResult
+    }
+  }
+
+  /**
+   * Posts an action activation. Currently done by posting internally to the controller.
+   * TODO: use a poper path that does not route through HTTP.
+   *
+   * @param rule the name of the rule that is activated
+   * @param args the arguments to post to the action
+   * @return a future with the HTTP response from the action activation
+   */
+  private def postActivation(user: Identity, rule: ReducedRule, args: JsObject): Future[HttpResponse] = {
+    // Build the url to invoke an action mapped to the rule
+    val actionUrl = baseControllerPath / rule.action.path.root.asString / "actions"
+
+    val actionPath = {
+      rule.action.path.relativePath.map { pkg =>
+        (Path.SingleSlash + pkg.namespace) / rule.action.name.asString
+      } getOrElse {
+        Path.SingleSlash + rule.action.name.asString
+      }
+    }.toString
+
+    val request = HttpRequest(
+      method = POST,
+      uri = url.withPath(actionUrl + actionPath),
+      headers = List(Authorization(BasicHttpCredentials(user.authkey.uuid.asString, user.authkey.key.asString))),
+      entity = HttpEntity(MediaTypes.`application/json`, args.compactPrint))
+
+    Http().singleRequest(request)
+  }
+
+  /**
+   * Create JSON object containing the pertinent rule activation details.
+   * {
+   *   "rule": "my-rule",
+   *   "action": "my-action",
+   *   "statusCode": 0,
+   *   "status": "success",
+   *   "activationId": "...",                              // either this field, ...
+   *   "error": "The requested resource does not exist."   // ... or this field will be present
+   * }
+   *
+   * @param statusCode one of ActivationResponse values
+   * @param ruleName the name of the rule that was activated
+   * @param actionName the name of the action activated by the rule
+   * @param actionActivationId the activation id, if there is one
+   * @param errorMsg the rror messages otherwise
+   * @return JsObject as formatted above
+   */
+  private def ruleResult(statusCode: Int,
+                         ruleName: FullyQualifiedEntityName,
+                         actionName: FullyQualifiedEntityName,
+                         actionActivationId: Option[JsValue] = None,
+                         errorMsg: Option[String] = None): JsObject = {
+    JsObject(
+      Map(
+        "rule" -> JsString(ruleName.asString),
+        "action" -> JsString(actionName.asString),
+        "statusCode" -> JsNumber(statusCode),
+        "success" -> JsBoolean(statusCode == ActivationResponse.Success)) ++
+        actionActivationId.map("activationId" -> _.toJson) ++
+        errorMsg.map("error" -> JsString(_)))
+  }
+
+  /** Common base bath for the controller, used by internal action activation mechanism. */
+  private val baseControllerPath = Path("/api/v1/namespaces")
+
   /** Custom unmarshaller for query parameters "limit" for "list" operations. */
   private implicit val stringToListLimit: Unmarshaller[String, ListLimit] = RestApiCommons.stringToListLimit(collection)
 }
diff --git a/tests/src/test/scala/system/basic/WskBasicTests.scala b/tests/src/test/scala/system/basic/WskBasicTests.scala
index 1696fee..8e68d29 100644
--- a/tests/src/test/scala/system/basic/WskBasicTests.scala
+++ b/tests/src/test/scala/system/basic/WskBasicTests.scala
@@ -47,6 +47,11 @@ class WskBasicTests extends TestHelpers with WskTestHelpers {
   val wsk: common.rest.WskRest = new WskRest
   val defaultAction: Some[String] = Some(TestUtils.getTestActionFilename("hello.js"))
 
+  /**
+   * Append the current timestamp in ms
+   */
+  def withTimestamp(text: String) = s"${text}-${System.currentTimeMillis}"
+
   behavior of "Wsk REST"
 
   it should "reject creating duplicate entity" in withAssetCleaner(wskprops) { (wp, assetHelper) =>
@@ -484,26 +489,53 @@ class WskBasicTests extends TestHelpers with WskTestHelpers {
   behavior of "Wsk Trigger REST"
 
   it should "create, update, get, fire and list trigger" in withAssetCleaner(wskprops) { (wp, assetHelper) =>
-    val name = "listTriggers"
+    val ruleName = withTimestamp("r1toa1")
+    val triggerName = withTimestamp("t1tor1")
+    val actionName = withTimestamp("a1")
     val params = Map("a" -> "A".toJson)
-    assetHelper.withCleaner(wsk.trigger, name) { (trigger, _) =>
-      trigger.create(name, parameters = params)
-      trigger.create(name, update = true)
+    val ns = wsk.namespace.whois()
+
+    assetHelper.withCleaner(wsk.trigger, triggerName) { (trigger, _) =>
+      trigger.create(triggerName, parameters = params)
+      trigger.create(triggerName, update = true)
+    }
+
+    assetHelper.withCleaner(wsk.action, actionName) { (action, name) =>
+      action.create(name, defaultAction)
+    }
+
+    assetHelper.withCleaner(wsk.rule, ruleName) { (rule, name) =>
+      rule.create(name, trigger = triggerName, action = actionName)
     }
-    val trigger = wsk.trigger.get(name)
+
+    val trigger = wsk.trigger.get(triggerName)
     trigger.getFieldJsValue("parameters") shouldBe JsArray(JsObject("key" -> JsString("a"), "value" -> JsString("A")))
     trigger.getFieldJsValue("publish") shouldBe JsBoolean(false)
     trigger.getField("version") shouldBe "0.0.2"
 
     val dynamicParams = Map("t" -> "T".toJson)
-    val run = wsk.trigger.fire(name, dynamicParams)
+    val run = wsk.trigger.fire(triggerName, dynamicParams)
     withActivation(wsk.activation, run) { activation =>
       activation.response.result shouldBe Some(dynamicParams.toJson)
       activation.duration shouldBe 0L // shouldn't exist but CLI generates it
       activation.end shouldBe Instant.EPOCH // shouldn't exist but CLI generates it
+      activation.logs shouldBe defined
+      activation.logs.get.size shouldBe 1
+
+      val logEntry = activation.logs.get(0).parseJson.asJsObject
+      val logs = JsArray(logEntry)
+      val ruleActivationId: String = logEntry.getFields("activationId")(0).convertTo[String]
+      val expectedLogs = JsArray(
+        JsObject(
+          "statusCode" -> JsNumber(0),
+          "activationId" -> JsString(ruleActivationId),
+          "success" -> JsBoolean(true),
+          "rule" -> JsString(ns + "/" + ruleName),
+          "action" -> JsString(ns + "/" + actionName)))
+      logs shouldBe expectedLogs
     }
 
-    val runWithNoParams = wsk.trigger.fire(name, Map())
+    val runWithNoParams = wsk.trigger.fire(triggerName, Map())
     withActivation(wsk.activation, runWithNoParams) { activation =>
       activation.response.result shouldBe Some(JsObject())
       activation.duration shouldBe 0L // shouldn't exist but CLI generates it
@@ -512,7 +544,7 @@ class WskBasicTests extends TestHelpers with WskTestHelpers {
 
     val triggerList = wsk.trigger.list()
     val triggers = triggerList.getBodyListJsObject()
-    triggers.exists(trigger => RestResult.getField(trigger, "name") == name) shouldBe true
+    triggers.exists(trigger => RestResult.getField(trigger, "name") == triggerName) shouldBe true
   }
 
   it should "create, and get a trigger summary" in withAssetCleaner(wskprops) { (wp, assetHelper) =>
@@ -554,17 +586,26 @@ class WskBasicTests extends TestHelpers with WskTestHelpers {
   }
 
   it should "create, and fire a trigger using a parameter file" in withAssetCleaner(wskprops) {
-    val name = "paramFileTrigger"
-    val file = Some(TestUtils.getTestActionFilename("argCheck.js"))
+    val ruleName = withTimestamp("r1toa1")
+    val triggerName = withTimestamp("paramFileTrigger")
+    val actionName = withTimestamp("a1")
     val argInput = Some(TestUtils.getTestActionFilename("validInput2.json"))
 
     (wp, assetHelper) =>
-      assetHelper.withCleaner(wsk.trigger, name) { (trigger, _) =>
-        trigger.create(name)
+      assetHelper.withCleaner(wsk.trigger, triggerName) { (trigger, _) =>
+        trigger.create(triggerName)
+      }
+
+      assetHelper.withCleaner(wsk.action, actionName) { (action, name) =>
+        action.create(name, defaultAction)
+      }
+
+      assetHelper.withCleaner(wsk.rule, ruleName) { (rule, name) =>
+        rule.create(name, trigger = triggerName, action = actionName)
       }
 
       val expectedOutput = JsObject("payload" -> JsString("test"))
-      val run = wsk.trigger.fire(name, parameterFile = argInput)
+      val run = wsk.trigger.fire(triggerName, parameterFile = argInput)
       withActivation(wsk.activation, run) { activation =>
         activation.response.result shouldBe Some(expectedOutput)
       }
@@ -596,12 +637,23 @@ class WskBasicTests extends TestHelpers with WskTestHelpers {
   }
 
   it should "create, and fire a trigger to ensure result is empty" in withAssetCleaner(wskprops) { (wp, assetHelper) =>
-    val name = "emptyResultTrigger"
-    assetHelper.withCleaner(wsk.trigger, name) { (trigger, _) =>
-      trigger.create(name)
+    val ruleName = withTimestamp("r1toa1")
+    val triggerName = withTimestamp("emptyResultTrigger")
+    val actionName = withTimestamp("a1")
+
+    assetHelper.withCleaner(wsk.trigger, triggerName) { (trigger, _) =>
+      trigger.create(triggerName)
+    }
+
+    assetHelper.withCleaner(wsk.action, actionName) { (action, name) =>
+      action.create(name, defaultAction)
+    }
+
+    assetHelper.withCleaner(wsk.rule, ruleName) { (rule, name) =>
+      rule.create(name, trigger = triggerName, action = actionName)
     }
 
-    val run = wsk.trigger.fire(name)
+    val run = wsk.trigger.fire(triggerName)
     withActivation(wsk.activation, run) { activation =>
       activation.response.result shouldBe Some(JsObject())
     }
@@ -637,6 +689,65 @@ class WskBasicTests extends TestHelpers with WskTestHelpers {
     stderr should include regex ("""The requested resource does not exist.""")
   }
 
+  it should "create and fire a trigger with a rule whose action has been deleted" in withAssetCleaner(wskprops) {
+    (wp, assetHelper) =>
+      val ruleName1 = withTimestamp("r1toa1")
+      val ruleName2 = withTimestamp("r2toa2")
+      val triggerName = withTimestamp("t1tor1r2")
+      val actionName1 = withTimestamp("a1")
+      val actionName2 = withTimestamp("a2")
+      val ns = wsk.namespace.whois()
+
+      assetHelper.withCleaner(wsk.trigger, triggerName) { (trigger, _) =>
+        trigger.create(triggerName)
+        trigger.create(triggerName, update = true)
+      }
+
+      assetHelper.withCleaner(wsk.action, actionName1) { (action, name) =>
+        action.create(name, defaultAction)
+      }
+      wsk.action.create(actionName2, defaultAction) // Delete this after the rule is created
+
+      assetHelper.withCleaner(wsk.rule, ruleName1) { (rule, name) =>
+        rule.create(name, trigger = triggerName, action = actionName1)
+      }
+      assetHelper.withCleaner(wsk.rule, ruleName2) { (rule, name) =>
+        rule.create(name, trigger = triggerName, action = actionName2)
+      }
+      wsk.action.delete(actionName2)
+
+      val run = wsk.trigger.fire(triggerName)
+      withActivation(wsk.activation, run) { activation =>
+        activation.duration shouldBe 0L // shouldn't exist but CLI generates it
+        activation.end shouldBe Instant.EPOCH // shouldn't exist but CLI generates it
+        activation.logs shouldBe defined
+        activation.logs.get.size shouldBe 2
+
+        val logEntry1 = activation.logs.get(0).parseJson.asJsObject
+        val logEntry2 = activation.logs.get(1).parseJson.asJsObject
+        val logs = JsArray(logEntry1, logEntry2)
+        val ruleActivationId: String = if (logEntry1.getFields("activationId").size == 1) {
+          logEntry1.getFields("activationId")(0).convertTo[String]
+        } else {
+          logEntry2.getFields("activationId")(0).convertTo[String]
+        }
+        val expectedLogs = JsArray(
+          JsObject(
+            "statusCode" -> JsNumber(0),
+            "activationId" -> JsString(ruleActivationId),
+            "success" -> JsBoolean(true),
+            "rule" -> JsString(ns + "/" + ruleName1),
+            "action" -> JsString(ns + "/" + actionName1)),
+          JsObject(
+            "statusCode" -> JsNumber(1),
+            "success" -> JsBoolean(false),
+            "error" -> JsString("The requested resource does not exist."),
+            "rule" -> JsString(ns + "/" + ruleName2),
+            "action" -> JsString(ns + "/" + actionName2)))
+        logs shouldBe expectedLogs
+      }
+  }
+
   behavior of "Wsk Rule REST"
 
   it should "create rule, get rule, update rule and list rule" in withAssetCleaner(wskprops) { (wp, assetHelper) =>
@@ -808,18 +919,28 @@ class WskBasicTests extends TestHelpers with WskTestHelpers {
 
   it should "create a trigger, and fire a trigger to get its individual fields from an activation" in withAssetCleaner(
     wskprops) { (wp, assetHelper) =>
-    val name = "activationFields"
+    val ruleName = withTimestamp("r1toa1")
+    val triggerName = withTimestamp("activationFields")
+    val actionName = withTimestamp("a1")
 
-    assetHelper.withCleaner(wsk.trigger, name) { (trigger, _) =>
-      trigger.create(name)
+    assetHelper.withCleaner(wsk.trigger, triggerName) { (trigger, _) =>
+      trigger.create(triggerName)
+    }
+
+    assetHelper.withCleaner(wsk.action, actionName) { (action, name) =>
+      action.create(name, defaultAction)
+    }
+
+    assetHelper.withCleaner(wsk.rule, ruleName) { (rule, name) =>
+      rule.create(name, trigger = triggerName, action = actionName)
     }
 
     val ns = wsk.namespace.whois()
-    val run = wsk.trigger.fire(name)
+    val run = wsk.trigger.fire(triggerName)
     withActivation(wsk.activation, run) { activation =>
       var result = wsk.activation.get(Some(activation.activationId))
       result.getField("namespace") shouldBe ns
-      result.getField("name") shouldBe name
+      result.getField("name") shouldBe triggerName
       result.getField("version") shouldBe "0.0.1"
       result.getFieldJsValue("publish") shouldBe JsBoolean(false)
       result.getField("subject") shouldBe ns
diff --git a/tests/src/test/scala/system/basic/WskRuleTests.scala b/tests/src/test/scala/system/basic/WskRuleTests.scala
index 2baac6d..bcb9dea 100644
--- a/tests/src/test/scala/system/basic/WskRuleTests.scala
+++ b/tests/src/test/scala/system/basic/WskRuleTests.scala
@@ -100,13 +100,11 @@ abstract class WskRuleTests extends TestHelpers with WskTestHelpers {
 
     withActivation(wsk.activation, run) { triggerActivation =>
       triggerActivation.cause shouldBe None
-
-      withActivationsFromEntity(
-        wsk.activation,
-        ruleName,
-        since = Some(triggerActivation.start.minusMillis(activationTimeSkewFactorMs))) {
-        _.head.cause shouldBe Some(triggerActivation.activationId)
-      }
+      triggerActivation.logs.get.size shouldBe (1)
+      val logs = triggerActivation.logs.get.mkString(" ")
+      logs should include(""""statusCode":0""")
+      logs should include(""""activationId":""")
+      logs should include(""""success":true""")
 
       withActivationsFromEntity(
         wsk.activation,
@@ -137,13 +135,11 @@ abstract class WskRuleTests extends TestHelpers with WskTestHelpers {
 
     withActivation(wsk.activation, run) { triggerActivation =>
       triggerActivation.cause shouldBe None
-
-      withActivationsFromEntity(
-        wsk.activation,
-        ruleName,
-        since = Some(triggerActivation.start.minusMillis(activationTimeSkewFactorMs))) {
-        _.head.cause shouldBe Some(triggerActivation.activationId)
-      }
+      triggerActivation.logs.get.size shouldBe (1)
+      val logs = triggerActivation.logs.get.mkString(" ")
+      logs should include(""""statusCode":0""")
+      logs should include(""""activationId":""")
+      logs should include(""""success":true""")
 
       withActivationsFromEntity(
         wsk.activation,
@@ -177,13 +173,11 @@ abstract class WskRuleTests extends TestHelpers with WskTestHelpers {
 
     withActivation(wsk.activation, run) { triggerActivation =>
       triggerActivation.cause shouldBe None
-
-      withActivationsFromEntity(
-        wsk.activation,
-        ruleName,
-        since = Some(triggerActivation.start.minusMillis(activationTimeSkewFactorMs))) {
-        _.head.cause shouldBe Some(triggerActivation.activationId)
-      }
+      triggerActivation.logs.get.size shouldBe (1)
+      val logs = triggerActivation.logs.get.mkString(" ")
+      logs should include(""""statusCode":0""")
+      logs should include(""""activationId":""")
+      logs should include(""""success":true""")
 
       withActivationsFromEntity(
         wsk.activation,
diff --git a/tests/src/test/scala/whisk/core/cli/test/Swift311Tests.scala b/tests/src/test/scala/whisk/core/cli/test/Swift311Tests.scala
index a00b1ea..1dc19f0 100644
--- a/tests/src/test/scala/whisk/core/cli/test/Swift311Tests.scala
+++ b/tests/src/test/scala/whisk/core/cli/test/Swift311Tests.scala
@@ -37,6 +37,7 @@ class Swift311Tests extends TestHelpers with WskTestHelpers with Matchers {
   val wsk = new WskRest
   val expectedDuration = 45 seconds
   val activationPollDuration = 60 seconds
+  val defaultJsAction = Some(TestUtils.getTestActionFilename("hello.js"))
 
   lazy val runtimeContainer = "swift:3.1.1"
 
@@ -116,10 +117,20 @@ class Swift311Tests extends TestHelpers with WskTestHelpers with Matchers {
   it should "allow Swift actions to trigger events" in withAssetCleaner(wskprops) { (wp, assetHelper) =>
     // create a trigger
     val triggerName = s"TestTrigger ${System.currentTimeMillis()}"
+    val ruleName = s"TestTriggerRule ${System.currentTimeMillis()}"
+    val ruleActionName = s"TestTriggerAction ${System.currentTimeMillis()}"
     assetHelper.withCleaner(wsk.trigger, triggerName) { (trigger, _) =>
       trigger.create(triggerName)
     }
 
+    assetHelper.withCleaner(wsk.action, ruleActionName) { (action, name) =>
+      action.create(name, defaultJsAction)
+    }
+
+    assetHelper.withCleaner(wsk.rule, ruleName) { (rule, name) =>
+      rule.create(name, trigger = triggerName, action = ruleActionName)
+    }
+
     // create an action that fires the trigger
     val file = TestUtils.getTestActionFilename("trigger.swift")
     val actionName = "ActionThatTriggers"
diff --git a/tests/src/test/scala/whisk/core/controller/test/TriggersApiTests.scala b/tests/src/test/scala/whisk/core/controller/test/TriggersApiTests.scala
index 200086f..3a8d3ce 100644
--- a/tests/src/test/scala/whisk/core/controller/test/TriggersApiTests.scala
+++ b/tests/src/test/scala/whisk/core/controller/test/TriggersApiTests.scala
@@ -64,6 +64,7 @@ class TriggersApiTests extends ControllerTestCommon with WhiskTriggersApi {
   val namespace = EntityPath(creds.subject.asString)
   val collectionPath = s"/${EntityPath.DEFAULT}/${collection.path}"
   def aname() = MakeName.next("triggers_tests")
+  def afullname(namespace: EntityPath, name: String) = FullyQualifiedEntityName(namespace, EntityName(name))
   val parametersLimit = Parameters.sizeLimit
 
   //// GET /triggers
@@ -319,9 +320,13 @@ class TriggersApiTests extends ControllerTestCommon with WhiskTriggersApi {
   //// POST /triggers/name
   it should "fire a trigger" in {
     implicit val tid = transid()
-    val trigger = WhiskTrigger(namespace, aname(), Parameters("x", "b"))
+    val rule = WhiskRule(namespace, aname(), afullname(namespace, aname().name), afullname(namespace, "bogus action"))
+    val trigger = WhiskTrigger(namespace, rule.trigger.name, rules = Some {
+      Map(rule.fullyQualifiedName(false) -> ReducedRule(rule.action, Status.ACTIVE))
+    })
     val content = JsObject("xxx" -> "yyy".toJson)
     put(entityStore, trigger)
+    put(entityStore, rule)
     Post(s"$collectionPath/${trigger.name}", content) ~> Route.seal(routes(creds)) ~> check {
       status should be(Accepted)
       val response = responseAs[JsObject]
@@ -342,8 +347,12 @@ class TriggersApiTests extends ControllerTestCommon with WhiskTriggersApi {
 
   it should "fire a trigger without args" in {
     implicit val tid = transid()
-    val trigger = WhiskTrigger(namespace, aname(), Parameters("x", "b"))
+    val rule = WhiskRule(namespace, aname(), afullname(namespace, aname().name), afullname(namespace, "bogus action"))
+    val trigger = WhiskTrigger(namespace, rule.trigger.name, Parameters("x", "b"), rules = Some {
+      Map(rule.fullyQualifiedName(false) -> ReducedRule(rule.action, Status.ACTIVE))
+    })
     put(entityStore, trigger)
+    put(entityStore, rule)
     Post(s"$collectionPath/${trigger.name}") ~> Route.seal(routes(creds)) ~> check {
       val response = responseAs[JsObject]
       val JsString(id) = response.fields("activationId")

-- 
To stop receiving notification emails like this one, please contact
dubeejw@apache.org.