You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@openwhisk.apache.org by ch...@apache.org on 2019/08/15 20:19:28 UTC

[openwhisk] branch master updated: Api Gateway support in OpenWhisk Standalone mode (#4571)

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

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


The following commit(s) were added to refs/heads/master by this push:
     new 1f3504f  Api Gateway support in OpenWhisk Standalone mode (#4571)
1f3504f is described below

commit 1f3504faefafe5d6e7fd1c21790de3064d0e7d18
Author: Chetan Mehrotra <ch...@apache.org>
AuthorDate: Fri Aug 16 01:49:17 2019 +0530

    Api Gateway support in OpenWhisk Standalone mode (#4571)
    
    Enables support for API Gateway in Standalone jar
    
    * Build and copy route management actions to standalone jar
    * Install route management packages on startup
    * Launches Redis and API Gateway Containers
    
    To enable calls to be routed from within the Api Gateway container to Controller (running out of container) we need to determine the ip address of host. This is handled depending on the host OS.
---
 core/standalone/README.md                          |  16 +-
 core/standalone/build.gradle                       |  52 ++++++
 core/standalone/src/main/resources/standalone.conf |  10 ++
 .../openwhisk/standalone/ApiGwLauncher.scala       | 139 ++++++++++++++++
 .../openwhisk/standalone/DockerVersion.scala       |  50 ++++++
 .../openwhisk/standalone/InstallRouteMgmt.scala    | 107 +++++++++++++
 .../openwhisk/standalone/PreFlightChecks.scala     |  44 +++++-
 .../openwhisk/standalone/ServerStartupCheck.scala  |  31 +++-
 .../openwhisk/standalone/ServiceInfoLogger.scala   |  37 ++++-
 .../standalone/StandaloneDockerSupport.scala       | 176 +++++++++++++++++++++
 .../openwhisk/standalone/StandaloneOpenWhisk.scala | 126 ++++++++++++---
 .../openwhisk/standalone/DockerVersionTests.scala  |  23 ++-
 tests/build.gradle                                 |   5 +-
 ...erverTests.scala => StandaloneApiGwTests.scala} |  21 ++-
 .../standalone/StandaloneServerFixture.scala       |  33 ++--
 .../standalone/StandaloneServerTests.scala         |  20 +++
 tools/travis/runUnitTests.sh                       |   3 +
 17 files changed, 824 insertions(+), 69 deletions(-)

diff --git a/core/standalone/README.md b/core/standalone/README.md
index 299e99a..6b9aef9 100644
--- a/core/standalone/README.md
+++ b/core/standalone/README.md
@@ -75,8 +75,11 @@ $ java -jar openwhisk-standalone.jar -h
  \   \  /  \/    \___/| .__/ \___|_| |_|__/\__|_| |_|_|___/_|\_\
   \___\/ tm           |_|
 
+      --api-gw                  Enable API Gateway support
+      --api-gw-port  <arg>      Api Gateway Port
   -c, --config-file  <arg>      application.conf which overrides the default
                                 standalone.conf
+  -d, --data-dir  <arg>         Directory used for storage
       --disable-color-logging   Disables colored logging
   -m, --manifest  <arg>         Manifest json defining the supported runtimes
   -p, --port  <arg>             Server port
@@ -85,6 +88,7 @@ $ java -jar openwhisk-standalone.jar -h
       --version                 Show version of this program
 
 OpenWhisk standalone server
+
 ```
 
 Sections below would illustrate some of the supported options
@@ -189,5 +193,13 @@ whisk {
 
 Then pass this config file via `-c` option.
 
-[1]: https://github.com/apache/openwhisk/blob/master/docs/cli.md
-[2]: https://github.com/apache/openwhisk/blob/master/docs/samples.md
+#### Using Api Gateway
+
+Api Gateway mode can be enabled via `--api-gw` flag. In this mode upon launch a separate container for [OpenWhisk Api gateway][3]
+would be launched on port `3234` (can be changed with `--api-gw-port`). In this mode you can make use of the
+[api gateway][4] support.
+
+[1]: https://github.com/apache/incubator-openwhisk/blob/master/docs/cli.md
+[2]: https://github.com/apache/incubator-openwhisk/blob/master/docs/samples.md
+[3]: https://github.com/apache/incubator-openwhisk-apigateway
+[4]: https://github.com/apache/incubator-openwhisk/blob/master/docs/apigateway.md
diff --git a/core/standalone/build.gradle b/core/standalone/build.gradle
index 232a445..9225032 100644
--- a/core/standalone/build.gradle
+++ b/core/standalone/build.gradle
@@ -46,7 +46,42 @@ task copySwagger(type: Copy) {
     project.ext.swaggerUiDir = file("$buildDir/tmp/swagger/swagger-ui/swagger-ui-${version}/dist")
 }
 
+def apiGwActions = ['createApi', "deleteApi", "getApi"]
+
+task copyGWActions() {
+    doLast {
+        def routeMgmtDir = new File(project.projectDir.getParentFile(), "routemgmt")
+        def commonDir = new File(routeMgmtDir, "common")
+        def routeBuildDir = mkdir("$buildDir/tmp/routemgmt")
+        apiGwActions.each { actionName ->
+            def zipFileName = actionName + ".zip"
+            def actionDir = new File(routeMgmtDir, actionName)
+            def zipFile = new File(routeBuildDir, zipFileName)
+            if (!zipFile.exists()) {
+                ant.exec(dir:actionDir, executable:"npm", failonerror:true){
+                    arg(line:"install")
+                }
+                ant.zip(destfile:zipFile){
+                    fileset(dir:actionDir){
+                        exclude(name:zipFileName)
+                        //Somehow if in zip we add same file twice it causes issues
+                        //Its possible that installRouteMgmt.sh has been invoked which would
+                        //have copied the common files to each action dir.
+                        //Exclude such files to zipped twice
+                        commonDir.listFiles().each {f ->
+                            exclude(name:f.name)
+                        }
+                    }
+                    fileset(dir:commonDir)
+                }
+                logger.info("Create action zip $zipFileName")
+            }
+        }
+    }
+}
+
 processResources.dependsOn copySwagger
+processResources.dependsOn copyGWActions
 
 processResources {
     from(new File(project.rootProject.projectDir, "ansible/files/runtimes.json")) {
@@ -64,6 +99,11 @@ processResources {
         exclude "index.html"
         into("swagger-ui")
     }
+    apiGwActions.each { action ->
+        from(file("$buildDir/tmp/routemgmt/${action}.zip")){
+            into(".")
+        }
+    }
 }
 
 task copyBootJarToBin(type:Copy){
@@ -86,5 +126,17 @@ dependencies {
     compile project(':core:controller')
     compile project(':tools:admin')
     compile 'org.rogach:scallop_2.12:3.3.1'
+
+    testCompile 'junit:junit:4.11'
+    testCompile 'org.scalatest:scalatest_2.12:3.0.5'
 }
 
+gradle.projectsEvaluated {
+    tasks.withType(Test) {
+        testLogging {
+            events "passed", "skipped", "failed"
+            showStandardStreams = true
+            exceptionFormat = 'full'
+        }
+    }
+}
diff --git a/core/standalone/src/main/resources/standalone.conf b/core/standalone/src/main/resources/standalone.conf
index b10b093..cf2a091 100644
--- a/core/standalone/src/main/resources/standalone.conf
+++ b/core/standalone/src/main/resources/standalone.conf
@@ -81,4 +81,14 @@ whisk {
     file-system : false
     dir-path : "BOOT-INF/classes/swagger-ui"
   }
+
+  standalone {
+    redis {
+      image: "redis:4.0"
+    }
+
+    api-gateway {
+      image: "openwhisk/apigateway:nightly"
+    }
+  }
 }
diff --git a/core/standalone/src/main/scala/org/apache/openwhisk/standalone/ApiGwLauncher.scala b/core/standalone/src/main/scala/org/apache/openwhisk/standalone/ApiGwLauncher.scala
new file mode 100644
index 0000000..31d7562
--- /dev/null
+++ b/core/standalone/src/main/scala/org/apache/openwhisk/standalone/ApiGwLauncher.scala
@@ -0,0 +1,139 @@
+/*
+ * 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 org.apache.openwhisk.standalone
+
+import akka.actor.{ActorSystem, Scheduler}
+import akka.http.scaladsl.model.Uri
+import akka.pattern.RetrySupport
+import org.apache.openwhisk.common.{Logging, TransactionId}
+import org.apache.openwhisk.core.containerpool.docker.BrokenDockerContainer
+import org.apache.openwhisk.standalone.StandaloneDockerSupport.{containerName, createRunCmd}
+import pureconfig.loadConfigOrThrow
+
+import scala.concurrent.duration._
+import scala.concurrent.{ExecutionContext, Future}
+
+class ApiGwLauncher(docker: StandaloneDockerClient, apiGwApiPort: Int, apiGwMgmtPort: Int, serverPort: Int)(
+  implicit logging: Logging,
+  ec: ExecutionContext,
+  actorSystem: ActorSystem,
+  tid: TransactionId)
+    extends RetrySupport {
+  private implicit val scd: Scheduler = actorSystem.scheduler
+  case class RedisConfig(image: String)
+  case class ApiGwConfig(image: String)
+  private val redisConfig = loadConfigOrThrow[RedisConfig](StandaloneConfigKeys.redisConfigKey)
+  private val apiGwConfig = loadConfigOrThrow[ApiGwConfig](StandaloneConfigKeys.apiGwConfigKey)
+
+  def run(): Future[Seq[ServiceContainer]] = {
+    for {
+      (redis, redisSvcs) <- runRedis()
+      _ <- waitForRedis(redis)
+      (_, apiGwSvcs) <- runApiGateway(redis)
+      _ <- waitForApiGw()
+    } yield Seq(redisSvcs, apiGwSvcs).flatten
+  }
+
+  def runRedis(): Future[(StandaloneDockerContainer, Seq[ServiceContainer])] = {
+    val defaultRedisPort = 6379
+    val redisPort = StandaloneDockerSupport.checkOrAllocatePort(defaultRedisPort)
+    logging.info(this, s"Starting Redis at $redisPort")
+
+    val params = Map("-p" -> Set(s"$redisPort:6379"))
+    val name = containerName("redis")
+    val args = createRunCmd(name, dockerRunParameters = params)
+    val f = runDetached(redisConfig.image, args, pull = true)
+    val sc = ServiceContainer(redisPort, "Redis", name)
+    f.map(c => (c, Seq(sc)))
+  }
+
+  def waitForRedis(c: StandaloneDockerContainer): Future[Unit] = {
+    retry(() => isRedisUp(c), 12, 5.seconds)
+  }
+
+  private def isRedisUp(c: StandaloneDockerContainer) = {
+    val args = Seq(
+      "run",
+      "--rm",
+      "--name",
+      containerName("redis-test"),
+      redisConfig.image,
+      "redis-cli",
+      "-h",
+      c.addr.host,
+      "-p",
+      "6379",
+      "ping")
+    docker.runCmd(args, docker.clientConfig.timeouts.run).map(out => require(out.toLowerCase == "pong"))
+  }
+
+  def runApiGateway(redis: StandaloneDockerContainer): Future[(StandaloneDockerContainer, Seq[ServiceContainer])] = {
+    val hostIp = StandaloneDockerSupport.getLocalHostIp()
+    val env = Map(
+      "BACKEND_HOST" -> s"http://$hostIp:$serverPort",
+      "REDIS_HOST" -> redis.addr.host,
+      "REDIS_PORT" -> "6379",
+      //This is the name used to render the final url. So should be localhost
+      //as that would be used by end user outside of docker
+      "PUBLIC_MANAGEDURL_HOST" -> StandaloneDockerSupport.getLocalHostName(),
+      "PUBLIC_MANAGEDURL_PORT" -> apiGwMgmtPort.toString)
+
+    logging.info(this, s"Starting Api Gateway at api port: $apiGwApiPort, management port: $apiGwMgmtPort")
+    val name = containerName("apigw")
+    val params = Map("-p" -> Set(s"$apiGwApiPort:9000", s"$apiGwMgmtPort:8080"))
+    val args = createRunCmd(name, env, params)
+
+    //TODO ExecManifest is scoped to core. Ideally we would like to do
+    // ExecManifest.ImageName(apiGwConfig.image).prefix.contains("openwhisk")
+    val pull = apiGwConfig.image.startsWith("openwhisk")
+    val f = runDetached(apiGwConfig.image, args, pull)
+    val sc = Seq(
+      ServiceContainer(apiGwApiPort, "Api Gateway - Api Service", name),
+      ServiceContainer(apiGwMgmtPort, "Api Gateway - Management Service", name))
+    f.map(c => (c, sc))
+  }
+
+  def waitForApiGw(): Future[Unit] = {
+    new ServerStartupCheck(
+      Uri(s"http://${StandaloneDockerSupport.getLocalHostName()}:$apiGwApiPort/v1/apis"),
+      "ApiGateway")
+      .waitForServerToStart()
+    Future.successful(())
+  }
+
+  private def runDetached(image: String, args: Seq[String], pull: Boolean): Future[StandaloneDockerContainer] = {
+    for {
+      _ <- if (pull) docker.pull(image) else Future.successful(())
+      id <- docker.run(image, args).recoverWith {
+        case t @ BrokenDockerContainer(brokenId, _) =>
+          // Remove the broken container - but don't wait or check for the result.
+          // If the removal fails, there is nothing we could do to recover from the recovery.
+          docker.rm(brokenId)
+          Future.failed(t)
+        case t => Future.failed(t)
+      }
+      ip <- docker.inspectIPAddress(id, StandaloneDockerSupport.network).recoverWith {
+        // remove the container immediately if inspect failed as
+        // we cannot recover that case automatically
+        case e =>
+          docker.rm(id)
+          Future.failed(e)
+      }
+    } yield StandaloneDockerContainer(id, ip)
+  }
+}
diff --git a/core/standalone/src/main/scala/org/apache/openwhisk/standalone/DockerVersion.scala b/core/standalone/src/main/scala/org/apache/openwhisk/standalone/DockerVersion.scala
new file mode 100644
index 0000000..c584f74
--- /dev/null
+++ b/core/standalone/src/main/scala/org/apache/openwhisk/standalone/DockerVersion.scala
@@ -0,0 +1,50 @@
+/*
+ * 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 org.apache.openwhisk.standalone
+
+import scala.util.control.NonFatal
+
+case class DockerVersion(major: Int, minor: Int, patch: Int) extends Ordered[DockerVersion] {
+  import scala.math.Ordered.orderingToOrdered
+  def compare(that: DockerVersion): Int =
+    (this.major, this.minor, this.patch) compare (that.major, that.minor, that.patch)
+
+  override def toString = s"$major.$minor.$patch"
+}
+
+object DockerVersion {
+  implicit val ord: Ordering[DockerVersion] = Ordering.by(unapply)
+  private val pattern = ".*Docker version ([\\d.]+).*".r
+
+  def apply(str: String): DockerVersion = {
+    try {
+      val parts = if (str != null && str.nonEmpty) str.split('.') else Array[String]()
+      val major = if (parts.length >= 1) parts(0).toInt else 0
+      val minor = if (parts.length >= 2) parts(1).toInt else 0
+      val patch = if (parts.length >= 3) parts(2).toInt else 0
+      DockerVersion(major, minor, patch)
+    } catch {
+      case NonFatal(_) => throw new IllegalArgumentException(s"bad docker version $str")
+    }
+  }
+
+  def fromVersionCommand(str: String): DockerVersion = {
+    val pattern(version) = str
+    apply(version)
+  }
+}
diff --git a/core/standalone/src/main/scala/org/apache/openwhisk/standalone/InstallRouteMgmt.scala b/core/standalone/src/main/scala/org/apache/openwhisk/standalone/InstallRouteMgmt.scala
new file mode 100644
index 0000000..ad61af2
--- /dev/null
+++ b/core/standalone/src/main/scala/org/apache/openwhisk/standalone/InstallRouteMgmt.scala
@@ -0,0 +1,107 @@
+/*
+ * 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 org.apache.openwhisk.standalone
+
+import java.io.File
+
+import akka.http.scaladsl.model.Uri
+import org.apache.commons.io.{FileUtils, IOUtils}
+import org.apache.openwhisk.common.TransactionId.systemPrefix
+import org.apache.openwhisk.common.{Logging, TransactionId}
+
+import scala.sys.process.ProcessLogger
+import scala.util.Try
+import scala.sys.process._
+
+case class InstallRouteMgmt(workDir: File,
+                            authKey: String,
+                            apiHost: Uri,
+                            namespace: String,
+                            gatewayUrl: Uri,
+                            wsk: String)(implicit log: Logging) {
+  case class Action(name: String, desc: String)
+  private val noopLogger = ProcessLogger(_ => ())
+  private implicit val tid: TransactionId = TransactionId(systemPrefix + "apiMgmt")
+  val actionNames = Array(
+    Action("createApi", "Create an API"),
+    Action("deleteApi", "Delete the API"),
+    Action("getApi", "Retrieve the specified API configuration (in JSON format)"))
+
+  def run(): Unit = {
+    require(wskExists, s"wsk command not found at $wsk. Route management actions cannot be installed")
+    log.info(this, packageUpdateCmd.!!.trim)
+    //TODO Optimize to ignore this if package already installed
+    actionNames.foreach { action =>
+      val name = action.name
+      val actionZip = new File(workDir, s"$name.zip")
+      FileUtils.copyURLToFile(IOUtils.resourceToURL(s"/$name.zip"), actionZip)
+      val cmd = createActionUpdateCmd(action, name, actionZip)
+      val result = cmd.!!.trim
+      log.info(this, s"Installed $name - $result")
+      FileUtils.deleteQuietly(actionZip)
+    }
+    //This log message is used by tests to confirm that actions are installed
+    log.info(this, "Installed Route Management Actions")
+  }
+
+  private def createActionUpdateCmd(action: Action, name: String, actionZip: File) = {
+    Seq(
+      wsk,
+      "--apihost",
+      apiHost.toString(),
+      "--auth",
+      authKey,
+      "action",
+      "update",
+      s"$namespace/apimgmt/$name",
+      actionZip.getAbsolutePath,
+      "-a",
+      "description",
+      action.desc,
+      "--kind",
+      "nodejs:default",
+      "-a",
+      "web-export",
+      "true",
+      "-a",
+      "final",
+      "true")
+  }
+
+  private def packageUpdateCmd = {
+    Seq(
+      wsk,
+      "--apihost",
+      apiHost.toString(),
+      "--auth",
+      authKey,
+      "package",
+      "update",
+      s"$namespace/apimgmt",
+      "--shared",
+      "no",
+      "-a",
+      "description",
+      "This package manages the gateway API configuration.",
+      "-p",
+      "gwUrlV2",
+      gatewayUrl.toString())
+  }
+
+  def wskExists: Boolean = Try(s"$wsk property get --cliversion".!(noopLogger)).getOrElse(-1) == 0
+}
diff --git a/core/standalone/src/main/scala/org/apache/openwhisk/standalone/PreFlightChecks.scala b/core/standalone/src/main/scala/org/apache/openwhisk/standalone/PreFlightChecks.scala
index f3a82d3..9e95784 100644
--- a/core/standalone/src/main/scala/org/apache/openwhisk/standalone/PreFlightChecks.scala
+++ b/core/standalone/src/main/scala/org/apache/openwhisk/standalone/PreFlightChecks.scala
@@ -19,7 +19,7 @@ package org.apache.openwhisk.standalone
 
 import com.typesafe.config.{Config, ConfigFactory}
 import org.apache.commons.lang3.StringUtils
-import org.apache.openwhisk.standalone.StandaloneOpenWhisk.usersConfigKey
+import org.apache.openwhisk.standalone.StandaloneDockerSupport.isPortFree
 import pureconfig.loadConfigOrThrow
 
 import scala.io.AnsiColor
@@ -37,12 +37,16 @@ case class PreFlightChecks(conf: Conf) extends AnsiColor {
   private val cliDownloadUrl = "https://s.apache.org/openwhisk-cli-download"
   private val dockerUrl = "https://docs.docker.com/install/"
 
+  //Support for host.docker.internal is from 18.03
+  private val supportedDockerVersion = DockerVersion("18.03")
+
   def run(): Unit = {
     println(separator)
     println("Running pre flight checks ...")
     println()
     checkForDocker()
     checkForWsk()
+    checkForPorts()
     println()
     println(separator)
   }
@@ -53,7 +57,13 @@ case class PreFlightChecks(conf: Conf) extends AnsiColor {
       println(s"$failed 'docker' cli not found.")
       println(s"\t Install docker from $dockerUrl")
     } else {
-      println(s"$pass 'docker' cli found. $dockerVersion")
+      val versionCmdOutput = dockerVersion
+      println(s"$pass 'docker' cli found. $versionCmdOutput")
+
+      //Wrap in try to discard any issue related to version parsing
+      Try(checkDockerVersion(versionCmdOutput)).failed.foreach(t =>
+        println(s"Error occurred while parsing version - ${t.getMessage}"))
+
       checkDockerIsRunning()
       //Other things we can possibly check for
       //1. add check for minimal supported docker version
@@ -62,6 +72,15 @@ case class PreFlightChecks(conf: Conf) extends AnsiColor {
     }
   }
 
+  private def checkDockerVersion(versionCmdOutput: String)(implicit ordering: Ordering[DockerVersion]): Unit = {
+    val dv = DockerVersion.fromVersionCommand(versionCmdOutput)
+    if (dv < supportedDockerVersion) {
+      println(s"$failed 'docker' version $dv older than minimum supported $supportedDockerVersion")
+    } else {
+      println(s"$pass 'docker' version $dv is newer than minimum supported $supportedDockerVersion")
+    }
+  }
+
   private def dockerVersion = version("docker --version '{{.Client.Version}}'")
 
   private def version(cmd: String) = Try(cmd !! (noopLogger)).map(v => s"(${v.trim})").getOrElse("")
@@ -87,12 +106,12 @@ case class PreFlightChecks(conf: Conf) extends AnsiColor {
   }
 
   def checkWskProps(): Unit = {
-    val users = loadConfigOrThrow[Map[String, String]](loadConfig(), usersConfigKey)
+    val users = loadConfigOrThrow[Map[String, String]](loadConfig(), StandaloneConfigKeys.usersConfigKey)
 
     val configuredAuth = "wsk property get --auth".!!.trim
     val apihost = "wsk property get --apihost".!!.trim
 
-    val requiredHostValue = s"http://localhost:${conf.port()}"
+    val requiredHostValue = s"http://${StandaloneDockerSupport.getLocalHostName()}:${conf.port()}"
 
     //We can use -o option to get raw value. However as its a recent addition
     //using a lazy approach where we check if output ends with one of the configured auth keys or
@@ -116,6 +135,23 @@ case class PreFlightChecks(conf: Conf) extends AnsiColor {
     }
   }
 
+  def checkForPorts(): Unit = {
+    if (isPortFree(conf.port())) {
+      println(s"$pass Server port [${conf.port()}] is free")
+    } else {
+      println(s"$failed Server port [${conf.port()}] is not free. Standalone server cannot start")
+    }
+
+    if (conf.apiGw()) {
+      val port = conf.apiGwPort()
+      if (isPortFree(conf.apiGwPort())) {
+        println(s"$pass Api gateway port [$port] is free")
+      } else {
+        println(s"$warn Api gateway port [$port] is not free. Api gateway cannot start")
+      }
+    }
+  }
+
   private def wskCliVersion = version("wsk property get --cliversion -o raw")
 
   private def loadConfig(): Config = {
diff --git a/tests/src/test/scala/org/apache/openwhisk/standalone/StandaloneServerTests.scala b/core/standalone/src/main/scala/org/apache/openwhisk/standalone/ServerStartupCheck.scala
similarity index 54%
copy from tests/src/test/scala/org/apache/openwhisk/standalone/StandaloneServerTests.scala
copy to core/standalone/src/main/scala/org/apache/openwhisk/standalone/ServerStartupCheck.scala
index 725033c..04a2c9f 100644
--- a/tests/src/test/scala/org/apache/openwhisk/standalone/StandaloneServerTests.scala
+++ b/core/standalone/src/main/scala/org/apache/openwhisk/standalone/ServerStartupCheck.scala
@@ -17,12 +17,29 @@
 
 package org.apache.openwhisk.standalone
 
-import common.WskProps
-import org.junit.runner.RunWith
-import org.scalatest.junit.JUnitRunner
-import system.basic.WskRestBasicTests
+import java.net.{HttpURLConnection, URI, URL}
 
-@RunWith(classOf[JUnitRunner])
-class StandaloneServerTests extends WskRestBasicTests with StandaloneServerFixture {
-  override implicit val wskprops = WskProps().copy(apihost = serverUrl)
+import akka.http.scaladsl.model.Uri
+import com.google.common.base.Stopwatch
+import org.apache.openwhisk.utils.retry
+
+import scala.concurrent.duration._
+
+class ServerStartupCheck(uri: Uri, serverName: String) {
+
+  def waitForServerToStart(): Unit = {
+    val w = Stopwatch.createStarted()
+    retry({
+      println(s"Waiting for $serverName server at $uri to start since $w")
+      require(getResponseCode() == 200)
+    }, 30, Some(1.second))
+  }
+
+  private def getResponseCode(): Int = {
+    val u = new URL(uri.toString())
+    val hc = u.openConnection().asInstanceOf[HttpURLConnection]
+    hc.setRequestMethod("GET")
+    hc.connect()
+    hc.getResponseCode
+  }
 }
diff --git a/tests/src/test/scala/org/apache/openwhisk/standalone/StandaloneServerTests.scala b/core/standalone/src/main/scala/org/apache/openwhisk/standalone/ServiceInfoLogger.scala
similarity index 50%
copy from tests/src/test/scala/org/apache/openwhisk/standalone/StandaloneServerTests.scala
copy to core/standalone/src/main/scala/org/apache/openwhisk/standalone/ServiceInfoLogger.scala
index 725033c..7a9b322 100644
--- a/tests/src/test/scala/org/apache/openwhisk/standalone/StandaloneServerTests.scala
+++ b/core/standalone/src/main/scala/org/apache/openwhisk/standalone/ServiceInfoLogger.scala
@@ -17,12 +17,33 @@
 
 package org.apache.openwhisk.standalone
 
-import common.WskProps
-import org.junit.runner.RunWith
-import org.scalatest.junit.JUnitRunner
-import system.basic.WskRestBasicTests
-
-@RunWith(classOf[JUnitRunner])
-class StandaloneServerTests extends WskRestBasicTests with StandaloneServerFixture {
-  override implicit val wskprops = WskProps().copy(apihost = serverUrl)
+import java.io.File
+
+import org.apache.commons.lang3.StringUtils
+import org.apache.openwhisk.standalone.ColorOutput.clr
+
+import scala.io.AnsiColor
+
+class ServiceInfoLogger(conf: Conf, services: Seq[ServiceContainer], workDir: File) extends AnsiColor {
+  private val separator = "=" * 80
+
+  def run(): Unit = {
+    println(separator)
+    println("Launched service details")
+    println()
+    services.foreach(logService)
+    println()
+    println(s"Local working directory - ${workDir.getAbsolutePath}")
+    println(separator)
+  }
+
+  private def logService(s: ServiceContainer): Unit = {
+    val msg = s"${portInfo(s.port)} ${s.description} (${clr(s.name, BOLD, conf.colorEnabled)})"
+    println(msg)
+  }
+
+  private def portInfo(port: Int) = {
+    val msg = StringUtils.center(port.toString, 7)
+    s"[${clr(msg, GREEN, conf.colorEnabled)}]"
+  }
 }
diff --git a/core/standalone/src/main/scala/org/apache/openwhisk/standalone/StandaloneDockerSupport.scala b/core/standalone/src/main/scala/org/apache/openwhisk/standalone/StandaloneDockerSupport.scala
new file mode 100644
index 0000000..cf95c27
--- /dev/null
+++ b/core/standalone/src/main/scala/org/apache/openwhisk/standalone/StandaloneDockerSupport.scala
@@ -0,0 +1,176 @@
+/*
+ * 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 org.apache.openwhisk.standalone
+
+import java.io.FileNotFoundException
+import java.net.{ServerSocket, Socket}
+import java.nio.file.{Files, Paths}
+
+import akka.Done
+import akka.actor.{ActorSystem, CoordinatedShutdown}
+import org.apache.commons.lang3.SystemUtils
+import org.apache.openwhisk.common.{Logging, TransactionId}
+import org.apache.openwhisk.core.ConfigKeys
+import org.apache.openwhisk.core.containerpool.docker.{DockerClient, DockerClientConfig, WindowsDockerClient}
+import org.apache.openwhisk.core.containerpool.{ContainerAddress, ContainerId}
+import pureconfig.{loadConfig, loadConfigOrThrow}
+
+import scala.concurrent.duration._
+import scala.concurrent.{Await, ExecutionContext, Future}
+import scala.sys.process._
+import scala.util.Try
+
+class StandaloneDockerSupport(docker: DockerClient)(implicit logging: Logging,
+                                                    ec: ExecutionContext,
+                                                    actorSystem: ActorSystem) {
+  CoordinatedShutdown(actorSystem)
+    .addTask(
+      CoordinatedShutdown.PhaseBeforeActorSystemTerminate,
+      "cleanup containers launched for Standalone Server support") { () =>
+      cleanup()
+      Future.successful(Done)
+    }
+
+  def cleanup(): Unit = {
+    implicit val transid = TransactionId(TransactionId.systemPrefix + "standalone")
+    val cleaning =
+      docker.ps(filters = Seq("name" -> StandaloneDockerSupport.prefix), all = true).flatMap { containers =>
+        logging.info(this, s"removing ${containers.size} containers launched for Standalone server support.")
+        val removals = containers.map { id =>
+          docker.rm(id)
+        }
+        Future.sequence(removals)
+      }
+    Await.ready(cleaning, 30.seconds)
+  }
+}
+
+case class ServiceContainer(port: Int, description: String, name: String)
+
+object StandaloneDockerSupport {
+  val prefix = "whisk-"
+  val network = "bridge"
+
+  def checkOrAllocatePort(preferredPort: Int): Int = {
+    if (isPortFree(preferredPort)) preferredPort else freePort()
+  }
+
+  private def freePort(): Int = {
+    val socket = new ServerSocket(0)
+    try socket.getLocalPort
+    finally if (socket != null) socket.close()
+  }
+
+  def isPortFree(port: Int): Boolean = {
+    Try(new Socket("localhost", port).close()).isFailure
+  }
+
+  def createRunCmd(name: String,
+                   environment: Map[String, String] = Map.empty,
+                   dockerRunParameters: Map[String, Set[String]] = Map.empty): Seq[String] = {
+    val environmentArgs = environment.flatMap {
+      case (key, value) => Seq("-e", s"$key=$value")
+    }
+
+    val params = dockerRunParameters.flatMap {
+      case (key, valueList) => valueList.toList.flatMap(Seq(key, _))
+    }
+
+    Seq("--name", name, "--network", network) ++
+      environmentArgs ++ params
+  }
+
+  def containerName(name: String) = {
+    prefix + name
+  }
+
+  /**
+   * Returns the address to be used by code running outside of container to connect to
+   * server. On non linux setups its 'localhost'. However for Linux setups its the ip used
+   * by docker for docker0 network to refer to host system
+   */
+  def getLocalHostName(): String = {
+    if (SystemUtils.IS_OS_LINUX) hostIpLinux
+    else "localhost"
+  }
+
+  def getLocalHostIp(): String = {
+    if (SystemUtils.IS_OS_MAC || SystemUtils.IS_OS_WINDOWS)
+      hostIpNonLinux
+    else hostIpLinux
+  }
+
+  /**
+   * Determines the name/ip which code running within container can use to connect back to Controller
+   */
+  def getLocalHostInternalName(): String = {
+    if (SystemUtils.IS_OS_MAC || SystemUtils.IS_OS_WINDOWS)
+      "host.docker.internal"
+    else hostIpLinux
+  }
+
+  private lazy val hostIpLinux: String = {
+    //Gets the hostIp for linux https://github.com/docker/for-linux/issues/264#issuecomment-387525409
+    // Typical output would be like and we need line with default
+    // $ docker run --rm alpine ip route
+    // default via 172.17.0.1 dev eth0
+    // 172.17.0.0/16 dev eth0 scope link  src 172.17.0.2
+    val cmdResult = s"$dockerCmd run --rm alpine ip route".!!
+    cmdResult.linesIterator
+      .find(_.contains("default"))
+      .map(_.split(' ').apply(2).trim)
+      .getOrElse(throw new IllegalStateException(s"'ip route' result did not match expected output - \n$cmdResult"))
+  }
+
+  private lazy val hostIpNonLinux: String = {
+    //Gets the hostIp as names like host.docker.internal do not resolve for some reason in api gateway
+    //Based on https://unix.stackexchange.com/a/20793
+    //$ docker run --rm alpine getent hosts host.docker.internal
+    //192.168.65.2      host.docker.internal  host.docker.internal
+    val hostName = "host.docker.internal"
+    val cmdResult = s"$dockerCmd run --rm alpine getent hosts $hostName".!!
+    cmdResult.linesIterator
+      .find(_.contains(hostName))
+      .map(_.split(" ").head.trim)
+      .getOrElse(throw new IllegalStateException(
+        s"'getent hosts host.docker.internal' result did not match expected output - \n$cmdResult"))
+  }
+
+  private lazy val dockerCmd = {
+    //TODO Logic duplicated from DockerClient and WindowsDockerClient for now
+    val executable = loadConfig[String]("whisk.docker.executable").map(Some(_)).getOrElse(None)
+    val alternatives =
+      List("/usr/bin/docker", "/usr/local/bin/docker", "C:\\Program Files\\Docker\\Docker\\resources\\bin\\docker.exe") ++ executable
+    Try {
+      alternatives.find(a => Files.isExecutable(Paths.get(a))).get
+    } getOrElse {
+      throw new FileNotFoundException(s"Couldn't locate docker binary (tried: ${alternatives.mkString(", ")}).")
+    }
+  }
+}
+
+class StandaloneDockerClient(implicit log: Logging, as: ActorSystem, ec: ExecutionContext)
+    extends DockerClient()(ec)
+    with WindowsDockerClient {
+  override def runCmd(args: Seq[String], timeout: Duration)(implicit transid: TransactionId): Future[String] =
+    super.runCmd(args, timeout)
+
+  val clientConfig: DockerClientConfig = loadConfigOrThrow[DockerClientConfig](ConfigKeys.dockerClient)
+}
+
+case class StandaloneDockerContainer(id: ContainerId, addr: ContainerAddress)
diff --git a/core/standalone/src/main/scala/org/apache/openwhisk/standalone/StandaloneOpenWhisk.scala b/core/standalone/src/main/scala/org/apache/openwhisk/standalone/StandaloneOpenWhisk.scala
index ccaf22f..1181a15 100644
--- a/core/standalone/src/main/scala/org/apache/openwhisk/standalone/StandaloneOpenWhisk.scala
+++ b/core/standalone/src/main/scala/org/apache/openwhisk/standalone/StandaloneOpenWhisk.scala
@@ -23,9 +23,10 @@ import java.util.Properties
 
 import akka.actor.ActorSystem
 import akka.event.slf4j.SLF4JLogging
+import akka.http.scaladsl.model.Uri
 import akka.stream.ActorMaterializer
-import org.apache.commons.io.{FileUtils, IOUtils}
-import org.apache.commons.lang3.SystemUtils
+import org.apache.commons.io.{FileUtils, FilenameUtils, IOUtils}
+import org.apache.openwhisk.common.TransactionId.systemPrefix
 import org.apache.openwhisk.common.{AkkaLogging, Config, Logging, TransactionId}
 import org.apache.openwhisk.core.cli.WhiskAdmin
 import org.apache.openwhisk.core.controller.Controller
@@ -35,10 +36,10 @@ import org.rogach.scallop.ScallopConf
 import pureconfig.loadConfigOrThrow
 
 import scala.collection.JavaConverters._
-import scala.concurrent.Await
 import scala.concurrent.duration._
+import scala.concurrent.{Await, ExecutionContext}
 import scala.io.AnsiColor
-import scala.util.Try
+import scala.util.{Failure, Success, Try}
 
 class Conf(arguments: Seq[String]) extends ScallopConf(arguments) {
   banner(StandaloneOpenWhisk.banner)
@@ -53,16 +54,26 @@ class Conf(arguments: Seq[String]) extends ScallopConf(arguments) {
 
   val verbose = tally()
   val disableColorLogging = opt[Boolean](descr = "Disables colored logging", noshort = true)
+  val apiGw = opt[Boolean](descr = "Enable API Gateway support", noshort = true)
+  val apiGwPort = opt[Int](descr = "Api Gateway Port", default = Some(3234), noshort = true)
+  val dataDir = opt[File](descr = "Directory used for storage", default = Some(StandaloneOpenWhisk.defaultWorkDir))
 
   verify()
 
   val colorEnabled = !disableColorLogging()
+
+  def serverUrl: Uri = Uri(s"http://${StandaloneDockerSupport.getLocalHostName()}:${port()}")
 }
 
 case class GitInfo(commitId: String, commitTime: String)
 
-object StandaloneOpenWhisk extends SLF4JLogging {
+object StandaloneConfigKeys {
   val usersConfigKey = "whisk.users"
+  val redisConfigKey = "whisk.standalone.redis"
+  val apiGwConfigKey = "whisk.standalone.api-gateway"
+}
+
+object StandaloneOpenWhisk extends SLF4JLogging {
 
   val banner =
     """
@@ -104,6 +115,10 @@ object StandaloneOpenWhisk extends SLF4JLogging {
 
   val gitInfo: Option[GitInfo] = loadGitInfo()
 
+  val defaultWorkDir = new File(FilenameUtils.concat(FileUtils.getUserDirectoryPath, ".openwhisk/standalone"))
+
+  val wskPath = System.getProperty("whisk.standalone.wsk", "wsk")
+
   def main(args: Array[String]): Unit = {
     val conf = new Conf(args)
 
@@ -116,19 +131,36 @@ object StandaloneOpenWhisk extends SLF4JLogging {
     implicit val actorSystem = ActorSystem("standalone-actor-system")
     implicit val materializer = ActorMaterializer.create(actorSystem)
     implicit val logger: Logging = createLogging(actorSystem, conf)
+    implicit val ec: ExecutionContext = actorSystem.dispatcher
+
+    val (dataDir, workDir) = initializeDirs(conf)
+    val (apiGwApiPort, svcs) = if (conf.apiGw()) {
+      startApiGateway(conf)
+    } else (-1, Seq.empty)
+
+    if (svcs.nonEmpty) {
+      new ServiceInfoLogger(conf, svcs, dataDir).run()
+    }
+
+    startServer(conf)
+    new ServerStartupCheck(conf.serverUrl, "OpenWhisk").waitForServerToStart()
 
-    startServer()
+    if (conf.apiGw()) {
+      installRouteMgmt(conf, workDir, apiGwApiPort)
+    }
   }
 
   def initialize(conf: Conf): Unit = {
     configureBuildInfo()
     configureServerPort(conf)
+    configureOSSpecificOpts()
     initConfigLocation(conf)
     configureRuntimeManifest(conf)
     loadWhiskConfig()
   }
 
-  def startServer()(implicit actorSystem: ActorSystem, materializer: ActorMaterializer, logging: Logging): Unit = {
+  def startServer(
+    conf: Conf)(implicit actorSystem: ActorSystem, materializer: ActorMaterializer, logging: Logging): Unit = {
     bootstrapUsers()
     startController()
   }
@@ -140,7 +172,11 @@ object StandaloneOpenWhisk extends SLF4JLogging {
     setConfigProp(WhiskConfig.servicePort, port.toString)
     setConfigProp(WhiskConfig.wskApiPort, port.toString)
     setConfigProp(WhiskConfig.wskApiProtocol, "http")
-    setConfigProp(WhiskConfig.wskApiHostname, localHostName)
+
+    //Using hostInternalName instead of getLocalHostIp as using docker alpine way to
+    //determine the ip is seen to be failing with older version of Docker
+    //So to keep main flow which does not use api gw working fine play safe
+    setConfigProp(WhiskConfig.wskApiHostname, StandaloneDockerSupport.getLocalHostInternalName())
   }
 
   private def initConfigLocation(conf: Conf): Unit = {
@@ -183,11 +219,9 @@ object StandaloneOpenWhisk extends SLF4JLogging {
   private def bootstrapUsers()(implicit actorSystem: ActorSystem,
                                materializer: ActorMaterializer,
                                logging: Logging): Unit = {
-    val users = loadConfigOrThrow[Map[String, String]](usersConfigKey)
-    implicit val userTid: TransactionId = TransactionId("userBootstrap")
-    users.foreach {
-      case (name, key) =>
-        val subject = name.replace('-', '.')
+    implicit val userTid: TransactionId = TransactionId(systemPrefix + "userBootstrap")
+    getUsers().foreach {
+      case (subject, key) =>
         val conf = new org.apache.openwhisk.core.cli.Conf(Seq("user", "create", "--auth", key, subject))
         val admin = WhiskAdmin(conf)
         Await.ready(admin.executeCommand(), 60.seconds)
@@ -195,13 +229,9 @@ object StandaloneOpenWhisk extends SLF4JLogging {
     }
   }
 
-  private def localHostName = {
-    //For connecting back to controller on container host following name needs to be used
-    // on Windows and Mac
-    // https://docs.docker.com/docker-for-windows/networking/#use-cases-and-workarounds
-    if (SystemUtils.IS_OS_MAC || SystemUtils.IS_OS_WINDOWS)
-      "host.docker.internal"
-    else "localhost"
+  private def configureOSSpecificOpts(): Unit = {
+    //Set the interface based on OS
+    setSysProp("whisk.controller.interface", StandaloneDockerSupport.getLocalHostName())
   }
 
   private def loadGitInfo() = {
@@ -257,4 +287,60 @@ object StandaloneOpenWhisk extends SLF4JLogging {
     else
       new ColoredAkkaLogging(adapter)
   }
+
+  private def startApiGateway(
+    conf: Conf)(implicit logging: Logging, as: ActorSystem, ec: ExecutionContext): (Int, Seq[ServiceContainer]) = {
+    implicit val tid: TransactionId = TransactionId(systemPrefix + "apiMgmt")
+
+    // api port is the port used by rout management actions to configure the api gw upon wsk api commands
+    // mgmt port is the port used by end user while making actual use of api gw
+    val apiGwApiPort = StandaloneDockerSupport.checkOrAllocatePort(9000)
+    val apiGwMgmtPort = conf.apiGwPort()
+
+    val dockerClient = new StandaloneDockerClient()
+    val dockerSupport = new StandaloneDockerSupport(dockerClient)
+
+    //Remove any existing launched containers
+    dockerSupport.cleanup()
+    val gw = new ApiGwLauncher(dockerClient, apiGwApiPort, apiGwMgmtPort, conf.port())
+    val f = gw.run()
+    val g = f.andThen {
+      case Success(_) =>
+        logging.info(
+          this,
+          s"Api Gateway started successfully at http://${StandaloneDockerSupport.getLocalHostName()}:$apiGwMgmtPort")
+      case Failure(t) =>
+        logging.error(this, "Error starting Api Gateway" + t)
+    }
+    val services = Await.result(g, 5.minutes)
+    (apiGwApiPort, services)
+  }
+
+  private def installRouteMgmt(conf: Conf, workDir: File, apiGwApiPort: Int)(implicit logging: Logging): Unit = {
+    val user = "whisk.system"
+    val apiGwHostv2 = s"http://${StandaloneDockerSupport.getLocalHostIp()}:$apiGwApiPort/v2"
+    val authKey = getUsers().getOrElse(
+      user,
+      throw new Exception(s"Did not found auth key for $user which is needed to install the api management package"))
+    val installer = InstallRouteMgmt(workDir, authKey, conf.serverUrl, "/" + user, Uri(apiGwHostv2), wskPath)
+    installer.run()
+  }
+
+  private def initializeDirs(conf: Conf): (File, File) = {
+    val baseDir = conf.dataDir()
+    val thisServerDir = s"server-${conf.port()}"
+    val dataDir = new File(baseDir, thisServerDir)
+    FileUtils.forceMkdir(dataDir)
+    log.info(s"Using [${dataDir.getAbsolutePath}] as data directory")
+
+    val workDir = new File(dataDir, "tmp")
+    FileUtils.deleteDirectory(workDir)
+    FileUtils.forceMkdir(workDir)
+    (dataDir, workDir)
+  }
+
+  private def getUsers(): Map[String, String] = {
+    val m = loadConfigOrThrow[Map[String, String]](StandaloneConfigKeys.usersConfigKey)
+    m.map { case (name, key) => (name.replace('-', '.'), key) }
+  }
 }
diff --git a/tests/src/test/scala/org/apache/openwhisk/standalone/StandaloneServerTests.scala b/core/standalone/src/test/scala/org/apache/openwhisk/standalone/DockerVersionTests.scala
similarity index 57%
copy from tests/src/test/scala/org/apache/openwhisk/standalone/StandaloneServerTests.scala
copy to core/standalone/src/test/scala/org/apache/openwhisk/standalone/DockerVersionTests.scala
index 725033c..b42af91 100644
--- a/tests/src/test/scala/org/apache/openwhisk/standalone/StandaloneServerTests.scala
+++ b/core/standalone/src/test/scala/org/apache/openwhisk/standalone/DockerVersionTests.scala
@@ -17,12 +17,27 @@
 
 package org.apache.openwhisk.standalone
 
-import common.WskProps
 import org.junit.runner.RunWith
+import org.scalatest.{FlatSpec, Matchers}
 import org.scalatest.junit.JUnitRunner
-import system.basic.WskRestBasicTests
 
 @RunWith(classOf[JUnitRunner])
-class StandaloneServerTests extends WskRestBasicTests with StandaloneServerFixture {
-  override implicit val wskprops = WskProps().copy(apihost = serverUrl)
+class DockerVersionTests extends FlatSpec with Matchers {
+  behavior of "DockerVersion"
+
+  it should "parse docker version" in {
+    val v = DockerVersion("18.09.2")
+    v shouldBe DockerVersion(18, 9, 2)
+  }
+
+  it should "parse docker version from command output" in {
+    val v = DockerVersion.fromVersionCommand("Docker version 18.09.2, build 624796")
+    v shouldBe DockerVersion(18, 9, 2)
+  }
+
+  it should "compare 2 versions semantically" in {
+    DockerVersion("17.09.2") should be < DockerVersion("18.09.2")
+    DockerVersion("17.09.2") should be < DockerVersion("18.03.2")
+    DockerVersion("17.09") should be < DockerVersion("18.03.2")
+  }
 }
diff --git a/tests/build.gradle b/tests/build.gradle
index d25813f..4590e77 100644
--- a/tests/build.gradle
+++ b/tests/build.gradle
@@ -71,6 +71,7 @@ ext.testSets = [
         "excludes" : [
             "org/apache/openwhisk/core/admin/**",
             "org/apache/openwhisk/core/apigw/actions/test/**",
+            "org/apache/openwhisk/standalone/**",
             "org/apache/openwhisk/core/cli/test/**",
             "org/apache/openwhisk/core/limits/**",
             "**/*CacheConcurrencyTests*",
@@ -80,7 +81,9 @@ ext.testSets = [
         ]
     ],
     "REQUIRE_SYSTEM" : [
-        "includes" : systemIncludes,
+        "includes" : systemIncludes + [
+            "org/apache/openwhisk/standalone/**"
+        ],
         "excludes": [
             "system/basic/WskMultiRuntimeTests*",
             'invokerShoot/**'
diff --git a/tests/src/test/scala/org/apache/openwhisk/standalone/StandaloneServerTests.scala b/tests/src/test/scala/org/apache/openwhisk/standalone/StandaloneApiGwTests.scala
similarity index 58%
copy from tests/src/test/scala/org/apache/openwhisk/standalone/StandaloneServerTests.scala
copy to tests/src/test/scala/org/apache/openwhisk/standalone/StandaloneApiGwTests.scala
index 725033c..c0da1d3 100644
--- a/tests/src/test/scala/org/apache/openwhisk/standalone/StandaloneServerTests.scala
+++ b/tests/src/test/scala/org/apache/openwhisk/standalone/StandaloneApiGwTests.scala
@@ -17,12 +17,27 @@
 
 package org.apache.openwhisk.standalone
 
-import common.WskProps
+import com.google.common.base.Stopwatch
+import common.{FreePortFinder, WskProps}
+import org.apache.openwhisk.core.cli.test.ApiGwRestTests
+import org.apache.openwhisk.utils.retry
 import org.junit.runner.RunWith
 import org.scalatest.junit.JUnitRunner
-import system.basic.WskRestBasicTests
+
+import scala.concurrent.duration._
 
 @RunWith(classOf[JUnitRunner])
-class StandaloneServerTests extends WskRestBasicTests with StandaloneServerFixture {
+class StandaloneApiGwTests extends ApiGwRestTests with StandaloneServerFixture {
   override implicit val wskprops = WskProps().copy(apihost = serverUrl)
+
+  override protected def extraArgs: Seq[String] = Seq("--api-gw", "--api-gw-port", FreePortFinder.freePort().toString)
+
+  override protected def waitForOtherThings(): Unit = {
+    val w = Stopwatch.createStarted()
+    retry({
+      println(s"Waiting for route management actions to be installed since $w")
+      require(logLines.exists(_.contains("Installed Route Management Actions")))
+    }, 30, Some(500.millis))
+  }
+
 }
diff --git a/tests/src/test/scala/org/apache/openwhisk/standalone/StandaloneServerFixture.scala b/tests/src/test/scala/org/apache/openwhisk/standalone/StandaloneServerFixture.scala
index 2da1a23..ebfa109 100644
--- a/tests/src/test/scala/org/apache/openwhisk/standalone/StandaloneServerFixture.scala
+++ b/tests/src/test/scala/org/apache/openwhisk/standalone/StandaloneServerFixture.scala
@@ -23,13 +23,12 @@ import java.nio.charset.StandardCharsets.UTF_8
 
 import com.google.common.base.Stopwatch
 import common.WhiskProperties.WHISK_SERVER
-import common.{FreePortFinder, StreamLogging, WhiskProperties}
+import common.{FreePortFinder, StreamLogging, WhiskProperties, Wsk}
 import io.restassured.RestAssured
 import org.apache.commons.io.FileUtils
-import org.apache.commons.lang3.SystemUtils
 import org.apache.openwhisk.core.WhiskConfig
 import org.apache.openwhisk.utils.retry
-import org.scalatest.{BeforeAndAfterAll, Pending, Suite, TestSuite}
+import org.scalatest.{BeforeAndAfterAll, Suite, TestSuite}
 
 import scala.concurrent.duration._
 import scala.sys.process._
@@ -46,17 +45,12 @@ trait StandaloneServerFixture extends TestSuite with BeforeAndAfterAll with Stre
   private val disablePullConfig = "whisk.docker.standalone.container-factory.pull-standard-images"
   private var serverStartedForTest = false
 
-  //Following tests always fail on Mac but pass when standalone server is running on Linux
-  //It looks related to how networking works on Mac for Docker container
-  //For now ignoring there failure
-  private val ignoredTestsOnMac = Set(
-    "Wsk Action REST should create, and invoke an action that utilizes a docker container",
-    "Wsk Action REST should create, and invoke an action that utilizes dockerskeleton with native zip",
-    "Wsk Action REST should create and invoke a blocking action resulting in an application error response",
-    "Wsk Action REST should create an action, and invoke an action that returns an empty JSON object")
-
   private val whiskServerPreDefined = System.getProperty(WHISK_SERVER) != null
 
+  protected def extraArgs: Seq[String] = Seq.empty
+
+  protected def waitForOtherThings(): Unit = {}
+
   override def beforeAll(): Unit = {
     val serverUrlViaSysProp = Option(System.getProperty(WHISK_SERVER))
     serverUrlViaSysProp match {
@@ -65,17 +59,19 @@ trait StandaloneServerFixture extends TestSuite with BeforeAndAfterAll with Stre
         println(s"Connecting to existing server at $serverUrl")
       case None =>
         System.setProperty(WHISK_SERVER, serverUrl)
-        //TODO avoid starting the server if url whisk.server property is predefined
         super.beforeAll()
         println(s"Running standalone server from ${standaloneServerJar.getAbsolutePath}")
         manifestFile = getRuntimeManifest()
         val args = Seq(
           Seq(
             "java",
+            //For tests let it bound on all ip to make it work on travis which uses linux
+            "-Dwhisk.controller.interface=0.0.0.0",
+            s"-Dwhisk.standalone.wsk=${Wsk.defaultCliPath}",
             s"-D$disablePullConfig=false",
             "-jar",
             standaloneServerJar.getAbsolutePath,
-            "--disable-color-logging"),
+            "--disable-color-logging") ++ extraArgs,
           Seq("-p", serverPort.toString),
           manifestFile.map(f => Seq("-m", f.getAbsolutePath)).getOrElse(Seq.empty)).flatten
 
@@ -83,6 +79,7 @@ trait StandaloneServerFixture extends TestSuite with BeforeAndAfterAll with Stre
         val w = waitForServerToStart()
         serverStartedForTest = true
         println(s"Started test server at $serverUrl in [$w]")
+        waitForOtherThings()
     }
   }
 
@@ -103,11 +100,7 @@ trait StandaloneServerFixture extends TestSuite with BeforeAndAfterAll with Stre
       println(logLines.mkString("\n"))
     }
     stream.reset()
-    val result = if (outcome.isFailed && SystemUtils.IS_OS_MAC && ignoredTestsOnMac.contains(test.name)) {
-      println(s"Ignoring known failed test for Mac [${test.name}]")
-      Pending
-    } else outcome
-    result
+    outcome
   }
 
   def waitForServerToStart(): Stopwatch = {
@@ -117,7 +110,7 @@ trait StandaloneServerFixture extends TestSuite with BeforeAndAfterAll with Stre
         println(s"Waiting for OpenWhisk server to start since $w")
         val response = RestAssured.get(new URI(serverUrl))
         require(response.statusCode() == 200)
-      }, 30, Some(1.second))
+      }, 60, Some(1.second))
     } catch {
       case NonFatal(e) =>
         println(logLines.mkString("\n"))
diff --git a/tests/src/test/scala/org/apache/openwhisk/standalone/StandaloneServerTests.scala b/tests/src/test/scala/org/apache/openwhisk/standalone/StandaloneServerTests.scala
index 725033c..5699ec1 100644
--- a/tests/src/test/scala/org/apache/openwhisk/standalone/StandaloneServerTests.scala
+++ b/tests/src/test/scala/org/apache/openwhisk/standalone/StandaloneServerTests.scala
@@ -18,11 +18,31 @@
 package org.apache.openwhisk.standalone
 
 import common.WskProps
+import org.apache.commons.lang3.SystemUtils
 import org.junit.runner.RunWith
+import org.scalatest.Pending
 import org.scalatest.junit.JUnitRunner
 import system.basic.WskRestBasicTests
 
 @RunWith(classOf[JUnitRunner])
 class StandaloneServerTests extends WskRestBasicTests with StandaloneServerFixture {
   override implicit val wskprops = WskProps().copy(apihost = serverUrl)
+
+  //Following tests always fail on Mac but pass when standalone server is running on Linux
+  //It looks related to how networking works on Mac for Docker container
+  //For now ignoring there failure
+  private val ignoredTestsOnMac = Set(
+    "Wsk Action REST should create, and invoke an action that utilizes a docker container",
+    "Wsk Action REST should create, and invoke an action that utilizes dockerskeleton with native zip",
+    "Wsk Action REST should create and invoke a blocking action resulting in an application error response",
+    "Wsk Action REST should create an action, and invoke an action that returns an empty JSON object")
+
+  override def withFixture(test: NoArgTest) = {
+    val outcome = super.withFixture(test)
+    val result = if (outcome.isFailed && SystemUtils.IS_OS_MAC && ignoredTestsOnMac.contains(test.name)) {
+      println(s"Ignoring known failed test for Mac [${test.name}]")
+      Pending
+    } else outcome
+    result
+  }
 }
diff --git a/tools/travis/runUnitTests.sh b/tools/travis/runUnitTests.sh
index 0c93099..c6fc63e 100755
--- a/tools/travis/runUnitTests.sh
+++ b/tools/travis/runUnitTests.sh
@@ -35,3 +35,6 @@ cat "$ROOTDIR/tests/src/test/resources/application.conf"
 
 ./runTests.sh
 
+cd $ROOTDIR
+TERM=dumb ./gradlew :core:standalone:cleanTest :core:standalone:test
+