Middleware upgrade (#554)

* Adds root level middleware

* Added todo

* Better way for extensions to be added.

* Bad conflict merge?
This commit is contained in:
Travis Reeder
2017-12-05 08:22:03 -08:00
committed by GitHub
parent 9a17c79a3b
commit 0798f9fac8
23 changed files with 660 additions and 287 deletions

View File

@@ -1,5 +1,5 @@
# build stage # build stage
FROM golang:alpine AS build-env FROM golang:1.9-alpine AS build-env
RUN apk --no-cache add build-base git bzr mercurial gcc RUN apk --no-cache add build-base git bzr mercurial gcc
ENV D=/go/src/github.com/fnproject/fn ENV D=/go/src/github.com/fnproject/fn
ADD . $D ADD . $D

View File

@@ -15,9 +15,9 @@ import (
"github.com/fnproject/fn/api/agent/drivers/docker" "github.com/fnproject/fn/api/agent/drivers/docker"
"github.com/fnproject/fn/api/agent/protocol" "github.com/fnproject/fn/api/agent/protocol"
"github.com/fnproject/fn/api/common" "github.com/fnproject/fn/api/common"
"github.com/fnproject/fn/api/extensions"
"github.com/fnproject/fn/api/id" "github.com/fnproject/fn/api/id"
"github.com/fnproject/fn/api/models" "github.com/fnproject/fn/api/models"
"github.com/fnproject/fn/fnext"
"github.com/opentracing/opentracing-go" "github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/log" "github.com/opentracing/opentracing-go/log"
"github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/client_golang/prometheus/promhttp"
@@ -112,7 +112,7 @@ type Agent interface {
// Return the http.Handler used to handle Prometheus metric requests // Return the http.Handler used to handle Prometheus metric requests
PromHandler() http.Handler PromHandler() http.Handler
AddCallListener(extensions.CallListener) AddCallListener(fnext.CallListener)
} }
type agent struct { type agent struct {
@@ -120,7 +120,7 @@ type agent struct {
mq models.MessageQueue mq models.MessageQueue
ds models.Datastore ds models.Datastore
ls models.LogStore ls models.LogStore
callListeners []extensions.CallListener callListeners []fnext.CallListener
driver drivers.Driver driver drivers.Driver

View File

@@ -3,8 +3,8 @@ package agent
import ( import (
"context" "context"
"github.com/fnproject/fn/api/extensions"
"github.com/fnproject/fn/api/models" "github.com/fnproject/fn/api/models"
"github.com/fnproject/fn/fnext"
) )
type callTrigger interface { type callTrigger interface {
@@ -12,7 +12,7 @@ type callTrigger interface {
fireAfterCall(context.Context, *models.Call) error fireAfterCall(context.Context, *models.Call) error
} }
func (a *agent) AddCallListener(listener extensions.CallListener) { func (a *agent) AddCallListener(listener fnext.CallListener) {
a.callListeners = append(a.callListeners, listener) a.callListeners = append(a.callListeners, listener)
} }

View File

@@ -3,12 +3,12 @@ package server
import ( import (
"context" "context"
"github.com/fnproject/fn/api/extensions"
"github.com/fnproject/fn/api/models" "github.com/fnproject/fn/api/models"
"github.com/fnproject/fn/fnext"
) )
// AddAppListener adds a listener that will be notified on App created. // AddAppListener adds a listener that will be notified on App created.
func (s *Server) AddAppListener(listener extensions.AppListener) { func (s *Server) AddAppListener(listener fnext.AppListener) {
s.appListeners = append(s.appListeners, listener) s.appListeners = append(s.appListeners, listener)
} }

View File

@@ -6,40 +6,17 @@ import (
"github.com/fnproject/fn/api" "github.com/fnproject/fn/api"
"github.com/fnproject/fn/api/models" "github.com/fnproject/fn/api/models"
"github.com/fnproject/fn/fnext"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
type ApiHandlerFunc func(w http.ResponseWriter, r *http.Request) func (s *Server) apiHandlerWrapperFunc(apiHandler fnext.ApiHandler) gin.HandlerFunc {
// ServeHTTP calls f(w, r).
func (f ApiHandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) {
f(w, r)
}
type ApiHandler interface {
// Handle(ctx context.Context)
ServeHTTP(w http.ResponseWriter, r *http.Request)
}
type ApiAppHandler interface {
// Handle(ctx context.Context)
ServeHTTP(w http.ResponseWriter, r *http.Request, app *models.App)
}
type ApiAppHandlerFunc func(w http.ResponseWriter, r *http.Request, app *models.App)
// ServeHTTP calls f(w, r).
func (f ApiAppHandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request, app *models.App) {
f(w, r, app)
}
func (s *Server) apiHandlerWrapperFunc(apiHandler ApiHandler) gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
apiHandler.ServeHTTP(c.Writer, c.Request) apiHandler.ServeHTTP(c.Writer, c.Request)
} }
} }
func (s *Server) apiAppHandlerWrapperFunc(apiHandler ApiAppHandler) gin.HandlerFunc { func (s *Server) apiAppHandlerWrapperFunc(apiHandler fnext.ApiAppHandler) gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
// get the app // get the app
appName := c.Param(api.CApp) appName := c.Param(api.CApp)
@@ -59,21 +36,7 @@ func (s *Server) apiAppHandlerWrapperFunc(apiHandler ApiAppHandler) gin.HandlerF
} }
} }
// per Route func (s *Server) apiRouteHandlerWrapperFunc(apiHandler fnext.ApiRouteHandler) gin.HandlerFunc {
type ApiRouteHandler interface {
// Handle(ctx context.Context)
ServeHTTP(w http.ResponseWriter, r *http.Request, app *models.App, route *models.Route)
}
type ApiRouteHandlerFunc func(w http.ResponseWriter, r *http.Request, app *models.App, route *models.Route)
// ServeHTTP calls f(w, r).
func (f ApiRouteHandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request, app *models.App, route *models.Route) {
f(w, r, app, route)
}
func (s *Server) apiRouteHandlerWrapperFunc(apiHandler ApiRouteHandler) gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
context := c.Request.Context() context := c.Request.Context()
// get the app // get the app
@@ -108,7 +71,7 @@ func (s *Server) apiRouteHandlerWrapperFunc(apiHandler ApiRouteHandler) gin.Hand
} }
// AddEndpoint adds an endpoint to /v1/x // AddEndpoint adds an endpoint to /v1/x
func (s *Server) AddEndpoint(method, path string, handler ApiHandler) { func (s *Server) AddEndpoint(method, path string, handler fnext.ApiHandler) {
v1 := s.Router.Group("/v1") v1 := s.Router.Group("/v1")
// v1.GET("/apps/:app/log", logHandler(cfg)) // v1.GET("/apps/:app/log", logHandler(cfg))
v1.Handle(method, path, s.apiHandlerWrapperFunc(handler)) v1.Handle(method, path, s.apiHandlerWrapperFunc(handler))
@@ -116,27 +79,27 @@ func (s *Server) AddEndpoint(method, path string, handler ApiHandler) {
// AddEndpoint adds an endpoint to /v1/x // AddEndpoint adds an endpoint to /v1/x
func (s *Server) AddEndpointFunc(method, path string, handler func(w http.ResponseWriter, r *http.Request)) { func (s *Server) AddEndpointFunc(method, path string, handler func(w http.ResponseWriter, r *http.Request)) {
s.AddEndpoint(method, path, ApiHandlerFunc(handler)) s.AddEndpoint(method, path, fnext.ApiHandlerFunc(handler))
} }
// AddAppEndpoint adds an endpoints to /v1/apps/:app/x // AddAppEndpoint adds an endpoints to /v1/apps/:app/x
func (s *Server) AddAppEndpoint(method, path string, handler ApiAppHandler) { func (s *Server) AddAppEndpoint(method, path string, handler fnext.ApiAppHandler) {
v1 := s.Router.Group("/v1") v1 := s.Router.Group("/v1")
v1.Handle(method, "/apps/:app"+path, s.apiAppHandlerWrapperFunc(handler)) v1.Handle(method, "/apps/:app"+path, s.apiAppHandlerWrapperFunc(handler))
} }
// AddAppEndpoint adds an endpoints to /v1/apps/:app/x // AddAppEndpoint adds an endpoints to /v1/apps/:app/x
func (s *Server) AddAppEndpointFunc(method, path string, handler func(w http.ResponseWriter, r *http.Request, app *models.App)) { func (s *Server) AddAppEndpointFunc(method, path string, handler func(w http.ResponseWriter, r *http.Request, app *models.App)) {
s.AddAppEndpoint(method, path, ApiAppHandlerFunc(handler)) s.AddAppEndpoint(method, path, fnext.ApiAppHandlerFunc(handler))
} }
// AddRouteEndpoint adds an endpoints to /v1/apps/:app/routes/:route/x // AddRouteEndpoint adds an endpoints to /v1/apps/:app/routes/:route/x
func (s *Server) AddRouteEndpoint(method, path string, handler ApiRouteHandler) { func (s *Server) AddRouteEndpoint(method, path string, handler fnext.ApiRouteHandler) {
v1 := s.Router.Group("/v1") v1 := s.Router.Group("/v1")
v1.Handle(method, "/apps/:app/routes/:route"+path, s.apiRouteHandlerWrapperFunc(handler)) // conflicts with existing wildcard v1.Handle(method, "/apps/:app/routes/:route"+path, s.apiRouteHandlerWrapperFunc(handler)) // conflicts with existing wildcard
} }
// AddRouteEndpoint adds an endpoints to /v1/apps/:app/routes/:route/x // AddRouteEndpoint adds an endpoints to /v1/apps/:app/routes/:route/x
func (s *Server) AddRouteEndpointFunc(method, path string, handler func(w http.ResponseWriter, r *http.Request, app *models.App, route *models.Route)) { func (s *Server) AddRouteEndpointFunc(method, path string, handler func(w http.ResponseWriter, r *http.Request, app *models.App, route *models.Route)) {
s.AddRouteEndpoint(method, path, ApiRouteHandlerFunc(handler)) s.AddRouteEndpoint(method, path, fnext.ApiRouteHandlerFunc(handler))
} }

30
api/server/extensions.go Normal file
View File

@@ -0,0 +1,30 @@
package server
import (
"fmt"
"log"
"github.com/fnproject/fn/fnext"
)
// TODO: Move this into `github.com/fnproject/fn` package after main is moved out of root dir.
var extensions = map[string]fnext.Extension{}
// RegisterExtension registers the extension so it's available, but does not initialize it or anything
func RegisterExtension(ext fnext.Extension) {
extensions[ext.Name()] = ext
}
// AddExtensionByName This essentially just makes sure the extensions are ordered properly.
// It could do some initialization if required too.
func (s *Server) AddExtensionByName(name string) {
fmt.Printf("extensions: %+v\n", extensions)
e, ok := extensions[name]
if !ok {
log.Fatalf("Extension %v not registered.\n", name)
}
err := e.Setup(s)
if err != nil {
log.Fatalf("Failed to add extension %v: %v\n", name, err)
}
}

View File

@@ -0,0 +1,96 @@
// This is middleware we're using for the entire server.
package server
import (
"context"
"fmt"
"strings"
"github.com/fnproject/fn/api"
"github.com/fnproject/fn/api/common"
"github.com/fnproject/fn/api/models"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
opentracing "github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/ext"
"github.com/sirupsen/logrus"
)
func optionalCorsWrap(r *gin.Engine) {
// By default no CORS are allowed unless one
// or more Origins are defined by the API_CORS
// environment variable.
corsStr := getEnv(EnvAPICORS, "")
if len(corsStr) > 0 {
origins := strings.Split(strings.Replace(corsStr, " ", "", -1), ",")
corsConfig := cors.DefaultConfig()
corsConfig.AllowOrigins = origins
logrus.Infof("CORS enabled for domains: %s", origins)
r.Use(cors.New(corsConfig))
}
}
// we should use http grr
func traceWrap(c *gin.Context) {
// try to grab a span from the request if made from another service, ignore err if not
wireContext, _ := opentracing.GlobalTracer().Extract(
opentracing.HTTPHeaders,
opentracing.HTTPHeadersCarrier(c.Request.Header))
// Create the span referring to the RPC client if available.
// If wireContext == nil, a root span will be created.
// TODO we should add more tags?
serverSpan := opentracing.StartSpan("serve_http", ext.RPCServerOption(wireContext), opentracing.Tag{Key: "path", Value: c.Request.URL.Path})
serverSpan.SetBaggageItem("fn_appname", c.Param(api.CApp))
serverSpan.SetBaggageItem("fn_path", c.Param(api.CRoute))
defer serverSpan.Finish()
ctx := opentracing.ContextWithSpan(c.Request.Context(), serverSpan)
c.Request = c.Request.WithContext(ctx)
c.Next()
}
func panicWrap(c *gin.Context) {
defer func(c *gin.Context) {
if rec := recover(); rec != nil {
err, ok := rec.(error)
if !ok {
err = fmt.Errorf("fn: %v", rec)
}
handleErrorResponse(c, err)
c.Abort()
}
}(c)
c.Next()
}
func loggerWrap(c *gin.Context) {
ctx, _ := common.LoggerWithFields(c.Request.Context(), extractFields(c))
if appName := c.Param(api.CApp); appName != "" {
c.Set(api.AppName, appName)
ctx = context.WithValue(ctx, api.AppName, appName)
}
if routePath := c.Param(api.CRoute); routePath != "" {
c.Set(api.Path, routePath)
ctx = context.WithValue(ctx, api.Path, routePath)
}
c.Request = c.Request.WithContext(ctx)
c.Next()
}
func appWrap(c *gin.Context) {
appName := c.GetString(api.AppName)
if appName == "" {
handleErrorResponse(c, models.ErrAppsMissingName)
c.Abort()
return
}
c.Next()
}

View File

@@ -1,8 +1,8 @@
package server package server
import "github.com/fnproject/fn/api/extensions" import "github.com/fnproject/fn/fnext"
// AddCallListener adds a listener that will be fired before and after a function is executed. // AddCallListener adds a listener that will be fired before and after a function is executed.
func (s *Server) AddCallListener(listener extensions.CallListener) { func (s *Server) AddCallListener(listener fnext.CallListener) {
s.Agent.AddCallListener(listener) s.Agent.AddCallListener(listener)
} }

View File

@@ -2,29 +2,58 @@ package server
import ( import (
"context" "context"
"fmt"
"net/http" "net/http"
"github.com/fnproject/fn/api/common" "github.com/fnproject/fn/api/common"
"github.com/fnproject/fn/fnext"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// Middleware just takes a http.Handler and returns one. So the next middle ware must be called type middlewareController struct {
// within the returned handler or it would be ignored. // NOTE: I tried to make this work as if it were a normal context, but it just doesn't work right. If someone
type Middleware interface { // does something like context.WithValue, then the return is a new &context.valueCtx{} which can't be cast. So now stuffing it into a value instead.
Chain(next http.Handler) http.Handler // context.Context
// separating this out so we can use it and don't have to reimplement context.Context above
ginContext *gin.Context
server *Server
functionCalled bool
} }
// MiddlewareFunc is a here to allow a plain function to be a middleware. // CallFunction bypasses any further gin routing and calls the function directly
type MiddlewareFunc func(next http.Handler) http.Handler func (c *middlewareController) CallFunction(w http.ResponseWriter, r *http.Request) {
c.functionCalled = true
// Chain used to allow middlewarefuncs to be middleware. ctx := r.Context()
func (m MiddlewareFunc) Chain(next http.Handler) http.Handler { ctx = context.WithValue(ctx, fnext.MiddlewareControllerKey, c)
return m(next) r = r.WithContext(ctx)
c.ginContext.Request = r
c.server.handleFunctionCall(c.ginContext)
c.ginContext.Abort()
}
func (c *middlewareController) FunctionCalled() bool {
return c.functionCalled
} }
func (s *Server) middlewareWrapperFunc(ctx context.Context) gin.HandlerFunc { func (s *Server) apiMiddlewareWrapper() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
if len(s.middlewares) > 0 { s.runMiddleware(c, s.apiMiddlewares)
}
}
func (s *Server) rootMiddlewareWrapper() gin.HandlerFunc {
return func(c *gin.Context) {
s.runMiddleware(c, s.rootMiddlewares)
}
}
// This is basically a single gin middleware that runs a bunch of fn middleware.
// The final handler will pass it back to gin for further processing.
func (s *Server) runMiddleware(c *gin.Context, ms []fnext.Middleware) {
if len(ms) == 0 {
c.Next()
return
}
defer func() { defer func() {
//This is so that if the server errors or panics on a middleware the server will still respond and not send eof to client. //This is so that if the server errors or panics on a middleware the server will still respond and not send eof to client.
err := recover() err := recover()
@@ -33,36 +62,68 @@ func (s *Server) middlewareWrapperFunc(ctx context.Context) gin.HandlerFunc {
handleErrorResponse(c, ErrInternalServerError) handleErrorResponse(c, ErrInternalServerError)
} }
}() }()
var h http.Handler
keepgoing := false ctx := context.WithValue(c.Request.Context(), fnext.MiddlewareControllerKey, s.newMiddlewareController(c))
h = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { last := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c.Request = c.Request.WithContext(r.Context()) fmt.Println("final function called")
keepgoing = true // check for bypass
mctx := fnext.GetMiddlewareController(r.Context())
if mctx.FunctionCalled() {
fmt.Println("function already called, skipping")
c.Abort()
return
}
c.Next()
}) })
s.chainAndServe(c.Writer, c.Request, h) chainAndServe(ms, c.Writer, c.Request.WithContext(ctx), last)
if !keepgoing {
c.Abort()
}
} }
func (s *Server) newMiddlewareController(c *gin.Context) *middlewareController {
return &middlewareController{
ginContext: c,
server: s,
} }
} }
func (s *Server) chainAndServe(w http.ResponseWriter, r *http.Request, h http.Handler) { // chainAndServe essentially makes a chain of middleware wrapped around each other, then calls ServerHTTP on the end result.
for _, m := range s.middlewares { // then each middleware also calls ServeHTTP within it
h = m.Chain(h) func chainAndServe(ms []fnext.Middleware, w http.ResponseWriter, r *http.Request, last http.Handler) {
h := last
// These get chained in reverse order so they play out in the right order. Don't ask.
for i := len(ms) - 1; i >= 0; i-- {
m := ms[i]
h = m.Handle(h)
} }
h.ServeHTTP(w, r) h.ServeHTTP(w, r)
} }
// AddMiddleware add middleware // AddMiddleware DEPRECATED - see AddAPIMiddleware
func (s *Server) AddMiddleware(m Middleware) { func (s *Server) AddMiddleware(m fnext.Middleware) {
//Prepend to array so that we can do first,second,third,last,third,second,first s.AddAPIMiddleware(m)
//and not third,second,first,last,first,second,third
s.middlewares = append([]Middleware{m}, s.middlewares...)
} }
// AddMiddlewareFunc add middlewarefunc // AddMiddlewareFunc DEPRECATED - see AddAPIMiddlewareFunc
func (s *Server) AddMiddlewareFunc(m MiddlewareFunc) { func (s *Server) AddMiddlewareFunc(m fnext.MiddlewareFunc) {
s.AddMiddleware(m) s.AddAPIMiddlewareFunc(m)
}
// AddAPIMiddleware add middleware
func (s *Server) AddAPIMiddleware(m fnext.Middleware) {
s.apiMiddlewares = append(s.apiMiddlewares, m)
}
// AddAPIMiddlewareFunc add middlewarefunc
func (s *Server) AddAPIMiddlewareFunc(m fnext.MiddlewareFunc) {
s.AddAPIMiddleware(m)
}
// AddRootMiddleware add middleware add middleware for end user applications
func (s *Server) AddRootMiddleware(m fnext.Middleware) {
s.rootMiddlewares = append(s.rootMiddlewares, m)
}
// AddRootMiddlewareFunc add middleware for end user applications
func (s *Server) AddRootMiddlewareFunc(m fnext.MiddlewareFunc) {
s.AddRootMiddleware(m)
} }

