mirror of
https://github.com/fnproject/fn.git
synced 2022-10-28 21:29:17 +03:00
Middleware (#502)
* API endpoint extensions working. extensions example. extensions example. * Added server.NewEnv and some docs for the API extensions example. extensions example. extensions example. * Uncommented special handler stuff. * First example of middleware. easier to use. * Added a special Middleware context to make middleware easier to use. * Fix tests. * Cleanup based on PR comments.
This commit is contained in:
102
api/server/middleware.go
Normal file
102
api/server/middleware.go
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
// TODO: it would be nice to move these into the top level folder so people can use these with the "functions" package, eg: functions.AddMiddleware(...)
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/iron-io/functions/api/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Middleware is the interface required for implementing functions middlewar
|
||||||
|
type Middleware interface {
|
||||||
|
// Serve is what the Middleware must implement. Can modify the request, write output, etc.
|
||||||
|
// todo: should we abstract the HTTP out of this? In case we want to support other protocols.
|
||||||
|
Serve(ctx MiddlewareContext, w http.ResponseWriter, r *http.Request, app *models.App) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// MiddlewareFunc func form of Middleware
|
||||||
|
type MiddlewareFunc func(ctx MiddlewareContext, w http.ResponseWriter, r *http.Request, app *models.App) error
|
||||||
|
|
||||||
|
// Serve wrapper
|
||||||
|
func (f MiddlewareFunc) Serve(ctx MiddlewareContext, w http.ResponseWriter, r *http.Request, app *models.App) error {
|
||||||
|
return f(ctx, w, r, app)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MiddlewareContext extends context.Context for Middleware
|
||||||
|
type MiddlewareContext interface {
|
||||||
|
context.Context
|
||||||
|
// Middleware can call Next() explicitly to call the next middleware in the chain. If Next() is not called and an error is not returned, Next() will automatically be called.
|
||||||
|
Next()
|
||||||
|
// Index returns the index of where we're at in the chain
|
||||||
|
Index() int
|
||||||
|
}
|
||||||
|
|
||||||
|
type middlewareContextImpl struct {
|
||||||
|
context.Context
|
||||||
|
|
||||||
|
ginContext *gin.Context
|
||||||
|
nextCalled bool
|
||||||
|
index int
|
||||||
|
middlewares []Middleware
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *middlewareContextImpl) Next() {
|
||||||
|
c.nextCalled = true
|
||||||
|
c.index++
|
||||||
|
c.serveNext()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *middlewareContextImpl) serveNext() {
|
||||||
|
if c.Index() >= len(c.middlewares) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// make shallow copy:
|
||||||
|
fctx2 := *c
|
||||||
|
fctx2.nextCalled = false
|
||||||
|
r := c.ginContext.Request.WithContext(fctx2)
|
||||||
|
err := c.middlewares[c.Index()].Serve(&fctx2, c.ginContext.Writer, r, nil)
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Warnln("Middleware error")
|
||||||
|
// todo: might be a good idea to check if anything is written yet, and if not, output the error: simpleError(err)
|
||||||
|
// see: http://stackoverflow.com/questions/39415827/golang-http-check-if-responsewriter-has-been-written
|
||||||
|
c.ginContext.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !fctx2.nextCalled {
|
||||||
|
// then we automatically call next
|
||||||
|
fctx2.Next()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *middlewareContextImpl) Index() int {
|
||||||
|
return c.index
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) middlewareWrapperFunc(ctx context.Context) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
if len(s.middlewares) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx = c.MustGet("ctx").(context.Context)
|
||||||
|
fctx := &middlewareContextImpl{Context: ctx}
|
||||||
|
fctx.index = -1
|
||||||
|
fctx.ginContext = c
|
||||||
|
fctx.middlewares = s.middlewares
|
||||||
|
// start the chain:
|
||||||
|
fctx.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddAppEndpoint adds an endpoints to /v1/apps/:app/x
|
||||||
|
func (s *Server) AddMiddleware(m Middleware) {
|
||||||
|
s.middlewares = append(s.middlewares, m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddAppEndpoint adds an endpoints to /v1/apps/:app/x
|
||||||
|
func (s *Server) AddMiddlewareFunc(m func(ctx MiddlewareContext, w http.ResponseWriter, r *http.Request, app *models.App) error) {
|
||||||
|
s.AddMiddleware(MiddlewareFunc(m))
|
||||||
|
}
|
||||||
@@ -32,7 +32,7 @@ func testRouterAsync(ds models.Datastore, mq models.MessageQueue, rnr *runner.Ru
|
|||||||
r.Use(gin.Logger())
|
r.Use(gin.Logger())
|
||||||
|
|
||||||
r.Use(prepareMiddleware(ctx))
|
r.Use(prepareMiddleware(ctx))
|
||||||
s.bindHandlers()
|
s.bindHandlers(ctx)
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ type Server struct {
|
|||||||
|
|
||||||
specialHandlers []SpecialHandler
|
specialHandlers []SpecialHandler
|
||||||
appListeners []AppListener
|
appListeners []AppListener
|
||||||
|
middlewares []Middleware
|
||||||
runnerListeners []RunnerListener
|
runnerListeners []RunnerListener
|
||||||
|
|
||||||
mu sync.Mutex // protects hotroutes
|
mu sync.Mutex // protects hotroutes
|
||||||
@@ -95,7 +96,7 @@ func New(ctx context.Context, ds models.Datastore, mq models.MessageQueue, apiUR
|
|||||||
}
|
}
|
||||||
|
|
||||||
s.Router.Use(prepareMiddleware(ctx))
|
s.Router.Use(prepareMiddleware(ctx))
|
||||||
s.bindHandlers()
|
s.bindHandlers(ctx)
|
||||||
|
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
opt(s)
|
opt(s)
|
||||||
@@ -103,6 +104,7 @@ func New(ctx context.Context, ds models.Datastore, mq models.MessageQueue, apiUR
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// todo: remove this or change name
|
||||||
func prepareMiddleware(ctx context.Context) gin.HandlerFunc {
|
func prepareMiddleware(ctx context.Context) gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
ctx, _ := common.LoggerWithFields(ctx, extractFields(c))
|
ctx, _ := common.LoggerWithFields(ctx, extractFields(c))
|
||||||
@@ -239,7 +241,7 @@ func (s *Server) startGears(ctx context.Context) {
|
|||||||
svr.Serve(ctx)
|
svr.Serve(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) bindHandlers() {
|
func (s *Server) bindHandlers(ctx context.Context) {
|
||||||
engine := s.Router
|
engine := s.Router
|
||||||
|
|
||||||
engine.GET("/", handlePing)
|
engine.GET("/", handlePing)
|
||||||
@@ -247,6 +249,7 @@ func (s *Server) bindHandlers() {
|
|||||||
engine.GET("/stats", s.handleStats)
|
engine.GET("/stats", s.handleStats)
|
||||||
|
|
||||||
v1 := engine.Group("/v1")
|
v1 := engine.Group("/v1")
|
||||||
|
v1.Use(s.middlewareWrapperFunc(ctx))
|
||||||
{
|
{
|
||||||
v1.GET("/apps", s.handleAppList)
|
v1.GET("/apps", s.handleAppList)
|
||||||
v1.POST("/apps", s.handleAppCreate)
|
v1.POST("/apps", s.handleAppCreate)
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ func testServer(ds models.Datastore, mq models.MessageQueue, rnr *runner.Runner,
|
|||||||
r.Use(gin.Logger())
|
r.Use(gin.Logger())
|
||||||
|
|
||||||
s.Router.Use(prepareMiddleware(ctx))
|
s.Router.Use(prepareMiddleware(ctx))
|
||||||
s.bindHandlers()
|
s.bindHandlers(ctx)
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ func (h *testSpecialHandler) Handle(c HandlerContext) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSpecialHandlerSet(t *testing.T) {
|
func TestSpecialHandlerSet(t *testing.T) {
|
||||||
// temporarily commented until we figure out if we want this anymore
|
// todo: temporarily commented as we may remove special handlers
|
||||||
// ctx := context.Background()
|
// ctx := context.Background()
|
||||||
|
|
||||||
// tasks := make(chan task.Request)
|
// tasks := make(chan task.Request)
|
||||||
|
|||||||
@@ -2,6 +2,13 @@
|
|||||||
|
|
||||||
IronFunctions is extensible so you can add custom functionality and extend the project without needing to modify the core.
|
IronFunctions is extensible so you can add custom functionality and extend the project without needing to modify the core.
|
||||||
|
|
||||||
|
There are 4 different ways to extend the functionality of IronFunctions.
|
||||||
|
|
||||||
|
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 IronFunctions API.
|
||||||
|
1. Special Handlers - TODO: DO WE NEED THIS ANYMORE??
|
||||||
|
|
||||||
## Listeners
|
## Listeners
|
||||||
|
|
||||||
Listeners are the main way to extend IronFunctions.
|
Listeners are the main way to extend IronFunctions.
|
||||||
@@ -34,9 +41,7 @@ func (c *myCustomListener) BeforeAppDelete(ctx context.Context, app *models.App)
|
|||||||
function main () {
|
function main () {
|
||||||
srv := server.New(/* Here all required parameters to initialize the server */)
|
srv := server.New(/* Here all required parameters to initialize the server */)
|
||||||
|
|
||||||
srv.AddAppCreateListener(myCustomListener)
|
srv.AddAppListener(myCustomListener)
|
||||||
srv.AddAppUpdateListener(myCustomListener)
|
|
||||||
srv.AddAppDeleteListener(myCustomListener)
|
|
||||||
|
|
||||||
srv.Run()
|
srv.Run()
|
||||||
}
|
}
|
||||||
@@ -48,45 +53,11 @@ These are all available listeners:
|
|||||||
|
|
||||||
#### App Listeners
|
#### App Listeners
|
||||||
|
|
||||||
To be a valid listener your struct should respect interfaces combined or alone found [in this file](/iron-io/functions/blob/master/api/server/apps_listeners.go)
|
See the godoc for AppListener [in this file](/iron-io/functions/blob/master/api/server/apps_listeners.go)
|
||||||
|
|
||||||
##### AppCreateListener
|
|
||||||
|
|
||||||
_Triggers before and after every app creation that happens in the API_
|
|
||||||
|
|
||||||
Triggered on requests to the following routes:
|
|
||||||
|
|
||||||
- POST /v1/apps
|
|
||||||
- POST /v1/apps/:app/routes
|
|
||||||
|
|
||||||
##### AppUpdateListener
|
|
||||||
|
|
||||||
_Triggers before and after every app updates that happens in the API_
|
|
||||||
|
|
||||||
Triggered during requests to the following routes:
|
|
||||||
|
|
||||||
- PUT /v1/apps
|
|
||||||
|
|
||||||
##### AppDeleteListener
|
|
||||||
|
|
||||||
_Triggers before and after every app deletion that happens in the API_
|
|
||||||
|
|
||||||
Triggered during requests to the following routes:
|
|
||||||
|
|
||||||
- DELETE /v1/apps/:app
|
|
||||||
|
|
||||||
#### Runner Listeners
|
#### Runner Listeners
|
||||||
|
|
||||||
To be a valid listener your struct should respect interfaces combined or alone found [in this file](/iron-io/functions/blob/master/api/server/runner_listeners.go).
|
See the godoc for RunnerListner [in this file](/iron-io/functions/blob/master/api/server/runner_listeners.go).
|
||||||
|
|
||||||
##### RunnerListener
|
|
||||||
|
|
||||||
_Triggers before and after every function run_
|
|
||||||
|
|
||||||
Triggered during requests to the following routes:
|
|
||||||
|
|
||||||
- GET /r/:app/:route
|
|
||||||
- POST /r/:app/:route
|
|
||||||
|
|
||||||
## Adding API Endpoints
|
## Adding API Endpoints
|
||||||
|
|
||||||
@@ -94,6 +65,16 @@ You can add API endpoints by using the `AddEndpoint` and `AddEndpointFunc` metho
|
|||||||
|
|
||||||
See examples of this in [/examples/extensions/main.go](/examples/extensions/main.go).
|
See examples of this in [/examples/extensions/main.go](/examples/extensions/main.go).
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
* cancel the chain by returning an error from your Middleware's Serve method.
|
||||||
|
|
||||||
## Special Handlers
|
## Special Handlers
|
||||||
|
|
||||||
To understand how **Special Handlers** works you need to understand what are **Special Routes**.
|
To understand how **Special Handlers** works you need to understand what are **Special Routes**.
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
# Extensions Example
|
# Extensions Example
|
||||||
|
|
||||||
This example adds extra endpoints to the API.
|
This example adds extra endpoints to the API. See [main.go](main.go) for example code.
|
||||||
|
|
||||||
## Building and Running
|
## Building and Running
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
go build -o functions
|
go build
|
||||||
./functions
|
./extensions
|
||||||
```
|
```
|
||||||
|
|
||||||
Then test with:
|
Then test with:
|
||||||
|
|||||||
2
examples/middleware/.gitignore
vendored
Normal file
2
examples/middleware/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/middleware
|
||||||
|
/middleware.exe
|
||||||
27
examples/middleware/README.md
Normal file
27
examples/middleware/README.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# Middleware Example
|
||||||
|
|
||||||
|
This example adds a simple authentication middleware to IronFunctions. See [main.go](main.go) for example code.
|
||||||
|
|
||||||
|
## Building and Running
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go build
|
||||||
|
./middleware
|
||||||
|
```
|
||||||
|
|
||||||
|
Then test with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# First, create an app
|
||||||
|
fn apps create myapp
|
||||||
|
# And test
|
||||||
|
curl http://localhost:8080/v1/apps
|
||||||
|
```
|
||||||
|
|
||||||
|
You should get a 401 error.
|
||||||
|
|
||||||
|
Add an auth header and it should go through successfully:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -X GET -H "Authorization: Bearer KlaatuBaradaNikto" http://localhost:8080/v1/apps
|
||||||
|
```
|
||||||
50
examples/middleware/main.go
Normal file
50
examples/middleware/main.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/iron-io/functions/api/models"
|
||||||
|
"github.com/iron-io/functions/api/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
funcServer := server.NewEnv(ctx)
|
||||||
|
|
||||||
|
funcServer.AddMiddlewareFunc(func(ctx server.MiddlewareContext, w http.ResponseWriter, r *http.Request, app *models.App) error {
|
||||||
|
start := time.Now()
|
||||||
|
fmt.Println("CustomMiddlewareFunc called at:", start)
|
||||||
|
// TODO: probably need a way to let the chain go forward here and return back to the middleware, for things like timing, etc.
|
||||||
|
ctx.Next()
|
||||||
|
fmt.Println("Duration:", (time.Now().Sub(start)))
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
funcServer.AddMiddleware(&CustomMiddleware{})
|
||||||
|
|
||||||
|
funcServer.Start(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
type CustomMiddleware struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *CustomMiddleware) Serve(ctx server.MiddlewareContext, w http.ResponseWriter, r *http.Request, app *models.App) error {
|
||||||
|
fmt.Println("CustomMiddleware called")
|
||||||
|
|
||||||
|
// check auth header
|
||||||
|
tokenHeader := strings.SplitN(r.Header.Get("Authorization"), " ", 3)
|
||||||
|
if len(tokenHeader) < 2 || tokenHeader[1] != "KlaatuBaradaNikto" {
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
m := map[string]string{"error": "Invalid Authorization token. Sorry!"}
|
||||||
|
json.NewEncoder(w).Encode(m)
|
||||||
|
return errors.New("Invalid authorization token.")
|
||||||
|
}
|
||||||
|
fmt.Println("auth succeeded!")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user