Repository: envoyproxy/gateway

Issue:
`backendHTTPRouteIndexFunc()` is not adding indexer when Backend kind is ServiceImport
*Description*:
The `backendHTTPRouteIndexFunc()` in the Kubernetes controller is NOT adding field indexer for when Backend kind is ServiceImport.

```
for _, backend := range rule.BackendRefs {
	if backend.Kind == nil || string(*backend.Kind) == resource.KindService || string(*backend.Kind) == egv1a1.KindBackend {
		...
	}
}
```

source: https://github.com/envoyproxy/gateway/blob/c4920899659e75ceb342df02665e1b484eb0ecbc/internal/provider/kubernetes/indexers.go#L114

*Repro steps*:
N/A

>**Note**: If there are privacy concerns, sanitize the data prior to
sharing.

*Environment*:
>Include the environment like gateway version, envoy version and so on.

*Logs*:
>Include the access logs and the Envoy logs.


Discussion:

Solution patch:
diff --git a/internal/provider/kubernetes/indexers.go b/internal/provider/kubernetes/indexers.go
index 7afe71278..ad698ba79 100644
--- a/internal/provider/kubernetes/indexers.go
+++ b/internal/provider/kubernetes/indexers.go
@@ -111,7 +111,7 @@ func backendHTTPRouteIndexFunc(rawObj client.Object) []string {
 	var backendRefs []string
 	for _, rule := range httproute.Spec.Rules {
 		for _, backend := range rule.BackendRefs {
-			if backend.Kind == nil || string(*backend.Kind) == resource.KindService || string(*backend.Kind) == egv1a1.KindBackend {
+			if backend.Kind == nil || string(*backend.Kind) == resource.KindService || string(*backend.Kind) == resource.KindServiceImport || string(*backend.Kind) == egv1a1.KindBackend {
 				backendRefs = append(backendRefs,
 					types.NamespacedName{
 						Namespace: gatewayapi.NamespaceDerefOr(backend.Namespace, httproute.Namespace),
diff --git a/release-notes/current.yaml b/release-notes/current.yaml
index 5a42fb7da..b7c4d81af 100644
--- a/release-notes/current.yaml
+++ b/release-notes/current.yaml
@@ -30,6 +30,7 @@ bug fixes: |
   Fix translating backendSettings for extAuth
   Fix an issue that stats compressor was not working.
   Added support for BackendTLSPolicy and EnvoyExtensionPolicy parsing in Standalone mode.
+  Retrigger reconciliation when backendRef of type ServiceImport is updated or when EndpointSlice(s) for a ServiceImport are updated.
 
 # Enhancements that improve performance.
 performance improvements: |



Test patch:
diff --git a/internal/provider/kubernetes/kubernetes_test.go b/internal/provider/kubernetes/kubernetes_test.go
index 541237c2f..4665f510e 100644
--- a/internal/provider/kubernetes/kubernetes_test.go
+++ b/internal/provider/kubernetes/kubernetes_test.go
@@ -1429,20 +1429,22 @@ func TestNamespaceSelectorProvider(t *testing.T) {
 		require.NoError(t, cli.Delete(ctx, nonWatchedSvc))
 	}()
 
+	watchedServiceBackendRef := test.GetServiceBackendRef(types.NamespacedName{Name: watchedSvc.Name}, 80)
 	watchedHTTPRoute := test.GetHTTPRoute(types.NamespacedName{
 		Namespace: watchedNS.Name,
 		Name:      "watched-http-route",
-	}, watchedGateway.Name, types.NamespacedName{Name: watchedSvc.Name}, 80, "")
+	}, watchedGateway.Name, watchedServiceBackendRef, "")
 
 	require.NoError(t, cli.Create(ctx, watchedHTTPRoute))
 	defer func() {
 		require.NoError(t, cli.Delete(ctx, watchedHTTPRoute))
 	}()
 
+	nonWatchedServiceBackendRef := test.GetServiceBackendRef(types.NamespacedName{Name: nonWatchedSvc.Name}, 8001)
 	nonWatchedHTTPRoute := test.GetHTTPRoute(types.NamespacedName{
 		Namespace: nonWatchedNS.Name,
 		Name:      "non-watched-http-route",
-	}, nonWatchedGateway.Name, types.NamespacedName{Name: nonWatchedSvc.Name}, 8001, "")
+	}, nonWatchedGateway.Name, nonWatchedServiceBackendRef, "")
 	require.NoError(t, cli.Create(ctx, nonWatchedHTTPRoute))
 	defer func() {
 		require.NoError(t, cli.Delete(ctx, nonWatchedHTTPRoute))
diff --git a/internal/provider/kubernetes/predicates_test.go b/internal/provider/kubernetes/predicates_test.go
index dc0d70cdc..61a564957 100644
--- a/internal/provider/kubernetes/predicates_test.go
+++ b/internal/provider/kubernetes/predicates_test.go
@@ -123,11 +123,12 @@ func TestGatewayClassHasMatchingNamespaceLabels(t *testing.T) {
 				Build(),
 		}
 		t.Run(tc.name, func(t *testing.T) {
+			sampleServiceBackendRef := test.GetServiceBackendRef(types.NamespacedName{Name: "service"}, 80)
 			res := r.hasMatchingNamespaceLabels(
 				test.GetHTTPRoute(types.NamespacedName{
 					Namespace: ns,
 					Name:      "httproute-test",
-				}, "scheduled-status-test", types.NamespacedName{Name: "service"}, 80, ""))
+				}, "scheduled-status-test", sampleServiceBackendRef, ""))
 			require.Equal(t, tc.expect, res)
 		})
 	}
