Merge pull request #366 from fnproject/saner-route-cfg

more strict configuration of routes
This commit is contained in:
Reed Allman
2017-09-27 09:03:51 -07:00
committed by GitHub
18 changed files with 166 additions and 156 deletions

View File

@@ -32,7 +32,10 @@ jobs:
- run: docker version - run: docker version
# login here for tests # login here for tests
- run: docker login -u $DOCKER_USER -p $DOCKER_PASS - run: docker login -u $DOCKER_USER -p $DOCKER_PASS
- run: ./test.sh - run: make docker-build
- run: make install
- run: make test
# TODO these should be inside test.sh file ?
- run: ./api_test.sh mysql 4 - run: ./api_test.sh mysql 4
- run: ./api_test.sh postgres 4 - run: ./api_test.sh postgres 4
- run: ./api_test.sh sqlite 4 - run: ./api_test.sh sqlite 4

View File

@@ -10,6 +10,9 @@ dep-up:
build: build:
go build -o functions go build -o functions
install:
go install
test: test:
./test.sh ./test.sh

View File

@@ -146,6 +146,13 @@ func FromRequest(appName, path string, req *http.Request) CallOpt {
} }
} }
// this ensures that there is an image, path, timeouts, memory, etc are valid.
// NOTE: this means assign any changes above into route's fields
err = route.Validate()
if err != nil {
return err
}
c.Call = &models.Call{ c.Call = &models.Call{
ID: id, ID: id,
AppName: appName, AppName: appName,
@@ -166,14 +173,6 @@ func FromRequest(appName, path string, req *http.Request) CallOpt {
Method: req.Method, Method: req.Method,
} }
// TODO if these made it to here we have a problemo. error instead?
if c.Timeout <= 0 {
c.Timeout = models.DefaultRouteTimeout
}
if c.IdleTimeout <= 0 {
c.IdleTimeout = models.DefaultIdleTimeout
}
c.req = req c.req = req
return nil return nil
} }

View File

