Add base framework for api server. (#2094)

* Add base framework for api server.

* Fix build issues.

* Remove extra newlines.

* add config files as resources into api server jar

* package heron api server into tools

* Implement submit/kill/activate/deactivate for the rest api server.

* Remove excess logging and print statements from SubmitterMain.

* Add help flag and remove println and hardcoded directory.

* Add apiserver tests for the ConfigUtils.

* Add user and verion params for submit topology.

* Add release file flag for apiserver.

The api server will read the, build version, build user
and build tag from the release file and use those values
for submitting topologies.

* Set the submit_user when submitting a topology.

* adding URL based handling in heron submit

* Fix activate/deactivate/kill api paths in cli.

* support multiple deployment modes

* Return status 200 for a successful kill topology.

* Catch exceptions as opposed to runtime exceptions in topology resources.

* Update topology actions to check for status code 200.

kill/activate/deactivate endpoints will return a
http status code of 200 when successful.

* fix printing debug information

* Add script to start the api server.

* package heron api server

* get the actual binary since heron-apiserver is a symbolic link

* Add base-template flag and method to find where the jar is installed.

* Print error message if there is a parsing exception.

* Add error message when server fails to start.

* Update bind exception message.

* Add port flag for api server.

* added the service-url parameter

* - Remove HERON_VERSION from docker files
- Add error message reporting for service mode deployment

* remove debug print out

* Fix how yaml files are written to files.

* Pass extra parameters to update and restart extra parameters
Added exception handling for requests call

* Apply submit overrides and add version endpoint.

* Add restart rest endpoint.

* append a list element instead of extending it

* Add printing of args in debug mode

* Add rest update endpoint.

* Add verbose flag to apiserver.

* Allow update to accept overrides.

* Add more scheduler implementations.

* sort the version items and print it

* Add dryrun response for submit and update endpoints.

* fix python command line errors

* Fix api server activate handler.

* fixed submission to local cluster

* fix spelling mistake of function invocation

* Remove api depenedency from apiserver build file.
diff --git a/WORKSPACE b/WORKSPACE
index aa7d4c2..a5bf649 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -7,6 +7,12 @@
 reef_version = "0.14.0"
 slf4j_version = "1.7.7"
 
+# heron api server
+jetty_version = "9.4.6.v20170531"
+jersey_verion = "2.25.1"
+hk2_api = "2.5.0-b32"
+
+
 maven_server(
   name = "default",
   url = "http://central.maven.org/maven2/",
@@ -471,3 +477,155 @@
   artifact = "org.apache.pulsar:pulsar-client:jar:shaded:1.19.0-incubating"
 )
 # end Pulsar Client
+
+
+# heron api server
+# jetty
+maven_jar(
+  name = "org_eclipse_jetty_server",
+  artifact = "org.eclipse.jetty:jetty-server:" + jetty_version
+)
+
+maven_jar(
+  name = "org_eclipse_jetty_http",
+  artifact = "org.eclipse.jetty:jetty-http:" + jetty_version
+)
+
+maven_jar(
+  name = "org_eclipse_jetty_util",
+  artifact = "org.eclipse.jetty:jetty-util:" + jetty_version
+)
+
+maven_jar(
+  name = "org_eclipse_jetty_io",
+  artifact = "org.eclipse.jetty:jetty-io:" + jetty_version
+)
+
+maven_jar(
+  name = "org_eclipse_jetty_security",
+  artifact = "org.eclipse.jetty:jetty-security:" + jetty_version
+)
+
+maven_jar(
+  name = "org_eclipse_jetty_servlet",
+  artifact = "org.eclipse.jetty:jetty-servlet:" + jetty_version
+)
+
+maven_jar(
+  name = "org_eclipse_jetty_servlets",
+  artifact = "org.eclipse.jetty:jetty-servlets:" + jetty_version
+)
+
+maven_jar(
+  name = "org_eclipse_jetty_continuation",
+  artifact = "org.eclipse.jetty:jetty-continuation:" + jetty_version
+)
+
+maven_jar(
+  name = "javax_servlet_api",
+  artifact = "javax.servlet:javax.servlet-api:3.1.0"
+)
+# end jetty
+
+# jersey
+maven_jar(
+  name = "jersey_container_servlet_core",
+  artifact = "org.glassfish.jersey.containers:jersey-container-servlet-core:" + jersey_verion
+)
+
+maven_jar(
+  name = "jersey_container_servlet",
+  artifact = "org.glassfish.jersey.containers:jersey-container-servlet:" + jersey_verion
+)
+
+maven_jar(
+  name = "jersey_server",
+  artifact = "org.glassfish.jersey.core:jersey-server:" + jersey_verion
+)
+
+maven_jar(
+  name = "jersey_client",
+  artifact = "org.glassfish.jersey.core:jersey-client:" + jersey_verion
+)
+
+maven_jar(
+  name = "jersey_common",
+  artifact = "org.glassfish.jersey.core:jersey-common:jar:" + jersey_verion
+)
+
+maven_jar(
+  name = "jersey_media_multipart",
+  artifact = "org.glassfish.jersey.media:jersey-media-multipart:" + jersey_verion
+)
+
+maven_jar(
+  name = "jersey_media_jaxb",
+  artifact = "org.glassfish.jersey.media:jersey-media-jaxb:" + jersey_verion
+)
+
+maven_jar(
+  name = "jersey_guava",
+  artifact = "org.glassfish.jersey.bundles.repackaged:jersey-guava:" + jersey_verion
+)
+# end jersey
+
+maven_jar(
+  name = "javax_inject",
+  artifact = "org.glassfish.hk2.external:javax.inject:2.5.0-b32"
+)
+
+maven_jar(
+  name = "javax_annotation",
+  artifact = "javax.annotation:javax.annotation-api:1.2"
+)
+
+maven_jar(
+  name = "javax_validation",
+  artifact = "javax.validation:validation-api:1.1.0.Final"
+)
+
+maven_jar(
+  name = "javax_ws_rs_api",
+  artifact = "javax.ws.rs:javax.ws.rs-api:2.0.1"
+)
+
+maven_jar(
+  name = "hk2_api",
+  artifact = "org.glassfish.hk2:hk2-api:" + hk2_api
+)
+
+maven_jar(
+  name = "hk2_utils",
+  artifact = "org.glassfish.hk2:hk2-utils:" + hk2_api
+)
+
+maven_jar(
+  name = "hk2_aopalliance_repackaged",
+  artifact = "org.glassfish.hk2.external:aopalliance-repackaged:" + hk2_api
+)
+
+maven_jar(
+  name = "hk2_locator",
+  artifact = "org.glassfish.hk2:hk2-locator:" + hk2_api
+)
+
+maven_jar(
+  name = "hk2_osgi_resource_locator",
+  artifact = "org.glassfish.hk2:osgi-resource-locator:1.0.1"
+)
+
+maven_jar(
+  name = "org_javassit",
+  artifact = "org.javassist:javassist:3.20.0-GA"
+)
+
+maven_jar(
+  name = "mimepull",
+  artifact = "org.jvnet.mimepull:mimepull:1.9.7"
+)
+
+maven_jar(
+  name = "org_apache_commons_compress",
+  artifact = "org.apache.commons:commons-compress:1.14",
+)
+# end heron api server
diff --git a/docker/dist/Dockerfile.dist.centos7 b/docker/dist/Dockerfile.dist.centos7
index 19044d0..6a218c4 100644
--- a/docker/dist/Dockerfile.dist.centos7
+++ b/docker/dist/Dockerfile.dist.centos7
@@ -1,7 +1,5 @@
 FROM centos:centos7
 
-ARG heronVersion
-
 RUN yum -y upgrade
 RUN yum -y install python; yum clean all
 RUN yum -y install unzip; yum clean all 
diff --git a/docker/dist/Dockerfile.dist.ubuntu14.04 b/docker/dist/Dockerfile.dist.ubuntu14.04
index c3f9073..0df770e 100644
--- a/docker/dist/Dockerfile.dist.ubuntu14.04
+++ b/docker/dist/Dockerfile.dist.ubuntu14.04
@@ -1,7 +1,5 @@
 FROM ubuntu:14.04
 
-ARG heronVersion
-
 RUN apt-get update 
 RUN apt-get -y install python ; apt-get clean all
 RUN apt-get -y install unzip ; apt-get clean all
diff --git a/docker/dist/Dockerfile.dist.ubuntu15.10 b/docker/dist/Dockerfile.dist.ubuntu15.10
index 7375eae..5881348 100644
--- a/docker/dist/Dockerfile.dist.ubuntu15.10
+++ b/docker/dist/Dockerfile.dist.ubuntu15.10
@@ -1,7 +1,5 @@
 FROM ubuntu:16.04
 
-ARG heronVersion
-
 RUN apt-get update
 RUN apt-get -y install python ; apt-get clean all
 RUN apt-get -y install unzip ; apt-get clean all
diff --git a/docker/dist/Dockerfile.dist.ubuntu16.04 b/docker/dist/Dockerfile.dist.ubuntu16.04
index 7375eae..5881348 100644
--- a/docker/dist/Dockerfile.dist.ubuntu16.04
+++ b/docker/dist/Dockerfile.dist.ubuntu16.04
@@ -1,7 +1,5 @@
 FROM ubuntu:16.04
 
-ARG heronVersion
-
 RUN apt-get update
 RUN apt-get -y install python ; apt-get clean all
 RUN apt-get -y install unzip ; apt-get clean all
diff --git a/docker/scripts/build-docker.sh b/docker/scripts/build-docker.sh
index a0f8285..6e946e8 100755
--- a/docker/scripts/build-docker.sh
+++ b/docker/scripts/build-docker.sh
@@ -5,8 +5,7 @@
   echo "$(cd "$(dirname "$1")"; pwd)/$(basename "$1")"
 }
 
-DOCKER_DIR=$(dirname $(realpath $0))
-PROJECT_DIR=$(dirname $DOCKER_DIR )
+DOCKER_DIR=$(dirname $(dirname $(realpath $0)))
 SCRATCH_DIR="$HOME/.heron-docker"
 
 cleanup() {
@@ -25,7 +24,7 @@
     mkdir $1/artifacts
   fi
 
-  cp $DOCKER_DIR/* $1
+  cp $DOCKER_DIR/dist/* $1
 }
 
 run_build() {
@@ -37,13 +36,14 @@
 
   setup_scratch_dir $SCRATCH_DIR
 
-  echo "Building docker image with tag:$DOCKER_TAG"
   #need to copy artifacts locally
+  echo "Building docker image with tag:$DOCKER_TAG"
   cp -pr "$OUTPUT_DIRECTORY"/*$HERON_VERSION* "$SCRATCH_DIR/artifacts"
+  mv $SCRATCH_DIR/artifacts/heron-tools-install-$HERON_VERSION-$TARGET_PLATFORM.sh $SCRATCH_DIR/artifacts/heron-tools-install.sh
+  mv $SCRATCH_DIR/artifacts/heron-core-$HERON_VERSION-$TARGET_PLATFORM.tar.gz $SCRATCH_DIR/artifacts/heron-core.tar.gz
 
   export HERON_VERSION
-  docker build --build-arg heronVersion=$HERON_VERSION -t "$DOCKER_TAG" -f "$DOCKER_FILE" "$SCRATCH_DIR"
-
+  docker build -t "$DOCKER_TAG" -f "$DOCKER_FILE" "$SCRATCH_DIR"
 }
 
 case $# in
diff --git a/docker/scripts/ci-docker.sh b/docker/scripts/ci-docker.sh
index 71d89cb..79db0ac 100755
--- a/docker/scripts/ci-docker.sh
+++ b/docker/scripts/ci-docker.sh
@@ -65,9 +65,9 @@
   # build the image
   echo "Building docker image with tag:$DOCKER_TAG"
   if [ "$HERON_VERSION" == "nightly" ]; then 
-    docker build --build-arg heronVersion=$HERON_VERSION -t "$DOCKER_TAG" -f "$DOCKER_FILE" "$SCRATCH_DIR"
+    docker build -t "$DOCKER_TAG" -f "$DOCKER_FILE" "$SCRATCH_DIR"
   else
-    docker build --build-arg heronVersion=$HERON_VERSION -t "$DOCKER_TAG" -t "$DOCKER_LATEST_TAG" -f "$DOCKER_FILE" "$SCRATCH_DIR"
+    docker build -t "$DOCKER_TAG" -t "$DOCKER_LATEST_TAG" -f "$DOCKER_FILE" "$SCRATCH_DIR"
   fi
 
   # save the image as a tar file
diff --git a/heron/config/src/yaml/BUILD b/heron/config/src/yaml/BUILD
index c918e66..606f74c 100644
--- a/heron/config/src/yaml/BUILD
+++ b/heron/config/src/yaml/BUILD
@@ -9,7 +9,7 @@
 
 filegroup(
     name = "conf-yaml",
-    srcs = glob(["conf/*.yaml"]),
+    srcs = glob(["conf/**/*.yaml"]),
 )
 
 filegroup(
diff --git a/heron/scheduler-core/src/java/com/twitter/heron/scheduler/RuntimeManagerMain.java b/heron/scheduler-core/src/java/com/twitter/heron/scheduler/RuntimeManagerMain.java
index 7fb40e6..3925fff 100644
--- a/heron/scheduler-core/src/java/com/twitter/heron/scheduler/RuntimeManagerMain.java
+++ b/heron/scheduler-core/src/java/com/twitter/heron/scheduler/RuntimeManagerMain.java
@@ -35,8 +35,7 @@
 import com.twitter.heron.scheduler.client.ISchedulerClient;
 import com.twitter.heron.scheduler.client.SchedulerClientFactory;
 import com.twitter.heron.scheduler.dryrun.UpdateDryRunResponse;
-import com.twitter.heron.scheduler.dryrun.UpdateRawDryRunRenderer;
-import com.twitter.heron.scheduler.dryrun.UpdateTableDryRunRenderer;
+import com.twitter.heron.scheduler.utils.DryRunRenders;
 import com.twitter.heron.spi.common.Config;
 import com.twitter.heron.spi.common.ConfigLoader;
 import com.twitter.heron.spi.common.Context;
@@ -321,7 +320,7 @@
       LOG.log(Level.FINE, "Sending out dry-run response");
       // Output may contain UTF-8 characters, so we should print using UTF-8 encoding
       PrintStream out = new PrintStream(System.out, true, StandardCharsets.UTF_8.name());
-      out.print(runtimeManagerMain.renderDryRunResponse(response));
+      out.print(DryRunRenders.render(response, Context.dryRunFormatType(config)));
       // SUPPRESS CHECKSTYLE RegexpSinglelineJava
       // Exit with status code 200 to indicate dry-run response is sent out
       System.exit(200);
@@ -453,18 +452,4 @@
       throws SchedulerException {
     return new SchedulerClientFactory(config, runtime).getSchedulerClient();
   }
-
-  protected String renderDryRunResponse(UpdateDryRunResponse resp) {
-    DryRunFormatType formatType = Context.dryRunFormatType(config);
-    switch (formatType) {
-      case RAW :
-        return new UpdateRawDryRunRenderer(resp).render();
-      case TABLE:
-        return new UpdateTableDryRunRenderer(resp, false).render();
-      case COLORED_TABLE:
-        return new UpdateTableDryRunRenderer(resp, true).render();
-      default: throw new IllegalArgumentException(
-          String.format("Unexpected rendering format: %s", formatType));
-    }
-  }
 }