View File

@@ -1,39 +1,59 @@
package server package server
import ( import (
"context"
"fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os"
"strings"
"testing" "testing"
"github.com/fnproject/fn/api/datastore"
"github.com/fnproject/fn/api/logs"
"github.com/fnproject/fn/api/models"
"github.com/fnproject/fn/api/mqs"
"github.com/fnproject/fn/fnext"
"github.com/gin-gonic/gin"
) )
func TestMain(m *testing.M) {
// call flag.Parse() here if TestMain uses flags
os.Exit(m.Run())
}
type middleWareStruct struct { type middleWareStruct struct {
name string name string
} }
func (m *middleWareStruct) Chain(next http.Handler) http.Handler { func (m *middleWareStruct) Handle(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(m.name + ",")) w.Write([]byte(m.name + ","))
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
}) })
} }
func TestMiddleWareChaining(t *testing.T) { func TestMiddlewareChaining(t *testing.T) {
var lastHandler http.Handler var lastHandler http.Handler
lastHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { lastHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("last")) w.Write([]byte("last"))
}) })
s := Server{} s := Server{}
s.AddMiddleware(&middleWareStruct{"first"}) s.AddAPIMiddleware(&middleWareStruct{"first"})
s.AddMiddleware(&middleWareStruct{"second"}) s.AddAPIMiddleware(&middleWareStruct{"second"})
s.AddMiddleware(&middleWareStruct{"third"}) s.AddAPIMiddleware(&middleWareStruct{"third"})
s.AddMiddleware(&middleWareStruct{"fourth"}) s.AddAPIMiddleware(&middleWareStruct{"fourth"})
c := &gin.Context{}
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
req, _ := http.NewRequest("get", "http://localhost/", nil) req, _ := http.NewRequest("get", "http://localhost/", nil)
ctx := context.WithValue(req.Context(), fnext.MiddlewareControllerKey, s.newMiddlewareController(c))
req = req.WithContext(ctx)
c.Request = req
s.chainAndServe(rec, req, lastHandler) chainAndServe(s.apiMiddlewares, rec, req, lastHandler)
result, err := ioutil.ReadAll(rec.Result().Body) result, err := ioutil.ReadAll(rec.Result().Body)
if err != nil { if err != nil {
@@ -41,6 +61,89 @@ func TestMiddleWareChaining(t *testing.T) {
} }
if string(result) != "first,second,third,fourth,last" { if string(result) != "first,second,third,fourth,last" {
t.Fatal("You failed to chain correctly.", string(result)) t.Fatal("You failed to chain correctly:", string(result))
}
}
func TestRootMiddleware(t *testing.T) {
ds := datastore.NewMockInit(
[]*models.App{
{Name: "myapp", Config: models.Config{}},
{Name: "myapp2", Config: models.Config{}},
},
[]*models.Route{
{Path: "/", AppName: "myapp", Image: "fnproject/hello", Type: "sync", Memory: 128, Timeout: 30, IdleTimeout: 30, Headers: map[string][]string{"X-Function": {"Test"}}},
{Path: "/myroute", AppName: "myapp", Image: "fnproject/hello", Type: "sync", Memory: 128, Timeout: 30, IdleTimeout: 30, Headers: map[string][]string{"X-Function": {"Test"}}},
{Path: "/app2func", AppName: "myapp2", Image: "fnproject/hello", Type: "sync", Memory: 128, Timeout: 30, IdleTimeout: 30, Headers: map[string][]string{"X-Function": {"Test"}},
Config: map[string]string{"NAME": "johnny"},
},
}, nil,
)
rnr, cancelrnr := testRunner(t, ds)
defer cancelrnr()
fnl := logs.NewMock()
srv := testServer(ds, &mqs.Mock{}, fnl, rnr)
srv.AddRootMiddlewareFunc(func(next http.Handler) http.Handler {
// this one will override a call to the API based on a header
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("funcit") != "" {
fmt.Fprintf(os.Stderr, "breaker breaker!\n")
ctx := r.Context()
// TODO: this is a little dicey, should have some functions to set these in case the context keys change or something.
ctx = context.WithValue(ctx, "app_name", "myapp2")
ctx = context.WithValue(ctx, "path", "/app2func")
mctx := fnext.GetMiddlewareController(ctx)
mctx.CallFunction(w, r.WithContext(ctx))
return
}
// If any context changes, user should use this: next.ServeHTTP(w, r.WithContext(ctx))
next.ServeHTTP(w, r)
})
})
srv.AddRootMiddlewareFunc(func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// fmt.Fprintf(os.Stderr, "middle log\n")
next.ServeHTTP(w, r)
})
})
srv.AddRootMiddleware(&middleWareStruct{"middle"})
for i, test := range []struct {
path string
body string
method string
headers map[string][]string
expectedCode int
expectedInBody string
}{
{"/r/myapp", ``, "GET", map[string][]string{}, http.StatusOK, "middle"},
{"/r/myapp/myroute", ``, "GET", map[string][]string{}, http.StatusOK, "middle"},
{"/v1/apps", ``, "GET", map[string][]string{"funcit": {"Test"}}, http.StatusOK, "johnny"},
} {
body := strings.NewReader(test.body)
req, err := http.NewRequest(test.method, "http://127.0.0.1:8080"+test.path, body)
if err != nil {
t.Fatalf("Test: Could not create %s request to %s: %v", test.method, test.path, err)
}
for k, v := range test.headers {
req.Header.Add(k, v[0])
}
fmt.Println("TESTING:", req.URL.String())
_, rec := routerRequest2(t, srv.Router, req)
// t.Log("REC: %+v\n", rec)
result, err := ioutil.ReadAll(rec.Result().Body)
if err != nil {
t.Fatal(err)
}
rbody := string(result)
t.Log("rbody:", rbody)
if !strings.Contains(rbody, test.expectedInBody) {
t.Fatal(i, "middleware didn't work correctly", string(result))
}
} }
} }

