Add support for IAM based Cloudant DB instances (#184)

diff --git a/.gitignore b/.gitignore
index 4891796..009fe45 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,7 @@
 actions/event-actions/package.json
 .idea
 *.iml
+package-lock.json
 
 # Eclipse
 bin/
diff --git a/Dockerfile b/Dockerfile
index 2df3aac..e5e8a21 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,17 +1,4 @@
-FROM ubuntu:14.04
-
-ENV DEBIAN_FRONTEND noninteractive
-
-# Initial update and some basics.
-# This odd double update seems necessary to get curl to download without 404 errors.
-RUN apt-get update --fix-missing && \
-  apt-get install -y wget && \
-  apt-get update && \
-  apt-get install -y curl && \
-  apt-get update && \
-  apt-get remove -y nodejs && \
-  curl -sL https://deb.nodesource.com/setup_8.x | bash - && \
-  apt-get install -y nodejs
+FROM node:8.12.0
 
 # only package.json
 ADD package.json /
@@ -23,4 +10,4 @@
 EXPOSE 8080
 
 # Run the app
-CMD ["/bin/bash", "-c", "node /cloudantTrigger/app.js >> /logs/cloudantTrigger_logs.log 2>&1"]
+CMD ["/bin/bash", "-c", "node /cloudantTrigger/app.js"]
diff --git a/actions/event-actions/changesWebAction.js b/actions/event-actions/changesWebAction.js
index af2b0e8..1e171ea 100644
--- a/actions/event-actions/changesWebAction.js
+++ b/actions/event-actions/changesWebAction.js
@@ -29,11 +29,10 @@
         if (!params.host) {
             return common.sendError(400, 'cloudant trigger feed: missing host parameter');
         }
-        if (!params.username) {
-            return common.sendError(400, 'cloudant trigger feed: missing username parameter');
-        }
-        if (!params.password) {
-            return common.sendError(400, 'cloudant trigger feed: missing password parameter');
+        if (!params.iamApiKey) {
+            if (!params.username || !params.password) {
+                return common.sendError(400, 'cloudant trigger feed: Must specify parameter/s of iamApiKey or username/password');
+            }
         }
 
         var query_params;
@@ -56,30 +55,34 @@
             return common.sendError(400, 'The query_params parameter is only allowed if the filter parameter is defined');
         }
 
-        var newTrigger = {
-            id: triggerID,
-            host: params.host,
-            port: params.port,
-            protocol: params.protocol || 'https',
-            dbname: params.dbname,
-            user: params.username,
-            pass: params.password,
-            apikey: triggerData.apikey,
-            since: params.since,
-            maxTriggers: params.maxTriggers || -1,
-            filter: params.filter,
-            query_params: query_params,
-            status: {
-                'active': true,
-                'dateChanged': Date.now()
-            },
-            additionalData: triggerData.additionalData
-        };
-
         return new Promise(function (resolve, reject) {
+            var newTrigger;
+
             common.verifyTriggerAuth(triggerData, false)
             .then(() => {
                 db = new Database(params.DB_URL, params.DB_NAME);
+
+                newTrigger = {
+                    id: triggerID,
+                    host: params.host,
+                    port: params.port,
+                    protocol: params.protocol || 'https',
+                    dbname: params.dbname,
+                    user: params.username,
+                    pass: params.password,
+                    apikey: triggerData.apikey,
+                    since: params.since,
+                    maxTriggers: params.maxTriggers || -1,
+                    filter: params.filter,
+                    query_params: query_params,
+                    status: {
+                        'active': true,
+                        'dateChanged': Date.now()
+                    },
+                    additionalData: triggerData.additionalData,
+                    iamApiKey: params.iamApiKey,
+                    iamUrl: params.iamUrl || 'https://iam.bluemix.net/identity/token'
+                };
                 return verifyUserDB(newTrigger);
             })
             .then(() => {
@@ -124,6 +127,8 @@
                         since: doc.since,
                         filter: doc.filter,
                         query_params: doc.query_params,
+                        iamApiKey: doc.iamApiKey,
+                        iamUrl: doc.iamUrl
                     },
                     status: {
                         active: doc.status.active,
@@ -242,17 +247,28 @@
 }
 
 function verifyUserDB(triggerObj) {
-    var dbURL = `${triggerObj.protocol}://${triggerObj.user}:${triggerObj.pass}@${triggerObj.host}`;
 
-    // add port if specified
-    if (triggerObj.port) {
-        dbURL += ':' + triggerObj.port;
+    var Cloudant = require('@cloudant/cloudant');
+    var cloudant;
+
+    if (triggerObj.iamApiKey) {
+        var dbURL = `${triggerObj.protocol}://${triggerObj.host}`;
+        if (triggerObj.port) {
+            dbURL += ':' + triggerObj.port;
+        }
+        cloudant = new Cloudant({ url: dbURL, plugins: { iamauth: { iamApiKey: triggerObj.iamApiKey, iamTokenUrl: triggerObj.iamUrl } } });
+    }
+    else {
+        var url = `${triggerObj.protocol}://${triggerObj.user}:${triggerObj.pass}@${triggerObj.host}`;
+        if (triggerObj.port) {
+            url += ':' + triggerObj.port;
+        }
+        cloudant = Cloudant(url);
     }
 
     return new Promise(function(resolve, reject) {
         try {
-            var nanoConnection = require('nano')(dbURL);
-            var userDB = nanoConnection.use(triggerObj.dbname);
+            var userDB = cloudant.use(triggerObj.dbname);
             userDB.info(function(err, body) {
                 if (!err) {
                     resolve();
diff --git a/actions/event-actions/changesWeb_package.json b/actions/event-actions/changesWeb_package.json
index f2b3fc6..5c46d8d 100644
--- a/actions/event-actions/changesWeb_package.json
+++ b/actions/event-actions/changesWeb_package.json
@@ -1,5 +1,8 @@
 {
   "name": "changesWebAction",
   "version": "1.0.0",
-  "main": "changesWebAction.js"
+  "main": "changesWebAction.js",
+  "dependencies" : {
+    "@cloudant/cloudant": "3.0.0"
+  }
 }
diff --git a/actions/event-actions/lib/Database.js b/actions/event-actions/lib/Database.js
index 962b2df..9bc180d 100644
--- a/actions/event-actions/lib/Database.js
+++ b/actions/event-actions/lib/Database.js
@@ -2,8 +2,8 @@
 
 // constructor for DB object - a thin, promise-loving wrapper around nano
 module.exports = function(dbURL, dbName) {
-    var nano = require('nano')(dbURL);
-    this.db = nano.db.use(dbName);
+    var cloudant = require('@cloudant/cloudant')(dbURL);
+    this.db = cloudant.db.use(dbName);
     var utilsDB = this;
 
     this.getWorkerID = function(availabeWorkers) {
diff --git a/installCatalog.sh b/installCatalog.sh
index 7ce6d5a..7787ec7 100755
--- a/installCatalog.sh
+++ b/installCatalog.sh
@@ -51,12 +51,8 @@
 
 $WSK_CLI -i --apihost "$EDGEHOST" package update --auth "$AUTH" --shared yes cloudant \
     -a description "Cloudant database service" \
-    -a parameters '[ {"name":"bluemixServiceName", "required":false, "bindTime":true}, {"name":"username", "required":true, "bindTime":true, "description": "Your Cloudant username"}, {"name":"password", "required":true, "type":"password", "bindTime":true, "description": "Your Cloudant password"}, {"name":"host", "required":true, "bindTime":true, "description": "This is usually your username.cloudant.com"}, {"name":"dbname", "required":false, "description": "The name of your Cloudant database"}, {"name":"overwrite", "required":false, "type": "boolean"} ]' \
+    -a parameters '[  {"name":"bluemixServiceName", "required":false, "bindTime":true}, {"name":"username", "required":false, "bindTime":true, "description": "Your Cloudant username"}, {"name":"password", "required":false, "type":"password", "bindTime":true, "description": "Your Cloudant password"}, {"name":"host", "required":true, "bindTime":true, "description": "This is usually your username.cloudant.com"}, {"name":"iamApiKey", "required":false}, {"name":"iamUrl", "required":false}, {"name":"dbname", "required":false, "description": "The name of your Cloudant database"}, {"name":"overwrite", "required":false, "type": "boolean"} ]' \
     -p bluemixServiceName 'cloudantNoSQLDB' \
-    -p host '' \
-    -p username '' \
-    -p password '' \
-    -p dbname '' \
     -p apihost "$APIHOST"
 
 # make changesFeed.zip
@@ -73,7 +69,7 @@
     -t 90000 \
     -a feed true \
     -a description 'Database change feed' \
-    -a parameters '[ {"name":"dbname", "required":true, "updatable":false}, {"name": "filter", "required":false, "updatable":true, "type": "string", "description": "The name of your Cloudant database filter"}, {"name": "query_params", "required":false, "updatable":true, "description": "JSON Object containing query parameters that are passed to the filter"} ]' \
+    -a parameters '[ {"name":"dbname", "required":true, "updatable":false}, {"name":"iamApiKey", "required":false, "updatable":false}, {"name":"iamUrl", "required":false, "updatable":false}, {"name": "filter", "required":false, "updatable":true, "type": "string", "description": "The name of your Cloudant database filter"}, {"name": "query_params", "required":false, "updatable":true, "description": "JSON Object containing query parameters that are passed to the filter"} ]' \
     -a sampleInput '{ "dbname": "mydb", "filter": "mailbox/by_status", "query_params": {"status": "new"} }'
 
 COMMAND=" -i --apihost $EDGEHOST package update --auth $AUTH --shared no cloudantWeb \
@@ -89,12 +85,13 @@
 
 # make changesWebAction.zip
 cp -f changesWeb_package.json package.json
+npm install
 
 if [ -e changesWebAction.zip ]; then
     rm -rf changesWebAction.zip
 fi
 
-zip -r changesWebAction.zip lib package.json changesWebAction.js
+zip -r changesWebAction.zip lib package.json changesWebAction.js node_modules
 
 $WSK_CLI -i --apihost "$EDGEHOST" action update --kind "$ACTION_RUNTIME_VERSION" --auth "$AUTH" cloudantWeb/changesWebAction "$PACKAGE_HOME/actions/event-actions/changesWebAction.zip" \
     -a description 'Create/Delete a trigger in cloudant provider Database' \
diff --git a/package.json b/package.json
index 2b8ecb0..6214f12 100644
--- a/package.json
+++ b/package.json
@@ -12,12 +12,12 @@
     "moment": "^2.11.1",
     "lodash": "^3.10.1",
     "request": "^2.83.0",
-    "cloudant-nano": "6.7.0",
+    "@cloudant/cloudant": "3.0.0",
     "json-stringify-safe": "^5.0.1",
     "http-status-codes": "^1.0.5",
     "request-promise": "^1.0.2",
-    "redis":"^2.7.1",
+    "redis": "^2.7.1",
     "bluebird": "^3.5.0",
     "systeminformation": "^3.19.0"
   }
-}
\ No newline at end of file
+}
diff --git a/provider/app.js b/provider/app.js
index fc9149d..2488931 100644
--- a/provider/app.js
+++ b/provider/app.js
@@ -49,11 +49,11 @@
     var method = 'createDatabase';
     logger.info(method, 'creating the trigger database');
 
-    var nano = require('cloudant-nano')(dbProtocol + '://' + dbUsername + ':' + dbPassword + '@' + dbHost);
+    var cloudant = require('@cloudant/cloudant')(dbProtocol + '://' + dbUsername + ':' + dbPassword + '@' + dbHost);
 
-    if (nano !== null) {
+    if (cloudant !== null) {
         return new Promise(function (resolve, reject) {
-            nano.db.create(databaseName, function (err, body) {
+            cloudant.db.create(databaseName, function (err, body) {
                 if (!err) {
                     logger.info(method, 'created trigger database:', databaseName);
                 }
@@ -74,7 +74,7 @@
                     }
                 };
 
-                createDesignDoc(nano.db.use(databaseName), viewDDName, viewDD)
+                createDesignDoc(cloudant.db.use(databaseName), viewDDName, viewDD)
                 .then(db => {
                     var filterDD = {
                         filters: {
@@ -114,7 +114,7 @@
         });
     }
     else {
-        Promise.reject('nano provider did not get created.  check db URL: ' + dbHost);
+        Promise.reject('cloudant provider did not get created.  check db URL: ' + dbHost);
     }
 }
 
@@ -181,7 +181,7 @@
 // Initialize the Provider Server
 function init(server) {
     var method = 'init';
-    var nanoDb;
+    var cloudantDb;
     var providerUtils;
 
     if (server !== null) {
@@ -194,11 +194,11 @@
 
     createDatabase()
     .then(db => {
-        nanoDb = db;
+        cloudantDb = db;
         return createRedisClient();
     })
     .then(client => {
-        providerUtils = new ProviderUtils(logger, nanoDb, client);
+        providerUtils = new ProviderUtils(logger, cloudantDb, client);
         return providerUtils.initRedis();
     })
     .then(() => {
diff --git a/provider/lib/utils.js b/provider/lib/utils.js
index 4f08530..aa12c6d 100644
--- a/provider/lib/utils.js
+++ b/provider/lib/utils.js
@@ -30,17 +30,26 @@
     this.createTrigger = function(triggerData) {
         var method = 'createTrigger';
 
-        // both couch and cloudant should have their URLs in the username:password@host format
-        var dbURL = `${triggerData.protocol}://${triggerData.user}:${triggerData.pass}@${triggerData.host}`;
+        var Cloudant = require('@cloudant/cloudant');
+        var cloudantConnection;
 
-        // add port if specified
-        if (triggerData.port) {
-            dbURL += ':' + triggerData.port;
+        if (triggerData.iamApiKey) {
+            var dbURL = `${triggerData.protocol}://${triggerData.host}`;
+            if (triggerData.port) {
+                dbURL += ':' + triggerData.port;
+            }
+            cloudantConnection = new Cloudant({ url: dbURL, plugins: { iamauth: { iamApiKey: triggerData.iamApiKey, iamTokenUrl: triggerData.iamUrl } } });
+        }
+        else {
+            var url = `${triggerData.protocol}://${triggerData.user}:${triggerData.pass}@${triggerData.host}`;
+            if (triggerData.port) {
+                url += ':' + triggerData.port;
+            }
+            cloudantConnection = Cloudant(url);
         }
 
         try {
-            var nanoConnection = require('cloudant-nano')(dbURL);
-            var triggeredDB = nanoConnection.use(triggerData.dbname);
+            var triggeredDB = cloudantConnection.use(triggerData.dbname);
 
             // Listen for changes on this database.
             var feed = triggeredDB.follow({since: triggerData.since, include_docs: false});
@@ -107,7 +116,9 @@
             triggersLeft: maxTriggers,
             filter: newTrigger.filter,
             query_params: newTrigger.query_params,
-            additionalData: newTrigger.additionalData
+            additionalData: newTrigger.additionalData,
+            iamApiKey: newTrigger.iamApiKey,
+            iamUrl: newTrigger.iamUrl
         };
 
         return trigger;
diff --git a/tests/src/test/scala/system/packages/CloudantFeedTests.scala b/tests/src/test/scala/system/packages/CloudantFeedTests.scala
index 00c370a..7e70d8c 100644
--- a/tests/src/test/scala/system/packages/CloudantFeedTests.scala
+++ b/tests/src/test/scala/system/packages/CloudantFeedTests.scala
@@ -132,7 +132,7 @@
                         "host" -> myCloudantCreds.host().toJson),
                         expectedExitCode = 246)
             }
-            feedCreationResult.stderr should include("cloudant trigger feed: missing password parameter")
+            feedCreationResult.stderr should include("cloudant trigger feed: Must specify parameter/s of iamApiKey or username/password")
 
     }
 
@@ -163,7 +163,7 @@
                         "host" -> myCloudantCreds.host().toJson),
                         expectedExitCode = 246)
             }
-            feedCreationResult.stderr should include("cloudant trigger feed: missing username parameter")
+            feedCreationResult.stderr should include("cloudant trigger feed: Must specify parameter/s of iamApiKey or username/password")
 
     }
 
diff --git a/tests/src/test/scala/system/packages/CloudantFeedWebTests.scala b/tests/src/test/scala/system/packages/CloudantFeedWebTests.scala
index da9d518..499e61a 100644
--- a/tests/src/test/scala/system/packages/CloudantFeedWebTests.scala
+++ b/tests/src/test/scala/system/packages/CloudantFeedWebTests.scala
@@ -70,13 +70,13 @@
     it should "reject post of a trigger due to missing username argument" in {
         val params = JsObject(requiredParams.fields - "username")
 
-        makePostCallWithExpectedResult(params, JsObject("error" -> JsString("cloudant trigger feed: missing username parameter")), 400)
+        makePostCallWithExpectedResult(params, JsObject("error" -> JsString("cloudant trigger feed: Must specify parameter/s of iamApiKey or username/password")), 400)
     }
 
     it should "reject post of a trigger due to missing password argument" in {
         val params = JsObject(requiredParams.fields - "password")
 
-        makePostCallWithExpectedResult(params, JsObject("error" -> JsString("cloudant trigger feed: missing password parameter")), 400)
+        makePostCallWithExpectedResult(params, JsObject("error" -> JsString("cloudant trigger feed: Must specify parameter/s of iamApiKey or username/password")), 400)
     }
 
     it should "reject post of a trigger due to missing dbname argument" in {