Repository: kumahq/kuma
Pull request #13108: fix(meshservice): fix inbound port selecting logic on MeshService
Author: login: jijiechen
Labels: name: release-2.10
Created: 2025-03-14T09:37:45Z | Merged: 2025-03-17T11:36:41Z

## Motivation

Fixing an issue on the MeshService updater which is preventing data planes of MeshServices from being shown as "Available": they are not generated as endpoints, which makes these DPs not available to other DPs in the mesh. This is also impacting MeshMultiZoneServices as they are selecting MeshServices under the hood.

This issue was newly introduced in this release. Before this PR, we were using `name` of MeshService ports to match inbound ports on Dataplanes. In an earlier PR of this release, we changed the way of assigning `name` values on MeshService which actually broke the matching.

Given that said, when the port on MeshService and Dataplane will not match when one of the following is true, we'll hit the issue:

* the pod is not defined on the Dataplane Pod (using `PodSpec.containers[].ports[].containerPort`) or it has a different name than what it is on the Service
* the Pod port is exposed to a different port on the Service

## Implementation information

> update MeshService updater to take into account target port

Use targetPort on MeshService to match DP inbound ports, instead of using  port `name` on MeshService.

## Supporting documentation

<!-- Is there a MADR? An Issue? A related PR? -->

fixes https://github.com/kumahq/kuma/issues/12042


> Changelog: skip