View File

@@ -2,9 +2,9 @@ package server
import ( import (
"bytes" "bytes"
"errors"
"net/http" "net/http"
"path" "path"
"strings"
"time" "time"
"github.com/fnproject/fn/api" "github.com/fnproject/fn/api"
@@ -20,24 +20,35 @@ type runnerResponse struct {
Error *models.ErrorBody `json:"error,omitempty"` Error *models.ErrorBody `json:"error,omitempty"`
} }
func (s *Server) handleRequest(c *gin.Context) { // handleFunctionCall executes the function.
if strings.HasPrefix(c.Request.URL.Path, "/v1") { // Requires the following in the context:
c.Status(http.StatusNotFound) // * "app_name"
// * "path"
func (s *Server) handleFunctionCall(c *gin.Context) {
// @treeder: Is this necessary? An app could have this prefix too. Leaving here for review.
// if strings.HasPrefix(c.Request.URL.Path, "/v1") {
// c.Status(http.StatusNotFound)
// return
// }
ctx := c.Request.Context()
var p string
r := ctx.Value(api.Path)
if r == nil {
p = "/"
} else {
p = r.(string)
}
var a string
ai := ctx.Value(api.AppName)
if ai == nil {
handleErrorResponse(c, errors.New("app name not set"))
return return
} }
a = ai.(string)
r, routeExists := c.Get(api.Path) s.serve(c, a, path.Clean(p))
if !routeExists {
r = "/"
}
reqRoute := &models.Route{
AppName: c.MustGet(api.AppName).(string),
Path: path.Clean(r.(string)),
}
s.serve(c, reqRoute.AppName, reqRoute.Path)
} }
// convert gin.Params to agent.Params to avoid introducing gin // convert gin.Params to agent.Params to avoid introducing gin

View File

@@ -11,24 +11,19 @@ import (
"os" "os"
"path" "path"
"strconv" "strconv"
"strings"
"syscall" "syscall"
"github.com/fnproject/fn/api"
"github.com/fnproject/fn/api/agent" "github.com/fnproject/fn/api/agent"
"github.com/fnproject/fn/api/common"
"github.com/fnproject/fn/api/datastore" "github.com/fnproject/fn/api/datastore"
"github.com/fnproject/fn/api/datastore/cache" "github.com/fnproject/fn/api/datastore/cache"
"github.com/fnproject/fn/api/extensions"
"github.com/fnproject/fn/api/id" "github.com/fnproject/fn/api/id"
"github.com/fnproject/fn/api/logs" "github.com/fnproject/fn/api/logs"
"github.com/fnproject/fn/api/models" "github.com/fnproject/fn/api/models"
"github.com/fnproject/fn/api/mqs" "github.com/fnproject/fn/api/mqs"
"github.com/fnproject/fn/api/version" "github.com/fnproject/fn/api/version"
"github.com/gin-contrib/cors" "github.com/fnproject/fn/fnext"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/opentracing/opentracing-go" "github.com/opentracing/opentracing-go"
"github.com/opentracing/opentracing-go/ext"
"github.com/openzipkin/zipkin-go-opentracing" "github.com/openzipkin/zipkin-go-opentracing"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@@ -58,8 +53,9 @@ type Server struct {
MQ models.MessageQueue MQ models.MessageQueue
LogDB models.LogStore LogDB models.LogStore
appListeners []extensions.AppListener appListeners []fnext.AppListener
middlewares []Middleware rootMiddlewares []fnext.Middleware
apiMiddlewares []fnext.Middleware
} }
// NewFromEnv creates a new Functions server based on env vars. // NewFromEnv creates a new Functions server based on env vars.
@@ -96,23 +92,6 @@ func NewFromURLs(ctx context.Context, dbURL, mqURL, logstoreURL string, opts ...
return New(ctx, ds, mq, logDB, opts...) return New(ctx, ds, mq, logDB, opts...)
} }
func optionalCorsWrap(r *gin.Engine) {
// By default no CORS are allowed unless one
// or more Origins are defined by the API_CORS
// environment variable.
corsStr := getEnv(EnvAPICORS, "")
if len(corsStr) > 0 {
origins := strings.Split(strings.Replace(corsStr, " ", "", -1), ",")
corsConfig := cors.DefaultConfig()
corsConfig.AllowOrigins = origins
logrus.Infof("CORS enabled for domains: %s", origins)
r.Use(cors.New(corsConfig))
}
}
// New creates a new Functions server with the passed in datastore, message queue and API URL // New creates a new Functions server with the passed in datastore, message queue and API URL
func New(ctx context.Context, ds models.Datastore, mq models.MessageQueue, ls models.LogStore, opts ...ServerOption) *Server { func New(ctx context.Context, ds models.Datastore, mq models.MessageQueue, ls models.LogStore, opts ...ServerOption) *Server {
setTracer() setTracer()
@@ -125,10 +104,10 @@ func New(ctx context.Context, ds models.Datastore, mq models.MessageQueue, ls mo
LogDB: ls, LogDB: ls,
} }
// NOTE: testServer() in tests doesn't use these
setMachineID() setMachineID()
s.Router.Use(loggerWrap, traceWrap, panicWrap) s.Router.Use(loggerWrap, traceWrap, panicWrap)
optionalCorsWrap(s.Router) optionalCorsWrap(s.Router)
s.bindHandlers(ctx) s.bindHandlers(ctx)
for _, opt := range opts { for _, opt := range opts {
@@ -140,26 +119,6 @@ func New(ctx context.Context, ds models.Datastore, mq models.MessageQueue, ls mo
return s return s
} }
// we should use http grr
func traceWrap(c *gin.Context) {
// try to grab a span from the request if made from another service, ignore err if not
wireContext, _ := opentracing.GlobalTracer().Extract(
opentracing.HTTPHeaders,
opentracing.HTTPHeadersCarrier(c.Request.Header))
// Create the span referring to the RPC client if available.
// If wireContext == nil, a root span will be created.
// TODO we should add more tags?
serverSpan := opentracing.StartSpan("serve_http", ext.RPCServerOption(wireContext), opentracing.Tag{Key: "path", Value: c.Request.URL.Path})
serverSpan.SetBaggageItem("fn_appname", c.Param(api.CApp))
serverSpan.SetBaggageItem("fn_path", c.Param(api.CRoute))
defer serverSpan.Finish()
ctx := opentracing.ContextWithSpan(c.Request.Context(), serverSpan)
c.Request = c.Request.WithContext(ctx)
c.Next()
}
func setTracer() { func setTracer() {
var ( var (
debugMode = false debugMode = false
@@ -239,51 +198,6 @@ func whoAmI() net.IP {
return nil return nil
} }
func panicWrap(c *gin.Context) {
defer func(c *gin.Context) {
if rec := recover(); rec != nil {
err, ok := rec.(error)
if !ok {
err = fmt.Errorf("fn: %v", rec)
}
handleErrorResponse(c, err)
c.Abort()
}
}(c)
c.Next()
}
func loggerWrap(c *gin.Context) {
ctx, _ := common.LoggerWithFields(c.Request.Context(), extractFields(c))
if appName := c.Param(api.CApp); appName != "" {
c.Set(api.AppName, appName)
c.Request = c.Request.WithContext(context.WithValue(c.Request.Context(), api.AppName, appName))
}
if routePath := c.Param(api.CRoute); routePath != "" {
c.Set(api.Path, routePath)
c.Request = c.Request.WithContext(context.WithValue(c.Request.Context(), api.Path, routePath))
}
c.Request = c.Request.WithContext(ctx)
c.Next()
}
func appWrap(c *gin.Context) {
appName := c.GetString(api.AppName)
if appName == "" {
handleErrorResponse(c, models.ErrAppsMissingName)
c.Abort()
return
}
c.Next()
}
func (s *Server) handleRunnerRequest(c *gin.Context) {
s.handleRequest(c)
}
func extractFields(c *gin.Context) logrus.Fields { func extractFields(c *gin.Context) logrus.Fields {
fields := logrus.Fields{"action": path.Base(c.HandlerName())} fields := logrus.Fields{"action": path.Base(c.HandlerName())}
for _, param := range c.Params { for _, param := range c.Params {
@@ -342,15 +256,18 @@ func (s *Server) startGears(ctx context.Context, cancel context.CancelFunc) {
func (s *Server) bindHandlers(ctx context.Context) { func (s *Server) bindHandlers(ctx context.Context) {
engine := s.Router engine := s.Router
// now for extendible middleware
engine.Use(s.rootMiddlewareWrapper())
engine.GET("/", handlePing) engine.GET("/", handlePing)
engine.GET("/version", handleVersion) engine.GET("/version", handleVersion)
// TODO: move the following under v1
engine.GET("/stats", s.handleStats) engine.GET("/stats", s.handleStats)
engine.GET("/metrics", s.handlePrometheusMetrics) engine.GET("/metrics", s.handlePrometheusMetrics)
{ {
v1 := engine.Group("/v1") v1 := engine.Group("/v1")
v1.Use(s.middlewareWrapperFunc(ctx)) v1.Use(s.apiMiddlewareWrapper())
v1.GET("/apps", s.handleAppList) v1.GET("/apps", s.handleAppList)
v1.POST("/apps", s.handleAppCreate) v1.POST("/apps", s.handleAppCreate)
@@ -379,8 +296,8 @@ func (s *Server) bindHandlers(ctx context.Context) {
{ {
runner := engine.Group("/r") runner := engine.Group("/r")
runner.Use(appWrap) runner.Use(appWrap)
runner.Any("/:app", s.handleRunnerRequest) runner.Any("/:app", s.handleFunctionCall)
runner.Any("/:app/*route", s.handleRunnerRequest) runner.Any("/:app/*route", s.handleFunctionCall)
} }
engine.NoRoute(func(c *gin.Context) { engine.NoRoute(func(c *gin.Context) {

View File

@@ -44,6 +44,10 @@ func routerRequest(t *testing.T, router *gin.Engine, method, path string, body i
if err != nil { if err != nil {
t.Fatalf("Test: Could not create %s request to %s: %v", method, path, err) t.Fatalf("Test: Could not create %s request to %s: %v", method, path, err)
} }
return routerRequest2(t, router, req)
}
func routerRequest2(t *testing.T, router *gin.Engine, req *http.Request) (*http.Request, *httptest.ResponseRecorder) {
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
router.ServeHTTP(rec, req) router.ServeHTTP(rec, req)

View File

@@ -0,0 +1,3 @@
# Docs for Contributors
* [Writing Extensions](extending.md)

View File

@@ -0,0 +1,95 @@
# Writing Extensions
Fn is extensible so you can add custom functionality and extend the project without needing to modify the core.
There are multiple ways to extend the functionality of Fn.
1. Listeners - listen to API events such as a route getting updated and react accordingly.
1. Middleware - a chain of middleware is executed before an API handler is called.
1. Add API Endpoints - extend the default Fn API.
To create an extension, there are just a couple of rules to follow.
* All config should be via ENV vars.
## Code
Your extension code needs to register itself with an `init()` method which states it's name and a function
to be called during setup:
```go
func init() {
server.RegisterExtension(&fnext.Extension{
Name: "logspam",
Setup: setup, // Fn will call this during startup
})
}
func setup(s *fnext.ExtServer) error {
// Add all the hooks you extension needs here
s.AddCallListener(&LogSpam{})
}
```
## Listeners
Listeners are the main way to extend Fn.
The following listener types are supported:
* App Listeners - [GoDoc](https://godoc.org/github.com/fnproject/fn/api/server#AppListener)
* Call Listeners - [GoDoc](https://godoc.org/github.com/fnproject/fn/api/server#CallListener)
### Creating a Listener
You can easily use add listeners by creating a struct with valid methods satisfying the interface
for the respective listener, adding it to `main.go` then compiling.
Example:
```go
package main
import (
"context"
"github.com/fnproject/fn/api/server"
"github.com/fnproject/fn/api/models"
)
type myCustomListener struct{}
func (c *myCustomListener) BeforeAppCreate(ctx context.Context, app *models.App) error { return nil }
func (c *myCustomListener) AfterAppCreate(ctx context.Context, app *models.App) error { return nil }
func (c *myCustomListener) BeforeAppUpdate(ctx context.Context, app *models.App) error { return nil }
func (c *myCustomListener) AfterAppUpdate(ctx context.Context, app *models.App) error { return nil }
func (c *myCustomListener) BeforeAppDelete(ctx context.Context, app *models.App) error { return nil }
func (c *myCustomListener) BeforeAppDelete(ctx context.Context, app *models.App) error { return nil }
function main () {
srv := server.New(/* Here all required parameters to initialize the server */)
srv.AddAppListener(myCustomListener)
srv.Run()
}
```
## Middleware
Middleware enables you to add functionality to every API request. For every request, the chain of Middleware will be called
in order, allowing you to modify or reject requests, as well as write output and cancel the chain.
NOTES:
* middleware is responsible for writing output if it's going to cancel the chain.
See examples of this in [examples/middleware/main.go](../../examples/middleware/main.go).
## Adding API Endpoints
You can add API endpoints to the Fn server by using the `AddEndpoint` and `AddEndpointFunc` methods.
See examples of this in [examples/extensions/main.go](../../examples/extensions/main.go).

View File

@@ -1,73 +1,25 @@
# Extending Fn # Building Custom Server with Extensions
FN is extensible so you can add custom functionality and extend the project without needing to modify the core. You can easily add any number of extensions to Fn and then build your own custom image.
There are multiple ways to extend the functionality of Fn. Simply create an `ext.yaml` file with the extensions you want added:
1. Listeners - listen to API events such as a route getting updated and react accordingly. ```yaml
1. Middleware - a chain of middleware is executed before an API handler is called. extensions:
1. Add API Endpoints - extend the default Fn API. - name: github.com/treeder/fn-ext-example/logspam
- name: github.com/treeder/fn-ext-example/logspam2
## Listeners
Listeners are the main way to extend Fn.
The following listener types are supported:
* App Listeners - [GoDoc](https://godoc.org/github.com/fnproject/fn/api/server#AppListener)
* Call Listeners - [GoDoc](https://godoc.org/github.com/fnproject/fn/api/server#CallListener)
### Creating a Listener
You can easily use add listeners by creating a struct with valid methods satisfying the interface
for the respective listener, adding it to `main.go` then compiling.
Example:
```
package main
import (
"context"
"github.com/fnproject/fn/api/server"
"github.com/fnproject/fn/api/models"
)
type myCustomListener struct{}
func (c *myCustomListener) BeforeAppCreate(ctx context.Context, app *models.App) error { return nil }
func (c *myCustomListener) AfterAppCreate(ctx context.Context, app *models.App) error { return nil }
func (c *myCustomListener) BeforeAppUpdate(ctx context.Context, app *models.App) error { return nil }
func (c *myCustomListener) AfterAppUpdate(ctx context.Context, app *models.App) error { return nil }
func (c *myCustomListener) BeforeAppDelete(ctx context.Context, app *models.App) error { return nil }
func (c *myCustomListener) BeforeAppDelete(ctx context.Context, app *models.App) error { return nil }
function main () {
srv := server.New(/* Here all required parameters to initialize the server */)
srv.AddAppListener(myCustomListener)
srv.Run()
}
``` ```
## Middleware Build it:
Middleware enables you to add functionality to every API request. For every request, the chain of Middleware will be called ```sh
in order, allowing you to modify or reject requests, as well as write output and cancel the chain. fn build-server -t imageuser/imagename
```
NOTES: `-t` takes the same input as `docker build -t`, tagging your image.
* middleware is responsible for writing output if it's going to cancel the chain. Now run your new server:
* cancel the chain by returning an error from your Middleware's Serve method.
See examples of this in [examples/middleware/main.go](../../examples/middleware/main.go). ```sh
docker run --rm --name fnserver -it -v /var/run/docker.sock:/var/run/docker.sock -v $PWD/data:/app/data -p 8080:8080 imageuser/imagename
## Adding API Endpoints ```
You can add API endpoints to the Fn server by using the `AddEndpoint` and `AddEndpointFunc` methods.
See examples of this in [examples/extensions/main.go](../../examples/extensions/main.go).

View File

@@ -33,7 +33,7 @@ func main() {
type CustomMiddleware struct { type CustomMiddleware struct {
} }
func (h *CustomMiddleware) Chain(next http.Handler) http.Handler { func (h *CustomMiddleware) Handle(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Println("CustomMiddleware called") fmt.Println("CustomMiddleware called")

43
fnext/api.go Normal file
View File

@@ -0,0 +1,43 @@
package fnext
import (
"net/http"
"github.com/fnproject/fn/api/models"
)
type ApiHandlerFunc func(w http.ResponseWriter, r *http.Request)
// ServeHTTP calls f(w, r).
func (f ApiHandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) {
f(w, r)
}
type ApiHandler interface {
// Handle(ctx context.Context)
ServeHTTP(w http.ResponseWriter, r *http.Request)
}
type ApiAppHandler interface {
// Handle(ctx context.Context)
ServeHTTP(w http.ResponseWriter, r *http.Request, app *models.App)
}
type ApiAppHandlerFunc func(w http.ResponseWriter, r *http.Request, app *models.App)
// ServeHTTP calls f(w, r).
func (f ApiAppHandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request, app *models.App) {
f(w, r, app)
}
type ApiRouteHandler interface {
// Handle(ctx context.Context)
ServeHTTP(w http.ResponseWriter, r *http.Request, app *models.App, route *models.Route)
}
type ApiRouteHandlerFunc func(w http.ResponseWriter, r *http.Request, app *models.App, route *models.Route)
// ServeHTTP calls f(w, r).
func (f ApiRouteHandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request, app *models.App, route *models.Route) {
f(w, r, app, route)
}

View File

@@ -1,4 +1,4 @@
package extensions package fnext
import ( import (
"context" "context"

52
fnext/middleware.go Normal file
View File

@@ -0,0 +1,52 @@
package fnext
import (
"context"
"net/http"
)
var (
// MiddlewareControllerKey is a context key. It can be used in handlers with context.WithValue to
// access the MiddlewareContext.
MiddlewareControllerKey = contextKey("middleware-controller")
)
// MiddlewareController allows a bit more flow control to the middleware, since we multiple paths a request can go down.
// 1) Could be routed towards the API
// 2) Could be routed towards a function
type MiddlewareController interface {
// CallFunction skips any API routing and goes down the function path
CallFunction(w http.ResponseWriter, r *http.Request)
// If function has already been called
FunctionCalled() bool
}
// GetMiddlewareController returns MiddlewareController from context.
func GetMiddlewareController(ctx context.Context) MiddlewareController {
// return ctx.(MiddlewareContext)
v := ctx.Value(MiddlewareControllerKey)
return v.(MiddlewareController)
}
// Middleware just takes a http.Handler and returns one. So the next middle ware must be called
// within the returned handler or it would be ignored.
type Middleware interface {
Handle(next http.Handler) http.Handler
}
// MiddlewareFunc is a here to allow a plain function to be a middleware.
type MiddlewareFunc func(next http.Handler) http.Handler
// Handle used to allow middlewarefuncs to be middleware.
func (m MiddlewareFunc) Handle(next http.Handler) http.Handler {
return m(next)
}
// good reading on this: https://twitter.com/sajma/status/757217773852487680
type contextKey string
// func (c contextKey) String() string {
// return "fnext context key " + string(c)
// }

40
fnext/setup.go Normal file
View File

@@ -0,0 +1,40 @@
package fnext
import (
"net/http"
"github.com/fnproject/fn/api/models"
)
type Extension interface {
Name() string
Setup(s ExtServer) error
}
// NOTE: ExtServer limits what the extension should do and prevents dependency loop
type ExtServer interface {
AddAppListener(listener AppListener)
AddCallListener(listener CallListener)
// AddAPIMiddleware add middleware
AddAPIMiddleware(m Middleware)
// AddAPIMiddlewareFunc add middlewarefunc
AddAPIMiddlewareFunc(m MiddlewareFunc)
// AddRootMiddleware add middleware add middleware for end user applications
AddRootMiddleware(m Middleware)
// AddRootMiddlewareFunc add middleware for end user applications
AddRootMiddlewareFunc(m MiddlewareFunc)
// AddEndpoint adds an endpoint to /v1/x
AddEndpoint(method, path string, handler ApiHandler)
// AddEndpoint adds an endpoint to /v1/x
AddEndpointFunc(method, path string, handler func(w http.ResponseWriter, r *http.Request))
// AddAppEndpoint adds an endpoints to /v1/apps/:app/x
AddAppEndpoint(method, path string, handler ApiAppHandler)
// AddAppEndpoint adds an endpoints to /v1/apps/:app/x
AddAppEndpointFunc(method, path string, handler func(w http.ResponseWriter, r *http.Request, app *models.App))
// AddRouteEndpoint adds an endpoints to /v1/apps/:app/routes/:route/x
AddRouteEndpoint(method, path string, handler ApiRouteHandler)
// AddRouteEndpoint adds an endpoints to /v1/apps/:app/routes/:route/x
AddRouteEndpointFunc(method, path string, handler func(w http.ResponseWriter, r *http.Request, app *models.App, route *models.Route))
}

View File

@@ -11,7 +11,11 @@ type Person struct {
} }
func main() { func main() {
p := &Person{Name: "World"} n := os.Getenv("NAME") // can grab name from env or input
if n == "" {
n = "World"
}
p := &Person{Name: n}
json.NewDecoder(os.Stdin).Decode(p) json.NewDecoder(os.Stdin).Decode(p)
fmt.Printf("Hello %v!\n", p.Name) fmt.Printf("Hello %v!\n", p.Name)
} }

View File

@@ -4,12 +4,11 @@ import (
"context" "context"
"github.com/fnproject/fn/api/server" "github.com/fnproject/fn/api/server"
// EXTENSIONS: Add extension imports here or use `fn build-server`. Learn more: https://github.com/fnproject/fn/blob/master/docs/operating/extending.md
) )
func main() { func main() {
ctx := context.Background() ctx := context.Background()
funcServer := server.NewFromEnv(ctx) funcServer := server.NewFromEnv(ctx)
// Setup your custom extensions, listeners, etc here
funcServer.Start(ctx) funcServer.Start(ctx)
} }