diff --git a/heron/scheduler-core/src/java/com/twitter/heron/scheduler/RuntimeManagerRunner.java b/heron/scheduler-core/src/java/com/twitter/heron/scheduler/RuntimeManagerRunner.java
index d090c0b..fe4f366 100644
--- a/heron/scheduler-core/src/java/com/twitter/heron/scheduler/RuntimeManagerRunner.java
+++ b/heron/scheduler-core/src/java/com/twitter/heron/scheduler/RuntimeManagerRunner.java
@@ -42,7 +42,7 @@
 import com.twitter.heron.spi.utils.TMasterUtils;
 
 public class RuntimeManagerRunner {
-  static final String NEW_COMPONENT_PARALLELISM_KEY = "NEW_COMPONENT_PARALLELISM";
+  public static final String NEW_COMPONENT_PARALLELISM_KEY = "NEW_COMPONENT_PARALLELISM";
   private static final Logger LOG = Logger.getLogger(RuntimeManagerRunner.class.getName());
   private final Config config;
   private final Config runtime;
diff --git a/heron/scheduler-core/src/java/com/twitter/heron/scheduler/SubmitterMain.java b/heron/scheduler-core/src/java/com/twitter/heron/scheduler/SubmitterMain.java
index 6066d7a..f0ae589 100644
--- a/heron/scheduler-core/src/java/com/twitter/heron/scheduler/SubmitterMain.java
+++ b/heron/scheduler-core/src/java/com/twitter/heron/scheduler/SubmitterMain.java
@@ -32,13 +32,12 @@
 
 import com.twitter.heron.api.generated.TopologyAPI;
 import com.twitter.heron.common.basics.DryRunFormatType;
-import com.twitter.heron.common.basics.PackageType;
 import com.twitter.heron.common.basics.SysUtils;
 import com.twitter.heron.common.utils.logging.LoggingHelper;
 import com.twitter.heron.scheduler.dryrun.SubmitDryRunResponse;
-import com.twitter.heron.scheduler.dryrun.SubmitRawDryRunRenderer;
-import com.twitter.heron.scheduler.dryrun.SubmitTableDryRunRenderer;
+import com.twitter.heron.scheduler.utils.DryRunRenders;
 import com.twitter.heron.scheduler.utils.LauncherUtils;
+import com.twitter.heron.scheduler.utils.SubmitterUtils;
 import com.twitter.heron.spi.common.Config;
 import com.twitter.heron.spi.common.ConfigLoader;
 import com.twitter.heron.spi.common.Context;
@@ -61,29 +60,6 @@
   private static final Logger LOG = Logger.getLogger(SubmitterMain.class.getName());
 
   /**
-   * Load the topology config
-   *
-   * @param topologyPackage, tar ball containing user submitted jar/tar, defn and config
-   * @param topologyBinaryFile, name of the user submitted topology jar/tar/pex file
-   * @param topology, proto in memory version of topology definition
-   * @return config, the topology config
-   */
-  protected static Config topologyConfigs(
-      String topologyPackage, String topologyBinaryFile, String topologyDefnFile,
-      TopologyAPI.Topology topology) {
-    PackageType packageType = PackageType.getPackageType(topologyBinaryFile);
-
-    return Config.newBuilder()
-        .put(Key.TOPOLOGY_ID, topology.getId())
-        .put(Key.TOPOLOGY_NAME, topology.getName())
-        .put(Key.TOPOLOGY_DEFINITION_FILE, topologyDefnFile)
-        .put(Key.TOPOLOGY_PACKAGE_FILE, topologyPackage)
-        .put(Key.TOPOLOGY_BINARY_FILE, topologyBinaryFile)
-        .put(Key.TOPOLOGY_PACKAGE_TYPE, packageType)
-        .build();
-  }
-
-  /**
    * Load the config parameters from the command line
    *
    * @param cluster, name of the cluster
@@ -258,6 +234,7 @@
     return cmd.hasOption("v");
   }
 
+  @SuppressWarnings("JavadocMethod")
   @VisibleForTesting
   public static Config loadConfig(CommandLine cmd, TopologyAPI.Topology topology) {
     String cluster = cmd.getOptionValue("cluster");
@@ -293,8 +270,9 @@
     return Config.toLocalMode(Config.newBuilder()
         .putAll(ConfigLoader.loadConfig(heronHome, configPath, releaseFile, overrideConfigFile))
         .putAll(commandLineConfigs(cluster, role, environ, submitUser, dryRun,
-                                   dryRunFormat, isVerbose(cmd)))
-        .putAll(topologyConfigs(topologyPackage, topologyBinaryFile, topologyDefnFile, topology))
+            dryRunFormat, isVerbose(cmd)))
+        .putAll(SubmitterUtils.topologyConfigs(topologyPackage, topologyBinaryFile,
+            topologyDefnFile, topology))
         .build());
   }
 
@@ -351,7 +329,7 @@
       LOG.log(Level.FINE, "Sending out dry-run response");
       // Output may contain UTF-8 characters, so we should print using UTF-8 encoding
       PrintStream out = new PrintStream(System.out, true, StandardCharsets.UTF_8.name());
-      out.print(submitterMain.renderDryRunResponse(response));
+      out.print(DryRunRenders.render(response, Context.dryRunFormatType(config)));
       // Exit with status code 200 to indicate dry-run response is sent out
       // SUPPRESS CHECKSTYLE RegexpSinglelineJava
       System.exit(200);
@@ -570,15 +548,4 @@
     LaunchRunner launchRunner = new LaunchRunner(config, runtime);
     launchRunner.call();
   }
-
-  protected String renderDryRunResponse(SubmitDryRunResponse resp) {
-    DryRunFormatType formatType = Context.dryRunFormatType(config);
-    switch (formatType) {
-      case RAW : return new SubmitRawDryRunRenderer(resp).render();
-      case TABLE: return new SubmitTableDryRunRenderer(resp, false).render();
-      case COLORED_TABLE: return new SubmitTableDryRunRenderer(resp, true).render();
-      default: throw new IllegalArgumentException(
-          String.format("Unexpected rendering format: %s", formatType));
-    }
-  }
 }
diff --git a/heron/scheduler-core/src/java/com/twitter/heron/scheduler/utils/DryRunRenders.java b/heron/scheduler-core/src/java/com/twitter/heron/scheduler/utils/DryRunRenders.java
new file mode 100644
index 0000000..d62fdfa
--- /dev/null
+++ b/heron/scheduler-core/src/java/com/twitter/heron/scheduler/utils/DryRunRenders.java
@@ -0,0 +1,54 @@
+//  Copyright 2017 Twitter. All rights reserved.
+//
+//  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 com.twitter.heron.scheduler.utils;
+
+import com.twitter.heron.common.basics.DryRunFormatType;
+import com.twitter.heron.scheduler.dryrun.SubmitDryRunResponse;
+import com.twitter.heron.scheduler.dryrun.SubmitRawDryRunRenderer;
+import com.twitter.heron.scheduler.dryrun.SubmitTableDryRunRenderer;
+import com.twitter.heron.scheduler.dryrun.UpdateDryRunResponse;
+import com.twitter.heron.scheduler.dryrun.UpdateRawDryRunRenderer;
+import com.twitter.heron.scheduler.dryrun.UpdateTableDryRunRenderer;
+
+public final class DryRunRenders {
+
+  public static String render(SubmitDryRunResponse response, DryRunFormatType formatType) {
+    switch (formatType) {
+      case RAW :
+        return new SubmitRawDryRunRenderer(response).render();
+      case TABLE:
+        return new SubmitTableDryRunRenderer(response, false).render();
+      case COLORED_TABLE:
+        return new SubmitTableDryRunRenderer(response, true).render();
+      default: throw new IllegalArgumentException(
+          String.format("Unexpected rendering format: %s", formatType));
+    }
+  }
+
+  public static String render(UpdateDryRunResponse response, DryRunFormatType formatType) {
+    switch (formatType) {
+      case RAW :
+        return new UpdateRawDryRunRenderer(response).render();
+      case TABLE:
+        return new UpdateTableDryRunRenderer(response, false).render();
+      case COLORED_TABLE:
+        return new UpdateTableDryRunRenderer(response, true).render();
+      default: throw new IllegalArgumentException(
+          String.format("Unexpected rendering format: %s", formatType));
+    }
+  }
+
+  private DryRunRenders() {
+  }
+}
diff --git a/heron/scheduler-core/src/java/com/twitter/heron/scheduler/utils/SubmitterUtils.java b/heron/scheduler-core/src/java/com/twitter/heron/scheduler/utils/SubmitterUtils.java
new file mode 100644
index 0000000..0e17f62
--- /dev/null
+++ b/heron/scheduler-core/src/java/com/twitter/heron/scheduler/utils/SubmitterUtils.java
@@ -0,0 +1,48 @@
+//  Copyright 2017 Twitter. All rights reserved.
+//
+//  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 com.twitter.heron.scheduler.utils;
+
+import com.twitter.heron.api.generated.TopologyAPI;
+import com.twitter.heron.common.basics.PackageType;
+import com.twitter.heron.spi.common.Config;
+import com.twitter.heron.spi.common.Key;
+
+public final class SubmitterUtils {
+
+  /**
+   * Create the topology config
+   *
+   * @param topologyPackagePath path to the tar ball containing user submitted jar/tar, defn and config
+   * @param topologyBinaryFile name of the user submitted topology jar/tar/pex file
+   * @param topologyDefinitionPath path to the topology definition file
+   * @param topology proto in memory version of topology definition
+   * @return config the topology config
+   */
+  public static Config topologyConfigs(String topologyPackagePath, String topologyBinaryFile,
+        String topologyDefinitionPath, TopologyAPI.Topology topology) {
+    PackageType packageType = PackageType.getPackageType(topologyBinaryFile);
+
+    return Config.newBuilder()
+        .put(Key.TOPOLOGY_ID, topology.getId())
+        .put(Key.TOPOLOGY_NAME, topology.getName())
+        .put(Key.TOPOLOGY_DEFINITION_FILE, topologyDefinitionPath)
+        .put(Key.TOPOLOGY_PACKAGE_FILE, topologyPackagePath)
+        .put(Key.TOPOLOGY_BINARY_FILE, topologyBinaryFile)
+        .put(Key.TOPOLOGY_PACKAGE_TYPE, packageType)
+        .build();
+  }
+
+  private SubmitterUtils() {
+  }
+}
diff --git a/heron/spi/src/java/com/twitter/heron/spi/common/Config.java b/heron/spi/src/java/com/twitter/heron/spi/common/Config.java
index 28f0d8d..5a82937 100644
--- a/heron/spi/src/java/com/twitter/heron/spi/common/Config.java
+++ b/heron/spi/src/java/com/twitter/heron/spi/common/Config.java
@@ -297,6 +297,10 @@
     return cfgMap.keySet();
   }
 
