Add support for Function and Trigger domain objects (#1060)

Vast commit, includes:

 * Introduces the Trigger domain entity.
 * Introduces the Fns domain entity.
 * V2 of the API for interacting with the new entities in swaggerv2.yml
 * Adds v2 end points for Apps to support PUT updates.
 * Rewrites the datastore level tests into a new pattern.
 * V2 routes use entity ID over name as the path parameter.
This commit is contained in:
Tom Coupland
2018-06-25 15:37:06 +01:00
committed by GitHub
parent a5abecaafb
commit 3ebff051a4
76 changed files with 5820 additions and 892 deletions

View File

@@ -82,8 +82,7 @@ func TestCallConfigurationRequest(t *testing.T) {
cfg := models.Config{"APP_VAR": "FOO"} cfg := models.Config{"APP_VAR": "FOO"}
rCfg := models.Config{"ROUTE_VAR": "BAR"} rCfg := models.Config{"ROUTE_VAR": "BAR"}
app := &models.App{Name: appName, Config: cfg} app := &models.App{ID: "app_id", Name: appName, Config: cfg}
app.SetDefaults()
ds := datastore.NewMockInit( ds := datastore.NewMockInit(
[]*models.App{app}, []*models.App{app},
[]*models.Route{ []*models.Route{
@@ -203,7 +202,7 @@ func TestCallConfigurationRequest(t *testing.T) {
func TestCallConfigurationModel(t *testing.T) { func TestCallConfigurationModel(t *testing.T) {
app := &models.App{Name: "myapp"} app := &models.App{Name: "myapp"}
app.SetDefaults()
path := "/" path := "/"
image := "fnproject/fn-test-utils" image := "fnproject/fn-test-utils"
const timeout = 1 const timeout = 1
@@ -265,8 +264,8 @@ func TestCallConfigurationModel(t *testing.T) {
} }
func TestAsyncCallHeaders(t *testing.T) { func TestAsyncCallHeaders(t *testing.T) {
app := &models.App{Name: "myapp"} app := &models.App{ID: "app_id", Name: "myapp"}
app.SetDefaults()
path := "/" path := "/"
image := "fnproject/fn-test-utils" image := "fnproject/fn-test-utils"
const timeout = 1 const timeout = 1
@@ -415,8 +414,7 @@ func (l testListener) BeforeCall(context.Context, *models.Call) error {
} }
func TestReqTooLarge(t *testing.T) { func TestReqTooLarge(t *testing.T) {
app := &models.App{Name: "myapp"} app := &models.App{ID: "app_id", Name: "myapp"}
app.SetDefaults()
cm := &models.Call{ cm := &models.Call{
AppID: app.ID, AppID: app.ID,
@@ -455,7 +453,7 @@ func TestReqTooLarge(t *testing.T) {
} }
func TestSubmitError(t *testing.T) { func TestSubmitError(t *testing.T) {
app := &models.App{Name: "myapp"} app := &models.App{Name: "myapp"}
app.SetDefaults()
path := "/" path := "/"
image := "fnproject/fn-test-utils" image := "fnproject/fn-test-utils"
const timeout = 10 const timeout = 10
@@ -547,8 +545,8 @@ func TestHTTPWithoutContentLengthWorks(t *testing.T) {
path := "/hello" path := "/hello"
url := "http://127.0.0.1:8080/r/" + appName + path url := "http://127.0.0.1:8080/r/" + appName + path
app := &models.App{Name: appName} app := &models.App{ID: "app_id", Name: appName}
app.SetDefaults()
// we need to load in app & route so that FromRequest works // we need to load in app & route so that FromRequest works
ds := datastore.NewMockInit( ds := datastore.NewMockInit(
[]*models.App{app}, []*models.App{app},
@@ -648,8 +646,8 @@ func TestTmpFsRW(t *testing.T) {
path := "/hello" path := "/hello"
url := "http://127.0.0.1:8080/r/" + appName + path url := "http://127.0.0.1:8080/r/" + appName + path
app := &models.App{Name: appName} app := &models.App{ID: "app_id", Name: appName}
app.SetDefaults()
// we need to load in app & route so that FromRequest works // we need to load in app & route so that FromRequest works
ds := datastore.NewMockInit( ds := datastore.NewMockInit(
[]*models.App{app}, []*models.App{app},
@@ -745,8 +743,8 @@ func TestTmpFsSize(t *testing.T) {
path := "/hello" path := "/hello"
url := "http://127.0.0.1:8080/r/" + appName + path url := "http://127.0.0.1:8080/r/" + appName + path
app := &models.App{Name: appName} app := &models.App{ID: "app_id", Name: appName}
app.SetDefaults()
// we need to load in app & route so that FromRequest works // we need to load in app & route so that FromRequest works
ds := datastore.NewMockInit( ds := datastore.NewMockInit(
[]*models.App{app}, []*models.App{app},
@@ -850,8 +848,8 @@ func testCall() *models.Call {
appName := "myapp" appName := "myapp"
path := "/" path := "/"
image := "fnproject/fn-test-utils:latest" image := "fnproject/fn-test-utils:latest"
app := &models.App{Name: appName} app := &models.App{ID: "app_id", Name: appName}
app.SetDefaults()
const timeout = 10 const timeout = 10
const idleTimeout = 20 const idleTimeout = 20
const memory = 256 const memory = 256
@@ -1074,7 +1072,7 @@ func TestPipesDontMakeSpuriousCalls(t *testing.T) {
call.IdleTimeout = 60 // keep this bad boy alive call.IdleTimeout = 60 // keep this bad boy alive
call.Timeout = 4 // short call.Timeout = 4 // short
app := &models.App{Name: "myapp"} app := &models.App{Name: "myapp"}
app.SetDefaults()
app.ID = call.AppID app.ID = call.AppID
// we need to load in app & route so that FromRequest works // we need to load in app & route so that FromRequest works
ds := datastore.NewMockInit( ds := datastore.NewMockInit(
@@ -1171,8 +1169,8 @@ func TestNBIOResourceTracker(t *testing.T) {
call.IdleTimeout = 60 call.IdleTimeout = 60
call.Timeout = 30 call.Timeout = 30
call.Memory = 50 call.Memory = 50
app := &models.App{Name: "myapp"} app := &models.App{ID: "app_id", Name: "myapp"}
app.SetDefaults()
app.ID = call.AppID app.ID = call.AppID
// we need to load in app & route so that FromRequest works // we need to load in app & route so that FromRequest works
ds := datastore.NewMockInit( ds := datastore.NewMockInit(

View File

@@ -1,12 +1,16 @@
package api package api
// Request context key names
const ( const (
App string = "app_name" // Gin Request context key names
AppID string = "app_id" AppName string = "app_name"
Path string = "path" AppID string = "app_id"
Call string = "call" Path string = "path"
// Short forms for API URLs
CApp string = "app" // Gin URL template parameters
CRoute string = "route" ParamAppID string = "appId"
ParamAppName string = "appName"
ParamRouteName string = "route"
ParamTriggerID string = "triggerId"
ParamCallID string = "call"
ParamFnID string = "fnId"
) )

View File

@@ -5,6 +5,7 @@ import (
"net/url" "net/url"
"fmt" "fmt"
"github.com/fnproject/fn/api/common" "github.com/fnproject/fn/api/common"
"github.com/fnproject/fn/api/datastore/internal/datastoreutil" "github.com/fnproject/fn/api/datastore/internal/datastoreutil"
"github.com/fnproject/fn/api/models" "github.com/fnproject/fn/api/models"

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,6 @@ import (
"go.opencensus.io/trace" "go.opencensus.io/trace"
"github.com/fnproject/fn/api/models" "github.com/fnproject/fn/api/models"
"github.com/jmoiron/sqlx"
) )
func MetricDS(ds models.Datastore) models.Datastore { func MetricDS(ds models.Datastore) models.Datastore {
@@ -83,8 +82,66 @@ func (m *metricds) RemoveRoute(ctx context.Context, appID string, routePath stri
return m.ds.RemoveRoute(ctx, appID, routePath) return m.ds.RemoveRoute(ctx, appID, routePath)
} }
// instant & no context ;) func (m *metricds) InsertTrigger(ctx context.Context, trigger *models.Trigger) (*models.Trigger, error) {
func (m *metricds) GetDatabase() *sqlx.DB { return m.ds.GetDatabase() } ctx, span := trace.StartSpan(ctx, "ds_insert_trigger")
defer span.End()
return m.ds.InsertTrigger(ctx, trigger)
}
func (m *metricds) UpdateTrigger(ctx context.Context, trigger *models.Trigger) (*models.Trigger, error) {
ctx, span := trace.StartSpan(ctx, "ds_update_trigger")
defer span.End()
return m.ds.UpdateTrigger(ctx, trigger)
}
func (m *metricds) RemoveTrigger(ctx context.Context, triggerID string) error {
ctx, span := trace.StartSpan(ctx, "ds_remove_trigger")
defer span.End()
return m.ds.RemoveTrigger(ctx, triggerID)
}
func (m *metricds) GetTriggerByID(ctx context.Context, triggerID string) (*models.Trigger, error) {
ctx, span := trace.StartSpan(ctx, "ds_get_trigger_by_id")
defer span.End()
return m.ds.GetTriggerByID(ctx, triggerID)
}
func (m *metricds) GetTriggers(ctx context.Context, filter *models.TriggerFilter) ([]*models.Trigger, error) {
ctx, span := trace.StartSpan(ctx, "ds_get_triggers")
defer span.End()
return m.ds.GetTriggers(ctx, filter)
}
func (m *metricds) InsertFn(ctx context.Context, fn *models.Fn) (*models.Fn, error) {
ctx, span := trace.StartSpan(ctx, "ds_insert_func")
defer span.End()
return m.ds.InsertFn(ctx, fn)
}
func (m *metricds) UpdateFn(ctx context.Context, fn *models.Fn) (*models.Fn, error) {
ctx, span := trace.StartSpan(ctx, "ds_insert_func")
defer span.End()
return m.ds.UpdateFn(ctx, fn)
}
func (m *metricds) GetFns(ctx context.Context, filter *models.FnFilter) ([]*models.Fn, error) {
ctx, span := trace.StartSpan(ctx, "ds_get_funcs")
defer span.End()
return m.ds.GetFns(ctx, filter)
}
func (m *metricds) GetFnByID(ctx context.Context, fnID string) (*models.Fn, error) {
ctx, span := trace.StartSpan(ctx, "ds_get_func")
defer span.End()
return m.ds.GetFnByID(ctx, fnID)
}
func (m *metricds) RemoveFn(ctx context.Context, fnID string) error {
ctx, span := trace.StartSpan(ctx, "ds_remove_func")
defer span.End()
return m.ds.RemoveFn(ctx, fnID)
}
// Close calls Close on the underlying Datastore // Close calls Close on the underlying Datastore
func (m *metricds) Close() error { func (m *metricds) Close() error {

View File

@@ -2,8 +2,7 @@ package datastoreutil
import ( import (
"context" "context"
"time"
"github.com/jmoiron/sqlx"
"github.com/fnproject/fn/api/models" "github.com/fnproject/fn/api/models"
) )
@@ -26,7 +25,7 @@ func (v *validator) GetAppID(ctx context.Context, appName string) (string, error
func (v *validator) GetAppByID(ctx context.Context, appID string) (*models.App, error) { func (v *validator) GetAppByID(ctx context.Context, appID string) (*models.App, error) {
if appID == "" { if appID == "" {
return nil, models.ErrDatastoreEmptyAppID return nil, models.ErrAppsMissingID
} }
return v.Datastore.GetAppByID(ctx, appID) return v.Datastore.GetAppByID(ctx, appID)
@@ -41,8 +40,9 @@ func (v *validator) InsertApp(ctx context.Context, app *models.App) (*models.App
if app == nil { if app == nil {
return nil, models.ErrDatastoreEmptyApp return nil, models.ErrDatastoreEmptyApp
} }
if app.ID != "" {
app.SetDefaults() return nil, models.ErrAppIDProvided
}
if err := app.Validate(); err != nil { if err := app.Validate(); err != nil {
return nil, err return nil, err
} }
@@ -56,7 +56,7 @@ func (v *validator) UpdateApp(ctx context.Context, app *models.App) (*models.App
return nil, models.ErrDatastoreEmptyApp return nil, models.ErrDatastoreEmptyApp
} }
if app.ID == "" { if app.ID == "" {
return nil, models.ErrDatastoreEmptyAppID return nil, models.ErrAppsMissingID
} }
return v.Datastore.UpdateApp(ctx, app) return v.Datastore.UpdateApp(ctx, app)
@@ -65,7 +65,7 @@ func (v *validator) UpdateApp(ctx context.Context, app *models.App) (*models.App
// name will never be empty. // name will never be empty.
func (v *validator) RemoveApp(ctx context.Context, appID string) error { func (v *validator) RemoveApp(ctx context.Context, appID string) error {
if appID == "" { if appID == "" {
return models.ErrDatastoreEmptyAppID return models.ErrAppsMissingID
} }
return v.Datastore.RemoveApp(ctx, appID) return v.Datastore.RemoveApp(ctx, appID)
@@ -74,7 +74,7 @@ func (v *validator) RemoveApp(ctx context.Context, appID string) error {
// appName and routePath will never be empty. // appName and routePath will never be empty.
func (v *validator) GetRoute(ctx context.Context, appID, routePath string) (*models.Route, error) { func (v *validator) GetRoute(ctx context.Context, appID, routePath string) (*models.Route, error) {
if appID == "" { if appID == "" {
return nil, models.ErrDatastoreEmptyAppID return nil, models.ErrRoutesMissingAppID
} }
if routePath == "" { if routePath == "" {
return nil, models.ErrRoutesMissingPath return nil, models.ErrRoutesMissingPath
@@ -86,7 +86,7 @@ func (v *validator) GetRoute(ctx context.Context, appID, routePath string) (*mod
// appName will never be empty // appName will never be empty
func (v *validator) GetRoutesByApp(ctx context.Context, appID string, routeFilter *models.RouteFilter) (routes []*models.Route, err error) { func (v *validator) GetRoutesByApp(ctx context.Context, appID string, routeFilter *models.RouteFilter) (routes []*models.Route, err error) {
if appID == "" { if appID == "" {
return nil, models.ErrDatastoreEmptyAppID return nil, models.ErrRoutesMissingAppID
} }
return v.Datastore.GetRoutesByApp(ctx, appID, routeFilter) return v.Datastore.GetRoutesByApp(ctx, appID, routeFilter)
@@ -98,7 +98,6 @@ func (v *validator) InsertRoute(ctx context.Context, route *models.Route) (*mode
return nil, models.ErrDatastoreEmptyRoute return nil, models.ErrDatastoreEmptyRoute
} }
route.SetDefaults()
if err := route.Validate(); err != nil { if err := route.Validate(); err != nil {
return nil, err return nil, err
} }
@@ -123,7 +122,7 @@ func (v *validator) UpdateRoute(ctx context.Context, newroute *models.Route) (*m
// appName and routePath will never be empty. // appName and routePath will never be empty.
func (v *validator) RemoveRoute(ctx context.Context, appID string, routePath string) error { func (v *validator) RemoveRoute(ctx context.Context, appID string, routePath string) error {
if appID == "" { if appID == "" {
return models.ErrDatastoreEmptyAppID return models.ErrRoutesMissingAppID
} }
if routePath == "" { if routePath == "" {
return models.ErrRoutesMissingPath return models.ErrRoutesMissingPath
@@ -132,7 +131,82 @@ func (v *validator) RemoveRoute(ctx context.Context, appID string, routePath str
return v.Datastore.RemoveRoute(ctx, appID, routePath) return v.Datastore.RemoveRoute(ctx, appID, routePath)
} }
// GetDatabase returns the underlying sqlx database implementation func (v *validator) InsertTrigger(ctx context.Context, t *models.Trigger) (*models.Trigger, error) {
func (v *validator) GetDatabase() *sqlx.DB {
return v.Datastore.GetDatabase() if t.ID != "" {
return nil, models.ErrTriggerIDProvided
}
if !time.Time(t.CreatedAt).IsZero() {
return nil, models.ErrCreatedAtProvided
}
if !time.Time(t.UpdatedAt).IsZero() {
return nil, models.ErrUpdatedAtProvided
}
return v.Datastore.InsertTrigger(ctx, t)
}
func (v *validator) UpdateTrigger(ctx context.Context, trigger *models.Trigger) (*models.Trigger, error) {
return v.Datastore.UpdateTrigger(ctx, trigger)
}
func (v *validator) GetTriggers(ctx context.Context, filter *models.TriggerFilter) ([]*models.Trigger, error) {
if filter.AppID == "" {
return nil, models.ErrTriggerMissingAppID
}
return v.Datastore.GetTriggers(ctx, filter)
}
func (v *validator) RemoveTrigger(ctx context.Context, triggerID string) error {
if triggerID == "" {
return models.ErrMissingID
}
return v.Datastore.RemoveTrigger(ctx, triggerID)
}
func (v *validator) InsertFn(ctx context.Context, fn *models.Fn) (*models.Fn, error) {
if fn == nil {
return nil, models.ErrDatastoreEmptyFn
}
if fn.ID != "" {
return nil, models.ErrFnsIDProvided
}
if fn.AppID == "" {
return nil, models.ErrFnsMissingAppID
}
if fn.Name == "" {
return nil, models.ErrFnsMissingName
}
return v.Datastore.InsertFn(ctx, fn)
}
func (v *validator) UpdateFn(ctx context.Context, fn *models.Fn) (*models.Fn, error) {
return v.Datastore.UpdateFn(ctx, fn)
}
func (v *validator) GetFnByID(ctx context.Context, fnID string) (*models.Fn, error) {
if fnID == "" {
return nil, models.ErrDatastoreEmptyFnID
}
return v.Datastore.GetFnByID(ctx, fnID)
}
func (v *validator) GetFns(ctx context.Context, filter *models.FnFilter) ([]*models.Fn, error) {
if filter.AppID == "" {
return nil, models.ErrFnsMissingAppID
}
return v.Datastore.GetFns(ctx, filter)
}
func (v *validator) RemoveFn(ctx context.Context, fnID string) error {
if fnID == "" {
return models.ErrDatastoreEmptyFnID
}
return v.Datastore.RemoveFn(ctx, fnID)
} }

View File

@@ -5,15 +5,20 @@ import (
"sort" "sort"
"strings" "strings"
"time"
"github.com/fnproject/fn/api/common"
"github.com/fnproject/fn/api/datastore/internal/datastoreutil" "github.com/fnproject/fn/api/datastore/internal/datastoreutil"
"github.com/fnproject/fn/api/id"
"github.com/fnproject/fn/api/logs" "github.com/fnproject/fn/api/logs"
"github.com/fnproject/fn/api/models" "github.com/fnproject/fn/api/models"
"github.com/jmoiron/sqlx"
) )
type mock struct { type mock struct {
Apps []*models.App Apps []*models.App
Routes []*models.Route Routes []*models.Route
Fns []*models.Fn
Triggers []*models.Trigger
models.LogStore models.LogStore
} }
@@ -31,6 +36,11 @@ func NewMockInit(args ...interface{}) models.Datastore {
mocker.Apps = x mocker.Apps = x
case []*models.Route: case []*models.Route:
mocker.Routes = x mocker.Routes = x
case []*models.Fn:
mocker.Fns = x
case []*models.Trigger:
mocker.Triggers = x
default: default:
panic("not accounted for data type sent to mock init. add it") panic("not accounted for data type sent to mock init. add it")
} }
@@ -52,7 +62,7 @@ func (m *mock) GetAppID(ctx context.Context, appName string) (string, error) {
func (m *mock) GetAppByID(ctx context.Context, appID string) (*models.App, error) { func (m *mock) GetAppByID(ctx context.Context, appID string) (*models.App, error) {
for _, a := range m.Apps { for _, a := range m.Apps {
if a.ID == appID { if a.ID == appID {
return a, nil return a.Clone(), nil
} }
} }
@@ -74,47 +84,101 @@ func (m *mock) GetApps(ctx context.Context, appFilter *models.AppFilter) ([]*mod
if len(apps) == appFilter.PerPage { if len(apps) == appFilter.PerPage {
break break
} }
if len(appFilter.NameIn) > 0 {
var found bool
for _, fn := range appFilter.NameIn {
if fn == a.Name {
found = true
break
}
}
if !found {
continue
}
}
if strings.Compare(appFilter.Cursor, a.Name) < 0 { if strings.Compare(appFilter.Cursor, a.Name) < 0 {
apps = append(apps, a) apps = append(apps, a.Clone())
} }
} }
return apps, nil return apps, nil
} }
func (m *mock) InsertApp(ctx context.Context, app *models.App) (*models.App, error) { func (m *mock) InsertApp(ctx context.Context, newApp *models.App) (*models.App, error) {
if a, _ := m.GetAppByID(ctx, app.ID); a != nil { for _, a := range m.Apps {
return nil, models.ErrAppsAlreadyExists if newApp.Name == a.Name {
return nil, models.ErrAppsAlreadyExists
}
} }
app := newApp.Clone()
app.CreatedAt = common.DateTime(time.Now())
app.UpdatedAt = app.CreatedAt
app.ID = id.New().String()
m.Apps = append(m.Apps, app) m.Apps = append(m.Apps, app)
return app, nil return app.Clone(), nil
} }
func (m *mock) UpdateApp(ctx context.Context, app *models.App) (*models.App, error) { func (m *mock) UpdateApp(ctx context.Context, app *models.App) (*models.App, error) {
a, err := m.GetAppByID(ctx, app.ID)
if err != nil {
return nil, err
}
a.Update(app)
return a.Clone(), nil appID := app.ID
for idx, a := range m.Apps {
if a.ID == appID {
if app.Name != "" && app.Name != a.Name {
return nil, models.ErrAppsNameImmutable
}
c := a.Clone()
c.Update(app)
err := c.Validate()
if err != nil {
return nil, err
}
m.Apps[idx] = c
return c.Clone(), nil
}
}
return nil, models.ErrAppsNotFound
} }
func (m *mock) RemoveApp(ctx context.Context, appID string) error { func (m *mock) RemoveApp(ctx context.Context, appID string) error {
m.batchDeleteRoutes(ctx, appID) m.batchDeleteRoutes(ctx, appID)
for i, a := range m.Apps { for i, a := range m.Apps {
if a.ID == appID { if a.ID == appID {
m.Apps = append(m.Apps[:i], m.Apps[i+1:]...) var newFns []*models.Fn
var newTriggers []*models.Trigger
newApps := append(m.Apps[0:i], m.Apps[i+1:]...)
for _, fn := range m.Fns {
if fn.AppID != appID {
newFns = append(newFns, fn)
}
}
for _, t := range m.Triggers {
if t.AppID != appID {
newTriggers = append(newTriggers, t)
}
}
m.Apps = newApps
m.Triggers = newTriggers
m.Fns = newFns
return nil return nil
} }
} }
return models.ErrAppsNotFound return models.ErrAppsNotFound
} }
func (m *mock) GetRoute(ctx context.Context, appID, routePath string) (*models.Route, error) { func (m *mock) GetRoute(ctx context.Context, appID, routePath string) (*models.Route, error) {
for _, r := range m.Routes { for _, r := range m.Routes {
if r.AppID == appID && r.Path == routePath { if r.AppID == appID && r.Path == routePath {
return r, nil return r.Clone(), nil
} }
} }
return nil, models.ErrRoutesNotFound return nil, models.ErrRoutesNotFound
@@ -140,13 +204,19 @@ func (m *mock) GetRoutesByApp(ctx context.Context, appID string, routeFilter *mo
(routeFilter.Image == "" || routeFilter.Image == r.Image) && (routeFilter.Image == "" || routeFilter.Image == r.Image) &&
strings.Compare(routeFilter.Cursor, r.Path) < 0 { strings.Compare(routeFilter.Cursor, r.Path) < 0 {
routes = append(routes, r) routes = append(routes, r.Clone())
} }
} }
return return
} }
func (m *mock) InsertRoute(ctx context.Context, route *models.Route) (*models.Route, error) { func (m *mock) InsertRoute(ctx context.Context, route *models.Route) (*models.Route, error) {
c := route.Clone()
c.SetDefaults()
c.CreatedAt = common.DateTime(time.Now())
c.UpdatedAt = c.CreatedAt
if _, err := m.GetAppByID(ctx, route.AppID); err != nil { if _, err := m.GetAppByID(ctx, route.AppID); err != nil {
return nil, err return nil, err
} }
@@ -154,8 +224,8 @@ func (m *mock) InsertRoute(ctx context.Context, route *models.Route) (*models.Ro
if r, _ := m.GetRoute(ctx, route.AppID, route.Path); r != nil { if r, _ := m.GetRoute(ctx, route.AppID, route.Path); r != nil {
return nil, models.ErrRoutesAlreadyExists return nil, models.ErrRoutesAlreadyExists
} }
m.Routes = append(m.Routes, route) m.Routes = append(m.Routes, c)
return route, nil return c.Clone(), nil
} }
func (m *mock) UpdateRoute(ctx context.Context, route *models.Route) (*models.Route, error) { func (m *mock) UpdateRoute(ctx context.Context, route *models.Route) (*models.Route, error) {
@@ -184,7 +254,7 @@ func (m *mock) RemoveRoute(ctx context.Context, appID, routePath string) error {
} }
func (m *mock) batchDeleteRoutes(ctx context.Context, appID string) error { func (m *mock) batchDeleteRoutes(ctx context.Context, appID string) error {
newRoutes := []*models.Route{} var newRoutes []*models.Route
for _, c := range m.Routes { for _, c := range m.Routes {
if c.AppID != appID { if c.AppID != appID {
newRoutes = append(newRoutes, c) newRoutes = append(newRoutes, c)
@@ -194,9 +264,223 @@ func (m *mock) batchDeleteRoutes(ctx context.Context, appID string) error {
return nil return nil
} }
// GetDatabase returns nil here since shouldn't really be used func (m *mock) InsertFn(ctx context.Context, fn *models.Fn) (*models.Fn, error) {
func (m *mock) GetDatabase() *sqlx.DB { _, err := m.GetAppByID(ctx, fn.AppID)
return nil if err != nil {
return nil, err
}
for _, f := range m.Fns {
if f.ID == fn.ID ||
(f.AppID == fn.AppID &&
f.Name == fn.Name) {
return nil, models.ErrFnsExists
}
}
cl := fn.Clone()
cl.ID = id.New().String()
cl.CreatedAt = common.DateTime(time.Now())
cl.UpdatedAt = cl.CreatedAt
err = fn.Validate()
if err != nil {
return nil, err
}
m.Fns = append(m.Fns, cl)
return cl.Clone(), nil
}
func (m *mock) UpdateFn(ctx context.Context, fn *models.Fn) (*models.Fn, error) {
// update if exists
for _, f := range m.Fns {
if f.ID == fn.ID {
clone := f.Clone()
clone.Update(fn)
err := clone.Validate()
if err != nil {
return nil, err
}
*f = *clone
return f, nil
}
}
return nil, models.ErrFnsNotFound
}
type sortF []*models.Fn
func (s sortF) Len() int { return len(s) }
func (s sortF) Less(i, j int) bool { return strings.Compare(s[i].Name, s[j].Name) < 0 }
func (s sortF) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (m *mock) GetFns(ctx context.Context, filter *models.FnFilter) ([]*models.Fn, error) {
// sort them all first for cursoring (this is for testing, n is small & mock is not concurrent..)
sort.Sort(sortF(m.Fns))
funcs := []*models.Fn{}
for _, f := range m.Fns {
if filter.PerPage > 0 && len(funcs) == filter.PerPage {
break
}
if strings.Compare(filter.Cursor, f.Name) < 0 &&
(filter.AppID == "" || filter.AppID == f.AppID) &&
(filter.Name == "" || filter.Name == f.Name) {
funcs = append(funcs, f)
}
}
return funcs, nil
}
func (m *mock) GetFnByID(ctx context.Context, fnID string) (*models.Fn, error) {
for _, f := range m.Fns {
if f.ID == fnID {
return f, nil
}
}
return nil, models.ErrFnsNotFound
}
func (m *mock) RemoveFn(ctx context.Context, fnID string) error {
for i, f := range m.Fns {
if f.ID == fnID {
m.Fns = append(m.Fns[:i], m.Fns[i+1:]...)
var newTriggers []*models.Trigger
for _, t := range m.Triggers {
if t.FnID != f.ID {
newTriggers = append(newTriggers, t)
}
}
m.Triggers = newTriggers
return nil
}
}
return models.ErrFnsNotFound
}
func (m *mock) InsertTrigger(ctx context.Context, trigger *models.Trigger) (*models.Trigger, error) {
_, err := m.GetAppByID(ctx, trigger.AppID)
if err != nil {
return nil, err
}
fn, err := m.GetFnByID(ctx, trigger.FnID)
if err != nil {
return nil, err
}
if fn.AppID != trigger.AppID {
return nil, models.ErrTriggerFnIDNotSameApp
}
for _, t := range m.Triggers {
if t.ID == trigger.ID ||
(t.AppID == trigger.AppID &&
t.FnID == trigger.FnID &&
t.Name == trigger.Name) {
return nil, models.ErrTriggerExists
}
}
cl := trigger.Clone()
cl.CreatedAt = common.DateTime(time.Now())
cl.UpdatedAt = cl.CreatedAt
cl.ID = id.New().String()
err = trigger.Validate()
if err != nil {
return nil, err
}
m.Triggers = append(m.Triggers, cl)
return cl.Clone(), nil
}
func (m *mock) UpdateTrigger(ctx context.Context, trigger *models.Trigger) (*models.Trigger, error) {
for _, t := range m.Triggers {
if t.ID == trigger.ID {
cl := t.Clone()
cl.Update(trigger)
err := cl.Validate()
if err != nil {
return nil, err
}
*t = *cl
return cl.Clone(), nil
}
}
return nil, models.ErrTriggerNotFound
}
func (m *mock) GetTrigger(ctx context.Context, appId, fnId, triggerName string) (*models.Trigger, error) {
for _, t := range m.Triggers {
if t.AppID == appId && t.FnID == fnId && t.Name == triggerName {
return t.Clone(), nil
}
}
return nil, models.ErrTriggerNotFound
}
func (m *mock) GetTriggerByID(ctx context.Context, triggerId string) (*models.Trigger, error) {
for _, t := range m.Triggers {
if t.ID == triggerId {
return t.Clone(), nil
}
}
return nil, models.ErrTriggerNotFound
}
type sortT []*models.Trigger
func (s sortT) Len() int { return len(s) }
func (s sortT) Less(i, j int) bool { return strings.Compare(s[i].ID, s[j].ID) < 0 }
func (s sortT) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (m *mock) GetTriggers(ctx context.Context, filter *models.TriggerFilter) ([]*models.Trigger, error) {
sort.Sort(sortT(m.Triggers))
res := []*models.Trigger{}
for _, t := range m.Triggers {
if filter.PerPage > 0 && len(res) == filter.PerPage {
break
}
matched := true
if filter.Cursor != "" && t.ID <= filter.Cursor {
matched = false
}
if t.AppID != filter.AppID {
matched = false
}
if filter.FnID != "" && filter.FnID != t.FnID {
matched = false
}
if filter.Name != "" && filter.Name != t.Name {
matched = false
}
if matched {
res = append(res, t)
}
}
return res, nil
}
func (m *mock) RemoveTrigger(ctx context.Context, triggerID string) error {
for i, t := range m.Triggers {
if t.ID == triggerID {
m.Triggers = append(m.Triggers[:i], m.Triggers[i+1:]...)
return nil
}
}
return models.ErrTriggerNotFound
} }
func (m *mock) Close() error { func (m *mock) Close() error {

View File

@@ -11,5 +11,5 @@ func TestDatastore(t *testing.T) {
f := func(t *testing.T) models.Datastore { f := func(t *testing.T) models.Datastore {
return NewMock() return NewMock()
} }
datastoretest.Test(t, f) datastoretest.RunAllTests(t, f, datastoretest.NewBasicResourceProvider())
} }

View File

@@ -0,0 +1,41 @@
package migrations
import (
"context"
"github.com/fnproject/fn/api/datastore/sql/migratex"
"github.com/jmoiron/sqlx"
)
func up16(ctx context.Context, tx *sqlx.Tx) error {
createQuery := `CREATE TABLE IF NOT EXISTS fns (
id varchar(256) NOT NULL PRIMARY KEY,
name varchar(256) NOT NULL,
app_id varchar(256) NOT NULL,
image varchar(256) NOT NULL,
format varchar(16) NOT NULL,
memory int NOT NULL,
timeout int NOT NULL,
idle_timeout int NOT NULL,
config text NOT NULL,
annotations text NOT NULL,
created_at varchar(256) NOT NULL,
updated_at varchar(256) NOT NULL,
CONSTRAINT name_app_id_unique UNIQUE (app_id, name)
);`
_, err := tx.ExecContext(ctx, createQuery)
return err
}
func down16(ctx context.Context, tx *sqlx.Tx) error {
_, err := tx.ExecContext(ctx, "DROP TABLE fns;")
return err
}
func init() {
Migrations = append(Migrations, &migratex.MigFields{
VersionFunc: vfunc(16),
UpFunc: up16,
DownFunc: down16,
})
}

View File

@@ -20,6 +20,7 @@ import (
"github.com/fnproject/fn/api/datastore" "github.com/fnproject/fn/api/datastore"
"github.com/fnproject/fn/api/datastore/sql/dbhelper" "github.com/fnproject/fn/api/datastore/sql/dbhelper"
"github.com/fnproject/fn/api/id"
"github.com/fnproject/fn/api/logs" "github.com/fnproject/fn/api/logs"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@@ -77,11 +78,40 @@ var tables = [...]string{`CREATE TABLE IF NOT EXISTS routes (
PRIMARY KEY (id) PRIMARY KEY (id)
);`, );`,
`CREATE TABLE IF NOT EXISTS triggers (
id varchar(256) NOT NULL PRIMARY KEY,
name varchar(256) NOT NULL,
app_id varchar(256) NOT NULL,
fn_id varchar(256) NOT NULL,
created_at varchar(256) NOT NULL,
updated_at varchar(256) NOT NULL,
type varchar(256) NOT NULL,
source varchar(256) NOT NULL,
annotations text NOT NULL,
CONSTRAINT name_app_id_fn_id_unique UNIQUE (app_id, fn_id,name)
);`,
`CREATE TABLE IF NOT EXISTS logs ( `CREATE TABLE IF NOT EXISTS logs (
id varchar(256) NOT NULL PRIMARY KEY, id varchar(256) NOT NULL PRIMARY KEY,
app_id varchar(256) NOT NULL, app_id varchar(256) NOT NULL,
log text NOT NULL log text NOT NULL
);`, );`,
`CREATE TABLE IF NOT EXISTS fns (
id varchar(256) NOT NULL PRIMARY KEY,
name varchar(256) NOT NULL,
app_id varchar(256) NOT NULL,
image varchar(256) NOT NULL,
format varchar(16) NOT NULL,
memory int NOT NULL,
timeout int NOT NULL,
idle_timeout int NOT NULL,
config text NOT NULL,
annotations text NOT NULL,
created_at varchar(256) NOT NULL,
updated_at varchar(256) NOT NULL,
CONSTRAINT name_app_id_unique UNIQUE (app_id, name)
);`,
} }
const ( const (
@@ -90,6 +120,12 @@ const (
appIDSelector = `SELECT id, name, config, annotations, syslog_url, created_at, updated_at FROM apps WHERE id=?` appIDSelector = `SELECT id, name, config, annotations, syslog_url, created_at, updated_at FROM apps WHERE id=?`
ensureAppSelector = `SELECT id FROM apps WHERE name=?` ensureAppSelector = `SELECT id FROM apps WHERE name=?`
fnSelector = `SELECT id,name,app_id,image,format,memory,timeout,idle_timeout,config,annotations,created_at,updated_at FROM fns`
fnIDSelector = fnSelector + ` WHERE id=?`
triggerSelector = `SELECT id,name,app_id,fn_id,type,source,annotations,created_at,updated_at FROM triggers`
triggerIDSelector = triggerSelector + ` WHERE id=?`
EnvDBPingMaxRetries = "FN_DS_DB_PING_MAX_RETRIES" EnvDBPingMaxRetries = "FN_DS_DB_PING_MAX_RETRIES"
) )
@@ -301,6 +337,18 @@ func (ds *SQLStore) clear() error {
return err return err
} }
query = tx.Rebind(`DELETE FROM triggers`)
_, err = tx.Exec(query)
if err != nil {
return err
}
query = tx.Rebind(`DELETE FROM fns`)
_, err = tx.Exec(query)
if err != nil {
return err
}
query = tx.Rebind(`DELETE FROM logs`) query = tx.Rebind(`DELETE FROM logs`)
_, err = tx.Exec(query) _, err = tx.Exec(query)
return err return err
@@ -323,7 +371,17 @@ func (ds *SQLStore) GetAppID(ctx context.Context, appName string) (string, error
return app.ID, nil return app.ID, nil
} }
func (ds *SQLStore) InsertApp(ctx context.Context, app *models.App) (*models.App, error) { func (ds *SQLStore) InsertApp(ctx context.Context, newApp *models.App) (*models.App, error) {
app := newApp.Clone()
app.CreatedAt = common.DateTime(time.Now())
app.UpdatedAt = app.CreatedAt
app.ID = id.New().String()
if app.Config == nil {
// keeps the json from being nil
app.Config = map[string]string{}
}
query := ds.db.Rebind(`INSERT INTO apps ( query := ds.db.Rebind(`INSERT INTO apps (
id, id,
name, name,
@@ -355,6 +413,7 @@ func (ds *SQLStore) InsertApp(ctx context.Context, app *models.App) (*models.App
func (ds *SQLStore) UpdateApp(ctx context.Context, newapp *models.App) (*models.App, error) { func (ds *SQLStore) UpdateApp(ctx context.Context, newapp *models.App) (*models.App, error) {
var app models.App var app models.App
err := ds.Tx(func(tx *sqlx.Tx) error { err := ds.Tx(func(tx *sqlx.Tx) error {
// NOTE: must query whole object since we're returning app, Update logic // NOTE: must query whole object since we're returning app, Update logic
// must only modify modifiable fields (as seen here). need to fix brittle.. // must only modify modifiable fields (as seen here). need to fix brittle..
@@ -370,6 +429,9 @@ func (ds *SQLStore) UpdateApp(ctx context.Context, newapp *models.App) (*models.
return err return err
} }
if newapp.Name != "" && app.Name != newapp.Name {
return models.ErrAppsNameImmutable
}
app.Update(newapp) app.Update(newapp)
err = app.Validate() err = app.Validate()
if err != nil { if err != nil {
@@ -416,6 +478,8 @@ func (ds *SQLStore) RemoveApp(ctx context.Context, appID string) error {
`DELETE FROM logs WHERE app_id=?`, `DELETE FROM logs WHERE app_id=?`,
`DELETE FROM calls WHERE app_id=?`, `DELETE FROM calls WHERE app_id=?`,
`DELETE FROM routes WHERE app_id=?`, `DELETE FROM routes WHERE app_id=?`,
`DELETE FROM fns WHERE app_id=?`,
`DELETE FROM triggers WHERE app_id=?`,
} }
for _, stmt := range deletes { for _, stmt := range deletes {
_, err := tx.ExecContext(ctx, tx.Rebind(stmt), appID) _, err := tx.ExecContext(ctx, tx.Rebind(stmt), appID)
@@ -445,15 +509,17 @@ func (ds *SQLStore) GetAppByID(ctx context.Context, appID string) (*models.App,
// GetApps retrieves an array of apps according to a specific filter. // GetApps retrieves an array of apps according to a specific filter.
func (ds *SQLStore) GetApps(ctx context.Context, filter *models.AppFilter) ([]*models.App, error) { func (ds *SQLStore) GetApps(ctx context.Context, filter *models.AppFilter) ([]*models.App, error) {
res := []*models.App{} var res []*models.App
if filter.NameIn != nil && len(filter.NameIn) == 0 { // this basically makes sure it doesn't return ALL apps if filter.NameIn != nil && len(filter.NameIn) == 0 { // this basically makes sure it doesn't return ALL apps
return res, nil return res, nil
} }
query, args, err := buildFilterAppQuery(filter) query, args, err := buildFilterAppQuery(filter)
if err != nil { if err != nil {
return nil, err return nil, err
} }
query = ds.db.Rebind(fmt.Sprintf("SELECT DISTINCT name, config, annotations, syslog_url, created_at, updated_at FROM apps %s", query)) query = ds.db.Rebind(fmt.Sprintf("SELECT DISTINCT id, name, config, annotations, syslog_url, created_at, updated_at FROM apps %s", query))
rows, err := ds.db.QueryxContext(ctx, query, args...) rows, err := ds.db.QueryxContext(ctx, query, args...)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -478,7 +544,11 @@ func (ds *SQLStore) GetApps(ctx context.Context, filter *models.AppFilter) ([]*m
return res, nil return res, nil
} }
func (ds *SQLStore) InsertRoute(ctx context.Context, route *models.Route) (*models.Route, error) { func (ds *SQLStore) InsertRoute(ctx context.Context, newRoute *models.Route) (*models.Route, error) {
route := newRoute.Clone()
route.CreatedAt = common.DateTime(time.Now())
route.UpdatedAt = route.CreatedAt
err := ds.Tx(func(tx *sqlx.Tx) error { err := ds.Tx(func(tx *sqlx.Tx) error {
query := tx.Rebind(`SELECT 1 FROM apps WHERE id=?`) query := tx.Rebind(`SELECT 1 FROM apps WHERE id=?`)
r := tx.QueryRowContext(ctx, query, route.AppID) r := tx.QueryRowContext(ctx, query, route.AppID)
@@ -658,6 +728,7 @@ func (ds *SQLStore) GetRoutesByApp(ctx context.Context, appID string, filter *mo
} }
res = append(res, &route) res = append(res, &route)
} }
if err := rows.Err(); err != nil { if err := rows.Err(); err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return res, nil // no error for empty list return res, nil // no error for empty list
@@ -667,6 +738,190 @@ func (ds *SQLStore) GetRoutesByApp(ctx context.Context, appID string, filter *mo
return res, nil return res, nil
} }
func (ds *SQLStore) InsertFn(ctx context.Context, newFn *models.Fn) (*models.Fn, error) {
fn := newFn.Clone()
fn.ID = id.New().String()
fn.CreatedAt = common.DateTime(time.Now())
fn.UpdatedAt = fn.CreatedAt
err := newFn.Validate()
if err != nil {
return nil, err
}
err = ds.Tx(func(tx *sqlx.Tx) error {
query := tx.Rebind(`SELECT 1 FROM apps WHERE id=?`)
r := tx.QueryRowContext(ctx, query, fn.AppID)
if err := r.Scan(new(int)); err != nil {
if err == sql.ErrNoRows {
return models.ErrAppsNotFound
}
}
query = tx.Rebind(`INSERT INTO fns (
id,
name,
app_id,
image,
format,
memory,
timeout,
idle_timeout,
config,
annotations,
created_at,
updated_at
)
VALUES (
:id,
:name,
:app_id,
:image,
:format,
:memory,
:timeout,
:idle_timeout,
:config,
:annotations,
:created_at,
:updated_at
);`)
_, err = tx.NamedExecContext(ctx, query, fn)
return err
})
if err != nil {
if ds.helper.IsDuplicateKeyError(err) {
return nil, models.ErrFnsExists
}
return nil, err
}
return fn, nil
}
func (ds *SQLStore) UpdateFn(ctx context.Context, fn *models.Fn) (*models.Fn, error) {
err := ds.Tx(func(tx *sqlx.Tx) error {
var dst models.Fn
query := tx.Rebind(fnIDSelector)
row := tx.QueryRowxContext(ctx, query, fn.ID)
err := row.StructScan(&dst)
if err == sql.ErrNoRows {
return models.ErrFnsNotFound
} else if err != nil {
return err
}
dst.Update(fn)
err = dst.Validate()
if err != nil {
return err
}
fn = &dst // set for query & to return
query = tx.Rebind(`UPDATE fns SET
name = :name,
image = :image,
format = :format,
memory = :memory,
timeout = :timeout,
idle_timeout = :idle_timeout,
config = :config,
annotations = :annotations,
updated_at = :updated_at
WHERE id=:id;`)
_, err = tx.NamedExecContext(ctx, query, fn)
return err
})
if err != nil {
return nil, err
}
return fn, nil
}
func (ds *SQLStore) GetFns(ctx context.Context, filter *models.FnFilter) ([]*models.Fn, error) {
var res []*models.Fn // for json empty list
if filter == nil {
filter = new(models.FnFilter)
}
filterQuery, args := buildFilterFnQuery(filter)
query := fmt.Sprintf("%s %s", fnSelector, filterQuery)
query = ds.db.Rebind(query)
rows, err := ds.db.QueryxContext(ctx, query, args...)
if err != nil {
if err == sql.ErrNoRows {
return res, nil // no error for empty list
}
return nil, err
}
defer rows.Close()
for rows.Next() {
var fn models.Fn
err := rows.StructScan(&fn)
if err != nil {
continue
}
res = append(res, &fn)
}
if err := rows.Err(); err != nil {
if err == sql.ErrNoRows {
return res, nil // no error for empty list
}
}
return res, nil
}
func (ds *SQLStore) GetFnByID(ctx context.Context, fnID string) (*models.Fn, error) {
query := ds.db.Rebind(fmt.Sprintf("%s WHERE id=?", fnSelector))
row := ds.db.QueryRowxContext(ctx, query, fnID)
var fn models.Fn
err := row.StructScan(&fn)
if err == sql.ErrNoRows {
return nil, models.ErrFnsNotFound
} else if err != nil {
return nil, err
}
return &fn, nil
}
func (ds *SQLStore) RemoveFn(ctx context.Context, fnID string) error {
return ds.Tx(func(tx *sqlx.Tx) error {
query := tx.Rebind(fmt.Sprintf("%s WHERE id=?", fnSelector))
row := tx.QueryRowxContext(ctx, query, fnID)
var fn models.Fn
err := row.StructScan(&fn)
if err == sql.ErrNoRows {
return models.ErrFnsNotFound
}
query = tx.Rebind(`DELETE FROM triggers WHERE fn_id=?`)
_, err = tx.ExecContext(ctx, query, fnID)
if err != nil {
return err
}
query = tx.Rebind(`DELETE FROM fns WHERE id=?`)
_, err = tx.ExecContext(ctx, query, fnID)
return err
})
}
func (ds *SQLStore) Tx(f func(*sqlx.Tx) error) error { func (ds *SQLStore) Tx(f func(*sqlx.Tx) error) error {
tx, err := ds.db.Beginx() tx, err := ds.db.Beginx()
if err != nil { if err != nil {
@@ -791,20 +1046,9 @@ func buildFilterRouteQuery(filter *models.RouteFilter) (string, []interface{}) {
var b bytes.Buffer var b bytes.Buffer
var args []interface{} var args []interface{}
where := func(colOp, val string) { args = where(&b, args, "app_id=? ", filter.AppID)
if val != "" { args = where(&b, args, "image=?", filter.Image)
args = append(args, val) args = where(&b, args, "path>?", filter.Cursor)
if len(args) == 1 {
fmt.Fprintf(&b, `WHERE %s`, colOp)
} else {
fmt.Fprintf(&b, ` AND %s`, colOp)
}
}
}
where("app_id=? ", filter.AppID)
where("image=?", filter.Image)
where("path>?", filter.Cursor)
// where("path LIKE ?%", filter.PathPrefix) TODO needs escaping // where("path LIKE ?%", filter.PathPrefix) TODO needs escaping
fmt.Fprintf(&b, ` ORDER BY path ASC`) // TODO assert this is indexed fmt.Fprintf(&b, ` ORDER BY path ASC`) // TODO assert this is indexed
@@ -822,32 +1066,9 @@ func buildFilterAppQuery(filter *models.AppFilter) (string, []interface{}, error
var b bytes.Buffer var b bytes.Buffer
// todo: this same thing is in several places in here, DRY it up across this file
where := func(colOp, val interface{}) {
if val == nil {
return
}
switch v := val.(type) {
case string:
if v == "" {
return
}
case []string:
if len(v) == 0 {
return
}
}
args = append(args, val)
if len(args) == 1 {
fmt.Fprintf(&b, `WHERE %s`, colOp)
} else {
fmt.Fprintf(&b, ` AND %s`, colOp)
}
}
// where("name LIKE ?%", filter.Name) // TODO needs escaping? // where("name LIKE ?%", filter.Name) // TODO needs escaping?
where("name>?", filter.Cursor) args = where(&b, args, "name>?", filter.Cursor)
where("name IN (?)", filter.NameIn) args = where(&b, args, "name IN (?)", filter.NameIn)
fmt.Fprintf(&b, ` ORDER BY name ASC`) // TODO assert this is indexed fmt.Fprintf(&b, ` ORDER BY name ASC`) // TODO assert this is indexed
fmt.Fprintf(&b, ` LIMIT ?`) fmt.Fprintf(&b, ` LIMIT ?`)
@@ -865,26 +1086,15 @@ func buildFilterCallQuery(filter *models.CallFilter) (string, []interface{}) {
var b bytes.Buffer var b bytes.Buffer
var args []interface{} var args []interface{}
where := func(colOp, val string) { args = where(&b, args, "id<?", filter.Cursor)
if val != "" {
args = append(args, val)
if len(args) == 1 {
fmt.Fprintf(&b, `WHERE %s?`, colOp)
} else {
fmt.Fprintf(&b, ` AND %s?`, colOp)
}
}
}
where("id<", filter.Cursor)
if !time.Time(filter.ToTime).IsZero() { if !time.Time(filter.ToTime).IsZero() {
where("created_at<", filter.ToTime.String()) args = where(&b, args, "created_at<?", filter.ToTime.String())
} }
if !time.Time(filter.FromTime).IsZero() { if !time.Time(filter.FromTime).IsZero() {
where("created_at>", filter.FromTime.String()) args = where(&b, args, "created_at>?", filter.FromTime.String())
} }
where("app_id=", filter.AppID) args = where(&b, args, "app_id=?", filter.AppID)
where("path=", filter.Path) args = where(&b, args, "path=?", filter.Path)
fmt.Fprintf(&b, ` ORDER BY id DESC`) // TODO assert this is indexed fmt.Fprintf(&b, ` ORDER BY id DESC`) // TODO assert this is indexed
fmt.Fprintf(&b, ` LIMIT ?`) fmt.Fprintf(&b, ` LIMIT ?`)
@@ -893,9 +1103,276 @@ func buildFilterCallQuery(filter *models.CallFilter) (string, []interface{}) {
return b.String(), args return b.String(), args
} }
// GetDatabase returns the underlying sqlx database implementation func buildFilterFnQuery(filter *models.FnFilter) (string, []interface{}) {
func (ds *SQLStore) GetDatabase() *sqlx.DB { if filter == nil {
return ds.db return "", nil
}
var b bytes.Buffer
var args []interface{}
// where(fmt.Sprintf("image LIKE '%s%%'"), filter.Image) // TODO needs escaping, prob we want prefix query to ignore tags
args = where(&b, args, "app_id=? ", filter.AppID)
args = where(&b, args, "name>?", filter.Cursor)
fmt.Fprintf(&b, ` ORDER BY name ASC`)
if filter.PerPage > 0 {
fmt.Fprintf(&b, ` LIMIT ?`)
args = append(args, filter.PerPage)
}
return b.String(), args
}
func where(b *bytes.Buffer, args []interface{}, colOp string, val interface{}) []interface{} {
if val == nil {
return args
}
switch v := val.(type) {
case string:
if v == "" {
return args
}
case []string:
if len(v) == 0 {
return args
}
}
args = append(args, val)
if len(args) == 1 {
fmt.Fprintf(b, `WHERE %s`, colOp)
} else {
fmt.Fprintf(b, ` AND %s`, colOp)
}
return args
}
func (ds *SQLStore) InsertTrigger(ctx context.Context, newTrigger *models.Trigger) (*models.Trigger, error) {
trigger := newTrigger.Clone()
trigger.CreatedAt = common.DateTime(time.Now())
trigger.UpdatedAt = trigger.CreatedAt
trigger.ID = id.New().String()
err := trigger.Validate()
if err != nil {
return nil, err
}
err = ds.Tx(func(tx *sqlx.Tx) error {
query := tx.Rebind(`SELECT 1 FROM apps WHERE id=?`)
r := tx.QueryRowContext(ctx, query, trigger.AppID)
if err := r.Scan(new(int)); err != nil {
if err == sql.ErrNoRows {
return models.ErrAppsNotFound
}
}
query = tx.Rebind(`SELECT app_id FROM fns WHERE id=?`)
r = tx.QueryRowContext(ctx, query, trigger.FnID)
var app_id string
if err := r.Scan(&app_id); err != nil {
if err == sql.ErrNoRows {
return models.ErrFnsNotFound
}
}
if app_id != trigger.AppID {
return models.ErrTriggerFnIDNotSameApp
}
query = tx.Rebind(`INSERT INTO triggers (
id,
name,
app_id,
fn_id,
created_at,
updated_at,
type,
source,
annotations
)
VALUES (
:id,
:name,
:app_id,
:fn_id,
:created_at,
:updated_at,
:type,
:source,
:annotations
);`)
_, err = tx.NamedExecContext(ctx, query, trigger)
return err
})
if err != nil {
if ds.helper.IsDuplicateKeyError(err) {
return nil, models.ErrTriggerExists
}
return nil, err
}
return trigger, err
}
func (ds *SQLStore) UpdateTrigger(ctx context.Context, trigger *models.Trigger) (*models.Trigger, error) {
err := ds.Tx(func(tx *sqlx.Tx) error {
var dst models.Trigger
query := tx.Rebind(triggerIDSelector)
row := tx.QueryRowxContext(ctx, query, trigger.ID)
err := row.StructScan(&dst)
if err != nil && err != sql.ErrNoRows {
return err
} else if err == sql.ErrNoRows {
return models.ErrTriggerNotFound
}
dst.Update(trigger)
err = dst.Validate()
if err != nil {
return err
}
trigger = &dst // set for query & to return
query = tx.Rebind(`UPDATE triggers SET
name = :name,
fn_id = :fn_id,
updated_at = :updated_at,
source = :source,
annotations = :annotations
WHERE id = :id;`)
_, err = tx.NamedExecContext(ctx, query, trigger)
return err
})
if err != nil {
return nil, err
}
return trigger, nil
}
func (ds *SQLStore) GetTrigger(ctx context.Context, appId, fnId, triggerName string) (*models.Trigger, error) {
var trigger models.Trigger
query := ds.db.Rebind(fmt.Sprintf("%s WHERE name=? AND app_id=? AND fn_id=?", fnSelector))
row := ds.db.QueryRowxContext(ctx, query, triggerName, appId, fnId)
err := row.StructScan(&trigger)
if err == sql.ErrNoRows {
return nil, models.ErrTriggerNotFound
}
if err != nil {
return nil, err
}
return &trigger, nil
}
func (ds *SQLStore) RemoveTrigger(ctx context.Context, triggerId string) error {
query := ds.db.Rebind(`DELETE FROM triggers WHERE id = ?;`)
res, err := ds.db.ExecContext(ctx, query, triggerId)
if err != nil {
return err
}
n, err := res.RowsAffected()
if err != nil {
return err
}
if n == 0 {
return models.ErrTriggerNotFound
}
return nil
}
func (ds *SQLStore) GetTriggerByID(ctx context.Context, triggerID string) (*models.Trigger, error) {
var trigger models.Trigger
query := ds.db.Rebind(triggerIDSelector)
row := ds.db.QueryRowxContext(ctx, query, triggerID)
err := row.StructScan(&trigger)
if err == sql.ErrNoRows {
return nil, models.ErrTriggerNotFound
}
if err != nil {
return nil, err
}
return &trigger, nil
}
func buildFilterTriggerQuery(filter *models.TriggerFilter) (string, []interface{}) {
var b bytes.Buffer
var args []interface{}
fmt.Fprintf(&b, `app_id = ?`)
args = append(args, filter.AppID)
if filter.FnID != "" {
fmt.Fprintf(&b, ` AND fn_id = ?`)
args = append(args, filter.FnID)
}
if filter.Name != "" {
fmt.Fprintf(&b, ` AND name = ?`)
args = append(args, filter.Name)
}
if filter.Cursor != "" {
fmt.Fprintf(&b, ` AND id > ?`)
args = append(args, filter.Cursor)
}
fmt.Fprintf(&b, ` ORDER BY name ASC`)
if filter.PerPage != 0 {
fmt.Fprintf(&b, ` LIMIT ?`)
args = append(args, filter.PerPage)
}
return b.String(), args
}
func (ds *SQLStore) GetTriggers(ctx context.Context, filter *models.TriggerFilter) ([]*models.Trigger, error) {
var res []*models.Trigger // for json empty list
if filter == nil {
filter = new(models.TriggerFilter)
}
filterQuery, args := buildFilterTriggerQuery(filter)
logrus.Error(filterQuery, args)
query := fmt.Sprintf("%s WHERE %s", triggerSelector, filterQuery)
query = ds.db.Rebind(query)
rows, err := ds.db.QueryxContext(ctx, query, args...)
if err != nil {
if err == sql.ErrNoRows {
return res, nil // no error for empty list
}
return nil, err
}
defer rows.Close()
for rows.Next() {
var trigger models.Trigger
err := rows.StructScan(&trigger)
if err != nil {
continue
}
res = append(res, &trigger)
}
if err := rows.Err(); err != nil {
if err == sql.ErrNoRows {
return res, nil // no error for empty list
}
}
return res, nil
} }
// Close closes the database, releasing any open resources. // Close closes the database, releasing any open resources.

View File

@@ -65,7 +65,9 @@ func TestDatastore(t *testing.T) {
ds := f(t) ds := f(t)
return datastoreutil.NewValidator(ds) return datastoreutil.NewValidator(ds)
} }
datastoretest.Test(t, f2) t.Run(u.Scheme, func(t *testing.T) {
datastoretest.RunAllTests(t, f2, datastoretest.NewBasicResourceProvider())
})
// also logs // also logs
logstoretest.Test(t, f(t)) logstoretest.Test(t, f(t))
@@ -96,7 +98,7 @@ func TestDatastore(t *testing.T) {
} }
// test fresh w/o migrations // test fresh w/o migrations
datastoretest.Test(t, f2) t.Run(u.Scheme, func(t *testing.T) { datastoretest.RunAllTests(t, f2, datastoretest.NewBasicResourceProvider()) })
// also test sql implements logstore // also test sql implements logstore
logstoretest.Test(t, f(t)) logstoretest.Test(t, f(t))
@@ -119,7 +121,7 @@ func TestDatastore(t *testing.T) {
} }
// test that migrations work & things work with them // test that migrations work & things work with them
datastoretest.Test(t, f2) t.Run(u.Scheme, func(t *testing.T) { datastoretest.RunAllTests(t, f2, datastoretest.NewBasicResourceProvider()) })
// also test sql implements logstore // also test sql implements logstore
logstoretest.Test(t, f(t)) logstoretest.Test(t, f(t))

View File

@@ -26,8 +26,6 @@ var testRoute = &models.Route{
} }
func SetupTestCall(t *testing.T, ctx context.Context, ls models.LogStore) *models.Call { func SetupTestCall(t *testing.T, ctx context.Context, ls models.LogStore) *models.Call {
testApp.SetDefaults()
var call models.Call var call models.Call
call.AppID = testApp.ID call.AppID = testApp.ID
call.CreatedAt = common.DateTime(time.Now()) call.CreatedAt = common.DateTime(time.Now())

View File

@@ -21,7 +21,7 @@ func (v *validator) InsertLog(ctx context.Context, appID, callID string, callLog
return models.ErrDatastoreEmptyCallID return models.ErrDatastoreEmptyCallID
} }
if appID == "" { if appID == "" {
return models.ErrDatastoreEmptyAppID return models.ErrMissingAppID
} }
return v.LogStore.InsertLog(ctx, appID, callID, callLog) return v.LogStore.InsertLog(ctx, appID, callID, callLog)
} }
@@ -32,7 +32,7 @@ func (v *validator) GetLog(ctx context.Context, appID, callID string) (io.Reader
return nil, models.ErrDatastoreEmptyCallID return nil, models.ErrDatastoreEmptyCallID
} }
if appID == "" { if appID == "" {
return nil, models.ErrDatastoreEmptyAppID return nil, models.ErrMissingAppID
} }
return v.LogStore.GetLog(ctx, appID, callID) return v.LogStore.GetLog(ctx, appID, callID)
} }
@@ -43,7 +43,7 @@ func (v *validator) InsertCall(ctx context.Context, call *models.Call) error {
return models.ErrDatastoreEmptyCallID return models.ErrDatastoreEmptyCallID
} }
if call.AppID == "" { if call.AppID == "" {
return models.ErrDatastoreEmptyAppID return models.ErrMissingAppID
} }
return v.LogStore.InsertCall(ctx, call) return v.LogStore.InsertCall(ctx, call)
} }
@@ -54,7 +54,7 @@ func (v *validator) GetCall(ctx context.Context, appID, callID string) (*models.
return nil, models.ErrDatastoreEmptyCallID return nil, models.ErrDatastoreEmptyCallID
} }
if appID == "" { if appID == "" {
return nil, models.ErrDatastoreEmptyAppID return nil, models.ErrMissingAppID
} }
return v.LogStore.GetCall(ctx, appID, callID) return v.LogStore.GetCall(ctx, appID, callID)
} }

View File

@@ -1,6 +1,7 @@
package models package models
import ( import (
"errors"
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
@@ -9,7 +10,50 @@ import (
"unicode" "unicode"
"github.com/fnproject/fn/api/common" "github.com/fnproject/fn/api/common"
"github.com/fnproject/fn/api/id" )
var (
ErrAppsMissingID = err{
code: http.StatusBadRequest,
error: errors.New("Missing app ID"),
}
ErrAppIDProvided = err{
code: http.StatusBadRequest,
error: errors.New("App ID cannot be supplied on create"),
}
ErrAppsIDMismatch = err{
code: http.StatusBadRequest,
error: errors.New("App ID in path does not match ID in body"),
}
ErrAppsMissingName = err{
code: http.StatusBadRequest,
error: errors.New("Missing app name"),
}
ErrAppsTooLongName = err{
code: http.StatusBadRequest,
error: fmt.Errorf("App name must be %v characters or less", maxAppName),
}
ErrAppsInvalidName = err{
code: http.StatusBadRequest,
error: errors.New("Invalid app name"),
}
ErrAppsAlreadyExists = err{
code: http.StatusConflict,
error: errors.New("App already exists"),
}
ErrAppsMissingNew = err{
code: http.StatusBadRequest,
error: errors.New("Missing new application"),
}
ErrAppsNameImmutable = err{
code: http.StatusConflict,
error: errors.New("Could not update - name is immutable"),
}
ErrAppsNotFound = err{
code: http.StatusNotFound,
error: errors.New("App not found"),
}
) )
type App struct { type App struct {
@@ -22,25 +66,10 @@ type App struct {
UpdatedAt common.DateTime `json:"updated_at,omitempty" db:"updated_at"` UpdatedAt common.DateTime `json:"updated_at,omitempty" db:"updated_at"`
} }
func (a *App) SetDefaults() {
if time.Time(a.CreatedAt).IsZero() {
a.CreatedAt = common.DateTime(time.Now())
}
if time.Time(a.UpdatedAt).IsZero() {
a.UpdatedAt = common.DateTime(time.Now())
}
if a.Config == nil {
// keeps the json from being nil
a.Config = map[string]string{}
}
if a.ID == "" {
a.ID = id.New().String()
}
}
func (a *App) Validate() error { func (a *App) Validate() error {
if a.Name == "" { if a.Name == "" {
return ErrAppsMissingName return ErrMissingName
} }
if len(a.Name) > maxAppName { if len(a.Name) > maxAppName {
return ErrAppsTooLongName return ErrAppsTooLongName
@@ -147,7 +176,6 @@ func (e ErrInvalidSyslog) Error() string { return string(e) }
// AppFilter is the filter used for querying apps // AppFilter is the filter used for querying apps
type AppFilter struct { type AppFilter struct {
Name string
// NameIn will filter by all names in the list (IN query) // NameIn will filter by all names in the list (IN query)
NameIn []string NameIn []string
PerPage int PerPage int

View File

@@ -3,13 +3,10 @@ package models
import ( import (
"context" "context"
"io" "io"
"github.com/jmoiron/sqlx"
) )
type Datastore interface { type Datastore interface {
// GetAppByID gets an App by ID. // GetAppByID gets an App by ID.
// Returns ErrDatastoreEmptyAppID for empty appID.
// Returns ErrAppsNotFound if no app is found. // Returns ErrAppsNotFound if no app is found.
GetAppByID(ctx context.Context, appID string) (*App, error) GetAppByID(ctx context.Context, appID string) (*App, error)
@@ -59,8 +56,42 @@ type Datastore interface {
// ErrDatastoreEmptyRoutePath when routePath is empty. Returns ErrRoutesNotFound when no route exists. // ErrDatastoreEmptyRoutePath when routePath is empty. Returns ErrRoutesNotFound when no route exists.
RemoveRoute(ctx context.Context, appID, routePath string) error RemoveRoute(ctx context.Context, appID, routePath string) error
// GetDatabase returns the underlying sqlx database implementation // InsertFn inserts a new function if one does not exist, applying any defaults necessary,
GetDatabase() *sqlx.DB InsertFn(ctx context.Context, fn *Fn) (*Fn, error)
// UpdateFn updates a function that exists under the same id.
// ErrMissingName is func.Name is empty.
UpdateFn(ctx context.Context, fn *Fn) (*Fn, error)
// GetFns returns a list of funcs, applying any additional filters provided.
GetFns(ctx context.Context, filter *FnFilter) ([]*Fn, error)
// GetFnByID returns a function by ID. Returns ErrDatastoreEmptyFnID if fnID is empty.
// Returns ErrFnsNotFound if a fn is not found.
GetFnByID(ctx context.Context, fnID string) (*Fn, error)
// RemoveFn removes a function. Returns ErrDatastoreEmptyFnID if fnID is empty.
// Returns ErrFnsNotFound if a func is not found.
RemoveFn(ctx context.Context, fnID string) error
// InsertTrigger inserts a trigger. Returns ErrDatastoreEmptyTrigger when trigger is nil, and specific errors for each field
// Returns ErrTriggerAlreadyExists if the exact apiID, fnID, source, type combination already exists
InsertTrigger(ctx context.Context, trigger *Trigger) (*Trigger, error)
//UpdateTrigger updates a trigger object in the data store
UpdateTrigger(ctx context.Context, trigger *Trigger) (*Trigger, error)
// Removes a Trigger. Returns field specific errors if they are empty.
// Returns nil if successful
RemoveTrigger(ctx context.Context, triggerID string) error
// GetTriggerByID gets a trigger by it's id.
// Returns ErrTriggerNotFound when no matching trigger is found
GetTriggerByID(ctx context.Context, triggerID string) (*Trigger, error)
// GetTriggers gets a list of triggers that match the specified filter
// Return ErrDatastoreEmptyAppId if no AppID set in the filter
GetTriggers(ctx context.Context, filter *TriggerFilter) ([]*Trigger, error)
// implements io.Closer to shutdown // implements io.Closer to shutdown
io.Closer io.Closer

View File

@@ -8,7 +8,9 @@ import (
// TODO we can put constants all in this file too // TODO we can put constants all in this file too
const ( const (
maxAppName = 30 maxAppName = 30
maxFnName = 30
MaxTriggerName = 30
) )
var ( var (
@@ -24,62 +26,49 @@ var (
code: http.StatusServiceUnavailable, code: http.StatusServiceUnavailable,
error: errors.New("Timed out - server too busy"), error: errors.New("Timed out - server too busy"),
} }
ErrAppsMissingName = err{
ErrMissingID = err{
code: http.StatusBadRequest, code: http.StatusBadRequest,
error: errors.New("Missing app name"), error: errors.New("Missing ID")}
}
ErrAppsTooLongName = err{ ErrMissingAppID = err{
code: http.StatusBadRequest, code: http.StatusBadRequest,
error: fmt.Errorf("App name must be %v characters or less", maxAppName), error: errors.New("Missing App ID")}
} ErrMissingName = err{
ErrAppsInvalidName = err{
code: http.StatusBadRequest, code: http.StatusBadRequest,
error: errors.New("Invalid app name"), error: errors.New("Missing Name")}
}
ErrAppsAlreadyExists = err{ ErrCreatedAtProvided = err{
code: http.StatusConflict,
error: errors.New("App already exists"),
}
ErrAppsMissingNew = err{
code: http.StatusBadRequest, code: http.StatusBadRequest,
error: errors.New("Missing new application"), error: errors.New("Trigger Created At Provided for Create")}
} ErrUpdatedAtProvided = err{
ErrAppsNameImmutable = err{ code: http.StatusBadRequest,
code: http.StatusConflict, error: errors.New("Trigger ID Provided for Create")}
error: errors.New("Could not update - name is immutable"),
}
ErrAppsNotFound = err{
code: http.StatusNotFound,
error: errors.New("App not found"),
}
ErrDeleteAppsWithRoutes = err{
code: http.StatusConflict,
error: errors.New("Cannot remove apps with routes"),
}
ErrDatastoreEmptyApp = err{ ErrDatastoreEmptyApp = err{
code: http.StatusBadRequest, code: http.StatusBadRequest,
error: errors.New("Missing app"), error: errors.New("Missing app"),
} }
ErrDatastoreEmptyAppID = err{
code: http.StatusBadRequest,
error: errors.New("Missing app ID"),
}
ErrDatastoreEmptyRoute = err{
code: http.StatusBadRequest,
error: errors.New("Missing route"),
}
ErrDatastoreEmptyKey = err{
code: http.StatusBadRequest,
error: errors.New("Missing key"),
}
ErrDatastoreEmptyCallID = err{ ErrDatastoreEmptyCallID = err{
code: http.StatusBadRequest, code: http.StatusBadRequest,
error: errors.New("Missing call ID"), error: errors.New("Missing call ID"),
} }
ErrDatastoreEmptyFn = err{
code: http.StatusBadRequest,
error: errors.New("Missing Fn"),
}
ErrDatastoreEmptyFnID = err{
code: http.StatusBadRequest,
error: errors.New("Missing Fn ID"),
}
ErrInvalidPayload = err{ ErrInvalidPayload = err{
code: http.StatusBadRequest, code: http.StatusBadRequest,
error: errors.New("Invalid payload"), error: errors.New("Invalid payload"),
} }
ErrDatastoreEmptyRoute = err{
code: http.StatusBadRequest,
error: errors.New("Missing route"),
}
ErrRoutesAlreadyExists = err{ ErrRoutesAlreadyExists = err{
code: http.StatusConflict, code: http.StatusConflict,
error: errors.New("Route already exists"), error: errors.New("Route already exists"),
@@ -128,10 +117,6 @@ var (
code: http.StatusBadRequest, code: http.StatusBadRequest,
error: errors.New("Missing route Path"), error: errors.New("Missing route Path"),
} }
ErrRoutesMissingType = err{
code: http.StatusBadRequest,
error: errors.New("Missing route Type"),
}
ErrPathMalformed = err{ ErrPathMalformed = err{
code: http.StatusBadRequest, code: http.StatusBadRequest,
error: errors.New("Path malformed"), error: errors.New("Path malformed"),
@@ -156,10 +141,19 @@ var (
code: http.StatusBadRequest, code: http.StatusBadRequest,
error: fmt.Errorf("memory value is out of range. It should be between 0 and %d", RouteMaxMemory), error: fmt.Errorf("memory value is out of range. It should be between 0 and %d", RouteMaxMemory),
} }
ErrInvalidMemory = err{
code: http.StatusBadRequest,
error: fmt.Errorf("memory value is out of range. It should be between 0 and %d", RouteMaxMemory),
}
ErrCallNotFound = err{ ErrCallNotFound = err{
code: http.StatusNotFound, code: http.StatusNotFound,
error: errors.New("Call not found"), error: errors.New("Call not found"),
} }
ErrInvalidCPUs = err{
code: http.StatusBadRequest,
error: fmt.Errorf("Cpus is invalid. Value should be either between [%.3f and %.3f] or [%dm and %dm] milliCPU units",
float64(MinMilliCPUs)/1000.0, float64(MaxMilliCPUs)/1000.0, MinMilliCPUs, MaxMilliCPUs),
}
ErrCallLogNotFound = err{ ErrCallLogNotFound = err{
code: http.StatusNotFound, code: http.StatusNotFound,
error: errors.New("Call log not found"), error: errors.New("Call log not found"),
@@ -176,11 +170,6 @@ var (
code: http.StatusNotFound, code: http.StatusNotFound,
error: errors.New("Path not found"), error: errors.New("Path not found"),
} }
ErrInvalidCPUs = err{
code: http.StatusBadRequest,
error: fmt.Errorf("Cpus is invalid. Value should be either between [%.3f and %.3f] or [%dm and %dm] milliCPU units",
float64(MinMilliCPUs)/1000.0, float64(MaxMilliCPUs)/1000.0, MinMilliCPUs, MaxMilliCPUs),
}
ErrFunctionResponseTooBig = err{ ErrFunctionResponseTooBig = err{
code: http.StatusBadGateway, code: http.StatusBadGateway,
error: fmt.Errorf("function response too large"), error: fmt.Errorf("function response too large"),
@@ -240,11 +229,11 @@ func GetAPIErrorCode(e error) int {
return 0 return 0
} }
// Error uniform error output // ErrorWrapper uniform error output (v1) only
type Error struct { type ErrorWrapper struct {
Error *ErrorBody `json:"error,omitempty"` Error *Error `json:"error,omitempty"`
} }
func (m *Error) Validate() error { func (m *ErrorWrapper) Validate() error {
return nil return nil
} }

View File

@@ -1,11 +1,11 @@
package models package models
type ErrorBody struct { type Error struct {
Message string `json:"message,omitempty"` Message string `json:"message,omitempty"`
Fields string `json:"fields,omitempty"` Fields string `json:"fields,omitempty"`
} }
// Validate validates this error body // Validate validates this error body
func (m *ErrorBody) Validate() error { func (m *Error) Validate() error {
return nil return nil
} }

281
api/models/fn.go Normal file
View File

@@ -0,0 +1,281 @@
package models
import (
"errors"
"fmt"
"net/http"
"net/url"
"time"
"github.com/fnproject/fn/api/common"
)
var (
// these are vars so that they can be configured. these apply
// across function & trigger (resource config)
MaxMemory uint64 = 8 * 1024 // 8GB
MaxTimeout int32 = 300 // 5m
MaxIdleTimeout int32 = 3600 // 1h
ErrFnsIDMismatch = err{
code: http.StatusBadRequest,
error: errors.New("Fn ID in path does not match that in body"),
}
ErrFnsIDProvided = err{
code: http.StatusBadRequest,
error: errors.New("ID cannot be provided for Fn creation"),
}
ErrFnsMissingID = err{
code: http.StatusBadRequest,
error: errors.New("Missing Fn ID"),
}
ErrFnsMissingName = err{
code: http.StatusBadRequest,
error: errors.New("Missing Fn name"),
}
ErrFnsInvalidName = err{
code: http.StatusBadRequest,
error: errors.New("name must be a valid string"),
}
ErrFnsTooLongName = err{
code: http.StatusBadRequest,
error: fmt.Errorf("Fn name must be %v characters or less", maxFnName),
}
ErrFnsMissingAppID = err{
code: http.StatusBadRequest,
error: errors.New("Missing AppID on Fn"),
}
ErrFnsMissingImage = err{
code: http.StatusBadRequest,
error: errors.New("Missing image on Fn"),
}
ErrFnsInvalidFormat = err{
code: http.StatusBadRequest,
error: errors.New("Invalid format on Fn"),
}
ErrFnsInvalidTimeout = err{
code: http.StatusBadRequest,
error: fmt.Errorf("timeout value is out of range, must be between 0 and %d", MaxTimeout),
}
ErrFnsInvalidIdleTimeout = err{
code: http.StatusBadRequest,
error: fmt.Errorf("idle_timeout value is out of range, must be between 0 and %d", MaxIdleTimeout),
}
ErrFnsNotFound = err{
code: http.StatusNotFound,
error: errors.New("Fn not found"),
}
ErrFnsExists = err{
code: http.StatusConflict,
error: errors.New("Fn with specified name already exists"),
}
)
// Fn contains information about a function configuration.
type Fn struct {
// ID is the generated resource id.
ID string `json:"id" db:"id"`
// Name is a user provided name for this fn.
Name string `json:"name" db:"name"`
// AppID is the name of the app this fn belongs to.
AppID string `json:"app_id" db:"app_id"`
// Image is the fully qualified container registry address to execute.
// examples: hub.docker.io/me/myfunc, me/myfunc, me/func:0.0.1
Image string `json:"image" db:"image"`
// ResourceConfig specifies resource constraints.
ResourceConfig // embed (TODO or not?)
// Config is the configuration passed to a function at execution time.
Config Config `json:"config" db:"config"`
// Annotations allow additional configuration of a function, these are not passed to the function.
Annotations Annotations `json:"annotations,omitempty" db:"annotations"`
// CreatedAt is the UTC timestamp when this function was created.
CreatedAt common.DateTime `json:"created_at,omitempty" db:"created_at"`
// UpdatedAt is the UTC timestamp of the last time this func was modified.
UpdatedAt common.DateTime `json:"updated_at,omitempty" db:"updated_at"`
// TODO wish to kill but not yet ?
// Format is the container protocol the function will accept,
// may be one of: json | http | cloudevent | default
Format string `json:"format" db:"format"`
}
// ResourceConfig specified resource constraints imposed on a function execution.
type ResourceConfig struct {
// Memory is the amount of memory allotted, in MB.
Memory uint64 `json:"memory,omitempty" db:"memory"`
// Timeout is the max execution time for a function, in seconds.
// TODO this should probably be milliseconds?
Timeout int32 `json:"timeout,omitempty" db:"timeout"`
// IdleTimeout is the
// TODO this should probably be milliseconds
IdleTimeout int32 `json:"idle_timeout,omitempty" db:"idle_timeout"`
}
// SetCreated sets zeroed field to defaults.
func (f *Fn) SetDefaults() {
if f.Memory == 0 {
f.Memory = DefaultMemory
}
if f.Format == "" {
f.Format = FormatDefault
}
if f.Config == nil {
// keeps the json from being nil
f.Config = map[string]string{}
}
if f.Timeout == 0 {
f.Timeout = DefaultTimeout
}
if f.IdleTimeout == 0 {
f.IdleTimeout = DefaultIdleTimeout
}
if time.Time(f.CreatedAt).IsZero() {
f.CreatedAt = common.DateTime(time.Now())
}
if time.Time(f.UpdatedAt).IsZero() {
f.UpdatedAt = common.DateTime(time.Now())
}
}
// Validate validates all field values, returning the first error, if any.
func (f *Fn) Validate() error {
if f.Name == "" {
return ErrFnsMissingName
}
if len(f.Name) > maxFnName {
return ErrFnsTooLongName
}
if url.PathEscape(f.Name) != f.Name {
return ErrFnsInvalidName
}
if f.AppID == "" {
return ErrFnsMissingAppID
}
if f.Image == "" {
return ErrFnsMissingImage
}
switch f.Format {
case FormatDefault, FormatHTTP, FormatJSON, FormatCloudEvent:
default:
return ErrFnsInvalidFormat
}
if f.Timeout <= 0 || f.Timeout > MaxTimeout {
return ErrFnsInvalidTimeout
}
if f.IdleTimeout <= 0 || f.IdleTimeout > MaxIdleTimeout {
return ErrFnsInvalidIdleTimeout
}
if f.Memory < 1 || f.Memory > MaxMemory {
return ErrInvalidMemory
}
return f.Annotations.Validate()
}
func (f *Fn) Clone() *Fn {
clone := new(Fn)
*clone = *f // shallow copy
// now deep copy the maps
if f.Config != nil {
clone.Config = make(Config, len(f.Config))
for k, v := range f.Config {
clone.Config[k] = v
}
}
if f.Annotations != nil {
clone.Annotations = make(Annotations, len(f.Annotations))
for k, v := range f.Annotations {
// TODO technically, we need to deep copy the bytes
clone.Annotations[k] = v
}
}
return clone
}
func (f1 *Fn) Equals(f2 *Fn) bool {
// start off equal, check equivalence of each field.
// the RHS of && won't eval if eq==false so config/headers checking is lazy
eq := true
eq = eq && f1.ID == f2.ID
eq = eq && f1.Name == f2.Name
eq = eq && f1.AppID == f2.AppID
eq = eq && f1.Image == f2.Image
eq = eq && f1.Memory == f2.Memory
eq = eq && f1.Format == f2.Format
eq = eq && f1.Timeout == f2.Timeout
eq = eq && f1.IdleTimeout == f2.IdleTimeout
eq = eq && f1.Config.Equals(f2.Config)
eq = eq && f1.Annotations.Equals(f2.Annotations)
// NOTE: datastore tests are not very fun to write with timestamp checks,
// and these are not values the user may set so we kind of don't care.
//eq = eq && time.Time(f1.CreatedAt).Equal(time.Time(f2.CreatedAt))
//eq = eq && time.Time(f2.UpdatedAt).Equal(time.Time(f2.UpdatedAt))
return eq
}
// Update updates fields in f with non-zero field values from new, and sets
// updated_at if any of the fields change. 0-length slice Header values, and
// empty-string Config values trigger removal of map entry.
func (f *Fn) Update(patch *Fn) {
original := f.Clone()
if patch.Image != "" {
f.Image = patch.Image
}
if patch.Memory != 0 {
f.Memory = patch.Memory
}
if patch.Timeout != 0 {
f.Timeout = patch.Timeout
}
if patch.IdleTimeout != 0 {
f.IdleTimeout = patch.IdleTimeout
}
if patch.Format != "" {
f.Format = patch.Format
}
if patch.Config != nil {
if f.Config == nil {
f.Config = make(Config)
}
for k, v := range patch.Config {
if v == "" {
delete(f.Config, k)
} else {
f.Config[k] = v
}
}
}
f.Annotations = f.Annotations.MergeChange(patch.Annotations)
if !f.Equals(original) {
f.UpdatedAt = common.DateTime(time.Now())
}
}
type FnFilter struct {
AppID string // this is exact match
Name string //exact match
Cursor string
PerPage int
}

View File

@@ -25,7 +25,7 @@ type LogStore interface {
InsertCall(ctx context.Context, call *Call) error InsertCall(ctx context.Context, call *Call) error
// GetCall returns a call at a certain id and app name. // GetCall returns a call at a certain id and app name.
GetCall(ctx context.Context, appName, callID string) (*Call, error) GetCall(ctx context.Context, appId, callID string) (*Call, error)
// GetCalls returns a list of calls that satisfy the given CallFilter. If no // GetCalls returns a list of calls that satisfy the given CallFilter. If no
// calls exist, an empty list and a nil error are returned. // calls exist, an empty list and a nil error are returned.

View File

@@ -17,7 +17,6 @@ const (
MaxSyncTimeout = 120 // 2 minutes MaxSyncTimeout = 120 // 2 minutes
MaxAsyncTimeout = 3600 // 1 hour MaxAsyncTimeout = 3600 // 1 hour
MaxIdleTimeout = MaxAsyncTimeout
) )
var RouteMaxMemory = uint64(8 * 1024) var RouteMaxMemory = uint64(8 * 1024)
@@ -73,13 +72,6 @@ func (r *Route) SetDefaults() {
r.IdleTimeout = DefaultIdleTimeout r.IdleTimeout = DefaultIdleTimeout
} }
if time.Time(r.CreatedAt).IsZero() {
r.CreatedAt = common.DateTime(time.Now())
}
if time.Time(r.UpdatedAt).IsZero() {
r.UpdatedAt = common.DateTime(time.Now())
}
} }
// Validate validates all field values, returning the first error, if any. // Validate validates all field values, returning the first error, if any.

180
api/models/trigger.go Normal file
View File

@@ -0,0 +1,180 @@
package models
import (
"errors"
"fmt"
"net/http"
"time"
"unicode"
"github.com/fnproject/fn/api/common"
)
type Trigger struct {
ID string `json:"id" db:"id"`
Name string `json:"name" db:"name"`
AppID string `json:"app_id" db:"app_id"`
FnID string `json:"fn_id" db:"fn_id"`
CreatedAt common.DateTime `json:"created_at,omitempty" db:"created_at"`
UpdatedAt common.DateTime `json:"updated_at,omitempty" db:"updated_at"`
Type string `json:"type" db:"type"`
Source string `json:"source" db:"source"`
Annotations Annotations `json:"annotations,omitempty" db:"annotations"`
}
func (t *Trigger) Equals(t2 *Trigger) bool {
eq := true
eq = eq && t.ID == t2.ID
eq = eq && t.Name == t2.Name
eq = eq && t.AppID == t2.AppID
eq = eq && t.FnID == t2.FnID
eq = eq && t.Type == t2.Type
eq = eq && t.Source == t2.Source
eq = eq && t.Annotations.Equals(t2.Annotations)
return eq
}
var triggerTypes = []string{"http"}
func ValidTriggerTypes() []string {
return triggerTypes
}
func ValidTriggerType(a string) bool {
for _, b := range triggerTypes {
if b == a {
return true
}
}
return false
}
var (
ErrTriggerIDProvided = err{
code: http.StatusBadRequest,
error: errors.New("ID cannot be provided for Trigger creation"),
}
ErrTriggerIDMismatch = err{
code: http.StatusBadRequest,
error: errors.New("ID in path does not match ID in body"),
}
ErrTriggerMissingName = err{
code: http.StatusBadRequest,
error: errors.New("Missing name on Trigger")}
ErrTriggerTooLongName = err{
code: http.StatusBadRequest,
error: fmt.Errorf("Trigger name must be %v characters or less", MaxTriggerName)}
ErrTriggerInvalidName = err{
code: http.StatusBadRequest,
error: errors.New("Invalid name for Trigger")}
ErrTriggerMissingAppID = err{
code: http.StatusBadRequest,
error: errors.New("Missing App ID on Trigger")}
ErrTriggerMissingFnID = err{
code: http.StatusBadRequest,
error: errors.New("Missing Fn ID on Trigger")}
ErrTriggerFnIDNotSameApp = err{
code: http.StatusBadRequest,
error: errors.New("Invalid Fn ID - not owned by specified app")}
ErrTriggerTypeUnknown = err{
code: http.StatusBadRequest,
error: errors.New("Trigger Type Not Supported")}
ErrTriggerMissingSource = err{
code: http.StatusBadRequest,
error: errors.New("Missing Trigger Source")}
ErrTriggerNotFound = err{
code: http.StatusNotFound,
error: errors.New("Trigger not found")}
ErrTriggerExists = err{
code: http.StatusConflict,
error: errors.New("Trigger already exists")}
)
func (t *Trigger) Validate() error {
if t.Name == "" {
return ErrTriggerMissingName
}
if t.AppID == "" {
return ErrTriggerMissingAppID
}
if len(t.Name) > MaxTriggerName {
return ErrTriggerTooLongName
}
for _, c := range t.Name {
if !(unicode.IsLetter(c) || unicode.IsNumber(c) || c == '_' || c == '-') {
return ErrTriggerInvalidName
}
}
if t.FnID == "" {
return ErrTriggerMissingFnID
}
if !ValidTriggerType(t.Type) {
return ErrTriggerTypeUnknown
}
if t.Source == "" {
return ErrTriggerMissingSource
}
err := t.Annotations.Validate()
if err != nil {
return err
}
return nil
}
func (t *Trigger) Clone() *Trigger {
clone := new(Trigger)
*clone = *t // shallow copy
if t.Annotations != nil {
clone.Annotations = make(Annotations, len(t.Annotations))
for k, v := range t.Annotations {
// TODO technically, we need to deep copy the bytes
clone.Annotations[k] = v
}
}
return clone
}
func (t *Trigger) Update(patch *Trigger) {
original := t.Clone()
if patch.AppID != "" {
t.AppID = patch.AppID
}
if patch.FnID != "" {
t.FnID = patch.FnID
}
if patch.Name != "" {
t.Name = patch.Name
}
if patch.Source != "" {
t.Source = patch.Source
}
t.Annotations = t.Annotations.MergeChange(patch.Annotations)
if !t.Equals(original) {
t.UpdatedAt = common.DateTime(time.Now())
}
}
type TriggerFilter struct {
AppID string // this is exact match
FnID string // this is exact match
Name string // exact match
Cursor string
PerPage int
}

View File

@@ -0,0 +1,51 @@
package models
import (
"encoding/json"
"testing"
)
var openEmptyJson = `{"id":"","name":"","app_id":"","fn_id":"","created_at":"0001-01-01T00:00:00.000Z","updated_at":"0001-01-01T00:00:00.000Z","type":"","source":""`
var triggerJsonCases = []struct {
val *Trigger
valString string
}{
{val: &Trigger{}, valString: openEmptyJson + "}"},
}
func TestTriggerJsonMarshalling(t *testing.T) {
for _, tc := range triggerJsonCases {
v, err := json.Marshal(tc.val)
if err != nil {
t.Fatalf("Failed to marshal json into %s: %v", tc.valString, err)
}
if string(v) != tc.valString {
t.Errorf("Invalid trigger value, expected %s, got %s", tc.valString, string(v))
}
}
}
var httpTrigger = &Trigger{Name: "name", AppID: "foo", FnID: "bar", Type: "http", Source: "baz"}
var invalidTrigger = &Trigger{Name: "name", AppID: "foo", FnID: "bar", Type: "error", Source: "baz"}
var triggerValidateCases = []struct {
val *Trigger
valid bool
}{
{val: &Trigger{}, valid: false},
{val: invalidTrigger, valid: false},
{val: httpTrigger, valid: true},
}
func TestTriggerValidate(t *testing.T) {
for _, tc := range triggerValidateCases {
v := tc.val.Validate()
if v != nil && tc.valid {
t.Errorf("Expected Trigger to be valid, but err (%s) returned. Trigger: %#v", v, tc.val)
}
if v == nil && !tc.valid {
t.Errorf("Expected Trigger to be invalid, but no err returned. Trigger: %#v", tc.val)
}
}
}

View File

@@ -10,9 +10,9 @@ import (
func (s *Server) handleAppCreate(c *gin.Context) { func (s *Server) handleAppCreate(c *gin.Context) {
ctx := c.Request.Context() ctx := c.Request.Context()
var wapp models.AppWrapper app := &models.App{}
err := c.BindJSON(&wapp) err := c.BindJSON(app)
if err != nil { if err != nil {
if models.IsAPIError(err) { if models.IsAPIError(err) {
handleErrorResponse(c, err) handleErrorResponse(c, err)
@@ -22,17 +22,11 @@ func (s *Server) handleAppCreate(c *gin.Context) {
return return
} }
app := wapp.App
if app == nil {
handleErrorResponse(c, models.ErrAppsMissingNew)
return
}
app, err = s.datastore.InsertApp(ctx, app) app, err = s.datastore.InsertApp(ctx, app)
if err != nil { if err != nil {
handleErrorResponse(c, err) handleErrorResponse(c, err)
return return
} }
c.JSON(http.StatusOK, appResponse{"App successfully created", app}) c.JSON(http.StatusOK, app)
} }

View File

@@ -10,11 +10,11 @@ import (
func (s *Server) handleAppDelete(c *gin.Context) { func (s *Server) handleAppDelete(c *gin.Context) {
ctx := c.Request.Context() ctx := c.Request.Context()
err := s.datastore.RemoveApp(ctx, c.MustGet(api.AppID).(string)) err := s.datastore.RemoveApp(ctx, c.Param(api.ParamAppID))
if err != nil { if err != nil {
handleErrorResponse(c, err) handleErrorResponse(c, err)
return return
} }
c.JSON(http.StatusOK, gin.H{"message": "App deleted"}) c.String(http.StatusNoContent, "")
} }

View File

@@ -7,26 +7,15 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
func (s *Server) handleAppGetByName(c *gin.Context) { func (s *Server) handleAppGet(c *gin.Context) {
ctx := c.Request.Context() ctx := c.Request.Context()
app, err := s.datastore.GetAppByID(ctx, c.MustGet(api.AppID).(string)) appId := c.Param(api.ParamAppID)
app, err := s.datastore.GetAppByID(ctx, appId)
if err != nil { if err != nil {
handleErrorResponse(c, err) handleErrorResponse(c, err)
return return
} }
c.JSON(http.StatusOK, appResponse{"Successfully loaded app", app}) c.JSON(http.StatusOK, app)
}
func (s *Server) handleAppGetByID(c *gin.Context) {
ctx := c.Request.Context()
app, err := s.datastore.GetAppByID(ctx, c.Param(api.CApp))
if err != nil {
handleErrorResponse(c, err)
return
}
c.JSON(http.StatusOK, appResponse{"Successfully loaded app", app})
} }

View File

@@ -13,6 +13,10 @@ func (s *Server) handleAppList(c *gin.Context) {
filter := &models.AppFilter{} filter := &models.AppFilter{}
filter.Cursor, filter.PerPage = pageParams(c, true) filter.Cursor, filter.PerPage = pageParams(c, true)
name := c.Query("name")
if name != "" {
filter.NameIn = []string{name}
}
apps, err := s.datastore.GetApps(ctx, filter) apps, err := s.datastore.GetApps(ctx, filter)
if err != nil { if err != nil {
@@ -26,9 +30,8 @@ func (s *Server) handleAppList(c *gin.Context) {
nextCursor = base64.RawURLEncoding.EncodeToString(last) nextCursor = base64.RawURLEncoding.EncodeToString(last)
} }
c.JSON(http.StatusOK, appsResponse{ c.JSON(http.StatusOK, appListResponse{
Message: "Successfully listed applications",
NextCursor: nextCursor, NextCursor: nextCursor,
Apps: apps, Items: apps,
}) })
} }

View File

@@ -11,6 +11,7 @@ import (
"testing" "testing"
"time" "time"
"fmt"
"github.com/fnproject/fn/api/datastore" "github.com/fnproject/fn/api/datastore"
"github.com/fnproject/fn/api/logs" "github.com/fnproject/fn/api/logs"
"github.com/fnproject/fn/api/models" "github.com/fnproject/fn/api/models"
@@ -46,21 +47,21 @@ func TestAppCreate(t *testing.T) {
expectedError error expectedError error
}{ }{
// errors // errors
{datastore.NewMock(), logs.NewMock(), "/v1/apps", ``, http.StatusBadRequest, models.ErrInvalidJSON}, {datastore.NewMock(), logs.NewMock(), "/v2/apps", ``, http.StatusBadRequest, models.ErrInvalidJSON},
{datastore.NewMock(), logs.NewMock(), "/v1/apps", `{}`, http.StatusBadRequest, models.ErrAppsMissingNew}, {datastore.NewMock(), logs.NewMock(), "/v2/apps", `{}`, http.StatusBadRequest, models.ErrMissingName},
{datastore.NewMock(), logs.NewMock(), "/v1/apps", `{ "name": "Test" }`, http.StatusBadRequest, models.ErrAppsMissingNew}, {datastore.NewMock(), logs.NewMock(), "/v2/apps", `{"name": "app", "id":"badId"}`, http.StatusBadRequest, models.ErrAppIDProvided},
{datastore.NewMock(), logs.NewMock(), "/v1/apps", `{ "app": { "name": "" } }`, http.StatusBadRequest, models.ErrAppsMissingName}, {datastore.NewMock(), logs.NewMock(), "/v2/apps", `{ "name": "" }`, http.StatusBadRequest, models.ErrMissingName},
{datastore.NewMock(), logs.NewMock(), "/v1/apps", `{ "app": { "name": "1234567890123456789012345678901" } }`, http.StatusBadRequest, models.ErrAppsTooLongName}, {datastore.NewMock(), logs.NewMock(), "/v2/apps", `{"name": "1234567890123456789012345678901" }`, http.StatusBadRequest, models.ErrAppsTooLongName},
{datastore.NewMock(), logs.NewMock(), "/v1/apps", `{ "app": { "name": "&&%@!#$#@$" } }`, http.StatusBadRequest, models.ErrAppsInvalidName}, {datastore.NewMock(), logs.NewMock(), "/v2/apps", `{ "name": "&&%@!#$#@$" }`, http.StatusBadRequest, models.ErrAppsInvalidName},
{datastore.NewMock(), logs.NewMock(), "/v1/apps", `{ "app": { "name": "&&%@!#$#@$" } }`, http.StatusBadRequest, models.ErrAppsInvalidName}, {datastore.NewMock(), logs.NewMock(), "/v2/apps", `{ "name": "app", "annotations" : { "":"val" }}`, http.StatusBadRequest, models.ErrInvalidAnnotationKey},
{datastore.NewMock(), logs.NewMock(), "/v1/apps", `{ "app": { "name": "app", "annotations" : { "":"val" }}}`, http.StatusBadRequest, models.ErrInvalidAnnotationKey}, {datastore.NewMock(), logs.NewMock(), "/v2/apps", `{"name": "app", "annotations" : { "key":"" }}`, http.StatusBadRequest, models.ErrInvalidAnnotationValue},
{datastore.NewMock(), logs.NewMock(), "/v1/apps", `{ "app": { "name": "app", "annotations" : { "key":"" }}}`, http.StatusBadRequest, models.ErrInvalidAnnotationValue}, {datastore.NewMock(), logs.NewMock(), "/v2/apps", `{ "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"}}`, http.StatusBadRequest, errors.New(`invalid syslog url: "yo"`)}, {datastore.NewMock(), logs.NewMock(), "/v2/apps", `{"name": "app", "syslog_url":"yo://sup.com:1"}`, http.StatusBadRequest, errors.New(`invalid syslog url: "yo://sup.com:1"`)},
{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 // success
{datastore.NewMock(), logs.NewMock(), "/v1/apps", `{ "app": { "name": "teste" } }`, http.StatusOK, nil}, {datastore.NewMock(), logs.NewMock(), "/v2/apps", `{ "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(), "/v2/apps", `{ "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}, {datastore.NewMock(), logs.NewMock(), "/v2/apps", `{"name": "teste", "syslog_url":"tcp://example.com:443" } `, http.StatusOK, nil},
{datastore.NewMockInit([]*models.App{&models.App{ID: "appid", Name: "teste"}}), logs.NewMock(), "/v2/apps", `{ "name": "teste" }`, http.StatusConflict, models.ErrAppsAlreadyExists},
} { } {
rnr, cancel := testRunner(t) rnr, cancel := testRunner(t)
srv := testServer(test.mock, &mqs.Mock{}, test.logDB, rnr, ServerTypeFull) srv := testServer(test.mock, &mqs.Mock{}, test.logDB, rnr, ServerTypeFull)
@@ -77,22 +78,20 @@ func TestAppCreate(t *testing.T) {
if test.expectedError != nil { if test.expectedError != nil {
resp := getErrorResponse(t, rec) resp := getErrorResponse(t, rec)
if !strings.Contains(resp.Error.Message, test.expectedError.Error()) { if !strings.Contains(resp.Message, test.expectedError.Error()) {
t.Errorf("Test %d: Expected error message to have `%s` but got `%s`", t.Errorf("Test %d: Expected error message to have `%s` but got `%s`",
i, test.expectedError.Error(), resp.Error.Message) i, test.expectedError.Error(), resp.Message)
} }
} }
if test.expectedCode == http.StatusOK { if test.expectedCode == http.StatusOK {
var awrap models.AppWrapper var app models.App
err := json.NewDecoder(rec.Body).Decode(&awrap) err := json.NewDecoder(rec.Body).Decode(&app)
if err != nil { if err != nil {
t.Log(buf.String()) t.Log(buf.String())
t.Errorf("Test %d: error decoding body for 'ok' json, it was a lie: %v", i, err) 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 // 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)) { if time.Time(app.CreatedAt).Before(time.Now().Add(-1 * time.Hour)) {
t.Log(buf.String()) t.Log(buf.String())
@@ -118,8 +117,8 @@ func TestAppDelete(t *testing.T) {
app := &models.App{ app := &models.App{
Name: "myapp", Name: "myapp",
ID: "appId",
} }
app.SetDefaults()
ds := datastore.NewMockInit([]*models.App{app}) ds := datastore.NewMockInit([]*models.App{app})
for i, test := range []struct { for i, test := range []struct {
ds models.Datastore ds models.Datastore
@@ -129,8 +128,8 @@ func TestAppDelete(t *testing.T) {
expectedCode int expectedCode int
expectedError error expectedError error
}{ }{
{datastore.NewMock(), logs.NewMock(), "/v1/apps/myapp", "", http.StatusNotFound, nil}, {datastore.NewMock(), logs.NewMock(), "/v2/apps/myapp", "", http.StatusNotFound, nil},
{ds, logs.NewMock(), "/v1/apps/myapp", "", http.StatusOK, nil}, {ds, logs.NewMock(), "/v2/apps/appId", "", http.StatusNoContent, nil},
} { } {
rnr, cancel := testRunner(t) rnr, cancel := testRunner(t)
srv := testServer(test.ds, &mqs.Mock{}, test.logDB, rnr, ServerTypeFull) srv := testServer(test.ds, &mqs.Mock{}, test.logDB, rnr, ServerTypeFull)
@@ -145,7 +144,7 @@ func TestAppDelete(t *testing.T) {
if test.expectedError != nil { if test.expectedError != nil {
resp := getErrorResponse(t, rec) resp := getErrorResponse(t, rec)
if !strings.Contains(resp.Error.Message, test.expectedError.Error()) { if !strings.Contains(resp.Message, test.expectedError.Error()) {
t.Errorf("Test %d: Expected error message to have `%s`", t.Errorf("Test %d: Expected error message to have `%s`",
i, test.expectedError.Error()) i, test.expectedError.Error())
} }
@@ -186,12 +185,12 @@ func TestAppList(t *testing.T) {
expectedLen int expectedLen int
nextCursor string nextCursor string
}{ }{
{"/v1/apps?per_page", "", http.StatusOK, nil, 3, ""}, {"/v2/apps?per_page", "", http.StatusOK, nil, 3, ""},
{"/v1/apps?per_page=1", "", http.StatusOK, nil, 1, a1b}, {"/v2/apps?per_page=1", "", http.StatusOK, nil, 1, a1b},
{"/v1/apps?per_page=1&cursor=" + a1b, "", http.StatusOK, nil, 1, a2b}, {"/v2/apps?per_page=1&cursor=" + a1b, "", http.StatusOK, nil, 1, a2b},
{"/v1/apps?per_page=1&cursor=" + a2b, "", http.StatusOK, nil, 1, a3b}, {"/v2/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) {"/v2/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 {"/v2/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) _, rec := routerRequest(t, srv.Router, "GET", test.path, nil)
@@ -203,20 +202,20 @@ func TestAppList(t *testing.T) {
if test.expectedError != nil { if test.expectedError != nil {
resp := getErrorResponse(t, rec) resp := getErrorResponse(t, rec)
if !strings.Contains(resp.Error.Message, test.expectedError.Error()) { if !strings.Contains(resp.Message, test.expectedError.Error()) {
t.Errorf("Test %d: Expected error message to have `%s`", t.Errorf("Test %d: Expected error message to have `%s`",
i, test.expectedError.Error()) i, test.expectedError.Error())
} }
} else { } else {
// normal path // normal path
var resp appsResponse var resp appListResponse
err := json.NewDecoder(rec.Body).Decode(&resp) err := json.NewDecoder(rec.Body).Decode(&resp)
if err != nil { if err != nil {
t.Errorf("Test %d: Expected response body to be a valid json object. err: %v", i, err) t.Errorf("Test %d: Expected response body to be a valid json object. err: %v", i, err)
} }
if len(resp.Apps) != test.expectedLen { if len(resp.Items) != test.expectedLen {
t.Errorf("Test %d: Expected apps length to be %d, but got %d", i, test.expectedLen, len(resp.Apps)) t.Errorf("Test %d: Expected apps length to be %d, but got %d", i, test.expectedLen, len(resp.Items))
} }
if resp.NextCursor != test.nextCursor { if resp.NextCursor != test.nextCursor {
t.Errorf("Test %d: Expected next_cursor to be %s, but got %s", i, test.nextCursor, resp.NextCursor) t.Errorf("Test %d: Expected next_cursor to be %s, but got %s", i, test.nextCursor, resp.NextCursor)
@@ -235,7 +234,11 @@ func TestAppGet(t *testing.T) {
rnr, cancel := testRunner(t) rnr, cancel := testRunner(t)
defer cancel() defer cancel()
ds := datastore.NewMock() app := &models.App{
ID: "appId",
Name: "app",
}
ds := datastore.NewMockInit([]*models.App{app})
fnl := logs.NewMock() fnl := logs.NewMock()
srv := testServer(ds, &mqs.Mock{}, fnl, rnr, ServerTypeFull) srv := testServer(ds, &mqs.Mock{}, fnl, rnr, ServerTypeFull)
@@ -245,7 +248,8 @@ func TestAppGet(t *testing.T) {
expectedCode int expectedCode int
expectedError error expectedError error
}{ }{
{"/v1/apps/myapp", "", http.StatusNotFound, nil}, {"/v2/apps/unknownApp", "", http.StatusNotFound, models.ErrAppsNotFound},
{"/v2/apps/appId", "", http.StatusOK, nil},
} { } {
_, rec := routerRequest(t, srv.Router, "GET", test.path, nil) _, rec := routerRequest(t, srv.Router, "GET", test.path, nil)
@@ -257,7 +261,7 @@ func TestAppGet(t *testing.T) {
if test.expectedError != nil { if test.expectedError != nil {
resp := getErrorResponse(t, rec) resp := getErrorResponse(t, rec)
if !strings.Contains(resp.Error.Message, test.expectedError.Error()) { if !strings.Contains(resp.Message, test.expectedError.Error()) {
t.Errorf("Test %d: Expected error message to have `%s`", t.Errorf("Test %d: Expected error message to have `%s`",
i, test.expectedError.Error()) i, test.expectedError.Error())
} }
@@ -275,8 +279,8 @@ func TestAppUpdate(t *testing.T) {
app := &models.App{ app := &models.App{
Name: "myapp", Name: "myapp",
ID: "appId",
} }
app.SetDefaults()
ds := datastore.NewMockInit([]*models.App{app}) ds := datastore.NewMockInit([]*models.App{app})
for i, test := range []struct { for i, test := range []struct {
@@ -288,68 +292,73 @@ func TestAppUpdate(t *testing.T) {
expectedError error expectedError error
}{ }{
// errors // errors
{ds, logs.NewMock(), "/v1/apps/myapp", ``, http.StatusBadRequest, models.ErrInvalidJSON}, {ds, logs.NewMock(), "/v2/apps/not_app", `{ }`, http.StatusNotFound, models.ErrAppsNotFound},
{ds, logs.NewMock(), "/v2/apps/appId", ``, http.StatusBadRequest, models.ErrInvalidJSON},
// Addresses #380 // Addresses #380
{ds, logs.NewMock(), "/v1/apps/myapp", `{ "app": { "name": "othername" } }`, http.StatusConflict, nil}, {ds, logs.NewMock(), "/v2/apps/appId", `{ "name": "othername" }`, http.StatusConflict, models.ErrAppsNameImmutable},
// success: add/set MD key // success: add/set MD key
{ds, logs.NewMock(), "/v1/apps/myapp", `{ "app": { "annotations": {"k-0" : "val"} } }`, http.StatusOK, nil}, {ds, logs.NewMock(), "/v2/apps/appId", `{ "annotations":{"foo":"bar"}}`, http.StatusOK, nil},
// success // success
{ds, logs.NewMock(), "/v1/apps/myapp", `{ "app": { "config": { "test": "1" } } }`, http.StatusOK, nil}, {ds, logs.NewMock(), "/v2/apps/appId", `{ "config": { "test": "1" } }`, http.StatusOK, nil},
// success // success
{ds, logs.NewMock(), "/v1/apps/myapp", `{ "app": { "config": { "test": "1" } } }`, http.StatusOK, nil}, {ds, logs.NewMock(), "/v2/apps/appId", `{ "config": { "test": "1" } }`, http.StatusOK, nil},
// success // success
{ds, logs.NewMock(), "/v1/apps/myapp", `{ "app": { "syslog_url":"tcp://example.com:443" } }`, http.StatusOK, nil}, {ds, logs.NewMock(), "/v2/apps/appId", `{ "syslog_url":"tcp://example.com:443" }`, http.StatusOK, nil},
} { } {
rnr, cancel := testRunner(t) t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
srv := testServer(test.mock, &mqs.Mock{}, test.logDB, rnr, ServerTypeFull) rnr, cancel := testRunner(t)
defer cancel()
srv := testServer(test.mock, &mqs.Mock{}, test.logDB, rnr, ServerTypeFull)
body := bytes.NewBuffer([]byte(test.body)) body := bytes.NewBuffer([]byte(test.body))
_, rec := routerRequest(t, srv.Router, "PATCH", test.path, body) _, rec := routerRequest(t, srv.Router, "PUT", test.path, body)
if rec.Code != test.expectedCode { if rec.Code != test.expectedCode {
t.Errorf("Test %d: Expected status code to be %d but was %d", t.Fatalf("Test %d: Expected status code to be %d but was %d",
i, test.expectedCode, rec.Code) i, test.expectedCode, rec.Code)
}
if test.expectedError != nil {
resp := getErrorResponse(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 if test.expectedError != nil {
// IsZero() doesn't really work, this ensures it's not unset as long as we're not in 1970 fmt.Printf("resp: %s", rec.Body)
if time.Time(app.UpdatedAt).Before(time.Now().Add(-1 * time.Hour)) { resp := getErrorResponse(t, rec)
t.Log(buf.String())
t.Errorf("Test %d: expected updated_at to be set on app, it wasn't: %s", i, app.UpdatedAt) if !strings.Contains(resp.Message, test.expectedError.Error()) {
t.Errorf("Test %d: Expected error message to have `%s` but was `%s`",
i, test.expectedError.Error(), resp.Message)
}
} }
// this isn't perfect, since a PATCH could succeed without updating any if test.expectedCode == http.StatusOK {
// fields (among other reasons), but just don't make a test for that or var app models.App
// special case (the body or smth) to ignore it here! err := json.NewDecoder(rec.Body).Decode(&app)
// this is a decent approximation that the timestamp gets changed if err != nil {
if (time.Time(app.UpdatedAt)).Equal(time.Time(app.CreatedAt)) { t.Log(buf.String())
t.Log(buf.String()) t.Errorf("Test %d: error decoding body for 'ok' json, it was a lie: %v", i, err)
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) }
}
} // IsZero() doesn't really work, this ensures it's not unset as long as we're not in 1970
if time.Time(app.UpdatedAt).Before(time.Now().Add(-1 * time.Hour)) {
t.Log(buf.String())
t.Errorf("Test %d: expected updated_at to be set on app, it wasn't: %s", i, app.UpdatedAt)
}
// this isn't perfect, since a PATCH could succeed without updating any
// fields (among other reasons), but just don't make a test for that or
// special case (the body or smth) to ignore it here!
// this is a decent approximation that the timestamp gets changed
if (time.Time(app.UpdatedAt)).Equal(time.Time(app.CreatedAt)) {
t.Log(buf.String())
t.Errorf("Test %d: expected updated_at to not be the same as created at, it wasn't: %s %s", i, app.CreatedAt, app.UpdatedAt)
}
}
})
cancel()
} }
} }

View File

@@ -11,9 +11,9 @@ import (
func (s *Server) handleAppUpdate(c *gin.Context) { func (s *Server) handleAppUpdate(c *gin.Context) {
ctx := c.Request.Context() ctx := c.Request.Context()
wapp := models.AppWrapper{} app := &models.App{}
err := c.BindJSON(&wapp) err := c.BindJSON(app)
if err != nil { if err != nil {
if models.IsAPIError(err) { if models.IsAPIError(err) {
handleErrorResponse(c, err) handleErrorResponse(c, err)
@@ -23,24 +23,20 @@ func (s *Server) handleAppUpdate(c *gin.Context) {
return return
} }
if wapp.App == nil { id := c.Param(api.ParamAppID)
handleErrorResponse(c, models.ErrAppsMissingNew)
if app.ID == "" {
app.ID = id
}
if app.ID != id {
handleErrorResponse(c, models.ErrAppsIDMismatch)
return return
} }
app, err = s.datastore.UpdateApp(ctx, app)
if wapp.App.Name != "" {
handleErrorResponse(c, models.ErrAppsNameImmutable)
return
}
wapp.App.Name = c.MustGet(api.App).(string)
wapp.App.ID = c.MustGet(api.AppID).(string)
app, err := s.datastore.UpdateApp(ctx, wapp.App)
if err != nil { if err != nil {
handleErrorResponse(c, err) handleErrorResponse(c, err)
return return
} }
c.JSON(http.StatusOK, appResponse{"App successfully updated", app}) c.JSON(http.StatusOK, app)
} }

View File

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

View File

@@ -0,0 +1,21 @@
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"})
}

23
api/server/apps_v1_get.go Normal file
View File

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

View File

@@ -0,0 +1,35 @@
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 = pageParams(c, true)
apps, err := s.datastore.GetApps(ctx, filter)
if err != nil {
handleV1ErrorResponse(c, err)
return
}
var nextCursor string
if len(apps) > 0 && len(apps) == filter.PerPage {
last := []byte(apps[len(apps)-1].Name)
nextCursor = base64.RawURLEncoding.EncodeToString(last)
}
c.JSON(http.StatusOK, appsV1Response{
Message: "Successfully listed applications",
NextCursor: nextCursor,
Apps: apps,
})
}

342
api/server/apps_v1_test.go Normal file
View File

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

View File

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

View File

@@ -10,12 +10,12 @@ import (
func (s *Server) handleCallGet(c *gin.Context) { func (s *Server) handleCallGet(c *gin.Context) {
ctx := c.Request.Context() ctx := c.Request.Context()
callID := c.Param(api.Call) callID := c.Param(api.ParamCallID)
appID := c.MustGet(api.AppID).(string) appID := c.MustGet(api.AppID).(string)
callObj, err := s.logstore.GetCall(ctx, appID, callID) callObj, err := s.logstore.GetCall(ctx, appID, callID)
if err != nil { if err != nil {
handleErrorResponse(c, err) handleV1ErrorResponse(c, err)
return return
} }

View File

@@ -16,13 +16,13 @@ func (s *Server) handleCallList(c *gin.Context) {
var err error var err error
appID := c.MustGet(api.AppID).(string) appID := c.MustGet(api.AppID).(string)
// TODO api.CRoute needs to be escaped probably, since it has '/' a lot // TODO api.ParamRouteName needs to be escaped probably, since it has '/' a lot
filter := models.CallFilter{AppID: appID, Path: c.Query("path")} filter := models.CallFilter{AppID: appID, Path: c.Query("path")}
filter.Cursor, filter.PerPage = pageParams(c, false) // ids are url safe filter.Cursor, filter.PerPage = pageParams(c, false) // ids are url safe
filter.FromTime, filter.ToTime, err = timeParams(c) filter.FromTime, filter.ToTime, err = timeParams(c)
if err != nil { if err != nil {
handleErrorResponse(c, err) handleV1ErrorResponse(c, err)
return return
} }

View File

@@ -38,11 +38,11 @@ func (s *Server) handleCallLogGet(c *gin.Context) {
ctx := c.Request.Context() ctx := c.Request.Context()
appID := c.MustGet(api.AppID).(string) appID := c.MustGet(api.AppID).(string)
callID := c.Param(api.Call) callID := c.Param(api.ParamCallID)
logReader, err := s.logstore.GetLog(ctx, appID, callID) logReader, err := s.logstore.GetLog(ctx, appID, callID)
if err != nil { if err != nil {
handleErrorResponse(c, err) handleV1ErrorResponse(c, err)
return return
} }
@@ -70,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 // if we've reached this point it means that Fn didn't recognize Accepted content type
handleErrorResponse(c, models.NewAPIError(http.StatusNotAcceptable, handleV1ErrorResponse(c, models.NewAPIError(http.StatusNotAcceptable,
errors.New("unable to respond within acceptable response content types"))) errors.New("unable to respond within acceptable response content types")))
} }

View File

@@ -24,8 +24,7 @@ func TestCallGet(t *testing.T) {
} }
}() }()
app := &models.App{Name: "myapp"} app := &models.App{Name: "myapp", ID: "app_id"}
app.SetDefaults()
call := &models.Call{ call := &models.Call{
AppID: app.ID, AppID: app.ID,
ID: id.New().String(), ID: id.New().String(),
@@ -73,7 +72,7 @@ func TestCallGet(t *testing.T) {
} }
if test.expectedError != nil { if test.expectedError != nil {
resp := getErrorResponse(t, rec) resp := getV1ErrorResponse(t, rec)
if !strings.Contains(resp.Error.Message, test.expectedError.Error()) { if !strings.Contains(resp.Error.Message, test.expectedError.Error()) {
t.Log(resp.Error.Message) t.Log(resp.Error.Message)
@@ -94,8 +93,7 @@ func TestCallList(t *testing.T) {
} }
}() }()
app := &models.App{Name: "myapp"} app := &models.App{Name: "myapp", ID: "app_id"}
app.SetDefaults()
call := &models.Call{ call := &models.Call{
AppID: app.ID, AppID: app.ID,
@@ -168,7 +166,7 @@ func TestCallList(t *testing.T) {
} }
if test.expectedError != nil { if test.expectedError != nil {
resp := getErrorResponse(t, rec) resp := getV1ErrorResponse(t, rec)
if resp.Error == nil || !strings.Contains(resp.Error.Message, test.expectedError.Error()) { if resp.Error == nil || !strings.Contains(resp.Error.Message, test.expectedError.Error()) {
t.Errorf("Test %d: Expected error message to have `%s`, got: `%s`", t.Errorf("Test %d: Expected error message to have `%s`, got: `%s`",

View File

@@ -16,15 +16,54 @@ import (
// ErrInternalServerError returned when something exceptional happens. // ErrInternalServerError returned when something exceptional happens.
var ErrInternalServerError = errors.New("internal server error") 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 { func simpleError(err error) *models.Error {
return &models.Error{Error: &models.ErrorBody{Message: err.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
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) { func handleErrorResponse(c *gin.Context, err error) {
HandleErrorResponse(c.Request.Context(), c.Writer, err) HandleErrorResponse(c.Request.Context(), c.Writer, err)
} }
// HandleErrorResponse used to handle response errors in the same way. // 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")
}
}
// handleV1ErrorResponse used to handle response errors in the same way.
func HandleErrorResponse(ctx context.Context, w http.ResponseWriter, err error) { func HandleErrorResponse(ctx context.Context, w http.ResponseWriter, err error) {
log := common.Logger(ctx) log := common.Logger(ctx)
var statuscode int var statuscode int
@@ -46,7 +85,7 @@ func HandleErrorResponse(ctx context.Context, w http.ResponseWriter, err error)
WriteError(ctx, w, statuscode, err) WriteError(ctx, w, statuscode, err)
} }
// WriteError easy way to do standard error response, but can set statuscode and error message easier than HandleErrorResponse // WriteError easy way to do standard error response, but can set statuscode and error message easier than handleV1ErrorResponse
func WriteError(ctx context.Context, w http.ResponseWriter, statuscode int, err error) { func WriteError(ctx context.Context, w http.ResponseWriter, statuscode int, err error) {
log := common.Logger(ctx) log := common.Logger(ctx)
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "application/json; charset=utf-8")

View File

@@ -10,24 +10,24 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
func (s *Server) apiHandlerWrapperFunc(apiHandler fnext.ApiHandler) gin.HandlerFunc { func (s *Server) apiHandlerWrapperFn(apiHandler fnext.ApiHandler) gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
apiHandler.ServeHTTP(c.Writer, c.Request) apiHandler.ServeHTTP(c.Writer, c.Request)
} }
} }
func (s *Server) apiAppHandlerWrapperFunc(apiHandler fnext.ApiAppHandler) gin.HandlerFunc { func (s *Server) apiAppHandlerWrapperFn(apiHandler fnext.ApiAppHandler) gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
// get the app // get the app
appID := c.MustGet(api.AppID).(string) appID := c.MustGet(api.AppID).(string)
app, err := s.datastore.GetAppByID(c.Request.Context(), appID) app, err := s.datastore.GetAppByID(c.Request.Context(), appID)
if err != nil { if err != nil {
handleErrorResponse(c, err) handleV1ErrorResponse(c, err)
c.Abort() c.Abort()
return return
} }
if app == nil { if app == nil {
handleErrorResponse(c, models.ErrAppsNotFound) handleV1ErrorResponse(c, models.ErrAppsNotFound)
c.Abort() c.Abort()
return return
} }
@@ -36,31 +36,31 @@ func (s *Server) apiAppHandlerWrapperFunc(apiHandler fnext.ApiAppHandler) gin.Ha
} }
} }
func (s *Server) apiRouteHandlerWrapperFunc(apiHandler fnext.ApiRouteHandler) gin.HandlerFunc { func (s *Server) apiRouteHandlerWrapperFn(apiHandler fnext.ApiRouteHandler) gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
context := c.Request.Context() context := c.Request.Context()
appID := c.MustGet(api.AppID).(string) appID := c.MustGet(api.AppID).(string)
routePath := "/" + c.Param(api.CRoute) routePath := "/" + c.Param(api.ParamRouteName)
route, err := s.datastore.GetRoute(context, appID, routePath) route, err := s.datastore.GetRoute(context, appID, routePath)
if err != nil { if err != nil {
handleErrorResponse(c, err) handleV1ErrorResponse(c, err)
c.Abort() c.Abort()
return return
} }
if route == nil { if route == nil {
handleErrorResponse(c, models.ErrRoutesNotFound) handleV1ErrorResponse(c, models.ErrRoutesNotFound)
c.Abort() c.Abort()
return return
} }
app, err := s.datastore.GetAppByID(context, appID) app, err := s.datastore.GetAppByID(context, appID)
if err != nil { if err != nil {
handleErrorResponse(c, err) handleV1ErrorResponse(c, err)
c.Abort() c.Abort()
return return
} }
if app == nil { if app == nil {
handleErrorResponse(c, models.ErrAppsNotFound) handleV1ErrorResponse(c, models.ErrAppsNotFound)
c.Abort() c.Abort()
return return
} }
@@ -73,7 +73,7 @@ func (s *Server) apiRouteHandlerWrapperFunc(apiHandler fnext.ApiRouteHandler) gi
func (s *Server) AddEndpoint(method, path string, handler fnext.ApiHandler) { func (s *Server) AddEndpoint(method, path string, handler fnext.ApiHandler) {
v1 := s.Router.Group("/v1") v1 := s.Router.Group("/v1")
// v1.GET("/apps/:app/log", logHandler(cfg)) // v1.GET("/apps/:app/log", logHandler(cfg))
v1.Handle(method, path, s.apiHandlerWrapperFunc(handler)) v1.Handle(method, path, s.apiHandlerWrapperFn(handler))
} }
// AddEndpoint adds an endpoint to /v1/x // AddEndpoint adds an endpoint to /v1/x
@@ -85,7 +85,7 @@ func (s *Server) AddEndpointFunc(method, path string, handler func(w http.Respon
func (s *Server) AddAppEndpoint(method, path string, handler fnext.ApiAppHandler) { func (s *Server) AddAppEndpoint(method, path string, handler fnext.ApiAppHandler) {
v1 := s.Router.Group("/v1") v1 := s.Router.Group("/v1")
v1.Use(s.checkAppPresenceByName()) v1.Use(s.checkAppPresenceByName())
v1.Handle(method, "/apps/:app"+path, s.apiAppHandlerWrapperFunc(handler)) v1.Handle(method, "/apps/:app"+path, s.apiAppHandlerWrapperFn(handler))
} }
// AddAppEndpoint adds an endpoints to /v1/apps/:app/x // AddAppEndpoint adds an endpoints to /v1/apps/:app/x
@@ -97,7 +97,7 @@ func (s *Server) AddAppEndpointFunc(method, path string, handler func(w http.Res
func (s *Server) AddRouteEndpoint(method, path string, handler fnext.ApiRouteHandler) { func (s *Server) AddRouteEndpoint(method, path string, handler fnext.ApiRouteHandler) {
v1 := s.Router.Group("/v1") v1 := s.Router.Group("/v1")
v1.Use(s.checkAppPresenceByName()) v1.Use(s.checkAppPresenceByName())
v1.Handle(method, "/apps/:app/routes/:route"+path, s.apiRouteHandlerWrapperFunc(handler)) // conflicts with existing wildcard v1.Handle(method, "/apps/:app/routes/:route"+path, s.apiRouteHandlerWrapperFn(handler)) // conflicts with existing wildcard
} }
// AddRouteEndpoint adds an endpoints to /v1/apps/:app/routes/:route/x // AddRouteEndpoint adds an endpoints to /v1/apps/:app/routes/:route/x

View File

@@ -0,0 +1,76 @@
package server
import (
"context"
"github.com/fnproject/fn/api/models"
"github.com/fnproject/fn/fnext"
)
type fnListeners []fnext.FnListener
var _ fnext.FnListener = new(fnListeners)
func (s *Server) AddFnListener(listener fnext.FnListener) {
*s.fnListeners = append(*s.fnListeners, listener)
}
func (a *fnListeners) BeforeFnCreate(ctx context.Context, fn *models.Fn) error {
for _, l := range *a {
err := l.BeforeFnCreate(ctx, fn)
if err != nil {
return err
}
}
return nil
}
func (a *fnListeners) AfterFnCreate(ctx context.Context, fn *models.Fn) error {
for _, l := range *a {
err := l.AfterFnCreate(ctx, fn)
if err != nil {
return err
}
}
return nil
}
func (a *fnListeners) BeforeFnUpdate(ctx context.Context, fn *models.Fn) error {
for _, l := range *a {
err := l.BeforeFnUpdate(ctx, fn)
if err != nil {
return err
}
}
return nil
}
func (a *fnListeners) AfterFnUpdate(ctx context.Context, fn *models.Fn) error {
for _, l := range *a {
err := l.AfterFnUpdate(ctx, fn)
if err != nil {
return err
}
}
return nil
}
func (a *fnListeners) BeforeFnDelete(ctx context.Context, fnID string) error {
for _, l := range *a {
err := l.BeforeFnDelete(ctx, fnID)
if err != nil {
return err
}
}
return nil
}
func (a *fnListeners) AfterFnDelete(ctx context.Context, fnID string) error {
for _, l := range *a {
err := l.AfterFnDelete(ctx, fnID)
if err != nil {
return err
}
}
return nil
}

30
api/server/fns_create.go Normal file
View File

@@ -0,0 +1,30 @@
package server
import (
"net/http"
"github.com/fnproject/fn/api/models"
"github.com/gin-gonic/gin"
)
func (s *Server) handleFnCreate(c *gin.Context) {
ctx := c.Request.Context()
fn := &models.Fn{}
err := c.BindJSON(fn)
if err != nil {
if !models.IsAPIError(err) {
err = models.ErrInvalidJSON
}
handleErrorResponse(c, err)
return
}
fn.SetDefaults()
fnCreated, err := s.datastore.InsertFn(ctx, fn)
if err != nil {
handleErrorResponse(c, err)
}
c.JSON(http.StatusOK, fnCreated)
}

22
api/server/fns_delete.go Normal file
View File

@@ -0,0 +1,22 @@
package server
import (
"net/http"
"github.com/fnproject/fn/api"
"github.com/gin-gonic/gin"
)
func (s *Server) handleFnDelete(c *gin.Context) {
ctx := c.Request.Context()
fnID := c.Param(api.ParamFnID)
err := s.datastore.RemoveFn(ctx, fnID)
if err != nil {
handleErrorResponse(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"message": "Successfully deleted func"})
}

19
api/server/fns_get.go Normal file
View File

@@ -0,0 +1,19 @@
package server
import (
"net/http"
"github.com/fnproject/fn/api"
"github.com/gin-gonic/gin"
)
func (s *Server) handleFnGet(c *gin.Context) {
ctx := c.Request.Context()
f, err := s.datastore.GetFnByID(ctx, c.Param(api.ParamFnID))
if err != nil {
handleErrorResponse(c, err)
return
}
c.JSON(http.StatusOK, f)
}

33
api/server/fns_list.go Normal file
View File

@@ -0,0 +1,33 @@
package server
import (
"net/http"
"github.com/fnproject/fn/api/models"
"github.com/gin-gonic/gin"
)
func (s *Server) handleFnList(c *gin.Context) {
ctx := c.Request.Context()
var filter models.FnFilter
filter.Cursor, filter.PerPage = pageParams(c, false)
filter.AppID = c.Query("app_id")
filter.Name = c.Query("name")
fns, err := s.datastore.GetFns(ctx, &filter)
if err != nil {
handleErrorResponse(c, err)
return
}
var nextCursor string
if len(fns) > 0 && len(fns) == filter.PerPage {
nextCursor = fns[len(fns)-1].Name
}
c.JSON(http.StatusOK, fnListResponse{
NextCursor: nextCursor,
Items: fns,
})
}

355
api/server/fns_test.go Normal file
View File

@@ -0,0 +1,355 @@
package server
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"strings"
"testing"
"time"
"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"
)
type funcTestCase struct {
ds models.Datastore
logDB models.LogStore
method string
path string
body string
expectedCode int
expectedError error
}
func (test *funcTestCase) 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.Fatalf("Test %d: Expected status code to be %d but was %d",
i, test.expectedCode, rec.Code)
}
if test.expectedError != nil {
resp := getErrorResponse(t, rec)
if resp == nil {
t.Log(buf.String())
t.Errorf("Test %d: Expected error message to have `%s`, but it was nil",
i, test.expectedError)
} else if resp.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.Message)
}
}
if test.expectedCode == http.StatusOK {
var fn models.Fn
err := json.NewDecoder(rec.Body).Decode(&fn)
if err != nil {
t.Log(buf.String())
t.Errorf("Test %d: error decoding body for 'ok' json, it was a lie: %v", i, err)
}
if test.method == http.MethodPut {
// IsZero() doesn't really work, this ensures it's not unset as long as we're not in 1970
if time.Time(fn.CreatedAt).Before(time.Now().Add(-1 * time.Hour)) {
t.Log(buf.String())
t.Errorf("Test %d: expected created_at to be set on func, it wasn't: %s", i, fn.CreatedAt)
}
if time.Time(fn.UpdatedAt).Before(time.Now().Add(-1 * time.Hour)) {
t.Log(buf.String())
t.Errorf("Test %d: expected updated_at to be set on func, it wasn't: %s", i, fn.UpdatedAt)
}
if fn.ID == "" {
t.Log(buf.String())
t.Errorf("Test %d: expected id to be non-empty, it was empty: %v", i, fn)
}
}
}
cancel()
buf.Reset()
}
func TestFnCreate(t *testing.T) {
buf := setLogBuffer()
a := &models.App{Name: "a", ID: "aid"}
ds := datastore.NewMockInit([]*models.App{a})
ls := logs.NewMock()
for i, test := range []funcTestCase{
// errors
{ds, ls, http.MethodPost, "/v2/fns", ``, http.StatusBadRequest, models.ErrInvalidJSON},
{ds, ls, http.MethodPost, "/v2/fns", `{ }`, http.StatusBadRequest, models.ErrFnsMissingAppID},
{ds, ls, http.MethodPost, "/v2/fns", fmt.Sprintf(`{ "app_id": "%s" }`, a.ID), http.StatusBadRequest, models.ErrFnsMissingName},
{ds, ls, http.MethodPost, "/v2/fns", fmt.Sprintf(`{ "app_id": "%s", "name": "a" }`, a.ID), http.StatusBadRequest, models.ErrFnsMissingImage},
{ds, ls, http.MethodPost, "/v2/fns", fmt.Sprintf(`{ "app_id": "%s", "name": " ", "image": "fnproject/fn-test-utils" }`, a.ID), http.StatusBadRequest, models.ErrFnsInvalidName},
{ds, ls, http.MethodPost, "/v2/fns", fmt.Sprintf(`{ "app_id": "%s", "name": "a", "image": "fnproject/fn-test-utils", "format": "wazzup" }`, a.ID), http.StatusBadRequest, models.ErrFnsInvalidFormat},
{ds, ls, http.MethodPost, "/v2/fns", fmt.Sprintf(`{ "app_id": "%s", "name": "a", "image": "fnproject/fn-test-utils", "timeout": 3601 }`, a.ID), http.StatusBadRequest, models.ErrFnsInvalidTimeout},
{ds, ls, http.MethodPost, "/v2/fns", fmt.Sprintf(`{ "app_id": "%s", "name": "a", "image": "fnproject/fn-test-utils", "idle_timeout": 3601 }`, a.ID), http.StatusBadRequest, models.ErrFnsInvalidIdleTimeout},
{ds, ls, http.MethodPost, "/v2/fns", fmt.Sprintf(`{ "app_id": "%s", "name": "a", "image": "fnproject/fn-test-utils", "memory": 100000000000000 }`, a.ID), http.StatusBadRequest, models.ErrInvalidMemory},
// success create & update
{ds, ls, http.MethodPost, "/v2/fns", fmt.Sprintf(`{ "app_id": "%s", "name": "myfunc", "image": "fnproject/fn-test-utils" }`, a.ID), http.StatusOK, nil},
{ds, ls, http.MethodPost, "/v2/fns", fmt.Sprintf(`{ "app_id": "%s", "name": "myfunc", "image": "fnproject/fn-test-utils" }`, a.ID), http.StatusConflict, models.ErrFnsExists},
} {
test.run(t, i, buf)
}
}
func TestFnUpdate(t *testing.T) {
buf := setLogBuffer()
a := &models.App{Name: "a", ID: "app_id"}
f := &models.Fn{ID: "fn_id", Name: "f", AppID: a.ID}
f.SetDefaults()
ds := datastore.NewMockInit([]*models.App{a}, []*models.Fn{f})
ls := logs.NewMock()
for i, test := range []funcTestCase{
{ds, ls, http.MethodPut, "/v2/fns/missing", `{ }`, http.StatusNotFound, models.ErrFnsNotFound},
{ds, ls, http.MethodPut, fmt.Sprintf("/v2/fns/%s", f.ID), `{ "id": "nottheid" }`, http.StatusBadRequest, models.ErrFnsIDMismatch},
{ds, ls, http.MethodPut, fmt.Sprintf("/v2/fns/%s", f.ID), `{ "image": "fnproject/test" }`, http.StatusOK, nil},
{ds, ls, http.MethodPut, fmt.Sprintf("/v2/fns/%s", f.ID), `{ "format": "http" }`, http.StatusOK, nil},
{ds, ls, http.MethodPut, fmt.Sprintf("/v2/fns/%s", f.ID), `{ "memory": 1000 }`, http.StatusOK, nil},
{ds, ls, http.MethodPut, fmt.Sprintf("/v2/fns/%s", f.ID), `{ "timeout": 10 }`, http.StatusOK, nil},
{ds, ls, http.MethodPut, fmt.Sprintf("/v2/fns/%s", f.ID), `{ "idle_timeout": 10 }`, http.StatusOK, nil},
{ds, ls, http.MethodPut, fmt.Sprintf("/v2/fns/%s", f.ID), `{ "config": {"k":"v"} }`, http.StatusOK, nil},
{ds, ls, http.MethodPut, fmt.Sprintf("/v2/fns/%s", f.ID), `{ "annotations": {"k":"v"} }`, http.StatusOK, nil},
// test that partial update fails w/ same errors as create
{ds, ls, http.MethodPut, fmt.Sprintf("/v2/fns/%s", f.ID), `{ "format": "wazzup" }`, http.StatusBadRequest, models.ErrFnsInvalidFormat},
{ds, ls, http.MethodPut, fmt.Sprintf("/v2/fns/%s", f.ID), `{ "timeout": 3601 }`, http.StatusBadRequest, models.ErrFnsInvalidTimeout},
{ds, ls, http.MethodPut, fmt.Sprintf("/v2/fns/%s", f.ID), `{ "idle_timeout": 3601 }`, http.StatusBadRequest, models.ErrFnsInvalidIdleTimeout},
{ds, ls, http.MethodPut, fmt.Sprintf("/v2/fns/%s", f.ID), `{ "memory": 100000000000000 }`, http.StatusBadRequest, models.ErrInvalidMemory},
} {
test.run(t, i, buf)
}
}
func TestFnDelete(t *testing.T) {
buf := setLogBuffer()
a := &models.App{Name: "a", ID: "appid"}
f := &models.Fn{ID: "fn_id", Name: "myfunc", AppID: a.ID}
f.SetDefaults()
commonDS := datastore.NewMockInit([]*models.App{a}, []*models.Fn{f})
for i, test := range []struct {
ds models.Datastore
logDB models.LogStore
path string
body string
expectedCode int
expectedError error
}{
{commonDS, logs.NewMock(), "/v2/fns/missing", "", http.StatusNotFound, models.ErrFnsNotFound},
{commonDS, logs.NewMock(), fmt.Sprintf("/v2/fns/%s", f.ID), "", 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 := getErrorResponse(t, rec)
if !strings.Contains(resp.Message, test.expectedError.Error()) {
t.Log(buf.String())
t.Errorf("Test %d: Expected error message to have `%s`",
i, test.expectedError.Error())
}
}
cancel()
}
}
func TestFnList(t *testing.T) {
buf := setLogBuffer()
rnr, cancel := testRunner(t)
defer cancel()
// ids are sortable, need to test cursoring works as expected
r1b := id.New().String()
r2b := id.New().String()
r3b := id.New().String()
r4b := id.New().String()
fn1 := "myfunc1"
fn2 := "myfunc2"
fn3 := "myfunc3"
fn4 := "myfunc3"
app1 := &models.App{Name: "myapp1", ID: "app_id1"}
app2 := &models.App{Name: "myapp2", ID: "app_id2"}
ds := datastore.NewMockInit(
[]*models.App{app1, app2},
[]*models.Fn{
{
ID: r1b,
Name: fn1,
AppID: app1.ID,
Image: "fnproject/fn-test-utils",
},
{
ID: r2b,
Name: fn2,
AppID: app1.ID,
Image: "fnproject/fn-test-utils",
},
{
ID: r3b,
Name: fn3,
AppID: app1.ID,
Image: "fnproject/yo",
},
{
ID: r4b,
Name: fn4,
AppID: app2.ID,
Image: "fnproject/foo",
},
},
)
fnl := logs.NewMock()
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
}{
{"/v2/fns", "", http.StatusBadRequest, models.ErrFnsMissingAppID, 0, ""},
{fmt.Sprintf("/v2/fns?app_id=%s", app1.ID), "", http.StatusOK, nil, 3, ""},
{fmt.Sprintf("/v2/fns?app_id=%s&per_page=1", app1.ID), "", http.StatusOK, nil, 1, fn1},
{fmt.Sprintf("/v2/fns?app_id=%s&per_page=1&cursor=%s", app1.ID, fn1), "", http.StatusOK, nil, 1, fn2},
{fmt.Sprintf("/v2/fns?app_id=%s&per_page=1&cursor=%s", app1.ID, fn2), "", http.StatusOK, nil, 1, fn3},
{fmt.Sprintf("/v2/fns?app_id=%s&per_page=100&cursor=%s", app1.ID, fn3), "", http.StatusOK, nil, 0, ""}, // cursor is empty if per_page > len(results)
{fmt.Sprintf("/v2/fns?app_id=%s&per_page=1&cursor=%s", app1.ID, fn3), "", 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.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 := getErrorResponse(t, rec)
if !strings.Contains(resp.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 fnListResponse
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.Items) != test.expectedLen {
t.Errorf("Test %d: Expected fns 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)
}
}
}
}
func TestFnGet(t *testing.T) {
buf := setLogBuffer()
rnr, cancel := testRunner(t)
defer cancel()
app := &models.App{Name: "myapp", ID: "appid"}
ds := datastore.NewMockInit(
[]*models.App{app},
[]*models.Fn{
{
ID: "myfnId",
Name: "myfunc",
AppID: "appid",
Image: "fnproject/fn-test-utils",
},
})
fnl := logs.NewMock()
nilFn := new(models.Fn)
expectedFn := &models.Fn{
ID: "myfnId",
Name: "myfunc",
Image: "fnproject/fn-test-utils"}
srv := testServer(ds, &mqs.Mock{}, fnl, rnr, ServerTypeFull)
for i, test := range []struct {
path string
body string
expectedCode int
expectedError error
desiredFn *models.Fn
}{
{"/v2/fns/missing", "", http.StatusNotFound, models.ErrFnsNotFound, nilFn},
{"/v2/fns/myfnId", "", http.StatusOK, nil, expectedFn},
} {
_, rec := routerRequest(t, srv.Router, "GET", test.path, nil)
if rec.Code != test.expectedCode {
t.Log(buf.String())
t.Fatalf("Test %d: Expected status code to be %d but was %d",
i, test.expectedCode, rec.Code)
}
if test.expectedError != nil {
resp := getErrorResponse(t, rec)
if !strings.Contains(resp.Message, test.expectedError.Error()) {
t.Log(buf.String())
t.Errorf("Test %d: Expected error message to have `%s`, got `%s`",
i, test.expectedError.Error(), resp.Message)
}
}
if !test.desiredFn.Equals(nilFn) {
var fn models.Fn
err := json.NewDecoder(rec.Body).Decode(&fn)
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
if test.desiredFn.Equals(&fn) {
t.Errorf("Test %d: Expected fn [%v] got [%v]", i, test.desiredFn, fn)
}
}
}
}

41
api/server/fns_update.go Normal file
View File

@@ -0,0 +1,41 @@
package server
import (
"net/http"
"github.com/fnproject/fn/api"
"github.com/fnproject/fn/api/models"
"github.com/gin-gonic/gin"
)
func (s *Server) handleFnUpdate(c *gin.Context) {
ctx := c.Request.Context()
fn := &models.Fn{}
err := c.BindJSON(fn)
if err != nil {
if !models.IsAPIError(err) {
err = models.ErrInvalidJSON
}
handleErrorResponse(c, err)
return
}
pathFnID := c.Param(api.ParamFnID)
if fn.ID == "" {
fn.ID = pathFnID
} else {
if pathFnID != fn.ID {
handleErrorResponse(c, models.ErrFnsIDMismatch)
}
}
fnUpdated, err := s.datastore.UpdateFn(ctx, fn)
if err != nil {
handleErrorResponse(c, err)
return
}
c.JSON(http.StatusOK, fnUpdated)
}

View File

@@ -61,8 +61,8 @@ func traceWrap(c *gin.Context) {
logrus.Fatal(err) logrus.Fatal(err)
} }
ctx, err := tag.New(c.Request.Context(), ctx, err := tag.New(c.Request.Context(),
tag.Insert(appKey, c.Param(api.CApp)), tag.Insert(appKey, c.Param(api.ParamAppName)),
tag.Insert(pathKey, c.Param(api.CRoute)), tag.Insert(pathKey, c.Param(api.ParamRouteName)),
) )
if err != nil { if err != nil {
logrus.Fatal(err) logrus.Fatal(err)
@@ -133,7 +133,7 @@ func panicWrap(c *gin.Context) {
if !ok { if !ok {
err = fmt.Errorf("fn: %v", rec) err = fmt.Errorf("fn: %v", rec)
} }
handleErrorResponse(c, err) handleV1ErrorResponse(c, err)
c.Abort() c.Abort()
} }
}(c) }(c)
@@ -143,12 +143,12 @@ func panicWrap(c *gin.Context) {
func loggerWrap(c *gin.Context) { func loggerWrap(c *gin.Context) {
ctx, _ := common.LoggerWithFields(c.Request.Context(), extractFields(c)) ctx, _ := common.LoggerWithFields(c.Request.Context(), extractFields(c))
if appName := c.Param(api.CApp); appName != "" { if appName := c.Param(api.ParamAppName); appName != "" {
c.Set(api.App, appName) c.Set(api.AppName, appName)
ctx = context.WithValue(ctx, api.App, appName) ctx = context.WithValue(ctx, api.AppName, appName)
} }
if routePath := c.Param(api.CRoute); routePath != "" { if routePath := c.Param(api.ParamRouteName); routePath != "" {
c.Set(api.Path, routePath) c.Set(api.Path, routePath)
ctx = context.WithValue(ctx, api.Path, routePath) ctx = context.WithValue(ctx, api.Path, routePath)
} }
@@ -161,11 +161,11 @@ func (s *Server) checkAppPresenceByNameAtRunner() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
ctx, _ := common.LoggerWithFields(c.Request.Context(), extractFields(c)) ctx, _ := common.LoggerWithFields(c.Request.Context(), extractFields(c))
appName := c.Param(api.CApp) appName := c.Param(api.ParamAppName)
if appName != "" { if appName != "" {
appID, err := s.agent.GetAppID(ctx, appName) appID, err := s.agent.GetAppID(ctx, appName)
if err != nil { if err != nil {
handleErrorResponse(c, err) handleV1ErrorResponse(c, err)
c.Abort() c.Abort()
return return
} }
@@ -181,11 +181,11 @@ func (s *Server) checkAppPresenceByName() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
ctx, _ := common.LoggerWithFields(c.Request.Context(), extractFields(c)) ctx, _ := common.LoggerWithFields(c.Request.Context(), extractFields(c))
appName := c.MustGet(api.App).(string) appName := c.MustGet(api.AppName).(string)
if appName != "" { if appName != "" {
appID, err := s.datastore.GetAppID(ctx, appName) appID, err := s.datastore.GetAppID(ctx, appName)
if err != nil { if err != nil {
handleErrorResponse(c, err) handleV1ErrorResponse(c, err)
c.Abort() c.Abort()
return return
} }
@@ -199,17 +199,28 @@ func (s *Server) checkAppPresenceByName() gin.HandlerFunc {
func setAppNameInCtx(c *gin.Context) { func setAppNameInCtx(c *gin.Context) {
// add appName to context // add appName to context
appName := c.GetString(api.App) appName := c.GetString(api.AppName)
if appName != "" { if appName != "" {
c.Request = c.Request.WithContext(context.WithValue(c.Request.Context(), fnext.AppNameKey, appName)) c.Request = c.Request.WithContext(context.WithValue(c.Request.Context(), fnext.AppNameKey, appName))
} }
c.Next() c.Next()
} }
func setAppIDInCtx(c *gin.Context) {
// add appName to context
appID := c.Param(api.ParamAppID)
if appID != "" {
c.Set(api.AppID, appID)
c.Request = c.Request.WithContext(c)
}
c.Next()
}
func appNameCheck(c *gin.Context) { func appNameCheck(c *gin.Context) {
appName := c.GetString(api.App) appName := c.GetString(api.AppName)
if appName == "" { if appName == "" {
handleErrorResponse(c, models.ErrAppsMissingName) handleV1ErrorResponse(c, models.ErrAppsMissingName)
c.Abort() c.Abort()
return return
} }

View File

@@ -18,9 +18,9 @@ func (s *Server) handleRunnerEnqueue(c *gin.Context) {
err := c.BindJSON(&call) err := c.BindJSON(&call)
if err != nil { if err != nil {
if models.IsAPIError(err) { if models.IsAPIError(err) {
handleErrorResponse(c, err) handleV1ErrorResponse(c, err)
} else { } else {
handleErrorResponse(c, models.ErrInvalidJSON) handleV1ErrorResponse(c, models.ErrInvalidJSON)
} }
return return
} }
@@ -39,7 +39,7 @@ func (s *Server) handleRunnerEnqueue(c *gin.Context) {
call.Status = "queued" call.Status = "queued"
_, err = s.mq.Push(ctx, &call) _, err = s.mq.Push(ctx, &call)
if err != nil { if err != nil {
handleErrorResponse(c, err) handleV1ErrorResponse(c, err)
return return
} }
@@ -70,7 +70,7 @@ func (s *Server) handleRunnerDequeue(c *gin.Context) {
for { for {
call, err := s.mq.Reserve(ctx) call, err := s.mq.Reserve(ctx)
if err != nil { if err != nil {
handleErrorResponse(c, err) handleV1ErrorResponse(c, err)
return return
} }
if call != nil { if call != nil {
@@ -97,9 +97,9 @@ func (s *Server) handleRunnerStart(c *gin.Context) {
err := c.BindJSON(&call) err := c.BindJSON(&call)
if err != nil { if err != nil {
if models.IsAPIError(err) { if models.IsAPIError(err) {
handleErrorResponse(c, err) handleV1ErrorResponse(c, err)
} else { } else {
handleErrorResponse(c, models.ErrInvalidJSON) handleV1ErrorResponse(c, models.ErrInvalidJSON)
} }
return return
} }
@@ -129,11 +129,11 @@ func (s *Server) handleRunnerStart(c *gin.Context) {
// TODO change this to only delete message if the status change fails b/c it already ran // TODO change this to only delete message if the status change fails b/c it already ran
// after messaging semantics change // after messaging semantics change
if err := s.mq.Delete(ctx, &call); err != nil { // TODO change this to take some string(s), not a whole call if err := s.mq.Delete(ctx, &call); err != nil { // TODO change this to take some string(s), not a whole call
handleErrorResponse(c, err) handleV1ErrorResponse(c, err)
return return
} }
//} //}
//handleErrorResponse(c, err) //handleV1ErrorResponse(c, err)
//return //return
//} //}
@@ -152,9 +152,9 @@ func (s *Server) handleRunnerFinish(c *gin.Context) {
err := c.BindJSON(&body) err := c.BindJSON(&body)
if err != nil { if err != nil {
if models.IsAPIError(err) { if models.IsAPIError(err) {
handleErrorResponse(c, err) handleV1ErrorResponse(c, err)
} else { } else {
handleErrorResponse(c, models.ErrInvalidJSON) handleV1ErrorResponse(c, models.ErrInvalidJSON)
} }
return return
} }

View File

@@ -32,11 +32,11 @@ func (c *middlewareController) CallFunction(w http.ResponseWriter, r *http.Reque
// since we added middleware that checks the app ID // since we added middleware that checks the app ID
// we need to ensure that we set it as soon as possible // we need to ensure that we set it as soon as possible
appName := ctx.Value(api.CApp).(string) appName := ctx.Value(api.AppName).(string)
if appName != "" { if appName != "" {
appID, err := c.server.datastore.GetAppID(ctx, appName) appID, err := c.server.datastore.GetAppID(ctx, appName)
if err != nil { if err != nil {
handleErrorResponse(c.ginContext, err) handleV1ErrorResponse(c.ginContext, err)
c.ginContext.Abort() c.ginContext.Abort()
return return
} }
@@ -78,7 +78,7 @@ func (s *Server) runMiddleware(c *gin.Context, ms []fnext.Middleware) {
err := recover() err := recover()
if err != nil { if err != nil {
common.Logger(c.Request.Context()).WithField("MiddleWarePanicRecovery:", err).Errorln("A panic occurred during middleware.") common.Logger(c.Request.Context()).WithField("MiddleWarePanicRecovery:", err).Errorln("A panic occurred during middleware.")
handleErrorResponse(c, ErrInternalServerError) handleV1ErrorResponse(c, ErrInternalServerError)
} }
}() }()

View File

@@ -10,6 +10,7 @@ import (
"strings" "strings"
"testing" "testing"
"fmt"
"github.com/fnproject/fn/api/datastore" "github.com/fnproject/fn/api/datastore"
"github.com/fnproject/fn/api/logs" "github.com/fnproject/fn/api/logs"
"github.com/fnproject/fn/api/models" "github.com/fnproject/fn/api/models"
@@ -67,10 +68,8 @@ func TestMiddlewareChaining(t *testing.T) {
func TestRootMiddleware(t *testing.T) { func TestRootMiddleware(t *testing.T) {
app1 := &models.App{Name: "myapp", Config: models.Config{}} app1 := &models.App{ID: "app_id_1", Name: "myapp", Config: models.Config{}}
app1.SetDefaults() app2 := &models.App{ID: "app_id_2", Name: "myapp2", Config: models.Config{}}
app2 := &models.App{Name: "myapp2", Config: models.Config{}}
app2.SetDefaults()
ds := datastore.NewMockInit( ds := datastore.NewMockInit(
[]*models.App{app1, app2}, []*models.App{app1, app2},
[]*models.Route{ []*models.Route{
@@ -94,7 +93,7 @@ func TestRootMiddleware(t *testing.T) {
t.Log("breaker breaker!") t.Log("breaker breaker!")
ctx := r.Context() ctx := r.Context()
// TODO: this is a little dicey, should have some functions to set these in case the context keys change or something. // TODO: this is a little dicey, should have some functions to set these in case the context keys change or something.
ctx = context.WithValue(ctx, "app", "myapp2") ctx = context.WithValue(ctx, "app_name", "myapp2")
ctx = context.WithValue(ctx, "path", "/app2func") ctx = context.WithValue(ctx, "path", "/app2func")
mctx := fnext.GetMiddlewareController(ctx) mctx := fnext.GetMiddlewareController(ctx)
mctx.CallFunction(w, r.WithContext(ctx)) mctx.CallFunction(w, r.WithContext(ctx))
@@ -132,28 +131,31 @@ func TestRootMiddleware(t *testing.T) {
{"/r/myapp/myroute", `{"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"}, {"/v1/apps", `{"isDebug": true}`, "GET", map[string][]string{"funcit": {"Test"}}, http.StatusOK, "johnny"},
} { } {
body := strings.NewReader(test.body) t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
req, err := http.NewRequest(test.method, "http://127.0.0.1:8080"+test.path, body) body := strings.NewReader(test.body)
if err != nil { req, err := http.NewRequest(test.method, "http://127.0.0.1:8080"+test.path, body)
t.Fatalf("Test: Could not create %s request to %s: %v", test.method, test.path, err) if err != nil {
} t.Fatalf("Test: Could not create %s request to %s: %v", test.method, test.path, err)
for k, v := range test.headers { }
req.Header.Add(k, v[0]) for k, v := range test.headers {
} req.Header.Add(k, v[0])
t.Log("TESTING:", req.URL.String()) }
_, rec := routerRequest2(t, srv.Router, req) t.Log("TESTING:", req.URL.String())
// t.Log("REC: %+v\n", rec) _, rec := routerRequest2(t, srv.Router, req)
// t.Log("REC: %+v\n", rec)
result, err := ioutil.ReadAll(rec.Result().Body) result, err := ioutil.ReadAll(rec.Result().Body)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
rbody := string(result)
t.Logf("Test %v: response body: %v", i, rbody)
if !strings.Contains(rbody, test.expectedInBody) {
t.Fatal(i, "middleware didn't work correctly", string(result))
}
})
rbody := string(result)
t.Logf("Test %v: response body: %v", i, rbody)
if !strings.Contains(rbody, test.expectedInBody) {
t.Fatal(i, "middleware didn't work correctly", string(result))
}
} }
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/v1/apps", strings.NewReader("{\"app\": {\"name\": \"myapp3\"}}"))

View File

@@ -27,7 +27,7 @@ func (a *routeListeners) BeforeRouteCreate(ctx context.Context, route *models.Ro
func (a *routeListeners) AfterRouteCreate(ctx context.Context, route *models.Route) error { func (a *routeListeners) AfterRouteCreate(ctx context.Context, route *models.Route) error {
for _, l := range *a { for _, l := range *a {
err := l.BeforeRouteCreate(ctx, route) err := l.AfterRouteCreate(ctx, route)
if err != nil { if err != nil {
return err return err
} }
@@ -55,9 +55,9 @@ func (a *routeListeners) AfterRouteUpdate(ctx context.Context, route *models.Rou
return nil return nil
} }
func (a *routeListeners) BeforeRouteDelete(ctx context.Context, appName string, routePath string) error { func (a *routeListeners) BeforeRouteDelete(ctx context.Context, appId string, routePath string) error {
for _, l := range *a { for _, l := range *a {
err := l.BeforeRouteDelete(ctx, appName, routePath) err := l.BeforeRouteDelete(ctx, appId, routePath)
if err != nil { if err != nil {
return err return err
} }
@@ -65,9 +65,9 @@ func (a *routeListeners) BeforeRouteDelete(ctx context.Context, appName string,
return nil return nil
} }
func (a *routeListeners) AfterRouteDelete(ctx context.Context, appName string, routePath string) error { func (a *routeListeners) AfterRouteDelete(ctx context.Context, appId string, routePath string) error {
for _, l := range *a { for _, l := range *a {
err := l.AfterRouteDelete(ctx, appName, routePath) err := l.AfterRouteDelete(ctx, appId, routePath)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -30,20 +30,20 @@ func (s *Server) handleRoutesPostPut(c *gin.Context) {
var wroute models.RouteWrapper var wroute models.RouteWrapper
err := bindRoute(c, method, &wroute) err := bindRoute(c, method, &wroute)
if err != nil { if err != nil {
handleErrorResponse(c, err) handleV1ErrorResponse(c, err)
return return
} }
appName := c.MustGet(api.App).(string) appName := c.MustGet(api.AppName).(string)
appID, err := s.ensureApp(ctx, appName, method) appID, err := s.ensureApp(ctx, appName, method)
if err != nil { if err != nil {
handleErrorResponse(c, err) handleV1ErrorResponse(c, err)
return return
} }
resp, err := s.ensureRoute(ctx, appID, &wroute, method) resp, err := s.ensureRoute(ctx, appID, &wroute, method)
if err != nil { if err != nil {
handleErrorResponse(c, err) handleV1ErrorResponse(c, err)
return return
} }
@@ -57,14 +57,14 @@ func (s *Server) handleRoutesPatch(c *gin.Context) {
var wroute models.RouteWrapper var wroute models.RouteWrapper
err := bindRoute(c, method, &wroute) err := bindRoute(c, method, &wroute)
if err != nil { if err != nil {
handleErrorResponse(c, err) handleV1ErrorResponse(c, err)
return return
} }
appID := c.MustGet(api.AppID).(string) appID := c.MustGet(api.AppID).(string)
resp, err := s.ensureRoute(ctx, appID, &wroute, method) resp, err := s.ensureRoute(ctx, appID, &wroute, method)
if err != nil { if err != nil {
handleErrorResponse(c, err) handleV1ErrorResponse(c, err)
return return
} }
@@ -72,6 +72,9 @@ func (s *Server) handleRoutesPatch(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 {
if wroute.Route != nil {
wroute.Route.SetDefaults()
}
r, err := s.datastore.InsertRoute(ctx, wroute.Route) r, err := s.datastore.InsertRoute(ctx, wroute.Route)
if err != nil { if err != nil {
return err return err

View File

@@ -15,12 +15,12 @@ func (s *Server) handleRouteDelete(c *gin.Context) {
routePath := path.Clean(c.MustGet(api.Path).(string)) routePath := path.Clean(c.MustGet(api.Path).(string))
if _, err := s.datastore.GetRoute(ctx, appID, routePath); err != nil { if _, err := s.datastore.GetRoute(ctx, appID, routePath); err != nil {
handleErrorResponse(c, err) handleV1ErrorResponse(c, err)
return return
} }
if err := s.datastore.RemoveRoute(ctx, appID, routePath); err != nil { if err := s.datastore.RemoveRoute(ctx, appID, routePath); err != nil {
handleErrorResponse(c, err) handleV1ErrorResponse(c, err)
return return
} }

View File

@@ -14,7 +14,7 @@ func routeGet(s *Server, appID string, c *gin.Context) {
routePath := path.Clean("/" + c.MustGet(api.Path).(string)) routePath := path.Clean("/" + c.MustGet(api.Path).(string))
route, err := s.datastore.GetRoute(ctx, appID, routePath) route, err := s.datastore.GetRoute(ctx, appID, routePath)
if err != nil { if err != nil {
handleErrorResponse(c, err) handleV1ErrorResponse(c, err)
return return
} }
@@ -26,5 +26,5 @@ func (s *Server) handleRouteGetAPI(c *gin.Context) {
} }
func (s *Server) handleRouteGetRunner(c *gin.Context) { func (s *Server) handleRouteGetRunner(c *gin.Context) {
routeGet(s, c.Param(api.CApp), c) routeGet(s, c.MustGet(api.AppID).(string), c)
} }

View File

@@ -19,7 +19,7 @@ func (s *Server) handleRouteList(c *gin.Context) {
routes, err := s.datastore.GetRoutesByApp(ctx, c.MustGet(api.AppID).(string), &filter) routes, err := s.datastore.GetRoutesByApp(ctx, c.MustGet(api.AppID).(string), &filter)
if err != nil { if err != nil {
handleErrorResponse(c, err) handleV1ErrorResponse(c, err)
return return
} }

View File

@@ -9,6 +9,7 @@ import (
"testing" "testing"
"time" "time"
"fmt"
"github.com/fnproject/fn/api/datastore" "github.com/fnproject/fn/api/datastore"
"github.com/fnproject/fn/api/logs" "github.com/fnproject/fn/api/logs"
"github.com/fnproject/fn/api/models" "github.com/fnproject/fn/api/models"
@@ -40,7 +41,7 @@ func (test *routeTestCase) run(t *testing.T, i int, buf *bytes.Buffer) {
} }
if test.expectedError != nil { if test.expectedError != nil {
resp := getErrorResponse(t, rec) resp := getV1ErrorResponse(t, rec)
if resp.Error == nil { if resp.Error == nil {
t.Log(buf.String()) t.Log(buf.String())
t.Errorf("Test %d: Expected error message to have `%s`, but it was nil", t.Errorf("Test %d: Expected error message to have `%s`, but it was nil",
@@ -98,8 +99,7 @@ func (test *routeTestCase) run(t *testing.T, i int, buf *bytes.Buffer) {
func TestRouteCreate(t *testing.T) { func TestRouteCreate(t *testing.T) {
buf := setLogBuffer() buf := setLogBuffer()
a := &models.App{Name: "a"} a := &models.App{Name: "a", ID: "app_id"}
a.SetDefaults()
commonDS := datastore.NewMockInit([]*models.App{a}) commonDS := datastore.NewMockInit([]*models.App{a})
for i, test := range []routeTestCase{ for i, test := range []routeTestCase{
// errors // errors
@@ -133,8 +133,7 @@ func TestRouteCreate(t *testing.T) {
func TestRoutePut(t *testing.T) { func TestRoutePut(t *testing.T) {
buf := setLogBuffer() buf := setLogBuffer()
a := &models.App{Name: "a"} a := &models.App{Name: "a", ID: "app_id"}
a.SetDefaults()
commonDS := datastore.NewMockInit([]*models.App{a}) commonDS := datastore.NewMockInit([]*models.App{a})
for i, test := range []routeTestCase{ for i, test := range []routeTestCase{
@@ -153,15 +152,17 @@ func TestRoutePut(t *testing.T) {
{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", "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}, {commonDS, logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "route": { "image": "fnproject/fn-test-utils", "type": "sync" } }`, http.StatusOK, nil},
} { } {
test.run(t, i, buf) t.Run(fmt.Sprintf("case %d", i),
func(t *testing.T) {
test.run(t, i, buf)
})
} }
} }
func TestRouteDelete(t *testing.T) { func TestRouteDelete(t *testing.T) {
buf := setLogBuffer() buf := setLogBuffer()
a := &models.App{Name: "a"} a := &models.App{Name: "a", ID: "app_id"}
a.SetDefaults()
routes := []*models.Route{{AppID: a.ID, Path: "/myroute"}} routes := []*models.Route{{AppID: a.ID, Path: "/myroute"}}
commonDS := datastore.NewMockInit([]*models.App{a}, routes) commonDS := datastore.NewMockInit([]*models.App{a}, routes)
@@ -188,7 +189,7 @@ func TestRouteDelete(t *testing.T) {
} }
if test.expectedError != nil { if test.expectedError != nil {
resp := getErrorResponse(t, rec) resp := getV1ErrorResponse(t, rec)
if !strings.Contains(resp.Error.Message, test.expectedError.Error()) { if !strings.Contains(resp.Error.Message, test.expectedError.Error()) {
t.Log(buf.String()) t.Log(buf.String())
@@ -206,8 +207,7 @@ func TestRouteList(t *testing.T) {
rnr, cancel := testRunner(t) rnr, cancel := testRunner(t)
defer cancel() defer cancel()
app := &models.App{Name: "myapp"} app := &models.App{Name: "myapp", ID: "app_id"}
app.SetDefaults()
ds := datastore.NewMockInit( ds := datastore.NewMockInit(
[]*models.App{app}, []*models.App{app},
[]*models.Route{ []*models.Route{
@@ -262,7 +262,7 @@ func TestRouteList(t *testing.T) {
} }
if test.expectedError != nil { if test.expectedError != nil {
resp := getErrorResponse(t, rec) resp := getV1ErrorResponse(t, rec)
if !strings.Contains(resp.Error.Message, test.expectedError.Error()) { if !strings.Contains(resp.Error.Message, test.expectedError.Error()) {
t.Log(buf.String()) t.Log(buf.String())
@@ -315,7 +315,7 @@ func TestRouteGet(t *testing.T) {
} }
if test.expectedError != nil { if test.expectedError != nil {
resp := getErrorResponse(t, rec) resp := getV1ErrorResponse(t, rec)
if !strings.Contains(resp.Error.Message, test.expectedError.Error()) { if !strings.Contains(resp.Error.Message, test.expectedError.Error()) {
t.Log(buf.String()) t.Log(buf.String())

View File

@@ -21,13 +21,13 @@ import (
func (s *Server) handleFunctionCall(c *gin.Context) { func (s *Server) handleFunctionCall(c *gin.Context) {
err := s.handleFunctionCall2(c) err := s.handleFunctionCall2(c)
if err != nil { if err != nil {
handleErrorResponse(c, err) handleV1ErrorResponse(c, err)
} }
} }
// handleFunctionCall2 executes the function and returns an error // handleFunctionCall2 executes the function and returns an error
// Requires the following in the context: // Requires the following in the context:
// * "app_name" // * "app"
// * "path" // * "path"
func (s *Server) handleFunctionCall2(c *gin.Context) error { func (s *Server) handleFunctionCall2(c *gin.Context) error {
ctx := c.Request.Context() ctx := c.Request.Context()
@@ -81,7 +81,7 @@ func (s *Server) serve(c *gin.Context, app *models.App, path string) error {
return err return err
} }
model := call.Model() model := call.Model()
{ // scope this, to disallow ctx use outside of this scope. add id for handleErrorResponse logger { // 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}) ctx, _ := common.LoggerWithFields(c.Request.Context(), logrus.Fields{"id": model.ID})
c.Request = c.Request.WithContext(ctx) c.Request = c.Request.WithContext(ctx)
} }

View File

@@ -36,8 +36,7 @@ func testRouterAsync(ds models.Datastore, mq models.MessageQueue, rnr agent.Agen
func TestRouteRunnerAsyncExecution(t *testing.T) { func TestRouteRunnerAsyncExecution(t *testing.T) {
buf := setLogBuffer() buf := setLogBuffer()
app := &models.App{Name: "myapp", Config: map[string]string{"app": "true"}} app := &models.App{ID: "app_id", Name: "myapp", Config: map[string]string{"app": "true"}}
app.SetDefaults()
ds := datastore.NewMockInit( ds := datastore.NewMockInit(
[]*models.App{app}, []*models.App{app},
[]*models.Route{ []*models.Route{

View File

@@ -60,8 +60,7 @@ func testRunner(_ *testing.T, args ...interface{}) (agent.Agent, context.CancelF
func TestRouteRunnerGet(t *testing.T) { func TestRouteRunnerGet(t *testing.T) {
buf := setLogBuffer() buf := setLogBuffer()
app := &models.App{Name: "myapp", Config: models.Config{}} app := &models.App{ID: "app_id", Name: "myapp", Config: models.Config{}}
app.SetDefaults()
ds := datastore.NewMockInit( ds := datastore.NewMockInit(
[]*models.App{app}, []*models.App{app},
) )
@@ -90,7 +89,7 @@ func TestRouteRunnerGet(t *testing.T) {
} }
if test.expectedError != nil { if test.expectedError != nil {
resp := getErrorResponse(t, rec) resp := getV1ErrorResponse(t, rec)
if !strings.Contains(resp.Error.Message, test.expectedError.Error()) { if !strings.Contains(resp.Error.Message, test.expectedError.Error()) {
t.Log(buf.String()) t.Log(buf.String())
@@ -104,8 +103,7 @@ func TestRouteRunnerGet(t *testing.T) {
func TestRouteRunnerPost(t *testing.T) { func TestRouteRunnerPost(t *testing.T) {
buf := setLogBuffer() buf := setLogBuffer()
app := &models.App{Name: "myapp", Config: models.Config{}} app := &models.App{ID: "app_id", Name: "myapp", Config: models.Config{}}
app.SetDefaults()
ds := datastore.NewMockInit( ds := datastore.NewMockInit(
[]*models.App{app}, []*models.App{app},
) )
@@ -136,7 +134,7 @@ func TestRouteRunnerPost(t *testing.T) {
} }
if test.expectedError != nil { if test.expectedError != nil {
resp := getErrorResponse(t, rec) resp := getV1ErrorResponse(t, rec)
respMsg := resp.Error.Message respMsg := resp.Error.Message
expMsg := test.expectedError.Error() expMsg := test.expectedError.Error()
if respMsg != expMsg && !strings.Contains(respMsg, expMsg) { if respMsg != expMsg && !strings.Contains(respMsg, expMsg) {
@@ -162,8 +160,7 @@ func TestRouteRunnerExecEmptyBody(t *testing.T) {
rHdr := map[string][]string{"X-Function": {"Test"}} rHdr := map[string][]string{"X-Function": {"Test"}}
rImg := "fnproject/fn-test-utils" rImg := "fnproject/fn-test-utils"
app := &models.App{Name: "soup"} app := &models.App{ID: "app_id", Name: "soup"}
app.SetDefaults()
ds := datastore.NewMockInit( ds := datastore.NewMockInit(
[]*models.App{app}, []*models.App{app},
[]*models.Route{ []*models.Route{
@@ -243,8 +240,7 @@ func TestRouteRunnerExecution(t *testing.T) {
rImgBs1 := "fnproject/imagethatdoesnotexist" rImgBs1 := "fnproject/imagethatdoesnotexist"
rImgBs2 := "localhost:5050/fnproject/imagethatdoesnotexist" rImgBs2 := "localhost:5050/fnproject/imagethatdoesnotexist"
app := &models.App{Name: "myapp"} app := &models.App{ID: "app_id", Name: "myapp"}
app.SetDefaults()
ds := datastore.NewMockInit( ds := datastore.NewMockInit(
[]*models.App{app}, []*models.App{app},
[]*models.Route{ []*models.Route{
@@ -454,8 +450,7 @@ func (mock *errorMQ) Code() int {
func (mock *errorMQ) Close() error { return nil } func (mock *errorMQ) Close() error { return nil }
func TestFailedEnqueue(t *testing.T) { func TestFailedEnqueue(t *testing.T) {
buf := setLogBuffer() buf := setLogBuffer()
app := &models.App{Name: "myapp", Config: models.Config{}} app := &models.App{ID: "app_id", Name: "myapp", Config: models.Config{}}
app.SetDefaults()
ds := datastore.NewMockInit( ds := datastore.NewMockInit(
[]*models.App{app}, []*models.App{app},
[]*models.Route{ []*models.Route{
@@ -503,8 +498,7 @@ func TestRouteRunnerTimeout(t *testing.T) {
models.RouteMaxMemory = uint64(1024 * 1024 * 1024) // 1024 TB models.RouteMaxMemory = uint64(1024 * 1024 * 1024) // 1024 TB
hugeMem := uint64(models.RouteMaxMemory - 1) hugeMem := uint64(models.RouteMaxMemory - 1)
app := &models.App{Name: "myapp", Config: models.Config{}} app := &models.App{ID: "app_id", Name: "myapp", Config: models.Config{}}
app.SetDefaults()
ds := datastore.NewMockInit( ds := datastore.NewMockInit(
[]*models.App{app}, []*models.App{app},
[]*models.Route{ []*models.Route{
@@ -577,8 +571,7 @@ func TestRouteRunnerTimeout(t *testing.T) {
func TestRouteRunnerMinimalConcurrentHotSync(t *testing.T) { func TestRouteRunnerMinimalConcurrentHotSync(t *testing.T) {
buf := setLogBuffer() buf := setLogBuffer()
app := &models.App{Name: "myapp", Config: models.Config{}} app := &models.App{ID: "app_id", Name: "myapp", Config: models.Config{}}
app.SetDefaults()
ds := datastore.NewMockInit( ds := datastore.NewMockInit(
[]*models.App{app}, []*models.App{app},
[]*models.Route{ []*models.Route{
@@ -638,30 +631,3 @@ func TestRouteRunnerMinimalConcurrentHotSync(t *testing.T) {
} }
} }
} }
//func TestMatchRoute(t *testing.T) {
//buf := setLogBuffer()
//for i, test := range []struct {
//baseRoute string
//route string
//expectedParams []Param
//}{
//{"/myroute/", `/myroute/`, nil},
//{"/myroute/:mybigparam", `/myroute/1`, []Param{{"mybigparam", "1"}}},
//{"/:param/*test", `/1/2`, []Param{{"param", "1"}, {"test", "/2"}}},
//} {
//if params, match := matchRoute(test.baseRoute, test.route); match {
//if test.expectedParams != nil {
//for j, param := range test.expectedParams {
//if params[j].Key != param.Key || params[j].Value != param.Value {
//t.Log(buf.String())
//t.Errorf("Test %d: expected param %d, key = %s, value = %s", i, j, param.Key, param.Value)
//}
//}
//}
//} else {
//t.Log(buf.String())
//t.Errorf("Test %d: %s should match %s", i, test.route, test.baseRoute)
//}
//}
//}

View File

@@ -105,22 +105,24 @@ type Server struct {
Router *gin.Engine Router *gin.Engine
AdminRouter *gin.Engine AdminRouter *gin.Engine
webListenPort int webListenPort int
adminListenPort int adminListenPort int
grpcListenPort int grpcListenPort int
agent agent.Agent agent agent.Agent
datastore models.Datastore datastore models.Datastore
mq models.MessageQueue mq models.MessageQueue
logstore models.LogStore logstore models.LogStore
nodeType ServerNodeType nodeType ServerNodeType
cert string cert string
certKey string certKey string
certAuthority string certAuthority string
appListeners *appListeners appListeners *appListeners
routeListeners *routeListeners routeListeners *routeListeners
rootMiddlewares []fnext.Middleware fnListeners *fnListeners
apiMiddlewares []fnext.Middleware triggerListeners *triggerListeners
promExporter *prometheus.Exporter rootMiddlewares []fnext.Middleware
apiMiddlewares []fnext.Middleware
promExporter *prometheus.Exporter
// Extensions can append to this list of contexts so that cancellations are properly handled. // Extensions can append to this list of contexts so that cancellations are properly handled.
extraCtxs []context.Context extraCtxs []context.Context
} }
@@ -549,9 +551,11 @@ func New(ctx context.Context, opts ...ServerOption) *Server {
s.appListeners = new(appListeners) s.appListeners = new(appListeners)
s.routeListeners = new(routeListeners) s.routeListeners = new(routeListeners)
s.fnListeners = new(fnListeners)
s.triggerListeners = new(triggerListeners)
s.datastore = datastore.Wrap(s.datastore) s.datastore = datastore.Wrap(s.datastore)
s.datastore = fnext.NewDatastore(s.datastore, s.appListeners, s.routeListeners) s.datastore = fnext.NewDatastore(s.datastore, s.appListeners, s.routeListeners, s.fnListeners, s.triggerListeners)
s.logstore = logs.Wrap(s.logstore) s.logstore = logs.Wrap(s.logstore)
return s return s
@@ -886,19 +890,21 @@ func (s *Server) bindHandlers(ctx context.Context) {
v1 := clean.Group("") v1 := clean.Group("")
v1.Use(setAppNameInCtx) v1.Use(setAppNameInCtx)
v1.Use(s.apiMiddlewareWrapper()) v1.Use(s.apiMiddlewareWrapper())
v1.GET("/apps", s.handleAppList) v1.GET("/apps", s.handleV1AppList)
v1.POST("/apps", s.handleAppCreate) v1.POST("/apps", s.handleV1AppCreate)
{ {
apps := v1.Group("/apps/:app") apps := v1.Group("/apps/:appName")
apps.Use(appNameCheck) apps.Use(appNameCheck)
{ {
withAppCheck := apps.Group("") withAppCheck := apps.Group("")
withAppCheck.Use(s.checkAppPresenceByName()) withAppCheck.Use(s.checkAppPresenceByName())
withAppCheck.GET("", s.handleAppGetByName)
withAppCheck.PATCH("", s.handleAppUpdate) withAppCheck.GET("", s.handleV1AppGetByName)
withAppCheck.DELETE("", s.handleAppDelete) withAppCheck.PATCH("", s.handleV1AppUpdate)
withAppCheck.DELETE("", s.handleV1AppDelete)
withAppCheck.GET("/routes", s.handleRouteList) withAppCheck.GET("/routes", s.handleRouteList)
withAppCheck.GET("/routes/:route", s.handleRouteGetAPI) withAppCheck.GET("/routes/:route", s.handleRouteGetAPI)
withAppCheck.PATCH("/routes/*route", s.handleRoutesPatch) withAppCheck.PATCH("/routes/*route", s.handleRoutesPatch)
@@ -912,6 +918,30 @@ func (s *Server) bindHandlers(ctx context.Context) {
apps.PUT("/routes/*route", s.handleRoutesPostPut) apps.PUT("/routes/*route", s.handleRoutesPostPut)
} }
cleanv2 := engine.Group("/v2")
v2 := cleanv2.Group("")
v2.Use(s.apiMiddlewareWrapper())
{
v2.GET("/apps", s.handleAppList)
v2.POST("/apps", s.handleAppCreate)
v2.GET("/apps/:appId", s.handleAppGet)
v2.PUT("/apps/:appId", s.handleAppUpdate)
v2.DELETE("/apps/:appId", s.handleAppDelete)
v2.GET("/fns", s.handleFnList)
v2.POST("/fns", s.handleFnCreate)
v2.GET("/fns/:fnId", s.handleFnGet)
v2.PUT("/fns/:fnId", s.handleFnUpdate)
v2.DELETE("/fns/:fnId", s.handleFnDelete)
v2.GET("/triggers", s.handleTriggerList)
v2.POST("/triggers", s.handleTriggerCreate)
v2.GET("/triggers/:triggerId", s.handleTriggerGet)
v2.PUT("/triggers/:triggerId", s.handleTriggerUpdate)
v2.DELETE("/triggers/:triggerId", s.handleTriggerDelete)
}
{ {
runner := clean.Group("/runner") runner := clean.Group("/runner")
runner.PUT("/async", s.handleRunnerEnqueue) runner.PUT("/async", s.handleRunnerEnqueue)
@@ -920,10 +950,12 @@ func (s *Server) bindHandlers(ctx context.Context) {
runner.POST("/start", s.handleRunnerStart) runner.POST("/start", s.handleRunnerStart)
runner.POST("/finish", s.handleRunnerFinish) runner.POST("/finish", s.handleRunnerFinish)
appsAPIV2 := runner.Group("/apps/:app") runnerAppApi := runner.Group(
appsAPIV2.Use(setAppNameInCtx)
appsAPIV2.GET("", s.handleAppGetByID) "/apps/:appId")
appsAPIV2.GET("/routes/:route", s.handleRouteGetRunner) runnerAppApi.Use(setAppIDInCtx)
runnerAppApi.GET("", s.handleV1AppGetByName)
runnerAppApi.GET("/routes/:route", s.handleRouteGetRunner)
} }
} }
@@ -931,8 +963,8 @@ func (s *Server) bindHandlers(ctx context.Context) {
if s.nodeType != ServerTypeAPI { if s.nodeType != ServerTypeAPI {
runner := engine.Group("/r") runner := engine.Group("/r")
runner.Use(s.checkAppPresenceByNameAtRunner()) runner.Use(s.checkAppPresenceByNameAtRunner())
runner.Any("/:app", s.handleFunctionCall) runner.Any("/:appName", s.handleFunctionCall)
runner.Any("/:app/*route", s.handleFunctionCall) runner.Any("/:appName/*route", s.handleFunctionCall)
} }
} }
@@ -948,7 +980,7 @@ func (s *Server) bindHandlers(ctx context.Context) {
var e models.APIError = models.ErrPathNotFound var e models.APIError = models.ErrPathNotFound
err = models.NewAPIError(e.Code(), fmt.Errorf("%v: %s", e.Error(), c.Request.URL.Path)) err = models.NewAPIError(e.Code(), fmt.Errorf("%v: %s", e.Error(), c.Request.URL.Path))
} }
handleErrorResponse(c, err) handleV1ErrorResponse(c, err)
}) })
} }
@@ -986,7 +1018,8 @@ type appResponse struct {
App *models.App `json:"app"` App *models.App `json:"app"`
} }
type appsResponse struct { //TODO deprecate with V1
type appsV1Response struct {
Message string `json:"message"` Message string `json:"message"`
NextCursor string `json:"next_cursor"` NextCursor string `json:"next_cursor"`
Apps []*models.App `json:"apps"` Apps []*models.App `json:"apps"`
@@ -1013,3 +1046,18 @@ type callsResponse struct {
NextCursor string `json:"next_cursor"` NextCursor string `json:"next_cursor"`
Calls []*models.Call `json:"calls"` Calls []*models.Call `json:"calls"`
} }
type appListResponse struct {
NextCursor string `json:"next_cursor"`
Items []*models.App `json:"items"`
}
type fnListResponse struct {
NextCursor string `json:"next_cursor"`
Items []*models.Fn `json:"items"`
}
type triggerListResponse struct {
NextCursor string `json:"next_cursor"`
Items []*models.Trigger `json:"items"`
}

View File

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

View File

@@ -86,6 +86,15 @@ func newRouterRequest(t *testing.T, method, path string, body io.Reader) (*http.
return req, rec 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 { func getErrorResponse(t *testing.T, rec *httptest.ResponseRecorder) *models.Error {
var err models.Error var err models.Error
decodeErr := json.NewDecoder(rec.Body).Decode(&err) decodeErr := json.NewDecoder(rec.Body).Decode(&err)
@@ -150,14 +159,17 @@ func TestFullStack(t *testing.T) {
{"get deleted app", "GET", "/v1/apps/myapp", ``, http.StatusNotFound, 0}, {"get deleted app", "GET", "/v1/apps/myapp", ``, http.StatusNotFound, 0},
{"get deleteds route on deleted app", "GET", "/v1/apps/myapp/routes/myroute", ``, http.StatusNotFound, 0}, {"get deleteds 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))) 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 != 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)
}
} }
} }
@@ -268,8 +280,7 @@ func TestApiNode(t *testing.T) {
func TestHybridEndpoints(t *testing.T) { func TestHybridEndpoints(t *testing.T) {
buf := setLogBuffer() buf := setLogBuffer()
app := &models.App{Name: "myapp"} app := &models.App{ID: "app_id", Name: "myapp"}
app.SetDefaults()
ds := datastore.NewMockInit( ds := datastore.NewMockInit(
[]*models.App{app}, []*models.App{app},
[]*models.Route{{ []*models.Route{{

View File

@@ -0,0 +1,31 @@
package server
import (
"net/http"
"github.com/fnproject/fn/api/models"
"github.com/gin-gonic/gin"
)
func (s *Server) handleTriggerCreate(c *gin.Context) {
ctx := c.Request.Context()
trigger := &models.Trigger{}
err := c.BindJSON(trigger)
if err != nil {
if models.IsAPIError(err) {
handleErrorResponse(c, err)
} else {
handleErrorResponse(c, models.ErrInvalidJSON)
}
return
}
triggerCreated, err := s.datastore.InsertTrigger(ctx, trigger)
if err != nil {
handleErrorResponse(c, err)
return
}
c.JSON(http.StatusOK, triggerCreated)
}

View File

@@ -0,0 +1,20 @@
package server
import (
"net/http"
"github.com/fnproject/fn/api"
"github.com/gin-gonic/gin"
)
func (s *Server) handleTriggerDelete(c *gin.Context) {
ctx := c.Request.Context()
err := s.datastore.RemoveTrigger(ctx, c.Param(api.ParamTriggerID))
if err != nil {
handleErrorResponse(c, err)
return
}
c.JSON(http.StatusNoContent, nil)
}

20
api/server/trigger_get.go Normal file
View File

@@ -0,0 +1,20 @@
package server
import (
"net/http"
"github.com/fnproject/fn/api"
"github.com/gin-gonic/gin"
)
func (s *Server) handleTriggerGet(c *gin.Context) {
ctx := c.Request.Context()
trigger, err := s.datastore.GetTriggerByID(ctx, c.Param(api.ParamTriggerID))
if err != nil {
handleErrorResponse(c, err)
return
}
c.JSON(http.StatusOK, trigger)
}

View File

@@ -0,0 +1,42 @@
package server
import (
"encoding/base64"
"net/http"
"github.com/fnproject/fn/api/models"
"github.com/gin-gonic/gin"
)
func (s *Server) handleTriggerList(c *gin.Context) {
ctx := c.Request.Context()
filter := &models.TriggerFilter{}
filter.Cursor, filter.PerPage = pageParams(c, true)
filter.AppID = c.Query("app_id")
if filter.AppID == "" {
handleErrorResponse(c, models.ErrTriggerMissingAppID)
}
filter.FnID = c.Query("fn_id")
filter.Name = c.Query("name")
triggers, err := s.datastore.GetTriggers(ctx, filter)
if err != nil {
handleErrorResponse(c, err)
return
}
var nextCursor string
if len(triggers) > 0 && len(triggers) == filter.PerPage {
last := []byte(triggers[len(triggers)-1].ID)
nextCursor = base64.RawURLEncoding.EncodeToString(last)
}
c.JSON(http.StatusOK, triggerListResponse{
NextCursor: nextCursor,
Items: triggers,
})
}

View File

@@ -0,0 +1,77 @@
package server
import (
"context"
"github.com/fnproject/fn/api/models"
"github.com/fnproject/fn/fnext"
)
type triggerListeners []fnext.TriggerListener
var _ fnext.TriggerListener = new(triggerListeners)
func (t *triggerListeners) BeforeTriggerCreate(ctx context.Context, trigger *models.Trigger) error {
for _, l := range *t {
err := l.BeforeTriggerCreate(ctx, trigger)
if err != nil {
return err
}
}
return nil
}
func (t *triggerListeners) AfterTriggerCreate(ctx context.Context, trigger *models.Trigger) error {
for _, l := range *t {
err := l.AfterTriggerCreate(ctx, trigger)
if err != nil {
return err
}
}
return nil
}
func (t *triggerListeners) BeforeTriggerUpdate(ctx context.Context, trigger *models.Trigger) error {
for _, l := range *t {
err := l.BeforeTriggerUpdate(ctx, trigger)
if err != nil {
return err
}
}
return nil
}
func (t *triggerListeners) AfterTriggerUpdate(ctx context.Context, trigger *models.Trigger) error {
for _, l := range *t {
err := l.AfterTriggerUpdate(ctx, trigger)
if err != nil {
return err
}
}
return nil
}
func (t *triggerListeners) BeforeTriggerDelete(ctx context.Context, triggerID string) error {
for _, l := range *t {
err := l.BeforeTriggerDelete(ctx, triggerID)
if err != nil {
return err
}
}
return nil
}
func (t *triggerListeners) AfterTriggerDelete(ctx context.Context, triggerID string) error {
for _, l := range *t {
err := l.AfterTriggerDelete(ctx, triggerID)
if err != nil {
return err
}
}
return nil
}
// AddTriggerListener adds an TriggerListener for the server to use.
func (s *Server) AddTriggerListener(listener fnext.TriggerListener) {
*s.triggerListeners = append(*s.triggerListeners, listener)
}

384
api/server/trigger_test.go Normal file
View File

@@ -0,0 +1,384 @@
package server
import (
"bytes"
"encoding/base64"
"encoding/json"
"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"
)
const (
BaseRoute = "/v2/triggers"
)
func TestTriggerCreate(t *testing.T) {
buf := setLogBuffer()
defer func() {
if t.Failed() {
t.Log(buf.String())
}
}()
a := &models.App{ID: "appid"}
a2 := &models.App{ID: "appid2"}
fn := &models.Fn{ID: "fnid", AppID: a.ID}
fn.SetDefaults()
commonDS := datastore.NewMockInit([]*models.App{a, a2}, []*models.Fn{fn})
for i, test := range []struct {
mock models.Datastore
logDB models.LogStore
path string
body string
expectedCode int
expectedError error
}{
// errors
{commonDS, logs.NewMock(), BaseRoute, ``, http.StatusBadRequest, models.ErrInvalidJSON},
{commonDS, logs.NewMock(), BaseRoute, `{}`, http.StatusNotFound, models.ErrAppsNotFound},
{commonDS, logs.NewMock(), BaseRoute, `{"app_id":"appid"}`, http.StatusNotFound, models.ErrFnsNotFound},
{commonDS, logs.NewMock(), BaseRoute, `{"app_id":"appid", "fn_id":"fnid"}`, http.StatusBadRequest, models.ErrTriggerMissingName},
{commonDS, logs.NewMock(), BaseRoute, `{"app_id":"appid", "fn_id":"fnid", "name": "Test" }`, http.StatusBadRequest, models.ErrTriggerTypeUnknown},
{commonDS, logs.NewMock(), BaseRoute, `{ "name": "Test", "app_id": "appid", "fn_id": "fnid", "type":"http"}`, http.StatusBadRequest, models.ErrTriggerMissingSource},
{commonDS, logs.NewMock(), BaseRoute, `{ "name": "1234567890123456789012345678901", "app_id": "appid", "fn_id": "fnid", "type":"http"}`, http.StatusBadRequest, models.ErrTriggerTooLongName},
{commonDS, logs.NewMock(), BaseRoute, `{ "name": "&&%@!#$#@$","app_id": "appid", "fn_id": "fnid", "type":"http" }`, http.StatusBadRequest, models.ErrTriggerInvalidName},
{commonDS, logs.NewMock(), BaseRoute, `{ "name": "trigger", "app_id": "appid", "fn_id": "fnid", "type": "http", "source": "src", "annotations" : { "":"val" }}`, http.StatusBadRequest, models.ErrInvalidAnnotationKey},
{commonDS, logs.NewMock(), BaseRoute, `{ "id": "asdasca", "name": "trigger", "app_id": "appid", "fn_id": "fnid", "type": "http", "source": "src"}`, http.StatusBadRequest, models.ErrTriggerIDProvided},
{commonDS, logs.NewMock(), BaseRoute, `{ "name": "trigger", "app_id": "appid", "fn_id": "fnid", "type": "unsupported", "source": "src"}`, http.StatusBadRequest, models.ErrTriggerTypeUnknown},
{commonDS, logs.NewMock(), BaseRoute, `{ "name": "trigger", "app_id": "appid2", "fn_id": "fnid", "type": "http", "source": "src"}`, http.StatusBadRequest, models.ErrTriggerFnIDNotSameApp},
// // success
{commonDS, logs.NewMock(), BaseRoute, `{ "name": "trigger", "app_id": "appid", "fn_id": "fnid", "type": "http", "source": "src"}`, http.StatusOK, nil},
//repeated name
{commonDS, logs.NewMock(), BaseRoute, `{ "name": "trigger", "app_id": "appid", "fn_id": "fnid", "type": "http", "source": "src"}`, http.StatusConflict, 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 := getErrorResponse(t, rec)
if !strings.Contains(resp.Message, test.expectedError.Error()) {
t.Errorf("Test %d: Expected error message to have `%s` but got `%s`",
i, test.expectedError.Error(), resp.Message)
}
}
if test.expectedCode == http.StatusOK {
var trigger models.Trigger
err := json.NewDecoder(rec.Body).Decode(&trigger)
if err != nil {
t.Log(buf.String())
t.Errorf("Test %d: error decoding body for 'ok' json, it was a lie: %v", i, err)
}
if trigger.ID == "" {
t.Fatalf("Missing ID ")
}
// IsZero() doesn't really work, this ensures it's not unset as long as we're not in 1970
if time.Time(trigger.CreatedAt).Before(time.Now().Add(-1 * time.Hour)) {
t.Log(buf.String())
t.Errorf("Test %d: expected created_at to be set on trigger, it wasn't: %s", i, trigger.CreatedAt)
}
if !(time.Time(trigger.CreatedAt)).Equal(time.Time(trigger.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, trigger.CreatedAt, trigger.UpdatedAt)
}
_, rec := routerRequest(t, router, "GET", BaseRoute+"/"+trigger.ID, body)
if rec.Code != http.StatusOK {
t.Log(buf.String())
t.Errorf("Test %d: Expected to be able to GET trigger after successful PUT: %d", i, rec.Code)
}
var triggerGet models.Trigger
err = json.NewDecoder(rec.Body).Decode(&triggerGet)
if err != nil {
t.Log(buf.String())
t.Errorf("Test %d: error decoding body for GET 'ok' json, it was a lie: %v", i, err)
}
if !triggerGet.Equals(&trigger) {
t.Errorf("Test %d: GET trigger should match result of PUT trigger: %v, %v", i, triggerGet, trigger)
}
cancel()
}
}
}
func TestTriggerDelete(t *testing.T) {
buf := setLogBuffer()
defer func() {
if t.Failed() {
t.Log(buf.String())
}
}()
trig := &models.Trigger{
ID: "triggerid",
}
ds := datastore.NewMockInit([]*models.Trigger{trig})
for i, test := range []struct {
ds models.Datastore
logDB models.LogStore
path string
body string
expectedCode int
expectedError error
}{
{datastore.NewMock(), logs.NewMock(), BaseRoute + "/triggerid", "", http.StatusNotFound, nil},
{ds, logs.NewMock(), BaseRoute + "/triggerid", "", http.StatusNoContent, 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 := getErrorResponse(t, rec)
if !strings.Contains(resp.Message, test.expectedError.Error()) {
t.Errorf("Test %d: Expected error message to have `%s`",
i, test.expectedError.Error())
}
}
cancel()
}
}
func TestTriggerList(t *testing.T) {
buf := setLogBuffer()
defer func() {
if t.Failed() {
t.Log(buf.String())
}
}()
rnr, cancel := testRunner(t)
defer cancel()
app1 := &models.App{ID: "app_id1", Name: "myapp1"}
app2 := &models.App{ID: "app_id2", Name: "myapp2"}
fn1 := &models.Fn{ID: "fn_id1", Name: "myfn1"}
fn2 := &models.Fn{ID: "fn_id2", Name: "myfn2"}
fn3 := &models.Fn{ID: "fn_id3", Name: "myfn3"}
ds := datastore.NewMockInit(
[]*models.App{app1, app2},
[]*models.Fn{fn1, fn2, fn3},
[]*models.Trigger{
{ID: "trigger1", AppID: app1.ID, FnID: fn1.ID, Name: "trigger1"},
{ID: "trigger2", AppID: app1.ID, FnID: fn1.ID, Name: "trigger2"},
{ID: "trigger3", AppID: app1.ID, FnID: fn1.ID, Name: "trigger3"},
{ID: "trigger4", AppID: app1.ID, FnID: fn2.ID, Name: "trigger4"},
{ID: "trigger5", AppID: app2.ID, FnID: fn3.ID, Name: "trigger5"},
},
)
fnl := logs.NewMock()
srv := testServer(ds, &mqs.Mock{}, fnl, rnr, ServerTypeFull)
a1b := base64.RawURLEncoding.EncodeToString([]byte("trigger1"))
a2b := base64.RawURLEncoding.EncodeToString([]byte("trigger2"))
a3b := base64.RawURLEncoding.EncodeToString([]byte("trigger3"))
for i, test := range []struct {
path string
body string
expectedCode int
expectedError error
expectedLen int
nextCursor string
}{
{"/v2/triggers?per_page", "", http.StatusBadRequest, nil, 0, ""},
{"/v2/triggers?app_id=app_id1", "", http.StatusOK, nil, 4, ""},
{"/v2/triggers?app_id=app_id1&name=trigger1", "", http.StatusOK, nil, 1, ""},
{"/v2/triggers?app_id=app_id1&fn_id=fn_id1", "", http.StatusOK, nil, 3, ""},
{"/v2/triggers?app_id=app_id1&fn_id=fn_id1&per_page", "", http.StatusOK, nil, 3, ""},
{"/v2/triggers?app_id=app_id1&fn_id=fn_id1&per_page=1", "", http.StatusOK, nil, 1, a1b},
{"/v2/triggers?app_id=app_id1&fn_id=fn_id1&per_page=1&cursor=" + a1b, "", http.StatusOK, nil, 1, a2b},
{"/v2/triggers?app_id=app_id1&fn_id=fn_id1&per_page=1&cursor=" + a2b, "", http.StatusOK, nil, 1, a3b},
{"/v2/triggers?app_id=app_id1&fn_id=fn_id1&per_page=100&cursor=" + a2b, "", http.StatusOK, nil, 1, ""}, // cursor is empty if per_page > len(results)
{"/v2/triggers?app_id=app_id1&fn_id=fn_id1&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)
resp := getErrorResponse(t, rec)
t.Errorf("Message %s", resp.Message)
}
if test.expectedError != nil {
resp := getErrorResponse(t, rec)
if !strings.Contains(resp.Message, test.expectedError.Error()) {
t.Errorf("Test %d: Expected error message to have `%s`",
i, test.expectedError.Error())
}
} else {
// normal path
var resp triggerListResponse
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.Items) != test.expectedLen {
t.Errorf("Test %d: Expected triggers 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)
}
}
}
}
func TestTriggerGet(t *testing.T) {
buf := setLogBuffer()
defer func() {
if t.Failed() {
t.Log(buf.String())
}
}()
a := &models.App{ID: "appid"}
fn := &models.Fn{ID: "fnid"}
fn.SetDefaults()
trig := &models.Trigger{ID: "triggerid"}
commonDS := datastore.NewMockInit([]*models.App{a}, []*models.Fn{fn}, []*models.Trigger{trig})
for i, test := range []struct {
mock models.Datastore
logDB models.LogStore
path string
expectedCode int
}{
{commonDS, logs.NewMock(), BaseRoute + "/notexist", http.StatusNotFound},
{commonDS, logs.NewMock(), BaseRoute + "/triggerid", http.StatusOK},
} {
rnr, cancel := testRunner(t)
defer cancel()
srv := testServer(test.mock, &mqs.Mock{}, test.logDB, rnr, ServerTypeFull)
router := srv.Router
_, rec := routerRequest(t, router, "GET", test.path, bytes.NewBuffer([]byte("")))
if rec.Code != test.expectedCode {
t.Errorf("Test %d: Expected status code to be %d but was %d",
i, test.expectedCode, rec.Code)
}
var triggerGet models.Trigger
err := json.NewDecoder(rec.Body).Decode(&triggerGet)
if err != nil {
t.Errorf("Test %d: Expected to decode json: %s", i, err)
}
}
}
func TestTriggerUpdate(t *testing.T) {
buf := setLogBuffer()
defer func() {
if t.Failed() {
t.Log(buf.String())
}
}()
a := &models.App{ID: "appid"}
fn := &models.Fn{ID: "fnid"}
fn.SetDefaults()
trig := &models.Trigger{ID: "triggerid",
Name: "Name",
AppID: "appid",
FnID: "fnid",
Type: "http",
Source: "source"}
commonDS := datastore.NewMockInit([]*models.App{a}, []*models.Fn{fn}, []*models.Trigger{trig})
for i, test := range []struct {
mock models.Datastore
logDB models.LogStore
path string
body string
name string
expectedCode int
expectedError error
}{
{commonDS, logs.NewMock(), BaseRoute + "/notexist", `{"id": "triggerid", "name":"changed"}`, "", http.StatusBadRequest, nil},
{commonDS, logs.NewMock(), BaseRoute + "/notexist", `{"id": "notexist", "name":"changed"}`, "", http.StatusNotFound, nil},
{commonDS, logs.NewMock(), BaseRoute + "/triggerid", `{"id": "nonmatching", "name":"changed}`, "", http.StatusBadRequest, models.ErrTriggerIDMismatch},
{commonDS, logs.NewMock(), BaseRoute + "/triggerid", `{"id": "triggerid", "name":"changed"}`, "changed", http.StatusOK, nil},
{commonDS, logs.NewMock(), BaseRoute + "/triggerid", `{"name":"again"}`, "again", http.StatusOK, nil},
} {
rnr, cancel := testRunner(t)
defer cancel()
srv := testServer(test.mock, &mqs.Mock{}, test.logDB, rnr, ServerTypeFull)
router := srv.Router
body := bytes.NewBuffer([]byte(test.body))
_, rec := routerRequest(t, router, "PUT", 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 := getErrorResponse(t, rec)
if !strings.Contains(resp.Message, test.expectedError.Error()) {
t.Errorf("Test %d: Expected error message to have `%s` but got `%s`",
i, test.expectedError.Error(), resp.Message)
}
}
}
if rec.Code == http.StatusOK {
_, rec := routerRequest(t, router, "GET", BaseRoute+"/triggerid", bytes.NewBuffer([]byte("")))
var triggerGet models.Trigger
err := json.NewDecoder(rec.Body).Decode(&triggerGet)
if err != nil {
t.Errorf("Test %d: Expected to decode json: %s", i, err)
}
trig.Name = test.name
if !triggerGet.Equals(trig) {
t.Errorf("Test%d: trigger should be updated: %v : %v", i, trig, triggerGet)
}
}
}
}

View File

@@ -0,0 +1,41 @@
package server
import (
"net/http"
"github.com/fnproject/fn/api"
"github.com/fnproject/fn/api/models"
"github.com/gin-gonic/gin"
)
func (s *Server) handleTriggerUpdate(c *gin.Context) {
trigger := &models.Trigger{}
err := c.BindJSON(trigger)
if err != nil {
if models.IsAPIError(err) {
handleErrorResponse(c, err)
} else {
handleErrorResponse(c, models.ErrInvalidJSON)
}
return
}
pathTriggerID := c.Param(api.ParamTriggerID)
if trigger.ID == "" {
trigger.ID = pathTriggerID
} else {
if pathTriggerID != trigger.ID {
handleErrorResponse(c, models.ErrTriggerIDMismatch)
}
}
triggerUpdated, err := s.datastore.UpdateTrigger(c, trigger)
if err != nil {
handleErrorResponse(c, err)
return
}
c.JSON(http.StatusOK, triggerUpdated)
}

646
docs/swagger_v2.yml Normal file
View File

@@ -0,0 +1,646 @@
swagger: '2.0'
info:
title: fn
description: The open source serverless platform.
version: "2.0.0"
# the domain of the service
host: "127.0.0.1:8080"
# array of all schemes that your API supports
schemes:
- https
- http
# will be prefixed to all paths
basePath: /v2
consumes:
- application/json
produces:
- application/json
paths:
/apps:
get:
operationId: "ListApps"
summary: "Get applications"
description: "Get a filtered applications returned in alphabetical order."
tags:
- Apps
parameters:
- $ref: '#/parameters/cursor'
- $ref: '#/parameters/perPage'
- name: name
in: query
description: Application name to filter by
required: false
type: string
responses:
200:
description: List of apps.
schema:
$ref: '#/definitions/AppList'
default:
description: Unexpected error
schema:
$ref: '#/definitions/Error'
post:
operationId: "CreateApp"
summary: "Post new app"
description: "Insert a new app"
tags:
- Apps
parameters:
- name: body
in: body
description: App to modify.
required: true
schema:
$ref: '#/definitions/App'
responses:
200:
description: App details and stats.
schema:
$ref: '#/definitions/App'
400:
description: Parameters are missing or invalid.
schema:
$ref: '#/definitions/Error'
409:
description: App already exists.
schema:
$ref: '#/definitions/Error'
default:
description: Unexpected error
schema:
$ref: '#/definitions/Error'
/apps/{appID}:
delete:
operationId: "DeleteApp"
summary: "Delete an app."
description: "Delete an app."
tags:
- Apps
parameters:
- $ref: '#/parameters/AppID'
responses:
200:
description: Apps successfully deleted.
404:
description: App does not exist.
schema:
$ref: '#/definitions/Error'
default:
description: Unexpected error
schema:
$ref: '#/definitions/Error'
get:
operationId: "GetApp"
summary: "Get information for a app."
description: "This gives more details about a app, such as statistics."
tags:
- Apps
parameters:
- $ref: '#/parameters/AppID'
responses:
200:
description: App details and stats.
schema:
$ref: '#/definitions/App'
404:
description: App does not exist.
schema:
$ref: '#/definitions/Error'
default:
description: Unexpected error
schema:
$ref: '#/definitions/Error'
put:
operationId: "UpdateApp"
summary: "Update an app."
description: "Updates and application."
tags:
- Apps
parameters:
- $ref: '#/parameters/AppID'
- name: body
in: body
description: App to modify.
required: true
schema:
$ref: '#/definitions/App'
responses:
200:
description: App details and stats.
schema:
$ref: '#/definitions/App'
404:
description: App does not exist.
schema:
$ref: '#/definitions/Error'
400:
description: Parameters are missing or invalid.
schema:
$ref: '#/definitions/Error'
default:
description: Unexpected error
schema:
$ref: '#/definitions/Error'
/fns:
get:
operationId: "ListFns"
summary: "Get all fns"
description: "Get a list of all the Functions in alphabetical order."
tags:
- Fns
parameters:
- $ref: '#/parameters/AppIDQuery'
- $ref: '#/parameters/cursor'
- $ref: '#/parameters/perPage'
- name: name
in: query
description: Function name to filter by
required: false
type: string
responses:
200:
description: List of fns.
schema:
$ref: '#/definitions/FnList'
default:
description: Error
schema:
$ref: '#/definitions/Error'
post:
operationId: "CreateFn"
summary: "Create a fn"
description: "Creates a new Function, returning the complete entity."
tags:
- Fns
parameters:
- name: body
in: body
description: Fn to upsert
required: true
schema:
$ref: '#/definitions/Fn'
responses:
200:
description: Fn.
schema:
$ref: '#/definitions/Fn'
409:
description: Fn with name already exists.
schema:
$ref: '#/definitions/Error'
400:
description: Invalid Fn
schema:
$ref: '#/definitions/Error'
default:
description: Error
schema:
$ref: '#/definitions/Error'
/fns/{fnID}:
get:
operationId: "GetFn"
summary: "Get definition for a function"
description: "Get definition for a function."
tags:
- Fns
parameters:
- $ref: '#/parameters/FnID'
responses:
200:
description: Function definition
schema:
$ref: '#/definitions/Fn'
404:
description: Function does not exist.
schema:
$ref: '#/definitions/Error'
default:
description: Error
schema:
$ref: '#/definitions/Error'
put:
operationId: "UpdateFn"
summary: "Updates a fn"
description: "Updates a Function via merging the provided values."
tags:
- Fns
parameters:
- $ref: '#/parameters/FnID'
- name: body
in: body
description: Fn data to merge with current value
required: true
schema:
$ref: '#/definitions/Fn'
responses:
200:
description: Fn metadata
schema:
$ref: '#/definitions/Fn'
404:
description: Fn does not exist.
schema:
$ref: '#/definitions/Error'
400:
description: Parameters are missing or invalid.
schema:
$ref: '#/definitions/Error'
default:
description: Error
schema:
$ref: '#/definitions/Error'
delete:
operationId: "DeleteFn"
summary: "Delete a fn"
description: "Delete a function."
tags:
- Fns
parameters:
- $ref: '#/parameters/FnID'
responses:
204:
description: Fn successfully deleted
404:
description: Fn does not exist.
schema:
$ref: '#/definitions/Error'
default:
description: Error
schema:
$ref: '#/definitions/Error'
/triggers:
get:
operationId: "ListTriggers"
summary: List triggers associated with app
description: This will list all Triggers for a particular Application, returned in name alphabetical order.
tags:
- Triggers
parameters:
- $ref: '#/parameters/AppIDQuery'
- $ref: '#/parameters/FnIDQuery'
- name: name
in: query
description: Trigger name to filter by
required: false
type: string
- $ref: '#/parameters/cursor'
- $ref: '#/parameters/perPage'
responses:
200:
description: Trigger data
schema:
$ref: '#/definitions/TriggerList'
default:
description: Error
schema:
$ref: '#/definitions/Error'
post:
operationId: "CreateTrigger"
summary: Create a Trigger.
description: Creates a Trigger.
tags:
- Triggers
parameters:
- name: body
in: body
description: Trigger to create.
required: true
schema:
$ref: '#/definitions/Trigger'
responses:
200:
description: Created Triggers data
schema:
$ref: '#/definitions/Trigger'
409:
description: Trigger with name already exists.
schema:
$ref: '#/definitions/Error'
400:
description: Invalid Trigger
schema:
$ref: '#/definitions/Error'
default:
description: Error
schema:
$ref: '#/definitions/Error'
/triggers/{triggerID}:
get:
operationId: "GetTrigger"
summary: Gets Trigger by ID
description: Gets a Trigger by ID.
tags:
- Triggers
parameters:
- $ref: '#/parameters/TriggerID'
responses:
200:
description: Trigger information
schema:
$ref: '#/definitions/Trigger'
404:
description: Trigger does not exist.
schema:
$ref: '#/definitions/Error'
default:
description: Error
schema:
$ref: '#/definitions/Error'
put:
operationId: "UpdateTrigger"
summary: Update a Trigger
description: Updates a Trigger by merging the provided values.
tags:
- Triggers
parameters:
- $ref: '#/parameters/TriggerID'
- name: body
in: body
description: Trigger values to merge into current value.
required: true
schema:
$ref: '#/definitions/Trigger'
responses:
200:
description: Created Triggers data
schema:
$ref: '#/definitions/Trigger'
404:
description: Trigger does not exist.
schema:
$ref: '#/definitions/Error'
400:
description: Parameters are missing or invalid.
schema:
$ref: '#/definitions/Error'
default:
description: Error
schema:
$ref: '#/definitions/Error'
delete:
operationId: "DeleteTrigger"
summary: Deletes the Trigger
description: Deletes the Trigger.
tags:
- Triggers
parameters:
- $ref: '#/parameters/TriggerID'
responses:
404:
description: Trigger does not exist.
schema:
$ref: '#/definitions/Error'
204:
description: Trigger successfully deleted.
default:
description: Error
schema:
$ref: '#/definitions/Error'
definitions:
App:
type: object
properties:
id:
type: string
description: App ID
readOnly: true
name:
type: string
description: "Name of this app. Must be different than the image name. Can ony contain alphanumeric, -, and _."
readOnly: true
config:
type: object
description: Application function configuration, applied to all routes.
additionalProperties:
type: string
annotations:
type: object
description: Application annotations - this is a map of annotations attached to this app, keys must not exceed 128 bytes and must consist of non-whitespace printable ascii characters, and the seralized representation of individual values must not exeed 512 bytes
additionalProperties:
type: object
syslog_url:
type: string
description: A comma separated list of syslog urls to send all function logs to. supports tls, udp or tcp. e.g. tls://logs.papertrailapp.com:1
created_at:
type: string
format: date-time
description: Time when app was created. Always in UTC.
readOnly: true
updated_at:
type: string
format: date-time
description: Most recent time that app was updated. Always in UTC.
readOnly: true
Fn:
type: object
properties:
id:
type: string
description: Unique identifier
readOnly: true
name:
type: string
description: unique name for this function.
app_id:
type: string
description: App ID.
image:
type: string
description: "full container image name, e.g. hub.docker.com/fnproject/yo or fnproject/yo (default registry: hub.docker.com)"
mem:
type: integer
format: uint64
description: Max usable memory given to function (MiB).
timeout:
type: integer
default: 30
format: int32
description: Timeout for executions of a function. Value in Seconds
idle_timeout:
type: integer
default: 30
format: int32
description: Hot functions idle timeout before container termination. Value in Seconds
config:
type: object
description: Func configuration key values
additionalProperties:
type: string
format:
enum:
- default
- http
- json
- cloudevent
description: Payload format sent into function.
type: string
annotations:
type: object
description: Func annotations - this is a map of annotations attached to this func, keys must not exceed 128 bytes and must consist of non-whitespace printable ascii characters, and the seralized representation of individual values must not exeed 512 bytes
additionalProperties:
type: object
created_at:
type: string
format: date-time
description: Time when function was created. Always in UTC RFC3339.
readOnly: true
updated_at:
type: string
format: date-time
description: Most recent time that function was updated. Always in UTC RFC3339.
readOnly: true
FnList:
type: object
required:
- items
properties:
next_cursor:
type: string
description: cursor to send with subsequent request to receive the next page, if non-empty
readOnly: true
items:
type: array
items:
$ref: '#/definitions/Fn'
Trigger:
type: object
properties:
id:
type: string
description: Unique trigger identifier
readOnly: true
name:
type: string
description: unique name for this trigger, used to identify this trigger
type:
type: string
description: class of trigger, e.g. schedule, http, queue
source:
type: string
description: uri path for this trigger. e.g. `sayHello`, `say/hello`
fn_id:
type: string
description: opaque, unique function identifier
readOnly: true
app_id:
type: string
description: opaque, unique application identifier
readOnly: true
annotations:
type: object
description: Trigger annotations - this is a map of annotations attached to this trigger, keys must not exceed 128 bytes and must consist of non-whitespace printable ascii characters, and the seralized representation of individual values must not exeed 512 bytes
additionalProperties:
type: object
created_at:
type: string
format: date-time
description: Time when trigger was created. Always in UTC.
readOnly: true
updated_at:
type: string
format: date-time
description: Most recent time that trigger was updated. Always in UTC.
readOnly: true
TriggerList:
type: object
required:
- items
properties:
next_cursor:
type: string
description: cursor to send with subsequent request to receive the next page, if non-empty
readOnly: true
items:
type: array
items:
$ref: '#/definitions/Trigger'
AppList:
type: object
required:
- items
properties:
next_cursor:
type: string
description: cursor to send with subsequent request to receive the next page, if non-empty
readOnly: true
items:
type: array
items:
$ref: '#/definitions/App'
ErrorBody:
type: object
properties:
message:
type: string
readOnly: true
fields:
type: string
readOnly: true
Error:
type: object
properties:
error:
$ref: '#/definitions/ErrorBody'
parameters:
cursor:
name: cursor
description: Cursor from previous response.next_cursor to begin results after, if any.
required: false
type: string
in: query
perPage:
name: per_page
description: Number of results to return, defaults to 30. Max of 100.
required: false
type: integer
in: query
AppID:
name: appID
in: path
description: Opaque Unique application ID
required: true
type: string
FnID:
name: fnID
in: path
description: Function ID.
required: true
type: string
TriggerID:
name: triggerID
in: path
description: Trigger ID.
required: true
type: string
FnIDQuery:
name: fn_id
in: query
description: Function ID.
required: false
type: string
AppIDQuery:
name: app_id
in: query
description: Function ID.
required: false
type: string

View File

@@ -6,11 +6,13 @@ import (
"github.com/fnproject/fn/api/models" "github.com/fnproject/fn/api/models"
) )
func NewDatastore(ds models.Datastore, al AppListener, rl RouteListener) models.Datastore { func NewDatastore(ds models.Datastore, al AppListener, rl RouteListener, fl FnListener, tl TriggerListener) models.Datastore {
return &extds{ return &extds{
Datastore: ds, Datastore: ds,
al: al, al: al,
rl: rl, rl: rl,
fl: fl,
tl: tl,
} }
} }
@@ -18,6 +20,53 @@ type extds struct {
models.Datastore models.Datastore
al AppListener al AppListener
rl RouteListener rl RouteListener
fl FnListener
tl TriggerListener
}
func (e *extds) InsertTrigger(ctx context.Context, trigger *models.Trigger) (*models.Trigger, error) {
err := e.tl.BeforeTriggerCreate(ctx, trigger)
if err != nil {
return nil, err
}
t, err := e.Datastore.InsertTrigger(ctx, trigger)
if err != nil {
return nil, err
}
err = e.tl.AfterTriggerCreate(ctx, t)
return t, err
}
func (e *extds) UpdateTrigger(ctx context.Context, trigger *models.Trigger) (*models.Trigger, error) {
err := e.tl.BeforeTriggerUpdate(ctx, trigger)
if err != nil {
return nil, err
}
t, err := e.Datastore.UpdateTrigger(ctx, trigger)
if err != nil {
return nil, err
}
err = e.tl.AfterTriggerUpdate(ctx, t)
return t, err
}
func (e *extds) RemoveTrigger(ctx context.Context, triggerID string) error {
err := e.tl.BeforeTriggerDelete(ctx, triggerID)
if err != nil {
return err
}
err = e.Datastore.RemoveTrigger(ctx, triggerID)
if err != nil {
return err
}
err = e.tl.AfterTriggerDelete(ctx, triggerID)
return err
} }
func (e *extds) GetAppByID(ctx context.Context, appID string) (*models.App, error) { func (e *extds) GetAppByID(ctx context.Context, appID string) (*models.App, error) {
@@ -126,14 +175,70 @@ func (e *extds) UpdateRoute(ctx context.Context, route *models.Route) (*models.R
return route, err return route, err
} }
func (e *extds) RemoveRoute(ctx context.Context, appName string, routePath string) error { func (e *extds) RemoveRoute(ctx context.Context, appId string, routePath string) error {
err := e.rl.BeforeRouteDelete(ctx, appName, routePath) err := e.rl.BeforeRouteDelete(ctx, appId, routePath)
if err != nil { if err != nil {
return err return err
} }
err = e.Datastore.RemoveRoute(ctx, appName, routePath) err = e.Datastore.RemoveRoute(ctx, appId, routePath)
if err != nil { if err != nil {
return err return err
} }
return e.rl.AfterRouteDelete(ctx, appName, routePath) return e.rl.AfterRouteDelete(ctx, appId, routePath)
}
func (e *extds) InsertFn(ctx context.Context, fn *models.Fn) (*models.Fn, error) {
err := e.fl.BeforeFnCreate(ctx, fn)
if err != nil {
return nil, err
}
f, err := e.Datastore.InsertFn(ctx, fn)
if err != nil {
return nil, err
}
err = e.fl.AfterFnCreate(ctx, fn)
if err != nil {
return nil, err
}
return f, nil
}
func (e *extds) UpdateFn(ctx context.Context, fn *models.Fn) (*models.Fn, error) {
err := e.fl.BeforeFnUpdate(ctx, fn)
if err != nil {
return nil, err
}
f, err := e.Datastore.UpdateFn(ctx, fn)
if err != nil {
return nil, err
}
err = e.fl.AfterFnUpdate(ctx, fn)
if err != nil {
return nil, err
}
return f, nil
}
func (e *extds) RemoveFn(ctx context.Context, fnID string) error {
err := e.fl.BeforeFnDelete(ctx, fnID)
if err != nil {
return err
}
err = e.Datastore.RemoveFn(ctx, fnID)
if err != nil {
return err
}
err = e.fl.AfterFnDelete(ctx, fnID)
if err != nil {
return err
}
return nil
} }

View File

@@ -51,9 +51,41 @@ type RouteListener interface {
// AfterRouteUpdate called after route updated in datastore // AfterRouteUpdate called after route updated in datastore
AfterRouteUpdate(ctx context.Context, route *models.Route) error AfterRouteUpdate(ctx context.Context, route *models.Route) error
// BeforeRouteDelete called before route deleted from the datastore // BeforeRouteDelete called before route deleted from the datastore
BeforeRouteDelete(ctx context.Context, appName string, routePath string) error BeforeRouteDelete(ctx context.Context, appId string, routePath string) error
// AfterRouteDelete called after route deleted from the datastore // AfterRouteDelete called after route deleted from the datastore
AfterRouteDelete(ctx context.Context, appName string, routePath string) error AfterRouteDelete(ctx context.Context, appId string, routePath string) error
}
// FnListener enables callbacks around Fn events
type FnListener interface {
// BeforeFnCreate called before fn created in the datastore
BeforeFnCreate(ctx context.Context, fn *models.Fn) error
// AfterFnCreate called after fn create in the datastore
AfterFnCreate(ctx context.Context, fn *models.Fn) error
// BeforeFnUpdate called before fn update in datastore
BeforeFnUpdate(ctx context.Context, fn *models.Fn) error
// AfterFnUpdate called after fn updated in datastore
AfterFnUpdate(ctx context.Context, fn *models.Fn) error
// BeforeFnDelete called before fn deleted from the datastore
BeforeFnDelete(ctx context.Context, fnID string) error
// AfterFnDelete called after fn deleted from the datastore
AfterFnDelete(ctx context.Context, fnID string) error
}
//// TriggerListener enables callbacks around Trigger events
type TriggerListener interface {
// BeforeTriggerCreate called before trigger created in the datastore
BeforeTriggerCreate(ctx context.Context, trigger *models.Trigger) error
// AfterTriggerCreate called after trigger create in the datastore
AfterTriggerCreate(ctx context.Context, trigger *models.Trigger) error
// BeforeTriggerUpdate called before trigger update in datastore
BeforeTriggerUpdate(ctx context.Context, trigger *models.Trigger) error
// AfterTriggerUpdate called after trigger updated in datastore
AfterTriggerUpdate(ctx context.Context, trigger *models.Trigger) error
// BeforeTriggerDelete called before trigger deleted from the datastore
BeforeTriggerDelete(ctx context.Context, triggerId string) error
// AfterTriggerDelete called after trigger deleted from the datastore
AfterTriggerDelete(ctx context.Context, triggerId string) error
} }
// CallListener enables callbacks around Call events // CallListener enables callbacks around Call events

View File

@@ -24,3 +24,4 @@ go vet $(go list ./... | grep -v vendor)
remove_containers ${CONTEXT} remove_containers ${CONTEXT}
docker run -v `pwd`:/go/src/github.com/fnproject/fn --rm fnproject/swagger:0.0.1 /go/src/github.com/fnproject/fn/docs/swagger.yml docker run -v `pwd`:/go/src/github.com/fnproject/fn --rm fnproject/swagger:0.0.1 /go/src/github.com/fnproject/fn/docs/swagger.yml
docker run -v `pwd`:/go/src/github.com/fnproject/fn --rm fnproject/swagger:0.0.1 /go/src/github.com/fnproject/fn/docs/swagger_v2.yml

View File

@@ -331,7 +331,7 @@ func callFN(ctx context.Context, u string, content io.Reader, output io.Writer,
} }
func getAPIURL() (string, *url.URL) { func getAPIURL() (string, *url.URL) {
apiURL := getEnv("FN_API_URL", "http://localhost:8080") apiURL := getEnv("FN_API_URL", "http://localhost:8085")
u, err := url.Parse(apiURL) u, err := url.Parse(apiURL)
if err != nil { if err != nil {
log.Fatalf("Couldn't parse API URL: %s error: %s", apiURL, err) log.Fatalf("Couldn't parse API URL: %s error: %s", apiURL, err)

View File

@@ -34,10 +34,6 @@ const (
LBAddress = "http://127.0.0.1:8081" LBAddress = "http://127.0.0.1:8081"
) )
type SystemTestNodePool struct {
runners []pool.Runner
}
func LB() (string, error) { func LB() (string, error) {
u, err := url.Parse(LBAddress) u, err := url.Parse(LBAddress)
if err != nil { if err != nil {