First steps in #serverless with fnproject.io

Ralf Mueller
Sep 10, 2018 · 11 min read

Overview

Setup

> brew install fn (Mac OSX)
or
> curl -LSs https://raw.githubusercontent.com/fnproject/cli/master/install | sh (Linux)
db:
image: "postgres"
restart: always
networks:
- fn-network
ports:
- "5432:5432"
environment:
- "POSTGRES_PASSWORD=welcome1"
volumes:
- ./data/postgres:/var/lib/postgresql/data
FN_DB_URL: "postgres://postgres:welcome1@db:5432/funcs?sslmode=disable"
docker-compose -f docker-compose-postgres.yml up
docker run -it --rm --link func-postgres:postgres postgres \
psql -h postgres -U postgres -c "CREATE DATABASE funcs;"
docker run -it --rm --link func-postgres:postgres postgres \
psql -h postgres -U postgres -c 'GRANT ALL PRIVILEGES ON DATABASE funcs TO postgres;'

DMN

Promotion Decision Table
PromotionDecisionService URL and request/response payloads.
http://localhost:8088/bpm/api/4.0/dmn/spaces/My%20Space/decision-models/PromotionDecider/versions/1.0/definition/decision-services/PromotionDecisionService/

Testing Decisions

Dialog for testing a Decision Model
Result of Decision Model Test

Fn Function

Importing packages

package mainimport (
"bytes"
"context"
"encoding/json"
"io"
"io/ioutil"
"log"
"net/http"
"github.com/google/uuid"
// Import Fn Go FDK and cloudevents.io support
cle "github.com/fnproject/cloudevent"
fdk "github.com/fnproject/fdk-go"
)

Function Handler

// main function calls fdk.Handle function
func main() {
fdk.Handle(fdk.HandlerFunc(withError))
}
func withError(ctx context.Context, in io.Reader, out io.Writer){
err := myHandler(ctx, in, out)
if err != nil {
log.Println("unable to decode STDIN: ", err.Error())
fdk.WriteStatus(out, http.StatusInternalServerError)
out.Write([]byte(err.Error()))
return
}
}

Creating a CloudEvent from the function input

func myHandler(ctx context.Context, in io.Reader, out io.Writer) error {
var ce cle.CloudEvent
err := json.NewDecoder(in).Decode(&ce)
if err != nil {
return err
}
var data = ce.Data

Calling into DMN REST Service

// Get the Decision Service URL from the Fn Config
url := fdk.Context(ctx).Config["DMN_API_URL"]
// Marshal the data to JSON and prepare HTTP POST call
jsonData, _ := json.Marshal(data)
req, err := http.NewRequest("POST", url, bytes.NewBuffer([]byte(string(jsonData))))
req.Header.Set("Content-Type", "application/json")
// Call into DMN Microservice and handle error
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
// Get the result body and signal FDK that the call was successful
body, _ := ioutil.ReadAll(resp.Body)
fdk.WriteStatus(out, http.StatusOK)
// Unmarshal response into interface{}...
var f interface{}
err = json.Unmarshal(body, &f)
if err != nil {
return err
}
// ... and cast to map
m := f.(map[string]interface{})
// Get the interpretation of the result and cast to map again
imap := m["interpretation"].(map[string]interface{})
fn config app <app name> <key> <value>
Edit Source dialog in Fn UI.

Creating the result CloudEvent

// Create a new CloudEvent
var cer cle.CloudEvent
// Create an UUID and assign to the EventID of result CloudEvent
uuidb, _ := uuid.New().MarshalText()
cer.EventID = string(uuidb)
// Copy some data from the incoming CloudEvent
cer.CloudEventsVersion = ce.CloudEventsVersion
cer.ContentType = ce.ContentType
cer.EventTime = ce.EventTime
cer.EventType = "com.oracle.oic.myaction"
cer.EventTypeVersion = "1.0"
cer.Extensions = ce.Extensions
// The result Data will be the output of the Decision Service
cer.Data = imap

The func.yaml file

schema_version: 20180708
name: customerpromotion
version: 0.0.1
runtime: go
entrypoint: ./func
format: json

Putting it all together

fn --verbose deploy --app customerpromotionapp --registry phx.ocir.io/oicpaas1/ralmuell/fn
fn create route customerpromotionrapp /customerpromotion customerpromotion:0.0.1

Testing

{
"eventType":"com.oracle.oic.example",
"eventTypeVersion":"1.0",
"cloudEventsVersion":"1.0",
"source":"TBD-Source",
"eventID":"265b5c2b-cd0a-460a-9994-1131058946ba",
"eventTime":"2018-08-30T14:25:17Z",
"contentType":"application/json",
"extensions":{
"destination" : {
"function" : "customerpromotionapp/customerpromotion"
}
},
"data": {
"customerStatus" : "GOLD",
"salesAmount" : 10000,
"region" : "US"
}
}
> cat ce.json | fn call customerpromotionapp /customerpromotion
{
"eventType":"com.oracle.oic.myaction",
"eventTypeVersion":"1.0",
"cloudEventsVersion":"1.0",
"source":"TBD-Source",
"eventID":"a509de1a-d0dd-4a6c-aa8b-4ca1da1b86e4",
"eventTime":"2018-08-30T14:25:17Z",
"contentType":"application/json",
"extensions":{
"destination" : {
"function" : "customerpromotionapp/customerpromotion"
}
},
"data": {
"discount" : 15,
"expire" : "24-12-2018"
}
}
> fn list routes customerpromotionapp
PATH                IMAGE                                   ENDPOINT
/customerpromotion phx.ocir.io/oicpaas1/ralmuell/fn/customerpromotion:0.0.1 localhost:8080/r/customerpromotionapp/customerpromotion

Monitoring

Example Fn usage Grafana Dashboard

What’s Next?

Ralf Mueller

Written by

Software guy, photography enthusiast. I work for Oracle Corporation, opinions expressed here are my own.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade