diff --git a/gateway/build.sh b/gateway/build.sh index 4f071232..83924acd 100755 --- a/gateway/build.sh +++ b/gateway/build.sh @@ -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 diff --git a/gateway/handlers/baseurlresolver_test.go b/gateway/handlers/baseurlresolver_test.go index db32015d..318cba37 100644 --- a/gateway/handlers/baseurlresolver_test.go +++ b/gateway/handlers/baseurlresolver_test.go @@ -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() diff --git a/gateway/handlers/forwarding_proxy.go b/gateway/handlers/forwarding_proxy.go index b826e1f7..f270f59e 100644 --- a/gateway/handlers/forwarding_proxy.go +++ b/gateway/handlers/forwarding_proxy.go @@ -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) diff --git a/gateway/handlers/forwarding_proxy_test.go b/gateway/handlers/forwarding_proxy_test.go index ed0f15b1..237068d0 100644 --- a/gateway/handlers/forwarding_proxy_test.go +++ b/gateway/handlers/forwarding_proxy_test.go @@ -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/", diff --git a/gateway/handlers/function_prefix_trimming_url_path_transformer_test.go b/gateway/handlers/function_prefix_trimming_url_path_transformer_test.go index 9ecc358f..f344ba99 100644 --- a/gateway/handlers/function_prefix_trimming_url_path_transformer_test.go +++ b/gateway/handlers/function_prefix_trimming_url_path_transformer_test.go @@ -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) diff --git a/gateway/handlers/notifiers.go b/gateway/handlers/notifiers.go index 927cacf7..1154b75b 100644 --- a/gateway/handlers/notifiers.go +++ b/gateway/handlers/notifiers.go @@ -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. diff --git a/gateway/server.go b/gateway/server.go index b7814e95..18de83c4 100644 --- a/gateway/server.go +++ b/gateway/server.go @@ -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 diff --git a/gateway/types/readconfig.go b/gateway/types/readconfig.go index 89ffa7ab..dfb08ed9 100644 --- a/gateway/types/readconfig.go +++ b/gateway/types/readconfig.go @@ -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 diff --git a/gateway/types/readconfig_test.go b/gateway/types/readconfig_test.go index 13b6ed92..bfdda03a 100644 --- a/gateway/types/readconfig_test.go +++ b/gateway/types/readconfig_test.go @@ -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) }