refactor runner

This commit is contained in:
Pedro Nasser
2016-08-21 19:40:08 -03:00
parent 3b5ff970df
commit 8b0d0f1e13
11 changed files with 114 additions and 88 deletions

View File

@@ -1,8 +1,8 @@
package runner package runner
import ( import (
"bytes"
"fmt" "fmt"
"io"
"time" "time"
"golang.org/x/net/context" "golang.org/x/net/context"
@@ -24,24 +24,15 @@ type Config struct {
Timeout time.Duration Timeout time.Duration
RequestURL string RequestURL string
AppName string AppName string
Stdout io.Writer
Stderr io.Writer
} }
type Runner struct { type Runner struct {
cfg *Config driver drivers.Driver
status string
out bytes.Buffer
err bytes.Buffer
} }
func New(cfg *Config) *Runner { func New() (*Runner, error) {
return &Runner{
cfg: cfg,
}
}
func (r *Runner) Run() error {
var err error
// TODO: Is this really required for Titan's driver? // TODO: Is this really required for Titan's driver?
// Can we remove it? // Can we remove it?
env := common.NewEnvironment(func(e *common.Environment) {}) env := common.NewEnvironment(func(e *common.Environment) {})
@@ -49,36 +40,28 @@ func (r *Runner) Run() error {
// TODO: Create a drivers.New(runnerConfig) in Titan // TODO: Create a drivers.New(runnerConfig) in Titan
driver, err := selectDriver("docker", env, &driverscommon.Config{}) driver, err := selectDriver("docker", env, &driverscommon.Config{})
if err != nil { if err != nil {
return err return nil, err
} }
return &Runner{
driver: driver,
}, nil
}
func (r *Runner) Run(ctx context.Context, cfg *Config) (drivers.RunResult, error) {
var err error
ctask := &containerTask{ ctask := &containerTask{
cfg: r.cfg, cfg: cfg,
auth: &agent.ConfigAuth{}, auth: &agent.ConfigAuth{},
stdout: &r.out,
stderr: &r.err,
} }
result, err := driver.Run(r.cfg.Ctx, ctask) result, err := r.driver.Run(ctx, ctask)
if err != nil { if err != nil {
return err return nil, err
} }
r.status = result.Status() return result, nil
return nil
}
func (r *Runner) ReadOut() []byte {
return r.out.Bytes()
}
func (r Runner) ReadErr() []byte {
return r.err.Bytes()
}
func (r Runner) Status() string {
return r.status
} }
func selectDriver(driver string, env *common.Environment, conf *driverscommon.Config) (drivers.Driver, error) { func selectDriver(driver string, env *common.Environment, conf *driverscommon.Config) (drivers.Driver, error) {

View File

@@ -11,6 +11,13 @@ import (
) )
func TestRunnerHello(t *testing.T) { func TestRunnerHello(t *testing.T) {
runner, err := New()
if err != nil {
t.Fatalf("Test error during New() - %s", err)
}
ctx := context.Background()
for i, test := range []struct { for i, test := range []struct {
route *models.Route route *models.Route
payload string payload string
@@ -21,33 +28,43 @@ func TestRunnerHello(t *testing.T) {
{&models.Route{Image: "iron/hello"}, ``, "success", "Hello World!", ""}, {&models.Route{Image: "iron/hello"}, ``, "success", "Hello World!", ""},
{&models.Route{Image: "iron/hello"}, `{"name": "test"}`, "success", "Hello test!", ""}, {&models.Route{Image: "iron/hello"}, `{"name": "test"}`, "success", "Hello test!", ""},
} { } {
runner := New(&Config{ var stdout, stderr bytes.Buffer
cfg := &Config{
ID: fmt.Sprintf("task-hello-%d-%d", i, time.Now().Unix()), ID: fmt.Sprintf("task-hello-%d-%d", i, time.Now().Unix()),
Ctx: context.Background(),
Route: test.route, Route: test.route,
Timeout: 5 * time.Second, Timeout: 5 * time.Second,
Payload: test.payload, Payload: test.payload,
}) Stdout: &stdout,
Stderr: &stderr,
}
if err := runner.Run(); err != nil { result, err := runner.Run(ctx, cfg)
if err != nil {
t.Fatalf("Test %d: error during Run() - %s", i, err) t.Fatalf("Test %d: error during Run() - %s", i, err)
} }
if test.expectedStatus != runner.Status() { if test.expectedStatus != result.Status() {
t.Fatalf("Test %d: expected result status to be `%s` but it was `%s`", i, test.expectedStatus, runner.Status()) t.Fatalf("Test %d: expected result status to be `%s` but it was `%s`", i, test.expectedStatus, result.Status())
} }
if !bytes.Contains(runner.ReadOut(), []byte(test.expectedOut)) { if !bytes.Contains(stdout.Bytes(), []byte(test.expectedOut)) {
t.Fatalf("Test %d: expected output log to contain `%s` in `%s`", i, test.expectedOut, runner.ReadOut()) t.Fatalf("Test %d: expected output log to contain `%s` in `%s`", i, test.expectedOut, stdout.String())
} }
if !bytes.Contains(runner.ReadErr(), []byte(test.expectedErr)) { if !bytes.Contains(stderr.Bytes(), []byte(test.expectedErr)) {
t.Fatalf("Test %d: expected error log to contain `%s` in `%s`", i, test.expectedErr, runner.ReadErr()) t.Fatalf("Test %d: expected error log to contain `%s` in `%s`", i, test.expectedErr, stderr.String())
} }
} }
} }
func TestRunnerError(t *testing.T) { func TestRunnerError(t *testing.T) {
runner, err := New()
if err != nil {
t.Fatalf("Test error during New() - %s", err)
}
ctx := context.Background()
for i, test := range []struct { for i, test := range []struct {
route *models.Route route *models.Route
payload string payload string
@@ -58,28 +75,31 @@ func TestRunnerError(t *testing.T) {
{&models.Route{Image: "iron/error"}, ``, "error", "", "RuntimeError"}, {&models.Route{Image: "iron/error"}, ``, "error", "", "RuntimeError"},
{&models.Route{Image: "iron/error"}, `{"name": "test"}`, "error", "", "RuntimeError"}, {&models.Route{Image: "iron/error"}, `{"name": "test"}`, "error", "", "RuntimeError"},
} { } {
runner := New(&Config{ var stdout, stderr bytes.Buffer
ID: fmt.Sprintf("task-error-%d-%d", i, time.Now().Unix()), cfg := &Config{
Ctx: context.Background(), ID: fmt.Sprintf("task-err-%d-%d", i, time.Now().Unix()),
Route: test.route, Route: test.route,
Timeout: 5 * time.Second, Timeout: 5 * time.Second,
Payload: test.payload, Payload: test.payload,
}) Stdout: &stdout,
Stderr: &stderr,
}
if err := runner.Run(); err != nil { result, err := runner.Run(ctx, cfg)
if err != nil {
t.Fatalf("Test %d: error during Run() - %s", i, err) t.Fatalf("Test %d: error during Run() - %s", i, err)
} }
if test.expectedStatus != runner.Status() { if test.expectedStatus != result.Status() {
t.Fatalf("Test %d: expected result status to be `%s` but it was `%s`", i, test.expectedStatus, runner.Status()) t.Fatalf("Test %d: expected result status to be `%s` but it was `%s`", i, test.expectedStatus, result.Status())
} }
if !bytes.Contains(runner.ReadOut(), []byte(test.expectedOut)) { if !bytes.Contains(stdout.Bytes(), []byte(test.expectedOut)) {
t.Fatalf("Test %d: expected output log to contain `%s` in `%s`", i, test.expectedOut, runner.ReadOut()) t.Fatalf("Test %d: expected output log to contain `%s` in `%s`", i, test.expectedOut, stdout.String())
} }
if !bytes.Contains(runner.ReadErr(), []byte(test.expectedErr)) { if !bytes.Contains(stderr.Bytes(), []byte(test.expectedErr)) {
t.Fatalf("Test %d: expected error log to contain `%s` in `%s`", i, test.expectedErr, runner.ReadErr()) t.Fatalf("Test %d: expected error log to contain `%s` in `%s`", i, test.expectedErr, stderr.String())
} }
} }
} }

View File

@@ -8,10 +8,8 @@ import (
) )
type containerTask struct { type containerTask struct {
auth tasker.Auther auth tasker.Auther
stdout io.Writer cfg *Config
stderr io.Writer
cfg *Config
} }
func (t *containerTask) Command() string { return "" } func (t *containerTask) Command() string { return "" }
@@ -34,7 +32,7 @@ func (t *containerTask) Id() string { return t.cfg.ID }
func (t *containerTask) Group() string { return "" } func (t *containerTask) Group() string { return "" }
func (t *containerTask) Image() string { return t.cfg.Route.Image } func (t *containerTask) Image() string { return t.cfg.Route.Image }
func (t *containerTask) Timeout() uint { return uint(t.cfg.Timeout.Seconds()) } func (t *containerTask) Timeout() uint { return uint(t.cfg.Timeout.Seconds()) }
func (t *containerTask) Logger() (stdout, stderr io.Writer) { return t.stdout, t.stderr } func (t *containerTask) Logger() (stdout, stderr io.Writer) { return t.cfg.Stdout, t.cfg.Stderr }
func (t *containerTask) Volumes() [][2]string { return [][2]string{} } func (t *containerTask) Volumes() [][2]string { return [][2]string{} }
func (t *containerTask) WorkDir() string { return "" } func (t *containerTask) WorkDir() string { return "" }

View File

@@ -11,7 +11,7 @@ import (
) )
func TestAppCreate(t *testing.T) { func TestAppCreate(t *testing.T) {
New(&datastore.Mock{}, &models.Config{}) New(&models.Config{}, &datastore.Mock{}, testRunner(t))
router := testRouter() router := testRouter()
for i, test := range []struct { for i, test := range []struct {
@@ -52,7 +52,7 @@ func TestAppCreate(t *testing.T) {
} }
func TestAppDelete(t *testing.T) { func TestAppDelete(t *testing.T) {
New(&datastore.Mock{}, &models.Config{}) New(&models.Config{}, &datastore.Mock{}, testRunner(t))
router := testRouter() router := testRouter()
for i, test := range []struct { for i, test := range []struct {
@@ -83,7 +83,7 @@ func TestAppDelete(t *testing.T) {
} }
func TestAppList(t *testing.T) { func TestAppList(t *testing.T) {
New(&datastore.Mock{}, &models.Config{}) New(&models.Config{}, &datastore.Mock{}, testRunner(t))
router := testRouter() router := testRouter()
for i, test := range []struct { for i, test := range []struct {
@@ -113,7 +113,7 @@ func TestAppList(t *testing.T) {
} }
func TestAppGet(t *testing.T) { func TestAppGet(t *testing.T) {
New(&datastore.Mock{}, &models.Config{}) New(&models.Config{}, &datastore.Mock{}, testRunner(t))
router := testRouter() router := testRouter()
for i, test := range []struct { for i, test := range []struct {
@@ -143,7 +143,7 @@ func TestAppGet(t *testing.T) {
} }
func TestAppUpdate(t *testing.T) { func TestAppUpdate(t *testing.T) {
New(&datastore.Mock{}, &models.Config{}) New(&models.Config{}, &datastore.Mock{}, testRunner(t))
router := testRouter() router := testRouter()
for i, test := range []struct { for i, test := range []struct {

View File

@@ -12,6 +12,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/iron-io/functions/api/models" "github.com/iron-io/functions/api/models"
"github.com/iron-io/functions/api/runner"
titancommon "github.com/iron-io/titan/common" titancommon "github.com/iron-io/titan/common"
) )
@@ -47,6 +48,14 @@ func testRouter() *gin.Engine {
return r return r
} }
func testRunner(t *testing.T) *runner.Runner {
r, err := runner.New()
if err != nil {
t.Fatal("Test: failed to create new runner")
}
return r
}
func routerRequest(t *testing.T, router *gin.Engine, method, path string, body io.Reader) (*http.Request, *httptest.ResponseRecorder) { func routerRequest(t *testing.T, router *gin.Engine, method, path string, body io.Reader) (*http.Request, *httptest.ResponseRecorder) {
req, err := http.NewRequest(method, "http://localhost:8080"+path, body) req, err := http.NewRequest(method, "http://localhost:8080"+path, body)
if err != nil { if err != nil {

View File

@@ -11,7 +11,7 @@ import (
) )
func TestRouteCreate(t *testing.T) { func TestRouteCreate(t *testing.T) {
New(&datastore.Mock{}, &models.Config{}) New(&models.Config{}, &datastore.Mock{}, testRunner(t))
router := testRouter() router := testRouter()
for i, test := range []struct { for i, test := range []struct {
@@ -52,7 +52,7 @@ func TestRouteCreate(t *testing.T) {
} }
func TestRouteDelete(t *testing.T) { func TestRouteDelete(t *testing.T) {
New(&datastore.Mock{}, &models.Config{}) New(&models.Config{}, &datastore.Mock{}, testRunner(t))
router := testRouter() router := testRouter()
for i, test := range []struct { for i, test := range []struct {
@@ -83,7 +83,7 @@ func TestRouteDelete(t *testing.T) {
} }
func TestRouteList(t *testing.T) { func TestRouteList(t *testing.T) {
New(&datastore.Mock{}, &models.Config{}) New(&models.Config{}, &datastore.Mock{}, testRunner(t))
router := testRouter() router := testRouter()
for i, test := range []struct { for i, test := range []struct {
@@ -113,7 +113,7 @@ func TestRouteList(t *testing.T) {
} }
func TestRouteGet(t *testing.T) { func TestRouteGet(t *testing.T) {
New(&datastore.Mock{}, &models.Config{}) New(&models.Config{}, &datastore.Mock{}, testRunner(t))
router := testRouter() router := testRouter()
for i, test := range []struct { for i, test := range []struct {
@@ -143,7 +143,7 @@ func TestRouteGet(t *testing.T) {
} }
func TestRouteUpdate(t *testing.T) { func TestRouteUpdate(t *testing.T) {
New(&datastore.Mock{}, &models.Config{}) New(&models.Config{}, &datastore.Mock{}, testRunner(t))
router := testRouter() router := testRouter()
for i, test := range []struct { for i, test := range []struct {

View File

@@ -1,6 +1,7 @@
package server package server
import ( import (
"bytes"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
@@ -81,7 +82,6 @@ func handleRunner(c *gin.Context) {
c.JSON(http.StatusBadRequest, simpleError(models.ErrAppsNotFound)) c.JSON(http.StatusBadRequest, simpleError(models.ErrAppsNotFound))
return return
} }
route := c.Param("route") route := c.Param("route")
if route == "" { if route == "" {
route = c.Request.URL.Path route = c.Request.URL.Path
@@ -109,17 +109,19 @@ func handleRunner(c *gin.Context) {
log.WithField("routes", routes).Debug("Got routes from datastore") log.WithField("routes", routes).Debug("Got routes from datastore")
for _, el := range routes { for _, el := range routes {
if el.Path == route { if el.Path == route {
run := runner.New(&runner.Config{ var stdout, stderr bytes.Buffer
Ctx: c, cfg := &runner.Config{
Route: el, Route: el,
Payload: string(payload), Payload: string(payload),
Timeout: 30 * time.Second, Timeout: 30 * time.Second,
ID: reqID, ID: reqID,
RequestURL: c.Request.URL.String(), RequestURL: c.Request.URL.String(),
AppName: appName, AppName: appName,
}) Stdout: &stdout,
Stderr: &stderr,
}
if err := run.Run(); err != nil { if result, err := Api.Runner.Run(c, cfg); err != nil {
log.WithError(err).Error(models.ErrRunnerRunRoute) log.WithError(err).Error(models.ErrRunnerRunRoute)
c.JSON(http.StatusInternalServerError, simpleError(models.ErrRunnerRunRoute)) c.JSON(http.StatusInternalServerError, simpleError(models.ErrRunnerRunRoute))
} else { } else {
@@ -127,10 +129,11 @@ func handleRunner(c *gin.Context) {
c.Header(k, v[0]) c.Header(k, v[0])
} }
if run.Status() == "success" { if result.Status() == "success" {
c.Data(http.StatusOK, "", run.ReadOut()) c.Data(http.StatusOK, "", stdout.Bytes())
} else { } else {
c.Data(http.StatusInternalServerError, "", run.ReadErr()) log.WithFields(logrus.Fields{"app": appName, "route": el, "req_id": reqID}).Debug(stderr.String())
c.AbortWithStatus(http.StatusInternalServerError)
} }
} }
return return

View File

@@ -12,7 +12,7 @@ import (
) )
func TestRouteRunnerGet(t *testing.T) { func TestRouteRunnerGet(t *testing.T) {
New(&datastore.Mock{}, &models.Config{}) New(&models.Config{}, &datastore.Mock{}, testRunner(t))
router := testRouter() router := testRouter()
for i, test := range []struct { for i, test := range []struct {
@@ -45,7 +45,7 @@ func TestRouteRunnerGet(t *testing.T) {
} }
func TestRouteRunnerPost(t *testing.T) { func TestRouteRunnerPost(t *testing.T) {
New(&datastore.Mock{}, &models.Config{}) New(&models.Config{}, &datastore.Mock{}, testRunner(t))
router := testRouter() router := testRouter()
for i, test := range []struct { for i, test := range []struct {
@@ -81,12 +81,12 @@ func TestRouteRunnerPost(t *testing.T) {
} }
func TestRouteRunnerExecution(t *testing.T) { func TestRouteRunnerExecution(t *testing.T) {
New(&datastore.Mock{ New(&models.Config{}, &datastore.Mock{
FakeRoutes: []*models.Route{ FakeRoutes: []*models.Route{
{Path: "/myroute", Image: "iron/hello", Headers: map[string][]string{"X-Function": []string{"Test"}}}, {Path: "/myroute", Image: "iron/hello", Headers: map[string][]string{"X-Function": []string{"Test"}}},
{Path: "/myerror", Image: "iron/error", Headers: map[string][]string{"X-Function": []string{"Test"}}}, {Path: "/myerror", Image: "iron/error", Headers: map[string][]string{"X-Function": []string{"Test"}}},
}, },
}, &models.Config{}) }, testRunner(t))
router := testRouter() router := testRouter()
for i, test := range []struct { for i, test := range []struct {
@@ -97,6 +97,10 @@ func TestRouteRunnerExecution(t *testing.T) {
}{ }{
{"/r/myapp/myroute", ``, http.StatusOK, map[string][]string{"X-Function": []string{"Test"}}}, {"/r/myapp/myroute", ``, http.StatusOK, map[string][]string{"X-Function": []string{"Test"}}},
{"/r/myapp/myerror", ``, http.StatusInternalServerError, map[string][]string{"X-Function": []string{"Test"}}}, {"/r/myapp/myerror", ``, http.StatusInternalServerError, map[string][]string{"X-Function": []string{"Test"}}},
// Added same tests again to check if time is reduced by the auth cache
{"/r/myapp/myroute", ``, http.StatusOK, map[string][]string{"X-Function": []string{"Test"}}},
{"/r/myapp/myerror", ``, http.StatusInternalServerError, map[string][]string{"X-Function": []string{"Test"}}},
} { } {
body := bytes.NewBuffer([]byte(test.body)) body := bytes.NewBuffer([]byte(test.body))
_, rec := routerRequest(t, router, "GET", test.path, body) _, rec := routerRequest(t, router, "GET", test.path, body)

View File

@@ -9,6 +9,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/iron-io/functions/api/ifaces" "github.com/iron-io/functions/api/ifaces"
"github.com/iron-io/functions/api/models" "github.com/iron-io/functions/api/models"
"github.com/iron-io/functions/api/runner"
titancommon "github.com/iron-io/titan/common" titancommon "github.com/iron-io/titan/common"
) )
@@ -17,6 +18,7 @@ import (
var Api *Server var Api *Server
type Server struct { type Server struct {
Runner *runner.Runner
Router *gin.Engine Router *gin.Engine
Config *models.Config Config *models.Config
Datastore models.Datastore Datastore models.Datastore
@@ -24,11 +26,12 @@ type Server struct {
SpecialHandlers []ifaces.SpecialHandler SpecialHandlers []ifaces.SpecialHandler
} }
func New(ds models.Datastore, config *models.Config) *Server { func New(c *models.Config, ds models.Datastore, r *runner.Runner) *Server {
Api = &Server{ Api = &Server{
Router: gin.Default(), Router: gin.Default(),
Config: config, Config: c,
Datastore: ds, Datastore: ds,
Runner: r,
} }
return Api return Api
} }

View File

@@ -26,7 +26,7 @@ func TestFullStack(t *testing.T) {
ds, close := prepareBolt(t) ds, close := prepareBolt(t)
defer close() defer close()
New(ds, &models.Config{}) New(&models.Config{}, ds, testRunner(t))
router := testRouter() router := testRouter()
for i, test := range []struct { for i, test := range []struct {

View File

@@ -5,6 +5,7 @@ import (
"github.com/iron-io/functions/api/config" "github.com/iron-io/functions/api/config"
"github.com/iron-io/functions/api/datastore" "github.com/iron-io/functions/api/datastore"
"github.com/iron-io/functions/api/models" "github.com/iron-io/functions/api/models"
"github.com/iron-io/functions/api/runner"
"github.com/iron-io/functions/api/server" "github.com/iron-io/functions/api/server"
"github.com/spf13/viper" "github.com/spf13/viper"
"golang.org/x/net/context" "golang.org/x/net/context"
@@ -26,6 +27,11 @@ func main() {
log.WithError(err).Fatalln("Invalid DB url.") log.WithError(err).Fatalln("Invalid DB url.")
} }
srv := server.New(ds, c) runner, err := runner.New()
if err != nil {
log.WithError(err).Fatalln("Failed to create a runner")
}
srv := server.New(c, ds, runner)
srv.Run(ctx) srv.Run(ctx)
} }