+  public Set<Map.Entry<String, Object>> getEntrySet() {
+    return cfgMap.entrySet();
+  }
+
   @Override
   public String toString() {
     Map<String, Object> treeMap = new TreeMap<>(cfgMap);
diff --git a/heron/tools/apiserver/src/java/BUILD b/heron/tools/apiserver/src/java/BUILD
new file mode 100644
index 0000000..f193d31
--- /dev/null
+++ b/heron/tools/apiserver/src/java/BUILD
@@ -0,0 +1,63 @@
+package(default_visibility = ["//visibility:public"])
+
+load("/tools/rules/heron_deps", "heron_java_proto_files")
+
+api_deps_files = [
+  "//heron/spi/src/java:heron-spi",
+  "//heron/common/src/java:basics-java",
+]
+
+scheduler_deps_files = [
+  "//heron/scheduler-core/src/java:scheduler-java",
+  "//heron/schedulers/src/java:local-scheduler-java",
+  "//heron/schedulers/src/java:kubernetes-scheduler-java",
+  "//heron/schedulers/src/java:marathon-scheduler-java",
+  "//heron/schedulers/src/java:mesos-scheduler-java",
+  "//heron/schedulers/src/java:yarn-scheduler-java",
+  "//heron/schedulers/src/java:slurm-scheduler-java",
+]
+
+packing_deps_files = [
+  "//heron/packing/src/java:roundrobin-packing",
+  "//heron/packing/src/java:binpacking-packing",
+  "//heron/packing/src/java:builder"
+]
+
+uploader_deps_files = [
+  "//heron/uploaders/src/java:localfs-uploader-java",
+  "//heron/uploaders/src/java:gcs-uploader-java",
+  "//heron/uploaders/src/java:s3-uploader-java",
+]
+
+state_manager_deps_files = [
+  "//heron/statemgrs/src/java:statemgrs-java",
+]
+
+apiserver_deps_files = \
+  api_deps_files + \
+  heron_java_proto_files() + \
+  state_manager_deps_files + \
+  scheduler_deps_files + \
+  packing_deps_files + \
+  uploader_deps_files + [
+    "//third_party/java:cli",
+    "@org_yaml_snakeyaml//jar",
+    "//third_party/java:jetty-jersey-java",
+    "//third_party/java:commons-compress",
+    "//third_party/java:jackson",
+    "//third_party/java:logging",
+  ]
+
+java_binary(
+    name = 'heron-apiserver-unshaded',
+    srcs = glob(["**/apiserver/**/*.java"]),
+    main_class = "com.twitter.heron.apiserver.Runtime",
+    deps = apiserver_deps_files,
+)
+
+genrule(
+    name = "heron-apiserver",
+    srcs = [":heron-apiserver-unshaded_deploy.jar"],
+    outs = ["heron-apiserver.jar"],
+    cmd  = "cp $< $@",
+)
diff --git a/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/Constants.java b/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/Constants.java
new file mode 100644
index 0000000..1a09a9b
--- /dev/null
+++ b/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/Constants.java
@@ -0,0 +1,41 @@
+//  Copyright 2017 Twitter. All rights reserved.
+//
+//  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 com.twitter.heron.apiserver;
+
+import com.twitter.heron.spi.common.Key;
+
+public final class Constants {
+
+  static final int DEFAULT_PORT = 9000;
+
+  static final String DEFAULT_HERON_LOCAL = "~/.heron";
+
+  static final String DEFAULT_HERON_CLUSTER = "$HERON_HOME";
+
+  static final String DEFAULT_HERON_CONFIG_DIRECTORY = "conf";
+
+  static final String DEFAULT_HERON_RELEASE_FILE = "release.yaml";
+
+  public static final String DEFAULT_HERON_SANDBOX_CONFIG =
+      Key.HERON_CLUSTER_CONF.getDefaultString();
+
+  public static final String OVERRIDE_FILE = "override.yaml";
+
+  public static final String STATE_MANAGER_FILE = "statemgr.yaml";
+
+  public static final String DEFAULT_HERON_ENVIRONMENT = "default";
+
+  private Constants() {
+  }
+}
diff --git a/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/Resources.java b/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/Resources.java
new file mode 100644
index 0000000..6790006
--- /dev/null
+++ b/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/Resources.java
@@ -0,0 +1,44 @@
+//  Copyright 2017 Twitter. All rights reserved.
+//
+//  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 com.twitter.heron.apiserver;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.glassfish.jersey.media.multipart.MultiPartFeature;
+
+import com.twitter.heron.apiserver.resources.ConfigurationResource;
+import com.twitter.heron.apiserver.resources.NotFoundExceptionHandler;
+import com.twitter.heron.apiserver.resources.TopologyResource;
+
+public final class Resources {
+
+  private Resources() {
+  }
+
+  static Set<Class<?>> get() {
+    return new HashSet<>(getClasses());
+  }
+
+  private static List<Class<?>> getClasses() {
+    return Arrays.asList(
+        ConfigurationResource.class,
+        TopologyResource.class,
+        NotFoundExceptionHandler.class,
+        MultiPartFeature.class
+    );
+  }
+}
diff --git a/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/Runtime.java b/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/Runtime.java
new file mode 100644
index 0000000..cb42006
--- /dev/null
+++ b/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/Runtime.java
@@ -0,0 +1,289 @@
+//  Copyright 2017 Twitter. All rights reserved.
+//
+//  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 com.twitter.heron.apiserver;
+
+import java.io.IOException;
+import java.net.BindException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.file.Paths;
+
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.servlet.ServletContainer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.twitter.heron.apiserver.resources.HeronResource;
+import com.twitter.heron.apiserver.utils.ConfigUtils;
+import com.twitter.heron.apiserver.utils.Logging;
+import com.twitter.heron.spi.common.Config;
+
+public final class Runtime {
+
+  private static final Logger LOG = LoggerFactory.getLogger(Runtime.class);
+
+  private static final String API_BASE_PATH = "/api/v1/*";
+
+  private enum Flag {
+    Help("h"),
+    BaseTemplate("base-template"),
+    Cluster("cluster"),
+    ConfigPath("config-path"),
+    Port("port"),
+    Property("D"),
+    ReleaseFile("release-file"),
+    Verbose("verbose");
+
+    final String name;
+
+    Flag(String name) {
+      this.name = name;
+    }
+  }
+
+  private static Options createOptions() {
+    final Option cluster = Option.builder()
+        .desc("Cluster in which to deploy topologies")
+        .longOpt(Flag.Cluster.name)
+        .hasArg()
+        .argName(Flag.Cluster.name)
+        .required()
+        .build();
+
+    final Option baseTemplate = Option.builder()
+        .desc("Base configuration to use for deloying topologies")
+        .longOpt(Flag.BaseTemplate.name)
+        .hasArg()
+        .argName(Flag.BaseTemplate.name)
+        .required(false)
+        .build();
+
+    final Option config = Option.builder()
+        .desc("Path to the base configuration for deploying topologies")
+        .longOpt(Flag.ConfigPath.name)
+        .hasArg()
+        .argName(Flag.ConfigPath.name)
+        .required(false)
+        .build();
+
+    final Option port = Option.builder()
+        .desc("Port to bind to")
+        .longOpt(Flag.Port.name)
+        .hasArg()
+        .argName(Flag.Port.name)
+        .required(false)
+        .build();
+
+    final Option property = Option.builder(Flag.Property.name)
+        .argName("property=value")
+        .numberOfArgs(2)
+        .valueSeparator()
+        .desc("use value for given property")
+        .build();
+
+    final Option release = Option.builder()
+        .desc("Path to the release file")
+        .longOpt(Flag.ReleaseFile.name)
+        .hasArg()
+        .argName(Flag.ReleaseFile.name)
+        .required(false)
+        .build();
+
+    final Option verbose = Option.builder()
+        .desc("Verbose mode. Increases logging level to show debug messages.")
+        .longOpt(Flag.Verbose.name)
+        .hasArg(false)
+        .argName(Flag.Verbose.name)
+        .required(false)
+        .build();
+
+    return new Options()
+        .addOption(baseTemplate)
+        .addOption(cluster)
+        .addOption(config)
+        .addOption(port)
+        .addOption(release)
+        .addOption(property)
+        .addOption(verbose);
+  }
+
+  private static Options constructHelpOptions() {
+    Option help = Option.builder(Flag.Help.name)
+        .desc("List all options and their description")
+        .longOpt("help")
+        .hasArg(false)
+        .required(false)
+        .build();
+
+    return new Options()
+        .addOption(help);
+  }
+
+  // Print usage options
+  private static void usage(Options options) {
+    HelpFormatter formatter = new HelpFormatter();
+    formatter.printHelp("heron-apiserver", options);
+  }
+
+  private static String getConfigurationDirectory(String toolsHome, CommandLine cmd) {
+    if (cmd.hasOption(Flag.ConfigPath.name)) {
+      return cmd.getOptionValue(Flag.ConfigPath.name);
+    } else if (cmd.hasOption(Flag.BaseTemplate.name)) {
+      return Paths.get(toolsHome, Constants.DEFAULT_HERON_CONFIG_DIRECTORY,
+          cmd.getOptionValue(Flag.BaseTemplate.name)).toFile().getAbsolutePath();
+    }
+    return Paths.get(toolsHome, Constants.DEFAULT_HERON_CONFIG_DIRECTORY,
+        cmd.getOptionValue(Flag.Cluster.name)).toFile().getAbsolutePath();
+  }
+
+  private static String getHeronDirectory(CommandLine cmd) {
+    final String cluster = cmd.getOptionValue(Flag.Cluster.name);
+    return "local".equalsIgnoreCase(cluster)
+        ? Constants.DEFAULT_HERON_LOCAL : Constants.DEFAULT_HERON_CLUSTER;
+  }
+
+  private static String getReleaseFile(String toolsHome, CommandLine cmd) {
+    if (cmd.hasOption(Flag.ReleaseFile.name)) {
+      return cmd.getOptionValue(Flag.ReleaseFile.name);
+    }
+    return Paths.get(toolsHome, Constants.DEFAULT_HERON_RELEASE_FILE)
+        .toFile().getAbsolutePath();
+  }
+
+  private static int getPort(CommandLine cmd) {
+    if (cmd.hasOption(Flag.Port.name)) {
+      return Integer.valueOf(cmd.getOptionValue(Flag.Port.name));
+    }
+
+    return Constants.DEFAULT_PORT;
+  }
+
+  private static String loadOverrides(CommandLine cmd) throws IOException {
+    return ConfigUtils.createOverrideConfiguration(
+        cmd.getOptionProperties(Flag.Property.name));
+  }
+
+  private static String getToolsHome() throws URISyntaxException {
+    final String jarLocation =
+        Runtime.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath();
+    return Paths.get(jarLocation).getParent().getParent().toFile().getAbsolutePath();
+  }
+
+  private static Boolean isVerbose(CommandLine cmd) {
+    return cmd.hasOption(Flag.Verbose.name) ? Boolean.TRUE : Boolean.FALSE;
+  }
+
+  @SuppressWarnings({"IllegalCatch", "RegexpSinglelineJava"})
+  public static void main(String[] args) throws Exception {
+    final Options options = createOptions();
+    final Options helpOptions = constructHelpOptions();
+
+    CommandLineParser parser = new DefaultParser();
+
+    // parse the help options first.
+    CommandLine cmd = parser.parse(helpOptions, args, true);
+    if (cmd.hasOption(Flag.Help.name)) {
+      usage(options);
+      return;
+    }
+
+    try {
+      cmd = parser.parse(options, args);
+    } catch (ParseException pe) {
+      System.err.println(pe.getMessage());
+      usage(options);
+      return;
+    }
+
+    LOG.debug("apiserver overrides:\n {}", cmd.getOptionProperties(Flag.Property.name));
+
+    final String toolsHome = getToolsHome();
+
+    // read command line flags
+    final String heronConfigurationDirectory = getConfigurationDirectory(toolsHome, cmd);
+    final String heronDirectory = getHeronDirectory(cmd);
+    final String releaseFile = getReleaseFile(toolsHome, cmd);
+    final String configurationOverrides = loadOverrides(cmd);
+    final int port = getPort(cmd);
+
+
+    final Config baseConfiguration =
+        ConfigUtils.getBaseConfiguration(heronDirectory,
+            heronConfigurationDirectory,
+            releaseFile,
+            configurationOverrides);
+
+    final ResourceConfig config = new ResourceConfig(Resources.get());
+    final Server server = new Server(port);
+
+    final ServletContextHandler contextHandler =
+        new ServletContextHandler(ServletContextHandler.NO_SESSIONS);
+    contextHandler.setContextPath("/");
+
+    LOG.info("using configuration path: {}", heronConfigurationDirectory);
+
+    contextHandler.setAttribute(HeronResource.ATTRIBUTE_CONFIGURATION, baseConfiguration);
+    contextHandler.setAttribute(HeronResource.ATTRIBUTE_CONFIGURATION_DIRECTORY,
+        heronConfigurationDirectory);
+    contextHandler.setAttribute(HeronResource.ATTRIBUTE_CONFIGURATION_OVERRIDE_PATH,
+        configurationOverrides);
+
+    server.setHandler(contextHandler);
+
+    // set logging level
+    Logging.setVerbose(isVerbose(cmd));
+
+    final ServletHolder apiServlet =
+        new ServletHolder(new ServletContainer(config));
+
+    contextHandler.addServlet(apiServlet, API_BASE_PATH);
+
+    try {
+      server.start();
+
+      LOG.info("Heron apiserver started at {}", server.getURI());
+
+      server.join();
+    } catch (Exception ex) {
+      final String message = getErrorMessage(server, port, ex);
+      LOG.error(message);
+      System.err.println(message);
+      System.exit(1);
+    } finally {
+      server.destroy();
+    }
+  }
+
+  private static String getErrorMessage(Server server, int port, Exception ex) {
+    if (ex instanceof BindException) {
+      final URI uri = server.getURI();
+      return String.format("%s http://%s:%d", ex.getMessage(), uri.getHost(), port);
+    }
+
+    return ex.getMessage();
+  }
+
+  private Runtime() {
+  }
+}
diff --git a/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/actions/Action.java b/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/actions/Action.java
new file mode 100644
index 0000000..1ea520b
--- /dev/null
+++ b/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/actions/Action.java
@@ -0,0 +1,18 @@
+//  Copyright 2017 Twitter. All rights reserved.
+//
+//  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 com.twitter.heron.apiserver.actions;
+
+public interface Action {
+  void execute();
+}
diff --git a/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/actions/ActionFactory.java b/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/actions/ActionFactory.java
new file mode 100644
index 0000000..b795511
--- /dev/null
+++ b/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/actions/ActionFactory.java
@@ -0,0 +1,24 @@
+//  Copyright 2017 Twitter. All rights reserved.
+//
+//  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 com.twitter.heron.apiserver.actions;
+
+import com.twitter.heron.spi.common.Config;
+
+public interface ActionFactory {
+
+  Action createSubmitAction(Config config, String topologyPackagePath,
+        String topologyBinaryFileName, String topologyDefinitionPath);
+
+  Action createRuntimeAction(Config config, ActionType type);
+}
diff --git a/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/actions/ActionFactoryImpl.java b/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/actions/ActionFactoryImpl.java
new file mode 100644
index 0000000..67e807c
--- /dev/null
+++ b/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/actions/ActionFactoryImpl.java
@@ -0,0 +1,43 @@
+//  Copyright 2017 Twitter. All rights reserved.
+//
+//  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 com.twitter.heron.apiserver.actions;
+
+import com.twitter.heron.api.generated.TopologyAPI;
+import com.twitter.heron.apiserver.utils.ConfigUtils;
+import com.twitter.heron.spi.common.Config;
+import com.twitter.heron.spi.utils.TopologyUtils;
+
+public class ActionFactoryImpl implements ActionFactory {
+
+  @Override
+  public Action createSubmitAction(Config config, String topologyPackagePath,
+        String topologyBinaryFileName, String topologyDefinitionPath) {
+    final TopologyAPI.Topology topology = TopologyUtils.getTopology(topologyDefinitionPath);
+    final Config topologyConfig =
+        ConfigUtils.getTopologyConfig(topologyPackagePath, topologyBinaryFileName,
+            topologyDefinitionPath, topology);
+    final Config configWithTopology =
+        Config.newBuilder()
+            .putAll(config)
+            .putAll(topologyConfig)
+            .build();
+
+    return new SubmitTopologyAction(configWithTopology, topology);
+  }
+
+  @Override
+  public Action createRuntimeAction(Config config, ActionType type) {
+    return new TopologyRuntimeAction(config, type.command);
+  }
+}
diff --git a/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/actions/ActionType.java b/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/actions/ActionType.java
new file mode 100644
index 0000000..e2db49b
--- /dev/null
+++ b/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/actions/ActionType.java
@@ -0,0 +1,30 @@
+//  Copyright 2017 Twitter. All rights reserved.
+//
+//  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 com.twitter.heron.apiserver.actions;
+
+import com.twitter.heron.scheduler.Command;
+
+public enum  ActionType {
+  KILL(Command.KILL),
+  ACTIVATE(Command.ACTIVATE),
+  DEACTIVATE(Command.DEACTIVATE),
+  RESTART(Command.RESTART),
+  UPDATE(Command.UPDATE);
+
+  public final Command command;
+
+  ActionType(Command command) {
+    this.command = command;
+  }
+}
diff --git a/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/actions/Keys.java b/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/actions/Keys.java
new file mode 100644
index 0000000..f38fd34
--- /dev/null
+++ b/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/actions/Keys.java
@@ -0,0 +1,25 @@
+//  Copyright 2017 Twitter. All rights reserved.
+//
+//  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 com.twitter.heron.apiserver.actions;
+
+import com.twitter.heron.scheduler.RuntimeManagerRunner;
+
+public final class Keys {
+
+  public static final String NEW_COMPONENT_PARALLELISM_KEY =
+      RuntimeManagerRunner.NEW_COMPONENT_PARALLELISM_KEY;
+
+  private Keys() {
+  }
+}
diff --git a/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/actions/SubmitTopologyAction.java b/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/actions/SubmitTopologyAction.java
new file mode 100644
index 0000000..d7b78a5
--- /dev/null
+++ b/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/actions/SubmitTopologyAction.java
@@ -0,0 +1,35 @@
+//  Copyright 2017 Twitter. All rights reserved.
+//
+//  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 com.twitter.heron.apiserver.actions;
+
+import com.twitter.heron.api.generated.TopologyAPI;
+import com.twitter.heron.scheduler.SubmitterMain;
+import com.twitter.heron.spi.common.Config;
+
+public class SubmitTopologyAction implements Action {
+
+  private final Config configuration;
+  private final TopologyAPI.Topology topology;
+
+  SubmitTopologyAction(Config configuration, TopologyAPI.Topology topology) {
+    this.configuration = configuration;
+    this.topology = topology;
+  }
+
+  @Override
+  public void execute() {
+    final SubmitterMain submitter = new SubmitterMain(configuration, topology);
+    submitter.submitTopology();
+  }
+}
diff --git a/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/actions/TopologyRuntimeAction.java b/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/actions/TopologyRuntimeAction.java
new file mode 100644
index 0000000..9c554a4
--- /dev/null
+++ b/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/actions/TopologyRuntimeAction.java
@@ -0,0 +1,35 @@
+//  Copyright 2017 Twitter. All rights reserved.
+//
+//  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 com.twitter.heron.apiserver.actions;
+
+import com.twitter.heron.scheduler.Command;
+import com.twitter.heron.scheduler.RuntimeManagerMain;
+import com.twitter.heron.spi.common.Config;
+
+public class TopologyRuntimeAction implements Action {
+
+  private final Config config;
+  private final Command command;
+
+  TopologyRuntimeAction(Config config, Command command) {
+    this.config = config;
+    this.command = command;
+  }
+
+  @Override
+  public void execute() {
+    final RuntimeManagerMain runtimeManagerMain = new RuntimeManagerMain(config, command);
+    runtimeManagerMain.manageTopology();
+  }
+}
diff --git a/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/resources/ConfigurationResource.java b/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/resources/ConfigurationResource.java
new file mode 100644
index 0000000..701f3c2
--- /dev/null
+++ b/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/resources/ConfigurationResource.java
@@ -0,0 +1,57 @@
+//  Copyright 2017 Twitter. All rights reserved.
+//
+//  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 com.twitter.heron.apiserver.resources;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+import com.twitter.heron.spi.common.Config;
+
+@Path("/")
+public class ConfigurationResource extends HeronResource {
+
+  @Path("version")
+  @GET
+  @Produces(MediaType.APPLICATION_JSON)
+  public Response release() throws JsonProcessingException {
+    final Config configuration = getBaseConfiguration();
+    final ObjectMapper mapper = new ObjectMapper();
+    final ObjectNode node = mapper.createObjectNode();
+    final Set<Map.Entry<String, Object>> sortedConfig = new TreeSet<>(Map.Entry.comparingByKey());
+    sortedConfig.addAll(configuration.getEntrySet());
+    for (Map.Entry<String, Object> entry : sortedConfig) {
+      if (entry.getKey().contains("heron.build")) {
+        node.put(entry.getKey(), entry.getValue().toString());
+      }
+    }
+
+    return Response.ok()
+        .type(MediaType.APPLICATION_JSON)
+        .entity(mapper
+            .writerWithDefaultPrettyPrinter()
+            .writeValueAsString(node))
+        .build();
+  }
+}
diff --git a/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/resources/Forms.java b/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/resources/Forms.java
new file mode 100644
index 0000000..ef57c71
--- /dev/null
+++ b/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/resources/Forms.java
@@ -0,0 +1,58 @@
+//  Copyright 2017 Twitter. All rights reserved.
+//
+//  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 com.twitter.heron.apiserver.resources;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+import javax.ws.rs.core.MultivaluedMap;
+
+import org.glassfish.jersey.media.multipart.FormDataBodyPart;
+import org.glassfish.jersey.media.multipart.FormDataMultiPart;
+
+import com.twitter.heron.apiserver.utils.FileHelper;
+
+final class Forms {
+
+  static String getString(FormDataMultiPart form, String key) {
+    return getValueAs(form, key, String.class);
+  }
+
+  static String getString(FormDataMultiPart form, String key, String defaultValue) {
+    return form.getFields().containsKey(key) ? getValueAs(form, key, String.class) : defaultValue;
+  }
+
+  static String getFirstOrDefault(MultivaluedMap<String, String> params, String key,
+        String defaultValue) {
+    return params.containsKey(key) ? params.getFirst(key) : defaultValue;
+  }
+
+  static File uploadFile(FormDataBodyPart part, String directory) throws IOException {
+    try (InputStream in = part.getValueAs(InputStream.class)) {
+      Path path = Paths.get(directory, part.getFormDataContentDisposition().getFileName());
+      FileHelper.copy(in, path);
+      return path.toFile();
+    }
+  }
+
+  private static <T> T getValueAs(FormDataMultiPart form, String key, Class<T> type) {
+    return form.getField(key).getValueAs(type);
+  }
+
+  private Forms() {
+  }
+}
diff --git a/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/resources/HeronResource.java b/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/resources/HeronResource.java
new file mode 100644
index 0000000..313b120
--- /dev/null
+++ b/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/resources/HeronResource.java
@@ -0,0 +1,57 @@
+//  Copyright 2017 Twitter. All rights reserved.
+//
+//  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 com.twitter.heron.apiserver.resources;
+
+import javax.servlet.ServletContext;
+import javax.ws.rs.core.Context;
+
+import com.twitter.heron.spi.common.Config;
+
+public class HeronResource {
+
+  public static final String ATTRIBUTE_CONFIGURATION = "configuration";
+  public static final String ATTRIBUTE_CONFIGURATION_DIRECTORY = "configuration_directory";
+  public static final String ATTRIBUTE_CONFIGURATION_OVERRIDE_PATH = "configuration_override";
+
+  @Context
+  protected ServletContext servletContext;
+
+  private Config baseConfiguration;
+  private String configurationDirectory;
+  private String configurationOverridePath;
+
+  Config getBaseConfiguration() {
+    if (baseConfiguration == null) {
+      baseConfiguration = (Config) servletContext.getAttribute(ATTRIBUTE_CONFIGURATION);
+    }
+    return baseConfiguration;
+  }
+
+  String getConfigurationDirectory() {
+    if (configurationDirectory == null) {
+      configurationDirectory =
+          (String) servletContext.getAttribute(ATTRIBUTE_CONFIGURATION_DIRECTORY);
+    }
+    return configurationDirectory;
+  }
+
+  String getConfigurationOverridePath() {
+    if (configurationOverridePath == null) {
+      configurationOverridePath =
+          (String) servletContext.getAttribute(ATTRIBUTE_CONFIGURATION_OVERRIDE_PATH);
+    }
+
+    return configurationOverridePath;
+  }
+}
diff --git a/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/resources/NotFoundExceptionHandler.java b/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/resources/NotFoundExceptionHandler.java
new file mode 100644
index 0000000..63eb4d8
--- /dev/null
+++ b/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/resources/NotFoundExceptionHandler.java
@@ -0,0 +1,50 @@
+//  Copyright 2017 Twitter. All rights reserved.
+//
+//  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 com.twitter.heron.apiserver.resources;
+
+import javax.ws.rs.NotFoundException;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+@Provider
+public class NotFoundExceptionHandler implements ExceptionMapper<NotFoundException> {
+  @Override
+  public Response toResponse(NotFoundException exception) {
+    final ObjectMapper mapper = new ObjectMapper()
+        .enable(SerializationFeature.INDENT_OUTPUT)
+        .enable(SerializationFeature.WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED);
+    final ObjectNode node = mapper.createObjectNode();
+    final ArrayNode arrayNode = node.putArray("paths");
+    arrayNode.add("/api/v1/topologies");
+    arrayNode.add("/api/v1/version");
+
+    final String response;
+    try {
+      response = mapper.writeValueAsString(node);
+    } catch (JsonProcessingException e) {
+      throw new RuntimeException("Error creating page not found response", e);
+    }
+
+    return Response.status(Response.Status.NOT_FOUND)
+        .entity(response)
+        .build();
+  }
+}
diff --git a/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/resources/TopologyResource.java b/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/resources/TopologyResource.java
new file mode 100644
index 0000000..41ee7cc
--- /dev/null
+++ b/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/resources/TopologyResource.java
@@ -0,0 +1,518 @@
+//  Copyright 2017 Twitter. All rights reserved.
+//
+//  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 com.twitter.heron.apiserver.resources;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.DefaultValue;
+import javax.ws.rs.FormParam;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+import org.glassfish.jersey.media.multipart.FormDataBodyPart;
+import org.glassfish.jersey.media.multipart.FormDataMultiPart;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.twitter.heron.apiserver.Constants;
+import com.twitter.heron.apiserver.actions.ActionFactory;
+import com.twitter.heron.apiserver.actions.ActionFactoryImpl;
+import com.twitter.heron.apiserver.actions.ActionType;
+import com.twitter.heron.apiserver.actions.Keys;
+import com.twitter.heron.apiserver.utils.ConfigUtils;
+import com.twitter.heron.apiserver.utils.FileHelper;
+import com.twitter.heron.apiserver.utils.Logging;
+import com.twitter.heron.common.basics.DryRunFormatType;
+import com.twitter.heron.common.basics.FileUtils;
+import com.twitter.heron.common.basics.Pair;
+import com.twitter.heron.scheduler.dryrun.DryRunResponse;
+import com.twitter.heron.scheduler.dryrun.SubmitDryRunResponse;
+import com.twitter.heron.scheduler.dryrun.UpdateDryRunResponse;
+import com.twitter.heron.scheduler.utils.DryRunRenders;
+import com.twitter.heron.spi.common.Config;
+import com.twitter.heron.spi.common.Key;
+
+@Path("/topologies")
+public class TopologyResource extends HeronResource {
+
+  private static final Logger LOG = LoggerFactory.getLogger(TopologyResource.class);
+
+  private static final String TOPOLOGY_TAR_GZ_FILENAME = "topology.tar.gz";
+
+  private static final int HTTP_UNPROCESSABLE_ENTITY_CODE = 422;
+
+  private static final String FORM_KEY_NAME = "name";
+  private static final String FORM_KEY_CLUSTER = "cluster";
+  private static final String FORM_KEY_ROLE = "role";
+  private static final String FORM_KEY_ENVIRONMENT = "environment";
+  private static final String FORM_KEY_DEFINITION = "definition";
+  private static final String FORM_KEY_TOPOLOGY = "topology";
+  private static final String FORM_KEY_USER = "user";
+
+  private static final Set<String> SUBMIT_TOPOLOGY_PARAMS = Collections.unmodifiableSet(
+      new HashSet<>(
+        Arrays.asList(
+            FORM_KEY_NAME,
+            FORM_KEY_CLUSTER,
+            FORM_KEY_ROLE,
+            FORM_KEY_ENVIRONMENT,
+            FORM_KEY_DEFINITION,
+            FORM_KEY_TOPOLOGY,
+            FORM_KEY_USER
+        )
+      )
+  );
+
+  private static final String[] REQUIRED_SUBMIT_TOPOLOGY_PARAMS = {
+      FORM_KEY_NAME,
+      FORM_KEY_CLUSTER,
+      FORM_KEY_ROLE,
+      FORM_KEY_DEFINITION,
+      FORM_KEY_TOPOLOGY
+  };
+
+  private static final String PARAM_COMPONENT_PARALLELISM = "component_parallelism";
+  private static final String PARAM_DRY_RUN = "dry_run";
+  private static final String PARAM_DRY_RUN_FORMAT = "dry_run_format";
+  private static final String DEFAULT_DRY_RUN_FORMAT = DryRunFormatType.TABLE.toString();
+
+  // path format /topologies/{cluster}/{role}/{environment}/{name}
+  private static final String TOPOLOGY_PATH_FORMAT = "/topologies/%s/%s/%s/%s";
+
+  private final ActionFactory actionFactory = new ActionFactoryImpl();
+
+  @POST
+  @Consumes(MediaType.MULTIPART_FORM_DATA)
+  @Produces(MediaType.APPLICATION_JSON)
+  @SuppressWarnings({"IllegalCatch", "JavadocMethod"})
+  public Response submit(FormDataMultiPart form) throws IOException {
+    // verify that all we have all the required params
+    final List<String> missingDataKeys =
+        verifyKeys(form.getFields().keySet(), REQUIRED_SUBMIT_TOPOLOGY_PARAMS);
+    if (!missingDataKeys.isEmpty()) {
+      // return error since we are missing required parameters
+      final String message = String.format("Validation failed missing required params: %s",
+          missingDataKeys.toString());
+      return Response.status(HTTP_UNPROCESSABLE_ENTITY_CODE)
+          .type(MediaType.APPLICATION_JSON)
+          .entity(createValidationError(message, missingDataKeys))
+          .build();
+    }
+
+    final String topologyName = Forms.getString(form, FORM_KEY_NAME);
+    final String cluster = Forms.getString(form, FORM_KEY_CLUSTER);
+    final String role = Forms.getString(form, FORM_KEY_ROLE);
+    final String environment =
+        Forms.getString(form, FORM_KEY_ENVIRONMENT, Constants.DEFAULT_HERON_ENVIRONMENT);
+    final String user = Forms.getString(form, FORM_KEY_USER, role);
+
+    // submit overrides are passed key=value
+    final Map<String, String> submitOverrides = getSubmitOverrides(form);
+
+
+    final String topologyDirectory =
+        Files.createTempDirectory(topologyName).toFile().getAbsolutePath();
+
+    try {
+      // upload the topology definition file to the topology directory
+      final FormDataBodyPart definitionFilePart = form.getField(FORM_KEY_DEFINITION);
+      final File topologyDefinitionFile = Forms.uploadFile(definitionFilePart, topologyDirectory);
+
+      // upload the topology binary file to the topology directory
+      final FormDataBodyPart topologyFilePart = form.getField(FORM_KEY_TOPOLOGY);
+      final File topologyBinaryFile = Forms.uploadFile(topologyFilePart, topologyDirectory);
+
+      final boolean isDryRun = form.getFields().containsKey(PARAM_DRY_RUN);
+      final Config config = configWithKeyValues(
+          Arrays.asList(
+              Pair.create(Key.CLUSTER.value(), cluster),
+              Pair.create(Key.TOPOLOGY_NAME.value(), topologyName),
+              Pair.create(Key.ROLE.value(), role),
+              Pair.create(Key.ENVIRON.value(), environment),
+              Pair.create(Key.SUBMIT_USER.value(), user),
+              Pair.create(Key.DRY_RUN.value(), isDryRun)
+          )
+      );
+
+      // copy configuration files to the sandbox config location
+      // topology-dir/<default-heron-sandbox-config>
+      FileHelper.copyDirectory(
+          Paths.get(getConfigurationDirectory()),
+          Paths.get(topologyDirectory, Constants.DEFAULT_HERON_SANDBOX_CONFIG));
+
+
+      final java.nio.file.Path overridesPath =
+          Paths.get(topologyDirectory, Constants.DEFAULT_HERON_SANDBOX_CONFIG,
+              Constants.OVERRIDE_FILE);
+      // copy override file into topology configuration directory
+      FileHelper.copy(Paths.get(getConfigurationOverridePath()), overridesPath);
+
+      // apply submit overrides
+      ConfigUtils.applyOverrides(overridesPath, submitOverrides);
+
+      // apply overrides to state manager config
+      ConfigUtils.applyOverridesToStateManagerConfig(overridesPath,
+          Paths.get(topologyDirectory, Constants.DEFAULT_HERON_SANDBOX_CONFIG,
+              Constants.STATE_MANAGER_FILE)
+      );
+
+      // create tar file from the contents of the topology directory
+      final File topologyPackageFile =
+          Paths.get(topologyDirectory, TOPOLOGY_TAR_GZ_FILENAME).toFile();
+      FileHelper.createTarGz(topologyPackageFile, FileHelper.getChildren(topologyDirectory));
+
+      // submit the topology
+      getActionFactory()
+          .createSubmitAction(config,
+              topologyPackageFile.getAbsolutePath(),
+              topologyBinaryFile.getName(),
+              topologyDefinitionFile.getAbsolutePath())
+          .execute();
+
+      return Response.created(
+          URI.create(String.format(TOPOLOGY_PATH_FORMAT,
+              cluster, role, environment, topologyName)))
+          .type(MediaType.APPLICATION_JSON)
+          .entity(createdResponse(cluster, role, environment, topologyName)).build();
+    } catch (SubmitDryRunResponse response) {
+      return createDryRunResponse(response,
+          Forms.getString(form, PARAM_DRY_RUN_FORMAT, DEFAULT_DRY_RUN_FORMAT));
+    } catch (Exception ex) {
+      return Response.serverError()
+          .type(MediaType.APPLICATION_JSON)
+          .entity(createMessage(ex.getMessage()))
+          .build();
+    } finally {
+      FileUtils.deleteDir(topologyDirectory);
+    }
+  }
+
+  @POST
+  @Path("/{cluster}/{role}/{environment}/{name}/activate")
+  @Produces(MediaType.APPLICATION_JSON)
+  @SuppressWarnings("IllegalCatch")
+  public Response activate(
+      final @PathParam("cluster") String cluster,
+      final @PathParam("role") String role,
+      final @PathParam("environment") String environment,
+      final @PathParam("name") String name) {
+    try {
+      final Config config = getConfig(cluster, role, environment, name);
+      getActionFactory().createRuntimeAction(config, ActionType.ACTIVATE).execute();
+
+      return Response.ok()
+          .type(MediaType.APPLICATION_JSON)
+          .entity(createMessage(String.format("%s activated", name)))
+          .build();
+    } catch (Exception ex) {
+      return Response.serverError()
+          .type(MediaType.APPLICATION_JSON)
+          .entity(createMessage(ex.getMessage()))
+          .build();
+    }
+  }
+
+  @POST
+  @Path("/{cluster}/{role}/{environment}/{name}/deactivate")
+  @Produces(MediaType.APPLICATION_JSON)
+  @SuppressWarnings("IllegalCatch")
+  public Response deactivate(
+      final @PathParam("cluster") String cluster,
+      final @PathParam("role") String role,
+      final @PathParam("environment") String environment,
+      final @PathParam("name") String name) {
+    try {
+      final Config config = getConfig(cluster, role, environment, name);
+      getActionFactory().createRuntimeAction(config, ActionType.DEACTIVATE).execute();
+
+      return Response.ok()
+          .type(MediaType.APPLICATION_JSON)
+          .entity(createMessage(String.format("%s deactivated", name)))
+          .build();
+    } catch (Exception ex) {
+      return Response.serverError()
+          .type(MediaType.APPLICATION_JSON)
+          .entity(createMessage(ex.getMessage()))
+          .build();
+    }
+  }
+
+  @POST
+  @Path("/{cluster}/{role}/{environment}/{name}/restart")
+  @Produces(MediaType.APPLICATION_JSON)
+  @SuppressWarnings("IllegalCatch")
+  public Response restart(
+      final @PathParam("cluster") String cluster,
+      final @PathParam("role") String role,
+      final @PathParam("environment") String environment,
+      final @PathParam("name") String name,
+      final @DefaultValue("-1") @FormParam("container_id") int containerId) {
+    try {
+      final List<Pair<String, Object>> keyValues = new ArrayList<>(
+          Arrays.asList(
+            Pair.create(Key.CLUSTER.value(), cluster),
+            Pair.create(Key.ROLE.value(), role),
+            Pair.create(Key.ENVIRON.value(), environment),
+            Pair.create(Key.TOPOLOGY_NAME.value(), name),
+            Pair.create(Key.TOPOLOGY_CONTAINER_ID.value(),  containerId)
+          )
+      );
+
+      final Config config = configWithKeyValues(keyValues);
+      getActionFactory().createRuntimeAction(config, ActionType.RESTART).execute();
+
+      return Response.ok()
+          .type(MediaType.APPLICATION_JSON)
+          .entity(createMessage(String.format("%s restarted", name)))
+          .build();
+    } catch (Exception ex) {
+      return Response.serverError()
+          .type(MediaType.APPLICATION_JSON)
+          .entity(createMessage(ex.getMessage()))
+          .build();
+    }
+  }
+
+  @POST
+  @Path("/{cluster}/{role}/{environment}/{name}/update")
+  @Produces(MediaType.APPLICATION_JSON)
+  @SuppressWarnings({"IllegalCatch", "JavadocMethod"})
+  public Response update(
+      final @PathParam("cluster") String cluster,
+      final @PathParam("role") String role,
+      final @PathParam("environment") String environment,
+      final @PathParam("name") String name,
+      MultivaluedMap<String, String> params) {
+    try {
+      if (params == null || !params.containsKey(PARAM_COMPONENT_PARALLELISM)) {
+        return Response.status(HTTP_UNPROCESSABLE_ENTITY_CODE)
+            .type(MediaType.APPLICATION_JSON)
+            .entity(createMessage("missing component_parallelism param"))
+            .build();
+      }
+
+      List<String> components = params.get(PARAM_COMPONENT_PARALLELISM);
+      final List<Pair<String, Object>> keyValues = new ArrayList<>(
+          Arrays.asList(
+              Pair.create(Key.CLUSTER.value(), cluster),
+              Pair.create(Key.ROLE.value(), role),
+              Pair.create(Key.ENVIRON.value(), environment),
+              Pair.create(Key.TOPOLOGY_NAME.value(), name),
+              Pair.create(Keys.NEW_COMPONENT_PARALLELISM_KEY,
+                  String.join(",", components))
+          )
+      );
+
+      // has a dry run been requested?
+      if (params.containsKey(PARAM_DRY_RUN)) {
+        keyValues.add(Pair.create(Key.DRY_RUN.value(), Boolean.TRUE));
+      }
+
+      final Set<Pair<String, Object>> overrides = getUpdateOverrides(params);
+      // apply overrides if they exists
+      if (!overrides.isEmpty()) {
+        keyValues.addAll(overrides);
+      }
+
+      final Config config = configWithKeyValues(keyValues);
+      getActionFactory().createRuntimeAction(config, ActionType.UPDATE).execute();
+
+      return Response.ok()
+          .type(MediaType.APPLICATION_JSON)
+          .entity(createMessage(String.format("%s updated", name)))
+          .build();
+    } catch (UpdateDryRunResponse response) {
+      return createDryRunResponse(response,
+          Forms.getFirstOrDefault(params, PARAM_DRY_RUN_FORMAT, DEFAULT_DRY_RUN_FORMAT));
+    } catch (Exception ex) {
+      LOG.error("error updating topology {}", name, ex);
+      return Response.serverError()
+          .type(MediaType.APPLICATION_JSON)
+          .entity(createMessage(ex.getMessage()))
+          .build();
+    }
+  }
+
+  @DELETE
+  @Path("/{cluster}/{role}/{environment}/{name}")
+  @Produces(MediaType.APPLICATION_JSON)
+  @SuppressWarnings("IllegalCatch")
+  public Response kill(
+        final @PathParam("cluster") String cluster,
+        final @PathParam("role") String role,
+        final @PathParam("environment") String environment,
+        final @PathParam("name") String name) {
+    try {
+      final Config config = getConfig(cluster, role, environment, name);
+      getActionFactory().createRuntimeAction(config, ActionType.KILL).execute();
+
+      return Response.ok()
+          .type(MediaType.APPLICATION_JSON)
+          .entity(createMessage(String.format("%s killed", name)))
+          .build();
+    } catch (Exception ex) {
+      final String message = ex.getMessage();
+      final Response.Status status = message.contains("does not exist")
+          ? Response.Status.NOT_FOUND : Response.Status.INTERNAL_SERVER_ERROR;
+      return Response.status(status)
+          .type(MediaType.APPLICATION_JSON)
+          .entity(createMessage(ex.getMessage()))
+          .build();
+    }
+  }
+
+  ActionFactory getActionFactory() {
+    return actionFactory;
+  }
+
+  private Config getConfig(String cluster, String role, String environment, String topologyName) {
+    return configWithKeyValues(
+        Arrays.asList(
+            Pair.create(Key.CLUSTER.value(), cluster),
+            Pair.create(Key.ROLE.value(), role),
+            Pair.create(Key.ENVIRON.value(), environment),
+            Pair.create(Key.TOPOLOGY_NAME.value(), topologyName)
+        ));
+  }
+
+  private Config configWithKeyValues(Collection<Pair<String, Object>> keyValues) {
+    final Config.Builder builder = Config.newBuilder().putAll(getBaseConfiguration());
+    for (Pair<String, Object> keyValue : keyValues) {
+      builder.put(keyValue.first, keyValue.second);
+    }
+    builder.put(Key.VERBOSE, Logging.isVerbose());
+    return Config.toLocalMode(builder.build());
+  }
+
+  private static List<String> verifyKeys(Set<String> keys, String... requiredKeys) {
+    final List<String> missingKeys = new ArrayList<>();
+    if (requiredKeys != null) {
+      for (String key : requiredKeys) {
+        if (!keys.contains(key)) {
+          missingKeys.add(key);
+        }
+      }
+    }
+    return missingKeys;
+  }
+
+  private static Map<String, String> getSubmitOverrides(FormDataMultiPart form) {
+    final Map<String, String> overrides = new HashMap<>();
+    for (String key : form.getFields().keySet()) {
+      if (!SUBMIT_TOPOLOGY_PARAMS.contains(key)) {
+        overrides.put(key, Forms.getString(form, key));
+      }
+    }
+    return overrides;
+  }
+
+  private static Set<Pair<String, Object>> getUpdateOverrides(
+      MultivaluedMap<String, String> params) {
+    final Set<Pair<String, Object>> overrides = new HashSet<>();
+    for (String key : params.keySet()) {
+      if (!PARAM_COMPONENT_PARALLELISM.equalsIgnoreCase(key)) {
+        overrides.add(Pair.create(key, params.getFirst(key)));
+      }
+    }
+    return overrides;
+  }
+
+  @SuppressWarnings("IllegalCatch")
+  private static DryRunFormatType getDryRunFormatType(String type) {
+    try {
+      if (type != null) {
+        return DryRunFormatType.valueOf(type);
+      }
+    } catch (Exception ex) {
+      LOG.warn("unknown dry format render type {} defaulting to table", type);
+    }
+    return DryRunFormatType.TABLE;
+  }
+
+  private static String getDryRunResponse(DryRunResponse response, String type) {
+    if (response instanceof SubmitDryRunResponse) {
+      return DryRunRenders.render((SubmitDryRunResponse) response,
+          getDryRunFormatType(type));
+    } else if (response instanceof UpdateDryRunResponse) {
+      return DryRunRenders.render((UpdateDryRunResponse) response,
+          getDryRunFormatType(type));
+    }
+    return "Unknown dry run response type " + response.getClass().getName();
+  }
+
+  private static Response createDryRunResponse(DryRunResponse response, String type) {
+    final String body = new ObjectMapper().createObjectNode()
+        .put("response", getDryRunResponse(response, type))
+        .toString();
+
+    return Response.ok()
+        .type(MediaType.APPLICATION_JSON)
+        .entity(body)
+        .build();
+  }
+
+  private static String createdResponse(String cluster, String role, String environment,
+        String topologyName) {
+    return new ObjectMapper().createObjectNode()
+        .put("name", topologyName)
+        .put("cluster", cluster)
+        .put("role", role)
+        .put("environment", environment)
+        .toString();
+  }
+
+  private static ObjectNode createBaseError(String message) {
+    final ObjectMapper mapper = new ObjectMapper();
+    return mapper.createObjectNode().put("message", message);
+  }
+
+  private static String createMessage(String message) {
+    return createBaseError(message).toString();
+  }
+
+  private static String createValidationError(String message, List<String> missing) {
+    ObjectNode node = createBaseError(message);
+    ObjectNode errors = node.putObject("errors");
+    ArrayNode missingParameters = errors.putArray("missing_parameters");
+    for (String param : missing) {
+      missingParameters.add(param);
+    }
+
+    return node.toString();
+  }
+}
diff --git a/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/utils/ConfigUtils.java b/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/utils/ConfigUtils.java
new file mode 100644
index 0000000..caf3085
--- /dev/null
+++ b/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/utils/ConfigUtils.java
@@ -0,0 +1,152 @@
+//  Copyright 2017 Twitter. All rights reserved.
+//
+//  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 com.twitter.heron.apiserver.utils;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.Writer;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+import org.yaml.snakeyaml.DumperOptions;
+import org.yaml.snakeyaml.Yaml;
+
+import com.twitter.heron.api.generated.TopologyAPI;
+import com.twitter.heron.common.basics.SysUtils;
+import com.twitter.heron.scheduler.utils.SubmitterUtils;
+import com.twitter.heron.spi.common.Config;
+import com.twitter.heron.spi.common.ConfigLoader;
+
+public final class ConfigUtils {
+
+  private static final String CONFIG_SUFFIX = ".yaml";
+
+  public static Config.Builder builder(Config baseConfiguration) {
+    return Config.newBuilder().putAll(baseConfiguration);
+  }
+
+  public static String createOverrideConfiguration(Properties properties) throws IOException {
+    final Path overridesPath = Files.createTempFile("overrides-", CONFIG_SUFFIX);
+    try (Writer writer = Files.newBufferedWriter(overridesPath)) {
+      final Map<Object, Object> overrides = new HashMap<>();
+      for (Map.Entry<Object, Object> entry : properties.entrySet()) {
+        overrides.put(entry.getKey(), entry.getValue());
+      }
+      final Yaml yaml = newYaml();
+      yaml.dump(overrides, writer);
+
+      return overridesPath.toFile().getAbsolutePath();
+    } finally {
+      overridesPath.toFile().deleteOnExit();
+    }
+  }
+
+  public static Config getBaseConfiguration(String heronDirectory,
+        String heronConfigDirectory,
+        String releaseFile,
+        String overrideConfigurationFile) {
+    // TODO add release file
+    return ConfigLoader.loadConfig(heronDirectory,
+        heronConfigDirectory,
+        releaseFile,
+        overrideConfigurationFile);
+  }
+
+  public static Config getTopologyConfig(String topologyPackage, String topologyBinaryFile,
+        String topologyDefinitionFile, TopologyAPI.Topology topology) {
+    return SubmitterUtils.topologyConfigs(
+        topologyPackage,
+        topologyBinaryFile,
+        topologyDefinitionFile,
+        topology);
+  }
+
+  @SuppressWarnings("unchecked")
+  public static void applyOverrides(Path overridesPath, Map<String, String> overrides)
+      throws IOException {
+    if (overrides.isEmpty()) {
+      return;
+    }
+    final Path tempOverridesPath = Files.createTempFile("overrides-", CONFIG_SUFFIX);
+
+    Reader overrideReader = null;
+    try (Writer writer = Files.newBufferedWriter(tempOverridesPath)) {
+      overrideReader = Files.newBufferedReader(overridesPath);
+      final Map<String, Object> currentOverrides =
+          (Map<String, Object>) new Yaml().load(overrideReader);
+      currentOverrides.putAll(overrides);
+
+      // write updated overrides
+      newYaml().dump(currentOverrides, writer);
+
+      // close override file so we can replace it with the updated overrides
+      overrideReader.close();
+
+      FileHelper.copy(tempOverridesPath, overridesPath);
+    } finally {
+      tempOverridesPath.toFile().delete();
+      SysUtils.closeIgnoringExceptions(overrideReader);
+    }
+  }
+
+  // this is needed because the heron executor ignores the override.yaml
+  @SuppressWarnings("unchecked")
+  public static void applyOverridesToStateManagerConfig(Path overridesPath,
+        Path stateManagerPath) throws IOException {
+    final Path tempStateManagerPath = Files.createTempFile("statemgr-", CONFIG_SUFFIX);
+    Reader stateManagerReader = null;
+    try (
+        Reader overrideReader = Files.newBufferedReader(overridesPath);
+        Writer writer = Files.newBufferedWriter(tempStateManagerPath);
+    ) {
+      stateManagerReader = Files.newBufferedReader(stateManagerPath);
+
+      final Map<String, Object> overrides = (Map<String, Object>) new Yaml().load(overrideReader);
+      final Map<String, Object> stateMangerConfig =
+          (Map<String, Object>) new Yaml().load(stateManagerReader);
+      // update the state manager config with the overrides
+      for (Map.Entry<String, Object> entry : overrides.entrySet()) {
+        // does this key have an override?
+        if (stateMangerConfig.containsKey(entry.getKey())) {
+          stateMangerConfig.put(entry.getKey(), entry.getValue());
+        }
+      }
+
+      // write new state manager config
+      newYaml().dump(stateMangerConfig, writer);
+
+      // close state manager file so we can replace it with the updated configuration
+      stateManagerReader.close();
+
+      FileHelper.copy(tempStateManagerPath, stateManagerPath);
+    } finally {
+      tempStateManagerPath.toFile().delete();
+      SysUtils.closeIgnoringExceptions(stateManagerReader);
+    }
+  }
+
+  private static Yaml newYaml() {
+    final DumperOptions options = new DumperOptions();
+    options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
+    options.setPrettyFlow(true);
+
+    return new Yaml(options);
+  }
+
+  private ConfigUtils() {
+  }
+}
diff --git a/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/utils/FileHelper.java b/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/utils/FileHelper.java
new file mode 100644
index 0000000..39a0294
--- /dev/null
+++ b/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/utils/FileHelper.java
@@ -0,0 +1,142 @@
+//  Copyright 2017 Twitter. All rights reserved.
+//
+//  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 com.twitter.heron.apiserver.utils;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.StandardCopyOption;
+import java.nio.file.attribute.BasicFileAttributes;
+
+import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
+import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
+import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public final class FileHelper {
+
+  private static final Logger LOG = LoggerFactory.getLogger(FileHelper.class);
+
+  public static boolean copy(InputStream in, Path to) {
+    try {
+      Files.copy(in, to, StandardCopyOption.REPLACE_EXISTING);
+    } catch (IOException ioe) {
+      LOG.error("Failed to copy file to {}", to, ioe);
+      return false;
+    }
+    return true;
+  }
+
+  public static boolean copy(Path from, Path to) {
+    try {
+      Files.copy(from, to, StandardCopyOption.REPLACE_EXISTING);
+    } catch (IOException ioe) {
+      LOG.error("Failed to copy file from {} to {}", from, to, ioe);
+      return false;
+    }
+    return true;
+  }
+
+  public static File[] getChildren(String path) {
+    final File file = new File(path);
+    return file.isDirectory() ? file.listFiles() : new File[] {};
+  }
+
+  public static boolean copyDirectory(Path from, Path to) {
+    try {
+      Files.walkFileTree(from, new CopyDirectoryVisitor(from, to));
+    } catch (IOException ioe) {
+      LOG.error("Failed to copy directory from {} to {}", from, to, ioe);
+      return false;
+    }
+    return true;
+  }
+
+  public static boolean createTarGz(File archive, File... files) {
+    try (
+        FileOutputStream fileOutputStream = new FileOutputStream(archive);
+        BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(fileOutputStream);
+        GzipCompressorOutputStream gzipOuputStream =
+            new GzipCompressorOutputStream(bufferedOutputStream);
+        TarArchiveOutputStream archiveOutputStream = new TarArchiveOutputStream(gzipOuputStream)
+    ) {
+      for (File file : files) {
+        addFileToArchive(archiveOutputStream, file, "");
+      }
+      archiveOutputStream.finish();
+    } catch (IOException ioe) {
+      LOG.error("Failed to create archive {} file.", archive, ioe);
+      return false;
+    }
+    return true;
+  }
+
+  private static void addFileToArchive(TarArchiveOutputStream archiveOutputStream, File file,
+                                       String base) throws IOException {
+    final File absoluteFile = file.getAbsoluteFile();
+    final String entryName = base + file.getName();
+    final TarArchiveEntry tarArchiveEntry = new TarArchiveEntry(file, entryName);
+    archiveOutputStream.putArchiveEntry(tarArchiveEntry);
+
+    if (absoluteFile.isFile()) {
+      Files.copy(file.toPath(), archiveOutputStream);
+      archiveOutputStream.closeArchiveEntry();
+    } else {
+      archiveOutputStream.closeArchiveEntry();
+      if (absoluteFile.listFiles() != null) {
+        for (File f : absoluteFile.listFiles()) {
+          addFileToArchive(archiveOutputStream, f, entryName + "/");
+        }
+      }
+    }
+  }
+
+  private static final class CopyDirectoryVisitor extends SimpleFileVisitor<Path> {
+    private final Path fromPath;
+    private final Path toPath;
+    private final StandardCopyOption copyOption = StandardCopyOption.REPLACE_EXISTING;
+
+    private CopyDirectoryVisitor(Path from, Path to) {
+      fromPath = from;
+      toPath = to;
+    }
+
+    @Override
+    public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
+        throws IOException {
+      Path targetPath = toPath.resolve(fromPath.relativize(dir));
+      if (!Files.exists(targetPath)) {
+        Files.createDirectory(targetPath);
+      }
+      return FileVisitResult.CONTINUE;
+    }
+
+    @Override
+    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+      Files.copy(file, toPath.resolve(fromPath.relativize(file)), copyOption);
+      return FileVisitResult.CONTINUE;
+    }
+  }
+
+  private FileHelper() {
+  }
+}
diff --git a/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/utils/Logging.java b/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/utils/Logging.java
new file mode 100644
index 0000000..d71ac7d
--- /dev/null
+++ b/heron/tools/apiserver/src/java/com/twitter/heron/apiserver/utils/Logging.java
@@ -0,0 +1,30 @@
+//  Copyright 2017 Twitter. All rights reserved.
+//
+//  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 com.twitter.heron.apiserver.utils;
+
+public final class Logging {
+
+  private static volatile boolean isVerbose;
+
+  public static void setVerbose(boolean verbose) {
+    isVerbose = verbose;
+  }
+
+  public static boolean isVerbose() {
+    return isVerbose;
+  }
+
+  private Logging() {
+  }
+}
diff --git a/heron/tools/apiserver/src/shell/BUILD b/heron/tools/apiserver/src/shell/BUILD
new file mode 100644
index 0000000..d96eec8
--- /dev/null
+++ b/heron/tools/apiserver/src/shell/BUILD
@@ -0,0 +1,6 @@
+package(default_visibility = ["//visibility:public"])
+
+sh_binary(
+  name = "heron-apiserver",
+  srcs = [ "heron-apiserver.sh" ],
+)
diff --git a/heron/tools/apiserver/src/shell/heron-apiserver.sh b/heron/tools/apiserver/src/shell/heron-apiserver.sh
new file mode 100755
index 0000000..f217011
--- /dev/null
+++ b/heron/tools/apiserver/src/shell/heron-apiserver.sh
@@ -0,0 +1,19 @@
+#!/usr/bin/env bash
+
+BINDIR=$(dirname $(readlink $0))
+HERON_TOOLS_HOME=$(dirname ${BINDIR})
+HERON_APISERVER_JAR=${HERON_TOOLS_HOME}/lib/heron-apiserver.jar
+RELEASE_FILE=${HERON_TOOLS_HOME}/release.yaml
+
+# Check for the java to use
+if [[ -z $JAVA_HOME ]]; then
+  JAVA=$(which java)
+  if [ $? != 0 ]; then
+   echo "Error: JAVA_HOME not set, and no java executable found in $PATH." 1>&2
+   exit 1
+  fi
+else
+  JAVA=${JAVA_HOME}/bin/java
+fi
+
+exec $JAVA -jar $HERON_APISERVER_JAR $@
diff --git a/heron/tools/apiserver/tests/java/BUILD b/heron/tools/apiserver/tests/java/BUILD
new file mode 100644
index 0000000..8017bbc
--- /dev/null
+++ b/heron/tools/apiserver/tests/java/BUILD
@@ -0,0 +1,29 @@
+load("/tools/rules/java_tests", "java_tests")
+
+load("/tools/rules/heron_deps", "heron_java_proto_files")
+
+common_deps_files = [
+    "//third_party/java:powermock",
+    "@commons_io_commons_io//jar",
+    "//third_party/java:mockito",
+    "//third_party/java:junit4",
+]
+
+apiserver_test_deps_files = \
+  common_deps_files + [
+    "//heron/tools/apiserver/src/java:heron-apiserver"
+  ]
+
+java_library(
+    name = "tests",
+    srcs = glob(["**/apiserver/**/*.java"]),
+    deps = apiserver_test_deps_files,
+)
+
+java_tests(
+  test_classes = [
+    "com.twitter.heron.apiserver.utils.ConfigUtilsTests",
+  ],
+  runtime_deps = [ ":tests" ],
+  size = "small",
+)
diff --git a/heron/tools/apiserver/tests/java/com/twitter/heron/apiserver/utils/ConfigUtilsTests.java b/heron/tools/apiserver/tests/java/com/twitter/heron/apiserver/utils/ConfigUtilsTests.java
new file mode 100644
index 0000000..d02f1c2
--- /dev/null
+++ b/heron/tools/apiserver/tests/java/com/twitter/heron/apiserver/utils/ConfigUtilsTests.java
@@ -0,0 +1,187 @@
+//  Copyright 2017 Twitter. All rights reserved.
+//
+//  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 com.twitter.heron.apiserver.utils;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.Writer;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+import org.junit.Test;
+
+import org.yaml.snakeyaml.Yaml;
+
+import com.twitter.heron.common.basics.Pair;
+
+import static org.junit.Assert.assertEquals;
+
+public class ConfigUtilsTests {
+
+  @Test
+  @SuppressWarnings("unchecked")
+  public void testCreateOverrides() throws IOException {
+    final Properties overrideProperties = createOverrideProperties(
+        Pair.create("heron.statemgr.connection.string", "zookeeper:2181"),
+        Pair.create("heron.kubernetes.scheduler.uri", "http://localhost:8001")
+    );
+
+    final String overridesPath = ConfigUtils.createOverrideConfiguration(overrideProperties);
+    try (Reader reader = Files.newBufferedReader(Paths.get(overridesPath))) {
+      final Map<String, Object> overrides =
+          (Map<String, Object>) new Yaml().loadAs(reader, Map.class);
+      assertEquals(overrides.size(), overrideProperties.size());
+      for (String key : overrides.keySet()) {
+        assertEquals(overrides.get(key), overrideProperties.getProperty(key));
+      }
+    }
+  }
+
+  @Test
+  @SuppressWarnings("unchecked")
+  public void testStateManagerFileOverrides() throws IOException {
+    final Properties overrideProperties = createOverrideProperties(
+        Pair.create("heron.statemgr.connection.string", "zookeeper:2181"),
+        Pair.create("heron.kubernetes.scheduler.uri", "http://localhost:8001")
+    );
+
+    final String overridesPath = ConfigUtils.createOverrideConfiguration(overrideProperties);
+
+    // write default state manager config
+    final Path stateManagerPath = Files.createTempFile("statemgr-", ".yaml");
+    stateManagerPath.toFile().deleteOnExit();
+    try (Writer writer = Files.newBufferedWriter(stateManagerPath)) {
+      final Map<String, String> config = new HashMap<>();
+      config.put("heron.statemgr.connection.string", "<host>:<port>");
+      new Yaml().dump(config, writer);
+    }
+
+    // apply the overrides
+    ConfigUtils.applyOverridesToStateManagerConfig(Paths.get(overridesPath), stateManagerPath);
+
+    try (Reader reader = Files.newBufferedReader(stateManagerPath)) {
+      final Map<String, Object> stateManagerWithOverrides =
+          (Map<String, Object>) new Yaml().loadAs(reader, Map.class);
+      assertEquals(stateManagerWithOverrides.size(), 1);
+      assertEquals(stateManagerWithOverrides.get("heron.statemgr.connection.string"),
+          "zookeeper:2181");
+    }
+  }
+
+  @Test
+  @SuppressWarnings("unchecked")
+  public void testNoOverridesAppliedToStateManager() throws IOException {
+    final Properties overrideProperties = createOverrideProperties(
+        Pair.create("heron.kubernetes.scheduler.uri", "http://localhost:8001")
+    );
+
+    final String overridesPath = ConfigUtils.createOverrideConfiguration(overrideProperties);
+
+    // write default state manager config
+    final Path stateManagerPath = Files.createTempFile("statemgr-", ".yaml");
+    stateManagerPath.toFile().deleteOnExit();
+    try (Writer writer = Files.newBufferedWriter(stateManagerPath)) {
+      final Map<String, String> config = new HashMap<>();
+      config.put("heron.statemgr.connection.string", "<host>:<port>");
+      new Yaml().dump(config, writer);
+    }
+
+    // apply the overrides
+    ConfigUtils.applyOverridesToStateManagerConfig(Paths.get(overridesPath), stateManagerPath);
+
+    try (Reader reader = Files.newBufferedReader(stateManagerPath)) {
+      final Map<String, Object> stateManagerWithOverrides =
+          (Map<String, Object>) new Yaml().loadAs(reader, Map.class);
+      assertEquals(stateManagerWithOverrides.size(), 1);
+      assertEquals(stateManagerWithOverrides.get("heron.statemgr.connection.string"),
+          "<host>:<port>");
+    }
+  }
+
+  @Test
+  @SuppressWarnings("unchecked")
+  public void testApplyOverrides() throws IOException {
+    final Properties overrideProperties = createOverrideProperties(
+        Pair.create("heron.statemgr.connection.string", "zookeeper:2181"),
+        Pair.create("heron.kubernetes.scheduler.uri", "http://localhost:8001")
+    );
+
+    final String overridesPath = ConfigUtils.createOverrideConfiguration(overrideProperties);
+
+    final Map<String, String> overrides = createOverrides(
+        Pair.create("my.override.key", "my.override.value")
+    );
+
+    ConfigUtils.applyOverrides(Paths.get(overridesPath), overrides);
+
+    final Map<String, String> combinedOverrides = new HashMap<>();
+    combinedOverrides.putAll(overrides);
+    for (String key : overrideProperties.stringPropertyNames()) {
+      combinedOverrides.put(key, overrideProperties.getProperty(key));
+    }
+
+    try (Reader reader = Files.newBufferedReader(Paths.get(overridesPath))) {
+      final Map<String, Object> newOverrides =
+          (Map<String, Object>) new Yaml().loadAs(reader, Map.class);
+      assertEquals(newOverrides, combinedOverrides);
+    }
+  }
+
+  @Test
+  @SuppressWarnings("unchecked")
+  public void testApplyEmptyOverrides() throws IOException {
+    final Properties overrideProperties = createOverrideProperties(
+        Pair.create("heron.statemgr.connection.string", "zookeeper:2181"),
+        Pair.create("heron.kubernetes.scheduler.uri", "http://localhost:8001")
+    );
+
+    final String overridesPath = ConfigUtils.createOverrideConfiguration(overrideProperties);
+
+    ConfigUtils.applyOverrides(Paths.get(overridesPath), new HashMap<>());
+
+    final Map<String, String> overrides = new HashMap<>();
+    for (String key : overrideProperties.stringPropertyNames()) {
+      overrides.put(key, overrideProperties.getProperty(key));
+    }
+
+    try (Reader reader = Files.newBufferedReader(Paths.get(overridesPath))) {
+      final Map<String, Object> newOverrides =
+          (Map<String, Object>) new Yaml().loadAs(reader, Map.class);
+      assertEquals(newOverrides, overrides);
+    }
+  }
+
+  @SuppressWarnings("unchecked")
+  private Map<String, String> createOverrides(Pair<String, String>... keyValues) {
+    final Map<String, String> overrides = new HashMap<>();
+    for (Pair<String, String> kv : keyValues) {
+      overrides.put(kv.first, kv.second);
+    }
+    return overrides;
+  }
+
+  @SuppressWarnings("unchecked")
+  private Properties createOverrideProperties(Pair<String, String>... props) {
+    final Properties properties = new Properties();
+    for (Pair<String, String> prop : props) {
+      properties.setProperty(prop.first, prop.second);
+    }
+
+    return properties;
+  }
+}
diff --git a/heron/tools/cli/src/python/BUILD b/heron/tools/cli/src/python/BUILD
index 718a922..23ff8bf 100644
--- a/heron/tools/cli/src/python/BUILD
+++ b/heron/tools/cli/src/python/BUILD
@@ -12,7 +12,11 @@
         "//heron/tools/common/src/python:common-py",
         "//heron/proto:proto-py",
     ],
