HTTP trigger http-stream tests (#1241)

This commit is contained in:
Reed Allman
2018-09-26 05:25:48 -07:00
committed by Owen Cliffe
parent 5d907821b1
commit 01b8e8679d
20 changed files with 548 additions and 403 deletions

33
vendor/github.com/fnproject/fdk-go/circle.yml generated vendored Normal file
View File

@@ -0,0 +1,33 @@
version: 2
jobs:
build:
docker:
- image: circleci/golang:1.11.0
working_directory: ~/fdk-go
steps:
- checkout
- setup_remote_docker:
docker_layer_caching: true
- run: docker version
- run: docker pull fnproject/fnserver
# installing Fn CLI and starting the Fn server
- run:
command: |
curl -LSs https://raw.githubusercontent.com/fnproject/cli/master/install | sh
- run:
command: fn build
working_directory: examples/hello
- deploy:
command: |
if [[ "${CIRCLE_BRANCH}" == "master" && -z "${CIRCLE_PR_REPONAME}" ]]; then
func_version=$(awk '/^version:/ { print $2; }' func.yaml)
printenv DOCKER_PASS | docker login -u ${DOCKER_USER} --password-stdin
git config --global user.email "ci@fnproject.com"
git config --global user.name "CI"
git branch --set-upstream-to=origin/${CIRCLE_BRANCH} ${CIRCLE_BRANCH}
docker tag "hello:${func_version}" "fnproject/fdk-go-hello:${func_version}"
docker tag "hello:${func_version}" "fnproject/fdk-go-hello:latest"
docker push "fnproject/fdk-go-hello:${func_version}"
docker push "fnproject/fdk-go-hello:latest"
fi
working_directory: examples/hello

View File

@@ -8,7 +8,7 @@
".",
"utils"
]
revision = "5d768b2006f11737b6a69a758ddd6d2fac04923e"
revision = "c6ce6afbce0935ffdf0e228471406d0f966736a5"
[solve-meta]
analyzer-name = "dep"

View File

