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