@@ -449,7 +449,7 @@ func Test(t *testing.T, dsf func() models.Datastore) {
updated, err := ds.UpdateRoute(ctx, &models.Route{ updated, err := ds.UpdateRoute(ctx, &models.Route{
AppName: testRoute.AppName, AppName: testRoute.AppName,
Path: testRoute.Path, Path: testRoute.Path,
Timeout: 100, Timeout: 2,
Config: map[string]string{ Config: map[string]string{
"FIRST": "1", "FIRST": "1",
"SECOND": "2", "SECOND": "2",
@@ -467,13 +467,15 @@ func Test(t *testing.T, dsf func() models.Datastore) {
} }
expected := &models.Route{ expected := &models.Route{
// unchanged // unchanged
AppName: testRoute.AppName, AppName: testRoute.AppName,
Path: testRoute.Path, Path: testRoute.Path,
Image: "fnproject/hello", Image: "fnproject/hello",
Type: "sync", Type: "sync",
Format: "http", Format: "http",
IdleTimeout: testRoute.IdleTimeout,
Memory: testRoute.Memory,
// updated // updated
Timeout: 100, Timeout: 2,
Config: map[string]string{ Config: map[string]string{
"FIRST": "1", "FIRST": "1",
"SECOND": "2", "SECOND": "2",
@@ -510,12 +512,14 @@ func Test(t *testing.T, dsf func() models.Datastore) {
} }
expected = &models.Route{ expected = &models.Route{
// unchanged // unchanged
AppName: testRoute.AppName, AppName: testRoute.AppName,
Path: testRoute.Path, Path: testRoute.Path,
Image: "fnproject/hello", Image: "fnproject/hello",
Type: "sync", Type: "sync",
Format: "http", Format: "http",
Timeout: 100, Timeout: 2,
Memory: testRoute.Memory,
IdleTimeout: testRoute.IdleTimeout,
// updated // updated
Config: map[string]string{ Config: map[string]string{
"FIRST": "first", "FIRST": "first",
@@ -682,9 +686,12 @@ var testApp = &models.App{
} }
var testRoute = &models.Route{ var testRoute = &models.Route{
AppName: testApp.Name, AppName: testApp.Name,
Path: "/test", Path: "/test",
Image: "fnproject/hello", Image: "fnproject/hello",
Type: "sync", Type: "sync",
Format: "http", Format: "http",
Timeout: models.DefaultTimeout,
IdleTimeout: models.DefaultIdleTimeout,
Memory: models.DefaultMemory,
} }

View File

@@ -144,8 +144,14 @@ func (m *mock) UpdateRoute(ctx context.Context, route *models.Route) (*models.Ro
if err != nil { if err != nil {
return nil, err return nil, err
} }
r.Update(route) clone := r.Clone()
return r.Clone(), nil clone.Update(route)
err = clone.Validate()
if err != nil {
return nil, err
}
r.Update(route) // only if validate works (pointer)
return clone, nil
} }
func (m *mock) RemoveRoute(ctx context.Context, appName, routePath string) error { func (m *mock) RemoveRoute(ctx context.Context, appName, routePath string) error {

View File

@@ -335,6 +335,10 @@ func (ds *sqlStore) UpdateRoute(ctx context.Context, newroute *models.Route) (*m
} }
route.Update(newroute) route.Update(newroute)
err = route.Validate()
if err != nil {
return err
}
query = tx.Rebind(`UPDATE routes SET query = tx.Rebind(`UPDATE routes SET
image = :image, image = :image,

View File

@@ -7,14 +7,14 @@ type App struct {
func (a *App) Validate() error { func (a *App) Validate() error {
if a.Name == "" { if a.Name == "" {
return ErrAppsValidationMissingName return ErrAppsMissingName
} }
if len(a.Name) > maxAppName { if len(a.Name) > maxAppName {
return ErrAppsValidationTooLongName return ErrAppsTooLongName
} }
for _, c := range a.Name { for _, c := range a.Name {
if (c < '0' || '9' < c) && (c < 'A' || 'Z' > c) && (c < 'a' || 'z' < c) && c != '_' && c != '-' { if (c < '0' || '9' < c) && (c < 'A' || 'Z' > c) && (c < 'a' || 'z' < c) && c != '_' && c != '-' {
return ErrAppsValidationInvalidName return ErrAppsInvalidName
} }
} }
return nil return nil

View File

@@ -20,15 +20,15 @@ var (
code: http.StatusGatewayTimeout, code: http.StatusGatewayTimeout,
error: errors.New("Timed out"), error: errors.New("Timed out"),
} }
ErrAppsValidationMissingName = err{ ErrAppsMissingName = err{
code: http.StatusBadRequest, code: http.StatusBadRequest,
error: errors.New("Missing app name"), error: errors.New("Missing app name"),
} }
ErrAppsValidationTooLongName = err{ ErrAppsTooLongName = err{
code: http.StatusBadRequest, code: http.StatusBadRequest,
error: fmt.Errorf("App name must be %v characters or less", maxAppName), error: fmt.Errorf("App name must be %v characters or less", maxAppName),
} }
ErrAppsValidationInvalidName = err{ ErrAppsInvalidName = err{
code: http.StatusBadRequest, code: http.StatusBadRequest,
error: errors.New("Invalid app name"), error: errors.New("Invalid app name"),
} }
@@ -42,7 +42,7 @@ var (
} }
ErrAppsNameImmutable = err{ ErrAppsNameImmutable = err{
code: http.StatusConflict, code: http.StatusConflict,
error: errors.New("Could not update app - name is immutable"), error: errors.New("Could not update - name is immutable"),
} }
ErrAppsNotFound = err{ ErrAppsNotFound = err{
code: http.StatusNotFound, code: http.StatusNotFound,
@@ -94,41 +94,41 @@ var (
} }
ErrRoutesPathImmutable = err{ ErrRoutesPathImmutable = err{
code: http.StatusConflict, code: http.StatusConflict,
error: errors.New("Could not update route - path is immutable"), error: errors.New("Could not update - path is immutable"),
} }
ErrFoundDynamicURL = err{ ErrFoundDynamicURL = err{
code: http.StatusBadRequest, code: http.StatusBadRequest,
error: errors.New("Dynamic URL is not allowed"), error: errors.New("Dynamic URL is not allowed"),
} }
ErrInvalidPath = err{ ErrRoutesInvalidPath = err{
code: http.StatusBadRequest, code: http.StatusBadRequest,
error: errors.New("Invalid Path format"), error: errors.New("Invalid route path format"),
} }
ErrInvalidType = err{ ErrRoutesInvalidType = err{
code: http.StatusBadRequest, code: http.StatusBadRequest,
error: errors.New("Invalid route Type"), error: errors.New("Invalid route Type"),
} }
ErrInvalidFormat = err{ ErrRoutesInvalidFormat = err{
code: http.StatusBadRequest, code: http.StatusBadRequest,
error: errors.New("Invalid route Format"), error: errors.New("Invalid route Format"),
} }
ErrMissingAppName = err{ ErrRoutesMissingAppName = err{
code: http.StatusBadRequest, code: http.StatusBadRequest,
error: errors.New("Missing route AppName"), error: errors.New("Missing route AppName"),
} }
ErrMissingImage = err{ ErrRoutesMissingImage = err{
code: http.StatusBadRequest, code: http.StatusBadRequest,
error: errors.New("Missing route Image"), error: errors.New("Missing route Image"),
} }
ErrMissingName = err{ ErrRoutesMissingName = err{
code: http.StatusBadRequest, code: http.StatusBadRequest,
error: errors.New("Missing route Name"), error: errors.New("Missing route Name"),
} }
ErrMissingPath = err{ ErrRoutesMissingPath = err{
code: http.StatusBadRequest, code: http.StatusBadRequest,
error: errors.New("Missing route Path"), error: errors.New("Missing route Path"),
} }
ErrMissingType = err{ ErrRoutesMissingType = err{
code: http.StatusBadRequest, code: http.StatusBadRequest,
error: errors.New("Missing route Type"), error: errors.New("Missing route Type"),
} }
@@ -144,13 +144,17 @@ var (
code: http.StatusBadRequest, code: http.StatusBadRequest,
error: errors.New("from_time is not an epoch time"), error: errors.New("from_time is not an epoch time"),
} }
ErrNegativeTimeout = err{ ErrRoutesInvalidTimeout = err{
code: http.StatusBadRequest, code: http.StatusBadRequest,
error: errors.New("Negative timeout"), error: fmt.Errorf("timeout value is too large or small. 0 < timeout < max. async max: %d sync max: %d", MaxAsyncTimeout, MaxSyncTimeout),
} }
ErrNegativeIdleTimeout = err{ ErrRoutesInvalidIdleTimeout = err{
code: http.StatusBadRequest, code: http.StatusBadRequest,
error: errors.New("Negative idle timeout"), error: fmt.Errorf("idle_timeout value is too large or small. 0 < timeout < %d", MaxIdleTimeout),
}
ErrRoutesInvalidMemory = err{
code: http.StatusBadRequest,
error: fmt.Errorf("memory value is invalid. 0 < memory < %d", MaxMemory),
} }
ErrCallNotFound = err{ ErrCallNotFound = err{
code: http.StatusNotFound, code: http.StatusNotFound,

View File

@@ -8,8 +8,14 @@ import (
) )
const ( const (
DefaultRouteTimeout = 30 // seconds DefaultTimeout = 30 // seconds
DefaultIdleTimeout = 30 // seconds DefaultIdleTimeout = 30 // seconds
DefaultMemory = 128 // MB
MaxSyncTimeout = 120 // 2 minutes
MaxAsyncTimeout = 3600 // 1 hour
MaxIdleTimeout = MaxAsyncTimeout
MaxMemory = 1024 * 8 // 8GB TODO should probably be a var of machine max?
) )
type Routes []*Route type Routes []*Route
@@ -30,7 +36,7 @@ type Route struct {
// SetDefaults sets zeroed field to defaults. // SetDefaults sets zeroed field to defaults.
func (r *Route) SetDefaults() { func (r *Route) SetDefaults() {
if r.Memory == 0 { if r.Memory == 0 {
r.Memory = 128 r.Memory = DefaultMemory
} }
if r.Type == TypeNone { if r.Type == TypeNone {
@@ -50,7 +56,7 @@ func (r *Route) SetDefaults() {
} }
if r.Timeout == 0 { if r.Timeout == 0 {
r.Timeout = DefaultRouteTimeout r.Timeout = DefaultTimeout
} }
if r.IdleTimeout == 0 { if r.IdleTimeout == 0 {
@@ -58,24 +64,15 @@ func (r *Route) SetDefaults() {
} }
} }
// Validate validates field values, skipping zeroed fields if skipZero is true. // Validate validates all field values, returning the first error, if any.
// it returns the first error, if any. func (r *Route) Validate() error {
func (r *Route) Validate(skipZero bool) error { if r.AppName == "" {
if !skipZero { return ErrRoutesMissingAppName
if r.AppName == "" {
return ErrMissingAppName
}
if r.Path == "" {
return ErrMissingPath
}
if r.Image == "" {
return ErrMissingImage
}
} }
if !skipZero || r.Path != "" { if r.Path == "" {
return ErrRoutesMissingPath
} else {
u, err := url.Parse(r.Path) u, err := url.Parse(r.Path)
if err != nil { if err != nil {
return ErrPathMalformed return ErrPathMalformed
@@ -86,28 +83,34 @@ func (r *Route) Validate(skipZero bool) error {
} }
if !path.IsAbs(u.Path) { if !path.IsAbs(u.Path) {
return ErrInvalidPath return ErrRoutesInvalidPath
} }
} }
if !skipZero || r.Type != "" { if r.Image == "" {
if r.Type != TypeAsync && r.Type != TypeSync { return ErrRoutesMissingImage
return ErrInvalidType
}
} }
if !skipZero || r.Format != "" { if r.Type != TypeAsync && r.Type != TypeSync {
if r.Format != FormatDefault && r.Format != FormatHTTP { return ErrRoutesInvalidType
return ErrInvalidFormat
}
} }
if r.Timeout < 0 { if r.Format != FormatDefault && r.Format != FormatHTTP {
return ErrNegativeTimeout return ErrRoutesInvalidFormat
} }
if r.IdleTimeout < 0 { if r.Timeout <= 0 ||
return ErrNegativeIdleTimeout (r.Type == TypeSync && r.Timeout > MaxSyncTimeout) ||
(r.Type == TypeAsync && r.Timeout > MaxAsyncTimeout) {
return ErrRoutesInvalidTimeout
}
if r.IdleTimeout <= 0 || r.IdleTimeout > MaxIdleTimeout {
return ErrRoutesInvalidIdleTimeout
}
if r.Memory < 1 || r.Memory > MaxMemory {
return ErrRoutesInvalidMemory
} }
return nil return nil

View File

@@ -4,15 +4,9 @@ type RouteWrapper struct {
Route *Route `json:"route"` Route *Route `json:"route"`
} }
func (m *RouteWrapper) Validate(skipZero bool) error { return m.validateRoute(skipZero) } func (m *RouteWrapper) Validate() error {
func (m *RouteWrapper) validateRoute(skipZero bool) error {
if m.Route != nil { if m.Route != nil {
if err := m.Route.Validate(skipZero); err != nil { return m.Route.Validate()
return err
}
} }
return nil return nil
} }

View File

@@ -41,10 +41,10 @@ func TestAppCreate(t *testing.T) {
{datastore.NewMock(), logs.NewMock(), "/v1/apps", ``, http.StatusBadRequest, models.ErrInvalidJSON}, {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", `{}`, http.StatusBadRequest, models.ErrAppsMissingNew},
{datastore.NewMock(), logs.NewMock(), "/v1/apps", `{ "name": "Test" }`, 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.ErrAppsValidationMissingName}, {datastore.NewMock(), logs.NewMock(), "/v1/apps", `{ "app": { "name": "" } }`, http.StatusBadRequest, models.ErrAppsMissingName},
{datastore.NewMock(), logs.NewMock(), "/v1/apps", `{ "app": { "name": "1234567890123456789012345678901" } }`, http.StatusBadRequest, models.ErrAppsValidationTooLongName}, {datastore.NewMock(), logs.NewMock(), "/v1/apps", `{ "app": { "name": "1234567890123456789012345678901" } }`, http.StatusBadRequest, models.ErrAppsTooLongName},
{datastore.NewMock(), logs.NewMock(), "/v1/apps", `{ "app": { "name": "&&%@!#$#@$" } }`, http.StatusBadRequest, models.ErrAppsValidationInvalidName}, {datastore.NewMock(), logs.NewMock(), "/v1/apps", `{ "app": { "name": "&&%@!#$#@$" } }`, http.StatusBadRequest, models.ErrAppsInvalidName},
{datastore.NewMock(), logs.NewMock(), "/v1/apps", `{ "app": { "name": "&&%@!#$#@$" } }`, http.StatusBadRequest, models.ErrAppsValidationInvalidName}, {datastore.NewMock(), logs.NewMock(), "/v1/apps", `{ "app": { "name": "&&%@!#$#@$" } }`, http.StatusBadRequest, models.ErrAppsInvalidName},
// success // success
{datastore.NewMock(), logs.NewMock(), "/v1/apps", `{ "app": { "name": "teste" } }`, http.StatusOK, nil}, {datastore.NewMock(), logs.NewMock(), "/v1/apps", `{ "app": { "name": "teste" } }`, http.StatusOK, nil},

View File

@@ -57,7 +57,7 @@ func TestCallGet(t *testing.T) {
expectedCode int expectedCode int
expectedError error expectedError error
}{ }{
{"/v1/apps//calls/" + call.ID, "", http.StatusBadRequest, models.ErrMissingAppName}, {"/v1/apps//calls/" + call.ID, "", http.StatusBadRequest, models.ErrAppsMissingName},
{"/v1/apps/nodawg/calls/" + call.ID, "", http.StatusNotFound, models.ErrCallNotFound}, // TODO a little weird {"/v1/apps/nodawg/calls/" + call.ID, "", http.StatusNotFound, models.ErrCallNotFound}, // TODO a little weird
{"/v1/apps/myapp/calls/" + call.ID[:3], "", http.StatusNotFound, models.ErrCallNotFound}, {"/v1/apps/myapp/calls/" + call.ID[:3], "", http.StatusNotFound, models.ErrCallNotFound},
{"/v1/apps/myapp/calls/" + call.ID, "", http.StatusOK, nil}, {"/v1/apps/myapp/calls/" + call.ID, "", http.StatusOK, nil},
@@ -140,7 +140,7 @@ func TestCallList(t *testing.T) {
expectedLen int expectedLen int
nextCursor string nextCursor string
}{ }{
{"/v1/apps//calls", "", http.StatusBadRequest, models.ErrMissingAppName, 0, ""}, {"/v1/apps//calls", "", http.StatusBadRequest, models.ErrAppsMissingName, 0, ""},
{"/v1/apps/nodawg/calls", "", http.StatusNotFound, models.ErrAppsNotFound, 0, ""}, {"/v1/apps/nodawg/calls", "", http.StatusNotFound, models.ErrAppsNotFound, 0, ""},
{"/v1/apps/myapp/calls", "", http.StatusOK, nil, 3, ""}, {"/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", "", http.StatusOK, nil, 1, c3.ID},

View File

@@ -49,7 +49,8 @@ func (s *Server) handleRoutesPostPutPatch(c *gin.Context) {
} }
func (s *Server) submitRoute(ctx context.Context, wroute *models.RouteWrapper) error { func (s *Server) submitRoute(ctx context.Context, wroute *models.RouteWrapper) error {
err := s.setDefaultsAndValidate(wroute) wroute.Route.SetDefaults()
err := wroute.Route.Validate()
if err != nil { if err != nil {
return err return err
} }
@@ -62,10 +63,6 @@ func (s *Server) submitRoute(ctx context.Context, wroute *models.RouteWrapper) e
} }
func (s *Server) changeRoute(ctx context.Context, wroute *models.RouteWrapper) error { func (s *Server) changeRoute(ctx context.Context, wroute *models.RouteWrapper) error {
err := wroute.Validate(true)
if err != nil {
return err
}
r, err := s.Datastore.UpdateRoute(ctx, wroute.Route) r, err := s.Datastore.UpdateRoute(ctx, wroute.Route)
if err != nil { if err != nil {
return err return err
@@ -163,13 +160,8 @@ func bindRoute(c *gin.Context, method string, wroute *models.RouteWrapper) error
} }
if method == http.MethodPost { if method == http.MethodPost {
if wroute.Route.Path == "" { if wroute.Route.Path == "" {
return models.ErrMissingPath return models.ErrRoutesMissingPath
} }
} }
return nil return nil
} }
func (s *Server) setDefaultsAndValidate(wroute *models.RouteWrapper) error {
wroute.Route.SetDefaults()
return wroute.Validate(false)
}

View File

@@ -61,11 +61,11 @@ func TestRouteCreate(t *testing.T) {
{datastore.NewMock(), logs.NewMock(), http.MethodPost, "/v1/apps/a/routes", ``, http.StatusBadRequest, models.ErrInvalidJSON}, {datastore.NewMock(), logs.NewMock(), http.MethodPost, "/v1/apps/a/routes", ``, http.StatusBadRequest, models.ErrInvalidJSON},
{datastore.NewMock(), logs.NewMock(), http.MethodPost, "/v1/apps/a/routes", `{ "type": "sync" }`, http.StatusBadRequest, models.ErrRoutesMissingNew}, {datastore.NewMock(), logs.NewMock(), http.MethodPost, "/v1/apps/a/routes", `{ "type": "sync" }`, http.StatusBadRequest, models.ErrRoutesMissingNew},
{datastore.NewMock(), logs.NewMock(), http.MethodPost, "/v1/apps/a/routes", `{ "path": "/myroute", "type": "sync" }`, http.StatusBadRequest, models.ErrRoutesMissingNew}, {datastore.NewMock(), logs.NewMock(), http.MethodPost, "/v1/apps/a/routes", `{ "path": "/myroute", "type": "sync" }`, http.StatusBadRequest, models.ErrRoutesMissingNew},
{datastore.NewMock(), logs.NewMock(), http.MethodPost, "/v1/apps/a/routes", `{ "route": { } }`, http.StatusBadRequest, models.ErrMissingPath}, {datastore.NewMock(), logs.NewMock(), http.MethodPost, "/v1/apps/a/routes", `{ "route": { } }`, http.StatusBadRequest, models.ErrRoutesMissingPath},
{datastore.NewMock(), logs.NewMock(), http.MethodPost, "/v1/apps/a/routes", `{ "route": { "path": "/myroute", "type": "sync" } }`, http.StatusBadRequest, models.ErrMissingImage}, {datastore.NewMock(), logs.NewMock(), http.MethodPost, "/v1/apps/a/routes", `{ "route": { "path": "/myroute", "type": "sync" } }`, http.StatusBadRequest, models.ErrRoutesMissingImage},
{datastore.NewMock(), logs.NewMock(), http.MethodPost, "/v1/apps/a/routes", `{ "route": { "image": "fnproject/hello", "type": "sync" } }`, http.StatusBadRequest, models.ErrMissingPath}, {datastore.NewMock(), logs.NewMock(), http.MethodPost, "/v1/apps/a/routes", `{ "route": { "image": "fnproject/hello", "type": "sync" } }`, http.StatusBadRequest, models.ErrRoutesMissingPath},
{datastore.NewMock(), logs.NewMock(), http.MethodPost, "/v1/apps/a/routes", `{ "route": { "image": "fnproject/hello", "path": "myroute", "type": "sync" } }`, http.StatusBadRequest, models.ErrInvalidPath}, {datastore.NewMock(), logs.NewMock(), http.MethodPost, "/v1/apps/a/routes", `{ "route": { "image": "fnproject/hello", "path": "myroute", "type": "sync" } }`, http.StatusBadRequest, models.ErrRoutesInvalidPath},
{datastore.NewMock(), logs.NewMock(), http.MethodPost, "/v1/apps/$/routes", `{ "route": { "image": "fnproject/hello", "path": "/myroute", "type": "sync" } }`, http.StatusBadRequest, models.ErrAppsValidationInvalidName}, {datastore.NewMock(), logs.NewMock(), http.MethodPost, "/v1/apps/$/routes", `{ "route": { "image": "fnproject/hello", "path": "/myroute", "type": "sync" } }`, http.StatusBadRequest, models.ErrAppsInvalidName},
{datastore.NewMockInit(nil, {datastore.NewMockInit(nil,
[]*models.Route{ []*models.Route{
{ {
@@ -89,13 +89,13 @@ func TestRoutePut(t *testing.T) {
// errors (NOTE: this route doesn't exist yet) // errors (NOTE: this route doesn't exist yet)
{datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ }`, http.StatusBadRequest, models.ErrRoutesMissingNew}, {datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ }`, http.StatusBadRequest, models.ErrRoutesMissingNew},
{datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "path": "/myroute", "type": "sync" }`, http.StatusBadRequest, models.ErrRoutesMissingNew}, {datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "path": "/myroute", "type": "sync" }`, http.StatusBadRequest, models.ErrRoutesMissingNew},
{datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "route": { "type": "sync" } }`, http.StatusBadRequest, models.ErrMissingImage}, {datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "route": { "type": "sync" } }`, http.StatusBadRequest, models.ErrRoutesMissingImage},
{datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "route": { "path": "/myroute", "type": "sync" } }`, http.StatusBadRequest, models.ErrMissingImage}, {datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "route": { "path": "/myroute", "type": "sync" } }`, http.StatusBadRequest, models.ErrRoutesMissingImage},
{datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "route": { "image": "fnproject/hello", "path": "myroute", "type": "sync" } }`, http.StatusConflict, models.ErrRoutesPathImmutable}, {datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "route": { "image": "fnproject/hello", "path": "myroute", "type": "sync" } }`, http.StatusConflict, models.ErrRoutesPathImmutable},
{datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "route": { "image": "fnproject/hello", "path": "diffRoute", "type": "sync" } }`, http.StatusConflict, models.ErrRoutesPathImmutable}, {datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "route": { "image": "fnproject/hello", "path": "diffRoute", "type": "sync" } }`, http.StatusConflict, models.ErrRoutesPathImmutable},
{datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/$/routes/myroute", `{ "route": { "image": "fnproject/hello", "path": "/myroute", "type": "sync" } }`, http.StatusBadRequest, models.ErrAppsValidationInvalidName}, {datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/$/routes/myroute", `{ "route": { "image": "fnproject/hello", "path": "/myroute", "type": "sync" } }`, http.StatusBadRequest, models.ErrAppsInvalidName},
{datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "route": { "image": "fnproject/hello", "path": "/myroute", "type": "invalid-type" } }`, http.StatusBadRequest, models.ErrInvalidType}, {datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "route": { "image": "fnproject/hello", "path": "/myroute", "type": "invalid-type" } }`, http.StatusBadRequest, models.ErrRoutesInvalidType},
{datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "route": { "image": "fnproject/hello", "path": "/myroute", "format": "invalid-format", "type": "sync" } }`, http.StatusBadRequest, models.ErrInvalidFormat}, {datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "route": { "image": "fnproject/hello", "path": "/myroute", "format": "invalid-format", "type": "sync" } }`, http.StatusBadRequest, models.ErrRoutesInvalidFormat},
// success // success
{datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "route": { "image": "fnproject/hello", "path": "/myroute", "type": "sync" } }`, http.StatusOK, nil}, {datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "route": { "image": "fnproject/hello", "path": "/myroute", "type": "sync" } }`, http.StatusOK, nil},
@@ -189,7 +189,7 @@ func TestRouteList(t *testing.T) {
expectedLen int expectedLen int
nextCursor string nextCursor string
}{ }{
{"/v1/apps//routes", "", http.StatusBadRequest, models.ErrMissingAppName, 0, ""}, {"/v1/apps//routes", "", http.StatusBadRequest, models.ErrAppsMissingName, 0, ""},
{"/v1/apps/a/routes", "", http.StatusNotFound, models.ErrAppsNotFound, 0, ""}, {"/v1/apps/a/routes", "", http.StatusNotFound, models.ErrAppsNotFound, 0, ""},
{"/v1/apps/myapp/routes", "", http.StatusOK, nil, 3, ""}, {"/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", "", http.StatusOK, nil, 1, r1b},
@@ -274,33 +274,29 @@ func TestRouteGet(t *testing.T) {
func TestRouteUpdate(t *testing.T) { func TestRouteUpdate(t *testing.T) {
buf := setLogBuffer() buf := setLogBuffer()
ds := datastore.NewMockInit(nil, nil, nil)
for i, test := range []routeTestCase{ for i, test := range []routeTestCase{
// errors
{datastore.NewMock(), logs.NewMock(), http.MethodPatch, "/v1/apps/a/routes/myroute/do", ``, http.StatusBadRequest, models.ErrInvalidJSON},
{datastore.NewMock(), logs.NewMock(), http.MethodPatch, "/v1/apps/a/routes/myroute/do", `{}`, http.StatusBadRequest, models.ErrRoutesMissingNew},
{datastore.NewMock(), logs.NewMock(), http.MethodPatch, "/v1/apps/a/routes/myroute/do", `{ "route": { "type": "invalid-type" } }`, http.StatusBadRequest, models.ErrInvalidType},
{datastore.NewMock(), logs.NewMock(), http.MethodPatch, "/v1/apps/a/routes/myroute/do", `{ "route": { "format": "invalid-format" } }`, http.StatusBadRequest, models.ErrInvalidFormat},
// success // success
{datastore.NewMockInit(nil, {ds, logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute/do", `{ "route": { "image": "fnproject/yodawg" } }`, http.StatusOK, nil},
[]*models.Route{ {ds, logs.NewMock(), http.MethodPatch, "/v1/apps/a/routes/myroute/do", `{ "route": { "image": "fnproject/hello" } }`, http.StatusOK, nil},
{
AppName: "a", // errors (after success, so route exists)
Path: "/myroute/do", {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},
}, nil, {ds, logs.NewMock(), http.MethodPatch, "/v1/apps/a/routes/myroute/do", `{ "route": { "type": "invalid-type" } }`, http.StatusBadRequest, models.ErrRoutesInvalidType},
), logs.NewMock(), http.MethodPatch, "/v1/apps/a/routes/myroute/do", `{ "route": { "image": "fnproject/hello" } }`, http.StatusOK, nil}, {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},
// 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 // Addresses #381
{datastore.NewMockInit(nil, {ds, logs.NewMock(), http.MethodPatch, "/v1/apps/a/routes/myroute/do", `{ "route": { "path": "/otherpath" } }`, http.StatusConflict, models.ErrRoutesPathImmutable},
[]*models.Route{
{
AppName: "a",
Path: "/myroute/do",
},
}, nil,
), logs.NewMock(), http.MethodPatch, "/v1/apps/a/routes/myroute/do", `{ "route": { "path": "/otherpath" } }`, http.StatusConflict, models.ErrRoutesPathImmutable},
} { } {
test.run(t, i, buf) test.run(t, i, buf)
} }

View File

@@ -39,9 +39,9 @@ func TestRouteRunnerAsyncExecution(t *testing.T) {
{Name: "myapp", Config: map[string]string{"app": "true"}}, {Name: "myapp", Config: map[string]string{"app": "true"}},
}, },
[]*models.Route{ []*models.Route{
{Type: "async", Path: "/myroute", AppName: "myapp", Image: "fnproject/hello", Config: map[string]string{"test": "true"}}, {Type: "async", Path: "/myroute", AppName: "myapp", Image: "fnproject/hello", Config: map[string]string{"test": "true"}, Memory: 128, Timeout: 30, IdleTimeout: 30},
{Type: "async", Path: "/myerror", AppName: "myapp", Image: "fnproject/error", Config: map[string]string{"test": "true"}}, {Type: "async", Path: "/myerror", AppName: "myapp", Image: "fnproject/error", Config: map[string]string{"test": "true"}, Memory: 128, Timeout: 30, IdleTimeout: 30},
{Type: "async", Path: "/myroute/:param", AppName: "myapp", Image: "fnproject/hello", Config: map[string]string{"test": "true"}}, {Type: "async", Path: "/myroute/:param", AppName: "myapp", Image: "fnproject/hello", Config: map[string]string{"test": "true"}, Memory: 128, Timeout: 30, IdleTimeout: 30},
}, nil, }, nil,
) )
mq := &mqs.Mock{} mq := &mqs.Mock{}

View File

@@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"context" "context"
"errors" "errors"
"io/ioutil"
"net/http" "net/http"
"strings" "strings"
"testing" "testing"
@@ -128,11 +129,11 @@ func TestRouteRunnerExecution(t *testing.T) {
{Name: "myapp", Config: models.Config{}}, {Name: "myapp", Config: models.Config{}},
}, },
[]*models.Route{ []*models.Route{
{Path: "/", AppName: "myapp", Image: "fnproject/hello", Headers: map[string][]string{"X-Function": {"Test"}}}, {Path: "/", AppName: "myapp", Image: "fnproject/hello", Type: "sync", Memory: 128, Timeout: 30, IdleTimeout: 30, Headers: map[string][]string{"X-Function": {"Test"}}},
{Path: "/myroute", AppName: "myapp", Image: "fnproject/hello", Headers: map[string][]string{"X-Function": {"Test"}}}, {Path: "/myroute", AppName: "myapp", Image: "fnproject/hello", Type: "sync", Memory: 128, Timeout: 30, IdleTimeout: 30, Headers: map[string][]string{"X-Function": {"Test"}}},
{Path: "/myerror", AppName: "myapp", Image: "fnproject/error", Headers: map[string][]string{"X-Function": {"Test"}}}, {Path: "/myerror", AppName: "myapp", Image: "fnproject/error", Type: "sync", Memory: 128, Timeout: 30, IdleTimeout: 30, Headers: map[string][]string{"X-Function": {"Test"}}},
{Path: "/mydne", AppName: "myapp", Image: "fnproject/imagethatdoesnotexist"}, {Path: "/mydne", AppName: "myapp", Image: "fnproject/imagethatdoesnotexist", Type: "sync", Memory: 128, Timeout: 30, IdleTimeout: 30},
{Path: "/mydnehot", AppName: "myapp", Image: "fnproject/imagethatdoesnotexist", Format: "http"}, {Path: "/mydnehot", AppName: "myapp", Image: "fnproject/imagethatdoesnotexist", Type: "sync", Format: "http", Memory: 128, Timeout: 30, IdleTimeout: 30},
}, nil, }, nil,
) )
@@ -165,8 +166,9 @@ func TestRouteRunnerExecution(t *testing.T) {
if rec.Code != test.expectedCode { if rec.Code != test.expectedCode {
t.Log(buf.String()) t.Log(buf.String())
t.Errorf("Test %d: Expected status code to be %d but was %d", bod, _ := ioutil.ReadAll(rec.Body)
i, test.expectedCode, rec.Code) t.Errorf("Test %d: Expected status code to be %d but was %d. body: %s",
i, test.expectedCode, rec.Code, string(bod))
} }
if test.expectedHeaders == nil { if test.expectedHeaders == nil {
@@ -200,7 +202,7 @@ func TestFailedEnqueue(t *testing.T) {
{Name: "myapp", Config: models.Config{}}, {Name: "myapp", Config: models.Config{}},
}, },
[]*models.Route{ []*models.Route{
{Path: "/dummy", AppName: "myapp", Image: "dummy/dummy", Type: "async"}, {Path: "/dummy", AppName: "myapp", Image: "dummy/dummy", Type: "async", Memory: 128, Timeout: 30, IdleTimeout: 30},
}, nil, }, nil,
) )
err := errors.New("Unable to push task to queue") err := errors.New("Unable to push task to queue")

View File

@@ -215,7 +215,7 @@ func loggerWrap(c *gin.Context) {
func appWrap(c *gin.Context) { func appWrap(c *gin.Context) {
appName := c.GetString(api.AppName) appName := c.GetString(api.AppName)
if appName == "" { if appName == "" {
handleErrorResponse(c, models.ErrMissingAppName) handleErrorResponse(c, models.ErrAppsMissingName)
c.Abort() c.Abort()
return return
} }

View File

@@ -2,9 +2,6 @@
set -ex set -ex
make build
make docker-build
docker rm -fv func-postgres-test || echo No prev test db container docker rm -fv func-postgres-test || echo No prev test db container
docker run --name func-postgres-test -p 15432:5432 -d postgres docker run --name func-postgres-test -p 15432:5432 -d postgres
docker rm -fv func-mysql-test || echo No prev mysql test db container docker rm -fv func-mysql-test || echo No prev mysql test db container