github.com/iron-io/functions@v0.0.0-20180820112432-d59d7d1c40b2/api/server/runner.go (about)

     1  package server
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"io/ioutil"
    10  	"net/http"
    11  	"path"
    12  	"strings"
    13  	"time"
    14  
    15  	"github.com/Sirupsen/logrus"
    16  	"github.com/gin-gonic/gin"
    17  	"github.com/iron-io/functions/api"
    18  	"github.com/iron-io/functions/api/models"
    19  	"github.com/iron-io/functions/api/runner"
    20  	"github.com/iron-io/functions/api/runner/task"
    21  	f_common "github.com/iron-io/functions/common"
    22  	"github.com/iron-io/runner/common"
    23  	uuid "github.com/satori/go.uuid"
    24  )
    25  
    26  type runnerResponse struct {
    27  	RequestID string            `json:"request_id,omitempty"`
    28  	Error     *models.ErrorBody `json:"error,omitempty"`
    29  }
    30  
    31  func (s *Server) handleSpecial(c *gin.Context) {
    32  	ctx := c.MustGet("ctx").(context.Context)
    33  	log := common.Logger(ctx)
    34  
    35  	ctx = context.WithValue(ctx, api.AppName, "")
    36  	c.Set(api.AppName, "")
    37  	ctx = context.WithValue(ctx, api.Path, c.Request.URL.Path)
    38  	c.Set(api.Path, c.Request.URL.Path)
    39  
    40  	ctx, err := s.UseSpecialHandlers(ctx, c.Request, c.Writer)
    41  	if err == ErrNoSpecialHandlerFound {
    42  		log.WithError(err).Errorln("Not special handler found")
    43  		c.JSON(http.StatusNotFound, http.StatusText(http.StatusNotFound))
    44  		return
    45  	} else if err != nil {
    46  		log.WithError(err).Errorln("Error using special handler!")
    47  		c.JSON(http.StatusInternalServerError, simpleError(errors.New("Failed to run function")))
    48  		return
    49  	}
    50  
    51  	c.Set("ctx", ctx)
    52  	c.Set(api.AppName, ctx.Value(api.AppName).(string))
    53  	if c.MustGet(api.AppName).(string) == "" {
    54  		log.WithError(err).Errorln("Specialhandler returned empty app name")
    55  		c.JSON(http.StatusBadRequest, simpleError(models.ErrRunnerRouteNotFound))
    56  		return
    57  	}
    58  
    59  	// now call the normal runner call
    60  	s.handleRequest(c, nil)
    61  }
    62  
    63  func toEnvName(envtype, name string) string {
    64  	name = strings.ToUpper(strings.Replace(name, "-", "_", -1))
    65  	if envtype == "" {
    66  		return name
    67  	}
    68  	return fmt.Sprintf("%s_%s", envtype, name)
    69  }
    70  
    71  func (s *Server) handleRequest(c *gin.Context, enqueue models.Enqueue) {
    72  	if strings.HasPrefix(c.Request.URL.Path, "/v1") {
    73  		c.Status(http.StatusNotFound)
    74  		return
    75  	}
    76  
    77  	ctx := c.MustGet("ctx").(context.Context)
    78  
    79  	reqID := uuid.NewV5(uuid.Nil, fmt.Sprintf("%s%s%d", c.Request.RemoteAddr, c.Request.URL.Path, time.Now().Unix())).String()
    80  	ctx, log := common.LoggerWithFields(ctx, logrus.Fields{"call_id": reqID})
    81  
    82  	var err error
    83  	var payload io.Reader
    84  
    85  	if c.Request.Method == "POST" {
    86  		payload = c.Request.Body
    87  		// Load complete body and close
    88  		defer func() {
    89  			io.Copy(ioutil.Discard, c.Request.Body)
    90  			c.Request.Body.Close()
    91  		}()
    92  	} else if c.Request.Method == "GET" {
    93  		reqPayload := c.Request.URL.Query().Get("payload")
    94  		payload = strings.NewReader(reqPayload)
    95  	}
    96  
    97  	reqRoute := &models.Route{
    98  		AppName: c.MustGet(api.AppName).(string),
    99  		Path:    path.Clean(c.MustGet(api.Path).(string)),
   100  	}
   101  
   102  	s.FireBeforeDispatch(ctx, reqRoute)
   103  
   104  	appName := reqRoute.AppName
   105  	path := reqRoute.Path
   106  
   107  	app, err := s.Datastore.GetApp(ctx, appName)
   108  	if err != nil || app == nil {
   109  		log.WithError(err).Error(models.ErrAppsNotFound)
   110  		c.JSON(http.StatusNotFound, simpleError(models.ErrAppsNotFound))
   111  		return
   112  	}
   113  
   114  	log.WithFields(logrus.Fields{"app": appName, "path": path}).Debug("Finding route on datastore")
   115  	routes, err := s.loadroutes(ctx, models.RouteFilter{AppName: appName, Path: path})
   116  	if err != nil {
   117  		log.WithError(err).Error(models.ErrRoutesList)
   118  		c.JSON(http.StatusInternalServerError, simpleError(models.ErrRoutesList))
   119  		return
   120  	}
   121  
   122  	if len(routes) == 0 {
   123  		log.WithError(err).Error(models.ErrRunnerRouteNotFound)
   124  		c.JSON(http.StatusNotFound, simpleError(models.ErrRunnerRouteNotFound))
   125  		return
   126  	}
   127  
   128  	log.WithField("routes", len(routes)).Debug("Got routes from datastore")
   129  	route := routes[0]
   130  	log = log.WithFields(logrus.Fields{"app": appName, "path": route.Path, "image": route.Image})
   131  
   132  	if err = f_common.AuthJwt(route.JwtKey, c.Request); err != nil {
   133  		log.WithError(err).Error("JWT Authentication Failed")
   134  		c.Writer.Header().Set("WWW-Authenticate", "Bearer realm=\"\"")
   135  		c.JSON(http.StatusUnauthorized, simpleError(err))
   136  		return
   137  	}
   138  
   139  	if s.serve(ctx, c, appName, route, app, path, reqID, payload, enqueue) {
   140  		s.FireAfterDispatch(ctx, reqRoute)
   141  		return
   142  	}
   143  
   144  	log.Error(models.ErrRunnerRouteNotFound)
   145  	c.JSON(http.StatusNotFound, simpleError(models.ErrRunnerRouteNotFound))
   146  }
   147  
   148  func (s *Server) loadroutes(ctx context.Context, filter models.RouteFilter) ([]*models.Route, error) {
   149  	if route, ok := s.cacheget(filter.AppName, filter.Path); ok {
   150  		return []*models.Route{route}, nil
   151  	}
   152  	resp, err := s.singleflight.do(
   153  		filter,
   154  		func() (interface{}, error) {
   155  			return s.Datastore.GetRoutesByApp(ctx, filter.AppName, &filter)
   156  		},
   157  	)
   158  	return resp.([]*models.Route), err
   159  }
   160  
   161  // TODO: Should remove *gin.Context from these functions, should use only context.Context
   162  func (s *Server) serve(ctx context.Context, c *gin.Context, appName string, found *models.Route, app *models.App, route, reqID string, payload io.Reader, enqueue models.Enqueue) (ok bool) {
   163  	ctx, log := common.LoggerWithFields(ctx, logrus.Fields{"app": appName, "route": found.Path, "image": found.Image})
   164  
   165  	params, match := matchRoute(found.Path, route)
   166  	if !match {
   167  		return false
   168  	}
   169  
   170  	var stdout bytes.Buffer // TODO: should limit the size of this, error if gets too big. akin to: https://golang.org/pkg/io/#LimitReader
   171  
   172  	envVars := map[string]string{
   173  		"METHOD": c.Request.Method,
   174  		"ROUTE":  found.Path,
   175  		"REQUEST_URL": fmt.Sprintf("%v//%v%v", func() string {
   176  			if c.Request.TLS == nil {
   177  				return "http"
   178  			}
   179  			return "https"
   180  		}(), c.Request.Host, c.Request.URL.String()),
   181  	}
   182  
   183  	// app config
   184  	for k, v := range app.Config {
   185  		envVars[toEnvName("", k)] = v
   186  	}
   187  	for k, v := range found.Config {
   188  		envVars[toEnvName("", k)] = v
   189  	}
   190  
   191  	// params
   192  	for _, param := range params {
   193  		envVars[toEnvName("PARAM", param.Key)] = param.Value
   194  	}
   195  
   196  	// headers
   197  	for header, value := range c.Request.Header {
   198  		envVars[toEnvName("HEADER", header)] = strings.Join(value, " ")
   199  	}
   200  
   201  	cfg := &task.Config{
   202  		AppName:        appName,
   203  		Path:           found.Path,
   204  		Env:            envVars,
   205  		Format:         found.Format,
   206  		ID:             reqID,
   207  		Image:          found.Image,
   208  		MaxConcurrency: found.MaxConcurrency,
   209  		Memory:         found.Memory,
   210  		Stdin:          payload,
   211  		Stdout:         &stdout,
   212  		Timeout:        time.Duration(found.Timeout) * time.Second,
   213  		IdleTimeout:    time.Duration(found.IdleTimeout) * time.Second,
   214  	}
   215  
   216  	s.Runner.Enqueue()
   217  	switch found.Type {
   218  	case "async":
   219  		// Read payload
   220  		pl, err := ioutil.ReadAll(cfg.Stdin)
   221  		if err != nil {
   222  			log.WithError(err).Error(models.ErrInvalidPayload)
   223  			c.JSON(http.StatusBadRequest, simpleError(models.ErrInvalidPayload))
   224  			return true
   225  		}
   226  
   227  		// Create Task
   228  		priority := int32(0)
   229  		task := &models.Task{}
   230  		task.Image = &cfg.Image
   231  		task.ID = cfg.ID
   232  		task.Path = found.Path
   233  		task.AppName = cfg.AppName
   234  		task.Priority = &priority
   235  		task.EnvVars = cfg.Env
   236  		task.Payload = string(pl)
   237  		// Push to queue
   238  		enqueue(c, s.MQ, task)
   239  		log.Info("Added new task to queue")
   240  		c.JSON(http.StatusAccepted, map[string]string{"call_id": task.ID})
   241  
   242  	default:
   243  		result, err := runner.RunTask(s.tasks, ctx, cfg)
   244  		if err != nil {
   245  			c.JSON(http.StatusInternalServerError, runnerResponse{
   246  				RequestID: cfg.ID,
   247  				Error: &models.ErrorBody{
   248  					Message: err.Error(),
   249  				},
   250  			})
   251  			log.WithError(err).Error("Failed to run task")
   252  			break
   253  		}
   254  		for k, v := range found.Headers {
   255  			c.Header(k, v[0])
   256  		}
   257  
   258  		switch result.Status() {
   259  		case "success":
   260  			c.Data(http.StatusOK, "", stdout.Bytes())
   261  		case "timeout":
   262  			c.JSON(http.StatusGatewayTimeout, runnerResponse{
   263  				RequestID: cfg.ID,
   264  				Error: &models.ErrorBody{
   265  					Message: models.ErrRunnerTimeout.Error(),
   266  				},
   267  			})
   268  		default:
   269  			c.JSON(http.StatusInternalServerError, runnerResponse{
   270  				RequestID: cfg.ID,
   271  				Error: &models.ErrorBody{
   272  					Message: result.Error(),
   273  				},
   274  			})
   275  		}
   276  	}
   277  
   278  	return true
   279  }
   280  
   281  var fakeHandler = func(http.ResponseWriter, *http.Request, Params) {}
   282  
   283  func matchRoute(baseRoute, route string) (Params, bool) {
   284  	tree := &node{}
   285  	tree.addRoute(baseRoute, fakeHandler)
   286  	handler, p, _ := tree.getValue(route)
   287  	if handler == nil {
   288  		return nil, false
   289  	}
   290  
   291  	return p, true
   292  }