Skip to content

Commit

Permalink
host rewrite can target multiple K8s service variants
Browse files Browse the repository at this point in the history
  • Loading branch information
dprotaso committed Nov 23, 2021
1 parent 75d86c5 commit befe8f9
Show file tree
Hide file tree
Showing 3 changed files with 302 additions and 4 deletions.
140 changes: 139 additions & 1 deletion test/conformance/ingress/rewrite.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
8 changes: 5 additions & 3 deletions test/conformance/ingress/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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){
Expand Down
158 changes: 158 additions & 0 deletions test/conformance/ingress/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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()

Expand All @@ -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 {
Expand Down Expand Up @@ -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

Expand Down

0 comments on commit befe8f9

Please sign in to comment.