Skip to content

Commit

Permalink
Merge pull request #39 from fnproject/http-context
Browse files Browse the repository at this point in the history
first rev of fdk 2.0
  • Loading branch information
rdallman committed Oct 25, 2018
2 parents 1c783fe + eccde19 commit 26ed643
Show file tree
Hide file tree
Showing 9 changed files with 495 additions and 953 deletions.
35 changes: 25 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ will take 2 minutes!
# Advanced example

TODO going to move to [examples](examples/) too :)
TODO make these `_example.go` instead of in markdown ;)

```go
package main
Expand All @@ -41,21 +42,32 @@ func main() {
fdk.Handle(fdk.HandlerFunc(myHandler))
}

// TODO make http.Handler example

func myHandler(ctx context.Context, in io.Reader, out io.Writer) {
fnctx := fdk.Context(ctx)
fnctx, ok := fdk.GetContext(ctx).(fdk.HTTPContext)
if !ok {
// optionally, this may be a good idea
fdk.WriteStatus(out, 400)
fdk.SetHeader(out, "Content-Type", "application/json")
io.WriteString(out, `{"error":"function not invoked via http trigger"}`)
return
}

contentType := fnctx.Header.Get("Content-Type")
contentType := fnctx.Header().Get("Content-Type")
if contentType != "application/json" {
// can assert content type for your api this way
fdk.WriteStatus(out, 400)
fdk.SetHeader(out, "Content-Type", "application/json")
io.WriteString(out, `{"error":"invalid content type"}`)
return
}

if fnctx.Method != "PUT" {
if fnctx.RequestMethod() != "PUT" {
// can assert certain request methods for certain endpoints
fdk.WriteStatus(out, 404)
fdk.SetHeader(out, "Content-Type", "application/json")
io.WriteString(out, `{"error":"route not found"}`)
io.WriteString(out, `{"error":"route not found, method not supported"}`)
return
}

Expand All @@ -64,20 +76,23 @@ func myHandler(ctx context.Context, in io.Reader, out io.Writer) {
}
json.NewDecoder(in).Decode(&person)

// you can write your own headers & status, if you'd like to
fdk.WriteStatus(out, 201)
fdk.SetHeader(out, "Content-Type", "application/json")
// this is where you might insert person into a database or do something else

all := struct {
Name string `json:"name"`
URL string `json:"url"`
Header http.Header `json:"header"`
Config map[string]string `json:"config"`
}{
Name: person.Name,
Header: fnctx.Header,
Config: fnctx.Config,
Name: person.Name,
URL: fnctx.RequestURL(),
Header: fnctx.Header(),
Config: fnctx.Config(),
}

// you can write your own headers & status, if you'd like to
fdk.SetHeader(out, "Content-Type", "application/json")
fdk.WriteStatus(out, 201)
json.NewEncoder(out).Encode(&all)
}
```
193 changes: 141 additions & 52 deletions fdk.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,94 +3,183 @@ package fdk
import (
"context"
"io"
"log"
"net/http"
"os"

"github.com/fnproject/fdk-go/utils"
"time"
)

// Handler is a function handler, representing 1 invocation of a function
type Handler interface {
Serve(ctx context.Context, in io.Reader, out io.Writer)
// Serve contains a context with request configuration, the body of the
// request as a stream of bytes, and a writer to output to; user's may set
// headers via the resp writer using the fdk's SetHeader/AddHeader methods -
// if you've a better idea, pipe up.
Serve(ctx context.Context, body io.Reader, resp io.Writer)
}

// HandlerFunc makes a Handler so that you don't have to!
type HandlerFunc func(ctx context.Context, in io.Reader, out io.Writer)

// Serve implements Handler
func (f HandlerFunc) Serve(ctx context.Context, in io.Reader, out io.Writer) {
f(ctx, in, out)
}

