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:
Travis Reeder
2017-01-30 14:43:23 -08:00
committed by C Cirello
parent 37efa47bdf
commit ce26f665ea
10 changed files with 213 additions and 48 deletions

102
api/server/middleware.go Normal file
View 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))
}

View File

@@ -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
} }

View File

@@ -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)

View File

@@ -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
} }

View File

@@ -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)

View File

@@ -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,10 +41,8 @@ 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**.

View File

@@ -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
View File

@@ -0,0 +1,2 @@
/middleware
/middleware.exe

View 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
```

View 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
}