You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@dubbo.apache.org by zh...@apache.org on 2026/05/14 04:45:28 UTC
(dubbo-kubernetes) branch master updated: Version 0.4.0 related functional logic (#907)
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 1127279a Version 0.4.0 related functional logic (#907)
1127279a is described below
commit 1127279ad4e036f36b2db7b14dda92d8c4bd1255
Author: mfordjody <11...@qq.com>
AuthorDate: Thu May 14 12:45:24 2026 +0800
Version 0.4.0 related functional logic (#907)
* Update kdubbo API module versions for MeshService
* Remove MeshService visibleTo usage
* Test proxyless mTLS xDS generation
* Wire proxyless runtime security config
* Update api and xds-api dependencies
* Add mtls gateway logic and necessary configuration
---
.asf.yaml | 2 +-
dubbod/discovery/cmd/app/cmd.go | 2 +
dubbod/discovery/cmd/app/cmd_test.go | 5 +-
dubbod/discovery/cmd/app/xds_client.go | 161 ++++++++++++++---
dubbod/discovery/cmd/app/xds_client_test.go | 43 +++++
dubbod/discovery/cmd/app/xds_server.go | 193 ++++++++++++++++++++
dubbod/discovery/cmd/app/xds_server_test.go | 172 ++++++++++++++++++
.../pkg/bootstrap/proxyless_grpc_controller.go | 130 +++++++++++++-
.../bootstrap/proxyless_grpc_controller_test.go | 199 +++++++++++++++++++++
.../kube/gateway/deployment_controller_test.go | 9 +
dubbod/discovery/pkg/model/virtualservice.go | 4 +-
.../discovery/pkg/networking/grpcgen/mtls_test.go | 192 ++++++++++++++++++++
dubbod/discovery/pkg/xds/eds.go | 1 -
.../pkg/xds/endpoints/endpoint_builder.go | 51 +++++-
.../pkg/xds/endpoints/endpoint_builder_test.go | 167 +++++++++++++++++
dubboinstaller/charts/base/Chart.yaml | 2 +-
dubboinstaller/charts/base/files/crd-all.gen.yaml | 5 -
dubboinstaller/charts/dubbod/Chart.yaml | 2 +-
.../charts/dubbod/files/grpc-engine.yaml | 21 +++
.../charts/dubbod/files/kube-gateway.yaml | 3 +
.../charts/dubbod/templates/deployment.yaml | 2 +-
.../templates/mutatingwebhookconfiguration.yaml | 38 +++-
go.mod | 10 +-
go.sum | 10 +-
pkg/kube/inject/inject.go | 2 +-
pkg/kube/inject/proxyless.go | 2 +
pkg/kube/inject/proxyless_test.go | 32 +++-
pkg/kube/inject/service_test.go | 188 +++++++++++++++++++
pkg/kube/inject/webhook.go | 97 ++++++++++
samples/addons/00-namespace.yaml | 19 --
samples/addons/README.md | 38 ----
samples/app/README.md | 20 ++-
samples/app/deployment.yaml | 16 ++
samples/app/meshservice.yaml | 41 -----
34 files changed, 1700 insertions(+), 179 deletions(-)
diff --git a/.asf.yaml b/.asf.yaml
index e7744c3e..a57176f4 100644
--- a/.asf.yaml
+++ b/.asf.yaml
@@ -21,7 +21,7 @@ notifications:
github:
homepage: "https://kdubbo.github.io"
- description: "Embed gRPC, use xDS, build inherent mesh."
+ description: "Build a native mesh using gRPC and xDS."
features:
# Enable wiki for documentation
wiki: false
diff --git a/dubbod/discovery/cmd/app/cmd.go b/dubbod/discovery/cmd/app/cmd.go
index ee16ef77..4ab203fd 100644
--- a/dubbod/discovery/cmd/app/cmd.go
+++ b/dubbod/discovery/cmd/app/cmd.go
@@ -37,6 +37,7 @@ const (
executeLogScope = "setup"
waitLogScope = "wait"
xclientLogScope = "xclient"
+ xserverLogScope = "xserver"
)
func NewRootCommand() *cobra.Command {
@@ -61,6 +62,7 @@ func NewRootCommand() *cobra.Command {
cmd.AddFlags(rootCmd)
rootCmd.AddCommand(waitCmd)
rootCmd.AddCommand(newXClientCommand())
+ rootCmd.AddCommand(newXServerCommand())
return rootCmd
}
diff --git a/dubbod/discovery/cmd/app/cmd_test.go b/dubbod/discovery/cmd/app/cmd_test.go
index 70d81fe7..dda5b982 100644
--- a/dubbod/discovery/cmd/app/cmd_test.go
+++ b/dubbod/discovery/cmd/app/cmd_test.go
@@ -32,7 +32,7 @@ func TestRootCommandRegistersRenamedCommands(t *testing.T) {
commands[command.Name()] = true
}
- for _, name := range []string{"execute", "xclient"} {
+ for _, name := range []string{"execute", "xclient", "xserver"} {
if !commands[name] {
t.Fatalf("expected command %q to be registered; commands=%v", name, commands)
}
@@ -47,7 +47,7 @@ func TestRootCommandRegistersRenamedCommands(t *testing.T) {
func TestCommandsSetLogScopes(t *testing.T) {
defer func() {
log.SetDefaultScope("log")
- for _, name := range []string{"log", executeLogScope, waitLogScope, xclientLogScope} {
+ for _, name := range []string{"log", executeLogScope, waitLogScope, xclientLogScope, xserverLogScope} {
if scope := log.FindScope(name); scope != nil {
scope.SetOutput(os.Stderr)
}
@@ -62,6 +62,7 @@ func TestCommandsSetLogScopes(t *testing.T) {
{command: "execute", scope: "setup"},
{command: "wait", scope: "wait"},
{command: "xclient", scope: "xclient"},
+ {command: "xserver", scope: "xserver"},
} {
t.Run(tt.command, func(t *testing.T) {
var out bytes.Buffer
diff --git a/dubbod/discovery/cmd/app/xds_client.go b/dubbod/discovery/cmd/app/xds_client.go
index f0be4e13..7153839d 100644
--- a/dubbod/discovery/cmd/app/xds_client.go
+++ b/dubbod/discovery/cmd/app/xds_client.go
@@ -39,6 +39,7 @@ import (
corev1 "github.com/kdubbo/xds-api/core/v1"
endpointv1 "github.com/kdubbo/xds-api/endpoint/v1"
hcmv1 "github.com/kdubbo/xds-api/extensions/filters/v1/network/http_connection_manager"
+ tlsv1 "github.com/kdubbo/xds-api/extensions/transport_sockets/tls/v1"
xdsresolver "github.com/kdubbo/xds-api/grpc/resolver"
listenerv1 "github.com/kdubbo/xds-api/listener/v1"
routev1 "github.com/kdubbo/xds-api/route/v1"
@@ -85,7 +86,11 @@ type xdsDestination struct {
Host string `json:"host"`
Subset string `json:"subset,omitempty"`
Weight uint32 `json:"weight"`
+ TLSMode string `json:"tlsMode,omitempty"`
+ SNI string `json:"sni,omitempty"`
Endpoints []xdsEndpoint `json:"endpoints,omitempty"`
+
+ tlsContext *tlsv1.UpstreamTlsContext
}
type xdsRouteSnapshot struct {
@@ -102,12 +107,15 @@ type sampleADSClient struct {
host string
port int
- mu sync.RWMutex
- subs map[string][]string
- route map[string]uint32
- endpoints map[string][]xdsEndpoint
- updates chan struct{}
- errs chan error
+ mu sync.RWMutex
+ subs map[string][]string
+ route map[string]uint32
+ endpoints map[string][]xdsEndpoint
+ clusterTLS map[string]*tlsv1.UpstreamTlsContext
+ updates chan struct{}
+ errs chan error
+ bootstrapPath string
+ bootstrap *xdsresolver.BootstrapConfig
}
func newXClientCommand() *cobra.Command {
@@ -232,17 +240,19 @@ func newSampleADSClient(ctx context.Context, opts *xdsClientOptions) (*sampleADS
return nil, fmt.Errorf("open ADS stream: %w", err)
}
return &sampleADSClient{
- conn: conn,
- stream: stream,
- node: node,
- target: opts.target,
- host: opts.host,
- port: opts.port,
- subs: map[string][]string{},
- route: map[string]uint32{},
- endpoints: map[string][]xdsEndpoint{},
- updates: make(chan struct{}, 1),
- errs: make(chan error, 1),
+ conn: conn,
+ stream: stream,
+ node: node,
+ target: opts.target,
+ host: opts.host,
+ port: opts.port,
+ subs: map[string][]string{},
+ route: map[string]uint32{},
+ endpoints: map[string][]xdsEndpoint{},
+ clusterTLS: map[string]*tlsv1.UpstreamTlsContext{},
+ updates: make(chan struct{}, 1),
+ errs: make(chan error, 1),
+ bootstrapPath: opts.bootstrapPath,
}, nil
}
@@ -410,10 +420,16 @@ func (c *sampleADSClient) handleResponse(resp *discovery.DiscoveryResponse) erro
return c.subscribe(v1.ClusterType, clusters)
}
case v1.ClusterType:
- edsNames, err := edsNamesFromClusters(resp.Resources)
+ edsNames, clusterTLS, err := edsNamesAndTLSFromClusters(resp.Resources)
if err != nil {
return err
}
+ c.mu.Lock()
+ for clusterName, tlsContext := range clusterTLS {
+ c.clusterTLS[clusterName] = tlsContext
+ }
+ c.mu.Unlock()
+ c.notify()
if len(edsNames) > 0 {
return c.subscribe(v1.EndpointType, edsNames)
}
@@ -478,7 +494,7 @@ func (c *sampleADSClient) readySnapshot(expected map[string]uint32) (xdsRouteSna
continue
}
eps := append([]xdsEndpoint(nil), c.endpoints[clusterName]...)
- dest := destinationFromCluster(clusterName, weight, eps)
+ dest := destinationFromCluster(clusterName, weight, eps, c.clusterTLS[clusterName])
snapshot.Destinations = append(snapshot.Destinations, dest)
}
sort.Slice(snapshot.Destinations, func(i, j int) bool {
@@ -569,13 +585,15 @@ func routeWeightsFromRoutes(resources []*anypb.Any) (map[string]uint32, []string
return weights, sortedUnique(clusters), nil
}
-func edsNamesFromClusters(resources []*anypb.Any) ([]string, error) {
+func edsNamesAndTLSFromClusters(resources []*anypb.Any) ([]string, map[string]*tlsv1.UpstreamTlsContext, error) {
out := make([]string, 0, len(resources))
+ clusterTLS := map[string]*tlsv1.UpstreamTlsContext{}
for _, resource := range resources {
c := &clusterv1.Cluster{}
if err := proto.Unmarshal(resource.Value, c); err != nil {
- return nil, err
+ return nil, nil, err
}
+ clusterTLS[c.Name] = upstreamTLSContextFromCluster(c)
serviceName := c.GetName()
if eds := c.GetEdsClusterConfig(); eds != nil && eds.GetServiceName() != "" {
serviceName = eds.GetServiceName()
@@ -584,7 +602,22 @@ func edsNamesFromClusters(resources []*anypb.Any) ([]string, error) {
out = append(out, serviceName)
}
}
- return sortedUnique(out), nil
+ return sortedUnique(out), clusterTLS, nil
+}
+
+func upstreamTLSContextFromCluster(c *clusterv1.Cluster) *tlsv1.UpstreamTlsContext {
+ if c == nil || c.TransportSocket == nil {
+ return nil
+ }
+ typedConfig := c.TransportSocket.GetTypedConfig()
+ if typedConfig == nil {
+ return nil
+ }
+ upstreamTLS := &tlsv1.UpstreamTlsContext{}
+ if err := anypb.UnmarshalTo(typedConfig, upstreamTLS, proto.UnmarshalOptions{}); err != nil {
+ return nil
+ }
+ return upstreamTLS
}
func endpointsFromAssignments(resources []*anypb.Any) (map[string][]xdsEndpoint, error) {
@@ -622,13 +655,17 @@ func endpointsFromAssignments(resources []*anypb.Any) (map[string][]xdsEndpoint,
return out, nil
}
-func destinationFromCluster(clusterName string, weight uint32, endpoints []xdsEndpoint) xdsDestination {
+func destinationFromCluster(clusterName string, weight uint32, endpoints []xdsEndpoint, tlsContext *tlsv1.UpstreamTlsContext) xdsDestination {
parts := strings.Split(clusterName, "|")
- dest := xdsDestination{Cluster: clusterName, Weight: weight, Endpoints: endpoints}
+ dest := xdsDestination{Cluster: clusterName, Weight: weight, Endpoints: endpoints, tlsContext: tlsContext}
if len(parts) == 4 {
dest.Host = parts[3]
dest.Subset = parts[2]
}
+ if tlsContext != nil {
+ dest.TLSMode = "DUBBO_MUTUAL"
+ dest.SNI = tlsContext.Sni
+ }
return dest
}
@@ -638,7 +675,7 @@ func runSampleRequests(ctx context.Context, adsClient *sampleADSClient, snapshot
return err
}
currentSignature := snapshotSignature(snapshot)
- client := &http.Client{Timeout: requestTimeout}
+ clients := newSampleRequestClients(adsClient, requestTimeout)
for i := 0; i < count; i++ {
if updated, ok := adsClient.readySnapshot(nil); ok {
updatedSignature := snapshotSignature(updated)
@@ -651,17 +688,21 @@ func runSampleRequests(ctx context.Context, adsClient *sampleADSClient, snapshot
}
}
}
- _, endpoint, err := picker.Next()
+ destination, endpoint, err := picker.Next()
+ if err != nil {
+ return err
+ }
+ httpClient, scheme, err := clients.clientForDestination(destination)
if err != nil {
return err
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet,
- fmt.Sprintf("http://%s/", net.JoinHostPort(endpoint.Address, strconv.Itoa(int(endpoint.Port)))), nil)
+ fmt.Sprintf("%s://%s/", scheme, net.JoinHostPort(endpoint.Address, strconv.Itoa(int(endpoint.Port)))), nil)
if err != nil {
return err
}
req.Host = snapshot.Host
- resp, err := client.Do(req)
+ resp, err := httpClient.Do(req)
if err != nil {
return err
}
@@ -686,6 +727,70 @@ func runSampleRequests(ctx context.Context, adsClient *sampleADSClient, snapshot
return nil
}
+type sampleRequestClients struct {
+ adsClient *sampleADSClient
+ requestTimeout time.Duration
+ plaintext *http.Client
+ tlsClients map[string]*http.Client
+}
+
+func newSampleRequestClients(adsClient *sampleADSClient, requestTimeout time.Duration) *sampleRequestClients {
+ return &sampleRequestClients{
+ adsClient: adsClient,
+ requestTimeout: requestTimeout,
+ plaintext: &http.Client{Timeout: requestTimeout},
+ tlsClients: map[string]*http.Client{},
+ }
+}
+
+func (c *sampleRequestClients) clientForDestination(destination xdsDestination) (*http.Client, string, error) {
+ if destination.tlsContext == nil {
+ return c.plaintext, "http", nil
+ }
+ bootstrap, err := c.adsClient.bootstrapConfig()
+ if err != nil {
+ return nil, "", err
+ }
+ tlsConfig, err := xdsresolver.DataPlaneTLSConfigFromBootstrap(bootstrap, destination.tlsContext, destination.Host)
+ if err != nil {
+ return nil, "", err
+ }
+ key := destination.TLSMode + "|" + destination.SNI
+ if key == "|" {
+ key = destination.Cluster
+ }
+ if cached := c.tlsClients[key]; cached != nil {
+ return cached, "https", nil
+ }
+ client := &http.Client{
+ Timeout: c.requestTimeout,
+ Transport: &http.Transport{
+ TLSClientConfig: tlsConfig,
+ },
+ }
+ c.tlsClients[key] = client
+ return client, "https", nil
+}
+
+func (c *sampleADSClient) bootstrapConfig() (*xdsresolver.BootstrapConfig, error) {
+ if c.bootstrap != nil {
+ return c.bootstrap, nil
+ }
+ path := c.bootstrapPath
+ if path == "" {
+ path = os.Getenv("GRPC_XDS_BOOTSTRAP")
+ }
+ if path == "" {
+ return nil, fmt.Errorf("data-plane mTLS requires GRPC_XDS_BOOTSTRAP or --bootstrap")
+ }
+ bootstrap, err := xdsresolver.ParseBootstrap(path)
+ if err != nil {
+ return nil, err
+ }
+ c.bootstrap = bootstrap
+ return c.bootstrap, nil
+}
+
func activeDestinations(snapshot xdsRouteSnapshot) []xdsDestination {
out := make([]xdsDestination, 0, len(snapshot.Destinations))
for _, dest := range snapshot.Destinations {
diff --git a/dubbod/discovery/cmd/app/xds_client_test.go b/dubbod/discovery/cmd/app/xds_client_test.go
index 3beaa8d6..6a74ebfc 100644
--- a/dubbod/discovery/cmd/app/xds_client_test.go
+++ b/dubbod/discovery/cmd/app/xds_client_test.go
@@ -13,6 +13,8 @@ import (
"strings"
"testing"
"time"
+
+ tlsv1 "github.com/kdubbo/xds-api/extensions/transport_sockets/tls/v1"
)
func TestAutoDiscoverServiceTargetSingleService(t *testing.T) {
@@ -203,6 +205,47 @@ func TestRunSampleRequestsUsesUpdatedSnapshotOnSameStream(t *testing.T) {
}
}
+func TestSampleRequestClientsDoNotBypassMTLS(t *testing.T) {
+ clients := newSampleRequestClients(&sampleADSClient{}, time.Second)
+ destination := xdsDestination{
+ Cluster: "outbound|80|v1|nginx.app.svc.cluster.local",
+ Host: "nginx.app.svc.cluster.local",
+ TLSMode: "DUBBO_MUTUAL",
+ SNI: "nginx.app.svc.cluster.local",
+ tlsContext: &tlsv1.UpstreamTlsContext{
+ Sni: "nginx.app.svc.cluster.local",
+ },
+ }
+
+ _, _, err := clients.clientForDestination(destination)
+ if err == nil {
+ t.Fatalf("clientForDestination() error = nil, want bootstrap error for mTLS route")
+ }
+ if !strings.Contains(err.Error(), "data-plane mTLS requires GRPC_XDS_BOOTSTRAP or --bootstrap") {
+ t.Fatalf("error = %q, want bootstrap requirement", err.Error())
+ }
+}
+
+func TestDestinationFromClusterExposesTLSContext(t *testing.T) {
+ tlsContext := &tlsv1.UpstreamTlsContext{Sni: "nginx.app.svc.cluster.local"}
+ destination := destinationFromCluster(
+ "outbound|80|v1|nginx.app.svc.cluster.local",
+ 50,
+ []xdsEndpoint{{Address: "10.0.0.1", Port: 80}},
+ tlsContext,
+ )
+
+ if destination.TLSMode != "DUBBO_MUTUAL" {
+ t.Fatalf("TLSMode = %q, want DUBBO_MUTUAL", destination.TLSMode)
+ }
+ if destination.SNI != "nginx.app.svc.cluster.local" {
+ t.Fatalf("SNI = %q, want nginx host", destination.SNI)
+ }
+ if destination.tlsContext != tlsContext {
+ t.Fatalf("tlsContext was not preserved")
+ }
+}
+
func endpointForServer(t *testing.T, server *httptest.Server) xdsEndpoint {
t.Helper()
parsed, err := neturl.Parse(server.URL)
diff --git a/dubbod/discovery/cmd/app/xds_server.go b/dubbod/discovery/cmd/app/xds_server.go
new file mode 100644
index 00000000..c53ea4f4
--- /dev/null
+++ b/dubbod/discovery/cmd/app/xds_server.go
@@ -0,0 +1,193 @@
+// 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 app
+
+import (
+ "context"
+ "crypto/tls"
+ "crypto/x509"
+ "fmt"
+ "io"
+ "net"
+ "os"
+ "sync"
+ "time"
+
+ "github.com/apache/dubbo-kubernetes/pkg/kube/inject"
+ "github.com/apache/dubbo-kubernetes/pkg/log"
+ xdsresolver "github.com/kdubbo/xds-api/grpc/resolver"
+ "github.com/spf13/cobra"
+)
+
+type xdsServerOptions struct {
+ listen string
+ upstream string
+ bootstrapPath string
+ acceptTimeout time.Duration
+ connectTimeout time.Duration
+}
+
+func newXServerCommand() *cobra.Command {
+ opts := &xdsServerOptions{
+ listen: firstNonEmpty(os.Getenv("DUBBO_XSERVER_LISTEN"), fmt.Sprintf(":%d", inject.ProxylessXServerPort)),
+ upstream: firstNonEmpty(os.Getenv("DUBBO_XSERVER_UPSTREAM"), "127.0.0.1:80"),
+ bootstrapPath: os.Getenv("GRPC_XDS_BOOTSTRAP"),
+ acceptTimeout: durationSecondsFromEnv("DUBBO_XSERVER_ACCEPT_TIMEOUT", 0),
+ connectTimeout: durationSecondsFromEnv("DUBBO_XSERVER_CONNECT_TIMEOUT", 5*time.Second),
+ }
+ c := &cobra.Command{
+ Use: "xserver",
+ Short: "run an inbound mTLS data-plane proxy for proxyless workloads",
+ Args: cobra.NoArgs,
+ PreRunE: func(cmd *cobra.Command, args []string) error {
+ log.SetDefaultScope(xserverLogScope)
+ return nil
+ },
+ RunE: func(cmd *cobra.Command, _ []string) error {
+ return opts.run(cmd.Context())
+ },
+ }
+ c.Flags().StringVar(&opts.listen, "listen", opts.listen, "mTLS listener address")
+ c.Flags().StringVar(&opts.upstream, "upstream", opts.upstream, "local plaintext upstream address")
+ c.Flags().StringVar(&opts.bootstrapPath, "bootstrap", opts.bootstrapPath, "gRPC xDS bootstrap file")
+ c.Flags().DurationVar(&opts.acceptTimeout, "accept-timeout", opts.acceptTimeout, "optional TLS handshake timeout")
+ c.Flags().DurationVar(&opts.connectTimeout, "connect-timeout", opts.connectTimeout, "timeout for connecting to the local upstream")
+ return c
+}
+
+func (o *xdsServerOptions) run(ctx context.Context) error {
+ if o.bootstrapPath == "" {
+ return fmt.Errorf("xserver requires GRPC_XDS_BOOTSTRAP or --bootstrap")
+ }
+ if o.listen == "" {
+ return fmt.Errorf("xserver listen address is required")
+ }
+ if o.upstream == "" {
+ return fmt.Errorf("xserver upstream address is required")
+ }
+ bootstrap, err := xdsresolver.ParseBootstrap(o.bootstrapPath)
+ if err != nil {
+ return err
+ }
+ tlsConfig, err := xserverTLSConfigFromBootstrap(bootstrap)
+ if err != nil {
+ return err
+ }
+ lis, err := net.Listen("tcp", o.listen)
+ if err != nil {
+ return fmt.Errorf("listen xserver %s: %w", o.listen, err)
+ }
+ defer lis.Close()
+ return serveXServer(ctx, lis, tlsConfig, o.upstream, o.acceptTimeout, o.connectTimeout)
+}
+
+func xserverTLSConfigFromBootstrap(bootstrap *xdsresolver.BootstrapConfig) (*tls.Config, error) {
+ if bootstrap == nil {
+ return nil, fmt.Errorf("bootstrap config is nil")
+ }
+ cfg, ok := bootstrap.CertProviders["default"]
+ if !ok {
+ return nil, fmt.Errorf("certificate_providers[default] not found")
+ }
+ if cfg.CertificateFile == "" || cfg.PrivateKeyFile == "" {
+ return nil, fmt.Errorf("xserver mTLS requires certificate_file and private_key_file")
+ }
+ if cfg.CACertificateFile == "" {
+ return nil, fmt.Errorf("xserver mTLS requires ca_certificate_file")
+ }
+ cert, err := tls.LoadX509KeyPair(cfg.CertificateFile, cfg.PrivateKeyFile)
+ if err != nil {
+ return nil, fmt.Errorf("load xserver certificate/key: %w", err)
+ }
+ rootPEM, err := os.ReadFile(cfg.CACertificateFile)
+ if err != nil {
+ return nil, fmt.Errorf("read xserver CA certificate %s: %w", cfg.CACertificateFile, err)
+ }
+ clientCAs := x509.NewCertPool()
+ if !clientCAs.AppendCertsFromPEM(rootPEM) {
+ return nil, fmt.Errorf("parse xserver CA certificate %s: no certificates found", cfg.CACertificateFile)
+ }
+ return &tls.Config{
+ MinVersion: tls.VersionTLS12,
+ Certificates: []tls.Certificate{cert},
+ ClientCAs: clientCAs,
+ ClientAuth: tls.RequireAndVerifyClientCert,
+ }, nil
+}
+
+func serveXServer(ctx context.Context, lis net.Listener, tlsConfig *tls.Config, upstream string, acceptTimeout, connectTimeout time.Duration) error {
+ tlsListener := tls.NewListener(lis, tlsConfig)
+ go func() {
+ <-ctx.Done()
+ _ = tlsListener.Close()
+ }()
+ for {
+ conn, err := tlsListener.Accept()
+ if err != nil {
+ select {
+ case <-ctx.Done():
+ return nil
+ default:
+ return err
+ }
+ }
+ go proxyXServerConnection(conn, upstream, acceptTimeout, connectTimeout)
+ }
+}
+
+func proxyXServerConnection(inbound net.Conn, upstream string, acceptTimeout, connectTimeout time.Duration) {
+ defer inbound.Close()
+ if acceptTimeout > 0 {
+ _ = inbound.SetDeadline(time.Now().Add(acceptTimeout))
+ }
+ if tlsConn, ok := inbound.(*tls.Conn); ok {
+ if err := tlsConn.Handshake(); err != nil {
+ return
+ }
+ }
+ _ = inbound.SetDeadline(time.Time{})
+ dialer := net.Dialer{Timeout: connectTimeout}
+ outbound, err := dialer.Dial("tcp", upstream)
+ if err != nil {
+ return
+ }
+ defer outbound.Close()
+ copyBothDirections(inbound, outbound)
+}
+
+func copyBothDirections(a, b net.Conn) {
+ var wg sync.WaitGroup
+ wg.Add(2)
+ go func() {
+ defer wg.Done()
+ _, _ = io.Copy(a, b)
+ closeWrite(a)
+ }()
+ go func() {
+ defer wg.Done()
+ _, _ = io.Copy(b, a)
+ closeWrite(b)
+ }()
+ wg.Wait()
+}
+
+func closeWrite(conn net.Conn) {
+ if c, ok := conn.(interface{ CloseWrite() error }); ok {
+ _ = c.CloseWrite()
+ return
+ }
+ _ = conn.Close()
+}
diff --git a/dubbod/discovery/cmd/app/xds_server_test.go b/dubbod/discovery/cmd/app/xds_server_test.go
new file mode 100644
index 00000000..3ce19265
--- /dev/null
+++ b/dubbod/discovery/cmd/app/xds_server_test.go
@@ -0,0 +1,172 @@
+package app
+
+import (
+ "context"
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/tls"
+ "crypto/x509"
+ "crypto/x509/pkix"
+ "encoding/pem"
+ "fmt"
+ "io"
+ "math/big"
+ "net"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "path/filepath"
+ "testing"
+ "time"
+
+ xdsresolver "github.com/kdubbo/xds-api/grpc/resolver"
+)
+
+func TestXServerRequiresClientCertificateAndProxiesHTTP(t *testing.T) {
+ caCert, caKey := newTestCA(t)
+ serverCert, serverKey := newSignedCert(t, caCert, caKey, "xserver")
+ clientCert, clientKey := newSignedCert(t, caCert, caKey, "xclient")
+ dir := t.TempDir()
+ writePEM(t, filepath.Join(dir, "root-cert.pem"), "CERTIFICATE", caCert.Raw)
+ writePEM(t, filepath.Join(dir, "cert-chain.pem"), "CERTIFICATE", serverCert.Raw)
+ writePEM(t, filepath.Join(dir, "key.pem"), "RSA PRIVATE KEY", x509.MarshalPKCS1PrivateKey(serverKey))
+ writePEM(t, filepath.Join(dir, "client-cert.pem"), "CERTIFICATE", clientCert.Raw)
+ writePEM(t, filepath.Join(dir, "client-key.pem"), "RSA PRIVATE KEY", x509.MarshalPKCS1PrivateKey(clientKey))
+
+ tlsConfig, err := xserverTLSConfigFromBootstrap(&xdsresolver.BootstrapConfig{
+ CertProviders: map[string]xdsresolver.FileWatcherCertConfig{
+ "default": {
+ CertificateFile: filepath.Join(dir, "cert-chain.pem"),
+ PrivateKeyFile: filepath.Join(dir, "key.pem"),
+ CACertificateFile: filepath.Join(dir, "root-cert.pem"),
+ },
+ },
+ })
+ if err != nil {
+ t.Fatalf("xserverTLSConfigFromBootstrap() failed: %v", err)
+ }
+
+ upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+ _, _ = fmt.Fprintln(w, "nginx v1")
+ }))
+ defer upstream.Close()
+ upstreamAddr := upstream.Listener.Addr().String()
+
+ listener, err := net.Listen("tcp", "127.0.0.1:0")
+ if err != nil {
+ t.Fatalf("net.Listen() failed: %v", err)
+ }
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ errCh := make(chan error, 1)
+ go func() {
+ errCh <- serveXServer(ctx, listener, tlsConfig, upstreamAddr, time.Second, time.Second)
+ }()
+ t.Cleanup(func() {
+ cancel()
+ select {
+ case err := <-errCh:
+ if err != nil {
+ t.Fatalf("serveXServer() error = %v", err)
+ }
+ case <-time.After(time.Second):
+ t.Fatalf("serveXServer() did not stop")
+ }
+ })
+
+ url := "https://" + listener.Addr().String() + "/"
+ _, err = (&http.Client{Timeout: time.Second, Transport: &http.Transport{TLSClientConfig: &tls.Config{
+ InsecureSkipVerify: true,
+ }}}).Get(url)
+ if err == nil {
+ t.Fatalf("plain TLS request without client certificate succeeded, want failure")
+ }
+
+ cert, err := tls.LoadX509KeyPair(filepath.Join(dir, "client-cert.pem"), filepath.Join(dir, "client-key.pem"))
+ if err != nil {
+ t.Fatalf("tls.LoadX509KeyPair() failed: %v", err)
+ }
+ resp, err := (&http.Client{Timeout: time.Second, Transport: &http.Transport{TLSClientConfig: &tls.Config{
+ InsecureSkipVerify: true,
+ Certificates: []tls.Certificate{cert},
+ }}}).Get(url)
+ if err != nil {
+ t.Fatalf("mTLS request failed: %v", err)
+ }
+ defer resp.Body.Close()
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ t.Fatalf("io.ReadAll() failed: %v", err)
+ }
+ if got, want := string(body), "nginx v1\n"; got != want {
+ t.Fatalf("body = %q, want %q", got, want)
+ }
+}
+
+func newTestCA(t *testing.T) (*x509.Certificate, *rsa.PrivateKey) {
+ t.Helper()
+ key, err := rsa.GenerateKey(rand.Reader, 2048)
+ if err != nil {
+ t.Fatalf("rsa.GenerateKey() failed: %v", err)
+ }
+ tmpl := &x509.Certificate{
+ SerialNumber: big.NewInt(1),
+ Subject: pkix.Name{CommonName: "test-ca"},
+ NotBefore: time.Now().Add(-time.Minute),
+ NotAfter: time.Now().Add(time.Hour),
+ KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
+ BasicConstraintsValid: true,
+ IsCA: true,
+ }
+ der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
+ if err != nil {
+ t.Fatalf("x509.CreateCertificate(CA) failed: %v", err)
+ }
+ cert, err := x509.ParseCertificate(der)
+ if err != nil {
+ t.Fatalf("x509.ParseCertificate(CA) failed: %v", err)
+ }
+ return cert, key
+}
+
+func newSignedCert(t *testing.T, caCert *x509.Certificate, caKey *rsa.PrivateKey, commonName string) (*x509.Certificate, *rsa.PrivateKey) {
+ t.Helper()
+ key, err := rsa.GenerateKey(rand.Reader, 2048)
+ if err != nil {
+ t.Fatalf("rsa.GenerateKey() failed: %v", err)
+ }
+ serial, err := rand.Int(rand.Reader, big.NewInt(1<<62))
+ if err != nil {
+ t.Fatalf("rand.Int() failed: %v", err)
+ }
+ tmpl := &x509.Certificate{
+ SerialNumber: serial,
+ Subject: pkix.Name{CommonName: commonName},
+ NotBefore: time.Now().Add(-time.Minute),
+ NotAfter: time.Now().Add(time.Hour),
+ KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
+ ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
+ DNSNames: []string{commonName},
+ }
+ der, err := x509.CreateCertificate(rand.Reader, tmpl, caCert, &key.PublicKey, caKey)
+ if err != nil {
+ t.Fatalf("x509.CreateCertificate(%s) failed: %v", commonName, err)
+ }
+ cert, err := x509.ParseCertificate(der)
+ if err != nil {
+ t.Fatalf("x509.ParseCertificate(%s) failed: %v", commonName, err)
+ }
+ return cert, key
+}
+
+func writePEM(t *testing.T, path, typ string, der []byte) {
+ t.Helper()
+ file, err := os.Create(path)
+ if err != nil {
+ t.Fatalf("os.Create(%s) failed: %v", path, err)
+ }
+ defer file.Close()
+ if err := pem.Encode(file, &pem.Block{Type: typ, Bytes: der}); err != nil {
+ t.Fatalf("pem.Encode(%s) failed: %v", path, err)
+ }
+}
diff --git a/dubbod/discovery/pkg/bootstrap/proxyless_grpc_controller.go b/dubbod/discovery/pkg/bootstrap/proxyless_grpc_controller.go
index 55c2749c..0c3b19c7 100644
--- a/dubbod/discovery/pkg/bootstrap/proxyless_grpc_controller.go
+++ b/dubbod/discovery/pkg/bootstrap/proxyless_grpc_controller.go
@@ -32,6 +32,7 @@ import (
"github.com/apache/dubbo-kubernetes/pkg/config/host"
configlabels "github.com/apache/dubbo-kubernetes/pkg/config/labels"
meshconfig "github.com/apache/dubbo-kubernetes/pkg/config/mesh"
+ "github.com/apache/dubbo-kubernetes/pkg/config/schema/kind"
"github.com/apache/dubbo-kubernetes/pkg/dubboagency/grpcxds"
kubelib "github.com/apache/dubbo-kubernetes/pkg/kube"
"github.com/apache/dubbo-kubernetes/pkg/kube/controllers"
@@ -46,6 +47,7 @@ import (
"github.com/apache/dubbo-kubernetes/dubbod/security/pkg/pki/ca"
pkiutil "github.com/apache/dubbo-kubernetes/dubbod/security/pkg/pki/util"
caserver "github.com/apache/dubbo-kubernetes/dubbod/security/pkg/server/ca"
+ networking "github.com/kdubbo/api/networking/v1alpha3"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -88,6 +90,15 @@ func (s *Server) initProxylessGRPCWorkloads() error {
controller := newProxylessGRPCWorkloadController(s)
s.proxylessGRPCWorkloadController = controller
+ if s.XDSServer != nil {
+ previous := s.XDSServer.RuntimeConfigUpdate
+ s.XDSServer.RuntimeConfigUpdate = func(req *discoverymodel.PushRequest) {
+ if previous != nil {
+ previous(req)
+ }
+ controller.handleRuntimeConfigUpdate(req)
+ }
+ }
s.addStartFunc(proxylessGRPCControllerName, func(stop <-chan struct{}) error {
go controller.Run(stop)
return nil
@@ -164,6 +175,32 @@ func (c *proxylessGRPCWorkloadController) enqueueAllPods() {
}
}
+func (c *proxylessGRPCWorkloadController) handleRuntimeConfigUpdate(req *discoverymodel.PushRequest) {
+ if !proxylessGRPCRuntimeConfigNeedsUpdate(req) {
+ return
+ }
+ c.enqueueAllPods()
+}
+
+func proxylessGRPCRuntimeConfigNeedsUpdate(req *discoverymodel.PushRequest) bool {
+ if req == nil {
+ return false
+ }
+ if req.Full || req.Forced || len(req.AddressesUpdated) > 0 {
+ return true
+ }
+ for cfg := range req.ConfigsUpdated {
+ switch cfg.Kind {
+ case kind.MeshService, kind.PeerAuthentication, kind.Service, kind.EndpointSlice, kind.Endpoints, kind.Pod, kind.Namespace:
+ return true
+ }
+ }
+ if req.Reason.Has(discoverymodel.EndpointUpdate) || req.Reason.Has(discoverymodel.HeadlessEndpointUpdate) || req.Reason.Has(discoverymodel.GlobalUpdate) {
+ return true
+ }
+ return false
+}
+
func (c *proxylessGRPCWorkloadController) enqueueManagedPodsBatched(stop <-chan struct{}) {
keys := c.managedPodKeys()
for start := 0; start < len(keys); start += proxylessGRPCBundleRequeueBatch {
@@ -313,8 +350,9 @@ type proxylessGRPCServiceRuntimeConfig struct {
}
type proxylessGRPCPortRuntimeConfig struct {
- Name string `json:"name,omitempty"`
- Port int `json:"port"`
+ Name string `json:"name,omitempty"`
+ Port int `json:"port"`
+ MTLSMode string `json:"mtlsMode,omitempty"`
}
type proxylessGRPCRouteRuntimeConfig struct {
@@ -327,6 +365,7 @@ type proxylessGRPCDestinationRuntimeConfig struct {
Host string `json:"host"`
Subset string `json:"subset,omitempty"`
Weight int `json:"weight"`
+ TLSMode string `json:"tlsMode,omitempty"`
Endpoints []proxylessGRPCEndpointRuntimeConfig `json:"endpoints,omitempty"`
}
@@ -350,7 +389,8 @@ func (c *proxylessGRPCWorkloadController) buildSecret(pod *corev1.Pod, current *
return nil, time.Time{}, err
}
- runtimeConfigJSON, err := buildRuntimeConfigJSON(workload, nil, nil)
+ services, routes := c.buildRuntimeTrafficConfig()
+ runtimeConfigJSON, err := buildRuntimeConfigJSON(workload, services, routes)
if err != nil {
return nil, time.Time{}, err
}
@@ -555,7 +595,7 @@ func (c *proxylessGRPCWorkloadController) buildRuntimeTrafficConfig() ([]proxyle
if svc == nil {
continue
}
- serviceConfigs = append(serviceConfigs, buildRuntimeServiceConfig(endpoints, svc))
+ serviceConfigs = append(serviceConfigs, buildRuntimeServiceConfig(push, endpoints, svc))
for _, port := range svc.Ports {
if port == nil {
continue
@@ -566,7 +606,7 @@ func (c *proxylessGRPCWorkloadController) buildRuntimeTrafficConfig() ([]proxyle
return serviceConfigs, routeConfigs
}
-func buildRuntimeServiceConfig(endpointIndex *discoverymodel.EndpointIndex, svc *discoverymodel.Service) proxylessGRPCServiceRuntimeConfig {
+func buildRuntimeServiceConfig(push *discoverymodel.PushContext, endpointIndex *discoverymodel.EndpointIndex, svc *discoverymodel.Service) proxylessGRPCServiceRuntimeConfig {
cfg := proxylessGRPCServiceRuntimeConfig{
Host: string(svc.Hostname),
Namespace: svc.Attributes.Namespace,
@@ -576,7 +616,11 @@ func buildRuntimeServiceConfig(endpointIndex *discoverymodel.EndpointIndex, svc
if port == nil {
continue
}
- cfg.Ports = append(cfg.Ports, proxylessGRPCPortRuntimeConfig{Name: port.Name, Port: port.Port})
+ cfg.Ports = append(cfg.Ports, proxylessGRPCPortRuntimeConfig{
+ Name: port.Name,
+ Port: port.Port,
+ MTLSMode: runtimeInboundMTLSMode(push, svc.Attributes.Namespace, port.Port),
+ })
cfg.Endpoints = append(cfg.Endpoints, runtimeEndpointsForService(endpointIndex, svc, port.Port, nil)...)
}
sortRuntimeEndpoints(cfg.Endpoints)
@@ -590,10 +634,12 @@ func buildRuntimeRouteConfig(push *discoverymodel.PushContext, endpointIndex *di
}
vs := push.VirtualServiceForHost(svc.Hostname)
if vs == nil || len(vs.Http) == 0 {
+ tlsMode := runtimeDestinationTLSMode(push, svc.Attributes.Namespace, svc.Hostname, "")
cfg.Destinations = []proxylessGRPCDestinationRuntimeConfig{{
Host: string(svc.Hostname),
Weight: 100,
- Endpoints: runtimeEndpointsForService(endpointIndex, svc, port, nil),
+ TLSMode: tlsMode,
+ Endpoints: runtimeRouteEndpointsForService(endpointIndex, svc, port, nil, tlsMode),
}}
return cfg
}
@@ -624,25 +670,45 @@ func buildRuntimeRouteConfig(push *discoverymodel.PushContext, endpointIndex *di
if subsetFound {
endpoints = runtimeEndpointsForService(endpointIndex, targetSvc, port, selector)
}
+ tlsMode := runtimeDestinationTLSMode(push, targetSvc.Attributes.Namespace, targetSvc.Hostname, subset)
cfg.Destinations = append(cfg.Destinations, proxylessGRPCDestinationRuntimeConfig{
Host: targetHost,
Subset: subset,
Weight: int(weighted.Weight),
- Endpoints: endpoints,
+ TLSMode: tlsMode,
+ Endpoints: rewriteRuntimeEndpointPortsForTLS(endpoints, tlsMode),
})
}
}
if len(cfg.Destinations) == 0 {
+ tlsMode := runtimeDestinationTLSMode(push, svc.Attributes.Namespace, svc.Hostname, "")
cfg.Destinations = []proxylessGRPCDestinationRuntimeConfig{{
Host: string(svc.Hostname),
Weight: 100,
- Endpoints: runtimeEndpointsForService(endpointIndex, svc, port, nil),
+ TLSMode: tlsMode,
+ Endpoints: runtimeRouteEndpointsForService(endpointIndex, svc, port, nil, tlsMode),
}}
}
normalizeRuntimeRouteWeights(cfg.Destinations)
return cfg
}
+func runtimeRouteEndpointsForService(endpointIndex *discoverymodel.EndpointIndex, svc *discoverymodel.Service, port int, selector configlabels.Instance, tlsMode string) []proxylessGRPCEndpointRuntimeConfig {
+ return rewriteRuntimeEndpointPortsForTLS(runtimeEndpointsForService(endpointIndex, svc, port, selector), tlsMode)
+}
+
+func rewriteRuntimeEndpointPortsForTLS(endpoints []proxylessGRPCEndpointRuntimeConfig, tlsMode string) []proxylessGRPCEndpointRuntimeConfig {
+ if tlsMode != "DUBBO_MUTUAL" {
+ return endpoints
+ }
+ out := make([]proxylessGRPCEndpointRuntimeConfig, len(endpoints))
+ copy(out, endpoints)
+ for i := range out {
+ out[i].Port = inject.ProxylessXServerPort
+ }
+ return out
+}
+
func runtimeSubsetSelector(push *discoverymodel.PushContext, namespace string, hostname host.Name, subset string) (configlabels.Instance, bool) {
if subset == "" {
return nil, true
@@ -659,6 +725,52 @@ func runtimeSubsetSelector(push *discoverymodel.PushContext, namespace string, h
return nil, false
}
+func runtimeDestinationTLSMode(push *discoverymodel.PushContext, namespace string, hostname host.Name, subset string) string {
+ if push == nil {
+ return ""
+ }
+ rule := push.DestinationRuleForService(namespace, hostname)
+ if rule == nil {
+ return ""
+ }
+ if subset != "" {
+ for _, ss := range rule.Subsets {
+ if ss.Name == subset {
+ if ss.TrafficPolicy != nil {
+ return runtimeTrafficPolicyTLSMode(ss.TrafficPolicy)
+ }
+ break
+ }
+ }
+ }
+ return runtimeTrafficPolicyTLSMode(rule.TrafficPolicy)
+}
+
+func runtimeTrafficPolicyTLSMode(policy *networking.TrafficPolicy) string {
+ if policy == nil || policy.Tls == nil {
+ return ""
+ }
+ return policy.Tls.Mode.String()
+}
+
+func runtimeInboundMTLSMode(push *discoverymodel.PushContext, namespace string, port int) string {
+ if push == nil || push.AuthenticationPolicies == nil || port <= 0 {
+ return ""
+ }
+ return runtimeMutualTLSModeString(push.AuthenticationPolicies.EffectiveMutualTLSMode(namespace, nil, uint32(port)))
+}
+
+func runtimeMutualTLSModeString(mode discoverymodel.MutualTLSMode) string {
+ switch mode {
+ case discoverymodel.MTLSDisable:
+ return "DISABLE"
+ case discoverymodel.MTLSStrict:
+ return "STRICT"
+ default:
+ return ""
+ }
+}
+
func normalizeRuntimeRouteWeights(destinations []proxylessGRPCDestinationRuntimeConfig) {
hasPositiveWeight := false
for i := range destinations {
diff --git a/dubbod/discovery/pkg/bootstrap/proxyless_grpc_controller_test.go b/dubbod/discovery/pkg/bootstrap/proxyless_grpc_controller_test.go
index d048bf87..f8ff7716 100644
--- a/dubbod/discovery/pkg/bootstrap/proxyless_grpc_controller_test.go
+++ b/dubbod/discovery/pkg/bootstrap/proxyless_grpc_controller_test.go
@@ -20,9 +20,23 @@ import (
"testing"
"time"
+ "github.com/apache/dubbo-kubernetes/dubbod/discovery/pkg/config/memory"
+ discoverymodel "github.com/apache/dubbo-kubernetes/dubbod/discovery/pkg/model"
+ "github.com/apache/dubbo-kubernetes/pkg/config"
"github.com/apache/dubbo-kubernetes/pkg/config/constants"
+ "github.com/apache/dubbo-kubernetes/pkg/config/host"
+ "github.com/apache/dubbo-kubernetes/pkg/config/mesh"
+ "github.com/apache/dubbo-kubernetes/pkg/config/mesh/meshwatcher"
+ "github.com/apache/dubbo-kubernetes/pkg/config/protocol"
+ "github.com/apache/dubbo-kubernetes/pkg/config/schema/collections"
+ "github.com/apache/dubbo-kubernetes/pkg/config/schema/gvk"
+ "github.com/apache/dubbo-kubernetes/pkg/config/schema/kind"
"github.com/apache/dubbo-kubernetes/pkg/dubboagency/grpcxds"
"github.com/apache/dubbo-kubernetes/pkg/kube/inject"
+ "github.com/apache/dubbo-kubernetes/pkg/kube/krt"
+ "github.com/apache/dubbo-kubernetes/pkg/util/sets"
+ networking "github.com/kdubbo/api/networking/v1alpha3"
+ security "github.com/kdubbo/api/security/v1alpha3"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
@@ -169,6 +183,80 @@ func TestBuildRuntimeConfigJSON(t *testing.T) {
}
}
+func TestBuildRuntimeTrafficConfigCapturesProxylessSecurity(t *testing.T) {
+ hostname := host.Name("provider.grpc-app.svc.cluster.local")
+ svc := newProxylessRuntimeTestService("provider", "grpc-app", string(hostname), 17070)
+ push := newProxylessRuntimeTestPushContext(t, []config.Config{
+ newProxylessMTLSMeshServiceConfig("provider-mtls", "grpc-app", hostname),
+ newProxylessStrictPeerAuthenticationConfig("grpc-app-strict-mtls", "grpc-app"),
+ }, []*discoverymodel.Service{svc})
+
+ serviceConfig := buildRuntimeServiceConfig(push, nil, svc)
+ if len(serviceConfig.Ports) != 1 {
+ t.Fatalf("ports = %d, want 1", len(serviceConfig.Ports))
+ }
+ if got := serviceConfig.Ports[0].MTLSMode; got != "STRICT" {
+ t.Fatalf("mtlsMode = %q, want STRICT", got)
+ }
+
+ routeConfig := buildRuntimeRouteConfig(push, nil, svc, 17070)
+ if len(routeConfig.Destinations) != 2 {
+ t.Fatalf("destinations = %d, want 2", len(routeConfig.Destinations))
+ }
+ wantWeights := map[string]int{"v1": 50, "v2": 50}
+ for _, destination := range routeConfig.Destinations {
+ if destination.TLSMode != "DUBBO_MUTUAL" {
+ t.Fatalf("destination %s tlsMode = %q, want DUBBO_MUTUAL", destination.Subset, destination.TLSMode)
+ }
+ if wantWeights[destination.Subset] != destination.Weight {
+ t.Fatalf("destination %s weight = %d, want %d", destination.Subset, destination.Weight, wantWeights[destination.Subset])
+ }
+ }
+}
+
+func TestProxylessGRPCRuntimeConfigNeedsUpdate(t *testing.T) {
+ tests := []struct {
+ name string
+ req *discoverymodel.PushRequest
+ want bool
+ }{
+ {
+ name: "full push",
+ req: &discoverymodel.PushRequest{Full: true},
+ want: true,
+ },
+ {
+ name: "meshservice",
+ req: &discoverymodel.PushRequest{
+ ConfigsUpdated: sets.New(discoverymodel.ConfigKey{Kind: kind.MeshService, Name: "provider-mtls", Namespace: "grpc-app"}),
+ },
+ want: true,
+ },
+ {
+ name: "peerauthentication",
+ req: &discoverymodel.PushRequest{
+ ConfigsUpdated: sets.New(discoverymodel.ConfigKey{Kind: kind.PeerAuthentication, Name: "strict", Namespace: "grpc-app"}),
+ },
+ want: true,
+ },
+ {
+ name: "unrelated configmap",
+ req: &discoverymodel.PushRequest{
+ ConfigsUpdated: sets.New(discoverymodel.ConfigKey{Kind: kind.ConfigMap, Name: "ui", Namespace: "dubbo-system"}),
+ },
+ want: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := proxylessGRPCRuntimeConfigNeedsUpdate(tt.req); got != tt.want {
+ t.Fatalf("proxylessGRPCRuntimeConfigNeedsUpdate() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
func TestNextRotationTime(t *testing.T) {
now := time.Now()
rotations := map[types.NamespacedName]time.Time{
@@ -189,6 +277,117 @@ func TestNextRotationTime(t *testing.T) {
}
}
+func newProxylessRuntimeTestPushContext(t *testing.T, configs []config.Config, services []*discoverymodel.Service) *discoverymodel.PushContext {
+ t.Helper()
+
+ store := memory.Make(collections.DubboGatewayAPI())
+ for _, cfg := range configs {
+ if _, err := store.Create(cfg); err != nil {
+ t.Fatalf("create config %s/%s: %v", cfg.Namespace, cfg.Name, err)
+ }
+ }
+
+ env := discoverymodel.NewEnvironment()
+ env.ConfigStore = store
+ env.ServiceDiscovery = proxylessRuntimeStaticServiceDiscovery{services: services}
+ env.Watcher = meshwatcher.ConfigAdapter(krt.NewStatic(&meshwatcher.MeshConfigResource{
+ MeshConfig: mesh.DefaultMeshConfig(),
+ }, true))
+ env.Init()
+
+ push := discoverymodel.NewPushContext()
+ push.InitContext(env, nil, nil)
+ return push
+}
+
+func newProxylessRuntimeTestService(name, namespace, hostname string, port int) *discoverymodel.Service {
+ return &discoverymodel.Service{
+ Hostname: host.Name(hostname),
+ Ports: discoverymodel.PortList{
+ {
+ Name: "grpc",
+ Port: port,
+ Protocol: protocol.HTTP2,
+ },
+ },
+ Attributes: discoverymodel.ServiceAttributes{
+ Name: name,
+ Namespace: namespace,
+ },
+ }
+}
+
+func newProxylessMTLSMeshServiceConfig(name, namespace string, hostname host.Name) config.Config {
+ return config.Config{
+ Meta: config.Meta{
+ GroupVersionKind: gvk.MeshService,
+ Name: name,
+ Namespace: namespace,
+ Domain: "cluster.local",
+ },
+ Spec: &networking.MeshService{
+ Hosts: []string{string(hostname)},
+ TrafficPolicy: &networking.TrafficPolicy{
+ Tls: &networking.ClientTLSSettings{
+ Mode: networking.ClientTLSSettings_DUBBO_MUTUAL,
+ },
+ },
+ Routes: []*networking.MeshServiceRoute{{
+ Service: []*networking.ServiceDestination{{
+ Name: "v1",
+ Host: string(hostname),
+ Labels: map[string]string{"version": "v1"},
+ Port: &networking.ServicePort{Number: 17070},
+ Weight: 50,
+ }, {
+ Name: "v2",
+ Host: string(hostname),
+ Labels: map[string]string{"version": "v2"},
+ Port: &networking.ServicePort{Number: 17070},
+ Weight: 50,
+ }},
+ }},
+ },
+ }
+}
+
+func newProxylessStrictPeerAuthenticationConfig(name, namespace string) config.Config {
+ return config.Config{
+ Meta: config.Meta{
+ GroupVersionKind: gvk.PeerAuthentication,
+ Name: name,
+ Namespace: namespace,
+ Domain: "cluster.local",
+ },
+ Spec: &security.PeerAuthentication{
+ Mtls: &security.PeerAuthentication_MutualTLS{
+ Mode: security.PeerAuthentication_MutualTLS_STRICT,
+ },
+ },
+ }
+}
+
+type proxylessRuntimeStaticServiceDiscovery struct {
+ services []*discoverymodel.Service
+}
+
+func (s proxylessRuntimeStaticServiceDiscovery) Services() []*discoverymodel.Service {
+ return s.services
+}
+
+func (s proxylessRuntimeStaticServiceDiscovery) GetService(hostname host.Name) *discoverymodel.Service {
+ for _, svc := range s.services {
+ if svc.Hostname == hostname {
+ return svc
+ }
+ }
+ return nil
+}
+
+func (s proxylessRuntimeStaticServiceDiscovery) GetProxyServiceTargets(*discoverymodel.Proxy) []discoverymodel.ServiceTarget {
+ return nil
+}
+
func TestScheduleRotationTracksEarliestTimer(t *testing.T) {
now := time.Now()
controller := &proxylessGRPCWorkloadController{
diff --git a/dubbod/discovery/pkg/config/kube/gateway/deployment_controller_test.go b/dubbod/discovery/pkg/config/kube/gateway/deployment_controller_test.go
index 25de3817..3cc8b9a8 100644
--- a/dubbod/discovery/pkg/config/kube/gateway/deployment_controller_test.go
+++ b/dubbod/discovery/pkg/config/kube/gateway/deployment_controller_test.go
@@ -318,12 +318,21 @@ func TestKubeGatewayTemplateRendersDxgateResources(t *testing.T) {
if !strings.Contains(rendered[2], "DXGATE_BOOTSTRAP") || strings.Contains(rendered[2], "DXGATE_STATIC_CONFIG") {
t.Fatalf("deployment did not switch from static config to bootstrap:\n%s", rendered[2])
}
+ if !strings.Contains(rendered[2], `proxyless.dubbo.apache.org/inject: "true"`) {
+ t.Fatalf("deployment pod template did not enable proxyless injection for dxgate mTLS certs:\n%s", rendered[2])
+ }
+ if !strings.Contains(rendered[2], "inject.dubbo.apache.org/templates: grpc-engine") {
+ t.Fatalf("deployment pod template did not request grpc-engine injection:\n%s", rendered[2])
+ }
if !strings.Contains(rendered[2], "app.kubernetes.io/instance: public-dubbo") {
t.Fatalf("deployment did not render stable dxgate instance label:\n%s", rendered[2])
}
if !strings.Contains(rendered[3], "targetPort: http") {
t.Fatalf("service did not target dxgate http port:\n%s", rendered[3])
}
+ if !strings.Contains(rendered[3], `proxyless.dubbo.apache.org/inject: "false"`) {
+ t.Fatalf("service did not opt out of proxyless targetPort rewriting:\n%s", rendered[3])
+ }
if !strings.Contains(rendered[3], "app.kubernetes.io/instance: public-dubbo") {
t.Fatalf("service did not render stable dxgate instance selector:\n%s", rendered[3])
}
diff --git a/dubbod/discovery/pkg/model/virtualservice.go b/dubbod/discovery/pkg/model/virtualservice.go
index a4a31607..5ff17981 100644
--- a/dubbod/discovery/pkg/model/virtualservice.go
+++ b/dubbod/discovery/pkg/model/virtualservice.go
@@ -28,8 +28,7 @@ func meshServiceToVirtualServiceConfig(msConfig config.Config) config.Config {
return r
}
vs := &networking.VirtualService{
- Hosts: append([]string(nil), ms.Hosts...),
- ExportTo: append([]string(nil), ms.VisibleTo...),
+ Hosts: append([]string(nil), ms.Hosts...),
}
for _, route := range ms.Routes {
if route == nil || len(route.Service) == 0 {
@@ -69,7 +68,6 @@ func meshServiceToDestinationRuleConfig(msConfig config.Config) config.Config {
dr := &networking.DestinationRule{
Host: firstMeshServiceHost(ms, r.Meta),
TrafficPolicy: ms.TrafficPolicy,
- ExportTo: append([]string(nil), ms.VisibleTo...),
}
seen := setsString{}
for _, route := range ms.Routes {
diff --git a/dubbod/discovery/pkg/networking/grpcgen/mtls_test.go b/dubbod/discovery/pkg/networking/grpcgen/mtls_test.go
new file mode 100644
index 00000000..55bd2225
--- /dev/null
+++ b/dubbod/discovery/pkg/networking/grpcgen/mtls_test.go
@@ -0,0 +1,192 @@
+package grpcgen
+
+import (
+ "testing"
+
+ "github.com/apache/dubbo-kubernetes/dubbod/discovery/pkg/model"
+ "github.com/apache/dubbo-kubernetes/pkg/config"
+ "github.com/apache/dubbo-kubernetes/pkg/config/host"
+ "github.com/apache/dubbo-kubernetes/pkg/config/schema/gvk"
+ "github.com/apache/dubbo-kubernetes/pkg/dubboagency/grpcxds"
+ networking "github.com/kdubbo/api/networking/v1alpha3"
+ security "github.com/kdubbo/api/security/v1alpha3"
+ cluster "github.com/kdubbo/xds-api/cluster/v1"
+ tlsv1 "github.com/kdubbo/xds-api/extensions/transport_sockets/tls/v1"
+ listener "github.com/kdubbo/xds-api/listener/v1"
+)
+
+func TestMeshServiceDUBBOMutualBuildsUpstreamTLS(t *testing.T) {
+ hostname := host.Name("nginx.app.svc.cluster.local")
+ clusterName := model.BuildSubsetKey(model.TrafficDirectionOutbound, "", hostname, 80)
+ push := newRDSTestPushContext(t, []config.Config{
+ newMTLSMeshServiceConfig("nginx-mtls", "app", hostname),
+ }, []*model.Service{
+ newRDSTestService("nginx", "app", string(hostname), 80),
+ })
+
+ resources := (&GrpcConfigGenerator{}).BuildClusters(newMTLSTestProxy(), push, []string{clusterName})
+ c := findCluster(t, resources, clusterName)
+ if c.GetTransportSocket() == nil {
+ t.Fatalf("cluster %s has no transport socket", clusterName)
+ }
+
+ tlsContext := &tlsv1.UpstreamTlsContext{}
+ if err := c.GetTransportSocket().GetTypedConfig().UnmarshalTo(tlsContext); err != nil {
+ t.Fatalf("unmarshal upstream tls context: %v", err)
+ }
+ if tlsContext.GetSni() != string(hostname) {
+ t.Fatalf("SNI = %q, want %q", tlsContext.GetSni(), hostname)
+ }
+ assertCommonTLSContext(t, tlsContext.GetCommonTlsContext())
+}
+
+func TestPeerAuthenticationStrictBuildsDownstreamMTLSListener(t *testing.T) {
+ svc := newRDSTestService("nginx", "app", "nginx.app.svc.cluster.local", 80)
+ proxy := newMTLSTestProxy()
+ proxy.ServiceTargets = []model.ServiceTarget{{
+ Service: svc,
+ Port: model.ServiceInstancePort{
+ ServicePort: svc.Ports[0],
+ TargetPort: 80,
+ },
+ }}
+ push := newRDSTestPushContext(t, []config.Config{
+ newStrictPeerAuthenticationConfig("app-strict-mtls", "app"),
+ }, []*model.Service{svc})
+
+ listenerName := grpcxds.ServerListenerNamePrefix + "0.0.0.0:80"
+ resources := (&GrpcConfigGenerator{}).BuildListeners(proxy, push, []string{listenerName})
+ l := findListener(t, resources, listenerName)
+ if len(l.GetFilterChains()) != 1 {
+ t.Fatalf("filter chains = %d, want 1", len(l.GetFilterChains()))
+ }
+ transportSocket := l.GetFilterChains()[0].GetTransportSocket()
+ if transportSocket == nil {
+ t.Fatalf("listener %s has no transport socket", listenerName)
+ }
+
+ tlsContext := &tlsv1.DownstreamTlsContext{}
+ if err := transportSocket.GetTypedConfig().UnmarshalTo(tlsContext); err != nil {
+ t.Fatalf("unmarshal downstream tls context: %v", err)
+ }
+ if tlsContext.GetRequireClientCertificate() == nil || !tlsContext.GetRequireClientCertificate().GetValue() {
+ t.Fatalf("RequireClientCertificate = %v, want true", tlsContext.GetRequireClientCertificate())
+ }
+ assertCommonTLSContext(t, tlsContext.GetCommonTlsContext())
+}
+
+func newMTLSTestProxy() *model.Proxy {
+ return &model.Proxy{
+ ID: "proxyless~10.0.0.1~nginx-consumer.app~app.svc.cluster.local",
+ Type: model.Proxyless,
+ DNSDomain: "app.svc.cluster.local",
+ ConfigNamespace: "app",
+ Metadata: &model.NodeMetadata{
+ Namespace: "app",
+ },
+ }
+}
+
+func newMTLSMeshServiceConfig(name, namespace string, hostname host.Name) config.Config {
+ return config.Config{
+ Meta: config.Meta{
+ GroupVersionKind: gvk.MeshService,
+ Name: name,
+ Namespace: namespace,
+ Domain: "cluster.local",
+ },
+ Spec: &networking.MeshService{
+ Hosts: []string{string(hostname)},
+ TrafficPolicy: &networking.TrafficPolicy{
+ Tls: &networking.ClientTLSSettings{
+ Mode: networking.ClientTLSSettings_DUBBO_MUTUAL,
+ },
+ },
+ Routes: []*networking.MeshServiceRoute{{
+ Service: []*networking.ServiceDestination{{
+ Name: "v1",
+ Host: string(hostname),
+ Labels: map[string]string{"version": "v1"},
+ Port: &networking.ServicePort{Number: 80},
+ Weight: 50,
+ }, {
+ Name: "v2",
+ Host: string(hostname),
+ Labels: map[string]string{"version": "v2"},
+ Port: &networking.ServicePort{Number: 80},
+ Weight: 50,
+ }},
+ }},
+ },
+ }
+}
+
+func newStrictPeerAuthenticationConfig(name, namespace string) config.Config {
+ return config.Config{
+ Meta: config.Meta{
+ GroupVersionKind: gvk.PeerAuthentication,
+ Name: name,
+ Namespace: namespace,
+ Domain: "cluster.local",
+ },
+ Spec: &security.PeerAuthentication{
+ Mtls: &security.PeerAuthentication_MutualTLS{
+ Mode: security.PeerAuthentication_MutualTLS_STRICT,
+ },
+ },
+ }
+}
+
+func findCluster(t *testing.T, resources model.Resources, name string) *cluster.Cluster {
+ t.Helper()
+ for _, resource := range resources {
+ if resource.Name != name {
+ continue
+ }
+ c := &cluster.Cluster{}
+ if err := resource.Resource.UnmarshalTo(c); err != nil {
+ t.Fatalf("unmarshal cluster %s: %v", name, err)
+ }
+ return c
+ }
+ t.Fatalf("cluster %s not found in %d resources", name, len(resources))
+ return nil
+}
+
+func findListener(t *testing.T, resources model.Resources, name string) *listener.Listener {
+ t.Helper()
+ for _, resource := range resources {
+ if resource.Name != name {
+ continue
+ }
+ l := &listener.Listener{}
+ if err := resource.Resource.UnmarshalTo(l); err != nil {
+ t.Fatalf("unmarshal listener %s: %v", name, err)
+ }
+ return l
+ }
+ t.Fatalf("listener %s not found in %d resources", name, len(resources))
+ return nil
+}
+
+func assertCommonTLSContext(t *testing.T, ctx *tlsv1.CommonTlsContext) {
+ t.Helper()
+ if ctx == nil {
+ t.Fatal("common TLS context is nil")
+ }
+ if got := ctx.GetAlpnProtocols(); len(got) != 1 || got[0] != "h2" {
+ t.Fatalf("ALPN protocols = %v, want [h2]", got)
+ }
+ cert := ctx.GetTlsCertificateCertificateProviderInstance()
+ if cert == nil || cert.GetInstanceName() != "default" || cert.GetCertificateName() != "default" {
+ t.Fatalf("workload cert provider = %v, want default/default", cert)
+ }
+ combined := ctx.GetCombinedValidationContext()
+ if combined == nil {
+ t.Fatal("combined validation context is nil")
+ }
+ root := combined.GetValidationContextCertificateProviderInstance()
+ if root == nil || root.GetInstanceName() != "default" || root.GetCertificateName() != "ROOTCA" {
+ t.Fatalf("root cert provider = %v, want default/ROOTCA", root)
+ }
+}
diff --git a/dubbod/discovery/pkg/xds/eds.go b/dubbod/discovery/pkg/xds/eds.go
index d49ec190..bdd484e1 100644
--- a/dubbod/discovery/pkg/xds/eds.go
+++ b/dubbod/discovery/pkg/xds/eds.go
@@ -36,7 +36,6 @@ type EdsGenerator struct {
var _ model.XdsDeltaResourceGenerator = &EdsGenerator{}
var skippedEdsConfigs = sets.New(
- kind.MeshService,
kind.PeerAuthentication,
kind.Secret,
)
diff --git a/dubbod/discovery/pkg/xds/endpoints/endpoint_builder.go b/dubbod/discovery/pkg/xds/endpoints/endpoint_builder.go
index d99fd205..fea15978 100644
--- a/dubbod/discovery/pkg/xds/endpoints/endpoint_builder.go
+++ b/dubbod/discovery/pkg/xds/endpoints/endpoint_builder.go
@@ -23,8 +23,10 @@ import (
"github.com/apache/dubbo-kubernetes/dubbod/discovery/pkg/networking/util"
"github.com/apache/dubbo-kubernetes/pkg/config/host"
"github.com/apache/dubbo-kubernetes/pkg/config/labels"
+ "github.com/apache/dubbo-kubernetes/pkg/kube/inject"
dubbolog "github.com/apache/dubbo-kubernetes/pkg/log"
"github.com/cespare/xxhash/v2"
+ networking "github.com/kdubbo/api/networking/v1alpha3"
// core "github.com/kdubbo/xds-api/core/v1"
core "github.com/kdubbo/xds-api/core/v1"
// endpoint "github.com/kdubbo/xds-api/endpoint/v1"
@@ -84,14 +86,6 @@ func (b *EndpointBuilder) BuildClusterLoadAssignment(endpointIndex *model.Endpoi
return buildEmptyClusterLoadAssignment(b.clusterName)
}
- // Get endpoints from the endpoint index
- allServices := endpointIndex.AllServices()
- if len(allServices) > 0 {
- log.Infof("EndpointIndex contains %d services: %v", len(allServices), allServices)
- } else {
- log.Warnf("EndpointIndex is empty - no services registered")
- }
-
shards, ok := endpointIndex.ShardsForService(string(b.hostname), b.service.Attributes.Namespace)
if !ok {
// Also log what services ARE available in the namespace
@@ -267,7 +261,7 @@ func (b *EndpointBuilder) buildLbEndpoint(ep *model.DubboEndpoint) *endpoint.LbE
return nil
}
- address := util.BuildAddress(ep.Addresses[0], ep.EndpointPort)
+ address := util.BuildAddress(ep.Addresses[0], b.endpointPort(ep))
if address == nil {
return nil
}
@@ -298,6 +292,45 @@ func (b *EndpointBuilder) buildLbEndpoint(ep *model.DubboEndpoint) *endpoint.LbE
}
}
+func (b *EndpointBuilder) endpointPort(ep *model.DubboEndpoint) uint32 {
+ if b.useXServerEndpointPort() {
+ return inject.ProxylessXServerPort
+ }
+ return ep.EndpointPort
+}
+
+func (b *EndpointBuilder) useXServerEndpointPort() bool {
+ if b == nil || b.proxy == nil || !b.proxy.IsProxylessGrpc() || b.push == nil || b.service == nil {
+ return false
+ }
+ return b.destinationUsesDUBBOMutual()
+}
+
+func (b *EndpointBuilder) destinationUsesDUBBOMutual() bool {
+ rule := b.push.DestinationRuleForService(b.service.Attributes.Namespace, b.hostname)
+ if rule == nil && b.service.Hostname != "" && b.service.Hostname != b.hostname {
+ rule = b.push.DestinationRuleForService(b.service.Attributes.Namespace, b.service.Hostname)
+ }
+ if rule == nil {
+ return false
+ }
+ if b.subsetName != "" {
+ for _, subset := range rule.Subsets {
+ if subset.Name == b.subsetName && subset.TrafficPolicy != nil {
+ return trafficPolicyUsesDUBBOMutual(subset.TrafficPolicy)
+ }
+ }
+ }
+ return trafficPolicyUsesDUBBOMutual(rule.TrafficPolicy)
+}
+
+func trafficPolicyUsesDUBBOMutual(policy *networking.TrafficPolicy) bool {
+ if policy == nil || policy.Tls == nil {
+ return false
+ }
+ return policy.Tls.Mode == networking.ClientTLSSettings_DUBBO_MUTUAL || policy.Tls.Mode.String() == "DUBBO_MUTUAL"
+}
+
func buildEmptyClusterLoadAssignment(clusterName string) *endpoint.ClusterLoadAssignment {
// preventing "weighted-target: no targets to pick from" errors
return &endpoint.ClusterLoadAssignment{
diff --git a/dubbod/discovery/pkg/xds/endpoints/endpoint_builder_test.go b/dubbod/discovery/pkg/xds/endpoints/endpoint_builder_test.go
new file mode 100644
index 00000000..21714332
--- /dev/null
+++ b/dubbod/discovery/pkg/xds/endpoints/endpoint_builder_test.go
@@ -0,0 +1,167 @@
+package endpoints
+
+import (
+ "testing"
+
+ "github.com/apache/dubbo-kubernetes/dubbod/discovery/pkg/config/memory"
+ "github.com/apache/dubbo-kubernetes/dubbod/discovery/pkg/model"
+ "github.com/apache/dubbo-kubernetes/pkg/config"
+ "github.com/apache/dubbo-kubernetes/pkg/config/host"
+ "github.com/apache/dubbo-kubernetes/pkg/config/mesh"
+ "github.com/apache/dubbo-kubernetes/pkg/config/mesh/meshwatcher"
+ "github.com/apache/dubbo-kubernetes/pkg/config/protocol"
+ "github.com/apache/dubbo-kubernetes/pkg/config/schema/collections"
+ "github.com/apache/dubbo-kubernetes/pkg/config/schema/gvk"
+ "github.com/apache/dubbo-kubernetes/pkg/kube/inject"
+ "github.com/apache/dubbo-kubernetes/pkg/kube/krt"
+ networking "github.com/kdubbo/api/networking/v1alpha3"
+ endpoint "github.com/kdubbo/xds-api/endpoint/v1"
+)
+
+func TestBuildClusterLoadAssignmentUsesXServerPortForDUBBOMutual(t *testing.T) {
+ hostname := host.Name("nginx.app.svc.cluster.local")
+ svc := newEndpointTestService("nginx", "app", string(hostname), 80)
+ push := newEndpointTestPushContext(t, []config.Config{
+ newEndpointTestMTLSMeshService("nginx-routing", "app", hostname),
+ }, []*model.Service{svc})
+ index := model.NewEndpointIndex(model.DisabledCache{})
+ index.UpdateServiceEndpoints(model.ShardKey{}, string(hostname), "app", []*model.DubboEndpoint{{
+ Addresses: []string{"10.0.0.1"},
+ EndpointPort: 80,
+ ServicePortName: "http",
+ Labels: map[string]string{"version": "v1"},
+ HealthStatus: model.Healthy,
+ }}, false)
+
+ clusterName := model.BuildSubsetKey(model.TrafficDirectionOutbound, "v1", hostname, 80)
+ builder := NewEndpointBuilder(clusterName, newEndpointTestProxy(), push)
+ cla := builder.BuildClusterLoadAssignment(index)
+
+ if got := firstEndpointPort(t, cla); got != inject.ProxylessXServerPort {
+ t.Fatalf("endpoint port = %d, want xserver port %d", got, inject.ProxylessXServerPort)
+ }
+}
+
+func TestBuildClusterLoadAssignmentKeepsAppPortWithoutDUBBOMutual(t *testing.T) {
+ hostname := host.Name("nginx.app.svc.cluster.local")
+ svc := newEndpointTestService("nginx", "app", string(hostname), 80)
+ push := newEndpointTestPushContext(t, nil, []*model.Service{svc})
+ index := model.NewEndpointIndex(model.DisabledCache{})
+ index.UpdateServiceEndpoints(model.ShardKey{}, string(hostname), "app", []*model.DubboEndpoint{{
+ Addresses: []string{"10.0.0.1"},
+ EndpointPort: 80,
+ ServicePortName: "http",
+ HealthStatus: model.Healthy,
+ }}, false)
+
+ clusterName := model.BuildSubsetKey(model.TrafficDirectionOutbound, "", hostname, 80)
+ builder := NewEndpointBuilder(clusterName, newEndpointTestProxy(), push)
+ cla := builder.BuildClusterLoadAssignment(index)
+
+ if got := firstEndpointPort(t, cla); got != 80 {
+ t.Fatalf("endpoint port = %d, want app port 80", got)
+ }
+}
+
+func firstEndpointPort(t *testing.T, cla *endpoint.ClusterLoadAssignment) uint32 {
+ t.Helper()
+ localities := cla.GetEndpoints()
+ if len(localities) == 0 || len(localities[0].GetLbEndpoints()) == 0 {
+ t.Fatalf("CLA has no endpoints")
+ }
+ return localities[0].GetLbEndpoints()[0].GetEndpoint().GetAddress().GetSocketAddress().GetPortValue()
+}
+
+func newEndpointTestPushContext(t *testing.T, configs []config.Config, services []*model.Service) *model.PushContext {
+ t.Helper()
+ store := memory.Make(collections.DubboGatewayAPI())
+ for _, cfg := range configs {
+ if _, err := store.Create(cfg); err != nil {
+ t.Fatalf("create config %s/%s: %v", cfg.Namespace, cfg.Name, err)
+ }
+ }
+ env := model.NewEnvironment()
+ env.ConfigStore = store
+ env.ServiceDiscovery = endpointTestServiceDiscovery{services: services}
+ env.Watcher = meshwatcher.ConfigAdapter(krt.NewStatic(&meshwatcher.MeshConfigResource{
+ MeshConfig: mesh.DefaultMeshConfig(),
+ }, true))
+ env.Init()
+ push := model.NewPushContext()
+ push.InitContext(env, nil, nil)
+ return push
+}
+
+func newEndpointTestProxy() *model.Proxy {
+ return &model.Proxy{
+ ID: "proxyless~10.0.0.2~nginx-consumer.app~app.svc.cluster.local",
+ Type: model.Proxyless,
+ ConfigNamespace: "app",
+ Metadata: &model.NodeMetadata{
+ Generator: "grpc",
+ Namespace: "app",
+ },
+ }
+}
+
+func newEndpointTestService(name, namespace, hostname string, port int) *model.Service {
+ return &model.Service{
+ Hostname: host.Name(hostname),
+ Ports: model.PortList{{
+ Name: "http",
+ Port: port,
+ Protocol: protocol.HTTP2,
+ }},
+ Attributes: model.ServiceAttributes{
+ Name: name,
+ Namespace: namespace,
+ },
+ }
+}
+
+func newEndpointTestMTLSMeshService(name, namespace string, hostname host.Name) config.Config {
+ return config.Config{
+ Meta: config.Meta{
+ GroupVersionKind: gvk.MeshService,
+ Name: name,
+ Namespace: namespace,
+ Domain: "cluster.local",
+ },
+ Spec: &networking.MeshService{
+ Hosts: []string{string(hostname)},
+ TrafficPolicy: &networking.TrafficPolicy{
+ Tls: &networking.ClientTLSSettings{Mode: networking.ClientTLSSettings_DUBBO_MUTUAL},
+ },
+ Routes: []*networking.MeshServiceRoute{{
+ Service: []*networking.ServiceDestination{{
+ Name: "v1",
+ Host: string(hostname),
+ Labels: map[string]string{"version": "v1"},
+ Port: &networking.ServicePort{Number: 80},
+ Weight: 100,
+ }},
+ }},
+ },
+ }
+}
+
+type endpointTestServiceDiscovery struct {
+ services []*model.Service
+}
+
+func (s endpointTestServiceDiscovery) Services() []*model.Service {
+ return s.services
+}
+
+func (s endpointTestServiceDiscovery) GetService(hostname host.Name) *model.Service {
+ for _, svc := range s.services {
+ if svc.Hostname == hostname {
+ return svc
+ }
+ }
+ return nil
+}
+
+func (s endpointTestServiceDiscovery) GetProxyServiceTargets(*model.Proxy) []model.ServiceTarget {
+ return nil
+}
diff --git a/dubboinstaller/charts/base/Chart.yaml b/dubboinstaller/charts/base/Chart.yaml
index 0827c2ca..0fb64454 100644
--- a/dubboinstaller/charts/base/Chart.yaml
+++ b/dubboinstaller/charts/base/Chart.yaml
@@ -20,7 +20,7 @@ name: base
home: https://github.com/apache/dubbo-kubernetes
icon: https://avatars.githubusercontent.com/u/11751992?s=200&v=4
description: Helm chart for deploying dubbo cluster resources and CRDs.
-version: 0.3.7
+version: 0.3.9
appVersion: "latest"
maintainers:
- name: mfordjody
diff --git a/dubboinstaller/charts/base/files/crd-all.gen.yaml b/dubboinstaller/charts/base/files/crd-all.gen.yaml
index ea4cd3fc..575eccd1 100644
--- a/dubboinstaller/charts/base/files/crd-all.gen.yaml
+++ b/dubboinstaller/charts/base/files/crd-all.gen.yaml
@@ -232,11 +232,6 @@ spec:
x-kubernetes-int-or-string: true
type: object
type: object
- visibleTo:
- description: Namespaces that can see this mesh service.
- items:
- type: string
- type: array
type: object
status:
properties:
diff --git a/dubboinstaller/charts/dubbod/Chart.yaml b/dubboinstaller/charts/dubbod/Chart.yaml
index cae1aa51..64091abc 100644
--- a/dubboinstaller/charts/dubbod/Chart.yaml
+++ b/dubboinstaller/charts/dubbod/Chart.yaml
@@ -25,7 +25,7 @@ keywords:
- dubbod
- dubbo-discovery
- dubbo-control-plane
-version: 0.3.7
+version: 0.3.9
appVersion: "latest"
maintainers:
- name: mfordjody
diff --git a/dubboinstaller/charts/dubbod/files/grpc-engine.yaml b/dubboinstaller/charts/dubbod/files/grpc-engine.yaml
index 61a84dc6..7f3a2d9e 100644
--- a/dubboinstaller/charts/dubbod/files/grpc-engine.yaml
+++ b/dubboinstaller/charts/dubbod/files/grpc-engine.yaml
@@ -74,6 +74,27 @@ spec:
- name: dubbo-xds
mountPath: /etc/dubbo/proxy
readOnly: true
+{{- $appPort := 80 }}
+{{- if and (gt (len .Spec.Containers) 0) (gt (len ((index .Spec.Containers 0).Ports)) 0) }}
+{{- $appPort = int ((index ((index .Spec.Containers 0).Ports) 0).ContainerPort) }}
+{{- end }}
+ - name: dubbo-xserver
+ image: {{ .ProxyImage | quote }}
+ imagePullPolicy: IfNotPresent
+ args:
+ - xserver
+ - --listen
+ - :25080
+ - --upstream
+ - 127.0.0.1:{{ $appPort }}
+ ports:
+ - name: xserver
+ containerPort: 25080
+ protocol: TCP
+ volumeMounts:
+ - name: dubbo-xds
+ mountPath: /etc/dubbo/proxy
+ readOnly: true
volumes:
- name: dubbo-xds
secret:
diff --git a/dubboinstaller/charts/dubbod/files/kube-gateway.yaml b/dubboinstaller/charts/dubbod/files/kube-gateway.yaml
index 138247f9..e880d51f 100644
--- a/dubboinstaller/charts/dubbod/files/kube-gateway.yaml
+++ b/dubboinstaller/charts/dubbod/files/kube-gateway.yaml
@@ -63,9 +63,11 @@ spec:
app.kubernetes.io/managed-by: dubbod
gateway.dubbo.apache.org/managed: dubbo.apache.org-gateway-controller
gateway.networking.k8s.io/gateway-name: {{ .Gateway.Name }}
+ proxyless.dubbo.apache.org/inject: "true"
annotations:
dubbo.apache.org/rev: {{ .Revision | quote }}
gateway.dubbo.apache.org/bootstrap-hash: {{ .BootstrapConfigHash | quote }}
+ inject.dubbo.apache.org/templates: grpc-engine
spec:
serviceAccountName: {{ .ServiceAccount }}
containers:
@@ -149,6 +151,7 @@ metadata:
app.kubernetes.io/instance: {{ .DeploymentName }}
app.kubernetes.io/managed-by: dubbod
gateway.networking.k8s.io/gateway-name: {{ .Gateway.Name }}
+ proxyless.dubbo.apache.org/inject: "false"
spec:
type: {{ .ServiceType }}
selector:
diff --git a/dubboinstaller/charts/dubbod/templates/deployment.yaml b/dubboinstaller/charts/dubbod/templates/deployment.yaml
index cb89138b..075c58cd 100644
--- a/dubboinstaller/charts/dubbod/templates/deployment.yaml
+++ b/dubboinstaller/charts/dubbod/templates/deployment.yaml
@@ -40,7 +40,7 @@ spec:
containers:
- name: execute
image: "kdubbo/dubbod:debug"
- imagePullPolicy: Always
+ imagePullPolicy: IfNotPresent
args:
- "execute"
- --domain
diff --git a/dubboinstaller/charts/dubbod/templates/mutatingwebhookconfiguration.yaml b/dubboinstaller/charts/dubbod/templates/mutatingwebhookconfiguration.yaml
index 4da36af4..7d70ffd7 100644
--- a/dubboinstaller/charts/dubbod/templates/mutatingwebhookconfiguration.yaml
+++ b/dubboinstaller/charts/dubbod/templates/mutatingwebhookconfiguration.yaml
@@ -55,6 +55,15 @@ webhooks:
- CREATE
resources:
- pods
+ - apiGroups:
+ - ""
+ apiVersions:
+ - v1
+ operations:
+ - CREATE
+ - UPDATE
+ resources:
+ - services
sideEffects: None
- admissionReviewVersions:
- v1
@@ -92,6 +101,15 @@ webhooks:
- CREATE
resources:
- pods
+ - apiGroups:
+ - ""
+ apiVersions:
+ - v1
+ operations:
+ - CREATE
+ - UPDATE
+ resources:
+ - services
sideEffects: None
- admissionReviewVersions:
- v1
@@ -124,6 +142,15 @@ webhooks:
- CREATE
resources:
- pods
+ - apiGroups:
+ - ""
+ apiVersions:
+ - v1
+ operations:
+ - CREATE
+ - UPDATE
+ resources:
+ - services
sideEffects: None
- admissionReviewVersions:
- v1
@@ -154,4 +181,13 @@ webhooks:
- CREATE
resources:
- pods
- sideEffects: None
\ No newline at end of file
+ - apiGroups:
+ - ""
+ apiVersions:
+ - v1
+ operations:
+ - CREATE
+ - UPDATE
+ resources:
+ - services
+ sideEffects: None
diff --git a/go.mod b/go.mod
index a8db9367..5c97c5f6 100644
--- a/go.mod
+++ b/go.mod
@@ -53,9 +53,9 @@ require (
github.com/hashicorp/go-multierror v1.1.1
github.com/hashicorp/golang-lru/v2 v2.0.5
github.com/heroku/color v0.0.6
- github.com/kdubbo/api v0.0.0-20260430152956-c0ec9648793f
- github.com/kdubbo/client-go v0.0.0-20260322200550-82e70d319918
- github.com/kdubbo/xds-api v0.0.0-20260423094330-4e01ed7e8f82
+ github.com/kdubbo/api v0.0.0-20260513025923-86f2ad63c4ab
+ github.com/kdubbo/client-go v0.0.0-20260506035847-a9460155f65d
+ github.com/kdubbo/xds-api v0.0.0-20260513100730-a07294e322cc
github.com/moby/term v0.5.2
github.com/ory/viper v1.7.5
github.com/pkg/errors v0.9.1
@@ -257,7 +257,3 @@ replace (
github.com/moby/buildkit => github.com/moby/buildkit v0.10.6
github.com/moby/dockerfile => github.com/moby/dockerfile v1.4.1
)
-
-replace github.com/kdubbo/api => ../api
-
-replace github.com/kdubbo/client-go => ../client-go
diff --git a/go.sum b/go.sum
index 0be007d5..e59f6c4d 100644
--- a/go.sum
+++ b/go.sum
@@ -335,8 +335,14 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
-github.com/kdubbo/xds-api v0.0.0-20260423094330-4e01ed7e8f82 h1:pZBmQ8vP6gW1502ANOxx00rUtSsZNdijldFNwFgJSYo=
-github.com/kdubbo/xds-api v0.0.0-20260423094330-4e01ed7e8f82/go.mod h1:o2HDUgL1ntaDbWomZ4cD2tt8jBamuG2qRtjXOa1zZ0Q=
+github.com/kdubbo/api v0.0.0-20260513025923-86f2ad63c4ab h1:HJdyq0CIsepvNZlGW/mWg78FreoVF4E9HXi8dlpXfAQ=
+github.com/kdubbo/api v0.0.0-20260513025923-86f2ad63c4ab/go.mod h1:8BtJiIovg7QCPsCxXcw3gDf922VcvYq5ihOSvj49Rq8=
+github.com/kdubbo/client-go v0.0.0-20260506035847-a9460155f65d h1:YYS3OfwT5mfqIgf17yFqCjio9NGHNIURNh1OvjLCujs=
+github.com/kdubbo/client-go v0.0.0-20260506035847-a9460155f65d/go.mod h1:VoiR0j6MxAcabmLL3dyz5vC3ShxCc3gKUjEcju3QeiE=
+github.com/kdubbo/xds-api v0.0.0-20260513025923-effbe30b748a h1:Quuv4Y8zq6ttf0qEFhwPbDBPTH/jfqKjkHEvkd2/oEE=
+github.com/kdubbo/xds-api v0.0.0-20260513025923-effbe30b748a/go.mod h1:o2HDUgL1ntaDbWomZ4cD2tt8jBamuG2qRtjXOa1zZ0Q=
+github.com/kdubbo/xds-api v0.0.0-20260513100730-a07294e322cc h1:dhfVaVHjt9RmNjeq8qcOWZPLSvRT3BHEbDyUeFiAkJg=
+github.com/kdubbo/xds-api v0.0.0-20260513100730-a07294e322cc/go.mod h1:o2HDUgL1ntaDbWomZ4cD2tt8jBamuG2qRtjXOa1zZ0Q=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
diff --git a/pkg/kube/inject/inject.go b/pkg/kube/inject/inject.go
index 95b9fd59..f77488e5 100644
--- a/pkg/kube/inject/inject.go
+++ b/pkg/kube/inject/inject.go
@@ -115,7 +115,7 @@ func RunTemplate(params InjectionParameters) (mergedPod *corev1.Pod, templatePod
MeshConfig: meshConfig,
Values: params.valuesConfig.asMap,
Revision: params.revision,
- ProxyImage: getProxyImage(params.valuesConfig.asMap, "dubboregistry/dubbo-proxy:0.3.6"),
+ ProxyImage: getProxyImage(params.valuesConfig.asMap, "kdubbo/dubbod:debug"),
CompliancePolicy: common_features.CompliancePolicy,
}
diff --git a/pkg/kube/inject/proxyless.go b/pkg/kube/inject/proxyless.go
index 4add5856..567c83ed 100644
--- a/pkg/kube/inject/proxyless.go
+++ b/pkg/kube/inject/proxyless.go
@@ -35,6 +35,8 @@ const (
ProxylessGRPCConfigEnvName = "DUBBO_GRPC_XDS_CONFIG"
ProxylessGRPCConfigFileName = "dubbo-grpc-xds.json"
ProxylessGRPCConfigPath = ProxylessXDSMountPath + "/" + ProxylessGRPCConfigFileName
+ ProxylessXServerContainerName = "dubbo-xserver"
+ ProxylessXServerPort = 25080
)
var ProxylessInjectTemplatesAnnoName = annotation.OrgApacheDubboInjectTemplates.Name
diff --git a/pkg/kube/inject/proxyless_test.go b/pkg/kube/inject/proxyless_test.go
index 41554a07..3300cce3 100644
--- a/pkg/kube/inject/proxyless_test.go
+++ b/pkg/kube/inject/proxyless_test.go
@@ -83,17 +83,18 @@ func TestInstallerGRPCEngineTemplateInjectsDirectXDSConnection(t *testing.T) {
t.Fatalf("RunTemplate() failed: %v", err)
}
- if len(injectedPod.Spec.Containers) != 1 {
- t.Fatalf("template containers = %d, want 1 hard-coded application container overlay", len(injectedPod.Spec.Containers))
+ if len(injectedPod.Spec.Containers) != 2 {
+ t.Fatalf("template containers = %d, want app overlay plus xserver", len(injectedPod.Spec.Containers))
}
if err := postProcessPod(mergedPod, *injectedPod, req); err != nil {
t.Fatalf("postProcessPod() failed: %v", err)
}
- if len(mergedPod.Spec.Containers) != 1 {
- t.Fatalf("containers = %d, want 1 application container", len(mergedPod.Spec.Containers))
+ if len(mergedPod.Spec.Containers) != 2 {
+ t.Fatalf("containers = %d, want application container plus xserver", len(mergedPod.Spec.Containers))
}
assertDirectXDSConnection(t, mergedPod, "app", ProxylessGRPCSecretNameForMeta(pod.ObjectMeta))
+ assertXServerContainer(t, mergedPod)
}
func TestInstallerGRPCEngineTemplateUsesGenerateNameForDeploymentPods(t *testing.T) {
@@ -152,10 +153,11 @@ func TestInstallerGRPCEngineTemplateUsesGenerateNameForDeploymentPods(t *testing
if err := postProcessPod(mergedPod, *injectedPod, req); err != nil {
t.Fatalf("postProcessPod() failed: %v", err)
}
- if len(mergedPod.Spec.Containers) != 1 {
- t.Fatalf("containers = %d, want original nginx container only", len(mergedPod.Spec.Containers))
+ if len(mergedPod.Spec.Containers) != 2 {
+ t.Fatalf("containers = %d, want original nginx container plus xserver", len(mergedPod.Spec.Containers))
}
assertDirectXDSConnection(t, mergedPod, "nginx", ProxylessGRPCSecretNameForMeta(pod.ObjectMeta))
+ assertXServerContainer(t, mergedPod)
if got := mergedPod.Spec.Volumes[0].Secret.SecretName; got == ProxylessGRPCSecretName("") {
t.Fatalf("secret name = %q, want generateName-based secret", got)
}
@@ -228,6 +230,24 @@ func assertNoArgs(t *testing.T, pod *corev1.Pod) {
}
}
+func assertXServerContainer(t *testing.T, pod *corev1.Pod) {
+ t.Helper()
+ container := FindContainer(ProxylessXServerContainerName, pod.Spec.Containers)
+ if container == nil {
+ t.Fatalf("%s container missing", ProxylessXServerContainerName)
+ }
+ if container.Image != "kdubbo/dubbod:debug" {
+ t.Fatalf("xserver image = %q, want kdubbo/dubbod:debug", container.Image)
+ }
+ wantArgs := []string{"xserver", "--listen", ":25080", "--upstream", "127.0.0.1:80"}
+ if strings.Join(container.Args, ",") != strings.Join(wantArgs, ",") {
+ t.Fatalf("xserver args = %v, want %v", container.Args, wantArgs)
+ }
+ if !hasMount(container.VolumeMounts, ProxylessXDSVolumeName, ProxylessXDSMountPath, true) {
+ t.Fatalf("xserver proxyless xds mount missing")
+ }
+}
+
func TestAddApplicationContainerConfigInjectsProxylessGRPCContract(t *testing.T) {
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
diff --git a/pkg/kube/inject/service_test.go b/pkg/kube/inject/service_test.go
new file mode 100644
index 00000000..52f82f22
--- /dev/null
+++ b/pkg/kube/inject/service_test.go
@@ -0,0 +1,188 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package inject
+
+import (
+ "encoding/json"
+ "strings"
+ "testing"
+
+ "github.com/apache/dubbo-kubernetes/pkg/kube"
+ corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/apimachinery/pkg/types"
+ "k8s.io/apimachinery/pkg/util/intstr"
+)
+
+func TestRewriteProxylessServiceTargetPortsRoutesTCPServiceToXServer(t *testing.T) {
+ svc := &corev1.Service{
+ ObjectMeta: metav1.ObjectMeta{Name: "nginx", Namespace: "app"},
+ Spec: corev1.ServiceSpec{
+ Selector: map[string]string{"app": "nginx"},
+ Ports: []corev1.ServicePort{
+ {Name: "http", Port: 80, TargetPort: intstr.FromInt(80), Protocol: corev1.ProtocolTCP},
+ {Name: "metrics", Port: 9090, TargetPort: intstr.FromString("metrics"), Protocol: corev1.ProtocolTCP},
+ },
+ },
+ }
+
+ if !rewriteProxylessServiceTargetPorts(svc) {
+ t.Fatalf("rewriteProxylessServiceTargetPorts() = false, want true")
+ }
+ for _, port := range svc.Spec.Ports {
+ if got := port.TargetPort.IntVal; got != ProxylessXServerPort {
+ t.Fatalf("port %s targetPort = %d, want %d", port.Name, got, ProxylessXServerPort)
+ }
+ }
+}
+
+func TestRewriteProxylessServiceTargetPortsSkipsUnsafeServices(t *testing.T) {
+ cases := []struct {
+ name string
+ svc corev1.Service
+ }{
+ {
+ name: "selectorless",
+ svc: corev1.Service{
+ ObjectMeta: metav1.ObjectMeta{Name: "manual", Namespace: "app"},
+ Spec: corev1.ServiceSpec{
+ Ports: []corev1.ServicePort{{Name: "http", Port: 80, TargetPort: intstr.FromInt(80)}},
+ },
+ },
+ },
+ {
+ name: "external-name",
+ svc: corev1.Service{
+ ObjectMeta: metav1.ObjectMeta{Name: "external", Namespace: "app"},
+ Spec: corev1.ServiceSpec{
+ Type: corev1.ServiceTypeExternalName,
+ ExternalName: "example.com",
+ Selector: map[string]string{"app": "external"},
+ Ports: []corev1.ServicePort{{Name: "http", Port: 80, TargetPort: intstr.FromInt(80)}},
+ },
+ },
+ },
+ }
+
+ for _, tt := range cases {
+ t.Run(tt.name, func(t *testing.T) {
+ svc := tt.svc
+ if rewriteProxylessServiceTargetPorts(&svc) {
+ t.Fatalf("rewriteProxylessServiceTargetPorts() = true, want false")
+ }
+ if got := svc.Spec.Ports[0].TargetPort.IntVal; got != 80 {
+ t.Fatalf("targetPort = %d, want 80", got)
+ }
+ })
+ }
+}
+
+func TestRewriteProxylessServiceTargetPortsSkipsNonTCPPorts(t *testing.T) {
+ svc := &corev1.Service{
+ ObjectMeta: metav1.ObjectMeta{Name: "dns", Namespace: "app"},
+ Spec: corev1.ServiceSpec{
+ Selector: map[string]string{"app": "dns"},
+ Ports: []corev1.ServicePort{
+ {Name: "dns", Port: 53, TargetPort: intstr.FromInt(53), Protocol: corev1.ProtocolUDP},
+ },
+ },
+ }
+
+ if rewriteProxylessServiceTargetPorts(svc) {
+ t.Fatalf("rewriteProxylessServiceTargetPorts() = true, want false")
+ }
+ if got := svc.Spec.Ports[0].TargetPort.IntVal; got != 53 {
+ t.Fatalf("targetPort = %d, want 53", got)
+ }
+}
+
+func TestInjectServiceCreatesXServerTargetPortPatch(t *testing.T) {
+ svc := corev1.Service{
+ ObjectMeta: metav1.ObjectMeta{Name: "nginx", Namespace: "app"},
+ Spec: corev1.ServiceSpec{
+ Selector: map[string]string{"app": "nginx"},
+ Ports: []corev1.ServicePort{
+ {Name: "http", Port: 80, TargetPort: intstr.FromInt(80), Protocol: corev1.ProtocolTCP},
+ },
+ },
+ }
+ raw, err := json.Marshal(svc)
+ if err != nil {
+ t.Fatalf("json.Marshal() failed: %v", err)
+ }
+
+ wh := &Webhook{Config: &Config{Policy: InjectionPolicyEnabled}}
+ resp := wh.injectService(&kube.AdmissionReview{
+ Request: &kube.AdmissionRequest{
+ UID: types.UID("test"),
+ Kind: metav1.GroupVersionKind{Version: "v1", Kind: "Service"},
+ Namespace: "app",
+ Operation: kube.Create,
+ Object: runtime.RawExtension{Raw: raw},
+ },
+ }, "/inject")
+
+ if !resp.Allowed {
+ t.Fatalf("Allowed = false, want true")
+ }
+ if resp.PatchType == nil || *resp.PatchType != "JSONPatch" {
+ t.Fatalf("PatchType = %v, want JSONPatch", resp.PatchType)
+ }
+ if !strings.Contains(string(resp.Patch), `"value":25080`) {
+ t.Fatalf("patch = %s, want targetPort 25080", string(resp.Patch))
+ }
+}
+
+func TestInjectServiceSkipsProxylessOptOutService(t *testing.T) {
+ svc := corev1.Service{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "dxgate-gateway",
+ Namespace: "app",
+ Labels: map[string]string{
+ "proxyless.dubbo.apache.org/inject": "false",
+ },
+ },
+ Spec: corev1.ServiceSpec{
+ Selector: map[string]string{"app.kubernetes.io/name": "dxgate"},
+ Ports: []corev1.ServicePort{
+ {Name: "http", Port: 80, TargetPort: intstr.FromString("http"), Protocol: corev1.ProtocolTCP},
+ },
+ },
+ }
+ raw, err := json.Marshal(svc)
+ if err != nil {
+ t.Fatalf("json.Marshal() failed: %v", err)
+ }
+
+ wh := &Webhook{Config: &Config{Policy: InjectionPolicyEnabled}}
+ resp := wh.injectService(&kube.AdmissionReview{
+ Request: &kube.AdmissionRequest{
+ UID: types.UID("test"),
+ Kind: metav1.GroupVersionKind{Version: "v1", Kind: "Service"},
+ Namespace: "app",
+ Operation: kube.Create,
+ Object: runtime.RawExtension{Raw: raw},
+ },
+ }, "/inject")
+
+ if !resp.Allowed {
+ t.Fatalf("Allowed = false, want true")
+ }
+ if len(resp.Patch) != 0 {
+ t.Fatalf("patch = %s, want no patch", string(resp.Patch))
+ }
+}
diff --git a/pkg/kube/inject/webhook.go b/pkg/kube/inject/webhook.go
index 4df7f767..760b932b 100644
--- a/pkg/kube/inject/webhook.go
+++ b/pkg/kube/inject/webhook.go
@@ -45,6 +45,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/types"
+ "k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/apimachinery/pkg/util/mergepatch"
"k8s.io/apimachinery/pkg/util/strategicpatch"
"sigs.k8s.io/yaml"
@@ -230,6 +231,20 @@ func (wh *Webhook) serveInject(w http.ResponseWriter, r *http.Request) {
}
func (wh *Webhook) inject(ar *kube.AdmissionReview, path string) *kube.AdmissionResponse {
+ if ar == nil || ar.Request == nil {
+ return toAdmissionResponse(errors.New("admission request is nil"))
+ }
+ switch ar.Request.Kind.Kind {
+ case "Pod":
+ return wh.injectPod(ar, path)
+ case "Service":
+ return wh.injectService(ar, path)
+ default:
+ return &kube.AdmissionResponse{Allowed: true}
+ }
+}
+
+func (wh *Webhook) injectPod(ar *kube.AdmissionReview, path string) *kube.AdmissionResponse {
log := webhookLog.WithLabels("path", path)
req := ar.Request
var pod corev1.Pod
@@ -295,6 +310,55 @@ func (wh *Webhook) inject(ar *kube.AdmissionReview, path string) *kube.Admission
return &reviewResponse
}
+func (wh *Webhook) injectService(ar *kube.AdmissionReview, path string) *kube.AdmissionResponse {
+ log := webhookLog.WithLabels("path", path)
+ req := ar.Request
+ var svc corev1.Service
+ if err := json.Unmarshal(req.Object.Raw, &svc); err != nil {
+ log.Errorf("Could not unmarshal raw service object: %v %s", err, string(req.Object.Raw))
+ return toAdmissionResponse(err)
+ }
+ if svc.Namespace == "" {
+ svc.Namespace = req.Namespace
+ }
+
+ log = log.WithLabels("service", svc.Namespace+"/"+svc.Name)
+ log.Infof("Process proxyless service request")
+
+ wh.mu.RLock()
+ required := injectRequired(IgnoredNamespaces.UnsortedList(), wh.Config, &corev1.PodSpec{}, svc.ObjectMeta)
+ wh.mu.RUnlock()
+ if !required {
+ log.Infof("Skipping service due to policy check")
+ return &kube.AdmissionResponse{Allowed: true}
+ }
+
+ originalService, err := json.Marshal(svc)
+ if err != nil {
+ return toAdmissionResponse(err)
+ }
+ if !rewriteProxylessServiceTargetPorts(&svc) {
+ return &kube.AdmissionResponse{Allowed: true}
+ }
+
+ patchBytes, err := createServicePatch(&svc, originalService)
+ if err != nil {
+ log.Errorf("Service injection failed: %v", err)
+ return toAdmissionResponse(err)
+ }
+
+ log.Infof("Service injection successfully, patch size: %d bytes", len(patchBytes))
+ reviewResponse := kube.AdmissionResponse{
+ Allowed: true,
+ Patch: patchBytes,
+ PatchType: func() *string {
+ pt := "JSONPatch"
+ return &pt
+ }(),
+ }
+ return &reviewResponse
+}
+
func (wh *Webhook) Run(stop <-chan struct{}) {
go wh.watcher.Run(stop)
}
@@ -609,6 +673,7 @@ func reorderPod(pod *corev1.Pod, req InjectionParameters) error {
// Proxy container should be last to ensure `kubectl exec` and similar commands
// continue to default to the user's container
pod.Spec.Containers = modifyContainers(pod.Spec.Containers, ProxyContainerName, MoveLast)
+ pod.Spec.Containers = modifyContainers(pod.Spec.Containers, ProxylessXServerContainerName, MoveLast)
return nil
}
@@ -624,6 +689,38 @@ func createPatch(pod *corev1.Pod, original []byte) ([]byte, error) {
return json.Marshal(p)
}
+func createServicePatch(svc *corev1.Service, original []byte) ([]byte, error) {
+ reinjected, err := json.Marshal(svc)
+ if err != nil {
+ return nil, err
+ }
+ p, err := jsonpatch.CreatePatch(original, reinjected)
+ if err != nil {
+ return nil, err
+ }
+ return json.Marshal(p)
+}
+
+func rewriteProxylessServiceTargetPorts(svc *corev1.Service) bool {
+ if svc.Spec.Type == corev1.ServiceTypeExternalName || len(svc.Spec.Selector) == 0 {
+ return false
+ }
+
+ changed := false
+ for i := range svc.Spec.Ports {
+ port := &svc.Spec.Ports[i]
+ if port.Protocol != "" && port.Protocol != corev1.ProtocolTCP {
+ continue
+ }
+ if port.TargetPort.Type == intstr.Int && port.TargetPort.IntVal == ProxylessXServerPort {
+ continue
+ }
+ port.TargetPort = intstr.FromInt(ProxylessXServerPort)
+ changed = true
+ }
+ return changed
+}
+
func applyOverlayYAML(target *corev1.Pod, overlayYAML []byte) (*corev1.Pod, error) {
currentJSON, err := json.Marshal(target)
if err != nil {
diff --git a/samples/addons/00-namespace.yaml b/samples/addons/00-namespace.yaml
deleted file mode 100644
index 42e674c6..00000000
--- a/samples/addons/00-namespace.yaml
+++ /dev/null
@@ -1,19 +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.
-
-apiVersion: v1
-kind: Namespace
-metadata:
- name: dubbo-system
diff --git a/samples/addons/README.md b/samples/addons/README.md
deleted file mode 100644
index 0fed25f2..00000000
--- a/samples/addons/README.md
+++ /dev/null
@@ -1,38 +0,0 @@
-# Dubbo 可观测性 Addons
-
-这个目录提供开箱即用的 Prometheus 和 Grafana 部署,用于观察 `dubbo-system/dubbod` 的日志信号、监控指标和控制面追踪耗时。
-
-## 安装
-
-```bash
-kubectl apply -f samples/addons
-kubectl -n dubbo-system rollout status deploy/prometheus --timeout=180s
-kubectl -n dubbo-system rollout status deploy/grafana --timeout=180s
-```
-
-## 打开 Grafana
-
-```bash
-kubectl -n dubbo-system port-forward svc/grafana 3000:3000
-```
-
-浏览器打开 `http://127.0.0.1:3000`,进入 `Dubbo / Dubbo control plane Observability` 仪表盘。
-
-## 验证 Prometheus
-
-```bash
-kubectl -n dubbo-system port-forward svc/prometheus 9090:9090
-curl 'http://127.0.0.1:9090/api/v1/query?query=dubbod_uptime_seconds'
-```
-
-## 覆盖内容
-
-- 日志:`dubbod_log_messages_total` 按 `level` 和 `scope` 统计日志速率,原始日志仍通过 `kubectl -n dubbo-system logs deploy/dubbod` 查看。
-- 监控:Prometheus 抓取 `dubbod.dubbo-system.svc:8080/metrics`,Grafana 内置控制面总览面板。
-- 追踪:通过 `dubbod_xds_push_time`、`dubbod_xds_send_time`、`dubbod_proxy_queue_time` 等直方图展示 xDS 推送链路耗时。
-
-## 清理
-
-```bash
-kubectl delete -f samples/addons --ignore-not-found=true
-```
diff --git a/samples/app/README.md b/samples/app/README.md
index 13d3dbc5..8ee4663d 100644
--- a/samples/app/README.md
+++ b/samples/app/README.md
@@ -15,7 +15,7 @@ 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`。`nginx-consumer` 使用 `kdubbo/dubbod` 镜像,会自动启动 `xclient --watch`。
+`dubbo-injection=enabled` 开启后,会自动注入 `grpc-engine`。`xclient` 只主动发起请求,不监听端口;服务端 Pod 会注入 `xserver` 接收 mTLS 并转发到本地 nginx,Service 会被准入层改到 `targetPort: 25080`,样例 NetworkPolicy 会禁止从其他 Pod 直连 nginx `80`。
## 配置流量规则
@@ -46,10 +46,24 @@ nginx.app.svc.cluster.local:80 v1=23 endpoints=1,v2=77 endpoints=1
## 验证流量结果
```bash
-kubectl -n app exec deploy/nginx-consumer -- dubbod xclient 100 | sort | uniq -c
+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
```
-`xclient` 现在是逐请求选路,不再先算好固定数量再批量发送。
+`--print-route` 里应看到 `tlsMode: DUBBO_MUTUAL`,endpoint 端口应为 `25080`。
+
+确认 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/
+```
+
+第一条应输出 `25080`,第二条不能返回 `nginx v1` 或 `nginx v2`。
## 在线更新验证
diff --git a/samples/app/deployment.yaml b/samples/app/deployment.yaml
index 778f8e0e..7ee87728 100644
--- a/samples/app/deployment.yaml
+++ b/samples/app/deployment.yaml
@@ -37,6 +37,22 @@ spec:
targetPort: 80
protocol: TCP
---
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+ name: nginx-proxyless-only
+ namespace: app
+spec:
+ podSelector:
+ matchLabels:
+ app: nginx
+ policyTypes:
+ - Ingress
+ ingress:
+ - ports:
+ - protocol: TCP
+ port: 25080
+---
apiVersion: v1
kind: ConfigMap
metadata:
diff --git a/samples/app/meshservice.yaml b/samples/app/meshservice.yaml
deleted file mode 100644
index 2a24ae88..00000000
--- a/samples/app/meshservice.yaml
+++ /dev/null
@@ -1,41 +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.
-
-apiVersion: networking.dubbo.apache.org/v1alpha3
-kind: MeshService
-metadata:
- name: nginx-routing
- namespace: app
-spec:
- hosts:
- - nginx.app.svc.cluster.local
- visibleTo:
- - "."
- routes:
- - service:
- - name: v1
- host: nginx.app.svc.cluster.local
- labels:
- version: v1
- port:
- number: 80
- weight: 20
- - name: v2
- host: nginx.app.svc.cluster.local
- labels:
- version: v2
- port:
- number: 80
- weight: 80