-    reqs = ["PyYAML==3.10", "enum34==1.1.6"],
+    reqs = [
+      "PyYAML==3.10",
+      "enum34==1.1.6",
+      "requests==2.18.1",
+    ],
 )
 
 pex_binary(
diff --git a/heron/tools/cli/src/python/activate.py b/heron/tools/cli/src/python/activate.py
index 306509e..365d6c9 100644
--- a/heron/tools/cli/src/python/activate.py
+++ b/heron/tools/cli/src/python/activate.py
@@ -12,6 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 ''' activate '''
+from heron.common.src.python.utils.log import Log
 import heron.tools.cli.src.python.cli_helper as cli_helper
 
 def create_parser(subparsers):
@@ -31,4 +32,5 @@
   :param unknown_args:
   :return:
   '''
+  Log.debug("Activate Args: %s", cl_args)
   return cli_helper.run(command, cl_args, "activate topology")
diff --git a/heron/tools/cli/src/python/args.py b/heron/tools/cli/src/python/args.py
index 5f7feb6..ab33fde 100644
--- a/heron/tools/cli/src/python/args.py
+++ b/heron/tools/cli/src/python/args.py
@@ -85,6 +85,16 @@
   )
   return parser
 
+def add_service_url(parser):
+  '''
+  :param parser:
+  :return:
+  '''
+  parser.add_argument(
+      '--service-url',
+      default="",
+      help='API service end point')
+  return parser
 
 def add_config(parser):
   '''
diff --git a/heron/tools/cli/src/python/cdefs.py b/heron/tools/cli/src/python/cdefs.py
new file mode 100644
index 0000000..8b52304
--- /dev/null
+++ b/heron/tools/cli/src/python/cdefs.py
@@ -0,0 +1,63 @@
+# Copyright 2016 Twitter. All rights reserved.
+#
+# 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.
+''' cdefs.py '''
+import os
+import yaml
+
+import heron.common.src.python.utils.log as Log
+import heron.tools.common.src.python.utils.config as config
+
+################################################################################
+def read_server_mode_cluster_definition(cluster, cl_args, config_file):
+  '''
+  Read the cluster definition for server mode
+  :param cluster:
+  :param cl_args:
+  :param config_file:
+  :return:
+  '''
+
+  client_confs = dict()
+
+  # check if the config file exists, if it does, read it
+  if os.path.isfile(config_file):
+    with open(config_file, 'r') as conf_file:
+      client_confs = yaml.load(conf_file)
+
+  if not client_confs:
+    client_confs = dict()
+    client_confs[cluster] = dict()
+
+  # now check if the service-url from command line is set, if so override it
+  if cl_args['service_url']:
+    client_confs[cluster]['service_url'] = cl_args['service_url']
+
+  # the return value of yaml.load can be None if conf_file is an empty file
+  # or there is no service-url in command line, if needed.
+
+  return client_confs
+
+################################################################################
+def check_direct_mode_cluster_definition(cluster, config_path):
+  '''
+  Check the cluster definition for direct mode
+  :param cluster:
+  :param config_path:
+  :return:
+  '''
+  config_path = config.get_heron_cluster_conf_dir(cluster, config_path)
+  if not os.path.isdir(config_path):
+    Log.error("Cluster config directory \'%s\' does not exist", config_path)
+    return False
+  return True
diff --git a/heron/tools/cli/src/python/cli_helper.py b/heron/tools/cli/src/python/cli_helper.py
index 7385f9f..c33d93fd 100644
--- a/heron/tools/cli/src/python/cli_helper.py
+++ b/heron/tools/cli/src/python/cli_helper.py
@@ -13,10 +13,13 @@
 # limitations under the License.
 ''' cli_helper.py '''
 import logging
+import requests
 import heron.tools.common.src.python.utils.config as config
+from heron.tools.cli.src.python.result import SimpleResult, Status
 import heron.tools.cli.src.python.args as args
 import heron.tools.cli.src.python.execute as execute
 import heron.tools.cli.src.python.jars as jars
+import heron.tools.cli.src.python.rest as rest
 
 from heron.common.src.python.utils.log import Log
 
@@ -39,15 +42,64 @@
   args.add_topology(parser)
 
   args.add_config(parser)
+  args.add_service_url(parser)
   args.add_verbose(parser)
 
   parser.set_defaults(subcommand=action)
   return parser
 
+################################################################################
+def flatten_args(fargs):
+  temp_args = []
+  for k, v in fargs.iteritems():
+    if isinstance(v, list):
+      temp_args.extend([(k, value) for value in v])
+    else:
+      temp_args.append((k, v))
+  return temp_args
 
 ################################################################################
 # pylint: disable=dangerous-default-value
-def run(command, cl_args, action, extra_args=[], extra_lib_jars=[]):
+def run_server(command, cl_args, action, extra_args=dict()):
+  '''
+  helper function to take action on topologies using REST API
+  :param command:
+  :param cl_args:
+  :param action:        description of action taken
+  :return:
+  '''
+  topology_name = cl_args['topology-name']
+
+  service_endpoint = cl_args['service_url']
+  apiroute = rest.ROUTE_SIGNATURES[command][1] % (
+      cl_args['cluster'],
+      cl_args['role'],
+      cl_args['environ'],
+      topology_name
+  )
+  service_apiurl = service_endpoint + apiroute
+  service_method = rest.ROUTE_SIGNATURES[command][0]
+
+  # convert the dictionary to a list of tuples
+  data = flatten_args(extra_args)
+
+  err_msg = "Failed to %s: %s" % (action, topology_name)
+  succ_msg = "Successfully %s: %s" % (action, topology_name)
+
+  try:
+    r = service_method(service_apiurl, data=data)
+    s = Status.Ok if r.status_code == requests.codes.ok else Status.HeronError
+    if r.status_code != requests.codes.ok:
+      Log.error(r.json().get('message', "Unknown error from api server %d" % r.status_code))
+  except (requests.exceptions.ConnectionError, requests.exceptions.HTTPError) as err:
+    Log.error(err)
+    return SimpleResult(Status.HeronError, err_msg, succ_msg)
+
+  return SimpleResult(s, err_msg, succ_msg)
+
+################################################################################
+# pylint: disable=dangerous-default-value
+def run_direct(command, cl_args, action, extra_args=[], extra_lib_jars=[]):
   '''
   helper function to take action on topologies
   :param command:
@@ -89,3 +141,10 @@
   succ_msg = "Successfully %s: %s" % (action, topology_name)
   result.add_context(err_msg, succ_msg)
   return result
+
+################################################################################
+def run(command, cl_args, action, extra_lib_jars=[]):
+  if cl_args['deploy_mode'] == config.SERVER_MODE:
+    return run_server(command, cl_args, action, extra_args=dict())
+  else:
+    return run_direct(command, cl_args, action, extra_args=[], extra_lib_jars=extra_lib_jars)
diff --git a/heron/tools/cli/src/python/deactivate.py b/heron/tools/cli/src/python/deactivate.py
index 08dc04a..be23bee 100644
--- a/heron/tools/cli/src/python/deactivate.py
+++ b/heron/tools/cli/src/python/deactivate.py
@@ -12,9 +12,9 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 ''' deactivate.py '''
+from heron.common.src.python.utils.log import Log
 import heron.tools.cli.src.python.cli_helper as cli_helper
 
-
 def create_parser(subparsers):
   '''
   :param subparsers:
@@ -32,4 +32,5 @@
   :param unknown_args:
   :return:
   '''
+  Log.debug("Deactivate Args: %s", cl_args)
   return cli_helper.run(command, cl_args, "deactivate topology")
diff --git a/heron/tools/cli/src/python/kill.py b/heron/tools/cli/src/python/kill.py
index 928c4d5..ac91070 100644
--- a/heron/tools/cli/src/python/kill.py
+++ b/heron/tools/cli/src/python/kill.py
@@ -12,9 +12,9 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 ''' kill.py '''
+from heron.common.src.python.utils.log import Log
 import heron.tools.cli.src.python.cli_helper as cli_helper
 
-
 def create_parser(subparsers):
   '''
   :param subparsers:
@@ -32,4 +32,5 @@
   :param unknown_args:
   :return:
   '''
+  Log.debug("Kill Args: %s", cl_args)
   return cli_helper.run(command, cl_args, "kill topology")
diff --git a/heron/tools/cli/src/python/main.py b/heron/tools/cli/src/python/main.py
index f77a879..e7f0a21 100644
--- a/heron/tools/cli/src/python/main.py
+++ b/heron/tools/cli/src/python/main.py
@@ -25,6 +25,7 @@
 
 import heron.common.src.python.utils.log as log
 import heron.tools.common.src.python.utils.config as config
+import heron.tools.cli.src.python.cdefs as cdefs
 import heron.tools.cli.src.python.help as cli_help
 import heron.tools.cli.src.python.activate as activate
 import heron.tools.cli.src.python.deactivate as deactivate
@@ -144,6 +145,118 @@
   if not config.check_release_file_exists():
     sys.exit(1)
 
+################################################################################
+# pylint: disable=unused-argument
+def server_deployment_mode(command, parser, cluster, cl_args):
+  '''
+  check the server deployment mode for the given cluster
+  if it is valid return the valid set of args
+  :param cluster:
+  :param cl_args:
+  :return:
+  '''
+  config_file = config.heron_rc_file()
+
+  # Read the cluster definition, if not found
+  client_confs = cdefs.read_server_mode_cluster_definition(cluster, cl_args, config_file)
+  if not client_confs[cluster]:
+    return dict()
+
+  # tell the user which definition that we are using
+  if not cl_args['service_url']:
+    Log.info("Using cluster definition from file %s" % config_file)
+  else:
+    Log.info("Using cluster service url %s" % cl_args['service_url'])
+
+  # if cluster definition exists, but service_url is not set, it is an error
+  if not 'service_url' in client_confs[cluster]:
+    Log.error('No service url for %s cluster in %s', cluster, config_file)
+    sys.exit(1)
+
+  try:
+    cluster_role_env = (cl_args['cluster'], cl_args['role'], cl_args['environ'])
+    config.server_mode_cluster_role_env(cluster_role_env, client_confs, config_file)
+    cluster_tuple = config.defaults_cluster_role_env(cluster_role_env)
+  except Exception as ex:
+    Log.error("Argument cluster/[role]/[env] is not correct: %s", str(ex))
+    sys.exit(1)
+
+  new_cl_args = dict()
+  new_cl_args['cluster'] = cluster_tuple[0]
+  new_cl_args['role'] = cluster_tuple[1]
+  new_cl_args['environ'] = cluster_tuple[2]
+  new_cl_args['service_url'] = client_confs[cluster]['service_url'].rstrip('/')
+  new_cl_args['deploy_mode'] = config.SERVER_MODE
+
+  cl_args.update(new_cl_args)
+  return cl_args
+
+################################################################################
+def direct_deployment_mode(command, parser, cluster, cl_args):
+  '''
+  check the direct deployment mode for the given cluster
+  if it is valid return the valid set of args
+  :param command:
+  :param parser:
+  :param cluster:
+  :param cl_args:
+  :return:
+  '''
+
+  cluster = cl_args['cluster']
+  try:
+    config_path = cl_args['config_path']
+    override_config_file = config.parse_override_config(cl_args['config_property'])
+  except KeyError:
+    # if some of the arguments are not found, print error and exit
+    subparser = config.get_subparser(parser, command)
+    print subparser.format_help()
+    return dict()
+
+  # check if the cluster config directory exists
+  if not cdefs.check_direct_mode_cluster_definition(cluster, config_path):
+    return dict()
+
+  config_path = config.get_heron_cluster_conf_dir(cluster, config_path)
+  if not os.path.isdir(config_path):
+    Log.error("Cluster config directory \'%s\' does not exist", config_path)
+    return dict()
+
+  Log.info("Using cluster definition in %s" % config_path)
+
+  try:
+    cluster_role_env = (cl_args['cluster'], cl_args['role'], cl_args['environ'])
+    config.direct_mode_cluster_role_env(cluster_role_env, config_path)
+    cluster_tuple = config.defaults_cluster_role_env(cluster_role_env)
+  except Exception as ex:
+    Log.error("Argument cluster/[role]/[env] is not correct: %s", str(ex))
+    return dict()
+
+  new_cl_args = dict()
+  new_cl_args['cluster'] = cluster_tuple[0]
+  new_cl_args['role'] = cluster_tuple[1]
+  new_cl_args['environ'] = cluster_tuple[2]
+  new_cl_args['config_path'] = config_path
+  new_cl_args['override_config_file'] = override_config_file
+  new_cl_args['deploy_mode'] = config.DIRECT_MODE
+
+  cl_args.update(new_cl_args)
+  return cl_args
+
+################################################################################
+def deployment_mode(command, parser, cl_args):
+  # first check if it is server mode
+  new_cl_args = server_deployment_mode(command, parser, cl_args['cluster'], cl_args)
+  if len(new_cl_args) > 0:
+    return new_cl_args
+
+  # now check if it is direct mode
+  new_cl_args = direct_deployment_mode(command, parser, cl_args['cluster'], cl_args)
+  if len(new_cl_args) > 0:
+    return new_cl_args
+
+  return dict()
+
 
 ################################################################################
 def extract_common_args(command, parser, cl_args):
@@ -156,37 +269,25 @@
   '''
   try:
     cluster_role_env = cl_args.pop('cluster/[role]/[env]')
-    config_path = cl_args['config_path']
-    override_config_file = config.parse_override_config(cl_args['config_property'])
   except KeyError:
-    # if some of the arguments are not found, print error and exit
-    subparser = config.get_subparser(parser, command)
-    print subparser.format_help()
-    return dict()
-
-  cluster = config.get_heron_cluster(cluster_role_env)
-  config_path = config.get_heron_cluster_conf_dir(cluster, config_path)
-  if not os.path.isdir(config_path):
-    Log.error("Config path cluster directory does not exist: %s", config_path)
-    return dict()
+    try:
+      cluster_role_env = cl_args.pop('cluster')  # for version command
+    except KeyError:
+      # if some of the arguments are not found, print error and exit
+      subparser = config.get_subparser(parser, command)
+      print subparser.format_help()
+      return dict()
 
   new_cl_args = dict()
-  try:
-    cluster_tuple = config.parse_cluster_role_env(cluster_role_env, config_path)
-    new_cl_args['cluster'] = cluster_tuple[0]
-    new_cl_args['role'] = cluster_tuple[1]
-    new_cl_args['environ'] = cluster_tuple[2]
-    new_cl_args['submit_user'] = getpass.getuser()
-    new_cl_args['config_path'] = config_path
-    new_cl_args['override_config_file'] = override_config_file
-  except Exception as ex:
-    Log.error("Argument cluster/[role]/[env] is not correct: %s", str(ex))
-    return dict()
+  cluster_tuple = config.get_cluster_role_env(cluster_role_env)
+  new_cl_args['cluster'] = cluster_tuple[0]
+  new_cl_args['role'] = cluster_tuple[1]
+  new_cl_args['environ'] = cluster_tuple[2]
+  new_cl_args['submit_user'] = getpass.getuser()
 
   cl_args.update(new_cl_args)
   return cl_args
 
-
 ################################################################################
 def main():
   '''
@@ -220,19 +321,29 @@
   # command to be execute
   command = command_line_args['subcommand']
 
+  if command == 'version':
+    results = run(command, parser, command_line_args, unknown_args)
+    return 0 if result.is_successful(results) else 1
+
   if command not in ('help', 'version'):
     log.set_logging_level(command_line_args)
+    Log.debug("Input Command Line Args: %s", command_line_args)
+
+    # determine the mode of deployment
     command_line_args = extract_common_args(command, parser, command_line_args)
+    command_line_args = deployment_mode(command, parser, command_line_args)
+
     # bail out if args are empty
     if not command_line_args:
       return 1
-    # register dirs cleanup function during exit
-    cleaned_up_files.append(command_line_args['override_config_file'])
 
-  atexit.register(cleanup, cleaned_up_files)
+    # register dirs cleanup function during exit
+    if command_line_args['deploy_mode'] == config.DIRECT_MODE and command != "version":
+      cleaned_up_files.append(command_line_args['override_config_file'])
+      atexit.register(cleanup, cleaned_up_files)
 
   # print the input parameters, if verbose is enabled
-  Log.debug(command_line_args)
+  Log.debug("Processed Command Line Args: %s", command_line_args)
 
   start = time.time()
   results = run(command, parser, command_line_args, unknown_args)
@@ -242,7 +353,7 @@
 
   if command not in ('help', 'version'):
     sys.stdout.flush()
-    Log.debug('Elapsed time: %.3fs.', (end - start))
+    Log.info('Elapsed time: %.3fs.', (end - start))
 
   return 0 if result.is_successful(results) else 1
 
diff --git a/heron/tools/cli/src/python/rest.py b/heron/tools/cli/src/python/rest.py
new file mode 100644
index 0000000..6b878b1
--- /dev/null
+++ b/heron/tools/cli/src/python/rest.py
@@ -0,0 +1,25 @@
+# Copyright 2016 Twitter. All rights reserved.
+#
+# 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.
+''' rest.py '''
+import requests
+
+ROUTE_SIGNATURES = {
+    'activate': (requests.post, '/api/v1/topologies/%s/%s/%s/%s/activate', []),
+    'deactivate': (requests.post, '/api/v1/topologies/%s/%s/%s/%s/deactivate', []),
+    'kill': (requests.delete, '/api/v1/topologies/%s/%s/%s/%s', []),
+    'restart': (requests.post, '/api/v1/topologies/%s/%s/%s/%s/restart', []),
+    'submit': (requests.post, '/api/v1/topologies', ['name', 'cluster', 'role', 'env', 'user']),
+    'update': (requests.post, '/api/v1/topologies/%s/%s/%s/%s/update', []),
+    'version': (requests.get, '/api/v1/version', []),
+}
diff --git a/heron/tools/cli/src/python/restart.py b/heron/tools/cli/src/python/restart.py
index bc77ec2..ec2575a 100644
--- a/heron/tools/cli/src/python/restart.py
+++ b/heron/tools/cli/src/python/restart.py
@@ -12,9 +12,10 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 ''' restart.py '''
+from heron.common.src.python.utils.log import Log
 import heron.tools.cli.src.python.args as args
 import heron.tools.cli.src.python.cli_helper as cli_helper
-
+import heron.tools.common.src.python.utils.config as config
 
 def create_parser(subparsers):
   '''
@@ -39,6 +40,7 @@
       help='Identifier of the container to be restarted')
 
   args.add_config(parser)
