Remove V1 endpoints and Routes (#1210)

Largely a removal job, however many tests, particularly system level
ones relied on Routes. These have been migrated to use Fns.

* Add 410 response to swagger
* No app names in log tags
* Adding constraint in GetCall for FnID
* Adding test to check FnID is required on call
* Add fn_id to call selector
* Fix text in docker mem warning
* Correct buildConfig func name
* Test fix up
* Removing CPU setting from Agent test

CPU setting has been deprecated, but the code base is still riddled
with it. This just removes it from this layer. Really we need to
remove it from Call.

* Remove fn id check on calls
* Reintroduce fn id required on call
* Adding fnID to calls for execute test
* Correct setting of app id in middleware
* Removes root middlewares ability to redirect fun invocations
* Add over sized test check
* Removing call fn id check
This commit is contained in:
Tom Coupland
2018-09-17 16:44:51 +01:00
committed by Owen Cliffe
parent 6a01dae923
commit d56a49b321
82 changed files with 572 additions and 5558 deletions

View File

@@ -12,7 +12,7 @@ func (s *Server) handleAppList(c *gin.Context) {
filter := &models.AppFilter{}
filter.Cursor, filter.PerPage = pageParamsV2(c)
filter.Cursor, filter.PerPage = pageParams(c)
filter.Name = c.Query("name")

View File

@@ -1,39 +0,0 @@
package server
import (
"net/http"
"github.com/fnproject/fn/api/models"
"github.com/gin-gonic/gin"
)
//TODO deprecate with V2
func (s *Server) handleV1AppCreate(c *gin.Context) {
ctx := c.Request.Context()
var wapp models.AppWrapper
err := c.BindJSON(&wapp)
if err != nil {
if models.IsAPIError(err) {
handleV1ErrorResponse(c, err)
} else {
handleV1ErrorResponse(c, models.ErrInvalidJSON)
}
return
}
app := wapp.App
if app == nil {
handleV1ErrorResponse(c, models.ErrAppsMissingNew)
return
}
app, err = s.datastore.InsertApp(ctx, app)
if err != nil {
handleV1ErrorResponse(c, err)
return
}
c.JSON(http.StatusOK, appResponse{"App successfully created", app})
}

View File

@@ -1,21 +0,0 @@
package server
import (
"net/http"
"github.com/fnproject/fn/api"
"github.com/gin-gonic/gin"
)
// TODO: Deprecate with v1
func (s *Server) handleV1AppDelete(c *gin.Context) {
ctx := c.Request.Context()
err := s.datastore.RemoveApp(ctx, c.MustGet(api.AppID).(string))
if err != nil {
handleV1ErrorResponse(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"message": "App deleted"})
}

View File

@@ -1,23 +0,0 @@
package server
import (
"net/http"
"github.com/fnproject/fn/api"
"github.com/gin-gonic/gin"
)
// TODO: Deprecate with V1 API
func (s *Server) handleV1AppGetByIdOrName(c *gin.Context) {
ctx := c.Request.Context()
param := c.MustGet(api.AppID).(string)
app, err := s.datastore.GetAppByID(ctx, param)
if err != nil {
handleV1ErrorResponse(c, err)
return
}
c.JSON(http.StatusOK, appResponse{"Successfully loaded app", app})
}

View File

@@ -1,35 +0,0 @@
package server
import (
"encoding/base64"
"net/http"
"github.com/fnproject/fn/api/models"
"github.com/gin-gonic/gin"
)
// TODO: Deprecate with V1 API
func (s *Server) handleV1AppList(c *gin.Context) {
ctx := c.Request.Context()
filter := &models.AppFilter{}
filter.Cursor, filter.PerPage = pageParamsV2(c)
apps, err := s.datastore.GetApps(ctx, filter)
if err != nil {
handleV1ErrorResponse(c, err)
return
}
var nextCursor string
if len(apps.Items) > 0 && len(apps.Items) == filter.PerPage {
last := []byte(apps.Items[len(apps.Items)-1].Name)
nextCursor = base64.RawURLEncoding.EncodeToString(last)
}
c.JSON(http.StatusOK, appsV1Response{
Message: "Successfully listed applications",
NextCursor: nextCursor,
Apps: apps.Items,
})
}

View File

@@ -1,342 +0,0 @@
package server
import (
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"net/http"
"strings"
"testing"
"time"
"github.com/fnproject/fn/api/datastore"
"github.com/fnproject/fn/api/logs"
"github.com/fnproject/fn/api/models"
"github.com/fnproject/fn/api/mqs"
)
func TestV1AppCreate(t *testing.T) {
buf := setLogBuffer()
defer func() {
if t.Failed() {
t.Log(buf.String())
}
}()
for i, test := range []struct {
mock models.Datastore
logDB models.LogStore
path string
body string
expectedCode int
expectedError error
}{
// errors
{datastore.NewMock(), logs.NewMock(), "/v1/apps", ``, http.StatusBadRequest, models.ErrInvalidJSON},
{datastore.NewMock(), logs.NewMock(), "/v1/apps", `{}`, http.StatusBadRequest, models.ErrAppsMissingNew},
{datastore.NewMock(), logs.NewMock(), "/v1/apps", `{ "name": "Test" }`, http.StatusBadRequest, models.ErrAppsMissingNew},
{datastore.NewMock(), logs.NewMock(), "/v1/apps", `{ "app": { "name": "" } }`, http.StatusBadRequest, models.ErrMissingName},
{datastore.NewMock(), logs.NewMock(), "/v1/apps", `{ "app": { "name": "1234567890123456789012345678901" } }`, http.StatusBadRequest, models.ErrAppsTooLongName},
{datastore.NewMock(), logs.NewMock(), "/v1/apps", `{ "app": { "name": "&&%@!#$#@$" } }`, http.StatusBadRequest, models.ErrAppsInvalidName},
{datastore.NewMock(), logs.NewMock(), "/v1/apps", `{ "app": { "name": "&&%@!#$#@$" } }`, http.StatusBadRequest, models.ErrAppsInvalidName},
{datastore.NewMock(), logs.NewMock(), "/v1/apps", `{ "app": { "name": "app", "annotations" : { "":"val" }}}`, http.StatusBadRequest, models.ErrInvalidAnnotationKey},
{datastore.NewMock(), logs.NewMock(), "/v1/apps", `{ "app": { "name": "app", "annotations" : { "key":"" }}}`, http.StatusBadRequest, models.ErrInvalidAnnotationValue},
{datastore.NewMock(), logs.NewMock(), "/v1/apps", `{ "app": { "name": "app", "syslog_url":"yo"}}`, http.StatusBadRequest, errors.New(`invalid syslog url: "yo"`)},
{datastore.NewMock(), logs.NewMock(), "/v1/apps", `{ "app": { "name": "app", "syslog_url":"yo://sup.com:1"}}`, http.StatusBadRequest, errors.New(`invalid syslog url: "yo://sup.com:1"`)},
// success
{datastore.NewMock(), logs.NewMock(), "/v1/apps", `{ "app": { "name": "teste" } }`, http.StatusOK, nil},
{datastore.NewMock(), logs.NewMock(), "/v1/apps", `{ "app": { "name": "teste" , "annotations": {"k1":"v1", "k2":[]}}}`, http.StatusOK, nil},
{datastore.NewMock(), logs.NewMock(), "/v1/apps", `{ "app": { "name": "teste", "syslog_url":"tcp://example.com:443" } }`, http.StatusOK, nil},
} {
rnr, cancel := testRunner(t)
srv := testServer(test.mock, &mqs.Mock{}, test.logDB, rnr, ServerTypeFull)
router := srv.Router
body := bytes.NewBuffer([]byte(test.body))
_, rec := routerRequest(t, router, "POST", test.path, body)
if rec.Code != test.expectedCode {
t.Errorf("Test %d: Expected status code to be %d but was %d",
i, test.expectedCode, rec.Code)
}
if test.expectedError != nil {
resp := getV1ErrorResponse(t, rec)
if !strings.Contains(resp.Error.Message, test.expectedError.Error()) {
t.Errorf("Test %d: Expected error message to have `%s` but got `%s`",
i, test.expectedError.Error(), resp.Error.Message)
}
}
if test.expectedCode == http.StatusOK {
var awrap models.AppWrapper
err := json.NewDecoder(rec.Body).Decode(&awrap)
if err != nil {
t.Log(buf.String())
t.Errorf("Test %d: error decoding body for 'ok' json, it was a lie: %v", i, err)
}
app := awrap.App
// IsZero() doesn't really work, this ensures it's not unset as long as we're not in 1970
if time.Time(app.CreatedAt).Before(time.Now().Add(-1 * time.Hour)) {
t.Log(buf.String())
t.Errorf("Test %d: expected created_at to be set on app, it wasn't: %s", i, app.CreatedAt)
}
if !(time.Time(app.CreatedAt)).Equal(time.Time(app.UpdatedAt)) {
t.Log(buf.String())
t.Errorf("Test %d: expected updated_at to be set and same as created at, it wasn't: %s %s", i, app.CreatedAt, app.UpdatedAt)
}
}
cancel()
}
}
func TestV1AppDelete(t *testing.T) {
buf := setLogBuffer()
defer func() {
if t.Failed() {
t.Log(buf.String())
}
}()
app := &models.App{
Name: "myapp",
ID: "appId",
}
ds := datastore.NewMockInit([]*models.App{app})
for i, test := range []struct {
ds models.Datastore
logDB models.LogStore
path string
body string
expectedCode int
expectedError error
}{
{datastore.NewMock(), logs.NewMock(), "/v1/apps/myapp", "", http.StatusNotFound, nil},
{ds, logs.NewMock(), "/v1/apps/myapp", "", http.StatusOK, nil},
} {
rnr, cancel := testRunner(t)
srv := testServer(test.ds, &mqs.Mock{}, test.logDB, rnr, ServerTypeFull)
_, rec := routerRequest(t, srv.Router, "DELETE", test.path, nil)
if rec.Code != test.expectedCode {
t.Errorf("Test %d: Expected status code to be %d but was %d",
i, test.expectedCode, rec.Code)
}
if test.expectedError != nil {
resp := getV1ErrorResponse(t, rec)
if !strings.Contains(resp.Error.Message, test.expectedError.Error()) {
t.Errorf("Test %d: Expected error message to have `%s`",
i, test.expectedError.Error())
}
}
cancel()
}
}
func TestV1AppList(t *testing.T) {
buf := setLogBuffer()
defer func() {
if t.Failed() {
t.Log(buf.String())
}
}()
rnr, cancel := testRunner(t)
defer cancel()
ds := datastore.NewMockInit(
[]*models.App{
{Name: "myapp"},
{Name: "myapp2"},
{Name: "myapp3"},
},
)
fnl := logs.NewMock()
srv := testServer(ds, &mqs.Mock{}, fnl, rnr, ServerTypeFull)
a1b := base64.RawURLEncoding.EncodeToString([]byte("myapp"))
a2b := base64.RawURLEncoding.EncodeToString([]byte("myapp2"))
a3b := base64.RawURLEncoding.EncodeToString([]byte("myapp3"))
for i, test := range []struct {
path string
body string
expectedCode int
expectedError error
expectedLen int
nextCursor string
}{
{"/v1/apps?per_page", "", http.StatusOK, nil, 3, ""},
{"/v1/apps?per_page=1", "", http.StatusOK, nil, 1, a1b},
{"/v1/apps?per_page=1&cursor=" + a1b, "", http.StatusOK, nil, 1, a2b},
{"/v1/apps?per_page=1&cursor=" + a2b, "", http.StatusOK, nil, 1, a3b},
{"/v1/apps?per_page=100&cursor=" + a2b, "", http.StatusOK, nil, 1, ""}, // cursor is empty if per_page > len(results)
{"/v1/apps?per_page=1&cursor=" + a3b, "", http.StatusOK, nil, 0, ""}, // cursor could point to empty page
} {
_, rec := routerRequest(t, srv.Router, "GET", test.path, nil)
if rec.Code != test.expectedCode {
t.Errorf("Test %d: Expected status code to be %d but was %d",
i, test.expectedCode, rec.Code)
}
if test.expectedError != nil {
resp := getV1ErrorResponse(t, rec)
if !strings.Contains(resp.Error.Message, test.expectedError.Error()) {
t.Errorf("Test %d: Expected error message to have `%s`",
i, test.expectedError.Error())
}
} else {
// normal path
var resp appsV1Response
err := json.NewDecoder(rec.Body).Decode(&resp)
if err != nil {
t.Errorf("Test %d: Expected response body to be a valid json object. err: %v", i, err)
}
if len(resp.Apps) != test.expectedLen {
t.Errorf("Test %d: Expected apps length to be %d, but got %d", i, test.expectedLen, len(resp.Apps))
}
if resp.NextCursor != test.nextCursor {
t.Errorf("Test %d: Expected next_cursor to be %s, but got %s", i, test.nextCursor, resp.NextCursor)
}
}
}
}
func TestV1AppGet(t *testing.T) {
buf := setLogBuffer()
defer func() {
if t.Failed() {
t.Log(buf.String())
}
}()
rnr, cancel := testRunner(t)
defer cancel()
ds := datastore.NewMock()
fnl := logs.NewMock()
srv := testServer(ds, &mqs.Mock{}, fnl, rnr, ServerTypeFull)
for i, test := range []struct {
path string
body string
expectedCode int
expectedError error
}{
{"/v1/apps/myapp", "", http.StatusNotFound, nil},
} {
_, rec := routerRequest(t, srv.Router, "GET", test.path, nil)
if rec.Code != test.expectedCode {
t.Errorf("Test %d: Expected status code to be %d but was %d",
i, test.expectedCode, rec.Code)
}
if test.expectedError != nil {
resp := getV1ErrorResponse(t, rec)
if !strings.Contains(resp.Error.Message, test.expectedError.Error()) {
t.Errorf("Test %d: Expected error message to have `%s`",
i, test.expectedError.Error())
}
}
}
}
func TestV1AppUpdate(t *testing.T) {
buf := setLogBuffer()
defer func() {
if t.Failed() {
t.Log(buf.String())
}
}()
app := &models.App{
Name: "myapp",
ID: "app_id",
}
ds := datastore.NewMockInit([]*models.App{app})
for i, test := range []struct {
mock models.Datastore
logDB models.LogStore
path string
body string
expectedCode int
expectedError error
}{
// errors
{ds, logs.NewMock(), "/v1/apps/myapp", ``, http.StatusBadRequest, models.ErrInvalidJSON},
// Addresses #380
{ds, logs.NewMock(), "/v1/apps/myapp", `{ "app": { "name": "othername" } }`, http.StatusConflict, nil},
// success: add/set MD key
{ds, logs.NewMock(), "/v1/apps/myapp", `{ "app": { "annotations": {"k-0" : "val"} } }`, http.StatusOK, nil},
// success
{ds, logs.NewMock(), "/v1/apps/myapp", `{ "app": { "config": { "test": "1" } } }`, http.StatusOK, nil},
// success
{ds, logs.NewMock(), "/v1/apps/myapp", `{ "app": { "config": { "test": "1" } } }`, http.StatusOK, nil},
// success
{ds, logs.NewMock(), "/v1/apps/myapp", `{ "app": { "syslog_url":"tcp://example.com:443" } }`, http.StatusOK, nil},
} {
rnr, cancel := testRunner(t)
srv := testServer(test.mock, &mqs.Mock{}, test.logDB, rnr, ServerTypeFull)
body := bytes.NewBuffer([]byte(test.body))
_, rec := routerRequest(t, srv.Router, "PATCH", test.path, body)
if rec.Code != test.expectedCode {
t.Errorf("Test %d: Expected status code to be %d but was %d",
i, test.expectedCode, rec.Code)
}
if test.expectedError != nil {
resp := getV1ErrorResponse(t, rec)
if !strings.Contains(resp.Error.Message, test.expectedError.Error()) {
t.Errorf("Test %d: Expected error message to have `%s` but was `%s`",
i, test.expectedError.Error(), resp.Error.Message)
}
}
if test.expectedCode == http.StatusOK {
var awrap models.AppWrapper
err := json.NewDecoder(rec.Body).Decode(&awrap)
if err != nil {
t.Log(buf.String())
t.Errorf("Test %d: error decoding body for 'ok' json, it was a lie: %v", i, err)
}
app := awrap.App
// IsZero() doesn't really work, this ensures it's not unset as long as we're not in 1970
if time.Time(app.UpdatedAt).Before(time.Now().Add(-1 * time.Hour)) {
t.Log(buf.String())
t.Errorf("Test %d: expected updated_at to be set on app, it wasn't: %s", i, app.UpdatedAt)
}
// this isn't perfect, since a PATCH could succeed without updating any
// fields (among other reasons), but just don't make a test for that or
// special case (the body or smth) to ignore it here!
// this is a decent approximation that the timestamp gets changed
if (time.Time(app.UpdatedAt)).Equal(time.Time(app.CreatedAt)) {
t.Log(buf.String())
t.Errorf("Test %d: expected updated_at to not be the same as created at, it wasn't: %s %s", i, app.CreatedAt, app.UpdatedAt)
}
}
cancel()
}
}

View File

@@ -1,47 +0,0 @@
package server
import (
"net/http"
"github.com/fnproject/fn/api"
"github.com/fnproject/fn/api/models"
"github.com/gin-gonic/gin"
)
// TODO: Deprecate with V1 API
func (s *Server) handleV1AppUpdate(c *gin.Context) {
ctx := c.Request.Context()
wapp := models.AppWrapper{}
err := c.BindJSON(&wapp)
if err != nil {
if models.IsAPIError(err) {
handleV1ErrorResponse(c, err)
} else {
handleV1ErrorResponse(c, models.ErrInvalidJSON)
}
return
}
if wapp.App == nil {
handleV1ErrorResponse(c, models.ErrAppsMissingNew)
return
}
if wapp.App.Name != "" {
handleV1ErrorResponse(c, models.ErrAppsNameImmutable)
return
}
wapp.App.Name = c.MustGet(api.AppName).(string)
wapp.App.ID = c.MustGet(api.AppID).(string)
app, err := s.datastore.UpdateApp(ctx, wapp.App)
if err != nil {
handleV1ErrorResponse(c, err)
return
}
c.JSON(http.StatusOK, appResponse{"AppName successfully updated", app})
}

View File

@@ -4,35 +4,36 @@ import (
"net/http"
"github.com/fnproject/fn/api"
"github.com/fnproject/fn/api/models"
"github.com/gin-gonic/gin"
)
func (s *Server) handleCallGet1(c *gin.Context) {
ctx := c.Request.Context()
callID := c.Param(api.ParamCallID)
appID := c.MustGet(api.AppID).(string)
callObj, err := s.logstore.GetCall1(ctx, appID, callID)
if err != nil {
handleV1ErrorResponse(c, err)
return
}
c.JSON(http.StatusOK, callResponse{"Successfully loaded call", callObj})
}
func (s *Server) handleCallGet(c *gin.Context) {
ctx := c.Request.Context()
fnID := c.Param(api.ParamFnID)
callID := c.Param(api.ParamCallID)
callObj, err := s.logstore.GetCall(ctx, fnID, callID)
if err != nil {
handleV1ErrorResponse(c, err)
if fnID == "" {
handleErrorResponse(c, models.ErrFnsMissingID)
return
}
c.JSON(http.StatusOK, callResponse{"Successfully loaded call", callObj})
_, err := s.datastore.GetFnByID(ctx, c.Param(api.ParamFnID))
if err != nil {
handleErrorResponse(c, err)
return
}
callID := c.Param(api.ParamCallID)
if callID == "" {
handleErrorResponse(c, models.ErrDatastoreEmptyCallID)
}
callObj, err := s.logstore.GetCall(ctx, fnID, callID)
if err != nil {
handleErrorResponse(c, err)
return
}
c.JSON(http.StatusOK, callObj)
}

View File

@@ -11,48 +11,29 @@ import (
"github.com/gin-gonic/gin"
)
func (s *Server) handleCallList1(c *gin.Context) {
ctx := c.Request.Context()
var err error
appID := c.MustGet(api.AppID).(string)
// TODO api.ParamRouteName needs to be escaped probably, since it has '/' a lot
filter := models.CallFilter{AppID: appID, Path: c.Query("path")}
filter.Cursor, filter.PerPage = pageParams(c, false) // ids are url safe
filter.FromTime, filter.ToTime, err = timeParams(c)
if err != nil {
handleV1ErrorResponse(c, err)
return
}
calls, err := s.logstore.GetCalls1(ctx, &filter)
var nextCursor string
if len(calls) > 0 && len(calls) == filter.PerPage {
nextCursor = calls[len(calls)-1].ID
// don't base64, IDs are url safe
}
c.JSON(http.StatusOK, callsResponse{
Message: "Successfully listed calls",
NextCursor: nextCursor,
Calls: calls,
})
}
func (s *Server) handleCallList(c *gin.Context) {
ctx := c.Request.Context()
var err error
fnID := c.MustGet(api.ParamFnID).(string)
// TODO api.ParamRouteName needs to be escaped probably, since it has '/' a lot
fnID := c.Param(api.ParamFnID)
if fnID == "" {
handleErrorResponse(c, models.ErrFnsMissingID)
return
}
_, err = s.datastore.GetFnByID(ctx, c.Param(api.ParamFnID))
if err != nil {
handleErrorResponse(c, err)
return
}
filter := models.CallFilter{FnID: fnID}
filter.Cursor, filter.PerPage = pageParams(c, false) // ids are url safe
filter.Cursor, filter.PerPage = pageParams(c)
filter.FromTime, filter.ToTime, err = timeParams(c)
if err != nil {
handleV1ErrorResponse(c, err)
handleErrorResponse(c, err)
return
}

View File

@@ -34,46 +34,6 @@ func writeJSON(c *gin.Context, callID string, logReader io.Reader) {
}})
}
func (s *Server) handleCallLogGet1(c *gin.Context) {
ctx := c.Request.Context()
appID := c.MustGet(api.AppID).(string)
callID := c.Param(api.ParamCallID)
logReader, err := s.logstore.GetLog(ctx, appID, callID)
if err != nil {
handleV1ErrorResponse(c, err)
return
}
mimeTypes, _ := c.Request.Header["Accept"]
if len(mimeTypes) == 0 {
writeJSON(c, callID, logReader)
return
}
for _, mimeType := range mimeTypes {
if strings.Contains(mimeType, "application/json") {
writeJSON(c, callID, logReader)
return
}
if strings.Contains(mimeType, "text/plain") {
io.Copy(c.Writer, logReader)
return
}
if strings.Contains(mimeType, "*/*") {
writeJSON(c, callID, logReader)
return
}
}
// if we've reached this point it means that Fn didn't recognize Accepted content type
handleV1ErrorResponse(c, models.NewAPIError(http.StatusNotAcceptable,
errors.New("unable to respond within acceptable response content types")))
}
func (s *Server) handleCallLogGet(c *gin.Context) {
ctx := c.Request.Context()
@@ -82,7 +42,7 @@ func (s *Server) handleCallLogGet(c *gin.Context) {
logReader, err := s.logstore.GetLog(ctx, fnID, callID)
if err != nil {
handleV1ErrorResponse(c, err)
handleErrorResponse(c, err)
return
}
@@ -110,6 +70,6 @@ func (s *Server) handleCallLogGet(c *gin.Context) {
}
// if we've reached this point it means that Fn didn't recognize Accepted content type
handleV1ErrorResponse(c, models.NewAPIError(http.StatusNotAcceptable,
handleErrorResponse(c, models.NewAPIError(http.StatusNotAcceptable,
errors.New("unable to respond within acceptable response content types")))
}

View File

@@ -1,6 +1,7 @@
package server
import (
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
@@ -24,11 +25,10 @@ func TestCallGet(t *testing.T) {
}
}()
app := &models.App{Name: "myapp", ID: "app_id"}
fn := &models.Fn{Name: "myfn", ID: "fn_id"}
call := &models.Call{
AppID: app.ID,
FnID: fn.ID,
ID: id.New().String(),
Path: "/thisisatest",
Image: "fnproject/hello",
// Delay: 0,
Type: "sync",
@@ -46,7 +46,7 @@ func TestCallGet(t *testing.T) {
rnr, cancel := testRunner(t)
defer cancel()
ds := datastore.NewMockInit(
[]*models.App{app},
[]*models.Fn{fn},
)
fnl := logs.NewMock([]*models.Call{call})
srv := testServer(ds, &mqs.Mock{}, fnl, rnr, ServerTypeFull)
@@ -57,11 +57,11 @@ func TestCallGet(t *testing.T) {
expectedCode int
expectedError error
}{
{"/v1/apps//calls/" + call.ID, "", http.StatusBadRequest, models.ErrAppsMissingName},
{"/v1/apps/nodawg/calls/" + call.ID, "", http.StatusNotFound, models.ErrAppsNotFound},
{"/v1/apps/myapp/calls/" + id.New().String(), "", http.StatusNotFound, models.ErrCallNotFound},
{"/v1/apps/myapp/calls/" + call.ID[:3], "", http.StatusNotFound, models.ErrCallNotFound},
{"/v1/apps/myapp/calls/" + call.ID, "", http.StatusOK, nil},
{"/v2/fns//calls/" + call.ID, "", http.StatusBadRequest, models.ErrFnsMissingID},
{"/v2/fns/missing_fn/calls/" + call.ID, "", http.StatusNotFound, models.ErrFnsNotFound},
{"/v2/fns/fn_id/calls/" + id.New().String(), "", http.StatusNotFound, models.ErrCallNotFound},
{"/v2/fns/fn_id/calls/" + call.ID[:3], "", http.StatusNotFound, models.ErrCallNotFound},
{"/v2/fns/fn_id/calls/" + call.ID, "", http.StatusOK, nil},
} {
_, rec := routerRequest(t, srv.Router, "GET", test.path, nil)
@@ -72,10 +72,10 @@ func TestCallGet(t *testing.T) {
}
if test.expectedError != nil {
resp := getV1ErrorResponse(t, rec)
resp := getErrorResponse(t, rec)
if !strings.Contains(resp.Error.Message, test.expectedError.Error()) {
t.Log(resp.Error.Message)
if !strings.Contains(resp.Message, test.expectedError.Error()) {
t.Log(resp.Message)
t.Log(rec.Body.String())
t.Errorf("Test %d: Expected error message to have `%s`",
i, test.expectedError.Error())
@@ -93,12 +93,11 @@ func TestCallList(t *testing.T) {
}
}()
app := &models.App{Name: "myapp", ID: "app_id"}
fn := &models.Fn{ID: "fn_id"}
call := &models.Call{
AppID: app.ID,
FnID: fn.ID,
ID: id.New().String(),
Path: "/thisisatest",
Image: "fnproject/hello",
// Delay: 0,
Type: "sync",
@@ -116,15 +115,17 @@ func TestCallList(t *testing.T) {
c3 := *call
c2.CreatedAt = common.DateTime(time.Now().Add(100 * time.Second))
c2.ID = id.New().String()
c2.Path = "test2"
c3.CreatedAt = common.DateTime(time.Now().Add(200 * time.Second))
c3.ID = id.New().String()
c3.Path = "/test3"
encodedC1ID := base64.RawURLEncoding.EncodeToString([]byte(call.ID))
encodedC2ID := base64.RawURLEncoding.EncodeToString([]byte(c2.ID))
encodedC3ID := base64.RawURLEncoding.EncodeToString([]byte(c3.ID))
rnr, cancel := testRunner(t)
defer cancel()
ds := datastore.NewMockInit(
[]*models.App{app},
[]*models.Fn{fn},
)
fnl := logs.NewMock([]*models.Call{call, &c2, &c3})
srv := testServer(ds, &mqs.Mock{}, fnl, rnr, ServerTypeFull)
@@ -143,20 +144,20 @@ func TestCallList(t *testing.T) {
expectedLen int
nextCursor string
}{
{"/v1/apps//calls", "", http.StatusBadRequest, models.ErrAppsMissingName, 0, ""},
{"/v1/apps/nodawg/calls", "", http.StatusNotFound, models.ErrAppsNotFound, 0, ""},
{"/v1/apps/myapp/calls", "", http.StatusOK, nil, 3, ""},
{"/v1/apps/myapp/calls?per_page=1", "", http.StatusOK, nil, 1, c3.ID},
{"/v1/apps/myapp/calls?per_page=1&cursor=" + c3.ID, "", http.StatusOK, nil, 1, c2.ID},
{"/v1/apps/myapp/calls?per_page=1&cursor=" + c2.ID, "", http.StatusOK, nil, 1, call.ID},
{"/v1/apps/myapp/calls?per_page=100&cursor=" + c2.ID, "", http.StatusOK, nil, 1, ""}, // cursor is empty if per_page > len(results)
{"/v1/apps/myapp/calls?per_page=1&cursor=" + call.ID, "", http.StatusOK, nil, 0, ""}, // cursor could point to empty page
{"/v1/apps/myapp/calls?" + rangeTest, "", http.StatusOK, nil, 1, ""},
{"/v1/apps/myapp/calls?from_time=xyz", "", http.StatusBadRequest, models.ErrInvalidFromTime, 0, ""},
{"/v1/apps/myapp/calls?to_time=xyz", "", http.StatusBadRequest, models.ErrInvalidToTime, 0, ""},
{"/v2/fns//calls", "", http.StatusBadRequest, models.ErrFnsMissingID, 0, ""},
{"/v2/fns/nodawg/calls", "", http.StatusNotFound, models.ErrFnsNotFound, 0, ""},
{"/v2/fns/fn_id/calls", "", http.StatusOK, nil, 3, ""},
{"/v2/fns/fn_id/calls?per_page=1", "", http.StatusOK, nil, 1, encodedC3ID},
{"/v2/fns/fn_id/calls?per_page=1&cursor=" + encodedC3ID, "", http.StatusOK, nil, 1, encodedC2ID},
{"/v2/fns/fn_id/calls?per_page=1&cursor=" + encodedC2ID, "", http.StatusOK, nil, 1, encodedC1ID},
{"/v2/fns/fn_id/calls?per_page=100&cursor=" + encodedC2ID, "", http.StatusOK, nil, 1, ""}, // cursor is empty if per_page > len(results)
{"/v2/fns/fn_id/calls?per_page=1&cursor=" + encodedC1ID, "", http.StatusOK, nil, 0, ""}, // cursor could point to empty page
{"/v2/fns/fn_id/calls?" + rangeTest, "", http.StatusOK, nil, 1, ""},
{"/v2/fns/fn_id/calls?from_time=xyz", "", http.StatusBadRequest, models.ErrInvalidFromTime, 0, ""},
{"/v2/fns/fn_id/calls?to_time=xyz", "", http.StatusBadRequest, models.ErrInvalidToTime, 0, ""},
// TODO path isn't url safe w/ '/', so this is weird. hack in for tests
{"/v1/apps/myapp/calls?path=test2", "", http.StatusOK, nil, 1, ""},
// // TODO path isn't url safe w/ '/', so this is weird. hack in for tests
// {"/v2/fns/fn_id/calls?path=test2", "", http.StatusOK, nil, 1, ""},
} {
_, rec := routerRequest(t, srv.Router, "GET", test.path, nil)
@@ -166,22 +167,22 @@ func TestCallList(t *testing.T) {
}
if test.expectedError != nil {
resp := getV1ErrorResponse(t, rec)
resp := getErrorResponse(t, rec)
if resp.Error == nil || !strings.Contains(resp.Error.Message, test.expectedError.Error()) {
if resp.Message == "" || !strings.Contains(resp.Message, test.expectedError.Error()) {
t.Errorf("Test %d: Expected error message to have `%s`, got: `%s`",
i, test.expectedError.Error(), resp.Error)
i, test.expectedError.Error(), resp.Message)
}
} else {
// normal path
var resp callsResponse
var resp models.CallList
err := json.NewDecoder(rec.Body).Decode(&resp)
if err != nil {
t.Errorf("Test %d: Expected response body to be a valid json object. err: %v", i, err)
}
if len(resp.Calls) != test.expectedLen {
t.Fatalf("Test %d: Expected apps length to be %d, but got %d", i, test.expectedLen, len(resp.Calls))
if len(resp.Items) != test.expectedLen {
t.Fatalf("Test %d: Expected calls length to be %d, but got %d", i, test.expectedLen, len(resp.Items))
}
if resp.NextCursor != test.nextCursor {
t.Errorf("Test %d: Expected next_cursor to be %s, but got %s", i, test.nextCursor, resp.NextCursor)

View File

@@ -16,61 +16,14 @@ import (
// ErrInternalServerError returned when something exceptional happens.
var ErrInternalServerError = errors.New("internal server error")
func simpleV1Error(err error) *models.ErrorWrapper {
return &models.ErrorWrapper{Error: &models.Error{Message: err.Error()}}
}
func simpleError(err error) *models.Error {
return &models.Error{Message: err.Error()}
}
// Legacy this is the old wrapped error
// TODO delete me !
func handleV1ErrorResponse(ctx *gin.Context, err error) {
log := common.Logger(ctx)
w := ctx.Writer
if ctx.Err() == context.Canceled {
log.Info("client context cancelled")
w.WriteHeader(models.ErrClientCancel.Code())
return
}
var statuscode int
if e, ok := err.(models.APIError); ok {
if e.Code() >= 500 {
log.WithFields(logrus.Fields{"code": e.Code()}).WithError(e).Error("api error")
}
if err == models.ErrCallTimeoutServerBusy {
// TODO: Determine a better delay value here (perhaps ask Agent). For now 15 secs with
// the hopes that fnlb will land this on a better server immediately.
w.Header().Set("Retry-After", "15")
}
statuscode = e.Code()
} else {
log.WithError(err).WithFields(logrus.Fields{"stack": string(debug.Stack())}).Error("internal server error")
statuscode = http.StatusInternalServerError
err = ErrInternalServerError
}
writeV1Error(ctx, w, statuscode, err)
}
func handleErrorResponse(c *gin.Context, err error) {
HandleErrorResponse(c.Request.Context(), c.Writer, err)
}
// WriteError easy way to do standard error response, but can set statuscode and error message easier than handleV1ErrorResponse
func writeV1Error(ctx context.Context, w http.ResponseWriter, statuscode int, err error) {
log := common.Logger(ctx)
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(statuscode)
err = json.NewEncoder(w).Encode(simpleV1Error(err))
if err != nil {
log.WithError(err).Errorln("error encoding error json")
}
}
// HandleErrorResponse used to handle response errors in the same way.
func HandleErrorResponse(ctx context.Context, w http.ResponseWriter, err error) {
log := common.Logger(ctx)

View File

@@ -23,12 +23,12 @@ func (s *Server) apiAppHandlerWrapperFn(apiHandler fnext.APIAppHandler) gin.Hand
appID := c.MustGet(api.AppID).(string)
app, err := s.datastore.GetAppByID(c.Request.Context(), appID)
if err != nil {
handleV1ErrorResponse(c, err)
handleErrorResponse(c, err)
c.Abort()
return
}
if app == nil {
handleV1ErrorResponse(c, models.ErrAppsNotFound)
handleErrorResponse(c, models.ErrAppsNotFound)
c.Abort()
return
}
@@ -37,39 +37,6 @@ func (s *Server) apiAppHandlerWrapperFn(apiHandler fnext.APIAppHandler) gin.Hand
}
}
func (s *Server) apiRouteHandlerWrapperFn(apiHandler fnext.APIRouteHandler) gin.HandlerFunc {
return func(c *gin.Context) {
context := c.Request.Context()
appID := c.MustGet(api.AppID).(string)
routePath := "/" + c.Param(api.ParamRouteName)
route, err := s.datastore.GetRoute(context, appID, routePath)
if err != nil {
handleV1ErrorResponse(c, err)
c.Abort()
return
}
if route == nil {
handleV1ErrorResponse(c, models.ErrRoutesNotFound)
c.Abort()
return
}
app, err := s.datastore.GetAppByID(context, appID)
if err != nil {
handleV1ErrorResponse(c, err)
c.Abort()
return
}
if app == nil {
handleV1ErrorResponse(c, models.ErrAppsNotFound)
c.Abort()
return
}
apiHandler.ServeHTTP(c.Writer, c.Request, app, route)
}
}
// AddEndpoint adds an endpoint to /v1/x
func (s *Server) AddEndpoint(method, path string, handler fnext.APIHandler) {
v1 := s.Router.Group("/v1")
@@ -93,15 +60,3 @@ func (s *Server) AddAppEndpoint(method, path string, handler fnext.APIAppHandler
func (s *Server) AddAppEndpointFunc(method, path string, handler func(w http.ResponseWriter, r *http.Request, app *models.App)) {
s.AddAppEndpoint(method, path, fnext.APIAppHandlerFunc(handler))
}
// AddRouteEndpoint adds an endpoints to /v1/apps/:app/routes/:route/x
func (s *Server) AddRouteEndpoint(method, path string, handler fnext.APIRouteHandler) {
v1 := s.Router.Group("/v1")
v1.Use(s.checkAppPresenceByName())
v1.Handle(method, "/apps/:app/routes/:route"+path, s.apiRouteHandlerWrapperFn(handler)) // conflicts with existing wildcard
}
// AddRouteEndpointFunc 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)) {
s.AddRouteEndpoint(method, path, fnext.APIRouteHandlerFunc(handler))
}

View File

@@ -12,7 +12,7 @@ func (s *Server) handleFnList(c *gin.Context) {
ctx := c.Request.Context()
var filter models.FnFilter
filter.Cursor, filter.PerPage = pageParamsV2(c)
filter.Cursor, filter.PerPage = pageParams(c)
filter.AppID = c.Query("app_id")
filter.Name = c.Query("name")

View File

@@ -7,10 +7,12 @@ import (
"fmt"
"strings"
"strconv"
"time"
"github.com/fnproject/fn/api"
"github.com/fnproject/fn/api/common"
"github.com/fnproject/fn/api/models"
"github.com/fnproject/fn/fnext"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
@@ -18,8 +20,6 @@ import (
"go.opencensus.io/stats/view"
"go.opencensus.io/tag"
"go.opencensus.io/trace"
"strconv"
"time"
)
var (
@@ -67,13 +67,18 @@ func traceWrap(c *gin.Context) {
if err != nil {
logrus.Fatal(err)
}
pathKey, err := tag.NewKey("fn_path")
appIDKey, err := tag.NewKey("fn_app_id")
if err != nil {
logrus.Fatal(err)
}
fnKey, err := tag.NewKey("fn_fn_id")
if err != nil {
logrus.Fatal(err)
}
ctx, err := tag.New(c.Request.Context(),
tag.Insert(appKey, c.Param(api.ParamAppName)),
tag.Insert(pathKey, c.Param(api.ParamRouteName)),
tag.Insert(appIDKey, c.Param(api.ParamAppID)),
tag.Insert(fnKey, c.Param(api.ParamFnID)),
)
if err != nil {
logrus.Fatal(err)
@@ -169,7 +174,7 @@ func panicWrap(c *gin.Context) {
if !ok {
err = fmt.Errorf("fn: %v", rec)
}
handleV1ErrorResponse(c, err)
handleErrorResponse(c, err)
c.Abort()
}
}(c)
@@ -184,27 +189,41 @@ func loggerWrap(c *gin.Context) {
ctx = ContextWithApp(ctx, appName)
}
if routePath := c.Param(api.ParamRouteName); routePath != "" {
c.Set(api.Path, routePath)
ctx = ContextWithPath(ctx, routePath)
if appID := c.Param(api.ParamAppID); appID != "" {
c.Set(api.ParamAppID, appID)
ctx = ContextWithAppID(ctx, appID)
}
if fnID := c.Param(api.ParamFnID); fnID != "" {
c.Set(api.ParamFnID, fnID)
ctx = ContextWithFnID(ctx, fnID)
}
c.Request = c.Request.WithContext(ctx)
c.Next()
}
type ctxPathKey string
type ctxFnIDKey string
// ContextWithPath sets the routePath value on a context, it may be retrieved
// using PathFromContext.
// TODO this is also used as a gin.Key -- stop one of these two things.
func ContextWithPath(ctx context.Context, routePath string) context.Context {
return context.WithValue(ctx, ctxPathKey(api.Path), routePath)
func ContextWithFnID(ctx context.Context, fnID string) context.Context {
return context.WithValue(ctx, ctxFnIDKey(api.ParamFnID), fnID)
}
// PathFromContext returns the path from a context, if set.
func PathFromContext(ctx context.Context) string {
r, _ := ctx.Value(ctxPathKey(api.Path)).(string)
// FnIDFromContext returns the app from a context, if set.
func FnIDFromContext(ctx context.Context) string {
r, _ := ctx.Value(ctxFnIDKey(api.ParamFnID)).(string)
return r
}
type ctxAppIDKey string
func ContextWithAppID(ctx context.Context, appID string) context.Context {
return context.WithValue(ctx, ctxAppIDKey(api.ParamAppID), appID)
}
// AppIDFromContext returns the app from a context, if set.
func AppIDFromContext(ctx context.Context) string {
r, _ := ctx.Value(ctxAppIDKey(api.ParamAppID)).(string)
return r
}
@@ -223,26 +242,6 @@ func AppFromContext(ctx context.Context) string {
return r
}
func (s *Server) checkAppPresenceByNameAtLB() gin.HandlerFunc {
return func(c *gin.Context) {
ctx, _ := common.LoggerWithFields(c.Request.Context(), extractFields(c))
appName := c.Param(api.ParamAppName)
if appName != "" {
appID, err := s.lbReadAccess.GetAppID(ctx, appName)
if err != nil {
handleV1ErrorResponse(c, err)
c.Abort()
return
}
c.Set(api.AppID, appID)
}
c.Request = c.Request.WithContext(ctx)
c.Next()
}
}
func (s *Server) checkAppPresenceByName() gin.HandlerFunc {
return func(c *gin.Context) {
ctx, _ := common.LoggerWithFields(c.Request.Context(), extractFields(c))
@@ -251,7 +250,7 @@ func (s *Server) checkAppPresenceByName() gin.HandlerFunc {
if appName != "" {
appID, err := s.datastore.GetAppID(ctx, appName)
if err != nil {
handleV1ErrorResponse(c, err)
handleErrorResponse(c, err)
c.Abort()
return
}
@@ -263,15 +262,6 @@ func (s *Server) checkAppPresenceByName() gin.HandlerFunc {
}
}
func setAppNameInCtx(c *gin.Context) {
// add appName to context
appName := c.GetString(api.AppName)
if appName != "" {
c.Request = c.Request.WithContext(context.WithValue(c.Request.Context(), fnext.AppNameKey, appName))
}
c.Next()
}
func setAppIDInCtx(c *gin.Context) {
// add appName to context
appID := c.Param(api.ParamAppID)
@@ -283,10 +273,10 @@ func setAppIDInCtx(c *gin.Context) {
c.Next()
}
func appNameCheck(c *gin.Context) {
appName := c.GetString(api.AppName)
if appName == "" {
handleV1ErrorResponse(c, models.ErrAppsMissingName)
func appIDCheck(c *gin.Context) {
appID := c.GetString(api.ParamAppID)
if appID == "" {
handleErrorResponse(c, models.ErrAppsMissingID)
c.Abort()
return
}

View File

@@ -8,7 +8,6 @@ import (
"errors"
"fmt"
"net/http"
"path"
"github.com/fnproject/fn/api"
"github.com/fnproject/fn/api/common"
@@ -189,20 +188,6 @@ func (s *Server) handleRunnerFinish(c *gin.Context) {
c.String(http.StatusNoContent, "")
}
// This is a sort of interim route that is V2 API style but due for deprectation
func (s *Server) handleRunnerGetRoute(c *gin.Context) {
ctx := c.Request.Context()
routePath := path.Clean("/" + c.MustGet(api.Path).(string))
route, err := s.datastore.GetRoute(ctx, c.MustGet(api.AppID).(string), routePath)
if err != nil {
handleErrorResponse(c, err)
return
}
c.JSON(http.StatusOK, route)
}
func (s *Server) handleRunnerGetTriggerBySource(c *gin.Context) {
ctx := c.Request.Context()

View File

@@ -3,25 +3,24 @@ package server
import (
"bytes"
"encoding/json"
"net/http"
"strings"
"testing"
"github.com/fnproject/fn/api/datastore"
"github.com/fnproject/fn/api/id"
"github.com/fnproject/fn/api/logs"
"github.com/fnproject/fn/api/models"
"github.com/fnproject/fn/api/mqs"
"net/http"
"strings"
"testing"
)
func TestHybridEndpoints(t *testing.T) {
buf := setLogBuffer()
app := &models.App{ID: "app_id", Name: "myapp"}
fn := &models.Fn{ID: "fn_id", AppID: app.ID}
ds := datastore.NewMockInit(
[]*models.App{app},
[]*models.Route{{
AppID: app.ID,
Path: "yodawg",
}},
[]*models.Fn{fn},
)
logDB := logs.NewMock()
@@ -30,10 +29,8 @@ func TestHybridEndpoints(t *testing.T) {
newCallBody := func() string {
call := &models.Call{
AppID: app.ID,
ID: id.New().String(),
Path: "yodawg",
// TODO ?
FnID: fn.ID,
ID: id.New().String(),
}
var b bytes.Buffer
json.NewEncoder(&b).Encode(&call)

View File

@@ -4,7 +4,6 @@ import (
"context"
"net/http"
"github.com/fnproject/fn/api"
"github.com/fnproject/fn/api/common"
"github.com/fnproject/fn/fnext"
"github.com/gin-gonic/gin"
@@ -16,38 +15,8 @@ type middlewareController struct {
// 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
}
// CallFunction bypasses any further gin routing and calls the function directly
func (c *middlewareController) CallFunction(w http.ResponseWriter, r *http.Request) {
c.functionCalled = true
ctx := r.Context()
ctx = context.WithValue(ctx, fnext.MiddlewareControllerKey, c)
r = r.WithContext(ctx)
c.ginContext.Request = r
// since we added middleware that checks the app ID
// we need to ensure that we set it as soon as possible
appName := AppFromContext(ctx)
if appName != "" {
appID, err := c.server.datastore.GetAppID(ctx, appName)
if err != nil {
handleV1ErrorResponse(c.ginContext, err)
c.ginContext.Abort()
return
}
c.ginContext.Set(api.AppID, appID)
}
c.server.handleV1FunctionCall(c.ginContext)
c.ginContext.Abort()
}
func (c *middlewareController) FunctionCalled() bool {
return c.functionCalled
ginContext *gin.Context
server *Server
}
func (s *Server) apiMiddlewareWrapper() gin.HandlerFunc {
@@ -78,7 +47,7 @@ func (s *Server) runMiddleware(c *gin.Context, ms []fnext.Middleware) {
err := recover()
if err != nil {
common.Logger(c.Request.Context()).WithField("MiddleWarePanicRecovery:", err).Errorln("A panic occurred during middleware.")
handleV1ErrorResponse(c, ErrInternalServerError)
handleErrorResponse(c, ErrInternalServerError)
}
}()
@@ -86,13 +55,6 @@ func (s *Server) runMiddleware(c *gin.Context, ms []fnext.Middleware) {
last := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// fmt.Println("final handler called")
ctx := r.Context()
mctx := fnext.GetMiddlewareController(ctx)
// check for bypass
if mctx.FunctionCalled() {
// fmt.Println("func already called, skipping")
c.Abort()
return
}
c.Request = r.WithContext(ctx)
c.Next()
})

View File

@@ -11,6 +11,7 @@ import (
"testing"
"fmt"
"github.com/fnproject/fn/api/datastore"
"github.com/fnproject/fn/api/logs"
"github.com/fnproject/fn/api/models"
@@ -78,12 +79,10 @@ func TestRootMiddleware(t *testing.T) {
app2 := &models.App{ID: "app_id_2", Name: "myapp2", Config: models.Config{}}
ds := datastore.NewMockInit(
[]*models.App{app1, app2},
[]*models.Route{
{Path: "/", AppID: app1.ID, Image: "fnproject/hello", Type: "sync", Memory: 128, Timeout: 30, IdleTimeout: 30, Headers: map[string][]string{"X-Function": {"Test"}}},
{Path: "/myroute", AppID: app1.ID, Image: "fnproject/hello", Type: "sync", Memory: 128, Timeout: 30, IdleTimeout: 30, Headers: map[string][]string{"X-Function": {"Test"}}},
{Path: "/app2func", AppID: app2.ID, Image: "fnproject/hello", Type: "sync", Memory: 128, Timeout: 30, IdleTimeout: 30, Headers: map[string][]string{"X-Function": {"Test"}},
Config: map[string]string{"NAME": "johnny"},
},
[]*models.Fn{
{ID: "fn_id1", AppID: app1.ID, Image: "fnproject/hello"},
{ID: "fn_id2", AppID: app1.ID, Image: "fnproject/hello"},
{ID: "fn_id3", AppID: app2.ID, Image: "fnproject/hello"},
},
)
@@ -97,11 +96,7 @@ func TestRootMiddleware(t *testing.T) {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("funcit") != "" {
t.Log("breaker breaker!")
ctx := r.Context()
ctx = ContextWithApp(ctx, "myapp2")
ctx = ContextWithPath(ctx, "/app2func")
mctx := fnext.GetMiddlewareController(ctx)
mctx.CallFunction(w, r.WithContext(ctx))
w.Write([]byte("Rerooted"))
return
}
// If any context changes, user should use this: next.ServeHTTP(w, r.WithContext(ctx))
@@ -132,9 +127,9 @@ func TestRootMiddleware(t *testing.T) {
expectedCode int
expectedInBody string
}{
{"/r/myapp", `{"isDebug": true}`, "GET", map[string][]string{}, http.StatusOK, "middle"},
{"/r/myapp/myroute", `{"isDebug": true}`, "GET", map[string][]string{}, http.StatusOK, "middle"},
{"/v1/apps", `{"isDebug": true}`, "GET", map[string][]string{"funcit": {"Test"}}, http.StatusOK, "johnny"},
{"/invoke/fn_id1", `{"isDebug": true}`, "POST", map[string][]string{}, http.StatusOK, "middle"},
{"/v2/apps/app_id_1/fns/fn_id1", `{"isDebug": true}`, "POST", map[string][]string{}, http.StatusOK, "middle"},
{"/v2/apps", `{"isDebug": true}`, "POST", map[string][]string{"funcit": {"Test"}}, http.StatusOK, "Rerooted"},
} {
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
body := strings.NewReader(test.body)
@@ -163,7 +158,7 @@ func TestRootMiddleware(t *testing.T) {
}
req, err := http.NewRequest("POST", "http://127.0.0.1:8080/v1/apps", strings.NewReader("{\"app\": {\"name\": \"myapp3\"}}"))
req, err := http.NewRequest("POST", "http://127.0.0.1:8080/v2/apps", strings.NewReader("{\"name\": \"myapp3\"}"))
if err != nil {
t.Fatalf("Test: Could not create create app request")
}

View File

@@ -1,78 +0,0 @@
package server
import (
"context"
"github.com/fnproject/fn/api/models"
"github.com/fnproject/fn/fnext"
)
type routeListeners []fnext.RouteListener
var _ fnext.RouteListener = new(routeListeners)
// AddRouteListener adds a route listener extension to the set of listeners
// to be called around each route operation.
func (s *Server) AddRouteListener(listener fnext.RouteListener) {
*s.routeListeners = append(*s.routeListeners, listener)
}
func (a *routeListeners) BeforeRouteCreate(ctx context.Context, route *models.Route) error {
for _, l := range *a {
err := l.BeforeRouteCreate(ctx, route)
if err != nil {
return err
}
}
return nil
}
func (a *routeListeners) AfterRouteCreate(ctx context.Context, route *models.Route) error {
for _, l := range *a {
err := l.AfterRouteCreate(ctx, route)
if err != nil {
return err
}
}
return nil
}
func (a *routeListeners) BeforeRouteUpdate(ctx context.Context, route *models.Route) error {
for _, l := range *a {
err := l.BeforeRouteUpdate(ctx, route)
if err != nil {
return err
}
}
return nil
}
func (a *routeListeners) AfterRouteUpdate(ctx context.Context, route *models.Route) error {
for _, l := range *a {
err := l.AfterRouteUpdate(ctx, route)
if err != nil {
return err
}
}
return nil
}
func (a *routeListeners) BeforeRouteDelete(ctx context.Context, appId string, routePath string) error {
for _, l := range *a {
err := l.BeforeRouteDelete(ctx, appId, routePath)
if err != nil {
return err
}
}
return nil
}
func (a *routeListeners) AfterRouteDelete(ctx context.Context, appId string, routePath string) error {
for _, l := range *a {
err := l.AfterRouteDelete(ctx, appId, routePath)
if err != nil {
return err
}
}
return nil
}

View File

@@ -1,178 +0,0 @@
package server
import (
"context"
"net/http"
"path"
"strings"
"github.com/fnproject/fn/api"
"github.com/fnproject/fn/api/models"
"github.com/gin-gonic/gin"
)
/* handleRouteCreateOrUpdate is used to handle POST PUT and PATCH for routes.
Post will only create route if its not there and create app if its not.
create only
Post does not skip validation of zero values
Put will create app if its not there and if route is there update if not it will create new route.
update if exists or create if not exists
Put does not skip validation of zero values
Patch will not create app if it does not exist since the route needs to exist as well...
update only
Patch accepts partial updates / skips validation of zero values.
*/
func (s *Server) handleRoutesPostPut(c *gin.Context) {
ctx := c.Request.Context()
method := strings.ToUpper(c.Request.Method)
var wroute models.RouteWrapper
err := bindRoute(c, method, &wroute)
if err != nil {
handleV1ErrorResponse(c, err)
return
}
appName := c.MustGet(api.AppName).(string)
appID, err := s.ensureApp(ctx, appName, method)
if err != nil {
handleV1ErrorResponse(c, err)
return
}
resp, err := s.ensureRoute(ctx, appID, &wroute, method)
if err != nil {
handleV1ErrorResponse(c, err)
return
}
c.JSON(http.StatusOK, resp)
}
func (s *Server) handleRoutesPatch(c *gin.Context) {
ctx := c.Request.Context()
method := strings.ToUpper(c.Request.Method)
var wroute models.RouteWrapper
err := bindRoute(c, method, &wroute)
if err != nil {
handleV1ErrorResponse(c, err)
return
}
appID := c.MustGet(api.AppID).(string)
resp, err := s.ensureRoute(ctx, appID, &wroute, method)
if err != nil {
handleV1ErrorResponse(c, err)
return
}
c.JSON(http.StatusOK, resp)
}
func (s *Server) submitRoute(ctx context.Context, wroute *models.RouteWrapper) error {
if wroute.Route != nil {
wroute.Route.SetDefaults()
}
r, err := s.datastore.InsertRoute(ctx, wroute.Route)
if err != nil {
return err
}
wroute.Route = r
return nil
}
func (s *Server) changeRoute(ctx context.Context, wroute *models.RouteWrapper) error {
r, err := s.datastore.UpdateRoute(ctx, wroute.Route)
if err != nil {
return err
}
wroute.Route = r
return nil
}
func (s *Server) ensureRoute(ctx context.Context, appID string, wroute *models.RouteWrapper, method string) (routeResponse, error) {
bad := new(routeResponse)
wroute.Route.AppID = appID
switch method {
case http.MethodPost:
err := s.submitRoute(ctx, wroute)
if err != nil {
return *bad, err
}
return routeResponse{"Route successfully created", wroute.Route}, nil
case http.MethodPut:
_, err := s.datastore.GetRoute(ctx, appID, wroute.Route.Path)
if err != nil && err == models.ErrRoutesNotFound {
err := s.submitRoute(ctx, wroute)
if err != nil {
return *bad, err
}
return routeResponse{"Route successfully created", wroute.Route}, nil
}
err = s.changeRoute(ctx, wroute)
if err != nil {
return *bad, err
}
return routeResponse{"Route successfully updated", wroute.Route}, nil
case http.MethodPatch:
err := s.changeRoute(ctx, wroute)
if err != nil {
return *bad, err
}
return routeResponse{"Route successfully updated", wroute.Route}, nil
}
return *bad, nil
}
// ensureApp will only execute if it is on post or put. Patch is not allowed to create apps.
func (s *Server) ensureApp(ctx context.Context, appName string, method string) (string, error) {
appID, err := s.datastore.GetAppID(ctx, appName)
if err != nil && err != models.ErrAppsNotFound {
return "", err
} else if appID == "" {
// Create a new application assuming that /:app/ is an app name
newapp := &models.App{Name: appName}
app, err := s.datastore.InsertApp(ctx, newapp)
if err != nil {
return "", err
}
return app.ID, nil
}
return appID, nil
}
// bindRoute binds the RouteWrapper to the json from the request.
func bindRoute(c *gin.Context, method string, wroute *models.RouteWrapper) error {
err := c.BindJSON(wroute)
if err != nil {
if models.IsAPIError(err) {
return err
}
return models.ErrInvalidJSON
}
if wroute.Route == nil {
return models.ErrRoutesMissingNew
}
if method == http.MethodPut || method == http.MethodPatch {
p := path.Clean(c.MustGet(api.Path).(string))
if wroute.Route.Path != "" && wroute.Route.Path != p {
return models.ErrRoutesPathImmutable
}
wroute.Route.Path = p
}
if method == http.MethodPost {
if wroute.Route.Path == "" {
return models.ErrRoutesMissingPath
}
}
return nil
}

View File

@@ -1,28 +0,0 @@
package server
import (
"net/http"
"path"
"github.com/fnproject/fn/api"
"github.com/gin-gonic/gin"
)
func (s *Server) handleRouteDelete(c *gin.Context) {
ctx := c.Request.Context()
appID := c.MustGet(api.AppID).(string)
routePath := path.Clean(c.MustGet(api.Path).(string))
if _, err := s.datastore.GetRoute(ctx, appID, routePath); err != nil {
handleV1ErrorResponse(c, err)
return
}
if err := s.datastore.RemoveRoute(ctx, appID, routePath); err != nil {
handleV1ErrorResponse(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"message": "Route deleted"})
}

View File

@@ -1,21 +0,0 @@
package server
import (
"github.com/fnproject/fn/api"
"github.com/gin-gonic/gin"
"net/http"
"path"
)
func (s *Server) handleRouteGetAPI(c *gin.Context) {
ctx := c.Request.Context()
routePath := path.Clean("/" + c.MustGet(api.Path).(string))
route, err := s.datastore.GetRoute(ctx, c.MustGet(api.AppID).(string), routePath)
if err != nil {
handleV1ErrorResponse(c, err)
return
}
c.JSON(http.StatusOK, routeResponse{"Successfully loaded route", route})
}

View File

@@ -1,37 +0,0 @@
package server
import (
"encoding/base64"
"net/http"
"github.com/fnproject/fn/api"
"github.com/fnproject/fn/api/models"
"github.com/gin-gonic/gin"
)
func (s *Server) handleRouteList(c *gin.Context) {
ctx := c.Request.Context()
var filter models.RouteFilter
filter.Image = c.Query("image")
// filter.PathPrefix = c.Query("path_prefix") TODO not hooked up
filter.Cursor, filter.PerPage = pageParams(c, true)
routes, err := s.datastore.GetRoutesByApp(ctx, c.MustGet(api.AppID).(string), &filter)
if err != nil {
handleV1ErrorResponse(c, err)
return
}
var nextCursor string
if len(routes) > 0 && len(routes) == filter.PerPage {
last := []byte(routes[len(routes)-1].Path)
nextCursor = base64.RawURLEncoding.EncodeToString(last)
}
c.JSON(http.StatusOK, routesResponse{
Message: "Successfully listed routes",
NextCursor: nextCursor,
Routes: routes,
})
}

View File

@@ -1,358 +0,0 @@
package server
import (
"bytes"
"encoding/base64"
"encoding/json"
"net/http"
"strings"
"testing"
"time"
"fmt"
"github.com/fnproject/fn/api/datastore"
"github.com/fnproject/fn/api/logs"
"github.com/fnproject/fn/api/models"
"github.com/fnproject/fn/api/mqs"
)
type routeTestCase struct {
ds models.Datastore
logDB models.LogStore
method string
path string
body string
expectedCode int
expectedError error
}
func (test *routeTestCase) run(t *testing.T, i int, buf *bytes.Buffer) {
rnr, cancel := testRunner(t)
srv := testServer(test.ds, &mqs.Mock{}, test.logDB, rnr, ServerTypeFull)
body := bytes.NewBuffer([]byte(test.body))
_, rec := routerRequest(t, srv.Router, test.method, test.path, body)
if rec.Code != test.expectedCode {
t.Log(buf.String())
t.Log(rec.Body.String())
t.Errorf("Test %d: Expected status code to be %d but was %d",
i, test.expectedCode, rec.Code)
}
if test.expectedError != nil {
resp := getV1ErrorResponse(t, rec)
if resp.Error == nil {
t.Log(buf.String())
t.Errorf("Test %d: Expected error message to have `%s`, but it was nil",
i, test.expectedError)
} else if !strings.Contains(resp.Error.Message, test.expectedError.Error()) {
t.Log(buf.String())
t.Errorf("Test %d: Expected error message to have `%s`, but it was `%s`",
i, test.expectedError, resp.Error.Message)
}
}
if test.expectedCode == http.StatusOK {
var rwrap models.RouteWrapper
err := json.NewDecoder(rec.Body).Decode(&rwrap)
if err != nil {
t.Log(buf.String())
t.Errorf("Test %d: error decoding body for 'ok' json, it was a lie: %v", i, err)
}
route := rwrap.Route
if test.method == http.MethodPost {
// IsZero() doesn't really work, this ensures it's not unset as long as we're not in 1970
if time.Time(route.CreatedAt).Before(time.Now().Add(-1 * time.Hour)) {
t.Log(buf.String())
t.Errorf("Test %d: expected created_at to be set on route, it wasn't: %s", i, route.CreatedAt)
}
if !(time.Time(route.CreatedAt)).Equal(time.Time(route.UpdatedAt)) {
t.Log(buf.String())
t.Errorf("Test %d: expected updated_at to be set and same as created at, it wasn't: %s %s", i, route.CreatedAt, route.UpdatedAt)
}
}
if test.method == http.MethodPatch {
// IsZero() doesn't really work, this ensures it's not unset as long as we're not in 1970
if time.Time(route.UpdatedAt).Before(time.Now().Add(-1 * time.Hour)) {
t.Log(buf.String())
t.Errorf("Test %d: expected updated_at to be set on route, it wasn't: %s", i, route.UpdatedAt)
}
// this isn't perfect, since a PATCH could succeed without updating any
// fields (among other reasons), but just don't make a test for that or
// special case (the body or smth) to ignore it here!
// this is a decent approximation that the timestamp gets changed
if (time.Time(route.UpdatedAt)).Equal(time.Time(route.CreatedAt)) {
t.Log(buf.String())
t.Errorf("Test %d: expected updated_at to not be the same as created at, it wasn't: %s %s", i, route.CreatedAt, route.UpdatedAt)
}
}
}
cancel()
buf.Reset()
}
func TestRouteCreate(t *testing.T) {
buf := setLogBuffer()
a := &models.App{Name: "a", ID: "app_id"}
commonDS := datastore.NewMockInit([]*models.App{a})
for i, test := range []routeTestCase{
// errors
{commonDS, logs.NewMock(), http.MethodPost, "/v1/apps/a/routes", ``, http.StatusBadRequest, models.ErrInvalidJSON},
{commonDS, logs.NewMock(), http.MethodPost, "/v1/apps/a/routes", `{ "type": "sync" }`, http.StatusBadRequest, models.ErrRoutesMissingNew},
{commonDS, logs.NewMock(), http.MethodPost, "/v1/apps/a/routes", `{ "path": "/myroute", "type": "sync" }`, http.StatusBadRequest, models.ErrRoutesMissingNew},
{commonDS, logs.NewMock(), http.MethodPost, "/v1/apps/a/routes", `{ "route": { } }`, http.StatusBadRequest, models.ErrRoutesMissingPath},
{commonDS, logs.NewMock(), http.MethodPost, "/v1/apps/a/routes", `{ "route": { "path": "/myroute", "type": "sync" } }`, http.StatusBadRequest, models.ErrRoutesMissingImage},
{commonDS, logs.NewMock(), http.MethodPost, "/v1/apps/a/routes", `{ "route": { "image": "fnproject/fn-test-utils", "type": "sync" } }`, http.StatusBadRequest, models.ErrRoutesMissingPath},
{commonDS, logs.NewMock(), http.MethodPost, "/v1/apps/a/routes", `{ "route": { "image": "fnproject/fn-test-utils", "path": "myroute", "type": "sync" } }`, http.StatusBadRequest, models.ErrRoutesInvalidPath},
{commonDS, logs.NewMock(), http.MethodPost, "/v1/apps/$/routes", `{ "route": { "image": "fnproject/fn-test-utils", "path": "/myroute", "type": "sync" } }`, http.StatusBadRequest, models.ErrAppsInvalidName},
{commonDS, logs.NewMock(), http.MethodPost, "/v1/apps/a/routes", `{ "route": { "image": "fnproject/fn-test-utils", "path": "/myroute", "type": "sync", "cpus": "-100" } }`, http.StatusBadRequest, models.ErrInvalidCPUs},
{datastore.NewMockInit([]*models.App{a},
[]*models.Route{
{
AppID: a.ID,
Path: "/myroute",
},
},
), logs.NewMock(), http.MethodPost, "/v1/apps/a/routes", `{ "route": { "image": "fnproject/fn-test-utils", "path": "/myroute", "type": "sync" } }`, http.StatusConflict, models.ErrRoutesAlreadyExists},
// success
{datastore.NewMock(), logs.NewMock(), http.MethodPost, "/v1/apps/a/routes", `{ "route": { "image": "fnproject/fn-test-utils", "path": "/myroute", "type": "sync" } }`, http.StatusOK, nil},
{datastore.NewMock(), logs.NewMock(), http.MethodPost, "/v1/apps/a/routes", `{ "route": { "image": "fnproject/fn-test-utils", "path": "/myroute", "type": "sync", "cpus": "100m" } }`, http.StatusOK, nil},
{datastore.NewMock(), logs.NewMock(), http.MethodPost, "/v1/apps/a/routes", `{ "route": { "image": "fnproject/fn-test-utils", "path": "/myroute", "type": "sync", "cpus": "0.2" } }`, http.StatusOK, nil},
} {
test.run(t, i, buf)
}
}
func TestRoutePut(t *testing.T) {
buf := setLogBuffer()
a := &models.App{Name: "a", ID: "app_id"}
commonDS := datastore.NewMockInit([]*models.App{a})
for i, test := range []routeTestCase{
// errors (NOTE: this route doesn't exist yet)
{commonDS, logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ }`, http.StatusBadRequest, models.ErrRoutesMissingNew},
{commonDS, logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "path": "/myroute", "type": "sync" }`, http.StatusBadRequest, models.ErrRoutesMissingNew},
{commonDS, logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "route": { "type": "sync" } }`, http.StatusBadRequest, models.ErrRoutesMissingImage},
{commonDS, logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "route": { "path": "/myroute", "type": "sync" } }`, http.StatusBadRequest, models.ErrRoutesMissingImage},
{commonDS, logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "route": { "image": "fnproject/fn-test-utils", "path": "myroute", "type": "sync" } }`, http.StatusConflict, models.ErrRoutesPathImmutable},
{commonDS, logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "route": { "image": "fnproject/fn-test-utils", "path": "diffRoute", "type": "sync" } }`, http.StatusConflict, models.ErrRoutesPathImmutable},
{commonDS, logs.NewMock(), http.MethodPut, "/v1/apps/$/routes/myroute", `{ "route": { "image": "fnproject/fn-test-utils", "path": "/myroute", "type": "sync" } }`, http.StatusBadRequest, models.ErrAppsInvalidName},
{commonDS, logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "route": { "image": "fnproject/fn-test-utils", "path": "/myroute", "type": "invalid-type" } }`, http.StatusBadRequest, models.ErrRoutesInvalidType},
{commonDS, logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "route": { "image": "fnproject/fn-test-utils", "path": "/myroute", "format": "invalid-format", "type": "sync" } }`, http.StatusBadRequest, models.ErrRoutesInvalidFormat},
// success
{commonDS, logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "route": { "image": "fnproject/fn-test-utils", "path": "/myroute", "type": "sync" } }`, http.StatusOK, nil},
{commonDS, logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "route": { "image": "fnproject/fn-test-utils", "type": "sync" } }`, http.StatusOK, nil},
} {
t.Run(fmt.Sprintf("case %d", i),
func(t *testing.T) {
test.run(t, i, buf)
})
}
}
func TestRouteDelete(t *testing.T) {
buf := setLogBuffer()
a := &models.App{Name: "a", ID: "app_id"}
routes := []*models.Route{{AppID: a.ID, Path: "/myroute"}}
commonDS := datastore.NewMockInit([]*models.App{a}, routes)
for i, test := range []struct {
ds models.Datastore
logDB models.LogStore
path string
body string
expectedCode int
expectedError error
}{
{commonDS, logs.NewMock(), "/v1/apps/a/routes/missing", "", http.StatusNotFound, models.ErrRoutesNotFound},
{commonDS, logs.NewMock(), "/v1/apps/a/routes/myroute", "", http.StatusOK, nil},
} {
rnr, cancel := testRunner(t)
srv := testServer(test.ds, &mqs.Mock{}, test.logDB, rnr, ServerTypeFull)
_, rec := routerRequest(t, srv.Router, "DELETE", test.path, nil)
if rec.Code != test.expectedCode {
t.Log(buf.String())
t.Log(rec.Body.String())
t.Errorf("Test %d: Expected status code to be %d but was %d",
i, test.expectedCode, rec.Code)
}
if test.expectedError != nil {
resp := getV1ErrorResponse(t, rec)
if !strings.Contains(resp.Error.Message, test.expectedError.Error()) {
t.Log(buf.String())
t.Errorf("Test %d: Expected error message to have `%s`",
i, test.expectedError.Error())
}
}
cancel()
}
}
func TestRouteList(t *testing.T) {
buf := setLogBuffer()
rnr, cancel := testRunner(t)
defer cancel()
app := &models.App{Name: "myapp", ID: "app_id"}
ds := datastore.NewMockInit(
[]*models.App{app},
[]*models.Route{
{
Path: "/myroute",
AppID: app.ID,
},
{
Path: "/myroute1",
AppID: app.ID,
},
{
Path: "/myroute2",
Image: "fnproject/fn-test-utils",
AppID: app.ID,
},
},
)
fnl := logs.NewMock()
r1b := base64.RawURLEncoding.EncodeToString([]byte("/myroute"))
r2b := base64.RawURLEncoding.EncodeToString([]byte("/myroute1"))
r3b := base64.RawURLEncoding.EncodeToString([]byte("/myroute2"))
srv := testServer(ds, &mqs.Mock{}, fnl, rnr, ServerTypeFull)
for i, test := range []struct {
path string
body string
expectedCode int
expectedError error
expectedLen int
nextCursor string
}{
{"/v1/apps//routes", "", http.StatusBadRequest, models.ErrAppsMissingName, 0, ""},
{"/v1/apps/a/routes", "", http.StatusNotFound, models.ErrAppsNotFound, 0, ""},
{"/v1/apps/myapp/routes", "", http.StatusOK, nil, 3, ""},
{"/v1/apps/myapp/routes?per_page=1", "", http.StatusOK, nil, 1, r1b},
{"/v1/apps/myapp/routes?per_page=1&cursor=" + r1b, "", http.StatusOK, nil, 1, r2b},
{"/v1/apps/myapp/routes?per_page=1&cursor=" + r2b, "", http.StatusOK, nil, 1, r3b},
{"/v1/apps/myapp/routes?per_page=100&cursor=" + r2b, "", http.StatusOK, nil, 1, ""}, // cursor is empty if per_page > len(results)
{"/v1/apps/myapp/routes?per_page=1&cursor=" + r3b, "", http.StatusOK, nil, 0, ""}, // cursor could point to empty page
{"/v1/apps/myapp/routes?image=fnproject/fn-test-utils", "", http.StatusOK, nil, 1, ""},
} {
_, rec := routerRequest(t, srv.Router, "GET", test.path, nil)
if rec.Code != test.expectedCode {
t.Log(buf.String())
t.Errorf("Test %d: Expected status code to be %d but was %d",
i, test.expectedCode, rec.Code)
}
if test.expectedError != nil {
resp := getV1ErrorResponse(t, rec)
if !strings.Contains(resp.Error.Message, test.expectedError.Error()) {
t.Log(buf.String())
t.Errorf("Test %d: Expected error message to have `%s`",
i, test.expectedError.Error())
}
} else {
// normal path
var resp routesResponse
err := json.NewDecoder(rec.Body).Decode(&resp)
if err != nil {
t.Errorf("Test %d: Expected response body to be a valid json object. err: %v", i, err)
}
if len(resp.Routes) != test.expectedLen {
t.Errorf("Test %d: Expected route length to be %d, but got %d", i, test.expectedLen, len(resp.Routes))
}
if resp.NextCursor != test.nextCursor {
t.Errorf("Test %d: Expected next_cursor to be %s, but got %s", i, test.nextCursor, resp.NextCursor)
}
}
}
}
func TestRouteGet(t *testing.T) {
buf := setLogBuffer()
rnr, cancel := testRunner(t)
defer cancel()
ds := datastore.NewMock()
fnl := logs.NewMock()
srv := testServer(ds, &mqs.Mock{}, fnl, rnr, ServerTypeFull)
for i, test := range []struct {
path string
body string
expectedCode int
expectedError error
}{
{"/v1/apps/a/routes/myroute", "", http.StatusNotFound, nil},
} {
_, rec := routerRequest(t, srv.Router, "GET", test.path, nil)
if rec.Code != test.expectedCode {
t.Log(buf.String())
t.Errorf("Test %d: Expected status code to be %d but was %d",
i, test.expectedCode, rec.Code)
}
if test.expectedError != nil {
resp := getV1ErrorResponse(t, rec)
if !strings.Contains(resp.Error.Message, test.expectedError.Error()) {
t.Log(buf.String())
t.Errorf("Test %d: Expected error message to have `%s`",
i, test.expectedError.Error())
}
}
}
}
func TestRouteUpdate(t *testing.T) {
buf := setLogBuffer()
ds := datastore.NewMockInit()
for i, test := range []routeTestCase{
// success
{ds, logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute/do", `{ "route": { "image": "fnproject/yodawg" } }`, http.StatusOK, nil},
{ds, logs.NewMock(), http.MethodPatch, "/v1/apps/a/routes/myroute/do", `{ "route": { "image": "fnproject/fn-test-utils" } }`, http.StatusOK, nil},
// errors (after success, so route exists)
{ds, logs.NewMock(), http.MethodPatch, "/v1/apps/a/routes/myroute/do", ``, http.StatusBadRequest, models.ErrInvalidJSON},
{ds, logs.NewMock(), http.MethodPatch, "/v1/apps/a/routes/myroute/do", `{}`, http.StatusBadRequest, models.ErrRoutesMissingNew},
{ds, logs.NewMock(), http.MethodPatch, "/v1/apps/a/routes/myroute/do", `{ "route": { "type": "invalid-type" } }`, http.StatusBadRequest, models.ErrRoutesInvalidType},
{ds, logs.NewMock(), http.MethodPatch, "/v1/apps/a/routes/myroute/do", `{ "route": { "format": "invalid-format" } }`, http.StatusBadRequest, models.ErrRoutesInvalidFormat},
{ds, logs.NewMock(), http.MethodPatch, "/v1/apps/a/routes/myroute/do", `{ "route": { "timeout": 121 } }`, http.StatusBadRequest, models.ErrRoutesInvalidTimeout},
{ds, logs.NewMock(), http.MethodPatch, "/v1/apps/a/routes/myroute/do", `{ "route": { "type": "async", "timeout": 3601 } }`, http.StatusBadRequest, models.ErrRoutesInvalidTimeout},
{ds, logs.NewMock(), http.MethodPatch, "/v1/apps/a/routes/myroute/do", `{ "route": { "type": "async", "timeout": 121, "idle_timeout": 240 } }`, http.StatusOK, nil}, // should work if async
{ds, logs.NewMock(), http.MethodPatch, "/v1/apps/a/routes/myroute/do", `{ "route": { "idle_timeout": 3601 } }`, http.StatusBadRequest, models.ErrRoutesInvalidIdleTimeout},
{ds, logs.NewMock(), http.MethodPatch, "/v1/apps/a/routes/myroute/do", `{ "route": { "memory": 100000000000000 } }`, http.StatusBadRequest, models.ErrRoutesInvalidMemory},
{ds, logs.NewMock(), http.MethodPatch, "/v1/apps/a/routes/myroute/do", `{ "route": { "cpus": "foo" } }`, http.StatusBadRequest, models.ErrInvalidCPUs},
// TODO this should be correct, waiting for patch to come in
//{ds, logs.NewMock(), http.MethodPatch, "/v1/apps/b/routes/myroute/dont", `{ "route": {} }`, http.StatusNotFound, models.ErrAppsNotFound},
{ds, logs.NewMock(), http.MethodPatch, "/v1/apps/a/routes/myroute/dont", `{ "route": {} }`, http.StatusNotFound, models.ErrRoutesNotFound},
// Addresses #381
{ds, logs.NewMock(), http.MethodPatch, "/v1/apps/a/routes/myroute/do", `{ "route": { "path": "/otherpath" } }`, http.StatusConflict, models.ErrRoutesPathImmutable},
} {
test.run(t, i, buf)
}
}

View File

@@ -1,164 +0,0 @@
package server
import (
"bytes"
"io"
"net/http"
"path"
"strconv"
"sync"
"time"
"github.com/fnproject/fn/api"
"github.com/fnproject/fn/api/agent"
"github.com/fnproject/fn/api/common"
"github.com/fnproject/fn/api/models"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
)
// handleV1FunctionCall executes the function, for router handlers
func (s *Server) handleV1FunctionCall(c *gin.Context) {
err := s.handleFunctionCall2(c)
if err != nil {
handleV1ErrorResponse(c, err)
}
}
// handleFunctionCall2 executes the function and returns an error
// Requires the following in the context:
// * "app"
// * "path"
func (s *Server) handleFunctionCall2(c *gin.Context) error {
ctx := c.Request.Context()
var p string
r := PathFromContext(ctx)
if r == "" {
p = "/"
} else {
p = r
}
appID := c.MustGet(api.AppID).(string)
app, err := s.lbReadAccess.GetAppByID(ctx, appID)
if err != nil {
return err
}
routePath := path.Clean(p)
route, err := s.lbReadAccess.GetRoute(ctx, appID, routePath)
if err != nil {
return err
}
// gin sets this to 404 on NoRoute, so we'll just ensure it's 200 by default.
c.Status(200) // this doesn't write the header yet
return s.ServeRoute(c, app, route)
}
var (
bufPool = &sync.Pool{New: func() interface{} { return new(bytes.Buffer) }}
)
// ServeRoute serves an HTTP route for a given app
// This is exported to allow extensions to plugin their own route handling
func (s *Server) ServeRoute(c *gin.Context, app *models.App, route *models.Route) error {
buf := bufPool.Get().(*bytes.Buffer)
buf.Reset()
writer := syncResponseWriter{
Buffer: buf,
headers: c.Writer.Header(), // copy ref
}
defer bufPool.Put(buf) // TODO need to ensure this is safe with Dispatch?
// GetCall can mod headers, assign an id, look up the route/app (cached),
// strip params, etc.
// this should happen ASAP to turn app name to app ID
// GetCall can mod headers, assign an id, look up the route/app (cached),
// strip params, etc.
call, err := s.agent.GetCall(
agent.WithWriter(&writer), // XXX (reed): order matters [for now]
agent.FromRequest(app, route, c.Request),
)
if err != nil {
return err
}
model := call.Model()
{ // scope this, to disallow ctx use outside of this scope. add id for handleV1ErrorResponse logger
ctx, _ := common.LoggerWithFields(c.Request.Context(), logrus.Fields{"id": model.ID})
c.Request = c.Request.WithContext(ctx)
}
if model.Type == "async" {
// TODO we should push this into GetCall somehow (CallOpt maybe) or maybe agent.Queue(Call) ?
if c.Request.ContentLength > 0 {
buf.Grow(int(c.Request.ContentLength))
}
_, err := buf.ReadFrom(c.Request.Body)
if err != nil {
return models.ErrInvalidPayload
}
model.Payload = buf.String()
err = s.lbEnqueue.Enqueue(c.Request.Context(), model)
if err != nil {
return err
}
c.JSON(http.StatusAccepted, map[string]string{"call_id": model.ID})
return nil
}
err = s.agent.Submit(call)
if err != nil {
// NOTE if they cancel the request then it will stop the call (kind of cool),
// we could filter that error out here too as right now it yells a little
if err == models.ErrCallTimeoutServerBusy || err == models.ErrCallTimeout {
// TODO maneuver
// add this, since it means that start may not have been called [and it's relevant]
c.Writer.Header().Add("XXX-FXLB-WAIT", time.Now().Sub(time.Time(model.CreatedAt)).String())
}
return err
}
// if they don't set a content-type - detect it
if writer.Header().Get("Content-Type") == "" {
// see http.DetectContentType, the go server is supposed to do this for us but doesn't appear to?
var contentType string
jsonPrefix := [1]byte{'{'} // stack allocated
if bytes.HasPrefix(buf.Bytes(), jsonPrefix[:]) {
// try to detect json, since DetectContentType isn't a hipster.
contentType = "application/json; charset=utf-8"
} else {
contentType = http.DetectContentType(buf.Bytes())
}
writer.Header().Set("Content-Type", contentType)
}
writer.Header().Set("Content-Length", strconv.Itoa(int(buf.Len())))
if writer.status > 0 {
c.Writer.WriteHeader(writer.status)
}
io.Copy(c.Writer, &writer)
return nil
}
var _ http.ResponseWriter = new(syncResponseWriter)
// implements http.ResponseWriter
// this little guy buffers responses from user containers and lets them still
// set headers and such without us risking writing partial output [as much, the
// server could still die while we're copying the buffer]. this lets us set
// content length and content type nicely, as a bonus. it is sad, yes.
type syncResponseWriter struct {
headers http.Header
status int
*bytes.Buffer
}
func (s *syncResponseWriter) Header() http.Header { return s.headers }
func (s *syncResponseWriter) WriteHeader(code int) { s.status = code }

View File

@@ -1,105 +0,0 @@
package server
import (
"bytes"
"context"
"net/http"
"testing"
"github.com/fnproject/fn/api/agent"
"github.com/fnproject/fn/api/datastore"
"github.com/fnproject/fn/api/models"
"github.com/fnproject/fn/api/mqs"
"github.com/gin-gonic/gin"
)
func testRouterAsync(ds models.Datastore, mq models.MessageQueue, rnr agent.Agent) *gin.Engine {
ctx := context.Background()
engine := gin.New()
s := &Server{
agent: rnr,
Router: engine,
AdminRouter: engine,
datastore: ds,
lbReadAccess: ds,
lbEnqueue: agent.NewDirectEnqueueAccess(mq),
mq: mq,
nodeType: ServerTypeFull,
}
r := s.Router
r.Use(gin.Logger())
s.Router.Use(loggerWrap)
s.bindHandlers(ctx)
return r
}
func TestRouteRunnerAsyncExecution(t *testing.T) {
buf := setLogBuffer()
app := &models.App{ID: "app_id", Name: "myapp", Config: map[string]string{"app": "true"}}
ds := datastore.NewMockInit(
[]*models.App{app},
[]*models.Route{
{Type: "async", Path: "/hot-http", AppID: app.ID, Image: "fnproject/fn-test-utils", Format: "http", Config: map[string]string{"test": "true"}, Memory: 128, Timeout: 4, IdleTimeout: 30},
{Type: "async", Path: "/hot-json", AppID: app.ID, Image: "fnproject/fn-test-utils", Format: "json", Config: map[string]string{"test": "true"}, Memory: 128, Timeout: 4, IdleTimeout: 30},
{Type: "async", Path: "/myroute", AppID: app.ID, Image: "fnproject/hello", Config: map[string]string{"test": "true"}, Memory: 128, CPUs: 200, Timeout: 30, IdleTimeout: 30},
{Type: "async", Path: "/myerror", AppID: app.ID, Image: "fnproject/error", Config: map[string]string{"test": "true"}, Memory: 128, Timeout: 30, IdleTimeout: 30},
{Type: "async", Path: "/myroute/:param", AppID: app.ID, Image: "fnproject/hello", Config: map[string]string{"test": "true"}, Memory: 128, Timeout: 30, IdleTimeout: 30},
},
)
mq := &mqs.Mock{}
for i, test := range []struct {
path string
body string
headers map[string][]string
expectedCode int
expectedEnv map[string]string
}{
{"/r/myapp/myroute", `{"isDebug": true}`, map[string][]string{}, http.StatusAccepted, map[string]string{"TEST": "true", "APP": "true"}},
{"/r/myapp/hot-http", `{"isDebug": true}`, map[string][]string{}, http.StatusAccepted, map[string]string{"TEST": "true", "APP": "true"}},
{"/r/myapp/hot-json", `{"isDebug": true}`, map[string][]string{}, http.StatusAccepted, map[string]string{"TEST": "true", "APP": "true"}},
// FIXME: this just hangs
//{"/r/myapp/myroute/1", ``, map[string][]string{}, http.StatusAccepted, map[string]string{"TEST": "true", "APP": "true"}},
{"/r/myapp/myerror", `{"isDebug": true, "isCrash": true}`, map[string][]string{}, http.StatusAccepted, map[string]string{"TEST": "true", "APP": "true"}},
{"/r/myapp/myroute", `{"echoContent": "test","isDebug": true}`, map[string][]string{}, http.StatusAccepted, map[string]string{"TEST": "true", "APP": "true"}},
{
"/r/myapp/myroute",
`{"isDebug": true}`,
map[string][]string{"X-Function": []string{"test"}},
http.StatusAccepted,
map[string]string{
"TEST": "true",
"APP": "true",
"HEADER_X_FUNCTION": "test",
},
},
} {
body := bytes.NewBuffer([]byte(test.body))
t.Log("About to start router")
rnr, cancel := testRunner(t, ds)
router := testRouterAsync(ds, mq, rnr)
t.Log("making requests")
req, rec := newRouterRequest(t, "POST", test.path, body)
for name, value := range test.headers {
req.Header.Set(name, value[0])
}
t.Log("About to start router2")
router.ServeHTTP(rec, req)
t.Log("after servehttp")
if rec.Code != test.expectedCode {
t.Log(buf.String())
t.Errorf("Test %d: Expected status code to be %d but was %d",
i, test.expectedCode, rec.Code)
}
// TODO can test body and headers in the actual mq message w/ an agent that doesn't dequeue?
// this just makes sure tasks are submitted (ok)...
cancel()
}
}

View File

@@ -5,6 +5,7 @@ import (
"io"
"net/http"
"strconv"
"sync"
"time"
"github.com/fnproject/fn/api"
@@ -15,6 +16,26 @@ import (
"github.com/sirupsen/logrus"
)
var (
bufPool = &sync.Pool{New: func() interface{} { return new(bytes.Buffer) }}
)
var _ http.ResponseWriter = new(syncResponseWriter)
// implements http.ResponseWriter
// this little guy buffers responses from user containers and lets them still
// set headers and such without us risking writing partial output [as much, the
// server could still die while we're copying the buffer]. this lets us set
// content length and content type nicely, as a bonus. it is sad, yes.
type syncResponseWriter struct {
headers http.Header
status int
*bytes.Buffer
}
func (s *syncResponseWriter) Header() http.Header { return s.headers }
func (s *syncResponseWriter) WriteHeader(code int) { s.status = code }
// handleFnInvokeCall executes the function, for router handlers
func (s *Server) handleFnInvokeCall(c *gin.Context) {
fnID := c.Param(api.ParamFnID)

View File

@@ -1,6 +1,8 @@
package server
import (
"crypto/rand"
"encoding/base64"
"fmt"
"io/ioutil"
"net/http"
@@ -163,7 +165,7 @@ func TestFnInvokeRunnerExecution(t *testing.T) {
rnr, cancelrnr := testRunner(t, ds, ls)
defer cancelrnr()
srv := testServer(ds, &mqs.Mock{}, ls, rnr, ServerTypeFull)
srv := testServer(ds, &mqs.Mock{}, ls, rnr, ServerTypeFull, LimitRequestBody(32256))
expHeaders := map[string][]string{"Content-Type": {"application/json; charset=utf-8"}}
expCTHeaders := map[string][]string{"Content-Type": {"foo/bar"}}
@@ -182,6 +184,10 @@ func TestFnInvokeRunnerExecution(t *testing.T) {
// sleep between logs and with debug enabled, fn-test-utils will log header/footer below:
multiLog := `{"echoContent": "_TRX_ID_", "sleepTime": 1000, "isDebug": true}`
//over sized request
var bigbufa [32257]byte
rand.Read(bigbufa[:])
bigbuf := base64.StdEncoding.EncodeToString(bigbufa[:]) // this will be > bigbufa, but json compatible
bigoutput := `{"echoContent": "_TRX_ID_", "isDebug": true, "trailerRepeat": 1000}` // 1000 trailers to exceed 2K
smalloutput := `{"echoContent": "_TRX_ID_", "isDebug": true, "trailerRepeat": 1}` // 1 trailer < 2K
@@ -222,6 +228,7 @@ func TestFnInvokeRunnerExecution(t *testing.T) {
{"/invoke/http_fn_id", smalloutput, "POST", http.StatusOK, nil, "", nil},
{"/invoke/default_fn_id", bigoutput, "POST", http.StatusBadGateway, nil, "", nil},
{"/invoke/default_fn_id", smalloutput, "POST", http.StatusOK, nil, "", nil},
{"/invoke/http_fn_id", bigbuf, "POST", http.StatusRequestEntityTooLarge, nil, "", nil},
}
callIds := make([]string, len(testCases))
@@ -294,8 +301,8 @@ func TestInvokeRunnerTimeout(t *testing.T) {
}
}()
models.RouteMaxMemory = uint64(1024 * 1024 * 1024) // 1024 TB
hugeMem := uint64(models.RouteMaxMemory - 1)
models.MaxMemory = uint64(1024 * 1024 * 1024) // 1024 TB
hugeMem := uint64(models.MaxMemory - 1)
app := &models.App{ID: "app_id", Name: "myapp", Config: models.Config{}}
coldFn := &models.Fn{ID: "cold", Name: "cold", AppID: app.ID, Format: "", Image: "fnproject/fn-test-utils", ResourceConfig: models.ResourceConfig{Memory: 128, Timeout: 4, IdleTimeout: 30}}

View File

@@ -9,7 +9,6 @@ import (
"testing"
"context"
"errors"
"os"
"github.com/fnproject/fn/api/agent"
@@ -88,52 +87,6 @@ func checkLogs(t *testing.T, tnum int, ds models.LogStore, callID string, expect
return true
}
// implement models.MQ and models.APIError
type errorMQ struct {
error
code int
}
func (mock *errorMQ) Push(context.Context, *models.Call) (*models.Call, error) { return nil, mock }
func (mock *errorMQ) Reserve(context.Context) (*models.Call, error) { return nil, mock }
func (mock *errorMQ) Delete(context.Context, *models.Call) error { return mock }
func (mock *errorMQ) Code() int { return mock.code }
func (mock *errorMQ) Close() error { return nil }
func TestFailedEnqueue(t *testing.T) {
buf := setLogBuffer()
app := &models.App{ID: "app_id", Name: "myapp", Config: models.Config{}}
ds := datastore.NewMockInit(
[]*models.App{app},
[]*models.Route{
{Path: "/dummy", Image: "dummy/dummy", Type: "async", Memory: 128, Timeout: 30, IdleTimeout: 30, AppID: app.ID},
},
)
err := errors.New("Unable to push task to queue")
mq := &errorMQ{err, http.StatusInternalServerError}
fnl := logs.NewMock()
rnr, cancelrnr := testRunner(t, ds, mq, fnl)
defer cancelrnr()
srv := testServer(ds, mq, fnl, rnr, ServerTypeFull)
for i, test := range []struct {
path string
body string
method string
expectedCode int
expectedHeaders map[string][]string
}{
{"/r/myapp/dummy", ``, "POST", http.StatusInternalServerError, nil},
} {
body := strings.NewReader(test.body)
_, rec := routerRequest(t, srv.Router, test.method, test.path, body)
if rec.Code != test.expectedCode {
t.Log(buf.String())
t.Errorf("Test %d: Expected status code to be %d but was %d",
i, test.expectedCode, rec.Code)
}
}
}
func TestTriggerRunnerGet(t *testing.T) {
buf := setLogBuffer()
app := &models.App{ID: "app_id", Name: "myapp", Config: models.Config{}}
@@ -475,8 +428,8 @@ func TestTriggerRunnerTimeout(t *testing.T) {
}
}()
models.RouteMaxMemory = uint64(1024 * 1024 * 1024) // 1024 TB
hugeMem := uint64(models.RouteMaxMemory - 1)
models.MaxMemory = uint64(1024 * 1024 * 1024) // 1024 TB
hugeMem := uint64(models.MaxMemory - 1)
app := &models.App{ID: "app_id", Name: "myapp", Config: models.Config{}}
coldFn := &models.Fn{ID: "cold", Name: "cold", AppID: app.ID, Format: "", Image: "fnproject/fn-test-utils", ResourceConfig: models.ResourceConfig{Memory: 128, Timeout: 4, IdleTimeout: 30}}

View File

@@ -1,490 +0,0 @@
package server
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"strings"
"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"
)
// TODO Deprecate with Routes
func TestRouteRunnerGet(t *testing.T) {
buf := setLogBuffer()
app := &models.App{ID: "app_id", Name: "myapp", Config: models.Config{}}
ds := datastore.NewMockInit(
[]*models.App{app},
)
rnr, cancel := testRunner(t, ds)
defer cancel()
logDB := logs.NewMock()
srv := testServer(ds, &mqs.Mock{}, logDB, rnr, ServerTypeFull)
for i, test := range []struct {
path string
body string
expectedCode int
expectedError error
}{
{"/route", "", http.StatusNotFound, nil},
{"/r/app/route", "", http.StatusNotFound, models.ErrAppsNotFound},
{"/r/myapp/route", "", http.StatusNotFound, models.ErrRoutesNotFound},
} {
_, rec := routerRequest(t, srv.Router, "GET", test.path, nil)
if rec.Code != test.expectedCode {
t.Log(buf.String())
t.Errorf("Test %d: Expected status code for path %s to be %d but was %d",
i, test.path, test.expectedCode, rec.Code)
}
if test.expectedError != nil {
resp := getV1ErrorResponse(t, rec)
if !strings.Contains(resp.Error.Message, test.expectedError.Error()) {
t.Log(buf.String())
t.Errorf("Test %d: Expected error message to have `%s`, but got `%s`",
i, test.expectedError.Error(), resp.Error.Message)
}
}
}
}
func TestRouteRunnerPost(t *testing.T) {
buf := setLogBuffer()
app := &models.App{ID: "app_id", Name: "myapp", Config: models.Config{}}
ds := datastore.NewMockInit(
[]*models.App{app},
)
rnr, cancel := testRunner(t, ds)
defer cancel()
fnl := logs.NewMock()
srv := testServer(ds, &mqs.Mock{}, fnl, rnr, ServerTypeFull)
for i, test := range []struct {
path string
body string
expectedCode int
expectedError error
}{
{"/route", `{ "payload": "" }`, http.StatusNotFound, nil},
{"/r/app/route", `{ "payload": "" }`, http.StatusNotFound, models.ErrAppsNotFound},
{"/r/myapp/route", `{ "payload": "" }`, http.StatusNotFound, models.ErrRoutesNotFound},
} {
body := bytes.NewBuffer([]byte(test.body))
_, rec := routerRequest(t, srv.Router, "POST", test.path, body)
if rec.Code != test.expectedCode {
t.Log(buf.String())
t.Errorf("Test %d: Expected status code for path %s to be %d but was %d",
i, test.path, test.expectedCode, rec.Code)
}
if test.expectedError != nil {
resp := getV1ErrorResponse(t, rec)
respMsg := resp.Error.Message
expMsg := test.expectedError.Error()
if respMsg != expMsg && !strings.Contains(respMsg, expMsg) {
t.Log(buf.String())
t.Errorf("Test %d: Expected error message to have `%s`",
i, test.expectedError.Error())
}
}
}
}
func TestRouteRunnerExecEmptyBody(t *testing.T) {
buf := setLogBuffer()
isFailure := false
defer func() {
if isFailure {
t.Log(buf.String())
}
}()
rCfg := map[string]string{"ENABLE_HEADER": "yes", "ENABLE_FOOTER": "yes"} // enable container start/end header/footer
rHdr := map[string][]string{"X-Function": {"Test"}}
rImg := "fnproject/fn-test-utils"
app := &models.App{ID: "app_id", Name: "soup"}
ds := datastore.NewMockInit(
[]*models.App{app},
[]*models.Route{
{Path: "/cold", AppID: app.ID, Image: rImg, Type: "sync", Memory: 64, Timeout: 10, IdleTimeout: 20, Headers: rHdr, Config: rCfg},
{Path: "/hothttp", AppID: app.ID, Image: rImg, Type: "sync", Format: "http", Memory: 64, Timeout: 10, IdleTimeout: 20, Headers: rHdr, Config: rCfg},
{Path: "/hotjson", AppID: app.ID, Image: rImg, Type: "sync", Format: "json", Memory: 64, Timeout: 10, IdleTimeout: 20, Headers: rHdr, Config: rCfg},
},
)
ls := logs.NewMock()
rnr, cancelrnr := testRunner(t, ds, ls)
defer cancelrnr()
srv := testServer(ds, &mqs.Mock{}, ls, rnr, ServerTypeFull)
expHeaders := map[string][]string{"X-Function": {"Test"}}
emptyBody := `{"echoContent": "_TRX_ID_", "isDebug": true, "isEmptyBody": true}`
// Test hot cases twice to rule out hot-containers corrupting next request.
testCases := []struct {
path string
}{
{"/r/soup/cold/"},
{"/r/soup/hothttp"},
{"/r/soup/hothttp"},
{"/r/soup/hotjson"},
{"/r/soup/hotjson"},
}
for i, test := range testCases {
trx := fmt.Sprintf("_trx_%d_", i)
body := strings.NewReader(strings.Replace(emptyBody, "_TRX_ID_", trx, 1))
_, rec := routerRequest(t, srv.Router, "GET", test.path, body)
respBytes, _ := ioutil.ReadAll(rec.Body)
respBody := string(respBytes)
maxBody := len(respBody)
if maxBody > 1024 {
maxBody = 1024
}
if rec.Code != http.StatusOK {
isFailure = true
t.Errorf("Test %d: Expected status code to be %d but was %d. body: %s",
i, http.StatusOK, rec.Code, respBody[:maxBody])
} else if len(respBytes) != 0 {
isFailure = true
t.Errorf("Test %d: Expected empty body but got %d. body: %s",
i, len(respBytes), respBody[:maxBody])
}
for name, header := range expHeaders {
if header[0] != rec.Header().Get(name) {
isFailure = true
t.Errorf("Test %d: Expected header `%s` to be %s but was %s. body: %s",
i, name, header[0], rec.Header().Get(name), respBody)
}
}
}
}
func TestRouteRunnerExecution(t *testing.T) {
buf := setLogBuffer()
isFailure := false
tweaker := envTweaker("FN_MAX_RESPONSE_SIZE", "2048")
defer tweaker()
// Log once after we are done, flow of events are important (hot/cold containers, idle timeout, etc.)
// for figuring out why things failed.
defer func() {
if isFailure {
t.Log(buf.String())
}
}()
rCfg := map[string]string{"ENABLE_HEADER": "yes", "ENABLE_FOOTER": "yes"} // enable container start/end header/footer
rHdr := map[string][]string{"X-Function": {"Test"}}
rImg := "fnproject/fn-test-utils"
rImgBs1 := "fnproject/imagethatdoesnotexist"
rImgBs2 := "localhost:5050/fnproject/imagethatdoesnotexist"
app := &models.App{ID: "app_id", Name: "myapp"}
ds := datastore.NewMockInit(
[]*models.App{app},
[]*models.Route{
{Path: "/", AppID: app.ID, Image: rImg, Type: "sync", Memory: 64, Timeout: 30, IdleTimeout: 30, Headers: rHdr, Config: rCfg},
{Path: "/myhot", AppID: app.ID, Image: rImg, Type: "sync", Format: "http", Memory: 64, Timeout: 30, IdleTimeout: 30, Headers: rHdr, Config: rCfg},
{Path: "/myhotjason", AppID: app.ID, Image: rImg, Type: "sync", Format: "json", Memory: 64, Timeout: 30, IdleTimeout: 30, Headers: rHdr, Config: rCfg},
{Path: "/myroute", AppID: app.ID, Image: rImg, Type: "sync", Memory: 64, Timeout: 30, IdleTimeout: 30, Headers: rHdr, Config: rCfg},
{Path: "/myerror", AppID: app.ID, Image: rImg, Type: "sync", Memory: 64, Timeout: 30, IdleTimeout: 30, Headers: rHdr, Config: rCfg},
{Path: "/mydne", AppID: app.ID, Image: rImgBs1, Type: "sync", Memory: 64, Timeout: 30, IdleTimeout: 30, Headers: rHdr, Config: rCfg},
{Path: "/mydnehot", AppID: app.ID, Image: rImgBs1, Type: "sync", Format: "http", Memory: 64, Timeout: 30, IdleTimeout: 30, Headers: rHdr, Config: rCfg},
{Path: "/mydneregistry", AppID: app.ID, Image: rImgBs2, Type: "sync", Format: "http", Memory: 64, Timeout: 30, IdleTimeout: 30, Headers: rHdr, Config: rCfg},
{Path: "/myoom", AppID: app.ID, Image: rImg, Type: "sync", Memory: 8, Timeout: 30, IdleTimeout: 30, Headers: rHdr, Config: rCfg},
{Path: "/mybigoutputcold", AppID: app.ID, Image: rImg, Type: "sync", Memory: 64, Timeout: 10, IdleTimeout: 20, Headers: rHdr, Config: rCfg},
{Path: "/mybigoutputhttp", AppID: app.ID, Image: rImg, Type: "sync", Format: "http", Memory: 64, Timeout: 10, IdleTimeout: 20, Headers: rHdr, Config: rCfg},
{Path: "/mybigoutputjson", AppID: app.ID, Image: rImg, Type: "sync", Format: "json", Memory: 64, Timeout: 10, IdleTimeout: 20, Headers: rHdr, Config: rCfg},
},
)
ls := logs.NewMock()
rnr, cancelrnr := testRunner(t, ds, ls)
defer cancelrnr()
srv := testServer(ds, &mqs.Mock{}, ls, rnr, ServerTypeFull)
expHeaders := map[string][]string{"X-Function": {"Test"}, "Content-Type": {"application/json; charset=utf-8"}}
expCTHeaders := map[string][]string{"X-Function": {"Test"}, "Content-Type": {"foo/bar"}}
// Checking for EndOfLogs currently depends on scheduling of go-routines (in docker/containerd) that process stderr & stdout.
// Therefore, not testing for EndOfLogs for hot containers (which has complex I/O processing) anymore.
multiLogExpectCold := []string{"BeginOfLogs", "EndOfLogs"}
multiLogExpectHot := []string{"BeginOfLogs" /*, "EndOfLogs" */}
crasher := `{"echoContent": "_TRX_ID_", "isDebug": true, "isCrash": true}` // crash container
oomer := `{"echoContent": "_TRX_ID_", "isDebug": true, "allocateMemory": 12000000}` // ask for 12MB
badHot := `{"echoContent": "_TRX_ID_", "invalidResponse": true, "isDebug": true}` // write a not json/http as output
ok := `{"echoContent": "_TRX_ID_", "isDebug": true}` // good response / ok
respTypeLie := `{"echoContent": "_TRX_ID_", "responseContentType": "foo/bar", "isDebug": true}` // Content-Type: foo/bar
respTypeJason := `{"echoContent": "_TRX_ID_", "jasonContentType": "foo/bar", "isDebug": true}` // Content-Type: foo/bar
// sleep between logs and with debug enabled, fn-test-utils will log header/footer below:
multiLog := `{"echoContent": "_TRX_ID_", "sleepTime": 1000, "isDebug": true}`
bigoutput := `{"echoContent": "_TRX_ID_", "isDebug": true, "trailerRepeat": 1000}` // 1000 trailers to exceed 2K
smalloutput := `{"echoContent": "_TRX_ID_", "isDebug": true, "trailerRepeat": 1}` // 1 trailer < 2K
testCases := []struct {
path string
body string
method string
expectedCode int
expectedHeaders map[string][]string
expectedErrSubStr string
expectedLogsSubStr []string
}{
{"/r/myapp/", ok, "GET", http.StatusOK, expHeaders, "", nil},
{"/r/myapp/myhot", badHot, "GET", http.StatusBadGateway, expHeaders, "invalid http response", nil},
// hot container now back to normal:
{"/r/myapp/myhot", ok, "GET", http.StatusOK, expHeaders, "", nil},
{"/r/myapp/myhotjason", badHot, "GET", http.StatusBadGateway, expHeaders, "invalid json response", nil},
// hot container now back to normal:
{"/r/myapp/myhotjason", ok, "GET", http.StatusOK, expHeaders, "", nil},
{"/r/myapp/myhot", respTypeLie, "GET", http.StatusOK, expCTHeaders, "", nil},
{"/r/myapp/myhotjason", respTypeLie, "GET", http.StatusOK, expCTHeaders, "", nil},
{"/r/myapp/myhotjason", respTypeJason, "GET", http.StatusOK, expCTHeaders, "", nil},
{"/r/myapp/myroute", ok, "GET", http.StatusOK, expHeaders, "", nil},
{"/r/myapp/myerror", crasher, "GET", http.StatusBadGateway, expHeaders, "container exit code 2", nil},
{"/r/myapp/mydne", ``, "GET", http.StatusNotFound, nil, "pull access denied", nil},
{"/r/myapp/mydnehot", ``, "GET", http.StatusNotFound, nil, "pull access denied", nil},
// hit a registry that doesn't exist, make sure the real error body gets plumbed out
{"/r/myapp/mydneregistry", ``, "GET", http.StatusInternalServerError, nil, "connection refused", nil},
{"/r/myapp/myoom", oomer, "GET", http.StatusBadGateway, nil, "container out of memory", nil},
{"/r/myapp/myhot", multiLog, "GET", http.StatusOK, nil, "", multiLogExpectHot},
{"/r/myapp/", multiLog, "GET", http.StatusOK, nil, "", multiLogExpectCold},
{"/r/myapp/mybigoutputjson", bigoutput, "GET", http.StatusBadGateway, nil, "function response too large", nil},
{"/r/myapp/mybigoutputjson", smalloutput, "GET", http.StatusOK, nil, "", nil},
{"/r/myapp/mybigoutputhttp", bigoutput, "GET", http.StatusBadGateway, nil, "", nil},
{"/r/myapp/mybigoutputhttp", smalloutput, "GET", http.StatusOK, nil, "", nil},
{"/r/myapp/mybigoutputcold", bigoutput, "GET", http.StatusBadGateway, nil, "", nil},
{"/r/myapp/mybigoutputcold", smalloutput, "GET", http.StatusOK, nil, "", nil},
}
callIds := make([]string, len(testCases))
for i, test := range testCases {
trx := fmt.Sprintf("_trx_%d_", i)
body := strings.NewReader(strings.Replace(test.body, "_TRX_ID_", trx, 1))
_, rec := routerRequest(t, srv.Router, test.method, test.path, body)
respBytes, _ := ioutil.ReadAll(rec.Body)
respBody := string(respBytes)
maxBody := len(respBody)
if maxBody > 1024 {
maxBody = 1024
}
callIds[i] = rec.Header().Get("Fn_call_id")
if rec.Code != test.expectedCode {
isFailure = true
t.Errorf("Test %d: Expected status code to be %d but was %d. body: %s",
i, test.expectedCode, rec.Code, respBody[:maxBody])
}
if rec.Code == http.StatusOK && !strings.Contains(respBody, trx) {
isFailure = true
t.Errorf("Test %d: Expected response to include %s but got body: %s",
i, trx, respBody[:maxBody])
}
if test.expectedErrSubStr != "" && !strings.Contains(respBody, test.expectedErrSubStr) {
isFailure = true
t.Errorf("Test %d: Expected response to include %s but got body: %s",
i, test.expectedErrSubStr, respBody[:maxBody])
}
if test.expectedHeaders != nil {
for name, header := range test.expectedHeaders {
if header[0] != rec.Header().Get(name) {
isFailure = true
t.Errorf("Test %d: Expected header `%s` to be %s but was %s. body: %s",
i, name, header[0], rec.Header().Get(name), respBody)
}
}
}
}
for i, test := range testCases {
if test.expectedLogsSubStr != nil {
if !checkLogs(t, i, ls, callIds[i], test.expectedLogsSubStr) {
isFailure = true
}
}
}
}
func TestRouteRunnerTimeout(t *testing.T) {
buf := setLogBuffer()
isFailure := false
// Log once after we are done, flow of events are important (hot/cold containers, idle timeout, etc.)
// for figuring out why things failed.
defer func() {
if isFailure {
t.Log(buf.String())
}
}()
models.RouteMaxMemory = uint64(1024 * 1024 * 1024) // 1024 TB
hugeMem := uint64(models.RouteMaxMemory - 1)
app := &models.App{ID: "app_id", Name: "myapp", Config: models.Config{}}
ds := datastore.NewMockInit(
[]*models.App{app},
[]*models.Route{
{Path: "/cold", Image: "fnproject/fn-test-utils", Type: "sync", Memory: 128, Timeout: 4, IdleTimeout: 30, AppID: app.ID},
{Path: "/hot", Image: "fnproject/fn-test-utils", Type: "sync", Format: "http", Memory: 128, Timeout: 4, IdleTimeout: 30, AppID: app.ID},
{Path: "/hot-json", Image: "fnproject/fn-test-utils", Type: "sync", Format: "json", Memory: 128, Timeout: 4, IdleTimeout: 30, AppID: app.ID},
{Path: "/bigmem-cold", Image: "fnproject/fn-test-utils", Type: "sync", Memory: hugeMem, Timeout: 1, IdleTimeout: 30, AppID: app.ID},
{Path: "/bigmem-hot", Image: "fnproject/fn-test-utils", Type: "sync", Format: "http", Memory: hugeMem, Timeout: 1, IdleTimeout: 30, AppID: app.ID},
},
)
fnl := logs.NewMock()
rnr, cancelrnr := testRunner(t, ds, fnl)
defer cancelrnr()
srv := testServer(ds, &mqs.Mock{}, fnl, rnr, ServerTypeFull)
for i, test := range []struct {
path string
body string
method string
expectedCode int
expectedHeaders map[string][]string
}{
{"/r/myapp/cold", `{"echoContent": "_TRX_ID_", "sleepTime": 0, "isDebug": true}`, "POST", http.StatusOK, nil},
{"/r/myapp/cold", `{"echoContent": "_TRX_ID_", "sleepTime": 5000, "isDebug": true}`, "POST", http.StatusGatewayTimeout, nil},
{"/r/myapp/hot", `{"echoContent": "_TRX_ID_", "sleepTime": 5000, "isDebug": true}`, "POST", http.StatusGatewayTimeout, nil},
{"/r/myapp/hot", `{"echoContent": "_TRX_ID_", "sleepTime": 0, "isDebug": true}`, "POST", http.StatusOK, nil},
{"/r/myapp/hot-json", `{"echoContent": "_TRX_ID_", "sleepTime": 5000, "isDebug": true}`, "POST", http.StatusGatewayTimeout, nil},
{"/r/myapp/hot-json", `{"echoContent": "_TRX_ID_", "sleepTime": 0, "isDebug": true}`, "POST", http.StatusOK, nil},
{"/r/myapp/bigmem-cold", `{"echoContent": "_TRX_ID_", "sleepTime": 0, "isDebug": true}`, "POST", http.StatusBadRequest, nil},
{"/r/myapp/bigmem-hot", `{"echoContent": "_TRX_ID_", "sleepTime": 0, "isDebug": true}`, "POST", http.StatusBadRequest, nil},
} {
trx := fmt.Sprintf("_trx_%d_", i)
body := strings.NewReader(strings.Replace(test.body, "_TRX_ID_", trx, 1))
_, rec := routerRequest(t, srv.Router, test.method, test.path, body)
respBytes, _ := ioutil.ReadAll(rec.Body)
respBody := string(respBytes)
maxBody := len(respBody)
if maxBody > 1024 {
maxBody = 1024
}
if rec.Code != test.expectedCode {
isFailure = true
t.Errorf("Test %d: Expected status code to be %d but was %d body: %#v",
i, test.expectedCode, rec.Code, respBody[:maxBody])
}
if rec.Code == http.StatusOK && !strings.Contains(respBody, trx) {
isFailure = true
t.Errorf("Test %d: Expected response to include %s but got body: %s",
i, trx, respBody[:maxBody])
}
if test.expectedHeaders != nil {
for name, header := range test.expectedHeaders {
if header[0] != rec.Header().Get(name) {
isFailure = true
t.Errorf("Test %d: Expected header `%s` to be %s but was %s body: %#v",
i, name, header[0], rec.Header().Get(name), respBody[:maxBody])
}
}
}
}
}
// Minimal test that checks the possibility of invoking concurrent hot sync functions.
func TestRouteRunnerMinimalConcurrentHotSync(t *testing.T) {
buf := setLogBuffer()
app := &models.App{ID: "app_id", Name: "myapp", Config: models.Config{}}
ds := datastore.NewMockInit(
[]*models.App{app},
[]*models.Route{
{Path: "/hot", AppID: app.ID, Image: "fnproject/fn-test-utils", Type: "sync", Format: "http", Memory: 128, Timeout: 30, IdleTimeout: 5},
},
)
fnl := logs.NewMock()
rnr, cancelrnr := testRunner(t, ds, fnl)
defer cancelrnr()
srv := testServer(ds, &mqs.Mock{}, fnl, rnr, ServerTypeFull)
for i, test := range []struct {
path string
body string
method string
expectedCode int
expectedHeaders map[string][]string
}{
{"/r/myapp/hot", `{"sleepTime": 100, "isDebug": true}`, "POST", http.StatusOK, nil},
} {
errs := make(chan error)
numCalls := 4
for k := 0; k < numCalls; k++ {
go func() {
body := strings.NewReader(test.body)
_, rec := routerRequest(t, srv.Router, test.method, test.path, body)
if rec.Code != test.expectedCode {
t.Log(buf.String())
errs <- fmt.Errorf("Test %d: Expected status code to be %d but was %d body: %#v",
i, test.expectedCode, rec.Code, rec.Body.String())
return
}
if test.expectedHeaders == nil {
errs <- nil
return
}
for name, header := range test.expectedHeaders {
if header[0] != rec.Header().Get(name) {
t.Log(buf.String())
errs <- fmt.Errorf("Test %d: Expected header `%s` to be %s but was %s body: %#v",
i, name, header[0], rec.Header().Get(name), rec.Body.String())
return
}
}
errs <- nil
}()
}
for k := 0; k < numCalls; k++ {
err := <-errs
if err != nil {
t.Errorf("%v", err)
}
}
}
}

View File

@@ -5,7 +5,6 @@ import (
"bytes"
"context"
"crypto/tls"
"encoding/base64"
"errors"
"fmt"
"io"
@@ -204,7 +203,6 @@ type Server struct {
noFnInvokeEndpoint bool
noCallEndpoints bool
appListeners *appListeners
routeListeners *routeListeners
fnListeners *fnListeners
triggerListeners *triggerListeners
rootMiddlewares []fnext.Middleware
@@ -703,13 +701,12 @@ func New(ctx context.Context, opts ...Option) *Server {
s.bindHandlers(ctx)
s.appListeners = new(appListeners)
s.routeListeners = new(routeListeners)
s.fnListeners = new(fnListeners)
s.triggerListeners = new(triggerListeners)
// TODO it's not clear that this is always correct as the read store won't get wrapping
s.datastore = datastore.Wrap(s.datastore)
s.datastore = fnext.NewDatastore(s.datastore, s.appListeners, s.routeListeners, s.fnListeners, s.triggerListeners)
s.datastore = fnext.NewDatastore(s.datastore, s.appListeners, s.fnListeners, s.triggerListeners)
s.logstore = logs.Wrap(s.logstore)
return s
@@ -1086,38 +1083,6 @@ func (s *Server) bindHandlers(ctx context.Context) {
switch s.nodeType {
case ServerTypeFull, ServerTypeAPI:
clean := engine.Group("/v1")
v1 := clean.Group("")
v1.Use(setAppNameInCtx)
v1.Use(s.apiMiddlewareWrapper())
v1.GET("/apps", s.handleV1AppList)
v1.POST("/apps", s.handleV1AppCreate)
{
apps := v1.Group("/apps/:appName")
apps.Use(appNameCheck)
{
withAppCheck := apps.Group("")
withAppCheck.Use(s.checkAppPresenceByName())
withAppCheck.GET("", s.handleV1AppGetByIdOrName)
withAppCheck.PATCH("", s.handleV1AppUpdate)
withAppCheck.DELETE("", s.handleV1AppDelete)
withAppCheck.GET("/routes", s.handleRouteList)
withAppCheck.GET("/routes/:route", s.handleRouteGetAPI)
withAppCheck.PATCH("/routes/*route", s.handleRoutesPatch)
withAppCheck.DELETE("/routes/*route", s.handleRouteDelete)
withAppCheck.GET("/calls/:call", s.handleCallGet1)
withAppCheck.GET("/calls/:call/log", s.handleCallLogGet1)
withAppCheck.GET("/calls", s.handleCallList1)
}
apps.POST("/routes", s.handleRoutesPostPut)
apps.PUT("/routes/*route", s.handleRoutesPostPut)
}
cleanv2 := engine.Group("/v2")
v2 := cleanv2.Group("")
v2.Use(s.apiMiddlewareWrapper())
@@ -1165,7 +1130,6 @@ func (s *Server) bindHandlers(ctx context.Context) {
runnerAppAPI.Use(setAppIDInCtx)
// Both of these are somewhat odd -
// Deprecate, remove with routes
runnerAppAPI.GET("/routes/*route", s.handleRunnerGetRoute)
runnerAppAPI.GET("/triggerBySource/:triggerType/*triggerSource", s.handleRunnerGetTriggerBySource)
}
}
@@ -1176,33 +1140,19 @@ func (s *Server) bindHandlers(ctx context.Context) {
lbTriggerGroup := engine.Group("/t")
lbTriggerGroup.Any("/:appName", s.handleHTTPTriggerCall)
lbTriggerGroup.Any("/:appName/*triggerSource", s.handleHTTPTriggerCall)
// TODO Deprecate with routes
lbRouteGroup := engine.Group("/r")
lbRouteGroup.Use(s.checkAppPresenceByNameAtLB())
lbRouteGroup.Any("/:appName", s.handleV1FunctionCall)
lbRouteGroup.Any("/:appName/*route", s.handleV1FunctionCall)
}
if !s.noFnInvokeEndpoint {
lbFnInvokeGroup := engine.Group("/invoke")
lbFnInvokeGroup.POST("/:fnID", s.handleFnInvokeCall)
}
}
engine.NoRoute(func(c *gin.Context) {
var err error
switch {
case s.nodeType == ServerTypeAPI && strings.HasPrefix(c.Request.URL.Path, "/r/"):
err = models.ErrInvokeNotSupported
case s.nodeType == ServerTypeRunner && strings.HasPrefix(c.Request.URL.Path, "/v1/"):
err = models.ErrAPINotSupported
default:
var e models.APIError = models.ErrPathNotFound
err = models.NewAPIError(e.Code(), fmt.Errorf("%v: %s", e.Error(), c.Request.URL.Path))
}
handleV1ErrorResponse(c, err)
var e models.APIError = models.ErrPathNotFound
err = models.NewAPIError(e.Code(), fmt.Errorf("%v: %s", e.Error(), c.Request.URL.Path))
handleErrorResponse(c, err)
})
}
@@ -1217,26 +1167,7 @@ func (s *Server) Agent() agent.Agent {
return s.agent
}
// returns the unescaped ?cursor and ?perPage values
// pageParams clamps 0 < ?perPage <= 100 and defaults to 30 if 0
// ignores parsing errors and falls back to defaults.
func pageParams(c *gin.Context, base64d bool) (cursor string, perPage int) {
cursor = c.Query("cursor")
if base64d {
cbytes, _ := base64.RawURLEncoding.DecodeString(cursor)
cursor = string(cbytes)
}
perPage, _ = strconv.Atoi(c.Query("per_page"))
if perPage > 100 {
perPage = 100
} else if perPage <= 0 {
perPage = 30
}
return cursor, perPage
}
func pageParamsV2(c *gin.Context) (cursor string, perPage int) {
func pageParams(c *gin.Context) (cursor string, perPage int) {
cursor = c.Query("cursor")
perPage, _ = strconv.Atoi(c.Query("per_page"))
@@ -1247,37 +1178,3 @@ func pageParamsV2(c *gin.Context) (cursor string, perPage int) {
}
return cursor, perPage
}
type appResponse struct {
Message string `json:"message"`
App *models.App `json:"app"`
}
//TODO deprecate with V1
type appsV1Response struct {
Message string `json:"message"`
NextCursor string `json:"next_cursor"`
Apps []*models.App `json:"apps"`
}
type routeResponse struct {
Message string `json:"message"`
Route *models.Route `json:"route"`
}
type routesResponse struct {
Message string `json:"message"`
NextCursor string `json:"next_cursor"`
Routes []*models.Route `json:"routes"`
}
type callResponse struct {
Message string `json:"message"`
Call *models.Call `json:"call"`
}
type callsResponse struct {
Message string `json:"message"`
NextCursor string `json:"next_cursor"`
Calls []*models.Call `json:"calls"`
}

View File

@@ -64,7 +64,7 @@ func limitRequestBody(max int64) func(c *gin.Context) {
if cl > max {
// try to deny this quickly, instead of just letting it get lopped off
handleV1ErrorResponse(c, errTooBig{cl, max})
handleErrorResponse(c, errTooBig{cl, max})
c.Abort()
return
}

View File

@@ -3,32 +3,20 @@ package server
import (
"bytes"
"context"
"crypto/rand"
"encoding/base64"
"encoding/json"
"errors"
"io"
"net/http"
"net/http/httptest"
"net/url"
"os"
"strconv"
"strings"
"testing"
"github.com/fnproject/fn/api/agent"
_ "github.com/fnproject/fn/api/agent/drivers/docker"
"github.com/fnproject/fn/api/datastore"
"github.com/fnproject/fn/api/datastore/sql"
_ "github.com/fnproject/fn/api/datastore/sql/sqlite"
"github.com/fnproject/fn/api/logs"
"github.com/fnproject/fn/api/models"
"github.com/fnproject/fn/api/mqs"
"github.com/gin-gonic/gin"
)
var tmpDatastoreTests = "/tmp/func_test_datastore.db"
func testServer(ds models.Datastore, mq models.MessageQueue, logDB models.LogStore, rnr agent.Agent, nodeType NodeType, opts ...Option) *Server {
return New(context.Background(), append(opts,
WithLogLevel(getEnv(EnvLogLevel, DefaultLogLevel)),
@@ -91,15 +79,6 @@ func newRouterRequest(t *testing.T, method, path string, body io.Reader) (*http.
return req, rec
}
func getV1ErrorResponse(t *testing.T, rec *httptest.ResponseRecorder) *models.ErrorWrapper {
var err models.ErrorWrapper
decodeErr := json.NewDecoder(rec.Body).Decode(&err)
if decodeErr != nil {
t.Error("Test: Expected not empty response body")
}
return &err
}
func getErrorResponse(t *testing.T, rec *httptest.ResponseRecorder) *models.Error {
var err models.Error
decodeErr := json.NewDecoder(rec.Body).Decode(&err)
@@ -108,202 +87,3 @@ func getErrorResponse(t *testing.T, rec *httptest.ResponseRecorder) *models.Erro
}
return &err
}
func prepareDB(ctx context.Context, t *testing.T) (models.Datastore, models.LogStore, func()) {
os.Remove(tmpDatastoreTests)
uri, err := url.Parse("sqlite3://" + tmpDatastoreTests)
if err != nil {
t.Fatal(err)
}
ss, err := sql.New(ctx, uri)
if err != nil {
t.Fatalf("Error when creating datastore: %s", err)
}
logDB := logs.Wrap(ss)
ds := datastore.Wrap(ss)
return ds, logDB, func() {
os.Remove(tmpDatastoreTests)
}
}
func TestFullStack(t *testing.T) {
ctx := context.Background()
buf := setLogBuffer()
ds, logDB, close := prepareDB(ctx, t)
defer close()
rnr, rnrcancel := testRunner(t, ds)
defer rnrcancel()
srv := testServer(ds, &mqs.Mock{}, logDB, rnr, ServerTypeFull, LimitRequestBody(32256))
var bigbufa [32257]byte
rand.Read(bigbufa[:])
bigbuf := base64.StdEncoding.EncodeToString(bigbufa[:]) // this will be > bigbufa, but json compatible
toobigerr := errors.New("Content-Length too large for this server")
gatewayerr := errors.New("container exit code")
for _, test := range []struct {
name string
method string
path string
body string
expectedCode int
expectedCacheSize int // TODO kill me
expectedError error
}{
{"create my app", "POST", "/v1/apps", `{ "app": { "name": "myapp" } }`, http.StatusOK, 0, nil},
{"list apps", "GET", "/v1/apps", ``, http.StatusOK, 0, nil},
{"get app", "GET", "/v1/apps/myapp", ``, http.StatusOK, 0, nil},
// NOTE: cache is lazy, loads when a request comes in for the route, not when added
{"add myroute", "POST", "/v1/apps/myapp/routes", `{ "route": { "name": "myroute", "path": "/myroute", "image": "fnproject/fn-test-utils", "type": "sync" } }`, http.StatusOK, 0, nil},
{"add myroute2", "POST", "/v1/apps/myapp/routes", `{ "route": { "name": "myroute2", "path": "/myroute2", "image": "fnproject/fn-test-utils", "type": "sync" } }`, http.StatusOK, 0, nil},
{"get myroute", "GET", "/v1/apps/myapp/routes/myroute", ``, http.StatusOK, 0, nil},
{"get myroute2", "GET", "/v1/apps/myapp/routes/myroute2", ``, http.StatusOK, 0, nil},
{"get all routes", "GET", "/v1/apps/myapp/routes", ``, http.StatusOK, 0, nil},
{"execute myroute", "POST", "/r/myapp/myroute", `{ "echoContent": "Teste" }`, http.StatusOK, 1, nil},
// fails
{"execute myroute2", "POST", "/r/myapp/myroute2", `{"sleepTime": 0, "isDebug": true, "isCrash": true}`, http.StatusBadGateway, 2, gatewayerr},
{"request body too large", "POST", "/r/myapp/myroute", bigbuf, http.StatusRequestEntityTooLarge, 0, toobigerr},
{"get myroute2", "GET", "/v1/apps/myapp/routes/myroute2", ``, http.StatusOK, 2, nil},
{"delete myroute", "DELETE", "/v1/apps/myapp/routes/myroute", ``, http.StatusOK, 1, nil},
{"delete myroute2", "DELETE", "/v1/apps/myapp/routes/myroute2", ``, http.StatusOK, 0, nil},
{"delete app (success)", "DELETE", "/v1/apps/myapp", ``, http.StatusOK, 0, nil},
{"get deleted app", "GET", "/v1/apps/myapp", ``, http.StatusNotFound, 0, models.ErrAppsNotFound},
{"get deleteds route on deleted app", "GET", "/v1/apps/myapp/routes/myroute", ``, http.StatusNotFound, 0, models.ErrAppsNotFound},
} {
t.Run(test.name, func(t *testing.T) {
_, rec := routerRequest(t, srv.Router, test.method, test.path, bytes.NewBuffer([]byte(test.body)))
if rec.Code != test.expectedCode {
t.Log(buf.String())
t.Log(rec.Body.String())
t.Errorf("Test \"%s\": Expected status code to be %d but was %d",
test.name, test.expectedCode, rec.Code)
}
if rec.Code > 300 && test.expectedError == nil {
t.Log(buf.String())
t.Error("got error when not expected error", rec.Body.String())
} else if test.expectedError != nil {
if !strings.Contains(rec.Body.String(), test.expectedError.Error()) {
t.Log(buf.String())
t.Errorf("Test %s: Expected error message to have `%s`, but got `%s`",
test.name, test.expectedError.Error(), rec.Body.String())
}
}
})
}
}
func TestRunnerNode(t *testing.T) {
ctx := context.Background()
buf := setLogBuffer()
ds, logDB, close := prepareDB(ctx, t)
defer close()
rnr, rnrcancel := testRunner(t, ds)
defer rnrcancel()
// Add route with an API server using the same DB
{
apiServer := testServer(ds, &mqs.Mock{}, logDB, nil, ServerTypeAPI)
_, rec := routerRequest(t, apiServer.Router, "POST", "/v1/apps/myapp/routes", bytes.NewBuffer([]byte(`{ "route": { "name": "myroute", "path": "/myroute", "image": "fnproject/fn-test-utils", "type": "sync" } }`)))
if rec.Code != http.StatusOK {
t.Errorf("Expected status code 200 when creating sync route, but got %d", rec.Code)
}
_, rec = routerRequest(t, apiServer.Router, "POST", "/v1/apps/myapp/routes", bytes.NewBuffer([]byte(`{ "route": { "name": "myasyncroute", "path": "/myasyncroute", "image": "fnproject/fn-test-utils", "type": "async" } }`)))
if rec.Code != http.StatusOK {
t.Errorf("Expected status code 200 when creating async route, but got %d", rec.Code)
}
}
srv := testServer(ds, &mqs.Mock{}, logDB, rnr, ServerTypeRunner)
for _, test := range []struct {
name string
method string
path string
body string
expectedCode int
expectedCacheSize int // TODO kill me
}{
// Support sync and async API calls
{"execute sync route succeeds", "POST", "/r/myapp/myroute", `{ "echoContent": "Teste" }`, http.StatusOK, 1},
{"execute async route succeeds", "POST", "/r/myapp/myasyncroute", `{ "echoContent": "Teste" }`, http.StatusAccepted, 1},
// All other API functions should not be available on runner nodes
{"create app not found", "POST", "/v1/apps", `{ "app": { "name": "myapp" } }`, http.StatusBadRequest, 0},
{"list apps not found", "GET", "/v1/apps", ``, http.StatusBadRequest, 0},
{"get app not found", "GET", "/v1/apps/myapp", ``, http.StatusBadRequest, 0},
{"add route not found", "POST", "/v1/apps/myapp/routes", `{ "route": { "name": "myroute", "path": "/myroute", "image": "fnproject/fn-test-utils", "type": "sync" } }`, http.StatusBadRequest, 0},
{"get route not found", "GET", "/v1/apps/myapp/routes/myroute", ``, http.StatusBadRequest, 0},
{"get all routes not found", "GET", "/v1/apps/myapp/routes", ``, http.StatusBadRequest, 0},
{"delete app not found", "DELETE", "/v1/apps/myapp", ``, http.StatusBadRequest, 0},
} {
t.Run(test.name, func(t *testing.T) {
_, rec := routerRequest(t, srv.Router, test.method, test.path, bytes.NewBuffer([]byte(test.body)))
if rec.Code != test.expectedCode {
t.Log(buf.String())
t.Errorf("Test \"%s\": Expected status code to be %d but was %d",
test.name, test.expectedCode, rec.Code)
}
})
}
}
func TestApiNode(t *testing.T) {
ctx := context.Background()
buf := setLogBuffer()
ds, logDB, close := prepareDB(ctx, t)
defer close()
srv := testServer(ds, &mqs.Mock{}, logDB, nil, ServerTypeAPI)
for _, test := range []struct {
name string
method string
path string
body string
expectedCode int
expectedCacheSize int // TODO kill me
}{
// All routes should be supported
{"create my app", "POST", "/v1/apps", `{ "app": { "name": "myapp" } }`, http.StatusOK, 0},
{"list apps", "GET", "/v1/apps", ``, http.StatusOK, 0},
{"get app", "GET", "/v1/apps/myapp", ``, http.StatusOK, 0},
{"add myroute", "POST", "/v1/apps/myapp/routes", `{ "route": { "name": "myroute", "path": "/myroute", "image": "fnproject/fn-test-utils", "type": "sync" } }`, http.StatusOK, 0},
{"add myroute2", "POST", "/v1/apps/myapp/routes", `{ "route": { "name": "myroute2", "path": "/myroute2", "image": "fnproject/fn-test-utils", "type": "sync" } }`, http.StatusOK, 0},
{"add myasyncroute", "POST", "/v1/apps/myapp/routes", `{ "route": { "name": "myasyncroute", "path": "/myasyncroute", "image": "fnproject/fn-test-utils", "type": "async" } }`, http.StatusOK, 0},
{"get myroute", "GET", "/v1/apps/myapp/routes/myroute", ``, http.StatusOK, 0},
{"get myroute2", "GET", "/v1/apps/myapp/routes/myroute2", ``, http.StatusOK, 0},
{"get all routes", "GET", "/v1/apps/myapp/routes", ``, http.StatusOK, 0},
// Don't support calling sync or async
{"execute myroute", "POST", "/r/myapp/myroute", `{ "echoContent": "Teste" }`, http.StatusBadRequest, 1},
{"execute myroute2", "POST", "/r/myapp/myroute2", `{ "echoContent": "Teste" }`, http.StatusBadRequest, 2},
{"execute myasyncroute", "POST", "/r/myapp/myasyncroute", `{ "echoContent": "Teste" }`, http.StatusBadRequest, 1},
{"get myroute2", "GET", "/v1/apps/myapp/routes/myroute2", ``, http.StatusOK, 2},
{"delete myroute", "DELETE", "/v1/apps/myapp/routes/myroute", ``, http.StatusOK, 1},
{"delete myroute2", "DELETE", "/v1/apps/myapp/routes/myroute2", ``, http.StatusOK, 0},
{"delete app (success)", "DELETE", "/v1/apps/myapp", ``, http.StatusOK, 0},
{"get deleted app", "GET", "/v1/apps/myapp", ``, http.StatusNotFound, 0},
{"get deleted route on deleted app", "GET", "/v1/apps/myapp/routes/myroute", ``, http.StatusNotFound, 0},
} {
_, rec := routerRequest(t, srv.Router, test.method, test.path, bytes.NewBuffer([]byte(test.body)))
if rec.Code != test.expectedCode {
t.Log(buf.String())
t.Errorf("Test \"%s\": Expected status code to be %d but was %d",
test.name, test.expectedCode, rec.Code)
}
}
}

View File

@@ -13,7 +13,7 @@ func (s *Server) handleTriggerList(c *gin.Context) {
ctx := c.Request.Context()
filter := &models.TriggerFilter{}
filter.Cursor, filter.PerPage = pageParamsV2(c)
filter.Cursor, filter.PerPage = pageParams(c)
filter.AppID = c.Query("app_id")