From 16c7e382479cea1b826694b69e9a2348355754d4 Mon Sep 17 00:00:00 2001 From: Zhiying Lin <54013513+zhiying-lin@users.noreply.github.com> Date: Tue, 31 Dec 2024 14:57:54 +0800 Subject: [PATCH] interface: update traffic manager APIs to support splitting weight behind serviceImport (#233) interfaces: update traffic manager APIs to support splitting weight behind serviceImport --- api/v1alpha1/internalserviceexport_types.go | 2 + api/v1alpha1/serviceexport_types.go | 24 +++++++ api/v1alpha1/trafficmanagerbackend_types.go | 38 ++++++++--- api/v1alpha1/trafficmanagerprofile_types.go | 4 ++ api/v1alpha1/zz_generated.deepcopy.go | 66 +++++++++++++++++-- ...leet.azure.com_internalserviceexports.yaml | 20 ++++++ ...orking.fleet.azure.com_serviceexports.yaml | 20 ++++++ ...leet.azure.com_trafficmanagerbackends.yaml | 34 +++++++--- ...leet.azure.com_trafficmanagerprofiles.yaml | 5 ++ .../hub/trafficmanagerbackend/controller.go | 16 +++-- .../controller_integration_test.go | 30 ++++++--- .../trafficmanager/validator/backend.go | 2 +- 12 files changed, 222 insertions(+), 39 deletions(-) diff --git a/api/v1alpha1/internalserviceexport_types.go b/api/v1alpha1/internalserviceexport_types.go index 39993590..6a5e967a 100644 --- a/api/v1alpha1/internalserviceexport_types.go +++ b/api/v1alpha1/internalserviceexport_types.go @@ -13,6 +13,8 @@ import ( // InternalServiceExportSpec specifies the spec of an exported Service; at this stage only the ports of an // exported Service are sync'd. type InternalServiceExportSpec struct { + // ServiceExportSpec contains the spec of the ServiceExport. + ServiceExportSpec *ServiceExportSpec `json:"serviceExportSpec,omitempty"` // A list of ports exposed by the exported Service. // +listType=atomic Ports []ServicePort `json:"ports"` diff --git a/api/v1alpha1/serviceexport_types.go b/api/v1alpha1/serviceexport_types.go index a3f90a92..497fccae 100644 --- a/api/v1alpha1/serviceexport_types.go +++ b/api/v1alpha1/serviceexport_types.go @@ -9,6 +9,27 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +const ( + // ExportedAnnotationsWeight is the key for the weight annotation as an exportedAnnotation on a ServiceExport. + ExportedAnnotationsWeight = "weight" +) + +// ServiceExportSpec describes an exported service extra information. +type ServiceExportSpec struct { + // exportedLabels describes the labels exported. + // +optional + ExportedLabels map[string]string `json:"exportedLabels,omitempty"` + // exportedAnnotations describes the annotations exported. + // Possible Annotations: "weight". + // "weight" specifies the proportion of requests forwarded to the cluster within a serviceImport. + // This is computed as weight/(sum of all weights in the serviceImport). + // If weight is set to 0, no traffic should be forwarded for this entry. If unspecified, weight defaults to 1. + // The value should be in the range [0, 1000]. + // + // +optional + ExportedAnnotations map[string]string `json:"exportedAnnotations,omitempty"` +} + // ServiceExportConditionType identifies a specific condition on a ServiceExport. type ServiceExportConditionType string @@ -45,6 +66,9 @@ type ServiceExport struct { metav1.TypeMeta `json:",inline"` // +optional metav1.ObjectMeta `json:"metadata,omitempty"` + // spec defines the behavior of a ServiceExport. + // +optional + Spec ServiceExportSpec `json:"spec,omitempty"` // +optional Status ServiceExportStatus `json:"status,omitempty"` } diff --git a/api/v1alpha1/trafficmanagerbackend_types.go b/api/v1alpha1/trafficmanagerbackend_types.go index e241363b..940da5c2 100644 --- a/api/v1alpha1/trafficmanagerbackend_types.go +++ b/api/v1alpha1/trafficmanagerbackend_types.go @@ -43,13 +43,20 @@ type TrafficManagerBackendSpec struct { Backend TrafficManagerBackendRef `json:"backend"` // The total weight of endpoints behind the serviceImport when using the 'Weighted' traffic routing method. - // Possible values are from 1 to 1000. - // By default, the routing method is 'Weighted', so that it is required for now. + // Possible values are from 0 to 1000. + // By default, the routing method is 'Weighted'. + // If weight is set to 0, all the endpoints behind the serviceImport will be removed from the profile. + // The actual weight of each endpoint is the ceiling value of a number computed as weight/(sum of all weights behind the serviceImport) + // * weight of serviceExport. + // For example, if the weight is 500 and there are two serviceExports from cluster-1 (weight: 100) and cluster-2 (weight: 200) + // behind serviceImport. + // As a result, two endpoints will be created. + // The weight of endpoint from cluster-1 is 100/(100+200)*500 = 167, and the weight of cluster-2 is 200/(100+200)*500 = 334. + // There may be slight deviations from the exact proportions defined in the serviceExports due to ceiling calculations. // +optional - // +kubebuilder:validation:Minimum=1 + // +kubebuilder:validation:Minimum=0 // +kubebuilder:validation:Maximum=1000 - // For example, if there are two clusters exporting the service via public ip, each public ip will be configured - // as "Weight"/2. + // +kubebuilder:default=1 Weight *int64 `json:"weight,omitempty"` } @@ -75,8 +82,12 @@ type TrafficManagerEndpointStatus struct { // +required Name string `json:"name"` + // ResourceID is the fully qualified Azure resource Id for the resource. + // Ex - /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Network/trafficManagerProfiles/{profileName}/azureEndpoints/{name} + ResourceID string `json:"resourceID,omitempty"` + // The weight of this endpoint when using the 'Weighted' traffic routing method. - // Possible values are from 1 to 1000. + // Possible values are from 0 to 1000. // +optional Weight *int64 `json:"weight,omitempty"` @@ -84,9 +95,20 @@ type TrafficManagerEndpointStatus struct { // +optional Target *string `json:"target,omitempty"` - // Cluster is where the endpoint is exported from. + // From is where the endpoint is exported from. + // +optional + From *FromCluster `json:"from,omitempty"` +} + +// FromCluster contains service configuration mapped to a specific source cluster. +type FromCluster struct { + // ClusterStatus describes the source cluster status. + ClusterStatus `json:",inline"` + + // Weight defines the weight configured in the serviceExport from the source cluster. + // Possible values are from 0 to 1000. // +optional - Cluster *ClusterStatus `json:"cluster,omitempty"` + Weight *int64 `json:"weight,omitempty"` } type TrafficManagerBackendStatus struct { diff --git a/api/v1alpha1/trafficmanagerprofile_types.go b/api/v1alpha1/trafficmanagerprofile_types.go index 52e9f175..1870dff5 100644 --- a/api/v1alpha1/trafficmanagerprofile_types.go +++ b/api/v1alpha1/trafficmanagerprofile_types.go @@ -102,6 +102,10 @@ type TrafficManagerProfileStatus struct { // +optional DNSName *string `json:"dnsName,omitempty"` + // ResourceID is the fully qualified Azure resource Id for the resource. + // Ex - /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Network/trafficManagerProfiles/{resourceName} + ResourceID string `json:"resourceID,omitempty"` + // Current profile status. // +optional // +patchMergeKey=type diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index f96e908b..e39c17a0 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -11,7 +11,7 @@ package v1alpha1 import ( corev1 "k8s.io/api/core/v1" - v1 "k8s.io/api/discovery/v1" + "k8s.io/api/discovery/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) @@ -214,6 +214,27 @@ func (in *ExportedObjectReference) DeepCopy() *ExportedObjectReference { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FromCluster) DeepCopyInto(out *FromCluster) { + *out = *in + out.ClusterStatus = in.ClusterStatus + if in.Weight != nil { + in, out := &in.Weight, &out.Weight + *out = new(int64) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FromCluster. +func (in *FromCluster) DeepCopy() *FromCluster { + if in == nil { + return nil + } + out := new(FromCluster) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *InternalServiceExport) DeepCopyInto(out *InternalServiceExport) { *out = *in @@ -276,6 +297,11 @@ func (in *InternalServiceExportList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *InternalServiceExportSpec) DeepCopyInto(out *InternalServiceExportSpec) { *out = *in + if in.ServiceExportSpec != nil { + in, out := &in.ServiceExportSpec, &out.ServiceExportSpec + *out = new(ServiceExportSpec) + (*in).DeepCopyInto(*out) + } if in.Ports != nil { in, out := &in.Ports, &out.Ports *out = make([]ServicePort, len(*in)) @@ -561,6 +587,7 @@ func (in *ServiceExport) DeepCopyInto(out *ServiceExport) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) } @@ -614,6 +641,35 @@ func (in *ServiceExportList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceExportSpec) DeepCopyInto(out *ServiceExportSpec) { + *out = *in + if in.ExportedLabels != nil { + in, out := &in.ExportedLabels, &out.ExportedLabels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.ExportedAnnotations != nil { + in, out := &in.ExportedAnnotations, &out.ExportedAnnotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceExportSpec. +func (in *ServiceExportSpec) DeepCopy() *ServiceExportSpec { + if in == nil { + return nil + } + out := new(ServiceExportSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServiceExportStatus) DeepCopyInto(out *ServiceExportStatus) { *out = *in @@ -927,10 +983,10 @@ func (in *TrafficManagerEndpointStatus) DeepCopyInto(out *TrafficManagerEndpoint *out = new(string) **out = **in } - if in.Cluster != nil { - in, out := &in.Cluster, &out.Cluster - *out = new(ClusterStatus) - **out = **in + if in.From != nil { + in, out := &in.From, &out.From + *out = new(FromCluster) + (*in).DeepCopyInto(*out) } } diff --git a/config/crd/bases/networking.fleet.azure.com_internalserviceexports.yaml b/config/crd/bases/networking.fleet.azure.com_internalserviceexports.yaml index 9123b6b6..71e2a399 100644 --- a/config/crd/bases/networking.fleet.azure.com_internalserviceexports.yaml +++ b/config/crd/bases/networking.fleet.azure.com_internalserviceexports.yaml @@ -115,6 +115,26 @@ spec: type: object type: array x-kubernetes-list-type: atomic + serviceExportSpec: + description: ServiceExportSpec contains the spec of the ServiceExport. + properties: + exportedAnnotations: + additionalProperties: + type: string + description: |- + exportedAnnotations describes the annotations exported. + Possible Annotations: "weight". + "weight" specifies the proportion of requests forwarded to the cluster within a serviceImport. + This is computed as weight/(sum of all weights in the serviceImport). + If weight is set to 0, no traffic should be forwarded for this entry. If unspecified, weight defaults to 1. + The value should be in the range [0, 1000]. + type: object + exportedLabels: + additionalProperties: + type: string + description: exportedLabels describes the labels exported. + type: object + type: object serviceReference: description: The reference to the source Service. properties: diff --git a/config/crd/bases/networking.fleet.azure.com_serviceexports.yaml b/config/crd/bases/networking.fleet.azure.com_serviceexports.yaml index a361fc92..ef5ed669 100644 --- a/config/crd/bases/networking.fleet.azure.com_serviceexports.yaml +++ b/config/crd/bases/networking.fleet.azure.com_serviceexports.yaml @@ -51,6 +51,26 @@ spec: type: string metadata: type: object + spec: + description: spec defines the behavior of a ServiceExport. + properties: + exportedAnnotations: + additionalProperties: + type: string + description: |- + exportedAnnotations describes the annotations exported. + Possible Annotations: "weight". + "weight" specifies the proportion of requests forwarded to the cluster within a serviceImport. + This is computed as weight/(sum of all weights in the serviceImport). + If weight is set to 0, no traffic should be forwarded for this entry. If unspecified, weight defaults to 1. + The value should be in the range [0, 1000]. + type: object + exportedLabels: + additionalProperties: + type: string + description: exportedLabels describes the labels exported. + type: object + type: object status: description: ServiceExportStatus contains the current status of an export. properties: diff --git a/config/crd/bases/networking.fleet.azure.com_trafficmanagerbackends.yaml b/config/crd/bases/networking.fleet.azure.com_trafficmanagerbackends.yaml index 7433e7b8..604911ab 100644 --- a/config/crd/bases/networking.fleet.azure.com_trafficmanagerbackends.yaml +++ b/config/crd/bases/networking.fleet.azure.com_trafficmanagerbackends.yaml @@ -87,15 +87,22 @@ spec: - message: spec.profile is immutable rule: self == oldSelf weight: + default: 1 description: |- The total weight of endpoints behind the serviceImport when using the 'Weighted' traffic routing method. - Possible values are from 1 to 1000. - By default, the routing method is 'Weighted', so that it is required for now. - For example, if there are two clusters exporting the service via public ip, each public ip will be configured - as "Weight"/2. + Possible values are from 0 to 1000. + By default, the routing method is 'Weighted'. + If weight is set to 0, all the endpoints behind the serviceImport will be removed from the profile. + The actual weight of each endpoint is the ceiling value of a number computed as weight/(sum of all weights behind the serviceImport) + * weight of serviceExport. + For example, if the weight is 500 and there are two serviceExports from cluster-1 (weight: 100) and cluster-2 (weight: 200) + behind serviceImport. + As a result, two endpoints will be created. + The weight of endpoint from cluster-1 is 100/(100+200)*500 = 167, and the weight of cluster-2 is 200/(100+200)*500 = 334. + There may be slight deviations from the exact proportions defined in the serviceExports due to ceiling calculations. format: int64 maximum: 1000 - minimum: 1 + minimum: 0 type: integer required: - backend @@ -172,19 +179,30 @@ spec: TrafficManagerEndpointStatus is the status of Azure Traffic Manager endpoint which is successfully accepted under the traffic manager Profile. properties: - cluster: - description: Cluster is where the endpoint is exported from. + from: + description: From is where the endpoint is exported from. properties: cluster: description: cluster is the name of the exporting cluster. Must be a valid RFC-1123 DNS label. type: string + weight: + description: |- + Weight defines the weight configured in the serviceExport from the source cluster. + Possible values are from 0 to 1000. + format: int64 + type: integer required: - cluster type: object name: description: Name of the endpoint. type: string + resourceID: + description: |- + ResourceID is the fully qualified Azure resource Id for the resource. + Ex - /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Network/trafficManagerProfiles/{profileName}/azureEndpoints/{name} + type: string target: description: The fully-qualified DNS name or IP address of the endpoint. @@ -192,7 +210,7 @@ spec: weight: description: |- The weight of this endpoint when using the 'Weighted' traffic routing method. - Possible values are from 1 to 1000. + Possible values are from 0 to 1000. format: int64 type: integer required: diff --git a/config/crd/bases/networking.fleet.azure.com_trafficmanagerprofiles.yaml b/config/crd/bases/networking.fleet.azure.com_trafficmanagerprofiles.yaml index 0a157cea..c5155d81 100644 --- a/config/crd/bases/networking.fleet.azure.com_trafficmanagerprofiles.yaml +++ b/config/crd/bases/networking.fleet.azure.com_trafficmanagerprofiles.yaml @@ -182,6 +182,11 @@ spec: domain name (FQDN) of the profile. For example, "-.trafficmanager.net" type: string + resourceID: + description: |- + ResourceID is the fully qualified Azure resource Id for the resource. + Ex - /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Network/trafficManagerProfiles/{resourceName} + type: string type: object required: - spec diff --git a/pkg/controllers/hub/trafficmanagerbackend/controller.go b/pkg/controllers/hub/trafficmanagerbackend/controller.go index 8a4248ba..d402e8ae 100644 --- a/pkg/controllers/hub/trafficmanagerbackend/controller.go +++ b/pkg/controllers/hub/trafficmanagerbackend/controller.go @@ -513,12 +513,14 @@ func generateAzureTrafficManagerEndpoint(backend *fleetnetv1alpha1.TrafficManage } } -func buildAcceptedEndpointStatus(endpoint *armtrafficmanager.Endpoint, cluster *fleetnetv1alpha1.ClusterStatus) fleetnetv1alpha1.TrafficManagerEndpointStatus { +func buildAcceptedEndpointStatus(endpoint *armtrafficmanager.Endpoint, cluster fleetnetv1alpha1.ClusterStatus) fleetnetv1alpha1.TrafficManagerEndpointStatus { return fleetnetv1alpha1.TrafficManagerEndpointStatus{ - Name: strings.ToLower(*endpoint.Name), // name is case-insensitive - Target: endpoint.Properties.Target, - Weight: endpoint.Properties.Weight, - Cluster: cluster, + Name: strings.ToLower(*endpoint.Name), // name is case-insensitive + Target: endpoint.Properties.Target, + Weight: endpoint.Properties.Weight, + From: &fleetnetv1alpha1.FromCluster{ + ClusterStatus: cluster, + }, } } @@ -575,7 +577,7 @@ func (r *Reconciler) updateTrafficManagerEndpointsAndUpdateStatusIfUnknown(ctx c if equalAzureTrafficManagerEndpoint(*endpoint, desired.Endpoint) { klog.V(2).InfoS("Skipping updating the existing Traffic Manager endpoint", "trafficManagerBackend", backendKObj, "atmProfile", profile.Name, "atmEndpoint", endpointName) delete(desiredEndpoints, endpointName) // no need to update the existing endpoint - acceptedEndpoints = append(acceptedEndpoints, buildAcceptedEndpointStatus(endpoint, &desired.Cluster)) + acceptedEndpoints = append(acceptedEndpoints, buildAcceptedEndpointStatus(endpoint, desired.Cluster)) continue } // no need to update the endpoint if it's the same } @@ -604,7 +606,7 @@ func (r *Reconciler) updateTrafficManagerEndpointsAndUpdateStatusIfUnknown(ctx c return nil, nil, updateErr } klog.V(2).InfoS("Created or updated Traffic Manager endpoint", "trafficManagerBackend", backendKObj, "atmProfile", profile.Name, "atmEndpoint", endpointName) - acceptedEndpoints = append(acceptedEndpoints, buildAcceptedEndpointStatus(&res.Endpoint, &endpoint.Cluster)) + acceptedEndpoints = append(acceptedEndpoints, buildAcceptedEndpointStatus(&res.Endpoint, endpoint.Cluster)) } klog.V(2).InfoS("Successfully updated the Traffic Manager endpoints", "trafficManagerBackend", backendKObj, "atmProfile", profile.Name, "numberOfAcceptedEndpoints", len(acceptedEndpoints), "numberOfBadEndpoints", len(badEndpointsError)) return acceptedEndpoints, badEndpointsError, nil diff --git a/pkg/controllers/hub/trafficmanagerbackend/controller_integration_test.go b/pkg/controllers/hub/trafficmanagerbackend/controller_integration_test.go index 657cf8bd..713810ac 100644 --- a/pkg/controllers/hub/trafficmanagerbackend/controller_integration_test.go +++ b/pkg/controllers/hub/trafficmanagerbackend/controller_integration_test.go @@ -764,8 +764,10 @@ var _ = Describe("Test TrafficManagerBackend Controller", func() { Endpoints: []fleetnetv1alpha1.TrafficManagerEndpointStatus{ { Name: fmt.Sprintf(AzureResourceEndpointNameFormat, backendName+"#", serviceName, memberClusterNames[0]), - Cluster: &fleetnetv1alpha1.ClusterStatus{ - Cluster: memberClusterNames[0], + From: &fleetnetv1alpha1.FromCluster{ + ClusterStatus: fleetnetv1alpha1.ClusterStatus{ + Cluster: memberClusterNames[0], + }, }, Weight: ptr.To(fakeprovider.Weight), // populate the weight using atm endpoint Target: ptr.To(fakeprovider.ValidEndpointTarget), @@ -804,16 +806,20 @@ var _ = Describe("Test TrafficManagerBackend Controller", func() { Endpoints: []fleetnetv1alpha1.TrafficManagerEndpointStatus{ { Name: fmt.Sprintf(AzureResourceEndpointNameFormat, backendName+"#", serviceName, memberClusterNames[0]), - Cluster: &fleetnetv1alpha1.ClusterStatus{ - Cluster: memberClusterNames[0], + From: &fleetnetv1alpha1.FromCluster{ + ClusterStatus: fleetnetv1alpha1.ClusterStatus{ + Cluster: memberClusterNames[0], + }, }, Weight: ptr.To(fakeprovider.Weight), // populate the weight using atm endpoint Target: ptr.To(fakeprovider.ValidEndpointTarget), }, { Name: fmt.Sprintf(AzureResourceEndpointNameFormat, backendName+"#", serviceName, memberClusterNames[3]), - Cluster: &fleetnetv1alpha1.ClusterStatus{ - Cluster: memberClusterNames[3], + From: &fleetnetv1alpha1.FromCluster{ + ClusterStatus: fleetnetv1alpha1.ClusterStatus{ + Cluster: memberClusterNames[3], + }, }, Weight: ptr.To(fakeprovider.Weight), // populate the weight using atm endpoint Target: ptr.To(fakeprovider.ValidEndpointTarget), @@ -852,8 +858,10 @@ var _ = Describe("Test TrafficManagerBackend Controller", func() { Endpoints: []fleetnetv1alpha1.TrafficManagerEndpointStatus{ { Name: fmt.Sprintf(AzureResourceEndpointNameFormat, backendName+"#", serviceName, memberClusterNames[0]), - Cluster: &fleetnetv1alpha1.ClusterStatus{ - Cluster: memberClusterNames[0], + From: &fleetnetv1alpha1.FromCluster{ + ClusterStatus: fleetnetv1alpha1.ClusterStatus{ + Cluster: memberClusterNames[0], + }, }, Weight: ptr.To(fakeprovider.Weight), // populate the weight using atm endpoint Target: ptr.To(fakeprovider.ValidEndpointTarget), @@ -949,8 +957,10 @@ var _ = Describe("Test TrafficManagerBackend Controller", func() { Endpoints: []fleetnetv1alpha1.TrafficManagerEndpointStatus{ { Name: fmt.Sprintf(AzureResourceEndpointNameFormat, backendName+"#", serviceName, memberClusterNames[0]), - Cluster: &fleetnetv1alpha1.ClusterStatus{ - Cluster: memberClusterNames[0], + From: &fleetnetv1alpha1.FromCluster{ + ClusterStatus: fleetnetv1alpha1.ClusterStatus{ + Cluster: memberClusterNames[0], + }, }, Weight: ptr.To(fakeprovider.Weight), // populate the weight using atm endpoint Target: ptr.To(fakeprovider.ValidEndpointTarget), diff --git a/test/common/trafficmanager/validator/backend.go b/test/common/trafficmanager/validator/backend.go index cdddfa70..ae8dda21 100644 --- a/test/common/trafficmanager/validator/backend.go +++ b/test/common/trafficmanager/validator/backend.go @@ -27,7 +27,7 @@ var ( commonCmpOptions, cmpopts.IgnoreFields(fleetnetv1alpha1.TrafficManagerBackend{}, "TypeMeta"), cmpopts.SortSlices(func(s1, s2 fleetnetv1alpha1.TrafficManagerEndpointStatus) bool { - return s1.Cluster.Cluster < s2.Cluster.Cluster + return s1.From.Cluster < s2.From.Cluster }), cmpConditionOptions, }