mirror of
https://github.com/fnproject/fn.git
synced 2022-10-28 21:29:17 +03:00
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:
committed by
Owen Cliffe
parent
6a01dae923
commit
d56a49b321
@@ -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")
|
||||
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
@@ -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"})
|
||||
}
|
||||
@@ -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})
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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")))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"})
|
||||
}
|
||||
@@ -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})
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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}}
|
||||
|
||||
@@ -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}}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user