This is an automated email from the ASF dual-hosted git repository.

github-actions[bot] pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/dubbo-kubernetes.git


The following commit(s) were added to refs/heads/master by this push:
     new 80922c2b Added probe injection feature (#976)
80922c2b is described below

commit 80922c2b9ee8c7cd9d2568c0390c5f5d97724b1c
Author: mfordjody <[email protected]>
AuthorDate: Wed Jul 22 00:55:05 2026 +0800

    Added probe injection feature (#976)
---
 pkg/kube/inject/app_probe.go        | 105 +++++++++++++++++++-
 pkg/kube/inject/app_probe_test.go   | 191 ++++++++++++++++++++++++++++++++++++
 pkg/kube/inject/webhook.go          |   2 +
 samples/moviereview/README.md       |   7 +-
 samples/moviereview/deployment.yaml |  24 ++---
 5 files changed, 312 insertions(+), 17 deletions(-)

diff --git a/pkg/kube/inject/app_probe.go b/pkg/kube/inject/app_probe.go
index 7cf0ec53..06df2cac 100644
--- a/pkg/kube/inject/app_probe.go
+++ b/pkg/kube/inject/app_probe.go
@@ -16,7 +16,13 @@
 
 package inject
 
-import corev1 "k8s.io/api/core/v1"
+import (
+       "strconv"
+       "strings"
+
+       corev1 "k8s.io/api/core/v1"
+       "k8s.io/apimachinery/pkg/util/intstr"
+)
 
 func FindProxy(pod *corev1.Pod) *corev1.Container {
        return FindContainerFromPod(ProxyContainerName, pod)
@@ -28,3 +34,100 @@ func FindContainerFromPod(name string, pod *corev1.Pod) 
*corev1.Container {
        }
        return FindContainer(name, pod.Spec.Containers)
 }
+
+// RewriteAppProbes moves HTTP probes aimed at the application port onto the
+// inbound sidecar port.
+//
+// The CNI rules REJECT every inbound TCP port of a managed Pod except the
+// sidecar's, so the kubelet can never reach a probe pointed at the application
+// port. The sidecar forwards its listen port to 127.0.0.1:<application port>,
+// which means the rewritten probe still checks the same endpoint on the same
+// container. Without this, application manifests have to hardcode the sidecar
+// port and stop working outside the mesh.
+//
+// Only httpGet probes whose port resolves to the port the sidecar actually
+// forwards to are touched; nothing forwards the other ports, so rewriting them
+// would point the kubelet at an endpoint that does not exist.
+func RewriteAppProbes(pod *corev1.Pod) {
+       inbound := FindContainerFromPod(ProxylessGRPCInboundContainerName, pod)
+       if inbound == nil {
+               return
+       }
+       upstreamPort, found := inboundUpstreamPort(inbound)
+       if !found {
+               return
+       }
+       listenPort := inboundListenPort(inbound)
+       for i := range pod.Spec.Containers {
+               container := &pod.Spec.Containers[i]
+               if container.Name == ProxylessGRPCInboundContainerName {
+                       continue
+               }
+               for _, probe := range []*corev1.Probe{container.ReadinessProbe, 
container.LivenessProbe, container.StartupProbe} {
+                       rewriteProbePort(probe, container, upstreamPort, 
listenPort)
+               }
+       }
+}
+
+func rewriteProbePort(probe *corev1.Probe, container *corev1.Container, from, 
to int32) {
+       if probe == nil || probe.HTTPGet == nil {
+               return
+       }
+       port, resolved := resolveProbePort(probe.HTTPGet.Port, container)
+       if !resolved || port != from {
+               return
+       }
+       probe.HTTPGet.Port = intstr.FromInt32(to)
+}
+
+// resolveProbePort turns a probe port into a number, following the container's
+// named ports when the probe refers to one.
+func resolveProbePort(port intstr.IntOrString, container *corev1.Container) 
(int32, bool) {
+       if port.Type == intstr.Int {
+               return port.IntVal, true
+       }
+       for _, containerPort := range container.Ports {
+               if containerPort.Name == port.StrVal {
+                       return containerPort.ContainerPort, true
+               }
+       }
+       return 0, false
+}
+
+// inboundUpstreamPort reads the port the sidecar forwards to from its
+// `--upstream <host>:<port>` argument. Reading it back from the rendered
+// container keeps this in step with the template that produced it.
+func inboundUpstreamPort(container *corev1.Container) (int32, bool) {
+       if value, found := argValue(container.Args, "--upstream"); found {
+               if _, portText, ok := strings.Cut(value, ":"); ok {
+                       if port, err := strconv.ParseInt(portText, 10, 32); err 
== nil {
+                               return int32(port), true
+                       }
+               }
+       }
+       return 0, false
+}
+
+func inboundListenPort(container *corev1.Container) int32 {
+       if value, found := argValue(container.Args, "--listen"); found {
+               if _, portText, ok := strings.Cut(value, ":"); ok {
+                       if port, err := strconv.ParseInt(portText, 10, 32); err 
== nil {
+                               return int32(port)
+                       }
+               }
+       }
+       return ProxylessGRPCInboundPort
+}
+
+// argValue supports both `--flag value` and `--flag=value`.
+func argValue(args []string, name string) (string, bool) {
+       for i, arg := range args {
+               if arg == name && i+1 < len(args) {
+                       return args[i+1], true
+               }
+               if value, found := strings.CutPrefix(arg, name+"="); found {
+                       return value, true
+               }
+       }
+       return "", false
+}
diff --git a/pkg/kube/inject/app_probe_test.go 
b/pkg/kube/inject/app_probe_test.go
new file mode 100644
index 00000000..aebb422f
--- /dev/null
+++ b/pkg/kube/inject/app_probe_test.go
@@ -0,0 +1,191 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements.  See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package inject
+
+import (
+       "testing"
+
+       corev1 "k8s.io/api/core/v1"
+       "k8s.io/apimachinery/pkg/util/intstr"
+)
+
+func httpProbe(port intstr.IntOrString) *corev1.Probe {
+       return &corev1.Probe{ProbeHandler: corev1.ProbeHandler{HTTPGet: 
&corev1.HTTPGetAction{
+               Path: "/healthz",
+               Port: port,
+       }}}
+}
+
+func inboundContainer(listen, upstream string) corev1.Container {
+       return corev1.Container{
+               Name:  ProxylessGRPCInboundContainerName,
+               Image: "kdubbo/dubbod:latest",
+               Args:  []string{"grpc-inbound", "--listen", listen, 
"--upstream", upstream},
+               Ports: []corev1.ContainerPort{{Name: "grpc-inbound", 
ContainerPort: ProxylessGRPCInboundPort}},
+       }
+}
+
+func appContainer(probes ...*corev1.Probe) corev1.Container {
+       c := corev1.Container{
+               Name:  "app",
+               Image: "example/app:latest",
+               Ports: []corev1.ContainerPort{{Name: "http", ContainerPort: 
9080}},
+       }
+       if len(probes) > 0 {
+               c.ReadinessProbe = probes[0]
+       }
+       if len(probes) > 1 {
+               c.LivenessProbe = probes[1]
+       }
+       if len(probes) > 2 {
+               c.StartupProbe = probes[2]
+       }
+       return c
+}
+
+func podWith(containers ...corev1.Container) *corev1.Pod {
+       return &corev1.Pod{Spec: corev1.PodSpec{Containers: containers}}
+}
+
+// The point of the rewrite: a manifest that names its own port keeps working 
once
+// the CNI rules make that port unreachable from the kubelet.
+func TestRewriteAppProbesMovesApplicationPortToSidecar(t *testing.T) {
+       pod := podWith(
+               appContainer(httpProbe(intstr.FromInt32(9080)), 
httpProbe(intstr.FromInt32(9080)), httpProbe(intstr.FromInt32(9080))),
+               inboundContainer(":15080", "127.0.0.1:9080"),
+       )
+       RewriteAppProbes(pod)
+       app := pod.Spec.Containers[0]
+       for name, probe := range map[string]*corev1.Probe{
+               "readiness": app.ReadinessProbe,
+               "liveness":  app.LivenessProbe,
+               "startup":   app.StartupProbe,
+       } {
+               if got := probe.HTTPGet.Port.IntValue(); got != 
ProxylessGRPCInboundPort {
+                       t.Fatalf("%s probe port = %d, want %d", name, got, 
ProxylessGRPCInboundPort)
+               }
+               if probe.HTTPGet.Path != "/healthz" {
+                       t.Fatalf("%s probe path changed to %q", name, 
probe.HTTPGet.Path)
+               }
+       }
+}
+
+func TestRewriteAppProbesResolvesNamedPort(t *testing.T) {
+       pod := podWith(
+               appContainer(httpProbe(intstr.FromString("http"))),
+               inboundContainer(":15080", "127.0.0.1:9080"),
+       )
+       RewriteAppProbes(pod)
+       if got := 
pod.Spec.Containers[0].ReadinessProbe.HTTPGet.Port.IntValue(); got != 
ProxylessGRPCInboundPort {
+               t.Fatalf("port = %d, want %d", got, ProxylessGRPCInboundPort)
+       }
+}
+
+// Nothing forwards other ports, so pointing the kubelet at the sidecar for 
them
+// would break a probe that the mesh happens to leave working.
+func TestRewriteAppProbesLeavesOtherPortsAlone(t *testing.T) {
+       pod := podWith(
+               appContainer(httpProbe(intstr.FromInt32(8081))),
+               inboundContainer(":15080", "127.0.0.1:9080"),
+       )
+       RewriteAppProbes(pod)
+       if got := 
pod.Spec.Containers[0].ReadinessProbe.HTTPGet.Port.IntValue(); got != 8081 {
+               t.Fatalf("port = %d, want 8081 unchanged", got)
+       }
+}
+
+func TestRewriteAppProbesIgnoresNonHTTPProbes(t *testing.T) {
+       app := appContainer()
+       app.ReadinessProbe = &corev1.Probe{ProbeHandler: corev1.ProbeHandler{
+               TCPSocket: &corev1.TCPSocketAction{Port: 
intstr.FromInt32(9080)},
+       }}
+       app.LivenessProbe = &corev1.Probe{ProbeHandler: corev1.ProbeHandler{
+               Exec: &corev1.ExecAction{Command: []string{"true"}},
+       }}
+       pod := podWith(app, inboundContainer(":15080", "127.0.0.1:9080"))
+       RewriteAppProbes(pod)
+       if got := 
pod.Spec.Containers[0].ReadinessProbe.TCPSocket.Port.IntValue(); got != 9080 {
+               t.Fatalf("tcpSocket port = %d, want 9080 unchanged", got)
+       }
+       if pod.Spec.Containers[0].LivenessProbe.Exec == nil {
+               t.Fatal("exec probe was modified")
+       }
+}
+
+// An uninjected Pod must come out untouched, otherwise the probe would point 
at a
+// port no container listens on.
+func TestRewriteAppProbesNoopWithoutSidecar(t *testing.T) {
+       pod := podWith(appContainer(httpProbe(intstr.FromInt32(9080))))
+       RewriteAppProbes(pod)
+       if got := 
pod.Spec.Containers[0].ReadinessProbe.HTTPGet.Port.IntValue(); got != 9080 {
+               t.Fatalf("port = %d, want 9080 unchanged", got)
+       }
+}
+
+func TestRewriteAppProbesNoopWhenUpstreamUnknown(t *testing.T) {
+       inbound := inboundContainer(":15080", "127.0.0.1:9080")
+       inbound.Args = []string{"grpc-inbound", "--listen", ":15080"}
+       pod := podWith(appContainer(httpProbe(intstr.FromInt32(9080))), inbound)
+       RewriteAppProbes(pod)
+       if got := 
pod.Spec.Containers[0].ReadinessProbe.HTTPGet.Port.IntValue(); got != 9080 {
+               t.Fatalf("port = %d, want 9080 unchanged", got)
+       }
+}
+
+func TestRewriteAppProbesFollowsSidecarListenPort(t *testing.T) {
+       pod := podWith(
+               appContainer(httpProbe(intstr.FromInt32(9080))),
+               inboundContainer(":15081", "127.0.0.1:9080"),
+       )
+       RewriteAppProbes(pod)
+       if got := 
pod.Spec.Containers[0].ReadinessProbe.HTTPGet.Port.IntValue(); got != 15081 {
+               t.Fatalf("port = %d, want 15081", got)
+       }
+}
+
+func TestRewriteAppProbesAcceptsEqualsSyntax(t *testing.T) {
+       inbound := inboundContainer(":15080", "127.0.0.1:9080")
+       inbound.Args = []string{"grpc-inbound", "--listen=:15080", 
"--upstream=127.0.0.1:9080"}
+       pod := podWith(appContainer(httpProbe(intstr.FromInt32(9080))), inbound)
+       RewriteAppProbes(pod)
+       if got := 
pod.Spec.Containers[0].ReadinessProbe.HTTPGet.Port.IntValue(); got != 
ProxylessGRPCInboundPort {
+               t.Fatalf("port = %d, want %d", got, ProxylessGRPCInboundPort)
+       }
+}
+
+// Running twice must not move an already-rewritten probe again.
+func TestRewriteAppProbesIsIdempotent(t *testing.T) {
+       pod := podWith(
+               appContainer(httpProbe(intstr.FromInt32(9080))),
+               inboundContainer(":15080", "127.0.0.1:9080"),
+       )
+       RewriteAppProbes(pod)
+       RewriteAppProbes(pod)
+       if got := 
pod.Spec.Containers[0].ReadinessProbe.HTTPGet.Port.IntValue(); got != 
ProxylessGRPCInboundPort {
+               t.Fatalf("port = %d, want %d", got, ProxylessGRPCInboundPort)
+       }
+}
+
+func TestRewriteAppProbesLeavesSidecarProbeAlone(t *testing.T) {
+       inbound := inboundContainer(":15080", "127.0.0.1:9080")
+       inbound.ReadinessProbe = httpProbe(intstr.FromInt32(9080))
+       pod := podWith(appContainer(), inbound)
+       RewriteAppProbes(pod)
+       if got := 
pod.Spec.Containers[1].ReadinessProbe.HTTPGet.Port.IntValue(); got != 9080 {
+               t.Fatalf("sidecar probe port = %d, want 9080 unchanged", got)
+       }
+}
diff --git a/pkg/kube/inject/webhook.go b/pkg/kube/inject/webhook.go
index f9629327..f816aa9d 100644
--- a/pkg/kube/inject/webhook.go
+++ b/pkg/kube/inject/webhook.go
@@ -458,6 +458,8 @@ func postProcessPod(pod *corev1.Pod, injectedPod 
corev1.Pod, req InjectionParame
                if err := addApplicationContainerConfig(pod, req); err != nil {
                        return err
                }
+               // Must run after the sidecar is merged in: the rewrite reads 
the port it forwards to.
+               RewriteAppProbes(pod)
        }
 
        if err := reorderPod(pod, req); err != nil {
diff --git a/samples/moviereview/README.md b/samples/moviereview/README.md
index 803e7743..6406d031 100644
--- a/samples/moviereview/README.md
+++ b/samples/moviereview/README.md
@@ -2,9 +2,8 @@
 
 moviepage / details / reviews(v1,v2,v3) / ratings 六个服务,应用代码本身是裸 HTTP、监听 
9080,不依赖任何 Dubbo SDK。
 
-**这个示例必须在开启注入的 namespace 里运行。** 注入后 CNI 会拦截 pod 入站流量,
-9080 对 kubelet 不可达,健康检查只能走 sidecar 的 15080 端口 —— `deployment.yaml`
-里的探针端口就是按这个前提写的。不打注入标签,Pod 会因为探针失败而反复重启。
+这组清单不含任何网格专用字段,作为普通 Kubernetes 应用可以独立部署。开启注入后,
+注入器会把探针端口改写到 sidecar 上,无需改动清单。
 
 ### 构建镜像
 
@@ -18,7 +17,7 @@ PLATFORM=linux/amd64 samples/moviereview/build.sh             
 # 集群节点架
 
 ```bash
 kubectl create ns moviereview
-kubectl label ns moviereview dubbo-injection=enabled   # 必需
+kubectl label ns moviereview dubbo-injection=enabled   # 可选,纳入网格时才需要
 kubectl apply -f samples/moviereview/deployment.yaml
 kubectl -n moviereview rollout status deploy/moviepage
 kubectl -n moviereview get svc frontend
diff --git a/samples/moviereview/deployment.yaml 
b/samples/moviereview/deployment.yaml
index 37e969ec..d572022b 100644
--- a/samples/moviereview/deployment.yaml
+++ b/samples/moviereview/deployment.yaml
@@ -192,13 +192,13 @@ spec:
         readinessProbe:
           httpGet:
             path: /healthz
-            port: 15080
+            port: 9080
           initialDelaySeconds: 3
           periodSeconds: 5
         livenessProbe:
           httpGet:
             path: /healthz
-            port: 15080
+            port: 9080
           initialDelaySeconds: 10
           periodSeconds: 10
 ---
@@ -234,13 +234,13 @@ spec:
         readinessProbe:
           httpGet:
             path: /healthz
-            port: 15080
+            port: 9080
           initialDelaySeconds: 3
           periodSeconds: 5
         livenessProbe:
           httpGet:
             path: /healthz
-            port: 15080
+            port: 9080
           initialDelaySeconds: 10
           periodSeconds: 10
 ---
@@ -276,13 +276,13 @@ spec:
         readinessProbe:
           httpGet:
             path: /healthz
-            port: 15080
+            port: 9080
           initialDelaySeconds: 3
           periodSeconds: 5
         livenessProbe:
           httpGet:
             path: /healthz
-            port: 15080
+            port: 9080
           initialDelaySeconds: 10
           periodSeconds: 10
 ---
@@ -318,13 +318,13 @@ spec:
         readinessProbe:
           httpGet:
             path: /healthz
-            port: 15080
+            port: 9080
           initialDelaySeconds: 3
           periodSeconds: 5
         livenessProbe:
           httpGet:
             path: /healthz
-            port: 15080
+            port: 9080
           initialDelaySeconds: 10
           periodSeconds: 10
 ---
@@ -363,13 +363,13 @@ spec:
         readinessProbe:
           httpGet:
             path: /healthz
-            port: 15080
+            port: 9080
           initialDelaySeconds: 3
           periodSeconds: 5
         livenessProbe:
           httpGet:
             path: /healthz
-            port: 15080
+            port: 9080
           initialDelaySeconds: 10
           periodSeconds: 10
 ---
@@ -408,12 +408,12 @@ spec:
         readinessProbe:
           httpGet:
             path: /healthz
-            port: 15080
+            port: 9080
           initialDelaySeconds: 3
           periodSeconds: 5
         livenessProbe:
           httpGet:
             path: /healthz
-            port: 15080
+            port: 9080
           initialDelaySeconds: 10
           periodSeconds: 10

Reply via email to