// Context will return an *fn.Ctx that can be used to read configuration and
// request information from an incoming request.
func Context(ctx context.Context) *Ctx {
utilsCtx := utils.Context(ctx)
return &Ctx{
HTTPHeader: utilsCtx.HTTPHeader,
Header: utilsCtx.Header,
Config: utilsCtx.Config,
RequestURL: utilsCtx.RequestURL,
Method: utilsCtx.Method,
}
// HTTPHandler makes a Handler from an http.Handler, if the function invocation
// is from an http trigger the request is identical to the client request to the
// http gateway (sans some hop headers).
func HTTPHandler(h http.Handler) Handler {
return &httpHandlerFunc{h}
}

func WithContext(ctx context.Context, fnctx *Ctx) context.Context {
utilsCtx := &utils.Ctx{
HTTPHeader: fnctx.HTTPHeader,
Header: fnctx.Header,
Config: fnctx.Config,
RequestURL: fnctx.RequestURL,
Method: fnctx.Method,
type httpHandlerFunc struct {
http.Handler
}

// Serve implements Handler
func (f *httpHandlerFunc) Serve(ctx context.Context, in io.Reader, out io.Writer) {
reqURL := "http://localhost/invoke"
reqMethod := "POST"
if ctx, ok := GetContext(ctx).(HTTPContext); ok {
reqURL = ctx.RequestURL()
reqMethod = ctx.RequestMethod()
}
return utils.WithContext(ctx, utilsCtx)

req, err := http.NewRequest(reqMethod, reqURL, in)
if err != nil {
panic("cannot re-create request from context")
}

req.Header = GetContext(ctx).Header()
req = req.WithContext(ctx)

rw, ok := out.(http.ResponseWriter)
if !ok {
panic("output is not a response writer, this was poorly planned please yell at me")
}

f.ServeHTTP(rw, req)
}

// GetContext will return an fdk Context that can be used to read configuration and
// request information from an incoming request.
func GetContext(ctx context.Context) Context {
return ctx.Value(ctxKey).(Context)
}

// WithContext adds an fn context to a context context. It is unclear why this is
// an exported method but hey here ya go don't hurt yourself.
func WithContext(ctx context.Context, fnctx Context) context.Context {
return context.WithValue(ctx, ctxKey, fnctx)
}

type key struct{}

var ctxKey = new(key)

// Context contains all configuration for a function invocation
type Context interface {
// Config is a map of all env vars set on a function, the base set of fn
// headers in addition to any app and function configuration
Config() map[string]string

// Header are the headers sent to this function invocation
Header() http.Header

// ContentType is Header().Get("Content-Type") but with 15 less chars, you are welcome
ContentType() string

// CallID is the call id for this function invocation
CallID() string

// AppName is Config()["FN_APP_ID"]
AppID() string

// FnID is Config()["FN_FN_ID"]
FnID() string
}

// Ctx provides access to Config and Headers from fn.
type Ctx struct {
// 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
// HTTPContext contains all configuration for a function invocation sourced
// from an http gateway trigger, which will make the function appear to receive
// from the client request they were sourced from, with no additional headers.
type HTTPContext interface {
Context

// RequestURL is the request url from the gateway client http request
RequestURL() string

// RequestMethod is the request method from the gateway client http request
RequestMethod() string
}

// AddHeader will add a header on the function response, for hot function
// formats.
type baseCtx struct {
header http.Header
config map[string]string
callID string
}

type httpCtx struct {
// XXX(reed): if we embed we won't preserve the original headers. since we have an
// interface handy now we could change this under the covers when/if we want... idk
baseCtx
requestURL string
requestMethod string
}

func (c baseCtx) Config() map[string]string { return c.config }
func (c baseCtx) Header() http.Header { return c.header }
func (c baseCtx) ContentType() string { return c.header.Get("Content-Type") }
func (c baseCtx) CallID() string { return c.callID }
func (c baseCtx) AppID() string { return c.config["FN_APP_ID"] }
func (c baseCtx) FnID() string { return c.config["FN_FN_ID"] }

func (c httpCtx) RequestURL() string { return c.requestURL }
func (c httpCtx) RequestMethod() string { return c.requestMethod }

func ctxWithDeadline(ctx context.Context, fnDeadline string) (context.Context, context.CancelFunc) {
t, err := time.Parse(time.RFC3339, fnDeadline)
if err == nil {
return context.WithDeadline(ctx, t)
}
return context.WithCancel(ctx)
}

// AddHeader will add a header onto the function response
func AddHeader(out io.Writer, key, value string) {
if resp, ok := out.(*utils.Response); ok {
resp.Header.Add(key, value)
if resp, ok := out.(http.ResponseWriter); ok {
resp.Header().Add(key, value)
}
}

// SetHeader will set a header on the function response, for hot function
// formats.
// SetHeader will set a header on the function response
func SetHeader(out io.Writer, key, value string) {
if resp, ok := out.(*utils.Response); ok {
resp.Header.Set(key, value)
if resp, ok := out.(http.ResponseWriter); ok {
resp.Header().Set(key, value)
}
}

// WriteStatus will set the status code to return in the function response, for
// hot function formats.
// WriteStatus will set the status code to return in the function response
func WriteStatus(out io.Writer, status int) {
if resp, ok := out.(*utils.Response); ok {
resp.Status = status
if resp, ok := out.(http.ResponseWriter); ok {
resp.WriteHeader(status)
}
}

// Handle will run the event loop for a function. Handle should be invoked
// through main() in a user's function and can handle communication between the
// function and fn server via any of the supported formats.
func Handle(handler Handler) {
format, _ := os.LookupEnv("FN_FORMAT")
HandleContext(context.Background(), handler)
}

path := os.Getenv("FN_LISTENER")
if path != "" {
utils.StartHTTPServer(handler, path, format)
return
// HandleContext works the same as Handle, but takes a context that will
// exit the handler loop when canceled/timed out.
func HandleContext(ctx context.Context, handler Handler) {
format, _ := os.LookupEnv("FN_FORMAT")
if format != "" && format != "http-stream" {
log.Fatal("only http-stream format is supported, please set function.format=http-stream against your fn service")
}

utils.Do(handler, format, os.Stdin, os.Stdout)
path := os.Getenv("FN_LISTENER")
startHTTPServer(ctx, handler, path)
}
Loading

0 comments on commit 26ed643

Please sign in to comment.