You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@openwhisk.apache.org by GitBox <gi...@apache.org> on 2017/12/05 06:16:06 UTC

[GitHub] ServoKvd closed pull request #265: Move incubator-openwhisk/core/routemgmt to incubator-openwhisk-apigateway /routemgmt

ServoKvd closed pull request #265: Move incubator-openwhisk/core/routemgmt to incubator-openwhisk-apigateway /routemgmt 
URL: https://github.com/apache/incubator-openwhisk-apigateway/pull/265
 
 
   

This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:

As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):

diff --git a/routemgmt/common/apigw-utils.js b/routemgmt/common/apigw-utils.js
new file mode 100644
index 0000000..c78f4c8
--- /dev/null
+++ b/routemgmt/common/apigw-utils.js
@@ -0,0 +1,896 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Route management action common API GW utilities
+ */
+var request = require('request');
+var _ = require('lodash');
+
+const ApimgmtUserAgent = "OpenWhisk-apimgmt/1.0.0";
+var UserAgent = ApimgmtUserAgent;
+
+/**
+ * Configures an API route on the API Gateway.  This API will map to an OpenWhisk action that
+ * will be invoked by the API Gateway when the API route is accessed.
+ *
+ * @param gwInfo Required.
+ * @param    gwUrl     Required. The base URL gateway path (i.e.  'PROTOCOL://gw.host.domain:PORT/CONTEXT')
+ * @param    gwAuth    Required. The user bearer token used to access the API Gateway REST endpoints
+ * @param spaceGuid    Required. User's space guid.  APIs are stored under this context
+ * @param swaggerApi   Required. The API swagger object to send to the API gateway
+ * @param apiId        Required. API id. When specified, the API exists and will be updated; otherwise the API is created anew
+ * @return A promise for an object describing the result with fields error and response
+ */
+function addApiToGateway(gwInfo, spaceGuid, swaggerApi, apiId) {
+  var requestFcn = request.post;
+
+  console.log('addApiToGateway: ');
+  try {
+  var options = {
+    followAllRedirects: true,
+    url: gwInfo.gwUrl+'/'+encodeURIComponent(spaceGuid) + '/apis',
+    json: swaggerApi,  // Use of json automatically sets header: 'Content-Type': 'application/json'
+    headers: {
+      'User-Agent': UserAgent
+    }
+  };
+  if (gwInfo.gwAuth) {
+    _.set(options, "headers.Authorization", 'Bearer ' + gwInfo.gwAuth);
+  }
+
+  if (apiId) {
+    console.log("addApiToGateway: Updating existing API");
+    options.url = gwInfo.gwUrl + '/' + encodeURIComponent(spaceGuid) + '/apis/' + encodeURIComponent(apiId);
+    requestFcn = request.put;
+  }
+
+  console.log('addApiToGateway: request: '+JSON.stringify(options, " ", 2));
+  }
+  catch (e) {
+    console.error('addApiToGateway exception: '+e);
+  }
+  return new Promise(function(resolve, reject) {
+    requestFcn(options, function(error, response, body) {
+      var statusCode = response ? response.statusCode : undefined;
+      console.log('addApiToGateway: response status:'+ statusCode);
+      if (error) console.error('Warning: addRouteToGateway request failed: '+ makeJsonString(error));
+      if (response && response.headers) console.log('addApiToGateway: response headers: '+makeJsonString(response.headers));
+      if (body) console.log('addApiToGateway: response body: '+makeJsonString(body));
+      if (error) {
+        console.error('addApiToGateway: Unable to configure the API Gateway');
+        reject('Unable to configure the API Gateway: '+makeJsonString(error));
+      } else if (statusCode != 200) {
+        if (body) {
+          var errMsg = makeJsonString(body);
+          if (body.error && body.error.message) errMsg = body.error.message;
+          reject('Unable to configure the API Gateway (status code '+statusCode+'): '+ errMsg);
+        } else {
+          reject('Unable to configure the API Gateway: Response failure code: '+statusCode);
+        }
+      } else if (!body) {
+        console.error('addApiToGateway: Unable to configure the API Gateway: No response body');
+        reject('Unable to configure the API Gateway: No response received from the API Gateway');
+      } else {
+        resolve(body);
+      }
+    });
+  });
+}
+
+/**
+ * Removes an API route from the API Gateway.
+ *
+ * @param gwInfo     Required.
+ * @param    gwUrl   Required. The base URL gateway path (i.e.  'PROTOCOL://gw.host.domain:PORT/CONTEXT')
+ * @param    gwAuth  Optional. The credentials used to access the API Gateway REST endpoints
+ * @param spaceGuid  Required. User's space guid.  APIs are stored under this context
+ * @param apiId      Required.  API basepath.  Unique per spaceGuid
+ * @return A promise for an object describing the result with fields error and response
+ */
+function deleteApiFromGateway(gwInfo, spaceGuid, apiId) {
+  var options = {
+    followAllRedirects: true,
+    url: gwInfo.gwUrl+'/'+encodeURIComponent(spaceGuid)+'/apis/'+encodeURIComponent(apiId),
+    agentOptions: {rejectUnauthorized: false},
+    headers: {
+      'Accept': 'application/json',
+      'User-Agent': UserAgent
+    }
+  };
+  if (gwInfo.gwAuth) {
+    options.headers.Authorization = 'Bearer ' + gwInfo.gwAuth;
+  }
+  console.log('deleteApiFromGateway: request: '+JSON.stringify(options));
+
+  return new Promise(function(resolve, reject) {
+    request.delete(options, function(error, response, body) {
+      var statusCode = response ? response.statusCode : undefined;
+      console.log('deleteApiFromGateway: response status:'+ statusCode);
+      if (error) console.error('Warning: deleteGatewayApi request failed: '+ makeJsonString(error));
+      if (body) console.log('deleteApiFromGateway: response body: '+makeJsonString(body));
+      if (response && response.headers) console.log('deleteApiFromGateway: response headers: '+makeJsonString(response.headers));
+      if (error) {
+        console.error('deleteApiFromGateway: Unable to delete the API Gateway');
+        reject('Unable to delete the API Gateway: '+makeJsonString(error));
+      } else if (statusCode != 200  && statusCode != 204) {
+        if (body) {
+          var errMsg = makeJsonString(body);
+          if (body.error && body.error.message) errMsg = body.error.message;
+          reject('Unable to delete the API Gateway (status code '+statusCode+'): '+ errMsg);
+        } else {
+          reject('Unable to delete the API Gateway: Response failure code: '+statusCode);
+        }
+      } else {
+        resolve();
+      }
+    });
+  });
+}
+
+/**
+ * Return an array of APIs
+ */
+function getApis(gwInfo, spaceGuid, bpOrApiName) {
+  var qsBasepath = { 'basePath' : bpOrApiName };
+  var qsApiName = { 'title' : bpOrApiName };
+  var qs;
+  if (bpOrApiName) {
+    if (bpOrApiName.indexOf('/') !== 0) {
+      console.log('getApis: querying APIs based on api name');
+      qs = qsApiName;
+    } else {
+      console.log('getApis: querying APIs based on basepath');
+      qs = qsBasepath;
+    }
+  }
+  var options = {
+    followAllRedirects: true,
+    url: gwInfo.gwUrl+'/'+encodeURIComponent(spaceGuid)+'/apis',
+    headers: {
+      'Accept': 'application/json',
+      'User-Agent': UserAgent
+    },
+    json: true
+  };
+  if (qs) {
+    options.qs = qs;
+  }
+  if (gwInfo.gwAuth) {
+    options.headers.Authorization = 'Bearer ' + gwInfo.gwAuth;
+  }
+  console.log('getApis: request: '+JSON.stringify(options));
+
+  return new Promise(function(resolve, reject) {
+    request.get(options, function(error, response, body) {
+      var statusCode = response ? response.statusCode : undefined;
+      console.log('getApis: response status: '+ statusCode);
+      if (error) console.error('Warning: getApis request failed: '+makeJsonString(error));
+      if (response && response.headers) console.log('getApis: response headers: '+makeJsonString(response.headers));
+      console.log('getApis: body type = '+typeof body);
+      if (body) console.log('getApis: response JSON.stringify(body): '+makeJsonString(body));
+      if (error) {
+        console.error('getApis: Unable to obtain API(s) from the API Gateway');
+        reject('Unable to obtain API(s) from the API Gateway: '+makeJsonString(error));
+      } else if (statusCode != 200) {
+        console.error('getApis: failure: response code: '+statusCode);
+        if (body) {
+          var errMsg = makeJsonString(body);
+          if (body.error && body.error.message) errMsg = body.error.message;
+          reject('Unable to obtain API(s) from the API Gateway (status code '+statusCode+'): '+ errMsg);
+        } else {
+          reject('Unable to obtain API(s) from the API Gateway: Response failure code: '+statusCode);
+        }
+      } else {
+        if (body) {
+          if (Array.isArray(body)) {
+            resolve(body);
+          } else {
+            console.error('getApis: Invalid API GW response body; a JSON array was not returned');
+            resolve( [] );
+          }
+        } else {
+          console.log('getApis: No APIs found');
+          resolve( [] );
+        }
+      }
+    });
+  });
+}
+
+/*
+ * Convert API object array into specified format
+ * Parameters:
+ *  apis    : array of 0 or more APIs
+ *  format  : 'apigw' or 'swagger'
+ * Returns:
+ *  array   : New array of API object - each in the specified format
+ */
+function transformApis(apis, format) {
+  var apisOutput;
+  try {
+    if (format.toLowerCase() === 'apigw') {
+      apisOutput = apis;
+    } else if (format.toLowerCase() === 'swagger') {
+      apisOutput = JSON.parse(JSON.stringify(apis));
+      for (var i = 0; i < apisOutput.length; i++) {
+        apisOutput[i] = generateSwaggerApiFromGwApi(apisOutput[i]);
+      }
+    } else {
+      console.error('transformApis: Invalid format specification: '+format);
+      throw 'Internal error. Invalid format specification: '+format;
+    }
+  } catch(e) {
+    console.error('transformApis: exception caught: '+e);
+    throw 'API format transformation error: '+e;
+  }
+
+  return apisOutput;
+}
+
+/*
+ * Convert API object into swagger JSON format
+ * Parameters:
+ *  gwApi  : API object as returned from the API Gateway
+ * Returns:
+ *  object : New API object in swagger JSON format
+ */
+function generateSwaggerApiFromGwApi(gwApi) {
+  // Start with a copy of the gwApi object.  It's close to the desired swagger format
+  var swaggerApi = JSON.parse(JSON.stringify(gwApi));
+  swaggerApi.swagger = '2.0';
+  swaggerApi.info = {
+    title: gwApi.name,
+    version: '1.0.0'
+  };
+
+  // Copy the gwAPI's 'resources' object as the starting point for the swagger 'paths' object
+  swaggerApi.paths = JSON.parse(JSON.stringify(gwApi.resources));
+  for (var path in swaggerApi.paths) {
+    if (!swaggerApi.paths[path]) {
+      console.error('generateSwaggerApiFromGwApi: no operations defined for ignored relpath \''+path+'\'');
+      delete swaggerApi.paths[path];
+      continue;
+    }
+    for (var op in swaggerApi.paths[path].operations) {
+      console.log('generateSwaggerApiFromGwApi: processing path '+path+'; operation '+op);
+      if (!op) {
+        console.error('generateSwaggerApiFromGwApi: path \''+path+'\' has no operations!');
+        continue;
+      }
+      // swagger wants lower case operations
+      var oplower = op.toLowerCase();
+
+      // Valid swagger requires a 'responses' object for each operation
+      swaggerApi.paths[path][oplower] = {
+        responses: {
+          default: {
+            description: 'Default response'
+          }
+        }
+      };
+      // Custom swagger extension to hold the action mapping configuration
+      swaggerApi.paths[path][oplower]['x-ibm-op-ext'] = {
+        backendMethod : swaggerApi.paths[path].operations[op].backendMethod,
+        backendUrl : swaggerApi.paths[path].operations[op].backendUrl,
+        policies : JSON.parse(JSON.stringify(swaggerApi.paths[path].operations[op].policies)),
+        actionName: getActionNameFromActionUrl(swaggerApi.paths[path].operations[op].backendUrl),
+        actionNamespace: getActionNamespaceFromActionUrl(swaggerApi.paths[path].operations[op].backendUrl)
+      };
+    }
+    delete swaggerApi.paths[path].operations;
+  }
+  delete swaggerApi.resources;
+  delete swaggerApi.name;
+  delete swaggerApi.id;
+  delete swaggerApi.managedUrl;
+  delete swaggerApi.tenantId;
+  return swaggerApi;
+}
+
+/*
+ * Take an API in JSON swagger format and create an API GW compatible
+ * API configuration JSON object
+ * Parameters:
+ *   swaggerApi - JSON object defining API in swagger format
+ * Returns:
+ *   gwApi      - JSON object defining API in API GW format
+ */
+function generateGwApiFromSwaggerApi(swaggerApi) {
+  var gwApi = {};
+  gwApi.basePath = swaggerApi.basePath;
+  gwApi.name = swaggerApi.info.title;
+  gwApi.resources = {};
+  for (var path in swaggerApi.paths) {
+  console.log('generateGwApiFromSwaggerApi: processing swaggerApi path: ', path);
+    gwApi.resources[path] = {};
+    var gwpathop = gwApi.resources[path].operations = {};
+    for (var operation in swaggerApi.paths[path]) {
+      console.log('generateGwApiFromSwaggerApi: processing swaggerApi operation: ', operation);
+      console.log('generateGwApiFromSwaggerApi: processing operation backendMethod: ', swaggerApi.paths[path][operation]['x-ibm-op-ext'].backendMethod);
+      var gwop = gwpathop[operation] = {};
+      gwop.backendMethod = swaggerApi.paths[path][operation]['x-ibm-op-ext'].backendMethod;
+      gwop.backendUrl = swaggerApi.paths[path][operation]['x-ibm-op-ext'].backendUrl;
+      gwop.policies = swaggerApi.paths[path][operation]['x-ibm-op-ext'].policies;
+    }
+  }
+  return gwApi;
+}
+
+/*
+ * Create a base swagger API object containing the API basepath, but no endpoints
+ * Parameters:
+ *   basepath   - Required. API basepath
+ *   apiname    - Optional. API friendly name. Defaults to basepath
+ * Returns:
+ *   swaggerApi - API swagger JSON object
+ */
+function generateBaseSwaggerApi(basepath, apiname) {
+  var swaggerApi = {
+    'swagger': '2.0',
+    'info': {
+      'title': apiname || basepath,
+      'version': '1.0.0'
+    },
+    'basePath': basepath,
+    'paths': {},
+    'x-ibm-configuration': {
+      'assembly': {
+      },
+      'cors': {
+        'enabled': true
+      }
+    }
+  };
+  return swaggerApi;
+}
+
+/*
+ * Take an existing API in JSON swagger format, and update it with a single path/operation.
+ * The addition can be an entirely new path or a new operation under an existing path.
+ * Parameters:
+ *   swaggerApi - API to augment in swagger JSON format.  This will be updated.
+ *   endpoint   - JSON object describing new path/operation.  Required fields
+ *                {
+ *                  gatewayMethod:
+ *                  gatewayPath:
+ *                  action: {
+ *                    authkey:
+ *                    backendMethod:
+ *                    backendUrl:
+ *                    name:
+ *                    namespace:
+ *                  }
+ *                }
+ *   responsetype Optional. The web action invocation .extension.  Defaults to json
+ * Returns:
+ *   swaggerApi - Input JSON object in swagger format containing the union of swaggerApi + new path/operation
+ */
+function addEndpointToSwaggerApi(swaggerApi, endpoint, responsetype) {
+  var operation = endpoint.gatewayMethod.toLowerCase();
+  var operationId = makeOperationId(operation, endpoint.gatewayPath);
+  responsetype = responsetype || 'json';
+  console.log('addEndpointToSwaggerApi: operationid = '+operationId);
+  try {
+    var auth_base64 = Buffer.from(endpoint.action.authkey,'ascii').toString('base64');
+
+    // If the relative path already exists, append to it; otherwise create it
+    if (!swaggerApi.paths[endpoint.gatewayPath]) {
+      swaggerApi.paths[endpoint.gatewayPath] = {};
+    }
+
+    swaggerApi.paths[endpoint.gatewayPath][operation] = {
+      'operationId': operationId,
+      'x-openwhisk': {
+        'url': makeWebActionBackendUrl(endpoint.action, responsetype),
+        'namespace': endpoint.action.namespace,
+        'package': getPackageNameFromFqActionName(endpoint.action.name),
+        'action': getActionNameFromFqActionName(endpoint.action.name),
+      },
+      'responses': {
+        'default': {
+          'description': 'Default response'
+        }
+      }
+    };
+
+    // API GW extensions
+    console.log('addEndpointToSwaggerApi: setting api gw extension values');
+    setActionOperationInvocationDetails(swaggerApi, endpoint, operationId, responsetype);
+  }
+  catch(e) {
+    console.log("addEndpointToSwaggerApi: exception "+e);
+    throw 'API swagger generation error: '+e;
+  }
+
+  return swaggerApi;
+}
+
+function setActionOperationInvocationDetails(swagger, endpoint, operationId, responsetype) {
+  var caseArr = _.get(swagger, 'x-ibm-configuration.assembly.execute[0].operation-switch.case') || [];
+  var caseIdx = getCaseOperationIdx(caseArr, operationId);
+  var operations = [operationId];
+  _.set(swagger, 'x-ibm-configuration.assembly.execute[0].operation-switch.case['+caseIdx+'].operations', operations);
+  _.set(swagger, 'x-ibm-configuration.assembly.execute[0].operation-switch.case['+caseIdx+'].execute[0].invoke.target-url',  makeWebActionBackendUrl(endpoint.action, responsetype));
+  _.set(swagger, 'x-ibm-configuration.assembly.execute[0].operation-switch.case['+caseIdx+'].execute[0].invoke.verb', 'keep');
+}
+
+// Return the numeric index into case[] into which the associated operation will be configured
+// If the array is empty, the returned index is 0
+// If the operation exists, the existing index will be returned
+// Otherwise the index will be the last existing index + 1
+function getCaseOperationIdx(caseArr, operationId) {
+  var i;
+  for (i=0; i<caseArr.length; i++) {
+    if (caseArr[i].operations[0] == operationId) {
+      console.log('getCaseOperationIdx: found existing operation for '+operationId+' at case index '+i);
+      break;
+    }
+  }
+  return i;
+}
+
+// Create the external URL used to invoke a web-action.  Examples:
+// - https://localhost/api/v1/web/whisk.system/default/echo-web.json
+// - https://localhost/api/v1/web/whisk.system/mypkg/echo-web.json
+// NOTE: Use "default" as the package name when a package is not explicitly defined.
+// Parameters
+//   endpointAction       - fully qualified action name (i.e. /ns/pkg/action or /ns/action)
+//   endpointResponseType - determines the action invocation extension without the '.' (i.e. http, json, etc)
+// Returns:
+//   string               - web-action URL
+function makeWebActionBackendUrl(endpointAction, endpointResponseType) {
+  host = getHostFromActionUrl(endpointAction.backendUrl);
+  ns = endpointAction.namespace;
+  pkg = getPackageNameFromFqActionName(endpointAction.name) || 'default';
+  name = getActionNameFromFqActionName(endpointAction.name);
+  return 'https://' + host + '/api/v1/web/' + ns + '/' + pkg + '/' + name + '.' + endpointResponseType;
+}
+
+/*
+ * Update an existing Swagger API document by removing the specified relpath/operation section.
+ *   swaggerApi - API from which to remove the specified endpoint.  This object will be updated.
+ *   endpoint   - JSON object describing new path/operation.  Required fields
+ *                {
+ *                  gatewayPath:    Optional.  The relative path.  If not provided, the original swaggerApi is returned
+ *                  gatewayMethod:  Optional.  The operation under gatewayPath.  If not provided, the entire gatewayPath is deleted.
+ *                                             If updated gatewayPath has no more operations, then the entire gatewayPath is deleted.
+ *                }
+ * @returns Updated JSON swagger API
+ */
+function removeEndpointFromSwaggerApi(swaggerApi, endpoint) {
+  var relpath = endpoint.gatewayPath;
+  var operation = endpoint.gatewayMethod ? endpoint.gatewayMethod.toLowerCase() : endpoint.gatewayMethod;
+  console.log('removeEndpointFromSwaggerApi: relpath '+relpath+' operation '+operation);
+  if (!relpath) {
+      console.log('removeEndpointFromSwaggerApi: No relpath specified; nothing to remove');
+      return 'No path provided; nothing to remove';
+  }
+
+  // If an operation is not specified, delete the entire relpath
+  if (!operation) {
+      console.log('removeEndpointFromSwaggerApi: No operation; removing entire relpath '+relpath);
+      if (swaggerApi.paths[relpath]) {
+          for (var op in swaggerApi.paths[relpath]) {
+            deleteActionOperationInvocationDetails(swaggerApi, makeOperationId(op, relpath));
+          }
+          delete swaggerApi.paths[relpath];
+      } else {
+          console.log('removeEndpointFromSwaggerApi: relpath '+relpath+' does not exist in the API');
+          return 'path \''+relpath+'\' does not exist in the API';
+      }
+  } else { // relpath and operation are specified, just delete the specific operation
+      if (swaggerApi.paths[relpath] && swaggerApi.paths[relpath][operation]) {
+          delete swaggerApi.paths[relpath][operation];
+          if (Object.keys(swaggerApi.paths[relpath]).length === 0) {
+            console.log('removeEndpointFromSwaggerApi: after deleting operation '+operation+', relpath '+relpath+' has no more operations; so deleting entire relpath '+relpath);
+            delete swaggerApi.paths[relpath];
+          }
+          deleteActionOperationInvocationDetails(swaggerApi, makeOperationId(operation, relpath));
+      } else {
+          console.log('removeEndpointFromSwaggerApi: relpath '+relpath+' with operation '+operation+' does not exist in the API');
+          return 'path \''+relpath+'\' with operation \''+operation+'\' does not exist in the API';
+      }
+  }
+
+  return swaggerApi;
+}
+
+function deleteActionOperationInvocationDetails(swagger, operationId) {
+  console.log('deleteActionOperationInvocationDetails: deleting case entry for ' + operationId);
+  var caseArr = _.get(swagger, 'x-ibm-configuration.assembly.execute[0].operation-switch.case') || [];
+  if (caseArr.length > 0) {
+    var caseIdx = getCaseOperationIdx(caseArr, operationId);
+    _.pullAt(caseArr, caseIdx);
+    _.set(swagger, 'x-ibm-configuration.assembly.execute[0].operation-switch.case', caseArr);
+  } else {
+    console.log('deleteActionOperationInvocationDetails: empty case[] array; case operation '+operationId+' does not exist');
+  }
+}
+
+function confidentialPrint(str) {
+    var printStr;
+    if (str) {
+        printStr = 'XXXXXXXXXX';
+    }
+    return printStr;
+}
+
+/* Create the CLI response payload from an array of GW API objects
+ * Parameters:
+ *  gwApis    - Array of JSON GW API objects
+ * Returns:
+ *  respApis  - A new array of JSON CLI API objects
+ */
+function generateCliResponse(gwApis) {
+  var respApis = [];
+  try {
+    for (var i=0; i<gwApis.length; i++) {
+      respApis.push(generateCliApiFromGwApi(gwApis[i]));
+    }
+  } catch(e) {
+    console.error('generateCliResponse: exception caught: '+e);
+    throw 'API format transformation error: '+e;
+  }
+  return respApis;
+}
+
+/* Use the specified GW API object to create an API JSON object in for format the CLI expects.
+ * Parameters:
+ *  gwApi      - JSON GW API object
+ * Returns:
+ *  cliApi     - JSON CLI API object
+ */
+function generateCliApiFromGwApi(gwApi) {
+  console.log('generateCliApiFromGwApi: ' + JSON.stringify(gwApi, " ", 2));
+  var cliApi = {};
+  cliApi.id = 'Not Used';
+  cliApi.key = 'Not Used';
+  cliApi.value = {};
+  cliApi.value.namespace = 'Not Used';
+  cliApi.value.gwApiActivated = true;
+  cliApi.value.tenantId = 'Not Used';
+  cliApi.value.gwApiUrl = gwApi.managed_url;
+  cliApi.value.apidoc = gwApi.open_api_doc;
+  return cliApi;
+}
+
+/*
+ * Parses the openwhisk action URL and returns the various components
+ * Parameters
+ *  url    - in format PROTOCOL://HOST/api/v1/web/NAMESPACE/PACKAGE/ACTION.http
+ * Returns
+ *  result - an array of strings.
+ *           result[0] : Entire URL
+ *           result[1] : protocol (i.e. https)
+ *           result[2] : host (i.e. myco.com, 1.2.3.4)
+ *           result[3] : namespace
+ *           result[4] : package name
+ *           result[5] : action name
+ *           result[6] : action response type (i.e http, json, text, html, or svg)
+ */
+function parseActionUrl(actionUrl) {
+  console.log('parseActionUrl: parsing action url: '+actionUrl);
+  var actionUrlPattern = /(\w+):\/\/([:\w.\-]+)\/api\/v\d\/web\/([@\w .\-]+)\/([@\w .\-]+)\/([@\w .\-\/]+)\.(\w+)/;
+  try {
+    return actionUrl.match(actionUrlPattern);
+  } catch(e) {
+    console.error('parseActionUrl: exception: '+e);
+    throw 'parseActionUrl: exception: '+e;
+  }
+}
+
+/*
+ * https://172.17.0.1/api/v1/web/NAMESPACE/PACKAGE/ACTION.json
+ * would return ACTION
+ */
+function getActionNameFromActionUrl(actionUrl) {
+  return parseActionUrl(actionUrl)[5];
+}
+
+/*
+ * https://172.17.0.1/api/v1/web/NAMESPACE/PACKAGE/ACTION.json
+ * would return NAMESPACE
+ */
+function getPackageNameFromActionUrl(actionUrl) {
+  return parseActionUrl(actionUrl)[4];
+}
+
+/*
+ * https://172.17.0.1/api/v1/web/NAMESPACE/PACKAGE/ACTION.json
+ * would return NAMESPACE
+ */
+function getActionNamespaceFromActionUrl(actionUrl) {
+  return parseActionUrl(actionUrl)[3];
+}
+
+/*
+ * https://172.17.0.1/api/v1/namespaces/whisk.system/actions/getaction
+ * would return 172.17.0.1
+ * https://my-host.mycompany.com/api/v1/namespaces/myid@gmail.com_dev/actions/mypkg/getaction
+ * would return my-host.mycompany.com
+ */
+function getHostFromActionUrl(actionUrl) {
+  return parseActionUrl(actionUrl)[2];
+}
+
+/*
+ * Parses an openwhisk action name into its various components
+ * Parameters
+ *  fqname - in one of the following formats:
+ *           (1)   /[namespace]/[package]/[action]
+ *           (2)   [package]/[action]
+ *           (3)   [action]
+ * Returns
+ *  result - an array of strings; depending on input
+ *           Input (1):
+ *             result[0] : fqname (i.e. /ns/pkg/action)
+ *             result[1] : namespace
+ *             result[2] : package
+ *             result[3] : action name
+ *           Input (2):
+ *             result[0] : fqname (i.e.  pkg/action)
+ *             result[1] : package
+ *             result[2] : action name
+ *             result[3] : ''
+ *           Input (3):
+ *             result[0] : fqname   (i.e. action)
+ *             result[1] : action name
+ *             result[2] : ''
+ *             result[3] : ''
+ */
+function parseActionName(fqname) {
+  console.log('parseActionName: parsing action: '+fqname);
+  var actionNamePattern = /[\/]?([@ .\-\w]*)[\/]?([@ .\-\w]*)[\/]?([@ .\-\w]*)/;
+  try {
+    return fqname.match(actionNamePattern);
+  } catch(e) {
+    console.error('parseActionName: exception: '+e);
+    throw 'parseActionName: exception: '+e;
+  }
+}
+
+function getNamespaceFromFqActionName(fqAction) {
+  var ns = '';
+  var parsedAction = parseActionName(fqAction);
+  if (parsedAction[3].length > 0) {
+    ns = parsedAction[1];
+  }
+  return ns;
+}
+
+function getPackageNameFromFqActionName(fqAction) {
+  var pkg = '';
+  var parsedAction = parseActionName(fqAction);
+  if (parsedAction[3].length > 0) {
+    pkg = parsedAction[2];
+  } else if (parsedAction[2].length > 0) {
+    pkg = parsedAction[1];
+  }
+  return pkg;
+}
+
+function getActionNameFromFqActionName(fqAction) {
+  var action = '';
+  var parsedAction = parseActionName(fqAction);
+  if (parsedAction[3].length > 0) {
+    action = parsedAction[3];
+  } else if (parsedAction[2].length > 0) {
+    action = parsedAction[2];
+  } else {
+    action = parsedAction[1];
+  }
+  return action;
+}
+
+/*
+ * Replace the namespace values that are used in the apidoc with the
+ * specified namespace
+ */
+function updateNamespace(apidoc, namespace) {
+  if (apidoc && namespace) {
+    if (apidoc.action) {
+      // The action namespace does not have to match the CLI user's namespace
+      // If it is different, leave it alone; otherwise use the replacement namespace
+      if (apidoc.namespace === apidoc.action.namespace) {
+        apidoc.action.namespace = namespace;
+        apidoc.action.backendUrl = replaceNamespaceInUrl(apidoc.action.backendUrl, namespace);      }
+    }
+    apidoc.namespace = namespace;
+  }
+}
+
+/*
+ * Take an OpenWhisk URL (i.e. action invocation URL) and replace the namespace
+ * path parameter value with the provided namespace value
+ */
+function replaceNamespaceInUrl(url, namespace) {
+  var namespacesPattern = /\/api\/v1\/web\/([\w@.-]+)\//;
+  console.log('replaceNamespaceInUrl: namspace='+namespace+' url before - '+url);
+  matchResult = url.match(namespacesPattern);
+  if (matchResult !== null) {
+    console.log('replaceNamespaceInUrl: replacing namespace \''+matchResult[1]+'\' with \''+namespace+'\'');
+    url = url.replace(namespacesPattern, '/api/v1/web/'+namespace+'/');
+  }
+  console.log('replaceNamespaceInUrl: url after - '+url);
+  return url;
+}
+
+/*
+ * Take an error string and create a response object suitable for inclusion in
+ * a Promise.reject() call.
+ *
+ * The response object can take two formats. If the api management action was
+ * invoked as a web-action (i.e. via https://OW-HOST/api/v1/web/NS/PKG/ACTION.http),
+ * then the response is an error object that mimics a non-webaction openwhisk
+ * action's application error response - like so:
+ *     {
+ *        statusCode: 502,    <- signifies an application error
+ *        headers: {'Content-Type': 'application/json'},
+ *        body: Base64 encoded JSON error string
+ *     }
+ * Otherwise, the action was invoked as a regular OpenWhisk action
+ * (i.e. https://OW-HOST/api/v1/namesapces/NS/actions/ACTION) and the
+ * error response is just a string.  OpenWhisk backend logic will ultimately
+ * convert this string into the above error object format.
+ *
+ * Parameters
+ *  err             - Error string
+ *  isWebAction     - Boolean. True -> generate a web-action response
+ *                             False -> Generate an action response
+ */
+function makeErrorResponseObject(err, isWebAction) {
+  console.log('makeErrorResponseObject: isWebAction: '+isWebAction);
+  if (!isWebAction) {
+    console.log('makeErrorResponseObject: not called as a web action');
+    return err;
+  }
+
+  var bodystr;
+  if (typeof err === 'string') {
+    bodystr = JSON.stringify({
+      "error": JSON.parse(makeJsonString(err)),  // Make sure err is plain old string to avoid duplicate JSON escaping
+    });
+  } else {
+    bodystr = JSON.stringify(err);
+  }
+  return {
+    statusCode: 502,
+    headers: { 'Content-Type': 'application/json' },
+    body: new Buffer(bodystr).toString('base64'),
+  };
+}
+
+/*
+ * Take an response string and create a response object suitable for inclusion in
+ * a Promise.resolve() call.
+ *
+ * The response object can take two formats. If the api management action was
+ * invoked as a web-action (i.e. via https://OW-HOST/api/v1/web/NS/PKG/ACTION.http),
+ * then the response is an object that mimics a non-webaction openwhisk
+ * action's application successful response - like so:
+ *     {
+ *        statusCode: 200,    <- signifies a successful action
+ *        headers: {'Content-Type': 'application/json'},
+ *        body: Base64 encoded JSON error string
+ *     }
+ * Otherwise, the action was invoked as a regular OpenWhisk action
+ * (i.e. https://OW-HOST/api/v1/namesapces/NS/actions/ACTION) and the
+ * response is just a string.  OpenWhisk backend logic will ultimately
+ * convert this string into the above object format.
+ *
+ * Parameters
+ *  err             - Error string
+ *  isWebAction     - Boolean. True -> generate a web-action response
+ *                             False -> generate an action response
+ */
+function makeResponseObject(resp, isWebAction) {
+  console.log('makeResponseObject: isWebAction: '+isWebAction);
+  if (!isWebAction) {
+    console.log('makeErrorResponseObject: not called as a web action');
+    return resp;
+  }
+
+  var bodystr;
+  if (typeof resp === 'string') {
+    bodystr = makeJsonString(resp);
+  } else {
+    bodystr = JSON.stringify(resp);
+  }
+  retobj = {
+    statusCode: 200,
+    headers: { 'Content-Type': 'application/json' },
+    body: new Buffer(bodystr).toString('base64')
+  };
+  return retobj;
+}
+
+/*
+ * Take an object and serialize it into a JSON string.
+ *
+ * Special consideration is give to strings that are already JSON formatted since
+ * serializing these strings can result in redundant escaping.
+ *
+ * If the value is simply not JSON compliant, a JSON error string is returned.
+ */
+function makeJsonString(x) {
+  // If the value is not already a string, rely on JSON.stringify to convert it correctly
+  if (typeof x != 'string') {
+    try {
+      return JSON.stringify(x);
+    } catch (e) {
+      console.error('makeJsonString: value cannot be JSON serialized: '+e);
+      return e;
+    }
+  } else {
+    // It's a string. If it's already a JSON formatted string, leave it alone
+    // Otherwise, convert it into a JSON formatted string
+    try {
+      var temp = JSON.parse(x);
+      return x;
+    } catch (e) {
+      // The string is not a JSON string, so convert it to a JSON string.
+      console.log('makeJsonString: String is not JSON, so need to convert it: '+e);
+      return JSON.stringify(x);
+    }
+  }
+  return 'Unexpected JSON parsing failure';
+}
+
+/*
+ * Generate and return a swagger OperationId value
+ *
+ * Parameters
+ *   operation  - String. HTTP method (i.e. get, post, etc)
+ *   repath     - String. Swagger path value. The path relative to the base path
+ */
+function makeOperationId(operation, relpath) {
+   // Concatenate operation + relpath, stripping '/' and camelCasing after each '/' delimiter
+   // relpath special character handling in each path segment:
+   //   . ~ ! $ & ' ( ) * + , ; = : @ are removed and the following characters in the same path segment are camel cased
+   //   - _  are retained and the following characters in the same path segment are lower cased
+  return operation.toLowerCase() +
+         relpath.replace(/[^0-9a-z_-]/gi, ' ').replace(/\w\S*/g, function(word) {return makeCamelCase(word);}).replace(/\s/g, '');
+}
+
+function makeCamelCase(str) {
+  return str.charAt(0).toUpperCase() + str.substr(1).toLowerCase();
+}
+
+function setSubUserAgent(subAgent) {
+  if (subAgent && subAgent.length > 0) {
+    UserAgent = UserAgent + " " + subAgent;
+  }
+}
+
+module.exports.getApis = getApis;
+module.exports.addApiToGateway = addApiToGateway;
+module.exports.deleteApiFromGateway = deleteApiFromGateway;
+module.exports.generateBaseSwaggerApi = generateBaseSwaggerApi;
+module.exports.generateGwApiFromSwaggerApi = generateGwApiFromSwaggerApi;
+module.exports.transformApis = transformApis;
+module.exports.generateSwaggerApiFromGwApi = generateSwaggerApiFromGwApi;
+module.exports.addEndpointToSwaggerApi = addEndpointToSwaggerApi;
+module.exports.removeEndpointFromSwaggerApi = removeEndpointFromSwaggerApi;
+module.exports.confidentialPrint = confidentialPrint;
+module.exports.generateCliResponse = generateCliResponse;
+module.exports.generateCliApiFromGwApi = generateCliApiFromGwApi;
+module.exports.updateNamespace = updateNamespace;
+module.exports.makeErrorResponseObject = makeErrorResponseObject;
+module.exports.makeResponseObject = makeResponseObject;
+module.exports.makeJsonString = makeJsonString;
+module.exports.setSubUserAgent = setSubUserAgent;
diff --git a/routemgmt/common/utils.js b/routemgmt/common/utils.js
new file mode 100644
index 0000000..6e3c3a7
--- /dev/null
+++ b/routemgmt/common/utils.js
@@ -0,0 +1,776 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Route management action common utilities
+ */
+var request = require('request');
+var utils2 = require('./apigw-utils.js');
+
+/**
+ * Register a tenant with the API GW.
+ * A new tenant is created for each unique namespace/tenantinstance.  If the
+ * tenant already exists, the tenant is left as-is
+ * Parameters:
+ *  gwInfo         - Required. API GW connection information (gwUrl, gwAuth)
+ *  namespace      - Required. Namespace of tenant
+ *  tenantInstance - Optional. Tenanant instance used to create >1 tenant per namespace
+ *                   Defaults to 'openwhisk'
+ * Returns:
+ *  tenant object  - JSON object representing the tenant in the following format:
+ *                   { id: GUID, namespace: NAMESPACE, instance: 'openwhisk' }
+ */
+function createTenant(gwInfo, namespace, tenantInstance) {
+  var instance = tenantInstance || 'openwhisk';  // Default to a fixed instance so all openwhisk tenants have a common instance
+  var options = {
+    followAllRedirects: true,
+    url: gwInfo.gwUrl+'/tenants',
+    headers: {
+      'Accept': 'application/json'
+    },
+    json: {                     // Auto set header: 'Content-Type': 'application/json'
+      instance: instance,
+      namespace: namespace
+    }
+  };
+  if (gwInfo.gwAuth) {
+    options.headers.Authorization = 'Basic ' + gwInfo.gwAuth;
+  }
+  console.log('addTenantToGateway: request: '+JSON.stringify(options));
+
+  return new Promise(function(resolve, reject) {
+    request.put(options, function(error, response, body) {
+      var statusCode = response ? response.statusCode : undefined;
+      console.log('addTenantToGateway: response status: '+ statusCode);
+      if (error) console.error('Warning: addTenantToGateway request failed: '+utils2.makeJsonString(error));
+      if (body) console.log('addTenantToGateway: response body: '+utils2.makeJsonString(body));
+
+      if (error) {
+        console.error('addTenantToGateway: Unable to configure a tenant on the API Gateway');
+        reject('Unable to configure the API Gateway: '+utils2.makeJsonString(error));
+      } else if (statusCode != 200) {
+        if (body) {
+          var errMsg = JSON.stringify(body);
+          if (body.error && body.error.message) errMsg = body.error.message;
+          reject('API Gateway failure (status code '+statusCode+'): '+ errMsg);
+        } else {
+          reject('Unable to configure the API Gateway: Response failure code: '+statusCode);
+        }
+
+      } else {
+        if (body && body.id) {  // body has format like:  { id: GUID, namespace: NAMESPACE, instance: 'openwhisk' }
+          console.log('addTenantToGateway: got a single tenant response');
+          resolve(body);
+        } else {
+          console.error('addTenantToGateway: failure: No tenant guid provided');
+          reject('Unable to configure the API Gateway: Invalid response from API Gateway');
+        }
+      }
+    });
+  });
+}
+
+/*
+ * Return an array of tenants
+ */
+function getTenants(gwInfo, ns, tenantInstance) {
+  var qsNsOnly = { 'filter[where][namespace]' : ns };
+  var qsNsAndInstance = { 'filter[where][namespace]' : ns,
+                          'filter[where][instance]'  : tenantInstance };
+  var qs = qsNsOnly;
+  if (tenantInstance) qs = qsNsAndInstance;
+  var options = {
+    followAllRedirects: true,
+    url: gwInfo.gwUrl+'/tenants',
+    qs: qs,
+    headers: {
+      'Accept': 'application/json'
+    },
+  };
+  if (gwInfo.gwAuth) {
+    options.headers.Authorization = 'Basic ' + gwInfo.gwAuth;
+  }
+  console.log('getTenants: request: '+JSON.stringify(options));
+
+  return new Promise(function(resolve, reject) {
+    request.get(options, function(error, response, body) {
+      var statusCode = response ? response.statusCode : undefined;
+      console.log('getTenants: response status: '+ statusCode);
+      if (error) console.error('Warning: getTenant request failed: '+utils2.makeJsonString(error));
+      if (body) console.log('getTenants: response body: '+utils2.makeJsonString(body));
+      if (error) {
+        console.error('getTenants: Unable to obtain tenant from the API Gateway');
+        reject('Unable to obtain Tenant from the API Gateway: '+utils2.makeJsonString(error));
+      } else if (statusCode != 200) {
+        if (body) {
+          var errMsg = JSON.stringify(body);
+          if (body.error && body.error.message) errMsg = body.error.message;
+          reject('API Gateway failure (status code '+statusCode+'): '+ errMsg);
+        } else {
+          reject('Unable to configure the API Gateway: Response failure code: '+statusCode);
+        }
+      } else {
+        if (body) {
+          try {
+            var bodyJson = JSON.parse(body);
+            if (Array.isArray(bodyJson)) {
+              resolve(bodyJson);
+            } else {
+              console.error('getTenants: Invalid API GW response body; a JSON array was not returned');
+              resolve( [] );
+            }
+          } catch(e) {
+            console.error('getTenants: Invalid API GW response body; JSON.parse() failure: '+e);
+            reject('Internal error. Invalid API Gateway response: '+e);
+          }
+        } else {
+          console.log('getTenants: No tenants found');
+          resolve( [] );
+        }
+      }
+    });
+  });
+}
+
+/**
+ * Configures an API route on the API Gateway.  This API will map to an OpenWhisk action that
+ * will be invoked by the API Gateway when the API route is accessed.
+ *
+ * @param gwInfo Required.
+ * @param    gwUrl   Required.  The base URL gateway path (i.e.  'PROTOCOL://gw.host.domain:PORT/CONTEXT')
+ * @param    gwAuth  Required.  The credentials used to access the API Gateway REST endpoints
+ * @param tenantId   Required.
+ * @param swaggerApi   Required. The gateway API object to send to the API gateway
+ * @param   payload.namespace  Required. The OpenWhisk namespace of the user defining this API route
+ * @param   payload.gatewayPath  Required.  The relative path for this route
+ * @param   payload.gatewayMethod  Required.  The gateway route REST verb
+ * @param   payload.backendUrl  Required.  The full REST URL used to invoke the associated action
+ * @param   payload.backendMethod  Required.  The REST verb used to invoke the associated action
+ * @return A promise for an object describing the result with fields error and response
+ */
+function addApiToGateway(gwInfo, tenantId, swaggerApi, gwApiId) {
+  var requestFcn = request.post;
+
+  // Init the GW API configuration object; base it off the swagger API
+  var gwApi;
+  try {
+    gwApi = generateGwApiFromSwaggerApi(swaggerApi);
+  } catch(e) {
+    console.error('generateGwApiFromSwaggerApi exception: '+e);
+    return Promise.reject('Invalid API configuration: '+e);
+  }
+  gwApi.tenantId = tenantId;
+
+  var options = {
+    followAllRedirects: true,
+    url: gwInfo.gwUrl+'/apis',
+    headers: {
+      'Accept': 'application/json'
+    },
+    json: gwApi,  // Use of json automaticatlly sets header: 'Content-Type': 'application/json'
+  };
+  if (gwInfo.gwAuth) {
+    options.headers.Authorization = 'Basic ' + gwInfo.gwAuth;
+  }
+
+  if (gwApiId) {
+    console.log("addApiToGateway: Updating existing API");
+    gwApi.id = gwApiId;
+    options.url = gwInfo.gwUrl+'/apis/'+gwApiId;
+    requestFcn = request.put;
+  }
+
+  console.log('addApiToGateway: request: '+JSON.stringify(options, " ", 2));
+
+  return new Promise(function(resolve, reject) {
+    requestFcn(options, function(error, response, body) {
+      var statusCode = response ? response.statusCode : undefined;
+      console.log('addApiToGateway: response status:'+ statusCode);
+      if (error) console.error('Warning: addRouteToGateway request failed: '+ utils2.makeJsonString(error));
+      if (body) console.log('addApiToGateway: response body: '+utils2.makeJsonString(body));
+
+      if (error) {
+        console.error('addApiToGateway: Unable to configure the API Gateway');
+        reject('Unable to configure the API Gateway: '+utils2.makeJsonString(error));
+      } else if (statusCode != 200) {
+        if (body) {
+          var errMsg = JSON.stringify(body);
+          if (body.error && body.error.message) errMsg = body.error.message;
+          reject('Unable to configure the API Gateway (status code '+statusCode+'): '+ errMsg);
+        } else {
+          reject('Unable to configure the API Gateway: Response failure code: '+statusCode);
+        }
+      } else if (!body) {
+        console.error('addApiToGateway: Unable to configure the API Gateway: No response body');
+        reject('Unable to configure the API Gateway: No response received from the API Gateway');
+      } else {
+        resolve(body);
+      }
+    });
+  });
+}
+
+/**
+ * Removes an API route from the API Gateway.
+ *
+ * @param gwInfo Required.
+ * @param    gwUrl   Required. The base URL gateway path (i.e.  'PROTOCOL://gw.host.domain:PORT/CONTEXT')
+ * @param    gwAuth  Optional. The credentials used to access the API Gateway REST endpoints
+ * @param apiId  Required.  Unique Gateway API Id
+ * @return A promise for an object describing the result with fields error and response
+ */
+function deleteApiFromGateway(gwInfo, gwApiId) {
+  var options = {
+    followAllRedirects: true,
+    url: gwInfo.gwUrl+'/apis/'+gwApiId,
+    agentOptions: {rejectUnauthorized: false},
+    headers: {
+      'Accept': 'application/json'
+    }
+  };
+  if (gwInfo.gwAuth) {
+    options.headers.Authorization = 'Basic ' + gwInfo.gwAuth;
+  }
+  console.log('deleteApiFromGateway: request: '+JSON.stringify(options, " ", 2));
+
+  return new Promise(function(resolve, reject) {
+    request.delete(options, function(error, response, body) {
+      var statusCode = response ? response.statusCode : undefined;
+      console.log('deleteApiFromGateway: response status:'+ statusCode);
+      if (error) console.error('Warning: deleteGatewayApi request failed: '+ utils2.makeJsonString(error));
+      if (body) console.log('deleteApiFromGateway: response body: '+utils2.makeJsonString(body));
+
+      if (error) {
+        console.error('deleteApiFromGateway: Unable to delete the API Gateway');
+        reject('Unable to delete the API Gateway: '+utils2.makeJsonString(error));
+      } else if (statusCode != 200) {
+        if (body) {
+          var errMsg = JSON.stringify(body);
+          if (body.error && body.error.message) errMsg = body.error.message;
+          reject('Unable to delete the API Gateway (status code '+statusCode+'): '+ errMsg);
+        } else {
+          reject('Unable to delete the API Gateway: Response failure code: '+statusCode);
+        }
+      } else {
+        resolve();
+      }
+    });
+  });
+}
+
+/**
+ * Return an array of APIs
+ */
+function getApis(gwInfo, tenantId, bpOrApiName) {
+  var qsBasepath = { 'filter[where][basePath]' : bpOrApiName };
+  var qsApiName = { 'filter[where][name]' : bpOrApiName };
+  var qs;
+  if (bpOrApiName) {
+    if (bpOrApiName.indexOf('/') !== 0) {
+      console.log('getApis: querying APIs based on api name');
+      qs = qsApiName;
+    } else {
+      console.log('getApis: querying APIs based on basepath');
+      qs = qsBasepath;
+    }
+  }
+  var options = {
+    followAllRedirects: true,
+    url: gwInfo.gwUrl+'/tenants/'+tenantId+'/apis',
+    headers: {
+      'Accept': 'application/json'
+    },
+  };
+  if (qs) {
+    options.qs = qs;
+  }
+  if (gwInfo.gwAuth) {
+    options.headers.Authorization = 'Basic ' + gwInfo.gwAuth;
+  }
+  console.log('getApis: request: '+JSON.stringify(options));
+
+  return new Promise(function(resolve, reject) {
+    request.get(options, function(error, response, body) {
+      var statusCode = response ? response.statusCode : undefined;
+      console.log('getApis: response status: '+ statusCode);
+      if (error) console.error('Warning: getApis request failed: '+utils2.makeJsonString(error));
+      if (body) console.log('getApis: response body: '+utils2.makeJsonString(body));
+      if (error) {
+        console.error('getApis: Unable to obtain API(s) from the API Gateway');
+        reject('Unable to obtain API(s) from the API Gateway: '+utils2.makeJsonString(error));
+      } else if (statusCode != 200) {
+        if (body) {
+          var errMsg = JSON.stringify(body);
+          if (body.error && body.error.message) errMsg = body.error.message;
+          reject('Unable to obtain API(s) from the API Gateway (status code '+statusCode+'): '+ errMsg);
+        } else {
+          reject('Unable to obtain API(s) from the API Gateway: Response failure code: '+statusCode);
+        }
+      } else {
+        if (body) {
+          try {
+            var bodyJson = JSON.parse(body);
+            if (Array.isArray(bodyJson)) {
+              resolve(bodyJson);
+            } else {
+              console.error('getApis: Invalid API GW response body; a JSON array was not returned');
+              resolve( [] );
+            }
+          } catch(e) {
+            console.error('getApis: Invalid API GW response body; JSON.parse() failure: '+e);
+            reject('Invalid API Gateway response: '+e);
+          }
+        } else {
+          console.log('getApis: No APIs found');
+          resolve( [] );
+        }
+      }
+    });
+  });
+}
+
+/**
+ * Convert API object array into specified format
+ * Parameters:
+ *  apis    : array of 0 or more APIs
+ *  format  : 'apigw' or 'swagger'
+ * Returns:
+ *  array   : New array of API object - each in the specified format
+ */
+function transformApis(apis, format) {
+  var apisOutput;
+  try {
+    if (format.toLowerCase() === 'apigw') {
+      apisOutput = apis;
+    } else if (format.toLowerCase() === 'swagger') {
+      apisOutput = JSON.parse(JSON.stringify(apis));
+      for (var i = 0; i < apisOutput.length; i++) {
+        apisOutput[i] = generateSwaggerApiFromGwApi(apisOutput[i]);
+      }
+    } else {
+      console.error('transformApis: Invalid format specification: '+format);
+      throw 'Internal error. Invalid format specification: '+format;
+    }
+  } catch(e) {
+    console.error('transformApis: exception caught: '+e);
+    throw 'API format transformation error: '+e;
+  }
+
+  return apisOutput;
+}
+
+/**
+ * Convert API object into swagger JSON format
+ * Parameters:
+ *  gwApi  : API object as returned from the API Gateway
+ * Returns:
+ *  object : New API object in swagger JSON format
+ */
+function generateSwaggerApiFromGwApi(gwApi) {
+  // Start with a copy of the gwApi object.  It's close to the desired swagger format
+  var swaggerApi = JSON.parse(JSON.stringify(gwApi));
+  swaggerApi.swagger = '2.0';
+  swaggerApi.info = {
+    title: gwApi.name,
+    version: '1.0.0'
+  };
+
+  // Copy the gwAPI's 'resources' object as the starting point for the swagger 'paths' object
+  swaggerApi.paths = JSON.parse(JSON.stringify(gwApi.resources));
+  for (var path in swaggerApi.paths) {
+    if (!swaggerApi.paths[path]) {
+      console.error('generateSwaggerApiFromGwApi: no operations defined for ignored relpath \''+path+'\'');
+      delete swaggerApi.paths[path];
+      continue;
+    }
+    for (var op in swaggerApi.paths[path].operations) {
+      console.log('generateSwaggerApiFromGwApi: processing path '+path+'; operation '+op);
+      if (!op) {
+        console.error('generateSwaggerApiFromGwApi: path \''+path+'\' has no operations!');
+        continue;
+      }
+      // swagger wants lower case operations
+      var oplower = op.toLowerCase();
+
+      // Valid swagger requires a 'responses' object for each operation
+      swaggerApi.paths[path][oplower] = {
+        responses: {
+          default: {
+            description: 'Default response'
+          }
+        }
+      };
+      // Custom swagger extension to hold the action mapping configuration
+      swaggerApi.paths[path][oplower]['x-ibm-op-ext'] = {
+        backendMethod : swaggerApi.paths[path].operations[op].backendMethod,
+        backendUrl : swaggerApi.paths[path].operations[op].backendUrl,
+        policies : JSON.parse(JSON.stringify(swaggerApi.paths[path].operations[op].policies)),
+        actionName: getActionNameFromActionUrl(swaggerApi.paths[path].operations[op].backendUrl),
+        actionNamespace: getActionNamespaceFromActionUrl(swaggerApi.paths[path].operations[op].backendUrl)
+      };
+    }
+    delete swaggerApi.paths[path].operations;
+  }
+  delete swaggerApi.resources;
+  delete swaggerApi.name;
+  delete swaggerApi.id;
+  delete swaggerApi.managedUrl;
+  delete swaggerApi.tenantId;
+  return swaggerApi;
+}
+
+/**
+ * Create a base swagger API object containing the API basepath, but no endpoints
+ * Parameters:
+ *   basepath   - Required. API basepath
+ *   apiname    - Optional. API friendly name. Defaults to basepath
+ * Returns:
+ *   swaggerApi - API swagger JSON object
+ */
+function generateBaseSwaggerApi(basepath, apiname) {
+  var swaggerApi = {
+    swagger: "2.0",
+    info: {
+      title: apiname || basepath,
+      version: "1.0.0"
+    },
+    basePath: basepath,
+    paths: {}
+  };
+  return swaggerApi;
+}
+
+/**
+ * Take an API in JSON swagger format and create an API GW compatible
+ * API configuration JSON object
+ * Parameters:
+ *   swaggerApi - JSON object defining API in swagger format
+ * Returns:
+ *   gwApi      - JSON object defining API in API GW format
+ */
+function generateGwApiFromSwaggerApi(swaggerApi) {
+  var gwApi = {};
+  gwApi.basePath = swaggerApi.basePath;
+  gwApi.name = swaggerApi.info.title;
+  gwApi.resources = {};
+  for (var path in swaggerApi.paths) {
+  console.log('generateGwApiFromSwaggerApi: processing swaggerApi path: ', path);
+    gwApi.resources[path] = {};
+    var gwpathop = gwApi.resources[path].operations = {};
+    for (var operation in swaggerApi.paths[path]) {
+      console.log('generateGwApiFromSwaggerApi: processing swaggerApi operation: ', operation);
+      console.log('generateGwApiFromSwaggerApi: processing operation backendMethod: ', swaggerApi.paths[path][operation]['x-ibm-op-ext'].backendMethod);
+      var gwop = gwpathop[operation] = {};
+      gwop.backendMethod = swaggerApi.paths[path][operation]['x-ibm-op-ext'].backendMethod;
+      gwop.backendUrl = swaggerApi.paths[path][operation]['x-ibm-op-ext'].backendUrl;
+      gwop.policies = swaggerApi.paths[path][operation]['x-ibm-op-ext'].policies;
+    }
+  }
+  return gwApi;
+}
+
+/**
+ * Take an existing API in JSON swagger format, and update it with a single path/operation.
+ * The addition can be an entirely new path or a new operation under an existing path.
+ * Parameters:
+ *   swaggerApi - API to augment in swagger JSON format.  This will be updated.
+ *   endpoint   - JSON object describing new path/operation.  Required fields
+ *                {
+ *                  gatewayMethod:
+ *                  gatewayPath:
+ *                  action: {
+ *                    authkey:
+ *                    backendMethod:
+ *                    backendUrl:
+ *                    name:
+ *                    namespace:
+ *                  }
+ *                }
+ * Returns:
+ *   swaggerApi - Input JSON object in swagger format containing the union of swaggerApi + new path/operation
+ */
+function addEndpointToSwaggerApi(swaggerApi, endpoint) {
+  var operation = endpoint.gatewayMethod.toLowerCase();
+  var auth_base64 = Buffer.from(endpoint.action.authkey,'ascii').toString('base64');
+
+  // If the relative path already exists, append to it; otherwise create it
+  if (!swaggerApi.paths[endpoint.gatewayPath]) {
+    swaggerApi.paths[endpoint.gatewayPath] = {};
+  }
+  swaggerApi.paths[endpoint.gatewayPath][operation] = {
+    'x-ibm-op-ext': {
+      backendMethod: endpoint.action.backendMethod,
+      backendUrl: endpoint.action.backendUrl,
+      actionName: endpoint.action.name,
+      actionNamespace: endpoint.action.namespace,
+      policies: [
+        {
+          type: 'reqMapping',
+          value: [
+            {
+              action: 'transform',
+              from: {
+                name: '*',
+                location: 'query'
+              },
+              to: {
+                name: '*',
+                location: 'body'
+              }
+            },
+            {
+              action: 'insert',
+              from: {
+                value: 'Basic '+auth_base64
+              },
+              to: {
+                name: 'Authorization',
+                location: 'header'
+              }
+            },
+            {
+              action: 'insert',
+              from: {
+                value: 'application/json'
+              },
+              to: {
+                name: 'Content-Type',
+                location: 'header'
+              }
+            },
+            {
+              action: 'insert',
+              from: {
+                value: 'true'
+              },
+              to: {
+                name: 'blocking',
+                location: 'query'
+              }
+            },
+            {
+              action: 'insert',
+              from: {
+                value: 'true'
+              },
+              to: {
+                name: 'result',
+                location: 'query'
+              }
+            }
+          ]
+        }
+      ]
+    },
+    responses: {
+      default: {
+        description: "Default response"
+      }
+    }
+  };
+
+  return swaggerApi;
+}
+
+/**
+ * Update an existing DB API document by removing the specified relpath/operation section.
+ *   swaggerApi - API from which to remove the specified endpoint.  This object will be updated.
+ *   endpoint   - JSON object describing new path/operation.  Required fields
+ *                {
+ *                  gatewayPath:    Optional.  The relative path.  If not provided, the original swaggerApi is returned
+ *                  gatewayMethod:  Optional.  The operation under gatewayPath.  If not provided, the entire gatewayPath is deleted.
+ *                                             If updated gatewayPath has no more operations, then the entire gatewayPath is deleted.
+ *                }
+ * @returns Updated JSON swagger API
+ */
+function removeEndpointFromSwaggerApi(swaggerApi, endpoint) {
+  var relpath = endpoint.gatewayPath;
+  var operation = endpoint.gatewayMethod ? endpoint.gatewayMethod.toLowerCase() : endpoint.gatewayMethod;
+  console.log('removeEndpointFromSwaggerApi: relpath '+relpath+' operation '+operation);
+  if (!relpath) {
+      console.log('removeEndpointFromSwaggerApi: No relpath specified; nothing to remove');
+      return 'No relpath provided; nothing to remove';
+  }
+
+  // If an operation is not specified, delete the entire relpath
+  if (!operation) {
+      console.log('removeEndpointFromSwaggerApi: No operation; removing entire relpath '+relpath);
+      if (swaggerApi.paths[relpath]) {
+          delete swaggerApi.paths[relpath];
+      } else {
+          console.log('removeEndpointFromSwaggerApi: relpath '+relpath+' does not exist in the API; already deleted');
+          return 'relpath '+relpath+' does not exist in the API';
+      }
+  } else {
+      if (swaggerApi.paths[relpath] && swaggerApi.paths[relpath][operation]) {
+          delete swaggerApi.paths[relpath][operation];
+          if (Object.keys(swaggerApi.paths[relpath]).length === 0) {
+            console.log('removeEndpointFromSwaggerApi: after deleting operation '+operation+', relpath '+relpath+' has no more operations; so deleting entire relpath '+relpath);
+            delete swaggerApi.paths[relpath];
+          }
+      } else {
+          console.log('removeEndpointFromSwaggerApi: relpath '+relpath+' with operation '+operation+' does not exist in the API');
+          return 'relpath '+relpath+' with operation '+operation+' does not exist in the API';
+      }
+  }
+
+  return swaggerApi;
+}
+
+function confidentialPrint(str) {
+    var printStr;
+    if (str) {
+        printStr = 'XXXXXXXXXX';
+    }
+    return printStr;
+}
+
+/**
+ * Create the CLI response payload from an array of GW API objects
+ * Parameters:
+ *  gwApis    - Array of JSON GW API objects
+ * Returns:
+ *  respApis  - A new array of JSON CLI API objects
+ */
+function generateCliResponse(gwApis) {
+  var respApis = [];
+  try {
+    for (var i=0; i<gwApis.length; i++) {
+      respApis.push(generateCliApiFromGwApi(gwApis[i]));
+    }
+  } catch(e) {
+    console.error('generateCliResponse: exception caught: '+e);
+    throw 'API format transformation error: '+e;
+  }
+  return respApis;
+}
+
+/**
+ * Use the specified GW API object to create an API JSON object in for format the CLI expects.
+ * Parameters:
+ *  gwApi      - JSON GW API object
+ * Returns:
+ *  cliApi     - JSON CLI API object
+ */
+function generateCliApiFromGwApi(gwApi) {
+  var cliApi = {};
+  cliApi.id = 'Not Used';
+  cliApi.key = 'Not Used';
+  cliApi.value = {};
+  cliApi.value.namespace = 'Not Used';
+  cliApi.value.gwApiActivated = true;
+  cliApi.value.tenantId = 'Not Used';
+  cliApi.value.gwApiUrl = gwApi.managedUrl;
+  cliApi.value.apidoc = generateSwaggerApiFromGwApi(gwApi);
+  return cliApi;
+}
+
+/*
+ * Parses the openwhisk action URL and returns the various components
+ * Parameters
+ *  url    - in format PROTOCOL://HOST/api/v1/namespaces/NAMESPACE/actions/ACTIONNAME
+ * Returns
+ *  result - an array of strings.
+ *           result[0] : Entire URL
+ *           result[1] : protocol (i.e. https)
+ *           result[2] : host (i.e. myco.com, 1.2.3.4)
+ *           result[3] : namespace
+ *           result[4] : action name, including the package if used (i.e. myaction, mypkg/myaction)
+ */
+function parseActionUrl(actionUrl) {
+  var actionUrlPattern = /(\w+):\/\/([:\w.\-]+)\/api\/v\d\/namespaces\/([@\w .\-]+)\/actions\/([@\w .\-\/]+)/;
+  try {
+    return actionUrl.match(actionUrlPattern);
+  } catch(e) {
+    console.error('parseActionUrl: exception: '+e);
+    throw 'parseActionUrl: exception: '+e;
+  }
+}
+
+/*
+ * https://172.17.0.1/api/v1/namespaces/whisk.system/actions/getaction
+ * would return getaction
+ * https://my-host.mycompany.com/api/v1/namespaces/myid@gmail.com_dev/actions/getaction
+ * would return getaction
+ *
+ * https://172.17.0.1/api/v1/namespaces/whisk.system/actions/mypkg/getaction
+ * would return mypkg/getaction
+ * https://my-host.mycompany.com/api/v1/namespaces/myid@gmail.com_dev/actions/mypkg/getaction
+ * would return mypkg/getaction
+ */
+function getActionNameFromActionUrl(actionUrl) {
+  return parseActionUrl(actionUrl)[4];
+}
+
+/*
+ * https://172.17.0.1/api/v1/namespaces/whisk.system/actions/getaction
+ * would return whisk.system
+ * https://my-host.mycompany.com/api/v1/namespaces/myid@gmail.com_dev/actions/mypkg/getaction
+ * would return myid@gmail.com_dev
+ */
+function getActionNamespaceFromActionUrl(actionUrl) {
+  return parseActionUrl(actionUrl)[3];
+}
+
+/*
+ * Replace the namespace values that are used in the apidoc with the
+ * specified namespace
+ */
+function updateNamespace(apidoc, namespace) {
+  if (apidoc && namespace) {
+    if (apidoc.action) {
+      // The action namespace does not have to match the CLI user's namespace
+      // If it is different, leave it alone; otherwise use the replacement namespace
+      if (apidoc.namespace === apidoc.action.namespace) {
+        apidoc.action.namespace = namespace;
+        apidoc.action.backendUrl = replaceNamespaceInUrl(apidoc.action.backendUrl, namespace);      }
+    }
+    apidoc.namespace = namespace;
+  }
+}
+
+/*
+ * Take an OpenWhisk URL (i.e. action invocation URL) and replace the namespace
+ * path parameter value with the provided namespace value
+ */
+function replaceNamespaceInUrl(url, namespace) {
+  var namespacesPattern = /\/namespaces\/([\w@.-]+)\//;
+  console.log('replaceNamespaceInUrl: url before - '+url);
+  matchResult = url.match(namespacesPattern);
+  if (matchResult !== null) {
+    console.log('replaceNamespaceInUrl: replacing namespace \''+matchResult[1]+'\' with \''+namespace+'\'');
+    url = url.replace(namespacesPattern, '/namespaces/'+namespace+'/');
+  }
+  console.log('replaceNamespaceInUrl: url after - '+url);
+  return url;
+}
+
+module.exports.createTenant = createTenant;
+module.exports.getTenants = getTenants;
+module.exports.getApis = getApis;
+module.exports.addApiToGateway = addApiToGateway;
+module.exports.deleteApiFromGateway = deleteApiFromGateway;
+module.exports.generateBaseSwaggerApi = generateBaseSwaggerApi;
+module.exports.generateGwApiFromSwaggerApi = generateGwApiFromSwaggerApi;
+module.exports.transformApis = transformApis;
+module.exports.generateSwaggerApiFromGwApi = generateSwaggerApiFromGwApi;
+module.exports.addEndpointToSwaggerApi = addEndpointToSwaggerApi;
+module.exports.removeEndpointFromSwaggerApi = removeEndpointFromSwaggerApi;
+module.exports.confidentialPrint = confidentialPrint;
+module.exports.generateCliResponse = generateCliResponse;
+module.exports.generateCliApiFromGwApi = generateCliApiFromGwApi;
+module.exports.updateNamespace = updateNamespace;
diff --git a/routemgmt/createApi/createApi.js b/routemgmt/createApi/createApi.js
new file mode 100644
index 0000000..a984b33
--- /dev/null
+++ b/routemgmt/createApi/createApi.js
@@ -0,0 +1,337 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+ /*
+  * Add a new or update an existing API configuration in the API Gateway
+  * https://docs.cloudant.com/document.html#documentCreate
+  *
+  * Parameters (all as fields in the message JSON object)
+  *   gwUrlV2              Required when accesstoken is provided. The V2 API Gateway base path (i.e. http://gw.com)
+  *   gwUrl                Required. The API Gateway base path (i.e. http://gw.com)
+  *   gwUser               Optional. The API Gateway authentication
+  *   gwPwd                Optional. The API Gateway authentication
+  *   __ow_user            Required. Namespace of API author.  Set by controller
+  *                          The value overrides namespace values in the apidoc
+  *                          Don't override namespace values in the swagger though
+  *   tenantInstance       Optional. Instance identifier used when creating the specific API GW Tenant
+  *   accesstoken          Optional. Dynamic API GW auth.  Overrides gwUser/gwPwd
+  *   spaceguid            Optional. Namespace unique id.
+  *   responsetype         Optional. web action response .extension to use.  default to json
+  *   apidoc               Required. The API Gateway mapping document
+  *      namespace           Required.  Namespace of user/caller
+  *      apiName             Optional if swagger not specified.  API descriptive name
+  *      gatewayBasePath     Required if swagger not specified.  API base path
+  *      gatewayPath         Required if swagger not specified.  Specific API path (relative to base path)
+  *      gatewayMethod       Required if swagger not specified.  API path operation
+  *      id                  Optional if swagger not specified.  Unique id of API
+  *      action              Required. if swagger not specified
+  *           name             Required.  Action name (includes package)
+  *           namespace        Required.  Action namespace
+  *           backendMethod    Required.  Action invocation REST verb.  "POST"
+  *           backendUrl       Required.  Action invocation REST url
+  *           authkey          Required.  Action invocation auth key
+  *      swagger             Required if gatewayBasePath not provided.  API swagger JSON
+  *
+  * NOTE: The package containing this action will be bound to the following values:
+  *         gwUrl, gwAuth
+  *       As such, the caller to this action should normally avoid explicitly setting
+  *       these values
+  */
+var utils = require('./utils.js');
+var utils2 = require('./apigw-utils.js');
+
+function main(message) {
+  //console.log('message: '+JSON.stringify(message));  // ONLY FOR TEMPORARY/LOCAL DEBUG; DON'T ENABLE PERMANENTLY
+  var badArgMsg = validateArgs(message);
+  if (badArgMsg) {
+    return Promise.reject(utils2.makeErrorResponseObject(badArgMsg, (message.__ow_method !== undefined)));
+  }
+
+  var gwInfo = {
+    gwUrl: message.gwUrl,
+  };
+
+  // Replace the CLI provided namespace values with the controller provided namespace value
+  // If __ow_user is not set, the namespace values are left alone
+  if (message.accesstoken) {
+    utils2.updateNamespace(message.apidoc, message.__ow_user);
+  } else {
+    utils.updateNamespace(message.apidoc, message.__ow_user);
+  }
+
+  // Set the User-Agent header value
+  if (message.__ow_headers && message.__ow_headers['user-agent']) {
+    utils2.setSubUserAgent(message.__ow_headers['user-agent']);
+  }
+
+  // message.apidoc already validated; creating shortcut to it
+  var doc;
+  if (typeof message.apidoc === 'object') {
+    doc = message.apidoc;
+  } else if (typeof message.apidoc === 'string') {
+    doc = JSON.parse(message.apidoc);
+  }
+
+  // message.swagger already validated; creating object
+  var swaggerObj;
+  if (typeof doc.swagger === 'object') {
+    swaggerObj = doc.swagger;
+  } else if (typeof doc.swagger === 'string') {
+    swaggerObj = JSON.parse(doc.swagger);
+  }
+  doc.swagger = swaggerObj;
+
+  var basepath = getBasePath(doc);
+
+  var tenantInstance = message.tenantInstance || 'openwhisk';
+
+  // This can be invoked as either a standard web action or as a normal action
+  var calledAsWebAction = message.__ow_method !== undefined;
+
+  // Log parameter values
+  console.log('GW URL        : '+message.gwUrl);
+  console.log('GW URL V2     : '+message.gwUrlV2);
+  console.log('GW Auth       : '+utils.confidentialPrint(message.gwPwd));
+  console.log('__ow_user     : '+message.__ow_user);
+  console.log('namespace     : '+doc.namespace);
+  console.log('tenantInstance: '+message.tenantInstance+' / '+tenantInstance);
+  console.log('accesstoken   : '+message.accesstoken);
+  console.log('spaceguid     : '+message.spaceguid);
+  console.log('responsetype  : '+message.responsetype);
+  console.log('API name      : '+doc.apiName);
+  console.log('basepath      : '+basepath);
+  console.log('relpath       : '+doc.gatewayPath);
+  console.log('GW method     : '+doc.gatewayMethod);
+  if (doc.action) {
+    console.log('action name: '+doc.action.name);
+    console.log('action namespace: '+doc.action.namespace);
+    console.log('action backendUrl: '+doc.action.backendUrl);
+    console.log('action backendMethod: '+doc.action.backendMethod);
+    console.log('action authkey: '+utils.confidentialPrint(doc.action.authkey));
+  }
+  console.log('calledAsWebAction: '+calledAsWebAction);
+  console.log('apidoc        :\n'+JSON.stringify(doc));
+
+  // If an API GW access token is provided, use the API GW V2 URL and use this token to auth with the API GW
+  // Otherwise, use the API GW "V1" URL and use the supplied GW auth credentials to auth with the API GW
+  if (message.accesstoken) {
+    var apiDocId;
+    gwInfo.gwUrl = message.gwUrlV2;
+    gwInfo.gwAuth = message.accesstoken;
+    // 1. If an existing API exists for this namespace/basepath combination, retrieve it and update it
+    // 2. If not, create a new API
+    return utils2.getApis(gwInfo, message.spaceguid, basepath)
+    .then(function(endpointDocs) {
+      console.log('Got '+endpointDocs.length+' APIs');
+      if (endpointDocs.length === 0) {
+        console.log('No API found for namespace '+doc.namespace + ' with basePath '+ basepath);
+        return Promise.resolve(utils2.generateBaseSwaggerApi(basepath, doc.apiName));
+      } else {
+        apiDocId = endpointDocs[0].artifact_id;
+        return Promise.resolve(endpointDocs[0].open_api_doc);
+      }
+    })
+    .then(function(endpointDoc) {
+      if (doc.swagger) {
+        console.log('Use provided swagger as the entire API; override any existing API');
+        return Promise.resolve(doc.swagger);
+      } else {
+        console.log('Add the provided API endpoint');
+        return Promise.resolve(utils2.addEndpointToSwaggerApi(endpointDoc, doc, message.responsetype));
+      }
+    })
+    .then(function(apiSwagger) {
+      console.log('Final swagger API config: '+ JSON.stringify(apiSwagger));
+      return utils2.addApiToGateway(gwInfo, message.spaceguid, apiSwagger, apiDocId);
+    })
+    .then(function(gwApi) {
+      console.log('API GW configured with API');
+      var cliApi = utils2.generateCliApiFromGwApi(gwApi).value;
+      console.log('createApi success');
+      return Promise.resolve(utils2.makeResponseObject(cliApi, calledAsWebAction));
+    })
+    .catch(function(reason) {
+      var rejmsg = 'API creation failure: ' + JSON.parse(utils2.makeJsonString(reason)); // Avoid unnecessary JSON escapes
+      console.error(rejmsg);
+      return Promise.reject(utils2.makeErrorResponseObject(rejmsg, calledAsWebAction));
+    });
+  } else {
+    // Create and activate a new API path
+    // 1. Create tenant id this namespace.  If id exists, create is a noop
+    // 2. Obtain any existing configuration for the target API.  If none, this is a new API
+    // 3. Create the API document to send to the API GW.  If API exists, update it
+    // 4. Configure API GW with the new/updated API
+    var tenantId;
+    var gwApiId;
+    if (message.gwUser && message.gwPwd) {
+      gwInfo.gwAuth = Buffer.from(message.gwUser+':'+message.gwPwd,'ascii').toString('base64');
+    }
+    return utils.createTenant(gwInfo, doc.namespace, tenantInstance)
+    .then(function(tenant) {
+      console.log('Got the API GW tenant: '+JSON.stringify(tenant));
+      tenantId = tenant.id;
+      return Promise.resolve(utils.getApis(gwInfo, tenant.id, basepath));
+    })
+    .then(function(apis) {
+      console.log('Got '+apis.length+' APIs');
+      if (apis.length === 0) {
+        console.log('No APIs found for namespace '+doc.namespace+' with basepath '+basepath);
+        return Promise.resolve(utils.generateBaseSwaggerApi(basepath, doc.apiName));
+      } else if (apis.length > 1) {
+        console.error('Multiple APIs found for namespace '+doc.namespace+' with basepath '+basepath);
+        return Promise.reject('Internal error. Multiple APIs found for namespace '+doc.namespace+' with basepath '+basepath);
+      }
+      gwApiId = apis[0].id;
+      return Promise.resolve(utils.generateSwaggerApiFromGwApi(apis[0]));
+    })
+    .then(function(swaggerApi) {
+      if (doc.swagger) {
+        console.log('Use provided swagger as the entire API; override any existing API');
+        return Promise.resolve(doc.swagger);
+      } else {
+        console.log('Add the provided API endpoint');
+        return Promise.resolve(utils.addEndpointToSwaggerApi(swaggerApi, doc));
+      }
+    })
+    .then(function(swaggerApi) {
+      console.log('Final swagger API config: '+ JSON.stringify(swaggerApi));
+      return utils.addApiToGateway(gwInfo, tenantId, swaggerApi, gwApiId);
+    })
+    .then(function(gwApi) {
+      console.log('API GW configured with API');
+      var cliApi = utils.generateCliApiFromGwApi(gwApi).value;
+      console.log('createApi success');
+      return Promise.resolve(utils2.makeResponseObject(cliApi, calledAsWebAction));
+    })
+    .catch(function(reason) {
+      var rejmsg = 'API creation failure: ' + JSON.parse(utils2.makeJsonString(reason)); // Avoid unnecessary JSON escapes
+      console.error(rejmsg);
+      return Promise.reject(utils2.makeErrorResponseObject(rejmsg, calledAsWebAction));
+    });
+  }
+}
+
+function getBasePath(apidoc) {
+  if (apidoc.swagger) {
+    return apidoc.swagger.basePath;
+  }
+  return apidoc.gatewayBasePath;
+}
+
+
+function validateArgs(message) {
+  var tmpdoc;
+  if(!message) {
+    console.error('No message argument!');
+    return 'Internal error.  A message parameter was not supplied.';
+  }
+
+  if (!message.gwUrl && !message.gwUrlV2) {
+    return 'gwUrl is required.';
+  }
+
+  if (!message.__ow_user) {
+    return 'A valid auth key is required.';
+  }
+
+  if(!message.apidoc) {
+    return 'apidoc is required.';
+  }
+  if (typeof message.apidoc == 'object') {
+    tmpdoc = message.apidoc;
+  } else if (typeof message.apidoc === 'string') {
+    try {
+      tmpdoc = JSON.parse(message.apidoc);
+    } catch (e) {
+      return 'apidoc field cannot be parsed. Ensure it is valid JSON.';
+    }
+  } else {
+    return 'apidoc field is of type ' + (typeof message.apidoc) + ' and should be a JSON object or a JSON string.';
+  }
+
+  if (!tmpdoc.namespace) {
+    return 'apidoc is missing the namespace field';
+  }
+
+ var tmpSwaggerDoc;
+  if(tmpdoc.swagger) {
+    if (tmpdoc.gatewayBasePath) {
+      return 'swagger and gatewayBasePath are mutually exclusive and cannot be specified together.';
+    }
+    if (typeof tmpdoc.swagger == 'object') {
+      tmpSwaggerDoc = tmpdoc.swagger;
+    } else if (typeof tmpdoc.swagger === 'string') {
+      try {
+        tmpSwaggerDoc = JSON.parse(tmpdoc.swagger);
+      } catch (e) {
+        return 'swagger field cannot be parsed. Ensure it is valid JSON.';
+      }
+    } else {
+      return 'swagger field is ' + (typeof tmpdoc.swagger) + ' and should be an object or a JSON string.';
+    }
+    console.log('Swagger JSON object: ', tmpSwaggerDoc);
+    if (!tmpSwaggerDoc.basePath) {
+      return 'swagger is missing the basePath field.';
+    }
+    if (!tmpSwaggerDoc.paths) {
+      return 'swagger is missing the paths field.';
+    }
+    if (!tmpSwaggerDoc.info) {
+      return 'swagger is missing the info field.';
+    }
+  } else {
+    if (!tmpdoc.gatewayBasePath) {
+      return 'apidoc is missing the gatewayBasePath field';
+    }
+
+    if (!tmpdoc.gatewayPath) {
+      return 'apidoc is missing the gatewayPath field';
+    }
+
+    if (!tmpdoc.gatewayMethod) {
+      return 'apidoc is missing the gatewayMethod field';
+    }
+
+    if (!tmpdoc.action) {
+      return 'apidoc is missing the action field.';
+    }
+
+    if (!tmpdoc.action.backendMethod) {
+      return 'action is missing the backendMethod field.';
+    }
+
+    if (!tmpdoc.action.backendUrl) {
+      return 'action is missing the backendUrl field.';
+    }
+
+    if (!tmpdoc.action.namespace) {
+      return 'action is missing the namespace field.';
+    }
+
+    if(!tmpdoc.action.name) {
+      return 'action is missing the name field.';
+    }
+
+    if (!tmpdoc.action.authkey) {
+      return 'action is missing the authkey field.';
+    }
+  }
+
+  return '';
+}
+
+module.exports.main = main;
diff --git a/routemgmt/createApi/package.json b/routemgmt/createApi/package.json
new file mode 100644
index 0000000..f8b49b4
--- /dev/null
+++ b/routemgmt/createApi/package.json
@@ -0,0 +1,3 @@
+{
+  "main": "createApi.js"
+}
diff --git a/routemgmt/deleteApi/deleteApi.js b/routemgmt/deleteApi/deleteApi.js
new file mode 100644
index 0000000..421c25b
--- /dev/null
+++ b/routemgmt/deleteApi/deleteApi.js
@@ -0,0 +1,244 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ *
+ * Delete an API Gateway to action mapping document from the database:
+ * https://docs.cloudant.com/document.html#delete
+ *
+ * Parameters (all as fields in the message JSON object)
+ *   gwUrlV2              Required when accesstoken is provided. The V2 API Gateway base path (i.e. http://gw.com)
+ *   gwUrl                Required. The API Gateway base path (i.e. http://gw.com)
+ *   gwUser               Optional. The API Gateway authentication
+ *   gwPwd                Optional. The API Gateway authentication
+ *   __ow_user            Optional. Set to the authenticated API authors's namespace when valid authentication is supplied.
+ *   namespace            Required if __ow_user not specified.  Namespace of API author
+ *   accesstoken          Optional. Dynamic API GW auth.  Overrides gwUser/gwPwd
+ *   spaceguid            Optional. Namespace unique id.
+ *   tenantInstance       Optional. Instance identifier used when creating the specific API GW Tenant
+ *   basepath             Required. Base path or API name of the API
+ *   relpath              Optional. Delete just this relative path from the API.  Required if operation is specified
+ *   operation            Optional. Delete just this relpath's operation from the API.
+ *
+ * NOTE: The package containing this action will be bound to the following values:
+ *         gwUrl, gwAuth
+ *       As such, the caller to this action should normally avoid explicitly setting
+ *       these values
+ **/
+var utils = require('./utils.js');
+var utils2 = require('./apigw-utils.js');
+var _ = require('lodash');
+
+function main(message) {
+  //console.log('message: '+JSON.stringify(message));  // ONLY FOR TEMPORARY/LOCAL DEBUG; DON'T ENABLE PERMANENTLY
+  var badArgMsg = validateArgs(message);
+  if (badArgMsg) {
+    return Promise.reject(utils2.makeErrorResponseObject(badArgMsg, (message.__ow_method != undefined)));
+  }
+
+  var gwInfo = {
+    gwUrl: message.gwUrl,
+  };
+  if (message.gwUser && message.gwPwd) {
+    gwInfo.gwAuth = Buffer.from(message.gwUser+':'+message.gwPwd,'ascii').toString('base64');
+  }
+
+  // Set the User-Agent header value
+  if (message.__ow_headers && message.__ow_headers['user-agent']) {
+    utils2.setSubUserAgent(message.__ow_headers['user-agent']);
+  }
+
+  // Set namespace override if provided
+  message.namespace = message.__ow_user || message.namespace;
+
+  var tenantInstance = message.tenantInstance || 'openwhisk';
+
+  // This can be invoked as either a web action or as a normal action
+  var calledAsWebAction = message.__ow_method !== undefined;
+
+  // Log parameter values
+  console.log('GW URL        : '+message.gwUrl);
+  console.log('GW URL V2     : '+message.gwUrlV2);
+  console.log('GW User       : '+utils.confidentialPrint(message.gwUser));
+  console.log('GW Pwd        : '+utils.confidentialPrint(message.gwPwd));
+  console.log('__ow_user     : '+message.__ow_user);
+  console.log('namespace     : '+message.namespace);
+  console.log('tenantInstance: '+message.tenantInstance+' / '+tenantInstance);
+  console.log('accesstoken   : '+message.accesstoken);
+  console.log('spaceguid     : '+message.spaceguid);
+  console.log('basepath/name : '+message.basepath);
+  console.log('relpath       : '+message.relpath);
+  console.log('operation     : '+message.operation);
+  console.log('calledAsWebAction: '+calledAsWebAction);
+
+  // If no relpath (or relpath/operation) is specified, delete the entire API
+  var deleteEntireApi = !message.relpath;
+
+  if (message.accesstoken) {
+    // Delete an API route
+    // 1. Use the spaceguid and basepath to obtain the API from the API GW
+    // 2. If a relpath or relpath/operation is specified (i.e. delete subset of API)
+    //    a. Remove that section from the API config
+    //    b. Update API GW with updated API config
+    // 3. If relpath or replath/operation is NOT specified (i.e. delete entire API)
+    //    a. Delete entire API from API GW
+    gwInfo.gwUrl = message.gwUrlV2;
+    gwInfo.gwAuth = message.accesstoken;
+
+    return utils2.getApis(gwInfo, message.spaceguid, message.basepath)
+    .then(function(endpointDocs) {
+      console.log('Got '+endpointDocs.length+' APIs');
+      if (endpointDocs.length === 0) {
+        console.log('No API found for namespace '+message.namespace + ' with basePath '+ message.basepath);
+        return Promise.reject('API \''+message.basepath+'\' does not exist.');
+      } else if (endpointDocs.length > 1) {
+        console.error('Multiple APIs found for namespace '+message.namespace+' with basepath/apiname '+message.basepath);
+      }
+      return Promise.resolve(endpointDocs[0]);
+    })
+    .then(function(endpointDoc) {
+      console.log('Got API');
+      if (deleteEntireApi) {
+        console.log('Removing entire API '+message.basepath+' from API GW');
+        return utils2.deleteApiFromGateway(gwInfo, message.spaceguid, endpointDoc.artifact_id);
+      } else {
+        console.log('Removing path '+message.relpath+' with operation '+message.operation+' from API '+message.basepath);
+        var endpointToRemove = {
+          gatewayMethod: message.operation,
+          gatewayPath: message.relpath
+        };
+        var swaggerOrErrMsg = utils2.removeEndpointFromSwaggerApi(endpointDoc.open_api_doc, endpointToRemove);
+        if (typeof swaggerOrErrMsg === 'string' ) {
+          return Promise.reject(swaggerOrErrMsg);
+        }
+        if (_.isEmpty(swaggerOrErrMsg.paths)) {
+          console.log('After path/operation removal, no paths exist in API; so removing entire API '+message.basepath+' from API GW');
+          return utils2.deleteApiFromGateway(gwInfo, message.spaceguid, endpointDoc.artifact_id);
+        }
+        return utils2.addApiToGateway(gwInfo, message.spaceguid, swaggerOrErrMsg, endpointDoc.artifact_id);
+      }
+    })
+    .then(function() {
+      console.log('deleteApi success');
+      return Promise.resolve(utils2.makeResponseObject({}, calledAsWebAction));
+    })
+    .catch(function(reason) {
+        var rejmsg = 'API deletion failure: ' + JSON.parse(utils2.makeJsonString(reason)); // Avoid unnecessary JSON escapes
+        console.error(rejmsg);
+        return Promise.reject(utils2.makeErrorResponseObject(rejmsg, calledAsWebAction));
+    });
+  } else {
+    // Delete an API route
+    // 1. Get the tenant ID associated with the specified namespace and optional tenant instance
+    // 2. Obtain the tenantId/basepath/apiName associated API configuration from the API GW
+    // 3. If a relpath or relpath/operation is specified (i.e. delete subset of API)
+    //    a. Remove that section from the API config
+    //    b. Update API GW with updated API config
+    // 4. If relpath or replath/operation is NOT specified (i.e. delete entire API)
+    //    a. Delete entire API from API GW
+    var tenantId;
+    return utils.getTenants(gwInfo, message.namespace, tenantInstance)
+    .then(function(tenants) {
+      // If a non-empty tenant array was returned, pick the first one from the list
+      if (tenants.length === 0) {
+        console.error('No Tenant found for namespace '+message.namespace);
+        return Promise.reject('No Tenant found for namespace '+message.namespace);
+      } else if (tenants.length > 1 ) {
+        console.error('Multiple tenants found for namespace '+message.namespace+' and tenant instance '+tenantInstance);
+        return Promise.reject('Internal error. Multiple API Gateway tenants found for namespace '+message.namespace+' and tenant instance '+tenantInstance);
+      }
+      console.log('Got a tenant: '+JSON.stringify(tenants[0]));
+      tenantId = tenants[0].id;
+      return Promise.resolve(tenants[0].id);
+    })
+    .then(function(tenantId) {
+      console.log('Got Tenant ID: '+tenantId);
+      return utils.getApis(gwInfo, tenantId, message.basepath);
+    })
+    .then(function(apis) {
+      console.log('Got '+apis.length+' APIs');
+      if (apis.length === 0) {
+        console.log('No APIs found for namespace '+message.namespace+' with basepath/apiname '+message.basepath);
+        return Promise.reject('API \''+message.basepath+'\' does not exist.');
+      } else if (apis.length > 1) {
+        console.error('Multiple APIs found for namespace '+message.namespace+' with basepath/apiname '+message.basepath);
+        Promise.reject('Internal error. Multiple APIs found for namespace '+message.namespace+' with basepath '+message.basepath);
+      }
+      return Promise.resolve(apis[0]);
+    })
+    .then(function(gwApi) {
+      if (deleteEntireApi) {
+        console.log('Removing entire API '+gwApi.basePath+' from API GW');
+        return utils.deleteApiFromGateway(gwInfo, gwApi.id);
+      } else {
+        console.log('Removing path '+message.relpath+'; operation '+message.operation+' from API '+gwApi.basePath);
+        var swaggerApi = utils.generateSwaggerApiFromGwApi(gwApi);
+        var endpoint = {
+          gatewayMethod: message.operation,
+          gatewayPath: message.relpath
+        };
+        var swaggerOrErrMsg = utils.removeEndpointFromSwaggerApi(swaggerApi, endpoint);
+        if (typeof swaggerOrErrMsg === 'string' ) {
+          return Promise.reject(swaggerOrErrMsg);
+        }
+        return utils.addApiToGateway(gwInfo, gwApi.tenantId, swaggerOrErrMsg, gwApi.id);
+      }
+    })
+    .then(function() {
+      console.log('deleteApi success');
+      return Promise.resolve(utils2.makeResponseObject({}, calledAsWebAction));
+    })
+    .catch(function(reason) {
+      var rejmsg = 'API deletion failure: ' + JSON.parse(utils2.makeJsonString(reason)); // Avoid unnecessary JSON escapes
+      console.error(rejmsg);
+      return Promise.reject(utils2.makeErrorResponseObject(rejmsg, calledAsWebAction));
+    });
+  }
+}
+
+
+function validateArgs(message) {
+  var tmpdoc;
+  if(!message) {
+    console.error('No message argument!');
+    return 'Internal error.  A message parameter was not supplied.';
+  }
+
+  if (!message.gwUrl && !message.gwUrlV2) {
+    return 'gwUrl is required.';
+  }
+
+  if (!message.__ow_user && !message.namespace) {
+    return 'Invalid authentication.';
+  }
+
+  if (!message.basepath) {
+    return 'basepath is required.';
+  }
+
+  if (!message.relpath && message.operation) {
+    return 'When specifying an operation, the path is required.';
+  }
+
+  if (message.operation) {
+    message.operation = message.operation.toLowerCase();
+  }
+
+  return '';
+}
+
+module.exports.main = main;
diff --git a/routemgmt/deleteApi/package.json b/routemgmt/deleteApi/package.json
new file mode 100644
index 0000000..5063ac5
--- /dev/null
+++ b/routemgmt/deleteApi/package.json
@@ -0,0 +1,3 @@
+{
+  "main": "deleteApi.js"
+}
diff --git a/routemgmt/getApi/getApi.js b/routemgmt/getApi/getApi.js
new file mode 100644
index 0000000..198b820
--- /dev/null
+++ b/routemgmt/getApi/getApi.js
@@ -0,0 +1,178 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ *
+ * Retrieve API configuration from the API Gateway:
+ *
+ * Parameters (all as fields in the message JSON object)
+ *   gwUrlV2              Required when accesstoken is provided. The V2 API Gateway base path (i.e. http://gw.com)
+ *   gwUrl                Required. The API Gateway base path (i.e. http://gw.com)
+ *   gwUser               Optional. The API Gateway authentication
+ *   gwPwd                Optional. The API Gateway authentication
+ *   __ow_user            Optional. Set to the authenticated API authors's namespace when valid authentication is supplied.
+ *   namespace            Required if __ow_user not specified.  Namespace of API author
+ *   tenantInstance       Optional. Instance identifier used when creating the specific API GW Tenant
+ *   accesstoken          Optional. Dynamic API GW auth.  Overrides gwUser/gwPwd
+ *   spaceguid            Optional. Namespace unique id.
+ *   basepath             Optional. Base path or API name of the API.
+ *                                  If not provided, all APIs for the namespace are returned
+ *   relpath              Optional. Must be defined with 'operation'.  Filters API result to path/operation
+ *   operation            Optional. Must be defined with 'relpath'.  Filters API result to path/operation
+ *   outputFormat         Optional. Defaults to 'swagger'.  Possible values:
+ *                                  'apigw' = return API as obtained from the API Gateway
+ *                                  'swagger' = return API as swagger compliant JSON
+ *
+ * NOTE: The package containing this action will be bound to the following values:
+ *         gwUrl, gwAuth
+ *       As such, the caller to this action should normally avoid explicitly setting
+ *       these values
+ **/
+var utils = require('./utils.js');
+var utils2 = require('./apigw-utils.js');
+
+function main(message) {
+  console.log('message: '+JSON.stringify(message));  // ONLY FOR TEMPORARY/LOCAL DEBUG; DON'T ENABLE PERMANENTLY
+  var badArgMsg = validateArgs(message);
+  if (badArgMsg) {
+    return Promise.reject(utils2.makeErrorResponseObject(badArgMsg, (message.__ow_method !== undefined)));
+  }
+
+  message.outputFormat = message.outputFormat || 'swagger';
+  var tenantInstance = message.tenantInstance || 'openwhisk';
+
+  var gwInfo = {
+    gwUrl: message.gwUrl,
+  };
+  if (message.gwUser && message.gwPwd) {
+    gwInfo.gwAuth = Buffer.from(message.gwUser+':'+message.gwPwd,'ascii').toString('base64');
+  }
+
+  // Set the User-Agent header value
+  if (message.__ow_headers && message.__ow_headers['user-agent']) {
+    utils2.setSubUserAgent(message.__ow_headers['user-agent']);
+  }
+
+  // Set namespace override if provided
+  message.namespace = message.__ow_user || message.namespace;
+
+  // This can be invoked as either web action or as a normal action
+  var calledAsWebAction = message.__ow_method !== undefined;
+
+  // Log parameter values
+  console.log('gwUrl         : '+message.gwUrl);
+  console.log('GW URL V2     : '+message.gwUrlV2);
+  console.log('__ow_user     : '+message.__ow_user);
+  console.log('namespace     : '+message.namespace);
+  console.log('tenantInstance: '+message.tenantInstance+' / '+tenantInstance);
+  console.log('accesstoken   : '+message.accesstoken);
+  console.log('spaceguid     : '+message.spaceguid);
+  console.log('basepath/name : '+message.basepath);
+  console.log('relpath       : '+message.relpath);
+  console.log('operation     : '+message.operation);
+  console.log('outputFormat  : '+message.outputFormat);
+  console.log('calledAsWebAction: '+calledAsWebAction);
+
+  if (message.accesstoken) {
+    gwInfo.gwUrl = message.gwUrlV2;
+    gwInfo.gwAuth = message.accesstoken;
+    // Obtain the API from the API GW
+    return utils2.getApis(gwInfo, message.spaceguid, message.basepath)
+    .then(function(endpointDocs) {
+      console.log('Got '+endpointDocs.length+' APIs');
+      if (endpointDocs.length === 0) {
+        console.log('No API found for namespace '+message.namespace + ' with basePath '+ message.basepath);
+      }
+      var cliApis = utils2.generateCliResponse(endpointDocs);
+      console.log('getApi success');
+      return Promise.resolve(utils2.makeResponseObject({ apis: cliApis }, calledAsWebAction));
+    })
+    .catch(function(reason) {
+      var reasonstr = JSON.parse(utils2.makeJsonString(reason)); // Avoid unnecessary JSON escapes
+      console.error('API GW failure: '+reasonstr);
+      return Promise.reject(utils2.makeErrorResponseObject(reasonstr, calledAsWebAction));
+    });
+  } else {
+    // Issue a request to read API(s) from the API GW
+    // 1. Get the tenant ID associated with the specified namespace and optional tenant instance
+    // 2. Get the API(s) associated with the tenant ID and optional basepath/apiname
+    // 3. Format the API(s) per the outputFormat specification
+    return utils.getTenants(gwInfo, message.namespace, tenantInstance)
+    .then(function(tenants) {
+      // If a non-empty tenant array was returned, pick the first one from the list
+      if (tenants.length === 0) {
+        console.error('No Tenant found for namespace '+message.namespace);
+        return Promise.reject('No Tenant found for namespace '+message.namespace);
+      } else if (tenants.length > 1 ) {
+        console.error('Multiple tenants found for namespace '+message.namespace+' and tenant instance '+tenantInstance);
+        return Promise.reject('Internal error. Multiple API Gateway tenants found for namespace '+message.namespace+' and tenant instance '+tenantInstance);
+      }
+      console.log('Got a tenant: '+JSON.stringify(tenants[0]));
+      return Promise.resolve(tenants[0].id);
+    })
+    .then(function(tenantId) {
+      console.log('Got Tenant ID: '+tenantId);
+      return utils.getApis(gwInfo, tenantId, message.basepath);
+    })
+    .then(function(apis) {
+      console.log('Got API(s)');
+      if (apis.length === 0) {
+        console.error('No APIs found for namespace '+message.namespace);
+      }
+      var cliApis = utils.generateCliResponse(apis);
+      console.log('getApi success');
+      return Promise.resolve(utils2.makeResponseObject({ apis: cliApis }, calledAsWebAction));
+    })
+    .catch(function(reason) {
+      var reasonstr = JSON.parse(utils2.makeJsonString(reason)); // Avoid unnecessary JSON escapes
+      var rejmsg = 'API GW failure: ' + reasonstr;
+      console.error(rejmsg);
+      // Special case handling
+      // If no tenant id found, then just return an empty list of APIs
+      if ( (typeof reason === 'string') && (reason.indexOf('No Tenant found') !== -1) ) {
+        console.log('Namespace has no tenant id yet; returning empty list of APIs');
+        return Promise.resolve(utils2.makeResponseObject({ apis: utils.generateCliResponse([]) }, calledAsWebAction));
+      }
+      return Promise.reject(utils2.makeErrorResponseObject(reasonstr, calledAsWebAction));
+    });
+  }
+
+}
+
+
+function validateArgs(message) {
+  if(!message) {
+    console.error('No message argument!');
+    return 'Internal error. A message parameter was not supplied.';
+  }
+
+  if (!message.gwUrl && !message.gwUrlV2) {
+    return 'gwUrl is required.';
+  }
+
+  if (!message.__ow_user && !message.namespace) {
+    return 'Invalid authentication.';
+  }
+
+  if (message.outputFormat && !(message.outputFormat.toLowerCase() === 'apigw' || message.outputFormat.toLowerCase() === 'swagger')) {
+    return 'Invalid outputFormat value. Valid values are: apigw, swagger';
+  }
+
+  return '';
+}
+
+module.exports.main = main;
diff --git a/routemgmt/getApi/package.json b/routemgmt/getApi/package.json
new file mode 100644
index 0000000..d4ac67d
--- /dev/null
+++ b/routemgmt/getApi/package.json
@@ -0,0 +1,3 @@
+{
+  "main": "getApi.js"
+}
diff --git a/routemgmt/installRouteMgmt.sh b/routemgmt/installRouteMgmt.sh
new file mode 100755
index 0000000..bff48cb
--- /dev/null
+++ b/routemgmt/installRouteMgmt.sh
@@ -0,0 +1,62 @@
+#!/bin/bash
+#
+# use the command line interface to install standard actions deployed
+# automatically
+#
+# To run this command
+# ./installRouteMgmt.sh  <AUTH> <APIHOST> <NAMESPACE> <WSK_CLI>
+# AUTH, APIHOST and NAMESPACE are found in $HOME/.wskprops
+# WSK_CLI="$OPENWHISK_HOME/bin/wsk"
+
+set -e
+set -x
+
+if [ $# -eq 0 ]
+then
+echo "Usage: ./installRouteMgmt.sh AUTHKEY APIHOST NAMESPACE PATH_TO_WSK_CLI APIGW_AUTH_USER APIGW_AUTH_PWD APIGW_HOST_V2 "
+fi
+
+AUTH="$1"
+APIHOST="$2"
+NAMESPACE="$3"
+WSK_CLI="$4"
+
+GW_USER="$5"
+GW_PWD="$6"
+GW_HOST_V2="$7"
+
+# If the auth key file exists, read the key in the file. Otherwise, take the
+# first argument as the key itself.
+if [ -f "$AUTH" ]; then
+    AUTH=`cat $AUTH`
+fi
+
+export WSK_CONFIG_FILE= # override local property file to avoid namespace clashes
+
+echo Installing apimgmt package
+$WSK_CLI -i --apihost "$APIHOST" package update --auth "$AUTH"  --shared no "$NAMESPACE/apimgmt" \
+-a description "This package manages the gateway API configuration." \
+-p gwUser "$GW_USER" \
+-p gwPwd "$GW_PWD" \
+-p gwUrlV2 "$GW_HOST_V2"
+
+echo Creating NPM module .zip files
+zip -j "getApi/getApi.zip" "getApi/getApi.js" "getApi/package.json" "common/utils.js" "common/apigw-utils.js"
+zip -j "createApi/createApi.zip" "createApi/createApi.js" "createApi/package.json" "common/utils.js" "common/apigw-utils.js"
+zip -j "deleteApi/deleteApi.zip" "deleteApi/deleteApi.js" "deleteApi/package.json" "common/utils.js" "common/apigw-utils.js"
+
+echo Installing apimgmt actions
+$WSK_CLI -i --apihost "$APIHOST" action update --auth "$AUTH" "$NAMESPACE/apimgmt/getApi" "getApi/getApi.zip" \
+-a description 'Retrieve the specified API configuration (in JSON format)' \
+--kind nodejs:default \
+-a web-export true -a final true
+
+$WSK_CLI -i --apihost "$APIHOST" action update --auth "$AUTH" "$NAMESPACE/apimgmt/createApi" "createApi/createApi.zip" \
+-a description 'Create an API' \
+--kind nodejs:default \
+-a web-export true -a final true
+
+$WSK_CLI -i --apihost "$APIHOST" action update --auth "$AUTH" "$NAMESPACE/apimgmt/deleteApi" "deleteApi/deleteApi.zip" \
+-a description 'Delete the API' \
+--kind nodejs:default \
+-a web-export true -a final true
diff --git a/routemgmt/uninstallRouteMgmt.sh b/routemgmt/uninstallRouteMgmt.sh
new file mode 100755
index 0000000..d2267ee
--- /dev/null
+++ b/routemgmt/uninstallRouteMgmt.sh
@@ -0,0 +1,75 @@
+#!/bin/bash
+#
+# use the command line interface to install standard actions deployed
+# automatically
+#
+# To run this command
+# ./installRouteMgmt.sh  <AUTH> <APIHOST> <NAMESPACE> <WSK_CLI>
+# AUTH, APIHOST and NAMESPACE are found in $HOME/.wskprops
+# WSK_CLI="$OPENWHISK_HOME/bin/wsk"
+
+set -e
+set -x
+
+if [ $# -eq 0 ]
+then
+echo "Usage: ./uninstallRouteMgmt.sh AUTHKEY APIHOST NAMESPACE PATH_TO_WSK_CLI"
+fi
+
+AUTH="$1"
+APIHOST="$2"
+NAMESPACE="$3"
+WSK_CLI="$4"
+
+# If the auth key file exists, read the key in the file. Otherwise, take the
+# first argument as the key itself.
+if [ -f "$AUTH" ]; then
+    AUTH=`cat $AUTH`
+fi
+
+export WSK_CONFIG_FILE= # override local property file to avoid namespace clashes
+
+function deleteAction
+{
+  # The "get" command will fail if the resource does not exist, so use "set +e" to avoid exiting the script
+  set +e
+  $WSK_CLI -i --apihost "$APIHOST" action get --auth "$AUTH" "$1"
+  RC=$?
+  if [ $RC -eq 0 ]
+  then
+    set -e
+    $WSK_CLI -i --apihost "$APIHOST" action delete --auth "$AUTH" "$1"
+  fi
+  set -e
+}
+
+function deletePackage
+{
+  # The "get" command will fail if the resource does not exist, so use "set +e" to avoid exiting the script
+  set +e
+  $WSK_CLI -i --apihost "$APIHOST" package get --auth "$AUTH" "$1" -s
+  RC=$?
+  if [ $RC -eq 0 ]
+  then
+    set -e
+    $WSK_CLI -i --apihost "$APIHOST" package delete --auth "$AUTH" "$1"
+  fi
+}
+
+# Delete actions, then the package.  The order is important (can't delete a package that contains an action)!
+
+echo Deleting routemgmt actions
+deleteAction $NAMESPACE/routemgmt/getApi
+deleteAction $NAMESPACE/routemgmt/createApi
+deleteAction $NAMESPACE/routemgmt/deleteApi
+
+echo Deleting routemgmt package - but only if it exists
+deletePackage $NAMESPACE/routemgmt
+
+echo Deleting apimgmt actions
+deleteAction $NAMESPACE/apimgmt/getApi
+deleteAction $NAMESPACE/apimgmt/createApi
+deleteAction $NAMESPACE/apimgmt/deleteApi
+
+echo Deleting apimgmt package - but only if it exists
+deletePackage $NAMESPACE/apimgmt
diff --git a/tools/travis/build.sh b/tools/travis/build.sh
index 114516c..8b5dbaf 100755
--- a/tools/travis/build.sh
+++ b/tools/travis/build.sh
@@ -50,7 +50,9 @@ export OPENWHISK_HOME=$WHISKDIR
 # Tests
 cd $WHISKDIR
 cat whisk.properties
-WSK_TESTS_DEPS_EXCLUDE="-x :core:swift3Action:distDocker -x :core:pythonAction:distDocker -x :core:javaAction:distDocker -x :core:nodejsAction:distDocker -x :core:actionProxy:distDocker -x :sdk:docker:distDocker -x :core:python2Action:distDocker -x :tests:dat:blackbox:badaction:distDocker -x :tests:dat:blackbox:badproxy:distDocker"
+
+WSK_TESTS_DEPS_EXCLUDE="-x :actionRuntimes:swift3Action:distDocker -x :actionRuntimes:pythonAction:distDocker -x :actionRuntimes:javaAction:distDocker -x :actionRuntimes:nodejsAction:distDocker -x :actionRuntimes:actionProxy:distDocker -x :sdk:docker:distDocker -x :actionRuntimes:python2Action:distDocker -x :tests:dat:blackbox:badaction:distDocker -x :tests:dat:blackbox:badproxy:distDocker"
+
 TERM=dumb ./gradlew tests:test --tests apigw.healthtests.* ${WSK_TESTS_DEPS_EXCLUDE}
 sleep 60
 TERM=dumb ./gradlew tests:test --tests whisk.core.apigw.* ${WSK_TESTS_DEPS_EXCLUDE}


 

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
users@infra.apache.org


With regards,
Apache Git Services