package kubernetes import ( "errors" "fmt" "os" "testing" "time" "github.com/containous/traefik/provider/label" "github.com/containous/traefik/tls" "github.com/containous/traefik/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" extensionsv1beta1 "k8s.io/api/extensions/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" ) func TestLoadIngresses(t *testing.T) { ingresses := []*extensionsv1beta1.Ingress{ buildIngress( iNamespace("testing"), iRules( iRule(iHost("foo"), iPaths( onePath(iPath("/bar"), iBackend("service1", intstr.FromInt(80))), onePath(iPath("/namedthing"), iBackend("service4", intstr.FromString("https")))), ), iRule(iHost("bar"), iPaths( onePath(iBackend("service3", intstr.FromString("https"))), onePath(iBackend("service2", intstr.FromInt(802))), ), ), iRule(iHost("service5"), iPaths( onePath(iBackend("service5", intstr.FromInt(8888))), ), ), iRule(iHost("service6"), iPaths( onePath(iBackend("service6", intstr.FromInt(80))), ), ), ), ), } services := []*corev1.Service{ buildService( sName("service1"), sNamespace("testing"), sUID("1"), sSpec( clusterIP("10.0.0.1"), sPorts(sPort(80, ""))), ), buildService( sName("service2"), sNamespace("testing"), sUID("2"), sSpec( clusterIP("10.0.0.2"), sPorts(sPort(802, ""))), ), buildService( sName("service3"), sNamespace("testing"), sUID("3"), sSpec( clusterIP("10.0.0.3"), sPorts( sPort(80, "http"), sPort(443, "https")), ), ), buildService( sName("service4"), sNamespace("testing"), sUID("4"), sSpec( clusterIP("10.0.0.4"), sType("ExternalName"), sExternalName("example.com"), sPorts(sPort(443, "https"))), ), buildService( sName("service5"), sNamespace("testing"), sUID("5"), sSpec( clusterIP("10.0.0.5"), sType("ExternalName"), sExternalName("example.com"), sPorts(sPort(8888, "http"))), ), buildService( sName("service6"), sNamespace("testing"), sUID("6"), sSpec( clusterIP("10.0.0.6"), sPorts(sPort(80, ""))), ), } endpoints := []*corev1.Endpoints{ buildEndpoint( eNamespace("testing"), eName("service1"), eUID("1"), subset( eAddresses(eAddress("10.10.0.1")), ePorts(ePort(8080, ""))), subset( eAddresses(eAddress("10.21.0.1")), ePorts(ePort(8080, ""))), ), buildEndpoint( eNamespace("testing"), eName("service3"), eUID("3"), subset( eAddresses(eAddress("10.15.0.1")), ePorts( ePort(8080, "http"), ePort(8443, "https")), ), subset( eAddresses(eAddress("10.15.0.2")), ePorts( ePort(9080, "http"), ePort(9443, "https")), ), ), buildEndpoint( eNamespace("testing"), eName("service6"), eUID("6"), subset( eAddresses(eAddressWithTargetRef("http://10.15.0.3:80", "10.15.0.3")), ePorts(ePort(80, ""))), ), } watchChan := make(chan interface{}) client := clientMock{ ingresses: ingresses, services: services, endpoints: endpoints, watchChan: watchChan, } provider := Provider{} actual, err := provider.loadIngresses(client) require.NoError(t, err, "error loading ingresses") expected := buildConfiguration( backends( backend("foo/bar", lbMethod("wrr"), servers( server("http://10.10.0.1:8080", weight(1)), server("http://10.21.0.1:8080", weight(1))), ), backend("foo/namedthing", lbMethod("wrr"), servers( server("https://example.com", weight(1)), ), ), backend("bar", lbMethod("wrr"), servers( server("https://10.15.0.1:8443", weight(1)), server("https://10.15.0.2:9443", weight(1)), ), ), backend("service5", lbMethod("wrr"), servers( server("http://example.com:8888", weight(1)), ), ), backend("service6", lbMethod("wrr"), servers( server("http://10.15.0.3:80", weight(1)), ), ), ), frontends( frontend("foo/bar", passHostHeader(), routes( route("/bar", "PathPrefix:/bar"), route("foo", "Host:foo")), ), frontend("foo/namedthing", passHostHeader(), routes( route("/namedthing", "PathPrefix:/namedthing"), route("foo", "Host:foo")), ), frontend("bar", passHostHeader(), routes(route("bar", "Host:bar")), ), frontend("service5", passHostHeader(), routes(route("service5", "Host:service5")), ), frontend("service6", passHostHeader(), routes(route("service6", "Host:service6")), ), ), ) assert.Equal(t, expected, actual) } func TestRuleType(t *testing.T) { tests := []struct { desc string ingressRuleType string frontendRuleType string }{ { desc: "rule type annotation missing", ingressRuleType: "", frontendRuleType: ruleTypePathPrefix, }, { desc: "Path rule type annotation set", ingressRuleType: "Path", frontendRuleType: "Path", }, { desc: "PathStrip rule type annotation set", ingressRuleType: "PathStrip", frontendRuleType: "PathStrip", }, { desc: "PathStripPrefix rule type annotation set", ingressRuleType: "PathStripPrefix", frontendRuleType: "PathStripPrefix", }, } for _, test := range tests { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() ingress := buildIngress(iRules(iRule( iHost("host"), iPaths( onePath(iPath("/path"), iBackend("service", intstr.FromInt(80))), ), ))) if test.ingressRuleType != "" { ingress.Annotations = map[string]string{ annotationKubernetesRuleType: test.ingressRuleType, } } service := buildService( sName("service"), sUID("1"), sSpec(sPorts(sPort(801, "http"))), ) watchChan := make(chan interface{}) client := clientMock{ ingresses: []*extensionsv1beta1.Ingress{ingress}, services: []*corev1.Service{service}, watchChan: watchChan, } provider := Provider{DisablePassHostHeaders: true} actualConfig, err := provider.loadIngresses(client) require.NoError(t, err, "error loading ingresses") expected := buildFrontends(frontend("host/path", routes( route("/path", fmt.Sprintf("%s:/path", test.frontendRuleType)), route("host", "Host:host")), )) assert.Equal(t, expected, actualConfig.Frontends) }) } } func TestGetPassHostHeader(t *testing.T) { ingresses := []*extensionsv1beta1.Ingress{ buildIngress( iNamespace("awesome"), iRules(iRule( iHost("foo"), iPaths(onePath( iPath("/bar"), iBackend("service1", intstr.FromInt(801)))), )), ), } services := []*corev1.Service{ buildService( sNamespace("awesome"), sName("service1"), sUID("1"), sSpec(sPorts(sPort(801, "http"))), ), } watchChan := make(chan interface{}) client := clientMock{ ingresses: ingresses, services: services, watchChan: watchChan, } provider := Provider{DisablePassHostHeaders: true} actual, err := provider.loadIngresses(client) require.NoError(t, err, "error loading ingresses") expected := buildConfiguration( backends(backend("foo/bar", lbMethod("wrr"), servers())), frontends( frontend("foo/bar", routes( route("/bar", "PathPrefix:/bar"), route("foo", "Host:foo")), ), ), ) assert.Equal(t, expected, actual) } func TestGetPassTLSCert(t *testing.T) { ingresses := []*extensionsv1beta1.Ingress{ buildIngress(iNamespace("awesome"), iRules(iRule( iHost("foo"), iPaths(onePath(iPath("/bar"), iBackend("service1", intstr.FromInt(80))))), ), ), } services := []*corev1.Service{ buildService( sName("service1"), sNamespace("awesome"), sUID("1"), sSpec(sPorts(sPort(801, "http"))), ), } watchChan := make(chan interface{}) client := clientMock{ ingresses: ingresses, services: services, watchChan: watchChan, } provider := Provider{EnablePassTLSCert: true} actual, err := provider.loadIngresses(client) require.NoError(t, err, "error loading ingresses") expected := buildConfiguration( backends(backend("foo/bar", lbMethod("wrr"), servers())), frontends(frontend("foo/bar", passHostHeader(), passTLSCert(), routes( route("/bar", "PathPrefix:/bar"), route("foo", "Host:foo")), )), ) assert.Equal(t, expected, actual) } func TestOnlyReferencesServicesFromOwnNamespace(t *testing.T) { ingresses := []*extensionsv1beta1.Ingress{ buildIngress(iNamespace("awesome"), iRules(iRule( iHost("foo"), iPaths(onePath(iBackend("service", intstr.FromInt(80))))), ), ), } services := []*corev1.Service{ buildService( sNamespace("awesome"), sName("service"), sUID("1"), sSpec( clusterIP("10.0.0.1"), sPorts(sPort(80, "http"))), ), buildService( sNamespace("not-awesome"), sName("service"), sUID("2"), sSpec( clusterIP("10.0.0.2"), sPorts(sPort(80, "http"))), ), } watchChan := make(chan interface{}) client := clientMock{ ingresses: ingresses, services: services, watchChan: watchChan, } provider := Provider{} actual, err := provider.loadIngresses(client) require.NoError(t, err, "error loading ingresses") expected := buildConfiguration( backends(backend("foo", lbMethod("wrr"), servers())), frontends(frontend("foo", passHostHeader(), routes(route("foo", "Host:foo")), )), ) assert.Equal(t, expected, actual) } func TestHostlessIngress(t *testing.T) { ingresses := []*extensionsv1beta1.Ingress{ buildIngress(iNamespace("awesome"), iRules(iRule( iPaths(onePath(iPath("/bar"), iBackend("service1", intstr.FromInt(801))))), ), ), } services := []*corev1.Service{ buildService( sName("service1"), sNamespace("awesome"), sUID("1"), sSpec( clusterIP("10.0.0.1"), sPorts(sPort(801, "http"))), ), } watchChan := make(chan interface{}) client := clientMock{ ingresses: ingresses, services: services, watchChan: watchChan, } provider := Provider{DisablePassHostHeaders: true} actual, err := provider.loadIngresses(client) require.NoError(t, err, "error loading ingresses") expected := buildConfiguration( backends(backend("/bar", lbMethod("wrr"), servers())), frontends(frontend("/bar", routes(route("/bar", "PathPrefix:/bar")))), ) assert.Equal(t, expected, actual) } func TestServiceAnnotations(t *testing.T) { ingresses := []*extensionsv1beta1.Ingress{ buildIngress(iNamespace("testing"), iRules( iRule( iHost("foo"), iPaths(onePath(iPath("/bar"), iBackend("service1", intstr.FromInt(80))))), iRule( iHost("bar"), iPaths(onePath(iBackend("service2", intstr.FromInt(802))))), iRule( iHost("baz"), iPaths(onePath(iBackend("service3", intstr.FromInt(803))))), iRule( iHost("max-conn"), iPaths(onePath(iBackend("service4", intstr.FromInt(804))))), ), ), } services := []*corev1.Service{ buildService( sName("service1"), sNamespace("testing"), sUID("1"), sAnnotation(annotationKubernetesCircuitBreakerExpression, "NetworkErrorRatio() > 0.5"), sAnnotation(annotationKubernetesLoadBalancerMethod, "drr"), sSpec( clusterIP("10.0.0.1"), sPorts(sPort(80, ""))), ), buildService( sName("service2"), sNamespace("testing"), sUID("2"), sAnnotation(annotationKubernetesCircuitBreakerExpression, ""), sAnnotation(label.TraefikBackendLoadBalancerSticky, "true"), sSpec( clusterIP("10.0.0.2"), sPorts(sPort(802, ""))), ), buildService( sName("service3"), sNamespace("testing"), sUID("3"), sAnnotation(annotationKubernetesBuffering, ` maxrequestbodybytes: 10485760 memrequestbodybytes: 2097153 maxresponsebodybytes: 10485761 memresponsebodybytes: 2097152 retryexpression: IsNetworkError() && Attempts() <= 2 `), sSpec( clusterIP("10.0.0.3"), sPorts(sPort(803, "http"))), ), buildService( sName("service4"), sNamespace("testing"), sUID("4"), sAnnotation(annotationKubernetesMaxConnExtractorFunc, "client.ip"), sAnnotation(annotationKubernetesMaxConnAmount, "6"), sSpec( clusterIP("10.0.0.4"), sPorts(sPort(804, "http"))), ), } endpoints := []*corev1.Endpoints{ buildEndpoint( eNamespace("testing"), eName("service1"), eUID("1"), subset( eAddresses(eAddress("10.10.0.1")), ePorts(ePort(8080, ""))), subset( eAddresses(eAddress("10.21.0.1")), ePorts(ePort(8080, ""))), ), buildEndpoint( eNamespace("testing"), eName("service2"), eUID("2"), subset( eAddresses(eAddress("10.15.0.1")), ePorts(ePort(8080, ""))), subset( eAddresses(eAddress("10.15.0.2")), ePorts(ePort(8080, ""))), ), buildEndpoint( eNamespace("testing"), eName("service3"), eUID("3"), subset( eAddresses(eAddress("10.14.0.1")), ePorts(ePort(8080, "http"))), subset( eAddresses(eAddress("10.12.0.1")), ePorts(ePort(8080, "http"))), ), buildEndpoint( eNamespace("testing"), eName("service4"), eUID("4"), subset( eAddresses(eAddress("10.4.0.1")), ePorts(ePort(8080, "http"))), subset( eAddresses(eAddress("10.4.0.2")), ePorts(ePort(8080, "http"))), ), } watchChan := make(chan interface{}) client := clientMock{ ingresses: ingresses, services: services, endpoints: endpoints, watchChan: watchChan, } provider := Provider{} actual, err := provider.loadIngresses(client) require.NoError(t, err, "error loading ingresses") expected := buildConfiguration( backends( backend("foo/bar", servers( server("http://10.10.0.1:8080", weight(1)), server("http://10.21.0.1:8080", weight(1))), lbMethod("drr"), circuitBreaker("NetworkErrorRatio() > 0.5"), ), backend("bar", servers( server("http://10.15.0.1:8080", weight(1)), server("http://10.15.0.2:8080", weight(1))), lbMethod("wrr"), lbSticky(), ), backend("baz", servers( server("http://10.14.0.1:8080", weight(1)), server("http://10.12.0.1:8080", weight(1))), lbMethod("wrr"), buffering( maxRequestBodyBytes(10485760), memRequestBodyBytes(2097153), maxResponseBodyBytes(10485761), memResponseBodyBytes(2097152), retrying("IsNetworkError() && Attempts() <= 2"), ), ), backend("max-conn", servers( server("http://10.4.0.1:8080", weight(1)), server("http://10.4.0.2:8080", weight(1))), maxConnExtractorFunc("client.ip"), maxConnAmount(6), lbMethod("wrr"), ), ), frontends( frontend("foo/bar", passHostHeader(), routes( route("/bar", "PathPrefix:/bar"), route("foo", "Host:foo")), ), frontend("bar", passHostHeader(), routes(route("bar", "Host:bar"))), frontend("baz", passHostHeader(), routes(route("baz", "Host:baz"))), frontend("max-conn", passHostHeader(), routes( route("max-conn", "Host:max-conn"))), ), ) assert.EqualValues(t, expected, actual) } func TestIngressAnnotations(t *testing.T) { ingresses := []*extensionsv1beta1.Ingress{ buildIngress( iNamespace("testing"), iAnnotation(annotationKubernetesPreserveHost, "false"), iRules( iRule( iHost("foo"), iPaths(onePath(iPath("/bar"), iBackend("service1", intstr.FromInt(80))))), ), ), buildIngress( iNamespace("testing"), iAnnotation(annotationKubernetesPreserveHost, "true"), iAnnotation(annotationKubernetesIngressClass, traefikDefaultRealm), iRules( iRule( iHost("other"), iPaths(onePath(iPath("/stuff"), iBackend("service1", intstr.FromInt(80))))), ), ), buildIngress( iNamespace("testing"), iAnnotation(annotationKubernetesPassTLSCert, "true"), iAnnotation(annotationKubernetesIngressClass, traefikDefaultRealm), iRules( iRule( iHost("other"), iPaths(onePath(iPath("/sslstuff"), iBackend("service1", intstr.FromInt(80))))), ), ), buildIngress( iNamespace("testing"), iAnnotation(annotationKubernetesFrontendEntryPoints, "http,https"), iAnnotation(annotationKubernetesIngressClass, traefikDefaultRealm), iRules( iRule( iHost("other"), iPaths(onePath(iPath("/"), iBackend("service1", intstr.FromInt(80))))), ), ), buildIngress( iNamespace("testing"), iAnnotation(annotationKubernetesAuthType, "basic"), iAnnotation(annotationKubernetesAuthSecret, "mySecret"), iRules( iRule( iHost("basic"), iPaths(onePath(iPath("/auth"), iBackend("service1", intstr.FromInt(80))))), ), ), buildIngress( iNamespace("testing"), iAnnotation(annotationKubernetesIngressClass, traefikDefaultRealm+"-other"), iRules( iRule( iHost("herp"), iPaths(onePath(iPath("/derp"), iBackend("service2", intstr.FromInt(80))))), ), ), buildIngress( iNamespace("testing"), iAnnotation(annotationKubernetesWhiteListSourceRange, "1.1.1.1/24, 1234:abcd::42/32"), iAnnotation(annotationKubernetesWhiteListUseXForwardedFor, "true"), iRules( iRule( iHost("test"), iPaths(onePath(iPath("/whitelist-source-range"), iBackend("service1", intstr.FromInt(80))))), ), ), buildIngress( iNamespace("testing"), iAnnotation(annotationKubernetesRewriteTarget, "/"), iRules( iRule( iHost("rewrite"), iPaths(onePath(iPath("/api"), iBackend("service1", intstr.FromInt(80))))), ), ), buildIngress( iNamespace("testing"), iAnnotation(annotationKubernetesAuthRealm, "customized"), iRules( iRule( iHost("auth-realm-customized"), iPaths(onePath(iPath("/auth-realm-customized"), iBackend("service1", intstr.FromInt(80))))), ), ), buildIngress( iNamespace("testing"), iAnnotation(annotationKubernetesRedirectEntryPoint, "https"), iRules( iRule( iHost("redirect"), iPaths(onePath(iPath("/https"), iBackend("service1", intstr.FromInt(80))))), ), ), buildIngress( iNamespace("testing"), iAnnotation(annotationKubernetesIngressClass, "traefik"), iAnnotation(annotationKubernetesErrorPages, ` foo: status: - "123" - "456" backend: bar query: /bar `), iRules( iRule( iHost("error-pages"), iPaths(onePath(iPath("/errorpages"), iBackend("service1", intstr.FromInt(80))))), ), ), buildIngress( iNamespace("testing"), iAnnotation(annotationKubernetesIngressClass, "traefik"), iAnnotation(annotationKubernetesRateLimit, ` extractorfunc: client.ip rateset: bar: period: 3s average: 6 burst: 9 foo: period: 6s average: 12 burst: 18 `), iRules( iRule( iHost("rate-limit"), iPaths(onePath(iPath("/ratelimit"), iBackend("service1", intstr.FromInt(80))))), ), ), buildIngress( iNamespace("testing"), iAnnotation(annotationKubernetesAppRoot, "/root"), iRules( iRule( iHost("root"), iPaths( onePath(iPath("/"), iBackend("service1", intstr.FromInt(80))), onePath(iPath("/root1"), iBackend("service1", intstr.FromInt(80))), ), ), ), ), buildIngress( iNamespace("testing"), iAnnotation(annotationKubernetesAppRoot, "/root2"), iAnnotation(annotationKubernetesRewriteTarget, "/abc"), iRules( iRule( iHost("root2"), iPaths( onePath(iPath("/"), iBackend("service2", intstr.FromInt(80))), ), ), ), ), buildIngress( iNamespace("testing"), iAnnotation(annotationKubernetesRuleType, ruleTypeReplacePath), iAnnotation(annotationKubernetesRewriteTarget, "/abc"), iRules( iRule( iHost("root2"), iPaths( onePath(iPath("/"), iBackend("service2", intstr.FromInt(80))), ), ), ), ), buildIngress( iNamespace("testing"), iAnnotation(annotationKubernetesIngressClass, "traefik"), iAnnotation(annotationKubernetesCustomRequestHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), iAnnotation(annotationKubernetesCustomResponseHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), iAnnotation(annotationKubernetesSSLProxyHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), iAnnotation(annotationKubernetesAllowedHosts, "foo, fii, fuu"), iAnnotation(annotationKubernetesProxyHeaders, "foo, fii, fuu"), iAnnotation(annotationKubernetesHSTSMaxAge, "666"), iAnnotation(annotationKubernetesSSLForceHost, "true"), iAnnotation(annotationKubernetesSSLRedirect, "true"), iAnnotation(annotationKubernetesSSLTemporaryRedirect, "true"), iAnnotation(annotationKubernetesHSTSIncludeSubdomains, "true"), iAnnotation(annotationKubernetesForceHSTSHeader, "true"), iAnnotation(annotationKubernetesHSTSPreload, "true"), iAnnotation(annotationKubernetesFrameDeny, "true"), iAnnotation(annotationKubernetesContentTypeNosniff, "true"), iAnnotation(annotationKubernetesBrowserXSSFilter, "true"), iAnnotation(annotationKubernetesCustomBrowserXSSValue, "foo"), iAnnotation(annotationKubernetesIsDevelopment, "true"), iAnnotation(annotationKubernetesSSLHost, "foo"), iAnnotation(annotationKubernetesCustomFrameOptionsValue, "foo"), iAnnotation(annotationKubernetesContentSecurityPolicy, "foo"), iAnnotation(annotationKubernetesPublicKey, "foo"), iAnnotation(annotationKubernetesReferrerPolicy, "foo"), iRules( iRule( iHost("custom-headers"), iPaths(onePath(iPath("/customheaders"), iBackend("service1", intstr.FromInt(80))))), ), ), } services := []*corev1.Service{ buildService( sName("service1"), sNamespace("testing"), sUID("1"), sSpec( clusterIP("10.0.0.1"), sType("ExternalName"), sExternalName("example.com"), sPorts(sPort(80, "http"))), ), buildService( sName("service2"), sNamespace("testing"), sUID("2"), sSpec( clusterIP("10.0.0.2"), sPorts(sPort(802, ""))), ), } secrets := []*corev1.Secret{ { ObjectMeta: metav1.ObjectMeta{ Name: "mySecret", UID: "1", Namespace: "testing", }, Data: map[string][]byte{"auth": []byte("myUser:myEncodedPW")}, }, } watchChan := make(chan interface{}) client := clientMock{ ingresses: ingresses, services: services, secrets: secrets, watchChan: watchChan, } provider := Provider{} actual, err := provider.loadIngresses(client) require.NoError(t, err, "error loading ingresses") expected := buildConfiguration( backends( backend("foo/bar", servers( server("http://example.com", weight(1)), server("http://example.com", weight(1))), lbMethod("wrr"), ), backend("other/stuff", servers( server("http://example.com", weight(1)), server("http://example.com", weight(1))), lbMethod("wrr"), ), backend("other/", servers( server("http://example.com", weight(1)), server("http://example.com", weight(1))), lbMethod("wrr"), ), backend("other/sslstuff", servers( server("http://example.com", weight(1)), server("http://example.com", weight(1))), lbMethod("wrr"), ), backend("basic/auth", servers( server("http://example.com", weight(1)), server("http://example.com", weight(1))), lbMethod("wrr"), ), backend("redirect/https", servers( server("http://example.com", weight(1)), server("http://example.com", weight(1))), lbMethod("wrr"), ), backend("test/whitelist-source-range", servers( server("http://example.com", weight(1)), server("http://example.com", weight(1))), lbMethod("wrr"), ), backend("rewrite/api", servers( server("http://example.com", weight(1)), server("http://example.com", weight(1))), lbMethod("wrr"), ), backend("error-pages/errorpages", servers( server("http://example.com", weight(1)), server("http://example.com", weight(1))), lbMethod("wrr"), ), backend("rate-limit/ratelimit", servers( server("http://example.com", weight(1)), server("http://example.com", weight(1))), lbMethod("wrr"), ), backend("custom-headers/customheaders", servers( server("http://example.com", weight(1)), server("http://example.com", weight(1))), lbMethod("wrr"), ), backend("root/", servers( server("http://example.com", weight(1))), lbMethod("wrr"), ), backend("root/root1", servers( server("http://example.com", weight(1))), lbMethod("wrr"), ), backend("root2/", servers(), lbMethod("wrr"), ), ), frontends( frontend("foo/bar", routes( route("/bar", "PathPrefix:/bar"), route("foo", "Host:foo")), ), frontend("other/stuff", passHostHeader(), routes( route("/stuff", "PathPrefix:/stuff"), route("other", "Host:other")), ), frontend("other/", passHostHeader(), entryPoints("http", "https"), routes( route("/", "PathPrefix:/"), route("other", "Host:other")), ), frontend("other/sslstuff", passHostHeader(), passTLSCert(), routes( route("/sslstuff", "PathPrefix:/sslstuff"), route("other", "Host:other")), ), frontend("other/sslstuff", passHostHeader(), passTLSCert(), routes( route("/sslstuff", "PathPrefix:/sslstuff"), route("other", "Host:other")), ), frontend("basic/auth", passHostHeader(), basicAuthDeprecated("myUser:myEncodedPW"), routes( route("/auth", "PathPrefix:/auth"), route("basic", "Host:basic")), ), frontend("redirect/https", passHostHeader(), redirectEntryPoint("https"), routes( route("/https", "PathPrefix:/https"), route("redirect", "Host:redirect")), ), frontend("test/whitelist-source-range", passHostHeader(), whiteList(true, "1.1.1.1/24", "1234:abcd::42/32"), routes( route("/whitelist-source-range", "PathPrefix:/whitelist-source-range"), route("test", "Host:test")), ), frontend("rewrite/api", passHostHeader(), routes( route("/api", "PathPrefix:/api;ReplacePath:/"), route("rewrite", "Host:rewrite")), ), frontend("error-pages/errorpages", passHostHeader(), errorPage("foo", errorQuery("/bar"), errorStatus("123", "456"), errorBackend("bar")), routes( route("/errorpages", "PathPrefix:/errorpages"), route("error-pages", "Host:error-pages")), ), frontend("rate-limit/ratelimit", passHostHeader(), rateLimit(rateExtractorFunc("client.ip"), rateSet("foo", limitPeriod(6*time.Second), limitAverage(12), limitBurst(18)), rateSet("bar", limitPeriod(3*time.Second), limitAverage(6), limitBurst(9))), routes( route("/ratelimit", "PathPrefix:/ratelimit"), route("rate-limit", "Host:rate-limit")), ), frontend("custom-headers/customheaders", passHostHeader(), headers(&types.Headers{ CustomRequestHeaders: map[string]string{ "Access-Control-Allow-Methods": "POST,GET,OPTIONS", "Content-Type": "application/json; charset=utf-8", }, CustomResponseHeaders: map[string]string{ "Access-Control-Allow-Methods": "POST,GET,OPTIONS", "Content-Type": "application/json; charset=utf-8", }, SSLProxyHeaders: map[string]string{ "Access-Control-Allow-Methods": "POST,GET,OPTIONS", "Content-Type": "application/json; charset=utf-8", }, AllowedHosts: []string{"foo", "fii", "fuu"}, HostsProxyHeaders: []string{"foo", "fii", "fuu"}, STSSeconds: 666, SSLForceHost: true, SSLRedirect: true, SSLTemporaryRedirect: true, STSIncludeSubdomains: true, STSPreload: true, ForceSTSHeader: true, FrameDeny: true, ContentTypeNosniff: true, BrowserXSSFilter: true, IsDevelopment: true, CustomFrameOptionsValue: "foo", SSLHost: "foo", ContentSecurityPolicy: "foo", PublicKey: "foo", ReferrerPolicy: "foo", CustomBrowserXSSValue: "foo", }), routes( route("/customheaders", "PathPrefix:/customheaders"), route("custom-headers", "Host:custom-headers")), ), frontend("root/", passHostHeader(), routes( route("/", "PathPrefix:/;ReplacePath:/root"), route("root", "Host:root"), ), ), frontend("root/root1", passHostHeader(), routes( route("/root1", "PathPrefix:/root1"), route("root", "Host:root"), ), ), ), ) assert.Equal(t, expected, actual) } func TestIngressClassAnnotation(t *testing.T) { ingresses := []*extensionsv1beta1.Ingress{ buildIngress( iNamespace("testing"), iAnnotation(annotationKubernetesIngressClass, traefikDefaultIngressClass), iRules( iRule( iHost("other"), iPaths(onePath(iPath("/stuff"), iBackend("service1", intstr.FromInt(80))))), ), ), buildIngress( iNamespace("testing"), iAnnotation(annotationKubernetesIngressClass, ""), iRules( iRule( iHost("other"), iPaths(onePath(iPath("/sslstuff"), iBackend("service1", intstr.FromInt(80))))), ), ), buildIngress( iNamespace("testing"), iRules( iRule( iHost("other"), iPaths(onePath(iPath("/"), iBackend("service1", intstr.FromInt(80))))), ), ), buildIngress( iNamespace("testing"), iAnnotation(annotationKubernetesIngressClass, traefikDefaultIngressClass+"-other"), iRules( iRule( iHost("foo"), iPaths(onePath(iPath("/bar"), iBackend("service1", intstr.FromInt(80))))), ), ), buildIngress( iNamespace("testing"), iAnnotation(annotationKubernetesIngressClass, "custom"), iRules( iRule( iHost("foo"), iPaths(onePath(iPath("/bar"), iBackend("service2", intstr.FromInt(80))))), ), ), } services := []*corev1.Service{ buildService( sName("service1"), sNamespace("testing"), sUID("1"), sSpec( clusterIP("10.0.0.1"), sType("ExternalName"), sExternalName("example.com"), sPorts(sPort(80, "http"))), ), buildService( sName("service2"), sNamespace("testing"), sUID("2"), sSpec( clusterIP("10.0.0.2"), sPorts(sPort(80, "http"))), ), } endpoints := []*corev1.Endpoints{ buildEndpoint( eName("service2"), eUID("1"), eNamespace("testing"), subset( eAddresses(eAddress("10.10.0.1")), ePorts(ePort(80, "http"))), ), } watchChan := make(chan interface{}) client := clientMock{ ingresses: ingresses, services: services, endpoints: endpoints, watchChan: watchChan, } testCases := []struct { desc string provider Provider expected *types.Configuration }{ { desc: "Empty IngressClass annotation", provider: Provider{}, expected: buildConfiguration( backends( backend("other/stuff", servers( server("http://example.com", weight(1)), server("http://example.com", weight(1))), lbMethod("wrr"), ), backend("other/", servers( server("http://example.com", weight(1)), server("http://example.com", weight(1))), lbMethod("wrr"), ), backend("other/sslstuff", servers( server("http://example.com", weight(1)), server("http://example.com", weight(1))), lbMethod("wrr"), ), ), frontends( frontend("other/stuff", passHostHeader(), routes( route("/stuff", "PathPrefix:/stuff"), route("other", "Host:other")), ), frontend("other/", passHostHeader(), routes( route("/", "PathPrefix:/"), route("other", "Host:other")), ), frontend("other/sslstuff", passHostHeader(), routes( route("/sslstuff", "PathPrefix:/sslstuff"), route("other", "Host:other")), ), ), ), }, { desc: "Provided IngressClass annotation", provider: Provider{IngressClass: traefikDefaultRealm + "-other"}, expected: buildConfiguration( backends( backend("foo/bar", servers( server("http://example.com", weight(1))), lbMethod("wrr"), ), ), frontends( frontend("foo/bar", passHostHeader(), routes( route("/bar", "PathPrefix:/bar"), route("foo", "Host:foo")), ), ), ), }, { desc: "Provided IngressClass annotation", provider: Provider{IngressClass: "custom"}, expected: buildConfiguration( backends( backend("foo/bar", servers( server("http://10.10.0.1:80", weight(1))), lbMethod("wrr"), ), ), frontends( frontend("foo/bar", passHostHeader(), routes( route("/bar", "PathPrefix:/bar"), route("foo", "Host:foo")), ), ), ), }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() actual, err := test.provider.loadIngresses(client) require.NoError(t, err, "error loading ingresses") assert.Equal(t, test.expected, actual) }) } } func TestPriorityHeaderValue(t *testing.T) { ingresses := []*extensionsv1beta1.Ingress{ buildIngress( iNamespace("testing"), iAnnotation(annotationKubernetesPriority, "1337"), iRules( iRule( iHost("foo"), iPaths(onePath(iPath("/bar"), iBackend("service1", intstr.FromInt(80))))), ), ), } services := []*corev1.Service{ buildService( sName("service1"), sNamespace("testing"), sUID("1"), sSpec( clusterIP("10.0.0.1"), sType("ExternalName"), sExternalName("example.com"), sPorts(sPort(80, "http"))), ), } var endpoints []*corev1.Endpoints watchChan := make(chan interface{}) client := clientMock{ ingresses: ingresses, services: services, endpoints: endpoints, watchChan: watchChan, } provider := Provider{} actual, err := provider.loadIngresses(client) require.NoError(t, err, "error loading ingresses") expected := buildConfiguration( backends( backend("foo/bar", servers(server("http://example.com", weight(1))), lbMethod("wrr"), ), ), frontends( frontend("foo/bar", passHostHeader(), priority(1337), routes( route("/bar", "PathPrefix:/bar"), route("foo", "Host:foo")), ), ), ) assert.Equal(t, expected, actual) } func TestInvalidPassTLSCertValue(t *testing.T) { ingresses := []*extensionsv1beta1.Ingress{ buildIngress( iNamespace("testing"), iAnnotation(annotationKubernetesPassTLSCert, "herpderp"), iRules( iRule( iHost("foo"), iPaths(onePath(iPath("/bar"), iBackend("service1", intstr.FromInt(80))))), ), ), } services := []*corev1.Service{ buildService( sName("service1"), sNamespace("testing"), sUID("1"), sSpec( clusterIP("10.0.0.1"), sType("ExternalName"), sExternalName("example.com"), sPorts(sPort(80, "http"))), ), } watchChan := make(chan interface{}) client := clientMock{ ingresses: ingresses, services: services, watchChan: watchChan, } provider := Provider{} actual, err := provider.loadIngresses(client) require.NoError(t, err, "error loading ingresses") expected := buildConfiguration( backends( backend("foo/bar", servers(server("http://example.com", weight(1))), lbMethod("wrr"), ), ), frontends( frontend("foo/bar", passHostHeader(), routes( route("/bar", "PathPrefix:/bar"), route("foo", "Host:foo")), ), ), ) assert.Equal(t, expected, actual) } func TestInvalidPassHostHeaderValue(t *testing.T) { ingresses := []*extensionsv1beta1.Ingress{ buildIngress( iNamespace("testing"), iAnnotation(annotationKubernetesPreserveHost, "herpderp"), iRules( iRule( iHost("foo"), iPaths(onePath(iPath("/bar"), iBackend("service1", intstr.FromInt(80))))), ), ), } services := []*corev1.Service{ buildService( sName("service1"), sNamespace("testing"), sUID("1"), sSpec( clusterIP("10.0.0.1"), sType("ExternalName"), sExternalName("example.com"), sPorts(sPort(80, "http"))), ), } watchChan := make(chan interface{}) client := clientMock{ ingresses: ingresses, services: services, watchChan: watchChan, } provider := Provider{} actual, err := provider.loadIngresses(client) require.NoError(t, err, "error loading ingresses") expected := buildConfiguration( backends( backend("foo/bar", servers(server("http://example.com", weight(1))), lbMethod("wrr"), ), ), frontends( frontend("foo/bar", passHostHeader(), routes( route("/bar", "PathPrefix:/bar"), route("foo", "Host:foo")), ), ), ) assert.Equal(t, expected, actual) } func TestKubeAPIErrors(t *testing.T) { ingresses := []*extensionsv1beta1.Ingress{ buildIngress( iNamespace("testing"), iRules( iRule( iHost("foo"), iPaths(onePath(iPath("/bar"), iBackend("service1", intstr.FromInt(80))))), ), ), } services := []*corev1.Service{ buildService( sName("service1"), sNamespace("testing"), sUID("1"), sSpec( clusterIP("10.0.0.1"), sPorts(sPort(80, ""))), ), } watchChan := make(chan interface{}) apiErr := errors.New("failed kube api call") testCases := []struct { desc string apiServiceErr error apiEndpointsErr error }{ { desc: "failed service call", apiServiceErr: apiErr, }, { desc: "failed endpoints call", apiEndpointsErr: apiErr, }, } for _, tc := range testCases { tc := tc t.Run(tc.desc, func(t *testing.T) { t.Parallel() client := clientMock{ ingresses: ingresses, services: services, watchChan: watchChan, apiServiceError: tc.apiServiceErr, apiEndpointsError: tc.apiEndpointsErr, } provider := Provider{} if _, err := provider.loadIngresses(client); err != apiErr { t.Errorf("Got error %v, wanted error %v", err, apiErr) } }) } } func TestMissingResources(t *testing.T) { ingresses := []*extensionsv1beta1.Ingress{ buildIngress( iNamespace("testing"), iRules( iRule( iHost("fully_working"), iPaths(onePath(iBackend("fully_working_service", intstr.FromInt(80))))), iRule( iHost("missing_service"), iPaths(onePath(iBackend("missing_service_service", intstr.FromInt(80))))), iRule( iHost("missing_endpoints"), iPaths(onePath(iBackend("missing_endpoints_service", intstr.FromInt(80))))), iRule( iHost("missing_endpoint_subsets"), iPaths(onePath(iBackend("missing_endpoint_subsets_service", intstr.FromInt(80))))), ), ), } services := []*corev1.Service{ buildService( sName("fully_working_service"), sNamespace("testing"), sUID("1"), sSpec( clusterIP("10.0.0.1"), sPorts(sPort(80, ""))), ), buildService( sName("missing_endpoints_service"), sNamespace("testing"), sUID("3"), sSpec( clusterIP("10.0.0.3"), sPorts(sPort(80, ""))), ), buildService( sName("missing_endpoint_subsets_service"), sNamespace("testing"), sUID("4"), sSpec( clusterIP("10.0.0.4"), sPorts(sPort(80, ""))), ), } endpoints := []*corev1.Endpoints{ buildEndpoint( eName("fully_working_service"), eUID("1"), eNamespace("testing"), subset( eAddresses(eAddress("10.10.0.1")), ePorts(ePort(8080, ""))), ), buildEndpoint( eName("missing_endpoint_subsets_service"), eUID("4"), eNamespace("testing"), ), } watchChan := make(chan interface{}) client := clientMock{ ingresses: ingresses, services: services, endpoints: endpoints, watchChan: watchChan, } provider := Provider{} actual, err := provider.loadIngresses(client) require.NoError(t, err, "error loading ingresses") expected := buildConfiguration( backends( backend("fully_working", servers(server("http://10.10.0.1:8080", weight(1))), lbMethod("wrr"), ), backend("missing_service", servers(), lbMethod("wrr"), ), backend("missing_endpoints", servers(), lbMethod("wrr"), ), backend("missing_endpoint_subsets", servers(), lbMethod("wrr"), ), ), frontends( frontend("fully_working", passHostHeader(), routes(route("fully_working", "Host:fully_working")), ), frontend("missing_endpoints", passHostHeader(), routes(route("missing_endpoints", "Host:missing_endpoints")), ), frontend("missing_endpoint_subsets", passHostHeader(), routes(route("missing_endpoint_subsets", "Host:missing_endpoint_subsets")), ), ), ) assert.Equal(t, expected, actual) } func TestLoadIngressesBasicAuth(t *testing.T) { ingresses := []*extensionsv1beta1.Ingress{ buildIngress( iNamespace("testing"), iAnnotation(annotationKubernetesAuthType, "basic"), iAnnotation(annotationKubernetesAuthSecret, "mySecret"), iRules( iRule( iHost("basic"), iPaths(onePath(iPath("/auth"), iBackend("service1", intstr.FromInt(80))))), ), ), } services := []*corev1.Service{ buildService( sName("service1"), sNamespace("testing"), sUID("1"), sSpec( clusterIP("10.0.0.1"), sType("ExternalName"), sExternalName("example.com"), sPorts(sPort(80, "http"))), ), } secrets := []*corev1.Secret{{ ObjectMeta: metav1.ObjectMeta{ Name: "mySecret", UID: "1", Namespace: "testing", }, Data: map[string][]byte{ "auth": []byte("myUser:myEncodedPW"), }, }} var endpoints []*corev1.Endpoints watchChan := make(chan interface{}) client := clientMock{ ingresses: ingresses, services: services, secrets: secrets, endpoints: endpoints, watchChan: watchChan, } provider := Provider{} actual, err := provider.loadIngresses(client) require.NoError(t, err, "error loading ingresses") actual = provider.loadConfig(*actual) require.NotNil(t, actual) got := actual.Frontends["basic/auth"].Auth.Basic.Users assert.Equal(t, types.Users{"myUser:myEncodedPW"}, got) } func TestLoadIngressesForwardAuth(t *testing.T) { ingresses := []*extensionsv1beta1.Ingress{ buildIngress( iNamespace("testing"), iAnnotation(annotationKubernetesAuthType, "forward"), iAnnotation(annotationKubernetesAuthForwardURL, "https://auth.host"), iAnnotation(annotationKubernetesAuthForwardTrustHeaders, "true"), iAnnotation(annotationKubernetesAuthForwardResponseHeaders, "X-Auth,X-Test,X-Secret"), iRules( iRule(iHost("foo"), iPaths( onePath(iPath("/bar"), iBackend("service1", intstr.FromInt(80))))), ), ), } services := []*corev1.Service{ buildService( sName("service1"), sNamespace("testing"), sUID("1"), sSpec( clusterIP("10.0.0.1"), sPorts(sPort(80, ""))), ), } endpoints := []*corev1.Endpoints{ buildEndpoint( eNamespace("testing"), eName("service1"), eUID("1"), subset( eAddresses(eAddress("10.10.0.1")), ePorts(ePort(8080, ""))), ), } watchChan := make(chan interface{}) client := clientMock{ ingresses: ingresses, services: services, endpoints: endpoints, watchChan: watchChan, } provider := Provider{} actual, err := provider.loadIngresses(client) require.NoError(t, err, "error loading ingresses") expected := buildConfiguration( backends( backend("foo/bar", lbMethod("wrr"), servers( server("http://10.10.0.1:8080", weight(1))), ), ), frontends( frontend("foo/bar", passHostHeader(), auth(forwardAuth("https://auth.host", fwdTrustForwardHeader(), fwdAuthResponseHeaders("X-Auth", "X-Test", "X-Secret"))), routes( route("/bar", "PathPrefix:/bar"), route("foo", "Host:foo")), ), ), ) assert.Equal(t, expected, actual) } func TestLoadIngressesForwardAuthMissingURL(t *testing.T) { ingresses := []*extensionsv1beta1.Ingress{ buildIngress( iNamespace("testing"), iAnnotation(annotationKubernetesAuthType, "forward"), iRules( iRule(iHost("foo"), iPaths( onePath(iPath("/bar"), iBackend("service1", intstr.FromInt(80))))), ), ), } services := []*corev1.Service{ buildService( sName("service1"), sNamespace("testing"), sUID("1"), sSpec( clusterIP("10.0.0.1"), sPorts(sPort(80, ""))), ), } endpoints := []*corev1.Endpoints{ buildEndpoint( eNamespace("testing"), eName("service1"), eUID("1"), subset( eAddresses(eAddress("10.10.0.1")), ePorts(ePort(8080, ""))), ), } watchChan := make(chan interface{}) client := clientMock{ ingresses: ingresses, services: services, endpoints: endpoints, watchChan: watchChan, } provider := Provider{} actual, err := provider.loadIngresses(client) require.NoError(t, err, "error loading ingresses") expected := buildConfiguration( backends( backend("foo/bar", lbMethod("wrr"), servers(), ), ), frontends(), ) assert.Equal(t, expected, actual) } func TestLoadIngressesForwardAuthWithTLSSecret(t *testing.T) { ingresses := []*extensionsv1beta1.Ingress{ buildIngress( iNamespace("testing"), iAnnotation(annotationKubernetesAuthType, "forward"), iAnnotation(annotationKubernetesAuthForwardURL, "https://auth.host"), iAnnotation(annotationKubernetesAuthForwardTLSSecret, "secret"), iAnnotation(annotationKubernetesAuthForwardTLSInsecure, "true"), iRules( iRule(iHost("foo"), iPaths( onePath(iPath("/bar"), iBackend("service1", intstr.FromInt(80))))), ), ), } secrets := []*corev1.Secret{{ ObjectMeta: metav1.ObjectMeta{ Name: "secret", UID: "1", Namespace: "testing", }, Data: map[string][]byte{ "tls.crt": []byte("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"), "tls.key": []byte("-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----"), }, }} services := []*corev1.Service{ buildService( sName("service1"), sNamespace("testing"), sUID("1"), sSpec( clusterIP("10.0.0.1"), sPorts(sPort(80, ""))), ), } endpoints := []*corev1.Endpoints{ buildEndpoint( eNamespace("testing"), eName("service1"), eUID("1"), subset( eAddresses(eAddress("10.10.0.1")), ePorts(ePort(8080, ""))), ), } watchChan := make(chan interface{}) client := clientMock{ ingresses: ingresses, services: services, endpoints: endpoints, secrets: secrets, watchChan: watchChan, } provider := Provider{} actual, err := provider.loadIngresses(client) require.NoError(t, err, "error loading ingresses") expected := buildConfiguration( backends( backend("foo/bar", lbMethod("wrr"), servers( server("http://10.10.0.1:8080", weight(1))), ), ), frontends( frontend("foo/bar", passHostHeader(), auth( forwardAuth("https://auth.host", fwdAuthTLS( "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----", "-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----", true))), routes( route("/bar", "PathPrefix:/bar"), route("foo", "Host:foo")), ), ), ) assert.Equal(t, expected, actual) } func TestLoadIngressesForwardAuthWithTLSSecretFailures(t *testing.T) { tests := []struct { desc string secretName string certName string certData string keyName string keyData string }{ { desc: "empty certificate and key", secretName: "secret", certName: "", certData: "", keyName: "", keyData: "", }, { desc: "wrong secret name, empty certificate and key", secretName: "wrongSecret", certName: "", certData: "", keyName: "", keyData: "", }, { desc: "empty certificate data", secretName: "secret", certName: "tls.crt", certData: "", keyName: "tls.key", keyData: "-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----", }, { desc: "empty key data", secretName: "secret", certName: "tls.crt", certData: "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----", keyName: "tls.key", keyData: "", }, { desc: "wrong cert name", secretName: "secret", certName: "cert.crt", certData: "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE----", keyName: "tls.key", keyData: "-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----", }, { desc: "wrong key name", secretName: "secret", certName: "tls.crt", certData: "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----", keyName: "cert.key", keyData: "-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----", }, } ingresses := []*extensionsv1beta1.Ingress{ buildIngress( iNamespace("testing"), iAnnotation(annotationKubernetesAuthType, "forward"), iAnnotation(annotationKubernetesAuthForwardURL, "https://auth.host"), iAnnotation(annotationKubernetesAuthForwardTLSSecret, "secret"), iRules( iRule(iHost("foo"), iPaths( onePath(iPath("/bar"), iBackend("service1", intstr.FromInt(80))))), ), ), } services := []*corev1.Service{ buildService( sName("service1"), sNamespace("testing"), sUID("1"), sSpec( clusterIP("10.0.0.1"), sPorts(sPort(80, ""))), ), } endpoints := []*corev1.Endpoints{ buildEndpoint( eNamespace("testing"), eName("service1"), eUID("1"), subset( eAddresses(eAddress("10.10.0.1")), ePorts(ePort(8080, ""))), ), } for _, test := range tests { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() secrets := []*corev1.Secret{{ ObjectMeta: metav1.ObjectMeta{ Name: test.secretName, UID: "1", Namespace: "testing", }, Data: map[string][]byte{ test.certName: []byte(test.certData), test.keyName: []byte(test.keyData), }, }} watchChan := make(chan interface{}) client := clientMock{ ingresses: ingresses, services: services, endpoints: endpoints, secrets: secrets, watchChan: watchChan, } provider := Provider{} actual, err := provider.loadIngresses(client) require.NoError(t, err, "error loading ingresses") expected := buildConfiguration( backends( backend("foo/bar", lbMethod("wrr"), servers(), ), ), frontends(), ) assert.Equal(t, expected, actual) }) } } func TestTLSSecretLoad(t *testing.T) { ingresses := []*extensionsv1beta1.Ingress{ buildIngress( iNamespace("testing"), iAnnotation(annotationKubernetesFrontendEntryPoints, "ep1,ep2"), iRules( iRule(iHost("example.com"), iPaths( onePath(iBackend("example-com", intstr.FromInt(80))), )), iRule(iHost("example.org"), iPaths( onePath(iBackend("example-org", intstr.FromInt(80))), )), ), iTLSes( iTLS("myTlsSecret"), ), ), buildIngress( iNamespace("testing"), iAnnotation(annotationKubernetesFrontendEntryPoints, "ep3"), iRules( iRule(iHost("example.fail"), iPaths( onePath(iBackend("example-fail", intstr.FromInt(80))), )), ), iTLSes( iTLS("myUndefinedSecret"), ), ), } services := []*corev1.Service{ buildService( sName("example-com"), sNamespace("testing"), sUID("1"), sSpec( clusterIP("10.0.0.1"), sType("ClusterIP"), sPorts(sPort(80, "http"))), ), buildService( sName("example-org"), sNamespace("testing"), sUID("2"), sSpec( clusterIP("10.0.0.2"), sType("ClusterIP"), sPorts(sPort(80, "http"))), ), } secrets := []*corev1.Secret{ { ObjectMeta: metav1.ObjectMeta{ Name: "myTlsSecret", UID: "1", Namespace: "testing", }, Data: map[string][]byte{ "tls.crt": []byte("-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----"), "tls.key": []byte("-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----"), }, }, } var endpoints []*corev1.Endpoints watchChan := make(chan interface{}) client := clientMock{ ingresses: ingresses, services: services, secrets: secrets, endpoints: endpoints, watchChan: watchChan, } provider := Provider{} actual, err := provider.loadIngresses(client) if err != nil { t.Fatalf("error %+v", err) } expected := buildConfiguration( backends( backend("example.com", servers(), lbMethod("wrr"), ), backend("example.org", servers(), lbMethod("wrr"), ), ), frontends( frontend("example.com", entryPoints("ep1", "ep2"), passHostHeader(), routes( route("example.com", "Host:example.com"), ), ), frontend("example.org", entryPoints("ep1", "ep2"), passHostHeader(), routes( route("example.org", "Host:example.org"), ), ), ), tlsesSection( tlsSection( tlsEntryPoints("ep1", "ep2"), certificate( "-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----", "-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----"), ), ), ) assert.Equal(t, expected, actual) } func TestGetTLS(t *testing.T) { testIngressWithoutHostname := buildIngress( iNamespace("testing"), iRules( iRule(iHost("ep1.example.com")), iRule(iHost("ep2.example.com")), ), iTLSes( iTLS("test-secret"), ), ) tests := []struct { desc string ingress *extensionsv1beta1.Ingress client Client result []*tls.Configuration errResult string }{ { desc: "api client returns error", ingress: testIngressWithoutHostname, client: clientMock{ apiSecretError: errors.New("api secret error"), }, errResult: "failed to fetch secret testing/test-secret: api secret error", }, { desc: "api client doesn't find secret", ingress: testIngressWithoutHostname, client: clientMock{}, errResult: "secret testing/test-secret does not exist", }, { desc: "entry 'tls.crt' in secret missing", ingress: testIngressWithoutHostname, client: clientMock{ secrets: []*corev1.Secret{ { ObjectMeta: metav1.ObjectMeta{ Name: "test-secret", Namespace: "testing", }, Data: map[string][]byte{ "tls.key": []byte("tls-key"), }, }, }, }, errResult: "secret testing/test-secret is missing the following TLS data entries: tls.crt", }, { desc: "entry 'tls.key' in secret missing", ingress: testIngressWithoutHostname, client: clientMock{ secrets: []*corev1.Secret{ { ObjectMeta: metav1.ObjectMeta{ Name: "test-secret", Namespace: "testing", }, Data: map[string][]byte{ "tls.crt": []byte("tls-crt"), }, }, }, }, errResult: "secret testing/test-secret is missing the following TLS data entries: tls.key", }, { desc: "secret doesn't provide any of the required fields", ingress: testIngressWithoutHostname, client: clientMock{ secrets: []*corev1.Secret{ { ObjectMeta: metav1.ObjectMeta{ Name: "test-secret", Namespace: "testing", }, Data: map[string][]byte{}, }, }, }, errResult: "secret testing/test-secret is missing the following TLS data entries: tls.crt, tls.key", }, { desc: "add certificates to the configuration", ingress: buildIngress( iNamespace("testing"), iRules( iRule(iHost("ep1.example.com")), iRule(iHost("ep2.example.com")), iRule(iHost("ep3.example.com")), ), iTLSes( iTLS("test-secret"), iTLS("test-secret"), ), ), client: clientMock{ secrets: []*corev1.Secret{ { ObjectMeta: metav1.ObjectMeta{ Name: "test-secret", Namespace: "testing", }, Data: map[string][]byte{ "tls.crt": []byte("tls-crt"), "tls.key": []byte("tls-key"), }, }, }, }, result: []*tls.Configuration{ { Certificate: &tls.Certificate{ CertFile: tls.FileOrContent("tls-crt"), KeyFile: tls.FileOrContent("tls-key"), }, }, { Certificate: &tls.Certificate{ CertFile: tls.FileOrContent("tls-crt"), KeyFile: tls.FileOrContent("tls-key"), }, }, }, }, { desc: "pass the endpoints defined in the annotation to the certificate", ingress: buildIngress( iNamespace("testing"), iAnnotation(annotationKubernetesFrontendEntryPoints, "https,api-secure"), iRules(iRule(iHost("example.com"))), iTLSes(iTLS("test-secret")), ), client: clientMock{ secrets: []*corev1.Secret{ { ObjectMeta: metav1.ObjectMeta{ Name: "test-secret", Namespace: "testing", }, Data: map[string][]byte{ "tls.crt": []byte("tls-crt"), "tls.key": []byte("tls-key"), }, }, }, }, result: []*tls.Configuration{ { EntryPoints: []string{"https", "api-secure"}, Certificate: &tls.Certificate{ CertFile: tls.FileOrContent("tls-crt"), KeyFile: tls.FileOrContent("tls-key"), }, }, }, }, } for _, test := range tests { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() tlsConfigs, err := getTLS(test.ingress, test.client) if test.errResult != "" { assert.EqualError(t, err, test.errResult) } else { assert.Nil(t, err) assert.Equal(t, test.result, tlsConfigs) } }) } } func TestMultiPortServices(t *testing.T) { ingresses := []*extensionsv1beta1.Ingress{ buildIngress( iNamespace("testing"), iRules( iRule(iPaths( onePath(iPath("/cheddar"), iBackend("service", intstr.FromString("cheddar"))), onePath(iPath("/stilton"), iBackend("service", intstr.FromString("stilton"))), )), ), ), } services := []*corev1.Service{ buildService( sName("service"), sNamespace("testing"), sUID("1"), sSpec( clusterIP("10.0.0.1"), sPorts(sPort(80, "cheddar")), sPorts(sPort(81, "stilton")), ), ), } endpoints := []*corev1.Endpoints{ buildEndpoint( eNamespace("testing"), eName("service"), eUID("1"), subset( eAddresses( eAddress("10.10.0.1"), eAddress("10.10.0.2"), ), ePorts(ePort(8080, "cheddar")), ), subset( eAddresses( eAddress("10.20.0.1"), eAddress("10.20.0.2"), ), ePorts(ePort(8081, "stilton")), ), ), } watchChan := make(chan interface{}) client := clientMock{ ingresses: ingresses, services: services, endpoints: endpoints, watchChan: watchChan, } provider := Provider{} actual, err := provider.loadIngresses(client) require.NoError(t, err, "error loading ingresses") expected := buildConfiguration( backends( backend("/cheddar", lbMethod("wrr"), servers( server("http://10.10.0.1:8080", weight(1)), server("http://10.10.0.2:8080", weight(1)), ), ), backend("/stilton", lbMethod("wrr"), servers( server("http://10.20.0.1:8081", weight(1)), server("http://10.20.0.2:8081", weight(1)), ), ), ), frontends( frontend("/cheddar", passHostHeader(), routes(route("/cheddar", "PathPrefix:/cheddar")), ), frontend("/stilton", passHostHeader(), routes(route("/stilton", "PathPrefix:/stilton")), ), ), ) assert.Equal(t, expected, actual) } func TestProviderUpdateIngressStatus(t *testing.T) { testCases := []struct { desc string ingressEndpoint *IngressEndpoint apiServiceError error apiIngressStatusError error expectedError bool }{ { desc: "without IngressEndpoint configuration", expectedError: false, }, { desc: "without any IngressEndpoint option", ingressEndpoint: &IngressEndpoint{}, expectedError: true, }, { desc: "PublishedService - invalid format", ingressEndpoint: &IngressEndpoint{ PublishedService: "foo", }, expectedError: true, }, { desc: "PublishedService - missing service", ingressEndpoint: &IngressEndpoint{ PublishedService: "foo/bar", }, expectedError: true, }, { desc: "PublishedService - get service error", ingressEndpoint: &IngressEndpoint{ PublishedService: "foo/bar", }, apiServiceError: errors.New("error"), expectedError: true, }, { desc: "PublishedService - Skipping empty LoadBalancerIngress", ingressEndpoint: &IngressEndpoint{ PublishedService: "testing/service-empty-status", }, expectedError: false, }, { desc: "PublishedService - with update error", ingressEndpoint: &IngressEndpoint{ PublishedService: "testing/service", }, apiIngressStatusError: errors.New("error"), expectedError: true, }, { desc: "PublishedService - right service", ingressEndpoint: &IngressEndpoint{ PublishedService: "testing/service", }, expectedError: false, }, { desc: "IP - valid", ingressEndpoint: &IngressEndpoint{ IP: "127.0.0.1", }, expectedError: false, }, { desc: "IP - with update error", ingressEndpoint: &IngressEndpoint{ IP: "127.0.0.1", }, apiIngressStatusError: errors.New("error"), expectedError: true, }, { desc: "hostname - valid", ingressEndpoint: &IngressEndpoint{ Hostname: "foo", }, expectedError: false, }, { desc: "hostname - with update error", ingressEndpoint: &IngressEndpoint{ Hostname: "foo", }, apiIngressStatusError: errors.New("error"), expectedError: true, }, } for _, test := range testCases { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() p := &Provider{ IngressEndpoint: test.ingressEndpoint, } services := []*corev1.Service{ buildService( sName("service-empty-status"), sNamespace("testing"), sLoadBalancerStatus(), sUID("1"), sSpec( clusterIP("10.0.0.1"), sPorts(sPort(80, ""))), ), buildService( sName("service"), sNamespace("testing"), sLoadBalancerStatus(sLoadBalancerIngress("127.0.0.1", "")), sUID("2"), sSpec( clusterIP("10.0.0.2"), sPorts(sPort(80, ""))), ), } client := clientMock{ services: services, apiServiceError: test.apiServiceError, apiIngressStatusError: test.apiIngressStatusError, } i := &extensionsv1beta1.Ingress{} err := p.updateIngressStatus(i, client) if test.expectedError { assert.Error(t, err) } else { assert.NoError(t, err) } }) } } func TestPercentageWeightServiceAnnotation(t *testing.T) { ingresses := []*extensionsv1beta1.Ingress{ buildIngress( iAnnotation(annotationKubernetesServiceWeights, ` service1: 10% `), iNamespace("testing"), iRules( iRule( iHost("host1"), iPaths( onePath(iPath("/foo"), iBackend("service1", intstr.FromString("8080"))), onePath(iPath("/foo"), iBackend("service2", intstr.FromString("7070"))), onePath(iPath("/bar"), iBackend("service2", intstr.FromString("7070"))), )), ), ), } services := []*corev1.Service{ buildService( sName("service1"), sNamespace("testing"), sUID("1"), sSpec( clusterIP("10.0.0.1"), sPorts(sPort(8080, "")), ), ), buildService( sName("service2"), sNamespace("testing"), sUID("1"), sSpec( clusterIP("10.0.0.1"), sPorts(sPort(7070, "")), ), ), } endpoints := []*corev1.Endpoints{ buildEndpoint( eNamespace("testing"), eName("service1"), eUID("1"), subset( eAddresses( eAddress("10.10.0.1"), eAddress("10.10.0.2"), ), ePorts(ePort(8080, "")), ), ), buildEndpoint( eNamespace("testing"), eName("service2"), eUID("1"), subset( eAddresses( eAddress("10.10.0.3"), eAddress("10.10.0.4"), ), ePorts(ePort(7070, "")), ), ), } watchChan := make(chan interface{}) client := clientMock{ ingresses: ingresses, services: services, endpoints: endpoints, watchChan: watchChan, } provider := Provider{} actual, err := provider.loadIngresses(client) require.NoError(t, err, "error loading ingresses") expected := buildConfiguration( backends( backend("host1/foo", servers( server("http://10.10.0.1:8080", weight(int(newPercentageValueFromFloat64(0.05)))), server("http://10.10.0.2:8080", weight(int(newPercentageValueFromFloat64(0.05)))), server("http://10.10.0.3:7070", weight(int(newPercentageValueFromFloat64(0.45)))), server("http://10.10.0.4:7070", weight(int(newPercentageValueFromFloat64(0.45)))), ), lbMethod("wrr"), ), backend("host1/bar", servers( server("http://10.10.0.3:7070", weight(int(newPercentageValueFromFloat64(0.5)))), server("http://10.10.0.4:7070", weight(int(newPercentageValueFromFloat64(0.5)))), ), lbMethod("wrr"), ), ), frontends( frontend("host1/bar", passHostHeader(), routes( route("/bar", "PathPrefix:/bar"), route("host1", "Host:host1")), ), frontend("host1/foo", passHostHeader(), routes( route("/foo", "PathPrefix:/foo"), route("host1", "Host:host1")), ), ), ) assert.Equal(t, expected, actual, "error loading percentage weight annotation") } func TestProviderNewK8sInClusterClient(t *testing.T) { p := Provider{} os.Setenv("KUBERNETES_SERVICE_HOST", "localhost") os.Setenv("KUBERNETES_SERVICE_PORT", "443") defer os.Clearenv() _, err := p.newK8sClient("") assert.EqualError(t, err, "failed to create in-cluster configuration: open /var/run/secrets/kubernetes.io/serviceaccount/token: no such file or directory") } func TestProviderNewK8sInClusterClientFailLabelSel(t *testing.T) { p := Provider{} os.Setenv("KUBERNETES_SERVICE_HOST", "localhost") os.Setenv("KUBERNETES_SERVICE_PORT", "443") defer os.Clearenv() _, err := p.newK8sClient("%") assert.EqualError(t, err, "invalid ingress label selector: \"%\"") } func TestProviderNewK8sOutOfClusterClient(t *testing.T) { p := Provider{} p.Endpoint = "localhost" _, err := p.newK8sClient("") assert.NoError(t, err) }