Binary Environment (#256)
Adds a fission environment for arbitrary linux binaries.
This commit is contained in:
committed by
Soam Vasani
parent
a8fc7a7029
commit
85983c91cd
@@ -10,3 +10,4 @@ environments/php7/vendor/
|
||||
*~
|
||||
|
||||
vendor/
|
||||
local/
|
||||
@@ -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"]
|
||||
@@ -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.
|
||||
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
|
||||
GOOS=linux GOARCH=386 go build -o server .
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
```
|
||||
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
|
||||
printf "... "
|
||||
cat -
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
env | grep "^HTTP_"
|
||||
@@ -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())
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
echo "Hello World!"
|
||||
Reference in New Issue
Block a user