+  args.add_service_url(parser)
   args.add_verbose(parser)
 
   parser.set_defaults(subcommand='restart')
@@ -54,7 +56,12 @@
   :param unknown_args:
   :return:
   '''
+  Log.debug("Restart Args: %s", cl_args)
   container_id = cl_args['container-id']
-  extra_args = ["--container_id", str(container_id)]
 
-  return cli_helper.run(command, cl_args, "restart topology", extra_args=extra_args)
+  if cl_args['deploy_mode'] == config.SERVER_MODE:
+    dict_extra_args = {"container_id": str(container_id)}
+    return cli_helper.run_server(command, cl_args, "restart topology", extra_args=dict_extra_args)
+  else:
+    list_extra_args = ["--container_id", str(container_id)]
+    return cli_helper.run_direct(command, cl_args, "restart topology", extra_args=list_extra_args)
diff --git a/heron/tools/cli/src/python/submit.py b/heron/tools/cli/src/python/submit.py
index 834b201..b0ca038 100644
--- a/heron/tools/cli/src/python/submit.py
+++ b/heron/tools/cli/src/python/submit.py
@@ -16,6 +16,7 @@
 import logging
 import os
 import tempfile
+import requests
 
 from heron.common.src.python.utils.log import Log
 from heron.proto import topology_pb2
@@ -25,12 +26,24 @@
 import heron.tools.cli.src.python.jars as jars
 import heron.tools.cli.src.python.opts as opts
 import heron.tools.cli.src.python.result as result
+import heron.tools.cli.src.python.rest as rest
 import heron.tools.common.src.python.utils.config as config
 import heron.tools.common.src.python.utils.classpath as classpath
 
 # pylint: disable=too-many-return-statements
 
 ################################################################################
+def launch_mode_msg(cl_args):
+  '''
+  Depending on the mode of launching a topology provide a message
+  :param cl_args:
+  :return:
+  '''
+  if cl_args['dry_run']:
+    return "in dry-run mode"
+  return ""
+
+################################################################################
 def create_parser(subparsers):
   '''
   Create a subparser for the submit command
