mirror of
https://github.com/alibaba/higress.git
synced 2026-05-27 22:27:29 +08:00
feat: Map Nacos instance weights to Istio WorkloadEntry weights in watchers (#3342)
Co-authored-by: EricaLiu <30773688+Erica177@users.noreply.github.com>
This commit is contained in:
@@ -17,6 +17,7 @@ package v2
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"net"
|
"net"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -556,10 +557,19 @@ func (w *watcher) generateServiceEntry(host string, services []model.Instance) *
|
|||||||
if !isValidIP(service.Ip) {
|
if !isValidIP(service.Ip) {
|
||||||
isDnsService = true
|
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{
|
endpoint := &v1alpha3.WorkloadEntry{
|
||||||
Address: service.Ip,
|
Address: service.Ip,
|
||||||
Ports: map[string]uint32{port.Protocol: port.Number},
|
Ports: map[string]uint32{port.Protocol: port.Number},
|
||||||
Labels: service.Metadata,
|
Labels: service.Metadata,
|
||||||
|
Weight: weight,
|
||||||
}
|
}
|
||||||
endpoints = append(endpoints, endpoint)
|
endpoints = append(endpoints, endpoint)
|
||||||
}
|
}
|
||||||
|
|||||||
200
registry/nacos/v2/watcher_test.go
Normal file
200
registry/nacos/v2/watcher_test.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
package nacos
|
package nacos
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -352,10 +353,19 @@ func (w *watcher) generateServiceEntry(host string, services []model.SubscribeSe
|
|||||||
portList = append(portList, port)
|
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{
|
endpoint := v1alpha3.WorkloadEntry{
|
||||||
Address: service.Ip,
|
Address: service.Ip,
|
||||||
Ports: map[string]uint32{port.Protocol: port.Number},
|
Ports: map[string]uint32{port.Protocol: port.Number},
|
||||||
Labels: service.Metadata,
|
Labels: service.Metadata,
|
||||||
|
Weight: weight,
|
||||||
}
|
}
|
||||||
endpoints = append(endpoints, &endpoint)
|
endpoints = append(endpoints, &endpoint)
|
||||||
}
|
}
|
||||||
|
|||||||
173
registry/nacos/watcher_test.go
Normal file
173
registry/nacos/watcher_test.go
Normal file
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user