22 Feb 2019

Conversational UI with Oracle Digital Assistant and Fn Project. Part III. Moving to the cloud.

In this post I am going to continue the story of implementing a conversational UI for FlexDeploy on top of Oracle Digital Assistant and Fn Project. Today I am going to move the serverless API working around my chatbot to the cloud, so the entire solution is working in the cloud:



The API is implemented as a set of Fn functions collected into an Fn application. The beauty of Fn is that it's just a bunch of Docker containers that can equally run on your laptop on your local Docker engine and somewhere in the cloud. Having said that I can run my Fn application on a K8s cluster from any cloud provider as it is described here. But today is not that day. Today I am going to run my serverless API on a brand new cloud service Oracle Functions which is built on top of Fn. The service is not general available yet, but I participate in the Limited Availability program so I have a trial access to it, I can play with it and blog about it. In this solution I had to get rid of the Fn Flow implemented here and get back to my original implementation as Fn Flow is not supported by Oracle Functions yet. I hope it will be soon as this is actually the best part.

So, having our OCI environment configured and having Oracle Functions service up and running (I am not reposting Oracle tutorial on that here), we need to configure our Fn CLI to be able to communicate with the service:
fn create context oracle_fn --provider oracle 
fn use context oracle_fn
fn update context oracle.compartment-id MY_COMPARTMENT_ID
fn update context api-url https://functions.us-phoenix-1.oraclecloud.com
fn update context registry phx.ocir.io/flexagonoraclecloud/flexagon-repo
fn update context oracle.profile oracle_fn

Ok, so now our Fn command line interface is talking to Oracle Functions. The next step is to create an application in the Oracle Functions console:




Now we can deploy the Fn application to Oracle Functions:
Eugenes-MacBook-Pro-3:fn fedor$ ls -l
total 8
-rw-r--r--@ 1 fedor  staff   12 Dec  4 15:41 app.yaml
drwxr-xr-x  5 fedor  staff  160 Feb  9 15:24 createsnapshotfn
drwxr-xr-x  6 fedor  staff  192 Feb  9 15:25 receiveFromBotFn
drwxr-xr-x  6 fedor  staff  192 Feb  9 15:25 sendToBotFn
Eugenes-MacBook-Pro-3:fn fedor$ 
Eugenes-MacBook-Pro-3:fn fedor$ 
Eugenes-MacBook-Pro-3:fn fedor$ fn deploy --all 
Having done that we can observe the application in the Oracle Functions console:



The next step is to update API urls in the chatbot and on my laptop so the functions in the cloud are invoked instead of the previous local implementation. The urls can be retrieved with the following command:
fn list triggers odaapp
So far the migration from my laptop to Oracle Functions has been looking pretty nice and easy. But here is a little of pain. In order to invoke functions hosted in Oracle Functions with http requests, the requests should be signed so they can pass through the authentication. A node.js implementation of invoking a signed function call looks like this:
var fs = require('fs');
var https = require('https');
var os = require('os');
var httpSignature = require('http-signature');
var jsSHA = require("jssha");

var tenancyId = "ocid1.tenancy.oc1..aaaaaaaayonz5yhpr4vxqpbdof5rn7x5pfrlgjwjycwxasf4dkexiq";
var authUserId = "ocid1.user.oc1..aaaaaaaava2e3wd3cu6lew2sktd6by5hnz3d7prpgjho4oambterba";
var keyFingerprint = "88:3e:71:bb:a5:ea:68:b7:56:fa:3e:5d:ea:45:60:10";
var privateKeyPath = "/Users/fedor/.oci/functions_open.pem";
var privateKey = fs.readFileSync(privateKeyPath, 'ascii');
var identityDomain = "identity.us-ashburn-1.oraclecloud.com";


function sign(request, options) {
    var apiKeyId = options.tenancyId + "/" + options.userId + "/" + options.keyFingerprint;

    var headersToSign = [
        "host",
        "date",
        "(request-target)"
    ];

    var methodsThatRequireExtraHeaders = ["POST", "PUT"];

    if(methodsThatRequireExtraHeaders.indexOf(request.method.toUpperCase()) !== -1) {
        options.body = options.body || "";
        var shaObj = new jsSHA("SHA-256", "TEXT");
        shaObj.update(options.body);

        request.setHeader("Content-Length", options.body.length);
        request.setHeader("x-content-sha256", shaObj.getHash('B64'));

        headersToSign = headersToSign.concat([
            "content-type",
            "content-length",
            "x-content-sha256"
        ]);
    }


    httpSignature.sign(request, {
        key: options.privateKey,
        keyId: apiKeyId,
        headers: headersToSign
    });

    var newAuthHeaderValue = request.getHeader("Authorization").replace("Signature ", "Signature version=\"1\",");
    request.setHeader("Authorization", newAuthHeaderValue);
}


function handleRequest(callback) {

    return function(response) {
        var responseBody = "";
        response.on('data', function(chunk) {
        responseBody += chunk;
    });


        response.on('end', function() {
            callback(JSON.parse(responseBody));
        });
    }
}


function createSnapshot(release) {

    var body = release;

    var options = {
        host: 'af4qyj7yhva.us-phoenix-1.functions.oci.oraclecloud.com',
        path: '/t/createsnapshotfn',
        method: 'POST',
        headers: {
            "Content-Type": "application/text",
        }
    };


    var request = https.request(options, handleRequest(function(data) {
        console.log(data);
    }));


    sign(request, {
        body: body,
        privateKey: privateKey,
        keyFingerprint: keyFingerprint,
        tenancyId: tenancyId,
        userId: authUserId
    });

    request.end(body);
};

This approach should be used by Oracle Digital Assistant custom components and by the listener component on my laptop while invoking the serverless API hosted in Oracle Functions.



That's it!