blob: 6769ddaafa52a91fec1e3cd6f6f0342b4a5cdc0c [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package common.rest
import java.io.{File, FileInputStream}
import java.nio.charset.StandardCharsets
import java.security.KeyStore
import java.security.cert.X509Certificate
import java.time.Instant
import java.util.Base64
import akka.actor.ActorSystem
import akka.http.scaladsl.{Http, HttpsConnectionContext}
import akka.http.scaladsl.model.HttpMethods.{DELETE, GET, POST, PUT}
import akka.http.scaladsl.model.StatusCodes.{Accepted, NotFound, OK}
import akka.http.scaladsl.model._
import akka.http.scaladsl.model.Uri.{Path, Query}
import akka.http.scaladsl.model.headers.{Authorization, BasicHttpCredentials, OAuth2BearerToken}
import akka.util.ByteString
import common.TestUtils.{ANY_ERROR_EXIT, DONTCARE_EXIT, RunResult, SUCCESS_EXIT}
import common.rest.SSL.httpsConfig
import common.{
DeleteFromCollectionOperations,
HasActivation,
ListOrGetFromCollectionOperations,
WaitFor,
WhiskProperties,
WskProps,
_
}
import javax.net.ssl._
import org.apache.commons.io.{FileUtils, FilenameUtils}
import org.apache.openwhisk.common.Https.HttpsConfig
import org.apache.openwhisk.common.{AkkaLogging, Https, TransactionId}
import org.apache.openwhisk.core.entity.ByteSize
import org.apache.openwhisk.utils.retry
import org.scalatest.Matchers
import org.scalatest.concurrent.ScalaFutures
import org.scalatest.time.Span.convertDurationToSpan
import pureconfig._
import pureconfig.generic.auto._
import spray.json.DefaultJsonProtocol._
import spray.json._
import scala.collection.immutable.Seq
import scala.concurrent.ExecutionContext
import scala.concurrent.duration.{Duration, DurationInt, FiniteDuration}
import scala.language.postfixOps
import scala.util.{Failure, Success, Try}
class AcceptAllHostNameVerifier extends HostnameVerifier {
override def verify(s: String, sslSession: SSLSession): Boolean = true
}
object SSL {
lazy val httpsConfig: HttpsConfig = loadConfigOrThrow[HttpsConfig]("whisk.controller.https")
def keyManagers(clientAuth: Boolean): Array[KeyManager] = {
if (clientAuth)
keyManagersForClientAuth
else
Array.empty
}
def keyManagersForClientAuth: Array[KeyManager] = {
val keyFactoryType = "SunX509"
val keystorePassword = httpsConfig.keystorePassword.toCharArray
val ks: KeyStore = KeyStore.getInstance(httpsConfig.keystoreFlavor)
ks.load(new FileInputStream(httpsConfig.keystorePath), httpsConfig.keystorePassword.toCharArray)
val keyManagerFactory: KeyManagerFactory = KeyManagerFactory.getInstance(keyFactoryType)
keyManagerFactory.init(ks, keystorePassword)
keyManagerFactory.getKeyManagers
}
def nonValidatingContext(clientAuth: Boolean = false): SSLContext = {
class IgnoreX509TrustManager extends X509TrustManager {
def checkClientTrusted(chain: Array[X509Certificate], authType: String): Unit = ()
def checkServerTrusted(chain: Array[X509Certificate], authType: String): Unit = ()
def getAcceptedIssuers: Array[X509Certificate] = Array.empty
}
val context = SSLContext.getInstance("TLS")
context.init(keyManagers(clientAuth), Array(new IgnoreX509TrustManager), null)
context
}
def httpsConnectionContext(implicit system: ActorSystem): HttpsConnectionContext = {
Https.connectionContextClient(httpsConfig, true)
}
}
object HttpConnection {
/**
* Returns either the https context that is tailored for self-signed certificates on the controller, or
* a default connection context used in Http.SingleRequest
*
* @param protocol protocol used to communicate with controller API
* @param system actor system
* @return https connection context
*/
def getContext(protocol: String)(implicit system: ActorSystem): HttpsConnectionContext = {
if (protocol == "https") {
SSL.httpsConnectionContext
} else {
// supports http
Http().defaultClientHttpsContext
}
}
}
class WskRestOperations(implicit actorSytem: ActorSystem) extends WskOperations {
override implicit val action: RestActionOperations = new RestActionOperations
override implicit val trigger: RestTriggerOperations = new RestTriggerOperations
override implicit val rule: RestRuleOperations = new RestRuleOperations
override implicit val activation: RestActivationOperations = new RestActivationOperations
override implicit val pkg: RestPackageOperations = new RestPackageOperations
override implicit val namespace: RestNamespaceOperations = new RestNamespaceOperations
override implicit val api: RestGatewayOperations = new RestGatewayOperations
}
trait RestListOrGetFromCollectionOperations extends ListOrGetFromCollectionOperations with RunRestCmd {
import FullyQualifiedNames.resolve
/**
* List entities in collection.
*
* @param namespace (optional) if specified must be fully qualified namespace
* @param expectedExitCode (optional) the expected exit code for the command
* if the code is anything but DONTCARE_EXIT, assert the code is as expected
*/
override def list(namespace: Option[String] = None,
limit: Option[Int] = None,
nameSort: Option[Boolean] = None,
expectedExitCode: Int = OK.intValue)(implicit wp: WskProps): RestResult = {
val entPath = namespace map { ns =>
val (nspace, name) = getNamespaceEntityName(resolve(namespace))
if (name.isEmpty) Path(s"$basePath/namespaces/$nspace/$noun")
else Path(s"$basePath/namespaces/$nspace/$noun/$name/")
} getOrElse Path(s"$basePath/namespaces/${wp.namespace}/$noun")
val paramMap: Map[String, String] = Map("skip" -> "0", "docs" -> true.toString) ++
limit.map(l => Map("limit" -> l.toString)).getOrElse(Map.empty)
val resp = requestEntity(GET, entPath, paramMap)
val r = new RestResult(resp.status, getTransactionId(resp), getRespData(resp))
validateStatusCode(expectedExitCode, r.statusCode.intValue)
r
}
/**
* Gets entity from collection.
*
* @param name either a fully qualified name or a simple entity name
* @param expectedExitCode (optional) the expected exit code for the command
* if the code is anything but DONTCARE_EXIT, assert the code is as expected
*/
override def get(name: String,
expectedExitCode: Int = OK.intValue,
summary: Boolean = false,
fieldFilter: Option[String] = None,
url: Option[Boolean] = None,
save: Option[Boolean] = None,
saveAs: Option[String] = None)(implicit wp: WskProps): RestResult = {
val (ns, entity) = getNamespaceEntityName(name)
val entPath = Path(s"$basePath/namespaces/$ns/$noun/$entity")
val resp = requestEntity(GET, entPath)(wp)
val rr = new RestResult(resp.status, getTransactionId(resp), getRespData(resp))
validateStatusCode(expectedExitCode, rr.statusCode.intValue)
rr
}
}
trait RestDeleteFromCollectionOperations extends DeleteFromCollectionOperations with RunRestCmd {
/**
* Deletes entity from collection.
*
* @param name either a fully qualified name or a simple entity name
* @param expectedExitCode (optional) the expected exit code for the command
* if the code is anything but DONTCARE_EXIT, assert the code is as expected
*/
override def delete(name: String, expectedExitCode: Int = OK.intValue)(implicit wp: WskProps): RestResult = {
val (ns, entityName) = getNamespaceEntityName(name)
val path = Path(s"$basePath/namespaces/$ns/$noun/$entityName")
val resp = requestEntity(DELETE, path)(wp)
val rr = new RestResult(resp.status, getTransactionId(resp), getRespData(resp))
validateStatusCode(expectedExitCode, rr.statusCode.intValue)
rr
}
/**
* Deletes entity from collection but does not assert that the command succeeds.
* Use this if deleting an entity that may not exist and it is OK if it does not.
*
* @param name either a fully qualified name or a simple entity name
*/
override def sanitize(name: String)(implicit wp: WskProps): RestResult = {
delete(name, DONTCARE_EXIT)
}
}
trait RestActivation extends HasActivation {
/**
* Extracts activation id from invoke (action or trigger) or activation get
*/
override def extractActivationId(result: RunResult): Option[String] = {
extractActivationIdFromInvoke(result.asInstanceOf[RestResult])
}
/**
* Extracts activation id from 'wsk action invoke' or 'wsk trigger invoke'
*/
private def extractActivationIdFromInvoke(result: RestResult): Option[String] = {
if ((result.statusCode == OK) || (result.statusCode == Accepted))
Some(result.getField("activationId"))
else
None
}
}
class RestActionOperations(implicit val actorSystem: ActorSystem)
extends RestListOrGetFromCollectionOperations
with RestDeleteFromCollectionOperations
with RestActivation
with ActionOperations {
override protected val noun = "actions"
/**
* Creates action. Parameters mirror those available in the REST.
*
* @param name either a fully qualified name or a simple entity name
* @param expectedExitCode (optional) the expected exit code for the command
* if the code is anything but DONTCARE_EXIT, assert the code is as expected
*/
override def create(
name: String,
artifact: Option[String],
kind: Option[String] = None, // one of docker, copy, sequence or none for autoselect else an explicit type
main: Option[String] = None,
docker: Option[String] = None,
parameters: Map[String, JsValue] = Map.empty,
annotations: Map[String, JsValue] = Map.empty,
delAnnotations: Array[String] = Array(),
parameterFile: Option[String] = None,
annotationFile: Option[String] = None,
timeout: Option[Duration] = None,
memory: Option[ByteSize] = None,
logsize: Option[ByteSize] = None,
concurrency: Option[Int] = None,
shared: Option[Boolean] = None,
update: Boolean = false,
web: Option[String] = None,
websecure: Option[String] = None,
expectedExitCode: Int = OK.intValue)(implicit wp: WskProps): RestResult = {
val (namespace, actionName) = getNamespaceEntityName(name)
val (paramsInput, annosInput) = getParamsAnnos(parameters, annotations, parameterFile, annotationFile, web = web)
val (params: Array[JsValue], annos: Array[JsValue], exec: Map[String, JsValue]) = kind match {
case Some(k) =>
k match {
case "copy" =>
require(artifact.isDefined, "copy requires an artifact name")
val actionName = entityName(artifact.get)
val actionPath = Path(s"$basePath/namespaces/$namespace/$noun/$actionName")
val resp = requestEntity(GET, actionPath)
val result = new RestResult(resp.status, getTransactionId(resp), getRespData(resp))
val params = result.getFieldListJsObject("parameters").toArray[JsValue]
val annos = result.getFieldListJsObject("annotations").toArray[JsValue]
val exec = result.getFieldJsObject("exec").fields
(paramsInput ++ params, annosInput ++ annos, exec)
case "sequence" =>
require(artifact.isDefined, "sequence requires a component list")
val comps = convertIntoComponents(artifact.get)
val exec =
if (comps.nonEmpty) Map("components" -> comps.toJson, "kind" -> k.toJson)
else Map("kind" -> k.toJson)
(paramsInput, annosInput, exec)
case _ =>
val code = readCodeFromFile(artifact).map(c => Map("code" -> c.toJson)).getOrElse(Map.empty)
val exec: Map[String, JsValue] = if (k == "native" || k == "docker") {
require(k == "native" && docker.isEmpty || k == "docker" && docker.isDefined)
Map("kind" -> "blackbox".toJson, "image" -> docker.getOrElse("openwhisk/dockerskeleton").toJson) ++ code
} else {
require(artifact.isDefined, "file name required as an artifact")
Map("kind" -> k.toJson) ++ code
}
(paramsInput, annosInput, exec)
}
case None =>
docker
.map(_ => "blackbox")
.orElse {
artifact.map { file =>
getExt(file) match {
case "js" => "nodejs:default"
case "py" => "python:default"
case "swift" => "swift:default"
case "jar" => "java:default"
case _ =>
throw new IllegalStateException(s"Extension for $file not recognized and kind cannot be inferred.")
}
}
}
.map { k =>
val code = readCodeFromFile(artifact).map(c => Map("code" -> c.toJson)).getOrElse(Map.empty)
val image = docker.map(i => Map("image" -> i.toJson)).getOrElse(Map.empty)
(paramsInput, annosInput, Map("kind" -> k.toJson) ++ code ++ image)
}
.getOrElse {
if (!update && artifact.isDefined)
throw new IllegalStateException(
s"Extension for ${artifact.get} not recognized and kind cannot be inferred.")
else (paramsInput, annosInput, Map.empty)
}
}
val limits: Map[String, JsValue] = {
timeout.map(t => Map("timeout" -> t.toMillis.toJson)).getOrElse(Map.empty) ++
logsize.map(log => Map("logs" -> log.toMB.toJson)).getOrElse(Map.empty) ++
memory.map(m => Map("memory" -> m.toMB.toJson)).getOrElse(Map.empty) ++
concurrency.map(c => Map("concurrency" -> c.toJson)).getOrElse(Map.empty)
}
val body: Map[String, JsValue] = if (!update) {
require(exec.nonEmpty, "exec cannot be empty on create")
Map(
"exec" -> main.map(m => exec ++ Map("main" -> m.toJson)).getOrElse(exec).toJson,
"parameters" -> params.toJson,
"annotations" -> annos.toJson) ++ Map("limits" -> limits.toJson)
} else {
var content: Map[String, JsValue] = Map.empty
if (exec.nonEmpty)
content = Map("exec" -> main.map(m => exec ++ Map("main" -> m.toJson)).getOrElse(exec).toJson)
if (params.nonEmpty)
content = content + ("parameters" -> params.toJson)
if (annos.nonEmpty)
content = content + ("annotations" -> annos.toJson)
if (limits.nonEmpty)
content = content + ("limits" -> limits.toJson)
if (delAnnotations.nonEmpty)
content = content + ("delAnnotations" -> delAnnotations.toJson)
content
}
val path = Path(s"$basePath/namespaces/$namespace/$noun/$actionName")
val resp =
if (update) requestEntity(PUT, path, Map("overwrite" -> "true"), Some(JsObject(body).toString))
else requestEntity(PUT, path, body = Some(JsObject(body).toString))
val rr = new RestResult(resp.status, getTransactionId(resp), getRespData(resp))
validateStatusCode(expectedExitCode, rr.statusCode.intValue)
rr
}
override def invoke(name: String,
parameters: Map[String, JsValue] = Map.empty,
parameterFile: Option[String] = None,
blocking: Boolean = false,
result: Boolean = false,
expectedExitCode: Int = Accepted.intValue)(implicit wp: WskProps): RestResult = {
super.invokeAction(name, parameters, parameterFile, blocking, result, expectedExitCode = expectedExitCode)
}
private def readCodeFromFile(artifact: Option[String]): Option[String] = {
artifact.map { file =>
val ext = getExt(file)
val isBinary = ext == "zip" || ext == "jar" || ext == "bin"
if (!isBinary) {
FileUtils.readFileToString(new File(file), StandardCharsets.UTF_8)
} else {
val zip = FileUtils.readFileToByteArray(new File(file))
Base64.getEncoder.encodeToString(zip)
}
}
}
}
class RestTriggerOperations(implicit val actorSystem: ActorSystem)
extends RestListOrGetFromCollectionOperations
with RestDeleteFromCollectionOperations
with RestActivation
with TriggerOperations {
override protected val noun = "triggers"
/**
* Creates trigger. Parameters mirror those available in the REST.
*
* @param name either a fully qualified name or a simple entity name
* @param expectedExitCode (optional) the expected exit code for the command
* if the code is anything but DONTCARE_EXIT, assert the code is as expected
*/
override def create(name: String,
parameters: Map[String, JsValue] = Map.empty,
annotations: Map[String, JsValue] = Map.empty,
parameterFile: Option[String] = None,
annotationFile: Option[String] = None,
feed: Option[String] = None,
shared: Option[Boolean] = None,
update: Boolean = false,
expectedExitCode: Int = OK.intValue)(implicit wp: WskProps): RestResult = {
val (ns, triggerName) = getNamespaceEntityName(name)
val path = Path(s"$basePath/namespaces/$ns/$noun/$triggerName")
val (params, annos) = getParamsAnnos(parameters, annotations, parameterFile, annotationFile, feed)
var bodyContent: Map[String, JsValue] = Map.empty
if (!update) {
bodyContent =
Map("publish" -> shared.getOrElse(false).toJson, "parameters" -> params.toJson, "annotations" -> annos.toJson)
} else {
shared.foreach { p =>
bodyContent = Map("publish" -> p.toJson)
}
val inputParams = convertMapIntoKeyValue(parameters)
if (inputParams.nonEmpty) {
bodyContent = bodyContent + ("parameters" -> params.toJson)
}
val inputAnnos = convertMapIntoKeyValue(annotations)
if (inputAnnos.nonEmpty) {
bodyContent = bodyContent + ("annotations" -> annos.toJson)
}
}
val resp =
if (update) requestEntity(PUT, path, Map("overwrite" -> "true"), Some(JsObject(bodyContent).toString))
else requestEntity(PUT, path, body = Some(JsObject(bodyContent).toString))
val result = new RestResult(resp.status, getTransactionId(resp), getRespData(resp))
if (result.statusCode != OK) {
validateStatusCode(expectedExitCode, result.statusCode.intValue)
}
val rr = feed map { f =>
// Invoke the feed
val (nsFeed, feedName) = getNamespaceEntityName(f)
val path = Path(s"$basePath/namespaces/$nsFeed/actions/$feedName")
val paramMap = Map("blocking" -> "true", "result" -> "false")
var body: Map[String, JsValue] = Map(
"lifecycleEvent" -> "CREATE".toJson,
"triggerName" -> s"/$ns/$triggerName".toJson,
"authKey" -> s"${wp.authKey}".toJson)
body = body ++ parameters
val resp = requestEntity(POST, path, paramMap, Some(body.toJson.toString))
val resultInvoke = new RestResult(resp.status, getTransactionId(resp), getRespData(resp))
if ((expectedExitCode != DONTCARE_EXIT) && (expectedExitCode != ANY_ERROR_EXIT))
expectedExitCode shouldBe resultInvoke.statusCode.intValue
if (resultInvoke.statusCode != OK) {
// Remove the trigger, because the feed failed to invoke.
delete(triggerName)
new RestResult(NotFound, getTransactionId(resp))
} else {
result
}
} getOrElse {
validateStatusCode(expectedExitCode, result.statusCode.intValue)
result
}
rr
}
/**
* Fires trigger. Parameters mirror those available in the REST.
*
* @param name either a fully qualified name or a simple entity name
* @param expectedExitCode (optional) the expected exit code for the command
* if the code is anything but DONTCARE_EXIT, assert the code is as expected
*/
override def fire(name: String,
parameters: Map[String, JsValue] = Map.empty,
parameterFile: Option[String] = None,
expectedExitCode: Int = Accepted.intValue)(implicit wp: WskProps): RestResult = {
val path = getNamePath(wp.namespace, noun, name)
val params = parameterFile map { l =>
val input = FileUtils.readFileToString(new File(l), StandardCharsets.UTF_8)
input.parseJson.convertTo[Map[String, JsValue]]
} getOrElse parameters
val resp =
if (params.isEmpty) requestEntity(POST, path)
else requestEntity(POST, path, body = Some(params.toJson.toString))
new RestResult(resp.status.intValue, getTransactionId(resp), getRespData(resp))
}
}
class RestRuleOperations(implicit val actorSystem: ActorSystem)
extends RestListOrGetFromCollectionOperations
with RestDeleteFromCollectionOperations
with WaitFor
with RuleOperations {
override protected val noun = "rules"
/**
* Creates rule. Parameters mirror those available in the REST.
*
* @param name either a fully qualified name or a simple entity name
* @param trigger must be a simple name
* @param action must be a simple name
* @param expectedExitCode (optional) the expected exit code for the command
* if the code is anything but DONTCARE_EXIT, assert the code is as expected
*/
override def create(name: String,
trigger: String,
action: String,
annotations: Map[String, JsValue] = Map.empty,
shared: Option[Boolean] = None,
update: Boolean = false,
expectedExitCode: Int = SUCCESS_EXIT)(implicit wp: WskProps): RestResult = {
val path = getNamePath(wp.namespace, noun, name)
val annos = convertMapIntoKeyValue(annotations)
val published = shared.getOrElse(false)
val bodyContent = JsObject(
"trigger" -> fullEntityName(trigger).toJson,
"action" -> fullEntityName(action).toJson,
"publish" -> published.toJson,
"status" -> "".toJson,
"annotations" -> annos.toJson)
val resp =
if (update) requestEntity(PUT, path, Map("overwrite" -> "true"), Some(bodyContent.toString))
else requestEntity(PUT, path, body = Some(bodyContent.toString))
new RestResult(resp.status, getTransactionId(resp), getRespData(resp))
}
/**
* Enables rule.
*
* @param name either a fully qualified name or a simple entity name
* @param expectedExitCode (optional) the expected exit code for the command
* if the code is anything but DONTCARE_EXIT, assert the code is as expected
*/
override def enable(name: String, expectedExitCode: Int = SUCCESS_EXIT)(implicit wp: WskProps): RestResult = {
changeRuleState(name)
}
/**
* Disables rule.
*
* @param name either a fully qualified name or a simple entity name
* @param expectedExitCode (optional) the expected exit code for the command
* if the code is anything but DONTCARE_EXIT, assert the code is as expected
*/
override def disable(name: String, expectedExitCode: Int = SUCCESS_EXIT)(implicit wp: WskProps): RestResult = {
changeRuleState(name, "inactive")
}
/**
* Checks state of rule.
*
* @param name either a fully qualified name or a simple entity name
* @param expectedExitCode (optional) the expected exit code for the command
* if the code is anything but DONTCARE_EXIT, assert the code is as expected
*/
override def state(name: String, expectedExitCode: Int = OK.intValue)(implicit wp: WskProps): RestResult = {
get(name, expectedExitCode = expectedExitCode)
}
def changeRuleState(ruleName: String, state: String = "active")(implicit wp: WskProps): RestResult = {
val enName = entityName(ruleName)
val path = getNamePath(wp.namespace, noun, enName)
val bodyContent = JsObject("status" -> state.toJson)
val resp = requestEntity(POST, path, body = Some(bodyContent.toString))
new RestResult(resp.status, getTransactionId(resp), getRespData(resp))
}
}
class RestActivationOperations(implicit val actorSystem: ActorSystem)
extends ActivationOperations
with RunRestCmd
with RestActivation
with WaitFor {
protected val noun = "activations"
/**
* Activation polling console.
*
* @param duration exits console after duration
* @param since (optional) time travels back to activation since given duration
*/
override def console(duration: Duration,
since: Option[Duration] = None,
expectedExitCode: Int = SUCCESS_EXIT,
actionName: Option[String] = None)(implicit wp: WskProps): RestResult = {
require(duration > 1.second, "duration must be at least 1 second")
val sinceTime = {
val now = System.currentTimeMillis
since.map(s => now - s.toMillis).getOrElse(now)
}
retry({
val result = listActivation(since = Some(Instant.ofEpochMilli(sinceTime)))(wp)
if (result.stdout != "[]") result else throw new Throwable()
}, (duration / 1.second).toInt, Some(1.second))
}
/**
* Lists activations.
*
* @param filter (optional) if define, must be a simple entity name
* @param limit (optional) the maximum number of activation to return
* @param since (optional) only the activations since this timestamp are included
* @param expectedExitCode (optional) the expected exit code for the command
* if the code is anything but DONTCARE_EXIT, assert the code is as expected
*/
def listActivation(filter: Option[String] = None,
limit: Option[Int] = None,
since: Option[Instant] = None,
skip: Option[Int] = None,
docs: Boolean = true,
expectedExitCode: Int = SUCCESS_EXIT)(implicit wp: WskProps): RestResult = {
val entityPath = Path(s"$basePath/namespaces/${wp.namespace}/$noun")
val paramMap = Map("docs" -> docs.toString) ++
skip.map(s => Map("skip" -> s.toString)).getOrElse(Map.empty) ++
limit.map(l => Map("limit" -> l.toString)).getOrElse(Map.empty) ++
filter.map(f => Map("name" -> f.toString)).getOrElse(Map.empty) ++
since.map(s => Map("since" -> s.toEpochMilli.toString)).getOrElse(Map.empty)
val resp = requestEntity(GET, entityPath, paramMap)
new RestResult(resp.status, getTransactionId(resp), getRespData(resp))
}
/**
* Parses result of WskActivation.list to extract sequence of activation ids.
*
* @param rr run result, should be from WhiskActivation.list otherwise behavior is undefined
* @return sequence of activations
*/
def idsActivation(rr: RestResult): Seq[String] = {
rr.getBodyListJsObject.map(r => RestResult.getField(r, "activationId").toString)
}
/**
* Gets activation logs by id.
*
* @param activationId the activation id
* @param expectedExitCode (optional) the expected exit code for the command
* if the code is anything but DONTCARE_EXIT, assert the code is as expected
*/
def activationLogs(activationId: String, expectedExitCode: Int = OK.intValue)(implicit wp: WskProps): RestResult = {
val path = Path(s"$basePath/namespaces/${wp.namespace}/$noun/$activationId/logs")
val resp = requestEntity(GET, path)
val rr = new RestResult(resp.status, getTransactionId(resp), getRespData(resp))
validateStatusCode(expectedExitCode, rr.statusCode.intValue)
rr
}
/**
* Gets activation result by id.
*
* @param activationId the activation id
* @param expectedExitCode (optional) the expected exit code for the command
* if the code is anything but DONTCARE_EXIT, assert the code is as expected
*/
def activationResult(activationId: String, expectedExitCode: Int = OK.intValue)(implicit wp: WskProps): RestResult = {
val path = Path(s"$basePath/namespaces/${wp.namespace}/$noun/$activationId/result")
val resp = requestEntity(GET, path)
val rr = new RestResult(resp.status, getTransactionId(resp), getRespData(resp))
validateStatusCode(expectedExitCode, rr.statusCode.intValue)
rr
}
/**
* Polls activations list for at least N activations. The activations
* are optionally filtered for the given entity. Will return as soon as
* N activations are found. If after retry budget is exhausted, N activations
* are still not present, will return a partial result. Hence caller must
* check length of the result and not assume it is >= N.
*
* @param N the number of activations desired
* @param entity the name of the entity to filter from activation list
* @param limit the maximum number of entities to list (if entity name is not unique use Some(0))
* @param since (optional) only the activations since this timestamp are included
* @param skip (optional) the number of activations to skip
* @param retries the maximum retries (total timeout is retries + 1 seconds)
* @return activation ids found, caller must check length of sequence
*/
override def pollFor(N: Int,
entity: Option[String],
limit: Option[Int] = Some(30),
since: Option[Instant] = None,
skip: Option[Int] = Some(0),
retries: Int = 10,
pollPeriod: Duration = 1.second)(implicit wp: WskProps): Seq[String] = {
Try {
retry({
val result =
idsActivation(listActivation(filter = entity, limit = limit, since = since, skip = skip, docs = false))
if (result.length >= N) result else throw PartialResult(result)
}, retries, waitBeforeRetry = Some(pollPeriod))
} match {
case Success(ids) => ids
case Failure(PartialResult(ids)) => ids
case _ => Seq.empty
}
}
override def get(activationId: Option[String],
expectedExitCode: Int = OK.intValue,
fieldFilter: Option[String] = None,
last: Option[Boolean] = None,
summary: Option[Boolean] = None)(implicit wp: WskProps): RestResult = {
val actId = activationId match {
case Some(_) => activationId
case None =>
last match {
case Some(true) =>
val activations = pollFor(N = 1, entity = None, limit = Some(1))
require(activations.size <= 1)
activations.headOption
case _ => None
}
}
val rr = actId match {
case Some(id) =>
val resp = requestEntity(GET, getNamePath(wp.namespace, noun, id))
new RestResult(resp.status, getTransactionId(resp), getRespData(resp))
case None => new RestResult(NotFound, "")
}
validateStatusCode(expectedExitCode, rr.statusCode.intValue)
rr
}
/**
* Polls for an activation matching the given id. If found
* return Right(activation) else Left(result of calling REST API).
*
* @return either Left(error message) or Right(activation as JsObject)
*/
override def waitForActivation(activationId: String,
initialWait: Duration = 1 second,
pollPeriod: Duration = 1 second,
totalWait: Duration = 30 seconds)(implicit wp: WskProps): Either[String, JsObject] = {
val activation = waitfor(() => {
val result = get(Some(activationId), expectedExitCode = DONTCARE_EXIT)(wp)
if (result.statusCode == NotFound) {
null
} else result
}, initialWait, pollPeriod, totalWait)
Try {
assert(activation.statusCode == OK)
assert(activation.getField("activationId") != "")
activation.respBody
} map {
Right(_)
} getOrElse Left(s"No activation record for'$activationId'")
}
override def logs(activationId: Option[String] = None,
expectedExitCode: Int = OK.intValue,
last: Option[Boolean] = None)(implicit wp: WskProps): RestResult = {
val rr = activationId match {
case Some(id) =>
val resp = requestEntity(GET, getNamePath(wp.namespace, noun, s"$id/logs"))
new RestResult(resp.status, getTransactionId(resp), getRespData(resp))
case None =>
new RestResult(NotFound, "")
}
validateStatusCode(expectedExitCode, rr.statusCode.intValue)
rr
}
override def result(activationId: Option[String] = None,
expectedExitCode: Int = OK.intValue,
last: Option[Boolean] = None)(implicit wp: WskProps): RestResult = {
val rr = activationId match {
case Some(id) =>
val resp = requestEntity(GET, getNamePath(wp.namespace, noun, s"$id/result"))
new RestResult(resp.status, getTransactionId(resp), getRespData(resp))
case None =>
new RestResult(NotFound, "")
}
validateStatusCode(expectedExitCode, rr.statusCode.intValue)
rr
}
/** Used in polling for activations to record partial results from retry poll. */
private case class PartialResult(ids: Seq[String]) extends Throwable
}
class RestNamespaceOperations(implicit val actorSystem: ActorSystem) extends NamespaceOperations with RunRestCmd {
protected val noun = "namespaces"
/**
* Lists available namespaces for whisk key.
*
* @param expectedExitCode (optional) the expected exit code for the command
* if the code is anything but DONTCARE_EXIT, assert the code is as expected
*/
override def list(expectedExitCode: Int = OK.intValue, nameSort: Option[Boolean] = None)(implicit
wp: WskProps): RestResult = {
val entPath = Path(s"$basePath/namespaces")
val resp = requestEntity(GET, entPath)
val result = new RestResult(resp.status, getTransactionId(resp), getRespData(resp))
validateStatusCode(expectedExitCode, result.statusCode.intValue)
result
}
/**
* Looks up namespace for whisk props.
*
* @param wskprops instance of WskProps with an auth key to lookup
* @return namespace as string
*/
override def whois()(implicit wskprops: WskProps): String = {
val ns = list().getBodyListString
ns.headOption.map(_.toString).getOrElse("")
}
}
class RestPackageOperations(implicit val actorSystem: ActorSystem)
extends RestListOrGetFromCollectionOperations
with RestDeleteFromCollectionOperations
with PackageOperations {
override protected val noun = "packages"
/**
* Creates package. Parameters mirror those available in the REST.
*
* @param name either a fully qualified name or a simple entity name
* @param expectedExitCode (optional) the expected exit code for the command
* if the code is anything but DONTCARE_EXIT, assert the code is as expected
*/
override def create(name: String,
parameters: Map[String, JsValue] = Map.empty,
annotations: Map[String, JsValue] = Map.empty,
parameterFile: Option[String] = None,
annotationFile: Option[String] = None,
shared: Option[Boolean] = None,
update: Boolean = false,
expectedExitCode: Int = OK.intValue)(implicit wp: WskProps): RestResult = {
val path = getNamePath(wp.namespace, noun, name)
var bodyContent: Map[String, JsValue] = Map.empty
val (params, annos) = getParamsAnnos(parameters, annotations, parameterFile, annotationFile)
if (!update) {
val published = shared.getOrElse(false)
bodyContent = Map("publish" -> published.toJson, "parameters" -> params.toJson, "annotations" -> annos.toJson)
} else {
shared.foreach { s =>
bodyContent = bodyContent + ("publish" -> s.toJson)
}
val inputParams = convertMapIntoKeyValue(parameters)
if (inputParams.nonEmpty) {
bodyContent = bodyContent + ("parameters" -> params.toJson)
}
val inputAnnos = convertMapIntoKeyValue(annotations)
if (inputAnnos.nonEmpty) {
bodyContent = bodyContent + ("annotations" -> annos.toJson)
}
}
val resp =
if (update) requestEntity(PUT, path, Map("overwrite" -> "true"), Some(JsObject(bodyContent).toString))
else requestEntity(PUT, path, body = Some(JsObject(bodyContent).toString))
val r = new RestResult(resp.status, getTransactionId(resp), getRespData(resp))
validateStatusCode(expectedExitCode, r.statusCode.intValue)
r
}
/**
* Binds package. Parameters mirror those available in the REST.
*
* @param name either a fully qualified name or a simple entity name
* @param expectedExitCode (optional) the expected exit code for the command
* if the code is anything but DONTCARE_EXIT, assert the code is as expected
*/
override def bind(provider: String,
name: String,
parameters: Map[String, JsValue] = Map.empty,
annotations: Map[String, JsValue] = Map.empty,
expectedExitCode: Int = OK.intValue)(implicit wp: WskProps): RestResult = {
val params = convertMapIntoKeyValue(parameters)
val annos = convertMapIntoKeyValue(annotations)
val (ns, packageName) = getNamespaceEntityName(provider)
val path = getNamePath(wp.namespace, noun, name)
val binding = JsObject("namespace" -> ns.toJson, "name" -> packageName.toJson)
val bodyContent =
JsObject("binding" -> binding.toJson, "parameters" -> params.toJson, "annotations" -> annos.toJson)
val resp = requestEntity(PUT, path, Map("overwrite" -> "false"), Some(bodyContent.toString))
val rr = new RestResult(resp.status, getTransactionId(resp), getRespData(resp))
validateStatusCode(expectedExitCode, rr.statusCode.intValue)
rr
}
}
class RestGatewayOperations(implicit val actorSystem: ActorSystem) extends GatewayOperations with RunRestCmd {
protected val noun = "apis"
/**
* Creates and API endpoint. Parameters mirror those available in the REST.
*
* @param expectedExitCode (optional) the expected exit code for the command
* if the code is anything but DONTCARE_EXIT, assert the code is as expected
*/
override def create(basepath: Option[String] = None,
relpath: Option[String] = None,
operation: Option[String] = None,
action: Option[String] = None,
apiname: Option[String] = None,
swagger: Option[String] = None,
responsetype: Option[String] = None,
expectedExitCode: Int = SUCCESS_EXIT,
cliCfgFile: Option[String] = None)(implicit wp: WskProps): RestResult = {
val r = action match {
case Some(action) => {
val (ns, actionName) = getNamespaceEntityName(action)
val actionUrl = s"${WhiskProperties.getApiHostForAction}$basePath/web/$ns/default/$actionName.http"
val actionAuthKey = wp.authKey
val testaction = Some(
new ApiAction(name = actionName, namespace = ns, backendUrl = actionUrl, authkey = actionAuthKey))
val parms = Map("namespace" -> ns.toJson) ++ {
basepath map { b =>
Map("gatewayBasePath" -> b.toJson)
} getOrElse Map.empty
} ++ {
relpath map { r =>
Map("gatewayPath" -> r.toJson)
} getOrElse Map.empty
} ++ {
operation map { o =>
Map("gatewayMethod" -> o.toJson)
} getOrElse Map.empty
} ++ {
apiname map { an =>
Map("apiName" -> an.toJson)
} getOrElse Map.empty
} ++ {
testaction map { a =>
Map("action" -> a.toJson)
} getOrElse Map.empty
} ++ {
swagger map { s =>
val swaggerFile = FileUtils.readFileToString(new File(s), StandardCharsets.UTF_8)
Map("swagger" -> swaggerFile.toJson)
} getOrElse Map.empty
}
val spaceguid = if (wp.authKey.contains(":")) wp.authKey.split(":")(0) else wp.authKey
val parm = Map[String, JsValue]("apidoc" -> JsObject(parms)) ++ {
responsetype.map(r => Map("responsetype" -> r.toJson)).getOrElse(Map.empty)
} ++ {
Map("accesstoken" -> wp.authKey.toJson)
} ++ {
Map("spaceguid" -> spaceguid.toJson)
}
invokeAction(
name = "apimgmt/createApi",
parameters = parm,
blocking = true,
result = true,
web = true,
expectedExitCode = expectedExitCode)(wp)
}
case None =>
swagger match {
case Some(swaggerFile) =>
var file = ""
val fileName = swaggerFile.toString
try {
file = FileUtils.readFileToString(new File(fileName), StandardCharsets.UTF_8)
} catch {
case _: Throwable =>
return new RestResult(
NotFound,
"",
JsObject("error" -> s"Error reading swagger file '$fileName'".toJson).toString)
}
val parms = Map("namespace" -> s"${wp.namespace}".toJson, "swagger" -> file.toJson)
val parm = Map[String, JsValue]("apidoc" -> JsObject(parms)) ++ {
responsetype.map(r => Map("responsetype" -> r.toJson)).getOrElse(Map.empty)
} ++ {
Map("accesstoken" -> wp.authKey.toJson)
} ++ {
Map("spaceguid" -> wp.authKey.split(":")(0).toJson)
}
invokeAction(
name = "apimgmt/createApi",
parameters = parm,
blocking = true,
result = true,
web = true,
expectedExitCode = expectedExitCode)(wp)
case None => new RestResult(NotFound, "")
}
}
r
}
/**
* Retrieve a list of API endpoints. Parameters mirror those available in the REST.
*
* @param expectedExitCode (optional) the expected exit code for the command
* if the code is anything but DONTCARE_EXIT, assert the code is as expected
*/
override def list(basepathOrApiName: Option[String] = None,
relpath: Option[String] = None,
operation: Option[String] = None,
limit: Option[Int] = None,
since: Option[Instant] = None,
full: Option[Boolean] = None,
nameSort: Option[Boolean] = None,
expectedExitCode: Int = SUCCESS_EXIT,
cliCfgFile: Option[String] = None)(implicit wp: WskProps): RestResult = {
val parms = {
basepathOrApiName map { b =>
Map("basepath" -> b.toJson)
} getOrElse Map.empty
} ++ {
relpath map { r =>
Map("relpath" -> r.toJson)
} getOrElse Map.empty
} ++ {
operation map { o =>
Map("operation" -> o.toJson)
} getOrElse Map.empty
} ++ {
Map("accesstoken" -> wp.authKey.toJson)
} ++ {
Map("spaceguid" -> wp.authKey.split(":")(0).toJson)
}
invokeAction(
name = "apimgmt/getApi",
parameters = parms,
blocking = true,
result = true,
web = true,
expectedExitCode = OK.intValue)(wp)
}
/**
* Retieves an API's configuration. Parameters mirror those available in the REST.
* Runs a command wsk [params] where the arguments come in as a sequence.
*
* @param expectedExitCode (optional) the expected exit code for the command
* if the code is anything but DONTCARE_EXIT, assert the code is as expected
*/
override def get(basepathOrApiName: Option[String] = None,
full: Option[Boolean] = None,
expectedExitCode: Int = SUCCESS_EXIT,
cliCfgFile: Option[String] = None,
format: Option[String] = None)(implicit wp: WskProps): RestResult = {
val parms = {
basepathOrApiName map { b =>
Map("basepath" -> b.toJson)
} getOrElse Map.empty
} ++ {
Map("accesstoken" -> wp.authKey.toJson)
} ++ {
Map("spaceguid" -> wp.authKey.split(":")(0).toJson)
}
invokeAction(
name = "apimgmt/getApi",
parameters = parms,
blocking = true,
result = true,
web = true,
expectedExitCode = OK.intValue)(wp)
}
/**
* Delete an entire API or a subset of API endpoints. Parameters mirror those available in the REST.
*
* @param expectedExitCode (optional) the expected exit code for the command
* if the code is anything but DONTCARE_EXIT, assert the code is as expected
*/
override def delete(basepathOrApiName: String,
relpath: Option[String] = None,
operation: Option[String] = None,
expectedExitCode: Int = SUCCESS_EXIT,
cliCfgFile: Option[String] = None)(implicit wp: WskProps): RestResult = {
val parms = Map("basepath" -> basepathOrApiName.toJson) ++ {
relpath map { r =>
Map("relpath" -> r.toJson)
} getOrElse Map.empty
} ++ {
operation map { o =>
Map("operation" -> o.toJson)
} getOrElse Map.empty
} ++ {
Map("accesstoken" -> wp.authKey.toJson)
} ++ {
Map("spaceguid" -> wp.authKey.split(":")(0).toJson)
}
invokeAction(
name = "apimgmt/deleteApi",
parameters = parms,
blocking = true,
result = true,
web = true,
expectedExitCode = expectedExitCode)(wp)
}
}
trait RunRestCmd extends Matchers with ScalaFutures with SwaggerValidator {
val protocol: String = loadConfigOrThrow[String]("whisk.controller.protocol")
val idleTimeout: FiniteDuration = 90 seconds
val toStrictTimeout: FiniteDuration = 30 seconds
val queueSize = 10
val maxOpenRequest = 1024
val basePath = Path("/api/v1")
val systemNamespace = "whisk.system"
val logger = new AkkaLogging(actorSystem.log)
implicit val config: PatienceConfig = PatienceConfig(100 seconds, 15 milliseconds)
implicit val actorSystem: ActorSystem
lazy implicit val executionContext: ExecutionContext = actorSystem.dispatcher
lazy val connectionContext =
Https.connectionContextClient(SSL.nonValidatingContext(httpsConfig.clientAuth.toBoolean), true)
def isStatusCodeExpected(expectedExitCode: Int, statusCode: Int): Boolean = {
if ((expectedExitCode != DONTCARE_EXIT) && (expectedExitCode != ANY_ERROR_EXIT))
statusCode == expectedExitCode
else true
}
def validateStatusCode(expectedExitCode: Int, statusCode: Int): Unit = {
if ((expectedExitCode != DONTCARE_EXIT) && (expectedExitCode != ANY_ERROR_EXIT))
if (!isStatusCodeExpected(expectedExitCode, statusCode)) {
statusCode shouldBe expectedExitCode
}
}
def getNamePath(ns: String, noun: String, name: String) = Path(s"$basePath/namespaces/$ns/$noun/$name")
def getExt(filePath: String): String = Option(FilenameUtils.getExtension(filePath)).getOrElse("")
def requestEntity(method: HttpMethod,
path: Path,
params: Map[String, String] = Map.empty,
body: Option[String] = None)(implicit wp: WskProps): HttpResponse = {
val creds = getHttpCredentials(wp)
// startsWith(http) includes https
val hostWithScheme = if (wp.apihost.startsWith("http")) {
Uri(wp.apihost)
} else {
Uri().withScheme("https").withHost(wp.apihost)
}
val request = HttpRequest(
method,
hostWithScheme.withPath(path).withQuery(Query(params)),
List(Authorization(creds)),
entity =
body.map(b => HttpEntity.Strict(ContentTypes.`application/json`, ByteString(b))).getOrElse(HttpEntity.Empty))
val response = Http().singleRequest(request, connectionContext).flatMap { _.toStrict(toStrictTimeout) }.futureValue
logger.debug(this, s"Request: $request")
logger.debug(this, s"Response: $response")
val validationErrors = validateRequestAndResponse(request, response)
if (validationErrors.nonEmpty) {
fail(
s"HTTP request or response did not match the Swagger spec.\nRequest: $request\n" +
s"Response: $response\nValidation Error: $validationErrors")
}
response
}
private def getHttpCredentials(wp: WskProps) = {
if (wp.authKey.contains(":")) {
val authKey = wp.authKey.split(":")
new BasicHttpCredentials(authKey(0), authKey(1))
} else {
if (wp.basicAuth) {
new BasicHttpCredentials(wp.authKey, wp.authKey)
} else {
OAuth2BearerToken(wp.authKey)
}
}
}
def getParamsAnnos(parameters: Map[String, JsValue] = Map.empty,
annotations: Map[String, JsValue] = Map.empty,
parameterFile: Option[String] = None,
annotationFile: Option[String] = None,
feed: Option[String] = None,
web: Option[String] = None): (Array[JsValue], Array[JsValue]) = {
val params = parameterFile.map(convertStringIntoKeyValue(_)).getOrElse(convertMapIntoKeyValue(parameters))
val annos = annotationFile
.map(convertStringIntoKeyValue(_, feed, web))
.getOrElse(convertMapIntoKeyValue(annotations, feed, web))
(params, annos)
}
def convertStringIntoKeyValue(file: String,
feed: Option[String] = None,
web: Option[String] = None): Array[JsValue] = {
val input = FileUtils.readFileToString(new File(file), StandardCharsets.UTF_8)
val in = input.parseJson.convertTo[Map[String, JsValue]]
convertMapIntoKeyValue(in, feed, web)
}
def convertMapIntoKeyValue(params: Map[String, JsValue],
feed: Option[String] = None,
web: Option[String] = None,
oldParams: List[JsObject] = List.empty): Array[JsValue] = {
val newParams =
params
.map { case (key, value) => JsObject("key" -> key.toJson, "value" -> value) } ++ feed.map(f =>
JsObject("key" -> "feed".toJson, "value" -> f.toJson))
val paramsList = {
if (newParams.nonEmpty) newParams
else oldParams
}
val webOpt = web.map {
case "true" | "yes" =>
Seq(
JsObject("key" -> "web-export".toJson, "value" -> true.toJson),
JsObject("key" -> "raw-http".toJson, "value" -> false.toJson),
JsObject("key" -> "final".toJson, "value" -> true.toJson))
case "false" | "no" =>
Seq(
JsObject("key" -> "web-export".toJson, "value" -> false.toJson),
JsObject("key" -> "raw-http".toJson, "value" -> false.toJson),
JsObject("key" -> "final".toJson, "value" -> false.toJson))
case "raw" =>
Seq(
JsObject("key" -> "web-export".toJson, "value" -> true.toJson),
JsObject("key" -> "raw-http".toJson, "value" -> true.toJson),
JsObject("key" -> "final".toJson, "value" -> true.toJson))
case _ =>
Seq.empty
}
webOpt
.map(paramsList ++ _)
.getOrElse(paramsList)
.toArray
}
def entityName(name: String)(implicit wp: WskProps): String = {
val sep = "/"
if (name.startsWith(sep)) name.substring(name.indexOf(sep, name.indexOf(sep) + 1) + 1, name.length)
else name
}
def fullEntityName(name: String)(implicit wp: WskProps): String = {
val (ns, rest) = getNamespaceEntityName(name)
if (rest.nonEmpty) s"/$ns/$rest"
else s"/$ns"
}
def convertIntoComponents(comps: String)(implicit wp: WskProps): Array[JsValue] = {
comps.split(",").filter(_.nonEmpty).map(comp => fullEntityName(comp).toJson)
}
def getRespData(resp: HttpResponse): String = {
val timeout = toStrictTimeout
Try(resp.entity.toStrict(timeout).map { _.data }.map(_.utf8String).futureValue).getOrElse("")
}
def getTransactionId(resp: HttpResponse): String = {
val tidHeader = resp.headers.find(_.is(TransactionId.generatorConfig.lowerCaseHeader))
withClue(
s"The header ${TransactionId.generatorConfig} is not set. This means that the request did not reach nginx (or the controller if nginx is skipped in that test).") {
tidHeader shouldBe defined
}
tidHeader.get.value
}
def getNamespaceEntityName(name: String)(implicit wp: WskProps): (String, String) = {
name.split("/") match {
// Example: /namespace/package_name/entity_name
case Array(empty, namespace, packageName, entityName) if empty.isEmpty => (namespace, s"$packageName/$entityName")
// Example: /namespace/entity_name
case Array(empty, namespace, entityName) if empty.isEmpty => (namespace, entityName)
// Example: namespace/package_name/entity_name
case Array(namespace, packageName, entityName) => (namespace, s"$packageName/$entityName")
// Example: /namespace
case Array(empty, namespace) if empty.isEmpty => (namespace, "")
// Example: package_name/entity_name
case Array(packageName, entityName) if !packageName.isEmpty => (wp.namespace, s"$packageName/$entityName")
// Example: entity_name
case Array(entityName) => (wp.namespace, entityName)
case _ => (wp.namespace, name)
}
}
def invokeAction(name: String,
parameters: Map[String, JsValue] = Map.empty,
parameterFile: Option[String] = None,
blocking: Boolean = false,
result: Boolean = false,
web: Boolean = false,
expectedExitCode: Int = Accepted.intValue)(implicit wp: WskProps): RestResult = {
val (ns, actName) = getNamespaceEntityName(name)
val path =
if (web) Path(s"$basePath/web/$systemNamespace/$actName.http")
else Path(s"$basePath/namespaces/$ns/actions/$actName")
val paramMap = Map("blocking" -> blocking.toString, "result" -> result.toString)
val input = parameterFile map { pf =>
Some(FileUtils.readFileToString(new File(pf), StandardCharsets.UTF_8))
} getOrElse Some(parameters.toJson.toString)
val resp = requestEntity(POST, path, paramMap, input)
val rr = new RestResult(resp.status.intValue, getTransactionId(resp), getRespData(resp), blocking)
// If the statusCode does not not equal to expectedExitCode, it is acceptable that the statusCode
// equals to 200 for the case that either blocking or result is set to true.
if (!isStatusCodeExpected(expectedExitCode, rr.statusCode.intValue)) {
if (blocking || result) {
validateStatusCode(OK.intValue, rr.statusCode.intValue)
} else {
rr.statusCode.intValue shouldBe expectedExitCode
}
}
rr
}
}
object RestResult {
def getField(obj: JsObject, key: String): String = {
obj.fields.get(key).map(_.convertTo[String]).getOrElse("")
}
def getFieldJsObject(obj: JsObject, key: String): JsObject = {
obj.fields.get(key).map(_.asJsObject).getOrElse(JsObject.empty)
}
def getFieldJsValue(obj: JsObject, key: String): JsValue = {
obj.fields.getOrElse(key, JsObject.empty)
}
def getFieldListJsObject(obj: JsObject, key: String): Vector[JsObject] = {
obj.fields.get(key).map(_.convertTo[Vector[JsObject]]).getOrElse(Vector(JsObject.empty))
}
def convertStausCodeToExitCode(statusCode: StatusCode, blocking: Boolean = false): Int = {
if ((statusCode == OK) || (!blocking && (statusCode == Accepted))) 0
else statusCode.intValue % 256
}
def convertHttpResponseToStderr(respData: String): String = {
Try(getField(respData.parseJson.asJsObject, "error")).getOrElse("")
}
}
class RestResult(val statusCode: StatusCode, val tid: String, val respData: String = "", blocking: Boolean = false)
extends RunResult(
RestResult.convertStausCodeToExitCode(statusCode, blocking),
respData,
RestResult.convertHttpResponseToStderr(respData)) {
override def toString: String = {
super.toString + s"""statusCode: $statusCode
|tid: $tid
|respData: $respData
|blocking: $blocking""".stripMargin
}
def respBody: JsObject = respData.parseJson.asJsObject
def getField(key: String): String = {
RestResult.getField(respBody, key)
}
def getFieldJsObject(key: String): JsObject = {
RestResult.getFieldJsObject(respBody, key)
}
def getFieldJsValue(key: String): JsValue = {
RestResult.getFieldJsValue(respBody, key)
}
def getFieldListJsObject(key: String): Vector[JsObject] = {
RestResult.getFieldListJsObject(respBody, key)
}
def getBodyListJsObject: Vector[JsObject] = {
respData.parseJson.convertTo[Vector[JsObject]]
}
def getBodyListString: Vector[String] = {
respData.parseJson.convertTo[Vector[String]]
}
}
class ApiAction(val name: String,
val namespace: String,
val backendMethod: String = "POST",
val backendUrl: String,
val authkey: String) {
def toJson: JsObject = {
JsObject(
"name" -> name.toJson,
"namespace" -> namespace.toJson,
"backendMethod" -> backendMethod.toJson,
"backendUrl" -> backendUrl.toJson,
"authkey" -> authkey.toJson)
}
}