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。