mirror of
https://github.com/fnproject/fn.git
synced 2022-10-28 21:29:17 +03:00
refactor runner using titan
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
package server
|
package models
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
DatabaseURL string `json:"db"`
|
DatabaseURL string `json:"db"`
|
||||||
|
API string `json:"api"`
|
||||||
Logging struct {
|
Logging struct {
|
||||||
To string `json:"to"`
|
To string `json:"to"`
|
||||||
Level string `json:"level"`
|
Level string `json:"level"`
|
||||||
@@ -10,6 +11,5 @@ type Config struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) Validate() error {
|
func (c *Config) Validate() error {
|
||||||
// TODO:
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -17,5 +17,6 @@ func ApplyAppFilter(app *App, filter *AppFilter) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ApplyRouteFilter(route *Route, filter *RouteFilter) bool {
|
func ApplyRouteFilter(route *Route, filter *RouteFilter) bool {
|
||||||
return true
|
return (filter.Path != "" && route.Path == filter.Path) &&
|
||||||
|
(filter.AppName != "" && route.AppName == filter.AppName)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,5 +54,6 @@ func (r *Route) Validate() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type RouteFilter struct {
|
type RouteFilter struct {
|
||||||
|
Path string
|
||||||
AppName string
|
AppName string
|
||||||
}
|
}
|
||||||
|
|||||||
13
api/models/runner.go
Normal file
13
api/models/runner.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrRunnerRouteNotFound = errors.New("Route not found on that application")
|
||||||
|
ErrRunnerInvalidPayload = errors.New("Invalid payload")
|
||||||
|
ErrRunnerRunRoute = errors.New("Couldn't run this route in the job server")
|
||||||
|
ErrRunnerAPICantConnect = errors.New("Couldn`t connect to the job server API")
|
||||||
|
ErrRunnerAPICreateJob = errors.New("Could not create a job in job server")
|
||||||
|
ErrRunnerInvalidResponse = errors.New("Invalid response")
|
||||||
|
ErrRunnerTimeout = errors.New("Timed out")
|
||||||
|
)
|
||||||
@@ -1,208 +1,136 @@
|
|||||||
package runner
|
package runner
|
||||||
|
|
||||||
import (
|
import "github.com/iron-io/functions/api/models"
|
||||||
"bufio"
|
import "time"
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
|
||||||
"math/rand"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
type RouteRunner struct {
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/iron-io/functions/api/models"
|
|
||||||
)
|
|
||||||
|
|
||||||
type RunningApp struct {
|
|
||||||
Route *models.Route
|
Route *models.Route
|
||||||
Port int
|
Endpoint string
|
||||||
ContainerName string
|
Payload string
|
||||||
}
|
Timeout time.Duration
|
||||||
|
|
||||||
var (
|
|
||||||
ErrRunnerRouteNotFound = errors.New("Route not found on that application")
|
|
||||||
)
|
|
||||||
|
|
||||||
var runningImages map[string]*RunningApp
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
runningImages = make(map[string]*RunningApp)
|
|
||||||
fmt.Println("ENV:", os.Environ())
|
|
||||||
}
|
|
||||||
|
|
||||||
func Run(c *gin.Context) error {
|
|
||||||
log := c.MustGet("log").(logrus.FieldLogger)
|
|
||||||
store := c.MustGet("store").(models.Datastore)
|
|
||||||
|
|
||||||
appName := c.Param("app")
|
|
||||||
|
|
||||||
if appName == "" {
|
|
||||||
host := strings.Split(c.Request.Host, ":")[0]
|
|
||||||
appName = strings.Split(host, ".")[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
filter := &models.RouteFilter{
|
|
||||||
AppName: appName,
|
|
||||||
}
|
|
||||||
|
|
||||||
routes, err := store.GetRoutes(filter)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
route := c.Param("route")
|
|
||||||
|
|
||||||
log.WithFields(logrus.Fields{"app": appName}).Debug("Running app")
|
|
||||||
|
|
||||||
for _, el := range routes {
|
|
||||||
if el.Path == route {
|
|
||||||
err = checkAndPull(el.Image)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if el.Type == "app" {
|
|
||||||
return DockerHost(el, c)
|
|
||||||
} else {
|
|
||||||
return DockerRun(el, c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ErrRunnerRouteNotFound
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: use Docker utils from docker-job for this and a few others in here
|
// TODO: use Docker utils from docker-job for this and a few others in here
|
||||||
func DockerRun(route *models.Route, c *gin.Context) error {
|
// func DockerRun(route *models.Route, c *gin.Context) error {
|
||||||
image := route.Image
|
// image := route.Image
|
||||||
payload, err := ioutil.ReadAll(c.Request.Body)
|
// payload := c.Value("payload").(string)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// log.WithField("payload", "---"+string(payload)+"---").Infoln("incoming request")
|
|
||||||
// log.WithField("image", image).Infoln("About to run using this image")
|
|
||||||
|
|
||||||
for k, v := range route.Headers {
|
// for k, v := range route.Headers {
|
||||||
c.Header(k, v[0])
|
// c.Header(k, v[0])
|
||||||
}
|
// }
|
||||||
|
|
||||||
// TODO: swap all this out with Titan's running via API
|
// // TODO: swap all this out with Titan's running via API
|
||||||
cmd := exec.Command("docker", "run", "--rm", "-i", "-e", fmt.Sprintf("PAYLOAD=%v", string(payload)), image)
|
// cmd := exec.Command("docker", "run", "--rm", "-i", "-e", fmt.Sprintf("PAYLOAD=%v", payload), image)
|
||||||
stdout, err := cmd.StdoutPipe()
|
// stdout, err := cmd.StdoutPipe()
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
log.Fatal(err)
|
// log.Fatal(err)
|
||||||
}
|
// }
|
||||||
stderr, err := cmd.StderrPipe()
|
// stderr, err := cmd.StderrPipe()
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
log.Fatal(err)
|
// log.Fatal(err)
|
||||||
}
|
// }
|
||||||
if err := cmd.Start(); err != nil {
|
// if err := cmd.Start(); err != nil {
|
||||||
log.Fatal(err)
|
// log.Fatal(err)
|
||||||
}
|
// }
|
||||||
var b bytes.Buffer
|
// var b bytes.Buffer
|
||||||
buff := bufio.NewWriter(&b)
|
// buff := bufio.NewWriter(&b)
|
||||||
|
|
||||||
go io.Copy(buff, stdout)
|
// go io.Copy(buff, stdout)
|
||||||
go io.Copy(buff, stderr)
|
// go io.Copy(buff, stderr)
|
||||||
|
|
||||||
log.Printf("Waiting for command to finish...")
|
// log.Printf("Waiting for command to finish...")
|
||||||
if err = cmd.Wait(); err != nil {
|
// if err = cmd.Wait(); err != nil {
|
||||||
// job failed
|
// // job failed
|
||||||
// log.Infoln("job finished with err:", err)
|
// // log.Infoln("job finished with err:", err)
|
||||||
// log.WithFields(log.Fields{"metric": "run.errors", "value": 1, "type": "count"}).Infoln("failed run")
|
// // log.WithFields(log.Fields{"metric": "run.errors", "value": 1, "type": "count"}).Infoln("failed run")
|
||||||
return err
|
// return err
|
||||||
// TODO: wrap error in json "error": buff
|
// // TODO: wrap error in json "error": buff
|
||||||
}
|
// }
|
||||||
|
|
||||||
// log.Infoln("Docker ran successfully:", b.String())
|
// // log.Infoln("Docker ran successfully:", b.String())
|
||||||
// print
|
// // print
|
||||||
// log.WithFields(log.Fields{"metric": "run.success", "value": 1, "type": "count"}).Infoln("successful run")
|
// // log.WithFields(log.Fields{"metric": "run.success", "value": 1, "type": "count"}).Infoln("successful run")
|
||||||
// log.WithFields(log.Fields{"metric": "run", "value": 1, "type": "count"}).Infoln("job ran")
|
// // log.WithFields(log.Fields{"metric": "run", "value": 1, "type": "count"}).Infoln("job ran")
|
||||||
buff.Flush()
|
// buff.Flush()
|
||||||
|
|
||||||
c.Data(http.StatusOK, "", bytes.Trim(b.Bytes(), "\x00"))
|
// c.Data(http.StatusOK, "", bytes.Trim(b.Bytes(), "\x00"))
|
||||||
|
|
||||||
return nil
|
// return nil
|
||||||
}
|
// }
|
||||||
|
|
||||||
func DockerHost(el *models.Route, c *gin.Context) error {
|
// func DockerHost(el *models.Route, c *gin.Context) error {
|
||||||
ra := runningImages[el.Image]
|
// ra := runningImages[el.Image]
|
||||||
if ra == nil {
|
// if ra == nil {
|
||||||
ra = &RunningApp{}
|
// ra = &RunningApp{}
|
||||||
ra.Route = el
|
// ra.Route = el
|
||||||
ra.Port = rand.Intn(9999-9000) + 9000
|
// ra.Port = rand.Intn(9999-9000) + 9000
|
||||||
ra.ContainerName = fmt.Sprintf("c_%v", rand.Intn(10000))
|
// ra.ContainerName = fmt.Sprintf("c_%v", rand.Intn(10000))
|
||||||
runningImages[el.Image] = ra
|
// runningImages[el.Image] = ra
|
||||||
// TODO: timeout 59 minutes. Mark it in ra as terminated.
|
// // TODO: timeout 59 minutes. Mark it in ra as terminated.
|
||||||
cmd := exec.Command("docker", "run", "--name", ra.ContainerName, "--rm", "-i", "-p", fmt.Sprintf("%v:8080", ra.Port), el.Image)
|
// cmd := exec.Command("docker", "run", "--name", ra.ContainerName, "--rm", "-i", "-p", fmt.Sprintf("%v:8080", ra.Port), el.Image)
|
||||||
// TODO: What should we do with the output here? Store it? Send it to a log service?
|
// // TODO: What should we do with the output here? Store it? Send it to a log service?
|
||||||
// cmd.Stdout = os.Stdout
|
// // cmd.Stdout = os.Stdout
|
||||||
cmd.Stderr = os.Stderr
|
// cmd.Stderr = os.Stderr
|
||||||
// TODO: Need to catch interrupt and stop all containers that are started, see devo/dj for how to do this
|
// // TODO: Need to catch interrupt and stop all containers that are started, see devo/dj for how to do this
|
||||||
if err := cmd.Start(); err != nil {
|
// if err := cmd.Start(); err != nil {
|
||||||
return err
|
// return err
|
||||||
// TODO: What if the app fails to start? Don't want to keep starting the container
|
// // TODO: What if the app fails to start? Don't want to keep starting the container
|
||||||
}
|
// }
|
||||||
} else {
|
// } else {
|
||||||
// TODO: check if it's still running?
|
// // TODO: check if it's still running?
|
||||||
// TODO: if ra.terminated, then start new container?
|
// // TODO: if ra.terminated, then start new container?
|
||||||
}
|
// }
|
||||||
fmt.Println("RunningApp:", ra)
|
// fmt.Println("RunningApp:", ra)
|
||||||
// TODO: if connection fails, check if container still running? If not, start it again
|
// // TODO: if connection fails, check if container still running? If not, start it again
|
||||||
resp, err := http.Get(fmt.Sprintf("http://0.0.0.0:%v%v", ra.Port, el.ContainerPath))
|
// resp, err := http.Get(fmt.Sprintf("http://0.0.0.0:%v%v", ra.Port, el.ContainerPath))
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return err
|
// return err
|
||||||
}
|
// }
|
||||||
defer resp.Body.Close()
|
// defer resp.Body.Close()
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
// body, err := ioutil.ReadAll(resp.Body)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return err
|
// return err
|
||||||
}
|
// }
|
||||||
|
|
||||||
c.Data(http.StatusOK, "", body)
|
// c.Data(http.StatusOK, "", body)
|
||||||
return nil
|
// return nil
|
||||||
}
|
// }
|
||||||
|
|
||||||
func checkAndPull(image string) error {
|
// func checkAndPull(image string) error {
|
||||||
err := execAndPrint("docker", []string{"inspect", image})
|
// err := execAndPrint("docker", []string{"inspect", image})
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
// image does not exist, so let's pull
|
// // image does not exist, so let's pull
|
||||||
fmt.Println("Image not found locally, will pull.", err)
|
// fmt.Println("Image not found locally, will pull.", err)
|
||||||
err = execAndPrint("docker", []string{"pull", image})
|
// err = execAndPrint("docker", []string{"pull", image})
|
||||||
}
|
// }
|
||||||
return err
|
// return err
|
||||||
}
|
// }
|
||||||
|
|
||||||
func execAndPrint(cmdstr string, args []string) error {
|
// func execAndPrint(cmdstr string, args []string) error {
|
||||||
var bout bytes.Buffer
|
// var bout bytes.Buffer
|
||||||
buffout := bufio.NewWriter(&bout)
|
// buffout := bufio.NewWriter(&bout)
|
||||||
var berr bytes.Buffer
|
// var berr bytes.Buffer
|
||||||
bufferr := bufio.NewWriter(&berr)
|
// bufferr := bufio.NewWriter(&berr)
|
||||||
cmd := exec.Command(cmdstr, args...)
|
// cmd := exec.Command(cmdstr, args...)
|
||||||
stdout, err := cmd.StdoutPipe()
|
// stdout, err := cmd.StdoutPipe()
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return err
|
// return err
|
||||||
}
|
// }
|
||||||
stderr, err := cmd.StderrPipe()
|
// stderr, err := cmd.StderrPipe()
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return err
|
// return err
|
||||||
}
|
// }
|
||||||
if err := cmd.Start(); err != nil {
|
// if err := cmd.Start(); err != nil {
|
||||||
return err
|
// return err
|
||||||
}
|
// }
|
||||||
go io.Copy(buffout, stdout)
|
// go io.Copy(buffout, stdout)
|
||||||
go io.Copy(bufferr, stderr)
|
// go io.Copy(bufferr, stderr)
|
||||||
|
|
||||||
log.Printf("Waiting for cmd to finish...")
|
// log.Printf("Waiting for cmd to finish...")
|
||||||
err = cmd.Wait()
|
// err = cmd.Wait()
|
||||||
if berr.Len() != 0 {
|
// if berr.Len() != 0 {
|
||||||
fmt.Println("stderr:", berr.String())
|
// fmt.Println("stderr:", berr.String())
|
||||||
}
|
// }
|
||||||
fmt.Println("stdout:", bout.String())
|
// fmt.Println("stdout:", bout.String())
|
||||||
return err
|
// return err
|
||||||
}
|
// }
|
||||||
|
|||||||
117
api/runner/titan.go
Normal file
117
api/runner/titan.go
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
package runner
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/iron-io/functions/api/models"
|
||||||
|
tmodels "github.com/iron-io/titan/jobserver/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TitanJob struct {
|
||||||
|
runner *RouteRunner
|
||||||
|
resultChan chan error
|
||||||
|
result []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
var versionPath = "/v1"
|
||||||
|
|
||||||
|
func CreateTitanJob(runner *RouteRunner) *TitanJob {
|
||||||
|
t := &TitanJob{
|
||||||
|
runner: runner,
|
||||||
|
resultChan: make(chan error),
|
||||||
|
}
|
||||||
|
|
||||||
|
go t.Start()
|
||||||
|
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TitanJob) Start() {
|
||||||
|
newjob := tmodels.JobsWrapper{
|
||||||
|
Jobs: []*tmodels.Job{
|
||||||
|
&tmodels.Job{
|
||||||
|
NewJob: tmodels.NewJob{
|
||||||
|
Image: &t.runner.Route.Image,
|
||||||
|
Payload: t.runner.Payload,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
jobJSON, err := json.Marshal(newjob)
|
||||||
|
if err != nil {
|
||||||
|
t.resultChan <- models.ErrInvalidJSON
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := t.titanPOST(fmt.Sprintf("/groups/app-%s/jobs", t.runner.Route.AppName), bytes.NewBuffer(jobJSON))
|
||||||
|
if err != nil {
|
||||||
|
t.resultChan <- models.ErrRunnerAPICantConnect
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var resultJobs tmodels.JobsWrapper
|
||||||
|
respBody, err := ioutil.ReadAll(resp.Body)
|
||||||
|
err = json.Unmarshal(respBody, &resultJobs)
|
||||||
|
if err != nil {
|
||||||
|
t.resultChan <- models.ErrInvalidJSON
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if resultJobs.Jobs == nil {
|
||||||
|
t.resultChan <- models.ErrRunnerAPICreateJob
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
job := resultJobs.Jobs[0]
|
||||||
|
begin := time.Now()
|
||||||
|
for len(t.result) == 0 {
|
||||||
|
if time.Since(begin) > t.runner.Timeout {
|
||||||
|
t.resultChan <- models.ErrRunnerTimeout
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := t.titanGET(fmt.Sprintf("/groups/app-%s/jobs/%s/log", t.runner.Route.AppName, job.ID))
|
||||||
|
if err == nil {
|
||||||
|
fmt.Println(resp.Status)
|
||||||
|
if resp.StatusCode == http.StatusOK {
|
||||||
|
resBody, err := ioutil.ReadAll(resp.Body)
|
||||||
|
fmt.Println(string(resBody))
|
||||||
|
if err != nil {
|
||||||
|
t.resultChan <- models.ErrRunnerInvalidResponse
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
t.result = resBody
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.resultChan <- nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TitanJob) Wait() error {
|
||||||
|
return <-t.resultChan
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TitanJob) Result() []byte {
|
||||||
|
return t.result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TitanJob) titanPOST(path string, body io.Reader) (*http.Response, error) {
|
||||||
|
fmt.Println(fmt.Sprintf("%s%s%s", t.runner.Endpoint, versionPath, path))
|
||||||
|
return http.Post(fmt.Sprintf("%s%s%s", t.runner.Endpoint, versionPath, path), "application/json", body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TitanJob) titanGET(path string) (*http.Response, error) {
|
||||||
|
fmt.Println(fmt.Sprintf("%s%s%s", t.runner.Endpoint, versionPath, path))
|
||||||
|
return http.Get(fmt.Sprintf("%s%s%s", t.runner.Endpoint, versionPath, path))
|
||||||
|
}
|
||||||
@@ -266,6 +266,10 @@ func buildFilterQuery(filter *models.RouteFilter) string {
|
|||||||
filterQuery := ""
|
filterQuery := ""
|
||||||
|
|
||||||
filterQueries := []string{}
|
filterQueries := []string{}
|
||||||
|
if filter.Path != "" {
|
||||||
|
filterQueries = append(filterQueries, fmt.Sprintf("path = '%s'", filter.Path))
|
||||||
|
}
|
||||||
|
|
||||||
if filter.AppName != "" {
|
if filter.AppName != "" {
|
||||||
filterQueries = append(filterQueries, fmt.Sprintf("app_name = '%s'", filter.AppName))
|
filterQueries = append(filterQueries, fmt.Sprintf("app_name = '%s'", filter.AppName))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ func Start(engine *gin.Engine) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
engine.GET("/r/:app/*route", handleRunner)
|
engine.Any("/r/:app/*route", handleRunner)
|
||||||
|
engine.NoRoute(handleRunner)
|
||||||
}
|
}
|
||||||
|
|
||||||
func simpleError(err error) *models.Error {
|
func simpleError(err error) *models.Error {
|
||||||
|
|||||||
@@ -1,19 +1,84 @@
|
|||||||
package router
|
package router
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
"github.com/Sirupsen/logrus"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/iron-io/functions/api/models"
|
||||||
"github.com/iron-io/functions/api/runner"
|
"github.com/iron-io/functions/api/runner"
|
||||||
)
|
)
|
||||||
|
|
||||||
func handleRunner(c *gin.Context) {
|
func handleRunner(c *gin.Context) {
|
||||||
log := c.MustGet("log").(logrus.FieldLogger)
|
log := c.MustGet("log").(logrus.FieldLogger)
|
||||||
|
store := c.MustGet("store").(models.Datastore)
|
||||||
|
config := c.MustGet("config").(*models.Config)
|
||||||
|
|
||||||
err := runner.Run(c)
|
var err error
|
||||||
|
|
||||||
|
var payload []byte
|
||||||
|
if c.Request.Method == "POST" || c.Request.Method == "PUT" {
|
||||||
|
payload, err = ioutil.ReadAll(c.Request.Body)
|
||||||
|
} else if c.Request.Method == "GET" {
|
||||||
|
qPL := c.Request.URL.Query()["payload"]
|
||||||
|
if len(qPL) > 0 {
|
||||||
|
payload = []byte(qPL[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.WithField("payload", string(payload)).Debug("Got payload")
|
||||||
|
|
||||||
|
appName := c.Param("app")
|
||||||
|
if appName == "" {
|
||||||
|
host := strings.Split(c.Request.Header.Get("Host"), ":")[0]
|
||||||
|
appName = strings.Split(host, ".")[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
route := c.Param("route")
|
||||||
|
if route == "" {
|
||||||
|
route = c.Request.URL.Path
|
||||||
|
}
|
||||||
|
|
||||||
|
filter := &models.RouteFilter{
|
||||||
|
Path: route,
|
||||||
|
AppName: appName,
|
||||||
|
}
|
||||||
|
|
||||||
|
log.WithFields(logrus.Fields{"app": appName, "path": route}).Debug("Finding route on datastore")
|
||||||
|
|
||||||
|
routes, err := store.GetRoutes(filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug(err)
|
log.WithError(err).Error(models.ErrRoutesList)
|
||||||
c.JSON(http.StatusInternalServerError, simpleError(err))
|
c.JSON(http.StatusInternalServerError, simpleError(models.ErrRoutesList))
|
||||||
|
}
|
||||||
|
|
||||||
|
log.WithField("routes", routes).Debug("Got routes from datastore")
|
||||||
|
|
||||||
|
for _, el := range routes {
|
||||||
|
if el.Path == route {
|
||||||
|
titanJob := runner.CreateTitanJob(&runner.RouteRunner{
|
||||||
|
Route: el,
|
||||||
|
Endpoint: config.API,
|
||||||
|
Payload: string(payload),
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := titanJob.Wait(); err != nil {
|
||||||
|
log.WithError(err).Error(models.ErrRunnerRunRoute)
|
||||||
|
c.JSON(http.StatusInternalServerError, simpleError(models.ErrRunnerRunRoute))
|
||||||
|
} else {
|
||||||
|
for k, v := range el.Headers {
|
||||||
|
c.Header(k, v[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Data(http.StatusOK, "", bytes.Trim(titanJob.Result(), "\x00"))
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,16 +7,17 @@ import (
|
|||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
"github.com/Sirupsen/logrus"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/iron-io/functions/api/models"
|
||||||
"github.com/iron-io/functions/api/server/datastore"
|
"github.com/iron-io/functions/api/server/datastore"
|
||||||
"github.com/iron-io/functions/api/server/router"
|
"github.com/iron-io/functions/api/server/router"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
router *gin.Engine
|
router *gin.Engine
|
||||||
cfg *Config
|
cfg *models.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(config *Config) *Server {
|
func New(config *models.Config) *Server {
|
||||||
return &Server{
|
return &Server{
|
||||||
router: gin.Default(),
|
router: gin.Default(),
|
||||||
cfg: config,
|
cfg: config,
|
||||||
@@ -37,6 +38,10 @@ func (s *Server) Start() {
|
|||||||
s.cfg.DatabaseURL = fmt.Sprintf("bolt://%s/bolt.db?bucket=funcs", cwd)
|
s.cfg.DatabaseURL = fmt.Sprintf("bolt://%s/bolt.db?bucket=funcs", cwd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.cfg.API == "" {
|
||||||
|
s.cfg.API = "http://localhost:8080"
|
||||||
|
}
|
||||||
|
|
||||||
ds, err := datastore.New(s.cfg.DatabaseURL)
|
ds, err := datastore.New(s.cfg.DatabaseURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.WithError(err).Fatalln("Invalid DB url.")
|
logrus.WithError(err).Fatalln("Invalid DB url.")
|
||||||
@@ -46,6 +51,7 @@ func (s *Server) Start() {
|
|||||||
logrus.SetLevel(logrus.DebugLevel)
|
logrus.SetLevel(logrus.DebugLevel)
|
||||||
|
|
||||||
s.router.Use(func(c *gin.Context) {
|
s.router.Use(func(c *gin.Context) {
|
||||||
|
c.Set("config", s.cfg)
|
||||||
c.Set("store", ds)
|
c.Set("store", ds)
|
||||||
c.Set("log", logrus.WithFields(extractFields(c)))
|
c.Set("log", logrus.WithFields(extractFields(c)))
|
||||||
c.Next()
|
c.Next()
|
||||||
|
|||||||
@@ -1,168 +0,0 @@
|
|||||||
// Copyright 2010 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"container/heap"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"math/rand"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const nRequester = 100
|
|
||||||
const nWorker = 10
|
|
||||||
|
|
||||||
var roundRobin = flag.Bool("r", false, "use round-robin scheduling")
|
|
||||||
|
|
||||||
// Simulation of some work: just sleep for a while and report how long.
|
|
||||||
func op() int {
|
|
||||||
n := rand.Int63n(5)
|
|
||||||
time.Sleep(time.Duration(n) * time.Second)
|
|
||||||
return int(n)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Request struct {
|
|
||||||
fn func() int
|
|
||||||
c chan int
|
|
||||||
}
|
|
||||||
|
|
||||||
func requester(work chan Request) {
|
|
||||||
c := make(chan int)
|
|
||||||
for {
|
|
||||||
time.Sleep(time.Duration(rand.Int63n(nWorker)) * time.Second)
|
|
||||||
work <- Request{op, c}
|
|
||||||
<-c
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Worker struct {
|
|
||||||
i int
|
|
||||||
requests chan Request
|
|
||||||
pending int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Worker) work(done chan *Worker) {
|
|
||||||
for {
|
|
||||||
req := <-w.requests
|
|
||||||
req.c <- req.fn()
|
|
||||||
done <- w
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Pool []*Worker
|
|
||||||
|
|
||||||
func (p Pool) Len() int { return len(p) }
|
|
||||||
|
|
||||||
func (p Pool) Less(i, j int) bool {
|
|
||||||
return p[i].pending < p[j].pending
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Pool) Swap(i, j int) {
|
|
||||||
a := *p
|
|
||||||
a[i], a[j] = a[j], a[i]
|
|
||||||
a[i].i = i
|
|
||||||
a[j].i = j
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Pool) Push(x interface{}) {
|
|
||||||
a := *p
|
|
||||||
n := len(a)
|
|
||||||
a = a[0 : n+1]
|
|
||||||
w := x.(*Worker)
|
|
||||||
a[n] = w
|
|
||||||
w.i = n
|
|
||||||
*p = a
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Pool) Pop() interface{} {
|
|
||||||
a := *p
|
|
||||||
*p = a[0 : len(a)-1]
|
|
||||||
w := a[len(a)-1]
|
|
||||||
w.i = -1 // for safety
|
|
||||||
return w
|
|
||||||
}
|
|
||||||
|
|
||||||
type Balancer struct {
|
|
||||||
pool Pool
|
|
||||||
done chan *Worker
|
|
||||||
i int
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewBalancer() *Balancer {
|
|
||||||
done := make(chan *Worker, nWorker)
|
|
||||||
b := &Balancer{make(Pool, 0, nWorker), done, 0}
|
|
||||||
for i := 0; i < nWorker; i++ {
|
|
||||||
w := &Worker{requests: make(chan Request, nRequester)}
|
|
||||||
heap.Push(&b.pool, w)
|
|
||||||
go w.work(b.done)
|
|
||||||
}
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Balancer) balance(work chan Request) {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case req := <-work:
|
|
||||||
b.dispatch(req)
|
|
||||||
case w := <-b.done:
|
|
||||||
b.completed(w)
|
|
||||||
}
|
|
||||||
b.print()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Balancer) print() {
|
|
||||||
sum := 0
|
|
||||||
sumsq := 0
|
|
||||||
for _, w := range b.pool {
|
|
||||||
fmt.Printf("%d ", w.pending)
|
|
||||||
sum += w.pending
|
|
||||||
sumsq += w.pending * w.pending
|
|
||||||
}
|
|
||||||
avg := float64(sum) / float64(len(b.pool))
|
|
||||||
variance := float64(sumsq)/float64(len(b.pool)) - avg*avg
|
|
||||||
fmt.Printf(" %.2f %.2f\n", avg, variance)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Balancer) dispatch(req Request) {
|
|
||||||
if *roundRobin {
|
|
||||||
w := b.pool[b.i]
|
|
||||||
w.requests <- req
|
|
||||||
w.pending++
|
|
||||||
b.i++
|
|
||||||
if b.i >= len(b.pool) {
|
|
||||||
b.i = 0
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w := heap.Pop(&b.pool).(*Worker)
|
|
||||||
w.requests <- req
|
|
||||||
w.pending++
|
|
||||||
// fmt.Printf("started %p; now %d\n", w, w.pending)
|
|
||||||
heap.Push(&b.pool, w)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Balancer) completed(w *Worker) {
|
|
||||||
if *roundRobin {
|
|
||||||
w.pending--
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.pending--
|
|
||||||
// fmt.Printf("finished %p; now %d\n", w, w.pending)
|
|
||||||
heap.Remove(&b.pool, w.i)
|
|
||||||
heap.Push(&b.pool, w)
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
flag.Parse()
|
|
||||||
work := make(chan Request)
|
|
||||||
for i := 0; i < nRequester; i++ {
|
|
||||||
go requester(work)
|
|
||||||
}
|
|
||||||
NewBalancer().balance(work)
|
|
||||||
}
|
|
||||||
4
main.go
4
main.go
@@ -11,12 +11,14 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
|
|
||||||
log "github.com/Sirupsen/logrus"
|
log "github.com/Sirupsen/logrus"
|
||||||
|
"github.com/iron-io/functions/api/models"
|
||||||
"github.com/iron-io/functions/api/server"
|
"github.com/iron-io/functions/api/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
config := &server.Config{}
|
config := &models.Config{}
|
||||||
config.DatabaseURL = os.Getenv("DB")
|
config.DatabaseURL = os.Getenv("DB")
|
||||||
|
config.API = os.Getenv("API")
|
||||||
|
|
||||||
err := config.Validate()
|
err := config.Validate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user