blob: 15ee8a5cf49a996f6e6a17fbafa3d0a37f010a83 [file] [log] [blame]
/*
* Copyright 2015-2016 IBM Corporation
*
* Licensed 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
import java.io.File
import scala.Left
import scala.Right
import scala.collection.JavaConversions.mapAsJavaMap
import scala.collection.mutable.Buffer
import scala.concurrent.duration.Duration
import scala.concurrent.duration.DurationInt
import scala.language.postfixOps
import scala.util.Failure
import scala.util.Success
import scala.util.Try
import TestUtils._
import common.TestUtils.RunResult
import spray.json.JsObject
import spray.json.JsValue
import spray.json.pimpString
import whisk.utils.retry
import java.time.Instant
import whisk.core.entity.ByteSize
import org.scalatest.Matchers
/**
* Provide Scala bindings for the whisk CLI.
*
* Each of the top level CLI commands is a "noun" class that extends one
* of several traits that are common to the whisk collections and corresponds
* to one of the top level CLI nouns.
*
* Each of the "noun" classes mixes in the RunWskCmd trait which runs arbitrary
* wsk commands and returns the results. Optionally RunWskCmd can validate the exit
* code matched a desired value.
*
* The various collections support one or more of these as common traits:
* list, get, delete, and sanitize.
* Sanitize is akin to delete but accepts a failure because entity may not
* exit. Additionally, some of the nouns define custom commands.
*
* All of the commands define default values that are either optional
* or omitted in the common case. This makes for a compact implementation
* instead of using a Builder pattern.
*
* An implicit WskProps instance is required for all of CLI commands. This
* type provides the authentication key for the API as well as the namespace.
* It also sets the apihost and apiversion explicitly to avoid ambiguity with
* a local property file if it exists.
*/
case class WskProps(
authKey: String = WhiskProperties.readAuthKey(WhiskProperties.getAuthFileForTesting),
namespace: String = "_",
apiversion: String = "v1",
apihost: String = WhiskProperties.getEdgeHost) {
def overrides = Seq("-i", "--apihost", apihost, "--apiversion", apiversion)
}
class Wsk() extends RunWskCmd {
implicit val action = new WskAction
implicit val trigger = new WskTrigger
implicit val rule = new WskRule
implicit val activation = new WskActivation
implicit val pkg = new WskPackage
implicit val namespace = new WskNamespace
implicit val api = new WskApi
}
trait FullyQualifiedNames {
/**
* Fully qualifies the name of an entity with its namespace.
* If the name already starts with the PATHSEP character, then
* it already is fully qualified. Otherwise (package name or
* basic entity name) it is prefixed with the namespace. The
* namespace is derived from the implicit whisk properties.
*
* @param name to fully qualify iff it is not already fully qualified
* @param wp whisk properties
* @return name if it is fully qualified else a name fully qualified for a namespace
*/
def fqn(name: String)(implicit wp: WskProps) = {
val sep = "/" // Namespace.PATHSEP
if (name.startsWith(sep)) name
else s"$sep${wp.namespace}$sep$name"
}
/**
* Resolves a namespace. If argument is defined, it takes precedence.
* else resolve to namespace in implicit WskProps.
*
* @param namespace an optional namespace
* @param wp whisk properties
* @return resolved namespace
*/
def resolve(namespace: Option[String])(implicit wp: WskProps) = {
val sep = "/" // Namespace.PATHSEP
namespace getOrElse s"$sep${wp.namespace}"
}
}
trait ListOrGetFromCollection extends FullyQualifiedNames {
self: RunWskCmd =>
protected val noun: String
/**
* 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
*/
def list(
namespace: Option[String] = None,
limit: Option[Int] = None,
expectedExitCode: Int = SUCCESS_EXIT)(
implicit wp: WskProps): RunResult = {
val params = Seq(noun, "list", resolve(namespace), "--auth", wp.authKey) ++
{ limit map { l => Seq("--limit", l.toString) } getOrElse Seq() }
cli(wp.overrides ++ params, expectedExitCode)
}
/**
* 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
*/
def get(
name: String,
expectedExitCode: Int = SUCCESS_EXIT,
summary: Boolean = false,
fieldFilter: Option[String] = None)(
implicit wp: WskProps): RunResult = {
val params = Seq(noun, "get", "--auth", wp.authKey) ++
Seq(fqn(name)) ++
{ if (summary) Seq("--summary") else Seq() } ++
{ fieldFilter map { f => Seq(f) } getOrElse Seq() }
cli(wp.overrides ++ params, expectedExitCode)
}
}
trait DeleteFromCollection extends FullyQualifiedNames {
self: RunWskCmd =>
protected val noun: String
/**
* 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
*/
def delete(
name: String,
expectedExitCode: Int = SUCCESS_EXIT)(
implicit wp: WskProps): RunResult = {
cli(wp.overrides ++ Seq(noun, "delete", "--auth", wp.authKey, fqn(name)), expectedExitCode)
}
/**
* 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
*/
def sanitize(name: String)(implicit wp: WskProps): RunResult = {
delete(name, DONTCARE_EXIT)
}
}
trait HasActivation {
/**
* Extracts activation id from invoke (action or trigger) or activation get
*/
def extractActivationId(result: RunResult): Option[String] = {
Try {
// try to interpret the run result as the result of an invoke
extractActivationIdFromInvoke(result) getOrElse extractActivationIdFromActivation(result).get
} toOption
}
/**
* Extracts activation id from 'wsk activation get' run result
*/
private def extractActivationIdFromActivation(result: RunResult): Option[String] = {
Try {
// a characteristic string that comes right before the activationId
val idPrefix = "ok: got activation "
val stdout = result.stdout
assert(stdout.contains(idPrefix), stdout)
extractActivationId(idPrefix, stdout).get
} toOption
}
/**
* Extracts activation id from 'wsk action invoke' or 'wsk trigger invoke'
*/
private def extractActivationIdFromInvoke(result: RunResult): Option[String] = {
Try {
val stdout = result.stdout
assert(stdout.contains("ok: invoked") || stdout.contains("ok: triggered"), stdout)
// a characteristic string that comes right before the activationId
val idPrefix = "with id "
extractActivationId(idPrefix, stdout).get
} toOption
}
/**
* Extracts activation id preceded by a prefix (idPrefix) from a string (stdout)
*
* @param idPrefix the prefix of the activation id
* @param stdout the string to be used in the extraction
* @return an option containing the id as a string or None if the extraction failed for any reason
*/
private def extractActivationId(idPrefix: String, stdout: String): Option[String] = {
Try {
val start = stdout.indexOf(idPrefix) + idPrefix.length
var end = start
assert(start > 0)
while (end < stdout.length && stdout.charAt(end) != '\n')
end = end + 1
stdout.substring(start, end) // a uuid
} toOption
}
}
class WskAction()
extends RunWskCmd
with ListOrGetFromCollection
with DeleteFromCollection
with HasActivation {
override protected val noun = "action"
override def baseCommand = Wsk.baseCommand
/**
* Creates action. Parameters mirror those available in the CLI.
*
* @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
*/
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,
parameters: Map[String, JsValue] = Map(),
annotations: Map[String, JsValue] = Map(),
parameterFile: Option[String] = None,
annotationFile: Option[String] = None,
timeout: Option[Duration] = None,
memory: Option[ByteSize] = None,
logsize: Option[ByteSize] = None,
shared: Option[Boolean] = None,
update: Boolean = false,
expectedExitCode: Int = SUCCESS_EXIT)(
implicit wp: WskProps): RunResult = {
val params = Seq(noun, if (!update) "create" else "update", "--auth", wp.authKey, fqn(name)) ++
{ artifact map { Seq(_) } getOrElse Seq() } ++
{
kind map { k =>
if (k == "docker" || k == "sequence" || k == "copy") Seq(s"--$k")
else Seq("--kind", k)
} getOrElse Seq()
} ++
{ main.toSeq flatMap { p => Seq("--main", p) } } ++
{ parameters flatMap { p => Seq("-p", p._1, p._2.compactPrint) } } ++
{ annotations flatMap { p => Seq("-a", p._1, p._2.compactPrint) } } ++
{ parameterFile map { pf => Seq("-P", pf) } getOrElse Seq() } ++
{ annotationFile map { af => Seq("-A", af) } getOrElse Seq() } ++
{ timeout map { t => Seq("-t", t.toMillis.toString) } getOrElse Seq() } ++
{ memory map { m => Seq("-m", m.toMB.toString) } getOrElse Seq() } ++
{ logsize map { l => Seq("-l", l.toMB.toString) } getOrElse Seq() } ++
{ shared map { s => Seq("--shared", if (s) "yes" else "no") } getOrElse Seq() }
cli(wp.overrides ++ params, expectedExitCode)
}
/**
* Invokes action. Parameters mirror those available in the CLI.
*
* @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
*/
def invoke(
name: String,
parameters: Map[String, JsValue] = Map(),
parameterFile: Option[String] = None,
blocking: Boolean = false,
result: Boolean = false,
expectedExitCode: Int = SUCCESS_EXIT)(
implicit wp: WskProps): RunResult = {
val params = Seq(noun, "invoke", "--auth", wp.authKey, fqn(name)) ++
{ parameters flatMap { p => Seq("-p", p._1, p._2.compactPrint) } } ++
{ parameterFile map { pf => Seq("-P", pf) } getOrElse Seq() } ++
{ if (blocking) Seq("--blocking") else Seq() } ++
{ if (result) Seq("--result") else Seq() }
cli(wp.overrides ++ params, expectedExitCode)
}
}
class WskTrigger()
extends RunWskCmd
with ListOrGetFromCollection
with DeleteFromCollection
with HasActivation {
override protected val noun = "trigger"
/**
* Creates trigger. Parameters mirror those available in the CLI.
*
* @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
*/
def create(
name: String,
parameters: Map[String, JsValue] = Map(),
annotations: Map[String, JsValue] = Map(),
parameterFile: Option[String] = None,
annotationFile: Option[String] = None,
feed: Option[String] = None,
shared: Option[Boolean] = None,
update: Boolean = false,
expectedExitCode: Int = SUCCESS_EXIT)(
implicit wp: WskProps): RunResult = {
val params = Seq(noun, if (!update) "create" else "update", "--auth", wp.authKey, fqn(name)) ++
{ feed map { f => Seq("--feed", fqn(f)) } getOrElse Seq() } ++
{ parameters flatMap { p => Seq("-p", p._1, p._2.compactPrint) } } ++
{ annotations flatMap { p => Seq("-a", p._1, p._2.compactPrint) } } ++
{ parameterFile map { pf => Seq("-P", pf) } getOrElse Seq() } ++
{ annotationFile map { af => Seq("-A", af) } getOrElse Seq() } ++
{ shared map { s => Seq("--shared", if (s) "yes" else "no") } getOrElse Seq() }
cli(wp.overrides ++ params, expectedExitCode)
}
/**
* Fires trigger. Parameters mirror those available in the CLI.
*
* @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
*/
def fire(
name: String,
parameters: Map[String, JsValue] = Map(),
parameterFile: Option[String] = None,
expectedExitCode: Int = SUCCESS_EXIT)(
implicit wp: WskProps): RunResult = {
val params = Seq(noun, "fire", "--auth", wp.authKey, fqn(name)) ++
{ parameters flatMap { p => Seq("-p", p._1, p._2.compactPrint) } } ++
{ parameterFile map { pf => Seq("-P", pf) } getOrElse Seq() }
cli(wp.overrides ++ params, expectedExitCode)
}
}
class WskRule()
extends RunWskCmd
with ListOrGetFromCollection
with DeleteFromCollection
with WaitFor {
override protected val noun = "rule"
/**
* Creates rule. Parameters mirror those available in the CLI.
*
* @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
*/
def create(
name: String,
trigger: String,
action: String,
annotations: Map[String, JsValue] = Map(),
shared: Option[Boolean] = None,
update: Boolean = false,
expectedExitCode: Int = SUCCESS_EXIT)(
implicit wp: WskProps): RunResult = {
val params = Seq(noun, if (!update) "create" else "update", "--auth", wp.authKey, fqn(name), (trigger), (action)) ++
{ annotations flatMap { p => Seq("-a", p._1, p._2.compactPrint) } } ++
{ shared map { s => Seq("--shared", if (s) "yes" else "no") } getOrElse Seq() }
val result = cli(wp.overrides ++ params, expectedExitCode)
if (expectedExitCode == SUCCESS_EXIT) assert(result.stdout.contains("ok:"), result)
result
}
/**
* Deletes 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 delete(
name: String,
expectedExitCode: Int = SUCCESS_EXIT)(
implicit wp: WskProps): RunResult = {
super.delete(name, expectedExitCode)
}
/**
* 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
*/
def enableRule(
name: String,
expectedExitCode: Int = SUCCESS_EXIT)(
implicit wp: WskProps): RunResult = {
val result = cli(wp.overrides ++ Seq(noun, "enable", "--auth", wp.authKey, fqn(name)), expectedExitCode)
assert(result.stdout.contains("ok:"), result)
result
}
/**
* 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
*/
def disableRule(
name: String,
expectedExitCode: Int = SUCCESS_EXIT)(
implicit wp: WskProps): RunResult = {
val result = cli(wp.overrides ++ Seq(noun, "disable", "--auth", wp.authKey, fqn(name)), expectedExitCode)
assert(result.stdout.contains("ok:"), result)
result
}
/**
* 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
*/
def checkRuleState(
name: String,
active: Boolean)(
implicit wp: WskProps): Boolean = {
val result = cli(wp.overrides ++ Seq(noun, "status", "--auth", wp.authKey, fqn(name))).stdout
if (active) {
result.contains("is active")
} else {
result.contains("is inactive")
}
}
}
class WskActivation()
extends RunWskCmd
with HasActivation
with WaitFor {
protected val noun = "activation"
/**
* Activation polling console.
*
* @param duration exits console after duration
* @param since (optional) time travels back to activation since given duration
*/
def console(
duration: Duration,
since: Option[Duration] = None,
expectedExitCode: Int = SUCCESS_EXIT)(
implicit wp: WskProps): RunResult = {
val params = Seq(noun, "poll", "--auth", wp.authKey, "--exit", duration.toSeconds.toString) ++
{ since map { s => Seq("--since-seconds", s.toSeconds.toString) } getOrElse Seq() }
cli(wp.overrides ++ params, expectedExitCode)
}
/**
* 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 list(
filter: Option[String] = None,
limit: Option[Int] = None,
since: Option[Instant] = None,
expectedExitCode: Int = SUCCESS_EXIT)(
implicit wp: WskProps): RunResult = {
val params = Seq(noun, "list", "--auth", wp.authKey) ++
{ filter map { Seq(_) } getOrElse Seq() } ++
{ limit map { l => Seq("--limit", l.toString) } getOrElse Seq() } ++
{ since map { i => Seq("--since", i.toEpochMilli.toString) } getOrElse Seq() }
cli(wp.overrides ++ params, expectedExitCode)
}
/**
* 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 ids(rr: RunResult): Seq[String] = {
rr.stdout.split("\n") filter {
// remove empty lines the header
s => s.nonEmpty && s != "activations"
} map {
// split into (id, name)
_.split(" ")(0)
}
}
/**
* Gets activation 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 get(
activationId: String,
expectedExitCode: Int = SUCCESS_EXIT,
fieldFilter: Option[String] = None)(
implicit wp: WskProps): RunResult = {
val params = { fieldFilter map { f => Seq(f) } getOrElse Seq() }
cli(wp.overrides ++ Seq(noun, "get", "--auth", wp.authKey, activationId) ++ params, expectedExitCode)
}
/**
* 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 logs(
activationId: String,
expectedExitCode: Int = SUCCESS_EXIT)(
implicit wp: WskProps): RunResult = {
cli(wp.overrides ++ Seq(noun, "logs", activationId, "--auth", wp.authKey), expectedExitCode)
}
/**
* 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 result(
activationId: String,
expectedExitCode: Int = SUCCESS_EXIT)(
implicit wp: WskProps): RunResult = {
cli(wp.overrides ++ Seq(noun, "result", activationId, "--auth", wp.authKey), expectedExitCode)
}
/**
* 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 retries the maximum retries (total timeout is retries + 1 seconds)
* @return activation ids found, caller must check length of sequence
*/
def pollFor(
N: Int,
entity: Option[String],
limit: Option[Int] = None,
since: Option[Instant] = None,
retries: Int = 10,
pollPeriod: Duration = 1.second)(
implicit wp: WskProps): Seq[String] = {
Try {
retry({
val result = ids(list(filter = entity, limit = limit, since = since))
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()
}
}
/**
* Polls for an activation matching the given id. If found
* return Right(activation) else Left(result of running CLI command).
*
* @return either Left(error message) or Right(activation as JsObject)
*/
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 = cli(wp.overrides ++ Seq(noun, "get", activationId, "--auth", wp.authKey),
expectedExitCode = DONTCARE_EXIT)
if (result.exitCode == NOT_FOUND) {
null
} else if (result.exitCode == SUCCESS_EXIT) {
Right(result.stdout)
} else Left(s"$result")
}, initialWait, pollPeriod, totalWait)
Option(activation) map {
case Right(stdout) =>
Try {
// strip first line and convert the rest to JsObject
assert(stdout.startsWith("ok: got activation"))
parseJsonString(stdout)
} map {
Right(_)
} getOrElse Left(s"cannot parse activation from '$stdout'")
case Left(error) => Left(error)
} getOrElse Left(s"$activationId not found")
}
/** Used in polling for activations to record partial results from retry poll. */
private case class PartialResult(ids: Seq[String]) extends Throwable
}
class WskNamespace()
extends RunWskCmd
with FullyQualifiedNames {
protected val noun = "namespace"
/**
* Lists available namespaces for whisk properties.
*
* @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 list(expectedExitCode: Int = SUCCESS_EXIT)(
implicit wp: WskProps): RunResult = {
val params = Seq(noun, "list", "--auth", wp.authKey)
cli(wp.overrides ++ params, expectedExitCode)
}
/**
* Gets entities in namespace.
*
* @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
*/
def get(
namespace: Option[String] = None,
expectedExitCode: Int)(
implicit wp: WskProps): RunResult = {
cli(wp.overrides ++ Seq(noun, "get", resolve(namespace), "--auth", wp.authKey), expectedExitCode)
}
}
class WskPackage()
extends RunWskCmd
with ListOrGetFromCollection
with DeleteFromCollection {
override protected val noun = "package"
/**
* Creates package. Parameters mirror those available in the CLI.
*
* @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
*/
def create(
name: String,
parameters: Map[String, JsValue] = Map(),
annotations: Map[String, JsValue] = Map(),
parameterFile: Option[String] = None,
annotationFile: Option[String] = None,
shared: Option[Boolean] = None,
update: Boolean = false,
expectedExitCode: Int = SUCCESS_EXIT)(
implicit wp: WskProps): RunResult = {
val params = Seq(noun, if (!update) "create" else "update", "--auth", wp.authKey, fqn(name)) ++
{ parameters flatMap { p => Seq("-p", p._1, p._2.compactPrint) } } ++
{ annotations flatMap { p => Seq("-a", p._1, p._2.compactPrint) } } ++
{ parameterFile map { pf => Seq("-P", pf) } getOrElse Seq() } ++
{ annotationFile map { af => Seq("-A", af) } getOrElse Seq() } ++
{ shared map { s => Seq("--shared", if (s) "yes" else "no") } getOrElse Seq() }
cli(wp.overrides ++ params, expectedExitCode)
}
/**
* Binds package. Parameters mirror those available in the CLI.
*
* @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
*/
def bind(
provider: String,
name: String,
parameters: Map[String, JsValue] = Map(),
annotations: Map[String, JsValue] = Map(),
expectedExitCode: Int = SUCCESS_EXIT)(
implicit wp: WskProps): RunResult = {
val params = Seq(noun, "bind", "--auth", wp.authKey, fqn(provider), fqn(name)) ++
{ parameters flatMap { p => Seq("-p", p._1, p._2.compactPrint) } } ++
{ annotations flatMap { p => Seq("-a", p._1, p._2.compactPrint) } }
cli(wp.overrides ++ params, expectedExitCode)
}
}
class WskApi()
extends RunWskCmd {
protected val noun = "api-experimental"
/**
* Creates and API endpoint. Parameters mirror those available in the CLI.
*
* @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 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,
expectedExitCode: Int = SUCCESS_EXIT)(
implicit wp: WskProps): RunResult = {
val params = Seq(noun, "create", "--auth", wp.authKey) ++
{ basepath map { b => Seq(b) } getOrElse Seq() } ++
{ relpath map { r => Seq(r) } getOrElse Seq() } ++
{ operation map { o => Seq(o) } getOrElse Seq() } ++
{ action map { aa => Seq(aa) } getOrElse Seq() } ++
{ apiname map { a => Seq("--apiname", a) } getOrElse Seq() } ++
{ swagger map { s => Seq("--config-file", s) } getOrElse Seq() }
cli(wp.overrides ++ params, expectedExitCode, showCmd = true)
}
/**
* Retrieve a list of API endpoints. Parameters mirror those available in the CLI.
*
* @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 list(
basepathOrApiName: Option[String] = None,
relpath: Option[String] = None,
operation: Option[String] = None,
limit: Option[Int] = None,
since: Option[Instant] = None,
expectedExitCode: Int = SUCCESS_EXIT)(
implicit wp: WskProps): RunResult = {
val params = Seq(noun, "list", "--auth", wp.authKey) ++
{ basepathOrApiName map { b => Seq(b) } getOrElse Seq() } ++
{ relpath map { r => Seq(r) } getOrElse Seq() } ++
{ operation map { o => Seq(o) } getOrElse Seq() } ++
{ limit map { l => Seq("--limit", l.toString) } getOrElse Seq() } ++
{ since map { i => Seq("--since", i.toEpochMilli.toString) } getOrElse Seq() }
cli(wp.overrides ++ params, expectedExitCode, showCmd = true)
}
/**
* Retieves an API's configuration. Parameters mirror those available in the CLI.
* 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
*/
def get(
basepathOrApiName: Option[String] = None,
full: Option[Boolean] = None,
expectedExitCode: Int = SUCCESS_EXIT)(
implicit wp: WskProps): RunResult = {
val params = Seq(noun, "get", "--auth", wp.authKey) ++
{ basepathOrApiName map { b => Seq(b) } getOrElse Seq() } ++
{ full map { f => if (f) Seq("--full") else Seq() } getOrElse Seq() }
cli(wp.overrides ++ params, expectedExitCode, showCmd = true)
}
/**
* Delete an entire API or a subset of API endpoints. Parameters mirror those available in the CLI.
*
* @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 delete(
basepathOrApiName: String,
relpath: Option[String] = None,
operation: Option[String] = None,
expectedExitCode: Int = SUCCESS_EXIT)(
implicit wp: WskProps): RunResult = {
val params = Seq(noun, "delete", "--auth", wp.authKey, basepathOrApiName) ++
{ relpath map { r => Seq(r) } getOrElse Seq() } ++
{ operation map { o => Seq(o) } getOrElse Seq() }
cli(wp.overrides ++ params, expectedExitCode, showCmd = true)
}
}
trait WaitFor {
/**
* Waits up to totalWait seconds for a 'step' to return value.
* Often tests call this routine immediately after starting work.
* Performs an initial wait before entering poll loop.
*/
def waitfor[T](
step: () => T,
initialWait: Duration = 1 second,
pollPeriod: Duration = 1 second,
totalWait: Duration = 30 seconds): T = {
Thread.sleep(initialWait.toMillis)
val endTime = System.currentTimeMillis() + totalWait.toMillis
while (System.currentTimeMillis() < endTime) {
val predicate = step()
predicate match {
case (t: Boolean) if t =>
return predicate
case (t: Any) if t != null && !t.isInstanceOf[Boolean] =>
return predicate
case _ if System.currentTimeMillis() >= endTime =>
return predicate
case _ =>
Thread.sleep(pollPeriod.toMillis)
}
}
null.asInstanceOf[T]
}
}
object Wsk {
private val binaryName = "wsk"
/** What is the path to a downloaded CLI? **/
private def getDownloadedGoCLIPath = {
s"${System.getProperty("user.home")}${File.separator}.local${File.separator}bin${File.separator}${binaryName}"
}
def exists() = {
val cliPath = if (WhiskProperties.useCLIDownload) getDownloadedGoCLIPath else WhiskProperties.getCLIPath
assert((new File(cliPath)).exists, s"did not find $cliPath")
}
def baseCommand() =
if (WhiskProperties.useCLIDownload) Buffer(getDownloadedGoCLIPath) else Buffer(WhiskProperties.getCLIPath)
}
sealed trait RunWskCmd extends Matchers {
/**
* The base command to run.
*/
def baseCommand = Wsk.baseCommand
/**
* Runs a command wsk [params] where the arguments come in as a sequence.
*
* @return RunResult which contains stdout, stderr, exit code
*/
def cli(params: Seq[String],
expectedExitCode: Int = SUCCESS_EXIT,
verbose: Boolean = false,
env: Map[String, String] = Map("WSK_CONFIG_FILE" -> ""),
workingDir: File = new File("."),
showCmd: Boolean = false): RunResult = {
val args = baseCommand
if (verbose) args += "--verbose"
if (showCmd) println(args.mkString(" ") + " " + params.mkString(" "))
val rr = TestUtils.runCmd(DONTCARE_EXIT, workingDir, TestUtils.logger, sys.env ++ env, args ++ params: _*)
withClue(reportFailure(args ++ params, expectedExitCode, rr)) {
if (expectedExitCode != TestUtils.DONTCARE_EXIT) {
val ok = (rr.exitCode == expectedExitCode) || (expectedExitCode == TestUtils.ANY_ERROR_EXIT && rr.exitCode != 0)
if (!ok) {
rr.exitCode shouldBe expectedExitCode
}
}
}
rr
}
/*
* Utility function to return a JSON object from the CLI output that returns
* an optional a status line following by the JSON data
*/
def parseJsonString(jsonStr: String): JsObject = {
jsonStr.substring(jsonStr.indexOf("\n") + 1).parseJson.asJsObject // Skip optional status line before parsing
}
private def reportFailure(args: Buffer[String], ec: Integer, rr: RunResult) = {
val s = new StringBuilder()
s.append(args.mkString(" ") + "\n")
if (rr.stdout.nonEmpty) s.append(rr.stdout + "\n")
if (rr.stderr.nonEmpty) s.append(rr.stderr)
s.append("exit code:")
}
}
object WskAdmin {
private val binDir = WhiskProperties.getFileRelativeToWhiskHome("bin")
private val binaryName = "wskadmin"
def exists = {
val dir = binDir
val exec = new File(dir, binaryName)
assert(dir.exists, s"did not find $dir")
assert(exec.exists, s"did not find $exec")
}
def baseCommand = {
Buffer(WhiskProperties.python, new File(binDir, binaryName).toString)
}
/**
* returns user given the auth key
*/
def getUser(authKey: String): (String, String) = {
val wskadmin = new RunWskAdminCmd {}
val user = wskadmin.cli(Seq("user", "whois", authKey)).stdout.trim
assert(!user.contains("Subject id is not recognized"), s"failed to retrieve user from authkey '$authKey'")
val Seq(rawSubject, rawNamespace) = user.lines.toSeq
(rawSubject.replaceFirst("subject: ", ""), rawNamespace.replaceFirst("namespace: ", ""))
}
}
trait RunWskAdminCmd extends RunWskCmd {
override def baseCommand = WskAdmin.baseCommand
}