From befe8f903d8071ed7222d3db32d53d2ef9b1260a Mon Sep 17 00:00:00 2001 From: dprotaso Date: Mon, 22 Nov 2021 21:15:44 -0500 Subject: [PATCH] host rewrite can target multiple K8s service variants --- test/conformance/ingress/rewrite.go | 140 +++++++++++++++++++++++- test/conformance/ingress/run.go | 8 +- test/conformance/ingress/util.go | 158 ++++++++++++++++++++++++++++ 3 files changed, 302 insertions(+), 4 deletions(-) diff --git a/test/conformance/ingress/rewrite.go b/test/conformance/ingress/rewrite.go index f9ac7e888..abbddbd8e 100644 --- a/test/conformance/ingress/rewrite.go +++ b/test/conformance/ingress/rewrite.go @@ -27,7 +27,145 @@ import ( ) // TestRewriteHost verifies that a RewriteHost rule can be used to implement vanity URLs. -func TestRewriteHost(t *testing.T) { +func TestRewriteHost_HeadlessWithEndpointSlice_FQDN(t *testing.T) { + t.Parallel() + ctx, clients := context.Background(), test.Setup(t) + + name, port, _ := CreateRuntimeService(ctx, t, clients, networking.ServicePortNameHTTP1) + + privateServiceName := test.ObjectNameForTest(t) + privateHostName := privateServiceName + "." + test.ServingNamespace + ".svc." + test.NetworkingFlags.ClusterSuffix + + // Create a simple Ingress over the Service. + ing, _, _ := CreateIngressReady(ctx, t, clients, v1alpha1.IngressSpec{ + Rules: []v1alpha1.IngressRule{{ + Visibility: v1alpha1.IngressVisibilityClusterLocal, + Hosts: []string{privateHostName}, + HTTP: &v1alpha1.HTTPIngressRuleValue{ + Paths: []v1alpha1.HTTPIngressPath{{ + Splits: []v1alpha1.IngressBackendSplit{{ + IngressBackend: v1alpha1.IngressBackend{ + ServiceName: name, + ServiceNamespace: test.ServingNamespace, + ServicePort: intstr.FromInt(port), + }, + }}, + }}, + }, + }}, + }) + + // Slap an ExternalName service in front of the kingress + loadbalancerAddress := ing.Status.PrivateLoadBalancer.Ingress[0].DomainInternal + createServiceWithEndpointSliceFQDN(ctx, t, clients, privateHostName, loadbalancerAddress) + + hosts := []string{ + "vanity.ismy.name", + "vanity.isalsomy.number", + } + + // Using fixed hostnames can lead to conflicts when -count=N>1 + // so pseudo-randomize the hostnames to avoid conflicts. + for i, host := range hosts { + hosts[i] = name + "." + host + } + + // Now create a RewriteHost ingress to point a custom Host at the Service + _, client, _ := CreateIngressReady(ctx, t, clients, v1alpha1.IngressSpec{ + Rules: []v1alpha1.IngressRule{{ + Hosts: hosts, + Visibility: v1alpha1.IngressVisibilityExternalIP, + HTTP: &v1alpha1.HTTPIngressRuleValue{ + Paths: []v1alpha1.HTTPIngressPath{{ + RewriteHost: privateHostName, + Splits: []v1alpha1.IngressBackendSplit{{ + IngressBackend: v1alpha1.IngressBackend{ + ServiceName: privateServiceName, + ServiceNamespace: test.ServingNamespace, + ServicePort: intstr.FromInt(80), + }, + }}, + }}, + }, + }}, + }) + + for _, host := range hosts { + RuntimeRequest(ctx, t, client, "http://"+host) + } +} +func TestRewriteHost_HeadlessWithEndpoint(t *testing.T) { + t.Parallel() + ctx, clients := context.Background(), test.Setup(t) + + name, port, _ := CreateRuntimeService(ctx, t, clients, networking.ServicePortNameHTTP1) + + privateServiceName := test.ObjectNameForTest(t) + privateHostName := privateServiceName + "." + test.ServingNamespace + ".svc." + test.NetworkingFlags.ClusterSuffix + + // Create a simple Ingress over the Service. + ing, _, _ := CreateIngressReady(ctx, t, clients, v1alpha1.IngressSpec{ + Rules: []v1alpha1.IngressRule{{ + Visibility: v1alpha1.IngressVisibilityClusterLocal, + Hosts: []string{privateHostName}, + HTTP: &v1alpha1.HTTPIngressRuleValue{ + Paths: []v1alpha1.HTTPIngressPath{{ + Splits: []v1alpha1.IngressBackendSplit{{ + IngressBackend: v1alpha1.IngressBackend{ + ServiceName: name, + ServiceNamespace: test.ServingNamespace, + ServicePort: intstr.FromInt(port), + }, + }}, + }}, + }, + }}, + }) + + // Slap an Manual Service with Endpoint IP in front of the kingress + ip := ing.Status.PrivateLoadBalancer.Ingress[0].IP + if ip == "" { + ip = getInternalIP(ctx, t, ing, clients) + } + createServiceWithEndpointIP(ctx, t, clients, privateHostName, ip) + + hosts := []string{ + "vanity.ismy.name", + "vanity.isalsomy.number", + } + + // Using fixed hostnames can lead to conflicts when -count=N>1 + // so pseudo-randomize the hostnames to avoid conflicts. + for i, host := range hosts { + hosts[i] = name + "." + host + } + + // Now create a RewriteHost ingress to point a custom Host at the Service + _, client, _ := CreateIngressReady(ctx, t, clients, v1alpha1.IngressSpec{ + Rules: []v1alpha1.IngressRule{{ + Hosts: hosts, + Visibility: v1alpha1.IngressVisibilityExternalIP, + HTTP: &v1alpha1.HTTPIngressRuleValue{ + Paths: []v1alpha1.HTTPIngressPath{{ + RewriteHost: privateHostName, + Splits: []v1alpha1.IngressBackendSplit{{ + IngressBackend: v1alpha1.IngressBackend{ + ServiceName: privateServiceName, + ServiceNamespace: test.ServingNamespace, + ServicePort: intstr.FromInt(80), + }, + }}, + }}, + }, + }}, + }) + + for _, host := range hosts { + RuntimeRequest(ctx, t, client, "http://"+host) + } +} + +func TestRewriteHost_ExternalName(t *testing.T) { t.Parallel() ctx, clients := context.Background(), test.Setup(t) diff --git a/test/conformance/ingress/run.go b/test/conformance/ingress/run.go index d914521d0..023a0b9de 100644 --- a/test/conformance/ingress/run.go +++ b/test/conformance/ingress/run.go @@ -34,7 +34,7 @@ var stableTests = map[string]func(t *testing.T){ "hosts/multiple": TestMultipleHosts, "dispatch/path": TestPath, "dispatch/percentage": TestPercentage, - "dispatch/path_and_percentage": TestPathAndPercentageSplit, + "dispatch/path-and-percentage": TestPathAndPercentageSplit, "dispatch/rule": TestRule, "retry": TestRetry, "timeout": TestTimeout, @@ -50,8 +50,10 @@ var stableTests = map[string]func(t *testing.T){ var betaTests = map[string]func(t *testing.T){ // Add your conformance test for beta features - "host-rewrite": TestRewriteHost, - "headers/tags": TestTagHeaders, + "host-rewrite/external-name": TestRewriteHost_ExternalName, + "host-rewrite/headless-service": TestRewriteHost_HeadlessWithEndpoint, + "host-rewrite/endpoint-slice-fqdn": TestRewriteHost_HeadlessWithEndpointSlice_FQDN, + "headers/tags": TestTagHeaders, } var alphaTests = map[string]func(t *testing.T){ diff --git a/test/conformance/ingress/util.go b/test/conformance/ingress/util.go index 7d97513eb..210a4241a 100644 --- a/test/conformance/ingress/util.go +++ b/test/conformance/ingress/util.go @@ -41,6 +41,7 @@ import ( "time" corev1 "k8s.io/api/core/v1" + discoveryv1 "k8s.io/api/discovery/v1" apierrs "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ktypes "k8s.io/apimachinery/pkg/types" @@ -52,6 +53,7 @@ import ( "knative.dev/networking/test" "knative.dev/networking/test/types" "knative.dev/pkg/network" + "knative.dev/pkg/ptr" "knative.dev/pkg/reconciler" pkgTest "knative.dev/pkg/test" "knative.dev/pkg/test/logging" @@ -545,6 +547,51 @@ func createService(ctx context.Context, t *testing.T, clients *test.Clients, svc } } +func createEndpointSlice(ctx context.Context, t *testing.T, clients *test.Clients, ep *discoveryv1.EndpointSlice) { + t.Helper() + + if err := reconciler.RetryTestErrors(func(attempts int) error { + if attempts > 0 { + t.Logf("Attempt %d creating service %s", attempts, ep.Name) + } + _, err := clients.KubeClient.DiscoveryV1().EndpointSlices(ep.Namespace).Create(ctx, ep, metav1.CreateOptions{}) + if err != nil { + t.Logf("Attempt %d creating service failed with: %v", attempts, err) + } + return err + }); err != nil { + + t.Fatalf("Error creating Service \"%s/%s\": %v", ep.Namespace, ep.Name, err) + } + + t.Cleanup(func() { + clients.KubeClient.DiscoveryV1().EndpointSlices(ep.Namespace).Delete(ctx, ep.Name, metav1.DeleteOptions{}) + }) + +} +func createEndpoint(ctx context.Context, t *testing.T, clients *test.Clients, ep *corev1.Endpoints) { + t.Helper() + + if err := reconciler.RetryTestErrors(func(attempts int) error { + if attempts > 0 { + t.Logf("Attempt %d creating service %s", attempts, ep.Name) + } + _, err := clients.KubeClient.CoreV1().Endpoints(ep.Namespace).Create(ctx, ep, metav1.CreateOptions{}) + if err != nil { + t.Logf("Attempt %d creating service failed with: %v", attempts, err) + } + return err + }); err != nil { + + t.Fatalf("Error creating Service \"%s/%s\": %v", ep.Namespace, ep.Name, err) + } + + t.Cleanup(func() { + clients.KubeClient.CoreV1().Services(ep.Namespace).Delete(ctx, ep.Name, metav1.DeleteOptions{}) + }) + +} + func createExternalNameService(ctx context.Context, t *testing.T, clients *test.Clients, target, gatewayDomain string) context.CancelFunc { t.Helper() @@ -569,6 +616,87 @@ func createExternalNameService(ctx context.Context, t *testing.T, clients *test. return createService(ctx, t, clients, externalNameSvc) } +func createServiceWithEndpointSliceFQDN(ctx context.Context, t *testing.T, clients *test.Clients, target, gatewayDomain string) { + targetName := strings.SplitN(target, ".", 3) + svc := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: targetName[0], + Namespace: targetName[1], + }, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeClusterIP, + ClusterIP: corev1.ClusterIPNone, + SessionAffinity: corev1.ServiceAffinityNone, + Ports: []corev1.ServicePort{{ + Name: networking.ServicePortNameH2C, + Port: int32(80), + TargetPort: intstr.FromInt(80), + }}, + }, + } + + es := &discoveryv1.EndpointSlice{ + ObjectMeta: metav1.ObjectMeta{ + Name: targetName[0], + Namespace: targetName[1], + Labels: map[string]string{ + // EndpointsSlices are linked to their services using labels + // ie. kubernetes.io/service-name: helloworld + discoveryv1.LabelServiceName: targetName[0], + }, + }, + AddressType: discoveryv1.AddressTypeFQDN, + Ports: []discoveryv1.EndpointPort{{ + Name: ptr.String(networking.ServicePortNameH2C), + Port: ptr.Int32(80), + }}, + Endpoints: []discoveryv1.Endpoint{{ + Addresses: []string{gatewayDomain}, + }}, + } + + createService(ctx, t, clients, svc) + createEndpointSlice(ctx, t, clients, es) +} + +func createServiceWithEndpointIP(ctx context.Context, t *testing.T, clients *test.Clients, target, gatewayIP string) { + t.Helper() + + targetName := strings.SplitN(target, ".", 3) + externalNameSvc := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: targetName[0], + Namespace: targetName[1], + }, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeClusterIP, + ClusterIP: corev1.ClusterIPNone, + SessionAffinity: corev1.ServiceAffinityNone, + Ports: []corev1.ServicePort{{ + Name: networking.ServicePortNameH2C, + Port: int32(80), + TargetPort: intstr.FromInt(80), + }}, + }, + } + + endpoint := &corev1.Endpoints{ + ObjectMeta: externalNameSvc.ObjectMeta, + Subsets: []corev1.EndpointSubset{{ + Addresses: []corev1.EndpointAddress{{ + IP: gatewayIP, + }}, + Ports: []corev1.EndpointPort{{ + Name: networking.ServicePortNameH2C, + Port: int32(80), + }}, + }}, + } + + createService(ctx, t, clients, externalNameSvc) + createEndpoint(ctx, t, clients, endpoint) +} + // createPodAndService is a helper for creating the pod and service resources, setting // up their context.CancelFunc, and waiting for it to become ready. func createPodAndService(ctx context.Context, t *testing.T, clients *test.Clients, pod *corev1.Pod, svc *corev1.Service) context.CancelFunc { @@ -964,6 +1092,36 @@ func CreateDialContext(ctx context.Context, t *testing.T, ing *v1alpha1.Ingress, } } +func getInternalIP(ctx context.Context, t *testing.T, ing *v1alpha1.Ingress, clients *test.Clients) string { + t.Helper() + + if ing.Status.PrivateLoadBalancer == nil || len(ing.Status.PrivateLoadBalancer.Ingress) < 1 { + t.Fatal("Ingress does not have a private load balancer assigned.") + } + + internalDomain := ing.Status.PrivateLoadBalancer.Ingress[0].DomainInternal + parts := strings.SplitN(internalDomain, ".", 3) + if len(parts) < 3 { + t.Fatal("Too few parts in internal domain:", internalDomain) + } + name, namespace := parts[0], parts[1] + + var svc *corev1.Service + err := reconciler.RetryTestErrors(func(attempts int) (err error) { + svc, err = clients.KubeClient.CoreV1().Services(namespace).Get(ctx, name, metav1.GetOptions{}) + return err + }) + if err != nil { + t.Fatalf("Unable to retrieve Kubernetes service %s/%s: %v", namespace, name, err) + } + if svc.Spec.ClusterIP != "" { + return svc.Spec.ClusterIP + } + + t.Fatal("unable to get IP for internal domain") + return "" +} + type RequestOption func(*http.Request) type ResponseExpectation func(response *http.Response) error