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

mfordjody 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 23b8aa2e add moviereview example (#913)
23b8aa2e is described below

commit 23b8aa2e59a72330004c6b92d6a9f20c551c28c7
Author: mfordjody <[email protected]>
AuthorDate: Wed Jun 17 15:00:31 2026 +0800

    add moviereview example (#913)
---
 README.md                                          |   4 +-
 assets/brand/kdubbo-logo-preview.png               | Bin 0 -> 118671 bytes
 assets/brand/kdubbo-logo.png                       | Bin 0 -> 36363 bytes
 assets/brand/kdubbo-logo.svg                       |  37 ++
 assets/brand/[email protected]                    | Bin 0 -> 189804 bytes
 assets/brand/kdubbo-mark.png                       | Bin 0 -> 32929 bytes
 assets/brand/kdubbo-mark.svg                       |  29 ++
 assets/brand/[email protected]                    | Bin 0 -> 198521 bytes
 dubbod/agency/config/config.go                     |   6 +-
 dubbod/agency/options/agent.go                     |  61 ----
 dubbod/agency/options/options.go                   | 103 ------
 dubbod/agency/options/security.go                  | 127 -------
 samples/app/README.md                              |  82 -----
 samples/httpbin/README.md                          |  13 -
 samples/moviereview/README.md                      |   9 +
 samples/moviereview/deployment.yaml                | 365 ++++++++++++++++++++
 samples/moviereview/src/details/Dockerfile         |  11 +
 samples/moviereview/src/details/main.go            |  55 +++
 samples/moviereview/src/moviepage/Dockerfile       |   9 +
 samples/moviereview/src/moviepage/app.py           | 371 +++++++++++++++++++++
 samples/moviereview/src/moviepage/requirements.txt |   2 +
 samples/moviereview/src/ratings/Dockerfile         |  11 +
 .../moviereview/src/ratings/main.go                |  51 +--
 samples/moviereview/src/reviews-v1/Dockerfile      |  12 +
 .../src/reviews-v1/ReviewsApplication.java         |  61 ++++
 samples/moviereview/src/reviews-v2/Dockerfile      |   9 +
 samples/moviereview/src/reviews-v2/app.py          |  58 ++++
 .../moviereview/src/reviews-v2/requirements.txt    |   2 +
 samples/moviereview/src/reviews-v3/Dockerfile      |   7 +
 samples/moviereview/src/reviews-v3/server.js       |  90 +++++
 samples/multicluster/README.md                     |  99 ------
 31 files changed, 1174 insertions(+), 510 deletions(-)

diff --git a/README.md b/README.md
index b5a44f4c..4d9eee8e 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,6 @@
-<h1 align="center">Apache Kdubbo</h1>
+<h1 align="center">
+  <img src="assets/brand/kdubbo-logo.svg" alt="Apache Kdubbo" width="420" />
+</h1>
 
 <p align="center">
   <a href="https://pkg.go.dev/github.com/apache/dubbo-kubernetes";>
diff --git a/assets/brand/kdubbo-logo-preview.png 
b/assets/brand/kdubbo-logo-preview.png
new file mode 100644
index 00000000..8e702423
Binary files /dev/null and b/assets/brand/kdubbo-logo-preview.png differ
diff --git a/assets/brand/kdubbo-logo.png b/assets/brand/kdubbo-logo.png
new file mode 100644
index 00000000..124a16bd
Binary files /dev/null and b/assets/brand/kdubbo-logo.png differ
diff --git a/assets/brand/kdubbo-logo.svg b/assets/brand/kdubbo-logo.svg
new file mode 100644
index 00000000..968bb857
--- /dev/null
+++ b/assets/brand/kdubbo-logo.svg
@@ -0,0 +1,37 @@
+<svg xmlns="http://www.w3.org/2000/svg"; viewBox="0 0 960 240" role="img" 
aria-labelledby="title desc">
+  <title id="title">Apache Kdubbo logo</title>
+  <desc id="desc">Apache Kdubbo product logo for a Kubernetes-native Dubbo 
control plane with direct policy delivery to application runtimes.</desc>
+  <defs>
+    <style>
+      .boundary { fill: none; stroke: #2f6bff; stroke-width: 9; 
stroke-linecap: round; stroke-linejoin: round; }
+      .route { fill: none; stroke: #101828; stroke-width: 15; stroke-linecap: 
round; stroke-linejoin: round; }
+      .route-accent { fill: none; stroke: #6941c6; stroke-width: 8; 
stroke-linecap: round; stroke-linejoin: round; }
+      .plane { fill: #101828; }
+      .plane-accent { fill: #00a7d8; }
+      .workload { fill: #ffffff; stroke: #00a7d8; stroke-width: 8; }
+      .cutout { fill: #ffffff; }
+      .apache { fill: #667085; font-family: Inter, "Avenir Next", "Helvetica 
Neue", Arial, sans-serif; font-size: 30px; font-weight: 700; letter-spacing: 
2.8px; }
+      .name { fill: #101828; font-family: Inter, "Avenir Next", "Helvetica 
Neue", Arial, sans-serif; font-size: 82px; font-weight: 800; letter-spacing: 0; 
}
+      .tagline { fill: #475467; font-family: Inter, "Avenir Next", "Helvetica 
Neue", Arial, sans-serif; font-size: 24px; font-weight: 600; letter-spacing: 0; 
}
+    </style>
+  </defs>
+  <g transform="translate(48 28)">
+    <path class="boundary" d="M93 10 167 53v86l-74 43-74-43V53z"/>
+    <path class="route" d="M56 56v92"/>
+    <path class="route" d="M57 102 133 53"/>
+    <path class="route" d="M57 102 133 151"/>
+    <path class="route-accent" d="M71 102 128 65"/>
+    <path class="route-accent" d="M71 102 128 139"/>
+    <circle class="workload" cx="138" cy="51" r="18"/>
+    <circle class="workload" cx="138" cy="153" r="18"/>
+    <circle class="workload" cx="56" cy="102" r="20"/>
+    <rect class="plane" x="32" y="29" width="68" height="32" rx="9"/>
+    <rect class="plane-accent" x="44" y="40" width="32" height="10" rx="5"/>
+    <circle class="cutout" cx="138" cy="51" r="6"/>
+    <circle class="cutout" cx="138" cy="153" r="6"/>
+    <circle class="cutout" cx="56" cy="102" r="7"/>
+  </g>
+  <text class="apache" x="252" y="70">APACHE</text>
+  <text class="name" x="250" y="142">Kdubbo</text>
+  <text class="tagline" x="254" y="184">Sidecarless Dubbo control plane for 
Kubernetes</text>
+</svg>
diff --git a/assets/brand/[email protected] b/assets/brand/[email protected]
new file mode 100644
index 00000000..9a07aaf5
Binary files /dev/null and b/assets/brand/[email protected] differ
diff --git a/assets/brand/kdubbo-mark.png b/assets/brand/kdubbo-mark.png
new file mode 100644
index 00000000..af91c223
Binary files /dev/null and b/assets/brand/kdubbo-mark.png differ
diff --git a/assets/brand/kdubbo-mark.svg b/assets/brand/kdubbo-mark.svg
new file mode 100644
index 00000000..d787deb9
--- /dev/null
+++ b/assets/brand/kdubbo-mark.svg
@@ -0,0 +1,29 @@
+<svg xmlns="http://www.w3.org/2000/svg"; viewBox="0 0 512 512" role="img" 
aria-labelledby="title desc">
+  <title id="title">Apache Kdubbo mark</title>
+  <desc id="desc">A clean vector mark for a Kubernetes-native Dubbo control 
plane that sends policy directly to application runtimes without sidecar 
proxies.</desc>
+  <defs>
+    <style>
+      .boundary { fill: none; stroke: #2f6bff; stroke-width: 22; 
stroke-linecap: round; stroke-linejoin: round; }
+      .route { fill: none; stroke: #101828; stroke-width: 36; stroke-linecap: 
round; stroke-linejoin: round; }
+      .route-accent { fill: none; stroke: #6941c6; stroke-width: 18; 
stroke-linecap: round; stroke-linejoin: round; }
+      .plane { fill: #101828; }
+      .plane-accent { fill: #00a7d8; }
+      .workload { fill: #ffffff; stroke: #00a7d8; stroke-width: 20; }
+      .cutout { fill: #ffffff; }
+    </style>
+  </defs>
+  <path class="boundary" d="M256 54 417 147v186L256 426 95 333V147z"/>
+  <path class="route" d="M176 154v204"/>
+  <path class="route" d="M178 256 345 148"/>
+  <path class="route" d="M178 256 345 364"/>
+  <path class="route-accent" d="M210 256 334 176"/>
+  <path class="route-accent" d="M210 256 334 336"/>
+  <circle class="workload" cx="354" cy="144" r="37"/>
+  <circle class="workload" cx="354" cy="368" r="37"/>
+  <circle class="workload" cx="176" cy="256" r="42"/>
+  <rect class="plane" x="123" y="96" width="150" height="70" rx="20"/>
+  <rect class="plane-accent" x="150" y="121" width="70" height="20" rx="10"/>
+  <circle class="cutout" cx="354" cy="144" r="13"/>
+  <circle class="cutout" cx="354" cy="368" r="13"/>
+  <circle class="cutout" cx="176" cy="256" r="14"/>
+</svg>
diff --git a/assets/brand/[email protected] b/assets/brand/[email protected]
new file mode 100644
index 00000000..c2e487be
Binary files /dev/null and b/assets/brand/[email protected] differ
diff --git a/dubbod/agency/config/config.go b/dubbod/agency/config/config.go
index 2c38adda..a008b88c 100644
--- a/dubbod/agency/config/config.go
+++ b/dubbod/agency/config/config.go
@@ -18,10 +18,14 @@ package config
 
 import "strings"
 
+const (
+       local = "localhost"
+)
+
 func GetDubboSan(discoveryAddress string) string {
        discHost := strings.Split(discoveryAddress, ":")[0]
        // For local debugging - the discoveryAddress is set to localhost, but 
the cert issued for normal PA.
-       if discHost == "localhost" {
+       if discHost == local {
                discHost = "dubbod.dubbo-system.svc"
        }
        return discHost
diff --git a/dubbod/agency/options/agent.go b/dubbod/agency/options/agent.go
deleted file mode 100644
index 6f484f20..00000000
--- a/dubbod/agency/options/agent.go
+++ /dev/null
@@ -1,61 +0,0 @@
-//
-// 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 options
-
-import (
-       "os"
-       "path/filepath"
-       "strings"
-
-       dubboagent "github.com/apache/dubbo-kubernetes/pkg/dubboagency"
-       serviceRouteIndex "github.com/kdubbo/api/mesh/v1alpha1"
-)
-
-const xdsHeaderPrefix = "XDS_HEADER_"
-
-func NewAgentOptions(proxy *ProxyArgs, cfg *serviceRouteIndex.ProxyConfig, sds 
dubboagent.SDSServiceFactory) *dubboagent.AgentOptions {
-       o := &dubboagent.AgentOptions{
-               XDSRootCerts:               xdsRootCA,
-               CARootCerts:                caRootCA,
-               XDSHeaders:                 map[string]string{},
-               XdsUdsPath:                 filepath.Join(cfg.ConfigPath, 
"XDS"),
-               GRPCBootstrapPath:          grpcBootstrapEnv,
-               ProxyType:                  proxy.Type,
-               ServiceNode:                proxy.ServiceNode(),
-               EnableDynamicProxyConfig:   enableProxyConfigXdsEnv,
-               SDSFactory:                 sds,
-               WorkloadIdentitySocketFile: workloadIdentitySocketFile,
-               ProxyIPAddresses:           proxy.IPAddresses,
-               ProxyDomain:                proxy.DNSDomain,
-               DubbodSAN:                  dubbodSAN.Get(),
-       }
-       extractXDSHeadersFromEnv(o)
-       return o
-}
-
-func extractXDSHeadersFromEnv(o *dubboagent.AgentOptions) {
-       envs := os.Environ()
-       for _, e := range envs {
-               if strings.HasPrefix(e, xdsHeaderPrefix) {
-                       parts := strings.SplitN(e, "=", 2)
-                       if len(parts) != 2 {
-                               continue
-                       }
-                       o.XDSHeaders[parts[0][len(xdsHeaderPrefix):]] = parts[1]
-               }
-       }
-}
diff --git a/dubbod/agency/options/options.go b/dubbod/agency/options/options.go
deleted file mode 100644
index 3a0c184b..00000000
--- a/dubbod/agency/options/options.go
+++ /dev/null
@@ -1,103 +0,0 @@
-//
-// 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 options
-
-import (
-       "fmt"
-       "path/filepath"
-       "time"
-
-       "github.com/apache/dubbo-kubernetes/pkg/jwt"
-
-       "github.com/apache/dubbo-kubernetes/pkg/config/constants"
-       "github.com/apache/dubbo-kubernetes/pkg/env"
-       "github.com/apache/dubbo-kubernetes/pkg/security"
-)
-
-var (
-       InstanceIPVar   = env.Register("INSTANCE_IP", "", "")
-       PodNameVar      = env.Register("POD_NAME", "", "")
-       PodNamespaceVar = env.Register("POD_NAMESPACE", "", "")
-
-       serviceAccountVar = env.Register("SERVICE_ACCOUNT", "", "Name of 
service account")
-
-       xdsAuthProvider = env.Register("XDS_AUTH_PROVIDER", "", "Provider for 
XDS auth")
-
-       caRootCA = env.Register("CA_ROOT_CA", "",
-               "Explicitly set the root CA to expect for the CA 
connection.").Get()
-
-       pkcs8KeysEnv = env.Register("PKCS8_KEY", false,
-               "Whether to generate PKCS#8 private keys").Get()
-       eccSigAlgEnv          = env.Register("ECC_SIGNATURE_ALGORITHM", "", 
"The type of ECC signature algorithm to use when generating private keys").Get()
-       eccCurvEnv            = env.Register("ECC_CURVE", "P256", "The elliptic 
curve to use when ECC_SIGNATURE_ALGORITHM is set to ECDSA").Get()
-       workloadRSAKeySizeEnv = env.Register("WORKLOAD_RSA_KEY_SIZE", 2048,
-               "Specify the RSA key size to use for workload 
certificates.").Get()
-       workloadIdentitySocketFile = 
env.Register("WORKLOAD_IDENTITY_SOCKET_FILE", 
security.DefaultWorkloadIdentitySocketFile,
-               fmt.Sprintf("SPIRE workload identity SDS socket filename. If 
set, an SDS socket with this name must exist at %s", 
security.WorkloadIdentityPath)).Get()
-
-       secretTTLEnv = env.Register("SECRET_TTL", 24*time.Hour,
-               "The cert lifetime requested by dubbo agent").Get()
-
-       fileDebounceDuration = env.Register("FILE_DEBOUNCE_DURATION", 
100*time.Millisecond,
-               "The duration for which the file read operation is delayed once 
file update is detected").Get()
-
-       grpcBootstrapEnv = env.Register("GRPC_XDS_BOOTSTRAP", 
filepath.Join(constants.ConfigPathDir, "grpc-bootstrap.json"),
-               "Path where gRPC expects to read a bootstrap file. Agent will 
generate one if set.").Get()
-
-       caProviderEnv = env.Register("CA_PROVIDER", "Aegis", "name of 
authentication provider").Get()
-       caEndpointEnv = env.Register("CA_ADDRESS", "", "Address of the spiffe 
certificate provider. Defaults to discoveryAddress").Get()
-
-       clusterIDVar        = env.Register("DUBBO_META_CLUSTER_ID", "", "")
-       fileMountedCertsEnv = env.Register("FILE_MOUNTED_CERTS", false, 
"").Get()
-
-       trustDomainEnv = env.Register("TRUST_DOMAIN", "cluster.local",
-               "The trust domain for spiffe certificates").Get()
-
-       certSigner = env.Register("DUBBO_META_CERT_SIGNER", "",
-               "The cert signer info for workload cert")
-
-       ProxyConfigEnv = env.Register(
-               "PROXY_CONFIG",
-               "",
-               "The proxy configuration. This will be set by the injection - 
gateways will use file mounts.",
-       ).Get()
-
-       outputKeyCertToDir = env.Register("OUTPUT_CERTS", "",
-               "The output directory for the key and certificate. If empty, 
key and certificate will not be saved. "+
-                       "Must be set for VMs using provisioning 
certificates.").Get()
-
-       provCert = env.Register("PROV_CERT", "",
-               "Set to a directory containing provisioned certs, for 
VMs").Get()
-
-       dubbodSAN = env.Register("DUBBOD_SAN", "",
-               "Override the ServerName used to validate Dubbod certificate. "+
-                       "Can be used as an alternative to setting /etc/hosts 
for VMs - discovery address will be an IP:port")
-
-       jwtPolicy = env.Register("JWT_POLICY", jwt.PolicyThirdParty,
-               "The JWT validation policy.")
-
-       credFetcherTypeEnv = env.Register("CREDENTIAL_FETCHER_TYPE", 
security.JWT,
-               "The type of the credential fetcher. Currently supported types 
include GoogleComputeEngine").Get()
-       credIdentityProvider = env.Register("CREDENTIAL_IDENTITY_PROVIDER", 
"GoogleComputeEngine",
-               "The identity provider for credential. Currently default 
supported identity provider is GoogleComputeEngine").Get()
-
-       xdsRootCA = env.Register("XDS_ROOT_CA", "",
-               "Explicitly set the root CA to expect for the XDS 
connection.").Get()
-
-       enableProxyConfigXdsEnv = env.Register("PROXY_CONFIG_XDS_AGENT", false,
-               "If set to true, agent retrieves dynamic proxy-config updates 
via xds channel").Get()
-)
diff --git a/dubbod/agency/options/security.go 
b/dubbod/agency/options/security.go
deleted file mode 100644
index 04a73092..00000000
--- a/dubbod/agency/options/security.go
+++ /dev/null
@@ -1,127 +0,0 @@
-//
-// 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 options
-
-import (
-       "fmt"
-       "os"
-       "strings"
-
-       "github.com/apache/dubbo-kubernetes/dubbod/discovery/pkg/features"
-       
"github.com/apache/dubbo-kubernetes/dubbod/security/pkg/credentialfetcher"
-       
"github.com/apache/dubbo-kubernetes/dubbod/security/pkg/nodeagent/cafile"
-       "github.com/apache/dubbo-kubernetes/pkg/jwt"
-       "github.com/apache/dubbo-kubernetes/pkg/security"
-       serviceRouteIndex "github.com/kdubbo/api/mesh/v1alpha1"
-
-       dubbolog "github.com/apache/dubbo-kubernetes/pkg/log"
-)
-
-var log = dubbolog.RegisterScope("security", "security options debugging")
-
-const caHeaderPrefix = "CA_HEADER_"
-
-func NewSecurityOptions(proxyConfig *serviceRouteIndex.ProxyConfig, stsPort 
int, tokenManagerPlugin string) (*security.Options, error) {
-       o := &security.Options{
-               CAEndpoint:           caEndpointEnv,
-               CAProviderName:       caProviderEnv,
-               DubboCertProvider:    features.DubboCertProvider,
-               OutputKeyCertToDir:   outputKeyCertToDir,
-               ProvCert:             provCert,
-               ClusterID:            clusterIDVar.Get(),
-               FileMountedCerts:     fileMountedCertsEnv,
-               WorkloadNamespace:    PodNamespaceVar.Get(),
-               ServiceAccount:       serviceAccountVar.Get(),
-               XdsAuthProvider:      xdsAuthProvider.Get(),
-               TrustDomain:          trustDomainEnv,
-               WorkloadRSAKeySize:   workloadRSAKeySizeEnv,
-               Pkcs8Keys:            pkcs8KeysEnv,
-               ECCSigAlg:            eccSigAlgEnv,
-               ECCCurve:             eccCurvEnv,
-               SecretTTL:            secretTTLEnv,
-               FileDebounceDuration: fileDebounceDuration,
-               STSPort:              stsPort,
-               CertSigner:           certSigner.Get(),
-               CARootPath:           cafile.CACertFilePath,
-               CertChainFilePath:    security.DefaultCertChainFilePath,
-               KeyFilePath:          security.DefaultKeyFilePath,
-               RootCertFilePath:     security.DefaultRootCertFilePath,
-               CAHeaders:            map[string]string{},
-       }
-
-       o, err := SetupSecurityOptions(proxyConfig, o, jwtPolicy.Get(),
-               credFetcherTypeEnv, credIdentityProvider)
-       if err != nil {
-               return o, err
-       }
-
-       extractCAHeadersFromEnv(o)
-
-       return o, nil
-}
-
-func SetupSecurityOptions(proxyConfig *serviceRouteIndex.ProxyConfig, secOpt 
*security.Options, jwtPolicy,
-       credFetcherTypeEnv, credIdentityProvider string,
-) (*security.Options, error) {
-       // TODO jwtPath
-       switch jwtPolicy {
-       case jwt.PolicyThirdParty:
-               log.Info("JWT policy is third-party-jwt")
-
-       case jwt.PolicyFirstParty:
-               log.Warnf("Using deprecated JWT policy 'first-party-jwt'; 
treating as 'third-party-jwt'")
-
-       default:
-               log.Info("Using existing certs")
-       }
-
-       o := secOpt
-
-       // If not set explicitly, default to the discovery address.
-       if o.CAEndpoint == "" {
-               o.CAEndpoint = proxyConfig.DiscoveryAddress
-               o.CAEndpointSAN = dubbodSAN.Get()
-       }
-
-       o.CredIdentityProvider = credIdentityProvider
-       credFetcher, err := credentialfetcher.NewCredFetcher(credFetcherTypeEnv)
-       if err != nil {
-               return nil, fmt.Errorf("failed to create credential fetcher: 
%v", err)
-       }
-       log.Infof("using credential fetcher of %s type in %s trust domain", 
credFetcherTypeEnv, o.TrustDomain)
-       o.CredFetcher = credFetcher
-
-       if o.ProvCert != "" && o.FileMountedCerts {
-               return nil, fmt.Errorf("invalid options: PROV_CERT and 
FILE_MOUNTED_CERTS are mutually exclusive")
-       }
-       return o, nil
-}
-
-func extractCAHeadersFromEnv(o *security.Options) {
-       envs := os.Environ()
-       for _, e := range envs {
-               if !strings.HasPrefix(e, caHeaderPrefix) {
-                       continue
-               }
-
-               parts := strings.SplitN(e, "=", 2)
-               if len(parts) != 2 {
-                       continue
-               }
-               o.CAHeaders[parts[0][len(caHeaderPrefix):]] = parts[1]
-       }
-}
diff --git a/samples/app/README.md b/samples/app/README.md
deleted file mode 100644
index 872fa8ff..00000000
--- a/samples/app/README.md
+++ /dev/null
@@ -1,82 +0,0 @@
-# 示例说明
-
-## 准备
-
-先构建并更新 `kdubbo/dubbod:debug` 镜像,再执行下面命令。
-
-## 安装
-
-```bash
-kubectl create ns app --dry-run=client -o yaml | kubectl apply -f -
-kubectl label namespace app dubbo-injection=enabled --overwrite
-kubectl apply -f samples/app/deployment.yaml
-kubectl -n app rollout status deploy/nginx-v1 --timeout=180s
-kubectl -n app rollout status deploy/nginx-v2 --timeout=180s
-kubectl -n app rollout status deploy/nginx-consumer --timeout=180s
-```
-
-`dubbo-injection=enabled` 开启后,会自动注入 `grpc-engine`。`xclient` 只主动发起请求,不监听端口;服务端 
Pod 会注入 `xserver` 接收 mTLS 并转发到本地 nginx,Service 会被准入层改到 `targetPort: 15080`,样例 
NetworkPolicy 会禁止从其他 Pod 直连 nginx `80`。
-
-## 配置流量规则
-
-```bash
-kubectl apply -f samples/app/meshservice.yaml
-```
-
-## 查看 xDS 推送结果
-
-```bash
-kubectl -n app logs -f deploy/nginx-consumer
-```
-
-日志里会看到类似输出:
-
-```text
-nginx.app.svc.cluster.local:80 v1=23 endpoints=1,v2=77 endpoints=1
-```
-
-应用容器会拿到这些关键变量:
-
-- `GRPC_XDS_BOOTSTRAP`
-- `XDS_ADDRESS`
-- `CA_ADDRESS`
-
-## 验证流量结果
-
-```bash
-kubectl -n app exec deploy/nginx-consumer -- \
-  dubbod xclient --print-route --expect v1=50,v2=50
-kubectl -n app exec deploy/nginx-consumer -- \
-  dubbod xclient --expect v1=50,v2=50 100 | sort | uniq -c
-```
-
-`--print-route` 里应看到 `tlsMode: DUBBO_MUTUAL`,endpoint 端口应为 `15080`。
-
-确认 Service 也指向 `xserver`:
-
-```bash
-kubectl -n app get svc nginx -o jsonpath='{.spec.ports[0].targetPort}{"\n"}'
-kubectl -n app run plain-curl --rm -i --restart=Never \
-  --image=curlimages/curl:8.5.0 -- \
-  curl -sv --max-time 5 http://nginx.app.svc.cluster.local/
-```
-
-第一条应输出 `15080`,第二条不能返回 `nginx v1` 或 `nginx v2`。
-
-## 在线更新验证
-
-先在一个终端里持续发请求:
-
-```bash
-kubectl -n app exec deploy/nginx-consumer -- dubbod xclient --request-interval 
200ms 200
-```
-
-再在另一个终端里修改 `MeshService` 权重。`nginx-consumer` 不需要重启,同一个 `xclient` 进程会继续使用同一条 
xDS stream,后续请求会按新权重切换。
-
-## 清理
-
-```bash
-kubectl -n app delete meshservice nginx-routing --ignore-not-found=true
-kubectl delete -f samples/app/deployment.yaml --ignore-not-found=true
-kubectl delete ns app
-```
diff --git a/samples/httpbin/README.md b/samples/httpbin/README.md
deleted file mode 100644
index ad8ea232..00000000
--- a/samples/httpbin/README.md
+++ /dev/null
@@ -1,13 +0,0 @@
-# Httpbin 南北向流量示例
-
-这个示例部署默认命名空间的 `httpbin`,并通过共享的 `dxgate-gateway` + `HTTPRoute` 触发 dubbod 托管的固定 
dxgate 网关。
-
-```bash
-kubectl apply -f samples/httpbin/httpbin.yaml
-kubectl get gateway dxgate-gateway
-kubectl get svc dxgate-gateway
-```
-
-访问 `default/dxgate-gateway` 的外部地址即可进入 dxgate,再转发到 
`httpbin.default.svc.cluster.local:8000`。
-
-后续新增 `foo`、`bar` 这类服务时,继续创建 `HTTPRoute` 并把 `parentRefs.name` 指向 
`dxgate-gateway`,不要为每个业务服务再创建一个新的 `Gateway`。
diff --git a/samples/moviereview/README.md b/samples/moviereview/README.md
new file mode 100644
index 00000000..ee875203
--- /dev/null
+++ b/samples/moviereview/README.md
@@ -0,0 +1,9 @@
+## moviereview example
+
+```bash
+kubectl create ns moviereview
+kubectl apply -f samples/moviereview/deployment.yaml
+kubectl -n moviereview get svc frontend
+```
+
+访问 `http://<NodeIP>:30080`。
diff --git a/samples/moviereview/deployment.yaml 
b/samples/moviereview/deployment.yaml
new file mode 100644
index 00000000..92777d57
--- /dev/null
+++ b/samples/moviereview/deployment.yaml
@@ -0,0 +1,365 @@
+# 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.
+
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: moviereview
+  namespace: moviereview
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: frontend
+  namespace: moviereview
+  labels:
+    app: frontend
+spec:
+  type: NodePort
+  selector:
+    app: moviepage
+  ports:
+  - name: http
+    port: 80
+    targetPort: 9080
+    nodePort: 30080
+    protocol: TCP
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: moviepage
+  namespace: moviereview
+  labels:
+    app: moviepage
+spec:
+  selector:
+    app: moviepage
+  ports:
+  - name: http
+    port: 9080
+    targetPort: 9080
+    protocol: TCP
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: details
+  namespace: moviereview
+  labels:
+    app: details
+spec:
+  selector:
+    app: details
+  ports:
+  - name: http
+    port: 9080
+    targetPort: 9080
+    protocol: TCP
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: reviews
+  namespace: moviereview
+  labels:
+    app: reviews
+spec:
+  selector:
+    app: reviews
+  ports:
+  - name: http
+    port: 9080
+    targetPort: 9080
+    protocol: TCP
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: ratings
+  namespace: moviereview
+  labels:
+    app: ratings
+spec:
+  selector:
+    app: ratings
+  ports:
+  - name: http
+    port: 9080
+    targetPort: 9080
+    protocol: TCP
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: moviepage
+  namespace: moviereview
+  labels:
+    app: moviepage
+    version: v1
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: moviepage
+      version: v1
+  template:
+    metadata:
+      labels:
+        app: moviepage
+        version: v1
+    spec:
+      serviceAccountName: moviereview
+      containers:
+      - name: app
+        image: kdubbo/moviereview-moviepage:latest
+        imagePullPolicy: Always
+        ports:
+        - name: http
+          containerPort: 9080
+          protocol: TCP
+        env:
+        - name: DETAILS_URL
+          value: http://details:9080/details
+        - name: REVIEWS_URL
+          value: http://reviews:9080/reviews
+        readinessProbe:
+          httpGet:
+            path: /healthz
+            port: 9080
+          initialDelaySeconds: 3
+          periodSeconds: 5
+        livenessProbe:
+          httpGet:
+            path: /healthz
+            port: 9080
+          initialDelaySeconds: 10
+          periodSeconds: 10
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: details
+  namespace: moviereview
+  labels:
+    app: details
+    version: v1
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: details
+      version: v1
+  template:
+    metadata:
+      labels:
+        app: details
+        version: v1
+    spec:
+      serviceAccountName: moviereview
+      containers:
+      - name: app
+        image: kdubbo/moviereview-details:latest
+        imagePullPolicy: Always
+        ports:
+        - name: http
+          containerPort: 9080
+          protocol: TCP
+        readinessProbe:
+          httpGet:
+            path: /healthz
+            port: 9080
+          initialDelaySeconds: 3
+          periodSeconds: 5
+        livenessProbe:
+          httpGet:
+            path: /healthz
+            port: 9080
+          initialDelaySeconds: 10
+          periodSeconds: 10
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: ratings
+  namespace: moviereview
+  labels:
+    app: ratings
+    version: v1
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: ratings
+      version: v1
+  template:
+    metadata:
+      labels:
+        app: ratings
+        version: v1
+    spec:
+      serviceAccountName: moviereview
+      containers:
+      - name: app
+        image: kdubbo/moviereview-ratings:latest
+        imagePullPolicy: Always
+        ports:
+        - name: http
+          containerPort: 9080
+          protocol: TCP
+        readinessProbe:
+          httpGet:
+            path: /healthz
+            port: 9080
+          initialDelaySeconds: 3
+          periodSeconds: 5
+        livenessProbe:
+          httpGet:
+            path: /healthz
+            port: 9080
+          initialDelaySeconds: 10
+          periodSeconds: 10
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: reviews-v1
+  namespace: moviereview
+  labels:
+    app: reviews
+    version: v1
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: reviews
+      version: v1
+  template:
+    metadata:
+      labels:
+        app: reviews
+        version: v1
+    spec:
+      serviceAccountName: moviereview
+      containers:
+      - name: app
+        image: kdubbo/moviereview-reviews-v1:latest
+        imagePullPolicy: Always
+        ports:
+        - name: http
+          containerPort: 9080
+          protocol: TCP
+        readinessProbe:
+          httpGet:
+            path: /healthz
+            port: 9080
+          initialDelaySeconds: 3
+          periodSeconds: 5
+        livenessProbe:
+          httpGet:
+            path: /healthz
+            port: 9080
+          initialDelaySeconds: 10
+          periodSeconds: 10
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: reviews-v2
+  namespace: moviereview
+  labels:
+    app: reviews
+    version: v2
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: reviews
+      version: v2
+  template:
+    metadata:
+      labels:
+        app: reviews
+        version: v2
+    spec:
+      serviceAccountName: moviereview
+      containers:
+      - name: app
+        image: kdubbo/moviereview-reviews-v2:latest
+        imagePullPolicy: Always
+        ports:
+        - name: http
+          containerPort: 9080
+          protocol: TCP
+        env:
+        - name: RATINGS_URL
+          value: http://ratings:9080/ratings
+        readinessProbe:
+          httpGet:
+            path: /healthz
+            port: 9080
+          initialDelaySeconds: 3
+          periodSeconds: 5
+        livenessProbe:
+          httpGet:
+            path: /healthz
+            port: 9080
+          initialDelaySeconds: 10
+          periodSeconds: 10
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: reviews-v3
+  namespace: moviereview
+  labels:
+    app: reviews
+    version: v3
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: reviews
+      version: v3
+  template:
+    metadata:
+      labels:
+        app: reviews
+        version: v3
+    spec:
+      serviceAccountName: moviereview
+      containers:
+      - name: app
+        image: kdubbo/moviereview-reviews-v3:latest
+        imagePullPolicy: Always
+        ports:
+        - name: http
+          containerPort: 9080
+          protocol: TCP
+        env:
+        - name: RATINGS_URL
+          value: http://ratings:9080/ratings
+        readinessProbe:
+          httpGet:
+            path: /healthz
+            port: 9080
+          initialDelaySeconds: 3
+          periodSeconds: 5
+        livenessProbe:
+          httpGet:
+            path: /healthz
+            port: 9080
+          initialDelaySeconds: 10
+          periodSeconds: 10
diff --git a/samples/moviereview/src/details/Dockerfile 
b/samples/moviereview/src/details/Dockerfile
new file mode 100644
index 00000000..5d706a76
--- /dev/null
+++ b/samples/moviereview/src/details/Dockerfile
@@ -0,0 +1,11 @@
+FROM golang:1.25-alpine AS build
+
+WORKDIR /src
+COPY main.go .
+RUN CGO_ENABLED=0 go build -o /out/details main.go
+
+FROM alpine:3.22
+COPY --from=build /out/details /usr/local/bin/details
+
+EXPOSE 9080
+CMD ["details"]
diff --git a/samples/moviereview/src/details/main.go 
b/samples/moviereview/src/details/main.go
new file mode 100644
index 00000000..5fc787d4
--- /dev/null
+++ b/samples/moviereview/src/details/main.go
@@ -0,0 +1,55 @@
+// 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 main
+
+import (
+       "encoding/json"
+       "log"
+       "net/http"
+)
+
+type movieDetails struct {
+       Title    string   `json:"title"`
+       Year     int      `json:"year"`
+       Director string   `json:"director"`
+       Genres   []string `json:"genres"`
+       Summary  string   `json:"summary"`
+}
+
+func main() {
+       http.HandleFunc("/healthz", func(w http.ResponseWriter, _ 
*http.Request) {
+               _, _ = w.Write([]byte("ok\n"))
+       })
+       http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+               if r.URL.Path != "/" && r.URL.Path != "/details" {
+                       http.NotFound(w, r)
+                       return
+               }
+               writeJSON(w, movieDetails{
+                       Title:    "银河补习班",
+                       Year:     2019,
+                       Director: "Deng Chao",
+                       Genres:   []string{"Drama", "Family"},
+                       Summary:  "A father and son story used here as a small, 
normal microservice domain.",
+               })
+       })
+       log.Fatal(http.ListenAndServe(":9080", nil))
+}
+
+func writeJSON(w http.ResponseWriter, value any) {
+       w.Header().Set("Content-Type", "application/json")
+       _ = json.NewEncoder(w).Encode(value)
+}
diff --git a/samples/moviereview/src/moviepage/Dockerfile 
b/samples/moviereview/src/moviepage/Dockerfile
new file mode 100644
index 00000000..e5769d15
--- /dev/null
+++ b/samples/moviereview/src/moviepage/Dockerfile
@@ -0,0 +1,9 @@
+FROM python:3.12-alpine
+
+WORKDIR /app
+COPY requirements.txt .
+RUN pip install --no-cache-dir -r requirements.txt
+COPY app.py .
+
+EXPOSE 9080
+CMD ["python", "app.py"]
diff --git a/samples/moviereview/src/moviepage/app.py 
b/samples/moviereview/src/moviepage/app.py
new file mode 100644
index 00000000..19fc3ccf
--- /dev/null
+++ b/samples/moviereview/src/moviepage/app.py
@@ -0,0 +1,371 @@
+# 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.
+
+import os
+from html import escape
+
+import requests
+from flask import Flask
+
+
+app = Flask(__name__)
+
+DETAILS_URL = os.getenv("DETAILS_URL", "http://details:9080/details";)
+REVIEWS_URL = os.getenv("REVIEWS_URL", "http://reviews:9080/reviews";)
+
+
+def get_json(url, fallback):
+    try:
+        response = requests.get(url, timeout=2)
+        response.raise_for_status()
+        return response.json()
+    except requests.RequestException as exc:
+        data = fallback.copy()
+        data["error"] = str(exc)
+        return data
+
+
[email protected]("/healthz")
+def healthz():
+    return "ok\n"
+
+
[email protected]("/")
+def index():
+    details = get_json(DETAILS_URL, {"title": "Unknown", "director": "-", 
"year": "-", "genres": []})
+    reviews = get_json(REVIEWS_URL, {"version": "unavailable", "items": [], 
"rating": None})
+    title = escape(str(details.get("title", "Unknown")))
+    year = escape(str(details.get("year", "-")))
+    director = escape(str(details.get("director", "-")))
+    genres = " / ".join(escape(str(genre)) for genre in details.get("genres", 
[]))
+    summary = escape(str(details.get("summary", "")))
+    version = escape(str(reviews.get("version", "unavailable")))
+    review_items = "".join(
+        f"<article 
class='review-card'><span></span><p>{escape(str(item))}</p></article>"
+        for item in reviews.get("items", [])
+    )
+    rating = reviews.get("rating")
+    rating_html = "<p class='rating-empty'>No rating in this version</p>"
+    if rating:
+        color = escape(str(reviews.get("starColor", "#222")))
+        stars = "★" * int(rating.get("stars", 0))
+        rating_html = f"""
+        <p class='stars' style='color: {color}'>{stars}</p>
+        <p class='rating-source'>ratings service · movie-1</p>
+        """
+
+    return f"""<!doctype html>
+<html lang="zh-CN">
+<head>
+  <meta charset="utf-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+  <title>{title}</title>
+  <style>
+    :root {{
+      --ink: #171313;
+      --paper: #f7efe1;
+      --muted: #766957;
+      --line: rgba(23, 19, 19, .14);
+      --wine: #8f2434;
+      --teal: #126e72;
+      --gold: #d6a84f;
+      --night: #201b1a;
+    }}
+
+    * {{ box-sizing: border-box; }}
+
+    body {{
+      min-height: 100vh;
+      margin: 0;
+      color: var(--ink);
+      font-family: "Avenir Next", "Gill Sans", "Trebuchet MS", sans-serif;
+      background:
+        linear-gradient(90deg, rgba(32, 27, 26, .05) 1px, transparent 1px),
+        linear-gradient(rgba(32, 27, 26, .05) 1px, transparent 1px),
+        var(--paper);
+      background-size: 36px 36px;
+    }}
+
+    body::before {{
+      position: fixed;
+      inset: 0;
+      z-index: -1;
+      pointer-events: none;
+      content: "";
+      background:
+        radial-gradient(circle at 18% 14%, rgba(214, 168, 79, .22), 
transparent 28%),
+        linear-gradient(135deg, rgba(143, 36, 52, .13), transparent 34%, 
rgba(18, 110, 114, .13));
+    }}
+
+    main {{
+      width: min(1120px, calc(100% - 32px));
+      margin: 0 auto;
+      padding: 44px 0;
+    }}
+
+    .shell {{
+      display: grid;
+      grid-template-columns: minmax(280px, 380px) minmax(0, 1fr);
+      gap: 34px;
+      align-items: stretch;
+    }}
+
+    .poster {{
+      position: relative;
+      min-height: 620px;
+      overflow: hidden;
+      color: #fff8ea;
+      background:
+        linear-gradient(150deg, rgba(143, 36, 52, .92), rgba(18, 110, 114, 
.80) 55%, #151010),
+        var(--night);
+      border: 1px solid rgba(255, 255, 255, .24);
+      border-radius: 8px;
+      box-shadow: 0 28px 70px rgba(30, 20, 16, .32);
+      isolation: isolate;
+    }}
+
+    .poster::before {{
+      position: absolute;
+      inset: 22px;
+      content: "";
+      border: 1px solid rgba(255, 248, 234, .32);
+      border-radius: 4px;
+    }}
+
+    .poster::after {{
+      position: absolute;
+      right: -44px;
+      bottom: -32px;
+      z-index: -1;
+      content: "影";
+      font-family: Georgia, "Times New Roman", serif;
+      font-size: 240px;
+      line-height: 1;
+      color: rgba(255, 248, 234, .10);
+    }}
+
+    .poster-inner {{
+      position: relative;
+      display: flex;
+      min-height: 620px;
+      flex-direction: column;
+      justify-content: space-between;
+      padding: 36px;
+    }}
+
+    .label {{
+      width: max-content;
+      padding: 6px 10px;
+      border: 1px solid rgba(255, 248, 234, .36);
+      color: #fff8ea;
+      font-size: 12px;
+      font-weight: 800;
+      text-transform: uppercase;
+    }}
+
+    h1 {{
+      margin: 0;
+      font-family: Georgia, "Times New Roman", serif;
+      font-size: 58px;
+      line-height: .98;
+      font-weight: 700;
+    }}
+
+    .meta {{
+      margin: 18px 0 0;
+      color: rgba(255, 248, 234, .76);
+      font-size: 15px;
+      line-height: 1.6;
+    }}
+
+    .content {{
+      display: grid;
+      gap: 18px;
+    }}
+
+    .summary,
+    .reviews,
+    .rating {{
+      border: 1px solid var(--line);
+      border-radius: 8px;
+      background: rgba(255, 252, 244, .72);
+      box-shadow: 0 20px 42px rgba(70, 48, 34, .10);
+      backdrop-filter: blur(8px);
+    }}
+
+    .summary {{
+      padding: 28px;
+    }}
+
+    .section-kicker {{
+      margin: 0 0 10px;
+      color: var(--teal);
+      font-size: 12px;
+      font-weight: 900;
+      text-transform: uppercase;
+    }}
+
+    .summary p {{
+      max-width: 68ch;
+      margin: 0;
+      color: #3f342b;
+      font-size: 18px;
+      line-height: 1.75;
+    }}
+
+    .reviews {{
+      padding: 24px;
+    }}
+
+    .reviews-head {{
+      display: flex;
+      gap: 16px;
+      align-items: center;
+      justify-content: space-between;
+      margin-bottom: 18px;
+      border-bottom: 1px solid var(--line);
+      padding-bottom: 16px;
+    }}
+
+    .reviews-head h2 {{
+      margin: 0;
+      font-family: Georgia, "Times New Roman", serif;
+      font-size: 32px;
+      line-height: 1;
+    }}
+
+    .version {{
+      flex: 0 0 auto;
+      padding: 8px 12px;
+      border-radius: 4px;
+      background: var(--wine);
+      color: #fff8ea;
+      font-size: 13px;
+      font-weight: 900;
+    }}
+
+    .review-list {{
+      display: grid;
+      gap: 12px;
+    }}
+
+    .review-card {{
+      display: grid;
+      grid-template-columns: 8px minmax(0, 1fr);
+      gap: 14px;
+      align-items: start;
+      min-height: 74px;
+      margin: 0;
+      padding: 16px;
+      border: 1px solid rgba(23, 19, 19, .10);
+      border-radius: 6px;
+      background: #fffaf0;
+    }}
+
+    .review-card span {{
+      width: 8px;
+      height: 100%;
+      min-height: 42px;
+      border-radius: 999px;
+      background: linear-gradient(var(--gold), var(--teal));
+    }}
+
+    .review-card p {{
+      margin: 0;
+      color: #302820;
+      font-size: 17px;
+      line-height: 1.65;
+    }}
+
+    .rating {{
+      display: grid;
+      grid-template-columns: 1fr auto;
+      gap: 20px;
+      align-items: center;
+      padding: 22px 24px;
+    }}
+
+    .rating h2 {{
+      margin: 0;
+      font-family: Georgia, "Times New Roman", serif;
+      font-size: 28px;
+    }}
+
+    .stars {{
+      margin: 0;
+      font-size: 34px;
+      letter-spacing: 0;
+      text-shadow: 0 2px 0 rgba(0, 0, 0, .08);
+      white-space: nowrap;
+    }}
+
+    .rating-source,
+    .rating-empty {{
+      margin: 6px 0 0;
+      color: var(--muted);
+      font-size: 13px;
+    }}
+
+    @media (max-width: 820px) {{
+      main {{ padding: 20px 0; }}
+      .shell {{ grid-template-columns: 1fr; }}
+      .poster,
+      .poster-inner {{ min-height: 420px; }}
+      h1 {{ font-size: 42px; }}
+      .rating {{ grid-template-columns: 1fr; }}
+      .reviews-head {{ align-items: flex-start; flex-direction: column; }}
+    }}
+  </style>
+</head>
+<body>
+  <main>
+    <div class="shell">
+      <section class="poster">
+        <div class="poster-inner">
+          <span class="label">Movie Review</span>
+          <div>
+            <h1>{title}</h1>
+            <p class="meta">{year} · {director}<br>{genres}</p>
+          </div>
+        </div>
+      </section>
+      <section class="content">
+        <article class="summary">
+          <p class="section-kicker">Synopsis</p>
+          <p>{summary}</p>
+        </article>
+        <article class="reviews">
+          <div class="reviews-head">
+            <h2>Audience Notes</h2>
+            <span class="version">reviews {version}</span>
+          </div>
+          <div class="review-list">{review_items}</div>
+        </article>
+        <article class="rating">
+          <div>
+            <p class="section-kicker">Score</p>
+            <h2>Viewer Rating</h2>
+          </div>
+          <div>{rating_html}</div>
+        </article>
+      </section>
+    </div>
+  </main>
+</body>
+</html>"""
+
+
+if __name__ == "__main__":
+    app.run(host="0.0.0.0", port=9080)
diff --git a/samples/moviereview/src/moviepage/requirements.txt 
b/samples/moviereview/src/moviepage/requirements.txt
new file mode 100644
index 00000000..a54cc0b5
--- /dev/null
+++ b/samples/moviereview/src/moviepage/requirements.txt
@@ -0,0 +1,2 @@
+flask==3.1.0
+requests==2.32.3
diff --git a/samples/moviereview/src/ratings/Dockerfile 
b/samples/moviereview/src/ratings/Dockerfile
new file mode 100644
index 00000000..d02adb35
--- /dev/null
+++ b/samples/moviereview/src/ratings/Dockerfile
@@ -0,0 +1,11 @@
+FROM golang:1.25-alpine AS build
+
+WORKDIR /src
+COPY main.go .
+RUN CGO_ENABLED=0 go build -o /out/ratings main.go
+
+FROM alpine:3.22
+COPY --from=build /out/ratings /usr/local/bin/ratings
+
+EXPOSE 9080
+CMD ["ratings"]
diff --git a/dubbod/agency/options/agent_proxy.go 
b/samples/moviereview/src/ratings/main.go
similarity index 51%
rename from dubbod/agency/options/agent_proxy.go
rename to samples/moviereview/src/ratings/main.go
index 59eecd0f..c1028ffe 100644
--- a/dubbod/agency/options/agent_proxy.go
+++ b/samples/moviereview/src/ratings/main.go
@@ -1,4 +1,3 @@
-//
 // 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.
@@ -14,32 +13,38 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package options
-
-import "github.com/apache/dubbo-kubernetes/pkg/dubboagency"
-
-// ProxyArgs provides all of the configuration parameters for the Saku proxy.
-type ProxyArgs struct {
-       dubboagency.Proxy
-       Concurrency int
-       StsPort     int
-
-       TokenManagerPlugin string
+package main
 
-       MeshConfigFile string
-       TemplateFile   string
+import (
+       "encoding/json"
+       "log"
+       "net/http"
+)
 
-       PodName      string
-       PodNamespace string
+type rating struct {
+       MovieID string `json:"movieId"`
+       Stars   int    `json:"stars"`
 }
 
-func NewProxyArgs() ProxyArgs {
-       p := ProxyArgs{}
-       p.applyDefaults()
-       return p
+func main() {
+       http.HandleFunc("/healthz", func(w http.ResponseWriter, _ 
*http.Request) {
+               _, _ = w.Write([]byte("ok\n"))
+       })
+       http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+               if r.URL.Path != "/" && r.URL.Path != "/ratings" {
+                       http.NotFound(w, r)
+                       return
+               }
+               movieID := r.URL.Query().Get("movieId")
+               if movieID == "" {
+                       movieID = "movie-1"
+               }
+               writeJSON(w, rating{MovieID: movieID, Stars: 5})
+       })
+       log.Fatal(http.ListenAndServe(":9080", nil))
 }
 
-func (node *ProxyArgs) applyDefaults() {
-       node.PodName = PodNameVar.Get()
-       node.PodNamespace = PodNamespaceVar.Get()
+func writeJSON(w http.ResponseWriter, value any) {
+       w.Header().Set("Content-Type", "application/json")
+       _ = json.NewEncoder(w).Encode(value)
 }
diff --git a/samples/moviereview/src/reviews-v1/Dockerfile 
b/samples/moviereview/src/reviews-v1/Dockerfile
new file mode 100644
index 00000000..af3542d4
--- /dev/null
+++ b/samples/moviereview/src/reviews-v1/Dockerfile
@@ -0,0 +1,12 @@
+FROM eclipse-temurin:21-jdk-alpine AS build
+
+WORKDIR /src
+COPY ReviewsApplication.java .
+RUN javac ReviewsApplication.java
+
+FROM eclipse-temurin:21-jre-alpine
+WORKDIR /app
+COPY --from=build /src/*.class .
+
+EXPOSE 9080
+CMD ["java", "-cp", "/app", "ReviewsApplication"]
diff --git a/samples/moviereview/src/reviews-v1/ReviewsApplication.java 
b/samples/moviereview/src/reviews-v1/ReviewsApplication.java
new file mode 100644
index 00000000..ec94825b
--- /dev/null
+++ b/samples/moviereview/src/reviews-v1/ReviewsApplication.java
@@ -0,0 +1,61 @@
+/*
+ * 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.
+ */
+
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpServer;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+
+public class ReviewsApplication {
+    public static void main(String[] args) throws IOException {
+        HttpServer server = HttpServer.create(new InetSocketAddress(9080), 0);
+        server.createContext("/healthz", exchange -> write(exchange, 
"text/plain", "ok\n"));
+        server.createContext("/", ReviewsApplication::handle);
+        server.start();
+    }
+
+    private static void handle(HttpExchange exchange) throws IOException {
+        String path = exchange.getRequestURI().getPath();
+        if ("/".equals(path)) {
+            write(exchange, "text/plain", "reviews v1\n");
+            return;
+        }
+        if (!"/reviews".equals(path)) {
+            exchange.sendResponseHeaders(404, -1);
+            exchange.close();
+            return;
+        }
+        write(exchange, "application/json", """
+            {
+              "version": "v1",
+              "items": [
+                "故事完整,节奏朴素。",
+                "这个版本只返回评论文本。"
+              ]
+            }
+            """);
+    }
+
+    private static void write(HttpExchange exchange, String contentType, 
String body) throws IOException {
+        byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
+        exchange.getResponseHeaders().set("Content-Type", contentType + "; 
charset=utf-8");
+        exchange.sendResponseHeaders(200, bytes.length);
+        exchange.getResponseBody().write(bytes);
+        exchange.close();
+    }
+}
diff --git a/samples/moviereview/src/reviews-v2/Dockerfile 
b/samples/moviereview/src/reviews-v2/Dockerfile
new file mode 100644
index 00000000..e5769d15
--- /dev/null
+++ b/samples/moviereview/src/reviews-v2/Dockerfile
@@ -0,0 +1,9 @@
+FROM python:3.12-alpine
+
+WORKDIR /app
+COPY requirements.txt .
+RUN pip install --no-cache-dir -r requirements.txt
+COPY app.py .
+
+EXPOSE 9080
+CMD ["python", "app.py"]
diff --git a/samples/moviereview/src/reviews-v2/app.py 
b/samples/moviereview/src/reviews-v2/app.py
new file mode 100644
index 00000000..7122ca77
--- /dev/null
+++ b/samples/moviereview/src/reviews-v2/app.py
@@ -0,0 +1,58 @@
+# 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.
+
+import os
+
+import requests
+from flask import Flask, jsonify
+
+
+app = Flask(__name__)
+RATINGS_URL = os.getenv("RATINGS_URL", "http://ratings:9080/ratings";)
+
+
+def rating():
+    response = requests.get(RATINGS_URL, params={"movieId": "movie-1"}, 
timeout=2)
+    response.raise_for_status()
+    return response.json()
+
+
[email protected]("/healthz")
+def healthz():
+    return "ok\n"
+
+
[email protected]("/")
+def root():
+    return "reviews v2\n"
+
+
[email protected]("/reviews")
+def reviews():
+    return jsonify(
+        {
+            "version": "v2",
+            "items": [
+                "人物关系更清晰。",
+                "这个版本会调用 ratings 服务。"
+            ],
+            "rating": rating(),
+            "starColor": "#111827",
+        }
+    )
+
+
+if __name__ == "__main__":
+    app.run(host="0.0.0.0", port=9080)
diff --git a/samples/moviereview/src/reviews-v2/requirements.txt 
b/samples/moviereview/src/reviews-v2/requirements.txt
new file mode 100644
index 00000000..a54cc0b5
--- /dev/null
+++ b/samples/moviereview/src/reviews-v2/requirements.txt
@@ -0,0 +1,2 @@
+flask==3.1.0
+requests==2.32.3
diff --git a/samples/moviereview/src/reviews-v3/Dockerfile 
b/samples/moviereview/src/reviews-v3/Dockerfile
new file mode 100644
index 00000000..f1767c8b
--- /dev/null
+++ b/samples/moviereview/src/reviews-v3/Dockerfile
@@ -0,0 +1,7 @@
+FROM node:22-alpine
+
+WORKDIR /app
+COPY server.js .
+
+EXPOSE 9080
+CMD ["node", "server.js"]
diff --git a/samples/moviereview/src/reviews-v3/server.js 
b/samples/moviereview/src/reviews-v3/server.js
new file mode 100644
index 00000000..84e5d121
--- /dev/null
+++ b/samples/moviereview/src/reviews-v3/server.js
@@ -0,0 +1,90 @@
+/*
+ * 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.
+ */
+
+const http = require("http");
+
+const ratingsUrl = process.env.RATINGS_URL || "http://ratings:9080/ratings";;
+
+function write(res, status, contentType, body) {
+  res.writeHead(status, { "content-type": `${contentType}; charset=utf-8` });
+  res.end(body);
+}
+
+function getRating() {
+  const url = new URL(ratingsUrl);
+  url.searchParams.set("movieId", "movie-1");
+  return new Promise((resolve, reject) => {
+    const req = http.get(url, (res) => {
+      let body = "";
+      res.setEncoding("utf8");
+      res.on("data", (chunk) => {
+        body += chunk;
+      });
+      res.on("end", () => {
+        try {
+          resolve(JSON.parse(body));
+        } catch (err) {
+          reject(err);
+        }
+      });
+    });
+    req.setTimeout(2000, () => {
+      req.destroy(new Error("ratings request timed out"));
+    });
+    req.on("error", reject);
+  });
+}
+
+const server = http.createServer(async (req, res) => {
+  const path = new URL(req.url, "http://localhost";).pathname;
+  if (path === "/healthz") {
+    write(res, 200, "text/plain", "ok\n");
+    return;
+  }
+  if (path === "/") {
+    write(res, 200, "text/plain", "reviews v3\n");
+    return;
+  }
+  if (path !== "/reviews") {
+    write(res, 404, "text/plain", "not found\n");
+    return;
+  }
+
+  try {
+    const rating = await getRating();
+    write(
+      res,
+      200,
+      "application/json",
+      JSON.stringify({
+        version: "v3",
+        items: ["评论样式更醒目。", "这个版本也调用 ratings 服务。"],
+        rating,
+        starColor: "#dc2626",
+      }),
+    );
+  } catch (err) {
+    write(
+      res,
+      502,
+      "application/json",
+      JSON.stringify({ version: "v3", items: [], error: err.message }),
+    );
+  }
+});
+
+server.listen(9080, "0.0.0.0");
diff --git a/samples/multicluster/README.md b/samples/multicluster/README.md
deleted file mode 100644
index d59a8669..00000000
--- a/samples/multicluster/README.md
+++ /dev/null
@@ -1,99 +0,0 @@
-# 多集群主从跨网
-
-## 结论
-
-`chore_v47` 进入主从跨网方案:主集群运行 `dubbod`,远端集群通过 Secret 接入主控制面;跨集群流量不再直连远端 Pod 
IP,而是通过各集群的 east-west `dxgate` 转发。
-
-## 前提
-
-- 主集群 API Server 能访问远端 API Server。
-- 两个集群共享同一信任根。
-- 两个集群已安装 Gateway API CRD。
-- 远端 webhook 和远端 `dxgate` 都能访问主集群控制面地址。
-- 跨网模式允许 Pod CIDR 重叠,但每个集群都必须有可被其他集群访问的 east-west gateway 地址。
-
-## 安装主控制面
-
-主集群安装时同时打开远端访问和 east-west 配置:
-
-```bash
-dubboctl manifest generate \
-  --set values.global.multicluster.remoteAccess.enabled=true \
-  --set values.global.multicluster.remoteAccess.serviceType=NodePort \
-  --set values.global.multicluster.remoteAccess.grpcPort=26010 \
-  --set values.global.multicluster.remoteAccess.certificateHosts[0]=<主集群对外地址> \
-  --set values.global.multicluster.eastWestGateway.enabled=true \
-  --set values.global.multicluster.eastWestGateway.serviceType=NodePort \
-  --set values.global.multicluster.eastWestGateway.nodePort=32443 \
-  --set 
values.global.multicluster.eastWestGateway.gateways[0].clusterName=remote \
-  --set values.global.multicluster.eastWestGateway.gateways[0].address=<远端 
east-west 地址> \
-  --set values.global.multicluster.eastWestGateway.gateways[0].port=15443 \
-  --set 
values.global.multicluster.eastWestGateway.gateways[1].clusterName=Kubernetes \
-  --set values.global.multicluster.eastWestGateway.gateways[1].address=<主集群 
east-west 地址> \
-  --set values.global.multicluster.eastWestGateway.gateways[1].port=15443 | 
kubectl --context cluster1 apply -f -
-```
-
-`DUBBO_EASTWEST_GATEWAYS` 会写入 `dubbod` Deployment。EDS 下发时,本集群 endpoint 保持 Pod 
IP,远端 cluster shard 会改写成对应 gateway 地址。
-
-## 接入远端集群
-
-主集群创建远端 Secret:
-
-```bash
-dubboctl multicluster create-remote-secret \
-  --cluster-name remote \
-  --kubeconfig ~/.kube/config2 \
-  --context kubernetes-admin@kubernetes | kubectl --context cluster1 apply -f -
-```
-
-远端集群安装注入 webhook:
-
-```bash
-dubboctl multicluster generate-remote-manifest \
-  --cluster-name remote \
-  --webhook-url https://<主集群远端 webhook 地址>:<端口> \
-  --xds-address <主集群远端 xDS/CA 地址>:<端口> \
-  --ca-address <主集群远端 xDS/CA 地址>:<端口> \
-  --ca-bundle-file ./ca-cert.pem | kubectl --context cluster2 apply -f -
-```
-
-远端集群创建 east-west Gateway;主控制面会通过远端 Secret 监听该 Gateway,并在远端生成 `dxgate`:
-
-```bash
-dubboctl multicluster generate-eastwest-gateway \
-  --xds-address http://<主集群可访问 ADS 地址>:<grpc-xds NodePort 或 LB 端口> \
-  --service-type NodePort \
-  --node-port 32443 | kubectl --context cluster2 apply -f -
-```
-
-## 部署样例
-
-```bash
-for ctx in cluster1 cluster2; do
-  kubectl --context "${ctx}" create ns app --dry-run=client -o yaml | kubectl 
--context "${ctx}" apply -f -
-  kubectl --context "${ctx}" label namespace app dubbo-injection=enabled 
--overwrite
-  kubectl --context "${ctx}" apply -f samples/app/deployment.yaml
-done
-
-kubectl --context cluster1 -n app scale deploy/nginx-v2 --replicas=0
-kubectl --context cluster2 -n app scale deploy/nginx-v1 --replicas=0
-kubectl --context cluster1 apply -f samples/app/meshservice.yaml
-kubectl --context cluster1 apply -f 
samples/multicluster/eastwest-nginx-httproute.yaml
-```
-
-`HTTPRoute` 只需要放在主控制面的配置源里;远端 `dxgate` 连接主控制面 ADS 后会拿到同一份路由配置,并把流量转给远端本地 
`nginx` Service。
-
-## 验证
-
-```bash
-kubectl --context cluster2 -n app get pod -l app=nginx-consumer \
-  -o 
jsonpath='{.items[0].spec.containers[0].env[?(@.name=="DUBBO_META_CLUSTER_ID")].value}{"\n"}'
-
-kubectl --context cluster1 -n app exec deploy/nginx-consumer -- \
-  dubbod xclient --print-route --expect v1=50,v2=50
-
-kubectl --context cluster2 -n app exec deploy/nginx-consumer -- \
-  dubbod xclient --print-route --expect v1=50,v2=50
-```
-
-跨网模式下,`--print-route` 里远端 endpoint 应显示 east-west gateway 地址,而不是远端 Pod IP。

Reply via email to