<!--
Uncomment the above section to explicitly set a [`> Changelog:` entry here](https://github.com/kumahq/kuma/blob/master/CONTRIBUTING.md#submitting-a-patch)?
-->



Diff:
diff --git a/pkg/core/resources/apis/meshservice/match.go b/pkg/core/resources/apis/meshservice/match.go
index 5888bb9ecee5..ea26acc23106 100644
--- a/pkg/core/resources/apis/meshservice/match.go
+++ b/pkg/core/resources/apis/meshservice/match.go
@@ -1,6 +1,8 @@
 package meshservice
 
 import (
+	"k8s.io/apimachinery/pkg/util/intstr"
+
 	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
 	core_mesh "github.com/kumahq/kuma/pkg/core/resources/apis/mesh"
 	meshservice_api "github.com/kumahq/kuma/pkg/core/resources/apis/meshservice/api/v1alpha1"
@@ -140,3 +142,13 @@ func matchByTags(
 	}
 	return dpps
 }
+
+func MatchInboundWithMeshServicePort(inbound *mesh_proto.Dataplane_Networking_Inbound, meshServicePort meshservice_api.Port) bool {
+	switch meshServicePort.TargetPort.Type {
+	case intstr.Int:
+		return uint32(meshServicePort.TargetPort.IntVal) == inbound.Port
+	case intstr.String:
+		return meshServicePort.TargetPort.StrVal == inbound.Name
+	}
+	return false
+}
diff --git a/pkg/core/resources/apis/meshservice/status/updater.go b/pkg/core/resources/apis/meshservice/status/updater.go
index 7b3ac51722d3..7f26b3ca8b10 100644
--- a/pkg/core/resources/apis/meshservice/status/updater.go
+++ b/pkg/core/resources/apis/meshservice/status/updater.go
@@ -165,7 +165,7 @@ func (s *StatusUpdater) updateStatus(ctx context.Context) error {
 func buildDataplaneProxies(
 	dpps []*core_mesh.DataplaneResource,
 	insightsByKey map[core_model.ResourceKey]*core_mesh.DataplaneInsightResource,
-	ports []meshservice_api.Port,
+	meshServicePorts []meshservice_api.Port,
 ) meshservice_api.DataplaneProxies {
 	result := meshservice_api.DataplaneProxies{}
 	for _, dpp := range dpps {
@@ -176,32 +176,22 @@ func buildDataplaneProxies(
 			}
 		}
 		healthyInbounds := 0
-		for _, port := range ports {
-			if inbound := dpInboundForMeshServicePort(dpp.Spec.GetNetworking().Inbound, port); inbound != nil {
-				if inbound.State == mesh_proto.Dataplane_Networking_Inbound_Ready {
+		for _, meshServicePort := range meshServicePorts {
+			for _, dpInbound := range dpp.Spec.GetNetworking().Inbound {
+				if dpInbound.State == mesh_proto.Dataplane_Networking_Inbound_Ready &&
+					meshservice.MatchInboundWithMeshServicePort(dpInbound, meshServicePort) {
 					healthyInbounds++
+					break
 				}
 			}
 		}
-		if healthyInbounds == len(ports) {
+		if healthyInbounds == len(meshServicePorts) {
 			result.Healthy++
 		}
 	}
 	return result
 }
 
-func dpInboundForMeshServicePort(inbounds []*mesh_proto.Dataplane_Networking_Inbound, port meshservice_api.Port) *mesh_proto.Dataplane_Networking_Inbound {
-	for _, inbound := range inbounds {
-		if port.Name != "" && inbound.Name == port.Name {
-			return inbound
-		}
-		if port.Port == inbound.Port {
-			return inbound
-		}
-	}
-	return nil
-}
-
 func buildTLS(
 	existing meshservice_api.TLS,
 	dpps []*core_mesh.DataplaneResource,
diff --git a/pkg/core/resources/apis/meshservice/status/updater_test.go b/pkg/core/resources/apis/meshservice/status/updater_test.go
index b0d85abc4018..58a6d79c6721 100644
--- a/pkg/core/resources/apis/meshservice/status/updater_test.go
+++ b/pkg/core/resources/apis/meshservice/status/updater_test.go
@@ -230,10 +230,10 @@ var _ = Describe("Updater", func() {
 				Total:     3,
 			},
 		}),
-		Entry("should count healthy DPPs", dpProxiesTestCase{
+		Entry("should count healthy DPPs and use MeshService.ports[].targetPort to select DP inbound", dpProxiesTestCase{
 			meshService: samples.MeshServiceBackendBuilder().
 				WithDataplaneTagsSelectorKV("app", "backend").
-				AddIntPort(builders.FirstInboundPort+1, builders.FirstInboundServicePort+1, core_mesh.ProtocolHTTP),
+				AddIntPort(builders.FirstInboundServicePort+1, builders.FirstInboundPort+1, core_mesh.ProtocolHTTP),
 			dpps: []*builders.DataplaneBuilder{
 				builders.Dataplane().
 					WithName("dp-all-inbounds-healthy").
diff --git a/pkg/xds/topology/outbound.go b/pkg/xds/topology/outbound.go
index e4614e12f557..6a6895906aad 100644
--- a/pkg/xds/topology/outbound.go
+++ b/pkg/xds/topology/outbound.go
@@ -8,7 +8,6 @@ import (
 
 	"github.com/asaskevich/govalidator"
 	"github.com/pkg/errors"
-	"k8s.io/apimachinery/pkg/util/intstr"
 
 	common_tls "github.com/kumahq/kuma/api/common/v1alpha1/tls"
 	mesh_proto "github.com/kumahq/kuma/api/mesh/v1alpha1"
@@ -339,15 +338,8 @@ func fillLocalMeshServices(
 			dpNetworking := dpp.Spec.GetNetworking()
 			for _, inbound := range dpNetworking.GetHealthyInbounds() {
 				for _, port := range meshSvc.Spec.Ports {
-					switch port.TargetPort.Type {
-					case intstr.Int:
-						if uint32(port.TargetPort.IntVal) != inbound.Port {
-							continue
-						}
-					case intstr.String:
-						if port.TargetPort.StrVal != inbound.Name {
-							continue
-						}
+					if !meshservice.MatchInboundWithMeshServicePort(inbound, port) {
+						continue
 					}
 
 					inboundTags := maps.Clone(inbound.GetTags())


Reviews:
user: login: jijiechen
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-14T09:40:01Z
user: login: jijiechen
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-14T09:41:45Z
user: login: lobkovilya
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T09:59:49Z
user: login: jijiechen
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-14T11:08:08Z
user: login: lobkovilya
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T20:09:22Z
user: login: jijiechen
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T07:58:14Z
user: login: lobkovilya
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T08:38:42Z
user: login: jijiechen
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T08:56:58Z
user: login: lobkovilya
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T11:36:39Z

Review comments:
diff_hunk: @@ -230,10 +230,10 @@ var _ = Describe("Updater", func() {
 				Total:     3,
 			},
 		}),
-		Entry("should count healthy DPPs", dpProxiesTestCase{
+		Entry("should count healthy DPPs and use MeshService.ports[].targetPort to select DP inbound", dpProxiesTestCase{
 			meshService: samples.MeshServiceBackendBuilder().
 				WithDataplaneTagsSelectorKV("app", "backend").
-				AddIntPort(builders.FirstInboundPort+1, builders.FirstInboundServicePort+1, core_mesh.ProtocolHTTP),
path: pkg/core/resources/apis/meshservice/status/updater_test.go
user: login: jijiechen
body: The positions of parameters was in wrong order in previous code: service port should be the first argument, and DP port is  the second.
created_at: 2025-03-14T09:40:01Z
author_association: MEMBER
diff_hunk: @@ -230,10 +230,10 @@ var _ = Describe("Updater", func() {
 				Total:     3,
 			},
 		}),
-		Entry("should count healthy DPPs", dpProxiesTestCase{
+		Entry("should count healthy DPPs and use MeshService.ports[].targetPort to select DP inbound", dpProxiesTestCase{
 			meshService: samples.MeshServiceBackendBuilder().
 				WithDataplaneTagsSelectorKV("app", "backend").
-				AddIntPort(builders.FirstInboundPort+1, builders.FirstInboundServicePort+1, core_mesh.ProtocolHTTP),
path: pkg/core/resources/apis/meshservice/status/updater_test.go
user: login: jijiechen
body: `MeshServiceBuilder` Sorce: https://github.com/kumahq/kuma/blob/release-2.10/pkg/test/resources/builders/meshservice_builder.go#L67-L77
created_at: 2025-03-14T09:41:45Z
author_association: MEMBER
diff_hunk: @@ -195,8 +196,16 @@ func dpInboundForMeshServicePort(inbounds []*mesh_proto.Dataplane_Networking_Inb
 		if port.Name != "" && inbound.Name == port.Name {
path: pkg/core/resources/apis/meshservice/status/updater.go
user: login: lobkovilya
body: I'm not sure if this is actually correct. I don't think in Kubernetes when a service's port name matches the pod's port name then it's considered a match https://kubernetes.io/docs/reference/kubernetes-api/service-resources/service-v1/#ServiceSpec:

> ports.name (string)
>
> The name of this port within the service. This must be a DNS_LABEL. All ports within a ServiceSpec must have unique names. When considering the endpoints for a Service, this must match the 'name' field in the EndpointPort. Optional if only one ServicePort is defined on this service.

Service's port name is used only for matching with EndpointPort
created_at: 2025-03-14T09:59:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -195,8 +196,16 @@ func dpInboundForMeshServicePort(inbounds []*mesh_proto.Dataplane_Networking_Inb
 		if port.Name != "" && inbound.Name == port.Name {
path: pkg/core/resources/apis/meshservice/status/updater.go
user: login: jijiechen
body: So let's remove this statement? I think it's sufficient to only use the port number/name matching. My only concern is whether we always have such value of service target port in both Universal and Kubernetes scenarios 
created_at: 2025-03-14T11:08:08Z
author_association: MEMBER
diff_hunk: @@ -195,8 +196,16 @@ func dpInboundForMeshServicePort(inbounds []*mesh_proto.Dataplane_Networking_Inb
 		if port.Name != "" && inbound.Name == port.Name {
path: pkg/core/resources/apis/meshservice/status/updater.go
user: login: lobkovilya
body: Yes, let's remove this statement.


> My only concern is whether we always have such value of service target port in both Universal and Kubernetes scenarios

On Universal, we always set it to `inbound.Port`: https://github.com/kumahq/kuma/blob/bf28b95f72d634db11cb2378b38d1f022eb4d55f/pkg/core/resources/apis/meshservice/generate/generator.go#L96

On Kubernetes, we set whatever was set in the original Kubernetes Service: https://github.com/kumahq/kuma/blob/3cdbe225910a51ff1520853be20671dd00f10ea4/pkg/plugins/runtime/k8s/controllers/meshservice_controller.go#L403

So I guess it won't be a problem.

But you definitely need to mention this in UPGRADE.md as it's a breaking change.
created_at: 2025-03-14T20:09:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -195,8 +196,16 @@ func dpInboundForMeshServicePort(inbounds []*mesh_proto.Dataplane_Networking_Inb
 		if port.Name != "" && inbound.Name == port.Name {
path: pkg/core/resources/apis/meshservice/status/updater.go
user: login: jijiechen
body: I tried to compare the behaviour between 2.9 and 2.10. It turns out that the issue (this PR fixes) was newly introduced in this version 2.10: the behaviour from an end user's perspective is not changing, so there is no need to update the `UPGRADE.md`.

The `name` on DP inbound port was automatically assigned from MeshService after a port is matched using `targetPort` in 2.9: https://github.com/kumahq/kuma/commit/bf9c68f475af3eafc17be292204abad6d598619a

And the MeshService updater (the file this PR is trying to change) was also trying to compare this `name` from inbound with MeshService port names, this is also introduced in 2.9: 
https://github.com/kumahq/kuma/commit/11aa52ae0345b483a14d761e3ff4b1f580f398e6#diff-937cc279a26d2767aaca0b4e8cd1d880c38253918c46ddebb01192550f66b7deR146-R187

So, the updater in 2.9 was actually implicitly using `targetPort` because the `name` was assigned to DP inbound only after the port was matching one on MeshService with `targetPort`.  

The issue happens in this release because we changed the logic assigning the `name` of DP inbound port earlier in this PR https://github.com/kumahq/kuma/pull/12783 and we did not change the updater here. So this PR is here to help.
created_at: 2025-03-17T07:58:14Z
author_association: MEMBER
diff_hunk: @@ -176,32 +176,21 @@ func buildDataplaneProxies(
 			}
 		}
 		healthyInbounds := 0
-		for _, port := range ports {
-			if inbound := dpInboundForMeshServicePort(dpp.Spec.GetNetworking().Inbound, port); inbound != nil {
-				if inbound.State == mesh_proto.Dataplane_Networking_Inbound_Ready {
+		for _, meshServicePort := range meshServicePorts {
+			for _, dpInbound := range dpp.Spec.GetNetworking().Inbound {
+				if dpInbound.State == mesh_proto.Dataplane_Networking_Inbound_Ready &&
+					meshservice.MatchInboundWithMeshServicePort(dpInbound, meshServicePort) {
path: pkg/core/resources/apis/meshservice/status/updater.go
user: login: lobkovilya
body: once at least one inbound is matched you can stop iterating over the inbounds and move on to the next port
created_at: 2025-03-17T08:38:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -176,32 +176,21 @@ func buildDataplaneProxies(
 			}
 		}
 		healthyInbounds := 0
-		for _, port := range ports {
-			if inbound := dpInboundForMeshServicePort(dpp.Spec.GetNetworking().Inbound, port); inbound != nil {
-				if inbound.State == mesh_proto.Dataplane_Networking_Inbound_Ready {
+		for _, meshServicePort := range meshServicePorts {
+			for _, dpInbound := range dpp.Spec.GetNetworking().Inbound {
+				if dpInbound.State == mesh_proto.Dataplane_Networking_Inbound_Ready &&
+					meshservice.MatchInboundWithMeshServicePort(dpInbound, meshServicePort) {
path: pkg/core/resources/apis/meshservice/status/updater.go
user: login: jijiechen
body: Good point
created_at: 2025-03-17T08:56:58Z
author_association: MEMBER

PR comments:
user: login: github-actions[bot]
created_at: 2025-03-14T09:39:51Z
body: ## Reviewer Checklist
:mag: Each of these sections need to be checked by the reviewer of the PR :mag::
If something doesn't apply please check the box and add a justification if the reason is non obvious.
- [ ] Is the PR title satisfactory? Is this part of a larger feature and should be grouped using `> Changelog`?
- [ ] PR description is clear and complete. It [Links to relevant issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword) as well as docs and UI issues
- [ ] This will not break child repos: it doesn't hardcode values (.e.g "kumahq" as an image registry)
- [ ] IPv6 is taken into account (.e.g: no string concatenation of host port)
- [ ] Tests (Unit test, E2E tests, manual test on universal and k8s)
    - Don't forget `ci/` labels to run additional/fewer tests
- [ ] Does this contain a change that needs to be notified to users? In this case, [`UPGRADE.md`](../blob/master/UPGRADE.md) should be updated.
- [ ] Does it need to be backported according to the [backporting policy](../blob/master/CONTRIBUTING.md#backporting)? ([this](https://github.com/kumahq/kuma/actions/workflows/auto-backport.yaml) GH action will add "backport" label based on these [file globs](https://github.com/kumahq/kuma/blob/master/.github/workflows/auto-backport.yaml#L6), if you want to prevent it from adding the "backport" label use [no-backport-autolabel](https://github.com/kumahq/kuma/blob/master/.github/workflows/auto-backport.yaml#L8) label)
<!-- Sticky Pull Request CommentPR reviewer checklist -->
author_association: CONTRIBUTOR
user: login: jijiechen
created_at: 2025-03-14T09:44:31Z
body: I didn't add new test cases because there is already a test case in the existing code base and it covered the scenario. The reason why it didn't fail (as expected without the fix in this PR) in original code is because the test case was not written correctly.
author_association: MEMBER