From 9656b530c4c26865bf0d1154e1461c3a68ea5260 Mon Sep 17 00:00:00 2001 From: Alex Ellis Date: Fri, 30 Dec 2016 18:54:57 +0000 Subject: [PATCH] Enable routing to Alexa functions Add first Alexa-compatible function HostnameIntent --- faas | 1 + gateway/.gitignore | 1 + gateway/Dockerfile | 10 +- gateway/server.go | 96 ++++++++++++++++---- oneshot.sh | 12 +++ sample-functions/HostnameIntent/Dockerfile | 11 +++ sample-functions/HostnameIntent/handler.js | 22 +++++ sample-functions/HostnameIntent/package.json | 12 +++ sample-functions/HostnameIntent/sample.json | 16 ++++ 9 files changed, 156 insertions(+), 25 deletions(-) create mode 160000 faas create mode 100755 oneshot.sh create mode 100644 sample-functions/HostnameIntent/Dockerfile create mode 100644 sample-functions/HostnameIntent/handler.js create mode 100644 sample-functions/HostnameIntent/package.json create mode 100644 sample-functions/HostnameIntent/sample.json diff --git a/faas b/faas new file mode 160000 index 00000000..d94cfeb6 --- /dev/null +++ b/faas @@ -0,0 +1 @@ +Subproject commit d94cfeb660705028b6c101412a03519a8164712d diff --git a/gateway/.gitignore b/gateway/.gitignore index ad230ccf..86ca271b 100644 --- a/gateway/.gitignore +++ b/gateway/.gitignore @@ -1 +1,2 @@ gateway +sample.txt diff --git a/gateway/Dockerfile b/gateway/Dockerfile index ee309dfa..db3d4df6 100644 --- a/gateway/Dockerfile +++ b/gateway/Dockerfile @@ -1,11 +1,11 @@ from golang:1.7.3 RUN go get -d github.com/docker/docker/api/types \ - && go get -d github.com/docker/docker/api/types/filters \ - && go get -d github.com/docker/docker/api/types/swarm \ - && go get -d github.com/docker/docker/client -RUN go get github.com/gorilla/mux \ - && go get github.com/prometheus/client_golang/prometheus + && go get -d github.com/docker/docker/api/types/filters \ + && go get -d github.com/docker/docker/api/types/swarm \ + && go get -d github.com/docker/docker/client \ + && go get github.com/gorilla/mux \ + && go get github.com/prometheus/client_golang/prometheus WORKDIR /go/src/github.com/alexellis/faas/gateway diff --git a/gateway/server.go b/gateway/server.go index c3896f8e..6b4aa013 100644 --- a/gateway/server.go +++ b/gateway/server.go @@ -3,13 +3,16 @@ package main import ( "bytes" "context" + "fmt" "log" "net/http" + "strconv" + "strings" "time" "io/ioutil" - "strconv" + "encoding/json" "github.com/alexellis/faas/gateway/metrics" "github.com/docker/docker/api/types" @@ -19,6 +22,28 @@ import ( "github.com/prometheus/client_golang/prometheus" ) +type AlexaSessionApplication struct { + ApplicationId string `json:"applicationId"` +} + +type AlexaSession struct { + SessionId string `json:"sessionId"` + Application AlexaSessionApplication `json:"application"` +} + +type AlexaIntent struct { + Name string `json:"name"` +} + +type AlexaRequest struct { + Intent AlexaIntent `json:"intent"` +} + +type AlexaRequestBody struct { + Session AlexaSession `json:"session"` + Request AlexaRequest `json:"request"` +} + func lookupSwarmService(serviceName string) (bool, error) { var c *client.Client var err error @@ -33,38 +58,69 @@ func lookupSwarmService(serviceName string) (bool, error) { return len(services) > 0, err } +func isAlexa(requestBody []byte) AlexaRequestBody { + body := AlexaRequestBody{} + buf := bytes.NewBuffer(requestBody) + fmt.Println(buf) + str := buf.String() + parts := strings.Split(str, "sessionId") + if len(parts) > 0 { + json.Unmarshal(requestBody, &body) + fmt.Println("Alexa SDK request found") + fmt.Printf("Session=%s, Intent=%s, App=%s\n", body.Session.SessionId, body.Request.Intent, body.Session.Application.ApplicationId) + } + return body +} + +func invokeService(w http.ResponseWriter, r *http.Request, metrics metrics.MetricOptions, service string, requestBody []byte) { + start := time.Now() + buf := bytes.NewBuffer(requestBody) + url := "http://" + service + ":" + strconv.Itoa(8080) + "/" + fmt.Println("Forwarding request to ", url) + response, err := http.Post(url, "text/plain", buf) + if err != nil { + log.Fatalln(err) + } + + responseBody, _ := ioutil.ReadAll(response.Body) + w.Write(responseBody) + seconds := time.Since(start).Seconds() + fmt.Printf("Took %f\n", seconds) + metrics.GatewayServerlessServedTotal.Inc() + metrics.GatewayFunctions.Observe(seconds) +} + func makeProxy(metrics metrics.MetricOptions) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { metrics.GatewayRequestsTotal.Inc() - start := time.Now() - if r.Method == "POST" { log.Println(r.Header) header := r.Header["X-Function"] log.Println(header) - exists, err := lookupSwarmService(header[0]) - if err != nil { - log.Fatalln(err) - } - - if exists == true { - requestBody, _ := ioutil.ReadAll(r.Body) - buf := bytes.NewBuffer(requestBody) - - response, err := http.Post("http://"+header[0]+":"+strconv.Itoa(8080)+"/", "text/plain", buf) + if len(header) > 0 { + exists, err := lookupSwarmService(header[0]) if err != nil { log.Fatalln(err) } - responseBody, _ := ioutil.ReadAll(response.Body) - w.Write(responseBody) - metrics.GatewayServerlessServedTotal.Inc() - - metrics.GatewayFunctions.Observe(time.Since(start).Seconds()) + if exists == true { + requestBody, _ := ioutil.ReadAll(r.Body) + invokeService(w, r, metrics, header[0], requestBody) + } } else { - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte("Provide an x-function header.")) + requestBody, _ := ioutil.ReadAll(r.Body) + alexaService := isAlexa(requestBody) + if len(alexaService.Session.SessionId) > 0 && + len(alexaService.Session.Application.ApplicationId) > 0 && + len(alexaService.Request.Intent.Name) > 0 { + fmt.Println("Alexa skill detected") + invokeService(w, r, metrics, alexaService.Request.Intent.Name, requestBody) + + } else { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("Provide an x-function header.")) + } } } } diff --git a/oneshot.sh b/oneshot.sh new file mode 100755 index 00000000..7014d3e2 --- /dev/null +++ b/oneshot.sh @@ -0,0 +1,12 @@ +#!/bin/bash + + docker network create --driver overlay --attachable functions + git clone https://github.com/alexellis/faas && cd faas + cd watchdog + ./build.sh + docker build -t catservice . + docker service rm catservice ; docker service create --network=functions --name catservice catservice + cd .. + cd gateway + docker build -t server . ;docker rm -f server; docker run -v /var/run/docker.sock:/var/run/docker.sock --name server -p 8080:8080 --network=functions server + diff --git a/sample-functions/HostnameIntent/Dockerfile b/sample-functions/HostnameIntent/Dockerfile new file mode 100644 index 00000000..8866b5ff --- /dev/null +++ b/sample-functions/HostnameIntent/Dockerfile @@ -0,0 +1,11 @@ +FROM alpine:latest +RUN apk --update add nodejs +COPY ./fwatchdog /usr/bin/ + +COPY package.json . +COPY handler.js . +COPY sample.json . + +RUN npm i +ENV fprocess="node handler.js" +CMD ["fwatchdog"] diff --git a/sample-functions/HostnameIntent/handler.js b/sample-functions/HostnameIntent/handler.js new file mode 100644 index 00000000..ac58f2ea --- /dev/null +++ b/sample-functions/HostnameIntent/handler.js @@ -0,0 +1,22 @@ +"use strict" +let fs = require('fs'); +let sample = require("./sample.json"); + +var content = ''; +process.stdin.resume(); +process.stdin.on('data', function(buf) { content += buf.toString(); }); +process.stdin.on('end', function() { + fs.readFile("/etc/hostname", "utf8", (err, data) => { + if(err) { + return console.log(err); + } +// console.log(content); + + sample.response.outputSpeech.text = "Your hostname is: " + data; + sample.response.card.content = "Your hostname is: "+ data + sample.response.card.title = "Your hostname"; + console.log(JSON.stringify(sample)); + process.exit(0); + }); +}); + diff --git a/sample-functions/HostnameIntent/package.json b/sample-functions/HostnameIntent/package.json new file mode 100644 index 00000000..3bfc0919 --- /dev/null +++ b/sample-functions/HostnameIntent/package.json @@ -0,0 +1,12 @@ +{ + "name": "HostnameIntent", + "version": "1.0.0", + "description": "", + "main": "handler.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/sample-functions/HostnameIntent/sample.json b/sample-functions/HostnameIntent/sample.json new file mode 100644 index 00000000..27fc86ca --- /dev/null +++ b/sample-functions/HostnameIntent/sample.json @@ -0,0 +1,16 @@ +{ + "version": "1.0", + "response": { + "outputSpeech": { + "type": "PlainText", + "text": "There's currently 6 people in space" + }, + "card": { + "content": "There's currently 6 people in space", + "title": "People in space", + "type": "Simple" + }, + "shouldEndSession": true + }, + "sessionAttributes": {} +}