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

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


The following commit(s) were added to refs/heads/master by this push:
     new 0c953c40 Provide support for virtual machines (#978)
0c953c40 is described below

commit 0c953c407100c9790bb439ba822d3c82e1983997
Author: mfordjody <[email protected]>
AuthorDate: Wed Jul 22 23:22:40 2026 +0800

    Provide support for virtual machines (#978)
    
    * Added probe injection feature
    
    * Fix release workflow action policy (#977)
    
    * Provide support for virtual machines
    
    * fix ci
---
 README.md                                          | 19 ++---
 .../tests/PERFORMANCE.md                           |  0
 dubbod/discovery/pkg/config/kube/crd/conversion.go | 49 ++++++-----
 .../pkg/config/kube/crd/conversion_parse_test.go   | 76 +++++++++++++++++
 .../pkg/config/kube/crd/conversion_test.go         | 43 ++++++++++
 dubbod/discovery/pkg/model/service.go              |  9 ++-
 .../pkg/serviceregistry/serviceentry/controller.go | 39 +++++++--
 .../serviceentry/controller_test.go                | 18 ++++-
 dubbod/discovery/pkg/xds/debug.go                  | 83 +++++++++++++++++++
 dubbod/discovery/pkg/xds/debug_test.go             | 94 ++++++++++++++++++++++
 .../pkg/xds/endpoints/endpoint_builder.go          | 74 ++++++++++++++---
 .../pkg/xds/endpoints/endpoint_builder_test.go     | 49 +++++++++++
 go.mod                                             |  2 +-
 go.sum                                             |  4 +-
 manifests/profiles/observability.yaml              | 39 ---------
 pkg/config/validation/validators.go                | 26 +++++-
 pkg/config/validation/validators_test.go           |  6 +-
 samples/vm/README.md                               | 38 +++++++++
 samples/vm/vm-service.yaml                         | 39 +++++++++
 tests/e2e/run.sh                                   | 43 ++++++++--
 tests/e2e/testdata/vm-workload.yaml                | 36 +++++++++
 21 files changed, 687 insertions(+), 99 deletions(-)

diff --git a/README.md b/README.md
index f75b9413..a02356ea 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# Apache Kdubbo (dubbo-mesh)
+# Apache Kdubbo - dubbo-mesh
 
 [![Go 
Reference](https://pkg.go.dev/badge/github.com/apache/dubbo-kubernetes.svg)](https://pkg.go.dev/github.com/apache/dubbo-kubernetes)
 [![Go Report 
Card](https://goreportcard.com/badge/github.com/apache/dubbo-kubernetes)](https://goreportcard.com/report/github.com/apache/dubbo-kubernetes)
@@ -22,27 +22,22 @@ Dubbo component composition:
 
 Projects are distributed across the code directory repositories:
 
-- [dubbo/api](https://github.com/kdubbo/api). — Defines the component level 
APIs for the Dubbo control plane.
+- [api](https://github.com/kdubbo/api). — Defines the component level APIs for 
the Dubbo control plane.
 
-- [dubbo/xds-api](https://github.com/kdubbo/xds-api). — Define the xDS API for 
the Dubbo control plane.
+- [xds-api](https://github.com/kdubbo/xds-api). — Define the xDS API for the 
Dubbo control plane.
 
-- [dubbo/client-go](https://github.com/kdubbo/client-go). — Defines the 
Kubernetes clients automatically generated for Dubbo control plane resources.
+- [client-go](https://github.com/kdubbo/client-go). — Defines the Kubernetes 
clients automatically generated for Dubbo control plane resources.
 
-- [dubbo/cli](./cli). — Provides dubboctl command line tools for control plane 
management and other operations.
+- [dubboctl](./cli). — Provides dubboctl command line tools for control plane 
management and other operations.
 
-- [dubbo/dubbod](./dubbod) — The main code directory for the Dubbo control 
plane.
+- [dubbod](./dubbod) — The main code directory for the Dubbo control plane.
 
-- [dubbo/operator](./operator). — Provides user friendly options for operating 
the service mesh.
+- [operator](./operator). — Provides user friendly options for operating the 
service mesh.
 
 ## Contributing
 
 Refer to [CONTRIBUTING.md](./CONTRIBUTING.md)
 
-## Performance
-
-See [PERFORMANCE.md](./PERFORMANCE.md) for the control-plane scale matrix,
-repeatable benchmark commands, CI evidence, and cluster load-test methodology.
-
 ## License
 
 Apache License 2.0, see 
[LICENSE](https://github.com/apache/dubbo-kubernetes/blob/master/LICENSE).
diff --git a/PERFORMANCE.md b/architecture/tests/PERFORMANCE.md
similarity index 100%
rename from PERFORMANCE.md
rename to architecture/tests/PERFORMANCE.md
diff --git a/dubbod/discovery/pkg/config/kube/crd/conversion.go 
b/dubbod/discovery/pkg/config/kube/crd/conversion.go
index 749b5f90..a88c529d 100644
--- a/dubbod/discovery/pkg/config/kube/crd/conversion.go
+++ b/dubbod/discovery/pkg/config/kube/crd/conversion.go
@@ -24,7 +24,10 @@ import (
        "reflect"
 
        "github.com/apache/dubbo-kubernetes/pkg/config"
+       "github.com/apache/dubbo-kubernetes/pkg/config/schema/collections"
        "github.com/apache/dubbo-kubernetes/pkg/config/schema/resource"
+       "google.golang.org/protobuf/encoding/protojson"
+       "google.golang.org/protobuf/proto"
 
        kubeyaml "k8s.io/apimachinery/pkg/util/yaml"
 
@@ -56,26 +59,26 @@ func parseInputsImpl(inputs string, withValidate bool) 
([]config.Config, []Dubbo
                        continue
                }
 
-               // gvk := obj.GroupVersionKind()
-               // s, exists := 
collections.DubboGatewayAPI().FindByGroupVersionAliasesKind(resource.FromKubernetesGVK(&gvk))
-               // if !exists {
-               //      log.Debugf("unrecognized type %v", obj.Kind)
-               //      others = append(others, obj)
-               //      continue
-               // }
-               //
-               // cfg, err := ConvertObject(s, &obj, "")
-               // if err != nil {
-               //      return nil, nil, fmt.Errorf("cannot parse proto message 
for %v: %v", obj.Name, err)
-               // }
-               //
-               // if withValidate {
-               //      if _, err := s.ValidateConfig(*cfg); err != nil {
-               //              return nil, nil, fmt.Errorf("configuration is 
invalid: %v", err)
-               //      }
-               // }
-               //
-               // varr = append(varr, *cfg)
+               gvk := obj.GroupVersionKind()
+               s, exists := 
collections.DubboGatewayAPI().FindByGroupVersionAliasesKind(resource.FromKubernetesGVK(&gvk))
+               if !exists {
+                       log.Debugf("unrecognized type %v", obj.Kind)
+                       others = append(others, obj)
+                       continue
+               }
+
+               cfg, err := ConvertObject(s, &obj, "")
+               if err != nil {
+                       return nil, nil, fmt.Errorf("cannot parse proto message 
for %v: %v", obj.Name, err)
+               }
+
+               if withValidate {
+                       if _, err := s.ValidateConfig(*cfg); err != nil {
+                               return nil, nil, fmt.Errorf("configuration is 
invalid: %v", err)
+                       }
+               }
+
+               varr = append(varr, *cfg)
        }
 
        return varr, others, nil
@@ -90,6 +93,12 @@ func FromJSON(s resource.Schema, js string) (config.Spec, 
error) {
        if err != nil {
                return nil, err
        }
+       if pb, ok := c.(proto.Message); ok {
+               if err = (protojson.UnmarshalOptions{DiscardUnknown: 
true}).Unmarshal([]byte(js), pb); err != nil {
+                       return nil, err
+               }
+               return c, nil
+       }
        if err = config.ApplyJSON(c, js); err != nil {
                return nil, err
        }
diff --git a/dubbod/discovery/pkg/config/kube/crd/conversion_parse_test.go 
b/dubbod/discovery/pkg/config/kube/crd/conversion_parse_test.go
new file mode 100644
index 00000000..c4d3b610
--- /dev/null
+++ b/dubbod/discovery/pkg/config/kube/crd/conversion_parse_test.go
@@ -0,0 +1,76 @@
+// 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 crd
+
+import (
+       "testing"
+
+       "github.com/apache/dubbo-kubernetes/pkg/config/schema/gvk"
+       networking "github.com/kdubbo/api/networking/v1alpha3"
+)
+
+func TestParseInputsServiceEntryAndWorkloadEntry(t *testing.T) {
+       configs, others, err := ParseInputs(`
+apiVersion: networking.dubbo.apache.org/v1alpha3
+kind: ServiceEntry
+metadata:
+  name: payment
+  namespace: default
+spec:
+  hosts: [payment.mesh.test]
+  addresses: [240.0.0.20]
+  ports:
+  - name: http
+    number: 8080
+    protocol: HTTP
+  resolution: STATIC
+  workloadSelector:
+    matchLabels:
+      app: payment
+---
+apiVersion: networking.dubbo.apache.org/v1alpha3
+kind: WorkloadEntry
+metadata:
+  name: payment-local
+  namespace: default
+spec:
+  address: 127.0.0.1
+  ports:
+    http: 18080
+  labels:
+    app: payment
+`)
+       if err != nil {
+               t.Fatal(err)
+       }
+       if len(others) != 0 || len(configs) != 2 {
+               t.Fatalf("configs=%d others=%d, want configs=2 others=0", 
len(configs), len(others))
+       }
+       if configs[0].GroupVersionKind != gvk.ServiceEntry {
+               t.Fatalf("first kind = %v, want ServiceEntry", 
configs[0].GroupVersionKind)
+       }
+       service := configs[0].Spec.(*networking.ServiceEntry)
+       if service.GetResolution() != networking.ServiceEntry_STATIC || 
service.GetPorts()[0].GetProtocol() != "HTTP" {
+               t.Fatalf("unexpected ServiceEntry: %#v", service)
+       }
+       if configs[1].GroupVersionKind != gvk.WorkloadEntry {
+               t.Fatalf("second kind = %v, want WorkloadEntry", 
configs[1].GroupVersionKind)
+       }
+       workload := configs[1].Spec.(*networking.WorkloadEntry)
+       if workload.GetAddress() != "127.0.0.1" || workload.GetPorts()["http"] 
!= 18080 {
+               t.Fatalf("unexpected WorkloadEntry: %#v", workload)
+       }
+}
diff --git a/dubbod/discovery/pkg/config/kube/crd/conversion_test.go 
b/dubbod/discovery/pkg/config/kube/crd/conversion_test.go
new file mode 100644
index 00000000..fe45627a
--- /dev/null
+++ b/dubbod/discovery/pkg/config/kube/crd/conversion_test.go
@@ -0,0 +1,43 @@
+// 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 crd
+
+import (
+       "testing"
+
+       "github.com/apache/dubbo-kubernetes/pkg/config/schema/collections"
+       telemetry "github.com/kdubbo/api/telemetry/v1alpha1"
+)
+
+func TestFromJSONTelemetryScalarWrappers(t *testing.T) {
+       spec, err := FromJSON(collections.Telemetry, `{
+               "tracing": [{
+                       "randomSamplingPercentage": 100,
+                       "disableSpanReporting": true
+               }]
+       }`)
+       if err != nil {
+               t.Fatalf("FromJSON() error = %v", err)
+       }
+
+       got := spec.(*telemetry.Telemetry).GetTracing()[0]
+       if got.GetRandomSamplingPercentage().GetValue() != 100 {
+               t.Fatalf("randomSamplingPercentage = %v, want 100", 
got.GetRandomSamplingPercentage().GetValue())
+       }
+       if !got.GetDisableSpanReporting().GetValue() {
+               t.Fatal("disableSpanReporting = false, want true")
+       }
+}
diff --git a/dubbod/discovery/pkg/model/service.go 
b/dubbod/discovery/pkg/model/service.go
index 2914ccfe..33429f89 100644
--- a/dubbod/discovery/pkg/model/service.go
+++ b/dubbod/discovery/pkg/model/service.go
@@ -159,6 +159,9 @@ type DubboEndpoint struct {
        Addresses              []string
        ServicePortName        string
        Labels                 labels.Instance
+       Network                string
+       Locality               string
+       LbWeight               uint32
        HealthStatus           HealthStatus
        SendUnhealthyEndpoints bool
        DiscoverabilityPolicy  EndpointDiscoverabilityPolicy `json:"-"`
@@ -195,7 +198,11 @@ func (ep *DubboEndpoint) Equals(other *DubboEndpoint) bool 
{
        // Check things we can directly compare...
        eq := ep.ServicePortName == other.ServicePortName &&
                ep.ServiceAccount == other.ServiceAccount &&
-               ep.WorkloadName == other.WorkloadName
+               ep.WorkloadName == other.WorkloadName &&
+               ep.Network == other.Network &&
+               ep.Locality == other.Locality &&
+               ep.LbWeight == other.LbWeight &&
+               ep.SendUnhealthyEndpoints == other.SendUnhealthyEndpoints
        if !eq {
                return false
        }
diff --git a/dubbod/discovery/pkg/serviceregistry/serviceentry/controller.go 
b/dubbod/discovery/pkg/serviceregistry/serviceentry/controller.go
index cd454339..2011940c 100644
--- a/dubbod/discovery/pkg/serviceregistry/serviceentry/controller.go
+++ b/dubbod/discovery/pkg/serviceregistry/serviceentry/controller.go
@@ -19,6 +19,7 @@ package serviceentry
 import (
        "sort"
        "strconv"
+       "strings"
        "sync"
 
        "github.com/apache/dubbo-kubernetes/dubbod/discovery/pkg/model"
@@ -33,6 +34,7 @@ import (
        "github.com/apache/dubbo-kubernetes/pkg/config/visibility"
        "github.com/apache/dubbo-kubernetes/pkg/slices"
        "github.com/apache/dubbo-kubernetes/pkg/util/sets"
+       meta "github.com/kdubbo/api/meta/v1alpha1"
        networking "github.com/kdubbo/api/networking/v1alpha3"
 )
 
@@ -204,7 +206,7 @@ func selectWorkloads(namespace string, entry 
*networking.ServiceEntry, configs [
        if selector == nil {
                out := make([]namedWorkload, 0, len(entry.GetEndpoints()))
                for i, workload := range entry.GetEndpoints() {
-                       out = append(out, namedWorkload{name: entryName(i), 
workload: workload})
+                       out = append(out, namedWorkload{name: entryName(i), 
workload: workload, healthStatus: model.Healthy})
                }
                return out
        }
@@ -216,7 +218,7 @@ func selectWorkloads(namespace string, entry 
*networking.ServiceEntry, configs [
                }
                workload, ok := cfg.Spec.(*networking.WorkloadEntry)
                if ok && wanted.Match(labels.Instance(workload.GetLabels())) {
-                       out = append(out, namedWorkload{name: cfg.Name, 
workload: workload})
+                       out = append(out, namedWorkload{name: cfg.Name, 
workload: workload, healthStatus: workloadHealth(cfg.Status)})
                }
        }
        sort.Slice(out, func(i, j int) bool { return out[i].name < out[j].name 
})
@@ -224,8 +226,26 @@ func selectWorkloads(namespace string, entry 
*networking.ServiceEntry, configs [
 }
 
 type namedWorkload struct {
-       name     string
-       workload *networking.WorkloadEntry
+       name         string
+       workload     *networking.WorkloadEntry
+       healthStatus model.HealthStatus
+}
+
+func workloadHealth(status config.Status) model.HealthStatus {
+       dubboStatus, ok := status.(*meta.DubboStatus)
+       if !ok || dubboStatus == nil {
+               return model.Healthy
+       }
+       for _, condition := range dubboStatus.GetConditions() {
+               if condition == nil || (!strings.EqualFold(condition.GetType(), 
"Ready") && !strings.EqualFold(condition.GetType(), "Healthy")) {
+                       continue
+               }
+               if strings.EqualFold(condition.GetStatus(), "True") {
+                       return model.Healthy
+               }
+               return model.UnHealthy
+       }
+       return model.Healthy
 }
 
 func entryName(index int) string {
@@ -234,7 +254,7 @@ func entryName(index int) string {
 
 func buildEndpoints(cfg config.Config, entry *networking.ServiceEntry, svc 
*model.Service, workloads []namedWorkload) []*model.DubboEndpoint {
        if len(workloads) == 0 && (entry.GetResolution() == 
networking.ServiceEntry_DNS || entry.GetResolution() == 
networking.ServiceEntry_DNS_ROUND_ROBIN) {
-               workloads = []namedWorkload{{name: cfg.Name, workload: 
&networking.WorkloadEntry{Address: string(svc.Hostname)}}}
+               workloads = []namedWorkload{{name: cfg.Name, workload: 
&networking.WorkloadEntry{Address: string(svc.Hostname)}, healthStatus: 
model.Healthy}}
        }
        result := make([]*model.DubboEndpoint, 0, 
len(workloads)*len(entry.GetPorts()))
        for _, named := range workloads {
@@ -246,12 +266,19 @@ func buildEndpoints(cfg config.Config, entry 
*networking.ServiceEntry, svc *mode
                        if targetPort == 0 {
                                targetPort = port.GetNumber()
                        }
+                       weight := named.workload.GetWeight()
+                       if weight == 0 {
+                               weight = 1
+                       }
                        result = append(result, &model.DubboEndpoint{
                                ServiceAccount:  
named.workload.GetServiceAccount(),
                                Addresses:       
[]string{named.workload.GetAddress()},
                                ServicePortName: port.GetName(),
                                Labels:          
labels.Instance(named.workload.GetLabels()),
-                               HealthStatus:    model.Healthy,
+                               Network:         named.workload.GetNetwork(),
+                               Locality:        named.workload.GetLocality(),
+                               LbWeight:        weight,
+                               HealthStatus:    named.healthStatus,
                                EndpointPort:    targetPort,
                                WorkloadName:    named.name,
                                Namespace:       cfg.Namespace,
diff --git 
a/dubbod/discovery/pkg/serviceregistry/serviceentry/controller_test.go 
b/dubbod/discovery/pkg/serviceregistry/serviceentry/controller_test.go
index b513c459..59f92ab6 100644
--- a/dubbod/discovery/pkg/serviceregistry/serviceentry/controller_test.go
+++ b/dubbod/discovery/pkg/serviceregistry/serviceentry/controller_test.go
@@ -25,6 +25,7 @@ import (
        "github.com/apache/dubbo-kubernetes/pkg/config"
        "github.com/apache/dubbo-kubernetes/pkg/config/schema/collections"
        "github.com/apache/dubbo-kubernetes/pkg/config/schema/gvk"
+       meta "github.com/kdubbo/api/meta/v1alpha1"
        networking "github.com/kdubbo/api/networking/v1alpha3"
        typev1alpha3 "github.com/kdubbo/api/type/v1alpha3"
 )
@@ -46,8 +47,11 @@ func TestControllerReconcilesServiceEntryAndWorkloadEntry(t 
*testing.T) {
                Address:        "10.0.0.10",
                Ports:          map[string]uint32{"grpc": 16000},
                Labels:         map[string]string{"app": "reviews"},
+               Network:        "vm-network",
+               Locality:       "us-east-1/zone-a/rack-1",
+               Weight:         7,
                ServiceAccount: "reviews",
-       }}
+       }, Status: &meta.DubboStatus{Conditions: []*meta.DubboCondition{{Type: 
"Ready", Status: "False"}}}}
        configs.create(t, workload)
        entry := config.Config{Meta: config.Meta{
                GroupVersionKind: gvk.ServiceEntry,
@@ -80,6 +84,18 @@ func TestControllerReconcilesServiceEntryAndWorkloadEntry(t 
*testing.T) {
        if endpoint.FirstAddressOrNil() != "10.0.0.10" || endpoint.EndpointPort 
!= 16000 || endpoint.ServiceAccount != "reviews" {
                t.Fatalf("unexpected endpoint: address=%s port=%d 
serviceAccount=%s", endpoint.FirstAddressOrNil(), endpoint.EndpointPort, 
endpoint.ServiceAccount)
        }
+       if endpoint.Network != "vm-network" || endpoint.Locality != 
"us-east-1/zone-a/rack-1" || endpoint.LbWeight != 7 {
+               t.Fatalf("unexpected topology: network=%s locality=%s 
weight=%d", endpoint.Network, endpoint.Locality, endpoint.LbWeight)
+       }
+       if endpoint.HealthStatus != model.UnHealthy {
+               t.Fatalf("endpoint health = %v, want unhealthy", 
endpoint.HealthStatus)
+       }
+
+       workload.Status.(*meta.DubboStatus).Conditions[0].Status = "True"
+       configs.update(t, workload)
+       if got := updater.lastEDS(t).endpoints[0].HealthStatus; got != 
model.Healthy {
+               t.Fatalf("updated endpoint health = %v, want healthy", got)
+       }
 
        workload.Spec.(*networking.WorkloadEntry).Address = "10.0.0.11"
        configs.update(t, workload)
diff --git a/dubbod/discovery/pkg/xds/debug.go 
b/dubbod/discovery/pkg/xds/debug.go
index 92fdd77f..d8ede7e3 100644
--- a/dubbod/discovery/pkg/xds/debug.go
+++ b/dubbod/discovery/pkg/xds/debug.go
@@ -23,6 +23,7 @@ import (
        "sort"
        "time"
 
+       "github.com/apache/dubbo-kubernetes/dubbod/discovery/pkg/model"
        "github.com/apache/dubbo-kubernetes/pkg/config/schema/collections"
 )
 
@@ -64,6 +65,7 @@ func (s *DiscoveryServer) AppendDebugHandlers(mux 
*http.ServeMux) {
        register("/debug/syncz", "Synchronization status of all proxies 
connected to this dubbod instance", s.syncz)
        register("/debug/configz", "Configuration resources known to this 
dubbod instance", s.configz)
        register("/debug/registryz", "Services in the service registry", 
s.registryz)
+       register("/debug/endpointz", "Endpoints published through xDS", 
s.endpointz)
 
        mux.HandleFunc("/debug", func(w http.ResponseWriter, req *http.Request) 
{
                w.Header().Set("Content-Type", "application/json")
@@ -158,6 +160,87 @@ func (s *DiscoveryServer) registryz(w http.ResponseWriter, 
_ *http.Request) {
        writeJSON(w, services)
 }
 
+func (s *DiscoveryServer) endpointz(w http.ResponseWriter, _ *http.Request) {
+       w.Header().Set("Content-Type", "application/json")
+       if s.Env == nil || s.Env.EndpointIndex == nil {
+               writeJSON(w, []any{})
+               return
+       }
+       type debugEndpoint struct {
+               Hostname       string `json:"hostname"`
+               Namespace      string `json:"namespace"`
+               Address        string `json:"address"`
+               Port           uint32 `json:"port"`
+               Health         string `json:"health"`
+               Network        string `json:"network,omitempty"`
+               Locality       string `json:"locality,omitempty"`
+               Weight         uint32 `json:"weight"`
+               ServiceAccount string `json:"serviceAccount,omitempty"`
+               Workload       string `json:"workload,omitempty"`
+       }
+       result := make([]debugEndpoint, 0)
+       push := s.Env.PushContext()
+       if push == nil {
+               writeJSON(w, result)
+               return
+       }
+       for _, svc := range push.GetAllServices() {
+               if svc == nil {
+                       continue
+               }
+               shards, found := 
s.Env.EndpointIndex.ShardsForService(string(svc.Hostname), 
svc.Attributes.Namespace)
+               if !found {
+                       continue
+               }
+               shards.RLock()
+               for _, endpoints := range shards.Shards {
+                       for _, ep := range endpoints {
+                               if ep == nil {
+                                       continue
+                               }
+                               result = append(result, debugEndpoint{
+                                       Hostname:       string(svc.Hostname),
+                                       Namespace:      
svc.Attributes.Namespace,
+                                       Address:        ep.FirstAddressOrNil(),
+                                       Port:           ep.EndpointPort,
+                                       Health:         
endpointHealthString(ep.HealthStatus),
+                                       Network:        ep.Network,
+                                       Locality:       ep.Locality,
+                                       Weight:         ep.LbWeight,
+                                       ServiceAccount: ep.ServiceAccount,
+                                       Workload:       ep.WorkloadName,
+                               })
+                       }
+               }
+               shards.RUnlock()
+       }
+       sort.Slice(result, func(i, j int) bool {
+               if result[i].Hostname != result[j].Hostname {
+                       return result[i].Hostname < result[j].Hostname
+               }
+               if result[i].Address != result[j].Address {
+                       return result[i].Address < result[j].Address
+               }
+               return result[i].Port < result[j].Port
+       })
+       writeJSON(w, result)
+}
+
+func endpointHealthString(status model.HealthStatus) string {
+       switch status {
+       case model.Healthy:
+               return "HEALTHY"
+       case model.UnHealthy:
+               return "UNHEALTHY"
+       case model.Draining:
+               return "DRAINING"
+       case model.Terminating:
+               return "TERMINATING"
+       default:
+               return "UNKNOWN"
+       }
+}
+
 func writeJSON(w http.ResponseWriter, obj any) {
        enc := json.NewEncoder(w)
        enc.SetIndent("", "  ")
diff --git a/dubbod/discovery/pkg/xds/debug_test.go 
b/dubbod/discovery/pkg/xds/debug_test.go
new file mode 100644
index 00000000..e9244074
--- /dev/null
+++ b/dubbod/discovery/pkg/xds/debug_test.go
@@ -0,0 +1,94 @@
+// 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 xds
+
+import (
+       "encoding/json"
+       "net/http/httptest"
+       "testing"
+
+       "github.com/apache/dubbo-kubernetes/dubbod/discovery/pkg/model"
+       "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/kube/krt"
+)
+
+func TestEndpointzReportsVMEndpointState(t *testing.T) {
+       hostname := host.Name("reviews-vm.mesh.local")
+       service := &model.Service{
+               Hostname: hostname,
+               Ports: model.PortList{{
+                       Name: "grpc", Port: 50051, Protocol: protocol.GRPC,
+               }},
+               Attributes: model.ServiceAttributes{Name: "reviews-vm", 
Namespace: "bookinfo"},
+       }
+       env := model.NewEnvironment()
+       env.ServiceDiscovery = debugServiceDiscovery{service: service}
+       env.ConfigStore = testConfigStore{}
+       env.Watcher = 
meshwatcher.ConfigAdapter(krt.NewStatic(&meshwatcher.MeshConfigResource{
+               MeshConfig: mesh.DefaultMeshConfig(),
+       }, true))
+       env.Init()
+       push := model.NewPushContext()
+       push.InitContext(env, nil, nil)
+       env.SetPushContext(push)
+       env.EndpointIndex.UpdateServiceEndpoints(model.ShardKey{}, 
string(hostname), "bookinfo", []*model.DubboEndpoint{{
+               Addresses:       []string{"192.0.2.10"},
+               EndpointPort:    50051,
+               ServicePortName: "grpc",
+               HealthStatus:    model.UnHealthy,
+               Network:         "vm-network",
+               Locality:        "us-east-1/zone-a/rack-1",
+               LbWeight:        7,
+               ServiceAccount:  "reviews-vm",
+               WorkloadName:    "reviews-vm",
+       }}, false)
+
+       recorder := httptest.NewRecorder()
+       NewDiscoveryServer(env, nil, nil).endpointz(recorder, 
httptest.NewRequest("GET", "/debug/endpointz", nil))
+       if recorder.Code != 200 {
+               t.Fatalf("status code = %d, want 200", recorder.Code)
+       }
+       var result []map[string]any
+       if err := json.Unmarshal(recorder.Body.Bytes(), &result); err != nil {
+               t.Fatalf("decode endpointz: %v", err)
+       }
+       if len(result) != 1 {
+               t.Fatalf("endpoint count = %d, want 1", len(result))
+       }
+       endpoint := result[0]
+       if endpoint["hostname"] != string(hostname) || endpoint["address"] != 
"192.0.2.10" || endpoint["health"] != "UNHEALTHY" {
+               t.Fatalf("unexpected endpoint: %v", endpoint)
+       }
+       if endpoint["network"] != "vm-network" || endpoint["locality"] != 
"us-east-1/zone-a/rack-1" || endpoint["weight"] != float64(7) {
+               t.Fatalf("unexpected endpoint topology: %v", endpoint)
+       }
+}
+
+type debugServiceDiscovery struct {
+       service *model.Service
+}
+
+func (d debugServiceDiscovery) Services() []*model.Service { return 
[]*model.Service{d.service} }
+func (d debugServiceDiscovery) GetService(name host.Name) *model.Service {
+       if d.service != nil && d.service.Hostname == name {
+               return d.service
+       }
+       return nil
+}
+func (debugServiceDiscovery) GetProxyServiceTargets(*model.Proxy) 
[]model.ServiceTarget { return nil }
diff --git a/dubbod/discovery/pkg/xds/endpoints/endpoint_builder.go 
b/dubbod/discovery/pkg/xds/endpoints/endpoint_builder.go
index 97b41a28..3764d603 100644
--- a/dubbod/discovery/pkg/xds/endpoints/endpoint_builder.go
+++ b/dubbod/discovery/pkg/xds/endpoints/endpoint_builder.go
@@ -18,6 +18,8 @@ package endpoints
 
 import (
        "fmt"
+       "sort"
+       "strings"
 
        "github.com/apache/dubbo-kubernetes/dubbod/discovery/pkg/features"
        "github.com/apache/dubbo-kubernetes/dubbod/discovery/pkg/model"
@@ -35,6 +37,7 @@ import (
        // endpoint "github.com/kdubbo/xds-api/endpoint/v1"
        endpoint "github.com/kdubbo/xds-api/endpoint/v1"
 
+       "google.golang.org/protobuf/types/known/structpb"
        "google.golang.org/protobuf/types/known/wrapperspb"
 )
 
@@ -137,6 +140,12 @@ func (b *EndpointBuilder) 
BuildClusterLoadAssignmentWithGateways(endpointIndex *
        defer shards.RUnlock()
 
        // Filter and convert endpoints
+       type localityEndpoints struct {
+               locality  *core.Locality
+               endpoints []*endpoint.LbEndpoint
+               weight    uint32
+       }
+       localities := make(map[string]*localityEndpoints)
        var lbEndpoints []*endpoint.LbEndpoint
        var filteredCount int
        var totalEndpoints int
@@ -193,6 +202,13 @@ func (b *EndpointBuilder) 
BuildClusterLoadAssignmentWithGateways(endpointIndex *
                                continue
                        }
                        lbEndpoints = append(lbEndpoints, lbEp)
+                       group := localities[ep.Locality]
+                       if group == nil {
+                               group = &localityEndpoints{locality: 
parseLocality(ep.Locality)}
+                               localities[ep.Locality] = group
+                       }
+                       group.endpoints = append(group.endpoints, lbEp)
+                       group.weight += lbEp.GetLoadBalancingWeight().GetValue()
                }
        }
 
@@ -208,7 +224,6 @@ func (b *EndpointBuilder) 
BuildClusterLoadAssignmentWithGateways(endpointIndex *
                return buildEmptyClusterLoadAssignment(b.clusterName)
        }
 
-       // Create LocalityLbEndpoints with empty locality (default)
        healthyCount := 0
        unhealthyInLbCount := 0
        drainingCount := 0
@@ -225,14 +240,19 @@ func (b *EndpointBuilder) 
BuildClusterLoadAssignmentWithGateways(endpointIndex *
        log.Infof("cluster %s [hostname=%s, port=%d] - total endpoints: %d 
[healthy=%d, unhealthy=%d, draining=%d, SupportsUnhealthyEndpoints=%v]",
                b.clusterName, b.hostname, b.port, len(lbEndpoints), 
healthyCount, unhealthyInLbCount, drainingCount, 
b.service.SupportsUnhealthyEndpoints())
 
-       localityLbEndpoints := []*endpoint.LocalityLbEndpoints{
-               {
-                       Locality:    &core.Locality{},
-                       LbEndpoints: lbEndpoints,
-                       LoadBalancingWeight: &wrapperspb.UInt32Value{
-                               Value: uint32(len(lbEndpoints)),
-                       },
-               },
+       localityNames := make([]string, 0, len(localities))
+       for name := range localities {
+               localityNames = append(localityNames, name)
+       }
+       sort.Strings(localityNames)
+       localityLbEndpoints := make([]*endpoint.LocalityLbEndpoints, 0, 
len(localityNames))
+       for _, name := range localityNames {
+               group := localities[name]
+               localityLbEndpoints = append(localityLbEndpoints, 
&endpoint.LocalityLbEndpoints{
+                       Locality:            group.locality,
+                       LbEndpoints:         group.endpoints,
+                       LoadBalancingWeight: wrapperspb.UInt32(group.weight),
+               })
        }
 
        return &endpoint.ClusterLoadAssignment{
@@ -241,6 +261,21 @@ func (b *EndpointBuilder) 
BuildClusterLoadAssignmentWithGateways(endpointIndex *
        }
 }
 
+func parseLocality(value string) *core.Locality {
+       parts := strings.SplitN(value, "/", 3)
+       locality := &core.Locality{}
+       if len(parts) > 0 {
+               locality.Region = parts[0]
+       }
+       if len(parts) > 1 {
+               locality.Zone = parts[1]
+       }
+       if len(parts) > 2 {
+               locality.SubZone = parts[2]
+       }
+       return locality
+}
+
 func (b *EndpointBuilder) servicePort(port int) *model.Port {
        if !b.ServiceFound() {
                return nil
@@ -295,6 +330,10 @@ func (b *EndpointBuilder) buildLbEndpointForCluster(ep 
*model.DubboEndpoint, end
                healthStatus = core.HealthStatus_UNHEALTHY
        }
 
+       weight := ep.LbWeight
+       if weight == 0 {
+               weight = 1
+       }
        return &endpoint.LbEndpoint{
                HostIdentifier: &endpoint.LbEndpoint_Endpoint{
                        Endpoint: &endpoint.Endpoint{
@@ -302,12 +341,27 @@ func (b *EndpointBuilder) buildLbEndpointForCluster(ep 
*model.DubboEndpoint, end
                        },
                },
                HealthStatus: healthStatus,
+               Metadata:     buildEndpointMetadata(ep),
                LoadBalancingWeight: &wrapperspb.UInt32Value{
-                       Value: 1,
+                       Value: weight,
                },
        }
 }
 
+func buildEndpointMetadata(ep *model.DubboEndpoint) *core.Metadata {
+       if ep.Network == "" && ep.Locality == "" {
+               return nil
+       }
+       return &core.Metadata{FilterMetadata: map[string]*structpb.Struct{
+               "networking.dubbo.apache.org": {
+                       Fields: map[string]*structpb.Value{
+                               "network":  structpb.NewStringValue(ep.Network),
+                               "locality": 
structpb.NewStringValue(ep.Locality),
+                       },
+               },
+       }}
+}
+
 func (b *EndpointBuilder) eastWestGatewayForCluster(endpointCluster 
cluster.ID, gateways map[cluster.ID]multicluster.EastWestGateway) 
(multicluster.EastWestGateway, bool) {
        if endpointCluster == "" || len(gateways) == 0 {
                return multicluster.EastWestGateway{}, false
diff --git a/dubbod/discovery/pkg/xds/endpoints/endpoint_builder_test.go 
b/dubbod/discovery/pkg/xds/endpoints/endpoint_builder_test.go
index 06d38160..cf79c637 100644
--- a/dubbod/discovery/pkg/xds/endpoints/endpoint_builder_test.go
+++ b/dubbod/discovery/pkg/xds/endpoints/endpoint_builder_test.go
@@ -129,6 +129,55 @@ func 
TestBuildClusterLoadAssignmentKeepsLocalShardPodIPWhenGatewayConfigured(t *
        }
 }
 
+func TestBuildClusterLoadAssignmentPreservesVMLocalityHealthAndWeight(t 
*testing.T) {
+       hostname := host.Name("reviews.mesh.local")
+       svc := newEndpointTestService("reviews", "bookinfo", string(hostname), 
50051)
+       push := newEndpointTestPushContext(t, nil, []*model.Service{svc})
+       index := model.NewEndpointIndex(model.DisabledCache{})
+       index.UpdateServiceEndpoints(model.ShardKey{}, string(hostname), 
"bookinfo", []*model.DubboEndpoint{
+               {
+                       Addresses:       []string{"10.0.0.10"},
+                       EndpointPort:    50051,
+                       ServicePortName: "http",
+                       Locality:        "us-east-1/zone-a/rack-1",
+                       LbWeight:        7,
+                       HealthStatus:    model.Healthy,
+               },
+               {
+                       Addresses:       []string{"10.0.0.11"},
+                       EndpointPort:    50051,
+                       ServicePortName: "http",
+                       Locality:        "us-west-2/zone-b",
+                       LbWeight:        3,
+                       HealthStatus:    model.UnHealthy,
+               },
+       }, false)
+
+       clusterName := model.BuildSubsetKey(model.TrafficDirectionOutbound, "", 
hostname, 50051)
+       cla := NewEndpointBuilder(clusterName, newEndpointTestProxy(), 
push).BuildClusterLoadAssignment(index)
+       localities := cla.GetEndpoints()
+       if len(localities) != 2 {
+               t.Fatalf("localities = %d, want 2", len(localities))
+       }
+       if got := localities[0].GetLocality(); got.GetRegion() != "us-east-1" 
|| got.GetZone() != "zone-a" || got.GetSubZone() != "rack-1" {
+               t.Fatalf("first locality = %v", got)
+       }
+       if got := localities[0].GetLoadBalancingWeight().GetValue(); got != 7 {
+               t.Fatalf("first locality weight = %d, want 7", got)
+       }
+       first := localities[0].GetLbEndpoints()[0]
+       metadata := 
first.GetMetadata().GetFilterMetadata()["networking.dubbo.apache.org"]
+       if got := metadata.GetFields()["locality"].GetStringValue(); got != 
"us-east-1/zone-a/rack-1" {
+               t.Fatalf("first endpoint locality metadata = %q", got)
+       }
+       if got := first.GetLoadBalancingWeight().GetValue(); got != 7 {
+               t.Fatalf("first endpoint weight = %d, want 7", got)
+       }
+       if got := localities[1].GetLbEndpoints()[0].GetHealthStatus(); 
got.String() != "UNHEALTHY" {
+               t.Fatalf("second endpoint health = %v, want UNHEALTHY", got)
+       }
+}
+
 func firstEndpointAddress(t *testing.T, cla *endpoint.ClusterLoadAssignment) 
string {
        t.Helper()
        localities := cla.GetEndpoints()
diff --git a/go.mod b/go.mod
index ccf5efc6..d4fc22e0 100644
--- a/go.mod
+++ b/go.mod
@@ -54,7 +54,7 @@ require (
        github.com/heroku/color v0.0.6
        github.com/kdubbo/api v0.0.0-20260713105721-558a18fe4f33
        github.com/kdubbo/client-go v0.0.0-20260713105923-9a87feab8d96
-       github.com/kdubbo/xds-api v0.0.0-20260628124158-b93008db9162
+       github.com/kdubbo/xds-api v0.0.0-20260722141036-e1ddf72d7580
        github.com/moby/moby/client v0.4.1
        github.com/moby/term v0.5.2
        github.com/ory/viper v1.7.5
diff --git a/go.sum b/go.sum
index f43e6713..eafa9299 100644
--- a/go.sum
+++ b/go.sum
@@ -372,8 +372,8 @@ github.com/kdubbo/api v0.0.0-20260713105721-558a18fe4f33 
h1://TxoBFL/igcCqokYHOc
 github.com/kdubbo/api v0.0.0-20260713105721-558a18fe4f33/go.mod 
h1:8BtJiIovg7QCPsCxXcw3gDf922VcvYq5ihOSvj49Rq8=
 github.com/kdubbo/client-go v0.0.0-20260713105923-9a87feab8d96 
h1:co2FdpetF4dIozVHMatAd2yYPtKBqdpCgSbpdnXEkKw=
 github.com/kdubbo/client-go v0.0.0-20260713105923-9a87feab8d96/go.mod 
h1:NUl5DTlsovJaoYDj0SyHi263H+1Z11EXP7qnk6TPKjo=
-github.com/kdubbo/xds-api v0.0.0-20260628124158-b93008db9162 
h1:8T6FM/7KYiX84f8wMNmWxZ4YMv+uONCkZqTKiM6m/M8=
-github.com/kdubbo/xds-api v0.0.0-20260628124158-b93008db9162/go.mod 
h1:o2HDUgL1ntaDbWomZ4cD2tt8jBamuG2qRtjXOa1zZ0Q=
+github.com/kdubbo/xds-api v0.0.0-20260722141036-e1ddf72d7580 
h1:pzUsk+g0CqS+gyBv11LEz1nlJUyfnWNVpGj7uiwG/4o=
+github.com/kdubbo/xds-api v0.0.0-20260722141036-e1ddf72d7580/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/manifests/profiles/observability.yaml 
b/manifests/profiles/observability.yaml
deleted file mode 100644
index 64526b8e..00000000
--- a/manifests/profiles/observability.yaml
+++ /dev/null
@@ -1,39 +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.
-
-# The observability profile installs only the observability addons
-# (Prometheus, Grafana, tracing, OpenTelemetry collector) on top of an
-# existing control plane installation.
-apiVersion: install.dubbo.apache.org/v1alpha1
-kind: DubboOperator
-metadata:
-  namespace: dubbo-system
-spec:
-  profile: observability
-  components:
-    base:
-      enabled: false
-    dubbod:
-      enabled: false
-    prometheus:
-      enabled: true
-    grafana:
-      enabled: true
-    tracing:
-      enabled: true
-    otelCollector:
-      enabled: true
-  values:
-    global: {}
diff --git a/pkg/config/validation/validators.go 
b/pkg/config/validation/validators.go
index 0b4fa91f..7d33922f 100644
--- a/pkg/config/validation/validators.go
+++ b/pkg/config/validation/validators.go
@@ -315,7 +315,7 @@ func validateWorkloadEntry(field string, workload 
*networking.WorkloadEntry) err
                return fmt.Errorf("%s must not be null", field)
        }
        var errs error
-       if err := validateServiceEntryHost(field+".address", 
workload.GetAddress()); err != nil {
+       if err := validateWorkloadAddress(field+".address", 
workload.GetAddress()); err != nil {
                errs = AppendErrors(errs, err)
        }
        for name, port := range workload.GetPorts() {
@@ -329,9 +329,33 @@ func validateWorkloadEntry(field string, workload 
*networking.WorkloadEntry) err
        if err := labels.Instance(workload.GetLabels()).Validate(); err != nil {
                errs = AppendErrors(errs, fmt.Errorf("%s.labels: %v", field, 
err))
        }
+       if locality := workload.GetLocality(); locality != "" {
+               parts := strings.Split(locality, "/")
+               if len(parts) > 3 {
+                       errs = AppendErrors(errs, fmt.Errorf("%s.locality %q 
must use region/zone/subzone form", field, locality))
+               }
+               for _, part := range parts {
+                       if part == "" {
+                               errs = AppendErrors(errs, 
fmt.Errorf("%s.locality %q must not contain empty segments", field, locality))
+                               break
+                       }
+               }
+       }
+       if serviceAccount := workload.GetServiceAccount(); serviceAccount != "" 
{
+               if messages := kvalidation.IsDNS1123Subdomain(serviceAccount); 
len(messages) > 0 {
+                       errs = AppendErrors(errs, fmt.Errorf("%s.serviceAccount 
%q is invalid: %v", field, serviceAccount, messages))
+               }
+       }
        return errs
 }
 
+func validateWorkloadAddress(field, value string) error {
+       if strings.Contains(value, "*") {
+               return fmt.Errorf("%s %q must not contain a wildcard", field, 
value)
+       }
+       return validateServiceEntryHost(field, value)
+}
+
 // ValidateTelemetry checks that a Telemetry resource is well-formed.
 var ValidateTelemetry = RegisterValidateFunc("ValidateTelemetry",
        func(cfg config.Config) (Warning, error) {
diff --git a/pkg/config/validation/validators_test.go 
b/pkg/config/validation/validators_test.go
index c6276608..b661f062 100644
--- a/pkg/config/validation/validators_test.go
+++ b/pkg/config/validation/validators_test.go
@@ -342,10 +342,14 @@ func TestValidateWorkloadEntry(t *testing.T) {
                wantErr bool
        }{
                {name: "valid IP", spec: &networking.WorkloadEntry{Address: 
"10.0.0.1", Ports: map[string]uint32{"grpc": 50051}}, wantErr: false},
-               {name: "valid DNS", spec: &networking.WorkloadEntry{Address: 
"vm.example.com"}, wantErr: false},
+               {name: "valid DNS", spec: &networking.WorkloadEntry{Address: 
"vm.example.com", Locality: "us-east-1/zone-a/rack-1", ServiceAccount: 
"reviews-vm"}, wantErr: false},
                {name: "missing address", spec: &networking.WorkloadEntry{}, 
wantErr: true},
+               {name: "wildcard address", spec: 
&networking.WorkloadEntry{Address: "*.example.com"}, wantErr: true},
                {name: "invalid port", spec: &networking.WorkloadEntry{Address: 
"10.0.0.1", Ports: map[string]uint32{"grpc": 0}}, wantErr: true},
                {name: "invalid labels", spec: 
&networking.WorkloadEntry{Address: "10.0.0.1", Labels: map[string]string{"bad 
key": "value"}}, wantErr: true},
+               {name: "invalid locality empty segment", spec: 
&networking.WorkloadEntry{Address: "10.0.0.1", Locality: "us-east-1//rack-1"}, 
wantErr: true},
+               {name: "invalid locality depth", spec: 
&networking.WorkloadEntry{Address: "10.0.0.1", Locality: 
"region/zone/subzone/extra"}, wantErr: true},
+               {name: "invalid service account", spec: 
&networking.WorkloadEntry{Address: "10.0.0.1", ServiceAccount: "Bad_Account"}, 
wantErr: true},
        }
        for _, tc := range cases {
                t.Run(tc.name, func(t *testing.T) {
diff --git a/samples/vm/README.md b/samples/vm/README.md
new file mode 100644
index 00000000..bad31d33
--- /dev/null
+++ b/samples/vm/README.md
@@ -0,0 +1,38 @@
+# Virtual machine workloads
+
+`WorkloadEntry` registers a VM, bare-metal process, or other non-Kubernetes 
workload as a mesh endpoint. `ServiceEntry` gives the selected workloads a 
stable service hostname.
+
+## Register a workload
+
+Set `spec.address` in `vm-service.yaml` to an address reachable from mesh 
clients, then apply it:
+
+```console
+kubectl apply -f samples/vm/vm-service.yaml
+```
+
+The workload port overrides the matching service port by name. `network`, 
`locality`, `weight`, and `serviceAccount` are propagated with the endpoint. A 
missing weight defaults to `1`.
+
+## Publish health
+
+VM health agents or external monitors can update the standard `Ready` or 
`Healthy` condition. A false or unknown condition publishes the endpoint as 
unhealthy through xDS; no condition preserves the backward-compatible healthy 
default.
+
+```console
+kubectl patch workloadentry payment-vm-01 --subresource=status --type=merge \
+  -p 
'{"status":{"conditions":[{"type":"Ready","status":"False","reason":"HealthCheckFailed"}]}}'
+```
+
+Restore the endpoint after recovery:
+
+```console
+kubectl patch workloadentry payment-vm-01 --subresource=status --type=merge \
+  -p 
'{"status":{"conditions":[{"type":"Ready","status":"True","reason":"HealthCheckPassed"}]}}'
+```
+
+Inspect the state published by the control plane:
+
+```console
+kubectl -n dubbo-system port-forward deploy/dubbod 18080:8080
+curl http://127.0.0.1:18080/debug/endpointz
+```
+
+The control plane manages discovery and routing. The VM process, network 
reachability, workload certificate provisioning, and the component that reports 
its health remain deployment responsibilities.
diff --git a/samples/vm/vm-service.yaml b/samples/vm/vm-service.yaml
new file mode 100644
index 00000000..32bf3074
--- /dev/null
+++ b/samples/vm/vm-service.yaml
@@ -0,0 +1,39 @@
+# 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.
+
+apiVersion: networking.dubbo.apache.org/v1alpha3
+kind: WorkloadEntry
+metadata:
+  name: payment-vm-01
+  namespace: default
+spec:
+  address: 192.0.2.10
+  ports:
+    grpc: 50051
+  labels:
+    app: payment
+    version: v1
+  network: datacenter-1
+  locality: us-east-1/zone-a/rack-1
+  weight: 1
+  serviceAccount: payment
+---
+apiVersion: networking.dubbo.apache.org/v1alpha3
+kind: ServiceEntry
+metadata:
+  name: payment
+  namespace: default
+spec:
+  hosts:
+    - payment.mesh.local
+  location: MESH_INTERNAL
+  resolution: STATIC
+  ports:
+    - number: 50051
+      name: grpc
+      protocol: GRPC
+  workloadSelector:
+    matchLabels:
+      app: payment
diff --git a/tests/e2e/run.sh b/tests/e2e/run.sh
index 8029c309..672b9571 100755
--- a/tests/e2e/run.sh
+++ b/tests/e2e/run.sh
@@ -29,6 +29,8 @@
 #   UPGRADE_FROM_IMAGE   image expected by the previous chart (default: 
kdubbo/dubbod:debug)
 #   SKIP_BUILD      set to 1 to reuse an already-built ${IMAGE}
 #   KEEP_CLUSTER    set to 1 to keep the kind cluster after the run
+#   KIND            path to the kind binary      (default: kind)
+#   KIND_NODE_IMAGE kind node image override     (default: kind release 
default)
 
 set -euo pipefail
 
@@ -44,6 +46,8 @@ APP_NS="e2e"
 KUBECTL=(kubectl --context "kind-${CLUSTER_NAME}")
 UPGRADE_TMP_DIR=""
 PREVIOUS_CHART=""
+KIND="${KIND:-kind}"
+KIND_NODE_IMAGE="${KIND_NODE_IMAGE:-}"
 
 log() { echo "--- $*"; }
 
@@ -59,7 +63,7 @@ fail() {
 cleanup() {
   if [[ -n "${PF_PID:-}" ]]; then kill "${PF_PID}" 2>/dev/null || true; fi
   if [[ "${KEEP_CLUSTER:-0}" != "1" ]]; then
-    kind delete cluster --name "${CLUSTER_NAME}" || true
+    "${KIND}" delete cluster --name "${CLUSTER_NAME}" || true
   fi
   if [[ -n "${UPGRADE_TMP_DIR}" && "${UPGRADE_TMP_DIR}" == */dubbo-upgrade.* 
]]; then
     rm -rf -- "${UPGRADE_TMP_DIR}"
@@ -106,15 +110,19 @@ if [[ "${IMAGE}" != "${UPGRADE_FROM_IMAGE}" ]]; then
   docker tag "${IMAGE}" "${UPGRADE_FROM_IMAGE}"
 fi
 
-if ! kind get clusters 2>/dev/null | grep -qx "${CLUSTER_NAME}"; then
+if ! "${KIND}" get clusters 2>/dev/null | grep -qx "${CLUSTER_NAME}"; then
   log "creating kind cluster ${CLUSTER_NAME}"
-  kind create cluster --name "${CLUSTER_NAME}" --wait 120s
+  KIND_CREATE_ARGS=(create cluster --name "${CLUSTER_NAME}" --wait 120s)
+  if [[ -n "${KIND_NODE_IMAGE}" ]]; then
+    KIND_CREATE_ARGS+=(--image "${KIND_NODE_IMAGE}")
+  fi
+  "${KIND}" "${KIND_CREATE_ARGS[@]}"
 fi
 
 log "loading ${IMAGE} into kind"
-kind load docker-image "${IMAGE}" --name "${CLUSTER_NAME}"
+"${KIND}" load docker-image "${IMAGE}" --name "${CLUSTER_NAME}"
 if [[ "${IMAGE}" != "${UPGRADE_FROM_IMAGE}" ]]; then
-  kind load docker-image "${UPGRADE_FROM_IMAGE}" --name "${CLUSTER_NAME}"
+  "${KIND}" load docker-image "${UPGRADE_FROM_IMAGE}" --name "${CLUSTER_NAME}"
 fi
 
 log "installing Gateway API CRDs"
@@ -184,6 +192,10 @@ log "applying ServiceEntry through the validating webhook"
 "${KUBECTL[@]}" -n "${APP_NS}" apply -f 
"${ROOT}/tests/e2e/testdata/serviceentry.yaml" \
   || fail "valid ServiceEntry was rejected"
 
+log "registering a VM workload through WorkloadEntry"
+"${KUBECTL[@]}" -n "${APP_NS}" apply -f 
"${ROOT}/tests/e2e/testdata/vm-workload.yaml" \
+  || fail "valid VM WorkloadEntry was rejected"
+
 log "port-forwarding dubbod monitoring port"
 "${KUBECTL[@]}" -n "${SYSTEM_NS}" port-forward deploy/dubbod 18080:8080 
>/dev/null 2>&1 &
 PF_PID=$!
@@ -202,6 +214,12 @@ retry() {
 
 check_registry_service() { probe /debug/registryz | grep -q 
"httpbin.${APP_NS}.svc"; }
 check_registry_serviceentry() { probe /debug/registryz | grep -q 
"external.example.com"; }
+check_registry_vm() { probe /debug/registryz | grep -q 
"reviews-vm.mesh.local"; }
+check_vm_health() {
+  local health="$1"
+  probe /debug/endpointz | tr -d '\n ' \
+    | grep -q 
"\"hostname\":\"reviews-vm.mesh.local\".*\"address\":\"192.0.2.10\".*\"health\":\"${health}\".*\"network\":\"vm-network\".*\"locality\":\"us-east-1/zone-a/rack-1\".*\"weight\":7"
+}
 check_metrics() { probe /metrics | grep -q "^dubbod_"; }
 
 retry "monitoring endpoint up" probe /version
@@ -211,5 +229,20 @@ log "asserting httpbin service is in the registry"
 retry "httpbin in /debug/registryz" check_registry_service
 log "asserting ServiceEntry host is in the registry"
 retry "ServiceEntry in /debug/registryz" check_registry_serviceentry
+log "asserting VM service and endpoint topology are published"
+retry "VM ServiceEntry in /debug/registryz" check_registry_vm
+retry "healthy VM endpoint in /debug/endpointz" check_vm_health HEALTHY
+
+log "marking the VM endpoint unhealthy through the status subresource"
+"${KUBECTL[@]}" -n "${APP_NS}" patch workloadentry reviews-vm 
--subresource=status --type=merge \
+  -p 
'{"status":{"conditions":[{"type":"Ready","status":"False","reason":"E2EHealthCheck"}]}}'
 \
+  || fail "could not update VM health status"
+retry "unhealthy VM endpoint in /debug/endpointz" check_vm_health UNHEALTHY
+
+log "restoring the VM endpoint health"
+"${KUBECTL[@]}" -n "${APP_NS}" patch workloadentry reviews-vm 
--subresource=status --type=merge \
+  -p 
'{"status":{"conditions":[{"type":"Ready","status":"True","reason":"E2EHealthCheck"}]}}'
 \
+  || fail "could not restore VM health status"
+retry "recovered VM endpoint in /debug/endpointz" check_vm_health HEALTHY
 
 log "e2e smoke test passed"
diff --git a/tests/e2e/testdata/vm-workload.yaml 
b/tests/e2e/testdata/vm-workload.yaml
new file mode 100644
index 00000000..c3a26c5c
--- /dev/null
+++ b/tests/e2e/testdata/vm-workload.yaml
@@ -0,0 +1,36 @@
+# 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.
+
+apiVersion: networking.dubbo.apache.org/v1alpha3
+kind: WorkloadEntry
+metadata:
+  name: reviews-vm
+spec:
+  address: 192.0.2.10
+  ports:
+    grpc: 50051
+  labels:
+    app: reviews-vm
+  network: vm-network
+  locality: us-east-1/zone-a/rack-1
+  weight: 7
+  serviceAccount: reviews-vm
+---
+apiVersion: networking.dubbo.apache.org/v1alpha3
+kind: ServiceEntry
+metadata:
+  name: reviews-vm
+spec:
+  hosts:
+    - reviews-vm.mesh.local
+  location: MESH_INTERNAL
+  resolution: STATIC
+  ports:
+    - number: 50051
+      name: grpc
+      protocol: GRPC
+  workloadSelector:
+    matchLabels:
+      app: reviews-vm

Reply via email to