@@ -51,9 +64,10 @@
   cli_args.add_topology_class(parser)
   cli_args.add_config(parser)
   cli_args.add_deactive_deploy(parser)
-  cli_args.add_extra_launch_classpath(parser)
-  cli_args.add_system_property(parser)
   cli_args.add_dry_run(parser)
+  cli_args.add_extra_launch_classpath(parser)
+  cli_args.add_service_url(parser)
+  cli_args.add_system_property(parser)
   cli_args.add_verbose(parser)
 
   parser.set_defaults(subcommand='submit')
@@ -68,6 +82,7 @@
   :param tmp_dir:
   :param topology_file:
   :param topology_defn_file:
+  :param topology_name:
   :return:
   '''
   # get the normalized path for topology.tar.gz
@@ -119,16 +134,61 @@
       extra_jars=extra_jars,
       args=args,
       java_defines=[])
-  err_context = "Failed to launch topology '%s'" % topology_name
-  if cl_args["dry_run"]:
-    err_context += " in dry-run mode"
-  succ_context = "Successfully launched topology '%s'" % topology_name
-  if cl_args["dry_run"]:
-    succ_context += " in dry-run mode"
-  res.add_context(err_context, succ_context)
+
+  err_ctxt = "Failed to launch topology '%s' %s" % (topology_name, launch_mode_msg(cl_args))
+  succ_ctxt = "Successfully launched topology '%s' %s" % (topology_name, launch_mode_msg(cl_args))
+
+  res.add_context(err_ctxt, succ_ctxt)
   return res
 
 ################################################################################
+def launch_topology_server(cl_args, topology_file, topology_defn_file, topology_name):
+  '''
+  Launch a topology given topology jar, its definition file and configurations
+  :param cl_args:
+  :param topology_file:
+  :param topology_defn_file:
+  :param topology_name:
+  :return:
+  '''
+  service_apiurl = cl_args['service_url'] + rest.ROUTE_SIGNATURES['submit'][1]
+  service_method = rest.ROUTE_SIGNATURES['submit'][0]
+  data = dict(
+      name=topology_name,
+      cluster=cl_args['cluster'],
+      role=cl_args['role'],
+      environment=cl_args['environ'],
+      user=cl_args['submit_user'],
+  )
+
+  if cl_args['dry_run']:
+    data["dry_run"] = True
+
+  files = dict(
+      definition=open(topology_defn_file, 'rb'),
+      topology=open(topology_file, 'rb'),
+  )
+
+  err_ctxt = "Failed to launch topology '%s' %s" % (topology_name, launch_mode_msg(cl_args))
+  succ_ctxt = "Successfully launched topology '%s' %s" % (topology_name, launch_mode_msg(cl_args))
+
+  try:
+    r = service_method(service_apiurl, data=data, files=files)
+    ok = r.status_code is requests.codes.ok
+    created = r.status_code is requests.codes.created
+    s = Status.Ok if created or ok else Status.HeronError
+    if s is Status.HeronError:
+      Log.error(r.json().get('message', "Unknown error from api server %d" % r.status_code))
+    elif ok:
+      # this case happens when we request a dry_run
+      print r.json().get("response")
+  except (requests.exceptions.ConnectionError, requests.exceptions.HTTPError) as err:
+    Log.error(err)
+    return SimpleResult(Status.HeronError, err_ctxt, succ_ctxt)
+  return SimpleResult(s, err_ctxt, succ_ctxt)
+
+
+################################################################################
 def launch_topologies(cl_args, topology_file, tmp_dir):
   '''
   Launch topologies
