mirror of
https://github.com/openfaas/faasd.git
synced 2025-06-08 16:06:47 +00:00
284 lines
9.3 KiB
Go
Generated
284 lines
9.3 KiB
Go
Generated
// Package proxy provides a default function invocation proxy method for OpenFaaS providers.
|
|
//
|
|
// The function proxy logic is used by the Gateway when `direct_functions` is set to false.
|
|
// This means that the provider will direct call the function and return the results. This
|
|
// involves resolving the function by name and then copying the result into the original HTTP
|
|
// request.
|
|
//
|
|
// openfaas-provider has implemented a standard HTTP HandlerFunc that will handle setting
|
|
// timeout values, parsing the request path, and copying the request/response correctly.
|
|
//
|
|
// bootstrapHandlers := bootTypes.FaaSHandlers{
|
|
// FunctionProxy: proxy.NewHandlerFunc(timeout, resolver),
|
|
// DeleteHandler: handlers.MakeDeleteHandler(clientset),
|
|
// DeployHandler: handlers.MakeDeployHandler(clientset),
|
|
// FunctionLister: handlers.MakeFunctionLister(clientset),
|
|
// ReplicaReader: handlers.MakeReplicaReader(clientset),
|
|
// ReplicaUpdater: handlers.MakeReplicaUpdater(clientset),
|
|
// InfoHandler: handlers.MakeInfoHandler(),
|
|
// }
|
|
//
|
|
// proxy.NewHandlerFunc is optional, but does simplify the logic of your provider.
|
|
package proxy
|
|
|
|
import (
|
|
"io"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httputil"
|
|
"net/url"
|
|
"time"
|
|
|
|
"github.com/gorilla/mux"
|
|
fhttputil "github.com/openfaas/faas-provider/httputil"
|
|
"github.com/openfaas/faas-provider/types"
|
|
)
|
|
|
|
const (
|
|
watchdogPort = "8080"
|
|
defaultContentType = "text/plain"
|
|
openFaaSInternalHeader = "X-OpenFaaS-Internal"
|
|
)
|
|
|
|
// BaseURLResolver URL resolver for proxy requests
|
|
//
|
|
// The FaaS provider implementation is responsible for providing the resolver function implementation.
|
|
// BaseURLResolver.Resolve will receive the function name and should return the URL of the
|
|
// function service.
|
|
type BaseURLResolver interface {
|
|
Resolve(functionName string) (url.URL, error)
|
|
}
|
|
|
|
// NewHandlerFunc creates a standard http.HandlerFunc to proxy function requests.
|
|
// When verbose is set to true, the timing of each invocation will be printed out to
|
|
// stderr.
|
|
// The returned http.HandlerFunc will ensure:
|
|
//
|
|
// - proper proxy request timeouts
|
|
// - proxy requests for GET, POST, PATCH, PUT, and DELETE
|
|
// - path parsing including support for extracing the function name, sub-paths, and query paremeters
|
|
// - passing and setting the `X-Forwarded-Host` and `X-Forwarded-For` headers
|
|
// - logging errors and proxy request timing to stdout
|
|
//
|
|
// Note that this will panic if `resolver` is nil.
|
|
func NewHandlerFunc(config types.FaaSConfig, resolver BaseURLResolver, verbose bool) http.HandlerFunc {
|
|
if resolver == nil {
|
|
panic("NewHandlerFunc: empty proxy handler resolver, cannot be nil")
|
|
}
|
|
|
|
proxyClient := NewProxyClientFromConfig(config)
|
|
|
|
reverseProxy := httputil.ReverseProxy{}
|
|
reverseProxy.Director = func(req *http.Request) {
|
|
// At least an empty director is required to prevent runtime errors.
|
|
req.URL.Scheme = "http"
|
|
}
|
|
reverseProxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
|
|
}
|
|
|
|
// Errors are common during disconnect of client, no need to log them.
|
|
reverseProxy.ErrorLog = log.New(io.Discard, "", 0)
|
|
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Body != nil {
|
|
defer r.Body.Close()
|
|
}
|
|
|
|
switch r.Method {
|
|
case http.MethodPost,
|
|
http.MethodPut,
|
|
http.MethodPatch,
|
|
http.MethodDelete,
|
|
http.MethodGet,
|
|
http.MethodOptions,
|
|
http.MethodHead:
|
|
proxyRequest(w, r, proxyClient, resolver, &reverseProxy, verbose)
|
|
|
|
default:
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
}
|
|
}
|
|
}
|
|
|
|
// NewProxyClientFromConfig creates a new http.Client designed for proxying requests and enforcing
|
|
// certain minimum configuration values.
|
|
func NewProxyClientFromConfig(config types.FaaSConfig) *http.Client {
|
|
return NewProxyClient(config.GetReadTimeout(), config.GetMaxIdleConns(), config.GetMaxIdleConnsPerHost())
|
|
}
|
|
|
|
// NewProxyClient creates a new http.Client designed for proxying requests, this is exposed as a
|
|
// convenience method for internal or advanced uses. Most people should use NewProxyClientFromConfig.
|
|
func NewProxyClient(timeout time.Duration, maxIdleConns int, maxIdleConnsPerHost int) *http.Client {
|
|
return &http.Client{
|
|
// these Transport values ensure that the http Client will eventually timeout and prevents
|
|
// infinite retries. The default http.Client configure these timeouts. The specific
|
|
// values tuned via performance testing/benchmarking
|
|
//
|
|
// Additional context can be found at
|
|
// - https://medium.com/@nate510/don-t-use-go-s-default-http-client-4804cb19f779
|
|
// - https://blog.cloudflare.com/the-complete-guide-to-golang-net-http-timeouts/
|
|
//
|
|
// Additionally, these overrides for the default client enable re-use of connections and prevent
|
|
// CoreDNS from rate limiting under high traffic
|
|
//
|
|
// See also two similar projects where this value was updated:
|
|
// https://github.com/prometheus/prometheus/pull/3592
|
|
// https://github.com/minio/minio/pull/5860
|
|
Transport: &http.Transport{
|
|
Proxy: http.ProxyFromEnvironment,
|
|
DialContext: (&net.Dialer{
|
|
Timeout: timeout,
|
|
KeepAlive: 1 * time.Second,
|
|
DualStack: true,
|
|
}).DialContext,
|
|
MaxIdleConns: maxIdleConns,
|
|
MaxIdleConnsPerHost: maxIdleConnsPerHost,
|
|
IdleConnTimeout: 120 * time.Millisecond,
|
|
TLSHandshakeTimeout: 10 * time.Second,
|
|
ExpectContinueTimeout: 1500 * time.Millisecond,
|
|
},
|
|
Timeout: timeout,
|
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
|
return http.ErrUseLastResponse
|
|
},
|
|
}
|
|
}
|
|
|
|
// proxyRequest handles the actual resolution of and then request to the function service.
|
|
func proxyRequest(w http.ResponseWriter, originalReq *http.Request, proxyClient *http.Client, resolver BaseURLResolver, reverseProxy *httputil.ReverseProxy, verbose bool) {
|
|
ctx := originalReq.Context()
|
|
|
|
pathVars := mux.Vars(originalReq)
|
|
functionName := pathVars["name"]
|
|
if functionName == "" {
|
|
w.Header().Add(openFaaSInternalHeader, "proxy")
|
|
|
|
fhttputil.Errorf(w, http.StatusBadRequest, "Provide function name in the request path")
|
|
return
|
|
}
|
|
|
|
functionAddr, err := resolver.Resolve(functionName)
|
|
if err != nil {
|
|
w.Header().Add(openFaaSInternalHeader, "proxy")
|
|
|
|
// TODO: Should record the 404/not found error in Prometheus.
|
|
log.Printf("resolver error: no endpoints for %s: %s\n", functionName, err.Error())
|
|
fhttputil.Errorf(w, http.StatusServiceUnavailable, "No endpoints available for: %s.", functionName)
|
|
return
|
|
}
|
|
|
|
proxyReq, err := buildProxyRequest(originalReq, functionAddr, pathVars["params"])
|
|
if err != nil {
|
|
|
|
w.Header().Add(openFaaSInternalHeader, "proxy")
|
|
|
|
fhttputil.Errorf(w, http.StatusInternalServerError, "Failed to resolve service: %s.", functionName)
|
|
return
|
|
}
|
|
|
|
if proxyReq.Body != nil {
|
|
defer proxyReq.Body.Close()
|
|
}
|
|
|
|
if verbose {
|
|
start := time.Now()
|
|
defer func() {
|
|
seconds := time.Since(start)
|
|
log.Printf("%s took %f seconds\n", functionName, seconds.Seconds())
|
|
}()
|
|
}
|
|
|
|
if v := originalReq.Header.Get("Accept"); v == "text/event-stream" {
|
|
originalReq.URL = proxyReq.URL
|
|
|
|
reverseProxy.ServeHTTP(w, originalReq)
|
|
return
|
|
}
|
|
|
|
response, err := proxyClient.Do(proxyReq.WithContext(ctx))
|
|
|
|
if err != nil {
|
|
log.Printf("error with proxy request to: %s, %s\n", proxyReq.URL.String(), err.Error())
|
|
|
|
w.Header().Add(openFaaSInternalHeader, "proxy")
|
|
|
|
fhttputil.Errorf(w, http.StatusInternalServerError, "Can't reach service for: %s.", functionName)
|
|
return
|
|
}
|
|
|
|
if response.Body != nil {
|
|
defer response.Body.Close()
|
|
}
|
|
|
|
clientHeader := w.Header()
|
|
copyHeaders(clientHeader, &response.Header)
|
|
w.Header().Set("Content-Type", getContentType(originalReq.Header, response.Header))
|
|
|
|
w.WriteHeader(response.StatusCode)
|
|
if response.Body != nil {
|
|
io.Copy(w, response.Body)
|
|
}
|
|
}
|
|
|
|
// buildProxyRequest creates a request object for the proxy request, it will ensure that
|
|
// the original request headers are preserved as well as setting openfaas system headers
|
|
func buildProxyRequest(originalReq *http.Request, baseURL url.URL, extraPath string) (*http.Request, error) {
|
|
|
|
host := baseURL.Host
|
|
if baseURL.Port() == "" {
|
|
host = baseURL.Host + ":" + watchdogPort
|
|
}
|
|
|
|
url := url.URL{
|
|
Scheme: baseURL.Scheme,
|
|
Host: host,
|
|
Path: extraPath,
|
|
RawQuery: originalReq.URL.RawQuery,
|
|
}
|
|
|
|
upstreamReq, err := http.NewRequest(originalReq.Method, url.String(), nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
copyHeaders(upstreamReq.Header, &originalReq.Header)
|
|
|
|
if len(originalReq.Host) > 0 && upstreamReq.Header.Get("X-Forwarded-Host") == "" {
|
|
upstreamReq.Header["X-Forwarded-Host"] = []string{originalReq.Host}
|
|
}
|
|
if upstreamReq.Header.Get("X-Forwarded-For") == "" {
|
|
upstreamReq.Header["X-Forwarded-For"] = []string{originalReq.RemoteAddr}
|
|
}
|
|
|
|
if originalReq.Body != nil {
|
|
upstreamReq.Body = originalReq.Body
|
|
}
|
|
|
|
return upstreamReq, nil
|
|
}
|
|
|
|
// copyHeaders clones the header values from the source into the destination.
|
|
func copyHeaders(destination http.Header, source *http.Header) {
|
|
for k, v := range *source {
|
|
vClone := make([]string, len(v))
|
|
copy(vClone, v)
|
|
destination[k] = vClone
|
|
}
|
|
}
|
|
|
|
// getContentType resolves the correct Content-Type for a proxied function.
|
|
func getContentType(request http.Header, proxyResponse http.Header) (headerContentType string) {
|
|
responseHeader := proxyResponse.Get("Content-Type")
|
|
requestHeader := request.Get("Content-Type")
|
|
|
|
if len(responseHeader) > 0 {
|
|
headerContentType = responseHeader
|
|
} else if len(requestHeader) > 0 {
|
|
headerContentType = requestHeader
|
|
} else {
|
|
headerContentType = defaultContentType
|
|
}
|
|
|
|
return headerContentType
|
|
}
|