mirror of
https://github.com/fnproject/fn.git
synced 2022-10-28 21:29:17 +03:00
* Don't try to delete an app that wasn't successfully created in the case of failure * Allow datastore implementations to inject additional annotations on objects * Allow for datastores transparently adding annotations on apps, fns and triggers. Change NameIn filter to Name for apps. * Move *List types including JSON annotations for App, Fn and Trigger into models * Change return types for GetApps, GetFns and GetTriggers on datastore to be models.*List and ove cursor generation into datastore * Trigger cursor handling fixed into db layer Also changes the name generation so that it is not in the same order as the id (well is random), this means we are now testing our name ordering. * GetFns now respects cursors * Apps now feeds cursor back * Mock fixes * Fixing up api level cursor decoding * Tidy up treatment of cursors in the db layer * Adding conditions for non nil items lists * fix mock test
538 lines
12 KiB
Go
538 lines
12 KiB
Go
package datastore
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/fnproject/fn/api/common"
|
|
"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/models"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
type mock struct {
|
|
Apps []*models.App
|
|
Routes []*models.Route
|
|
Fns []*models.Fn
|
|
Triggers []*models.Trigger
|
|
|
|
models.LogStore
|
|
}
|
|
|
|
func NewMock() models.Datastore {
|
|
return NewMockInit()
|
|
}
|
|
|
|
// args helps break tests less if we change stuff
|
|
func NewMockInit(args ...interface{}) models.Datastore {
|
|
var mocker mock
|
|
for _, a := range args {
|
|
switch x := a.(type) {
|
|
case []*models.App:
|
|
mocker.Apps = x
|
|
case []*models.Route:
|
|
mocker.Routes = x
|
|
case []*models.Fn:
|
|
mocker.Fns = x
|
|
case []*models.Trigger:
|
|
mocker.Triggers = x
|
|
|
|
default:
|
|
panic("not accounted for data type sent to mock init. add it")
|
|
}
|
|
}
|
|
mocker.LogStore = logs.NewMock()
|
|
return datastoreutil.NewValidator(&mocker)
|
|
}
|
|
|
|
func (m *mock) GetAppID(ctx context.Context, appName string) (string, error) {
|
|
for _, a := range m.Apps {
|
|
if a.Name == appName {
|
|
return a.ID, nil
|
|
}
|
|
}
|
|
|
|
return "", models.ErrAppsNotFound
|
|
}
|
|
|
|
func (m *mock) GetAppByID(ctx context.Context, appID string) (*models.App, error) {
|
|
for _, a := range m.Apps {
|
|
if a.ID == appID {
|
|
return a.Clone(), nil
|
|
}
|
|
}
|
|
|
|
return nil, models.ErrAppsNotFound
|
|
}
|
|
|
|
type sortA []*models.App
|
|
|
|
func (s sortA) Len() int { return len(s) }
|
|
func (s sortA) Less(i, j int) bool { return strings.Compare(s[i].Name, s[j].Name) < 0 }
|
|
func (s sortA) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
|
|
|
func (m *mock) GetApps(ctx context.Context, filter *models.AppFilter) (*models.AppList, error) {
|
|
// sort them all first for cursoring (this is for testing, n is small & mock is not concurrent..)
|
|
sort.Sort(sortA(m.Apps))
|
|
|
|
var cursor string
|
|
if filter.Cursor != "" {
|
|
s, err := base64.RawURLEncoding.DecodeString(filter.Cursor)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
logrus.Error(s)
|
|
cursor = string(s)
|
|
}
|
|
|
|
apps := []*models.App{}
|
|
for _, a := range m.Apps {
|
|
if len(apps) == filter.PerPage {
|
|
break
|
|
}
|
|
if strings.Compare(cursor, a.Name) < 0 {
|
|
if filter.Name != "" && filter.Name != a.Name {
|
|
continue
|
|
}
|
|
apps = append(apps, a.Clone())
|
|
}
|
|
}
|
|
|
|
var nextCursor string
|
|
if len(apps) > 0 && len(apps) == filter.PerPage {
|
|
last := []byte(apps[len(apps)-1].Name)
|
|
nextCursor = base64.RawURLEncoding.EncodeToString(last)
|
|
}
|
|
|
|
return &models.AppList{
|
|
NextCursor: nextCursor,
|
|
Items: apps,
|
|
}, nil
|
|
}
|
|
|
|
func (m *mock) InsertApp(ctx context.Context, newApp *models.App) (*models.App, error) {
|
|
for _, a := range m.Apps {
|
|
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)
|
|
return app.Clone(), nil
|
|
}
|
|
|
|
func (m *mock) UpdateApp(ctx context.Context, app *models.App) (*models.App, error) {
|
|
|
|
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 {
|
|
m.batchDeleteRoutes(ctx, appID)
|
|
|
|
for i, a := range m.Apps {
|
|
if a.ID == appID {
|
|
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 models.ErrAppsNotFound
|
|
}
|
|
|
|
func (m *mock) GetRoute(ctx context.Context, appID, routePath string) (*models.Route, error) {
|
|
for _, r := range m.Routes {
|
|
if r.AppID == appID && r.Path == routePath {
|
|
return r.Clone(), nil
|
|
}
|
|
}
|
|
return nil, models.ErrRoutesNotFound
|
|
}
|
|
|
|
type sortR []*models.Route
|
|
|
|
func (s sortR) Len() int { return len(s) }
|
|
func (s sortR) Less(i, j int) bool { return strings.Compare(s[i].Path, s[j].Path) < 0 }
|
|
func (s sortR) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
|
|
|
func (m *mock) GetRoutesByApp(ctx context.Context, appID string, routeFilter *models.RouteFilter) (routes []*models.Route, err error) {
|
|
// sort them all first for cursoring (this is for testing, n is small & mock is not concurrent..)
|
|
sort.Sort(sortR(m.Routes))
|
|
|
|
for _, r := range m.Routes {
|
|
if len(routes) == routeFilter.PerPage {
|
|
break
|
|
}
|
|
|
|
if r.AppID == appID &&
|
|
//strings.HasPrefix(r.Path, routeFilter.PathPrefix) && // TODO
|
|
(routeFilter.Image == "" || routeFilter.Image == r.Image) &&
|
|
strings.Compare(routeFilter.Cursor, r.Path) < 0 {
|
|
|
|
routes = append(routes, r.Clone())
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
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 {
|
|
return nil, err
|
|
}
|
|
|
|
if r, _ := m.GetRoute(ctx, route.AppID, route.Path); r != nil {
|
|
return nil, models.ErrRoutesAlreadyExists
|
|
}
|
|
m.Routes = append(m.Routes, c)
|
|
return c.Clone(), nil
|
|
}
|
|
|
|
func (m *mock) UpdateRoute(ctx context.Context, route *models.Route) (*models.Route, error) {
|
|
r, err := m.GetRoute(ctx, route.AppID, route.Path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
clone := r.Clone()
|
|
clone.Update(route)
|
|
err = clone.Validate()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
r.Update(route) // only if validate works (pointer)
|
|
return clone, nil
|
|
}
|
|
|
|
func (m *mock) RemoveRoute(ctx context.Context, appID, routePath string) error {
|
|
for i, r := range m.Routes {
|
|
if r.AppID == appID && r.Path == routePath {
|
|
m.Routes = append(m.Routes[:i], m.Routes[i+1:]...)
|
|
return nil
|
|
}
|
|
}
|
|
return models.ErrRoutesNotFound
|
|
}
|
|
|
|
func (m *mock) batchDeleteRoutes(ctx context.Context, appID string) error {
|
|
var newRoutes []*models.Route
|
|
for _, c := range m.Routes {
|
|
if c.AppID != appID {
|
|
newRoutes = append(newRoutes, c)
|
|
}
|
|
}
|
|
m.Routes = newRoutes
|
|
return nil
|
|
}
|
|
|
|
func (m *mock) InsertFn(ctx context.Context, fn *models.Fn) (*models.Fn, error) {
|
|
_, err := m.GetAppByID(ctx, fn.AppID)
|
|
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.FnList, 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{}
|
|
|
|
var cursor string
|
|
if filter.Cursor != "" {
|
|
s, err := base64.RawURLEncoding.DecodeString(filter.Cursor)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cursor = string(s)
|
|
}
|
|
|
|
for _, f := range m.Fns {
|
|
if filter.PerPage > 0 && len(funcs) == filter.PerPage {
|
|
break
|
|
}
|
|
|
|
if strings.Compare(cursor, f.Name) < 0 &&
|
|
(filter.AppID == "" || filter.AppID == f.AppID) &&
|
|
(filter.Name == "" || filter.Name == f.Name) {
|
|
funcs = append(funcs, f)
|
|
}
|
|
|
|
}
|
|
|
|
var nextCursor string
|
|
if len(funcs) > 0 && len(funcs) == filter.PerPage {
|
|
last := []byte(funcs[len(funcs)-1].Name)
|
|
nextCursor = base64.RawURLEncoding.EncodeToString(last)
|
|
}
|
|
|
|
return &models.FnList{
|
|
NextCursor: nextCursor,
|
|
Items: 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].Name, s[j].Name) < 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.TriggerList, error) {
|
|
sort.Sort(sortT(m.Triggers))
|
|
|
|
var cursor string
|
|
if filter.Cursor != "" {
|
|
s, err := base64.RawURLEncoding.DecodeString(filter.Cursor)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cursor = string(s)
|
|
}
|
|
|
|
res := []*models.Trigger{}
|
|
for _, t := range m.Triggers {
|
|
if filter.PerPage > 0 && len(res) == filter.PerPage {
|
|
break
|
|
}
|
|
|
|
matched := true
|
|
if filter.Cursor != "" && t.Name <= 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)
|
|
}
|
|
}
|
|
|
|
var nextCursor string
|
|
if len(res) > 0 && len(res) == filter.PerPage {
|
|
last := []byte(res[len(res)-1].Name)
|
|
nextCursor = base64.RawURLEncoding.EncodeToString(last)
|
|
}
|
|
|
|
return &models.TriggerList{
|
|
NextCursor: nextCursor,
|
|
Items: 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 {
|
|
return nil
|
|
}
|