@@ -13,20 +13,17 @@ func main() {
fdk.Handle(fdk.HandlerFunc(myHandler))
}
func myHandler(ctx context.Context, in io.Reader, out io.Writer) {
var person struct {
Name string `json:"name"`
}
json.NewDecoder(in).Decode(&person)
if person.Name == "" {
person.Name = "World"
}
type Person struct {
Name string `json:"name"`
}
func myHandler(ctx context.Context, in io.Reader, out io.Writer) {
p := &Person{Name: "World"}
json.NewDecoder(in).Decode(p)
msg := struct {
Msg string `json:"message"`
}{
Msg: fmt.Sprintf("Hello %s", person.Name),
Msg: fmt.Sprintf("Hello %s", p.Name),
}
json.NewEncoder(out).Encode(&msg)
}

View File

@@ -0,0 +1,6 @@
schema_version: 20180708
name: hello
version: 0.0.1
runtime: go
entrypoint: ./func
format: http-stream

View File

@@ -24,6 +24,7 @@ func (f HandlerFunc) Serve(ctx context.Context, in io.Reader, out io.Writer) {
func Context(ctx context.Context) *Ctx {
utilsCtx := utils.Context(ctx)
return &Ctx{
HTTPHeader: utilsCtx.HTTPHeader,
Header: utilsCtx.Header,
Config: utilsCtx.Config,
RequestURL: utilsCtx.RequestURL,
@@ -33,6 +34,7 @@ func Context(ctx context.Context) *Ctx {
func WithContext(ctx context.Context, fnctx *Ctx) context.Context {
utilsCtx := &utils.Ctx{
HTTPHeader: fnctx.HTTPHeader,
Header: fnctx.Header,
Config: fnctx.Config,
RequestURL: fnctx.RequestURL,
@@ -43,7 +45,12 @@ func WithContext(ctx context.Context, fnctx *Ctx) context.Context {
// Ctx provides access to Config and Headers from fn.
type Ctx struct {
Header http.Header
// Header are the unmodified headers as sent to the container, see
// HTTPHeader for specific trigger headers
Header http.Header
// HTTPHeader are the request headers as they appear on the original HTTP request,
// for an http trigger.
HTTPHeader http.Header
Config map[string]string
RequestURL string
Method string
@@ -78,5 +85,12 @@ func WriteStatus(out io.Writer, status int) {
// function and fn server via any of the supported formats.
func Handle(handler Handler) {
format, _ := os.LookupEnv("FN_FORMAT")
path := os.Getenv("FN_LISTENER")
if path != "" {
utils.StartHTTPServer(handler, path, format)
return
}
utils.Do(handler, format, os.Stdin, os.Stdout)
}

168
vendor/github.com/fnproject/fdk-go/utils/httpstream.go generated vendored Normal file
View File

@@ -0,0 +1,168 @@
package utils
import (
"bytes"
"context"
"io"
"log"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
)
// in case we go over the timeout, need to use a pool since prev buffer may not be freed
var bufPool = &sync.Pool{New: func() interface{} { return new(bytes.Buffer) }}
type HTTPHandler struct {
handler Handler
}
func (h *HTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
buf := bufPool.Get().(*bytes.Buffer)
defer bufPool.Put(buf)
resp := Response{
Writer: buf,
Status: 200,
Header: make(http.Header), // XXX(reed): pool these too
}
ctx := WithContext(r.Context(), &Ctx{
Config: BuildConfig(),
})
ctx, cancel := decapHeaders(ctx, r)
defer cancel()
h.handler.Serve(ctx, r.Body, &resp)
encapHeaders(w, resp)
// XXX(reed): 504 if ctx is past due / handle errors with 5xx? just 200 for now
// copy response from user back up now with headers in place...
io.Copy(w, buf)
// XXX(reed): handle streaming, we have to intercept headers but not necessarily body (ie no buffer)
}
func encapHeaders(fn http.ResponseWriter, user Response) {
fnh := fn.Header()
fnh.Set("Fn-Http-Status", strconv.Itoa(user.Status))
for k, vs := range user.Header {
switch k {
case "Content-Type":
// don't modify this one...
default:
// prepend this guy
k = "Fn-Http-H-" + k
}
for _, v := range vs {
fnh.Add(k, v)
}
}
}
// TODO can make this the primary means of context construction
func decapHeaders(ctx context.Context, r *http.Request) (_ context.Context, cancel func()) {
rctx := Context(ctx)
var deadline string
// copy the original headers in then reduce for http headers
rctx.Header = r.Header
rctx.HTTPHeader = make(http.Header, len(r.Header)) // XXX(reed): oversized, esp if not http
// find things we need, and for http headers add them to the httph bucket
for k, vs := range r.Header {
switch k {
case "Fn-Deadline":
deadline = vs[0]
case "Fn-Call-Id":
rctx.callId = vs[0]
case "Content-Type":
// just leave this one instead of deleting
default:
continue
}
if !strings.HasPrefix(k, "Fn-Http-") {
// XXX(reed): we need 2 header buckets on ctx, one for these and one for the 'original req' headers
// for now just nuke so the headers are clean...
continue
}
switch {
case k == "Fn-Http-Request-Url":
rctx.RequestURL = vs[0]
case k == "Fn-Http-Method":
rctx.Method = vs[0]
case strings.HasPrefix(k, "Fn-Http-H-"):
for _, v := range vs {
rctx.HTTPHeader.Add(strings.TrimPrefix(k, "Fn-Http-H-"), v)
}
default:
// XXX(reed): just delete it? how is it here? maybe log/panic
}
}
return CtxWithDeadline(ctx, deadline)
}
func StartHTTPServer(handler Handler, path, format string) {
uri, err := url.Parse(path)
if err != nil {
log.Fatalln("url parse error: ", path, err)
}
server := http.Server{
Handler: &HTTPHandler{
handler: handler,
},
}
// try to remove pre-existing UDS: ignore errors here
phonySock := filepath.Join(filepath.Dir(uri.Path), "phony"+filepath.Base(uri.Path))
if uri.Scheme == "unix" {
os.Remove(phonySock)
os.Remove(uri.Path)
}
listener, err := net.Listen(uri.Scheme, phonySock)
if err != nil {
log.Fatalln("net.Listen error: ", err)
}
if uri.Scheme == "unix" {
sockPerm(phonySock, uri.Path)
}
err = server.Serve(listener)
if err != nil && err != http.ErrServerClosed {
log.Fatalln("serve error: ", err)
}
}
func sockPerm(phonySock, realSock string) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
// somehow this is the best way to get a permissioned sock file, don't ask questions, life is sad and meaningless
err := os.Chmod(phonySock, 0666)
if err != nil {
log.Fatalln("error giving sock file a perm", err)
}
err = os.Link(phonySock, realSock)
if err != nil {
log.Fatalln("error linking fake sock to real sock", err)
}
}

View File

@@ -26,12 +26,25 @@ func WithContext(ctx context.Context, fnctx *Ctx) context.Context {
// Ctx provides access to Config and Headers from fn.
type Ctx struct {
Header http.Header
// Header are the unmodified headers as sent to the container, see
// HTTPHeader for specific trigger headers
Header http.Header
// HTTPHeader are the request headers as they appear on the original HTTP request,
// for an http trigger.
HTTPHeader http.Header
Config map[string]string
RequestURL string
Method string
// XXX(reed): should turn this whole mess into some kind of event that we can
// morph into another type of an event after http/json/default die
// XXX(reed): should strip out eg FN_APP_NAME, etc as fields so Config is actually the config not config + fn's env vars
callId string
}
func (c Ctx) CallId() string { return c.callId }
type key struct{}
var ctxKey = new(key)