Files
fn-serverless/api/datastore/mock.go
Tom Coupland c3537399f1 The V2 Calls api endpoints have been added beneath fns: (#1203)
/fns/{fnID}/calls
/fns/{fnID}/calls/{callID}

The S3 implementation forces our hand as we if we want to list Calls
under a Fn, we have to use the FnID as a prefix on the object names,
which mean we need it to look up any Call. It also makes sense in
terms of resource hierarchy.

These endpoints can optionally be disabled (as other endpoints), if a
service provider needs to provide this functionality via other means.

The 'calls' test has been fully migrated to fn calls. This has been
done to reduce the copy pasta a bit, and on balance is ok as the
routes calls will be removed soon.
2018-09-12 15:45:53 +01:00

555 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()
}
var _ models.Datastore = &mock{}
func (m *mock) GetTriggerBySource(ctx context.Context, appId string, triggerType, source string) (*models.Trigger, error) {
for _, t := range m.Triggers {
if t.AppID == appId && t.Type == triggerType && t.Source == source {
return t, nil
}
}
return nil, models.ErrTriggerNotFound
}
// 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
}
if t.AppID == trigger.AppID &&
t.Source == trigger.Source &&
t.Type == trigger.Type {
return nil, models.ErrTriggerSourceExists
}
}
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
}