diff --git a/registry/nacos/v2/watcher.go b/registry/nacos/v2/watcher.go index f80df2703..1d3363408 100644 --- a/registry/nacos/v2/watcher.go +++ b/registry/nacos/v2/watcher.go @@ -17,6 +17,7 @@ package v2 import ( "errors" "fmt" + "math" "net" "strconv" "strings" @@ -556,10 +557,19 @@ func (w *watcher) generateServiceEntry(host string, services []model.Instance) * if !isValidIP(service.Ip) { isDnsService = true } + // Calculate weight from Nacos instance + // Nacos weight is float64, need to convert to uint32 for Istio + // Use math.Round to preserve fractional weights (e.g., 0.5, 1.5) + // If weight is 0 or negative, use default weight 1 + weight := uint32(1) + if service.Weight > 0 { + weight = uint32(math.Round(service.Weight)) + } endpoint := &v1alpha3.WorkloadEntry{ Address: service.Ip, Ports: map[string]uint32{port.Protocol: port.Number}, Labels: service.Metadata, + Weight: weight, } endpoints = append(endpoints, endpoint) } diff --git a/registry/nacos/v2/watcher_test.go b/registry/nacos/v2/watcher_test.go new file mode 100644 index 000000000..7da7ac0c8 --- /dev/null +++ b/registry/nacos/v2/watcher_test.go @@ -0,0 +1,200 @@ +// Copyright (c) 2022 Alibaba Group Holding Ltd. +// +// Licensed 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 v2 + +import ( + "testing" + + "github.com/nacos-group/nacos-sdk-go/v2/model" + "istio.io/api/networking/v1alpha3" +) + +func Test_generateServiceEntry_Weight(t *testing.T) { + w := &watcher{} + + testCases := []struct { + name string + services []model.Instance + expectedWeights []uint32 + description string + }{ + { + name: "normal integer weights", + services: []model.Instance{ + {Ip: "192.168.1.1", Port: 8080, Weight: 5.0}, + {Ip: "192.168.1.2", Port: 8080, Weight: 3.0}, + {Ip: "192.168.1.3", Port: 8080, Weight: 2.0}, + }, + expectedWeights: []uint32{5, 3, 2}, + description: "Integer weights should be converted correctly", + }, + { + name: "fractional weights with rounding", + services: []model.Instance{ + {Ip: "192.168.1.1", Port: 8080, Weight: 5.4}, + {Ip: "192.168.1.2", Port: 8080, Weight: 3.5}, + {Ip: "192.168.1.3", Port: 8080, Weight: 2.6}, + }, + expectedWeights: []uint32{5, 4, 3}, + description: "Fractional weights should be rounded to nearest integer", + }, + { + name: "zero weight defaults to 1", + services: []model.Instance{ + {Ip: "192.168.1.1", Port: 8080, Weight: 0.0}, + {Ip: "192.168.1.2", Port: 8080, Weight: 5.0}, + }, + expectedWeights: []uint32{1, 5}, + description: "Zero weight should default to 1", + }, + { + name: "negative weight defaults to 1", + services: []model.Instance{ + {Ip: "192.168.1.1", Port: 8080, Weight: -1.0}, + {Ip: "192.168.1.2", Port: 8080, Weight: 3.0}, + }, + expectedWeights: []uint32{1, 3}, + description: "Negative weight should default to 1", + }, + { + name: "very small fractional weight rounds to 0 then defaults to 1", + services: []model.Instance{ + {Ip: "192.168.1.1", Port: 8080, Weight: 0.4}, + {Ip: "192.168.1.2", Port: 8080, Weight: 0.5}, + {Ip: "192.168.1.3", Port: 8080, Weight: 0.6}, + }, + expectedWeights: []uint32{1, 1, 1}, + description: "Weights less than 0.5 round to 0, then default to 1; 0.5 and above round to 1", + }, + { + name: "large weights", + services: []model.Instance{ + {Ip: "192.168.1.1", Port: 8080, Weight: 100.0}, + {Ip: "192.168.1.2", Port: 8080, Weight: 50.5}, + }, + expectedWeights: []uint32{100, 51}, + description: "Large weights should be handled correctly", + }, + { + name: "mixed weights", + services: []model.Instance{ + {Ip: "192.168.1.1", Port: 8080, Weight: 0.0}, + {Ip: "192.168.1.2", Port: 8080, Weight: 1.5}, + {Ip: "192.168.1.3", Port: 8080, Weight: -5.0}, + {Ip: "192.168.1.4", Port: 8080, Weight: 10.7}, + }, + expectedWeights: []uint32{1, 2, 1, 11}, + description: "Mixed zero, negative, and fractional weights", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + se := w.generateServiceEntry("test-host", tc.services) + + if se == nil { + t.Fatal("generateServiceEntry returned nil") + } + + if len(se.Endpoints) != len(tc.expectedWeights) { + t.Fatalf("expected %d endpoints, got %d", len(tc.expectedWeights), len(se.Endpoints)) + } + + for i, endpoint := range se.Endpoints { + if endpoint.Weight != tc.expectedWeights[i] { + t.Errorf("endpoint[%d]: expected weight %d, got %d (original weight: %f) - %s", + i, tc.expectedWeights[i], endpoint.Weight, tc.services[i].Weight, tc.description) + } + } + }) + } +} + +func Test_generateServiceEntry_WeightFieldSet(t *testing.T) { + w := &watcher{} + + services := []model.Instance{ + {Ip: "192.168.1.1", Port: 8080, Weight: 5.0, Metadata: map[string]string{"zone": "a"}}, + } + + se := w.generateServiceEntry("test-host", services) + + if se == nil { + t.Fatal("generateServiceEntry returned nil") + } + + if len(se.Endpoints) != 1 { + t.Fatalf("expected 1 endpoint, got %d", len(se.Endpoints)) + } + + endpoint := se.Endpoints[0] + + // Verify all fields are set correctly + if endpoint.Address != "192.168.1.1" { + t.Errorf("expected address 192.168.1.1, got %s", endpoint.Address) + } + + if endpoint.Weight != 5 { + t.Errorf("expected weight 5, got %d", endpoint.Weight) + } + + if endpoint.Labels == nil || endpoint.Labels["zone"] != "a" { + t.Errorf("expected labels with zone=a, got %v", endpoint.Labels) + } + + if endpoint.Ports == nil { + t.Error("expected ports to be set") + } +} + +func Test_generateServiceEntry_EmptyServices(t *testing.T) { + w := &watcher{} + + se := w.generateServiceEntry("test-host", []model.Instance{}) + + if se == nil { + t.Fatal("generateServiceEntry returned nil") + } + + if len(se.Endpoints) != 0 { + t.Errorf("expected 0 endpoints for empty services, got %d", len(se.Endpoints)) + } +} + +func Test_generateServiceEntry_DNSResolution(t *testing.T) { + w := &watcher{} + + services := []model.Instance{ + {Ip: "example.com", Port: 8080, Weight: 5.0}, + } + + se := w.generateServiceEntry("test-host", services) + + if se == nil { + t.Fatal("generateServiceEntry returned nil") + } + + if se.Resolution != v1alpha3.ServiceEntry_DNS { + t.Errorf("expected DNS resolution for domain name, got %v", se.Resolution) + } + + if len(se.Endpoints) != 1 { + t.Fatalf("expected 1 endpoint, got %d", len(se.Endpoints)) + } + + if se.Endpoints[0].Weight != 5 { + t.Errorf("expected weight 5 for DNS endpoint, got %d", se.Endpoints[0].Weight) + } +} diff --git a/registry/nacos/watcher.go b/registry/nacos/watcher.go index 2092afee2..de44a149c 100644 --- a/registry/nacos/watcher.go +++ b/registry/nacos/watcher.go @@ -15,6 +15,7 @@ package nacos import ( + "math" "strconv" "strings" "sync" @@ -352,10 +353,19 @@ func (w *watcher) generateServiceEntry(host string, services []model.SubscribeSe portList = append(portList, port) } } + // Calculate weight from Nacos instance + // Nacos weight is float64, need to convert to uint32 for Istio + // Use math.Round to preserve fractional weights (e.g., 0.5, 1.5) + // If weight is 0 or negative, use default weight 1 + weight := uint32(1) + if service.Weight > 0 { + weight = uint32(math.Round(service.Weight)) + } endpoint := v1alpha3.WorkloadEntry{ Address: service.Ip, Ports: map[string]uint32{port.Protocol: port.Number}, Labels: service.Metadata, + Weight: weight, } endpoints = append(endpoints, &endpoint) } diff --git a/registry/nacos/watcher_test.go b/registry/nacos/watcher_test.go new file mode 100644 index 000000000..88db69e43 --- /dev/null +++ b/registry/nacos/watcher_test.go @@ -0,0 +1,173 @@ +// Copyright (c) 2022 Alibaba Group Holding Ltd. +// +// Licensed 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 nacos + +import ( + "testing" + + "github.com/nacos-group/nacos-sdk-go/model" +) + +func Test_generateServiceEntry_Weight(t *testing.T) { + w := &watcher{} + + testCases := []struct { + name string + services []model.SubscribeService + expectedWeights []uint32 + description string + }{ + { + name: "normal integer weights", + services: []model.SubscribeService{ + {Ip: "192.168.1.1", Port: 8080, Weight: 5.0}, + {Ip: "192.168.1.2", Port: 8080, Weight: 3.0}, + {Ip: "192.168.1.3", Port: 8080, Weight: 2.0}, + }, + expectedWeights: []uint32{5, 3, 2}, + description: "Integer weights should be converted correctly", + }, + { + name: "fractional weights with rounding", + services: []model.SubscribeService{ + {Ip: "192.168.1.1", Port: 8080, Weight: 5.4}, + {Ip: "192.168.1.2", Port: 8080, Weight: 3.5}, + {Ip: "192.168.1.3", Port: 8080, Weight: 2.6}, + }, + expectedWeights: []uint32{5, 4, 3}, + description: "Fractional weights should be rounded to nearest integer", + }, + { + name: "zero weight defaults to 1", + services: []model.SubscribeService{ + {Ip: "192.168.1.1", Port: 8080, Weight: 0.0}, + {Ip: "192.168.1.2", Port: 8080, Weight: 5.0}, + }, + expectedWeights: []uint32{1, 5}, + description: "Zero weight should default to 1", + }, + { + name: "negative weight defaults to 1", + services: []model.SubscribeService{ + {Ip: "192.168.1.1", Port: 8080, Weight: -1.0}, + {Ip: "192.168.1.2", Port: 8080, Weight: 3.0}, + }, + expectedWeights: []uint32{1, 3}, + description: "Negative weight should default to 1", + }, + { + name: "very small fractional weight rounds to 0 then defaults to 1", + services: []model.SubscribeService{ + {Ip: "192.168.1.1", Port: 8080, Weight: 0.4}, + {Ip: "192.168.1.2", Port: 8080, Weight: 0.5}, + {Ip: "192.168.1.3", Port: 8080, Weight: 0.6}, + }, + expectedWeights: []uint32{1, 1, 1}, + description: "Weights less than 0.5 round to 0, then default to 1; 0.5 and above round to 1", + }, + { + name: "large weights", + services: []model.SubscribeService{ + {Ip: "192.168.1.1", Port: 8080, Weight: 100.0}, + {Ip: "192.168.1.2", Port: 8080, Weight: 50.5}, + }, + expectedWeights: []uint32{100, 51}, + description: "Large weights should be handled correctly", + }, + { + name: "mixed weights", + services: []model.SubscribeService{ + {Ip: "192.168.1.1", Port: 8080, Weight: 0.0}, + {Ip: "192.168.1.2", Port: 8080, Weight: 1.5}, + {Ip: "192.168.1.3", Port: 8080, Weight: -5.0}, + {Ip: "192.168.1.4", Port: 8080, Weight: 10.7}, + }, + expectedWeights: []uint32{1, 2, 1, 11}, + description: "Mixed zero, negative, and fractional weights", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + se := w.generateServiceEntry("test-host", tc.services) + + if se == nil { + t.Fatal("generateServiceEntry returned nil") + } + + if len(se.Endpoints) != len(tc.expectedWeights) { + t.Fatalf("expected %d endpoints, got %d", len(tc.expectedWeights), len(se.Endpoints)) + } + + for i, endpoint := range se.Endpoints { + if endpoint.Weight != tc.expectedWeights[i] { + t.Errorf("endpoint[%d]: expected weight %d, got %d (original weight: %f) - %s", + i, tc.expectedWeights[i], endpoint.Weight, tc.services[i].Weight, tc.description) + } + } + }) + } +} + +func Test_generateServiceEntry_WeightFieldSet(t *testing.T) { + w := &watcher{} + + services := []model.SubscribeService{ + {Ip: "192.168.1.1", Port: 8080, Weight: 5.0, Metadata: map[string]string{"zone": "a"}}, + } + + se := w.generateServiceEntry("test-host", services) + + if se == nil { + t.Fatal("generateServiceEntry returned nil") + } + + if len(se.Endpoints) != 1 { + t.Fatalf("expected 1 endpoint, got %d", len(se.Endpoints)) + } + + endpoint := se.Endpoints[0] + + // Verify all fields are set correctly + if endpoint.Address != "192.168.1.1" { + t.Errorf("expected address 192.168.1.1, got %s", endpoint.Address) + } + + if endpoint.Weight != 5 { + t.Errorf("expected weight 5, got %d", endpoint.Weight) + } + + if endpoint.Labels == nil || endpoint.Labels["zone"] != "a" { + t.Errorf("expected labels with zone=a, got %v", endpoint.Labels) + } + + if endpoint.Ports == nil { + t.Error("expected ports to be set") + } +} + +func Test_generateServiceEntry_EmptyServices(t *testing.T) { + w := &watcher{} + + se := w.generateServiceEntry("test-host", []model.SubscribeService{}) + + if se == nil { + t.Fatal("generateServiceEntry returned nil") + } + + if len(se.Endpoints) != 0 { + t.Errorf("expected 0 endpoints for empty services, got %d", len(se.Endpoints)) + } +}