mirror of
https://github.com/fnproject/fn.git
synced 2022-10-28 21:29:17 +03:00
Merge pull request #349 from fnproject/pagination
add pagination to all list endpoints
This commit is contained in:
@@ -4,15 +4,13 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"log"
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/fnproject/fn/api/id"
|
"github.com/fnproject/fn/api/id"
|
||||||
"github.com/fnproject/fn/api/models"
|
"github.com/fnproject/fn/api/models"
|
||||||
|
|
||||||
"net/http"
|
|
||||||
"reflect"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/go-openapi/strfmt"
|
"github.com/go-openapi/strfmt"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
@@ -28,7 +26,7 @@ func setLogBuffer() *bytes.Buffer {
|
|||||||
return &buf
|
return &buf
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test(t *testing.T, ds models.Datastore) {
|
func Test(t *testing.T, dsf func() models.Datastore) {
|
||||||
buf := setLogBuffer()
|
buf := setLogBuffer()
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
@@ -42,6 +40,7 @@ func Test(t *testing.T, ds models.Datastore) {
|
|||||||
call.Path = testRoute.Path
|
call.Path = testRoute.Path
|
||||||
|
|
||||||
t.Run("call-insert", func(t *testing.T) {
|
t.Run("call-insert", func(t *testing.T) {
|
||||||
|
ds := dsf()
|
||||||
call.ID = id.New().String()
|
call.ID = id.New().String()
|
||||||
err := ds.InsertCall(ctx, call)
|
err := ds.InsertCall(ctx, call)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -51,6 +50,7 @@ func Test(t *testing.T, ds models.Datastore) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("call-get", func(t *testing.T) {
|
t.Run("call-get", func(t *testing.T) {
|
||||||
|
ds := dsf()
|
||||||
call.ID = id.New().String()
|
call.ID = id.New().String()
|
||||||
ds.InsertCall(ctx, call)
|
ds.InsertCall(ctx, call)
|
||||||
newCall, err := ds.GetCall(ctx, call.AppName, call.ID)
|
newCall, err := ds.GetCall(ctx, call.AppName, call.ID)
|
||||||
@@ -64,20 +64,120 @@ func Test(t *testing.T, ds models.Datastore) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("calls-get", func(t *testing.T) {
|
t.Run("calls-get", func(t *testing.T) {
|
||||||
filter := &models.CallFilter{AppName: call.AppName, Path: call.Path}
|
ds := dsf()
|
||||||
|
filter := &models.CallFilter{AppName: call.AppName, Path: call.Path, PerPage: 100}
|
||||||
call.ID = id.New().String()
|
call.ID = id.New().String()
|
||||||
ds.InsertCall(ctx, call)
|
call.CreatedAt = strfmt.DateTime(time.Now())
|
||||||
|
err := ds.InsertCall(ctx, call)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
calls, err := ds.GetCalls(ctx, filter)
|
calls, err := ds.GetCalls(ctx, filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test GetCalls(ctx, filter): unexpected error `%v`", err)
|
t.Fatalf("Test GetCalls(ctx, filter): unexpected error `%v`", err)
|
||||||
}
|
}
|
||||||
if len(calls) == 0 {
|
if len(calls) != 1 {
|
||||||
t.Log(buf.String())
|
t.Log(buf.String())
|
||||||
|
t.Fatalf("Test GetCalls(ctx, filter): unexpected length `%v`", len(calls))
|
||||||
|
}
|
||||||
|
|
||||||
|
c2 := *call
|
||||||
|
c3 := *call
|
||||||
|
c2.ID = id.New().String()
|
||||||
|
c2.CreatedAt = strfmt.DateTime(time.Now().Add(100 * time.Millisecond)) // add ms cuz db uses it for sort
|
||||||
|
c3.ID = id.New().String()
|
||||||
|
c3.CreatedAt = strfmt.DateTime(time.Now().Add(200 * time.Millisecond))
|
||||||
|
|
||||||
|
err = ds.InsertCall(ctx, &c2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
err = ds.InsertCall(ctx, &c3)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// test that no filter works too
|
||||||
|
calls, err = ds.GetCalls(ctx, &models.CallFilter{PerPage: 100})
|
||||||
|
if err != nil {
|
||||||
t.Fatalf("Test GetCalls(ctx, filter): unexpected error `%v`", err)
|
t.Fatalf("Test GetCalls(ctx, filter): unexpected error `%v`", err)
|
||||||
}
|
}
|
||||||
|
if len(calls) != 3 {
|
||||||
|
t.Log(buf.String())
|
||||||
|
t.Fatalf("Test GetCalls(ctx, filter): unexpected length `%v`", len(calls))
|
||||||
|
}
|
||||||
|
|
||||||
|
// test that pagination stuff works. id, descending
|
||||||
|
filter.PerPage = 1
|
||||||
|
calls, err = ds.GetCalls(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Test GetCalls(ctx, filter): unexpected error `%v`", err)
|
||||||
|
}
|
||||||
|
if len(calls) != 1 {
|
||||||
|
t.Log(buf.String())
|
||||||
|
t.Fatalf("Test GetCalls(ctx, filter): unexpected length `%v`", len(calls))
|
||||||
|
} else if calls[0].ID != c3.ID {
|
||||||
|
t.Log(buf.String())
|
||||||
|
t.Fatalf("Test GetCalls: call ids not in expected order: %v %v", calls[0].ID, c3.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
filter.PerPage = 100
|
||||||
|
filter.Cursor = calls[0].ID
|
||||||
|
calls, err = ds.GetCalls(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Test GetCalls(ctx, filter): unexpected error `%v`", err)
|
||||||
|
}
|
||||||
|
if len(calls) != 2 {
|
||||||
|
t.Log(buf.String())
|
||||||
|
t.Fatalf("Test GetCalls(ctx, filter): unexpected length `%v`", len(calls))
|
||||||
|
} else if calls[0].ID != c2.ID {
|
||||||
|
t.Log(buf.String())
|
||||||
|
t.Fatalf("Test GetCalls: call ids not in expected order: %v %v", calls[0].ID, c2.ID)
|
||||||
|
} else if calls[1].ID != call.ID {
|
||||||
|
t.Log(buf.String())
|
||||||
|
t.Fatalf("Test GetCalls: call ids not in expected order: %v %v", calls[1].ID, call.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// test that filters actually applied
|
||||||
|
calls, err = ds.GetCalls(ctx, &models.CallFilter{AppName: "wrongappname", PerPage: 100})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Test GetCalls(ctx, filter): unexpected error `%v`", err)
|
||||||
|
}
|
||||||
|
if len(calls) != 0 {
|
||||||
|
t.Log(buf.String())
|
||||||
|
t.Fatalf("Test GetCalls(ctx, filter): unexpected length `%v`", len(calls))
|
||||||
|
}
|
||||||
|
|
||||||
|
calls, err = ds.GetCalls(ctx, &models.CallFilter{Path: "wrongpath", PerPage: 100})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Test GetCalls(ctx, filter): unexpected error `%v`", err)
|
||||||
|
}
|
||||||
|
if len(calls) != 0 {
|
||||||
|
t.Log(buf.String())
|
||||||
|
t.Fatalf("Test GetCalls(ctx, filter): unexpected length `%v`", len(calls))
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure from_time and to_time work
|
||||||
|
filter = &models.CallFilter{
|
||||||
|
PerPage: 100,
|
||||||
|
FromTime: call.CreatedAt,
|
||||||
|
ToTime: c3.CreatedAt,
|
||||||
|
}
|
||||||
|
calls, err = ds.GetCalls(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Test GetCalls(ctx, filter): unexpected error `%v`", err)
|
||||||
|
}
|
||||||
|
if len(calls) != 1 {
|
||||||
|
t.Log(buf.String())
|
||||||
|
t.Fatalf("Test GetCalls(ctx, filter): unexpected length `%v`", len(calls))
|
||||||
|
} else if calls[0].ID != c2.ID {
|
||||||
|
t.Log(buf.String())
|
||||||
|
t.Fatalf("Test GetCalls: call id not expected", calls[0].ID, c2.ID)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("apps", func(t *testing.T) {
|
t.Run("apps", func(t *testing.T) {
|
||||||
|
ds := dsf()
|
||||||
// Testing insert app
|
// Testing insert app
|
||||||
_, err := ds.InsertApp(ctx, nil)
|
_, err := ds.InsertApp(ctx, nil)
|
||||||
if err != models.ErrDatastoreEmptyApp {
|
if err != models.ErrDatastoreEmptyApp {
|
||||||
@@ -166,7 +266,7 @@ func Test(t *testing.T, ds models.Datastore) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Testing list apps
|
// Testing list apps
|
||||||
apps, err := ds.GetApps(ctx, &models.AppFilter{})
|
apps, err := ds.GetApps(ctx, &models.AppFilter{PerPage: 100})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Log(buf.String())
|
t.Log(buf.String())
|
||||||
t.Fatalf("Test GetApps: unexpected error %v", err)
|
t.Fatalf("Test GetApps: unexpected error %v", err)
|
||||||
@@ -179,15 +279,74 @@ func Test(t *testing.T, ds models.Datastore) {
|
|||||||
t.Fatalf("Test GetApps: expected `app.Name` to be `%s` but it was `%s`", app.Name, testApp.Name)
|
t.Fatalf("Test GetApps: expected `app.Name` to be `%s` but it was `%s`", app.Name, testApp.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
apps, err = ds.GetApps(ctx, &models.AppFilter{Name: "Tes%"})
|
// test pagination stuff (ordering / limits / cursoring)
|
||||||
|
a2 := *testApp
|
||||||
|
a3 := *testApp
|
||||||
|
a2.Name = "Testa"
|
||||||
|
a3.Name = "Testb"
|
||||||
|
if _, err = ds.InsertApp(ctx, &a2); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err = ds.InsertApp(ctx, &a3); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
apps, err = ds.GetApps(ctx, &models.AppFilter{PerPage: 1})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Log(buf.String())
|
t.Log(buf.String())
|
||||||
t.Fatalf("Test GetApps(filter): unexpected error %v", err)
|
t.Fatalf("Test GetApps: error: %s", err)
|
||||||
}
|
}
|
||||||
if len(apps) == 0 {
|
if len(apps) != 1 {
|
||||||
t.Fatal("Test GetApps(filter): expected result count to be greater than 0")
|
t.Fatalf("Test GetApps: expected result count to be 1 but got %d", len(apps))
|
||||||
|
} else if apps[0].Name != testApp.Name {
|
||||||
|
t.Log(buf.String())
|
||||||
|
t.Fatalf("Test GetApps: expected `app.Name` to be `%s` but it was `%s`", testApp.Name, apps[0].Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
apps, err = ds.GetApps(ctx, &models.AppFilter{PerPage: 100, Cursor: apps[0].Name})
|
||||||
|
if err != nil {
|
||||||
|
t.Log(buf.String())
|
||||||
|
t.Fatalf("Test GetApps: error: %s", err)
|
||||||
|
}
|
||||||
|
if len(apps) != 2 {
|
||||||
|
t.Fatalf("Test GetApps: expected result count to be 2 but got %d", len(apps))
|
||||||
|
} else if apps[0].Name != a2.Name {
|
||||||
|
t.Log(buf.String())
|
||||||
|
t.Fatalf("Test GetApps: expected `app.Name` to be `%s` but it was `%s`", a2.Name, apps[0].Name)
|
||||||
|
} else if apps[1].Name != a3.Name {
|
||||||
|
t.Log(buf.String())
|
||||||
|
t.Fatalf("Test GetApps: expected `app.Name` to be `%s` but it was `%s`", a3.Name, apps[1].Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
a4 := *testApp
|
||||||
|
a4.Name = "Abcdefg" // < /test lexicographically, but not in length
|
||||||
|
|
||||||
|
if _, err = ds.InsertApp(ctx, &a4); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
apps, err = ds.GetApps(ctx, &models.AppFilter{PerPage: 100})
|
||||||
|
if err != nil {
|
||||||
|
t.Log(buf.String())
|
||||||
|
t.Fatalf("Test GetApps: error: %s", err)
|
||||||
|
}
|
||||||
|
if len(apps) != 4 {
|
||||||
|
t.Fatalf("Test GetApps: expected result count to be 4 but got %d", len(apps))
|
||||||
|
} else if apps[0].Name != a4.Name {
|
||||||
|
t.Log(buf.String())
|
||||||
|
t.Fatalf("Test GetApps: expected `app.Name` to be `%s` but it was `%s`", a4.Name, apps[0].Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO fix up prefix stuff
|
||||||
|
//apps, err = ds.GetApps(ctx, &models.AppFilter{Name: "Tes"})
|
||||||
|
//if err != nil {
|
||||||
|
//t.Log(buf.String())
|
||||||
|
//t.Fatalf("Test GetApps(filter): unexpected error %v", err)
|
||||||
|
//}
|
||||||
|
//if len(apps) != 3 {
|
||||||
|
//t.Fatal("Test GetApps(filter): expected result count to be 3, got", len(apps))
|
||||||
|
//}
|
||||||
|
|
||||||
// Testing app delete
|
// Testing app delete
|
||||||
err = ds.RemoveApp(ctx, "")
|
err = ds.RemoveApp(ctx, "")
|
||||||
if err != models.ErrDatastoreEmptyAppName {
|
if err != models.ErrDatastoreEmptyAppName {
|
||||||
@@ -224,6 +383,7 @@ func Test(t *testing.T, ds models.Datastore) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("routes", func(t *testing.T) {
|
t.Run("routes", func(t *testing.T) {
|
||||||
|
ds := dsf()
|
||||||
// Insert app again to test routes
|
// Insert app again to test routes
|
||||||
_, err := ds.InsertApp(ctx, testApp)
|
_, err := ds.InsertApp(ctx, testApp)
|
||||||
if err != nil && err != models.ErrAppsAlreadyExists {
|
if err != nil && err != models.ErrAppsAlreadyExists {
|
||||||
@@ -374,7 +534,7 @@ func Test(t *testing.T, ds models.Datastore) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Testing list routes
|
// Testing list routes
|
||||||
routes, err := ds.GetRoutesByApp(ctx, testApp.Name, &models.RouteFilter{})
|
routes, err := ds.GetRoutesByApp(ctx, testApp.Name, &models.RouteFilter{PerPage: 1})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Log(buf.String())
|
t.Log(buf.String())
|
||||||
t.Fatalf("Test GetRoutesByApp: unexpected error %v", err)
|
t.Fatalf("Test GetRoutesByApp: unexpected error %v", err)
|
||||||
@@ -390,7 +550,7 @@ func Test(t *testing.T, ds models.Datastore) {
|
|||||||
t.Fatalf("Test GetRoutes: expected `app.Name` to be `%s` but it was `%s`", testRoute.Path, routes[0].Path)
|
t.Fatalf("Test GetRoutes: expected `app.Name` to be `%s` but it was `%s`", testRoute.Path, routes[0].Path)
|
||||||
}
|
}
|
||||||
|
|
||||||
routes, err = ds.GetRoutesByApp(ctx, testApp.Name, &models.RouteFilter{Image: testRoute.Image})
|
routes, err = ds.GetRoutesByApp(ctx, testApp.Name, &models.RouteFilter{Image: testRoute.Image, PerPage: 1})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Log(buf.String())
|
t.Log(buf.String())
|
||||||
t.Fatalf("Test GetRoutesByApp: unexpected error %v", err)
|
t.Fatalf("Test GetRoutesByApp: unexpected error %v", err)
|
||||||
@@ -400,13 +560,13 @@ func Test(t *testing.T, ds models.Datastore) {
|
|||||||
}
|
}
|
||||||
if routes[0] == nil {
|
if routes[0] == nil {
|
||||||
t.Log(buf.String())
|
t.Log(buf.String())
|
||||||
t.Fatalf("Test GetRoutes: expected non-nil route")
|
t.Fatalf("Test GetRoutesByApp: expected non-nil route")
|
||||||
} else if routes[0].Path != testRoute.Path {
|
} else if routes[0].Path != testRoute.Path {
|
||||||
t.Log(buf.String())
|
t.Log(buf.String())
|
||||||
t.Fatalf("Test GetRoutes: expected `app.Name` to be `%s` but it was `%s`", testRoute.Path, routes[0].Path)
|
t.Fatalf("Test GetRoutesByApp: expected `route.Path` to be `%s` but it was `%s`", testRoute.Path, routes[0].Path)
|
||||||
}
|
}
|
||||||
|
|
||||||
routes, err = ds.GetRoutesByApp(ctx, "notreal", nil)
|
routes, err = ds.GetRoutesByApp(ctx, "notreal", &models.RouteFilter{PerPage: 1})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Log(buf.String())
|
t.Log(buf.String())
|
||||||
t.Fatalf("Test GetRoutesByApp: error: %s", err)
|
t.Fatalf("Test GetRoutesByApp: error: %s", err)
|
||||||
@@ -415,20 +575,68 @@ func Test(t *testing.T, ds models.Datastore) {
|
|||||||
t.Fatalf("Test GetRoutesByApp: expected result count to be 0 but got %d", len(routes))
|
t.Fatalf("Test GetRoutesByApp: expected result count to be 0 but got %d", len(routes))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Testing list routes
|
// test pagination stuff
|
||||||
routes, err = ds.GetRoutes(ctx, &models.RouteFilter{Image: testRoute.Image})
|
r2 := *testRoute
|
||||||
|
r3 := *testRoute
|
||||||
|
r2.Path = "/testa"
|
||||||
|
r3.Path = "/testb"
|
||||||
|
|
||||||
|
if _, err = ds.InsertRoute(ctx, &r2); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err = ds.InsertRoute(ctx, &r3); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
routes, err = ds.GetRoutesByApp(ctx, testApp.Name, &models.RouteFilter{PerPage: 1})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Log(buf.String())
|
t.Log(buf.String())
|
||||||
t.Fatalf("Test GetRoutes: error: %s", err)
|
t.Fatalf("Test GetRoutesByApp: error: %s", err)
|
||||||
}
|
}
|
||||||
if len(routes) == 0 {
|
if len(routes) != 1 {
|
||||||
t.Fatal("Test GetRoutes: expected result count to be greater than 0")
|
t.Fatalf("Test GetRoutesByApp: expected result count to be 1 but got %d", len(routes))
|
||||||
}
|
} else if routes[0].Path != testRoute.Path {
|
||||||
if routes[0].Path != testRoute.Path {
|
|
||||||
t.Log(buf.String())
|
t.Log(buf.String())
|
||||||
t.Fatalf("Test GetRoutes: expected `app.Name` to be `%s` but it was `%s`", testRoute.Path, routes[0].Path)
|
t.Fatalf("Test GetRoutesByApp: expected `route.Path` to be `%s` but it was `%s`", testRoute.Path, routes[0].Path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
routes, err = ds.GetRoutesByApp(ctx, testApp.Name, &models.RouteFilter{PerPage: 2, Cursor: routes[0].Path})
|
||||||
|
if err != nil {
|
||||||
|
t.Log(buf.String())
|
||||||
|
t.Fatalf("Test GetRoutesByApp: error: %s", err)
|
||||||
|
}
|
||||||
|
if len(routes) != 2 {
|
||||||
|
t.Fatalf("Test GetRoutesByApp: expected result count to be 2 but got %d", len(routes))
|
||||||
|
} else if routes[0].Path != r2.Path {
|
||||||
|
t.Log(buf.String())
|
||||||
|
t.Fatalf("Test GetRoutesByApp: expected `route.Path` to be `%s` but it was `%s`", r2.Path, routes[0].Path)
|
||||||
|
} else if routes[1].Path != r3.Path {
|
||||||
|
t.Log(buf.String())
|
||||||
|
t.Fatalf("Test GetRoutesByApp: expected `route.Path` to be `%s` but it was `%s`", r3.Path, routes[1].Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
r4 := *testRoute
|
||||||
|
r4.Path = "/abcdefg" // < /test lexicographically, but not in length
|
||||||
|
|
||||||
|
if _, err = ds.InsertRoute(ctx, &r4); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
routes, err = ds.GetRoutesByApp(ctx, testApp.Name, &models.RouteFilter{PerPage: 100})
|
||||||
|
if err != nil {
|
||||||
|
t.Log(buf.String())
|
||||||
|
t.Fatalf("Test GetRoutesByApp: error: %s", err)
|
||||||
|
}
|
||||||
|
if len(routes) != 4 {
|
||||||
|
t.Fatalf("Test GetRoutesByApp: expected result count to be 4 but got %d", len(routes))
|
||||||
|
} else if routes[0].Path != r4.Path {
|
||||||
|
t.Log(buf.String())
|
||||||
|
t.Fatalf("Test GetRoutesByApp: expected `route.Path` to be `%s` but it was `%s`", r4.Path, routes[0].Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO test weird ordering possibilities ?
|
||||||
|
// TODO test prefix filtering
|
||||||
|
|
||||||
// Testing route delete
|
// Testing route delete
|
||||||
err = ds.RemoveRoute(ctx, "", "")
|
err = ds.RemoveRoute(ctx, "", "")
|
||||||
if err != models.ErrDatastoreEmptyAppName {
|
if err != models.ErrDatastoreEmptyAppName {
|
||||||
|
|||||||
@@ -53,12 +53,6 @@ func (m *metricds) GetRoute(ctx context.Context, appName, routePath string) (*mo
|
|||||||
return m.ds.GetRoute(ctx, appName, routePath)
|
return m.ds.GetRoute(ctx, appName, routePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *metricds) GetRoutes(ctx context.Context, filter *models.RouteFilter) (routes []*models.Route, err error) {
|
|
||||||
span, ctx := opentracing.StartSpanFromContext(ctx, "ds_get_routes")
|
|
||||||
defer span.Finish()
|
|
||||||
return m.ds.GetRoutes(ctx, filter)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *metricds) GetRoutesByApp(ctx context.Context, appName string, filter *models.RouteFilter) (routes []*models.Route, err error) {
|
func (m *metricds) GetRoutesByApp(ctx context.Context, appName string, filter *models.RouteFilter) (routes []*models.Route, err error) {
|
||||||
span, ctx := opentracing.StartSpanFromContext(ctx, "ds_get_routes_by_app")
|
span, ctx := opentracing.StartSpanFromContext(ctx, "ds_get_routes_by_app")
|
||||||
defer span.Finish()
|
defer span.Finish()
|
||||||
|
|||||||
@@ -73,14 +73,6 @@ func (v *validator) GetRoute(ctx context.Context, appName, routePath string) (*m
|
|||||||
return v.Datastore.GetRoute(ctx, appName, routePath)
|
return v.Datastore.GetRoute(ctx, appName, routePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *validator) GetRoutes(ctx context.Context, routeFilter *models.RouteFilter) (routes []*models.Route, err error) {
|
|
||||||
if routeFilter != nil && routeFilter.AppName != "" {
|
|
||||||
return v.Datastore.GetRoutesByApp(ctx, routeFilter.AppName, routeFilter)
|
|
||||||
}
|
|
||||||
|
|
||||||
return v.Datastore.GetRoutes(ctx, routeFilter)
|
|
||||||
}
|
|
||||||
|
|
||||||
// appName will never be empty
|
// appName will never be empty
|
||||||
func (v *validator) GetRoutesByApp(ctx context.Context, appName string, routeFilter *models.RouteFilter) (routes []*models.Route, err error) {
|
func (v *validator) GetRoutesByApp(ctx context.Context, appName string, routeFilter *models.RouteFilter) (routes []*models.Route, err error) {
|
||||||
if appName == "" {
|
if appName == "" {
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ package datastore
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"sort"
|
||||||
"github.com/jmoiron/sqlx"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/fnproject/fn/api/datastore/internal/datastoreutil"
|
"github.com/fnproject/fn/api/datastore/internal/datastoreutil"
|
||||||
"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 {
|
||||||
@@ -37,8 +39,27 @@ func (m *mock) GetApp(ctx context.Context, appName string) (app *models.App, err
|
|||||||
return nil, models.ErrAppsNotFound
|
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, appFilter *models.AppFilter) ([]*models.App, error) {
|
func (m *mock) GetApps(ctx context.Context, appFilter *models.AppFilter) ([]*models.App, error) {
|
||||||
return m.Apps, nil
|
// sort them all first for cursoring (this is for testing, n is small & mock is not concurrent..)
|
||||||
|
sort.Sort(sortA(m.Apps))
|
||||||
|
|
||||||
|
var apps []*models.App
|
||||||
|
for _, a := range m.Apps {
|
||||||
|
if len(apps) == appFilter.PerPage {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if strings.Compare(appFilter.Cursor, a.Name) < 0 {
|
||||||
|
apps = append(apps, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return apps, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mock) InsertApp(ctx context.Context, app *models.App) (*models.App, error) {
|
func (m *mock) InsertApp(ctx context.Context, app *models.App) (*models.App, error) {
|
||||||
@@ -80,16 +101,26 @@ func (m *mock) GetRoute(ctx context.Context, appName, routePath string) (*models
|
|||||||
return nil, models.ErrRoutesNotFound
|
return nil, models.ErrRoutesNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mock) GetRoutes(ctx context.Context, routeFilter *models.RouteFilter) (routes []*models.Route, err error) {
|
type sortR []*models.Route
|
||||||
for _, r := range m.Routes {
|
|
||||||
routes = append(routes, r)
|
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 }
|
||||||
return
|
func (s sortR) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mock) GetRoutesByApp(ctx context.Context, appName string, routeFilter *models.RouteFilter) (routes []*models.Route, err error) {
|
func (m *mock) GetRoutesByApp(ctx context.Context, appName 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 {
|
for _, r := range m.Routes {
|
||||||
if r.AppName == appName && (routeFilter.Path == "" || r.Path == routeFilter.Path) && (routeFilter.AppName == "" || r.AppName == routeFilter.AppName) {
|
if len(routes) == routeFilter.PerPage {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.AppName == appName &&
|
||||||
|
//strings.HasPrefix(r.Path, routeFilter.PathPrefix) && // TODO
|
||||||
|
(routeFilter.Image == "" || routeFilter.Image == r.Image) &&
|
||||||
|
strings.Compare(routeFilter.Cursor, r.Path) < 0 {
|
||||||
|
|
||||||
routes = append(routes, r)
|
routes = append(routes, r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -147,7 +178,7 @@ func (m *mock) InsertCall(ctx context.Context, call *models.Call) error {
|
|||||||
|
|
||||||
func (m *mock) GetCall(ctx context.Context, appName, callID string) (*models.Call, error) {
|
func (m *mock) GetCall(ctx context.Context, appName, callID string) (*models.Call, error) {
|
||||||
for _, t := range m.Calls {
|
for _, t := range m.Calls {
|
||||||
if t.ID == callID {
|
if t.ID == callID && t.AppName == appName {
|
||||||
return t, nil
|
return t, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -155,8 +186,34 @@ func (m *mock) GetCall(ctx context.Context, appName, callID string) (*models.Cal
|
|||||||
return nil, models.ErrCallNotFound
|
return nil, models.ErrCallNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type sortC []*models.Call
|
||||||
|
|
||||||
|
func (s sortC) Len() int { return len(s) }
|
||||||
|
func (s sortC) Less(i, j int) bool { return strings.Compare(s[i].ID, s[j].ID) < 0 }
|
||||||
|
func (s sortC) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||||
|
|
||||||
func (m *mock) GetCalls(ctx context.Context, filter *models.CallFilter) ([]*models.Call, error) {
|
func (m *mock) GetCalls(ctx context.Context, filter *models.CallFilter) ([]*models.Call, error) {
|
||||||
return m.Calls, nil
|
// sort them all first for cursoring (this is for testing, n is small & mock is not concurrent..)
|
||||||
|
// calls are in DESC order so use sort.Reverse
|
||||||
|
sort.Sort(sort.Reverse(sortC(m.Calls)))
|
||||||
|
|
||||||
|
var calls []*models.Call
|
||||||
|
for _, c := range m.Calls {
|
||||||
|
if len(calls) == filter.PerPage {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.AppName == "" || c.AppName == filter.AppName) &&
|
||||||
|
(filter.Path == "" || filter.Path == c.Path) &&
|
||||||
|
(time.Time(filter.FromTime).IsZero() || time.Time(filter.FromTime).Before(time.Time(c.CreatedAt))) &&
|
||||||
|
(time.Time(filter.ToTime).IsZero() || time.Time(c.CreatedAt).Before(time.Time(filter.ToTime))) &&
|
||||||
|
(filter.Cursor == "" || strings.Compare(filter.Cursor, c.ID) > 0) {
|
||||||
|
|
||||||
|
calls = append(calls, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return calls, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mock) batchDeleteCalls(ctx context.Context, appName string) error {
|
func (m *mock) batchDeleteCalls(ctx context.Context, appName string) error {
|
||||||
|
|||||||
@@ -7,5 +7,5 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestDatastore(t *testing.T) {
|
func TestDatastore(t *testing.T) {
|
||||||
datastoretest.Test(t, NewMock())
|
datastoretest.Test(t, NewMock)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/fnproject/fn/api/models"
|
"github.com/fnproject/fn/api/models"
|
||||||
"github.com/go-sql-driver/mysql"
|
"github.com/go-sql-driver/mysql"
|
||||||
@@ -471,54 +472,23 @@ func (ds *sqlStore) GetRoute(ctx context.Context, appName, routePath string) (*m
|
|||||||
return &route, nil
|
return &route, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRoutes retrieves an array of routes according to a specific filter.
|
// GetRoutesByApp retrieves a route with a specific app name.
|
||||||
func (ds *sqlStore) GetRoutes(ctx context.Context, filter *models.RouteFilter) ([]*models.Route, error) {
|
|
||||||
res := []*models.Route{}
|
|
||||||
query, args := buildFilterRouteQuery(filter)
|
|
||||||
query = fmt.Sprintf("%s %s", routeSelector, query)
|
|
||||||
query = ds.db.Rebind(query)
|
|
||||||
rows, err := ds.db.QueryContext(ctx, query, args...)
|
|
||||||
// todo: check for no rows so we don't respond with a sql 500 err
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
for rows.Next() {
|
|
||||||
var route models.Route
|
|
||||||
err := scanRoute(rows, &route)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
res = append(res, &route)
|
|
||||||
|
|
||||||
}
|
|
||||||
if err := rows.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
GetRoutesByApp retrieves a route with a specific app name.
|
|
||||||
*/
|
|
||||||
func (ds *sqlStore) GetRoutesByApp(ctx context.Context, appName string, filter *models.RouteFilter) ([]*models.Route, error) {
|
func (ds *sqlStore) GetRoutesByApp(ctx context.Context, appName string, filter *models.RouteFilter) ([]*models.Route, error) {
|
||||||
res := []*models.Route{}
|
res := []*models.Route{}
|
||||||
var filterQuery string
|
|
||||||
var args []interface{}
|
|
||||||
if filter == nil {
|
if filter == nil {
|
||||||
filterQuery = "WHERE app_name = ?"
|
filter = new(models.RouteFilter)
|
||||||
args = []interface{}{appName}
|
|
||||||
} else {
|
|
||||||
filter.AppName = appName
|
|
||||||
filterQuery, args = buildFilterRouteQuery(filter)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
filter.AppName = appName
|
||||||
|
filterQuery, args := buildFilterRouteQuery(filter)
|
||||||
|
|
||||||
query := fmt.Sprintf("%s %s", routeSelector, filterQuery)
|
query := fmt.Sprintf("%s %s", routeSelector, filterQuery)
|
||||||
query = ds.db.Rebind(query)
|
query = ds.db.Rebind(query)
|
||||||
rows, err := ds.db.QueryContext(ctx, query, args...)
|
rows, err := ds.db.QueryContext(ctx, query, args...)
|
||||||
// todo: check for no rows so we don't respond with a sql 500 err
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return res, nil // no error for empty list
|
||||||
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
@@ -533,7 +503,9 @@ func (ds *sqlStore) GetRoutesByApp(ctx context.Context, appName string, filter *
|
|||||||
|
|
||||||
}
|
}
|
||||||
if err := rows.Err(); err != nil {
|
if err := rows.Err(); err != nil {
|
||||||
return nil, err
|
if err == sql.ErrNoRows {
|
||||||
|
return res, nil // no error for empty list
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return res, nil
|
return res, nil
|
||||||
@@ -739,26 +711,52 @@ func buildFilterRouteQuery(filter *models.RouteFilter) (string, []interface{}) {
|
|||||||
if val != "" {
|
if val != "" {
|
||||||
args = append(args, val)
|
args = append(args, val)
|
||||||
if len(args) == 1 {
|
if len(args) == 1 {
|
||||||
fmt.Fprintf(&b, `WHERE %s?`, colOp)
|
fmt.Fprintf(&b, `WHERE %s`, colOp)
|
||||||
} else {
|
} else {
|
||||||
fmt.Fprintf(&b, ` AND %s?`, colOp)
|
fmt.Fprintf(&b, ` AND %s`, colOp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
where("path=", filter.Path)
|
where("app_name=? ", filter.AppName)
|
||||||
where("app_name=", filter.AppName)
|
where("image=?", filter.Image)
|
||||||
where("image=", filter.Image)
|
where("path>?", filter.Cursor)
|
||||||
|
// where("path LIKE ?%", filter.PathPrefix) TODO needs escaping
|
||||||
|
|
||||||
|
fmt.Fprintf(&b, ` ORDER BY path ASC`) // TODO assert this is indexed
|
||||||
|
fmt.Fprintf(&b, ` LIMIT ?`)
|
||||||
|
args = append(args, filter.PerPage)
|
||||||
|
|
||||||
return b.String(), args
|
return b.String(), args
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildFilterAppQuery(filter *models.AppFilter) (string, []interface{}) {
|
func buildFilterAppQuery(filter *models.AppFilter) (string, []interface{}) {
|
||||||
if filter == nil || filter.Name == "" {
|
if filter == nil {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return "WHERE name LIKE ?", []interface{}{filter.Name}
|
var b bytes.Buffer
|
||||||
|
var args []interface{}
|
||||||
|
|
||||||
|
where := func(colOp, val string) {
|
||||||
|
if val != "" {
|
||||||
|
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>?", filter.Cursor)
|
||||||
|
|
||||||
|
fmt.Fprintf(&b, ` ORDER BY name ASC`) // TODO assert this is indexed
|
||||||
|
fmt.Fprintf(&b, ` LIMIT ?`)
|
||||||
|
args = append(args, filter.PerPage)
|
||||||
|
|
||||||
|
return b.String(), args
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildFilterCallQuery(filter *models.CallFilter) (string, []interface{}) {
|
func buildFilterCallQuery(filter *models.CallFilter) (string, []interface{}) {
|
||||||
@@ -779,11 +777,19 @@ func buildFilterCallQuery(filter *models.CallFilter) (string, []interface{}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
where("app_name=", filter.AppName)
|
where("id<", filter.Cursor)
|
||||||
|
if !time.Time(filter.ToTime).IsZero() {
|
||||||
if filter.Path != "" {
|
where("created_at<", filter.ToTime.String())
|
||||||
where("path=", filter.Path)
|
|
||||||
}
|
}
|
||||||
|
if !time.Time(filter.FromTime).IsZero() {
|
||||||
|
where("created_at>", filter.FromTime.String())
|
||||||
|
}
|
||||||
|
where("app_name=", filter.AppName)
|
||||||
|
where("path=", filter.Path)
|
||||||
|
|
||||||
|
fmt.Fprintf(&b, ` ORDER BY id DESC`) // TODO assert this is indexed
|
||||||
|
fmt.Fprintf(&b, ` LIMIT ?`)
|
||||||
|
args = append(args, filter.PerPage)
|
||||||
|
|
||||||
return b.String(), args
|
return b.String(), args
|
||||||
}
|
}
|
||||||
|
|||||||
29
api/datastore/sql/sql_test.go
Normal file
29
api/datastore/sql/sql_test.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package sql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/fnproject/fn/api/datastore/internal/datastoretest"
|
||||||
|
"github.com/fnproject/fn/api/datastore/internal/datastoreutil"
|
||||||
|
"github.com/fnproject/fn/api/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDatastore(t *testing.T) {
|
||||||
|
defer os.RemoveAll("sqlite_test_dir")
|
||||||
|
u, err := url.Parse("sqlite3://sqlite_test_dir")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
f := func() models.Datastore {
|
||||||
|
os.RemoveAll("sqlite_test_dir")
|
||||||
|
ds, err := New(u)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// we don't want to test the validator, really
|
||||||
|
return datastoreutil.NewValidator(ds)
|
||||||
|
}
|
||||||
|
datastoretest.Test(t, f)
|
||||||
|
}
|
||||||
@@ -55,6 +55,7 @@ func (a *App) UpdateConfig(patch Config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AppFilter struct {
|
type AppFilter struct {
|
||||||
// An SQL LIKE query. Empty does not filter.
|
Name string // prefix query TODO implemented
|
||||||
Name string
|
PerPage int
|
||||||
|
Cursor string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
strfmt "github.com/go-openapi/strfmt"
|
"github.com/go-openapi/strfmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -130,6 +130,10 @@ type Call struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CallFilter struct {
|
type CallFilter struct {
|
||||||
Path string
|
Path string // match
|
||||||
AppName string
|
AppName string // match
|
||||||
|
FromTime strfmt.DateTime
|
||||||
|
ToTime strfmt.DateTime
|
||||||
|
Cursor string
|
||||||
|
PerPage int
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,9 +36,6 @@ type Datastore interface {
|
|||||||
// Returns ErrRoutesNotFound when no matching route is found.
|
// Returns ErrRoutesNotFound when no matching route is found.
|
||||||
GetRoute(ctx context.Context, appName, routePath string) (*Route, error)
|
GetRoute(ctx context.Context, appName, routePath string) (*Route, error)
|
||||||
|
|
||||||
// GetRoutes gets a slice of Routes, optionally filtered by filter.
|
|
||||||
GetRoutes(ctx context.Context, filter *RouteFilter) ([]*Route, error)
|
|
||||||
|
|
||||||
// GetRoutesByApp gets a slice of routes for a appName, optionally filtering on filter (filter.AppName is ignored).
|
// GetRoutesByApp gets a slice of routes for a appName, optionally filtering on filter (filter.AppName is ignored).
|
||||||
// Returns ErrDatastoreEmptyAppName if appName is empty.
|
// Returns ErrDatastoreEmptyAppName if appName is empty.
|
||||||
GetRoutesByApp(ctx context.Context, appName string, filter *RouteFilter) ([]*Route, error)
|
GetRoutesByApp(ctx context.Context, appName string, filter *RouteFilter) ([]*Route, error)
|
||||||
|
|||||||
@@ -96,51 +96,59 @@ var (
|
|||||||
code: http.StatusConflict,
|
code: http.StatusConflict,
|
||||||
error: errors.New("Could not update route - path is immutable"),
|
error: errors.New("Could not update route - path is immutable"),
|
||||||
}
|
}
|
||||||
ErrRoutesValidationFoundDynamicURL = err{
|
ErrFoundDynamicURL = err{
|
||||||
code: http.StatusBadRequest,
|
code: http.StatusBadRequest,
|
||||||
error: errors.New("Dynamic URL is not allowed"),
|
error: errors.New("Dynamic URL is not allowed"),
|
||||||
}
|
}
|
||||||
ErrRoutesValidationInvalidPath = err{
|
ErrInvalidPath = err{
|
||||||
code: http.StatusBadRequest,
|
code: http.StatusBadRequest,
|
||||||
error: errors.New("Invalid Path format"),
|
error: errors.New("Invalid Path format"),
|
||||||
}
|
}
|
||||||
ErrRoutesValidationInvalidType = err{
|
ErrInvalidType = err{
|
||||||
code: http.StatusBadRequest,
|
code: http.StatusBadRequest,
|
||||||
error: errors.New("Invalid route Type"),
|
error: errors.New("Invalid route Type"),
|
||||||
}
|
}
|
||||||
ErrRoutesValidationInvalidFormat = err{
|
ErrInvalidFormat = err{
|
||||||
code: http.StatusBadRequest,
|
code: http.StatusBadRequest,
|
||||||
error: errors.New("Invalid route Format"),
|
error: errors.New("Invalid route Format"),
|
||||||
}
|
}
|
||||||
ErrRoutesValidationMissingAppName = err{
|
ErrMissingAppName = err{
|
||||||
code: http.StatusBadRequest,
|
code: http.StatusBadRequest,
|
||||||
error: errors.New("Missing route AppName"),
|
error: errors.New("Missing route AppName"),
|
||||||
}
|
}
|
||||||
ErrRoutesValidationMissingImage = err{
|
ErrMissingImage = err{
|
||||||
code: http.StatusBadRequest,
|
code: http.StatusBadRequest,
|
||||||
error: errors.New("Missing route Image"),
|
error: errors.New("Missing route Image"),
|
||||||
}
|
}
|
||||||
ErrRoutesValidationMissingName = err{
|
ErrMissingName = err{
|
||||||
code: http.StatusBadRequest,
|
code: http.StatusBadRequest,
|
||||||
error: errors.New("Missing route Name"),
|
error: errors.New("Missing route Name"),
|
||||||
}
|
}
|
||||||
ErrRoutesValidationMissingPath = err{
|
ErrMissingPath = err{
|
||||||
code: http.StatusBadRequest,
|
code: http.StatusBadRequest,
|
||||||
error: errors.New("Missing route Path"),
|
error: errors.New("Missing route Path"),
|
||||||
}
|
}
|
||||||
ErrRoutesValidationMissingType = err{
|
ErrMissingType = err{
|
||||||
code: http.StatusBadRequest,
|
code: http.StatusBadRequest,
|
||||||
error: errors.New("Missing route Type"),
|
error: errors.New("Missing route Type"),
|
||||||
}
|
}
|
||||||
ErrRoutesValidationPathMalformed = err{
|
ErrPathMalformed = err{
|
||||||
code: http.StatusBadRequest,
|
code: http.StatusBadRequest,
|
||||||
error: errors.New("Path malformed"),
|
error: errors.New("Path malformed"),
|
||||||
}
|
}
|
||||||
ErrRoutesValidationNegativeTimeout = err{
|
ErrInvalidToTime = err{
|
||||||
|
code: http.StatusBadRequest,
|
||||||
|
error: errors.New("to_time is not an epoch time"),
|
||||||
|
}
|
||||||
|
ErrInvalidFromTime = err{
|
||||||
|
code: http.StatusBadRequest,
|
||||||
|
error: errors.New("from_time is not an epoch time"),
|
||||||
|
}
|
||||||
|
ErrNegativeTimeout = err{
|
||||||
code: http.StatusBadRequest,
|
code: http.StatusBadRequest,
|
||||||
error: errors.New("Negative timeout"),
|
error: errors.New("Negative timeout"),
|
||||||
}
|
}
|
||||||
ErrRoutesValidationNegativeIdleTimeout = err{
|
ErrNegativeIdleTimeout = err{
|
||||||
code: http.StatusBadRequest,
|
code: http.StatusBadRequest,
|
||||||
error: errors.New("Negative idle timeout"),
|
error: errors.New("Negative idle timeout"),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,51 +63,51 @@ func (r *Route) SetDefaults() {
|
|||||||
func (r *Route) Validate(skipZero bool) error {
|
func (r *Route) Validate(skipZero bool) error {
|
||||||
if !skipZero {
|
if !skipZero {
|
||||||
if r.AppName == "" {
|
if r.AppName == "" {
|
||||||
return ErrRoutesValidationMissingAppName
|
return ErrMissingAppName
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.Path == "" {
|
if r.Path == "" {
|
||||||
return ErrRoutesValidationMissingPath
|
return ErrMissingPath
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.Image == "" {
|
if r.Image == "" {
|
||||||
return ErrRoutesValidationMissingImage
|
return ErrMissingImage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !skipZero || r.Path != "" {
|
if !skipZero || r.Path != "" {
|
||||||
u, err := url.Parse(r.Path)
|
u, err := url.Parse(r.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ErrRoutesValidationPathMalformed
|
return ErrPathMalformed
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.Contains(u.Path, ":") {
|
if strings.Contains(u.Path, ":") {
|
||||||
return ErrRoutesValidationFoundDynamicURL
|
return ErrFoundDynamicURL
|
||||||
}
|
}
|
||||||
|
|
||||||
if !path.IsAbs(u.Path) {
|
if !path.IsAbs(u.Path) {
|
||||||
return ErrRoutesValidationInvalidPath
|
return ErrInvalidPath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !skipZero || r.Type != "" {
|
if !skipZero || r.Type != "" {
|
||||||
if r.Type != TypeAsync && r.Type != TypeSync {
|
if r.Type != TypeAsync && r.Type != TypeSync {
|
||||||
return ErrRoutesValidationInvalidType
|
return ErrInvalidType
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !skipZero || r.Format != "" {
|
if !skipZero || r.Format != "" {
|
||||||
if r.Format != FormatDefault && r.Format != FormatHTTP {
|
if r.Format != FormatDefault && r.Format != FormatHTTP {
|
||||||
return ErrRoutesValidationInvalidFormat
|
return ErrInvalidFormat
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.Timeout < 0 {
|
if r.Timeout < 0 {
|
||||||
return ErrRoutesValidationNegativeTimeout
|
return ErrNegativeTimeout
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.IdleTimeout < 0 {
|
if r.IdleTimeout < 0 {
|
||||||
return ErrRoutesValidationNegativeIdleTimeout
|
return ErrNegativeIdleTimeout
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -168,9 +168,11 @@ func (r *Route) Update(new *Route) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO are these sql LIKE queries? or strict matches?
|
|
||||||
type RouteFilter struct {
|
type RouteFilter struct {
|
||||||
Path string
|
PathPrefix string // this is prefix match TODO
|
||||||
AppName string
|
AppName string // this is exact match (important for security)
|
||||||
Image string
|
Image string // this is exact match
|
||||||
|
|
||||||
|
Cursor string
|
||||||
|
PerPage int
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/fnproject/fn/api/models"
|
"github.com/fnproject/fn/api/models"
|
||||||
@@ -10,13 +11,24 @@ import (
|
|||||||
func (s *Server) handleAppList(c *gin.Context) {
|
func (s *Server) handleAppList(c *gin.Context) {
|
||||||
ctx := c.Request.Context()
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
filter := &models.AppFilter{}
|
var filter models.AppFilter
|
||||||
|
filter.Cursor, filter.PerPage = pageParams(c, true)
|
||||||
|
|
||||||
apps, err := s.Datastore.GetApps(ctx, filter)
|
apps, err := s.Datastore.GetApps(ctx, &filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleErrorResponse(c, err)
|
handleErrorResponse(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, appsResponse{"Successfully listed applications", apps})
|
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, appsResponse{
|
||||||
|
Message: "Successfully listed applications",
|
||||||
|
NextCursor: nextCursor,
|
||||||
|
Apps: apps,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package server
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -120,17 +122,36 @@ func TestAppList(t *testing.T) {
|
|||||||
|
|
||||||
rnr, cancel := testRunner(t)
|
rnr, cancel := testRunner(t)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
ds := datastore.NewMock()
|
ds := datastore.NewMockInit(
|
||||||
|
[]*models.App{
|
||||||
|
{Name: "myapp"},
|
||||||
|
{Name: "myapp2"},
|
||||||
|
{Name: "myapp3"},
|
||||||
|
},
|
||||||
|
nil, // no routes
|
||||||
|
nil, // no calls
|
||||||
|
)
|
||||||
fnl := logs.NewMock()
|
fnl := logs.NewMock()
|
||||||
srv := testServer(ds, &mqs.Mock{}, fnl, rnr)
|
srv := testServer(ds, &mqs.Mock{}, fnl, rnr)
|
||||||
|
|
||||||
|
a1b := base64.RawURLEncoding.EncodeToString([]byte("myapp"))
|
||||||
|
a2b := base64.RawURLEncoding.EncodeToString([]byte("myapp2"))
|
||||||
|
a3b := base64.RawURLEncoding.EncodeToString([]byte("myapp3"))
|
||||||
|
|
||||||
for i, test := range []struct {
|
for i, test := range []struct {
|
||||||
path string
|
path string
|
||||||
body string
|
body string
|
||||||
expectedCode int
|
expectedCode int
|
||||||
expectedError error
|
expectedError error
|
||||||
|
expectedLen int
|
||||||
|
nextCursor string
|
||||||
}{
|
}{
|
||||||
{"/v1/apps", "", http.StatusOK, nil},
|
{"/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)
|
_, rec := routerRequest(t, srv.Router, "GET", test.path, nil)
|
||||||
|
|
||||||
@@ -148,6 +169,20 @@ func TestAppList(t *testing.T) {
|
|||||||
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 {
|
||||||
|
// normal path
|
||||||
|
|
||||||
|
var resp appsResponse
|
||||||
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,5 +18,5 @@ func (s *Server) handleCallGet(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, fnCallResponse{"Successfully loaded call", callObj})
|
c.JSON(http.StatusOK, callResponse{"Successfully loaded call", callObj})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,45 +2,80 @@ package server
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/fnproject/fn/api"
|
"github.com/fnproject/fn/api"
|
||||||
"github.com/fnproject/fn/api/models"
|
"github.com/fnproject/fn/api/models"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/go-openapi/strfmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Server) handleCallList(c *gin.Context) {
|
func (s *Server) handleCallList(c *gin.Context) {
|
||||||
ctx := c.Request.Context()
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
name, ok := c.Get(api.AppName)
|
appName := c.MustGet(api.AppName).(string)
|
||||||
appName, conv := name.(string)
|
|
||||||
if ok && conv && appName == "" {
|
// TODO api.CRoute needs to be escaped probably, since it has '/' a lot
|
||||||
handleErrorResponse(c, models.ErrRoutesValidationMissingAppName)
|
filter := models.CallFilter{AppName: appName, Path: c.Query("path")}
|
||||||
|
filter.Cursor, filter.PerPage = pageParams(c, false) // ids are url safe
|
||||||
|
|
||||||
|
var err error
|
||||||
|
filter.FromTime, filter.ToTime, err = timeParams(c)
|
||||||
|
if err != nil {
|
||||||
|
handleErrorResponse(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
filter := models.CallFilter{AppName: appName, Path: c.Query(api.CRoute)}
|
|
||||||
|
|
||||||
calls, err := s.Datastore.GetCalls(ctx, &filter)
|
calls, err := s.Datastore.GetCalls(ctx, &filter)
|
||||||
if err != nil {
|
|
||||||
handleErrorResponse(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(calls) == 0 {
|
if len(calls) == 0 {
|
||||||
|
// TODO this should be done in front of this handler to even get here...
|
||||||
_, err = s.Datastore.GetApp(c, appName)
|
_, err = s.Datastore.GetApp(c, appName)
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleErrorResponse(c, err)
|
handleErrorResponse(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if filter.Path != "" {
|
var nextCursor string
|
||||||
_, err = s.Datastore.GetRoute(c, appName, filter.Path)
|
if len(calls) > 0 && len(calls) == filter.PerPage {
|
||||||
if err != nil {
|
nextCursor = calls[len(calls)-1].ID
|
||||||
handleErrorResponse(c, err)
|
// don't base64, IDs are url safe
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, fnCallsResponse{"Successfully listed calls", calls})
|
c.JSON(http.StatusOK, callsResponse{
|
||||||
|
Message: "Successfully listed calls",
|
||||||
|
NextCursor: nextCursor,
|
||||||
|
Calls: calls,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// "" gets parsed to a zero time, which is fine (ignored in query)
|
||||||
|
func timeParams(c *gin.Context) (fromTime, toTime strfmt.DateTime, err error) {
|
||||||
|
fromStr := c.Query("from_time")
|
||||||
|
toStr := c.Query("to_time")
|
||||||
|
var ok bool
|
||||||
|
if fromStr != "" {
|
||||||
|
fromTime, ok = strToTime(fromStr)
|
||||||
|
if !ok {
|
||||||
|
return fromTime, toTime, models.ErrInvalidFromTime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if toStr != "" {
|
||||||
|
toTime, ok = strToTime(toStr)
|
||||||
|
if !ok {
|
||||||
|
return fromTime, toTime, models.ErrInvalidToTime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fromTime, toTime, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func strToTime(str string) (strfmt.DateTime, bool) {
|
||||||
|
sec, err := strconv.ParseInt(str, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return strfmt.DateTime(time.Time{}), false
|
||||||
|
}
|
||||||
|
return strfmt.DateTime(time.Unix(sec, 0)), true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ func (s *Server) handleCallLogGet(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, fnCallLogResponse{"Successfully loaded call", callObj})
|
c.JSON(http.StatusOK, callLogResponse{"Successfully loaded call", callObj})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleCallLogDelete(c *gin.Context) {
|
func (s *Server) handleCallLogDelete(c *gin.Context) {
|
||||||
|
|||||||
190
api/server/calls_test.go
Normal file
190
api/server/calls_test.go
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"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"
|
||||||
|
"github.com/go-openapi/strfmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCallGet(t *testing.T) {
|
||||||
|
buf := setLogBuffer()
|
||||||
|
|
||||||
|
call := &models.Call{
|
||||||
|
ID: id.New().String(),
|
||||||
|
AppName: "myapp",
|
||||||
|
Path: "/thisisatest",
|
||||||
|
Image: "fnproject/hello",
|
||||||
|
// Delay: 0,
|
||||||
|
Type: "sync",
|
||||||
|
Format: "default",
|
||||||
|
// Payload: TODO,
|
||||||
|
Priority: new(int32), // TODO this is crucial, apparently
|
||||||
|
Timeout: 30,
|
||||||
|
IdleTimeout: 30,
|
||||||
|
Memory: 256,
|
||||||
|
BaseEnv: map[string]string{"YO": "DAWG"},
|
||||||
|
EnvVars: map[string]string{"YO": "DAWG"},
|
||||||
|
CreatedAt: strfmt.DateTime(time.Now()),
|
||||||
|
URL: "http://localhost:8080/r/myapp/thisisatest",
|
||||||
|
Method: "GET",
|
||||||
|
}
|
||||||
|
|
||||||
|
rnr, cancel := testRunner(t)
|
||||||
|
defer cancel()
|
||||||
|
ds := datastore.NewMockInit(
|
||||||
|
[]*models.App{
|
||||||
|
{Name: call.AppName},
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
[]*models.Call{call},
|
||||||
|
)
|
||||||
|
fnl := logs.NewMock()
|
||||||
|
srv := testServer(ds, &mqs.Mock{}, fnl, rnr)
|
||||||
|
|
||||||
|
for i, test := range []struct {
|
||||||
|
path string
|
||||||
|
body string
|
||||||
|
expectedCode int
|
||||||
|
expectedError error
|
||||||
|
}{
|
||||||
|
{"/v1/apps//calls/" + call.ID, "", http.StatusBadRequest, models.ErrMissingAppName},
|
||||||
|
{"/v1/apps/nodawg/calls/" + call.ID, "", http.StatusNotFound, models.ErrCallNotFound}, // TODO a little weird
|
||||||
|
{"/v1/apps/myapp/calls/" + call.ID[:3], "", http.StatusNotFound, models.ErrCallNotFound},
|
||||||
|
{"/v1/apps/myapp/calls/" + call.ID, "", http.StatusOK, nil},
|
||||||
|
} {
|
||||||
|
_, rec := routerRequest(t, srv.Router, "GET", test.path, nil)
|
||||||
|
|
||||||
|
if rec.Code != test.expectedCode {
|
||||||
|
t.Log(buf.String())
|
||||||
|
t.Errorf("Test %d: Expected status code to be %d but was %d",
|
||||||
|
i, test.expectedCode, rec.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
if test.expectedError != nil {
|
||||||
|
resp := getErrorResponse(t, rec)
|
||||||
|
|
||||||
|
if !strings.Contains(resp.Error.Message, test.expectedError.Error()) {
|
||||||
|
t.Log(buf.String())
|
||||||
|
t.Errorf("Test %d: Expected error message to have `%s`",
|
||||||
|
i, test.expectedError.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO json parse the body and assert fields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCallList(t *testing.T) {
|
||||||
|
buf := setLogBuffer()
|
||||||
|
|
||||||
|
call := &models.Call{
|
||||||
|
ID: id.New().String(),
|
||||||
|
AppName: "myapp",
|
||||||
|
Path: "/thisisatest",
|
||||||
|
Image: "fnproject/hello",
|
||||||
|
// Delay: 0,
|
||||||
|
Type: "sync",
|
||||||
|
Format: "default",
|
||||||
|
// Payload: TODO,
|
||||||
|
Priority: new(int32), // TODO this is crucial, apparently
|
||||||
|
Timeout: 30,
|
||||||
|
IdleTimeout: 30,
|
||||||
|
Memory: 256,
|
||||||
|
BaseEnv: map[string]string{"YO": "DAWG"},
|
||||||
|
EnvVars: map[string]string{"YO": "DAWG"},
|
||||||
|
CreatedAt: strfmt.DateTime(time.Now()),
|
||||||
|
URL: "http://localhost:8080/r/myapp/thisisatest",
|
||||||
|
Method: "GET",
|
||||||
|
}
|
||||||
|
c2 := *call
|
||||||
|
c3 := *call
|
||||||
|
c2.ID = id.New().String()
|
||||||
|
c2.CreatedAt = strfmt.DateTime(time.Now().Add(100 * time.Second))
|
||||||
|
c2.Path = "test2"
|
||||||
|
c3.ID = id.New().String()
|
||||||
|
c3.CreatedAt = strfmt.DateTime(time.Now().Add(200 * time.Second))
|
||||||
|
c3.Path = "/test3"
|
||||||
|
|
||||||
|
rnr, cancel := testRunner(t)
|
||||||
|
defer cancel()
|
||||||
|
ds := datastore.NewMockInit(
|
||||||
|
[]*models.App{
|
||||||
|
{Name: call.AppName},
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
[]*models.Call{call, &c2, &c3},
|
||||||
|
)
|
||||||
|
fnl := logs.NewMock()
|
||||||
|
srv := testServer(ds, &mqs.Mock{}, fnl, rnr)
|
||||||
|
|
||||||
|
// add / sub 1 second b/c unix time will lop off millis and mess up our comparisons
|
||||||
|
rangeTest := fmt.Sprintf("from_time=%d&to_time=%d",
|
||||||
|
time.Time(call.CreatedAt).Add(1*time.Second).Unix(),
|
||||||
|
time.Time(c3.CreatedAt).Add(-1*time.Second).Unix(),
|
||||||
|
)
|
||||||
|
|
||||||
|
for i, test := range []struct {
|
||||||
|
path string
|
||||||
|
body string
|
||||||
|
expectedCode int
|
||||||
|
expectedError error
|
||||||
|
expectedLen int
|
||||||
|
nextCursor string
|
||||||
|
}{
|
||||||
|
{"/v1/apps//calls", "", http.StatusBadRequest, models.ErrMissingAppName, 0, ""},
|
||||||
|
{"/v1/apps/nodawg/calls", "", http.StatusNotFound, models.ErrAppsNotFound, 0, ""},
|
||||||
|
{"/v1/apps/myapp/calls", "", http.StatusOK, nil, 3, ""},
|
||||||
|
{"/v1/apps/myapp/calls?per_page=1", "", http.StatusOK, nil, 1, c3.ID},
|
||||||
|
{"/v1/apps/myapp/calls?per_page=1&cursor=" + c3.ID, "", http.StatusOK, nil, 1, c2.ID},
|
||||||
|
{"/v1/apps/myapp/calls?per_page=1&cursor=" + c2.ID, "", http.StatusOK, nil, 1, call.ID},
|
||||||
|
{"/v1/apps/myapp/calls?per_page=100&cursor=" + c2.ID, "", http.StatusOK, nil, 1, ""}, // cursor is empty if per_page > len(results)
|
||||||
|
{"/v1/apps/myapp/calls?per_page=1&cursor=" + call.ID, "", http.StatusOK, nil, 0, ""}, // cursor could point to empty page
|
||||||
|
{"/v1/apps/myapp/calls?" + rangeTest, "", http.StatusOK, nil, 1, ""},
|
||||||
|
{"/v1/apps/myapp/calls?from_time=xyz", "", http.StatusBadRequest, models.ErrInvalidFromTime, 0, ""},
|
||||||
|
{"/v1/apps/myapp/calls?to_time=xyz", "", http.StatusBadRequest, models.ErrInvalidToTime, 0, ""},
|
||||||
|
|
||||||
|
// TODO path isn't url safe w/ '/', so this is weird. hack in for tests
|
||||||
|
{"/v1/apps/myapp/calls?path=test2", "", http.StatusOK, nil, 1, ""},
|
||||||
|
} {
|
||||||
|
_, 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 resp.Error == nil || !strings.Contains(resp.Error.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.Error)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// normal path
|
||||||
|
|
||||||
|
var resp callsResponse
|
||||||
|
err := json.NewDecoder(rec.Body).Decode(&resp)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Test %d: Expected response body to be a valid json object. err: %v", i, err)
|
||||||
|
}
|
||||||
|
if len(resp.Calls) != test.expectedLen {
|
||||||
|
t.Fatalf("Test %d: Expected apps length to be %d, but got %d", i, test.expectedLen, len(resp.Calls))
|
||||||
|
}
|
||||||
|
if resp.NextCursor != test.nextCursor {
|
||||||
|
t.Errorf("Test %d: Expected next_cursor to be %s, but got %s", i, test.nextCursor, resp.NextCursor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -163,7 +163,7 @@ func bindRoute(c *gin.Context, method string, wroute *models.RouteWrapper) error
|
|||||||
}
|
}
|
||||||
if method == http.MethodPost {
|
if method == http.MethodPost {
|
||||||
if wroute.Route.Path == "" {
|
if wroute.Route.Path == "" {
|
||||||
return models.ErrRoutesValidationMissingPath
|
return models.ErrMissingPath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/fnproject/fn/api"
|
"github.com/fnproject/fn/api"
|
||||||
@@ -11,24 +12,20 @@ import (
|
|||||||
func (s *Server) handleRouteList(c *gin.Context) {
|
func (s *Server) handleRouteList(c *gin.Context) {
|
||||||
ctx := c.Request.Context()
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
filter := &models.RouteFilter{}
|
appName := c.MustGet(api.AppName).(string)
|
||||||
|
|
||||||
if img := c.Query("image"); img != "" {
|
var filter models.RouteFilter
|
||||||
filter.Image = img
|
filter.Image = c.Query("image")
|
||||||
}
|
// filter.PathPrefix = c.Query("path_prefix") TODO not hooked up
|
||||||
|
filter.Cursor, filter.PerPage = pageParams(c, true)
|
||||||
|
|
||||||
var routes []*models.Route
|
routes, err := s.Datastore.GetRoutesByApp(ctx, appName, &filter)
|
||||||
var err error
|
|
||||||
appName, exists := c.Get(api.AppName)
|
// if there are no routes for the app, check if the app exists to return
|
||||||
name, ok := appName.(string)
|
// 404 if it does not
|
||||||
if exists && ok && name != "" {
|
// TODO this should be done in front of this handler to even get here...
|
||||||
routes, err = s.Datastore.GetRoutesByApp(ctx, name, filter)
|
if err == nil && len(routes) == 0 {
|
||||||
// if there are no routes for the app, check if the app exists to return 404 if it does not
|
_, err = s.Datastore.GetApp(ctx, appName)
|
||||||
if len(routes) == 0 {
|
|
||||||
_, err = s.Datastore.GetApp(ctx, name)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
routes, err = s.Datastore.GetRoutes(ctx, filter)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -36,5 +33,15 @@ func (s *Server) handleRouteList(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, routesResponse{"Successfully listed routes", routes})
|
var nextCursor string
|
||||||
|
if len(routes) > 0 && len(routes) == filter.PerPage {
|
||||||
|
last := []byte(routes[len(routes)-1].Path)
|
||||||
|
nextCursor = base64.RawURLEncoding.EncodeToString(last)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, routesResponse{
|
||||||
|
Message: "Successfully listed routes",
|
||||||
|
NextCursor: nextCursor,
|
||||||
|
Routes: routes,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package server
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -59,10 +61,10 @@ func TestRouteCreate(t *testing.T) {
|
|||||||
{datastore.NewMock(), logs.NewMock(), http.MethodPost, "/v1/apps/a/routes", ``, http.StatusBadRequest, models.ErrInvalidJSON},
|
{datastore.NewMock(), logs.NewMock(), http.MethodPost, "/v1/apps/a/routes", ``, http.StatusBadRequest, models.ErrInvalidJSON},
|
||||||
{datastore.NewMock(), logs.NewMock(), http.MethodPost, "/v1/apps/a/routes", `{ "type": "sync" }`, http.StatusBadRequest, models.ErrRoutesMissingNew},
|
{datastore.NewMock(), logs.NewMock(), http.MethodPost, "/v1/apps/a/routes", `{ "type": "sync" }`, http.StatusBadRequest, models.ErrRoutesMissingNew},
|
||||||
{datastore.NewMock(), logs.NewMock(), http.MethodPost, "/v1/apps/a/routes", `{ "path": "/myroute", "type": "sync" }`, http.StatusBadRequest, models.ErrRoutesMissingNew},
|
{datastore.NewMock(), logs.NewMock(), http.MethodPost, "/v1/apps/a/routes", `{ "path": "/myroute", "type": "sync" }`, http.StatusBadRequest, models.ErrRoutesMissingNew},
|
||||||
{datastore.NewMock(), logs.NewMock(), http.MethodPost, "/v1/apps/a/routes", `{ "route": { } }`, http.StatusBadRequest, models.ErrRoutesValidationMissingPath},
|
{datastore.NewMock(), logs.NewMock(), http.MethodPost, "/v1/apps/a/routes", `{ "route": { } }`, http.StatusBadRequest, models.ErrMissingPath},
|
||||||
{datastore.NewMock(), logs.NewMock(), http.MethodPost, "/v1/apps/a/routes", `{ "route": { "path": "/myroute", "type": "sync" } }`, http.StatusBadRequest, models.ErrRoutesValidationMissingImage},
|
{datastore.NewMock(), logs.NewMock(), http.MethodPost, "/v1/apps/a/routes", `{ "route": { "path": "/myroute", "type": "sync" } }`, http.StatusBadRequest, models.ErrMissingImage},
|
||||||
{datastore.NewMock(), logs.NewMock(), http.MethodPost, "/v1/apps/a/routes", `{ "route": { "image": "fnproject/hello", "type": "sync" } }`, http.StatusBadRequest, models.ErrRoutesValidationMissingPath},
|
{datastore.NewMock(), logs.NewMock(), http.MethodPost, "/v1/apps/a/routes", `{ "route": { "image": "fnproject/hello", "type": "sync" } }`, http.StatusBadRequest, models.ErrMissingPath},
|
||||||
{datastore.NewMock(), logs.NewMock(), http.MethodPost, "/v1/apps/a/routes", `{ "route": { "image": "fnproject/hello", "path": "myroute", "type": "sync" } }`, http.StatusBadRequest, models.ErrRoutesValidationInvalidPath},
|
{datastore.NewMock(), logs.NewMock(), http.MethodPost, "/v1/apps/a/routes", `{ "route": { "image": "fnproject/hello", "path": "myroute", "type": "sync" } }`, http.StatusBadRequest, models.ErrInvalidPath},
|
||||||
{datastore.NewMock(), logs.NewMock(), http.MethodPost, "/v1/apps/$/routes", `{ "route": { "image": "fnproject/hello", "path": "/myroute", "type": "sync" } }`, http.StatusBadRequest, models.ErrAppsValidationInvalidName},
|
{datastore.NewMock(), logs.NewMock(), http.MethodPost, "/v1/apps/$/routes", `{ "route": { "image": "fnproject/hello", "path": "/myroute", "type": "sync" } }`, http.StatusBadRequest, models.ErrAppsValidationInvalidName},
|
||||||
{datastore.NewMockInit(nil,
|
{datastore.NewMockInit(nil,
|
||||||
[]*models.Route{
|
[]*models.Route{
|
||||||
@@ -87,13 +89,13 @@ func TestRoutePut(t *testing.T) {
|
|||||||
// errors (NOTE: this route doesn't exist yet)
|
// errors (NOTE: this route doesn't exist yet)
|
||||||
{datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ }`, http.StatusBadRequest, models.ErrRoutesMissingNew},
|
{datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ }`, http.StatusBadRequest, models.ErrRoutesMissingNew},
|
||||||
{datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "path": "/myroute", "type": "sync" }`, http.StatusBadRequest, models.ErrRoutesMissingNew},
|
{datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "path": "/myroute", "type": "sync" }`, http.StatusBadRequest, models.ErrRoutesMissingNew},
|
||||||
{datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "route": { "type": "sync" } }`, http.StatusBadRequest, models.ErrRoutesValidationMissingImage},
|
{datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "route": { "type": "sync" } }`, http.StatusBadRequest, models.ErrMissingImage},
|
||||||
{datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "route": { "path": "/myroute", "type": "sync" } }`, http.StatusBadRequest, models.ErrRoutesValidationMissingImage},
|
{datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "route": { "path": "/myroute", "type": "sync" } }`, http.StatusBadRequest, models.ErrMissingImage},
|
||||||
{datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "route": { "image": "fnproject/hello", "path": "myroute", "type": "sync" } }`, http.StatusConflict, models.ErrRoutesPathImmutable},
|
{datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "route": { "image": "fnproject/hello", "path": "myroute", "type": "sync" } }`, http.StatusConflict, models.ErrRoutesPathImmutable},
|
||||||
{datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "route": { "image": "fnproject/hello", "path": "diffRoute", "type": "sync" } }`, http.StatusConflict, models.ErrRoutesPathImmutable},
|
{datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "route": { "image": "fnproject/hello", "path": "diffRoute", "type": "sync" } }`, http.StatusConflict, models.ErrRoutesPathImmutable},
|
||||||
{datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/$/routes/myroute", `{ "route": { "image": "fnproject/hello", "path": "/myroute", "type": "sync" } }`, http.StatusBadRequest, models.ErrAppsValidationInvalidName},
|
{datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/$/routes/myroute", `{ "route": { "image": "fnproject/hello", "path": "/myroute", "type": "sync" } }`, http.StatusBadRequest, models.ErrAppsValidationInvalidName},
|
||||||
{datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "route": { "image": "fnproject/hello", "path": "/myroute", "type": "invalid-type" } }`, http.StatusBadRequest, models.ErrRoutesValidationInvalidType},
|
{datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "route": { "image": "fnproject/hello", "path": "/myroute", "type": "invalid-type" } }`, http.StatusBadRequest, models.ErrInvalidType},
|
||||||
{datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "route": { "image": "fnproject/hello", "path": "/myroute", "format": "invalid-format", "type": "sync" } }`, http.StatusBadRequest, models.ErrRoutesValidationInvalidFormat},
|
{datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "route": { "image": "fnproject/hello", "path": "/myroute", "format": "invalid-format", "type": "sync" } }`, http.StatusBadRequest, models.ErrInvalidFormat},
|
||||||
|
|
||||||
// success
|
// success
|
||||||
{datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "route": { "image": "fnproject/hello", "path": "/myroute", "type": "sync" } }`, http.StatusOK, nil},
|
{datastore.NewMock(), logs.NewMock(), http.MethodPut, "/v1/apps/a/routes/myroute", `{ "route": { "image": "fnproject/hello", "path": "/myroute", "type": "sync" } }`, http.StatusOK, nil},
|
||||||
@@ -149,18 +151,53 @@ func TestRouteList(t *testing.T) {
|
|||||||
rnr, cancel := testRunner(t)
|
rnr, cancel := testRunner(t)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
ds := datastore.NewMock()
|
ds := datastore.NewMockInit(
|
||||||
|
[]*models.App{
|
||||||
|
{Name: "myapp"},
|
||||||
|
},
|
||||||
|
[]*models.Route{
|
||||||
|
{
|
||||||
|
AppName: "myapp",
|
||||||
|
Path: "/myroute",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
AppName: "myapp",
|
||||||
|
Path: "/myroute1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
AppName: "myapp",
|
||||||
|
Path: "/myroute2",
|
||||||
|
Image: "fnproject/hello",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nil, // no calls
|
||||||
|
)
|
||||||
fnl := logs.NewMock()
|
fnl := logs.NewMock()
|
||||||
|
|
||||||
|
r1b := base64.RawURLEncoding.EncodeToString([]byte("/myroute"))
|
||||||
|
r2b := base64.RawURLEncoding.EncodeToString([]byte("/myroute1"))
|
||||||
|
r3b := base64.RawURLEncoding.EncodeToString([]byte("/myroute2"))
|
||||||
|
|
||||||
srv := testServer(ds, &mqs.Mock{}, fnl, rnr)
|
srv := testServer(ds, &mqs.Mock{}, fnl, rnr)
|
||||||
|
|
||||||
for i, test := range []struct {
|
for i, test := range []struct {
|
||||||
path string
|
path string
|
||||||
body string
|
body string
|
||||||
|
|
||||||
expectedCode int
|
expectedCode int
|
||||||
expectedError error
|
expectedError error
|
||||||
|
expectedLen int
|
||||||
|
nextCursor string
|
||||||
}{
|
}{
|
||||||
{"/v1/apps/a/routes", "", http.StatusNotFound, models.ErrAppsNotFound},
|
{"/v1/apps//routes", "", http.StatusBadRequest, models.ErrMissingAppName, 0, ""},
|
||||||
|
{"/v1/apps/a/routes", "", http.StatusNotFound, models.ErrAppsNotFound, 0, ""},
|
||||||
|
{"/v1/apps/myapp/routes", "", http.StatusOK, nil, 3, ""},
|
||||||
|
{"/v1/apps/myapp/routes?per_page=1", "", http.StatusOK, nil, 1, r1b},
|
||||||
|
{"/v1/apps/myapp/routes?per_page=1&cursor=" + r1b, "", http.StatusOK, nil, 1, r2b},
|
||||||
|
{"/v1/apps/myapp/routes?per_page=1&cursor=" + r2b, "", http.StatusOK, nil, 1, r3b},
|
||||||
|
{"/v1/apps/myapp/routes?per_page=100&cursor=" + r2b, "", http.StatusOK, nil, 1, ""}, // cursor is empty if per_page > len(results)
|
||||||
|
{"/v1/apps/myapp/routes?per_page=1&cursor=" + r3b, "", http.StatusOK, nil, 0, ""}, // cursor could point to empty page
|
||||||
|
{"/v1/apps/myapp/routes?image=fnproject/hello", "", http.StatusOK, nil, 1, ""},
|
||||||
} {
|
} {
|
||||||
_, rec := routerRequest(t, srv.Router, "GET", test.path, nil)
|
_, rec := routerRequest(t, srv.Router, "GET", test.path, nil)
|
||||||
|
|
||||||
@@ -178,6 +215,20 @@ func TestRouteList(t *testing.T) {
|
|||||||
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 {
|
||||||
|
// normal path
|
||||||
|
|
||||||
|
var resp routesResponse
|
||||||
|
err := json.NewDecoder(rec.Body).Decode(&resp)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Test %d: Expected response body to be a valid json object. err: %v", i, err)
|
||||||
|
}
|
||||||
|
if len(resp.Routes) != test.expectedLen {
|
||||||
|
t.Errorf("Test %d: Expected route length to be %d, but got %d", i, test.expectedLen, len(resp.Routes))
|
||||||
|
}
|
||||||
|
if resp.NextCursor != test.nextCursor {
|
||||||
|
t.Errorf("Test %d: Expected next_cursor to be %s, but got %s", i, test.nextCursor, resp.NextCursor)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -228,8 +279,8 @@ func TestRouteUpdate(t *testing.T) {
|
|||||||
// errors
|
// errors
|
||||||
{datastore.NewMock(), logs.NewMock(), http.MethodPatch, "/v1/apps/a/routes/myroute/do", ``, http.StatusBadRequest, models.ErrInvalidJSON},
|
{datastore.NewMock(), logs.NewMock(), http.MethodPatch, "/v1/apps/a/routes/myroute/do", ``, http.StatusBadRequest, models.ErrInvalidJSON},
|
||||||
{datastore.NewMock(), logs.NewMock(), http.MethodPatch, "/v1/apps/a/routes/myroute/do", `{}`, http.StatusBadRequest, models.ErrRoutesMissingNew},
|
{datastore.NewMock(), logs.NewMock(), http.MethodPatch, "/v1/apps/a/routes/myroute/do", `{}`, http.StatusBadRequest, models.ErrRoutesMissingNew},
|
||||||
{datastore.NewMock(), logs.NewMock(), http.MethodPatch, "/v1/apps/a/routes/myroute/do", `{ "route": { "type": "invalid-type" } }`, http.StatusBadRequest, models.ErrRoutesValidationInvalidType},
|
{datastore.NewMock(), logs.NewMock(), http.MethodPatch, "/v1/apps/a/routes/myroute/do", `{ "route": { "type": "invalid-type" } }`, http.StatusBadRequest, models.ErrInvalidType},
|
||||||
{datastore.NewMock(), logs.NewMock(), http.MethodPatch, "/v1/apps/a/routes/myroute/do", `{ "route": { "format": "invalid-format" } }`, http.StatusBadRequest, models.ErrRoutesValidationInvalidFormat},
|
{datastore.NewMock(), logs.NewMock(), http.MethodPatch, "/v1/apps/a/routes/myroute/do", `{ "route": { "format": "invalid-format" } }`, http.StatusBadRequest, models.ErrInvalidFormat},
|
||||||
|
|
||||||
// success
|
// success
|
||||||
{datastore.NewMockInit(nil,
|
{datastore.NewMockInit(nil,
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ package server
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/fnproject/fn/api"
|
"github.com/fnproject/fn/api"
|
||||||
"github.com/fnproject/fn/api/agent"
|
"github.com/fnproject/fn/api/agent"
|
||||||
@@ -210,6 +212,16 @@ func loggerWrap(c *gin.Context) {
|
|||||||
c.Next()
|
c.Next()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func appWrap(c *gin.Context) {
|
||||||
|
appName := c.GetString(api.AppName)
|
||||||
|
if appName == "" {
|
||||||
|
handleErrorResponse(c, models.ErrMissingAppName)
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) handleRunnerRequest(c *gin.Context) {
|
func (s *Server) handleRunnerRequest(c *gin.Context) {
|
||||||
s.handleRequest(c)
|
s.handleRequest(c)
|
||||||
}
|
}
|
||||||
@@ -269,18 +281,20 @@ func (s *Server) bindHandlers(ctx context.Context) {
|
|||||||
engine.GET("/version", handleVersion)
|
engine.GET("/version", handleVersion)
|
||||||
engine.GET("/stats", s.handleStats)
|
engine.GET("/stats", s.handleStats)
|
||||||
|
|
||||||
|
{
|
||||||
v1 := engine.Group("/v1")
|
v1 := engine.Group("/v1")
|
||||||
v1.Use(s.middlewareWrapperFunc(ctx))
|
v1.Use(s.middlewareWrapperFunc(ctx))
|
||||||
{
|
|
||||||
v1.GET("/apps", s.handleAppList)
|
v1.GET("/apps", s.handleAppList)
|
||||||
v1.POST("/apps", s.handleAppCreate)
|
v1.POST("/apps", s.handleAppCreate)
|
||||||
|
|
||||||
v1.GET("/apps/:app", s.handleAppGet)
|
|
||||||
v1.PATCH("/apps/:app", s.handleAppUpdate)
|
|
||||||
v1.DELETE("/apps/:app", s.handleAppDelete)
|
|
||||||
|
|
||||||
apps := v1.Group("/apps/:app")
|
|
||||||
{
|
{
|
||||||
|
apps := v1.Group("/apps/:app")
|
||||||
|
apps.Use(appWrap)
|
||||||
|
|
||||||
|
apps.GET("", s.handleAppGet)
|
||||||
|
apps.PATCH("", s.handleAppUpdate)
|
||||||
|
apps.DELETE("", s.handleAppDelete)
|
||||||
|
|
||||||
apps.GET("/routes", s.handleRouteList)
|
apps.GET("/routes", s.handleRouteList)
|
||||||
apps.POST("/routes", s.handleRoutesPostPutPatch)
|
apps.POST("/routes", s.handleRoutesPostPutPatch)
|
||||||
apps.GET("/routes/*route", s.handleRouteGet)
|
apps.GET("/routes/*route", s.handleRouteGet)
|
||||||
@@ -293,12 +307,15 @@ func (s *Server) bindHandlers(ctx context.Context) {
|
|||||||
apps.GET("/calls/:call", s.handleCallGet)
|
apps.GET("/calls/:call", s.handleCallGet)
|
||||||
apps.GET("/calls/:call/log", s.handleCallLogGet)
|
apps.GET("/calls/:call/log", s.handleCallLogGet)
|
||||||
apps.DELETE("/calls/:call/log", s.handleCallLogDelete)
|
apps.DELETE("/calls/:call/log", s.handleCallLogDelete)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
engine.Any("/r/:app", s.handleRunnerRequest)
|
{
|
||||||
engine.Any("/r/:app/*route", s.handleRunnerRequest)
|
runner := engine.Group("/r")
|
||||||
|
runner.Use(appWrap)
|
||||||
|
runner.Any("/:app", s.handleRunnerRequest)
|
||||||
|
runner.Any("/:app/*route", s.handleRunnerRequest)
|
||||||
|
}
|
||||||
|
|
||||||
engine.NoRoute(func(c *gin.Context) {
|
engine.NoRoute(func(c *gin.Context) {
|
||||||
logrus.Debugln("not found", c.Request.URL.Path)
|
logrus.Debugln("not found", c.Request.URL.Path)
|
||||||
@@ -306,6 +323,25 @@ func (s *Server) bindHandlers(ctx context.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// returns the unescaped ?cursor and ?perPage values
|
||||||
|
// pageParams clamps 0 < ?perPage <= 100 and defaults to 30 if 0
|
||||||
|
// ignores parsing errors and falls back to defaults.
|
||||||
|
func pageParams(c *gin.Context, base64d bool) (cursor string, perPage int) {
|
||||||
|
cursor = c.Query("cursor")
|
||||||
|
if base64d {
|
||||||
|
cbytes, _ := base64.RawURLEncoding.DecodeString(cursor)
|
||||||
|
cursor = string(cbytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
perPage, _ = strconv.Atoi(c.Query("per_page"))
|
||||||
|
if perPage > 100 {
|
||||||
|
perPage = 100
|
||||||
|
} else if perPage <= 0 {
|
||||||
|
perPage = 30
|
||||||
|
}
|
||||||
|
return cursor, perPage
|
||||||
|
}
|
||||||
|
|
||||||
type appResponse struct {
|
type appResponse struct {
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
App *models.App `json:"app"`
|
App *models.App `json:"app"`
|
||||||
@@ -313,6 +349,7 @@ type appResponse struct {
|
|||||||
|
|
||||||
type appsResponse struct {
|
type appsResponse struct {
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
|
NextCursor string `json:"next_cursor"`
|
||||||
Apps []*models.App `json:"apps"`
|
Apps []*models.App `json:"apps"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,20 +360,22 @@ type routeResponse struct {
|
|||||||
|
|
||||||
type routesResponse struct {
|
type routesResponse struct {
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
|
NextCursor string `json:"next_cursor"`
|
||||||
Routes []*models.Route `json:"routes"`
|
Routes []*models.Route `json:"routes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type fnCallResponse struct {
|
type callResponse struct {
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
Call *models.Call `json:"call"`
|
Call *models.Call `json:"call"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type fnCallsResponse struct {
|
type callsResponse struct {
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
|
NextCursor string `json:"next_cursor"`
|
||||||
Calls []*models.Call `json:"calls"`
|
Calls []*models.Call `json:"calls"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type fnCallLogResponse struct {
|
type callLogResponse struct {
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
Log *models.CallLog `json:"log"`
|
Log *models.CallLog `json:"log"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -243,3 +243,46 @@ Server will replay with following JSON response:
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Pagination
|
||||||
|
|
||||||
|
The fn api utilizes 'cursoring' to paginate large result sets on endpoints
|
||||||
|
that list resources. The parameters are read from query parameters on incoming
|
||||||
|
requests, and a cursor will be returned to the user if they receive a full
|
||||||
|
page of data to use to retrieve the next page. We'll walk through with a
|
||||||
|
concrete example in just a minute.
|
||||||
|
|
||||||
|
To begin paging through a results set, a user should provide a `?cursor` with an
|
||||||
|
empty string or omit the cursor query parameter altogether. A user may specify
|
||||||
|
how many results per page they would like to receive with the `?per_page`
|
||||||
|
query parameter, which defaults to 30 and has a max of 100. After calling a
|
||||||
|
list endpoint, a user may receive a `response.next_cursor` value in the
|
||||||
|
response, next to the list of resources. If `next_cursor` is an empty string,
|
||||||
|
then there is no further data to retrieve and the user may stop paging. If
|
||||||
|
`next_cursor` is a non-empty string, the user may provide it in the next
|
||||||
|
request's `?cursor` parameter to receive the next page.
|
||||||
|
|
||||||
|
briefly, what this means, is user code should look similar to this:
|
||||||
|
|
||||||
|
```
|
||||||
|
req = "http://my.fn.com/v1/apps/"
|
||||||
|
cursor = ""
|
||||||
|
|
||||||
|
for {
|
||||||
|
req_with_cursor = req + "?" + cursor
|
||||||
|
resp = call_http(req_with_cursor)
|
||||||
|
do_things_with_apps(resp["apps"])
|
||||||
|
|
||||||
|
if resp["next_cursor"] == "" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
cursor = resp["next_cursor"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# done!
|
||||||
|
```
|
||||||
|
|
||||||
|
client libraries will have variables for each of these variables in their
|
||||||
|
respective languages to make this a bit easier, but may the for be with
|
||||||
|
you.
|
||||||
|
|
||||||
|
|||||||
@@ -19,9 +19,20 @@ paths:
|
|||||||
/apps:
|
/apps:
|
||||||
get:
|
get:
|
||||||
summary: "Get all app names."
|
summary: "Get all app names."
|
||||||
description: "Get a list of all the apps in the system."
|
description: "Get a list of all the apps in the system, returned in alphabetical order."
|
||||||
tags:
|
tags:
|
||||||
- Apps
|
- Apps
|
||||||
|
parameters:
|
||||||
|
- name: cursor
|
||||||
|
description: Cursor from previous response.next_cursor to begin results after, if any.
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
in: query
|
||||||
|
- name: per_page
|
||||||
|
description: Number of results to return, defaults to 30. Max of 100.
|
||||||
|
required: false
|
||||||
|
type: int
|
||||||
|
in: query
|
||||||
responses:
|
responses:
|
||||||
200:
|
200:
|
||||||
description: List of apps.
|
description: List of apps.
|
||||||
@@ -182,7 +193,7 @@ paths:
|
|||||||
|
|
||||||
get:
|
get:
|
||||||
summary: Get route list by app name.
|
summary: Get route list by app name.
|
||||||
description: This will list routes for a particular app.
|
description: This will list routes for a particular app, returned in alphabetical order.
|
||||||
tags:
|
tags:
|
||||||
- Routes
|
- Routes
|
||||||
parameters:
|
parameters:
|
||||||
@@ -191,6 +202,21 @@ paths:
|
|||||||
description: Name of app for this set of routes.
|
description: Name of app for this set of routes.
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
- name: image
|
||||||
|
description: Route image to match, exact.
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
in: query
|
||||||
|
- name: cursor
|
||||||
|
description: Cursor from previous response.next_cursor to begin results after, if any.
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
in: query
|
||||||
|
- name: per_page
|
||||||
|
description: Number of results to return, defaults to 30. Max of 100.
|
||||||
|
required: false
|
||||||
|
type: int
|
||||||
|
in: query
|
||||||
responses:
|
responses:
|
||||||
200:
|
200:
|
||||||
description: Route information
|
description: Route information
|
||||||
@@ -424,7 +450,7 @@ paths:
|
|||||||
/apps/{app}/calls:
|
/apps/{app}/calls:
|
||||||
get:
|
get:
|
||||||
summary: Get app-bound calls.
|
summary: Get app-bound calls.
|
||||||
description: Get app-bound calls can filter to route-bound calls.
|
description: Get app-bound calls can filter to route-bound calls, results returned in created_at, descending order (newest first).
|
||||||
tags:
|
tags:
|
||||||
- Call
|
- Call
|
||||||
parameters:
|
parameters:
|
||||||
@@ -433,11 +459,31 @@ paths:
|
|||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
in: path
|
in: path
|
||||||
- name: route
|
- name: path
|
||||||
description: App route.
|
description: Route path to match, exact.
|
||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
in: query
|
in: query
|
||||||
|
- name: cursor
|
||||||
|
description: Cursor from previous response.next_cursor to begin results after, if any.
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
in: query
|
||||||
|
- name: per_page
|
||||||
|
description: Number of results to return, defaults to 30. Max of 100.
|
||||||
|
required: false
|
||||||
|
type: int
|
||||||
|
in: query
|
||||||
|
- name: from_time
|
||||||
|
description: Unix timestamp in seconds, of call.created_at to begin the results at, default 0.
|
||||||
|
required: false
|
||||||
|
type: int
|
||||||
|
in: query
|
||||||
|
- name: to_time
|
||||||
|
description: Unix timestamp in seconds, of call.created_at to end the results at, defaults to latest.
|
||||||
|
required: false
|
||||||
|
type: int
|
||||||
|
in: query
|
||||||
responses:
|
responses:
|
||||||
200:
|
200:
|
||||||
description: Calls found
|
description: Calls found
|
||||||
@@ -524,6 +570,10 @@ definitions:
|
|||||||
required:
|
required:
|
||||||
- routes
|
- routes
|
||||||
properties:
|
properties:
|
||||||
|
next_cursor:
|
||||||
|
type: string
|
||||||
|
description: cursor to send with subsequent request to receive the next page, if non-empty
|
||||||
|
readOnly: true
|
||||||
routes:
|
routes:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
@@ -548,6 +598,10 @@ definitions:
|
|||||||
required:
|
required:
|
||||||
- apps
|
- apps
|
||||||
properties:
|
properties:
|
||||||
|
next_cursor:
|
||||||
|
type: string
|
||||||
|
description: cursor to send with subsequent request to receive the next page, if non-empty
|
||||||
|
readOnly: true
|
||||||
apps:
|
apps:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
@@ -570,6 +624,10 @@ definitions:
|
|||||||
required:
|
required:
|
||||||
- calls
|
- calls
|
||||||
properties:
|
properties:
|
||||||
|
next_cursor:
|
||||||
|
type: string
|
||||||
|
description: cursor to send with subsequent request to receive the next page, if non-empty
|
||||||
|
readOnly: true
|
||||||
calls:
|
calls:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
|
|||||||
@@ -26,24 +26,6 @@ func TestCalls(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("list-calls-for-missing-route", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
s := SetupDefaultSuite()
|
|
||||||
CreateApp(t, s.Context, s.Client, s.AppName, map[string]string{})
|
|
||||||
|
|
||||||
cfg := &call.GetAppsAppCallsParams{
|
|
||||||
App: s.AppName,
|
|
||||||
Route: &s.RoutePath,
|
|
||||||
Context: s.Context,
|
|
||||||
}
|
|
||||||
_, err := s.Client.Call.GetAppsAppCalls(cfg)
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("Must fail with missing route error, but got %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
DeleteApp(t, s.Context, s.Client, s.AppName)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("get-dummy-call", func(t *testing.T) {
|
t.Run("get-dummy-call", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
s := SetupDefaultSuite()
|
s := SetupDefaultSuite()
|
||||||
|
|||||||
Reference in New Issue
Block a user