@@ -412,7 +413,10 @@ func TestValidateSecretForReconcile(t *testing.T) {
 // TestValidateEndpointSliceForReconcile tests the validateEndpointSliceForReconcile
 // predicate function.
 func TestValidateEndpointSliceForReconcile(t *testing.T) {
+	sampleGatewayClass := test.GetGatewayClass("test-gc", egv1a1.GatewayControllerName, nil)
 	sampleGateway := test.GetGateway(types.NamespacedName{Namespace: "default", Name: "scheduled-status-test"}, "test-gc", 8080)
+	sampleServiceBackendRef := test.GetServiceBackendRef(types.NamespacedName{Name: "service"}, 80)
+	sampleServiceImportBackendRef := test.GetServiceImportBackendRef(types.NamespacedName{Name: "imported-service"}, 80)
 
 	testCases := []struct {
 		name          string
@@ -423,30 +427,40 @@ func TestValidateEndpointSliceForReconcile(t *testing.T) {
 		{
 			name: "route service but no routes exist",
 			configs: []client.Object{
-				test.GetGatewayClass("test-gc", egv1a1.GatewayControllerName, nil),
+				sampleGatewayClass,
 				sampleGateway,
 			},
-			endpointSlice: test.GetEndpointSlice(types.NamespacedName{Name: "endpointslice"}, "service"),
+			endpointSlice: test.GetEndpointSlice(types.NamespacedName{Name: "endpointslice"}, "service", false),
 			expect:        false,
 		},
 		{
 			name: "http route service routes exist, but endpointslice is associated with another service",
 			configs: []client.Object{
-				test.GetGatewayClass("test-gc", egv1a1.GatewayControllerName, nil),
+				sampleGatewayClass,
 				sampleGateway,
-				test.GetHTTPRoute(types.NamespacedName{Name: "httproute-test"}, "scheduled-status-test", types.NamespacedName{Name: "service"}, 80, ""),
+				test.GetHTTPRoute(types.NamespacedName{Name: "httproute-test"}, "scheduled-status-test", sampleServiceBackendRef, ""),
 			},
-			endpointSlice: test.GetEndpointSlice(types.NamespacedName{Name: "endpointslice"}, "other-service"),
+			endpointSlice: test.GetEndpointSlice(types.NamespacedName{Name: "endpointslice"}, "other-service", false),
 			expect:        false,
 		},
 		{
 			name: "http route service routes exist",
 			configs: []client.Object{
-				test.GetGatewayClass("test-gc", egv1a1.GatewayControllerName, nil),
+				sampleGatewayClass,
 				sampleGateway,
-				test.GetHTTPRoute(types.NamespacedName{Name: "httproute-test"}, "scheduled-status-test", types.NamespacedName{Name: "service"}, 80, ""),
+				test.GetHTTPRoute(types.NamespacedName{Name: "httproute-test"}, "scheduled-status-test", sampleServiceBackendRef, ""),
 			},
-			endpointSlice: test.GetEndpointSlice(types.NamespacedName{Name: "endpointslice"}, "service"),
+			endpointSlice: test.GetEndpointSlice(types.NamespacedName{Name: "endpointslice"}, "service", false),
+			expect:        true,
+		},
+		{
+			name: "http route serviceimport routes exist",
+			configs: []client.Object{
+				sampleGatewayClass,
+				sampleGateway,
+				test.GetHTTPRoute(types.NamespacedName{Name: "httproute-test"}, "scheduled-status-test", sampleServiceImportBackendRef, ""),
+			},
+			endpointSlice: test.GetEndpointSlice(types.NamespacedName{Name: "endpointslice"}, "imported-service", true),
 			expect:        true,
 		},
 	}
@@ -551,6 +565,7 @@ func TestValidateServiceForReconcile(t *testing.T) {
 			},
 		},
 	}
+	sampleServiceBackendRef := test.GetServiceBackendRef(types.NamespacedName{Name: "service"}, 80)
 
 	testCases := []struct {
 		name    string
@@ -614,7 +629,7 @@ func TestValidateServiceForReconcile(t *testing.T) {
 			configs: []client.Object{
 				test.GetGatewayClass("test-gc", egv1a1.GatewayControllerName, nil),
 				sampleGateway,
-				test.GetHTTPRoute(types.NamespacedName{Name: "httproute-test"}, "scheduled-status-test", types.NamespacedName{Name: "service"}, 80, ""),
+				test.GetHTTPRoute(types.NamespacedName{Name: "httproute-test"}, "scheduled-status-test", sampleServiceBackendRef, ""),
 			},
 			service: test.GetService(types.NamespacedName{Name: "service"}, nil, nil),
 			expect:  true,
@@ -626,7 +641,7 @@ func TestValidateServiceForReconcile(t *testing.T) {
 			name: "route service routes exist but with non-existing gateway reference",
 			configs: []client.Object{
 				test.GetGatewayClass("test-gc", egv1a1.GatewayControllerName, nil),
-				test.GetHTTPRoute(types.NamespacedName{Name: "httproute-test"}, "scheduled-status-test", types.NamespacedName{Name: "service"}, 80, ""),
+				test.GetHTTPRoute(types.NamespacedName{Name: "httproute-test"}, "scheduled-status-test", sampleServiceBackendRef, ""),
 			},
 			service: test.GetService(types.NamespacedName{Name: "service"}, nil, nil),
 			expect:  true,
@@ -1036,13 +1051,17 @@ func TestCheckObjectNamespaceLabels(t *testing.T) {
 	}{
 		{
 			name: "matching labels of namespace of the object is a subset of namespaceLabels",
-			object: test.GetHTTPRoute(types.NamespacedName{
-				Name:      "foo-route",
-				Namespace: "foo",
-			}, "eg", types.NamespacedName{
-				Name:      "foo-svc",
-				Namespace: "foo",
-			}, 8080, ""),
+			object: test.GetHTTPRoute(
+				types.NamespacedName{
+					Name:      "foo-route",
+					Namespace: "foo",
+				},
+				"eg",
+				test.GetServiceBackendRef(types.NamespacedName{
+					Name:      "foo-svc",
+					Namespace: "foo",
+				}, 8080),
+				""),
 			ns: &corev1.Namespace{
 				ObjectMeta: metav1.ObjectMeta{
 					Name: "foo",
@@ -1056,13 +1075,17 @@ func TestCheckObjectNamespaceLabels(t *testing.T) {
 		},
 		{
 			name: "non-matching labels of namespace of the object is a subset of namespaceLabels",
-			object: test.GetHTTPRoute(types.NamespacedName{
-				Name:      "bar-route",
-				Namespace: "bar",
-			}, "eg", types.NamespacedName{
-				Name:      "bar-svc",
-				Namespace: "bar",
-			}, 8080, ""),
+			object: test.GetHTTPRoute(
+				types.NamespacedName{
+					Name:      "bar-route",
+					Namespace: "bar",
+				},
+				"eg",
+				test.GetServiceBackendRef(types.NamespacedName{
+					Name:      "bar-svc",
+					Namespace: "bar",
+				}, 8080),
+				""),
 			ns: &corev1.Namespace{
 				ObjectMeta: metav1.ObjectMeta{
 					Name: "bar",
@@ -1216,6 +1239,7 @@ func TestValidateHTTPRouteFilerForReconcile(t *testing.T) {
 	sampleGWC := test.GetGatewayClass("test-gc", egv1a1.GatewayControllerName, nil)
 	sampleGateway := test.GetGateway(types.NamespacedName{Namespace: "default", Name: "scheduled-status-test"}, "test-gc", 8080)
 	sampleService := test.GetService(types.NamespacedName{Name: "service"}, nil, nil)
+	sampleServiceBackendRef := test.GetServiceBackendRef(types.NamespacedName{Name: "service"}, 80)
 	sampleHTTPRouteFilter := test.GetHTTPRouteFilter(types.NamespacedName{Name: "httproutefilter"})
 
 	testCases := []struct {
@@ -1242,7 +1266,7 @@ func TestValidateHTTPRouteFilerForReconcile(t *testing.T) {
 				sampleGateway,
 				sampleService,
 				sampleHTTPRouteFilter,
-				test.GetHTTPRoute(types.NamespacedName{Name: "httproute-test"}, "scheduled-status-test", types.NamespacedName{Name: "service"}, 80, "httproutefilter"),
+				test.GetHTTPRoute(types.NamespacedName{Name: "httproute-test"}, "scheduled-status-test", sampleServiceBackendRef, "httproutefilter"),
 			},
 			httpRouteFilter: sampleHTTPRouteFilter,
 			expect:          true,
diff --git a/internal/provider/kubernetes/test/utils.go b/internal/provider/kubernetes/test/utils.go
index 77bc50c5e..a6b2ef325 100644
--- a/internal/provider/kubernetes/test/utils.go
+++ b/internal/provider/kubernetes/test/utils.go
@@ -15,8 +15,11 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
 	gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
+	mcsapiv1a1 "sigs.k8s.io/mcs-api/pkg/apis/v1alpha1"
 
 	egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
+	"github.com/envoyproxy/gateway/internal/gatewayapi"
+	"github.com/envoyproxy/gateway/internal/gatewayapi/resource"
 )
 
 type GroupKindNamespacedName struct {
@@ -107,8 +110,24 @@ func GetSecret(nsName types.NamespacedName) *corev1.Secret {
 	}
 }
 
+func GetServiceBackendRef(name types.NamespacedName, port int32) gwapiv1.BackendObjectReference {
+	return gwapiv1.BackendObjectReference{
+		Name: gwapiv1.ObjectName(name.Name),
+		Port: ptr.To(gwapiv1.PortNumber(port)),
+	}
+}
+
+func GetServiceImportBackendRef(name types.NamespacedName, port int32) gwapiv1.BackendObjectReference {
+	return gwapiv1.BackendObjectReference{
+		Name:  gwapiv1.ObjectName(name.Name),
+		Port:  ptr.To(gwapiv1.PortNumber(port)),
+		Kind:  gatewayapi.KindPtr(resource.KindServiceImport),
+		Group: gatewayapi.GroupPtr(mcsapiv1a1.GroupName),
+	}
+}
+
 // GetHTTPRoute returns a sample HTTPRoute with a parent reference.
-func GetHTTPRoute(nsName types.NamespacedName, parent string, serviceName types.NamespacedName, port int32, httpRouteFilterName string) *gwapiv1.HTTPRoute {
+func GetHTTPRoute(nsName types.NamespacedName, parent string, backendRef gwapiv1.BackendObjectReference, httpRouteFilterName string) *gwapiv1.HTTPRoute {
 	httpRoute := &gwapiv1.HTTPRoute{
 		ObjectMeta: metav1.ObjectMeta{
 			Namespace: nsName.Namespace,
@@ -125,10 +144,7 @@ func GetHTTPRoute(nsName types.NamespacedName, parent string, serviceName types.
 					BackendRefs: []gwapiv1.HTTPBackendRef{
 						{
 							BackendRef: gwapiv1.BackendRef{
-								BackendObjectReference: gwapiv1.BackendObjectReference{
-									Name: gwapiv1.ObjectName(serviceName.Name),
-									Port: ptr.To(gwapiv1.PortNumber(port)),
-								},
+								BackendObjectReference: backendRef,
 							},
 						},
 					},
@@ -349,12 +365,19 @@ func GetService(nsName types.NamespacedName, labels map[string]string, ports map
 }
 
 // GetEndpointSlice returns a sample EndpointSlice.
-func GetEndpointSlice(nsName types.NamespacedName, svcName string) *discoveryv1.EndpointSlice {
+func GetEndpointSlice(nsName types.NamespacedName, svcName string, isServiceImport bool) *discoveryv1.EndpointSlice {
+	var labels map[string]string
+	if isServiceImport {
+		labels = map[string]string{mcsapiv1a1.LabelServiceName: svcName}
+	} else {
+		labels = map[string]string{discoveryv1.LabelServiceName: svcName}
+	}
+
 	return &discoveryv1.EndpointSlice{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      nsName.Name,
 			Namespace: nsName.Namespace,
-			Labels:    map[string]string{discoveryv1.LabelServiceName: svcName},
+			Labels:    labels,
 		},
 		Endpoints: []discoveryv1.Endpoint{
 			{