diff --git a/.travis.yml b/.travis.yml index c75654e2..f41cca97 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,5 +16,5 @@ script: - ./fission-bundle/build.sh - hack/verify-gofmt.sh - /tmp/test-etcd/etcd & - - go test -v -i $(go list ./... | grep -v '/vendor/') - - go test -v $(go list ./... | grep -v '/vendor/') + - go test -v -i $(go list ./... | grep -v '/vendor/' | grep -v 'examples/go') + - go test -v $(go list ./... | grep -v '/vendor/' | grep -v 'examples/go') diff --git a/environments/go/Dockerfile b/environments/go/Dockerfile new file mode 100644 index 00000000..84b84aaf --- /dev/null +++ b/environments/go/Dockerfile @@ -0,0 +1,14 @@ +FROM golang:1.8 + +ENV GOPATH /usr +ENV APP ${GOPATH}/src/github.com/fission/fission/environments/go + +ADD context ${APP}/context +ADD server.go ${APP} + +WORKDIR ${APP} +RUN go get +RUN go build -o /server server.go + +ENTRYPOINT ["/server"] +EXPOSE 8888 diff --git a/environments/go/README.md b/environments/go/README.md new file mode 100644 index 00000000..e974099e --- /dev/null +++ b/environments/go/README.md @@ -0,0 +1,29 @@ +# Fission: Go Environment + +This is the Go environment for Fission. + +It's a Docker image containing a Go 1.8rc3 runtime, along with a dynamic loader. + +## Build this image + +``` +docker build -t USER/go-runtime . && docker push USER/go-runtime +``` + +## Using the image in fission + +You can add this customized image to fission with "fission env +create": + +``` +fission env create --name go-runtime --image USER/go-runtime +``` + +Or, if you already have an environment, you can update its image: + +``` +fission env update --name go-runtime --image USER/go-runtime +``` + +After this, fission functions that have the env parameter set to the +same environment name as this command will use this environment. diff --git a/environments/go/context/context.go b/environments/go/context/context.go new file mode 100644 index 00000000..37c601a8 --- /dev/null +++ b/environments/go/context/context.go @@ -0,0 +1,10 @@ +package context + +type ( + Context map[string]interface{} +) + +func New() Context { + ctx := make(map[string]interface{}) + return ctx +} diff --git a/environments/go/server.go b/environments/go/server.go new file mode 100644 index 00000000..94e51afb --- /dev/null +++ b/environments/go/server.go @@ -0,0 +1,83 @@ +package main + +import ( + "fmt" + "net/http" + "os" + "plugin" + + "github.com/fission/fission/environments/go/context" +) + +const ( + CODE_PATH = "/userfunc/user" +) + +var userFunc http.HandlerFunc + +func loadPlugin() http.HandlerFunc { + p, err := plugin.Open(CODE_PATH) + if err != nil { + panic(err) + } + sym, err := p.Lookup("Handler") + if err != nil { + panic("Entry point not found") + } + + switch h := sym.(type) { + case *http.Handler: + return (*h).ServeHTTP + case *http.HandlerFunc: + return *h + case func(http.ResponseWriter, *http.Request): + return h + case func(context.Context, http.ResponseWriter, *http.Request): + return func(w http.ResponseWriter, r *http.Request) { + c := context.New() + h(c, w, r) + } + default: + panic("Entry point not found: bad type") + } +} + +func specializeHandler(w http.ResponseWriter, r *http.Request) { + if userFunc != nil { + w.WriteHeader(400) + w.Write([]byte("Not a generic container")) + return + } + + _, err := os.Stat(CODE_PATH) + if err != nil { + if os.IsNotExist(err) { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte(CODE_PATH + ": not found")) + return + } else { + panic(err) + } + } + + fmt.Println("Specializing ...") + userFunc = loadPlugin() + fmt.Println("Done") +} + +func main() { + http.HandleFunc("/specialize", specializeHandler) + + // Generic route -- all http requests go to the user function. + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + if userFunc == nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Generic container: no requests supported")) + return + } + userFunc(w, r) + }) + + fmt.Println("Listening on 8888 ...") + http.ListenAndServe(":8888", nil) +} diff --git a/examples/go/README.md b/examples/go/README.md new file mode 100644 index 00000000..934dca34 --- /dev/null +++ b/examples/go/README.md @@ -0,0 +1,30 @@ +# Go examples + +The `go` runtime uses the [`plugin` package](https://golang.org/pkg/plugin/) to dynamically load an HTTP handler. + +## Requirements + +- go 1.8 +- A [go-runtime fission environment](environments/go/README.md) + +## Examples + +### hello.go + +`hello.go` is an very basic HTTP handler returning `"Hello, World!"`. + + +``` +# Build the function as a plugin +$ ./build.sh + +# Upload the function to fission +$ fission function create --name hello --env go-runtime --package hello.so + +# Map /hello to the hello function +$ fission route create --method GET --url /hello --function hello + +# Run the function +$ curl http://$FISSION_ROUTER/hello +Hello, World! +``` diff --git a/examples/go/build.sh b/examples/go/build.sh new file mode 100755 index 00000000..9b2576da --- /dev/null +++ b/examples/go/build.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +FISSION_PATH="github.com/fission/fission" + +docker run --rm -v $GOPATH/src/$FISSION_PATH:/usr/src/$FISSION_PATH \ + -e GOPATH=/usr/ \ + -w /usr/src/$FISSION_PATH/examples/go \ + golang:1.8 go build -buildmode=plugin -o hello.so hello.go diff --git a/examples/go/hello.go b/examples/go/hello.go new file mode 100644 index 00000000..c58ebf27 --- /dev/null +++ b/examples/go/hello.go @@ -0,0 +1,10 @@ +package main + +import ( + "net/http" +) + +func Handler(w http.ResponseWriter, r *http.Request) { + msg := "Hello, World!" + w.Write([]byte(msg)) +} diff --git a/fission/function.go b/fission/function.go index 8599b2ff..24233400 100644 --- a/fission/function.go +++ b/fission/function.go @@ -74,7 +74,10 @@ func fnCreate(c *cli.Context) error { fileName := c.String("code") if len(fileName) == 0 { - fatal("Need --code argument.") + fileName = c.String("package") + if len(fileName) == 0 { + fatal("Need --code or --package argument.") + } } code := fnFetchCode(fileName) diff --git a/fission/main.go b/fission/main.go index c1f0d292..fb0c8265 100644 --- a/fission/main.go +++ b/fission/main.go @@ -39,13 +39,14 @@ func main() { fnNameFlag := cli.StringFlag{Name: "name", Usage: "function name"} fnEnvNameFlag := cli.StringFlag{Name: "env", Usage: "environment name for function"} fnCodeFlag := cli.StringFlag{Name: "code", Usage: "local path or URL for source code"} + fnPackageFlag := cli.StringFlag{Name: "package", Usage: "local path or URL for binary package"} fnUidFlag := cli.StringFlag{Name: "uid", Usage: "function uid, optional (use latest if unspecified)"} fnSubcommands := []cli.Command{ - {Name: "create", Usage: "Create new function (and optionally, an HTTP route to it)", Flags: []cli.Flag{fnNameFlag, fnEnvNameFlag, fnCodeFlag, htUrlFlag, htMethodFlag}, Action: fnCreate}, + {Name: "create", Usage: "Create new function (and optionally, an HTTP route to it)", Flags: []cli.Flag{fnNameFlag, fnEnvNameFlag, fnCodeFlag, fnPackageFlag, htUrlFlag, htMethodFlag}, Action: fnCreate}, {Name: "get", Usage: "Get function source code", Flags: []cli.Flag{fnNameFlag, fnUidFlag}, Action: fnGet}, {Name: "edit", Usage: "Edit function source code in $EDITOR", Flags: []cli.Flag{fnNameFlag, fnUidFlag}, Action: fnEdit}, {Name: "getmeta", Usage: "Get function metadata", Flags: []cli.Flag{fnNameFlag, fnUidFlag}, Action: fnGetMeta}, - {Name: "update", Usage: "Update function source code", Flags: []cli.Flag{fnNameFlag, fnEnvNameFlag, fnCodeFlag}, Action: fnUpdate}, + {Name: "update", Usage: "Update function source code", Flags: []cli.Flag{fnNameFlag, fnEnvNameFlag, fnCodeFlag, fnPackageFlag}, Action: fnUpdate}, {Name: "delete", Usage: "Delete function", Flags: []cli.Flag{fnNameFlag, fnUidFlag}, Action: fnDelete}, {Name: "list", Usage: "List all functions", Flags: []cli.Flag{}, Action: fnList}, }