@@ -154,12 +214,19 @@
     except Exception as e:
       err_context = "Cannot load topology definition '%s': %s" % (defn_file, e)
       return SimpleResult(Status.HeronError, err_context)
+
     # launch the topology
-    mode = " in dry-run mode" if cl_args['dry_run'] else ''
-    Log.info("Launching topology: \'%s\'%s", topology_defn.name, mode)
-    res = launch_a_topology(
-        cl_args, tmp_dir, topology_file, defn_file, topology_defn.name)
+    Log.info("Launching topology: \'%s\'%s", topology_defn.name, launch_mode_msg(cl_args))
+
+    # check if we have to do server or direct based deployment
+    if cl_args['deploy_mode'] == config.SERVER_MODE:
+      res = launch_topology_server(
+          cl_args, topology_file, defn_file, topology_defn.name)
+    else:
+      res = launch_a_topology(
+          cl_args, tmp_dir, topology_file, defn_file, topology_defn.name)
     results.append(res)
+
   return results
 
 
@@ -283,6 +350,8 @@
   :param unknown_args:
   :return:
   '''
+  Log.debug("Submit Args %s", cl_args)
+
   # get the topology file name
   topology_file = cl_args['topology-file-name']
 
diff --git a/heron/tools/cli/src/python/update.py b/heron/tools/cli/src/python/update.py
index 4a914e4..202f13a 100644
--- a/heron/tools/cli/src/python/update.py
+++ b/heron/tools/cli/src/python/update.py
@@ -12,9 +12,11 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 ''' restart.py '''
+from heron.common.src.python.utils.log import Log
 import heron.tools.cli.src.python.args as args
 import heron.tools.cli.src.python.cli_helper as cli_helper
 import heron.tools.cli.src.python.jars as jars
+import heron.tools.common.src.python.utils.config as config
 
 import argparse
 import re
@@ -49,6 +51,7 @@
 
   args.add_config(parser)
   args.add_dry_run(parser)
+  args.add_service_url(parser)
   args.add_verbose(parser)
 
   parser.set_defaults(subcommand='update')
@@ -58,12 +61,24 @@
 # pylint: disable=unused-argument
 def run(command, parser, cl_args, unknown_args):
   """ run the update command """
-  extra_args = ["--component_parallelism", ','.join(cl_args['component_parallelism'])]
+
+  Log.debug("Update Args: %s", cl_args)
   extra_lib_jars = jars.packing_jars()
   action = "update topology%s" % (' in dry-run mode' if cl_args["dry_run"] else '')
-  if cl_args["dry_run"]:
-    extra_args.append('--dry_run')
-    if "dry_run_format" in cl_args:
-      extra_args += ["--dry_run_format", cl_args["dry_run_format"]]
 
-  return cli_helper.run(command, cl_args, action, extra_args, extra_lib_jars)
+  if cl_args['deploy_mode'] == config.SERVER_MODE:
+    dict_extra_args = {"component_parallelism": cl_args['component_parallelism']}
+    if cl_args["dry_run"]:
+      dict_extra_args.update({'dry_run': True})
+      if "dry_run_format" in cl_args:
+        dict_extra_args.update({"dry_run_format", cl_args["dry_run_format"]})
+
+    return cli_helper.run_server(command, cl_args, action, dict_extra_args)
+  else:
+    list_extra_args = ["--component_parallelism", ','.join(cl_args['component_parallelism'])]
+    if cl_args["dry_run"]:
+      list_extra_args.append('--dry_run')
+      if "dry_run_format" in cl_args:
+        list_extra_args += ["--dry_run_format", cl_args["dry_run_format"]]
+
+    return cli_helper.run_direct(command, cl_args, action, list_extra_args, extra_lib_jars)
diff --git a/heron/tools/cli/src/python/version.py b/heron/tools/cli/src/python/version.py
index 8cf487d..d9b6607 100644
--- a/heron/tools/cli/src/python/version.py
+++ b/heron/tools/cli/src/python/version.py
@@ -12,10 +12,25 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 ''' version.py '''
+from heron.common.src.python.utils.log import Log
 from heron.tools.cli.src.python.result import SimpleResult, Status
 import heron.tools.cli.src.python.args as cli_args
 import heron.tools.common.src.python.utils.config as config
+import heron.tools.cli.src.python.cdefs as cdefs
+import heron.tools.cli.src.python.rest as rest
 
+import sys
+import requests
+
+def add_version_titles(parser):
+  '''
+  :param parser:
+  :return:
+  '''
+  # pylint: disable=protected-access
+  parser._positionals.title = "Optional positional arguments"
+  parser._optionals.title = "Optional arguments"
+  return parser
 
 def create_parser(subparsers):
   '''
@@ -25,17 +40,25 @@
   parser = subparsers.add_parser(
       'version',
       help='Print version of heron-cli',
-      usage="%(prog)s",
+      usage="%(prog)s [options] [cluster]",
       add_help=True)
 
-  cli_args.add_titles(parser)
+  add_version_titles(parser)
+
+  parser.add_argument(
+      'cluster',
+      nargs='?',
+      type=str,
+      default="",
+      help='Name of the cluster')
+
+  cli_args.add_service_url(parser)
 
   parser.set_defaults(subcommand='version')
   return parser
 
-
 # pylint: disable=unused-argument
-def run(command, parser, args, unknown_args):
+def run(command, parser, cl_args, unknown_args):
   '''
   :param command:
   :param parser:
@@ -43,5 +66,40 @@
   :param unknown_args:
   :return:
   '''
-  config.print_build_info()
+  cluster = cl_args['cluster']
+
+  # server mode
+  if cluster:
+    config_file = config.heron_rc_file()
+    client_confs = dict()
+
+    # Read the cluster definition, if not found
+    client_confs = cdefs.read_server_mode_cluster_definition(cluster, cl_args, config_file)
+
+    if not client_confs[cluster]:
+      Log.error('Neither service url nor %s cluster definition in %s file', cluster, config_file)
+      return SimpleResult(Status.HeronError)
+
+    # if cluster definition exists, but service_url is not set, it is an error
+    if not 'service_url' in client_confs[cluster]:
+      Log.error('No service url for %s cluster in %s', cluster, config_file)
+      sys.exit(1)
+
+    service_endpoint = cl_args['service_url']
+    service_apiurl = service_endpoint + rest.ROUTE_SIGNATURES[command][1]
+    service_method = rest.ROUTE_SIGNATURES[command][0]
+
+    try:
+      r = service_method(service_apiurl)
+      if r.status_code != requests.codes.ok:
+        Log.error(r.json().get('message', "Unknown error from api server %d" % r.status_code))
+      sorted_items = sorted(r.json().items(), key=lambda tup: tup[0])
+      for key, value in sorted_items:
+        print "%s : %s" % (key, value)
+    except (requests.exceptions.ConnectionError, requests.exceptions.HTTPError) as err:
+      Log.error(err)
+      return SimpleResult(Status.HeronError)
+  else:
+    config.print_build_info()
+
   return SimpleResult(Status.Ok)
diff --git a/heron/tools/cli/tests/python/client_command_unittest.py b/heron/tools/cli/tests/python/client_command_unittest.py
index 7859970..c747492 100644
--- a/heron/tools/cli/tests/python/client_command_unittest.py
+++ b/heron/tools/cli/tests/python/client_command_unittest.py
@@ -21,6 +21,7 @@
 import sys
 import tempfile
 import heron.tools.cli.src.python.main as main
+import heron.tools.cli.src.python.cdefs as cdefs
 import heron.tools.cli.src.python.submit as submit
 import heron.tools.cli.src.python.result as result
 import heron.tools.common.src.python.utils.config as config
@@ -51,6 +52,8 @@
     os.path.isdir = MagicMock(return_value=True)
     os.path.isfile = MagicMock(return_value=True)
     os.environ.copy = MagicMock(return_value={})
+    main.server_deployment_mode = MagicMock(return_value=dict())
+    cdefs.check_direct_mode_cluster_definition = MagicMock(return_value=True)
 
   def run_test(self, command, issued_commands, environ):
     calls = []
diff --git a/heron/tools/common/src/python/utils/config.py b/heron/tools/common/src/python/utils/config.py
index 9f827de..12e0de7 100644
--- a/heron/tools/common/src/python/utils/config.py
+++ b/heron/tools/common/src/python/utils/config.py
@@ -40,16 +40,23 @@
 ZIPPED_RELEASE_YAML = "scripts/packages/release.yaml"
 OVERRIDE_YAML = "override.yaml"
 
+# mode of deployment
+DIRECT_MODE = 'direct'
+SERVER_MODE = 'server'
+
 # directories for heron sandbox
 SANDBOX_CONF_DIR = "./heron-conf"
 
 # config file for heron cli
 CLIENT_YAML = "client.yaml"
 
-# cli configs for role and env
-IS_ROLE_REQUIRED = "heron.config.is.role.required"
-IS_ENV_REQUIRED = "heron.config.is.env.required"
+# client configs for role and env for direct deployment
+ROLE_REQUIRED = "heron.config.is.role.required"
+ENV_REQUIRED = "heron.config.is.env.required"
 
