mirror of
https://github.com/fnproject/fn.git
synced 2022-10-28 21:29:17 +03:00
* so it begins * add clarification to /dequeue, change response to list to future proof * Specify that runner endpoints are also under /v1 * Add a flag to choose operation mode (node type). This is specified using the `FN_NODE_TYPE` environment variable. The default is the existing behaviour, where the server supports all operations (full API plus asynchronous and synchronous runners). The additional modes are: * API - the full API is available, but no functions are executed by the node. Async calls are placed into a message queue, and synchronous calls are not supported (invoking them results in an API error). * Runner - only the invocation/route API is present. Asynchronous and synchronous invocation requests are supported, but asynchronous requests are placed onto the message queue, so might be handled by another runner. * Add agent type and checks on Submit * Sketch of a factored out data access abstraction for api/runner agents * Fix tests, adding node/agent types to constructors * Add tests for full, API, and runner server modes. * Added atomic UpdateCall to datastore * adds in server side endpoints * Made ServerNodeType public because tests use it * Made ServerNodeType public because tests use it * fix test build * add hybrid runner client pretty simple go api client that covers surface area needed for hybrid, returning structs from models that the agent can use directly. not exactly sure where to put this, so put it in `/clients/hybrid` but maybe we should make `/api/runner/client` or something and shove it in there. want to get integration tests set up and use the real endpoints next and then wrap this up in the DataAccessLayer stuff. * gracefully handles errors from fn * handles backoff & retry on 500s * will add to existing spans for debuggo action * minor fixes * meh
319 lines
10 KiB
Go
319 lines
10 KiB
Go
package server
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/fnproject/fn/api/agent"
|
|
"github.com/fnproject/fn/api/datastore"
|
|
"github.com/fnproject/fn/api/logs"
|
|
"github.com/fnproject/fn/api/models"
|
|
"github.com/fnproject/fn/api/mqs"
|
|
)
|
|
|
|
func testRunner(t *testing.T, args ...interface{}) (agent.Agent, context.CancelFunc) {
|
|
ds := datastore.NewMock()
|
|
var mq models.MessageQueue = &mqs.Mock{}
|
|
for _, a := range args {
|
|
switch arg := a.(type) {
|
|
case models.Datastore:
|
|
ds = arg
|
|
case models.MessageQueue:
|
|
mq = arg
|
|
}
|
|
}
|
|
r := agent.New(ds, ds, mq, agent.AgentTypeFull)
|
|
return r, func() { r.Close() }
|
|
}
|
|
|
|
func TestRouteRunnerGet(t *testing.T) {
|
|
buf := setLogBuffer()
|
|
ds := datastore.NewMockInit(
|
|
[]*models.App{
|
|
{Name: "myapp", Config: models.Config{}},
|
|
}, nil, nil,
|
|
)
|
|
|
|
rnr, cancel := testRunner(t, ds)
|
|
defer cancel()
|
|
logDB := logs.NewMock()
|
|
srv := testServer(ds, &mqs.Mock{}, logDB, rnr, ServerTypeFull)
|
|
|
|
for i, test := range []struct {
|
|
path string
|
|
body string
|
|
expectedCode int
|
|
expectedError error
|
|
}{
|
|
{"/route", "", http.StatusNotFound, nil},
|
|
{"/r/app/route", "", http.StatusNotFound, models.ErrAppsNotFound},
|
|
{"/r/myapp/route", "", http.StatusNotFound, models.ErrRoutesNotFound},
|
|
} {
|
|
_, 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`, but got `%s`",
|
|
i, test.expectedError.Error(), resp.Error.Message)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRouteRunnerPost(t *testing.T) {
|
|
buf := setLogBuffer()
|
|
|
|
ds := datastore.NewMockInit(
|
|
[]*models.App{
|
|
{Name: "myapp", Config: models.Config{}},
|
|
}, nil, nil,
|
|
)
|
|
|
|
rnr, cancel := testRunner(t, ds)
|
|
defer cancel()
|
|
|
|
fnl := logs.NewMock()
|
|
srv := testServer(ds, &mqs.Mock{}, fnl, rnr, ServerTypeFull)
|
|
|
|
for i, test := range []struct {
|
|
path string
|
|
body string
|
|
expectedCode int
|
|
expectedError error
|
|
}{
|
|
{"/route", `{ "payload": "" }`, http.StatusNotFound, nil},
|
|
{"/r/app/route", `{ "payload": "" }`, http.StatusNotFound, models.ErrAppsNotFound},
|
|
{"/r/myapp/route", `{ "payload": "" }`, http.StatusNotFound, models.ErrRoutesNotFound},
|
|
} {
|
|
body := bytes.NewBuffer([]byte(test.body))
|
|
_, rec := routerRequest(t, srv.Router, "POST", test.path, body)
|
|
|
|
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)
|
|
respMsg := resp.Error.Message
|
|
expMsg := test.expectedError.Error()
|
|
if respMsg != expMsg && !strings.Contains(respMsg, expMsg) {
|
|
t.Log(buf.String())
|
|
t.Errorf("Test %d: Expected error message to have `%s`",
|
|
i, test.expectedError.Error())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRouteRunnerExecution(t *testing.T) {
|
|
buf := setLogBuffer()
|
|
|
|
ds := datastore.NewMockInit(
|
|
[]*models.App{
|
|
{Name: "myapp", Config: models.Config{}},
|
|
},
|
|
[]*models.Route{
|
|
{Path: "/", AppName: "myapp", Image: "fnproject/hello", Type: "sync", Memory: 128, Timeout: 30, IdleTimeout: 30, Headers: map[string][]string{"X-Function": {"Test"}}},
|
|
{Path: "/myroute", AppName: "myapp", Image: "fnproject/hello", Type: "sync", Memory: 128, Timeout: 30, IdleTimeout: 30, Headers: map[string][]string{"X-Function": {"Test"}}},
|
|
{Path: "/myerror", AppName: "myapp", Image: "fnproject/error", Type: "sync", Memory: 128, Timeout: 30, IdleTimeout: 30, Headers: map[string][]string{"X-Function": {"Test"}}},
|
|
{Path: "/mydne", AppName: "myapp", Image: "fnproject/imagethatdoesnotexist", Type: "sync", Memory: 128, Timeout: 30, IdleTimeout: 30},
|
|
{Path: "/mydnehot", AppName: "myapp", Image: "fnproject/imagethatdoesnotexist", Type: "sync", Format: "http", Memory: 128, Timeout: 30, IdleTimeout: 30},
|
|
}, nil,
|
|
)
|
|
|
|
rnr, cancelrnr := testRunner(t, ds)
|
|
defer cancelrnr()
|
|
|
|
fnl := logs.NewMock()
|
|
|
|
srv := testServer(ds, &mqs.Mock{}, fnl, rnr, ServerTypeFull)
|
|
|
|
for i, test := range []struct {
|
|
path string
|
|
body string
|
|
method string
|
|
expectedCode int
|
|
expectedHeaders map[string][]string
|
|
}{
|
|
{"/r/myapp/", ``, "GET", http.StatusOK, map[string][]string{"X-Function": {"Test"}}},
|
|
{"/r/myapp/myroute", ``, "GET", http.StatusOK, map[string][]string{"X-Function": {"Test"}}},
|
|
{"/r/myapp/myerror", ``, "GET", http.StatusBadGateway, map[string][]string{"X-Function": {"Test"}}},
|
|
{"/r/myapp/mydne", ``, "GET", http.StatusNotFound, nil},
|
|
{"/r/myapp/mydnehot", ``, "GET", http.StatusNotFound, nil},
|
|
|
|
// Added same tests again to check if time is reduced by the auth cache
|
|
{"/r/myapp/", ``, "GET", http.StatusOK, map[string][]string{"X-Function": {"Test"}}},
|
|
{"/r/myapp/myroute", ``, "GET", http.StatusOK, map[string][]string{"X-Function": {"Test"}}},
|
|
{"/r/myapp/myerror", ``, "GET", http.StatusBadGateway, map[string][]string{"X-Function": {"Test"}}},
|
|
} {
|
|
body := strings.NewReader(test.body)
|
|
_, rec := routerRequest(t, srv.Router, test.method, test.path, body)
|
|
|
|
if rec.Code != test.expectedCode {
|
|
t.Log(buf.String())
|
|
bod, _ := ioutil.ReadAll(rec.Body)
|
|
t.Errorf("Test %d: Expected status code to be %d but was %d. body: %s",
|
|
i, test.expectedCode, rec.Code, string(bod))
|
|
}
|
|
|
|
if test.expectedHeaders == nil {
|
|
continue
|
|
}
|
|
for name, header := range test.expectedHeaders {
|
|
if header[0] != rec.Header().Get(name) {
|
|
t.Log(buf.String())
|
|
t.Errorf("Test %d: Expected header `%s` to be %s but was %s",
|
|
i, name, header[0], rec.Header().Get(name))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// implement models.MQ and models.APIError
|
|
type errorMQ struct {
|
|
error
|
|
code int
|
|
}
|
|
|
|
func (mock *errorMQ) Push(context.Context, *models.Call) (*models.Call, error) { return nil, mock }
|
|
func (mock *errorMQ) Reserve(context.Context) (*models.Call, error) { return nil, mock }
|
|
func (mock *errorMQ) Delete(context.Context, *models.Call) error { return mock }
|
|
func (mock *errorMQ) Code() int { return mock.code }
|
|
|
|
func TestFailedEnqueue(t *testing.T) {
|
|
buf := setLogBuffer()
|
|
ds := datastore.NewMockInit(
|
|
[]*models.App{
|
|
{Name: "myapp", Config: models.Config{}},
|
|
},
|
|
[]*models.Route{
|
|
{Path: "/dummy", AppName: "myapp", Image: "dummy/dummy", Type: "async", Memory: 128, Timeout: 30, IdleTimeout: 30},
|
|
}, nil,
|
|
)
|
|
err := errors.New("Unable to push task to queue")
|
|
mq := &errorMQ{err, http.StatusInternalServerError}
|
|
fnl := logs.NewMock()
|
|
rnr, cancelrnr := testRunner(t, ds, mq)
|
|
defer cancelrnr()
|
|
|
|
srv := testServer(ds, mq, fnl, rnr, ServerTypeFull)
|
|
for i, test := range []struct {
|
|
path string
|
|
body string
|
|
method string
|
|
expectedCode int
|
|
expectedHeaders map[string][]string
|
|
}{
|
|
{"/r/myapp/dummy", ``, "POST", http.StatusInternalServerError, nil},
|
|
} {
|
|
body := strings.NewReader(test.body)
|
|
_, rec := routerRequest(t, srv.Router, test.method, test.path, body)
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRouteRunnerTimeout(t *testing.T) {
|
|
buf := setLogBuffer()
|
|
|
|
models.RouteMaxMemory = uint64(1024 * 1024 * 1024) // 1024 TB
|
|
hugeMem := uint64(models.RouteMaxMemory - 1)
|
|
|
|
ds := datastore.NewMockInit(
|
|
[]*models.App{
|
|
{Name: "myapp", Config: models.Config{}},
|
|
},
|
|
[]*models.Route{
|
|
{Path: "/pull", AppName: "myapp", Image: "fnproject/sleeper", Type: "sync", Memory: 128, Timeout: 30, IdleTimeout: 30},
|
|
{Path: "/sleeper", AppName: "myapp", Image: "fnproject/sleeper", Type: "sync", Memory: 128, Timeout: 2, IdleTimeout: 30},
|
|
{Path: "/waitmemory", AppName: "myapp", Image: "fnproject/sleeper", Type: "sync", Memory: hugeMem, Timeout: 1, IdleTimeout: 30},
|
|
}, nil,
|
|
)
|
|
|
|
rnr, cancelrnr := testRunner(t, ds)
|
|
defer cancelrnr()
|
|
|
|
fnl := logs.NewMock()
|
|
srv := testServer(ds, &mqs.Mock{}, fnl, rnr, ServerTypeFull)
|
|
|
|
for i, test := range []struct {
|
|
path string
|
|
body string
|
|
method string
|
|
expectedCode int
|
|
expectedHeaders map[string][]string
|
|
}{
|
|
// first request with large timeout, we let the docker pull go through...
|
|
{"/r/myapp/pull", `{"sleep": 0}`, "POST", http.StatusOK, nil},
|
|
{"/r/myapp/sleeper", `{"sleep": 0}`, "POST", http.StatusOK, nil},
|
|
{"/r/myapp/sleeper", `{"sleep": 4}`, "POST", http.StatusGatewayTimeout, nil},
|
|
{"/r/myapp/waitmemory", `{"sleep": 0}`, "POST", http.StatusServiceUnavailable, map[string][]string{"Retry-After": {"15"}}},
|
|
} {
|
|
body := strings.NewReader(test.body)
|
|
_, rec := routerRequest(t, srv.Router, test.method, test.path, body)
|
|
|
|
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.expectedHeaders == nil {
|
|
continue
|
|
}
|
|
for name, header := range test.expectedHeaders {
|
|
if header[0] != rec.Header().Get(name) {
|
|
t.Log(buf.String())
|
|
t.Errorf("Test %d: Expected header `%s` to be %s but was %s",
|
|
i, name, header[0], rec.Header().Get(name))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//func TestMatchRoute(t *testing.T) {
|
|
//buf := setLogBuffer()
|
|
//for i, test := range []struct {
|
|
//baseRoute string
|
|
//route string
|
|
//expectedParams []Param
|
|
//}{
|
|
//{"/myroute/", `/myroute/`, nil},
|
|
//{"/myroute/:mybigparam", `/myroute/1`, []Param{{"mybigparam", "1"}}},
|
|
//{"/:param/*test", `/1/2`, []Param{{"param", "1"}, {"test", "/2"}}},
|
|
//} {
|
|
//if params, match := matchRoute(test.baseRoute, test.route); match {
|
|
//if test.expectedParams != nil {
|
|
//for j, param := range test.expectedParams {
|
|
//if params[j].Key != param.Key || params[j].Value != param.Value {
|
|
//t.Log(buf.String())
|
|
//t.Errorf("Test %d: expected param %d, key = %s, value = %s", i, j, param.Key, param.Value)
|
|
//}
|
|
//}
|
|
//}
|
|
//} else {
|
|
//t.Log(buf.String())
|
|
//t.Errorf("Test %d: %s should match %s", i, test.route, test.baseRoute)
|
|
//}
|
|
//}
|
|
//}
|