Add feature for invoking namespaced functions

When coupled with the latest version of faas-netes, the gateway
can now invoke, query and deploy functions into alternative
namespaces.

Tested e2e by creating a namespace "fn" and deploying, then
invoking a function deployed there and in the default namespace.

Signed-off-by: Alex Ellis (OpenFaaS Ltd) <alexellis2@gmail.com>
This commit is contained in:
Alex Ellis (OpenFaaS Ltd) 2019-09-20 13:21:29 +01:00 committed by Alex Ellis
parent 0a90125aba
commit 238ce1be23
9 changed files with 198 additions and 47 deletions

View File

@ -42,4 +42,4 @@ docker build --build-arg https_proxy=$https_proxy --build-arg http_proxy=$http_p
--build-arg VERSION="${VERSION:-dev}" \
--build-arg GOARM="${GOARM}" \
--build-arg ARCH="${arch}" \
-t $NS/gateway:$eTAG . -f $dockerfile --no-cache
-t $NS/gateway:$eTAG . -f $dockerfile

View File

@ -5,8 +5,10 @@ package handlers
import (
"fmt"
"log"
"net/http"
"net/url"
"strings"
"testing"
)
@ -27,6 +29,28 @@ func TestSingleHostBaseURLResolver(t *testing.T) {
const watchdogPort = 8080
func TestFunctionAsHostBaseURLResolver_WithNamespaceOverride(t *testing.T) {
suffix := "openfaas-fn.local.cluster.svc."
namespace := "openfaas-fn"
newNS := "production-fn"
r := FunctionAsHostBaseURLResolver{FunctionSuffix: suffix, FunctionNamespace: namespace}
req, _ := http.NewRequest(http.MethodGet, "http://localhost/function/hello."+newNS, nil)
resolved := r.Resolve(req)
newSuffix := strings.Replace(suffix, namespace, newNS, -1)
want := fmt.Sprintf("http://hello.%s:%d", newSuffix, watchdogPort)
log.Println(want)
if resolved != want {
t.Logf("r.Resolve failed, want: %s got: %s", want, resolved)
t.Fail()
}
}
func TestFunctionAsHostBaseURLResolver_WithSuffix(t *testing.T) {
suffix := "openfaas-fn.local.cluster.svc."
r := FunctionAsHostBaseURLResolver{FunctionSuffix: suffix}
@ -35,7 +59,7 @@ func TestFunctionAsHostBaseURLResolver_WithSuffix(t *testing.T) {
resolved := r.Resolve(req)
want := fmt.Sprintf("http://hello.%s:%d", suffix, watchdogPort)
log.Println(want)
if resolved != want {
t.Logf("r.Resolve failed, want: %s got: %s", want, resolved)
t.Fail()

View File

@ -179,7 +179,8 @@ func (s SingleHostBaseURLResolver) Resolve(r *http.Request) string {
// FunctionAsHostBaseURLResolver resolves URLs using a function from the URL as a host
type FunctionAsHostBaseURLResolver struct {
FunctionSuffix string
FunctionSuffix string
FunctionNamespace string
}
// Resolve the base URL for a request
@ -188,8 +189,13 @@ func (f FunctionAsHostBaseURLResolver) Resolve(r *http.Request) string {
const watchdogPort = 8080
var suffix string
if len(f.FunctionSuffix) > 0 {
suffix = "." + f.FunctionSuffix
if index := strings.LastIndex(svcName, "."); index > -1 && len(svcName) > index+1 {
suffix = strings.Replace(f.FunctionSuffix, f.FunctionNamespace, "", -1)
} else {
suffix = "." + f.FunctionSuffix
}
}
return fmt.Sprintf("http://%s%s:%d", svcName, suffix, watchdogPort)

View File

@ -135,6 +135,11 @@ func Test_getServiceName(t *testing.T) {
url: "/function/testFunc",
serviceName: "testFunc",
},
{
name: "includes namespace",
url: "/function/test1.fn",
serviceName: "test1.fn",
},
{
name: "can handle request with trailing slash",
url: "/function/testFunc/",

View File

@ -32,6 +32,30 @@ func Test_Transform_RemovesFunctionPrefixWithSingleParam(t *testing.T) {
}
}
func Test_Transform_RemovesFunctionPrefixWithDotInName(t *testing.T) {
req, _ := http.NewRequest(http.MethodGet, "/function/figlet.fn", nil)
transformer := FunctionPrefixTrimmingURLPathTransformer{}
want := ""
got := transformer.Transform(req)
if want != got {
t.Errorf("want: %s, got: %s", want, got)
}
}
func Test_Transform_RemovesFunctionPrefixWithDotInNameAndPath(t *testing.T) {
req, _ := http.NewRequest(http.MethodGet, "/function/figlet.fn/employees", nil)
transformer := FunctionPrefixTrimmingURLPathTransformer{}
want := "/employees"
got := transformer.Transform(req)
if want != got {
t.Errorf("want: %s, got: %s", want, got)
}
}
func Test_Transform_RemovesFunctionPrefixWithParams(t *testing.T) {
req, _ := http.NewRequest(http.MethodGet, "/function/figlet/employees/100", nil)

View File

@ -66,7 +66,7 @@ func getServiceName(urlValue string) string {
forward := "/function/"
if strings.HasPrefix(urlValue, forward) {
// With a path like `/function/xyz/rest/of/path?q=a`, the service
// name we wish to locate is just the `xyz` portion. With a postive
// name we wish to locate is just the `xyz` portion. With a positive
// match on the regex below, it will return a three-element slice.
// The item at index `0` is the same as `urlValue`, at `1`
// will be the service name we need, and at `2` the rest of the path.

View File

@ -26,7 +26,11 @@ func main() {
osEnv := types.OsEnv{}
readConfig := types.ReadConfig{}
config := readConfig.Read(osEnv)
config, configErr := readConfig.Read(osEnv)
if configErr != nil {
log.Fatalln(configErr)
}
log.Printf("HTTP Read Timeout: %s", config.ReadTimeout)
log.Printf("HTTP Write Timeout: %s", config.WriteTimeout)
@ -85,7 +89,10 @@ func main() {
nilURLTransformer := handlers.TransparentURLPathTransformer{}
if config.DirectFunctions {
functionURLResolver = handlers.FunctionAsHostBaseURLResolver{FunctionSuffix: config.DirectFunctionsSuffix}
functionURLResolver = handlers.FunctionAsHostBaseURLResolver{
FunctionSuffix: config.DirectFunctionsSuffix,
FunctionNamespace: config.Namespace,
}
functionURLTransformer = handlers.FunctionPrefixTrimmingURLPathTransformer{}
} else {
functionURLResolver = urlResolver

View File

@ -4,10 +4,11 @@
package types
import (
"log"
"fmt"
"net/url"
"os"
"strconv"
"strings"
"time"
)
@ -52,7 +53,7 @@ func parseIntOrDurationValue(val string, fallback time.Duration) time.Duration {
}
// Read fetches gateway server configuration from environmental variables
func (ReadConfig) Read(hasEnv HasEnv) GatewayConfig {
func (ReadConfig) Read(hasEnv HasEnv) (*GatewayConfig, error) {
cfg := GatewayConfig{
PrometheusHost: "prometheus",
PrometheusPort: 9090,
@ -68,7 +69,7 @@ func (ReadConfig) Read(hasEnv HasEnv) GatewayConfig {
var err error
cfg.FunctionsProviderURL, err = url.Parse(hasEnv.Getenv("functions_provider_url"))
if err != nil {
log.Fatal("If functions_provider_url is provided, then it should be a valid URL.", err)
return nil, fmt.Errorf("if functions_provider_url is provided, then it should be a valid URL, error: %s", err)
}
}
@ -76,7 +77,7 @@ func (ReadConfig) Read(hasEnv HasEnv) GatewayConfig {
var err error
cfg.LogsProviderURL, err = url.Parse(hasEnv.Getenv("logs_provider_url"))
if err != nil {
log.Fatal("If logs_provider_url is provided, then it should be a valid URL.", err)
return nil, fmt.Errorf("if logs_provider_url is provided, then it should be a valid URL, error: %s", err)
}
} else if cfg.FunctionsProviderURL != nil {
cfg.LogsProviderURL, _ = url.Parse(cfg.FunctionsProviderURL.String())
@ -93,7 +94,7 @@ func (ReadConfig) Read(hasEnv HasEnv) GatewayConfig {
if err == nil {
cfg.NATSPort = &port
} else {
log.Println("faas_nats_port invalid number: " + faasNATSPort)
return nil, fmt.Errorf("faas_nats_port invalid number: %s", faasNATSPort)
}
}
@ -101,10 +102,10 @@ func (ReadConfig) Read(hasEnv HasEnv) GatewayConfig {
if len(prometheusPort) > 0 {
prometheusPortVal, err := strconv.Atoi(prometheusPort)
if err != nil {
log.Println("Invalid port for faas_prometheus_port")
} else {
cfg.PrometheusPort = prometheusPortVal
return nil, fmt.Errorf("faas_prometheus_port invalid number: %s", faasNATSPort)
}
cfg.PrometheusPort = prometheusPortVal
}
prometheusHost := hasEnv.Getenv("faas_prometheus_host")
@ -131,26 +132,34 @@ func (ReadConfig) Read(hasEnv HasEnv) GatewayConfig {
if len(maxIdleConns) > 0 {
val, err := strconv.Atoi(maxIdleConns)
if err != nil {
log.Println("Invalid value for max_idle_conns")
} else {
cfg.MaxIdleConns = val
return nil, fmt.Errorf("invalid value for max_idle_conns: %s", maxIdleConns)
}
cfg.MaxIdleConns = val
}
maxIdleConnsPerHost := hasEnv.Getenv("max_idle_conns_per_host")
if len(maxIdleConnsPerHost) > 0 {
val, err := strconv.Atoi(maxIdleConnsPerHost)
if err != nil {
log.Println("Invalid value for max_idle_conns_per_host")
} else {
cfg.MaxIdleConnsPerHost = val
return nil, fmt.Errorf("invalid value for max_idle_conns_per_host: %s", maxIdleConnsPerHost)
}
cfg.MaxIdleConnsPerHost = val
}
cfg.AuthProxyURL = hasEnv.Getenv("auth_proxy_url")
cfg.AuthProxyPassBody = parseBoolValue(hasEnv.Getenv("auth_proxy_pass_body"))
return cfg
cfg.Namespace = hasEnv.Getenv("function_namespace")
if len(cfg.DirectFunctionsSuffix) > 0 && len(cfg.Namespace) > 0 {
if strings.HasPrefix(cfg.DirectFunctionsSuffix, cfg.Namespace) == false {
return nil, fmt.Errorf("function_namespace must be a sub-string of direct_functions_suffix")
}
}
return &cfg, nil
}
// GatewayConfig provides config for the API Gateway server process
@ -209,6 +218,9 @@ type GatewayConfig struct {
// AuthProxyPassBody pass body to validation proxy
AuthProxyPassBody bool
// Namespace for endpoints
Namespace string
}
// UseNATS Use NATSor not

View File

@ -31,7 +31,7 @@ func TestRead_UseExternalProvider_Defaults(t *testing.T) {
defaults := NewEnvBucket()
readConfig := ReadConfig{}
config := readConfig.Read(defaults)
config, _ := readConfig.Read(defaults)
if config.UseExternalProvider() != false {
t.Log("Default for UseExternalProvider should be false")
@ -44,9 +44,77 @@ func TestRead_UseExternalProvider_Defaults(t *testing.T) {
}
if len(config.DirectFunctionsSuffix) > 0 {
t.Log("Default for DirectFunctionsSuffix should be empty as a default")
t.Log("Default for DirectFunctionsSuffix should be empty")
t.Fail()
}
if len(config.Namespace) > 0 {
t.Log("Default for Namespace should be empty")
t.Fail()
}
}
func TestRead_NamespaceOverride(t *testing.T) {
defaults := NewEnvBucket()
readConfig := ReadConfig{}
defaults.Setenv("function_namespace", "fn")
wantSuffix := "fn"
config, _ := readConfig.Read(defaults)
if config.Namespace != wantSuffix {
t.Logf("Namespace want: %s, got: %s", wantSuffix, config.Namespace)
t.Fail()
}
}
func TestRead_NamespaceOverrideAgressWithFunctionSuffix_Valid(t *testing.T) {
defaults := NewEnvBucket()
readConfig := ReadConfig{}
defaults.Setenv("direct_functions", "true")
wantSuffix := "openfaas-fn.cluster.local.svc."
defaults.Setenv("direct_functions_suffix", wantSuffix)
defaults.Setenv("function_namespace", "openfaas-fn")
_, err := readConfig.Read(defaults)
if err != nil {
t.Logf("Error found: %s", err)
t.Fail()
}
}
func TestRead_NamespaceOverrideAgressWithFunctionSuffix_Invalid(t *testing.T) {
defaults := NewEnvBucket()
readConfig := ReadConfig{}
defaults.Setenv("direct_functions", "true")
wantSuffix := "openfaas-fn.cluster.local.svc."
defaults.Setenv("direct_functions_suffix", wantSuffix)
defaults.Setenv("function_namespace", "fn")
_, err := readConfig.Read(defaults)
if err == nil {
t.Logf("Expected an error because function_namespace should be a sub-string of direct_functions_suffix")
t.Fail()
return
}
want := "function_namespace must be a sub-string of direct_functions_suffix"
if want != err.Error() {
t.Logf("Error want: %s, got: %s", want, err.Error())
t.Fail()
}
}
func TestRead_DirectFunctionsOverride(t *testing.T) {
@ -56,7 +124,7 @@ func TestRead_DirectFunctionsOverride(t *testing.T) {
wantSuffix := "openfaas-fn.cluster.local.svc."
defaults.Setenv("direct_functions_suffix", wantSuffix)
config := readConfig.Read(defaults)
config, _ := readConfig.Read(defaults)
if config.DirectFunctions != true {
t.Logf("DirectFunctions should be true, got: %v", config.DirectFunctions)
@ -73,7 +141,7 @@ func TestRead_ScaleZeroDefaultAndOverride(t *testing.T) {
defaults := NewEnvBucket()
readConfig := ReadConfig{}
// defaults.Setenv("scale_from_zero", "true")
config := readConfig.Read(defaults)
config, _ := readConfig.Read(defaults)
want := false
if config.ScaleFromZero != want {
@ -82,7 +150,7 @@ func TestRead_ScaleZeroDefaultAndOverride(t *testing.T) {
}
defaults.Setenv("scale_from_zero", "true")
config = readConfig.Read(defaults)
config, _ = readConfig.Read(defaults)
want = true
if config.ScaleFromZero != want {
@ -96,7 +164,7 @@ func TestRead_EmptyTimeoutConfig(t *testing.T) {
defaults := NewEnvBucket()
readConfig := ReadConfig{}
config := readConfig.Read(defaults)
config, _ := readConfig.Read(defaults)
if (config.ReadTimeout) != time.Duration(8)*time.Second {
t.Log("ReadTimeout incorrect")
@ -114,7 +182,7 @@ func TestRead_ReadAndWriteTimeoutConfig(t *testing.T) {
defaults.Setenv("write_timeout", "60")
readConfig := ReadConfig{}
config := readConfig.Read(defaults)
config, _ := readConfig.Read(defaults)
if (config.ReadTimeout) != time.Duration(10)*time.Second {
t.Logf("ReadTimeout incorrect, got: %d\n", config.ReadTimeout)
@ -132,7 +200,7 @@ func TestRead_ReadAndWriteTimeoutDurationConfig(t *testing.T) {
defaults.Setenv("write_timeout", "1m30s")
readConfig := ReadConfig{}
config := readConfig.Read(defaults)
config, _ := readConfig.Read(defaults)
if (config.ReadTimeout) != time.Duration(20)*time.Second {
t.Logf("ReadTimeout incorrect, got: %d\n", config.ReadTimeout)
@ -148,7 +216,7 @@ func TestRead_UseNATSDefaultsToOff(t *testing.T) {
defaults := NewEnvBucket()
readConfig := ReadConfig{}
config := readConfig.Read(defaults)
config, _ := readConfig.Read(defaults)
if config.UseNATS() == true {
t.Log("NATS is supposed to be off by default")
@ -162,7 +230,7 @@ func TestRead_UseNATS(t *testing.T) {
defaults.Setenv("faas_nats_port", "6222")
readConfig := ReadConfig{}
config := readConfig.Read(defaults)
config, _ := readConfig.Read(defaults)
if config.UseNATS() == false {
t.Log("NATS was requested in config, but not enabled.")
@ -177,12 +245,17 @@ func TestRead_UseNATSBadPort(t *testing.T) {
defaults.Setenv("faas_nats_port", "6fff")
readConfig := ReadConfig{}
config := readConfig.Read(defaults)
_, err := readConfig.Read(defaults)
if config.UseNATS() == true {
t.Log("NATS had bad config, should not be enabled.")
t.Fail()
if err != nil {
want := "faas_nats_port invalid number: 6fff"
if want != err.Error() {
t.Errorf("want error: %q, got: %q", want, err.Error())
t.Fail()
}
}
}
func TestRead_PrometheusNonDefaults(t *testing.T) {
@ -191,7 +264,7 @@ func TestRead_PrometheusNonDefaults(t *testing.T) {
defaults.Setenv("faas_prometheus_port", "9999")
readConfig := ReadConfig{}
config := readConfig.Read(defaults)
config, _ := readConfig.Read(defaults)
if config.PrometheusHost != "prom1" {
t.Logf("config.PrometheusHost, want: %s, got: %s\n", "prom1", config.PrometheusHost)
@ -209,7 +282,7 @@ func TestRead_PrometheusDefaults(t *testing.T) {
readConfig := ReadConfig{}
config := readConfig.Read(defaults)
config, _ := readConfig.Read(defaults)
if config.PrometheusHost != "prometheus" {
t.Logf("config.PrometheusHost, want: %s, got: %s\n", "prometheus", config.PrometheusHost)
@ -227,7 +300,7 @@ func TestRead_BasicAuthDefaults(t *testing.T) {
readConfig := ReadConfig{}
config := readConfig.Read(defaults)
config, _ := readConfig.Read(defaults)
if config.UseBasicAuth != false {
t.Logf("config.UseBasicAuth, want: %t, got: %t\n", false, config.UseBasicAuth)
@ -248,7 +321,7 @@ func TestRead_BasicAuth_SetTrue(t *testing.T) {
readConfig := ReadConfig{}
config := readConfig.Read(defaults)
config, _ := readConfig.Read(defaults)
if config.UseBasicAuth != true {
t.Logf("config.UseBasicAuth, want: %t, got: %t\n", true, config.UseBasicAuth)
@ -267,7 +340,7 @@ func TestRead_MaxIdleConnsDefaults(t *testing.T) {
readConfig := ReadConfig{}
config := readConfig.Read(defaults)
config, _ := readConfig.Read(defaults)
if config.MaxIdleConns != 1024 {
t.Logf("config.MaxIdleConns, want: %d, got: %d\n", 1024, config.MaxIdleConns)
@ -287,7 +360,7 @@ func TestRead_MaxIdleConns_Override(t *testing.T) {
defaults.Setenv("max_idle_conns", fmt.Sprintf("%d", 100))
defaults.Setenv("max_idle_conns_per_host", fmt.Sprintf("%d", 2))
config := readConfig.Read(defaults)
config, _ := readConfig.Read(defaults)
if config.MaxIdleConns != 100 {
t.Logf("config.MaxIdleConns, want: %d, got: %d\n", 100, config.MaxIdleConns)
@ -307,7 +380,7 @@ func TestRead_AuthProxy_Defaults(t *testing.T) {
wantURL := ""
wantBody := false
config := readConfig.Read(defaults)
config, _ := readConfig.Read(defaults)
if config.AuthProxyPassBody != wantBody {
t.Logf("config.AuthProxyPassBody, want: %t, got: %t\n", wantBody, config.AuthProxyPassBody)
@ -329,7 +402,7 @@ func TestRead_AuthProxy_DefaultsOverrides(t *testing.T) {
defaults.Setenv("auth_proxy_url", wantURL)
defaults.Setenv("auth_proxy_pass_body", fmt.Sprintf("%t", wantBody))
config := readConfig.Read(defaults)
config, _ := readConfig.Read(defaults)
if config.AuthProxyPassBody != wantBody {
t.Logf("config.AuthProxyPassBody, want: %t, got: %t\n", wantBody, config.AuthProxyPassBody)
@ -347,7 +420,7 @@ func TestRead_LogsProviderURL(t *testing.T) {
t.Run("default value is nil when functions_provider_url is empty", func(t *testing.T) {
readConfig := ReadConfig{}
config := readConfig.Read(defaults)
config, _ := readConfig.Read(defaults)
if config.LogsProviderURL != nil {
t.Fatalf("config.LogsProviderURL, want: %s, got: %s\n", "", config.LogsProviderURL)
}
@ -358,7 +431,7 @@ func TestRead_LogsProviderURL(t *testing.T) {
defaults.Setenv("functions_provider_url", expected)
readConfig := ReadConfig{}
config := readConfig.Read(defaults)
config, _ := readConfig.Read(defaults)
if config.LogsProviderURL.String() != expected {
t.Fatalf("config.LogsProviderURL, want: %s, got: %s\n", expected, config.LogsProviderURL)
}
@ -369,7 +442,7 @@ func TestRead_LogsProviderURL(t *testing.T) {
defaults.Setenv("logs_provider_url", expected)
readConfig := ReadConfig{}
config := readConfig.Read(defaults)
config, _ := readConfig.Read(defaults)
if config.LogsProviderURL.String() != expected {
t.Fatalf("config.LogsProviderURL, want: %s, got: %s\n", expected, config.LogsProviderURL)
}