Binary Environment (#256)

Adds a fission environment for arbitrary linux binaries.
This commit is contained in:
Erwin van Eyk
2017-07-26 12:29:54 -07:00
committed by Soam Vasani
parent a8fc7a7029
commit 85983c91cd
11 changed files with 348 additions and 0 deletions
+1
View File
@@ -10,3 +10,4 @@ environments/php7/vendor/
*~
vendor/
local/
+10
View File
@@ -0,0 +1,10 @@
FROM alpine:3.5
RUN apk update
RUN apk add coreutils binutils findutils grep
COPY . /app
WORKDIR /app
EXPOSE 8888
ENTRYPOINT ["./server"]
+50
View File
@@ -0,0 +1,50 @@
# Binary Environment Examples
The `binary` runtime is a go server that uses a subprocess to invoke executables or execute shell scripts.
Use Cases
- Execute bash scripts
- Run executables of languages that have no dedicated environment yet.
## Words of Caution
The environment runs on an alpine image with some additional utilty commandline tools installed, such as 'grep'.
However, in case you want to make use of more ecsoteric commandline tools, you should add the relevant apk to the
Dockerfile and build a new binary environment. See 'Compiling' for instructions.
When executing functions using binaries, **ensure that the executable is built for the right architecture**.
Using the default binary environment this means that the binary should be build for Linux.
## Usage
The interface to the executable used by this environment is somewhat similar to a [CGI interface](https://en.wikipedia.org/wiki/Common_Gateway_Interface).
This means that any HTTP headers are converted to environment variables of the form "HTTP_<header-name>". For example these
are some of frequently occurring headers:
```
# Request Metadata
CONTENT_LENGTH
REQUEST_URI
REQUEST_METHOD
# HTTP Headers
HTTP_ACCEPT
HTTP_USER-AGENT
...
```
The body of HTTP piped over the STDIN to the executable.
All output that is provided to the server over the STDOUT will be transformed into the HTTP response.
## Compiling
In order to build the Dockerfile, the server needs to be compiled to the right architecture.
```bash
sh ./build.sh
```
Build the Dockerfile:
```bash
docker build --tag=${USER}/binary-env .
```
See the [README](../../examples/binary/README.md) in the binary examples directory for usage instructions.
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
GOOS=linux GOARCH=386 go build -o server .
+47
View File
@@ -0,0 +1,47 @@
package main
import (
"fmt"
"strings"
)
// Utility functions for working with environment variables
type Env struct {
Vars []*EnvVar
}
type EnvVar struct {
Key string
Val string
}
func FromString(rawEnvVar string) *EnvVar {
parts := strings.SplitN(rawEnvVar, "=", 2)
return &EnvVar{parts[0], parts[1]}
}
func (ev *EnvVar) ToString() string {
return fmt.Sprintf("%s=%s", ev.Key, ev.Val)
}
func (e *Env) SetEnv(envVar *EnvVar) {
e.Vars = append(e.Vars, envVar)
}
func (e *Env) ToStringEnv() []string {
var result []string
for _, envVar := range e.Vars {
result = append(result, envVar.ToString())
}
return result
}
func NewEnv(stringEnv []string) *Env {
env := &Env{}
if stringEnv != nil {
for _, rawEnvVar := range stringEnv {
env.SetEnv(FromString(rawEnvVar))
}
}
return env
}
+134
View File
@@ -0,0 +1,134 @@
package main
import (
"flag"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
)
const (
DEFAULT_CODE_PATH = "/userfunc/user"
DEFAULT_INTERNAL_CODE_PATH = "/bin/userfunc"
)
var specialized bool
type BinaryServer struct {
fetchedCodePath string
internalCodePath string
}
func (bs *BinaryServer) SpecializeHandler(w http.ResponseWriter, r *http.Request) {
if specialized {
w.WriteHeader(400)
w.Write([]byte("Not a generic container"))
return
}
_, err := os.Stat(bs.fetchedCodePath)
if err != nil {
if os.IsNotExist(err) {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte(bs.fetchedCodePath + ": not found"))
return
} else {
panic(err)
}
}
// Future: Check if executable is correct architecture/executable.
// Copy the executable to ensure that file is executable and immutable.
userFunc, err := ioutil.ReadFile(bs.fetchedCodePath)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Failed to read executable."))
return
}
err = ioutil.WriteFile(bs.internalCodePath, userFunc, 0555)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Failed to write executable to target location."))
return
}
fmt.Println("Specializing ...")
specialized = true
fmt.Println("Done")
}
func (bs *BinaryServer) InvocationHandler(w http.ResponseWriter, r *http.Request) {
if !specialized {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Generic container: no requests supported"))
return
}
// CGI-like passing of environment variables
execEnv := NewEnv(nil)
execEnv.SetEnv(&EnvVar{"REQUEST_METHOD", r.Method})
execEnv.SetEnv(&EnvVar{"REQUEST_URI", r.RequestURI})
execEnv.SetEnv(&EnvVar{"CONTENT_LENGTH", fmt.Sprintf("%d", r.ContentLength)})
for header, val := range r.Header {
execEnv.SetEnv(&EnvVar{fmt.Sprintf("HTTP_%s", strings.ToUpper(header)), val[0]})
}
// Future: could be improved by keeping subprocess open while environment is specialized
cmd := exec.Command(bs.internalCodePath)
cmd.Env = execEnv.ToStringEnv()
if r.ContentLength != 0 {
fmt.Println(r.ContentLength)
stdin, err := cmd.StdinPipe()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(fmt.Sprintf("Failed to get STDIN pipe: %s", err)))
panic(err)
}
_, err = io.Copy(stdin, r.Body)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(fmt.Sprintf("Failed to pipe input: %s", err)))
}
stdin.Close()
}
out, err := cmd.Output()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(fmt.Sprintf("Function error: %s", err)))
return
}
w.WriteHeader(http.StatusOK)
w.Write(out)
}
func main() {
codePath := flag.String("c", DEFAULT_CODE_PATH, "Path to expected fetched executable.")
internalCodePath := flag.String("i", DEFAULT_INTERNAL_CODE_PATH, "Path to specialized executable.")
flag.Parse()
absInternalCodePath, err := filepath.Abs(*internalCodePath)
if err != nil {
panic(err)
}
fmt.Printf("Using fetched code path: %s\n", *codePath)
fmt.Printf("Using internal code path: %s\n", absInternalCodePath)
server := &BinaryServer{*codePath, absInternalCodePath}
http.HandleFunc("/", server.InvocationHandler)
http.HandleFunc("/specialize", server.SpecializeHandler)
fmt.Println("Listening on 8888 ...")
err = http.ListenAndServe(":8888", nil)
if err != nil {
panic(err)
}
}
+80
View File
@@ -0,0 +1,80 @@
# Binary Environment Examples
The `binary` runtime is a go server that uses a subprocess to invoke executables or execute shell scripts.
For more info read the [environment README](../../environments/binary/README.md).
## Requirements
First, set up your fission deployment with the binary environment.
```bash
fission env create --name binary-env --image fission/binary-env
```
## Example Usage
### hello.sh
`hello.sh` is an very basic shell script that returns `"Hello, World!"`.
```bash
# Upload the function to fission
fission function create --name hello --env binary-env --code hello.sh
# Map /hello to the hello function
fission route create --method GET --url /hello --function hello
# Run the function
curl http://$FISSION_ROUTER/hello
```
This should return a HTTP response with the body `Hello World!`
### echo.sh
`echo.sh` shows the the use of STDIN to read the request body, echoing the input back in the response.
```bash
# Upload the function to fission
fission function create --name echo --env binary-env --code echo.sh
# Map /hello to the hello function
fission route create --method POST --url /echo --function echo
# Run the function
curl -XPOST -d 'Echoooooo!' http://$FISSION_ROUTER/echo
```
This should return a HTTP response with the body `... Echoooooo!`.
### headers.sh
`headers.sh` shows the access to the environment variables that hold the HTTP headers, returning the set HTTP headers.
```bash
# Upload the function to fission
fission function create --name headers --env binary-env --code headers.sh
# Map /hello to the hello function
fission route create --url /headers --function headers
# Run the function
curl -H 'X-FOO: BAR' http://$FISSION_ROUTER/headers
```
This should return a HTTP response with the body `... Echoooooo!`.
### hello..go
This example shows the differences between using shell scripts and binaries. `hello.go` returns `Hello World!` + the
environment variables it received from the server.
```bash
# Build the function targeted at the right architecture
GOOS=linux GOARCH=386 go build -o hello-go-func hello.go
# Upload the function to fission
fission function create --name hello-go --env binary-env --code hello-go-func
# Map /hello to the hello function
fission route create --url /hello-go --function hello-go
# Run the function
curl -H 'X-GO: AWESOME!' http://$FISSION_ROUTER/hello-go
```
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
printf "... "
cat -
+3
View File
@@ -0,0 +1,3 @@
#!/bin/sh
env | grep "^HTTP_"
+12
View File
@@ -0,0 +1,12 @@
package main
import (
"fmt"
"os"
)
// See README.md in the examples/binary directory for instructions
func main() {
fmt.Println("Hello World!")
fmt.Printf("Environment: %v", os.Environ())
}
+3
View File
@@ -0,0 +1,3 @@
#!/bin/sh
echo "Hello World!"