+# client config for role and env for server deployment
+ROLE_KEY = "role.required"
+ENVIRON_KEY = "env.required"
 
 def create_tar(tar_filename, files, config_dir, config_files):
   '''
@@ -240,12 +247,15 @@
   """Get the cluster to which topology is submitted"""
   return cluster_role_env.split('/')[0]
 
+def heron_rc_file():
+  """Get the full path name of the .heronrc file"""
+  return os.path.join(os.path.expanduser('~'), '.heronrc')
 
+################################################################################
 # pylint: disable=too-many-branches
 def parse_cluster_role_env(cluster_role_env, config_path):
   """Parse cluster/[role]/[environ], supply default, if not provided, not required"""
   parts = cluster_role_env.split('/')[:3]
-  Log.debug("Using config file under %s" % config_path)
   if not os.path.isdir(config_path):
     Log.error("Config path cluster directory does not exist: %s" % config_path)
     raise Exception("Invalid config path")
@@ -273,17 +283,17 @@
 
       # if role is required but not provided, raise exception
       if len(parts) == 1:
-        if (IS_ROLE_REQUIRED in cli_confs) and (cli_confs[IS_ROLE_REQUIRED] is True):
+        if (ROLE_REQUIRED in cli_confs) and (cli_confs[ROLE_REQUIRED] is True):
           raise Exception("role required but not provided (cluster/role/env = %s). See %s in %s"
-                          % (cluster_role_env, IS_ROLE_REQUIRED, CLIENT_YAML))
+                          % (cluster_role_env, ROLE_REQUIRED, cli_conf_file))
         else:
           parts.append(getpass.getuser())
 
       # if environ is required but not provided, raise exception
       if len(parts) == 2:
-        if (IS_ENV_REQUIRED in cli_confs) and (cli_confs[IS_ENV_REQUIRED] is True):
+        if (ENV_REQUIRED in cli_confs) and (cli_confs[ENV_REQUIRED] is True):
           raise Exception("environ required but not provided (cluster/role/env = %s). See %s in %s"
-                          % (cluster_role_env, IS_ENV_REQUIRED, CLIENT_YAML))
+                          % (cluster_role_env, ENV_REQUIRED, cli_conf_file))
         else:
           parts.append(ENVIRON)
 
@@ -294,6 +304,84 @@
 
   return (parts[0], parts[1], parts[2])
 
+################################################################################
+def get_cluster_role_env(cluster_role_env):
+  """Parse cluster/[role]/[environ], supply empty string, if not provided"""
+  parts = cluster_role_env.split('/')[:3]
+  if len(parts) == 3:
+    return (parts[0], parts[1], parts[2])
+
+  if len(parts) == 2:
+    return (parts[0], parts[1], "")
+
+  if len(parts) == 1:
+    return (parts[0], "", "")
+
+  return ("", "", "")
+
+################################################################################
+def direct_mode_cluster_role_env(cluster_role_env, config_path):
+  """Check cluster/[role]/[environ], if they are required"""
+
+  # otherwise, get the client.yaml file
+  cli_conf_file = os.path.join(config_path, CLIENT_YAML)
+
+  # if client conf doesn't exist, use default value
+  if not os.path.isfile(cli_conf_file):
+    return True
+
+  client_confs = {}
+  with open(cli_conf_file, 'r') as conf_file:
+    client_confs = yaml.load(conf_file)
+
+    # the return value of yaml.load can be None if conf_file is an empty file
+    if not client_confs:
+      return True
+
+    # if role is required but not provided, raise exception
+    role_present = True if len(cluster_role_env[1]) > 0 else False
+    if ROLE_REQUIRED in client_confs and client_confs[ROLE_REQUIRED] and not role_present:
+      raise Exception("role required but not provided (cluster/role/env = %s). See %s in %s"
+                      % (cluster_role_env, ROLE_REQUIRED, cli_conf_file))
+
+    # if environ is required but not provided, raise exception
+    environ_present = True if len(cluster_role_env[2]) > 0 else False
+    if ENV_REQUIRED in client_confs and client_confs[ENV_REQUIRED] and not environ_present:
+      raise Exception("environ required but not provided (cluster/role/env = %s). See %s in %s"
+                      % (cluster_role_env, ENV_REQUIRED, cli_conf_file))
+
+  return True
+
+################################################################################
+def server_mode_cluster_role_env(cluster_role_env, config_map, config_file):
+  """Check cluster/[role]/[environ], if they are required"""
+
+  cmap = config_map[cluster_role_env[0]]
+
+  # if role is required but not provided, raise exception
+  role_present = True if len(cluster_role_env[1]) > 0 else False
+  if ROLE_KEY in cmap and cmap[ROLE_KEY] and not role_present:
+    raise Exception("role required but not provided (cluster/role/env = %s). See %s in %s"
+                    % (cluster_role_env, ROLE_KEY, config_file))
+
+  # if environ is required but not provided, raise exception
+  environ_present = True if len(cluster_role_env[2]) > 0 else False
+  if ENVIRON_KEY in cmap and cmap[ENVIRON_KEY] and not environ_present:
+    raise Exception("environ required but not provided (cluster/role/env = %s). See %s in %s"
+                    % (cluster_role_env, ENVIRON_KEY, config_file))
+
+  return True
+
+################################################################################
+def defaults_cluster_role_env(cluster_role_env):
+  """
+  if role is not provided, supply userid
+  if environ is not provided, supply 'default'
+  """
+  if len(cluster_role_env[1]) == 0 and len(cluster_role_env[2]) == 0:
+    return (cluster_role_env[0], getpass.getuser(), ENVIRON)
+
+  return (cluster_role_env[0], cluster_role_env[1], ENVIRON)
 
 ################################################################################
 # Parse the command line for overriding the defaults
@@ -354,9 +442,12 @@
     release_file = get_zipped_heron_release_file()
   else:
     release_file = get_heron_release_file()
+
   with open(release_file) as release_info:
-    for line in release_info:
-      print line,
+    release_map = yaml.load(release_info)
+    release_items = sorted(release_map.items(), key=lambda tup: tup[0])
+    for key, value in release_items:
+      print "%s : %s" % (key, value)
 
 def get_version_number(zipped_pex=False):
   """Print version from release.yaml
diff --git a/scripts/get_all_heron_paths.sh b/scripts/get_all_heron_paths.sh
index 2e7f689..20679e9 100755
--- a/scripts/get_all_heron_paths.sh
+++ b/scripts/get_all_heron_paths.sh
@@ -67,7 +67,7 @@
 }
 
 function get_heron_java_paths() {
-  local java_paths=$(find {heron,tools,integration_test,contrib} -name "*.java" | sed "s|/src/java/.*$|/src/java|"| sed "s|/java/src/.*$|/java/src|" |  sed "s|/tests/java/.*$|/tests/java|" | sort -u | fgrep -v "heron/scheduler/" | fgrep -v "heron/scheduler/" )
+  local java_paths=$(find {heron,heron/tools,tools,integration_test,contrib} -name "*.java" | sed "s|/src/java/.*$|/src/java|"| sed "s|/java/src/.*$|/java/src|" |  sed "s|/tests/java/.*$|/tests/java|" | sort -u | fgrep -v "heron/scheduler/" | fgrep -v "heron/scheduler/" )
   if [ "$(uname -s | tr 'A-Z' 'a-z')" != "darwin" ]; then
     java_paths=$(echo "${java_paths}" | fgrep -v "/objc_tools/")
   fi
diff --git a/scripts/packages/BUILD b/scripts/packages/BUILD
index 2af67f9..5a7e76f 100644
--- a/scripts/packages/BUILD
+++ b/scripts/packages/BUILD
@@ -161,9 +161,24 @@
 )
 
 pkg_tar(
+    name = "heron-tools-lib",
+    package_dir = "lib",
+    files = heron_tools_lib_files(),
+)
+
+pkg_tar(
     name = "heron-tools-conf",
     package_dir = "conf",
-    files = heron_tools_conf_files(),
+    files = heron_tools_conf_files()
+)
+
+pkg_tar(
+    name = "heron-tools-cluster-conf",
+    strip_prefix = "/heron/config/src/yaml/conf",
+    package_dir = "conf",
+    files = [
+      "//heron/config/src/yaml:conf-yaml"
+    ], 
 )
 
 pkg_tar(
@@ -172,7 +187,9 @@
     files = generated_release_files,
     deps = [
         ":heron-tools-bin",
+        ":heron-tools-lib",
         ":heron-tools-conf",
+        ":heron-tools-cluster-conf",
     ],
 )
 
@@ -228,65 +245,12 @@
 )
 
 pkg_tar(
-    name = "heron-client-conf-local",
-    package_dir = "conf/local",
-    files = [
-        "//heron/config/src/yaml:conf-local-yaml",
-    ],
-)
-
-pkg_tar(
-    name = "heron-client-conf-slurm",
-    package_dir = "conf/slurm",
-    files = [
-        "//heron/config/src/yaml:conf-slurm-yaml",
-    ],
-)
-
-pkg_tar(
-    name = "heron-client-conf-aurora",
-    package_dir = "conf/aurora",
-    files = [
-        "//heron/config/src/yaml:conf-aurora-yaml",
-    ],
-)
-
-pkg_tar(
-    name = "heron-client-conf-yarn",
-    package_dir = "conf/yarn",
-    files = [
-        "//heron/config/src/yaml:conf-yarn-yaml",
-    ],
-)
-
-pkg_tar(
-     name = "heron-client-conf-mesos",
-     package_dir = "conf/mesos",
-     files = [
-         "//heron/config/src/yaml:conf-mesos-yaml",
-     ],
-)
-
-pkg_tar(
-    name = "heron-client-conf-marathon",
-    package_dir = "conf/marathon",
-    files = [
-        "//heron/config/src/yaml:conf-marathon-yaml",
-    ]
-)
-
-pkg_tar(
-    name = "heron-client-conf-kubernetes",
-    package_dir = "conf/kubernetes",
-    files = [
-        "//heron/config/src/yaml:conf-kubernetes-yaml",
-    ]
-)
-
-pkg_tar(
     name = "heron-client-conf",
+    strip_prefix = "/heron/config/src/yaml/conf",
     package_dir = "conf",
-    files = heron_client_conf_files(),
+    files = [
+        "//heron/config/src/yaml:conf-yaml",
+    ]
 )
 
 pkg_tar(
@@ -302,13 +266,6 @@
     deps = [
         ":heron-client-bin",
         ":heron-client-conf",
-        ":heron-client-conf-local",
-        ":heron-client-conf-aurora",
-        ":heron-client-conf-slurm",
-        ":heron-client-conf-yarn",
-        ":heron-client-conf-mesos",
-        ":heron-client-conf-marathon",
-        ":heron-client-conf-kubernetes",
         ":heron-client-dist",
         ":heron-client-examples",
         ":heron-client-lib-third_party",
diff --git a/scripts/packages/client_template_bin.sh b/scripts/packages/client_template_bin.sh
index 0235092..12c2764 100755
--- a/scripts/packages/client_template_bin.sh
+++ b/scripts/packages/client_template_bin.sh
@@ -19,7 +19,6 @@
 
 # Installation and etc prefix can be overriden from command line
 install_prefix=${1:-"/usr/local/heron"}
-heronrc=${2:-"/usr/local/heron/etc/heron.heronrc"}
 
 progname="$0"
 
@@ -31,9 +30,8 @@
   echo "Usage: $progname [options]" >&2
   echo "Options are:" >&2
   echo "  --prefix=/some/path set the prefix path (default=/usr/local)." >&2
-  echo "  --heronrc= set the heronrc path (default=/usr/local/heron/etc/heron.heronrc)." >&2
   echo "  --user configure for user install, expands to" >&2
-  echo '           `--prefix=$HOME/.heron --heronrc=$HOME/.heronrc`.' >&2
+  echo '           `--prefix=$HOME/.heron`.' >&2
   exit 1
 }
 
@@ -41,20 +39,15 @@
 bin="%prefix%/bin"
 base="%prefix%/heron"
 conf="%prefix%/heron/conf"
-heronrc="%prefix%/heron/etc/heron.heronrc"
 
 for opt in "${@}"; do
   case $opt in
     --prefix=*)
       prefix="$(echo "$opt" | cut -d '=' -f 2-)"
       ;;
-    --heronrc=*)
-      heronrc="$(echo "$opt" | cut -d '=' -f 2-)"
-      ;;
     --user)
       bin="$HOME/bin"
       base="$HOME/.heron"
-      heronrc="$HOME/.heronrc"
       ;;
     *)
       usage
@@ -64,14 +57,12 @@
 
 bin="${bin//%prefix%/${prefix}}"
 base="${base//%prefix%/${prefix}}"
-heronrc="${heronrc//%prefix%/${prefix}}"
 
 check_unzip; check_tar; check_java
 
 # Test for write access
 test_write "${bin}"
 test_write "${base}"
-test_write "${heronrc}"
 
 # Do the actual installation
 echo -n "Uncompressing."
@@ -111,16 +102,6 @@
 ln -s "${base}/bin/heron-explorer" "${bin}/heron-explorer"
 echo -n .
 
-if [ -f "${heronrc}" ]; then
-  echo
-  echo "${heronrc} already exists, not modifying it"
-else
-  touch "${heronrc}"
-  if [ "${UID}" -eq 0 ]; then
-    chmod 0644 "${heronrc}"
-  fi
-fi
-
 rm "${base}/heron-client.tar.gz"
 
 cat <<EOF
diff --git a/scripts/packages/tools_template_bin.sh b/scripts/packages/tools_template_bin.sh
index 4b050bf..4b48efc 100755
--- a/scripts/packages/tools_template_bin.sh
+++ b/scripts/packages/tools_template_bin.sh
@@ -73,6 +73,9 @@
 if [ -L "${bin}/heron-ui" ]; then
   rm -f "${bin}/heron-ui"
 fi
+if [ -L "${bin}/heron-apiserver" ]; then
+  rm -f "${bin}/heron-apiserver"
+fi
 if [ -d "${base}" -a -x "${base}/bin/heron-tracker" ]; then
   rm -fr "${base}"
 fi
@@ -84,14 +87,16 @@
 untar ${base}/heron-tools.tar.gz ${base}
 echo -n .
 chmod 0755 ${base}/bin/heron-tracker ${base}/bin/heron-ui
+chmod 0755 ${base}/bin/heron-apiserver
 echo -n .
 chmod -R og-w "${base}"
 chmod -R og+rX "${base}"
 chmod -R u+rwX "${base}"
 echo -n .
 
-ln -s "${base}/bin/heron-tracker" "${bin}/heron-tracker"
-ln -s "${base}/bin/heron-ui"      "${bin}/heron-ui"
+ln -s "${base}/bin/heron-tracker"   "${bin}/heron-tracker"
+ln -s "${base}/bin/heron-ui"        "${bin}/heron-ui"
+ln -s "${base}/bin/heron-apiserver" "${bin}/heron-apiserver"
 echo -n .
 
 rm "${base}/heron-tools.tar.gz"
diff --git a/third_party/java/BUILD b/third_party/java/BUILD
index 7efd80e..752ee35 100644
--- a/third_party/java/BUILD
+++ b/third_party/java/BUILD
@@ -230,3 +230,79 @@
         "@apache_pulsar_client//jar",
     ],
 )
+
+java_library(
+  name = "commons-compress",
+  srcs = [ "Empty.java" ],
+  exports = [
+      "@org_apache_commons_compress//jar",
+  ],
+  deps = [
+      "@org_apache_commons_compress//jar",
+  ]
+)
+
+java_library(
+    name = "jetty-jersey-java",
+    srcs = [ "Empty.java" ],
+    exports = [ 
+        "@org_eclipse_jetty_server//jar",
+        "@org_eclipse_jetty_http//jar",
+        "@org_eclipse_jetty_util//jar",
+        "@org_eclipse_jetty_io//jar",
+        "@org_eclipse_jetty_security//jar",
+        "@org_eclipse_jetty_continuation//jar",
+        "@org_eclipse_jetty_servlet//jar",
+        "@org_eclipse_jetty_servlets//jar",
+        "@javax_servlet_api//jar",
+        "@jersey_container_servlet_core//jar",
+        "@jersey_container_servlet//jar",
+        "@jersey_server//jar",
+        "@jersey_client//jar",
+        "@jersey_common//jar",
+        "@jersey_guava//jar",
+        "@jersey_media_multipart//jar",
+        "@jersey_media_jaxb//jar",
+        "@javax_inject//jar",
+        "@javax_annotation//jar",
+        "@javax_validation//jar",
+        "@javax_ws_rs_api//jar",
+        "@hk2_api//jar",
+        "@hk2_utils//jar",
+        "@hk2_locator//jar",
+        "@hk2_aopalliance_repackaged//jar",
+        "@hk2_osgi_resource_locator//jar",
+        "@org_javassit//jar",
+        "@mimepull//jar",
+    ],
+    deps = [ 
+        "@org_eclipse_jetty_server//jar",
+        "@org_eclipse_jetty_http//jar",
+        "@org_eclipse_jetty_util//jar",
+        "@org_eclipse_jetty_io//jar",
+        "@org_eclipse_jetty_security//jar",
+        "@org_eclipse_jetty_continuation//jar",
+        "@org_eclipse_jetty_servlet//jar",
+        "@org_eclipse_jetty_servlets//jar",
+        "@javax_servlet_api//jar",
+        "@jersey_container_servlet_core//jar",
+        "@jersey_container_servlet//jar",
+        "@jersey_server//jar",
+        "@jersey_client//jar",
+        "@jersey_common//jar",
+        "@jersey_guava//jar",
+        "@jersey_media_multipart//jar",
+        "@jersey_media_jaxb//jar",
+        "@javax_inject//jar",
+        "@javax_annotation//jar",
+        "@javax_validation//jar",
+        "@javax_ws_rs_api//jar",
+        "@hk2_api//jar",
+        "@hk2_utils//jar",
+        "@hk2_locator//jar",
+        "@hk2_aopalliance_repackaged//jar",
+        "@hk2_osgi_resource_locator//jar",
+        "@org_javassit//jar",
+        "@mimepull//jar",
+    ],   
+)
diff --git a/tools/rules/heron_tools.bzl b/tools/rules/heron_tools.bzl
index dfb79aa..dbeae82 100644
--- a/tools/rules/heron_tools.bzl
+++ b/tools/rules/heron_tools.bzl
@@ -8,12 +8,15 @@
     return [
         "//heron/tools/tracker/src/python:heron-tracker",
         "//heron/tools/ui/src/python:heron-ui",
+        "//heron/tools/apiserver/src/shell:heron-apiserver"
+    ]
+
+def heron_tools_lib_files():
+    return [
+        "//heron/tools/apiserver/src/java:heron-apiserver",
     ]
 
 def heron_tools_conf_files():
     return [
         "//heron/tools/config/src/yaml:tracker-yaml",
     ]
-
-def heron_tools_lib_files():
-    return []