diff --git a/docker-compose.yml b/docker-compose.yml index 6ff92336..0b290ded 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,6 +14,8 @@ services: dnsrr: "true" # Temporarily use dnsrr in place of VIP while issue persists on PWD faas_nats_address: "nats" faas_nats_port: 4222 + direct_functions: "true" # Functions are invoked directly over the overlay network + direct_functions_suffix: "" deploy: resources: # limits: # Enable if you want to limit memory usage diff --git a/gateway/Dockerfile b/gateway/Dockerfile index 80fe3fc1..e7269dad 100644 --- a/gateway/Dockerfile +++ b/gateway/Dockerfile @@ -1,10 +1,11 @@ FROM golang:1.9.4 as build -WORKDIR /go/src/github.com/openfaas/faas/gateway - RUN curl -sL https://github.com/alexellis/license-check/releases/download/0.2.2/license-check \ > /usr/bin/license-check \ && chmod +x /usr/bin/license-check +WORKDIR /go/src/github.com/openfaas/faas/gateway + + COPY vendor vendor COPY handlers handlers diff --git a/gateway/README.md b/gateway/README.md index 999d5cec..91917b94 100644 --- a/gateway/README.md +++ b/gateway/README.md @@ -9,7 +9,7 @@ In summary: * UI built-in * Deploy your own functions or from the Function Store * Instrumentation via Prometheus -* Autoscaling via AlertManager +* Auto-scaling via AlertManager * REST API available ![](https://raw.githubusercontent.com/openfaas/faas/master/docs/of-overview.png) @@ -48,7 +48,9 @@ The gateway can be configured through the following environment variables: | `write_timeout` | HTTP timeout for writing a response body from your function (in seconds). Default: `8` | | `read_timeout` | HTTP timeout for reading the payload from the client caller (in seconds). Default: `8` | | `functions_provider_url` | URL of upstream [functions provider](https://github.com/openfaas/faas-provider/) - i.e. Swarm, Kubernetes, Nomad etc | -| `faas_nats_address` | Address of NATS service. Required for asynchronous mode. | -| `faas_nats_port` | Port for NATS service. Requrired for asynchronous mode. | -| `faas_prometheus_host` | Host to connect to Prometheus. Default: `"prometheus"`. | -| `faas_promethus_port` | Port to connect to Prometheus. Default: `9090`. | +| `faas_nats_address` | Address of NATS service. Required for asynchronous mode | +| `faas_nats_port` | Port for NATS service. Requrired for asynchronous mode | +| `faas_prometheus_host` | Host to connect to Prometheus. Default: `"prometheus"` | +| `faas_promethus_port` | Port to connect to Prometheus. Default: `9090` | +| `direct_functions` | `true` or `false` - functions are invoked directly over overlay network without passing through provider | +| `direct_functions_suffix` | Provide a DNS suffix for invoking functions directly over overlay network | diff --git a/gateway/handlers/baseurlresolver_test.go b/gateway/handlers/baseurlresolver_test.go new file mode 100644 index 00000000..a69ab74d --- /dev/null +++ b/gateway/handlers/baseurlresolver_test.go @@ -0,0 +1,55 @@ +package handlers + +import ( + "fmt" + "net/http" + "net/url" + "testing" +) + +func TestSingleHostBaseURLResolver(t *testing.T) { + + urlVal, _ := url.Parse("http://upstream:8080/") + r := SingleHostBaseURLResolver{BaseURL: urlVal.String()} + + req, _ := http.NewRequest(http.MethodGet, "http://localhost/function/hello", nil) + + resolved := r.Resolve(req) + want := "http://upstream:8080" + if resolved != want { + t.Logf("r.Resolve failed, want: %s got: %s", want, resolved) + t.Fail() + } +} + +const watchdogPort = 8080 + +func TestFunctionAsHostBaseURLResolver_WithSuffix(t *testing.T) { + suffix := "openfaas-fn.local.cluster.svc." + r := FunctionAsHostBaseURLResolver{FunctionSuffix: suffix} + + req, _ := http.NewRequest(http.MethodGet, "http://localhost/function/hello", nil) + + resolved := r.Resolve(req) + want := fmt.Sprintf("http://hello.%s:%d", suffix, watchdogPort) + + if resolved != want { + t.Logf("r.Resolve failed, want: %s got: %s", want, resolved) + t.Fail() + } +} + +func TestFunctionAsHostBaseURLResolver_WithoutSuffix(t *testing.T) { + suffix := "" + r := FunctionAsHostBaseURLResolver{FunctionSuffix: suffix} + + req, _ := http.NewRequest(http.MethodGet, "http://localhost/function/hello", nil) + + resolved := r.Resolve(req) + want := fmt.Sprintf("http://hello%s:%d", suffix, watchdogPort) + + if resolved != want { + t.Logf("r.Resolve failed, want: %s got: %s", want, resolved) + t.Fail() + } +} diff --git a/gateway/handlers/forwarding_proxy.go b/gateway/handlers/forwarding_proxy.go index 8b85e60e..47af2ffa 100644 --- a/gateway/handlers/forwarding_proxy.go +++ b/gateway/handlers/forwarding_proxy.go @@ -2,6 +2,7 @@ package handlers import ( "context" + "fmt" "io" "log" "net/http" @@ -14,20 +15,22 @@ import ( "github.com/prometheus/client_golang/prometheus" ) +// HTTPNotifier notify about HTTP request/response type HTTPNotifier interface { Notify(method string, URL string, statusCode int, duration time.Duration) } +// BaseURLResolver URL resolver for upstream requests +type BaseURLResolver interface { + Resolve(r *http.Request) string +} + // MakeForwardingProxyHandler create a handler which forwards HTTP requests -func MakeForwardingProxyHandler(proxy *types.HTTPClientReverseProxy, notifiers []HTTPNotifier) http.HandlerFunc { - baseURL := proxy.BaseURL.String() - if strings.HasSuffix(baseURL, "/") { - baseURL = baseURL[0 : len(baseURL)-1] - } - +func MakeForwardingProxyHandler(proxy *types.HTTPClientReverseProxy, notifiers []HTTPNotifier, baseURLResolver BaseURLResolver) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + baseURL := baseURLResolver.Resolve(r) - requestURL := r.URL.String() + requestURL := r.URL.Path start := time.Now() @@ -90,10 +93,12 @@ func copyHeaders(destination http.Header, source *http.Header) { } } +// PrometheusFunctionNotifier records metrics to Prometheus type PrometheusFunctionNotifier struct { Metrics *metrics.MetricOptions } +// Notify records metrics in Prometheus func (p PrometheusFunctionNotifier) Notify(method string, URL string, statusCode int, duration time.Duration) { seconds := duration.Seconds() serviceName := getServiceName(URL) @@ -112,19 +117,52 @@ func (p PrometheusFunctionNotifier) Notify(method string, URL string, statusCode func getServiceName(urlValue string) string { var serviceName string forward := "/function/" - if startsWith(urlValue, forward) { + if strings.HasPrefix(urlValue, forward) { serviceName = urlValue[len(forward):] } return serviceName } -func startsWith(value, token string) bool { - return len(value) > len(token) && strings.Index(value, token) == 0 -} - +// LoggingNotifier notifies a log about a request type LoggingNotifier struct { } +// Notify a log about a request func (LoggingNotifier) Notify(method string, URL string, statusCode int, duration time.Duration) { log.Printf("Forwarded [%s] to %s - [%d] - %f seconds", method, URL, statusCode, duration.Seconds()) } + +// SingleHostBaseURLResolver resolves URLs against a single BaseURL +type SingleHostBaseURLResolver struct { + BaseURL string +} + +// Resolve the base URL for a request +func (s SingleHostBaseURLResolver) Resolve(r *http.Request) string { + + baseURL := s.BaseURL + + if strings.HasSuffix(baseURL, "/") { + baseURL = baseURL[0 : len(baseURL)-1] + } + return baseURL +} + +// FunctionAsHostBaseURLResolver resolves URLs using a function from the URL as a host +type FunctionAsHostBaseURLResolver struct { + FunctionSuffix string +} + +// Resolve the base URL for a request +func (f FunctionAsHostBaseURLResolver) Resolve(r *http.Request) string { + + svcName := getServiceName(r.URL.Path) + + const watchdogPort = 8080 + var suffix string + if len(f.FunctionSuffix) > 0 { + suffix = "." + f.FunctionSuffix + } + + return fmt.Sprintf("http://%s%s:%d", svcName, suffix, watchdogPort) +} diff --git a/gateway/handlers/proxy.go b/gateway/handlers/metrics.go similarity index 100% rename from gateway/handlers/proxy.go rename to gateway/handlers/metrics.go diff --git a/gateway/server.go b/gateway/server.go index 29b0a21c..112dac98 100644 --- a/gateway/server.go +++ b/gateway/server.go @@ -49,14 +49,23 @@ func main() { functionNotifiers := []handlers.HTTPNotifier{loggingNotifier, prometheusNotifier} forwardingNotifiers := []handlers.HTTPNotifier{loggingNotifier, prometheusNotifier} - faasHandlers.Proxy = handlers.MakeForwardingProxyHandler(reverseProxy, functionNotifiers) - faasHandlers.RoutelessProxy = handlers.MakeForwardingProxyHandler(reverseProxy, forwardingNotifiers) - faasHandlers.ListFunctions = handlers.MakeForwardingProxyHandler(reverseProxy, forwardingNotifiers) - faasHandlers.DeployFunction = handlers.MakeForwardingProxyHandler(reverseProxy, forwardingNotifiers) - faasHandlers.DeleteFunction = handlers.MakeForwardingProxyHandler(reverseProxy, forwardingNotifiers) - faasHandlers.UpdateFunction = handlers.MakeForwardingProxyHandler(reverseProxy, forwardingNotifiers) + urlResolver := handlers.SingleHostBaseURLResolver{BaseURL: config.FunctionsProviderURL.String()} + var functionURLResolver handlers.BaseURLResolver - queryFunction := handlers.MakeForwardingProxyHandler(reverseProxy, forwardingNotifiers) + if config.DirectFunctions { + functionURLResolver = handlers.FunctionAsHostBaseURLResolver{FunctionSuffix: config.DirectFunctionsSuffix} + } else { + functionURLResolver = urlResolver + } + + faasHandlers.Proxy = handlers.MakeForwardingProxyHandler(reverseProxy, functionNotifiers, functionURLResolver) + + faasHandlers.RoutelessProxy = handlers.MakeForwardingProxyHandler(reverseProxy, forwardingNotifiers, urlResolver) + faasHandlers.ListFunctions = handlers.MakeForwardingProxyHandler(reverseProxy, forwardingNotifiers, urlResolver) + faasHandlers.DeployFunction = handlers.MakeForwardingProxyHandler(reverseProxy, forwardingNotifiers, urlResolver) + faasHandlers.DeleteFunction = handlers.MakeForwardingProxyHandler(reverseProxy, forwardingNotifiers, urlResolver) + faasHandlers.UpdateFunction = handlers.MakeForwardingProxyHandler(reverseProxy, forwardingNotifiers, urlResolver) + queryFunction := handlers.MakeForwardingProxyHandler(reverseProxy, forwardingNotifiers, urlResolver) alertHandler := plugin.NewExternalServiceQuery(*config.FunctionsProviderURL) faasHandlers.Alert = handlers.MakeAlertHandler(alertHandler) diff --git a/gateway/tests/config_test.go b/gateway/tests/config_test.go index 0c9669a8..c5889556 100644 --- a/gateway/tests/config_test.go +++ b/gateway/tests/config_test.go @@ -38,6 +38,36 @@ func TestRead_UseExternalProvider_Defaults(t *testing.T) { t.Log("Default for UseExternalProvider should be false") t.Fail() } + + if config.DirectFunctions != false { + t.Log("Default for DirectFunctions should be false") + t.Fail() + } + + if len(config.DirectFunctionsSuffix) > 0 { + t.Log("Default for DirectFunctionsSuffix should be empty as a default") + t.Fail() + } +} + +func TestRead_DirectFunctionsOverride(t *testing.T) { + defaults := NewEnvBucket() + readConfig := types.ReadConfig{} + defaults.Setenv("direct_functions", "true") + wantSuffix := "openfaas-fn.cluster.local.svc." + defaults.Setenv("direct_functions_suffix", wantSuffix) + + config := readConfig.Read(defaults) + + if config.DirectFunctions != true { + t.Logf("DirectFunctions should be true, got: %v", config.DirectFunctions) + t.Fail() + } + + if config.DirectFunctionsSuffix != wantSuffix { + t.Logf("DirectFunctionsSuffix want: %s, got: %s", wantSuffix, config.DirectFunctionsSuffix) + t.Fail() + } } func TestRead_EmptyTimeoutConfig(t *testing.T) { diff --git a/gateway/types/readconfig.go b/gateway/types/readconfig.go index f52b8917..7eea2f1f 100644 --- a/gateway/types/readconfig.go +++ b/gateway/types/readconfig.go @@ -102,6 +102,9 @@ func (ReadConfig) Read(hasEnv HasEnv) GatewayConfig { cfg.PrometheusHost = prometheusHost } + cfg.DirectFunctions = parseBoolValue(hasEnv.Getenv("direct_functions")) + cfg.DirectFunctionsSuffix = hasEnv.Getenv("direct_functions_suffix") + return cfg } @@ -131,6 +134,12 @@ type GatewayConfig struct { // Port to connect to Prometheus. PrometheusPort int + + // If set to true we will access upstream functions directly rather than through the upstream provider + DirectFunctions bool + + // If set this will be used to resolve functions directly + DirectFunctionsSuffix string } // UseNATS Use NATSor not